Phase 4 重构: 仿真引擎改为纯执行器 + Kinematics工具类 + 4个端到端场景(2个通过)
This commit is contained in:
parent
3dd0ea6cc9
commit
2ef16d083c
@ -29,8 +29,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
IsDissipated = false;
|
||||
|
||||
// 风速 → 速度矢量
|
||||
var dir = WindToVector((WindDirection)env.WindDirection);
|
||||
_windVelocity = dir * (float)env.WindSpeed;
|
||||
var (vx, vy, vz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
|
||||
_windVelocity = new Algorithms.Vector3(vx, vy, vz);
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime, float windSpeed, WindDirection windDir)
|
||||
@ -40,8 +40,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
_elapsed += deltaTime;
|
||||
|
||||
// 更新风速(允许动态变化)
|
||||
var dir = WindToVector(windDir);
|
||||
_windVelocity = dir * windSpeed;
|
||||
var (vx, vy, vz) = Kinematics.WindToVector(windDir, windSpeed);
|
||||
_windVelocity = new Algorithms.Vector3(vx, vy, vz);
|
||||
|
||||
// 半径膨胀:初始半径 + 扩散速率 × 时间
|
||||
// 加入大气稳定度因子(简化:1.0)
|
||||
@ -69,23 +69,5 @@ namespace CounterDrone.Core.Algorithms
|
||||
Particles.Opacity = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 WindToVector(WindDirection dir)
|
||||
{
|
||||
var angle = dir switch
|
||||
{
|
||||
WindDirection.N => 0f,
|
||||
WindDirection.NE => 45f,
|
||||
WindDirection.E => 90f,
|
||||
WindDirection.SE => 135f,
|
||||
WindDirection.S => 180f,
|
||||
WindDirection.SW => 225f,
|
||||
WindDirection.W => 270f,
|
||||
WindDirection.NW => 315f,
|
||||
_ => 0f,
|
||||
};
|
||||
var rad = angle * (float)System.Math.PI / 180f;
|
||||
return new Vector3((float)System.Math.Sin(rad), 0, (float)System.Math.Cos(rad));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
105
src/CounterDrone.Core/Algorithms/Kinematics.cs
Normal file
105
src/CounterDrone.Core/Algorithms/Kinematics.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>通用运动学 / 几何工具</summary>
|
||||
public static class Kinematics
|
||||
{
|
||||
/// <summary>风向 → 单位速度矢量 (X, 0, Z),右手系 Y-up</summary>
|
||||
public static (float X, float Y, float Z) WindToVector(WindDirection dir, float speed)
|
||||
{
|
||||
var angle = dir switch
|
||||
{
|
||||
WindDirection.N => 0f,
|
||||
WindDirection.NE => 45f,
|
||||
WindDirection.E => 90f,
|
||||
WindDirection.SE => 135f,
|
||||
WindDirection.S => 180f,
|
||||
WindDirection.SW => 225f,
|
||||
WindDirection.W => 270f,
|
||||
WindDirection.NW => 315f,
|
||||
_ => 0f,
|
||||
};
|
||||
var rad = angle * (float)Math.PI / 180f;
|
||||
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
|
||||
}
|
||||
|
||||
/// <summary>计算抛物线炮弹的发射角</summary>
|
||||
/// <param name="range">水平距离 (m)</param>
|
||||
/// <param name="muzzleVelocity">初速 (m/s)</param>
|
||||
/// <param name="releaseAltitude">释放高度 (m),弹道顶点必须 ≥ 此值</param>
|
||||
/// <returns>发射角 (rad),取低弹道</returns>
|
||||
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
|
||||
{
|
||||
var v2 = muzzleVelocity * muzzleVelocity;
|
||||
var g = 9.81f;
|
||||
|
||||
// 1. 射程所需角:θr = arcsin(R*g/v²) / 2
|
||||
var rangeRatio = range * g / v2;
|
||||
var angleRange = rangeRatio >= 1.0f
|
||||
? 45f * (float)Math.PI / 180f
|
||||
: (float)Math.Asin(rangeRatio) / 2f;
|
||||
|
||||
// 2. 释放高度所需最小角:H = v²*sin²(θ)/(2g) → sin(θ) = √(2gH)/v
|
||||
var sinMinHeight = (float)Math.Sqrt(2f * g * releaseAltitude * 1.05f) / muzzleVelocity;
|
||||
var angleHeight = sinMinHeight >= 1.0f
|
||||
? 90f * (float)Math.PI / 180f
|
||||
: (float)Math.Asin(sinMinHeight);
|
||||
|
||||
return Math.Max(angleRange, angleHeight);
|
||||
}
|
||||
|
||||
/// <summary>根据发射参数计算抛物线位置</summary>
|
||||
public static (float X, float Y, float Z) ParabolicPosition(
|
||||
float startX, float startY, float startZ,
|
||||
float launchAngle, float azimuth,
|
||||
float muzzleVelocity, float time)
|
||||
{
|
||||
var cosA = (float)Math.Cos(launchAngle);
|
||||
var sinA = (float)Math.Sin(launchAngle);
|
||||
var g = 9.81f;
|
||||
|
||||
var horizontalDist = muzzleVelocity * cosA * time;
|
||||
var x = startX + horizontalDist * (float)Math.Sin(azimuth);
|
||||
var z = startZ + horizontalDist * (float)Math.Cos(azimuth);
|
||||
var y = startY + muzzleVelocity * sinA * time - 0.5f * g * time * time;
|
||||
|
||||
return (x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>点到矩形距离(简化判定)</summary>
|
||||
public static float Distance2D(float x1, float z1, float x2, float z2)
|
||||
{
|
||||
var dx = x2 - x1;
|
||||
var dz = z2 - z1;
|
||||
return (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
/// <summary>点到三维点距离</summary>
|
||||
public static float Distance3D(float x1, float y1, float z1, float x2, float y2, float z2)
|
||||
{
|
||||
var dx = x2 - x1;
|
||||
var dy = y2 - y1;
|
||||
var dz = z2 - z1;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/// <summary>射线法 — 点是否在水平多边形内</summary>
|
||||
public static bool PointInPolygon(float px, float pz, ReadOnlySpan<(float X, float Z)> vertices)
|
||||
{
|
||||
if (vertices.Length < 3) return false;
|
||||
bool inside = false;
|
||||
int j = vertices.Length - 1;
|
||||
for (int i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
var xi = vertices[i].X; var zi = vertices[i].Z;
|
||||
var xj = vertices[j].X; var zj = vertices[j].Z;
|
||||
if ((zi > pz) != (zj > pz) && px < (xj - xi) * (pz - zi) / (zj - zi) + xi)
|
||||
inside = !inside;
|
||||
j = i;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CounterDrone.Core.Simulation
|
||||
{
|
||||
public class ControlZone
|
||||
public class ControlZoneEntity
|
||||
{
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
@ -12,9 +13,9 @@ namespace CounterDrone.Core.Simulation
|
||||
public float MinAltitude { get; }
|
||||
public float MaxAltitude { get; }
|
||||
|
||||
public struct Vector3 { public float X, Y, Z; }
|
||||
public struct Vector3 { public float X { get; set; } public float Y { get; set; } public float Z { get; set; } }
|
||||
|
||||
public ControlZone(string id, string name, string verticesJson, float minAlt, float maxAlt)
|
||||
public ControlZoneEntity(string id, string name, string verticesJson, float minAlt, float maxAlt)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
@ -25,24 +26,13 @@ namespace CounterDrone.Core.Simulation
|
||||
|
||||
public bool ContainsPoint(float x, float y, float z)
|
||||
{
|
||||
// 高度检查
|
||||
if (y < MinAltitude || y > MaxAltitude) return false;
|
||||
|
||||
// Point-in-Polygon(射线法)
|
||||
if (Vertices.Count < 3) return false;
|
||||
|
||||
bool inside = false;
|
||||
int j = Vertices.Count - 1;
|
||||
var vertices2D = new (float X, float Z)[Vertices.Count];
|
||||
for (int i = 0; i < Vertices.Count; i++)
|
||||
{
|
||||
var xi = Vertices[i].X; var zi = Vertices[i].Z;
|
||||
var xj = Vertices[j].X; var zj = Vertices[j].Z;
|
||||
vertices2D[i] = (Vertices[i].X, Vertices[i].Z);
|
||||
|
||||
if ((zi > z) != (zj > z) && x < (xj - xi) * (z - zi) / (zj - zi) + xi)
|
||||
inside = !inside;
|
||||
j = i;
|
||||
}
|
||||
return inside;
|
||||
return Kinematics.PointInPolygon(x, z, vertices2D);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Simulation
|
||||
@ -42,6 +43,8 @@ namespace CounterDrone.Core.Simulation
|
||||
|
||||
// 编队偏移
|
||||
ApplyFormationOffset(formationIndex, spacing, mode);
|
||||
PosX += FormationOffsetX;
|
||||
PosY += FormationOffsetY;
|
||||
}
|
||||
|
||||
private void ApplyFormationOffset(int index, float spacing, FormationMode mode)
|
||||
@ -77,25 +80,30 @@ namespace CounterDrone.Core.Simulation
|
||||
var dy = (float)(target.PosY - PosY);
|
||||
var dz = (float)(target.PosZ - PosZ);
|
||||
var dist = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
var step = speedMs * deltaTime;
|
||||
|
||||
if (dist < 1.0f)
|
||||
if (dist < 1.0f || step >= dist)
|
||||
{
|
||||
PosX = (float)target.PosX;
|
||||
PosY = (float)target.PosY;
|
||||
PosZ = (float)target.PosZ;
|
||||
CurrentWaypointIndex++;
|
||||
if (CurrentWaypointIndex >= Route.Count - 1)
|
||||
Status = DroneStatus.ReachedTarget;
|
||||
return;
|
||||
}
|
||||
|
||||
// 移动方向
|
||||
var step = speedMs * deltaTime;
|
||||
var ratio = step / dist;
|
||||
PosX += dx * ratio;
|
||||
PosY += dy * ratio;
|
||||
PosZ += dz * ratio;
|
||||
|
||||
// 风偏
|
||||
var windVec = WindToVector(windDir, windSpeed);
|
||||
PosX += windVec.X * deltaTime;
|
||||
PosY += windVec.Y * deltaTime;
|
||||
PosZ += windVec.Z * deltaTime;
|
||||
(float wx, float wy, float wz) = Kinematics.WindToVector(windDir, windSpeed);
|
||||
PosX += wx * deltaTime;
|
||||
PosY += wy * deltaTime;
|
||||
PosZ += wz * deltaTime;
|
||||
}
|
||||
|
||||
public void ApplyDamage(float damage)
|
||||
@ -113,23 +121,5 @@ namespace CounterDrone.Core.Simulation
|
||||
{
|
||||
Status = DroneStatus.ZoneIntruded;
|
||||
}
|
||||
|
||||
private static (float X, float Y, float Z) WindToVector(WindDirection dir, float speed)
|
||||
{
|
||||
var angle = dir switch
|
||||
{
|
||||
WindDirection.N => 0f,
|
||||
WindDirection.NE => 45f,
|
||||
WindDirection.E => 90f,
|
||||
WindDirection.SE => 135f,
|
||||
WindDirection.S => 180f,
|
||||
WindDirection.SW => 225f,
|
||||
WindDirection.W => 270f,
|
||||
WindDirection.NW => 315f,
|
||||
_ => 0f,
|
||||
};
|
||||
var rad = angle * (float)Math.PI / 180f;
|
||||
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Simulation
|
||||
@ -22,6 +23,8 @@ namespace CounterDrone.Core.Simulation
|
||||
private readonly float _azimuth;
|
||||
private readonly float _startX, _startY, _startZ;
|
||||
|
||||
private bool _hasExceededReleaseAltitude;
|
||||
|
||||
public MunitionEntity(string id, PlatformType launchMode, AerosolType aerosolType,
|
||||
float startX, float startY, float startZ,
|
||||
float targetX, float targetY, float targetZ,
|
||||
@ -36,12 +39,13 @@ namespace CounterDrone.Core.Simulation
|
||||
_muzzleVelocity = muzzleVelocity;
|
||||
ReleaseAltitude = releaseAltitude;
|
||||
|
||||
// 计算发射角(简化抛物线)
|
||||
// 根据目标距离反算发射角(取低弹道)
|
||||
var dx = targetX - startX;
|
||||
var dz = targetZ - startZ;
|
||||
var horizontalDist = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||||
_azimuth = horizontalDist > 0 ? (float)System.Math.Atan2(dx, dz) : 0;
|
||||
_launchAngle = 45f * (float)System.Math.PI / 180f;
|
||||
_azimuth = horizontalDist > 0.001f ? (float)System.Math.Atan2(dx, dz) : 0;
|
||||
|
||||
_launchAngle = Kinematics.CalculateLaunchAngle(horizontalDist, muzzleVelocity, releaseAltitude);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
@ -51,17 +55,15 @@ namespace CounterDrone.Core.Simulation
|
||||
|
||||
if (LaunchMode == PlatformType.GroundBased)
|
||||
{
|
||||
// 抛物线弹道
|
||||
var v0 = _muzzleVelocity;
|
||||
var cosA = (float)System.Math.Cos(_launchAngle);
|
||||
var sinA = (float)System.Math.Sin(_launchAngle);
|
||||
(float x, float y, float z) = Kinematics.ParabolicPosition(
|
||||
_startX, _startY, _startZ, _launchAngle, _azimuth, _muzzleVelocity, _time);
|
||||
PosX = x; PosY = y; PosZ = z;
|
||||
|
||||
var horizontalDist = v0 * cosA * _time;
|
||||
PosX = _startX + horizontalDist * (float)System.Math.Sin(_azimuth);
|
||||
PosZ = _startZ + horizontalDist * (float)System.Math.Cos(_azimuth);
|
||||
PosY = _startY + v0 * sinA * _time - 0.5f * 9.81f * _time * _time;
|
||||
// 必须先从上方越过释放高度,再回落时才触发
|
||||
if (PosY > ReleaseAltitude)
|
||||
_hasExceededReleaseAltitude = true;
|
||||
|
||||
if (PosY <= ReleaseAltitude)
|
||||
if (_hasExceededReleaseAltitude && PosY <= ReleaseAltitude)
|
||||
{
|
||||
PosY = ReleaseAltitude;
|
||||
HasArrived = true;
|
||||
|
||||
@ -4,16 +4,12 @@ namespace CounterDrone.Core.Simulation
|
||||
{
|
||||
public enum SimulationState
|
||||
{
|
||||
Idle,
|
||||
Running,
|
||||
Paused,
|
||||
Stopped,
|
||||
Completed,
|
||||
Idle, Running, Paused, Stopped, Completed,
|
||||
}
|
||||
|
||||
public class SimEvent
|
||||
{
|
||||
public SimEventType Type { get; set; }
|
||||
public Models.SimEventType Type { get; set; }
|
||||
public float OccurredAt { get; set; }
|
||||
public string SourceId { get; set; } = string.Empty;
|
||||
public string TargetId { get; set; } = string.Empty;
|
||||
@ -21,23 +17,9 @@ namespace CounterDrone.Core.Simulation
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public enum SimEventType
|
||||
{
|
||||
TargetDetected,
|
||||
MunitionLaunched,
|
||||
CloudGenerated,
|
||||
DroneEnteredCloud,
|
||||
DroneDestroyed,
|
||||
DroneReachedTarget,
|
||||
ZoneIntruded,
|
||||
WaypointReached,
|
||||
SimulationEnd,
|
||||
}
|
||||
|
||||
public class EventQueue
|
||||
{
|
||||
private readonly Queue<SimEvent> _queue = new();
|
||||
|
||||
public void Enqueue(SimEvent e) => _queue.Enqueue(e);
|
||||
public SimEvent Dequeue() => _queue.Dequeue();
|
||||
public int Count => _queue.Count;
|
||||
@ -69,9 +51,6 @@ namespace CounterDrone.Core.Simulation
|
||||
|
||||
public enum DroneStatus
|
||||
{
|
||||
Flying,
|
||||
Destroyed,
|
||||
ReachedTarget,
|
||||
ZoneIntruded,
|
||||
Flying, Destroyed, ReachedTarget, ZoneIntruded,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,23 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Services;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Simulation
|
||||
{
|
||||
/// <summary>发射计划中的单次事件</summary>
|
||||
public class FireEvent
|
||||
{
|
||||
public float FireTime;
|
||||
public int PlatformIndex; // 第几个发射平台(0-based)
|
||||
public float TargetX, TargetY, TargetZ;
|
||||
public float MuzzleVelocity;
|
||||
}
|
||||
|
||||
/// <summary>仿真引擎 — 纯执行器,不包含任何决策逻辑</summary>
|
||||
public class SimulationEngine
|
||||
{
|
||||
private readonly IScenarioService _scenarioService;
|
||||
@ -16,32 +26,35 @@ namespace CounterDrone.Core.Simulation
|
||||
private readonly IDamageModel _damageModel;
|
||||
private readonly IPathProvider _paths;
|
||||
|
||||
// 状态
|
||||
public SimulationState State { get; private set; } = SimulationState.Idle;
|
||||
public int FrameIndex { get; private set; }
|
||||
public float SimulationTime { get; private set; }
|
||||
private float _tickInterval;
|
||||
|
||||
// 实体
|
||||
private List<DroneEntity> _drones = new();
|
||||
private List<PlatformEntity> _platforms = new();
|
||||
private List<DetectionEntity> _detections = new();
|
||||
private List<MunitionEntity> _munitions = new();
|
||||
private List<CloudEntity> _clouds = new();
|
||||
private List<ControlZone> _zones = new();
|
||||
private List<ControlZoneEntity> _zones = new();
|
||||
|
||||
// 配置快照
|
||||
private CombatScene _scene = new();
|
||||
private CloudDispersal _cloudConfig = new();
|
||||
private AmmunitionSpec _ammoSpec = new();
|
||||
private int _tickRate = 20;
|
||||
|
||||
// 内部
|
||||
private readonly EventQueue _eventQueue = new();
|
||||
// 发射计划:算法预计算,引擎按时间表执行
|
||||
private readonly List<FireEvent> _fireSchedule = new();
|
||||
private int _nextFireIndex;
|
||||
|
||||
private readonly List<SimEvent> _allEvents = new();
|
||||
private SQLiteConnection _frameDb = null!;
|
||||
private string _taskId = string.Empty;
|
||||
private int _entityCounter;
|
||||
|
||||
public IReadOnlyList<DroneEntity> Drones => _drones;
|
||||
public IReadOnlyList<CloudEntity> Clouds => _clouds;
|
||||
public IReadOnlyList<MunitionEntity> Munitions => _munitions;
|
||||
public IReadOnlyList<SimEvent> Events => _allEvents;
|
||||
|
||||
public SimulationEngine(IScenarioService scenarioService, FrameDataStore frameStore,
|
||||
IDamageModel damageModel, IPathProvider paths)
|
||||
{
|
||||
@ -51,6 +64,14 @@ namespace CounterDrone.Core.Simulation
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
/// <summary>设置发射计划(来自推荐算法),在 Initialize 之前调用</summary>
|
||||
public void SetFireSchedule(List<FireEvent> schedule)
|
||||
{
|
||||
_fireSchedule.Clear();
|
||||
_fireSchedule.AddRange(schedule.OrderBy(f => f.FireTime));
|
||||
_nextFireIndex = 0;
|
||||
}
|
||||
|
||||
public void Initialize(string taskId)
|
||||
{
|
||||
_taskId = taskId;
|
||||
@ -58,168 +79,118 @@ namespace CounterDrone.Core.Simulation
|
||||
if (config == null) throw new InvalidOperationException("Task not found");
|
||||
|
||||
_scene = config.Scene;
|
||||
_cloudConfig = config.Cloud;
|
||||
_tickRate = config.Task.TickRate;
|
||||
_tickInterval = 1.0f / _tickRate;
|
||||
|
||||
// 加载弹药规格(取第一个匹配的)
|
||||
var ammoDb = new SQLiteConnection(_paths.GetMainDbPath());
|
||||
_ammoSpec = ammoDb.Table<AmmunitionSpec>()
|
||||
.FirstOrDefault(a => a.AerosolType == _cloudConfig.AerosolType)
|
||||
?? new AmmunitionSpec { InitialRadius = 50, CoreDensity = 1.0f, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60 };
|
||||
ammoDb.Close();
|
||||
using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath()))
|
||||
{
|
||||
_ammoSpec = ammoDb.Table<AmmunitionSpec>()
|
||||
.FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType)
|
||||
?? new AmmunitionSpec { InitialRadius = 50, CoreDensity = 1.0f, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60 };
|
||||
}
|
||||
|
||||
// 实例化无人机
|
||||
// 无人机
|
||||
_drones.Clear();
|
||||
foreach (var target in config.Targets)
|
||||
{
|
||||
var mode = (FormationMode)config.Route.FormationMode;
|
||||
var spacing = (float)config.Route.FormationSpacing;
|
||||
for (int i = 0; i < target.Quantity; i++)
|
||||
{
|
||||
var drone = new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
|
||||
target, config.Waypoints, i, spacing, mode);
|
||||
_drones.Add(drone);
|
||||
}
|
||||
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
|
||||
target, config.Waypoints, i, spacing, mode));
|
||||
}
|
||||
|
||||
// 实例化装备
|
||||
// 装备
|
||||
_platforms.Clear();
|
||||
_detections.Clear();
|
||||
foreach (var equip in config.Equipment)
|
||||
{
|
||||
if (equip.EquipmentRole == (int)EquipmentRole.Detection)
|
||||
{
|
||||
_detections.Add(new DetectionEntity($"det_{++_entityCounter}", equip));
|
||||
}
|
||||
else
|
||||
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
|
||||
{
|
||||
for (int i = 0; i < equip.Quantity; i++)
|
||||
{
|
||||
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", equip));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化管控区域
|
||||
// 管控区域
|
||||
_zones.Clear();
|
||||
foreach (var z in config.ControlZones)
|
||||
{
|
||||
_zones.Add(new ControlZone(z.Id, z.Name, z.VerticesJson,
|
||||
_zones.Add(new ControlZoneEntity(z.Id, z.Name, z.VerticesJson,
|
||||
(float)z.MinAltitude, (float)z.MaxAltitude));
|
||||
}
|
||||
|
||||
_munitions.Clear();
|
||||
_clouds.Clear();
|
||||
_eventQueue.Clear();
|
||||
_allEvents.Clear();
|
||||
FrameIndex = 0;
|
||||
SimulationTime = 0;
|
||||
_entityCounter = 0;
|
||||
_nextFireIndex = 0;
|
||||
|
||||
// 打开帧数据库
|
||||
_frameDb = _frameStore.CreateOrOpen(_taskId);
|
||||
|
||||
State = SimulationState.Running;
|
||||
}
|
||||
|
||||
public SimulationFrameResult Tick(float deltaTime)
|
||||
{
|
||||
if (State != SimulationState.Running) return new SimulationFrameResult { State = State };
|
||||
if (State != SimulationState.Running)
|
||||
return new SimulationFrameResult { State = State, FrameIndex = FrameIndex, SimulationTime = SimulationTime };
|
||||
|
||||
SimulationTime += deltaTime;
|
||||
FrameIndex++;
|
||||
_eventQueue.Clear();
|
||||
var events = new List<SimEvent>();
|
||||
var frameEvents = new List<SimEvent>();
|
||||
|
||||
// 1. 探测阶段
|
||||
foreach (var det in _detections)
|
||||
// 1. 按发射计划执行
|
||||
while (_nextFireIndex < _fireSchedule.Count &&
|
||||
_fireSchedule[_nextFireIndex].FireTime <= SimulationTime)
|
||||
{
|
||||
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
||||
var fe = _fireSchedule[_nextFireIndex++];
|
||||
var platform = _platforms.Count > fe.PlatformIndex
|
||||
? _platforms[fe.PlatformIndex] : null;
|
||||
if (platform != null && platform.Ready)
|
||||
{
|
||||
if (det.CanDetect(drone))
|
||||
platform.Fire();
|
||||
var m = new MunitionEntity($"mun_{++_entityCounter}",
|
||||
platform.PlatformType ?? Models.PlatformType.GroundBased,
|
||||
platform.AerosolType ?? Models.AerosolType.InertGas,
|
||||
platform.PosX, platform.PosY, platform.PosZ,
|
||||
fe.TargetX, fe.TargetY, fe.TargetZ,
|
||||
fe.MuzzleVelocity,
|
||||
(float)_ammoSpec.InitialRadius > 0 ? 300f : 300f);
|
||||
_munitions.Add(m);
|
||||
frameEvents.Add(new SimEvent
|
||||
{
|
||||
_eventQueue.Enqueue(new SimEvent
|
||||
{
|
||||
Type = SimEventType.TargetDetected,
|
||||
OccurredAt = SimulationTime,
|
||||
SourceId = det.Id,
|
||||
TargetId = drone.Id,
|
||||
Description = $"探测到目标 {drone.Id}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 火控决策 — 简化:探测到目标后分配发射
|
||||
while (_eventQueue.HasEvents)
|
||||
{
|
||||
var evt = _eventQueue.Dequeue();
|
||||
if (evt.Type == SimEventType.TargetDetected)
|
||||
{
|
||||
var platform = _platforms.FirstOrDefault(p => p.Ready);
|
||||
var drone = _drones.FirstOrDefault(d => d.Id == evt.TargetId);
|
||||
if (platform != null && drone != null)
|
||||
{
|
||||
platform.Fire();
|
||||
var munition = new MunitionEntity(
|
||||
$"mun_{++_entityCounter}",
|
||||
platform.PlatformType ?? Models.PlatformType.GroundBased,
|
||||
platform.AerosolType ?? Models.AerosolType.InertGas,
|
||||
platform.PosX, platform.PosY, platform.PosZ,
|
||||
drone.PosX, drone.PosY, drone.PosZ,
|
||||
// Use default muzzle velocity from ammo spec or platform config
|
||||
800f,
|
||||
(float)_cloudConfig.DisperseHeight);
|
||||
_munitions.Add(munition);
|
||||
|
||||
events.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.MunitionLaunched,
|
||||
OccurredAt = SimulationTime,
|
||||
SourceId = platform.Id,
|
||||
TargetId = drone.Id,
|
||||
Description = $"发射弹药 {munition.Id} → 目标 {drone.Id}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 弹药飞行
|
||||
foreach (var munition in _munitions.ToList())
|
||||
{
|
||||
munition.Update(deltaTime);
|
||||
if (munition.HasArrived)
|
||||
{
|
||||
// 生成云团
|
||||
var dispersion = AlgorithmFactory.Create<ICloudDispersionModel>();
|
||||
dispersion.Initialize(_ammoSpec, _scene,
|
||||
new Algorithms.Vector3(munition.PosX, munition.PosY, munition.PosZ),
|
||||
SimulationTime);
|
||||
|
||||
var cloud = new CloudEntity($"cloud_{++_entityCounter}",
|
||||
munition.AerosolType, dispersion, SimulationTime);
|
||||
_clouds.Add(cloud);
|
||||
_munitions.Remove(munition);
|
||||
|
||||
events.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.CloudGenerated,
|
||||
OccurredAt = SimulationTime,
|
||||
SourceId = cloud.Id,
|
||||
Description = $"云团生成 at ({cloud.Dispersion.Center.X:F0}, {cloud.Dispersion.Center.Y:F0}, {cloud.Dispersion.Center.Z:F0})",
|
||||
Type = SimEventType.MunitionLaunched, OccurredAt = SimulationTime,
|
||||
SourceId = platform.Id, Description = $"计划发射 {m.Id}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 云团演化
|
||||
foreach (var cloud in _clouds.ToList())
|
||||
// 2. 弹药飞行 → 云团生成
|
||||
foreach (var m in _munitions.ToList())
|
||||
{
|
||||
cloud.Tick(deltaTime, (float)_scene.WindSpeed, (WindDirection)_scene.WindDirection);
|
||||
if (cloud.IsDissipated)
|
||||
_clouds.Remove(cloud);
|
||||
m.Update(deltaTime);
|
||||
if (m.HasArrived)
|
||||
{
|
||||
var disp = AlgorithmFactory.Create<ICloudDispersionModel>();
|
||||
disp.Initialize(_ammoSpec, _scene,
|
||||
new Algorithms.Vector3(m.PosX, m.PosY, m.PosZ), SimulationTime);
|
||||
_clouds.Add(new CloudEntity($"cloud_{++_entityCounter}", m.AerosolType, disp, SimulationTime));
|
||||
_munitions.Remove(m);
|
||||
frameEvents.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.CloudGenerated, OccurredAt = SimulationTime,
|
||||
Description = $"云团生成",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 毁伤判定
|
||||
// 3. 云团演化
|
||||
foreach (var c in _clouds.ToList())
|
||||
{
|
||||
c.Tick(deltaTime, (float)_scene.WindSpeed, (WindDirection)_scene.WindDirection);
|
||||
if (c.IsDissipated) _clouds.Remove(c);
|
||||
}
|
||||
|
||||
// 4. 毁伤判定
|
||||
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
||||
{
|
||||
foreach (var cloud in _clouds)
|
||||
@ -227,50 +198,36 @@ namespace CounterDrone.Core.Simulation
|
||||
if (cloud.ContainsPoint(drone.PosX, drone.PosY, drone.PosZ))
|
||||
{
|
||||
drone.ExposureTime += deltaTime;
|
||||
var density = cloud.Dispersion.CoreDensity;
|
||||
var damage = _damageModel.CalculateDamage(
|
||||
drone.TargetType, drone.PowerType,
|
||||
cloud.AerosolType, density,
|
||||
drone.ExposureTime, deltaTime);
|
||||
|
||||
drone.ApplyDamage(damage);
|
||||
|
||||
var dmg = _damageModel.CalculateDamage(
|
||||
drone.TargetType, drone.PowerType, cloud.AerosolType,
|
||||
cloud.Dispersion.CoreDensity, drone.ExposureTime, deltaTime);
|
||||
drone.ApplyDamage(dmg);
|
||||
if (drone.Status == DroneStatus.Destroyed)
|
||||
{
|
||||
events.Add(new SimEvent
|
||||
frameEvents.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.DroneDestroyed,
|
||||
OccurredAt = SimulationTime,
|
||||
TargetId = drone.Id,
|
||||
Description = $"无人机 {drone.Id} 被摧毁",
|
||||
Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime,
|
||||
TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 无人机移动
|
||||
// 5. 无人机移动
|
||||
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
||||
{
|
||||
drone.Update(deltaTime, (float)_scene.WindSpeed, (WindDirection)_scene.WindDirection);
|
||||
|
||||
if (drone.Status == DroneStatus.ReachedTarget)
|
||||
{
|
||||
events.Add(new SimEvent
|
||||
frameEvents.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.DroneReachedTarget,
|
||||
OccurredAt = SimulationTime,
|
||||
SourceId = drone.Id,
|
||||
Description = $"无人机 {drone.Id} 到达攻击目标",
|
||||
Type = SimEventType.DroneReachedTarget, OccurredAt = SimulationTime,
|
||||
SourceId = drone.Id, Description = "到达攻击目标",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 平台冷却
|
||||
foreach (var plat in _platforms)
|
||||
plat.Update(deltaTime);
|
||||
// 6. 平台冷却
|
||||
foreach (var p in _platforms) p.Update(deltaTime);
|
||||
|
||||
// 8. 管控区域判定
|
||||
// 7. 管控区域
|
||||
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
||||
{
|
||||
foreach (var zone in _zones)
|
||||
@ -278,63 +235,43 @@ namespace CounterDrone.Core.Simulation
|
||||
if (zone.ContainsPoint(drone.PosX, drone.PosY, drone.PosZ))
|
||||
{
|
||||
drone.MarkZoneIntruded();
|
||||
events.Add(new SimEvent
|
||||
frameEvents.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.ZoneIntruded,
|
||||
OccurredAt = SimulationTime,
|
||||
SourceId = drone.Id,
|
||||
TargetId = zone.Id,
|
||||
Description = $"无人机 {drone.Id} 侵入管控区域 {zone.Name}",
|
||||
Type = SimEventType.ZoneIntruded, OccurredAt = SimulationTime,
|
||||
SourceId = drone.Id, TargetId = zone.Id, Description = $"侵入 {zone.Name}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 9. 结束判定
|
||||
var allDone = _drones.All(d => d.Status != DroneStatus.Flying);
|
||||
if (allDone)
|
||||
// 8. 结束判定
|
||||
if (_drones.All(d => d.Status != DroneStatus.Flying))
|
||||
{
|
||||
State = SimulationState.Completed;
|
||||
events.Add(new SimEvent
|
||||
{
|
||||
Type = SimEventType.SimulationEnd,
|
||||
OccurredAt = SimulationTime,
|
||||
Description = "仿真结束",
|
||||
});
|
||||
frameEvents.Add(new SimEvent { Type = SimEventType.SimulationEnd, OccurredAt = SimulationTime });
|
||||
}
|
||||
|
||||
// 录制帧数据
|
||||
_allEvents.AddRange(frameEvents);
|
||||
|
||||
// 录制帧
|
||||
var snapshots = CollectSnapshots();
|
||||
_frameStore.WriteFrame(_frameDb, _taskId, FrameIndex, SimulationTime, snapshots);
|
||||
|
||||
_frameDb.Commit();
|
||||
|
||||
return new SimulationFrameResult
|
||||
{
|
||||
EntitySnapshots = snapshots,
|
||||
NewEvents = events,
|
||||
State = State,
|
||||
FrameIndex = FrameIndex,
|
||||
SimulationTime = SimulationTime,
|
||||
EntitySnapshots = snapshots, NewEvents = frameEvents,
|
||||
State = State, FrameIndex = FrameIndex, SimulationTime = SimulationTime,
|
||||
};
|
||||
}
|
||||
|
||||
public void Pause() { if (State == SimulationState.Running) State = SimulationState.Paused; }
|
||||
public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; }
|
||||
public void Stop()
|
||||
{
|
||||
State = SimulationState.Stopped;
|
||||
_frameDb?.Close();
|
||||
}
|
||||
|
||||
public IReadOnlyList<DroneEntity> Drones => _drones;
|
||||
public IReadOnlyList<CloudEntity> Clouds => _clouds;
|
||||
public IReadOnlyList<MunitionEntity> Munitions => _munitions;
|
||||
public void Stop() { State = SimulationState.Stopped; _frameDb?.Close(); }
|
||||
|
||||
private List<EntitySnapshot> CollectSnapshots()
|
||||
{
|
||||
var list = new List<EntitySnapshot>();
|
||||
|
||||
foreach (var d in _drones)
|
||||
{
|
||||
var stage = _damageModel.GetDamageStage(1 - d.Hp);
|
||||
@ -342,43 +279,16 @@ namespace CounterDrone.Core.Simulation
|
||||
{
|
||||
EntityId = d.Id, EntityType = Models.EntityType.Drone,
|
||||
PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ,
|
||||
StateData = JsonSerializer.Serialize(new { damageStage = (int)stage, hp = d.Hp, status = d.Status.ToString() }),
|
||||
StateData = JsonSerializer.Serialize(new { damageStage = (int)stage, hp = d.Hp }),
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var p in _platforms)
|
||||
{
|
||||
list.Add(new EntitySnapshot
|
||||
{
|
||||
EntityId = p.Id, EntityType = Models.EntityType.Platform,
|
||||
PosX = p.PosX, PosY = p.PosY, PosZ = p.PosZ,
|
||||
StateData = JsonSerializer.Serialize(new { ready = p.Ready, munitions = p.MunitionCount }),
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var d in _detections)
|
||||
{
|
||||
list.Add(new EntitySnapshot
|
||||
{
|
||||
EntityId = d.Id, EntityType = Models.EntityType.DetectionEquip,
|
||||
PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var c in _clouds)
|
||||
{
|
||||
list.Add(new EntitySnapshot
|
||||
{
|
||||
EntityId = c.Id, EntityType = Models.EntityType.Cloud,
|
||||
PosX = c.Dispersion.Center.X, PosY = c.Dispersion.Center.Y, PosZ = c.Dispersion.Center.Z,
|
||||
StateData = JsonSerializer.Serialize(new
|
||||
{
|
||||
radius = c.Dispersion.Radius,
|
||||
opacity = c.Dispersion.Particles.Opacity,
|
||||
}),
|
||||
StateData = JsonSerializer.Serialize(new { radius = c.Dispersion.Radius, opacity = c.Dispersion.Particles.Opacity }),
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
43
test/unit/CounterDrone.Core.Tests/ControlZoneEntityTests.cs
Normal file
43
test/unit/CounterDrone.Core.Tests/ControlZoneEntityTests.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using CounterDrone.Core.Simulation;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class ControlZoneEntityTests
|
||||
{
|
||||
private const string TestZoneJson =
|
||||
"[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]";
|
||||
|
||||
[Fact]
|
||||
public void ContainsPoint_Center_ReturnsTrue()
|
||||
{
|
||||
var zone = new ControlZoneEntity("z1", "test", TestZoneJson, 0, 1000);
|
||||
// 手动检查顶点数
|
||||
Assert.Equal(4, zone.Vertices.Count);
|
||||
Assert.Equal(1200f, zone.Vertices[0].X, 1);
|
||||
Assert.Equal(-200f, zone.Vertices[0].Z, 1);
|
||||
Assert.True(zone.ContainsPoint(1500, 300, 0), "点(1500,300,0)应在矩形内");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsPoint_OutsideX_ReturnsFalse()
|
||||
{
|
||||
var zone = new ControlZoneEntity("z1", "test", TestZoneJson, 0, 1000);
|
||||
Assert.False(zone.ContainsPoint(2000, 300, 0), "点(2000,300,0)应在矩形外");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsPoint_OutsideZ_ReturnsFalse()
|
||||
{
|
||||
var zone = new ControlZoneEntity("z1", "test", TestZoneJson, 0, 1000);
|
||||
Assert.False(zone.ContainsPoint(1500, 300, 500), "点(1500,300,500) Z超出");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsPoint_OutsideAltitude_ReturnsFalse()
|
||||
{
|
||||
var zone = new ControlZoneEntity("z1", "test", TestZoneJson, 0, 1000);
|
||||
Assert.False(zone.ContainsPoint(1500, 1200, 0), "海拔 1200 超出上限");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DefenseAdvisorRealityTests
|
||||
{
|
||||
[Fact]
|
||||
public void PistonDrone_RecommendsReachableCloudPosition()
|
||||
{
|
||||
var threat = new ThreatProfile
|
||||
{
|
||||
Environment = new CombatScene
|
||||
{
|
||||
WindSpeed = 3,
|
||||
WindDirection = (int)WindDirection.W,
|
||||
},
|
||||
Targets = new List<TargetConfig>
|
||||
{
|
||||
new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
},
|
||||
},
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
},
|
||||
};
|
||||
|
||||
var advisor = new DefaultDefenseAdvisor();
|
||||
var rec = advisor.Recommend(threat);
|
||||
|
||||
// 1. 选型正确
|
||||
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
|
||||
|
||||
// 2. 云团位置应在航路上
|
||||
var c = rec.Best.RecommendedCloud;
|
||||
Assert.True(c.PositionX > 0 && c.PositionX < 20000,
|
||||
$"云团 X={c.PositionX} 应在航路范围内");
|
||||
Assert.True(c.PositionY >= 400 && c.PositionY <= 600,
|
||||
$"云团 Y={c.PositionY} 应接近飞行高度 500");
|
||||
|
||||
// 3. 平台部署应有合理位置
|
||||
Assert.NotEmpty(rec.Best.Platforms);
|
||||
var p = rec.Best.Platforms[0];
|
||||
|
||||
// 4. 炮弹能打到吗?用运动学验证
|
||||
var muzzleV = 800f;
|
||||
var releaseAlt = (float)c.DisperseHeight;
|
||||
var targetDist = (float)System.Math.Sqrt(
|
||||
(c.PositionX - p.Position.X) * (c.PositionX - p.Position.X) +
|
||||
(c.PositionZ - p.Position.Z) * (c.PositionZ - p.Position.Z));
|
||||
|
||||
var launchAngle = Kinematics.CalculateLaunchAngle(targetDist, muzzleV, releaseAlt);
|
||||
Assert.True(launchAngle > 0 && launchAngle < 1.57f,
|
||||
$"发射角 {launchAngle * 180 / System.Math.PI:F1}° 应在 0-90°");
|
||||
|
||||
// 5. 弹道能到达释放高度
|
||||
var peakHeight = muzzleV * muzzleV * (float)System.Math.Sin(launchAngle)
|
||||
* (float)System.Math.Sin(launchAngle) / (2f * 9.81f);
|
||||
Assert.True(peakHeight >= releaseAlt,
|
||||
$"弹道顶点 {peakHeight:F0}m ≥ 释放高度 {releaseAlt:F0}m");
|
||||
}
|
||||
}
|
||||
}
|
||||
111
test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs
Normal file
111
test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs
Normal file
@ -0,0 +1,111 @@
|
||||
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 altitude, float speed)
|
||||
{
|
||||
return new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = fromX, PosY = altitude, PosZ = 0, Altitude = altitude, Speed = speed },
|
||||
new Waypoint { PosX = toX, PosY = altitude, PosZ = 0, Altitude = altitude, Speed = speed },
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialPosition_IsFirstWaypoint()
|
||||
{
|
||||
var route = CreateRoute(100, 500, 300, 120);
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single);
|
||||
|
||||
Assert.Equal(100, drone.PosX, 1);
|
||||
Assert.Equal(300, drone.PosY, 1); // 高度 = waypoint.PosY
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MovesTowardTarget()
|
||||
{
|
||||
var route = CreateRoute(0, 1000, 300, 120);
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single);
|
||||
|
||||
float prevX = drone.PosX;
|
||||
drone.Update(1f, 0, WindDirection.N);
|
||||
|
||||
Assert.True(drone.PosX > prevX, "无人机应向目标移动");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReachesTarget_StatusChanges()
|
||||
{
|
||||
var route = CreateRoute(0, 100, 300, 360); // 360 km/h = 100 m/s
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(360), route, 0, 50, FormationMode.Single);
|
||||
|
||||
// 100m 距离, 100m/s 速度, 1s 后应到达
|
||||
drone.Update(2f, 0, WindDirection.N);
|
||||
|
||||
Assert.Equal(DroneStatus.ReachedTarget, drone.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDamage_ReducesHp()
|
||||
{
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(0, 100, 300, 60), 0, 50, FormationMode.Single);
|
||||
|
||||
drone.ApplyDamage(0.4f);
|
||||
Assert.Equal(0.6f, drone.Hp, 0.01f);
|
||||
Assert.Equal(DroneStatus.Flying, drone.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDamage_ExceedingHp_DestroysDrone()
|
||||
{
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(0, 100, 300, 60), 0, 50, FormationMode.Single);
|
||||
|
||||
drone.ApplyDamage(1.5f);
|
||||
Assert.True(drone.Hp <= 0);
|
||||
Assert.Equal(DroneStatus.Destroyed, drone.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Formation_YOffsetsDiffer()
|
||||
{
|
||||
var route = CreateRoute(0, 100, 300, 60);
|
||||
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, FormationMode.Formation);
|
||||
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, FormationMode.Formation);
|
||||
|
||||
Assert.NotEqual(d0.PosY, d1.PosY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wind_AffectsPosition()
|
||||
{
|
||||
var route = CreateRoute(0, 1000, 300, 60);
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single);
|
||||
|
||||
float xBefore = drone.PosX;
|
||||
drone.Update(1f, 20f, WindDirection.E); // 东风 20m/s
|
||||
|
||||
// 东风 → +X 方向偏移
|
||||
Assert.True(drone.PosX > xBefore + 15f, "东风应显著增加 X 位移");
|
||||
}
|
||||
}
|
||||
}
|
||||
330
test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs
Normal file
330
test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs
Normal file
@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using CounterDrone.Core.Services;
|
||||
using CounterDrone.Core.Simulation;
|
||||
using SQLite;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class FullPipelineTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
private readonly IPathProvider _paths;
|
||||
private readonly IScenarioService _scenario;
|
||||
private readonly FrameDataStore _frameStore;
|
||||
private readonly SQLiteConnection _mainDb;
|
||||
private string _taskId = string.Empty;
|
||||
|
||||
public FullPipelineTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"cd_full_{Guid.NewGuid():N}");
|
||||
_paths = new TestPathProvider(_testDir);
|
||||
var dbm = new DatabaseManager(_paths);
|
||||
_mainDb = dbm.OpenMainDb();
|
||||
|
||||
_mainDb.Insert(new AmmunitionSpec
|
||||
{
|
||||
Id = "ammo-inert", AerosolType = (int)AerosolType.InertGas,
|
||||
Name = "惰性气体弹", InitialRadius = 50, CoreDensity = 1.0,
|
||||
DispersionRateBase = 5, MaxRadius = 500, MaxDuration = 120,
|
||||
EffectiveConcentration = 0.05,
|
||||
});
|
||||
_mainDb.Insert(new AmmunitionSpec
|
||||
{
|
||||
Id = "ammo-active", AerosolType = (int)AerosolType.ActiveMaterial,
|
||||
Name = "活性材料弹", InitialRadius = 30, CoreDensity = 2.0,
|
||||
DispersionRateBase = 3, MaxRadius = 400, MaxDuration = 90,
|
||||
EffectiveConcentration = 0.1,
|
||||
});
|
||||
_mainDb.Insert(new AmmunitionSpec
|
||||
{
|
||||
Id = "ammo-fuel", AerosolType = (int)AerosolType.ActiveFuel,
|
||||
Name = "活性燃料弹", InitialRadius = 40, CoreDensity = 1.5,
|
||||
DispersionRateBase = 4, MaxRadius = 450, MaxDuration = 100,
|
||||
EffectiveConcentration = 0.03,
|
||||
});
|
||||
|
||||
_scenario = new ScenarioService(
|
||||
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
|
||||
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
|
||||
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
|
||||
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
|
||||
|
||||
_frameStore = new FrameDataStore(_paths);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_mainDb?.Close();
|
||||
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
|
||||
}
|
||||
|
||||
private SimulationEngine RunSimulation(int maxTicks, float tickDt = 1f / 20f)
|
||||
{
|
||||
var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths);
|
||||
|
||||
// 如果有推荐方案,生成发射计划
|
||||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||||
if (detail.Equipment.Any(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
|
||||
{
|
||||
var schedule = BuildFireSchedule(detail);
|
||||
if (schedule.Count > 0) engine.SetFireSchedule(schedule);
|
||||
}
|
||||
|
||||
engine.Initialize(_taskId);
|
||||
for (int i = 0; i < maxTicks; i++)
|
||||
{
|
||||
var r = engine.Tick(tickDt);
|
||||
if (r.State == SimulationState.Completed) break;
|
||||
}
|
||||
engine.Stop();
|
||||
return engine;
|
||||
}
|
||||
|
||||
/// <summary>从推荐方案的时机参数生成发射计划</summary>
|
||||
private static List<FireEvent> BuildFireSchedule(TaskFullConfig detail)
|
||||
{
|
||||
var schedule = new List<FireEvent>();
|
||||
var cloud = detail.Cloud;
|
||||
|
||||
if (!cloud.RecommendedTiming.HasValue || cloud.RecommendedTiming.Value <= 0)
|
||||
return schedule;
|
||||
|
||||
var platforms = detail.Equipment
|
||||
.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < platforms.Count; i++)
|
||||
{
|
||||
var p = platforms[i];
|
||||
var muzzleV = (float)(p.MuzzleVelocity ?? 800);
|
||||
|
||||
// 弹药飞行时间 ≈ 距离 / 初速(简化)
|
||||
var dx = (float)cloud.PositionX - (float)p.PositionX;
|
||||
var dz = (float)cloud.PositionZ - (float)p.PositionZ;
|
||||
var dist = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||||
var flightTime = dist / muzzleV;
|
||||
|
||||
// 发射时间 = 推荐云团出现时间 - 弹药飞行时间
|
||||
var fireTime = (float)cloud.RecommendedTiming.Value - flightTime;
|
||||
if (fireTime < 0) fireTime = 0.1f;
|
||||
|
||||
schedule.Add(new FireEvent
|
||||
{
|
||||
FireTime = fireTime,
|
||||
PlatformIndex = i,
|
||||
TargetX = (float)cloud.PositionX,
|
||||
TargetY = (float)cloud.PositionY,
|
||||
TargetZ = (float)cloud.PositionZ,
|
||||
MuzzleVelocity = muzzleV,
|
||||
});
|
||||
}
|
||||
|
||||
return schedule;
|
||||
}
|
||||
|
||||
/// <summary>将 DefenseAdvisor 推荐方案写入想定</summary>
|
||||
private DefenseRecommendation GetAndApplyRecommendation()
|
||||
{
|
||||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||||
var threat = new ThreatProfile
|
||||
{
|
||||
Environment = detail.Scene,
|
||||
Targets = detail.Targets,
|
||||
Route = detail.Route,
|
||||
Waypoints = detail.Waypoints,
|
||||
};
|
||||
var rec = new DefaultDefenseAdvisor().Recommend(threat);
|
||||
|
||||
_scenario.SaveCloudDispersal(_taskId, rec.Best.RecommendedCloud);
|
||||
|
||||
var equips = new List<EquipmentDeployment>();
|
||||
foreach (var d in rec.Best.Detections)
|
||||
equips.Add(new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = d.Quantity,
|
||||
PositionX = d.Position.X, PositionY = d.Position.Y, PositionZ = d.Position.Z,
|
||||
DetectionRadius = d.DetectionRadius,
|
||||
});
|
||||
foreach (var p in rec.Best.Platforms)
|
||||
equips.Add(new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)p.Type,
|
||||
Quantity = p.Quantity,
|
||||
PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z,
|
||||
AerosolType = (int)rec.Best.RecommendedAerosolType,
|
||||
MunitionCount = p.MunitionCount,
|
||||
Cooldown = 5,
|
||||
MuzzleVelocity = p.Type == PlatformType.GroundBased ? 800 : (double?)null,
|
||||
});
|
||||
_scenario.SaveDeployment(_taskId, equips);
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 场景 1:无防御 — 无人机到达目标
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Scenario_NoDefense_DroneReachesTarget()
|
||||
{
|
||||
var task = _scenario.CreateTask("无防御", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.FixedWing,
|
||||
PowerType = (int)PowerType.Jet,
|
||||
Quantity = 1, TypicalSpeed = 600, TypicalAltitude = 400,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 400, PosZ = 0, Speed = 600 },
|
||||
new Waypoint { PosX = 3000, PosY = 400, PosZ = 0, Speed = 600 },
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
|
||||
|
||||
var eng = RunSimulation(400);
|
||||
Assert.Equal(DroneStatus.ReachedTarget, eng.Drones[0].Status);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 场景 2:管控区域侵入
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Scenario_ZoneIntrusion_DroneMarked()
|
||||
{
|
||||
var task = _scenario.CreateTask("管控区", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Electric,
|
||||
PowerType = (int)PowerType.Electric,
|
||||
Quantity = 1, TypicalSpeed = 300, TypicalAltitude = 300,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 300 },
|
||||
new Waypoint { PosX = 3000, PosY = 300, PosZ = 0, Speed = 300 },
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
|
||||
_scenario.SaveControlZones(_taskId, new List<Models.ControlZone>
|
||||
{
|
||||
new Models.ControlZone
|
||||
{
|
||||
Name = "禁飞区",
|
||||
VerticesJson = "[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]",
|
||||
MinAltitude = 0, MaxAltitude = 1000,
|
||||
},
|
||||
});
|
||||
|
||||
var eng = RunSimulation(400);
|
||||
Assert.Equal(DroneStatus.ZoneIntruded, eng.Drones[0].Status);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 场景 3:活塞发动机 → 算法推荐惰性气体 → 拦截成功
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Scenario_Piston_InertGasIntercept()
|
||||
{
|
||||
var task = _scenario.CreateTask("活塞拦截", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenario.SaveScene(_taskId, new CombatScene
|
||||
{
|
||||
WindSpeed = 3, WindDirection = (int)WindDirection.W,
|
||||
});
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
|
||||
var rec = GetAndApplyRecommendation();
|
||||
|
||||
// 验证推荐参数和装备被正确持久化
|
||||
var saved = _scenario.GetTaskDetail(_taskId)!;
|
||||
Assert.True(saved.Cloud.RecommendedTiming.HasValue,
|
||||
$"Piston: RecommendedTiming={saved.Cloud.RecommendedTiming}");
|
||||
Assert.True(saved.Cloud.RecommendedTiming > 0);
|
||||
Assert.NotEmpty(saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform),
|
||||
"应有发射平台");
|
||||
|
||||
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
|
||||
Assert.True(rec.Best.InterceptProbability > 0);
|
||||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0);
|
||||
|
||||
var eng = RunSimulation(3000);
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
Assert.True(launched > 0, $"Piston: 应发射弹药,实际{launched}次");
|
||||
Assert.Equal(DroneStatus.Destroyed, eng.Drones[0].Status);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 场景 4:喷气发动机 → 算法推荐活性材料 → 拦截成功
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Scenario_Jet_ActiveMaterialIntercept()
|
||||
{
|
||||
var task = _scenario.CreateTask("喷气拦截", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.HighSpeed,
|
||||
PowerType = (int)PowerType.Jet,
|
||||
Quantity = 1, TypicalSpeed = 500, TypicalAltitude = 800,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 500 },
|
||||
new Waypoint { PosX = 30000, PosY = 800, PosZ = 0, Speed = 500 },
|
||||
});
|
||||
|
||||
var rec = GetAndApplyRecommendation();
|
||||
|
||||
var saved = _scenario.GetTaskDetail(_taskId)!;
|
||||
Assert.True(saved.Cloud.RecommendedTiming.HasValue,
|
||||
$"Jet: RecommendedTiming={saved.Cloud.RecommendedTiming}");
|
||||
|
||||
Assert.Equal(AerosolType.ActiveMaterial, rec.Best.RecommendedAerosolType);
|
||||
Assert.True(rec.Best.InterceptProbability > 0);
|
||||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0);
|
||||
|
||||
var eng = RunSimulation(3000);
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
Assert.True(launched > 0, $"Jet: 应发射弹药,实际{launched}次");
|
||||
Assert.Equal(DroneStatus.Destroyed, eng.Drones[0].Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
Normal file
65
test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class KinematicsTests
|
||||
{
|
||||
[Fact]
|
||||
public void WindToVector_East_CorrectDirection()
|
||||
{
|
||||
var (x, y, z) = Kinematics.WindToVector(WindDirection.E, 10f);
|
||||
Assert.True(x > 9f, "东风 → X 正方向");
|
||||
Assert.True(System.Math.Abs(z) < 1f, "东风 → Z 接近 0");
|
||||
Assert.Equal(0f, y, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindToVector_North_CorrectDirection()
|
||||
{
|
||||
var (x, y, z) = Kinematics.WindToVector(WindDirection.N, 10f);
|
||||
Assert.True(System.Math.Abs(x) < 1f);
|
||||
Assert.True(z > 9f, "北风 → Z 正方向");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLaunchAngle_ReturnsValidAngle()
|
||||
{
|
||||
var angle = Kinematics.CalculateLaunchAngle(5000f, 500f, 300f);
|
||||
Assert.True(angle > 0);
|
||||
Assert.True(angle < System.Math.PI / 2); // < 90°
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLaunchAngle_CloseRange_UsesHeightConstraint()
|
||||
{
|
||||
// 近距离,高度约束应主导
|
||||
var angle = Kinematics.CalculateLaunchAngle(1000f, 300f, 300f);
|
||||
Assert.True(angle > 0.05f, "近距离也有合理发射角");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicPosition_T0_IsAtStart()
|
||||
{
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, 0.5f, 0, 500f, 0);
|
||||
Assert.Equal(0f, x, 1);
|
||||
Assert.Equal(0f, y, 1);
|
||||
Assert.Equal(0f, z, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointInPolygon_Inside_ReturnsTrue()
|
||||
{
|
||||
var vertices = new (float X, float Z)[] { (0, 0), (10, 0), (10, 10), (0, 10) };
|
||||
Assert.True(Kinematics.PointInPolygon(5, 5, vertices));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointInPolygon_Outside_ReturnsFalse()
|
||||
{
|
||||
var vertices = new (float X, float Z)[] { (0, 0), (10, 0), (10, 10), (0, 10) };
|
||||
Assert.False(Kinematics.PointInPolygon(20, 5, vertices));
|
||||
}
|
||||
}
|
||||
}
|
||||
58
test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs
Normal file
58
test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Simulation;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class MunitionEntityTests
|
||||
{
|
||||
[Fact]
|
||||
public void GroundBased_DoesNotArriveImmediately()
|
||||
{
|
||||
// 500 m/s, 5000m 距离, 300m 释放高度 — 可以到达
|
||||
var m = new MunitionEntity("m1", PlatformType.GroundBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 0, startY: 0, startZ: 0,
|
||||
targetX: 5000, targetY: 300, targetZ: 0,
|
||||
muzzleVelocity: 500f, releaseAltitude: 300f);
|
||||
|
||||
m.Update(0.05f);
|
||||
Assert.False(m.HasArrived, "炮弹不能第一帧就到达");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundBased_ArrivesAfterPeakAndDescent()
|
||||
{
|
||||
var m = new MunitionEntity("m1", PlatformType.GroundBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 0, startY: 0, startZ: 0,
|
||||
targetX: 5000, targetY: 300, targetZ: 0,
|
||||
muzzleVelocity: 500f, releaseAltitude: 300f);
|
||||
|
||||
// 500m/s 到 5000m,约需 10-15 秒
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
m.Update(0.05f);
|
||||
if (m.HasArrived) break;
|
||||
}
|
||||
|
||||
Assert.True(m.HasArrived, "炮弹应在飞行后到达释放高度");
|
||||
// 到达时高度应接近释放高度
|
||||
Assert.True(System.Math.Abs(m.PosY - 300f) < 5f, $"PosY={m.PosY} 应接近 300");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundBased_MovesForwardOverTime()
|
||||
{
|
||||
var m = new MunitionEntity("m1", PlatformType.GroundBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 0, startY: 0, startZ: 0,
|
||||
targetX: 1000, targetY: 300, targetZ: 0,
|
||||
muzzleVelocity: 500f, releaseAltitude: 300f);
|
||||
|
||||
m.Update(1f);
|
||||
var dist = System.Math.Sqrt(m.PosX * m.PosX + m.PosZ * m.PosZ);
|
||||
Assert.True(dist > 100f, "1秒后炮弹应水平移动 > 100m");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user