v0.56.0 — Subclasses, proficiencies, and new infrastructure
The v0.55.0 production playtest. Three of these are platform-wide (the portrait's framing, two clipboard paths into the artwork editor, and the equipment typeahead); the rest are 5e, and most of them are the same shape of gap — the module has the rule and the sheet has nowhere to show it.
Artwork and the portrait
- [x] A1 — The portrait crops from the top.
objectFit: 'cover'against a30remcap centres the crop, so a full-length character portrait is cropped to its midriff and the face — the one part of the image the sheet exists to show — is the first thing cut.object-position: top. - [x] A2 — Paste an image into the artwork editor's landing view. A
pastecarrying image data (what "Copy image" puts on the clipboard) uploads it and adds it to the strip, through the sameuploadArtworkthe+tile's drop already uses. The clipboard read is pure and testable; only the listener is not. - [x] A3 — "Paste Image Data" as a source on the Add Source Art view. The fifth entry beside
Upload / Search / URL / Library, for the case A2 cannot serve: replacing a specific artwork,
where the landing view's paste has no way to know which one is meant. Reads the clipboard on
demand via
navigator.clipboard.read()rather than waiting for a paste event.
Equipment
- [x] B1 — The quick-add typeahead never resolves. The catalogue fetch lives in an effect keyed
on
value, so the second keystroke tears down the first one's request through its cleanup (cancelled = true) whilerequested.currentalready blocks a retry. Nothing then callssetLoading(false)orsetRecords, and the spinner spins forever over an empty list. The request is fired once and its resolution stops being tied to an effect instance that a keystroke destroys. - [x] B2 — A quantity stepper per equipment line. Default 1. Build mode, where every other
per-row control on this list already lives; Play mode keeps the read-only
×Nbadge.−stops at 1 rather than deleting the row — the delete button three icons along already says that unambiguously — and composite items are left alone, since a container's count is the items inside it.
Hit points
- [x] C1 — Max HP reads zero on a character who has one. The derived HP row is projected with
components: []and its total ontotal, butNumericAttributeTilereads a range's maximum out of the components — so a Wizard 7 with Con 14 renders0 / 44, in danger colours, with a death-save track under it. The derived row carries its maximum where the tile looks for it. - [x] C2 — A custom max HP is recorded as an override, and says so. Absent means "derive it"; a stored value means the player disagreed. Build mode marks the override and offers a button that clears it back to the derived number.
Race, subrace and background
Module work is on vtt-modules branch version/dnd5e-0.8.0, cut from the dnd5e/v0.7.1 tag —
main is a different commit of the same name and does not compile.
- [x] D1 — An (i) button opens the full description. Beside the name in Play mode; on the Customize row in Build mode, where there is no room inline. Everything else the choice grants — ability increases first among them — comes out of the section body and lives only in that modal.
- [x] D2 — Subraces. A third origin slot beside Species and Background, shown only when the
chosen species declares subraces. Needs a Module API bump:
OriginKindSchema.slotis a closed'species' | 'background'union today, andCharacterOriginhas exactly those two keys. The module data is already there, which makes this smaller than it looks: 2014's subraces are ALREADY top-level records inraces.json(high-elf,hill-dwarf,lightfoot,rock-gnome), the parent names them inmetadata.subraces,origins.tscarries aparentfield, andTYPE_GROUPalready mapssubrace → races. 2024 declares none. So the work is adependsOnonOriginKindSchema, a parent argument onlistOrigins, and hiding the field when the filtered list is empty. One trap: subrace increases are CUMULATIVE in this data — Hill Dwarf isCon +2, Wis +1, which already contains Dwarf'sCon +2. So a dependent slot's grant must SUPERSEDE its parent's, not stack with it, or every subraced character gains its parent's bonus twice.grantingOriginKindreturns exactly one kind today and will need to account for this. - [x] D3 — A background grants its skill proficiencies. 2024 publishes them as
metadata.skillProficiencies; 2014's SRD does not, so that edition's table is written out.validateSkillsstops counting a background's skills against the class's expected number — today they read as an overspend.
Hosted modules
modules.hosted.json moves @purple-cat-games/dnd5e to 0.8.0, whose engine range is
^1.25.0 — the first time this module has needed a floor above ^1.5.0. It is not additive: on
1.24.0 the platform ignores dependsOn, so the Subrace field would appear for every race and a
subrace's increases would stack on its parent's rather than superseding them, quietly giving a Hill
Dwarf +4 Constitution.
Publish before deploying.
dnd5e/v0.8.0must be pushed and published to GitHub Packages before this release is deployed, or the frontend module install fails against a version that does not exist yet.
dnd5e 0.8.0 is branched from the dnd5e/v0.7.1 tag, not from vtt-modules main. Those two
have diverged and main's HEAD is a different commit that merely shares the tag's name; it fails
tsc and four validateSkills tests. Cut module releases from the tag.
Subclasses
- [x] E1 — Subclass data the module can actually serve.
metadata.subclassesis currently whatever the SRD scrape found as a heading — 2014's Wizard reports["Arcane Traditions", "Your Spellbook"], neither of which is a subclass. Real names, the level each class gets one, and the features by level. The SRD is thin here by design — it publishes ONE subclass per class (2014's Wizard has only the School of Evocation, 2024's reports["Evoker"]) — so this is not a scraping bug to fix so much as a table to write. The level a class gains its subclass also has to be stated: 2024 is uniformly 3, 2014 varies by class (Wizard 2, Cleric 1, Fighter 3). Both are in thelevelstable'sfeaturescolumn as prose ("Arcane Tradition"), which is parseable but not a structure. - [x] E2 — The Subclass section. One dropdown per class that has reached its subclass level, with Customize beside it and an (i) that opens every feature the subclass has — including the ones this character is not yet high enough level to use.
Skills and Proficiencies
- [x] F1 — Rename the section to "Skills and Proficiencies". Derived from the lists a system declares rather than hardcoded, so a system with only skills still says "Skills".
- [x] F2 — Tools, Weapons and Armor proficiency lists, each with the same treatment: hidden in Play mode when the character has none, always shown in Build mode with everything known.
- [x] F3 — "Add Custom" on each of the four lists, opening a modal for a name, a description, an ability score, and proficiency (on by default).
Misc
- [x] In the various character card lists (/characters, /campaigns/:id/characters) the portrair art should be used as the Card's content, but since the new character sheet, all such cards are blank.
- [x] In the Character Context, add some core information to more prominent locations:
- [x] Render HP to a bar at the bottom edge of the portrait art. It should be the same color config as the HP bar in the system/campaign. Full width of the Character Context (matching the portrait art container), with a height proportional so it's roughly the same aspect ratio as map bars. Show current/max values in the bar itself, horizontal and vertical center, with a contrasting outline (e.g. black text with white outline in modern dark theme) to ensure readability as the bar moves.
- [x] At 0 HP, show death saves inside the bar.
- [x] Above the HP bar, show a few key stats: AC, Speed, active conditions. Font treatment should be similar to play mode attributes--add an appropriately colored translucent backdrop to this row for readability.
- [x] The above applies to 5e, system5, and system8, not all systems universally.
- [x] Render HP to a bar at the bottom edge of the portrait art. It should be the same color config as the HP bar in the system/campaign. Full width of the Character Context (matching the portrait art container), with a height proportional so it's roughly the same aspect ratio as map bars. Show current/max values in the bar itself, horizontal and vertical center, with a contrasting outline (e.g. black text with white outline in modern dark theme) to ensure readability as the bar moves.
- [x] In the Token Artwork Editor, make the following changes:
- [x] On the landing page, the Token preview is in the left pane, underneath the source art; no longer on the top right.
- [x] On the landing page, the Portrait preview doubles in height, taking up all available space. This allows it to better represent a portrait aspect ratio.
- [x] When the user goes to the Token detail view, animate downward rather than rightward, to reflect the new position.
- [x] Portrait art no longer has to be cropped to be panned and zoomed. Panning and zoomed with no crop selected uses the original image aspect ratio as the clipping area.
- [x] On the landing page, allow basic pan/zoom for both the token and portrait art. For more advanced customizations, the user can still go to the full edit view for that art.
- [x] It seems like AI auto tokenization is not working well, even with easy use cases. Is the model out of date? Throwing errors? Investigate and hopefully fix.
Diagnosed, then fixed. It was never throwing — a live call returned
200with a valid cutout, and the 45s was the lazily-loaded pipeline's cold start — so the complaint was quality, and the model was a generation behind:briaai/RMBG-1.4. Swapped toonnx-community/BiRefNet_lite-ONNX(owner's call). Tested side by side on a real portrait, RMBG-1.4 truncated a hat feather to a hazy stub and left dark fringing along the hair; BiRefNet_lite preserved the feather whole. The licence mattered as much as the quality: BRIA's models are free for non-commercial use only, this build is MIT, and a guard test now pins that so a benchmark-chasing upgrade to RMBG-2.0 cannot quietly reintroduce a commercial-licensing obligation._literather than full BiRefNet because full is a 927 MB ONNX against lite's 213 MB, and the pipeline cold-loads on the first request after boot. The trade is speed: ~10s per image on CPU where RMBG took ~1.5s, paid once per tokenize. - [x] On the landing page, change the drop zone for uploaded art to the entire source art panel--it can be hard to bullseye the 'add or drop source art' button.
- [x] The default border thickness for tokens is zero. Every creation path (blank token,
generated/inserted monsters, encounter groups) and the tokenizer's own default transform now
write
borderWidth: 0; the border slider and saved per-user prefs remain the opt-in ways to get a ring. Legacy tokens saved without aborderWidthkeep rendering with the familiar width-5 ring. The video shader's tokenized gate now honorsmapAppearancelike the image path does, so a borderless tokenized video still crops to a circle. - [x] Every creation path gets a default faction. Tokens added as a Character (New Character, Existing Character, Import Character, Existing Party) default to Party member; tokens added as a Monster (Generate Monster, Select Monster from System, Generate Encounter, New Monster) default to Hostile; blank tokens stay Neutral. Most of this was already wired (v0.21.0, v0.34.0) — the two gaps were Select Monster from System (which sent no faction at all, so the server defaulted it to neutral) and the PDF Import Character flow (which opened the ordinary new-token modal and inherited its neutral default).
- [x] The artwork editor's landing previews no longer jitter after a pan/zoom. Releasing a drag (or letting a wheel burst settle) cleared the preview's local draft immediately while the persist was still an in-flight PATCH, so the preview rendered the stale stored transform for a round-trip — a visible snap back, then forward. The draft now outlives its commit: it keeps rendering until the stored prop actually moves off its at-commit value (our write landing, or another writer superseding it), and a value-equal refresh from an unrelated PATCH leaves it in place. Applies to both the Token and Portrait previews.
- [x] The HP tile no longer spills out of the Combat card in Build mode. The numeric tile painted its editor absolutely over a cell reserved at read-tile height — the ability tile's pattern, which works there because its editor is the read tile's size. A range's editor is taller (Current/Max plus the C2 "Custom" row), so the bottom of it hung outside the card over the section below. Build mode now puts the editor in the flow, so the card grows to hold it; only the focus reveal overlays, with the cell frozen at its settled height so the sheet does not reflow underneath. Verified live on a System 8 sheet.
Class proficiencies (5e and System 5)
Class selection now drives saves, weapon/armor/tool proficiencies and skill guidance. The
architecture is the saving-throw pattern generalized: grants are derived at projection time with
stored rows as the user's overrides — nothing is written to character.skills when a class is
picked, so existing characters light up retroactively and "pre-population" is instant. The platform
stays system-blind (CC-1): it relays two new module-computed metadata keys (grantedByClass on
proficiency rows, classAllowed on skill rows) without knowing which class grants what. Additive on
both sides — no Module API bump; an older platform simply shows the derived tiers without the hints.
- [x] Pre-populate saves and weapon/armor/tool proficiencies from class selection. Saves already
derived from class (both modules); armor/weapon/tool rows now derive tier 1 when a held class
grants them, from new per-class grant tables (dnd5e: hand-written per edition, the
subclasses.tspattern — the 2014 bundle's prose is wrong for Rogue/Ranger; System 5: emitted per-class from the docs frontmatter the generator previously flattened). - [x] Removing a class-granted proficiency is an error. New module validation rule
class-granted-proficiency-removed: fires on an explicit tier-0 override of a granted item, and on a save override where the class grants the save. The toggle still works — validation complains rather than the control refusing. - [x] Validators no longer count class-granted traits against expected totals. Derived grants
are never stored, so they cannot count; and a real pre-existing bug is fixed in both modules —
the skill-count validator counted proficient tool/weapon/armor rows (which share
character.skills) as trained skills. - [x] Class-allowed skills are highlighted in Build mode. Rows a held class points at — choice- list skills and outright grants — tint with the positive accent and say why in the tooltip ("on your class's skill list" / "granted by Fighter"). Play mode stays flat. dnd5e's lists are edition-aware (2024 Fighter adds Persuasion, Wizard adds Nature, Rogue drops Performance).
Late Misc
- [x] The full character page's build toggle says "Build" beside the pencil. On
/characters/:idand/campaigns/:id/characters/:idthe sticky header has the width to name the mode; the Character Context and the sheet modal keep the icon-only pencil. A caller decision (buildToggleLabel), not a width breakpoint — a widened sidebar should stay icon-only. One shared props object keeps the two renderings from drifting apart.