Warnings (9 to 0): events declared nullable, _cache nullable, IDamageModel param nullable. Dead code removed: DefaultFormations class + FormationTemplate; AlgorithmTypes legacy types (ThreatProfile/DefenseRecommendation/DefenseSolution/MultiGroupRecommendation/RecommendedPlatform/RecommendedDetection); DroneEntity CurrentWaypointIndex/FormationOffsetX/Y/ApplyFormationOffset/wind params; Vector3 operators+Length+DistanceTo; CloudExpansionModel Phase2Duration/TimeToDensity; Kinematics EstimatedShellTime/GaussianConcentration; InterceptCandidate write-only fields (CoverageDuration/FlightTime/ShellFlightTime); ParticleParams EmitRate/ColorHex. Naming: DefaultDefensePlanner -> DefensePlanner (only impl of IDefensePlanner); inCloudTime -> inCloudDistance (returns meters not seconds); DamageAssessment class summary fixed; wind comments clarified (drone wind removed vs cloud wind active). Tests: 191 pass, 0 warnings.
168 lines
7.1 KiB
C#
168 lines
7.1 KiB
C#
using System.Collections.Generic;
|
||
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<Waypoint> CreateRoute(float fromX, float toX, float alt, float speed)
|
||
{
|
||
return new List<Waypoint>
|
||
{
|
||
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<Waypoint>
|
||
{
|
||
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);
|
||
}
|
||
}
|
||
}
|