This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -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,