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';
import type { GameState } from '../engine/game';
/**
* Pure mapping from engine state to the view model the React shell renders.
* No React, no store — just data shaping, so it can be unit-tested directly.
*/
export interface ResourceView {
id: string;
name: string;
amount: number;
}
export interface GameView {
resources: ResourceView[];
activeActionId: string | null;
actionName: string | null;
/** Progress of the active action, clamped to 0..1. */
actionProgress: number;
}
export function toView(state: GameState, content: Content): GameView {
const resources: ResourceView[] = content.resources.map((resource) => ({
id: resource.id,
name: resource.name,
amount: state.resources[resource.id] ?? 0,
}));
const action = state.activeActionId ? content.actionsById[state.activeActionId] : undefined;
const actionProgress = action ? Math.min(1, state.actionElapsedMs / action.durationMs) : 0;
return {
resources,
activeActionId: state.activeActionId,
actionName: action ? action.name : null,
actionProgress,
};
}
/** Render an offline gap as `45s`, `1m 30s`, `2m`, `1h 1m`, `2h`. */
export function formatOfflineDuration(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
if (totalSeconds < 60) {
return `${totalSeconds}s`;
}
const totalMinutes = Math.floor(totalSeconds / 60);
if (totalMinutes < 60) {
const seconds = totalSeconds % 60;
return seconds === 0 ? `${totalMinutes}m` : `${totalMinutes}m ${seconds}s`;
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
}