
I forgot what I actually changed. It may not even be playable, I just want to get this up there.
3190 lines
69 KiB
JavaScript
3190 lines
69 KiB
JavaScript
// 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
|