Add event-driven gameplay feedback framework

This commit is contained in:
2026-03-24 09:03:27 +08:00
parent 48159be900
commit 2c03d1a702
20 changed files with 1718 additions and 64 deletions

View File

@@ -1,4 +1,5 @@
import { type LonLatPoint } from '../../utils/projection'
import { DEFAULT_GAME_AUDIO_CONFIG } from '../audio/audioConfig'
import { type GameControl, type GameDefinition } from '../core/gameDefinition'
import { type GameEvent } from '../core/gameEvent'
import { type GameEffect, type GameResult } from '../core/gameResult'
@@ -56,6 +57,31 @@ function getTargetText(control: GameControl): string {
return '目标圈'
}
function getGuidanceState(definition: GameDefinition, distanceMeters: number): GameSessionState['guidanceState'] {
if (distanceMeters <= definition.punchRadiusMeters) {
return 'ready'
}
const approachDistanceMeters = definition.audioConfig ? definition.audioConfig.approachDistanceMeters : DEFAULT_GAME_AUDIO_CONFIG.approachDistanceMeters
if (distanceMeters <= approachDistanceMeters) {
return 'approaching'
}
return 'searching'
}
function getGuidanceEffects(
previousState: GameSessionState['guidanceState'],
nextState: GameSessionState['guidanceState'],
controlId: string | null,
): GameEffect[] {
if (previousState === nextState) {
return []
}
return [{ type: 'guidance_state_changed', guidanceState: nextState, controlId }]
}
function buildPunchHintText(definition: GameDefinition, state: GameSessionState, currentTarget: GameControl | null): string {
if (state.status === 'idle') {
@@ -207,6 +233,7 @@ function applyCompletion(definition: GameDefinition, state: GameSessionState, cu
score: getScoringControls(definition).filter((control) => completedControlIds.includes(control.id)).length,
status: nextTarget || !definition.autoFinishOnLastControl ? state.status : 'finished',
endedAt: nextTarget || !definition.autoFinishOnLastControl ? state.endedAt : at,
guidanceState: nextTarget ? 'searching' : 'searching',
}
const effects: GameEffect[] = [buildCompletedEffect(currentTarget)]
@@ -235,6 +262,7 @@ export class ClassicSequentialRule implements RulePlugin {
currentTargetControlId: getInitialTargetId(definition),
inRangeControlId: null,
score: 0,
guidanceState: 'searching',
}
}
@@ -250,6 +278,7 @@ export class ClassicSequentialRule implements RulePlugin {
startedAt: event.at,
endedAt: null,
inRangeControlId: null,
guidanceState: 'searching',
}
return {
nextState,
@@ -263,6 +292,7 @@ export class ClassicSequentialRule implements RulePlugin {
...state,
status: 'finished',
endedAt: event.at,
guidanceState: 'searching',
}
return {
nextState,
@@ -291,19 +321,26 @@ export class ClassicSequentialRule implements RulePlugin {
if (event.type === 'gps_updated') {
const distanceMeters = getApproxDistanceMeters(currentTarget.point, { lon: event.lon, lat: event.lat })
const inRangeControlId = distanceMeters <= definition.punchRadiusMeters ? currentTarget.id : null
const guidanceState = getGuidanceState(definition, distanceMeters)
const nextState: GameSessionState = {
...state,
inRangeControlId,
guidanceState,
}
const guidanceEffects = getGuidanceEffects(state.guidanceState, guidanceState, currentTarget.id)
if (definition.punchPolicy === 'enter' && inRangeControlId === currentTarget.id) {
return applyCompletion(definition, nextState, currentTarget, event.at)
const completionResult = applyCompletion(definition, nextState, currentTarget, event.at)
return {
...completionResult,
effects: [...guidanceEffects, ...completionResult.effects],
}
}
return {
nextState,
presentation: buildPresentation(definition, nextState),
effects: [],
effects: guidanceEffects,
}
}