Add upper-left surface HUD showing planet name, type, and tether level
- GameScene now passes the world's type label and tether level into the SurfaceScene launch payload; new tetherLevelFor() resolves a world's anchored tether (by label or anchor position) for that line. - SurfaceScene builds/decodes a two-line HUD (Ethnocentric name in the menu title shade with black stroke, Centauri line below) while the surface clip loops, driven by the shared scramble decode; it is torn down on take-off and shutdown. - landing.json gains a hud config block for margins, fonts, spacing, strokes, and colors. - Added dev-only harnesses (surface-hud.html/.mjs test page and surface-hud-shot.html/.mjs screenshot probe) to verify the HUD styling, decode settle, payload, and teardown in headless Firefox.
This commit is contained in:
parent
1cbbf502df
commit
cd4d751edd
|
|
@ -21,6 +21,21 @@
|
|||
"enabled": true,
|
||||
"videoBase": "assets/videos/",
|
||||
"volume": 1,
|
||||
"_hud_comment": "The upper-left HUD while the surface clip loops: the planet's NAME (Ethnocentric, the SAME shade the menu's game title uses — data/menu.json → colors.title — with a black stroke behind it so it holds over any clip) and, beneath it in the body face (Centauri), the planet TYPE + TETHER LEVEL (\"Terran World, Tether Level 1\"). Both decode in with the shared scramble (the name first). marginX/marginY = corner offsets (px); name/line = font sizes, letter spacing (px), stroke color/thickness, and line color (the theme's dim by default).",
|
||||
"hud": {
|
||||
"marginX": 24,
|
||||
"marginY": 20,
|
||||
"nameFontSize": 32,
|
||||
"nameLetterSpacing": 2,
|
||||
"nameStroke": "#000000",
|
||||
"nameStrokeWidth": 4,
|
||||
"lineGap": 10,
|
||||
"lineFontSize": 16,
|
||||
"lineLetterSpacing": 1,
|
||||
"lineColor": "#7d92c4",
|
||||
"lineStroke": "#000000",
|
||||
"lineStrokeWidth": 2
|
||||
},
|
||||
"videos": [
|
||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4" },
|
||||
{ "land": "terran-land-02.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-02.mp4" },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev surface HUD shot</title>
|
||||
<base href="../" />
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/surface-hud-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Visual probe for the surface HUD screenshot — PUMP-DRIVEN (like
|
||||
* dev/landing-click.mjs): this box's headless Firefox freezes rAF/timers
|
||||
* after first paint, so the page advances only when the runner's poll
|
||||
* calls __HUD_TICK__(), which steps the engine loop once. It parks on
|
||||
* the surface stage with the upper-left HUD decoded, then HOLDS
|
||||
* (__HUD_SHOT__ = 'surface') so dev/shot-firefox.mjs can grab the frame.
|
||||
*
|
||||
* python3 -m http.server 8131
|
||||
* node dev/shot-firefox.mjs http://127.0.0.1:8131/dev/surface-hud-shot.html \
|
||||
* 'window.__HUD_TICK__(); window.__HUD_SHOT__ === "surface" ? window.__HUD_SHOT__ : null' \
|
||||
* /tmp/surface-hud.png 60000
|
||||
*/
|
||||
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';
|
||||
|
||||
window.__HUD_SHOT__ = 'booting';
|
||||
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
const gameConfig = createGameConfig();
|
||||
gameConfig.scene = [GameScene, SurfaceScene];
|
||||
const game = new Phaser.Game(gameConfig);
|
||||
window.game = game;
|
||||
|
||||
const scene = (key) => { try { return game.scene?.getScene(key) ?? null; } catch { return null; } };
|
||||
|
||||
let stage = 0;
|
||||
function tick() {
|
||||
// v4: do NOT step the loop before boot completes — `game.scene` is
|
||||
// created during boot, and pumping loop.step() before that wedges boot.
|
||||
if (!game.loop || !game.scene) return;
|
||||
try {
|
||||
game.loop.step(performance.now());
|
||||
|
||||
const gs = scene('GameScene');
|
||||
const ss = scene('SurfaceScene');
|
||||
|
||||
switch (stage) {
|
||||
case 0: // boot: the flight scene is live
|
||||
if (!(gs && gs.ship)) return;
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet)); // the real entry point
|
||||
stage = 1;
|
||||
return;
|
||||
|
||||
case 1: // the surface scene is up → force the loop stage
|
||||
if (!(ss && ss.sys.isActive())) return;
|
||||
ss.startSurface();
|
||||
if (!ss.hudName) ss.buildHud(); // headless: the loop clip may never cache
|
||||
if (ss.surfaceVideo && ss.surfaceVideo.video) {
|
||||
ss.surfaceVideo.video.muted = true; // headless autoplay policy
|
||||
ss.surfaceVideo.video.play().catch(() => {});
|
||||
}
|
||||
stage = 2;
|
||||
return;
|
||||
|
||||
case 2: // the HUD's decode has settled → HOLD for the screenshot
|
||||
if (ss.hudLines !== null) return;
|
||||
window.__HUD_SHOT__ = 'surface';
|
||||
window.__HUD_DIAG__ = {
|
||||
gs: gs.sys.getStatus(),
|
||||
ss: ss.sys.getStatus(),
|
||||
ssPhase: ss.phase,
|
||||
loopCached: ss.cache.video.has(`__surf_loop_${ss.planetFrame}`),
|
||||
surfaceVideo: !!ss.surfaceVideo,
|
||||
hudName: ss.hudName?.text,
|
||||
hudLine: ss.hudLine?.text,
|
||||
nameStyle: ss.hudName ? [ss.hudName.style.fontFamily, ss.hudName.style.color, ss.hudName.style.stroke, ss.hudName.style.strokeThickness] : null,
|
||||
lineStyle: ss.hudLine ? [ss.hudLine.style.fontFamily, ss.hudLine.style.color, ss.hudLine.style.stroke] : null,
|
||||
deckIds: (ss.actionBar?.slots ?? []).map((s) => s.id),
|
||||
};
|
||||
stage = 3;
|
||||
}
|
||||
} catch (err) {
|
||||
window.__HUD_SHOT__ = 'FAILED: ' + String(err?.message ?? err);
|
||||
stage = 99;
|
||||
}
|
||||
}
|
||||
window.__HUD_TICK__ = tick;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev surface HUD test</title>
|
||||
<!-- This page lives in /dev, but the game's relative asset paths are rooted
|
||||
at the project root — resolve them against it. ("../" keeps this
|
||||
working even if the project is served from a subdirectory.) -->
|
||||
<base href="../" />
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/surface-hud.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* Dev-only SURFACE HUD test: boots into the GameScene, lands on the home
|
||||
* world, and checks the new upper-left HUD (SurfaceScene.buildHud):
|
||||
*
|
||||
* - the launch payload carries the world's type + tether level
|
||||
* - the name text: Ethnocentric (header face), the menu title's shade
|
||||
* (data/menu.json → colors.title), a black stroke behind it
|
||||
* - the line below: "TYPE, TETHER LEVEL N" in the body face (Centauri)
|
||||
* - both lines decode in (the shared scramble) and settle on the
|
||||
* final strings
|
||||
* - destroyHud() tears it down (take-off/shutdown path)
|
||||
*
|
||||
* python3 -m http.server 8124
|
||||
* node dev/cdp-firefox.mjs http://127.0.0.1:8124/dev/surface-hud.html \
|
||||
* 'return window.__SURFACE_HUD_TEST__;'
|
||||
*/
|
||||
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 fail = (msg, extra = {}) => {
|
||||
window.__SURFACE_HUD_TEST__ = { pass: false, error: msg, diag: extra };
|
||||
console.error('[surface-hud] FAIL:', msg, extra);
|
||||
};
|
||||
window.__FLOW_STARTED__ = true;
|
||||
const step = (name) => {
|
||||
window.__STEPS__ = (window.__STEPS__ ?? []).concat(name);
|
||||
};
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
const gameConfig = createGameConfig();
|
||||
gameConfig.scene = [GameScene, SurfaceScene];
|
||||
const game = new Phaser.Game(gameConfig);
|
||||
window.game = game;
|
||||
|
||||
(async () => {
|
||||
// 1 — the flight scene is live (v4 boots asynchronously).
|
||||
let gs = null;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
gs = game.scene?.getScene('GameScene') ?? null;
|
||||
if (gs && gs.ship) break;
|
||||
await sleep(100);
|
||||
}
|
||||
if (!gs || !gs.ship) throw new Error('GameScene never came up');
|
||||
step('boot');
|
||||
|
||||
// 2 — the tether lookup the launch payload relies on.
|
||||
const target = gs.commsTargetFor(gs.planet);
|
||||
const level = gs.tetherLevelFor(target.name);
|
||||
if (!(level === gs.tetherField.tethers.find((t) => t.label === target.name)?.level))
|
||||
throw new Error('tetherLevelFor mismatch', { level, tethers: gs.tetherField.tethers });
|
||||
if (gs.tetherLevelFor('NOT A PLANET') !== 0) throw new Error('unknown world must be 0');
|
||||
step('tether-level-lookup');
|
||||
|
||||
// 3 — the real entry point: the launch payload carries type + level.
|
||||
gs.startLanding(target);
|
||||
await sleep(250);
|
||||
const ss = game.scene.getScene('SurfaceScene');
|
||||
if (!ss || !ss.sys.isActive()) throw new Error('SurfaceScene did not launch');
|
||||
if (ss.planetType !== target.kindLabel) throw new Error('planetType', { got: ss.planetType, want: target.kindLabel });
|
||||
if (ss.tetherLevel !== level) throw new Error('tetherLevel', { got: ss.tetherLevel, want: level });
|
||||
step('launch-payload', { name: ss.planetName, type: ss.planetType, level: ss.tetherLevel });
|
||||
|
||||
// 4 — the HUD exists: auto-built with the loop clip (startSurface), or
|
||||
// built directly when headless never cached the video.
|
||||
ss.startSurface();
|
||||
await sleep(50);
|
||||
if (!ss.hudName) ss.buildHud();
|
||||
if (!ss.hudName || !ss.hudLine || !ss.hudLines) throw new Error('HUD missing after buildHud');
|
||||
step('hud-built');
|
||||
|
||||
// 5 — the decode settles on the final strings (poll a few seconds).
|
||||
const nameFinal = String(ss.planetName).toUpperCase();
|
||||
const lineFinal = `${ss.planetType}, Tether Level ${ss.tetherLevel}`;
|
||||
let settled = false;
|
||||
for (let i = 0; i < 200 && !settled; i++) {
|
||||
await sleep(100);
|
||||
if (ss.hudLines === null && ss.hudName.text === nameFinal && ss.hudLine.text === lineFinal) settled = true;
|
||||
}
|
||||
if (!settled) throw new Error('decode did not settle', {
|
||||
name: ss.hudName?.text, line: ss.hudLine?.text, hudLines: !!ss.hudLines,
|
||||
});
|
||||
step('decode-settled', { name: ss.hudName.text, line: ss.hudLine.text });
|
||||
|
||||
// 6 — the styling contract.
|
||||
const nStyle = ss.hudName.style;
|
||||
const lStyle = ss.hudLine.style;
|
||||
if (!/Ethnocentric/.test(nStyle.fontFamily)) throw new Error('name font', { got: nStyle.fontFamily });
|
||||
if (!/Centauri/.test(lStyle.fontFamily)) throw new Error('line font', { got: lStyle.fontFamily });
|
||||
const titleCss = '#' + (config.get('menu.colors.title', '#eaf6ff').slice(1) || 'eaf6ff');
|
||||
if (nStyle.color.toLowerCase() !== titleCss) throw new Error('name shade', { got: nStyle.color, want: titleCss });
|
||||
if (nStyle.stroke.toLowerCase() !== '#000000' || nStyle.strokeThickness !== 4)
|
||||
throw new Error('name stroke', { got: [nStyle.stroke, nStyle.strokeThickness] });
|
||||
if (lStyle.stroke.toLowerCase() !== '#000000') throw new Error('line stroke', { got: lStyle.stroke });
|
||||
if (!(ss.hudName.x >= 16 && ss.hudName.y >= 12)) throw new Error('corner placement', { x: ss.hudName.x, y: ss.hudName.y });
|
||||
if (!(ss.hudLine.y > ss.hudName.y)) throw new Error('line sits below the name');
|
||||
step('styling');
|
||||
|
||||
// 7 — teardown (the take-off/shutdown path).
|
||||
ss.destroyHud();
|
||||
if (ss.hudName !== null || ss.hudLine !== null || ss.hudLines !== null) throw new Error('destroyHud left state');
|
||||
step('teardown');
|
||||
|
||||
window.__SURFACE_HUD_TEST__ = { pass: true, steps: window.__STEPS__ };
|
||||
console.info('[surface-hud] PASS:', window.__STEPS__.join(' → '));
|
||||
})().catch((err) => fail(String(err && err.message || err), {
|
||||
gs: game.scene?.getScene('GameScene')?.sys.getStatus?.(),
|
||||
ss: game.scene?.getScene('SurfaceScene') ? { phase: game.scene.getScene('SurfaceScene').phase } : null,
|
||||
}));
|
||||
|
|
@ -1279,8 +1279,9 @@ export class GameScene extends Phaser.Scene {
|
|||
/**
|
||||
* LAND (commsAction) — the surface sequence: SurfaceScene starts ON
|
||||
* TOP of this scene, handed the world's planets.png sheet frame so it
|
||||
* can pick the landing/surface videos from data/landing.json. v4
|
||||
* note: `launch` does not freeze the caller, so sleep the flight
|
||||
* can pick the landing/surface videos from data/landing.json, plus
|
||||
* the world's type label and tether level for its upper-left HUD.
|
||||
* v4 note: `launch` does not freeze the caller, so sleep the flight
|
||||
* world explicitly — Take Off (SurfaceScene) wakes it back, with the
|
||||
* ship, tethers and discovery exactly where they were.
|
||||
*/
|
||||
|
|
@ -1295,10 +1296,31 @@ export class GameScene extends Phaser.Scene {
|
|||
this.scene.launch('SurfaceScene', {
|
||||
frame: Number(target.frame ?? 0),
|
||||
name: target.name,
|
||||
type: target.kindLabel ?? '',
|
||||
tetherLevel: this.tetherLevelFor(target.name),
|
||||
});
|
||||
this.scene.sleep();
|
||||
}
|
||||
|
||||
/**
|
||||
* The tether LEVEL anchored on this world (the surface HUD's second
|
||||
* line — "Tether Level N"): the tether whose label is the world's
|
||||
* name, or whose anchor sits on the world's center (the home tether
|
||||
* is anchored at the origin with the home world's name). 0 = no
|
||||
* tether on this world yet.
|
||||
*/
|
||||
tetherLevelFor(name) {
|
||||
if (!name || !this.tetherField) return 0;
|
||||
const obj =
|
||||
this.systemPlanets.find((p) => p.discoveryName === name) ??
|
||||
(name === this.planet?.discoveryName ? this.planet : null);
|
||||
if (!obj) return 0;
|
||||
const t = this.tetherField.tethers.find(
|
||||
(tt) => tt.label === name || (tt.x === obj.x && tt.y === obj.y),
|
||||
);
|
||||
return t ? t.level : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mining phase changes (Mining → onPhase): the scene's share of the
|
||||
* sequence — the ship's STATE, the ship lock, the console calls, the sfx.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import Phaser from '../vendor/phaser.js';
|
||||
import { config } from '../config/Config.js';
|
||||
import { toCss } from '../utils/Color.js';
|
||||
import { fontStack, themeColor } from '../utils/Theme.js';
|
||||
import { ScrambleDecode, decodeDur } from '../utils/Decode.js';
|
||||
import { ActionBar } from '../ui/ActionBar.js';
|
||||
import { MenuSubBar } from '../ui/MenuSubBar.js';
|
||||
import { SavePanel } from '../ui/SavePanel.js';
|
||||
|
|
@ -17,7 +20,10 @@ import { SaveManager } from '../save/SaveManager.js';
|
|||
* replaced by SHOP (a seam for now) and TAKE OFF left of MENU
|
||||
* (data/actionbar.json → surface.buttons). Menu keeps the GameScene's
|
||||
* save sub-bar — Save/Load operate on the paused GameScene's state
|
||||
* (SavePanel `stateScene`).
|
||||
* (SavePanel `stateScene`). While the clip loops, the planet's name
|
||||
* decodes in at the upper-left (Ethnocentric, the menu title's shade,
|
||||
* black-stroked) with its type + tether level beneath (Centauri) —
|
||||
* see buildHud(); the HUD dies with the loop clip.
|
||||
*
|
||||
* 3. TAKE OFF — the world's one-shot take-off clip (`takeoff`) plays,
|
||||
* then this scene stops — the paused GameScene resumes exactly where
|
||||
|
|
@ -39,11 +45,17 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
init(data = {}) {
|
||||
this.planetFrame = Number(data.frame ?? 0); // planets.png frame (video key)
|
||||
this.planetName = String(data.name ?? '');
|
||||
this.planetType = String(data.type ?? ''); // e.g. "Rocky World" (planets.typeLabels)
|
||||
this.tetherLevel = Math.max(0, Math.round(Number(data.tetherLevel ?? 0)));
|
||||
this.phase = 'landing'; // 'landing' → 'surface'
|
||||
this.gameScene = null; // the paused GameScene beneath us (save state lives there)
|
||||
this.backdrop = null;
|
||||
this.landVideo = null;
|
||||
this.surfaceVideo = null;
|
||||
this.hudName = null; // upper-left: the planet's name (decodes in)
|
||||
this.hudLine = null; // upper-left: type + tether level (decodes in)
|
||||
this.hudLines = null; // the HUD's decode timeline (updateHud)
|
||||
this.hudT0 = null;
|
||||
this.actionBar = null;
|
||||
this.menuSubBar = null;
|
||||
this.savePanel = null;
|
||||
|
|
@ -151,11 +163,106 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.surfaceVideo = s;
|
||||
// A decode failure on the loop clip keeps the plate — the deck
|
||||
// still works, the world just stays dark.
|
||||
this.buildHud(); // the name + type/tether — only while the clip loops
|
||||
}
|
||||
|
||||
this.buildDeck();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// HUD (upper-left) — the planet's name + type + tether level
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The upper-left HUD, built with the looping surface clip and alive
|
||||
* exactly as long as that clip runs (destroyHud on take-off/shutdown):
|
||||
*
|
||||
* ESHKAELURA ← the planet's name — Ethnocentric
|
||||
* Home World, Tether Level 1 ← the body face (Centauri)
|
||||
*
|
||||
* The name takes the SAME shade the menu's game title uses
|
||||
* (data/menu.json → colors.title, falling back to the theme's ink) with
|
||||
* a black stroke behind it, so it holds over any clip regardless of
|
||||
* what the video shows beneath it. Both lines decode in with the
|
||||
* shared scramble (js/utils/Decode.js) — the name first, the line
|
||||
* beneath it after (the dossier's pacing, GameScene.createSystemHud).
|
||||
*/
|
||||
buildHud() {
|
||||
const hud = config.section('landing.hud', {});
|
||||
const X = hud.marginX ?? 24;
|
||||
const Y = hud.marginY ?? 20;
|
||||
|
||||
const name = String(this.planetName ?? '').toUpperCase();
|
||||
this.hudName = this.add
|
||||
.text(X, Y, '', {
|
||||
fontFamily: fontStack('header'), // Ethnocentric — the theme's header face
|
||||
fontSize: `${hud.nameFontSize ?? 32}px`,
|
||||
color: toCss(config.get('menu.colors.title') ?? themeColor('ink', 0xeaf6ff)),
|
||||
stroke: toCss(hud.nameStroke ?? '#000000'),
|
||||
strokeThickness: hud.nameStrokeWidth ?? 4,
|
||||
letterSpacing: hud.nameLetterSpacing ?? 2,
|
||||
})
|
||||
.setOrigin(0, 0)
|
||||
.setScrollFactor(0) // UI: pinned to the screen
|
||||
.setDepth(60); // above the deck (50) / sub-bar — it never overlaps them
|
||||
|
||||
const parts = [];
|
||||
if (this.planetType) parts.push(this.planetType);
|
||||
parts.push(`Tether Level ${this.tetherLevel}`);
|
||||
const line = parts.join(', ');
|
||||
this.hudLine = this.add
|
||||
.text(X, Y + this.hudName.height + (hud.lineGap ?? 10), '', {
|
||||
fontFamily: fontStack('body'), // Centauri — the theme's body face
|
||||
fontSize: `${hud.lineFontSize ?? 16}px`,
|
||||
color: toCss(hud.lineColor ?? themeColor('dim', 0x7d92c4)),
|
||||
stroke: toCss(hud.lineStroke ?? '#000000'),
|
||||
strokeThickness: hud.lineStrokeWidth ?? 2,
|
||||
letterSpacing: hud.lineLetterSpacing ?? 1,
|
||||
})
|
||||
.setOrigin(0, 0)
|
||||
.setScrollFactor(0) // UI: pinned to the screen
|
||||
.setDepth(60);
|
||||
|
||||
// The decode timeline — updateHud() drives it from the scene's
|
||||
// update() (the engine loop time, same base as the dossier's).
|
||||
this.hudLines = [
|
||||
{ text: this.hudName, value: name, delay: 140, dur: decodeDur(name.length), settled: false },
|
||||
{ text: this.hudLine, value: line, delay: 420, dur: decodeDur(line.length), settled: false },
|
||||
];
|
||||
this.hudT0 = null; // anchored on the first update frame (the clock is stale in create())
|
||||
}
|
||||
|
||||
/** Per-frame: the HUD's decode (driven from update() with the loop time). */
|
||||
updateHud(time) {
|
||||
if (!this.hudLines) return;
|
||||
if (this.hudT0 === null) {
|
||||
this.hudT0 = time;
|
||||
for (const ln of this.hudLines) ln.dec = new ScrambleDecode(ln.value, time + ln.delay, ln.dur);
|
||||
}
|
||||
let done = true;
|
||||
for (const ln of this.hudLines) {
|
||||
if (ln.settled) continue;
|
||||
if (!ln.dec.started(time)) {
|
||||
done = false;
|
||||
continue;
|
||||
}
|
||||
ln.text.setText(ln.dec.display(time));
|
||||
if (ln.dec.finished(time)) ln.settled = true;
|
||||
else done = false;
|
||||
}
|
||||
if (done) this.hudLines = null; // the text holds — stop the per-frame churn
|
||||
}
|
||||
|
||||
/** The HUD belongs to the surface stage — the take-off clip owns the screen. */
|
||||
destroyHud() {
|
||||
this.hudName?.destroy();
|
||||
this.hudLine?.destroy();
|
||||
this.hudName = null;
|
||||
this.hudLine = null;
|
||||
this.hudLines = null;
|
||||
this.hudT0 = null;
|
||||
}
|
||||
|
||||
buildDeck() {
|
||||
// The surface deck: same bar, different slots — Shop for Research,
|
||||
// Take Off left of Menu (data/actionbar.json → surface.buttons).
|
||||
|
|
@ -280,9 +387,10 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
* past duration + margin, must not hold the run.
|
||||
*/
|
||||
playTakeoff() {
|
||||
// The deck + loop clip belong to the surface stage — the clip owns
|
||||
// the screen (same "no UI mid-flight" rule as the landing stage).
|
||||
// The deck + loop clip + HUD belong to the surface stage — the clip
|
||||
// owns the screen (same "no UI mid-flight" rule as the landing stage).
|
||||
this.actionBar?.setVisible(false);
|
||||
this.destroyHud();
|
||||
if (this.surfaceVideo) {
|
||||
this.destroyVideo(this.surfaceVideo); // the opaque clip hides it — free the decoder
|
||||
this.surfaceVideo = null;
|
||||
|
|
@ -420,6 +528,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.actionBar?.update(time, delta);
|
||||
this.menuSubBar?.update(time, delta);
|
||||
this.savePanel?.update(time);
|
||||
this.updateHud(time); // the upper-left name/type decode (surface stage only)
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
|
|
@ -429,6 +538,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.landVideo = null;
|
||||
this.surfaceVideo = null;
|
||||
this.takeoffVideo = null;
|
||||
this.destroyHud();
|
||||
this.actionBar?.destroy();
|
||||
this.menuSubBar?.destroy();
|
||||
this.savePanel?.destroy();
|
||||
|
|
|
|||
Loading…
Reference in New Issue