fix(rules): aid another bonuses expire at the beginning of the aider's next turn, not the beneficiary's (SRD Aid Another)
This commit is contained in:
@@ -753,10 +753,14 @@ class CombatantState:
|
||||
# Aid another & run (not in the frozen data model): aid bonuses granted to
|
||||
# this combatant by an ally's aid another — aid_attack_bonus is consumed by
|
||||
# its next attack roll, aid_ac_bonus applies against the next attack that
|
||||
# targets it. Both (and the running flag, which denies the Dex bonus to AC
|
||||
# until the runner's next turn) reset at the start of the combatant's turn.
|
||||
# targets it. PF1e (SRD "Aid Another"): the +2 lasts "as long as that
|
||||
# attack comes before the beginning of your next turn" — YOUR = the AIDER,
|
||||
# so each grant is tracked on the aider (target id, mode) and expires at
|
||||
# the start of the aider's next turn. The running flag denies the Dex
|
||||
# bonus to AC until the runner's next turn.
|
||||
aid_attack_bonus: int = 0
|
||||
aid_ac_bonus: int = 0
|
||||
aid_grants: list[tuple[str, str]] = field(default_factory=list)
|
||||
running: bool = False
|
||||
# Bleeding (V5b, not in the frozen data model): damage taken at the start
|
||||
# of each turn; any effect that restores hit points clears it.
|
||||
@@ -1780,8 +1784,7 @@ class CombatEngine:
|
||||
state.defensive_ac_bonus = 0
|
||||
state.fight_defensively = False
|
||||
state.total_defense = False
|
||||
state.aid_attack_bonus = 0
|
||||
state.aid_ac_bonus = 0
|
||||
self._expire_aid_grants(state)
|
||||
state.running = False
|
||||
state.swift_used_this_turn = False
|
||||
original = self._policy(self, state)
|
||||
@@ -4221,6 +4224,7 @@ class CombatEngine:
|
||||
target.aid_ac_bonus += _AID_ANOTHER_BONUS
|
||||
else:
|
||||
target.aid_attack_bonus += _AID_ANOTHER_BONUS
|
||||
state.aid_grants.append((target.combatant.id, mode))
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"aid another {target.combatant.id} ({mode}) "
|
||||
@@ -4229,6 +4233,29 @@ class CombatEngine:
|
||||
)
|
||||
return ActionResult(kind=kind, ok=True, reason=_REASON_OK)
|
||||
|
||||
def _expire_aid_grants(self, aider: CombatantState) -> None:
|
||||
"""Remove this aider's unspent aid bonuses from its beneficiaries.
|
||||
|
||||
PF1e (SRD "Aid Another"): the +2 lasts "as long as that attack comes
|
||||
before the beginning of your next turn" — your = the AIDER's turn, so
|
||||
expiry is tracked on the aider. Each grant subtracts its +2 from the
|
||||
beneficiary's pool (floor 0 — a bonus already consumed by an attack
|
||||
is untouched). Simplification: an aider removed before its next turn
|
||||
never expires its grants (the engine has no death hook for them).
|
||||
"""
|
||||
for target_id, mode in aider.aid_grants:
|
||||
for state in self._states:
|
||||
if state.combatant.id != target_id:
|
||||
continue
|
||||
if mode == "ac":
|
||||
state.aid_ac_bonus = max(0, state.aid_ac_bonus - _AID_ANOTHER_BONUS)
|
||||
else:
|
||||
state.aid_attack_bonus = max(
|
||||
0, state.aid_attack_bonus - _AID_ANOTHER_BONUS
|
||||
)
|
||||
break
|
||||
aider.aid_grants = []
|
||||
|
||||
def _execute_run(
|
||||
self, state: CombatantState, target: CombatantState | None, action: Action
|
||||
) -> ActionResult:
|
||||
|
||||
@@ -293,6 +293,32 @@ def test_failed_aid_grants_nothing() -> None:
|
||||
assert hero.hp == 97
|
||||
|
||||
|
||||
# ── (c2) the aid bonus expires at the AIDER's next turn, not the beneficiary's ─
|
||||
|
||||
def test_aid_bonus_survives_beneficiary_turn_and_expires_at_aider_turn() -> None:
|
||||
# SRD "Aid Another": the +2 lasts "as long as that attack comes before the
|
||||
# beginning of your next turn" — your = the AIDER's. The ally aids the
|
||||
# hero (ac) in round 1; the hero's own round-2 turn start must NOT clear
|
||||
# the bonus, the ally's round-2 turn start must.
|
||||
seen: dict[str, int] = {}
|
||||
|
||||
def policy(engine: CombatEngine, state: CombatantState) -> tuple[Action, ...]:
|
||||
cid = state.combatant.id
|
||||
if cid == "hero" and engine._current_round == 2:
|
||||
seen["hero_turn"] = state.aid_ac_bonus
|
||||
if cid == "foe" and engine._current_round == 2:
|
||||
seen["foe_turn"] = engine._states[0].aid_ac_bonus
|
||||
if cid == "ally" and engine._current_round == 1:
|
||||
return (Action(kind="aid_another", target_id="hero", aid_mode="ac"),)
|
||||
return _WAIT
|
||||
|
||||
# Rolls: 3 initiatives (10,10,10); aid d20=10+5=15 >= 10 success.
|
||||
eng = _trio(policy, [10, 10, 10, 10], round_cap=2)
|
||||
eng.run()
|
||||
assert seen["hero_turn"] == 2 # survived the beneficiary's turn start
|
||||
assert seen["foe_turn"] == 0 # expired at the aider's turn start
|
||||
|
||||
|
||||
# ── (d) run moves up to 4x speed (destination and target), sets running ──────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user