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

9.7 KiB
Raw Permalink Blame History

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 contentid + 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-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 / 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 (schemaVersion 1, quests[]).
  • 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). 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 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