diff --git a/client/scripts/main.gd b/client/scripts/main.gd index ad9a7c1..29f8460 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -479,13 +479,9 @@ func _apply_authority_http_config_to_client(client: Node) -> void: func _on_item_definitions_ready(_definitions_by_id: Dictionary) -> void: _populate_craft_recipe_panel_if_ready() - if not _inventory_error.is_empty(): - return - if not _last_inventory_snapshot.is_empty(): + if _inventory_error.is_empty() and not _last_inventory_snapshot.is_empty(): _render_inventory_label() - if not _encounter_progress_error.is_empty(): - return - if not _last_encounter_progress_snapshot.is_empty(): + if _encounter_progress_error.is_empty() and not _last_encounter_progress_snapshot.is_empty(): _render_encounter_progress_labels() @@ -817,7 +813,7 @@ func _render_encounter_complete_label() -> void: var grant: Dictionary = grant_variant var item_id: String = str(grant.get("itemId", "")) var qty: int = int(grant.get("quantity", 0)) - var label: String = _item_display_name(item_id) if not item_id.is_empty() else "?" + var label: String = _inventory_item_label(item_id) if not item_id.is_empty() else "?" lines.append(" %s ×%d" % [label, qty]) if lines.size() == 1: lines.append(" (no grants)") diff --git a/client/test/encounter_progress_client_test.gd b/client/test/encounter_progress_client_test.gd index 2d8845d..46b8295 100644 --- a/client/test/encounter_progress_client_test.gd +++ b/client/test/encounter_progress_client_test.gd @@ -149,3 +149,26 @@ func test_http_404_emits_encounter_sync_failed() -> void: c.call("request_sync_from_server") # Assert await assert_signal(c).is_emitted("encounter_sync_failed", "HTTP 404 (player unknown)") + + +func test_parse_encounter_progress_json_returns_null_for_schema_mismatch() -> void: + # Arrange + var json := '{"schemaVersion":2,"playerId":"dev-local-1","encounters":[]}' + # Act + var snapshot: Variant = EncounterProgressClient.parse_encounter_progress_json(json) + # Assert + assert_that(snapshot).is_null() + + +func test_invalid_schema_emits_encounter_sync_failed() -> void: + # Arrange + var transport := MockHttpTransport.new() + transport.body_json = '{"schemaVersion":2,"playerId":"dev-local-1","encounters":[]}' + var c := _make_client(transport) + monitor_signals(c) + # Act + c.call("request_sync_from_server") + # Assert + await assert_signal(c).is_emitted( + "encounter_sync_failed", "non-JSON body or schemaVersion mismatch" + ) diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 2a50c60..c45a407 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -80,7 +80,7 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen **E5.M2 note:** Epic 5 **Slice 2** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); label **`E5.M2`**. Client capstone **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98). See [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2_NpcAiAndBehaviorProfiles.md](E5_M2_NpcAiAndBehaviorProfiles.md). Upstream **E5.M1 Ready**. **NEO-87 landed:** frozen three-behavior catalog + CI ([NEO-87 plan](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` ([NEO-88 plan](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI ([NEO-89 plan](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90 plan](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **NEO-91 landed:** **`PrototypeNpcRegistry`** + combat-target migration — three NPC instance ids, per-archetype catalog max HP ([NEO-91 plan](../../plans/NEO-91-implementation-plan.md)); [server README — Combat entity health (NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-92–NEO-96 landed:** aggro/threat, runtime state machine, **`GET …/npc-runtime-snapshot`**, player combat HP + NPC attack resolve, telemetry hooks ([NEO-92](../../plans/NEO-92-implementation-plan.md)–[NEO-96](../../plans/NEO-96-implementation-plan.md)). **NEO-97 landed:** client telegraph HUD ([NEO-97 plan](../../plans/NEO-97-implementation-plan.md), [`NEO-97` manual QA](../../manual-qa/NEO-97.md)). **NEO-98 landed:** playable NPC telegraph combat capstone ([NEO-98 plan](../../plans/NEO-98-implementation-plan.md), [`NEO-98` manual QA](../../manual-qa/NEO-98.md)); [client README — End-to-end NPC telegraph combat loop (NEO-98)](../../../client/README.md#end-to-end-npc-telegraph-combat-loop-neo-98). **Epic 5 Slice 2 complete — register row Ready.** Replaces **`prototype_target_alpha` / `beta`** with three NPC archetype instances; owns **`ThreatState`**, **`TelegraphEvent`**, session **`IPlayerCombatHealthStore`**. -**E5.M3 note:** Epic 5 **Slice 3** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111); label **`E5.M3`**. Client capstone **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111). See [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md), [E5_M3_EncounterAndRewardTables.md](E5_M3_EncounterAndRewardTables.md). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. Prototype spine: one encounter **`prototype_combat_pocket`** (defeat all three E5.M2 NPC ids) → reward table **`prototype_combat_pocket_clear`** ( **`scrap_metal_bulk` ×10**, **`contract_handoff_token` ×1** ); idempotent completion per player; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). **NEO-100 landed:** encounter + reward-table schemas, prototype catalogs, CI gates ([NEO-100 plan](../../plans/NEO-100-implementation-plan.md)). **NEO-101 landed:** fail-fast server load of `content/encounters/*_encounters.json` + `content/reward-tables/*_reward_tables.json` ([NEO-101 plan](../../plans/NEO-101-implementation-plan.md)); [server README — Encounter + reward-table catalogs (NEO-101)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-102 landed:** injectable **`IEncounterDefinitionRegistry`** + **`IRewardTableDefinitionRegistry`** + DI ([NEO-102 plan](../../plans/NEO-102-implementation-plan.md)); [server README — Encounter + reward-table registries (NEO-102)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-103 landed:** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + nested **`rewardTable`** summary ([NEO-103 plan](../../plans/NEO-103-implementation-plan.md)); [server README — Encounter definitions (NEO-103)](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`. **NEO-104 landed:** **`IEncounterProgressStore`** + **`IEncounterCompletionStore`** + **`EncounterProgressOperations`** ([NEO-104 plan](../../plans/NEO-104-implementation-plan.md)); [server README — Encounter progress (NEO-104)](../../../server/README.md#encounter-progress--completion-stores-neo-104). **NEO-105 landed:** **`EncounterCompletionOperations`** + **`EncounterCompleteEvent`** result payload ([NEO-105 plan](../../plans/NEO-105-implementation-plan.md)); [server README — Encounter completion (NEO-105)](../../../server/README.md#encounter-completion--inventory-grants-neo-105). **NEO-106 landed:** **`EncounterCombatWiring`** on **`AbilityCastApi`** ([NEO-106 plan](../../plans/NEO-106-implementation-plan.md)); [server README — Encounter combat wiring (NEO-106)](../../../server/README.md#encounter-combat-wiring-neo-106). **NEO-107 landed:** **`IEncounterCompleteEventStore`** + E7.M2 hook stub ([NEO-107 plan](../../plans/NEO-107-implementation-plan.md)); [server README — Encounter complete event (NEO-107)](../../../server/README.md#encounter-complete-event-record-neo-107). **NEO-108 landed:** **`GET /game/players/{id}/encounter-progress`** — `EncounterProgressApi` + DTOs ([NEO-108 plan](../../plans/NEO-108-implementation-plan.md)); [server README — Per-player encounter progress (NEO-108)](../../../server/README.md#per-player-encounter-progress-neo-108); Bruno `bruno/neon-sprawl-server/encounter-progress/`. **NEO-109 landed:** comment-only **`encounter_start`**, **`encounter_complete`**, **`reward_attribution`**, **`encounter_complete_denied`** hook sites ([NEO-109 plan](../../plans/NEO-109-implementation-plan.md)); [server README — Encounter telemetry hooks (NEO-109)](../../../server/README.md#encounter-telemetry-hooks-neo-109). **E5M3-11+ client HUD outstanding.** +**E5.M3 note:** Epic 5 **Slice 3** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111); label **`E5.M3`**. Client capstone **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111). See [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md), [E5_M3_EncounterAndRewardTables.md](E5_M3_EncounterAndRewardTables.md). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. Prototype spine: one encounter **`prototype_combat_pocket`** (defeat all three E5.M2 NPC ids) → reward table **`prototype_combat_pocket_clear`** ( **`scrap_metal_bulk` ×10**, **`contract_handoff_token` ×1** ); idempotent completion per player; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). **NEO-100 landed:** encounter + reward-table schemas, prototype catalogs, CI gates ([NEO-100 plan](../../plans/NEO-100-implementation-plan.md)). **NEO-101 landed:** fail-fast server load of `content/encounters/*_encounters.json` + `content/reward-tables/*_reward_tables.json` ([NEO-101 plan](../../plans/NEO-101-implementation-plan.md)); [server README — Encounter + reward-table catalogs (NEO-101)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-102 landed:** injectable **`IEncounterDefinitionRegistry`** + **`IRewardTableDefinitionRegistry`** + DI ([NEO-102 plan](../../plans/NEO-102-implementation-plan.md)); [server README — Encounter + reward-table registries (NEO-102)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-103 landed:** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + nested **`rewardTable`** summary ([NEO-103 plan](../../plans/NEO-103-implementation-plan.md)); [server README — Encounter definitions (NEO-103)](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`. **NEO-104 landed:** **`IEncounterProgressStore`** + **`IEncounterCompletionStore`** + **`EncounterProgressOperations`** ([NEO-104 plan](../../plans/NEO-104-implementation-plan.md)); [server README — Encounter progress (NEO-104)](../../../server/README.md#encounter-progress--completion-stores-neo-104). **NEO-105 landed:** **`EncounterCompletionOperations`** + **`EncounterCompleteEvent`** result payload ([NEO-105 plan](../../plans/NEO-105-implementation-plan.md)); [server README — Encounter completion (NEO-105)](../../../server/README.md#encounter-completion--inventory-grants-neo-105). **NEO-106 landed:** **`EncounterCombatWiring`** on **`AbilityCastApi`** ([NEO-106 plan](../../plans/NEO-106-implementation-plan.md)); [server README — Encounter combat wiring (NEO-106)](../../../server/README.md#encounter-combat-wiring-neo-106). **NEO-107 landed:** **`IEncounterCompleteEventStore`** + E7.M2 hook stub ([NEO-107 plan](../../plans/NEO-107-implementation-plan.md)); [server README — Encounter complete event (NEO-107)](../../../server/README.md#encounter-complete-event-record-neo-107). **NEO-108 landed:** **`GET /game/players/{id}/encounter-progress`** — `EncounterProgressApi` + DTOs ([NEO-108 plan](../../plans/NEO-108-implementation-plan.md)); [server README — Per-player encounter progress (NEO-108)](../../../server/README.md#per-player-encounter-progress-neo-108); Bruno `bruno/neon-sprawl-server/encounter-progress/`. **NEO-109 landed:** comment-only **`encounter_start`**, **`encounter_complete`**, **`reward_attribution`**, **`encounter_complete_denied`** hook sites ([NEO-109 plan](../../plans/NEO-109-implementation-plan.md)); [server README — Encounter telemetry hooks (NEO-109)](../../../server/README.md#encounter-telemetry-hooks-neo-109). **NEO-110 landed:** client encounter progress + loot HUD — **`encounter_progress_client.gd`**, defeat-triggered GET ([NEO-110 plan](../../plans/NEO-110-implementation-plan.md), [`NEO-110` manual QA](../../manual-qa/NEO-110.md)); [client README — Encounter progress + loot HUD (NEO-110)](../../../client/README.md#encounter-progress--loot-hud-neo-110). **E5M3-12 client capstone outstanding** ([NEO-111](https://linear.app/neon-sprawl/issue/NEO-111)). ### Epic 6 — PvP Security diff --git a/docs/reviews/2026-05-31-NEO-110.md b/docs/reviews/2026-05-31-NEO-110.md index 61b044d..896a69f 100644 --- a/docs/reviews/2026-05-31-NEO-110.md +++ b/docs/reviews/2026-05-31-NEO-110.md @@ -6,11 +6,11 @@ ## Verdict -**Request changes** +**Approve with nits** ## Summary -NEO-110 adds the Godot client for **`GET /game/players/{id}/encounter-progress`**: **`encounter_progress_client.gd`** mirrors **`gig_progression_client.gd`** (injectable HTTP, `_busy` guard, v1 parse, signals), **`main.gd`** wires boot hydrate + defeat-triggered refresh, and **`EncounterProgressLabel`** / **`EncounterCompleteLabel`** sit in **`HudRoot`** after **`PlayerCombatHpLabel`**. GdUnit covers parse paths and 404; manual QA and README are thorough. Architecture matches server authority and NEO-108 contract. One **runtime bug** in loot rendering blocks merge: **`main.gd`** calls **`_item_display_name`**, which is not defined on the main script (only on **`craft_recipe_panel.gd`**). Completing the third NPC will hit that path and error when painting grant lines. +NEO-110 adds the Godot client for **`GET /game/players/{id}/encounter-progress`**: **`encounter_progress_client.gd`** mirrors **`gig_progression_client.gd`** (injectable HTTP, `_busy` guard, v1 parse, signals), **`main.gd`** wires boot hydrate + defeat-triggered refresh, and **`EncounterProgressLabel`** / **`EncounterCompleteLabel`** sit in **`HudRoot`** after **`PlayerCombatHpLabel`**. GdUnit covers parse paths, 404, and schema mismatch; manual QA and README are thorough. Architecture matches server authority and NEO-108 contract. Review follow-up fixed loot rendering (**`_inventory_item_label`**), independent encounter re-paint on item defs, register drift, and extra GdUnit tests. ## Documentation checked @@ -20,22 +20,22 @@ NEO-110 adds the Godot client for **`GET /game/players/{id}/encounter-progress`* | `docs/plans/E5M3-prototype-backlog.md` (E5M3-11) | **Matches** — AC checked; landed note cites plan, manual QA, client README. | | `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | **Matches** — NEO-110 client HUD snapshot bullet added. | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E5.M3 row notes **NEO-110** client HUD; **E5M3-12** capstone still outstanding. | -| `docs/decomposition/modules/module_dependency_register.md` | **Partially matches** — E5.M3 note still ends with **“E5M3-11+ client HUD outstanding.”** Should cite **NEO-110** landed (non-blocking doc drift). | +| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E5.M3 note cites **NEO-110** landed; **NEO-111** capstone outstanding. Done. | | `docs/manual-qa/NEO-110.md` | **Matches** — Godot-only steps, defeat chain, HUD expectations, inventory auto-refresh. | | `client/README.md` | **Matches** — encounter HUD subsection; refresh triggers documented. | | Full-stack epic decomposition | **Matches** — client issue **NEO-110** fulfills server **NEO-108** counterpart; not claiming E5M3-12 capstone. | ## Blocking issues -1. **`_item_display_name` missing on `main.gd`** — `_render_encounter_complete_label()` calls `_item_display_name(item_id)` (line ~820), but **`main.gd` defines only `_inventory_item_label()`**, which already wraps **`ItemDefinitionsClient.display_name_for`**. **`craft_recipe_panel.gd`** owns **`_item_display_name`** on a different node. On encounter **`completed`**, Godot will throw when iterating **`rewardGrantSummary`**. **Fix:** use **`_inventory_item_label(item_id)`** (plan’s NEO-72 precedent) or add a shared helper on **`main.gd`**. +1. ~~**`_item_display_name` missing on `main.gd`**~~ — `_render_encounter_complete_label()` now calls **`_inventory_item_label(item_id)`**. Done. ## Suggestions -1. **`_on_item_definitions_ready` coupling to inventory error** — The handler returns early when **`_inventory_error`** is set, so a failed inventory GET prevents re-painting encounter loot after item defs load (plan adopted re-paint on **`definitions_ready`** when snapshot is **`completed`**). Consider re-painting encounter labels independently of inventory sync state. +1. ~~**`_on_item_definitions_ready` coupling to inventory error**~~ — Inventory and encounter HUD re-paint independently when their respective snapshots are valid. Done. -2. **Update `module_dependency_register.md` E5.M3 note** — Replace **“E5M3-11+ client HUD outstanding”** with **NEO-110 landed** + pointer to client README / manual QA; keep **NEO-111** as outstanding capstone. +2. ~~**Update `module_dependency_register.md` E5.M3 note**~~ — **NEO-110** landed + client README / manual QA pointers; **NEO-111** capstone outstanding. Done. -3. **GdUnit: invalid JSON / schema mismatch** — Optional test that **`parse_encounter_progress_json`** returns **`null`** and that a mock 200 with bad body emits **`encounter_sync_failed`**; improves parity with gig client coverage if added later. +3. ~~**GdUnit: invalid JSON / schema mismatch**~~ — Added **`test_parse_encounter_progress_json_returns_null_for_schema_mismatch`** and **`test_invalid_schema_emits_encounter_sync_failed`**. Done. ## Nits