336 lines
12 KiB
C#
336 lines
12 KiB
C#
using System.IO;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NeonSprawl.Server.Game.AbilityInput;
|
|
using NeonSprawl.Server.Game.Combat;
|
|
using NeonSprawl.Server.Tests;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Combat;
|
|
|
|
public class AbilityDefinitionRegistryTests
|
|
{
|
|
private static AbilityDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary<string, AbilityDefRow> byId)
|
|
{
|
|
var catalog = new AbilityDefinitionCatalog("/tmp/abilities", byId, catalogJsonFileCount: 1);
|
|
return new AbilityDefinitionRegistry(catalog);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenPulseExists()
|
|
{
|
|
// Arrange
|
|
var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
var registry = CreateRegistryFromRows(rows);
|
|
// Act
|
|
var found = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var def);
|
|
// Assert
|
|
Assert.True(found);
|
|
Assert.NotNull(def);
|
|
Assert.Equal(25, def.BaseDamage);
|
|
Assert.Equal(3.0, def.CooldownSeconds);
|
|
Assert.Equal("attack", def.AbilityKind);
|
|
Assert.Equal("Prototype Pulse", def.DisplayName);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenBurstExists()
|
|
{
|
|
// Arrange
|
|
var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypeBurst] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypeBurst,
|
|
"Prototype Burst",
|
|
40,
|
|
5.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
var registry = CreateRegistryFromRows(rows);
|
|
// Act
|
|
var found = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeBurst, out var def);
|
|
// Assert
|
|
Assert.True(found);
|
|
Assert.NotNull(def);
|
|
Assert.Equal(40, def.BaseDamage);
|
|
Assert.Equal(5.0, def.CooldownSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetDefinition_ShouldReturnFalse_WhenAbilityIdIsNull()
|
|
{
|
|
// Arrange
|
|
var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
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, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
var registry = CreateRegistryFromRows(rows);
|
|
// Act
|
|
var found = registry.TryGetDefinition("not_a_real_ability", out var def);
|
|
// Assert
|
|
Assert.False(found);
|
|
Assert.Null(def);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryNormalizeKnown_ShouldReturnTrueAndLowercaseId_WhenInputHasWhitespaceAndMixedCase()
|
|
{
|
|
// Arrange
|
|
var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
var registry = CreateRegistryFromRows(rows);
|
|
// Act
|
|
var found = registry.TryNormalizeKnown(" Prototype_Pulse ", out var normalized);
|
|
// Assert
|
|
Assert.True(found);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, normalized);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsNull()
|
|
{
|
|
// Arrange
|
|
var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
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, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
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, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
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_WhenMultipleAbilities()
|
|
{
|
|
// Arrange
|
|
var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
|
|
{
|
|
[PrototypeAbilityRegistry.PrototypeBurst] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypeBurst,
|
|
"Prototype Burst",
|
|
40,
|
|
5.0,
|
|
6.0,
|
|
"attack"),
|
|
[PrototypeAbilityRegistry.PrototypeDash] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypeDash,
|
|
"Prototype Dash",
|
|
0,
|
|
4.0,
|
|
6.0,
|
|
"movement"),
|
|
[PrototypeAbilityRegistry.PrototypeGuard] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypeGuard,
|
|
"Prototype Guard",
|
|
0,
|
|
6.0,
|
|
6.0,
|
|
"utility"),
|
|
[PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
|
|
PrototypeAbilityRegistry.PrototypePulse,
|
|
"Prototype Pulse",
|
|
25,
|
|
3.0,
|
|
6.0,
|
|
"attack"),
|
|
};
|
|
var registry = CreateRegistryFromRows(rows);
|
|
// Act
|
|
var list = registry.GetDefinitionsInIdOrder();
|
|
// Assert
|
|
Assert.Equal(4, list.Count);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, list[0].Id);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypeDash, list[1].Id);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypeGuard, list[2].Id);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, list[3].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
|
|
{
|
|
// Arrange
|
|
var root = Directory.CreateTempSubdirectory("neon-sprawl-ability-registry-loader-");
|
|
try
|
|
{
|
|
var abilitiesDir = Path.Combine(root.FullName, "content", "abilities");
|
|
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
|
|
Directory.CreateDirectory(abilitiesDir);
|
|
Directory.CreateDirectory(schemaDir);
|
|
var schemaPath = Path.Combine(schemaDir, "ability-def.schema.json");
|
|
File.Copy(AbilityCatalogTestPaths.DiscoverRepoAbilityDefSchemaPath(), schemaPath, overwrite: true);
|
|
File.Copy(
|
|
Path.Combine(AbilityCatalogTestPaths.DiscoverRepoAbilitiesDirectory(), "prototype_abilities.json"),
|
|
Path.Combine(abilitiesDir, "prototype_abilities.json"),
|
|
overwrite: true);
|
|
File.Copy(
|
|
Path.Combine(AbilityCatalogTestPaths.DiscoverRepoAbilitiesDirectory(), "prototype_npc_abilities.json"),
|
|
Path.Combine(abilitiesDir, "prototype_npc_abilities.json"),
|
|
overwrite: true);
|
|
var loaded = AbilityDefinitionCatalogLoader.Load(abilitiesDir, schemaPath, NullLogger.Instance);
|
|
var registry = new AbilityDefinitionRegistry(loaded);
|
|
// Act
|
|
var pulseOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var pulse);
|
|
var guardOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeGuard, out var guard);
|
|
var dashOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeDash, out var dash);
|
|
var burstOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeBurst, out var burst);
|
|
var list = registry.GetDefinitionsInIdOrder();
|
|
// Assert
|
|
Assert.True(pulseOk);
|
|
Assert.NotNull(pulse);
|
|
Assert.Equal(25, pulse!.BaseDamage);
|
|
Assert.Equal(3.0, pulse.CooldownSeconds);
|
|
Assert.True(guardOk);
|
|
Assert.NotNull(guard);
|
|
Assert.Equal(0, guard!.BaseDamage);
|
|
Assert.Equal(6.0, guard.CooldownSeconds);
|
|
Assert.True(dashOk);
|
|
Assert.NotNull(dash);
|
|
Assert.Equal(0, dash!.BaseDamage);
|
|
Assert.Equal(4.0, dash.CooldownSeconds);
|
|
Assert.True(burstOk);
|
|
Assert.NotNull(burst);
|
|
Assert.Equal(40, burst!.BaseDamage);
|
|
Assert.Equal(5.0, burst.CooldownSeconds);
|
|
Assert.Equal(7, list.Count);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, list[0].Id);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, 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<IAbilityDefinitionRegistry>();
|
|
var pulseFound = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var pulse);
|
|
var unknown = registry.TryGetDefinition("not_a_real_ability", out var missing);
|
|
var normalized = registry.TryNormalizeKnown(" Prototype_Burst ", out var burstId);
|
|
var list = registry.GetDefinitionsInIdOrder();
|
|
// Assert
|
|
Assert.True(pulseFound);
|
|
Assert.NotNull(pulse);
|
|
Assert.Equal(25, pulse!.BaseDamage);
|
|
Assert.Equal(3.0, pulse.CooldownSeconds);
|
|
Assert.False(unknown);
|
|
Assert.Null(missing);
|
|
Assert.True(normalized);
|
|
Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, burstId);
|
|
Assert.Equal(7, list.Count);
|
|
Assert.Contains(list, a => a.Id == PrototypeAbilityRegistry.PrototypeGuard && a.BaseDamage == 0);
|
|
}
|
|
}
|