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; 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, deps.StandingStore, deps.AuditStore, 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, deps.StandingStore, deps.AuditStore, 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, deps.StandingStore, deps.AuditStore, 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, deps.StandingStore, deps.AuditStore, 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, deps.StandingStore, deps.AuditStore, 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, deps.StandingStore, deps.AuditStore, 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, deps.StandingStore, deps.AuditStore, 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(), 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, IFactionStandingStore StandingStore, IReputationDeltaStore AuditStore); 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; } }