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); } } }