AdaptiveTactics / js /combat.js
EphAsad's picture
Upload 61 files
076c3cb verified
/**
* 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;
}