# 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 - [x] Defeating third required NPC triggers exactly one completion grant (`scrap_metal_bulk` ×10, `contract_handoff_token` ×1). - [x] Partial progress (1–2 defeats) does not grant loot. - [x] Gig XP still awarded per defeat via **`CombatDefeatGigXpGrant`** (25 per defeat, unchanged skill progression). ## Implementation reconciliation (shipped) - **Wiring:** `EncounterCombatWiring.TryProcessCastOutcome` — activate on `damageDealt > 0`, mark on `targetDefeated`, `TryCompleteAndGrant` when all required NPCs defeated. - **Integration:** `AbilityCastApi` injects encounter/inventory deps; call order preserves NEO-44 gig XP before encounter completion. - **Tests:** `EncounterCombatWiringTests` (8); `AbilityCastApiTests` — defeat-all-three loot once, two-defeat no loot, 75 gig XP after three defeats. - **Docs:** `server/README.md`; E5.M3 module snapshot + alignment register + backlog updated. ## 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 (non-lethal third hit); bag-full third defeat retains all-defeated progress without completion mark. | | `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** |