orbit/dev/station-shot.mjs

221 lines
8.3 KiB
JavaScript

/**
* Dev: the deep-space station VARIANTS (spacestations.png, frames 0..2)
* end-to-end — the sprite in-world, the comms panel (the variant label,
* REQUEST LANDING live), and the station landing flow (the ss-land /
* ss-takeoff clips, the silent flat deck — surface/shop clips are
* authored later):
*
* world — a deep-space station in its system, framed (the variant art
* the spread pass assigned it — settlement.stationFrame)
* panel — the comms panel on that station (variant label + REQUEST
* LANDING enabled)
* land — startLanding through the scene's real path (SurfaceScene up,
* the station's land clip queued under the station key)
* deck — the surface deck on a station (no surface clip — the flat
* deck; SHOP is a "no shop" note)
*
* The starting system (VESTRA — Oribreicor) holds a deep-space station
* ("Terminal 11", variant 1 — the smuggler's bar), so all four states are
* one system.
*
* node dev/server.mjs 8080
* node dev/wdshot.mjs http://localhost:8080/dev/station-shot.html /tmp/station-world.png \
* 'return await window.__STATION_SHOT__("world");' 40000 5000
* node dev/wdshot.mjs http://localhost:8080/dev/station-shot.html /tmp/station-panel.png \
* 'return await window.__STATION_SHOT__("panel");' 40000 5000
* node dev/wdshot.mjs http://localhost:8080/dev/station-shot.html /tmp/station-land.png \
* 'return await window.__STATION_SHOT__("land");' 40000 5000
* node dev/wdshot.mjs http://localhost:8080/dev/station-shot.html /tmp/station-deck.png \
* 'return await window.__STATION_SHOT__("deck");' 40000 5000
*/
import Phaser from '../js/vendor/phaser.js';
import { config } from '../js/config/Config.js';
import { ConfigLoader } from '../js/config/ConfigLoader.js';
import { createGameConfig } from '../js/config/GameConfig.js';
import { GameScene } from '../js/scenes/GameScene.js';
import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
const data = await ConfigLoader.load();
config.init(data);
// The game measures its UI text in the web font — wait for it BEFORE the
// scenes build any text (same contract as js/main.js).
const __fonts = globalThis.document?.fonts;
if (__fonts && typeof __fonts.load === 'function') {
const fams = ['header', 'body']
.map((k) => config.get(`theme.fonts.${k}.family`))
.filter((f) => typeof f === 'string' && f.length > 0);
if (fams.length > 0) {
await Promise.race([
Promise.allSettled(fams.map((f) => __fonts.load(`16px "${f}"`).catch(() => {}))),
new Promise((r) => setTimeout(r, 2000)),
]);
}
}
// The deterministic starting system (VESTRA — Oribreicor holds a
// deep-space station — variant 1, "Terminal 11"). A different starting
// system (and thus station variant) can be pinned per run:
// http://localhost:8080/dev/station-shot.html?seed=probe-0 (variant 0)
// http://localhost:8080/dev/station-shot.html?seed=probe-25 (variant 2)
globalThis.__ORBIT_DEV_SEED =
(globalThis.location?.search && new URLSearchParams(globalThis.location.search).get('seed')) || 'VESTRA';
const gameConfig = createGameConfig();
gameConfig.scene = [GameScene, SurfaceScene]; // GameScene boots first
if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true;
const game = new Phaser.Game(gameConfig);
window.game = game;
/**
* Drive the station-variant states through the scene's real paths and
* report a result object for dev/wdshot.mjs to print.
*/
window.__STATION_SHOT__ = async (which = 'world') => {
const g = window.game;
const s = g.scene.getScene('GameScene');
if (!s || !s.systemStations) return { ready: false, note: s ? 'scene not ready (yet?)' : 'no scene' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// ---- The station (the system's deep-space station) -------------------
const st = s.systemStations.find((o) => o.kind === 'deepSpaceStation') ?? s.systemStations[0] ?? null;
if (!st) return { ready: false, note: 'no free-space station in this system' };
// ---- Camera: frame the station at screen (640, 430) ------------------
const sx = 640;
const sy = 430;
s.ship.stop();
s.hideHint?.();
const cam = s.cameras.main;
if (which === 'world') {
// Park the ship just off the rim (it was flying there), lower-right.
const rr = st.radius + 70;
s.ship.x = st.x + rr * Math.cos(0.5);
s.ship.y = st.y + rr * Math.sin(0.5);
cam.scrollX = st.x - sx;
cam.scrollY = st.y - sy;
s.cameraFollowShip = false;
for (let i = 0; i < 200; i++) g.loop.step(performance.now());
cam.scrollX = st.x - sx;
cam.scrollY = st.y - sy;
const r = {
ready: true,
which,
station: st.discoveryName,
kind: st.kind,
size: st.size,
sheetFrame: st.sheetFrame,
variantLabel: config.get(`stations.typeLabels.${st.sheetFrame}`),
childType: st.list?.[0]?.type ?? st.list?.[0]?.constructor?.name,
childTexture: st.list?.[0]?.texture?.key ?? null,
childScale: st.list?.[0]?.scale,
comms: s.commsTargetFor(st),
};
window.__STATION_SHOT__RESULT__ = r;
return r;
}
if (which === 'panel') {
s.ship.x = st.x + (st.radius + 70) * Math.cos(0.5);
s.ship.y = st.y + (st.radius + 70) * Math.sin(0.5);
cam.scrollX = st.x - sx;
cam.scrollY = st.y - sy;
s.openCommsPanel(st, { worldX: st.x, worldY: st.y, x: sx, y: sy });
s.cameraFollowShip = false;
const done = () => s.commsPanel.alpha >= 0.999 && s.commsPanel.nameDec === null && s.commsPanel.repRevealT0 === null;
for (let i = 0; i < 6000 && !done(); i++) g.loop.step(performance.now());
cam.scrollX = st.x - sx;
cam.scrollY = st.y - sy;
const cp = s.commsPanel;
const r = {
ready: true,
which,
station: st.discoveryName,
sheetFrame: st.sheetFrame,
name: cp.nameText.text,
kindLabel: cp.settledText?.text ?? null,
settled: cp.settled,
btn1: cp.btn1.text.text,
btn1Disabled: cp.btn1.disabled,
btn2: cp.btn2.text.text,
onScreen: cp.rect
? (() => {
const lx = cp.rect.x - cam.scrollX;
const ly = cp.rect.y - cam.scrollY;
return lx >= -1 && ly >= -1 && lx + cp.rect.w <= s.scale.width + 1 && ly + cp.rect.h <= s.scale.height + 1;
})()
: false,
};
window.__STATION_SHOT__RESULT__ = r;
return r;
}
if (which === 'land') {
// The real entry point — the comms panel's own REQUEST LANDING path.
const target = s.commsTargetFor(st);
if (!target?.isStation || !target?.canLand) {
const r = { ready: false, which, target };
window.__STATION_SHOT__RESULT__ = r;
return r;
}
s.startLanding(target);
await sleep(300);
const ss = g.scene.getScene('SurfaceScene');
const landKey = `__surf_st_land_${ss.planetFrame}`;
const r = {
ready: true,
which,
station: st.discoveryName,
isStation: ss.isStation,
planetFrame: ss.planetFrame,
phase: ss.phase,
musicSpec: ss.musicSpec,
landKey,
landCached: ss.cache.video.has(landKey),
landVideo: ss.landVideo !== null,
gsSleeping: s.sys.isSleeping(),
};
window.__STATION_SHOT__RESULT__ = r;
return r;
}
if (which === 'deck') {
// Land, then force the deck (the station has NO surface clip — the
// flat deck is the station's surface state today).
const target = s.commsTargetFor(st);
if (!target?.isStation || !target?.canLand) {
const r = { ready: false, which, target };
window.__STATION_SHOT__RESULT__ = r;
return r;
}
s.startLanding(target);
await sleep(300);
const ss = g.scene.getScene('SurfaceScene');
ss.startSurface();
await sleep(300);
const ids = (ss.actionBar?.slots ?? []).map((slot) => slot.id);
const loopKey = `__surf_st_loop_${ss.planetFrame}`;
const r = {
ready: true,
which,
station: st.discoveryName,
isStation: ss.isStation,
planetFrame: ss.planetFrame,
phase: ss.phase,
deckSlots: ids,
loopKey,
loopCached: ss.cache.video.has(loopKey),
surfaceVideo: ss.surfaceVideo !== null,
hudName: ss.hudName?.text ?? null,
hudLine: ss.hudLine?.text ?? null,
};
window.__STATION_SHOT__RESULT__ = r;
return r;
}
const r = { ready: false, which, note: 'unknown mode' };
window.__STATION_SHOT__RESULT__ = r;
return r;
};