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

89 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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? | **User (implementation):** accepted agent recommendation — **NEO-42-style prep:** **`MissionRewardSkillXpGrant`** + tests + README + manual QA; **E7.M2** wires the real quest hand-in later. |
| **Reward shape** | Fixed prototype vs generic skill + amount? | **User (implementation):** accepted agent recommendation — **generic** **`skillId`** + **`amount`**; helper fixes **`sourceKind: mission_reward`**. |
| **Combat encounters ≠ skill XP** | No encounter/gig XP code under `server/` today. | **User (implementation):** accepted agent recommendation — **README + manual QA + plan** document the invariant and current call sites; no synthetic encounter guard test until 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 skills **`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
- [x] **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).
- [x] **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)).
## Decisions (implementation)
- **`MissionRewardSkillXpConstants.MissionRewardSourceKind`** — single canonical string for the helper and docs.
- **`MissionRewardDeniedRegistryWebApplicationFactory`** — **`salvage`** stub omits **`mission_reward`** (deny coverage); happy paths use disk catalog via **`InMemoryWebApplicationFactory`**.
- **Code review follow-up (2026-05-15):** **`documentation_and_implementation_alignment.md`** E2.M2 row lists **NEO-43 landed** and **Slice 3 still open****NEO-44** only; **`E7_M2_RewardAndUnlockRouter.md`** Summary **Status** notes NEO-43 prep; **`NEO-43-implementation-plan.md`** technical approach uses **`MissionRewardSkillXpConstants.MissionRewardSourceKind`** (matches code).
## 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.MissionRewardSourceKind`**.
- **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
None. **Risk:** Low — additive surface; helper **unreferenced** until E7.M2 (same pattern as NEO-42).