File size: 3,156 Bytes
3570fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Central bridge between the imperative game core and the React UI.
//
// Data flows one way per concern:
//   game -> store   state the UI renders (mode label, loco, telemetry, boot
//                   milestones). Written via useGame.setState from game code;
//                   high-frequency values (telemetry) are throttled at the
//                   producer so React never re-renders at frame rate.
//   UI  -> gameApi  intents (set colour, switch loco, reset, open/close
//                   menu side effects). gameApi is a plain object populated
//                   once the game boots; optional-chained calls make the UI
//                   safe to interact with before that.
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";

export const useGame = create(
  subscribeWithSelector(() => ({
    // Boot lifecycle
    prebootDone: false, // title assets ready, grey veil dropped
    entered: false, // first "Waddle in" clicked (latches)
    menuOpen: false, // title / pause overlay up (opened by App after preboot)
    biosVisible: false, // BIOS readout playing over the scene
    bootDone: false,
    bootFailed: false,

    // Game state mirrored for the UI
    modeLabel: "Run",
    loco: "legs", // "legs" | "rollers" - what the game is actually running
    locoWant: "legs", // what the quickbar asked for (game reconciles)
    locoSwitching: false,
    rollersLoading: false, // OSD line while the roller stack streams in
    variant: "classic",
    padConnected: false,
    touchMode: false,
    ballActive: false,
    // Throttled telemetry block (4 Hz), bottom-right OSD
    telemetry: { fps: 0, ctrlHz: 0, speed: 0, odo: 0, peers: 0 },
  })),
);

// Imperative game surface, assigned by game/game.js once booted. The UI
// only ever optional-chains into it.
export const gameApi = {};

// Debug handles for the console / automated QA (same spirit as window.rl).
if (typeof window !== "undefined") {
  window.__store = useGame;
  window.__gameApi = gameApi;
}

// BIOS/POST milestone log. Plain module state on purpose: the readout
// component polls it inside its own paced replay loop (same as the old
// playBios), so no reactivity is needed and log spam never re-renders React.
// Entries: { label, status, raw, halt, progress } - status null = pending.
export const bootLog = [];

export const bootLine = (label) => {
  const entry = { label, status: null, raw: false, progress: null };
  bootLog.push(entry);
  const done = (status = "OK") => {
    entry.status = status;
  };
  done.progress = (p) => {
    entry.progress = p;
  };
  return done;
};

export const bootNote = (label) => {
  bootLog.push({ label, status: "", raw: true });
};

// Fatal boot failure: freeze the BIOS on the halt screen (the readout is
// the diagnostic surface). Drops the title overlay if still up.
export function bootHalt(detail) {
  const s = useGame.getState();
  if (s.bootFailed || s.bootDone) return;
  bootNote(`>> ${detail}`);
  bootLog.push({ label: "SYSTEM HALTED", status: "", raw: true, halt: true });
  useGame.setState({ bootFailed: true, menuOpen: false, biosVisible: true });
}