Shorten research labels and fix console layout clipping

- Use fixed short button copy for the gates tech ("Unlock Jumpgates") and mining nodes (drop "Mining" prefix) since the tree node has no room for system names or category words
- Raise detail panel height and widen node/button plates so long descriptions and status labels stop bleeding past plate edges
- Add word-wrap to the unlocks line and trim redundant status prefixes ("RESEARCH CONSOLE STANDBY", "PROJECT COMPLETE")
- Fix ice-surface-01 video reference in landing.json (was pointing at rocky-surface)
- Add OAC speech clips and ice surface video asset; update planets.psd
- Tighten tether research descriptions to match the new short-copy style
This commit is contained in:
Brian Fertig 2026-09-09 08:28:32 -06:00
parent fa3e0cb1f8
commit 71cbea2fab
19 changed files with 58 additions and 37 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -26,7 +26,7 @@
"breathRate": 1.8
},
"activation": {
"_comment": "ACTIVATION (the SYSTEM research category — js/research/SystemCategory.js builds the per-system tree from this): '{system} Map' is granted the moment the player is in the system (duration 0 — 'starting'), and 'Unlock {system} Jumpgates' (researchDuration, in research.timeUnit seconds) becomes researchable once EVERY NAV point of the system is discovered (the central body, all planets, all space stations, all jump gates — the scene's discoverable set minus the asteroid clusters; data/game.json → discovery.distance). Completing it activates the system's gates AND the return gates in the systems they connect to — and per the ACTIVITY rule above, each activated gate anchors a level-`tetherLevel` tether at its own position (a TETHER ANCHOR in its own right; that tether saves/restores with the run's tether list). The label/description copy is templated: `{system}` is replaced with the system name.",
"_comment": "ACTIVATION (the SYSTEM research category — js/research/SystemCategory.js builds the per-system tree from this): '{system} Map' is granted the moment the player is in the system (duration 0 — 'starting'), and 'Unlock Jumpgates' (researchDuration, in research.timeUnit seconds) becomes researchable once EVERY NAV point of the system is discovered (the central body, all planets, all space stations, all jump gates — the scene's discoverable set minus the asteroid clusters; data/game.json → discovery.distance). Completing it activates the system's gates AND the return gates in the systems they connect to — and per the ACTIVITY rule above, each activated gate anchors a level-`tetherLevel` tether at its own position (a TETHER ANCHOR in its own right; that tether saves/restores with the run's tether list). The map label/description copy is templated: `{system}` is replaced with the system name. The gates tech's label is FIXED (no `{system}`) — the console's node button is too narrow for the system name; the map tech carries it.",
"researchDuration": 45,
"tetherLevel": 1,
"mapTech": {
@ -34,7 +34,7 @@
"description": "Added the Solar System of {system} to the onboard NAV System. Discover all NAV points to unlock the system Jumpgates."
},
"gatesTech": {
"label": "Unlock {system} Jumpgates",
"label": "Unlock Jumpgates",
"description": "With system NAV data complete, we have enough information to plot courses through this system's jumpgates."
}
},

View File

@ -63,7 +63,7 @@
{ "land": "rocky-land-01.mp4", "surface": "rocky-surface-01.mp4", "takeoff": "rocky-takeoff-01.mp4", "shop": "rocky-shop-01.mp4" },
{ "land": "rocky-land-02.mp4", "surface": "rocky-surface-02.mp4", "takeoff": "rocky-takeoff-02.mp4", "shop": "rocky-shop-02.mp4" },
{ "land": "rocky-land-03.mp4", "surface": "rocky-surface-03.mp4", "takeoff": "rocky-takeoff-03.mp4", "shop": "rocky-shop-03.mp4" },
{ "land": "ice-land-01.mp4", "surface": "rocky-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "rocky-shop-01.mp4" },
{ "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "rocky-shop-01.mp4" },
{ "land": "ice-land-02.mp4", "surface": "rocky-surface-02.mp4", "takeoff": "ice-takeoff-02.mp4", "shop": "rocky-shop-02.mp4" },
{ "land": "ice-land-03.mp4", "surface": "rocky-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "rocky-shop-03.mp4" }
],

View File

@ -4,7 +4,7 @@
"nodes": {
"tether_l1": {
"label": "Tether Level 1",
"description": "Your first circle of open space: a level-1 tether anchored on the home world, a 5,120 m ring of flyable void. Everything inside the line is yours to work; everything beyond it is a wall. This is the tether you launched with.",
"description": "Your first circle of open space: a level-1 tether on the home world — a 5,120 m ring of flyable void. Inside the line is yours to work; beyond it is a wall.",
"icon": "tether",
"duration": 0,
"requires": [],
@ -13,7 +13,7 @@
},
"tether_l2": {
"label": "Tether Level 2",
"description": "A second stage of field power. The blueprint for a stronger tether: install it with a TETHER - LEVEL 2 build on a world that holds a level-1 tether (200 minerals, 20 s — data/builds.json) and that world's boundary ring pulls out to 10,240 m.",
"description": "A stronger tether: install the Tether - Level 2 build (200 minerals, 20 s) on a world holding a level-1 tether — its boundary ring pulls out to 10,240 m.",
"icon": "tether",
"duration": 60,
"requires": ["tether_l1"],
@ -22,7 +22,7 @@
},
"tether_l3": {
"label": "Tether Level 3",
"description": "A third stage of field power. The blueprint for the strongest tether: install it with a TETHER - LEVEL 3 build on a world that holds a level-2 tether (300 minerals, 30 s — data/builds.json) and that world's boundary ring pulls out to 20,480 m — the outer belt of an average system sits comfortably inside it.",
"description": "The strongest tether: install the Tether - Level 3 build (300 minerals, 30 s) on a world holding a level-2 tether — its ring pulls out to 20,480 m, covering a whole system.",
"icon": "tether3",
"duration": 150,
"requires": ["tether_l2"],

View File

@ -3,7 +3,7 @@
"starting": [],
"nodes": {
"improved_arm": {
"label": "Improved Mining Arm",
"label": "Improved Arm",
"description": "Once built, an Improved Mining Arm can mine asteroids at 50% increased speed.",
"icon": "mining",
"duration": 60,
@ -12,7 +12,7 @@
"effects": {}
},
"advanced_arm": {
"label": "Advanced Mining Arm",
"label": "Advanced Arm",
"description": "Once built, an Advanced Mining Arm is twice as fast as a standard mining arm",
"icon": "mining2",
"duration": 60,
@ -21,7 +21,7 @@
"effects": {}
},
"improved_storage": {
"label": "Improved Mining Storage",
"label": "Improved Storage",
"description": "Once built, your ship will be able to store 100 more minerals (350 total)",
"icon": "storage",
"duration": 60,

View File

@ -143,7 +143,7 @@ for (const [id, n] of Object.entries(mnodes)) {
if (!Array.isArray(n.unlocks?.builds) || !Array.isArray(n.unlocks?.research)) mfieldsOk = false;
}
check('mining: every node has label/description/icon/duration/requires/unlocks', mfieldsOk);
check('mining: labels match the brief', mnodes.improved_arm?.label === 'Improved Mining Arm' && mnodes.advanced_arm?.label === 'Advanced Mining Arm' && mnodes.improved_storage?.label === 'Improved Mining Storage');
check('mining: labels are the short button copy (the tree node has no room for "Mining" — the category tab says it)', mnodes.improved_arm?.label === 'Improved Arm' && mnodes.advanced_arm?.label === 'Advanced Arm' && mnodes.improved_storage?.label === 'Improved Storage');
check('mining: all three take 60 s (research.timeUnit)', Object.values(mnodes).every((n) => n.duration === 60));
check('mining: improved_arm is the Tier-1 root', !!mnodes.improved_arm && (mnodes.improved_arm.requires ?? []).length === 0);
check('mining: both Tier-2 projects require the Tier-1 root', (mnodes.advanced_arm?.requires ?? []).includes('improved_arm') && (mnodes.improved_storage?.requires ?? []).includes('improved_arm'));

View File

@ -52,8 +52,8 @@ check('gates: mapTech label template names the system',
check('gates: mapTech description is the brief copy',
gates.activation?.mapTech?.description ===
'Added the Solar System of {system} to the onboard NAV System. Discover all NAV points to unlock the system Jumpgates.');
check('gates: gatesTech label template names the system',
typeof gates.activation?.gatesTech?.label === 'string' && gates.activation.gatesTech.label.includes('{system}'));
check('gates: gatesTech label is the FIXED button copy (the node has no room for the system name)',
gates.activation?.gatesTech?.label === 'Unlock Jumpgates');
check('gates: gatesTech description is the brief copy',
gates.activation?.gatesTech?.description ===
"With system NAV data complete, we have enough information to plot courses through this system's jumpgates.");
@ -104,7 +104,7 @@ check('tree: map tech — duration 0 (granted, never researched), no requires',
map.duration === 0 && JSON.stringify(map.requires) === JSON.stringify([]));
check('tree: map tech — icon from the procedural glyph roster', map.icon === 'map');
check('tree: gates tech — the brief label + copy',
gNode.label === 'Unlock Kepler Reach Jumpgates' &&
gNode.label === 'Unlock Jumpgates' &&
gNode.description === "With system NAV data complete, we have enough information to plot courses through this system's jumpgates.");
check('tree: gates tech — 45 s, requires the map, the activation effect',
gNode.duration === 45 && JSON.stringify(gNode.requires) === JSON.stringify([mapId]) && gNode.effects?.activateGates === true);
@ -124,7 +124,7 @@ check('hooks: the gates tech waits on the chart (map unlocked, chart incomplete)
check('hooks: missingRequires is EMPTY (the gate is the chart, not a require)',
JSON.stringify(missingRequires(tree, state, gatesId)) === JSON.stringify([]));
check('hooks: lockNote explains the locked gates tech',
typeof tree.lockNote(state, gatesId) === 'string' && tree.lockNote(state, gatesId).includes('KEPLER REACH'));
typeof tree.lockNote(state, gatesId) === 'string' && tree.lockNote(state, gatesId).includes('NAV CHART INCOMPLETE'));
check('hooks: lockNote is quiet on the map tech + when the chart is complete',
tree.lockNote(state, mapId) === null && (chartComplete = true) === true && tree.lockNote(state, gatesId) === null);

View File

@ -647,7 +647,9 @@ one shared category without collisions:
the scene onto the destination, see the Jump gates section).
Copy: *Added the Solar System of {system} to the onboard
NAV System. Discover all NAV points to unlock the system Jumpgates.*
- **`Unlock {System} Jumpgates`** — requires the map; researchable once
- **`Unlock Jumpgates`** — the button label is FIXED (no system name —
the tree node has no room for it; the map tech carries the name).
Requires the map; researchable once
**every NAV point of the system is discovered** (the home world in the
starting system, and otherwise all planets, all space stations, all
jump gates — the scene's discoverable
@ -1427,8 +1429,9 @@ The player holds a REPUTATION (standing) on each planet and space station:
passive view, `effects` carry the payload (e.g. `tether.level` grows
the home tether). First category **Exploration** is live (Tether
Level 13); second category
**Mining** is live (Improved Mining Arm → Advanced Mining Arm +
Improved Mining Storage — all 60 s blueprints, each unlocking its
**Mining** is live (Improved Arm → Advanced Arm +
Improved Storage — short button copy, the tree node has no room for
"Mining" — all 60 s blueprints, each unlocking its
ship-upgrade build in the console's Mining tab); add a category =
one JSON file + one line in the registry + one line in the manifest
(`js/ui/ResearchWindow.js`, `js/research/*`, `data/research.json`,

View File

@ -11,7 +11,7 @@
* in `starting` the scene
* unlocks it, ResearchState
* "already owned")
* "Unlock {System} Jumpgates" requires the map + EVERY NAV
* "Unlock Jumpgates" requires the map + EVERY NAV
* point of the system discovered
* (the availability hook below);
* 45 s (data/gates.json
@ -21,6 +21,10 @@
* gates in the systems they
* connect to.
*
* The buttons on the console's tech tree are width-capped, so the labels
* stay short: the map tech carries the system name ("{System} Map"), but
* the gates tech is fixed ("Unlock Jumpgates") no name in the button.
*
* Node ids embed the system id (`S000012_map`, `S000012_gates`) so a run
* can chart many systems in the one `system` category without collisions,
* and ResearchState's "category::id" keys stay unique per system.
@ -87,9 +91,11 @@ export function buildSystemTree(o = {}) {
const duration = Math.max(0, Number(config.get('gates.activation.researchDuration', 45)) || 0);
const isComplete = typeof o.isComplete === 'function' ? o.isComplete : () => false;
const chartLockedNote =
'NAV CHART INCOMPLETE — DISCOVER ALL PLANETS, STATIONS AND JUMP GATES OF ' +
systemName.toUpperCase();
// No system name — the detail panel has room, the node button doesn't:
// the player is IN this system, the name adds no information here.
// (Also short: the meta line caps at the detail column width —
// "LOCKED · NAV CHART INCOMPLETE" = ~397 px < 412.)
const chartLockedNote = 'NAV CHART INCOMPLETE';
const tree = {
id: SYSTEM_CATEGORY,
@ -112,7 +118,7 @@ export function buildSystemTree(o = {}) {
effects: {},
},
[gatesId]: {
label: fillTemplate(config.get('gates.activation.gatesTech.label', 'Unlock {system} Jumpgates'), systemName),
label: fillTemplate(config.get('gates.activation.gatesTech.label', 'Unlock Jumpgates'), systemName),
description: fillTemplate(
config.get(
'gates.activation.gatesTech.description',
@ -226,7 +232,7 @@ export function isNavComplete(discovery, systemId, content) {
}
/**
* The activation keys one completed "Unlock {System} Jumpgates" grants:
* The activation keys one completed "Unlock Jumpgates" grants:
* every gate in `systemId` (keyed "<systemId>><destination>") AND the
* RETURN gates in the systems those destinations connect back to
* the gate in `<destination>` whose destination is `systemId`.

View File

@ -822,7 +822,7 @@ export class GameScene extends Phaser.Scene {
// The SYSTEM category's live tree — built for the system the player is
// in (categories flagged `dynamic` in research.json have no data file):
// the '{System} Map' (granted on entry — _onEnterSystem below) and
// 'Unlock {System} Jumpgates' (chart complete → researchable → the
// 'Unlock Jumpgates' (chart complete → researchable → the
// gates + the linked systems' return gates go live). js/research/
// SystemCategory.js; copy + duration in data/gates.json → activation.
const sysCat = categories().find((c) => c.id === SYSTEM_CATEGORY);
@ -3611,7 +3611,7 @@ export class GameScene extends Phaser.Scene {
}
/**
* 'Unlock {System} Jumpgates' completed (SYSTEM category): the system's
* 'Unlock Jumpgates' completed (SYSTEM category): the system's
* gates AND the return gates in the systems they jump to go ACTIVE.
*
* The activation keys are per-run world state (the registry-backed set

View File

@ -248,7 +248,10 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
const rightW = rect.x + rect.w - pad - rightX;
const tabH = 38;
const tabGap = 10;
const detailH = 152;
// 200: the detail readout holds label + state + unlocks + the (long)
// tether descriptions — 152 clipped them; the tree gives up ~50 px of
// row spacing (mining's two rows keep a 300 px gap).
const detailH = 200;
const detailGap = 12;
const treeTop = bodyY + tabH + tabGap;
const detailY = bodyY + bodyH - detailH;
@ -550,7 +553,7 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
strip.fillRect(2, sy - 1, 26, 2);
cont.add(strip);
this.statusTxt = s
.text(2, sy + 10, 'RESEARCH CONSOLE STANDBY — SELECT A TECH TO BEGIN', {
.text(2, sy + 10, 'SELECT A TECH TO BEGIN', {
fontFamily: BODY,
fontSize: '10px',
color: toCss(C.faint),
@ -726,7 +729,13 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
}
}
probe.destroy();
const nodeW = Math.min(224, Math.max(132, Math.round(maxLabel + 88)));
// Width cap: the label slot inside a plate is nodeW - 60 (the 40 px
// icon gutter on the left, the status glyph + margin on the right).
// The theme's header face is a WIDE display font — the longest button
// label ("UNLOCK JUMP GATES") measures ~203 px at 12 px/ls-1.5, so
// the plate needs ~290 px. The old 224 cap clipped every mining /
// system label past the plate edge.
const nodeW = Math.min(300, Math.max(132, Math.round(maxLabel + 88)));
const nodeH = 48;
for (const cat of categories()) {
@ -957,6 +966,12 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
// text column
const tx = bx + box + 18;
// 240: the widest button label is "IN PROGRESS 100%" — the theme's
// header face is a wide display font (~218 px at 13 px/ls-3); 190 let
// it bleed past the plate edges. wrapW caps every text line in the
// column so nothing runs into the button.
const btnW = 240;
const wrapW = Math.max(120, rightX + rightW - 12 - btnW - 18 - tx);
this.dLabel = s
.text(tx, detailY + 16, '', {
fontFamily: HEADER,
@ -982,15 +997,12 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
fontSize: '9px',
color: toCss(C.faint),
letterSpacing: 1.5,
wordWrap: { width: wrapW }, // longest line (~770 px) must wrap, not cross the button
})
.setScrollFactor(0);
cont.add(this.dUnlocks);
// 190: the widest label is "IN PROGRESS 88%" (~166 px at 13px/ls-3) —
// 152 let it bleed past the plate edges.
const btnW = 190;
const wrapW = Math.max(120, rightX + rightW - 12 - btnW - 18 - tx);
this.dDesc = s
.text(tx, detailY + 76, '', {
.text(tx, detailY + 88, '', {
fontFamily: BODY,
fontSize: '12px',
color: toCss(C.dim),
@ -1399,7 +1411,7 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
meta = `RESEARCHED · DURATION ${durStr}`;
metaColor = accent;
} else if (missing.length) {
meta = `LOCKED · REQUIRES ${missingStr}`;
meta = `LOCKED · NEEDS ${missingStr}`;
metaColor = C.amber;
} else if (lockNote) {
meta = `LOCKED · ${lockNote}`;
@ -1540,7 +1552,7 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
// the status strip decodes the completion
const label = String(entry.tree.nodes[c.id]?.label ?? c.id).toUpperCase();
this.statusTxt.setColor(toCss(accent));
this.decodeTo(this.statusTxt, `PROJECT COMPLETE — ${label}`, t0, 560);
this.decodeTo(this.statusTxt, `COMPLETE — ${label}`, t0, 560);
}
/** Advance + draw the one-shot completion bursts (additive layer). */
@ -1619,13 +1631,13 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
const p = this.state?.progress(this.scene.time.now);
const entry = p ? this.trees.get(p.category) : null;
if (!p || !entry) {
this.statusTxt.setText('RESEARCH CONSOLE STANDBY — SELECT A TECH TO BEGIN');
this.statusTxt.setText('SELECT A TECH TO BEGIN');
this.statusTxt.setColor(toCss(C.faint));
this.statusBar.clear();
return;
}
const node = entry.tree.nodes[p.id];
this.statusTxt.setText(`RESEARCHING — ${String(node?.label ?? p.id).toUpperCase()} · ${Math.round(p.fraction * 100)}%`);
this.statusTxt.setText(`${String(node?.label ?? p.id).toUpperCase()} · ${Math.round(p.fraction * 100)}%`);
this.statusTxt.setColor(toCss(entry.accent));
const g = this.statusBar;
g.clear();