202 lines
6.6 KiB
TypeScript
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
|
|
>;
|