// ─── 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) => 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 | 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 }; }