Files
Genarrative/src/hooks/rpg-session/useRpgSessionBootstrap.ts
T

202 lines
6.6 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
buildCustomWorldRuntimeCharacters,
setRuntimeCharacterOverrides,
} from '../../data/characterPresets';
import { setRuntimeCustomWorldProfile } from '../../data/customWorldRuntime';
import { createEmptyEquipmentLoadout } from '../../data/equipmentEffects';
import { createInitialPlayerProgressionState } from '../../data/playerProgression';
import { createInitialGameRuntimeStats } from '../../data/runtimeStats';
import { getScenePreset } from '../../data/scenePresets';
import { beginRuntimeStorySession } from '../../services/rpg-runtime/rpgRuntimeStoryClient';
import { createEmptyStoryEngineMemoryState } from '../../services/storyEngine/visibilityEngine';
import {
AnimationState,
Character,
CustomWorldProfile,
GameState,
GameRuntimeMode,
WorldType,
} from '../../types';
import type { BottomTab } from './rpgSessionTypes';
const PLAYER_BASE_MAX_HP = 180;
function createSelectionGameState(): GameState {
return {
worldType: null,
customWorldProfile: null,
playerCharacter: null,
runtimeStats: createInitialGameRuntimeStats(),
playerProgression: createInitialPlayerProgressionState(),
currentScene: 'Selection',
storyHistory: [],
storyEngineMemory: createEmptyStoryEngineMemoryState(),
chapterState: null,
campaignState: null,
activeScenarioPackId: null,
activeCampaignPackId: null,
characterChats: {},
lastObserveSignsSceneId: null,
lastObserveSignsReport: null,
animationState: AnimationState.IDLE,
currentEncounter: null,
npcInteractionActive: false,
currentScenePreset: null,
sceneHostileNpcs: [],
playerX: 0,
playerOffsetY: 0,
playerFacing: 'right',
playerActionMode: 'idle',
scrollWorld: false,
inBattle: false,
playerHp: PLAYER_BASE_MAX_HP,
playerMaxHp: PLAYER_BASE_MAX_HP,
playerMana: 0,
playerMaxMana: 0,
playerSkillCooldowns: {},
activeBuildBuffs: [],
activeCombatEffects: [],
playerCurrency: 0,
playerInventory: [],
playerEquipment: createEmptyEquipmentLoadout(),
npcStates: {},
quests: [],
roster: [],
companions: [],
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
};
}
/**
* RPG session bootstrap 主实现。
* 工作包 C 起由新域 hook 承载世界选择、选角确认与新开局初始化。
*/
export function useRpgSessionBootstrap() {
const [gameState, setGameState] = useState<GameState>(() =>
createSelectionGameState(),
);
const [bottomTab, setBottomTab] = useState<BottomTab>('adventure');
const [isMapOpen, setIsMapOpen] = useState(false);
useEffect(() => {
// 中文注释:当前运行中的自定义世界 profile 需要同步给静态数据层,
// 这样角色预设、场景预设、运行时引用解析才能读取到同一份世界真相。
setRuntimeCustomWorldProfile(gameState.customWorldProfile);
setRuntimeCharacterOverrides(
gameState.customWorldProfile
? buildCustomWorldRuntimeCharacters(gameState.customWorldProfile)
: null,
);
}, [gameState.customWorldProfile]);
const resetGame = () => {
// 中文注释:reset 不只清 GameState,还要把底部 tab 和地图弹层一起还原,
// 避免返回入口后 UI 仍停留在上一次冒险的局部状态。
setBottomTab('adventure');
setIsMapOpen(false);
setGameState(createSelectionGameState());
};
const handleCustomWorldSelect = (
customWorldProfile: CustomWorldProfile,
options?: { mode?: GameRuntimeMode; disablePersistence?: boolean },
) => {
const resolvedWorldType = WorldType.CUSTOM;
const runtimeMode: GameRuntimeMode = 'play';
const runtimePersistenceDisabled =
options?.disablePersistence ?? false;
setRuntimeCustomWorldProfile(customWorldProfile);
setRuntimeCharacterOverrides(
buildCustomWorldRuntimeCharacters(customWorldProfile),
);
const initialScenePreset = getScenePreset(resolvedWorldType, 0) ?? null;
setIsMapOpen(false);
setGameState((prev) => ({
// 中文注释:世界刚选中时只进入“已装入世界,但尚未选角”的中间态;
// 正式开局 GameState 必须等待角色确认后由 server-rs 统一生成。
...prev,
worldType: resolvedWorldType,
customWorldProfile,
runtimeMode,
runtimePersistenceDisabled,
currentScenePreset: initialScenePreset,
sceneHostileNpcs: [],
currentEncounter: null,
npcInteractionActive: false,
playerProgression: createInitialPlayerProgressionState(),
storyEngineMemory: createEmptyStoryEngineMemoryState(),
chapterState: null,
campaignState: null,
activeScenarioPackId: customWorldProfile?.scenarioPackId ?? null,
activeCampaignPackId: customWorldProfile?.campaignPackId ?? null,
lastObserveSignsSceneId: null,
lastObserveSignsReport: null,
playerActionMode: 'idle',
activeCombatEffects: [],
activeBuildBuffs: [],
inBattle: false,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
}));
};
const handleBackToWorldSelect = () => {
setBottomTab('adventure');
setIsMapOpen(false);
setGameState(createSelectionGameState());
};
const handleCharacterSelect = (character: Character) => {
setBottomTab('adventure');
setIsMapOpen(false);
const launchState = gameState;
const resolvedWorldType = launchState.worldType;
if (!resolvedWorldType) {
return;
}
void beginRuntimeStorySession({
worldType: resolvedWorldType,
customWorldProfile: launchState.customWorldProfile,
character,
runtimeMode: launchState.runtimeMode ?? 'play',
disablePersistence: launchState.runtimePersistenceDisabled === true,
}).then((response) => {
// 中文注释:开局正式 GameState 由 server-rs 生成并持久化;
// 前端只接收后端快照,避免浏览器继续承担初始背包、装备、遭遇和 NPC 状态裁决。
setGameState(response.snapshot.gameState);
});
};
return {
gameState,
setGameState,
bottomTab,
setBottomTab,
isMapOpen,
setIsMapOpen,
resetGame,
handleCustomWorldSelect,
handleBackToWorldSelect,
handleCharacterSelect,
};
}
export type RpgSessionBootstrapResult = ReturnType<
typeof useRpgSessionBootstrap
>;