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

125 lines
9.7 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-115 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-115 |
| **Title** | E7M1-04: GET /game/world/quest-definitions |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-115/e7m1-04-get-gameworldquest-definitions |
| **Module** | [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-04** |
| **Branch** | `NEO-115-get-world-quest-definitions` |
| **Precursor** | [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) — injectable quest definition registry + DI (**landed on `main`**) |
| **Pattern** | [NEO-103](NEO-103-implementation-plan.md) — world definition GET + Bruno; [NEO-68](NEO-68-implementation-plan.md) — registry-backed projection |
| **Blocks** | [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) — player quest state store (E7M1-05); E7M1-06+ runtime |
| **Client counterpart** | [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) — optional cache of display names / step summaries (E7M1-11); not wired this story |
## Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Objective JSON shape** | Flat optional fields vs nested per-kind objects? | **Flat mirror content**`id` + `kind` + only relevant fields; omit unused keys (`JsonIgnoreCondition.WhenWritingNull`) — matches `quest-objective-def.schema.json` and `content/quests/prototype_quests.json` for NEO-122. | **Adopted** — flat mirror |
| **Manual QA doc** | Add `docs/manual-qa/NEO-115.md`? | **Skip** — server-only route; Bruno + API integration tests ([NEO-103](NEO-103-implementation-plan.md) precedent). | **Adopted** — skip |
## Goal, scope, and out-of-scope
**Goal:** Expose a stable, read-only JSON endpoint that returns the frozen prototype quest definitions (four quests) for Bruno, tooling, and optional client display ([NEO-122](https://linear.app/neon-sprawl/issue/NEO-122)) — backed by **`IQuestDefinitionRegistry`** without duplicating catalog truth.
**In scope (from Linear + [E7M1-04](E7M1-prototype-backlog.md#e7m1-04--get-gameworldquest-definitions)):**
- **`GET /game/world/quest-definitions`** with versioned envelope (`schemaVersion` **1**, **`quests`** array).
- Quest row fields: **`id`**, **`displayName`**, **`prerequisiteQuestIds`**, nested **`steps`** (`id`, `displayName`, **`objectives`** with flat objective projection).
- Quests ordered by **`id`** ordinal, matching **`GetDefinitionsInIdOrder()`**.
- Bruno folder `bruno/neon-sprawl-server/quest-definitions/`.
- `server/README.md` route section.
- API integration tests (AAA).
**Out of scope (from Linear + backlog):**
- Per-player progress ([NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) / E7M1-08).
- Accept/advance hooks, stores (E7M1-05+).
- Godot / client wiring ([NEO-122](https://linear.app/neon-sprawl/issue/NEO-122)).
- `docs/manual-qa/NEO-115.md` (server-only per kickoff).
## Acceptance criteria checklist
- [x] GET returns all four prototype quests with stable JSON v1 envelope (`schemaVersion` 1, `quests[]`).
- [x] Bruno request documents example response shape and passes in CI Bruno step.
## Implementation reconciliation (shipped)
- **Route:** `GET /game/world/quest-definitions` — injects `IQuestDefinitionRegistry`.
- **DTOs:** `QuestDefinitionsListResponse` (schema v1, `quests[]`); nested `steps` / flat `objectives` with `WhenWritingNull` on unused objective fields.
- **Wiring:** `MapQuestDefinitionsWorldApi()` in `Program.cs` after encounter definitions.
- **Tests:** AAA integration test in `QuestDefinitionsWorldApiTests`.
- **Bruno:** `bruno/neon-sprawl-server/quest-definitions/`.
- **Docs:** `server/README.md` section; E7.M1 module snapshot + alignment register + backlog updated.
## Technical approach
1. **Route:** **`GET /game/world/quest-definitions`** — parameterless read; inject **`IQuestDefinitionRegistry`** only (not `QuestDefinitionCatalog`).
2. **Response shape:** Top-level **`schemaVersion`** (`QuestDefinitionsListResponse.CurrentSchemaVersion` = **1**) plus **`quests`** array. Each row maps from **`QuestDefRow`**:
- **`id`**, **`displayName`**, **`prerequisiteQuestIds`** (preserve catalog order)
- **`steps`**: array of `{ id, displayName, objectives[] }`
- **`objectives`**: flat mirror of content — **`id`**, **`kind`**, and only populated fields for that kind:
- `gather_item` / `inventory_has_item`: **`itemId`**, **`quantity`**
- `craft_recipe`: **`recipeId`**, **`quantity`**
- `encounter_complete`: **`encounterId`**
- Optional objective properties use **`[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`** so unused keys are omitted (not `null`).
3. **Ordering:** Build list from **`questRegistry.GetDefinitionsInIdOrder()`**. Prototype freeze: four ids in **`PrototypeE7M1QuestCatalogRules.ExpectedQuestIds`** (ordinal by `id`).
4. **Implementation:** New **`QuestDefinitionsWorldApi`** + **`QuestDefinitionsListDtos.cs`** in `Game/Quests/` (mirror [`EncounterDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionsWorldApi.cs)). Wire **`app.MapQuestDefinitionsWorldApi()`** from **`Program.cs`** after **`MapEncounterDefinitionsWorldApi()`** (quest catalog loads after encounters).
5. **Bruno:** `bruno/neon-sprawl-server/quest-definitions/` with **`Get quest definitions.bru`** + **`folder.bru`**. Tests: 200, JSON, **`schemaVersion` 1**, **`quests.length === 4`**, ascending id order, spot-check **`prototype_quest_gather_intro`** (gather objective) and **`prototype_quest_operator_chain`** (four steps, terminal **`inventory_has_item`** objective).
6. **Docs:** Add **`server/README.md`** section (GET + curl). Update [E7_M1](E7_M1_QuestStateMachine.md) implementation snapshot and [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M1 row when landed.
### Expected prototype roster (from content)
| `id` | `displayName` | `prerequisiteQuestIds` | Steps |
|------|---------------|------------------------|-------|
| `prototype_quest_gather_intro` | Intro: Salvage Run | `[]` | 1 — gather `scrap_metal_bulk` ×3 |
| `prototype_quest_refine_intro` | Intro: Refine Stock | `[prototype_quest_gather_intro]` | 1 — craft `refine_scrap_standard` ×1 |
| `prototype_quest_combat_intro` | Intro: Clear the Pocket | `[]` | 1 — encounter `prototype_combat_pocket` |
| `prototype_quest_operator_chain` | Operator Chain | all three intros | 4 — gather ×5 → refine → stim → token ×1 |
**HTTP `quests` order (ordinal `id`):** `prototype_quest_combat_intro`, `prototype_quest_gather_intro`, `prototype_quest_operator_chain`, `prototype_quest_refine_intro`.
## Files to add
| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Quests/QuestDefinitionsWorldApi.cs` | `Map*` extension registering GET route; maps registry → JSON. |
| `server/NeonSprawl.Server/Game/Quests/QuestDefinitionsListDtos.cs` | Versioned response + quest/step/objective DTOs for JSON serialization. |
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema v1, four quests, frozen rows + chain depth. |
| `bruno/neon-sprawl-server/quest-definitions/Get quest definitions.bru` | Manual / CI Bruno verification against dev server. |
| `bruno/neon-sprawl-server/quest-definitions/folder.bru` | Bruno folder metadata. |
| `docs/plans/NEO-115-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Register `MapQuestDefinitionsWorldApi()` after encounter definitions world API. |
| `server/README.md` | Document `GET /game/world/quest-definitions`, curl example, registry injection note. |
| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | Implementation snapshot — HTTP read model bullet (NEO-115). |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M1 row — note NEO-115 HTTP projection when landed. |
| `docs/plans/E7M1-prototype-backlog.md` | Mark E7M1-04 acceptance criteria landed when complete. |
## Tests
| Test file | What it covers |
|-----------|----------------|
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionsWorldApiTests.cs` | **Arrange:** `InMemoryWebApplicationFactory`. **Act:** `GET /game/world/quest-definitions`. **Assert:** 200 OK; `schemaVersion` 1; `quests` length 4; ids match `PrototypeE7M1QuestCatalogRules.ExpectedQuestIds` in ordinal order; gather intro: `displayName` Intro: Salvage Run, one step, `gather_item` objective with `scrap_metal_bulk` quantity 3 (no spurious null keys on objective JSON where testable via deserialize); refine intro prerequisite contains `prototype_quest_gather_intro`; operator chain: four steps, last objective `inventory_has_item` + `contract_handoff_token` quantity 1; combat intro objective `encounter_complete` + `prototype_combat_pocket`. |
## Open questions / risks
| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Objective null omission** | Use `WhenWritingNull` on optional objective DTO properties; integration test deserializes into typed DTOs (not raw JSON key scan) unless a dedicated raw-body assert is added for one objective. | **adopted** |
| **No second registry** | Unlike NEO-103, quest rows are self-contained — only `IQuestDefinitionRegistry`; cross-refs validated at catalog load (NEO-113). | **adopted** |
| **Constants in tests** | Reuse `PrototypeE7M1QuestCatalogRules` ids / `ChainTerminalItemId`; avoid duplicating id strings. | **adopted** |
| **Bruno vs C# test drift** | Keep frozen spot-check values in sync with `content/quests/prototype_quests.json` and Bruno tests (same pattern as encounter Bruno). | **adopted** |