@@ -3,6 +3,13 @@ import type {
|
||||
RuntimeStoryChoicePayload,
|
||||
RuntimeStoryPatch,
|
||||
} from '../../../../packages/shared/src/contracts/story.js';
|
||||
import {
|
||||
buildInventoryUseResultText,
|
||||
incrementGameRuntimeStats,
|
||||
isInventoryItemUsable,
|
||||
removeInventoryItem,
|
||||
resolveInventoryItemUseEffect,
|
||||
} from '../../bridges/legacyInventoryRuntimeBridge.js';
|
||||
import { conflict } from '../../errors.js';
|
||||
import {
|
||||
appendBuildBuffs,
|
||||
@@ -32,6 +39,9 @@ type CombatActionConfig = {
|
||||
tags: string[];
|
||||
durationTurns: number;
|
||||
}>;
|
||||
consumedItemId?: string | null;
|
||||
usedItem?: RuntimeCombatInventoryItem | null;
|
||||
itemEffect?: NonNullable<ReturnType<typeof resolveInventoryItemUseEffect>> | null;
|
||||
};
|
||||
|
||||
export type CombatResolution = {
|
||||
@@ -50,6 +60,13 @@ const LEGACY_ATTACK_FUNCTION_IDS = new Set<string>([
|
||||
'battle_finisher_window',
|
||||
]);
|
||||
|
||||
type RuntimeCombatInventoryItem = Parameters<
|
||||
typeof resolveInventoryItemUseEffect
|
||||
>[0] & {
|
||||
id: string;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -58,10 +75,57 @@ function readString(value: unknown) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
||||
}
|
||||
|
||||
function readNumber(value: unknown, fallback = 0) {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function readArray(value: unknown) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function getAliveTarget(session: RuntimeSession) {
|
||||
return session.sceneHostileNpcs.find((npc) => npc.hp > 0) ?? null;
|
||||
}
|
||||
|
||||
function getCombatInventoryItem(
|
||||
session: RuntimeSession,
|
||||
itemId: string,
|
||||
): RuntimeCombatInventoryItem | null {
|
||||
const rawItem = readArray(session.rawGameState.playerInventory).find(
|
||||
(candidate) => isObject(candidate) && readString(candidate.id) === itemId,
|
||||
);
|
||||
if (!rawItem || !isObject(rawItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = readString(rawItem.name, itemId);
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rarity = readString(rawItem.rarity, 'common');
|
||||
const normalizedRarity =
|
||||
rarity === 'legendary' ||
|
||||
rarity === 'epic' ||
|
||||
rarity === 'rare' ||
|
||||
rarity === 'uncommon'
|
||||
? rarity
|
||||
: 'common';
|
||||
|
||||
return {
|
||||
id: itemId,
|
||||
name,
|
||||
quantity: Math.max(0, Math.round(readNumber(rawItem.quantity, 0))),
|
||||
rarity: normalizedRarity,
|
||||
tags: readArray(rawItem.tags).filter(
|
||||
(tag): tag is string => typeof tag === 'string' && tag.trim().length > 0,
|
||||
),
|
||||
useProfile: isObject(rawItem.useProfile)
|
||||
? (rawItem.useProfile as RuntimeCombatInventoryItem['useProfile'])
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function applySparAffinityReward(session: RuntimeSession) {
|
||||
const npcState = getEncounterNpcState(session);
|
||||
const encounter = session.currentEncounter;
|
||||
@@ -210,6 +274,53 @@ function resolveCombatActionConfig(params: {
|
||||
} satisfies CombatActionConfig;
|
||||
}
|
||||
|
||||
if (functionId === 'inventory_use') {
|
||||
const character = getPlayerCharacter(session);
|
||||
if (!character) {
|
||||
throw conflict('缺少玩家角色,无法结算战斗物品动作');
|
||||
}
|
||||
|
||||
const itemId = readString(isObject(payload) ? payload.itemId : '');
|
||||
if (!itemId) {
|
||||
throw conflict('inventory_use 缺少 itemId');
|
||||
}
|
||||
|
||||
const item = getCombatInventoryItem(session, itemId);
|
||||
if (!item || item.quantity <= 0) {
|
||||
throw conflict('未找到可用于战斗结算的物品');
|
||||
}
|
||||
|
||||
if (!isInventoryItemUsable(item)) {
|
||||
throw conflict(`${item.name} 当前不可在战斗中直接使用`);
|
||||
}
|
||||
|
||||
const effect = resolveInventoryItemUseEffect(item, character);
|
||||
if (
|
||||
!effect ||
|
||||
((effect.hpRestore ?? 0) <= 0 &&
|
||||
(effect.manaRestore ?? 0) <= 0 &&
|
||||
(effect.cooldownReduction ?? 0) <= 0 &&
|
||||
(effect.buildBuffs?.length ?? 0) <= 0)
|
||||
) {
|
||||
throw conflict(`${item.name} 当前没有可直接结算的战斗效果`);
|
||||
}
|
||||
|
||||
return {
|
||||
actionText: `使用${item.name}`,
|
||||
manaCost: 0,
|
||||
baseDamage: 0,
|
||||
counterMultiplier: 0.72,
|
||||
heal: effect.hpRestore,
|
||||
manaRestore: effect.manaRestore,
|
||||
cooldownBonus: effect.cooldownReduction,
|
||||
selectedSkillId: null,
|
||||
buildBuffs: effect.buildBuffs,
|
||||
consumedItemId: item.id,
|
||||
usedItem: item,
|
||||
itemEffect: effect,
|
||||
} satisfies CombatActionConfig;
|
||||
}
|
||||
|
||||
throw conflict(`暂不支持的战斗动作:${functionId}`);
|
||||
}
|
||||
|
||||
@@ -304,6 +415,25 @@ export function resolveCombatAction(
|
||||
}
|
||||
session.rawGameState.playerSkillCooldowns = nextCooldowns;
|
||||
|
||||
if (action.consumedItemId) {
|
||||
session.rawGameState.playerInventory = removeInventoryItem(
|
||||
session.rawGameState.playerInventory as Parameters<typeof removeInventoryItem>[0],
|
||||
action.consumedItemId,
|
||||
1,
|
||||
);
|
||||
session.rawGameState.runtimeStats = incrementGameRuntimeStats(
|
||||
(isObject(session.rawGameState.runtimeStats)
|
||||
? session.rawGameState.runtimeStats
|
||||
: {
|
||||
hostileNpcsDefeated: 0,
|
||||
questsAccepted: 0,
|
||||
itemsUsed: 0,
|
||||
scenesTraveled: 0,
|
||||
}) as Parameters<typeof incrementGameRuntimeStats>[0],
|
||||
{ itemsUsed: 1 },
|
||||
);
|
||||
}
|
||||
|
||||
if (action.buildBuffs?.length) {
|
||||
session.rawGameState.activeBuildBuffs = appendBuildBuffs(
|
||||
(session.rawGameState.activeBuildBuffs as Parameters<typeof appendBuildBuffs>[0]) ??
|
||||
@@ -354,7 +484,10 @@ export function resolveCombatAction(
|
||||
patches.push(affinityPatch);
|
||||
}
|
||||
outcome = 'spar_complete';
|
||||
resultText = `${target.name}也把你逼到了极限,这场切磋点到为止,双方都默认收手。`;
|
||||
resultText =
|
||||
params.functionId === 'inventory_use' && action.usedItem
|
||||
? `你刚用下${action.usedItem.name}稳住一口气,但${target.name}还是把你逼到了极限,这场切磋点到为止。`
|
||||
: `${target.name}也把你逼到了极限,这场切磋点到为止,双方都默认收手。`;
|
||||
} else if (!isSpar && session.playerHp <= 0) {
|
||||
session.playerHp = 0;
|
||||
session.inBattle = false;
|
||||
@@ -363,9 +496,21 @@ export function resolveCombatAction(
|
||||
session.npcInteractionActive = false;
|
||||
session.currentEncounter = null;
|
||||
outcome = 'escaped';
|
||||
resultText = `你在和${target.name}的交锋里被压到失去战斗能力,这轮正面冲突只能先断开。`;
|
||||
resultText =
|
||||
params.functionId === 'inventory_use' && action.usedItem
|
||||
? `你刚把${action.usedItem.name}用下去,却还是被${target.name}压到失去战斗能力,这轮正面冲突只能先断开。`
|
||||
: `你在和${target.name}的交锋里被压到失去战斗能力,这轮正面冲突只能先断开。`;
|
||||
} else if (params.functionId === 'battle_recover_breath') {
|
||||
resultText = `你先把伤势和气息稳住了一轮,但${target.name}仍在持续逼近。`;
|
||||
} else if (
|
||||
params.functionId === 'inventory_use' &&
|
||||
action.usedItem &&
|
||||
action.itemEffect
|
||||
) {
|
||||
resultText = `${buildInventoryUseResultText(
|
||||
action.usedItem,
|
||||
action.itemEffect,
|
||||
).replace(/。$/u, '')},但${target.name}仍在持续逼近。`;
|
||||
} else if (params.functionId === 'battle_use_skill') {
|
||||
resultText = `${action.actionText}命中了${target.name},这一轮技能效果已经直接结算。`;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user