using UnityEngine; using Fusion; namespace OnlyScove.Scripts { public class PlayerMovement : NetworkBehaviour { [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: 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; } // Networked shadow properties [Networked] private NetworkBool _isGroundedNet { get; set; } [Networked] private NetworkBool _wasGroundedNet { get; set; } [Networked] private float _velocityYNet { get; set; } [Networked] public Vector3 NetworkedPosition { get; set; } // Local backing fields for Offline mode private bool _isGroundedLocal; private bool _wasGroundedLocal; private float _velocityYLocal; // Public wrappers that handle both Online and Offline public bool IsGrounded { get => (Object != null && Object.IsValid) ? (bool)_isGroundedNet : _isGroundedLocal; set { if (Object != null && Object.IsValid) _isGroundedNet = value; _isGroundedLocal = value; } } public bool WasGrounded { get => (Object != null && Object.IsValid) ? (bool)_wasGroundedNet : _wasGroundedLocal; set { if (Object != null && Object.IsValid) _wasGroundedNet = value; _wasGroundedLocal = value; } } public float VelocityY { get => (Object != null && Object.IsValid) ? _velocityYNet : _velocityYLocal; set { if (Object != null && Object.IsValid) _velocityYNet = value; _velocityYLocal = value; } } private CharacterController controller; private float jumpCooldown = 0f; public void Initialize(CharacterController controller) { this.controller = controller; _isGroundedLocal = true; // Safe local initialization } public override void Spawned() { // Initialize networked state once spawned (only on State Authority) if (Object.HasStateAuthority) { _isGroundedNet = true; _wasGroundedNet = true; _velocityYNet = 0f; } } public void CheckGround(Transform playerTransform, float deltaTime) { if (jumpCooldown > 0) { jumpCooldown -= deltaTime; WasGrounded = IsGrounded; IsGrounded = false; return; } WasGrounded = IsGrounded; bool sphereCheck = Physics.CheckSphere(playerTransform.TransformPoint(GroundCheckOffset), GroundCheckRadius, GroundMask); bool ccCheck = (controller != null) && controller.isGrounded; IsGrounded = sphereCheck || ccCheck; if (Object != null && Object.IsValid && Object.HasStateAuthority) { // State authority updates the networked position for reconciliation NetworkedPosition = playerTransform.position; } } public void Jump() { // Physic formula: v = sqrt(h * -2 * g) VelocityY = Mathf.Sqrt(JumpHeight * -2f * Gravity); IsGrounded = false; jumpCooldown = 0.15f; // Ngăn không cho dính đất trong 0.15s đầu } public void Move(CharacterController controller, Vector3 velocity, float deltaTime) { if (controller != null && controller.enabled) { controller.Move(velocity * deltaTime); } } public void Rotate(Transform playerTransform, Vector3 moveDirection, float deltaTime) { if (moveDirection == Vector3.zero) return; Quaternion targetRot = Quaternion.LookRotation(moveDirection); playerTransform.rotation = Quaternion.RotateTowards(playerTransform.rotation, targetRot, RotationSpeed * deltaTime); } public void SetGroundCheck(float radius, Vector3 offset) { GroundCheckRadius = radius; GroundCheckOffset = offset; } } }