From 48c59d45608de0d28f10f6ef2979b97e7b5024df Mon Sep 17 00:00:00 2001 From: VinPropane Date: Mon, 11 May 2026 13:03:45 -0400 Subject: [PATCH] NEO-43: add implementation plan (kickoff) --- docs/plans/NEO-43-implementation-plan.md | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/plans/NEO-43-implementation-plan.md diff --git a/docs/plans/NEO-43-implementation-plan.md b/docs/plans/NEO-43-implementation-plan.md new file mode 100644 index 0000000..ed4ec95 --- /dev/null +++ b/docs/plans/NEO-43-implementation-plan.md @@ -0,0 +1,83 @@ +# NEO-43 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-43 | +| **Title** | Mission / quest rewards grant skill XP (sourceKind: mission_reward) | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-43/mission-quest-rewards-grant-skill-xp-sourcekind-mission-reward | +| **Labels** | E7.M2, E2.M2, server, Story | +| **Blocked by (Linear text)** | [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) — **done** on `main` (grant stack + `allowedXpSourceKinds`). **E7.M2** [RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) — **design only** at kickoff; no quest hand-in / `QuestRewardBundle` code in `server/` yet. | + +## Kickoff clarifications + +| Topic | Question / note | Resolution | +|--------|-----------------|------------| +| **Delivery before E7.M2 router** | E7.M2 router does not exist in repo; how to ship NEO-43? | **AskQuestion** offered options; **user skipped** — **default (agent):** mirror **[NEO-42](NEO-42-implementation-plan.md)** — add a **thin static helper** + **xUnit coverage** + **README / manual QA** documenting the contract; **E7.M2** calls the helper from the real quest/mission reward apply path when that module lands. | +| **Reward shape** | Fixed prototype vs generic skill + amount? | **Default:** **generic** parameters **`skillId`** + **`amount`** (positive int) with **`sourceKind`** fixed to catalog value **`mission_reward`** (matches future explicit reward line items; content chooses skill and payout). | +| **Combat encounters ≠ skill XP** | No encounter/gig XP code under `server/` today. | **Default:** satisfy AC by **(1)** plan + README stating the invariant; **(2)** confirming current grant call sites are **interact (gather)**, **refine prep (NEO-42)**, and **`POST …/skill-progression`** only — **no** combat encounter pipe. Future **E5 / encounter reward** work must **not** call `SkillProgressionGrantOperations` for default encounter outcomes (gig XP remains separate per [progression.md](../game-design/progression.md)). Optional follow-up: architecture test or doc lint if the team wants a machine-enforced guard once encounter code exists. | + +## Goal, scope, and out-of-scope + +**Goal:** When **E7.M2** (or any scripted mission/quest hand-in) applies **explicit** skill XP from a **mission reward** channel, route the grant through **E2.M2** / **NEO-38** using **`sourceKind: mission_reward`**, validated against the target skill’s **`allowedXpSourceKinds`**. **Not** the combat encounter pipe ([progression.md](../game-design/progression.md) **Mission rewards vs combat XP**). + +**In scope (from Linear + defaults above):** + +- **Preparatory (this slice):** one **server-side** helper that delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** with caller-supplied **`skillId`** / **`amount`** and fixed **`mission_reward`** `sourceKind`. +- **Tests:** happy path (grant applies when allowlist permits); deny path when **`mission_reward`** ∉ **`allowedXpSourceKinds`** for the chosen skill (stub registry pattern aligned with NEO-41/NEO-42 denied factories). +- **Docs:** README subsection + **`docs/manual-qa/NEO-43.md`** (verify via **`POST …/skill-progression`** with **`mission_reward`** and via helper in a test host, same spirit as NEO-42). + +**Out of scope:** + +- Full **`QuestRewardBundle`** schema, idempotent **`RewardDeliveryEvent`**, inventory/unlocks ([E7_M2_RewardAndUnlockRouter.md](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) backlog). +- **Combat encounter** implementation or gig XP awards. +- **Batch** mission grants in one HTTP call (can follow once router batches rewards). + +## Acceptance criteria checklist + +- [ ] **Scripted mission rewards can include skill XP;** validated against **`allowedXpSourceKinds`** (helper + tests; manual QA documents **`POST …/skill-progression`** with **`mission_reward`** for skills that allow it). +- [ ] **Combat encounters do not grant skill XP by default** — documented invariant + **current codebase** has no encounter grant path; gig XP remains separate (design reference: [progression.md](../game-design/progression.md), [E2_M1](../decomposition/modules/E2_M1_SkillDefinitionRegistry.md)). + +## Technical approach + +1. Add **`MissionRewardSkillXpConstants`** (or equivalent single type) holding the canonical **`mission_reward`** string used by the helper (avoid string drift; match **`prototype_skills.json`** / NEO-38 docs). + +2. Add **`MissionRewardSkillXpGrant`** (static class, parallel to **`RefineActivitySkillXpGrant`**): + - Method e.g. **`GrantFromMissionReward(playerId, skillId, amount, registry, xpStore, levelCurve)`** calling **`TryApplyGrant`** with **`MissionRewardSkillXpConstants.SourceKind`**. + - **Caller contract:** `playerId` normalized like other grant hooks; **`amount`** > 0 (delegate invalid amount to existing deny behavior); outcome may be ignored on “fire and forget” success paths or inspected when E7.M2 needs telemetry/deny handling. + +3. **Tests** — factory with registry where a prototype skill **excludes** **`mission_reward`** for deny coverage; in-memory progression store; assert snapshot / store deltas consistent with **`RefineActivitySkillXpGrantTests`** style. + +4. **Documentation** — **`server/README.md`**: short “Mission / quest skill XP (NEO-43)” subsection: E7.M2 should call **`MissionRewardSkillXpGrant`**; until then verify **`mission_reward`** via **`POST …/skill-progression`**; link **`docs/manual-qa/NEO-43.md`**. + +5. **Decomposition anchor** — one sentence in **`docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md`** (or **`documentation_and_implementation_alignment.md`** if E7.M2 file should stay design-pure): server prep hook name + NEO-43 plan link. + +## Files to add + +| Path | Purpose | +|------|---------| +| `server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpConstants.cs` | Canonical `mission_reward` source kind string for mission/quest grant path. | +| `server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpGrant.cs` | Static helper delegating to `SkillProgressionGrantOperations.TryApplyGrant` with fixed `mission_reward`. | +| `server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardSkillXpGrantTests.cs` | Happy path + GET snapshot alignment (optional mirror NEO-42); deny when `mission_reward` not allowed. | +| `server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardDeniedRegistryWebApplicationFactory.cs` | WebApplicationFactory with stub `ISkillDefinitionRegistry` where target skill omits `mission_reward` from `allowedXpSourceKinds`. | +| `docs/manual-qa/NEO-43.md` | curl/Bruno order: grant via `POST …/skill-progression` with `mission_reward` + note future E7.M2 calls helper. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/README.md` | Document NEO-43 mission-reward skill XP hook and verification until E7.M2 wires quest hand-in. | +| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | Implementation anchor: which server helper to call when applying skill XP from `QuestRewardBundle` / mission payout. | + +## Tests + +| Path | Coverage | +|------|----------| +| `server/NeonSprawl.Server.Tests/Game/Skills/MissionRewardSkillXpGrantTests.cs` | **Granted:** helper applies XP when `mission_reward` ∈ `allowedXpSourceKinds` for chosen `skillId`; optional round-trip **`GET …/skill-progression`**. **Denied:** stub registry without `mission_reward` on target skill → no XP delta; no throw. | +| Existing `SkillProgressionGrantApiTests.cs` | Regression only if POST path or shared types change during implementation (likely unchanged). | + +## Open questions / risks + +- **User override:** If you want a **fixed prototype** reward (single skill + constant XP) instead of **generic skillId + amount**, say so before implementation — the helper signature and tests adjust trivially. +- **Risk:** Low — additive surface; same persistence and validation as NEO-38; helper **unreferenced** until E7.M2 (same pattern as NEO-42).