A terminal UI (TUI) application for D&D Dungeon Masters to manage combat encounters.
Built with ratatui for the TUI (crossterm backend), rusqlite for persistence.
- Track initiative order, hit points, and damage for all combatants
- Apply actions: damage, healing, temporary HP, misses, buffs
- Distinguish action types: Action, Bonus Action, Reaction, Legendary Action
- Save prepared combats and resume them across sessions
- Turn and round tracking with automatic reordering (finished combatants move to the bottom)
cargo run -p combat
# Suppress stderr (logs go to output.log)
cargo run -p combat 2>/dev/nullThe application is a state machine driven by the Screen enum (defined in types.rs). Every key press is routed by event_handler::handle_key_event, which matches on the current Screen variant and dispatches to the appropriate handler module. Each handler returns a new Screen, and the app loop replaces app.screen with it.
Screen::Menu
│
├─[C] New combat ──────────────────► Screen::Create(CreateCombatState)
│ │
├─[L] Load combat ─► Screen::Load ├─[S] Start combat ──► Screen::Combat(CombatState)
│ │ │ │
│ └─[Enter] ─────────────────────────│ └─[A] Action ──► Screen::Action(ActionState)
│ └─[P] Save ──► DB │
│ ┌─[M] Miss ───────────┤
└─[q] Quit │ ├─[A] Attack ──► Screen::Prompt ──► Screen::Combat
│ ├─[H] Heal ────► Screen::Prompt ──► Screen::Combat
│ └─[T] THP ─────► Screen::Prompt ──► Screen::Combat
└───────────────────────────────────────────────────────► Screen::Combat
Overlay screens can appear from any context:
Screen::Prompt(PromptState)— a modal text-input dialog. Carries acallback: Fn(&mut App, String) -> Screenso any screen can push a prompt and regain control when the user confirms.Screen::Error(ErrorState)— a modal error display. Stores the originating screen and returns to it on dismissal.
| Variant | State struct | Purpose |
|---|---|---|
Screen::Menu |
— | Main menu; entry point on launch |
Screen::Create(CreateCombatState) |
CreateCombatState |
Add/edit combatants before combat begins; save as prepared or start immediately |
Screen::Load(LoadState) |
LoadState |
Browse saved combats from the database and resume one |
Screen::Combat(CombatState) |
CombatState |
Active combat: HP tracking, turn order, round progression |
Screen::Action(ActionState) |
ActionState |
Action flow: choose targets then apply an effect (damage / heal / THP / miss / buff) |
Screen::Prompt(PromptState) |
PromptState |
Generic reusable text-input overlay; resolves via a callback |
Screen::Error(ErrorState) |
ErrorState |
Error overlay; dismisses back to the originating screen |
src/
├── main.rs Entry point. Bootstraps logger (fern → output.log),
│ opens SQLite DB, runs migrations, initializes terminal.
├── app.rs App struct (database + current screen) and the main event loop.
├── types.rs All domain types: Screen, Combatant, Combat, Round, Turn,
│ Action, ActionType, ActionEffect, and all *State structs.
├── database.rs SQLite persistence. Combatants and rounds are stored as
│ MessagePack BLOBs via rmp-serde.
├── tui.rs Terminal initialization/teardown (crossterm raw mode + alternate screen).
├── errors.rs DatabaseError and panic/error hooks that restore the terminal.
├── event_handler.rs Top-level key event router; dispatches to per-screen handlers.
├── event_handler/
│ ├── menu.rs
│ ├── combat_create.rs
│ ├── load.rs
│ ├── combat.rs
│ ├── action.rs
│ ├── prompt.rs
│ └── error.rs
├── ui.rs Top-level render entry point.
└── ui/
├── logo.rs Logo rendering utility.
└── screens/ One render function per Screen variant (no state mutation).
The combat table stores combatants and rounds as MessagePack BLOBs. Migrations run on every startup via run_database_migrations (idempotent CREATE TABLE IF NOT EXISTS). The DB file is ./db.db3 relative to the working directory at runtime.