fix(rules): Weapon Focus filter matches weapon names case-insensitively (substring)

The filter 'Pistol' now matches 'Braises d'Automne (pistolet ?)' via
case-insensitive substring matching, so the feat is no longer dead.
Golden test_tammara_vs_goblin updated: round-4 pistol shot gains the +1
feat bonus (total 15 -> 16 vs AC 16), turning the MISS into a lethal HIT
(17 damage, goblin 6 -> -11); the goblin dies on round 4 so the old
rounds 4-10 exchange and the draw are gone.
This commit is contained in:
2026-09-16 19:45:56 +02:00
parent f31cc88124
commit 4d45409941
3 changed files with 64 additions and 20 deletions
+29
View File
@@ -16,11 +16,16 @@ from pf1e_simulator.abilities import (
MOUNTED_COMBAT,
POINT_BLANK_SHOT,
)
from pf1e_simulator.combat import CombatantState, CombatEngine
from pf1e_simulator.grid import Grid
from pf1e_simulator.loaders.combatant import (
_KNOWN_FEATS,
_NOOP_FEATS,
_resolve_feat,
load_combatant,
)
from pf1e_simulator.map import MapSpec, TerrainType
from pf1e_simulator.rng import ScriptedRng
PC_FILES = [
"data/pcs/esha.json",
@@ -100,6 +105,30 @@ def test_spot_check_weapon_focus_pistol() -> None:
assert any("Pistol" in str(e.weapon_filter) for e in spec.effects)
def test_weapon_focus_applies_to_real_tammara_pistols() -> None:
# The feat filter "Pistol" must match tammara's real weapon names
# ("Braises d'Automne (pistolet ?)", "Adieux du Printemps
# (pistolet, masterwork)") via case-insensitive substring matching,
# and must NOT match her rapier or alchemist's fire.
tammara = load_combatant(Path("data/pcs/tammara.json"))
state = CombatantState(
combatant=tammara, side="players", pos=(0, 0), hp=tammara.hp_max
)
legend = {".": TerrainType(type="floor", move_cost=1)}
grid = Grid.from_spec(MapSpec(name="t", terrain=tuple([".." for _ in range(2)]), legend=legend))
engine = CombatEngine(ScriptedRng([]), grid, [state])
def attack_total(weapon_name: str | None) -> int:
mods = engine._stat_modifiers(state, "attack", weapon_name=weapon_name)
return sum(m.value for m in mods if m.weapon_filter is not None)
assert attack_total("Braises d'Automne (pistolet ?)") == 1
assert attack_total("Adieux du Printemps (pistolet, masterwork)") == 1
assert attack_total("Rapier (masterwork)") == 0
assert attack_total("Alchemist's Fire") == 0
assert attack_total(None) == 0
def test_spot_check_toughness_level_tied() -> None:
spec = _resolve_feat("Toughness", 5)
assert spec.name == "Toughness"