Files
Genarrative/server-node/src/modules/inventory/inventoryStoryActionService.ts

196 lines
5.2 KiB
TypeScript

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<string>([
'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<string, unknown>;
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),
};
}