Skip to content

fix(charsheet): Spell Save DC + Spell Attack Bonus header & class features on Abilities tab - #416

Merged
100yenadmin merged 1 commit into
mainfrom
fix/char-sheet-spell-dc-features
May 31, 2026
Merged

fix(charsheet): Spell Save DC + Spell Attack Bonus header & class features on Abilities tab#416
100yenadmin merged 1 commit into
mainfrom
fix/char-sheet-spell-dc-features

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 31, 2026

Copy link
Copy Markdown
Member

What & why

Two character-sheet data-depth gaps an optimizer persona flagged as critical in a built-.app playtest. Both are DISPLAY / read-model gaps — the engine already has the data; the character screen didn't surface it. Scope is strictly viewer/server.py + viewer/openworlds/screen-character.jsx + viewer/tests/ (no engine write-paths, no wire contracts touched).


Bug 1 — Spell Save DC + Spell Attack Bonus not shown (caster planning was blind)

Engine has it / display showed it: The engine computes both (servers/engine/server.py:4747 spell_save_dc() returns {spell_save_dc: 8+prof+mod, spell_attack_bonus: prof+mod}), and PR #410 already added per-spell saveDc to the read model (_spell_save_dc). But the Spells tab had no once-at-the-top character-level Spell Save DC / Spell Attack Bonus (the way D&D Beyond shows them once). A caster couldn't plan.

Fix:

  • viewer/server.py: extracted _casting_ability(ch) (shared with the existing _spell_save_dc), added _spell_attack_bonus(ch) and _character_spellcasting(ch) — all reusing the fix(viewer): strip internal routing tag leaking into the chronicle (#410); verify roster/label reports #410 / engine formula (DC = 8 + prof + casting-mod, attack = prof + casting-mod). Exposed as hero.spellcasting ({ability, abilityShort, spellSaveDc, spellAttackBonus}) on the character surface.
  • screen-character.jsx: new SpellcastingHeader rendered prominently at the top of SpellsTab.
  • Honest: a non-caster (Fighter) resolves to None → the header is omitted entirely. No fabricated DC 0.

Bug 2 — Abilities tab empty for a L3 Wizard

Class-features finding — engine HAS (names), engine LACKS (descriptions + racial traits):

  • The engine does populate class/subclass features as NAMES in Character.features (models.py:673, filled from srd_tables.features_through/features_at), and the read model already projects them as hero.classFeatures / hero.proficiencies.
  • The bug was purely display: _character_sheet hardcodes "abilities": [], and AbilitiesTab only read hero.abilities (always empty) → "No active abilities recorded…". The populated classFeatures were only ever rendered on the Feats tab.
  • The engine does NOT model feature descriptions (features is list[str] of names; detail is always "") and has no racial-traits field at all (Character carries no racial_traits).

Fix:

  • screen-character.jsx: AbilitiesTab now renders hero.classFeatures (names) + honest class/subclass/level context (Level 3 · Wizard · School of Evocation).
  • Honest: names only — never invents feature body text; detail shows only if the data carries it. Racial traits / feature prose stay absent.
  • Follow-up (not in this PR): engine doesn't model (a) class-feature descriptions or (b) racial traits — recommend a tracked issue to add SRD-sourced feature text (data exists at data/srd/class_features.json) and a racial-traits field. Minor: class features now appear on both Abilities and Feats tabs; de-duping is a UX follow-up.

How verified

  • New viewer/tests/test_charsheet_depth.py (8 tests, mirrors test_readmodel_surfaces.py): caster DC/attack present (Wizard L3 INT16 → DC 13, atk +5); non-caster (Fighter) omitted cleanly (spellcasting is None); DC/attack track prof+ability (L5 INT18 → DC 15, atk +7); engine class-feature names surfaced; empty list when the engine has none (no fabrication).
  • Full viewer suite (CI lane): 231 passed, 0 failed.
  • screen-character.jsx compiles via esbuild (exit 0).

⚠️ Do NOT close on merge — verify on the next 5-persona sweep (optimizer ≥7, 0 critical).

Summary by CodeRabbit

Release Notes

  • New Features

    • Abilities tab now displays class features alongside character abilities
    • Caster characters now show a spellcasting summary including spell Save DC and spell attack bonus calculations
  • Tests

    • Added comprehensive tests for character sheet spellcasting calculations, ability tracking, and class feature display

…and class features (#optimizer)

Two read-model/display data-depth gaps an optimizer persona flagged as critical
in a built-.app playtest. Both are DISPLAY gaps — the engine has the data, the
character screen did not surface it. Read-model + viewer only; no engine writes.

Bug 1 — Spells tab never showed the character-level Spell Save DC / Spell Attack
Bonus. PR #410 resolved per-spell saveDc, but there was no once-at-the-top caster
summary. Added _casting_ability/_spell_attack_bonus/_character_spellcasting to
viewer/server.py (reusing the #410 DC formula; mirrors engine server.spell_save_dc:
DC = 8 + prof + mod, attack = prof + mod), exposed as hero.spellcasting on the
character surface, and rendered a SpellcastingHeader at the top of SpellsTab.
Non-caster -> None -> header omitted (no fabricated DC 0).

Bug 2 — Abilities tab read "No active abilities recorded" for a L3 Wizard. The
engine DOES populate class/subclass features as NAMES (Character.features via
srd_tables.features_through), already surfaced by the read-model as classFeatures —
but AbilitiesTab only read hero.abilities (always []) and class features were only
shown on the Feats tab. AbilitiesTab now renders hero.classFeatures + honest
class/subclass/level context. Names only — the engine does not model feature
DESCRIPTIONS or racial traits, so those stay absent (never fabricated).

Tests: viewer/tests/test_charsheet_depth.py (8) — caster DC/attack present,
non-caster omitted cleanly, DC/attack track prof+ability, class-feature names
surfaced + empty-when-engine-has-none. Full viewer suite: 231 passed.

Do NOT close on merge — verify on the next 5-persona sweep (optimizer >=7, 0 critical).
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR extends the character sheet to surface spellcasting summary data (Save DC and attack bonus derived from casting ability and proficiency) and displays class features alongside abilities. Backend helpers compute casting ability once and reuse it across DC and attack calculations; non-casters return None so the UI omits fabricated values. Frontend AbilitiesTab now displays engine-provided class features and exports SpellcastingHeader for rendering. Comprehensive tests validate caster/non-caster handling and HTTP endpoint behavior.

Changes

Spellcasting Surface and Class Features

Layer / File(s) Summary
Spellcasting computation helpers
viewer/server.py
New _casting_ability helper determines which SRD caster ability a character uses from the first matching class in ch["classes"]; _spell_save_dc is refactored to reuse it; new _spell_attack_bonus computes attack modifier; new _character_spellcasting assembler bundles ability key, DC, and attack bonus into a summary object returned by the projection.
Character sheet projection integration
viewer/server.py
_character_sheet now includes a top-level spellcasting field populated from _character_spellcasting, returning the summary object for casters or None for non-casters so the UI omits the block without fabricated values.
Frontend AbilitiesTab and window exports
viewer/openworlds/screen-character.jsx
AbilitiesTab now displays hero.classFeatures (engine-provided class feature names) alongside hero.abilities; computes a class-context header ("Level · Class · Archetype"); updates empty-state messaging; adds titleCaseWord helper for formatting; exports SpellcastingHeader to the global window object.
Test module setup, fixtures, and validation
viewer/tests/test_charsheet_depth.py
New test module with in-memory Wizard and Fighter snapshot; real ThreadingHTTPServer harness with isolated temp state; unit tests validate helpers return correct DC/bonus for casters and None for non-casters; HTTP route tests assert /character-surface exposes spellcasting fields with correct DC/attack/abilityShort, verifies non-caster spellcasting is None, confirms engine features appear in classFeatures, and validates empty-feature edge case.

Sequence Diagram

sequenceDiagram
  participant Client
  participant CharacterSurface as /character-surface
  participant CharacterSheet as _character_sheet
  participant Helpers as Spellcasting Helpers
  participant UI as Frontend AbilitiesTab
  Client->>CharacterSurface: GET ?campaign=wizard
  CharacterSurface->>CharacterSheet: _character_sheet(character_dict)
  CharacterSheet->>Helpers: _character_spellcasting(ch)
  Helpers->>Helpers: _casting_ability(ch) → "intelligence"
  Helpers->>Helpers: _spell_save_dc(ch) → 15
  Helpers->>Helpers: _spell_attack_bonus(ch) → 5
  Helpers-->>CharacterSheet: {abilityKey: "intelligence", spellSaveDc: 15, spellAttackBonus: 5}
  CharacterSheet-->>CharacterSurface: {spellcasting: {...}, classFeatures: [...], abilities: [...]}
  CharacterSurface-->>Client: 200 JSON response
  Client->>UI: render hero with spellcasting + classFeatures
  UI-->>Client: AbilitiesTab displays spellcasting block + class features list
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • electricsheephq/WorldOS#161: Introduced the /character-surface read model and initial character sheet UI integration that this PR extends with spellcasting and class features data.
  • electricsheephq/WorldOS#398: Concurrent work on spellcasting/DC computation and spell rendering in the same viewer/server.py and viewer/openworlds/screen-character.jsx files with overlapping refactoring of spell-related helpers.

Suggested labels

severity:major, screen:character

Poem

🐰 A wizard's arcane secrets now shine clear,
Save DC and bonus appear without fear,
Class features stand proud by abilities' side,
Honest data flows—no fabrications to hide,
The character sheet's depth, at last, on display!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding Spell Save DC + Spell Attack Bonus header and displaying class features on the Abilities tab.
Description check ✅ Passed The description provides detailed context on what changed and why, explaining both bugs and fixes. However, the Licensing/CLA and Validation checkboxes from the template are incomplete.
Linked Issues check ✅ Passed This PR addresses objectives from issue #410 by surfacing spell DC/attack bonus (objective 3) and class features display (part of objective 1). Tests confirm casters show computed values, non-casters omit them, and class features surface honestly.
Out of Scope Changes check ✅ Passed All changes are scoped to viewer-side display/read-model gaps: viewer/server.py (helper functions), viewer/openworlds/screen-character.jsx (UI components), and viewer/tests/ (test coverage). No engine write-paths or wire contracts modified.

✏️ 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.

@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: 2

🤖 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 709-713: The message shown when {nothing && (...) } incorrectly
claims no "racial features" without checking hero.raceTraits; update the
conditional that builds the string (the JSX block using classLine inside the
{nothing && (...) } render) to also verify hero.raceTraits (or equivalent
prop/state) before mentioning racial features, so the fallback text only
references racial features when hero.raceTraits is empty/absent — adjust the
ternary that uses classLine to include a check like classLine &&
!hero.raceTraits (or include hero.raceTraits in the overall `nothing`
calculation) so the copy accurately reflects available race traits.

In `@viewer/server.py`:
- Around line 3635-3638: The lookup in _casting_ability currently only uses
cl.get("name") via _text(...) so rows that use "class_name" are missed; update
the lookup (in the block that assigns a =
_CASTING_ABILITY.get(_text(cl.get("name")).lower())) to fallback to
cl.get("class_name") when "name" is not present or empty (e.g., compute a key
from _text(cl.get("name") or cl.get("class_name")).lower() or attempt both
keys), then use that key to query _CASTING_ABILITY so casters are correctly
detected and spellcasting is emitted.
🪄 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: e6a47ee3-0e2a-4801-bb9b-6b3eeecf22c8

📥 Commits

Reviewing files that changed from the base of the PR and between c31a95f and e0f1d02.

📒 Files selected for processing (3)
  • viewer/openworlds/screen-character.jsx
  • viewer/server.py
  • viewer/tests/test_charsheet_depth.py

Comment on lines +709 to +713
{nothing && (
<p className="body-sm muted" style={{ margin: 0 }}>
No active abilities recorded — this hero's edge is in their feats and class features.
{classLine
? `No class, subclass, or racial features are recorded for this ${classLine} yet.`
: "No active abilities recorded — this hero's edge is in their feats and class features."}

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

Don't mention racial features here unless you check hero.raceTraits.

Line 711 only proves hero.abilities and hero.classFeatures are empty, so a hero can still have projected racial traits and get told none are recorded.

✏️ Suggested copy fix
-            ? `No class, subclass, or racial features are recorded for this ${classLine} yet.`
+            ? `No class or subclass features are recorded for this ${classLine} yet.`
🤖 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 709 - 713, The message
shown when {nothing && (...) } incorrectly claims no "racial features" without
checking hero.raceTraits; update the conditional that builds the string (the JSX
block using classLine inside the {nothing && (...) } render) to also verify
hero.raceTraits (or equivalent prop/state) before mentioning racial features, so
the fallback text only references racial features when hero.raceTraits is
empty/absent — adjust the ternary that uses classLine to include a check like
classLine && !hero.raceTraits (or include hero.raceTraits in the overall
`nothing` calculation) so the copy accurately reflects available race traits.

Comment thread viewer/server.py
Comment on lines 3635 to +3638
a = _CASTING_ABILITY.get(_text(cl.get("name")).lower())
if a:
ability = a
break
return a
return None

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

Support class_name fallback in _casting_ability.

Line 3635 only reads cl.get("name"). If a snapshot class row uses class_name (already handled elsewhere in this file), casters are misclassified as non-casters and spellcasting is omitted.

Proposed fix
 def _casting_ability(ch: dict) -> str | None:
@@
     classes = ch.get("classes") if isinstance(ch.get("classes"), list) else []
     for cl in classes:
         if isinstance(cl, dict):
-            a = _CASTING_ABILITY.get(_text(cl.get("name")).lower())
+            class_name = _text(cl.get("name") or cl.get("class_name")).lower()
+            a = _CASTING_ABILITY.get(class_name)
             if a:
                 return a
     return None
🤖 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 3635 - 3638, The lookup in _casting_ability
currently only uses cl.get("name") via _text(...) so rows that use "class_name"
are missed; update the lookup (in the block that assigns a =
_CASTING_ABILITY.get(_text(cl.get("name")).lower())) to fallback to
cl.get("class_name") when "name" is not present or empty (e.g., compute a key
from _text(cl.get("name") or cl.get("class_name")).lower() or attempt both
keys), then use that key to query _CASTING_ABILITY so casters are correctly
detected and spellcasting is emitted.

@100yenadmin
100yenadmin merged commit ac3a338 into main May 31, 2026
9 checks passed
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