neon-sprawl/server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCombatWiringTests.cs

394 lines
14 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Encounters;
public sealed class EncounterCombatWiringTests
{
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 TryProcessCastOutcome_ShouldActivateEncounter_WhenDamagingHitWithoutDefeat()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
MeleeNpc,
damageDealt: 25,
targetDefeated: false,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, EncounterId, out var progress));
Assert.True(progress.Started);
Assert.Empty(progress.DefeatedNpcInstanceIds);
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
}
[Fact]
public async Task TryProcessCastOutcome_ShouldMarkDefeatWithoutCompletion_WhenOnlyOneTargetDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
MeleeNpc,
damageDealt: 25,
targetDefeated: true,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, EncounterId, out var progress));
Assert.Contains(MeleeNpc, progress.DefeatedNpcInstanceIds);
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
Assert.Equal(0, CountBagItem(inventory!, "scrap_metal_bulk"));
Assert.Equal(0, CountBagItem(inventory!, "contract_handoff_token"));
}
[Fact]
public async Task TryProcessCastOutcome_ShouldGrantLootOnce_WhenThirdTargetDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatNpc(deps, MeleeNpc);
DefeatNpc(deps, RangedNpc);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
EliteNpc,
damageDealt: 25,
targetDefeated: true,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
Assert.True(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
Assert.True(deps.CompleteEventStore.TryGet(PlayerId, EncounterId, out var storedEvent));
Assert.Equal($"{PlayerId}:{EncounterId}", storedEvent.IdempotencyKey);
Assert.Equal(10, CountBagItem(inventory!, "scrap_metal_bulk"));
Assert.Equal(1, CountBagItem(inventory!, "contract_handoff_token"));
}
[Fact]
public async Task TryProcessCastOutcome_ShouldNoOp_WhenNpcNotInAnyEncounter()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
"unknown_npc_instance",
damageDealt: 25,
targetDefeated: true,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
Assert.False(deps.ProgressStore.TryGetProgress(PlayerId, EncounterId, out _));
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
}
[Fact]
public async Task TryProcessCastOutcome_ShouldSkipActivateAndMark_WhenEncounterAlreadyCompleted()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatAllRequiredTargets(deps);
_ = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
deps.InventoryStore.TryGetSnapshot(PlayerId, out var before);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
MeleeNpc,
damageDealt: 25,
targetDefeated: true,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
deps.InventoryStore.TryGetSnapshot(PlayerId, out var after);
Assert.Equal(CountBagItem(before!, "scrap_metal_bulk"), CountBagItem(after!, "scrap_metal_bulk"));
Assert.Equal(CountBagItem(before!, "contract_handoff_token"), CountBagItem(after!, "contract_handoff_token"));
}
[Fact]
public async Task TryProcessCastOutcome_ShouldNotComplete_WhenOnlyTwoTargetsDefeatedAndThirdHitNonLethal()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatNpc(deps, MeleeNpc);
DefeatNpc(deps, RangedNpc);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
EliteNpc,
damageDealt: 25,
targetDefeated: false,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
Assert.False(
EncounterProgressOperations.IsAllRequiredTargetsDefeated(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.ProgressStore));
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
Assert.Equal(0, CountBagItem(inventory!, "scrap_metal_bulk"));
Assert.Equal(0, CountBagItem(inventory!, "contract_handoff_token"));
}
[Fact]
public async Task TryProcessCastOutcome_ShouldRetainProgressWithoutCompletion_WhenBagFullOnThirdDefeat()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
DefeatNpc(deps, MeleeNpc);
DefeatNpc(deps, RangedNpc);
FillBag(deps);
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
EliteNpc,
damageDealt: 25,
targetDefeated: true,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
Assert.True(
EncounterProgressOperations.IsAllRequiredTargetsDefeated(
PlayerId,
EncounterId,
deps.EncounterRegistry,
deps.ProgressStore));
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
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 TryProcessCastOutcome_ShouldNotActivate_WhenZeroDamageWithoutDefeat()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
MeleeNpc,
damageDealt: 0,
targetDefeated: false,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
// Assert
Assert.False(deps.ProgressStore.TryGetProgress(PlayerId, EncounterId, out var progress) && progress.Started);
}
private static void DefeatNpc(EncounterCombatWiringTestDependencies deps, string npcId)
{
EncounterCombatWiring.TryProcessCastOutcome(
PlayerId,
npcId,
damageDealt: 25,
targetDefeated: true,
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.ProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.QuestProgressStore,
TimeProvider.System);
}
private static void DefeatAllRequiredTargets(EncounterCombatWiringTestDependencies deps)
{
DefeatNpc(deps, MeleeNpc);
DefeatNpc(deps, RangedNpc);
DefeatNpc(deps, EliteNpc);
}
private static void FillBag(EncounterCombatWiringTestDependencies 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 int TotalBagQuantity(PlayerInventorySnapshot snapshot)
{
var total = 0;
foreach (var slot in snapshot.BagSlots)
{
if (!slot.IsEmpty)
{
total += slot.Quantity;
}
}
return total;
}
private static EncounterCombatWiringTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
{
var services = factory.Services;
return new EncounterCombatWiringTestDependencies(
services.GetRequiredService<IEncounterDefinitionRegistry>(),
services.GetRequiredService<IRewardTableDefinitionRegistry>(),
services.GetRequiredService<IEncounterProgressStore>(),
services.GetRequiredService<IEncounterCompletionStore>(),
services.GetRequiredService<IEncounterCompleteEventStore>(),
services.GetRequiredService<IItemDefinitionRegistry>(),
services.GetRequiredService<IPlayerInventoryStore>(),
services.GetRequiredService<IQuestDefinitionRegistry>(),
services.GetRequiredService<IPlayerQuestStateStore>());
}
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 sealed record EncounterCombatWiringTestDependencies(
IEncounterDefinitionRegistry EncounterRegistry,
IRewardTableDefinitionRegistry RewardTableRegistry,
IEncounterProgressStore ProgressStore,
IEncounterCompletionStore CompletionStore,
IEncounterCompleteEventStore CompleteEventStore,
IItemDefinitionRegistry ItemRegistry,
IPlayerInventoryStore InventoryStore,
IQuestDefinitionRegistry QuestRegistry,
IPlayerQuestStateStore QuestProgressStore);
}