Spaces:
Running
Running
| /** | |
| * 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; | |
| } | |