Refine telemetry-driven HUD and fitness feedback

This commit is contained in:
2026-03-24 11:24:50 +08:00
parent 2c03d1a702
commit a117a25824
12 changed files with 2071 additions and 211 deletions

View File

@@ -1,6 +1,7 @@
import { lonLatToWorldTile, webMercatorToLonLat, type LonLatPoint } from './projection'
import { parseOrienteeringCourseKml, type OrienteeringCourseData } from './orienteeringCourse'
import { mergeGameAudioConfig, type AudioCueKey, type GameAudioConfig, type GameAudioConfigOverrides, type PartialAudioCueConfig } from '../game/audio/audioConfig'
import { mergeTelemetryConfig, type TelemetryConfig } from '../game/telemetry/telemetryConfig'
import {
mergeGameHapticsConfig,
mergeGameUiEffectsConfig,
@@ -45,6 +46,7 @@ export interface RemoteMapConfig {
punchPolicy: 'enter' | 'enter-confirm'
punchRadiusMeters: number
autoFinishOnLastControl: boolean
telemetryConfig: TelemetryConfig
audioConfig: GameAudioConfig
hapticsConfig: GameHapticsConfig
uiEffectsConfig: GameUiEffectsConfig
@@ -59,6 +61,7 @@ interface ParsedGameConfig {
punchPolicy: 'enter' | 'enter-confirm'
punchRadiusMeters: number
autoFinishOnLastControl: boolean
telemetryConfig: TelemetryConfig
audioConfig: GameAudioConfig
hapticsConfig: GameHapticsConfig
uiEffectsConfig: GameUiEffectsConfig
@@ -206,6 +209,40 @@ function parsePunchPolicy(rawValue: unknown): 'enter' | 'enter-confirm' {
return rawValue === 'enter' ? 'enter' : 'enter-confirm'
}
function parseTelemetryConfig(rawValue: unknown): TelemetryConfig {
const normalized = normalizeObjectRecord(rawValue)
if (!Object.keys(normalized).length) {
return mergeTelemetryConfig()
}
const rawHeartRate = getFirstDefined(normalized, ['heartrate', 'heart_rate'])
const normalizedHeartRate = normalizeObjectRecord(rawHeartRate)
const ageRaw = getFirstDefined(normalizedHeartRate, ['age', 'userage', 'heartrateage', 'hrage']) !== undefined
? getFirstDefined(normalizedHeartRate, ['age', 'userage', 'heartrateage', 'hrage'])
: getFirstDefined(normalized, ['age', 'userage', 'heartrateage', 'hrage'])
const restingHeartRateRaw = getFirstDefined(normalizedHeartRate, ['restingheartratebpm', 'restingheartrate', 'restinghr', 'resting'])
!== undefined
? getFirstDefined(normalizedHeartRate, ['restingheartratebpm', 'restingheartrate', 'restinghr', 'resting'])
: getFirstDefined(normalized, ['restingheartratebpm', 'restingheartrate', 'restinghr', 'resting'])
const userWeightRaw = getFirstDefined(normalizedHeartRate, ['userweightkg', 'weightkg', 'weight'])
!== undefined
? getFirstDefined(normalizedHeartRate, ['userweightkg', 'weightkg', 'weight'])
: getFirstDefined(normalized, ['userweightkg', 'weightkg', 'weight'])
const telemetryOverrides: Partial<TelemetryConfig> = {}
if (ageRaw !== undefined) {
telemetryOverrides.heartRateAge = Number(ageRaw)
}
if (restingHeartRateRaw !== undefined) {
telemetryOverrides.restingHeartRateBpm = Number(restingHeartRateRaw)
}
if (userWeightRaw !== undefined) {
telemetryOverrides.userWeightKg = Number(userWeightRaw)
}
return mergeTelemetryConfig(telemetryOverrides)
}
function normalizeObjectRecord(rawValue: unknown): Record<string, unknown> {
if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
@@ -622,6 +659,7 @@ function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGam
}
}
const rawAudio = rawGame && rawGame.audio !== undefined ? rawGame.audio : parsed.audio
const rawTelemetry = rawGame && rawGame.telemetry !== undefined ? rawGame.telemetry : parsed.telemetry
const rawHaptics = rawGame && rawGame.haptics !== undefined ? rawGame.haptics : parsed.haptics
const rawUiEffects = rawGame && rawGame.uiEffects !== undefined
? rawGame.uiEffects
@@ -668,6 +706,7 @@ function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGam
normalizedGame.autofinishonlastcontrol !== undefined ? normalizedGame.autofinishonlastcontrol : normalized.autofinishonlastcontrol,
true,
),
telemetryConfig: parseTelemetryConfig(rawTelemetry),
audioConfig: parseAudioConfig(rawAudio, gameConfigUrl),
hapticsConfig: parseHapticsConfig(rawHaptics),
uiEffectsConfig: parseUiEffectsConfig(rawUiEffects),
@@ -716,6 +755,18 @@ function parseGameConfigFromYaml(text: string, gameConfigUrl: string): ParsedGam
5,
),
autoFinishOnLastControl: parseBoolean(config.autofinishonlastcontrol, true),
telemetryConfig: parseTelemetryConfig({
heartRate: {
age: config.heartrateage !== undefined ? config.heartrateage : config.telemetryheartrateage,
restingHeartRateBpm: config.restingheartratebpm !== undefined
? config.restingheartratebpm
: config.restingheartrate !== undefined
? config.restingheartrate
: config.telemetryrestingheartratebpm !== undefined
? config.telemetryrestingheartratebpm
: config.telemetryrestingheartrate,
},
}),
audioConfig: parseAudioConfig({
enabled: config.audioenabled,
masterVolume: config.audiomastervolume,
@@ -979,6 +1030,7 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
punchPolicy: gameConfig.punchPolicy,
punchRadiusMeters: gameConfig.punchRadiusMeters,
autoFinishOnLastControl: gameConfig.autoFinishOnLastControl,
telemetryConfig: gameConfig.telemetryConfig,
audioConfig: gameConfig.audioConfig,
hapticsConfig: gameConfig.hapticsConfig,
uiEffectsConfig: gameConfig.uiEffectsConfig,