1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 15:45:14 +08:00
parent 8a7bd90458
commit 1c72066bab
73 changed files with 7814 additions and 1018 deletions

View File

@@ -11,6 +11,11 @@ import {
resolveInventoryItemUseEffect,
} from '../../bridges/legacyInventoryRuntimeBridge.js';
import { conflict } from '../../errors.js';
import {
buildExperienceGrantResultText,
grantPlayerExperience,
} from '../progression/playerProgressionService.js';
import { resolveHostileBattleProfile } from '../progression/hostileProgressionService.js';
import {
appendBuildBuffs,
resolvePlayerOutgoingDamageResult,
@@ -41,7 +46,9 @@ type CombatActionConfig = {
}>;
consumedItemId?: string | null;
usedItem?: RuntimeCombatInventoryItem | null;
itemEffect?: NonNullable<ReturnType<typeof resolveInventoryItemUseEffect>> | null;
itemEffect?: NonNullable<
ReturnType<typeof resolveInventoryItemUseEffect>
> | null;
};
export type CombatResolution = {
@@ -87,6 +94,15 @@ function getAliveTarget(session: RuntimeSession) {
return session.sceneHostileNpcs.find((npc) => npc.hp > 0) ?? null;
}
function getVictoryResolvedTargets(
session: RuntimeSession,
primaryTargetId: string,
) {
return session.sceneHostileNpcs.filter(
(npc) => npc.id === primaryTargetId || npc.hp > 0,
);
}
function getCombatInventoryItem(
session: RuntimeSession,
itemId: string,
@@ -147,13 +163,64 @@ function applySparAffinityReward(session: RuntimeSession) {
}
function clampPlayerVitals(session: RuntimeSession) {
session.playerHp = Math.max(0, Math.min(session.playerHp, session.playerMaxHp));
session.playerHp = Math.max(
0,
Math.min(session.playerHp, session.playerMaxHp),
);
session.playerMana = Math.max(
0,
Math.min(session.playerMana, session.playerMaxMana),
);
}
function applyHostileVictoryRewards(
session: RuntimeSession,
resolvedTargets: RuntimeSession['sceneHostileNpcs'],
) {
if (resolvedTargets.length <= 0) {
return '';
}
const grantedXp = resolvedTargets.reduce((sum, hostileNpc) => {
const battleProfile = resolveHostileBattleProfile({
playerProgression: session.rawGameState.playerProgression,
encounter: {
hostile: true,
monsterPresetId: hostileNpc.id,
levelProfile: hostileNpc.levelProfile,
experienceReward: hostileNpc.experienceReward,
},
battleMode: 'fight',
});
return sum + battleProfile.experienceReward;
}, 0);
const experienceGrant = grantPlayerExperience(
session.rawGameState.playerProgression,
grantedXp,
{
source: 'hostile_npc',
},
);
session.rawGameState.playerProgression = experienceGrant.state;
session.rawGameState.runtimeStats = incrementGameRuntimeStats(
(isObject(session.rawGameState.runtimeStats)
? session.rawGameState.runtimeStats
: {
hostileNpcsDefeated: 0,
questsAccepted: 0,
itemsUsed: 0,
scenesTraveled: 0,
}) as Parameters<typeof incrementGameRuntimeStats>[0],
{
hostileNpcsDefeated: resolvedTargets.length,
},
);
return buildExperienceGrantResultText(experienceGrant);
}
function finishBattle(
session: RuntimeSession,
outcome: RuntimeBattlePresentation['outcome'],
@@ -194,10 +261,7 @@ function buildBasicAttackBaseDamage(session: RuntimeSession) {
);
}
function tickCooldownMap(
cooldowns: Record<string, number>,
turns: number,
) {
function tickCooldownMap(cooldowns: Record<string, number>, turns: number) {
let nextCooldowns = cooldowns;
for (let index = 0; index < Math.max(0, Math.floor(turns)); index += 1) {
@@ -232,7 +296,10 @@ function resolveCombatActionConfig(params: {
} satisfies CombatActionConfig;
}
if (functionId === 'battle_attack_basic' || LEGACY_ATTACK_FUNCTION_IDS.has(functionId)) {
if (
functionId === 'battle_attack_basic' ||
LEGACY_ATTACK_FUNCTION_IDS.has(functionId)
) {
return {
actionText: '普通攻击',
manaCost: 0,
@@ -253,7 +320,9 @@ function resolveCombatActionConfig(params: {
throw conflict('battle_use_skill 缺少 skillId');
}
const skill = character.skills.find((candidate) => candidate.id === skillId);
const skill = character.skills.find(
(candidate) => candidate.id === skillId,
);
if (!skill) {
throw conflict(`未找到技能:${skillId}`);
}
@@ -386,7 +455,9 @@ export function resolveCombatAction(
const damageResult =
action.baseDamage > 0
? resolvePlayerOutgoingDamageResult(
session.rawGameState as Parameters<typeof resolvePlayerOutgoingDamageResult>[0],
session.rawGameState as Parameters<
typeof resolvePlayerOutgoingDamageResult
>[0],
character,
action.baseDamage,
1,
@@ -397,7 +468,7 @@ export function resolveCombatAction(
? action.baseDamage > 0
? 1
: 0
: damageResult?.damage ?? 0;
: (damageResult?.damage ?? 0);
session.playerMana -= action.manaCost;
session.playerHp += action.heal ?? 0;
@@ -417,7 +488,9 @@ export function resolveCombatAction(
if (action.consumedItemId) {
session.rawGameState.playerInventory = removeInventoryItem(
session.rawGameState.playerInventory as Parameters<typeof removeInventoryItem>[0],
session.rawGameState.playerInventory as Parameters<
typeof removeInventoryItem
>[0],
action.consumedItemId,
1,
);
@@ -436,8 +509,9 @@ export function resolveCombatAction(
if (action.buildBuffs?.length) {
session.rawGameState.activeBuildBuffs = appendBuildBuffs(
(session.rawGameState.activeBuildBuffs as Parameters<typeof appendBuildBuffs>[0]) ??
[],
(session.rawGameState.activeBuildBuffs as Parameters<
typeof appendBuildBuffs
>[0]) ?? [],
action.buildBuffs as Parameters<typeof appendBuildBuffs>[1],
);
}
@@ -463,17 +537,21 @@ export function resolveCombatAction(
outcome = 'spar_complete';
resultText = `你和${target.name}这轮过招已经分出高下,对方也承认了你的身手。`;
} else {
const resolvedTargets = getVictoryResolvedTargets(session, target.id);
const experienceText = applyHostileVictoryRewards(
session,
resolvedTargets,
);
finishBattle(session, 'victory');
outcome = 'victory';
resultText = `你这一手彻底压垮了${target.name},眼前战斗已经正式结束。`;
resultText = experienceText
? `你这一手彻底压垮了${target.name},眼前战斗已经正式结束。 ${experienceText}`
: `你这一手彻底压垮了${target.name},眼前战斗已经正式结束。`;
}
} else {
const baseCounter = isSpar
? 1
: Math.max(
4,
Math.round(target.maxHp * 0.14 * 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);