c1cc70eeb9
Full rebrand: UI display text, package scope (@ai-tycoon/* -> @token-empire/*), localStorage keys, Docker/CI image paths, database names, and documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
import type { GameState } from '@token-empire/shared';
|
|
import { processTick } from './tick';
|
|
|
|
export interface GameEngineCallbacks {
|
|
getState: () => GameState;
|
|
setState: (partial: Partial<GameState>) => void;
|
|
onTick?: (tickCount: number) => void;
|
|
onEraChange?: (era: GameState['meta']['currentEra']) => void;
|
|
}
|
|
|
|
export class GameEngine {
|
|
private callbacks: GameEngineCallbacks;
|
|
private lastFrameTime = 0;
|
|
private accumulator = 0;
|
|
private animFrameId: number | null = null;
|
|
private tickIntervalMs = 1000;
|
|
|
|
constructor(callbacks: GameEngineCallbacks) {
|
|
this.callbacks = callbacks;
|
|
}
|
|
|
|
start(): void {
|
|
if (this.animFrameId !== null) return;
|
|
this.lastFrameTime = performance.now();
|
|
this.accumulator = 0;
|
|
this.loop(this.lastFrameTime);
|
|
}
|
|
|
|
stop(): void {
|
|
if (this.animFrameId !== null) {
|
|
cancelAnimationFrame(this.animFrameId);
|
|
this.animFrameId = null;
|
|
}
|
|
}
|
|
|
|
setSpeed(speed: number): void {
|
|
this.tickIntervalMs = 1000 / speed;
|
|
}
|
|
|
|
processOfflineTicks(missedTicks: number): { revenue: number; expenses: number; ticksProcessed: number } {
|
|
let totalRevenue = 0;
|
|
let totalExpenses = 0;
|
|
|
|
for (let i = 0; i < missedTicks; i++) {
|
|
const state = this.callbacks.getState();
|
|
const result = processTick(state);
|
|
this.callbacks.setState(result);
|
|
totalRevenue += result.economy?.revenuePerTick ?? 0;
|
|
totalExpenses += result.economy?.expensesPerTick ?? 0;
|
|
}
|
|
|
|
return { revenue: totalRevenue, expenses: totalExpenses, ticksProcessed: missedTicks };
|
|
}
|
|
|
|
private loop = (now: number): void => {
|
|
const delta = now - this.lastFrameTime;
|
|
this.lastFrameTime = now;
|
|
|
|
const state = this.callbacks.getState();
|
|
if (!state.meta.isPaused) {
|
|
this.accumulator += delta;
|
|
|
|
let ticksThisFrame = 0;
|
|
const maxTicksPerFrame = 10;
|
|
|
|
while (this.accumulator >= this.tickIntervalMs && ticksThisFrame < maxTicksPerFrame) {
|
|
const currentState = this.callbacks.getState();
|
|
const result = processTick(currentState);
|
|
this.callbacks.setState(result);
|
|
this.accumulator -= this.tickIntervalMs;
|
|
ticksThisFrame++;
|
|
this.callbacks.onTick?.(currentState.meta.tickCount + 1);
|
|
}
|
|
|
|
if (this.accumulator > this.tickIntervalMs * maxTicksPerFrame) {
|
|
this.accumulator = 0;
|
|
}
|
|
}
|
|
|
|
this.animFrameId = requestAnimationFrame(this.loop);
|
|
};
|
|
}
|