Commit Graph

26 Commits

Author SHA1 Message Date
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 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 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 a6cc5aef36 feat(rules): scale spell damage with caster level
DamageEffect gains optional scaling (1dX per N levels, capped) and scaling_flat (+X per N levels, capped) specs; effective_formula resolves them against the caster's level at cast time, so scaled dice flow through crits, energy resistance and metamagic like the base formula (CRB Magic - Caster Level). Loader rejects mismatched die types. Data: Fireball/Lightning Bolt 1d6/lvl max 10d6, Burning Hands 1d4/lvl max 5d4, Shocking Grasp 1d6/lvl max 5d6, Inflict Light Wounds +1/lvl max +5 (SRD-verified). Cure Light Wounds stays flat: its golden transcript pins 1d8+1 and HealEffect scaling is 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 10:48:09 +02:00
ctan 5b64fbee48 fix(spells): correct the fear condition
PF1e (SRD spells/fear.md): creatures become panicked unless they succeed on a Will save - the JSON applied frightened instead. Condition fixed to panicked. The shaken-for-1-round on a successful save needs partial-save support for conditions and the 30-ft cone needs a cone template, neither of which the engine has; both documented in README as out of scope rather than built.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 13:05:49 +02:00
ctan 910ed1dc7d fix(spells): make the bleed spell bleed
PF1e (SRD spells/bleed.md): "Saving Throw Will negates" and the target takes 1 point of damage per round. Two divergences: the JSON declared a Fortitude save, and the bleeding condition never set the engine's bleed_damage field, so the spell was a no-op. Bleed now saves Will; _apply_condition_effect sets bleed_damage = 1 when a spell applies bleeding, and _decrement_durations lands the final tick as the last bleeding condition expires before clearing the field (heal-path clearing already existed in _stop_bleeding). README updated.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 12:46:53 +02:00
ctan 8c7e7075ad fix(spells): correct the sound burst save
PF1e (SRD spells/sound-burst.md): "Every creature in the area takes 1d8 points of sonic damage and must succeed on a Fortitude save to avoid being stunned for 1 round" — the Fortitude partial negates the stun only, never the dice. The damage component declared half_on_save: true, halving the sonic damage on a successful save. DamageEffect gains a save_negates flag (default True, existing behavior unchanged); sound_burst.json declares half_on_save: false + save_negates: false so the 1d8 is full regardless while the condition keeps save_negates. README updated.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-12 12:31:06 +02:00
ctan 24af1687ab fix(spells): backfill casting times for all imported spells 2026-09-11 15:01:42 +02:00
ctan c5426ffe34 feat(spells): honor spell cast time in the action budget
SpellSpec gains a cast_time field (seven budget codes; the JSON loader
also accepts RAW phrasings like "1 round" or "1 minute"). The pure
budget filters take a CastTimeResolver so a cast_spell occupies the slot
its casting time maps to: standard->standard, swift->swift (competing
with deed/inspire courage and dropped by a consumed immediate),
immediate/free->free, full_round and one_round->full_round (one_round
clamped: resolved end-of-turn, documented approximation), long->dropped
before the budget filter with the new downgraded:cast_time reason. The
engine binds its registry lookup via _cast_time_of (unknown names fall
back to standard, keeping transcripts byte-identical);
_choose_spell_action never selects long casts; the staggered filter now
drops full-round casts and spell_combat via the effective slot
(_FULL_ROUND_KINDS becomes dead and is deleted). Summon Monster I/II are
the only data spells marked 1 round. Metamagic full-round casting for
spontaneous casters is out of scope (future hook documented in
_cast_time_of).

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-11 14:09:58 +02:00
ctan 890b7959d4 fix(rules): correct metamagic slots, touch-spell delivery, and defensive/grapple casting 2026-09-11 00:06:06 +02:00
ctan 5afe495c4a fix(rules): correct P1 PF1e conformance bugs (area saves, concentration, prone, slots, helpless, nauseated) 2026-09-10 20:52:28 +02:00
ctan 1ca5aed758 data(spells): add batch 3 of PC spell list (32 spells) 2026-09-10 19:43:07 +02:00
ctan 543da6a931 data(spells): add batch 2 of PC spell list (32 spells) 2026-09-10 19:12:57 +02:00
ctan 07e0d83300 data(spells): add batch 1 of PC spell list (32 spells) 2026-09-10 18:49:54 +02:00
ctan 7cb3653750 fix(combat): apply save negates to targeted spell damage effects 2026-09-05 12:52:58 +02:00
ctan 2e4d192522 refactor(combat): single source active_buffs/active_conditions 2026-09-04 23:02:23 +02:00
ctan 0058e71550 fix(combat): correct hex and AoE DCs, DR bypassed by spells 2026-09-04 23:02:22 +02:00
ctan fd94522546 feat(data): per-level spell slots for 8 PCs 2026-09-04 23:02:21 +02:00
ctan 1893072b5d chore(lint): restore green gate 2026-09-04 23:02:20 +02:00
ctan 934d443d7d feat(combat): summoning 2026-09-04 23:02:18 +02:00
ctan 8ae344fd16 feat(combat): area-of-effect spells 2026-09-04 23:02:17 +02:00
ctan d792b6e28f feat(combat): AoO and concentration on casting 2026-09-04 23:02:16 +02:00
ctan 789d2f5396 fix(combat): correct spell DC formula 2026-09-04 23:02:15 +02:00
ctan 828afc15c0 feat(spells): add spell system with 10 combat spells, cast_spell action
Spells are SpellSpec dataclasses with typed effects: DamageEffect
(dice + types, half-on-save), ConditionEffect (condition lookup +
save negates), HealEffect (capped at hp_max), BuffEffect (transient
StatModifiers). Range categories (personal/touch/close/medium/long)
scale with caster level via spell_range_ft. JSON loader
(load_spell/load_spell_registry) reads data/spells/*.json.

The engine integrates spells via Action(kind='cast_spell') and
_cast_spell: range check, line-of-effect gate, save resolution
(resolve_save), effect application (_apply_spell_effects with DR
via _apply_dr_for_types), transcript logging. The spell_registry
is passed to CombatEngine; Combatant.spells holds known spell names.

- spells.py: SpellSpec, 4 effect types, SpellRange/SaveType literals,
  spell_range_ft, JSON loader (uses Json type from foundry.py)
- conditions.py: CONDITIONS_BY_NAME registry for condition lookup
- combat.py: cast_spell Action kind, _execute_cast_spell dispatch,
  _cast_spell (range/LoE/save/effects/log), _apply_spell_effects,
  _apply_dr_for_types, _save_label helper, SpellSpec/BuffEffect/etc
  imports, spell_registry parameter on CombatEngine
- models.py: spells field on Combatant
- data/spells/: 10 spells (Magic Missile, Burning Hands, Fireball,
  Lightning Bolt, Acid Arrow, Cure Light/Moderate Wounds, Hold Person,
  Fear, Bull's Strength)
- tests/test_spells.py: 29 tests (loading, range, damage/save/condition/
  heal/buff/DR, out-of-range, unknown spell, dispatch, DC scaling)
- pyproject.toml: PLR0913 ignore for combat.py (CombatEngine.__init__)
- README.md: spells.py architecture section, Sorts removed from
  non-modeled list, 336 tests
2026-08-17 22:49:50 +02:00