using System.Net; using System.Net.Http.Json; using System.Text; using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Combat; using NeonSprawl.Server.Game.Gigs; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Game.Npc; using NeonSprawl.Server.Game.Targeting; using Xunit; namespace NeonSprawl.Server.Tests.Game.AbilityInput; /// HTTP integration tests for . /// /// NEO-30: every JSON deny ( false) asserts a non-whitespace /// — matches Slice 3 ability_cast_denied payload expectations; /// authoritative hook-site comments live on (cast route: MapAbilityCastApi extension in the same source file). /// Optional dev: NEON_SPRAWL_API_LOG=1 enables one-line ability_cast_* stdout per JSON cast response (see ). /// public sealed class AbilityCastApiTests { 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 = PrototypeNpcRegistry.PrototypeNpcMeleeId, }); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.SelectionApplied); } private static async Task BindSlot0GuardAsync(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.PrototypeGuard }], }); post.EnsureSuccessStatusCode(); } private static AbilityCastRequest PulseCastRequest() => new() { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; 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 PostAbilityCast_ShouldAccept_WhenLoadoutMatchesAndLockInRange() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Accepted); Assert.Null(body.ReasonCode); Assert.NotNull(body.CombatResolution); Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, body.CombatResolution!.AbilityId); Assert.Equal(PrototypeNpcRegistry.PrototypeNpcMeleeId, body.CombatResolution.TargetId); Assert.Equal(25, body.CombatResolution.DamageDealt); Assert.Equal(75, body.CombatResolution.TargetRemainingHp); Assert.False(body.CombatResolution.TargetDefeated); } [Fact] public async Task PostAbilityCast_ShouldAccept_WithTargetId_WhenLoadoutMatches() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = " Prototype_Npc_Melee ", }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Accepted); } [Fact] public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenTargetIdMissing() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = null, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenUnknownTargetId() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = "not_a_registered_combat_target", }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenTargetDoesNotMatchServerLock() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcRangedId, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenNoServerLock() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyOutOfRange_WhenAuthoritativePositionTooFarFromLock() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); await TeleportDevPlayerAsync(client, 50.0, 0.9, 50.0); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonOutOfRange, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenySlotUnbound_WhenSlotEmpty() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 2, AbilityId = PrototypeAbilityRegistry.PrototypeGuard, TargetId = null, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonSlotUnbound, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyLoadoutMismatch_WhenAbilityDoesNotMatchSlot() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypeBurst, TargetId = null, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonLoadoutMismatch, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenySlotOutOfBounds() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 8, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = null, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonSlotOutOfBounds, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldDenyUnknownAbility_WhenAbilityIdNotInRegistry() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/ability-cast", new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = "not_a_real_ability", TargetId = null, }); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.False(body!.Accepted); Assert.Equal(AbilityCastApi.ReasonUnknownAbility, body.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldReturnBadRequest_WhenBodyMalformed() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var malformed = new StringContent( "{\"schemaVersion\":1,\"slotIndex\":", Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync("/game/players/dev-local-1/ability-cast", malformed); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostAbilityCast_ShouldReturnBadRequest_WhenSchemaVersionWrong() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = new AbilityCastRequest { SchemaVersion = 999, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostAbilityCast_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, }; // Act var response = await client.PostAsJsonAsync("/game/players/missing-player/ability-cast", request); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostAbilityCast_ShouldStartCatalogCooldown_WhenResolveSucceedsAfterDefinitionGate() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = PulseCastRequest(); // Act var castResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); var snapshotResponse = await client.GetAsync("/game/players/dev-local-1/cooldown-snapshot"); // Assert var body = await castResponse.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, castResponse.StatusCode); Assert.NotNull(body); Assert.True(body!.Accepted); Assert.NotNull(body.CombatResolution); var snapshot = await snapshotResponse.Content.ReadFromJsonAsync(); Assert.NotNull(snapshot); var slot0 = snapshot!.Slots.Single(s => s.SlotIndex == 0); Assert.NotNull(slot0.CooldownEndsAtUtc); } [Fact] public async Task PostAbilityCast_ShouldDenyOnCooldown_WhenRepeatedWhileCooling() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; // Act var first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); // Assert var body1 = await first.Content.ReadFromJsonAsync(); var body2 = await second.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, first.StatusCode); Assert.Equal(HttpStatusCode.OK, second.StatusCode); Assert.NotNull(body1); Assert.NotNull(body2); Assert.True(body1!.Accepted); Assert.False(body2!.Accepted); Assert.Equal(AbilityCastApi.ReasonOnCooldown, body2.ReasonCode); } [Fact] public async Task PostAbilityCast_ShouldAcceptAgain_WhenCooldownElapsed() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; // Act var first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); factory.FakeClock!.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50)); var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); // Assert var body1 = await first.Content.ReadFromJsonAsync(); var body2 = await second.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, first.StatusCode); Assert.Equal(HttpStatusCode.OK, second.StatusCode); Assert.NotNull(body1); Assert.NotNull(body2); Assert.True(body1!.Accepted); Assert.True(body2!.Accepted); Assert.NotNull(body2.CombatResolution); Assert.Equal(50, body2.CombatResolution!.TargetRemainingHp); } [Fact] public async Task PostAbilityCast_ShouldReturnCombatResolution_WhenZeroDamageUtilityAccepted() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); await BindSlot0GuardAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypeGuard, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Accepted); Assert.NotNull(body.CombatResolution); Assert.Equal(0, body.CombatResolution!.DamageDealt); Assert.Equal(100, body.CombatResolution.TargetRemainingHp); Assert.False(body.CombatResolution.TargetDefeated); } [Fact] public async Task PostAbilityCast_ShouldDefeatTargetAfterFourPulses_ThenDenyTargetDefeated() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = PulseCastRequest(); AbilityCastResponse? first = null; AbilityCastResponse? second = null; AbilityCastResponse? third = null; for (var i = 0; i < 3; i++) { var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadFromJsonAsync(); if (i == 0) { first = body; } else if (i == 1) { second = body; } else { third = body; } factory.FakeClock.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50)); } // Act var fourthResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); factory.FakeClock.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50)); var fifthResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); // Assert Assert.NotNull(first); Assert.Equal(75, first!.CombatResolution!.TargetRemainingHp); Assert.NotNull(second); Assert.Equal(50, second!.CombatResolution!.TargetRemainingHp); Assert.NotNull(third); Assert.Equal(25, third!.CombatResolution!.TargetRemainingHp); var fourth = await fourthResponse.Content.ReadFromJsonAsync(); Assert.NotNull(fourth); Assert.True(fourth!.Accepted); Assert.NotNull(fourth.CombatResolution); Assert.Equal(0, fourth.CombatResolution!.TargetRemainingHp); Assert.True(fourth.CombatResolution.TargetDefeated); var fifth = await fifthResponse.Content.ReadFromJsonAsync(); Assert.NotNull(fifth); Assert.False(fifth!.Accepted); Assert.Equal(CombatReasonCodes.TargetDefeated, fifth.ReasonCode); Assert.Null(fifth.CombatResolution); } [Fact] public async Task PostAbilityCast_ShouldGrantGigXpOnceOnDefeat_WithoutChangingSkillProgression() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var skillBefore = await client.GetFromJsonAsync( "/game/players/dev-local-1/skill-progression"); Assert.NotNull(skillBefore); var salvageBefore = skillBefore!.Skills!.Single(static s => s.Id == "salvage"); var refineBefore = skillBefore.Skills!.Single(static s => s.Id == "refine"); var cast = PulseCastRequest(); // Act for (var i = 0; i < 3; i++) { var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); response.EnsureSuccessStatusCode(); factory.FakeClock.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50)); } var fourthResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); factory.FakeClock.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50)); var fifthResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); var gigSnapshot = await client.GetFromJsonAsync( "/game/players/dev-local-1/gig-progression"); var skillAfter = await client.GetFromJsonAsync( "/game/players/dev-local-1/skill-progression"); // Assert var fourth = await fourthResponse.Content.ReadFromJsonAsync(); Assert.NotNull(fourth); Assert.True(fourth!.CombatResolution!.TargetDefeated); var fifth = await fifthResponse.Content.ReadFromJsonAsync(); Assert.NotNull(fifth); Assert.False(fifth!.Accepted); Assert.NotNull(gigSnapshot); var breach = Assert.Single(gigSnapshot!.Gigs!); Assert.Equal(GigProgressionConstants.CombatDefeatXpAmount, breach.Xp); Assert.NotNull(skillAfter); var salvageAfter = skillAfter!.Skills!.Single(static s => s.Id == "salvage"); var refineAfter = skillAfter.Skills!.Single(static s => s.Id == "refine"); Assert.Equal(salvageBefore.Xp, salvageAfter.Xp); Assert.Equal(refineBefore.Xp, refineAfter.Xp); } [Fact] public async Task PostAbilityCast_ShouldUseCatalogCooldown_WhenGuardCooldownLongerThanPulse() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0GuardAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypeGuard, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; // Act var first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); factory.FakeClock.Advance(TimeSpan.FromSeconds(3.5)); var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); factory.FakeClock.Advance(TimeSpan.FromSeconds(3)); var third = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); // Assert var body1 = await first.Content.ReadFromJsonAsync(); var body2 = await second.Content.ReadFromJsonAsync(); var body3 = await third.Content.ReadFromJsonAsync(); Assert.True(body1!.Accepted); Assert.False(body2!.Accepted); Assert.Equal(AbilityCastApi.ReasonOnCooldown, body2.ReasonCode); Assert.True(body3!.Accepted); } }