improcket/dist/improcket.min.js
Asraelite c73130e3ff General improvements
I forgot what I actually changed. It may not even be playable, I just want to get this up there.
2023-03-31 11:43:56 +02:00

3190 lines
69 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Connectivity = [top, right, bottom, left] (same TRouBLe as CSS)
const modules = {
capsule: {
small: {
name: 'Small Capsule',
tooltip: 'A small, simple capsule. Provides a small amount ' +
'of rotational power and storage space.',
type: 'capsule',
id: 'small',
mass: 2,
value: 5,
connectivity: [false, false, true, false],
capacity: 3,
rotation: 1,
computation: 100,
},
large: {
name: 'Large Capsule',
tooltip: 'A large, bulky capsule. Heavy, but has a lot of ' +
'rotational power and storage space.',
type: 'capsule',
id: 'large',
mass: 4,
value: 10,
connectivity: [false, false, true, false],
capacity: 5,
rotation: 4,
computation: 130,
},
advanced: {
name: 'Advanced Capsule',
tooltip: 'A futuristic rocket capsule. Has a lot of storage ' +
'space and rotational power while still being light.',
type: 'capsule',
id: 'advanced',
mass: 2,
value: 30,
connectivity: [false, false, true, false],
capacity: 4,
rotation: 5,
computation: 150,
}
},
fuel: {
small: {
name: 'Small Fuel Tank',
tooltip: 'A small flimsy tank with enough fuel for a short trip.',
type: 'fuel',
id: 'small',
mass: 1,
value: 1,
connectivity: [true, false, true, false],
fuelCapacity: 5
},
large: {
name: 'Large Fuel Tank',
tooltip: 'A large, heavy fuel tank capable of hold a lot of fuel.',
type: 'fuel',
id: 'large',
mass: 2,
value: 3,
connectivity: [true, false, true, false],
fuelCapacity: 15
},
advanced: {
name: 'Advanced Fuel Tank',
tooltip: 'A very efficient fuel storage tank.',
type: 'fuel',
id: 'advanced',
mass: 1,
value: 15,
connectivity: [true, false, true, false],
fuelCapacity: 12
}
},
thruster: {
light: {
name: 'Light Thruster',
tooltip: 'Powerful enough to lift a small ship, but not much ' +
'more.',
type: 'thruster',
id: 'light',
mass: 2,
value: 3,
connectivity: [true, false, false, false],
thrust: 10
},
heavy: {
name: 'Heavy Thruster',
tooltip: 'A powerful thruster for lifting heavy ships.',
type: 'thruster',
id: 'heavy',
mass: 5,
value: 4,
connectivity: [true, false, false, false],
thrust: 40
},
advanced: {
name: 'Advanced Thruster',
tooltip: 'A very efficient thruster using advanced technology. ',
type: 'thruster',
id: 'advanced',
mass: 2,
value: 15,
connectivity: [true, false, false, false],
thrust: 30
}
},
connector: {
xheavy: {
name: 'Heavy 4-way Connector',
tooltip: 'Can connect ship parts in any direction, but is quite ' +
'heavy',
type: 'connector',
id: 'xheavy',
mass: 2,
value: 3,
connectivity: [true, true, true, true]
},
advanced: {
name: 'Advanced 4-way Connector',
tooltip: 'Connects ship parts while remaining light.',
type: 'connector',
id: 'advanced',
mass: 1,
value: 15,
connectivity: [true, true, true, true]
}
},
gyroscope: {
small: {
name: 'Small Gyroscope',
tooltip: 'Provides a small amount of rotational power to the ship.',
type: 'gyroscope',
id: 'small',
mass: 3,
value: 7,
connectivity: [true, false, true, false],
rotation: 2
},
large: {
name: 'Large Gyroscope',
tooltip: 'Provides a lot of rotational force for large ships.',
type: 'gyroscope',
id: 'large',
mass: 5,
value: 15,
connectivity: [true, false, true, false],
rotation: 4
}
},
navigation: {
small: {
name: 'Navigational Computer',
tooltip: 'Increases the length of your predicted orbital path.',
type: 'navigation',
id: 'small',
mass: 1,
value: 10,
connectivity: [true, false, true, false],
computation: 150,
},
},
cargo: {
small: {
name: 'Cargo bay',
tooltip: 'A cargo bay for storing modules.',
type: 'cargo',
id: 'small',
mass: 1,
value: 5,
connectivity: [true, false, true, false],
capacity: 5
}
}
};
const images = {
title: {
logo: 'logo.png',
logoSvg: 'logo2.svg'
},
background: {
back: 'background_small.png',
middle: 'stars_back.png',
front: 'stars_front.png'
},
modules: {
capsule: {
small: 'modules/small_capsule.svg',
large: 'modules/large_capsule.svg',
advanced: 'modules/advanced_capsule.svg'
},
fuel: {
small: 'modules/small_fuel_tank.svg',
large: 'modules/large_fuel_tank.svg',
advanced: 'modules/advanced_fuel_tank.svg'
},
thruster: {
light: {
off: 'modules/light_thruster.svg',
on: 'modules/light_thruster_on.svg'
},
heavy: {
off: 'modules/heavy_thruster.svg',
on: 'modules/heavy_thruster_on.svg'
},
advanced: {
off: 'modules/advanced_thruster.svg',
on: 'modules/advanced_thruster_on.svg'
}
},
connector: {
xheavy: 'modules/xheavy_connector.svg',
advanced: 'modules/advanced_connector.svg',
},
cargo: {
small: 'modules/cargo_bay.svg'
},
gyroscope: {
small: 'modules/small_gyroscope.svg',
large: 'modules/large_gyroscope.svg'
},
navigation: {
small: 'modules/small_navigation.svg',
},
fuelcan: 'modules/fuelcan.svg'
},
celestials: {
green: {
'0': 'celestials/green_0.svg',
'1': 'celestials/green_1.svg',
'2': 'celestials/green_2.svg',
'3': 'celestials/rock_0.svg',
'4': 'celestials/rock_1.svg',
'5': 'celestials/lava_0.svg'
}
}
};
const audio = {
itemPickup: 'up1.mp3',
fuelPickup: 'blip2.mp3',
endEdit: 'release1.mp3',
newPlanet: 'up2.mp3',
engine: 'rocket2.ogg',
music: 'music2.mp3',
toss: 'thunk1.mp3',
crash: 'crash2.mp3',
pause: 'swoosh1.mp3'
};
async function init$9() {
let parse = (obj, convert) => Object.entries(obj).forEach(([k, v]) => {
typeof v == 'object' ? parse(v, convert) : obj[k] = convert(v);
});
let promises = [];
parse(images, str => {
let img = new Image();
img.src = 'img/' + str;
promises.push(new Promise((res) => {
img.addEventListener('load', res);
}));
return img;
});
parse(audio, str => {
let audio = new Howl({
src: ['audio/' + str]
});
promises.push(new Promise((res) => {
audio.once('load', res);
}));
return audio;
});
await Promise.all(promises);
}
class Module {
constructor(x, y, ship, {
name = 'Unnamed Module',
type = 'block',
id = 'unknown',
mass = 1,
fuelCapacity = 0,
...properties
}) {
this.x = x;
this.y = y;
this.name = name;
this.type = type;
this.mass = mass;
this.ship = ship;
this.id = id;
this.images = images.modules[this.type][this.id];
this.data = modules[this.type][this.id];
if (this.type == 'thruster') {
this.power = 0;
}
}
reset() {
if (this.type == 'thruster') {
this.power = 0;
}
}
get com() {
return this.ship.getWorldPoint(...this.localCom);
}
get currentImage() {
if (this.type == 'thruster') {
return this.power > 0 ? this.images.on : this.images.off;
} else {
return this.images;
}
}
get localCom() {
return [this.x + 0.5, this.y + 0.5];
}
}
/*
* Constants that do not change during gameplay.
* This can kind of be treated like a configuration file, I guess.
*
* All length units are relative to the size of a small ship module, which
* is always 1x1.
*/
// For fixing floating point rounding errors.
const EPSILON = 1e-8;
// Don't change these.
const TAU$1 = Math.PI * 2;
// Unit length of sector. May affect spawning a bit.
const SECTOR_SIZE = 512;
// G, G-boy, The big G, Mr. G, g's big brother, G-dog
const GRAVITATIONAL_CONSTANT = 0.002;
// Perspective constraints. Higher zoom value = closer.
const MIN_ZOOM = 1;
const MAX_ZOOM = 100;
const DEFAULT_ZOOM = 10;
const ZOOM_SPEED = 0.01;
// Ship landing. Angle in radians.
const TIP_ANGLE = 0.25;
const TIP_SPEED = 0.03;
const CRASH_SPEED = 0.7;
// Ship flight mechanics. Speed measured in units per tick.
const FUEL_BURN_RATE = 0.5;
const THRUST_POWER = 0.004;
const TURN_POWER = 0.07;
// Distance at which an orbited planet will not be considered a parent body.
const MAX_PARENT_CELESTIAL_DISTANCE = 120;
// Ship editing.
const EDIT_MARGIN = 2;
// Floating items.
const ENTITY_ROTATION_RATE = 0.01;
// World generation.
const PLANET_SPAWN_RATE = 100;
const ENTITY_SPAWN_RATE = 8;
const MIN_CELESTIAL_SPACING = 15;
const FUEL_CAN_AMOUNT = 10000;
const PLANET_IMAGE_SIZE = 250;
class Body {
constructor(x, y, mass) {
this.x = x;
this.y = y;
this.r = 0;
this.xvel = 0;
this.yvel = 0;
this.rvel = 0;
this.rfriction = 0.9;
this.mass = mass;
}
get com() {
return [this.x, this.y];
}
get pos() {
return [this.x, this.y];
}
get speed() {
return Math.sqrt(this.xvel ** 2 + this.yvel ** 2);
}
angleDifference(a, b) {
return Math.atan2(Math.sin(a - b), Math.cos(a - b));
}
normalizeAngle(a = this.r) {
return ((a % TAU$1) + TAU$1) % TAU$1;
}
getCelestialCollision(celestials) {
let result = false;
celestials.forEach(c => {
let dis = this.distanceTo(c);
if (dis < c.radius) result = c;
});
return result;
}
getWorldPoint(lx, ly, test) {
let [cx, cy] = this.localCom;
let [nx, ny] = this.rotateVector(lx - cx, ly - cy, this.r);
return [nx + this.x + cx, ny + this.y + cy];
}
getLocalPoint(wx, wy) {
let [lx, ly] = [wx - this.x, wy - this.y];
let [cx, cy] = this.localCom;
let [nx, ny] = this.rotateVector(lx, ly, -this.r);
return [nx - cx, ny - cy];
}
rotateVector(x, y, r = this.r) {
return [(x * Math.cos(-r) + y * Math.sin(-r)),
-(-y * Math.cos(-r) + x * Math.sin(-r))];
}
// TODO: Remove and replace uses with `rotateVector`.
relativeVector(x, y) {
return this.rotateVector(x, y, this.r);
}
tickMotion(speed = 1) {
this.x += this.xvel * speed;
this.y += this.yvel * speed;
this.r += this.rvel * speed;
this.rvel *= this.rfriction * speed;
}
tickGravity(bodies, speed = 1) {
for (let body of bodies) {
const distanceSquared = this.distanceToSquared(body);
if (distanceSquared > (1000 ** 2)) continue;
let force = body.mass / distanceSquared * GRAVITATIONAL_CONSTANT;
let [[ax, ay], [bx, by]] = [this.com, body.com];
let angle = Math.atan2(by - ay, bx - ax);
this.xvel += Math.cos(angle) * force * speed;
this.yvel += Math.sin(angle) * force * speed;
}
}
distanceTo(body) {
let [[ax, ay], [bx, by]] = [this.com, body.com];
return Math.max(Math.sqrt(((bx - ax) ** 2) +
((by - ay) ** 2)), 1);
}
distanceToSquared(body) {
let [[ax, ay], [bx, by]] = [this.com, body.com];
return Math.max(((bx - ax) ** 2) +
((by - ay) ** 2), 1);
}
angleTo(ax, ay, bx, by) {
return Math.atan2(by - ay, bx - ax);
}
approach(body, distance) {
let [[ax, ay], [bx, by]] = [this.com, body.com];
let angle = Math.atan2(by - ay, bx - ax);
this.x += Math.cos(angle) * distance;
this.y += Math.sin(angle) * distance;
}
halt() {
this.xvel = 0;
this.yvel = 0;
}
applyDirectionalForce(x, y, r) {
let [vx, vy] = this.rotateVector(x, y);
this.xvel += vx / this.mass;
this.yvel += vy / this.mass;
this.rvel += r / this.mass;
}
orbit(cel, altitude, angle = 0) {
this.gravity = true;
let speed = Math.sqrt(GRAVITATIONAL_CONSTANT * cel.mass / (altitude + cel.radius));
let [cx, cy] = cel.com;
let [comX, comY] = this.localCom;
let [dx, dy] = this.rotateVector(0, -(altitude + cel.radius), angle);
[this.xvel, this.yvel] = this.rotateVector(speed, 0, angle);
this.x = cx + dx - comX;
this.y = cy + dy - comY;
}
}
function createThrustExhaust(thruster) {
let ship = thruster.ship;
let [fx, fy] = ship.relativeVector(0, 0.2);
let [dx, dy] = ship.relativeVector((Math.random() - 0.5) * 0.5, 0.5);
let [cx, cy] = thruster.com;
particles.add(new Particle(cx + dx, cy + dy, {
xvel: ship.xvel + fx,
yvel: ship.yvel + fy,
color: '#f4c542',
lifetime: 5,
size: 0.07
}));
}
function createEndEditBurst(ship) {
for (let i = 0; i < 20; i++) {
particles.add(new Particle(...ship.poc, {
color: '#ccc',
lifetime: Math.random() * 30 + 25,
size: Math.random() * 0.3 + 0.05,
spray: 0.3,
gravity: true
}));
}
}
function createCrash(ship) {
for (let i = 0; i < ship.mass + 3; i++) {
particles.add(new Particle(...ship.com, {
color: '#f2e860',
lifetime: Math.random() * 50 + 40,
size: Math.random() * 0.2 + 0.2,
spray: 0.9,
gravity: true
}));
}
for (let i = 0; i < ship.mass + 3; i++) {
particles.add(new Particle(...ship.com, {
color: '#f75722',
lifetime: Math.random() * 50 + 40,
size: Math.random() * 0.2 + 0.2,
spray: 0.5,
gravity: true
}));
}
for (let i = 0; i < ship.mass * 2 + 3; i++) {
particles.add(new Particle(...ship.com, {
color: '#888',
lifetime: Math.random() * 30 + 55,
size: Math.random() * 0.5 + 0.4,
spray: 2,
friction: 0.9,
gravity: false
}));
}
}
function createPickupBurst(ship, point) {
for (let i = 0; i < 20; i++) {
particles.add(new Particle(...point, {
xvel: ship.xvel,
yvel: ship.yvel,
color: '#eae55d',
lifetime: Math.random() * 20 + 15,
friction: 0.95,
size: Math.random() * 0.2 + 0.05,
spray: 0.3
}));
}
}
function createItemToss(ship) {
particles.add(new Particle(...ship.com, {
xvel: ship.xvel,
yvel: ship.yvel,
color: '#a87234',
lifetime: 50,
size: 0.6,
spray: 0.4
}));
}
class Particle extends Body {
constructor(x, y, {
xvel = 0,
yvel = 0,
spray = 0.1,
fizzle = 0,
maxFizzle = fizzle * 2,
color = '#fff',
gravity = false,
lifetime = 50,
size = 0.1,
friction = 0.99
}) {
super(x, y, 0.1);
this.size = size;
this.xvel = xvel + (Math.random() - 0.5) * spray;
this.yvel = yvel + (Math.random() - 0.5) * spray;
this.friction = friction;
this.fizzle = fizzle;
this.maxFizzle = maxFizzle;
this.color = color;
this.gravity = gravity;
this.life = lifetime;
}
get com() {
return [this.x - this.size / 2, this.y - this.size / 2];
}
tick() {
if (this.life-- <= 0) {
particles.delete(this);
return;
}
this.tickMotion();
if (this.gravity) this.tickGravity(celestials);
this.xvel *= this.friction;
this.yvel *= this.friction;
this.x += (Math.random() - 0.5) * this.fizzle;
this.y += (Math.random() - 0.5) * this.fizzle;
if (this.fizzle < this.mazFizzle) this.fizzle *= 1.05;
}
}
const items = new Map();
let currentItem = null;
let capacity = 0;
let usedSpace = 0;
let onupdate = () => {};
function init$8() {
items.clear();
update();
}
function canToss() {
return !state.editing || message === 'inventory too full'
|| message === '';
}
function getTiles() {
let list = Array.from(items.values());
list.sort((a, b) => toId(...a.ident) < toId(...b.ident));
usedSpace = list.reduce((a, b) => a + b.quantity, 0);
return list;
}
function addItem(type, id) {
let mapId = toId(type, id);
if (!items.has(mapId)) items.set(mapId, new Tile$1(type, id));
let tile = items.get(mapId);
tile.increase();
update();
}
function removeItem(type, id) {
let mapId = toId(type, id);
if (!items.has(mapId)) return;
let tile = items.get(mapId);
tile.decrease();
if (tile.quantity == 0) {
items.delete(mapId);
currentItem = null;
}
if (canToss())
tossItem();
update();
validate();
}
function selectItem(type, id) {
currentItem = items.get(toId(type, id));
update();
}
function setOnupdate(func) {
onupdate = func;
}
function update() {
capacity = playerShip.cargoCapacity;
onupdate();
}
function toId(type, id) {
return `${type}.${id}`;
}
class Tile$1 {
constructor(type, id, q = 0) {
this.type = type;
this.id = id;
this.mapId = toId(type, id);
this.quantity = q;
this.image = images.modules[type][id];
this.data = modules[type][id];
if (type === 'thruster') this.image = this.image.off;
}
get textInfo() {
let text = this.data.name + '\n\n' + this.data.tooltip + '\n\n';
text += 'Mass: ' + this.data.mass + '\n';
if (this.type === 'thruster')
text += 'Power: ' + this.data.thrust + '\n';
if (this.type === 'fuel')
text += 'Fuel capacity: ' + this.data.fuelCapacity + '\n';
if (this.type === 'capsule') {
text += 'Rotational power: ' + this.data.rotation + '\n';
text += 'Cargo space: ' + this.data.capacity + '\n';
}
return text;
}
get ident() {
return [this.type, this.id];
}
increase() {
this.quantity++;
}
decrease() {
this.quantity = Math.max(0, this.quantity - 1);
}
}
const playing = new Map();
function play(name) {
audio[name].play();
}
function start(name) {
if (!playing.has(name))
playing.set(name, audio[name]);
let howl = playing.get(name);
howl.loop(true);
howl.play();
}
function stop(name) {
if (!playing.has(name)) return false;
let howl = playing.get(name);
if (howl.playing()) {
howl.stop();
return true;
}
return false;
}
function toggle(name) {
if (!stop(name)) start(name);
}
function volume(name, level) {
if (!playing.has(name)) return;
playing.get(name).volume(level);
}
let shipLanded = false;
let score = 0;
let gameOverReason = '';
let scoreText = '';
let notification = null;
let notLife = 0;
let landedPlanets = new Set();
function init$7() {
score = 0;
shipLanded = false;
}
function outOfFuel() {
gameOver('You ran out of fuel');
}
function playMusic() {
start('music');
volume('music', 0.4);
}
function notify(message, time = 80) {
if (notification === null) return;
notification.text = message;
notLife = time;
}
function tick$6() {
if (notification === null) return;
if ((notLife-- <= 0 || state.gameOver) && !state.paused)
notification.text = '';
}
function setNotificationElement(el) {
notification = el;
}
function startGame() {
init$7();
state.gameOver = false;
changeView('game');
perspective.reset();
perspective.focusPlayer();
}
function toMenu() {
changeView('menu');
}
function togglePause() {
console.log(state.paused);
state.paused = !state.paused;
play('pause');
if (state.paused) {
notify('Paused', 0);
}
}
function landShip(planet) {
shipLanded = true;
if (!landedPlanets.has(planet)) {
newPlanet(planet);
}
state.landed = true;
}
function howToPlay() {
changeView('instructions');
}
function newPlanet(planet) {
let value = (planet.radius * 2 + 50) | 0;
landedPlanets.add(planet);
play('newPlanet');
score += value;
notify('Landed on new planet: +' + value);
}
function launchShip() {
shipLanded = false;
state.landed = false;
}
function crash() {
gameOver('You crashed');
play('crash');
createCrash(playerShip);
}
function gameOver(reason) {
if (state.editing)
endEditing();
gameOverReason = reason;
state.gameOver = true;
state.inventory = false;
state.editing = false;
perspective.changeZoom(MIN_ZOOM, 0.99);
let massScore = playerShip.mass * 100;
let fuelScore = playerShip.fuel * 50 | 0;
let finalScore = massScore + fuelScore + score;
scoreText = 'Ship mass: ' +
' '.repeat(5 - ('' + massScore).length) + massScore + '\n' +
'Remaining fuel: ' +
' '.repeat(5 - ('' + fuelScore).length) + fuelScore + '\n' +
'Score: ' +
' '.repeat(5 - ('' + score).length) + score + '\n\n' +
'Final score: ' +
' '.repeat(5 - ('' + finalScore).length) + finalScore;
}
function toggleEdit() {
if (state.editing) {
endEditing();
return;
}
state.editing = true;
state.inventory = true;
init$5();
}
function toggleTrace$1() {
let trace = toggleTrace();
notify('Path prediction: ' + (trace ? 'on' : 'off'));
}
function toggleMarkers$1() {
let markers = toggleMarkers();
notify('Item markers: ' + (markers ? 'on' : 'off'));
}
function cycleRotationMode$1() {
let message = {
parent: 'planet',
local: 'ship',
universe: 'universe'
}[cycleRotationMode()];
notify('Rotation view: ' + message);
}
function endEditing() {
let {valid, reason} = end();
if (valid) {
play('endEdit');
createEndEditBurst(playerShip);
changePerspective('universe');
state.editing = false;
state.inventory = false;
}
}
function tossItem() {
createItemToss(playerShip);
play('toss');
}
function collectItem(type, id, name) {
if (type === 'fuelcan') {
playerShip.addFuel(FUEL_CAN_AMOUNT);
play('fuelPickup');
score += 10;
notify('Collected fuel: +10');
return true;
} else {
if (usedSpace >= capacity) {
notify('No space left in inventory', 60);
return false;
}
addItem(type, id);
play('itemPickup');
score += 20;
notify(`Collected "${name}" module: +20`, 150);
return true;
}
}
class Tracer extends Body {
constructor(ship) {
super(...ship.pos, 0.1);
this.ship = ship;
this.path = [];
}
run(distance) {
this.path = [];
[this.x, this.y] = this.ship.com;
[this.xvel, this.yvel] = [this.ship.xvel, this.ship.yvel];
let dis = 0;
let last = this.pos;
let factor = 5;
for (let i = 0; dis < distance; i++) {
if (this.tickPath(factor)) break;
this.path.push(this.pos);
if (i % 10 === 0) {
let [lx, ly] = last;
dis += Math.sqrt((this.x - lx) ** 2 + (this.y - ly) ** 2);
last = this.pos;
}
if (i > distance * 5) break;
}
[this.x, this.y] = this.ship.com;
}
tick() {
this.run(this.ship.computation);
}
tickPath(speed) {
this.tickMotion(speed);
this.tickGravity(celestials, speed);
return !!this.getCelestialCollision(celestials);
}
}
class Ship extends Body {
constructor(x, y) {
super(x, y, 0);
this.localCom = [0, 0];
this.modules = new Set();
this.maxRadius = 0;
this.landed = false;
this.lastContactModule = null;
this.poc = this.com;
this.maxFuel = 0;
this.fuel = 0;
this.rotationPower = 0;
this.cargoCapacity = 0;
this.thrust = 0;
this.computation = 0;
this.crashed = false;
this.timeWithoutFuel = 0;
}
get com() {
let [lx, ly] = this.localCom;
return [this.x + lx, this.y + ly];
}
get parentCelestial() {
let closest = null;
let closestDistance = 0;
celestials.forEach(c => {
let dis = this.distanceTo(c);
if (closest === null || dis < closestDistance) {
closest = c;
closestDistance = dis;
}
});
if (closestDistance > MAX_PARENT_CELESTIAL_DISTANCE)
return null;
return closest;
}
tick() {
if (this.crashed) return;
if (!state.editing) this.tickMotion();
if (!this.landed) this.tickGravity(celestials);
if (!state.editing) this.resolveCollisions();
this.modules.forEach(m => {
if (m.type == 'thruster' && m.power !== 0) {
for (let i = 0; i < 2; i++) createThrustExhaust(m);
}
});
this.modules.forEach(m => m.reset());
if (shipLanded != this.landed) {
if (this.landed) {
landShip(this.parentCelestial);
} else {
launchShip();
}
}
if (this.fuel === 0 && !state.gameOver) {
if (this.timeWithoutFuel++ > 300)
outOfFuel();
} else {
this.timeWithoutFuel = 0;
}
}
clearModules() {
this.modules.clear();
}
addFuel(amount) {
this.fuel = Math.min(this.fuel + amount, this.maxFuel);
}
addModule(x, y, properties, options) {
let module = new Module(x, y, this, {...properties, ...options});
this.modules.add(module);
this.refreshShape();
}
deleteModule(module) {
this.modules.delete(module);
this.refreshShape();
}
refreshShape() {
let points = [];
this.modules.forEach(m => points.push([...m.localCom, m.mass]));
this.mass = points.reduce((a, [,,b]) => a + b, 0);
this.localCom = points.reduce(([ax, ay], [bx, by, bm]) =>
[ax + bx * bm, ay + by * bm], [0, 0])
.map(x => x / this.mass);
let [lx, ly] = this.localCom;
this.maxRadius = points.reduce((a, [bx, by]) =>
Math.max(Math.sqrt((bx - lx) ** 2 + (by - ly) ** 2), a), 0) + 1;
this.maxFuel = 0;
this.rotationPower = 0;
this.cargoCapacity = 0;
this.thrust = 0;
this.computation = 0;
this.modules.forEach(m => {
if (m.type === 'fuel') {
this.maxFuel += m.data.fuelCapacity;
} else if (m.type === 'capsule') {
this.rotationPower += m.data.rotation;
this.cargoCapacity += m.data.capacity;
this.computation += m.data.computation;
} else if (m.type === 'thruster') {
this.thrust += m.data.thrust;
} else if (m.type === 'gyroscope') {
this.rotationPower += m.data.rotation;
} else if (m.type === 'cargo') {
this.cargoCapacity += m.data.capacity;
} else if (m.type === 'navigation') {
this.computation += m.data.computation;
}
});
}
colliding(point, radius) {
let [px, py] = point;
let result = false;
this.modules.forEach(m => {
let [mx, my] = this.getWorldPoint(...m.localCom);
let dis = Math.sqrt((py - my) ** 2 + (px - mx) ** 2);
if (dis < radius) result = true;
});
return result;
}
resolveCollisions() {
this.landed = false;
celestials.forEach(c => {
let dis = this.distanceTo(c);
if (dis < c.radius + this.maxRadius) {
this.modules.forEach(m => {
this.checkModuleCollision(m, c);
});
}
});
}
checkModuleCollision(module, body) {
let p = this.getWorldPoint(...module.localCom);
let dis = body.distanceTo({ com: p });
if (dis < body.radius + 0.5 + EPSILON) {
this.approach(body, dis - (body.radius + 0.5));
this.moduleCollided(module);
this.halt();
this.resolveCelestialCollision(p, body, module);
this.poc = p;
this.lastContactModule = module;
}
}
moduleCollided(module) {
if (this.landed) return;
let speed = Math.sqrt(this.xvel ** 2 + this.yvel ** 2);
if (module.type !== 'thruster' || speed > CRASH_SPEED) {
crash();
this.crashed = true;
}
}
resolveCelestialCollision(pos, cel, module) {
let celToCom = this.angleTo(...this.com, ...cel.com);
let celToPoc = this.angleTo(...pos, ...cel.com);
let pocToCom = this.angleTo(...this.com, ...pos);
let shipAngle = this.normalizeAngle(this.r + Math.PI / 2);
let turnAngle = this.angleDifference(celToPoc, pocToCom);
let checkAngle = this.angleDifference(celToPoc, shipAngle);
let correctionAngle = this.angleDifference(shipAngle, celToCom);
let [force] = this.rotateVector(0, 1, turnAngle);
if (Math.abs(checkAngle) < TIP_ANGLE) {
[force] = this.rotateVector(0, 1, correctionAngle);
force *= 0.2;
}
let canLand = Math.abs(checkAngle) < 0.03
&& Math.abs(this.rvel) < 0.001;
if (canLand) {
this.landed = true;
this.rvel = 0;
this.r = celToCom - Math.PI / 2;
}
this.rvel += force * TIP_SPEED;
}
applyThrust({ forward = 0, left = 0, right = 0, back = 0,
turnLeft = 0, turnRight = 0}) {
let thrustForce = -forward * THRUST_POWER * this.thrust;
let turnForce = (turnRight - turnLeft) * TURN_POWER;
if (this.fuel <= 0) {
this.fuel = 0;
thrustForce = 0;
} else {
this.fuel -= Math.abs(thrustForce) * FUEL_BURN_RATE;
}
turnForce *= this.rotationPower;
this.applyDirectionalForce(0, thrustForce, turnForce);
if (Math.abs(this.rvel) > 0.1) {
this.rvel *= 0.7;
}
this.modules.forEach(m => {
if (m.type !== 'thruster' || thrustForce == 0) return;
m.power += forward;
});
}
}
class Celestial extends Body {
constructor(x, y, radius, {
density = 1,
type = 'rock'
}) {
let mass = (radius ** 2) * density;
super(x, y, mass);
this.radius = radius;
this.type = type;
const imageArr = Object.values(images.celestials[this.type]);
const svgImage = imageArr[Math.random() * imageArr.length | 0];
tempCanvas.width = PLANET_IMAGE_SIZE;
tempCanvas.height = PLANET_IMAGE_SIZE;
tempContext.clearRect(0, 0, PLANET_IMAGE_SIZE, PLANET_IMAGE_SIZE);
tempContext.drawImage(svgImage, 0, 0, PLANET_IMAGE_SIZE, PLANET_IMAGE_SIZE);
this.image = new Image();
this.image.src = tempCanvas.toDataURL();
// this.image = tempContext.getImageData(0, 0, PLANET_IMAGE_SIZE, PLANET_IMAGE_SIZE);
}
get com() {
return [this.x + this.radius, this.y + this.radius];
}
get escapeVelocity() {
}
tick() {
}
get diameter() {
return this.radius * 2;
}
}
class Entity extends Body {
constructor(x, y, type = 'fuel', id = 'small', {
xvel = 0,
yvel = 0,
gravity = false
} = {}) {
super(x, y, 0.1);
this.xvel = xvel;
this.yvel = yvel;
this.width = 1;
this.height = 1;
this.radius = (this.width + this.height) / 2;
this.type = type;
this.id = id;
if (this.type === 'fuelcan') {
this.image = images.modules.fuelcan;
this.name = 'Fuel Can';
} else {
this.image = images.modules[type][id];
this.name = modules[type][id].name;
if (this.type === 'thruster')
this.image = this.image.off;
}
this.gravity = gravity;
this.touched = false;
}
get com() {
return [this.x + this.width / 2, this.y + this.height / 2];
}
get localCom() {
return [this.width / 2, this.height / 2];
}
remove() {
entities.delete(this);
}
tick() {
if (Math.abs(playerShip.x - this.x) > 500 ||
Math.abs(playerShip.y - this.y) > 500) return;
this.r += ENTITY_ROTATION_RATE;
this.tickMotion();
if (this.gravity) this.tickGravity(celestials);
let col = this.getCelestialCollision(celestials);
if (col !== false) {
this.remove();
}
if (playerShip.colliding(this.com, this.radius)) {
if (this.touched) return;
this.touched = true;
let success = collectItem(this.type, this.id, this.name);
if (!success) return;
createPickupBurst(playerShip, this.com);
this.remove();
} else {
this.touched = false;
}
}
}
let spawnedSectors = new Map();
const visibleRadius = (400 / MIN_ZOOM) + SECTOR_SIZE;
function tick$5() {
let [px, py] = playerShip.com;
for (let x = px - visibleRadius; x < px + visibleRadius; x += SECTOR_SIZE)
for (let y = py - visibleRadius; y < py + visibleRadius; y += SECTOR_SIZE) {
let [sx, sy] = [x / SECTOR_SIZE | 0, y / SECTOR_SIZE | 0];
let id = `${sx}.${sy}`;
if (!spawnedSectors.has(id)) spawnSector(sx, sy);
}
spawnedSectors.forEach((objects, key) => {
let [sx, sy] = key.split('.');
let [wx, wy] = [sx * SECTOR_SIZE, sy * SECTOR_SIZE];
let dis = (wx - px) ** 2 + (wy - py) ** 2;
if (dis > (SECTOR_SIZE * 4) ** 2) {
spawnedSectors.delete(key);
objects.forEach(remove);
}
});
}
function nearest(x, y, set) {
let closest = null;
let closestDis = 0;
set.forEach(e => {
let dis = e.distanceTo({ com: [x, y] });
if (closest === null || dis < closestDis) {
closest = e;
closestDis = dis;
}
});
return [closest, closestDis];
}
function spawnSector(x, y) {
let area = SECTOR_SIZE ** 2;
let spawned = new Set();
for (let i = 0; i < area / 1000; i++) {
let [px, py] = [(x + Math.random()) * SECTOR_SIZE, (y + Math.random()) * SECTOR_SIZE];
if (Math.random() < PLANET_SPAWN_RATE / 1000) {
spawned.add(randomPlanet(px, py));
} else if (Math.random() < ENTITY_SPAWN_RATE / 1000){
spawned.add(randomEntity(px, py));
}
}
spawnedSectors.set(`${x}.${y}`, spawned);
}
function randomPlanet(x, y, {
radius = Math.random() * 60 + 30,
type = 'green',
density = 3
} = {}) {
let [cel, dis] = nearest(x, y, celestials);
let mcs = MIN_CELESTIAL_SPACING;
if (cel !== null && dis < Math.max(radius, cel.radius) * mcs) return;
let planet = celestial$1(x, y, radius, {
density: density,
type: type
});
for (let i = 0.1; i < 10; i += 0.5) {
if (Math.random() > 0.95) {
let e = randomEntity();
e.orbit(planet, i * radius, Math.random() * Math.PI * 2);
}
}
for (let i = 0; i < 10; i++) {
if (Math.random() > 0.7) {
let e = randomEntity();
e.orbit(planet, 1.5, Math.random() * Math.PI * 2);
e.gravity = false;
e.halt();
}
}
return planet;
}
function randomEntity(x, y) {
let entity;
if (Math.random() > 0.5) {
entity = new Entity(x, y, 'fuelcan');
} else {
let type, id;
while (true) {
let arr = Object.entries(modules);
[type, arr] = arr[Math.random() * arr.length | 0];
arr = Object.keys(arr);
id = arr[Math.random() * arr.length | 0];
let value = modules[type][id].value;
if (Math.random() < (1 / value)) break;
}
entity = new Entity(x, y, type, id);
}
entities.add(entity);
return entity;
}
function player() {
let ship = new Ship(0, -45);
ship.addModule(0, 0, modules.capsule.small);
ship.addModule(0, 1, modules.fuel.small);
ship.addModule(0, 2, modules.thruster.light);
//ship.addModule(1, 2, modules.thruster.light);
//ship.addModule(-1, 2, modules.thruster.light);
ships.add(ship);
setPlayerShip(ship);
ship.addFuel(ship.maxFuel);
let tracer = new Tracer(ship);
tracers.add(tracer);
return ship;
}
function startPlanet() {
let planet = randomPlanet(0, 0, {
radius: 40,
density: 3,
type: 'green'
});
let fuel = new Entity(0, 0, 'fuelcan');
entities.add(fuel);
fuel.orbit(planet, 10, -0.5);
return planet;
}
function celestial$1(x, y, radius, params) {
let celestial = new Celestial(x - radius, y - radius, radius, params);
celestials.add(celestial);
return celestial;
}
const entities = new Set();
const celestials = new Set();
const ships = new Set();
const particles = new Set();
const tracers = new Set();
let playerShip = null;
let speed = 1;
function setPlayerShip(ship) {
playerShip = ship;
}
function init$6() {
clear();
player();
startPlanet();
tick$5();
}
function clear() {
entities.clear();
celestials.clear();
ships.clear();
particles.clear();
tracers.clear();
}
function remove(object) {
entities.delete(object);
celestials.delete(object);
}
function increaseSpeed() {
if (speed < 5) speed += 1;
}
function decreaseSpeed() {
if (speed > 1) speed -= 1;
}
function tick$4() {
for (let i = 0; i < speed; i++) {
particles.forEach(p => p.tick());
celestials.forEach(c => c.tick());
entities.forEach(e => e.tick());
ships.forEach(s => s.tick());
}
tick$5();
if (trace) tracers.forEach(t => t.tick());
}
let tiles = new Map();
let width = 0;
let height = 0;
let position = [0, 0];
let message = '';
let info = '';
function init$5() {
let ship = playerShip;
let modules = ship.modules;
tiles.clear();
modules.forEach(m => {
let pos = [m.x, m.y];
tiles.set(posId(...pos), new Tile(...pos, m));
});
message = '';
adjustSize();
adjustGraphics();
}
function adjustGraphics() {
let neededZoom = canvas.width / (Math.max(width, height) + 10);
changePerspective('planet', 0, -3);
setZoom(neededZoom);
}
function adjustSize() {
let margin = EDIT_MARGIN;
let [sx, ex, sy, ey] = getBoundaries();
[width, height] = [ex - sx + margin * 2 + 1, ey - sy + margin * 2 + 1];
position = [sx - margin, sy - margin];
getAttributes();
}
function end() {
let result = validate();
result = {
valid: result === false,
reason: result
};
if (result.valid) {
let ship = playerShip;
let [ox, oy] = ship.com;
ship.clearModules();
tiles.forEach(t => {
if (t.type === null) return;
ship.addModule(t.x, t.y, modules[t.type][t.id]);
});
let [nx, ny] = ship.com;
let [dx, dy] = [nx - ox, ny - oy];
ship.x -= dx;
ship.y -= dy;
const [rdx, rdy] = ship.rotateVector(dx, dy);
ship.x -= rdx;
ship.y -= rdy;
}
return result;
}
function getAttributes() {
let cargo = 0;
let fuel = 0;
let rotation = 0;
let mass = 0;
let thrust = 0;
let computation = 0;
tiles.forEach(t => {
if (t.type === null) return;
if (t.type === 'fuel') {
fuel += t.module.fuelCapacity;
} else if (t.type === 'capsule') {
rotation += t.module.rotation;
cargo += t.module.capacity;
computation += t.module.computation;
} else if (t.type === 'thruster') {
thrust += t.module.thrust;
} else if (t.type === 'gyroscope') {
rotation += t.module.rotation;
} else if (t.type === 'cargo') {
cargo += t.module.capacity;
} else if (t.type === 'nafivation') {
computation += t.module.computation;
}
mass += t.module.mass;
});
info = 'Mass: ' + mass + '\n' +
'Fuel capacity: ' + fuel + '\n' +
'Thrust/mass ratio: ' + (thrust / Math.max(mass, 1)).toFixed(1) + '\n' +
'Rotation speed: ' + (rotation / Math.max(mass, 1) * 100).toFixed(1)
+ '\n' +
'Cargo capacity: ' + cargo + '\n' +
'Navigational computation: ' + computation;
}
function validate() {
let capsulesFound = 0;
let thrustersFound = 0;
let fuelFound = 0;
let unvisited = new Set();
tiles.forEach(t => {
if (t.type !== null) unvisited.add(t);
});
let reason = '';
if (unvisited.size == 0) {
reason = 'no capsule';
}
let visit = (tile) => {
unvisited.delete(tile);
if (tile.type == 'capsule') capsulesFound++;
if (tile.type == 'thruster') thrustersFound++;
if (tile.type == 'fuel') fuelFound++;
tile.neighbours.forEach(n => {
if (unvisited.has(n) && n.neighbours.indexOf(tile) > -1) {
visit(n);
}
});
};
if (unvisited.size > 0)
visit(unvisited.values().next().value);
if (unvisited.size > 0) {
reason = 'not connected';
} else if (capsulesFound === 0) {
reason = 'no capsule';
} else if (thrustersFound === 0) {
reason = 'no thruster';
} else if (fuelFound === 0) {
reason = 'no fuel tank';
} else if (usedSpace > capacity) {
reason = 'inventory too full';
} else {
reason = false;
}
if (reason === false) {
message = '';
} else {
message = reason;
}
return reason;
}
function positionAdjust(x, y) {
let [px, py] = position;
return [x + px, y + py];
}
function clickTile(x, y) {
if (currentItem === null) return;
let current = getTile(x, y).source;
if (current.type !== null) {
return;
}
let pos = positionAdjust(x, y);
let id = posId(...pos);
tiles.set(id, new Tile(...pos, currentItem));
removeItem(...currentItem.ident);
adjustSize();
validate();
}
function rightClickTile(x, y) {
let current = getTile(x, y).source;
if (current.type === null) return;
let { x: tx, y: ty } = current;
let id = posId(tx, ty);
tiles.set(id, new Tile(tx, ty, null));
addItem(current.type, current.id);
adjustSize();
validate();
}
function getTile(x, y) {
let [px, py] = position;
return getRawTile(px + x, py + y);
}
function getRawTile(x, y) {
let id = posId(x, y);
if (!tiles.has(id))
tiles.set(id, new Tile(x, y, null));
return tiles.get(id);
// TODO: Get linked tiles.
}
function posId(x, y) {
return `${x}.${y}`;
}
function getBoundaries() {
let sx = sy = ex = ey = null;
tiles.forEach(t => {
if (t.type === null) return;
if (sx === null || t.x < sx) sx = t.x;
if (ex === null || t.x > ex) ex = t.x;
if (sy === null || t.y < sy) sy = t.y;
if (ey === null || t.y > ey) ey = t.y;
});
return [sx, ex, sy, ey];
}
class Tile {
constructor(x, y, module) {
if (module === null) {
this.module = null;
this.image = null;
this.type = null;
this.id = null;
} else {
({type: this.type, id: this.id} = module);
this.module = modules[this.type][this.id];
this.image = images.modules[this.type][this.id];
if (module.type === 'thruster') this.image = this.image.off;
}
this.x = x;
this.y = y;
}
get valid() {
return true;
}
get neighbours() {
return [[0, -1], [1, 0], [0, 1], [-1, 0]].filter((_, i) => {
return this.module.connectivity[i];
}).map(([dx, dy]) => getRawTile(this.x + dx, this.y + dy));
}
get source() {
return this;
}
get drawPos() {
let [px, py] = pos;
return [this.x + px, this.y + py];
}
}
const mouse = { pressed: {}, held: {}, x: 0, y: 0, scroll: 0 };
const keyCode = { pressed: {}, held: {} };
const key = { pressed: {}, held: {} };
function tick$3() {
mouse.pressed = {};
keyCode.pressed = {};
key.pressed = {};
mouse.scroll = 0;
}
function init$4() {
window.addEventListener('keydown', event => {
keyCode.pressed[event.code] = !keyCode.held[event.code];
keyCode.held[event.code] = true;
key.pressed[event.key] = !keyCode.held[event.key];
key.held[event.key] = true;
});
window.addEventListener('keyup', event => {
keyCode.held[event.code] = false;
key.held[event.key] = false;
});
// Ṕ͕͖ẖ̨̖̺͓̪̹n̼͇͔̯̝̖g̙̩̭͕ͅͅl̻̰͘u͎̥͍̗ͅi̼̞̪̩͚̜͖ ̫̝̻͚͟m͎̳̙̭̩̩̕g̟̤̬̮l̫̕w̶͚͈͚̟͔͖n͏̝͖̞̺ͅa͏̹͓̬̺f̗̬̬̬̖̫͜h͙̘̝̱̬̗͜ ̼͎͖C̱͔̱͖ṭ̬̱͖h̵̰̼̘̩ùlh̙́u̪̫ ̪̺̹̙̯R̞͓̹̞͍͎͉͎̦͙ͅl͇̠̮y̙̪̰̪͙̖e̢̩͉͙̼h̗͔̹̳ ̶w̨̼͍̝̭̣̣ͅg̶̳̦̳a̴͉̹͙̭̟ͅh͈͎̞̜͉̼̜̠́͞n̲a̯g̮͚͓̝l̠ ̹̹̱͙̝f̧̝͖̱h̪̟̻͖̖t͎͘aͅg̤̘͜n̶͈̻̻̝̳
window.addEventListener('mousedown', event => {
mouse.pressed[event.button] = !mouse.held[event.button];
mouse.held[event.button] = true;
tickAfterMouse = false;
});
window.addEventListener('mouseup', event => {
mouse.held[event.button] = false;
});
window.addEventListener('mousemove', event => {
let rect = canvas.getBoundingClientRect();
mouse.x = event.clientX - rect.left;
mouse.y = event.clientY - rect.top;
});
window.addEventListener('wheel', event => {
mouse.scroll = event.deltaY;
// event.preventDefault();
});
window.addEventListener('contextmenu', event => {
event.preventDefault();
});
}
class Rect {
constructor(x = 0, y = 0, w = 0, h = 0) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.onclick = null;
this.mouseHeld = false;
this.rightMouseHeld = false;
this.onRightClick = null;
}
click() {}
rightClick() {}
tickMouse() {
if (this.mouseHeld == true && !mouse.held[0] && this.mouseOver)
this.click();
if (!this.mouseHeld && mouse.pressed[0] && this.mouseOver)
this.mouseHeld = true;
if (!mouse.held[0])
this.mouseHeld = false;
if (this.rightMouseHeld == true && !mouse.held[2]
&&this.mouseOver)
this.rightClick();
if (!this.rightMouseHeld && mouse.pressed[2] && this.mouseOver)
this.rightMouseHeld = true;
if (!mouse.held[2])
this.rightMouseHeld = false;
}
get shape() {
return [this.x, this.y, this.w, this.h];
}
get end() {
return [this.x + this.w, this.y + this.h];
}
get center() {
return [this.x + this.w / 2, this.y + this.h / 2];
}
get mouseOver() {
return this.containsPoint(mouse.x, mouse.y);
}
get mouseClicked() {
return this.mouseOver() && mouse.pressed[0];
}
containsPoint(x, y) {
return x > this.x && x < this.x + this.w
&& y > this.y && y < this.y + this.h;
}
}
const defaultOptions = {
draw: true, // Whether the element itself will be rendered.
drawChildren: true // Whether children will be rendered.
};
class GuiElement extends Rect {
constructor(x, y, w, h, options = {}) {
super(x, y, w, h);
this.children = new Set();
this.parent = null;
this.type = 'element';
this.options = Object.assign({}, defaultOptions, options);
}
tickElement() {
this.tickMouse();
this.tick();
this.children.forEach(c => c.tickElement());
}
tick() {
}
get drawn() {
if (!this.options.drawChildren) return false;
if (!this.parent) return true;
return this.parent.drawn;
}
append(element) {
this.children.add(element);
element.parent = this;
element.x += this.x;
element.y += this.y;
}
clear() {
this.children.clear();
}
// Code should be self-describing, comments are for fucking about.
// - Albert Einstein
posRelative({x = null, xc = 0, y = null, yc = 0, w = null, h = null}) {
if (x !== null)
this.x = (this.parent.w * x) - (this.w * xc) + this.parent.x;
if (y !== null)
this.y = (this.parent.h * y) - (this.h * yc) + this.parent.y;
if (w !== null)
this.w = this.parent.w * w;
if (h !== null)
this.h = this.parent.h * h;
}
}
class GuiFrame extends GuiElement {
constructor(x, y, w, h, options) {
super(x, y, w, h, options);
this.type = 'frame';
}
}
class GuiImage extends GuiElement {
constructor(src, x, y, w, h) {
w = w || src.width;
h = h || src.height;
super(x, y, w, h);
this.type = 'image';
this.image = src;
this.imgRatio = src.width / src.height;
}
scaleImage({ w = null, h = null }) {
if (w !== null && h === null) {
this.w = w;
this.h = w / this.imgRatio;
} else if (h !== null && w === null) {
this.h = h;
this.w = h / this.imgRatio;
} else if ( h !== null && w !== null) {
this.w = w;
this.h = h;
}
}
}
class GuiButton extends GuiElement {
constructor(text, onclick, x, y, w = 100, h = 30) {
super(x, y, w, h);
this.type = 'button';
this.text = text;
this.onclick = onclick;
}
click() {
if (this.drawn && !this.options.disabled)
this.onclick();
}
}
class GuiItemButton extends GuiButton {
constructor(tile, onclick, x, y, w = 50, h = 50, {
padding = 0,
selected = false,
quantity = 1,
} = {}) {
super(null, onclick, x, y, w, h);
this.module = tile.module;
this.image = tile.image;
this.type = 'itemButton';
this.padding = padding;
this.selected = selected;
this.quantity = quantity;
}
click() {
if (this.drawn)
this.onclick('left');
}
rightClick() {
if (this.drawn)
this.onclick('right');
}
}
class GuiInventory extends GuiElement {
constructor(x, y, w = 100, h = 30) {
super(x, y, w, h);
this.type = 'inventory';
this.tileWidth = 4;
this.tileHeight = 5;
this.currentPage = 0;
setOnupdate(this.updateTiles.bind(this));
this.guiInfo = null;
}
updateTiles() {
this.children.clear();
let tileRatio = this.tileWidth / this.tileHeight;
let rectRatio = this.w / this.h;
let tileSize;
let [ox, oy] = [0, 0];
if (tileRatio < rectRatio) {
tileSize = this.h / this.tileHeight;
ox = (this.w - (tileSize * this.tileWidth)) / 2;
} else {
tileSize = this.w / this.tileWidth;
oy = (this.h - (tileSize * this.tileHeight)) / 2;
}
let spacing = 0.15 * tileSize;
let pageSize = this.tileWidth * this.tileHeight;
let offset = pageSize * this.currentPage;
let tiles = getTiles().slice(offset);
let tile;
let cur = currentItem;
for (let y = 0; y < this.tileHeight; y++)
for (let x = 0; x < this.tileWidth && tiles.length; x++) {
y * this.tileWidth + (x % this.tileWidth) + offset;
tile = tiles.shift();
let ex = x * tileSize + spacing / 2 + ox;
let ey = y * tileSize + spacing / 2 + oy;
let [ew, eh] = [tileSize - spacing, tileSize - spacing];
let ident = tile.ident;
let onclick = (button) => {
this.tileClicked(...ident, button);
};
let selected = cur !== null && tile.type === cur.type
&& tile.id === cur.id;
let el = new GuiItemButton(tile, onclick, ex, ey, ew, eh, {
padding: 0.1,
selected: selected,
quantity: tile.quantity
});
this.append(el);
}
this.guiInfo.text = cur === null ? '' : cur.textInfo;
this.guiInfo.splitLines();
}
tick() {
if (state.inventory && !this.active) this.updateTiles();
this.active = state.inventory;
this.parent.options.drawChildren = this.active;
if (!this.active) return;
this.children;
}
getTile(x, y) {
return this.getTile(x + px, y + py);
}
tileClicked(type, id, button) {
if (button == 'left') selectItem(type, id);
if (button == 'right') {
if (canToss()) {
removeItem(type, id);
}
}
this.updateTiles();
}
}
class GuiEdit extends GuiElement {
constructor(x, y, w = 100, h = 30) {
super(x, y, w, h);
this.type = 'edit';
this.tileWidth = 0;
this.tileHeight = 0;
this.active = false;
this.guiInventory = null;
}
updateTiles() {
this.children.clear();
[this.tileWidth, this.tileHeight] = [width, height];
let tileRatio = this.tileWidth / this.tileHeight;
let rectRatio = this.w / this.h;
let tileSize;
let [ox, oy] = [0, 0];
if (tileRatio < rectRatio) {
tileSize = this.h / this.tileHeight;
ox = (this.w - (tileSize * this.tileWidth)) / 2;
} else {
tileSize = this.w / this.tileWidth;
oy = (this.h - (tileSize * this.tileHeight)) / 2;
}
let spacing = 0.1 * tileSize;
for (let x = 0; x < this.tileWidth; x++)
for (let y = 0; y < this.tileHeight; y++) {
let tile = getTile(x, y);
let ex = x * tileSize + spacing / 2 + ox;
let ey = y * tileSize + spacing / 2 + oy;
let [ew, eh] = [tileSize - spacing, tileSize - spacing];
let onclick = (button) => {
this.tileClicked(x, y, button);
};
let el = new GuiItemButton(tile, onclick, ex, ey, ew, eh);
this.append(el);
}
}
tick() {
if (state.editing && !this.active) this.updateTiles();
this.active = state.editing;
this.parent.options.drawChildren = this.active;
if (!this.active) return;
this.textElements.info.text = info;
[this.tileWidth, this.tileHeight] = [width, height];
}
getTile(x, y) {
let [px, py] = position;
return getTile(x + px, y + py);
}
tileClicked(x, y, button) {
if (button == 'left') {
clickTile(x, y);
} else if (button == 'right') {
rightClickTile(x, y);
}
this.updateTiles();
this.guiInventory.updateTiles();
}
}
class GuiText extends GuiElement {
constructor(text = '', x, y, w, h, {
size = 10,
align = 'left',
valign = 'top',
color = '#fff'
} = {}) {
w = w;
h = h;
super(x, y, w, h);
this.type = 'text';
this.color = color;
this.text = text;
this.spacing = size * 1.2;
this.font = size + 'px Courier New';
this.align = align;
this.valign = valign;
}
splitLines() {
// Not very robust, but good enough for now. Mebe fix later?
context.font = this.font;
let maxLineLength = (this.w / context.measureText('A').width) | 0;
maxLineLength = Math.max(maxLineLength, 1);
let lines = [];
let currentLine = '';
this.text.split('\n').forEach(l => {
currentLine = '';
l.split(' ').forEach(word => {
if (word.length + currentLine.length > maxLineLength) {
lines.push(currentLine.slice(0, -1));
currentLine = '';
}
currentLine += word + ' ';
});
lines.push(currentLine.slice(0, -1));
});
this.text = lines.join('\n');
}
}
function root$1() {
return new GuiFrame(0, 0, canvas.width, canvas.height, {
draw: false
});
}
function title() {
let shadow = root$1();
let logo = new GuiImage(images.title.logo);
shadow.append(logo);
logo.scaleImage({ w: shadow.w * 0.7 });
logo.posRelative({ x: 0.5, xc: 0.5, y: 0.2 });
let start = new GuiButton('Start game', startGame, 0, 0, 200);
shadow.append(start);
start.posRelative({ x: 0.5, xc: 0.5, y: 1 });
start.y -= 160;
let secondFunction = () => {};
let second = new GuiButton('Don\'t start game', secondFunction, 0, 0, 200);
shadow.append(second);
second.posRelative({ x: 0.5, xc: 0.5, y: 1 });
second.y -= 110;
let thirdFunction = howToPlay;
let third = new GuiButton('How to play', thirdFunction, 0, 0, 200);
shadow.append(third);
third.posRelative({ x: 0.5, xc: 0.5, y: 1 });
third.y -= 60;
return shadow;
}
const instructionText = `\
Flight controls
WAD: Movement
Shift + WAD: Fine movement
E: Open/close inventory
R: Toggle item markers
T: Toggle path prediction
P: Pause/unpause
M: Toggle music
Ship editing and inventory controls
Left click: Select module in inventory
Right click: Toss away module in inventory
Left click: Place module on ship
Right click: Remove module from ship
Escape: Exit ship editing screen
Fly around collecting modules and fuel, and land to build your ship using \
those collected modules. Get the highest score possible without crashing or \
running out of fuel.
`;
function instructions() {
let shadow = root$1();
let frame = new GuiFrame();
shadow.append(frame);
frame.posRelative({x: 0.1, y: 0.1, w: 0.8, h: 0.8});
let back = new GuiButton('Return to menu', toMenu, 0, 0, 200);
frame.append(back);
back.posRelative({ x: 0.5, xc: 0.5, y: 1 });
back.y -= 60;
let text = new GuiText(instructionText, 0, 0, 0, 0, {
size: 12,
align: 'left',
valign: 'top'
});
frame.append(text);
text.posRelative({x: 0.05, y: 0.05, w: 0.9, h: 0.9});
text.splitLines();
return shadow;
}
function game() {
let shadow = root$1();
let editButton = new GuiButton('Edit rocket', toggleEdit, 0, 0, 200);
shadow.append(editButton);
editButton.posRelative({ x: 0.5, xc: 0.5, y: 1 });
editButton.y -= 45;
editButton.tick = () => {
let usable = state.landed && !state.gameOver;
editButton.options.draw = usable;
editButton.options.disabled = usable && message !== '';
if (state.editing) {
editButton.text = 'Finish';
if (message !== '') editButton.text = '(' + message + ')';
} else {
editButton.text = 'Edit rocket';
}
};
let fuel = new GuiText('', 0, 0, 0, 0, {
size: 14,
align: 'right',
valign: 'bottom'
});
shadow.append(fuel);
fuel.posRelative({x: 1, y: 1});
fuel.y -= 10;
fuel.x -= 10;
fuel.tick = () => {
let ship = playerShip;
fuel.text = 'Fuel: ' + ship.fuel.toFixed(1) + '/' +
ship.maxFuel.toFixed(1);
};
let speed$1 = new GuiText('', 0, 0, 0, 0, {
size: 14,
align: 'right',
valign: 'bottom',
});
shadow.append(speed$1);
speed$1.posRelative({x: 1, y: 1});
speed$1.y -= 30;
speed$1.x -= 10;
speed$1.tick = () => {
speed$1.text = 'Speed: ' + speed.toFixed(1) + 'x';
};
let score$1 = new GuiText('', 0, 0, 0, 0, {
size: 14,
align: 'left',
valign: 'bottom'
});
shadow.append(score$1);
score$1.posRelative({x: 0, y: 1});
score$1.y -= 10;
score$1.x += 10;
score$1.tick = () => {
score$1.text = 'Score: ' + score;
};
let editShadow = root$1();
shadow.append(editShadow);
editShadow.posRelative({x: 0.45, y: 0, w: 0.55, h: 0.6});
editShadow.x -= 10;
editShadow.y += 10;
let edit = new GuiEdit(0, 0, 0, 0);
editShadow.append(edit);
edit.posRelative({w: 1, h: 1});
let editInfoText = new GuiText('', 0, 0, 0, 0, {
size: 12,
align: 'right'
});
editShadow.append(editInfoText);
editInfoText.posRelative({x: 1, y: 1});
editInfoText.y += 5;
editInfoText.x -= 20;
let editWarnText = new GuiText('', 0, 0, 0, 0, {
size: 12,
align: 'center'
});
editShadow.append(editWarnText);
editWarnText.posRelative({x: 0.5, y: 1});
editWarnText.y += 20;
edit.textElements = {
info: editInfoText,
warn: editWarnText
};
let invShadow = root$1();
shadow.append(invShadow);
invShadow.posRelative({x: 0, w: 0.4, h: 0.6});
invShadow.x += 10;
invShadow.y += 10;
let invElement = new GuiInventory(0, 0, 0, 0);
invShadow.append(invElement);
invElement.posRelative({w: 1, h: 0.8});
let capacityInfo = new GuiText('', 0, 0, 0, 0, {
size: 12,
align: 'left',
valign: 'bottom'
});
invShadow.append(capacityInfo);
capacityInfo.posRelative({x: 0, y: 1});
capacityInfo.y -= 5;
capacityInfo.x += 5;
capacityInfo.tick = () => {
capacityInfo.text = 'Space used: ' + usedSpace + ' / ' +
capacity;
};
let moduleInfo = new GuiText('', 0, 0, 0, 0, {
size: 12,
align: 'left',
valign: 'top'
});
invShadow.append(moduleInfo);
moduleInfo.posRelative({x: 0, y: 1, w: 1});
moduleInfo.splitLines();
moduleInfo.y += 5;
invElement.guiInfo = moduleInfo;
edit.guiInventory = invElement;
let notification = new GuiText('', 0, 0, 0, 0, {
size: 14,
align: 'center',
valign: 'top'
});
shadow.append(notification);
notification.posRelative({x: 0.5});
notification.y += 10;
setNotificationElement(notification);
let gameOver = root$1();
shadow.append(gameOver);
gameOver.posRelative({x: 0.2, y: 0.2, w: 0.6, h: 0.6});
let gameOverMain = new GuiText('Game over', 0, 0, 0, 0, {
size: 48,
align: 'center',
valign: 'top'
});
gameOver.append(gameOverMain);
gameOverMain.posRelative({x: 0.5});
gameOverMain.y += 10;
gameOver.tick = () => {
gameOver.options.drawChildren = state.gameOver;
};
let gameOverReason$1 = new GuiText('', 0, 0, 0, 0, {
size: 14,
align: 'center',
valign: 'top'
});
gameOver.append(gameOverReason$1);
gameOverReason$1.posRelative({x: 0.5});
gameOverReason$1.y += 100;
gameOverReason$1.tick = () => {
gameOverReason$1.text = gameOverReason;
};
let gameOverScore = new GuiText('', 0, 0, 0, 0, {
size: 14,
align: 'center',
valign: 'top'
});
gameOver.append(gameOverScore);
gameOverScore.posRelative({x: 0.5});
gameOverScore.y += 200;
gameOverScore.tick = () => {
gameOverScore.text = scoreText;
};
let gameOverExit = new GuiButton('Main menu', toMenu, 0, 0, 200);
gameOver.append(gameOverExit);
gameOverExit.posRelative({ x: 0.5, xc: 0.5, y: 1 });
gameOverExit.y -= 10;
return shadow;
}
const elements = new Set();
let root;
function init$3() {
elements.clear();
root = root$1();
changeView$1('menu');
}
function tick$2() {
root.tickElement();
}
function changeView$1(view) {
root.clear();
if (view === 'menu') {
root.append(title());
}
if (view === 'game') {
root.append(game());
}
if (view === 'instructions') {
root.append(instructions());
}
}
function render$3() {
renderElement(root);
}
function renderElement(element) {
if (element.options.draw) {
if (element.type === 'frame') renderFrame(element);
if (element.type === 'image') renderImage(element);
if (element.type === 'button') renderButton(element);
if (element.type === 'edit') ;
if (element.type === 'itemButton') renderItemButton(element);
if (element.type === 'inventory') renderInventory(element);
if (element.type === 'text') renderText(element);
}
if (element.options.drawChildren)
element.children.forEach(renderElement);
}
function renderFrame(element) {
context.fillStyle = '#a3977c';
context.fillRect(...element.shape);
context.lineWidth = 3;
context.strokeStyle = '#6d634b';
context.strokeRect(...element.shape);
}
function renderImage(element) {
context.drawImage(element.image, ...element.shape);
}
function renderText(element) {
context.font = element.font;
context.textAlign = element.align;
context.textBaseline = element.valign;
context.fillStyle = element.color;
element.text.split('\n').forEach((line, i) =>
context.fillText(line, element.x, element.y + i * element.spacing)
);
}
function renderButton(element) {
if (element.mouseHeld && !element.options.disabled) {
context.fillStyle = '#706244';
} else if (element.mouseOver && !element.options.disabled) {
context.fillStyle = '#ad9869';
} else {
context.fillStyle = '#917f58';
}
if (element.options.disabled) {
context.globalAlpha = 0.5;
}
let [sx, sy, w, h] = element.shape;
let [ex, ey] = [sx + w, sy + h];
let rad = 5;
context.beginPath();
context.moveTo(sx + rad, sy);
context.lineTo(ex - rad, sy);
context.quadraticCurveTo(ex, sy, ex, sy + rad);
context.lineTo(ex, ey - rad);
context.quadraticCurveTo(ex, ey, ex - rad, ey);
context.lineTo(sx + rad, ey);
context.quadraticCurveTo(sx, ey, sx, ey - rad);
context.lineTo(sx, sy + rad);
context.quadraticCurveTo(sx, sy, sx + rad, sy);
context.closePath();
context.fill();
context.strokeStyle = '#541';
context.lineWidth = 2;
context.stroke();
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = '#fff';
context.font = '12pt Courier New';
context.fillText(element.text, ...element.center);
context.globalAlpha = 1;
}
function renderItemButton(element) {
context.globalAlpha = 0.5;
if (element.mouseHeld || element.rightMouseHeld) {
context.fillStyle = '#080808';
} else {
context.fillStyle = element.mouseOver ? '#222' : '#0e0e0e';
}
context.fillRect(...element.shape);
if (element.selected) {
context.strokeStyle = '#fff';
context.lineWidth = 2;
} else {
context.strokeStyle = '#333';
context.lineWidth = 1;
}
context.strokeRect(...element.shape);
context.globalAlpha = 1;
if (element.image) {
let p = element.padding;
let ox = element.x + (p / 2 * element.w);
let oy = element.y + (p / 2 * element.h);
let [dw, dh] = [element.w * (1 - p), element.h * (1 - p)];
context.drawImage(element.image, ox, oy, dw, dh);
}
if (element.quantity > 1) {
context.textAlign = 'right';
context.textBaseline = 'bottom';
context.fillStyle = '#fff';
context.font = 'bold 10pt Courier New';
let [ex, ey] = element.end;
context.fillText('x' + element.quantity, ex - 2, ey - 2);
}
}
function renderInventory(element) {
context.globalAlpha = 0.1;
context.fillStyle = '#541';
context.fillRect(...element.parent.shape);
context.globalAlpha = 1;
}
function render$2() {
for (particle of particles) renderParticle(particle);
for (celestial of celestials) if (isVisible(celestial)) renderCelestial(celestial);
if (trace) for (tracer of tracers) renderTracer(tracer);
for (ship of ships) renderShip(ship);
for (entity of entities) if (isVisible(entity)) renderEntity(entity);
/*
if (typeof window.q === 'undefined') window.q = [];
q.forEach(p => {
context.fillStyle = p[2];
context.fillRect(p[0] - 0.05, p[1] - 0.05, 0.1, 0.1);
});
*/
}
function isVisible(body) {
const [bx, by] = body.com;
const [px, py] = [perspective.x, perspective.y];
perspective.bounds;
const [centerX, centerY] = [px, py];
const margin = 1000;
return bx > centerX - margin && bx < centerX + margin &&
by > centerY - margin && by < centerY + margin;
}
function renderParticle(particle) {
context.fillStyle = particle.color;
context.fillRect(...particle.com, particle.size, particle.size);
}
function renderEntity(entity) {
context.save();
context.translate(...entity.com);
let alpha = Math.max(1 - ((perspective.zoom - 1) / 2), 0) ** 2;
if (alpha > 0 && markers) {
context.globalAlpha = alpha;
context.beginPath();
context.arc(0, 0, 4, 0, 2 * Math.PI);
context.lineWidth = 1;
context.strokeStyle = '#31911b';
if (entity.type === 'fuelcan')
context.strokeStyle = '#af4021';
context.stroke();
context.globalAlpha = 1;
}
context.rotate(entity.r);
context.drawImage(entity.image, -0.5, -0.5, 1, 1);
context.restore();
}
function renderShip(ship) {
if (ship.crashed) return;
context.save();
context.translate(...ship.com);
context.rotate(ship.r);
let [cx, cy] = ship.localCom;
context.translate(-cx, -cy);
ship.modules.forEach(m => {
[m.x, m.y];
if (state.editing) ;
context.drawImage(m.currentImage, m.x, m.y, 1, 1);
});
context.restore();
}
({
green: Object.values(images.celestials.green)
});
function renderCelestial(cel) {
context.drawImage(cel.image, cel.x, cel.y,
cel.diameter, cel.diameter);
}
function renderTracer(tracer) {
context.lineWidth = 0.1;
context.strokeStyle = '#fff';
context.beginPath();
context.moveTo(...tracer.pos);
let path = tracer.path;
for (let i = 0; i < path.length; i++) {
context.lineTo(...path[i]);
if (i % 5 === 0 || i == path.length - 1) {
context.stroke();
context.globalAlpha = (1 - (i / path.length)) * 0.5;
}
}
context.globalAlpha = 1;
}
let patterns = null;
function init$2() {
patterns = {
back: context.createPattern(images.background.back, 'repeat'),
middle: context.createPattern(images.background.middle, 'repeat'),
front: context.createPattern(images.background.front, 'repeat')
};
}
function render$1(angle) {
if (patterns === null) init$2();
// renderLayer(patterns.back, 0.3, 1, angle);
// renderLayer(patterns.middle, 0.5, 0.3, angle);
// renderLayer(patterns.front, 0.7, 0.3, angle);
}
const TAU = TAU$1;
let canvas, context, tempCanvas, tempContext;
let perspective;
let trace = true;
let markers = true;
function init$1() {
canvas = document.querySelector('#main');
context = canvas.getContext('2d');
tempCanvas = document.querySelector('#temp');
tempContext = tempCanvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
perspective = new Perspective();
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
context.font = '36px Courier New';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = '#fff';
context.fillText('Loading...', canvas.width / 2, canvas.height / 2);
}
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.rect(0, 0, canvas.width, canvas.height);
context.clip();
context.save();
perspective.tick();
perspective.transformRotate();
render$1(perspective.rotation);
perspective.transformCanvas();
render$2();
context.restore();
render$3();
}
function changePerspective(rotationMode, shiftX = 0, shiftY = 0) {
perspective.changeRotationMode(rotationMode);
perspective.changeShift(shiftX, shiftY);
perspective.transition = 1;
}
function cycleRotationMode() {
if (perspective.rotationMode === 'parent') {
perspective.changeRotationMode('local');
} else if (perspective.rotationMode === 'local') {
perspective.changeRotationMode('universe');
} else {
perspective.changeRotationMode('parent');
}
perspective.transition = 1;
return perspective.rotationMode;
}
function toggleTrace() {
trace = !trace;
return trace;
}
function toggleMarkers() {
markers = !markers;
return markers;
}
function changeZoom(delta) {
perspective.zoomDelta(delta);
}
function setZoom(target) {
perspective.changeZoom(target);
}
class Perspective {
constructor() {
this.x = 0;
this.y = 0;
this.shiftX = 0;
this.shiftY = 0;
this.zoom = 0;
this.bounds = [0, 0, canvas.width, canvas.height];
this.reset();
}
reset() {
this.rotationMode = 'universe';
this.targetZoom = DEFAULT_ZOOM;
this.oldTarget = 0;
this.oldShift = [0, 0];
this.shiftX = 0;
this.shiftY = 0;
this.oldZoom = 0;
this.transition = 0;
this.zoomTransition = 0;
this.zoomTransitionSpeed = 0.9;
this.rotation = 0;
this.targetRotation = 0;
this.zoom = DEFAULT_ZOOM;
this.targetZoom = this.zoom;
this.focus = null;
this.rotationFocus = null;
}
changeRotationMode(mode) {
this.oldShift = this.currentShift;
this.oldTarget = this.currentRotation;
this.rotationMode = mode;
}
changeShift(x, y) {
this.oldShift = this.currentShift;
[this.shiftX, this.shiftY] = [x, y];
}
changeZoom(zoom, speed = 0.9) {
this.oldZoom = this.currentZoom;
this.targetZoom = zoom;
this.zoomTransition = 1;
this.zoomTransitionSpeed = speed;
}
get currentShift() {
let [ox, oy] = this.oldShift;
return [this.interpolate(this.shiftX, ox),
this.interpolate(this.shiftY, oy)];
}
get currentRotation() {
return this.interpolateAngles(this.targetRotation, this.oldTarget);
}
get currentZoom() {
let t = this.zoomTransition;
return (this.oldZoom * t + this.targetZoom * (1 - t));
}
interpolate(cur, old, x = this.transition) {
return (old * x + cur * (1 - x));
}
interpolateAngles(cur, old, x = this.transition) {
return old + this.angleDifference(old, cur) * (1 - x);
}
angleDifference(a, b) {
return Math.atan2(Math.sin(b - a), Math.cos(b - a));
}
tick() {
if (this.focus !== null)
[this.x, this.y] = this.focus.com;
if (this.focus === null || this.rotationMode === 'universe') {
this.targetRotation = 0;
} else if (this.rotationMode === 'parent') {
let parent = this.focus.parentCelestial;
if (parent === null) {
this.targetRotation = 0;
} else {
let a = this.focus.angleTo(...this.focus.com, ...parent.com);
this.targetRotation = a - Math.PI / 2;
}
} else {
this.targetRotation = this.focus.r;
}
this.normalize();
let dif = Math.abs(this.targetRotation - this.rotation);
this.rotationMet = dif < (this.rotationMet ? 0.3 : 0.05);
this.rotation = this.currentRotation;
this.zoom = this.currentZoom;
this.transition *= 0.9;
this.zoomTransition *= this.zoomTransitionSpeed;
}
focusPlayer() {
this.focus = playerShip;
this.rotationFocus = playerShip;
}
zoomDelta(delta) {
let factor = 1 + (ZOOM_SPEED * Math.abs(delta));
let target = this.targetZoom * (delta > 0 ? factor : 1 / factor);
this.changeZoom(target, 0.7);
this.normalize();
}
normalize() {
this.targetZoom = Math.max(MIN_ZOOM,
Math.min(MAX_ZOOM, this.targetZoom));
this.targetRotation %= TAU;
}
transformRotate() {
let [,,bw, bh] = this.bounds;
context.translate(bw / 2, bh / 2);
context.rotate(-this.rotation);
context.translate(-bw / 2, -bh / 2);
}
rotateVector(x, y, r = this.rotation) {
return [(x * Math.cos(r) - y * Math.sin(r)),
(y * Math.cos(r) - x * Math.sin(r))];
}
transformCanvas() {
let [,,bw, bh] = this.bounds;
let [sx, sy] = this.rotateVector(...this.currentShift, this.rotation);
let tx = -(this.x + sx) * this.zoom;
let ty = -(this.y + sy) * this.zoom;
context.translate(tx + bw / 2, ty + bh / 2);
context.scale(this.zoom, this.zoom);
}
normalizeAngle(a = this.r) {
return ((a % TAU) + TAU) % TAU;
}
}
const mapping = {
thrust: 'KeyW',
left: 'KeyA',
right: 'KeyD',
reduce: 'ShiftLeft',
exitEdit: 'Escape',
inventory: 'KeyE',
cycleRotation: 'KeyC',
toggleTrace: 'KeyT',
toggleMarkers: 'KeyR',
toggleMusic: 'KeyM',
togglePause: 'KeyP',
zoomIn: 'KeyZ',
zoomOut: 'KeyX',
increaseSpeed: 'Period',
decreaseSpeed: 'Comma',
};
let held, pressed;
function tick$1() {
held = keyCode.held;
pressed = keyCode.pressed;
if (state.editing) {
tickEditing();
} else if (state.playing && !state.gameOver && !state.paused) {
tickPlaying();
}
if (!state.editing) {
if (mouse.scroll !== 0) {
// Fix for Firefox.
let delta = mouse.scroll > 0 ? -50 : 50;
changeZoom(delta);
}
if (held[mapping.zoomIn]) {
changeZoom(-10);
}
if (held[mapping.zoomOut]) {
changeZoom(10);
}
if (pressed[mapping.togglePause] && !state.gameOver) {
togglePause();
}
if (pressed[mapping.increaseSpeed]) {
increaseSpeed();
}
if (pressed[mapping.decreaseSpeed]) {
decreaseSpeed();
}
}
if (state.gameOver) {
stop('engine');
}
if (pressed[mapping.toggleMusic]) {
toggle('music');
}
}
function tickPlaying() {
let power = held[mapping.reduce] ? 0.3 : 1;
if (held[mapping.thrust] && playerShip.fuel !== 0) {
playerShip.applyThrust({ forward: power });
let vol = Math.min(0.7, perspective.zoom / 10);
volume('engine', vol);
} else {
stop('engine');
}
if (pressed[mapping.thrust]) {
if (playerShip.fuel !== 0) {
start('engine');
} else {
stop('engine');
}
}
if (held[mapping.left]) {
playerShip.applyThrust({ turnLeft: power });
}
if (held[mapping.right]) {
playerShip.applyThrust({ turnRight: power });
}
if (pressed[mapping.inventory]) {
state.inventory = !state.inventory;
}
if (pressed[mapping.cycleRotation]) {
cycleRotationMode$1();
}
if (pressed[mapping.toggleTrace]) {
toggleTrace$1();
}
if (pressed[mapping.toggleMarkers]) {
toggleMarkers$1();
}
}
function tickEditing() {
if (pressed[mapping.exitEdit]) {
endEditing();
}
}
let state;
async function init() {
state = {
view: 'menu',
playing: false,
editing: false,
paused: false,
inventory: false,
gameOver: false
};
init$1();
await init$9();
init$3();
init$4();
playMusic();
//events.startGame();
loop(tick);
}
function changeView(view) {
state.view = view;
changeView$1(view);
if (view === 'game') {
state.playing = true;
state.editing = false;
state.paused = false;
init$6();
init$5();
perspective.reset();
init$8();
} else if (view === 'instructions') {
state.playing = false;
changeView$1('instructions');
} else if (view === 'menu') {
changeView$1('menu');
clear();
}
}
function loop(fn, fps = 60) {
(function loop(time) {
fn();
requestAnimationFrame(loop);
})();
}
function tick() {
tick$6();
if (state.view == 'game' && !state.paused) {
tick$4();
}
tick$1();
tick$2();
render();
tick$3();
}
window.addEventListener('load', init);
//# sourceMappingURL=improcket.min.js.map