7.8 KiB
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 — done on main (grant stack + allowedXpSourceKinds). E7.M2 RewardAndUnlockRouter — 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 — 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). 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 Mission rewards vs combat XP).
In scope (from Linear + defaults above):
- Preparatory (this slice): one server-side helper that delegates to
SkillProgressionGrantOperations.TryApplyGrantwith caller-suppliedskillId/amountand fixedmission_rewardsourceKind. - Tests: happy path (grant applies when allowlist permits); deny path when
mission_reward∉allowedXpSourceKindsfor the chosen skill (stub registry pattern aligned with NEO-41/NEO-42 denied factories). - Docs: README subsection +
docs/manual-qa/NEO-43.md(verify viaPOST …/skill-progressionwithmission_rewardand via helper in a test host, same spirit as NEO-42).
Out of scope:
- Full
QuestRewardBundleschema, idempotentRewardDeliveryEvent, inventory/unlocks (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 documentsPOST …/skill-progressionwithmission_rewardfor 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, E2_M1).
Technical approach
-
Add
MissionRewardSkillXpConstants(or equivalent single type) holding the canonicalmission_rewardstring used by the helper (avoid string drift; matchprototype_skills.json/ NEO-38 docs). -
Add
MissionRewardSkillXpGrant(static class, parallel toRefineActivitySkillXpGrant):- Method e.g.
GrantFromMissionReward(playerId, skillId, amount, registry, xpStore, levelCurve)callingTryApplyGrantwithMissionRewardSkillXpConstants.SourceKind. - Caller contract:
playerIdnormalized 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.
- Method e.g.
-
Tests — factory with registry where a prototype skill excludes
mission_rewardfor deny coverage; in-memory progression store; assert snapshot / store deltas consistent withRefineActivitySkillXpGrantTestsstyle. -
Documentation —
server/README.md: short “Mission / quest skill XP (NEO-43)” subsection: E7.M2 should callMissionRewardSkillXpGrant; until then verifymission_rewardviaPOST …/skill-progression; linkdocs/manual-qa/NEO-43.md. -
Decomposition anchor — one sentence in
docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md(ordocumentation_and_implementation_alignment.mdif 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).