feat: bootstrap walking skeleton
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 is the *same* code path: a large `now` delta simply owes many
|
||||
* ticks. `maxTicks` caps how many run per call so a huge delta can't lock the
|
||||
* thread; the remainder stays in the accumulator and drains on the next call,
|
||||
* so no ticks are ever lost. Clamping the *credited* offline window lives in the
|
||||
* save layer (T3.3), not here.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user