1
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import type {
|
||||
RuntimeBattlePresentation,
|
||||
RuntimeStoryChoicePayload,
|
||||
RuntimeStoryPatch,
|
||||
} from '../../../../packages/shared/src/contracts/story.js';
|
||||
import { conflict } from '../../errors.js';
|
||||
import {
|
||||
appendBuildBuffs,
|
||||
resolvePlayerOutgoingDamageResult,
|
||||
} from '../runtime/runtimeBuildModule.js';
|
||||
import {
|
||||
getEncounterNpcState,
|
||||
getPlayerCharacter,
|
||||
getPlayerSkillCooldowns,
|
||||
setEncounterNpcState,
|
||||
type RuntimeSession,
|
||||
} from '../story/runtimeSession.js';
|
||||
@@ -16,6 +23,15 @@ type CombatActionConfig = {
|
||||
counterMultiplier: number;
|
||||
heal?: number;
|
||||
manaRestore?: number;
|
||||
cooldownBonus?: number;
|
||||
selectedSkillId?: string | null;
|
||||
appliedCooldownTurns?: number;
|
||||
buildBuffs?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
tags: string[];
|
||||
durationTurns: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type CombatResolution = {
|
||||
@@ -26,46 +42,21 @@ export type CombatResolution = {
|
||||
storyText?: string;
|
||||
};
|
||||
|
||||
const COMBAT_ACTIONS: Record<string, CombatActionConfig> = {
|
||||
battle_all_in_crush: {
|
||||
actionText: '正面强压',
|
||||
manaCost: 14,
|
||||
baseDamage: 22,
|
||||
counterMultiplier: 1.25,
|
||||
},
|
||||
battle_feint_step: {
|
||||
actionText: '虚晃切步',
|
||||
manaCost: 8,
|
||||
baseDamage: 16,
|
||||
counterMultiplier: 0.7,
|
||||
},
|
||||
battle_finisher_window: {
|
||||
actionText: '抓破绽终结',
|
||||
manaCost: 10,
|
||||
baseDamage: 18,
|
||||
counterMultiplier: 0.9,
|
||||
},
|
||||
battle_guard_break: {
|
||||
actionText: '破架重击',
|
||||
manaCost: 9,
|
||||
baseDamage: 17,
|
||||
counterMultiplier: 0.95,
|
||||
},
|
||||
battle_probe_pressure: {
|
||||
actionText: '稳步试探',
|
||||
manaCost: 5,
|
||||
baseDamage: 12,
|
||||
counterMultiplier: 0.8,
|
||||
},
|
||||
battle_recover_breath: {
|
||||
actionText: '边守边调息',
|
||||
manaCost: 0,
|
||||
baseDamage: 0,
|
||||
counterMultiplier: 0.55,
|
||||
heal: 12,
|
||||
manaRestore: 9,
|
||||
},
|
||||
};
|
||||
const LEGACY_ATTACK_FUNCTION_IDS = new Set<string>([
|
||||
'battle_all_in_crush',
|
||||
'battle_guard_break',
|
||||
'battle_probe_pressure',
|
||||
'battle_feint_step',
|
||||
'battle_finisher_window',
|
||||
]);
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readString(value: unknown) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
||||
}
|
||||
|
||||
function getAliveTarget(session: RuntimeSession) {
|
||||
return session.sceneHostileNpcs.find((npc) => npc.hp > 0) ?? null;
|
||||
@@ -124,19 +115,120 @@ function finishBattle(
|
||||
}
|
||||
}
|
||||
|
||||
function buildBasicAttackBaseDamage(session: RuntimeSession) {
|
||||
const character = getPlayerCharacter(session);
|
||||
if (!character) {
|
||||
return 10;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
8,
|
||||
Math.round(
|
||||
character.attributes.strength * 0.85 +
|
||||
character.attributes.agility * 0.45,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function tickCooldownMap(
|
||||
cooldowns: Record<string, number>,
|
||||
turns: number,
|
||||
) {
|
||||
let nextCooldowns = cooldowns;
|
||||
|
||||
for (let index = 0; index < Math.max(0, Math.floor(turns)); index += 1) {
|
||||
nextCooldowns = Object.fromEntries(
|
||||
Object.entries(nextCooldowns).map(([skillId, value]) => [
|
||||
skillId,
|
||||
Math.max(0, Math.floor(value) - 1),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return nextCooldowns;
|
||||
}
|
||||
|
||||
function resolveCombatActionConfig(params: {
|
||||
session: RuntimeSession;
|
||||
functionId: string;
|
||||
payload?: RuntimeStoryChoicePayload;
|
||||
}) {
|
||||
const { session, functionId, payload } = params;
|
||||
|
||||
if (functionId === 'battle_recover_breath') {
|
||||
return {
|
||||
actionText: '恢复',
|
||||
manaCost: 0,
|
||||
baseDamage: 0,
|
||||
counterMultiplier: 0.55,
|
||||
heal: 12,
|
||||
manaRestore: 9,
|
||||
cooldownBonus: 1,
|
||||
selectedSkillId: null,
|
||||
} satisfies CombatActionConfig;
|
||||
}
|
||||
|
||||
if (functionId === 'battle_attack_basic' || LEGACY_ATTACK_FUNCTION_IDS.has(functionId)) {
|
||||
return {
|
||||
actionText: '普通攻击',
|
||||
manaCost: 0,
|
||||
baseDamage: buildBasicAttackBaseDamage(session),
|
||||
counterMultiplier: 1,
|
||||
selectedSkillId: null,
|
||||
} satisfies CombatActionConfig;
|
||||
}
|
||||
|
||||
if (functionId === 'battle_use_skill') {
|
||||
const character = getPlayerCharacter(session);
|
||||
if (!character) {
|
||||
throw conflict('缺少玩家角色,无法结算技能动作');
|
||||
}
|
||||
|
||||
const skillId = readString(isObject(payload) ? payload.skillId : '');
|
||||
if (!skillId) {
|
||||
throw conflict('battle_use_skill 缺少 skillId');
|
||||
}
|
||||
|
||||
const skill = character.skills.find((candidate) => candidate.id === skillId);
|
||||
if (!skill) {
|
||||
throw conflict(`未找到技能:${skillId}`);
|
||||
}
|
||||
|
||||
const cooldowns = getPlayerSkillCooldowns(session);
|
||||
if ((cooldowns[skill.id] ?? 0) > 0) {
|
||||
throw conflict(`${skill.name} 仍在冷却中`);
|
||||
}
|
||||
|
||||
return {
|
||||
actionText: skill.name,
|
||||
manaCost: skill.manaCost,
|
||||
baseDamage: skill.damage,
|
||||
counterMultiplier: 0.95,
|
||||
selectedSkillId: skill.id,
|
||||
appliedCooldownTurns: skill.cooldownTurns,
|
||||
buildBuffs: skill.buildBuffs ?? [],
|
||||
} satisfies CombatActionConfig;
|
||||
}
|
||||
|
||||
throw conflict(`暂不支持的战斗动作:${functionId}`);
|
||||
}
|
||||
|
||||
export function resolveCombatAction(
|
||||
session: RuntimeSession,
|
||||
functionId: string,
|
||||
params: {
|
||||
functionId: string;
|
||||
payload?: RuntimeStoryChoicePayload;
|
||||
},
|
||||
): CombatResolution {
|
||||
const target = getAliveTarget(session);
|
||||
if (!session.inBattle || !target) {
|
||||
throw conflict('当前不在可结算战斗态,不能执行该战斗动作');
|
||||
}
|
||||
|
||||
if (functionId === 'battle_escape_breakout') {
|
||||
if (params.functionId === 'battle_escape_breakout') {
|
||||
finishBattle(session, 'escaped');
|
||||
return {
|
||||
actionText: '强行脱离战斗',
|
||||
actionText: '逃跑',
|
||||
resultText: `你抓住空当摆脱了${target.name}的压制,先把这轮正面冲突拉开了。`,
|
||||
battle: {
|
||||
targetId: target.id,
|
||||
@@ -146,7 +238,7 @@ export function resolveCombatAction(
|
||||
patches: [
|
||||
{
|
||||
type: 'battle_resolved',
|
||||
functionId,
|
||||
functionId: params.functionId,
|
||||
targetId: target.id,
|
||||
outcome: 'escaped',
|
||||
},
|
||||
@@ -165,27 +257,66 @@ export function resolveCombatAction(
|
||||
};
|
||||
}
|
||||
|
||||
const action = COMBAT_ACTIONS[functionId];
|
||||
if (!action) {
|
||||
throw conflict(`暂不支持的战斗动作:${functionId}`);
|
||||
}
|
||||
|
||||
const action = resolveCombatActionConfig({
|
||||
session,
|
||||
functionId: params.functionId,
|
||||
payload: params.payload,
|
||||
});
|
||||
if (action.manaCost > session.playerMana) {
|
||||
throw conflict('当前灵力不足,无法执行这个战斗动作');
|
||||
}
|
||||
|
||||
const character = getPlayerCharacter(session);
|
||||
if (!character) {
|
||||
throw conflict('缺少玩家角色,无法结算战斗动作');
|
||||
}
|
||||
|
||||
const isSpar = session.currentNpcBattleMode === 'spar';
|
||||
const targetHpRatio = target.hp / Math.max(target.maxHp, 1);
|
||||
const damageBonus =
|
||||
functionId === 'battle_finisher_window' && targetHpRatio <= 0.4 ? 8 : 0;
|
||||
const damageDealt = isSpar ? 1 : action.baseDamage + damageBonus;
|
||||
const damageResult =
|
||||
action.baseDamage > 0
|
||||
? resolvePlayerOutgoingDamageResult(
|
||||
session.rawGameState as Parameters<typeof resolvePlayerOutgoingDamageResult>[0],
|
||||
character,
|
||||
action.baseDamage,
|
||||
1,
|
||||
`${params.functionId}:${action.selectedSkillId ?? 'default'}:${target.id}:${session.runtimeVersion}`,
|
||||
)
|
||||
: null;
|
||||
const damageDealt = isSpar
|
||||
? action.baseDamage > 0
|
||||
? 1
|
||||
: 0
|
||||
: damageResult?.damage ?? 0;
|
||||
|
||||
session.playerMana -= action.manaCost;
|
||||
session.playerHp += action.heal ?? 0;
|
||||
session.playerMana += action.manaRestore ?? 0;
|
||||
|
||||
let nextCooldowns = tickCooldownMap(getPlayerSkillCooldowns(session), 1);
|
||||
if ((action.cooldownBonus ?? 0) > 0) {
|
||||
nextCooldowns = tickCooldownMap(nextCooldowns, action.cooldownBonus ?? 0);
|
||||
}
|
||||
if (action.selectedSkillId && (action.appliedCooldownTurns ?? 0) > 0) {
|
||||
nextCooldowns = {
|
||||
...nextCooldowns,
|
||||
[action.selectedSkillId]: action.appliedCooldownTurns,
|
||||
};
|
||||
}
|
||||
session.rawGameState.playerSkillCooldowns = nextCooldowns;
|
||||
|
||||
if (action.buildBuffs?.length) {
|
||||
session.rawGameState.activeBuildBuffs = appendBuildBuffs(
|
||||
(session.rawGameState.activeBuildBuffs as Parameters<typeof appendBuildBuffs>[0]) ??
|
||||
[],
|
||||
action.buildBuffs as Parameters<typeof appendBuildBuffs>[1],
|
||||
);
|
||||
}
|
||||
|
||||
clampPlayerVitals(session);
|
||||
|
||||
target.hp = Math.max(isSpar ? 1 : 0, target.hp - damageDealt);
|
||||
if (damageDealt > 0) {
|
||||
target.hp = Math.max(isSpar ? 1 : 0, target.hp - damageDealt);
|
||||
}
|
||||
|
||||
const patches: RuntimeStoryPatch[] = [];
|
||||
let resultText = '';
|
||||
@@ -204,12 +335,15 @@ export function resolveCombatAction(
|
||||
} else {
|
||||
finishBattle(session, 'victory');
|
||||
outcome = 'victory';
|
||||
resultText = `你彻底压垮了${target.name}的节奏,这场战斗已经收口。`;
|
||||
resultText = `你这一手彻底压垮了${target.name},眼前战斗已经正式结束。`;
|
||||
}
|
||||
} else {
|
||||
const baseCounter = isSpar
|
||||
? 1
|
||||
: Math.max(4, Math.round(target.maxHp * 0.16 * action.counterMultiplier));
|
||||
: Math.max(
|
||||
4,
|
||||
Math.round(target.maxHp * 0.14 * action.counterMultiplier),
|
||||
);
|
||||
damageTaken = baseCounter;
|
||||
session.playerHp = Math.max(isSpar ? 1 : 0, session.playerHp - damageTaken);
|
||||
|
||||
@@ -220,7 +354,7 @@ export function resolveCombatAction(
|
||||
patches.push(affinityPatch);
|
||||
}
|
||||
outcome = 'spar_complete';
|
||||
resultText = `${target.name}也逼到了你的极限,这场切磋点到为止,双方都默认收手。`;
|
||||
resultText = `${target.name}也把你逼到了极限,这场切磋点到为止,双方都默认收手。`;
|
||||
} else if (!isSpar && session.playerHp <= 0) {
|
||||
session.playerHp = 0;
|
||||
session.inBattle = false;
|
||||
@@ -230,15 +364,19 @@ export function resolveCombatAction(
|
||||
session.currentEncounter = null;
|
||||
outcome = 'escaped';
|
||||
resultText = `你在和${target.name}的交锋里被压到失去战斗能力,这轮正面冲突只能先断开。`;
|
||||
} else if (params.functionId === 'battle_recover_breath') {
|
||||
resultText = `你先把伤势和气息稳住了一轮,但${target.name}仍在持续逼近。`;
|
||||
} else if (params.functionId === 'battle_use_skill') {
|
||||
resultText = `${action.actionText}命中了${target.name},这一轮技能效果已经直接结算。`;
|
||||
} else {
|
||||
resultText = `${action.actionText}命中了${target.name},但对方仍然顶住并回敬了一轮压力。`;
|
||||
resultText = `${action.actionText}命中了${target.name},本次攻击已经完成结算。`;
|
||||
}
|
||||
}
|
||||
|
||||
patches.push(
|
||||
{
|
||||
type: 'battle_resolved',
|
||||
functionId,
|
||||
functionId: params.functionId,
|
||||
targetId: target.id,
|
||||
damageDealt,
|
||||
damageTaken,
|
||||
|
||||
Reference in New Issue
Block a user