8.9 KiB
NEO-41 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-41 |
| Title | Gather loop awards skill XP (sourceKind: activity) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-41/gather-loop-awards-skill-xp-sourcekind-activity |
| Labels | E3.M1, E2.M2, server |
| Depends on | NEO-38 — Done (authoritative POST …/skill-progression). |
Kickoff clarifications
| Topic | Question / note | Resolution |
|---|---|---|
| What counts as “gather completion”? | Repo has no GatherResult / E3.M1 gather API yet; InteractionApi + prototype resource_node interactable exist. |
User (AskQuestion): Prototype stand-in — treat a successful POST /game/players/{id}/interact whose resolved interactable has Kind == "resource_node" (today: PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId / prototype_resource_node_alpha) as gather complete for this slice. |
| XP amount | Linear does not fix per-gather XP. | Plan default: 10 XP per qualifying interact (prototype constant, server-side only), same order of magnitude as NEO-38 examples; adjust in implementation PR if product wants a different number before merge. |
| Target skill | Which SkillDef.id receives the grant? |
salvage — prototype gather anchor per E2_M1_SkillDefinitionRegistry.md; allowedXpSourceKinds already includes activity. |
Goal, scope, and out-of-scope
Goal: When a gather-class prototype interaction succeeds, apply skill XP through the same authoritative persistence and validation rules as NEO-38 (ISkillDefinitionRegistry allowlist, IPlayerSkillProgressionStore, ISkillLevelCurve), with sourceKind: activity. No ad-hoc client XP counters.
In scope (from Linear):
- Gather completion → skill XP via the same path as NEO-38 (shared server logic, not a duplicate client-only bump).
- Never emit a grant when
activityis not allowed for the chosen skill (guard + unit/integration coverage). - Testable (and manually verifiable) progression:
salvageXP increases after a successfulresource_nodeinteract; other interactables unchanged.
Out of scope:
- Full E3.M1
GatherResult/ yield tables / inventory (E3_M1_ResourceNodeAndGatherLoop.md backlog beyond this wire). - Content-driven XP amounts or multi-skill-per-node mapping (follow-up once gather content exists).
- Changing
InteractionResponseschema for XP echo (visibility viaGET …/skill-progressionafter interact is enough for this slice).
Acceptance criteria checklist
- Gather completion grants skill XP through the same authoritative path as NEO-38 (shared grant application; no ad-hoc client XP).
- Disallowed
sourceKind/ skill combinations never emitted from gather (defensive: onlysalvage+activityforresource_nodekind; registry check before apply). - Visible or testable progression: automated test (and manual QA) shows
salvageXP increases after successfulresource_nodeinteract from in-range position.
Technical approach
-
Extract NEO-38 grant apply logic from
SkillProgressionSnapshotApiinto a single shared helper or small service inGame/Skills/(e.g. staticSkillProgressionGrantOperations.ApplyGrantorISkillProgressionGrantApplier) that:- Accepts
playerId,skillId,amount,sourceKind, and dependencies (ISkillDefinitionRegistry,IPlayerSkillProgressionStore,ISkillLevelCurve). - Performs the same validation order as today: positive amount → known skill →
sourceKindinAllowedXpSourceKinds→TryApplyXpDelta→ level curve →SkillProgressionGrantResponse(success or structured deny with snapshot). - Does not re-check
IPositionStateStorefor POST callers that already gated; interact path must only run after existing interact Allowed path (player already known to exist at validated position).
- Accepts
-
SkillProgressionSnapshotApiPOST — delegate body handling to the shared helper so HTTP contract stays identical. -
InteractionApiPOST — afterAllowed == true, ifentry.Kind == "resource_node"(string compare ordinal against literal"resource_node"or a shared constant onPrototypeInteractableRegistry), call the helper withsalvage, fixedGatherActivityXpAmount(10),activity. On deny from helper, do not change interact outcome: interaction remains Allowed; XP grant is a silent no-op when validation fails (unexpected if catalog stable) — prefer: if deny, still return sameInteractionResponseas today (gather “succeeds” UX) but no XP applied; add unit test that corrupt catalog case does not throw. Clarification: ifTryApplyXpDeltareturns false (e.g. Postgres missing row), same — no XP, interaction still allowed. -
Constants —
PrototypeInteractableRegistry.ResourceNodeKindor documentKindstring"resource_node"as the gather trigger;GatherActivityXpAmount = 10in one place (e.g.GatherSkillXpConstants.cs). -
Telemetry — NEO-40 hook sites on
POST …/skill-progressionstill fire when clients use that API; interact-triggered grants should go through the same helper so persistence matches; optional follow-up to duplicate hook comments on interact path — out of scope unless trivial one-line reference in helper. -
Docs / Bruno / README — Describe “prototype gather = interact
prototype_resource_node_alphain range →salvageXP” and curl order: move → interact → GET progression.
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs |
Shared NEO-38 grant apply + snapshot envelope for POST and gather hook. |
server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantApplyOutcome.cs |
TryApplyGrant outcome kind + optional response (deny / granted / store missing). |
server/NeonSprawl.Server/Game/Skills/GatherSkillXpConstants.cs |
Prototype salvage + activity + XP amount for resource-node gather. |
server/NeonSprawl.Server.Tests/Game/Interaction/InteractionResourceNodeGatherXpTests.cs |
In-range resource_node interact increases salvage XP on GET snapshot; terminal interact does not; stub-registry deny path. |
server/NeonSprawl.Server.Tests/Game/Interaction/SalvageActivityDeniedRegistryWebApplicationFactory.cs |
Test host mirroring in-memory overrides with salvage excluding activity for defensive coverage. |
docs/manual-qa/NEO-41.md |
curl: move near node → interact → GET skill-progression. |
bruno/neon-sprawl-server/position/Post move near prototype resource node.bru |
Setup move for NEO-41 Bruno flow. |
bruno/neon-sprawl-server/interaction/Post interact prototype resource node gather.bru |
Bruno: interact prototype_resource_node_alpha after move. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs |
Call shared grant helper from POST handler; remove inlined duplicate logic. |
server/NeonSprawl.Server/Game/Interaction/InteractionApi.cs |
After successful interact on resource_node kind, invoke grant helper with salvage / activity / fixed amount; extend minimal API lambda parameter list for new services. |
server/README.md |
Short subsection: prototype gather XP via interact + link to NEO-41 manual QA. |
docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md |
One-sentence implementation anchor: prototype gather completion currently signaled via InteractionApi + Kind: resource_node until GatherResult exists. |
Tests
| Path | Coverage |
|---|---|
server/NeonSprawl.Server.Tests/Game/Skills/SkillProgressionGrantApiTests.cs |
Regression: existing POST cases still pass after refactor to shared helper. |
New or extended Interaction*Tests.cs |
resource_node interact from seeded/in-range position → GET …/skill-progression shows increased salvage XP; prototype_terminal interact → salvage XP unchanged. |
SalvageActivityDeniedRegistryWebApplicationFactory + third test method |
Stub ISkillDefinitionRegistry where salvage disallows activity: interact still allowed: true, salvage XP stays 0 (no throw). |
Open questions / risks
- Long-term: Real E3.M1
GatherResultshould become the single completion signal; this interact hook is a deliberate prototype stand-in — migrate grant call to gather pipeline without changing NEO-38 POST contract. - Risk: Players could spam interact for XP; out of scope for NEO-41 (cooldowns / depletion in E3.M1).