Files
Mana-Loop/src/lib/game/stores/combat-damage.ts
T
n8n-gitea 098ec86189
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
fix: spire combat 11 high-severity discrepancies (issue #333)
D-01: Implement per-weapon cast progress (weaponCastProgress record)
D-04: Bypass Executioner/Berserker discipline specials for golem attacks
D-09: Fix lightning counter direction (lightning→water, not lightning→earth)
D-10: Add full composite element counters (blackflame/radiantflames ↔ frost/water/light/dark)
D-15: Fix Executioner to check per-enemy HP < 25% instead of floorHP ratio
D-20: Fix dodge formula to match spec (min(0.55, floor × 0.003), starts at 0)
D-22: Fix shield modifier to use flat HP pool instead of percentage barrier
D-23: Wire up applyMageBarrierRecharge in the damage pipeline
D-25: Move guardian regen from per-damage-event to once-per-tick
D-26: Add guardian armor reduction to the guardian defensive pipeline
D-31: Fix armor_corrode to be temporary (restore armor on effect expiry)
D-38: Implement AoE damage distribution across enemies

All 1069 tests pass. No files exceed 400 lines.
2026-06-08 18:25:05 +02:00

88 lines
3.2 KiB
TypeScript

// ─── Per-Enemy Damage Application (spec §3.2) ─────────────────────────────────
// Extracted from combat-actions.ts to stay under the 400-line file limit.
import type { CombatStore, CombatState } from './combat-state.types';
import type { EnemyState } from '../types';
/**
* Find the enemy with the lowest current HP in the room (focus-fire targeting).
* Returns null if no living enemies exist.
*/
export function lowestHPEnemy(enemies: EnemyState[]): EnemyState | null {
let lowest: EnemyState | null = null;
for (const enemy of enemies) {
if (enemy.hp <= 0) continue;
if (!lowest || enemy.hp < lowest.hp) {
lowest = enemy;
}
}
return lowest;
}
/**
* Apply damage to enemies in the current room.
*
* Spec §3.2:
* - AoE spells: damage is distributed across up to aoeTargets enemies
* - Single-target: target the enemy with lowest HP (focus-fire)
* - Recalculates floorHP as sum of all enemy HP
* - Triggers onRoomCleared when all enemies reach 0 HP
*
* @param aoeTargets — max number of enemies hit by AoE (spec §3.2, D-38 fix)
* @returns { floorHP, floorMaxHP, roomCleared }
*/
export function applyDamageToRoom(
get: () => CombatStore,
set: (state: Partial<CombatState>) => void,
dmg: number,
isAoe: boolean,
aoeTargets?: number,
): { floorHP: number; floorMaxHP: number; roomCleared: boolean } {
const state = get();
const room = state.currentRoom;
if (!room || room.enemies.length === 0) {
// No enemies in room — fall back to direct floorHP subtraction
const newFloorHP = Math.max(0, state.floorHP - dmg);
set({ floorHP: newFloorHP });
return { floorHP: newFloorHP, floorMaxHP: state.floorMaxHP, roomCleared: newFloorHP <= 0 };
}
// For single-target, find the lowest HP enemy once (focus-fire)
const singleTarget = isAoe ? null : lowestHPEnemy(room.enemies);
// For AoE, select up to aoeTargets living enemies (focus-fire: lowest HP first)
let aoeTargetSet: Set<string> | null = null;
if (isAoe && aoeTargets && aoeTargets > 0) {
const livingEnemies = room.enemies
.filter(e => e.hp > 0)
.sort((a, b) => a.hp - b.hp);
const targets = livingEnemies.slice(0, aoeTargets);
aoeTargetSet = new Set(targets.map(e => e.id));
}
const updatedEnemies = room.enemies.map((enemy) => {
if (enemy.hp <= 0) return enemy;
if (isAoe) {
// AoE: distribute damage across up to aoeTargets enemies (spec §3.2, D-38 fix)
if (aoeTargetSet && !aoeTargetSet.has(enemy.id)) return enemy;
const dmgPerEnemy = aoeTargetSet ? dmg / aoeTargetSet.size : dmg;
return { ...enemy, hp: Math.max(0, enemy.hp - dmgPerEnemy) };
}
// Single-target: only damage the lowest HP enemy
if (singleTarget && enemy.id === singleTarget.id) {
return { ...enemy, hp: Math.max(0, enemy.hp - dmg) };
}
return enemy;
});
const newFloorHP = updatedEnemies.reduce((sum, e) => sum + e.hp, 0);
const allDead = updatedEnemies.every((e) => e.hp <= 0);
set({
currentRoom: { ...room, enemies: updatedEnemies },
floorHP: newFloorHP,
});
return { floorHP: newFloorHP, floorMaxHP: state.floorMaxHP, roomCleared: allDead };
}