1
This commit is contained in:
@@ -17,15 +17,18 @@ import {
|
||||
applyQuestProgressFromSpar,
|
||||
} from '../../data/questFlow';
|
||||
import { incrementGameRuntimeStats } from '../../data/runtimeStats';
|
||||
import { getScenePresetById } from '../../data/scenePresets';
|
||||
import { resolveFunctionOption } from '../../data/stateFunctions';
|
||||
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
|
||||
import { streamNpcChatTurn } from '../../services/aiService';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import {
|
||||
advanceSceneActRuntimeState,
|
||||
getSceneConnectionDirectionText,
|
||||
resolveLimitedPrimaryNpcChatState,
|
||||
resolveSceneActProgression,
|
||||
} from '../../services/customWorldSceneActRuntime';
|
||||
import { appendStoryEngineCarrierMemory } from '../../services/storyEngine/echoMemory';
|
||||
import { createHistoryMoment } from '../../services/storyHistory';
|
||||
import { createEmptyStoryEngineMemoryState } from '../../services/storyEngine/visibilityEngine';
|
||||
import type {
|
||||
Character,
|
||||
Encounter,
|
||||
@@ -50,15 +53,6 @@ type CommitGeneratedStateWithEncounterEntry = (
|
||||
lastFunctionId?: string,
|
||||
) => Promise<void> | void;
|
||||
|
||||
type GenerateStoryForState = (params: {
|
||||
state: GameState;
|
||||
character: Character;
|
||||
history: StoryMoment[];
|
||||
choice?: string;
|
||||
lastFunctionId?: string | null;
|
||||
optionCatalog?: StoryOption[] | null;
|
||||
}) => Promise<StoryMoment>;
|
||||
|
||||
type NpcInteractionFlowActions = {
|
||||
openTradeModal: (encounter: Encounter, actionText: string) => void;
|
||||
openGiftModal: (encounter: Encounter, actionText: string) => void;
|
||||
@@ -82,6 +76,16 @@ type NpcChatDirective = {
|
||||
remainingTurns?: number | null;
|
||||
limitReason?: 'negative_affinity' | null;
|
||||
forceExitAfterTurn?: boolean;
|
||||
closingMode?: 'free' | 'foreshadow_close' | null;
|
||||
terminationMode?: 'none' | 'hostile_model' | null;
|
||||
terminationReason?: 'hostile_breakoff' | 'player_exit' | null;
|
||||
isHostileChat?: boolean;
|
||||
functionOptions?: Array<{
|
||||
functionId: string;
|
||||
actionText: string;
|
||||
detailText?: string | null;
|
||||
action?: string | null;
|
||||
}>;
|
||||
} | null;
|
||||
|
||||
type NpcChatCombatContext = NonNullable<
|
||||
@@ -119,11 +123,9 @@ export function createStoryNpcEncounterActions({
|
||||
buildOpeningCampChatContext,
|
||||
buildStoryContextFromState,
|
||||
buildFallbackStoryForState,
|
||||
generateStoryForState,
|
||||
getStoryGenerationHostileNpcs,
|
||||
getAvailableOptionsForState,
|
||||
buildContinueAdventureOption,
|
||||
getNpcEncounterKey,
|
||||
getResolvedNpcState,
|
||||
updateNpcState,
|
||||
cloneInventoryItemForOwner,
|
||||
@@ -169,7 +171,6 @@ export function createStoryNpcEncounterActions({
|
||||
options: StoryOption[],
|
||||
streaming?: boolean,
|
||||
) => StoryMoment;
|
||||
generateStoryForState: GenerateStoryForState;
|
||||
getStoryGenerationHostileNpcs: (
|
||||
state: GameState,
|
||||
) => GameState['sceneHostileNpcs'];
|
||||
@@ -185,7 +186,6 @@ export function createStoryNpcEncounterActions({
|
||||
) => StoryOption[];
|
||||
sortOptions: (options: StoryOption[]) => StoryOption[];
|
||||
buildContinueAdventureOption: () => StoryOption;
|
||||
getNpcEncounterKey: (encounter: Encounter) => string;
|
||||
getResolvedNpcState: (
|
||||
state: GameState,
|
||||
encounter: Encounter,
|
||||
@@ -315,13 +315,6 @@ export function createStoryNpcEncounterActions({
|
||||
}`;
|
||||
};
|
||||
|
||||
const buildPostQuestOfferChatSuggestions = (encounter: Encounter) =>
|
||||
[
|
||||
'那先继续聊聊你刚才没说完的部分',
|
||||
'除了委托,你对眼前局势还有什么判断',
|
||||
'先把这附近真正危险的地方说清楚',
|
||||
].map((actionText) => buildNpcChatOption(encounter, actionText));
|
||||
|
||||
const extractRecentCombatLogLines = (history: GameState['storyHistory']) =>
|
||||
history
|
||||
.slice(-6)
|
||||
@@ -365,12 +358,6 @@ export function createStoryNpcEncounterActions({
|
||||
}
|
||||
|
||||
const reopenedNpcState = getResolvedNpcState(params.nextState, params.encounter);
|
||||
const chatDirective = resolveLimitedPrimaryNpcChatState({
|
||||
state: params.nextState,
|
||||
npcId: params.encounter.id ?? params.encounter.npcName,
|
||||
affinity: reopenedNpcState.affinity,
|
||||
nextTurnCount: 0,
|
||||
});
|
||||
const baseStory = buildNpcStory(
|
||||
params.nextState,
|
||||
playerCharacter,
|
||||
@@ -389,6 +376,16 @@ export function createStoryNpcEncounterActions({
|
||||
actionText: params.actionText,
|
||||
historyBase: params.nextState.storyHistory,
|
||||
});
|
||||
const chatDirective = toNpcChatDirectiveWithFunctionOptions(
|
||||
resolveLimitedPrimaryNpcChatState({
|
||||
state: params.nextState,
|
||||
npcId: params.encounter.id ?? params.encounter.npcName,
|
||||
affinity: reopenedNpcState.affinity,
|
||||
nextTurnCount: 0,
|
||||
}),
|
||||
params.encounter,
|
||||
playerCharacter,
|
||||
);
|
||||
|
||||
setCurrentStory(
|
||||
buildNpcChatStoryMoment({
|
||||
@@ -591,6 +588,29 @@ export function createStoryNpcEncounterActions({
|
||||
},
|
||||
}));
|
||||
|
||||
const cloneNpcChatFunctionOption = (option: StoryOption): StoryOption => ({
|
||||
...option,
|
||||
visuals: {
|
||||
...option.visuals,
|
||||
monsterChanges: option.visuals.monsterChanges.map((change) => ({
|
||||
...change,
|
||||
})),
|
||||
},
|
||||
interaction: option.interaction ? { ...option.interaction } : undefined,
|
||||
runtimePayload: option.runtimePayload
|
||||
? { ...option.runtimePayload }
|
||||
: option.runtimePayload,
|
||||
});
|
||||
|
||||
const rewriteNpcChatFunctionOption = (
|
||||
option: StoryOption,
|
||||
actionText: string,
|
||||
): StoryOption => ({
|
||||
...cloneNpcChatFunctionOption(option),
|
||||
actionText,
|
||||
text: actionText,
|
||||
});
|
||||
|
||||
const NPC_CHAT_SUGGESTION_LIMIT = 20;
|
||||
|
||||
const trimNpcChatSuggestion = (text: string) =>
|
||||
@@ -639,9 +659,9 @@ export function createStoryNpcEncounterActions({
|
||||
sanitizeNpcChatSuggestion(playerMessage) || '刚才那句',
|
||||
);
|
||||
return [
|
||||
sanitizeNpcChatSuggestion(`你刚才那句是什么意思`),
|
||||
sanitizeNpcChatSuggestion('我愿意先听你说完'),
|
||||
sanitizeNpcChatSuggestion(`这件事和${topic}有关吗`),
|
||||
sanitizeNpcChatSuggestion('你愿意再说清楚点吗'),
|
||||
sanitizeNpcChatSuggestion('你别再避重就轻'),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -706,11 +726,103 @@ export function createStoryNpcEncounterActions({
|
||||
...fallbackSuggestions.filter(
|
||||
(suggestion) => !seenActionTexts.has(suggestion),
|
||||
),
|
||||
].slice(0, 3);
|
||||
];
|
||||
|
||||
return buildNpcChatTurnOptions(encounter, mergedSuggestions);
|
||||
};
|
||||
|
||||
const buildNpcChatFunctionOptionCatalog = (
|
||||
encounter: Encounter,
|
||||
playerCharacter: Character,
|
||||
) =>
|
||||
buildPostNpcChatOptionCatalog(encounter, playerCharacter)
|
||||
.filter((option) => option.functionId !== 'battle_escape_breakout')
|
||||
.filter((option) => !isNpcChatOptionForEncounter(option, encounter))
|
||||
.filter((option) => option.interaction?.kind === 'npc')
|
||||
.map(cloneNpcChatFunctionOption);
|
||||
|
||||
const toNpcChatDirectiveWithFunctionOptions = (
|
||||
directive: NpcChatDirective,
|
||||
encounter: Encounter,
|
||||
playerCharacter: Character,
|
||||
options?: {
|
||||
forcePlayerExit?: boolean;
|
||||
},
|
||||
): NpcChatDirective => {
|
||||
const functionOptions = buildNpcChatFunctionOptionCatalog(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
).map((option) => ({
|
||||
functionId: option.functionId,
|
||||
actionText: option.actionText,
|
||||
detailText: option.detailText ?? null,
|
||||
action: option.interaction?.kind === 'npc' ? option.interaction.action : null,
|
||||
}));
|
||||
const isHostileChat =
|
||||
directive?.isHostileChat === true ||
|
||||
directive?.terminationMode === 'hostile_model';
|
||||
|
||||
return {
|
||||
...(directive ?? {}),
|
||||
terminationMode: isHostileChat ? 'hostile_model' : 'none',
|
||||
isHostileChat,
|
||||
terminationReason: options?.forcePlayerExit
|
||||
? 'player_exit'
|
||||
: (directive?.terminationReason ?? null),
|
||||
closingMode: options?.forcePlayerExit
|
||||
? 'foreshadow_close'
|
||||
: (directive?.closingMode ?? 'free'),
|
||||
forceExitAfterTurn:
|
||||
options?.forcePlayerExit || directive?.forceExitAfterTurn || false,
|
||||
functionOptions,
|
||||
};
|
||||
};
|
||||
|
||||
const buildNpcChatMixedTurnOptions = (
|
||||
encounter: Encounter,
|
||||
playerCharacter: Character,
|
||||
suggestions: string[],
|
||||
functionSuggestions?: Array<{
|
||||
functionId?: string;
|
||||
actionText?: string;
|
||||
}>,
|
||||
) => {
|
||||
const chatOptions = buildNpcChatTurnOptions(encounter, suggestions);
|
||||
const functionCatalog = buildNpcChatFunctionOptionCatalog(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
);
|
||||
const functionOptions = (functionSuggestions ?? [])
|
||||
.map((suggestion) => {
|
||||
if (!suggestion.functionId || !suggestion.actionText) return null;
|
||||
const matchedOption = functionCatalog.find(
|
||||
(option) => option.functionId === suggestion.functionId,
|
||||
);
|
||||
return matchedOption
|
||||
? rewriteNpcChatFunctionOption(
|
||||
matchedOption,
|
||||
sanitizeNpcChatSuggestion(suggestion.actionText) ||
|
||||
matchedOption.actionText,
|
||||
)
|
||||
: null;
|
||||
})
|
||||
.filter((option): option is StoryOption => Boolean(option));
|
||||
|
||||
const mergedOptions = [...chatOptions, ...functionOptions];
|
||||
const seen = new Set<string>();
|
||||
|
||||
return mergedOptions.filter((option) => {
|
||||
const key = [
|
||||
option.functionId,
|
||||
option.actionText,
|
||||
option.interaction?.kind === 'npc' ? option.interaction.action : '',
|
||||
].join('::');
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const buildNpcChatStoryMoment = (params: {
|
||||
encounter: Encounter;
|
||||
dialogue: NonNullable<StoryMoment['dialogue']>;
|
||||
@@ -742,6 +854,9 @@ export function createStoryNpcEncounterActions({
|
||||
remainingTurns: params.chatDirective?.remainingTurns ?? null,
|
||||
limitReason: params.chatDirective?.limitReason ?? null,
|
||||
forceExitAfterTurn: params.chatDirective?.forceExitAfterTurn ?? false,
|
||||
terminationMode: params.chatDirective?.terminationMode ?? null,
|
||||
terminationReason: params.chatDirective?.terminationReason ?? null,
|
||||
isHostileChat: params.chatDirective?.isHostileChat ?? false,
|
||||
pendingQuestOffer: params.pendingQuestOffer ?? null,
|
||||
combatContext: params.combatContext ?? null,
|
||||
},
|
||||
@@ -773,7 +888,6 @@ export function createStoryNpcEncounterActions({
|
||||
getAvailableOptionsForState(gameState, playerCharacter) ?? [],
|
||||
);
|
||||
const currentStoryOptions = currentStory?.options ?? [];
|
||||
const currentNpcKey = encounter.id ?? encounter.npcName;
|
||||
const currentChatOptions = currentStoryOptions.filter((option) =>
|
||||
isNpcChatOptionForEncounter(option, encounter),
|
||||
);
|
||||
@@ -808,6 +922,124 @@ export function createStoryNpcEncounterActions({
|
||||
return mergedOptions;
|
||||
};
|
||||
|
||||
const buildSceneConnectionTravelOptions = (state: GameState) => {
|
||||
if (!state.worldType || !state.currentScenePreset) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seenSceneIds = new Set<string>();
|
||||
|
||||
return (state.currentScenePreset.connections ?? [])
|
||||
.filter((connection) => {
|
||||
if (!connection.sceneId || seenSceneIds.has(connection.sceneId)) {
|
||||
return false;
|
||||
}
|
||||
seenSceneIds.add(connection.sceneId);
|
||||
return true;
|
||||
})
|
||||
.map((connection) => {
|
||||
const targetScene = getScenePresetById(
|
||||
state.worldType!,
|
||||
connection.sceneId,
|
||||
);
|
||||
const targetSceneName = targetScene?.name ?? connection.sceneId;
|
||||
const directionText = getSceneConnectionDirectionText(
|
||||
connection.relativePosition,
|
||||
);
|
||||
const actionText = `${directionText},前往${targetSceneName}`;
|
||||
|
||||
return {
|
||||
functionId: 'idle_travel_next_scene',
|
||||
actionText,
|
||||
text: actionText,
|
||||
detailText: connection.summary,
|
||||
priority: 12,
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.RUN,
|
||||
playerMoveMeters: 1.1,
|
||||
playerOffsetY: 0,
|
||||
playerFacing:
|
||||
connection.relativePosition === 'west' ||
|
||||
connection.relativePosition === 'left' ||
|
||||
connection.relativePosition === 'back'
|
||||
? 'left'
|
||||
: 'right',
|
||||
scrollWorld: false,
|
||||
monsterChanges: [],
|
||||
},
|
||||
runtimePayload: {
|
||||
targetSceneId: connection.sceneId,
|
||||
},
|
||||
} satisfies StoryOption;
|
||||
});
|
||||
};
|
||||
|
||||
const buildPostNpcChatProgressionOptions = (
|
||||
encounter: Encounter,
|
||||
playerCharacter: Character,
|
||||
) => {
|
||||
const progression = resolveSceneActProgression({
|
||||
profile: gameState.customWorldProfile,
|
||||
sceneId: gameState.currentScenePreset?.id ?? null,
|
||||
storyEngineMemory: gameState.storyEngineMemory,
|
||||
});
|
||||
|
||||
if (!progression) {
|
||||
return {
|
||||
deferredRuntimeState: null,
|
||||
options: currentStory?.deferredOptions?.length
|
||||
? currentStory.deferredOptions
|
||||
: buildPostNpcChatOptionCatalog(encounter, playerCharacter),
|
||||
};
|
||||
}
|
||||
|
||||
if (!progression.isLastAct) {
|
||||
const nextActState = advanceSceneActRuntimeState({ progress: progression });
|
||||
const nextStoryEngineMemory = nextActState
|
||||
? {
|
||||
...(gameState.storyEngineMemory ??
|
||||
createEmptyStoryEngineMemoryState()),
|
||||
currentSceneActState: nextActState,
|
||||
}
|
||||
: gameState.storyEngineMemory;
|
||||
const nextState = {
|
||||
...gameState,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [],
|
||||
inBattle: false,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
storyEngineMemory: nextStoryEngineMemory,
|
||||
};
|
||||
const nextOptions = collapseNpcChatOptions(
|
||||
getAvailableOptionsForState(nextState, playerCharacter) ?? [],
|
||||
);
|
||||
|
||||
return {
|
||||
deferredRuntimeState: {
|
||||
currentScenePreset: nextState.currentScenePreset,
|
||||
storyEngineMemory: nextState.storyEngineMemory,
|
||||
},
|
||||
options:
|
||||
nextOptions.length > 0
|
||||
? nextOptions
|
||||
: buildPostNpcChatOptionCatalog(encounter, playerCharacter),
|
||||
};
|
||||
}
|
||||
|
||||
const travelOptions = buildSceneConnectionTravelOptions(gameState);
|
||||
|
||||
return {
|
||||
deferredRuntimeState: null,
|
||||
options:
|
||||
travelOptions.length > 0
|
||||
? travelOptions
|
||||
: buildPostNpcChatOptionCatalog(encounter, playerCharacter),
|
||||
};
|
||||
};
|
||||
|
||||
const buildLegacyNpcChatOpeningPlaceholder = (encounter: Encounter) =>
|
||||
`${encounter.npcName}看着你,像是在等你把话接下去。`;
|
||||
|
||||
@@ -968,6 +1200,14 @@ export function createStoryNpcEncounterActions({
|
||||
openingSource: 'npc_initiated' | 'player_reply' = 'player_reply',
|
||||
) => {
|
||||
const openingDialogue = buildNpcChatDialogueHistory(encounter, 0);
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
const resolvedChatDirective = playerCharacter
|
||||
? toNpcChatDirectiveWithFunctionOptions(
|
||||
chatDirective ?? null,
|
||||
encounter,
|
||||
playerCharacter,
|
||||
)
|
||||
: chatDirective;
|
||||
|
||||
setAiError(null);
|
||||
setCurrentStory(
|
||||
@@ -981,7 +1221,7 @@ export function createStoryNpcEncounterActions({
|
||||
),
|
||||
streaming: false,
|
||||
turnCount: 0,
|
||||
chatDirective,
|
||||
chatDirective: resolvedChatDirective,
|
||||
openingSource,
|
||||
}),
|
||||
);
|
||||
@@ -1006,6 +1246,11 @@ export function createStoryNpcEncounterActions({
|
||||
}
|
||||
|
||||
const npcState = getResolvedNpcState(gameState, encounter);
|
||||
const resolvedChatDirective = toNpcChatDirectiveWithFunctionOptions(
|
||||
chatDirective ?? null,
|
||||
encounter,
|
||||
playerCharacter,
|
||||
);
|
||||
const openingCampContext = buildOpeningCampChatContext(
|
||||
gameState,
|
||||
playerCharacter,
|
||||
@@ -1027,7 +1272,7 @@ export function createStoryNpcEncounterActions({
|
||||
options: [],
|
||||
streaming: true,
|
||||
turnCount: 0,
|
||||
chatDirective,
|
||||
chatDirective: resolvedChatDirective,
|
||||
openingSource: 'npc_initiated',
|
||||
}),
|
||||
);
|
||||
@@ -1067,12 +1312,12 @@ export function createStoryNpcEncounterActions({
|
||||
options: [],
|
||||
streaming: true,
|
||||
turnCount: 0,
|
||||
chatDirective,
|
||||
chatDirective: resolvedChatDirective,
|
||||
openingSource: 'npc_initiated',
|
||||
}),
|
||||
);
|
||||
},
|
||||
chatDirective,
|
||||
chatDirective: resolvedChatDirective,
|
||||
npcInitiatesConversation: true,
|
||||
},
|
||||
);
|
||||
@@ -1091,15 +1336,17 @@ export function createStoryNpcEncounterActions({
|
||||
text: chatTurn.npcReply,
|
||||
},
|
||||
],
|
||||
options: buildNpcChatTurnOptions(
|
||||
options: buildNpcChatMixedTurnOptions(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
chatTurn.suggestions.length > 0
|
||||
? chatTurn.suggestions
|
||||
: openingOptions.map((option) => option.actionText),
|
||||
chatTurn.functionSuggestions,
|
||||
),
|
||||
streaming: false,
|
||||
turnCount: 0,
|
||||
chatDirective,
|
||||
chatDirective: resolvedChatDirective,
|
||||
openingSource: 'npc_initiated',
|
||||
}),
|
||||
);
|
||||
@@ -1122,6 +1369,9 @@ export function createStoryNpcEncounterActions({
|
||||
const handleNpcChatTurn = async (
|
||||
encounter: Encounter,
|
||||
playerMessage: string,
|
||||
options: {
|
||||
forcePlayerExit?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
if (!playerCharacter || !gameState.worldType) {
|
||||
@@ -1157,6 +1407,14 @@ export function createStoryNpcEncounterActions({
|
||||
affinity: npcState.affinity,
|
||||
nextTurnCount,
|
||||
});
|
||||
const chatDirective = toNpcChatDirectiveWithFunctionOptions(
|
||||
limitedChatDirective,
|
||||
encounter,
|
||||
playerCharacter,
|
||||
{
|
||||
forcePlayerExit: options.forcePlayerExit,
|
||||
},
|
||||
);
|
||||
const openingCampContext = buildOpeningCampChatContext(
|
||||
gameState,
|
||||
playerCharacter,
|
||||
@@ -1172,7 +1430,7 @@ export function createStoryNpcEncounterActions({
|
||||
options: [],
|
||||
streaming: true,
|
||||
turnCount: nextTurnCount,
|
||||
chatDirective: limitedChatDirective,
|
||||
chatDirective,
|
||||
combatContext: currentCombatContext,
|
||||
}),
|
||||
);
|
||||
@@ -1212,18 +1470,18 @@ export function createStoryNpcEncounterActions({
|
||||
options: [],
|
||||
streaming: true,
|
||||
turnCount: nextTurnCount,
|
||||
chatDirective: limitedChatDirective,
|
||||
chatDirective,
|
||||
combatContext: currentCombatContext,
|
||||
}),
|
||||
);
|
||||
},
|
||||
questOfferContext: limitedChatDirective
|
||||
questOfferContext: chatDirective?.isHostileChat
|
||||
? null
|
||||
: {
|
||||
state: gameState,
|
||||
turnCount: nextTurnCount,
|
||||
},
|
||||
chatDirective: limitedChatDirective,
|
||||
chatDirective,
|
||||
combatContext: currentCombatContext,
|
||||
},
|
||||
);
|
||||
@@ -1283,21 +1541,31 @@ export function createStoryNpcEncounterActions({
|
||||
const pendingQuest =
|
||||
(chatTurn.pendingQuestOffer?.quest as QuestLogEntry | undefined) ??
|
||||
null;
|
||||
const resolvedChatDirective = limitedChatDirective
|
||||
const resolvedChatDirective = chatDirective
|
||||
? {
|
||||
sceneActId: limitedChatDirective.sceneActId ?? null,
|
||||
sceneActId: chatDirective.sceneActId ?? null,
|
||||
turnLimit:
|
||||
chatTurn.chatDirective?.turnLimit ??
|
||||
limitedChatDirective.turnLimit ??
|
||||
chatDirective.turnLimit ??
|
||||
null,
|
||||
remainingTurns:
|
||||
chatTurn.chatDirective?.remainingTurns ??
|
||||
limitedChatDirective.remainingTurns ??
|
||||
chatDirective.remainingTurns ??
|
||||
null,
|
||||
limitReason: limitedChatDirective.limitReason ?? null,
|
||||
forceExitAfterTurn:
|
||||
limitReason: chatDirective.limitReason ?? null,
|
||||
terminationMode: chatDirective.terminationMode ?? null,
|
||||
terminationReason:
|
||||
chatTurn.chatDirective?.terminationReason ??
|
||||
chatDirective.terminationReason ??
|
||||
null,
|
||||
isHostileChat: chatDirective.isHostileChat ?? false,
|
||||
closingMode:
|
||||
chatTurn.chatDirective?.closingMode ??
|
||||
chatDirective.closingMode ??
|
||||
'free',
|
||||
forceExitAfterTurn:
|
||||
chatTurn.chatDirective?.forceExit ??
|
||||
limitedChatDirective.forceExitAfterTurn ??
|
||||
chatDirective.forceExitAfterTurn ??
|
||||
false,
|
||||
}
|
||||
: null;
|
||||
@@ -1308,11 +1576,11 @@ export function createStoryNpcEncounterActions({
|
||||
if (shouldForceExitAfterTurn) {
|
||||
const closingDialogue = [
|
||||
...nextDialogue,
|
||||
{
|
||||
speaker: 'system' as const,
|
||||
text: '这轮交谈先在这里收束,对方留下的线索把你推向了下一步。',
|
||||
},
|
||||
];
|
||||
const progressionResult = buildPostNpcChatProgressionOptions(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
);
|
||||
setCurrentStory({
|
||||
text: closingDialogue.map((turn) => turn.text).join('\n'),
|
||||
options: [buildContinueAdventureOption()],
|
||||
@@ -1320,6 +1588,9 @@ export function createStoryNpcEncounterActions({
|
||||
dialogue: closingDialogue,
|
||||
streaming: false,
|
||||
npcAffinityEffect: latestAffinityEffect,
|
||||
deferredOptions: progressionResult.options,
|
||||
deferredRuntimeState:
|
||||
progressionResult.deferredRuntimeState ?? undefined,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -1355,11 +1626,13 @@ export function createStoryNpcEncounterActions({
|
||||
buildNpcChatStoryMoment({
|
||||
encounter,
|
||||
dialogue: nextDialogue,
|
||||
options: buildNpcChatTurnOptions(
|
||||
options: buildNpcChatMixedTurnOptions(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
chatTurn.suggestions.length > 0
|
||||
? chatTurn.suggestions
|
||||
: buildFallbackNpcChatSuggestions(playerMessage),
|
||||
chatTurn.functionSuggestions,
|
||||
),
|
||||
streaming: false,
|
||||
turnCount: nextTurnCount,
|
||||
@@ -1382,7 +1655,7 @@ export function createStoryNpcEncounterActions({
|
||||
),
|
||||
streaming: false,
|
||||
turnCount: nextTurnCount,
|
||||
chatDirective: limitedChatDirective,
|
||||
chatDirective,
|
||||
combatContext: currentCombatContext,
|
||||
}),
|
||||
);
|
||||
@@ -1392,7 +1665,7 @@ export function createStoryNpcEncounterActions({
|
||||
}
|
||||
};
|
||||
|
||||
const exitNpcChat = () => {
|
||||
const continueAfterNpcChatClosure = async () => {
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
const encounter = gameState.currentEncounter;
|
||||
if (!playerCharacter || !isNpcEncounter(encounter)) {
|
||||
@@ -1400,48 +1673,50 @@ export function createStoryNpcEncounterActions({
|
||||
}
|
||||
|
||||
setAiError(null);
|
||||
setIsLoading(true);
|
||||
const progressionResult = buildPostNpcChatProgressionOptions(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
);
|
||||
const nextState = {
|
||||
...gameState,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [],
|
||||
inBattle: false,
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
currentScenePreset:
|
||||
progressionResult.deferredRuntimeState?.currentScenePreset ??
|
||||
gameState.currentScenePreset,
|
||||
storyEngineMemory:
|
||||
progressionResult.deferredRuntimeState?.storyEngineMemory ??
|
||||
gameState.storyEngineMemory,
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
const choiceText = `结束与${encounter.npcName}的这轮交谈,重新观察当前局势`;
|
||||
setGameState(nextState);
|
||||
setCurrentStory({
|
||||
text: currentStory?.dialogue?.at(-1)?.text ?? currentStory?.text ?? '',
|
||||
options: progressionResult.options,
|
||||
displayMode: 'narrative',
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
const postChatOptionCatalog = buildPostNpcChatOptionCatalog(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
);
|
||||
const nextStory = await generateStoryForState({
|
||||
state: gameState,
|
||||
character: playerCharacter,
|
||||
history: gameState.storyHistory,
|
||||
choice: choiceText,
|
||||
lastFunctionId: 'npc_chat',
|
||||
optionCatalog: postChatOptionCatalog,
|
||||
});
|
||||
const nextHistory = [
|
||||
...gameState.storyHistory,
|
||||
createHistoryMoment(choiceText, 'action'),
|
||||
createHistoryMoment(nextStory.text, 'result', nextStory.options),
|
||||
];
|
||||
const recoveredState = applyStoryReasoningRecovery({
|
||||
...gameState,
|
||||
storyHistory: nextHistory,
|
||||
});
|
||||
setGameState(recoveredState);
|
||||
setCurrentStory(nextStory);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to continue story after exiting npc chat:',
|
||||
error,
|
||||
);
|
||||
setAiError(
|
||||
error instanceof Error ? error.message : '退出聊天后的剧情推理失败',
|
||||
);
|
||||
setCurrentStory(buildFallbackStoryForState(gameState, playerCharacter));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
const exitNpcChat = () => {
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
const encounter = gameState.currentEncounter;
|
||||
if (!playerCharacter || !isNpcEncounter(encounter)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void handleNpcChatTurn(
|
||||
encounter,
|
||||
`我先结束这轮交谈,继续往前走。`,
|
||||
{
|
||||
forcePlayerExit: true,
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -1694,6 +1969,13 @@ export function createStoryNpcEncounterActions({
|
||||
};
|
||||
|
||||
const handleNpcInteraction = (option: StoryOption) => {
|
||||
if (
|
||||
currentStory?.deferredOptions?.length &&
|
||||
option.functionId === 'story_continue_adventure'
|
||||
) {
|
||||
void continueAfterNpcChatClosure();
|
||||
return true;
|
||||
}
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
if (!playerCharacter || !isNpcEncounter(gameState.currentEncounter)) {
|
||||
return false;
|
||||
@@ -1800,6 +2082,7 @@ export function createStoryNpcEncounterActions({
|
||||
reopenNpcChatAfterBattle,
|
||||
handleNpcChatTurn,
|
||||
exitNpcChat,
|
||||
continueAfterNpcChatClosure,
|
||||
replacePendingNpcQuestOffer,
|
||||
abandonPendingNpcQuestOffer,
|
||||
acceptPendingNpcQuestOffer,
|
||||
|
||||
Reference in New Issue
Block a user