1
This commit is contained in:
272
server-node/src/modules/combat/combatResolutionService.ts
Normal file
272
server-node/src/modules/combat/combatResolutionService.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import type {
|
||||
RuntimeBattlePresentation,
|
||||
RuntimeStoryPatch,
|
||||
} from '../../../../packages/shared/src/contracts/story.js';
|
||||
import { conflict } from '../../errors.js';
|
||||
import {
|
||||
getEncounterNpcState,
|
||||
setEncounterNpcState,
|
||||
type RuntimeSession,
|
||||
} from '../story/runtimeSession.js';
|
||||
|
||||
type CombatActionConfig = {
|
||||
actionText: string;
|
||||
manaCost: number;
|
||||
baseDamage: number;
|
||||
counterMultiplier: number;
|
||||
heal?: number;
|
||||
manaRestore?: number;
|
||||
};
|
||||
|
||||
export type CombatResolution = {
|
||||
actionText: string;
|
||||
resultText: string;
|
||||
battle: RuntimeBattlePresentation;
|
||||
patches: RuntimeStoryPatch[];
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
function getAliveTarget(session: RuntimeSession) {
|
||||
return session.sceneHostileNpcs.find((npc) => npc.hp > 0) ?? null;
|
||||
}
|
||||
|
||||
function applySparAffinityReward(session: RuntimeSession) {
|
||||
const npcState = getEncounterNpcState(session);
|
||||
const encounter = session.currentEncounter;
|
||||
if (!npcState || !encounter || encounter.kind !== 'npc') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextAffinity = npcState.affinity + 3;
|
||||
setEncounterNpcState(session, {
|
||||
...npcState,
|
||||
affinity: nextAffinity,
|
||||
});
|
||||
|
||||
return {
|
||||
npcId: encounter.id,
|
||||
previousAffinity: npcState.affinity,
|
||||
nextAffinity,
|
||||
} satisfies Extract<RuntimeStoryPatch, { type: 'npc_affinity_changed' }>;
|
||||
}
|
||||
|
||||
function clampPlayerVitals(session: RuntimeSession) {
|
||||
session.playerHp = Math.max(0, Math.min(session.playerHp, session.playerMaxHp));
|
||||
session.playerMana = Math.max(
|
||||
0,
|
||||
Math.min(session.playerMana, session.playerMaxMana),
|
||||
);
|
||||
}
|
||||
|
||||
function finishBattle(
|
||||
session: RuntimeSession,
|
||||
outcome: RuntimeBattlePresentation['outcome'],
|
||||
) {
|
||||
session.inBattle = false;
|
||||
session.sceneHostileNpcs = [];
|
||||
session.currentNpcBattleMode = null;
|
||||
session.currentNpcBattleOutcome =
|
||||
outcome === 'spar_complete'
|
||||
? 'spar_complete'
|
||||
: outcome === 'victory'
|
||||
? 'fight_victory'
|
||||
: null;
|
||||
|
||||
if (outcome === 'victory' || outcome === 'escaped') {
|
||||
session.currentEncounter = null;
|
||||
session.npcInteractionActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.currentEncounter?.kind === 'npc') {
|
||||
session.npcInteractionActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCombatAction(
|
||||
session: RuntimeSession,
|
||||
functionId: string,
|
||||
): CombatResolution {
|
||||
const target = getAliveTarget(session);
|
||||
if (!session.inBattle || !target) {
|
||||
throw conflict('当前不在可结算战斗态,不能执行该战斗动作');
|
||||
}
|
||||
|
||||
if (functionId === 'battle_escape_breakout') {
|
||||
finishBattle(session, 'escaped');
|
||||
return {
|
||||
actionText: '强行脱离战斗',
|
||||
resultText: `你抓住空当摆脱了${target.name}的压制,先把这轮正面冲突拉开了。`,
|
||||
battle: {
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
outcome: 'escaped',
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
type: 'battle_resolved',
|
||||
functionId,
|
||||
targetId: target.id,
|
||||
outcome: 'escaped',
|
||||
},
|
||||
{
|
||||
type: 'status_changed',
|
||||
inBattle: session.inBattle,
|
||||
npcInteractionActive: session.npcInteractionActive,
|
||||
currentNpcBattleMode: session.currentNpcBattleMode,
|
||||
currentNpcBattleOutcome: session.currentNpcBattleOutcome,
|
||||
},
|
||||
{
|
||||
type: 'encounter_changed',
|
||||
encounterId: session.currentEncounter?.id ?? null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const action = COMBAT_ACTIONS[functionId];
|
||||
if (!action) {
|
||||
throw conflict(`暂不支持的战斗动作:${functionId}`);
|
||||
}
|
||||
|
||||
if (action.manaCost > session.playerMana) {
|
||||
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;
|
||||
|
||||
session.playerMana -= action.manaCost;
|
||||
session.playerHp += action.heal ?? 0;
|
||||
session.playerMana += action.manaRestore ?? 0;
|
||||
clampPlayerVitals(session);
|
||||
|
||||
target.hp = Math.max(isSpar ? 1 : 0, target.hp - damageDealt);
|
||||
|
||||
const patches: RuntimeStoryPatch[] = [];
|
||||
let resultText = '';
|
||||
let outcome: RuntimeBattlePresentation['outcome'] = 'ongoing';
|
||||
let damageTaken = 0;
|
||||
|
||||
if ((isSpar && target.hp <= 1) || (!isSpar && target.hp <= 0)) {
|
||||
if (isSpar) {
|
||||
const affinityPatch = applySparAffinityReward(session);
|
||||
finishBattle(session, 'spar_complete');
|
||||
if (affinityPatch) {
|
||||
patches.push(affinityPatch);
|
||||
}
|
||||
outcome = 'spar_complete';
|
||||
resultText = `你和${target.name}这轮过招已经分出高下,对方也承认了你的身手。`;
|
||||
} else {
|
||||
finishBattle(session, 'victory');
|
||||
outcome = 'victory';
|
||||
resultText = `你彻底压垮了${target.name}的节奏,这场战斗已经收口。`;
|
||||
}
|
||||
} else {
|
||||
const baseCounter = isSpar
|
||||
? 1
|
||||
: Math.max(4, Math.round(target.maxHp * 0.16 * action.counterMultiplier));
|
||||
damageTaken = baseCounter;
|
||||
session.playerHp = Math.max(isSpar ? 1 : 0, session.playerHp - damageTaken);
|
||||
|
||||
if (isSpar && session.playerHp <= 1) {
|
||||
const affinityPatch = applySparAffinityReward(session);
|
||||
finishBattle(session, 'spar_complete');
|
||||
if (affinityPatch) {
|
||||
patches.push(affinityPatch);
|
||||
}
|
||||
outcome = 'spar_complete';
|
||||
resultText = `${target.name}也逼到了你的极限,这场切磋点到为止,双方都默认收手。`;
|
||||
} else if (!isSpar && session.playerHp <= 0) {
|
||||
session.playerHp = 0;
|
||||
session.inBattle = false;
|
||||
session.sceneHostileNpcs = [];
|
||||
session.currentNpcBattleMode = null;
|
||||
session.npcInteractionActive = false;
|
||||
session.currentEncounter = null;
|
||||
outcome = 'escaped';
|
||||
resultText = `你在和${target.name}的交锋里被压到失去战斗能力,这轮正面冲突只能先断开。`;
|
||||
} else {
|
||||
resultText = `${action.actionText}命中了${target.name},但对方仍然顶住并回敬了一轮压力。`;
|
||||
}
|
||||
}
|
||||
|
||||
patches.push(
|
||||
{
|
||||
type: 'battle_resolved',
|
||||
functionId,
|
||||
targetId: target.id,
|
||||
damageDealt,
|
||||
damageTaken,
|
||||
outcome,
|
||||
},
|
||||
{
|
||||
type: 'status_changed',
|
||||
inBattle: session.inBattle,
|
||||
npcInteractionActive: session.npcInteractionActive,
|
||||
currentNpcBattleMode: session.currentNpcBattleMode,
|
||||
currentNpcBattleOutcome: session.currentNpcBattleOutcome,
|
||||
},
|
||||
{
|
||||
type: 'encounter_changed',
|
||||
encounterId: session.currentEncounter?.id ?? null,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
actionText: action.actionText,
|
||||
resultText,
|
||||
battle: {
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
damageDealt,
|
||||
damageTaken,
|
||||
outcome,
|
||||
},
|
||||
patches,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user