All seven callings are now named; no [CANON-TBD] remains in the specs.
Reaver (Barbarian). To reave is to raid — occupational, like Sellsword and
Trapper, naming what he does for money rather than what he is inside. That is
the rule the five mock names already obeyed and that §8's own list broke: a
calling is what a village calls you, not what a rulebook calls you. Bearsark
and Wildman were rejected for naming a condition; Breaker for being generic.
Bloodsworn (Warlock) was not a new coinage — the cosmology minted it a day
after the races spec was written ("what the Margreave calls bloodsworn",
rule.blood-price), and the cosmology spec had already applied it. It is the
exact FTH-axis mirror of Bonesetter: one channels the Warden, one pacts a king
of the Seven. Accepted cost, recorded in the spec: alone among the callings it
names a condition rather than a trade — the villain who pacts Ghaul is
bloodsworn too. A second, occupational word (Cursewright, Hexman) was
considered and rejected as a distinction the player will never feel.
Also retires the stale "reconcile class names" note on M4 and records the two
contract migrations that milestone owes: the canon log's `race` field, and the
deserter origin's dead allowed_classes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
22 KiB
Roadmap
Source of truth for scope is CLAUDE.md. This tracks sequencing.
The game — building the tactical CRPG (charter §17)
The POC's one question — does bounded AI dialogue feel alive? — is answered. Bounded NPC dialogue, validated moves, and the DM loop are built and live-proven (M0–M2). We are now building the game the mockups describe (§16,
/mockups): a tactical CRPG across 11 screens.
The engine is done and proven. What's left is the game on top of it — recreated screen by screen against the mockups' shared visual system, on the canon log / DM–NPC pipeline / bounded-moves / Luck / character-creation model we already built.
Reconcile as we build (charter §17)
- Damage determinism — §7/§10 (deterministic) vs the Combat HUD mock's damage ranges → combat brainstorm (M5).
- Class names — §8 names vs the mock's callings (same archetypes) → character-creation brainstorm (M4).
Guarded direction — the seven-campaign saga (spec)
One character, seven campaigns (one per demon king), one saga — then it ends. The character, companions, deeds and level carry between campaigns; the world does not. Difficulty is chosen once, at world-construction time, from the campaign index (tier 1..7) — never during play.
The spec adds no milestone and no content. It exists because M5 and M9 are about to be built blank and would foreclose the shape. What it constrains is marked ⛨ saga on the milestones below. World generation itself stays v2 — the spec only guarantees the Saga is the object that will feed it.
Out of scope (never / later)
Multiplayer (never) · mobile (later) · AI-generated story skeletons (v2) · credits/payments/auth (retrofit onto the proxy, §4).
Sequencing
Status: ✅ done · ▶ next · ○ planned. Each item is its own spec → plan → implementation cycle. Each carries its §2 side (state = code owns it · text = AI owns it · both = it is two systems), what it depends on, and the milestone goal it advances.
M0–M2 built the engine (the contract, the client, the server loop, bounded dialogue) and answered the aliveness question. M3+ build the game — the CRPG screens/systems of the mockups, sequenced toward a playable vertical slice first, then breadth:
| Milestone | Goal it advances |
|---|---|
| M0 — Foundations ✅ | The contract and the client-side engine exist and are trustworthy. |
| M1 — Prove the server loop ✅ | A real model sits behind /dm/narrate and the pipeline is reusable. |
| M2 — Prove aliveness ✅ | Does bounded AI dialogue feel alive? — answered; bounded NPC dialogue + DM loop live-proven. |
| M3 — Visual foundation & shell ✅ | The Theme + the exploration HUD; every screen inherits the look and lives in the shell. |
| M4 — Character creation ◀ here | The player enters the world; the proven stat/Luck model gets its UI. |
| M5 — Tactical combat (gridless) | Real stakes: the Combat HUD turn manager. |
| M6 — Dialogue screen | The proven NPC free-text loop inside the mock's conversation UI. |
| M7 — Inventory & character sheet | State the player can see and manage. |
| M8 — World systems | World-map travel, a shop economy, the quest log. |
| M9 — Framing, AI-flavor & persistence | Title/pause, save/load, and the Improviser/Banter/Luck-drift roles + fallback sweep. |
Landed out of band
Not every item is a milestone. These shipped on their own because a later milestone would have been more expensive to build on top of the old shape.
- ✅ Currency — copper with real denominations (spec).
1g = 100s = 10,000c, stored entirely in copper as oneGameState.purse_copperint, formatted only at the display edge (◈ 3s 47c).coin.json→copper/silver/gold. Money is not an inventory item:give_item/accept_itemcarry no quantity andgifts_givenis a global one-shot, so a lonecopperid would have been givable once, for one copper, per campaign — the denominations are move tokens instead (give_item(gold)= +10,000c), routed to the purse and never intoinventory, so money cannot surface in an inventory grid by construction. Taken before M7 whilecontent/world/items/held two files. Prices remain M8's content BOM — nothing is priced yet. §2: state · goal: money that means something. - ✅ A move applies at most once per reply — found by the review of the above.
TagExtractorreturns every tag occurrence andMoveValidatoris a membership test, so a model repeating a tag N times picked an amount in unary (§2 breach: the AI choosing a number). Now deduped by (name, id) across a reply, andadjust_disposition'sMAX_DELTAcaps the reply's net swing rather than each tag — it was per-tag, so three+15s moved standing by 45, the exact "wholesale swing" the constant forbids. §2: state · goal: the bounded vocabulary stays bounded.
M0 — Foundations ✅
- ✅ Canon log + origin JSON-Schema contract — api validation, unified 422 envelope (Plan A). §2: state · foundation.
- ✅ Client canon log engine — typed GDScript model (invariants in the model), new-game construction (origin + world + creation → canon log), turn-to-turn maintenance mutators, pure
[FACT]/[MOVE]/[ADJUST_DISPOSITION]tag extractor (Plan B). §2: state · depends on the contract.
M1 — Prove the server loop ✅
- ✅ Narrator / Ollama pipeline (server) — the shared server-side model-call pipeline (Ollama httpx client, role→model routing config, prompt loader + canon-log digest renderer, §10 call logging, single-retry → typed 502) wired through
/dm/narrate. Non-streaming. Both phases landed: (1) pipeline foundation (config/routing/prompts/ollama_client/call_log), (2) Narrator online (authorednarrator.mdbody +narrateservice + gated live smoke). Server returns raw prose; the client extracts tags. The other four roles become thin drop-ins on this pipeline. Live wire proven against qwen3.5; 49 tests. Merged todev(ddc00d4). §2: text · depends on the contract.
M2 — Prove aliveness ✅ (the POC's one question — build complete + live-proven; the feel is confirmed in playtest)
- ✅ Client HTTP loop — client posts its canon log to
/dm/narrate, displays the prose, harvests[FACT]via itsTagExtractorinto its own log, and shows the degraded-DM authored fallback (§13) on any non-200. First end-to-end vertical slice: state → HTTP → model → prose → screen → fact-harvest. PureDmServicecore behind an injectable transport seam; throwaway harness scene for the first on-screen prose. Live-proven against qwen3.5 through Docker (grounded scene prose, no numbers per §7). 67/67 client tests; merged todev(b949cd2). Surfaced + fixed two Docker-dev gaps in M1 (prompts not packaged, no container→host-Ollama route) — M1's live proof had been venv-on-host only. §2: state (client owns the loop, consumes text) · goal: put the Narrator on screen. - ✅ Bounded NPC conversation —
/npc/speakwith server-owned persona +knowledge[](spoilers stay off the client) and client-computedavailable_moves[]; the client validates each emitted move against live state, applies the valid, silently drops the invalid, and always keeps the prose (§6). Server role is a thin drop-in on the M1 pipeline (npc.runmirrorsnarrate.py); the client added a pureMoveValidator(membership is the whole test) + aMoveApplierthat writes town-NPC disposition toGameState.npc_dispositions(not the party log), plusNpcContent.available_movesas the single legality home. Free text goes straight to the NPC prompt (no Adjudicator needed — the moves are the NPC's). Live-proven against qwen3.5: Fenn answered in-voice, grounded strictly in his authored knowledge, no Luck leak (§7), withreveal/offer_questmoves landing. The live smoke also caught two real tag-format drifts a unit test can't — paren-less zero-arg moves and a droppedMOVE:prefix leaking into prose — both fixed by makingTagExtractortolerant/scrub-safe (backward-compatible; narrator path unaffected). Built onfeature/npc-conversation; awaiting the human's--no-ffmerge todev(§18). §2: both (text = the NPC's voice · state = the validated moves) · goal: answer the one question. - ✅ Considering-state + transport timeout — while a model call is in flight the client rotates authored in-character "the Master considers…" lines (§13 spirit — even loading is content;
ConsideringPhrases+ConsideringIndicator, 3.0s cadence) instead of a frozen surface, andHttpTransportsets a request timeout (35s, rejects ≤0) so a hung call degrades to the authored fallback instead of spinning forever. Meets §14's anti-freeze goal by reframing the wait, on the proven non-streaming loop, with no new failure surface. Human-confirmed live including the degrade path (API down → fallback). Merged todev(099714d); 110/110 client tests. §2: text/UI · goal: the wait is framed (in-character) and bounded (never hangs). Streaming (§14)— SHELVED for the POC. Real token streaming would genuinely hide the ~2s call, but it is the POC's largest transport rework (clientHTTPRequest→ poll-basedHTTPClient) and adds mid-stream error/tag-boundary failure surface not worth the risk. The considering-state above reframes the wait instead and is tested and live. Design kept as a record (specs/2026-07-10-narrator-streaming-design.md, banner SHELVED); revive only if playtest proves the wait genuinely hurts. Conscious deviation from §14's "stream everything," recorded per §18.
Each screen is recreated as a Godot 4.7 Control-node scene against the mockups (§16); each .dc.html's Component class is the behavior spec. Sequenced toward a playable vertical slice (shell → creation → combat → dialogue) first, then breadth.
M3 — Visual foundation & shell ✅
- ✅ Shared
Theme— palette tokens, the three font families, and reusable styleboxes (parchment card, dark panel, primary CTA, tab, item tile, tag/chip) from the mock README as a GodotThemeresource. Every screen pulls from it; build it first so the look is one system. Merged todev. §2: n/a (presentation) · depends on nothing · goal: one consistent visual identity. - ✅ Main Window shell (2a) — the exploration HUD: isometric world-view slot + the permanent DM "narration book" (wired to the proven
/dm/narrateloop + considering-state), the turn-order rail, minimap slot, the slide-in system dock (Inventory/Character/Quest/Map/Party/Spellbook toggles), and the bottom command bar (HP/MP/gold, consumables, End Turn). The frame everything else lives in. Established the editor-first UI-scene convention (ADR 0001; how-to inclient/docs/README.md) and added Button-baseDockButton/ParchmentButton+ a baseRichTextLabelprose style to the theme. §2: state (client owns the HUD, consumes DM text) · depends on the DM loop (M2) · goal: the game's main screen, on screen. - ○ Title screen — new game / continue / load / settings; the entry point. Author it editor-first (ADR 0001). ⛨ saga: a fourth entry path (begin the next campaign of an existing saga) lands later — don't build it, just don't hardcode the flow as
new game → creation → worldso it can't be added. §2: n/a · depends on the Theme · goal: first impression, into the world.
M4 — Character creation
Content it consumes → BOM: calling blurbs + the Bloodsworn's patron choice (fixed); origins (variable). ~4 pieces.
- ○ Character creation UI — the mock's screen over our proven model: race + calling cards, six stats with LCK hidden entirely (§7), 3d6 floors, the +3 additive-only spend-up pool (§8, matches the
character-creation-statsmodel), live origin blurb + nameplate. The class-name reconcile is settled — all seven callings are named (Sellsword · Reaver · Cutpurse · Trapper · Hedge-Mage · Bonesetter · Bloodsworn; races/classes spec §4). Feeds new-game canon-log construction (already built). Two contract migrations land here, not silently: the canon log gains aracefield (the Narrator needs it to describe the player), andcontent/origins/deserter.jsonstill carries the deadallowed_classes: ["sellsword","assassin","priest"]. ⛨ saga:NewGame.constructtakescreationas a plainDictionary— keep it that way. The creation scene produces that dict but must not become the only thing that can (a saga synthesizes it and skips the UI). Emitrace_id+calling_idinto it. §2: state · depends on the creation model (M0) + Theme · goal: build a character.
M5 — Tactical combat (gridless)
Content it consumes → BOM: abilities for the 3 POC classes (fixed); 1 enemy family (4 units) + boss, tier 1 (variable per tier). ~15 pieces.
- ○ Combat turn manager + HUD — the Combat HUD
Componentstate machine as GDScript: initiative order, the action economy (Action/Bonus/Move), ability resolution, click-to-target, victory/defeat; tokens on the iso board as flavor only, no movement grid (§10). Seeded per encounter (§10). Reconcile damage determinism here (§7/§10 vs the mock's ranges). Combat flavor is async optional Narrator calls (reuses M1). ⛨ saga — the level curve is no longer open: design it to a ceiling of L20 across 7 tiers (tier N ≈ levels 3N−2 … 3N; exact bands tunable, the ceiling is not). Enemy stat blocks carrytier: 1..7; there is no scaling multiplier, and there never will be — a tier-6 world has different monsters (proximity to a king is the power scale), not a rat with 40× HP. Class abilities are planned to L20. The M5 content BOM (1 enemy family + boss) is tier 1. §2: state (flavor text a thin AI layer) · depends on the pipeline for flavor · goal: real stakes.
M6 — Dialogue screen
Content it consumes → BOM: 6 NPCs — persona + knowledge[] + gated topics (§6), variable. ~6 pieces.
- ○ Dialogue UI + reshaped Adjudicator — wrap the proven NPC free-text loop (
/npc/speak, M2) in the mock's conversation screen: NPC portrait, live disposition meter, the NPC's spoken line + DM narration beat, and the hybrid input (choice chips with skill/approach tags plus the free-text line, §15). The Adjudicator (/dm/adjudicate, strict JSON §12, one-retry) lands here as the interpreter for the free-text "describe your own action" escape hatch — most turns use buttons. §2: both (text = NPC voice · state = validated moves/actions) · depends on the NPC role (M2) · goal: conversation inside the game's UI.
M7 — Inventory & character sheet
Content it consumes → BOM: 6 weapons, 4 armour, 3 consumables, 1 cursed item (+STR / −LCK — §7 says day-one). All fixed. ~14 pieces. Prerequisite: the build tool gains item + quest namespaces (and tier: 1..7).
- ○ Inventory & equipment — the paper-doll (12 slots) + Satchel grid, category tabs, item detail, encumbrance, derived stats; item seed-data from the mock's
items()map asResources. §2: state · depends on the Theme + item model · goal: gear the player manages. - ○ Character sheet — attributes (modifier-forward, LCK still hidden), saving throws, the "in a fight" derived grid, skills, talents, afflictions/conditions, the DM's-notes bio. §2: state (display) · depends on the stat model · goal: the character, legible.
M8 — World systems
Content it consumes → BOM: the price table in copper (fixed); 3 map locations + the road, shop stock, the quest chain (variable). ~8 pieces. The currency value type already landed (see Landed out of band, above) — everything is stored in copper and formatted at the display edge, so the Shop economy inherits a real purse. What M8 still owes is the numbers: nothing is priced yet.
- ○ World-map travel — region map, location markers (settlement/dungeon/fog), roads + planned route, danger ratings, travel time, set-out. §2: state · depends on the shell · goal: move between places.
- ○ Shop economy — buy/sell with a fence: purses, stock/owned, the ~40%-sell loop, gritty disabled-CTA reasons, merchant reaction lines. §2: state (merchant reaction = a thin authored/AI layer) · depends on inventory · goal: an economy loop.
- ○ Quest log — active/completed/failed tabs, objectives checklists, rewards, tracked quest; reads the canon log's
active_quests(§11). §2: state · depends on the quest model · goal: track the story.
M9 — Framing, AI-flavor & persistence
Content it consumes → BOM: authored fallback per AI surface (§13); shrine + Luck-drift events; humiliation seeds. All fixed. ~10 pieces.
- ○ Save / load + pause / settings — the pause overlay + data-driven settings; save/load over the canon log + game state (seeds preserved, §10). ⛨ saga — the save is two objects, not one: a durable
Saga(sheet, level, companions + approval, humiliations, deeds,kings_slain,campaigns_completed, signature item) and a per-campaignCampaign(CanonLog+GameState+ world + seed). Humiliations move to theSagaand are projected into the canon log — today they live in the log and would die with the campaign. This is the §10 seed argument applied to persistence: cheap now, a rewrite once saves exist in the wild. §2: state · depends on the systems existing · goal: persistence + framing. - ○ Luck fully wired — generation exists (Plan B); this adds drift events (real bed, shrine blessing, killing something praying), one shrine, one cursed item (+STR / −LCK). Visible only in prose (§7). Gates the Improviser. §2: state · depends on the item + event systems · goal: the signature system, live.
- ○ Improviser —
/dm/improvise: minor off-script events, §7-sandboxed by construction — a validator that only applies whitelisted state changes (dignity, never progress; structurally incapable of ending a quest). Fires on Luck; hooks Cadwyn. §2: both · depends on Luck being wired · goal: bad luck as a story, not a fine. - ○ Banter —
/party/banter: companions comment on recent events; low stakes, high charm, aggressively cacheable; reads Brannoc's humiliation log (§9), stacking not overwriting. Prose only, no tags. §2: text · depends on the humiliation log (M0) + events · goal: the comedy loop closes. - ○ Authored-fallback sweep — every AI-dependent surface has authored fallback content (§13), written as writing; the final pass so no surface can show the player an error. §2: text · depends on every surface existing · goal: a degraded DM still has a voice.
Deferred (later / never)
Replicate provider (prod) · auth · metering · credits/payments (§4) — retrofit onto the pipeline later, the game doesn't notice. AI-generated story skeletons (v2 — the seeded story is authored for now). Streaming (§14) — SHELVED (specs/2026-07-10-narrator-streaming-design.md); the considering-state reframes the wait instead.
NPC & relationship depth (v2)
Three ideas mined from NarrativeEngine-P (MIT) — design borrowed, not code. All three are §2: state (code owns them; the AI only voices from what code decides), so they strengthen the §2 boundary rather than loosen it. They fold into the NPC role (§6) and canon log (§11); sequence them with or after the Dialogue screen (M6), since they enrich the same conversation surface.
- ○ Directional dispositions — split the single per-pair disposition into two directed fields (A→B and B→A), so liking is asymmetric. Unrequited regard, undisclosed betrayal (player still trusts the captain who already sold them out), and the Cadwyn↔Brannoc mutual-but-differently-flavored dislike (§9) all become expressible. Player→NPC gates dialogue tone; NPC→player gates offered moves (§6
available_moves) — two arrows, two levers. Extendsparty[]in the canon log (§11) andnpc_dispositionson the client; keep the −100..100 range, just double the field. §2: state · depends on the disposition model existing (M2, done) · goal: relationships that aren't symmetric. - ○ Witness tracking — every logged event carries a
witnesses: [...]list; a fact enters an NPC'sknowledge[](§6) only if they witnessed it or a witness told them. This is the missing input-side rule for the knowledge list — CLAUDE.md §6 says the list is "the entire content the NPC has to draw on" but never says how a fact gets in. Structurally prevents the blacksmith spoiling a twist he never saw, gives shady acts real "were you seen?" stakes without a stealth system, and turns rumor spread into a code operation (witness meets NPC → code copies the fact, optionally degraded) rather than AI invention. Adds a field torecent_events[]/established_facts[](§11) and a propagation rule on character encounters. §2: state · depends on the canon log event model (M0, done) + knowledge lists (§6) · goal: NPCs know only what they could plausibly know. - ○ Personality Hexagon persona — a richer NPC persona schema: six axes (Drive, Diligence, Boldness, Warmth, Empathy, Composure) supplied to the NPC prompt (§6) as structured voice-shaping input, replacing free-form persona blurbs. Server-owned (like
knowledge[]), so it never ships to the client. Purely additive to the §6 prompt contract — it steers how an NPC speaks, not what it may do (the move vocabulary is unchanged). §2: state (the schema) feeding text (the voice) · depends on the NPC role (M2, done) · goal: consistent, distinct NPC voices from data, not prose.