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(), 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, TimeProvider TimeProvider); }