Загрузка данных


using UnityEngine;

[DisallowMultipleComponent]
public class PlayerMovement : MonoBehaviour
{
    [Header("Main Paths (max 2)")]
    [SerializeField] private Path mainPath1;
    [SerializeField] private Path mainPath2;

    [Header("Movement")]
    [SerializeField] private float moveSpeed = 3.5f;
    [SerializeField] private float rotateSpeed = 14f;

    [Header("Switching (no teleport)")]
    [SerializeField] private float switchBlendDuration = 0.15f;

    [Header("Camera Follow")]
    [SerializeField] private Camera cam;
    [SerializeField] private float cameraSmoothTime = 0.12f;
    [SerializeField] private float cameraRotateSpeed = 10f;
    [SerializeField] private float cameraLookAhead = 0.5f;

    [Header("Headbob")]
    [SerializeField] private float bobFrequency = 8f;
    [SerializeField] private float bobAmplitude = 0.03f;
    [SerializeField] private float bobSwayAmplitude = 0.015f;

    [Header("Animations")]
    [SerializeField] private Animator animator;

    [Header("Visuals")]
    [SerializeField] private Transform meshRoot;
    [SerializeField] private float meshRotateSpeed = 18f;

    [Tooltip("0 or 180. If the character faces backwards, switch between 0 and 180.")]
    [SerializeField] private float modelYawOffset = 180f;

    [Tooltip("If true: when idle the mesh faces the camera (Y axis only).")]
    [SerializeField] private bool idleFaceCamera = true;

    [Header("FBX Offset Fix (IMPORTANT)")]
    [Tooltip("A point that represents the TRUE character position (hips/pelvis or skinned mesh). Used for snapping to path so FBX pivot offsets won't teleport you.")]
    [SerializeField] private Transform positionReference;

    [Tooltip("Optional tweak if your reference point is slightly off (usually keep at 0,0,0).")]
    [SerializeField] private Vector3 referenceOffset = Vector3.zero;

    // Paths / state
    private Path activePath;
    private Path lastMainPath;
    private float s;

    // Door trigger state
    private bool inDoorTrigger;
    private Path doorPathInTrigger;

    // Main<->Main trigger state
    private bool inMainSwitchTrigger;
    private Collider mainSwitchColliderIn;

    // No-teleport blend
    private bool blending;
    private float blendT;
    private Vector3 blendFromPos;

    // Camera rig
    private Transform camRig;
    private Vector3 camRigVel;
    private Vector3 camLocalBasePos;
    private float bobTimer;

    // Camera follow math
    private Vector3 camOffsetWorld;
    private Vector3 camOffsetLocal;
    private Quaternion camRigRotOffset;
    private Vector3 camFwdSmoothed;

    // door-camera lock
    private bool camLockedOnDoor;
    private Quaternion camLockedPathRot;

    // Animation state so we don't spam triggers
    private bool wasMoving;

    private void Start()
    {
        if (mainPath1 == null)
        {
            Debug.LogError("[PlayerMovement] mainPath1 missing.");
            enabled = false;
            return;
        }

        if (meshRoot == null) meshRoot = transform;

        activePath = mainPath1;
        lastMainPath = mainPath1;

        // Use reference (mesh/hips) to decide where we are on the path
        s = activePath.ClosestS(GetReferenceWorldPos());

        // Snap root so that reference point lands on the path (fixes FBX pivot offsets)
        SnapRootToPathAtS(s);

        // After snap, start blending from current (already correct) pos
        blending = false;

        SetupCamera();

        wasMoving = false;
        SetAnimIdle();
    }

    private void Update()
    {
        HandleSwitches();
        HandleMovement();
        HandleCamera();
    }

    // === REFERENCE / OFFSET FIX ===
    private Vector3 GetReferenceWorldPos()
    {
        if (positionReference != null) return positionReference.position + referenceOffset;
        return transform.position;
    }

    private void SnapRootToPathAtS(float sValue)
    {
        if (activePath == null || activePath.TotalLength <= 0f) return;

        Vector3 desiredRefPos = activePath.EvaluatePosition(sValue);

        if (positionReference == null)
        {
            transform.position = desiredRefPos;
            return;
        }

        Vector3 currentRefPos = positionReference.position + referenceOffset;
        Vector3 delta = desiredRefPos - currentRefPos;
        transform.position += delta;
    }

    private void SetupCamera()
    {
        if (cam == null) cam = Camera.main;
        if (cam == null) return;

        camRig = new GameObject("CameraRig_Runtime").transform;
        camRig.position = cam.transform.position;
        camRig.rotation = cam.transform.rotation;

        cam.transform.SetParent(camRig, true);
        camLocalBasePos = cam.transform.localPosition;

        camOffsetWorld = camRig.position - transform.position;

        Vector3 fwd0 = GetPathForward(activePath, s);
        Quaternion pathRot0 = Quaternion.LookRotation(fwd0, Vector3.up);

        camOffsetLocal = Quaternion.Inverse(pathRot0) * camOffsetWorld;
        camRigRotOffset = Quaternion.Inverse(pathRot0) * camRig.rotation;

        camFwdSmoothed = fwd0;

        camLockedOnDoor = false;
    }

    // === INPUT HELPERS ===
    private float GetMoveAxis(Path path)
    {
        if (path == null) return 0f;

        float axis = 0f;

        if (path.MoveAxis == PathMoveAxis.Horizontal)
        {
            if (Input.GetKey(KeyCode.A)) axis -= 1f;
            if (Input.GetKey(KeyCode.D)) axis += 1f;
        }
        else
        {
            if (Input.GetKey(KeyCode.S)) axis -= 1f;
            if (Input.GetKey(KeyCode.W)) axis += 1f;
        }

        return axis;
    }

    private bool OppositeAxisPressedDown(Path path)
    {
        if (path == null) return false;

        if (path.MoveAxis == PathMoveAxis.Horizontal)
            return Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S);
        else
            return Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D);
    }

    // === SWITCHING LOGIC ===
    private void HandleSwitches()
    {
        if (activePath == null) return;

        if (!activePath.isDoorPath)
        {
            if (inDoorTrigger && doorPathInTrigger != null && OppositeAxisPressedDown(activePath))
            {
                lastMainPath = activePath;
                SwitchActivePath(doorPathInTrigger);
                return;
            }

            if (inMainSwitchTrigger && mainSwitchColliderIn != null)
            {
                if (activePath.MainSwitchTrigger != null && activePath.MainSwitchTrigger == mainSwitchColliderIn)
                {
                    Path target = activePath.OtherMainPath;
                    if (target != null && OppositeAxisPressedDown(activePath))
                    {
                        SwitchActivePath(target);
                        return;
                    }
                }
            }
        }
        else
        {
            if (inDoorTrigger && doorPathInTrigger == activePath && OppositeAxisPressedDown(activePath))
            {
                Path back = lastMainPath != null ? lastMainPath : mainPath1;
                SwitchActivePath(back);
                return;
            }
        }
    }

    private void SwitchActivePath(Path newPath)
    {
        if (newPath == null) return;
        if (activePath == newPath) return;

        bool switchingToDoor = newPath.isDoorPath;

        if (switchingToDoor)
        {
            camLockedOnDoor = true;

            Vector3 fwd = camFwdSmoothed.sqrMagnitude < 0.0001f
                ? transform.forward
                : camFwdSmoothed.normalized;

            camLockedPathRot = Quaternion.LookRotation(fwd, Vector3.up);
        }
        else
        {
            camLockedOnDoor = false;
        }

        activePath = newPath;

        if (!activePath.isDoorPath)
            lastMainPath = activePath;

        // IMPORTANT: use reference point for ClosestS
        s = activePath.ClosestS(GetReferenceWorldPos());

        if (!switchingToDoor && activePath != null && activePath.TotalLength > 0f)
        {
            camFwdSmoothed = GetPathForward(activePath, s);
        }

        BeginBlend();
    }

    private void BeginBlend()
    {
        blending = true;
        blendT = 0f;
        blendFromPos = transform.position;
    }

    // === MOVEMENT ===
    private void HandleMovement()
    {
        if (activePath == null || activePath.TotalLength <= 0f) return;

        float axis = GetMoveAxis(activePath);
        bool moving = Mathf.Abs(axis) > 0.001f;

        // --- Animations (trigger only when state changes) ---
        if (animator != null && moving != wasMoving)
        {
            if (moving) SetAnimWalk();
            else SetAnimIdle();
            wasMoving = moving;
        }

        // --- Move along spline ---
        s = Mathf.Clamp(s + axis * moveSpeed * Time.deltaTime, 0f, activePath.TotalLength);

        Vector3 desiredRefPos = activePath.EvaluatePosition(s);

        Vector3 pathPosForRoot;
        if (positionReference != null)
        {
            Vector3 currentRefPos = positionReference.position + referenceOffset;
            Vector3 delta = desiredRefPos - currentRefPos;
            pathPosForRoot = transform.position + delta;
        }
        else
        {
            pathPosForRoot = desiredRefPos;
        }

        if (blending)
        {
            blendT += Time.deltaTime / Mathf.Max(0.0001f, switchBlendDuration);
            float t = Mathf.Clamp01(blendT);
            transform.position = Vector3.Lerp(blendFromPos, pathPosForRoot, t);
            if (t >= 1f) blending = false;
        }
        else
        {
            transform.position = pathPosForRoot;
        }

        // --- Root rotation (movement direction) ---
        if (moving)
        {
            Vector3 tan = activePath.EvaluateTangent(s);
            if (axis < 0f) tan = -tan;

            Vector3 flat = new Vector3(tan.x, 0f, tan.z);
            if (flat.sqrMagnitude < 0.0001f) flat = tan;

            Quaternion targetRot = Quaternion.LookRotation(flat.normalized, Vector3.up);

            // FIX: apply model offset so it doesn't walk backwards
            targetRot *= Quaternion.Euler(0f, modelYawOffset, 0f);

            transform.rotation = Quaternion.Slerp(
                transform.rotation,
                targetRot,
                1f - Mathf.Exp(-rotateSpeed * Time.deltaTime)
            );
        }

        // --- Mesh visual rotation rules ---
        UpdateMeshFacing(axis, moving);

        HandleHeadbob(moving);
    }

    private void UpdateMeshFacing(float axis, bool moving)
    {
        if (meshRoot == null) return;

        float t = 1f - Mathf.Exp(-meshRotateSpeed * Time.deltaTime);

        if (moving && activePath != null)
        {
            Vector3 tan = activePath.EvaluateTangent(s);
            if (axis < 0f) tan = -tan;

            Vector3 flat = new Vector3(tan.x, 0f, tan.z);
            if (flat.sqrMagnitude < 0.0001f) return;

            Quaternion meshTarget = Quaternion.LookRotation(flat.normalized, Vector3.up);

            // FIX: apply model offset so mesh faces correct way
            meshTarget *= Quaternion.Euler(0f, modelYawOffset, 0f);

            meshRoot.rotation = Quaternion.Slerp(meshRoot.rotation, meshTarget, t);
            return;
        }

        // IDLE: face TOWARDS the camera (Y axis only)
        if (idleFaceCamera && cam != null)
        {
            Vector3 toCam = cam.transform.position - meshRoot.position; // towards camera
            Vector3 flatToCam = new Vector3(toCam.x, 0f, toCam.z);
            if (flatToCam.sqrMagnitude < 0.0001f) return;

            Quaternion meshTarget = Quaternion.LookRotation(flatToCam.normalized, Vector3.up);

            // same model offset
            meshTarget *= Quaternion.Euler(0f, modelYawOffset, 0f);

            meshRoot.rotation = Quaternion.Slerp(meshRoot.rotation, meshTarget, t);
        }
    }

    private void SetAnimWalk()
    {
        animator.ResetTrigger("OnIdle");
        animator.SetTrigger("OnWalk");
    }

    private void SetAnimIdle()
    {
        animator.ResetTrigger("OnWalk");
        animator.SetTrigger("OnIdle");
    }

    // === CAMERA ===
    private void HandleCamera()
    {
        if (camRig == null || cam == null) return;

        Quaternion pathRot;

        if (camLockedOnDoor)
        {
            pathRot = camLockedPathRot;
        }
        else
        {
            if (activePath == null || activePath.TotalLength <= 0f) return;

            Vector3 targetFwd = GetPathForward(activePath, s);
            float rotT = 1f - Mathf.Exp(-cameraRotateSpeed * Time.deltaTime);

            camFwdSmoothed = Vector3.Slerp(camFwdSmoothed, targetFwd, rotT);
            if (camFwdSmoothed.sqrMagnitude < 0.0001f) camFwdSmoothed = targetFwd;

            pathRot = Quaternion.LookRotation(camFwdSmoothed.normalized, Vector3.up);
        }

        Vector3 desiredRigPos = transform.position + (pathRot * camOffsetLocal);

        camRig.position = Vector3.SmoothDamp(
            camRig.position,
            desiredRigPos,
            ref camRigVel,
            cameraSmoothTime
        );

        Quaternion desiredRigRot = pathRot * camRigRotOffset;

        float rotBlend = 1f - Mathf.Exp(-cameraRotateSpeed * Time.deltaTime);
        camRig.rotation = Quaternion.Slerp(camRig.rotation, desiredRigRot, rotBlend);
    }

    private Vector3 GetPathForward(Path path, float sNow)
    {
        if (path == null || path.TotalLength <= 0f) return transform.forward;

        float a = Mathf.Clamp(sNow - cameraLookAhead, 0f, path.TotalLength);
        float b = Mathf.Clamp(sNow + cameraLookAhead, 0f, path.TotalLength);

        Vector3 pa = path.EvaluatePosition(a);
        Vector3 pb = path.EvaluatePosition(b);

        Vector3 d = pb - pa;

        Vector3 flat = new Vector3(d.x, 0f, d.z);
        if (flat.sqrMagnitude < 0.0001f) flat = transform.forward;

        return flat.normalized;
    }

    private void HandleHeadbob(bool moving)
    {
        if (cam == null) return;

        if (moving)
        {
            bobTimer += Time.deltaTime * bobFrequency;
            float bobY = Mathf.Sin(bobTimer) * bobAmplitude;
            float bobX = Mathf.Cos(bobTimer * 0.5f) * bobSwayAmplitude;
            cam.transform.localPosition = camLocalBasePos + new Vector3(bobX, bobY, 0f);
        }
        else
        {
            bobTimer = 0f;
            cam.transform.localPosition = Vector3.Lerp(
                cam.transform.localPosition,
                camLocalBasePos,
                1f - Mathf.Exp(-12f * Time.deltaTime)
            );
        }
    }

    // === TRIGGERS ===
    private void OnTriggerEnter(Collider other)
    {
        Path p = other.GetComponentInParent<Path>();
        if (p != null && p.isDoorPath)
        {
            inDoorTrigger = true;
            doorPathInTrigger = p;
        }

        if (mainPath1 != null && mainPath1.MainSwitchTrigger == other)
        {
            inMainSwitchTrigger = true;
            mainSwitchColliderIn = other;
        }
        else if (mainPath2 != null && mainPath2.MainSwitchTrigger == other)
        {
            inMainSwitchTrigger = true;
            mainSwitchColliderIn = other;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        Path p = other.GetComponentInParent<Path>();
        if (p != null && p == doorPathInTrigger)
        {
            inDoorTrigger = false;
            doorPathInTrigger = null;
        }

        if (inMainSwitchTrigger && other == mainSwitchColliderIn)
        {
            inMainSwitchTrigger = false;
            mainSwitchColliderIn = null;
        }
    }
}