| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934 |
- 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'
- import { type GameSessionState } from '../core/gameSessionState'
- import { type GamePresentationState } from '../presentation/presentationState'
- import { type HudPresentationState } from '../presentation/hudPresentationState'
- import { type MapPresentationState } from '../presentation/mapPresentationState'
- import { type RulePlugin } from './rulePlugin'
- type ScoreOModeState = {
- phase: 'start' | 'controls' | 'finish' | 'done'
- focusedControlId: string | null
- guidanceControlId: string | null
- }
- 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)
- const dy = (b.lat - a.lat) * 110540
- return Math.sqrt(dx * dx + dy * dy)
- }
- function getStartControl(definition: GameDefinition): GameControl | null {
- return definition.controls.find((control) => control.kind === 'start') || null
- }
- function getFinishControl(definition: GameDefinition): GameControl | null {
- return definition.controls.find((control) => control.kind === 'finish') || null
- }
- function getScoreControls(definition: GameDefinition): GameControl[] {
- return definition.controls.filter((control) => control.kind === 'control')
- }
- function getControlScore(control: GameControl): number {
- return control.kind === 'control' && typeof control.score === 'number' ? control.score : 0
- }
- function getRemainingScoreControls(definition: GameDefinition, state: GameSessionState): GameControl[] {
- return getScoreControls(definition).filter((control) => !state.completedControlIds.includes(control.id))
- }
- function getModeState(state: GameSessionState): ScoreOModeState {
- const rawModeState = state.modeState as Partial<ScoreOModeState> | null
- return {
- phase: rawModeState && rawModeState.phase ? rawModeState.phase : 'start',
- focusedControlId: rawModeState && typeof rawModeState.focusedControlId === 'string' ? rawModeState.focusedControlId : null,
- guidanceControlId: rawModeState && typeof rawModeState.guidanceControlId === 'string' ? rawModeState.guidanceControlId : null,
- }
- }
- function withModeState(state: GameSessionState, modeState: ScoreOModeState): GameSessionState {
- return {
- ...state,
- modeState,
- }
- }
- function hasCompletedEnoughControlsForFinish(definition: GameDefinition, state: GameSessionState): boolean {
- const completedScoreControls = getScoreControls(definition)
- .filter((control) => state.completedControlIds.includes(control.id))
- .length
- return completedScoreControls >= definition.minCompletedControlsBeforeFinish
- }
- function canFocusFinish(definition: GameDefinition, state: GameSessionState): boolean {
- const startControl = getStartControl(definition)
- const finishControl = getFinishControl(definition)
- const completedStart = !!startControl && state.completedControlIds.includes(startControl.id)
- const completedFinish = !!finishControl && state.completedControlIds.includes(finishControl.id)
- return completedStart && !completedFinish && hasCompletedEnoughControlsForFinish(definition, state)
- }
- function isFinishPunchAvailable(
- definition: GameDefinition,
- state: GameSessionState,
- _modeState: ScoreOModeState,
- ): boolean {
- return canFocusFinish(definition, state)
- }
- function getNearestRemainingControl(
- definition: GameDefinition,
- state: GameSessionState,
- referencePoint?: LonLatPoint | null,
- ): GameControl | null {
- const remainingControls = getRemainingScoreControls(definition, state)
- if (!remainingControls.length) {
- return getFinishControl(definition)
- }
- if (!referencePoint) {
- return remainingControls[0]
- }
- let nearestControl = remainingControls[0]
- let nearestDistance = getApproxDistanceMeters(referencePoint, nearestControl.point)
- for (let index = 1; index < remainingControls.length; index += 1) {
- const control = remainingControls[index]
- const distance = getApproxDistanceMeters(referencePoint, control.point)
- if (distance < nearestDistance) {
- nearestControl = control
- nearestDistance = distance
- }
- }
- return nearestControl
- }
- function getNearestGuidanceTarget(
- definition: GameDefinition,
- state: GameSessionState,
- modeState: ScoreOModeState,
- referencePoint: LonLatPoint,
- ): GameControl | null {
- const candidates = getRemainingScoreControls(definition, state).slice()
- if (isFinishPunchAvailable(definition, state, modeState)) {
- const finishControl = getFinishControl(definition)
- if (finishControl && !state.completedControlIds.includes(finishControl.id)) {
- candidates.push(finishControl)
- }
- }
- if (!candidates.length) {
- return null
- }
- let nearestControl = candidates[0]
- let nearestDistance = getApproxDistanceMeters(referencePoint, nearestControl.point)
- for (let index = 1; index < candidates.length; index += 1) {
- const control = candidates[index]
- const distance = getApproxDistanceMeters(referencePoint, control.point)
- if (distance < nearestDistance) {
- nearestControl = control
- nearestDistance = distance
- }
- }
- return nearestControl
- }
- function getFocusedTarget(
- definition: GameDefinition,
- state: GameSessionState,
- remainingControls?: GameControl[],
- ): GameControl | null {
- const modeState = getModeState(state)
- if (!modeState.focusedControlId) {
- return null
- }
- const controls = remainingControls || getRemainingScoreControls(definition, state)
- for (const control of controls) {
- if (control.id === modeState.focusedControlId) {
- return control
- }
- }
- const finishControl = getFinishControl(definition)
- if (finishControl && canFocusFinish(definition, state) && finishControl.id === modeState.focusedControlId) {
- return finishControl
- }
- return null
- }
- function resolveInteractiveTarget(
- definition: GameDefinition,
- state: GameSessionState,
- modeState: ScoreOModeState,
- primaryTarget: GameControl | null,
- focusedTarget: GameControl | null,
- ): GameControl | null {
- if (modeState.phase === 'start') {
- return primaryTarget
- }
- if (modeState.phase === 'finish') {
- return primaryTarget
- }
- if (definition.requiresFocusSelection) {
- return focusedTarget
- }
- if (focusedTarget) {
- return focusedTarget
- }
- if (modeState.phase === 'controls' && isFinishPunchAvailable(definition, state, modeState)) {
- return getFinishControl(definition)
- }
- return primaryTarget
- }
- function getNearestInRangeControl(
- controls: GameControl[],
- referencePoint: LonLatPoint,
- radiusMeters: number,
- ): GameControl | null {
- let nearest: GameControl | null = null
- let nearestDistance = Number.POSITIVE_INFINITY
- for (const control of controls) {
- const distance = getApproxDistanceMeters(control.point, referencePoint)
- if (distance > radiusMeters) {
- continue
- }
- if (!nearest || distance < nearestDistance) {
- nearest = control
- nearestDistance = distance
- }
- }
- return nearest
- }
- function getGuidanceState(definition: GameDefinition, distanceMeters: number): GameSessionState['guidanceState'] {
- const audioConfig = definition.audioConfig || DEFAULT_GAME_AUDIO_CONFIG
- const readyDistanceMeters = Math.max(definition.punchRadiusMeters, audioConfig.readyDistanceMeters)
- const approachDistanceMeters = Math.max(readyDistanceMeters, audioConfig.approachDistanceMeters)
- const distantDistanceMeters = Math.max(approachDistanceMeters, audioConfig.distantDistanceMeters)
- if (distanceMeters <= readyDistanceMeters) {
- return 'ready'
- }
- if (distanceMeters <= approachDistanceMeters) {
- return 'approaching'
- }
- if (distanceMeters <= distantDistanceMeters) {
- return 'distant'
- }
- 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 getDisplayTargetLabel(control: GameControl | null): string {
- if (!control) {
- return '目标点'
- }
- if (control.kind === 'start') {
- return '开始点'
- }
- if (control.kind === 'finish') {
- return '终点'
- }
- return '目标点'
- }
- function buildPunchHintText(
- definition: GameDefinition,
- state: GameSessionState,
- primaryTarget: GameControl | null,
- focusedTarget: GameControl | null,
- ): string {
- if (state.status === 'idle') {
- return '点击开始后先打开始点'
- }
- if (state.status === 'finished') {
- return '本局已完成'
- }
- const modeState = getModeState(state)
- if (modeState.phase === 'controls' || modeState.phase === 'finish') {
- if (modeState.phase === 'controls' && definition.requiresFocusSelection && !focusedTarget) {
- return '点击地图选中一个目标点'
- }
- const displayTarget = resolveInteractiveTarget(definition, state, modeState, primaryTarget, focusedTarget)
- const targetLabel = getDisplayTargetLabel(displayTarget)
- if (displayTarget && state.inRangeControlId === displayTarget.id) {
- return definition.punchPolicy === 'enter'
- ? `${targetLabel}内,自动打点中`
- : `${targetLabel}内,可点击打点`
- }
- return definition.punchPolicy === 'enter'
- ? `进入${targetLabel}自动打点`
- : `进入${targetLabel}后点击打点`
- }
- const targetLabel = getDisplayTargetLabel(primaryTarget)
- if (state.inRangeControlId && primaryTarget && state.inRangeControlId === primaryTarget.id) {
- return definition.punchPolicy === 'enter'
- ? `${targetLabel}内,自动打点中`
- : `${targetLabel}内,可点击打点`
- }
- return definition.punchPolicy === 'enter'
- ? `进入${targetLabel}自动打点`
- : `进入${targetLabel}后点击打点`
- }
- function buildTargetSummaryText(
- definition: GameDefinition,
- state: GameSessionState,
- primaryTarget: GameControl | null,
- focusedTarget: GameControl | null,
- ): string {
- if (state.status === 'idle') {
- return primaryTarget ? `${primaryTarget.label} / 先打开始点` : '先打开始点'
- }
- if (state.status === 'finished') {
- return '本局已完成'
- }
- const modeState = getModeState(state)
- if (modeState.phase === 'start') {
- return primaryTarget ? `${primaryTarget.label} / 先打开始点` : '先打开始点'
- }
- if (modeState.phase === 'finish') {
- return primaryTarget ? `${primaryTarget.label} / 可随时结束` : '可前往终点结束'
- }
- if (focusedTarget && focusedTarget.kind === 'control') {
- return `${focusedTarget.label} / ${getControlScore(focusedTarget)} 分目标`
- }
- if (focusedTarget && focusedTarget.kind === 'finish') {
- return `${focusedTarget.label} / 结束比赛`
- }
- if (definition.requiresFocusSelection) {
- return '请选择目标点'
- }
- if (primaryTarget && primaryTarget.kind === 'control') {
- return `${primaryTarget.label} / ${getControlScore(primaryTarget)} 分目标`
- }
- return primaryTarget ? primaryTarget.label : '自由打点'
- }
- function buildCompletedEffect(control: GameControl, punchPolicy: GameDefinition['punchPolicy']): GameEffect {
- const allowAutoPopup = punchPolicy === 'enter'
- ? false
- : (control.displayContent ? control.displayContent.autoPopup : true)
- const autoOpenQuiz = control.kind === 'control'
- && !!control.displayContent
- && control.displayContent.ctas.some((item) => item.type === 'quiz')
- if (control.kind === 'start') {
- return {
- type: 'control_completed',
- controlId: control.id,
- controlKind: 'start',
- sequence: null,
- label: control.label,
- displayTitle: control.displayContent ? control.displayContent.title : '比赛开始',
- displayBody: control.displayContent ? control.displayContent.body : '已完成开始点打卡,开始自由打点。',
- displayAutoPopup: allowAutoPopup,
- displayOnce: control.displayContent ? control.displayContent.once : false,
- displayPriority: control.displayContent ? control.displayContent.priority : 1,
- autoOpenQuiz: false,
- }
- }
- if (control.kind === 'finish') {
- return {
- type: 'control_completed',
- controlId: control.id,
- controlKind: 'finish',
- sequence: null,
- label: control.label,
- displayTitle: control.displayContent ? control.displayContent.title : '比赛结束',
- displayBody: control.displayContent ? control.displayContent.body : '已完成终点打卡,本局结束。',
- displayAutoPopup: allowAutoPopup,
- displayOnce: control.displayContent ? control.displayContent.once : false,
- displayPriority: control.displayContent ? control.displayContent.priority : 2,
- autoOpenQuiz: false,
- }
- }
- const sequenceText = typeof control.sequence === 'number' ? String(control.sequence) : control.label
- return {
- type: 'control_completed',
- controlId: control.id,
- controlKind: 'control',
- sequence: control.sequence,
- label: control.label,
- displayTitle: control.displayContent ? control.displayContent.title : `收集 ${sequenceText}`,
- displayBody: control.displayContent ? control.displayContent.body : control.label,
- displayAutoPopup: allowAutoPopup,
- displayOnce: control.displayContent ? control.displayContent.once : false,
- displayPriority: control.displayContent ? control.displayContent.priority : 1,
- autoOpenQuiz,
- }
- }
- function resolvePunchableControl(
- definition: GameDefinition,
- state: GameSessionState,
- modeState: ScoreOModeState,
- focusedTarget: GameControl | null,
- ): GameControl | null {
- if (!state.inRangeControlId) {
- return null
- }
- const inRangeControl = definition.controls.find((control) => control.id === state.inRangeControlId) || null
- if (!inRangeControl) {
- return null
- }
- if (modeState.phase === 'start') {
- return inRangeControl.kind === 'start' ? inRangeControl : null
- }
- if (modeState.phase === 'finish') {
- return inRangeControl.kind === 'finish' ? inRangeControl : null
- }
- if (modeState.phase === 'controls') {
- if (inRangeControl.kind === 'finish' && isFinishPunchAvailable(definition, state, modeState)) {
- return inRangeControl
- }
- if (definition.requiresFocusSelection) {
- return focusedTarget && inRangeControl.id === focusedTarget.id ? inRangeControl : null
- }
- if (inRangeControl.kind === 'control') {
- return inRangeControl
- }
- }
- return null
- }
- function buildPresentation(definition: GameDefinition, state: GameSessionState): GamePresentationState {
- const modeState = getModeState(state)
- const running = state.status === 'running'
- const startControl = getStartControl(definition)
- const finishControl = getFinishControl(definition)
- const completedStart = !!startControl && state.completedControlIds.includes(startControl.id)
- const completedFinish = !!finishControl && state.completedControlIds.includes(finishControl.id)
- const remainingControls = getRemainingScoreControls(definition, state)
- const scoreControls = getScoreControls(definition)
- const primaryTarget = definition.controls.find((control) => control.id === state.currentTargetControlId) || null
- const focusedTarget = getFocusedTarget(definition, state, remainingControls)
- const canSelectFinish = running && completedStart && !completedFinish && !!finishControl
- const activeControlIds = running && modeState.phase === 'controls'
- ? remainingControls.map((control) => control.id)
- : []
- const activeControlSequences = running && modeState.phase === 'controls'
- ? remainingControls
- .filter((control) => typeof control.sequence === 'number')
- .map((control) => control.sequence as number)
- : []
- const completedControls = scoreControls.filter((control) => state.completedControlIds.includes(control.id))
- const completedControlSequences = completedControls
- .filter((control) => typeof control.sequence === 'number')
- .map((control) => control.sequence as number)
- const revealFullCourse = completedStart
- const punchableControl = resolvePunchableControl(definition, state, modeState, focusedTarget)
- const guidanceControl = modeState.guidanceControlId
- ? definition.controls.find((control) => control.id === modeState.guidanceControlId) || null
- : null
- const punchButtonEnabled = running
- && definition.punchPolicy === 'enter-confirm'
- && !!punchableControl
- const hudTargetControlId = modeState.phase === 'finish'
- ? (primaryTarget ? primaryTarget.id : null)
- : focusedTarget
- ? focusedTarget.id
- : modeState.phase === 'controls' && isFinishPunchAvailable(definition, state, modeState)
- ? (getFinishControl(definition) ? getFinishControl(definition)!.id : null)
- : definition.requiresFocusSelection
- ? null
- : primaryTarget
- ? primaryTarget.id
- : null
- const highlightedControlId = focusedTarget
- ? focusedTarget.id
- : punchableControl
- ? punchableControl.id
- : guidanceControl
- ? guidanceControl.id
- : null
- const showMultiTargetLabels = completedStart && modeState.phase !== 'start'
- const mapPresentation: MapPresentationState = {
- controlVisualMode: showMultiTargetLabels ? 'multi-target' : 'single-target',
- showCourseLegs: false,
- guidanceLegAnimationEnabled: false,
- focusableControlIds: canSelectFinish
- ? [...activeControlIds, finishControl!.id]
- : activeControlIds.slice(),
- focusedControlId: focusedTarget ? focusedTarget.id : null,
- focusedControlSequences: focusedTarget && focusedTarget.kind === 'control' && typeof focusedTarget.sequence === 'number'
- ? [focusedTarget.sequence]
- : [],
- activeControlIds,
- activeControlSequences,
- activeStart: running && modeState.phase === 'start',
- completedStart,
- activeFinish: running && (modeState.phase === 'finish' || (modeState.phase === 'controls' && isFinishPunchAvailable(definition, state, modeState))),
- focusedFinish: !!focusedTarget && focusedTarget.kind === 'finish',
- completedFinish,
- revealFullCourse,
- activeLegIndices: [],
- completedLegIndices: [],
- completedControlIds: completedControls.map((control) => control.id),
- completedControlSequences,
- skippedControlIds: [],
- skippedControlSequences: [],
- }
- const hudPresentation: HudPresentationState = {
- actionTagText: modeState.phase === 'start'
- ? '目标'
- : modeState.phase === 'finish'
- ? '终点'
- : focusedTarget && focusedTarget.kind === 'finish'
- ? '终点'
- : focusedTarget
- ? '目标'
- : '自由',
- distanceTagText: modeState.phase === 'start'
- ? '点距'
- : modeState.phase === 'finish'
- ? '终点距'
- : focusedTarget && focusedTarget.kind === 'finish'
- ? '终点距'
- : focusedTarget
- ? '选中点距'
- : '目标距',
- targetSummaryText: buildTargetSummaryText(definition, state, primaryTarget, focusedTarget),
- hudTargetControlId,
- progressText: `已收集 ${completedControls.length}/${scoreControls.length}`,
- punchableControlId: punchableControl ? punchableControl.id : null,
- punchButtonEnabled,
- punchButtonText: modeState.phase === 'start'
- ? '开始打卡'
- : (punchableControl && punchableControl.kind === 'finish')
- ? '结束打卡'
- : modeState.phase === 'finish'
- ? '结束打卡'
- : focusedTarget && focusedTarget.kind === 'finish'
- ? '结束打卡'
- : '打点',
- punchHintText: buildPunchHintText(definition, state, primaryTarget, focusedTarget),
- }
- return {
- map: mapPresentation,
- hud: hudPresentation,
- targeting: {
- punchableControlId: punchableControl ? punchableControl.id : null,
- guidanceControlId: guidanceControl ? guidanceControl.id : null,
- hudControlId: hudTargetControlId,
- highlightedControlId,
- },
- }
- }
- function applyCompletion(
- definition: GameDefinition,
- state: GameSessionState,
- control: GameControl,
- at: number,
- referencePoint: LonLatPoint | null,
- ): GameResult {
- const completedControlIds = state.completedControlIds.includes(control.id)
- ? state.completedControlIds
- : [...state.completedControlIds, control.id]
- const previousModeState = getModeState(state)
- const nextStateDraft: GameSessionState = {
- ...state,
- endReason: control.kind === 'finish' ? 'completed' : state.endReason,
- startedAt: control.kind === 'start' && state.startedAt === null ? at : state.startedAt,
- endedAt: control.kind === 'finish' ? at : state.endedAt,
- completedControlIds,
- currentTargetControlId: null,
- inRangeControlId: null,
- score: getScoreControls(definition)
- .filter((item) => completedControlIds.includes(item.id))
- .reduce((sum, item) => sum + getControlScore(item), 0),
- status: control.kind === 'finish' ? 'finished' : state.status,
- guidanceState: 'searching',
- }
- const remainingControls = getRemainingScoreControls(definition, nextStateDraft)
- let phase: ScoreOModeState['phase']
- if (control.kind === 'finish') {
- phase = 'done'
- } else if (control.kind === 'start') {
- phase = remainingControls.length ? 'controls' : 'finish'
- } else {
- phase = remainingControls.length ? 'controls' : 'finish'
- }
- const nextPrimaryTarget = phase === 'controls'
- ? getNearestRemainingControl(definition, nextStateDraft, referencePoint)
- : phase === 'finish'
- ? getFinishControl(definition)
- : null
- const nextModeState: ScoreOModeState = {
- phase,
- focusedControlId: control.id === previousModeState.focusedControlId ? null : previousModeState.focusedControlId,
- guidanceControlId: nextPrimaryTarget ? nextPrimaryTarget.id : null,
- }
- const nextState = withModeState({
- ...nextStateDraft,
- currentTargetControlId: nextPrimaryTarget ? nextPrimaryTarget.id : null,
- }, nextModeState)
- const effects: GameEffect[] = [buildCompletedEffect(control, definition.punchPolicy)]
- if (control.kind === 'finish') {
- effects.push({ type: 'session_finished' })
- }
- return {
- nextState,
- presentation: buildPresentation(definition, nextState),
- effects,
- }
- }
- export class ScoreORule implements RulePlugin {
- get mode(): 'score-o' {
- return 'score-o'
- }
- initialize(definition: GameDefinition): GameSessionState {
- const startControl = getStartControl(definition)
- return {
- status: 'idle',
- endReason: null,
- startedAt: null,
- endedAt: null,
- completedControlIds: [],
- skippedControlIds: [],
- currentTargetControlId: startControl ? startControl.id : null,
- inRangeControlId: null,
- score: 0,
- guidanceState: 'searching',
- modeState: {
- phase: 'start',
- focusedControlId: null,
- guidanceControlId: startControl ? startControl.id : null,
- },
- }
- }
- buildPresentation(definition: GameDefinition, state: GameSessionState): GamePresentationState {
- return buildPresentation(definition, state)
- }
- reduce(definition: GameDefinition, state: GameSessionState, event: GameEvent): GameResult {
- if (event.type === 'session_started') {
- const startControl = getStartControl(definition)
- const nextState = withModeState({
- ...state,
- status: 'running',
- endReason: null,
- startedAt: null,
- endedAt: null,
- currentTargetControlId: startControl ? startControl.id : null,
- inRangeControlId: null,
- guidanceState: 'searching',
- }, {
- phase: 'start',
- focusedControlId: null,
- guidanceControlId: startControl ? startControl.id : null,
- })
- return {
- nextState,
- presentation: buildPresentation(definition, nextState),
- effects: [{ type: 'session_started' }],
- }
- }
- if (event.type === 'session_ended') {
- const nextState = withModeState({
- ...state,
- status: 'finished',
- endReason: 'completed',
- endedAt: event.at,
- guidanceState: 'searching',
- }, {
- phase: 'done',
- focusedControlId: null,
- guidanceControlId: null,
- })
- return {
- nextState,
- presentation: buildPresentation(definition, nextState),
- effects: [{ type: 'session_finished' }],
- }
- }
- if (event.type === 'session_timed_out') {
- const nextState = withModeState({
- ...state,
- status: 'failed',
- endReason: 'timed_out',
- endedAt: event.at,
- guidanceState: 'searching',
- }, {
- phase: 'done',
- focusedControlId: null,
- guidanceControlId: null,
- })
- return {
- nextState,
- presentation: buildPresentation(definition, nextState),
- effects: [{ type: 'session_timed_out' }],
- }
- }
- if (state.status !== 'running') {
- return {
- nextState: state,
- presentation: buildPresentation(definition, state),
- effects: [],
- }
- }
- const modeState = getModeState(state)
- const targetControl = state.currentTargetControlId
- ? definition.controls.find((control) => control.id === state.currentTargetControlId) || null
- : null
- if (event.type === 'gps_updated') {
- const referencePoint = { lon: event.lon, lat: event.lat }
- const remainingControls = getRemainingScoreControls(definition, state)
- const nextStateBase = withModeState(state, modeState)
- const focusedTarget = getFocusedTarget(definition, nextStateBase, remainingControls)
- let nextPrimaryTarget = targetControl
- let guidanceTarget = targetControl
- let punchTarget: GameControl | null = null
- if (modeState.phase === 'controls') {
- nextPrimaryTarget = getNearestRemainingControl(definition, state, referencePoint)
- guidanceTarget = getNearestGuidanceTarget(definition, state, modeState, referencePoint)
- if (focusedTarget && getApproxDistanceMeters(focusedTarget.point, referencePoint) <= definition.punchRadiusMeters) {
- punchTarget = focusedTarget
- } else if (isFinishPunchAvailable(definition, state, modeState)) {
- const finishControl = getFinishControl(definition)
- if (finishControl && getApproxDistanceMeters(finishControl.point, referencePoint) <= definition.punchRadiusMeters) {
- punchTarget = finishControl
- }
- }
- if (!punchTarget && !definition.requiresFocusSelection) {
- punchTarget = getNearestInRangeControl(remainingControls, referencePoint, definition.punchRadiusMeters)
- }
- } else if (modeState.phase === 'finish') {
- nextPrimaryTarget = getFinishControl(definition)
- guidanceTarget = nextPrimaryTarget
- if (nextPrimaryTarget && getApproxDistanceMeters(nextPrimaryTarget.point, referencePoint) <= definition.punchRadiusMeters) {
- punchTarget = nextPrimaryTarget
- }
- } else if (targetControl) {
- guidanceTarget = targetControl
- if (getApproxDistanceMeters(targetControl.point, referencePoint) <= definition.punchRadiusMeters) {
- punchTarget = targetControl
- }
- }
- const guidanceState = guidanceTarget
- ? getGuidanceState(definition, getApproxDistanceMeters(guidanceTarget.point, referencePoint))
- : 'searching'
- const nextState: GameSessionState = {
- ...nextStateBase,
- currentTargetControlId: nextPrimaryTarget ? nextPrimaryTarget.id : null,
- inRangeControlId: punchTarget ? punchTarget.id : null,
- guidanceState,
- }
- const nextStateWithMode = withModeState(nextState, {
- ...modeState,
- guidanceControlId: guidanceTarget ? guidanceTarget.id : null,
- })
- const guidanceEffects = getGuidanceEffects(state.guidanceState, guidanceState, guidanceTarget ? guidanceTarget.id : null)
- if (definition.punchPolicy === 'enter' && punchTarget) {
- const completionResult = applyCompletion(definition, nextStateWithMode, punchTarget, event.at, referencePoint)
- return {
- ...completionResult,
- effects: [...guidanceEffects, ...completionResult.effects],
- }
- }
- return {
- nextState: nextStateWithMode,
- presentation: buildPresentation(definition, nextStateWithMode),
- effects: guidanceEffects,
- }
- }
- if (event.type === 'control_focused') {
- if (modeState.phase !== 'controls' && modeState.phase !== 'finish') {
- return {
- nextState: state,
- presentation: buildPresentation(definition, state),
- effects: [],
- }
- }
- const focusableControlIds = getRemainingScoreControls(definition, state).map((control) => control.id)
- const finishControl = getFinishControl(definition)
- if (finishControl && canFocusFinish(definition, state)) {
- focusableControlIds.push(finishControl.id)
- }
- const nextFocusedControlId = event.controlId && focusableControlIds.includes(event.controlId)
- ? modeState.focusedControlId === event.controlId
- ? null
- : event.controlId
- : null
- const nextState = withModeState({
- ...state,
- }, {
- ...modeState,
- focusedControlId: nextFocusedControlId,
- guidanceControlId: modeState.guidanceControlId,
- })
- return {
- nextState,
- presentation: buildPresentation(definition, nextState),
- effects: [],
- }
- }
- if (event.type === 'punch_requested') {
- const focusedTarget = getFocusedTarget(definition, state)
- let stateForPunch = state
- const finishControl = getFinishControl(definition)
- const finishInRange = !!(
- finishControl
- && event.lon !== null
- && event.lat !== null
- && getApproxDistanceMeters(finishControl.point, { lon: event.lon, lat: event.lat }) <= definition.punchRadiusMeters
- )
- if (definition.requiresFocusSelection && modeState.phase === 'controls' && !focusedTarget && !finishInRange) {
- return {
- nextState: state,
- presentation: buildPresentation(definition, state),
- effects: [{ type: 'punch_feedback', text: '请先选中目标点', tone: 'warning' }],
- }
- }
- let controlToPunch: GameControl | null = null
- if (state.inRangeControlId) {
- controlToPunch = definition.controls.find((control) => control.id === state.inRangeControlId) || null
- }
- if (!controlToPunch && event.lon !== null && event.lat !== null) {
- const referencePoint = { lon: event.lon, lat: event.lat }
- const nextStateBase = withModeState(state, modeState)
- stateForPunch = nextStateBase
- const remainingControls = getRemainingScoreControls(definition, state)
- const resolvedFocusedTarget = getFocusedTarget(definition, nextStateBase, remainingControls)
- if (resolvedFocusedTarget && getApproxDistanceMeters(resolvedFocusedTarget.point, referencePoint) <= definition.punchRadiusMeters) {
- controlToPunch = resolvedFocusedTarget
- } else if (isFinishPunchAvailable(definition, state, modeState)) {
- const finishControl = getFinishControl(definition)
- if (finishControl && getApproxDistanceMeters(finishControl.point, referencePoint) <= definition.punchRadiusMeters) {
- controlToPunch = finishControl
- }
- }
- if (!controlToPunch && !definition.requiresFocusSelection && modeState.phase === 'controls') {
- controlToPunch = getNearestInRangeControl(remainingControls, referencePoint, definition.punchRadiusMeters)
- }
- }
- if (!controlToPunch || (definition.requiresFocusSelection && modeState.phase === 'controls' && focusedTarget && controlToPunch.id !== focusedTarget.id)) {
- const isFinishLockedAttempt = !!(
- finishControl
- && event.lon !== null
- && event.lat !== null
- && getApproxDistanceMeters(finishControl.point, { lon: event.lon, lat: event.lat }) <= definition.punchRadiusMeters
- && !hasCompletedEnoughControlsForFinish(definition, state)
- )
- return {
- nextState: state,
- presentation: buildPresentation(definition, state),
- effects: [{
- type: 'punch_feedback',
- text: isFinishLockedAttempt
- ? `至少完成 ${definition.minCompletedControlsBeforeFinish} 个积分点后才能结束`
- : focusedTarget
- ? `未进入${getDisplayTargetLabel(focusedTarget)}打卡范围`
- : modeState.phase === 'start'
- ? '未进入开始点打卡范围'
- : '未进入目标打点范围',
- tone: 'warning',
- }],
- }
- }
- return applyCompletion(definition, stateForPunch, controlToPunch, event.at, this.getReferencePoint(definition, stateForPunch, controlToPunch))
- }
- return {
- nextState: state,
- presentation: buildPresentation(definition, state),
- effects: [],
- }
- }
- private getReferencePoint(definition: GameDefinition, state: GameSessionState, completedControl: GameControl): LonLatPoint | null {
- if (completedControl.kind === 'control') {
- const remaining = getRemainingScoreControls(definition, {
- ...state,
- completedControlIds: [...state.completedControlIds, completedControl.id],
- })
- return remaining.length ? completedControl.point : (getFinishControl(definition) ? getFinishControl(definition)!.point : completedControl.point)
- }
- return completedControl.point
- }
- }
|