This commit is contained in:
2026-04-10 15:37:02 +08:00
parent 161cd32277
commit f19e482c8f
233 changed files with 43987 additions and 5127 deletions

View File

@@ -60,6 +60,7 @@ import type {
} from '../../types';
import { AnimationState } from '../../types';
import type { CommitGeneratedState } from '../generatedState';
import { resolveServerRuntimeChoice } from './runtimeStoryCoordinator';
type CommitGeneratedStateWithEncounterEntry = (
entryState: GameState,
@@ -116,6 +117,7 @@ function isNpcEncounter(
export function createStoryNpcEncounterActions({
gameState,
currentStory,
setGameState,
setCurrentStory,
setAiError,
@@ -142,6 +144,7 @@ export function createStoryNpcEncounterActions({
npcInteractionFlow,
}: {
gameState: GameState;
currentStory: StoryMoment | null;
setGameState: Dispatch<SetStateAction<GameState>>;
setCurrentStory: Dispatch<SetStateAction<StoryMoment | null>>;
setAiError: Dispatch<SetStateAction<string | null>>;
@@ -588,6 +591,46 @@ export function createStoryNpcEncounterActions({
return true;
};
const resolveServerNpcStoryAction = async (params: {
option: StoryOption;
encounter: Encounter;
payload?: Record<string, unknown>;
}) => {
const playerCharacter = gameState.playerCharacter;
if (
!playerCharacter ||
!gameState.worldType ||
gameState.currentScene !== 'Story'
) {
return false;
}
setAiError(null);
setIsLoading(true);
try {
const { hydratedSnapshot, nextStory } = await resolveServerRuntimeChoice({
gameState,
currentStory,
option: params.option,
payload: params.payload,
});
setGameState(hydratedSnapshot.gameState);
setCurrentStory(nextStory);
return true;
} catch (error) {
console.error('Failed to resolve npc story action on the server:', error);
setAiError(error instanceof Error ? error.message : 'NPC 动作执行失败');
if (!currentStory) {
setCurrentStory(buildFallbackStoryForState(gameState, playerCharacter));
}
return false;
} finally {
setIsLoading(false);
}
};
const handleNpcInteraction = (option: StoryOption) => {
const playerCharacter = gameState.playerCharacter;
if (!playerCharacter || !option.interaction || !isNpcEncounter(gameState.currentEncounter)) {
@@ -835,141 +878,23 @@ export function createStoryNpcEncounterActions({
return true;
}
case 'quest_accept': {
const existingQuest = getQuestForIssuer(
gameState.quests,
getNpcEncounterKey(encounter),
);
if (existingQuest) return true;
setAiError(null);
setIsLoading(true);
void (async () => {
let committed = false;
try {
const quest =
(await generateQuestForNpcEncounter({
state: gameState,
encounter,
})) ??
buildQuestForEncounter({
issuerNpcId: getNpcEncounterKey(encounter),
issuerNpcName: encounter.npcName,
roleText: encounter.context,
scene: gameState.currentScenePreset,
worldType: gameState.worldType,
});
if (!quest) {
return;
}
const nextState = incrementRuntimeStats(
updateNpcState(
updateQuestLog(gameState, (quests) => acceptQuest(quests, quest)),
encounter,
(currentNpcState) => ({
...markNpcFirstMeaningfulContactResolved(currentNpcState),
stanceProfile: applyStoryChoiceToStanceProfile(
currentNpcState.stanceProfile,
'npc_quest_accept',
),
}),
),
{questsAccepted: 1},
);
await commitGeneratedState(
nextState,
playerCharacter,
option.actionText,
buildQuestAcceptResultText(quest),
option.functionId,
);
committed = true;
} catch (error) {
console.error('Failed to accept npc quest:', error);
const fallbackQuest = buildQuestForEncounter({
issuerNpcId: getNpcEncounterKey(encounter),
issuerNpcName: encounter.npcName,
roleText: encounter.context,
scene: gameState.currentScenePreset,
worldType: gameState.worldType,
});
if (!fallbackQuest) {
return;
}
const nextState = incrementRuntimeStats(
updateNpcState(
updateQuestLog(gameState, (quests) =>
acceptQuest(quests, fallbackQuest),
),
encounter,
(currentNpcState) => ({
...markNpcFirstMeaningfulContactResolved(currentNpcState),
stanceProfile: applyStoryChoiceToStanceProfile(
currentNpcState.stanceProfile,
'npc_quest_accept',
),
}),
),
{questsAccepted: 1},
);
await commitGeneratedState(
nextState,
playerCharacter,
option.actionText,
buildQuestAcceptResultText(fallbackQuest),
option.functionId,
);
committed = true;
} finally {
if (!committed) {
setIsLoading(false);
}
}
})();
void resolveServerNpcStoryAction({
option,
encounter,
});
return true;
}
case 'quest_turn_in': {
const questId = option.interaction.questId;
const quest = questId ? findQuestById(gameState.quests, questId) : null;
if (!quest || quest.status !== 'completed') return true;
const nextState = appendStoryEngineCarrierMemory({
...updateQuestLog(gameState, (quests) =>
markQuestTurnedIn(quests, quest.id),
),
npcStates: {
...gameState.npcStates,
[getNpcEncounterKey(encounter)]: {
...syncNpcNarrativeState({
encounter,
npcState: {
...npcState,
...markNpcFirstMeaningfulContactResolved(npcState),
affinity: npcState.affinity + quest.reward.affinityBonus,
relationState: buildRelationState(
npcState.affinity + quest.reward.affinityBonus,
),
},
customWorldProfile: gameState.customWorldProfile,
storyEngineMemory: gameState.storyEngineMemory,
}),
},
},
playerCurrency: gameState.playerCurrency + quest.reward.currency,
playerInventory: addInventoryItems(
gameState.playerInventory,
quest.reward.items,
),
} as GameState, quest.reward.items);
void commitGeneratedState(
nextState,
playerCharacter,
option.actionText,
buildQuestTurnInResultText(quest),
option.functionId,
);
void resolveServerNpcStoryAction({
option,
encounter,
payload: questId
? {
questId,
}
: undefined,
});
return true;
}
case 'leave': {