fix(rules): correct the inspire courage bonus type and saves
PF1e CRB (Bardic Performance - Inspire Courage): the attack/weapon-damage bonus is a COMPETENCE bonus (not morale), so it stacks with morale sources like Heroism; the performance also grants a +1 MORALE bonus on saving throws against charm and fear effects, implemented as two descriptor-conditional will modifiers (fear / mind_affecting) that dedupe via morale stacking. Simplifications (documented): flat +1 (no scaling per four bard levels) and mind_affecting stands in for the missing charm descriptor. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Literal
|
||||
|
||||
from pf1e_simulator.combat import Action, CombatantState, CombatEngine, default_policy
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.effects import ActiveBuff, StatModifier, resolve_modifiers
|
||||
from pf1e_simulator.grid import Grid
|
||||
from pf1e_simulator.map import MapSpec, TerrainType
|
||||
from pf1e_simulator.models import (
|
||||
@@ -375,3 +376,61 @@ def test_normal_full_attack_has_no_spell_combat_penalty() -> None:
|
||||
assert any(
|
||||
"sword vs goblin d20=10+3=13" in line for line in engine._transcript
|
||||
)
|
||||
|
||||
|
||||
def _inspired_ally() -> tuple[CombatEngine, CombatantState, CombatantState]:
|
||||
"""Misty + one ally in 30ft, inspire courage executed on the ally."""
|
||||
misty = _combatant("misty", resources={"bardic_rounds": 12})
|
||||
ally = _combatant("ally")
|
||||
engine, me, _ = _engine(misty, [_combatant("goblin", hp_max=6)])
|
||||
ally_state = _state(ally, "players", (5, 7))
|
||||
engine.states.append(ally_state)
|
||||
engine._execute(
|
||||
me, Action(kind="inspire_courage", target_id=ally_state.combatant.id)
|
||||
)
|
||||
return engine, me, ally_state
|
||||
|
||||
|
||||
def test_inspire_courage_grants_competence_attack_damage() -> None:
|
||||
"""CRB Inspire Courage: the attack/damage bonus is COMPETENCE (not morale)."""
|
||||
_, _, ally = _inspired_ally()
|
||||
mods = {
|
||||
(m.target, m.bonus_type): m.value
|
||||
for buff in ally.active_buffs
|
||||
for m in buff.modifiers
|
||||
}
|
||||
assert mods[("attack", "competence")] == 1
|
||||
assert mods[("damage", "competence")] == 1
|
||||
|
||||
|
||||
def test_inspire_courage_stacks_with_heroism_morale() -> None:
|
||||
"""CRB stacking: +1 competence (inspire courage) and +2 morale (heroism)
|
||||
are different bonus types — both apply to the attack roll."""
|
||||
engine, _, ally = _inspired_ally()
|
||||
ally.active_buffs.append(
|
||||
ActiveBuff(
|
||||
modifiers=(StatModifier(target="attack", value=2, bonus_type="morale"),),
|
||||
rounds_left=10,
|
||||
source="heroism",
|
||||
)
|
||||
)
|
||||
total = resolve_modifiers(engine._stat_modifiers(ally, "attack"))
|
||||
assert total == 3 # competence +1 + morale +2
|
||||
|
||||
|
||||
def test_inspire_courage_morale_bonus_on_charm_and_fear_saves() -> None:
|
||||
"""CRB Inspire Courage: +1 morale on saves vs charm (mind_affecting) and
|
||||
fear; morale never stacks with itself, so both descriptors total +1."""
|
||||
engine, _, ally = _inspired_ally()
|
||||
|
||||
def will_bonus(*descriptors: str) -> int:
|
||||
return resolve_modifiers(
|
||||
engine._stat_modifiers(
|
||||
ally, "will", save_descriptors=frozenset(descriptors)
|
||||
)
|
||||
)
|
||||
|
||||
assert will_bonus("fear") == 1
|
||||
assert will_bonus("mind_affecting") == 1
|
||||
assert will_bonus("fear", "mind_affecting") == 1
|
||||
assert will_bonus() == 0
|
||||
|
||||
Reference in New Issue
Block a user