# 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** |