23e629f37e
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m6s
- Add applyDamageToRoom() function targeting individual enemies - Add lowestHPEnemy() helper for focus-fire targeting - AoE spells distribute damage across all enemies - Single-target attacks hit lowest HP enemy first - Refactor processCombatTick spell/equipment/melee/DoT damage to use per-enemy system - Update golemApplyDamageToRoom in golem-combat pipeline for per-enemy targeting - Add currentRoom to CombatTickResult and sync through gameStore - Update combat-actions tests with proper enemy setup for per-enemy tests - Extract combat-damage.ts module to stay under 400-line limit
74 lines
2.5 KiB
TypeScript
74 lines
2.5 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: each enemy takes full damage
|
|
* - 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
|
|
*
|
|
* @returns { floorHP, floorMaxHP, roomCleared }
|
|
*/
|
|
export function applyDamageToRoom(
|
|
get: () => CombatStore,
|
|
set: (state: Partial<CombatState>) => void,
|
|
dmg: number,
|
|
isAoe: boolean,
|
|
): { 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);
|
|
|
|
const updatedEnemies = room.enemies.map((enemy) => {
|
|
if (enemy.hp <= 0) return enemy;
|
|
if (isAoe) {
|
|
// AoE: each enemy takes full damage
|
|
return { ...enemy, hp: Math.max(0, enemy.hp - dmg) };
|
|
}
|
|
// 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 };
|
|
}
|