From 6e27a2bba12b8cbc525754e14c54efe2f647f788 Mon Sep 17 00:00:00 2001 From: ctan Date: Fri, 4 Sep 2026 22:55:37 +0200 Subject: [PATCH] fix(combat): correct spell DC formula --- data/pcs/esha.json | 2 ++ data/pcs/harvie.json | 2 ++ data/pcs/ierlieth.json | 2 ++ data/pcs/jeanne.json | 2 ++ data/pcs/misty.json | 4 ++++ data/pcs/nairda.json | 4 ++++ data/pcs/oni.json | 4 ++++ data/pcs/tammara.json | 4 ++++ src/pf1e_simulator/combat.py | 7 +++++-- src/pf1e_simulator/models.py | 2 ++ tests/test_spells.py | 17 +++++++++-------- 11 files changed, 40 insertions(+), 10 deletions(-) diff --git a/data/pcs/esha.json b/data/pcs/esha.json index be07283..affc986 100644 --- a/data/pcs/esha.json +++ b/data/pcs/esha.json @@ -24,6 +24,8 @@ "ref": 10, "will": 5 }, + "casting_ability": "int", + "resources": {}, "attacks": [ { "name": "Lance", diff --git a/data/pcs/harvie.json b/data/pcs/harvie.json index b1acc14..69a6301 100644 --- a/data/pcs/harvie.json +++ b/data/pcs/harvie.json @@ -24,6 +24,8 @@ "ref": 5, "will": 2 }, + "casting_ability": "wis", + "resources": {}, "attacks": [ { "name": "Talons (attaque naturelle)", diff --git a/data/pcs/ierlieth.json b/data/pcs/ierlieth.json index b75de22..889c75d 100644 --- a/data/pcs/ierlieth.json +++ b/data/pcs/ierlieth.json @@ -24,6 +24,8 @@ "ref": 6, "will": 3 }, + "casting_ability": "wis", + "resources": {}, "attacks": [ { "name": "Bite (attaque naturelle)", diff --git a/data/pcs/jeanne.json b/data/pcs/jeanne.json index 1b37c4c..4e82611 100644 --- a/data/pcs/jeanne.json +++ b/data/pcs/jeanne.json @@ -24,6 +24,8 @@ "ref": 6, "will": 2 }, + "casting_ability": "wis", + "resources": {}, "attacks": [ { "name": "Hooves (attaque naturelle)", diff --git a/data/pcs/misty.json b/data/pcs/misty.json index 503c9d8..064f219 100644 --- a/data/pcs/misty.json +++ b/data/pcs/misty.json @@ -24,6 +24,10 @@ "ref": 9, "will": 3 }, + "casting_ability": "cha", + "resources": { + "bardic_rounds": 12 + }, "attacks": [ { "name": "MWK Rapier", diff --git a/data/pcs/nairda.json b/data/pcs/nairda.json index ea2f7f2..c24eec9 100644 --- a/data/pcs/nairda.json +++ b/data/pcs/nairda.json @@ -24,6 +24,10 @@ "ref": 5, "will": 7 }, + "casting_ability": "int", + "resources": { + "hexes": 5 + }, "attacks": [ { "name": "Coup à mains nues (aucune arme encodée)", diff --git a/data/pcs/oni.json b/data/pcs/oni.json index 0ea6a68..ce6fb2d 100644 --- a/data/pcs/oni.json +++ b/data/pcs/oni.json @@ -24,6 +24,10 @@ "ref": 7, "will": 9 }, + "casting_ability": "wis", + "resources": { + "channel_energy": 6 + }, "attacks": [ { "name": "Longbow", diff --git a/data/pcs/tammara.json b/data/pcs/tammara.json index 441d9ab..fcd030e 100644 --- a/data/pcs/tammara.json +++ b/data/pcs/tammara.json @@ -24,6 +24,10 @@ "ref": 10, "will": 8 }, + "casting_ability": "wis", + "resources": { + "grit": 3 + }, "attacks": [ { "name": "Rapier (masterwork)", diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index e0eb1e5..a6d0148 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -834,8 +834,11 @@ class CombatEngine: f"round {self._current_round} {caster.combatant.id}: " f"cast {spell_name} -> NO LINE OF EFFECT" ) - return - dc = spell.base_dc + caster.combatant.level + # DC = 10 + spell level + caster's casting ability modifier + ability_name = caster.combatant.casting_ability.lower() + ability_score = getattr(caster.combatant.abilities, f"{ability_name}_score") + ability_mod = (ability_score - 10) // 2 + dc = 10 + spell.level + ability_mod save_result = ( self.resolve_save(target, spell.save, dc) if spell.save != "none" else None ) diff --git a/src/pf1e_simulator/models.py b/src/pf1e_simulator/models.py index 6015047..e2fcdc5 100644 --- a/src/pf1e_simulator/models.py +++ b/src/pf1e_simulator/models.py @@ -113,3 +113,5 @@ class Combatant(_FrozenModel): notes: str = "" features: list[AbilitySpec] = [] spells: list[str] = [] + casting_ability: str = "wis" # ability used for spell DCs: str/dex/con/int/wis/cha + resources: dict[str, int] = Field(default_factory=dict) # resource pools per combat diff --git a/tests/test_spells.py b/tests/test_spells.py index f9c5853..e9aa67b 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -56,6 +56,7 @@ def _make_caster(cid: str = "caster", *, level: int = 5) -> Combatant: speed_land_ft=30, saves=Saves(fort=3, ref=4, will=4), attacks=[attack], + casting_ability="int", ) @@ -366,18 +367,18 @@ class TestSpellDispatchViaExecute: ) assert t.hp < hp_before - class TestSpellDcScalesWithLevel: - """Given: a caster with level N - When: a spell with base_dc 10 is cast - Then: the DC is 10 + N.""" + """Given: a caster with level N and casting ability mod M + When: a spell of level L is cast + Then: the DC is 10 + L + M.""" - def test_dc_is_base_plus_level(self) -> None: - caster = _make_caster(level=5) + def test_dc_is_ten_plus_spell_level_plus_ability_mod(self) -> None: + caster = _make_caster(level=5) # int 18 -> mod +4 target = _make_target() engine, c, t = _make_engine(caster, target, [20, 1, 1, 1, 1, 1, 1]) - engine._cast_spell(c, t, "Fireball") - assert any("DC15" in line for line in engine._transcript) + engine._cast_spell(c, t, "Fireball") # fireball is level 3 + # DC = 10 + spell_level(3) + int_mod(4) = 17 + assert any("DC17" in line for line in engine._transcript) class TestSpellDamageWithDr: