103 lines
3.0 KiB
TypeScript
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),
|
|
};
|
|
}
|
|
|