nodes/combat.pyΒΆ

Part of Tanks of Freedom.

 1"""Pure-function combat resolution.
 2
 3Mirrors upstream's `battle_stats.gd` shape: attacker hits first, defender
 4retaliates if still alive. Damage scales with attacker HP fraction so wounded
 5units hit softer (matches the upstream spec: ``attack * life/max_life``).
 6"""
 7
 8from __future__ import annotations
 9
10from dataclasses import dataclass
11
12from .data import UNIT_STATS, can_attack_unit_type
13
14
15@dataclass
16class CombatResult:
17    attacker_alive: bool
18    defender_alive: bool
19    attacker_dmg_taken: int
20    defender_dmg_taken: int
21
22
23def _hit_amount(att_unit, def_unit) -> int:
24    base = UNIT_STATS[att_unit.type]["attack"]
25    frac = att_unit.life / UNIT_STATS[att_unit.type]["max_life"]
26    return max(1, int(round(base * frac)))
27
28
29def resolve_attack(attacker, defender) -> CombatResult:
30    """Apply damage in-place. ``life`` decremented; mutate the unit objects."""
31    # Attacker must spend attack_ap; caller checks ap before calling.
32    dmg_to_def = _hit_amount(attacker, defender)
33    defender.life = max(0, defender.life - dmg_to_def)
34
35    dmg_to_att = 0
36    if defender.life > 0 and can_attack_unit_type(defender.type, attacker.type):
37        # Defender retaliation at half strength (upstream allows full retaliation;
38        # we soften slightly so ranged/glass units don't trade 1:1 every clash).
39        retal = max(1, _hit_amount(defender, attacker) // 2)
40        dmg_to_att = retal
41        attacker.life = max(0, attacker.life - retal)
42
43    return CombatResult(
44        attacker_alive=attacker.life > 0,
45        defender_alive=defender.life > 0,
46        attacker_dmg_taken=dmg_to_att,
47        defender_dmg_taken=dmg_to_def,
48    )