Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions Content.Client/_Mono/FireControl/UI/FireControlLeadSolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2026 HardLight contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later

using System.Numerics;

namespace Content.Client._Mono.FireControl.UI;

/// <summary>
/// Client-side helper that mirrors the firing-solution math used by
/// <c>ShipTargetingSystem.FireWeapons</c>. Given a gun position, a target
/// position+velocity and a projectile speed, it returns the world-space
/// point where the gunner should aim so the projectile intercepts the
/// target.
/// </summary>
public static class FireControlLeadSolver
{
/// <summary>
/// Compute the predicted intercept point in world space.
/// </summary>
/// <param name="gunWorldPos">World position of the firing weapon.</param>
/// <param name="targetWorldPos">Current world position of the target.</param>
/// <param name="targetVel">Target's world linear velocity (m/s).</param>
/// <param name="ourVel">Firing platform's world linear velocity (m/s).</param>
/// <param name="projectileSpeed">Projectile muzzle speed (m/s).</param>
/// <param name="lead">Outputs the lead displacement applied to the target (= relVel * hitTime).</param>
/// <param name="hitTime">Predicted travel time, in seconds.</param>
/// <returns>True if a real intercept exists; false otherwise (target uncatchable).</returns>
public static bool TrySolve(
Vector2 gunWorldPos,
Vector2 targetWorldPos,
Vector2 targetVel,
Vector2 ourVel,
float projectileSpeed,
out Vector2 interceptWorldPos,
out Vector2 lead,
out float hitTime)
{
interceptWorldPos = targetWorldPos;
lead = Vector2.Zero;
hitTime = 0f;

if (projectileSpeed <= 0f)
return false;

var gunToDest = targetWorldPos - gunWorldPos;
if (gunToDest.LengthSquared() < 0.0001f)
return false;

var dir = Vector2.Normalize(gunToDest);
var relVel = targetVel - ourVel;

// Decompose relative velocity into along-axis (normVel) and cross-axis (tgVel).
var normVel = dir * Vector2.Dot(relVel, dir);
var tgVel = relVel - normVel;

// Tangential component too fast to ever catch (>= so the sqrt below stays > 0).
var projSpeedSq = projectileSpeed * projectileSpeed;
var tgVelLenSq = tgVel.LengthSquared();
if (tgVelLenSq >= projSpeedSq)
return false;

var normTarget = dir * MathF.Sqrt(projSpeedSq - tgVelLenSq);

// Target receding faster than we can chase.
if (Vector2.Dot(normTarget, normVel) > 0f && normVel.Length() > normTarget.Length())
return false;

var approachVel = (normTarget - normVel).Length();
if (approachVel < 0.001f)
return false;

hitTime = gunToDest.Length() / approachVel;
lead = relVel * hitTime;
interceptWorldPos = targetWorldPos + lead;
return true;
}
}
Loading
Loading