Split custom world generation into staged lightweight batches
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -6,6 +6,7 @@ import { NPC_PREVIEW_TALK_FUNCTION } from '../../data/functionCatalog';
|
||||
import {
|
||||
addInventoryItems,
|
||||
buildNpcChatResultText,
|
||||
buildNpcHelpCommitActionText,
|
||||
buildNpcHelpResultText,
|
||||
buildNpcHelpReward,
|
||||
buildNpcLeaveResultText,
|
||||
@@ -35,9 +36,11 @@ import {
|
||||
createSceneCallOutEncounter,
|
||||
resolveSceneEncounterPreview,
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
|
||||
import { generateNextStep, streamNpcChatDialogue } from '../../services/ai';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { generateQuestForNpcEncounter } from '../../services/questDirector';
|
||||
import { createHistoryMoment } from '../../services/storyHistory';
|
||||
import type {
|
||||
Character,
|
||||
Encounter,
|
||||
@@ -60,6 +63,15 @@ 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;
|
||||
@@ -108,6 +120,7 @@ export function createStoryNpcEncounterActions({
|
||||
buildStoryContextFromState,
|
||||
buildFallbackStoryForState,
|
||||
buildDialogueStoryMoment,
|
||||
generateStoryForState,
|
||||
getStoryGenerationHostileNpcs,
|
||||
getTypewriterDelay,
|
||||
getAvailableOptionsForState,
|
||||
@@ -153,6 +166,7 @@ export function createStoryNpcEncounterActions({
|
||||
options: StoryOption[],
|
||||
streaming?: boolean,
|
||||
) => StoryMoment;
|
||||
generateStoryForState: GenerateStoryForState;
|
||||
getStoryGenerationHostileNpcs: (
|
||||
state: GameState,
|
||||
) => GameState['sceneMonsters'];
|
||||
@@ -218,6 +232,10 @@ export function createStoryNpcEncounterActions({
|
||||
const battleNpcId = state.currentBattleNpcId;
|
||||
const npcState = state.npcStates[battleNpcId];
|
||||
if (!npcState) return null;
|
||||
const activeBattleHostiles =
|
||||
state.sceneMonsters.length > 0
|
||||
? state.sceneMonsters
|
||||
: (state.sceneHostileNpcs ?? []);
|
||||
|
||||
if (battleMode === 'spar' && battleOutcome === 'spar_complete') {
|
||||
const nextAffinity = npcState.affinity + NPC_SPAR_AFFINITY_GAIN;
|
||||
@@ -233,6 +251,7 @@ export function createStoryNpcEncounterActions({
|
||||
currentNpcBattleOutcome: null,
|
||||
currentEncounter: restoredEncounter,
|
||||
npcInteractionActive: true,
|
||||
sceneMonsters: [],
|
||||
sceneHostileNpcs: [],
|
||||
npcStates: {
|
||||
...state.npcStates,
|
||||
@@ -260,6 +279,7 @@ export function createStoryNpcEncounterActions({
|
||||
return {
|
||||
nextState,
|
||||
resultText: buildNpcSparResultText(
|
||||
activeBattleHostiles[0]?.name ?? '对方',
|
||||
NPC_SPAR_AFFINITY_GAIN,
|
||||
nextAffinity,
|
||||
),
|
||||
@@ -269,9 +289,9 @@ export function createStoryNpcEncounterActions({
|
||||
const lootItems = getNpcLootItems(npcState, character).map((item) =>
|
||||
cloneInventoryItemForOwner(item, 'player'),
|
||||
);
|
||||
const defeatedHostileNpcIds = (
|
||||
state.sceneHostileNpcs ?? state.sceneMonsters
|
||||
).map((hostileNpc) => hostileNpc.id);
|
||||
const defeatedHostileNpcIds = activeBattleHostiles.map(
|
||||
(hostileNpc) => hostileNpc.id,
|
||||
);
|
||||
const progressedQuests = applyQuestProgressFromHostileNpcDefeat(
|
||||
state.quests,
|
||||
state.currentScenePreset?.id ?? null,
|
||||
@@ -291,6 +311,7 @@ export function createStoryNpcEncounterActions({
|
||||
currentNpcBattleOutcome: null,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneMonsters: [],
|
||||
sceneHostileNpcs: [],
|
||||
playerInventory: addInventoryItems(state.playerInventory, lootItems),
|
||||
quests: progressedQuests,
|
||||
@@ -324,9 +345,13 @@ export function createStoryNpcEncounterActions({
|
||||
lootItems.length > 0
|
||||
? lootItems.map((item) => item.name).join(', ')
|
||||
: '无战利品';
|
||||
const defeatedNames =
|
||||
activeBattleHostiles.map((hostileNpc) => hostileNpc.name).join('、') ||
|
||||
battleNpcId ||
|
||||
'对手';
|
||||
return {
|
||||
nextState,
|
||||
resultText: `胜利奖励:${lootText}。`,
|
||||
resultText: `${defeatedNames}已经败下阵来。胜利奖励:${lootText}。`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -337,7 +362,11 @@ export function createStoryNpcEncounterActions({
|
||||
actionText: string,
|
||||
resultText: string,
|
||||
lastFunctionId?: string,
|
||||
contextNpcStateOverride?: GameState['npcStates'][string] | null,
|
||||
options: {
|
||||
contextNpcStateOverride?: GameState['npcStates'][string] | null;
|
||||
preserveResultTextInHistory?: boolean;
|
||||
revealMode?: 'deferred_options' | 'immediate_story';
|
||||
} = {},
|
||||
) => {
|
||||
const provisionalHistory = appendHistory(gameState, actionText, resultText);
|
||||
const provisionalState = {
|
||||
@@ -391,11 +420,11 @@ export function createStoryNpcEncounterActions({
|
||||
character,
|
||||
encounter,
|
||||
getStoryGenerationHostileNpcs(provisionalState),
|
||||
gameState.storyHistory,
|
||||
provisionalHistory,
|
||||
buildStoryContextFromState(provisionalState, {
|
||||
lastFunctionId,
|
||||
...provisionalOpeningCampContext,
|
||||
encounterNpcStateOverride: contextNpcStateOverride,
|
||||
encounterNpcStateOverride: options.contextNpcStateOverride,
|
||||
}),
|
||||
actionText,
|
||||
resultText,
|
||||
@@ -409,19 +438,19 @@ export function createStoryNpcEncounterActions({
|
||||
streamCompleted = true;
|
||||
await typewriterPromise;
|
||||
|
||||
const finalHistory = appendHistory(
|
||||
gameState,
|
||||
actionText,
|
||||
dialogueText || resultText,
|
||||
);
|
||||
const finalDialogueText = dialogueText || resultText;
|
||||
const finalHistory = options.preserveResultTextInHistory
|
||||
? finalDialogueText && finalDialogueText !== resultText
|
||||
? [
|
||||
...provisionalHistory,
|
||||
createHistoryMoment(finalDialogueText, 'result'),
|
||||
]
|
||||
: provisionalHistory
|
||||
: appendHistory(gameState, actionText, finalDialogueText);
|
||||
const finalState = {
|
||||
...nextState,
|
||||
storyHistory: finalHistory,
|
||||
};
|
||||
const availableOptions = getAvailableOptionsForState(
|
||||
finalState,
|
||||
character,
|
||||
);
|
||||
const finalOpeningCampContext = buildOpeningCampChatContext(
|
||||
finalState,
|
||||
character,
|
||||
@@ -429,6 +458,35 @@ export function createStoryNpcEncounterActions({
|
||||
);
|
||||
setGameState(finalState);
|
||||
|
||||
if (options.revealMode === 'immediate_story') {
|
||||
setCurrentStory(
|
||||
buildDialogueStoryMoment(
|
||||
encounter.npcName,
|
||||
finalDialogueText,
|
||||
[],
|
||||
false,
|
||||
),
|
||||
);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 260));
|
||||
|
||||
const nextStory = await generateStoryForState({
|
||||
state: finalState,
|
||||
character,
|
||||
history: finalHistory,
|
||||
choice: actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
const recoveredState = applyStoryReasoningRecovery(finalState);
|
||||
setGameState(recoveredState);
|
||||
setCurrentStory(nextStory);
|
||||
return;
|
||||
}
|
||||
|
||||
const availableOptions = getAvailableOptionsForState(
|
||||
finalState,
|
||||
character,
|
||||
);
|
||||
|
||||
const response = await generateNextStep(
|
||||
gameState.worldType!,
|
||||
character,
|
||||
@@ -446,6 +504,8 @@ export function createStoryNpcEncounterActions({
|
||||
? response.options
|
||||
: sanitizeOptions(response.options, character, finalState),
|
||||
);
|
||||
const recoveredState = applyStoryReasoningRecovery(finalState);
|
||||
setGameState(recoveredState);
|
||||
|
||||
setCurrentStory({
|
||||
...buildDialogueStoryMoment(
|
||||
@@ -463,6 +523,12 @@ export function createStoryNpcEncounterActions({
|
||||
setAiError(
|
||||
error instanceof Error ? error.message : '角色对话智能生成不可用。',
|
||||
);
|
||||
if (options.revealMode === 'immediate_story') {
|
||||
setCurrentStory(
|
||||
buildFallbackStoryForState(provisionalState, character, resultText),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const fallbackOptions =
|
||||
getAvailableOptionsForState(provisionalState, character) ?? [];
|
||||
setCurrentStory(
|
||||
@@ -489,7 +555,8 @@ export function createStoryNpcEncounterActions({
|
||||
};
|
||||
|
||||
const enterNpcInteraction = (encounter: Encounter, actionText: string) => {
|
||||
if (!gameState.playerCharacter) return false;
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
if (!playerCharacter) return false;
|
||||
|
||||
const nextState: GameState = {
|
||||
...gameState,
|
||||
@@ -498,7 +565,7 @@ export function createStoryNpcEncounterActions({
|
||||
|
||||
void commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
actionText,
|
||||
`${encounter.npcName} turns their attention toward you, as if waiting for you to speak first.`,
|
||||
NPC_PREVIEW_TALK_FUNCTION.id,
|
||||
@@ -507,11 +574,8 @@ export function createStoryNpcEncounterActions({
|
||||
};
|
||||
|
||||
const handleNpcInteraction = (option: StoryOption) => {
|
||||
if (
|
||||
!gameState.playerCharacter ||
|
||||
!option.interaction ||
|
||||
!isNpcEncounter(gameState.currentEncounter)
|
||||
) {
|
||||
const playerCharacter = gameState.playerCharacter;
|
||||
if (!playerCharacter || !option.interaction || !isNpcEncounter(gameState.currentEncounter)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -603,12 +667,19 @@ export function createStoryNpcEncounterActions({
|
||||
: nextState.playerInventory,
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
await commitNpcChatState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
playerCharacter,
|
||||
encounter,
|
||||
buildNpcHelpCommitActionText(encounter, reward),
|
||||
buildNpcHelpResultText(encounter, reward),
|
||||
option.functionId,
|
||||
{
|
||||
contextNpcStateOverride:
|
||||
nextState.npcStates[getNpcEncounterKey(encounter)] ?? null,
|
||||
preserveResultTextInHistory: true,
|
||||
revealMode: 'immediate_story',
|
||||
},
|
||||
);
|
||||
committed = true;
|
||||
} catch (error) {
|
||||
@@ -663,12 +734,19 @@ export function createStoryNpcEncounterActions({
|
||||
: nextState.playerInventory,
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
await commitNpcChatState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
playerCharacter,
|
||||
encounter,
|
||||
buildNpcHelpCommitActionText(encounter, reward),
|
||||
buildNpcHelpResultText(encounter, reward),
|
||||
option.functionId,
|
||||
{
|
||||
contextNpcStateOverride:
|
||||
nextState.npcStates[getNpcEncounterKey(encounter)] ?? null,
|
||||
preserveResultTextInHistory: true,
|
||||
revealMode: 'immediate_story',
|
||||
},
|
||||
);
|
||||
committed = true;
|
||||
} finally {
|
||||
@@ -681,7 +759,7 @@ export function createStoryNpcEncounterActions({
|
||||
}
|
||||
case 'chat': {
|
||||
const chatOutcome = getChatAffinityOutcome({
|
||||
playerCharacter: gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
encounter,
|
||||
npcState,
|
||||
actionText: option.actionText,
|
||||
@@ -706,7 +784,7 @@ export function createStoryNpcEncounterActions({
|
||||
);
|
||||
void commitNpcChatState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
encounter,
|
||||
option.actionText,
|
||||
npcState.recruited
|
||||
@@ -722,7 +800,9 @@ export function createStoryNpcEncounterActions({
|
||||
attributeSummary,
|
||||
),
|
||||
option.functionId,
|
||||
npcState,
|
||||
{
|
||||
contextNpcStateOverride: npcState,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -765,7 +845,7 @@ export function createStoryNpcEncounterActions({
|
||||
);
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
option.actionText,
|
||||
buildQuestAcceptResultText(quest),
|
||||
option.functionId,
|
||||
@@ -797,7 +877,7 @@ export function createStoryNpcEncounterActions({
|
||||
);
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
option.actionText,
|
||||
buildQuestAcceptResultText(fallbackQuest),
|
||||
option.functionId,
|
||||
@@ -840,7 +920,7 @@ export function createStoryNpcEncounterActions({
|
||||
|
||||
void commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
option.actionText,
|
||||
buildQuestTurnInResultText(quest),
|
||||
option.functionId,
|
||||
@@ -853,6 +933,7 @@ export function createStoryNpcEncounterActions({
|
||||
ambientIdleMode: undefined,
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneMonsters: [],
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerFacing: 'right' as const,
|
||||
@@ -878,7 +959,7 @@ export function createStoryNpcEncounterActions({
|
||||
void commitGeneratedStateWithEncounterEntry(
|
||||
entryState,
|
||||
resolvedState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
option.actionText,
|
||||
buildNpcLeaveResultText(encounter),
|
||||
option.functionId,
|
||||
@@ -886,6 +967,10 @@ export function createStoryNpcEncounterActions({
|
||||
return true;
|
||||
}
|
||||
case 'fight': {
|
||||
const battleMonster = createNpcBattleMonster(encounter, npcState, 'fight', {
|
||||
worldType: gameState.worldType,
|
||||
customWorldProfile: gameState.customWorldProfile,
|
||||
});
|
||||
const nextState = {
|
||||
...gameState,
|
||||
npcStates: {
|
||||
@@ -896,9 +981,8 @@ export function createStoryNpcEncounterActions({
|
||||
},
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [
|
||||
createNpcBattleMonster(encounter, npcState, 'fight'),
|
||||
],
|
||||
sceneMonsters: [battleMonster],
|
||||
sceneHostileNpcs: [battleMonster],
|
||||
playerX: 0,
|
||||
playerFacing: 'right' as const,
|
||||
animationState: AnimationState.IDLE,
|
||||
@@ -915,7 +999,7 @@ export function createStoryNpcEncounterActions({
|
||||
};
|
||||
void commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
option.actionText,
|
||||
`You lunge at ${encounter.npcName} with clear hostile intent, and the atmosphere turns dangerous at once.`,
|
||||
option.functionId,
|
||||
@@ -923,7 +1007,15 @@ export function createStoryNpcEncounterActions({
|
||||
return true;
|
||||
}
|
||||
case 'spar': {
|
||||
const sparPlayerMaxHp = getNpcSparMaxHp(gameState.playerCharacter);
|
||||
const sparPlayerMaxHp = getNpcSparMaxHp(
|
||||
playerCharacter,
|
||||
gameState.worldType,
|
||||
gameState.customWorldProfile,
|
||||
);
|
||||
const battleMonster = createNpcBattleMonster(encounter, npcState, 'spar', {
|
||||
worldType: gameState.worldType,
|
||||
customWorldProfile: gameState.customWorldProfile,
|
||||
});
|
||||
const nextState = {
|
||||
...gameState,
|
||||
npcStates: {
|
||||
@@ -934,9 +1026,8 @@ export function createStoryNpcEncounterActions({
|
||||
},
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
sceneHostileNpcs: [
|
||||
createNpcBattleMonster(encounter, npcState, 'spar'),
|
||||
],
|
||||
sceneMonsters: [battleMonster],
|
||||
sceneHostileNpcs: [battleMonster],
|
||||
playerX: 0,
|
||||
playerHp: sparPlayerMaxHp,
|
||||
playerMaxHp: sparPlayerMaxHp,
|
||||
@@ -955,7 +1046,7 @@ export function createStoryNpcEncounterActions({
|
||||
};
|
||||
void commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
playerCharacter,
|
||||
option.actionText,
|
||||
`${encounter.npcName} salutes you and agrees to keep the spar controlled and respectful.`,
|
||||
option.functionId,
|
||||
|
||||
Reference in New Issue
Block a user