Color route marks by destination kind

- Cargo deliveries set from the ship manifest or surface scout now read brown on the compass and in-world pulse; story-quest and manual picks keep the route's orange
- Add `destination.kind` ('manual' | 'cargo' | 'quest') to save/load so the mark survives a session
- Carve out held "Special Delivery" quest: a cargo pick that matches its home delivery flips to kind 'quest' so the story keeps orange
- Thread the resolved mark color through JumpGate/Planet/Station `setRouteTarget` and DiscoveryCompass entry recoloring
- Fix lava shop clip references in landing.json (ice-shop → lava-shop) and add arid-shop-01.png asset
- Extend dev probes: ship-cargo-compass checks brown/orange per kind, scout-dest asserts cargo kind, ship-shot gains a park shot for the off-screen route arrow
This commit is contained in:
Brian Fertig 2026-09-19 12:38:56 -06:00
parent 9bd10a86db
commit dd898bd594
13 changed files with 214 additions and 36 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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