neon-sprawl/server/NeonSprawl.Server.Tests/Game/Gathering/GatherOperationsTests.cs

376 lines
13 KiB
C#

using Microsoft.Extensions.DependencyInjection;
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.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<IResourceNodeDefinitionRegistry>(),
services.GetRequiredService<IItemDefinitionRegistry>(),
services.GetRequiredService<IPlayerInventoryStore>(),
services.GetRequiredService<ISkillDefinitionRegistry>(),
services.GetRequiredService<IPlayerSkillProgressionStore>(),
services.GetRequiredService<ISkillLevelCurve>(),
services.GetRequiredService<PerkUnlockEngine>(),
services.GetRequiredService<IResourceNodeInstanceStore>(),
services.GetRequiredService<IQuestDefinitionRegistry>(),
services.GetRequiredService<IPlayerQuestStateStore>(),
services.GetRequiredService<IPlayerPerkStateStore>(),
services.GetRequiredService<IRewardDeliveryStore>(),
services.GetRequiredService<IFactionStandingStore>(),
services.GetRequiredService<IReputationDeltaStore>());
}
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<string, int> GetXpTotals(string playerId) =>
new Dictionary<string, int>(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<string, int> skillXpTotals) => false;
}
}