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

11 KiB
Raw Blame History

NEO-105 — Implementation plan

Story reference

Field Value
Key NEO-105
Title E5M3-06: EncounterCompletionOperations + inventory reward apply
Linear https://linear.app/neon-sprawl/issue/NEO-105/e5m3-06-encountercompletionoperations-inventory-reward-apply
Module E5.M3 — EncounterAndRewardTables · Epic 5 Slice 3 · backlog E5M3-06
Branch NEO-105-encounter-completion-inventory-reward-apply
Precursor NEO-104IEncounterProgressStore + IEncounterCompletionStore + EncounterProgressOperations (landed on main)
Pattern NEO-62 / GatherOperations — static ops + reason codes + grant summary; NEO-69 / CraftOperations — simulate-all pre-flight + compensating rollback on partial apply failure
Blocks NEO-106 — combat defeat → progress/completion wiring; NEO-107EncounterCompleteEvent persistence + E7.M2 hook stub
Client counterpart NEO-110 — encounter progress + loot feedback labels (via NEO-108 HTTP read)

Kickoff clarifications

Topic Question Agent recommendation Answer
EncounterCompleteEvent scope Define event type on NEO-105 vs defer entirely to NEO-107? Define EncounterCompleteEvent + return in EncounterCompletionResult on success — E5M3-06 goal calls for internal completion payload; NEO-107 persists the record and adds E7.M2 hook stub (E5M3-09). Adopted — result payload only; no persistence
Grant transaction Pre-flight vs apply-then-hope? Simulate all fixedGrants on inventory snapshot, then apply sequentially with compensating rollback — mirror CraftOperations; never TryMarkCompleted unless every grant applied. Adopted (repo precedent; Linear AC “inventory deny fails closed”)
Completion gate Who checks all-defeated? TryCompleteAndGrant requires EncounterProgressOperations.IsAllRequiredTargetsDefeated + not already completed — NEO-106 calls after last defeat mark. Adopted (backlog + NEO-104 split)
Combat / HTTP wiring Wire AbilityCastApi or HTTP on this story? Defer — NEO-106 / NEO-108; NEO-105 ships operations + unit tests only. Adopted (backlog out-of-scope)
Bruno Add encounter-completion Bruno folder? Defer to NEO-108 — no player HTTP route yet; backlog marks Bruno optional here. Adopted (backlog)

Goal, scope, and out-of-scope

Goal: EncounterCompletionOperations.TryCompleteAndGrant applies RewardTable.fixedGrants via PlayerInventoryOperations exactly once per player+encounter, marks the completion store only when all grants succeed, and returns an internal EncounterCompleteEvent payload for downstream routers (persistence deferred).

In scope (from Linear + E5M3-06):

  • EncounterCompletionOperations.TryCompleteAndGrant — resolve encounter → reward table → pre-flight simulate → apply fixed grants → TryMarkCompleted with TimeProvider.GetUtcNow().
  • EncounterCompletionResult, EncounterCompletionReasonCodes, EncounterGrantApplied, EncounterCompleteEvent (internal record; not persisted).
  • Structured deny codes: already_completed, not_ready, unknown_encounter, unknown_reward_table, inventory_full, inventory_store_missing, passthrough invalid_item / invalid_quantity.
  • Compensating inventory rollback when a later grant fails after earlier grants applied, or when TryMarkCompleted returns false after grants (race/idempotent replay).
  • Unit tests (AAA) with InMemoryWebApplicationFactory: first completion grants frozen prototype loot once; second completion no duplicate; bag-full deny without completion flag.
  • server/README.md encounter completion section (brief).

Out of scope (from Linear + backlog):

  • Combat defeat wiring (NEO-106).
  • EncounterCompleteEvent store / E7.M2 quest-credit hook (NEO-107).
  • HTTP GET /game/players/{id}/encounter-progress (NEO-108).
  • Godot HUD — client counterpart NEO-110.
  • Postgres persistence, Bruno, docs/manual-qa/NEO-105.md.

Acceptance criteria checklist

  • First completion grants scrap_metal_bulk ×10 and contract_handoff_token ×1 deterministically.
  • Second completion does not duplicate grants.
  • Inventory deny fails closed without marking complete.

Implementation reconciliation (shipped)

  • Operations: EncounterCompletionOperations.TryCompleteAndGrant — pre-flight simulate, sequential grant apply with compensating rollback, idempotent TryMarkCompleted.
  • Types: EncounterCompletionResult, EncounterCompletionReasonCodes, EncounterGrantApplied, EncounterCompleteEvent (result payload only; NEO-107 persists).
  • Tests: EncounterCompletionOperationsTests — grant once, already_completed replay, not_ready, inventory_full fail-closed, apply-phase rollback (InventoryStoreFullAfterFirstGrant), mark-fail rollback.
  • Docs: server/README.md; E5.M3 module snapshot + alignment register + backlog updated.

Technical approach

  1. EncounterCompletionReasonCodes

    • Domain-specific: already_completed, not_ready, unknown_encounter, unknown_reward_table, inventory_store_missing.
    • Passthrough: inventory_full, invalid_item, invalid_quantity from PlayerInventoryReasonCodes.
  2. EncounterGrantApplied — readonly record (ItemId, Quantity) mirroring GatherGrantApplied.

  3. EncounterCompleteEvent — readonly record: PlayerId, EncounterId, RewardTableId, IReadOnlyList<EncounterGrantApplied> GrantedItems, DateTimeOffset CompletedAt, IdempotencyKey (stable string e.g. {playerId}:{encounterId} normalized). Returned on success only; NEO-107 will persist/emit.

  4. EncounterCompletionResult(Success, ReasonCode?, GrantsApplied, EncounterCompleteEvent? CompleteEvent, DateTimeOffset? CompletedAt).

  5. EncounterCompletionOperations.TryCompleteAndGrant

    • Normalize encounterId via IEncounterDefinitionRegistry.TryNormalizeKnown; load EncounterDefRow.
    • Early deny already_completed if IEncounterCompletionStore.IsCompleted.
    • Early deny not_ready if !EncounterProgressOperations.IsAllRequiredTargetsDefeated.
    • Resolve rewardTableIdIRewardTableDefinitionRegistry.TryGetDefinition; deny unknown_reward_table if missing.
    • Load FixedGrants (prototype: scrap ×10, token ×1).
    • Pre-flight: clone inventory snapshot; PlayerInventoryOperations.TrySimulateAddStack for each grant in catalog order.
    • Apply: foreach grant TryAddStack; on deny/store-missing, CompensatingRemoveGrants for prior applied rows and return deny without marking complete.
    • TryMarkCompleted(playerId, encounterId, timeProvider.GetUtcNow()); if false, compensating remove all grants and return already_completed.
    • Success: build EncounterCompleteEvent, return grants + event + CompletedAt.
    • Comment-only hook site: // TODO(E7.M2): quest-credit router consumes EncounterCompleteEvent (NEO-107 expands).
  6. No new DI registration — static operations like GatherOperations; callers inject stores/registries from existing AddEncounterAndRewardCatalogs + inventory DI.

Prototype completion flow (frozen)

Step Expected
Setup Activate + defeat all three NPCs → IsAllRequiredTargetsDefeated true
First grant TryCompleteAndGrant → success; bag +10 scrap, +1 token; completion store marked
Replay Second TryCompleteAndGrant → deny already_completed; inventory unchanged
Bag full Fill bag slots → TryCompleteAndGrant → deny inventory_full; IsCompleted false

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Encounters/EncounterCompletionReasonCodes.cs Stable deny reason constants.
server/NeonSprawl.Server/Game/Encounters/EncounterGrantApplied.cs Successful grant row in result/event.
server/NeonSprawl.Server/Game/Encounters/EncounterCompleteEvent.cs Internal completion payload (returned, not persisted on NEO-105).
server/NeonSprawl.Server/Game/Encounters/EncounterCompletionResult.cs Server-internal completion resolution envelope.
server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs TryCompleteAndGrant orchestration.
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCompletionOperationsTests.cs AAA unit tests for grant-once, idempotent deny, inventory-full fail-closed.
docs/plans/NEO-105-implementation-plan.md This plan.

Files to modify

Path Rationale
server/README.md Document EncounterCompletionOperations, reason codes, and deferrals to NEO-106/107/108.
docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md Implementation snapshot — completion + inventory grant ops (NEO-105).
docs/decomposition/modules/documentation_and_implementation_alignment.md E5.M3 row — note NEO-105 completion grants when landed.
docs/decomposition/modules/module_dependency_register.md E5.M3 note — NEO-105 completion ops landed (when shipped).
docs/plans/E5M3-prototype-backlog.md Mark E5M3-06 AC when complete.

Tests

Test file What it covers
EncounterCompletionOperationsTests.cs Arrange: InMemoryWebApplicationFactory + activate/defeat-all helper for prototype_combat_pocket. Act/Assert: first TryCompleteAndGrant success with scrap ×10 + token ×1 in inventory and CompleteEvent populated; second call already_completed with unchanged quantities; not_ready when only 2/3 defeated; bag-full deny inventory_full with IsCompleted false and zero grant delta; apply-phase rollback via InventoryStoreFullAfterFirstGrant when second grant fails after first succeeds; mark-fail rollback when TryMarkCompleted returns false.

Open questions / risks

Question / risk Agent recommendation Status
Grant order Apply in FixedGrants catalog order (scrap then token); rollback in reverse on failure. adopted
Mark-complete race If TryMarkCompleted false after grants, remove grants and return already_completed — treat as lost race to another completer. adopted
Event persistence Deferred to NEO-107 — NEO-105 only returns payload in result. adopted
Postgres follow-on Defer — completion store Postgres migration is a future session-persistence issue. deferred