using System.Collections; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; using TMPro; public class CharacterMovement : MonoBehaviour { //jump system variables private bool jump; GroundCheck groundCheck; public FallCheck fallCheck; [SerializeField] private float jumpForce = 20f; [SerializeField] private float gravity = -9.81f; [SerializeField] private float gravityScale = 5f; private float floatSpeed; private float gvelocity; //camera direction variables private bool useCharacterForward = false; private bool lockToCameraForward = false; [SerializeField] private float turnSpeed = 10f; public GameObject player; public GameObject enemy; public float distance; private AnimatorClipInfo[] m_CurrentClipInfo; private float m_CurrentClipLength; private string m_ClipName; public float nextAnimationTime; public float animationInterval; private float turnSpeedMultiplier; private float direction = 0f; private bool isSprinting = false; public Animator anim; private Vector3 targetDirection; private Quaternion freeRotation; private Camera mainCamera; private float velocity; // Cached reference: avoids FindFirstObjectByType every FixedUpdate (was 300 calls/sec) private PlayerManagerNew _cachedPlayerManager; private bool _playerManagerCached; public bool block; Vector2 input; public PlayerScript playerScript; // New modular input handler (used if PlayerScript is not present) private CharacterInputHandler inputHandler; private AnimatorClipInfo[] playerclipinfo; private Vector3 playerDirection; private float rotationSpeed; [SerializeField] private float joystickSensitivity = 1f; private Vector2 joystickInput; float inputThreshold; // Scale protection: some animation clips contain baked scale curves from FBX import // that can shrink the character model. We save the original scale and restore it every frame. private Vector3 originalScale; [Header("Grounding")] [Tooltip("Layers to raycast against for ground detection. Must exclude character layers to prevent floating.")] [SerializeField] private LayerMask groundLayerMask = 0; // Animation transition parameters private float animationBlendTime = 0.15f; // Time to blend between animations private float movementSmoothTime = 0.1f; // Time to smooth movement speed change private float rotationSmoothTime = 0.1f; // Time to smooth rotation private float currentMoveSpeed = 0f; // Current movement speed for smooth transitions private float currentTurnSpeed = 0f; // Current turn speed for smooth transitions [SerializeField] private float smoothTurnSpeed = 5f; // Lower = smoother, for realistic turning // Walk/Run sound effects [Header("Footstep Sound Effects")] public AudioClip walkSoundEffect; public AudioClip runSoundEffect; private AudioSource footstepAudioSource; private bool isFootstepPlaying = false; private float lastSpeedParam = 0f; //Link to Mission Handler Script //Initialize the scripts MissionHandler mission_handler; GameStatsManager game_statsManager; TextMeshProUGUI txt; // Add reference to AnimationManager private AnimationManager animationManager; void OnEnable(){ GameObject gamestatsmanager_go = GameObject.Find("GameStatsManager"); if (gamestatsmanager_go != null) { game_statsManager = gamestatsmanager_go.GetComponent(); mission_handler = gamestatsmanager_go.GetComponent(); } else { DevLog.LogWarning("GameStatsManager not found - mission tracking will be disabled"); } //txt = gamestatsmanager_go.transform.Find("Canvas/move_txt").GetComponent(); if (mission_handler != null){ //DevLog.LogWarning("Character Movement Found"); } } // Start is called before the first frame update void Start() { // Save original scale before anything can modify it originalScale = transform.localScale; jumpForce = 40; gravity = -9.81f; gravityScale = 5; rotationSpeed = 2; input = Vector2.zero; anim = GetComponent(); // Set animator transition parameters if (anim != null) { foreach (var layer in new int[] { 0, 1, 2 }) // Check multiple layers if needed { if (layer < anim.layerCount) { for (int i = 0; i < anim.parameters.Length; i++) { // Adjust transition times in the Animator AnimatorControllerParameter param = anim.parameters[i]; if (param.type == AnimatorControllerParameterType.Float && param.name == "TransitionTime") { anim.SetFloat(param.name, animationBlendTime); } } } } } mainCamera = Camera.main; player = GameObject.FindGameObjectWithTag("Player"); enemy = GameObject.FindGameObjectWithTag("Enemy"); animationInterval = 3.0f; block = false; // Get PlayerScript from this character - this is essential for movement // Try new modular CharacterInputHandler first, fall back to PlayerScript inputHandler = GetComponent(); playerScript = GetComponent(); if (inputHandler == null && playerScript == null) { DevLog.LogWarning($"[CharacterMovement] No CharacterInputHandler or PlayerScript found on {gameObject.name}. Movement will not work for player-controlled characters."); } else if (inputHandler != null) { DevLog.Log($"[CharacterMovement] Using new modular CharacterInputHandler on {gameObject.name}"); } else { DevLog.Log($"[CharacterMovement] Using legacy PlayerScript on {gameObject.name}"); } // Get or add required components with null checks groundCheck = GetComponent(); if (groundCheck == null) { groundCheck = gameObject.AddComponent(); DevLog.LogWarning("GroundCheck component not found, adding one automatically."); } fallCheck = GetComponent(); if (fallCheck == null) { fallCheck = gameObject.AddComponent(); DevLog.LogWarning("FallCheck component not found, adding one automatically."); } inputThreshold = 0.3f; // Get or add AnimationManager component animationManager = GetComponent(); if (animationManager == null) { animationManager = gameObject.AddComponent(); } // Setup footstep audio source footstepAudioSource = GetComponent(); if (footstepAudioSource == null) { footstepAudioSource = gameObject.AddComponent(); } footstepAudioSource.spatialBlend = 1f; // 3D sound footstepAudioSource.loop = true; footstepAudioSource.playOnAwake = false; } void FixedUpdate() { // Cached PlayerManagerNew lookup — avoids FindFirstObjectByType every FixedUpdate if (!_playerManagerCached) { _cachedPlayerManager = GameObject.FindFirstObjectByType(); _playerManagerCached = true; } if (_cachedPlayerManager != null && _cachedPlayerManager.pause) return; // Determine if movement should be blocked based on available input source bool isBlocking = false; bool isDodging = false; bool hasInputSource = false; if (inputHandler != null) { isBlocking = inputHandler.block; isDodging = inputHandler.dodge; hasInputSource = true; } else if (playerScript != null) { isBlocking = playerScript.block; isDodging = playerScript.dodge; hasInputSource = true; } // Always update footstep sounds based on animator state (for AI and all characters) UpdateFootstepSoundsFromAnimator(); // No input source - don't process player movement // This is fine for AI characters which use NavMeshAgent if (!hasInputSource) { return; } // Block movement only during blocking or dodging if (isBlocking || isDodging) return; gvelocity += gravity * gravityScale * Time.deltaTime; if (groundCheck != null && groundCheck.isGrounded && gvelocity < 0) { //print("jump reset"); gvelocity = 0; jump = false; } // Get input from available source (prefer new modular handler) if (inputHandler != null) { input = inputHandler.input; } else if (playerScript != null) { input = playerScript.input; } inputThreshold = 0.3f; // Adjust this value as needed //ensure the character always stays upright if (transform.rotation.z > 0.5 || transform.rotation.x > 0.5 || transform.rotation.z < -0.5 || transform.rotation.x < -0.5) { print("make player upright"); Quaternion rotation = Quaternion.Euler(0, transform.rotation.y, 0); //transform.rotation = rotation; // Dampen towards the target rotation transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime); } //ensure that the character is always grounded if they are not jumping if (jump == false) { // Auto-configure ground layer mask if not set (exclude character layers) if (groundLayerMask.value == 0) { groundLayerMask = ~0; // Start with everything int playerLayer = LayerMask.NameToLayer("Player"); int enemyLayer = LayerMask.NameToLayer("Enemy"); int characterLayer = LayerMask.NameToLayer("Character"); int ignoreRaycast = LayerMask.NameToLayer("Ignore Raycast"); if (playerLayer >= 0) groundLayerMask &= ~(1 << playerLayer); if (enemyLayer >= 0) groundLayerMask &= ~(1 << enemyLayer); if (characterLayer >= 0) groundLayerMask &= ~(1 << characterLayer); if (ignoreRaycast >= 0) groundLayerMask &= ~(1 << ignoreRaycast); } RaycastHit hit; // CRITICAL: Use groundLayerMask to ignore character body colliders // Without this, raycast hits character chest/head colliders and thinks "ground" is at chest height if (Physics.Raycast(transform.position + Vector3.up * 0.5f, Vector3.down, out hit, 50f, groundLayerMask)) { float groundY = hit.point.y; if (transform.position.y > groundY + 0.15f) { // Instant snap to ground instead of slow 1f/frame drift transform.position = new Vector3(transform.position.x, groundY + 0.05f, transform.position.z); } } } // Protect against animation clips overwriting scale if (transform.localScale != originalScale) { transform.localScale = originalScale; } Move(); } public virtual void UpdateTargetDirection() { if (playerScript != null) { input = playerScript.input; } if (mainCamera == null) mainCamera = Camera.main; Vector3 forward = mainCamera.transform.forward; Vector3 right = mainCamera.transform.right; // Project onto XZ plane forward.y = 0f; right.y = 0f; forward.Normalize(); right.Normalize(); // input.x = horizontal (A/D or left/right), input.y = vertical (W/S or up/down) targetDirection = right * input.x + forward * input.y; } public void Move() { if (IsInHitReactionAnimation())// return; UpdateTargetDirection(); // Calculate raw input magnitude for animation float inputMagnitude = input.magnitude; // Define thresholds float walkThreshold = 0.1f; // Minimum input to start walking float runThreshold = 0.7f; // Input magnitude to start running float speedParam = 0f; if (inputMagnitude > walkThreshold && inputMagnitude < runThreshold) { // Walking speedParam = 0.2f; // Match your blend tree's walk value } else if (inputMagnitude >= runThreshold) { // Running speedParam = 1f; } else { // Idle speedParam = 0f; } // Debug logging for movement diagnosis if (inputMagnitude > 0.01f) { DevLog.Log($"[CharacterMovement] Input: ({input.x:F2}, {input.y:F2}), Magnitude: {inputMagnitude:F2}, Speed: {speedParam}"); } anim.SetFloat("Speed", speedParam); // --- Footstep Sound Logic --- if (speedParam == 0f) { if (footstepAudioSource.isPlaying) footstepAudioSource.Stop(); isFootstepPlaying = false; } else if (speedParam > 0f && !isFootstepPlaying) { if (speedParam < 0.7f && walkSoundEffect != null) { footstepAudioSource.clip = walkSoundEffect; footstepAudioSource.pitch = 1f; footstepAudioSource.Play(); isFootstepPlaying = true; } else if (speedParam >= 0.7f && runSoundEffect != null) { footstepAudioSource.clip = runSoundEffect; footstepAudioSource.pitch = 1.1f; footstepAudioSource.Play(); isFootstepPlaying = true; } } else if (isFootstepPlaying) { // Switch between walk/run sound if needed if (speedParam < 0.7f && footstepAudioSource.clip != walkSoundEffect && walkSoundEffect != null) { footstepAudioSource.clip = walkSoundEffect; footstepAudioSource.pitch = 1f; footstepAudioSource.Play(); } else if (speedParam >= 0.7f && footstepAudioSource.clip != runSoundEffect && runSoundEffect != null) { footstepAudioSource.clip = runSoundEffect; footstepAudioSource.pitch = 1.1f; footstepAudioSource.Play(); } } lastSpeedParam = speedParam; // --- End Footstep Sound Logic --- // Only move and rotate if there's actual input if (inputMagnitude > walkThreshold) { Vector3 moveAmount = targetDirection * 4f * Time.deltaTime; transform.Translate(moveAmount, Space.World); if (targetDirection != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up); // Use a lower turn speed for smoother, more realistic turning transform.rotation = Quaternion.Slerp( transform.rotation, targetRotation, smoothTurnSpeed * Time.deltaTime ); } } else if (inputMagnitude <= walkThreshold && speedParam == 0f) { anim.SetFloat("Speed", 0f); } } private bool IsInHitReactionAnimation() { // Use AnimationManager to check if playing hit reaction animations if (animationManager != null) { return animationManager.IsPlayingAnimation( // Active animation reactions only AnimationNames.JabReaction, AnimationNames.HaymakerReaction, AnimationNames.SpinningBackfistReaction, AnimationNames.ElbowSmashReaction, AnimationNames.ChargedPunchReaction, AnimationNames.AirPunchReaction, AnimationNames.DropKickReaction, AnimationNames.LowKickReaction /* Commented out other reactions for future use // Original hit reactions AnimationNames.RightHookHitReaction, AnimationNames.BodyKickHitReaction, AnimationNames.HookHitReaction, AnimationNames.JabHitReaction, AnimationNames.LegKickHitReaction, AnimationNames.UppercutPowerHitReaction, AnimationNames.OverhandHitReaction, // Other new hit reactions AnimationNames.ChopReaction, AnimationNames.GroundPoundReaction, AnimationNames.AirKickReaction, AnimationNames.SignSwingReaction, AnimationNames.SignOverheadSlamReaction, AnimationNames.SignChargeAttackReaction */ ); } // Fall back to original implementation if AnimationManager is not available AnimatorClipInfo[] m_CurrentClipInfo; m_CurrentClipInfo = gameObject.GetComponent().GetCurrentAnimatorClipInfo(0); if (m_CurrentClipInfo.Length == 0) { // No clips are currently playing return false; } m_ClipName = m_CurrentClipInfo[0].clip.name; // Check for active animation reactions only return m_ClipName == AnimationNames.JabReaction || m_ClipName == AnimationNames.HaymakerReaction || m_ClipName == AnimationNames.SpinningBackfistReaction || m_ClipName == AnimationNames.ElbowSmashReaction || m_ClipName == AnimationNames.ChargedPunchReaction || m_ClipName == AnimationNames.AirPunchReaction || m_ClipName == AnimationNames.DropKickReaction || m_ClipName == AnimationNames.LowKickReaction; /* Commented out other reactions for future use m_ClipName == AnimationNames.RightHookHitReaction || m_ClipName == AnimationNames.BodyKickHitReaction || m_ClipName == AnimationNames.HookHitReaction || m_ClipName == AnimationNames.JabHitReaction || m_ClipName == AnimationNames.LegKickHitReaction || m_ClipName == AnimationNames.UppercutPowerHitReaction || m_ClipName == AnimationNames.OverhandHitReaction || // Other new hit reactions m_ClipName == AnimationNames.ChopReaction || m_ClipName == AnimationNames.GroundPoundReaction || m_ClipName == AnimationNames.AirKickReaction || m_ClipName == AnimationNames.SignSwingReaction || m_ClipName == AnimationNames.SignOverheadSlamReaction || m_ClipName == AnimationNames.SignChargeAttackReaction; */ } /// /// Updates footstep sounds based on animator Speed parameter. /// Called for all characters (including AI) to ensure footstep sounds play. /// private void UpdateFootstepSoundsFromAnimator() { if (anim == null || footstepAudioSource == null) return; // Read current speed from animator float speedParam = anim.GetFloat("Speed"); // Use a slight threshold to avoid noise if (speedParam < 0.05f) { if (footstepAudioSource.isPlaying) { footstepAudioSource.Stop(); } isFootstepPlaying = false; } else if (speedParam > 0.05f && !isFootstepPlaying) { // Start playing appropriate sound if (speedParam < 0.7f && walkSoundEffect != null) { footstepAudioSource.clip = walkSoundEffect; footstepAudioSource.pitch = 1f; footstepAudioSource.Play(); isFootstepPlaying = true; } else if (speedParam >= 0.7f && runSoundEffect != null) { footstepAudioSource.clip = runSoundEffect; footstepAudioSource.pitch = 1.1f; footstepAudioSource.Play(); isFootstepPlaying = true; } } else if (isFootstepPlaying) { // Switch between walk/run sound if needed if (speedParam < 0.7f && footstepAudioSource.clip != walkSoundEffect && walkSoundEffect != null) { footstepAudioSource.clip = walkSoundEffect; footstepAudioSource.pitch = 1f; footstepAudioSource.Play(); } else if (speedParam >= 0.7f && footstepAudioSource.clip != runSoundEffect && runSoundEffect != null) { footstepAudioSource.clip = runSoundEffect; footstepAudioSource.pitch = 1.1f; footstepAudioSource.Play(); } } lastSpeedParam = speedParam; } }