From fc09808482d7896644e9183a7d6f0df9b064f94c Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 18:05:42 -0400 Subject: [PATCH] NEO-106: Add kickoff implementation plan for combat encounter wiring. --- docs/plans/NEO-106-implementation-plan.md | 134 ++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/plans/NEO-106-implementation-plan.md diff --git a/docs/plans/NEO-106-implementation-plan.md b/docs/plans/NEO-106-implementation-plan.md new file mode 100644 index 0000000..5bb893f --- /dev/null +++ b/docs/plans/NEO-106-implementation-plan.md @@ -0,0 +1,134 @@ +# NEO-106 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-106 | +| **Title** | E5M3-07: Wire combat defeat → encounter progress/completion | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-106/e5m3-07-wire-combat-defeat-encounter-progresscompletion | +| **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-07** | +| **Branch** | `NEO-106-wire-combat-defeat-encounter-progress-completion` | +| **Precursor** | [NEO-105](https://linear.app/neon-sprawl/issue/NEO-105) — `EncounterCompletionOperations.TryCompleteAndGrant` + inventory grants (**landed on `main`**) | +| **Pattern** | [NEO-44](NEO-44-implementation-plan.md) / `CombatDefeatGigXpGrant` — static wiring helper called from `AbilityCastApi`; [NEO-104](NEO-104-implementation-plan.md) / [NEO-105](NEO-105-implementation-plan.md) — encounter ops already exist | +| **Blocks** | [NEO-108](https://linear.app/neon-sprawl/issue/NEO-108) — `GET /game/players/{id}/encounter-progress` + Bruno; [NEO-109](https://linear.app/neon-sprawl/issue/NEO-109) — telemetry hook sites (activates `encounter_start` comment from NEO-104) | +| **Client counterpart** | [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) — encounter progress + loot feedback labels (via NEO-108 HTTP read) | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Wiring structure** | Inline in `AbilityCastApi` vs dedicated helper? | **`EncounterCombatWiring` static helper** — mirror `CombatDefeatGigXpGrant`; keeps cast route readable; unit-testable without HTTP. | **Adopted** | +| **Activation trigger** | First damaging cast vs any successful accept? | **First cast with `DamageDealt > 0`** against an encounter NPC — E5M3 kickoff default (“first damaging cast”); colocate with existing aggro block. | **Adopted** (backlog + NEO-104 precedent; not separately asked) | +| **Integration tests** | `AbilityCastApiTests` in-proc vs Bruno defeat-all-three now? | **Extend `AbilityCastApiTests` only** — existing defeat spine helpers + NEO-44 gig XP test precedent; Bruno deferred to NEO-108 per backlog. | **Adopted** | +| **Cast response DTO** | Expose encounter completion on `AbilityCastResponse`? | **No** — NEO-108 adds authoritative GET; cast response unchanged (inventory + stores verified in tests). | **Adopted** (backlog out-of-scope; not separately asked) | +| **Completion on inventory deny** | Third defeat when bag full? | **Defeat progress recorded; `TryCompleteAndGrant` fail-closed per NEO-105** — no completion mark, no grants; player can retry when bag has space (future HTTP will reflect `active` + all-defeated). | **Adopted** (NEO-105 contract; not separately asked) | + +## Goal, scope, and out-of-scope + +**Goal:** Hook the **`POST …/ability-cast`** accept path so NPC defeats update encounter progress and invoke **`EncounterCompletionOperations.TryCompleteAndGrant`** when all required targets are defeated — preserving **NEO-44** per-defeat gig XP. + +**In scope (from Linear + [E5M3-07](E5M3-prototype-backlog.md#e5m3-07--wire-combat-defeat--encounter-progresscompletion)):** + +- **`EncounterCombatWiring.TryProcessCastOutcome`** — on damaging hit (`DamageDealt > 0`): **`EncounterProgressOperations.TryActivateOnFirstEngagement`**; on lethal hit (`TargetDefeated`): **`TryMarkTargetDefeated`**, then when **`IsAllRequiredTargetsDefeated`**: **`EncounterCompletionOperations.TryCompleteAndGrant`** for resolved encounter id. +- Wire helper from **`AbilityCastApi`** after combat resolve (alongside existing aggro + **`CombatDefeatGigXpGrant`** on defeat). +- Inject encounter + inventory dependencies into cast route handler (registries, progress/completion stores, item registry, inventory store, `TimeProvider`). +- **`EncounterCombatWiringTests`** — AAA unit tests for activate-on-damage, mark-on-defeat, completion-on-third-defeat (mocked deps / factory services). +- **`AbilityCastApiTests`** — in-proc integration: defeat all three prototype NPCs via cast → inventory grants once; partial progress (1–2 defeats) → no loot; gig XP **25** per defeat preserved (**75** total after three defeats). +- `server/README.md` — document combat → encounter wiring; update NEO-104/105 deferral notes. + +**Out of scope (from Linear + backlog):** + +- **`GET /game/players/{id}/encounter-progress`** + Bruno ([NEO-108](https://linear.app/neon-sprawl/issue/NEO-108)). +- **`EncounterCompleteEvent` persistence** + E7.M2 hook ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)). +- Godot HUD — **client counterpart [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110)**. +- Cast response schema changes, Postgres, `docs/manual-qa/NEO-106.md`. + +## Acceptance criteria checklist + +- [ ] Defeating third required NPC triggers exactly one completion grant (`scrap_metal_bulk` ×10, `contract_handoff_token` ×1). +- [ ] Partial progress (1–2 defeats) does not grant loot. +- [ ] Gig XP still awarded per defeat via **`CombatDefeatGigXpGrant`** (25 per defeat, unchanged skill progression). + +## Technical approach + +1. **`EncounterCombatWiring`** (`Game/Encounters/`) + - **`TryProcessCastOutcome(playerId, targetNpcInstanceId, damageDealt, targetDefeated, …deps)`** — best-effort (no throw); outcome ignored on cast accept path (mirror gig XP grant). + - **Activation:** when `damageDealt > 0`, call **`TryActivateOnFirstEngagement(playerId, targetNpcInstanceId, …)`**. + - **Defeat mark:** when `targetDefeated`, call **`TryMarkTargetDefeated(…)`**; resolve **`encounterId`** via **`TryResolveEncounterForNpc`**. + - **Completion:** after mark, if **`IsAllRequiredTargetsDefeated(playerId, encounterId, …)`**, call **`TryCompleteAndGrant(playerId, encounterId, …)`** — success/failure does not change cast accept JSON. + - Optional debug log on completion deny (inventory full, etc.) — same pattern as **`CombatDefeatGigXpGrant`**. + +2. **`AbilityCastApi` changes** + - Add DI parameters: **`IEncounterDefinitionRegistry`**, **`IRewardTableDefinitionRegistry`**, **`IEncounterProgressStore`**, **`IEncounterCompletionStore`**, **`IItemDefinitionRegistry`**, **`IPlayerInventoryStore`**, **`TimeProvider`** (reuse route `clock` for completion timestamp). + - After successful combat resolve: + - Existing **`DamageDealt > 0`** aggro block: also call wiring with activation context (or call wiring once per accept with both flags). + - Existing **`TargetDefeated`** block (after **`CombatDefeatGigXpGrant`**): call wiring with defeat context — **gig XP grant order preserved before encounter completion** so NEO-44 AC holds even if completion fails. + - Update XML remarks: NEO-106 encounter wiring. + +3. **Call order on lethal accept (frozen)** + + | Step | Action | + |------|--------| + | 1 | `CombatOperations.TryResolve` success | + | 2 | Aggro (if `DamageDealt > 0`) | + | 3 | Cooldown start | + | 4 | `NpcRuntimeOperations.TryStopOnTargetDefeat` (if defeated) | + | 5 | **`CombatDefeatGigXpGrant.GrantOnCombatDefeat`** (if defeated) | + | 6 | **`EncounterCombatWiring.TryProcessCastOutcome`** — activate (if damage > 0) + mark + maybe complete | + | 7 | Return accept JSON | + +4. **Integration test helpers** (`AbilityCastApiTests`) + - **`LockPrototypeTargetAsync(client, npcId)`** — generalize existing alpha lock helper. + - **`DefeatLockedTargetWithPulseAsync(client, factory, npcId)`** — bind pulse, lock target, 4 casts with cooldown advance (100 HP / 25 pulse damage). + - **`PostAbilityCast_ShouldGrantEncounterLootOnce_WhenAllThreeNpcDefeated`** — assert inventory + completion store after third defeat. + - **`PostAbilityCast_ShouldNotGrantEncounterLoot_WhenOnlyTwoDefeated`** — assert zero scrap/token delta. + - Extend or duplicate gig XP test to assert **75** breach XP after three defeats (3 × 25). + +5. **No new DI registration** — encounter stores/registries already in **`AddEncounterAndRewardCatalogs`**. + +### Prototype cast → encounter flow (frozen) + +| Cast event | Encounter effect | +|------------|------------------| +| First pulse hit (damage > 0) vs `prototype_npc_*` | `TryActivateOnFirstEngagement` → progress **started** | +| Lethal hit vs any required NPC | `TryMarkTargetDefeated` → +1 defeated id | +| Third distinct required NPC defeated | `TryCompleteAndGrant` → +10 scrap, +1 token, completion marked | +| Re-hit defeated NPC | Combat deny `target_defeated`; encounter ops no-op | + +## Files to add + +| Path | Purpose | +|------|---------| +| `server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs` | Static helper: activate on damage, mark on defeat, complete when all defeated. | +| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCombatWiringTests.cs` | AAA unit tests for wiring orchestration (factory-backed deps). | +| `docs/plans/NEO-106-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` | Inject encounter/inventory deps; call `EncounterCombatWiring` on accept. | +| `server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs` | In-proc defeat-all-three integration tests + partial progress + gig XP preservation. | +| `server/README.md` | Document combat → encounter wiring; remove NEO-106 deferral notes from progress/completion sections. | +| `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | Implementation snapshot — E5M3-07 combat wiring (when landed). | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 row — note NEO-106 wiring when landed. | +| `docs/decomposition/modules/module_dependency_register.md` | E5.M3 note — NEO-106 combat wiring when landed. | +| `docs/plans/E5M3-prototype-backlog.md` | Mark E5M3-07 AC when complete. | + +## Tests + +| Test file | What it covers | +|-----------|----------------| +| `EncounterCombatWiringTests.cs` | **Arrange:** `InMemoryWebApplicationFactory` services. **Act/Assert:** damaging hit activates encounter; defeat marks progress; third defeat invokes completion (inventory + `IsCompleted`); non-encounter NPC no-op; already-completed encounter skips activate/mark; completion not called when only 2/3 defeated. | +| `AbilityCastApiTests.cs` | **Arrange:** HTTP client + pulse bind + per-NPC lock/defeat helpers. **Act/Assert:** defeat all three via cast → `scrap_metal_bulk` ×10 + `contract_handoff_token` ×1 once; two defeats → zero loot; gig XP 75 on breach after three defeats, skill XP unchanged; existing NEO-44 single-defeat test still passes. | + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Helper vs inline wiring** | **`EncounterCombatWiring`** — adopted at kickoff. | **adopted** | +| **Bruno defeat spine** | Defer to **NEO-108** — in-proc tests sufficient for NEO-106 AC. | **adopted** | +| **NPC HP variance** | All three prototype NPCs use catalog **100 HP**; pulse **25** → 4 casts each — reuse existing defeat loop pattern. | **adopted** | +| **Inventory-full on third defeat** | Progress stays all-defeated; completion retriable when bag has space — document in README; no cast deny. | **adopted** | +| **Event persistence** | Deferred to **NEO-107** — wiring returns/completes via existing `EncounterCompleteEvent` in result only. | **deferred** |