132 lines
4.7 KiB
C
132 lines
4.7 KiB
C
// All target coordinates come from delivered HQ observations, never a player's
|
|
// current transform. Road projection stops at an unknown junction/road end.
|
|
class ERR_MortarPrediction
|
|
{
|
|
// Read immediately after a failed synchronous prediction; no extra world scan.
|
|
static string FailureReason;
|
|
// Includes observation uplink, HQ downlink and native aiming. All stages
|
|
// retain the original observation time; radio delivery never renews it.
|
|
static const float MAX_REPORT_AGE_MS = 60000;
|
|
|
|
static bool Fresh(ERR_HQReport report, float now)
|
|
{
|
|
return report && report.Target && !report.Distress && now >= report.ObservedAt && now - report.ObservedAt <= MAX_REPORT_AGE_MS && report.TravelMode != "aircraft";
|
|
}
|
|
|
|
static bool Reject(string reason)
|
|
{
|
|
FailureReason = reason;
|
|
return false;
|
|
}
|
|
|
|
static vector ClosestOnSegment(vector point, vector start, vector end)
|
|
{
|
|
vector segment = end - start;
|
|
segment[1] = 0;
|
|
vector offset = point - start;
|
|
offset[1] = 0;
|
|
float lengthSq = segment.LengthSq();
|
|
if (lengthSq < 0.01)
|
|
return start;
|
|
return vector.Lerp(start, end, Math.Clamp(vector.Dot(offset, segment) / lengthSq, 0, 1));
|
|
}
|
|
|
|
static bool FollowRoad(array<vector> points, vector observed, vector velocity, float seconds, out vector impact)
|
|
{
|
|
if (!points || points.Count() < 2)
|
|
return Reject("road polyline has fewer than two points");
|
|
int segment = -1;
|
|
float closest = 20;
|
|
vector projected;
|
|
for (int i = 0; i < points.Count() - 1; i++)
|
|
{
|
|
vector candidate = ClosestOnSegment(observed, points[i], points[i + 1]);
|
|
float distance = vector.DistanceXZ(candidate, observed);
|
|
if (distance < closest)
|
|
{
|
|
closest = distance;
|
|
segment = i;
|
|
projected = candidate;
|
|
}
|
|
}
|
|
if (segment < 0)
|
|
return Reject("observed vehicle is not within 20m of a road segment");
|
|
vector forward = points[segment + 1] - points[segment];
|
|
forward[1] = 0;
|
|
forward.Normalize();
|
|
vector travel = velocity;
|
|
travel[1] = 0;
|
|
float speed = travel.Length();
|
|
travel.Normalize();
|
|
float alignment = vector.Dot(forward, travel);
|
|
if (Math.AbsFloat(alignment) < 0.7)
|
|
return Reject("vehicle heading crosses road; alignment below 0.7");
|
|
int step = 1;
|
|
int next = segment + 1;
|
|
if (alignment < 0)
|
|
{
|
|
step = -1;
|
|
next = segment;
|
|
}
|
|
float remaining = speed * seconds;
|
|
vector position = projected;
|
|
while (next >= 0 && next < points.Count())
|
|
{
|
|
float length = vector.DistanceXZ(position, points[next]);
|
|
if (length >= remaining && length > 0.01)
|
|
{
|
|
impact = vector.Lerp(position, points[next], remaining / length);
|
|
return true;
|
|
}
|
|
remaining -= length;
|
|
position = points[next];
|
|
next += step;
|
|
}
|
|
return Reject("prediction reaches unknown road end/junction");
|
|
}
|
|
|
|
static bool Predict(ERR_HQReport report, float now, float flightSeconds, out vector impact)
|
|
{
|
|
FailureReason = "";
|
|
if (!report) return Reject("target report missing");
|
|
if (report.Distress) return Reject("distress report cannot authorize fire");
|
|
if (now < report.ObservedAt) return Reject("observation timestamp is in the future");
|
|
if (now - report.ObservedAt > MAX_REPORT_AGE_MS) return Reject("observation older than 60s");
|
|
float seconds = (now - report.ObservedAt) / 1000 + flightSeconds;
|
|
if (seconds < 0 || seconds > 90)
|
|
return Reject("observation age plus flight outside 0..90s");
|
|
if (report.TravelMode == "aircraft") return Reject("aircraft target unsupported");
|
|
if (report.TravelMode == "mixed travel") return Reject("mixed travel cannot be predicted");
|
|
impact = report.Position;
|
|
if (!report.HasVelocity || report.Velocity.Length() < 0.5)
|
|
return true;
|
|
if (report.Velocity.Length() * seconds > 1800)
|
|
return Reject("predicted travel exceeds 1800m");
|
|
bool vehicle = report.TravelMode == "vehicle" || report.TravelMode == "in vehicle";
|
|
if (vehicle)
|
|
{
|
|
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
|
if (!world || !world.GetRoadNetworkManager())
|
|
return Reject("road network unavailable");
|
|
BaseRoad road;
|
|
float roadDistance;
|
|
world.GetRoadNetworkManager().GetClosestRoad(report.Position, road, roadDistance, skipNavlinks: true);
|
|
if (!road) return Reject("no road near reported vehicle position");
|
|
if (roadDistance > 20) return Reject("reported vehicle farther than 20m from road");
|
|
array<vector> points = {};
|
|
road.GetPoints(points);
|
|
return FollowRoad(points, report.Position, report.Velocity, seconds, impact);
|
|
}
|
|
impact += report.Velocity * seconds;
|
|
return true;
|
|
}
|
|
|
|
static bool ThreatensFriendly(vector impact, vector position, vector velocity, float flightSeconds)
|
|
{
|
|
velocity[1] = 0;
|
|
vector future = position + velocity * Math.Clamp(flightSeconds, 0, 90);
|
|
// Protect the entire predicted path, not only where a soldier is now.
|
|
return vector.DistanceXZ(impact, ClosestOnSegment(impact, position, future)) <= 100;
|
|
}
|
|
}
|