using System.Collections.Generic; using UnityEngine; using Fusion; namespace OnlyScove.Scripts { [RequireComponent(typeof(CharacterController), typeof(InputReader), typeof(Animator))] [RequireComponent(typeof(PlayerStats), typeof(PlayerInteraction), typeof(PlayerMovement))] [RequireComponent(typeof(PlayerAnimationHandler))] 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; } [Header("Modules")] public PlayerStats Stats; public PlayerInteraction Interaction; public PlayerMovement Movement; public PlayerAnimationHandler AnimationHandler; [Networked] public Quaternion NetworkedCameraRotation { get; set; } [Networked] public Vector2 NetworkedMoveInput { get; set; } [Networked] public float NetworkedSpeed { get; set; } // Pass-through properties for State Compatibility public Vector2 MoveInput { get; private set; } public bool IsSprintHeld { get; private set; } public float VelocityY { get => (Movement != null) ? Movement.VelocityY : 0f; set { if (Movement != null) Movement.VelocityY = value; } } public bool IsGrounded => (Movement != null) ? Movement.IsGrounded : true; public bool WasGrounded => (Movement != null) ? Movement.WasGrounded : true; public float WalkSpeed => Movement.WalkSpeed; public float RunSpeed => Movement.RunSpeed; public float SprintSpeed => Movement.SprintSpeed; public float SneakSpeed => Movement.SneakSpeed; public float DashForce => Movement.DashForce; public float JumpHeight => Movement.JumpHeight; public float ThrustDownwardForce => Movement.ThrustDownwardForce; public float Gravity => Movement.Gravity; public float InteractionRange => Interaction.InteractionRange; public LayerMask InteractionMask => Interaction.InteractionMask; public static PlayerStateMachine Local { get; private set; } public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None"; public Quaternion CameraRotation { get { if (Runner != null && Runner.IsRunning && Object != null && Object.IsValid) return NetworkedCameraRotation; return Cam != null ? Cam.PlanarRotation : transform.rotation; } } private PlayerBaseState currentState; private bool hasControl = true; private float localAnimatorSpeed; protected virtual void Awake() { Controller = GetComponent(); Input = GetComponent(); Anim = GetComponentInChildren(); Scanner = GetComponent(); Stats = GetComponent(); Interaction = GetComponent(); Movement = GetComponent(); AnimationHandler = GetComponent(); AnimationHandler.Initialize(Anim); Movement.Initialize(Controller); Interaction.Initialize(Scanner); } private void Start() { if (Runner == null || !Runner.IsRunning) InitializePlayer(); } public override void Spawned() { InitializePlayer(); if (Object != null && !Object.HasInputAuthority && Runner.IsClient) { if (Controller != null) Controller.enabled = false; } } private void InitializePlayer() { if (currentState == null) SwitchState(new PlayerIdleState(this)); bool isOffline = Runner == null || !Runner.IsRunning; if (isOffline || (Object != null && Object.HasInputAuthority)) { Local = this; CameraController cameraController = GameObject.FindAnyObjectByType(); if (cameraController != null) { Cam = cameraController; Cam.followTarget = transform; Cam.inputReader = Input; } if (Input != null) { Input.OnNextInteractEvent -= Interaction.NextInteract; Input.OnNextInteractEvent += Interaction.NextInteract; Input.OnPreviousInteractEvent -= Interaction.PreviousInteract; Input.OnPreviousInteractEvent += Interaction.PreviousInteract; } if (Controller != null) Controller.enabled = true; } } private void OnDestroy() { if (Input != null && Interaction != null) { Input.OnNextInteractEvent -= Interaction.NextInteract; Input.OnPreviousInteractEvent -= Interaction.PreviousInteract; } } public void Rotate(Vector3 moveDirection, float deltaTime) { Movement.Rotate(transform, moveDirection, deltaTime); } public void Move(Vector3 velocity, float animatorSpeed, float deltaTime) { bool canMove = (Runner == null || !Runner.IsRunning) || (Object != null && Object.IsValid && (Object.HasInputAuthority || Runner.IsServer)); if (!canMove) return; Movement.Move(Controller, velocity, deltaTime); localAnimatorSpeed = animatorSpeed; if (Object != null && Object.IsValid && Object.HasStateAuthority) { NetworkedSpeed = animatorSpeed; NetworkedMoveInput = MoveInput; } UpdateAnimator(deltaTime); } private void UpdateAnimator(float deltaTime) { bool isNetworked = Runner != null && Runner.IsRunning && Object != null && Object.IsValid; float speedValue = (!isNetworked || Object.HasInputAuthority) ? localAnimatorSpeed : NetworkedSpeed; Vector2 inputVector = (!isNetworked || Object.HasInputAuthority) ? MoveInput : NetworkedMoveInput; // Pass IsGrounded to handle air/ground transitions AnimationHandler.UpdateAnimator(speedValue, inputVector, IsGrounded, deltaTime); } public override void FixedUpdateNetwork() { bool isRunning = Runner != null && Runner.IsRunning; if (isRunning && (Object == null || !Object.IsValid)) return; float deltaTime = isRunning ? Runner.DeltaTime : Time.fixedDeltaTime; if (GetInput(out PlayerInputData data)) { MoveInput = data.Direction; IsSprintHeld = (bool)data.sprint; if (isRunning) NetworkedCameraRotation = data.rot; if (data.jump) TriggerJump(); } else if (!isRunning) { MoveInput = new Vector2(UnityEngine.Input.GetAxisRaw("Horizontal"), UnityEngine.Input.GetAxisRaw("Vertical")); IsSprintHeld = UnityEngine.Input.GetKey(KeyCode.LeftShift); if (Input.ConsumeJumpInput()) TriggerJump(); } if (!isRunning || (Object != null && Object.IsValid && (Object.HasInputAuthority || Runner.IsServer))) { if (hasControl) { Movement.CheckGround(transform, deltaTime); Interaction.UpdateInteractables(); currentState?.Tick(deltaTime); } } } public void TriggerJump() { if (!IsGrounded) return; if (Scanner != null) { var hitData = Scanner.ObstacleCheck(); if (hitData.forwardHitFound && hitData.heightHitFound) { SwitchState(new PlayerParkourState(this)); return; } } float jumpMoveSpeed = (IsSprintHeld) ? Movement.SprintSpeed : Movement.WalkSpeed; SwitchState(new PlayerJumpState(this, jumpMoveSpeed)); } public override void Render() { bool isRunning = Runner != null && Runner.IsRunning; if (isRunning && Object != null && Object.IsValid) { if (!Object.HasInputAuthority) { // Proxies if (Movement.NetworkedPosition != Vector3.zero) { transform.position = Vector3.Lerp(transform.position, Movement.NetworkedPosition, Runner.DeltaTime * 20f); } } UpdateAnimator(Runner.DeltaTime); } else if (!isRunning) { UpdateAnimator(Time.deltaTime); } } private void Update() { if (Runner == null || !Runner.IsRunning) FixedUpdateNetwork(); } public IInteractable GetInteractable() => Interaction.GetInteractable(); public void SetGroundCheck(float radius, Vector3 offset) => Movement.SetGroundCheck(radius, 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) AnimationHandler.SetSpeed(0f); } private void OnDrawGizmosSelected() { if (Movement == null) return; Gizmos.color = new Color(0, 1, 0, 0.5f); Gizmos.DrawSphere(transform.TransformPoint(Movement.GroundCheckOffset), Movement.GroundCheckRadius); } } }