36 KiB
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
- Scaffold a Tauri v2 project.
- Add Rust bindings to a local LLM (
llama-cpp-2,llama_cpp, or Candle).- Build modular "utilities" (World Builder, NPC Generator, Encounter Designer, etc.).
- Wire them up in the front-end via
tauri.invoke+ Tauri Channels for streaming.- Package and ship — no internet required after first run.
Table of Contents
- What a DM Needs
- System Architecture
- Quick Start — Boilerplate
- Local LLM Integration
- Core Utilities & AI Features
- Front-End Architecture (React/TS)
- Design System — Visual Language
- UI/UX Feature Design
- Data Persistence
- Model Management UX
- Lore Search / RAG
- Security & Legal Considerations
- Optional Enhancements
- Resources & Quick Links
- Development Checkpoints
- 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:
{
"$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:
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$APPDATA/dm-toolkit/**" }]
}
3. Quick Start — Boilerplate
Prerequisites
# 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
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.rsandsrc-tauri/src/main.rs(mobile/desktop share the lib). Import commands from@tauri-apps/api/core, not the old v1 path.
Add Dependencies
# 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.
// 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
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
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
// 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.
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 | "Create a fantasy continent with 3 climates and one legendary creature." |
| NPC Generator | Personality traits, background story, NPC goals, relationship webs | "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 | "Design an urban ambush for level-5 PCs with 3 bandits and a surprise mob." |
| Item Forge | Stat tables, lore sentences, rarity levels | "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
// 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:
.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):
// 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-reacticon.
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!):
@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
// 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-deepwith 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
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
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 INTOfor 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 5–30 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 |
| 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:
- Chunk your world bible into embeddings (local embedding model or llama.cpp embeddings).
- Store vectors in
sqlite-vecextension or Qdrant-lite for semantic recall. - 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 ✅
- Scaffold with
npm create tauri-app@latest(react-ts template) - Configure Tauri v2 capabilities (
src-tauri/capabilities/default.json) - Verify
invoke()round-trip works with a simplegreetcommand - Add
tauri-plugin-store,tauri-plugin-fs,tauri-plugin-log - Commit and tag
v0.1.0-skeleton
Milestone 2 — LLM Integration ✅
- Integrate LLM client with Ollama + OpenAI-compatible API support
- Implement basic
generate(prompt, max_tokens)command - Implement streaming infrastructure via Tauri Channels (
LlmEvent) - Build LLM settings panel (API URL, key, model, temperature)
- Add generation controls (temperature, top-p, max_tokens)
- Commit and tag
v0.2.0-llm
Milestone 3 — Persistence Layer ✅
- Set up
tauri-plugin-storeandtauri-plugin-fs - Implement LLM config state management (get/set via Tauri commands)
- Campaign file persistence ready (JSON via fs plugin)
- Commit (included in v0.2.0-llm)
Milestone 4 — Core Utilities & Design System (First Pass) ✅
- NPC Generator command + UI
- World Builder command + UI (placeholder for react-konva)
- Encounter Builder command + UI
- Session Logger command + UI
- Dice Roller command + UI
- Implement design system:
BentoCard, glassmorphism, dark blue + gold tokens - Build bento grid dashboard shell with left rail nav
- Commit and tag
v0.4.0-utilities
Milestone 5 — DM-Specific Tools ✅
- Initiative / Combat Tracker (add combatants, HP bars, conditions, round counter)
- Random Tables roller (4 built-in tables, dice parser, history)
- Calendar & Weather generator (Faerûn calendar, events, month nav)
- Encounter Builder (AI-generated via LLM, terrain selector, difficulty)
- Commit and tag
v0.5.0-dm-tools
Milestone 6 — Lore RAG & Polish
- Add lore chunking + embedding storage (
sqlite-vec) - Inject retrieved lore into LLM prompts
- Soundboard / ambience module
- Handout renderer (Markdown → PDF)
- Polish all glassmorphism effects, micro-interactions, and gold glow states
- Ensure WCAG AA contrast on all text over glass cards
- Responsive bento collapse (2-column on narrow windows)
- 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:
- Scaffold with
npm create tauri-app@latest(Tauri v2, react-ts) - Configure capabilities (
src-tauri/capabilities/default.json) - Integrate LLM client (Ollama + OpenAI-compatible APIs)
- Stream tokens via Tauri Channels
- Add persistence:
tauri-plugin-store+tauri-plugin-fs - Implement design system: glassmorphism, bento grid, dark blue + gold palette, Cinzel/Inter/JetBrains Mono fonts
- Implement core utilities: World Builder, NPC Generator, Encounter Builder, Dice Roller, Session Logger
- Implement DM-specific tools: Initiative Tracker, Random Tables, Calendar, Encounter Builder
- Add lore RAG / semantic search (
sqlite-vec) - Build first-run model download / license wizard
- Add tests, CI (
tauri-action), code signing, updater, then package withnpm run tauri build
You're now ready to spin up your own AI-powered Dungeon Master toolkit!
Happy crafting — may your dice always land favorably! 🎲