This commit is contained in:
2026-04-10 15:37:02 +08:00
parent 161cd32277
commit f19e482c8f
233 changed files with 43987 additions and 5127 deletions

View 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,
};
}