ctan
f7886135bd
fix(rules): stabilize is a StabilizeEffect that auto-stabilizes a dying creature
2026-09-20 10:48:08 +02:00
ctan
a8a65b18cd
fix(rules): virtue is 1 temporary hit point for 1 minute
2026-09-20 10:32:05 +02:00
ctan
1238c40e43
fix(rules): resistance is a +1 resistance bonus on fort/ref/will saves for 1 minute - SRD spells/resistance.md grants the subject a +1 resistance bonus on saving throws for 1 minute (Will negates, harmless); the resistance bonus type already exists in the stacking model (effects.py BonusType), so the buff is expressible as a BuffEffect and was only deferred because _find_buff_spell returns the FIRST BuffEffect in known-spell order and cantrips cost no slots - encoding it would have made default_policy cast Resistance every round (for Tammara it would preempt Divine Favor and never kill anything); resistance.json now carries a BuffEffect (+1 resistance on fort/ref/will, 10 rounds = 1 minute) and _find_buff_spell skips level-0 spells (cantrips have no spell slot in the engine's budget - spell_slots_left has no level-0 key - and the policy deliberately never auto-casts them, a cantrip buff would be cast every round degrading the policy), so Resistance cannot preempt a leveled buff (Divine Favor) in the known-spell order; leveled buffs keep the historical first-match behavior (if the first one's slots are exhausted the buff branch gives up - the golden pins that); the resistance bonus type does not stack with itself (resolve_modifiers keeps the higher value); +9 tests (test_resistance.py: buff shape, cast on a willing ally consumes no d20 and applies without a save roll, the bonus rides save rolls, no self-stacking, expiry at 10 rounds, _find_buff_spell skips the slot-less cantrip / returns None without a leveled buff, _choose_spell_action proposes Divine Favor and never Resistance); golden untouched; 1714 tests green
2026-09-20 10:10:32 +02:00
ctan
08f9af9e3e
fix(rules): guidance is a one-shot +1 competence bonus on a single attack roll - SRD spells/guidance.md grants '+1 competence bonus on a SINGLE attack roll, saving throw, or skill check' (duration 1 minute or until discharged); the engine had no spell effect that could write the one-shot attack track (aid_attack_bonus is hardwired to aid-another +2 and expires at the start of the beneficiary's next turn), and a persistent BuffEffect would misrepresent the discharge rule (it would boost every roll for a minute) and would become the first buff spell in the casters' known-spell order, degrading default_policy; a new OneShotEffect type is added (track/value/bonus_type/duration_rounds, parser 'one_shot', SpellEffect union, summary 'one-shot +X type on track'), stored as ActiveOneShot on CombatantState (one_shot_bonuses, decremented with the other duration-tracked effects, exposed in the snapshot), discharged by the FIRST roll of the encoded track via _discharge_one_shot (best value wins, each casting is its own charge) and folded into the resolve_modifiers pool so bonus-type stacking applies; the attack track rides the attack roll only, NOT the crit-confirm roll (the SRD grants it to a SINGLE attack roll, so unlike aid another the confirmation is untouched - _attack_roll_modifiers returns (pool_mod, attack_roll_mod) and resolve_attack uses the latter for the total while the confirm keeps the former); the save and skill tracks are wired in the mechanism (resolve_save folds a save-track shot into save_mods, resolve_skill discharges a skill-track shot into _skill_static_bonus via extra_mods) but guidance.json encodes the attack track only - the SRD 'attack OR save OR skill' choice has no declaration channel in the engine, so the attack-roll variant is encoded (documented); _apply_targeted_spell_effect dispatches OneShotEffect to _apply_one_shot_effect (a successful save negates it, 'one-shot negated'); guidance.json now carries the effect (1 minute = 10 rounds) and default_policy never casts it (cantrips have no spell slot and the one-shot type is not a buff/heal/offensive pick); _spell_effect_summary split into _damage_effect_summary/_non_damage_effect_summary to stay under the PLR0911 return cap; test_spell_registry_batch2._EFFECT_TYPES gains OneShotEffect; +14 tests (test_one_shot_guidance.py: cast-and-discharge on the next attack, not on the crit-confirm roll, save/skill tracks, stacking by bonus type, expiry at 10 rounds, save negation, snapshot exposure, no auto-cast by default_policy); golden untouched; 1705 tests green
2026-09-20 09:53:53 +02:00
ctan
d347dde3fc
fix(rules): a multi-cell creature moving under mixed terrain pays the difficult rate for its whole footprint - SRD Movement, Position and Distance: 'If you occupy squares with different kinds of terrain, you can move only as fast as the most difficult terrain you occupy will allow'; the charge/run/5-foot-step already gated on the whole footprint (_footprint_touches_difficult) but the move/withdraw DESCENT priced every step by the anchor cell alone (grid.step_cost), so a 2x2 creature straddling rubble crossed at full speed; the descent now prices a step by the most difficult square under the footprint via _footprint_step_cost -> a new grid.step_cost_with_terrain (the 5-10-5 + elevation formula stays centralized in grid.py, step_cost delegates to it); 1x1 creatures are bit-identical (footprint == anchor cell); the direction Dijkstra field remains anchor-priced (making it creature-aware rewrites grid.reachable wholesale - the documented deferral); golden untouched (open-floor arena, no difficult terrain); +7 tests (trailing-square orthogonal/diagonal cost, 1x1 bit-identical regression, full-lane move budget 3-vs-6 squares, withdraw hampering)
2026-09-20 00:29:36 +02:00
ctan
b775d46850
fix(rules): document the burst center-to-center convention as a deferred SRD approximation - the SRD (aiming-a-spell.md) measures a burst from a grid INTERSECTION with the far-edge rule ('count from intersection to intersection'; a square is inside only 'if the far edge of a square is within the spell's area'), but the engine's burst origin is a CELL (target_pos, centered_on_caster, resolved target) counted center-to-center 5-10-5 in _creatures_in_range; matching RAW exactly needs an intersection coordinate added to AreaEffect/Action.target_pos/the observation area snapshot contract plus a per-caster corner choice (the far-edge boundary result depends entirely on which of the cell's four corners is the origin), so it is DEFERRED with a precise explanation in areas.py and a pointer from _creatures_in_range; no behavior change, golden untouched
2026-09-20 00:16:18 +02:00
ctan
e6021a6dda
fix(rules): expose healing_done in per-combatant stats and the CLI report - the channel/spell healing deliberately removed from damage_dealt ( 2e2873f) was invisible; CombatantStats/_LiveStats gain a healing_done counter booked with the hp actually restored (capped at the target's hp_max, overheal uncounted) by _channel_heal and by spell HealEffect on living targets, while cure-as-damage on undead stays in damage_dealt and the damage counters keep their exact semantics; the runner aggregates and averages the new field and the French report prints it as 'soins prodigués'; +4 tests (channel heal, cure spell, undead cure guard, run_encounter aggregate) +1 CLI assertion
2026-09-19 23:23:00 +02:00
ctan
5e7436b210
fix(rules): duplicate summons get disambiguated ids - the engine now counts id occurrences (seeded from the deployed roster) and gives each new summon of the same creature a build_states-style suffix (celestial_wolf, celestial_wolf-2, ...), so two summons no longer share one _LiveStats slot (the second summon used to reset the first one's counters and their attrition merged); run_encounter aggregation verified to report each slot separately; runner 'known limitation' comment updated; +4 tests (id split, distinct slots, per-slot attrition, run_encounter aggregate)
2026-09-19 23:13:06 +02:00
ctan
4f8a131ba0
fix(rules): lich stat block aligned to the KB entry - bestiary/lich.md gives hp 111 (11d6+55 plus 15 false life), Str 10, Int 22, Init +2 and 'Aura fear (60-ft. radius, DC 18)'; the JSON carried scenario-tuned 100 hp / Str 12 / Int 20 / Init +6 and the engine's feature-keyed aura constants carried 30 ft / DC 22 (BAB/touch/AC/saves/CON already conformed); the sim's own spell list is kept (the KB's prepared list includes spells outside data/spells/) and the choice is documented in the JSON notes and the README; golden tammara-vs-lich re-pinned for the two SRD-correct line changes (initiative d20=10+2=12 from the KB +2, Magic Missile DC17 = 10+1+6 from Int 22; damage unchanged - the 75 ft deployment distance stays beyond even the widened 60 ft aura, so no save die is consumed); +2 aura range tests (75 ft outside, 60 ft inside)
2026-09-19 23:05:25 +02:00
ctan
5fe2b72339
fix(rules): selective channeling modeled as a policy-chosen exclusion - Action(kind=channel_energy) gains channel_exclude, honored only with the Selective Channeling feat and capped at the Charisma modifier (SRD feats/selective-channeling.md: 'choose a number of targets in the area up to your Charisma modifier... not affected by your channeled energy'); without the feat the list is ignored and logged (SRD Normal: all targets affected); observable in current data because heal mode also heals living ENEMIES in the burst (deny them the heal) and harm mode can spare an undead ally; default_policy never excludes (opt-in, like weapon_id/destination/metamagic); golden transcripts unchanged; +5 tests
2026-09-19 22:49:40 +02:00
ctan
9f0d86273a
fix(rules): self-centered bursts for bless and bane - AreaEffect gains a data-driven 'centered_on_caster' flag (bless.json/bane.json set it per SRD bless.md '50-ft. burst, centered on the caster' and bane.md 'centered on you'); the burst origin is the caster's square regardless of the resolved target or a U7b target_pos, and with no chosen origin there is no primary save - every creature in the area rolls its own (mirror of the point-cast path); snapshot area key exposes centered_on_caster; golden transcript unchanged; +6 tests
2026-09-19 22:38:39 +02:00
ctan
4665a1c245
fix(rules): harmless spells on willing targets forgo the save roll - SpellSpec gains a data-driven 'harmless' flag set on the 29 SRD '(harmless)' spells (verified one by one against the KB); a same-side/self target consumes NO save die (SRD saving-throw.md: '(harmless) ... can attempt a saving throw if it desires' + 'Voluntarily Giving up a Saving Throw'), while hostile targets and undead receiving a cure (harmful to them, Will half for undead damage) keep the roll; snapshot spell summaries expose 'harmless'; golden transcript unchanged (no harmless-save cast in it); +8 tests, 3 fixtures re-pinned for the removed die
2026-09-19 22:27:46 +02:00
ctan
ea835293a0
fix(rules): run is bounded by Constitution endurance and the Run feat - after a number of consecutive run rounds equal to the CON score each further round requires a Constitution check (DC 10 +1 per prior check, no auto nat-1/20, undead/constructs exempt with no CON score); failure stops the run and forces the SRD 1-minute (10-round) rest; the Run feat grants +4 on the check and keeps the Dex bonus to AC while running (SRD Full-Round Actions - Run); heavy-armor x3 speed clause documented out-of-scope (no armor field); +5 tests
2026-09-19 22:02:36 +02:00
ctan
238e5f201e
fix(combat): witch hex trigger uses the SRD close range scaled by caster level (25 ft + 5 ft per 2 levels, extracted as spells.close_range_ft and shared with spell_range_ft) instead of the fixed _HEX_RANGE_FT 25 - a level-5 witch reaches 35 ft; + scaling regression test
2026-09-19 19:07:41 +02:00
ctan
6f01a8ebdd
fix(combat): _find_ally_needing_heal counts only living allies (SRD Channel Energy: heal mode heals living creatures, positive energy never heals undead) - an undead ally below half triggered heal mode, spending the channel to heal nobody; + trigger regression test
2026-09-19 19:01:03 +02:00
ctan
c23fc366e4
fix(combat): _find_ally_needing_heal uses hp*2 < hp_max (strictly below half) - the floored hp < hp_max//2 was off by one for odd hp_max (5 -> threshold 2, so 2/5 hp counted healthy); + threshold regression test
2026-09-19 18:58:03 +02:00
ctan
f93c55b3fb
fix(combat): _swift_bonus_actions emits up_close_and_deadly only when the turn will actually fire a firearm attack within 30 ft - the deed burns 1 grit at execution and its +1d6 rides only a firearm attack, so emitting it alongside a melee swing or a spell cast wasted grit for a lost bonus; + 2 policy tests, deed test now uses a firearm-only tammara
2026-09-19 18:55:17 +02:00
ctan
bdf15aa8fe
fix(combat): _choose_spell_action heal branch gates on spell range + line of effect - a touch-range Cure on a distant ally was rejected by _cast_spell (OUT OF RANGE / NO LINE OF EFFECT) and wasted the caster's whole turn; extracted the offensive branch's reach check as _spell_reaches and reused it (the duplicated below-half check in the branch was a no-op copy of _find_ally_needing_heal, dropped) + 3 policy tests
2026-09-19 18:48:58 +02:00
ctan
31d47aee79
fix(combat): _spell_combat_actions gates on _has_slot - with exhausted slots the cast is rejected (NO SPELL SLOT) and spell_combat resolves as a bare full attack at -2, strictly worse than the plain full attack; the policy now falls through (mirrors _choose_spell_action's slot gate) + 2 regression tests
2026-09-19 18:41:46 +02:00
ctan
2ee19d34e4
fix(docs): correct the Observation 'frozen' claim - the dataclass is frozen at the top level but the inner per-combatant/pair dicts are mutable copies; wording softened in the docstring and README (deep-freeze rejected: it would change the LLM contract and require touching pinned observation tests; copies are never read back by the engine, so policy mutation is already inert)
2026-09-19 17:09:08 +02:00
ctan
9459adee02
fix(actions): add downgraded:immediate_swift to ACTION_RESULT_REASONS (emitted since conformance L10 but absent from the published vocabulary); grep showed no other emitted-but-missing ActionResult reason (no_charge_path is a ChargeResult field) - drift guard test pins every _REASON_* constant against the frozenset
2026-09-19 17:06:30 +02:00
ctan
0ad627c97c
fix(stats): non-attack damage now lands in damage_taken - bleed ticks, recurring spell damage (Acid Arrow) and cure-spells-damage-undead (SRD Cure Light Wounds negative-energy hit, attributed to the caster's damage_dealt) were invisible to attrition while attacks were counted; transcripts and RNG untouched
2026-09-19 17:02:21 +02:00
ctan
2e2873f331
fix(stats): channel-energy heal is no longer counted as damage_dealt (attrition tracks damage only, README 'dégâts infligés/subis'; consistent with spell HealEffect which is counted nowhere) - harm mode still records damage_dealt/damage_taken, pinned by new tests
2026-09-19 16:52:04 +02:00
ctan
1e2de7f89d
fix(rules): a cone/line template excludes the caster's whole footprint, not just the anchor cell (SRD aiming-a-spell.md: 'It starts from any corner of your square' - every occupied square is origin space and never inside its own area) - cone_cells/line_cells take caster_cells; combat passes caster.occupied_cells(); 1x1 casters bit-identical; documented simplification: a grazed caster cell still consumes a length slot
2026-09-19 16:34:22 +02:00
ctan
2e468ba619
fix(rules): point-cast cones/lines no longer demand line of effect to the aim point (SRD aiming-a-spell.md: a cone/line 'shoots away from you in the direction you designate' and 'starts from any corner of your square' - the LoE-to-the-space rule binds targets and burst origins only) - _point_target_rejection now requires in-bounds for every area and caster->point LoE only for bursts (and area-less spells, conservatively); template LoE stays enforced by cone_cells/line_cells
2026-09-19 16:29:38 +02:00
ctan
778d6a8620
fix(rules): a blinded creature cannot charge, run, or withdraw (SRD charge.md: 'If you don't have line of sight to the opponent at the start of your turn, you can't charge that opponent'; full-round-actions.md: 'You can't run ... if you can't see where you're going' and 'you can't withdraw from combat if you're blinded') - can_charge gates blinded with no_line_of_effect, _execute_run joins the cant_charge no-op, _withdraw rejects blinded
2026-09-19 16:24:31 +02:00
ctan
792cec140b
fix(rules): dying-but-alive creatures still occupy their squares (SRD cover.md: soft cover from 'a square occupied by a creature'; SRD aiming-a-spell.md: a burst 'affects whatever it catches in its area, including creatures that you can't see') - soft-cover interposer sets and area templates filtered on active, so a dying creature's square was ignored; new CombatantState.occupies_space (not dead) gates both; corpses stay excluded (documented); channel energy keeps its active-only filter (pinned)
2026-09-19 16:19:23 +02:00
ctan
9086038bfb
fix(rules): charge clear-path tests the whole footprint for difficult terrain (SRD movement-position-and-distance-2.md: 'You can't run or charge across difficult terrain' + 'you can move only as fast as the most difficult terrain you occupy') - _charge_path_clear priced only the anchor cell, so a 2x2 creature with one trailing square in rubble charged freely; the move-descent footprint pricing is deferred (would rewrite the anchor-based Dijkstra cost field)
2026-09-19 15:51:15 +02:00
ctan
5e478bfd27
fix(rules): charge and run walks enforce the diagonal corner rule (SRD measuring-distance.md: 'You can't move diagonally past a corner') - _charge_path_clear and _run_line_path never checked diagonal_allowed, so a charge/run could cut diagonally through a wall corner; both walks now stop/reject a diagonal step whose flanks are not both passable, same predicate the Dijkstra descent uses
2026-09-19 15:44:12 +02:00
ctan
8774e51a12
fix(rules): cover grants a Reflex-save bonus against area effects (SRD cover.md 'Cover and Reflex Saves': +2 vs standard cover - half the AC value, so +1 partial/+2 standard/+4 improved; soft cover grants none) - _area_save adds terrain cover//2 from the effect's point of origin (burst center, caster square for cone/line) to Reflex rolls; resolve_save gains an explicit flat_bonus
2026-09-19 14:16:26 +02:00
ctan
948edc1815
fix(rules): bursts cannot affect creatures in total cover from the point of origin (SRD aiming-a-spell.md: a burst "can't affect creatures with total cover from its point of origin (in other words, its effects don't extend around corners)"; Line of Effect: a burst affects only what it has LoE to from its origin) - _area_creatures skips creatures whose occupied cells have no line of effect from the burst center
2026-09-19 14:08:24 +02:00
ctan
56310b191a
fix(rules): mirror image rolled image count with dynamic images/(images+1) concealment miss chance, bleed cantrip gated on the SRD dying-target eligibility, bane -1 fear-scoped penalty via a dedicated bane condition (SRD spells/mirror-image.md, spells/bleed.md, spells/bane.md)
2026-09-19 13:22:19 +02:00
ctan
22724f583f
fix(rules): Extend Spell doubles SummonEffect duration (SRD extend-spell: lasts twice as long; summon's 1 round/level is neither instantaneous, concentration nor permanent)
2026-09-19 11:55:30 +02:00
ctan
65a5e24609
fix(rules): Combat Casting's +4 no longer applies to the entangled concentration check (SRD combat-casting: the feat covers only casting defensively or while grappled)
2026-09-19 11:50:53 +02:00
ctan
cec3c5f459
fix(rules): summon duration scales with caster level (SRD summon-monster: 1 round/level (D)) - SummonEffect gains duration_rounds_per_level resolved at cast time, summon_monster_i/ii JSON updated
2026-09-19 11:47:34 +02:00
ctan
418fdc826b
fix(rules): a missed melee touch attack holds the charge instead of expending the spell (SRD Holding the Charge: re-attempted at the start of later turns, dissipated by casting another spell; ranged touch still expended on a miss)
2026-09-19 11:40:02 +02:00
ctan
81a09c88f0
fix(rules): metamagic no longer raises the save DC - only Heighten Spell does (SRD empower-spell: saving throws not affected; heighten-spell: DCs calculated at the heightened level, modeled at its minimum +1 slot adjustment)
2026-09-19 11:33:10 +02:00
ctan
b74baf1aa9
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)
2026-09-19 10:50:50 +02:00
ctan
0c3f5fd907
fix(rules): condition durations scale with caster level (hold person, doom, touch of fatigue, vanish)
...
ConditionEffect gains duration_rounds_per_level and max_duration_rounds
(mirroring PenaltyEffect), applied as caster_level * per_level rounds capped
at max: hold person 1 rd/lvl, doom 1 min/lvl, touch of fatigue 1 rd/lvl,
vanish 1 rd/lvl (max 5). Extend Spell doubles both the per-level rate and
the cap. Tests cover scaling and the vanish cap.
2026-09-17 23:26:45 +02:00
ctan
a144423522
fix(rules): total defense and fighting defensively cannot be combined (SRD Total Defense) - no stacking, second declaration rejected
2026-09-17 23:13:22 +02:00
ctan
980d53d80b
fix(rules): aid another bonuses expire at the beginning of the aider's next turn, not the beneficiary's (SRD Aid Another)
2026-09-17 23:09:44 +02:00
ctan
e253b03ffe
fix(rules): restricted charge - a creature limited to a standard action (staggered/disabled, surprise round) can still charge at 1x speed
2026-09-17 23:06:03 +02:00
ctan
a5bd3c5a85
fix(rules): total defense denies attacks of opportunity (SRD Total Defense: allowance drops to 0 while the bonus is active)
2026-09-17 23:00:10 +02:00
ctan
47f563e11a
fix(rules): shield of faith grants its SRD deflection AC buff (+2, +1 per six caster levels, max +5) via a new BuffEffect flat scaling
2026-09-17 22:36:18 +02:00
ctan
83bbcae952
fix(rules): ray of enfeeblement applies a rolled Strength penalty (Fort half) instead of fatigued
...
SRD ray-of-enfeeblement.md: 1d6 + 1 per 2 caster levels (max 1d6+5) Strength
penalty, Fortitude half, 1 round/level. Adds a PenaltyEffect type (rolled
formula, scaling, half on save, level-scaled duration) applied as negative
untyped modifiers on the rolls Strength feeds (attack, damage), since the
engine has no ability-score track. Formerly modeled as a fatigued condition
with Fort negates.
2026-09-17 21:28:00 +02:00
ctan
ae037c984f
fix(rules): minimum damage - a landed hit reduced below 1 by DR/energy resistance deals 1 (CRB Damage - Minimum Damage, nonlethal approximated as normal)
2026-09-16 21:46:04 +02:00
ctan
cc9fba3b52
fix(rules): helpless Dex-0 AC adds exactly -5 on flat-footed base; touch base strips full Dex mod when helpless (no negative-Dex double count)
2026-09-16 21:32:19 +02:00
ctan
e0bf484394
fix(rules): firing into melee exemption measured target-to-nearest-ally, not attacker-to-target (SRD Shooting into a Melee)
2026-09-16 21:24:48 +02:00
ctan
e89ea42168
fix(rules): README/abilities wording - grapple applies only grappled (pin out of scope), pinned/nauseated cant_attack, confused no-AoO, Weapon Focus group matching
2026-09-16 20:32:15 +02:00
ctan
3a5948c35c
fix(rules): CMD never adds Dex on top of a Tiny Dex-substituted pre-baked CMB
2026-09-16 20:25:34 +02:00