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; /// HTTP integration tests for NEO-92 aggro acquire + leash clear wiring. 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(); 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(); 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); } }