169 lines
4.6 KiB
TypeScript
169 lines
4.6 KiB
TypeScript
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,
|
|
};
|
|
}
|