051a111855
- Wrap main.js body in async main() to eliminate top-level await deadlock in Rollup - Add npc.lastPlan so clouds persist after hidden arrival/destruction until analytic envelope expires - Add maxEnvelopeArrival() helper; rewrite playerClouds() to use lastPlan for envelope-gated clouds - Block markSeen() while player is mid-lane (no observation from hyperspace) - Pass obsSys=null to playerClouds when player is in a lane (don't exclude visible band from hyperspace) - Galaxy map clicks now select-only (intel); jumps via Nav buttons / gate diamonds only - Add preview script, favicon no-op, .playwright-mcp/ and *.png to .gitignore, update README controls text
124 lines
4.2 KiB
JavaScript
124 lines
4.2 KiB
JavaScript
import { Container, Graphics, Text } from 'pixi.js';
|
|
import { hazardColor, makeViewTransform } from './app.js';
|
|
import { playerClouds } from '../sim/clouds.js';
|
|
import { playerSystem } from '../sim/player.js';
|
|
import { otherEnd } from '../sim/galaxy.js';
|
|
|
|
export class GalaxyView {
|
|
// onSelectSystem(systemId) -> UI intel panel
|
|
constructor(renderer, world, { onSelectSystem, onJump }) {
|
|
this.renderer = renderer;
|
|
this.world = world;
|
|
this.onSelectSystem = onSelectSystem;
|
|
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.cloudG = new Graphics();
|
|
this.dynamicG = new Graphics();
|
|
this.container.addChild(this.staticG, this.cloudG, this.dynamicG);
|
|
this.hitNodes = new Container();
|
|
this.container.addChild(this.hitNodes);
|
|
this.buildStatic();
|
|
}
|
|
|
|
buildStatic() {
|
|
const { galaxy } = this.world;
|
|
const v = makeViewTransform(this.renderer.app);
|
|
this.v = v;
|
|
const g = this.staticG;
|
|
g.clear();
|
|
for (const lane of galaxy.lanes) {
|
|
const a = galaxy.systems[lane.a], b = galaxy.systems[lane.b];
|
|
g.moveTo(v.x(a.x), v.y(a.y)).lineTo(v.x(b.x), v.y(b.y))
|
|
.stroke({ width: 2, color: hazardColor(lane.hazard), alpha: 0.55 });
|
|
}
|
|
for (const s of galaxy.systems) {
|
|
g.circle(v.x(s.x), v.y(s.y), 7).fill({ color: 0xcfeeff });
|
|
}
|
|
this.labelContainer.removeChildren();
|
|
this.hitNodes.removeChildren();
|
|
for (const s of galaxy.systems) {
|
|
const label = new Text({ text: s.name, style: { fill: '#7fa8c9', fontSize: 11, fontFamily: 'system-ui' } });
|
|
label.position.set(v.x(s.x) + 10, v.y(s.y) - 6);
|
|
this.labelContainer.addChild(label);
|
|
|
|
const hit = new Graphics().circle(0, 0, 16).fill({ color: 0xffffff, alpha: 0.001 });
|
|
hit.position.set(v.x(s.x), v.y(s.y));
|
|
hit.eventMode = 'static';
|
|
hit.cursor = 'pointer';
|
|
hit.on('pointertap', () => this.handleSystemTap(s.id));
|
|
this.hitNodes.addChild(hit);
|
|
}
|
|
}
|
|
|
|
handleSystemTap(systemId) {
|
|
this.onSelectSystem(systemId);
|
|
}
|
|
|
|
lanePoint(lane, fromSys, f) {
|
|
const { galaxy } = this.world;
|
|
const a = galaxy.systems[fromSys];
|
|
const b = galaxy.systems[otherEnd(lane, fromSys)];
|
|
return { x: this.v.x(a.x + (b.x - a.x) * f), y: this.v.y(a.y + (b.y - a.y) * f) };
|
|
}
|
|
|
|
update() {
|
|
const sim = this.world;
|
|
const v = this.v;
|
|
const cur = playerSystem(sim);
|
|
// Mid-lane the player observes nothing; don't exclude any visible band.
|
|
const obsSys = sim.player.loc.kind === 'lane' ? null : cur;
|
|
|
|
// Probability clouds: glowing strokes along lane segments, alpha ~ weight.
|
|
const cg = this.cloudG;
|
|
cg.clear();
|
|
for (const cloud of playerClouds(sim, obsSys)) {
|
|
for (const seg of cloud.segs) {
|
|
const lane = sim.galaxy.lanes[seg.laneId];
|
|
const p0 = this.lanePoint(lane, seg.fromSys, seg.f0);
|
|
const p1 = this.lanePoint(lane, seg.fromSys, seg.f1);
|
|
const alpha = Math.min(0.85, 0.18 + seg.w * 0.9);
|
|
cg.moveTo(p0.x, p0.y).lineTo(p1.x, p1.y)
|
|
.stroke({ width: 7, color: 0xffa94d, alpha });
|
|
}
|
|
}
|
|
|
|
// Player marker: bright cyan triangle at current position.
|
|
const dg = this.dynamicG;
|
|
dg.clear();
|
|
const pos = this.playerXY();
|
|
if (pos) {
|
|
dg.poly([pos.x, pos.y - 8, pos.x + 6, pos.y + 6, pos.x - 6, pos.y + 6]).fill({ color: 0x4dffd2 });
|
|
}
|
|
// Halo on the player's current system.
|
|
if (cur != null) {
|
|
const s = sim.galaxy.systems[cur];
|
|
dg.circle(v.x(s.x), v.y(s.y), 12).stroke({ width: 2, color: 0x4dffd2, alpha: 0.8 });
|
|
}
|
|
}
|
|
|
|
playerXY() {
|
|
const sim = this.world;
|
|
const loc = sim.player.loc;
|
|
const v = this.v;
|
|
if (loc.kind === 'lane') {
|
|
const lane = sim.galaxy.lanes[loc.laneId];
|
|
const f = Math.min(1, (sim.t - loc.departT) / (loc.arriveT - loc.departT));
|
|
return this.lanePoint(lane, loc.fromSys, f);
|
|
}
|
|
const sysId = playerSystem(sim);
|
|
if (sysId == null) return null;
|
|
const s = sim.galaxy.systems[sysId];
|
|
return { x: v.x(s.x), y: v.y(s.y) };
|
|
}
|
|
|
|
setVisible(on) {
|
|
this.container.visible = on;
|
|
this.labelContainer.visible = on;
|
|
}
|
|
}
|