using System.Linq; using System.Net; using System.Net.Http.Json; using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Combat; using NeonSprawl.Server.Game.Targeting; using Xunit; namespace NeonSprawl.Server.Tests.Game.Combat; public class CombatTargetsWorldApiTests { /// Prototype combat targets in registry id order (ordinal). Keep in sync with Bruno. public static readonly string[] PrototypeTargetsInIdOrder = [ PrototypeTargetRegistry.PrototypeTargetAlphaId, PrototypeTargetRegistry.PrototypeTargetBetaId, ]; 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 LockPrototypeTargetAlphaAsync(HttpClient client) { var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/target/select", new TargetSelectRequest { SchemaVersion = TargetSelectRequest.CurrentSchemaVersion, TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId, }); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.SelectionApplied); } private static AbilityCastRequest PulseCastRequest() => new() { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId, }; [Fact] public async Task GetCombatTargets_ShouldReturnSchemaV1_WithBothTargetsAtFullHp() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/world/combat-targets"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(CombatTargetsListResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.NotNull(body.Targets); Assert.Equal(2, body.Targets.Count); var ids = body.Targets.Select(static t => t.TargetId).ToList(); Assert.Equal(PrototypeTargetsInIdOrder, ids); var alpha = body.Targets.Single(t => t.TargetId == PrototypeTargetRegistry.PrototypeTargetAlphaId); Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, alpha.MaxHp); Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, alpha.CurrentHp); Assert.False(alpha.Defeated); var beta = body.Targets.Single(t => t.TargetId == PrototypeTargetRegistry.PrototypeTargetBetaId); Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, beta.MaxHp); Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, beta.CurrentHp); Assert.False(beta.Defeated); } [Fact] public async Task GetCombatTargets_ShouldReflectCastDamage_AfterOnePulseOnAlpha() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var castResponse = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", PulseCastRequest()); castResponse.EnsureSuccessStatusCode(); var castBody = await castResponse.Content.ReadFromJsonAsync(); // Act var response = await client.GetAsync("/game/world/combat-targets"); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.NotNull(castBody); Assert.True(castBody!.Accepted); Assert.NotNull(castBody.CombatResolution); Assert.Equal(75, castBody.CombatResolution!.TargetRemainingHp); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); var alpha = body!.Targets.Single(t => t.TargetId == PrototypeTargetRegistry.PrototypeTargetAlphaId); Assert.Equal(75, alpha.CurrentHp); Assert.False(alpha.Defeated); var beta = body.Targets.Single(t => t.TargetId == PrototypeTargetRegistry.PrototypeTargetBetaId); Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, beta.CurrentHp); Assert.False(beta.Defeated); } [Fact] public async Task GetCombatTargets_ShouldShowDefeated_AfterFourPulsesOnAlpha() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = PulseCastRequest(); for (var i = 0; i < 4; i++) { var castResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); castResponse.EnsureSuccessStatusCode(); if (i < 3) { factory.FakeClock!.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50)); } } // Act var response = await client.GetAsync("/game/world/combat-targets"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); var alpha = body!.Targets.Single(t => t.TargetId == PrototypeTargetRegistry.PrototypeTargetAlphaId); Assert.Equal(0, alpha.CurrentHp); Assert.True(alpha.Defeated); var beta = body.Targets.Single(t => t.TargetId == PrototypeTargetRegistry.PrototypeTargetBetaId); Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, beta.CurrentHp); Assert.False(beta.Defeated); } }