9.7 KiB
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 · Epic 7 Slice 1 · backlog E7M1-04 |
| Branch | NEO-115-get-world-quest-definitions |
| Precursor | NEO-114 — injectable quest definition registry + DI (landed on main) |
| Pattern | NEO-103 — world definition GET + Bruno; NEO-68 — registry-backed projection |
| Blocks | NEO-116 — player quest state store (E7M1-05); E7M1-06+ runtime |
| Client counterpart | 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 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) — backed by IQuestDefinitionRegistry without duplicating catalog truth.
In scope (from Linear + E7M1-04):
GET /game/world/quest-definitionswith versioned envelope (schemaVersion1,questsarray).- Quest row fields:
id,displayName,prerequisiteQuestIds, nestedsteps(id,displayName,objectiveswith flat objective projection). - Quests ordered by
idordinal, matchingGetDefinitionsInIdOrder(). - Bruno folder
bruno/neon-sprawl-server/quest-definitions/. server/README.mdroute section.- API integration tests (AAA).
Out of scope (from Linear + backlog):
- Per-player progress (NEO-119 / E7M1-08).
- Accept/advance hooks, stores (E7M1-05+).
- Godot / client wiring (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 (
schemaVersion1,quests[]). - Bruno request documents example response shape and passes in CI Bruno step.
Implementation reconciliation (shipped)
- Route:
GET /game/world/quest-definitions— injectsIQuestDefinitionRegistry. - DTOs:
QuestDefinitionsListResponse(schema v1,quests[]); nestedsteps/ flatobjectiveswithWhenWritingNullon unused objective fields. - Wiring:
MapQuestDefinitionsWorldApi()inProgram.csafter encounter definitions. - Tests: AAA integration test in
QuestDefinitionsWorldApiTests. - Bruno:
bruno/neon-sprawl-server/quest-definitions/. - Docs:
server/README.mdsection; E7.M1 module snapshot + alignment register + backlog updated.
Technical approach
-
Route:
GET /game/world/quest-definitions— parameterless read; injectIQuestDefinitionRegistryonly (notQuestDefinitionCatalog). -
Response shape: Top-level
schemaVersion(QuestDefinitionsListResponse.CurrentSchemaVersion= 1) plusquestsarray. Each row maps fromQuestDefRow: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,quantitycraft_recipe:recipeId,quantityencounter_complete:encounterId
- Optional objective properties use
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]so unused keys are omitted (notnull).
-
Ordering: Build list from
questRegistry.GetDefinitionsInIdOrder(). Prototype freeze: four ids inPrototypeE7M1QuestCatalogRules.ExpectedQuestIds(ordinal byid). -
Implementation: New
QuestDefinitionsWorldApi+QuestDefinitionsListDtos.csinGame/Quests/(mirrorEncounterDefinitionsWorldApi). Wireapp.MapQuestDefinitionsWorldApi()fromProgram.csafterMapEncounterDefinitionsWorldApi()(quest catalog loads after encounters). -
Bruno:
bruno/neon-sprawl-server/quest-definitions/withGet quest definitions.bru+folder.bru. Tests: 200, JSON,schemaVersion1,quests.length === 4, ascending id order, spot-checkprototype_quest_gather_intro(gather objective) andprototype_quest_operator_chain(four steps, terminalinventory_has_itemobjective). -
Docs: Add
server/README.mdsection (GET + curl). Update E7_M1 implementation snapshot and 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 |