162 lines
12 KiB
Markdown
162 lines
12 KiB
Markdown
# 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** |
|