import { GameRuntimeStats, GameState } from '../types'; function clampNonNegativeInteger(value: unknown) { if (typeof value !== 'number' || !Number.isFinite(value)) return 0; return Math.max(0, Math.floor(value)); } function getIsoTimestamp(now: number) { return new Date(now).toISOString(); } export function createInitialGameRuntimeStats( options: { isActiveRun?: boolean; now?: number; } = {}, ): GameRuntimeStats { const now = options.now ?? Date.now(); return { playTimeMs: 0, lastPlayTickAt: options.isActiveRun ? getIsoTimestamp(now) : null, hostileNpcsDefeated: 0, questsAccepted: 0, itemsUsed: 0, scenesTraveled: 0, }; } export function normalizeGameRuntimeStats( stats: Partial | null | undefined, options: { isActiveRun?: boolean; now?: number; } = {}, ): GameRuntimeStats { const now = options.now ?? Date.now(); return { playTimeMs: typeof stats?.playTimeMs === 'number' && Number.isFinite(stats.playTimeMs) ? Math.max(0, stats.playTimeMs) : 0, lastPlayTickAt: options.isActiveRun ? getIsoTimestamp(now) : null, hostileNpcsDefeated: clampNonNegativeInteger(stats?.hostileNpcsDefeated), questsAccepted: clampNonNegativeInteger(stats?.questsAccepted), itemsUsed: clampNonNegativeInteger(stats?.itemsUsed), scenesTraveled: clampNonNegativeInteger(stats?.scenesTraveled), }; } export function incrementGameRuntimeStats( stats: GameRuntimeStats, increments: Partial>, ): GameRuntimeStats { return { ...stats, hostileNpcsDefeated: stats.hostileNpcsDefeated + clampNonNegativeInteger(increments.hostileNpcsDefeated), questsAccepted: stats.questsAccepted + clampNonNegativeInteger(increments.questsAccepted), itemsUsed: stats.itemsUsed + clampNonNegativeInteger(increments.itemsUsed), scenesTraveled: stats.scenesTraveled + clampNonNegativeInteger(increments.scenesTraveled), }; } export function syncGameRuntimePlayTime(stats: GameRuntimeStats, now = Date.now()): GameRuntimeStats { if (!stats.lastPlayTickAt) { return { ...stats, lastPlayTickAt: getIsoTimestamp(now), }; } const lastTickMs = Date.parse(stats.lastPlayTickAt); if (Number.isNaN(lastTickMs)) { return { ...stats, lastPlayTickAt: getIsoTimestamp(now), }; } return { ...stats, playTimeMs: stats.playTimeMs + Math.max(0, now - lastTickMs), lastPlayTickAt: getIsoTimestamp(now), }; } export function getLiveGamePlayTimeMs(stats: GameRuntimeStats, now = Date.now()) { if (!stats.lastPlayTickAt) return stats.playTimeMs; const lastTickMs = Date.parse(stats.lastPlayTickAt); if (Number.isNaN(lastTickMs)) return stats.playTimeMs; return stats.playTimeMs + Math.max(0, now - lastTickMs); } export function syncGameStatePlayTime(state: GameState, now = Date.now()): GameState { return { ...state, runtimeStats: syncGameRuntimePlayTime(state.runtimeStats, now), }; }