Starting an encounter meant searching for each party member by name, one at a time. The management view now carries an "Add party to encounter" button that puts all of them on the board in one click and closes, so the initiative list is what you see next. Members already in the encounter are skipped. Matching is on playerCharacterId, not on name — otherwise a second click would hand you "Thorin 2" via the auto-numbering, and renaming a combatant to match a mini (the reason the rename feature exists) would break the check. Rows for those members show a muted swords icon, and the button disables once nothing is left to add, so the greyed-out state has a visible cause. The whole batch pushes a single undo entry rather than one per combatant: undoing a four-person party should not take four keystrokes. That is why the reducer loops internally instead of the UI dispatching N times, and why addOneFromPlayerCharacter is now split out of the single-add handler for both paths to share. Spec 005 gains story PC-8, FR-020..FR-023, SC-010/SC-011 and the edge cases around partial parties, renamed combatants and orphaned ones. The existing "multiple copies of the same PC are allowed" edge case is marked as still true for individual adds — the party button is the deliberate exception. Key Entities now documents the combatant/PC link the dedup relies on, which had never been written down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Initiative
A local-first initiative tracker and encounter manager for tabletop RPGs (D&D 5e / 2024). Runs entirely in the browser — no server, no account, no data leaves your machine.
What it does
- Initiative tracking — add combatants (batch-add from bestiary, custom creatures with optional stats), roll initiative (manual or d20), cycle turns and rounds
- Encounter state — HP, AC, conditions, concentration tracking with visual status indicators
- Bestiary integration — import bestiary JSON sources, search creatures, and view full stat blocks
- Player characters — create reusable player character templates with name, AC, HP, level, color, and icon; search and add them to encounters with pre-filled stats; manage (edit/delete) from a dedicated panel
- Encounter difficulty — live 3-bar indicator in the top bar showing encounter difficulty (Trivial/Low/Moderate/High) based on the 2024 5.5e XP budget system; automatically derived from PC levels and bestiary creature CRs
- Undo/redo — reverse any encounter action with Undo/Redo buttons or keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z, Cmd on Mac); history persists across page reloads
- Import/export — export the full encounter state (combatants, undo/redo history, player characters) as a JSON file or copy to clipboard; import from file upload or pasted JSON with validation and confirmation
- Persistent — encounters survive page reloads via localStorage; bestiary data cached in IndexedDB; player characters stored independently
Prerequisites
- Node.js 22+
- pnpm 10.6+
Getting Started
pnpm install
pnpm --filter web dev
Open http://localhost:5173.
Scripts
| Command | Description |
|---|---|
pnpm --filter web dev |
Start the dev server |
pnpm --filter web build |
Production build |
pnpm test |
Run all tests (Vitest) |
pnpm test:watch |
Tests in watch mode |
pnpm vitest run path/to/test.ts |
Run a single test file |
pnpm typecheck |
TypeScript type checking |
pnpm lint |
Biome lint |
pnpm format |
Biome format (writes changes) |
pnpm check |
Full merge gate (see below) |
Merge gate (pnpm check)
All of these run at pre-commit via Lefthook (in parallel where possible):
pnpm audit— security auditknip— unused code detectionbiome check— formatting + lintingoxlint— type-aware linting (complements Biome)- Custom scripts — lint-ignore caps, className enforcement, component prop limits
tsc --build— TypeScript strict modevitest run— tests with per-path coverage thresholdsjscpd+jsinspect— copy-paste and structural duplication detection
Tech Stack
- TypeScript 5.8 (strict mode), React 19, Vite 6
- Tailwind CSS v4 (dark/light theme)
- Biome 2.4 (formatting + linting), oxlint (type-aware linting)
- Vitest (testing, v8 coverage), Lefthook (pre-commit hooks)
- Knip (unused code), jscpd + jsinspect (duplication detection)
Project Structure
apps/web/ React 19 + Vite — UI components, hooks, adapters
packages/domain/ Pure functions — state transitions, types, validation
packages/application/ Use cases — orchestrates domain via port interfaces
data/bestiary/ Pre-built bestiary search index (~10k creatures)
scripts/ Build tooling (layer checks, index generation)
specs/ Feature specifications (spec → plan → tasks)
Architecture
Strict layered architecture with enforced dependency direction:
apps/web (adapters) → packages/application (use cases) → packages/domain (pure logic)
- Domain — pure functions, no I/O, no randomness, no framework imports. Errors returned as values (
DomainError), never thrown. - Application — orchestrates domain calls via port interfaces (
EncounterStore,PlayerCharacterStore, etc.). No business logic. - Web — React adapter. Implements ports using hooks/state. All UI components, persistence, and external data access live here.
Layer boundaries are enforced by automated import checks that run as part of the test suite.
Contributing
Workflow
Development is spec-driven. Feature specs live in specs/NNN-feature-name/ and are managed through Claude Code skills (see CLAUDE.md for full details).
| Scope | What to do |
|---|---|
| Bug fix / CSS tweak | Fix it, run pnpm check, commit. Optionally use /browser-interactive-testing for visual verification. |
| Change to existing feature | Update the feature spec, then implement |
| Larger change to existing feature | Update the spec → /rpi-research → /rpi-plan → /rpi-implement |
| New feature | /speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.implement |
Use /write-issue to create well-structured Gitea issues, and /integrate-issue to pull an existing issue's requirements into the relevant feature spec.
Before committing
Run pnpm check — Lefthook runs this automatically at pre-commit, but running it manually first saves time. All checks must pass.
Conventions
- Biome for formatting and linting — tab indentation, 80-char lines
- TypeScript strict mode with
verbatimModuleSyntax(type-only imports must useimport type) - Max 8 props per component interface — use React context for shared state
- Tests in
__tests__/directories — test pure functions directly, userenderHookfor hooks
See CLAUDE.md for the full conventions and project constitution.
Bestiary Index
The bestiary search index (data/bestiary/index.json) is pre-built and checked into the repo. To regenerate it (e.g., after a new source book release):
- Clone 5etools-mirror-3/5etools-src locally
- Run
node scripts/generate-bestiary-index.mjs /path/to/5etools-src
The script extracts creature names, stats, and source info into a compact search index.