From 62e6bfdf0ab89ca49f0906974e3d73c57ac5663d Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 15:38:53 -0400 Subject: [PATCH] NEO-103: Add implementation plan for encounter-definitions GET. Kickoff clarifications adopt nested rewardTable summary, nested completionCriteria shape, and skip manual QA (server-only). --- docs/plans/NEO-103-implementation-plan.md | 113 ++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/plans/NEO-103-implementation-plan.md diff --git a/docs/plans/NEO-103-implementation-plan.md b/docs/plans/NEO-103-implementation-plan.md new file mode 100644 index 0000000..6706c78 --- /dev/null +++ b/docs/plans/NEO-103-implementation-plan.md @@ -0,0 +1,113 @@ +# 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 + +- [ ] GET returns frozen encounter in ascending **`id`** order with **`schemaVersion`** **1**. +- [ ] Bruno happy GET passes in CI Bruno step. + +## 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** |