feat(combat): resolve all natural melee weapons in full_attack

This commit is contained in:
2026-09-06 10:22:58 +02:00
parent fdcd103e8a
commit 9c49f85af8
2 changed files with 247 additions and 4 deletions
+42 -4
View File
@@ -2295,13 +2295,13 @@ class CombatEngine:
return
if target is None:
return
weapon = self.weapon_for(state, target)
if weapon is None:
return
if kind == "attack":
weapon = self.weapon_for(state, target)
if weapon is None:
return
self._single_attack(state, target, weapon)
elif kind == "full_attack":
self._full_attack(state, target, weapon)
self._full_attack(state, target)
def _execute_charge(self, state: CombatantState, target: CombatantState | None) -> None:
if target is None or not target.active:
return
@@ -2819,8 +2819,30 @@ class CombatEngine:
self._resolve_swing(attacker, target, weapon, label=weapon.name)
def _full_attack(
self, attacker: CombatantState, target: CombatantState
) -> None:
melee_weapons = [
w
for w in attacker.usable_attacks()
if w.kind == "melee" and self._melee_weapon_usable(attacker, target, w)
]
if not melee_weapons:
# No melee weapon reaches: fall back to weapon_for (ranged/touch),
# preserving the pre-L3 single-weapon behavior.
weapon = self.weapon_for(attacker, target)
if weapon is None:
return
self._full_attack_weapon(attacker, target, weapon)
return
for weapon in melee_weapons:
if not attacker.active or not target.active:
return
self._full_attack_weapon(attacker, target, weapon)
def _full_attack_weapon(
self, attacker: CombatantState, target: CombatantState, weapon: AttackSpec
) -> None:
"""Resolve one weapon's full-attack swings (count swings or BAB iteratives)."""
if weapon.count > 1:
self._attack(attacker, target, weapon)
return
@@ -2831,6 +2853,8 @@ class CombatEngine:
for i in range(n):
if not attacker.active:
return
if not target.active:
return
if self._check_provoked_aoo(attacker, weapon):
return
label = weapon.name if n == 1 else f"{weapon.name} #{i + 1}"
@@ -2840,6 +2864,20 @@ class CombatEngine:
):
return
def _melee_weapon_usable(
self, attacker: CombatantState, target: CombatantState, weapon: AttackSpec
) -> bool:
"""True if ``weapon`` (a melee weapon) can reach ``target`` with clear LoE."""
if not has_line_of_effect_cells(
self._grid, attacker.occupied_cells(), target.occupied_cells()
):
return False
dist_ft = self.creature_distance(attacker, target) * _SQUARE_FT
effective_reach_ft = _effective_reach_ft(attacker, weapon)
if weapon.reach_ft > _SQUARE_FT:
return _SQUARE_FT < dist_ft <= effective_reach_ft
return dist_ft <= effective_reach_ft
def _step5(self, state: CombatantState, target: CombatantState) -> None:
if state.moved_this_turn:
return