sessionRecovery.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { type LonLatPoint } from '../../utils/projection'
  2. import { type GameLaunchEnvelope } from '../../utils/gameLaunch'
  3. import { type GameSessionState } from './gameSessionState'
  4. export interface RecoveryTelemetrySnapshot {
  5. distanceMeters: number
  6. currentSpeedKmh: number | null
  7. averageSpeedKmh: number | null
  8. heartRateBpm: number | null
  9. caloriesKcal: number | null
  10. lastGpsPoint: LonLatPoint | null
  11. lastGpsAt: number | null
  12. lastGpsAccuracyMeters: number | null
  13. }
  14. export interface RecoveryViewportSnapshot {
  15. zoom: number
  16. centerTileX: number
  17. centerTileY: number
  18. rotationDeg: number
  19. gpsLockEnabled: boolean
  20. hasGpsCenteredOnce: boolean
  21. }
  22. export interface RecoveryRuntimeSnapshot {
  23. gameState: GameSessionState
  24. telemetry: RecoveryTelemetrySnapshot
  25. viewport: RecoveryViewportSnapshot
  26. currentGpsPoint: LonLatPoint | null
  27. currentGpsAccuracyMeters: number | null
  28. currentGpsInsideMap: boolean
  29. bonusScore: number
  30. quizCorrectCount: number
  31. quizWrongCount: number
  32. quizTimeoutCount: number
  33. }
  34. export interface SessionRecoverySnapshot {
  35. schemaVersion: 1
  36. savedAt: number
  37. launchEnvelope: GameLaunchEnvelope
  38. configAppId: string
  39. configVersion: string
  40. runtime: RecoveryRuntimeSnapshot
  41. }
  42. const SESSION_RECOVERY_STORAGE_KEY = 'cmr.sessionRecovery.v1'
  43. function cloneLonLatPoint(point: LonLatPoint | null): LonLatPoint | null {
  44. if (!point) {
  45. return null
  46. }
  47. return {
  48. lon: point.lon,
  49. lat: point.lat,
  50. }
  51. }
  52. function cloneGameSessionState(state: GameSessionState): GameSessionState {
  53. return {
  54. status: state.status,
  55. endReason: state.endReason,
  56. startedAt: state.startedAt,
  57. endedAt: state.endedAt,
  58. completedControlIds: state.completedControlIds.slice(),
  59. skippedControlIds: state.skippedControlIds.slice(),
  60. currentTargetControlId: state.currentTargetControlId,
  61. inRangeControlId: state.inRangeControlId,
  62. score: state.score,
  63. guidanceState: state.guidanceState,
  64. modeState: state.modeState
  65. ? JSON.parse(JSON.stringify(state.modeState)) as Record<string, unknown>
  66. : null,
  67. }
  68. }
  69. export function cloneSessionRecoverySnapshot(snapshot: SessionRecoverySnapshot): SessionRecoverySnapshot {
  70. return {
  71. schemaVersion: 1,
  72. savedAt: snapshot.savedAt,
  73. launchEnvelope: JSON.parse(JSON.stringify(snapshot.launchEnvelope)) as GameLaunchEnvelope,
  74. configAppId: snapshot.configAppId,
  75. configVersion: snapshot.configVersion,
  76. runtime: {
  77. gameState: cloneGameSessionState(snapshot.runtime.gameState),
  78. telemetry: {
  79. distanceMeters: snapshot.runtime.telemetry.distanceMeters,
  80. currentSpeedKmh: snapshot.runtime.telemetry.currentSpeedKmh,
  81. averageSpeedKmh: snapshot.runtime.telemetry.averageSpeedKmh,
  82. heartRateBpm: snapshot.runtime.telemetry.heartRateBpm,
  83. caloriesKcal: snapshot.runtime.telemetry.caloriesKcal,
  84. lastGpsPoint: cloneLonLatPoint(snapshot.runtime.telemetry.lastGpsPoint),
  85. lastGpsAt: snapshot.runtime.telemetry.lastGpsAt,
  86. lastGpsAccuracyMeters: snapshot.runtime.telemetry.lastGpsAccuracyMeters,
  87. },
  88. viewport: {
  89. zoom: snapshot.runtime.viewport.zoom,
  90. centerTileX: snapshot.runtime.viewport.centerTileX,
  91. centerTileY: snapshot.runtime.viewport.centerTileY,
  92. rotationDeg: snapshot.runtime.viewport.rotationDeg,
  93. gpsLockEnabled: snapshot.runtime.viewport.gpsLockEnabled,
  94. hasGpsCenteredOnce: snapshot.runtime.viewport.hasGpsCenteredOnce,
  95. },
  96. currentGpsPoint: cloneLonLatPoint(snapshot.runtime.currentGpsPoint),
  97. currentGpsAccuracyMeters: snapshot.runtime.currentGpsAccuracyMeters,
  98. currentGpsInsideMap: snapshot.runtime.currentGpsInsideMap,
  99. bonusScore: snapshot.runtime.bonusScore,
  100. quizCorrectCount: snapshot.runtime.quizCorrectCount,
  101. quizWrongCount: snapshot.runtime.quizWrongCount,
  102. quizTimeoutCount: snapshot.runtime.quizTimeoutCount,
  103. },
  104. }
  105. }
  106. function normalizeSessionRecoverySnapshot(raw: unknown): SessionRecoverySnapshot | null {
  107. if (!raw || typeof raw !== 'object') {
  108. return null
  109. }
  110. const candidate = raw as SessionRecoverySnapshot
  111. if (candidate.schemaVersion !== 1 || !candidate.runtime || !candidate.runtime.gameState) {
  112. return null
  113. }
  114. return cloneSessionRecoverySnapshot(candidate)
  115. }
  116. export function loadSessionRecoverySnapshot(): SessionRecoverySnapshot | null {
  117. try {
  118. return normalizeSessionRecoverySnapshot(wx.getStorageSync(SESSION_RECOVERY_STORAGE_KEY))
  119. } catch {
  120. return null
  121. }
  122. }
  123. export function saveSessionRecoverySnapshot(snapshot: SessionRecoverySnapshot): void {
  124. try {
  125. wx.setStorageSync(SESSION_RECOVERY_STORAGE_KEY, cloneSessionRecoverySnapshot(snapshot))
  126. } catch {}
  127. }
  128. export function clearSessionRecoverySnapshot(): void {
  129. try {
  130. wx.removeStorageSync(SESSION_RECOVERY_STORAGE_KEY)
  131. } catch {}
  132. }