diff --git a/data/landing.json b/data/landing.json index 24ee51c..148ce34 100644 --- a/data/landing.json +++ b/data/landing.json @@ -66,7 +66,7 @@ ], "stationVideos": [ - { "land": "ss-land-01.mp4", "surface": null, "takeoff": "ss-takeoff-01.mp4", "shop": null }, + { "land": "ss-land-01.mp4", "surface": "ss-surface-01.mp4", "takeoff": "ss-takeoff-01.mp4", "shop": null }, { "land": "ss-land-02.mp4", "surface": null, "takeoff": "ss-takeoff-02.mp4", "shop": null }, { "land": "ss-land-03.mp4", "surface": null, "takeoff": "ss-takeoff-03.mp4", "shop": null } ] diff --git a/dev/asteroids.test.mjs b/dev/asteroids.test.mjs index 6b899ba..03c8b16 100644 --- a/dev/asteroids.test.mjs +++ b/dev/asteroids.test.mjs @@ -298,7 +298,8 @@ check( check('home system clusters use multiple frames', new Set(all.map((r) => r.frame)).size >= 3); check('home system clusters mix spin directions', all.some((r) => r.spin > 0) && all.some((r) => r.spin < 0)); const report = formatSystemReport(c); - check(`report subtitle names the clusters (${c.asteroids.length})`, report.subtitle.includes(`${c.asteroids.length} asteroid cluster`)); + check('report subtitle is the compact identity line (no cluster count)', + /^[A-Za-z ]+ system · star [A-Z]$/.test(report.subtitle)); } // --- Determinism ------------------------------------------------------------- diff --git a/dev/galaxy.test.mjs b/dev/galaxy.test.mjs index 1978032..fe6d4ef 100644 --- a/dev/galaxy.test.mjs +++ b/dev/galaxy.test.mjs @@ -373,16 +373,19 @@ let big; pathToFileURL(join(__dirname, '../js/galaxy/SystemReport.js')).href ); const anySys = big2.currentSystem(); - const report = formatSystemReport(big2.ensureContent(anySys.id)); - check('report has title/subtitle/settlements/gates/summary', !!report.title && !!report.subtitle && Array.isArray(report.settlements) && Array.isArray(report.gates) && typeof report.summary === 'string'); - check('report lines name their anchor world or open space', report.settlements.every((s) => /on .+|in open space/.test(s.text))); - check('report gate lines name their destination star', report.gates.every((gt) => /jump to .+/.test(gt.text))); - check('report population sums match', report.population === report.settlements.reduce((s, x) => s + x.population, 0)); + const anyContent = big2.ensureContent(anySys.id); + const report = formatSystemReport(anyContent); + check('report has title/subtitle/status', !!report.title && !!report.subtitle && typeof report.status === 'string'); + check('report subtitle is the compact identity line (" system · star ")', + /^[A-Za-z ]+ system · star [A-Z]$/.test(report.subtitle)); + check('report status shows faction control + population (or unclaimed)', + /Faction: Neutral · (Pop ~[0-9.]+[kM]?|Unclaimed)$/.test(report.status)); + check('report population sums match', report.population === (anyContent.settlements ?? []).reduce((s, x) => s + (x.population ?? 0), 0)); check('formatPop() scales (1.2k / 9.0M)', formatPop(1234) === '1.2k' && formatPop(9000000) === '9.0M' && formatPop(12) === '12'); // The BARREN systems (objectCount → 0) are genuinely unclaimed — that - // is the "unclaimed · N jump gates" report branch in the wild now (not - // just a defensive fallback). + // is the "Faction: Neutral · Unclaimed" report branch in the wild now + // (not just a defensive fallback). // No-duplicate guarantee: within a system, planet names, station names // and jump-gate names never repeat (drawn without replacement from the diff --git a/dev/system-hud.test.mjs b/dev/system-hud.test.mjs index 359a4b9..d28dd19 100644 --- a/dev/system-hud.test.mjs +++ b/dev/system-hud.test.mjs @@ -15,7 +15,7 @@ * - the state caret (▾) sits right of the name, starts hidden, and * appears once the name has landed (expanded state = pointing down); * - the details are OPEN by default; the dossier folds itself 10 s - * after arrival — deconstructing in REVERSE build order (seed line + * after arrival — deconstructing in REVERSE build order (status line * first … subtitle last) — then the caret swings down→right; * - clicking the name (toggleHud) re-opens (caret down, lines type in * forward) and re-closes (lines erase in reverse, caret right); @@ -79,13 +79,7 @@ const galaxy = Galaxy.create('decode-hud-test'); const rec = galaxy.currentSystem(); const content = galaxy.ensureContent(rec.id); const report = formatSystemReport(content); -const detailValues = [ - report.subtitle, - ...report.settlements.map((s) => s.text), - ...(report.gates ?? []).map((gt) => gt.text), - report.summary, - `seed ${galaxy.seed}`, -]; +const detailValues = [report.subtitle, report.status]; console.log(`dossier under test:\n ${report.title}\n${detailValues.map((s) => ' ' + s).join('\n')}\n`); // --- Stub scene: what createSystemHud()/updateHud()/toggleHud() touch ----- @@ -211,17 +205,7 @@ check('arrival timeline: name first, then the detail lines', ys.push(y); // subtitle y += 20; y += 2; - for (let i = 0; i < report.settlements.length; i++) { - ys.push(y); - y += 20; - } - for (let i = 0; i < (report.gates ?? []).length; i++) { - ys.push(y); - y += 20; - } - ys.push(y); // summary - y += 20; - ys.push(y); // seed + ys.push(y); // status (faction + population) y += 20; check('y layout unchanged by the decode', [scene.hudTitle, ...scene.hudDetail.map((d) => d.text)].every((t, i) => t.y === ys[i])); check('hudEndY tracks the dossier bottom', scene.hudEndY === y + 6); @@ -296,7 +280,7 @@ for (let t = T0 + 4000; t <= T0 + 13000; t += STEP) { } check('the auto-fold fired at 10 s after arrival', collapseSeen !== null && collapseSeen.t >= T0 + 10000 && collapseSeen.t <= T0 + 10000 + STEP); -check('deconstruction runs in REVERSE build order (seed → … → subtitle)', +check('deconstruction runs in REVERSE build order (status → subtitle)', collapseSeen !== null && collapseSeen.order[0] === scene.hudDetail[scene.hudDetail.length - 1].text && collapseSeen.order[1] === scene.hudDetail[scene.hudDetail.length - 2].text && @@ -320,7 +304,7 @@ check('toggle while folded starts a FORWARD rebuild', scene.hudPhase === 'constructing' && scene.hudTimeline.mode === 'expand'); { const tl = scene.hudTimeline; - check('rebuild order is build order (subtitle → … → seed)', + check('rebuild order is build order (subtitle → status)', tl.lines.length === detailValues.length && tl.lines[0].text === scene.hudDetail[0].text && tl.lines[tl.lines.length - 1].text === scene.hudDetail[scene.hudDetail.length - 1].text && diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md index 14fa738..84b7567 100644 --- a/docs/PROJECT_NOTES.md +++ b/docs/PROJECT_NOTES.md @@ -177,9 +177,13 @@ branch is now the normal case, not a defensive fallback. Model & seams: where **factions and pirates** will plug in later (claim, flag, relations). Deliberately absent for now — no factions yet. - **Pure report formatter** — `js/galaxy/SystemReport.js` - (`formatSystemReport(content)` → title/subtitle/settlements/gates/ - summary). The GameScene HUD renders it; future star map / terminal UI - reuse it. + (`formatSystemReport(content)` → title/subtitle/faction/population/ + status). Compact by design: the HUD shows the system name, identity + ("Main Sequence system · star G"), and standing ("Faction: Neutral · + Pop ~1.2M" — faction is Neutral for now; unclaimed systems read + "Unclaimed"). Per-settlement / gate detail stays in the content + + star chart. The GameScene HUD renders it; a future star map / terminal + UI reuses it. - Landing/exploration (a future feature) will treat settlements as points of interest: the data already says what's there and where (anchor = planet ordinal or open space). diff --git a/js/galaxy/SystemReport.js b/js/galaxy/SystemReport.js index a25dc2c..5da6ccb 100644 --- a/js/galaxy/SystemReport.js +++ b/js/galaxy/SystemReport.js @@ -8,62 +8,42 @@ import { config } from '../config/Config.js'; * or a terminal UI later). Scenes stay thin: they render what this * returns. * + * The dossier is deliberately compact — the per-settlement and gate + * detail lives elsewhere (star chart, map, the content itself). What it + * carries: identity (type + star class), who controls the system, and + * how many are settled here. + * * const report = formatSystemReport(content); - * // { title, subtitle, settlements: [{ text, color, population }], - * // gates: [{ text, color }], summary, population } + * // { title, subtitle, faction, population, status, statusColor } + * // title — the system name + * // subtitle — "Main Sequence system · star G" + * // status — "Faction: Neutral · Pop ~1.2M" (or "… · Unclaimed") */ -export function formatSystemReport(content, typeDefs = null, kindDefs = null) { +export function formatSystemReport(content, typeDefs = null) { const types = typeDefs ?? config.get('systems.types', {}); - const kinds = kindDefs ?? config.get('settlements.kinds', {}); const typeDef = types[content.type] ?? {}; - const planetName = (ordinal) => { - const p = content.planets.find((pl) => pl.ordinal === ordinal); - return p ? p.name : `planet ${ordinal}`; - }; + const population = (content.settlements ?? []).reduce( + (sum, s) => sum + (s.population ?? 0), + 0, + ); - const settlements = []; - for (const s of content.settlements ?? []) { - const def = kinds[s.kind] ?? {}; - const where = - s.anchor?.type === 'planet' ? `on ${planetName(s.anchor.ordinal)}` : 'in open space'; - settlements.push({ - text: `${s.name} · ${def.label ?? s.kind} ${where}`, - color: def.theme?.color ?? '#8fa0c9', - population: s.population ?? 0, - }); - } + // Faction control — every system is Neutral for now. When factions + // land (the `owner` seam on settlements), this is where the faction's + // name + theme color will plug in (Neutral = light gray). + const faction = 'Neutral'; + const statusColor = '#c4cad8'; - const population = settlements.reduce((sum, s) => sum + s.population, 0); - - // The jump gates — the system's exits (content.jumps; each line names - // the star the gate jumps to, in the gate's cyan). - const gates = (content.jumps ?? []).map((j) => ({ - text: `${j.name} · jump to ${j.toName}`, - color: config.get('gates.theme.color', '#5fd4ff'), - })); - - const n = settlements.length; - const g = gates.length; - const summary = - n === 0 - ? g === 0 - ? 'charted · unclaimed' - : `unclaimed · ${g} jump gate${g === 1 ? '' : 's'}` - : `${n} settlement${n === 1 ? '' : 's'} · pop ~${formatPop(population)}` + - (g ? ` · ${g} jump gate${g === 1 ? '' : 's'}` : ''); - - const planetN = content.planets.length; - const asteroidN = Array.isArray(content.asteroids) ? content.asteroids.length : 0; return { title: content.name, - subtitle: - `${typeDef.label ?? content.type} system · star ${content.star.class} · ${planetN} planet${planetN === 1 ? '' : 's'}` + - (asteroidN ? ` · ${asteroidN} asteroid cluster${asteroidN === 1 ? '' : 's'}` : ''), - settlements, - gates, - summary, + subtitle: `${typeDef.label ?? content.type} system · star ${content.star.class}`, + faction, population, + status: + population > 0 + ? `Faction: ${faction} · Pop ~${formatPop(population)}` + : `Faction: ${faction} · Unclaimed`, + statusColor, }; } diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index ab4bb89..fa6cdc4 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -1181,9 +1181,11 @@ export class GameScene extends Phaser.Scene { } /** - * Top-left HUD: the current system's dossier — name, identity, and what - *'s ALREADY THERE: colonies, mining stations, cloud bases, stations adrift - * in open space (or "charted · unclaimed" when nobody's settled here). + * Top-left HUD: the current system's dossier — the system name, its + * identity (" system · star "), and its standing: faction + * control (Neutral for now) + total population ("Unclaimed" when none). + * Deliberately compact — the per-settlement and gate detail lives + * elsewhere (star chart, map, the content itself). * Formatted by the pure SystemReport helper; this method only renders. * * The dossier types itself in with the menu's decode scramble @@ -1197,7 +1199,7 @@ export class GameScene extends Phaser.Scene { * beside it), details type in * │ after 10 s (HUD_AUTO_COLLAPSE_MS) — OR a click on the name * ▼ - * details DECONSTRUCT in reverse build order (seed line + * details DECONSTRUCT in reverse build order (status line * first … subtitle last), then the caret swings down→right * │ * ▼ a click on the name (or the caret) @@ -1261,7 +1263,7 @@ export class GameScene extends Phaser.Scene { this.hudTitleRect = { x: X - 4, y: y - 4, w: titleW + 24, h: titleH + 8 }; y += 26; - // --- The details (open by default): what's already there ------------ + // --- The details (open by default) ---------------------------------- const detail = []; const line = (value, style) => { const t = this.add @@ -1272,16 +1274,9 @@ export class GameScene extends Phaser.Scene { detail.push({ text: t, value }); y += 20; }; - line(report.subtitle, { fontFamily: fam, fontSize: '12px', color: '#8fa0c9', letterSpacing: 1 }); + line(report.subtitle, { fontFamily: fam, fontSize: '12px', color: '#ffffff', letterSpacing: 1 }); y += 2; - for (const s of report.settlements) { - line(s.text, { fontFamily: fam, fontSize: '13px', color: toCss(s.color, '#8fa0c9') }); - } - for (const gt of report.gates ?? []) { - line(gt.text, { fontFamily: fam, fontSize: '13px', color: toCss(gt.color, '#5fd4ff') }); - } - line(report.summary, { fontFamily: fam, fontSize: '12px', color: '#54608a' }); - line(`seed ${this.galaxy.seed}`, { fontFamily: fam, fontSize: '11px', color: '#3d476b' }); + line(report.status, { fontFamily: fam, fontSize: '13px', color: toCss(report.statusColor, '#8fa0c9') }); this.hudDetail = detail; this.hudEndY = y + 6; // bottom of the dossier block @@ -1312,7 +1307,7 @@ export class GameScene extends Phaser.Scene { * erases its lines one by one with the shared decode scramble; * - on arrival the name lands first and the state caret fades in * beside it; - * - on a collapse the LAST line built erases first (seed → … → + * - on a collapse the LAST line built erases first (status → * subtitle), then the caret swings down→right; * - the one-shot 10 s auto-fold fires once (cancelled by any manual * toggle). @@ -1387,7 +1382,7 @@ export class GameScene extends Phaser.Scene { /** * Re-open the details: the caret swings back down first, then the lines - * type back in in build order (subtitle → … → seed) — the same decode as + * type back in in build order (subtitle → status) — the same decode as * arrival, just without the name (it never left). */ startExpand(t) { @@ -1406,7 +1401,7 @@ export class GameScene extends Phaser.Scene { } /** - * Fold the details: the lines DECONSTRUCT in reverse build order (seed + * Fold the details: the lines DECONSTRUCT in reverse build order (status * line erases first … subtitle last) — the same scramble played * backwards — and the caret swings down→right once the last one is gone * (updateHud, when the timeline completes). @@ -2176,7 +2171,11 @@ export class GameScene extends Phaser.Scene { // (anchor.type 'planet', anchor.ordinal = the planet's ordinal). const rec = (this.systemContent.planets ?? []).find((p) => p.name === obj.discoveryName); if (rec) { - kindLabel = config.get(`planets.typeLabels.${rec.name}`, rec.name); + // typeLabels is keyed by the planet's CLASS (rec.class — + // 'rocky'/'gas'/…), not its proper name (rec.name) — a name + // lookup would miss and fall back to the name, so the comms + // panel and the surface HUD would show the name twice. + kindLabel = config.get(`planets.typeLabels.${rec.class}`, rec.class); const anchored = (this.systemContent.settlements ?? []).filter( (s) => s.anchor?.type === 'planet' && s.anchor?.ordinal === rec.ordinal, );