v0.49.0
Character Sheet Overhaul: Validation for System 8 character sheets
Main article: docs/committed/5-character-sheet-overhaul/phase-2-validation.md
Rules confirmed with the user 2026-08-08. Three groups, in dependency order: 2a is the platform
contract, 2b is the System 8 rules themselves (sibling repo vtt-modules, so it ships as a module
release), 2c is the UI. 2b is the only group that changes what counts as valid; 2a and 2c are
plumbing and presentation. minimum-*-rank checks against character rank, not slot rank — a
system-side re-spec decision taken specifically so validate() needs nothing but the character.
2a — Contract and preferences (platform)
- [x] 2a.1 — CS-3.3 rule catalog types.
ValidationRule(id, name, description, category, defaultSeverity),SystemModule.validationRulesas an ordered array (display order = order of first appearance, so the catalog owns category ordering and the platform carries no table of its own), andValidationIssue.ruleId. Contract tests + MODULE_API_VERSION bump. - [x] 2a.2 — Preference resolution. Pure resolver: module default → system → campaign → character, most specific wins; each scope stores only overrides; unknown rule ids are preserved, not pruned. Plus the issue partition (active vs muted) the panel renders from.
- [x] 2a.3 — Preference persistence. A dedicated
validation_rule_preferencestable (scope_type, scope_id, rule_id, enabled), uniform across all three scopes, plus GET / PUT / DELETE under/api/validation-rule-preferences. System scope keys onsystems.id, not on the module version that declared the rule, so an admin's setting survives a module upgrade.
2b — System 8 rules (module: vtt-modules/modules/system8)
-
[x] 2b.1 — Rank ladder and slot schedules.
rank_levels→[5, 10, 15, 20]; feats → 3 +[2, 4, 7, 10, 12, 15, 18, 20]; powers → 2 +[3, 6, 8, 11, 14, 16, 19]. Constants become module-owned and authoritative (the wiki-override path is no longer consulted for validation).sheetSchemafollows automatically. RetiregetRankStatus. -
[x] 2b.2 — Ability-score model. Reachability from
16 14 12 12 10 10by ≤2 point-neutral −2/+2 swaps, cap 16; retire "at most one 8" and the 18-max warning; dropracial/ancestryfrom recognized components (Ancestral Feats set minimums via Buffs now); exact-name matching instead of substring. -
[x] 2b.3 — Classes, levels and derived attributes.
class-levels-match-character-level(error),hp-formula=(5 + Con bonus) × level,ac-formula= armor + Dex bonus,ac-not-validatablefor manual mode. -
[x] 2b.4 — Slot and rank rules.
no-empty-*-slots(info),no-extra-*-slots(warning, not exempted bymetadata.extraFeatSlots),minimum-power-rank/minimum-feat-rankagainst character rank. Replaces the oldpower.rank ≤ getRankForLevel(power.level)check. -
[x] 2b.5 — Publish the catalog. Every issue tagged with its
ruleId;validationRulesexported in display order with user-facing names. -
[x] 2b.6 — Retire the duplicate validator.
apps/web/src/utils/system8AbilityScores.ts'svalidateAbilityScoresgoes; the Monster Generator consumes the module and rendersValidationIssues instead of strings (CS-4.2: one engine). -
[ ] 2b.7 — Ship the module. system8 release +
modules.hosted.jsonbump, published before deploy per the hosted-module release flow. -
[x] 2a.4 — Sync the platform's duplicate System 8 constants. Found during 2b.1:
packages/shared/src/types/system8Advancement.tscarries its OWNDEFAULT_SYSTEM8_ADVANCEMENT_METADATAwith the pre-0.7.0 numbers, and it feedspackages/shared/src/utils/rank.ts,templates.service.ts,AddPowerModal,AddFeatModal,FeatsListandPowersList. Until it is reconciled, the add-modals and the old sheet use the old ladder and schedules while validation uses the new ones. Either re-point these at the module (the CC-1 direction) or update the copy.
2c — Validation UI (platform)
- [x] 2c.1 — Side panel shell. Fixed right sidebar when the available width fits ~28–30rem
(desktop, or a wide-enough Context Panel); Issues tab; one Mantine
Alertper issue — red forerror, blue forwarningandinfo— grouped in catalog order with per-category counts, expanded by default, affirmative zero state. - [x] 2c.2 — Narrow-viewport fallback. Sticky-header "n Issues" alert opening a modal with the same content.
- [x] 2c.3 — Inline mute control. Per-issue "disable this rule for this character / campaign / system" (the last two gated on rights), plus the "N muted" reveal. Muting never touches character data.
- [x] 2c.4 — Rules tab. Character-scope toggle list, as a second tab of the panel.
- [x] 2c.5 — Campaign rules page. Subnav item alongside the campaign's existing sections.
- [x] 2c.6 — System rules page. Subnav item alongside the system's existing sections.
- [x] 2c.8 — Rules tab preamble. Explain that the toggles mute notifications and apply to THIS character only, plus a link to the campaign-wide rules for a campaign owner.
- [x] 2c.9 — Dock the aside on the full character page.
/campaign/:id/characters/:idcaps the sheet at 40rem and the aside sits INSIDE that cap, so it measures 640px against a 928px threshold and never docks. The cap belongs on the sheet column, not on the pair. - [x] 2c.10 — Per-section issue indicators. A small icon on each sheet Section that has issues — same icon and colour as the Alert — opening a Popover with that section's issues. Needs an issue→section resolver, since issues carry a category and a field while sections are derived from attribute groups and the module's schema.
- [x] 2c.7 — Add-modal rank affordance.
AddPowerModal's rank badges still speak the retiredoptimal/downrank/upranklanguage; replace with "grey out, don't hide" entries above the character's rank.
Drawing
- [x] In the Stroke Color modal, add a "no stroke" option
- [x] In the Stroke Width input, allow 0px width (same as "no stroke" option)
- [x] When the user changes the grid size of the map, warn them that they're about to mess up all their drawings. Offer a choice to automatically scale and reposition all drawings to the new grid size (we'll make our best effort). While grid size changes dynamically as the the input value changes, do not attempt to scale drawings until the user presses Save and opts in to the scaling. If they don't opt in, leave the drawings as-is and let them deal with it.
- [x] Designate the active tool in the drawing palette with a high-contrast fill, and use the same transformation for every other toggle (snap mode, square mode, and the main tool palette).
- [x] On mobile the drawing sub-palette wraps into a row of stubby columns, because each control group wraps internally. When the strip is too wide for the screen, lay it out as two rows instead: the drawing modes first, then everything else.
Square Mode
- [x] When drawing any shape, you can toggle "square mode". Instead of a literal circle, rectangle, cone, etc, this mode will fill the entirety of each square that is 50% or more within the shape drawn, but not draw the actual shape. The final drawing will be a filled and stroked (as configured) path that exactly matches grid squares.
In pencil mode, merely entering a square with the pencil will fill the whole square.Superseded: this is the brush, a separate tool (below), not a pencil behaviour.
- [x] In square mode, keep showing the outline of the pure shape until the drawing is finalized — it's hard to tell what you're drawing otherwise. The pure shape is unfilled and ignores the stroke settings, using a high-visibility antipodal color derived from the fill (or the stroke when there's no fill); the squared-off shape uses the configured stroke/fill, and the pure shape disappears on mouseup.
Brush
- [x] Add a brush tool, distinct from the pencil. The pencil is a traditional vector shape that outlines an area — drawing a perfect circle with it is equivalent to the circle tool. The brush has a circular nib of configurable radius: the area it sweeps is filled, and the outline around that area is stroked. In square mode it fills every square the nib enters, however slightly (no 50% rule), and the pure-shape guide keeps the nib visible so the fill doesn't hide where you're painting.