neon-sprawl/docs/plans/NEO-110-implementation-plan.md

162 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEO-110 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-110 |
| **Title** | E5M3-11: Client encounter progress + loot feedback HUD |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-110/e5m3-11-client-encounter-progress-loot-feedback-hud |
| **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-11** |
| **Branch** | `NEO-110-client-encounter-progress-loot-feedback-hud` |
| **Server deps** | [NEO-108](https://linear.app/neon-sprawl/issue/NEO-108) — `GET /game/players/{id}/encounter-progress` (**Done** on `main`); [NEO-107](https://linear.app/neon-sprawl/issue/NEO-107) — complete event + grant summary (**Done** on `main`) |
| **Pattern** | [NEO-72](NEO-72-implementation-plan.md) / [NEO-86](NEO-86-implementation-plan.md) — thin HTTP client, `main.gd` HUD render, defeat-triggered refresh; [NEO-73](NEO-73-implementation-plan.md) — inventory refresh after server-side success |
| **Blocks** | [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) — playable encounter clear capstone (Godot manual QA) |
| **Server counterpart** | [NEO-108](https://linear.app/neon-sprawl/issue/NEO-108) — authoritative GET; Bruno is not prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) |
## Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Refresh strategy** | When to call encounter-progress GET? | **Boot hydrate + refresh after each cast with `targetDefeated`** — mirrors NEO-86 gig refresh; no periodic poll (NEO-108 deferred cadence to this story). | **Adopted** — boot + after `targetDefeated` |
| **Progress denominator** | How to show **2/3** targets? | **Hardcode 3** for frozen **`prototype_combat_pocket`** — single prototype encounter; avoids new encounter-definitions client (NEO-103 server-only). | **Adopted** — constant `3` |
| **Grant display names** | Loot lines on completion label? | **Reuse `ItemDefinitionsClient.display_name_for`** — NEO-72 precedent; fallback raw **`itemId`**. | **Adopted** — reuse item defs |
| **HUD placement** | Where do the two labels live? | **`UICanvas/HudRoot`** after combat HP labels — visible during Tab/cast loop (not buried in economy collapse). | **Adopted** — HudRoot combat area |
| **Manual refresh key** | Dedicated input action? | **Skip** — defeat-driven refresh + boot suffice for AC; NEO-111 capstone is the full Godot script. | **Adopted** (implicit — no new key) |
## Goal, scope, and out-of-scope
**Goal:** Godot shows **server-owned** encounter progress and completion loot using **`GET /game/players/{id}/encounter-progress`** — player-visible **N/3** defeat progress after each NPC kill and a completion grant summary matching the GET body.
**In scope (from Linear + [E5M3-11](E5M3-prototype-backlog.md#e5m3-11--client-encounter-progress-loot-feedback-hud)):**
- **`encounter_progress_client.gd`:** GET snapshot; parse v1 envelope; **`encounter_row(encounter_id)`** helper; signals **`encounter_progress_received`** / **`encounter_sync_failed`**; public **`request_sync_from_server()`**; **`push_warning`** on failures (NEO-72 pattern).
- **`EncounterProgressLabel`** + **`EncounterCompleteLabel`** under **`UICanvas/HudRoot`** (after **`PlayerCombatHpLabel`**).
- **`main.gd`:** boot hydrate; on accepted cast with **`targetDefeated: true`** → encounter-progress GET (alongside existing gig/combat refreshes); render progress **`defeatedTargetIds.size() / 3`** for **`prototype_combat_pocket`** when **`state`** is **`inactive`** or **`active`**; on **`completed`** paint grant summary from **`rewardGrantSummary`** and **`_request_inventory_refresh()`** (NEO-72 inventory HUD).
- GdUnit tests with HTTP doubles ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)).
- **`docs/manual-qa/NEO-110.md`** — Godot steps (server + client; no Bruno).
- **`client/README.md`** encounter HUD subsection.
**Out of scope (from Linear + backlog):**
- Final loot VFX art; encounter picker UI beyond frozen pocket.
- **`GET /game/world/encounter-definitions`** client (denominator hardcoded).
- Periodic encounter poll timer; dedicated manual-refresh key.
- Server route/DTO changes.
- **`docs/manual-qa/NEO-111.md`** (capstone story).
## Acceptance criteria checklist
- [x] Player-visible progress updates after each NPC defeat (e.g. **2/3** targets).
- [x] Completion label shows grant summary matching server GET.
- [x] Manual QA checklist exercisable without Bruno.
## Implementation reconciliation (shipped)
**Manual QA:** [`docs/manual-qa/NEO-110.md`](../manual-qa/NEO-110.md) — passed (Godot defeat chain, progress **1/3 → 2/3 → completed**, loot + inventory). **PR #149** merged to `main`.
- **`encounter_progress_client.gd`:** GET parse v1; **`encounter_row`**; **`encounter_progress_received`** / **`encounter_sync_failed`**.
- **`main.gd`:** boot + **`targetDefeated`** refresh; HUD render; inventory refresh on **`completed`**; item-defs re-paint when defs arrive.
- **Scene:** **`EncounterProgressClient`**, **`EncounterProgressLabel`**, **`EncounterCompleteLabel`** in **`HudRoot`** after **`PlayerCombatHpLabel`**.
- **Tests:** `client/test/encounter_progress_client_test.gd` (inactive/active/completed parse + 404).
- **Docs:** `client/README.md` encounter section; `docs/manual-qa/NEO-110.md`.
## Technical approach
### Server contract (landed — NEO-108)
- **`GET /game/players/{id}/encounter-progress`** → **`schemaVersion` 1**, **`encounters[]`** rows with **`encounterId`**, **`state`** (`inactive` | `active` | `completed`), **`defeatedTargetIds`**, optional **`completedAt`**, optional **`rewardGrantSummary`** (`{ itemId, quantity }[]`).
- Prototype row **`prototype_combat_pocket`**: inactive → **0/3**; after defeats → **`active`** with growing **`defeatedTargetIds`**; all three E5.M2 NPCs defeated → **`completed`** with **`scrap_metal_bulk` ×10**, **`contract_handoff_token` ×1**.
### Client constants (`main.gd`)
```gdscript
const PROTOTYPE_ENCOUNTER_ID := "prototype_combat_pocket"
const PROTOTYPE_ENCOUNTER_REQUIRED_TARGETS := 3
```
### 1. `encounter_progress_client.gd`
- Mirror **`gig_progression_client.gd`**: injectable HTTP, **`_busy`** guard, **`GET …/encounter-progress`**.
- **`parse_encounter_progress_json(text)`** static: require **`schemaVersion` 1**, **`encounters`** array.
- **`encounter_row(encounter_id, snapshot)`** → row dict or `{}`.
- Signals: **`encounter_progress_received(snapshot)`**, **`encounter_sync_failed(reason)`**.
### 2. Boot + refresh wiring (`main.gd`)
- **`_setup_encounter_progress_sync()`** from **`_ready()`** after authority HTTP config (same as gig/inventory).
- **`EncounterProgressClient`** node in **`main.tscn`**; **`_apply_authority_http_config_to_client`**.
- Boot: **`request_sync_from_server()`** once catalogs/inventory chain has started (parallel with gig boot is fine).
- **`_on_cast_result_received`:** when **`accepted`** and **`resolution.targetDefeated`** → **`_request_encounter_progress_refresh()`** (in addition to existing cooldown/combat/gig refreshes).
### 3. HUD rendering
**`EncounterProgressLabel`** (header **`Encounter:`**):
| **`state`** | Copy |
|-------------|------|
| **`inactive`** | **`prototype_combat_pocket: not started (0/3)`** |
| **`active`** | **`prototype_combat_pocket: {n}/3`** where **`n = defeatedTargetIds.size()`** |
| **`completed`** | **`prototype_combat_pocket: completed (3/3)`** |
| sync error | **`Encounter: error — {reason}`** |
**`EncounterCompleteLabel`** (header **`Loot:`**):
| **`state`** | Copy |
|-------------|------|
| not **`completed`** | **`Loot: —`** |
| **`completed`** | One line per **`rewardGrantSummary`** grant: **`{displayName or itemId} ×{quantity}`**; optional **`completedAt`** suffix on header line |
| sync error | **`Loot: error — {reason}`** (or leave progress label as primary error — prefer single error on progress label, complete shows **`—`**) |
**Inventory:** on **`encounter_progress_received`**, if prototype row **`state == "completed"`** → **`_request_inventory_refresh()`** so **`InventoryLabel`** reflects grants without manual **I** (backlog + NEO-73 precedent).
### 4. Scene
- **`main.tscn`:** add **`EncounterProgressClient`** node; **`EncounterProgressLabel`** + **`EncounterCompleteLabel`** as **`HudRoot`** children **after** **`PlayerCombatHpLabel`**, **before** **`NpcStateLabel`** (VBox order).
### 5. Tests (GdUnit, AAA)
- Happy path: v1 JSON with one **`active`** row, two defeats → **`encounter_row`** returns **`defeatedTargetIds.size() == 2`**.
- **`completed`** row includes **`rewardGrantSummary`** array parse.
- 404 → **`encounter_sync_failed`** + **`push_warning`**.
- Optional: static/format helper test for **`main`** render strings (only if extracted to **`encounter_progress_hud_format.gd`** — prefer keeping format in **`main.gd`** unless tests need it; client test focuses on HTTP client).
### 6. Docs on land
- **`docs/manual-qa/NEO-110.md`**: fresh server; Godot **F5**; Tab + cast three prototype NPCs; assert progress **1/3 → 2/3 → 3/3**; completion loot lines match GET; inventory shows grants without **I**.
- Update [E5_M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) client slice note, [E5M3-prototype-backlog.md](E5M3-prototype-backlog.md) E5M3-11 checkboxes, [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) when implementation completes.
## Files to add
| Path | Purpose |
|------|---------|
| `docs/plans/NEO-110-implementation-plan.md` | This plan. |
| `client/scripts/encounter_progress_client.gd` | GET encounter-progress; parse v1; **`encounter_row`** helper; signals. |
| `client/scripts/encounter_progress_client.gd.uid` | Godot uid companion (tracked). |
| `client/test/encounter_progress_client_test.gd` | GdUnit: parse active/completed rows; 404 failure signal. |
| `docs/manual-qa/NEO-110.md` | Godot manual QA (server + client; defeat chain + HUD assertions). |
## Files to modify
| Path | Rationale |
|------|-----------|
| `client/scenes/main.tscn` | Add **`EncounterProgressClient`**, **`EncounterProgressLabel`**, **`EncounterCompleteLabel`** nodes. |
| `client/scripts/main.gd` | Boot/refresh wiring, defeat-triggered GET, HUD render, inventory refresh on **`completed`**. |
| `client/README.md` | Encounter progress + loot HUD subsection: server dep NEO-108, refresh triggers, label format. |
## Tests
| Test file | What it covers |
|-----------|----------------|
| `client/test/encounter_progress_client_test.gd` | Mock 200: **`inactive`** row parse; **`active`** with two **`defeatedTargetIds`**; **`completed`** + **`rewardGrantSummary`**; 404 → **`encounter_sync_failed`**. AAA layout (`# Arrange` / `# Act` / `# Assert`). |
No new **C#** tests (client-only; server covered by **`EncounterProgressApiTests`**). No Bruno changes.
## Open questions / risks
| Item | Agent recommendation | Status |
|------|----------------------|--------|
| **Concurrent GETs on triple-defeat frame** | **`_busy`** guard drops overlap — last completed response wins; acceptable for prototype. | **adopted** |
| **Item defs not loaded before completion render** | Re-render encounter labels when **`definitions_ready`** fires if snapshot already **`completed`** (mirror NEO-72 inventory re-paint). | **adopted**`_on_item_definitions_ready` re-paints encounter HUD |
| **NEO-111 capstone overlap** | NEO-110 manual QA is HUD-focused; NEO-111 adds full loop + idempotency — keep scopes separate. | **deferred** |