feat: bootstrap walking skeleton

This commit is contained in:
ginnoir
2026-06-11 17:06:52 -05:00
commit ec8f857845
44 changed files with 6677 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import type { Content } from '../content/schema';
/**
* Core game state and per-tick simulation.
*
* Pure TS — no React, no DOM, no wall clock. The tick loop (tickLoop.ts) drives
* `tickGame` once per tick; `now`/scheduling lives entirely outside this module.
*/
export interface GameState {
/** resourceId -> current amount. */
resources: Record<string, number>;
/** The action currently running, or null. */
activeActionId: string | null;
/** Progress of the active action, in milliseconds. */
actionElapsedMs: number;
}
export function createGameState(content: Content): GameState {
const resources: Record<string, number> = {};
for (const resource of content.resources) {
resources[resource.id] = resource.startAmount;
}
return { resources, activeActionId: null, actionElapsedMs: 0 };
}
/** Begin running an action, resetting its progress. Throws on an unknown id. */
export function startAction(state: GameState, content: Content, actionId: string): void {
if (!content.actionsById[actionId]) {
throw new Error(`Unknown action "${actionId}"`);
}
state.activeActionId = actionId;
state.actionElapsedMs = 0;
}
/**
* Advance the active action by `tickMs`. Each time it reaches its duration it
* grants its yield and repeats, carrying the remainder — so one large tick (the
* offline catch-up path) can complete an action many times.
*/
export function tickGame(state: GameState, content: Content, tickMs: number): void {
if (!state.activeActionId) {
return;
}
const action = content.actionsById[state.activeActionId];
if (!action) {
return;
}
state.actionElapsedMs += tickMs;
while (state.actionElapsedMs >= action.durationMs) {
state.actionElapsedMs -= action.durationMs;
state.resources[action.yields.resourceId] += action.yields.amount;
}
}