NEO-118: Wire quest objectives to gather, craft, and encounter events.
Add QuestObjectiveWiring with auto step advance/complete, hook into GatherOperations, CraftOperations, EncounterCompletionOperations, and QuestStateOperations accept/advance for inventory_has_item checks.pull/157/head
parent
286a1a036f
commit
71dbdd9a3e
|
|
@ -5,7 +5,7 @@ meta {
|
|||
}
|
||||
|
||||
docs {
|
||||
NEO-82 spine: slot 1 (slot 0 may be cooling from happy seq 22). Pre-request locks prototype_npc_melee, casts pulse four times (3.2s between), fifth request expects target_defeated deny.
|
||||
NEO-82 spine: slot 1 (slot 0 may be cooling from happy seq 22). Pre-request locks prototype_npc_melee, casts pulse four times (3.2s between), fifth request expects target_defeated deny. NEO-118: encounter completion grant path runs quest objective wiring (active encounter_complete quests).
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ meta {
|
|||
}
|
||||
|
||||
docs {
|
||||
NEO-70 prototype spine: accumulate >=11 scrap_metal_bulk (gather when nodes allow, reuse persisted inventory after dotnet test), refine_scrap_efficient when refined_plate_stock < 2, then make_field_stim_mk0. No inventory POST shortcuts for spine materials.
|
||||
NEO-70 prototype spine: accumulate >=11 scrap_metal_bulk (gather when nodes allow, reuse persisted inventory after dotnet test), refine_scrap_efficient when refined_plate_stock < 2, then make_field_stim_mk0. NEO-118: best-effort quest objective wiring on craft success (active craft_recipe quests). No inventory POST shortcuts for spine materials.
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ meta {
|
|||
}
|
||||
|
||||
docs {
|
||||
NEO-63: successful interact on `resource_node` kind runs GatherOperations — inventory grant (scrap_metal_bulk), salvage XP (activity, 10), and depletion. Pre-request moves in range of alpha anchor (12, -6). Optional: GET inventory / skill-progression after this request.
|
||||
NEO-63: successful interact on `resource_node` kind runs GatherOperations — inventory grant (scrap_metal_bulk), salvage XP (activity, 10), and depletion. NEO-118: best-effort quest objective wiring on gather success (active gather_item quests). Pre-request moves in range of alpha anchor (12, -6). Optional: GET inventory / skill-progression after this request.
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
| **Module ID** | E7.M1 |
|
||||
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
||||
| **Stage target** | Prototype |
|
||||
| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; objective wiring from **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) |
|
||||
| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) **`QuestObjectiveWiring`** **landed**; HTTP + client from **E7M1-08** [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) |
|
||||
| **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) |
|
||||
|
||||
## Purpose
|
||||
|
|
@ -84,7 +84,9 @@ The **first shipped quest spine** is **frozen** for prototype tuning until a del
|
|||
|
||||
**Player quest state store (NEO-116):** **`IPlayerQuestStateStore`** — in-memory + Postgres (`V008__player_quest_progress.sql`) per-player rows keyed by `(playerId, questId)`; implicit `not_started` when missing ([NEO-116](../../plans/NEO-116-implementation-plan.md)); [server README — Quest progress store (NEO-116)](../../../server/README.md#quest-progress-store-neo-116).
|
||||
|
||||
**Quest state operations (NEO-117):** **`QuestStateOperations`** — accept, step advance, complete with **`QuestStateReasonCodes`**; prerequisite gate on accept; idempotent complete at operations layer ([NEO-117](../../plans/NEO-117-implementation-plan.md)); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117). HTTP read/accept deferred to E7M1-08/09; objective wiring in E7M1-07.
|
||||
**Quest state operations (NEO-117):** **`QuestStateOperations`** — accept, step advance, complete with **`QuestStateReasonCodes`**; prerequisite gate on accept; idempotent complete at operations layer ([NEO-117](../../plans/NEO-117-implementation-plan.md)); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117). HTTP read/accept deferred to E7M1-08/09.
|
||||
|
||||
**Quest objective wiring (NEO-118):** **`QuestObjectiveWiring`** — gather/craft/encounter success hooks + **`inventory_has_item`** on accept/step advance/inventory mutations; auto advance/complete via **`QuestStateOperations`** ([NEO-118](../../plans/NEO-118-implementation-plan.md)); [server README — Quest objective wiring (NEO-118)](../../../server/README.md#quest-objective-wiring-neo-118).
|
||||
|
||||
**Reward policy (Slice 1):** Quest completion updates **`QuestStepState` only** — no duplicate item/XP grants. Gather/craft/encounter paths keep existing payouts; [E7.M2](E7_M2_RewardAndUnlockRouter.md) adds **`QuestRewardBundle`** apply in Slice 2.
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -109,7 +109,7 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl
|
|||
|---|---|---|---|---|---|
|
||||
| E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned |
|
||||
|
||||
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); register row stays **Planned** until E7M1-07+ objective wiring and client capstone.
|
||||
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); **E7M1-07 / NEO-118** **`QuestObjectiveWiring`** landed ([NEO-118 plan](../../plans/NEO-118-implementation-plan.md)); register row stays **Planned** until E7M1-08+ HTTP and client capstone.
|
||||
| E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned |
|
||||
| E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned |
|
||||
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Manual QA — NEO-118 (E7M1-07 quest objective wiring)
|
||||
|
||||
Server-side verification until [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) exposes **`GET /game/players/{id}/quest-progress`**.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Server running locally (`dotnet run` from `server/NeonSprawl.Server`)
|
||||
- Dev player **`dev-local-1`** with position row (default Development seed)
|
||||
- Automated suite green: `dotnet test server/NeonSprawl.Server.Tests --filter FullyQualifiedName~QuestObjectiveWiringTests`
|
||||
|
||||
## Checklist
|
||||
|
||||
1. [ ] **Gather intro** — Accept is not HTTP yet; use unit test `GatherIntro_ShouldComplete_WhenScrapGatheredThreeTimes` or temporary debug accept in REPL/tests. After accept + three alpha-node gathers, progress row for **`prototype_quest_gather_intro`** is **completed** (store test / wiring test).
|
||||
2. [ ] **Refine intro** — With gather intro complete, accept **`prototype_quest_refine_intro`**, craft **`refine_scrap_standard`** via `POST /game/players/dev-local-1/craft`; quest completes (wiring test `RefineIntro_ShouldComplete_WhenRefineScrapStandardCraftSucceeds`).
|
||||
3. [ ] **Combat intro** — Accept **`prototype_quest_combat_intro`**, complete **`prototype_combat_pocket`** via ability cast (NEO-106 path); quest completes (wiring test `CombatIntro_ShouldComplete_WhenEncounterCompletes`).
|
||||
4. [ ] **Operator chain** — With all three intros complete, accept **`prototype_quest_operator_chain`**, perform gather → refine → stim crafts per catalog; terminal **`inventory_has_item`** **`contract_handoff_token`** completes chain (wiring test `ChainQuest_ShouldAdvanceThroughMixedObjectives_WhenPrerequisitesMet`).
|
||||
5. [ ] **Regression** — Full server test suite passes (674+ tests).
|
||||
|
||||
## Notes
|
||||
|
||||
- Player-visible HUD verification lands in [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122).
|
||||
- Capstone Godot loop: [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) / `docs/manual-qa/NEO-123.md`.
|
||||
|
|
@ -243,10 +243,10 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco
|
|||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds.
|
||||
- [ ] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success.
|
||||
- [ ] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes.
|
||||
- [ ] Chain quest steps advance in order across mixed objective kinds.
|
||||
- [x] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds.
|
||||
- [x] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success.
|
||||
- [x] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes.
|
||||
- [x] Chain quest steps advance in order across mixed objective kinds.
|
||||
|
||||
**Client counterpart:** progress visible via [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) GET HUD after mutations.
|
||||
|
||||
|
|
|
|||
|
|
@ -55,10 +55,17 @@
|
|||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [ ] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds.
|
||||
- [ ] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success.
|
||||
- [ ] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes.
|
||||
- [ ] Chain quest steps advance in order across mixed objective kinds.
|
||||
- [x] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds.
|
||||
- [x] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success.
|
||||
- [x] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes.
|
||||
- [x] Chain quest steps advance in order across mixed objective kinds.
|
||||
|
||||
## Implementation reconciliation (shipped)
|
||||
|
||||
- **Types:** `QuestObjectiveKinds`, `QuestObjectiveWiring` (`TryProcessGatherSuccess`, `TryProcessCraftSuccess`, `TryProcessEncounterCompleteSuccess`, `TryProcessInventoryHasItemForQuest`, `TryCompleteStepIfSatisfied`).
|
||||
- **Hooks:** `GatherOperations`, `CraftOperations`, `EncounterCompletionOperations`, `QuestStateOperations.TryAccept` / `TryAdvanceStep`.
|
||||
- **Tests:** `QuestObjectiveWiringTests` (6 AAA cases); existing gather/craft/encounter/state tests updated for new parameters.
|
||||
- **Docs:** `server/README.md` objective wiring section; `docs/manual-qa/NEO-118.md`.
|
||||
|
||||
## Technical approach
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using NeonSprawl.Server.Game.Crafting;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
|
@ -31,7 +32,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -72,7 +76,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -104,7 +111,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
|
|
@ -138,7 +148,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -169,7 +182,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
|
|
@ -201,7 +217,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -230,7 +249,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -255,7 +277,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -283,7 +308,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
|
@ -313,7 +341,10 @@ public sealed class CraftOperationsTests
|
|||
deps.SkillRegistry,
|
||||
failingXpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine);
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
|
|
@ -359,7 +390,9 @@ public sealed class CraftOperationsTests
|
|||
services.GetRequiredService<ISkillDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerSkillProgressionStore>(),
|
||||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>());
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -394,7 +427,9 @@ public sealed class CraftOperationsTests
|
|||
ISkillDefinitionRegistry SkillRegistry,
|
||||
IPlayerSkillProgressionStore XpStore,
|
||||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine);
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore ProgressStore);
|
||||
|
||||
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Encounters;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -34,6 +35,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -63,6 +66,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -96,6 +101,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -127,6 +134,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -151,6 +160,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var before);
|
||||
|
||||
|
|
@ -167,6 +178,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -197,6 +210,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -236,6 +251,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -272,6 +289,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -292,6 +311,8 @@ public sealed class EncounterCombatWiringTests
|
|||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +361,9 @@ public sealed class EncounterCombatWiringTests
|
|||
services.GetRequiredService<IEncounterCompletionStore>(),
|
||||
services.GetRequiredService<IEncounterCompleteEventStore>(),
|
||||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>());
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -364,5 +387,7 @@ public sealed class EncounterCombatWiringTests
|
|||
IEncounterCompletionStore CompletionStore,
|
||||
IEncounterCompleteEventStore CompleteEventStore,
|
||||
IItemDefinitionRegistry ItemRegistry,
|
||||
IPlayerInventoryStore InventoryStore);
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore QuestProgressStore);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Encounters;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -34,6 +35,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
timeProvider);
|
||||
|
||||
// Assert
|
||||
|
|
@ -75,6 +78,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
timeProvider);
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterFirst);
|
||||
|
||||
|
|
@ -89,6 +94,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
timeProvider);
|
||||
|
||||
// Assert
|
||||
|
|
@ -145,6 +152,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -177,6 +186,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -211,6 +222,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -243,6 +256,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
inventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -273,6 +288,8 @@ public sealed class EncounterCompletionOperationsTests
|
|||
deps.EventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.QuestProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
// Assert
|
||||
|
|
@ -333,7 +350,9 @@ public sealed class EncounterCompletionOperationsTests
|
|||
services.GetRequiredService<IEncounterCompletionStore>(),
|
||||
services.GetRequiredService<IEncounterCompleteEventStore>(),
|
||||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>());
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
}
|
||||
|
||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -371,7 +390,9 @@ public sealed class EncounterCompletionOperationsTests
|
|||
IEncounterCompletionStore CompletionStore,
|
||||
IEncounterCompleteEventStore EventStore,
|
||||
IItemDefinitionRegistry ItemRegistry,
|
||||
IPlayerInventoryStore InventoryStore);
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore QuestProgressStore);
|
||||
|
||||
private sealed class CompletionStoreDeniesMark(IEncounterCompletionStore inner) : IEncounterCompletionStore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using NeonSprawl.Server.Game.Gathering;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
|
@ -32,7 +33,10 @@ public sealed class GatherOperationsTests
|
|||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
var salvageXp = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");
|
||||
|
|
@ -83,7 +87,10 @@ public sealed class GatherOperationsTests
|
|||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
var salvageXpAfter = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");
|
||||
|
|
@ -116,7 +123,10 @@ public sealed class GatherOperationsTests
|
|||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
Assert.True(gather.Success);
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +144,10 @@ public sealed class GatherOperationsTests
|
|||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
var salvageXpAfter = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");
|
||||
|
|
@ -166,7 +179,10 @@ public sealed class GatherOperationsTests
|
|||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
var rowExists = deps.InstanceStore.TryGetRemainingGathers(unknownId, out _);
|
||||
|
||||
|
|
@ -196,7 +212,10 @@ public sealed class GatherOperationsTests
|
|||
failingXpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||
deps.InstanceStore.TryGetRemainingGathers(AlphaId, out var nodeSnapshot);
|
||||
|
|
@ -227,7 +246,10 @@ public sealed class GatherOperationsTests
|
|||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore);
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
TimeProvider.System);
|
||||
|
||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||
|
||||
|
|
@ -249,7 +271,9 @@ public sealed class GatherOperationsTests
|
|||
services.GetRequiredService<IPlayerSkillProgressionStore>(),
|
||||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IResourceNodeInstanceStore>());
|
||||
services.GetRequiredService<IResourceNodeInstanceStore>(),
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
}
|
||||
|
||||
private static int CountItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||
|
|
@ -285,7 +309,9 @@ public sealed class GatherOperationsTests
|
|||
IPlayerSkillProgressionStore XpStore,
|
||||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IResourceNodeInstanceStore InstanceStore);
|
||||
IResourceNodeInstanceStore InstanceStore,
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore ProgressStore);
|
||||
|
||||
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using NeonSprawl.Server.Game.Items;
|
|||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Npc;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using Npgsql;
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
|||
d.ServiceType == typeof(IPlayerInventoryStore) ||
|
||||
d.ServiceType == typeof(IResourceNodeInstanceStore) ||
|
||||
d.ServiceType == typeof(IPlayerPerkStateStore) ||
|
||||
d.ServiceType == typeof(IPlayerQuestStateStore) ||
|
||||
d.ServiceType == typeof(TimeProvider) ||
|
||||
d.ServiceType == typeof(IPlayerAbilityCooldownStore) ||
|
||||
d.ServiceType == typeof(ISkillDefinitionRegistry) ||
|
||||
|
|
@ -89,6 +91,7 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
|||
services.AddSingleton<IPlayerInventoryStore, InMemoryPlayerInventoryStore>();
|
||||
services.AddSingleton<IResourceNodeInstanceStore, InMemoryResourceNodeInstanceStore>();
|
||||
services.AddSingleton<IPlayerPerkStateStore, InMemoryPlayerPerkStateStore>();
|
||||
services.AddSingleton<IPlayerQuestStateStore, InMemoryPlayerQuestStateStore>();
|
||||
services.AddSingleton<IPlayerAbilityCooldownStore, InMemoryPlayerAbilityCooldownStore>();
|
||||
services.AddSingleton<ISkillDefinitionRegistry, SalvageActivityDeniedSkillRegistry>();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,332 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Crafting;
|
||||
using NeonSprawl.Server.Game.Encounters;
|
||||
using NeonSprawl.Server.Game.Gathering;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace NeonSprawl.Server.Tests.Game.Quests;
|
||||
|
||||
public sealed class QuestObjectiveWiringTests
|
||||
{
|
||||
private const string PlayerId = "dev-local-1";
|
||||
private const string AlphaNodeId = "prototype_resource_node_alpha";
|
||||
private const string DeltaNodeId = "prototype_urban_bulk_delta";
|
||||
private const string CombatEncounterId = "prototype_combat_pocket";
|
||||
private const string GatherQuestId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId;
|
||||
private const string RefineQuestId = PrototypeE7M1QuestCatalogRules.RefineIntroQuestId;
|
||||
private const string CombatQuestId = PrototypeE7M1QuestCatalogRules.CombatIntroQuestId;
|
||||
private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId;
|
||||
private const string GatherObjectiveId = PrototypeE7M1QuestCatalogRules.GatherIntroFirstObjectiveId;
|
||||
private const string ChainGatherObjectiveId = PrototypeE7M1QuestCatalogRules.ChainFirstObjectiveId;
|
||||
|
||||
[Fact]
|
||||
public async Task GatherIntro_ShouldComplete_WhenScrapGatheredThreeTimes()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
Assert.True(TryGather(deps, AlphaNodeId).Success);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, GatherQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefineIntro_ShouldComplete_WhenRefineScrapStandardCraftSucceeds()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
AcceptAndMarkComplete(deps, GatherQuestId);
|
||||
Assert.True(TryAccept(deps, RefineQuestId).Success);
|
||||
SeedStack(deps, "scrap_metal_bulk", 5);
|
||||
|
||||
// Act
|
||||
var craft = TryCraft(deps, "refine_scrap_standard");
|
||||
|
||||
// Assert
|
||||
Assert.True(craft.Success);
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, RefineQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CombatIntro_ShouldComplete_WhenEncounterCompletes()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(TryAccept(deps, CombatQuestId).Success);
|
||||
|
||||
// Act
|
||||
QuestObjectiveWiring.TryProcessEncounterCompleteSuccess(
|
||||
PlayerId,
|
||||
CombatEncounterId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider);
|
||||
|
||||
// Assert
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, CombatQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChainQuest_ShouldAdvanceThroughMixedObjectives_WhenPrerequisitesMet()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
AcceptAndMarkComplete(deps, GatherQuestId);
|
||||
AcceptAndMarkComplete(deps, RefineQuestId);
|
||||
CompleteCombatEncounter(deps);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
|
||||
// Act — step 0: gather 5 scrap (delta node yield)
|
||||
Assert.True(TryGather(deps, DeltaNodeId).Success);
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var afterGather));
|
||||
Assert.Equal(1, afterGather.CurrentStepIndex);
|
||||
|
||||
// Act — step 1: refine craft
|
||||
SeedStack(deps, "scrap_metal_bulk", 5);
|
||||
Assert.True(TryCraft(deps, "refine_scrap_standard").Success);
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var afterRefine));
|
||||
Assert.Equal(2, afterRefine.CurrentStepIndex);
|
||||
|
||||
// Act — step 2: field stim craft
|
||||
SeedStack(deps, "refined_plate_stock", 2);
|
||||
SeedStack(deps, "scrap_metal_bulk", 1);
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GatherIntro_ShouldCompleteInOneGather_WhenNodeYieldExceedsObjectiveQuantity()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
Assert.True(TryGather(deps, DeltaNodeId).Success);
|
||||
|
||||
// Assert
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, GatherQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
|
||||
Assert.Equal(3, progress.ObjectiveCounters[GatherObjectiveId]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InventoryHasItem_ShouldCompleteChainTerminalStep_WhenTokenAlreadyHeldOnStepAdvance()
|
||||
{
|
||||
// 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(QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
ChainQuestId,
|
||||
1,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider).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(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);
|
||||
|
||||
// Assert
|
||||
Assert.True(advance.Success);
|
||||
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var progress));
|
||||
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
|
||||
}
|
||||
|
||||
private static void CompleteCombatEncounter(QuestWiringTestDependencies deps)
|
||||
{
|
||||
Assert.True(TryAccept(deps, CombatQuestId).Success);
|
||||
DefeatAllRequiredTargets(deps);
|
||||
var result = EncounterCompletionOperations.TryCompleteAndGrant(
|
||||
PlayerId,
|
||||
CombatEncounterId,
|
||||
deps.EncounterRegistry,
|
||||
deps.RewardTableRegistry,
|
||||
deps.EncounterProgressStore,
|
||||
deps.CompletionStore,
|
||||
deps.CompleteEventStore,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.TimeProvider);
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
private static void DefeatAllRequiredTargets(QuestWiringTestDependencies deps)
|
||||
{
|
||||
foreach (var npcId in new[] { "prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite" })
|
||||
{
|
||||
_ = EncounterProgressOperations.TryActivateOnFirstEngagement(
|
||||
PlayerId,
|
||||
npcId,
|
||||
deps.EncounterRegistry,
|
||||
deps.EncounterProgressStore,
|
||||
deps.CompletionStore);
|
||||
_ = EncounterProgressOperations.TryMarkTargetDefeated(
|
||||
PlayerId,
|
||||
npcId,
|
||||
deps.EncounterRegistry,
|
||||
deps.EncounterProgressStore,
|
||||
deps.CompletionStore);
|
||||
}
|
||||
}
|
||||
|
||||
private static QuestStateOperationResult TryAccept(QuestWiringTestDependencies deps, string questId) =>
|
||||
QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
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);
|
||||
}
|
||||
|
||||
private static GatherResult TryGather(QuestWiringTestDependencies deps, string nodeId) =>
|
||||
GatherOperations.TryGather(
|
||||
PlayerId,
|
||||
nodeId,
|
||||
deps.NodeRegistry,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.InstanceStore,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
||||
CraftOperations.TryCraft(
|
||||
PlayerId,
|
||||
recipeId,
|
||||
quantity: 1,
|
||||
deps.RecipeRegistry,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore,
|
||||
deps.SkillRegistry,
|
||||
deps.XpStore,
|
||||
deps.LevelCurve,
|
||||
deps.PerkUnlockEngine,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static void SeedStack(QuestWiringTestDependencies deps, string itemId, int quantity)
|
||||
{
|
||||
var outcome = PlayerInventoryOperations.TryAddStack(
|
||||
PlayerId,
|
||||
itemId,
|
||||
quantity,
|
||||
deps.ItemRegistry,
|
||||
deps.InventoryStore);
|
||||
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
|
||||
}
|
||||
|
||||
private static QuestWiringTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
|
||||
{
|
||||
var services = factory.Services;
|
||||
return new QuestWiringTestDependencies(
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
services.GetRequiredService<IResourceNodeDefinitionRegistry>(),
|
||||
services.GetRequiredService<IResourceNodeInstanceStore>(),
|
||||
services.GetRequiredService<IRecipeDefinitionRegistry>(),
|
||||
services.GetRequiredService<ISkillDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerSkillProgressionStore>(),
|
||||
services.GetRequiredService<ISkillLevelCurve>(),
|
||||
services.GetRequiredService<PerkUnlockEngine>(),
|
||||
services.GetRequiredService<IEncounterDefinitionRegistry>(),
|
||||
services.GetRequiredService<IRewardTableDefinitionRegistry>(),
|
||||
services.GetRequiredService<IEncounterProgressStore>(),
|
||||
services.GetRequiredService<IEncounterCompletionStore>(),
|
||||
services.GetRequiredService<IEncounterCompleteEventStore>(),
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
private readonly record struct QuestWiringTestDependencies(
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore ProgressStore,
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IItemDefinitionRegistry ItemRegistry,
|
||||
IResourceNodeDefinitionRegistry NodeRegistry,
|
||||
IResourceNodeInstanceStore InstanceStore,
|
||||
IRecipeDefinitionRegistry RecipeRegistry,
|
||||
ISkillDefinitionRegistry SkillRegistry,
|
||||
IPlayerSkillProgressionStore XpStore,
|
||||
ISkillLevelCurve LevelCurve,
|
||||
PerkUnlockEngine PerkUnlockEngine,
|
||||
IEncounterDefinitionRegistry EncounterRegistry,
|
||||
IRewardTableDefinitionRegistry RewardTableRegistry,
|
||||
IEncounterProgressStore EncounterProgressStore,
|
||||
IEncounterCompletionStore CompletionStore,
|
||||
IEncounterCompleteEventStore CompleteEventStore,
|
||||
TimeProvider TimeProvider);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Tests;
|
||||
|
|
@ -24,11 +25,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
|
|
@ -46,11 +43,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
RefineQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, RefineQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -65,7 +58,7 @@ public sealed class QuestStateOperationsTests
|
|||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
|
|
@ -74,11 +67,7 @@ public sealed class QuestStateOperationsTests
|
|||
timeProvider).Success);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
RefineQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, RefineQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
|
|
@ -94,11 +83,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
"prototype_quest_missing",
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, "prototype_quest_missing");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -111,14 +96,10 @@ public sealed class QuestStateOperationsTests
|
|||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -134,7 +115,7 @@ public sealed class QuestStateOperationsTests
|
|||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
|
|
@ -143,11 +124,7 @@ public sealed class QuestStateOperationsTests
|
|||
timeProvider).Success);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -163,11 +140,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
UnknownPlayerId,
|
||||
RefineQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, UnknownPlayerId, RefineQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -182,11 +155,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAccept(
|
||||
UnknownPlayerId,
|
||||
GatherQuestId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAccept(deps, UnknownPlayerId, GatherQuestId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -200,19 +169,14 @@ public sealed class QuestStateOperationsTests
|
|||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
AcceptAndComplete(deps, GatherQuestId, timeProvider);
|
||||
AcceptAndComplete(deps, RefineQuestId, timeProvider);
|
||||
AcceptAndComplete(deps, CombatQuestId, timeProvider);
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
AcceptAndComplete(deps, GatherQuestId);
|
||||
AcceptAndComplete(deps, RefineQuestId);
|
||||
AcceptAndComplete(deps, CombatQuestId);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainObjectiveId, 5, out _));
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
ChainQuestId,
|
||||
1,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAdvanceStep(deps, ChainQuestId, 1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
|
|
@ -230,18 +194,13 @@ public sealed class QuestStateOperationsTests
|
|||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
AcceptAndComplete(deps, GatherQuestId, timeProvider);
|
||||
AcceptAndComplete(deps, RefineQuestId, timeProvider);
|
||||
AcceptAndComplete(deps, CombatQuestId, timeProvider);
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
AcceptAndComplete(deps, GatherQuestId);
|
||||
AcceptAndComplete(deps, RefineQuestId);
|
||||
AcceptAndComplete(deps, CombatQuestId);
|
||||
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
ChainQuestId,
|
||||
newStepIndex,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAdvanceStep(deps, ChainQuestId, newStepIndex);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -256,12 +215,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
"prototype_quest_missing",
|
||||
1,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAdvanceStep(deps, "prototype_quest_missing", 1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -296,12 +250,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
1,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAdvanceStep(deps, GatherQuestId, 1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -315,7 +264,7 @@ public sealed class QuestStateOperationsTests
|
|||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
|
|
@ -324,12 +273,7 @@ public sealed class QuestStateOperationsTests
|
|||
timeProvider).Success);
|
||||
|
||||
// Act
|
||||
var result = QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
1,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore);
|
||||
var result = TryAdvanceStep(deps, GatherQuestId, 1);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
|
|
@ -343,7 +287,7 @@ public sealed class QuestStateOperationsTests
|
|||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var deps = ResolveDependencies(factory);
|
||||
var timeProvider = TimeProvider.System;
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
Assert.True(TryAccept(deps, GatherQuestId).Success);
|
||||
|
||||
// Act
|
||||
var first = QuestStateOperations.TryMarkComplete(
|
||||
|
|
@ -398,7 +342,7 @@ public sealed class QuestStateOperationsTests
|
|||
var deps = ResolveDependencies(factory);
|
||||
|
||||
// Act
|
||||
var accept = QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore);
|
||||
var accept = TryAccept(deps, GatherQuestId);
|
||||
var complete = QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
GatherQuestId,
|
||||
|
|
@ -436,26 +380,56 @@ public sealed class QuestStateOperationsTests
|
|||
Assert.True(canWrite);
|
||||
}
|
||||
|
||||
private static void AcceptAndComplete(
|
||||
(IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) deps,
|
||||
string questId,
|
||||
TimeProvider timeProvider)
|
||||
private static void AcceptAndComplete(QuestTestDependencies deps, string questId)
|
||||
{
|
||||
Assert.True(QuestStateOperations.TryAccept(PlayerId, questId, deps.QuestRegistry, deps.ProgressStore).Success);
|
||||
Assert.True(TryAccept(deps, questId).Success);
|
||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||
PlayerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
timeProvider).Success);
|
||||
deps.TimeProvider).Success);
|
||||
}
|
||||
|
||||
private static (IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) ResolveDependencies(
|
||||
InMemoryWebApplicationFactory factory)
|
||||
private static QuestStateOperationResult TryAccept(QuestTestDependencies deps, string questId) =>
|
||||
TryAccept(deps, PlayerId, questId);
|
||||
|
||||
private static QuestStateOperationResult TryAccept(QuestTestDependencies deps, string playerId, string questId) =>
|
||||
QuestStateOperations.TryAccept(
|
||||
playerId,
|
||||
questId,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestStateOperationResult TryAdvanceStep(QuestTestDependencies deps, string questId, int newStepIndex) =>
|
||||
QuestStateOperations.TryAdvanceStep(
|
||||
PlayerId,
|
||||
questId,
|
||||
newStepIndex,
|
||||
deps.QuestRegistry,
|
||||
deps.ProgressStore,
|
||||
deps.InventoryStore,
|
||||
deps.ItemRegistry,
|
||||
deps.TimeProvider);
|
||||
|
||||
private static QuestTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
|
||||
{
|
||||
var services = factory.Services;
|
||||
return (
|
||||
return new QuestTestDependencies(
|
||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||
services.GetRequiredService<IPlayerQuestStateStore>());
|
||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||
services.GetRequiredService<IPlayerInventoryStore>(),
|
||||
services.GetRequiredService<IItemDefinitionRegistry>(),
|
||||
TimeProvider.System);
|
||||
}
|
||||
|
||||
private readonly record struct QuestTestDependencies(
|
||||
IQuestDefinitionRegistry QuestRegistry,
|
||||
IPlayerQuestStateStore ProgressStore,
|
||||
IPlayerInventoryStore InventoryStore,
|
||||
IItemDefinitionRegistry ItemRegistry,
|
||||
TimeProvider TimeProvider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using NeonSprawl.Server.Game.Gigs;
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Npc;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Targeting;
|
||||
using NeonSprawl.Server.Game.World;
|
||||
|
||||
|
|
@ -78,6 +79,8 @@ public static class AbilityCastApi
|
|||
IEncounterCompleteEventStore encounterCompleteEventStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore questProgressStore,
|
||||
ILoggerFactory loggerFactory,
|
||||
TimeProvider clock) =>
|
||||
{
|
||||
|
|
@ -277,6 +280,8 @@ public static class AbilityCastApi
|
|||
encounterCompleteEventStore,
|
||||
itemRegistry,
|
||||
inventoryStore,
|
||||
questRegistry,
|
||||
questProgressStore,
|
||||
clock,
|
||||
loggerFactory.CreateLogger(nameof(EncounterCombatWiring)));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Crafting;
|
||||
|
|
@ -26,7 +27,10 @@ public static class CraftOperations
|
|||
ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore xpStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine)
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (quantity <= 0)
|
||||
{
|
||||
|
|
@ -153,6 +157,16 @@ public static class CraftOperations
|
|||
// (skillId, amount, sourceKind). No ingest or ILogger here (comments-only). Successful output grants also pass
|
||||
// through PlayerInventoryOperations (`item_created` hook, NEO-56). PlayerCraftApi delegates here (no duplicate hooks).
|
||||
|
||||
QuestObjectiveWiring.TryProcessCraftSuccess(
|
||||
playerId,
|
||||
recipeKey,
|
||||
quantity,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
|
||||
return new CraftResult(
|
||||
Success: true,
|
||||
ReasonCode: null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,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.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Crafting;
|
||||
|
|
@ -16,7 +17,8 @@ public static class PlayerCraftApi
|
|||
IRecipeDefinitionRegistry recipeRegistry, IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore, ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine) =>
|
||||
PerkUnlockEngine perkUnlockEngine, IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore, TimeProvider timeProvider) =>
|
||||
{
|
||||
if (body is null || body.SchemaVersion != PlayerCraftRequest.CurrentSchemaVersion)
|
||||
{
|
||||
|
|
@ -45,7 +47,10 @@ public static class PlayerCraftApi
|
|||
skillRegistry,
|
||||
xpStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine);
|
||||
perkUnlockEngine,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
timeProvider);
|
||||
|
||||
if (!result.Success &&
|
||||
(string.Equals(result.ReasonCode, CraftReasonCodes.InventoryStoreMissing, StringComparison.Ordinal) ||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Encounters;
|
||||
|
||||
|
|
@ -18,11 +19,13 @@ public static class EncounterCombatWiring
|
|||
bool targetDefeated,
|
||||
IEncounterDefinitionRegistry encounterRegistry,
|
||||
IRewardTableDefinitionRegistry rewardTableRegistry,
|
||||
IEncounterProgressStore progressStore,
|
||||
IEncounterProgressStore encounterProgressStore,
|
||||
IEncounterCompletionStore completionStore,
|
||||
IEncounterCompleteEventStore completeEventStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore questProgressStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
|
|
@ -32,7 +35,7 @@ public static class EncounterCombatWiring
|
|||
playerId,
|
||||
targetNpcInstanceId,
|
||||
encounterRegistry,
|
||||
progressStore,
|
||||
encounterProgressStore,
|
||||
completionStore);
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ public static class EncounterCombatWiring
|
|||
playerId,
|
||||
targetNpcInstanceId,
|
||||
encounterRegistry,
|
||||
progressStore,
|
||||
encounterProgressStore,
|
||||
completionStore))
|
||||
{
|
||||
return;
|
||||
|
|
@ -64,7 +67,7 @@ public static class EncounterCombatWiring
|
|||
playerId,
|
||||
encounterId,
|
||||
encounterRegistry,
|
||||
progressStore))
|
||||
encounterProgressStore))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -74,11 +77,13 @@ public static class EncounterCombatWiring
|
|||
encounterId,
|
||||
encounterRegistry,
|
||||
rewardTableRegistry,
|
||||
progressStore,
|
||||
encounterProgressStore,
|
||||
completionStore,
|
||||
completeEventStore,
|
||||
itemRegistry,
|
||||
inventoryStore,
|
||||
questRegistry,
|
||||
questProgressStore,
|
||||
timeProvider);
|
||||
|
||||
if (!completion.Success)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Encounters;
|
||||
|
||||
|
|
@ -26,6 +27,8 @@ public static class EncounterCompletionOperations
|
|||
IEncounterCompleteEventStore completeEventStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore questProgressStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!encounterRegistry.TryNormalizeKnown(encounterId, out var normalizedEncounterId) ||
|
||||
|
|
@ -135,6 +138,15 @@ public static class EncounterCompletionOperations
|
|||
// Planned payload: playerId, encounterId, rewardTableId, grantedItems[] (EncounterGrantApplied batch).
|
||||
// Inventory layer still emits item_created per stack via PlayerInventoryOperations (NEO-56).
|
||||
|
||||
QuestObjectiveWiring.TryProcessEncounterCompleteSuccess(
|
||||
normalizedPlayerId,
|
||||
normalizedEncounterId,
|
||||
questRegistry,
|
||||
questProgressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
|
||||
return new EncounterCompletionResult(
|
||||
Success: true,
|
||||
ReasonCode: null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Gathering;
|
||||
|
|
@ -27,7 +28,10 @@ public static class GatherOperations
|
|||
IPlayerSkillProgressionStore xpStore,
|
||||
ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine,
|
||||
IResourceNodeInstanceStore instanceStore)
|
||||
IResourceNodeInstanceStore instanceStore,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
var key = ResourceNodeInstanceIds.Normalize(interactableId);
|
||||
if (key.Length == 0 ||
|
||||
|
|
@ -128,6 +132,16 @@ public static class GatherOperations
|
|||
// (last gatherer), maxGathers from definition when wiring E9.M1.
|
||||
}
|
||||
|
||||
QuestObjectiveWiring.TryProcessGatherSuccess(
|
||||
playerId,
|
||||
yield.ItemId,
|
||||
yield.Quantity,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
|
||||
return new GatherResult(
|
||||
Success: true,
|
||||
ReasonCode: null,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using NeonSprawl.Server.Game.Gathering;
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Quests;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Game.World;
|
||||
|
||||
|
|
@ -21,7 +22,8 @@ public static class InteractionApi
|
|||
IResourceNodeDefinitionRegistry nodeRegistry, IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore, ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine,
|
||||
IResourceNodeInstanceStore nodeInstanceStore) =>
|
||||
IResourceNodeInstanceStore nodeInstanceStore, IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore, TimeProvider timeProvider) =>
|
||||
{
|
||||
if (body is null || body.SchemaVersion != InteractionRequest.CurrentSchemaVersion)
|
||||
{
|
||||
|
|
@ -86,7 +88,10 @@ public static class InteractionApi
|
|||
xpStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine,
|
||||
nodeInstanceStore);
|
||||
nodeInstanceStore,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
timeProvider);
|
||||
|
||||
if (!gather.Success)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
/// <summary>Quest objective kind strings aligned with <c>quest-objective-def.schema.json</c> (NEO-118).</summary>
|
||||
public static class QuestObjectiveKinds
|
||||
{
|
||||
public const string GatherItem = "gather_item";
|
||||
|
||||
public const string CraftRecipe = "craft_recipe";
|
||||
|
||||
public const string EncounterComplete = "encounter_complete";
|
||||
|
||||
public const string InventoryHasItem = "inventory_has_item";
|
||||
}
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
/// <summary>
|
||||
/// NEO-118: best-effort quest objective evaluation on gather/craft/encounter success and
|
||||
/// <see cref="QuestObjectiveKinds.InventoryHasItem"/> snapshot passes (accept / step advance / inventory mutations).
|
||||
/// </summary>
|
||||
public static class QuestObjectiveWiring
|
||||
{
|
||||
private const int MaxStepCompletionIterations = 8;
|
||||
|
||||
/// <summary>Credits <see cref="QuestObjectiveKinds.GatherItem"/> objectives after a successful gather grant.</summary>
|
||||
public static void TryProcessGatherSuccess(
|
||||
string playerId,
|
||||
string itemId,
|
||||
int quantityGranted,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
if (quantityGranted <= 0 || itemId.Trim().Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var normalizedItemId = itemId.Trim();
|
||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||
{
|
||||
if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyGatherItemObjectives(
|
||||
playerId,
|
||||
definition,
|
||||
progress,
|
||||
normalizedItemId,
|
||||
quantityGranted,
|
||||
progressStore);
|
||||
}
|
||||
|
||||
RefreshInventoryHasItemCountersForPlayer(
|
||||
playerId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry);
|
||||
|
||||
TryCompleteAllActiveQuests(
|
||||
playerId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogDebug(ex, "Quest gather wiring skipped for player {PlayerId}", playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Credits <see cref="QuestObjectiveKinds.CraftRecipe"/> objectives after a successful craft.</summary>
|
||||
public static void TryProcessCraftSuccess(
|
||||
string playerId,
|
||||
string recipeId,
|
||||
int batchQuantity,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
if (batchQuantity <= 0 || recipeId.Trim().Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var normalizedRecipeId = recipeId.Trim();
|
||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||
{
|
||||
if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyCraftRecipeObjectives(
|
||||
playerId,
|
||||
definition,
|
||||
progress,
|
||||
normalizedRecipeId,
|
||||
batchQuantity,
|
||||
progressStore);
|
||||
}
|
||||
|
||||
RefreshInventoryHasItemCountersForPlayer(
|
||||
playerId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry);
|
||||
|
||||
TryCompleteAllActiveQuests(
|
||||
playerId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogDebug(ex, "Quest craft wiring skipped for player {PlayerId}", playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Credits <see cref="QuestObjectiveKinds.EncounterComplete"/> objectives after encounter completion.</summary>
|
||||
public static void TryProcessEncounterCompleteSuccess(
|
||||
string playerId,
|
||||
string encounterId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
if (encounterId.Trim().Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var normalizedEncounterId = encounterId.Trim();
|
||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||
{
|
||||
if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyEncounterCompleteObjectives(
|
||||
playerId,
|
||||
definition,
|
||||
progress,
|
||||
normalizedEncounterId,
|
||||
progressStore);
|
||||
}
|
||||
|
||||
RefreshInventoryHasItemCountersForPlayer(
|
||||
playerId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry);
|
||||
|
||||
TryCompleteAllActiveQuests(
|
||||
playerId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogDebug(ex, "Quest encounter wiring skipped for player {PlayerId}", playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Re-evaluates <see cref="QuestObjectiveKinds.InventoryHasItem"/> counters for every active quest.</summary>
|
||||
public static void RefreshInventoryHasItemCountersForPlayer(
|
||||
string playerId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry)
|
||||
{
|
||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||
{
|
||||
TryProcessInventoryHasItemForQuest(
|
||||
playerId,
|
||||
definition.Id,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Re-evaluates <see cref="QuestObjectiveKinds.InventoryHasItem"/> on the active step for one quest.</summary>
|
||||
public static void TryProcessInventoryHasItemForQuest(
|
||||
string playerId,
|
||||
string questId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry)
|
||||
{
|
||||
if (!TryGetActiveProgress(playerId, questId, progressStore, out var progress) ||
|
||||
!questRegistry.TryGetDefinition(questId, out var definition) ||
|
||||
!inventoryStore.TryGetSnapshot(playerId, out var inventory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stepIndex = progress.CurrentStepIndex;
|
||||
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var objective in definition.Steps[stepIndex].Objectives)
|
||||
{
|
||||
if (!string.Equals(objective.Kind, QuestObjectiveKinds.InventoryHasItem, StringComparison.Ordinal) ||
|
||||
objective.ItemId is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var required = GetRequiredQuantity(objective);
|
||||
var held = PlayerInventoryOperations.GetHeldQuantity(inventory, objective.ItemId, itemRegistry);
|
||||
var newCount = Math.Min(held, required);
|
||||
var current = GetCounter(progress, objective.Id);
|
||||
if (newCount == current)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
progressStore.TryUpdateObjectiveCounter(playerId, questId, objective.Id, newCount, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Advances or completes the quest when the active step's objectives are satisfied.</summary>
|
||||
public static void TryCompleteStepIfSatisfied(
|
||||
string playerId,
|
||||
string questId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
for (var iteration = 0; iteration < MaxStepCompletionIterations; iteration++)
|
||||
{
|
||||
if (!TryGetActiveProgress(playerId, questId, progressStore, out var progress) ||
|
||||
!questRegistry.TryGetDefinition(questId, out var definition))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stepIndex = progress.CurrentStepIndex;
|
||||
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
inventoryStore.TryGetSnapshot(playerId, out var inventory);
|
||||
var step = definition.Steps[stepIndex];
|
||||
if (!IsStepSatisfied(step, progress.ObjectiveCounters, inventory, itemRegistry))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepIndex >= definition.Steps.Count - 1)
|
||||
{
|
||||
QuestStateOperations.TryMarkComplete(
|
||||
playerId,
|
||||
questId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
timeProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
var advance = QuestStateOperations.TryAdvanceStep(
|
||||
playerId,
|
||||
questId,
|
||||
stepIndex + 1,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
|
||||
if (!advance.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryCompleteAllActiveQuests(
|
||||
string playerId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||
{
|
||||
if (TryGetActiveProgress(playerId, definition.Id, progressStore, out _))
|
||||
{
|
||||
TryCompleteStepIfSatisfied(
|
||||
playerId,
|
||||
definition.Id,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyGatherItemObjectives(
|
||||
string playerId,
|
||||
QuestDefRow definition,
|
||||
QuestStepState progress,
|
||||
string itemId,
|
||||
int quantityGranted,
|
||||
IPlayerQuestStateStore progressStore)
|
||||
{
|
||||
var stepIndex = progress.CurrentStepIndex;
|
||||
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var objective in definition.Steps[stepIndex].Objectives)
|
||||
{
|
||||
if (!string.Equals(objective.Kind, QuestObjectiveKinds.GatherItem, StringComparison.Ordinal) ||
|
||||
objective.ItemId is null ||
|
||||
!string.Equals(objective.ItemId, itemId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var required = GetRequiredQuantity(objective);
|
||||
var current = GetCounter(progress, objective.Id);
|
||||
var newCount = Math.Min(current + quantityGranted, required);
|
||||
if (newCount == current)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, newCount, out _);
|
||||
progressStore.TryGetProgress(playerId, definition.Id, out progress);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyCraftRecipeObjectives(
|
||||
string playerId,
|
||||
QuestDefRow definition,
|
||||
QuestStepState progress,
|
||||
string recipeId,
|
||||
int batchQuantity,
|
||||
IPlayerQuestStateStore progressStore)
|
||||
{
|
||||
var stepIndex = progress.CurrentStepIndex;
|
||||
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var objective in definition.Steps[stepIndex].Objectives)
|
||||
{
|
||||
if (!string.Equals(objective.Kind, QuestObjectiveKinds.CraftRecipe, StringComparison.Ordinal) ||
|
||||
objective.RecipeId is null ||
|
||||
!string.Equals(objective.RecipeId, recipeId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var required = GetRequiredQuantity(objective);
|
||||
var current = GetCounter(progress, objective.Id);
|
||||
var newCount = Math.Min(current + batchQuantity, required);
|
||||
if (newCount == current)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, newCount, out _);
|
||||
progressStore.TryGetProgress(playerId, definition.Id, out progress);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyEncounterCompleteObjectives(
|
||||
string playerId,
|
||||
QuestDefRow definition,
|
||||
QuestStepState progress,
|
||||
string encounterId,
|
||||
IPlayerQuestStateStore progressStore)
|
||||
{
|
||||
var stepIndex = progress.CurrentStepIndex;
|
||||
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var objective in definition.Steps[stepIndex].Objectives)
|
||||
{
|
||||
if (!string.Equals(objective.Kind, QuestObjectiveKinds.EncounterComplete, StringComparison.Ordinal) ||
|
||||
objective.EncounterId is null ||
|
||||
!string.Equals(objective.EncounterId, encounterId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetCounter(progress, objective.Id) >= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, 1, out _);
|
||||
progressStore.TryGetProgress(playerId, definition.Id, out progress);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsStepSatisfied(
|
||||
QuestStepDefRow step,
|
||||
IReadOnlyDictionary<string, int> counters,
|
||||
PlayerInventorySnapshot inventory,
|
||||
IItemDefinitionRegistry itemRegistry)
|
||||
{
|
||||
foreach (var objective in step.Objectives)
|
||||
{
|
||||
var required = GetRequiredQuantity(objective);
|
||||
if (string.Equals(objective.Kind, QuestObjectiveKinds.InventoryHasItem, StringComparison.Ordinal))
|
||||
{
|
||||
if (objective.ItemId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var held = PlayerInventoryOperations.GetHeldQuantity(inventory, objective.ItemId, itemRegistry);
|
||||
if (held < required)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetCounter(counters, objective.Id) < required)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return step.Objectives.Count > 0;
|
||||
}
|
||||
|
||||
private static bool TryGetActiveProgress(
|
||||
string playerId,
|
||||
string questId,
|
||||
IPlayerQuestStateStore progressStore,
|
||||
out QuestStepState progress) =>
|
||||
progressStore.TryGetProgress(playerId, questId, out progress!) &&
|
||||
progress.Status == QuestProgressStatus.Active;
|
||||
|
||||
private static int GetRequiredQuantity(QuestObjectiveDefRow objective) =>
|
||||
objective.Quantity is > 0 ? objective.Quantity.Value : 1;
|
||||
|
||||
private static int GetCounter(QuestStepState progress, string objectiveId) =>
|
||||
GetCounter(progress.ObjectiveCounters, objectiveId);
|
||||
|
||||
private static int GetCounter(IReadOnlyDictionary<string, int> counters, string objectiveId) =>
|
||||
counters.TryGetValue(objectiveId, out var count) ? count : 0;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using NeonSprawl.Server.Game.Items;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Quests;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -13,7 +15,10 @@ public static class QuestStateOperations
|
|||
string playerId,
|
||||
string questId,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore)
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
|
||||
|
|
@ -47,6 +52,27 @@ public static class QuestStateOperations
|
|||
|
||||
if (progressStore.TryActivate(playerId, normalizedQuestId, out var snapshot))
|
||||
{
|
||||
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
|
||||
playerId,
|
||||
normalizedQuestId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry);
|
||||
QuestObjectiveWiring.TryCompleteStepIfSatisfied(
|
||||
playerId,
|
||||
normalizedQuestId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
|
||||
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
||||
{
|
||||
return Success(refreshed);
|
||||
}
|
||||
|
||||
return Success(snapshot);
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +87,10 @@ public static class QuestStateOperations
|
|||
string questId,
|
||||
int newStepIndex,
|
||||
IQuestDefinitionRegistry questRegistry,
|
||||
IPlayerQuestStateStore progressStore)
|
||||
IPlayerQuestStateStore progressStore,
|
||||
IPlayerInventoryStore inventoryStore,
|
||||
IItemDefinitionRegistry itemRegistry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
|
||||
|
|
@ -91,6 +120,27 @@ public static class QuestStateOperations
|
|||
|
||||
if (progressStore.TryAdvanceStep(playerId, normalizedQuestId, newStepIndex, out var snapshot))
|
||||
{
|
||||
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
|
||||
playerId,
|
||||
normalizedQuestId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry);
|
||||
QuestObjectiveWiring.TryCompleteStepIfSatisfied(
|
||||
playerId,
|
||||
normalizedQuestId,
|
||||
questRegistry,
|
||||
progressStore,
|
||||
inventoryStore,
|
||||
itemRegistry,
|
||||
timeProvider);
|
||||
|
||||
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
||||
{
|
||||
return Success(refreshed);
|
||||
}
|
||||
|
||||
return Success(snapshot);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,18 @@ Completed rows cannot regress to active without a reset API (none in prototype).
|
|||
| **`not_active`** | Advance or complete when quest was never accepted or is not **active**. |
|
||||
| **`invalid_step_index`** | Advance when index does not increase or is past the last step. |
|
||||
|
||||
Objective wiring (gather/craft/encounter hooks) is E7M1-07 ([NEO-118](https://linear.app/neon-sprawl/issue/NEO-118)). Plan: [NEO-117 implementation plan](../../docs/plans/NEO-117-implementation-plan.md).
|
||||
### Quest objective wiring (NEO-118)
|
||||
|
||||
**`QuestObjectiveWiring`** (static, best-effort) evaluates active quests on server-side gather/craft/encounter success and on **`inventory_has_item`** snapshot passes. When every objective on the current step is satisfied, wiring calls **`QuestStateOperations.TryAdvanceStep`** or **`TryMarkComplete`** (terminal step). Failures are logged at debug and never fail the primary gather/craft/encounter operation.
|
||||
|
||||
| Hook site | Objective kind | Trigger |
|
||||
|-----------|----------------|---------|
|
||||
| **`GatherOperations.TryGather`** success | **`gather_item`** | Item id + yield quantity credited to matching active quests; counter capped at objective **`quantity`**. |
|
||||
| **`CraftOperations.TryCraft`** success | **`craft_recipe`** | Recipe id + batch quantity. |
|
||||
| **`EncounterCompletionOperations.TryCompleteAndGrant`** success | **`encounter_complete`** | Encounter id (counter → 1). |
|
||||
| Inventory mutations above + **`QuestStateOperations.TryAccept`** / **`TryAdvanceStep`** | **`inventory_has_item`** | Bag snapshot read; counter set to `min(held, required)`. |
|
||||
|
||||
**`inventory_has_item`** is **not** re-evaluated for other objective kinds on accept/step entry (event-driven only for gather/craft/encounter). HTTP **`GET …/quest-progress`** is E7M1-08 ([NEO-119](https://linear.app/neon-sprawl/issue/NEO-119)). Plan: [NEO-118 implementation plan](../../docs/plans/NEO-118-implementation-plan.md).
|
||||
|
||||
## Encounter definitions (NEO-103)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue