NEO-115: add kickoff implementation plan for quest definitions GET.
parent
437894ccd1
commit
a5be355a22
|
|
@ -0,0 +1,115 @@
|
|||
# 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
|
||||
|
||||
- [ ] GET returns all four prototype quests with stable JSON v1 envelope (`schemaVersion` 1, `quests[]`).
|
||||
- [ ] Bruno request documents example response shape and passes in CI Bruno step.
|
||||
|
||||
## 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** |
|
||||
Loading…
Reference in New Issue