using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.Factions; 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; 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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, deps.StandingStore, deps.AuditStore, deps.ContractInstanceStore, deps.ContractOutcomeStore, deps.ContractTemplateRegistry, 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(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService()); } 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, IFactionStandingStore StandingStore, IReputationDeltaStore AuditStore, IContractInstanceStore ContractInstanceStore, IContractOutcomeStore ContractOutcomeStore, IContractTemplateRegistry ContractTemplateRegistry); }