Skip to content

Current Production Version: v0.53.0 — Spells, and the Background section

The half of the 5e sheet that needs to know a character's species and spell list. Splitting it out of v0.52.0 is a scoping call, not a doubt: it is mostly new sheet surface — three sections and two new generic module seams — rather than validation of surface that already exists.

3a — Platform contract

  • [x] 3a.1 — CS-1.10 resource pools. A generic declaration for a countable, spendable, restorable resource with tiers: { id, label, tiers, spendable, chatOnSpend }. 5e spell slots are the first consumer, and Ki, Sorcery Points, Rages and warlock Pact Magic all fit the same surface later; System 8 declares none and renders nothing new. Storage is a typed metadata.resources blob — platform-owned shape, module-declared vocabulary.
  • [x] 3a.2 — CS-1.11 rest types and applyRest. The module declares the rests it has ({ id, label, prompts? }); the platform renders the buttons, collects the prompt answers and calls applyRest(character, restId, choices) for the patch to write. Policy — what a long rest restores, how many hit dice come back, whether a short rest returns slots — stays in the module. The platform never learns what a Warlock is.
  • [x] 3a.3 — A chat seam for the sheet. Spending a slot, resetting slots and finishing a rest all emit chat, and the sheet's only outbound channel today is onExecuteAction(IAction). Add an explicit onSendMessage prop wired at the sheet's call sites, so a section can post a message without impersonating an action.
  • [x] 3a.4 — Prepared spells are a view, not a second collection. CharacterPower gains prepared?: boolean, and TraitSubtypeSchema gains a filtered-view declaration so one collection renders as two sections with two different pickers. (DECIDE — default as described. The alternative, a second collection, duplicates every wizard spell on disk and makes the one state that matters — known but not prepared — unrepresentable without a diff.)
  • [x] 3a.5 — Species and background are character data. A declared origin the module can read: the species (with its ability bonuses), the background, and the free-text escape hatch for both. Modelled as first-class fields rather than metadata, since 3d applies them to every ability score on the sheet.

3b — 5e rules (module)

  • [x] 3b.1 — Spell slots. The multiclass spellcaster table: full casters (Bard, Cleric, Druid, Sorcerer, Wizard) count every level, half casters (Paladin, Ranger) half rounded down, third casters (Eldritch Knight, Arcane Trickster) a third — and Warlock is not in the table at all, since Pact Magic is its own progression that stacks by addition rather than by combined level. Rules: spell-slots-below-table / spell-slots-above-table (warning — a GM may have granted slots, so this describes rather than gates) and spell-slot-level-unavailable.
  • [x] 3b.2 — Prepared and known. A per-class preparation model — Bard, Ranger, Sorcerer and Warlock know spells permanently; Cleric, Druid and Paladin prepare from the whole class list; Wizard does both, preparing from its spellbook — declared as class data, since it is the only thing that decides which of the two sections a character sees. Rules: prepared-count (prepared ≤ spellcasting modifier + caster level, warning), prepared-not-known (a wizard preparing a spell that is not in its book, error) and spell-not-on-class-list (info — subclasses and feats grant off-list spells constantly, so this can only ever be advisory).
  • [x] 3b.3 — Rest policy (applyRest). Long rest: HP to max, every slot to max, half the character's total hit dice back (minimum one), the short-rest tally cleared. Short rest: spend n hit dice, each rolled and added to Con modifier; a Warlock's slots all return with no prompt; a Wizard is offered Arcane Recovery, whose budget is slot levels totalling half the Wizard level rounded up, once per day.
  • [x] 3b.4 — Species ability bonuses. The SRD species and their +2/+1 (2014) or the 2024 background equivalent, as data. Feeds 3d.1's automatic application and finally makes the racial component v0.52.0 ignores into something checkable.
  • [x] 3b.5 — Skills, exactly. skill-proficiency-count stops assuming two background proficiencies and counts the background's actual grant.

3c — Spells on the sheet (apps/web)

  • [x] 3c.1 — Spells Known and Spells Prepared. Today's Spells section is renamed Spells Known; a Spells Prepared section appears for a class whose preparation model calls for one, rendering the same collection filtered by 3a.4's flag. The two pickers differ in domain, which is the substance of the split: a Wizard prepares from its own known list, a Cleric from the entire class list. A character with both models sees both sections.
  • [x] 3c.2 — The Spell Slots section. Rendered like Attributes: a horizontal row of ${level} over ${available} tiles. Clicking one spends a slot — decrement plus a chat message naming the level — and a Reset button restores every level to max, emitting a chat message that states what the counts were before the reset, so a mis-click is recoverable by reading the log.
  • [x] 3c.3 — Editing slots in Build mode. Current and max are freely editable per level; 3b.1's rules describe any disagreement with the table rather than preventing it.
  • [x] 3c.4 — Spell actions spend their slot. An action derived from a spell decrements the matching slot when it runs. Two things stand between here and there: actions execute against a token while slots live on the character, so this needs a token→character write path; and an upcast spell spends a slot the action cannot know about without asking. (Default: a cantrip spends nothing, and every other spell spends a slot of its own level — the slot-choice prompt is 5E-3c's open blocker and rides with v0.54.0's work rather than holding this up. The item most likely to slip; if it does, the manual click in 3c.2 is the whole feature and the release is unharmed.)
  • [x] 3c.5 — The Rest section. Long Rest applies immediately. Short Rest opens a prompt for how many hit dice to spend — each rolled through the dice engine and added to the Con modifier — plus whatever slot recovery the class offers (a Warlock is told, not asked). The section displays hit dice remaining and short rests taken since the last long rest. Both post a chat message summarising what changed.

3d — The Background section (apps/web)

  • [x] 3d.1 — Choose a species and a background, from the SRD lists, or write in a custom one — the same shape as v0.51.0's custom feat and spell authoring, since a homebrew species is the normal case rather than the exception. A custom species carries its own ability bonuses.
  • [x] 3d.2 — Bonuses apply automatically. The species' +2/+1 lands on the final ability scores without the player pressing + — that stepper means an ASI and only an ASI (2f.5), so a species bonus arriving through it would be counted against the ASI budget and flagged. It writes the racial component, which the sheet shows and 3b.4 validates.

3e — System 8 generates its scores the way 5e does

Requested after playing both sheets side by side: 5e's generation modal is the better way to arrive at a set of base scores, and System 8 has an array too — it just never had a surface for it.

  • [x] 3e.1 — CS-1.1 arrayTrades (Module API 1.23.0). A standard array is otherwise take-it-as-it-comes: six fixed values and one decision, where each goes. System 8's is 16 14 12 12 10 10 plus up to two trades, each dropping a score by 2 to raise another by 2 and never raising one past 16. Declared as numbers — { max, cost, gain, raiseCap } — because the platform has to render the trade, and a predicate can only answer yes or no about a finished array. The platform applies them in the order made, which is the same walk the module's reachability check performs, so what the modal produces is what validate() accepts.
  • [x] 3e.2 — Trades in the generation modal. Explicit PAIRS — "−2 from Charisma → +2 to Strength" — with a budget line and Add/Remove, not steppers on each score: the pairing is the rule, and per-score steppers would let a player sit in a half-made trade with the penalty taken and the bonus unspent. A slot whose value a trade moved shows both (1̶0̶ 12), so the six chips being assigned and the scores being written stay visibly different things. A gain clamped by raiseCap shows no change at all, which is what a wasted trade looks like. The pairs persist on the generation record, so reopening shows the trade as a trade.
  • [x] 3e.3 — A single-method system still gets the modal. shouldOfferMethodPicker gates the Select inside the modal, and gating the modal itself on it hid the assignment surface from the one system whose generation is entirely assignment. The button now asks shouldOfferGeneration — is there a method here that generates anything? — and the modal opens straight on the sole method with no Select at all. A recorded method the module has since stopped offering is ignored rather than opened on, which is what stops every existing System 8 character (all of them stamped manual) from opening into a dead end.

System 8's own half of this — declaring the array, its trades, and stepComponent: 'proficiency' so the sheet's + / − stop rewriting base scores — ships as module system8@0.8.0.

Playtest fixes

Found while playing the sheet the four groups above built.

  • [x] Casting with no slot left is called out, not blocked. The spend resolver collapsed "costs nothing" and "nothing left to pay with" into one answer, so casting a 3rd-level spell on an empty slot was indistinguishable from casting a cantrip — the action ran and the log said nothing. Exhaustion is now its own outcome and posts a marked-up line naming the spell and the tier. The action still runs: whether a table allows it is a rules question the module owns.

  • [x] Top-bar tidying. The logo block's divider now meets the left nav rail's right edge — the block was already the rail's width and centred over it, but the parent group's 4px gap pushed the divider past the corner. The Campaign breadcrumb gains the left nav's own crown, so all three sections carry the glyph their nav item does. And the words "Campaign", "Map" and "Character" are gone from the bar: three of them repeated the same nouns on every visit in a viewport that has no room to spare, and the icons already carried the distinction. Each word survives as the accessible name.

  • [x] A Wall could not be selected, and so could not be edited, moved or deleted. One missing case in the map's drawing hit-test: wall fell through to default: return false, so no click ever landed on one. Everything downstream already handled walls — the store moves them, the renderer outlines them, Delete removes them — which is why it read as a wall-specific feature gap rather than a missing branch. The hit-test moved to utils/drawingHitTest.ts to be testable at all; inside WebGLMap.tsx it pulls in three.js and the whole map stack, which is how a hole in a switch over seven shapes went unnoticed.

  • [x] The attribute editor could get stuck open. The focus reveal was driven by React's onBlur alone, and a control that vanishes under the pointer — a stepper the same click disabled, a deleted component row — moves focus to <body> with no blur event at all in Chrome. The tile never learned it had lost focus. The rule now lives in useRevealOnFocus, shared by both tiles: it also closes on focus landing anywhere else (which is the "focusing one attribute blurs the others" fallback, for free) and on a pointer press outside, the only signal left once focus has been orphaned. Every control that can disappear puts focus back in the input, so the orphan is not created in the first place.

  • [x] HP showed one unlabelled number. Two causes: a never-damaged character has no current recorded — null, correctly, in storage — which made Play mode render the max alone and hid the Current control entirely; and Build mode only offered Current once the tile had focus. A character who has never been damaged is at full health, so the sheet reads an absent current as full, and a range now shows Current / Max in Build mode (both labelled, both editable) and current / max in Play.

  • [x] Tab now walks the ability scores. The revealed editor puts two steppers and an Advanced button between one input and the next, so the browser's own order took three presses to reach the following score — eighteen to fill in six. Tab and Shift+Tab move between the inputs and wrap; the steppers stay focusable, just not in the fast path.

  • [x] Empty feat and power slots were invisible until the first one was filled. The section short-circuited on "no traits" and rendered "None.", so the slot scaffold — the whole point of a level-scaffolded subtype — appeared only once a character had somehow acquired a feat. Build mode now renders the slots; Play mode is unchanged.

  • [x] The filters popup inside the feat/power picker opened behind it. A Mantine Popover portals at a default z-index of 300 and the modal is stacked at 2000, so it opened correctly and invisibly. Both pickers' Popover and page-size Select now take a layer above their host.

  • [x] The save indicator sat on top of a number field's +/- chevrons. Mantine draws them in the right section and renders rightSection || controls — one or the other — so every auto-persisting number field reported "saving" and "saved" by taking its chevrons away for the duration, and a level could not be stepped twice in a row. The icon is drawn inside the field now, inset past the section by Mantine's own per-size width, so the two never share pixels.

  • [x] System 8 reported no unfilled feat/power slots — because the platform was running a three-day-old build. The rules were right and dist/system.js still held the pre-0.7.0 validate() with no slot check in it. Rebuilt, and system8 gained the distFreshness guard dnd5e has — extended to compare timestamps, since this incident is precisely the already-built-but-edited case dnd5e's existence check says it cannot catch.

  • [x] The context header was too busy. Play/Build becomes a single pencil — filled in the skin's CTA colour while Build mode is on, a subtle outline while it is off, with a tooltip that names the transition rather than the state. The validation control drops its label at every width and keeps the icon, with the count it used to show leading the tooltip and the severity breakdown after it. The Linked badge is left alone pending a look at the result.

  • [x] Tooltips did not appear anywhere on the sheet. Mantine's default z-index is 300 and the sheet lives inside a Context Panel — often inside a modal — stacked well above it, so every tooltip opened correctly and invisibly behind the thing it described. Both themes now default tooltips to the registry's MODAL_TOOLTIP layer.

  • [x] The artwork button is a photo-edit icon, not a second plain pencil an inch below the Build-mode one, and says "Edit or replace this artwork".

  • [x] Spells Prepared's Add button did nothing. A CS-1.3d view has no picker of its own, so resolving one for prepared-power found nothing registered and rendered nothing at all — no error, no request. A view now borrows its source subtype's picker, and is handed that subtype's identity, since a picker branches on what it is choosing.

  • [x] The spell picker sorts, and filters by a level RANGE (module side, dnd5e 0.6.0).

  • [x] The inactive Build-mode pencil was nearly invisible — --vtt-color-secondary is a dark brown meant to sit behind text, and as a line colour on the header's own dark chrome it barely showed. It draws in gray-3 now, matching the map toolbar's unfilled buttons.

  • [x] Background and Race/Species were clunky. The typeaheads become Selects led by a "Custom …" option — a text field that silently took a catalogue entry's increases when the name happened to match was doing something invisible, and gave no hint a catalogue existed. The help text moved into a tooltip on the field's label, Build mode only. A Customize button opens a modal for the name, the prose and the increases, offered for an SRD pick as well as a custom one: a table's elf is a Crick Elf as often as a Wood Elf, and renaming one keeps its slug, so the sheet still knows which species it is a variant OF.

  • [x] The spell picker gained section headings and a breadcrumb (module side, dnd5e 0.6.0) — headings derived from the sort, and a crumb per grouping level whose menu scrolls its siblings into view.

  • [x] Saves are a section of their own. CS-1.9 rows can claim one with sectionLabel; six of them under six score tiles competed for a 60px column and broke across two lines to repeat a number the tile had already shown. Captioned by ability, with the Build-mode toggle under the modifier. It sits directly after the attribute group its rows come FROM — 5e's saves follow Ability Scores and precede Combat, with the layout naming none of the three — and shares the width through the same grid the scores use, so each save lands under its ability and six fit one row wherever six scores do.

  • [x] Layout tightening. The source-art filename truncates (its old fix was written as a nested selector in Mantine's styles prop, which emits nothing — it is a stylesheet now); the origin bonuses sit under Customize rather than beside it; ability-score inputs are wider at the expense of tile padding; currency inputs are narrower so four fit on one row.

  • [x] The spell picker's class filter opens on the character's own class and groups the options — theirs first, the rest dimmed (module side, dnd5e 0.6.0).