123 lines
9.6 KiB
Markdown
123 lines
9.6 KiB
Markdown
# 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** |
|