using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Targeting; namespace NeonSprawl.Server.Tests.Game.Npc; public sealed class NpcRuntimeSnapshotWorldApiTests { 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 LockPrototypeNpcMeleeAsync(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 AbilityCastRequest PulseCastRequest() => new() { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; [Fact] public async Task GetNpcRuntimeSnapshot_ShouldReturnSchemaV1_WithThreeIdleInstances() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/world/npc-runtime-snapshot"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(NpcRuntimeSnapshotResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.NotEqual(default, body.ServerTimeUtc); Assert.NotNull(body.NpcInstances); Assert.Equal(3, body.NpcInstances.Count); var ids = body.NpcInstances.Select(static row => row.NpcInstanceId).ToList(); Assert.Equal(PrototypeNpcRegistryTests.InstancesInIdOrder, ids); foreach (var row in body.NpcInstances) { Assert.Equal("idle", row.State); Assert.Null(row.AggroHolderPlayerId); Assert.Null(row.ActiveTelegraph); } var melee = body.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcMeleeId); Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, melee.BehaviorDefId); } [Fact] public async Task GetNpcRuntimeSnapshot_ShouldShowAggro_OnMeleeAfterDamagingCast() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeNpcMeleeAsync(client); var castResponse = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", PulseCastRequest()); castResponse.EnsureSuccessStatusCode(); // Act var response = await client.GetAsync("/game/world/npc-runtime-snapshot"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); var melee = body!.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcMeleeId); Assert.Equal("aggro", melee.State); Assert.Equal("dev-local-1", melee.AggroHolderPlayerId); Assert.Null(melee.ActiveTelegraph); var ranged = body.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcRangedId); Assert.Equal("idle", ranged.State); Assert.Null(ranged.AggroHolderPlayerId); Assert.Null(ranged.ActiveTelegraph); var elite = body.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcEliteId); Assert.Equal("idle", elite.State); Assert.Null(elite.AggroHolderPlayerId); Assert.Null(elite.ActiveTelegraph); } [Fact] public async Task GetNpcRuntimeSnapshot_ShouldShowTelegraph_AfterAttackCooldownElapses() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeNpcMeleeAsync(client); var castResponse = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", PulseCastRequest()); castResponse.EnsureSuccessStatusCode(); var initResponse = await client.GetAsync("/game/world/npc-runtime-snapshot"); initResponse.EnsureSuccessStatusCode(); factory.FakeClock!.Advance(TimeSpan.FromSeconds(3)); // Act var response = await client.GetAsync("/game/world/npc-runtime-snapshot"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); var melee = body!.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcMeleeId); Assert.Equal("telegraph_windup", melee.State); Assert.NotNull(melee.ActiveTelegraph); Assert.Equal("melee_pressure", melee.ActiveTelegraph!.ArchetypeKind); Assert.Equal(PrototypeNpcRegistry.PrototypeNpcMeleeId, melee.ActiveTelegraph.NpcInstanceId); Assert.InRange(melee.ActiveTelegraph.WindupRemainingSeconds, 1.49, 1.51); } [Fact] public async Task GetNpcRuntimeSnapshot_ShouldDecreaseWindupRemaining_AcrossPolls() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeNpcMeleeAsync(client); var castResponse = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", PulseCastRequest()); castResponse.EnsureSuccessStatusCode(); var initResponse = await client.GetAsync("/game/world/npc-runtime-snapshot"); initResponse.EnsureSuccessStatusCode(); factory.FakeClock!.Advance(TimeSpan.FromSeconds(3)); var firstResponse = await client.GetAsync("/game/world/npc-runtime-snapshot"); factory.FakeClock.Advance(TimeSpan.FromSeconds(1)); // Act var secondResponse = await client.GetAsync("/game/world/npc-runtime-snapshot"); // Assert Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); var firstBody = await firstResponse.Content.ReadFromJsonAsync(); var secondBody = await secondResponse.Content.ReadFromJsonAsync(); Assert.NotNull(firstBody); Assert.NotNull(secondBody); var firstMelee = firstBody!.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcMeleeId); var secondMelee = secondBody!.NpcInstances.Single(row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcMeleeId); Assert.NotNull(firstMelee.ActiveTelegraph); Assert.NotNull(secondMelee.ActiveTelegraph); Assert.True(secondMelee.ActiveTelegraph!.WindupRemainingSeconds < firstMelee.ActiveTelegraph!.WindupRemainingSeconds); Assert.InRange(secondMelee.ActiveTelegraph.WindupRemainingSeconds, 0.49, 0.51); } }