Files
Genarrative/src/hooks/combat/resolvedChoice.ts
2026-04-27 22:50:18 +08:00

135 lines
3.8 KiB
TypeScript

import {
getForwardScenePreset,
getScenePresetById,
getScenePresetsByWorld,
getTravelScenePreset,
} from '../../data/scenePresets';
import { getFunctionEffect } from '../../data/stateFunctions';
import type {
Character,
GameState,
StoryOption,
WorldType,
} from '../../types';
import { classifyCombatOption } from '../combatStoryUtils';
import { buildIdleAfterSequence } from '../idleAdventureFlow';
import {
applyRecoveryEffectToState,
type BattlePlan,
} from './battlePlan';
import { buildEscapeAfterSequence } from './escapeFlow';
type OptionKind = 'battle' | 'escape' | 'idle';
export type ResolvedChoiceState = {
optionKind: OptionKind;
battlePlan: BattlePlan | null;
afterSequence: GameState;
};
function getShiftedScenePreset(
worldType: WorldType | null,
currentScenePreset: GameState['currentScenePreset'],
sceneShift: number | undefined,
): GameState['currentScenePreset'] {
if (!worldType || sceneShift === undefined || sceneShift === 0) return currentScenePreset;
if (!currentScenePreset) return getScenePresetsByWorld(worldType)[0] ?? null;
if (sceneShift > 0) {
let nextScene = currentScenePreset;
for (let index = 0; index < sceneShift; index += 1) {
nextScene = getForwardScenePreset(worldType, nextScene.id) ?? nextScene;
}
return nextScene;
}
const scenes = getScenePresetsByWorld(worldType);
if (scenes.length === 0) return currentScenePreset;
const currentIndex = scenes.findIndex(scene => scene.id === currentScenePreset.id);
const safeIndex = currentIndex >= 0 ? currentIndex : 0;
const nextIndex = ((safeIndex + sceneShift) % scenes.length + scenes.length) % scenes.length;
return scenes[nextIndex] ?? null;
}
function getSceneTargetForFunction(
worldType: WorldType | null,
currentScenePreset: GameState['currentScenePreset'],
option: StoryOption,
): GameState['currentScenePreset'] {
if (!worldType) return currentScenePreset;
const targetSceneId =
typeof option.runtimePayload?.targetSceneId === 'string'
? option.runtimePayload.targetSceneId
: typeof option.runtimePayload?.escapeTargetSceneId === 'string'
? option.runtimePayload.escapeTargetSceneId
: null;
if (targetSceneId) {
return getScenePresetById(worldType, targetSceneId) ?? currentScenePreset;
}
if (option.functionId === 'idle_travel_next_scene') {
return getTravelScenePreset(worldType, currentScenePreset?.id) ?? currentScenePreset;
}
return getShiftedScenePreset(
worldType,
currentScenePreset,
getFunctionEffect(option.functionId).sceneShift,
);
}
export function buildResolvedChoiceState(params: {
state: GameState;
option: StoryOption;
character: Character;
buildBattlePlan: (state: GameState, option: StoryOption, character: Character) => BattlePlan;
}) {
const {
state,
option,
character,
buildBattlePlan,
} = params;
const nextScenePreset = getSceneTargetForFunction(
state.worldType,
state.currentScenePreset,
option,
);
const optionKind = classifyCombatOption(option);
if (optionKind === 'battle') {
const battlePlan = buildBattlePlan(state, option, character);
const battleResult: GameState = {
...battlePlan.finalState,
currentScenePreset: nextScenePreset ?? battlePlan.finalState.currentScenePreset,
};
return {
optionKind,
battlePlan,
afterSequence: battleResult,
} satisfies ResolvedChoiceState;
}
if (optionKind === 'escape') {
return {
optionKind,
battlePlan: null,
afterSequence: buildEscapeAfterSequence(state, option, nextScenePreset),
} satisfies ResolvedChoiceState;
}
return {
optionKind,
battlePlan: null,
afterSequence: buildIdleAfterSequence({
state,
option,
character,
nextScenePreset,
applyRecoveryEffectToState,
}),
} satisfies ResolvedChoiceState;
}