|
|
@@ -9,7 +9,7 @@ import { lonLatToWorldTile, worldTileToLonLat, type LonLatPoint, type MapCalibra
|
|
|
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
|
|
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
|
|
import { GameRuntime } from '../../game/core/gameRuntime'
|
|
|
-import { type GameEffect } from '../../game/core/gameResult'
|
|
|
+import { type GameEffect, type GameResult } from '../../game/core/gameResult'
|
|
|
import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
|
|
|
import { FeedbackDirector } from '../../game/feedback/feedbackDirector'
|
|
|
import { EMPTY_GAME_PRESENTATION_STATE, type GamePresentationState } from '../../game/presentation/presentationState'
|
|
|
@@ -170,6 +170,7 @@ export interface MapEngineViewState {
|
|
|
panelAccuracyUnitText: string
|
|
|
punchButtonText: string
|
|
|
punchButtonEnabled: boolean
|
|
|
+ skipButtonEnabled: boolean
|
|
|
punchHintText: string
|
|
|
punchFeedbackVisible: boolean
|
|
|
punchFeedbackText: string
|
|
|
@@ -194,6 +195,18 @@ export interface MapEngineCallbacks {
|
|
|
onData: (patch: Partial<MapEngineViewState>) => void
|
|
|
}
|
|
|
|
|
|
+export interface MapEngineGameInfoRow {
|
|
|
+ label: string
|
|
|
+ value: string
|
|
|
+}
|
|
|
+
|
|
|
+export interface MapEngineGameInfoSnapshot {
|
|
|
+ title: string
|
|
|
+ subtitle: string
|
|
|
+ localRows: MapEngineGameInfoRow[]
|
|
|
+ globalRows: MapEngineGameInfoRow[]
|
|
|
+}
|
|
|
+
|
|
|
const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|
|
'buildVersion',
|
|
|
'renderMode',
|
|
|
@@ -273,6 +286,7 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|
|
'panelAccuracyUnitText',
|
|
|
'punchButtonText',
|
|
|
'punchButtonEnabled',
|
|
|
+ 'skipButtonEnabled',
|
|
|
'punchHintText',
|
|
|
'punchFeedbackVisible',
|
|
|
'punchFeedbackText',
|
|
|
@@ -338,6 +352,19 @@ function interpolateAngleDeg(currentDeg: number, targetDeg: number, factor: numb
|
|
|
return normalizeRotationDeg(currentDeg + normalizeAngleDeltaDeg(targetDeg - currentDeg) * factor)
|
|
|
}
|
|
|
|
|
|
+function formatGameSessionStatusText(status: 'idle' | 'running' | 'finished' | 'failed'): string {
|
|
|
+ if (status === 'running') {
|
|
|
+ return '进行中'
|
|
|
+ }
|
|
|
+ if (status === 'finished') {
|
|
|
+ return '已结束'
|
|
|
+ }
|
|
|
+ if (status === 'failed') {
|
|
|
+ return '已失败'
|
|
|
+ }
|
|
|
+ return '未开始'
|
|
|
+}
|
|
|
+
|
|
|
function formatRotationText(rotationDeg: number): string {
|
|
|
return `${Math.round(normalizeRotationDeg(rotationDeg))}deg`
|
|
|
}
|
|
|
@@ -577,12 +604,21 @@ export class MapEngine {
|
|
|
courseData: OrienteeringCourseData | null
|
|
|
courseOverlayVisible: boolean
|
|
|
cpRadiusMeters: number
|
|
|
+ configAppId: string
|
|
|
+ configSchemaVersion: string
|
|
|
+ configVersion: string
|
|
|
+ controlScoreOverrides: Record<string, number>
|
|
|
+ defaultControlScore: number | null
|
|
|
gameRuntime: GameRuntime
|
|
|
telemetryRuntime: TelemetryRuntime
|
|
|
gamePresentation: GamePresentationState
|
|
|
gameMode: 'classic-sequential' | 'score-o'
|
|
|
punchPolicy: 'enter' | 'enter-confirm'
|
|
|
punchRadiusMeters: number
|
|
|
+ requiresFocusSelection: boolean
|
|
|
+ skipEnabled: boolean
|
|
|
+ skipRadiusMeters: number
|
|
|
+ skipRequiresConfirm: boolean
|
|
|
autoFinishOnLastControl: boolean
|
|
|
punchFeedbackTimer: number
|
|
|
contentCardTimer: number
|
|
|
@@ -734,6 +770,11 @@ export class MapEngine {
|
|
|
this.courseData = null
|
|
|
this.courseOverlayVisible = false
|
|
|
this.cpRadiusMeters = 5
|
|
|
+ this.configAppId = ''
|
|
|
+ this.configSchemaVersion = '1'
|
|
|
+ this.configVersion = ''
|
|
|
+ this.controlScoreOverrides = {}
|
|
|
+ this.defaultControlScore = null
|
|
|
this.gameRuntime = new GameRuntime()
|
|
|
this.telemetryRuntime = new TelemetryRuntime()
|
|
|
this.telemetryRuntime.configure()
|
|
|
@@ -741,6 +782,10 @@ export class MapEngine {
|
|
|
this.gameMode = 'classic-sequential'
|
|
|
this.punchPolicy = 'enter-confirm'
|
|
|
this.punchRadiusMeters = 5
|
|
|
+ this.requiresFocusSelection = false
|
|
|
+ this.skipEnabled = false
|
|
|
+ this.skipRadiusMeters = 30
|
|
|
+ this.skipRequiresConfirm = true
|
|
|
this.autoFinishOnLastControl = true
|
|
|
this.punchFeedbackTimer = 0
|
|
|
this.contentCardTimer = 0
|
|
|
@@ -754,7 +799,7 @@ export class MapEngine {
|
|
|
projectionMode: PROJECTION_MODE,
|
|
|
mapReady: false,
|
|
|
mapReadyText: 'BOOTING',
|
|
|
- mapName: 'LCX 测试地图',
|
|
|
+ mapName: '未命名配置',
|
|
|
configStatusText: '远程配置待加载',
|
|
|
zoom: DEFAULT_ZOOM,
|
|
|
rotationDeg: 0,
|
|
|
@@ -836,6 +881,7 @@ export class MapEngine {
|
|
|
gameSessionStatus: 'idle',
|
|
|
gameModeText: '顺序赛',
|
|
|
punchButtonEnabled: false,
|
|
|
+ skipButtonEnabled: false,
|
|
|
punchHintText: '等待进入检查点范围',
|
|
|
punchFeedbackVisible: false,
|
|
|
punchFeedbackText: '',
|
|
|
@@ -895,6 +941,68 @@ export class MapEngine {
|
|
|
return { ...this.state }
|
|
|
}
|
|
|
|
|
|
+ getGameInfoSnapshot(): MapEngineGameInfoSnapshot {
|
|
|
+ const definition = this.gameRuntime.definition
|
|
|
+ const sessionState = this.gameRuntime.state
|
|
|
+ const telemetryState = this.telemetryRuntime.state
|
|
|
+ const telemetryPresentation = this.telemetryRuntime.getPresentation()
|
|
|
+ const currentTarget = definition && sessionState
|
|
|
+ ? definition.controls.find((control) => control.id === sessionState.currentTargetControlId) || null
|
|
|
+ : null
|
|
|
+ const currentTargetText = currentTarget
|
|
|
+ ? `${currentTarget.label} / ${currentTarget.kind === 'start'
|
|
|
+ ? '开始点'
|
|
|
+ : currentTarget.kind === 'finish'
|
|
|
+ ? '结束点'
|
|
|
+ : '检查点'}`
|
|
|
+ : '--'
|
|
|
+ const title = this.state.mapName || (definition ? definition.title : '当前游戏')
|
|
|
+ const subtitle = `${this.getGameModeText()} / ${formatGameSessionStatusText(this.state.gameSessionStatus)}`
|
|
|
+ const localRows: MapEngineGameInfoRow[] = [
|
|
|
+ { label: '比赛名称', value: title || '--' },
|
|
|
+ { label: '配置版本', value: this.configVersion || '--' },
|
|
|
+ { label: 'Schema版本', value: this.configSchemaVersion || '--' },
|
|
|
+ { label: '活动ID', value: this.configAppId || '--' },
|
|
|
+ { label: '地图', value: this.state.mapName || '--' },
|
|
|
+ { label: '模式', value: this.getGameModeText() },
|
|
|
+ { label: '状态', value: formatGameSessionStatusText(this.state.gameSessionStatus) },
|
|
|
+ { label: '当前目标', value: currentTargetText },
|
|
|
+ { label: '进度', value: this.gamePresentation.hud.progressText || '--' },
|
|
|
+ { label: '当前积分', value: sessionState ? String(sessionState.score) : '0' },
|
|
|
+ { label: '已完成点', value: sessionState ? String(sessionState.completedControlIds.length) : '0' },
|
|
|
+ { label: '已跳过点', value: sessionState ? String(sessionState.skippedControlIds.length) : '0' },
|
|
|
+ { label: '打点规则', value: `${this.punchPolicy} / ${this.punchRadiusMeters}m` },
|
|
|
+ { label: '跳点规则', value: this.skipEnabled ? `${this.skipRadiusMeters}m / ${this.skipRequiresConfirm ? '确认跳过' : '直接跳过'}` : '关闭' },
|
|
|
+ { label: '定位源', value: this.state.locationSourceText || '--' },
|
|
|
+ { label: '当前位置', value: this.state.gpsCoordText || '--' },
|
|
|
+ { label: 'GPS精度', value: telemetryState.lastGpsAccuracyMeters == null ? '--' : `${telemetryState.lastGpsAccuracyMeters.toFixed(1)}m` },
|
|
|
+ { label: '目标距离', value: `${telemetryPresentation.distanceToTargetValueText}${telemetryPresentation.distanceToTargetUnitText}` || '--' },
|
|
|
+ { label: '当前速度', value: `${telemetryPresentation.speedText} km/h` },
|
|
|
+ { label: '心率源', value: this.state.heartRateSourceText || '--' },
|
|
|
+ { label: '当前心率', value: this.state.panelHeartRateValueText === '--' ? '--' : `${this.state.panelHeartRateValueText}${this.state.panelHeartRateUnitText}` },
|
|
|
+ { label: '心率设备', value: this.state.heartRateDeviceText || '--' },
|
|
|
+ { label: '心率分区', value: this.state.panelHeartRateZoneNameText === '--' ? '--' : `${this.state.panelHeartRateZoneNameText} ${this.state.panelHeartRateZoneRangeText}` },
|
|
|
+ { label: '本局用时', value: telemetryPresentation.timerText },
|
|
|
+ { label: '累计里程', value: telemetryPresentation.mileageText },
|
|
|
+ { label: '累计消耗', value: `${telemetryPresentation.caloriesValueText}${telemetryPresentation.caloriesUnitText}` },
|
|
|
+ { label: '提示状态', value: this.state.punchHintText || '--' },
|
|
|
+ ]
|
|
|
+ const globalRows: MapEngineGameInfoRow[] = [
|
|
|
+ { label: '全球积分', value: '未接入' },
|
|
|
+ { label: '全球排名', value: '未接入' },
|
|
|
+ { label: '在线人数', value: '未接入' },
|
|
|
+ { label: '队伍状态', value: '未接入' },
|
|
|
+ { label: '实时广播', value: '未接入' },
|
|
|
+ ]
|
|
|
+
|
|
|
+ return {
|
|
|
+ title,
|
|
|
+ subtitle,
|
|
|
+ localRows,
|
|
|
+ globalRows,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
destroy(): void {
|
|
|
this.clearInertiaTimer()
|
|
|
this.clearPreviewResetTimer()
|
|
|
@@ -948,6 +1056,12 @@ export class MapEngine {
|
|
|
this.setCourseHeading(null)
|
|
|
}
|
|
|
|
|
|
+ clearStartSessionResidue(): void {
|
|
|
+ this.currentGpsTrack = []
|
|
|
+ this.courseOverlayVisible = false
|
|
|
+ this.setCourseHeading(null)
|
|
|
+ }
|
|
|
+
|
|
|
handleClearMapTestArtifacts(): void {
|
|
|
this.clearFinishedTestOverlay()
|
|
|
this.setState({
|
|
|
@@ -963,6 +1077,29 @@ export class MapEngine {
|
|
|
return this.gamePresentation.hud.hudTargetControlId
|
|
|
}
|
|
|
|
|
|
+ isSkipAvailable(): boolean {
|
|
|
+ const definition = this.gameRuntime.definition
|
|
|
+ const state = this.gameRuntime.state
|
|
|
+ if (!definition || !state || state.status !== 'running' || !definition.skipEnabled) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentTarget = definition.controls.find((control) => control.id === state.currentTargetControlId) || null
|
|
|
+ if (!currentTarget || currentTarget.kind !== 'control' || !this.currentGpsPoint) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ const avgLatRad = ((currentTarget.point.lat + this.currentGpsPoint.lat) / 2) * Math.PI / 180
|
|
|
+ const dx = (this.currentGpsPoint.lon - currentTarget.point.lon) * 111320 * Math.cos(avgLatRad)
|
|
|
+ const dy = (this.currentGpsPoint.lat - currentTarget.point.lat) * 110540
|
|
|
+ const distanceMeters = Math.sqrt(dx * dx + dy * dy)
|
|
|
+ return distanceMeters <= definition.skipRadiusMeters
|
|
|
+ }
|
|
|
+
|
|
|
+ shouldConfirmSkipAction(): boolean {
|
|
|
+ return !!(this.gameRuntime.definition && this.gameRuntime.definition.skipRequiresConfirm)
|
|
|
+ }
|
|
|
+
|
|
|
getLocationControllerViewPatch(): Partial<MapEngineViewState> {
|
|
|
const debugState = this.locationController.getDebugState()
|
|
|
return {
|
|
|
@@ -993,10 +1130,10 @@ export class MapEngine {
|
|
|
return this.gameMode === 'score-o' ? '积分赛' : '顺序赛'
|
|
|
}
|
|
|
|
|
|
- loadGameDefinitionFromCourse(): GameEffect[] {
|
|
|
+ loadGameDefinitionFromCourse(): GameResult | null {
|
|
|
if (!this.courseData) {
|
|
|
this.clearGameRuntime()
|
|
|
- return []
|
|
|
+ return null
|
|
|
}
|
|
|
|
|
|
const definition = buildGameDefinitionFromCourse(
|
|
|
@@ -1006,18 +1143,20 @@ export class MapEngine {
|
|
|
this.autoFinishOnLastControl,
|
|
|
this.punchPolicy,
|
|
|
this.punchRadiusMeters,
|
|
|
+ this.requiresFocusSelection,
|
|
|
+ this.skipEnabled,
|
|
|
+ this.skipRadiusMeters,
|
|
|
+ this.skipRequiresConfirm,
|
|
|
+ this.controlScoreOverrides,
|
|
|
+ this.defaultControlScore,
|
|
|
)
|
|
|
const result = this.gameRuntime.loadDefinition(definition)
|
|
|
this.telemetryRuntime.loadDefinition(definition)
|
|
|
- this.gamePresentation = result.presentation
|
|
|
this.courseOverlayVisible = true
|
|
|
- this.telemetryRuntime.syncGameState(this.gameRuntime.definition, result.nextState, this.getHudTargetControlId())
|
|
|
- this.refreshCourseHeadingFromPresentation()
|
|
|
+ this.syncGameResultState(result)
|
|
|
+ this.telemetryRuntime.syncGameState(this.gameRuntime.definition, result.nextState, result.presentation.hud.hudTargetControlId)
|
|
|
this.updateSessionTimerLoop()
|
|
|
- this.setState({
|
|
|
- gameModeText: this.getGameModeText(),
|
|
|
- })
|
|
|
- return result.effects
|
|
|
+ return result
|
|
|
}
|
|
|
|
|
|
refreshCourseHeadingFromPresentation(): void {
|
|
|
@@ -1083,6 +1222,7 @@ export class MapEngine {
|
|
|
panelProgressText: this.gamePresentation.hud.progressText,
|
|
|
punchButtonText: this.gamePresentation.hud.punchButtonText,
|
|
|
punchButtonEnabled: this.gamePresentation.hud.punchButtonEnabled,
|
|
|
+ skipButtonEnabled: this.isSkipAvailable(),
|
|
|
punchHintText: this.gamePresentation.hud.punchHintText,
|
|
|
}
|
|
|
|
|
|
@@ -1121,6 +1261,28 @@ export class MapEngine {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ resetTransientGameUiState(): void {
|
|
|
+ this.clearPunchFeedbackTimer()
|
|
|
+ this.clearContentCardTimer()
|
|
|
+ this.clearMapPulseTimer()
|
|
|
+ this.clearStageFxTimer()
|
|
|
+ this.setState({
|
|
|
+ punchFeedbackVisible: false,
|
|
|
+ punchFeedbackText: '',
|
|
|
+ punchFeedbackTone: 'neutral',
|
|
|
+ punchFeedbackFxClass: '',
|
|
|
+ contentCardVisible: false,
|
|
|
+ contentCardTitle: '',
|
|
|
+ contentCardBody: '',
|
|
|
+ contentCardFxClass: '',
|
|
|
+ mapPulseVisible: false,
|
|
|
+ mapPulseFxClass: '',
|
|
|
+ stageFxVisible: false,
|
|
|
+ stageFxClass: '',
|
|
|
+ punchButtonFxClass: '',
|
|
|
+ }, true)
|
|
|
+ }
|
|
|
+
|
|
|
clearSessionTimerInterval(): void {
|
|
|
if (this.sessionTimerInterval) {
|
|
|
clearInterval(this.sessionTimerInterval)
|
|
|
@@ -1300,6 +1462,33 @@ export class MapEngine {
|
|
|
return this.resolveGameStatusText(effects)
|
|
|
}
|
|
|
|
|
|
+ syncGameResultState(result: GameResult): void {
|
|
|
+ this.gamePresentation = result.presentation
|
|
|
+ this.refreshCourseHeadingFromPresentation()
|
|
|
+ }
|
|
|
+
|
|
|
+ resolveAppliedGameStatusText(result: GameResult, fallbackStatusText?: string | null): string | null {
|
|
|
+ return this.applyGameEffects(result.effects) || fallbackStatusText || this.resolveGameStatusText(result.effects)
|
|
|
+ }
|
|
|
+
|
|
|
+ commitGameResult(
|
|
|
+ result: GameResult,
|
|
|
+ fallbackStatusText?: string | null,
|
|
|
+ extraPatch: Partial<MapEngineViewState> = {},
|
|
|
+ syncRenderer = true,
|
|
|
+ ): string | null {
|
|
|
+ this.syncGameResultState(result)
|
|
|
+ const gameStatusText = this.resolveAppliedGameStatusText(result, fallbackStatusText)
|
|
|
+ this.setState({
|
|
|
+ ...this.getGameViewPatch(gameStatusText),
|
|
|
+ ...extraPatch,
|
|
|
+ }, true)
|
|
|
+ if (syncRenderer) {
|
|
|
+ this.syncRenderer()
|
|
|
+ }
|
|
|
+ return gameStatusText
|
|
|
+ }
|
|
|
+
|
|
|
handleStartGame(): void {
|
|
|
if (!this.gameRuntime.definition || !this.gameRuntime.state) {
|
|
|
this.setState({
|
|
|
@@ -1312,6 +1501,10 @@ export class MapEngine {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ this.feedbackDirector.reset()
|
|
|
+ this.resetTransientGameUiState()
|
|
|
+ this.clearStartSessionResidue()
|
|
|
+
|
|
|
if (!this.locationController.listening) {
|
|
|
this.locationController.start()
|
|
|
}
|
|
|
@@ -1328,15 +1521,30 @@ export class MapEngine {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- this.gamePresentation = this.gameRuntime.getPresentation()
|
|
|
this.courseOverlayVisible = true
|
|
|
- this.refreshCourseHeadingFromPresentation()
|
|
|
const defaultStatusText = this.currentGpsPoint
|
|
|
? `顺序打点已开始 (${this.buildVersion})`
|
|
|
: `顺序打点已开始,GPS定位启动中 (${this.buildVersion})`
|
|
|
- const gameStatusText = this.applyGameEffects(gameResult.effects) || defaultStatusText
|
|
|
+ this.commitGameResult(gameResult, defaultStatusText)
|
|
|
+ }
|
|
|
+
|
|
|
+ handleForceExitGame(): void {
|
|
|
+ this.feedbackDirector.reset()
|
|
|
+
|
|
|
+ if (!this.courseData) {
|
|
|
+ this.clearGameRuntime()
|
|
|
+ this.resetTransientGameUiState()
|
|
|
+ this.setState({
|
|
|
+ ...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
|
|
+ }, true)
|
|
|
+ this.syncRenderer()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.loadGameDefinitionFromCourse()
|
|
|
+ this.resetTransientGameUiState()
|
|
|
this.setState({
|
|
|
- ...this.getGameViewPatch(gameStatusText),
|
|
|
+ ...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
|
|
}, true)
|
|
|
this.syncRenderer()
|
|
|
}
|
|
|
@@ -1347,13 +1555,7 @@ export class MapEngine {
|
|
|
type: 'punch_requested',
|
|
|
at: Date.now(),
|
|
|
})
|
|
|
- this.gamePresentation = gameResult.presentation
|
|
|
- this.refreshCourseHeadingFromPresentation()
|
|
|
- const gameStatusText = this.applyGameEffects(gameResult.effects)
|
|
|
- this.setState({
|
|
|
- ...this.getGameViewPatch(gameStatusText),
|
|
|
- }, true)
|
|
|
- this.syncRenderer()
|
|
|
+ this.commitGameResult(gameResult)
|
|
|
}
|
|
|
|
|
|
handleLocationUpdate(longitude: number, latitude: number, accuracyMeters: number | null): void {
|
|
|
@@ -1388,9 +1590,8 @@ export class MapEngine {
|
|
|
lat: latitude,
|
|
|
accuracyMeters,
|
|
|
})
|
|
|
- this.gamePresentation = gameResult.presentation
|
|
|
- this.refreshCourseHeadingFromPresentation()
|
|
|
- gameStatusText = this.applyGameEffects(gameResult.effects)
|
|
|
+ this.syncGameResultState(gameResult)
|
|
|
+ gameStatusText = this.resolveAppliedGameStatusText(gameResult)
|
|
|
}
|
|
|
|
|
|
if (gpsInsideMap && !this.hasGpsCenteredOnce) {
|
|
|
@@ -1462,14 +1663,24 @@ export class MapEngine {
|
|
|
}
|
|
|
|
|
|
this.gameMode = nextMode
|
|
|
- const effects = this.loadGameDefinitionFromCourse()
|
|
|
+ const result = this.loadGameDefinitionFromCourse()
|
|
|
const modeText = this.getGameModeText()
|
|
|
- const statusText = this.applyGameEffects(effects) || `已切换到${modeText} (${this.buildVersion})`
|
|
|
- this.setState({
|
|
|
- ...this.getGameViewPatch(statusText),
|
|
|
+ if (!result) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.commitGameResult(result, `已切换到${modeText} (${this.buildVersion})`, {
|
|
|
gameModeText: modeText,
|
|
|
- }, true)
|
|
|
- this.syncRenderer()
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ handleSkipAction(): void {
|
|
|
+ const gameResult = this.gameRuntime.dispatch({
|
|
|
+ type: 'skip_requested',
|
|
|
+ at: Date.now(),
|
|
|
+ lon: this.currentGpsPoint ? this.currentGpsPoint.lon : null,
|
|
|
+ lat: this.currentGpsPoint ? this.currentGpsPoint.lat : null,
|
|
|
+ })
|
|
|
+ this.commitGameResult(gameResult)
|
|
|
}
|
|
|
|
|
|
handleConnectHeartRate(): void {
|
|
|
@@ -1625,9 +1836,18 @@ export class MapEngine {
|
|
|
this.tileBoundsByZoom = config.tileBoundsByZoom
|
|
|
this.courseData = config.course
|
|
|
this.cpRadiusMeters = config.cpRadiusMeters
|
|
|
+ this.configAppId = config.configAppId
|
|
|
+ this.configSchemaVersion = config.configSchemaVersion
|
|
|
+ this.configVersion = config.configVersion
|
|
|
+ this.controlScoreOverrides = config.controlScoreOverrides
|
|
|
+ this.defaultControlScore = config.defaultControlScore
|
|
|
this.gameMode = config.gameMode
|
|
|
this.punchPolicy = config.punchPolicy
|
|
|
this.punchRadiusMeters = config.punchRadiusMeters
|
|
|
+ this.requiresFocusSelection = config.requiresFocusSelection
|
|
|
+ this.skipEnabled = config.skipEnabled
|
|
|
+ this.skipRadiusMeters = config.skipRadiusMeters
|
|
|
+ this.skipRequiresConfirm = config.skipRequiresConfirm
|
|
|
this.autoFinishOnLastControl = config.autoFinishOnLastControl
|
|
|
this.telemetryRuntime.configure(config.telemetryConfig)
|
|
|
this.feedbackDirector.configure({
|
|
|
@@ -1636,10 +1856,11 @@ export class MapEngine {
|
|
|
uiEffectsConfig: config.uiEffectsConfig,
|
|
|
})
|
|
|
|
|
|
- const gameEffects = this.loadGameDefinitionFromCourse()
|
|
|
- const gameStatusText = this.applyGameEffects(gameEffects)
|
|
|
+ const gameResult = this.loadGameDefinitionFromCourse()
|
|
|
+ const gameStatusText = gameResult ? this.resolveAppliedGameStatusText(gameResult) : null
|
|
|
const statePatch: Partial<MapEngineViewState> = {
|
|
|
- configStatusText: `远程配置已载入 / ${config.courseStatusText}`,
|
|
|
+ mapName: config.configTitle,
|
|
|
+ configStatusText: `配置已载入 / ${config.configTitle} / ${config.courseStatusText}`,
|
|
|
projectionMode: config.projectionModeText,
|
|
|
tileSource: config.tileSource,
|
|
|
sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg === null ? null : getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)),
|
|
|
@@ -1647,7 +1868,7 @@ export class MapEngine {
|
|
|
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
|
|
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
|
|
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
|
|
|
- ...this.getGameViewPatch(),
|
|
|
+ ...this.getGameViewPatch(gameStatusText),
|
|
|
}
|
|
|
|
|
|
if (!this.state.stageWidth || !this.state.stageHeight) {
|
|
|
@@ -1869,12 +2090,10 @@ export class MapEngine {
|
|
|
at: Date.now(),
|
|
|
controlId: focusedControlId,
|
|
|
})
|
|
|
- this.gamePresentation = gameResult.presentation
|
|
|
- this.telemetryRuntime.syncGameState(this.gameRuntime.definition, this.gameRuntime.state, this.getHudTargetControlId())
|
|
|
- this.setState({
|
|
|
- ...this.getGameViewPatch(focusedControlId ? `已选择目标点 (${this.buildVersion})` : `已取消目标点选择 (${this.buildVersion})`),
|
|
|
- }, true)
|
|
|
- this.syncRenderer()
|
|
|
+ this.commitGameResult(
|
|
|
+ gameResult,
|
|
|
+ focusedControlId ? `已选择目标点 (${this.buildVersion})` : `已取消目标点选择 (${this.buildVersion})`,
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
findFocusableControlAt(stageX: number, stageY: number): string | null | undefined {
|
|
|
@@ -2472,6 +2691,8 @@ export class MapEngine {
|
|
|
activeLegIndices: this.gamePresentation.map.activeLegIndices,
|
|
|
completedLegIndices: this.gamePresentation.map.completedLegIndices,
|
|
|
completedControlSequences: this.gamePresentation.map.completedControlSequences,
|
|
|
+ skippedControlIds: this.gamePresentation.map.skippedControlIds,
|
|
|
+ skippedControlSequences: this.gamePresentation.map.skippedControlSequences,
|
|
|
osmReferenceEnabled: this.state.osmReferenceEnabled,
|
|
|
overlayOpacity: MAP_OVERLAY_OPACITY,
|
|
|
}
|