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).
This commit is contained in:
2026-09-16 15:36:43 +02:00
parent 881f2ef9b2
commit 3e19d2974e
7 changed files with 161 additions and 14 deletions
+5 -4
View File
@@ -129,13 +129,13 @@ def _make_engine(
class TestSpellLoading:
"""Given: data/spells/ directory with 107 JSON spell files
"""Given: data/spells/ directory with 108 JSON spell files
When: load_spell_registry is called
Then: all 107 spells are loaded with correct fields."""
Then: all 108 spells are loaded with correct fields."""
def test_loads_all_10_spells(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
assert len(reg) == 107
assert len(reg) == 108
def test_spell_names_present(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
@@ -144,9 +144,10 @@ class TestSpellLoading:
"Cure Light Wounds", "Cure Moderate Wounds", "Hold Person",
"Fear", "Bull's Strength", "Acid Arrow",
"Summon Monster II",
"Mage Armor",
}
assert expected <= set(reg)
assert len(reg) == 107
assert len(reg) == 108
def test_fireball_fields(self) -> None:
spell = load_spell(_SPELLS_DIR / "fireball.json")