Add event-driven gameplay feedback framework
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user