# NEO-42 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-42 | | **Title** | Craft / refine awards skill XP (sourceKind: activity) | | **Linear** | https://linear.app/neon-sprawl/issue/NEO-42/craft-refine-awards-skill-xp-sourcekind-activity | | **Labels** | E3.M2, E2.M2, server | | **Blocked by (Linear)** | [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) (grant path — **done** on `main`); **E3.M2** recipe/refine execution (not present in repo at kickoff). | ## Kickoff clarifications | Topic | Question / note | Resolution | |--------|-----------------|------------| | **Delivery shape** | User asked what to recommend until E3.M2 HTTP/recipe loop exists. | **Agent recommendation (accepted unless you object in chat):** add **`RefineSkillXpConstants`** + a thin **`RefineActivitySkillXpGrant`** (or equivalent) that delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** — **no** new public craft HTTP in NEO-42. E3.M2 calls this helper from the real **`CraftResult`** success path when that module lands. | | **Prototype XP amount** | User asked what to recommend. | **Agent recommendation:** **10** XP per successful craft/refine completion for the prototype, matching **[NEO-41](NEO-41-implementation-plan.md)** gather constant pattern; replace with recipe-driven amounts in a later issue when content exists. | | **“Craft” vs “refine” skill id** | `content/skills/prototype_skills.json` defines **`refine`** (process) with **`activity`** in `allowedXpSourceKinds`; there is no separate **`craft`** skill row today. | **Prototype mapping:** both **recipe craft** and **refine** success paths call the helper with **`skillId: refine`** and **`sourceKind: activity`** so allowlist enforcement matches catalog. If a dedicated craft skill is added later, E3.M2 can call the same helper with a different `skillId` per recipe metadata. | ## Goal, scope, and out-of-scope **Goal:** When **E3.M2** reports a **successful** craft or refine completion, award **refine** skill XP through the **NEO-38** grant stack (`TryApplyGrant` → registry allowlist → store → level curve) with **`sourceKind: activity`** wherever **`activity`** is listed on the target skill’s **`allowedXpSourceKinds`**. **In scope (from Linear):** - Wire (or **prepare the single call site** for) craft/refine completion → **`SkillProgressionGrantOperations.TryApplyGrant`** with **`activity`**. - **Respect catalog allowlists** (no grant when `activity` is disallowed — mirror NEO-41 defensive tests). **Out of scope:** - Owning the full **E3.M2** recipe validation, inventory mutations, or new public craft routes (separate story/module). - **Recipe-driven XP amounts** and multi-skill mapping from **`RecipeDef`** (follow-up once execution + content exist). - **Telemetry** (`NEO-40`) — comment-only hook sites already noted on grant path. ## Acceptance criteria checklist - [x] **Preparatory:** **`RefineActivitySkillXpGrant`** invokes the **NEO-38** path (**`TryApplyGrant`**, same stack as **`POST …/skill-progression`**). **Follow-up:** **E3.M2** success handler must call **`GrantOnSuccessfulCraftOrRefine`** (documented in **`server/README.md`** + manual QA); no craft route in repo yet. - [x] Grant channels respect **skill catalog** `allowedXpSourceKinds` (stub registry test: **`refine`** without **`activity`** → no XP). ## Decisions (implementation) - **`RefineActivityDeniedRegistryWebApplicationFactory`** added for allowlist-deny coverage (parallel to NEO-41 salvage-denied factory). ## Technical approach 1. **Constants** — Add `RefineSkillXpConstants` (parallel to `GatherSkillXpConstants`): **`refine`** skill id, **`activity`** source kind, **10** XP per completion (name clearly scoped to prototype craft/refine success). 2. **Shared grant helper** — Add a small static type (e.g. `RefineActivitySkillXpGrant`) whose sole job is to invoke **`SkillProgressionGrantOperations.TryApplyGrant`** with those constants. **Discard** the outcome on success paths intended to mirror gather (UI reads **`GET …/skill-progression`**); callers that need structured denies can call **`TryApplyGrant`** directly later. 3. **E3.M2 integration** — When **`CraftResult`** (or equivalent) success is finalized in server code, add **one call** to the helper with `playerId` + injected `ISkillDefinitionRegistry`, `IPlayerSkillProgressionStore`, `ISkillLevelCurve`. If E3.M2 is still absent when NEO-42 implements (1)–(2), document the **exact line / type** in this plan’s **Decisions** after the merge that adds the success handler. 4. **Tests** — (a) Happy path: grant applies **refine** XP with **`activity`** when catalog allows it. (b) Deny path: stub registry where **`refine`** disallows **`activity`** → helper run leaves **0** XP (same spirit as `SalvageActivityDeniedRegistryWebApplicationFactory` in NEO-41). Use **in-memory** store + real or test JSON catalog + placeholder level curve; **no** dependency on craft HTTP. 5. **Manual QA** — If only (1)–(2) ship before E3.M2: **`docs/manual-qa/NEO-42.md`** can note “verify via unit tests + temporary `POST …/skill-progression` control grant for `refine`/`activity`”; once E3.M2 exists, extend checklist to **Bruno** craft success + **GET** progression. ## Files to add | Path | Purpose | |------|---------| | `server/NeonSprawl.Server/Game/Skills/RefineSkillXpConstants.cs` | NEO-42 prototype constants: `refine`, `activity`, flat XP amount. | | `server/NeonSprawl.Server/Game/Skills/RefineActivitySkillXpGrant.cs` | Single entry point calling `TryApplyGrant` for successful craft/refine (prototype). | | `server/NeonSprawl.Server.Tests/Game/Skills/RefineActivitySkillXpGrantTests.cs` | AAA tests: grant when allowed; no XP when `activity` not in allowlist for `refine`. | | `server/NeonSprawl.Server.Tests/Game/Skills/RefineActivityDeniedRegistryWebApplicationFactory.cs` | Test host with **`refine`** catalog row that omits **`activity`**. | | `docs/manual-qa/NEO-42.md` | Curl checklist + E3.M2 follow-up note. | ## Files to modify | Path | Rationale | |------|-----------| | `server/README.md` | Document NEO-42 hook type and E3.M2 integration expectation. | | **E3.M2 craft success handler** (path TBD) | Call **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** once on successful craft/refine — **not done in this PR** (module absent). | ## Tests | Test file | What it covers | |-----------|------------------| | `RefineActivitySkillXpGrantTests.cs` | **Arrange / Act / Assert:** successful grant increases **`refine`** XP via shared path; registry without **`activity`** for **`refine`** yields **no** XP delta (allowlist respected). | ## Open questions / risks - **Merge ordering:** If NEO-42 merges **before** E3.M2, acceptance checkbox “craft/refine completion” is only fully satisfied after a tiny follow-up commit adds the call in the new success handler — track in Linear or a child issue if needed. - **XP balance:** Flat **10** is a placeholder; design may change before public playtests.