/** * Adaptive Tactics - Combat System * Executes attacks, heals, and manages combat flow */ import { MapData } from './models.js'; import { calculateDamage, calculateHitChance, canDouble, canCounter, rollHit, getTerrainAvoid, getTerrainDef } from './rules.js'; /** * Combat result structure */ export class CombatResult { constructor() { this.attacks = []; this.attackerDied = false; this.defenderDied = false; } addAttack(attacker, defender, hit, damage, isCounter = false) { this.attacks.push({ attackerId: attacker.id, attackerName: attacker.name, defenderId: defender.id, defenderName: defender.name, hit, damage, isCounter, defenderHpAfter: defender.hp }); } } /** * Execute a full combat exchange between attacker and defender */ export function executeCombat(attacker, defender, map, gameRules) { const result = new CombatResult(); const attackerWeapon = attacker.getEquippedWeapon(); if (!attackerWeapon || attackerWeapon.isHealingWeapon()) { return result; } const defenderWeapon = defender.getEquippedWeapon(); const distance = MapData.getDistance(attacker.row, attacker.col, defender.row, defender.col); const defenderTile = map.getTile(defender.row, defender.col); const attackerTile = map.getTile(attacker.row, attacker.col); const defTerrainAvoid = getTerrainAvoid(defenderTile); const defTerrainDef = getTerrainDef(defenderTile); const atkTerrainAvoid = getTerrainAvoid(attackerTile); const atkTerrainDef = getTerrainDef(attackerTile); const speedGap = gameRules?.double_speed_gap ?? 4; // Check capabilities const attackerDoubles = canDouble(attacker, defender, speedGap); const defenderCanCounter = canCounter(attacker, defender, distance); const defenderDoubles = defenderCanCounter && canDouble(defender, attacker, speedGap); // Build attack sequence // Standard FE order: attacker -> defender counter -> attacker double -> defender double const sequence = []; // Attacker's first attack sequence.push({ unit: attacker, target: defender, weapon: attackerWeapon, terrainAvoid: defTerrainAvoid, terrainDef: defTerrainDef, isCounter: false }); // Defender's counter (if possible) if (defenderCanCounter && defenderWeapon) { sequence.push({ unit: defender, target: attacker, weapon: defenderWeapon, terrainAvoid: atkTerrainAvoid, terrainDef: atkTerrainDef, isCounter: true }); } // Attacker's double if (attackerDoubles) { sequence.push({ unit: attacker, target: defender, weapon: attackerWeapon, terrainAvoid: defTerrainAvoid, terrainDef: defTerrainDef, isCounter: false }); } // Defender's double if (defenderDoubles && defenderWeapon) { sequence.push({ unit: defender, target: attacker, weapon: defenderWeapon, terrainAvoid: atkTerrainAvoid, terrainDef: atkTerrainDef, isCounter: true }); } // Execute sequence for (const action of sequence) { // Skip if either unit is dead if (!action.unit.alive || !action.target.alive) continue; const hitChance = calculateHitChance(action.unit, action.target, action.weapon, action.terrainAvoid); const hit = rollHit(hitChance); let damage = 0; if (hit) { damage = calculateDamage(action.unit, action.target, action.weapon, action.terrainDef); action.target.takeDamage(damage); } result.addAttack(action.unit, action.target, hit, damage, action.isCounter); // Check for deaths if (!action.target.alive) { if (action.target === attacker) { result.attackerDied = true; } else { result.defenderDied = true; } } } return result; } /** * Execute a heal action */ export function executeHeal(healer, target, map) { const weapon = healer.getEquippedWeapon(); if (!weapon || !weapon.isHealingWeapon()) { return { success: false, amount: 0 }; } const distance = MapData.getDistance(healer.row, healer.col, target.row, target.col); if (!weapon.canReach(distance)) { return { success: false, amount: 0 }; } const healAmount = weapon.heal; const hpBefore = target.hp; target.heal(healAmount); const actualHeal = target.hp - hpBefore; return { success: true, amount: actualHeal, healerName: healer.name, targetName: target.name }; } /** * Apply throne healing to Ephraim * Returns heal amount (0 if not on throne or not applicable) */ export function applyThroneHeal(ephraim, map, wasDamagedThisTurn, gameRules) { const tile = map.getTile(ephraim.row, ephraim.col); if (!tile || !tile.isThrone()) { return 0; } const healAmount = wasDamagedThisTurn ? (gameRules?.throne_heal_damaged ?? 4) : (gameRules?.throne_heal_untouched ?? 7); const hpBefore = ephraim.hp; ephraim.heal(healAmount); return ephraim.hp - hpBefore; } /** * Remove a dead unit from the map */ export function removeDeadUnit(unit, map) { const tile = map.getTile(unit.row, unit.col); if (tile && tile.unit === unit) { tile.removeUnit(); } } /** * Move a unit to a new position */ export function moveUnit(unit, targetRow, targetCol, map) { // Remove from current tile const currentTile = map.getTile(unit.row, unit.col); if (currentTile) { currentTile.removeUnit(); } // Place on new tile const targetTile = map.getTile(targetRow, targetCol); if (targetTile) { targetTile.placeUnit(unit); return true; } return false; } /** * Generate combat log entries from a combat result */ export function generateCombatLog(result) { const entries = []; for (const attack of result.attacks) { if (attack.hit) { let entry = `${attack.attackerName} hit ${attack.defenderName} for ${attack.damage}`; if (attack.isCounter) { entry = `${attack.attackerName} countered for ${attack.damage}`; } entries.push({ text: entry, type: attack.isCounter ? 'counter' : 'attack' }); } else { entries.push({ text: `${attack.attackerName} missed`, type: 'miss' }); } } if (result.defenderDied) { const lastAttack = result.attacks[result.attacks.length - 1]; entries.push({ text: `${lastAttack.defenderName} was defeated`, type: 'death' }); } if (result.attackerDied) { const lastAttack = result.attacks[result.attacks.length - 1]; entries.push({ text: `${lastAttack.attackerName} was defeated`, type: 'death' }); } return entries; }