fix(rules): natural weapons get no BAB iteratives, secondary attacks at -5

PF1e (CRB "Natural Attacks"):
- Natural weapons (natural=true) get exactly one swing per full attack,
  regardless of BAB (no BAB iteratives for natural attacks).
- Secondary natural attacks (secondary=true) attack at BAB -5; the
  Multiattack feat reduces this to -2.
- Manifested in combat.py (_full_attack_weapon, _resolve_swing),
  models.py (AttackSpec.natural/secondary), 6 data JSONs, 4 new tests,
  and README documentation.

1461 passed, ruff clean, basedpyright 0 errors.
This commit is contained in:
2026-09-16 12:25:30 +02:00
parent 59caf67909
commit e5691bdb6f
10 changed files with 739 additions and 508 deletions
+12
View File
@@ -6699,6 +6699,12 @@ class CombatEngine:
target_live = self._stats[target.combatant.id]
hp_before = target.hp
override = bonus_override
if weapon.secondary:
# PF1e (CRB "Natural Attacks"): secondary natural attacks are at
# base attack bonus -5; the Multiattack feat reduces this to -2.
penalty = 2 if self._has_feat(attacker, "Multiattack") else 5
base = weapon.attack_bonus if override is None else override
override = base - penalty
if attack_penalty:
base = weapon.attack_bonus if override is None else override
override = base + attack_penalty
@@ -6829,6 +6835,12 @@ class CombatEngine:
if weapon.count > 1:
self._attack(attacker, target, weapon, attack_penalty=attack_penalty)
return
if weapon.natural:
# PF1e (CRB "Natural Attacks"): "You do not receive additional
# natural attacks for a high base attack bonus." One swing per
# natural weapon, at its own attack bonus.
self._attack(attacker, target, weapon, attack_penalty=attack_penalty)
return
n = min(
_MAX_ITERATIVES,
1 + max(0, (attacker.combatant.bab - 1) // _ITERATIVE_PENALTY),