// 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