Implement the 021-bestiary-statblock feature that adds a searchable D&D 2024 Monster Manual creature library with inline autocomplete suggestions, full stat block display in a fixed side panel, auto-numbering of duplicate creature names, HP/AC pre-fill from bestiary data, and automatic stat block display on turn change for wide viewports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-09 11:01:07 +01:00
parent 04a4f18f98
commit fa078be2f9
30 changed files with 66221 additions and 56 deletions

View File

@@ -0,0 +1,96 @@
# Implementation Plan: Bestiary Search & Stat Block Display
**Branch**: `021-bestiary-statblock` | **Date**: 2026-03-06 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/021-bestiary-statblock/spec.md`
## Summary
Add a searchable creature library (2024 Monster Manual, 503 creatures) to the initiative tracker. Users search by name, view full stat blocks in a responsive side panel, and add creatures as combatants with HP/AC/name pre-filled. Data is bundled as static JSON, normalized at load time via an adapter. Auto-numbering handles duplicate creature names. Layout adapts between side-by-side (desktop) and drawer (mobile).
## Technical Context
**Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax)
**Primary Dependencies**: React 19, Vite 6, Tailwind CSS v4, Lucide React (icons)
**Storage**: Browser localStorage (existing adapter, extended for creatureId)
**Testing**: Vitest (unit tests for domain functions, adapter normalization, tag stripping)
**Target Platform**: Modern browsers (desktop + mobile)
**Project Type**: Web application (local-first, single-user)
**Performance Goals**: Search results in <200ms for 500 creatures (trivially met with in-memory filter)
**Constraints**: Offline-capable, no backend, bundle size increase ~300-500KB gzipped
**Scale/Scope**: 503 creatures (2024 MM), extensible to multiple bestiary sources
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Deterministic Domain Core | PASS | Creature types and auto-numbering are pure. No I/O in domain. Normalization adapter lives in web layer. |
| II. Layered Architecture | PASS | Domain: types + auto-numbering. Application: N/A (no new use case needed — addCombatant reused). Web: adapter, hooks, components. |
| III. Agent Boundary | N/A | No agent layer involvement. |
| IV. Clarification-First | PASS | All ambiguities resolved in spec discussion and research phase. |
| V. Escalation Gates | PASS | Feature fully specified before planning. |
| VI. MVP Baseline Language | PASS | Custom creature editor, fuzzy search, multiple bestiary sources described as future iterations. |
| VII. No Gameplay Rules | PASS | Stat block display is data rendering, not gameplay mechanics. |
**Post-Phase 1 Re-check**: All gates still pass. The `Creature` type in domain contains no I/O. The bestiary adapter in the web layer handles all data transformation. The `creatureId` extension to `Combatant` is a minimal, optional field.
## Project Structure
### Documentation (this feature)
```text
specs/021-bestiary-statblock/
├── spec.md
├── plan.md # This file
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── ui-contracts.md
├── checklists/
│ └── requirements.md
└── tasks.md # Generated by /speckit.tasks
```
### Source Code (repository root)
```text
data/
└── bestiary/
└── xmm.json # Raw 5etools bestiary (2024 MM)
packages/domain/src/
├── types.ts # MODIFIED: add creatureId to Combatant
├── creature-types.ts # NEW: Creature, CreatureId, TraitBlock, etc.
├── auto-number.ts # NEW: resolveCreatureName()
├── index.ts # MODIFIED: export new types
└── __tests__/
├── auto-number.test.ts # NEW
└── layer-boundaries.test.ts # EXISTING (unchanged)
packages/application/src/
└── (no changes — reuses existing addCombatant use case)
apps/web/src/
├── App.tsx # MODIFIED: two-column layout, stat block state
├── adapters/
│ ├── bestiary-adapter.ts # NEW: raw JSON → Creature[]
│ ├── strip-tags.ts # NEW: {@tag} → plain text
│ └── __tests__/
│ ├── bestiary-adapter.test.ts # NEW
│ └── strip-tags.test.ts # NEW
├── hooks/
│ ├── use-encounter.ts # MODIFIED: addFromBestiary, auto-numbering
│ └── use-bestiary.ts # NEW: search + lookup
├── components/
│ ├── action-bar.tsx # MODIFIED: search icon, suggestions
│ ├── bestiary-search.tsx # NEW: search input + dropdown
│ ├── stat-block.tsx # NEW: creature stat block renderer
│ └── stat-block-panel.tsx # NEW: responsive panel/drawer wrapper
├── persistence/
│ └── encounter-storage.ts # MODIFIED: persist/rehydrate creatureId
└── index.css # MODIFIED: stat block styling
```
**Structure Decision**: Follows existing monorepo layout. New domain types in `packages/domain`. Bestiary adapter and UI components in `apps/web`. Raw data in top-level `data/` directory (not inside any package, as it's a static asset consumed by the web app's build).