Files
gmtool/README.md
T
slaguru666andClaude Opus 4.8 e79f643409 feat: native iPad wrapper — Capacitor iOS scaffold
Wrap the PWA in a thin native iOS shell (Capacitor 8):
- capacitor.config.json (webDir dist, appId uk.timevans.thedirector,
  iOS safe-area contentInset + theme background).
- @capacitor/{core,cli,ios} as devDependencies (web app keeps zero
  runtime deps); ios:add / ios:sync / ios:open / ios npm scripts.
- main.js skips service-worker registration under Capacitor
  (window.Capacitor.isNativePlatform) — assets are already local.
- iOS <meta> hints in index.html; .gitignore for native build output.
- docs/native-ipad-wrapper.md: full Xcode + CocoaPods build guide,
  incl. the keep-awake plugin note for the wake toggle in WKWebView.

The iOS project generation/build (npx cap add ios → Xcode) needs a Mac
with full Xcode + CocoaPods, which this env lacks; it is documented for
the user to run. tests/tools/capacitor-config.test.js guards the config.
211 tests pass; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:55:20 +01:00

9.0 KiB
Raw Blame History

The Director

An offline-first tablet app for running tabletop RPG sessions at conventions.

Its permanent feature is the Director Rail — an always-on pacing co-pilot that keeps a hard-clocked slot on time — sitting above a pluggable shell that hosts any game system and any scenario. Built for face-to-face play: no server, no wifi, no install ceremony. Open it on the tablet and it works.

Working name "The Director". Repo gmtool. First deployment: Continuum 2026 — but built as a reusable product for any convention, not one con's materials.

Status

Slice 1 — complete. Proven end-to-end against the AFTERIMAGE pilot scenario:

  • <gm-shell> — root: owns the session clock, scenario, beat stamps, and persistence.
  • Director Rail — session clock, "you are here" vs target with a live drift badge, next hard trigger countdown, and one-tap "Reached it" stamping (no mental arithmetic mid-scene).
  • Dice engine — one seedable core (any die: d4–d100, dF, pools, modifiers) plus pluggable system rule-packs that interpret a roll. Ships with Year-Zero (6–9 = success, 10+ = two).
  • Scenario data model — one structured file per scenario; the source of truth (see below).
  • Persistence — namespaced localStorage with JSON export/import for backup and device swap.
  • Installable PWA — a service worker precaches the shell so it launches offline.

Deferred to later slices: NPC generator, art generator, the rest of the tray tools, more rule-packs and scenarios, and the clue safety-net. See the roadmap below.

Quick start

npm install
npm run dev       # local dev server
npm test          # Vitest suite (jsdom)
npm run build     # static production build → dist/
npm run preview   # serve the production build

Requires Node ≥ 18. No runtime framework — the only dependencies are dev tools (Vite, Vitest, jsdom).

Architecture

Vanilla ES modules and Web Components (light DOM) over a thin core of pure, deterministic logic. All testable logic lives in src/core and src/dice and takes an injected now (ms) or rng (() => [0,1)) — no Date.now() / Math.random() inside — so tests are fully deterministic. Components are dumb renderers that emit events; the shell wires them together.

Unit Responsibility
src/core/store.js Namespaced localStorage (gmd.<namespace>.<key>) + export/import
src/core/clock.js Pure session clock: start / pause / resume / elapsed
src/core/timeline.js Drift + next-hard-trigger analysis from the scenario timeline
src/core/scenario.js Scenario validator
src/core/format.js Elapsed + drift display formatting
src/dice/roller.js Seedable dice core (pools, modifiers)
src/dice/rulepacks/ System interpreters (Year-Zero, CoC d100, BRP d100, VANITY d6, Panic & Glory, Dee Sanction) + registry
src/npc/ NPC generator (generator.js) + genre table packs (packs/noir.js)
src/art/ Pencil-art library search (search.js) + tagged manifest; online generate.js (configurable endpoint) + library.js (persisted, capped generated-art cache)
src/clues/safety-net.js Clue safety-net engine (essential-gap + solvability)
src/con/schedule.js Pure convention-schedule analysis (live / up-next / done / upcoming)
src/components/con-hub.js <con-hub> — the convention landing screen; deep-links into a scenario
src/scenarios/index.js Scenario registry (getScenario(id)) the hub deep-links through
src/con/ Convention data (continuum-2026.js; example-con.js is a fully-timed template)
src/components/director-rail.js <director-rail> — the always-on pacing bar (🏠 🎲 👤 ✏️ 🔍 👥 ☕ chips; ☕ shows an on-break badge)
src/components/dice-tray.js <dice-tray> — pack selector + die picker + roll + verdict
src/components/npc-tray.js · art-tray.js · prop-viewer.js · clue-net.js · cast-tray.js · break-timer.js · parking-lot.js The tray tools (cast-tray = roster w/ tap-to-reveal secrets; break-timer pauses the clock so breaks never poison drift; parking-lot = timestamped thread capture)
src/core/wake-lock.js Screen Wake Lock controller (keep the tablet awake; re-acquires on foreground; graceful no-op where unsupported)
src/components/gm-shell.js <gm-shell> — root wiring + persistence
src/scenarios/ Scenario data files (six Continuum slots: afterimage, day-one, vain-crown, silvery-moon, chopper, princes-bride; example-with-clues.js is a template); index.js is the registry
public/ manifest.webmanifest, sw.js (offline service worker)

Scenario data model

A scenario is one JS module — the single source of truth the Rail reads. The timeline is authored in relative minutes from Start (con slots begin late; relative timing keeps drift honest).

export default {
  meta: { id: 'afterimage', title: 'AFTERIMAGE', system: 'year-zero', players: 4, playMinutes: 210, slot: 'Fri · Slot 2' },
  timeline: [
    // targetMin = minutes after the GM taps Start
    { id: 'a1-open',    label: 'The pier — the case lands', targetMin: 0,   hardTrigger: true },
    { id: 'a1-parlor',  label: 'Parlor 88 interview',       targetMin: 35,  cutHint: 'Summarise the ledger; skip the tea ritual' },
    { id: 'a2-doorcam', label: 'Door-cam reveal',           targetMin: 120, hardTrigger: true },
    // …
  ],
  clues: [], cast: [], props: [],   // reserved for later slices
}

meta.system selects the dice rule-pack. hardTrigger marks immovable beats; cutHint is what to compress if you reach a beat behind schedule.

Generating a scenario from markdown

Instead of hand-writing the module, author a scenario in a structured markdown file and generate it:

npm run gen:scenario -- input.md src/scenarios/my-scenario.js   # or omit the output path to print

The tool (tools/scenario-md.js, wrapped by tools/md-to-scenario.mjs) is a deterministic parser — frontmatter → meta, and ## Timeline / ## Clues / ## Cast lists → the arrays. See tools/example-scenario.md for the full format; in short:

---
id: my-scenario
system: year-zero
players: 4
playMinutes: 210
---
## Timeline
- [0] The opening beat {hard} #a1-open        ← [minutes] label, {hard}, optional #id
- [35] Interview | cut: skip the tea ritual   ← | cut: … → cutHint
## Clues
- The door-cam face {essential} {act: Act One} | fallback: the monologue cards
## Cast
- MARY FLETCHER — Innkeeper; saw the watchers | secret: she saw Crowe sew

IDs are auto-slugged from the label/name when not pinned with #id. The output is validated against validateScenario before it is written. It emits app data only, not prose — freeform con docs are not parsed; you write the structured markdown. The tools/ directory is dev-only (never bundled).

Roadmap

  1. Slice 1 — shell + Director Rail + data model + dice engine (Year-Zero). ✅
  2. Slice 2 — NPC generator (offline genre tables) + art generator (offline pencil-art library). ✅
  3. Slice 3 — dice rule-packs (CoC d100, VANITY d6-pool, Panic & Glory, Dee Sanction) + tray pack selector. ✅
  4. Slice 4 — the clue safety-net (essential-clue gap tracker + fallbacks). ✅
  5. Slice 5 — the convention hub (all slots, live "live now / up next / done", deep-links into each scenario). ✅ All six Continuum 2026 slots ported and scheduled with real times.
  6. Native iPad wrapper — a Capacitor iOS shell around the PWA is scaffolded (config, scripts, integration). Generate + build it on a Mac with Xcode + CocoaPods: see docs/native-ipad-wrapper.md. ✅ (config) / ⬜ (iOS build needs the toolchain)

The full design is now built. All §5 tray tools (dice with 6 packs, NPC, art with online Generate, clue-net, cast, break timer, parking-lot, wake-lock), the con hub, six ported Continuum scenarios, the markdown → scenario-data generator, and the native-wrapper scaffold.

Online art "Generate"

The art tray can generate new pencil art at the table and cache it into the searchable library (offline thereafter). It needs a GM-configured image-gen endpoint — nothing is hardcoded and no key is baked in. Paste your endpoint URL into the tray's ⚙ field (persisted). The endpoint receives POST { prompt } and must reply JSON with an image under image | src | url | dataUrl (a data: URL makes the result offline-cacheable), plus optional label / tags. With no endpoint or no network the Generate button is disabled and the library stays fully searchable — the network is never load-bearing.

Docs


Personal convention-play tooling. System rule-references paraphrase their respective rulebooks for private table use.