diff --git a/assets/images/planets.psd b/assets/images/planets.psd index e4166e3..a7f23d4 100644 Binary files a/assets/images/planets.psd and b/assets/images/planets.psd differ diff --git a/assets/speech/oac/discovery-complete-01.mp3 b/assets/speech/oac/discovery-complete-01.mp3 new file mode 100644 index 0000000..e63cfbc Binary files /dev/null and b/assets/speech/oac/discovery-complete-01.mp3 differ diff --git a/assets/speech/oac/discovery-complete-02.mp3 b/assets/speech/oac/discovery-complete-02.mp3 new file mode 100644 index 0000000..684c65e Binary files /dev/null and b/assets/speech/oac/discovery-complete-02.mp3 differ diff --git a/assets/speech/oac/discovery-complete-03.mp3 b/assets/speech/oac/discovery-complete-03.mp3 new file mode 100644 index 0000000..db4646a Binary files /dev/null and b/assets/speech/oac/discovery-complete-03.mp3 differ diff --git a/assets/speech/oac/new-research.mp3 b/assets/speech/oac/new-research.mp3 new file mode 100644 index 0000000..a075891 Binary files /dev/null and b/assets/speech/oac/new-research.mp3 differ diff --git a/assets/speech/oac/quest-complete.mp3 b/assets/speech/oac/quest-complete.mp3 new file mode 100644 index 0000000..d4728a9 Binary files /dev/null and b/assets/speech/oac/quest-complete.mp3 differ diff --git a/assets/speech/oac/quest-new.mp3 b/assets/speech/oac/quest-new.mp3 new file mode 100644 index 0000000..9642d82 Binary files /dev/null and b/assets/speech/oac/quest-new.mp3 differ diff --git a/assets/speech/oac/research-complete.mp3 b/assets/speech/oac/research-complete.mp3 new file mode 100644 index 0000000..e7ea38c Binary files /dev/null and b/assets/speech/oac/research-complete.mp3 differ diff --git a/assets/videos/ice-surface-01.mp4 b/assets/videos/ice-surface-01.mp4 new file mode 100644 index 0000000..4d22fd1 Binary files /dev/null and b/assets/videos/ice-surface-01.mp4 differ diff --git a/data/gates.json b/data/gates.json index a0a345e..0689aaa 100644 --- a/data/gates.json +++ b/data/gates.json @@ -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." } }, diff --git a/data/landing.json b/data/landing.json index 6012073..388fd3c 100644 --- a/data/landing.json +++ b/data/landing.json @@ -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" } ], diff --git a/data/research/exploration.json b/data/research/exploration.json index d5d4b73..63ee396 100644 --- a/data/research/exploration.json +++ b/data/research/exploration.json @@ -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"], diff --git a/data/research/mining.json b/data/research/mining.json index df986b9..2bd6aab 100644 --- a/data/research/mining.json +++ b/data/research/mining.json @@ -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, diff --git a/dev/research-builds.test.mjs b/dev/research-builds.test.mjs index 01cd965..273f65e 100644 --- a/dev/research-builds.test.mjs +++ b/dev/research-builds.test.mjs @@ -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')); diff --git a/dev/system-category.test.mjs b/dev/system-category.test.mjs index c9e5142..ce098b7 100644 --- a/dev/system-category.test.mjs +++ b/dev/system-category.test.mjs @@ -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); diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md index a107b3d..2f529c2 100644 --- a/docs/PROJECT_NOTES.md +++ b/docs/PROJECT_NOTES.md @@ -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 1–3); 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`, diff --git a/js/research/SystemCategory.js b/js/research/SystemCategory.js index 133cde0..27fa43c 100644 --- a/js/research/SystemCategory.js +++ b/js/research/SystemCategory.js @@ -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 ">") AND the * RETURN gates in the systems those destinations connect back to — * the gate in `` whose destination is `systemId`. diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index ec75523..dc2b2b3 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -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 — diff --git a/js/ui/ResearchWindow.js b/js/ui/ResearchWindow.js index 04b2125..b3a266b 100644 --- a/js/ui/ResearchWindow.js +++ b/js/ui/ResearchWindow.js @@ -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();