Add configurable game flow, finish punching, and audio cues
This commit is contained in:
31
miniprogram/game/core/gameDefinition.ts
Normal file
31
miniprogram/game/core/gameDefinition.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { type LonLatPoint } from '../../utils/projection'
|
||||
|
||||
export type GameMode = 'classic-sequential'
|
||||
export type GameControlKind = 'start' | 'control' | 'finish'
|
||||
export type PunchPolicyType = 'enter' | 'enter-confirm'
|
||||
|
||||
export interface GameControlDisplayContent {
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export interface GameControl {
|
||||
id: string
|
||||
code: string
|
||||
label: string
|
||||
kind: GameControlKind
|
||||
point: LonLatPoint
|
||||
sequence: number | null
|
||||
displayContent: GameControlDisplayContent | null
|
||||
}
|
||||
|
||||
export interface GameDefinition {
|
||||
id: string
|
||||
mode: GameMode
|
||||
title: string
|
||||
controlRadiusMeters: number
|
||||
punchRadiusMeters: number
|
||||
punchPolicy: PunchPolicyType
|
||||
controls: GameControl[]
|
||||
autoFinishOnLastControl: boolean
|
||||
}
|
||||
5
miniprogram/game/core/gameEvent.ts
Normal file
5
miniprogram/game/core/gameEvent.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type GameEvent =
|
||||
| { type: 'session_started'; at: number }
|
||||
| { type: 'gps_updated'; at: number; lon: number; lat: number; accuracyMeters: number | null }
|
||||
| { type: 'punch_requested'; at: number }
|
||||
| { type: 'session_ended'; at: number }
|
||||
14
miniprogram/game/core/gameResult.ts
Normal file
14
miniprogram/game/core/gameResult.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type GameSessionState } from './gameSessionState'
|
||||
import { type GamePresentationState } from '../presentation/presentationState'
|
||||
|
||||
export type GameEffect =
|
||||
| { type: 'session_started' }
|
||||
| { type: 'punch_feedback'; text: string; tone: 'neutral' | 'success' | 'warning' }
|
||||
| { type: 'control_completed'; controlId: string; controlKind: 'start' | 'control' | 'finish'; sequence: number | null; label: string; displayTitle: string; displayBody: string }
|
||||
| { type: 'session_finished' }
|
||||
|
||||
export interface GameResult {
|
||||
nextState: GameSessionState
|
||||
presentation: GamePresentationState
|
||||
effects: GameEffect[]
|
||||
}
|
||||
89
miniprogram/game/core/gameRuntime.ts
Normal file
89
miniprogram/game/core/gameRuntime.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { type GameDefinition } from './gameDefinition'
|
||||
import { type GameEvent } from './gameEvent'
|
||||
import { type GameResult } from './gameResult'
|
||||
import { type GameSessionState } from './gameSessionState'
|
||||
import { EMPTY_GAME_PRESENTATION_STATE, type GamePresentationState } from '../presentation/presentationState'
|
||||
import { ClassicSequentialRule } from '../rules/classicSequentialRule'
|
||||
import { type RulePlugin } from '../rules/rulePlugin'
|
||||
|
||||
export class GameRuntime {
|
||||
definition: GameDefinition | null
|
||||
plugin: RulePlugin | null
|
||||
state: GameSessionState | null
|
||||
presentation: GamePresentationState
|
||||
lastResult: GameResult | null
|
||||
|
||||
constructor() {
|
||||
this.definition = null
|
||||
this.plugin = null
|
||||
this.state = null
|
||||
this.presentation = EMPTY_GAME_PRESENTATION_STATE
|
||||
this.lastResult = null
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.definition = null
|
||||
this.plugin = null
|
||||
this.state = null
|
||||
this.presentation = EMPTY_GAME_PRESENTATION_STATE
|
||||
this.lastResult = null
|
||||
}
|
||||
|
||||
loadDefinition(definition: GameDefinition): GameResult {
|
||||
this.definition = definition
|
||||
this.plugin = this.resolvePlugin(definition)
|
||||
this.state = this.plugin.initialize(definition)
|
||||
const result: GameResult = {
|
||||
nextState: this.state,
|
||||
presentation: this.plugin.buildPresentation(definition, this.state),
|
||||
effects: [],
|
||||
}
|
||||
this.presentation = result.presentation
|
||||
this.lastResult = result
|
||||
return result
|
||||
}
|
||||
|
||||
startSession(startAt = Date.now()): GameResult {
|
||||
return this.dispatch({ type: 'session_started', at: startAt })
|
||||
}
|
||||
|
||||
dispatch(event: GameEvent): GameResult {
|
||||
if (!this.definition || !this.plugin || !this.state) {
|
||||
const emptyState: GameSessionState = {
|
||||
status: 'idle',
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
completedControlIds: [],
|
||||
currentTargetControlId: null,
|
||||
inRangeControlId: null,
|
||||
score: 0,
|
||||
}
|
||||
const result: GameResult = {
|
||||
nextState: emptyState,
|
||||
presentation: EMPTY_GAME_PRESENTATION_STATE,
|
||||
effects: [],
|
||||
}
|
||||
this.lastResult = result
|
||||
this.presentation = result.presentation
|
||||
return result
|
||||
}
|
||||
|
||||
const result = this.plugin.reduce(this.definition, this.state, event)
|
||||
this.state = result.nextState
|
||||
this.presentation = result.presentation
|
||||
this.lastResult = result
|
||||
return result
|
||||
}
|
||||
|
||||
getPresentation(): GamePresentationState {
|
||||
return this.presentation
|
||||
}
|
||||
|
||||
resolvePlugin(definition: GameDefinition): RulePlugin {
|
||||
if (definition.mode === 'classic-sequential') {
|
||||
return new ClassicSequentialRule()
|
||||
}
|
||||
|
||||
throw new Error(`未支持的玩法模式: ${definition.mode}`)
|
||||
}
|
||||
}
|
||||
11
miniprogram/game/core/gameSessionState.ts
Normal file
11
miniprogram/game/core/gameSessionState.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type GameSessionStatus = 'idle' | 'running' | 'finished' | 'failed'
|
||||
|
||||
export interface GameSessionState {
|
||||
status: GameSessionStatus
|
||||
startedAt: number | null
|
||||
endedAt: number | null
|
||||
completedControlIds: string[]
|
||||
currentTargetControlId: string | null
|
||||
inRangeControlId: string | null
|
||||
score: number
|
||||
}
|
||||
Reference in New Issue
Block a user