244 lines
9.7 KiB
C#
244 lines
9.7 KiB
C#
using NeonSprawl.Server.Game.Npc;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Npc;
|
|
|
|
public sealed class NpcRuntimeOperationsTests
|
|
{
|
|
private static readonly DateTimeOffset T0 =
|
|
new(2026, 5, 27, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
private static (INpcRuntimeStateStore Runtime, IThreatStateStore Threat, INpcBehaviorDefinitionRegistry Behavior)
|
|
CreateFixture()
|
|
{
|
|
return (
|
|
new InMemoryNpcRuntimeStateStore(),
|
|
new InMemoryThreatStateStore(),
|
|
PrototypeNpcTestFixtures.CreateBehaviorRegistry());
|
|
}
|
|
|
|
private static void SetHolder(IThreatStateStore threatStore, string npcId, string playerId = "dev-local-1") =>
|
|
_ = threatStore.TrySetHolder(npcId, playerId);
|
|
|
|
private static void Advance(
|
|
DateTimeOffset nowUtc,
|
|
INpcRuntimeStateStore runtimeStore,
|
|
IThreatStateStore threatStore,
|
|
INpcBehaviorDefinitionRegistry behaviorRegistry,
|
|
double maxDeltaSeconds = NpcRuntimeOperations.DefaultMaxDeltaSeconds) =>
|
|
NpcRuntimeOperations.AdvanceAll(nowUtc, runtimeStore, threatStore, behaviorRegistry, maxDeltaSeconds);
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldKeepIdle_WhenNoAggroHolder()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
// Act
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Idle, snapshot.BehaviorState);
|
|
Assert.Null(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldEnterAggro_WhenHolderSetAndRowIdle()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
// Act
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Aggro, snapshot.BehaviorState);
|
|
Assert.Equal(T0, snapshot.PhaseStartedUtc);
|
|
Assert.Null(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldNotTelegraph_WhenNoAggroHolder()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
runtime.LastAdvancedUtc = T0;
|
|
// Act
|
|
Advance(T0.AddSeconds(10), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Idle, snapshot.BehaviorState);
|
|
Assert.Null(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldEnterTelegraphAfterMeleeAttackCooldownSeconds()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(3), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.TelegraphWindup, snapshot.BehaviorState);
|
|
Assert.Equal(T0.AddSeconds(3), snapshot.PhaseStartedUtc);
|
|
Assert.NotNull(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldRemainAggro_BeforeMeleeAttackCooldownCompletes()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(2.9), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Aggro, snapshot.BehaviorState);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldRemainTelegraphWindup_BeforeMeleeWindupCompletes()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(3), runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(4.4), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.TelegraphWindup, snapshot.BehaviorState);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldEnterRecover_WhenMeleeWindupCompletes()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(3), runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(4.4), runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(4.5), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Recover, snapshot.BehaviorState);
|
|
Assert.Equal(T0.AddSeconds(4.5), snapshot.PhaseStartedUtc);
|
|
Assert.Null(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldRunFullMeleeCycle_ToSecondTelegraph()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(3), runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(4.5), runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(7.5), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.TelegraphWindup, snapshot.BehaviorState);
|
|
Assert.Equal(T0.AddSeconds(7.5), snapshot.PhaseStartedUtc);
|
|
Assert.NotNull(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldRunFullMeleeCycle_ToSecondTelegraph_InSingleAdvanceCall()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(7.5), runtime, threat, behavior, maxDeltaSeconds: 10);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.TelegraphWindup, snapshot.BehaviorState);
|
|
Assert.Equal(T0.AddSeconds(7.5), snapshot.PhaseStartedUtc);
|
|
Assert.NotNull(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldReturnToIdle_WhenHolderClearedMidWindup()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(3), runtime, threat, behavior);
|
|
_ = threat.TryClearHolder(PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
// Act
|
|
Advance(T0.AddSeconds(3.5), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Idle, snapshot.BehaviorState);
|
|
Assert.Null(snapshot.ActiveTelegraph);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("prototype_npc_ranged", 4.0)]
|
|
[InlineData("prototype_npc_elite", 5.0)]
|
|
public void AdvanceAll_ShouldEnterTelegraph_AtCatalogCooldownForArchetype(
|
|
string npcInstanceId,
|
|
double attackCooldownSeconds)
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, npcInstanceId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(attackCooldownSeconds), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(npcInstanceId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.TelegraphWindup, snapshot.BehaviorState);
|
|
Assert.Equal(T0.AddSeconds(attackCooldownSeconds), snapshot.PhaseStartedUtc);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("prototype_npc_ranged", 4.0, 2.0)]
|
|
[InlineData("prototype_npc_elite", 5.0, 2.5)]
|
|
public void AdvanceAll_ShouldEnterRecover_AfterCatalogWindupForArchetype(
|
|
string npcInstanceId,
|
|
double attackCooldownSeconds,
|
|
double telegraphWindupSeconds)
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, npcInstanceId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
Advance(T0.AddSeconds(attackCooldownSeconds), runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(attackCooldownSeconds + telegraphWindupSeconds), runtime, threat, behavior);
|
|
// Assert
|
|
runtime.TryGet(npcInstanceId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Recover, snapshot.BehaviorState);
|
|
Assert.Equal(
|
|
T0.AddSeconds(attackCooldownSeconds + telegraphWindupSeconds),
|
|
snapshot.PhaseStartedUtc);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdvanceAll_ShouldCapSimulatedDelta_PerAdvanceCall()
|
|
{
|
|
// Arrange
|
|
var (runtime, threat, behavior) = CreateFixture();
|
|
SetHolder(threat, PrototypeNpcRegistry.PrototypeNpcMeleeId);
|
|
Advance(T0, runtime, threat, behavior);
|
|
// Act
|
|
Advance(T0.AddSeconds(10), runtime, threat, behavior, maxDeltaSeconds: 5);
|
|
// Assert
|
|
runtime.TryGet(PrototypeNpcRegistry.PrototypeNpcMeleeId, out var snapshot);
|
|
Assert.Equal(NpcBehaviorState.Recover, snapshot.BehaviorState);
|
|
Assert.Equal(T0.AddSeconds(4.5), snapshot.PhaseStartedUtc);
|
|
}
|
|
}
|