init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,430 @@
import { addInventoryItems } from '../../data/npcInteractions';
import { applyQuestProgressFromHostileNpcDefeat } from '../../data/questFlow';
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
import { generateNextStep } from '../../services/aiService';
import type { StoryGenerationContext } from '../../services/aiTypes';
import { appendStoryEngineCarrierMemory } from '../../services/storyEngine/echoMemory';
import { createHistoryMoment } from '../../services/storyHistory';
import type {
Character,
Encounter,
GameState,
StoryMoment,
StoryOption,
} from '../../types';
import type { EscapePlaybackSync } from '../combat/escapeFlow';
import type { ResolvedChoiceState } from '../combat/resolvedChoice';
import {
buildCombatResolutionContextText,
buildHostileNpcBattleReward,
buildReasonedOptionCatalog,
} from './storyChoiceRuntime';
import type { BattleRewardSummary } from './uiTypes';
type RuntimeStatsIncrements = Partial<
Pick<
GameState['runtimeStats'],
'hostileNpcsDefeated' | 'questsAccepted' | 'itemsUsed' | 'scenesTraveled'
>
>;
type BuildFallbackStoryForState = (
state: GameState,
character: Character,
fallbackText?: string,
) => StoryMoment;
type BuildStoryFromResponse = (
state: GameState,
character: Character,
response: StoryMoment,
availableOptions: StoryOption[] | null,
optionCatalog?: StoryOption[] | null,
) => StoryMoment;
type BuildNpcStory = (
state: GameState,
character: Character,
encounter: Encounter,
overrideText?: string,
) => StoryMoment;
type HandleNpcBattleConversationContinuation = (params: {
nextState: GameState;
encounter: Encounter;
character: Character;
actionText: string;
resultText: string;
battleMode: NonNullable<GameState['currentNpcBattleMode']>;
}) => boolean;
type BuildStoryContextFromState = (
state: GameState,
extras?: {
lastFunctionId?: string | null;
observeSignsRequested?: boolean;
recentActionResult?: string | null;
},
) => StoryGenerationContext;
type UpdateQuestLog = (
state: GameState,
updater: (quests: GameState['quests']) => GameState['quests'],
) => GameState;
type IncrementRuntimeStats = (
state: GameState,
increments: RuntimeStatsIncrements,
) => GameState;
export async function runLocalStoryChoiceContinuation(params: {
gameState: GameState;
currentStory: StoryMoment | null;
option: StoryOption;
character: Character;
setGameState: (state: GameState) => void;
setCurrentStory: (story: StoryMoment) => void;
setAiError: (message: string | null) => void;
setIsLoading: (loading: boolean) => void;
setBattleReward: (reward: BattleRewardSummary | null) => void;
buildResolvedChoiceState: (
state: GameState,
option: StoryOption,
character: Character,
) => ResolvedChoiceState;
playResolvedChoice: (
state: GameState,
option: StoryOption,
character: Character,
resolvedChoice: ResolvedChoiceState,
sync?: EscapePlaybackSync,
) => Promise<GameState>;
buildStoryContextFromState: BuildStoryContextFromState;
buildStoryFromResponse: BuildStoryFromResponse;
buildFallbackStoryForState: BuildFallbackStoryForState;
generateStoryForState: (params: {
state: GameState;
character: Character;
history: StoryMoment[];
choice?: string;
lastFunctionId?: string | null;
optionCatalog?: StoryOption[] | null;
}) => Promise<StoryMoment>;
getAvailableOptionsForState: (
state: GameState,
character: Character,
) => StoryOption[] | null;
getStoryGenerationHostileNpcs: (
state: GameState,
) => GameState['sceneHostileNpcs'];
getResolvedSceneHostileNpcs: (
state: GameState,
) => GameState['sceneHostileNpcs'];
buildNpcStory: BuildNpcStory;
handleNpcBattleConversationContinuation: HandleNpcBattleConversationContinuation;
updateQuestLog: UpdateQuestLog;
incrementRuntimeStats: IncrementRuntimeStats;
finalizeNpcBattleResult: (
state: GameState,
character: Character,
battleMode: NonNullable<GameState['currentNpcBattleMode']>,
battleOutcome: GameState['currentNpcBattleOutcome'],
) => { nextState: GameState; resultText: string } | null;
isRegularNpcEncounter: (
encounter: GameState['currentEncounter'],
) => encounter is Encounter;
}) {
params.setBattleReward(null);
params.setAiError(null);
params.setIsLoading(true);
const baseChoiceState =
params.isRegularNpcEncounter(params.gameState.currentEncounter) &&
!params.gameState.npcInteractionActive &&
!params.option.interaction
? {
...params.gameState,
currentEncounter: null,
npcInteractionActive: false,
}
: params.gameState;
let fallbackState = baseChoiceState;
try {
const history = baseChoiceState.storyHistory;
const resolvedChoice = params.buildResolvedChoiceState(
baseChoiceState,
params.option,
params.character,
);
const projectedState = resolvedChoice.afterSequence;
const shouldUseLocalNpcVictory = Boolean(
baseChoiceState.currentBattleNpcId &&
resolvedChoice.optionKind === 'battle' &&
(projectedState.currentNpcBattleOutcome ||
(baseChoiceState.currentNpcBattleMode === 'fight' &&
!projectedState.inBattle)),
);
const projectedBattleReward = shouldUseLocalNpcVictory
? null
: await buildHostileNpcBattleReward(
baseChoiceState,
projectedState,
resolvedChoice.optionKind,
params.getResolvedSceneHostileNpcs,
);
const projectedStateWithBattleReward = projectedBattleReward
? appendStoryEngineCarrierMemory(
{
...projectedState,
playerInventory: addInventoryItems(
projectedState.playerInventory,
projectedBattleReward.items,
),
} as GameState,
projectedBattleReward.items,
)
: projectedState;
fallbackState = projectedStateWithBattleReward;
const projectedAvailableOptions = params.getAvailableOptionsForState(
projectedStateWithBattleReward,
params.character,
);
const combatResolutionContextText = buildCombatResolutionContextText({
baseState: baseChoiceState,
afterSequence: projectedStateWithBattleReward,
optionKind: resolvedChoice.optionKind,
projectedBattleReward,
getResolvedSceneHostileNpcs: params.getResolvedSceneHostileNpcs,
});
const historyForStoryGeneration = combatResolutionContextText
? [
...history,
createHistoryMoment(params.option.actionText, 'action'),
createHistoryMoment(combatResolutionContextText, 'result'),
]
: history;
const responsePromise = shouldUseLocalNpcVictory
? Promise.resolve(null)
: generateNextStep(
params.gameState.worldType!,
params.character,
params.getStoryGenerationHostileNpcs(projectedStateWithBattleReward),
historyForStoryGeneration,
params.option.actionText,
params.buildStoryContextFromState(projectedStateWithBattleReward, {
lastFunctionId: params.option.functionId,
observeSignsRequested:
params.option.functionId === 'idle_observe_signs',
recentActionResult: combatResolutionContextText,
}),
projectedAvailableOptions
? { availableOptions: projectedAvailableOptions }
: undefined,
);
const responseSettledPromise = responsePromise.then(
() => undefined,
() => undefined,
);
const playbackSync: EscapePlaybackSync | undefined =
resolvedChoice.optionKind === 'escape'
? { waitForStoryResponse: responseSettledPromise }
: undefined;
const actionPromise = params.playResolvedChoice(
baseChoiceState,
params.option,
params.character,
resolvedChoice,
playbackSync,
);
const [actionResult, responseResult] = await Promise.allSettled([
actionPromise,
responsePromise,
]);
if (actionResult.status === 'rejected') {
throw actionResult.reason;
}
let afterSequence = shouldUseLocalNpcVictory
? resolvedChoice.afterSequence
: actionResult.value;
if (projectedBattleReward) {
afterSequence = appendStoryEngineCarrierMemory(
{
...afterSequence,
playerInventory: addInventoryItems(
afterSequence.playerInventory,
projectedBattleReward.items,
),
} as GameState,
projectedBattleReward.items,
);
}
fallbackState = afterSequence;
if (shouldUseLocalNpcVictory) {
const victory = params.finalizeNpcBattleResult(
afterSequence,
params.character,
baseChoiceState.currentNpcBattleMode!,
afterSequence.currentNpcBattleOutcome,
);
if (victory) {
const historyBase =
baseChoiceState.currentNpcBattleMode === 'spar'
? (afterSequence.sparStoryHistoryBefore ?? [])
: baseChoiceState.storyHistory;
const nextHistory = [
...historyBase,
createHistoryMoment(params.option.actionText, 'action'),
createHistoryMoment(victory.resultText, 'result'),
];
const nextState = {
...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,
),
);
}
return;
}
}
if (responseResult.status === 'rejected') {
throw responseResult.reason;
}
const response = responseResult.value!;
const defeatedHostileNpcIds =
baseChoiceState.currentBattleNpcId ||
resolvedChoice.optionKind === 'escape'
? []
: params
.getResolvedSceneHostileNpcs(baseChoiceState)
.map((hostileNpc) => hostileNpc.id)
.filter(
(hostileNpcId) =>
!params
.getResolvedSceneHostileNpcs(afterSequence)
.some((hostileNpc) => hostileNpc.id === hostileNpcId),
);
const nextHistory = combatResolutionContextText
? [
...historyForStoryGeneration,
createHistoryMoment(response.storyText, 'result', response.options),
]
: [
...baseChoiceState.storyHistory,
createHistoryMoment(params.option.actionText, 'action'),
createHistoryMoment(response.storyText, 'result', response.options),
];
const nextState = params.incrementRuntimeStats(
{
...params.updateQuestLog(afterSequence, (quests) =>
applyQuestProgressFromHostileNpcDefeat(
quests,
baseChoiceState.currentScenePreset?.id ?? null,
defeatedHostileNpcIds,
),
),
lastObserveSignsSceneId:
params.option.functionId === 'idle_observe_signs'
? (afterSequence.currentScenePreset?.id ?? null)
: afterSequence.lastObserveSignsSceneId ?? null,
lastObserveSignsReport:
params.option.functionId === 'idle_observe_signs'
? response.storyText
: afterSequence.lastObserveSignsReport ?? null,
storyHistory: nextHistory,
},
{
hostileNpcsDefeated: defeatedHostileNpcIds.length,
},
);
const recoveredState = applyStoryReasoningRecovery(nextState);
params.setGameState(recoveredState);
if (projectedBattleReward) {
params.setBattleReward(projectedBattleReward);
}
params.setCurrentStory(
params.buildStoryFromResponse(
recoveredState,
params.character,
{
text: response.storyText,
options: response.options,
},
projectedAvailableOptions,
),
);
} catch (error) {
console.error('Failed to get next step:', error);
params.setAiError(
error instanceof Error ? error.message : '未知智能生成错误',
);
params.setCurrentStory(
params.buildFallbackStoryForState(fallbackState, params.character),
);
} finally {
params.setIsLoading(false);
}
}