diff --git a/Assets/_Project/Tests/EditMode/EnemyAIMathTests.cs b/Assets/_Project/Tests/EditMode/EnemyAIMathTests.cs index 3ace23e07..0c984f84b 100644 --- a/Assets/_Project/Tests/EditMode/EnemyAIMathTests.cs +++ b/Assets/_Project/Tests/EditMode/EnemyAIMathTests.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using ProjectM.Simulation; using Unity.Mathematics; +using Unity.Collections; namespace ProjectM.Tests { @@ -111,5 +112,63 @@ namespace ProjectM.Tests Assert.AreEqual(v.x, slid.x, Eps); Assert.AreEqual(v.z, slid.z, Eps); } + // ---- EB-1 fortress aggro: PickWeightedNearest ---- + + [Test] + public void PickWeightedNearest_NoStructures_PicksNearestPlayer() + { + using var players = new NativeList(Allocator.Temp); + using var structs = new NativeList(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(Allocator.Temp); + using var structs = new NativeList(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(Allocator.Temp); + using var structs = new NativeList(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(Allocator.Temp); + using var structs = new NativeList(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(Allocator.Temp); + using var structs = new NativeList(Allocator.Temp); + EnemyAIMath.PickWeightedNearest(float3.zero, players, structs, 0.7f, out bool isStruct, out int idx); + Assert.AreEqual(-1, idx); + } + } } diff --git a/Assets/_Project/Tests/EditMode/HealthApplyDamageSystemTests.cs b/Assets/_Project/Tests/EditMode/HealthApplyDamageSystemTests.cs index 961513fd9..8bf426f5a 100644 --- a/Assets/_Project/Tests/EditMode/HealthApplyDamageSystemTests.cs +++ b/Assets/_Project/Tests/EditMode/HealthApplyDamageSystemTests.cs @@ -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(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(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(e).Current, 1e-4f, "No stats ceiling -> 100-35 = 65."); + } + } + } } diff --git a/Assets/_Project/Tests/EditMode/SavePersistenceTests.cs b/Assets/_Project/Tests/EditMode/SavePersistenceTests.cs index 5ce88e404..596338c3c 100644 --- a/Assets/_Project/Tests/EditMode/SavePersistenceTests.cs +++ b/Assets/_Project/Tests/EditMode/SavePersistenceTests.cs @@ -103,5 +103,44 @@ namespace ProjectM.Tests Assert.AreEqual(0, em.GetBuffer(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(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(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."); + } + } } diff --git a/Assets/_Project/Tests/EditMode/TuningConfigTests.cs b/Assets/_Project/Tests/EditMode/TuningConfigTests.cs index 123c59fff..a79532862 100644 --- a/Assets/_Project/Tests/EditMode/TuningConfigTests.cs +++ b/Assets/_Project/Tests/EditMode/TuningConfigTests.cs @@ -34,6 +34,7 @@ namespace ProjectM.Tests Assert.AreEqual(36f, d.ChargerWhiffStaggerTicks, 1e-6f, "ChargerWhiffStaggerTicks"); // 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(0.7f, d.StructureAggroWeight, 1e-6f, "EB-1 StructureAggroWeight default (<1 prefers structures)"); } [Test]