using System;
using System.Numerics;
namespace AcDream.Core.Physics.Motion;
///
/// R4-V1 — pure-math free functions consumed by the (future) MoveToManager
/// port: heading_diff, heading_greater, Position::heading
/// / Frame::get_heading / Frame::set_heading, and
/// Position::cylinder_distance. No GL/App dependency — Core-only,
/// per the Code Structure Rules. NAME WATCH: this file (not
/// MoveToManager.cs, per r4-port-plan.md §3 "New code target") is the
/// R4-V1 deliverable; the manager itself is R4-V2.
///
public static class MoveToMath
{
///
/// Universal heading/distance epsilon (same literal as R3's A5/A6 —
/// r4-moveto-decomp.md §12 constants inventory).
///
public const float Epsilon = 0.000199999995f;
///
/// Retail heading_diff (0x00528fb0, free function, raw
/// 306327-306347), PINNED by direct disassembly of the PDB-matched
/// retail binary (ghidra-confirmations.md §P3 — the strongest evidence
/// tier in the R4 pin set):
///
/// d = h1 - h2;
/// if (fabs(h1 - h2) < F_EPSILON) d = 0;
/// if (d < -F_EPSILON) d += 360;
/// if (F_EPSILON < d && turnCmd != TurnRight) d = 360 - d; // the mirror
/// return d;
///
/// The mirror gates on the turn command NOT being TurnRight
/// (0x6500000d) — TurnLeft (and any other command) measures the
/// COMPLEMENTARY angle. This CONTRADICTS r4-moveto-decomp.md §5g's
/// "arg3 UNUSED" claim, which the Ghidra disassembly pin overrides
/// (V0-pins.md §P3 adjudication). Call sites: BeginTurnToHeading
/// passes the CONSTANT TurnRight (mirror explicitly disabled — the
/// direction pick stays the ≤180 test elsewhere); HandleTurnToHeading
/// passes the LIVE current_command (can be TurnLeft).
///
/// First heading, degrees.
/// Second heading, degrees.
/// The active turn command id — gates the mirror.
/// Normalized heading difference, degrees.
public static float HeadingDiff(float h1, float h2, uint turnCmd)
{
float d = h1 - h2;
if (MathF.Abs(h1 - h2) < Epsilon)
{
d = 0f;
}
if (d < -Epsilon)
{
d += 360f;
}
if (Epsilon < d && turnCmd != MotionCommand.TurnRight)
{
d = 360f - d;
}
return d;
}
///
/// Retail heading_greater (00528f60, free function, raw
/// 306281-306323), verbatim per r4-moveto-decomp.md §5f:
///
/// if (fabs(a - b) > 180) greater = (b > a); // wrapped case: compare flipped
/// else greater = (a > b);
/// if (turnCmd == TurnRight) return greater;
/// return !greater; // TurnLeft (and any other cmd): inverted
///
/// "Has the turn passed the target heading" — direction-aware,
/// 360°-wrap-aware. The visible TurnRight-arg idiom: the gate is
/// == TurnRight (not != TurnRight as in
/// 's mirror) — every OTHER command inverts,
/// not just TurnLeft specifically.
///
public static bool HeadingGreater(float a, float b, uint turnCmd)
{
bool greater = MathF.Abs(a - b) > 180f
? b > a
: a > b;
return turnCmd == MotionCommand.TurnRight ? greater : !greater;
}
///
/// Retail Position::heading(from, to) (0x005a9520, raw
/// 438288-438290), PINNED per V0-pins.md §P5: compass degrees, 0 =
/// North (+Y), 90 = East (+X), CLOCKWISE, range [0,360).
///
/// heading(from, to) = (450 - atan2Deg(dy, dx)) % 360
///
/// Golden cardinals: N(0,+1)→0, E(+1,0)→90, S(0,-1)→180, W(-1,0)→270.
/// Horizontal (X/Y) only — Z (height) does not participate, matching
/// retail's compass-heading semantics. An in-tree twin of this formula
/// already exists at SceneryHelpers.cs:75 (render-side,
/// independently verified — not reused directly to keep this file
/// GL-free per the Code Structure Rules, but the formula is identical).
///
public static float PositionHeading(Vector3 from, Vector3 to)
{
float dx = to.X - from.X;
float dy = to.Y - from.Y;
float headingDeg = 450f - MathF.Atan2(dy, dx) * (180f / MathF.PI);
headingDeg %= 360f;
if (headingDeg < 0f) headingDeg += 360f;
return headingDeg;
}
///
/// Retail Frame::get_heading (0x00535760, raw 319781) —
/// extracts the compass heading (P5 convention) from a body orientation
/// quaternion. The packer-reuse trap (V0-pins §P5 correction):
/// acdream's outbound packer (GameWindow.YawToAcQuaternion) is
/// wire-correct at the QUATERNION level but its internal scalar
/// intermediate (headingDeg = 180 - yawDeg) is holtburger's
/// SHIFTED convention, not retail's. This method uses the CORRECT
/// scalar bridge derived from acdream's own body convention
/// (PlayerMovementController.cs:1022-1025: Orientation =
/// AxisAngle(Z, Yaw - PI/2), local-forward = +Y, Yaw=0 faces +X):
/// world-forward = (cos Yaw, sin Yaw), so
/// YawDeg = atan2Deg(forward.Y, forward.X) and
/// heading = (90 - YawDeg) mod 360 — the exact inverse of
/// . Identity quaternion (Yaw=PI/2, i.e. facing
/// +Y/North) → heading 0, matching P5's "identity quaternion faces
/// heading 0" pin.
///
public static float GetHeading(Quaternion orientation)
{
var forward = Vector3.Transform(new Vector3(0f, 1f, 0f), orientation);
float yawDeg = MathF.Atan2(forward.Y, forward.X) * (180f / MathF.PI);
float headingDeg = 90f - yawDeg;
headingDeg %= 360f;
if (headingDeg < 0f) headingDeg += 360f;
return headingDeg;
}
///
/// Retail Frame::set_heading (0x00535e40, raw
/// 320055-320066) — builds a body orientation quaternion facing
/// (P5 compass convention), preserving
/// acdream's body-orientation convention (rotation about world Z only;
/// 's pitch/roll, if any, is
/// discarded — matching retail's set_heading, which is a pure
/// yaw-about-Z setter). Exact inverse of :
/// YawDeg = 90 - headingDeg, then Orientation =
/// AxisAngle(Z, Yaw - PI/2) per
/// PlayerMovementController.cs:1025's convention.
///
/// Unused beyond signature parity with
/// the render-side SceneryHelpers.SetHeading twin — retail's
/// set_heading is a pure yaw-about-Z setter with no dependency
/// on the prior orientation's roll/pitch component in the body-frame
/// convention this port uses.
/// Desired compass heading, degrees.
public static Quaternion SetHeading(Quaternion baseOrientation, float headingDeg)
{
_ = baseOrientation;
float yawDeg = 90f - headingDeg;
float yaw = yawDeg * (MathF.PI / 180f);
return Quaternion.CreateFromAxisAngle(Vector3.UnitZ, yaw - MathF.PI / 2f);
}
///
/// Retail Position::cylinder_distance, the pure-math shape
/// consumed by MoveToManager::GetCurrentDistance
/// (005291b0, r4-moveto-decomp.md §5a) when use_spheres
/// (wire bit 0x400) is set — object moves use edge-to-edge cylinder
/// distance; position moves use plain center (Euclidean) distance
/// instead (not ported here — Vector3.Distance already covers
/// it). BN garbles the x87 plumbing in the raw, so the exact
/// radius-combination arithmetic is not directly visible; ported per
/// the PDB argument ORDER (own radius/height/position, target
/// radius/height/position) with the standard cylinder-distance shape:
/// planar (X/Y) center distance minus the sum of both radii, clamped
/// at zero (overlapping cylinders report 0, never negative). Height is
/// accepted for signature parity with the retail call (own/target
/// height feed the caller's contact-plane logic elsewhere) but does
/// NOT participate in this edge-to-edge planar computation — matching
/// retail's use of cylinder_distance purely for the horizontal arrival
/// gate.
///
public static float CylinderDistance(
float ownRadius, float ownHeight, Vector3 ownPos,
float targetRadius, float targetHeight, Vector3 targetPos)
{
_ = ownHeight;
_ = targetHeight;
float dx = targetPos.X - ownPos.X;
float dy = targetPos.Y - ownPos.Y;
float centerDist = MathF.Sqrt(dx * dx + dy * dy);
float edgeDist = centerDist - ownRadius - targetRadius;
return edgeDist > 0f ? edgeDist : 0f;
}
}