neon-sprawl/server/NeonSprawl.Server.Tests/Game/Quests/QuestProgressApiTests.cs

362 lines
15 KiB
C#

using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Crafting;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Gathering;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Quests;
public sealed class QuestProgressApiTests
{
private const string PlayerId = "dev-local-1";
private const string AlphaNodeId = "prototype_resource_node_alpha";
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 GatherObjectiveId = PrototypeE7M1QuestCatalogRules.GatherIntroFirstObjectiveId;
private const string ChainGatherObjectiveId = PrototypeE7M1QuestCatalogRules.ChainFirstObjectiveId;
[Fact]
public async Task GetQuestProgress_ShouldReturnNotFound_WhenPlayerUnknown()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/players/missing-player/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetQuestProgress_ShouldReturnNotFound_WhenPlayerIdIsWhitespaceOnly()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/players/%20/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetQuestProgress_ShouldReturnNotStartedForAllQuests_WhenNoProgress()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var registry = factory.Services.GetRequiredService<IQuestDefinitionRegistry>();
var expectedQuestOrder = registry.GetDefinitionsInIdOrder().Select(static d => d.Id).ToArray();
// Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<QuestProgressListResponse>();
Assert.NotNull(body);
Assert.Equal(QuestProgressListResponse.CurrentSchemaVersion, body!.SchemaVersion);
Assert.Equal(PlayerId, body.PlayerId);
Assert.Equal(expectedQuestOrder.Length, body.Quests.Count);
Assert.Equal(expectedQuestOrder, body.Quests.Select(static row => row.QuestId).ToArray());
foreach (var row in body.Quests)
{
Assert.Equal(QuestProgressApi.StatusNotStarted, row.Status);
Assert.Equal(0, row.CurrentStepIndex);
Assert.Empty(row.ObjectiveCounters);
Assert.Null(row.CompletedAt);
}
}
[Fact]
public async Task GetQuestProgress_ShouldReturnActivePartialCounters_WhenGatherIntroInProgress()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
var client = factory.CreateClient();
Assert.True(TryAccept(deps, GatherQuestId).Success);
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(
PlayerId,
GatherQuestId,
GatherObjectiveId,
2,
out _));
// Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<QuestProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Quests, static r => r.QuestId == GatherQuestId);
Assert.Equal(QuestProgressApi.StatusActive, row.Status);
Assert.Equal(0, row.CurrentStepIndex);
Assert.Equal(2, row.ObjectiveCounters[GatherObjectiveId]);
Assert.Null(row.CompletedAt);
}
[Fact]
public async Task GetQuestProgress_ShouldReturnCompletedGatherIntro_WhenObjectivesSatisfied()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
var client = factory.CreateClient();
Assert.True(TryAccept(deps, GatherQuestId).Success);
for (var i = 0; i < 3; i++)
{
Assert.True(TryGather(deps, AlphaNodeId).Success);
}
// Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<QuestProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Quests, static r => r.QuestId == GatherQuestId);
Assert.Equal(QuestProgressApi.StatusCompleted, row.Status);
Assert.Equal(0, row.CurrentStepIndex);
Assert.NotNull(row.CompletedAt);
}
[Fact]
public async Task GetQuestProgress_ShouldReturnCompletedChainAtTerminalStepIndex_WhenOperatorChainFinished()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
var client = factory.CreateClient();
CompleteOperatorChain(deps);
// Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<QuestProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Quests, static r => r.QuestId == ChainQuestId);
Assert.Equal(QuestProgressApi.StatusCompleted, row.Status);
Assert.Equal(3, row.CurrentStepIndex);
Assert.NotNull(row.CompletedAt);
}
[Fact]
public async Task GetQuestProgress_ShouldCompleteChainTerminal_WhenTokenHeldAndInventoryRefreshRunsOnGet()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
var client = factory.CreateClient();
AcceptAndMarkComplete(deps, GatherQuestId);
AcceptAndMarkComplete(deps, RefineQuestId);
AcceptAndMarkComplete(deps, CombatQuestId);
SeedStack(deps, PrototypeE7M1QuestCatalogRules.ChainTerminalItemId, 1);
Assert.True(TryAccept(deps, ChainQuestId).Success);
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainGatherObjectiveId, 5, out _));
Assert.True(deps.ProgressStore.TryAdvanceStep(PlayerId, ChainQuestId, 1, out _));
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_refine", 1, out _));
Assert.True(deps.ProgressStore.TryAdvanceStep(PlayerId, ChainQuestId, 2, out _));
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, "chain_obj_stim", 1, out _));
Assert.True(deps.ProgressStore.TryAdvanceStep(PlayerId, ChainQuestId, 3, out _));
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var beforeGet));
Assert.Equal(QuestProgressStatus.Active, beforeGet.Status);
Assert.Equal(3, beforeGet.CurrentStepIndex);
// Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<QuestProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Quests, static r => r.QuestId == ChainQuestId);
Assert.Equal(QuestProgressApi.StatusCompleted, row.Status);
Assert.Equal(3, row.CurrentStepIndex);
Assert.NotNull(row.CompletedAt);
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var afterGet));
Assert.Equal(QuestProgressStatus.Completed, afterGet.Status);
Assert.Equal(3, afterGet.CurrentStepIndex);
Assert.NotNull(afterGet.CompletedAt);
}
private static void CompleteOperatorChain(QuestWiringTestDependencies deps)
{
AcceptAndMarkComplete(deps, GatherQuestId);
AcceptAndMarkComplete(deps, RefineQuestId);
CompleteCombatEncounter(deps);
Assert.True(TryAccept(deps, ChainQuestId).Success);
Assert.True(TryGather(deps, "prototype_urban_bulk_delta").Success);
SeedStack(deps, "scrap_metal_bulk", 5);
Assert.True(TryCraft(deps, "refine_scrap_standard").Success);
SeedStack(deps, "refined_plate_stock", 2);
SeedStack(deps, "scrap_metal_bulk", 1);
Assert.True(TryCraft(deps, "make_field_stim_mk0").Success);
Assert.True(deps.ProgressStore.TryGetProgress(PlayerId, ChainQuestId, out var progress));
Assert.Equal(QuestProgressStatus.Completed, progress.Status);
Assert.Equal(3, progress.CurrentStepIndex);
}
private static void CompleteCombatEncounter(QuestWiringTestDependencies deps)
{
Assert.True(TryAccept(deps, CombatQuestId).Success);
DefeatAllRequiredTargets(deps);
var result = EncounterCompletionOperations.TryCompleteAndGrant(
PlayerId,
"prototype_combat_pocket",
deps.EncounterRegistry,
deps.RewardTableRegistry,
deps.EncounterProgressStore,
deps.CompletionStore,
deps.CompleteEventStore,
deps.ItemRegistry,
deps.InventoryStore,
deps.QuestRegistry,
deps.ProgressStore,
deps.TimeProvider);
Assert.True(result.Success);
}
private static void DefeatAllRequiredTargets(QuestWiringTestDependencies deps)
{
foreach (var npcId in new[] { "prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite" })
{
_ = EncounterProgressOperations.TryActivateOnFirstEngagement(
PlayerId,
npcId,
deps.EncounterRegistry,
deps.EncounterProgressStore,
deps.CompletionStore);
_ = EncounterProgressOperations.TryMarkTargetDefeated(
PlayerId,
npcId,
deps.EncounterRegistry,
deps.EncounterProgressStore,
deps.CompletionStore);
}
}
private static QuestStateOperationResult TryAccept(QuestWiringTestDependencies deps, string questId) =>
QuestStateOperations.TryAccept(
PlayerId,
questId,
deps.QuestRegistry,
deps.ProgressStore,
deps.InventoryStore,
deps.ItemRegistry,
deps.TimeProvider);
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
{
Assert.True(TryAccept(deps, questId).Success);
Assert.True(QuestStateOperations.TryMarkComplete(
PlayerId,
questId,
deps.QuestRegistry,
deps.ProgressStore,
deps.TimeProvider).Success);
}
private static GatherResult TryGather(QuestWiringTestDependencies deps, string nodeId) =>
GatherOperations.TryGather(
PlayerId,
nodeId,
deps.NodeRegistry,
deps.ItemRegistry,
deps.InventoryStore,
deps.SkillRegistry,
deps.XpStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.InstanceStore,
deps.QuestRegistry,
deps.ProgressStore,
deps.TimeProvider);
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
CraftOperations.TryCraft(
PlayerId,
recipeId,
quantity: 1,
deps.RecipeRegistry,
deps.ItemRegistry,
deps.InventoryStore,
deps.SkillRegistry,
deps.XpStore,
deps.LevelCurve,
deps.PerkUnlockEngine,
deps.QuestRegistry,
deps.ProgressStore,
deps.TimeProvider);
private static void SeedStack(QuestWiringTestDependencies deps, string itemId, int quantity)
{
var outcome = PlayerInventoryOperations.TryAddStack(
PlayerId,
itemId,
quantity,
deps.ItemRegistry,
deps.InventoryStore);
Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
}
private static QuestWiringTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
{
var services = factory.Services;
return new QuestWiringTestDependencies(
services.GetRequiredService<IQuestDefinitionRegistry>(),
services.GetRequiredService<IPlayerQuestStateStore>(),
services.GetRequiredService<IPlayerInventoryStore>(),
services.GetRequiredService<IItemDefinitionRegistry>(),
services.GetRequiredService<IResourceNodeDefinitionRegistry>(),
services.GetRequiredService<IResourceNodeInstanceStore>(),
services.GetRequiredService<IRecipeDefinitionRegistry>(),
services.GetRequiredService<ISkillDefinitionRegistry>(),
services.GetRequiredService<IPlayerSkillProgressionStore>(),
services.GetRequiredService<ISkillLevelCurve>(),
services.GetRequiredService<PerkUnlockEngine>(),
services.GetRequiredService<IEncounterDefinitionRegistry>(),
services.GetRequiredService<IRewardTableDefinitionRegistry>(),
services.GetRequiredService<IEncounterProgressStore>(),
services.GetRequiredService<IEncounterCompletionStore>(),
services.GetRequiredService<IEncounterCompleteEventStore>(),
TimeProvider.System);
}
private readonly record struct QuestWiringTestDependencies(
IQuestDefinitionRegistry QuestRegistry,
IPlayerQuestStateStore ProgressStore,
IPlayerInventoryStore InventoryStore,
IItemDefinitionRegistry ItemRegistry,
IResourceNodeDefinitionRegistry NodeRegistry,
IResourceNodeInstanceStore InstanceStore,
IRecipeDefinitionRegistry RecipeRegistry,
ISkillDefinitionRegistry SkillRegistry,
IPlayerSkillProgressionStore XpStore,
ISkillLevelCurve LevelCurve,
PerkUnlockEngine PerkUnlockEngine,
IEncounterDefinitionRegistry EncounterRegistry,
IRewardTableDefinitionRegistry RewardTableRegistry,
IEncounterProgressStore EncounterProgressStore,
IEncounterCompletionStore CompletionStore,
IEncounterCompleteEventStore CompleteEventStore,
TimeProvider TimeProvider);
}