neon-sprawl/server/NeonSprawl.Server.Tests/Game/Encounters/EncounterProgressApiTests.cs

252 lines
9.8 KiB
C#

using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.AbilityInput;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Npc;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Targeting;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Encounters;
public sealed class EncounterProgressApiTests
{
private const string PrototypeEncounterId = "prototype_combat_pocket";
[Fact]
public async Task GetEncounterProgress_ShouldReturnNotFound_WhenPlayerUnknown()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/players/missing-player/encounter-progress");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetEncounterProgress_ShouldReturnInactive_WhenNoEngagement()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/players/dev-local-1/encounter-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<EncounterProgressListResponse>();
Assert.NotNull(body);
Assert.Equal(EncounterProgressListResponse.CurrentSchemaVersion, body!.SchemaVersion);
Assert.Equal("dev-local-1", body.PlayerId);
var row = Assert.Single(body.Encounters);
Assert.Equal(PrototypeEncounterId, row.EncounterId);
Assert.Equal(EncounterProgressApi.StateInactive, row.State);
Assert.Empty(row.DefeatedTargetIds);
Assert.Null(row.CompletedAt);
Assert.Null(row.RewardGrantSummary);
}
[Fact]
public async Task GetEncounterProgress_ShouldReturnActiveWithOneDefeat_WhenFirstNpcDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
// Act
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcMeleeId);
var response = await client.GetAsync("/game/players/dev-local-1/encounter-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<EncounterProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Encounters);
Assert.Equal(EncounterProgressApi.StateActive, row.State);
Assert.Equal([PrototypeNpcRegistry.PrototypeNpcMeleeId], row.DefeatedTargetIds);
Assert.Null(row.CompletedAt);
Assert.Null(row.RewardGrantSummary);
}
[Fact]
public async Task GetEncounterProgress_ShouldReturnActiveWithTwoDefeats_WhenTwoNpcDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
// Act
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcMeleeId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcRangedId);
var response = await client.GetAsync("/game/players/dev-local-1/encounter-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<EncounterProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Encounters);
Assert.Equal(EncounterProgressApi.StateActive, row.State);
Assert.Equal(
[
PrototypeNpcRegistry.PrototypeNpcMeleeId,
PrototypeNpcRegistry.PrototypeNpcRangedId,
],
row.DefeatedTargetIds);
Assert.Null(row.CompletedAt);
Assert.Null(row.RewardGrantSummary);
var completionStore = factory.Services.GetRequiredService<IEncounterCompletionStore>();
Assert.False(completionStore.IsCompleted("dev-local-1", PrototypeEncounterId));
}
[Fact]
public async Task GetEncounterProgress_ShouldReturnCompletedWithGrantSummary_WhenAllThreeNpcDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
// Act
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcMeleeId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcRangedId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcEliteId);
var response = await client.GetAsync("/game/players/dev-local-1/encounter-progress");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<EncounterProgressListResponse>();
Assert.NotNull(body);
var row = Assert.Single(body!.Encounters);
Assert.Equal(EncounterProgressApi.StateCompleted, row.State);
Assert.Equal(
[
PrototypeNpcRegistry.PrototypeNpcEliteId,
PrototypeNpcRegistry.PrototypeNpcMeleeId,
PrototypeNpcRegistry.PrototypeNpcRangedId,
],
row.DefeatedTargetIds);
Assert.NotNull(row.CompletedAt);
Assert.NotNull(row.RewardGrantSummary);
Assert.Equal(10, row.RewardGrantSummary!.Single(g => g.ItemId == "scrap_metal_bulk").Quantity);
Assert.Equal(1, row.RewardGrantSummary.Single(g => g.ItemId == "contract_handoff_token").Quantity);
var completionStore = factory.Services.GetRequiredService<IEncounterCompletionStore>();
var eventStore = factory.Services.GetRequiredService<IEncounterCompleteEventStore>();
Assert.True(completionStore.IsCompleted("dev-local-1", PrototypeEncounterId));
Assert.True(eventStore.TryGet("dev-local-1", PrototypeEncounterId, out _));
}
private static async Task BindSlot0PulseAsync(HttpClient client)
{
var post = await client.PostAsJsonAsync(
"/game/players/dev-local-1/hotbar-loadout",
new HotbarLoadoutUpdateRequest
{
SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion,
Slots = [new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }],
});
post.EnsureSuccessStatusCode();
}
private static async Task LockPrototypeTargetAsync(HttpClient client, string targetId)
{
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
new TargetSelectRequest
{
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = targetId,
});
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
Assert.NotNull(body);
Assert.True(body!.SelectionApplied);
}
private static async Task MoveDevPlayerNearNpcAsync(HttpClient client, string npcInstanceId)
{
Assert.True(PrototypeNpcRegistry.TryGet(npcInstanceId, out var entry));
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/move",
new MoveCommandRequest
{
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector
{
X = entry.X - 1,
Y = 0.9,
Z = entry.Z - 1,
},
});
response.EnsureSuccessStatusCode();
}
private static AbilityCastRequest PulseCastRequestForTarget(string targetId) =>
new()
{
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
SlotIndex = 0,
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
TargetId = targetId,
};
private static async Task DefeatLockedTargetWithPulseAsync(
HttpClient client,
InMemoryWebApplicationFactory factory,
string targetId)
{
Assert.NotNull(factory.FakeClock);
factory.FakeClock.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50));
await BindSlot0PulseAsync(client);
await MoveDevPlayerNearNpcAsync(client, targetId);
await LockPrototypeTargetAsync(client, targetId);
var cast = PulseCastRequestForTarget(targetId);
AbilityCastResponse? defeatBody = null;
for (var i = 0; i < 12; i++)
{
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
Assert.NotNull(body);
Assert.True(body!.Accepted, body.ReasonCode);
Assert.NotNull(body.CombatResolution);
if (body.CombatResolution!.TargetDefeated)
{
defeatBody = body;
break;
}
factory.FakeClock!.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50));
}
Assert.NotNull(defeatBody);
Assert.True(defeatBody!.CombatResolution!.TargetDefeated);
}
}