405 lines
14 KiB
C#
405 lines
14 KiB
C#
using NeonSprawl.Server.Game.Items;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Items;
|
|
|
|
public sealed class PlayerInventoryOperationsTests
|
|
{
|
|
[Fact]
|
|
public void TryAddStack_ToEmptyBag_ShouldPlaceInFirstSlot()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"scrap_metal_bulk",
|
|
quantity: 10,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
|
|
Assert.NotNull(outcome.Snapshot);
|
|
Assert.Equal("scrap_metal_bulk", outcome.Snapshot!.BagSlots[0].ItemId);
|
|
Assert.Equal(10, outcome.Snapshot.BagSlots[0].Quantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_ShouldMergeIntoPartialStackRespectingStackMax()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
_ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 15, registry, store);
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 5, registry, store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
|
|
Assert.Equal(20, CountItem(outcome.Snapshot!, "field_stim_mk0"));
|
|
Assert.Equal(1, OccupiedBagSlotCount(outcome.Snapshot!));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_WhenBagFull_ShouldDenyWithInventoryFull()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
|
|
{
|
|
var add = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"survey_drone_kit",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind);
|
|
}
|
|
|
|
store.TryGetSnapshot("dev-local-1", out var before);
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"scrap_metal_bulk",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, outcome.ReasonCode);
|
|
store.TryGetSnapshot("dev-local-1", out var after);
|
|
Assert.Equal(TotalBagQuantity(before!), TotalBagQuantity(after!));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_ToEquipment_ShouldUseEquipmentContainer()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"prototype_armor_shell",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
|
|
Assert.Equal("prototype_armor_shell", outcome.Snapshot!.EquipmentSlots[0].ItemId);
|
|
Assert.Equal(1, outcome.Snapshot.EquipmentSlots[0].Quantity);
|
|
Assert.All(outcome.Snapshot.BagSlots, static s => Assert.True(s.IsEmpty));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_WhenEquipmentOccupied_ShouldDenyWithInventoryFull()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
_ = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"prototype_armor_shell",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"prototype_armor_shell",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_ForUnknownItem_ShouldDenyWithInvalidItem()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"nonexistent_item_id",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InvalidItem, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_ForNonPositiveQuantity_ShouldDenyWithInvalidQuantity()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"scrap_metal_bulk",
|
|
quantity: 0,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryRemoveStack_ShouldDrainLowestSlotIndexFirst()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
_ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 20, registry, store);
|
|
_ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 5, registry, store);
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryRemoveStack(
|
|
"dev-local-1",
|
|
"field_stim_mk0",
|
|
quantity: 20,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
|
|
Assert.True(outcome.Snapshot!.BagSlots[0].IsEmpty);
|
|
Assert.Equal("field_stim_mk0", outcome.Snapshot.BagSlots[1].ItemId);
|
|
Assert.Equal(5, outcome.Snapshot.BagSlots[1].Quantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryRemoveStack_WhenInsufficientQuantity_ShouldDenyWithoutMutation()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
_ = PlayerInventoryOperations.TryAddStack("dev-local-1", "scrap_metal_bulk", 5, registry, store);
|
|
store.TryGetSnapshot("dev-local-1", out var before);
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryRemoveStack(
|
|
"dev-local-1",
|
|
"scrap_metal_bulk",
|
|
quantity: 6,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InsufficientQuantity, outcome.ReasonCode);
|
|
store.TryGetSnapshot("dev-local-1", out var after);
|
|
Assert.Equal(TotalBagQuantity(before!), TotalBagQuantity(after!));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_WhenQuantityExceedsStackMax_ShouldSpanMultipleEmptySlots()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"field_stim_mk0",
|
|
quantity: 41,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
|
|
Assert.Equal(41, CountItem(outcome.Snapshot!, "field_stim_mk0"));
|
|
Assert.Equal(3, OccupiedBagSlotCount(outcome.Snapshot!));
|
|
Assert.Equal(20, outcome.Snapshot!.BagSlots[0].Quantity);
|
|
Assert.Equal(20, outcome.Snapshot.BagSlots[1].Quantity);
|
|
Assert.Equal(1, outcome.Snapshot.BagSlots[2].Quantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_ForUnknownPlayer_ShouldReturnStoreMissing()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"unknown-player-xyz",
|
|
"scrap_metal_bulk",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.StoreMissing, outcome.Kind);
|
|
Assert.Null(outcome.ReasonCode);
|
|
Assert.Null(outcome.Snapshot);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryAddStack_ForUnknownPlayer_WithInvalidQuantity_ShouldDenyWithReasonCode()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
|
|
// Act
|
|
var outcome = PlayerInventoryOperations.TryAddStack(
|
|
"unknown-player-xyz",
|
|
"scrap_metal_bulk",
|
|
quantity: 0,
|
|
registry,
|
|
store);
|
|
|
|
// Assert
|
|
Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
|
|
Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, outcome.ReasonCode);
|
|
Assert.Null(outcome.Snapshot);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrySimulateAddStack_WhenBagFullForBagItem_ShouldReturnFalse()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
|
|
{
|
|
_ = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"survey_drone_kit",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
}
|
|
|
|
store.TryGetSnapshot("dev-local-1", out var snapshot);
|
|
var simulated = new PlayerInventorySnapshot
|
|
{
|
|
BagSlots = PlayerInventorySnapshot.CloneSlots(snapshot!.BagSlots),
|
|
EquipmentSlots = PlayerInventorySnapshot.CloneSlots(snapshot.EquipmentSlots),
|
|
};
|
|
|
|
// Act
|
|
var canPlace = PlayerInventoryOperations.TrySimulateAddStack(
|
|
ref simulated,
|
|
"refined_plate_stock",
|
|
quantity: 1,
|
|
registry);
|
|
|
|
// Assert
|
|
Assert.False(canPlace);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrySimulateAddStack_WhenBagFullButEquipmentRouteAvailable_ShouldReturnTrue()
|
|
{
|
|
// Arrange
|
|
using var factory = new InMemoryWebApplicationFactory();
|
|
var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
|
|
var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
|
|
{
|
|
_ = PlayerInventoryOperations.TryAddStack(
|
|
"dev-local-1",
|
|
"survey_drone_kit",
|
|
quantity: 1,
|
|
registry,
|
|
store);
|
|
}
|
|
|
|
store.TryGetSnapshot("dev-local-1", out var snapshot);
|
|
var simulated = new PlayerInventorySnapshot
|
|
{
|
|
BagSlots = PlayerInventorySnapshot.CloneSlots(snapshot!.BagSlots),
|
|
EquipmentSlots = PlayerInventorySnapshot.CloneSlots(snapshot.EquipmentSlots),
|
|
};
|
|
|
|
// Act
|
|
var canPlace = PlayerInventoryOperations.TrySimulateAddStack(
|
|
ref simulated,
|
|
"prototype_armor_shell",
|
|
quantity: 1,
|
|
registry);
|
|
|
|
// Assert
|
|
Assert.True(canPlace);
|
|
Assert.Equal("prototype_armor_shell", simulated.EquipmentSlots[0].ItemId);
|
|
Assert.Equal(1, simulated.EquipmentSlots[0].Quantity);
|
|
}
|
|
|
|
private static int CountItem(PlayerInventorySnapshot snapshot, string itemId)
|
|
{
|
|
var total = 0;
|
|
foreach (var slot in snapshot.BagSlots)
|
|
{
|
|
if (!slot.IsEmpty && slot.ItemId == itemId)
|
|
{
|
|
total += slot.Quantity;
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
private static int OccupiedBagSlotCount(PlayerInventorySnapshot snapshot)
|
|
{
|
|
var count = 0;
|
|
foreach (var slot in snapshot.BagSlots)
|
|
{
|
|
if (!slot.IsEmpty)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static int TotalBagQuantity(PlayerInventorySnapshot snapshot)
|
|
{
|
|
var total = 0;
|
|
foreach (var slot in snapshot.BagSlots)
|
|
{
|
|
total += slot.Quantity;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
}
|