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.
Round-2 audits (feats/class abilities, combat maneuvers, conditions,
spell data) surfaced 6 High / ~28 Med / ~45 Low findings. This commit
records them with a per-finding disposition (fixed / out of scope /
assumed residual) plus the 17 fix commits (b03a61d..e89ea42) and the
single justified golden update (Weapon Focus now applies).
The filter 'Pistol' now matches 'Braises d'Automne (pistolet ?)' via
case-insensitive substring matching, so the feat is no longer dead.
Golden test_tammara_vs_goblin updated: round-4 pistol shot gains the +1
feat bonus (total 15 -> 16 vs AC 16), turning the MISS into a lethal HIT
(17 damage, goblin 6 -> -11); the goblin dies on round 4 so the old
rounds 4-10 exchange and the draw are gone.
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.
- 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.
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.
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).
Verified against the KB (bestiary/lich.md, Bestiary Lich CR 12):
- bab 8 -> 5 ('Base Atk +5')
- Paralyzing Touch attack_bonus 10 -> 5 ('Melee touch +5 (1d8+5 plus
paralyzing touch)') and marked natural: true (template: 'A lich has a
touch attack that it can use once per round as a natural weapon [...]
treated as a primary natural weapon' - no BAB iteratives per e5691bd)
- damage_bonus 5 -> 0: the +5 (1 point per 2 HD) is already in the
1d8+5 formula and _resolve_swing adds damage_bonus on top (double
counting: the old data dealt 1d8+10)
- ac 25/14/23 -> 23/14/21 ('AC 23, touch 14, flat-footed 21'; touch was
already correct)
- saves 10/10/14 -> 6/7/11 ('Fort +6, Ref +7, Will +11')
- con_score 14 -> 10: 'Being undead, a lich has no Constitution score'
(Con '—'); the int-only schema encodes 'no score' as the neutral 10
(modifier +0), inert anyway since undead are destroyed at hp <= 0.
Left as intentional engine tuning (residual divergences, documented in
the final report): hp 100 vs 111, Str 12 vs 10, Int 20 vs 22,
initiative +6 vs +2, fear aura 30 ft/DC 22 vs 60 ft/DC 18.
Golden transcript untouched (the lich wins via Magic Missile before any
weapon swing; MM DC depends on Int, unchanged). Pinned a new
test_json_matches_srd_stat_block.
SRD Moving Through a Square: 'Any creature can move through a square
occupied by a creature three size categories larger than itself. A big
creature can move through a square occupied by a creature three size
categories smaller than it is. Creatures moving through squares occupied
by other creatures provoke attacks of opportunity from those creatures.'
- _can_pass_through: size ranks differ by >= 3 opens the through-gate
(_blocked_hostile) while _blocked/_cells_free keep the end-gate
('You can't end your movement in the same square as another creature
unless it is helpless').
- _check_enter_aoo wired into _walk_steps (move, explicit-destination
move, and run all commit steps there): the occupant strikes once per
fresh entry, capped by the per-round allowance; a dropped mover stops
the walk. S4c co-occupiable pairs (Tiny-or-smaller) keep no-AoO sharing.
- _has_free_descent generalized to a descending BFS so a walk can cross
a multi-cell Huge+ creature (one-step lookahead dead-ended inside).
- Golden transcript untouched (all 1480 tests green).
A 5-foot step is hampered by difficult terrain: stepping out of OR into a
difficult cell is blocked. _step5 (auto-directed) logs 'wait (cannot step)';
_step5_to (explicit destination) rejects with unreachable_destination.
Engine-level tests cover destination, auto-path, and origin cases.
SRD Conditions Fatigued: A fatigued character can neither run nor
charge. _execute_charge already checked cant_charge; _execute_run did
not -- a fatigued/exhausted/entangled combatant could still run.
Added cant_charge guard to _execute_run (mirrors _execute_charge),
logging wait (cannot run) and returning ok. Two engine-level tests:
fatigued cannot run, exhausted cannot run.
The greedy move/run/flee/charge descent helpers hardcoded diagonal parity 0,
letting a speed-30 walker cover 6 diagonal cells (cell-count budget). Under
5-10-5 (SRD Movement, Tactical) the same walker covers only 4 cells
(1+2+1+2=6 squares).
Threading the running diagonal count:
- _best_move_step / _best_flee_step: accept diagonals_used param
- _descend_path / _run_line_path / _flee_path: track and pass parity
- _charge_end: use _path_cost_5105() instead of len(path) for max check
- _path_cost_5105: new helper; computes total 5-10-5 cost of any path
Updated golden paths in test_aoo_limit_one_per_round (5-cell path now
takes an orthogonal detour to avoid expensive 2nd diagonal), test_run_*
(speed-30 diagonal move stops at 4 cells), and staggered_withdraw
(single-speed diagonal withdraw stops at 4 cells).
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.
SRD Undead/Construct traits: 'immediately destroyed when reduced to 0 hit
points or fewer' — undead and constructs have no Constitution score, so
the PF1e hp <= -CON death rule does not apply to them.
- combat.py: CombatantState.death_threshold returns 0 for creature_type
undead/construct (in addition to the opt-in dies_at_zero flag)
- tests: TestUndeadDeathThreshold pins undead dead at 0 and below, living
keeps the disabled band at 0 and the dying band down to -CON
- golden unchanged: tammara-vs-lich has the lich winning in 1 round
without taking damage
SRD spells/bane.md: enemies within a 50-ft burst centered on the caster
take a -1 penalty on attack rolls and a -1 penalty on saving throws
against fear effects; Will negates. The -1 penalty is approximated with
the shaken condition (-2), matching the existing Doom approximation.
- bane.json: single-target condition -> AreaEffect burst radius_ft 50,
targets "enemies", ConditionEffect shaken 10 rounds save_negates
(range/save kept as medium/will for the registry pin)
- combat.py: _metamagic_transform_sub now copies AreaEffect.targets so
an Extend Spell on an area spell preserves the allies/enemies filter
- tests: TestBaneEnemiesOnly (enemy in burst debuffed, allies/caster
unaffected, successful Will save negates)
SRD spells/bless.md: 'The caster and all allies within a 50-ft. burst,
centered on the caster'. Add an AreaEffect.targets filter (all/allies/
enemies) applied where area creatures are collected; bless.json now
declares radius_ft 50 and targets allies (the caster included).