diff --git a/assets/images/originals/arid-shop-01.png b/assets/images/originals/arid-shop-01.png new file mode 100644 index 0000000..13cb0b2 Binary files /dev/null and b/assets/images/originals/arid-shop-01.png differ diff --git a/data/gates.json b/data/gates.json index a61a501..ac086ab 100644 --- a/data/gates.json +++ b/data/gates.json @@ -60,8 +60,9 @@ "combatToast": "CANNOT JUMP WHILE UNDER ATTACK" }, "route": { - "_comment": "ROUTE (js/galaxy/Route.js + GameScene): the player's plotted course to a SET DESTINATION (SYSTEM tab of the MAP console). The run persists only the destination (systemId + objectId); the path from the current system is derived (planRoute — the jump network is a spanning tree, so it is the unique route). While a route is active, the compass marks the NEXT STOP in ORANGE (compassColor), replacing that object's ordinary arrow, and shows it even while still UNDISCOVERED (it is the thing to find): in an intermediate system the next stop is that system's jump gate toward the destination (typeLabel 'Route Gate'); in the DESTINATION system the next stop is the destination OBJECT itself (destTypeLabel) — the last hop is across the system to the world, and it stays orange until the ship actually reaches it (GameScene._checkDestinationReached fires reachedToast, then the route clears). The NEXT STOP in this system ALSO wears an in-world orange pulse the moment it is on screen (js/entities/RoutePulse.js — GameScene marks it each frame): the FULL pulse on the route GATE (pulse), the SAME pulse SUBDUED on the destination OBJECT in the destination system (objectPulse). A jump to any other system re-plots from the detour (replotToast). A destination with NO specific object clears the moment its system is entered. setToast fires when the destination is set ({dest}/{hops}).", + "_comment": "ROUTE (js/galaxy/Route.js + GameScene): the player's plotted course to a SET DESTINATION (SYSTEM tab of the MAP console). The run persists only the destination (systemId + objectId); the path from the current system is derived (planRoute — the jump network is a spanning tree, so it is the unique route). While a route is active, the compass marks the NEXT STOP in ORANGE (compassColor), replacing that object's ordinary arrow, and shows it even while still UNDISCOVERED (it is the thing to find): in an intermediate system the next stop is that system's jump gate toward the destination (typeLabel 'Route Gate'); in the DESTINATION system the next stop is the destination OBJECT itself (destTypeLabel) — the last hop is across the system to the world, and it stays orange until the ship actually reaches it (GameScene._checkDestinationReached fires reachedToast, then the route clears). The NEXT STOP in this system ALSO wears an in-world orange pulse the moment it is on screen (js/entities/RoutePulse.js — GameScene marks it each frame): the FULL pulse on the route GATE (pulse), the SAME pulse SUBDUED on the destination OBJECT in the destination system (objectPulse). A jump to any other system re-plots from the detour (replotToast). A destination with NO specific object clears the moment its system is entered. setToast fires when the destination is set ({dest}/{hops}). COLOR BY KIND (GameScene.setDestination stores destination.kind, _routeMarkColor resolves it): a CARGO delivery set by the PLAYER — the ship console's cargo manifest or the surface shop's scout (kind 'cargo') — wears the mark in BROWN (cargoColor), so the player's own delivery runs read apart from the story; a STORY-QUEST destination (the quest's own beat — kind 'quest') and the MAP console's manual pick (kind 'manual') keep the route's ORANGE (compassColor). The compass entry (arrow + chip) and the in-world pulse (RoutePulse) both follow the mark's color.", "compassColor": "#ff8c1a", + "cargoColor": "#9c6b30", "typeLabel": "Route Gate", "destTypeLabel": "Destination", "pulse": { diff --git a/data/landing.json b/data/landing.json index 83f46ba..c6b5f57 100644 --- a/data/landing.json +++ b/data/landing.json @@ -76,9 +76,9 @@ { "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, { "land": "ice-land-02.mp4", "surface": "ice-surface-02.mp4", "takeoff": "ice-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, - { "land": "lava-land-01.mp4", "surface": "lava-surface-01.mp4", "takeoff": "lava-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, + { "land": "lava-land-01.mp4", "surface": "lava-surface-01.mp4", "takeoff": "lava-takeoff-01.mp4", "shop": "lava-shop-01.mp4" }, { "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "lava-shop-02.mp4" }, - { "land": "lava-land-03.mp4", "surface": "lava-surface-03.mp4", "takeoff": "lava-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, + { "land": "lava-land-03.mp4", "surface": "lava-surface-03.mp4", "takeoff": "lava-takeoff-03.mp4", "shop": "lava-shop-03.mp4" }, { "land": "arid-land-01.mp4", "surface": "arid-surface-01.mp4", "takeoff": "arid-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, { "land": "arid-land-02.mp4", "surface": "arid-surface-02.mp4", "takeoff": "arid-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, { "land": "arid-land-03.mp4", "surface": "arid-surface-03.mp4", "takeoff": "arid-takeoff-03.mp4", "shop": "arid-shop-03.mp4" }, diff --git a/dev/scout-dest.mjs b/dev/scout-dest.mjs index a286dcb..101c46a 100644 --- a/dev/scout-dest.mjs +++ b/dev/scout-dest.mjs @@ -181,6 +181,9 @@ window.game = game; step('destination-set', gs.destination?.systemId === remoteExit.systemId && gs.destination?.objectId === (remoteExit.objectId ?? null), { destination: gs.destination, wantObject: remoteExit.objectId ?? null }); + // The scout's run is a CARGO delivery (kind 'cargo') — the compass mark + // reads BROWN (GameScene._routeMarkColor), not the route's orange. + step('destination-kind-cargo', gs.destination?.kind === 'cargo', { kind: gs.destination?.kind }); if (remoteExit.objectId) { // The destination must name a REAL object of the exit's world — a // planet (discoveryId = its name) or the deep-space station. diff --git a/dev/ship-cargo-compass.mjs b/dev/ship-cargo-compass.mjs index c2069d2..4aed2f9 100644 --- a/dev/ship-cargo-compass.mjs +++ b/dev/ship-cargo-compass.mjs @@ -170,10 +170,12 @@ try { check('phase A: the gate is off-screen now', !circleInView(gate.x, gate.y, gate.bound, viewA), { view: viewA, gate: { x: gate.x, y: gate.y } }); const entriesA = compassEntries(s); const routeEntry = entriesA.find((e) => e.id === `route:${gate.discoveryId}`); - const orange = (config.get('gates.route.compassColor', '#ff8c1a')); + const orange = String(config.get('gates.route.compassColor', '#ff8c1a')); const orangeInt = Number.parseInt(String(orange).replace('#', ''), 16); + const brown = String(config.get('gates.route.cargoColor', '#9c6b30')); + const brownInt = Number.parseInt(String(brown).replace('#', ''), 16); check('phase A: the compass carries the route gate entry', !!routeEntry, { entries: entriesA.map((e) => e.id) }); - check('phase A: the route entry is ORANGE', routeEntry?.color === orangeInt, { got: routeEntry?.color, want: orangeInt }); + check('phase A: the cargo delivery\'s route entry is BROWN', routeEntry?.color === brownInt, { got: routeEntry?.color, want: brownInt }); check('phase A: the route entry is the full readout (never folds)', routeEntry?.mode === 'full' && routeEntry?.arrowScale === 1, { mode: routeEntry?.mode, scale: routeEntry?.arrowScale }); check('phase A: the chip reads ROUTE GATE + the gate name', String(routeEntry?.type).toUpperCase().includes('ROUTE') && String(routeEntry?.name).toUpperCase() === String(gate.discoveryName ?? '').toUpperCase(), @@ -187,6 +189,8 @@ try { { entries: entriesA.map((e) => e.id) }); check('phase A: the gate wears the in-world route pulse', !!gate.routePulse && gate.routePulse.on === true, { pulse: { on: gate.routePulse?.on, ring: gate.routePulse?.ring?.visible } }); + check('phase A: the gate pulse is BROWN (the cargo mark)', gate.routePulse?.color === brownInt, + { got: gate.routePulse?.color, want: brownInt }); // ---- jump along the route, hop by hop -------------------------------- let hops = 0; @@ -242,7 +246,7 @@ try { const entriesB = compassEntries(cur); const destEntry = entriesB.find((e) => e.id === `dest:${destObj.id}`); check('phase B: the compass carries the DESTINATION entry', !!destEntry, { entries: entriesB.map((e) => e.id) }); - check('phase B: the destination entry is ORANGE', destEntry?.color === orangeInt, { got: destEntry?.color, want: orangeInt }); + check('phase B: the cargo delivery\'s destination entry is BROWN', destEntry?.color === brownInt, { got: destEntry?.color, want: brownInt }); check('phase B: full readout + DESTINATION label', destEntry?.mode === 'full' && String(destEntry?.type).toUpperCase().includes('DESTINATION'), { mode: destEntry?.mode, type: destEntry?.type, name: destEntry?.name }); if (destEntry?.arrowRot !== null) { @@ -252,6 +256,8 @@ try { const solidB = cur.solids?.find((o) => o.discoveryId === destObj.id) ?? cur.discoverableObjects().find((o) => o.id === destObj.id); check('phase B: the object wears the (subdued) route pulse', !!(solidB?.routePulse?.on === true), { pulse: solidB?.routePulse && { on: solidB.routePulse.on, ring: solidB.routePulse.ring?.visible } }); + check('phase B: the object pulse is BROWN (the cargo mark)', solidB?.routePulse?.color === brownInt, + { got: solidB?.routePulse?.color, want: brownInt }); // ---- REACHED: the ship at the keep-out rim clears the route ----------- const shipR = cur.ship.radius ?? 0; @@ -267,6 +273,42 @@ try { { entries: entriesC.map((e) => e.id) }); check('reached: the pulse drops off the object', solidB?.routePulse?.on === false, { on: solidB?.routePulse?.on }); + // ---- Phase C: NOT cargo stays ORANGE (the story/manual keeps orange) -- + const sysIds = [...cur.galaxy.byId.keys()]; + const otherSys = sysIds.find((id) => id !== cur.systemRecord.id && id !== cur.galaxy?.homeSystemId); + cur.setDestination(otherSys, null, 'quest'); // the map's seam (a story/manual pick) + await sleep(300); + check('phase C: a story/manual destination keeps kind != cargo', cur.destination?.kind === 'quest', { kind: cur.destination?.kind }); + check('phase C: the story/manual mark resolves ORANGE', cur._routeMarkColor() === orangeInt, { got: cur._routeMarkColor(), want: orangeInt }); + const gateC = cur.routeNextGate(); + if (gateC) { + check('phase C: the gate pulse is ORANGE again (the story mark)', gateC.routePulse?.color === orangeInt, + { got: gateC.routePulse?.color, want: orangeInt }); + } + + // ---- Phase D: the story-delivery carve-out (Special Delivery held) --- + // The player sets the home world from the cargo UI (kind 'cargo'), but + // the held 'Special Delivery' quest OWNS that delivery — the story keeps + // the route's orange (setDestination's kind guard). + const homeId = cur.galaxy?.homeSystemId; + if (homeId && homeId !== cur.systemRecord.id) { + cur.questState?.granted?.add('special_delivery'); // simulate the quest being held + cur.setDestination(homeId, 'home', 'cargo'); // the player's cargo pick + await sleep(300); + check('phase D: the story delivery carve-out flips kind to quest', cur.destination?.kind === 'quest', { kind: cur.destination?.kind }); + check('phase D: the story delivery mark resolves ORANGE', cur._routeMarkColor() === orangeInt, { got: cur._routeMarkColor(), want: orangeInt }); + // ...and without the quest held, the same pick is a free cargo run (brown) + cur.questState?.granted?.delete('special_delivery'); + const other2 = sysIds.find((id) => id !== cur.systemRecord.id && id !== homeId && id !== otherSys) ?? otherSys; + cur.setDestination(other2, null, 'cargo'); + await sleep(300); + check('phase D: without the quest, the cargo pick stays BROWN', cur.destination?.kind === 'cargo' && cur._routeMarkColor() === brownInt, + { kind: cur.destination?.kind, color: cur._routeMarkColor() }); + cur.questState?.granted?.delete('special_delivery'); // leave the ledger clean + cur.destination = null; + cur.registry.set('destination', null); + } + window.__SHIP_CARGO_COMPASS__ = { pass: true, results, hops }; console.info('[ship-cargo-compass] PASS:', results.join(' | ')); } catch (err) { diff --git a/dev/ship-shot.mjs b/dev/ship-shot.mjs index b0610e5..250cb83 100644 --- a/dev/ship-shot.mjs +++ b/dev/ship-shot.mjs @@ -240,6 +240,25 @@ try { } } + // ---- PARK away from the route gate (?park=1) — the compass arrow shot + // The route arrow is the OFF-SCREEN half of the mark, so park the ship + // on the far side of the home world (inside the tether) from the next + // gate: the gate leaves the view and its ROUTE GATE chip + arrow sit on + // the screen edge (brown for a cargo delivery, orange otherwise). + let park = 'n/a'; + if (q.get('park') === '1' && s.destination && typeof s.routeNextGate === 'function' && s.routeNextGate()) { + const gate = s.routeNextGate(); + const hx = s.planet?.x ?? 0, hy = s.planet?.y ?? 0; + const gd = Math.hypot(gate.x - hx, gate.y - hy) || 1; + s.ship.setPosition(hx - ((gate.x - hx) / gd) * 3800, hy - ((gate.y - hy) / gd) * 3800); + s.ship.stop(); + await sleep(2600); // the camera settles, the compass eases in + const entry = [...s.compass.entries.entries()].find(([id]) => id.startsWith('route:')); + park = entry + ? `parked · ${entry[0]} · color=#${entry[1].neon.toString(16)} · mode=${entry[1].mode} · pulse=${gate.routePulse?.on ? 'on' : 'off'}/#${(gate.routePulse?.color ?? 0).toString(16)}` + : 'parked · NO ROUTE ENTRY'; + } + // ---- live refresh while open (?mutate=1) ------------------------------- // The console polls the scene's snapshot seam on its own beat; changing the // hold through the run's OWN api (a buy landing in a bay, ore aboard) must @@ -280,6 +299,7 @@ try { `minerals ${s.ship.minerals}/${s.ship.stats.mineralStorage} · credits ${s.ship.credits}`, `locked tab: ${locked}`, `live update: ${liveUpdate}`, + `park: ${park}`, `exit tap: ${exitTap}`, ]; setReport(lines); diff --git a/js/entities/JumpGate.js b/js/entities/JumpGate.js index 37f37b3..2161e25 100644 --- a/js/entities/JumpGate.js +++ b/js/entities/JumpGate.js @@ -245,8 +245,9 @@ export class JumpGate extends Phaser.GameObjects.Container { * True shows the FULL pulse (update() animates it); false clears + * hides it. A no-op when unchanged. */ - setRouteTarget(on) { + setRouteTarget(on, color = null) { this.routePulse?.set(on); + if (color != null) this.routePulse.color = color; } /** The swirl spins + breathes (active); the fallback art drifts. (GameScene.update drives this.) */ diff --git a/js/entities/Planet.js b/js/entities/Planet.js index e7cb1e8..133977f 100644 --- a/js/entities/Planet.js +++ b/js/entities/Planet.js @@ -95,8 +95,9 @@ export class Planet extends Phaser.GameObjects.Sprite { * destination system. True shows the subdued pulse (update() animates * it); false clears + hides it. A no-op when unchanged. */ - setRouteTarget(on) { + setRouteTarget(on, color = null) { this.routePulse?.set(on); + if (color != null) this.routePulse.color = color; } /** The route pulse (a no-op while unmarked). (GameScene.update drives this.) */ diff --git a/js/entities/Station.js b/js/entities/Station.js index fa6aa9b..26a3ba9 100644 --- a/js/entities/Station.js +++ b/js/entities/Station.js @@ -222,8 +222,9 @@ export class Station extends Phaser.GameObjects.Container { * destination system. True shows the subdued pulse (update() animates * it); false clears + hides it. A no-op when unchanged. */ - setRouteTarget(on) { + setRouteTarget(on, color = null) { this.routePulse?.set(on); + if (color != null) this.routePulse.color = color; } // ---- SOLID (the same contract as Planet / AsteroidCluster) --------- diff --git a/js/save/SaveData.js b/js/save/SaveData.js index c3222b9..81aab9b 100644 --- a/js/save/SaveData.js +++ b/js/save/SaveData.js @@ -315,12 +315,20 @@ export function captureState(scene, now) { visitedSystems: Array.from(scene.visitedSystems ?? []), usedGates: Array.from(scene.usedGates ?? []), // THE PLOTTED DESTINATION (the MAP console's SYSTEM tab — js/galaxy/ - // Route.js): { systemId, objectId } | null. Only the destination is - // stored — the route itself is derived from the current system at load - // time (a save predating it has no field → no destination). + // Route.js): { systemId, objectId, kind } | null. Only the destination + // is stored — the route itself is derived from the current system at + // load time (a save predating it has no field → no destination). The + // KIND is the mark's owner ('manual' the map's pick, 'cargo' the + // player's cargo delivery, 'quest' a story beat — GameScene + // ._routeMarkColor: cargo reads brown, the rest the route's orange); + // a save predating it has no kind → 'manual' (the old orange look). destination: scene.destination && typeof scene.destination.systemId === 'string' - ? { systemId: scene.destination.systemId, objectId: scene.destination.objectId ?? null } + ? { + systemId: scene.destination.systemId, + objectId: scene.destination.objectId ?? null, + kind: scene.destination.kind ?? 'manual', + } : null, playTimeMs: Math.round(scene.playTimeMs ?? 0), }; @@ -380,11 +388,17 @@ export function prepareLoad(registry, record) { new Set(Array.isArray(record.usedGates) ? record.usedGates : []), ); // The plotted destination (a save predating it has no field → null). - // The route is derived from the current system at play time. + // The route is derived from the current system at play time. The kind + // (mark's owner — cargo deliveries read brown) rides along; a save + // predating it has no kind → 'manual' (the route's orange). registry.set( 'destination', record.destination && typeof record.destination.systemId === 'string' - ? { systemId: record.destination.systemId, objectId: record.destination.objectId ?? null } + ? { + systemId: record.destination.systemId, + objectId: record.destination.objectId ?? null, + kind: record.destination.kind ?? 'manual', + } : null, ); // The quest ledger (a save predating quests has no field → null — the diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index 44c7c72..8cfc540 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -1391,7 +1391,10 @@ export class GameScene extends Phaser.Scene { // toast and the orange compass are actually on screen for it. onSetDestination: (systemId, objectId) => { this.shipWindow?.close(); - this.setDestination(systemId, objectId); + // A CARGO delivery — the manifest's run (kind 'cargo' — the mark + // reads BROWN, unless it is a held story quest's delivery — see + // setDestination's kind guard). + this.setDestination(systemId, objectId, 'cargo'); }, // The row for the system the ship is IN can never be a destination — the // manifest marks it HERE and leaves it inert. @@ -2488,10 +2491,12 @@ export class GameScene extends Phaser.Scene { // the dust orbits. (The keep-out constraint runs in onPostUpdate, // after the physics step has moved the ship.) for (const c of this.asteroidClusters) c.update(_time); - // The route's NEXT STOP in this system wears the orange route pulse + // The route's NEXT STOP in this system wears the route pulse // (js/entities/RoutePulse.js — the in-world "your destination is // HERE" mark, visible the moment the target is on screen; the - // compass's orange arrow is the off-screen half): in an intermediate + // compass's arrow is the off-screen half), in the mark's color — + // the route's ORANGE, or BROWN for the player's own cargo delivery + // (_routeMarkColor): in an intermediate // system it is the system's JUMP GATE toward the destination // (routeNextGate — the FULL pulse, route.pulse); in the DESTINATION // system it is the destination OBJECT (a planet or a space station — @@ -2500,10 +2505,11 @@ export class GameScene extends Phaser.Scene { // destination is set or the route is done. const _routeGate = this.routeNextGate(); const _routeDest = this._routeDestinationEntity(); - if (this.planet) this.planet.setRouteTarget(this.planet === _routeDest); - for (const p of this.systemPlanets) p.setRouteTarget(p === _routeDest); - for (const st of this.systemStations) st.setRouteTarget(st === _routeDest); - for (const gt of this.systemGates) gt.setRouteTarget(gt === _routeGate); + const _markColor = this._routeMarkColor(); // the pulse follows the mark (cargo brown / route orange) + if (this.planet) this.planet.setRouteTarget(this.planet === _routeDest, _markColor); + for (const p of this.systemPlanets) p.setRouteTarget(p === _routeDest, _markColor); + for (const st of this.systemStations) st.setRouteTarget(st === _routeDest, _markColor); + for (const gt of this.systemGates) gt.setRouteTarget(gt === _routeGate, _markColor); for (const p of this.systemPlanets) p.update(_time); // the route pulse (a no-op when unmarked) for (const st of this.systemStations) st.update(_time); // the ring turns, the beacon breathes, the route pulse for (const gt of this.systemGates) gt.update(_time); // the swirl spins + breathes (active gates); fallback art drifts @@ -2680,13 +2686,16 @@ export class GameScene extends Phaser.Scene { // system's jump gate toward the destination (routeNextGate); in the // DESTINATION system it is the destination OBJECT itself (the last hop // is across the system to the world — routeDestinationObject). It - // claims a compass slot in ORANGE (data/gates.json → route.compassColor) - // so the player always knows where to fly next — and is shown EVEN while + // claims a compass slot in the mark's color — the route's ORANGE + // (data/gates.json → route.compassColor), or BROWN (route.cargoColor) + // when the destination is the player's own cargo delivery — so the + // player always knows where to fly next — and is shown EVEN while // still undiscovered (it is the thing to find). That object's ordinary // (cyan/green) entry is suppressed so the two don't stack; when it is on // screen there is no arrow at all (the player can see it) and the - // suppress is moot. The destination object stays orange until the ship - // actually reaches it (_checkDestinationReached). + // suppress is moot. The destination object stays the mark's color + // until the ship actually reaches it (_checkDestinationReached). + const routeColor = this._routeMarkColor(); // the mark's accent (cargo = brown, else the route's orange) const routeGate = this.routeNextGate(); const routeGateId = routeGate?.discoveryId ?? null; const routeOffscreen = !!( @@ -2725,7 +2734,7 @@ export class GameScene extends Phaser.Scene { y: routeGate.y, radius: routeGate.bound, typeLabel: config.get('gates.route.typeLabel', 'Route Gate'), - color: config.get('gates.route.compassColor', '#ff8c1a'), + color: routeColor, name: routeGate.discoveryName, // The "next gate" is the one thing the player must find — keep it at // the full readout (arrow at scale 1 + type + name) no matter how far @@ -2740,7 +2749,7 @@ export class GameScene extends Phaser.Scene { y: destObj.y, radius: destObj.radius, typeLabel: config.get('gates.route.destTypeLabel', 'Destination'), - color: config.get('gates.route.compassColor', '#ff8c1a'), + color: routeColor, name: destObj.name, // The final stop — same always-on treatment as the next gate. alwaysFull: true, @@ -3795,7 +3804,7 @@ export class GameScene extends Phaser.Scene { if (questId === 'special_delivery' && this._specialDeliveryHeld()) { const homeId = this.galaxy?.homeSystemId; if (homeId && this.systemRecord?.id !== homeId) { - this.setDestination(homeId, 'home'); // the compass points home + this.setDestination(homeId, 'home', 'quest'); // the compass points home out.routed = true; } } @@ -5352,7 +5361,7 @@ export class GameScene extends Phaser.Scene { if (!fresh.includes('pirate_problems')) return; const t = this.galaxy?.neutralCapitalTarget?.(); if (t?.systemId) { - this.setDestination(t.systemId, t.objectId); + this.setDestination(t.systemId, t.objectId, 'quest'); } this._trestleQ4Beats(); // The AMBUSH's other arm: the grant may have landed while the player @@ -5713,7 +5722,7 @@ export class GameScene extends Phaser.Scene { }); return; } - this.setDestination(t.systemId, t.objectId); + this.setDestination(t.systemId, t.objectId, 'quest'); } /** @@ -6137,7 +6146,7 @@ export class GameScene extends Phaser.Scene { * anywhere else) re-plots it from the detour (_routeForJump toasts the * re-plot); arriving at the destination CLEARS it. */ - setDestination(systemId, objectId) { + setDestination(systemId, objectId, kind = 'manual') { const sys = this.galaxy?.byId?.get(systemId); if (!sys) { this.consoleToast('UNKNOWN DESTINATION — NO SUCH SYSTEM', { @@ -6153,10 +6162,27 @@ export class GameScene extends Phaser.Scene { }); return; } + // KIND — who owns this destination (the compass mark's color follows it): + // 'manual' — the player's own pick (the MAP console's SYSTEM tab) + // 'cargo' — a CARGO delivery the player set (the ship console's cargo + // manifest, the surface shop's scout — their run) + // 'quest' — a story-quest beat (the quest itself aims the route) + // A cargo pick that is ALSO the delivery of a held story quest is + // that quest's run — the story keeps the route's orange (a free cargo + // run reads BROWN instead, _routeMarkColor). + let destKind = kind === 'cargo' ? 'cargo' : kind === 'quest' ? 'quest' : 'manual'; + if (destKind === 'cargo') { + const obj = objectId ?? null; + const isStoryDelivery = this._storyCargoDeliveryTargets().some( + (t) => t.systemId === systemId && (t.objectId == null || t.objectId === obj), + ); + if (isStoryDelivery) destKind = 'quest'; + } // Store the destination (the ROUTE itself is derived from it). The // object within the destination system is carried along for the - // arrival handling (autopilot / the REACHED toast). - this.destination = { systemId, objectId: objectId ?? null }; + // arrival handling (autopilot / the REACHED toast); the kind carries + // the mark's color (SaveData captures it alongside). + this.destination = { systemId, objectId: objectId ?? null, kind: destKind }; this.registry.set('destination', this.destination); const plan = planRoute(this.galaxy, this.systemRecord.id, systemId); const hops = plan?.hops ?? 0; @@ -6173,6 +6199,52 @@ export class GameScene extends Phaser.Scene { return toColor(config.get('gates.route.compassColor', '#ff8c1a'), 0xff8c1a); } + /** + * The destination mark's accent — BROWN (data/gates.json → + * route.cargoColor) while the active destination is a CARGO delivery + * the player set (destination.kind 'cargo' — the ship console's cargo + * manifest or the surface shop's scout), so the player's own delivery + * runs read apart from the story; the route's ORANGE (compassColor) + * otherwise (a story-quest beat or the map's manual pick). The compass + * entry (arrow + chip) and the in-world pulse follow it. + */ + _routeMarkColor() { + if (this.destination?.kind === 'cargo') { + return toColor(config.get('gates.route.cargoColor', '#9c6b30'), 0x9c6b30); + } + return this._routeColor(); + } + + /** + * The CARGO-delivery destination a HELD story quest currently owns — + * the 'part of a story quest' carve-out for the player's own delivery + * runs. A cargo destination the player sets that MATCHES one of these + * is the story's delivery, not a free cargo run, so it keeps the route's + * ORANGE (setDestination's kind guard); everything else the player sets + * from the cargo UI reads BROWN. + * + * Today the only cargo-delivery story quest is 'Special Delivery' + * (data/quests.json → special_delivery — deliver Carol's package to the + * HOME WORLD — the one quest whose objective is a cargo run). The other + * story destinations (the neutral-capital rendezvous of 'Leaving the + * Nest' / 'Pirate Problems') are travel, not cargo deliveries, so they + * are NOT listed here. Extend this list as new cargo-delivery story + * quests are added. + * @returns {Array<{systemId: string, objectId: string|null}>} + */ + _storyCargoDeliveryTargets() { + const out = []; + const held = (id) => + !!(this.questState?.granted?.has(id) && !this.questState?.claimed?.has(id)); + // 'Special Delivery' — the package's delivery home (the quest's own + // beat, onVignetteComplete beat 3, aims the route there while held). + if (held('special_delivery')) { + const homeId = this.galaxy?.homeSystemId; + if (homeId) out.push({ systemId: homeId, objectId: 'home' }); + } + return out; + } + /** * The CURRENT route: the path from this system to the destination * (planRoute), or null when no destination is set. diff --git a/js/scenes/SurfaceScene.js b/js/scenes/SurfaceScene.js index 3dbd165..2d94925 100644 --- a/js/scenes/SurfaceScene.js +++ b/js/scenes/SurfaceScene.js @@ -923,10 +923,18 @@ export class SurfaceScene extends Phaser.Scene { const d = this._setDest; this._setDest = null; if (!d) return; - gs.setDestination(d.exit.systemId, d.exit.objectId ?? null); // the map's seam (the exit's world, or null → system level) + // A CARGO delivery — the scout's run (kind 'cargo': the compass + // mark reads BROWN, unless it is the held story quest's delivery — + // GameScene.setDestination's kind guard). + gs.setDestination(d.exit.systemId, d.exit.objectId ?? null, 'cargo'); const dest = String(d.exit.objectName ?? d.exit.systemName ?? d.exit.systemId).toUpperCase(); const label = `DESTINATION SET — ${dest}` + (d.hops == null ? '' : ` · ${d.hops} HOP(S)`); - this.consoleNote(label, { glyph: '◆', glyphColor: toCss(config.get('gates.route.compassColor', '#ff8c1a')) }); + // The note's glyph follows the mark's color (the scene resolves it + // — the story's delivery reads the route's orange, a free run brown). + const glyphColor = typeof gs._routeMarkColor === 'function' + ? toCss(gs._routeMarkColor()) + : toCss(config.get('gates.route.compassColor', '#ff8c1a')); + this.consoleNote(label, { glyph: '◆', glyphColor }); }, time: this.time.now, }); diff --git a/js/ui/DiscoveryCompass.js b/js/ui/DiscoveryCompass.js index 938e9b6..66232b7 100644 --- a/js/ui/DiscoveryCompass.js +++ b/js/ui/DiscoveryCompass.js @@ -117,6 +117,21 @@ export class DiscoveryCompass extends Phaser.GameObjects.Container { } if (this.entries.size === 0) return; + // Recolor entries whose target changed accent (the route mark flips + // when the destination's KIND changes — a cargo delivery reads BROWN, + // a story-quest or manual pick the route's ORANGE — the entry's id + // can stay the same: same gate, different owner). The arrow is tinted, + // the type label + chip restroke in the new neon; the name stays ink. + for (const t of targets) { + const e = this.entries.get(t.id); + const want = t.color ? toColor(t.color) : themeColor('neon', 0x00e5ff); + if (e.neon === want) continue; + e.neon = want; + e.arrow.setTint(want); + if (e.typeText) e.typeText.setColor(toCss(want)); + this.drawChip(e, e.hovered); + } + const dt = Math.min(delta, 64) / 1000; // Layout strip: the reserved bottom UI (the command deck) is removed, // so arrows + chips never end up buried under it.