196 lines
5.2 KiB
TypeScript
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),
|
|
};
|
|
}
|