11 KiB
NEO-120 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-120 |
| Title | E7M1-09: POST /game/players/{id}/quests/{questId}/accept |
| Linear | https://linear.app/neon-sprawl/issue/NEO-120/e7m1-09-post-gameplayersidquestsquestidaccept |
| Module | E7.M1 — QuestStateMachine · Epic 7 Slice 1 · backlog E7M1-09 |
| Branch | NEO-120-quest-accept-post |
| Precursor | NEO-117 — QuestStateOperations.TryAccept (landed on main); NEO-119 — GET quest-progress (landed on main) |
| Pattern | NEO-117 — operations + reason codes; NEO-119 — quest progress row JSON; NEO-55 / PlayerInventoryApi — mutation envelope (applied + reasonCode + nested snapshot, HTTP 200 on deny) |
| Blocks | NEO-122 — client quest progress + accept HUD (E7M1-11) |
| Client counterpart | NEO-122 — accept binding + feedback label polls this POST then GET quest-progress. Bruno-only accept verification is not prototype-complete per full-stack epic decomposition. |
Kickoff clarifications
No clarifications needed. E7M1-09 backlog, Linear AC, landed NEO-117 operations, and mutation HTTP precedents (PlayerInventoryApi, PlayerCraftApi, AbilityCastApi) settle scope:
- Response envelope:
accepted+ optionalreasonCode+ optional singlequestrow (QuestProgressRowJsonshape from NEO-119) — backlog “updated progress row or deny payload”; inventory-style 200 on structured deny. - Request body: optional v1 — omit body, empty body, or
{ "schemaVersion": 1 }all valid; 400 only when body is present with wrongschemaVersion. - Player gate:
IPositionStateStore→ 404 for unknown/blank ids (GET quest-progress / inventory precedent) beforeTryAccept. - Unknown quest: 200 +
accepted: false+reasonCode: unknown_quest(operations layer; not HTTP 404). - Deny with snapshot:
already_active/already_completedinclude currentquestrow from operations snapshot (no mutation on deny). - Manual QA doc: skip
docs/manual-qa/NEO-120.md— server-only; Bruno + API integration tests (NEO-119 precedent).
Goal, scope, and out-of-scope
Goal: Expose player-initiated quest accept over HTTP so Godot (and Bruno) can transition a quest from not_started → active with structured deny feedback when prerequisites or state block accept.
In scope (from Linear + E7M1-09):
POST /game/players/{id}/quests/{questId}/accept—QuestAcceptApi; optional request body v1; response withaccepted, optionalreasonCode, optionalquestprogress row.- Wire
QuestStateOperations.TryAccept(inventory refresh + step completion on accept already in operations). - Shared row projection with GET (
QuestProgressRowJsonfromQuestStepState/ missing row). - Bruno
bruno/neon-sprawl-server/quest-progress/— happy accept, prerequisite deny, duplicate accept. - Integration tests (AAA): 404 player gate; happy accept;
prerequisite_incomplete;already_active;unknown_quest. server/README.mdaccept route section.
Out of scope (from Linear + backlog):
- Abandon/reset POST.
- Godot HUD (NEO-122).
- Telemetry hook comments (NEO-121).
- Dev fixture reset API for quest rows.
docs/manual-qa/NEO-120.md(server-only per kickoff).
Acceptance criteria checklist
- Successful accept transitions row
not_started→active. - Deny returns structured
reasonCodewithout mutation. - Bruno + integration tests pass in CI.
Implementation reconciliation (shipped)
- Route:
POST /game/players/{id}/quests/{questId}/accept—QuestAcceptApi.MapResponse; 404 viaIPositionStateStore; optional body v1. - DTOs:
QuestAcceptRequest,QuestAcceptResponse(accepted,reasonCode,questrow). - Shared mapper:
QuestProgressApi.MapQuestProgressRowoverloads for GET + accept POST. - Tests: ten AAA cases in
QuestAcceptApiTests(incl.already_completeddeny snapshot, empty{}body). - Bruno: accept gather intro, refine prerequisite deny, duplicate accept.
- Docs:
server/README.md; E7.M1 module snapshot + backlog updated.
Technical approach
-
Route:
POST /game/players/{id}/quests/{questId}/accept— injectIPositionStateStore,IQuestDefinitionRegistry,IPlayerQuestStateStore,IPlayerInventoryStore,IItemDefinitionRegistry,TimeProvider. -
Request:
QuestAcceptRequestwithschemaVersion1. Handler acceptsnull, empty JSON, or{ "schemaVersion": 1 }. 400 when body is non-null andschemaVersion≠ 1. -
Player gate: Trim
id; return 404 when empty or position missing (mirrorQuestProgressApiGET). -
Accept:
QuestStateOperations.TryAccept(trimmedId, questId, …)—questIdfrom route (operations normalizes via registry). -
Response mapping (
QuestAcceptResponsev1):Outcome HTTP acceptedreasonCodequestrowSuccess 200 trueomitted active row from snapshot Deny (no snapshot) 200 falseoperations code omitted Deny (with snapshot) 200 falseoperations code row from snapshot ( already_active,already_completed)Map
QuestStepState→QuestProgressRowJsonvia shared helper extracted fromQuestProgressApi.MapRow(e.g.MapQuestProgressRow) so GET and POST stay aligned. -
Register:
app.MapQuestAcceptApi()inProgram.csimmediately afterMapQuestProgressApi(). -
Bruno (
bruno/neon-sprawl-server/quest-progress/):Accept gather intro.bru— POSTdev-local-1/prototype_quest_gather_intro→ 200,accepted: true,quest.status === active,currentStepIndex === 0.Accept refine prerequisite deny.bru— POST refine intro without gather complete → 200,accepted: false,reasonCode === prerequisite_incomplete, no active refine row on subsequent GET.Accept duplicate.bru— accept gather twice → second 200,accepted: false,reasonCode === already_active,quest.status === active.
-
Docs: Add
server/README.mdPOST accept section (curl, response shape, reason codes). Update E7.M1 module snapshot / alignment register / E7M1 backlog on story land.
Expected v1 success response (prototype)
{
"schemaVersion": 1,
"accepted": true,
"quest": {
"questId": "prototype_quest_gather_intro",
"status": "active",
"currentStepIndex": 0,
"objectiveCounters": {}
}
}
Expected v1 deny response (prerequisite)
{
"schemaVersion": 1,
"accepted": false,
"reasonCode": "prerequisite_incomplete"
}
Accept flow
flowchart TD
R[POST accept] --> B{Body schema ok?}
B -->|no| B400[400]
B -->|yes| G{Known player?}
G -->|no| N404[404]
G -->|yes| A[QuestStateOperations.TryAccept]
A --> M[Map QuestAcceptResponse]
M --> J[JSON 200]
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Quests/QuestAcceptApi.cs |
Map* extension; player gate; TryAccept; response mapping. |
server/NeonSprawl.Server/Game/Quests/QuestAcceptDtos.cs |
Optional request + v1 accept response DTOs. |
server/NeonSprawl.Server.Tests/Game/Quests/QuestAcceptApiTests.cs |
AAA HTTP integration: 404, happy accept, prerequisite deny, duplicate accept, unknown quest. |
bruno/neon-sprawl-server/quest-progress/Accept gather intro.bru |
Bruno happy-path accept. |
bruno/neon-sprawl-server/quest-progress/Accept refine prerequisite deny.bru |
Bruno prerequisite deny. |
bruno/neon-sprawl-server/quest-progress/Accept duplicate.bru |
Bruno duplicate accept deny. |
docs/plans/NEO-120-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Quests/QuestProgressApi.cs |
Extract shared MapQuestProgressRow (or equivalent) for GET + accept POST row projection. |
server/NeonSprawl.Server/Program.cs |
Register MapQuestAcceptApi() after MapQuestProgressApi(). |
server/README.md |
Document POST accept route, request/response shape, reason codes; replace E7M1-09 deferral stub. |
docs/decomposition/modules/E7_M1_QuestStateMachine.md |
HTTP quest accept bullet (on story complete). |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E7.M1 row — NEO-120 HTTP (on story complete). |
docs/decomposition/modules/module_dependency_register.md |
E7.M1 note — NEO-120 (on story complete). |
docs/plans/E7M1-prototype-backlog.md |
E7M1-09 acceptance checkboxes (on story complete). |
Tests
| Test file | What it covers |
|---|---|
QuestAcceptApiTests.cs |
404: unknown player + whitespace id. Happy accept: POST gather intro → accepted: true, quest.status active, step 0, empty counters; GET confirms active. Prerequisite deny: POST refine intro without gather complete → prerequisite_incomplete, GET still not_started for refine. Duplicate accept: second POST → already_active with quest snapshot. Completed deny: accept + complete gather intro → re-accept → already_completed with quest.status completed + completedAt. Unknown quest: bogus quest id → unknown_quest. Optional body: POST with no body or {} succeeds for happy accept. |
| Existing quest test suite | Regression smoke — operations + GET unchanged when accept not called. |
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
Response field accepted vs applied |
Use accepted — matches AbilityCastResponse / perk selected mutation naming; backlog does not mandate applied |
adopted |
| Shared row mapper location | Extract from QuestProgressApi to internal static helper in same file or small shared type — avoid duplicating status/counter mapping |
adopted |
| Accept may auto-complete first step | Operations already run inventory wiring + step completion on accept (NEO-118); tests assert post-accept row as returned, not forced step 0 if wiring advances | adopted |
| Bruno collection ordering | Add accept .bru files beside existing quest-progress folder; no new collection root |
adopted |