Add score-o mode and split game map HUD presentation

This commit is contained in:
2026-03-24 12:27:45 +08:00
parent a117a25824
commit 0295893b56
18 changed files with 1121 additions and 113 deletions

View File

@@ -5,8 +5,15 @@ import { type GameEvent } from '../core/gameEvent'
import { type GameEffect, type GameResult } from '../core/gameResult'
import { type GameSessionState } from '../core/gameSessionState'
import { EMPTY_GAME_PRESENTATION_STATE, type GamePresentationState } from '../presentation/presentationState'
import { type HudPresentationState } from '../presentation/hudPresentationState'
import { type MapPresentationState } from '../presentation/mapPresentationState'
import { type RulePlugin } from './rulePlugin'
type ClassicSequentialModeState = {
mode: 'classic-sequential'
phase: 'start' | 'course' | 'finish' | 'done'
}
function getApproxDistanceMeters(a: LonLatPoint, b: LonLatPoint): number {
const avgLatRad = ((a.lat + b.lat) / 2) * Math.PI / 180
const dx = (b.lon - a.lon) * 111320 * Math.cos(avgLatRad)
@@ -132,43 +139,84 @@ function buildPresentation(definition: GameDefinition, state: GameSessionState):
: '打点'
: '打点'
const revealFullCourse = completedStart
const hudPresentation: HudPresentationState = {
actionTagText: '目标',
distanceTagText: '点距',
hudTargetControlId: currentTarget ? currentTarget.id : null,
progressText: '0/0',
punchButtonText,
punchableControlId: punchButtonEnabled && currentTarget ? currentTarget.id : null,
punchButtonEnabled,
punchHintText: buildPunchHintText(definition, state, currentTarget),
}
if (!scoringControls.length) {
return {
...EMPTY_GAME_PRESENTATION_STATE,
activeStart,
completedStart,
activeFinish,
completedFinish,
revealFullCourse,
activeLegIndices,
completedLegIndices,
progressText: '0/0',
punchButtonText,
punchableControlId: punchButtonEnabled && currentTarget ? currentTarget.id : null,
punchButtonEnabled,
punchHintText: buildPunchHintText(definition, state, currentTarget),
map: {
...EMPTY_GAME_PRESENTATION_STATE.map,
controlVisualMode: 'single-target',
showCourseLegs: true,
guidanceLegAnimationEnabled: true,
focusableControlIds: [],
focusedControlId: null,
focusedControlSequences: [],
activeStart,
completedStart,
activeFinish,
focusedFinish: false,
completedFinish,
revealFullCourse,
activeLegIndices,
completedLegIndices,
},
hud: hudPresentation,
}
}
return {
const mapPresentation: MapPresentationState = {
controlVisualMode: 'single-target',
showCourseLegs: true,
guidanceLegAnimationEnabled: true,
focusableControlIds: [],
focusedControlId: null,
focusedControlSequences: [],
activeControlIds: running && currentTarget ? [currentTarget.id] : [],
activeControlSequences: running && currentTarget && currentTarget.kind === 'control' && typeof currentTarget.sequence === 'number' ? [currentTarget.sequence] : [],
activeStart,
completedStart,
activeFinish,
focusedFinish: false,
completedFinish,
revealFullCourse,
activeLegIndices,
completedLegIndices,
completedControlIds: completedControls.map((control) => control.id),
completedControlSequences: getCompletedControlSequences(definition, state),
progressText: `${completedControls.length}/${scoringControls.length}`,
punchableControlId: punchButtonEnabled && currentTarget ? currentTarget.id : null,
punchButtonEnabled,
punchButtonText,
punchHintText: buildPunchHintText(definition, state, currentTarget),
}
return {
map: mapPresentation,
hud: {
...hudPresentation,
progressText: `${completedControls.length}/${scoringControls.length}`,
},
}
}
function resolveClassicPhase(nextTarget: GameControl | null, currentTarget: GameControl, finished: boolean): ClassicSequentialModeState['phase'] {
if (finished || currentTarget.kind === 'finish') {
return 'done'
}
if (currentTarget.kind === 'start') {
return nextTarget && nextTarget.kind === 'finish' ? 'finish' : 'course'
}
if (nextTarget && nextTarget.kind === 'finish') {
return 'finish'
}
return 'course'
}
function getInitialTargetId(definition: GameDefinition): string | null {
@@ -237,6 +285,10 @@ function applyCompletion(definition: GameDefinition, state: GameSessionState, cu
status: finished ? 'finished' : state.status,
endedAt: finished ? at : state.endedAt,
guidanceState: nextTarget ? 'searching' : 'searching',
modeState: {
mode: 'classic-sequential',
phase: resolveClassicPhase(nextTarget, currentTarget, finished),
},
}
const effects: GameEffect[] = [buildCompletedEffect(currentTarget)]
@@ -266,6 +318,10 @@ export class ClassicSequentialRule implements RulePlugin {
inRangeControlId: null,
score: 0,
guidanceState: 'searching',
modeState: {
mode: 'classic-sequential',
phase: 'start',
},
}
}
@@ -282,6 +338,10 @@ export class ClassicSequentialRule implements RulePlugin {
endedAt: null,
inRangeControlId: null,
guidanceState: 'searching',
modeState: {
mode: 'classic-sequential',
phase: 'start',
},
}
return {
nextState,
@@ -296,6 +356,10 @@ export class ClassicSequentialRule implements RulePlugin {
status: 'finished',
endedAt: event.at,
guidanceState: 'searching',
modeState: {
mode: 'classic-sequential',
phase: 'done',
},
}
return {
nextState,