feat: 收敛玩法运行时配置并加入故障恢复
This commit is contained in:
@@ -79,15 +79,23 @@ function getTargetText(control: GameControl): string {
|
||||
}
|
||||
|
||||
function getGuidanceState(definition: GameDefinition, distanceMeters: number): GameSessionState['guidanceState'] {
|
||||
if (distanceMeters <= definition.punchRadiusMeters) {
|
||||
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'
|
||||
}
|
||||
|
||||
const approachDistanceMeters = definition.audioConfig ? definition.audioConfig.approachDistanceMeters : DEFAULT_GAME_AUDIO_CONFIG.approachDistanceMeters
|
||||
if (distanceMeters <= approachDistanceMeters) {
|
||||
return 'approaching'
|
||||
}
|
||||
|
||||
if (distanceMeters <= distantDistanceMeters) {
|
||||
return 'distant'
|
||||
}
|
||||
|
||||
return 'searching'
|
||||
}
|
||||
|
||||
@@ -129,6 +137,29 @@ function buildPunchHintText(definition: GameDefinition, state: GameSessionState,
|
||||
: `${targetText}内,可点击打点`
|
||||
}
|
||||
|
||||
function buildTargetSummaryText(state: GameSessionState, currentTarget: GameControl | null): string {
|
||||
if (state.status === 'finished') {
|
||||
return '本局已完成'
|
||||
}
|
||||
|
||||
if (!currentTarget) {
|
||||
return '等待路线初始化'
|
||||
}
|
||||
|
||||
if (currentTarget.kind === 'start') {
|
||||
return `${currentTarget.label} / 先打开始点`
|
||||
}
|
||||
|
||||
if (currentTarget.kind === 'finish') {
|
||||
return `${currentTarget.label} / 前往终点`
|
||||
}
|
||||
|
||||
const sequenceText = typeof currentTarget.sequence === 'number'
|
||||
? `第 ${currentTarget.sequence} 点`
|
||||
: '当前目标点'
|
||||
return `${sequenceText} / ${currentTarget.label}`
|
||||
}
|
||||
|
||||
function buildSkipFeedbackText(currentTarget: GameControl): string {
|
||||
if (currentTarget.kind === 'start') {
|
||||
return '开始点不可跳过'
|
||||
@@ -193,6 +224,7 @@ function buildPresentation(definition: GameDefinition, state: GameSessionState):
|
||||
const hudPresentation: HudPresentationState = {
|
||||
actionTagText: '目标',
|
||||
distanceTagText: '点距',
|
||||
targetSummaryText: buildTargetSummaryText(state, currentTarget),
|
||||
hudTargetControlId: currentTarget ? currentTarget.id : null,
|
||||
progressText: '0/0',
|
||||
punchButtonText,
|
||||
@@ -200,6 +232,12 @@ function buildPresentation(definition: GameDefinition, state: GameSessionState):
|
||||
punchButtonEnabled,
|
||||
punchHintText: buildPunchHintText(definition, state, currentTarget),
|
||||
}
|
||||
const targetingPresentation = {
|
||||
punchableControlId: punchButtonEnabled && currentTarget ? currentTarget.id : null,
|
||||
guidanceControlId: currentTarget ? currentTarget.id : null,
|
||||
hudControlId: currentTarget ? currentTarget.id : null,
|
||||
highlightedControlId: running && currentTarget ? currentTarget.id : null,
|
||||
}
|
||||
|
||||
if (!scoringControls.length) {
|
||||
return {
|
||||
@@ -223,6 +261,7 @@ function buildPresentation(definition: GameDefinition, state: GameSessionState):
|
||||
skippedControlSequences: [],
|
||||
},
|
||||
hud: hudPresentation,
|
||||
targeting: targetingPresentation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +294,7 @@ function buildPresentation(definition: GameDefinition, state: GameSessionState):
|
||||
...hudPresentation,
|
||||
progressText: `${completedControls.length}/${scoringControls.length}`,
|
||||
},
|
||||
targeting: targetingPresentation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +323,9 @@ function buildCompletedEffect(control: GameControl, punchPolicy: GameDefinition[
|
||||
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',
|
||||
@@ -295,6 +338,7 @@ function buildCompletedEffect(control: GameControl, punchPolicy: GameDefinition[
|
||||
displayAutoPopup: allowAutoPopup,
|
||||
displayOnce: control.displayContent ? control.displayContent.once : false,
|
||||
displayPriority: control.displayContent ? control.displayContent.priority : 1,
|
||||
autoOpenQuiz: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +354,7 @@ function buildCompletedEffect(control: GameControl, punchPolicy: GameDefinition[
|
||||
displayAutoPopup: allowAutoPopup,
|
||||
displayOnce: control.displayContent ? control.displayContent.once : false,
|
||||
displayPriority: control.displayContent ? control.displayContent.priority : 2,
|
||||
autoOpenQuiz: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +373,7 @@ function buildCompletedEffect(control: GameControl, punchPolicy: GameDefinition[
|
||||
displayAutoPopup: allowAutoPopup,
|
||||
displayOnce: control.displayContent ? control.displayContent.once : false,
|
||||
displayPriority: control.displayContent ? control.displayContent.priority : 1,
|
||||
autoOpenQuiz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +386,7 @@ function applyCompletion(definition: GameDefinition, state: GameSessionState, cu
|
||||
const finished = completedFinish || (!nextTarget && definition.autoFinishOnLastControl)
|
||||
const nextState: GameSessionState = {
|
||||
...state,
|
||||
endReason: finished ? 'completed' : state.endReason,
|
||||
startedAt: currentTarget.kind === 'start' && state.startedAt === null ? at : state.startedAt,
|
||||
completedControlIds,
|
||||
skippedControlIds: currentTarget.id === state.currentTargetControlId
|
||||
@@ -410,6 +457,7 @@ export class ClassicSequentialRule implements RulePlugin {
|
||||
initialize(definition: GameDefinition): GameSessionState {
|
||||
return {
|
||||
status: 'idle',
|
||||
endReason: null,
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
completedControlIds: [],
|
||||
@@ -434,6 +482,7 @@ export class ClassicSequentialRule implements RulePlugin {
|
||||
const nextState: GameSessionState = {
|
||||
...state,
|
||||
status: 'running',
|
||||
endReason: null,
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
inRangeControlId: null,
|
||||
@@ -454,6 +503,7 @@ export class ClassicSequentialRule implements RulePlugin {
|
||||
const nextState: GameSessionState = {
|
||||
...state,
|
||||
status: 'finished',
|
||||
endReason: 'completed',
|
||||
endedAt: event.at,
|
||||
guidanceState: 'searching',
|
||||
modeState: {
|
||||
@@ -468,6 +518,25 @@ export class ClassicSequentialRule implements RulePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'session_timed_out') {
|
||||
const nextState: GameSessionState = {
|
||||
...state,
|
||||
status: 'failed',
|
||||
endReason: 'timed_out',
|
||||
endedAt: event.at,
|
||||
guidanceState: 'searching',
|
||||
modeState: {
|
||||
mode: 'classic-sequential',
|
||||
phase: 'done',
|
||||
},
|
||||
}
|
||||
return {
|
||||
nextState,
|
||||
presentation: buildPresentation(definition, nextState),
|
||||
effects: [{ type: 'session_timed_out' }],
|
||||
}
|
||||
}
|
||||
|
||||
if (state.status !== 'running' || !state.currentTargetControlId) {
|
||||
return {
|
||||
nextState: state,
|
||||
|
||||
Reference in New Issue
Block a user