462 lines
16 KiB
C#
462 lines
16 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Game.Quests;
|
|
using NeonSprawl.Server.Tests;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Quests;
|
|
|
|
public sealed class QuestStateOperationsTests
|
|
{
|
|
private const string PlayerId = "dev-local-1";
|
|
private const string UnknownPlayerId = "unknown-player-xyz";
|
|
private const string GatherQuestId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId;
|
|
private const string RefineQuestId = PrototypeE7M1QuestCatalogRules.RefineIntroQuestId;
|
|
private const string CombatQuestId = PrototypeE7M1QuestCatalogRules.CombatIntroQuestId;
|
|
private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId;
|
|
private const string ChainObjectiveId = PrototypeE7M1QuestCatalogRules.ChainFirstObjectiveId;
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldActivateGatherIntro_WhenNotStarted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Null(result.ReasonCode);
|
|
Assert.NotNull(result.Snapshot);
|
|
Assert.Equal(QuestProgressStatus.Active, result.Snapshot!.Status);
|
|
Assert.Equal(0, result.Snapshot.CurrentStepIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldDenyPrerequisiteIncomplete_WhenRefineIntroBeforeGatherComplete()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
PlayerId,
|
|
RefineQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.PrerequisiteIncomplete, result.ReasonCode);
|
|
Assert.Null(result.Snapshot);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldSucceedRefineIntro_WhenGatherIntroCompleted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
Assert.True(QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider).Success);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
PlayerId,
|
|
RefineQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Null(result.ReasonCode);
|
|
Assert.Equal(QuestProgressStatus.Active, result.Snapshot!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
PlayerId,
|
|
"prototype_quest_missing",
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldDenyAlreadyActive_WhenQuestAlreadyAccepted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.AlreadyActive, result.ReasonCode);
|
|
Assert.NotNull(result.Snapshot);
|
|
Assert.Equal(QuestProgressStatus.Active, result.Snapshot!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldDenyAlreadyCompleted_WhenQuestAlreadyCompleted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
Assert.True(QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider).Success);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.AlreadyCompleted, result.ReasonCode);
|
|
Assert.Equal(QuestProgressStatus.Completed, result.Snapshot!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldDenyUnknownPlayer_WhenPrerequisiteQuestNotWritable()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
UnknownPlayerId,
|
|
RefineQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.UnknownPlayer, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAccept_ShouldDenyUnknownPlayer_WhenPlayerNotInStore()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAccept(
|
|
UnknownPlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.UnknownPlayer, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAdvanceStep_ShouldAdvanceChainQuestAndClearCounters()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
AcceptAndComplete(deps, GatherQuestId, timeProvider);
|
|
AcceptAndComplete(deps, RefineQuestId, timeProvider);
|
|
AcceptAndComplete(deps, CombatQuestId, timeProvider);
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainObjectiveId, 5, out _));
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAdvanceStep(
|
|
PlayerId,
|
|
ChainQuestId,
|
|
1,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.True(result.Success);
|
|
Assert.Null(result.ReasonCode);
|
|
Assert.Equal(1, result.Snapshot!.CurrentStepIndex);
|
|
Assert.Empty(result.Snapshot.ObjectiveCounters);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(4)]
|
|
public async Task TryAdvanceStep_ShouldDenyInvalidStepIndex_WhenIndexNotIncreasingOrPastLastStep(int newStepIndex)
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
AcceptAndComplete(deps, GatherQuestId, timeProvider);
|
|
AcceptAndComplete(deps, RefineQuestId, timeProvider);
|
|
AcceptAndComplete(deps, CombatQuestId, timeProvider);
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAdvanceStep(
|
|
PlayerId,
|
|
ChainQuestId,
|
|
newStepIndex,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.InvalidStepIndex, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAdvanceStep_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAdvanceStep(
|
|
PlayerId,
|
|
"prototype_quest_missing",
|
|
1,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryMarkComplete_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
"prototype_quest_missing",
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
TimeProvider.System);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAdvanceStep_ShouldDenyNotActive_WhenQuestNotAccepted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAdvanceStep(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
1,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.NotActive, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryAdvanceStep_ShouldDenyAlreadyCompleted_WhenQuestCompleted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
Assert.True(QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider).Success);
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryAdvanceStep(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
1,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.AlreadyCompleted, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryMarkComplete_ShouldBeIdempotentAtOperationsLayer()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
|
|
// Act
|
|
var first = QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider);
|
|
var second = QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider);
|
|
|
|
// Assert
|
|
Assert.True(first.Success);
|
|
Assert.Null(first.ReasonCode);
|
|
Assert.NotNull(first.Snapshot!.CompletedAt);
|
|
Assert.True(second.Success);
|
|
Assert.Null(second.ReasonCode);
|
|
Assert.Equal(first.Snapshot.CompletedAt, second.Snapshot!.CompletedAt);
|
|
Assert.Equal(QuestProgressStatus.Completed, second.Snapshot.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryMarkComplete_ShouldDenyNotActive_WhenQuestNeverAccepted()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
var timeProvider = TimeProvider.System;
|
|
|
|
// Act
|
|
var result = QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider);
|
|
|
|
// Assert
|
|
Assert.False(result.Success);
|
|
Assert.Equal(QuestStateReasonCodes.NotActive, result.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Host_ShouldResolveRegistryAndStore_ForQuestStateOperations()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var deps = ResolveDependencies(factory);
|
|
|
|
// Act
|
|
var accept = QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore);
|
|
var complete = QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
GatherQuestId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
TimeProvider.System);
|
|
|
|
// Assert
|
|
Assert.True(accept.Success);
|
|
Assert.True(complete.Success);
|
|
Assert.Equal(QuestProgressStatus.Completed, complete.Snapshot!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void CanWritePlayer_ShouldReturnFalse_ForUnknownPlayer()
|
|
{
|
|
// Arrange
|
|
var store = new InMemoryPlayerQuestStateStore(
|
|
Microsoft.Extensions.Options.Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }));
|
|
// Act
|
|
var canWrite = store.CanWritePlayer(UnknownPlayerId);
|
|
// Assert
|
|
Assert.False(canWrite);
|
|
}
|
|
|
|
[Fact]
|
|
public void CanWritePlayer_ShouldReturnTrue_ForDevPlayer()
|
|
{
|
|
// Arrange
|
|
var store = new InMemoryPlayerQuestStateStore(
|
|
Microsoft.Extensions.Options.Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }));
|
|
// Act
|
|
var canWrite = store.CanWritePlayer(PlayerId);
|
|
// Assert
|
|
Assert.True(canWrite);
|
|
}
|
|
|
|
private static void AcceptAndComplete(
|
|
(IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) deps,
|
|
string questId,
|
|
TimeProvider timeProvider)
|
|
{
|
|
Assert.True(QuestStateOperations.TryAccept(PlayerId, questId, deps.QuestRegistry, deps.ProgressStore).Success);
|
|
Assert.True(QuestStateOperations.TryMarkComplete(
|
|
PlayerId,
|
|
questId,
|
|
deps.QuestRegistry,
|
|
deps.ProgressStore,
|
|
timeProvider).Success);
|
|
}
|
|
|
|
private static (IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) ResolveDependencies(
|
|
InMemoryWebApplicationFactory factory)
|
|
{
|
|
var services = factory.Services;
|
|
return (
|
|
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
|
services.GetRequiredService<IPlayerQuestStateStore>());
|
|
}
|
|
}
|