NEO-44: Add kickoff implementation plan for gig XP on combat defeat.
Documents E5M1-10 scope: breach main gig, 25 XP per targetDefeated, GET gig-progression snapshot, Postgres+in-memory store, and cast hook after NEO-82 combat resolution — outside E2.M2 skill XP.pull/118/head
parent
25876a16fd
commit
30d379c98c
|
|
@ -0,0 +1,164 @@
|
||||||
|
# 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](../decomposition/modules/E5_M1_CombatRulesEngine.md) · Epic 5 Slice 1 · backlog **E5M1-10** |
|
||||||
|
| **Branch** | `NEO-44-gig-xp-combat-defeat` |
|
||||||
|
| **Precursor** | [NEO-82](https://linear.app/neon-sprawl/issue/NEO-82) — cast POST + `combatResolution.targetDefeated` (**Done** on `main`) |
|
||||||
|
| **Pattern** | [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) — player-scoped GET snapshot; [NEO-42](https://linear.app/neon-sprawl/issue/NEO-42) — activity grant helper + constants; **not** `SkillProgressionGrantOperations` |
|
||||||
|
| **Blocks** | [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86) capstone optional gig visibility |
|
||||||
|
| **Client counterpart** | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) / [NEO-86](https://linear.app/neon-sprawl/issue/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](../game-design/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](E5M1-prototype-backlog.md#e5m1-10--gig-xp-on-combat-defeat-neo-44)):**
|
||||||
|
|
||||||
|
- 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](../game-design/gigs.md)).
|
||||||
|
- Gig XP on cast response wire block (kickoff: GET only).
|
||||||
|
- Data-driven gig level curves / content catalog.
|
||||||
|
- Godot gig bar — **[NEO-86](https://linear.app/neon-sprawl/issue/NEO-86)** optional.
|
||||||
|
- **`POST …/gig-progression`** grant route (combat hook is the only prototype writer).
|
||||||
|
- Mission / **`mission_reward`** skill XP — unchanged ([NEO-43](https://linear.app/neon-sprawl/issue/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](../game-design/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`** + **`PostgresGigProgressionBootstrap`** — **`V007__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](../game-design/progression.md); contrast with **`MissionRewardSkillXpGrant`** and gather/craft **`activity`** paths. Reconcile [E5M1-prototype-backlog.md](E5M1-prototype-backlog.md) E5M1-10 checkboxes and [E5_M1_CombatRulesEngine.md](../decomposition/modules/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 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.
|
||||||
Loading…
Reference in New Issue