# NEO-103 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-103 | | **Title** | E5M3-04: GET /game/world/encounter-definitions + Bruno | | **Linear** | https://linear.app/neon-sprawl/issue/NEO-103/e5m3-04-get-gameworldencounter-definitions-bruno | | **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-04** | | **Branch** | `NEO-103-get-world-encounter-definitions` | | **Precursor** | [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) — injectable encounter + reward-table registries + DI (**landed on `main`**) | | **Pattern** | [NEO-78](NEO-78-implementation-plan.md) — world definition GET + Bruno; [NEO-60](NEO-60-implementation-plan.md) — nested child projection via second registry | | **Blocks** | [NEO-104](https://linear.app/neon-sprawl/issue/NEO-104) — encounter progress + completion stores | | **Client counterpart** | None this story — player-visible labels start at [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) via this HTTP route | ## Kickoff clarifications | Topic | Question | Agent recommendation | Answer | |--------|----------|----------------------|--------| | **Reward table in response** | Nested summary vs linked `rewardTableId` only? | **Nested `rewardTable`** (`id`, `displayName`, `fixedGrants[]`) — backlog goal + one round-trip for NEO-110 loot preview; inject **`IRewardTableDefinitionRegistry`**. | **Adopted** — nested summary | | **Completion criteria JSON shape** | Nested `completionCriteria: { kind }` vs flat `completionCriteriaKind`? | **Nested object** — matches `encounter-def.schema.json` and catalog JSON. | **Adopted** — nested object | | **Manual QA doc** | Add `docs/manual-qa/NEO-103.md`? | **Skip** — server-only (NEO-78 precedent); Bruno + automated API tests suffice. | **Adopted** — skip | ## Goal, scope, and out-of-scope **Goal:** Expose a stable, read-only JSON endpoint that returns the frozen prototype encounter definition(s) for Bruno, tooling, and optional client preview ([NEO-110](https://linear.app/neon-sprawl/issue/NEO-110)) — backed by **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** without duplicating catalog truth. **In scope (from Linear + [E5M3-04](E5M3-prototype-backlog.md#e5m3-04--get-gameworldencounter-definitions)):** - **`GET /game/world/encounter-definitions`** with versioned envelope (`schemaVersion` **1**, **`encounters`** array). - Encounter row fields: **`id`**, **`displayName`**, **`completionCriteria`** (`{ kind }`), **`requiredNpcInstanceIds`**, nested **`rewardTable`** summary (`id`, `displayName`, `fixedGrants[]` with `itemId` + `quantity`). - Encounters ordered by **`id`** ordinal, matching **`GetDefinitionsInIdOrder()`**. - Bruno folder `bruno/neon-sprawl-server/encounter-definitions/`. - `server/README.md` route section. - API integration tests (AAA). **Out of scope (from Linear + backlog):** - Per-player progress, completion grants, combat hooks (E5M3-05+). - Separate **`GET /game/world/reward-table-definitions`** route (not in backlog; nested summary covers prototype). - Godot / client wiring ([NEO-110](https://linear.app/neon-sprawl/issue/NEO-110)). - `docs/manual-qa/NEO-103.md` (server-only per kickoff). ## Acceptance criteria checklist - [x] GET returns frozen encounter in ascending **`id`** order with **`schemaVersion`** **1**. - [x] Bruno happy GET passes in CI Bruno step. ## Implementation reconciliation (shipped) - **Route:** `GET /game/world/encounter-definitions` — injects `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry`. - **DTOs:** `EncounterDefinitionsListResponse` (schema v1, `encounters[]`); nested `completionCriteria: { kind }`; nested `rewardTable` with `fixedGrants[]`. - **Wiring:** `MapEncounterDefinitionsWorldApi()` in `Program.cs` after resource-node definitions. - **Tests:** AAA integration test in `EncounterDefinitionsWorldApiTests`. - **Bruno:** `bruno/neon-sprawl-server/encounter-definitions/`. - **Docs:** `server/README.md` section; E5.M3 module snapshot + alignment register + backlog updated. ## Technical approach 1. **Route:** **`GET /game/world/encounter-definitions`** — parameterless read; inject **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** only (not catalog types). 2. **Response shape:** Top-level **`schemaVersion`** (`EncounterDefinitionsListResponse.CurrentSchemaVersion` = **1**) plus **`encounters`** array. Each row maps from **`EncounterDefRow`**: - **`id`**, **`displayName`** - **`completionCriteria`**: `{ "kind": "" }` (nested, matching content schema) - **`requiredNpcInstanceIds`**: string array (preserve catalog order) - **`rewardTable`**: resolve via **`rewardTableRegistry.TryGetDefinition(encounter.RewardTableId, …)`**; on miss throw **`InvalidOperationException`** (fail-fast — catalog cross-ref validated at load; same pattern as [`ResourceNodeDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs) missing yield). Nested object: **`id`**, **`displayName`**, **`fixedGrants`** (`itemId`, `quantity`). 3. **Ordering:** Build list from **`encounterRegistry.GetDefinitionsInIdOrder()`**. Prototype freeze: single id **`prototype_combat_pocket`**. 4. **Implementation:** New **`EncounterDefinitionsWorldApi`** + **`EncounterDefinitionsListDtos.cs`** in `Game/Encounters/` (mirror [`AbilityDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Combat/AbilityDefinitionsWorldApi.cs)). Wire **`app.MapEncounterDefinitionsWorldApi()`** from **`Program.cs`** after other world definition APIs (near **`MapResourceNodeDefinitionsWorldApi()`**). 5. **Bruno:** `bruno/neon-sprawl-server/encounter-definitions/` with **`Get encounter definitions.bru`** + **`folder.bru`**. Tests: 200, JSON, **`schemaVersion` 1**, **`encounters.length === 1`**, id **`prototype_combat_pocket`**, nested **`completionCriteria.kind`** **`defeat_all_targets`**, three NPC ids, nested **`rewardTable`** with frozen grants. 6. **Docs:** Add **`server/README.md`** section (GET + curl). Update [E5_M3](E5_M3_EncounterAndRewardTables.md) **Related implementation slices** / implementation snapshot and [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E5.M3 row when landed. ### Expected prototype row values (from content) | Field | Value | |-------|--------| | **`id`** | `prototype_combat_pocket` | | **`displayName`** | Prototype Combat Pocket | | **`completionCriteria.kind`** | `defeat_all_targets` | | **`requiredNpcInstanceIds`** | `prototype_npc_melee`, `prototype_npc_ranged`, `prototype_npc_elite` | | **`rewardTable.id`** | `prototype_combat_pocket_clear` | | **`rewardTable.displayName`** | Prototype Combat Pocket Clear | | **`rewardTable.fixedGrants`** | `scrap_metal_bulk` ×10, `contract_handoff_token` ×1 | ## Files to add | Path | Purpose | |------|---------| | `server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionsWorldApi.cs` | `Map*` extension registering GET route; maps both registries → JSON. | | `server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionsListDtos.cs` | Versioned response + row/nested reward DTOs for JSON serialization. | | `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema v1, frozen encounter + nested reward table. | | `bruno/neon-sprawl-server/encounter-definitions/Get encounter definitions.bru` | Manual / CI Bruno verification against dev server. | | `bruno/neon-sprawl-server/encounter-definitions/folder.bru` | Bruno folder metadata. | | `docs/plans/NEO-103-implementation-plan.md` | This plan. | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Program.cs` | Register `MapEncounterDefinitionsWorldApi()` alongside other game world definition APIs. | | `server/README.md` | Document `GET /game/world/encounter-definitions`, curl example, registry injection note. | | `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | Implementation snapshot — HTTP read model bullet (NEO-103). | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 row — note NEO-103 HTTP projection when landed. | | `docs/plans/E5M3-prototype-backlog.md` | Mark E5M3-04 acceptance criteria landed when complete. | ## Tests | Test file | What it covers | |-----------|----------------| | `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionsWorldApiTests.cs` | **Arrange:** `InMemoryWebApplicationFactory`. **Act:** `GET /game/world/encounter-definitions`. **Assert:** 200 OK; `schemaVersion` 1; exactly one encounter; id `prototype_combat_pocket`; `displayName` Prototype Combat Pocket; `completionCriteria.kind` `defeat_all_targets`; `requiredNpcInstanceIds` contains three frozen NPC ids; nested `rewardTable.id` `prototype_combat_pocket_clear`; `fixedGrants` quantities 10 + 1 for `scrap_metal_bulk` and `contract_handoff_token`. | ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | **Missing reward table at HTTP time** | Throw **`InvalidOperationException`** — load-time cross-ref should guarantee presence; mirrors resource-node yield guard. | **adopted** | | **Separate reward-table GET later** | Defer — nested summary satisfies prototype; add dedicated route only if a future story needs reward tables without encounters. | **deferred** | | **NPC id order in JSON** | Preserve catalog/`EncounterDefRow` order (matches `prototype_encounters.json`); do not re-sort. | **adopted** |