14 KiB
NEO-44 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-44 |
| Title | E5M1-10: Combat encounter awards gig XP only (no skill XP via E2.M2) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-44/e5m1-10-combat-encounter-awards-gig-xp-only-no-skill-xp-via-e2m2 |
| Module | E5.M1 — CombatRulesEngine · Epic 5 Slice 1 · backlog E5M1-10 |
| Branch | NEO-44-gig-xp-combat-defeat |
| Precursor | NEO-82 — cast POST + combatResolution.targetDefeated (Done on main) |
| Pattern | NEO-37 — player-scoped GET snapshot; NEO-42 — activity grant helper + constants; not SkillProgressionGrantOperations |
| Blocks | NEO-86 capstone optional gig visibility |
| Client counterpart | NEO-85 / NEO-86 — Godot gig row optional (out of scope here; no client change) |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Prototype main gig id | Which gig id receives combat XP before loadout/hub swap exists? | Frozen breach — E5M1-10 backlog default; damage gig from gigs.md. |
User: breach. |
| Gig XP per defeat | How much XP on targetDefeated transition? |
25 — matches prototype_pulse baseDamage; one readable step per alpha defeat. |
User: 25. |
| Wire visibility | GET route vs cast response block? | GET /game/players/{id}/gig-progression only — NEO-37 pattern; keeps gig path outside E2.M2. |
User: GET only. |
| Persistence | Postgres migration vs in-memory only? | Postgres + in-memory split — mirror IPlayerSkillProgressionStore / V003 (NEO-29-style). |
User: Postgres + in-memory. |
| Level field | Derive level from curve or fixed? | level: 1 fixed, xp accumulates — no gig level-curve content yet (NEO-37 pre-NEO-39 pattern). |
Adopted (implicit with GET-only + prototype scope). |
Goal, scope, and out-of-scope
Goal: When combat resolution transitions a prototype target to defeated (combatResolution.targetDefeated === true), grant gig XP to the attacker's main gig (breach) via a dedicated gig progression store. Do not call SkillProgressionGrantOperations or mutate skill XP. Expose a read-only GET snapshot for verification.
In scope (from Linear + E5M1-10):
- Minimal
IPlayerGigProgressionStore+ in-memory + Postgres implementations (V007__player_gig_progression.sql). CombatDefeatGigXpGrant(or equivalent) +GigProgressionConstants(breach, 25 XP per defeat).- Hook in
AbilityCastApiafter successfulCombatOperations.TryResolvewhenresult.TargetDefeatedis true. GET /game/players/{id}/gig-progression— versioned snapshot with prototypemainGigId+ single row forbreach(xp,level: 1).- Integration tests (AAA): defeat grants gig XP once;
target_defeatedre-hit does not; skill snapshot unchanged. - Bruno: gig-progression GET + extend defeat spine to assert gig XP after 4th pulse.
server/README.mdgig progression + combat defeat invariant;docs/manual-qa/NEO-44.md.
Out of scope (from Linear + backlog):
- Full nine-gig catalog, hub swap, sub-gig XP (gigs.md).
- Gig XP on cast response wire block (kickoff: GET only).
- Data-driven gig level curves / content catalog.
- Godot gig bar — NEO-86 optional.
POST …/gig-progressiongrant route (combat hook is the only prototype writer).- Mission /
mission_rewardskill XP — unchanged (NEO-43).
Acceptance criteria checklist
- Combat awards gig XP (main gig
breach); no default skill XP grant from encounters. - Mission/scripted skill payouts remain on the mission_reward / reward-router path.
- Defeating
prototype_target_alphagrants gig XP once per defeat transition. - Skill progression snapshot unchanged by combat defeat in tests.
- Documented invariant matches progression.md.
Technical approach
-
Constants (
GigProgressionConstants) — frozen prototype values:PrototypeMainGigId=breachCombatDefeatXpAmount= 25- Document: all players implicitly main
breachuntil loadout/hub swap lands.
-
Store (
Game/Gigs/) — parallel to skill progression, keyed by(playerId, gigId):IPlayerGigProgressionStore:GetXpTotals(playerId),TryApplyXpDelta(playerId, gigId, amount, out previous, out new).InMemoryPlayerGigProgressionStore— seeds dev player bucket (same pattern asInMemoryPlayerSkillProgressionStore).PostgresPlayerGigProgressionStore+PostgresGigProgressionBootstrap—V007__player_gig_progression.sql; FK toplayer_positionlike V003.GigProgressionServiceCollectionExtensions.AddGigProgressionStore— Postgres when connection string set, else in-memory.
-
Grant helper (
CombatDefeatGigXpGrant) — static method invoked from cast accept path:GrantOnCombatDefeat(playerId, gigStore)→TryApplyXpDelta(playerId, breach, 25, …).- No dependency on
ISkillDefinitionRegistry,IPlayerSkillProgressionStore, orSkillProgressionGrantOperations.
-
Cast hook (
AbilityCastApi) — after combat success, before returning accept JSON:- If
combatResult.TargetDefeated: callCombatDefeatGigXpGrant.GrantOnCombatDefeat(trimmedPlayerId, gigStore). - Inject
IPlayerGigProgressionStorealongside existing combat dependencies. - Cast response unchanged (no
gigXpGrantblock — kickoff decision).
- If
-
GET snapshot (
GigProgressionSnapshotApi) —GET /game/players/{id}/gig-progression:- 404 when player unknown to
IPositionStateStore(NEO-37 gate). - Response
schemaVersion1,playerId,mainGigId(breach),gigs: array with one row{ id: "breach", xp, level: 1 }(level fixed until gig curves exist). - Wire in
Program.csviaapp.MapGigProgressionSnapshotApi().
- 404 when player unknown to
-
Tests — extend
InMemoryWebApplicationFactoryto replaceIPlayerGigProgressionStorewith in-memory implementation (same loop as skill store). -
Bruno (
bruno/neon-sprawl-server/gig-progression/):Get gig progression.bru— fresh server:xp0,level1,mainGigIdbreach.Get gig progression after defeat spine.bru— pre-request: reuse ability-cast defeat spine (4 pulses on alpha); GET assertsbreach.xp === 25; optional GET skill-progression assertssalvage/refineunchanged from pre-defeat baseline.
-
Docs — README subsection: combat → gig XP invariant; link progression.md; contrast with
MissionRewardSkillXpGrantand gather/craftactivitypaths. Reconcile E5M1-prototype-backlog.md E5M1-10 checkboxes and E5_M1_CombatRulesEngine.md when landed.
Expected outcomes (4th pulse defeats alpha)
| Step | Cast accepted |
targetDefeated |
Gig XP (breach) |
Skill XP (salvage/refine) |
|---|---|---|---|---|
| Pulses 1–3 | true | false | 0 | unchanged |
| Pulse 4 | true | true | +25 → 25 | unchanged |
| Pulse 5 | false (target_defeated) |
— | 25 (no delta) | unchanged |
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Gigs/GigProgressionConstants.cs |
Frozen breach main gig + 25 combat defeat XP. |
server/NeonSprawl.Server/Game/Gigs/IPlayerGigProgressionStore.cs |
Gig XP persistence contract. |
server/NeonSprawl.Server/Game/Gigs/InMemoryPlayerGigProgressionStore.cs |
Dev/test in-memory store. |
server/NeonSprawl.Server/Game/Gigs/PostgresPlayerGigProgressionStore.cs |
Postgres-backed store. |
server/NeonSprawl.Server/Game/Gigs/PostgresGigProgressionBootstrap.cs |
Flyway DDL bootstrap for V007. |
server/NeonSprawl.Server/Game/Gigs/GigProgressionServiceCollectionExtensions.cs |
DI registration (Postgres vs in-memory). |
server/NeonSprawl.Server/Game/Gigs/CombatDefeatGigXpGrant.cs |
Defeat transition grant helper. |
server/NeonSprawl.Server/Game/Gigs/GigProgressionSnapshotApi.cs |
GET …/gig-progression route. |
server/NeonSprawl.Server/Game/Gigs/GigProgressionSnapshotDtos.cs |
Versioned response DTOs. |
server/db/migrations/V007__player_gig_progression.sql |
Postgres DDL for gig XP totals. |
server/NeonSprawl.Server.Tests/Game/Gigs/GigProgressionSnapshotApiTests.cs |
GET snapshot + 404 gate (AAA). |
server/NeonSprawl.Server.Tests/Game/Gigs/CombatDefeatGigXpGrantTests.cs |
Unit tests for grant helper. |
server/NeonSprawl.Server.Tests/Game/Gigs/GigProgressionGrantPersistenceIntegrationTests.cs |
Optional Postgres round-trip (mirror NEO-38 pattern). |
bruno/neon-sprawl-server/gig-progression/Get gig progression.bru |
Fresh snapshot smoke. |
bruno/neon-sprawl-server/gig-progression/Get gig progression after defeat spine.bru |
Defeat → gig XP AC spine. |
bruno/neon-sprawl-server/gig-progression/folder.bru |
Run order + cross-links. |
docs/manual-qa/NEO-44.md |
Bruno/curl defeat → GET gig XP; skill snapshot unchanged. |
docs/plans/NEO-44-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs |
Inject gig store; call grant when TargetDefeated on combat success. |
server/NeonSprawl.Server/Program.cs |
AddGigProgressionStore + MapGigProgressionSnapshotApi. |
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs |
Replace Postgres gig store with in-memory in tests. |
server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs |
Defeat chain: gig XP +25 on 4th pulse; 5th no delta; skill GET unchanged. |
server/README.md |
Gig progression GET + combat defeat → gig XP invariant. |
docs/plans/E5M1-prototype-backlog.md |
Mark E5M1-10 landed when complete. |
docs/decomposition/modules/E5_M1_CombatRulesEngine.md |
E5M1-10 handoff paragraph + Linear row. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E5.M1 / E2.M2 Slice 3 rows when landed. |
Tests
| File | Coverage |
|---|---|
server/NeonSprawl.Server.Tests/Game/Gigs/CombatDefeatGigXpGrantTests.cs |
Grant: delta +25 on breach. Idempotent store failure: no throw when player missing. AAA. |
server/NeonSprawl.Server.Tests/Game/Gigs/GigProgressionSnapshotApiTests.cs |
GET 404 unknown player; GET dev player xp 0, level 1, mainGigId breach. AAA. |
server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs |
Defeat chain: after 4th pulse, GET …/gig-progression → breach.xp 25; GET …/skill-progression salvage/refine unchanged; 5th cast deny → gig XP still 25. AAA. |
server/NeonSprawl.Server.Tests/Game/Gigs/GigProgressionGrantPersistenceIntegrationTests.cs |
Optional: Postgres store persists defeat grant (skip if no test DB — mirror skill persistence test guard). |
Bruno defeat spine + manual QA complement automated coverage.
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| Main gig assignment without loadout | Frozen breach for all players (user kickoff). |
adopted |
| Gig XP amount | 25 per defeat (user kickoff). | adopted |
| Cast response gig block | Defer — GET only (user kickoff); NEO-85/86 may poll GET. | adopted |
| Level curve absence | level: 1 fixed until gig content issue. |
adopted |
| Beta dummy defeat | Same grant path for any prototype target defeat (alpha AC explicit; beta covered by shared hook). | adopted |
| Grant failure on defeat | Best-effort apply — cast accept still returns combat resolution even if gig store write fails (log/monitor later); prototype in-memory path should not fail for known dev player. | adopted |
Decisions (kickoff)
- Prototype main gig
breach; 25 gig XP pertargetDefeatedtransition. - Dedicated
GET /game/players/{id}/gig-progression; no cast wire extension. - Postgres + in-memory gig store (V007); no E2.M2 skill grant on combat defeat.
docs/manual-qa/NEO-44.mdfor server verification spine.
Reconciliation (implementation)
Game/Gigs/—IPlayerGigProgressionStore, in-memory + Postgres (V007),CombatDefeatGigXpGrant,GET …/gig-progression.AbilityCastApi— best-effort gig XP grant whencombatResult.TargetDefeatedon accept path.- Tests:
GigProgressionSnapshotApiTests,CombatDefeatGigXpGrantTests, extendedAbilityCastApiTestsdefeat + skill unchanged; optional Postgres persistence test. - Bruno
gig-progression/;server/README.md, E5M1 backlog, E5_M1 module, alignment register updated.