|
|
@@ -1,5 +1,17 @@
|
|
|
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 {
|
|
|
+ mergeGameHapticsConfig,
|
|
|
+ mergeGameUiEffectsConfig,
|
|
|
+ type FeedbackCueKey,
|
|
|
+ type GameHapticsConfig,
|
|
|
+ type GameHapticsConfigOverrides,
|
|
|
+ type GameUiEffectsConfig,
|
|
|
+ type GameUiEffectsConfigOverrides,
|
|
|
+ type PartialHapticCueConfig,
|
|
|
+ type PartialUiCueConfig,
|
|
|
+} from '../game/feedback/feedbackConfig'
|
|
|
|
|
|
export interface TileZoomBounds {
|
|
|
minX: number
|
|
|
@@ -33,6 +45,9 @@ export interface RemoteMapConfig {
|
|
|
punchPolicy: 'enter' | 'enter-confirm'
|
|
|
punchRadiusMeters: number
|
|
|
autoFinishOnLastControl: boolean
|
|
|
+ audioConfig: GameAudioConfig
|
|
|
+ hapticsConfig: GameHapticsConfig
|
|
|
+ uiEffectsConfig: GameUiEffectsConfig
|
|
|
}
|
|
|
|
|
|
interface ParsedGameConfig {
|
|
|
@@ -44,6 +59,9 @@ interface ParsedGameConfig {
|
|
|
punchPolicy: 'enter' | 'enter-confirm'
|
|
|
punchRadiusMeters: number
|
|
|
autoFinishOnLastControl: boolean
|
|
|
+ audioConfig: GameAudioConfig
|
|
|
+ hapticsConfig: GameHapticsConfig
|
|
|
+ uiEffectsConfig: GameUiEffectsConfig
|
|
|
declinationDeg: number
|
|
|
}
|
|
|
|
|
|
@@ -188,6 +206,134 @@ function parsePunchPolicy(rawValue: unknown): 'enter' | 'enter-confirm' {
|
|
|
return rawValue === 'enter' ? 'enter' : 'enter-confirm'
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+function normalizeObjectRecord(rawValue: unknown): Record<string, unknown> {
|
|
|
+ if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalized: Record<string, unknown> = {}
|
|
|
+ const keys = Object.keys(rawValue as Record<string, unknown>)
|
|
|
+ for (const key of keys) {
|
|
|
+ normalized[key.toLowerCase()] = (rawValue as Record<string, unknown>)[key]
|
|
|
+ }
|
|
|
+ return normalized
|
|
|
+}
|
|
|
+
|
|
|
+function getFirstDefined(record: Record<string, unknown>, keys: string[]): unknown {
|
|
|
+ for (const key of keys) {
|
|
|
+ if (record[key] !== undefined) {
|
|
|
+ return record[key]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function resolveAudioSrc(baseUrl: string, rawValue: unknown): string | undefined {
|
|
|
+ if (typeof rawValue !== 'string') {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+
|
|
|
+ const trimmed = rawValue.trim()
|
|
|
+ if (!trimmed) {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+
|
|
|
+ if (/^https?:\/\//i.test(trimmed)) {
|
|
|
+ return trimmed
|
|
|
+ }
|
|
|
+
|
|
|
+ if (trimmed.startsWith('/assets/')) {
|
|
|
+ return trimmed
|
|
|
+ }
|
|
|
+
|
|
|
+ if (trimmed.startsWith('assets/')) {
|
|
|
+ return `/${trimmed}`
|
|
|
+ }
|
|
|
+
|
|
|
+ return resolveUrl(baseUrl, trimmed)
|
|
|
+}
|
|
|
+
|
|
|
+function buildAudioCueOverride(rawValue: unknown, baseUrl: string): PartialAudioCueConfig | null {
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const src = resolveAudioSrc(baseUrl, rawValue)
|
|
|
+ return src ? { src } : null
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalized = normalizeObjectRecord(rawValue)
|
|
|
+ if (!Object.keys(normalized).length) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const src = resolveAudioSrc(baseUrl, getFirstDefined(normalized, ['src', 'url', 'path']))
|
|
|
+ const volumeRaw = getFirstDefined(normalized, ['volume'])
|
|
|
+ const loopRaw = getFirstDefined(normalized, ['loop'])
|
|
|
+ const loopGapRaw = getFirstDefined(normalized, ['loopgapms', 'loopgap'])
|
|
|
+ const cue: PartialAudioCueConfig = {}
|
|
|
+
|
|
|
+ if (src) {
|
|
|
+ cue.src = src
|
|
|
+ }
|
|
|
+
|
|
|
+ if (volumeRaw !== undefined) {
|
|
|
+ cue.volume = parsePositiveNumber(volumeRaw, 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (loopRaw !== undefined) {
|
|
|
+ cue.loop = parseBoolean(loopRaw, false)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (loopGapRaw !== undefined) {
|
|
|
+ cue.loopGapMs = parsePositiveNumber(loopGapRaw, 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ return cue.src || cue.volume !== undefined || cue.loop !== undefined || cue.loopGapMs !== undefined ? cue : null
|
|
|
+}
|
|
|
+
|
|
|
+function parseAudioConfig(rawValue: unknown, baseUrl: string): GameAudioConfig {
|
|
|
+ const normalized = normalizeObjectRecord(rawValue)
|
|
|
+ if (!Object.keys(normalized).length) {
|
|
|
+ return mergeGameAudioConfig()
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalizedCues = normalizeObjectRecord(getFirstDefined(normalized, ['cues', 'events']))
|
|
|
+ const cueMap: Array<{ key: AudioCueKey; aliases: string[] }> = [
|
|
|
+ { key: 'session_started', aliases: ['session_started', 'sessionstarted', 'session-started', 'start', 'session_start'] },
|
|
|
+ { key: 'control_completed:start', aliases: ['control_completed:start', 'controlcompleted:start', 'start_completed', 'startcomplete', 'start-complete'] },
|
|
|
+ { key: 'control_completed:control', aliases: ['control_completed:control', 'controlcompleted:control', 'control_completed', 'controlcompleted', 'control_complete', 'controlcomplete'] },
|
|
|
+ { key: 'control_completed:finish', aliases: ['control_completed:finish', 'controlcompleted:finish', 'finish_completed', 'finishcomplete', 'finish-complete'] },
|
|
|
+ { key: 'punch_feedback:warning', aliases: ['punch_feedback:warning', 'punchfeedback:warning', 'warning', 'punch_warning', 'punchwarning'] },
|
|
|
+ { key: 'guidance:searching', aliases: ['guidance:searching', 'guidance_searching', 'searching', 'search', 'normal_search'] },
|
|
|
+ { key: 'guidance:approaching', aliases: ['guidance:approaching', 'guidance_approaching', 'approaching', 'approach', 'near'] },
|
|
|
+ { key: 'guidance:ready', aliases: ['guidance:ready', 'guidance_ready', 'ready', 'punch_ready', 'can_punch'] },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const cues: GameAudioConfigOverrides['cues'] = {}
|
|
|
+ for (const cueDef of cueMap) {
|
|
|
+ const cueRaw = getFirstDefined(normalizedCues, cueDef.aliases)
|
|
|
+ const cue = buildAudioCueOverride(cueRaw, baseUrl)
|
|
|
+ if (cue) {
|
|
|
+ cues[cueDef.key] = cue
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return mergeGameAudioConfig({
|
|
|
+ enabled: normalized.enabled !== undefined ? parseBoolean(normalized.enabled, true) : undefined,
|
|
|
+ masterVolume: normalized.mastervolume !== undefined
|
|
|
+ ? parsePositiveNumber(normalized.mastervolume, 1)
|
|
|
+ : normalized.volume !== undefined
|
|
|
+ ? parsePositiveNumber(normalized.volume, 1)
|
|
|
+ : undefined,
|
|
|
+ obeyMuteSwitch: normalized.obeymuteswitch !== undefined ? parseBoolean(normalized.obeymuteswitch, true) : undefined,
|
|
|
+ approachDistanceMeters: normalized.approachdistancemeters !== undefined
|
|
|
+ ? parsePositiveNumber(normalized.approachdistancemeters, 20)
|
|
|
+ : normalized.approachdistance !== undefined
|
|
|
+ ? parsePositiveNumber(normalized.approachdistance, 20)
|
|
|
+ : undefined,
|
|
|
+ cues,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
function parseLooseJsonObject(text: string): Record<string, unknown> {
|
|
|
const parsed: Record<string, unknown> = {}
|
|
|
const pairPattern = /"([^"]+)"\s*:\s*("([^"]*)"|-?\d+(?:\.\d+)?|true|false|null)/g
|
|
|
@@ -214,7 +360,244 @@ function parseLooseJsonObject(text: string): Record<string, unknown> {
|
|
|
return parsed
|
|
|
}
|
|
|
|
|
|
-function parseGameConfigFromJson(text: string): ParsedGameConfig {
|
|
|
+
|
|
|
+function parseHapticPattern(rawValue: unknown): 'short' | 'long' | undefined {
|
|
|
+ if (rawValue === 'short' || rawValue === 'long') {
|
|
|
+ return rawValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const normalized = rawValue.trim().toLowerCase()
|
|
|
+ if (normalized === 'short' || normalized === 'long') {
|
|
|
+ return normalized
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function parsePunchFeedbackMotion(rawValue: unknown): 'none' | 'pop' | 'success' | 'warning' | undefined {
|
|
|
+ if (rawValue === 'none' || rawValue === 'pop' || rawValue === 'success' || rawValue === 'warning') {
|
|
|
+ return rawValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const normalized = rawValue.trim().toLowerCase()
|
|
|
+ if (normalized === 'none' || normalized === 'pop' || normalized === 'success' || normalized === 'warning') {
|
|
|
+ return normalized
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function parseContentCardMotion(rawValue: unknown): 'none' | 'pop' | 'finish' | undefined {
|
|
|
+ if (rawValue === 'none' || rawValue === 'pop' || rawValue === 'finish') {
|
|
|
+ return rawValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const normalized = rawValue.trim().toLowerCase()
|
|
|
+ if (normalized === 'none' || normalized === 'pop' || normalized === 'finish') {
|
|
|
+ return normalized
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function parsePunchButtonMotion(rawValue: unknown): 'none' | 'ready' | 'warning' | undefined {
|
|
|
+ if (rawValue === 'none' || rawValue === 'ready' || rawValue === 'warning') {
|
|
|
+ return rawValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const normalized = rawValue.trim().toLowerCase()
|
|
|
+ if (normalized === 'none' || normalized === 'ready' || normalized === 'warning') {
|
|
|
+ return normalized
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function parseMapPulseMotion(rawValue: unknown): 'none' | 'ready' | 'control' | 'finish' | undefined {
|
|
|
+ if (rawValue === 'none' || rawValue === 'ready' || rawValue === 'control' || rawValue === 'finish') {
|
|
|
+ return rawValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const normalized = rawValue.trim().toLowerCase()
|
|
|
+ if (normalized === 'none' || normalized === 'ready' || normalized === 'control' || normalized === 'finish') {
|
|
|
+ return normalized
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function parseStageMotion(rawValue: unknown): 'none' | 'finish' | undefined {
|
|
|
+ if (rawValue === 'none' || rawValue === 'finish') {
|
|
|
+ return rawValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof rawValue === 'string') {
|
|
|
+ const normalized = rawValue.trim().toLowerCase()
|
|
|
+ if (normalized === 'none' || normalized === 'finish') {
|
|
|
+ return normalized
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function buildHapticsCueOverride(rawValue: unknown): PartialHapticCueConfig | null {
|
|
|
+ if (typeof rawValue === 'boolean') {
|
|
|
+ return { enabled: rawValue }
|
|
|
+ }
|
|
|
+
|
|
|
+ const pattern = parseHapticPattern(rawValue)
|
|
|
+ if (pattern) {
|
|
|
+ return { enabled: true, pattern }
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalized = normalizeObjectRecord(rawValue)
|
|
|
+ if (!Object.keys(normalized).length) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const cue: PartialHapticCueConfig = {}
|
|
|
+ if (normalized.enabled !== undefined) {
|
|
|
+ cue.enabled = parseBoolean(normalized.enabled, true)
|
|
|
+ }
|
|
|
+
|
|
|
+ const parsedPattern = parseHapticPattern(getFirstDefined(normalized, ['pattern', 'type']))
|
|
|
+ if (parsedPattern) {
|
|
|
+ cue.pattern = parsedPattern
|
|
|
+ }
|
|
|
+
|
|
|
+ return cue.enabled !== undefined || cue.pattern !== undefined ? cue : null
|
|
|
+}
|
|
|
+
|
|
|
+function buildUiCueOverride(rawValue: unknown): PartialUiCueConfig | null {
|
|
|
+ const normalized = normalizeObjectRecord(rawValue)
|
|
|
+ if (!Object.keys(normalized).length) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const cue: PartialUiCueConfig = {}
|
|
|
+ if (normalized.enabled !== undefined) {
|
|
|
+ cue.enabled = parseBoolean(normalized.enabled, true)
|
|
|
+ }
|
|
|
+
|
|
|
+ const punchFeedbackMotion = parsePunchFeedbackMotion(getFirstDefined(normalized, ['punchfeedbackmotion', 'feedbackmotion', 'toastmotion']))
|
|
|
+ if (punchFeedbackMotion) {
|
|
|
+ cue.punchFeedbackMotion = punchFeedbackMotion
|
|
|
+ }
|
|
|
+
|
|
|
+ const contentCardMotion = parseContentCardMotion(getFirstDefined(normalized, ['contentcardmotion', 'cardmotion']))
|
|
|
+ if (contentCardMotion) {
|
|
|
+ cue.contentCardMotion = contentCardMotion
|
|
|
+ }
|
|
|
+
|
|
|
+ const punchButtonMotion = parsePunchButtonMotion(getFirstDefined(normalized, ['punchbuttonmotion', 'buttonmotion']))
|
|
|
+ if (punchButtonMotion) {
|
|
|
+ cue.punchButtonMotion = punchButtonMotion
|
|
|
+ }
|
|
|
+
|
|
|
+ const mapPulseMotion = parseMapPulseMotion(getFirstDefined(normalized, ['mappulsemotion', 'mapmotion']))
|
|
|
+ if (mapPulseMotion) {
|
|
|
+ cue.mapPulseMotion = mapPulseMotion
|
|
|
+ }
|
|
|
+
|
|
|
+ const stageMotion = parseStageMotion(getFirstDefined(normalized, ['stagemotion', 'screenmotion']))
|
|
|
+ if (stageMotion) {
|
|
|
+ cue.stageMotion = stageMotion
|
|
|
+ }
|
|
|
+
|
|
|
+ const durationRaw = getFirstDefined(normalized, ['durationms', 'duration'])
|
|
|
+ if (durationRaw !== undefined) {
|
|
|
+ cue.durationMs = parsePositiveNumber(durationRaw, 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ return cue.enabled !== undefined ||
|
|
|
+ cue.punchFeedbackMotion !== undefined ||
|
|
|
+ cue.contentCardMotion !== undefined ||
|
|
|
+ cue.punchButtonMotion !== undefined ||
|
|
|
+ cue.mapPulseMotion !== undefined ||
|
|
|
+ cue.stageMotion !== undefined ||
|
|
|
+ cue.durationMs !== undefined
|
|
|
+ ? cue
|
|
|
+ : null
|
|
|
+}
|
|
|
+
|
|
|
+function parseHapticsConfig(rawValue: unknown): GameHapticsConfig {
|
|
|
+ const normalized = normalizeObjectRecord(rawValue)
|
|
|
+ if (!Object.keys(normalized).length) {
|
|
|
+ return mergeGameHapticsConfig()
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalizedCues = normalizeObjectRecord(getFirstDefined(normalized, ['cues', 'events']))
|
|
|
+ const cueMap: Array<{ key: FeedbackCueKey; aliases: string[] }> = [
|
|
|
+ { key: 'session_started', aliases: ['session_started', 'sessionstarted', 'session-started', 'start'] },
|
|
|
+ { key: 'session_finished', aliases: ['session_finished', 'sessionfinished', 'session-finished', 'finish'] },
|
|
|
+ { key: 'control_completed:start', aliases: ['control_completed:start', 'start_completed', 'startcomplete', 'start-complete'] },
|
|
|
+ { key: 'control_completed:control', aliases: ['control_completed:control', 'control_completed', 'controlcomplete', 'control_complete'] },
|
|
|
+ { key: 'control_completed:finish', aliases: ['control_completed:finish', 'finish_completed', 'finishcomplete', 'finish-complete'] },
|
|
|
+ { key: 'punch_feedback:warning', aliases: ['punch_feedback:warning', 'warning', 'punch_warning', 'punchwarning'] },
|
|
|
+ { key: 'guidance:searching', aliases: ['guidance:searching', 'searching', 'search'] },
|
|
|
+ { key: 'guidance:approaching', aliases: ['guidance:approaching', 'approaching', 'approach', 'near'] },
|
|
|
+ { key: 'guidance:ready', aliases: ['guidance:ready', 'ready', 'punch_ready', 'can_punch'] },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const cues: GameHapticsConfigOverrides['cues'] = {}
|
|
|
+ for (const cueDef of cueMap) {
|
|
|
+ const cue = buildHapticsCueOverride(getFirstDefined(normalizedCues, cueDef.aliases))
|
|
|
+ if (cue) {
|
|
|
+ cues[cueDef.key] = cue
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return mergeGameHapticsConfig({
|
|
|
+ enabled: normalized.enabled !== undefined ? parseBoolean(normalized.enabled, true) : undefined,
|
|
|
+ cues,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function parseUiEffectsConfig(rawValue: unknown): GameUiEffectsConfig {
|
|
|
+ const normalized = normalizeObjectRecord(rawValue)
|
|
|
+ if (!Object.keys(normalized).length) {
|
|
|
+ return mergeGameUiEffectsConfig()
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalizedCues = normalizeObjectRecord(getFirstDefined(normalized, ['cues', 'events']))
|
|
|
+ const cueMap: Array<{ key: FeedbackCueKey; aliases: string[] }> = [
|
|
|
+ { key: 'session_started', aliases: ['session_started', 'sessionstarted', 'session-started', 'start'] },
|
|
|
+ { key: 'session_finished', aliases: ['session_finished', 'sessionfinished', 'session-finished', 'finish'] },
|
|
|
+ { key: 'control_completed:start', aliases: ['control_completed:start', 'start_completed', 'startcomplete', 'start-complete'] },
|
|
|
+ { key: 'control_completed:control', aliases: ['control_completed:control', 'control_completed', 'controlcomplete', 'control_complete'] },
|
|
|
+ { key: 'control_completed:finish', aliases: ['control_completed:finish', 'finish_completed', 'finishcomplete', 'finish-complete'] },
|
|
|
+ { key: 'punch_feedback:warning', aliases: ['punch_feedback:warning', 'warning', 'punch_warning', 'punchwarning'] },
|
|
|
+ { key: 'guidance:searching', aliases: ['guidance:searching', 'searching', 'search'] },
|
|
|
+ { key: 'guidance:approaching', aliases: ['guidance:approaching', 'approaching', 'approach', 'near'] },
|
|
|
+ { key: 'guidance:ready', aliases: ['guidance:ready', 'ready', 'punch_ready', 'can_punch'] },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const cues: GameUiEffectsConfigOverrides['cues'] = {}
|
|
|
+ for (const cueDef of cueMap) {
|
|
|
+ const cue = buildUiCueOverride(getFirstDefined(normalizedCues, cueDef.aliases))
|
|
|
+ if (cue) {
|
|
|
+ cues[cueDef.key] = cue
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return mergeGameUiEffectsConfig({
|
|
|
+ enabled: normalized.enabled !== undefined ? parseBoolean(normalized.enabled, true) : undefined,
|
|
|
+ cues,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGameConfig {
|
|
|
let parsed: Record<string, unknown>
|
|
|
try {
|
|
|
parsed = JSON.parse(text)
|
|
|
@@ -238,6 +621,19 @@ function parseGameConfigFromJson(text: string): ParsedGameConfig {
|
|
|
normalizedGame[key.toLowerCase()] = rawGame[key]
|
|
|
}
|
|
|
}
|
|
|
+ const rawAudio = rawGame && rawGame.audio !== undefined ? rawGame.audio : parsed.audio
|
|
|
+ const rawHaptics = rawGame && rawGame.haptics !== undefined ? rawGame.haptics : parsed.haptics
|
|
|
+ const rawUiEffects = rawGame && rawGame.uiEffects !== undefined
|
|
|
+ ? rawGame.uiEffects
|
|
|
+ : rawGame && rawGame.uieffects !== undefined
|
|
|
+ ? rawGame.uieffects
|
|
|
+ : rawGame && rawGame.ui !== undefined
|
|
|
+ ? rawGame.ui
|
|
|
+ : (parsed as Record<string, unknown>).uiEffects !== undefined
|
|
|
+ ? (parsed as Record<string, unknown>).uiEffects
|
|
|
+ : (parsed as Record<string, unknown>).uieffects !== undefined
|
|
|
+ ? (parsed as Record<string, unknown>).uieffects
|
|
|
+ : (parsed as Record<string, unknown>).ui
|
|
|
|
|
|
const mapRoot = typeof normalized.map === 'string' ? normalized.map : ''
|
|
|
const mapMeta = typeof normalized.mapmeta === 'string' ? normalized.mapmeta : ''
|
|
|
@@ -272,11 +668,14 @@ function parseGameConfigFromJson(text: string): ParsedGameConfig {
|
|
|
normalizedGame.autofinishonlastcontrol !== undefined ? normalizedGame.autofinishonlastcontrol : normalized.autofinishonlastcontrol,
|
|
|
true,
|
|
|
),
|
|
|
+ audioConfig: parseAudioConfig(rawAudio, gameConfigUrl),
|
|
|
+ hapticsConfig: parseHapticsConfig(rawHaptics),
|
|
|
+ uiEffectsConfig: parseUiEffectsConfig(rawUiEffects),
|
|
|
declinationDeg: parseDeclinationValue(normalized.declination),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function parseGameConfigFromYaml(text: string): ParsedGameConfig {
|
|
|
+function parseGameConfigFromYaml(text: string, gameConfigUrl: string): ParsedGameConfig {
|
|
|
const config: Record<string, string> = {}
|
|
|
const lines = text.split(/\r?\n/)
|
|
|
|
|
|
@@ -317,6 +716,48 @@ function parseGameConfigFromYaml(text: string): ParsedGameConfig {
|
|
|
5,
|
|
|
),
|
|
|
autoFinishOnLastControl: parseBoolean(config.autofinishonlastcontrol, true),
|
|
|
+ audioConfig: parseAudioConfig({
|
|
|
+ enabled: config.audioenabled,
|
|
|
+ masterVolume: config.audiomastervolume,
|
|
|
+ obeyMuteSwitch: config.audioobeymuteswitch,
|
|
|
+ approachDistanceMeters: config.audioapproachdistancemeters !== undefined ? config.audioapproachdistancemeters : config.audioapproachdistance,
|
|
|
+ cues: {
|
|
|
+ session_started: config.audiosessionstarted,
|
|
|
+ 'control_completed:start': config.audiostartcomplete,
|
|
|
+ 'control_completed:control': config.audiocontrolcomplete,
|
|
|
+ 'control_completed:finish': config.audiofinishcomplete,
|
|
|
+ 'punch_feedback:warning': config.audiowarning,
|
|
|
+ 'guidance:searching': config.audiosearching,
|
|
|
+ 'guidance:approaching': config.audioapproaching,
|
|
|
+ 'guidance:ready': config.audioready,
|
|
|
+ },
|
|
|
+ }, gameConfigUrl),
|
|
|
+ hapticsConfig: parseHapticsConfig({
|
|
|
+ enabled: config.hapticsenabled,
|
|
|
+ cues: {
|
|
|
+ session_started: config.hapticsstart,
|
|
|
+ session_finished: config.hapticsfinish,
|
|
|
+ 'control_completed:start': config.hapticsstartcomplete,
|
|
|
+ 'control_completed:control': config.hapticscontrolcomplete,
|
|
|
+ 'control_completed:finish': config.hapticsfinishcomplete,
|
|
|
+ 'punch_feedback:warning': config.hapticswarning,
|
|
|
+ 'guidance:searching': config.hapticssearching,
|
|
|
+ 'guidance:approaching': config.hapticsapproaching,
|
|
|
+ 'guidance:ready': config.hapticsready,
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ uiEffectsConfig: parseUiEffectsConfig({
|
|
|
+ enabled: config.uieffectsenabled,
|
|
|
+ cues: {
|
|
|
+ session_started: { enabled: config.uistartenabled, punchButtonMotion: config.uistartbuttonmotion },
|
|
|
+ session_finished: { enabled: config.uifinishenabled, contentCardMotion: config.uifinishcardmotion },
|
|
|
+ 'control_completed:start': { enabled: config.uistartcompleteenabled, contentCardMotion: config.uistartcompletecardmotion, punchFeedbackMotion: config.uistartcompletetoastmotion },
|
|
|
+ 'control_completed:control': { enabled: config.uicontrolcompleteenabled, contentCardMotion: config.uicontrolcompletecardmotion, punchFeedbackMotion: config.uicontrolcompletetoastmotion },
|
|
|
+ 'control_completed:finish': { enabled: config.uifinishcompleteenabled, contentCardMotion: config.uifinishcompletecardmotion, punchFeedbackMotion: config.uifinishcompletetoastmotion },
|
|
|
+ 'punch_feedback:warning': { enabled: config.uiwarningenabled, punchFeedbackMotion: config.uiwarningtoastmotion, punchButtonMotion: config.uiwarningbuttonmotion, durationMs: config.uiwarningdurationms },
|
|
|
+ 'guidance:ready': { enabled: config.uireadyenabled, punchButtonMotion: config.uireadybuttonmotion, durationMs: config.uireadydurationms },
|
|
|
+ },
|
|
|
+ }),
|
|
|
declinationDeg: parseDeclinationValue(config.declination),
|
|
|
}
|
|
|
}
|
|
|
@@ -328,7 +769,7 @@ function parseGameConfig(text: string, gameConfigUrl: string): ParsedGameConfig
|
|
|
trimmedText.startsWith('[') ||
|
|
|
/\.json(?:[?#].*)?$/i.test(gameConfigUrl)
|
|
|
|
|
|
- return isJson ? parseGameConfigFromJson(trimmedText) : parseGameConfigFromYaml(trimmedText)
|
|
|
+ return isJson ? parseGameConfigFromJson(trimmedText, gameConfigUrl) : parseGameConfigFromYaml(trimmedText, gameConfigUrl)
|
|
|
}
|
|
|
|
|
|
function extractStringField(text: string, key: string): string | null {
|
|
|
@@ -538,6 +979,9 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
|
|
|
punchPolicy: gameConfig.punchPolicy,
|
|
|
punchRadiusMeters: gameConfig.punchRadiusMeters,
|
|
|
autoFinishOnLastControl: gameConfig.autoFinishOnLastControl,
|
|
|
+ audioConfig: gameConfig.audioConfig,
|
|
|
+ hapticsConfig: gameConfig.hapticsConfig,
|
|
|
+ uiEffectsConfig: gameConfig.uiEffectsConfig,
|
|
|
}
|
|
|
}
|
|
|
|