neon-sprawl/server/NeonSprawl.Server.Tests/Game/Npc/NpcBehaviorDefinitionRegist...

303 lines
12 KiB
C#

using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NeonSprawl.Server.Game.Npc;
using NeonSprawl.Server.Tests;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Npc;
public class NpcBehaviorDefinitionRegistryTests
{
private static NpcBehaviorDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary<string, NpcBehaviorDefRow> byId)
{
var catalog = new NpcBehaviorDefinitionCatalog("/tmp/npc-behaviors", byId, catalogJsonFileCount: 1);
return new NpcBehaviorDefinitionRegistry(catalog);
}
private static NpcBehaviorDefRow MeleeRow() =>
new(
PrototypeNpcBehaviorRegistry.PrototypeMeleePressure,
"Melee Pressure",
"melee_pressure",
100,
8.0,
16.0,
1.5,
15,
3.0);
private static NpcBehaviorDefRow RangedRow() =>
new(
PrototypeNpcBehaviorRegistry.PrototypeRangedControl,
"Ranged Control",
"ranged_control",
80,
10.0,
20.0,
2.0,
12,
4.0);
private static NpcBehaviorDefRow EliteRow() =>
new(
PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss,
"Elite Mini-Boss",
"elite_mini_boss",
200,
8.0,
18.0,
2.5,
25,
5.0);
[Fact]
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenMeleeExists()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, out var def);
// Assert
Assert.True(found);
Assert.NotNull(def);
Assert.Equal(100, def.MaxHp);
Assert.Equal(8.0, def.AggroRadius);
Assert.Equal(16.0, def.LeashRadius);
Assert.Equal(15, def.AttackDamage);
Assert.Equal(1.5, def.TelegraphWindupSeconds);
Assert.Equal("melee_pressure", def.ArchetypeKind);
Assert.Equal("Melee Pressure", def.DisplayName);
}
[Fact]
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenEliteExists()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss] = EliteRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, out var def);
// Assert
Assert.True(found);
Assert.NotNull(def);
Assert.Equal(200, def.MaxHp);
Assert.Equal(25, def.AttackDamage);
Assert.Equal(2.5, def.TelegraphWindupSeconds);
}
[Fact]
public void TryGetDefinition_ShouldReturnFalse_WhenBehaviorIdIsNull()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryGetDefinition(null, out var def);
// Assert
Assert.False(found);
Assert.Null(def);
}
[Fact]
public void TryGetDefinition_ShouldReturnFalse_WhenIdUnknown()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryGetDefinition("not_a_real_behavior", out var def);
// Assert
Assert.False(found);
Assert.Null(def);
}
[Fact]
public void TryNormalizeKnown_ShouldReturnTrueAndLowercaseId_WhenInputHasWhitespaceAndMixedCase()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryNormalizeKnown(" Prototype_Melee_Pressure ", out var normalized);
// Assert
Assert.True(found);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, normalized);
}
[Fact]
public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsNull()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryNormalizeKnown(null, out var normalized);
// Assert
Assert.False(found);
Assert.Equal(string.Empty, normalized);
}
[Fact]
public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsWhitespaceOnly()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryNormalizeKnown(" ", out var normalized);
// Assert
Assert.False(found);
Assert.Equal(string.Empty, normalized);
}
[Fact]
public void TryNormalizeKnown_ShouldReturnFalse_WhenIdNotInCatalog()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var found = registry.TryNormalizeKnown("prototype_unknown", out var normalized);
// Assert
Assert.False(found);
Assert.Equal("prototype_unknown", normalized);
}
[Fact]
public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleBehaviors()
{
// Arrange
var rows = new Dictionary<string, NpcBehaviorDefRow>(StringComparer.Ordinal)
{
[PrototypeNpcBehaviorRegistry.PrototypeRangedControl] = RangedRow(),
[PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss] = EliteRow(),
[PrototypeNpcBehaviorRegistry.PrototypeMeleePressure] = MeleeRow(),
};
var registry = CreateRegistryFromRows(rows);
// Act
var list = registry.GetDefinitionsInIdOrder();
// Assert
Assert.Equal(3, list.Count);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, list[0].Id);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, list[1].Id);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, list[2].Id);
}
[Fact]
public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
{
// Arrange
var root = Directory.CreateTempSubdirectory("neon-sprawl-npc-registry-loader-");
try
{
var npcBehaviorsDir = Path.Combine(root.FullName, "content", "npc-behaviors");
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
Directory.CreateDirectory(npcBehaviorsDir);
Directory.CreateDirectory(schemaDir);
var schemaPath = Path.Combine(schemaDir, "npc-behavior-def.schema.json");
File.Copy(NpcBehaviorCatalogTestPaths.DiscoverRepoNpcBehaviorDefSchemaPath(), schemaPath, overwrite: true);
File.Copy(
Path.Combine(NpcBehaviorCatalogTestPaths.DiscoverRepoNpcBehaviorsDirectory(), "prototype_npc_behaviors.json"),
Path.Combine(npcBehaviorsDir, "prototype_npc_behaviors.json"),
overwrite: true);
var loaded = NpcBehaviorDefinitionCatalogLoader.Load(npcBehaviorsDir, schemaPath, NullLogger.Instance);
var registry = new NpcBehaviorDefinitionRegistry(loaded);
// Act
var meleeOk = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, out var melee);
var rangedOk = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, out var ranged);
var eliteOk = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, out var elite);
var list = registry.GetDefinitionsInIdOrder();
// Assert
Assert.True(meleeOk);
Assert.NotNull(melee);
Assert.Equal(100, melee!.MaxHp);
Assert.Equal(8.0, melee.AggroRadius);
Assert.Equal(16.0, melee.LeashRadius);
Assert.Equal(15, melee.AttackDamage);
Assert.Equal(1.5, melee.TelegraphWindupSeconds);
Assert.True(rangedOk);
Assert.NotNull(ranged);
Assert.Equal(80, ranged!.MaxHp);
Assert.Equal(10.0, ranged.AggroRadius);
Assert.Equal(20.0, ranged.LeashRadius);
Assert.Equal(12, ranged.AttackDamage);
Assert.Equal(2.0, ranged.TelegraphWindupSeconds);
Assert.True(eliteOk);
Assert.NotNull(elite);
Assert.Equal(200, elite!.MaxHp);
Assert.Equal(8.0, elite.AggroRadius);
Assert.Equal(18.0, elite.LeashRadius);
Assert.Equal(25, elite.AttackDamage);
Assert.Equal(2.5, elite.TelegraphWindupSeconds);
Assert.Equal(3, list.Count);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss, list[0].Id);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, list[^1].Id);
}
finally
{
try
{
Directory.Delete(root.FullName, recursive: true);
}
catch (IOException)
{
// Best-effort: transient lock or race on some hosts; temp dir is unique per run.
}
}
}
[Fact]
public async Task Host_ShouldResolveRegistryFromDi_WhenStartupSucceeds()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
using var client = factory.CreateClient();
_ = await client.GetAsync("/health");
// Act
var registry = factory.Services.GetRequiredService<INpcBehaviorDefinitionRegistry>();
var meleeFound = registry.TryGetDefinition(PrototypeNpcBehaviorRegistry.PrototypeMeleePressure, out var melee);
var unknown = registry.TryGetDefinition("not_a_real_behavior", out var missing);
var normalized = registry.TryNormalizeKnown(" Prototype_Ranged_Control ", out var rangedId);
var list = registry.GetDefinitionsInIdOrder();
// Assert
Assert.True(meleeFound);
Assert.NotNull(melee);
Assert.Equal(100, melee!.MaxHp);
Assert.Equal(15, melee.AttackDamage);
Assert.False(unknown);
Assert.Null(missing);
Assert.True(normalized);
Assert.Equal(PrototypeNpcBehaviorRegistry.PrototypeRangedControl, rangedId);
Assert.Equal(3, list.Count);
Assert.Contains(
list,
b => b.Id == PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss && b.MaxHp == 200 && b.AttackDamage == 25);
}
}