Files
idlegame/src/engine/tickLoop.ts
T

76 lines
2.2 KiB
TypeScript

/**
* Fixed-timestep tick loop with an accumulator.
*
* The engine advances in whole ticks of `tickMs`. `advance(loop, now)` folds the
* elapsed wall-clock time into an accumulator and drains it one tick at a time,
* keeping the sub-tick remainder so results are independent of how the caller
* chunks calls (determinism — see the test suite).
*
* Offline catch-up uses this same path: a large `now` delta owes many ticks.
* `maxTicks` caps work per `advance()` call; the accumulator retains the
* remainder so no ticks are lost across calls.
*
* The save layer (`save.ts`) separately clamps how much *wall-clock* elapsed
* time is credited on load via `DEFAULT_MAX_OFFLINE_MS`. Tick loop batching
* and offline credit clamping are independent concerns.
*/
export const TICK_HZ = 10;
export const TICK_MS = 1000 / TICK_HZ;
export const DEFAULT_MAX_TICKS_PER_ADVANCE = 100_000;
export interface TickLoop {
/** Milliseconds of simulated time per tick. */
readonly tickMs: number;
/** Total ticks run over this loop's lifetime. */
tickCount: number;
/** Sub-tick wall-clock remainder, in milliseconds. */
accumulatorMs: number;
/** Last timestamp seen, or null until the first `advance` establishes a baseline. */
lastNow: number | null;
}
export function createTickLoop(opts?: { tickMs?: number; startNow?: number }): TickLoop {
const tickMs = opts?.tickMs ?? TICK_MS;
if (!(tickMs > 0)) {
throw new RangeError(`tickMs must be > 0, got ${tickMs}`);
}
return {
tickMs,
tickCount: 0,
accumulatorMs: 0,
lastNow: opts?.startNow ?? null,
};
}
/**
* Advance the loop to `now`, running owed ticks (capped at `maxTicks`).
* Returns the number of ticks run this call.
*/
export function advance(
loop: TickLoop,
now: number,
onTick?: (tickIndex: number) => void,
maxTicks: number = DEFAULT_MAX_TICKS_PER_ADVANCE,
): number {
if (loop.lastNow === null) {
loop.lastNow = now;
return 0;
}
const elapsed = now - loop.lastNow;
loop.lastNow = now;
if (elapsed > 0) {
loop.accumulatorMs += elapsed;
}
let ran = 0;
while (loop.accumulatorMs >= loop.tickMs && ran < maxTicks) {
loop.accumulatorMs -= loop.tickMs;
loop.tickCount += 1;
ran += 1;
onTick?.(loop.tickCount);
}
return ran;
}