NEO-120: Add implementation plan for quest accept POST.
Kickoff plan for E7M1-09 — wire QuestStateOperations.TryAccept over HTTP with Bruno and integration tests; client counterpart NEO-122.pull/159/head
parent
c388949b8d
commit
b22b68ad1f
|
|
@ -0,0 +1,161 @@
|
|||
# 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](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-09** |
|
||||
| **Branch** | `NEO-120-quest-accept-post` |
|
||||
| **Precursor** | [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) — `QuestStateOperations.TryAccept` (**landed on `main`**); [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) — GET quest-progress (**landed on `main`**) |
|
||||
| **Pattern** | [NEO-117](NEO-117-implementation-plan.md) — operations + reason codes; [NEO-119](NEO-119-implementation-plan.md) — quest progress row JSON; [NEO-55](NEO-55-implementation-plan.md) / `PlayerInventoryApi` — mutation envelope (`applied` + `reasonCode` + nested snapshot, HTTP **200** on deny) |
|
||||
| **Blocks** | [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) — client quest progress + accept HUD (E7M1-11) |
|
||||
| **Client counterpart** | [NEO-122](https://linear.app/neon-sprawl/issue/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](../../.cursor/rules/full-stack-epic-decomposition.md). |
|
||||
|
||||
## Kickoff clarifications
|
||||
|
||||
**No clarifications needed.** [E7M1-09](E7M1-prototype-backlog.md#e7m1-09--post-gameplayersidquestsquestidaccept) backlog, Linear AC, landed [NEO-117](NEO-117-implementation-plan.md) operations, and mutation HTTP precedents (`PlayerInventoryApi`, `PlayerCraftApi`, `AbilityCastApi`) settle scope:
|
||||
|
||||
- **Response envelope:** `accepted` + optional `reasonCode` + optional single **`quest`** row (`QuestProgressRowJson` shape 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 wrong `schemaVersion`.
|
||||
- **Player gate:** `IPositionStateStore` → **404** for unknown/blank ids (GET quest-progress / inventory precedent) before `TryAccept`.
|
||||
- **Unknown quest:** **200** + `accepted: false` + `reasonCode: unknown_quest` (operations layer; not HTTP 404).
|
||||
- **Deny with snapshot:** `already_active` / `already_completed` include current **`quest`** row 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](E7M1-prototype-backlog.md#e7m1-09--post-gameplayersidquestsquestidaccept)):**
|
||||
|
||||
- **`POST /game/players/{id}/quests/{questId}/accept`** — `QuestAcceptApi`; optional request body v1; response with **`accepted`**, optional **`reasonCode`**, optional **`quest`** progress row.
|
||||
- Wire **`QuestStateOperations.TryAccept`** (inventory refresh + step completion on accept already in operations).
|
||||
- Shared row projection with GET (`QuestProgressRowJson` from `QuestStepState` / 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.md` accept route section.
|
||||
|
||||
**Out of scope (from Linear + backlog):**
|
||||
|
||||
- Abandon/reset POST.
|
||||
- Godot HUD ([NEO-122](https://linear.app/neon-sprawl/issue/NEO-122)).
|
||||
- Telemetry hook comments ([NEO-121](https://linear.app/neon-sprawl/issue/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 **`reasonCode`** without mutation.
|
||||
- [ ] Bruno + integration tests pass in CI.
|
||||
|
||||
## Technical approach
|
||||
|
||||
1. **Route:** **`POST /game/players/{id}/quests/{questId}/accept`** — inject **`IPositionStateStore`**, **`IQuestDefinitionRegistry`**, **`IPlayerQuestStateStore`**, **`IPlayerInventoryStore`**, **`IItemDefinitionRegistry`**, **`TimeProvider`**.
|
||||
|
||||
2. **Request:** `QuestAcceptRequest` with `schemaVersion` **1**. Handler accepts **`null`**, empty JSON, or `{ "schemaVersion": 1 }`. **400** when body is non-null and `schemaVersion` ≠ 1.
|
||||
|
||||
3. **Player gate:** Trim `id`; return **404** when empty or position missing (mirror **`QuestProgressApi`** GET).
|
||||
|
||||
4. **Accept:** `QuestStateOperations.TryAccept(trimmedId, questId, …)` — `questId` from route (operations normalizes via registry).
|
||||
|
||||
5. **Response mapping** (`QuestAcceptResponse` v1):
|
||||
|
||||
| Outcome | HTTP | `accepted` | `reasonCode` | `quest` row |
|
||||
|---------|------|------------|--------------|-------------|
|
||||
| Success | 200 | `true` | omitted | active row from snapshot |
|
||||
| Deny (no snapshot) | 200 | `false` | operations code | omitted |
|
||||
| Deny (with snapshot) | 200 | `false` | operations code | row from snapshot (`already_active`, `already_completed`) |
|
||||
|
||||
Map `QuestStepState` → `QuestProgressRowJson` via shared helper extracted from **`QuestProgressApi.MapRow`** (e.g. **`MapQuestProgressRow`**) so GET and POST stay aligned.
|
||||
|
||||
6. **Register:** `app.MapQuestAcceptApi()` in **`Program.cs`** immediately after **`MapQuestProgressApi()`**.
|
||||
|
||||
7. **Bruno** (`bruno/neon-sprawl-server/quest-progress/`):
|
||||
|
||||
- **`Accept gather intro.bru`** — POST `dev-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`.
|
||||
|
||||
8. **Docs:** Add **`server/README.md`** POST accept section (curl, response shape, reason codes). Update E7.M1 module snapshot / alignment register / E7M1 backlog on story land.
|
||||
|
||||
### Expected v1 success response (prototype)
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"accepted": true,
|
||||
"quest": {
|
||||
"questId": "prototype_quest_gather_intro",
|
||||
"status": "active",
|
||||
"currentStepIndex": 0,
|
||||
"objectiveCounters": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Expected v1 deny response (prerequisite)
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"accepted": false,
|
||||
"reasonCode": "prerequisite_incomplete"
|
||||
}
|
||||
```
|
||||
|
||||
### Accept flow
|
||||
|
||||
```mermaid
|
||||
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, no counter drift. **Unknown quest:** bogus quest id → `unknown_quest`. **Optional body:** POST with no body 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** |
|
||||
Loading…
Reference in New Issue