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

14 KiB
Raw Blame History

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 AbilityCastApi after successful CombatOperations.TryResolve when result.TargetDefeated is true.
  • GET /game/players/{id}/gig-progression — versioned snapshot with prototype mainGigId + single row for breach (xp, level: 1).
  • Integration tests (AAA): defeat grants gig XP once; target_defeated re-hit does not; skill snapshot unchanged.
  • Bruno: gig-progression GET + extend defeat spine to assert gig XP after 4th pulse.
  • server/README.md gig 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-progression grant route (combat hook is the only prototype writer).
  • Mission / mission_reward skill 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_alpha grants gig XP once per defeat transition.
  • Skill progression snapshot unchanged by combat defeat in tests.
  • Documented invariant matches progression.md.

Technical approach

  1. Constants (GigProgressionConstants) — frozen prototype values:

    • PrototypeMainGigId = breach
    • CombatDefeatXpAmount = 25
    • Document: all players implicitly main breach until loadout/hub swap lands.
  2. 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 as InMemoryPlayerSkillProgressionStore).
    • PostgresPlayerGigProgressionStore + PostgresGigProgressionBootstrapV007__player_gig_progression.sql; FK to player_position like V003.
    • GigProgressionServiceCollectionExtensions.AddGigProgressionStore — Postgres when connection string set, else in-memory.
  3. Grant helper (CombatDefeatGigXpGrant) — static method invoked from cast accept path:

    • GrantOnCombatDefeat(playerId, gigStore)TryApplyXpDelta(playerId, breach, 25, …).
    • No dependency on ISkillDefinitionRegistry, IPlayerSkillProgressionStore, or SkillProgressionGrantOperations.
  4. Cast hook (AbilityCastApi) — after combat success, before returning accept JSON:

    • If combatResult.TargetDefeated: call CombatDefeatGigXpGrant.GrantOnCombatDefeat(trimmedPlayerId, gigStore).
    • Inject IPlayerGigProgressionStore alongside existing combat dependencies.
    • Cast response unchanged (no gigXpGrant block — kickoff decision).
  5. GET snapshot (GigProgressionSnapshotApi)GET /game/players/{id}/gig-progression:

    • 404 when player unknown to IPositionStateStore (NEO-37 gate).
    • Response schemaVersion 1, playerId, mainGigId (breach), gigs: array with one row { id: "breach", xp, level: 1 } (level fixed until gig curves exist).
    • Wire in Program.cs via app.MapGigProgressionSnapshotApi().
  6. Tests — extend InMemoryWebApplicationFactory to replace IPlayerGigProgressionStore with in-memory implementation (same loop as skill store).

  7. Bruno (bruno/neon-sprawl-server/gig-progression/):

    • Get gig progression.bru — fresh server: xp 0, level 1, mainGigId breach.
    • Get gig progression after defeat spine.bru — pre-request: reuse ability-cast defeat spine (4 pulses on alpha); GET asserts breach.xp === 25; optional GET skill-progression asserts salvage/refine unchanged from pre-defeat baseline.
  8. Docs — README subsection: combat → gig XP invariant; link progression.md; contrast with MissionRewardSkillXpGrant and gather/craft activity paths. 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 13 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-progressionbreach.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 per targetDefeated transition.
  • 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.md for server verification spine.

Reconciliation (implementation)

  • Game/Gigs/IPlayerGigProgressionStore, in-memory + Postgres (V007), CombatDefeatGigXpGrant, GET …/gig-progression.
  • AbilityCastApi — best-effort gig XP grant when combatResult.TargetDefeated on accept path.
  • Tests: GigProgressionSnapshotApiTests, CombatDefeatGigXpGrantTests, extended AbilityCastApiTests defeat + skill unchanged; optional Postgres persistence test.
  • Bruno gig-progression/; server/README.md, E5M1 backlog, E5_M1 module, alignment register updated.