From 10f811e80ec70eb659587eacbfb8469923b8a93b Mon Sep 17 00:00:00 2001 From: ctan Date: Sun, 20 Sep 2026 17:36:45 +0200 Subject: [PATCH] fix(models): require a positive range_increment_ft (reject 0 and negatives) --- README.md | 3 ++- src/pf1e_simulator/models.py | 6 +++++- tests/test_models.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1faed75..ee3313f 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,8 @@ passe par les conditions (`miss_chance`), pas par la carte. ``` Champ `range_increment_ft` (optionnel, uniquement pour `kind: ranged` ou -`kind: touch`) : incrément de portée en pieds. La pénalité de portée vaut −2 +`kind: touch`) : incrément de portée en pieds, entier **strictement positif** +(zéro ou négatif = erreur de données, rejetée au chargement). La pénalité de portée vaut −2 par incrément complet au-delà du premier ; une arme `ranged` reste utilisable jusqu'à 10 incréments, une arme `touch` jusqu'à 5 (jet/firearm), au-delà elle n'est pas sélectionnable. `damage_bonus` et `dr` sont optionnels. diff --git a/src/pf1e_simulator/models.py b/src/pf1e_simulator/models.py index cd6b825..1f3420b 100644 --- a/src/pf1e_simulator/models.py +++ b/src/pf1e_simulator/models.py @@ -278,7 +278,11 @@ class AttackSpec(_FrozenModel): crit_mult: int = Field(default=2, ge=2, le=4) reach_ft: int = 5 reach_weapon: bool = False - range_increment_ft: int | None = None + # Positive or None (None = no range increments: melee, or touch at natural + # reach). A zero increment would crash the engine's range_penalty with a + # ZeroDivisionError (dist_ft // 0); a negative one silently makes the + # weapon unusable -- both are data errors, rejected at the schema. + range_increment_ft: int | None = Field(default=None, ge=1) count: int = Field(default=1, ge=1) firearm: bool = False two_handed: bool = False diff --git a/tests/test_models.py b/tests/test_models.py index 0fc77f5..e25d5aa 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -116,6 +116,21 @@ class TestAttackSpec: with pytest.raises(ValidationError): AttackSpec.model_validate(_attack_dict(damage=[])) + @pytest.mark.parametrize("increment", [0, -30]) + def test_rejects_non_positive_range_increment(self, increment: int) -> None: + # Given a range_increment_ft of 0 (ZeroDivisionError in range_penalty) + # or negative (silently unusable weapon) + # When/Then validation rejects it + with pytest.raises(ValidationError): + AttackSpec.model_validate(_attack_dict(kind="ranged", range_increment_ft=increment)) + + def test_accepts_positive_range_increment(self) -> None: + # Given a legal range increment + # When validated + attack = AttackSpec.model_validate(_attack_dict(kind="ranged", range_increment_ft=30)) + # Then it is kept + assert attack.range_increment_ft == 30 + def test_rejects_crit_mult_below_2(self) -> None: # Given crit_mult=1 (a weapon that never multiplies is not PF1e) # When/Then validation rejects it