fix(rules): Lonely and Bitter drawbacks replace bogus +1 Will bonuses (SRD: Lonely -2 Sense Motive and -2 saves vs charm with mind_affecting standing in; Bitter reduces ally-sourced healing by 1 via engine hook)
This commit is contained in:
@@ -13,16 +13,20 @@ from pathlib import Path
|
||||
|
||||
from pf1e_simulator.abilities import (
|
||||
AXE_TO_GRIND,
|
||||
BITTER,
|
||||
BORN_DAMNED,
|
||||
COMBAT_REFLEXES,
|
||||
EXALTED_SOCIETY,
|
||||
EXTREMELY_FASHIONABLE,
|
||||
FORTIFIED_DRINKER,
|
||||
FRIENDLY_FACE,
|
||||
LONELY,
|
||||
MOUNTED_COMBAT,
|
||||
POINT_BLANK_SHOT,
|
||||
AbilitySpec,
|
||||
)
|
||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.grid import Grid
|
||||
from pf1e_simulator.loaders.combatant import (
|
||||
_KNOWN_FEATS,
|
||||
@@ -31,7 +35,16 @@ from pf1e_simulator.loaders.combatant import (
|
||||
load_combatant,
|
||||
)
|
||||
from pf1e_simulator.map import MapSpec, TerrainType
|
||||
from pf1e_simulator.models import (
|
||||
AbilityScores,
|
||||
ACProfile,
|
||||
AttackSpec,
|
||||
Combatant,
|
||||
DamageComponent,
|
||||
Saves,
|
||||
)
|
||||
from pf1e_simulator.rng import ScriptedRng
|
||||
from pf1e_simulator.spells import HealEffect
|
||||
|
||||
PC_FILES = [
|
||||
"data/pcs/esha.json",
|
||||
@@ -290,3 +303,95 @@ def test_extremely_fashionable_is_inert_documented_noop() -> None:
|
||||
inert no-op (old flat +4 Diplomacy was the wrong value and unconditional)."""
|
||||
assert EXTREMELY_FASHIONABLE.effects == ()
|
||||
assert "not modeled" in EXTREMELY_FASHIONABLE.source_detail
|
||||
|
||||
|
||||
def _make_battle_combatant(
|
||||
cid: str, *, features: tuple[AbilitySpec, ...] = (), hp_max: int = 30
|
||||
) -> Combatant:
|
||||
return Combatant(
|
||||
id=cid,
|
||||
name=cid,
|
||||
level=1,
|
||||
size="Medium",
|
||||
abilities=AbilityScores(
|
||||
str_score=12, dex_score=12, con_score=12,
|
||||
int_score=10, wis_score=10, cha_score=10,
|
||||
),
|
||||
hp_max=hp_max,
|
||||
ac=ACProfile(total=12, touch=12, flat_footed=10),
|
||||
bab=1,
|
||||
initiative_mod=0,
|
||||
speed_land_ft=30,
|
||||
saves=Saves(fort=0, ref=0, will=0),
|
||||
attacks=[
|
||||
AttackSpec(
|
||||
id=f"{cid}-w",
|
||||
name="club",
|
||||
kind="melee",
|
||||
attack_bonus=1,
|
||||
damage=[DamageComponent(formula=parse_dice("1d6"), types=["bludgeoning"])],
|
||||
)
|
||||
],
|
||||
features=list(features),
|
||||
)
|
||||
|
||||
|
||||
def _bitter_pair() -> tuple[CombatEngine, CombatantState, CombatantState]:
|
||||
"""Two same-side states: a plain healer and a target carrying Bitter."""
|
||||
healer_c = _make_battle_combatant("healer")
|
||||
bitter_c = _make_battle_combatant("bitter", features=(BITTER,))
|
||||
healer = CombatantState(combatant=healer_c, side="players", pos=(0, 0), hp=30)
|
||||
target = CombatantState(combatant=bitter_c, side="players", pos=(1, 0), hp=10)
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
grid = Grid.from_spec(
|
||||
MapSpec(name="t", terrain=tuple([".." for _ in range(4)]), legend=legend)
|
||||
)
|
||||
engine = CombatEngine(ScriptedRng([]), grid, [healer, target])
|
||||
return engine, healer, target
|
||||
|
||||
|
||||
def test_lonely_is_a_negative_drawback_not_a_bonus() -> None:
|
||||
"""SRD (drawback): -2 Sense Motive and -2 saves vs charm (mind_affecting
|
||||
stands in). No positive modifier may remain."""
|
||||
assert LONELY.effects
|
||||
assert all(e.value < 0 for e in LONELY.effects), LONELY.effects
|
||||
assert any(e.target == "sense_motive" and e.value == -2 for e in LONELY.effects)
|
||||
will = [e for e in LONELY.effects if e.target == "will"]
|
||||
assert len(will) == 1
|
||||
assert will[0].value == -2
|
||||
assert will[0].condition == "mind_affecting"
|
||||
|
||||
|
||||
def test_bitter_is_a_behavior_marker_without_stat_effects() -> None:
|
||||
"""The drawback is applied by the engine at healing sites, not as a stat
|
||||
modifier - and the old positive '+1 Will vs fear' is gone."""
|
||||
assert BITTER.effects == ()
|
||||
assert "engine behavior" in BITTER.source_detail
|
||||
|
||||
|
||||
def test_bitter_reduces_ally_channel_healing_by_one() -> None:
|
||||
"""SRD (drawback): ally class-feature healing received is reduced by 1."""
|
||||
engine, healer, target = _bitter_pair()
|
||||
engine._channel_heal(healer, target, 6)
|
||||
assert target.hp == 15 # 10 + 6 - 1
|
||||
|
||||
|
||||
def test_bitter_reduces_ally_spell_healing_by_one() -> None:
|
||||
"""Ally spell healing is reduced by 1 as well (rolled 8 -> heals 7)."""
|
||||
engine, healer, target = _bitter_pair()
|
||||
engine._rng = ScriptedRng([8])
|
||||
effect = HealEffect(formula=parse_dice("1d8"))
|
||||
engine._apply_heal_effect(
|
||||
effect, target, caster_level=1, empower=False, maximize=False,
|
||||
saved=False, healer=healer,
|
||||
)
|
||||
assert target.hp == 17 # 10 + 8 - 1
|
||||
|
||||
|
||||
def test_bitter_leaves_self_healing_untouched() -> None:
|
||||
"""The drawback is about accepting help from allies, not self-healing."""
|
||||
engine, healer, _ = _bitter_pair()
|
||||
healer.combatant.features.append(BITTER)
|
||||
healer.hp = 10
|
||||
engine._channel_heal(healer, healer, 6)
|
||||
assert healer.hp == 16 # no reduction
|
||||
|
||||
Reference in New Issue
Block a user