import { buildNpcGiftModalState, buildNpcRecruitModalState, buildNpcTradeModalState, NPC_GIFT_FUNCTION, NPC_RECRUIT_FUNCTION, NPC_TRADE_FUNCTION, shouldNpcRecruitOpenModal, } from '../../data/functionCatalog'; import { applyQuestProgressFromSceneReached, } from '../../data/questFlow'; import { MAX_COMPANIONS } from '../../data/npcInteractions'; import { incrementGameRuntimeStats } from '../../data/runtimeStats'; import { ensureSceneEncounterPreview } from '../../data/sceneEncounterPreviews'; import { getScenePresetById } from '../../data/scenePresets'; import { AnimationState, type Encounter, type GameState, type StoryOption, } from '../../types'; import type { GiftModalState, RecruitModalState, TradeModalState, } from './uiTypes'; export type NpcInteractionDecision = | { kind: 'none' } | { kind: 'trade_modal'; modal: TradeModalState } | { kind: 'gift_modal'; modal: GiftModalState } | { kind: 'recruit_modal'; modal: RecruitModalState } | { kind: 'recruit_immediate' }; export type MapTravelResolution = { nextState: GameState; actionText: string; travelResultText: string; }; function isNpcEncounter( encounter: GameState['currentEncounter'], ): encounter is Encounter { return Boolean(encounter?.kind === 'npc'); } export function getNpcEncounterKey(encounter: Encounter) { return encounter.id ?? encounter.npcName; } function findPreferredTradeItemId( items: Array<{ itemId: string; canSubmit: boolean }>, ) { return items.find(item => item.canSubmit)?.itemId ?? items[0]?.itemId ?? null; } export function resolveNpcInteractionDecision( state: GameState, option: StoryOption, ): NpcInteractionDecision { if ( !state.playerCharacter || !option.interaction || !isNpcEncounter(state.currentEncounter) ) { return { kind: 'none' }; } const encounter = state.currentEncounter; switch (option.functionId) { case NPC_TRADE_FUNCTION.id: return { kind: 'trade_modal', modal: buildNpcTradeModalState( encounter, option.actionText, findPreferredTradeItemId( state.runtimeNpcInteraction?.trade.buyItems ?? [], ), findPreferredTradeItemId( state.runtimeNpcInteraction?.trade.sellItems ?? [], ), ), }; case NPC_GIFT_FUNCTION.id: { const selectedGiftItemId = state.runtimeNpcInteraction?.gift.items.find(item => item.canSubmit) ?.itemId ?? state.runtimeNpcInteraction?.gift.items[0]?.itemId ?? null; if (!selectedGiftItemId) { return { kind: 'none' }; } return { kind: 'gift_modal', modal: buildNpcGiftModalState( encounter, option.actionText, selectedGiftItemId, ), }; } case NPC_RECRUIT_FUNCTION.id: if (shouldNpcRecruitOpenModal(state.companions.length, MAX_COMPANIONS)) { return { kind: 'recruit_modal', modal: buildNpcRecruitModalState(state, encounter, option.actionText), }; } return { kind: 'recruit_immediate' }; default: return { kind: 'none' }; } } export function buildMapTravelResolution( state: GameState, sceneId: string, ): MapTravelResolution | null { if (!state.worldType || !state.playerCharacter) { return null; } const targetScene = getScenePresetById(state.worldType, sceneId); if (!targetScene || targetScene.id === state.currentScenePreset?.id) { return null; } const nextState = ensureSceneEncounterPreview({ ...state, runtimeStats: incrementGameRuntimeStats(state.runtimeStats, { scenesTraveled: 1, }), quests: applyQuestProgressFromSceneReached(state.quests, targetScene.id), currentScenePreset: targetScene, currentEncounter: null, npcInteractionActive: false, sceneHostileNpcs: [], playerX: 0, playerFacing: 'right' as const, animationState: AnimationState.IDLE, playerActionMode: 'idle' as const, activeCombatEffects: [], scrollWorld: false, inBattle: false, lastObserveSignsSceneId: null, lastObserveSignsReport: null, currentBattleNpcId: null, currentNpcBattleMode: null, currentNpcBattleOutcome: null, sparReturnEncounter: null, sparPlayerHpBefore: null, sparPlayerMaxHpBefore: null, sparStoryHistoryBefore: null, }); const travelResultText = `你离开${state.currentScenePreset?.name ?? '当前位置'},前往${targetScene.name}。`; return { nextState, actionText: `前往${targetScene.name}`, travelResultText, }; }