File size: 9,128 Bytes
d32ddf2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* ============================================================
   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 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,
};