Compare commits
2 Commits
efe17f3d2e
...
a5152590db
| Author | SHA1 | Date |
|---|---|---|
|
|
a5152590db | |
|
|
6609df2736 |
Binary file not shown.
Binary file not shown.
|
|
@ -48,5 +48,13 @@
|
|||
"toast": "JUMP — {dest}",
|
||||
"dormantToast": "JUMP GATE DORMANT — CHART {system} TO ACTIVATE ITS JUMP GATES",
|
||||
"miningToast": "CANNOT JUMP WHILE MINING"
|
||||
},
|
||||
"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 shows its NEXT point — the current system's jump gate toward the destination — in ORANGE (compassColor), replacing that gate's ordinary cyan arrow; the gate is shown even while still UNDISCOVERED (it is the thing to find). On arrival at the destination the route clears (reachedToast); a jump to any other system re-plots from the detour (replotToast). setToast fires when the destination is set ({dest}/{hops}).",
|
||||
"compassColor": "#ff8c1a",
|
||||
"typeLabel": "Route Gate",
|
||||
"setToast": "DESTINATION SET — {dest} · {hops} HOP(S)",
|
||||
"reachedToast": "DESTINATION REACHED — {dest}",
|
||||
"replotToast": "ROUTE RE-PLOTTED — {hops} HOP(S) TO {dest}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,8 +281,10 @@
|
|||
"label": "ENGAGE"
|
||||
},
|
||||
"systemConfirm": {
|
||||
"_comment": "The SYSTEM tab's SET DESTINATION dialog (same shared ConfirmOverlay — the clicked object on a CHARTED star's chart; the ship isn't in that system, so no distance row / autopilot). The destination model is still being defined — CONFIRM acknowledges the pick through onSetDestination (systemId + objectId), CANCEL / scrim / ESC leave the map as it was.",
|
||||
"_comment": "The SYSTEM tab's SET DESTINATION dialog (same shared ConfirmOverlay — the clicked object on a CHARTED star's chart; the ship isn't in that system, so no distance row / autopilot). The destination model is still being defined — CONFIRM acknowledges the pick through onSetDestination (systemId + objectId), CANCEL / scrim / ESC leave the map as it was. width/height: wider than the AUTOPILOT dialog — the SET DESTINATION button is a long label, so the window gives the button room and the button grows to fit its text (ConfirmOverlay.show sizes it per label).",
|
||||
"title": "SET DESTINATION",
|
||||
"label": "SET DESTINATION"
|
||||
"label": "SET DESTINATION",
|
||||
"width": 520,
|
||||
"height": 176
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* ROUTE test (dev tool, run with Node — no browser):
|
||||
*
|
||||
* node dev/route.test.mjs
|
||||
*
|
||||
* Pins the pure course-planning behind SET DESTINATION (js/galaxy/Route.js,
|
||||
* driven by GameScene.setDestination / routeNextGate / _routeForJump):
|
||||
* - the jump network is a spanning tree (data/gates.json → shortcuts:false),
|
||||
* so there is EXACTLY ONE route between any two systems — planRoute must
|
||||
* return a valid, connected path that honours the gate edges;
|
||||
* - from === to is already-there (path [id], next null, 0 hops);
|
||||
* - the path starts at `from`, ends at `to`, and every step is a real gate
|
||||
* link (both directions exist — the tree is bidirected);
|
||||
* - `next` is the first hop after `from` (what the compass points at) and is
|
||||
* always a gate neighbour of `from`;
|
||||
* - DETERMINISM: same seed + endpoints ⇒ same route; and the tree is
|
||||
* symmetric, so the from→to path reversed equals the to→from path;
|
||||
* - REACHABILITY: every ordered pair resolves (the network is strongly
|
||||
* connected) — no null where a route must exist.
|
||||
*/
|
||||
process.env.NODE_ENV = 'dev';
|
||||
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
|
||||
const fs = await import('node:fs');
|
||||
const dataDir = join(__dirname, '../data');
|
||||
const configData = {};
|
||||
for (const f of fs.readdirSync(dataDir)) {
|
||||
if (!f.endsWith('.json') || f === 'manifest.json') continue;
|
||||
configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8'));
|
||||
}
|
||||
config.init(configData);
|
||||
|
||||
const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href);
|
||||
const { planRoute, nextSystem } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Route.js')).href);
|
||||
|
||||
let failures = 0;
|
||||
const check = (label, cond, extra = '') => {
|
||||
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}${cond ? '' : ' — ' + extra}`);
|
||||
if (!cond) failures++;
|
||||
};
|
||||
|
||||
const SEED = 'route-test-seed';
|
||||
const g = Galaxy.create(SEED);
|
||||
const ids = g.records.map((r) => r.id);
|
||||
const HOME = g.currentSystemId;
|
||||
const adj = (id) => new Set(g.jumpNetwork.gates.get(id) ?? []);
|
||||
|
||||
console.log(`\ngalaxy: ${g.records.length} systems, home ${HOME}\n`);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 1. Already there — from === to
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
const r = planRoute(g, HOME, HOME);
|
||||
check('from===to → path [id]', Array.isArray(r?.path) && r.path.length === 1 && r.path[0] === HOME);
|
||||
check('from===to → next null, 0 hops', r?.next === null && r?.hops === 0);
|
||||
check('from===to → distance 0', r?.distance === 0);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 2. Path validity — endpoints, connectivity, gate edges
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
// Pick a real destination a couple of hops out (not home, not a direct
|
||||
// neighbour, so the path is non-trivial).
|
||||
const homeAdj = adj(HOME);
|
||||
let dest = null;
|
||||
for (const id of ids) {
|
||||
if (id === HOME || homeAdj.has(id)) { dest = id; break; }
|
||||
}
|
||||
const r = planRoute(g, HOME, dest);
|
||||
check('resolves a route home → a non-neighbour system', !!r, `dest ${dest}`);
|
||||
check('path starts at from', r?.path?.[0] === HOME);
|
||||
check('path ends at to', r?.path?.[r.path.length - 1] === dest);
|
||||
check('hops = path.length - 1', r?.hops === r.path.length - 1);
|
||||
// Every step is a real gate link (tree edges run both ways).
|
||||
let connected = true;
|
||||
let why = '';
|
||||
for (let i = 0; i + 1 < r.path.length; i++) {
|
||||
const a = r.path[i];
|
||||
const b = r.path[i + 1];
|
||||
if (!adj(a).has(b) || !adj(b).has(a)) {
|
||||
connected = false;
|
||||
why = `${a} !~ ${b}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
check('every step is a bidirected gate link', connected, why);
|
||||
// `next` is the first hop and a gate neighbour of from.
|
||||
check('next is path[1]', r?.next === r.path[1]);
|
||||
check('next is a gate neighbour of from', adj(HOME).has(r?.next));
|
||||
check('distance is finite + non-negative', Number.isFinite(r?.distance) && r.distance >= 0);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 3. Determinism + tree symmetry (reverse path)
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
const a = ids[0];
|
||||
const b = ids[ids.length - 1];
|
||||
const r1 = planRoute(g, a, b);
|
||||
const r2 = planRoute(g, a, b);
|
||||
check('deterministic: same seed ⇒ same route', JSON.stringify(r1) === JSON.stringify(r2));
|
||||
const rb = planRoute(g, b, a);
|
||||
const reversed = [...(r1?.path ?? [])].reverse();
|
||||
check(
|
||||
'tree symmetry: a→b reversed === b→a',
|
||||
JSON.stringify(reversed) === JSON.stringify(rb?.path),
|
||||
`${JSON.stringify(r1?.path)} vs ${JSON.stringify(rb?.path)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 4. Reachability — every ordered pair resolves (strongly connected)
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
let unresolved = 0;
|
||||
let badNext = 0;
|
||||
// Sample a slice of ordered pairs (the full N² is large; cover a wide mix
|
||||
// including home and several far systems).
|
||||
const sample = [HOME, ...ids.filter((id) => id !== HOME).slice(0, 24)];
|
||||
for (const from of sample) {
|
||||
for (const to of sample) {
|
||||
if (from === to) continue;
|
||||
const r = planRoute(g, from, to);
|
||||
if (!r || r.path[0] !== from || r.path[r.path.length - 1] !== to) {
|
||||
unresolved++;
|
||||
continue;
|
||||
}
|
||||
// next must be a gate neighbour of from (the compass's target gate).
|
||||
if (r.next !== null && !adj(from).has(r.next)) badNext++;
|
||||
}
|
||||
}
|
||||
check('every sampled ordered pair resolves (strong connectivity)', unresolved === 0, `${unresolved} unresolved`);
|
||||
check('every route\'s next is a gate neighbour of from', badNext === 0, `${badNext} bad`);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 5. nextSystem helper
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
const dest = ids.find((id) => id !== HOME) ?? HOME;
|
||||
const r = planRoute(g, HOME, dest);
|
||||
check('nextSystem === planRoute.next', nextSystem(g, HOME, dest) === (r?.next ?? null));
|
||||
check('nextSystem(from,from) === null', nextSystem(g, HOME, HOME) === null);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 6. Guard rails — unknown endpoints
|
||||
// ----------------------------------------------------------------------
|
||||
{
|
||||
check('unknown from → null', planRoute(g, 'NOPE', HOME) === null);
|
||||
check('unknown to → null', planRoute(g, HOME, 'NOPE') === null);
|
||||
check('non-string → null', planRoute(g, null, HOME) === null);
|
||||
// No galaxy + a real destination (from ≠ to) → no route to compute.
|
||||
// (from === to is the "already there" short-circuit — valid without a
|
||||
// graph, so it is asserted in section 1, not here.)
|
||||
check('no galaxy + distinct endpoints → null', planRoute(null, HOME, 'S000001') === null);
|
||||
check('empty galaxy + distinct endpoints → null', planRoute({ byId: new Map(), jumpNetwork: { gates: new Map() } }, 'A', 'B') === null);
|
||||
}
|
||||
|
||||
console.log(`\n${failures === 0 ? 'ALL PASS' : failures + ' FAILURE(S)'}\n`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
|
|
@ -369,12 +369,18 @@ collide). **Barren systems** (no anchors — the `objectCount` → 0 stops)
|
|||
## Galaxy map — the MAP console's GALAXY tab
|
||||
|
||||
The deck's MAP button opens the cartography console
|
||||
(`js/ui/MapWindow.js`, `data/map.json`, depth 80). It has two lives on
|
||||
the plate, switched by the tabs:
|
||||
(`js/ui/MapWindow.js`, `data/map.json`, depth 80). It has three tabs on
|
||||
the plate:
|
||||
|
||||
- **CURRENT SYSTEM** — the system chart (canvas-painted: discovered
|
||||
objects, the tether union, the fog of war), hover + ENGAGE AUTOPILOT,
|
||||
wheel zoom / drag pan. Unchanged by the galaxy work.
|
||||
- **SYSTEM** — the same painter applied to a CHARTED star's system
|
||||
(no ship/tether): pick a charted star on the GALAXY tab to arm it, then
|
||||
click an object on its chart to **SET DESTINATION** (see the Route
|
||||
section below). Locked ("NO SYSTEM TARGET") until a charted star is
|
||||
picked. The shared `ConfirmOverlay` is label-sized to fit the dialog
|
||||
(`baseWidth` + measured width, per-show `width`/`height` overrides).
|
||||
- **GALAXY** (live — `tabs.galaxy.standby` is the killswitch; `true`
|
||||
returns it to the old "OFFLINE" toast) — the whole-galaxy chart,
|
||||
`js/ui/GalaxyView.js`, fed by `GameScene.galaxySnapshot()`
|
||||
|
|
@ -431,6 +437,61 @@ alpha). The settlements' `owner: null` seam (above) +
|
|||
the parallel-polygon corners, the pulse's bounds + determinism, the
|
||||
archetype colors, and the save round-trip incl. the legacy default).
|
||||
|
||||
## Route — SET DESTINATION (the MAP console's SYSTEM tab)
|
||||
|
||||
The SYSTEM tab charts a CHARTED star's system (the same painter as the
|
||||
CURRENT SYSTEM tab, minus the ship/tether). Clicking an object on that
|
||||
chart asks **SET DESTINATION** (`MapWindow._askObject` → the shared
|
||||
`ConfirmOverlay`, widened to fit the label). Confirming plots the route
|
||||
and closes the map window.
|
||||
|
||||
**The route is between SYSTEMS, and it is DERIVED** (`js/galaxy/Route.js`,
|
||||
pure + Node-tested). The run persists ONLY the destination —
|
||||
`GameScene.destination = { systemId, objectId }` (registry-backed, like
|
||||
`visitedSystems`). The path from wherever the ship is NOW to that system
|
||||
is computed on demand by `planRoute()` (Dijkstra by travel distance over
|
||||
the gate graph — the jump network is a spanning tree,
|
||||
`data/gates.json → shortcuts:false`, so this is the unique route;
|
||||
strong connectivity guarantees one always exists). Deriving it (instead
|
||||
of storing a progress counter) makes the follow/detour rules automatic:
|
||||
|
||||
- **Set** (`setDestination`) — store the destination, toast the hop count
|
||||
(`data/gates.json → route.setToast`), close the map. The compass then
|
||||
points at the route's NEXT STEP.
|
||||
- **Next step** (`routeNextGate`) — the current system's jump gate whose
|
||||
`gate.to` is the route's first hop. `updateDiscovery()` marks it on the
|
||||
compass in **ORANGE** (`route.compassColor`), replacing that gate's
|
||||
ordinary cyan arrow and showing it even while still UNDISCOVERED (it is
|
||||
the thing to find). The gate's normal compass entry is suppressed so the
|
||||
two don't stack; on screen there is no arrow (the player can see it).
|
||||
- **Jump** (`jumpThroughGate` → `_routeForJump`, BEFORE `captureState` so
|
||||
the cleared state is what saves): entered the DESTINATION → route done,
|
||||
clear `destination` (the compass orange drops) + a REACHED notice;
|
||||
entered the route's NEXT → on course, silent (the derived route just
|
||||
shortens); entered anything else → a DETOUR, the route re-plots from the
|
||||
detour + a RE-PLOTTED notice. Because the route is derived, the re-plot
|
||||
is automatic — this only announces it.
|
||||
- **Notices** — a jump fires its own toast + full-screen clip, which would
|
||||
swallow an immediate REACHED/RE-PLOTTED toast, so those are QUEUED in
|
||||
the registry (`routeNotice`) and surfaced by the DESTINATION scene's
|
||||
`create()` a beat after spawn. `resetRunState` clears it (a New Game
|
||||
inherits no pending notice).
|
||||
- **Save** — `destination` is captured/restored/reset in
|
||||
`js/save/SaveData.js` (a save predating it has no field → no route);
|
||||
the route itself is re-derived at load from the saved current system.
|
||||
|
||||
**Tuning** — `data/gates.json → route` (the orange `compassColor`, the
|
||||
`typeLabel`, the three toasts with their `{dest}`/`{hops}` placeholders).
|
||||
**Tests** — `dev/route.test.mjs` (already-there short-circuit, path
|
||||
validity over real gate edges, `next` is a gate neighbour, determinism +
|
||||
tree symmetry (a→b reversed === b→a), strong connectivity across sampled
|
||||
ordered pairs, guard rails). The compass orange, the map close, and the
|
||||
notice queue are Phaser glue — verified in a headless browser
|
||||
(`dev/shot-firefox.mjs` / geckodriver `execute/sync`):
|
||||
set → orange `route:<gate>` compass entry (`#ff8c1a`); arrive → route
|
||||
clears + REACHED notice queued; detour → destination kept + RE-PLOTTED
|
||||
notice; clear → the orange entry drops on the next compass reconcile.
|
||||
|
||||
## The tether — the player's range (important)
|
||||
|
||||
The ship starts with **one level-1 tether** anchored on the home world
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Route — a plotted course between two SYSTEMS of the jump network.
|
||||
*
|
||||
* The player sets a destination (SYSTEM tab of the MAP console) and the
|
||||
* run plots the route it will take to get there. The compass then marks
|
||||
* the NEXT point of that route — the jump gate in the current system that
|
||||
* leads toward the destination — in orange, so the player always knows
|
||||
* where to fly next.
|
||||
*
|
||||
* THE JUMP NETWORK IS A SPANNING TREE (data/gates.json → shortcuts:false),
|
||||
* so there is EXACTLY ONE route between any two systems — the galaxy reads
|
||||
* as a maze. planRoute still computes the CHEAPEST path (Dijkstra by travel
|
||||
* distance over the gate graph), which is that unique route under the
|
||||
* current config and degrades gracefully to "the fastest route" if
|
||||
* shortcuts are ever enabled. Deterministic: same galaxy + endpoints ⇒
|
||||
* same route.
|
||||
*
|
||||
* ROUTE STATE IS DERIVED. The run persists only the DESTINATION
|
||||
* (systemId + objectId); the path from wherever the ship is NOW to that
|
||||
* destination is computed on demand. That makes the follow/detour rules
|
||||
* automatic, with no progress counter to desync:
|
||||
* · jump to the route's next system → the remainder just shortens
|
||||
* (re-plan from the new system, same destination);
|
||||
* · jump anywhere else (a DETOUR) → the route re-plots from the detour
|
||||
* (GameScene toasts the re-plot);
|
||||
* · arrive at the destination system → the route is DONE and clears.
|
||||
*
|
||||
* Pure module (no Phaser) — Node-testable (dev/route.test.mjs).
|
||||
*/
|
||||
|
||||
/** Euclidean distance between two roster points. */
|
||||
function dist(ax, ay, bx, by) {
|
||||
const dx = ax - bx;
|
||||
const dy = ay - by;
|
||||
return Math.hypot(dx, dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan the route between two systems.
|
||||
*
|
||||
* @param {object} galaxy — the run's Galaxy: needs `byId` (Map sysId →
|
||||
* record with x/y) and `jumpNetwork.gates` (Map sysId → [sysId, …]).
|
||||
* @param {string} fromId — the current system.
|
||||
* @param {string} toId — the destination system.
|
||||
* @returns {{path: string[], next: string|null, hops: number, distance: number}|null}
|
||||
* `path` is [from, …, to]; `next` is the FIRST hop after `from` (null
|
||||
* when from === to — already there); `hops` = path.length - 1. `null`
|
||||
* when an endpoint is unknown or there is no route (should not happen —
|
||||
* the network is strongly connected by construction).
|
||||
*/
|
||||
export function planRoute(galaxy, fromId, toId) {
|
||||
if (typeof fromId !== 'string' || typeof toId !== 'string') return null;
|
||||
if (fromId === toId) return { path: [toId], next: null, hops: 0, distance: 0 };
|
||||
const byId = galaxy?.byId;
|
||||
const gates = galaxy?.jumpNetwork?.gates;
|
||||
if (!byId || !gates || !byId.has(fromId) || !byId.has(toId)) return null;
|
||||
if (typeof byId.get(fromId)?.x !== 'number' || typeof byId.get(toId)?.x !== 'number') return null;
|
||||
|
||||
// Dijkstra by travel distance (edge cost = the hop's Euclidean span).
|
||||
// The graph is small (a few hundred systems, degree ≤ maxGates), so a
|
||||
// plain O(V²) select is plenty; ties break on system id (deterministic).
|
||||
const best = new Map(); // sysId → best distance seen
|
||||
best.set(fromId, 0);
|
||||
const prev = new Map(); // sysId → predecessor on the best path
|
||||
const done = new Set();
|
||||
for (;;) {
|
||||
let u = null;
|
||||
let du = Infinity;
|
||||
for (const [id, d] of best) {
|
||||
if (done.has(id)) continue;
|
||||
if (d < du || (d === du && (u === null || id < u))) { u = id; du = d; }
|
||||
}
|
||||
if (u === null) break; // nothing left to relax
|
||||
if (u === toId) break; // reached the goal at its final cost
|
||||
done.add(u);
|
||||
const ru = byId.get(u);
|
||||
for (const v of gates.get(u) ?? []) {
|
||||
if (done.has(v) || !byId.has(v)) continue;
|
||||
const rv = byId.get(v);
|
||||
const nd = du + dist(ru.x, ru.y, rv.x, rv.y);
|
||||
if (nd < (best.get(v) ?? Infinity)) {
|
||||
best.set(v, nd);
|
||||
prev.set(v, u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!best.has(toId)) return null; // unreachable — defensive (a tree can't do this)
|
||||
|
||||
const path = [];
|
||||
let cur = toId;
|
||||
for (;;) {
|
||||
path.push(cur);
|
||||
if (cur === fromId) break;
|
||||
cur = prev.get(cur);
|
||||
if (cur === undefined) return null; // no chain back — defensive
|
||||
}
|
||||
path.reverse();
|
||||
return {
|
||||
path,
|
||||
next: path.length > 1 ? path[1] : null,
|
||||
hops: path.length - 1,
|
||||
distance: best.get(toId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The system the route leaves NEXT from `fromId` on the way to `toId`
|
||||
* (planRoute's `next`), or null when already there / no route.
|
||||
*/
|
||||
export function nextSystem(galaxy, fromId, toId) {
|
||||
return planRoute(galaxy, fromId, toId)?.next ?? null;
|
||||
}
|
||||
|
|
@ -123,6 +123,14 @@ export function captureState(scene, now) {
|
|||
// (old saves load, the region starts small).
|
||||
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).
|
||||
destination:
|
||||
scene.destination && typeof scene.destination.systemId === 'string'
|
||||
? { systemId: scene.destination.systemId, objectId: scene.destination.objectId ?? null }
|
||||
: null,
|
||||
playTimeMs: Math.round(scene.playTimeMs ?? 0),
|
||||
};
|
||||
const err = SaveManager.validateRecord(rec);
|
||||
|
|
@ -180,6 +188,14 @@ export function prepareLoad(registry, record) {
|
|||
'usedGates',
|
||||
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.
|
||||
registry.set(
|
||||
'destination',
|
||||
record.destination && typeof record.destination.systemId === 'string'
|
||||
? { systemId: record.destination.systemId, objectId: record.destination.objectId ?? null }
|
||||
: null,
|
||||
);
|
||||
registry.set(PENDING_RESTORE_KEY, {
|
||||
ship: record.ship,
|
||||
tethers: Array.isArray(record.tethers) ? record.tethers : [],
|
||||
|
|
@ -211,6 +227,8 @@ export function resetRunState(registry) {
|
|||
registry.set('activatedGates', null);
|
||||
registry.set('visitedSystems', null);
|
||||
registry.set('usedGates', null);
|
||||
registry.set('destination', null);
|
||||
registry.set('routeNotice', null);
|
||||
registry.set(PENDING_RESTORE_KEY, null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { ResearchWindow } from '../ui/ResearchWindow.js';
|
|||
import { MapWindow } from '../ui/MapWindow.js';
|
||||
import { navDiscoveryStats, resourceStats, systemChartSnapshot } from '../galaxy/SystemChart.js';
|
||||
import { buildGalaxySnapshot, edgeKey } from '../galaxy/GalaxyChart.js';
|
||||
import { planRoute } from '../galaxy/Route.js';
|
||||
import { ResearchState } from '../research/ResearchState.js';
|
||||
import { categories, loadCategory, isAvailable, buildDefs } from '../research/ResearchModel.js';
|
||||
import {
|
||||
|
|
@ -320,6 +321,31 @@ export class GameScene extends Phaser.Scene {
|
|||
this.usedGates = new Set();
|
||||
this.registry.set('usedGates', this.usedGates);
|
||||
}
|
||||
// THE PLOTTED DESTINATION (the MAP console's SYSTEM tab — js/galaxy/
|
||||
// Route.js): where the run is headed. Persisted as { systemId,
|
||||
// objectId } — the ROUTE itself is derived on demand (planRoute from
|
||||
// wherever the ship is now to destination.systemId), so following it
|
||||
// just shortens it and a detour re-plots it (no progress counter to
|
||||
// desync). Null until the player sets one; cleared on arrival.
|
||||
this.destination = this.registry.get('destination') ?? null;
|
||||
// A ROUTE NOTICE parked by the jump we just completed (Route._routeFor
|
||||
// Jump → _queueRouteNotice): the destination-REACHED / route-RE-PLOTTED
|
||||
// message. The jump's own toast + full-screen clip already played on
|
||||
// the source scene, so surface this one here, a beat after spawn, so
|
||||
// it reads cleanly on the destination system.
|
||||
{
|
||||
const notice = this.registry.get('routeNotice');
|
||||
if (notice && typeof notice.text === 'string') {
|
||||
this.registry.set('routeNotice', null);
|
||||
this.time.delayedCall(900, () =>
|
||||
this.consoleToast(notice.text, {
|
||||
glyph: notice.glyph ?? '◆',
|
||||
glyphColor: notice.glyphColor ?? toCss(this._routeColor()),
|
||||
durationMs: notice.durationMs ?? 3400,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
this.visitedSystems.add(this.systemRecord.id); // we start here — it's charted
|
||||
for (const j of this.systemContent.jumps ?? []) {
|
||||
if (j && this.activatedGates.has(`${this.systemRecord.id}>${j.to}`)) j.active = true;
|
||||
|
|
@ -1476,16 +1502,42 @@ export class GameScene extends Phaser.Scene {
|
|||
const fresh = this.discovery.check(sysId, this.ship.x, this.ship.y, objects);
|
||||
for (const o of fresh) this.celebrateDiscovery(o);
|
||||
|
||||
// Which discovered objects are NOT on screen right now? (The camera
|
||||
// never zooms, so the world view is scroll + canvas size.)
|
||||
// Which objects are NOT on screen right now? (The camera never
|
||||
// zooms, so the world view is scroll + canvas size.)
|
||||
const cam = this.cameras.main;
|
||||
const view = { left: cam.scrollX, top: cam.scrollY, w: this.scale.width, h: this.scale.height };
|
||||
|
||||
// The active route's NEXT POINT — the current system's jump gate
|
||||
// toward the destination (routeNextGate). 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 still undiscovered
|
||||
// (it is the thing to find). That gate's ordinary (cyan) entry is
|
||||
// suppressed so the two don't stack; when the gate is on screen there
|
||||
// is no arrow at all (the player can see it) and the suppress is moot.
|
||||
const routeGate = this.routeNextGate();
|
||||
const routeGateId = routeGate?.discoveryId ?? null;
|
||||
const routeOffscreen = !!(
|
||||
routeGate && !circleInView(routeGate.x, routeGate.y, routeGate.bound, view)
|
||||
);
|
||||
|
||||
const offscreen = [];
|
||||
for (const o of objects) {
|
||||
if (o.id === routeGateId) continue; // the route arrow takes this slot
|
||||
if (this.discovery.isDiscovered(sysId, o.id) && !circleInView(o.x, o.y, o.radius, view)) {
|
||||
offscreen.push(o);
|
||||
}
|
||||
}
|
||||
if (routeOffscreen) {
|
||||
offscreen.push({
|
||||
id: `route:${routeGate.discoveryId}`,
|
||||
x: routeGate.x,
|
||||
y: routeGate.y,
|
||||
radius: routeGate.bound,
|
||||
typeLabel: config.get('gates.route.typeLabel', 'Route Gate'),
|
||||
color: config.get('gates.route.compassColor', '#ff8c1a'),
|
||||
name: routeGate.discoveryName,
|
||||
});
|
||||
}
|
||||
// The ship's position drives the compass's FAR display (targets
|
||||
// beyond game.discovery.compass.farDistance fold to their compact
|
||||
// chip + shrunken arrow until hovered — js/ui/DiscoveryCompass.js).
|
||||
|
|
@ -1759,6 +1811,11 @@ export class GameScene extends Phaser.Scene {
|
|||
// the current config): the destination's origin (its center is empty —
|
||||
// the star is invisible flavor, so this is just open space).
|
||||
const destContent = this.galaxy.ensureContent(destId);
|
||||
// ROUTE BOOKKEEPING — resolve what this hop means for the active route
|
||||
// BEFORE captureState below snapshots the run (so a destination we've
|
||||
// just reached is cleared in the saved state, not resurrected):
|
||||
// arrived-at-destination → clear; on-course → silent; detour → toast.
|
||||
this._routeForJump(from.id, destId);
|
||||
// THE RUN'S FOOTPRINT — record the lane we just traveled + the system
|
||||
// we are entering (the GALAXY tab's brighter lane + charted region;
|
||||
// captured into `rec` right below, so the save carries it).
|
||||
|
|
@ -2476,19 +2533,140 @@ export class GameScene extends Phaser.Scene {
|
|||
* defined — for now the pick is acknowledged (the seam is stable:
|
||||
* systemId + objectId).
|
||||
*/
|
||||
/**
|
||||
* SET DESTINATION (the MAP console's SYSTEM tab — MapWindow._askObject):
|
||||
* record the target system and plot the route to it. The route's FIRST
|
||||
* STEP is what the player flies now — the current system's jump gate
|
||||
* toward the destination (routeNextGate) — and it is marked ORANGE on
|
||||
* the compass (updateDiscovery) so the player always knows where to go
|
||||
* next. The MAP window closes itself once this returns (MapWindow's
|
||||
* confirm handler calls this.close()).
|
||||
*
|
||||
* The ROUTE IS DERIVED (js/galaxy/Route.js): only the DESTINATION
|
||||
* (systemId + the object within it) is stored — the path from wherever
|
||||
* the ship is NOW to that system is computed on demand. Following the
|
||||
* route (a jump to its next system) just shortens it; a DETOUR (a jump
|
||||
* anywhere else) re-plots it from the detour (_routeForJump toasts the
|
||||
* re-plot); arriving at the destination CLEARS it.
|
||||
*/
|
||||
setDestination(systemId, objectId) {
|
||||
const content = this.galaxy?.contentOf?.(systemId);
|
||||
const name =
|
||||
(content?.planets ?? []).find((p) => p.name === objectId)?.name ??
|
||||
(content?.settlements ?? []).find((s) => s.id === objectId)?.name ??
|
||||
(content?.jumps ?? []).find((j) => j.id === objectId)?.name ??
|
||||
(content?.asteroids ?? []).find((c) => c.id === objectId)?.name ??
|
||||
objectId;
|
||||
const sys = content?.name ?? systemId;
|
||||
console.info(`[orbit] set destination: ${systemId} → ${objectId}`);
|
||||
this.consoleToast(`DESTINATION NOTED — ${String(name).toUpperCase()} · ${String(sys).toUpperCase()}`, {
|
||||
const sys = this.galaxy?.byId?.get(systemId);
|
||||
if (!sys) {
|
||||
this.consoleToast('UNKNOWN DESTINATION — NO SUCH SYSTEM', {
|
||||
glyph: '✕',
|
||||
glyphColor: toCss(themeColor('neon2', 0xff9b9b)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (systemId === this.systemRecord.id) {
|
||||
this.consoleToast('ALREADY AT THAT SYSTEM', {
|
||||
glyph: '✕',
|
||||
glyphColor: toCss(themeColor('neon2', 0xff9b9b)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 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 };
|
||||
this.registry.set('destination', this.destination);
|
||||
const plan = planRoute(this.galaxy, this.systemRecord.id, systemId);
|
||||
const hops = plan?.hops ?? 0;
|
||||
this.consoleToast(
|
||||
String(config.get('gates.route.setToast', 'DESTINATION SET — {dest} · {hops} HOP(S)'))
|
||||
.replace('{dest}', String(sys.name).toUpperCase())
|
||||
.replace('{hops}', String(hops)),
|
||||
{ glyph: '◆', glyphColor: toCss(this._routeColor()) },
|
||||
);
|
||||
}
|
||||
|
||||
/** The route's accent — orange (data/gates.json → route.compassColor). */
|
||||
_routeColor() {
|
||||
return toColor(config.get('gates.route.compassColor', '#ff8c1a'), 0xff8c1a);
|
||||
}
|
||||
|
||||
/**
|
||||
* The CURRENT route: the path from this system to the destination
|
||||
* (planRoute), or null when no destination is set.
|
||||
*/
|
||||
routePlan() {
|
||||
if (!this.destination?.systemId) return null;
|
||||
return planRoute(this.galaxy, this.systemRecord.id, this.destination.systemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route's NEXT POINT — the jump gate in the CURRENT system that
|
||||
* leads toward the destination (its gate.to === the route's next
|
||||
* system). This is what the compass marks in orange and what the
|
||||
* player flies to. null when there is no active route, the route is
|
||||
* complete (we are at the destination), or the current system holds no
|
||||
* gate toward the next system (defensive — the network is connected,
|
||||
* so a next system always has a gate here).
|
||||
*/
|
||||
routeNextGate() {
|
||||
const plan = this.routePlan();
|
||||
if (!plan?.next) return null;
|
||||
for (const gt of this.systemGates ?? []) if (gt.gate?.to === plan.next) return gt;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route bookkeeping at a JUMP (called from jumpThroughGate with the
|
||||
* system we are LEAVING and the one we are ENTERING). Derives what the
|
||||
* hop means for the active route and QUEUES a notice (routeNotice — the
|
||||
* toast must appear AFTER the jump cut, on the destination scene: the
|
||||
* jump itself fires its own toast + full-screen clip, which would
|
||||
* swallow an immediate one):
|
||||
* · entering the DESTINATION → route complete — clear it (REACHED);
|
||||
* · entering the route's NEXT → on course (the derived route simply
|
||||
* shortens — no notice, the compass already pointed there);
|
||||
* · entering anything else → a DETOUR — re-plot from the detour
|
||||
* (RE-PLOTTED). Because the route is derived, the re-plot is
|
||||
* automatic; this only announces it.
|
||||
* The state change (clearing the destination on arrival) happens NOW —
|
||||
* before captureState snapshots the run — so the saved state is right;
|
||||
* only the player-facing message is deferred.
|
||||
*/
|
||||
_routeForJump(fromId, destId) {
|
||||
if (!this.destination?.systemId) return; // no active route
|
||||
const destId2 = this.destination.systemId;
|
||||
const destName = this.galaxy?.byId?.get(destId2)?.name ?? destId2;
|
||||
if (destId === destId2) {
|
||||
// Arrived at the destination — the route is done. Clear the tracked
|
||||
// destination (the compass orange drops, the SYSTEM tab unlocks for
|
||||
// the next target), and queue the REACHED notice for the new scene.
|
||||
this.destination = null;
|
||||
this.registry.set('destination', null);
|
||||
this._queueRouteNotice(
|
||||
String(config.get('gates.route.reachedToast', 'DESTINATION REACHED — {dest}'))
|
||||
.replace('{dest}', String(destName).toUpperCase()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Not the destination — were we on the route, or off it?
|
||||
const expected = planRoute(this.galaxy, fromId, destId2)?.next ?? null;
|
||||
if (expected === destId) return; // on course — the route just shortened
|
||||
const plan = planRoute(this.galaxy, destId, destId2);
|
||||
const hops = plan?.hops ?? 0;
|
||||
this._queueRouteNotice(
|
||||
String(config.get('gates.route.replotToast', 'ROUTE RE-PLOTTED — {hops} HOP(S) TO {dest}'))
|
||||
.replace('{hops}', String(hops))
|
||||
.replace('{dest}', String(destName).toUpperCase()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park a route notice (data/gates.json → route.* toast) in the shared
|
||||
* registry so the DESTINATION scene's create() can surface it after the
|
||||
* jump cut (the jump's own toast + clip would swallow an immediate one).
|
||||
* Consumed (and cleared) exactly once by the next GameScene.create().
|
||||
*/
|
||||
_queueRouteNotice(text) {
|
||||
this.registry.set('routeNotice', {
|
||||
text,
|
||||
glyph: '◆',
|
||||
glyphColor: toCss(themeColor('neon', 0x00e5ff)),
|
||||
glyphColor: toCss(this._routeColor()),
|
||||
durationMs: 3400,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
|||
);
|
||||
this.confirmBtn.setAlpha(0);
|
||||
this.cancelBtn.setAlpha(0);
|
||||
// the natural widths (show() grows a button only as wide as its label
|
||||
// needs — and never leaves it grown for the NEXT dialog, which may
|
||||
// have a shorter label)
|
||||
this.confirmBtn.style.baseWidth = this.confirmBtn.style.width;
|
||||
this.cancelBtn.style.baseWidth = this.cancelBtn.style.width;
|
||||
this.add([this.cancelBtn, this.confirmBtn]);
|
||||
// Hidden dialog = inert buttons (same v4 `input.enabled` rule as the
|
||||
// scrim — otherwise the invisible CONFIRM/CANCEL swallow centre-
|
||||
|
|
@ -130,7 +135,49 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
|||
this._titleDec = null;
|
||||
this._btnsUp = false;
|
||||
this._confirmUp = false;
|
||||
this._layout(); // size/label land in show(); positions derive from w/h
|
||||
}
|
||||
|
||||
/**
|
||||
* Place the title, body lines and the two buttons for the CURRENT
|
||||
* w/h + button sizes: the buttons sit in the panel's bottom band
|
||||
* (CANCEL left, CONFIRM right — each kept inside the panel edge with
|
||||
* a 20px margin, whatever its label grows it to).
|
||||
*/
|
||||
_layout() {
|
||||
this.title.setPosition(0, -this.h / 2 + 24);
|
||||
this.bodyTexts.forEach((t, i) => t.setPosition(0, -this.h / 2 + 52 + i * 17));
|
||||
const margin = 20;
|
||||
const cw = this.cancelBtn.style.width;
|
||||
const bw = this.confirmBtn.style.width;
|
||||
const bh = this.confirmBtn.style.height;
|
||||
const cy = this.h / 2 - margin - bh / 2;
|
||||
this.cancelBtn.setPosition(-this.w / 2 + margin + cw / 2, cy);
|
||||
this.confirmBtn.setPosition(this.w / 2 - margin - bw / 2, cy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grow a button's panel to FIT its label (the SET DESTINATION label is
|
||||
* wider than the fixed 148px the CONFIRM button was built for — the
|
||||
* text used to run past the panel edge). Re-measures with the button's
|
||||
* own text style, then repaints + resets the hit area.
|
||||
*/
|
||||
_fitBtn(btn, label, minWidth) {
|
||||
const s = btn.style;
|
||||
const m = this.scene.add.text(0, 0, String(label), s.textStyle);
|
||||
const w = Math.max(s.baseWidth ?? s.width, minWidth, Math.ceil(m.width) + 28);
|
||||
m.destroy();
|
||||
if (w === s.width) return;
|
||||
s.width = w;
|
||||
btn.setSize(w, s.height);
|
||||
btn.panel.setInteractive({
|
||||
useHandCursor: true,
|
||||
hitArea: new Phaser.Geom.Rectangle(-w / 2, -s.height / 2, w, s.height),
|
||||
hitAreaCallback: (p, px, py) => Phaser.Geom.Rectangle.Contains(p, px, py),
|
||||
});
|
||||
btn.paint('base');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Showing
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -139,6 +186,9 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
|||
* spec: {
|
||||
* title, body: string[], accent?: number,
|
||||
* confirmLabel?, cancelLabel?,
|
||||
* width?, height? — override the dialog's size for THIS show (the
|
||||
* SYSTEM tab's SET DESTINATION is wider than AUTOPILOT's ENGAGE —
|
||||
* its confirm label is longer); the layout re-flows for it,
|
||||
* onConfirm?, onCancel? — fired after the close animation lands
|
||||
* time — the engine time (for the open animation)
|
||||
* }
|
||||
|
|
@ -148,6 +198,11 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
|||
this.onConfirm = typeof spec.onConfirm === 'function' ? spec.onConfirm : null;
|
||||
this.onCancel = typeof spec.onCancel === 'function' ? spec.onCancel : null;
|
||||
|
||||
// Per-show size (the SET DESTINATION dialog is wider than ENGAGE).
|
||||
if (Number(spec.width) > 0) this.w = Number(spec.width);
|
||||
if (Number(spec.height) > 0) this.h = Number(spec.height);
|
||||
this.setSize(this.w, this.h);
|
||||
|
||||
const setBtn = (btn, label) => {
|
||||
const s = String(label ?? '').toUpperCase();
|
||||
btn.labelText.setText(s);
|
||||
|
|
@ -156,6 +211,12 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
|||
};
|
||||
setBtn(this.confirmBtn, spec.confirmLabel ?? 'CONFIRM');
|
||||
setBtn(this.cancelBtn, spec.cancelLabel ?? 'CANCEL');
|
||||
// The confirm button must FIT its label (the longest one is
|
||||
// SET DESTINATION) — grow the panel, then place both in the bottom
|
||||
// band, inside the panel edges.
|
||||
this._fitBtn(this.confirmBtn, String(spec.confirmLabel ?? 'CONFIRM').toUpperCase(), 110);
|
||||
this._fitBtn(this.cancelBtn, String(spec.cancelLabel ?? 'CANCEL').toUpperCase(), 88);
|
||||
this._layout();
|
||||
// Accent the confirm button's edge + hover glow.
|
||||
this.confirmBtn.style.stroke = this.accent;
|
||||
this.confirmBtn.style.neon = this.accent;
|
||||
|
|
|
|||
|
|
@ -520,8 +520,15 @@ export class GalaxyView {
|
|||
}
|
||||
|
||||
/** Stars whose centers have left the plate window are culled (their
|
||||
glow/core are textures — the only clean clip for them). */
|
||||
glow/core are textures — the only clean clip for them).
|
||||
|
||||
CRITICAL: this runs from setVisible() *after* the bulk hide/show, so it
|
||||
must respect the view's overall visibility — otherwise re-showing the
|
||||
in-plate stars here resurrects them and the whole galaxy layer bleeds
|
||||
through the system chart (the "galaxy stars behind it" bug). When the
|
||||
view is off, every star stays off, regardless of plate culling. */
|
||||
_applyCull() {
|
||||
const show = this._visible;
|
||||
for (const st of this._stars) {
|
||||
const inPlate =
|
||||
st.lx >= this.px &&
|
||||
|
|
@ -529,8 +536,8 @@ export class GalaxyView {
|
|||
st.ly >= this.py &&
|
||||
st.ly <= this.py + this.ph;
|
||||
st.culled = !inPlate;
|
||||
st.glow.setVisible(inPlate);
|
||||
st.core.setVisible(inPlate);
|
||||
st.glow.setVisible(show && inPlate);
|
||||
st.core.setVisible(show && inPlate);
|
||||
st.label.setVisible(this._labelShown(st));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -737,8 +737,16 @@ function paintChart(ctx, w, h, dpr, snap, view) {
|
|||
drawLabel(ctx, j.x, ly, j.name, j.sub);
|
||||
}
|
||||
|
||||
// 7 — FOG: dim everything the tether union doesn't cover
|
||||
drawFog(ctx, w, h, dpr, snap.tethers ?? [], tf);
|
||||
// 7 — FOG OF WAR: dim everything the tether union doesn't cover.
|
||||
// The fog is anchored to the player's tether reach — a CURRENT-system
|
||||
// concept. A CHARTED star's chart (the SYSTEM tab) has no tethers
|
||||
// (ship + tethers live only in the current system), so there is
|
||||
// nothing to anchor the fog to: applying it would paint the WHOLE
|
||||
// plate at fogAlpha (0.55) and wash the discovered planets / gates /
|
||||
// star down to ~45% — the "objects look semi-transparent" the player
|
||||
// reported. Skip it, and the charted objects read at full strength.
|
||||
const fogTethers = snap.tethers ?? [];
|
||||
if (fogTethers.length) drawFog(ctx, w, h, dpr, fogTethers, tf);
|
||||
|
||||
// 8 — the tether union boundary (over the fog, sharp)
|
||||
drawTetherArcs(ctx, snap.tethers ?? [], tf);
|
||||
|
|
@ -1325,6 +1333,11 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
_switchMode(id) {
|
||||
const inGalaxy = id === 'galaxy';
|
||||
this._mode = id;
|
||||
// The player is interacting — the boot reveal is a one-shot animation
|
||||
// and is done the moment they touch a tab. Finalize it so the plate
|
||||
// reads as a SOLID, fully-opaque surface: never a translucent overlay
|
||||
// mid-fade with the galaxy (or the game world) bleeding through.
|
||||
this._finalizeReveal();
|
||||
// move the plate's input + surface (the system chart ↔ the galaxy)
|
||||
setInteractiveEnabled(this.mapImg, !inGalaxy);
|
||||
this.mapImg.setVisible(!inGalaxy);
|
||||
|
|
@ -1340,7 +1353,13 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
this._pollNext = 0; // poll the galaxy life immediately
|
||||
} else {
|
||||
// back to a system chart (CURRENT SYSTEM or the picked SYSTEM) —
|
||||
// restore its stats chrome + force a fresh poll (redraw + stats)
|
||||
// restore its stats chrome + force a fresh poll (redraw + stats).
|
||||
// Belt-and-braces on the opacity: _finalizeReveal already pushed the
|
||||
// plate to full alpha, but assert it here too so a plate shown right
|
||||
// after a switch can never render translucent (the "galaxy behind
|
||||
// it" the player reported). The galaxy life is off, so nothing
|
||||
// legitimate sits under the plate to show through.
|
||||
this.mapImg.setAlpha(1);
|
||||
this.legendTexts?.forEach((t) => t.setVisible(true));
|
||||
this.navLabel.setText(this._navLabel0);
|
||||
this.resLabel.setText(this._resLabel0);
|
||||
|
|
@ -1353,6 +1372,22 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
this._paintTabs();
|
||||
}
|
||||
|
||||
/** Complete the boot-reveal timeline immediately: snap every revealed
|
||||
object to its final state (full alpha, rest y, unit scale) and clear
|
||||
the list so the per-frame driver (update()) stops dialing their
|
||||
alpha. Called on tab switch — the moment the player interacts, the
|
||||
one-shot open animation is over and everything is at full strength.
|
||||
No-op once the reveal has already run out. */
|
||||
_finalizeReveal() {
|
||||
if (!this.reveal || !this.reveal.length) return;
|
||||
for (const r of this.reveal) {
|
||||
r.o?.setAlpha?.(1);
|
||||
if (r.mode === 'rise' && r.baseY != null) r.o?.setY?.(r.baseY);
|
||||
else if (r.mode === 'pop') r.o?.setScale?.(1);
|
||||
}
|
||||
this.reveal = [];
|
||||
}
|
||||
|
||||
/** Build the galaxy life lazily (first tab switch). */
|
||||
_ensureGalaxy() {
|
||||
if (this._galaxy) return this._galaxy;
|
||||
|
|
@ -2512,7 +2547,8 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
* onSelect (the scene plots the course) and closes the console.
|
||||
* SYSTEM — SET DESTINATION on a charted star's object (the ship
|
||||
* isn't in that system, so no distance row); CONFIRM fires
|
||||
* onSetDestination (the destination model is still being defined).
|
||||
* onSetDestination (the scene plots the route — see the Route
|
||||
* section in PROJECT_NOTES), and the map closes.
|
||||
* CANCEL, the scrim, or ESC leave the map exactly as it was. While the
|
||||
* dialog is up the plate is inert (the guards in _startDrag /
|
||||
* _onWheelZoom / _setHover — the scrim covers the whole window, so it
|
||||
|
|
@ -2530,6 +2566,11 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
body: [`DESTINATION — ${label}`, sysName ? `SYSTEM — ${sysName}` : ''].filter(Boolean),
|
||||
accent: C.neon,
|
||||
confirmLabel: config.get('map.systemConfirm.label', 'SET DESTINATION'),
|
||||
// wider than AUTOPILOT's dialog — the SET DESTINATION label is
|
||||
// long, and it gets the room (button fits the text, text fits
|
||||
// the panel)
|
||||
width: config.get('map.systemConfirm.width', 520),
|
||||
height: config.get('map.systemConfirm.height', 176),
|
||||
onConfirm: () => {
|
||||
// the pick ripples once while the window fades out
|
||||
this._flash = { x: o.x + this.geo.mapX, y: o.y + this.geo.mapY, r: o.r, t0: this.scene.time.now };
|
||||
|
|
@ -2548,6 +2589,10 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
body,
|
||||
accent: C.amber,
|
||||
confirmLabel: config.get('map.confirm.label', 'ENGAGE'),
|
||||
// its own (narrower) size — the dialog instance is shared with the
|
||||
// wider SET DESTINATION one, so each call states its own footprint
|
||||
width: config.get('map.confirm.width', 440),
|
||||
height: config.get('map.confirm.height', 168),
|
||||
onConfirm: () => {
|
||||
// the pick ripples once while the window fades out
|
||||
this._flash = { x: o.x + this.geo.mapX, y: o.y + this.geo.mapY, r: o.r, t0: this.scene.time.now };
|
||||
|
|
|
|||
Loading…
Reference in New Issue