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 )