Add config-driven game host updates
This commit is contained in:
@@ -22,6 +22,10 @@ export interface TileZoomBounds {
|
||||
}
|
||||
|
||||
export interface RemoteMapConfig {
|
||||
configTitle: string
|
||||
configAppId: string
|
||||
configSchemaVersion: string
|
||||
configVersion: string
|
||||
tileSource: string
|
||||
minZoom: number
|
||||
maxZoom: number
|
||||
@@ -45,7 +49,13 @@ export interface RemoteMapConfig {
|
||||
gameMode: 'classic-sequential' | 'score-o'
|
||||
punchPolicy: 'enter' | 'enter-confirm'
|
||||
punchRadiusMeters: number
|
||||
requiresFocusSelection: boolean
|
||||
skipEnabled: boolean
|
||||
skipRadiusMeters: number
|
||||
skipRequiresConfirm: boolean
|
||||
autoFinishOnLastControl: boolean
|
||||
controlScoreOverrides: Record<string, number>
|
||||
defaultControlScore: number | null
|
||||
telemetryConfig: TelemetryConfig
|
||||
audioConfig: GameAudioConfig
|
||||
hapticsConfig: GameHapticsConfig
|
||||
@@ -53,14 +63,25 @@ export interface RemoteMapConfig {
|
||||
}
|
||||
|
||||
interface ParsedGameConfig {
|
||||
title: string
|
||||
appId: string
|
||||
schemaVersion: string
|
||||
version: string
|
||||
mapRoot: string
|
||||
mapMeta: string
|
||||
course: string | null
|
||||
cpRadiusMeters: number
|
||||
defaultZoom: number | null
|
||||
gameMode: 'classic-sequential' | 'score-o'
|
||||
punchPolicy: 'enter' | 'enter-confirm'
|
||||
punchRadiusMeters: number
|
||||
requiresFocusSelection: boolean
|
||||
skipEnabled: boolean
|
||||
skipRadiusMeters: number
|
||||
skipRequiresConfirm: boolean
|
||||
autoFinishOnLastControl: boolean
|
||||
controlScoreOverrides: Record<string, number>
|
||||
defaultControlScore: number | null
|
||||
telemetryConfig: TelemetryConfig
|
||||
audioConfig: GameAudioConfig
|
||||
hapticsConfig: GameHapticsConfig
|
||||
@@ -668,6 +689,18 @@ function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGam
|
||||
const rawGame = parsed.game && typeof parsed.game === 'object' && !Array.isArray(parsed.game)
|
||||
? parsed.game as Record<string, unknown>
|
||||
: null
|
||||
const rawApp = parsed.app && typeof parsed.app === 'object' && !Array.isArray(parsed.app)
|
||||
? parsed.app as Record<string, unknown>
|
||||
: null
|
||||
const rawMap = parsed.map && typeof parsed.map === 'object' && !Array.isArray(parsed.map)
|
||||
? parsed.map as Record<string, unknown>
|
||||
: null
|
||||
const rawPlayfield = parsed.playfield && typeof parsed.playfield === 'object' && !Array.isArray(parsed.playfield)
|
||||
? parsed.playfield as Record<string, unknown>
|
||||
: null
|
||||
const rawPlayfieldSource = rawPlayfield && rawPlayfield.source && typeof rawPlayfield.source === 'object' && !Array.isArray(rawPlayfield.source)
|
||||
? rawPlayfield.source as Record<string, unknown>
|
||||
: null
|
||||
const normalizedGame: Record<string, unknown> = {}
|
||||
if (rawGame) {
|
||||
const gameKeys = Object.keys(rawGame)
|
||||
@@ -690,41 +723,150 @@ function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGam
|
||||
? (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 : ''
|
||||
const rawSession = rawGame && rawGame.session && typeof rawGame.session === 'object' && !Array.isArray(rawGame.session)
|
||||
? rawGame.session as Record<string, unknown>
|
||||
: null
|
||||
const rawPunch = rawGame && rawGame.punch && typeof rawGame.punch === 'object' && !Array.isArray(rawGame.punch)
|
||||
? rawGame.punch as Record<string, unknown>
|
||||
: null
|
||||
const rawSequence = rawGame && rawGame.sequence && typeof rawGame.sequence === 'object' && !Array.isArray(rawGame.sequence)
|
||||
? rawGame.sequence as Record<string, unknown>
|
||||
: null
|
||||
const rawSkip = rawSequence && rawSequence.skip && typeof rawSequence.skip === 'object' && !Array.isArray(rawSequence.skip)
|
||||
? rawSequence.skip as Record<string, unknown>
|
||||
: null
|
||||
const rawScoring = rawGame && rawGame.scoring && typeof rawGame.scoring === 'object' && !Array.isArray(rawGame.scoring)
|
||||
? rawGame.scoring as Record<string, unknown>
|
||||
: null
|
||||
|
||||
const mapRoot = rawMap && typeof rawMap.tiles === 'string'
|
||||
? rawMap.tiles
|
||||
: typeof normalized.map === 'string'
|
||||
? normalized.map
|
||||
: ''
|
||||
const mapMeta = rawMap && typeof rawMap.mapmeta === 'string'
|
||||
? rawMap.mapmeta
|
||||
: typeof normalized.mapmeta === 'string'
|
||||
? normalized.mapmeta
|
||||
: ''
|
||||
if (!mapRoot || !mapMeta) {
|
||||
throw new Error('game.json 缺少 map 或 mapmeta 字段')
|
||||
}
|
||||
|
||||
const modeValue = typeof normalizedGame.mode === 'string' ? normalizedGame.mode : normalized.gamemode
|
||||
const gameMode = parseGameMode(modeValue)
|
||||
const rawControlOverrides = rawPlayfield && rawPlayfield.controlOverrides && typeof rawPlayfield.controlOverrides === 'object' && !Array.isArray(rawPlayfield.controlOverrides)
|
||||
? rawPlayfield.controlOverrides as Record<string, unknown>
|
||||
: null
|
||||
const controlScoreOverrides: Record<string, number> = {}
|
||||
if (rawControlOverrides) {
|
||||
const keys = Object.keys(rawControlOverrides)
|
||||
for (const key of keys) {
|
||||
const item = rawControlOverrides[key]
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
continue
|
||||
}
|
||||
const scoreValue = Number((item as Record<string, unknown>).score)
|
||||
if (Number.isFinite(scoreValue)) {
|
||||
controlScoreOverrides[key] = scoreValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: rawApp && typeof rawApp.title === 'string' ? rawApp.title : '',
|
||||
appId: rawApp && typeof rawApp.id === 'string' ? rawApp.id : '',
|
||||
schemaVersion: typeof parsed.schemaVersion === 'string' ? parsed.schemaVersion : '1',
|
||||
version: typeof parsed.version === 'string' ? parsed.version : '',
|
||||
mapRoot,
|
||||
mapMeta,
|
||||
course: typeof normalized.course === 'string' ? normalized.course : null,
|
||||
cpRadiusMeters: parsePositiveNumber(normalized.cpradius, 5),
|
||||
gameMode,
|
||||
punchPolicy: parsePunchPolicy(normalizedGame.punchpolicy !== undefined ? normalizedGame.punchpolicy : normalized.punchpolicy),
|
||||
punchRadiusMeters: parsePositiveNumber(
|
||||
normalizedGame.punchradiusmeters !== undefined
|
||||
? normalizedGame.punchradiusmeters
|
||||
: normalizedGame.punchradius !== undefined
|
||||
? normalizedGame.punchradius
|
||||
: normalized.punchradiusmeters !== undefined
|
||||
? normalized.punchradiusmeters
|
||||
: normalized.punchradius,
|
||||
course: rawPlayfieldSource && typeof rawPlayfieldSource.url === 'string'
|
||||
? rawPlayfieldSource.url
|
||||
: typeof normalized.course === 'string'
|
||||
? normalized.course
|
||||
: null,
|
||||
cpRadiusMeters: parsePositiveNumber(
|
||||
rawPlayfield && rawPlayfield.CPRadius !== undefined ? rawPlayfield.CPRadius : normalized.cpradius,
|
||||
5,
|
||||
),
|
||||
autoFinishOnLastControl: parseBoolean(
|
||||
normalizedGame.autofinishonlastcontrol !== undefined ? normalizedGame.autofinishonlastcontrol : normalized.autofinishonlastcontrol,
|
||||
defaultZoom: rawMap && rawMap.initialView && typeof rawMap.initialView === 'object' && !Array.isArray(rawMap.initialView)
|
||||
? parsePositiveNumber((rawMap.initialView as Record<string, unknown>).zoom, 17)
|
||||
: null,
|
||||
gameMode,
|
||||
punchPolicy: parsePunchPolicy(
|
||||
rawPunch && rawPunch.policy !== undefined
|
||||
? rawPunch.policy
|
||||
: normalizedGame.punchpolicy !== undefined
|
||||
? normalizedGame.punchpolicy
|
||||
: normalized.punchpolicy,
|
||||
),
|
||||
punchRadiusMeters: parsePositiveNumber(
|
||||
rawPunch && rawPunch.radiusMeters !== undefined
|
||||
? rawPunch.radiusMeters
|
||||
: normalizedGame.punchradiusmeters !== undefined
|
||||
? normalizedGame.punchradiusmeters
|
||||
: normalizedGame.punchradius !== undefined
|
||||
? normalizedGame.punchradius
|
||||
: normalized.punchradiusmeters !== undefined
|
||||
? normalized.punchradiusmeters
|
||||
: normalized.punchradius,
|
||||
5,
|
||||
),
|
||||
requiresFocusSelection: parseBoolean(
|
||||
rawPunch && rawPunch.requiresFocusSelection !== undefined
|
||||
? rawPunch.requiresFocusSelection
|
||||
: normalizedGame.requiresfocusselection !== undefined
|
||||
? normalizedGame.requiresfocusselection
|
||||
: rawPunch && (rawPunch as Record<string, unknown>).requiresfocusselection !== undefined
|
||||
? (rawPunch as Record<string, unknown>).requiresfocusselection
|
||||
: normalized.requiresfocusselection,
|
||||
false,
|
||||
),
|
||||
skipEnabled: parseBoolean(
|
||||
rawSkip && rawSkip.enabled !== undefined
|
||||
? rawSkip.enabled
|
||||
: normalizedGame.skipenabled !== undefined
|
||||
? normalizedGame.skipenabled
|
||||
: normalized.skipenabled,
|
||||
false,
|
||||
),
|
||||
skipRadiusMeters: parsePositiveNumber(
|
||||
rawSkip && rawSkip.radiusMeters !== undefined
|
||||
? rawSkip.radiusMeters
|
||||
: normalizedGame.skipradiusmeters !== undefined
|
||||
? normalizedGame.skipradiusmeters
|
||||
: normalizedGame.skipradius !== undefined
|
||||
? normalizedGame.skipradius
|
||||
: normalized.skipradiusmeters !== undefined
|
||||
? normalized.skipradiusmeters
|
||||
: normalized.skipradius,
|
||||
30,
|
||||
),
|
||||
skipRequiresConfirm: parseBoolean(
|
||||
rawSkip && rawSkip.requiresConfirm !== undefined
|
||||
? rawSkip.requiresConfirm
|
||||
: normalizedGame.skiprequiresconfirm !== undefined
|
||||
? normalizedGame.skiprequiresconfirm
|
||||
: normalized.skiprequiresconfirm,
|
||||
true,
|
||||
),
|
||||
autoFinishOnLastControl: parseBoolean(
|
||||
rawSession && rawSession.autoFinishOnLastControl !== undefined
|
||||
? rawSession.autoFinishOnLastControl
|
||||
: normalizedGame.autofinishonlastcontrol !== undefined
|
||||
? normalizedGame.autofinishonlastcontrol
|
||||
: normalized.autofinishonlastcontrol,
|
||||
true,
|
||||
),
|
||||
controlScoreOverrides,
|
||||
defaultControlScore: rawScoring && rawScoring.defaultControlScore !== undefined
|
||||
? parsePositiveNumber(rawScoring.defaultControlScore, 10)
|
||||
: null,
|
||||
telemetryConfig: parseTelemetryConfig(rawTelemetry),
|
||||
audioConfig: parseAudioConfig(rawAudio, gameConfigUrl),
|
||||
hapticsConfig: parseHapticsConfig(rawHaptics),
|
||||
uiEffectsConfig: parseUiEffectsConfig(rawUiEffects),
|
||||
declinationDeg: parseDeclinationValue(normalized.declination),
|
||||
declinationDeg: parseDeclinationValue(rawMap && rawMap.declination !== undefined ? rawMap.declination : normalized.declination),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,17 +897,31 @@ function parseGameConfigFromYaml(text: string, gameConfigUrl: string): ParsedGam
|
||||
const gameMode = parseGameMode(config.gamemode)
|
||||
|
||||
return {
|
||||
title: '',
|
||||
appId: '',
|
||||
schemaVersion: '1',
|
||||
version: '',
|
||||
mapRoot,
|
||||
mapMeta,
|
||||
course: typeof config.course === 'string' ? config.course : null,
|
||||
cpRadiusMeters: parsePositiveNumber(config.cpradius, 5),
|
||||
defaultZoom: null,
|
||||
gameMode,
|
||||
punchPolicy: parsePunchPolicy(config.punchpolicy),
|
||||
punchRadiusMeters: parsePositiveNumber(
|
||||
config.punchradiusmeters !== undefined ? config.punchradiusmeters : config.punchradius,
|
||||
5,
|
||||
),
|
||||
requiresFocusSelection: parseBoolean(config.requiresfocusselection, false),
|
||||
skipEnabled: parseBoolean(config.skipenabled, false),
|
||||
skipRadiusMeters: parsePositiveNumber(
|
||||
config.skipradiusmeters !== undefined ? config.skipradiusmeters : config.skipradius,
|
||||
30,
|
||||
),
|
||||
skipRequiresConfirm: parseBoolean(config.skiprequiresconfirm, true),
|
||||
autoFinishOnLastControl: parseBoolean(config.autofinishonlastcontrol, true),
|
||||
controlScoreOverrides: {},
|
||||
defaultControlScore: null,
|
||||
telemetryConfig: parseTelemetryConfig({
|
||||
heartRate: {
|
||||
age: config.heartrateage !== undefined ? config.heartrateage : config.telemetryheartrateage,
|
||||
@@ -1010,13 +1166,17 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
|
||||
}
|
||||
}
|
||||
|
||||
const defaultZoom = clamp(17, mapMeta.minZoom, mapMeta.maxZoom)
|
||||
const defaultZoom = clamp(gameConfig.defaultZoom || 17, mapMeta.minZoom, mapMeta.maxZoom)
|
||||
const boundsCorners = mapMeta.bounds ? getBoundsCorners(mapMeta.bounds, mapMeta.projection) : null
|
||||
const centerWorldTile = boundsCorners
|
||||
? lonLatToWorldTile(boundsCorners.center, defaultZoom)
|
||||
: { x: 0, y: 0 }
|
||||
|
||||
return {
|
||||
configTitle: gameConfig.title || '未命名配置',
|
||||
configAppId: gameConfig.appId || '',
|
||||
configSchemaVersion: gameConfig.schemaVersion || '1',
|
||||
configVersion: gameConfig.version || '',
|
||||
tileSource: resolveUrl(mapRootUrl, mapMeta.tilePathTemplate),
|
||||
minZoom: mapMeta.minZoom,
|
||||
maxZoom: mapMeta.maxZoom,
|
||||
@@ -1040,7 +1200,13 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
|
||||
gameMode: gameConfig.gameMode,
|
||||
punchPolicy: gameConfig.punchPolicy,
|
||||
punchRadiusMeters: gameConfig.punchRadiusMeters,
|
||||
requiresFocusSelection: gameConfig.requiresFocusSelection,
|
||||
skipEnabled: gameConfig.skipEnabled,
|
||||
skipRadiusMeters: gameConfig.skipRadiusMeters,
|
||||
skipRequiresConfirm: gameConfig.skipRequiresConfirm,
|
||||
autoFinishOnLastControl: gameConfig.autoFinishOnLastControl,
|
||||
controlScoreOverrides: gameConfig.controlScoreOverrides,
|
||||
defaultControlScore: gameConfig.defaultControlScore,
|
||||
telemetryConfig: gameConfig.telemetryConfig,
|
||||
audioConfig: gameConfig.audioConfig,
|
||||
hapticsConfig: gameConfig.hapticsConfig,
|
||||
|
||||
Reference in New Issue
Block a user