Merge pull request #167 from ViPro-Technologies/NEO-128-e7m2-quest-complete-reward-wiring
NEO-128: Wire quest complete to reward router (deliver-then-mark)pull/168/head
commit
c1849740b2
|
|
@ -0,0 +1,28 @@
|
|||
meta {
|
||||
name: GET health (quest complete reward wiring NEO-128)
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/health
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
docs {
|
||||
NEO-128 wires RewardRouterOperations into QuestStateOperations.TryMarkComplete (deliver-then-mark).
|
||||
No new HTTP routes — quest-progress GET/accept POST signatures gained reward-router DI only.
|
||||
Use this request to confirm the host started after wiring; verify bundle delivery via dotnet test
|
||||
(QuestStateOperationsTests, QuestObjectiveWiringTests).
|
||||
}
|
||||
|
||||
tests {
|
||||
test("status 200", function () {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
|
||||
test("service identity", function () {
|
||||
expect(res.getBody().service).to.equal("NeonSprawl.Server");
|
||||
});
|
||||
}
|
||||
|
|
@ -71,3 +71,5 @@ Epic 7 **Slice 2** — `reward_delivery`, `unlock_granted`; no double-claim on r
|
|||
**NEO-126 (E7M2-03 delivery store):** Quest completion reward delivery uses **`IRewardDeliveryStore`** + **`InMemoryRewardDeliveryStore`** in `server/NeonSprawl.Server/Game/Rewards/` — idempotent **`RewardDeliveryEvent`** per player+quest (idempotency key **`{playerId}:quest_complete:{questId}`**; grant snapshots for item + skill XP rows). Quest-state wiring lands in NEO-128; HTTP **`completionRewardSummary`** in NEO-129. See [NEO-126 implementation plan](../../plans/NEO-126-implementation-plan.md); [server README — Reward delivery store (NEO-126)](../../../server/README.md#reward-delivery-store-neo-126).
|
||||
|
||||
**NEO-127 (E7M2-04 router apply):** **`RewardRouterOperations.TryDeliverQuestCompletion`** in `server/NeonSprawl.Server/Game/Rewards/` applies **`QuestRewardBundleRow`** item grants via **`PlayerInventoryOperations`** and skill XP via **`SkillProgressionGrantOperations.TryApplyGrant`** (`mission_reward` only), with compensating item rollback on partial failure, then **`IRewardDeliveryStore.TryRecord`**. See [NEO-127 implementation plan](../../plans/NEO-127-implementation-plan.md); [server README — Reward router (NEO-127)](../../../server/README.md#reward-router-neo-127).
|
||||
|
||||
**NEO-128 (E7M2-05 quest-state wiring):** **`QuestStateOperations.TryMarkComplete`** calls **`RewardRouterOperations.TryDeliverQuestCompletion`** on the first-time completion path (deliver-then-mark); idempotent **`completed`** replay skips the router. **`QuestObjectiveWiring`** threads reward dependencies through gather/craft/encounter/GET refresh into the same path. HTTP **`completionRewardSummary`** in NEO-129. See [NEO-128 implementation plan](../../plans/NEO-128-implementation-plan.md); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117).
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -194,8 +194,8 @@ Combat intro bundle is **skill XP only** — encounter loot already granted **`c
|
|||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Completing a quest via objective wiring delivers bundle once.
|
||||
- [ ] Idempotent complete does not double-grant items or XP.
|
||||
- [x] Completing a quest via objective wiring delivers bundle once.
|
||||
- [x] Idempotent complete does not double-grant items or XP.
|
||||
|
||||
**Client counterpart:** [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) — HUD surfacing (blocked by E7M2-06).
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
# NEO-128 — Implementation plan
|
||||
|
||||
## Story reference
|
||||
|
||||
| Field | Value |
|
||||
|--------|--------|
|
||||
| **Key** | NEO-128 |
|
||||
| **Title** | E7M2-05: Wire router into `QuestStateOperations.TryMarkComplete` |
|
||||
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-128/e7m2-05-wire-router-into-queststateoperationstrymarkcomplete |
|
||||
| **Module** | [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) · Epic 7 Slice 2 · backlog **E7M2-05** |
|
||||
| **Branch** | `NEO-128-e7m2-quest-complete-reward-wiring` |
|
||||
| **Precursor** | [NEO-127](https://linear.app/neon-sprawl/issue/NEO-127) **`Done`** — `RewardRouterOperations.TryDeliverQuestCompletion` |
|
||||
| **Pattern** | [NEO-105](NEO-105-implementation-plan.md) — deliver side effects before completion mark; [NEO-118](NEO-118-implementation-plan.md) — `QuestObjectiveWiring` → `TryMarkComplete` on terminal step |
|
||||
| **Blocks** | [NEO-129](https://linear.app/neon-sprawl/issue/NEO-129) (`completionRewardSummary` HTTP), [NEO-130](https://linear.app/neon-sprawl/issue/NEO-130) (telemetry hook comments) |
|
||||
| **Client counterpart** | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) — HUD surfacing (blocked by E7M2-06 / NEO-129) |
|
||||
|
||||
## Kickoff clarifications
|
||||
|
||||
| Topic | Question | Agent recommendation | Answer |
|
||||
|--------|----------|----------------------|--------|
|
||||
| **Delivery vs mark ordering** | When should bundle apply run relative to `TryMarkComplete`? | **Deliver bundle first, then mark complete** — E7M2 “transactional equivalent” to fail-closed; mirrors NEO-105 (grants before completion mark); avoids new `TryRevertCompletion` store API. | **Adopted** — deliver-then-mark |
|
||||
| **Bruno smoke** | Add Bruno requests for quest-complete reward delivery? | **Skip** — server-internal wiring only; no HTTP DTO changes (NEO-129); automated integration tests cover AC; Bruno optional per backlog because verification is store/inventory/skill assertions, not a new route. | **Adopted** — skip Bruno (functional smoke); **`GET /health`** Bruno added only for pre-commit hook when `*Api.cs` DI signatures changed — bundle asserts remain in `dotnet test`. |
|
||||
|
||||
**Additional defaults (no kickoff question — settled by backlog / landed code):**
|
||||
|
||||
- **Idempotent quest replay** — when progress is already **`completed`**, return success immediately and **do not** call the router (NEO-121 `quest_complete` hook path unchanged; no double-grant).
|
||||
- **Deny reason passthrough** — on delivery deny, return **`QuestStateOperationResult`** with **`Success: false`** and **`ReasonCode`** from **`RewardDeliveryResult.ReasonCode`** (`inventory_full`, etc.); quest row stays **`active`**.
|
||||
- **`QuestStateOperationResult` shape** — unchanged; HTTP projection of grants is NEO-129.
|
||||
- **Bundle source** — `definition.CompletionRewardBundle` from **`IQuestDefinitionRegistry`**; all four prototype quests have bundles (NEO-124/125 CI gates).
|
||||
- **Wiring best-effort** — `QuestObjectiveWiring` continues to ignore `TryMarkComplete` deny envelopes (primary gather/craft/encounter ops unaffected); on inventory-full deny the quest remains active for retry.
|
||||
|
||||
## Goal, scope, and out-of-scope
|
||||
|
||||
**Goal:** First-time quest completion triggers **`completionRewardBundle`** delivery exactly once via **`RewardRouterOperations`**; idempotent complete replay does not double-grant items or XP.
|
||||
|
||||
**In scope (from Linear + [E7M2-05](E7M2-prototype-backlog.md#e7m2-05--wire-router-into-queststateoperationstrymarkcomplete)):**
|
||||
|
||||
- Call **`RewardRouterOperations.TryDeliverQuestCompletion`** from **`QuestStateOperations.TryMarkComplete`** on the first-time completion path (before store mark per adopted ordering).
|
||||
- Skip router on idempotent **`completed`** early return.
|
||||
- Thread router dependencies through **`QuestObjectiveWiring`** and upstream ops (`GatherOperations`, `CraftOperations`, `EncounterCompletionOperations`, `QuestProgressApi`) so objective-wiring completion exercises the same path.
|
||||
- Integration tests (AAA): gather-intro wiring → salvage +25 + store record; idempotent `TryMarkComplete` replay → no duplicate XP; inventory-full deny → quest stays active, no store record; operator-chain → item + skill grants.
|
||||
- `server/README.md` quest-state section update; E7.M2 module alignment anchor.
|
||||
|
||||
**Out of scope (from Linear):**
|
||||
|
||||
- HTTP **`completionRewardSummary`** ([NEO-129](https://linear.app/neon-sprawl/issue/NEO-129) E7M2-06).
|
||||
- E9.M1 telemetry ingest ([NEO-130](https://linear.app/neon-sprawl/issue/NEO-130) E7M2-07) — existing comment-only hook sites stay.
|
||||
- Encounter completion path; per-step bundles.
|
||||
- Bruno smoke (deferred per kickoff).
|
||||
- Godot client.
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [x] Completing a quest via objective wiring delivers bundle once.
|
||||
- [x] Idempotent complete replay does not double-grant items or XP.
|
||||
|
||||
## Implementation reconciliation (shipped)
|
||||
|
||||
- **Operations:** `QuestStateOperations.TryMarkComplete` — deliver-then-mark via `RewardRouterOperations`; idempotent `completed` skip; deny passthrough from `RewardDeliveryReasonCodes`.
|
||||
- **Wiring:** `QuestObjectiveWiring` + `GatherOperations` / `CraftOperations` / `EncounterCompletionOperations` / quest HTTP APIs thread reward-router dependencies.
|
||||
- **Tests:** `QuestStateOperationsTests` — gather + operator-chain bundle delivery, idempotent replay, inventory-full deny; `QuestObjectiveWiringTests` — gather-intro wiring + replay no double-grant; operator-chain wiring happy path (`survey_drone_kit` + salvage +50); wiring inventory-full stay-active on terminal advance.
|
||||
- **Docs:** `server/README.md`; E7.M2 module anchor + alignment register updated.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### 1. Extend `TryMarkComplete` (deliver-then-mark)
|
||||
|
||||
Add router dependencies to the static signature (mirror **`RewardRouterOperations.TryDeliverQuestCompletion`** injectables):
|
||||
|
||||
```csharp
|
||||
public static QuestStateOperationResult TryMarkComplete(
|
||||
string playerId,
|
||||
string questId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
```
|
||||
|
||||
**First-time completion flow** (after existing registry / active guards):
|
||||
|
||||
1. Load **`definition.CompletionRewardBundle`** from registry (already resolved in guard).
|
||||
2. **`RewardRouterOperations.TryDeliverQuestCompletion(...)`** — includes store idempotent replay (`already_delivered` success when store hit).
|
||||
3. If **`!delivery.Success`**, return **`Deny(delivery.ReasonCode!)`** — quest stays **`active`**.
|
||||
4. **`progressStore.TryMarkComplete(...)`** — on success, emit existing NEO-121 **`quest_complete`** comment hook; return **`Success(snapshot)`**.
|
||||
5. Preserve existing idempotent / race handling when **`TryMarkComplete`** returns false but progress is already **`completed`** (re-read paths unchanged).
|
||||
|
||||
**Idempotent replay** (progress already **`completed`**): existing early return — **no router call**.
|
||||
|
||||
### 2. Dependency threading
|
||||
|
||||
| Layer | Change |
|
||||
|-------|--------|
|
||||
| **`QuestObjectiveWiring`** | Add reward-router parameters to **`TryCompleteStepIfSatisfied`**, **`TryCompleteAllActiveQuests`**, and all public entry points that call them (`TryProcessGatherSuccess`, `TryProcessCraftSuccess`, `TryProcessEncounterCompleteSuccess`, `TryRefreshInventoryProgressForPlayer`). |
|
||||
| **`QuestStateOperations`** | Pass new deps into **`TryMarkComplete`** from **`TryAccept`** / **`TryAdvanceStep`** wiring calls. |
|
||||
| **`GatherOperations`**, **`CraftOperations`**, **`EncounterCompletionOperations`** | Extend method signatures + call sites to pass skill/progress/perk/delivery deps into wiring (resolve from existing HTTP/ops injectables same as gather/craft already thread quest stores). |
|
||||
| **`QuestProgressApi`** | Pass reward deps into **`TryRefreshInventoryProgressForPlayer`**. |
|
||||
|
||||
Use **`InMemoryWebApplicationFactory`** DI resolution in tests — no new service registration (stores already in **`Program.cs`**).
|
||||
|
||||
### 3. Tests
|
||||
|
||||
| Test file | New / extended cases |
|
||||
|-----------|---------------------|
|
||||
| **`QuestStateOperationsTests`** | **`TryMarkComplete_ShouldDeliverGatherIntroBundle_WhenFirstCompletion`** — accept + complete → **`IRewardDeliveryStore.TryGet`** true, salvage XP +25. **`TryMarkComplete_ShouldNotDoubleGrant_WhenIdempotentReplay`** — second complete → same store event, XP unchanged. **`TryMarkComplete_ShouldDenyAndStayActive_WhenInventoryFull`** — fill bag, complete operator-chain quest setup → deny **`inventory_full`**, status **`active`**, no store record. |
|
||||
| **`QuestObjectiveWiringTests`** | Extend **`GatherIntro_ShouldComplete_WhenScrapGatheredThreeTimes`** (or sibling) — assert salvage XP +25 and delivery store after wiring-driven complete. **`GatherIntro_ShouldNotDoubleGrantXp_WhenCompleteReplayed`** — manual second **`TryMarkComplete`** after wiring complete. |
|
||||
|
||||
Reuse **`RewardRouterTestDependencies`**-style resolution or extend existing **`QuestTestDependencies`** / **`QuestWiringTestDependencies`** records with skill + delivery services.
|
||||
|
||||
### 4. Docs
|
||||
|
||||
- **`server/README.md`** — update **`QuestStateOperations.TryMarkComplete`** section: deliver-then-mark order, idempotent skip, deny passthrough, pointer to **`RewardRouterOperations`**.
|
||||
- **`documentation_and_implementation_alignment.md`** — E7.M2 row: quest-state wiring (NEO-128).
|
||||
- **`E7_M2_RewardAndUnlockRouter.md`** — implementation anchor for **`TryMarkComplete`** wiring.
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `docs/plans/NEO-128-implementation-plan.md` | This plan. |
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs` | Wire **`RewardRouterOperations`** into **`TryMarkComplete`**; extend signature; thread deps from accept/advance. |
|
||||
| `server/NeonSprawl.Server/Game/Quests/QuestObjectiveWiring.cs` | Pass reward-router deps through completion wiring chain. |
|
||||
| `server/NeonSprawl.Server/Game/Gathering/GatherOperations.cs` | Thread reward deps into **`TryProcessGatherSuccess`**. |
|
||||
| `server/NeonSprawl.Server/Game/Crafting/CraftOperations.cs` | Thread reward deps into **`TryProcessCraftSuccess`**. |
|
||||
| `server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs` | Thread reward deps into **`TryProcessEncounterCompleteSuccess`**. |
|
||||
| `server/NeonSprawl.Server/Game/Quests/QuestProgressApi.cs` | Thread reward deps into inventory refresh wiring. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestStateOperationsTests.cs` | Bundle delivery, idempotent replay, inventory-full deny tests. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestObjectiveWiringTests.cs` | Wiring path asserts store + skill XP after gather complete. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestAcceptApiTests.cs` | Update **`TryMarkComplete`** call sites with new parameters. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestProgressApiTests.cs` | Update **`TryMarkComplete`** / wiring dependency resolution. |
|
||||
| `server/README.md` | Document quest-complete reward wiring on **`TryMarkComplete`**. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M2 alignment — quest-state wiring (NEO-128). |
|
||||
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | NEO-128 implementation anchor. |
|
||||
|
||||
## Tests
|
||||
|
||||
| Target | Coverage |
|
||||
|--------|----------|
|
||||
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestStateOperationsTests.cs` | Direct **`TryMarkComplete`**: first completion delivers bundle + records store; idempotent replay no double XP; inventory-full deny leaves quest active without store record. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestObjectiveWiringTests.cs` | Gather-intro wiring path delivers salvage +25 once; replay does not double-grant. |
|
||||
| **Compile-fix only** — `QuestAcceptApiTests`, `QuestProgressApiTests` | Updated **`TryMarkComplete`** signatures; existing accept/progress behavior unchanged. |
|
||||
| **Manual verification** | `dotnet test server/NeonSprawl.Server.Tests --filter "QuestStateOperationsTests|QuestObjectiveWiringTests"`; dev server boot unchanged. |
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
| Question or risk | Agent recommendation | Status |
|
||||
|------------------|----------------------|--------|
|
||||
| Delivery succeeds but **`TryMarkComplete`** store write fails while quest still active | Extremely unlikely solo prototype; handle via existing re-read idempotent paths when another caller completed; defer explicit delivery rollback on mark-fail unless concurrency story requires it. | **deferred** |
|
||||
| Orphan store record if mark never commits | **`TryDeliver`** idempotent replay on retry allows **`already_delivered`** success then mark on next complete attempt — self-healing. | **adopted** |
|
||||
| Large signature expansion across wiring call graph | Accept explicit parameter threading (NEO-118 precedent); no new DI facade type in this story. | **adopted** |
|
||||
| **`QuestStateOperationResult` grant projection** | Defer to NEO-129 HTTP read model. | **deferred** |
|
||||
|
||||
None beyond the above.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# Code review — NEO-128 (E7M2-05)
|
||||
|
||||
**Date:** 2026-06-07
|
||||
**Scope:** Branch `NEO-128-e7m2-quest-complete-reward-wiring` vs `origin/main` — commits `7abe7dc` … `da99769`
|
||||
**Base:** `origin/main`
|
||||
|
||||
## Verdict
|
||||
|
||||
**Approve with nits** — deliver-then-mark wiring in `QuestStateOperations.TryMarkComplete`, idempotent skip, deny passthrough, and dependency threading through objective wiring and upstream ops match the adopted plan and NEO-105 precedent; tests cover the primary AC paths; risk is low (server-internal until NEO-129 HTTP projection).
|
||||
|
||||
## Summary
|
||||
|
||||
NEO-128 wires **`RewardRouterOperations.TryDeliverQuestCompletion`** into **`QuestStateOperations.TryMarkComplete`** on the first-time active→completed path: bundle delivery runs before **`IPlayerQuestStateStore.TryMarkComplete`**, idempotent **`completed`** replay returns early without calling the router, and delivery denies passthrough router **`reasonCode`** values while leaving the quest **active**. Reward-router dependencies are threaded through **`QuestObjectiveWiring`**, **`GatherOperations`**, **`CraftOperations`**, **`EncounterCompletionOperations`**, **`QuestProgressApi`**, **`QuestAcceptApi`**, **`AbilityCastApi`**, and related call sites so wiring-driven completion exercises the same path. Five new/extended AAA integration tests lock gather-intro bundle delivery, idempotent replay (direct and wiring), and inventory-full deny on operator-chain setup; compile-fix updates propagate the expanded signatures across quest/gather/craft/encounter test harnesses. Docs (plan reconciliation, E7.M2 anchor, alignment register, backlog AC, `server/README.md`) are updated. A minimal Bruno health smoke was added (host boot check only).
|
||||
|
||||
## Documentation checked
|
||||
|
||||
| Path | Result |
|
||||
|------|--------|
|
||||
| `docs/plans/NEO-128-implementation-plan.md` | **Matches** — kickoff deliver-then-mark and idempotent skip adopted; reconciliation accurate; AC checked. |
|
||||
| `docs/plans/E7M2-prototype-backlog.md` (E7M2-05) | **Matches** — AC checkboxes complete; Bruno listed as optional in backlog (health-only smoke landed). |
|
||||
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | **Matches** — NEO-128 implementation anchor added. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7.M2 row notes E7M2-05 quest-state wiring; row correctly stays **Planned** until NEO-132 capstone. |
|
||||
| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — no register change required. |
|
||||
| `server/README.md` | **Matches** — `TryMarkComplete` table row and reward-router cross-ref document deliver-then-mark, idempotent skip, deny passthrough. |
|
||||
| Full-stack epic decomposition | **N/A** — E7M2-05 is server engine; client counterpart NEO-131 correctly deferred (blocked by NEO-129). |
|
||||
|
||||
**Register / tracking:** E7.M2 correctly remains **Planned** until capstone NEO-132; alignment update is appropriate.
|
||||
|
||||
## Blocking issues
|
||||
|
||||
None.
|
||||
|
||||
## Suggestions
|
||||
|
||||
1. ~~**Operator-chain happy-path delivery assertions** — The plan lists integration coverage for “operator-chain → item + skill grants.” `TryMarkComplete_ShouldDenyAndStayActive_WhenInventoryFull` exercises chain quest at the operations layer (deny only). `ChainQuest_ShouldAdvanceThroughMixedObjectives_WhenPrerequisitesMet` completes via wiring but does not assert **`IRewardDeliveryStore`**, **`survey_drone_kit`**, or salvage +50. NEO-127 covers router apply in isolation; extending the wiring or operations happy-path test would lock end-to-end **`TryMarkComplete`** + item grant for the only bundle with **`itemGrants`**.~~ **Done.** Added `TryMarkComplete_ShouldDeliverOperatorChainBundle_WhenFirstCompletion`; extended `ChainQuest_ShouldAdvanceThroughMixedObjectives_WhenPrerequisitesMet` with store, item, and skill XP asserts.
|
||||
|
||||
2. ~~**Inventory-full via wiring path** — Deny is tested via direct **`TryMarkComplete`** after manual chain setup. Optional: one wiring-driven test (e.g. fill bag then complete terminal **`inventory_has_item`** step) to prove **`QuestObjectiveWiring`** best-effort ignore does not mask the deny for primary ops while quest stays **active** for retry.~~ **Done.** Added `ChainQuest_ShouldStayActive_WhenBagFullOnTerminalWiringComplete` (terminal advance via `TryAdvanceStep` while bag full; quest stays **active**, no store record).
|
||||
|
||||
## Nits
|
||||
|
||||
- ~~Nit: Kickoff adopted “skip Bruno” for bundle verification, but a **`GET /health`** smoke was added under `bruno/neon-sprawl-server/quest-progress/` with docs pointing to dotnet test for bundle asserts. Harmless and consistent with backlog “optional” wording; consider a one-line plan note if authors want kickoff vs backlog Bruno policy explicit.~~ **Done.** Plan kickoff clarifications note health-only Bruno for pre-commit hook; functional verification via `dotnet test`.
|
||||
|
||||
- Nit: **`TryCompleteStepIfSatisfied`** discards **`TryMarkComplete`** return value (by design per plan — wiring best-effort). Inventory-full deny therefore does not surface on gather/craft/encounter envelopes; acceptable for prototype, worth remembering for NEO-131 client surfacing.
|
||||
|
||||
- Nit: Delivery succeeds then **`TryMarkComplete`** store write fails — orphan store record self-heals via **`already_delivered`** on retry (plan deferred). No action required for prototype.
|
||||
|
||||
- Nit: Expanded parameter lists on **`TryAccept`** / **`TryAdvanceStep`** mirror NEO-118 threading precedent; a future facade/DI record could reduce call-site churn but is explicitly out of scope.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
dotnet test server/NeonSprawl.Server.Tests --filter "QuestStateOperationsTests|QuestObjectiveWiringTests"
|
||||
dotnet test server/NeonSprawl.Server.Tests --filter "FullyQualifiedName~Quests|FullyQualifiedName~GatherOperations|FullyQualifiedName~CraftOperations|FullyQualifiedName~EncounterCompletion|FullyQualifiedName~EncounterCombatWiring"
|
||||
```
|
||||
|
||||
All 32 quest-state/wiring tests and 141 related tests pass (verified 2026-06-07; +2 after review follow-up).
|
||||
|
||||
Optional before merge:
|
||||
|
||||
- Boot dev server; run Bruno `GET health (quest complete reward wiring NEO-128)` or `curl /health` — no new DI registration beyond existing reward stores.
|
||||
|
|
@ -3,6 +3,7 @@ using NeonSprawl.Server.Game.Crafting;
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
|
@ -35,6 +36,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -79,6 +82,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -114,6 +119,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -151,6 +158,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -185,6 +194,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -220,6 +231,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -252,6 +265,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -280,6 +295,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -311,6 +328,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -344,6 +363,8 @@ public sealed class CraftOperationsTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -392,7 +413,9 @@ public sealed class CraftOperationsTests
|
|||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>());
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -429,7 +452,9 @@ public sealed class CraftOperationsTests
|
|||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore ProgressStore);
|
||||
IPlayerQuestStateStore ProgressStore,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore);
|
||||
|
||||
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Encounters;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -37,6 +40,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -68,6 +77,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -103,6 +118,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -136,6 +157,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -162,6 +189,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var before);
|
||||
|
||||
|
|
@ -180,6 +213,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -212,6 +251,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -253,6 +298,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -291,6 +342,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -313,6 +370,12 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
|
|
@ -363,7 +426,13 @@ public sealed class EncounterCombatWiringTests
|
|||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<ISkillDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerSkillProgressionStore>(),
|
||||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>());
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -389,5 +458,11 @@ public sealed class EncounterCombatWiringTests
|
|||
IItemDefinitionRegistry ItemRegistry,
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore QuestProgressStore);
|
||||
IPlayerQuestStateStore QuestProgressStore,
|
||||
ISkillDefinitionRegistry SkillRegistry,
|
||||
IPlayerSkillProgressionStore SkillProgressionStore,
|
||||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Encounters;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -37,6 +40,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
timeProvider);
|
||||
|
||||
// Assert
|
||||
|
|
@ -80,6 +89,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
timeProvider);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterFirst);
|
||||
|
||||
|
|
@ -96,6 +111,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
timeProvider);
|
||||
|
||||
// Assert
|
||||
|
|
@ -154,6 +175,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -188,6 +215,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -224,6 +257,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -258,6 +297,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
inventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -290,6 +335,12 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -352,7 +403,13 @@ public sealed class EncounterCompletionOperationsTests
|
|||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<ISkillDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerSkillProgressionStore>(),
|
||||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>());
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -392,7 +449,13 @@ public sealed class EncounterCompletionOperationsTests
|
|||
IItemDefinitionRegistry ItemRegistry,
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore QuestProgressStore);
|
||||
IPlayerQuestStateStore QuestProgressStore,
|
||||
ISkillDefinitionRegistry SkillRegistry,
|
||||
IPlayerSkillProgressionStore SkillProgressionStore,
|
||||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore);
|
||||
|
||||
private sealed class CompletionStoreDeniesMark(IEncounterCompletionStore inner) : IEncounterCompletionStore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using NeonSprawl.Server.Game.Gathering;
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
|
@ -36,6 +37,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -90,6 +93,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
|
|
@ -126,6 +131,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
Assert.True(gather.Success);
|
||||
}
|
||||
|
|
@ -147,6 +154,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
|
|
@ -182,6 +191,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
var rowExists = deps.InstanceStore.TryGetRemainingGathers(unknownId, out _);
|
||||
|
|
@ -215,6 +226,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
|
|
@ -249,6 +262,8 @@ public sealed class GatherOperationsTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -273,7 +288,9 @@ public sealed class GatherOperationsTests
|
|||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IResourceNodeInstanceStore>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>());
|
||||
}
|
||||
|
||||
private static int CountItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -311,7 +328,9 @@ public sealed class GatherOperationsTests
|
|||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IResourceNodeInstanceStore InstanceStore,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore ProgressStore);
|
||||
IPlayerQuestStateStore ProgressStore,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore);
|
||||
|
||||
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ using System.Text;
|
|||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -202,22 +205,45 @@ public sealed class QuestAcceptApiTests
|
|||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
var questRegistry = factory.Services.GetRequiredService<IQuestDefinitionRegistry>();
|
||||
var progressStore = factory.Services.GetRequiredService<IPlayerQuestStateStore>();
|
||||
var services = factory.Services;
|
||||
var questRegistry = services.GetRequiredService<IQuestDefinitionRegistry>();
|
||||
var progressStore = services.GetRequiredService<IPlayerQuestStateStore>();
|
||||
var inventoryStore = services.GetRequiredService<IPlayerInventoryStore>();
|
||||
var itemRegistry = services.GetRequiredService<IItemDefinitionRegistry>();
|
||||
var skillRegistry = services.GetRequiredService<ISkillDefinitionRegistry>();
|
||||
var skillProgressionStore = services.GetRequiredService<IPlayerSkillProgressionStore>();
|
||||
var levelCurve = services.GetRequiredService<ISkillLevelCurve>();
|
||||
var perkUnlockEngine = services.GetRequiredService<PerkUnlockEngine>();
|
||||
var perkStore = services.GetRequiredService<IPlayerPerkStateStore>();
|
||||
var deliveryStore = services.GetRequiredService<IRewardDeliveryStore>();
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
factory.Services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
factory.Services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
itemRegistry,
|
||||
inventoryStore,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider).Success);
|
||||
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using NeonSprawl.Server.Game.Gathering;
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
|
@ -41,6 +42,37 @@ public sealed class QuestObjectiveWiringTests
|
|||
// Assert
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, GatherQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
|
||||
Assert.Equal(
|
||||
3 * GatherSkillXpConstants.ActivityXpPerResourceNodeGather + 25,
|
||||
deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var deliveryEvent));
|
||||
Assert.Single(deliveryEvent.GrantedSkillXp);
|
||||
Assert.Equal(25, deliveryEvent.GrantedSkillXp[0].Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GatherIntro_ShouldNotDoubleGrantXp_WhenCompleteReplayed()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
Assert.True(TryGather(deps, AlphaNodeId).Success);
|
||||
}
|
||||
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var firstEvent));
|
||||
var salvageXpBeforeReplay = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");
|
||||
|
||||
// Act
|
||||
var replay = TryMarkComplete(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(replay.Success);
|
||||
Assert.Equal(salvageXpBeforeReplay, deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var replayEvent));
|
||||
Assert.Equal(firstEvent.IdempotencyKey, replayEvent.IdempotencyKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -78,6 +110,12 @@ public sealed class QuestObjectiveWiringTests
|
|||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
// Assert
|
||||
|
|
@ -110,12 +148,56 @@ public sealed class QuestObjectiveWiringTests
|
|||
// Act — step 2: field stim craft
|
||||
SeedStack(deps, "refined_plate_stock", 2);
|
||||
SeedStack(deps, "scrap_metal_bulk", 1);
|
||||
var salvageXpBeforeComplete = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
|
||||
var kitBefore = CountBagItem(beforeInventory!, "survey_drone_kit");
|
||||
Assert.True(TryCraft(deps, "make_field_stim_mk0").Success);
|
||||
|
||||
// Assert — terminal inventory_has_item (contract_handoff_token from combat intro)
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var completed));
|
||||
Assert.Equal(QuestProgressStatus.Completed, completed.Status);
|
||||
Assert.Equal(3, completed.CurrentStepIndex);
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, ChainQuestId, out var deliveryEvent));
|
||||
Assert.Single(deliveryEvent.GrantedItems);
|
||||
Assert.Equal("survey_drone_kit", deliveryEvent.GrantedItems[0].ItemId);
|
||||
Assert.Single(deliveryEvent.GrantedSkillXp);
|
||||
Assert.Equal(50, deliveryEvent.GrantedSkillXp[0].Amount);
|
||||
Assert.Equal(salvageXpBeforeComplete + 50, deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
Assert.Equal(kitBefore + 1, CountBagItem(afterInventory!, "survey_drone_kit"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChainQuest_ShouldStayActive_WhenBagFullOnTerminalWiringComplete()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
AcceptAndMarkComplete(deps, GatherQuestId);
|
||||
AcceptAndMarkComplete(deps, RefineQuestId);
|
||||
AcceptAndMarkComplete(deps, CombatQuestId);
|
||||
SeedStack(deps, PrototypeE7M1QuestCatalogRules.ChainTerminalItemId, 1);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainGatherObjectiveId, 5, out _));
|
||||
Assert.True(TryAdvanceStep(deps, ChainQuestId, 1).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_refine", 1, out _));
|
||||
Assert.True(TryAdvanceStep(deps, ChainQuestId, 2).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_stim", 1, out _));
|
||||
FillRemainingBagSlots(deps);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
|
||||
var kitBefore = CountBagItem(beforeInventory!, "survey_drone_kit");
|
||||
|
||||
// Act
|
||||
var advance = TryAdvanceStep(deps, ChainQuestId, 3);
|
||||
|
||||
// Assert
|
||||
Assert.True(advance.Success);
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Active, progress.Status);
|
||||
Assert.Equal(3, progress.CurrentStepIndex);
|
||||
Assert.False(deps.DeliveryStore.TryGet(PlayerId, ChainQuestId, out _));
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
Assert.Equal(kitBefore, CountBagItem(afterInventory!, "survey_drone_kit"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -147,37 +229,13 @@ public sealed class QuestObjectiveWiringTests
|
|||
SeedStack(deps, PrototypeE7M1QuestCatalogRules.ChainTerminalItemId, 1);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainGatherObjectiveId, 5, out _));
|
||||
Assert.True(QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
ChainQuestId,
|
||||
1,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider).Success);
|
||||
Assert.True(TryAdvanceStep(deps, ChainQuestId, 1).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_refine", 1, out _));
|
||||
Assert.True(QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
ChainQuestId,
|
||||
2,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider).Success);
|
||||
Assert.True(TryAdvanceStep(deps, ChainQuestId, 2).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_stim", 1, out _));
|
||||
|
||||
// Act
|
||||
var advance = QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
ChainQuestId,
|
||||
3,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider);
|
||||
var advance = TryAdvanceStep(deps, ChainQuestId, 3);
|
||||
|
||||
// Assert
|
||||
Assert.True(advance.Success);
|
||||
|
|
@ -201,6 +259,12 @@ public sealed class QuestObjectiveWiringTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
|
@ -232,17 +296,51 @@ public sealed class QuestObjectiveWiringTests
|
|||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestStateOperationResult TryAdvanceStep(QuestWiringTestDependencies deps, string questId, int step) =>
|
||||
QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
questId,
|
||||
step,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestStateOperationResult TryMarkComplete(QuestWiringTestDependencies deps, string questId) =>
|
||||
QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
|
||||
{
|
||||
Assert.True(TryAccept(deps, questId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.TimeProvider).Success);
|
||||
Assert.True(TryMarkComplete(deps, questId).Success);
|
||||
}
|
||||
|
||||
private static GatherResult TryGather(QuestWiringTestDependencies deps, string nodeId) =>
|
||||
|
|
@ -259,6 +357,8 @@ public sealed class QuestObjectiveWiringTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
||||
|
|
@ -275,8 +375,40 @@ public sealed class QuestObjectiveWiringTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var slot in snapshot.BagSlots)
|
||||
{
|
||||
if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
|
||||
{
|
||||
total += slot.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static void FillRemainingBagSlots(QuestWiringTestDependencies deps)
|
||||
{
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var snapshot);
|
||||
var emptySlots = snapshot!.BagSlots.Count(static slot => slot.IsEmpty);
|
||||
for (var i = 0; i < emptySlots; i++)
|
||||
{
|
||||
var add = PlayerInventoryOperations.TryAddStack(
|
||||
PlayerId,
|
||||
"survey_drone_kit",
|
||||
quantity: 1,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore);
|
||||
Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SeedStack(QuestWiringTestDependencies deps, string itemId, int quantity)
|
||||
{
|
||||
var outcome = PlayerInventoryOperations.TryAddStack(
|
||||
|
|
@ -308,6 +440,8 @@ public sealed class QuestObjectiveWiringTests
|
|||
services.GetRequiredService<IEncounterProgressStore>(),
|
||||
services.GetRequiredService<IEncounterCompletionStore>(),
|
||||
services.GetRequiredService<IEncounterCompleteEventStore>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
|
|
@ -328,5 +462,7 @@ public sealed class QuestObjectiveWiringTests
|
|||
IEncounterProgressStore EncounterProgressStore,
|
||||
IEncounterCompletionStore CompletionStore,
|
||||
IEncounterCompleteEventStore CompleteEventStore,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore,
|
||||
TimeProvider TimeProvider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using NeonSprawl.Server.Game.Gathering;
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
|
@ -230,6 +231,12 @@ public sealed class QuestProgressApiTests
|
|||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
|
@ -261,17 +268,34 @@ public sealed class QuestProgressApiTests
|
|||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestStateOperationResult TryMarkComplete(QuestWiringTestDependencies deps, string questId) =>
|
||||
QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
|
||||
{
|
||||
Assert.True(TryAccept(deps, questId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.TimeProvider).Success);
|
||||
Assert.True(TryMarkComplete(deps, questId).Success);
|
||||
}
|
||||
|
||||
private static GatherResult TryGather(QuestWiringTestDependencies deps, string nodeId) =>
|
||||
|
|
@ -288,6 +312,8 @@ public sealed class QuestProgressApiTests
|
|||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
||||
|
|
@ -304,6 +330,8 @@ public sealed class QuestProgressApiTests
|
|||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static void SeedStack(QuestWiringTestDependencies deps, string itemId, int quantity)
|
||||
|
|
@ -337,6 +365,8 @@ public sealed class QuestProgressApiTests
|
|||
services.GetRequiredService<IEncounterProgressStore>(),
|
||||
services.GetRequiredService<IEncounterCompletionStore>(),
|
||||
services.GetRequiredService<IEncounterCompleteEventStore>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
|
|
@ -357,5 +387,7 @@ public sealed class QuestProgressApiTests
|
|||
IEncounterProgressStore EncounterProgressStore,
|
||||
IEncounterCompletionStore CompletionStore,
|
||||
IEncounterCompleteEventStore CompleteEventStore,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore,
|
||||
TimeProvider TimeProvider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -59,12 +62,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider).Success);
|
||||
Assert.True(TryMarkComplete(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var result = TryAccept(deps, RefineQuestId);
|
||||
|
|
@ -116,12 +114,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider).Success);
|
||||
Assert.True(TryMarkComplete(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var result = TryAccept(deps, GatherQuestId);
|
||||
|
|
@ -230,12 +223,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
"prototype_quest_missing",
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
var result = TryMarkComplete(deps, "prototype_quest_missing");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -265,12 +253,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider).Success);
|
||||
Assert.True(TryMarkComplete(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var result = TryAdvanceStep(deps, GatherQuestId, 1);
|
||||
|
|
@ -280,28 +263,114 @@ public sealed class QuestStateOperationsTests
|
|||
Assert.Equal(QuestStateReasonCodes.AlreadyCompleted, result.ReasonCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryMarkComplete_ShouldDeliverGatherIntroBundle_WhenFirstCompletion()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var result = TryMarkComplete(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.ReasonCode);
|
||||
Assert.Equal(QuestProgressStatus.Completed, result.Snapshot!.Status);
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var deliveryEvent));
|
||||
Assert.Equal(25, deps.SkillProgressionStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||
Assert.Single(deliveryEvent.GrantedSkillXp);
|
||||
Assert.Equal("salvage", deliveryEvent.GrantedSkillXp[0].SkillId);
|
||||
Assert.Equal(25, deliveryEvent.GrantedSkillXp[0].Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryMarkComplete_ShouldNotDoubleGrant_WhenIdempotentReplay()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(TryMarkComplete(deps, GatherQuestId).Success);
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var firstEvent));
|
||||
|
||||
// Act
|
||||
var replay = TryMarkComplete(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(replay.Success);
|
||||
Assert.Equal(25, deps.SkillProgressionStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var replayEvent));
|
||||
Assert.Equal(firstEvent.IdempotencyKey, replayEvent.IdempotencyKey);
|
||||
Assert.Equal(firstEvent.DeliveredAt, replayEvent.DeliveredAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryMarkComplete_ShouldDeliverOperatorChainBundle_WhenFirstCompletion()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
AcceptAndComplete(deps, GatherQuestId);
|
||||
AcceptAndComplete(deps, RefineQuestId);
|
||||
AcceptAndComplete(deps, CombatQuestId);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
|
||||
|
||||
// Act
|
||||
var result = TryMarkComplete(deps, ChainQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.ReasonCode);
|
||||
Assert.Equal(QuestProgressStatus.Completed, result.Snapshot!.Status);
|
||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, ChainQuestId, out var deliveryEvent));
|
||||
Assert.Single(deliveryEvent.GrantedItems);
|
||||
Assert.Equal("survey_drone_kit", deliveryEvent.GrantedItems[0].ItemId);
|
||||
Assert.Equal(1, deliveryEvent.GrantedItems[0].Quantity);
|
||||
Assert.Single(deliveryEvent.GrantedSkillXp);
|
||||
Assert.Equal("salvage", deliveryEvent.GrantedSkillXp[0].SkillId);
|
||||
Assert.Equal(50, deliveryEvent.GrantedSkillXp[0].Amount);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
Assert.Equal(CountBagItem(beforeInventory!, "survey_drone_kit") + 1, CountBagItem(afterInventory!, "survey_drone_kit"));
|
||||
Assert.Equal(100, deps.SkillProgressionStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryMarkComplete_ShouldDenyAndStayActive_WhenInventoryFull()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
AcceptAndComplete(deps, GatherQuestId);
|
||||
AcceptAndComplete(deps, RefineQuestId);
|
||||
AcceptAndComplete(deps, CombatQuestId);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
FillBag(deps);
|
||||
|
||||
// Act
|
||||
var result = TryMarkComplete(deps, ChainQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(RewardDeliveryReasonCodes.InventoryFull, result.ReasonCode);
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Active, progress.Status);
|
||||
Assert.False(deps.DeliveryStore.TryGet(PlayerId, ChainQuestId, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryMarkComplete_ShouldBeIdempotentAtOperationsLayer()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var first = QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider);
|
||||
var second = QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider);
|
||||
var first = TryMarkComplete(deps, GatherQuestId);
|
||||
var second = TryMarkComplete(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(first.Success);
|
||||
|
|
@ -319,15 +388,9 @@ public sealed class QuestStateOperationsTests
|
|||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider);
|
||||
var result = TryMarkComplete(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -343,12 +406,7 @@ public sealed class QuestStateOperationsTests
|
|||
|
||||
// Act
|
||||
var accept = TryAccept(deps, GatherQuestId);
|
||||
var complete = QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
var complete = TryMarkComplete(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(accept.Success);
|
||||
|
|
@ -380,16 +438,55 @@ public sealed class QuestStateOperationsTests
|
|||
Assert.True(canWrite);
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var slot in snapshot.BagSlots)
|
||||
{
|
||||
if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
|
||||
{
|
||||
total += slot.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static void FillBag(QuestTestDependencies deps)
|
||||
{
|
||||
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
|
||||
{
|
||||
var add = PlayerInventoryOperations.TryAddStack(
|
||||
PlayerId,
|
||||
"survey_drone_kit",
|
||||
quantity: 1,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore);
|
||||
Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AcceptAndComplete(QuestTestDependencies deps, string questId)
|
||||
{
|
||||
Assert.True(TryAccept(deps, questId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
Assert.True(TryMarkComplete(deps, questId).Success);
|
||||
}
|
||||
|
||||
private static QuestStateOperationResult TryMarkComplete(QuestTestDependencies deps, string questId) =>
|
||||
QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.TimeProvider).Success);
|
||||
}
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestStateOperationResult TryAccept(QuestTestDependencies deps, string questId) =>
|
||||
TryAccept(deps, PlayerId, questId);
|
||||
|
|
@ -402,6 +499,12 @@ public sealed class QuestStateOperationsTests
|
|||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestStateOperationResult TryAdvanceStep(QuestTestDependencies deps, string questId, int newStepIndex) =>
|
||||
|
|
@ -413,6 +516,12 @@ public sealed class QuestStateOperationsTests
|
|||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.SkillRegistry,
|
||||
deps.SkillProgressionStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.PerkStore,
|
||||
deps.DeliveryStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
|
||||
|
|
@ -423,6 +532,12 @@ public sealed class QuestStateOperationsTests
|
|||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
services.GetRequiredService<ISkillDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerSkillProgressionStore>(),
|
||||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
|
|
@ -431,5 +546,11 @@ public sealed class QuestStateOperationsTests
|
|||
IPlayerQuestStateStore ProgressStore,
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IItemDefinitionRegistry ItemRegistry,
|
||||
ISkillDefinitionRegistry SkillRegistry,
|
||||
IPlayerSkillProgressionStore SkillProgressionStore,
|
||||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IPlayerPerkStateStore PerkStore,
|
||||
IRewardDeliveryStore DeliveryStore,
|
||||
TimeProvider TimeProvider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ using NeonSprawl.Server.Game.Combat;
|
|||
using NeonSprawl.Server.Game.Encounters;
|
||||
using NeonSprawl.Server.Game.Gigs;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Npc;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Game.Targeting;
|
||||
using NeonSprawl.Server.Game.World;
|
||||
|
||||
|
|
@ -81,6 +84,12 @@ public static class AbilityCastApi
|
|||
IPlayerInventoryStore inventoryStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore questProgressStore,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
ILoggerFactory loggerFactory,
|
||||
TimeProvider clock) =>
|
||||
{
|
||||
|
|
@ -282,6 +291,12 @@ public static class AbilityCastApi
|
|||
inventoryStore,
|
||||
questRegistry,
|
||||
questProgressStore,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
clock,
|
||||
loggerFactory.CreateLogger(nameof(EncounterCombatWiring)));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Crafting;
|
||||
|
|
@ -30,6 +31,8 @@ public static class CraftOperations
|
|||
PerkUnlockEngine perkUnlockEngine,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (quantity <= 0)
|
||||
|
|
@ -165,6 +168,12 @@ public static class CraftOperations
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
xpStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
return new CraftResult(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using NeonSprawl.Server.Game.Items;
|
|||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Crafting;
|
||||
|
|
@ -18,7 +19,8 @@ public static class PlayerCraftApi
|
|||
IPlayerInventoryStore inventoryStore, ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine, IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore, TimeProvider timeProvider) =>
|
||||
IPlayerQuestStateStore progressStore, IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider) =>
|
||||
{
|
||||
if (body is null || body.SchemaVersion != PlayerCraftRequest.CurrentSchemaVersion)
|
||||
{
|
||||
|
|
@ -50,6 +52,8 @@ public static class PlayerCraftApi
|
|||
perkUnlockEngine,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (!result.Success &&
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Encounters;
|
||||
|
||||
|
|
@ -26,6 +29,12 @@ public static class EncounterCombatWiring
|
|||
IPlayerInventoryStore inventoryStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore questProgressStore,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
|
|
@ -84,6 +93,12 @@ public static class EncounterCombatWiring
|
|||
inventoryStore,
|
||||
questRegistry,
|
||||
questProgressStore,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (!completion.Success)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Encounters;
|
||||
|
||||
|
|
@ -29,6 +32,12 @@ public static class EncounterCompletionOperations
|
|||
IPlayerInventoryStore inventoryStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore questProgressStore,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!encounterRegistry.TryNormalizeKnown(encounterId, out var normalizedEncounterId) ||
|
||||
|
|
@ -145,6 +154,12 @@ public static class EncounterCompletionOperations
|
|||
questProgressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
return new EncounterCompletionResult(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Gathering;
|
||||
|
|
@ -31,6 +32,8 @@ public static class GatherOperations
|
|||
IResourceNodeInstanceStore instanceStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
var key = ResourceNodeInstanceIds.Normalize(interactableId);
|
||||
|
|
@ -140,6 +143,12 @@ public static class GatherOperations
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
xpStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
return new GatherResult(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using NeonSprawl.Server.Game.Items;
|
|||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Game.World;
|
||||
|
||||
|
|
@ -23,7 +24,8 @@ public static class InteractionApi
|
|||
IPlayerInventoryStore inventoryStore, ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine,
|
||||
IResourceNodeInstanceStore nodeInstanceStore, IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore, TimeProvider timeProvider) =>
|
||||
IPlayerQuestStateStore progressStore, IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider) =>
|
||||
{
|
||||
if (body is null || body.SchemaVersion != InteractionRequest.CurrentSchemaVersion)
|
||||
{
|
||||
|
|
@ -91,6 +93,8 @@ public static class InteractionApi
|
|||
nodeInstanceStore,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (!gather.Success)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
|
|
@ -13,7 +16,9 @@ public static class QuestAcceptApi
|
|||
(string id, string questId, QuestAcceptRequest? body, IPositionStateStore positions,
|
||||
IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider) =>
|
||||
ISkillDefinitionRegistry skillRegistry, IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine, IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider) =>
|
||||
{
|
||||
if (body is not null &&
|
||||
body.SchemaVersion != 0 &&
|
||||
|
|
@ -35,6 +40,12 @@ public static class QuestAcceptApi
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
return Results.Json(MapResponse(result));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
|
|
@ -19,6 +22,12 @@ public static class QuestObjectiveWiring
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
|
|
@ -59,6 +68,12 @@ public static class QuestObjectiveWiring
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -76,6 +91,12 @@ public static class QuestObjectiveWiring
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
|
|
@ -116,6 +137,12 @@ public static class QuestObjectiveWiring
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -132,6 +159,12 @@ public static class QuestObjectiveWiring
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
|
|
@ -171,6 +204,12 @@ public static class QuestObjectiveWiring
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -209,6 +248,12 @@ public static class QuestObjectiveWiring
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
|
|
@ -227,6 +272,12 @@ public static class QuestObjectiveWiring
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -286,6 +337,12 @@ public static class QuestObjectiveWiring
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
for (var iteration = 0; iteration < MaxStepCompletionIterations; iteration++)
|
||||
|
|
@ -316,6 +373,14 @@ public static class QuestObjectiveWiring
|
|||
questId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
itemRegistry,
|
||||
inventoryStore,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
return;
|
||||
}
|
||||
|
|
@ -328,6 +393,12 @@ public static class QuestObjectiveWiring
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (!advance.Success)
|
||||
|
|
@ -343,6 +414,12 @@ public static class QuestObjectiveWiring
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||
|
|
@ -356,6 +433,12 @@ public static class QuestObjectiveWiring
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
|
|
@ -16,7 +19,10 @@ public static class QuestProgressApi
|
|||
"/game/players/{id}/quest-progress",
|
||||
(string id, IPositionStateStore positions, IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry, TimeProvider timeProvider, ILogger<Program>? logger) =>
|
||||
IItemDefinitionRegistry itemRegistry, ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore, ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine, IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider, ILogger<Program>? logger) =>
|
||||
{
|
||||
var trimmedId = id.Trim();
|
||||
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
|
||||
|
|
@ -30,6 +36,12 @@ public static class QuestProgressApi
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider,
|
||||
logger);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Rewards;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
|
|
@ -20,6 +23,12 @@ public static class QuestStateOperations
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||
|
|
@ -73,6 +82,12 @@ public static class QuestStateOperations
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
||||
|
|
@ -97,6 +112,12 @@ public static class QuestStateOperations
|
|||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||
|
|
@ -146,6 +167,12 @@ public static class QuestStateOperations
|
|||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
||||
|
|
@ -160,17 +187,26 @@ public static class QuestStateOperations
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a quest complete. Idempotent: replays return success with the existing snapshot.
|
||||
/// Delivers <see cref="QuestDefRow.CompletionRewardBundle"/> then marks complete (NEO-128).
|
||||
/// Idempotent: replays return success with the existing snapshot without re-delivering.
|
||||
/// </summary>
|
||||
public static QuestStateOperationResult TryMarkComplete(
|
||||
string playerId,
|
||||
string questId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore skillProgressionStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IPlayerPerkStateStore perkStore,
|
||||
IRewardDeliveryStore deliveryStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||
!questRegistry.TryGetDefinition(normalizedQuestId, out _))
|
||||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
|
||||
{
|
||||
return Deny(QuestStateReasonCodes.UnknownQuest);
|
||||
}
|
||||
|
|
@ -190,6 +226,25 @@ public static class QuestStateOperations
|
|||
return Deny(QuestStateReasonCodes.NotActive);
|
||||
}
|
||||
|
||||
var delivery = RewardRouterOperations.TryDeliverQuestCompletion(
|
||||
playerId,
|
||||
normalizedQuestId,
|
||||
definition.CompletionRewardBundle,
|
||||
itemRegistry,
|
||||
inventoryStore,
|
||||
skillRegistry,
|
||||
skillProgressionStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
perkStore,
|
||||
deliveryStore,
|
||||
timeProvider);
|
||||
|
||||
if (!delivery.Success)
|
||||
{
|
||||
return Deny(delivery.ReasonCode ?? RewardDeliveryReasonCodes.InvalidQuestId, progress);
|
||||
}
|
||||
|
||||
if (progressStore.TryMarkComplete(playerId, normalizedQuestId, timeProvider.GetUtcNow(), out var snapshot))
|
||||
{
|
||||
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_complete` ---
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ Completed rows cannot regress to active without a reset API (none in prototype).
|
|||
|--------|------|
|
||||
| **`TryAccept`** | Validates quest id + **`prerequisiteQuestIds`** (each prerequisite must be **completed**), then activates at step 0. |
|
||||
| **`TryAdvanceStep`** | Requires active row; **`newStepIndex`** must be greater than current and less than step count from **`QuestDefRow`**. |
|
||||
| **`TryMarkComplete`** | Marks active quest complete. **Idempotent:** second call returns **`success: true`** with unchanged snapshot (unlike encounter completion). |
|
||||
| **`TryMarkComplete`** | **NEO-128:** delivers **`completionRewardBundle`** via **`RewardRouterOperations`** (deliver-then-mark), then marks active quest complete. **Idempotent:** when already **completed**, returns **`success: true`** without calling the router; replay after first-time complete also skips re-delivery. On delivery deny, quest stays **active** and **`reasonCode`** passthroughs router codes (e.g. **`inventory_full`**). |
|
||||
|
||||
**Reason codes:**
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ Encounter loot remains on **`IEncounterCompleteEventStore`** (E5.M3); quest comp
|
|||
| **`unknown_skill`**, **`invalid_amount`**, **`source_kind_not_allowed`** | Skill XP grant denied. |
|
||||
| **`progression_store_missing`** | Skill progression store could not apply XP delta. |
|
||||
|
||||
Apply order: **items first**, then **skill XP**. Inventory deny or skill deny fails closed without store record (compensating item rollback on partial apply or skill failure after items). Idempotent replay checks **`TryGet`** before apply. If grants succeed but **`TryRecord`** returns `false` (concurrent race), this call **rolls back** its item grants and calls **`SkillProgressionGrantOperations.TryRevertAppliedSkillGrants`** — negative XP delta per applied skill row plus removal of perks unlocked during this call only — then **`ReevaluateAfterLevelUp`** at post-rollback levels so a concurrent winner's path-auto unlocks are preserved — then returns the existing store event (mirror **`EncounterCompletionOperations`** compensating rollback when **`TryMarkCompleted`** loses). Quest-state wiring lands in NEO-128. Plan: [NEO-127 implementation plan](../../docs/plans/NEO-127-implementation-plan.md).
|
||||
Apply order: **items first**, then **skill XP**. Inventory deny or skill deny fails closed without store record (compensating item rollback on partial apply or skill failure after items). Idempotent replay checks **`TryGet`** before apply. If grants succeed but **`TryRecord`** returns `false` (concurrent race), this call **rolls back** its item grants and calls **`SkillProgressionGrantOperations.TryRevertAppliedSkillGrants`** — negative XP delta per applied skill row plus removal of perks unlocked during this call only — then **`ReevaluateAfterLevelUp`** at post-rollback levels so a concurrent winner's path-auto unlocks are preserved — then returns the existing store event (mirror **`EncounterCompletionOperations`** compensating rollback when **`TryMarkCompleted`** loses). **Quest-state wiring (NEO-128):** **`QuestStateOperations.TryMarkComplete`** and **`QuestObjectiveWiring`** terminal-step completion call this operation before **`IPlayerQuestStateStore.TryMarkComplete`**. Plans: [NEO-127](../../docs/plans/NEO-127-implementation-plan.md), [NEO-128](../../docs/plans/NEO-128-implementation-plan.md).
|
||||
|
||||
## Encounter definitions (NEO-103)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue