From 3128f36df9fa740e7e9942ce9d2d741bebcc1ae3 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 19:34:27 -0400 Subject: [PATCH] NEO-110: kickoff implementation plan for encounter HUD client. --- docs/plans/NEO-110-implementation-plan.md | 151 ++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/plans/NEO-110-implementation-plan.md diff --git a/docs/plans/NEO-110-implementation-plan.md b/docs/plans/NEO-110-implementation-plan.md new file mode 100644 index 0000000..9b7db47 --- /dev/null +++ b/docs/plans/NEO-110-implementation-plan.md @@ -0,0 +1,151 @@ +# 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 + +- [ ] Player-visible progress updates after each NPC defeat (e.g. **2/3** targets). +- [ ] Completion label shows grant summary matching server GET. +- [ ] Manual QA checklist exercisable without Bruno. + +## 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). | **pending** (implement with inventory pattern) | +| **NEO-111 capstone overlap** | NEO-110 manual QA is HUD-focused; NEO-111 adds full loop + idempotency — keep scopes separate. | **deferred** |