ugly-game 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,10 @@ export interface CoopOpts {
19
19
  onNeedSnap?: (from: string) => void;
20
20
  onSnapReady?: (from: string) => void;
21
21
  onLate?: (n: number) => void;
22
+ /** Whether co-op is active from construction (default true). Pass false for OPT-IN co-op (no ambient socket): the
23
+ * lockstep driver + transport are wired but `ctl.coop` stays false — the game flips it on join/leave, and until
24
+ * then the sim free-runs solo. */
25
+ autoStart?: boolean;
22
26
  }
23
27
  export interface SimHostOpts<State, Input> {
24
28
  layout: Layout<State>;
@@ -80,7 +80,7 @@ export function createSimHost(opts) {
80
80
  onSnapReady: opts.coop.onSnapReady ?? (() => undefined),
81
81
  },
82
82
  });
83
- ctl.coop = true;
83
+ ctl.coop = opts.coop.autoStart ?? true; // opt-in co-op leaves this false until the game joins a session
84
84
  }
85
85
  // ── Per-frame driver ─────────────────────────────────────────
86
86
  // Co-op bookkeeping runs on the MAIN thread from the shared state, so the local sample and the value sent to
@@ -91,7 +91,7 @@ export function createSimHost(opts) {
91
91
  let acc = 0;
92
92
  const hashEvery = opts.coop?.hashEvery ?? 600;
93
93
  const drive = (nowMs, doStep) => {
94
- if (lockstep) {
94
+ if (lockstep && ctl.coop) {
95
95
  const tk = worker ? ctl.tick : opts.tickOf(state);
96
96
  if (tk % hashEvery === 0 && !sampled.has(tk))
97
97
  sampled.set(tk, probe());
@@ -142,8 +142,8 @@ export function createSimHost(opts) {
142
142
  },
143
143
  available: useWorker,
144
144
  send(input) {
145
- if (lockstep)
146
- lockstep.send(input); // stamped + forwarded; comes back via onCommand
145
+ if (lockstep && ctl.coop)
146
+ lockstep.send(input); // co-op: stamped + forwarded; comes back via onCommand
147
147
  else
148
148
  enqueue({ c: input, at: opts.tickOf(state) + 1 }); // solo: apply next tick
149
149
  },
@@ -20,5 +20,6 @@ export interface SimWorkerConfig<S, I> {
20
20
  mirror?: (s: S, ctl: ControlView<string>) => void;
21
21
  beforeTick?: (due: Stamped<I>[], state: S) => void;
22
22
  onMessage?: (m: Record<string, unknown>, ctx: SimWorkerContext<S>) => void;
23
+ onReinit?: (ctx: SimWorkerContext<S>) => void;
23
24
  }
24
25
  export declare function runSimWorker<S, I>(cfg: SimWorkerConfig<S, I>): void;
@@ -3,11 +3,13 @@ import { soloStepBatch, coopStepBatch, } from './simCore';
3
3
  import { CommandQueue } from './commandQueue';
4
4
  export function runSimWorker(cfg) {
5
5
  const ctx = globalThis;
6
+ const post = (msg) => {
7
+ ctx.postMessage(msg);
8
+ };
6
9
  const queue = new CommandQueue();
7
10
  let state = null;
8
11
  let ctl = null;
9
12
  let acc = 0;
10
- let coop = false;
11
13
  const clock = { now: () => performance.now() };
12
14
  const lastRef = { t: 0 };
13
15
  let fps = 60;
@@ -20,7 +22,9 @@ export function runSimWorker(cfg) {
20
22
  const onLate = (n) => {
21
23
  c.addLate(n);
22
24
  };
23
- if (coop) {
25
+ // Co-op mode is read from the control block EVERY loop (not fixed at init): a game can join/leave a session
26
+ // at runtime (co-op is opt-in — no ambient socket), toggling ctl.coop from the main thread.
27
+ if (ctl.coop) {
24
28
  coopStepBatch({
25
29
  sim: cfg.sim,
26
30
  state,
@@ -81,7 +85,8 @@ export function runSimWorker(cfg) {
81
85
  state = layout.attach(init.dataBuf);
82
86
  ctl = createControlView(init.ctlBuf, init.scalars ?? []);
83
87
  fps = init.simFps ?? 60;
84
- coop = !!init.coop;
88
+ // ctl.coop is set by the main thread (createSimHost) before init and read live each loop — the worker doesn't
89
+ // latch it here (co-op can start/stop mid-session).
85
90
  ctl.running = true;
86
91
  lastRef.t = clock.now();
87
92
  loop();
@@ -98,6 +103,8 @@ export function runSimWorker(cfg) {
98
103
  queue.clear();
99
104
  lastRef.t = clock.now();
100
105
  acc = 0;
106
+ if (ctl)
107
+ cfg.onReinit?.({ state, ctl, post });
101
108
  }
102
109
  else if (m.t === 'cmd') {
103
110
  queue.push(m.s);
@@ -108,13 +115,7 @@ export function runSimWorker(cfg) {
108
115
  }
109
116
  else if (state && ctl) {
110
117
  // A message this runner doesn't own — hand it to the game (diagnostics requests, etc.).
111
- cfg.onMessage?.(m, {
112
- state,
113
- ctl,
114
- post: (msg) => {
115
- ctx.postMessage(msg);
116
- },
117
- });
118
+ cfg.onMessage?.(m, { state, ctl, post });
118
119
  }
119
120
  };
120
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-game",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {