312 lines
11 KiB
C#
312 lines
11 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using NeonSprawl.Server.Game.Gathering;
|
|
using NeonSprawl.Server.Game.Items;
|
|
using NeonSprawl.Server.Game.Mastery;
|
|
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.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.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);
|
|
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.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);
|
|
|
|
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.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.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>());
|
|
}
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
}
|