Skip to content

feat(viewer+engine): browsable full class spell list for prepared casters (#754) - #887

Merged
100yenadmin merged 1 commit into
mainfrom
fix/caster-spell-mgmt-754
Jun 14, 2026
Merged

feat(viewer+engine): browsable full class spell list for prepared casters (#754)#887
100yenadmin merged 1 commit into
mainfrom
fix/caster-spell-mgmt-754

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 14, 2026

Copy link
Copy Markdown
Member

Persona complaint (2026-06-15 confirm sweep, optimizer — MAJOR)

A Paladin / prepared caster genuinely could not plan:

  1. "Spellbook shows ONLY prepared spells — full Paladin spell list not browsable" ([character][major] Spellbook has no unprepared pool; chargen never populates a leveled spellbook #754). A prepared caster should be able to BROWSE the full class spell list (the spells available to prepare), not just the currently-prepared few.
  2. "Rest & Prepare has NO interactive spell-selection step — can't choose prepared spells." The engine prepare_spells tool existed but no viewer step let the caster choose which spells to prepare from a browsable pool.

Fix

Engine (servers/engine/spells.py, servers/engine/server.py) — additive, derived-only, round-trips; engine stays sole writer:

  • spells.class_spell_list(class_name, max_level=None) re-derives a class's full SRD spell list straight from the srd524 classes field (srd-2024_<class> slugs), sorted by (level, name), optionally capped to a slot level. SRD-correct: Paladin L1 == the 13 srd524 paladin L1 spells; half-casters (Paladin/Ranger) carry no cantrips.
  • get_character returns a new preparable_spells field — the full preparable pool for the caster's class(es), capped to the highest available slot level (a L10 Paladin → 30 spells across L1–3), multiclass-merged. [] for a non-caster. Never persisted (purely derived).

Viewer (viewer/server.py, viewer/openworlds/screen-character.jsx) — read-only move-sink, no new write path:

Invariants honored

  • Engine = sole writer: preparable_spells is a read-only derived field on get_character; never written to the snapshot (verified: not persisted, round-trips stable). The viewer relays a do move — it never writes campaign state.
  • Additive-by-default: new field defaults to [] for non-casters / old snapshots; nothing else on the sheet changes.
  • SRD 5.2 correctness: derived against data/srd/srd524/Spell.json (the class↔spell map), so the list is never hand-maintained and can't drift.
  • Wire contracts frozen: no new /move kind, no env/id changes; reuses the existing do relay.

Tests

  • servers/engine/tests/test_spellcasting.py (+5): class_spell_list full pool / max-level cap / unknown-class-empty; get_character preparable_spells for a L10 Paladin + empty for a Fighter.
  • viewer/tests/test_readmodel_surfaces.py (+2): /character-surface surfaces the browsable pool for a prepared caster (enriched, capped to L3) + empty for a non-caster.
  • viewer/tests/test_spellbook_preparable.py (new, +8): JSX-harness over the shipped screen-character.jsx — prep step lists the full pool / selects a non-prepared spell / relays prepare_spells / pre-selects current preparation; SpellbookBrowser shows the Available-to-Prepare section + Prepared-vs-Available marking; non-caster guard.
  • Tier-0 qa/fast_gate.sh: PASS (215 deterministic engine tests). Combined engine+viewer regression: 307 passed.

DO NOT MERGE — for review.

Summary by CodeRabbit

  • New Features
    • Prepared casters now see the full class spell pool when preparing spells, organized by spell level
    • Added "Available to Prepare" section in spellbook showing prepared versus available spells
    • Spell preparation UI seeds selections with currently prepared spells

…ters (#754)

Optimizer persona (2026-06-15 confirm sweep, MAJOR): a Paladin/prepared caster
genuinely couldn't PLAN — the Spellbook + Rest & Prepare only showed the FEW
currently-prepared/known spells, never the full class spell list to prepare FROM.

Engine (additive, derived-only — round-trips; engine stays sole writer):
- spells.class_spell_list(class_name, max_level): re-derives a class's full SRD
  spell list from the srd524 'classes' field (srd-2024_<class> slugs), sorted by
  (level, name), optionally capped to a slot level. SRD-correct (Paladin L1 == the
  13 srd524 paladin L1 spells; half-casters carry no cantrips).
- get_character returns preparable_spells: the full preparable pool for the
  caster's class(es), capped to the highest available slot level (L10 Paladin -> 30
  spells across L1-3), multiclass-merged. [] for a non-caster. Never persisted.

Viewer (read-only move-sink — no new write path):
- /character-surface party shape carries hero.preparableSpells (enriched spell cards),
  mirroring the engine derivation via the lazily-imported spells module.
- SpellbookBrowser shows an 'Available to Prepare' section: the WHOLE class list,
  each tagged Prepared vs Available, alongside the prepared/known groups.
- RestPrepareModal prep step iterates the FULL preparable pool (grouped by level), so
  the caster can SELECT a spell they haven't prepared; the chosen set rides the existing
  'do' /move which now explicitly names prepare_spells (replace) — the engine prepares.

Tests: engine test_spellcasting (+5: class_spell_list pool/cap/unknown, get_character
preparable_spells for caster + empty for non-caster); viewer test_readmodel_surfaces
(+2: browsable pool surfaced + empty for non-caster); new JSX-harness
test_spellbook_preparable (+8: prep-step pool browse/select/relay/preselect, spellbook
available section + prepared-vs-available marking, non-caster guard).

fast_gate: PASS (215). Additive; SRD-correct; round-trip safe.
@100yenadmin 100yenadmin added this to the v1.0.5 milestone Jun 14, 2026
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a "preparable spell pool" feature end-to-end. The engine gains a cached SRD class spell index and a class_spell_list helper; get_character exposes preparable_spells capped to the character's highest slot level. The viewer server projects this as an enriched preparableSpells field. The React UI uses this pool in the Rest & Prepare modal picker (replacing slot-count-based limiting) and in a new "Available to Prepare" section inside SpellbookBrowser.

Changes

Preparable Spell Pool Feature

Layer / File(s) Summary
SRD class spell index and class_spell_list helper
servers/engine/spells.py
Introduces _CLASS_SLUG_PREFIX, a cached _class_spell_index() builder reading SRD spell classes slugs, and the public class_spell_list(class_name, max_level) function returning sorted {name, level} entries.
Engine get_character preparable_spells field
servers/engine/server.py
Adds _highest_slot_level and _preparable_spells helpers (merge, deduplicate, cap by slot level) and wires preparable_spells into the get_character return value.
Engine tests
servers/engine/tests/test_spellcasting.py
Tests validate Paladin pool contents, level range 1–5, (level, name) ordering, max_level capping, empty return for unknown classes, preparable_spells non-empty for prepared casters and empty for non-casters.
Viewer server preparableSpells projection
viewer/server.py
Adds _highest_slot_level_from_sheet and _preparable_spells_view; the latter lazily imports the engine spells module, fetches and enriches the SRD class spell list via _spell_card and save DC, degrades to [] on missing data, and is wired into the character-surface object as preparableSpells.
Viewer server surface tests
viewer/tests/test_readmodel_surfaces.py
Asserts a level-10 Paladin surface returns a preparableSpells pool larger than the prepared list, includes unprepared spells, is capped to highest slot level, includes enriched SRD metadata; Fighter returns an empty array.
RestPrepareModal: full pool picker and prepare_spells move
viewer/openworlds/screen-character.jsx
Adds a slug helper. Rewrites RestPrepareModal to build prepPool from hero.preparableSpells grouped by level, seeds prepared state from the current "Prepared" group, changes completePrep to relay prepare_spells, replace with exact selected names, and updates the spell-step UI with full-pool rendering, empty-state, and aria-pressed toggles.
SpellsTab caster detection and SpellbookBrowser "Available to Prepare" section
viewer/openworlds/screen-character.jsx
SpellsTab treats any hero with preparableSpells as a caster. SpellbookBrowser gains a preparable prop, computes poolByLevel/preparedNames, adds testid attributes to existing group containers, and renders a new "Available to Prepare" section tagging each spell as "Prepared" or "Available".
UI preparable pool tests (Node/Babel harness)
viewer/tests/test_spellbook_preparable.py
Adds a new test file with an embedded Node/Babel VM harness covering RestPrepareModal pool rendering, prepare_spells move relay, preselection of prepared spells, spellbook "Available to Prepare" section presence and tagging, and non-caster exclusion of the section.

Sequence Diagram(s)

sequenceDiagram
  participant RestPrepareModal
  participant hero.preparableSpells
  participant EngineServer
  participant spells.class_spell_list

  RestPrepareModal->>hero.preparableSpells: read grouped prepPool by level
  RestPrepareModal->>RestPrepareModal: seed prepared state from hero.spells["Prepared"]
  RestPrepareModal->>EngineServer: relay /move "prepare_spells, replace [selected names]"
  EngineServer->>spells.class_spell_list: class_spell_list(class, max_level=highest_slot)
  spells.class_spell_list-->>EngineServer: [{name, level}] sorted SRD pool
  EngineServer-->>RestPrepareModal: updated character sheet with preparable_spells
Loading
sequenceDiagram
  participant SpellsTab
  participant SpellbookBrowser
  participant hero.preparableSpells
  participant preparedNames

  SpellsTab->>SpellbookBrowser: open with preparable=hero.preparableSpells
  SpellbookBrowser->>hero.preparableSpells: build poolByLevel
  SpellbookBrowser->>preparedNames: extract from hero.spells["Prepared"]
  SpellbookBrowser-->>SpellsTab: render "Available to Prepare" section<br>with "Prepared"/"Available" tags per spell
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • electricsheephq/WorldOS#298: Introduces SpellbookBrowser({ hero, groups, onClose }) — the exact component this PR extends with the preparable prop and "Available to Prepare" section.
  • electricsheephq/WorldOS#161: Wires screen-character.jsx to live /character-surface data, which is the same projection this PR extends with preparableSpells.

Suggested labels

enhancement, screen:character

🐇 Hoppity hop, a new spell to find,
The SRD index was carefully signed,
Paladin pools in level-and-name order gleam,
"Available to Prepare" now fills up the screen!
prepare_spells, replace — what a wonderful dream!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is comprehensive and well-structured. It clearly explains the problem, the solution across both engine and viewer, design invariants honored, and test coverage. However, it does not include the required CLA and validation checkboxes from the repository's description template. Add the 'Licensing / CLA' and 'Validation' sections with appropriate checkboxes and content as specified in the repository's PR description template.
Docstring Coverage ⚠️ Warning Docstring coverage is 35.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main feature: adding a browsable full class spell list for prepared casters. It identifies the affected components (viewer+engine), uses clear terminology, and conveys the core value to users.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0569b91ea8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread viewer/server.py
Comment on lines +4138 to +4142
# Only real caster classes contribute a preparable pool (mirror _casting_ability).
if not cname or cname.lower() not in _CASTING_ABILITY:
continue
try:
entries = mod.class_spell_list(cname, max_level=max_lvl) or []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve known-spell limits in preparable pools

Because this calls class_spell_list for every caster class, /character-surface now offers the full Wizard/Bard/Sorcerer/Warlock lists whenever they have slots, even when those spells are not in spells_known. The Rest & Prepare modal relays any selected name to prepare_spells, and cast_spell treats a non-empty prepared list as sufficient, so a L5 Wizard with only starter known spells can prepare/cast Fireball without learning it; restrict this pool to classes that prepare from the full class list, or to the character's known spellbook for limited-known casters.

Useful? React with 👍 / 👎.

Comment on lines +707 to 708
} else {
setPrepared({ ...prepared, [lv]: [...cur, name] });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cap the number of spells a player can prepare

With the guard changed to an unconditional append, a player can click every entry in the new full pool and the Seal action sends that entire set to prepare_spells; that engine tool only canonicalizes and stores the list, so a L10 Paladin can prepare all 30 visible L1–L3 Paladin spells instead of a daily preparation limit. Please enforce a preparation cap before appending or before relaying the replacement list.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
viewer/tests/test_readmodel_surfaces.py (1)

462-462: 💤 Low value

Consider defensive assertion before indexing wyll["spells"][0].

The test assumes wyll["spells"] is non-empty when accessing [0]["list"]. While the character setup includes spells_prepared, adding an explicit assertion would make the test more robust and self-documenting:

self.assertTrue(len(wyll["spells"]) > 0, "prepared caster should have at least one spell group")
self.assertGreater(len(pool), len(wyll["spells"][0]["list"]))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/tests/test_readmodel_surfaces.py` at line 462, Add a defensive
assertion before the existing assertGreater call that validates wyll["spells"]
contains at least one element. Insert a assertTrue assertion that checks
len(wyll["spells"]) is greater than 0, with a descriptive message like "prepared
caster should have at least one spell group", immediately before the line
containing assertGreater(len(pool), len(wyll["spells"][0]["list"])). This makes
the test more robust by explicitly documenting the assumption about the spell
data structure and prevents obscure IndexError failures if the assumption is
violated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@viewer/openworlds/screen-character.jsx`:
- Around line 685-701: The React.useEffect hook pre-seeds state from hero.spells
and hero.preparableSpells but only declares hero.id as a dependency, causing the
effect to skip re-running if spells change while the hero ID remains the same.
Add hero.spells and hero.preparableSpells to the dependency array inside the
useEffect call to ensure re-seeding occurs when prepared spells are updated, or
if intentionally preserving in-progress edits is preferred, replace the
eslint-disable-next-line comment with a detailed code comment explaining why
these dependencies are intentionally omitted.

In `@viewer/server.py`:
- Around line 4138-4140: The current filter checks if the class name is in
_CASTING_ABILITY to control access to preparableSpells, but _CASTING_ABILITY
includes all caster classes including non-prepared-casters like Bard, Sorcerer,
and Warlock. Replace the _CASTING_ABILITY reference with a more restrictive
collection that only includes prepared-caster classes (such as Cleric, Wizard,
Druid) that actually support the "prepare from class list" mechanic. This
ensures preparableSpells is only made available for classes that can
legitimately prepare spells.

In `@viewer/tests/test_readmodel_surfaces.py`:
- Line 442: Replace the EN DASH character (–) with a standard ASCII HYPHEN-MINUS
(-) in the comment on line 442. Change "L1–3" to "L1-3" to avoid encoding issues
while maintaining the same meaning in the slot level reference comment.
- Around line 465-466: The test validates that Paladin spell pools cap at level
3 (the maximum), but it does not verify that cantrips (level-0 spells) are
excluded as required. Add an assertion after the existing max-level check to
confirm that the minimum spell level in the pool is at least 1, ensuring no
level-0 spells are present. Reference the same pool variable and sp["level"]
structure used in the existing max assertion.

---

Nitpick comments:
In `@viewer/tests/test_readmodel_surfaces.py`:
- Line 462: Add a defensive assertion before the existing assertGreater call
that validates wyll["spells"] contains at least one element. Insert a assertTrue
assertion that checks len(wyll["spells"]) is greater than 0, with a descriptive
message like "prepared caster should have at least one spell group", immediately
before the line containing assertGreater(len(pool),
len(wyll["spells"][0]["list"])). This makes the test more robust by explicitly
documenting the assumption about the spell data structure and prevents obscure
IndexError failures if the assumption is violated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c9e6930-4957-4201-940f-d041e3a6a923

📥 Commits

Reviewing files that changed from the base of the PR and between ff3412d and 0569b91.

📒 Files selected for processing (7)
  • servers/engine/server.py
  • servers/engine/spells.py
  • servers/engine/tests/test_spellcasting.py
  • viewer/openworlds/screen-character.jsx
  • viewer/server.py
  • viewer/tests/test_readmodel_surfaces.py
  • viewer/tests/test_spellbook_preparable.py

Comment on lines +685 to +701
// Pre-seed the picker with the caster's CURRENTLY prepared spells (from hero.spells'
// "Prepared" group) so opening the modal shows their real preparation, and "Seal" edits it.
React.useEffect(() => {
const seed = {};
for (const grp of (Array.isArray(hero.spells) ? hero.spells : [])) {
if (String(grp.level).toLowerCase() !== "prepared") continue;
for (const sp of (grp.list || [])) {
// place each prepared spell into its level bucket using the pool's known level
const inPool = (Array.isArray(hero.preparableSpells) ? hero.preparableSpells : [])
.find((p) => p.name === sp.name);
const lv = inPool ? (Number(inPool.level) || 0) : 0;
(seed[lv] = seed[lv] || []).push(sp.name);
}
}
if (Object.keys(seed).length) setPrepared(seed);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hero.id]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The useEffect dependency array excludes hero.spells and hero.preparableSpells, risking stale pre-seeding.

The effect pre-seeds prepared state from hero.spells but only depends on hero.id. If the hero's prepared spells change (e.g., after a successful prepare action and surface refresh) while keeping the same hero.id, the picker won't re-seed with the updated preparation.

This may cause the picker to show outdated selections after the surface refreshes. Consider adding hero.spells to the dependency array, or document why re-seeding on spell changes is intentionally avoided (e.g., to preserve in-progress edits).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/openworlds/screen-character.jsx` around lines 685 - 701, The
React.useEffect hook pre-seeds state from hero.spells and hero.preparableSpells
but only declares hero.id as a dependency, causing the effect to skip re-running
if spells change while the hero ID remains the same. Add hero.spells and
hero.preparableSpells to the dependency array inside the useEffect call to
ensure re-seeding occurs when prepared spells are updated, or if intentionally
preserving in-progress edits is preferred, replace the eslint-disable-next-line
comment with a detailed code comment explaining why these dependencies are
intentionally omitted.

Comment thread viewer/server.py
Comment on lines +4138 to +4140
# Only real caster classes contribute a preparable pool (mirror _casting_ability).
if not cname or cname.lower() not in _CASTING_ABILITY:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict preparableSpells to prepared-caster classes only.

On Line 4139, filtering by _CASTING_ABILITY includes classes like Bard/Sorcerer/Warlock, which are casters but not prepared-casters. That makes this pool available where “prepare from class list” is not valid.

💡 Suggested fix
 _CASTING_ABILITY = {
     "bard": "charisma", "cleric": "wisdom", "druid": "wisdom", "paladin": "charisma",
     "ranger": "wisdom", "sorcerer": "charisma", "warlock": "charisma", "wizard": "intelligence",
 }
+
+# Classes that prepare spells from a daily class list.
+_PREPARABLE_CASTER_CLASSES = {"cleric", "druid", "paladin", "ranger", "wizard"}
@@
-        if not cname or cname.lower() not in _CASTING_ABILITY:
+        if not cname or cname.lower() not in _PREPARABLE_CASTER_CLASSES:
             continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/server.py` around lines 4138 - 4140, The current filter checks if the
class name is in _CASTING_ABILITY to control access to preparableSpells, but
_CASTING_ABILITY includes all caster classes including non-prepared-casters like
Bard, Sorcerer, and Warlock. Replace the _CASTING_ABILITY reference with a more
restrictive collection that only includes prepared-caster classes (such as
Cleric, Wizard, Druid) that actually support the "prepare from class list"
mechanic. This ensures preparableSpells is only made available for classes that
can legitimately prepare spells.

# #754 (optimizer): the Spellbook must let a prepared caster BROWSE the full class spell
# list (what they can prepare FROM), not just the few currently prepared. The surface
# projects `preparableSpells` — the whole Paladin list, capped to the caster's highest
# slot level (L10 Paladin -> L1–3), enriched with the same SRD rules cards.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace EN DASH with HYPHEN-MINUS in comment.

The comment uses an EN DASH (–) in "L1–3" which can cause encoding issues. Use the standard ASCII hyphen-minus instead.

📝 Proposed fix
-        # projects `preparableSpells` — the whole Paladin list, capped to the caster's highest
+        # projects `preparableSpells` - the whole Paladin list, capped to the caster's highest
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 442-442: Comment contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/tests/test_readmodel_surfaces.py` at line 442, Replace the EN DASH
character (–) with a standard ASCII HYPHEN-MINUS (-) in the comment on line 442.
Change "L1–3" to "L1-3" to avoid encoding issues while maintaining the same
meaning in the slot level reference comment.

Source: Linters/SAST tools

Comment on lines +465 to +466
# capped to the highest slot level (L3) — no L4/L5 spells the L10 Paladin can't slot
self.assertEqual(max(sp["level"] for sp in pool), 3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Verify that the Paladin pool excludes cantrips.

Per the PR objectives, "half-casters carry no cantrips." The test verifies the max level but doesn't confirm the absence of level-0 spells. Add an assertion to validate this SRD-correct behavior:

 # capped to the highest slot level (L3) — no L4/L5 spells the L10 Paladin can't slot
 self.assertEqual(max(sp["level"] for sp in pool), 3)
+# half-casters carry no cantrips (level 0)
+self.assertTrue(all(sp["level"] >= 1 for sp in pool), "Paladin pool must exclude cantrips")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# capped to the highest slot level (L3) — no L4/L5 spells the L10 Paladin can't slot
self.assertEqual(max(sp["level"] for sp in pool), 3)
# capped to the highest slot level (L3) — no L4/L5 spells the L10 Paladin can't slot
self.assertEqual(max(sp["level"] for sp in pool), 3)
# half-casters carry no cantrips (level 0)
self.assertTrue(all(sp["level"] >= 1 for sp in pool), "Paladin pool must exclude cantrips")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/tests/test_readmodel_surfaces.py` around lines 465 - 466, The test
validates that Paladin spell pools cap at level 3 (the maximum), but it does not
verify that cantrips (level-0 spells) are excluded as required. Add an assertion
after the existing max-level check to confirm that the minimum spell level in
the pool is at least 1, ensuring no level-0 spells are present. Reference the
same pool variable and sp["level"] structure used in the existing max assertion.

@100yenadmin
100yenadmin merged commit 3ba0d22 into main Jun 14, 2026
16 checks passed
@100yenadmin
100yenadmin deleted the fix/caster-spell-mgmt-754 branch June 14, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant