Files
2026-07-21 08:56:10 +03:00

186 lines
7.1 KiB
C#

using UnityEngine;
public class AxisRotateStrategy : IGhostMovementStrategy
{
public bool OverridesSurfaceTracking => true;
public enum LocalAxis { Up, Forward, Right }
private readonly LocalAxis _localAxis;
private readonly Vector3 _lockedPosition;
private bool _isRotating;
private float _accumulatedDegrees;
private Vector2 _lastMousePos;
private Quaternion _unsnappedRot;
private Vector3 _unsnappedPos;
private bool _wasSnapped;
public float Sensitivity = 0.4f;
public float SnapAngle = 15f;
public bool EnableSnapping = false;
/// <param name="localAxis">Local axis of the ghost to rotate around.</param>
/// <param name="lockedPosition">Ghost world position frozen for the entire interaction.</param>
public AxisRotateStrategy(LocalAxis localAxis, Vector3 lockedPosition)
{
_localAxis = localAxis;
_lockedPosition = lockedPosition;
}
public void BeginRotate()
{
_lastMousePos = Input.mousePosition;
_accumulatedDegrees = 0f;
_isRotating = true;
_wasSnapped = false;
}
public void EndRotate() => _isRotating = false;
public void UpdateMovement(ref GhostMovementContext ctx)
{
// 1. Revert previous frame's snap so we rotate from the true axial position
if (_wasSnapped)
{
ctx.GhostTransform.position = _unsnappedPos;
ctx.GhostTransform.rotation = _unsnappedRot;
_wasSnapped = false;
}
// Read the axis from the ghost's current orientation every frame
// so it stays local regardless of how the ghost has been rotated
Vector3 worldAxis = GetWorldAxis(ctx.GhostTransform);
if (_isRotating)
{
Vector2 mouseDelta = (Vector2)Input.mousePosition - _lastMousePos;
_lastMousePos = Input.mousePosition;
if (mouseDelta.sqrMagnitude > 0.001f)
{
// Project the world axis into screen space to get its 2D direction
Vector3 axisScreenStart = Camera.main.WorldToScreenPoint(_lockedPosition);
Vector3 axisScreenEnd = Camera.main.WorldToScreenPoint(_lockedPosition + worldAxis);
Vector2 axisScreen = (axisScreenEnd - axisScreenStart).normalized;
// The rotation direction is the perpendicular to the screen-space axis
// Dot the mouse delta against that perpendicular to get signed rotation
Vector2 perpendicular = new Vector2(-axisScreen.y, axisScreen.x);
float signedDelta = Vector2.Dot(mouseDelta, perpendicular);
if (ctx.SnapRotation || EnableSnapping)
{
_accumulatedDegrees += signedDelta * Sensitivity;
float sign = Mathf.Sign(_accumulatedDegrees);
while (Mathf.Abs(_accumulatedDegrees) >= SnapAngle)
{
ctx.GhostTransform.RotateAround(_lockedPosition, worldAxis, SnapAngle * sign);
_accumulatedDegrees -= SnapAngle * sign;
}
}
else
{
float pendingDegrees = signedDelta * Sensitivity;
if (pendingDegrees != 0f)
{
ctx.GhostTransform.RotateAround(_lockedPosition, worldAxis, pendingDegrees);
}
}
}
}
// Save the pure axial rotation state
_unsnappedPos = ctx.GhostTransform.position;
_unsnappedRot = ctx.GhostTransform.rotation;
// Attempt to snap to nearby sockets visually
if (TrySocketSnap(ref ctx))
{
_wasSnapped = true;
}
ctx.RotationAxis = worldAxis;
}
private bool TrySocketSnap(ref GhostMovementContext ctx)
{
SocketPoint[] mySockets = ctx.GhostTransform.GetComponentsInChildren<SocketPoint>();
if (mySockets.Length == 0) return false;
SocketPoint bestMySocket = null;
SocketPoint bestTargetSocket = null;
float bestDist = 0.5f;
foreach (var mySocket in mySockets)
{
Collider[] hits = Physics.OverlapSphere(mySocket.transform.position, bestDist, Physics.AllLayers, QueryTriggerInteraction.Collide);
foreach (var hit in hits)
{
if (hit.transform.root == ctx.GhostTransform.root) continue;
SocketPoint[] targetSockets = hit.transform.root.GetComponentsInChildren<SocketPoint>();
foreach (var target in targetSockets)
{
if (target.IsOccupied()) continue;
if (!target.CanAccept(mySocket)) continue;
float dist = Vector3.Distance(mySocket.transform.position, target.transform.position);
if (dist < bestDist)
{
bestDist = dist;
bestMySocket = mySocket;
bestTargetSocket = target;
}
}
}
}
if (bestMySocket != null && bestTargetSocket != null)
{
// Align the rotations exactly like FreeMoveStrategy
Vector3 myNormal = bestMySocket.GetNormal().normalized;
Vector3 myTargetNormal = -bestTargetSocket.GetNormal().normalized;
Quaternion primaryRot = Quaternion.FromToRotation(myNormal, myTargetNormal);
ctx.GhostTransform.rotation = primaryRot * ctx.GhostTransform.rotation;
Vector3 mySecondary = Mathf.Abs(Vector3.Dot(bestMySocket.transform.up, myTargetNormal)) < 0.9f
? bestMySocket.transform.up
: bestMySocket.transform.forward;
Vector3 targetSecondary = Mathf.Abs(Vector3.Dot(bestTargetSocket.transform.up, -myTargetNormal)) < 0.9f
? bestTargetSocket.transform.up
: bestTargetSocket.transform.forward;
Vector3 projectedMySecondary = Vector3.ProjectOnPlane(mySecondary, myTargetNormal).normalized;
Vector3 projectedTargetSecondary = Vector3.ProjectOnPlane(targetSecondary, myTargetNormal).normalized;
if (projectedMySecondary != Vector3.zero && projectedTargetSecondary != Vector3.zero)
{
float angle = Vector3.SignedAngle(projectedMySecondary, projectedTargetSecondary, myTargetNormal);
float correction = angle - Mathf.Round(angle / 45f) * 45f;
ctx.GhostTransform.rotation = Quaternion.AngleAxis(correction, myTargetNormal) * ctx.GhostTransform.rotation;
}
// Translate to connect the sockets perfectly
Vector3 shift = bestTargetSocket.transform.position - bestMySocket.transform.position;
ctx.GhostTransform.position += shift;
return true;
}
return false;
}
// Re-evaluated every frame from the ghost's current transform
private Vector3 GetWorldAxis(Transform t) => _localAxis switch
{
LocalAxis.Up => t.up,
LocalAxis.Forward => t.forward,
LocalAxis.Right => t.right,
_ => t.up,
};
}