@hiddentao/clockwork-engine
Version:
A TypeScript/PIXI.js game engine for deterministic, replayable games with built-in rendering
180 lines (179 loc) • 6.29 kB
JavaScript
import { TIMER_CONSTANTS } from "./lib/internals";
export class Timer {
constructor() {
this.timers = new Map();
this.nextId = 1;
this.currentTick = 0;
this.updateStartTick = 0;
this.isUpdating = false;
}
/**
* Schedule a one-time callback to execute after the specified number of ticks
* @param callback Callback to execute
* @param ticks Number of ticks to wait before execution
* @returns Timer ID that can be used to cancel the timer
*/
setTimeout(callback, ticks) {
const id = this.nextId++;
// Use updateStartTick if we're currently updating, otherwise use currentTick
const baseTick = this.isUpdating ? this.updateStartTick : this.currentTick;
this.timers.set(id, {
id,
callback,
targetTick: baseTick + ticks,
isActive: true,
});
return id;
}
/**
* Schedule a repeating callback to execute every specified number of ticks
* @param callback Callback to execute
* @param ticks Number of ticks between executions
* @returns Timer ID that can be used to cancel the timer
*/
setInterval(callback, ticks) {
const id = this.nextId++;
// Use updateStartTick if we're currently updating, otherwise use currentTick
const baseTick = this.isUpdating ? this.updateStartTick : this.currentTick;
this.timers.set(id, {
id,
callback,
targetTick: baseTick + ticks,
interval: ticks,
isActive: true,
});
return id;
}
/**
* Cancel a timer
* @param id Timer ID returned from setTimeout or setInterval
* @returns True if timer was found and cancelled
*/
clearTimer(id) {
return this.timers.delete(id);
}
/**
* Update the timer system - called by GameEngine
* Executes all ready timers with proper error handling
*/
update(_deltaTicks, totalTicks) {
this.updateStartTick = this.currentTick;
this.currentTick = totalTicks;
this.isUpdating = true;
// Process timers until no more are ready to execute
let hasExecutions = true;
let maxIterations = TIMER_CONSTANTS.MAX_ITERATIONS;
let iterations = 0;
while (hasExecutions && iterations < maxIterations) {
hasExecutions = false;
iterations++;
const readyTimers = [];
// Collect all timers ready for execution, sorted by targetTick for deterministic order
for (const timer of this.timers.values()) {
if (timer.isActive && this.currentTick >= timer.targetTick) {
readyTimers.push(timer);
}
}
// Sort timers by target tick, then by ID for deterministic execution order
readyTimers.sort((a, b) => {
if (a.targetTick !== b.targetTick) {
return a.targetTick - b.targetTick;
}
return a.id - b.id;
});
if (readyTimers.length > 0) {
hasExecutions = true;
// Execute all ready timers with error handling
for (const timer of readyTimers) {
try {
timer.callback();
}
catch (error) {
console.error(`Timer ${timer.id} failed:`, error);
// Don't rethrow - we don't want one timer failure to break others
}
}
// Reschedule or remove timers after execution
for (const timer of readyTimers) {
if (timer.interval !== undefined && timer.isActive) {
// For repeating timers, handle different interval cases
if (timer.interval === 0) {
// Zero interval - execute once per update, don't loop infinitely
timer.targetTick = this.currentTick + 1;
hasExecutions = false; // Prevent infinite loop for zero intervals
}
else {
// Reschedule repeating timer
timer.targetTick += timer.interval;
}
}
else {
// Remove one-time timer
this.timers.delete(timer.id);
}
}
}
}
this.isUpdating = false;
}
/**
* Reset the timer system
* Clears all timers and resets the tick counter
*/
reset() {
this.timers.clear();
this.currentTick = 0;
// Don't reset nextId - tests expect it to keep incrementing across resets
}
/**
* Get the number of active timers
*/
getActiveTimerCount() {
return Array.from(this.timers.values()).filter((timer) => timer.isActive)
.length;
}
/**
* Get information about all active timers
*/
getTimerInfo() {
return Array.from(this.timers.values()).map((timer) => ({
id: timer.id,
targetTick: timer.targetTick,
ticksRemaining: Math.max(0, timer.targetTick - this.currentTick),
isRepeating: timer.interval !== undefined,
isActive: timer.isActive,
}));
}
/**
* Pause a specific timer
* @param id Timer ID
* @returns True if timer was found and paused
*/
pauseTimer(id) {
const timer = this.timers.get(id);
if (timer) {
timer.isActive = false;
return true;
}
return false;
}
/**
* Resume a paused timer
* @param id Timer ID
* @returns True if timer was found and resumed
*/
resumeTimer(id) {
const timer = this.timers.get(id);
if (timer) {
timer.isActive = true;
return true;
}
return false;
}
/**
* Get the total number of timers (including inactive ones)
*/
getTotalTimerCount() {
return this.timers.size;
}
}