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:
2026-09-17 23:09:44 +02:00
parent e253b03ffe
commit 980d53d80b
2 changed files with 57 additions and 4 deletions
+31 -4
View File
@@ -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: