Implement the 016-combatant-ac feature that adds an optional Armor Class field to combatants with shield icon display and inline editing in the encounter tracker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-06 10:41:56 +01:00
parent 2793a66672
commit 78c6591973
20 changed files with 914 additions and 4 deletions

View File

@@ -0,0 +1,152 @@
# Tasks: Combatant Armor Class Display
**Input**: Design documents from `/specs/016-combatant-ac/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
**Tests**: Domain tests and persistence round-trip tests are included (consistent with existing testing patterns in the codebase).
**Organization**: Tasks are grouped by user story. US1 (set AC) and US2 (display AC) are both P1 and share foundational work, so they are combined into a single phase. US3 (edit AC) is P2 but is naturally fulfilled by the same inline input from US1/US2.
## 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: Foundational (Domain + Application Layer)
**Purpose**: Add AC to the domain model, create the `setAc` pure function, event type, and application use case. All user stories depend on this phase.
**⚠️ CRITICAL**: No UI or persistence work can begin until this phase is complete.
- [x] T001 [P] Add `readonly ac?: number` to `Combatant` interface and add `AcSet` event interface to the `DomainEvent` union in `packages/domain/src/types.ts` and `packages/domain/src/events.ts`
- [x] T002 [P] Create `setAc` pure function in `packages/domain/src/set-ac.ts` following the `set-initiative.ts` pattern: find combatant by ID, validate AC is non-negative integer when defined (>= 0), return updated encounter with `AcSet` event or `DomainError`
- [x] T003 Write domain tests in `packages/domain/src/__tests__/set-ac.test.ts`: set AC to valid value, set AC to 0, clear AC (undefined), combatant not found error, negative AC error, non-integer AC error, AC unchanged preserves other fields
- [x] T004 Export `setAc`, `SetAcSuccess` type, and `AcSet` event type from `packages/domain/src/index.ts`
- [x] T005 Create `setAcUseCase` in `packages/application/src/set-ac-use-case.ts` following `set-initiative-use-case.ts` pattern, and export from `packages/application/src/index.ts`
**Checkpoint**: Domain function and use case ready. Run `pnpm test` — all tests should pass including new `set-ac.test.ts`.
---
## Phase 2: User Story 1 + 2 — Set and Display AC (Priority: P1) 🎯 MVP
**Goal**: Users can set AC for a combatant and see it displayed as a shield icon with the numeric value in the encounter list.
**Independent Test**: Add a combatant, set its AC via the inline input, and verify the shield icon + value appears next to the name. Verify combatants without AC show no icon.
### Implementation
- [x] T006 [P] [US1] Add AC rehydration validation in `apps/web/src/persistence/encounter-storage.ts`: validate `typeof entry.ac === "number" && Number.isInteger(entry.ac) && entry.ac >= 0`, attach to combatant object during deserialization (follow the `initiative` validation pattern)
- [x] T007 [P] [US1] Add AC round-trip tests in `apps/web/src/persistence/__tests__/encounter-storage.test.ts`: persist and load combatant with AC, persist without AC, reject invalid AC values (negative, non-integer, string)
- [x] T008 [US1] Add `setAc` callback to `apps/web/src/hooks/use-encounter.ts` following the `setInitiative` callback pattern: call `setAcUseCase`, check for domain error, append events
- [x] T009 [US1] [US2] Add `onSetAc` prop to `CombatantRowProps` and implement AC display + inline editable input in `apps/web/src/components/combatant-row.tsx`: Lucide `Shield` icon + numeric value between name and HP columns, inline input following `MaxHpInput` pattern (click to edit, blur/Enter to commit, empty clears), hidden when AC is `undefined`
- [x] T010 [US2] Wire `setAc` callback from `useEncounter` hook to `CombatantRow` `onSetAc` prop in the encounter list parent component (likely `apps/web/src/components/encounter-tracker.tsx` or equivalent)
**Checkpoint**: US1 + US2 fully functional. Run `pnpm check` — full merge gate should pass. Manually verify: add combatant, set AC inline, see shield icon + value. Clear AC, icon disappears.
---
## Phase 3: User Story 3 — Edit AC for Existing Combatant (Priority: P2)
**Goal**: Users can change or remove an existing combatant's AC value.
**Independent Test**: Set a combatant's AC to 15, then change it to 18 and verify the display updates. Clear the AC field and verify the shield icon disappears.
**Note**: This story is naturally fulfilled by the inline editable input implemented in Phase 2 (T009). This phase is a verification checkpoint — no additional code tasks are needed unless the inline input from T009 does not yet support editing existing values.
- [x] T011 [US3] Verify and confirm that the inline AC input in `apps/web/src/components/combatant-row.tsx` handles all US3 acceptance scenarios: changing AC from 15 to 18 updates display, clearing AC field removes shield icon, setting AC on a combatant that previously had none shows shield icon
**Checkpoint**: All user stories functional. Run `pnpm check` — full merge gate must pass.
---
## Phase 4: Polish & Cross-Cutting Concerns
**Purpose**: Final validation and cleanup.
- [x] T012 Run `pnpm check` to verify full merge gate passes (knip + format + lint + typecheck + test)
- [x] T013 Verify no regression in existing functionality: HP tracking, initiative sorting, turn navigation, combatant add/remove/rename all work with AC field present
---
## Dependencies & Execution Order
### Phase Dependencies
- **Foundational (Phase 1)**: No dependencies — can start immediately
- **US1+US2 (Phase 2)**: Depends on Phase 1 completion (domain function + use case must exist)
- **US3 (Phase 3)**: Depends on Phase 2 completion (inline input must exist)
- **Polish (Phase 4)**: Depends on Phase 3 completion
### Within Phase 1 (Foundational)
```
T001 (types + events) ──┐
├──→ T003 (tests) ──→ T004 (exports) ──→ T005 (use case)
T002 (setAc function) ──┘
```
T001 and T002 can run in parallel (different files). T003 depends on both. T004 depends on T002. T005 depends on T004.
### Within Phase 2 (US1+US2)
```
T006 (persistence) ──┐
T007 (storage tests) ┤
├──→ T009 (combatant row UI) ──→ T010 (wire to parent)
T008 (hook callback) ─┘
```
T006, T007, T008 can run in parallel (different files). T009 depends on T008. T010 depends on T009.
### Parallel Opportunities
- **Phase 1**: T001 and T002 in parallel (types.ts/events.ts vs set-ac.ts)
- **Phase 2**: T006, T007, and T008 in parallel (persistence vs tests vs hook)
---
## Parallel Example: Phase 1
```bash
# Launch in parallel (different files):
Task: "Add ac field to Combatant interface in packages/domain/src/types.ts and AcSet event in packages/domain/src/events.ts"
Task: "Create setAc pure function in packages/domain/src/set-ac.ts"
# Then sequentially:
Task: "Write domain tests in packages/domain/src/__tests__/set-ac.test.ts"
Task: "Export from packages/domain/src/index.ts"
Task: "Create use case in packages/application/src/set-ac-use-case.ts"
```
---
## Implementation Strategy
### MVP First (US1 + US2)
1. Complete Phase 1: Foundational (domain + application)
2. Complete Phase 2: US1 + US2 (persistence + hook + UI)
3. **STOP and VALIDATE**: Set AC on combatants, verify shield icon display
4. Run `pnpm check` — merge gate must pass
### Incremental Delivery
1. Phase 1 → Domain model and logic ready
2. Phase 2 → AC visible and settable in encounter list (MVP!)
3. Phase 3 → Verify editing works (should be automatic from Phase 2)
4. Phase 4 → Final polish and regression check
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- US1 and US2 are combined because they share all implementation work and are both P1
- US3 requires no new code — the inline input from US1/US2 inherently supports editing
- Follow `set-initiative.ts` as the primary pattern reference throughout
- Commit after each phase or logical group of tasks