using System.Collections.Generic; using CounterDrone.Core.Algorithms; using CounterDrone.Core.Models; using CounterDrone.Core.Simulation; using Xunit; namespace CounterDrone.Core.Tests { public class DroneEntityTests { private TargetConfig CreateConfig(float speed = 120, float altitude = 300) { return new TargetConfig { TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston, TypicalSpeed = speed, TypicalAltitude = altitude, Wingspan = 2.5, Quantity = 1, }; } private List CreateRoute(float fromX, float toX, float alt, float speed) { return new List { new() { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, new() { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, }; } [Fact] public void InitialPosition_IsFirstWaypoint() { var drone = new DroneEntity("d1", "g1", CreateConfig(), CreateRoute(100, 500, 300, 120), 0, 50, 0, 0, FormationMode.Single); Assert.Equal(100, drone.PosX, 1); Assert.Equal(300, drone.PosY, 1); } [Fact] public void Update_MovesTowardTarget() { var drone = new DroneEntity("d1", "g1", CreateConfig(), CreateRoute(0, 1000, 300, 120), 0, 50, 0, 0, FormationMode.Single); float prevX = drone.PosX; drone.Update(1f); Assert.True(drone.PosX > prevX); } [Fact] public void Update_ReachesTarget_StatusChanges() { var drone = new DroneEntity("d1", "g1", CreateConfig(360), CreateRoute(0, 100, 300, 360), 0, 50, 0, 0, FormationMode.Single); drone.Update(2f); Assert.Equal(DroneStatus.ReachedTarget, drone.Status); } [Fact] public void Damage_ReducesHp() { var drone = new DroneEntity("d1", "g1", CreateConfig(), CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single); drone.ApplyDamage(0.4f); Assert.Equal(0.6f, drone.Hp, 0.01f); } [Fact] public void Damage_ExceedingHp_Destroys() { var drone = new DroneEntity("d1", "g1", CreateConfig(), CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single); drone.ApplyDamage(1.5f); Assert.True(drone.Hp <= 0); Assert.Equal(DroneStatus.Destroyed, drone.Status); } [Fact] public void Formation_Lateral_ZOffsetsDiffer() { var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 0, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 0, FormationMode.Formation); var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 0, 0, FormationMode.Formation); // 起始Z不同 Assert.NotEqual(d0.PosZ, d1.PosZ); Assert.NotEqual(d1.PosZ, d2.PosZ); // 航点Z不同(偏移持久化到航点) Assert.NotEqual(d0.Route[0].PosZ, d1.Route[0].PosZ); Assert.NotEqual(d0.Route[1].PosZ, d1.Route[1].PosZ); } [Fact] public void Formation_Longitudinal_XOffsetsDiffer() { var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 0, 0, 100, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 0, 1, 100, FormationMode.Formation); Assert.NotEqual(d0.PosX, d1.PosX); } [Fact] public void Formation_Mixed_2x2() { var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 100, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 100, FormationMode.Formation); var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 50, 1, 100, FormationMode.Formation); var d3 = new DroneEntity("d3", "g1", CreateConfig(), route, 1, 50, 1, 100, FormationMode.Formation); Assert.NotEqual(d0.PosZ, d1.PosZ); // lateral Assert.NotEqual(d0.PosX, d2.PosX); // longitudinal } [Fact] public void Swarm_ProducesRandomOffsets() { var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 30, 0, 0, FormationMode.Swarm); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 30, 0, 0, FormationMode.Swarm); Assert.True(d0.PosX != d1.PosX || d0.PosZ != d1.PosZ); } // ═══════════════════════════════════════ // RouteGeometry 驱动的多 waypoint 折线运动 // 验证无人机沿 L 形航路(X 段→Z 段)正确移动并最终到达终点 // ═══════════════════════════════════════ [Fact] public void MultiWaypoint_LShape_MovesAlongBothSegments() { // L 形航路:(0,0) → (100,0) → (100,100),总长 200m,速度 60km/h≈16.67m/s var route = new List { new() { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, new() { PosX = 100, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, new() { PosX = 100, PosY = 300, PosZ = 100, Altitude = 300, Speed = 60 }, }; var drone = new DroneEntity("d1", "g1", CreateConfig(speed: 60), route, 0, 0, 0, 0, FormationMode.Single); // 推进到第一段中点(弧长 50):应在 X=50, Z=0 drone.Update(50f / (60f / 3.6f)); Assert.Equal(50f, drone.PosX, 1); Assert.Equal(0f, drone.PosZ, 1); Assert.Equal(DroneStatus.Flying, drone.Status); // 推进到拐点(弧长 100):应在 X=100, Z=0 drone.Update(50f / (60f / 3.6f)); Assert.Equal(100f, drone.PosX, 1); Assert.Equal(0f, drone.PosZ, 1); Assert.Equal(DroneStatus.Flying, drone.Status); // 推进到第二段中点(弧长 150):应在 X=100, Z=50 drone.Update(50f / (60f / 3.6f)); Assert.Equal(100f, drone.PosX, 1); Assert.Equal(50f, drone.PosZ, 1); Assert.Equal(DroneStatus.Flying, drone.Status); // 推进到终点(弧长 200):应在 X=100, Z=100,状态 ReachedTarget drone.Update(50f / (60f / 3.6f)); Assert.Equal(100f, drone.PosX, 1); Assert.Equal(100f, drone.PosZ, 1); Assert.Equal(DroneStatus.ReachedTarget, drone.Status); } // ═══════════════════════════════════════ // 弧长缓存一致性:多段航路上非整段边界处的位置必须与 RouteGeometry.PositionAt 精确一致 // 防止 Update() 内段长缓存引入的数值偏差 // ═══════════════════════════════════════ [Fact] public void MultiWaypoint_SubSegmentPositions_MatchGeometry() { // 不规则三段折线,段长各异(50, 120, 80),避免整段边界凑巧对齐 var route = new List { new() { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, new() { PosX = 50, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, // 段1: 50m new() { PosX = 50, PosY = 300, PosZ = 120, Altitude = 300, Speed = 60 }, // 段2: 120m new() { PosX = 130, PosY = 300, PosZ = 120, Altitude = 300, Speed = 60 }, // 段3: 80m }; const float speed = 60f; float speedMs = speed / 3.6f; // 取若干非整段边界的弧长采样点,验证每一步位置 = RouteGeometry.PositionAt float[] sampleArcs = { 17f, 50f, 73f, 170f, 200f, 249f }; foreach (var arc in sampleArcs) { var drone = new DroneEntity("d", "g", CreateConfig(speed), route, 0, 0, 0, 0, FormationMode.Single); drone.Update(arc / speedMs); var (ex, ey, ez) = RouteGeometry.PositionAt(route, arc); Assert.True(System.Math.Abs(drone.PosX - ex) < 0.5f, $"arc={arc}: X {drone.PosX} vs {ex}"); Assert.True(System.Math.Abs(drone.PosY - ey) < 0.5f, $"arc={arc}: Y {drone.PosY} vs {ey}"); Assert.True(System.Math.Abs(drone.PosZ - ez) < 0.5f, $"arc={arc}: Z {drone.PosZ} vs {ez}"); } } } }