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,289 @@
import { useCallback, useEffect } from 'react';
import type {
Character,
Encounter,
GameState,
StoryMoment,
StoryOption,
} from '../../types';
import type { EscapePlaybackSync as ResolvedChoicePlaybackSync } from '../combat/escapeFlow';
import type { ResolvedChoiceState } from '../combat/resolvedChoice';
import { useTreasureFlow } from '../useTreasureFlow';
import { useStoryInventoryActions } from './inventoryActions';
import { useStoryNpcInteractionFlow } from './npcInteraction';
import type {
ChoiceRuntimeController,
ChoiceRuntimeSupport,
StoryChoiceCoordinatorParams,
} from './storyChoiceCoordinator';
import type { StoryInteractionCoordinatorConfig } from './storyInteractionCoordinator';
import type { StoryRuntimeSupport } from './storyRuntimeSupport';
import { createRpgRuntimeNpcEncounterActions } from './useRpgRuntimeNpcInteraction';
import { useStoryChoiceCoordinator } from './useStoryChoiceCoordinator';
type RpgRuntimeInteractionFlowParams = {
gameState: GameState;
isLoading: boolean;
interactionConfig: StoryInteractionCoordinatorConfig;
runtimeSupport: StoryRuntimeSupport;
buildResolvedChoiceState: (
state: GameState,
option: StoryOption,
character: Character,
) => ResolvedChoiceState;
playResolvedChoice: (
state: GameState,
option: StoryOption,
character: Character,
resolvedChoice: ResolvedChoiceState,
sync?: ResolvedChoicePlaybackSync,
) => Promise<GameState>;
buildStoryFromResponse: ChoiceRuntimeController['buildStoryFromResponse'];
getResolvedSceneHostileNpcs: (
state: GameState,
) => GameState['sceneHostileNpcs'];
getCampCompanionTravelScene: StoryChoiceCoordinatorParams['runtimeController']['getCampCompanionTravelScene'];
isContinueAdventureOption: (option: StoryOption) => boolean;
isCampTravelHomeOption: (option: StoryOption) => boolean;
isRegularNpcEncounter: (
encounter: GameState['currentEncounter'],
) => encounter is Encounter;
isNpcEncounter: (
encounter: GameState['currentEncounter'],
) => encounter is Encounter;
npcPreviewTalkFunctionId: string;
fallbackCompanionName: string;
turnVisualMs: number;
};
export function createClearStoryInteractionUi(params: {
clearStoryChoiceUi: () => void;
clearNpcInteractionUi: () => void;
}) {
return () => {
params.clearStoryChoiceUi();
params.clearNpcInteractionUi();
};
}
/**
* RPG runtime 交互分发层。
* 统一串起宝箱、背包、NPC 交互与 story choice 的正式分发。
*/
export function useRpgRuntimeInteractionFlow({
gameState,
isLoading,
interactionConfig,
runtimeSupport,
buildResolvedChoiceState,
playResolvedChoice,
buildStoryFromResponse,
getResolvedSceneHostileNpcs,
getCampCompanionTravelScene,
isContinueAdventureOption,
isCampTravelHomeOption,
isRegularNpcEncounter,
isNpcEncounter,
npcPreviewTalkFunctionId,
fallbackCompanionName,
turnVisualMs,
}: RpgRuntimeInteractionFlowParams) {
const { handleTreasureInteraction } = useTreasureFlow(
interactionConfig.treasureFlow,
);
const { inventoryUi } = useStoryInventoryActions(
interactionConfig.inventoryFlow,
);
const npcInteractionFlow = useStoryNpcInteractionFlow(
interactionConfig.npcInteractionFlow,
);
const {
enterNpcInteraction,
handleNpcInteraction,
finalizeNpcBattleResult,
reopenNpcChatAfterBattle,
handleNpcChatTurn,
exitNpcChat,
replacePendingNpcQuestOffer,
abandonPendingNpcQuestOffer,
acceptPendingNpcQuestOffer,
} = createRpgRuntimeNpcEncounterActions({
...interactionConfig.npcEncounterActions,
buildNpcStory: runtimeSupport.buildNpcStory,
npcInteractionFlow,
});
useEffect(() => {
if (isLoading || gameState.inBattle || gameState.npcInteractionActive) {
return;
}
if (isNpcEncounter(gameState.currentEncounter)) {
enterNpcInteraction(
gameState.currentEncounter,
`${gameState.currentEncounter.npcName}搭话`,
);
}
}, [
enterNpcInteraction,
gameState.currentEncounter,
gameState.inBattle,
gameState.npcInteractionActive,
isLoading,
isNpcEncounter,
]);
const choiceRuntimeController: Parameters<
typeof useStoryChoiceCoordinator
>[0]['runtimeController'] = {
currentStory: interactionConfig.npcEncounterActions.currentStory,
buildStoryContextFromState:
interactionConfig.npcEncounterActions.buildStoryContextFromState,
buildStoryFromResponse: (
state: GameState,
character: Character,
response: StoryMoment,
availableOptions: StoryOption[] | null,
optionCatalog?: StoryOption[] | null,
) =>
buildStoryFromResponse(
state,
character,
response,
availableOptions,
optionCatalog,
),
buildFallbackStoryForState:
interactionConfig.npcEncounterActions.buildFallbackStoryForState,
generateStoryForState: async (params) =>
interactionConfig.npcEncounterActions.generateStoryForState(params),
getAvailableOptionsForState:
interactionConfig.npcEncounterActions.getAvailableOptionsForState,
getCampCompanionTravelScene: (state, character) =>
getCampCompanionTravelScene(state, character),
commitGeneratedStateWithEncounterEntry: async (
entryState,
resolvedState,
character,
actionText,
resultText,
lastFunctionId,
) => {
await interactionConfig.npcEncounterActions.commitGeneratedStateWithEncounterEntry(
entryState,
resolvedState,
character,
actionText,
resultText,
lastFunctionId,
);
},
};
const choiceRuntimeSupport: ChoiceRuntimeSupport = {
...runtimeSupport,
handleNpcBattleConversationContinuation: ({
nextState,
encounter,
actionText,
resultText,
battleMode,
}) =>
reopenNpcChatAfterBattle({
nextState,
encounter,
actionText,
resultText,
battleMode,
}),
};
const { handleChoice, battleRewardUi, clearStoryChoiceUi } =
useStoryChoiceCoordinator({
gameState,
isLoading,
setGameState: interactionConfig.npcEncounterActions.setGameState,
setCurrentStory: interactionConfig.npcEncounterActions.setCurrentStory,
setAiError: interactionConfig.npcEncounterActions.setAiError,
setIsLoading: interactionConfig.npcEncounterActions.setIsLoading,
buildResolvedChoiceState,
playResolvedChoice,
getStoryGenerationHostileNpcs:
interactionConfig.npcEncounterActions.getStoryGenerationHostileNpcs,
getResolvedSceneHostileNpcs,
runtimeController: choiceRuntimeController,
runtimeSupport: choiceRuntimeSupport,
enterNpcInteraction,
handleNpcInteraction,
handleTreasureInteraction,
finalizeNpcBattleResult,
sortOptions: interactionConfig.npcEncounterActions.sortOptions,
buildContinueAdventureOption:
interactionConfig.npcEncounterActions.buildContinueAdventureOption,
isContinueAdventureOption,
isCampTravelHomeOption,
isRegularNpcEncounter,
isNpcEncounter,
npcPreviewTalkFunctionId,
fallbackCompanionName,
turnVisualMs,
});
const clearStoryInteractionUi = useCallback(
createClearStoryInteractionUi({
clearStoryChoiceUi,
clearNpcInteractionUi: npcInteractionFlow.clearNpcInteractionUi,
}),
[clearStoryChoiceUi, npcInteractionFlow.clearNpcInteractionUi],
);
return {
handleChoice,
battleRewardUi,
npcUi: npcInteractionFlow.npcUi,
inventoryUi,
clearStoryInteractionUi,
handleNpcChatInput: (input: string) => {
const encounter = interactionConfig.npcEncounterActions.gameState.currentEncounter;
if (!encounter || encounter.kind !== 'npc') {
return false;
}
void handleNpcChatTurn(encounter, input);
return true;
},
refreshNpcChatOptions: () => {
const story = interactionConfig.npcEncounterActions.currentStory;
const encounter = interactionConfig.npcEncounterActions.gameState.currentEncounter;
if (!story?.npcChatState || !story.options.length || !encounter || encounter.kind !== 'npc') {
return false;
}
const [firstOption, ...restOptions] = story.options;
if (!firstOption || restOptions.length === 0) {
return false;
}
interactionConfig.npcEncounterActions.setCurrentStory({
...story,
options: [...restOptions, firstOption],
});
return true;
},
exitNpcChat,
npcChatQuestOfferUi: {
replacePendingOffer: replacePendingNpcQuestOffer,
abandonPendingOffer: abandonPendingNpcQuestOffer,
acceptPendingOffer: acceptPendingNpcQuestOffer,
},
};
}
export type UseRpgRuntimeInteractionFlowParams = Parameters<
typeof useRpgRuntimeInteractionFlow
>[0];
export type RpgRuntimeInteractionFlowResult = ReturnType<
typeof useRpgRuntimeInteractionFlow
>;
export const createRpgRuntimeInteractionUiResetter =
createClearStoryInteractionUi;