Commit Graph

170 Commits

Author SHA1 Message Date
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 aa424fa385 docs(rules): conformance journal rounds 5-6 (31 fix commits) and final residual inventory
Rounds 5-6 covered the spell subsystem (metamagic DC, holding the charge,
summon duration, Extend, Combat Casting/entangled, ASF/bonus-spells/dismissal
docs), the 24 feat/racial residuals, the deferred spell effects (mirror image,
bleed, bane, acid arrow, ice slick, owl's wisdom, bear's endurance, aid...)
and the map/LoS area-of-effect findings (line truncation at barriers, cone/
burst line of effect, cover-to-Reflex, adjacent melee cover, charge/run
corner rule, charge footprint terrain). Records the 31 commits
(d0db208..9086038), the deferred/lower-severity residuals, and the audit
coverage gap (oracle quota exhausted).
2026-09-19 15:58:53 +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 27ce33ba67 fix(rules): adjacent melee cover uses the SRD any-line rule (cover.md: 'any line from any corner of your square to the target's square goes through a wall') - the gate previously applied the ranged best-corner rule to distance-1 melee, systematically under-granting cover; non-adjacent melee (reach) keeps the ranged rule, bonus scoring aligned per branch, README 'pire coin' wording updated 2026-09-19 15:35:56 +02:00
ctan 9f8655da76 fix(rules): README sync for the area-of-effect line-of-effect fixes - line truncation at barriers, cone/burst LoE from the origin, terrain cover Reflex half-bonus (SRD aiming-a-spell.md, cover.md); areas.py architecture entry updated 2026-09-19 14:35:17 +02:00
ctan fb6f58acc1 fix(rules): document the diagonal corner rule as stricter-than-RAW (SRD measuring-distance.md: 'You can't move diagonally past a corner' vs 'You can also move diagonally past other impassable obstacles, such as pits' - RAW blocks only both-flank corners; the engine requires both flanks passable for every diagonal, an accepted conservative approximation because relaxing it would rewrite Dijkstra paths and golden positions) 2026-09-19 14:32:44 +02:00
ctan beb8421600 fix(rules): pin the high-ground +1 as melee-only with the SRD proof (Table 8-5 'Attack Roll Modifiers': 'On higher ground - Melee +1, Ranged +0') - engine verified conformant, no code change 2026-09-19 14:30:46 +02:00
ctan a2ca78fce3 fix(rules): document the blocks_los flag as LINE OF EFFECT semantics (SRD aiming-a-spell.md: 'A line of effect is canceled by a solid barrier. It's like line of sight for ranged weapons, except that it's not blocked by fog, darkness, and other factors that limit normal sight') - vision-only concealment terrain is not modeled; concealment rides conditions (miss_chance), not the map 2026-09-19 14:26:26 +02:00
ctan b95533d49f fix(rules): bear's endurance +2 hp per Hit Die via split flat scaling, aid morale scoped to fear-descriptor saves (SRD spells/aid.md), charm person range close per SRD spells/charm-person.md, README synced (29 conditions, acid arrow total cap, mirror image instance-level miss chance) 2026-09-19 13:34:30 +02:00
ctan 17c06e18cc fix(rules): document dismissal of (D)-duration spells as out of scope (SRD Cast a Spell: Dismiss a Spell - no dismiss action nor active-spell registry exists in the engine) 2026-09-19 12:06:24 +02:00
ctan 1902877cfe fix(rules): document the spell_slots data convention - JSON slots are final per-encounter budgets, engine computes no bonus spells (evidence: PC sheets carry the base class table without bonus, lich slots match no RAW daily table) 2026-09-19 12:04:30 +02:00
ctan b2739c5747 fix(rules): document arcane spell failure as out of scope (SRD Arcane Spells and Armor - the model has no armor field nor spell components, so ASF has neither source nor filter) 2026-09-19 12:01:40 +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 1a68dfa6db docs(rules): conformance journal rounds 3-4 (22 fix commits) and residual inventory
Round 3 (attack resolution, bestiary/PC stat blocks, action economy) and
round 4 (remaining spells, PC feats) found 2 High, ~11 Med and ~70 Low
findings. Records the 22 fix commits (e0bf484..0c3f5fd), both justified
golden updates (Weapon Focus, Divine Favor), and the residual inventory:
~13 deferred spell effects, 20 feat/racial mismatches, and the missing
subsystems (alignment, items, skills). Journal marked paused at round 4.
2026-09-17 23:28:32 +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 06da519fc6 fix(rules): dirty trick RAW list extended to shaken and sickened (APG), with runtime validation 2026-09-16 20:16:35 +02:00
ctan 2b5920c85c fix(rules): disarm drops the wielded weapon (weapon_dropped) instead of a synthetic -4 penalty 2026-09-16 20:08:10 +02:00
ctan 350844fc68 fix(rules): Up Close and Deadly deed requires a firearm attack (Ultimate Combat) 2026-09-16 19:55:53 +02:00
ctan 2807f719ca fix(rules): any concealment denies sneak attack (SRD rogue: 'a creature with concealment') 2026-09-16 19:50:40 +02:00
ctan 99e9203d8c docs(rules): add the SRD conformance audit journal
Consolidates the round-1 audit: 15 fixed divergences (one commit each,
312f9fd..ea60b2e), the deliberate out-of-scope rules, the assumed
residuals, and the verified-conformant guard rails. Linked from README.
2026-09-16 18:10:05 +02:00
ctan ea60b2e4b3 docs(rules): README - undead death, pass-through movement, squeezing limit
- Paragraphe « Mourir et stabilisation » : les morts-vivants et les
  constructeurs sont detruits des hp <= 0 (SRD traits Undead/Construct :
  'immediately destroyed when reduced to 0 hit points or fewer'), sans
  bande d'invalidite ni agonie - conformance deja codee (59caf67).
- Paragraphe « Mouvement » : la traverse des ennuis a 3 categories de
  taille d'ecart avec AoO d'entree (SRD 'Moving Through a Square' : 'Any
  creature can move through a square occupied by a creature three size
  categories larger than itself [...] provoke attacks of opportunity
  from those creatures' ; 'You can't end your movement in the same
  square as another creature unless it is helpless') ; la mention
  'les ennemis bloquent toujours le chemin' est corrigee (blocage total
  seulement a 0-2 categories).
- « Limites hors perimetre » : nouvelle entree squeeze (largeur de
  corridor vs taille non modelisee), entree des approximations du
  pass-through (Acrobatics Tumbling et la clause 'counts as 2 squares'
  des tres grosses creatures inertes non modelisees), et entree de
  l'echec d'incantation du sourd (tache 11, differe).
- « Attaques multiples par action » : verifie conforme aux armes
  naturelles apres e5691bd (pas d'iteratifs, secondaires a -5,
  Multiattack -2) - aucune modification necessaire.
- Compteur de tests de la section « Developpement » : 1408 -> 1491.
2026-09-16 15:53:46 +02:00
ctan cad35e2e13 fix(rules): cure spells damage undead instead of healing
SRD Cure Light Wounds: 'Since undead are powered by negative energy,
this spell deals damage to them instead of curing their wounds. An
undead creature can apply spell resistance, and can attempt a Will save
to take half damage.'

- _apply_heal_effect takes the rolled save; an undead target takes the
  rolled healing as damage (halved on a successful save; SR already
  resolved upstream, DR never applies to spell energy, no bleeding stop
  or stabilization). Living targets are bit-identical.
- cure_light/cure_moderate descriptions fixed: the SRD save line is
  'Will half (harmless)', not 'negates'.
- Deafened 20% verbal spell failure DEFERRED to the README « Limites
  hors perimetre »: SpellSpec carries no components data (would need a
  verbal flag on all 108 spells) and sound-based Perception is not
  modeled either.
2026-09-16 15:48:36 +02:00
ctan 3e19d2974e feat(rules): SRD Mage Armor spell + non-stacking armor bonus type
SRD spells/mage-armor.md: 'An invisible but tangible field of force
surrounds the subject of a mage armor spell, providing a +4 armor bonus
to AC' - 'School conjuration (creation) [force]', level 1, 'Casting Time
1 standard action', 'Range touch', 'Duration 1 hour/level (D)',
'Spell Resistance no'.

- data/spells/mage_armor.json: buff +4 armor to AC, 600 rounds (1
  hour/level modeled fixed, the shield.json convention). Description
  documents the known simplification: the engine bakes worn armor into
  the AC total and cannot see it, so the armor bonus stacks additively
  with that baked value (two mage armors still do not stack).
- effects.py: 'armor' added to BonusType; absent from _STACKING_TYPES it
  behaves as a keep-the-highest type (verified by resolve_modifiers
  tests).
- Spell-count pins 107 -> 108 (test_runner_spells, batch3, test_spells)
  and the README references.
- Golden transcript untouched (buff branch only fires when no offensive
  spell is selected; the lich still opens with Magic Missile).
2026-09-16 15:36:43 +02:00
ctan e5691bdb6f fix(rules): natural weapons get no BAB iteratives, secondary attacks at -5
PF1e (CRB "Natural Attacks"):
- Natural weapons (natural=true) get exactly one swing per full attack,
  regardless of BAB (no BAB iteratives for natural attacks).
- Secondary natural attacks (secondary=true) attack at BAB -5; the
  Multiattack feat reduces this to -2.
- Manifested in combat.py (_full_attack_weapon, _resolve_swing),
  models.py (AttackSpec.natural/secondary), 6 data JSONs, 4 new tests,
  and README documentation.

1461 passed, ruff clean, basedpyright 0 errors.
2026-09-16 12:25:30 +02:00
ctan 6c4bdc5d14 docs(rules): document the bladed dash approximation
Bladed Dash's description now matches the SRD (standard action, move up
to 30 ft in a straight line without provoking attacks of opportunity,
single melee attack at highest BAB against any creature adjacent along
the path) and the README's spell-limits section records the engine
approximation: a +30 ft speed buff for 1 round, with the free melee
attack, the no-AoO movement, the straight-line constraint and the
circumstance attack bonus left unmodeled.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 17:43:09 +02:00
ctan b84238792c feat(rules): scale healing spells with caster level
PF1e (SRD spells/cure-light-wounds.md): 'cures 1d8 points of damage +1
point per caster level (maximum +5)'; (spells/cure-moderate-wounds.md):
'cures 2d8 points of damage +1 point per caster level (maximum +10)'.
A HealEffect now carries the same DamageFlatScaling spec as damage
effects (S1b) and resolves it through effective_formula(caster_level)
at cast time, so the flat bonus grows with the caster and stops at the
stated cap. The engine passes the caster's level into _apply_heal_effect
(required keyword: no silent level-0 under-heal); Maximize Spell
maximizes the scaled formula. Cure Light/Moderate Wounds ship the SRD
values; inflict_light_wounds already carried its scaling_flat.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 17:19:58 +02:00
ctan a2a80b8690 feat(rules): add firearm misfire and the quick clear deed
Ultimate Combat Firearm Rules: a firearm attack rolling a natural 1 gains the
broken condition (weapon_broken, logged MISFIRE). Quick Clear now actually
clears the first misfired held firearm for 1 grit (reported no-op without
grit), and occupies the standard budget slot via _effective_slot; the
swift-consumed filter is slot-based so the standard deed survives.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 16:41:40 +02:00
ctan b579cb4934 fix(rules): give Tiny-or-smaller creatures zero reach and square sharing 2026-09-13 15:35:23 +02:00
ctan 7fb7ec55dd fix(rules): update initiative when a readied action fires
SRD Ready - Initiative Consequences of Readying: 'your initiative result becomes the count on which you took the readied action' and you 'act immediately ahead of the character whose action triggered your readied action'. _check_readied fired the payload off-turn but never touched the readier's initiative. It now calls _ready_shift_initiative, which rewrites the readier's _initiative_keys entry (trigger total + 0.5, keeping its mod and prior index) using the same tuple shape _execute_delay writes, so _build_round_order holds the new position for all following rounds. Deterministic: no RNG consumed, current round order untouched.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 14:54:15 +02:00
ctan 6702efc652 fix(rules): constrain a run to a straight line
SRD Full-Round Actions - Run: 'you can move up to four times your speed in a straight line'. _execute_run reused the Dijkstra move machinery, so it turned corners around walls. It now walks the straight Bresenham line (_run_line_path) and stops at the first obstacle (impassable terrain, difficult terrain, or a blocking creature), mirroring the charge clear-path logic. Keeps the 4x budget, step-by-step AoO provocation, moved_this_turn and the running/Dex-loss flag; a destination the straight line cannot honour is rejected (unreachable_destination).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 14:48:25 +02:00
ctan f5dc9da0c4 feat(rules): resolve one-round casts on the following turn
SRD Casting Time: a spell that takes 1 round is a full-round action
that comes into effect just before the caster's turn in the next round;
the caster then acts normally. _cast_spell now spends the slot and runs
the start-of-casting gates, then records a PendingCast (spell, target,
center, metamagic, slot level, hp at cast); delivery (touch attack, SR,
saves, effects) resolves at the start of the caster's next turn, before
the policy runs. Net hp lost in between forces the injured-while-casting
concentration check (DC 10 + damage + spell level, no Combat Casting) or
the spell fizzles; a caster knocked out of the fight loses it. Exposed
in the observation via pending_cast. Bounded model documented: net-hp
measurement instead of a per-damage-site hook; long (1 minute+) casting
stays out of scope.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 13:39:44 +02:00
ctan 3cc0502228 feat(rules): add the surprise round
SRD Surprise: when some but not all combatants are aware of their
opponents, a surprise round precedes round 1. Aware combatants take a
single standard OR move action plus free actions (swift and full-round
are dropped, full_attack collapses to one standard attack, withdraw
survives as the Restricted Withdraw standard); unaware combatants skip
their turn. New Combatant.aware flag (default true, parsed at the JSON
boundary) keeps every existing encounter bit-identical, exposed in the
observation snapshot. Unaware combatants are not flagged flat-footed
(documented simplification).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 13:20:37 +02:00
ctan 7a8351e3b2 docs(readme): update the test count after the spell feature batches 2026-09-13 12:50:54 +02:00
ctan 9768f5ae1d feat(spells): allow spells to ignore spell resistance
SpellSpec gains spell_resistance: bool = True (JSON 'spell_resistance'). When false, _spell_delivery_blocked skips the targeted SR check and _apply_area_effect skips the per-creature SR check (threaded through the effect chain). Verified each imported spell against the local KB and set false on the 28 whose SRD block says 'Spell Resistance: no' (Acid Arrow, Flurry of Snowballs, Web, Darkness, Sanctuary, the Protection from X spells, etc.). Added no-SR targeted/area tests and a high-SR control in test_spell_resistance.py, and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 12:44:35 +02:00
ctan c96b36d3f9 fix(spells): apply a shaken effect when a fear save succeeds
ConditionEffect gains save_partial (JSON 'on_success': condition_name + duration_rounds). On a successful save that would otherwise negate, _apply_condition_effect now imposes the weaker condition (SRD fear.md: panicked on a failed Will save, shaken for 1 round on a success). Extended _metamagic_transform_sub to preserve/double it; extracted _impose_condition helper. Updated fear.json, added failed/successful-save and loader tests in test_spells.py, and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 12:32:45 +02:00
ctan a59ebee37a feat(rules): add ongoing spell damage
Acid Arrow (SRD spells/acid-arrow.md) deals 2d4 acid, then 2d4 again at the start of each of the target's turns for 1 round + 1 per three caster levels (max 6). DamageEffect gains a RecurringDamageSpec (JSON 'recurring'); a landed hit attaches a RecurringDamage source to CombatantState.recurring_damage, ticked in _turn_gate (after bleed), reduced by energy resistance/immunity but never DR (spell damage), cleared at expiry, and exposed in the observation snapshot. Extracted _resolve_one_projectile to keep _apply_damage_effect under the complexity ceiling. Updated acid_arrow.json, added tests/test_recurring_damage.py, and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 12:25:38 +02:00
ctan 88ef9f39be feat(rules): support multi-projectile spells
SRD Magic Missile fires one missile +1 per two caster levels beyond 1st (max 5, 1d4+1 force, auto-hit); Scorching Ray fires one ray +1 per four levels beyond 3rd (max 3, 4d6 fire, ranged touch each). DamageEffect gains a ProjectileScaling spec (JSON 'projectiles'); _apply_damage_effect loops count_for(caster_level) projectiles, each rolling its own damage and (for a declared-projectile ray) its own ranged touch attack, with the target-drops-mid-sequence convention losing the rest. Updated magic_missile.json/scorching_ray.json, the golden lich transcript (CL 11 now fires 5 missiles, Tammara dies round 1: 43 damage), the scripted MM queues in test_spells/test_conformance_spellflow, added tests/test_multi_projectile.py, and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 12:05:56 +02:00
ctan 642f449b9e fix(rules): exclude the caster from a line spell's area
CRB Magic - Aiming a Spell: a line 'starts from any corner of your square', so the caster's own square is outside the template (SRD Lightning Bolt: 'The bolt begins at your fingertips' - it leaves the square, it does not fill it). line_cells now drops the origin and takes the length_cells squares beyond it; a degenerate aim affects nothing. Updated the Lightning Bolt and Line Sparks tests (caster no longer caught) and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 11:30:30 +02:00
ctan ee8c8f80ff fix(spells): set the correct area shapes
SRD-verified area templates: Burning Hands 15-ft cone, Color Spray 15-ft cone, Fear 30-ft cone, Flurry of Snowballs 30-ft cone, Lightning Bolt 120-ft line (all converted from single-target/burst approximations to proper AreaEffect shapes); Fireball 20-ft burst, Sound Burst 10-ft spread, Sleep 10-ft burst, Web 20-ft spread kept. Close-range spells note the 15/30-ft RAW ranges as an approximation in their descriptions. README: cone/scaling limitations moved out of the limitations list, new area-shape + caster-level scaling rules documented, areas.py added to the architecture.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 11:01:07 +02:00
ctan 734cfa4e55 docs(rules): document quick clear and doom description
README: the quick_clear deed is classified swift in the engine (SRD: standard action, or move with All Firearms) and is a functional no-op without a misfire model - documented rather than half-implemented. doom.json: the description mentioned a '-1 on a successful save' (a D&D 3.5 rule); PF1e Doom is Will negates with no partial success - description text corrected, effects unchanged (save_negates was already correct).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 15:11:56 +02:00
ctan 5bf2850e2f fix(rules): use the attacker's best corner for cover bonus
SRD 'Cover': 'To determine whether your target has cover from your ranged attack, choose a corner of your square.' The ranged scorer in cover_bonus_cells took the MAX over all attacker corners (worst corner); the cover gate has_cover_cells and RAW use the attacker's best corner. Ranged cells are now scored min over attacker corners of (max over target-corner lines), then min over attacker cells - the bonus can no longer exceed what the gate admits. Melee keeps the worst-corner rule ('any line from ANY corner of your square'). Updated the two pins that locked the old worst-corner scores.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 14:57:56 +02:00
ctan a989266747 fix(rules): require adjacency or pull the target when grappling
SRD 'Grapple': 'If you successfully grapple a creature that is not adjacent to you, move that creature to an adjacent open space (if no space is available, your grapple fails).' _maneuver_grapple now pulls a non-adjacent target to the nearest free anchor whose footprint is adjacent to the grappler (5-10-5 distance, (row,col) tie-break, new _nearest_anchor_adjacent_to helper) and fails outright when no adjacent open space exists. Three tests: pull, no-space failure, adjacent no-move.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 14:45:55 +02:00
ctan 25149c64ba fix(rules): allow a free weapon draw with a move at BAB +1
SRD 'Move Actions': 'If you have a base attack bonus of +1 or higher, you may draw a weapon as a free action combined with a regular move.' _filter_turn_actions computes free_draw (BAB >= 1 AND a move action in the turn sequence) and threads it through _effective_slot/_action_economy_filter/_staggered_filter: retrieve_weapon then occupies the free slot instead of the move slot. Static classification stays 'move' (repair_weapon and no-move draws unchanged). Filter-level and engine-level (BAB +2 vs BAB 0) tests added.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 14:35:42 +02:00
ctan 317c68ecdb fix(rules): allow a staggered creature to withdraw as a standard action
SRD 'Full-Round Actions - Restricted Withdraw': 'If you are limited to taking only a standard action each round you can withdraw as a standard action. In this case, you may move up to your speed.' _staggered_filter now keeps withdraw as the one move-or-standard action (spending the slot) instead of dropping it, and _flee_path halves the budget to single speed for a staggered/disabled creature (new _is_staggered_limited helper). Filter-level and engine-level tests added.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 14:21:04 +02:00
ctan a4487a85f9 fix(rules): block immediate actions while flat-footed
SRD 'Immediate Actions': 'You also cannot use an immediate action if you are flat-footed.' _check_reactions now skips states carrying the flat_footed condition before consulting the reaction policy. The engine never applies flat_footed itself (latent gate), so the unit test injects the condition via active_conditions, with a control test proving the same setup fires when not flat-footed.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 14:08:44 +02:00
ctan 7810d88488 fix(rules): expire a readied action at the combatant's next turn
SRD 'Ready - Initiative Consequences of Readying': 'If you come to your next action and have not yet performed your readied action, you don't get to take the readied action (though you can ready the same action again).' _take_turn's start-of-turn block now clears state.readied/readied_trigger (logged 'ready expired'), before the policy call; the ready still survives the round boundary and can fire in the next round as long as the readier's own turn has not arrived. Updated the two tests that pinned the old infinite persistence and added cross-round fire + expiry tests.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 13:59:11 +02:00
ctan ec2088ab90 fix(rules): apply cover concealment and criticals to touch attacks
CRB 'Cast a Spell - Touch Attacks': 'You can score critical hits with either type of attack as long as the spell deals damage'; CRB 'Range - Touch': a touch spell threatens on a natural 20 and deals double damage on a successful confirmation. The melee and ranged spell-touch gates now add the target's cover bonus to touch AC (new _attack_cover_bonus helper shared with resolve_attack), roll the concealment miss chance after the hit (blinded attacker = total concealment convention), and resolve crit threat/confirm with damage doubling for damage-dealing touch spells. Non-damage touch spells do not threaten (no die consumed). Golden transcript unaffected.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 13:47:03 +02:00
ctan 35c8d101b5 fix(rules): treat a blinded attacker as facing total concealment
CRB 'Conditions - Blinded': 'All opponents are considered to have total concealment (50% miss chance) against the blinded character.' The sneak-attack gate and the AoO suppression gate now apply the same max(target_concealment, 50) convention as _resolve_hit_damage: a blinded attacker cannot sneak attack and its AoOs are suppressed without spending the budget.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 13:26:52 +02:00