Evasion term: reachable spread from the thrust and turn envelope, realised by changes of vector
Every hit price gains one factor, PDeviation(Evasion, Tracking): how far the target can and does move off the line of fire within SolutionWindow (0.75 s). Capability is Reach, an eight-face closed form over the ship's ManoeuvreEnvelope (summed from what each live thruster and aether drive reports with its own Execute arithmetic, read live: a thruster's Thrust property is stale until it fires, which is why the Djinni moved 1.6 times its stats, M25). Behaviour is ManoeuvreTrack, observed once per Entity.Update from the measured acceleration; a steady burn, coasting and a dither much faster than the window do not evade, and sideways speed alone does not (ruling evasion-speed). The commit price adds the term at any flight time, so an instant weapon feels a jinking target. Forecast owns the term for HitProbability and Inspect, which gain Evasion and PEvasion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5100daa297
commit
da2957b81c
7 changed files with 786 additions and 8 deletions
|
|
@ -291,9 +291,13 @@ public static class FireControl
|
|||
// and to CellsOf, out of this cut's scope. Cut 10's own guard is what actually matters
|
||||
// operationally: HitProbability and CommitProbability both bail before Forecast is ever called for the
|
||||
// common gated-out case (GatedOutHitProbabilityAllocatesNothing pins that at exactly 0).
|
||||
private static (Silhouette Silhouette, float PSpread) Forecast(Weapon weapon, Entity source, Entity target, HullData targetHull, float precision, float range)
|
||||
// Evasion fix (evasion-term): the same call also prices how far the target can and does move off the line of
|
||||
// fire, along the same travel direction, so the live price and the HUD read one evasion.
|
||||
private static (Silhouette Silhouette, float PSpread, float Evasion) Forecast(Weapon weapon, Entity source, Entity target, HullData targetHull, float precision, float range)
|
||||
{
|
||||
var bearing = Bearing(target, TravelDirection(weapon, source, target));
|
||||
var travel = TravelDirection(weapon, source, target);
|
||||
var evasion = Evasion(target, travel);
|
||||
var bearing = Bearing(target, travel);
|
||||
var buffer = ArrayPool<Interval>.Shared.Rent(targetHull.Shape.Coordinates.Length);
|
||||
float pSpread;
|
||||
Silhouette sil;
|
||||
|
|
@ -310,7 +314,7 @@ public static class FireControl
|
|||
// sil.Intervals is the returned buffer -- cleared here so nothing outside this function can read it
|
||||
// after it goes back to the pool and another caller starts overwriting it.
|
||||
sil.Intervals = null;
|
||||
return (sil, pSpread);
|
||||
return (sil, pSpread, evasion);
|
||||
}
|
||||
|
||||
public static float HitProbability(Weapon weapon, Entity source, Entity target)
|
||||
|
|
@ -319,8 +323,8 @@ public static class FireControl
|
|||
if (pFire <= 0f) return 0f;
|
||||
|
||||
var targetHull = source.ItemManager.GetData(target.Hull) as HullData;
|
||||
var (sil, pSpread) = Forecast(weapon, source, target, targetHull, Precision(source), range);
|
||||
return pFire * pSpread * sil.POnHull;
|
||||
var (sil, pSpread, evasion) = Forecast(weapon, source, target, targetHull, Precision(source), range);
|
||||
return pFire * pSpread * sil.POnHull * PDeviation(evasion, Tracking(source));
|
||||
}
|
||||
|
||||
// Presentation only: the debug HUD's view of exactly the factors HitProbability multiplies. It computes
|
||||
|
|
@ -357,9 +361,11 @@ public static class FireControl
|
|||
diagnostic.PSensor = PSensor(settings, diagnostic.Resolution, diagnostic.Info);
|
||||
diagnostic.PFire = diagnostic.Accuracy * diagnostic.PSensor;
|
||||
|
||||
var (sil, pSpread) = Forecast(weapon, source, target, targetHull, diagnostic.Precision, diagnostic.Range);
|
||||
var (sil, pSpread, evasion) = Forecast(weapon, source, target, targetHull, diagnostic.Precision, diagnostic.Range);
|
||||
diagnostic.PSpread = pSpread;
|
||||
diagnostic.POnHull = sil.POnHull;
|
||||
diagnostic.Evasion = evasion;
|
||||
diagnostic.PEvasion = PDeviation(evasion, diagnostic.Tracking);
|
||||
|
||||
diagnostic.PBase = HitProbability(weapon, source, target);
|
||||
return diagnostic;
|
||||
|
|
@ -427,14 +433,104 @@ public static class FireControl
|
|||
public static float PDeviation(float deviation, float tolerance) =>
|
||||
tolerance <= 0f ? (deviation <= 0f ? 1f : 0f) : saturate(1f - deviation / tolerance);
|
||||
|
||||
// The commit price's deviation: what the target has already moved off the frozen forecast, plus what it can and
|
||||
// does move off the line of fire (Evasion). The second part applies at any flight time, so an instant weapon
|
||||
// (flight time 0, realized deviation 0) still feels a jinking target.
|
||||
public static float DeviationProbability(PendingShot shot, float now, out float deviation)
|
||||
{
|
||||
var elapsed = now - shot.FireTime;
|
||||
var predicted = shot.FireTargetPosition + shot.FireTargetVelocity * elapsed;
|
||||
deviation = shot.Target == null ? 0f : length((shot.Target.Position - predicted).xz);
|
||||
deviation = shot.Target == null ? 0f : length((shot.Target.Position - predicted).xz) + Evasion(shot.Target, shot.TravelDirection);
|
||||
return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking);
|
||||
}
|
||||
|
||||
// How far a target can and does move off the line of fire in the solution window: the term DeviationProbability
|
||||
// and HitProbability both add. Behaviour (its observed unpredictability across the line of sight) realises the
|
||||
// evasion and capability (its envelope's reach) caps it, so a coasting ship evades nothing whatever it could do
|
||||
// and a steady burn or a dither much faster than the window averages out. A target that is not a Ship has no
|
||||
// envelope. This is the one call guns and munitions make.
|
||||
public static float Evasion(Entity target, float2 lineOfSight)
|
||||
{
|
||||
var ship = target as Ship;
|
||||
return ship == null
|
||||
? 0f
|
||||
: Evasion(ship.Envelope, ship.Direction, ship.Manoeuvre, lineOfSight, target.ItemManager.GameplaySettings.SolutionWindow);
|
||||
}
|
||||
|
||||
// The pure core, allocation-free. n is perpendicular to the line of sight; u is the half-width the observed
|
||||
// innovation spreads over the window (0.5 * rms * T^2); the answer is that spread, capped by the half-width of
|
||||
// what the envelope can reach across the line of sight. Zero spread returns before any reach is evaluated.
|
||||
public static float Evasion(in ManoeuvreEnvelope envelope, float2 heading, in ManoeuvreTrack track, float2 lineOfSight, float window)
|
||||
{
|
||||
var los = lengthsq(lineOfSight) < 1e-12f ? float2(0, 1) : normalize(lineOfSight);
|
||||
var n = float2(-los.y, los.x);
|
||||
var spread = .5f * track.LateralRms(n) * window * window;
|
||||
if (spread <= 0f) return 0f;
|
||||
return min(.5f * (Reach(envelope, heading, n, window) + Reach(envelope, heading, -n, window)), spread);
|
||||
}
|
||||
|
||||
// The largest displacement from the coasting path along `direction` within `window`, for a ship that turns
|
||||
// kinematically (controls-turn-inertia) and thrusts within its body-frame box. Eight faces (the four thrust axes
|
||||
// and the four corners of the box), each turned toward the direction at full rate and thrusting whenever it
|
||||
// points within 90 degrees of it, then held; and the no-turn support of the box. Matched a brute-force search
|
||||
// within 0.1% (map M26).
|
||||
public static float Reach(in ManoeuvreEnvelope e, float2 heading, float2 direction, float window)
|
||||
{
|
||||
if (lengthsq(heading) < 1e-12f || lengthsq(direction) < 1e-12f) return 0f;
|
||||
var forward = normalize(heading);
|
||||
var right = forward.Rotate(ItemRotation.Clockwise);
|
||||
var wanted = normalize(direction);
|
||||
var beta = atan2(dot(wanted, right), dot(wanted, forward));
|
||||
|
||||
// The box's support along the wanted direction with no turning: thrust is always free to fire the right sign.
|
||||
var c = cos(beta);
|
||||
var s = sin(beta);
|
||||
var best = .5f * ((c > 0f ? e.Forward * c : -e.Reverse * c) + (s > 0f ? e.Right * s : -e.Left * s)) * window * window;
|
||||
|
||||
best = max(best, ReachFace(e.Forward, 0f, beta, e, window));
|
||||
best = max(best, ReachFace(e.Reverse, MathF.PI, beta, e, window));
|
||||
best = max(best, ReachFace(e.Right, MathF.PI / 2f, beta, e, window));
|
||||
best = max(best, ReachFace(e.Left, -MathF.PI / 2f, beta, e, window));
|
||||
best = max(best, ReachFace(sqrt(e.Forward * e.Forward + e.Right * e.Right), atan2(e.Right, e.Forward), beta, e, window));
|
||||
best = max(best, ReachFace(sqrt(e.Forward * e.Forward + e.Left * e.Left), atan2(-e.Left, e.Forward), beta, e, window));
|
||||
best = max(best, ReachFace(sqrt(e.Reverse * e.Reverse + e.Right * e.Right), atan2(e.Right, -e.Reverse), beta, e, window));
|
||||
best = max(best, ReachFace(sqrt(e.Reverse * e.Reverse + e.Left * e.Left), atan2(-e.Left, -e.Reverse), beta, e, window));
|
||||
return best;
|
||||
}
|
||||
|
||||
// One face of acceleration `a`, at body angle `faceAngle` (clockwise from forward), turned to point along the
|
||||
// wanted direction at body angle `beta`, at the rate its side of the envelope allows; thrust only while the face
|
||||
// points within 90 degrees of the wanted direction, so the integrand is (T - t) a cos(angle between).
|
||||
private static float ReachFace(float a, float faceAngle, float beta, in ManoeuvreEnvelope e, float window)
|
||||
{
|
||||
if (a <= 0f) return 0f;
|
||||
var turn = Wrap(beta - faceAngle);
|
||||
var rate = turn > 0f ? e.Clockwise : e.CounterClockwise;
|
||||
var phi0 = abs(turn);
|
||||
if (rate <= 1e-4f) return phi0 <= 1e-6f ? .5f * a * window * window : 0f;
|
||||
|
||||
var turnTime = phi0 / rate;
|
||||
var onTime = phi0 <= MathF.PI / 2f ? 0f : (phi0 - MathF.PI / 2f) / rate;
|
||||
if (onTime >= window) return 0f;
|
||||
|
||||
var endTime = min(turnTime, window);
|
||||
float Integral(float t)
|
||||
{
|
||||
var u = phi0 - rate * t;
|
||||
return -(window - t) * sin(u) / rate - cos(u) / (rate * rate);
|
||||
}
|
||||
var turning = endTime > onTime ? a * (Integral(endTime) - Integral(onTime)) : 0f;
|
||||
var holding = turnTime < window ? .5f * a * (window - turnTime) * (window - turnTime) : 0f;
|
||||
return turning + holding;
|
||||
}
|
||||
|
||||
private static float Wrap(float angle)
|
||||
{
|
||||
while (angle > MathF.PI) angle -= 2f * MathF.PI;
|
||||
while (angle <= -MathF.PI) angle += 2f * MathF.PI;
|
||||
return angle;
|
||||
}
|
||||
|
||||
// The one decision about what a round does, made from the shooter's state and a target and nothing else:
|
||||
// no draw, no side effect. Fire freezes it into a PendingShot; Inspect shows it to the HUD; the weapons ask
|
||||
// it whether a round is refused before they spend anything -- so a refusal is decided once, by the same
|
||||
|
|
@ -1848,6 +1944,9 @@ public struct FireControlDiagnostic
|
|||
public float PFire;
|
||||
public float PSpread;
|
||||
public float POnHull;
|
||||
// The target's evasion along the shot (metres) and the share of the shot that survives it at this shooter's Tracking.
|
||||
public float Evasion;
|
||||
public float PEvasion;
|
||||
public float PBase;
|
||||
// What a round from this weapon does (Solve): the forecast for a fused weapon, whose percentages above do
|
||||
// not describe it. BurstReach is read only when Outcome is Burst.
|
||||
|
|
@ -1855,6 +1954,52 @@ public struct FireControlDiagnostic
|
|||
public float BurstReach;
|
||||
}
|
||||
|
||||
// What a ship can do at this instant, in its own frame: acceleration along each body axis (m/s^2) and the kinematic
|
||||
// turn rate each way (rad/s; turning toward the ship's right is Clockwise). Each propulsor reports its share with
|
||||
// its own Execute arithmetic and Ship sums them; nothing stores it. Zero is the envelope of anything that is not a Ship.
|
||||
public readonly struct ManoeuvreEnvelope
|
||||
{
|
||||
public readonly float Forward, Reverse, Left, Right, Clockwise, CounterClockwise;
|
||||
|
||||
public ManoeuvreEnvelope(float forward, float reverse, float left, float right, float clockwise, float counterClockwise)
|
||||
{
|
||||
Forward = forward;
|
||||
Reverse = reverse;
|
||||
Left = left;
|
||||
Right = right;
|
||||
Clockwise = clockwise;
|
||||
CounterClockwise = counterClockwise;
|
||||
}
|
||||
|
||||
public static ManoeuvreEnvelope operator +(ManoeuvreEnvelope a, ManoeuvreEnvelope b) => new ManoeuvreEnvelope(
|
||||
a.Forward + b.Forward, a.Reverse + b.Reverse, a.Left + b.Left, a.Right + b.Right,
|
||||
a.Clockwise + b.Clockwise, a.CounterClockwise + b.CounterClockwise);
|
||||
}
|
||||
|
||||
// How unpredictably an entity is changing its motion, observed from its measured acceleration. The trend is an
|
||||
// exponential average of the acceleration, the innovation is what departs from it (low-passed at a quarter of the
|
||||
// window), and the moments are the exponential average of that innovation's outer product. A steady burn and
|
||||
// coasting have no innovation; a weave at the window's scale does; dithering much faster than the window averages out.
|
||||
public struct ManoeuvreTrack
|
||||
{
|
||||
public float2 Trend;
|
||||
public float2 Innovation;
|
||||
public float3 Moments;
|
||||
|
||||
public void Observe(float2 acceleration, float dt, float window)
|
||||
{
|
||||
if (dt <= 0f || window <= 0f) return;
|
||||
var k = 1f - exp(-dt / window);
|
||||
var k4 = 1f - exp(-4f * dt / window);
|
||||
Trend += k * (acceleration - Trend);
|
||||
Innovation += k4 * ((acceleration - Trend) - Innovation);
|
||||
Moments += k * (float3(Innovation.x * Innovation.x, Innovation.x * Innovation.y, Innovation.y * Innovation.y) - Moments);
|
||||
}
|
||||
|
||||
// The root-mean-square innovation across the axis n (a unit vector).
|
||||
public float LateralRms(float2 n) => sqrt(max(0f, n.x * n.x * Moments.x + 2f * n.x * n.y * Moments.y + n.y * n.y * Moments.z));
|
||||
}
|
||||
|
||||
// Cut 12.2 (docs/fire-control-cut.md): a merged interval of a hull's lateral shadow, in the schematic frame's
|
||||
// lateral (ell) coordinate.
|
||||
// S3 fix batch (Hands, 2026-09-25): IComparable<Interval> (sorted by Lo) instead of a separate IComparer<T> --
|
||||
|
|
|
|||
Loading…
Reference in a new issue