feat: system view with star, planets, gates, concrete ships

This commit is contained in:
2026-06-12 14:56:06 +00:00
parent 61244453e3
commit 881518417a
2 changed files with 182 additions and 3 deletions
+23 -3
View File
@@ -1,17 +1,37 @@
import { createWorld, advance } from './sim/world.js';
import { createRenderer } from './render/app.js';
import { GalaxyView } from './render/galaxyView.js';
import { goTo } from './sim/player.js';
import { SystemView } from './render/systemView.js';
import { goTo, playerSystem } from './sim/player.js';
const world = createWorld('neon-1');
const renderer = await createRenderer(document.getElementById('stage'));
const galaxyView = new GalaxyView(renderer, world, {
const handlers = {
onSelectSystem: (id) => console.log('select system', id),
onJump: (laneId) => goTo(world, { laneId }),
onGotoPlanet: (planetId) => goTo(world, { planetId }),
};
const galaxyView = new GalaxyView(renderer, world, handlers);
const systemView = new SystemView(renderer, world, handlers);
let view = 'system';
function setView(name) {
view = name;
galaxyView.setVisible(name === 'galaxy');
systemView.setVisible(name === 'system');
}
setView('system');
window.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
setView(view === 'galaxy' ? 'system' : 'galaxy');
}
});
const DAYS_PER_SEC = 0.25;
renderer.app.ticker.add(() => {
advance(world, world.t + (renderer.app.ticker.deltaMS / 1000) * DAYS_PER_SEC);
galaxyView.update();
if (view === 'galaxy') galaxyView.update();
else if (playerSystem(world) != null) systemView.update();
});
+159
View File
@@ -0,0 +1,159 @@
import { Container, Graphics, Text } from 'pixi.js';
import { shipsIn } from '../sim/npc.js';
import { playerSystem } from '../sim/player.js';
import { otherEnd } from '../sim/galaxy.js';
const GATE_RADIUS = 290;
export class SystemView {
constructor(renderer, world, { onGotoPlanet, onJump }) {
this.renderer = renderer;
this.world = world;
this.onGotoPlanet = onGotoPlanet;
this.onJump = onJump;
this.container = new Container();
this.labelContainer = new Container();
renderer.world.addChild(this.container);
renderer.labels.addChild(this.labelContainer);
this.staticG = new Graphics();
this.shipG = new Graphics();
this.container.addChild(this.staticG, this.shipG);
this.hitNodes = new Container();
this.container.addChild(this.hitNodes);
this.builtFor = null;
}
center() {
return { x: this.renderer.app.renderer.width / 2, y: this.renderer.app.renderer.height / 2 };
}
planetXY(planet) {
const c = this.center();
return { x: c.x + Math.cos(planet.orbit.a) * planet.orbit.r, y: c.y + Math.sin(planet.orbit.a) * planet.orbit.r };
}
gateXY(lane, systemId) {
const { galaxy } = this.world;
const here = galaxy.systems[systemId];
const there = galaxy.systems[otherEnd(lane, systemId)];
const ang = Math.atan2(there.y - here.y, there.x - here.x);
const c = this.center();
return { x: c.x + Math.cos(ang) * GATE_RADIUS, y: c.y + Math.sin(ang) * GATE_RADIUS, ang };
}
build(systemId) {
this.builtFor = systemId;
const { galaxy } = this.world;
const sys = galaxy.systems[systemId];
const c = this.center();
const g = this.staticG;
g.clear();
this.labelContainer.removeChildren();
this.hitNodes.removeChildren();
g.circle(c.x, c.y, 30).fill({ color: 0xffe9a8 }); // the star, brightest thing on screen
for (const planetId of sys.planets) {
const planet = galaxy.planets[planetId];
const p = this.planetXY(planet);
g.circle(c.x, c.y, planet.orbit.r).stroke({ width: 1, color: 0x1b3a55, alpha: 0.6 });
g.circle(p.x, p.y, 10).fill({ color: 0x7fb4ff });
const label = new Text({
text: `${planet.name} (${planet.kind})`,
style: { fill: '#7fa8c9', fontSize: 11, fontFamily: 'system-ui' },
});
label.position.set(p.x + 14, p.y - 6);
this.labelContainer.addChild(label);
const hit = new Graphics().circle(0, 0, 18).fill({ color: 0xffffff, alpha: 0.001 });
hit.position.set(p.x, p.y);
hit.eventMode = 'static';
hit.cursor = 'pointer';
hit.on('pointertap', () => this.onGotoPlanet(planetId));
this.hitNodes.addChild(hit);
}
for (const laneId of sys.lanes) {
const lane = galaxy.lanes[laneId];
const gp = this.gateXY(lane, systemId);
g.poly([gp.x, gp.y - 8, gp.x + 8, gp.y, gp.x, gp.y + 8, gp.x - 8, gp.y]).fill({ color: 0x9fd8ff });
const name = galaxy.systems[otherEnd(lane, systemId)].name;
const label = new Text({ text: `${name}`, style: { fill: '#7fa8c9', fontSize: 11, fontFamily: 'system-ui' } });
label.position.set(gp.x + 10, gp.y - 6);
this.labelContainer.addChild(label);
const hit = new Graphics().circle(0, 0, 16).fill({ color: 0xffffff, alpha: 0.001 });
hit.position.set(gp.x, gp.y);
hit.eventMode = 'static';
hit.cursor = 'pointer';
hit.on('pointertap', () => this.onJump(laneId));
this.hitNodes.addChild(hit);
}
}
// A ship triangle pointing along its heading.
drawShip(g, x, y, ang, color) {
const cos = Math.cos(ang), sin = Math.sin(ang);
const pt = (dx, dy) => [x + dx * cos - dy * sin, y + dx * sin + dy * cos];
g.poly([...pt(10, 0), ...pt(-7, 6), ...pt(-7, -6)]).fill({ color });
}
update() {
const sim = this.world;
const sysId = playerSystem(sim);
if (sysId == null) return;
if (this.builtFor !== sysId) this.build(sysId);
const { galaxy } = sim;
const c = this.center();
const g = this.shipG;
g.clear();
// Concrete NPC ships.
for (const entry of shipsIn(sim, sysId)) {
if (entry.kind === 'docked') {
const p = this.planetXY(galaxy.planets[entry.planetId]);
this.drawShip(g, p.x + 18, p.y - 14, -Math.PI / 2, 0xffa94d);
} else if (entry.kind === 'arriving' || entry.kind === 'departing') {
const lane = galaxy.lanes[entry.laneId];
const gp = this.gateXY(lane, sysId);
const dest = entry.npc.plan ? galaxy.planets[entry.npc.plan.destPlanet] : null;
const target = dest && dest.systemId === sysId ? this.planetXY(dest) : c;
const f = entry.kind === 'arriving' ? entry.progress : 1 - entry.progress;
const x = gp.x + (target.x - gp.x) * f * 0.5;
const y = gp.y + (target.y - gp.y) * f * 0.5;
const ang = entry.kind === 'arriving'
? Math.atan2(target.y - gp.y, target.x - gp.x)
: Math.atan2(gp.y - target.y, gp.x - target.x);
this.drawShip(g, x, y, ang, 0xffa94d);
} else if (entry.kind === 'local-hop') {
const from = this.planetXY(galaxy.planets[entry.npc.plan.originPlanet]);
const to = this.planetXY(galaxy.planets[entry.npc.plan.destPlanet]);
const x = from.x + (to.x - from.x) * entry.progress;
const y = from.y + (to.y - from.y) * entry.progress;
this.drawShip(g, x, y, Math.atan2(to.y - from.y, to.x - from.x), 0xffa94d);
}
}
// The player.
const loc = sim.player.loc;
if (loc.kind === 'docked') {
const p = this.planetXY(galaxy.planets[loc.planetId]);
this.drawShip(g, p.x - 18, p.y - 14, -Math.PI / 2, 0x4dffd2);
} else if (loc.kind === 'at-gate') {
const gp = this.gateXY(galaxy.lanes[loc.laneId], sysId);
this.drawShip(g, gp.x, gp.y - 16, -Math.PI / 2, 0x4dffd2);
} else if (loc.kind === 'insys-leg') {
const resolve = (d) => d.type === 'planet'
? this.planetXY(galaxy.planets[d.id])
: this.gateXY(galaxy.lanes[d.laneId], sysId);
const from = resolve(loc.from), to = resolve(loc.to);
const f = Math.min(1, (sim.t - loc.departT) / (loc.arriveT - loc.departT));
const x = from.x + (to.x - from.x) * f;
const y = from.y + (to.y - from.y) * f;
this.drawShip(g, x, y, Math.atan2(to.y - from.y, to.x - from.x), 0x4dffd2);
}
}
setVisible(on) {
this.container.visible = on;
this.labelContainer.visible = on;
}
}