Commit Graph

393 Commits

Author SHA1 Message Date
ctan 3fd9860b5c docs(phase2): record the arbitrated decisions (external prompts, cartography-first)
All recommendations accepted by the user except prompt location: prompts live
outside this repo (separate pack, configurable path, versioned; prompt_version
feeds the cache key and the replay record; a pinned pack for integration, a
minimal local fixture for unit tests, no silent fallback). Also records the
repo-scope decision (code only: calculation + LLM + report) with the final
detachment of data/sheets/prompts as a backlog item and the design constraint
to avoid hardcoded data paths in the new code. Priority: cartography first.
2026-09-21 10:49:00 +02:00
ctan d794828c7e docs(phase2): add the optimizer and adversarial-cartography layers
The execution tiers answer 'how to run a tactic', not 'which tactic is best'
nor 'where is the danger'. Adds two decision layers: an optimizer over
StrategySpec (objectives incl. tail risk, coarse-to-fine search, LLM as
candidate seeder) and an adversarial cartography (AdversarySpec + alternating
min-max + scenario sweeps + danger frontier + sensitivity), giving the danger
view for BOTH sides. Reframes the LLM as hypothesis generator / red team /
explicator / reporter, never the optimizer or per-turn executor in sweeps.
Adds D12-D14 and phases P3 (optimizer) / P4 (robustness).
2026-09-21 10:27:11 +02:00
ctan 630f5321e0 docs(phase2): revise the LLM architecture after critique
Key correction: the LLM must not sit on the per-turn hot path of a Monte
Carlo sweep (runs x rounds x combatants calls, no cross-run cache reuse).
Adopts a two-tier design - a compiled, parameterised deterministic Policy for
quantitative sweeps (tier A) and a per-turn NLPolicy for a handful of showcase
battles (tier B). Also fixes: 'engine unchanged' -> rules unchanged/additive
boundary; replay keyed by a monotonic call_seq (delay re-queues, summons);
composite response-cache key incl. model id; ObservationView as a structured
object; candidates derived from pairs instead of a second legality engine;
per-run PolicyFactory; cost guardrails in P1; concurrency; D1 to settle in P0.
2026-09-21 09:55:53 +02:00
ctan be64451ba4 docs(phase2): architecture and implementation plan for the LLM decision layer
Design-only document (no code): target architecture (compact versioned
observation view, legal-actions API, LLM client + cache, NL policy with
parse/validate/repair/fallback, strategy sweep, replay/eval harness), the
concrete gaps in the current runner/CLI wiring, the decisions to settle and a
phased implementation plan (P0 foundations, P1 client+policy, P2 strategy
sweep, P3 provider+report) with exit criteria and risks.
2026-09-21 09:51:05 +02:00
ctan 7e4a9f5edf docs(rules): conformance journal round 15 - nonlethal, splash, skills and flight (out of scope -> modeled)
Brings in the last four out-of-scope mechanics: the nonlethal damage track
(unarmed strikes, minimum damage), splash weapons (direct + scatter),
combat skills (skill checks, Acrobatics Tumbling, Stealth/opposed Perception)
and 3D flight over ground terrain (vertical line of effect deferred). Updates
the out-of-scope and residual tables to drop the now-modeled items and marks
the round-2 disposition tables as historical. Gate 1981.
2026-09-20 22:35:55 +02:00
ctan 9210a54589 docs(rules): document 3D flight over ground terrain (airborne view: difficult terrain cost 1, cross non-LoS impassable cells, blocks_los still stops flight; elevation climb still applies; LoE/cover/high-ground unchanged) and the deferred 3D line-of-effect/maneuverability with SRD citations (Movement, Position and Distance; Aiming a Spell); map legend + grid.py notes, test count 1981 2026-09-20 22:29:05 +02:00
ctan 2ae86ab29f feat(rules): 3D flight over ground terrain - airborne creatures (fly >= land, V4 budget from fly speed) move on Grid.airborne_view: ground difficult terrain costs 1 and cells impassable to walkers (pits, water, rubble, low obstacles) are crossable at cost 1, EXCEPT blocks_los cells (solid barriers, assumed full height) which still stop flight; run/charge/5-foot-step gates and diagonal corner rule follow the same view (flying past a pit is legal, past a wall corner is not); elevation climb cost still applies (step heights, not wall heights - documented); line of effect, cover and high ground use the unchanged real grid so combat geometry is unaffected by flight (SRD Movement, Position and Distance: flying creatures not hampered by difficult terrain, able to avoid most obstacles), 11 tests 2026-09-20 22:21:40 +02:00
ctan 1257bb3849 docs(rules): document skill checks, Acrobatics Tumbling and Stealth in README (rule list, observation schema hidden_from, out-of-scope updates, test count 1970) 2026-09-20 21:48:02 +02:00
ctan 2c40994610 feat(rules): Stealth + opposed Perception - opt-in hide move action (Stealth d20 + trained class + ACP + size mod vs each enemy Perception with -1/10ft distance; observed creatures cannot be hidden from), per-observer hidden_from state treated as total concealment + unseen-attacker +2/deny-Dex for failed observers, attacking or running/charging breaks stealth, move/5-foot-step/withdraw re-check the opposed roll, observation exposes hidden_from (SRD Stealth/Perception/Skill Checks), 13 tests 2026-09-20 21:39:54 +02:00
ctan 61686b5f4e feat(rules): Acrobatics TUMBLING opt-in on move - half speed budget, one Acrobatics check per crossed opponent (DC = CMD, +5 fresh entry into its space trained-only, +2 per additional opponent beyond the first), success spends the opportunity without a strike, failure provokes the AoO and ends the movement (SRD Acrobatics Tumble / Moving Through a Square), transcript logs each check, 9 tests 2026-09-20 21:06:12 +02:00
ctan e0fb5beae2 feat(rules): skill-check subsystem foundation - public skill_check helper (d20 + ranks + ability + trained-class-skill +3 + armor check penalty + typed bonuses, one-shot skill track discharged, no natural 1/20 auto-success/fail per SRD Skill Checks), resolve_skill delegates to it, class-skill +3 now requires invested ranks (Table 4-2 Trained Class Skill row), 8 tests 2026-09-20 20:48:26 +02:00
ctan 678a4d8b31 feat(rules): splash weapon test suite - direct hit full damage + 1 splash to adjacent only, miss scatters d8 clockwise one range increment dealing splash alone (thrower can be caught), energy resistance/immunity apply, DR does not, splash never crit-multiplied, no precision damage, non-splash RNG stream bit-identical, schema rejects splash on melee, observation exposes splash; transcript logs splash after the damage segment 2026-09-20 19:33:12 +02:00
ctan a1b1c29ca3 feat(rules): expose splash in observation attack reference + AttackResult.splash, document splash weapon rule and JSON schema in README (SRD Throw Splash Weapon) 2026-09-20 19:23:15 +02:00
ctan 2e60bbb419 feat(rules): splash weapon resolution - direct hit splashes 1 energy to all within 5 ft (target excluded), miss scatters d8 (1=short toward thrower, 2-8 clockwise) one range increment and splashes the landing + adjacent squares; energy resistance applies, DR and crit never; no precision damage (SRD Throw Splash Weapon) 2026-09-20 19:18:46 +02:00
ctan 9cfcd67f70 feat(rules): SplashSpec data model - data-driven splash marker on thrown weapons, Alchemist's Fire splash 1 fire on oni/tammara (SRD Throw Splash Weapon, Alchemist's fire) 2026-09-20 19:11:39 +02:00
ctan 83c6e541ac feat(rules): nonlethal sources - unarmed strikes deal nonlethal by default (SRD Unarmed Attacks), Minimum Damage floor lands 1 nonlethal (CRB Damage), transcript + AttackResult expose the track 2026-09-20 18:54:22 +02:00
ctan cba7bbf3c9 feat(rules): nonlethal damage track - separate total, derived staggered/unconscious, max-hp overflow, undead/construct immunity, healing removal (SRD Nonlethal Damage) 2026-09-20 18:41:53 +02:00
ctan 3d0fd0c101 docs(rules): conformance journal round 14 - transcript/format audit (14 bugs fixed)
A read-only audit of the transcript rendering, map and loader formats found
14 confirmed bugs (4 Med, 10 Low: duplicate --side, id-disambiguation
collision, range_increment_ft division by zero, silent spell_slots/features
drops, negative round-cap, multi-char legend keys, blank rows, id parsing,
duplicate YAML keys, move_cost default) - all fixed. The transcript/format
area is audited; only the structural out-of-scope items remain. Gate 1900.
2026-09-20 18:08:29 +02:00
ctan dd0b42b73b docs(map): make omitted move_cost explicitly mean impassable 2026-09-20 18:03:46 +02:00
ctan ee22f7548a fix(map): reject duplicate YAML keys via a SafeLoader duplicate-check 2026-09-20 18:00:27 +02:00
ctan 53c6dc0b43 fix(models): reject combatant ids that misparse the transcript format 2026-09-20 17:51:40 +02:00
ctan bce0a59e60 fix(map): fail on blank rows inside a layer, keep trailing-space cells verbatim 2026-09-20 17:48:47 +02:00
ctan 205ddece0b fix(map): require single-character legend keys 2026-09-20 17:43:50 +02:00
ctan c46e636d12 fix(cli): require --round-cap >= 1 2026-09-20 17:40:29 +02:00
ctan 10f811e80e fix(models): require a positive range_increment_ft (reject 0 and negatives) 2026-09-20 17:36:45 +02:00
ctan e9c2a63122 fix(runner): disambiguation suffix skips ids already taken by explicit combatants 2026-09-20 17:33:35 +02:00
ctan de9309ca43 fix(cli): reject duplicate --side names with a clear error 2026-09-20 17:29:10 +02:00
ctan 130087a556 fix(map): reject a zones layer without a deployment section
The README documents deployment as required when zones exist, but load_map
only enforced the reverse direction (deployment without zones). A map with
a zones layer and no deployment now fails with MapValidationError instead
of loading with unreachable deployment zones.
2026-09-20 17:15:29 +02:00
ctan a8c7e37f8e fix(loaders): resource pools validated consistently before grant merge
_merge_resource_grants silently dropped non-int pool values when features
were present (a "5" pool vanished, leaving only the feat grant) while
Pydantic coerced the same value when features were absent. _validated_resources
now runs the JSON resources through TypeAdapter(dict[str, int]) -- the same
lax coercion as Combatant.resources -- so both paths coerce-or-fail
identically, and grants are added to the coerced ints.
2026-09-20 17:13:09 +02:00
ctan 9adc87c9e1 fix(loaders): coerce combatant level with Pydantic-consistent rules
Feat resolution read level with an isinstance(int) check and fell back
to 1, while Pydantic accepts numeric strings and lossless floats: a
level-4 sheet authored as "4" got Toughness for level 1. _parse_level
now runs the value through TypeAdapter(int) (same lax coercion as the
model) and raises CombatantLoadError for values the model would reject.
2026-09-20 17:05:36 +02:00
ctan b8e96294a6 fix(loaders): non-list features and blank/non-string entries fail cleanly
features was popped and only re-added when it was a list, so a bare
string (or dict/null) loaded with zero feats - silent data loss. Now a
present non-list features raises CombatantLoadError, and the list
resolution adopts an accept-string-or-error contract: non-string or
blank entries raise instead of being silently dropped.
2026-09-20 17:02:39 +02:00
ctan 9656ae820a fix(loaders): malformed spell_slots key raises CombatantLoadError, not bare ValueError
int(k) on a non-numeric JSON key escaped the loader as an unhandled
ValueError traceback (CLI exit 1) instead of the documented exit code 2.
Route the conversion through _parse_spell_slot_keys which wraps it in a
CombatantLoadError with a clear message. Audited the other loader-side
int()/conversions: foundry.py guards every int() with isinstance/isdigit
checks and combatant.py has no other unguarded conversion.
2026-09-20 16:58:57 +02:00
ctan 5911602736 docs(rules): conformance journal round 13 - objects/inventory (out of scope -> modeled)
Sunder now uses object hardness/HP with the SRD broken condition (usable at
-2), steal transfers real carried items and unarmed disarm auto-picks up the
dropped weapon. Objects/inventory leave the out-of-scope list; skills,
nonlethal, splash, 3D flight and the transcript remain. Gate 1861.
2026-09-20 16:10:00 +02:00
ctan d0f7f3a107 feat(rules): carried items + real steal transfers, unarmed-disarm auto-pickup
- Combatant.carried_items (APG Steal: non-wielded, non-closely-worn gear),
  loaded from JSON, seeded into CombatantState, exposed in the snapshot.
- _maneuver_steal: success transfers the first carried item victim -> thief
  (logged); empty list keeps the documented nothing-stealable behaviour.
- _maneuver_disarm: unarmed attacker's success auto-picks the dropped weapon
  into its carried_items (SRD Disarm); armed disarms unchanged. Dropped-weapon
  recovery via retrieve_weapon (move action) verified + pinned by tests.
- data/pcs: gear transcribed from Foundry sheet inventories (esha 13,
  jeanne 3, misty 9, nairda 7, oni 6, tammara 20); harvie/ierlieth/
  esha_mount and all monsters empty (no stealable inventory), noted in README.
2026-09-20 16:05:07 +02:00
ctan 74f5d64c43 feat(rules): sunder deals real weapon damage vs object hardness/hp; broken weapons usable at -2 with restricted crits
SRD Sunder + CRB 'Smashing an Object' / 'Conditions - Broken': weapons
carry Table 7-12 object stats (AttackSpec.hardness/hit_points/enhancement,
defaults by weapon class); a successful sunder deals the attacker's actual
physical weapon damage minus the target weapon's hardness, subtracted from
its hp. At <= half hp the weapon gains the broken condition - still usable
at -2 attack/damage, crits only on a natural 20 at x2. At <= 0 it is
destroyed (weapon_destroyed, unusable). The old flat str_mod+2 / durability
10 / synthetic 'disarmed' condition is gone. Firearm misfire carve-out kept
(broken firearms stay unusable until Quick Clear - misfire escalation not
modeled, documented).
2026-09-20 15:43:45 +02:00
ctan 544d870688 docs(rules): conformance journal round 12 - spell components and arcane spell failure
107/108 spells carry their SRD components; arcane_caster and
armor_spell_failure are set from the sheets; the deafened 20% verbal
failure and the arcane spell failure roll are modeled. Components/ASF
leave the out-of-scope list. Gate 1850.
2026-09-20 14:38:42 +02:00
ctan 9f72948d23 docs(rules): README - component/ASF data now encoded, dedup snapshot bullet
The components/ASF bullet no longer defers the data encoding (done in
the two preceding commits): 107/108 registry spells carry their KB
components (Create Alcool is a homebrew cantrip with no KB entry) and
all 15 combatants carry arcane_caster plus an effective
armor_spell_failure - zero everywhere because the shipped arcane casters
are unarmored or wear light armor whose ASF their magus/bard feature
annuls for their own-list spells. Also collapses an accidental
duplicated armor_spell_failure/arcane_caster description in the
observation snapshot bullet.
2026-09-20 14:34:33 +02:00
ctan f6292f8bdc feat(rules): encode arcane_caster and armor_spell_failure from the sheets
- arcane_caster true: misty (Magus/Bard sheet class), nairda (Witch/
  Sorcerer), thrune_agent (Human magus 8, KB bestiary/thrune-agent.md);
  lich already true. False: oni (Cleric), tammara (Gunslinger/
  Warpriest - divine), esha (Cavalier/Rogue, spell-like only), the
  companions harvie/ierlieth/jeanne/esha_mount and the remaining
  monsters.
- armor_spell_failure stays 0 everywhere, as the effective PF1e value:
  nairda and the lich wear no armor (sheet note "Pas d'armure", KB lich
  block); misty's mithral shirt (RAW ASF 10%, magic-items/mithral-shirt
  .md) and the thrune agent's +1 studded leather (15%) are light armor
  and every spell they cast is on their own class list - magus and bard
  cast their own spells in light armor with no arcane spell failure
  (classes/magus.md, classes/bard.md).
- tests/test_spell_failure.py: the lich/oni smoke test becomes a
  full-data table pinning all 15 combatants' flags with per-entry
  sources. No shipped combatant incurs a non-zero ASF; the failure roll
  itself stays covered by the synthetic armored-caster tests.
2026-09-20 14:30:57 +02:00
ctan 91666d84b9 feat(rules): encode SRD spell components for all 108 registry spells
- Every data/spells/*.json now carries a "components" field with the
  V/S/M/F/DF tokens from its KB Components line (material/focus
  descriptions dropped; M/DF and F/DF alternatives kept verbatim).
- 107/108 encoded. Create Alcool (data/spells/create_alcool.json) is
  left without the field: it is a homebrew cantrip (level 0) with no KB
  entry - the closest real spell, Enhance Water (Inner Sea Gods, V, S),
  is already a separate registry entry.
- summon_monster_i/ii map to the KB's numeric files (summon-monster-1/2);
  hidden_diplomacy maps to its KB name Egorian Diplomacy (same spell,
  Spymaster's Handbook rename): V, S, M.
- tests/test_spell_failure.py: the placeholder asserting the registry was
  not yet encoded is replaced by an encoded-state smoke test (all spells
  but Create Alcool declare components; five spot-checks against the SRD
  stat blocks). Golden transcripts unchanged: the component gates still
  need deafened/ASF conditions no golden combat has.
2026-09-20 14:21:15 +02:00
ctan 1f8f068dd1 feat(rules): spell components + deafened verbal failure and arcane spell failure rolls
- SpellSpec.components (frozenset of V/S/M/F/DF, JSON key accepting the RAW
  string notation "V, S, M/DF" or a token list; slash = alternative; unknown
  token is a data error). Default empty = not encoded yet -> no failure.
- Combatant.armor_spell_failure (0-100 percent, authored on the JSON - the
  model has no equipment) and arcane_caster (divine casters never incur ASF);
  both load directly through load_combatant.
- _cast_spell: after the start-of-casting gates and before delivery, two
  conditional d100 rolls (convention mirrors the concealment roll, <=):
  deafened + V component spoils the spell on <=20 (SRD cast-a-spell.md);
  arcane_caster + S component + armor_spell_failure > 0 fails the spell on
  <= the ASF percent (SRD arcane-spells-and-armor.md). The slot stays spent
  (mirrors the concentration fizzle). No condition -> no roll: the RNG
  stream is bit-identical for every current combat (registry encodes no
  components yet - full encoding is a follow-up task).
- Observation: spell summary gains "components" (sorted tuple), combatant
  snapshot gains "armor_spell_failure"/"arcane_caster"; README documents
  the keys and the two former out-of-scope bullets now describe the modeled
  rule with its data-encoding follow-up.
- Data smoke test: lich arcane_caster true / ASF 0, oni arcane_caster false.
- tests/test_spell_failure.py: 21 tests (parsing, thresholds <=20 / <=ASF,
  divine/no-ASF/no-component never roll, roll order, slot stays spent,
  observation keys). Golden transcripts unchanged.
2026-09-20 14:02:10 +02:00
ctan a483070e13 docs(rules): conformance journal round 11 - alignment subsystem (out of scope -> modeled)
Adds the alignment field + axis flags, the AlignmentGate on spell damage
(Shard of Chaos, Spear of Purity) and the attacker-alignment-conditional
wards (Protection from Evil/Law + communal). Alignment leaves the
out-of-scope list; objects/inventory, skills, nonlethal and ASF are recorded
as the remaining out-of-scope mechanics. Gate 1829.
2026-09-20 13:02:15 +02:00
ctan 3be22b9c55 feat(rules): alignment-gated protection from evil/law wards via attacker context
Add attacker-alignment modifier conditions (attacker_is_evil/lawful/good/
chaotic) evaluated against an optional attacker context threaded through
_stat_modifiers/_modifier_condition_holds, _attack_ac_mods (resolve_attack
AC path), resolve_save and _area_save (spell save paths), plus the fear
aura. Without a source in scope the condition is False, so existing rolls
are untouched (golden transcripts bit-identical).

Give the four protection_from_evil/law(+communal) JSONs their SRD buff:
+2 deflection to AC and +2 resistance on fort/ref/will, gated on the
attacker/caster alignment (SRD: bonuses apply 'against attacks or effects
created by evil creatures'); duration 1 min/level approximated as 10
rounds (engine convention). Descriptions document the unmodeled mental-
control suppression and communal duration-split clauses. Level/range/
save/SR verified against the KB.
2026-09-20 12:57:29 +02:00
ctan 6a18d456f6 feat(rules): alignment-gated spell damage for Shard of Chaos and Spear of Purity
DamageEffect gains an optional AlignmentGate (axis law_chaos/good_evil):
the immune side (chaotic/good) is wholly unaffected (no damage dice rolled),
neutral-on-axis targets take half damage with no condition (gate halving
stacks after the save halving), and the full side (lawful/evil) takes full
damage plus the gate condition on a failed save. Shard of Chaos maps the
SRD slowed to the engine staggered condition (the -1 penalty and half-speed
of slow are not modeled); the outsider-only 1d6/level variants are not
modeled (no outsider subtype). Non-gated damage spells are bit-identical.
Loader validates axis/side pairing with clear ValueErrors.
2026-09-20 12:32:14 +02:00
ctan f9e51e377a feat(rules): add alignment field to Combatant with axis gating flags
Adds the PF1e two-axis alignment code (LG/NG/CG/LN/N/CN/LE/NE/CE,
default N) to the Combatant model with a strict validator, derived
is_lawful/is_chaotic/is_good/is_evil properties, exposure in the
observation snapshot, and alignment values for all monsters and PCs
sourced from the bestiary KB and the Foundry sheets.
2026-09-20 11:49:04 +02:00
ctan 40088e1869 docs(rules): conformance journal round 10 - utility spells (guidance, resistance, virtue, stabilize)
Guidance (new OneShotEffect), resistance (+1 resistance save buff, with the
cantrip-skip policy guard), virtue (temporary HP track) and stabilize
(auto-stabilization) are now modeled; spiritual_weapon and saving_finale
remain deferred with a precise engine reason. Gate 1734.
2026-09-20 10:52:22 +02:00
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 154c3f69a0 docs(rules): conformance journal round 9 - assumed residuals closed (10 fixes, 1 deferred)
Rounds 1-3 closed the assumed residuals: run bounded by CON endurance + Run
feat, harmless spells forgo the save on willing targets, self-centered bursts
for bless/bane, Selective Channeling, the lich stat block aligned to the KB,
summon-stats disambiguation, healing_done exposure, multi-cell difficult-terrain
move pricing, and the grazed-caster-cell cone/line fix. The burst intersection
origin is deferred with a precise rationale. Rewrites the residual table to
drop the now-fixed and stale rows, leaving only the genuinely remaining items.
2026-09-20 00:46:08 +02:00