Add configurable game flow, finish punching, and audio cues
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user