/** * Adaptive Tactics - Combat Tests * Tests for combat execution, healing, death handling */ import { Unit, Weapon, MapData } from '../js/models.js'; import { executeCombat, executeHeal, CombatResult, generateCombatLog } from '../js/combat.js'; // Test utilities function assert(condition, message) { if (!condition) { throw new Error(`FAILED: ${message}`); } console.log(`PASSED: ${message}`); } function createTestWeapon(overrides = {}) { return new Weapon({ id: 'test_sword', name: 'Test Sword', kind: 'physical', might: 5, hit: 100, // 100% hit for deterministic tests minRange: 1, maxRange: 1, heal: 0, ...overrides }); } function createTestUnit(id, faction, weaponOverrides = {}) { const weapon = createTestWeapon(weaponOverrides); return new Unit({ id: id, name: `Test ${id}`, sprite: 'test.png', faction: faction, stats: { hp: 30, maxHp: 30, power: 10, skill: 15, speed: 10, def: 5, res: 5, mov: 5 }, weapons: [weapon.id], equippedWeapon: weapon.id }, { [weapon.id]: weapon }); } function createTestMap() { const mapData = new MapData({ id: 'test_map', name: 'Test Map', rows: 5, cols: 5, tiles: { blocked: [], bush: [], throne: [] }, player_spawn: {}, enemy_layout: {}, flex_nodes: {}, reinforcement_entries: {} }); mapData.initTiles(); return mapData; } const testGameRules = { double_speed_gap: 4 }; // Test: Basic Combat Exchange function testBasicCombat() { const map = createTestMap(); const attacker = createTestUnit('attacker', 'player', { hit: 100 }); const defender = createTestUnit('defender', 'enemy', { hit: 100 }); attacker.power = 12; attacker.setPosition(2, 2); defender.def = 5; defender.setPosition(2, 3); map.getTile(2, 2).placeUnit(attacker); map.getTile(2, 3).placeUnit(defender); const result = executeCombat(attacker, defender, map, testGameRules); // Damage: 12 + 5 - 5 = 12 assert(result.attacks.length >= 1, 'Should have at least 1 attack'); assert(result.attacks[0].damage === 12, `First attack should deal 12 damage, got ${result.attacks[0].damage}`); assert(defender.hp === 18, `Defender should have 18 HP remaining, got ${defender.hp}`); } // Test: Counter Attack function testCounterAttack() { const map = createTestMap(); const attacker = createTestUnit('attacker', 'player', { hit: 100 }); const defender = createTestUnit('defender', 'enemy', { hit: 100 }); attacker.power = 10; defender.power = 8; attacker.setPosition(2, 2); defender.setPosition(2, 3); map.getTile(2, 2).placeUnit(attacker); map.getTile(2, 3).placeUnit(defender); const result = executeCombat(attacker, defender, map, testGameRules); // Both should have acted const attackerAttacks = result.attacks.filter(a => a.attackerId === 'attacker'); const defenderCounters = result.attacks.filter(a => a.attackerId === 'defender' && a.isCounter); assert(attackerAttacks.length >= 1, 'Attacker should have attacked'); assert(defenderCounters.length >= 1, 'Defender should have countered'); } // Test: No Counter at Range 2 with Sword function testNoCounterAtRange2() { const map = createTestMap(); const attacker = createTestUnit('attacker', 'player', { id: 'spear', minRange: 1, maxRange: 2, hit: 100 }); const defender = createTestUnit('defender', 'enemy', { id: 'sword', minRange: 1, maxRange: 1, hit: 100 }); attacker.setPosition(2, 2); defender.setPosition(2, 4); // 2 tiles away map.getTile(2, 2).placeUnit(attacker); map.getTile(2, 4).placeUnit(defender); const result = executeCombat(attacker, defender, map, testGameRules); const defenderCounters = result.attacks.filter(a => a.attackerId === 'defender'); assert(defenderCounters.length === 0, 'Sword defender should not counter at range 2'); } // Test: Doubling function testDoubling() { const map = createTestMap(); const attacker = createTestUnit('attacker', 'player', { hit: 100 }); const defender = createTestUnit('defender', 'enemy', { hit: 100 }); attacker.speed = 15; defender.speed = 10; // 5 speed difference, should double attacker.setPosition(2, 2); defender.setPosition(2, 3); map.getTile(2, 2).placeUnit(attacker); map.getTile(2, 3).placeUnit(defender); const result = executeCombat(attacker, defender, map, testGameRules); const attackerAttacks = result.attacks.filter(a => a.attackerId === 'attacker'); assert(attackerAttacks.length === 2, `Attacker should attack twice (double), got ${attackerAttacks.length}`); } // Test: Death During Combat function testDeathDuringCombat() { const map = createTestMap(); const attacker = createTestUnit('attacker', 'player', { hit: 100, might: 30 }); const defender = createTestUnit('defender', 'enemy', { hit: 100 }); attacker.power = 30; // Will one-shot defender.hp = 20; defender.maxHp = 20; attacker.setPosition(2, 2); defender.setPosition(2, 3); map.getTile(2, 2).placeUnit(attacker); map.getTile(2, 3).placeUnit(defender); const result = executeCombat(attacker, defender, map, testGameRules); assert(result.defenderDied === true, 'Defender should be marked as died'); assert(defender.alive === false, 'Defender should be dead'); assert(defender.hp === 0, 'Defender HP should be 0'); } // Test: Defender Dies Before Counter function testDefenderDiesBeforeCounter() { const map = createTestMap(); const attacker = createTestUnit('attacker', 'player', { hit: 100 }); const defender = createTestUnit('defender', 'enemy', { hit: 100 }); attacker.power = 30; defender.hp = 10; defender.maxHp = 10; defender.power = 20; // Would deal big damage if alive attacker.setPosition(2, 2); defender.setPosition(2, 3); map.getTile(2, 2).placeUnit(attacker); map.getTile(2, 3).placeUnit(defender); const result = executeCombat(attacker, defender, map, testGameRules); const defenderCounters = result.attacks.filter(a => a.attackerId === 'defender'); assert(defenderCounters.length === 0, 'Dead defender should not counter'); assert(attacker.hp === 30, 'Attacker should be at full HP'); } // Test: Healing function testHealing() { const map = createTestMap(); const healWeapon = new Weapon({ id: 'staff', name: 'Heal Staff', kind: 'heal', might: 0, hit: 100, minRange: 1, maxRange: 1, heal: 6 }); const healer = new Unit({ id: 'healer', name: 'Healer', sprite: 'test.png', faction: 'player', stats: { hp: 20, maxHp: 20, power: 5, skill: 10, speed: 8, def: 3, res: 10, mov: 4 }, weapons: ['staff'], equippedWeapon: 'staff' }, { staff: healWeapon }); const target = createTestUnit('target', 'player'); target.hp = 15; target.maxHp = 30; healer.setPosition(2, 2); target.setPosition(2, 3); map.getTile(2, 2).placeUnit(healer); map.getTile(2, 3).placeUnit(target); const result = executeHeal(healer, target, map); assert(result.success === true, 'Heal should succeed'); assert(result.amount === 6, `Should heal for 6, got ${result.amount}`); assert(target.hp === 21, `Target should have 21 HP, got ${target.hp}`); } // Test: Overheal Capped at MaxHP function testOverhealCapped() { const map = createTestMap(); const healWeapon = new Weapon({ id: 'staff', name: 'Heal Staff', kind: 'heal', might: 0, hit: 100, minRange: 1, maxRange: 1, heal: 10 }); const healer = new Unit({ id: 'healer', name: 'Healer', sprite: 'test.png', faction: 'player', stats: { hp: 20, maxHp: 20, power: 5, skill: 10, speed: 8, def: 3, res: 10, mov: 4 }, weapons: ['staff'], equippedWeapon: 'staff' }, { staff: healWeapon }); const target = createTestUnit('target', 'player'); target.hp = 28; target.maxHp = 30; healer.setPosition(2, 2); target.setPosition(2, 3); map.getTile(2, 2).placeUnit(healer); map.getTile(2, 3).placeUnit(target); const result = executeHeal(healer, target, map); assert(result.success === true, 'Heal should succeed'); assert(result.amount === 2, `Should only heal 2 (to cap), got ${result.amount}`); assert(target.hp === 30, `Target should be at max HP 30, got ${target.hp}`); } // Test: Combat Log Generation function testCombatLogGeneration() { const result = new CombatResult(); result.addAttack( { id: 'a', name: 'Attacker' }, { id: 'd', name: 'Defender' }, true, 12, false ); result.addAttack( { id: 'd', name: 'Defender' }, { id: 'a', name: 'Attacker' }, false, 0, true ); const logs = generateCombatLog(result); assert(logs.length === 2, `Should have 2 log entries, got ${logs.length}`); assert(logs[0].text.includes('hit') && logs[0].text.includes('12'), 'First log should mention hit and damage'); assert(logs[1].text.includes('missed'), 'Second log should mention miss'); } // Run all tests function runAllTests() { console.log('=== Running Combat Tests ===\n'); try { testBasicCombat(); testCounterAttack(); testNoCounterAtRange2(); testDoubling(); testDeathDuringCombat(); testDefenderDiesBeforeCounter(); testHealing(); testOverhealCapped(); testCombatLogGeneration(); console.log('\n=== All Combat Tests Passed ==='); } catch (e) { console.error('\n=== Test Failed ==='); console.error(e.message); throw e; } } // Export for use in browser or Node if (typeof window !== 'undefined') { window.runCombatTests = runAllTests; } export { runAllTests };