362 lines
15 KiB
C#
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);
|
|
}
|