199 lines
15 KiB
Markdown
199 lines
15 KiB
Markdown
# Tasks: Bestiary Search & Stat Block Display
|
|
|
|
**Input**: Design documents from `/specs/021-bestiary-statblock/`
|
|
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ui-contracts.md, quickstart.md
|
|
|
|
**Tests**: Included for domain logic, adapter normalization, and tag stripping (core correctness). UI components tested via manual verification.
|
|
|
|
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
|
|
|
## Format: `[ID] [P?] [Story] Description`
|
|
|
|
- **[P]**: Can run in parallel (different files, no dependencies)
|
|
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
|
- Include exact file paths in descriptions
|
|
|
|
---
|
|
|
|
## Phase 1: Setup
|
|
|
|
**Purpose**: Bestiary data and domain type foundations
|
|
|
|
- [x] T001 Copy raw 5etools 2024 Monster Manual JSON into `data/bestiary/xmm.json` (download from 5etools-mirror-3 repo, commit as static asset)
|
|
- [x] T002 [P] Define `CreatureId` branded type and all creature-related types (`Creature`, `TraitBlock`, `LegendaryBlock`, `SpellcastingBlock`, `DailySpells`, `BestiarySource`) in `packages/domain/src/creature-types.ts` per data-model.md
|
|
- [x] T003 [P] Define `proficiencyBonus(cr: string): number` pure function in `packages/domain/src/creature-types.ts` using the CR-to-bonus table from data-model.md
|
|
- [x] T004 Export all new creature types from `packages/domain/src/index.ts`
|
|
|
|
---
|
|
|
|
## Phase 2: Foundational (Blocking Prerequisites)
|
|
|
|
**Purpose**: Adapter layer that normalizes raw bestiary data — required before any search or display can work
|
|
|
|
**CRITICAL**: No user story work can begin until this phase is complete
|
|
|
|
- [x] T005 Implement `stripTags(text: string): string` in `apps/web/src/adapters/strip-tags.ts` — converts all `{@tag ...}` markup patterns to plain text per research.md R-002 tag resolution rules (15+ tag types: spell, dice, damage, dc, hit, h, hom, atkr, recharge, actSave, actSaveFail, actSaveSuccess, actTrigger, actResponse, variantrule, action, skill, creature, hazard, status, condition, actSaveSuccessOrFail, actSaveFailBy)
|
|
- [x] T006 Write tests for `stripTags` in `apps/web/src/adapters/__tests__/strip-tags.test.ts` — cover each tag type with at least one example, edge cases (nested tags, unknown tags, no tags)
|
|
- [x] T007 Implement `normalizeBestiary(raw: { monster: RawMonster[] }): Creature[]` adapter in `apps/web/src/adapters/bestiary-adapter.ts` — normalizes all 5etools format variations: `type` as string/object (with tags, swarmSize), `size` as string array, `ac` as int array (extract numeric value and optional `acSource` string from armor description), `cr` as string/object, `alignment` codes to text, `speed` object to formatted string, ability score modifiers, saving throws, skills, vulnerabilities/resistances/immunities (strings and conditional objects), condition immunities, senses, languages, traits/actions/legendary/bonus/reactions with recursive entry rendering, spellcasting blocks (will/daily/restLong). Uses `stripTags` for all text content. Generates `CreatureId` from source + name slug.
|
|
- [x] T008 Write tests for `normalizeBestiary` in `apps/web/src/adapters/__tests__/bestiary-adapter.test.ts` — test with representative raw entries: simple creature, creature with legendary actions, creature with spellcasting, creature with conditional resistances, creature with object-type `type` field, creature with multiple sizes
|
|
- [x] T009 Add optional `creatureId?: CreatureId` field to `Combatant` interface in `packages/domain/src/types.ts`
|
|
- [x] T010 Update `loadEncounter` in `apps/web/src/persistence/encounter-storage.ts` to rehydrate the `creatureId` field (validate as string, brand with `creatureId()` constructor, gracefully ignore if absent)
|
|
|
|
**Checkpoint**: Bestiary adapter tested and working. Combatant type extended. Ready for user story implementation.
|
|
|
|
---
|
|
|
|
## Phase 3: User Story 1 — Search and Add a Creature from the Bestiary (Priority: P1) MVP
|
|
|
|
**Goal**: Users can search the bundled bestiary by creature name and add a matching creature as a combatant with name, HP, and AC pre-filled.
|
|
|
|
**Independent Test**: Search for "Goblin", select it, verify combatant appears with correct name/HP/AC. Add another Goblin, verify auto-numbering ("Goblin 1", "Goblin 2").
|
|
|
|
### Implementation for User Story 1
|
|
|
|
- [x] T011 [P] [US1] Implement `resolveCreatureName(baseName: string, existingNames: readonly string[]): { newName: string; renames: ReadonlyArray<{ from: string; to: string }> }` pure function in `packages/domain/src/auto-number.ts` — handles auto-numbering logic: no conflict returns name as-is; first conflict renames existing to "Name 1" and new to "Name 2"; subsequent conflicts append next number. Include unit tests in `packages/domain/src/__tests__/auto-number.test.ts`
|
|
- [x] T012 [P] [US1] Export `resolveCreatureName` from `packages/domain/src/index.ts`
|
|
- [x] T013 [US1] Implement `useBestiary()` hook in `apps/web/src/hooks/use-bestiary.ts` — lazy-loads and normalizes `xmm.json` via `normalizeBestiary`, memoizes result. Returns `search(query: string): Creature[]` (case-insensitive substring filter, sorted alphabetically, max 10 results), `getCreature(id: CreatureId): Creature | undefined`, `allCreatures: Creature[]`, and `isLoaded: boolean`. If loading fails, `isLoaded` remains false, search returns empty results, and the bestiary search icon in the action bar is hidden.
|
|
- [x] T014 [US1] Create `BestiarySearch` component in `apps/web/src/components/bestiary-search.tsx` — search input with autocomplete dropdown: minimum 2 chars to trigger, keyboard navigation (ArrowUp/ArrowDown/Enter/Escape), shows creature name with source tag (e.g., "Goblin (MM 2024)"), "No creatures found" empty state. Calls `onSelectCreature(creature)` on selection, `onClose()` on dismiss.
|
|
- [x] T015 [US1] Add `addFromBestiary(creature: Creature): void` to `useEncounter` hook in `apps/web/src/hooks/use-encounter.ts` — calls `resolveCreatureName` to get auto-numbered name and any renames, applies renames via `editCombatant`, then calls `addCombatantUseCase` with resolved name. Sets `creatureId`, `maxHp`, `currentHp` (= hp.average), and `ac` on the new combatant. (Note: setting HP and AC requires calling `setHpUseCase` and `setAcUseCase` after add.)
|
|
- [x] T016 [US1] Modify `ActionBar` in `apps/web/src/components/action-bar.tsx` — add magnifying glass icon button (Lucide `Search` icon) next to the existing input. Clicking it opens `BestiarySearch` overlay. Wire `onSelectCreature` to `addFromBestiary`. Keep existing name input and Add button for plain combatant adding.
|
|
|
|
**Checkpoint**: User Story 1 fully functional — search, select, auto-number, pre-fill. Testable independently.
|
|
|
|
---
|
|
|
|
## Phase 4: User Story 2 — View Creature Stat Block (Priority: P2)
|
|
|
|
**Goal**: Display a full creature stat block in a side panel when a creature is selected from search or when clicking a bestiary-linked combatant.
|
|
|
|
**Independent Test**: Select any creature from search, verify all stat block sections render with correct data and no raw markup tags visible.
|
|
|
|
### Implementation for User Story 2
|
|
|
|
- [x] T017 [P] [US2] Create `StatBlock` component in `apps/web/src/components/stat-block.tsx` — renders the full creature stat block per contracts/ui-contracts.md section order: (1) header (name, size/type/alignment), (2) stats bar (AC, HP with formula, Speed), (3) ability scores with modifiers in 6-column grid, (4) properties block (saves, skills, vulnerabilities, resistances, immunities, condition immunities, senses, languages, CR + proficiency bonus), (5) traits, (6) spellcasting, (7) actions, (8) bonus actions, (9) reactions, (10) legendary actions with preamble. Sections omitted when data is absent. Style with Tailwind to match classic stat block aesthetic (section dividers, bold labels, italic names).
|
|
- [x] T018 [P] [US2] Create `StatBlockPanel` responsive wrapper in `apps/web/src/components/stat-block-panel.tsx` — desktop (>= 1024px): right-side panel with close button, independently scrollable. Mobile (< 1024px): slide-over drawer from right (85% width) with backdrop overlay, click-backdrop-to-close. Animate open/close transitions.
|
|
- [x] T019 [US2] Add stat block state management to `App.tsx` in `apps/web/src/App.tsx` — track `selectedCreature: Creature | null` state. When set, render `StatBlockPanel`. Modify layout: when panel open on desktop, switch from centered `max-w-2xl` to side-by-side flex layout (tracker flex-1, panel ~400px). When closed, revert to centered layout.
|
|
- [x] T020 [US2] Wire creature selection to stat block in `apps/web/src/App.tsx` — when `addFromBestiary` is called, also set `selectedCreature`. Add `onShowStatBlock` prop to `CombatantRow` — when clicking a combatant that has `creatureId`, resolve to `Creature` via `useBestiary().getCreature()` and set `selectedCreature`.
|
|
- [x] T021 [US2] Add stat block-related CSS in `apps/web/src/index.css` — stat block section dividers (gradient line), drawer slide animation (`@keyframes slide-in-right`), backdrop fade, responsive transitions.
|
|
|
|
**Checkpoint**: User Stories 1 AND 2 both work — search adds creatures AND stat block displays.
|
|
|
|
---
|
|
|
|
## Phase 5: User Story 3 — Quick-Add with Bestiary Suggestions (Priority: P3)
|
|
|
|
**Goal**: Show bestiary suggestions while typing in the existing combatant name input, allowing seamless switching between plain-name add and bestiary-assisted add.
|
|
|
|
**Independent Test**: Type "Dragon" in name input, see suggestions appear, select one (stats pre-filled) or press Enter to add plain combatant.
|
|
|
|
### Implementation for User Story 3
|
|
|
|
- [x] T022 [US3] Create inline suggestion dropdown for `ActionBar` in `apps/web/src/components/action-bar.tsx` — when text in name input >= 2 chars and matches bestiary creatures, show a suggestion list below the input (reuse search filtering from `useBestiary`). Reuse the dropdown rendering and keyboard navigation logic from `BestiarySearch` (T014) via a shared component or by extracting the dropdown portion. Selecting a suggestion calls `addFromBestiary`. Pressing Enter without selecting adds a plain combatant (current behavior). Suggestions show creature name + source tag.
|
|
|
|
**Checkpoint**: All three user stories independently functional.
|
|
|
|
---
|
|
|
|
## Phase 6: User Story 4 — Responsive Layout Transition (Priority: P4)
|
|
|
|
**Goal**: Layout adapts smoothly between side-by-side (desktop) and drawer (mobile) when viewport changes.
|
|
|
|
**Independent Test**: Open stat block on desktop, resize window below 1024px, verify transitions to drawer. Resize back, verify returns to side-by-side.
|
|
|
|
### Implementation for User Story 4
|
|
|
|
- [x] T023 [US4] Implement responsive transition in `apps/web/src/components/stat-block-panel.tsx` — use CSS media query or `matchMedia` listener to detect viewport width changes while panel is open. Ensure smooth transition between panel and drawer modes without losing scroll position or closing the panel. Test with browser resize and device rotation.
|
|
|
|
**Checkpoint**: All four user stories work across viewport sizes.
|
|
|
|
---
|
|
|
|
## Phase 7: Polish & Cross-Cutting Concerns
|
|
|
|
**Purpose**: Final validation and cleanup
|
|
|
|
- [x] T024 Verify layer boundaries pass — run `pnpm test` and confirm `layer-boundaries.test.ts` passes with new creature types in domain and adapter in web
|
|
- [x] T025 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues — ensure no unused exports from creature-types.ts, no lint errors in new components, typecheck passes with creatureId extension
|
|
- [ ] T026 Manually test full flow (requires human verification) end-to-end: search creature, add with pre-fill, verify auto-numbering, view stat block, close panel, click combatant to re-open stat block, test on narrow viewport drawer mode, verify localStorage persistence of creatureId across page reload
|
|
|
|
---
|
|
|
|
## Dependencies & Execution Order
|
|
|
|
### Phase Dependencies
|
|
|
|
- **Setup (Phase 1)**: No dependencies — can start immediately
|
|
- **Foundational (Phase 2)**: Depends on Phase 1 (T001 for raw data, T002-T004 for types)
|
|
- **User Story 1 (Phase 3)**: Depends on Phase 2 completion
|
|
- **User Story 2 (Phase 4)**: Depends on Phase 2 completion. Can run in parallel with US1, but better sequentially since US2 wires into US1's `addFromBestiary`
|
|
- **User Story 3 (Phase 5)**: Depends on US1 (reuses `addFromBestiary` and `useBestiary`)
|
|
- **User Story 4 (Phase 6)**: Depends on US2 (enhances `StatBlockPanel`)
|
|
- **Polish (Phase 7)**: Depends on all user stories
|
|
|
|
### User Story Dependencies
|
|
|
|
- **US1 (P1)**: Independent after Phase 2 — core MVP
|
|
- **US2 (P2)**: Best after US1 (wires into addFromBestiary flow), but StatBlock component itself is independent
|
|
- **US3 (P3)**: Depends on US1 (uses addFromBestiary and useBestiary)
|
|
- **US4 (P4)**: Depends on US2 (enhances StatBlockPanel)
|
|
|
|
### Parallel Opportunities
|
|
|
|
**Within Phase 1**: T002 and T003 can run in parallel (different concerns in same file, but logically separable)
|
|
**Within Phase 2**: T005+T006 (strip-tags) and T009+T010 (combatant extension) can run in parallel
|
|
**Within Phase 3**: T011 (auto-number) and T012 (export) can run in parallel with T013 (hook)
|
|
**Within Phase 4**: T017 (StatBlock) and T018 (StatBlockPanel) can run in parallel
|
|
|
|
---
|
|
|
|
## Parallel Example: User Story 1
|
|
|
|
```bash
|
|
# These can run in parallel (different files, no dependencies):
|
|
Task T011: "Implement resolveCreatureName in packages/domain/src/auto-number.ts"
|
|
Task T013: "Implement useBestiary hook in apps/web/src/hooks/use-bestiary.ts"
|
|
|
|
# Then sequentially:
|
|
Task T015: "Add addFromBestiary to useEncounter (depends on T011 + T013)"
|
|
Task T014: "Create BestiarySearch component (depends on T013 for search)"
|
|
Task T016: "Modify ActionBar to integrate BestiarySearch (depends on T014 + T015)"
|
|
```
|
|
|
|
## Parallel Example: User Story 2
|
|
|
|
```bash
|
|
# These can run in parallel (different files):
|
|
Task T017: "Create StatBlock component in apps/web/src/components/stat-block.tsx"
|
|
Task T018: "Create StatBlockPanel wrapper in apps/web/src/components/stat-block-panel.tsx"
|
|
|
|
# Then sequentially:
|
|
Task T019: "Add stat block state to App.tsx (depends on T017 + T018)"
|
|
Task T020: "Wire creature selection (depends on T019)"
|
|
Task T021: "Add stat block CSS (depends on T017)"
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Strategy
|
|
|
|
### MVP First (User Story 1 Only)
|
|
|
|
1. Complete Phase 1: Setup (T001-T004)
|
|
2. Complete Phase 2: Foundational (T005-T010)
|
|
3. Complete Phase 3: User Story 1 (T011-T016)
|
|
4. **STOP and VALIDATE**: Search for creatures, add with pre-fill, verify auto-numbering
|
|
5. This alone delivers significant value — bestiary-assisted combatant creation
|
|
|
|
### Incremental Delivery
|
|
|
|
1. Setup + Foundational -> Foundation ready
|
|
2. Add User Story 1 -> Test independently (MVP: search + add + pre-fill)
|
|
3. Add User Story 2 -> Test independently (stat block display)
|
|
4. Add User Story 3 -> Test independently (inline suggestions)
|
|
5. Add User Story 4 -> Test independently (responsive layout)
|
|
6. Polish -> Full merge gate validation
|
|
|
|
### Recommended Order
|
|
|
|
Sequential implementation in priority order (P1 -> P2 -> P3 -> P4) is recommended since each story builds on the previous one's infrastructure.
|