200 lines
6.1 KiB
TypeScript
200 lines
6.1 KiB
TypeScript
export type AudioCueKey =
|
|
| 'session_started'
|
|
| 'control_completed:start'
|
|
| 'control_completed:control'
|
|
| 'control_completed:finish'
|
|
| 'punch_feedback:warning'
|
|
| 'guidance:searching'
|
|
| 'guidance:distant'
|
|
| 'guidance:approaching'
|
|
| 'guidance:ready'
|
|
|
|
export interface AudioCueConfig {
|
|
src: string
|
|
volume: number
|
|
loop: boolean
|
|
loopGapMs: number
|
|
backgroundMode: 'disabled' | 'guidance'
|
|
}
|
|
|
|
export interface GameAudioConfig {
|
|
enabled: boolean
|
|
masterVolume: number
|
|
obeyMuteSwitch: boolean
|
|
backgroundAudioEnabled: boolean
|
|
distantDistanceMeters: number
|
|
approachDistanceMeters: number
|
|
readyDistanceMeters: number
|
|
cues: Record<AudioCueKey, AudioCueConfig>
|
|
}
|
|
|
|
export interface PartialAudioCueConfig {
|
|
src?: string
|
|
volume?: number
|
|
loop?: boolean
|
|
loopGapMs?: number
|
|
backgroundMode?: 'disabled' | 'guidance'
|
|
}
|
|
|
|
export interface GameAudioConfigOverrides {
|
|
enabled?: boolean
|
|
masterVolume?: number
|
|
obeyMuteSwitch?: boolean
|
|
backgroundAudioEnabled?: boolean
|
|
distantDistanceMeters?: number
|
|
approachDistanceMeters?: number
|
|
readyDistanceMeters?: number
|
|
cues?: Partial<Record<AudioCueKey, PartialAudioCueConfig>>
|
|
}
|
|
|
|
export const DEFAULT_GAME_AUDIO_CONFIG: GameAudioConfig = {
|
|
enabled: true,
|
|
masterVolume: 1,
|
|
obeyMuteSwitch: true,
|
|
backgroundAudioEnabled: true,
|
|
distantDistanceMeters: 80,
|
|
approachDistanceMeters: 20,
|
|
readyDistanceMeters: 5,
|
|
cues: {
|
|
session_started: {
|
|
src: '/assets/sounds/session-start.wav',
|
|
volume: 0.78,
|
|
loop: false,
|
|
loopGapMs: 0,
|
|
backgroundMode: 'disabled',
|
|
},
|
|
'control_completed:start': {
|
|
src: '/assets/sounds/start-complete.wav',
|
|
volume: 0.84,
|
|
loop: false,
|
|
loopGapMs: 0,
|
|
backgroundMode: 'disabled',
|
|
},
|
|
'control_completed:control': {
|
|
src: '/assets/sounds/control-complete.wav',
|
|
volume: 0.8,
|
|
loop: false,
|
|
loopGapMs: 0,
|
|
backgroundMode: 'disabled',
|
|
},
|
|
'control_completed:finish': {
|
|
src: '/assets/sounds/finish-complete.wav',
|
|
volume: 0.92,
|
|
loop: false,
|
|
loopGapMs: 0,
|
|
backgroundMode: 'disabled',
|
|
},
|
|
'punch_feedback:warning': {
|
|
src: '/assets/sounds/warning.wav',
|
|
volume: 0.72,
|
|
loop: false,
|
|
loopGapMs: 0,
|
|
backgroundMode: 'disabled',
|
|
},
|
|
'guidance:searching': {
|
|
src: '/assets/sounds/guidance-searching.wav',
|
|
volume: 0.48,
|
|
loop: true,
|
|
loopGapMs: 1800,
|
|
backgroundMode: 'guidance',
|
|
},
|
|
'guidance:distant': {
|
|
src: '/assets/sounds/guidance-searching.wav',
|
|
volume: 0.34,
|
|
loop: true,
|
|
loopGapMs: 4800,
|
|
backgroundMode: 'guidance',
|
|
},
|
|
'guidance:approaching': {
|
|
src: '/assets/sounds/guidance-approaching.wav',
|
|
volume: 0.58,
|
|
loop: true,
|
|
loopGapMs: 950,
|
|
backgroundMode: 'guidance',
|
|
},
|
|
'guidance:ready': {
|
|
src: '/assets/sounds/guidance-ready.wav',
|
|
volume: 0.68,
|
|
loop: true,
|
|
loopGapMs: 650,
|
|
backgroundMode: 'guidance',
|
|
},
|
|
},
|
|
}
|
|
|
|
function clampVolume(value: number, fallback: number): number {
|
|
return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : fallback
|
|
}
|
|
|
|
function clampDistance(value: number, fallback: number): number {
|
|
return Number.isFinite(value) && value > 0 ? value : fallback
|
|
}
|
|
|
|
|
|
function clampGap(value: number, fallback: number): number {
|
|
return Number.isFinite(value) && value >= 0 ? value : fallback
|
|
}
|
|
|
|
export function mergeGameAudioConfig(overrides?: GameAudioConfigOverrides | null): GameAudioConfig {
|
|
const cues: GameAudioConfig['cues'] = {
|
|
session_started: { ...DEFAULT_GAME_AUDIO_CONFIG.cues.session_started },
|
|
'control_completed:start': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['control_completed:start'] },
|
|
'control_completed:control': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['control_completed:control'] },
|
|
'control_completed:finish': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['control_completed:finish'] },
|
|
'punch_feedback:warning': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['punch_feedback:warning'] },
|
|
'guidance:searching': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['guidance:searching'] },
|
|
'guidance:distant': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['guidance:distant'] },
|
|
'guidance:approaching': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['guidance:approaching'] },
|
|
'guidance:ready': { ...DEFAULT_GAME_AUDIO_CONFIG.cues['guidance:ready'] },
|
|
}
|
|
|
|
if (overrides && overrides.cues) {
|
|
const keys = Object.keys(overrides.cues) as AudioCueKey[]
|
|
for (const key of keys) {
|
|
const cue = overrides.cues[key]
|
|
if (!cue) {
|
|
continue
|
|
}
|
|
|
|
if (typeof cue.src === 'string' && cue.src) {
|
|
cues[key].src = cue.src
|
|
}
|
|
|
|
if (cue.volume !== undefined) {
|
|
cues[key].volume = clampVolume(Number(cue.volume), cues[key].volume)
|
|
}
|
|
|
|
if (cue.loop !== undefined) {
|
|
cues[key].loop = !!cue.loop
|
|
}
|
|
|
|
if (cue.loopGapMs !== undefined) {
|
|
cues[key].loopGapMs = clampGap(Number(cue.loopGapMs), cues[key].loopGapMs)
|
|
}
|
|
|
|
if (cue.backgroundMode === 'disabled' || cue.backgroundMode === 'guidance') {
|
|
cues[key].backgroundMode = cue.backgroundMode
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
enabled: overrides && overrides.enabled !== undefined ? !!overrides.enabled : DEFAULT_GAME_AUDIO_CONFIG.enabled,
|
|
masterVolume: clampVolume(Number(overrides && overrides.masterVolume), DEFAULT_GAME_AUDIO_CONFIG.masterVolume),
|
|
obeyMuteSwitch: overrides && overrides.obeyMuteSwitch !== undefined ? !!overrides.obeyMuteSwitch : DEFAULT_GAME_AUDIO_CONFIG.obeyMuteSwitch,
|
|
backgroundAudioEnabled: overrides && overrides.backgroundAudioEnabled !== undefined
|
|
? !!overrides.backgroundAudioEnabled
|
|
: true,
|
|
distantDistanceMeters: clampDistance(
|
|
Number(overrides && overrides.distantDistanceMeters),
|
|
DEFAULT_GAME_AUDIO_CONFIG.distantDistanceMeters,
|
|
),
|
|
approachDistanceMeters: clampDistance(Number(overrides && overrides.approachDistanceMeters), DEFAULT_GAME_AUDIO_CONFIG.approachDistanceMeters),
|
|
readyDistanceMeters: clampDistance(
|
|
Number(overrides && overrides.readyDistanceMeters),
|
|
DEFAULT_GAME_AUDIO_CONFIG.readyDistanceMeters,
|
|
),
|
|
cues,
|
|
}
|
|
}
|