Refine telemetry-driven HUD and fitness feedback
This commit is contained in:
141
miniprogram/game/telemetry/telemetryConfig.ts
Normal file
141
miniprogram/game/telemetry/telemetryConfig.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
export interface TelemetryConfig {
|
||||
heartRateAge: number
|
||||
restingHeartRateBpm: number
|
||||
userWeightKg: number
|
||||
}
|
||||
|
||||
export type HeartRateTone = 'blue' | 'purple' | 'green' | 'yellow' | 'orange' | 'red'
|
||||
|
||||
type HeartRateToneMeta = {
|
||||
label: string
|
||||
heartRateRangeText: string
|
||||
speedRangeText: string
|
||||
}
|
||||
|
||||
const HEART_RATE_TONE_META: Record<HeartRateTone, HeartRateToneMeta> = {
|
||||
blue: {
|
||||
label: '激活放松',
|
||||
heartRateRangeText: '<=39%',
|
||||
speedRangeText: '<3.2 km/h',
|
||||
},
|
||||
purple: {
|
||||
label: '动态热身',
|
||||
heartRateRangeText: '40~54%',
|
||||
speedRangeText: '3.2~4.0 km/h',
|
||||
},
|
||||
green: {
|
||||
label: '脂肪燃烧',
|
||||
heartRateRangeText: '55~69%',
|
||||
speedRangeText: '4.1~5.5 km/h',
|
||||
},
|
||||
yellow: {
|
||||
label: '糖分消耗',
|
||||
heartRateRangeText: '70~79%',
|
||||
speedRangeText: '5.6~7.1 km/h',
|
||||
},
|
||||
orange: {
|
||||
label: '心肺训练',
|
||||
heartRateRangeText: '80~89%',
|
||||
speedRangeText: '7.2~8.8 km/h',
|
||||
},
|
||||
red: {
|
||||
label: '峰值锻炼',
|
||||
heartRateRangeText: '>=90%',
|
||||
speedRangeText: '>=8.9 km/h',
|
||||
},
|
||||
}
|
||||
|
||||
export function clampTelemetryAge(age: number): number {
|
||||
if (!Number.isFinite(age)) {
|
||||
return 30
|
||||
}
|
||||
|
||||
return Math.max(10, Math.min(85, Math.round(age)))
|
||||
}
|
||||
|
||||
export function estimateRestingHeartRateBpm(age: number): number {
|
||||
const safeAge = clampTelemetryAge(age)
|
||||
const estimated = 68 + (safeAge - 30) * 0.12
|
||||
return Math.max(56, Math.min(76, Math.round(estimated)))
|
||||
}
|
||||
|
||||
export function normalizeRestingHeartRateBpm(restingHeartRateBpm: number, age: number): number {
|
||||
if (!Number.isFinite(restingHeartRateBpm) || restingHeartRateBpm <= 0) {
|
||||
return estimateRestingHeartRateBpm(age)
|
||||
}
|
||||
|
||||
return Math.max(40, Math.min(95, Math.round(restingHeartRateBpm)))
|
||||
}
|
||||
|
||||
export function normalizeUserWeightKg(userWeightKg: number): number {
|
||||
if (!Number.isFinite(userWeightKg) || userWeightKg <= 0) {
|
||||
return 65
|
||||
}
|
||||
|
||||
return Math.max(35, Math.min(180, Math.round(userWeightKg)))
|
||||
}
|
||||
|
||||
export const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig = {
|
||||
heartRateAge: 30,
|
||||
restingHeartRateBpm: estimateRestingHeartRateBpm(30),
|
||||
userWeightKg: 65,
|
||||
}
|
||||
|
||||
export function mergeTelemetryConfig(overrides?: Partial<TelemetryConfig> | null): TelemetryConfig {
|
||||
const heartRateAge = overrides && overrides.heartRateAge !== undefined
|
||||
? clampTelemetryAge(overrides.heartRateAge)
|
||||
: DEFAULT_TELEMETRY_CONFIG.heartRateAge
|
||||
|
||||
const restingHeartRateBpm = overrides && overrides.restingHeartRateBpm !== undefined
|
||||
? normalizeRestingHeartRateBpm(overrides.restingHeartRateBpm, heartRateAge)
|
||||
: estimateRestingHeartRateBpm(heartRateAge)
|
||||
const userWeightKg = overrides && overrides.userWeightKg !== undefined
|
||||
? normalizeUserWeightKg(overrides.userWeightKg)
|
||||
: DEFAULT_TELEMETRY_CONFIG.userWeightKg
|
||||
|
||||
return {
|
||||
heartRateAge,
|
||||
restingHeartRateBpm,
|
||||
userWeightKg,
|
||||
}
|
||||
}
|
||||
|
||||
export function getHeartRateToneSampleBpm(tone: HeartRateTone, config: TelemetryConfig): number {
|
||||
const maxHeartRate = Math.max(120, 220 - config.heartRateAge)
|
||||
const restingHeartRate = Math.min(maxHeartRate - 15, config.restingHeartRateBpm)
|
||||
const reserve = Math.max(20, maxHeartRate - restingHeartRate)
|
||||
|
||||
if (tone === 'blue') {
|
||||
return Math.round(restingHeartRate + reserve * 0.3)
|
||||
}
|
||||
|
||||
if (tone === 'purple') {
|
||||
return Math.round(restingHeartRate + reserve * 0.47)
|
||||
}
|
||||
|
||||
if (tone === 'green') {
|
||||
return Math.round(restingHeartRate + reserve * 0.62)
|
||||
}
|
||||
|
||||
if (tone === 'yellow') {
|
||||
return Math.round(restingHeartRate + reserve * 0.745)
|
||||
}
|
||||
|
||||
if (tone === 'orange') {
|
||||
return Math.round(restingHeartRate + reserve * 0.845)
|
||||
}
|
||||
|
||||
return Math.round(restingHeartRate + reserve * 0.93)
|
||||
}
|
||||
|
||||
export function getHeartRateToneLabel(tone: HeartRateTone): string {
|
||||
return HEART_RATE_TONE_META[tone].label
|
||||
}
|
||||
|
||||
export function getHeartRateToneRangeText(tone: HeartRateTone): string {
|
||||
return HEART_RATE_TONE_META[tone].heartRateRangeText
|
||||
}
|
||||
|
||||
export function getSpeedToneRangeText(tone: HeartRateTone): string {
|
||||
return HEART_RATE_TONE_META[tone].speedRangeText
|
||||
}
|
||||
Reference in New Issue
Block a user