394 lines
14 KiB
C#
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);
|
|
}
|