import { type Dispatch, type SetStateAction, useEffect, useState } from 'react'; import type { RuntimeStoryInventoryActionView } from '../../../packages/shared/src/contracts/rpgRuntimeStoryState'; import { loadRpgRuntimeInventoryView, type RuntimeStoryChoicePayload, type RuntimeStoryInventoryView, } from '../../services/rpg-runtime'; import type { Character, GameState, StoryMoment } from '../../types'; import { resolveRpgRuntimeChoice } from '.'; import type { InventoryFlowUi } from './uiTypes'; type BuildFallbackStoryForState = ( state: GameState, character: Character, fallbackText?: string, ) => StoryMoment; export function useStoryInventoryActions({ gameState, runtime, }: { gameState: GameState; runtime: { currentStory: StoryMoment | null; setGameState: Dispatch>; setCurrentStory: Dispatch>; setAiError: Dispatch>; setIsLoading: Dispatch>; buildFallbackStoryForState: BuildFallbackStoryForState; }; }) { const { currentStory, setGameState, setCurrentStory, setAiError, setIsLoading, buildFallbackStoryForState, } = runtime; const [serverInventoryView, setServerInventoryView] = useState(null); const storySessionId = gameState.storySessionId; const runtimeActionVersion = gameState.runtimeActionVersion; const currentScene = gameState.currentScene; const hasPlayerCharacter = Boolean(gameState.playerCharacter); useEffect(() => { if (!hasPlayerCharacter || currentScene !== 'Story') { setServerInventoryView(null); return; } const controller = new AbortController(); void loadRpgRuntimeInventoryView( { gameState: { storySessionId, runtimeActionVersion, }, }, { signal: controller.signal }, ) .then((view) => { setServerInventoryView(view); }) .catch((error) => { if (controller.signal.aborted) { return; } console.error('Failed to load inventory runtime view:', error); setAiError(error instanceof Error ? error.message : '背包视图同步失败'); }); return () => { controller.abort(); }; }, [ currentScene, hasPlayerCharacter, runtimeActionVersion, storySessionId, setAiError, ]); const rejectInventoryAction = (message: string) => { setAiError(message); return false; }; const findBackpackItemView = (itemId: string) => serverInventoryView?.backpackItems.find( (candidate) => candidate.item.id === itemId, ) ?? null; const findEquipmentSlotView = (slot: 'weapon' | 'armor' | 'relic') => serverInventoryView?.equipmentSlots.find( (candidate) => candidate.slotId === slot, ) ?? null; const resolveServerInventoryAction = async (params: { functionId: string; actionText: string; payload?: RuntimeStoryChoicePayload; }) => { const character = gameState.playerCharacter; if ( !character || !gameState.worldType || gameState.currentScene !== 'Story' ) { return false; } setAiError(null); setIsLoading(true); try { const { response, hydratedSnapshot, nextStory } = await resolveRpgRuntimeChoice({ gameState, currentStory, option: { functionId: params.functionId, actionText: params.actionText, }, payload: params.payload, }); setGameState(hydratedSnapshot.gameState); setCurrentStory(nextStory); setServerInventoryView(response.viewModel.inventory); return true; } catch (error) { console.error('Failed to resolve inventory runtime action on the server:', error); setAiError(error instanceof Error ? error.message : '背包动作执行失败'); if (!currentStory) { setCurrentStory(buildFallbackStoryForState(gameState, character)); } return false; } finally { setIsLoading(false); } }; const submitInventoryAction = async ( action: RuntimeStoryInventoryActionView | undefined, fallbackReason: string, ) => { if (!action) { return rejectInventoryAction(fallbackReason); } if (!action.enabled) { return rejectInventoryAction(action.reason ?? fallbackReason); } return resolveServerInventoryAction({ functionId: action.functionId, actionText: action.actionText, payload: action.payload as RuntimeStoryChoicePayload | undefined, }); }; const useInventoryItem = async (itemId: string) => submitInventoryAction( findBackpackItemView(itemId)?.actions.use, '后端背包视图尚未提供该物品的使用动作。', ); const equipInventoryItem = async (itemId: string) => submitInventoryAction( findBackpackItemView(itemId)?.actions.equip, '后端背包视图尚未提供该物品的装备动作。', ); const unequipItem = async (slot: 'weapon' | 'armor' | 'relic') => submitInventoryAction( findEquipmentSlotView(slot)?.unequip, '后端装备视图尚未提供该槽位的卸装动作。', ); const craftRecipe = async (recipeId: string) => { const recipe = serverInventoryView?.forgeRecipes.find( (candidate) => candidate.id === recipeId, ); if (!recipe) { return rejectInventoryAction('后端锻造视图尚未提供该配方。'); } if (!recipe.canCraft) { return rejectInventoryAction( recipe.disabledReason ?? recipe.action.reason ?? '当前配方不可制作。', ); } return submitInventoryAction(recipe.action, '当前配方不可制作。'); }; const dismantleItem = async (itemId: string) => submitInventoryAction( findBackpackItemView(itemId)?.actions.dismantle, '后端背包视图尚未提供该物品的拆解动作。', ); const reforgeItem = async (itemId: string) => submitInventoryAction( findBackpackItemView(itemId)?.actions.reforge, '后端背包视图尚未提供该物品的重铸动作。', ); return { inventoryUi: { useInventoryItem, equipInventoryItem, unequipItem, playerCurrency: serverInventoryView?.playerCurrency ?? null, currencyText: serverInventoryView?.currencyText ?? null, backpackItems: serverInventoryView?.backpackItems ?? [], equipmentSlots: serverInventoryView?.equipmentSlots ?? [], forgeRecipes: serverInventoryView?.forgeRecipes ?? [], craftRecipe, dismantleItem, reforgeItem, } satisfies InventoryFlowUi, }; }