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.Rewards; using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Gathering; public sealed class GatherOperationsTests { private const string PlayerId = "dev-local-1"; private const string AlphaId = "prototype_resource_node_alpha"; private const string DeltaId = "prototype_urban_bulk_delta"; [Fact] public void TryGather_OnFreshAlphaNode_ShouldGrantItemXpAndDecrementCapacity() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveGatherDependencies(factory); // Act var result = GatherOperations.TryGather( PlayerId, AlphaId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory); var salvageXp = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"); // Assert Assert.True(result.Success); Assert.Null(result.ReasonCode); Assert.Single(result.GrantsApplied); Assert.Equal("scrap_metal_bulk", result.GrantsApplied[0].ItemId); Assert.Equal(1, result.GrantsApplied[0].Quantity); Assert.Equal(9, result.RemainingGathers); Assert.NotNull(result.XpGrantSummary); Assert.Equal("salvage", result.XpGrantSummary!.Value.SkillId); Assert.Equal(GatherSkillXpConstants.ActivityXpPerResourceNodeGather, result.XpGrantSummary.Value.Amount); Assert.Equal(GatherSkillXpConstants.ActivitySourceKind, result.XpGrantSummary.Value.SourceKind); Assert.Equal(1, CountItem(inventory!, "scrap_metal_bulk")); Assert.Equal(10, salvageXp); } [Fact] public void TryGather_WhenBagFull_ShouldDenyWithInventoryFullWithoutDepletingNode() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveGatherDependencies(factory); for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++) { var add = PlayerInventoryOperations.TryAddStack( PlayerId, "survey_drone_kit", quantity: 1, deps.ItemRegistry, deps.InventoryStore); Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind); } deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory); var salvageXpBefore = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"); // Act var result = GatherOperations.TryGather( PlayerId, AlphaId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory); var salvageXpAfter = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"); deps.InstanceStore.TryGetRemainingGathers(AlphaId, out var nodeSnapshot); // Assert Assert.False(result.Success); Assert.Equal(GatherReasonCodes.InventoryFull, result.ReasonCode); Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!)); Assert.Equal(salvageXpBefore, salvageXpAfter); Assert.True(deps.InstanceStore.TryGetRemainingGathers(AlphaId, out nodeSnapshot)); Assert.Equal(10, nodeSnapshot.RemainingGathers); } [Fact] public void TryGather_AfterCapacityExhausted_ShouldReturnNodeDepletedWithoutSideEffects() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveGatherDependencies(factory); for (var i = 0; i < 10; i++) { var gather = GatherOperations.TryGather( PlayerId, AlphaId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); Assert.True(gather.Success); } deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory); var salvageXpBefore = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"); // Act var result = GatherOperations.TryGather( PlayerId, AlphaId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory); var salvageXpAfter = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"); // Assert Assert.False(result.Success); Assert.Equal(GatherReasonCodes.NodeDepleted, result.ReasonCode); Assert.Equal(0, result.RemainingGathers); Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!)); Assert.Equal(salvageXpBefore, salvageXpAfter); } [Fact] public void TryGather_ForUnknownNode_ShouldDenyWithoutCreatingInstanceRow() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveGatherDependencies(factory); const string unknownId = "not_a_real_node_def"; // Act var result = GatherOperations.TryGather( PlayerId, unknownId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); var rowExists = deps.InstanceStore.TryGetRemainingGathers(unknownId, out _); // Assert Assert.False(result.Success); Assert.Equal(GatherReasonCodes.UnknownNode, result.ReasonCode); Assert.False(rowExists); } [Fact] public void TryGather_WhenProgressionStoreMissing_ShouldRollbackInventoryAndDeny() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveGatherDependencies(factory); var failingXpStore = new ProgressionStoreAlwaysMissing(); deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory); // Act var result = GatherOperations.TryGather( PlayerId, AlphaId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, failingXpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory); deps.InstanceStore.TryGetRemainingGathers(AlphaId, out var nodeSnapshot); // Assert Assert.False(result.Success); Assert.Equal(GatherReasonCodes.ProgressionStoreMissing, result.ReasonCode); Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!)); Assert.True(deps.InstanceStore.TryGetRemainingGathers(AlphaId, out nodeSnapshot)); Assert.Equal(10, nodeSnapshot.RemainingGathers); } [Fact] public void TryGather_OnDeltaNode_ShouldGrantConfiguredYieldQuantity() { // Arrange using var factory = new InMemoryWebApplicationFactory(); var deps = ResolveGatherDependencies(factory); // Act var result = GatherOperations.TryGather( PlayerId, DeltaId, deps.NodeRegistry, deps.ItemRegistry, deps.InventoryStore, deps.SkillRegistry, deps.XpStore, deps.LevelCurve, deps.PerkUnlockEngine, deps.InstanceStore, deps.QuestRegistry, deps.ProgressStore, deps.PerkStore, deps.DeliveryStore, TimeProvider.System); deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory); // Assert Assert.True(result.Success); Assert.Single(result.GrantsApplied); Assert.Equal(5, result.GrantsApplied[0].Quantity); Assert.Equal(5, CountItem(inventory!, "scrap_metal_bulk")); } private static GatherTestDependencies ResolveGatherDependencies(InMemoryWebApplicationFactory factory) { var services = factory.Services; return new GatherTestDependencies( services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService()); } private static int CountItem(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 int TotalBagQuantity(PlayerInventorySnapshot snapshot) { var total = 0; foreach (var slot in snapshot.BagSlots) { total += slot.Quantity; } return total; } private readonly record struct GatherTestDependencies( IResourceNodeDefinitionRegistry NodeRegistry, IItemDefinitionRegistry ItemRegistry, IPlayerInventoryStore InventoryStore, ISkillDefinitionRegistry SkillRegistry, IPlayerSkillProgressionStore XpStore, ISkillLevelCurve LevelCurve, PerkUnlockEngine PerkUnlockEngine, IResourceNodeInstanceStore InstanceStore, IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore, IPlayerPerkStateStore PerkStore, IRewardDeliveryStore DeliveryStore); private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore { public IReadOnlyDictionary GetXpTotals(string playerId) => new Dictionary(StringComparer.Ordinal); public bool TryApplyXpDelta( string playerId, string skillId, int amount, out int previousXp, out int newXp) { previousXp = 0; newXp = 0; return false; } public bool TrySetSkillXpTotal(string playerId, string skillId, int xp) => false; public bool TrySetSkillXpTotals(string playerId, IReadOnlyDictionary skillXpTotals) => false; } }