diff --git a/plans/0000-roadmap.md b/plans/0000-roadmap.md --- a/plans/0000-roadmap.md +++ b/plans/0000-roadmap.md @@ -121,6 +121,7 @@ session's first prompt carries the session brief and the replayed tail of the transcript, so the DM comes back remembering the story and not just the clock. *You can now: your character persists across sessions.* + ([0014](0014-persistence.md)) - [ ] **Phase 11: Behind the screen.** The `aside` tool: the DM commits secret canon to the transcript in a fenced block the player never sees. Secrets must be written, because a secret that lives only in @@ -159,8 +160,16 @@ It writes only to its own prep area, never to the notes, so what is merely intended never reads as canon. Deliberately loose; its real design starts after the Archivist has run for a while. *You can now: walk into a plot that was waiting for you.* -- [ ] **Phase 15 and beyond: ideas that have to earn it.** Character - creation, combat and initiative, background world motion, advancement, - name generation, colors and theming (the styles all live in - `markdown::style` waiting for it). Each one starts as a conversation, - not a commitment. +- [ ] **Phase 15: Encounter mode.** The DM gets two full modes: + storytelling, which is everything built so far, and encounter, for + when time slows down to rounds. Each mode has its own timekeeping + rules (the SRD's and homebrew's round scale instead of the campaign + clock's), its own tools, and its own prompts and context. Dedicated + scratch files give the DM somewhere to keep initiative order, HP + pools, and positioning arithmetic tight instead of holding them in + its head. Deliberately loose; the design starts when we get here. + *You can now: fight six goblins and the DM never loses count.* +- [ ] **Phase 16 and beyond: ideas that have to earn it.** Character + creation, background world motion, advancement, name generation, + colors and theming (the styles all live in `markdown::style` waiting + for it). Each one starts as a conversation, not a commitment. diff --git a/plans/0014-persistence.md b/plans/0014-persistence.md new file mode 100644 --- /dev/null +++ b/plans/0014-persistence.md @@ -0,0 +1,201 @@ +# 0014: Persistence + +Phase 10 from [0000](0000-roadmap.md). Worlds become named things you +return to, and a character becomes a file that outlives the session. +The recap and replay work ([0011](completed/0011-session-state.md)) +already lets a session pick the story back up; this phase gives it a +world worth returning to and a character to return as. + +## What we're building + +- `storied play `: open a named world under a worlds root, and + create it after a confirmation when it does not exist yet. +- The character sheet: a directory per character in the world, an + `entry.md` with frontmatter for mechanical state and prose for the + person, and a day-partitioned `log/` beside it. +- The sheet always in the prompt: the engine injects the active + character's sheet into every turn. +- Who is on stage: the engine works it out from the record, and the + `/play` slash command switches among characters. +- The `sheet` tool: the DM's one way to change mechanical state in + play, and every change lands in a log as an event. + +## Named worlds and the worlds root + +Worlds live by name under one root. The root resolves in this order: + +1. a `--worlds-root DIR` flag on the command line +2. the `STORIED_WORLDS` environment variable +3. a `worlds_root` key in the config file +4. `~/.local/share/storied/worlds`, the XDG data home default + +`storied play drowned-coast` opens `/drowned-coast`. When the +directory does not exist, the command asks at the prompt before it +creates anything, because creating a world will one day have a flow of +its own (a wizard, module picks, a session zero). Until then, a yes +creates an empty world and play begins; a no ends the command having +touched nothing. + +`storied sandbox` keeps its temp-dir world and its `--world DIR` +escape hatch, unchanged. The sandbox is for testing the machine; `play` +is for playing. + +## The anatomy of a world + +A named world's directory is the world layer itself. There is no +`world/` subdirectory inside it: `/drowned-coast/characters/`, +`/drowned-coast/people/`, the transcript, and the campaign log +all sit directly under the world's own directory. The sandbox's +`world/` and `player/` pair was a scaffold, and the sandbox keeps it; +named worlds flatten it. + +The player knowledge layer moves out of the world. It is the player's, +not the world's: house rules the player brings, private notes, and +knowledge that maybe isn't. It lives at `~/.local/share/storied/player` +and holds a `worlds/` directory keyed by world name: + +- files at the player root, outside `worlds/`, mount above every world + the player opens: knowledge the player carries everywhere +- `player/worlds//` mounts above that, only when its world is + open: what the player knows, or believes, about that world alone + +This phase creates the directories and mounts them, and otherwise +leaves them alone; what the DM should and should not see of the +player's own notes is a later conversation. + +## The character sheet + +A character owns a directory in the world: + +``` +characters//entry.md +characters//log/0001.md +``` + +The sheet is the character; the log is day-partitioned history, the +same shape as `campaign-log/` and `transcript/`, sharing their +`day_file` plumbing. The DM writes +`[[characters/sister-maren|Sister Maren]]` and the resolver finds the +sheet through a general rule, new in this phase: **an entity address +that names a directory resolves to the `entry.md` inside it.** The +rule is not about characters. Any entity can grow from a file into a +directory, `places/weatherford.md` into `places/weatherford/entry.md` +with sub-entries beside it, and every old wikilink still resolves. +Characters are just the first entities born as directories. The rule +also keeps a character's `log/` out of lookup: a directory +contributes one entry, its `entry.md`, and nothing else inside it. + +The file is technically the DM's, like all canon. The player's +ownership of the character is a matter of write authority, not +location. + +One file, two natures, split at the frontmatter fence: + +- **Frontmatter is the sheet**: HP, hit dice, AC, level, class, + abilities, slots, conditions, inventory. Whatever the engine or a + tool must read back reliably goes here. The engine does not own a 5e + schema; frontmatter is open key-value, and the DM decides what keys + a character needs. Only the keys the engine itself reads (none yet + beyond the sheet's own name) are contracts. +- **Prose is the character**: who she is, what she wants, what she + owes. The Archivist ([0012](0012-the-archivist.md)), when it + exists, tends and cross-links the prose like any other note, and + keeps out of the frontmatter entirely. + +The `sheet` tool and the player's own editor are the frontmatter's +writers. The file is the truth: the engine rereads it every turn, so a +hand edit in vim simply wins. + +The active character's whole file, frontmatter rendered as a stat +block plus the prose, is injected into every turn's prompt. The +attention rule that lets a tavern fall out of the prompt was designed +for scenery; a character's own body stays. This is the first real +weight on the context budget, and carrying it is this phase's job. + +## Who is on stage + +The engine needs to know which sheet to inject. The rules, in order: + +1. One character in `characters/`: that character is on stage. +2. More than one: the campaign log decides. The last stage-taking + event names the active character. +3. More than one, and no stage-taking event in the log (a hand-built + world): the session opens unbound, says so plainly, injects no + sheet, and suggests `/play`. + +There is no `active` flag in frontmatter. Who is on stage is a fact +about the session's history, so it lives in the record. + +`/play ` switches: it writes the stage-taking event to the +campaign log and the next turn carries the new sheet. The sheet tool +writes the same event when it creates a character's file, so the first +character a DM rolls takes the stage without ceremony. + +## The sheet tool + +The DM changes mechanical state through one tool, and every change is +an event. The tool takes the character, a shallow frontmatter patch of +absolute values (`hp: 15`, not `-7`, so a misremembered subtraction +cannot compound), and a required one-line event saying what happened. +The tool: + +1. patches the sheet's frontmatter, +2. appends the event to the character's own log, +3. shows a line in the transcript, the way `mark` shows its ⏰ line, + with the change rendered plainly: `📋 Sister Maren — hp: 22 → 15, + the ghoul's claws find her shoulder`. + +Creation is the same tool with a full sheet: complete frontmatter and +the opening prose. Creation also writes the stage-taking event to the +campaign log, per the section above. + +### The character log + +Every sheet change appends to the character's `log/`, day-partitioned +like the campaign log. Mechanical history reads like a ledger there: +every wound, every spent slot, every level, stamped with the campaign +clock. The campaign log stays narrative: stage-takings go there, and +big story beats keep arriving through `mark` as they already do. Two +logs, split by domain: what happened in the story, and what happened +to the sheet. A long campaign fills many day files, and the ledger +never needs reading whole; the day partition is the same answer the +campaign log already gave to the same problem. + +The alternative was sheet events in the campaign log itself, one +record for everything. It keeps one log, but fifty HP ticks would bury +the story beats that `recall` exists to find. The split keeps both +logs readable for what each is for. + +## What this does not cover + +- **Character creation as a flow.** Rolling a character stays + something the DM does in play, the way the sandbox scenarios do it. + A creation wizard, and a real `storied new`, have to earn their way + in later. +- **The context budget.** The sheet makes the prompt heavier every + turn. The budget that decides what to drop still waits until the + weight is real, likely once the Archivist fattens entity notes. +- **Multi-player.** Multiple characters fall out of this design; + multiple players need per-connection binding and belong to a far + later conversation. +- **The player layer's meaning.** This phase mounts it and leaves it + empty. What the DM sees of it, and whether wrong theories should + shape the DM's narration, is its own design. + +## Implementation order + +- [ ] 0. The worlds root and `storied play `: resolution order, + the confirmation on a missing world, the flattened world anatomy + (no `world/` subdirectory), and the player layer with its + `worlds//` overlay mounting above the world. +- [ ] 1. The sheet as an entity: the general resolver rule (a + directory address resolves to its `entry.md`), the + `characters//` directory shape that uses it, and injection + of the active sheet into every turn's prompt. +- [ ] 2. Who is on stage: the one-file rule, the log-derived rule, + and the unbound session's plain message. +- [ ] 3. The `sheet` tool: create and patch, the required event line, + the character log, the transcript's 📋 line, and the stage-taking + event on creation. +- [ ] 4. `/play`: list characters, switch, write the stage-taking + event, and tab completion alongside the other slash commands.