neon-sprawl/server/NeonSprawl.Server.Tests/Game/Npc/AggroThreatIntegrationTests.cs

114 lines
4.5 KiB
C#

using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.AbilityInput;
using NeonSprawl.Server.Game.Npc;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Targeting;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Npc;
/// <summary>HTTP integration tests for NEO-92 aggro acquire + leash clear wiring.</summary>
public sealed class AggroThreatIntegrationTests
{
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 LockMeleeAsync(HttpClient client)
{
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
new TargetSelectRequest
{
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId,
});
response.EnsureSuccessStatusCode();
}
private static async Task TeleportDevPlayerAsync(HttpClient client, double x, double y, double z)
{
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/move-stream",
new MoveStreamRequest
{
SchemaVersion = MoveStreamRequest.CurrentSchemaVersion,
Targets = [new PositionVector { X = x, Y = y, Z = z }],
});
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task CastThenMoveBeyondLeash_ShouldAcquireThenClearAggroHolder()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var threatStore = factory.Services.GetRequiredService<IThreatStateStore>();
await BindSlot0PulseAsync(client);
await LockMeleeAsync(client);
// Act
var castResponse = await client.PostAsJsonAsync(
"/game/players/dev-local-1/ability-cast",
new AbilityCastRequest
{
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
SlotIndex = 0,
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId,
});
castResponse.EnsureSuccessStatusCode();
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var afterCast);
await TeleportDevPlayerAsync(client, 12, 0, 12);
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var afterMove);
// Assert
Assert.Equal("dev-local-1", afterCast.AggroHolderPlayerId);
Assert.Null(afterMove.AggroHolderPlayerId);
}
[Fact]
public async Task ReacquireAfterLeashClear_ShouldSetHolderOnNextDamagingCast()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
var threatStore = factory.Services.GetRequiredService<IThreatStateStore>();
await BindSlot0PulseAsync(client);
await LockMeleeAsync(client);
var cast = new AbilityCastRequest
{
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
SlotIndex = 0,
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId,
};
var firstCast = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
firstCast.EnsureSuccessStatusCode();
await TeleportDevPlayerAsync(client, 12, 0, 12);
await TeleportDevPlayerAsync(client, 0, 0, 0);
await LockMeleeAsync(client);
factory.FakeClock!.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50));
// Act
var secondCast = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
secondCast.EnsureSuccessStatusCode();
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var afterSecondCast);
// Assert
Assert.Equal("dev-local-1", afterSecondCast.AggroHolderPlayerId);
}
}