using System.Collections; using DG.Tweening; using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class ChasePlayerController : MonoBehaviour { [Header("Movement")] public float moveSpeed = 5f; float startingMoveSpeed; [SerializeField] private float laneDistance = 2.5f; [SerializeField] private float laneSwitchSpeed = 10f; [Header("Jump (Tap/Hold)")] [SerializeField] private float maxHoldTime = 0.25f; [SerializeField] private float initialJumpVelocity = 6.5f; [SerializeField] private float holdUpwardAcceleration = 35f; [SerializeField] private float lowJumpGravityMultiplier = 2.0f; [SerializeField] private float fallGravityMultiplier = 2.5f; [SerializeField] private float coyoteTime = 0.12f; [SerializeField] private float jumpBufferTime = 0.12f; [SerializeField] private float maxVerticalSpeed = 15f; [Header("Air Control")] [SerializeField] private bool allowAirLaneSwitch = true; [Header("Ground Check")] [SerializeField] private Transform groundProbe; [SerializeField] private LayerMask groundMask = ~0; [SerializeField] private float groundCheckRadius = 0.22f; [SerializeField] private float groundCheckExtraDown = 0.06f; [Header("Animator Driving")] [SerializeField] private bool useSpeedBlendTree = true; [SerializeField] private string speedParamName = "Speed"; [SerializeField] private float runSpeedParamValue = 2f; [SerializeField] private string runStateName = "Locomotion"; [SerializeField] private int baseLayer = 0; [Header("Fall / GameOver")] public string fallStateName = "Fall"; public string fallingStateName = "Falling"; [SerializeField] private float secondHitWindow = 10f; [SerializeField] private float stateWaitTimeout = 3f; [Header("Threat-Based Speedup")] [SerializeField] private bool speedUpFromBomb = true; [SerializeField] private Transform bombEnemy; [SerializeField] private float boostDistance = 12f; [SerializeField] private float maxSpeedBoost = 4f; [SerializeField] private float boostLerp = 5f; private float baseMoveSpeed; public static System.Action OnMoveSpeedChanged; private int currentLane = 1; private float targetLaneX = 0f; private Rigidbody rb; private Animator animator; private Collider col; private bool isGrounded = true; private bool isJumping = false; private float lastGroundedTime = -999f; private float jumpPressTime = -999f; private float jumpHoldTimer = 0f; private bool jumpHeld = false; private Vector2 startTouchPosition; private bool swipeDetected = false; private const float MIN_SWIPE = 50f; private float lastObstacleHitTime = -999f; [HideInInspector] public bool waitingForGameOver = false; [SerializeField] private bool validateStatesOnStart = true; [SerializeField] private string runTag = "Run"; [SerializeField] private string fallTag = "Fall"; [SerializeField] private string fallingTag = "Falling"; public int runShortHash, fallShortHash, fallingShortHash; private bool hasSpeedFloat, hasIsGroundedBool, hasJumpTrigger, hasLandTrigger; private float originalMoveSpeed; private bool unableToMove = false; private bool speedPaused = false; private ChaseScoreManager scoreManager; [SerializeField] private bool jumpViaUI = false; [SerializeField] private RectTransform jumpImageRect; [SerializeField] private Canvas jumpImageCanvas; private int jumpCounter = 0; public int numOfJumpsAllowed; bool cantJump = false; // NEW: hook to shared death logic public Shared_DieZibu sharedDie; private bool TouchBeganOnJumpImage(Vector2 screenPos) { if (jumpImageRect == null) return false; var cam = jumpImageCanvas ? jumpImageCanvas.worldCamera : null; return RectTransformUtility.RectangleContainsScreenPoint(jumpImageRect, screenPos, cam); } public void OnJumpPress() { if (jumpCounter >= numOfJumpsAllowed || cantJump == true) return; jumpCounter++; jumpPressTime = Time.time; TryStartJump(); jumpHeld = true; jumpHoldTimer = 0f; } public void OnJumpRelease() { jumpHeld = false; } private void Start() { rb = GetComponent(); animator = GetComponent(); col = GetComponent(); scoreManager = FindObjectOfType(); // get shared die if present if (!sharedDie) sharedDie = GetComponent(); rb.interpolation = RigidbodyInterpolation.Interpolate; rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; rb.constraints = RigidbodyConstraints.FreezeRotation; runShortHash = Animator.StringToHash(runStateName); fallShortHash = Animator.StringToHash(fallStateName); fallingShortHash = Animator.StringToHash(fallingStateName); if (bombEnemy == null) { var bomb = GameObject.FindGameObjectWithTag("BombEnemy"); if (bomb) bombEnemy = bomb.transform; } if (validateStatesOnStart) { if (!animator.HasState(baseLayer, runShortHash)) Debug.LogError($"Run state '{runStateName}' not found"); if (!animator.HasState(baseLayer, fallShortHash)) Debug.LogError($"Fall state '{fallStateName}' not found"); if (!animator.HasState(baseLayer, fallingShortHash)) Debug.LogError($"Falling state '{fallingStateName}' not found"); } hasSpeedFloat = HasParam(speedParamName, AnimatorControllerParameterType.Float); hasIsGroundedBool = HasParam("IsGrounded", AnimatorControllerParameterType.Bool); hasJumpTrigger = HasParam("Jump", AnimatorControllerParameterType.Trigger); hasLandTrigger = HasParam("Land", AnimatorControllerParameterType.Trigger); baseMoveSpeed = moveSpeed; originalMoveSpeed = moveSpeed; targetLaneX = LaneToX(currentLane); if (animator) animator.applyRootMotion = false; ForceRunStart(); startingMoveSpeed = moveSpeed; } public void SetMoveSpeed(float newSpeed) { moveSpeed = newSpeed; OnMoveSpeedChanged?.Invoke(newSpeed); } private void Update() { UpdateGrounded(); DriveRunAnimation(); if (!unableToMove) { ReadKeyboardInput(); ReadTouchInput(); HandleJumpHold(); } ApplyBetterJumpGravity(); ClampVerticalSpeed(); ApplyThreatSpeedup(); } private void ApplyThreatSpeedup() { if (!speedUpFromBomb || bombEnemy == null) return; if (waitingForGameOver || speedPaused) return; if (baseMoveSpeed <= 0.001f) return; float dist = Vector3.Distance(transform.position, bombEnemy.position); float t = Mathf.Clamp01((boostDistance - dist) / boostDistance); float target = baseMoveSpeed + maxSpeedBoost * t; moveSpeed = Mathf.Lerp(moveSpeed, target, Time.deltaTime * boostLerp); } private void FixedUpdate() { MoveForwardAndSide(); } private void ReadKeyboardInput() { if ((isGrounded || allowAirLaneSwitch) && Input.GetKeyDown(KeyCode.LeftArrow) && currentLane > 0) SetLane(currentLane - 1); if ((isGrounded || allowAirLaneSwitch) && Input.GetKeyDown(KeyCode.RightArrow) && currentLane < 2) SetLane(currentLane + 1); if (Input.GetKeyDown(KeyCode.Space)) { jumpPressTime = Time.time; TryStartJump(); jumpHeld = true; jumpHoldTimer = 0f; } if (Input.GetKeyUp(KeyCode.Space)) { jumpHeld = false; } } private void ReadTouchInput() { if (Input.touchCount != 1) return; var t = Input.GetTouch(0); if (jumpViaUI) { switch (t.phase) { case TouchPhase.Began: startTouchPosition = t.position; swipeDetected = !TouchBeganOnJumpImage(t.position); break; case TouchPhase.Ended: if (!swipeDetected) break; Vector2 swipe = t.position - startTouchPosition; if (swipe.magnitude >= MIN_SWIPE && Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y)) { if ((isGrounded || allowAirLaneSwitch) && swipe.x > 0 && currentLane < 2) SetLane(currentLane + 1); else if ((isGrounded || allowAirLaneSwitch) && swipe.x < 0 && currentLane > 0) SetLane(currentLane - 1); } swipeDetected = false; break; } return; } switch (t.phase) { case TouchPhase.Began: startTouchPosition = t.position; swipeDetected = true; jumpHeld = true; jumpHoldTimer = 0f; jumpPressTime = Time.time; break; case TouchPhase.Ended: if (!swipeDetected) { jumpHeld = false; break; } Vector2 swipe = t.position - startTouchPosition; if (swipe.magnitude >= MIN_SWIPE) { if (Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y)) { if ((isGrounded || allowAirLaneSwitch) && swipe.x > 0 && currentLane < 2) SetLane(currentLane + 1); else if ((isGrounded || allowAirLaneSwitch) && swipe.x < 0 && currentLane > 0) SetLane(currentLane - 1); } else if (swipe.y > 0) TryStartJump(); } else { TryStartJump(); } swipeDetected = false; jumpHeld = false; break; } } private float LaneToX(int laneIndex) => (laneIndex - 1) * laneDistance; private void SetLane(int laneIndex) { currentLane = Mathf.Clamp(laneIndex, 0, 2); targetLaneX = LaneToX(currentLane); } private void MoveForwardAndSide() { if (unableToMove) return; Vector3 forwardStep = speedPaused ? Vector3.zero : Vector3.back * moveSpeed * Time.fixedDeltaTime; float newX = Mathf.MoveTowards(rb.position.x, targetLaneX, laneSwitchSpeed * Time.fixedDeltaTime); Vector3 targetPos = new Vector3(newX, rb.position.y, rb.position.z) + forwardStep; rb.MovePosition(targetPos); } private void TryStartJump() { bool canCoyote = (Time.time - lastGroundedTime) <= coyoteTime; if ((isGrounded || canCoyote) && !waitingForGameOver) { var v = rb.velocity; v.y = initialJumpVelocity; rb.velocity = v; isJumping = true; isGrounded = false; jumpPressTime = -999f; if (hasJumpTrigger) animator.SetTrigger("Jump"); if (hasIsGroundedBool) animator.SetBool("IsGrounded", false); } } private void HandleJumpHold() { if (!isJumping || !jumpHeld) return; if (jumpHoldTimer < maxHoldTime && rb.velocity.y > 0f) { rb.AddForce(Vector3.up * holdUpwardAcceleration, ForceMode.Acceleration); jumpHoldTimer += Time.deltaTime; } } private void ApplyBetterJumpGravity() { if (rb == null) return; if (rb.velocity.y < 0f) { rb.AddForce(Physics.gravity * (fallGravityMultiplier - 1f), ForceMode.Acceleration); } else if (!jumpHeld && rb.velocity.y > 0f) { rb.AddForce(Physics.gravity * (lowJumpGravityMultiplier - 1f), ForceMode.Acceleration); } } private void ClampVerticalSpeed() { var v = rb.velocity; if (v.y > maxVerticalSpeed) v.y = maxVerticalSpeed; if (v.y < -maxVerticalSpeed) v.y = -maxVerticalSpeed; rb.velocity = v; } private void UpdateGrounded() { bool groundedNow = GroundProbe(out _); if (groundedNow) { if (!isGrounded) { if (hasLandTrigger) animator.SetTrigger("Land"); if (hasIsGroundedBool) animator.SetBool("IsGrounded", true); isJumping = false; ForceRunStart(); } isGrounded = true; lastGroundedTime = Time.time; if ((Time.time - jumpPressTime) <= jumpBufferTime) { jumpPressTime = -999f; TryStartJump(); } } else { isGrounded = false; if (hasIsGroundedBool) animator.SetBool("IsGrounded", false); } } private bool GroundProbe(out Vector3 hitPoint) { hitPoint = transform.position; if (col != null) { Bounds b = col.bounds; Vector3 feet = new Vector3(b.center.x, b.min.y + 0.02f, b.center.z); Vector3 origin = groundProbe ? groundProbe.position : feet; if (Physics.SphereCast(origin, groundCheckRadius, Vector3.down, out RaycastHit hit, groundCheckExtraDown + 0.02f, groundMask, QueryTriggerInteraction.Ignore)) { hitPoint = hit.point; return true; } if (Physics.CheckSphere(origin + Vector3.down * groundCheckExtraDown, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore)) { hitPoint = origin + Vector3.down * groundCheckExtraDown; return true; } return false; } Vector3 o = groundProbe ? groundProbe.position : (transform.position + Vector3.down * 0.95f); bool ok = Physics.CheckSphere(o, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore); if (ok) hitPoint = o; return ok; } private void OnCollisionStay(Collision collision) { if (((1 << collision.gameObject.layer) & groundMask) != 0 && Mathf.Abs(rb.velocity.y) < 0.1f) { isGrounded = true; lastGroundedTime = Time.time; } } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("Ground")) { isGrounded = true; lastGroundedTime = Time.time; jumpCounter = 0; isJumping = false; if (hasIsGroundedBool) animator.SetBool("IsGrounded", true); ForceRunStart(); } } private void ForceRunStart(bool ignoreGuards = false) { if (!ignoreGuards && (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || waitingForGameOver)) return; moveSpeed = startingMoveSpeed; if (useSpeedBlendTree && hasSpeedFloat) animator.SetFloat(speedParamName, runSpeedParamValue); else if (!string.IsNullOrEmpty(runStateName)) animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } private void DriveRunAnimation() { if (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || isJumping) return; if (useSpeedBlendTree && hasSpeedFloat) { animator.SetFloat(speedParamName, runSpeedParamValue, 0.1f, Time.deltaTime); } else { var st = animator.GetCurrentAnimatorStateInfo(baseLayer); var nxt = animator.GetNextAnimatorStateInfo(baseLayer); bool inRunNow = st.shortNameHash == runShortHash || st.IsTag(runTag); bool goingToRun = animator.IsInTransition(baseLayer) && (nxt.shortNameHash == runShortHash || nxt.IsTag(runTag)); if (!isJumping && !inRunNow && !goingToRun) animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } } private bool HasParam(string name, AnimatorControllerParameterType type) { foreach (var p in animator.parameters) if (p.type == type && p.name == name) return true; return false; } private bool IsInOrGoingTo(int shortHash) { var st = animator.GetCurrentAnimatorStateInfo(baseLayer); if (st.shortNameHash == shortHash) return true; if (animator.IsInTransition(baseLayer)) { var nxt = animator.GetNextAnimatorStateInfo(baseLayer); if (nxt.shortNameHash == shortHash) return true; } return false; } // ======= Squish Kill (unchanged) ======= public void KillBySquish(Transform squashTarget = null, float squashTime = 0.25f, float postDelay = 0.35f) { waitingForGameOver = true; unableToMove = true; speedPaused = true; if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; rb.isKinematic = true; } SetMoveSpeed(0); var allCols = GetComponentsInChildren(); for (int i = 0; i < allCols.Length; i++) allCols[i].enabled = false; Transform t = squashTarget ? squashTarget : transform; Vector3 start = t.localScale; float widen = 1.1f; Sequence seq = DOTween.Sequence(); seq.Append(t.DOScaleY(0f, squashTime).SetEase(Ease.InCubic)) .Join(t.DOScaleX(start.x * widen, squashTime)) .Join(t.DOScaleZ(start.z * widen, squashTime)) .AppendInterval(postDelay) .OnComplete(() => { if (scoreManager) scoreManager.GameOver(); }); } public void OnObstacleHit() { if (waitingForGameOver) return; Camera.main.DOShakePosition(0.25f, new Vector3(0.15f, 0.15f, 0.15f), 20); if (Time.time - lastObstacleHitTime <= secondHitWindow) { waitingForGameOver = true; speedPaused = true; moveSpeed = 0; if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } StartCoroutine(PlayStateAndGameOver(fallingStateName, fallingShortHash)); } else { lastObstacleHitTime = Time.time; speedPaused = true; if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } PlayStateOnce(fallStateName); originalMoveSpeed = baseMoveSpeed; SetMoveSpeed(0); cantJump = true; StartCoroutine(ResumeRunAfter(fallStateName, fallShortHash)); } } private void PlayStateOnce(string stateName, float xfade = 0.08f) { if (!string.IsNullOrEmpty(stateName)) animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f); } private IEnumerator ResumeRunAfter(string stateName, int shortHash, float xfade = 0.1f) { yield return new WaitForSeconds(1.5f); float t0 = Time.time; while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null; while (animator.IsInTransition(baseLayer)) yield return null; t0 = Time.time; while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null; if (!waitingForGameOver) { speedPaused = false; ForceRunStart(ignoreGuards: true); baseMoveSpeed = originalMoveSpeed; SetMoveSpeed(originalMoveSpeed); cantJump = false; animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } } public IEnumerator PlayStateAndGameOver(string stateName, int shortHash, float xfade = 0.08f) { unableToMove = true; SetMoveSpeed(0); if (string.IsNullOrEmpty(stateName) || animator == null) { if (scoreManager) scoreManager.GameOver(); yield break; } animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f); float t0 = Time.time; while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null; while (animator.IsInTransition(baseLayer)) yield return null; t0 = Time.time; while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null; if (scoreManager) scoreManager.GameOver(); } private bool StateFinished(int shortHash) { var st = animator.GetCurrentAnimatorStateInfo(baseLayer); return st.shortNameHash == shortHash && !animator.IsInTransition(baseLayer) && st.normalizedTime >= 1f; } private bool TimedOut(float startTime) => stateWaitTimeout > 0f && (Time.time - startTime) > stateWaitTimeout; // Optional helper if you want to trigger shared normal death from here: public void DieNormal() { if (sharedDie == null) return; waitingForGameOver = true; moveSpeed = 0f; sharedDie.TriggerDeath(DeathType.Normal); } }