using UnityEngine; /// /// Strategy — free surface tracking. /// Ghost follows the ray hit point, aligns to the surface normal, /// and socket-snaps when a compatible socket is nearby. /// Rotation input rotates around the surface normal (or socket forward when snapped). /// public class FreeMoveStrategy : IGhostMovementStrategy { public void UpdateMovement(ref GhostMovementContext ctx) { bool snapped = TrySocketSnap(ref ctx); if (!snapped) { if (ctx.AlignToSurfaceNormal) AlignToSurfaceNormal(ref ctx); MoveAlongSurface(ref ctx); } // Rotation axis: socket forward when snapped, surface normal otherwise ctx.RotationAxis = (snapped && ctx.MySocket != null) ? ctx.MySocket.transform.forward : ctx.SurfaceNormal; } // ------------------------------------------------------------------------- private static bool TrySocketSnap(ref GhostMovementContext ctx) { if (ctx.TargetSocket == null) return false; bool compatible = ctx.MySocket == null || ctx.TargetSocket.CanAccept(ctx.MySocket); if (!compatible) return false; PerformSocketSnap(ref ctx); return true; } private static void PerformSocketSnap(ref GhostMovementContext ctx) { Vector3 targetNormal = ctx.TargetSocket.GetNormal().normalized; if (ctx.MySocket != null) { // 1. Primary Alignment: Align the exact socket normals Vector3 myNormal = ctx.MySocket.GetNormal().normalized; Vector3 myTargetNormal = -ctx.TargetSocket.GetNormal().normalized; Quaternion primaryRot = Quaternion.FromToRotation(myNormal, myTargetNormal); ctx.GhostTransform.rotation = primaryRot * ctx.GhostTransform.rotation; // 2. Secondary Alignment: Align the 'Up' (or Forward) vectors to prevent random twisting/rolling Vector3 mySecondary = Mathf.Abs(Vector3.Dot(ctx.MySocket.transform.up, myTargetNormal)) < 0.9f ? ctx.MySocket.transform.up : ctx.MySocket.transform.forward; Vector3 targetSecondary = Mathf.Abs(Vector3.Dot(ctx.TargetSocket.transform.up, -myTargetNormal)) < 0.9f ? ctx.TargetSocket.transform.up : ctx.TargetSocket.transform.forward; // Project secondary vectors onto the flat plane between the sockets Vector3 projectedMySecondary = Vector3.ProjectOnPlane(mySecondary, myTargetNormal).normalized; Vector3 projectedTargetSecondary = Vector3.ProjectOnPlane(targetSecondary, myTargetNormal).normalized; if (projectedMySecondary != Vector3.zero && projectedTargetSecondary != Vector3.zero) { // Find the twist angle needed to align the secondary axes float angle = Vector3.SignedAngle(projectedMySecondary, projectedTargetSecondary, myTargetNormal); // Snap to the nearest 45 degrees to allow manual 'R' rotations to persist! float correction = angle - Mathf.Round(angle / 45f) * 45f; // Rotate the ghost around its own pivot ctx.GhostTransform.rotation = Quaternion.AngleAxis(correction, myTargetNormal) * ctx.GhostTransform.rotation; } // 3. Translate the ghost so the sockets physically touch Vector3 shift = ctx.TargetSocket.transform.position - ctx.MySocket.transform.position; ctx.GhostTransform.position += shift; } else { Transform pivot = ctx.AttachPoint != null ? ctx.AttachPoint : ctx.GhostTransform; Vector3 pivotOffset = pivot.position - ctx.GhostTransform.position; ctx.GhostTransform.position = ctx.TargetSocket.transform.position - pivotOffset; } } private static void AlignToSurfaceNormal(ref GhostMovementContext ctx) { Transform pivot = GetCurrentPivot(ref ctx); Quaternion target = Quaternion.FromToRotation(pivot.up, ctx.SurfaceNormal) * ctx.GhostTransform.rotation; if (ctx.SnapRotation) target = SnapToNearestRightAngle(target); ctx.GhostTransform.rotation = target; } private static void MoveAlongSurface(ref GhostMovementContext ctx) { Transform pivot = GetCurrentPivot(ref ctx); Vector3 pivotOffset = pivot.position - ctx.GhostTransform.position; Vector3 targetPos = ctx.SurfacePoint - pivotOffset; if (ctx.HeightOffset != 0f) targetPos += ctx.SurfaceNormal * ctx.HeightOffset; ctx.GhostTransform.position = Vector3.Lerp( ctx.GhostTransform.position, targetPos, Time.deltaTime * ctx.FollowSpeed); } private static Transform GetCurrentPivot(ref GhostMovementContext ctx) { return ctx.AttachPoint != null ? ctx.AttachPoint : ctx.GhostTransform; } private static Quaternion SnapToNearestRightAngle(Quaternion q) { Vector3 e = q.eulerAngles; e.x = Mathf.Round(e.x / 90f) * 90f; e.y = Mathf.Round(e.y / 90f) * 90f; e.z = Mathf.Round(e.z / 90f) * 90f; return Quaternion.Euler(e); } }