638 lines
24 KiB
C#
638 lines
24 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
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;
|
|
using NeonSprawl.Server.Game.Skills;
|
|
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_ShouldApplyContractBundle_WhenActiveContractMatchesEncounter()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var issued = ContractGeneratorOperations.TryIssue(
|
|
PlayerId,
|
|
"prototype_contract_clear_combat_pocket",
|
|
"2026-06-22",
|
|
zoneDifficultyBand: null,
|
|
deps.ContractTemplateRegistry,
|
|
deps.ContractInstanceStore,
|
|
deps.StandingStore,
|
|
TimeProvider.System);
|
|
Assert.True(issued.Success);
|
|
var instanceId = issued.Snapshot!.ContractInstanceId;
|
|
DefeatAllRequiredTargets(deps);
|
|
|
|
// Act
|
|
var result = EncounterCompletionOperations.TryCompleteAndGrant(
|
|
PlayerId,
|
|
EncounterId,
|
|
deps.EncounterRegistry,
|
|
deps.RewardTableRegistry,
|
|
deps.ProgressStore,
|
|
deps.CompletionStore,
|
|
deps.EventStore,
|
|
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(result.Success);
|
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
|
Assert.Equal(15, CountBagItem(inventory!, "scrap_metal_bulk"));
|
|
Assert.Equal(1, CountBagItem(inventory!, "contract_handoff_token"));
|
|
Assert.True(deps.ContractInstanceStore.TryGet(PlayerId, instanceId, out var instance));
|
|
Assert.Equal(ContractInstanceStatus.Completed, instance.Status);
|
|
Assert.True(
|
|
deps.DeliveryStore.TryGet(
|
|
PlayerId,
|
|
RewardDeliverySourceKinds.ContractCompletion,
|
|
instanceId,
|
|
out _));
|
|
Assert.Single(deps.ContractOutcomeStore.GetOutcomesForInstance(instanceId));
|
|
}
|
|
|
|
[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.EventStore,
|
|
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);
|
|
|
|
// 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.True(deps.EventStore.TryGet(PlayerId, EncounterId, out var storedEvent));
|
|
Assert.Equal(result.CompleteEvent!.Value.IdempotencyKey, storedEvent.IdempotencyKey);
|
|
Assert.Equal(2, storedEvent.GrantedItems.Count);
|
|
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.EventStore,
|
|
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);
|
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterFirst);
|
|
|
|
// Act
|
|
var second = EncounterCompletionOperations.TryCompleteAndGrant(
|
|
PlayerId,
|
|
EncounterId,
|
|
deps.EncounterRegistry,
|
|
deps.RewardTableRegistry,
|
|
deps.ProgressStore,
|
|
deps.CompletionStore,
|
|
deps.EventStore,
|
|
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);
|
|
|
|
// Assert
|
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterSecond);
|
|
Assert.True(first.Success);
|
|
Assert.Null(first.ReasonCode);
|
|
Assert.Equal(2, first.GrantsApplied.Count);
|
|
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"));
|
|
Assert.True(deps.EventStore.TryGet(PlayerId, EncounterId, out var storedEvent));
|
|
Assert.Equal(first.CompleteEvent!.Value.IdempotencyKey, storedEvent.IdempotencyKey);
|
|
}
|
|
|
|
[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.EventStore,
|
|
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 afterInventory);
|
|
Assert.False(result.Success);
|
|
Assert.Equal(EncounterCompletionReasonCodes.NotReady, result.ReasonCode);
|
|
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
|
|
Assert.False(deps.EventStore.TryGet(PlayerId, EncounterId, out _));
|
|
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.EventStore,
|
|
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 afterInventory);
|
|
Assert.False(result.Success);
|
|
Assert.Equal(EncounterCompletionReasonCodes.InventoryFull, result.ReasonCode);
|
|
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
|
|
Assert.False(deps.EventStore.TryGet(PlayerId, EncounterId, out _));
|
|
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.EventStore,
|
|
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 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"));
|
|
Assert.False(deps.EventStore.TryGet(PlayerId, EncounterId, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryCompleteAndGrant_ShouldRollbackFirstGrant_WhenSecondApplyFails()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
DefeatAllRequiredTargets(deps);
|
|
var inventoryStore = new InventoryStoreFullAfterFirstGrant(deps.InventoryStore, deps.ItemRegistry);
|
|
|
|
// Act
|
|
var result = EncounterCompletionOperations.TryCompleteAndGrant(
|
|
PlayerId,
|
|
EncounterId,
|
|
deps.EncounterRegistry,
|
|
deps.RewardTableRegistry,
|
|
deps.ProgressStore,
|
|
deps.CompletionStore,
|
|
deps.EventStore,
|
|
deps.ItemRegistry,
|
|
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.False(result.Success);
|
|
Assert.Equal(EncounterCompletionReasonCodes.InventoryFull, result.ReasonCode);
|
|
Assert.False(deps.CompletionStore.IsCompleted(PlayerId, EncounterId));
|
|
Assert.Equal(0, CountBagItem(inventory!, "scrap_metal_bulk"));
|
|
Assert.Equal(0, CountBagItem(inventory!, "contract_handoff_token"));
|
|
Assert.False(deps.EventStore.TryGet(PlayerId, EncounterId, out _));
|
|
}
|
|
|
|
[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.EventStore,
|
|
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(result.Success);
|
|
Assert.Equal(EncounterCompletionReasonCodes.UnknownEncounter, result.ReasonCode);
|
|
Assert.False(deps.EventStore.TryGet(PlayerId, "unknown_encounter", out _));
|
|
}
|
|
|
|
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<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>(),
|
|
services.GetRequiredService<IFactionStandingStore>(),
|
|
services.GetRequiredService<IReputationDeltaStore>(),
|
|
services.GetRequiredService<IContractInstanceStore>(),
|
|
services.GetRequiredService<IContractOutcomeStore>(),
|
|
services.GetRequiredService<IContractTemplateRegistry>());
|
|
}
|
|
|
|
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,
|
|
IEncounterCompleteEventStore EventStore,
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// After the first successful inventory mutation, fills remaining bag slots so the next apply fails
|
|
/// while pre-flight simulate (on an empty snapshot) still passes.
|
|
/// </summary>
|
|
private sealed class InventoryStoreFullAfterFirstGrant(
|
|
IPlayerInventoryStore inner,
|
|
IItemDefinitionRegistry itemRegistry) : IPlayerInventoryStore
|
|
{
|
|
private bool _firstMutationApplied;
|
|
|
|
public bool TryGetSnapshot(string playerId, out PlayerInventorySnapshot snapshot) =>
|
|
inner.TryGetSnapshot(playerId, out snapshot);
|
|
|
|
public bool TryReplaceSnapshot(string playerId, PlayerInventorySnapshot snapshot) =>
|
|
inner.TryReplaceSnapshot(playerId, snapshot);
|
|
|
|
public bool TryMutateSnapshot(
|
|
string playerId,
|
|
Func<PlayerInventorySnapshot, PlayerInventoryMutationWrite> mutator,
|
|
out PlayerInventorySnapshot result)
|
|
{
|
|
if (!inner.TryMutateSnapshot(playerId, mutator, out result))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_firstMutationApplied)
|
|
{
|
|
_firstMutationApplied = true;
|
|
FillRemainingBagSlots(playerId);
|
|
inner.TryGetSnapshot(playerId, out result);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void FillRemainingBagSlots(string playerId)
|
|
{
|
|
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
|
|
{
|
|
var add = PlayerInventoryOperations.TryAddStack(
|
|
playerId,
|
|
"survey_drone_kit",
|
|
quantity: 1,
|
|
itemRegistry,
|
|
inner);
|
|
if (add.Kind != PlayerInventoryMutationKind.Applied)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|