1
This commit is contained in:
@@ -5,6 +5,7 @@ import { generateNextStep } from '../../services/aiService';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { appendStoryEngineCarrierMemory } from '../../services/storyEngine/echoMemory';
|
||||
import { createHistoryMoment } from '../../services/storyHistory';
|
||||
import { AnimationState } from '../../types';
|
||||
import type {
|
||||
Character,
|
||||
Encounter,
|
||||
@@ -13,11 +14,17 @@ import type {
|
||||
StoryOption,
|
||||
} from '../../types';
|
||||
import type { EscapePlaybackSync } from '../combat/escapeFlow';
|
||||
import type { BattlePlan } from '../combat/battlePlan';
|
||||
import type { ResolvedChoiceState } from '../combat/resolvedChoice';
|
||||
import {
|
||||
buildDeathStory,
|
||||
buildPostBattleVictoryState,
|
||||
buildPostBattleVictoryStory,
|
||||
buildRevivedFirstSceneState,
|
||||
} from './postBattleFlow';
|
||||
import {
|
||||
buildCombatResolutionContextText,
|
||||
buildHostileNpcBattleReward,
|
||||
buildReasonedOptionCatalog,
|
||||
} from './storyChoiceRuntime';
|
||||
import type { BattleRewardSummary } from './uiTypes';
|
||||
|
||||
@@ -77,6 +84,73 @@ type IncrementRuntimeStats = (
|
||||
increments: RuntimeStatsIncrements,
|
||||
) => GameState;
|
||||
|
||||
const PLAYER_REVIVE_DELAY_MS = 3000;
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function buildLocalCombatResultText(params: {
|
||||
option: StoryOption;
|
||||
battlePlan: BattlePlan | null;
|
||||
afterSequence: GameState;
|
||||
combatResolutionContextText: string | null;
|
||||
}) {
|
||||
if (params.combatResolutionContextText) {
|
||||
return params.combatResolutionContextText;
|
||||
}
|
||||
|
||||
const turns = params.battlePlan?.turns ?? [];
|
||||
const dealtDamage = turns
|
||||
.filter((turn) => turn.actor === 'player' || turn.actor === 'companion')
|
||||
.reduce((sum, turn) => sum + turn.damage, 0);
|
||||
const takenDamage = turns
|
||||
.filter((turn) => turn.actor === 'monster' && turn.target === 'player')
|
||||
.reduce((sum, turn) => sum + turn.damage, 0);
|
||||
|
||||
if (params.afterSequence.playerHp <= 0) {
|
||||
return takenDamage > 0
|
||||
? `你承受了${takenDamage}点伤害,气血归零。`
|
||||
: '你在战斗中倒下,气血归零。';
|
||||
}
|
||||
|
||||
const details = [
|
||||
dealtDamage > 0 ? `造成${dealtDamage}点伤害` : null,
|
||||
takenDamage > 0 ? `承受${takenDamage}点伤害` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
return details.length > 0
|
||||
? `${params.option.actionText}完成,${details.join(',')}。`
|
||||
: `${params.option.actionText}完成,双方仍在对峙。`;
|
||||
}
|
||||
|
||||
function buildDeterministicStoryForState(params: {
|
||||
state: GameState;
|
||||
character: Character;
|
||||
resultText: string;
|
||||
availableOptions: StoryOption[] | null;
|
||||
buildFallbackStoryForState: BuildFallbackStoryForState;
|
||||
}) {
|
||||
if (params.availableOptions?.length) {
|
||||
return {
|
||||
text: params.resultText,
|
||||
options: params.availableOptions,
|
||||
streaming: false,
|
||||
} satisfies StoryMoment;
|
||||
}
|
||||
|
||||
const fallbackStory = params.buildFallbackStoryForState(
|
||||
params.state,
|
||||
params.character,
|
||||
params.resultText,
|
||||
);
|
||||
return {
|
||||
...fallbackStory,
|
||||
text: params.resultText,
|
||||
streaming: false,
|
||||
} satisfies StoryMoment;
|
||||
}
|
||||
|
||||
export async function runLocalStoryChoiceContinuation(params: {
|
||||
gameState: GameState;
|
||||
currentStory: StoryMoment | null;
|
||||
@@ -159,6 +233,9 @@ export async function runLocalStoryChoiceContinuation(params: {
|
||||
params.character,
|
||||
);
|
||||
const projectedState = resolvedChoice.afterSequence;
|
||||
const shouldUseDeterministicCombatFlow =
|
||||
resolvedChoice.optionKind === 'battle' ||
|
||||
resolvedChoice.optionKind === 'escape';
|
||||
const shouldUseLocalNpcVictory = Boolean(
|
||||
baseChoiceState.currentBattleNpcId &&
|
||||
resolvedChoice.optionKind === 'battle' &&
|
||||
@@ -206,7 +283,7 @@ export async function runLocalStoryChoiceContinuation(params: {
|
||||
]
|
||||
: history;
|
||||
|
||||
const responsePromise = shouldUseLocalNpcVictory
|
||||
const responsePromise = shouldUseLocalNpcVictory || shouldUseDeterministicCombatFlow
|
||||
? Promise.resolve(null)
|
||||
: generateNextStep(
|
||||
params.gameState.worldType!,
|
||||
@@ -229,7 +306,7 @@ export async function runLocalStoryChoiceContinuation(params: {
|
||||
() => undefined,
|
||||
);
|
||||
const playbackSync: EscapePlaybackSync | undefined =
|
||||
resolvedChoice.optionKind === 'escape'
|
||||
resolvedChoice.optionKind === 'escape' && !shouldUseDeterministicCombatFlow
|
||||
? { waitForStoryResponse: responseSettledPromise }
|
||||
: undefined;
|
||||
const actionPromise = params.playResolvedChoice(
|
||||
@@ -286,66 +363,122 @@ export async function runLocalStoryChoiceContinuation(params: {
|
||||
...victory.nextState,
|
||||
storyHistory: nextHistory,
|
||||
};
|
||||
const postBattleOptionCatalog =
|
||||
baseChoiceState.currentNpcBattleMode === 'spar' &&
|
||||
nextState.currentEncounter
|
||||
? buildReasonedOptionCatalog(
|
||||
params.buildNpcStory(
|
||||
nextState,
|
||||
params.character,
|
||||
nextState.currentEncounter,
|
||||
).options,
|
||||
)
|
||||
: null;
|
||||
fallbackState = nextState;
|
||||
params.setGameState(nextState);
|
||||
if (
|
||||
nextState.currentEncounter &&
|
||||
params.handleNpcBattleConversationContinuation({
|
||||
nextState,
|
||||
encounter: nextState.currentEncounter,
|
||||
character: params.character,
|
||||
actionText: params.option.actionText,
|
||||
resultText: victory.resultText,
|
||||
battleMode: baseChoiceState.currentNpcBattleMode!,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const nextStory = await params.generateStoryForState({
|
||||
state: nextState,
|
||||
character: params.character,
|
||||
history: nextHistory,
|
||||
choice: params.option.actionText,
|
||||
lastFunctionId: params.option.functionId,
|
||||
optionCatalog: postBattleOptionCatalog,
|
||||
});
|
||||
const recoveredState = applyStoryReasoningRecovery(nextState);
|
||||
params.setGameState(recoveredState);
|
||||
params.setCurrentStory(nextStory);
|
||||
} catch (storyError) {
|
||||
console.error(
|
||||
'Failed to continue npc battle resolution story:',
|
||||
storyError,
|
||||
);
|
||||
params.setAiError(
|
||||
storyError instanceof Error
|
||||
? storyError.message
|
||||
: '未知智能生成错误',
|
||||
);
|
||||
params.setCurrentStory(
|
||||
params.buildFallbackStoryForState(
|
||||
nextState,
|
||||
params.character,
|
||||
victory.resultText,
|
||||
),
|
||||
);
|
||||
}
|
||||
const postBattleState = buildPostBattleVictoryState(nextState);
|
||||
const postBattle = buildPostBattleVictoryStory(
|
||||
postBattleState,
|
||||
victory.resultText,
|
||||
params.getAvailableOptionsForState(postBattleState, params.character) ?? [],
|
||||
);
|
||||
fallbackState = postBattle.state;
|
||||
params.setGameState(postBattle.state);
|
||||
params.setCurrentStory(postBattle.story);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUseDeterministicCombatFlow) {
|
||||
const defeatedHostileNpcIds =
|
||||
resolvedChoice.optionKind === 'escape' || baseChoiceState.currentBattleNpcId
|
||||
? []
|
||||
: params
|
||||
.getResolvedSceneHostileNpcs(baseChoiceState)
|
||||
.map((hostileNpc) => hostileNpc.id)
|
||||
.filter(
|
||||
(hostileNpcId) =>
|
||||
!params
|
||||
.getResolvedSceneHostileNpcs(afterSequence)
|
||||
.some((hostileNpc) => hostileNpc.id === hostileNpcId),
|
||||
);
|
||||
const resultText = buildLocalCombatResultText({
|
||||
option: params.option,
|
||||
battlePlan: resolvedChoice.battlePlan,
|
||||
afterSequence,
|
||||
combatResolutionContextText,
|
||||
});
|
||||
const nextHistory = [
|
||||
...baseChoiceState.storyHistory,
|
||||
createHistoryMoment(params.option.actionText, 'action'),
|
||||
createHistoryMoment(resultText, 'result'),
|
||||
];
|
||||
const nextState = params.incrementRuntimeStats(
|
||||
{
|
||||
...params.updateQuestLog(afterSequence, (quests) =>
|
||||
applyQuestProgressFromHostileNpcDefeat(
|
||||
quests,
|
||||
baseChoiceState.currentScenePreset?.id ?? null,
|
||||
defeatedHostileNpcIds,
|
||||
),
|
||||
),
|
||||
storyHistory: nextHistory,
|
||||
},
|
||||
{
|
||||
hostileNpcsDefeated: defeatedHostileNpcIds.length,
|
||||
},
|
||||
);
|
||||
|
||||
if (projectedBattleReward) {
|
||||
params.setBattleReward(projectedBattleReward);
|
||||
}
|
||||
|
||||
if (nextState.playerHp <= 0) {
|
||||
const deathState = {
|
||||
...nextState,
|
||||
animationState: AnimationState.DIE,
|
||||
playerActionMode: 'idle' as const,
|
||||
inBattle: false,
|
||||
activeCombatEffects: [],
|
||||
scrollWorld: false,
|
||||
};
|
||||
fallbackState = deathState;
|
||||
params.setGameState(deathState);
|
||||
await sleep(PLAYER_REVIVE_DELAY_MS);
|
||||
const revivedState = {
|
||||
...buildRevivedFirstSceneState(deathState),
|
||||
storyHistory: [
|
||||
...nextHistory,
|
||||
createHistoryMoment('你在第一个场景第一幕重新醒来。', 'result'),
|
||||
],
|
||||
};
|
||||
fallbackState = revivedState;
|
||||
params.setGameState(revivedState);
|
||||
params.setCurrentStory(buildDeathStory(revivedState));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
resolvedChoice.optionKind === 'battle' &&
|
||||
(!nextState.inBattle || nextState.currentNpcBattleOutcome === 'spar_complete')
|
||||
) {
|
||||
const postBattleState = buildPostBattleVictoryState(nextState);
|
||||
const postBattle = buildPostBattleVictoryStory(
|
||||
postBattleState,
|
||||
resultText,
|
||||
params.getAvailableOptionsForState(postBattleState, params.character) ?? [],
|
||||
);
|
||||
fallbackState = postBattle.state;
|
||||
params.setGameState(postBattle.state);
|
||||
params.setCurrentStory(postBattle.story);
|
||||
return;
|
||||
}
|
||||
|
||||
const availableOptions = params.getAvailableOptionsForState(
|
||||
nextState,
|
||||
params.character,
|
||||
);
|
||||
fallbackState = nextState;
|
||||
params.setGameState(nextState);
|
||||
params.setCurrentStory(
|
||||
buildDeterministicStoryForState({
|
||||
state: nextState,
|
||||
character: params.character,
|
||||
resultText,
|
||||
availableOptions,
|
||||
buildFallbackStoryForState: params.buildFallbackStoryForState,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseResult.status === 'rejected') {
|
||||
throw responseResult.reason;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user