Files
2026-08-09 13:49:54 +01:00

1065 lines
50 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI-Powered Dungeon Master Toolkit
*A local, offline-first desktop app built with **Tauri v2** + **Rust** that gives you every tool a DM needs — all powered by a local LLM.*
> **TL;DR**
> 1. Scaffold a Tauri v2 project.
> 2. Add Rust bindings to a local LLM (`llama-cpp-2`, `llama_cpp`, or Candle).
> 3. Build modular "utilities" (World Builder, NPC Generator, Encounter Designer, etc.).
> 4. Wire them up in the front-end via `tauri.invoke` + Tauri Channels for streaming.
> 5. Package and ship — no internet required after first run.
---
## Table of Contents
1. [What a DM Needs](#1-what-a-dm-needs)
2. [System Architecture](#2-system-architecture)
3. [Quick Start — Boilerplate](#3-quick-start--boilerplate)
4. [Local LLM Integration](#4-local-llm-integration)
5. [Core Utilities & AI Features](#5-core-utilities--ai-features)
- [4.4 Image Generation via Ollama](#44-image-generation-via-ollama)
6. [Front-End Architecture (React/TS)](#6-front-end-architecture-reactts)
7. [Design System — Visual Language](#7-design-system--visual-language)
8. [UI/UX Feature Design](#8-uiux-feature-design)
9. [Data Persistence](#9-data-persistence)
10. [Model Management UX](#10-model-management-ux)
11. [Lore Search / RAG](#11-lore-search--rag)
12. [Security & Legal Considerations](#12-security--legal-considerations)
13. [Optional Enhancements](#13-optional-enhancements)
14. [Resources & Quick Links](#14-resources--quick-links)
15. [Development Checkpoints](#15-development-checkpoints)
16. [Final Checklist](#16-final-checklist)
---
## 1. What a DM Needs
| Category | Typical Items | AI-Powered Utility |
|----------|---------------|--------------------|
| **World & Setting** | Campaign bible, maps, lore notes | Auto-generate continents, history timelines, religions, backstory snippets |
| **NPCs & Factions** | Sheets for each character, relationships | Create NPC bios, personality traits, dialogue hooks, agenda charts, relationship webs |
| **Plot / Quests** | Story arcs, side quests, beats | Draft story outlines, branching choices, plot twists |
| **Encounters** | Enemy lists, terrain descriptions | Generate balanced combat encounters, environmental modifiers, loot tables |
| **Magic & Items** | Item descriptions, stats, rarity | Forge unique items with lore, backstory, and mechanical effects |
| **Maps** | Tile sets, hand-drawn maps | Render vector/raster terrain, city layouts; fog-of-war overlays |
| **Player Materials** | Character sheets, handouts | Auto-populate sheets from templates; generate handouts/flashcards |
| **Session Management** | Logbook, notes, schedule | Track session logs, note highlights, future hook suggestions |
| **Initiative / Combat Tracker** | Turn order, HP, conditions, round timer | Visual combat tracker with condition tracking |
| **Dice & RNG** | Physical dice | Simulate dice rolls, record outcomes; random table roller |
| **Rule Reference** | PHB, DMG, 5e SRD | Lookup rules (SRD/CC-BY content only), convert stats across editions |
| **Calendar & Weather** | Fantasy calendar, seasonal events | Custom calendar system with weather generation |
| **Soundboard / Ambience** | Audio clips | Local audio playback for tavern, combat, forest ambience |
| **Name Generators** | Fantasy, sci-fi, historical name banks | AI-powered or procedural name generation |
| **Puzzle / Trap Designer** | Skill-check flows, trap DCs, hints | Generate puzzle mechanics, trap stat blocks |
| **Homebrew Importer** | Custom classes, spells, monsters | Import from Markdown/JSON templates |
---
## 2. System Architecture
```
┌────────────────────────────────────┐
│ Front-End (TS / React) │
│ Tauri v2 WebView · Capabilities │
│ UI · Map Viewer · Dialog Editor │
│ Drag-and-Drop · File Explorer │
└───────────────▲────────────────────┘
invoke() / Channel
┌───────────────┴────────────────────┐
│ Rust Back-End (Tauri API) │
│ Business Logic · LLM Inference │
│ File I/O · Session Storage │
├───────────────▲────────────────────┤
│ │ │
│ llama-cpp-2 │ SQLite / JSON │
│ (local GGUF)│ Storage Layer │
└───────────────┴────────────────────┘
```
### Key Points
- **Offline-first**: Core LLM runs locally via GGUF. Remote API is optional for higher-capacity models.
- **Tauri v2 security**: UI runs in a sandboxed WebView; all file/system access gated by capability permissions.
- **Plug-in-style utilities**: Each tool is a separate feature flag — start with "World Builder", add "Encounter Designer" later.
- **Recommended Tauri plugins**:
| Plugin | Purpose |
|--------|---------|
| `tauri-plugin-store` | User settings, UI state, recent campaigns |
| `tauri-plugin-fs` | Read/write campaign JSON/Markdown files |
| `tauri-plugin-dialog` | Open/save dialogs for campaigns and maps |
| `tauri-plugin-sql` | SQLite for indexed NPC/location/loot lookup |
| `tauri-plugin-log` | Structured logs from Rust and TS |
| `tauri-plugin-updater` | Auto-updates for app and model packs |
| `tauri-plugin-single-instance` | Prevent multiple app windows |
| `tauri-plugin-window-state` | Remember window size/position |
### Tauri v2 Capabilities
Create `src-tauri/capabilities/default.json`:
```json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Main DM toolkit capability",
"windows": ["main"],
"permissions": [
"core:default",
"store:default",
"fs:default",
"dialog:default",
"shell:allow-open",
"http:default"
]
}
```
For scoped file access:
```json
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$APPDATA/dm-toolkit/**" }]
}
```
---
## 3. Quick Start — Boilerplate
### Prerequisites
```bash
# Rust & Cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
# Node (for front-end tooling)
brew install node # macOS / Linux; or use nvm
# Tauri v2 CLI
npm install -D @tauri-apps/cli@latest
```
### Scaffold
```bash
npm create tauri-app@latest dm-toolkit -- --template react-ts
cd dm-toolkit
npm install
npm run tauri dev
```
> **Note**: Tauri v2 scaffolds both `src-tauri/src/lib.rs` and `src-tauri/src/main.rs` (mobile/desktop share the lib). Import commands from `@tauri-apps/api/core`, not the old v1 path.
### Add Dependencies
```bash
# LLM binding — pick one:
cargo add llama-cpp-2 # mature, tracks upstream llama.cpp
# cargo add llama_cpp # higher-level, easier to start
# cargo add candle-core # pure-Rust alternative (no C++ build deps)
# Remote API fallback
cargo add reqwest
# Storage
cargo add serde_json
cargo add sqlx # or use tauri-plugin-sql
```
---
## 4. Local LLM Integration
### 4.1 Using `llama-cpp-2` (Recommended)
`llama-cpp-2` is a mature Rust binding that tracks the upstream llama.cpp project. The real API requires: init backend → load GGUF → create context → tokenize prompt → `decode()` batch → sampling loop → decode token-by-token.
```rust
// src-tauri/src/llm.rs
use std::path::PathBuf;
use std::sync::Arc;
use llama_cpp_2::llama_backend::LlamaBackend;
use llama_cpp_2::model::LlamaModel;
use llama_cpp_2::model::params::LlamaModelParams;
use llama_cpp_2::context::params::LlamaContextParams;
pub struct LlmState {
model: Arc<LlamaModel>,
backend: LlamaBackend,
}
impl LlmState {
pub fn new(model_path: &str) -> anyhow::Result<Self> {
let backend = LlamaBackend::init()?;
let model = LlamaModel::load_from_file(
&backend,
model_path,
&LlamaModelParams::default(),
)?;
Ok(Self { model: Arc::new(model), backend })
}
pub fn generate(&self, prompt: &str, max_tokens: usize, temperature: f32) -> anyhow::Result<String> {
let mut ctx = self.model.new_context(&self.backend, LlamaContextParams::default())?;
let tokens = self.model.str_to_token(prompt, llama_cpp_2::model::AddBos::Always)?;
// ... decode batch, sampling loop, collect output tokens ...
// (Full implementation in llama-cpp-2 examples)
todo!("Implement sampling loop")
}
}
```
> **Tip** — Keep model files in `$APPDATA/dm-toolkit/models/`. On first run, ship a small base model and let the user download heavier ones later.
### 4.2 Streaming Tokens via Tauri Channels
`invoke()` returns a single promise, **not** a stream. For streaming LLM tokens, use **Tauri Channels** (`Channel<T>`) or the **event system** (`app.emit()` + frontend `listen()`).
#### Rust — Streaming Command
```rust
use tauri::ipc::Channel;
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(tag = "type", content = "payload", rename_all = "camelCase")]
enum LlmEvent {
Token(String),
Error(String),
Done,
}
#[tauri::command]
async fn generate_stream(
state: tauri::State<'_, LlmState>,
prompt: String,
max_tokens: i32,
channel: Channel<LlmEvent>,
) -> Result<(), String> {
tauri::async_runtime::spawn(async move {
// Init backend, load model, create context, tokenize...
// For each decoded token:
// channel.send(LlmEvent::Token(piece)).ok();
// When done:
// channel.send(LlmEvent::Done).ok();
});
Ok(())
}
```
#### Front-End — Consuming the Stream
```tsx
import { invoke, Channel } from "@tauri-apps/api/core";
async function streamGenerate(prompt: string) {
const onToken = new Channel<LlmEvent>();
onToken.onmessage = (event) => {
switch (event.type) {
case "Token": appendText(event.payload); break;
case "Error": console.error(event.payload); break;
case "Done": setStreaming(false); break;
}
};
await invoke("generate_stream", { prompt, maxTokens: 512, channel: onToken });
}
```
### 4.3 Optional Remote API Fallback
```rust
// src-tauri/src/api.rs
use reqwest::Client;
use serde_json::json;
pub async fn query_openai(prompt: &str) -> anyhow::Result<String> {
let client = Client::new();
let res = client.post("https://api.openai.com/v1/chat/completions")
.bearer_auth(std::env::var("OPENAI_API_KEY")?)
.json(&json!({
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": prompt }],
"max_tokens": 512
}))
.send()
.await?
.json::<serde_json::Value>()
.await?;
Ok(res["choices"][0]["message"]["content"].as_str().unwrap_or("").to_string())
}
```
> **Important**: GPT-4o-mini is a **remote API model**, not local. Keep local inference via GGUF and use remote APIs only as an optional fallback.
### 4.4 Image Generation via Ollama
Ollama's image models (e.g. `x/flux2-klein`, `x/z-image-turbo`) reuse the **same `/api/generate` endpoint** as text — just a different model name and a different response field. No new binding or dependency; the existing Ollama client in `src-tauri/src/llm/mod.rs` already posts here.
**Request** — identical to a text generate, `stream: false`:
```json
{ "model": "x/flux2-klein:4b", "prompt": "portrait of a grizzled dwarf blacksmith, warm forge light, oil-painting fantasy style", "stream": false }
```
**Response** — newline-delimited JSON; the *final* line (`done: true`) carries a **singular `image`** field (base64 PNG). Intermediate lines stream progress (`step`/`total`) for a progress bar. Note: it's `image`, **not** `images`.
```json
{"model":"x/flux2-klein:4b","done":false,"total":4}
{"model":"x/flux2-klein:4b","done":true,"image":"iVBORw0KGgoAAAANSUhEUgAA..."}
```
**Rust command** (lazy: reuse the existing Ollama POST, branch on `image` vs `response`):
```rust
// src-tauri/src/commands/image.rs
#[derive(Deserialize)]
pub struct ImageRequest { pub prompt: String, pub model: Option<String> }
#[derive(Deserialize)] struct OllamaImgResp { done: bool, image: Option<String> }
#[tauri::command]
pub async fn generate_image(
state: tauri::State<'_, AppState>,
req: ImageRequest,
) -> Result<String, String> {
let cfg = state.config.lock().unwrap().clone();
let model = req.model.unwrap_or_else(|| "x/flux2-klein:4b".into());
// POST {model, prompt, stream:false} to {cfg.api_url}/api/generate
// Read NDJSON, take the last line with done:true, return its `image` base64.
todo!("reuse existing ollama POST; return base64 PNG string")
}
```
**Front-end** — base64 PNG drops straight into an `<img src={`data:image/png;base64,${b64}`}>`, and is written to `$APPDATA/dm-toolkit/images/` via `tauri-plugin-fs` so it's reused, not regenerated each render.
**Where it's used** (auto-generated whenever one of these is created, with a ✨ regenerate button):
| Utility | Image prompt template |
|---------|----------------------|
| NPC Generator | `portrait of a {race} {class}, {alignment} demeanor, {descriptor from personality}, fantasy oil-painting` |
| Item Forge | `product shot of a {rarity} {weapon_type}, {magical_effect} glow, on dark velvet, dramatic lighting` |
| World Builder | `fantasy map of {continent_name}, {climate} biome, parchment texture, ink cartography, top-down` |
| Encounter Builder | `battle map tile, {terrain}, isometric, 1024x1024, for tabletop RPG` |
| Loot / Random Tables | item art per rolled result, same template as Item Forge |
| Handout Renderer | scene illustration from the handout's first paragraph |
**Constraints to bake in up front:**
- **macOS-only today** — Ollama image models only run on macOS (Apple Silicon via MLX). Gate the image-gen UI behind an OS check on first run; on other platforms fall back to a placeholder/emoji or the optional remote API. (This is an Ollama limitation, not ours.)
- **Slow + heavy** — 4B is ~5.7GB, 9B is ~12GB, generation is multi-second. Always generate in the background with a progress bar (drive it from the NDJSON `step`/`total` lines), never block the UI thread. Cache results to disk by hash of the prompt.
- **Model picker** — add `image_model` to `LlmConfig` (default `x/flux2-klein:4b`; `x/z-image-turbo` for fast/low-VRAM). Reuse the existing settings panel, don't build a second one.
- **Prompt engineering is the lever** — FLUX.2 handles readable text and hex colors, so item/NPC name labels can be rendered *into* the image where it helps. Default to 1024×1024.
---
## 5. Core Utilities & AI Features
Each module exposes a **Tauri command** (`#[tauri::command]`) that the front-end calls via `invoke`.
| Module | Key AI Tasks | Sample Prompt |
|--------|-------------|---------------|
| **World Builder** | Generate continent names, climate zones, major cities, mythic legends — **+ render a parchment-style map image** | *"Create a fantasy continent with 3 climates and one legendary creature."* |
| **NPC Generator** | Personality traits, background story, NPC goals, relationship webs — **+ render a portrait** | *"Generate a charismatic dwarf blacksmith who despises elves."* |
| **Quest Designer** | Multi-step plot arcs, branching outcomes, hooks | *"Outline a 4-chapter quest where the party seeks the lost crown of Rithor."* |
| **Encounter Builder** | Balanced monster groups, terrain modifiers, loot tables — **+ render a battle-map tile + loot item art** | *"Design an urban ambush for level-5 PCs with 3 bandits and a surprise mob."* |
| **Item Forge** | Stat tables, lore sentences, rarity levels — **+ render item art (label text rendered in-image)** | *"Create a +2 longsword of fire resistance that grants invisibility once per day."* |
| **Initiative Tracker** | Turn order, HP/conditions, round timer | Manage combat state, auto-sort by initiative |
| **Random Tables** | Roll on customizable tables | Treasure, encounters, names, weather |
| **Calendar & Weather** | Fantasy calendar, seasonal events | Custom calendar system with weather |
| **Soundboard** | Local audio clips | Tavern, combat, forest ambience |
| **Map Generator** | Procedural terrain, fog-of-war overlay | Use `react-konva` or `fabric.js` with offline vector tiles |
| **Dice Roller** | 1-to-6 dice + custom pools | *"Roll 4d8+2."* |
| **Session Logger** | Summarize actions, suggest next hooks | *"Summarize last session and propose a hook for the next meeting."* |
| **Puzzle / Trap Designer** | Skill-check flows, trap DCs, hints | Generate puzzle mechanics and trap stat blocks |
| **Name Generator** | Fantasy, sci-fi, historical name banks | Procedural or AI-powered name generation |
| **Homebrew Importer** | Import custom content | Markdown/JSON templates for classes, spells, monsters |
| **Handout Renderer** | Markdown → styled handout → export PNG/PDF | Generate player handouts |
### Example: NPC Generator Command
```rust
// src-tauri/src/commands/npc.rs
use tauri::command;
#[derive(serde::Deserialize)]
pub struct NpcRequest {
pub name: Option<String>,
pub race: String,
pub class: String,
pub alignment: String,
}
#[derive(serde::Serialize)]
pub struct NpcResponse {
pub bio: String,
pub personality: Vec<String>,
pub goals: Vec<String>,
}
#[command]
pub fn generate_npc(req: NpcRequest) -> anyhow::Result<NpcResponse> {
let prompt = format!(
"Create a detailed NPC profile:\n\
Name: {}\nRace: {}\nClass: {}\nAlignment: {}\n\n\
Output in JSON with keys bio, personality (array), goals (array).",
req.name.unwrap_or_else(|| "Unnamed".into()),
req.race, req.class, req.alignment
);
let raw = crate::llm::generate(&prompt, 512)?;
let parsed: NpcResponse = serde_json::from_str(&raw)?;
Ok(parsed)
}
```
---
## 6. Front-End Architecture (React/TS)
### State Management
| Concern | Recommended Tool |
|---------|------------------|
| UI state | **Zustand** or **Jotai** |
| Rust command data | **TanStack Query** (React Query) |
| Type-safe bindings | **`tauri-specta`** — generates TS types from Rust commands |
### Key Libraries
| Need | Library |
|------|---------|
| Map canvas, tokens, fog-of-war | `react-konva` or `fabric.js` |
| Quest / relationship graphs | `react-flow` |
| Markdown editing | `@uiw/react-md-editor` |
| Dice 3D animation | Three.js / `react-three-fiber` (optional) |
| Layout & styling | **`tailwindcss`** + CSS Grid — bento layouts, design tokens |
| Glassmorphism & motion | **`framer-motion`** (hover/press animations), native `backdrop-filter` |
| Icons | **`lucide-react`** — clean, consistent line icons |
| Scrollable panels | **`@radix-ui/react-scroll-area`** — for overflow in bento cards |
---
## 7. Design System — Visual Language
The app should feel like a *magical interface* — a DM's arcane console. The aesthetic is **dark, luminous, and tactile**: deep navy backgrounds with gold accents, glass cards that glow softly, and bento layouts that put every tool within reach without clutter.
### 7.1 Color Palette
| Token | Hex | Usage |
|-------|-----|-------|
| `--bg-deep` | `#0a0e1a` | App background, outer chrome |
| `--bg-surface` | `#111827` | Sidebar, navigation panels |
| `--bg-card` | `#1a2236` | Bento card surfaces |
| `--bg-card-hover` | `#1f2a42` | Hover state on cards |
| `--border-glass` | `rgba(212, 175, 55, 0.12)` | Glass card borders (gold-tinted) |
| `--border-subtle` | `rgba(255, 255, 255, 0.06)` | Dividers, separators |
| `--gold-bright` | `#d4af37` | Primary accent — CTAs, active states, headings |
| `--gold-muted` | `#a8872a` | Secondary accent — secondary buttons, icons |
| `--gold-glow` | `rgba(212, 175, 55, 0.25)` | Glow halos, focus rings, box-shadows |
| `--text-primary` | `#e8e0d0` | Main body text (warm off-white) |
| `--text-secondary` | `#8b9bb4` | Muted text, labels, descriptions |
| `--text-dim` | `#4a5568` | Disabled / placeholder text |
| `--danger` | `#ef4444` | Error, delete, HP-zero |
| `--success` | `#22c55e` | Success, save, HP-full |
| `--info` | `#3b82f6` | Informational highlights |
### 7.2 Glassmorphism
Every bento card uses the same glass treatment:
```css
.glass-card {
background: rgba(26, 34, 54, 0.55); /* --bg-card at 55% opacity */
backdrop-filter: blur(18px) saturate(1.3);
-webkit-backdrop-filter: blur(18px) saturate(1.3);
border: 1px solid rgba(212, 175, 55, 0.12); /* --border-glass */
border-radius: 16px;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.03),
0 2px 8px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
transition: box-shadow 0.2s, border-color 0.2s;
}
.glass-card:hover {
border-color: rgba(212, 175, 55, 0.25); /* --border-glass brightened */
box-shadow:
0 0 20px rgba(212, 175, 55, 0.08), /* --gold-glow halo */
0 4px 16px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
```
**Rules of thumb:**
- Glass cards sit on top of a subtle radial gradient or a dim background image (map, parchment texture). The blur lets the background bleed through just enough to feel *alive*.
- On hover, a soft gold glow appears — like candlelight catching the edge of a scroll.
- Active/selected cards get a brighter gold border (`--gold-bright`) and a stronger glow.
- Never use full opacity backgrounds inside bento cards — the glass effect is the identity.
### 7.3 Bento Grid Layout
The main dashboard is a **CSS Grid bento** — asymmetric tiles that pack related tools together. No sidebars-within-sidebars; everything is a card on the grid.
```
┌──────────────┬─────────┬─────────┐
│ │ NPC │ Dice │
│ World Map │ Sheet │ Roller │
│ (2×2) │ (1×1) │ (1×1) │
│ ├─────────┴─────────┤
│ │ │
├──────────────┤ Session Log │
│ Encounter │ (2×1) │
│ Builder │ │
│ (1×1) ├────────┬────────┤
│ │Initi- │ Random │
├──────────────┤ative │ Tables │
│ Quest │ Tracker│ (1×1) │
│ Designer │ (1×1) │ │
│ (2×1) │ │ │
└──────────────┴────────┴────────┘
```
**Implementation** (Tailwind + CSS Grid):
```tsx
// Each bento card is a <BentoCard> wrapper around the real component.
function BentoCard({
children,
className,
span = "col-span-1 row-span-1",
}: Props) {
return (
<div className={`glass-card p-4 ${span} ${className ?? ""}`}>
{children}
</div>
);
}
// Dashboard grid
<div className="grid grid-cols-4 grid-rows-4 gap-3 p-4 h-screen bg-bg-deep">
<BentoCard span="col-span-2 row-span-2"><WorldMap /></BentoCard>
<BentoCard span="col-span-1 row-span-1"><NpcSheet /></BentoCard>
<BentoCard span="col-span-1 row-span-1"><DiceRoller /></BentoCard>
<BentoCard span="col-span-1 row-span-1"><EncounterBuilder /></BentoCard>
<BentoCard span="col-span-2 row-span-1"><SessionLog /></BentoCard>
<BentoCard span="col-span-2 row-span-1"><QuestDesigner /></BentoCard>
<BentoCard span="col-span-1 row-span-1"><InitiativeTracker /></BentoCard>
<BentoCard span="col-span-1 row-span-1"><RandomTables /></BentoCard>
</div>
```
**Bento rules:**
- Cards are **draggable/resizable** — DMs can reorganize their workspace per session.
- Layout preferences persist to `tauri-plugin-store`.
- On narrow windows (laptop), the grid collapses to a 2-column flow.
- Each card header has a subtle gold underline and a `lucide-react` icon.
### 7.4 Typography
| Element | Font | Weight | Size |
|---------|------|--------|------|
| App title / H1 | *Cinzel* (serif display) | 700 | 28px |
| Card headings / H2 | *Cinzel* | 600 | 18px |
| Body text | *Inter* (sans-serif) | 400 | 14px |
| Code / stat blocks | *JetBrains Mono* | 400 | 13px |
| Dice results / numbers | *JetBrains Mono* | 700 | 24px |
Self-host the fonts (offline-first!):
```css
@font-face { font-family: 'Cinzel'; src: url('/fonts/Cinzel-SemiBold.woff2'); font-weight: 600; }
@font-face { font-family: 'Cinzel'; src: url('/fonts/Cinzel-Bold.woff2'); font-weight: 700; }
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Regular.woff2'); font-weight: 400; }
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Medium.woff2'); font-weight: 500; }
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-SemiBold.woff2'); font-weight: 600; }
@font-face { font-family: 'JetBrains Mono'; src: url('/fonts/JetBrainsMono-Regular.woff2'); font-weight: 400; }
@font-face { font-family: 'JetBrains Mono'; src: url('/fonts/JetBrainsMono-Bold.woff2'); font-weight: 700; }
```
### 7.5 Micro-interactions & Motion
| Interaction | Effect |
|-------------|--------|
| Card hover | Gold border brightens; subtle gold `box-shadow` glow fades in (0.2s) |
| Card press / active | Scale down to `0.985`; inner shadow deepens |
| Dice roll | Dice tumbles with `framer-motion` spring; result number scales in with a gold flash |
| AI generation typing | Streamed tokens fade in character-by-character with a gold cursor |
| Tab / panel switch | Crossfade (150ms) — no hard jumps |
| Drag bento card | Card lifts (scale 1.03, shadow deepens); drop zone outlines pulse gold |
| HP bar change | Smooth width transition (300ms); color shifts from `--success``--danger` as HP drops |
| Notification / toast | Slides in from top-right with gold left border; auto-dismisses after 4s |
### 7.6 Component Sketch — BentoCard
```tsx
// BentoCard.tsx — the building block of the entire UI
interface BentoCardProps {
title: string;
icon: React.ReactNode; // lucide-react icon
span?: string; // tailwind grid span classes
children: React.ReactNode;
}
export function BentoCard({ title, icon, span = "col-span-1 row-span-1", children }: BentoCardProps) {
return (
<motion.div
className={`glass-card flex flex-col overflow-hidden ${span}`}
whileHover={{ scale: 1.005 }}
whileTap={{ scale: 0.985 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
>
{/* Header */}
<div className="flex items-center gap-2 pb-2 mb-3 border-b border-[var(--border-glass)]">
<span className="text-[var(--gold-bright)]">{icon}</span>
<h2 className="font-cinzel text-sm font-semibold tracking-wide text-[var(--text-primary)]">
{title}
</h2>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto scrollbar-thin">
{children}
</div>
</motion.div>
);
}
```
### 7.7 App Shell Layout
```
┌────────────────────────────────────────────────────────────┐
│ ⚔ DM-Pal [Campaign ▾] [⚙] [_ □ ✕] │ ← Title bar (Tauri custom)
├────┬───────────────────────────────────────────────────────┤
│ 🗺 │ │
│ 👤 │ Bento Grid Dashboard │
│ ⚔ │ │
│ 🎲 │ ┌──────────┬─────────┬─────────┐ │
│ 📜 │ │ WorldMap │ NPC │ Dice │ │
│ 🎵 │ │ 2×2 │ Sheet │ Roller │ │
│ ⚙ │ │ ├─────────┴─────────┤ │
│ │ │ │ Session Log │ │
│ │ ├──────────┤ 2×1 │ │
│ │ │Encounter ├────────┬─────────┤ │
│ │ │Builder │Init. │ Random │ │
│ │ └──────────┴────────┴─────────┘ │
└────┴───────────────────────────────────────────────────────┘
```
- **Left rail** (56px): icon-only nav, dark surface (`--bg-surface`), gold active indicator.
- **Top bar**: Tauri custom title bar with app name in Cinzel, campaign dropdown, settings gear.
- **Main area**: the bento grid, padded, on `--bg-deep` with a subtle radial gradient center for depth.
- Cards are user-rearrangeable; layout saves to local store.
### 7.8 Dark Mode Is the Only Mode
This app is **dark-only**. The dark blue + gold palette *is* the brand — it evokes candlelit tavern tables and starlit skies. There is no light mode toggle. All design tokens above are tuned for dark backgrounds:
- Text contrast ratios meet WCAG AA on `--bg-card`.
- Gold accents provide vibrancy without neon.
- Glass blur prevents the UI from feeling flat while keeping readability.
- Error/success states use bright red/green that pop against navy.
---
## 8. UI/UX Feature Design
How each feature surfaces in the bento dashboard:
| Feature | Bento Card Design |
|---------|-------------------|
| **World Map** | Large 2×2 card; interactive canvas with zoom, draggable tokens (SVG/Canvas), overlaid lore pins. Background layer is the map image; glass overlay panels for region info on hover. |
| **NPC Sheet** | 1×1 card; gold-bordered portrait area at top; scrollable stats/bio below. AI-generated text streams in with a typing cursor. Tap the ✨ icon to regenerate. |
| **Encounter Board** | 1×1 card; mini monster tokens dragged onto a terrain thumbnail; difficulty badge (color from `--success``--danger`). Tap to expand into a full-screen overlay. |
| **Session Log** | Wide 2×1 card; Markdown editor on the left, AI summary panel on the right (glass panel). Timeline of dice rolls and events with gold timestamps. |
| **Dice Roller Panel** | 1×1 card; big result number in JetBrains Mono 700/24px. Roll history as a subtle scrolling list below. 3D tumble animation on roll. |
| **Initiative Tracker** | 1×1 card; vertical list of combatants, gold border on active turn. HP bars animate from green → red. Condition badges are small glass pills. |
| **Relationship Web** | Full-screen overlay; `react-flow` graph on dark background. Edges colored by sentiment (gold = ally, red = hostile, blue = neutral). |
| **Soundboard** | 1×1 card; grid of small glass buttons with waveform icons. Active ambience glows gold. Volume slider on each tile. |
| **Quest Designer** | 2×1 card; branching flowchart (`react-flow`) with gold connector lines. Nodes are glass cards. AI-suggest button pulses gold when ready. |
| **Calendar & Weather** | 1×1 card; mini calendar grid with gold today-marker. Weather icon per day. Tap to see forecast details in a glass overlay. |
---
## 9. Data Persistence
Two complementary storage layers:
| Layer | What it stores | Implementation |
|-------|----------------|----------------|
| **JSON / Markdown files** | World bible, session logs, character sheets — human-editable, shareable | `tauri-plugin-fs`, `$APPDATA/dm-toolkit/data/` |
| **SQLite** | Indexed search across NPCs, locations, items, tags, relationships | `tauri-plugin-sql` (sqlite feature) or `sqlx` with WAL mode + migrations |
### SQLite Migrations
```rust
use tauri_plugin_sql::{Migration, MigrationKind, Builder};
fn main() {
let migrations = vec![Migration {
version: 1,
description: "create_campaign_tables",
sql: "CREATE TABLE npcs (
id INTEGER PRIMARY KEY,
name TEXT,
race TEXT,
bio TEXT,
tags TEXT
);",
kind: MigrationKind::Up,
}];
tauri::Builder::default()
.plugin(
Builder::default()
.add_migrations("sqlite:campaign.db", migrations)
.build(),
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### File Persistence Command
```rust
use tauri::path::BaseDirectory;
#[tauri::command]
async fn save_campaign(
app: tauri::AppHandle,
name: String,
data: serde_json::Value,
) -> Result<(), String> {
let path = app
.path()
.resolve(format!("campaigns/{}.json", name), BaseDirectory::AppData)
.map_err(|e| e.to_string())?;
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
std::fs::write(&path, serde_json::to_string_pretty(&data).unwrap())
.map_err(|e| e.to_string())?;
Ok(())
}
```
> **Tip**: Use WAL mode for concurrent reads, run migrations on startup, and periodic `VACUUM INTO` for backups.
---
## 10. Model Management UX
Local LLMs are not "fire and forget." Plan for:
| Concern | Implementation |
|---------|---------------|
| **First-run wizard** | Detect RAM/VRAM → recommend model size → download GGUF from HuggingFace |
| **Model library UI** | List installed models, quant level, file size, license, hardware fit |
| **Loading state** | Model load can take 530 s on HDD/CPU. Show progress bar and cancel button |
| **GPU offloading** | Expose `n_gpu_layers` slider per model |
| **Context length** | 2k/4k/8k selector with memory warning |
| **Image model** | Separate `image_model` field in `LlmConfig` (default `x/flux2-klein:4b`, `x/z-image-turbo` for speed). macOS-only — show a gated notice on Linux/Windows and disable the ✨ buttons. Drive the progress bar from NDJSON `step`/`total`. |
| **Generation controls** | Streaming toggle, temperature, top-p, repeat-penalty per tool |
| **License acceptance** | First-run "model license + download" wizard; don't silently bundle 4 GB models |
---
## 11. Lore Search / RAG
To keep AI-generated content consistent with your world:
1. **Chunk** your world bible into embeddings (local embedding model or llama.cpp embeddings).
2. **Store** vectors in `sqlite-vec` extension or Qdrant-lite for semantic recall.
3. **Inject** relevant lore into LLM prompts before generating content so outputs stay consistent.
---
## 12. Security & Legal Considerations
- **Rule content**: Use **5e SRD / OGL / CC-BY** content only for built-in rule lookups. Full PHB/DMG text is copyrighted.
- **AI output**: AI-generated names, places, and stat blocks can still resemble Wizards IP — add a disclaimer and content moderation toggle.
- **Model licenses**: Model files ship under their own licenses (e.g., Llama 3 requires accepting Meta's license). Build a first-run license acceptance flow.
- **Tauri v2 security**: All file/system access goes through Rust and capability permissions — no arbitrary code execution from the WebView.
---
## 13. Optional Enhancements
| Feature | Implementation Hint |
|---------|----------------------|
| **Plugin System** | Load dynamic libraries (`.so/.dll`) that expose additional commands; register via Tauri's IPC at runtime |
| **Community Share Hub** | Sync campaigns via P2P or Git repo (DMs can clone and pull) |
| **Multiplayer Mode** | Use `tauri::ipc` over WebSocket to synchronize DM/player states in real time |
| **Voice-to-Text** | Integrate Whisper for dictating notes or NPC dialogues |
| **Custom Fine-Tuning** | Store prompt libraries locally; fine-tune the local model on your campaign data |
---
## 14. Resources & Quick Links
- **Tauri v2 Docs** — https://tauri.app/
- **Rust Book** — https://doc.rust-lang.org/book/
- **llama-cpp-2 (Rust bindings)** — https://github.com/utilityai/llama-cpp-rs
- **OpenAI API** — https://platform.openai.com/docs/api-reference/chat/create
- **React Flow** — https://reactflow.dev/
- **SQLite + SQLx** — https://docs.rs/sqlx/latest/sqlx/
- **tauri-specta** — https://github.com/specta-rs/tauri-specta
- **react-konva** — https://github.com/konvajs/react-konva
- **Tailwind CSS** — https://tailwindcss.com/
- **Framer Motion** — https://www.framer.com/motion/
- **Lucide Icons** — https://lucide.dev/
- **Cinzel Font** — https://fonts.google.com/specimen/Cinzel
- **Inter Font** — https://fonts.google.com/specimen/Inter
---
## 15. Development Checkpoints
### Milestone 1 — Skeleton & Hello World ✅
- [x] Scaffold with `npm create tauri-app@latest` (react-ts template)
- [x] Configure Tauri v2 capabilities (`src-tauri/capabilities/default.json`)
- [x] Verify `invoke()` round-trip works with a simple `greet` command
- [x] Add `tauri-plugin-store`, `tauri-plugin-fs`, `tauri-plugin-log`
- [x] Commit and tag `v0.1.0-skeleton`
### Milestone 2 — LLM Integration ✅
- [x] Integrate LLM client with Ollama + OpenAI-compatible API support
- [x] Implement basic `generate(prompt, max_tokens)` command
- [x] Implement streaming infrastructure via Tauri Channels (`LlmEvent`)
- [x] Build LLM settings panel (API URL, key, model, temperature)
- [x] Add generation controls (temperature, top-p, max_tokens)
- [x] Commit and tag `v0.2.0-llm`
### Milestone 3 — Persistence Layer ✅
- [x] Set up `tauri-plugin-store` and `tauri-plugin-fs`
- [x] Implement LLM config state management (get/set via Tauri commands)
- [x] Campaign file persistence ready (JSON via fs plugin)
- [x] Commit (included in v0.2.0-llm)
### Milestone 4 — Core Utilities & Design System (First Pass) ✅
- [x] NPC Generator command + UI
- [x] World Builder command + UI (placeholder for react-konva)
- [x] Encounter Builder command + UI
- [x] Session Logger command + UI
- [x] Dice Roller command + UI
- [x] Implement design system: `BentoCard`, glassmorphism, dark blue + gold tokens
- [x] Build bento grid dashboard shell with left rail nav
- [x] Commit and tag `v0.4.0-utilities`
### Milestone 5 — DM-Specific Tools ✅
- [x] Initiative / Combat Tracker (add combatants, HP bars, conditions, round counter)
- [x] Random Tables roller (4 built-in tables, dice parser, history)
- [x] Calendar & Weather generator (Faerûn calendar, events, month nav)
- [x] Encounter Builder (AI-generated via LLM, terrain selector, difficulty)
- [x] Commit and tag `v0.5.0-dm-tools`
### Milestone 6 — Lore RAG & Polish (incl. Image Generation)
- [ ] Add `generate_image` Tauri command (reuse Ollama `/api/generate`, parse singular `image` field)
- [ ] Wire ✨ generate/regenerate into NPC, Item Forge, World Builder (map), Encounter Builder (battle map + loot), Handout Renderer
- [ ] Persist generated PNGs to `$APPDATA/dm-toolkit/images/` keyed by prompt hash (cache, don't regenerate)
- [x] Progress bar driven from NDJSON `step`/`total`; background task, non-blocking UI
- [ ] Gate image-gen UI behind macOS check; placeholder fallback elsewhere
- [ ] Add `image_model` to `LlmConfig` + settings picker (no second settings panel)
- [x] Then proceed to lore RAG & polish below:
- [x] Add lore chunking + embedding storage (SQLite + brute-force cosine; `sqlite-vec` upgrade path noted)
- [x] Inject retrieved lore into LLM prompts (shared `generate` path)
- [x] Soundboard / ambience module
- [ ] Handout renderer (Markdown → PDF)
- [ ] Polish all glassmorphism effects, micro-interactions, and gold glow states
- [x] Ensure WCAG AA contrast on all text over glass cards
- [x] Responsive bento collapse (2-column on narrow windows)
- [x] Accessibility pass (keyboard nav, screen reader labels, focus rings in `--gold-bright`)
- [ ] Commit and tag `v0.6.0-lore-polish`
### Milestone 7 — Testing, CI, Distribution
- [ ] Unit tests for dice math, encounter balance, storage migrations
- [ ] Cross-platform CI with `tauri-apps/tauri-action`
- [ ] Code signing for macOS and Windows
- [ ] Auto-updater with `tauri-plugin-updater` + delta updates for model packs
- [ ] Package with `npm run tauri build`
- [ ] Commit and tag `v1.0.0-release`
---
## 16. Final Checklist
A consolidated checklist covering all phases:
1. [x] Scaffold with `npm create tauri-app@latest` (Tauri v2, react-ts)
2. [x] Configure capabilities (`src-tauri/capabilities/default.json`)
3. [x] Integrate LLM client (Ollama + OpenAI-compatible APIs)
4. [x] Stream tokens via Tauri Channels
5. [x] Add persistence: `tauri-plugin-store` + `tauri-plugin-fs`
6. [x] Implement design system: glassmorphism, bento grid, dark blue + gold palette, Cinzel/Inter/JetBrains Mono fonts
7. [x] Implement core utilities: World Builder, NPC Generator, Encounter Builder, Dice Roller, Session Logger
8. [x] Implement DM-specific tools: Initiative Tracker, Random Tables, Calendar, Encounter Builder
9. [x] Add lore RAG / semantic search (brute-force cosine over Ollama `nomic-embed-text` embeddings in SQLite; `sqlite-vec` upgrade path noted)
- [x] `RagStore` (rusqlite bundled): chunk-on-paragraph, batch embed via `/api/embed`, store as little-endian f32 BLOB
- [x] Commands `rag_add` / `rag_search` / `rag_list` / `rag_clear`
- [x] Lore injection in shared `generate` path via optional `ragQuery` field (every generator grounded for free)
- [x] `LorePanel` UI: add lore, list/clear sources, test retrieval
- [x] Wired `ragQuery` into NPC, Item Forge, World Builder; `embed_model` in settings
10. [x] Image generation via Ollama (`x/flux2-klein`) for NPCs, items, maps, loot, handouts — macOS-gated, cached to disk
- [x] `generate_image` command (reuses Ollama `/api/generate`, parses singular `image` field, disk cache by prompt hash)
- [x] `GeneratedImage` reusable component (loading / macOS-gate placeholder / ✨ regenerate)
- [x] Wired into NPC Generator (portrait) + Item Forge (item art)
- [ ] Wire into World Builder (map), Encounter Builder (battle map + loot), Handout Renderer when those surfaces ship
- [x] `image_model` in `LlmConfig` + settings picker
11. [ ] Build first-run model download / license wizard
12. [ ] Add tests, CI (`tauri-action`), code signing, updater, then package with `npm run tauri build`
---
## 17. Post-Review Action Items (Fresh Pass)
*Findings from a fresh codebase review (shell, components, backend, git
history). Ordered by impact within each tier. Each item ships as a concrete,
checkable change — no redesigns.*
### 17.1 Repo hygiene (do first — cheap)
- [x] **Rewrite `README.md`.** It is still the Vite template boilerplate
("React + TypeScript + Vite") and describes nothing about DM-Pal.
Replace with: one-paragraph pitch, screenshot, `npm run tauri dev`
quickstart, prerequisites (Ollama), where data lives, license note.
Highest-ROI 20-minute task in the repo.
- [x] **Scrub PII from `scripts/apple-signing.env.example`.** It contains a
real Apple ID email (`james.twose2711@gmail.com`) in a tracked file.
Replace with `you@example.com`.
- [x] **Delete dead scaffold: `src/components/Greet.tsx` + the `greet`
Tauri command in `src-tauri/src/lib.rs`.** Leftover from
`npm create tauri-app`; nothing in the shell references Greet.
Verify with `rg Greet` first.
- [x] **Fix pointless ternary in `App.tsx` NavButton:**
`<item.icon size={item.view === "tables" ? 18 : 18} />` — both
branches are 18. Just `size={18}`.
- [x] **Collapse the three nav→kind maps in `App.tsx`** (`PREFILLABLE`,
`PREFILLABLE_TOOLS`, `viewMaxWidth`) into a single
`Record<View, { kind?, Comp?, maxWidth }>` and derive the inverse
map for `rehydrate`. Today adding a tool means editing five places;
this makes it one.
### 17.2 Tests & CI (Milestone 7, currently unshipped)
- [x] **`src/lib/encounter-budget.ts` self-check.** Non-trivial combat
math with no test. Add an `assert`-based `demo()` / one small
`test_encounter_budget.ts` so a tweaked XP number fails loudly.
- [x] **Dice-parser test.** `DiceRoller` parses `NdX±M`, advantage
(`kh1`/`kl1`), templates — a parser bug = wrong rolls at the table.
One `test_dice.ts` asserting `{count, sides, mod, keep}` for
`4d6+3`, `2d20kh1`, `1d20-2` is the smallest thing that catches
regressions. (Rust side has good `generations` + image-base64
tests; frontend has almost none — only `worldMap.ts`.)
- [ ] **Gitea Actions CI** (not GitHub — repo ships via `scripts/gitea-release.sh`).
A `.gitea/workflows/ci.yml` running `oxlint`, `tsc -b`, the three
`node scripts/check-*.ts` self-checks, and `cargo test` on each push.
Even lint-only is better than none. Same YAML syntax as GitHub
Actions, just a different directory.
- [ ] **Code signing + notarization** for macOS and Windows.
- [ ] **Auto-updater** via `tauri-plugin-updater` (+ delta updates for
model packs), then package with `npm run tauri build`.
### 17.3 Features still missing (verified unchecked in §16)
- [ ] **Quest branching graph (`reactflow`).** Quests are a linear
carousel today. Branching quests with conditional edges ("if they
spare the bandit → step 3; if they kill him → step 5") are what
make a quest *designer* vs a quest *outliner*. The single biggest
remaining "wow" gap in the generators.
- [ ] **World hierarchy tree** (continent → country → region → city).
Today `WorldBuilder` emits a flat `regions[]` + `landmarks[]`
pinned on a map, with no nesting or "drill into a region to
generate its sub-regions." A collapsible tree on the left of the
existing two-pane layout turns World Builder from a one-shot
generator into a living campaign bible.
- [x] **Lore directory picker.** Doc said "needs tauri-plugin-dialog" —
but the plugin is **already installed and used** in
`SettingsPanel`. This is unblocked: add an "Add directory…" button
in `LorePanel` that calls `open({ directory: true })` and walks
`*.md`/`*.txt`. Low effort, high value for DMs with an existing
world-bible folder.
- [ ] **Real ambience packs + custom sound import (Soundboard).** Ship
23 CC0 loops (Freesound/Pixabay) in `public/sounds/`, keep
synthesis as fallback, and add drag-an-MP3 onto a tile. The
difference between "demo" and "session-ready."
- [ ] **First-run model download / license wizard.** Today a new user
must hand-configure API URL + model name with no guidance. A
one-time wizard on first launch (detect Ollama → list
`GET /api/tags` → pick text + image + embed model → save) collapses
the "open Settings, stare at empty form, give up" funnel. Reuse
the existing connection-test + model-list plumbing from
`SettingsPanel`.
- [ ] **Image-gen into World Builder (map) + Encounter Builder**
(battle map + loot), per §16 item 10 — still only wired into NPC +
Item Forge.
- [ ] **Handout renderer** (Markdown → PDF), still pending from §15.
### 17.4 UX refinements (beyond the existing ui-ux-improvements.md)
- [ ] **Campaign concept.** Everything is one global store
(`dm-pal-state.json`) + two global SQLite DBs; a DM running two
campaigns can't separate them. A lightweight "active campaign"
selector in the title bar that namespaces store keys + DB files
(`<dataDir>/<campaign>/`) unlocks multi-campaign without a schema
migration. The data-dir relocation machinery already exists —
generalize it per-campaign.
- [x] **`?` shortcut help overlay.** Checklist marks `?` as shipped but
there's no visible cheatsheet — shortcuts are documented only
here. A small `?`-triggered modal listing
`⌘K / 19 / Space / ⌘, / ⌘S / Esc` is the difference between
"shortcuts exist" and "shortcuts are discoverable." ~40 lines.
- [x] **Global streaming indicator.** `ConnectionPill` shows
LLM/image/lore status but there's no "an LLM call is in flight"
signal. During a 15s image gen the only feedback is a skeleton in
one card. A subtle gold pulse on the pill (or a thin top-of-window
progress bar) tells the DM something is working even after they've
navigated away from the generating tool.
- [x] **Consistent image-gen macOS gating.** `ImageGenerator` shows a
clear "macOS only" message, but the `GeneratedImage` ✨ buttons
inside NPC/Item silently fall back. Show the same inline
"macOS only" notice there so a Windows DM isn't left wondering why
nothing happens.
- [ ] **Finish the empty-state set for Initiative** (and verify the
others) — one `<p>` + CTA each, already done elsewhere.
- [ ] **i18n** — extract strings to a `t()` helper (P3, only if shipping
beyond EN).
- [ ] **Draggable bento layout** (`react-grid-layout`) with persisted
layout per campaign (P3).
- [ ] **Console mode** for the dashboard — embed 23 chosen tools as
live-session cards (P3).
- [ ] **Player view** — a web app showing the DM's screen (initiative,
dice) to phones on the local network (P3).
- [ ] **Compendium integration** — pull monster stat blocks from a
local SRD JSON (P3).
- [ ] **Voice-to-text** for session notes (Whisper) (P3).
- [ ] **Macros** — named dice-roll buttons (P3).
- [ ] **Session replay** — record rolls/initiative/soundboard state
and replay (P3).
### 17.5 Suggested order of attack
| # | Item | Effort | Impact |
|---|------|--------|--------|
| 1 | Rewrite README | 20 min | High |
| 2 | Delete Greet + `greet` cmd, fix `18:18` ternary | 10 min | Low |
| 3 | Collapse the 3 nav→kind maps into one | 30 min | Medium |
| 4 | `encounter-budget` + dice-parser self-tests | 1 hr | Medium |
| 5 | Lore directory picker (dialog already installed) | 1 hr | High |
| 6 | First-run model wizard (reuse connection test) | 3 hrs | High |
| 7 | Quest branching graph (`reactflow`) | 4 hrs | High |
| 8 | World hierarchy tree | 3 hrs | High |
| 9 | `?` help overlay + global streaming indicator | 1 hr | Medium |
| 10 | Real ambience packs + sound import | 2 hrs | Medium |
| 11 | GitHub Actions CI | 1 hr | Medium |
| 12 | Campaign namespace selector | 4 hrs | High |
Items 15 form a single low-risk PR: README, dead-code cleanup, map
consolidation, two self-tests, lore directory picker — all independently
shippable in one session.
---
**You're now ready to spin up your own AI-powered Dungeon Master toolkit!**
Happy crafting — may your dice always land favorably! 🎲