Add configurable game flow, finish punching, and audio cues

This commit is contained in:
2026-03-23 19:35:17 +08:00
parent 3b4b3ee3ec
commit 48159be900
23 changed files with 1620 additions and 68 deletions

View File

@@ -29,6 +29,10 @@ export interface RemoteMapConfig {
course: OrienteeringCourseData | null
courseStatusText: string
cpRadiusMeters: number
gameMode: 'classic-sequential'
punchPolicy: 'enter' | 'enter-confirm'
punchRadiusMeters: number
autoFinishOnLastControl: boolean
}
interface ParsedGameConfig {
@@ -36,6 +40,10 @@ interface ParsedGameConfig {
mapMeta: string
course: string | null
cpRadiusMeters: number
gameMode: 'classic-sequential'
punchPolicy: 'enter' | 'enter-confirm'
punchRadiusMeters: number
autoFinishOnLastControl: boolean
declinationDeg: number
}
@@ -158,6 +166,28 @@ function parsePositiveNumber(rawValue: unknown, fallbackValue: number): number {
return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : fallbackValue
}
function parseBoolean(rawValue: unknown, fallbackValue: boolean): boolean {
if (typeof rawValue === 'boolean') {
return rawValue
}
if (typeof rawValue === 'string') {
const normalized = rawValue.trim().toLowerCase()
if (normalized === 'true') {
return true
}
if (normalized === 'false') {
return false
}
}
return fallbackValue
}
function parsePunchPolicy(rawValue: unknown): 'enter' | 'enter-confirm' {
return rawValue === 'enter' ? 'enter' : 'enter-confirm'
}
function parseLooseJsonObject(text: string): Record<string, unknown> {
const parsed: Record<string, unknown> = {}
const pairPattern = /"([^"]+)"\s*:\s*("([^"]*)"|-?\d+(?:\.\d+)?|true|false|null)/g
@@ -198,17 +228,50 @@ function parseGameConfigFromJson(text: string): ParsedGameConfig {
normalized[key.toLowerCase()] = parsed[key]
}
const rawGame = parsed.game && typeof parsed.game === 'object' && !Array.isArray(parsed.game)
? parsed.game as Record<string, unknown>
: null
const normalizedGame: Record<string, unknown> = {}
if (rawGame) {
const gameKeys = Object.keys(rawGame)
for (const key of gameKeys) {
normalizedGame[key.toLowerCase()] = rawGame[key]
}
}
const mapRoot = typeof normalized.map === 'string' ? normalized.map : ''
const mapMeta = typeof normalized.mapmeta === 'string' ? normalized.mapmeta : ''
if (!mapRoot || !mapMeta) {
throw new Error('game.json 缺少 map 或 mapmeta 字段')
}
const gameMode = 'classic-sequential' as const
const modeValue = typeof normalizedGame.mode === 'string' ? normalizedGame.mode : normalized.gamemode
if (typeof modeValue === 'string' && modeValue !== gameMode) {
throw new Error(`暂不支持的 game.mode: ${modeValue}`)
}
return {
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,
5,
),
autoFinishOnLastControl: parseBoolean(
normalizedGame.autofinishonlastcontrol !== undefined ? normalizedGame.autofinishonlastcontrol : normalized.autofinishonlastcontrol,
true,
),
declinationDeg: parseDeclinationValue(normalized.declination),
}
}
@@ -237,11 +300,23 @@ function parseGameConfigFromYaml(text: string): ParsedGameConfig {
throw new Error('game.yaml 缺少 map 或 mapmeta 字段')
}
const gameMode = 'classic-sequential' as const
if (config.gamemode && config.gamemode !== gameMode) {
throw new Error(`暂不支持的 game.mode: ${config.gamemode}`)
}
return {
mapRoot,
mapMeta,
course: typeof config.course === 'string' ? config.course : null,
cpRadiusMeters: parsePositiveNumber(config.cpradius, 5),
gameMode,
punchPolicy: parsePunchPolicy(config.punchpolicy),
punchRadiusMeters: parsePositiveNumber(
config.punchradiusmeters !== undefined ? config.punchradiusmeters : config.punchradius,
5,
),
autoFinishOnLastControl: parseBoolean(config.autofinishonlastcontrol, true),
declinationDeg: parseDeclinationValue(config.declination),
}
}
@@ -459,5 +534,12 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
course,
courseStatusText,
cpRadiusMeters: gameConfig.cpRadiusMeters,
gameMode: gameConfig.gameMode,
punchPolicy: gameConfig.punchPolicy,
punchRadiusMeters: gameConfig.punchRadiusMeters,
autoFinishOnLastControl: gameConfig.autoFinishOnLastControl,
}
}