import { type LonLatPoint } from '../../utils/projection' import { type GameLaunchEnvelope } from '../../utils/gameLaunch' import { type GameSessionState } from './gameSessionState' export interface RecoveryTelemetrySnapshot { distanceMeters: number currentSpeedKmh: number | null averageSpeedKmh: number | null heartRateBpm: number | null caloriesKcal: number | null lastGpsPoint: LonLatPoint | null lastGpsAt: number | null lastGpsAccuracyMeters: number | null } export interface RecoveryViewportSnapshot { zoom: number centerTileX: number centerTileY: number rotationDeg: number gpsLockEnabled: boolean hasGpsCenteredOnce: boolean } export interface RecoveryRuntimeSnapshot { gameState: GameSessionState telemetry: RecoveryTelemetrySnapshot viewport: RecoveryViewportSnapshot currentGpsPoint: LonLatPoint | null currentGpsAccuracyMeters: number | null currentGpsInsideMap: boolean bonusScore: number quizCorrectCount: number quizWrongCount: number quizTimeoutCount: number } export interface SessionRecoverySnapshot { schemaVersion: 1 savedAt: number launchEnvelope: GameLaunchEnvelope configAppId: string configVersion: string runtime: RecoveryRuntimeSnapshot } const SESSION_RECOVERY_STORAGE_KEY = 'cmr.sessionRecovery.v1' function cloneLonLatPoint(point: LonLatPoint | null): LonLatPoint | null { if (!point) { return null } return { lon: point.lon, lat: point.lat, } } function cloneGameSessionState(state: GameSessionState): GameSessionState { return { status: state.status, endReason: state.endReason, startedAt: state.startedAt, endedAt: state.endedAt, completedControlIds: state.completedControlIds.slice(), skippedControlIds: state.skippedControlIds.slice(), currentTargetControlId: state.currentTargetControlId, inRangeControlId: state.inRangeControlId, score: state.score, guidanceState: state.guidanceState, modeState: state.modeState ? JSON.parse(JSON.stringify(state.modeState)) as Record : null, } } export function cloneSessionRecoverySnapshot(snapshot: SessionRecoverySnapshot): SessionRecoverySnapshot { return { schemaVersion: 1, savedAt: snapshot.savedAt, launchEnvelope: JSON.parse(JSON.stringify(snapshot.launchEnvelope)) as GameLaunchEnvelope, configAppId: snapshot.configAppId, configVersion: snapshot.configVersion, runtime: { gameState: cloneGameSessionState(snapshot.runtime.gameState), telemetry: { distanceMeters: snapshot.runtime.telemetry.distanceMeters, currentSpeedKmh: snapshot.runtime.telemetry.currentSpeedKmh, averageSpeedKmh: snapshot.runtime.telemetry.averageSpeedKmh, heartRateBpm: snapshot.runtime.telemetry.heartRateBpm, caloriesKcal: snapshot.runtime.telemetry.caloriesKcal, lastGpsPoint: cloneLonLatPoint(snapshot.runtime.telemetry.lastGpsPoint), lastGpsAt: snapshot.runtime.telemetry.lastGpsAt, lastGpsAccuracyMeters: snapshot.runtime.telemetry.lastGpsAccuracyMeters, }, viewport: { zoom: snapshot.runtime.viewport.zoom, centerTileX: snapshot.runtime.viewport.centerTileX, centerTileY: snapshot.runtime.viewport.centerTileY, rotationDeg: snapshot.runtime.viewport.rotationDeg, gpsLockEnabled: snapshot.runtime.viewport.gpsLockEnabled, hasGpsCenteredOnce: snapshot.runtime.viewport.hasGpsCenteredOnce, }, currentGpsPoint: cloneLonLatPoint(snapshot.runtime.currentGpsPoint), currentGpsAccuracyMeters: snapshot.runtime.currentGpsAccuracyMeters, currentGpsInsideMap: snapshot.runtime.currentGpsInsideMap, bonusScore: snapshot.runtime.bonusScore, quizCorrectCount: snapshot.runtime.quizCorrectCount, quizWrongCount: snapshot.runtime.quizWrongCount, quizTimeoutCount: snapshot.runtime.quizTimeoutCount, }, } } function normalizeSessionRecoverySnapshot(raw: unknown): SessionRecoverySnapshot | null { if (!raw || typeof raw !== 'object') { return null } const candidate = raw as SessionRecoverySnapshot if (candidate.schemaVersion !== 1 || !candidate.runtime || !candidate.runtime.gameState) { return null } return cloneSessionRecoverySnapshot(candidate) } export function loadSessionRecoverySnapshot(): SessionRecoverySnapshot | null { try { return normalizeSessionRecoverySnapshot(wx.getStorageSync(SESSION_RECOVERY_STORAGE_KEY)) } catch { return null } } export function saveSessionRecoverySnapshot(snapshot: SessionRecoverySnapshot): void { try { wx.setStorageSync(SESSION_RECOVERY_STORAGE_KEY, cloneSessionRecoverySnapshot(snapshot)) } catch {} } export function clearSessionRecoverySnapshot(): void { try { wx.removeStorageSync(SESSION_RECOVERY_STORAGE_KEY) } catch {} }