Files
Genarrative/src/hooks/rpg-session/useRpgRuntimeSession.ts
T
2026-04-28 19:36:39 +08:00

215 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from 'react';
import { DEFAULT_MUSIC_VOLUME } from '../../../packages/shared/src/contracts/runtime';
import { useAuthUi } from '../../components/auth/AuthUiContext';
import type { CustomWorldRuntimeLaunchOptions } from '../../components/platform-entry/platformEntryTypes';
import type { RpgRuntimeShellProps } from '../../components/rpg-runtime-shell/types';
import {
activateRosterCompanion,
benchActiveCompanion,
} from '../../data/companionRoster';
import { syncGameStatePlayTime } from '../../data/runtimeStats';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import { useBackgroundMusic } from '../useBackgroundMusic';
import { useCombatFlow } from '../useCombatFlow';
import { useNpcInteractionFlow } from '../useNpcInteractionFlow';
import { useRpgRuntimeStory } from '../rpg-runtime-story/useRpgRuntimeStory';
import { useRpgSessionBootstrap } from './useRpgSessionBootstrap';
import { useRpgSessionPersistence } from './useRpgSessionPersistence';
/**
* RPG 主运行态装配器真实实现。
* 工作包 C 起主链改为组合 `rpg-session` 下的 bootstrap / persistence 新入口。
*/
export function useRpgRuntimeSession(): RpgRuntimeShellProps {
const authUi = useAuthUi();
const {
gameState,
setGameState,
bottomTab,
setBottomTab,
isMapOpen,
setIsMapOpen,
resetGame,
handleCustomWorldSelect: selectCustomWorld,
handleBackToWorldSelect: backToWorldSelect,
handleCharacterSelect: selectCharacter,
} = useRpgSessionBootstrap();
// 中文注释:战斗播放与结算仍然沿用独立 combat flow
// runtime session 只消费它暴露出来的“选项结算结果”和“动画播放入口”。
const combatFlow = useCombatFlow({
setGameState,
});
// 中文注释:剧情流是运行时主链的另一半。
// 这里把 GameState 交给 runtime story,由它负责剧情文本、选项、NPC 交互与任务 UI。
const storyFlow = useRpgRuntimeStory({
gameState,
setGameState,
buildResolvedChoiceState: combatFlow.buildResolvedChoiceState,
playResolvedChoice: combatFlow.playResolvedChoice,
});
const { companionRenderStates, buildCompanionRenderStates } =
useNpcInteractionFlow(gameState);
// 中文注释:持久化层统一负责继续游戏、自动存档与退出保存,
// session 只把当前运行态快照和 story 水位传进去。
const persistence = useRpgSessionPersistence({
authenticatedUserId: authUi?.user?.id ?? null,
gameState,
bottomTab,
currentStory: storyFlow.currentStory,
isLoading: storyFlow.isLoading,
setGameState,
setBottomTab,
hydrateStoryState: storyFlow.hydrateStoryState,
resetStoryState: storyFlow.resetStoryState,
});
useBackgroundMusic({
active: Boolean(
gameState.playerCharacter && gameState.currentScene === 'Story',
),
volume: authUi?.musicVolume ?? DEFAULT_MUSIC_VOLUME,
});
useEffect(() => {
if (!gameState.playerCharacter || gameState.currentScene !== 'Story') {
return;
}
// 中文注释:游玩时长统计不跟每一帧绑定,而是固定 15 秒增量同步,
// 这样既能累计活跃时长,也不会因为高频 setState 拉高运行态噪音。
const intervalId = window.setInterval(() => {
setGameState((currentState) => {
if (
!currentState.playerCharacter ||
currentState.currentScene !== 'Story'
) {
return currentState;
}
return syncGameStatePlayTime(currentState);
});
}, 15000);
return () => window.clearInterval(intervalId);
}, [gameState.currentScene, gameState.playerCharacter, setGameState]);
const handleCustomWorldSelect = (
customWorldProfile: Parameters<typeof selectCustomWorld>[0],
options?: CustomWorldRuntimeLaunchOptions,
) => {
// 中文注释:切换世界前先清空上一局 story 控制器,
// 避免旧世界的 currentStory / 选项残留到新开局。
storyFlow.resetStoryState();
selectCustomWorld(customWorldProfile, {
mode: options?.mode,
disablePersistence: options?.disablePersistence,
});
};
const handleCharacterSelect = (
character: Parameters<typeof selectCharacter>[0],
) => {
// 中文注释:角色确认意味着正式进入新 run,
// 这里同样先清理 story 层,保证开场剧情重新按当前角色生成。
storyFlow.resetStoryState();
selectCharacter(character);
};
const handleBackToWorldSelect = () => {
storyFlow.resetStoryState();
backToWorldSelect();
};
const handleContinueGame = (snapshot?: HydratedSavedGameSnapshot | null) => {
// 中文注释:继续游戏是异步恢复链,内部会重新向服务端刷新 runtime story
// 所以这里显式丢给 persistence 异步执行。
void persistence.continueSavedGame(snapshot);
};
const handleStartNewGame = () => {
void persistence.clearSavedGame();
storyFlow.resetStoryState();
resetGame();
};
const handleSaveAndExit = () => {
// 中文注释:退出保存只请求服务端基于已存快照创建 checkpoint
// 游玩时长的最终刷新由后端 checkpoint 负责,不再上传本地同步后的 GameState。
void persistence.saveCurrentGame({
bottomTab,
currentStory: storyFlow.currentStory,
});
storyFlow.resetStoryState();
resetGame();
};
const handleBenchCompanion = (npcId: string) => {
setGameState((currentState) => benchActiveCompanion(currentState, npcId));
};
const handleActivateRosterCompanion = (
npcId: string,
swapNpcId?: string | null,
) => {
setGameState((currentState) =>
activateRosterCompanion(currentState, npcId, swapNpcId),
);
};
return {
session: {
gameState,
currentStory: storyFlow.currentStory,
isLoading: storyFlow.isLoading,
aiError: storyFlow.aiError,
bottomTab,
setBottomTab,
isMapOpen,
setIsMapOpen,
},
story: {
displayedOptions: storyFlow.displayedOptions,
canRefreshOptions: storyFlow.canRefreshOptions,
handleRefreshOptions: storyFlow.handleRefreshOptions,
handleChoice: storyFlow.handleChoice,
handleNpcChatInput: storyFlow.handleNpcChatInput,
refreshNpcChatOptions: storyFlow.refreshNpcChatOptions,
exitNpcChat: storyFlow.exitNpcChat,
handleMapTravelToScene: storyFlow.travelToSceneFromMap,
npcUi: storyFlow.npcUi,
characterChatUi: storyFlow.characterChatUi,
inventoryUi: storyFlow.inventoryUi,
battleRewardUi: storyFlow.battleRewardUi,
questUi: storyFlow.questUi,
npcChatQuestOfferUi: storyFlow.npcChatQuestOfferUi,
goalUi: storyFlow.goalUi,
},
entry: {
hasSavedGame: persistence.hasSavedGame,
savedSnapshot: persistence.savedSnapshot,
handleContinueGame,
handleStartNewGame,
handleSaveAndExit,
handleCustomWorldSelect,
handleBackToWorldSelect,
handleCharacterSelect,
},
companions: {
companionRenderStates,
buildCompanionRenderStates,
onBenchCompanion: handleBenchCompanion,
onActivateRosterCompanion: handleActivateRosterCompanion,
},
audio: {
musicVolume: authUi?.musicVolume ?? DEFAULT_MUSIC_VOLUME,
onMusicVolumeChange: authUi?.setMusicVolume ?? (() => {}),
},
};
}
export type RpgRuntimeSessionResult = ReturnType<typeof useRpgRuntimeSession>;