# 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** |