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

469 lines
16 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Game.Rewards;
using NeonSprawl.Server.Game.Skills;
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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,
deps.SkillRegistry,
deps.SkillProgressionStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.PerkStore,
deps.DeliveryStore,
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>(),
services.GetRequiredService<ISkillDefinitionRegistry>(),
services.GetRequiredService<IPlayerSkillProgressionStore>(),
services.GetRequiredService<ISkillLevelCurve>(),
services.GetRequiredService<PerkUnlockEngine>(),
services.GetRequiredService<IPlayerPerkStateStore>(),
services.GetRequiredService<IRewardDeliveryStore>());
}
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,
ISkillDefinitionRegistry SkillRegistry,
IPlayerSkillProgressionStore SkillProgressionStore,
ISkillLevelCurve LevelCurve,
PerkUnlockEngine PerkUnlockEngine,
IPlayerPerkStateStore PerkStore,
IRewardDeliveryStore DeliveryStore);
}