This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -1,14 +1,11 @@
import { useMemo, type Dispatch, type SetStateAction } from 'react';
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import type { RuntimeStoryInventoryActionView } from '../../../packages/shared/src/contracts/rpgRuntimeStoryState';
import {
EQUIPMENT_EQUIP_FUNCTION,
EQUIPMENT_UNEQUIP_FUNCTION,
FORGE_CRAFT_FUNCTION,
FORGE_DISMANTLE_FUNCTION,
FORGE_REFORGE_FUNCTION,
INVENTORY_USE_FUNCTION,
} from '../../data/functionCatalog';
import { getForgeRecipeViews } from '../../data/forgeSystem';
loadRpgRuntimeInventoryView,
type RuntimeStoryChoicePayload,
type RuntimeStoryInventoryView,
} from '../../services/rpg-runtime';
import type { Character, GameState, StoryMoment } from '../../types';
import { resolveRpgRuntimeChoice } from '.';
import type { InventoryFlowUi } from './uiTypes';
@@ -41,20 +38,71 @@ export function useStoryInventoryActions({
setIsLoading,
buildFallbackStoryForState,
} = runtime;
const forgeRecipes = useMemo(
() =>
getForgeRecipeViews(
gameState.playerInventory,
gameState.playerCurrency,
gameState.worldType,
),
[gameState.playerCurrency, gameState.playerInventory, gameState.worldType],
);
const [serverInventoryView, setServerInventoryView] =
useState<RuntimeStoryInventoryView | null>(null);
const runtimeSessionId = gameState.runtimeSessionId;
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: {
runtimeSessionId,
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,
runtimeSessionId,
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: Record<string, unknown>;
payload?: RuntimeStoryChoicePayload;
}) => {
const character = gameState.playerCharacter;
if (
@@ -69,7 +117,7 @@ export function useStoryInventoryActions({
setIsLoading(true);
try {
const { hydratedSnapshot, nextStory } = await resolveRpgRuntimeChoice({
const { response, hydratedSnapshot, nextStory } = await resolveRpgRuntimeChoice({
gameState,
currentStory,
option: {
@@ -81,6 +129,7 @@ export function useStoryInventoryActions({
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);
@@ -94,100 +143,80 @@ export function useStoryInventoryActions({
}
};
const useInventoryItem = async (itemId: string) => {
const item = gameState.playerInventory.find(
(candidate) => candidate.id === itemId,
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,
'后端背包视图尚未提供该物品的使用动作。',
);
if (!item) {
return false;
}
return resolveServerInventoryAction({
functionId: INVENTORY_USE_FUNCTION.id,
actionText: `使用${item.name}`,
payload: { itemId },
});
};
const equipInventoryItem = async (itemId: string) => {
const item = gameState.playerInventory.find(
(candidate) => candidate.id === itemId,
const equipInventoryItem = async (itemId: string) =>
submitInventoryAction(
findBackpackItemView(itemId)?.actions.equip,
'后端背包视图尚未提供该物品的装备动作。',
);
if (!item) {
return false;
}
return resolveServerInventoryAction({
functionId: EQUIPMENT_EQUIP_FUNCTION.id,
actionText: `装备${item.name}`,
payload: { itemId },
});
};
const unequipItem = async (slot: 'weapon' | 'armor' | 'relic') => {
const equippedItem = gameState.playerEquipment[slot];
if (!equippedItem) {
return false;
}
return resolveServerInventoryAction({
functionId: EQUIPMENT_UNEQUIP_FUNCTION.id,
actionText: `卸下${equippedItem.name}`,
payload: { slotId: slot },
});
};
const unequipItem = async (slot: 'weapon' | 'armor' | 'relic') =>
submitInventoryAction(
findEquipmentSlotView(slot)?.unequip,
'后端装备视图尚未提供该槽位的卸装动作。',
);
const craftRecipe = async (recipeId: string) => {
const recipe = forgeRecipes.find(
const recipe = serverInventoryView?.forgeRecipes.find(
(candidate) => candidate.id === recipeId,
);
if (!recipe) {
return false;
return rejectInventoryAction('后端锻造视图尚未提供该配方。');
}
if (!recipe.canCraft) {
return rejectInventoryAction(
recipe.disabledReason ?? recipe.action.reason ?? '当前配方不可制作。',
);
}
return resolveServerInventoryAction({
functionId: FORGE_CRAFT_FUNCTION.id,
actionText: `制作${recipe.resultLabel}`,
payload: { recipeId },
});
return submitInventoryAction(recipe.action, '当前配方不可制作。');
};
const dismantleItem = async (itemId: string) => {
const item = gameState.playerInventory.find(
(candidate) => candidate.id === itemId,
const dismantleItem = async (itemId: string) =>
submitInventoryAction(
findBackpackItemView(itemId)?.actions.dismantle,
'后端背包视图尚未提供该物品的拆解动作。',
);
if (!item) {
return false;
}
return resolveServerInventoryAction({
functionId: FORGE_DISMANTLE_FUNCTION.id,
actionText: `拆解${item.name}`,
payload: { itemId },
});
};
const reforgeItem = async (itemId: string) => {
const item = gameState.playerInventory.find(
(candidate) => candidate.id === itemId,
const reforgeItem = async (itemId: string) =>
submitInventoryAction(
findBackpackItemView(itemId)?.actions.reforge,
'后端背包视图尚未提供该物品的重铸动作。',
);
if (!item) {
return false;
}
return resolveServerInventoryAction({
functionId: FORGE_REFORGE_FUNCTION.id,
actionText: `重铸${item.name}`,
payload: { itemId },
});
};
return {
inventoryUi: {
useInventoryItem,
equipInventoryItem,
unequipItem,
forgeRecipes,
playerCurrency: serverInventoryView?.playerCurrency ?? null,
currencyText: serverInventoryView?.currencyText ?? null,
backpackItems: serverInventoryView?.backpackItems ?? [],
equipmentSlots: serverInventoryView?.equipmentSlots ?? [],
forgeRecipes: serverInventoryView?.forgeRecipes ?? [],
craftRecipe,
dismantleItem,
reforgeItem,