Skip to content

v0.86.0 — Summoners + chat dice grouping

A character can own other characters, and an action can put them on the map. Shelly the necromancer has two skeletons with their own sheets; today the only way to get Bill and Bob onto the map is the Import Character modal, twice, by hand, after she clicks "Summon Skeletons". The spec wants the button to do it: enter targeting mode, click two squares, and the skeletons appear there. That needs three things the platform does not have — a relationship between characters, a targeting mode that picks squares rather than tokens, and an Actions API surface that can create tokens at all.

Riding along: chat dice grouping. 12d6 in chat renders twelve squares. The idea page's finding is that grouping is not a renderer tweak: the rich dice token models exactly one die type, so 2d20+4d6+10 — the ask's own example — renders as a plain italic line today with no icons and no 3D dice. The multi-group token is the larger half of the work.

Decisions. Chat dice grouping's five open decisions were answered on 2026-09-10 at the idea page's working recommendations: one release, stacked at any count ≥ 2, 3D dice unchanged (but made to fire for multi-group expressions), grouping at render time over a payload that keeps every die, and a grouped d20 highlights if any die in it crits. Two Summoners questions were answered the same day: the Actions API gets a real version constant (ACTION_API_VERSION = '0.2.0', exposed as api.version) rather than a retitled document, because "increment the minor" needs a number to increment; and api.importParty takes an id or the party's name, because parties have no slug column and adding one for a lookup nobody has asked for is a schema change too far.

The spec came back from git history, again — the fourth release running. v0.81.0's migration kept Summoners' prose and dropped its eight tasks, and the dice-grouping page lost its five decisions and their working recommendations. Both restored in S.0, because the roadmap page is where the work lives.

Before either — the specs come back

  • [x] S.0 — Restore the task lists. Summoners' eight tasks (from 2d922c6c^:docs/committed/9-summoners/summoners.md) and chat dice grouping's scope and five decisions (from docs/ideas/chat-dice-grouping.md at the same commit) go back onto their roadmap pages, decisions answered in place. Ships with the plan, since restoring the spec is writing it.

Riding along — a playtest report

  • [x] The dice tooltip that was the timestamp. The chat bubble shows its timestamp on hover; a die with a pointer cursor under that tooltip read as "the timestamp is the die's tooltip", nonsensically. G.4's click popover on a stack is a hover tooltip now — the group's dice and their sum — a single die says what it is on hover, and the bubble's timestamp tooltip stays quiet while the pointer is over a dice row (Mantine tooltips do not nest away from each other, so the wrapper watches where the pointer is). A click still opens the full breakdown.

Summoners

Spec: summoners · Actions API 0.1 → 0.2

  • [x] S.1 — Character relationships. A character_relationships table: characterId1 (the owner), characterId2 (the related character), relationshipType (free string), directional — Shelly → Bill is "minion"; nothing is inferred in the other direction. sqlite mirror + Postgres migration. Routes under /api/characters/:id/relationships (list, create, delete), gated on edit access to the owning character and read access to the related one, plus /api/characters/:id/relationship-types — the types already in use across characters the user can see, for the typeahead.
  • [x] S.2 — The relationships UI. A Relationships block beside Campaign & Party in the sheet's metadata section — which is both surfaces the spec names, since the Character Context and /characters/:id render the same sheet. One row per related character: the character (chosen from the ones you have access to) and a pill input of its relationship types. Typing min suggests minion; Enter confirms the suggestion or creates the type exactly as typed; pills delete. Play mode shows the summary, Build mode edits, like its neighbours.
  • [x] S.3 — token.characterId in the Actions API. The proxy already reads the column through; a legacy token that names its character only in metadata.characterId gets it from there instead. Documented, in the completions, and on target / targets too.
  • [x] S.4 — A targeting mode that picks squares. Targeting today picks tokens; summoning wants empty squares. pickSquares(n, label) on the targeting store: enters targeting mode with a banner ("Choose 2 squares for Bill and Bob"), each left-click on the map takes the hovered grid square, right-click / Escape cancels, and the promise resolves with the points or null. Reused by S.5–S.7 and exposed as api.pickSquares so a script can place anything.
  • [x] S.5 — api.importCharacter(idOrSlug). Resolves the character (/api/characters/:idOrSlug already takes either), picks one square, and creates the token the way Import Character does — projectCharacterToken, then the same payload as the modal, so a summoned token is indistinguishable from an imported one. Returns the created token.
  • [x] S.6 — api.importParty(idOrName). The party's members, one square each, in one targeting session. Id first, then name (case-insensitive) among the parties the user can see.
  • [x] S.7 — api.importRelatedCharacters(characterId, [types]). The characters related to characterId by any of the named types (all types when the array is empty), one square each. Shelly's await api.importRelatedCharacters(token.characterId, ["minion"]) is the spec's example and the release's acceptance test.
  • [x] S.8 — roll(formula, { silent }). The options object gains silent as the readable spelling of pipeToChat: false / roll(expr, false). Both older forms keep working.
  • [x] S.9 — Action API 0.2. ACTION_API_VERSION = '0.2.0' in shared with a history block like MODULE_API_VERSION's, exposed as api.version; the de facto spec retitled 0.2 with the new surface documented; a reference page per new function; the editor completions updated. No per-action stamping and no compat shim — that is 1.0's job and nothing here needs it.

Chat dice grouping

Spec: chat-dice-grouping

  • [x] G.1 — The multi-group dice token. [DICE_ROLL:] gains groups: [{ dieType, dieCount, rolls }] beside the single-group fields, which stay for every message already in the log. 2d20+4d6+10 parses to two groups and a modifier instead of falling through to the plain line, from roll(), from /roll, and from typed chat; the 3D roller throws every die in every group with the same precomputed values.
  • [x] G.2 — Render-time grouping. The renderer lays out N groups plus the constant: [2d20: 27] + [4d6: 15] + 10 = 52. A group of ≥ 2 dice draws one icon with the group total; a single-group 12d6 message from last month gets the same treatment, because grouping happens at render.
  • [x] G.3 — Stacked outlines. Each of the seven die shapes gets a "two of these" variant — an offset second copy of the existing polygon, the way d100 already draws two circles. Used whenever a group has ≥ 2.
  • [x] G.4 — Per-group expansion and crits. Clicking a grouped icon reveals its individual dice, as clicking a single die already does. A grouped d20 takes the crit gradient if any die in it rolled a 20 (and the fumble tint if any rolled a 1).