Implement the 013-hp-status-indicators feature that adds visual HP status indicators to combatant rows with a pure domain function deriving bloodied/unconscious states

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-05 23:29:24 +01:00
parent 7d440677be
commit 97d3918cef
12 changed files with 550 additions and 2 deletions

View File

@@ -0,0 +1,36 @@
# Specification Quality Checklist: HP Status Indicators
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-05
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
- FR-001 defines the exact thresholds for each status, making the boundary conditions fully testable.
- Edge cases cover odd max HP values, min max HP (1 and 2), over-max HP, and unset max HP.

View File

@@ -0,0 +1,46 @@
# Data Model: HP Status Indicators
**Feature**: 013-hp-status-indicators
**Date**: 2026-03-05
## Entities
### HpStatus (derived type -- not stored)
A discriminated string union representing the health state of a combatant.
| Value | Condition | Description |
|-------|-----------|-------------|
| `"healthy"` | `currentHp >= maxHp / 2` | Combatant is above half HP |
| `"bloodied"` | `0 < currentHp < maxHp / 2` | Combatant is below half HP but still conscious |
| `"unconscious"` | `currentHp <= 0` | Combatant is at or below zero HP |
**Derivation rules**:
- Only meaningful when both `currentHp` and `maxHp` are defined
- Returns `undefined` when either value is missing (HP tracking not enabled)
- Evaluation order: check `unconscious` first (currentHp <= 0), then `bloodied` (currentHp < maxHp / 2), else `healthy`
### Combatant (existing -- unchanged)
No modifications to the `Combatant` interface. HP status is computed on demand from existing fields:
| Field | Type | Used for status |
|-------|------|----------------|
| `currentHp` | `number \| undefined` | Compared against thresholds |
| `maxHp` | `number \| undefined` | Defines the "half HP" bloodied threshold |
## State Transitions
HP status changes are a side effect of existing HP operations (adjustHp, setHp). No new state transitions are introduced.
```text
healthy ──[take damage below half]──> bloodied
bloodied ──[take damage to 0]──────> unconscious
unconscious ──[heal above 0]───────> bloodied or healthy
bloodied ──[heal above half]───────> healthy
healthy ──[take massive damage]────> unconscious (skip bloodied)
```
## Storage Impact
None. HP status is derived at render time from existing persisted `currentHp` and `maxHp` values. No localStorage schema changes.

View File

@@ -0,0 +1,64 @@
# Implementation Plan: HP Status Indicators
**Branch**: `013-hp-status-indicators` | **Date**: 2026-03-05 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/013-hp-status-indicators/spec.md`
## Summary
Add visual HP status indicators to combatant rows. A pure domain function derives the HP status ("healthy", "bloodied", "unconscious") from currentHp and maxHp. The UI applies color treatments: amber for bloodied HP text, red + muted row for unconscious/dead combatants. No new stored state -- status is computed on render.
## Technical Context
**Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax)
**Primary Dependencies**: React 19, Vite 6, Tailwind CSS v4, shadcn/ui-style components, Lucide React (icons)
**Storage**: N/A (no storage changes -- purely derived state, existing localStorage persistence unchanged)
**Testing**: Vitest (unit tests for domain function, existing layer boundary test)
**Target Platform**: Modern browsers (Vite dev server, production build)
**Project Type**: Web application (monorepo: domain + application + web adapter)
**Performance Goals**: Synchronous render -- indicator updates in the same React render cycle as HP changes
**Constraints**: Domain layer must remain pure (no UI imports); adapter layer handles all visual presentation
**Scale/Scope**: Single new domain function + UI changes to one component (combatant-row)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Deterministic Domain Core | PASS | `deriveHpStatus` is a pure function: same inputs always produce the same output. No I/O, randomness, or clocks. |
| II. Layered Architecture | PASS | Domain exports the pure function; UI adapter calls it in the component. No reverse dependency. |
| III. Agent Boundary | N/A | No agent involvement in this feature. |
| IV. Clarification-First | PASS | No ambiguity -- thresholds are defined in spec (bloodied = 0 < currentHp < maxHp/2, unconscious = currentHp <= 0). |
| V. Escalation Gates | PASS | All behavior is within spec scope. |
| VI. MVP Baseline Language | PASS | Assumptions section uses "MVP baseline does not include" language. |
| VII. No Gameplay Rules in Constitution | PASS | Bloodied/unconscious thresholds are in feature spec, not constitution. |
| Development Workflow | PASS | `pnpm check` must pass before commit. Domain testable via pure-function assertions. |
No violations. Complexity Tracking section not needed.
## Project Structure
### Documentation (this feature)
```text
specs/013-hp-status-indicators/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
└── tasks.md # Phase 2 output (/speckit.tasks command)
```
### Source Code (repository root)
```text
packages/domain/src/
├── hp-status.ts # NEW: deriveHpStatus pure function + HpStatus type
├── __tests__/hp-status.test.ts # NEW: unit tests for deriveHpStatus
├── index.ts # MODIFIED: export new function and type
apps/web/src/components/
├── combatant-row.tsx # MODIFIED: apply status-based styling
```
**Structure Decision**: Follows existing monorepo layout. New domain function in its own module (consistent with adjust-hp.ts, set-hp.ts pattern). UI changes confined to the existing combatant-row component. No new application-layer code needed since status is derived at render time, not via a use case.

View File

@@ -0,0 +1,46 @@
# Quickstart: HP Status Indicators
**Feature**: 013-hp-status-indicators
## Overview
Add a pure domain function `deriveHpStatus` and apply visual status indicators (color treatments) to combatant rows in the encounter tracker.
## Key Files
| File | Action | Purpose |
|------|--------|---------|
| `packages/domain/src/hp-status.ts` | CREATE | Pure function `deriveHpStatus` + `HpStatus` type |
| `packages/domain/src/__tests__/hp-status.test.ts` | CREATE | Unit tests for all status thresholds and edge cases |
| `packages/domain/src/index.ts` | MODIFY | Export `deriveHpStatus` and `HpStatus` |
| `apps/web/src/components/combatant-row.tsx` | MODIFY | Call `deriveHpStatus` and apply conditional CSS classes |
## Implementation Steps
1. **Domain function** (`packages/domain/src/hp-status.ts`):
- Define `type HpStatus = "healthy" | "bloodied" | "unconscious"`
- Implement `deriveHpStatus(currentHp: number | undefined, maxHp: number | undefined): HpStatus | undefined`
- Return `undefined` if either input is `undefined`
- Check `currentHp <= 0` first (unconscious), then `currentHp < maxHp / 2` (bloodied), else healthy
2. **Tests** (`packages/domain/src/__tests__/hp-status.test.ts`):
- Healthy: currentHp >= maxHp/2
- Bloodied: 0 < currentHp < maxHp/2
- Unconscious: currentHp <= 0 (including negative)
- Undefined inputs: returns undefined
- Edge cases: maxHp=1, maxHp=2, odd maxHp, currentHp > maxHp
3. **Export** (`packages/domain/src/index.ts`):
- Add `export { type HpStatus, deriveHpStatus } from "./hp-status.js"`
4. **UI styling** (`apps/web/src/components/combatant-row.tsx`):
- Import `deriveHpStatus` from `@initiative/domain`
- Call it with combatant's `currentHp` and `maxHp`
- Apply to HP text: `text-amber-400` for bloodied, `text-red-400` for unconscious
- Apply to row: `opacity-50` for unconscious combatants
## Validation
```bash
pnpm check # Must pass (knip + format + lint + typecheck + test)
```

View File

@@ -0,0 +1,60 @@
# Research: HP Status Indicators
**Feature**: 013-hp-status-indicators
**Date**: 2026-03-05
## R1: HP Status Derivation Approach
**Decision**: Implement as a pure domain function `deriveHpStatus(currentHp, maxHp)` returning a discriminated union type `HpStatus = "healthy" | "bloodied" | "unconscious"`.
**Rationale**:
- The status is a derived value, not stored state. Computing it on demand avoids synchronization issues and keeps the data model unchanged.
- A standalone pure function in the domain layer is consistent with the project's architecture (e.g., `adjustHp`, `setHp` are each in their own module).
- Returning a simple string union (not an object or class) is the lightest representation and easy to pattern-match in the UI.
**Alternatives considered**:
- Storing status as a field on `Combatant`: Rejected -- introduces redundant state that must stay in sync with HP changes. Violates principle of derived data.
- Computing in the UI layer only: Rejected -- violates constitution principle I (domain logic must be pure domain functions) and makes the logic untestable without a UI.
## R2: Bloodied Threshold
**Decision**: Bloodied when `0 < currentHp < maxHp / 2` (strict less-than for both bounds, floating-point division).
**Rationale**:
- Follows D&D 4e/5e convention where "bloodied" means below half HP.
- Using strict less-than means at exactly half HP the combatant is still "healthy" -- this is the most common TTRPG interpretation.
- Floating-point division handles odd maxHp values correctly (e.g., maxHp=21: bloodied at 10 since 10 < 10.5).
**Alternatives considered**:
- `currentHp <= maxHp / 2`: Would make combatants at exactly half HP appear bloodied. Less standard.
- `Math.floor(maxHp / 2)`: Would make maxHp=21 bloodied at 10 (same result) but maxHp=20 bloodied at 10 (also same). No practical difference but adds unnecessary complexity.
## R3: Visual Treatment Approach
**Decision**: Color the HP text and optionally mute the entire row for unconscious combatants:
- **Healthy**: Default foreground color (no change)
- **Bloodied**: Amber/orange HP text (`text-amber-400`) -- warm danger signal
- **Unconscious**: Red HP text (`text-red-400`) + reduced row opacity (`opacity-50`) -- clearly out of action
**Rationale**:
- Coloring the HP number is the most targeted indicator -- it draws the eye to the relevant data point.
- Row opacity reduction for unconscious combatants provides a strong "out of action" signal without removing the row (GM may still need to reference it).
- Amber and red are universally understood warning/danger colors and already used in the project (red for damage mode in quick-hp-input, `--color-destructive: #ef4444`).
- No new icons needed -- color alone is sufficient for two states and avoids visual clutter.
**Alternatives considered**:
- Full row background color: Too visually heavy, would conflict with the active-turn indicator (blue left border + bg-accent/10).
- Strikethrough on name for unconscious: Considered but opacity reduction is more modern and less ambiguous (strikethrough could imply "removed").
- Badge/pill indicator: Adds UI elements and visual weight. Color on existing text is simpler.
## R4: Function Signature & Return for Undefined HP
**Decision**: Return `undefined` when maxHp is not set (status is not applicable). Function signature: `deriveHpStatus(currentHp: number | undefined, maxHp: number | undefined): HpStatus | undefined`.
**Rationale**:
- When maxHp is undefined, HP tracking is disabled for that combatant. No status can be derived.
- Returning `undefined` (not a fourth status value) keeps the type clean and matches the "no HP tracking" semantic.
- The UI simply skips styling when the result is `undefined`.
**Alternatives considered**:
- Separate `"none"` status: Adds a value that is semantically different from the three health states. `undefined` is more idiomatic TypeScript for "not applicable".

View File

@@ -0,0 +1,98 @@
# Feature Specification: HP Status Indicators
**Feature Branch**: `013-hp-status-indicators`
**Created**: 2026-03-05
**Status**: Draft
**Input**: User description: "When a combatant's health is lower than half, the creature is considered bloodied, which makes the UI show an indicator. If a combatant is at <=0 HP the creature is unconscious/dead, which should also be shown. Visual treatment at implementer's discretion to fit the existing modern UI style."
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Bloodied Indicator (Priority: P1)
As a game master running an encounter, I want to see at a glance which combatants are bloodied (below half their maximum HP) so I can narrate the battle's intensity and make tactical decisions without mental math.
**Why this priority**: The bloodied condition is a core D&D/TTRPG concept that GMs check frequently during combat. Providing a visual indicator eliminates the need to compare current HP vs max HP for every combatant each round.
**Independent Test**: Can be fully tested by setting a combatant's max HP and reducing current HP below half, then verifying the visual indicator appears. Delivers immediate at-a-glance awareness.
**Acceptance Scenarios**:
1. **Given** a combatant with max HP set to 20 and current HP at 10, **When** the combatant row is displayed, **Then** the HP area shows a bloodied visual indicator (e.g., amber/orange color treatment on the HP value)
2. **Given** a combatant with max HP set to 20 and current HP at 9, **When** the combatant row is displayed, **Then** the bloodied indicator is visible
3. **Given** a combatant with max HP set to 20 and current HP at 11, **When** the combatant row is displayed, **Then** no bloodied indicator is shown (normal appearance)
4. **Given** a combatant with max HP set to 21 (odd number) and current HP at 10, **When** the combatant row is displayed, **Then** the bloodied indicator is shown (10 < 21/2 = 10.5)
5. **Given** a combatant with no max HP set, **When** the combatant row is displayed, **Then** no status indicator is shown regardless of current HP value
---
### User Story 2 - Unconscious/Dead Indicator (Priority: P1)
As a game master, I want to see which combatants have reached 0 HP or below so I can immediately identify who is down without scanning HP numbers.
**Why this priority**: Equally critical to bloodied -- knowing who is unconscious or dead is essential for combat flow and takes priority over other status information.
**Independent Test**: Can be fully tested by reducing a combatant's current HP to 0 or below and verifying the visual indicator appears.
**Acceptance Scenarios**:
1. **Given** a combatant with max HP set to 20 and current HP at 0, **When** the combatant row is displayed, **Then** the HP area shows an unconscious/dead visual indicator (e.g., red color treatment and/or a skull/death icon)
2. **Given** a combatant with max HP set to 20 and current HP at -5, **When** the combatant row is displayed, **Then** the unconscious/dead indicator is shown (negative HP still counts as down)
3. **Given** a combatant with max HP set to 20 and current HP at 1, **When** the combatant row is displayed, **Then** the unconscious/dead indicator is NOT shown (1 HP is bloodied, not dead)
4. **Given** a combatant at 0 HP whose HP is then healed above 0, **When** the HP changes, **Then** the unconscious/dead indicator is removed and replaced with the appropriate status (bloodied or healthy)
---
### User Story 3 - Status Transitions (Priority: P2)
As a game master adjusting HP during combat, I want the visual indicators to update in real time as I apply damage or healing so I always have an accurate picture of the battlefield.
**Why this priority**: Indicator transitions are a natural consequence of the first two stories but need explicit attention to ensure smooth, responsive visual feedback.
**Independent Test**: Can be tested by using the quick HP input to deal damage across thresholds and observing indicator changes.
**Acceptance Scenarios**:
1. **Given** a combatant at full HP (20/20), **When** I deal 11 damage (reducing to 9/20), **Then** the indicator transitions from healthy to bloodied
2. **Given** a bloodied combatant (5/20), **When** I deal 5 damage (reducing to 0/20), **Then** the indicator transitions from bloodied to unconscious/dead
3. **Given** an unconscious combatant (0/20), **When** I heal 15 HP (restoring to 15/20), **Then** the indicator transitions from unconscious/dead to healthy (skipping bloodied since 15 > 10)
---
### Edge Cases
- What happens when max HP is 1? At 1/1 HP the combatant is healthy; at 0/1 HP they are unconscious/dead. There is no bloodied state since 0 < 0.5 rounds to unconscious/dead.
- What happens when max HP is 2? At 1/2 HP the combatant is healthy (1 is not less than 2/2 = 1); at 0/2 they are unconscious/dead. Bloodied only applies when currentHp < maxHp / 2 (strict less-than with exact division).
- What happens when current HP exceeds max HP (e.g., temporary HP or manual override)? The combatant shows healthy status -- no special indicator for over-max.
- What happens when max HP is set but current HP has not been modified? Current HP equals max HP by default, so the combatant shows healthy status.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST derive a combatant's HP status from their current HP and max HP values: "healthy" (currentHp >= maxHp / 2), "bloodied" (0 < currentHp < maxHp / 2), or "unconscious" (currentHp <= 0)
- **FR-002**: System MUST display a visual indicator on the combatant row when a combatant is bloodied -- the HP value text MUST use a warm/amber color to signal danger
- **FR-003**: System MUST display a visual indicator on the combatant row when a combatant is unconscious/dead -- the HP value text MUST use a red/destructive color and the row MUST appear visually muted (reduced opacity or strikethrough name) to signal the combatant is out of action
- **FR-004**: System MUST NOT show any status indicator when max HP is not set for a combatant
- **FR-005**: System MUST update the visual indicator immediately when current HP changes (no delay or animation lag)
- **FR-006**: The HP status derivation MUST be a pure domain-level computation with no UI dependencies
### Key Entities
- **HP Status**: A derived value (not stored) computed from a combatant's currentHp and maxHp. Possible values: "healthy", "bloodied", "unconscious". Only meaningful when maxHp is defined.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can identify bloodied combatants at a glance without reading HP numbers -- 100% of combatants below half HP display a distinct visual treatment
- **SC-002**: Users can identify unconscious/dead combatants at a glance -- 100% of combatants at 0 HP or below display a distinct visual treatment that differs from the bloodied indicator
- **SC-003**: Visual indicators update within the same interaction frame as HP changes -- no perceptible delay between HP adjustment and indicator update
- **SC-004**: Status indicators are visually consistent with the existing UI design system (colors, spacing, typography match the established style)
## Assumptions
- "Bloodied" uses the D&D 4e/5e convention: strictly below half max HP (not at exactly half)
- The unconscious/dead label is intentionally combined into one state -- the system does not distinguish between unconscious and dead (this is a GM decision, not a system decision)
- No sound effects or animation beyond color/opacity changes for MVP baseline
- MVP baseline does not include a tooltip or label explaining what "bloodied" means
- The status is purely visual/derived -- it does not affect game mechanics or stored data

View File

@@ -0,0 +1,122 @@
# Tasks: HP Status Indicators
**Input**: Design documents from `/specs/013-hp-status-indicators/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
**Tests**: Included -- domain function requires unit tests per project convention.
**Organization**: Tasks grouped by user story. US1 and US2 share the same domain function (foundational), then diverge at the UI layer.
## 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 Function + Tests)
**Purpose**: Create the pure domain function that all user stories depend on
**CRITICAL**: No UI work can begin until this phase is complete
- [x] T001 Create `HpStatus` type and `deriveHpStatus` pure function in `packages/domain/src/hp-status.ts` -- returns `"healthy" | "bloodied" | "unconscious" | undefined` based on `currentHp` and `maxHp` inputs. Check `currentHp <= 0` first (unconscious), then `currentHp < maxHp / 2` (bloodied), else healthy. Return `undefined` when either input is `undefined`.
- [x] T002 Write unit tests for `deriveHpStatus` in `packages/domain/src/__tests__/hp-status.test.ts` covering: healthy (currentHp >= maxHp/2), bloodied (0 < currentHp < maxHp/2), unconscious (currentHp <= 0), unconscious with negative HP, undefined when maxHp undefined, undefined when currentHp undefined, undefined when both undefined, edge case maxHp=1 (no bloodied state possible), edge case maxHp=2 (1/2 is healthy not bloodied), odd maxHp=21 (10 is bloodied since 10 < 10.5), currentHp exceeding maxHp (healthy).
- [x] T003 Export `deriveHpStatus` and `HpStatus` type from `packages/domain/src/index.ts` -- add `export { type HpStatus, deriveHpStatus } from "./hp-status.js";`
**Checkpoint**: `pnpm check` passes. Domain function is tested and exported.
---
## Phase 2: User Story 1 - Bloodied Indicator (Priority: P1) + User Story 2 - Unconscious/Dead Indicator (Priority: P1)
**Goal**: Apply visual status indicators to the combatant row -- amber HP text for bloodied, red HP text + muted row for unconscious/dead
**Independent Test**: Set a combatant's max HP, reduce current HP below half -- verify amber text. Reduce to 0 -- verify red text and muted row. Heal above half -- verify normal appearance restored.
### Implementation
- [x] T004 [US1] [US2] Modify `apps/web/src/components/combatant-row.tsx` to import `deriveHpStatus` from `@initiative/domain` and call it with the combatant's `currentHp` and `maxHp` at the top of the `CombatantRow` component. Store result in a `status` variable.
- [x] T005 [US1] [US2] In `apps/web/src/components/combatant-row.tsx`, apply conditional CSS classes based on `status`: (1) On the `CurrentHpInput` wrapper/value -- add `text-amber-400` when bloodied, `text-red-400` when unconscious. (2) On the row's outer `<div>` -- add `opacity-50` when unconscious. Ensure the active-turn border styling still takes precedence for active combatants. Use the existing `cn()` utility for conditional class merging.
**Checkpoint**: `pnpm check` passes. Bloodied and unconscious indicators are visible in the UI.
---
## Phase 3: User Story 3 - Status Transitions (Priority: P2)
**Goal**: Ensure visual indicators update in real time as HP changes through quick HP input, direct entry, and +/- controls
**Independent Test**: Use the quick HP input to deal damage across thresholds (healthy -> bloodied -> unconscious) and heal back. Verify indicator changes instantly with no flicker or delay.
### Implementation
- [x] T006 [US3] Verify in `apps/web/src/components/combatant-row.tsx` that `deriveHpStatus` is called with the current combatant props (not stale state) so status updates in the same render cycle as HP changes. No additional code should be needed if T004-T005 are implemented correctly -- this task is a manual verification and visual QA pass.
**Checkpoint**: All transitions (healthy->bloodied->unconscious->healthy) work smoothly.
---
## Phase 4: Polish & Cross-Cutting Concerns
- [x] T007 Run `pnpm check` to validate all automated checks pass (knip, format, lint, typecheck, test)
- [x] T008 Verify layer boundary test still passes (domain must not import from React/UI) in `packages/domain/src/__tests__/layer-boundaries.test.ts`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Foundational (Phase 1)**: No dependencies -- can start immediately
- **US1+US2 (Phase 2)**: Depends on Phase 1 completion (needs domain function)
- **US3 (Phase 3)**: Depends on Phase 2 completion (needs UI indicators in place)
- **Polish (Phase 4)**: Depends on all phases complete
### Within Phases
- T001 before T002 (need function to test it)
- T002 before T003 (tests pass before exporting)
- T004 before T005 (import before styling)
### Parallel Opportunities
- T001 and T002 can be done together if writing tests alongside implementation (TDD)
- US1 and US2 are implemented together in Phase 2 since they modify the same file and component
---
## Parallel Example: Phase 1
```bash
# T001 and T002 can be written together (TDD style):
Task: "Create deriveHpStatus in packages/domain/src/hp-status.ts"
Task: "Write tests in packages/domain/src/__tests__/hp-status.test.ts"
```
---
## Implementation Strategy
### MVP First (Phase 1 + Phase 2)
1. Complete Phase 1: Domain function + tests + export
2. Complete Phase 2: UI styling for both bloodied and unconscious
3. **STOP and VALIDATE**: Manually test all scenarios from spec
4. Run `pnpm check`
### Full Delivery
1. Phase 1: Domain function (T001-T003)
2. Phase 2: UI indicators (T004-T005)
3. Phase 3: Transition verification (T006)
4. Phase 4: Final validation (T007-T008)
---
## Notes
- US1 (bloodied) and US2 (unconscious) are combined into Phase 2 because they modify the same component and the domain function handles both statuses. Implementing them separately would require touching the same lines twice.
- T006 is a verification task, not a code change -- if the React component re-renders on prop changes (which it does by default), transitions work automatically.
- Total: 8 tasks across 4 phases. Minimal scope -- no new application layer, no storage changes.