feat: 收敛玩法运行时配置并加入故障恢复

This commit is contained in:
2026-04-01 13:04:26 +08:00
parent 1635a11780
commit 3ef841ecc7
73 changed files with 8820 additions and 2122 deletions

View File

@@ -4,10 +4,10 @@ import {
getHeartRateToneLabel,
getHeartRateToneRangeText,
getSpeedToneRangeText,
mergeTelemetryConfig,
type HeartRateTone,
type TelemetryConfig,
} from './telemetryConfig'
import { mergeTelemetrySources, type PlayerTelemetryProfile } from './playerTelemetryProfile'
import { type GameSessionState } from '../core/gameSessionState'
import { type TelemetryEvent } from './telemetryEvent'
import { EMPTY_TELEMETRY_PRESENTATION, type TelemetryPresentation } from './telemetryPresentation'
@@ -17,6 +17,7 @@ import {
type HeadingConfidence,
type TelemetryState,
} from './telemetryState'
import { type RecoveryTelemetrySnapshot } from '../core/sessionRecovery'
const SPEED_SMOOTHING_ALPHA = 0.35
const DEVICE_HEADING_SMOOTHING_ALPHA = 0.28
const ACCELEROMETER_SMOOTHING_ALPHA = 0.2
@@ -109,6 +110,10 @@ function formatElapsedTimerText(totalMs: number): string {
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
function formatCountdownTimerText(remainingMs: number): string {
return formatElapsedTimerText(Math.max(0, remainingMs))
}
function formatDistanceText(distanceMeters: number): string {
if (distanceMeters >= 1000) {
return `${(distanceMeters / 1000).toFixed(distanceMeters >= 10000 ? 0 : 1)}km`
@@ -419,10 +424,18 @@ function shouldTrackCalories(state: TelemetryState): boolean {
export class TelemetryRuntime {
state: TelemetryState
config: TelemetryConfig
activityConfig: TelemetryConfig
playerProfile: PlayerTelemetryProfile | null
sessionCloseAfterMs: number
sessionCloseWarningMs: number
constructor() {
this.state = { ...EMPTY_TELEMETRY_STATE }
this.config = { ...DEFAULT_TELEMETRY_CONFIG }
this.activityConfig = { ...DEFAULT_TELEMETRY_CONFIG }
this.playerProfile = null
this.sessionCloseAfterMs = 2 * 60 * 60 * 1000
this.sessionCloseWarningMs = 10 * 60 * 1000
}
reset(): void {
@@ -440,10 +453,102 @@ export class TelemetryRuntime {
}
configure(config?: Partial<TelemetryConfig> | null): void {
this.config = mergeTelemetryConfig(config)
this.activityConfig = mergeTelemetrySources(config, null)
this.syncEffectiveConfig()
}
applyCompiledProfile(
config: TelemetryConfig,
playerProfile?: PlayerTelemetryProfile | null,
): void {
this.activityConfig = { ...config }
this.playerProfile = playerProfile ? { ...playerProfile } : null
this.config = { ...config }
}
setPlayerProfile(profile?: PlayerTelemetryProfile | null): void {
this.playerProfile = profile ? { ...profile } : null
this.syncEffectiveConfig()
}
clearPlayerProfile(): void {
this.playerProfile = null
this.syncEffectiveConfig()
}
exportRecoveryState(): RecoveryTelemetrySnapshot {
return {
distanceMeters: this.state.distanceMeters,
currentSpeedKmh: this.state.currentSpeedKmh,
averageSpeedKmh: this.state.averageSpeedKmh,
heartRateBpm: this.state.heartRateBpm,
caloriesKcal: this.state.caloriesKcal,
lastGpsPoint: this.state.lastGpsPoint
? {
lon: this.state.lastGpsPoint.lon,
lat: this.state.lastGpsPoint.lat,
}
: null,
lastGpsAt: this.state.lastGpsAt,
lastGpsAccuracyMeters: this.state.lastGpsAccuracyMeters,
}
}
restoreRecoveryState(
definition: GameDefinition,
gameState: GameSessionState,
snapshot: RecoveryTelemetrySnapshot,
hudTargetControlId?: string | null,
): void {
const targetControlId = hudTargetControlId || null
const targetControl = targetControlId
? definition.controls.find((control) => control.id === targetControlId) || null
: null
this.sessionCloseAfterMs = definition.sessionCloseAfterMs
this.sessionCloseWarningMs = definition.sessionCloseWarningMs
this.state = {
...EMPTY_TELEMETRY_STATE,
accelerometer: this.state.accelerometer,
accelerometerUpdatedAt: this.state.accelerometerUpdatedAt,
accelerometerSampleCount: this.state.accelerometerSampleCount,
gyroscope: this.state.gyroscope,
deviceMotion: this.state.deviceMotion,
deviceHeadingDeg: this.state.deviceHeadingDeg,
devicePose: this.state.devicePose,
headingConfidence: this.state.headingConfidence,
sessionStatus: gameState.status,
sessionStartedAt: gameState.startedAt,
sessionEndedAt: gameState.endedAt,
elapsedMs: gameState.startedAt === null
? 0
: Math.max(0, ((gameState.endedAt || Date.now()) - gameState.startedAt)),
distanceMeters: snapshot.distanceMeters,
currentSpeedKmh: snapshot.currentSpeedKmh,
averageSpeedKmh: snapshot.averageSpeedKmh,
distanceToTargetMeters: targetControl && snapshot.lastGpsPoint
? getApproxDistanceMeters(snapshot.lastGpsPoint, targetControl.point)
: null,
targetControlId: targetControl ? targetControl.id : null,
targetPoint: targetControl ? targetControl.point : null,
lastGpsPoint: snapshot.lastGpsPoint
? {
lon: snapshot.lastGpsPoint.lon,
lat: snapshot.lastGpsPoint.lat,
}
: null,
lastGpsAt: snapshot.lastGpsAt,
lastGpsAccuracyMeters: snapshot.lastGpsAccuracyMeters,
heartRateBpm: snapshot.heartRateBpm,
caloriesKcal: snapshot.caloriesKcal,
calorieTrackingAt: snapshot.lastGpsAt,
}
this.recomputeDerivedState()
}
loadDefinition(_definition: GameDefinition): void {
this.sessionCloseAfterMs = _definition.sessionCloseAfterMs
this.sessionCloseWarningMs = _definition.sessionCloseWarningMs
this.reset()
}
@@ -632,6 +737,15 @@ export class TelemetryRuntime {
this.syncCalorieAccumulation(now)
this.alignCalorieTracking(now)
this.recomputeDerivedState(now)
const elapsedTimerText = formatElapsedTimerText(this.state.elapsedMs)
const countdownActive = this.state.sessionStatus === 'running'
&& this.state.sessionEndedAt === null
&& this.state.sessionStartedAt !== null
&& this.sessionCloseAfterMs > 0
&& (this.sessionCloseAfterMs - this.state.elapsedMs) <= this.sessionCloseWarningMs
const countdownRemainingMs = countdownActive
? Math.max(0, this.sessionCloseAfterMs - this.state.elapsedMs)
: 0
const targetDistance = formatTargetDistance(this.state.distanceToTargetMeters)
const hasHeartRate = hasHeartRateSignal(this.state)
const heartRateTone = hasHeartRate
@@ -643,7 +757,9 @@ export class TelemetryRuntime {
return {
...EMPTY_TELEMETRY_PRESENTATION,
timerText: formatElapsedTimerText(this.state.elapsedMs),
timerText: countdownActive ? formatCountdownTimerText(countdownRemainingMs) : elapsedTimerText,
elapsedTimerText,
timerMode: countdownActive ? 'countdown' : 'elapsed',
mileageText: formatDistanceText(this.state.distanceMeters),
distanceToTargetValueText: targetDistance.valueText,
distanceToTargetUnitText: targetDistance.unitText,
@@ -716,4 +832,8 @@ export class TelemetryRuntime {
}
}
}
private syncEffectiveConfig(): void {
this.config = mergeTelemetrySources(this.activityConfig, this.playerProfile)
}
}