Skip to content

v0.104.0 — Timeline

Shipped 2026-09-21 — built on version/0.104.0, thirteen commits, merged to main as 432d0ffc, tagged v0.104.0. The fourteenth of the releases the near-term roadmap sequences from the August 2026 heuristic evaluation; the roadmap page is Timeline. Not the Time Machine, which is edit history: where the map tracks where things are, the Timeline tracks when. The help is The Timeline. No module release; Action API 0.6.0.

Initiative was a client store relayed between whoever was connected — persisted nowhere, so a reload asked the GM. Now initiative is the map's, a TimedEffect runs on a map's Timeline from a tick for a duration in rounds, indefinitely, or until a function says so (a save that ends it, asked of the token acting), actions create and end effects through api.timeline, and a vertical strip beside the chat draws it all — ticks and rounds, the now-line, tokens at their initiative, effects as coloured slices — with a drawer on the phone. Decisions TL-D1 (a function's ask is asynchronous; only the asked token waits) and TL-D2 (indefinite effects survive initiative ending; counted ones freeze) at their recommendations.

Initiative becomes state

  • maps.timeline (T.1) — { active, entries, turn, round, movementRule } on both dialects (migration 0046), the model and its pure functions in @purple-cat/shared's timeline/; GET / PATCH /api/maps/:id/timeline (a reader reads, an Editor writes), every write a map commit and a timeline viewstate announce.
  • The store is a cache (T.2) — useTimeline reads the map, useInitiativeSync applies it to the store and writes a GM's changes back; the store's persist and the initiative:start / update / end / request relays are gone. A reload, a late joiner and a second GM tab read the map.
  • Ticks (T.3) — a tick is a round, an initiative and a tie index, encoded as the spec's round.initiative (3.14, ties in the next two places) and ordered by compareTicks; advanceTimeline / endGroupTurn are the store's turn logic as pure functions; POST /timeline/advance (an Editor, or whoever may play the acting token; "turn advanced" / "round started" commits) is a player's End Turn, and the last relay but movement notes went with it.

TimedEffects

  • The table and the routes (T.4) — timed_effects (migration 0047): name, colour, startTick, a duration in rounds / indefinite / a function, the token it is on, how it ended; list / create / patch / end / delete under /api/maps/:id/effects, each a map commit (the effects are in the map's history set) and a timedEffect announce. A new timedEffect grant entity inherits from the map with the creator as intrinsic Owner: a Collaborator starts, the one who started it or the GM changes and ends, a Viewer sees.
  • Expiry (T.5) — every tick (writeTimeline sees it move) evaluates the running effects: counted ones by arithmetic; function durations in the script sandbox as (round, whoseTurn) => boolean, where whoseTurn.requestRoll(expr) is a pause, not an await — the first call records the ask and delivers the v0.86.0 prompt through the same door a token action's does (services/rollRequests.ts, lifted out of the socket handler), and the answer runs the function again with the roll in hand. A throwing function leaves the effect running and files a timed-effect-failed Message once.
  • Action API 0.6.0 (T.6) — api.timeline.list / createEffect / editEffect / endEffect through a new mapContext.sendJson; url defaults to the invoking action's document. Distinct colours (T.7) — the hue furthest from the running effects', behind the create default and GET /effects/next-color.

The strip

  • Layout (T.8) — layoutStore.timelineStrip (open / collapsed / auto: open while initiative runs or an effect exists, a 4px rail otherwise, until the user chooses), a fixed panel at the map's right edge left of the chat, the bottom bars clearing it.
  • The axis (T.9) — rows per initiative count with round headers, the now-line the scroll keeps at 15% and snaps back to on every tick, the recent past fading above it. Tokens (T.10) at their initiative in every round drawn, the current turn ringed; click selects and focuses, shift-click impersonates, the GM drags to reorder. Slices (T.11) stacking right-to-left, the hover card with what is left and the source, Edit / End now for the one who started it or the GM. The + (T.12) — the timed-effect dialogue (every field, click-to-set-start, rounds / until ended / until a function) and Add to initiative.
  • Mobile (T.13) — the same axis as a drawer of the Map tab, taking Palette's slot while initiative runs (M-D1 revisited).

Finishing

  • Buffs meet TimedEffects (T.14) — BuffDuration gains type: 'effect' with effectId; buffDurationToEffect (rounds by the unit, concentration and save-ends as functions); api.timeline.startBuff so a buff an action creates gets its slice. The evaluator of effects[] is still the roadmap's.
  • Guards and help (T.15) — maps.timeline written by writeTimeline alone; the retired relays stay retired; the help page.
  • Verification — harness suites for the timeline, the advance, the effects, the tick (a sandboxed save-to-end asked and answered, a throw reported once), the palette and the Action API; contract tests for the strip's frame and axis, the mobile slot, the store as a cache; the full gate green on every commit. Verified live on the dev map: Roll Initiative persisted to the map and a reload read it back, Next turn moved the map's turn, an external PATCH ending initiative switched the open tab, and the strip drew three rounds, the now-line at Dorb's 18, the portraits, and Bless and Mage Armor as two lanes.

Traps met on the way

  • The script sandbox is synchronous by design; a duration function cannot await. The ask is a pause and a re-run, and the docs say so.
  • versioned(entity, reason) reads a function reason after the handler: res.locals is how a route names a commit it decided during the request.
  • A new grant entity type touches seven places (the schema union, the loader, three spots in the permissions routes, the message subject union, the client's union) and a pin in the permissions editor's test.
  • The Style Guide's no-forced-uppercase guard reads every CSS module; the round labels lost their text-transform.
  • The Chrome dev tab is hidden: Mantine menus and popovers never open there. The axis, the tokens and the slices were verified through the DOM; the menus and modals through their contracts.