Skip to content

v0.21.0

2026-05-08

Misc

  • [x] When user clicks the map name in PlayTopBar, the list of maps they can switch to is in no order; it should be alphabetical.
  • [x] Map preview thumbnails don't seem to be updating anymore.
  • [x] useModuleLoader.computeEnabledModuleIds (apps/web/src/hooks/useModuleLoader.ts) silently drops newly-installed defaultEnabled modules for any user who has a non-null user_settings.enabled_modules array. The user's saved list completely replaces defaults instead of being unioned with them, so when a new module ships (e.g. system8), every existing user has it invisibly disabled in the client until they manually toggle it (or an admin patches their row). Fix: union user's saved list with clientModules.filter(m => m.defaultEnabled !== false) so newly-added default-on modules are auto-enabled for existing users; consider also recording per-user "modules I have explicitly disabled" so opt-outs still stick. Discovered via prod issue in v0.20.x where system8 wouldn't appear in the character sheet system dropdown despite being installed (data hotfix: append "system8" to each affected user_settings.enabled_modules).
  • [x] CharacterSheet and NewCharacterModal source their system dropdown from the client-side moduleRegistry via getSystemInfoList(), while /systems and CampaignEditModal source theirs from /api/systems (DB). This split means the character sheet dropdown is gated on the per-user module loader having actually run register() for that system in the current session, and won't reactively update if modules finish loading after the sheet has mounted (no subscription to registry changes). Pick one source of truth — preferred is to switch CharacterSheet/NewCharacterModal to useSystems() so the dropdown always matches what the server says is installed, independent of which client chunks the current user has loaded. Same prod issue as above.
  • [x] Persist the user's list page preferences to localStorage or the DB. This includes Sort and Group on Character, Campaign, and Map list pages. Don't persist Filter.
  • [x] When changing tabs in Chat, the context minimizes every time. Only the minimize button should do that.
  • [x] Remove the secondary initiative tracker from the upper left of the map pane; this is redundant with the new mini-initiative in the chat pane.
  • [x] Default faction:
    • Right now, the default faction for every token is Neutral.
    • When using Import Character or Import Party, assume the faction "Party Member"
    • When using Generate Monster or Generate Encounter, assume the faction "Hostile"
  • [x] In the special context UI that appears when multiple tokens are selected, add a control to set the faction for all selected tokens at once.
  • [x] Chat settings is not visible for mobile users; add it to the header when they tap the Chat tab on the bottom tab bar
    • Implemented in v0.21.0: the ChatSettings gear is now rendered inside the mobile chat overlay header in apps/web/src/components/Layout/MobileContextBar.tsx, alongside the new Chat / Transcript / Initiative dropdown.
  • [x] Mobile chat log has no way to switch between chat, transcript, and initiative. The Chat header should be a dropdown, like in non-mobile.
  • [x] Default 3d dice don't work, you have to choose a different theme or no dice will appear. If this theme can't work, change the default to 'blue green metal' which definitely works
  • [x] On the character sheet, add a way to choose chat color for that character, similar to the Chat Settings color option
  • [x] When sliding the HP bar to change HP, the chat notifications are spammy; only send on mouseup
  • [x] Imported character ability score checks are rolling twice, once with 3d and once in the action.
// Ability Check — await roll() so 3D dice settle and the result is a real number. The return includes the result, but also an additional dice expression that is evaluated.
const result = await roll('1d20+2');
return '<div style="padding: 8px;"><strong>Dexterity</strong> check<br/>1d20+2 → <strong>' + result + '</strong></div>'; // remove the 1d20+2
  • [x] genai module fails in prod: 'Failed to load AI models: Unexpected token '<', "<!doctype "... is not valid JSON'
  • [x] background token art issue: replaced source art, then token appeared tokenized (tokenized/unchanged should only change when the user directly specifies this, not as a result of the source art changing). Opened tokenization settings, selected Unchanged. Expected: token on map and thumbnail in edit artwork UI show unchanged art. Actual: token on map and thumbnail still show tokenized art.
  • [x] when dragging to move a token, pressing space anchors the current square, so a path can have multiple segments (e.g. walking around a 90 degree corner without the movement path phasing through walls). This happens automatically with movement blocking, but it's nice to have a manual option.
  • [x] changing the lighting mode of a map (e.g. indoor to outdoor) doesn't propagate to other players via WebSocket, they have to hard reload to see
  • [ ] indoor, with VBL, and light sources, the PCs can't see anything. Hover overlay shows their sight lines as if they could see, but the tokens and map are all black as if they were inside with no light sources and no darkvision.
    • DEFERRED in v0.21.0: LightingLayer is a global WebGL shader that takes only lightingMode/timeOfDay/isOwner and ignores per-viewer vision. To honor a PC's darkvision (ignoresDarkness/visionRange on their token's vision type) we'd need to either inject synthetic per-viewer "darkvision light sources" into the shader uniforms or render a separate brightness overlay scoped to the active viewer token. Both touch the shader pipeline and need end-to-end map-session testing across vision types + light combinations. Splitting into a focused sub-TODO for a later release.
  • [x] when impersonating, PCs can see targets completely obscured by VBL. Even if a token is lit, if there is no sight line to it, it should disappear
  • [x] when no token is impersonated, the PCs see everything, like the GM. Instead, hide any tokens that can't be seen by any token of faction "Party Member", and apply the same logic to map exploration (e.g. if a token of that faction hasn't seen a square, it should be dark for the PCs even if the GM can see it)
  • [x] Sometimes, when another PC rolls, I see their chat messages as mine. Not sure how to reproduce.
  • [x] mini-initiative is not visible to PCs, or even another tab of my (GM) user. Everyone should see it.
  • [x] add a "minimum" value for ability scores (all systems currently supported including system 8). It's optional. If set, the ability score cannot be below that number. Examples:
    • Strength: base 10, 2 stat adds, +2 race = 14. Minimum set to 18? Now Str is 18.
    • Dexterity: base 16, 4 stat adds, +2 race = 22. Minimum set to 20? Now Dex is 22, because the minimum doesn't reduce the score, it just sets a floor.
    • Implemented as an optional minimum-named component on CharacterAttribute.components. The floor logic lives in getAttributeValue (packages/shared/src/types/character.ts): components are summed normally, the minimum components do not contribute additively, and the final value is Math.max(sum, highestMinimum). D&D 5e (getAbilityTotal in packages/shared/src/systems/dnd5e-2014.ts) and System 8 (validateSingleAbility in vtt-modules/modules/system8/src/system.ts) both already route through getAttributeValue, so the floor applies to both. UI: AttributeBox in packages/ui-components/src/CharacterSheet/CharacterSheet.tsx gained a "Min" inline editor (0 = no floor). Note: System 8's RECOGNIZED_COMPONENT_TYPES list does not yet include "minimum", so authoring a non-zero minimum on a System 8 character will currently surface a benign "Unrecognized 'minimum' bonus" validator warning until the next @purple-cat-games/system8 release adds it to the recognized list.
  • feature idea: character versions. Basically a duplicate of a character linked to the original, so you can explore alternate builds without changing your original character or cluttering your library with multiple versions. The character sheet would have a dropdown to switch between versions, and the character list would show the active version's name with a "version" badge or something. When you click "new version", it creates a duplicate of the current character and sets it as the active version. You can also choose to make an existing version the active one. This is especially useful for level-up, where you want to keep your old character around for reference but also want to have a new version with updated stats and actions.
    • [x] Data layer: add version_group_id (uuid, nullable) and is_active_version (boolean, default true) columns to characters table. New characters get version_group_id = id (each character is its own group of one). Add migration + zod schema updates. Index on version_group_id.
    • [x] API: add endpoints to (a) list versions in a character's group, (b) create a new version by duplicating the current character (copy all fields except id; new row joins the same version_group_id; set newly created row to is_active_version=true and demote previous active), (c) set an existing version as active (flip flags within the group atomically). Server-side tests for each.
    • [x] Character list filter: by default, only show characters where is_active_version=true. Tokens/imports continue to reference whichever character id was selected (no change), but the list page hides the non-active siblings. Add a small "v2", "v3", etc. badge next to the name when the group has more than one version.
    • [x] Character sheet UI: add a "Versions" dropdown in the header showing all versions in the group with the active one marked. Selecting a different version navigates to that character. Add a "New Version" button that calls the duplicate endpoint, makes the new one active, and navigates to it. Add a "Make Active" affordance when viewing a non-active version.
  • [x] Changing to "transcript" does nothing in the ChatLog. Chat and Initiative work fine.
  • [x] Admin mode: add an "admin" toggle on the user level (default false). The only way to set an admin if none are present will be to edit the DB directly. Admins have access to an "Admin" top level nav section, with tabs for:
    • User management: list all users, their email, and whether they are admin. Allow toggling admin status. This UI already exists but is currently visible only on localhost.
    • System health: show status of various system dependencies (DB connection, Ollama connection, SMTP connection if configured, etc) and logs for debugging.
    • Hosted module management: list all hosted modules, their version, and whether they are installed. Allow installing/uninstalling modules and viewing their logs. (future functionality, stubbed in 0.21.0)
    • Implementation progress (atomic sub-commits, in order):
      • [x] Backend: is_admin column on user_settings (sqlite + pg), migration 0046_add_user_settings_is_admin.sql, runtime migrations in db.ts, shared middleware/requireAdmin.ts reading from DB (replacing the localhost-only gate), and new GET /api/admin/whoami endpoint.
      • [x] Frontend: useAdminStatus hook calling /api/admin/whoami; replace all isAdminMode() localhost callers (SideNav, UsersAdmin, useAuthFetch, etc.) with the server-driven flag.
      • [x] Nav restructure: collapse single admin-users nav item into a new top-level Admin section with sub-tabs Users / System Health / Hosted Modules; stub the new pages.
      • [x] Users tab: add "Admin" column + toggle (PATCH /api/admin/users/:id/admin).
      • [x] System Health tab: backend probe endpoint (DB / Ollama / SMTP) + status-card UI.
      • [x] Hosted Modules tab: read-only listing of data/modules-manifest.json + modules.hosted.json with disabled action buttons (full feature lands in 0.22.0).