v0.22.0
2026-05-14
Misc
- [x] For each user, track the number of times they access each character, campaign, and map. Just a simple counter, no metadata. Use this information to create new UI elements:
- On the /characters page, the first section will be "frequently accessed characters". Limited to one row, with a "view all" that expands the section to all characters you've accessed, sorted from most frequent to least. After this section is the normal section that exists today, with filter/sort/grouping as it does today.
- Ditto the campaigns and maps list pages.
- [ ] If I drag and drop a large pre-renderer map onto the background layer, the app correctly creates a background token with LOD. However, if I replace the source art with the same (or similar) full-res file, the resultant token on the map is low resolution. The image loaded is https://d1ybd7scqjtqc2.cloudfront.net/campaigns/campaign_1771469072279_0v53fdp4z/map-objects/moxcnant-ch96xz/image.webp?t=1778271012672 which I'm not sure is LOD specifically, but it is only 47 KB and 512x279, and the original is 4+ megapixels.
- Diagnostic note (v0.22.0): skipped pending fix. Likely cause: the source-image replace handler in
apps/web/src/components/UI/TokenModal/SourceImageEditorDrawer.tsx(around L67-70) clearsprocessedUrl/cutoutUrlbut does not trigger LOD regeneration for the newimageUrl. The LOD lookup path inapps/web/src/utils/lodUtils.ts(getLODUrl, ~L107-122) derives variant URLs by string-mangling the base URL (e.g.image.webp→image-lod1.webp) without checking whether those variants actually exist on the CDN. The renderer inapps/web/src/components/WebGLTokenLayer.tsx(~L689-749) then either fetches a stale 404'd LOD variant or — when its image cache is warm — reuses the previous low-res LOD it loaded before the replacement. Suggested fix: have the replace handler call the same LOD generation/upload path used during initial drag-and-drop, then write the newlodUrls(or whatever metadata field exists) back onto the token before the renderer revalidates.
- Diagnostic note (v0.22.0): skipped pending fix. Likely cause: the source-image replace handler in
- [x] Bug: when creating a new version of a character, several aspects of the duplicate are not duplicated when they should be:
- Linked campaigns
- Linked parties
- Currency
- Equipment
- [x] When pressing the + or - buttons for an equipment line item in the Character Sheet, some annoying behavior occurs:
- Each click, the equipment list briefly flashes out of existing, then back
- The ordering of the items may change each click, making it particularly difficult to click multiple times in a row on the same item. Ordering should not change unless explicitly done so by the user.
- [x] In the equipment list of the Character Sheet, add a handle to each line item allowing the user to drag to sort the items. Persist the ordering in a new field of the equipment data object.
- [x] In the equipment list of the Character Sheet, add two new modes to the + and - buttons that change quantity:
- shift-clicking will increment in units of 10
- ctrl-clicking will increment in units of 100 (use cmd-click on mac, ctrl-click is a right click)
- shift-ctrl-clicking will increment in units of 1000
- alt-clicking will increment in units of 0.1
- right clicking will pop up a modal allowing the user to enter an arbitrary quantity and confirm or cancel
- [x] In the Character Sheet, when Add to Map is clicked, the map dropdown in the resultant modal does not sort of the maps. They should be alphabetical.
- [x] Bug: intermittently, placing a character token on a map (Import Character or Add to Map from character sheet) causes the token art to render incorrectly on the map. It shows un-tokenized art (a square filling the grid square) instead of the tokenized art (the character portrait cropped to a circle). In the Edit Token modal, the tokenized thumbnail is correct. Merely entering that modal usually corrects the art on the map, but sometimes you need to make some sort of change to the tokenization.
- Fix (v0.22.0): rewrote the tokenization pipeline so the WebGL renderer applies the circular crop / pan / zoom / border at draw time using
tokenizeSettings.transform, rather than reading a pre-bakedprocessedUrldata URL written into the token at creation. The race condition is now structurally impossible:processedUrlis never read by the map renderer (getMapImageUrl/WebGLTokenLayer), and the auto-tokenize and editor-save paths inCharacterDetail.tsxandTokenizationEditorDrawer.tsxno longer write a data URL into the token — only the transform. NewTokenizedImageMaterial+TokenizedImageImplshader components mirror the existing video tokenization shader. Also adds a distinct broken-image graphic so a failed image load is visually distinguishable from a missing image (?placeholder). Existing tokens with a storedprocessedUrlcontinue to work — the field is simply ignored on read and self-heals on next save.
- Fix (v0.22.0): rewrote the tokenization pipeline so the WebGL renderer applies the circular crop / pan / zoom / border at draw time using
- [x] System 8 character sheet validation issues:
- When I create an Attack for a character with a 30 (+10) Dexterity, and make it a melee weapon based on Dexterity, with +0 hit bonus from the weapon, the following resultant values should be:
- To hit: +10 (formula is just the ability modifier plus hit bonus, no prof bonus for melee weapons). Observed: +13
- Damage: 2d8+10 (formula is the weapon damage dice plus the ability modifier, plus Hit Power * number of dice, no prof bonus for melee weapons). Observed: 2d8+13
- This doesn't seem to be happening for Implements: they have the correct modifiers.
- In System 8, no roll has a proficiency bonus, as proficiency bonus is built in to the ability scores themselves. Double check logic for unarmed strikes and natural weapons.
- When I create an Attack for a character with a 30 (+10) Dexterity, and make it a melee weapon based on Dexterity, with +0 hit bonus from the weapon, the following resultant values should be:
- [x] Bug: when importing a character into a map, they should be set to the Party Member faction. Observed: they are neutral.
- [x] Expanded feature: Factions
- Factions are now a first-class entity with their own data table, API routes, and editor UI.
- Faction UI routes are in /characters/factions
- When the user is in any /characters route, a second level of main navigation appears, showing the 2nd level options (characters, parties, factions). This is also a vertical bar on the left, next to the existing one, and is only visible in /characters routes. It's possible other routes may get 2nd-level nav in the future, so implement this in a reusable way that can be extended to other sections of the app.
- Default factions for any new campaign are: Party Member, Hostile, Neutral. The user can edit these factions like any other, but they cannot delete them.
- User can add any number of custom factions.
- There is a Relationship object that has three fields:
- subject party: reference to the party whose perspective this relationship is from
- object party: reference to the party whose perspective this relationship is about
- relationship: friendly or hostile
- mutual: boolean indicating whether the relationship is reciprocated, default true. If not, then another Relationship object can exist with the subject/object reversed and a different relationship value, allowing for non-reciprocal relationships (e.g. Party A is hostile to Party B, but Party B is neutral to Party A). If true, then the relationship is automatically reciprocated and any change to one side is reflected in the other.
- By default, faction relationships are neutral. We need only store non-neutral relationships in the DB.
- Characters and Parties can now be assigned to zero, one, or many Factions.
- If a party belongs to a given faction, then assigning a character to that party also assigns them to that faction. This doesn't need to be explicitly stored in the DB. For example:
- The party "the Three Amigos" consists of characters Dusty, Lucky, and Ned.
- The party is assigned to the "Party Member" faction.
- We add a new character, Carmen, to the Three Amigos.
- In the database, all 4 of these characters are assigned to the Three Amigos party, but none require an explicit assignment to the Party Member faction, because they all inherit that from the party.
- In the UI, the Party Member faction is shown on the character sheet for all 4 characters, as an assigned faction, but the presentation is special and can't be deleted, since it's inherited. A tooltip explains that to remove the Party Member faction from Carmen, you need to remove her from the Three Amigos party.
- [x] Add a Systems tab to the Admin Dashboard, in which:
- The timestamp of the last data load for each system is shown
- The admin can trigger a reload of the system data, which re-reads the system's JSON files and updates the DB. This is necessary because we currently only load system data on server startup, so if you add a new system or make changes to an existing system, those changes won't be reflected in the app until you restart the server.
- [x] In the Character Sheet, the drop zone for artwork labeled 'drop image or click to browse' is not actually drag-and-drop enabled.
- [x] In CurrentTokenBar, add a "zoom" icon below the Focus button, which when clicked opens a modal with a larger preview of the token's art, filling most of the screen. If they have multiple artworks, the left and right arrows allow them to switch between their artworks; same with left and right arrow keys.
- [x] View mode character sheet context in /play:
- [x] The character sheet is not scrollable.
- [x] Remove the borders around each section, and the borders around each ability score
- [x] Hide Proficiency in System 8
- [x] To the right of Proficiency, show HP (current and max, if linked to a token), AC, and movement
- [x] Make action buttons take up as much width as available so their labels are more readable
- [x] When feat and power names are clicked, they pop up a modal with no real contents. Using the detail url provided in feats.json and powers.json (System 8), go to the detail page for that feat or power, whose .json endpoint includes metadata with the full content, and populate the modal with that. This is used in the 'edit mode' character sheet already, for reference.
- [x] The equipment modals have a lower z-index than the feat/power modals, and consequently are behind the portrait in CurrentTokenBar. Make them match the feat/power modals.
- [x] The equipment modals should show the description field of the equipment objects.
- [x] Change double click behavior on map tokens and the portrait in CurrentTokenBar to open the edit token modal, instead of pinging the token.