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

12 KiB
Raw Blame History

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 · Epic 5 Slice 3 · backlog E5M3-07
Branch NEO-106-wire-combat-defeat-encounter-progress-completion
Precursor NEO-105EncounterCompletionOperations.TryCompleteAndGrant + inventory grants (landed on main)
Pattern NEO-44 / CombatDefeatGigXpGrant — static wiring helper called from AbilityCastApi; NEO-104 / NEO-105 — encounter ops already exist
Blocks NEO-108GET /game/players/{id}/encounter-progress + Bruno; NEO-109 — telemetry hook sites (activates encounter_start comment from NEO-104)
Client counterpart 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):

  • 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 (12 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).
  • EncounterCompleteEvent persistence + E7.M2 hook (NEO-107).
  • Godot HUD — client counterpart 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 (12 defeats) does not grant loot.
  • 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 (6); 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.
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