using System.Collections.Generic; using UnityEngine; using Fusion; namespace OnlyScove.Scripts { [RequireComponent(typeof(CharacterController), typeof(InputReader), typeof(Animator))] public class PlayerStateMachine : NetworkBehaviour { [field: Header("References")] [field: SerializeField] public CharacterController Controller { get; private set; } [field: SerializeField] public virtual InputReader Input { get; private set; } [field: SerializeField] public Animator Anim { get; private set; } [field: SerializeField] public EnvironmentScanner Scanner { get; private set; } public CameraController Cam { get; private set; } [field: Header("Animator Settings")] [SerializeField] private string speedParamName = "Speed"; [SerializeField] private string velocityXParamName = "Velocity X"; [SerializeField] private string velocityZParamName = "Velocity Z"; private int speedHash; private int velocityXHash; private int velocityZHash; [field: Header("Movement Settings")] [field: SerializeField] public float WalkSpeed { get; private set; } = 3f; [field: SerializeField] public float RunSpeed { get; private set; } = 6f; [field: SerializeField] public float SprintSpeed { get; private set; } = 9f; [field: SerializeField] public float SneakSpeed { get; private set; } = 1.5f; [field: SerializeField] public float DashForce { get; private set; } = 10f; [field: SerializeField] public float RotationSpeed { get; private set; } = 500f; [field: SerializeField] public float AnimationDamping { get; private set; } = 0.2f; [field: Header("Airborne Settings")] [field: SerializeField] public float JumpHeight { get; private set; } = 2f; [field: SerializeField] public float Gravity { get; private set; } = -15f; [field: SerializeField] public float ThrustDownwardForce { get; private set; } = -20f; [field: Header("Ground Check")] [field: SerializeField] public float GroundCheckRadius { get; private set; } = 0.2f; [field: SerializeField] public Vector3 GroundCheckOffset { get; private set; } [field: SerializeField] public LayerMask GroundMask { get; private set; } [field: Header("Interaction")] [field: SerializeField] public float InteractionRange { get; private set; } = 2f; [field: SerializeField] public LayerMask InteractionMask { get; private set; } [Networked] public Quaternion NetworkedCameraRotation { get; set; } public Quaternion CameraRotation { get { if (Runner != null && Runner.IsRunning && Object != null) return NetworkedCameraRotation; if (Cam != null) return Cam.PlanarRotation; return transform.rotation; } } [Networked] public Vector2 NetworkedMoveInput { get; set; } [Networked] public float NetworkedSpeed { get; set; } [Networked] public int StartTick { get; set; } public float FinishTime { get; set; } public bool IsFinished { get; set; } private float startTimeOffline; public Vector2 MoveInput { get; private set; } public bool IsSprintHeld { get; private set; } public float VelocityY { get; set; } public bool IsGrounded { get; private set; } public bool WasGrounded { get; private set; } private List interactablesNearby = new List(); private int currentInteractableIndex = 0; public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None"; public static PlayerStateMachine Local { get; private set; } private PlayerBaseState currentState; private bool hasControl = true; private float localAnimatorSpeed; private bool hasSpeedParam; private bool hasVelocityXParam; private bool hasVelocityZParam; protected virtual void Awake() { Controller = GetComponent(); Input = GetComponent(); Anim = GetComponentInChildren(); Scanner = GetComponent(); if (Anim != null) { // Ép tắt Root Motion để tránh lỗi cộng dồn tốc độ Anim.applyRootMotion = false; foreach (AnimatorControllerParameter param in Anim.parameters) { if (param.name == speedParamName) hasSpeedParam = true; if (param.name == velocityXParamName) hasVelocityXParam = true; if (param.name == velocityZParamName) hasVelocityZParam = true; } } speedHash = Animator.StringToHash(speedParamName); velocityXHash = Animator.StringToHash(velocityXParamName); velocityZHash = Animator.StringToHash(velocityZParamName); } private void Start() { if (Runner == null || !Runner.IsRunning) { InitializePlayer(); } } public override void Spawned() { InitializePlayer(); // QUAN TRỌNG: Vô hiệu hóa Controller của người chơi khác để tránh giật hình. // NetworkTransform sẽ tự lo việc làm mượt vị trí của họ. if (Object != null && !Object.HasInputAuthority && !Runner.IsServer) { if (Controller != null) Controller.enabled = false; } } private void InitializePlayer() { if (currentState == null) { SwitchState(new PlayerIdleState(this)); } bool isOffline = Runner == null || !Runner.IsRunning; bool hasAuthority = Object != null && Object.HasInputAuthority; if (isOffline || hasAuthority) { Local = this; startTimeOffline = Time.time; CameraController cameraController = GameObject.FindAnyObjectByType(); if (cameraController != null) { Cam = cameraController; Cam.followTarget = transform; Cam.inputReader = Input; } Input.OnNextInteractEvent += OnNextInteract; Input.OnPreviousInteractEvent += OnPreviousInteract; if (Controller != null) Controller.enabled = true; } if (!isOffline && Object.HasStateAuthority) { StartTick = (int)Runner.Tick; } } public void Rotate(Vector3 moveDirection, float deltaTime) { if (moveDirection == Vector3.zero) return; Quaternion targetRot = Quaternion.LookRotation(moveDirection); transform.rotation = Quaternion.RotateTowards( transform.rotation, targetRot, RotationSpeed * deltaTime ); } public void Move(Vector3 velocity, float animatorSpeed, float deltaTime) { bool canMove = (Runner == null || !Runner.IsRunning) || Object.HasInputAuthority || Runner.IsServer; if (!canMove) return; if (Controller != null && Controller.enabled) { // SỬA LỖI CHẠY NHANH: Chỉ thực hiện lệnh Move vật lý trong Forward Tick. // Điều này ngăn việc cộng dồn vận tốc khi Fusion chạy Re-simulation. if (Runner == null || !Runner.IsRunning || Runner.IsForward) { Controller.Move(velocity * deltaTime); } } localAnimatorSpeed = animatorSpeed; if (Object != null && Object.HasStateAuthority) { NetworkedSpeed = animatorSpeed; NetworkedMoveInput = MoveInput; } UpdateAnimator(deltaTime); } private void UpdateAnimator(float deltaTime) { if (Anim == null) return; float speedValue; Vector2 inputVector; if (Runner == null || !Runner.IsRunning || Object.HasInputAuthority) { speedValue = localAnimatorSpeed; inputVector = MoveInput; } else { speedValue = NetworkedSpeed; inputVector = NetworkedMoveInput; } if (hasSpeedParam) Anim.SetFloat(speedHash, speedValue, AnimationDamping, deltaTime); if (hasVelocityXParam) Anim.SetFloat(velocityXHash, inputVector.x * speedValue, AnimationDamping, deltaTime); if (hasVelocityZParam) Anim.SetFloat(velocityZHash, inputVector.y * speedValue, AnimationDamping, deltaTime); } public override void FixedUpdateNetwork() { if (Object == null || !Runner.IsRunning) return; if (GetInput(out PlayerInputData data)) { MoveInput = data.Direction; IsSprintHeld = data.sprint; NetworkedCameraRotation = data.rot; } else { MoveInput = Vector2.zero; IsSprintHeld = false; } // Chỉ giả lập cho máy có quyền điều khiển hoặc Server bool isSimulating = Object.HasInputAuthority || Runner.IsServer; if (!isSimulating) { UpdateAnimator(Runner.DeltaTime); return; } if (!hasControl) return; WasGrounded = IsGrounded; CheckGround(); UpdateInteractablesList(); currentState?.Tick(Runner.DeltaTime); } private void Update() { bool isOffline = Runner == null || !Runner.IsRunning; if (isOffline) { FixedUpdateNetwork(); if (!IsFinished && UI.MazeUI.Instance != null) { UI.MazeUI.Instance.UpdateTimer(Time.time - startTimeOffline); } } } private void CheckGround() { IsGrounded = Physics.CheckSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius, GroundMask); } private void UpdateInteractablesList() { interactablesNearby.Clear(); IInteractable target = Scanner.ScanForInteractable(InteractionRange, InteractionMask); if (target != null) interactablesNearby.Add(target); currentInteractableIndex = 0; } private void OnNextInteract() { if (interactablesNearby.Count <= 1) return; currentInteractableIndex = (currentInteractableIndex + 1) % interactablesNearby.Count; } private void OnPreviousInteract() { if (interactablesNearby.Count <= 1) return; currentInteractableIndex--; if (currentInteractableIndex < 0) currentInteractableIndex = interactablesNearby.Count - 1; } public IInteractable GetInteractable() { if (interactablesNearby.Count == 0) return null; return interactablesNearby[currentInteractableIndex]; } public void SetGroundCheck(float radius, Vector3 offset) { GroundCheckRadius = radius; GroundCheckOffset = offset; } public void SwitchState(PlayerBaseState newState) { currentState?.Exit(); currentState = newState; currentState?.Enter(); } public void SetControl(bool control) { hasControl = control; if (Controller != null) Controller.enabled = control; if (!control && Anim != null) Anim.SetFloat(speedHash, 0f); } private void OnDrawGizmosSelected() { Gizmos.color = new Color(0, 1, 0, 0.5f); Gizmos.DrawSphere(transform.TransformPoint(GroundCheckOffset), GroundCheckRadius); } #region Maze Winning Logic public void CompleteMaze() { if (IsFinished) return; float duration; if (Runner != null && Runner.IsRunning) { duration = ((int)Runner.Tick - StartTick) * Runner.DeltaTime; Rpc_BroadcastWin(Object.InputAuthority, duration); } else { duration = Time.time - startTimeOffline; if (UI.MazeUI.Instance != null) { UI.MazeUI.Instance.ShowWinMessage("YOU (Offline)", duration); } } IsFinished = true; FinishTime = duration; } [Rpc(RpcSources.All, RpcTargets.All)] public void Rpc_BroadcastWin(PlayerRef player, float time) { string playerName = $"Player {player.PlayerId}"; if (Runner != null && player == Runner.LocalPlayer) playerName = "YOU"; Debug.Log($"[WINNER] {playerName} reached the goal in {time:F2}s!"); if (UI.MazeUI.Instance != null) { UI.MazeUI.Instance.ShowWinMessage(playerName, time); } } public override void Render() { bool isOffline = Runner == null || !Runner.IsRunning; bool hasAuthority = Object != null && Object.HasInputAuthority; if ((isOffline || hasAuthority) && !IsFinished) { float currentTimer = isOffline ? (Time.time - startTimeOffline) : (((int)Runner.Tick - StartTick) * Runner.DeltaTime); if (UI.MazeUI.Instance != null) { UI.MazeUI.Instance.UpdateTimer(currentTimer); } } } #endregion } }