226 lines
6.6 KiB
TypeScript
226 lines
6.6 KiB
TypeScript
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<SetStateAction<GameState>>;
|
|
setCurrentStory: Dispatch<SetStateAction<StoryMoment | null>>;
|
|
setAiError: Dispatch<SetStateAction<string | null>>;
|
|
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
|
buildFallbackStoryForState: BuildFallbackStoryForState;
|
|
};
|
|
}) {
|
|
const {
|
|
currentStory,
|
|
setGameState,
|
|
setCurrentStory,
|
|
setAiError,
|
|
setIsLoading,
|
|
buildFallbackStoryForState,
|
|
} = runtime;
|
|
const [serverInventoryView, setServerInventoryView] =
|
|
useState<RuntimeStoryInventoryView | null>(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,
|
|
};
|
|
}
|