neon-sprawl/docs/plans/NEO-103-implementation-plan.md

123 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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": "<CompletionCriteriaKind>" }` (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** |