ugly-game 0.7.4 → 0.7.6
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.
- package/dist/loop/simCore.js +18 -10
- package/dist/loop/simHost.d.ts +4 -0
- package/dist/loop/simHost.js +5 -5
- package/dist/loop/simHost.worker.js +5 -3
- package/package.json +1 -1
package/dist/loop/simCore.js
CHANGED
|
@@ -8,11 +8,16 @@ export function soloStepBatch(opts, acc) {
|
|
|
8
8
|
const maxCatch = opts.maxCatchUpFrames ?? 6;
|
|
9
9
|
const now = opts.clock.now();
|
|
10
10
|
const elapsed = Math.max(0, (now - opts.lastRef.t) / 1000);
|
|
11
|
-
opts.lastRef.t = now;
|
|
12
|
-
let a =
|
|
13
|
-
|
|
14
|
-
a
|
|
15
|
-
opts.ctl.
|
|
11
|
+
opts.lastRef.t = now; // keep the clock base current even while paused, so resuming doesn't spend a huge catch-up
|
|
12
|
+
let a = acc;
|
|
13
|
+
// Only derive a wall-clock target while PLAYING; paused leaves targetTick where it was (coopStepBatch won't chase
|
|
14
|
+
// past it) so a paused solo world holds still. STEP_BUDGET still drains inside coopStepBatch.
|
|
15
|
+
if (opts.ctl.playing) {
|
|
16
|
+
a = Math.min(acc + elapsed, maxCatch * T);
|
|
17
|
+
const frames = Math.floor(a / T);
|
|
18
|
+
a -= frames * T;
|
|
19
|
+
opts.ctl.targetTick = opts.tickOf(opts.state) + frames;
|
|
20
|
+
}
|
|
16
21
|
const stepped = coopStepBatch({
|
|
17
22
|
sim: opts.sim,
|
|
18
23
|
state: opts.state,
|
|
@@ -53,14 +58,17 @@ export function coopStepBatch(opts) {
|
|
|
53
58
|
opts.sim.tick(opts.state, due.map((d) => d.c));
|
|
54
59
|
stepped++;
|
|
55
60
|
};
|
|
56
|
-
// STEP_BUDGET burst first (forced, deterministic — independent of the target
|
|
61
|
+
// STEP_BUDGET burst first (forced, deterministic — independent of the target AND of play/pause, so a harness can
|
|
62
|
+
// advance a paused world).
|
|
57
63
|
let budget = opts.ctl.takeStepBudget();
|
|
58
64
|
while (budget-- > 0 && stepped < maxCatch)
|
|
59
65
|
stepOnce();
|
|
60
|
-
// Then chase the target.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
// Then chase the target — but only while PLAYING. Pausing freezes the sim without touching the queue or budget.
|
|
67
|
+
if (opts.ctl.playing) {
|
|
68
|
+
const target = opts.ctl.targetTick;
|
|
69
|
+
while (opts.tickOf(opts.state) < target && stepped < maxCatch)
|
|
70
|
+
stepOnce();
|
|
71
|
+
}
|
|
64
72
|
return stepped;
|
|
65
73
|
}
|
|
66
74
|
export function fixedStepBatch(opts, acc) {
|
package/dist/loop/simHost.d.ts
CHANGED
|
@@ -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>;
|
package/dist/loop/simHost.js
CHANGED
|
@@ -23,6 +23,7 @@ export function createSimHost(opts) {
|
|
|
23
23
|
const probe = () => opts.sim.probeHash ? opts.sim.probeHash(state) : opts.sim.hash(state);
|
|
24
24
|
const sampled = new Map(); // our probe hash per grid tick (for divergence compare)
|
|
25
25
|
let lastHash = opts.sim.hash(state);
|
|
26
|
+
ctl.playing = true; // games start playing; the sim honors ctl.playing (pause freezes stepping, STEP_BUDGET aside)
|
|
26
27
|
// ── Worker path ──────────────────────────────────────────────
|
|
27
28
|
// request()/reply round-trip: the game's worker `onMessage` answers a diagnostics request by posting a reply that
|
|
28
29
|
// echoes `rid`; we resolve the matching pending Promise here. Keyed on `rid` alone, so it's oblivious to payload.
|
|
@@ -56,7 +57,6 @@ export function createSimHost(opts) {
|
|
|
56
57
|
coop: !!opts.coop,
|
|
57
58
|
...opts.initParams,
|
|
58
59
|
});
|
|
59
|
-
ctl.playing = true;
|
|
60
60
|
}
|
|
61
61
|
// ── Lockstep driver (co-op) — lives on the main thread; feeds commands to the queue (fallback) or worker. ──
|
|
62
62
|
const enqueue = (s) => {
|
|
@@ -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
|
},
|
|
@@ -10,7 +10,6 @@ export function runSimWorker(cfg) {
|
|
|
10
10
|
let state = null;
|
|
11
11
|
let ctl = null;
|
|
12
12
|
let acc = 0;
|
|
13
|
-
let coop = false;
|
|
14
13
|
const clock = { now: () => performance.now() };
|
|
15
14
|
const lastRef = { t: 0 };
|
|
16
15
|
let fps = 60;
|
|
@@ -23,7 +22,9 @@ export function runSimWorker(cfg) {
|
|
|
23
22
|
const onLate = (n) => {
|
|
24
23
|
c.addLate(n);
|
|
25
24
|
};
|
|
26
|
-
|
|
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) {
|
|
27
28
|
coopStepBatch({
|
|
28
29
|
sim: cfg.sim,
|
|
29
30
|
state,
|
|
@@ -84,7 +85,8 @@ export function runSimWorker(cfg) {
|
|
|
84
85
|
state = layout.attach(init.dataBuf);
|
|
85
86
|
ctl = createControlView(init.ctlBuf, init.scalars ?? []);
|
|
86
87
|
fps = init.simFps ?? 60;
|
|
87
|
-
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).
|
|
88
90
|
ctl.running = true;
|
|
89
91
|
lastRef.t = clock.now();
|
|
90
92
|
loop();
|