import { useEffect, useState } from 'react'; import { buildCustomWorldRuntimeCharacters, createCharacterSkillCooldowns, getCharacterMaxHp, getCharacterMaxMana, setRuntimeCharacterOverrides, } from '../../data/characterPresets'; import { setRuntimeCustomWorldProfile } from '../../data/customWorldRuntime'; import { getInitialPlayerCurrency } from '../../data/economy'; import { applyEquipmentLoadoutToState, buildInitialEquipmentLoadout, createEmptyEquipmentLoadout, } from '../../data/equipmentEffects'; import { buildInitialNpcState, buildInitialPlayerInventory, } from '../../data/npcInteractions'; import { createInitialPlayerProgressionState } from '../../data/playerProgression'; import { createInitialGameRuntimeStats } from '../../data/runtimeStats'; import { ensureSceneEncounterPreview, RESOLVED_ENTITY_X_METERS, } from '../../data/sceneEncounterPreviews'; import { getScenePreset, getWorldCampScenePreset } from '../../data/scenePresets'; import { createEmptyStoryEngineMemoryState } from '../../services/storyEngine/visibilityEngine'; import { AnimationState, Character, CustomWorldProfile, Encounter, EquipmentLoadout, GameState, InventoryItem, SceneNpc, WorldType, } from '../../types'; import type { BottomTab } from './rpgSessionTypes'; const PLAYER_BASE_MAX_HP = 180; function mergeStarterInventoryItems< T extends { category: string; name: string }, >(explicitItems: T[], fallbackItems: T[]) { const merged = new Map(); [...explicitItems, ...fallbackItems].forEach((item) => { merged.set(`${item.category}:${item.name}`, item); }); return [...merged.values()]; } function normalizeExplicitStarterCategory(category: string) { const normalized = category.trim(); return normalized === '专属物' ? '专属物品' : normalized; } function inferExplicitStarterSlot(category: string) { const normalized = normalizeExplicitStarterCategory(category); if (normalized === '武器') return 'weapon' as const; if (normalized === '护甲') return 'armor' as const; if ( normalized === '饰品' || normalized === '稀有品' || normalized === '专属物品' ) { return 'relic' as const; } return null; } function buildExplicitCustomWorldRoleStarterState( profile: CustomWorldProfile, character: Character, ) { const role = profile.playableNpcs.find((entry) => entry.id === character.id) ?? profile.storyNpcs.find((entry) => entry.id === character.id) ?? profile.playableNpcs.find((entry) => entry.name === character.name) ?? profile.storyNpcs.find((entry) => entry.name === character.name) ?? null; const inventory = role ? role.initialItems.map((item, index) => { const category = normalizeExplicitStarterCategory(item.category); return { id: `custom-role-item:${role.id}:${index + 1}`, category, name: item.name, quantity: Math.max(1, item.quantity), rarity: item.rarity, tags: [...item.tags], description: item.description, equipmentSlotId: inferExplicitStarterSlot(category), runtimeMetadata: { origin: 'ai_compiled' as const, generationChannel: 'discovery' as const, seedKey: `${role.id}:${index + 1}`, relationAnchor: { type: 'npc' as const, npcId: role.id, npcName: role.name, roleText: role.role, }, sourceReason: `${role.name}在自定义世界开局时自带的初始物品。`, }, } satisfies InventoryItem; }) : []; const equipment: EquipmentLoadout = createEmptyEquipmentLoadout(); inventory.forEach((item) => { const slot = item.equipmentSlotId; if (!slot || equipment[slot]) { return; } equipment[slot] = item; }); return { inventory, equipment, }; } function createInitialCampEncounter( worldType: WorldType | null, playerCharacter: Character, ): Encounter | null { if (!worldType) return null; const campScenePreset = getWorldCampScenePreset(worldType) ?? getScenePreset(worldType, 0); const npcCandidates = (campScenePreset?.npcs ?? []) .filter((npc: SceneNpc) => Boolean(npc.characterId)) .filter((npc: SceneNpc) => npc.characterId !== playerCharacter.id); if (npcCandidates.length === 0) return null; const npc = npcCandidates[Math.floor(Math.random() * npcCandidates.length)] ?? null; if (!npc) return null; return { id: npc.id, kind: 'npc', characterId: npc.characterId, npcName: npc.name, npcDescription: npc.description, npcAvatar: npc.avatar, context: npc.role, gender: npc.gender, xMeters: RESOLVED_ENTITY_X_METERS, }; } function createInitialGameState(): 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(() => createInitialGameState(), ); const [bottomTab, setBottomTab] = useState('adventure'); const [isMapOpen, setIsMapOpen] = useState(false); useEffect(() => { setRuntimeCustomWorldProfile(gameState.customWorldProfile); setRuntimeCharacterOverrides( gameState.customWorldProfile ? buildCustomWorldRuntimeCharacters(gameState.customWorldProfile) : null, ); }, [gameState.customWorldProfile]); const resetGame = () => { setBottomTab('adventure'); setIsMapOpen(false); setGameState(createInitialGameState()); }; const handleCustomWorldSelect = (customWorldProfile: CustomWorldProfile) => { const resolvedWorldType = WorldType.CUSTOM; setRuntimeCustomWorldProfile(customWorldProfile); setRuntimeCharacterOverrides( buildCustomWorldRuntimeCharacters(customWorldProfile), ); const initialScenePreset = getScenePreset(resolvedWorldType, 0) ?? null; setIsMapOpen(false); setGameState((prev) => ensureSceneEncounterPreview({ ...prev, worldType: resolvedWorldType, customWorldProfile, 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(createInitialGameState()); }; const handleCharacterSelect = (character: Character) => { setBottomTab('adventure'); setIsMapOpen(false); setGameState((prev) => { const resolvedWorldType = prev.worldType; const resolvedCustomWorldProfile = prev.customWorldProfile; const initialScenePreset = resolvedWorldType ? (getWorldCampScenePreset(resolvedWorldType) ?? getScenePreset(resolvedWorldType, 0)) : null; const initialEncounter = createInitialCampEncounter( resolvedWorldType, character, ); const initialNpcState = initialEncounter ? buildInitialNpcState(initialEncounter, resolvedWorldType, prev) : null; const initialEquipment = buildInitialEquipmentLoadout( character, resolvedCustomWorldProfile, ); const explicitStarterItems = resolvedWorldType === WorldType.CUSTOM ? buildExplicitCustomWorldRoleStarterState( resolvedCustomWorldProfile!, character, ) : null; const mergedStarterEquipment = { weapon: explicitStarterItems?.equipment.weapon ?? initialEquipment.weapon, armor: explicitStarterItems?.equipment.armor ?? initialEquipment.armor, relic: explicitStarterItems?.equipment.relic ?? initialEquipment.relic, }; const playerMaxHp = getCharacterMaxHp( character, resolvedWorldType, resolvedCustomWorldProfile, ); return ensureSceneEncounterPreview( applyEquipmentLoadoutToState( { ...prev, playerCharacter: character, runtimeStats: createInitialGameRuntimeStats({ isActiveRun: true }), playerProgression: createInitialPlayerProgressionState(), currentScene: 'Story', storyHistory: [], storyEngineMemory: createEmptyStoryEngineMemoryState(), chapterState: null, campaignState: null, activeScenarioPackId: prev.customWorldProfile?.scenarioPackId ?? null, activeCampaignPackId: prev.customWorldProfile?.campaignPackId ?? null, characterChats: {}, currentEncounter: initialEncounter, npcInteractionActive: false, currentScenePreset: initialScenePreset, lastObserveSignsSceneId: null, lastObserveSignsReport: null, animationState: AnimationState.IDLE, sceneHostileNpcs: [], playerX: 0, playerOffsetY: 0, playerFacing: 'right', playerActionMode: 'idle', scrollWorld: false, inBattle: false, playerHp: playerMaxHp, playerMaxHp: playerMaxHp, playerMana: getCharacterMaxMana(character), playerMaxMana: getCharacterMaxMana(character), playerSkillCooldowns: createCharacterSkillCooldowns(character), activeBuildBuffs: [], activeCombatEffects: [], playerCurrency: getInitialPlayerCurrency( resolvedWorldType, resolvedCustomWorldProfile, ), playerInventory: mergeStarterInventoryItems( explicitStarterItems?.inventory ?? [], buildInitialPlayerInventory( character, resolvedWorldType, resolvedCustomWorldProfile, ), ), playerEquipment: createEmptyEquipmentLoadout(), npcStates: initialEncounter && initialNpcState ? { [initialEncounter.id!]: initialNpcState, } : {}, quests: [], roster: [], companions: [], currentBattleNpcId: null, currentNpcBattleMode: null, currentNpcBattleOutcome: null, sparReturnEncounter: null, sparPlayerHpBefore: null, sparPlayerMaxHpBefore: null, sparStoryHistoryBefore: null, }, mergedStarterEquipment, ), ); }); }; return { gameState, setGameState, bottomTab, setBottomTab, isMapOpen, setIsMapOpen, resetGame, handleCustomWorldSelect, handleBackToWorldSelect, handleCharacterSelect, }; } export type RpgSessionBootstrapResult = ReturnType< typeof useRpgSessionBootstrap >;