Aetheria/tests/Aetheria.Shared.Tests/SemiAutoCatalogTests.cs
soul d2c293b799 Test: the semi-auto floor is the 0.1 s constant, not a multiple of the step
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 13:27:54 +02:00

99 lines
6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using Xunit;
// Fire mode is a design property (ruling fire-mode-is-the-design): a plain InstantWeapon is semi-automatic, and its cycle
// stays at a pace a finger can keep. A gun that cycles faster is an AutoWeapon.
public sealed class SemiAutoCatalogTests
{
private static PerformanceStat Const(float v) => new PerformanceStat { Min = v, Max = v };
private static GearData Gun(string name, InstantWeaponData data) => new GearData { Name = name, Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1, Behaviors = { data } };
[Fact]
public void TheShippedCatalogAndItsPackagesHoldNoFasterPlainGun()
{
var root = RunStartTests.FindRepoRoot();
using var work = new TempDirectory();
var (catalog, excluded) = ShipModCatalog.ResolveCatalog(Path.Combine(root, "GameData", "Aetheria.cc"), Path.Combine(work.Path, "derived.cc"),
Path.Combine(root, "GameData", "Mods"), TestCatalog.Registry());
Assert.Empty(excluded);
using var cache = new CultCache(TestCatalog.Registry());
cache.AddBackingStore(new SingleFileMessagePackBackingStore(catalog, true), AetheriaStores.CatalogTypes);
var gear = cache.GetAll<GearData>().ToArray();
Assert.Empty(InstantWeaponData.SemiAutoFaults(cache.GetAll<ItemData>()));
// The check reads a catalog that holds guns, and the two automatic ones are AutoWeapons.
foreach (var name in new[] { "Slug Rifle", "Long Slug Rifle" })
Assert.IsType<AutoWeaponData>(gear.Single(g => g.Name == name).Behaviors.Single(b => b is InstantWeaponData));
Assert.Contains(gear, g => g.Behaviors.Any(b => b is InstantWeaponData));
}
[Fact]
public void ThePlainGunCheckRefusesAFastSemiAutoAndOnlyThat()
{
var floor = InstantWeaponData.ShortestSemiAutoCooldown;
var fast = Gun("Fast", new InstantWeaponData { Cooldown = Const(floor / 2) });
var edge = Gun("Edge", new InstantWeaponData { Cooldown = Const(floor) });
var auto = Gun("Auto", new AutoWeaponData { Cooldown = Const(.01f) });
var wide = Gun("Wide", new InstantWeaponData { Cooldown = new PerformanceStat { Min = floor / 2, Max = floor * 2 } });
var faults = InstantWeaponData.SemiAutoFaults(new[] { fast, edge, auto, wide }).ToArray();
// The floor is 0.1 s: ten presses a second is allowed, twenty is not.
Assert.Equal(.1f, floor);
Assert.Single(InstantWeaponData.SemiAutoFaults(new[] { Gun("Twenty", new InstantWeaponData { Cooldown = Const(.05f) }) }));
Assert.Empty(InstantWeaponData.SemiAutoFaults(new[] { Gun("Ten", new InstantWeaponData { Cooldown = Const(.1f) }) }));
Assert.Equal(2, faults.Length);
Assert.Contains(faults, f => f.StartsWith("Fast:"));
Assert.Contains(faults, f => f.StartsWith("Wide:"));
}
// Fire mode is one property of the weapon's data: every kind but AutoWeaponData is semi-automatic, and the floor holds each of
// them, as gear or as a consumable. An automatic gun is exempt wherever it is carried.
public static IEnumerable<object[]> Kinds() => new[]
{
new object[] { "plain", (Func<InstantWeaponData>) (() => new InstantWeaponData()) },
new object[] { "guided", (Func<InstantWeaponData>) (() => new GuidedWeaponData()) },
new object[] { "mine", (Func<InstantWeaponData>) (() => new MineLayerData()) },
new object[] { "launcher", (Func<InstantWeaponData>) (() => new LauncherData()) },
new object[] { "charged", (Func<InstantWeaponData>) (() => new ChargedWeaponData { CanFireEarly = true }) },
};
[Theory, MemberData(nameof(Kinds))]
public void TheFloorRefusesEverySemiAutoKindBelowItAsGearOrConsumable(string kind, Func<InstantWeaponData> make)
{
InstantWeaponData At(float cooldown) { var data = make(); data.Cooldown = Const(cooldown); return data; }
Assert.False(make().Automatic);
var floor = InstantWeaponData.ShortestSemiAutoCooldown;
var slow = At(floor);
var fast = At(floor / 2);
Assert.Empty(InstantWeaponData.SemiAutoFaults(new ItemData[] { Gun(kind, slow), new ConsumableItemData { Name = kind + " dose", Behaviors = { slow } } }));
var faults = InstantWeaponData.SemiAutoFaults(new ItemData[] { Gun(kind, fast), new ConsumableItemData { Name = kind + " dose", Behaviors = { fast } } }).ToArray();
Assert.Equal(2, faults.Length);
Assert.Contains(faults, f => f.StartsWith(kind + ":"));
Assert.Contains(faults, f => f.StartsWith(kind + " dose:"));
}
[Fact]
public void AnAutomaticGunIsExemptAsGearOrConsumable()
{
var auto = new AutoWeaponData { Cooldown = Const(.01f) };
Assert.True(auto.Automatic);
Assert.Empty(InstantWeaponData.SemiAutoFaults(new ItemData[] { Gun("Auto", auto), new ConsumableItemData { Name = "Auto dose", Behaviors = { auto } } }));
}
// The floor is the 0.1 s constant itself, not a function of the tick: a 0.08 s semi-auto gun is refused at a step length
// whose own multiple (2 x GameStep at 30 Hz is 0.067 s) would let it through, and refused at the constant's edge from
// below however the step moves. Kills: SemiAutoFaults comparing against a step-derived value.
[Fact]
public void TheFloorIsTheConstantNotAMultipleOfTheStep()
{
Assert.NotEqual(InstantWeaponData.ShortestSemiAutoCooldown, SimClock.GameStep * 2f);
Assert.True(SimClock.GameStep * 2f < .08f, "fixture: a step-derived floor would let 0.08 s through");
Assert.Single(InstantWeaponData.SemiAutoFaults(new[] { Gun("Eighty", new InstantWeaponData { Cooldown = Const(.08f) }) }));
Assert.Single(InstantWeaponData.SemiAutoFaults(new[] { Gun("Just under", new InstantWeaponData { Cooldown = Const(.0999f) }) }));
Assert.Empty(InstantWeaponData.SemiAutoFaults(new[] { Gun("Just over", new InstantWeaponData { Cooldown = Const(.1001f) }) }));
}
}