Skip to content

v0.33.0 - Playtesting and bug fixes

2026-06-14

Playtest Feedback

  • [x] When the user creates a custom feat or power, allow them to edit the feat/power with an edit button that opens the same modal that created it, but pre-populated with the existing data. (Custom feats/powers — those not found in the system, i.e. isUnknown — now show a blue pencil edit button beside the remove button in edit mode, across both the scaffolded slot view and the flat list view. Clicking it reopens the same "Create Custom Feat/Power" modal retitled "Edit Custom Feat/Power" and pre-populated with the existing name + description/brief; saving routes through new onFeatUpdate/onPowerUpdate callbacks that replace the entry in place via name match, preserving its level/slotIndex/buffs. System feats/powers stay non-editable.)
  • [x] When multiple tokens are selected, their on-hover UI shows. With many tokens, this becomes untenable. No longer persist the hover UI when selected.
  • [x] The on-hover UI has three sections: top, right, and bottom. Currently, their position is based on the token's position and size, which can work at high zoom level, but below a certain pixel size, these elements start to overlap. Ensure that the left edge of the right section is never to the left of the right edge of the top and bottom sections.
  • [x] When hovering over a token, show the portrait art in the lower right, contained within a 20rem x 30rem container (invisible), with the portrait flush with the left edge of the chatlog (or 100vw if the log is hidden), and the bottom edge flush with 100vh.
  • [x] Hide the TargetActionBar component
  • [x] Roll requests should appear for anyone with edit access to the token who is impersonating it, not just the owner.
  • [x] On turn start (your impersonated token), center camera on token and ping. For the GM, this means every turn.
  • [x] When heal/harm mode is entered (M key or button in token hover ui), auto-focus the damage field. (Root cause: the prompt-form sanitizer stripped the autofocus attribute — ALLOWED_ATTRS now keeps autofocus/data-autofocus (the latter is Mantine's FocusTrap target convention), and the modal's focus pass runs after the open transition. M added as an alias of H for the heal/harm prompt.)
  • [x] Human TODO: investigate if the drawing tool is putting drawings on the token layer
    • Finding (investigated v0.33.0): yes. New drawings are persisted with layerId: currentLayer (WebGLMap's three completeDrawing() call sites), and since the active layer is almost always tokens, drawings land on the token layer. drawingStore.completeDrawing()'s own layerId: 'tokens' placeholder is what the overwrite happens to (the "Will be set properly when added" comment). The dedicated builtin drawings layer exists but the tool never switches to it — that's exactly what the Drawing-Mode layer-switch TODO below addresses.
  • [x] When a token is above a drawing and they're on the same layer, click dragging moves both of them. Normally when tokens are stacked, the topmost one responds to mouse events, shielding lower tokens from them. This should be the case with drawings as well.
  • [x] The Layer editor has no way to toggle allow_drawings and allow_tokens. Add these toggles.
  • [x] When the user enters Drawing Mode, if the current layer doesn't allow drawings, switch to the first layer lower than the current layer that does (remember the current layer). When they exit drawing mode, switch back to the previous layer if it was changed.
  • [x] Initiative Tracker (not mini, the big one) shouldn't fade out anymore, it should show a complete cycle until just before the current token's next turn.
  • [x] In mini-initiative tracker, mousing over a token's entry should show their full name, and clicking them should center the camera on their token and ping it.
  • [x] When a token's turn starts, anyone impersonating that token, as well as the GM, should get a notification of that token's conditions (e.g. "Poisoned", "Invisible", etc.), so they don't forget. The notification is persistent but dismissable, and not a full screen modal.
  • [x] Add a notes field to tokens, just a text area that GMs can use to keep track of things about the token (e.g. "this is the one with the cursed sword"). Add a new section to the hover UI, on the left side (following the same rules as right, just mirrored), that shows the notes when hovered. Add an edit button to the hover UI that opens a modal where the notes can be edited. (Notes section + edit button are GM-only.)
  • [x] Bug: in a system 8 (or presumably 5e) campaign, create a generic token. After creation, open Edit Token modal and set the HP to some positive value, let's say 20. Close modal.
    • Expected: token has a health bar at max, mousing over shows HP 20/20.
    • Observed: token has no health bar, mousing over shows HP 20.
    • Possible explanation: properties of type "range" internally store a max and current value. Import Character, Insert Monster, and other flows set both, but Create Token does not. (Confirmed: Edit Token's range writes only set ${key}; now setting the current value initializes a missing ${key}_max to that value and ${key}_min to 0, so the bar renders at 20/20.)
  • [x] Bug: heal a token, do some stuff, at some point in the future the healing wears off. Reprodicible: (Tested with Import Character and Insert Monster, but not generic token creation because of previous bug)
    • Create a token with a positive number of HP, let's say 20.
    • Harm the token for some amount, let's say 5, so it's now at 15/20 HP.
    • Heal the token for the same amount, so it's back at max HP.
    • Move the token a few squares.
    • Expected: HP is still at max.
    • Observed: HP is back down to the lower value (15/20 in this example).
    • (Root cause: the Heal action's token.setProperty('hp', …) only mutated the in-memory token — nothing persisted it, so the move's server echo restored the stale pre-heal HP. Harm was unaffected because applyDamage persists via the approval/auto-approve paths. ActionExecutor now diffs the executing token's metadata after every action run and PATCHes changes back.)
  • [x] Bug: focus on token button doesn't accurately point the camera at the token. May be an issue with grid size, or grid position not being in sync with some internal store. In theory, it's easy to calculate camera pixel position based on grid position and grid size. (Exactly that: focusToken fed raw GRID coords to the world-pixel camera. WebGLMap now syncs the live grid params into uiStore and focusToken converts to the token's world-space center, including token size.)
  • [x] Bug: Import Character writes all attributes correctly, except AC which it just ignores and leaves empty. (The "Armor Class" attribute fell through the generic name-lowercasing path into metadata["armor class"], while property definitions read the canonical ac key. AC/Armor Class now maps to ac — value only, no bogus ability modifier or check action.)
  • [x] Bug: moving multiple tokens at once has various inconsistencies. (Confirmed the suspected cause: destinations were computed from the uiStore selection snapshot taken at selection time, so the second drag applied the delta to stale bases. The layer now applies the primary's delta to live token positions and refreshes the selection store after each move. Implemented together with the two items below — they describe the same mechanism.)
    • The way this works is the user drags a single token in the group, and it looks like only that one is moving, but when they mouse up, all the other tokens snap to the same relative position to this primary token as they had at the start of the drag. So effectively you can move whole groups at a time without disrupting their formation. But the current behavior is:
    • Created 5 tokens, selected them all, and moved the primary token.
    • On mouse up, all secondary targets take their correct positions relative to the primary. Great.
    • Drag the primary again.
    • On mouse up, all secondary targets, and the primary, take a totally wrong position, albeit still in the same formation.
    • What seems to be happening (but I'm not 100% sure), is this:
      • Let's say the prime token is in 5,5, and the others are in their own positions (doesn't matter, formation isn't disrupted).
      • Drag the primary to 10,5. Great, it and all secondaries move 5 right.
      • Drag the primary to 11,5. You'd expect all tokens to move 1 right. Instead, they move to 6,5. The movement was "+1 right", but it was applied to their previous position.
      • This may be an issue where the apparent position on the map is not syncing with some internal store. The correct flow should be:
        • User moves a token and mouses up.
        • Immediately, the token stays in that position, having already been dragged there. (Secondaries take a bit of time).
        • An API call goes out to persist the new position, but in the local store, it's already in that position.
        • When the API call finishes, nothing happens because the local store is already correct.
        • If the user drags again before the API call finishes, the movement is calculated based on the current position in the local store, which is correct, not the position at the start of the drag. So the new API call will be correct as well. Persistence may lag behind the client, but should eventually be consistent. Only in the rare case of a hard reload right after a client update, before the API call returns, could there be a desync, and that assumes the app loads before the API call persists, which is unlikely.
        • If the API call fails, there should be an error notification, instructing the user that the client is out of sync, and they can try again (repeating the API call) or reload, possibly losing data.
  • [x] When moving multiple selected tokens, the current behavior is that only the primary token (the one you dragged) moves with the cursor, and the others snap to their new positions on mouse up. This is not ideal. The preferred behavior would be that all selected tokens move together with the cursor, maintaining their relative positions, snapping to the grid as they move, which is normal for single tokens. (The dragged token publishes its grid-snapped delta to a groupDragStore; every other selected token's mesh follows live.)
  • [x] When moving multiple selected tokens, on mouse up, a PATCH is sent for each token. There is visible lag between the mouse up and the token movement. This should be corrected in one of three ways:
    • 1: as described above, immediately move the token in client state on mouse up, and send the PATCHes in the background, with error handling to notify the user if any fail and resync if necessary.
    • 2: as described above, have all token move with the cursor, so the lag is not noticeable on mouse up, and the final positions are correct when the PATCHes return.
    • 3: send a single batch PATCH with all token movements, and apply the changes in the client immediately on mouse up, with error handling as described above.
    • It's possible to combine elements of some of these choices. Use your discretion to choose the best approach.
    • Done (combined 1+2): tokens move live with the cursor (option 2), the optimistic cache writes land synchronously on mouse-up so nothing jumps, and the per-token PATCHes settle in the background via Promise.allSettled — failures raise a persistent notification and the failed token resyncs from the server (the update hook invalidates on error).
  • [x] Current behavior: on hover, a special hover UI shows above, below, and to the right of a token. On click, it is "selected", but that has almost no meaning now. New behavior: hover is unchanged from current, but once selected, the hover UI is persistent until deselected. (Single selection only — multi-select stays persistent-UI-free per the first item; the persistent instance anchors to the live projected rect so it tracks camera and token movement.)
  • [x] Remove the server log of the entire token POST (packages/server-core dev: [tokens.post] Token inserted, returned data: entire JSON) (All three [tokens.post] debug logs removed — received body, insert payload, and returned row each dumped full metadata/actions JSON.)
  • [x] In Initiative, the token whose turn it is has a persistent animated highlight (e.g. a pulsing orange border). This makes it easier to find the token on the map, especially in large combats, and also adds some nice visual flair. (Replaces the old rotating gold/white ring with a pulsing orange border.)
  • [x] When a token is selected, persist the portrait art in the lower right corner that normally appears on hover. (Hovering a different token takes precedence while the hover lasts.)

Bugs

  • [x] Request Access button in Character Context doesn't work. When pressed, it says 'access request sent', and the POST to /api/characters/:id/request-access succeeds, but the character owner doesn't get any notification of the request. There should be a notification for the owner (pushed by web socket), and a badge on the characters menu item in the top level nav, and a list of pending requests in the Character list page. (The socket toast existed but was unreliable: the listener gated on isConnected and never re-joined its room on reconnect — and its Grant/Deny buttons threw on click (useAuthFetch wasn't destructured; fixing it removed 2 baseline TS errors). Beyond the toast fix, requests are now persistent-visible: new GET /api/character-sharing/access-requests/pending backs a count badge on the characters nav item (added to the invite count) and a Pending Access Requests panel with Grant View / Grant Edit / Deny at the top of the Character list page, refreshed live via characterAccessRequest:created/changed pushes.)
  • [x] Not sure what feature is causing this, but the server sometimes logs an enormous amount JSON from system8, line by line, spamming the logs and hiding legitimate messages. Find all verbose logs and squelch them, unless it's an error report. (The system8 content itself never logs — the spam was platform routes dumping whole payloads, which for system8 tokens/characters means 50+ wiki-derived actions pretty-printed line by line. The token POST dumps went in the previous item; this pass trims the map PUT triple-dump (body, parsed data, full updated row) to id/field-name one-liners and the drawings/VBL/fog/vision/light-source POST body dumps to one-line notices. console.error/warn untouched, and a regression test pins that route console.logs never receive req.body/parsed.data/whole rows again.)
  • [x] the mount feature doesn't work. You can mount, but the movement of the two tokens isn't linked. Strange things happen with token stacking, like the rider being above the mount (correct), but clicks to the rider hitting the mount. Correct behavior:
    • Upon mounting, the rider snaps to the center of the mount (even if that breaks grid snap). (Already worked — the mount endpoint centers the rider.)
    • Once mounted, any click-drag or arrow key movement of either the mount or rider moves them both together, maintaining their relative positions. (Server linkage is now bidirectional — moving any group member moves the mount + all riders by the same delta; the client mirrors it optimistically, live-drags linked meshes via the group-drag store, and dedupes PATCHes so a group never gets the delta twice.)
    • Dismounting can be done from either token, and just unlinks them without moving them, so they stay where they are when dismounted. Special case: if the rider is not snapped, they immediately snap to the nearest grid position on dismount, to prevent them from being left in a weird non-snapped position. (Dismount no longer relocates the rider to "the nearest unoccupied space" — it unlinks in place, rounding an off-grid rider to the nearest cell. Dismount remains on the rider's context menu; with linked movement the mount-vs-rider click distinction no longer affects dragging.)
  • [x] in the map token context menu, 'View Character Sheet' does nothing. It should load the character's character sheet in a new tab.
  • [x] Go to /play a map. Impersonate a token. Create a new map. Expected: after loading the new map, no token is impersonated. Observed: token from previous map is still impersonated after loading the new map. (The per-map state saves could fire mid-switch with the new mapId while still holding the old map's impersonated token, persisting it under the new map. Saves now record an impersonation only for the map the token belongs to, and UI.tsx drops any impersonated token whose mapId differs from the loaded map.)
  • [x] Create Token > Create Artwork > drag/drop a large image (e.g. 4k-10k on each side) > close edit token modal.
    • Expected: the token's art is processed as a large image, with compressed full-res and several LOD images stored, and the appropriate LOD image renders on the map based on zoom, with close-in zoom using the full-res image; also, the token's size matches the image (2k x 2k in disk = 2k x 2k on the map).
    • Observed: the token's art is processed as a small image, or in some other way, with no LOD generated, and a low-res image shows on the map, at a small size (probably the upper size limit of regular token art).
    • Note: if you drag-drop the same image directly onto the Background layer, instead of using the artwork editor in Edit Token modal, it works as expected.
    • Note: this has been broken and fixed multiple times.
    • (Three gaps closed this round: the GeneralTab "customize artwork" drawer mounted TokenArtworkEditor without layerId (wrong drop-zone compression profile + artwork-type defaults); the edit-mode upload didn't pass skipResize for map-scale art unlike the create path (server downscaled it, and the LOD pipeline keys off the same category); and edit mode never sized the token to its image — it now applies image-px/gridSize, only while the token still has its default 1x1 footprint.)
  • [x] Character Context shows no portrait for imported characters whose art lives only in the legacy artworkImages field (e.g. "Frost Giant Shaman" in the ABS campaign: character has artworkImages populated but modern artworks null, so the context portrait area renders nothing — while the Build-tab gallery, chat avatars, and the Edit Token modal all show the art).
    • Diagnosed root cause: PlayModeView.tsx (~line 251) resolves the portrait from modern character.artworks only, with no legacy fallback. resolveCharacterChatAvatar in utils/chatAvatar.ts solved the identical gap for chat avatars in v0.30.0 (modern artworks + portraitArtworkId/type fallback → legacy artworkImages); mirror that precedence here.
  • [x] The Token Image / Portrait Image slot trash icons in the Edit Token modal appear to do nothing: click trash on the Portrait Image slot and the same image stays in the slot (reproduced on Frost Giant Shaman and Frost Giant Warrior tokens; users read this as the art "un-deleting itself").
    • Diagnosed root cause: the slot trash only clears the binding id (onPortraitArtworkIdChange(undefined) → PATCH portraitArtworkId: null, which succeeds), but slot display (GeneralTab.tsx ~line 127) and every other consumer fall back by artwork type via getPortraitArt/getPrimaryTokenArt (packages/shared/src/types/artwork.ts) — so the just-unassigned artwork instantly re-binds. No re-import/un-delete is involved; the artwork is simply never removed from token.artworks.
    • Fix: make slot-trash unassign change something the fallback respects — either retype the artwork (e.g. portrait → other) or remove it from the artworks array like the (working) library-thumbnail trash does, with the same confirm. Apply the same semantics to both slots; also ensure unassigning the Token Image slot clears the legacy on-map imageUrl mirror.
  • [x] POST /api/characters/:id/artwork (packages/server-core/src/routes/characters.ts ~line 1319) still writes new art only to the legacy artworkImages column, so character-gallery adds and import flows keep minting legacy-only characters (regenerating the Character Context portrait gap above). Write to the modern artworks array instead (keeping the legacy mirror only if something still requires it), and consider an opportunistic read-time migration for legacy-only characters. (Now writes a portrait-typed ArtworkEntity into artworks and folds existing legacy entries into the array on the same write — deduped by id/url against earlier client-side migrations. The legacy column is no longer written but left untouched as historical data; read-time normalization already existed client-side in useCharacters.)