MiniGames/Assets/Scripts/ChaseOn/ChasePlayerController.cs
2025-08-18 17:52:40 +05:00

377 lines
12 KiB
C#

using System.Collections;
using DG.Tweening;
using UnityEngine;
public class ChasePlayerController : MonoBehaviour
{
[Header("Movement")]
public float moveSpeed = 5f;
public float laneDistance = 2.5f; // 0=Left,1=Mid,2=Right
public float jumpPower = 0.6f;
public float jumpDuration = 1.2f;
public float laneSwitchSpeed = 10f;
[Header("Animator Driving")]
[Tooltip("If true, sets a float parameter (e.g., 'Speed') each frame. If false, crossfades to a run state by name.")]
public bool useSpeedBlendTree = true;
[Tooltip("Animator float parameter name for the locomotion blend tree.")]
public string speedParamName = "Speed";
[Tooltip("Value to push while running (match your thresholds: 1=Run, 2=RunFast, 3=Sprint).")]
public float runSpeedParamValue = 2f;
[Tooltip("Fallback run/locomotion state name if not using a Speed parameter.")]
public string runStateName = "Locomotion";
[Tooltip("Base layer index for the states above.")]
public int baseLayer = 0;
[Header("Fall / GameOver Logic")]
public string fallStateName = "Fall";
public string fallingStateName = "Falling";
public float secondHitWindow = 10f;
public float stateWaitTimeout = 3f;
public static System.Action<float> OnMoveSpeedChanged;
// -------------------------------------------------
int currentLane = 1;
Rigidbody rb;
Animator animator;
bool isGrounded = true;
bool isJumping = false;
Vector3 targetLanePosition;
// touch swipe
Vector2 startTouchPosition;
bool swipeDetected = false;
float minSwipeDistance = 50f;
// fall tracking
float lastObstacleHitTime = -999f;
bool waitingForGameOver = false;
// state hashes
int runShortHash, fallShortHash, fallingShortHash;
[SerializeField] bool validateStatesOnStart = true;
[SerializeField] string runTag = "Run";
[SerializeField] string fallTag = "Fall";
[SerializeField] string fallingTag = "Falling";
// NEW: remember original forward speed so we can restore after stumble
float originalMoveSpeed;
ChaseScoreManager scoreManager;
void Start()
{
rb = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
scoreManager = FindObjectOfType<ChaseScoreManager>();
targetLanePosition = transform.position;
// cache hashes
runShortHash = Animator.StringToHash(runStateName);
fallShortHash = Animator.StringToHash(fallStateName);
fallingShortHash = Animator.StringToHash(fallingStateName);
if (validateStatesOnStart)
{
if (!animator.HasState(baseLayer, runShortHash))
Debug.LogError($"[ChasePlayerController] Run state '{runStateName}' not found on layer {baseLayer}");
if (!animator.HasState(baseLayer, fallShortHash))
Debug.LogError($"[ChasePlayerController] Fall state '{fallStateName}' not found on layer {baseLayer}");
if (!animator.HasState(baseLayer, fallingShortHash))
Debug.LogError($"[ChasePlayerController] Falling state '{fallingStateName}' not found on layer {baseLayer}");
}
originalMoveSpeed = moveSpeed; // NEW
ForceRunStart(); // autorunner
}
public void SetMoveSpeed(float newSpeed)
{
moveSpeed = newSpeed;
OnMoveSpeedChanged?.Invoke(newSpeed);
}
bool unableToMove = false;
void Update()
{
DriveRunAnimation();
if(!unableToMove)
{
HandleInput();
HandleSwipe();
}
UpdateLaneTarget();
}
void FixedUpdate()
{
MoveForward();
SmoothLaneSwitch();
}
// ----------------- Input -----------------
void HandleInput()
{
if (isJumping) return;
if (Input.GetKeyDown(KeyCode.LeftArrow) && currentLane > 0) currentLane--;
if (Input.GetKeyDown(KeyCode.RightArrow) && currentLane < 2) currentLane++;
if (Input.GetKeyDown(KeyCode.Space) && isGrounded) Jump();
}
void HandleSwipe()
{
if (Input.touchCount != 1) return;
var touch = Input.GetTouch(0);
switch (touch.phase)
{
case TouchPhase.Began:
startTouchPosition = touch.position;
swipeDetected = true;
break;
case TouchPhase.Ended:
if (!swipeDetected) return;
Vector2 swipe = touch.position - startTouchPosition;
if (swipe.magnitude >= minSwipeDistance)
{
if (isJumping) { swipeDetected = false; return; }
if (Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y))
{
if (swipe.x > 0 && currentLane < 2) currentLane++;
else if (swipe.x < 0 && currentLane > 0) currentLane--;
}
else
{
if (swipe.y > 0 && isGrounded) Jump();
}
}
swipeDetected = false;
break;
}
}
// ----------------- Movement -----------------
void UpdateLaneTarget()
{
if (isJumping) return;
float targetX = (currentLane - 1) * laneDistance;
targetLanePosition = new Vector3(targetX, rb.position.y, rb.position.z);
}
void SmoothLaneSwitch()
{
if (isJumping) return;
Vector3 newPos = Vector3.Lerp(rb.position, targetLanePosition, laneSwitchSpeed * Time.fixedDeltaTime);
rb.MovePosition(newPos);
}
void MoveForward()
{
rb.MovePosition(rb.position + Vector3.back * moveSpeed * Time.fixedDeltaTime);
}
void Jump()
{
if (!isGrounded) return;
isGrounded = false;
isJumping = true;
rb.velocity = Vector3.zero;
rb.useGravity = false;
float forwardDisplacement = moveSpeed * jumpDuration;
Vector3 jumpTarget = rb.position + Vector3.back * forwardDisplacement;
rb.DOJump(jumpTarget, jumpPower, 1, jumpDuration)
.SetEase(Ease.Linear).SetDelay(0.2f)
.OnStart(() =>
{
SafeSetTrigger("Jump");
SafeSetBool("IsGrounded", false);
})
.OnComplete(() =>
{
rb.useGravity = true;
isGrounded = true;
isJumping = false;
SafeSetTrigger("Land"); // if present
SafeSetBool("IsGrounded", true);
ForceRunStart();
});
}
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isGrounded = true;
SafeSetBool("IsGrounded", true);
ForceRunStart();
}
}
// ----------------- Animator driving -----------------
// CHANGED: can bypass guards when 'ignoreGuards' is true
void ForceRunStart(bool ignoreGuards = false)
{
if (!ignoreGuards)
{
if (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || waitingForGameOver)
return;
}
if (useSpeedBlendTree && HasParameter(speedParamName, AnimatorControllerParameterType.Float))
{
animator.SetFloat(speedParamName, runSpeedParamValue);
}
else if (!string.IsNullOrEmpty(runStateName))
{
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
}
}
void DriveRunAnimation()
{
if (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || isJumping) return;
if (useSpeedBlendTree && HasParameter(speedParamName, AnimatorControllerParameterType.Float))
{
animator.SetFloat(speedParamName, runSpeedParamValue, 0.1f, Time.deltaTime);
return;
}
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);
}
}
bool HasParameter(string name, AnimatorControllerParameterType type)
{
foreach (var p in animator.parameters)
if (p.type == type && p.name == name) return true;
return false;
}
void SafeSetTrigger(string trig)
{
if (HasParameter(trig, AnimatorControllerParameterType.Trigger))
animator.SetTrigger(trig);
}
void SafeSetBool(string param, bool v)
{
if (HasParameter(param, AnimatorControllerParameterType.Bool))
animator.SetBool(param, v);
}
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;
}
// ----------------- Obstacle/Fall flow -----------------
public void OnObstacleHit()
{
if (waitingForGameOver) return;
// Second hit within window → Falling then GameOver
if (Time.time - lastObstacleHitTime <= secondHitWindow)
{
waitingForGameOver = true;
//SetMoveSpeed(0);
moveSpeed = 0;
StartCoroutine(PlayStateAndGameOver(fallingStateName, fallingShortHash));
}
else
{
// First hit → Fall only (no GameOver), then resume run when Fall finishes
lastObstacleHitTime = Time.time;
PlayStateOnce(fallStateName);
originalMoveSpeed = moveSpeed; // remember whatever it was right now
//SetMoveSpeed(0f); // optional: pause forward motion during stumble
moveSpeed = 0;
StartCoroutine(ResumeRunAfter(fallStateName, fallShortHash));
}
}
void PlayStateOnce(string stateName, float xfade = 0.08f)
{
if (string.IsNullOrEmpty(stateName) || animator == null) return;
animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f);
}
IEnumerator ResumeRunAfter(string stateName, int shortHash, float xfade = 0.1f)
{
float t0 = Time.time;
while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null;
while (animator.IsInTransition(baseLayer)) yield return null;
// Wait for completion OR force resume after timeout
t0 = Time.time;
while (!StateFinished(shortHash) && !TimedOut(t0))
yield return null;
if (!waitingForGameOver)
{
ForceRunStart(ignoreGuards: true);
SetMoveSpeed(originalMoveSpeed);
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
}
}
IEnumerator PlayStateAndGameOver(string stateName, int shortHash, float xfade = 0.08f)
{
unableToMove = true;
if (string.IsNullOrEmpty(stateName) || animator == null)
{
if (scoreManager) scoreManager.GameOver();
yield break;
}
animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f);
// Wait to enter target state
float t0 = Time.time;
while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null;
while (animator.IsInTransition(baseLayer)) yield return null;
// Wait for it to finish (non-looping recommended)
t0 = Time.time;
while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null;
if (scoreManager) scoreManager.GameOver();
}
bool StateFinished(int shortHash)
{
var st = animator.GetCurrentAnimatorStateInfo(baseLayer);
return st.shortNameHash == shortHash && !animator.IsInTransition(baseLayer) && st.normalizedTime >= 1f;
}
bool TimedOut(float startTime)
{
return stateWaitTimeout > 0f && (Time.time - startTime) > stateWaitTimeout;
}
}