using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Crafting; using NeonSprawl.Server.Game.Encounters; using NeonSprawl.Server.Game.Factions; 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; 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); 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] 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.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.PerkStore, deps.DeliveryStore, 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); 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] 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(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 _)); // 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.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.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.PerkStore, deps.DeliveryStore, 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.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.PerkStore, deps.DeliveryStore, deps.StandingStore, 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(TryMarkComplete(deps, questId).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.PerkStore, deps.DeliveryStore, 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.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( 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(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), 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, IPlayerPerkStateStore PerkStore, IRewardDeliveryStore DeliveryStore, IFactionStandingStore StandingStore, TimeProvider TimeProvider); }