neon-sprawl/server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCompletionOperatio...

335 lines
13 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Encounters;
public sealed class EncounterCompletionOperationsTests
{
private const string PlayerId = "dev-local-1";
private const string EncounterId = "prototype_combat_pocket";
private const string MeleeNpc = "prototype_npc_melee";
private const string RangedNpc = "prototype_npc_ranged";
private const string EliteNpc = "prototype_npc_elite";
[Fact]
public async Task TryCompleteAndGrant_ShouldGrantPrototypeLootOnce_WhenAllTargetsDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatAllRequiredTargets(deps);
var timeProvider = TimeProvider.System;
// Act
var result = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
timeProvider);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
Assert.True(result.Success);
Assert.Null(result.ReasonCode);
Assert.Equal(2, result.GrantsApplied.Count);
Assert.Contains(result.GrantsApplied, g => g.ItemId == "scrap_metal_bulk" && g.Quantity == 10);
Assert.Contains(result.GrantsApplied, g => g.ItemId == "contract_handoff_token" && g.Quantity == 1);
Assert.NotNull(result.CompleteEvent);
Assert.Equal(PlayerId, result.CompleteEvent!.Value.PlayerId);
Assert.Equal(EncounterId, result.CompleteEvent.Value.EncounterId);
Assert.Equal("prototype_combat_pocket_clear", result.CompleteEvent.Value.RewardTableId);
Assert.Equal($"{PlayerId}:{EncounterId}", result.CompleteEvent.Value.IdempotencyKey);
Assert.NotNull(result.CompletedAt);
Assert.True(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
Assert.Equal(10, CountBagItem(inventory!, "scrap_metal_bulk"));
Assert.Equal(1, CountBagItem(inventory!, "contract_handoff_token"));
}
[Fact]
public async Task TryCompleteAndGrant_ShouldDenyAlreadyCompleted_WhenCalledTwice()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatAllRequiredTargets(deps);
var timeProvider = TimeProvider.System;
var first = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
timeProvider);
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterFirst);
// Act
var second = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
timeProvider);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterSecond);
Assert.True(first.Success);
Assert.False(second.Success);
Assert.Equal(EncounterCompletionReasonCodes.AlreadyCompleted, second.ReasonCode);
Assert.Empty(second.GrantsApplied);
Assert.Null(second.CompleteEvent);
Assert.Equal(10, CountBagItem(afterSecond!, "scrap_metal_bulk"));
Assert.Equal(1, CountBagItem(afterSecond!, "contract_handoff_token"));
Assert.Equal(CountBagItem(afterFirst!, "scrap_metal_bulk"), CountBagItem(afterSecond!, "scrap_metal_bulk"));
Assert.Equal(CountBagItem(afterFirst!, "contract_handoff_token"), CountBagItem(afterSecond!, "contract_handoff_token"));
}
[Fact]
public async Task TryCompleteAndGrant_ShouldDenyNotReady_WhenOnlyTwoTargetsDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
_ = EncounterProgressOperations.TryActivateOnFirstEngagement(
PlayerId,
MeleeNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
_ = EncounterProgressOperations.TryMarkTargetDefeated(
PlayerId,
MeleeNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
_ = EncounterProgressOperations.TryMarkTargetDefeated(
PlayerId,
RangedNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
// Act
var result = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
TimeProvider.System);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
Assert.False(result.Success);
Assert.Equal(EncounterCompletionReasonCodes.NotReady, result.ReasonCode);
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
}
[Fact]
public async Task TryCompleteAndGrant_ShouldDenyInventoryFull_WhenBagHasNoFreeSlots()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatAllRequiredTargets(deps);
FillBag(deps);
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
// Act
var result = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
TimeProvider.System);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
Assert.False(result.Success);
Assert.Equal(EncounterCompletionReasonCodes.InventoryFull, result.ReasonCode);
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
Assert.Equal(0, CountBagItem(afterInventory!, "scrap_metal_bulk"));
Assert.Equal(0, CountBagItem(afterInventory!, "contract_handoff_token"));
}
[Fact]
public async Task TryCompleteAndGrant_ShouldRollbackGrants_WhenCompletionMarkFails()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatAllRequiredTargets(deps);
var failingCompletionStore = new CompletionStoreDeniesMark(deps.CompletionStore);
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
// Act
var result = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
failingCompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
TimeProvider.System);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
Assert.False(result.Success);
Assert.Equal(EncounterCompletionReasonCodes.AlreadyCompleted, result.ReasonCode);
Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
Assert.Equal(0, CountBagItem(afterInventory!, "scrap_metal_bulk"));
Assert.Equal(0, CountBagItem(afterInventory!, "contract_handoff_token"));
}
[Fact]
public async Task TryCompleteAndGrant_ShouldDenyUnknownEncounter_WhenIdIsInvalid()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
var result = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
"unknown_encounter",
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.ItemRegistry,
deps.InventoryStore,
TimeProvider.System);
// Assert
Assert.False(result.Success);
Assert.Equal(EncounterCompletionReasonCodes.UnknownEncounter, result.ReasonCode);
}
private static void DefeatAllRequiredTargets(EncounterCompletionTestDependencies deps)
{
_ = EncounterProgressOperations.TryActivateOnFirstEngagement(
PlayerId,
MeleeNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
_ = EncounterProgressOperations.TryMarkTargetDefeated(
PlayerId,
MeleeNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
_ = EncounterProgressOperations.TryMarkTargetDefeated(
PlayerId,
RangedNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
_ = EncounterProgressOperations.TryMarkTargetDefeated(
PlayerId,
EliteNpc,
deps.EncounterRegistry,
deps.ProgressStore,
deps.CompletionStore);
}
private static void FillBag(EncounterCompletionTestDependencies deps)
{
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);
}
}
private static EncounterCompletionTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
{
var services = factory.Services;
return new EncounterCompletionTestDependencies(
services.GetRequiredService<IEncounterDefinitionRegistry>(),
services.GetRequiredService<IRewardTableDefinitionRegistry>(),
services.GetRequiredService<IEncounterProgressStore>(),
services.GetRequiredService<IEncounterCompletionStore>(),
services.GetRequiredService<IItemDefinitionRegistry>(),
services.GetRequiredService<IPlayerInventoryStore>());
}
private static int CountBagItem(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)
{
if (!slot.IsEmpty)
{
total += slot.Quantity;
}
}
return total;
}
private sealed record EncounterCompletionTestDependencies(
IEncounterDefinitionRegistry EncounterRegistry,
IRewardTableDefinitionRegistry RewardTableRegistry,
IEncounterProgressStore ProgressStore,
IEncounterCompletionStore CompletionStore,
IItemDefinitionRegistry ItemRegistry,
IPlayerInventoryStore InventoryStore);
private sealed class CompletionStoreDeniesMark(IEncounterCompletionStore inner) : IEncounterCompletionStore
{
public bool IsCompleted(string playerId, string encounterId) =>
inner.IsCompleted(playerId, encounterId);
public bool TryMarkCompleted(string playerId, string encounterId, DateTimeOffset completedAt) => false;
public bool TryGetCompletedAt(string playerId, string encounterId, out DateTimeOffset completedAt) =>
inner.TryGetCompletedAt(playerId, encounterId, out completedAt);
}
}