Compact the system dossier to identity + faction status

- SystemReport now returns a tight {title, subtitle, faction, population, status} shape instead of per-settlement/gate line lists; the HUD shows only the name, "<Type> system · star <class>", and "Faction: Neutral · Pop ~X" (or "Unclaimed").
- Drop the settlements/gates/summary/seed lines from the GameScene HUD render and update the decode/collapse docs to the new two-line layout.
- Fix planet kindLabel lookup to key on rec.class instead of rec.name so the comms/surface panels don't repeat the proper name.
- Update asteroid, galaxy, and system-hud tests plus PROJECT_NOTES to match the compact report shape.
This commit is contained in:
Brian Fertig 2026-09-08 12:07:18 -06:00
parent 92520a0503
commit f8d62dfe09
7 changed files with 68 additions and 97 deletions

View File

@ -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 }
]

View File

@ -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 -------------------------------------------------------------

View File

@ -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 ("<Type> system · star <class>")',
/^[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

View File

@ -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 downright;
* - 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 &&

View File

@ -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).

View File

@ -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,
};
}

View File

@ -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 ("<Type> system · star <class>"), 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 downright
*
* 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 downright;
* - 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 downright 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,
);