using Unity.Mathematics;
namespace ProjectM.Simulation
{
///
/// Pure, deterministic, Burst-safe helpers for turning a screen-cursor camera ray into a planar (XZ)
/// aim direction (and the ground point it targets) for the top-down player. Isolated from the managed
/// Camera so the projection is unit-testable without a render context (mirrors
/// / CharacterControlMath / BaseGridMath). Used client-side by the
/// input gather (facing direction) and the aim-reticle presentation (the world ground point), but kept
/// here in Simulation alongside the other pure combat/movement math.
///
public static class AimMath
{
///
/// Intersects a camera ray with the horizontal plane at . Returns false (and
/// = default) when the ray is parallel to / points away from the plane (cursor
/// above the horizon); otherwise is the world-space intersection point.
///
/// Camera ray origin (world space).
/// Camera ray direction (need not be normalized).
/// World Y of the aim plane (the player's movement plane).
/// The world-space ground intersection point, when this returns true.
public static bool TryGroundHit(float3 rayOrigin, float3 rayDir, float planeY, out float3 hit)
{
hit = default;
// Solve rayOrigin.y + t * rayDir.y == planeY. A near-zero rayDir.y means the ray runs parallel.
float denom = rayDir.y;
if (math.abs(denom) < 1e-6f)
return false;
float t = (planeY - rayOrigin.y) / denom;
if (t < 0f)
return false; // intersection behind the ray origin (cursor above the horizon line)
hit = rayOrigin + rayDir * t;
return true;
}
///
/// Normalized planar aim direction (world XZ mapped to float2(x, y)) from
/// toward the cursor's ground-projection point. Returns
/// when the ray misses the plane, OR the hit lies within
/// of the player (too close to define a stable heading), so the
/// caller holds its previous aim instead of snapping or spinning when the cursor is over the character.
///
/// Camera ray origin (world space).
/// Camera ray direction (need not be normalized).
/// Player world position; only XZ is used for the heading.
/// World Y of the aim plane.
/// Direction returned when there is no valid hit (e.g. the previous aim).
///
/// World radius around the player inside which facing is held (no update). 0 keeps only the tiny
/// epsilon guard against an exactly-coincident hit (the original behaviour).
///
public static float2 PlanarAimFromRay(float3 rayOrigin, float3 rayDir, float3 playerPos, float planeY,
float2 fallback, float deadZoneRadius = 0f)
{
if (!TryGroundHit(rayOrigin, rayDir, planeY, out var hit))
return fallback;
float2 planar = new float2(hit.x - playerPos.x, hit.z - playerPos.z);
float dz = math.max(1e-3f, deadZoneRadius); // never below the original coincident-hit epsilon
if (math.lengthsq(planar) < dz * dz)
return fallback;
return math.normalize(planar);
}
}
}