Files
Genarrative/src/hooks/rpg-runtime-story/storyGenerationState.ts
2026-04-28 19:36:39 +08:00

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,
};
}