Add 3D clamshell phone dialer spike + persistent-board design
A diegetic 90s handset rendered as a small three.js scene in a fixed 1:2 portrait stage (visual spike at /?phone=1, three.js lazy-loaded so the board bundle is unchanged). Blocky flat-shaded clamshell with glowing keys, a flip-open animation with an intro camera orbit that settles head-on before the DOM screen appears, a hinge barrel on the pivot axis, and a chubby antenna. Pressable 3D keypad (raycast + keyboard) with DTMF tones drives a dialer: connect / voicemail / SIT "unobtainable", against a stub number->node directory with flag-gated node enablement. The screen UI is real DOM positioned in percent of the stage, kept pixel-exact by the head-on ortho camera. Also documents the persistent-board flow model (docs/persistent-boards.md): board_key reuse across level nodes, present-but-hidden flag-gated exhibits with live arrival reveals, reset/new-game semantics, and A/B/M citation codes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# Persistent boards, gated exhibits, and citation codes
|
||||
|
||||
Status: **proposed** (design locked, not implemented). Extends the story flow graph
|
||||
([story-graph.md](story-graph.md)) and the narrative layer
|
||||
([narrative-todo.md](narrative-todo.md)); interlocks with the Claim/Case Report
|
||||
work in [TODO.md](TODO.md) Milestone 5. Depends on the **flags / case-state**
|
||||
primitive (a per-playthrough key→value store) that the gates and dialogue effects
|
||||
also read and write.
|
||||
|
||||
## Why
|
||||
|
||||
A mystery is broken into short pieces so we can **meter how much evidence the
|
||||
player carries at once**. Interruptions — a phone call, a knock, a revealed note —
|
||||
should not throw the player off the board they are working on. So a single
|
||||
investigation scene is a **persistent, player-mutated board** that several graph
|
||||
nodes return to, and new evidence **arrives into that same board** when the story
|
||||
warrants it rather than being dumped up front or split across duplicated templates.
|
||||
|
||||
Two capabilities, independent and both wanted:
|
||||
|
||||
1. **A board that survives across nodes** — `[board] → [phone call] → [same board]`
|
||||
returns the player to their exact arrangement and connections.
|
||||
2. **Flag-gated exhibits inside that board**, revealed with a diegetic "it arrived"
|
||||
moment (reuses the existing document locator beam).
|
||||
|
||||
## Board identity and reuse
|
||||
|
||||
An authored level node **cannot** reference a clone id — the clone is created per
|
||||
playthrough at runtime. Instead:
|
||||
|
||||
- A **level node** references a `level_template_version_id` **plus a `board_key`**
|
||||
(a logical board slot, e.g. `"harbor-desk"`). `board_key` defaults to the
|
||||
template if the author doesn't care about sharing.
|
||||
- A **playthrough** holds a map `board_key → level_id` (the mutable clone).
|
||||
- Entering a level node:
|
||||
- if the playthrough already has a clone for that `board_key` → **reuse it**
|
||||
(the player's mutated board, arrangement and connections intact);
|
||||
- otherwise → **clone the template** and record `board_key → new level_id`.
|
||||
|
||||
`board_key` is the thing that makes two nodes share a board. Two nodes on the same
|
||||
template with **different** keys get **separate** boards.
|
||||
|
||||
## Gated exhibits
|
||||
|
||||
- Every exhibit is **cloned in**, including gated ones; a gated exhibit carries a
|
||||
**`reveal` condition over flags**. The player's saved board already contains the
|
||||
note — it is simply inert until the flag flips.
|
||||
- **Visibility is computed server-side at load.** Given the playthrough's flags,
|
||||
the play-mode payload includes only exhibits whose `reveal` condition is met;
|
||||
hidden exhibits are stripped from the response (same discipline that already
|
||||
hides authoring-only fields like `expectedPartyKind`). Players cannot peek
|
||||
unrevealed evidence in the API.
|
||||
- **Gate the edges too.** Any `exhibit_connection` or event link whose endpoint is
|
||||
a not-yet-revealed exhibit is hidden until that exhibit appears, so there are no
|
||||
dangling references.
|
||||
|
||||
## The arrival reveal
|
||||
|
||||
- The board's effective content can change **while the player is standing on it** —
|
||||
the phone is an always-available surface, so a call can set a flag mid-scene. So
|
||||
reveal is **not load-only**: after any phone/dialogue interaction that sets flags,
|
||||
re-evaluate board visibility and reveal live (the phone overlay refetches
|
||||
visibility on hang-up).
|
||||
- Track a per-playthrough **`revealed-seen` set**. On (re)load or after an
|
||||
interaction, any exhibit now visible but not yet in the set → play the arrival
|
||||
animation (a sibling/reuse of the **document locator beam**), then add it to the
|
||||
set. The flourish fires **once**, not on every reload.
|
||||
|
||||
## Reset and New Game
|
||||
|
||||
- **Reset = tidy the desk back to the delivered state.** Authored exhibits return
|
||||
to their arrival positions and folders; the player's rearrangement is undone.
|
||||
**Flags are untouched**, so anything a flag has already revealed is still
|
||||
"arrived" and is restored at its authored position.
|
||||
- **New Game** re-clones the whole tree and is therefore the only thing that
|
||||
**resets flags** and returns gated exhibits to hidden.
|
||||
|
||||
**Open decision — player notes on reset.** Milestone 5's rule is "reset discards
|
||||
player-created claims." Current lean: reset also removes player-created exhibits
|
||||
(it restores the *delivered* state), and New Game is the only wipe of flags. The
|
||||
alternative is a gentler reset that re-files arrived exhibits but leaves the
|
||||
player's own notes in place. Settle before implementing.
|
||||
|
||||
## Exhibit citation codes (A / B / M)
|
||||
|
||||
A single running number breaks when players create many note exhibits, so codes
|
||||
live in fixed namespaces:
|
||||
|
||||
- **Authored exhibits: group letter + index** — `A1, A2, …` for the initial
|
||||
dossier, `B1, B2, …` for a batch that arrives later via a flag reveal, etc.
|
||||
**The letter is the arrival group**, which ties straight into gated reveal: "new
|
||||
evidence arrived" *is* the B-series lighting up. Indices are assigned **within
|
||||
each group at freeze time** and frozen in the template, so authored codes never
|
||||
shift, no matter what the player does.
|
||||
- **Player-created exhibits: a running `M` (miscellaneous) series** — `M1, M2,
|
||||
M3…` in creation order, in their own namespace so player note-making cannot
|
||||
disturb authored codes. M-numbering is per-level-instance and (given the reset
|
||||
lean above) restarts only when player notes are cleared.
|
||||
- The author chooses an exhibit's **group**; the system owns the **numbering**.
|
||||
|
||||
These codes are the **citation token everywhere** — board, luggage-tag Claims, the
|
||||
Case Report, and the LLM gate.
|
||||
|
||||
## LLM gate contract
|
||||
|
||||
- A gate can be **scripted to require specific authored codes** — e.g. "the report
|
||||
must cite `A1` and `B2`, connected." That authored requirement list is the
|
||||
deterministic backbone *under* the LLM, per the section-9 intent of scripting the
|
||||
cognitive shim rather than trusting it blind.
|
||||
- If the player types the full report freehand, they must include the Exhibit codes;
|
||||
the gate checks the required codes are present (and, later, that the reasoning
|
||||
holds).
|
||||
|
||||
## Relationship to fresh templates
|
||||
|
||||
This **complements**, not replaces, new templates:
|
||||
|
||||
- **Shared `board_key`** — interruptions *within* one investigation scene (calls, a
|
||||
knock, a revealed note). Cleanly subsumes the earlier "note appears on the next
|
||||
level" idea: the note is a gated exhibit on the *same* board that reveals after
|
||||
the call.
|
||||
- **A fresh template** — the player genuinely moves to a new location/chapter with a
|
||||
different board.
|
||||
|
||||
## Implemented vs deferred
|
||||
|
||||
Nothing here is built yet. New plumbing, smallest-first:
|
||||
|
||||
1. **Flags** — the per-playthrough key→value store (shared with gates/dialogue).
|
||||
2. **`board_key` reuse** in level traversal + the playthrough `board_key → level_id`
|
||||
map (clone-or-reuse).
|
||||
3. **`reveal` conditions on exhibits** + server-side play-mode visibility filtering
|
||||
(exhibits and their edges).
|
||||
4. **`revealed-seen` set** + the arrival animation (locator-beam sibling), live
|
||||
after flag changes.
|
||||
5. **A / B / M citation codes** — authored group + frozen index, player M-series;
|
||||
surfaced on the board and threaded into Claims/report/LLM gate.
|
||||
|
||||
Reset (desk-restore, flags-persist) and New Game (full re-clone) semantics as above.
|
||||
Reference in New Issue
Block a user