Exact source used by this build. SHA-256: 05c7a2d04c58a32e2c3ff8ed781b55727b6e32d5049a688ed7e9808be7a60f08.
1 import { systemRegistry } from "./systems";
2 import type { SimEvent, SimRules, WorldState } from "./types";
3
4 export const GAME_START_MINUTE = 6 * 60;
5 export const GAME_END_MINUTE = 26 * 60;
6 export const CLOCK_TICK_MINUTES = 10;
7 export const NORMAL_MS_PER_GAME_MINUTE = 700;
8
9 export interface ClockAdvanceResult { state: WorldState; events: SimEvent[] }
10
11 /**
12 * Advance only whole ten-minute ticks. Wall-clock time is supplied by the caller,
13 * keeping this function deterministic and safe to run in a Durable Object.
14 */
15 export function advanceClock(
16 state: WorldState,
17 realMsElapsed: number,
18 status: { playersOnline: number; paused: boolean; clockRateMsPerGameMinute?: number },
19 rules: SimRules = { crops: {} },
20 ): ClockAdvanceResult {
21 if (!Number.isFinite(realMsElapsed) || realMsElapsed <= 0 || status.paused || state.paused) return { state, events: [] };
22 if (status.playersOnline <= 0) return systemRegistry.runHook("onOfflineElapsed", state, rules, { elapsedMs: realMsElapsed });
23 // The caller may supply the active area's rate for a deep-cavern style area.
24 const rate = status.clockRateMsPerGameMinute ?? NORMAL_MS_PER_GAME_MINUTE;
25 if (!Number.isFinite(rate) || rate <= 0) return { state, events: [] };
26 let next = state;
27 const events: SimEvent[] = [];
28 const tickMs = rate * CLOCK_TICK_MINUTES;
29 let remainingMs = realMsElapsed;
30 while (remainingMs > 0 || next.clock.realMsRemainder >= tickMs) {
31 // Festival main events freeze only the calendar. Activities still receive each
32 // injected millisecond once; offline and explicit-pause rules remain above.
33 if (systemRegistry.pausesClock(next, rules)) {
34 if (remainingMs <= 0) break;
35 const elapsedMs = Math.min(remainingMs, systemRegistry.getClockPauseRemainingMs(next, rules));
36 const elapsed = systemRegistry.runHook("onElapsedTime", next, rules, { elapsedMs });
37 next = elapsed.state;
38 events.push(...elapsed.events);
39 remainingMs -= elapsedMs;
40 continue;
41 }
42 const elapsedMs = Math.min(remainingMs, Math.max(0, tickMs - next.clock.realMsRemainder));
43 if (elapsedMs > 0) {
44 const elapsed = systemRegistry.runHook("onElapsedTime", next, rules, { elapsedMs });
45 next = { ...elapsed.state, clock: { ...elapsed.state.clock, realMsRemainder: elapsed.state.clock.realMsRemainder + elapsedMs } };
46 events.push(...elapsed.events);
47 remainingMs -= elapsedMs;
48 }
49 if (next.clock.realMsRemainder < tickMs) break;
50 next = { ...next, revision: next.revision + 1, clock: { ...next.clock, minuteOfDay: next.clock.minuteOfDay + CLOCK_TICK_MINUTES, realMsRemainder: next.clock.realMsRemainder - tickMs, machineMinutes: next.clock.machineMinutes + CLOCK_TICK_MINUTES } };
51 const result = systemRegistry.runHook("onClockTick", next, rules, { elapsedMs: tickMs });
52 next = result.state;
53 events.push(...result.events);
54 }
55 return { state: next, events };
56 }
57