Files
Genarrative/src/data/runtimeStats.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

103 lines
3.0 KiB
TypeScript

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<GameRuntimeStats> | 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<Pick<GameRuntimeStats, 'hostileNpcsDefeated' | 'questsAccepted' | 'itemsUsed' | 'scenesTraveled'>>,
): 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),
};
}