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