import type { RuntimeStoryActionRequest, RuntimeStoryPatch, } from '../../../../packages/shared/src/contracts/story.js'; import { conflict, invalidRequest } from '../../errors.js'; import { getPlayerBuildDamageBreakdown, } from '../runtime/runtimeBuildModule.js'; import { craftForgeRecipe, dismantleInventoryItem, equipInventoryItem, reforgeInventoryItem, unequipInventoryItem, useInventoryItem, type InventoryMutationFailure, type InventoryMutationSuccess, type RuntimeGameState as InventoryRuntimeGameState, } from './inventoryMutationService.js'; import { replaceRuntimeSessionRawGameState, type RuntimeSession, } from '../story/runtimeSession.js'; const SUPPORTED_INVENTORY_STORY_FUNCTION_IDS = new Set([ 'equipment_equip', 'equipment_unequip', 'forge_craft', 'forge_dismantle', 'forge_reforge', 'inventory_use', ]); type InventoryStoryResolution = { actionText: string; resultText: string; patches: RuntimeStoryPatch[]; toast?: string | null; }; type JsonRecord = Record; function isObject(value: unknown): value is JsonRecord { return typeof value === 'object' && value !== null && !Array.isArray(value); } function readPayload(request: RuntimeStoryActionRequest) { return isObject(request.action.payload) ? request.action.payload : {}; } function readString(value: unknown) { return typeof value === 'string' && value.trim() ? value.trim() : ''; } function readItemId(request: RuntimeStoryActionRequest) { const payload = readPayload(request); return ( readString(payload.itemId) || readString(payload.targetId) || readString(request.action.targetId) ); } function readRecipeId(request: RuntimeStoryActionRequest) { const payload = readPayload(request); return ( readString(payload.recipeId) || readString(payload.targetId) || readString(request.action.targetId) ); } function readEquipmentSlotId(request: RuntimeStoryActionRequest) { const payload = readPayload(request); const slotId = readString(payload.slotId) || readString(request.action.targetId); if (slotId === 'weapon' || slotId === 'armor' || slotId === 'relic') { return slotId; } return ''; } function refreshSessionFromGameState( session: RuntimeSession, nextGameState: InventoryMutationSuccess['nextState'], ) { replaceRuntimeSessionRawGameState( session, nextGameState as unknown as JsonRecord, ); } export function buildBuildToast( nextState: InventoryMutationSuccess['nextState'], ) { if (!nextState.playerCharacter) { return null; } const buildMultiplier = getPlayerBuildDamageBreakdown( nextState, nextState.playerCharacter, ).buildDamageMultiplier.toFixed(2); return `当前 Build 倍率 x${buildMultiplier}`; } function throwMutationFailure(error: InventoryMutationFailure): never { switch (error.code) { case 'item_not_equippable': case 'recipe_not_available': throw invalidRequest(error.message); default: throw conflict(error.message); } } function resolveMutation( request: RuntimeStoryActionRequest, state: InventoryRuntimeGameState, ) { switch (request.action.functionId) { case 'inventory_use': { const itemId = readItemId(request); if (!itemId) { throw invalidRequest('inventory_use 缺少 itemId'); } return useInventoryItem(state, itemId); } case 'equipment_equip': { const itemId = readItemId(request); if (!itemId) { throw invalidRequest('equipment_equip 缺少 itemId'); } return equipInventoryItem(state, itemId); } case 'equipment_unequip': { const slotId = readEquipmentSlotId(request); if (!slotId) { throw invalidRequest('equipment_unequip 缺少合法 slotId'); } return unequipInventoryItem(state, slotId); } case 'forge_craft': { const recipeId = readRecipeId(request); if (!recipeId) { throw invalidRequest('forge_craft 缺少 recipeId'); } return craftForgeRecipe(state, recipeId); } case 'forge_dismantle': { const itemId = readItemId(request); if (!itemId) { throw invalidRequest('forge_dismantle 缺少 itemId'); } return dismantleInventoryItem(state, itemId); } case 'forge_reforge': { const itemId = readItemId(request); if (!itemId) { throw invalidRequest('forge_reforge 缺少 itemId'); } return reforgeInventoryItem(state, itemId); } default: throw invalidRequest(`暂不支持的 Inventory 动作:${request.action.functionId}`); } } export function isSupportedInventoryStoryFunctionId(functionId: string) { return SUPPORTED_INVENTORY_STORY_FUNCTION_IDS.has(functionId); } export function resolveInventoryStoryAction( session: RuntimeSession, request: RuntimeStoryActionRequest, ): InventoryStoryResolution { const mutation = resolveMutation( request, session.rawGameState as InventoryRuntimeGameState, ); if (!mutation.ok) { throwMutationFailure(mutation); } refreshSessionFromGameState(session, mutation.nextState); return { actionText: mutation.actionText, resultText: mutation.detailText, patches: [], toast: buildBuildToast(mutation.nextState), }; }