Files
initiative/specs/021-bestiary-statblock/tasks.md

15 KiB

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

  • T001 Copy raw 5etools 2024 Monster Manual JSON into data/bestiary/xmm.json (download from 5etools-mirror-3 repo, commit as static asset)
  • 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
  • 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
  • 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

  • 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)
  • 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)
  • 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.
  • 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
  • T009 Add optional creatureId?: CreatureId field to Combatant interface in packages/domain/src/types.ts
  • 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

  • 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
  • T012 [P] [US1] Export resolveCreatureName from packages/domain/src/index.ts
  • 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.
  • 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.
  • 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.)
  • 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

  • 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).
  • 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.
  • 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.
  • 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.
  • 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

  • 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

  • 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

  • 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
  • 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

# 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

# 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

Sequential implementation in priority order (P1 -> P2 -> P3 -> P4) is recommended since each story builds on the previous one's infrastructure.