Tests: EB-1 structure damage/death, fortress targeting, persistence v3
10 new EditMode tests (312 total, all green): HealthApplyDamage destroys a Destructible at 0 + a wounded structure survives clamped; PickWeightedNearest x5 (player-only, structure-preferred-by-weight, player-in-the-way wins, raze undefended base, no targets); persistence (StructureSave.HP round-trip + writes v3, v2 in the load floor, SaveApply.ToPending maps the wounded HP - the staging-copy bug the pre-code review caught); + the StructureAggroWeight default pin. See DR-032. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using ProjectM.Simulation;
|
using ProjectM.Simulation;
|
||||||
using Unity.Mathematics;
|
using Unity.Mathematics;
|
||||||
|
using Unity.Collections;
|
||||||
|
|
||||||
namespace ProjectM.Tests
|
namespace ProjectM.Tests
|
||||||
{
|
{
|
||||||
@@ -111,5 +112,63 @@ namespace ProjectM.Tests
|
|||||||
Assert.AreEqual(v.x, slid.x, Eps);
|
Assert.AreEqual(v.x, slid.x, Eps);
|
||||||
Assert.AreEqual(v.z, slid.z, Eps);
|
Assert.AreEqual(v.z, slid.z, Eps);
|
||||||
}
|
}
|
||||||
|
// ---- EB-1 fortress aggro: PickWeightedNearest ----
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PickWeightedNearest_NoStructures_PicksNearestPlayer()
|
||||||
|
{
|
||||||
|
using var players = new NativeList<float3>(Allocator.Temp);
|
||||||
|
using var structs = new NativeList<float3>(Allocator.Temp);
|
||||||
|
players.Add(new float3(10, 0, 0));
|
||||||
|
players.Add(new float3(3, 0, 0));
|
||||||
|
EnemyAIMath.PickWeightedNearest(float3.zero, players, structs, 0.7f, out bool isStruct, out int idx);
|
||||||
|
Assert.IsFalse(isStruct);
|
||||||
|
Assert.AreEqual(1, idx, "nearest player is index 1 (dist 3)");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PickWeightedNearest_PrefersStructure_WhenWeightShrinksItsDistance()
|
||||||
|
{
|
||||||
|
using var players = new NativeList<float3>(Allocator.Temp);
|
||||||
|
using var structs = new NativeList<float3>(Allocator.Temp);
|
||||||
|
players.Add(new float3(8, 0, 0)); // effective 8
|
||||||
|
structs.Add(new float3(10, 0, 0)); // 10 * weight 0.5 = effective 5 < 8
|
||||||
|
EnemyAIMath.PickWeightedNearest(float3.zero, players, structs, 0.5f, out bool isStruct, out int idx);
|
||||||
|
Assert.IsTrue(isStruct, "a weighted structure (10*0.5=5) beats a player at 8 -> Husks push for structures");
|
||||||
|
Assert.AreEqual(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PickWeightedNearest_PlayerInTheWay_WinsOverWeightedStructure()
|
||||||
|
{
|
||||||
|
using var players = new NativeList<float3>(Allocator.Temp);
|
||||||
|
using var structs = new NativeList<float3>(Allocator.Temp);
|
||||||
|
players.Add(new float3(2, 0, 0)); // a player right in the way (dist 2)
|
||||||
|
structs.Add(new float3(10, 0, 0)); // 10 * 0.7 = effective 7 > 2
|
||||||
|
EnemyAIMath.PickWeightedNearest(float3.zero, players, structs, 0.7f, out bool isStruct, out int idx);
|
||||||
|
Assert.IsFalse(isStruct, "a player closer than the weighted structure distance wins (attack the one in the way)");
|
||||||
|
Assert.AreEqual(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PickWeightedNearest_OnlyStructures_RazesTheUndefendedBase()
|
||||||
|
{
|
||||||
|
using var players = new NativeList<float3>(Allocator.Temp);
|
||||||
|
using var structs = new NativeList<float3>(Allocator.Temp);
|
||||||
|
structs.Add(new float3(0, 0, 12));
|
||||||
|
EnemyAIMath.PickWeightedNearest(float3.zero, players, structs, 0.7f, out bool isStruct, out int idx);
|
||||||
|
Assert.IsTrue(isStruct, "with no players, a Husk targets a structure (razes the undefended base)");
|
||||||
|
Assert.AreEqual(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PickWeightedNearest_NoTargets_ReturnsMinusOne()
|
||||||
|
{
|
||||||
|
using var players = new NativeList<float3>(Allocator.Temp);
|
||||||
|
using var structs = new NativeList<float3>(Allocator.Temp);
|
||||||
|
EnemyAIMath.PickWeightedNearest(float3.zero, players, structs, 0.7f, out bool isStruct, out int idx);
|
||||||
|
Assert.AreEqual(-1, idx);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,5 +154,45 @@ namespace ProjectM.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Lethal_Damage_Destroys_A_Destructible_Structure()
|
||||||
|
{
|
||||||
|
var (world, group) = MakeWorld("HealthApplyDamageDestructibleWorld");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
// EB-1: a structure = Health + DamageEvent + Destructible, NO EffectiveCharacterStats (clamps to 0).
|
||||||
|
var e = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(Destructible));
|
||||||
|
em.SetComponentData(e, new Health { Current = 100f, Max = 100f });
|
||||||
|
var dmg = em.GetBuffer<DamageEvent>(e);
|
||||||
|
dmg.Add(new DamageEvent { Amount = 60f, SourceNetworkId = -1 });
|
||||||
|
dmg.Add(new DamageEvent { Amount = 60f, SourceNetworkId = -1 }); // two Husk hits SUM (120 > 100)
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.IsFalse(em.Exists(e), "A Destructible at <=0 is destroyed exactly once (summed multi-hit, one DestroyEntity).");
|
||||||
|
using var q = em.CreateEntityQuery(typeof(Destructible));
|
||||||
|
Assert.AreEqual(0, q.CalculateEntityCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Wounded_Destructible_Survives_And_Clamps_To_Zero_Floor()
|
||||||
|
{
|
||||||
|
var (world, group) = MakeWorld("HealthApplyDamageWoundedStructureWorld");
|
||||||
|
using (world)
|
||||||
|
{
|
||||||
|
var em = world.EntityManager;
|
||||||
|
var e = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(Destructible));
|
||||||
|
em.SetComponentData(e, new Health { Current = 100f, Max = 100f });
|
||||||
|
em.GetBuffer<DamageEvent>(e).Add(new DamageEvent { Amount = 35f, SourceNetworkId = -1 });
|
||||||
|
|
||||||
|
group.Update();
|
||||||
|
|
||||||
|
Assert.IsTrue(em.Exists(e), "A non-lethally-hit structure survives WOUNDED (the persisted state).");
|
||||||
|
Assert.AreEqual(65f, em.GetComponentData<Health>(e).Current, 1e-4f, "No stats ceiling -> 100-35 = 65.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,5 +103,44 @@ namespace ProjectM.Tests
|
|||||||
|
|
||||||
Assert.AreEqual(0, em.GetBuffer<StorageEntry>(e).Length);
|
Assert.AreEqual(0, em.GetBuffer<StorageEntry>(e).Length);
|
||||||
}
|
}
|
||||||
|
[Test]
|
||||||
|
public void StructureSave_HP_RoundTrips_And_Writes_V3()
|
||||||
|
{
|
||||||
|
var data = new SaveData { Structures = new[] { new StructureSave { Type = 1, CellX = 1, CellZ = 2, HP = 37f } } };
|
||||||
|
var back = JsonUtility.FromJson<SaveData>(JsonUtility.ToJson(data));
|
||||||
|
Assert.AreEqual(3, back.Version, "EB-1: new saves write v3.");
|
||||||
|
Assert.AreEqual(1, back.Structures.Length);
|
||||||
|
Assert.AreEqual(37f, back.Structures[0].HP, 1e-4f, "the wounded HP round-trips through JSON.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void V2_Save_IsWithinLoadableRange_And_ZeroHp_Restores_Full()
|
||||||
|
{
|
||||||
|
// A pre-EB-1 v2 save sits inside the additive load floor [Min,Current], so SaveService.Load accepts it;
|
||||||
|
// an unset HP (0) is mapped by BaseRestoreSystem to the baked Max (structures come back at full HP).
|
||||||
|
var v2 = new SaveData { Version = 2, GoalCharge = 3, GoalTarget = 10, Structures = new[] { new StructureSave { Type = 1, CellX = 2, CellZ = 4 } } };
|
||||||
|
var back = JsonUtility.FromJson<SaveData>(JsonUtility.ToJson(v2));
|
||||||
|
Assert.AreEqual(2, back.Version);
|
||||||
|
Assert.GreaterOrEqual(back.Version, SaveData.MinLoadableVersion, "v2 is at/above the load floor.");
|
||||||
|
Assert.LessOrEqual(back.Version, SaveData.CurrentVersion);
|
||||||
|
Assert.AreEqual(0f, back.Structures[0].HP, 1e-4f, "unset HP (0) -> restore maps to baked Max.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ToPending_Maps_All_Fields_Including_The_Wounded_HP()
|
||||||
|
{
|
||||||
|
// The WorldLauncher save->stage copy: omitting any field here silently restores at full HP (review-caught).
|
||||||
|
var s = new StructureSave { Type = 1, CellX = 3, CellZ = -2, Direction = 2, RemainingTicks = 50, ConveyorResId = 1, ConveyorCount = 4, HP = 37f };
|
||||||
|
var p = SaveApply.ToPending(s);
|
||||||
|
Assert.AreEqual(1, p.Type);
|
||||||
|
Assert.AreEqual(3, p.CellX);
|
||||||
|
Assert.AreEqual(-2, p.CellZ);
|
||||||
|
Assert.AreEqual(2, p.Direction);
|
||||||
|
Assert.AreEqual(50u, p.RemainingTicks);
|
||||||
|
Assert.AreEqual(1, p.ConveyorResId);
|
||||||
|
Assert.AreEqual(4, p.ConveyorCount);
|
||||||
|
Assert.AreEqual(37f, p.HP, 1e-4f, "the wounded HP survives the save->staging copy.");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace ProjectM.Tests
|
|||||||
Assert.AreEqual(36f, d.ChargerWhiffStaggerTicks, 1e-6f, "ChargerWhiffStaggerTicks");
|
Assert.AreEqual(36f, d.ChargerWhiffStaggerTicks, 1e-6f, "ChargerWhiffStaggerTicks");
|
||||||
// GruntWindup must stay the canonical Tuning const (TelegraphTests couples to it).
|
// GruntWindup must stay the canonical Tuning const (TelegraphTests couples to it).
|
||||||
Assert.AreEqual((float)Tuning.AttackWindupTicks, d.GruntWindupTicks, 1e-6f, "GruntWindupTicks == Tuning.AttackWindupTicks");
|
Assert.AreEqual((float)Tuning.AttackWindupTicks, d.GruntWindupTicks, 1e-6f, "GruntWindupTicks == Tuning.AttackWindupTicks");
|
||||||
|
Assert.AreEqual(0.7f, d.StructureAggroWeight, 1e-6f, "EB-1 StructureAggroWeight default (<1 prefers structures)");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user