File size: 9,950 Bytes
d32ddf2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42b40d2
 
 
 
 
 
 
 
 
 
cae0952
 
 
 
1ff6f70
 
 
 
 
 
 
 
 
d32ddf2
 
 
 
18ec8e7
 
 
 
 
 
3d6d01c
 
 
 
 
 
 
 
 
 
 
 
25e82b6
 
 
 
 
 
 
 
 
 
 
 
1ad681c
 
 
 
 
4c8f450
 
 
 
 
 
bb92539
 
 
 
 
 
 
 
 
6bd2c72
 
 
 
 
 
 
 
b88cc9c
 
 
 
 
 
 
6bd2c72
 
 
 
450f6c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d32ddf2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/* ============================================================
   The live game session β€” the single mutable holder the UI modules read
   and boot.js reassigns. Before the split these were module-level `let`
   bindings in main.js that every function closed over; now they live on one
   shared object so hud / boot / save / the top-level listeners all see the
   same current game across a "choose another battlefield" restart.

   Read at CALL time (never destructured at module scope), exactly as the old
   closure vars were, so a fresh game swapped in by bootState is picked up by
   every consumer automatically.
   ============================================================ */

"use strict";

export const game = {
  state: null,   // the current engine game state (engine/state.js), or null on the splash screen
  input: null,   // the current input controller (input.js attachInput), or null before a game
  // The ADR-0004 seam every command-issuing UI module (input.js, inputCommands.js,
  // hudSelection.js) submits through β€” never null once a game is running, though which kind it
  // is varies with boot.js's own T-012 scoping: a real, server/session.js-backed
  // net/loopback.js transport for an ordinary skirmish, a live competition fixture, or a
  // loaded skirmish save; a net/directTransport.js adapter (ADR-0013) with no session behind it
  // β€” applying commands straight to `state`, no ownership/fog validation, no tick of its own β€”
  // for every boot path T-012 doesn't port yet (Odyssey, a scenario/raider/bounty, a spectated
  // match). Set by bootState/focusActivePlanet next to `state`/`input` above; UI code never
  // branches on which kind this is, by design (see ADR-0013).
  transport: null,
  // The Odyssey galaxy (engine/galaxy.js) when in open-world mode, else null. In
  // Odyssey `state` is the active planet's state = galaxy.planets.get(activeId);
  // credits + the other planets live on the galaxy. Read at call time like the rest.
  galaxy: null,
  // Which COMPETITION FIXTURE the running match belongs to, or null for an ordinary skirmish β€”
  // `{ kind: "gauntlet", index, opponent, humanName }` (docs/competitions-and-elo.md Phase 4).
  // Set by boot.js's startCompetitionMatch right after bootState (which clears it, exactly like
  // `galaxy` above), read at game-over by competition.js's captureCompetitionResult β€” which
  // records the result into the ledger and clears this again, so one finished match can only ever
  // be rated once β€” and by saveload.js's Home confirm, which has to warn that leaving a live
  // gauntlet match forfeits it. Never part of the sim or the persisted save: like `galaxy`, it
  // says which run this state belongs to, not anything about the state itself.
  competition: null,
  // Timestamp until which the supply readout flashes red after a blocked
  // production attempt: written by boot.js's frame-event pump, read by hud.js's
  // renderHUD. Kept here because it crosses that module boundary.
  supplyBlockedUntil: 0,
  // World-space {x,y} of the most recent under-attack alert, or null before the first one this
  // game β€” written by boot.js's frame-event pump (triggerUnderAttack), read by input.js's
  // Backspace "jump to last alert" keybind and by boot.js's own under-attack banner click
  // handler. Kept here, next to supplyBlockedUntil above, for the same reason: it crosses that
  // same module boundary.
  lastAttackAt: null,
  // { id, charge } of the player's Antimatter Gate as last observed by hud.js's renderHUD (P7
  // "Persistent Gate charge strip"), or null before the first sighting β€” lets the chip tell
  // "charge climbed since the last tick" from "charge is stalled" without engine/ carrying
  // UI-only comparison state (the engine side only ever knows the CURRENT charge, on the
  // building itself). Keyed by wonder id, not just the charge float, so a Gate razed mid-charge
  // and rebuilt (engine/wonder.js's own header documents that risk) starts its own clean
  // comparison instead of reading as stalled against the dead Gate's final charge. Reset by
  // hud.js's resetPanelSignature() β€” the same per-boot/per-jump reset the topbar signature
  // itself already goes through, so a new world's Gate never inherits the old one's charge
  // history (the exact bug class boot.js's resetWorldUiBookkeeping comment documents for
  // gateMilestone/lastAttackAt).
  lastGateCharge: null,
  // Live per-world alert badges for the Odyssey starmap (P6 "Starmap live colony ledger"):
  // planetId -> {type, at}, written by boot.js's notifyColony for every attack/hostile/lost
  // notification a background colony raises (engine/galaxy.js sweepColonies) β€” the SAME trigger
  // that already fires the transient toast, just remembered here too so starmap.js's
  // renderStarmap can badge a world at a glance instead of relying on whichever toast the player
  // happened to catch. `at` is a performance.now() timestamp; starmap.js treats an attack/hostile
  // entry as "live" only within its own ~30s window, while a "lost" entry persists as a badge
  // until boot.js's focusActivePlanet clears that ONE planet's entry β€” i.e. until the player
  // actually jumps back and looks. Reset to {} on every fresh boot (bootState), like `groups`
  // below; never part of the deterministic sim or the persisted save (same reasoning as
  // lastAttackAt above β€” this crosses the boot.js/starmap.js module boundary, nothing more).
  colonyAlerts: {},
  // Control groups, keyed per planet id β†’ { digit: [unitIds] }. Lives on the session (not
  // in the per-game input controller) so a group survives an Odyssey jump β€” which tears down
  // and rebuilds attachInput β€” and can be shown in the HUD and persisted UI-side. Never part
  // of the deterministic sim; kept out of engine/persist.js's sanitized save payload.
  groups: {},
  // The player's current formation choice (engine/formation.js FORMATION_SHAPES /
  // LEADER_POSITIONS) β€” a UI preference, not per-game state, so it's fine to just carry over
  // across a "choose another battlefield" restart like any other setup preference. Read by
  // input.js when issuing a move/attack-move/Hold-Formation order, and mutated directly by
  // hudSelection.js's formation picker buttons.
  formation: { shape: "grid", leaderPos: "front" },
  // Which collapsible panel sections (the Build submenu's per-category groups, the CC/Barracks
  // Produce lists, the Refinery/Datacenter Research lists, Market, the freighter Cargo list, the
  // Spaceport Jump list, …) the player has manually collapsed β€” a UI preference like `formation`
  // above, not per-unit/per-game state, so collapsing one keeps it collapsed across every later
  // selection until toggled back. A key ABSENT from the set means "expanded" (starts open,
  // unchanged behaviour) β€” so a new collapsible section needs zero migration code here, it just
  // starts expanded like everything else. Keys are the stable strings passed to hudSelection.js's
  // sectionToggle(), e.g. "build:economy", "cc:produce", "market".
  collapsedSections: new Set(),
  // Observer Mode (observer.js): a free-look, read-only spectator over the Odyssey galaxy β€”
  // fog reveals, any world/base is a free camera jump, no orders can be issued. Never touches
  // `state`/`galaxy.activeId` or the real input camera, so exiting always resumes normal play
  // exactly where it was left; never part of the deterministic sim or the persisted save.
  observerMode: false,
  // Which world is currently being spectated while observerMode is on β€” a planetId, or null
  // when observerMode is off. Defaults to the real active world on entry.
  spectateId: null,
  // The throwaway state observer.js is showing for a DORMANT world (engine/galaxy.js
  // previewPlanet), or null whenever the spectated world is one the galaxy is really simulating.
  // Only a seeded handful of worlds are alive from turn one (BACKGROUND_WORLDS), but Observer
  // Mode's promise is a free look at ANY world β€” so a dormant one is shown as the pristine world
  // it would be on arrival, WITHOUT waking it. Never ticked, never registered on the galaxy,
  // never saved: the read-only guarantee above holds.
  spectatePreview: null,
  // The observer's own camera (camera.js createCamera shape), separate from the real
  // game.input camera so panning/zooming while spectating never disturbs normal play's view β€”
  // and so clamping uses whichever world's map is actually being looked at, not the active one's.
  observerCamera: null,
  // A WATCHED AI-vs-AI exhibition match (docs/competitions-and-elo.md Phase 5), or null for every
  // ordinary game β€” `{ aName, bName, world, seed, difficulty, matchTimeLimit, onLeave }`. Set by
  // boot.js's startSpectatedMatch right after bootState (which clears it, exactly like `galaxy`
  // and `competition` above) and read in three places: boot.js's loop, which drives BOTH seats
  // through tools/selfplay.js's tickSelfPlay instead of the ordinary tick(); observer.js, which
  // lets Observer Mode be entered without an Odyssey when β€” and only when β€” this is set (a
  // spectated match is one the human is NOT playing, so revealing its fog is not a cheat); and the
  // spectate UI (observerPanel.js), whose Leave button calls `onLeave` rather than importing
  // competition.js and closing a new module cycle. Never part of the sim or the persisted save β€”
  // saveShape.js's resumableMode refuses to checkpoint a match this flag marks.
  spectateMatch: null,
  // The spectated match's speed multiplier β€” one of observer.js's SPECTATE_SPEEDS (1/2/4/8).
  // engine/loop.js reads it live through its `speed` option, and scales the SIM TIME a real second
  // buys, never the fixed timestep itself. Ignored (pinned to 1) whenever spectateMatch is null,
  // so an ordinary game can never inherit a leftover multiplier.
  spectateSpeed: 1,
};