using NeonSprawl.Server.Game.AbilityInput;
using NeonSprawl.Server.Game.Combat;
using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Gigs;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Targeting;
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 Task LockPrototypeTargetAlphaAsync(HttpClient client) =>
LockPrototypeTargetAsync(client, PrototypeNpcRegistry.PrototypeNpcMeleeId);
private static async Task LockPrototypeTargetAsync(HttpClient client, string targetId)
{
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/target/select",
new TargetSelectRequest
{
SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
TargetId = targetId,
});
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync();
Assert.NotNull(body);
Assert.True(body!.SelectionApplied);
}
private static async Task MoveDevPlayerNearNpcAsync(HttpClient client, string npcInstanceId)
{
Assert.True(PrototypeNpcRegistry.TryGet(npcInstanceId, out var entry));
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/move",
new MoveCommandRequest
{
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector
{
X = entry.X - 1,
Y = 0.9,
Z = entry.Z - 1,
},
});
response.EnsureSuccessStatusCode();
}
private static AbilityCastRequest PulseCastRequestForTarget(string targetId) =>
new()
{
SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
SlotIndex = 0,
AbilityId = PrototypeAbilityRegistry.PrototypePulse,
TargetId = targetId,
};
private static async Task DefeatLockedTargetWithPulseAsync(
HttpClient client,
InMemoryWebApplicationFactory factory,
string targetId)
{
Assert.NotNull(factory.FakeClock);
factory.FakeClock.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50));
await BindSlot0PulseAsync(client);
await MoveDevPlayerNearNpcAsync(client, targetId);
await LockPrototypeTargetAsync(client, targetId);
var cast = PulseCastRequestForTarget(targetId);
AbilityCastResponse? defeatBody = null;
for (var i = 0; i < 12; i++)
{
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync();
Assert.NotNull(body);
Assert.True(body!.Accepted, body.ReasonCode);
Assert.NotNull(body.CombatResolution);
if (body.CombatResolution!.TargetDefeated)
{
defeatBody = body;
break;
}
factory.FakeClock!.Advance(TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(50));
}
Assert.NotNull(defeatBody);
Assert.True(defeatBody!.CombatResolution!.TargetDefeated);
}
private static int CountBagItem(PlayerInventorySnapshotResponse snapshot, string itemId)
{
var total = 0;
foreach (var slot in snapshot.BagSlots)
{
if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
{
total += slot.Quantity;
}
}
return total;
}
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);
var threatStore = factory.Services.GetRequiredService();
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var threat);
Assert.Null(threat.AggroHolderPlayerId);
}
[Fact]
public async Task PostAbilityCast_ShouldSetAggroHolder_WhenDamagingCastAccepted()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var threatStore = factory.Services.GetRequiredService();
await BindSlot0PulseAsync(client);
await LockPrototypeTargetAlphaAsync(client);
// Act
var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", PulseCastRequest());
response.EnsureSuccessStatusCode();
threatStore.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var threat);
// Assert
Assert.Equal("dev-local-1", threat.AggroHolderPlayerId);
}
[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_ShouldStopNpcCombat_WhenTargetDefeated()
{
// 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 < 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));
}
// Act
var fourthResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
fourthResponse.EnsureSuccessStatusCode();
var snapshotResponse = await client.GetAsync("/game/world/npc-runtime-snapshot");
factory.FakeClock.Advance(TimeSpan.FromSeconds(30));
_ = await client.GetAsync("/game/world/npc-runtime-snapshot");
var healthAfterWaitResponse = await client.GetAsync("/game/players/dev-local-1/combat-health");
// Assert
var fourth = await fourthResponse.Content.ReadFromJsonAsync();
Assert.NotNull(fourth);
Assert.True(fourth!.CombatResolution!.TargetDefeated);
snapshotResponse.EnsureSuccessStatusCode();
var snapshotBody = await snapshotResponse.Content.ReadFromJsonAsync();
Assert.NotNull(snapshotBody);
var melee = snapshotBody!.NpcInstances.Single(
row => row.NpcInstanceId == PrototypeNpcRegistry.PrototypeNpcMeleeId);
Assert.Equal("idle", melee.State);
Assert.Null(melee.AggroHolderPlayerId);
Assert.Null(melee.ActiveTelegraph);
healthAfterWaitResponse.EnsureSuccessStatusCode();
var healthBody = await healthAfterWaitResponse.Content.ReadFromJsonAsync();
Assert.NotNull(healthBody);
Assert.Equal(PlayerCombatHealthDefaults.MaxHp, healthBody!.CurrentHp);
}
[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);
}
[Fact]
public async Task PostAbilityCast_ShouldGrantEncounterLootOnce_WhenAllThreeNpcDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
var inventoryBefore = await client.GetFromJsonAsync(
"/game/players/dev-local-1/inventory");
// Act
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcMeleeId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcRangedId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcEliteId);
// Assert
var inventoryAfter = await client.GetFromJsonAsync(
"/game/players/dev-local-1/inventory");
var completionStore = factory.Services.GetRequiredService();
Assert.NotNull(inventoryBefore);
Assert.NotNull(inventoryAfter);
Assert.Equal(
CountBagItem(inventoryBefore!, "scrap_metal_bulk") + 10,
CountBagItem(inventoryAfter!, "scrap_metal_bulk"));
Assert.Equal(
CountBagItem(inventoryBefore!, "contract_handoff_token") + 1,
CountBagItem(inventoryAfter!, "contract_handoff_token"));
Assert.True(completionStore.IsCompleted("dev-local-1", "prototype_combat_pocket"));
}
[Fact]
public async Task PostAbilityCast_ShouldNotGrantEncounterLoot_WhenOnlyTwoNpcDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
var inventoryBefore = await client.GetFromJsonAsync(
"/game/players/dev-local-1/inventory");
// Act
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcMeleeId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcRangedId);
// Assert
var inventoryAfter = await client.GetFromJsonAsync(
"/game/players/dev-local-1/inventory");
var completionStore = factory.Services.GetRequiredService();
Assert.NotNull(inventoryBefore);
Assert.NotNull(inventoryAfter);
Assert.Equal(
CountBagItem(inventoryBefore!, "scrap_metal_bulk"),
CountBagItem(inventoryAfter!, "scrap_metal_bulk"));
Assert.Equal(
CountBagItem(inventoryBefore!, "contract_handoff_token"),
CountBagItem(inventoryAfter!, "contract_handoff_token"));
Assert.False(completionStore.IsCompleted("dev-local-1", "prototype_combat_pocket"));
}
[Fact]
public async Task PostAbilityCast_ShouldGrantGigXpPerDefeat_WhenAllThreeNpcDefeated()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
Assert.NotNull(factory.FakeClock);
var gigBefore = await client.GetFromJsonAsync(
"/game/players/dev-local-1/gig-progression");
// Act
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcMeleeId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcRangedId);
await DefeatLockedTargetWithPulseAsync(
client,
factory,
PrototypeNpcRegistry.PrototypeNpcEliteId);
var gigAfter = await client.GetFromJsonAsync(
"/game/players/dev-local-1/gig-progression");
// Assert
Assert.NotNull(gigBefore);
Assert.NotNull(gigAfter);
var breachBefore = Assert.Single(gigBefore!.Gigs!);
var breachAfter = Assert.Single(gigAfter!.Gigs!);
Assert.Equal(0, breachBefore.Xp);
Assert.Equal(GigProgressionConstants.CombatDefeatXpAmount * 3, breachAfter.Xp);
}
}