chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
930
Assets/Scripts/Character/CharacterAIController.cs.bak
Normal file
930
Assets/Scripts/Character/CharacterAIController.cs.bak
Normal file
@@ -0,0 +1,930 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Unified AI Controller that handles both enemy and teammate AI.
|
||||
/// Replaces EnemyScript + EnemyNavigation with NavMeshAgent-based movement.
|
||||
/// Works with CashSystemAI for Cash System mode decisions.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Animator))]
|
||||
public class CharacterAIController : MonoBehaviour
|
||||
{
|
||||
#region Enums
|
||||
|
||||
public enum AIState
|
||||
{
|
||||
Idle,
|
||||
Chasing,
|
||||
Attacking,
|
||||
Retreating,
|
||||
Blocking,
|
||||
Dead,
|
||||
Patrolling // NEW: wander when no target in range
|
||||
}
|
||||
|
||||
public enum AIMode
|
||||
{
|
||||
Practice, // 1v1 mode - simple attack behavior
|
||||
CashSystem // Team mode - coordinate with CashSystemAI
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
[Header("AI Configuration")]
|
||||
[SerializeField] private CharacterStats characterStats;
|
||||
[SerializeField] private AIMode currentMode = AIMode.Practice;
|
||||
|
||||
[Header("Combat Permission")]
|
||||
[Tooltip("Combat permission flag — toggled by RoundsScript/PlayerManagerNew between rounds")]
|
||||
public bool attack = true;
|
||||
|
||||
[Header("Movement Settings")]
|
||||
[Tooltip("Base movement speed (replaces EnemyNavigation.speed)")]
|
||||
public float speed = 5f;
|
||||
[SerializeField] private float stoppingDistance = 1.5f;
|
||||
[SerializeField] private float rotationSpeed = 120f;
|
||||
|
||||
[Header("Combat Settings")]
|
||||
[SerializeField] private float attackRange = 2f;
|
||||
[SerializeField] private float chaseRange = 20f;
|
||||
[SerializeField] private float attackCooldown = 1.5f;
|
||||
[SerializeField] private float randomAttackVariation = 0.5f;
|
||||
|
||||
[Header("Retreat/Block Settings")]
|
||||
[SerializeField] private float retreatDistance = 4f;
|
||||
[SerializeField] private float retreatDuration = 1.5f;
|
||||
[SerializeField] private float blockDuration = 1.0f;
|
||||
[SerializeField] private float retreatChance = 0.15f;
|
||||
[SerializeField] private float blockChance = 0.20f;
|
||||
[SerializeField] private float healthRetreatThreshold = 30f;
|
||||
|
||||
[Header("Decision Loop")]
|
||||
[SerializeField] private float decisionInterval = 0.3f;
|
||||
[SerializeField] private float farDecisionInterval = 0.8f;
|
||||
[SerializeField] private float lodDistance = 25f;
|
||||
|
||||
[Header("Patrol Settings (Practice Mode)")]
|
||||
[SerializeField] private float patrolRadius = 10f;
|
||||
[SerializeField] private float patrolPauseMin = 1f;
|
||||
[SerializeField] private float patrolPauseMax = 2.5f;
|
||||
|
||||
[Header("Grounding")]
|
||||
[Tooltip("Layers to raycast against for ground detection. Must exclude character layers to prevent floating.")]
|
||||
[SerializeField] private LayerMask groundLayerMask = 0;
|
||||
|
||||
[Header("State")]
|
||||
[SerializeField] private AIState currentState = AIState.Idle;
|
||||
[SerializeField] private GameObject currentTarget;
|
||||
|
||||
// Cached components
|
||||
private Animator anim;
|
||||
private NavMeshAgent navAgent;
|
||||
private TeamMember teamMember;
|
||||
private AnimationManager animationManager;
|
||||
private CharacterAttacks characterAttacks;
|
||||
private HealthNew health;
|
||||
private CashSystemAI cashSystemAI;
|
||||
|
||||
// Cached external references
|
||||
private PlayerManagerNew cachedPlayerManager;
|
||||
private Camera cachedCamera;
|
||||
|
||||
// Internal state
|
||||
private float lastAttackTime;
|
||||
private float distanceToTarget;
|
||||
private bool isAttacking;
|
||||
private float retreatEndTime;
|
||||
private float blockEndTime;
|
||||
private Vector3 retreatDirection;
|
||||
private Coroutine decisionCoroutine;
|
||||
|
||||
// Patrol state
|
||||
private Vector3 patrolTarget;
|
||||
private bool hasPatrolTarget;
|
||||
private float patrolPauseEndTime;
|
||||
|
||||
// Gravity handling
|
||||
private float gvelocity;
|
||||
[SerializeField] private float gravity = -9.81f;
|
||||
[SerializeField] private float gravityScale = 0.75f;
|
||||
private bool jump;
|
||||
|
||||
// Scale protection: animation clips with baked scale curves from FBX import
|
||||
// can shrink the character model. Save and restore original scale each frame.
|
||||
private Vector3 originalScale;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
originalScale = transform.localScale;
|
||||
CacheComponents();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeAI();
|
||||
DetermineMode();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Reset state when AI is enabled
|
||||
currentState = AIState.Idle;
|
||||
isAttacking = false;
|
||||
|
||||
// Re-warp to NavMesh when re-enabled (e.g., after control switch)
|
||||
if (navAgent != null && navAgent.isActiveAndEnabled)
|
||||
{
|
||||
WarpToNavMesh();
|
||||
}
|
||||
|
||||
// Start the coroutine-based decision loop
|
||||
if (decisionCoroutine != null) StopCoroutine(decisionCoroutine);
|
||||
decisionCoroutine = StartCoroutine(AIDecisionLoop());
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (decisionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(decisionCoroutine);
|
||||
decisionCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FixedUpdate handles gravity and grounding every physics tick.
|
||||
/// This MUST run independently of the AI decision loop so characters
|
||||
/// stay grounded even before combat starts (attack == false).
|
||||
/// </summary>
|
||||
private void FixedUpdate()
|
||||
{
|
||||
ApplyGravity();
|
||||
EnsureUprightCharacter();
|
||||
|
||||
// Protect against animation clips overwriting scale
|
||||
if (transform.localScale != originalScale)
|
||||
{
|
||||
transform.localScale = originalScale;
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheComponents()
|
||||
{
|
||||
anim = GetComponent<Animator>();
|
||||
navAgent = GetComponent<NavMeshAgent>();
|
||||
teamMember = GetComponent<TeamMember>();
|
||||
animationManager = GetComponent<AnimationManager>();
|
||||
characterAttacks = GetComponent<CharacterAttacks>();
|
||||
health = GetComponent<HealthNew>();
|
||||
cashSystemAI = GetComponent<CashSystemAI>();
|
||||
|
||||
// Cache external singletons (lazy — may be null at Awake)
|
||||
cachedCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void InitializeAI()
|
||||
{
|
||||
// Apply stats if available
|
||||
if (characterStats != null)
|
||||
{
|
||||
attackRange = characterStats.preferredCombatRange;
|
||||
attackCooldown = 1f / characterStats.attackFrequency;
|
||||
}
|
||||
|
||||
// Apply speed to NavMeshAgent
|
||||
if (navAgent != null)
|
||||
{
|
||||
navAgent.speed = speed;
|
||||
navAgent.stoppingDistance = stoppingDistance;
|
||||
|
||||
// CRITICAL: Warp agent onto NavMesh surface to prevent floating.
|
||||
// NavMeshAgent can only manage grounding when it is ON the NavMesh.
|
||||
// If the agent was spawned slightly off the baked mesh, it floats.
|
||||
WarpToNavMesh();
|
||||
}
|
||||
|
||||
lastAttackTime = -attackCooldown; // Allow immediate first attack
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warp the NavMeshAgent to the nearest valid NavMesh point.
|
||||
/// Prevents floating caused by spawning off-NavMesh.
|
||||
/// </summary>
|
||||
private void WarpToNavMesh()
|
||||
{
|
||||
if (navAgent == null) return;
|
||||
|
||||
NavMeshHit navHit;
|
||||
if (NavMesh.SamplePosition(transform.position, out navHit, 20f, NavMesh.AllAreas))
|
||||
{
|
||||
navAgent.Warp(navHit.position);
|
||||
DevLog.Log($"[CharacterAIController] {gameObject.name}: Warped to NavMesh at Y={navHit.position.y} (was Y={transform.position.y})");
|
||||
}
|
||||
else
|
||||
{
|
||||
DevLog.LogWarning($"[CharacterAIController] {gameObject.name}: NO NavMesh found within 20m! Character will float. " +
|
||||
"Ensure NavMesh is baked for this scene.");
|
||||
}
|
||||
}
|
||||
|
||||
private void DetermineMode()
|
||||
{
|
||||
// Check if we're in Cash System mode
|
||||
if (SelectionOptions.Instance != null && SelectionOptions.Instance.isCashSystemSelected)
|
||||
{
|
||||
currentMode = AIMode.CashSystem;
|
||||
DevLog.Log($"[CharacterAIController] {gameObject.name} set to CashSystem mode");
|
||||
}
|
||||
else
|
||||
{
|
||||
currentMode = AIMode.Practice;
|
||||
DevLog.Log($"[CharacterAIController] {gameObject.name} set to Practice mode");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Coroutine Decision Loop
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine-based decision loop. Runs at variable intervals based on
|
||||
/// distance from camera (AI LOD). Much cheaper than Update() polling.
|
||||
/// </summary>
|
||||
private IEnumerator AIDecisionLoop()
|
||||
{
|
||||
yield return new WaitForSeconds(Random.Range(0.1f, 0.5f)); // Stagger AI startup
|
||||
|
||||
while (enabled)
|
||||
{
|
||||
if (!IsGamePaused() && attack)
|
||||
{
|
||||
// Choose interval based on distance from camera (AI LOD)
|
||||
float interval = GetDecisionInterval();
|
||||
|
||||
if (currentMode == AIMode.CashSystem && cashSystemAI != null)
|
||||
{
|
||||
UpdateCashSystemBehavior();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatePracticeBehavior();
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(interval);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Paused or attack disabled — stop moving, check again soon
|
||||
StopMovement();
|
||||
anim?.SetFloat("Speed", 0f);
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI LOD: slower decisions when far from camera
|
||||
/// </summary>
|
||||
private float GetDecisionInterval()
|
||||
{
|
||||
if (cachedCamera == null)
|
||||
{
|
||||
cachedCamera = Camera.main;
|
||||
if (cachedCamera == null) return decisionInterval;
|
||||
}
|
||||
|
||||
float distFromCamera = Vector3.Distance(transform.position, cachedCamera.transform.position);
|
||||
return distFromCamera > lodDistance ? farDecisionInterval : decisionInterval;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Practice Mode Behavior
|
||||
|
||||
private void UpdatePracticeBehavior()
|
||||
{
|
||||
// Find target if none
|
||||
if (currentTarget == null)
|
||||
{
|
||||
FindTarget();
|
||||
if (currentTarget == null) return;
|
||||
}
|
||||
|
||||
// Check if target is still valid
|
||||
HealthNew targetHealth = currentTarget.GetComponent<HealthNew>();
|
||||
if (targetHealth != null && targetHealth.currentHealth <= 0)
|
||||
{
|
||||
currentTarget = null;
|
||||
SetState(AIState.Idle);
|
||||
StopMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate distance
|
||||
distanceToTarget = CalculateDistanceToTarget();
|
||||
|
||||
|
||||
|
||||
// State machine
|
||||
switch (currentState)
|
||||
{
|
||||
case AIState.Idle:
|
||||
HandleIdleState();
|
||||
break;
|
||||
case AIState.Chasing:
|
||||
HandleChasingState();
|
||||
break;
|
||||
case AIState.Attacking:
|
||||
HandleAttackingState();
|
||||
break;
|
||||
case AIState.Retreating:
|
||||
HandleRetreatingState();
|
||||
break;
|
||||
case AIState.Blocking:
|
||||
HandleBlockingState();
|
||||
break;
|
||||
case AIState.Patrolling:
|
||||
HandlePatrollingState();
|
||||
break;
|
||||
}
|
||||
|
||||
// Always face target when in range
|
||||
if (distanceToTarget <= attackRange * 1.5f)
|
||||
{
|
||||
FaceTarget();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleIdleState()
|
||||
{
|
||||
if (currentTarget != null && distanceToTarget <= chaseRange)
|
||||
{
|
||||
SetState(AIState.Chasing);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No target in range — patrol instead of standing still
|
||||
SetState(AIState.Patrolling);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GTA-style patrol: pick random nearby NavMesh points and walk between them.
|
||||
/// Immediately interrupts when a target enters chase range.
|
||||
/// </summary>
|
||||
private void HandlePatrollingState()
|
||||
{
|
||||
// Priority: if a target is in chase range, switch to chasing
|
||||
if (currentTarget != null && distanceToTarget <= chaseRange)
|
||||
{
|
||||
SetState(AIState.Chasing);
|
||||
hasPatrolTarget = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find a target periodically
|
||||
if (currentTarget == null)
|
||||
{
|
||||
FindTarget();
|
||||
if (currentTarget != null && CalculateDistanceToTarget() <= chaseRange)
|
||||
{
|
||||
SetState(AIState.Chasing);
|
||||
hasPatrolTarget = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait if pausing at patrol point
|
||||
if (Time.time < patrolPauseEndTime)
|
||||
{
|
||||
StopMovement();
|
||||
anim?.SetFloat("Speed", 0f);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if reached current patrol point
|
||||
if (hasPatrolTarget)
|
||||
{
|
||||
float distToPatrol = Vector3.Distance(transform.position, patrolTarget);
|
||||
if (distToPatrol < 2f)
|
||||
{
|
||||
// Arrived — pause briefly, then pick new point
|
||||
patrolPauseEndTime = Time.time + Random.Range(patrolPauseMin, patrolPauseMax);
|
||||
hasPatrolTarget = false;
|
||||
StopMovement();
|
||||
anim?.SetFloat("Speed", 0f);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep moving toward patrol point
|
||||
MoveToTarget(patrolTarget);
|
||||
anim?.SetFloat("Speed", 1f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Pick a new random NavMesh point
|
||||
Vector3 randomDir = Random.insideUnitSphere * patrolRadius;
|
||||
randomDir.y = 0;
|
||||
Vector3 randomPoint = transform.position + randomDir;
|
||||
|
||||
NavMeshHit hit;
|
||||
if (NavMesh.SamplePosition(randomPoint, out hit, patrolRadius, NavMesh.AllAreas))
|
||||
{
|
||||
patrolTarget = hit.position;
|
||||
hasPatrolTarget = true;
|
||||
MoveToTarget(patrolTarget);
|
||||
anim?.SetFloat("Speed", 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleChasingState()
|
||||
{
|
||||
if (currentTarget == null) { SetState(AIState.Idle); return; }
|
||||
|
||||
MoveToTarget(currentTarget.transform.position);
|
||||
|
||||
if (distanceToTarget <= attackRange && CanAttack())
|
||||
{
|
||||
SetState(AIState.Attacking);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAttackingState()
|
||||
{
|
||||
// Stop moving when attacking
|
||||
StopMovement();
|
||||
|
||||
if (!isAttacking && CanAttack())
|
||||
{
|
||||
// Chance to retreat or block instead of attacking
|
||||
if (ShouldRetreat())
|
||||
{
|
||||
StartRetreat();
|
||||
return;
|
||||
}
|
||||
if (ShouldBlock())
|
||||
{
|
||||
StartBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
PerformAttack();
|
||||
}
|
||||
|
||||
// Return to chasing if out of range
|
||||
if (distanceToTarget > attackRange * 1.2f)
|
||||
{
|
||||
SetState(AIState.Chasing);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRetreatingState()
|
||||
{
|
||||
if (Time.time >= retreatEndTime)
|
||||
{
|
||||
// Retreat done — go back to chasing
|
||||
SetState(AIState.Chasing);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move away from target
|
||||
if (navAgent != null && navAgent.isActiveAndEnabled && navAgent.isOnNavMesh)
|
||||
{
|
||||
Vector3 retreatPos = transform.position + retreatDirection * retreatDistance;
|
||||
navAgent.isStopped = false;
|
||||
navAgent.SetDestination(retreatPos);
|
||||
|
||||
if (anim != null)
|
||||
{
|
||||
float spd = navAgent.velocity.magnitude / Mathf.Max(navAgent.speed, 0.01f);
|
||||
anim.SetFloat("Speed", spd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleBlockingState()
|
||||
{
|
||||
StopMovement();
|
||||
|
||||
if (Time.time >= blockEndTime)
|
||||
{
|
||||
// Block done — counterattack or chase
|
||||
if (anim != null) anim.SetBool("isBlocking", false);
|
||||
SetState(distanceToTarget <= attackRange ? AIState.Attacking : AIState.Chasing);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldRetreat()
|
||||
{
|
||||
if (health == null) return false;
|
||||
|
||||
// Higher chance to retreat when low health
|
||||
float healthPercent = (float)health.currentHealth / 100f;
|
||||
float adjustedChance = retreatChance;
|
||||
if (healthPercent * 100f < healthRetreatThreshold)
|
||||
{
|
||||
adjustedChance = retreatChance * 2.5f;
|
||||
}
|
||||
|
||||
return Random.value < adjustedChance;
|
||||
}
|
||||
|
||||
private bool ShouldBlock()
|
||||
{
|
||||
return Random.value < blockChance;
|
||||
}
|
||||
|
||||
private void StartRetreat()
|
||||
{
|
||||
SetState(AIState.Retreating);
|
||||
retreatEndTime = Time.time + retreatDuration;
|
||||
|
||||
// Calculate direction away from target
|
||||
if (currentTarget != null)
|
||||
{
|
||||
retreatDirection = (transform.position - currentTarget.transform.position).normalized;
|
||||
retreatDirection.y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
retreatDirection = -transform.forward;
|
||||
}
|
||||
|
||||
// Play retreat animation
|
||||
PlayAnimation(AnimationNames.JumpBack);
|
||||
}
|
||||
|
||||
private void StartBlock()
|
||||
{
|
||||
SetState(AIState.Blocking);
|
||||
blockEndTime = Time.time + blockDuration;
|
||||
|
||||
if (anim != null) anim.SetBool("isBlocking", true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cash System Mode Behavior
|
||||
|
||||
private void UpdateCashSystemBehavior()
|
||||
{
|
||||
// In Cash System mode, CashSystemAI tells us what to do
|
||||
// We handle animation sync and stability
|
||||
|
||||
if (cashSystemAI == null) return;
|
||||
|
||||
if (navAgent != null && navAgent.isActiveAndEnabled && navAgent.hasPath)
|
||||
{
|
||||
float spd = navAgent.velocity.magnitude / Mathf.Max(navAgent.speed, 0.01f);
|
||||
anim?.SetFloat("Speed", spd > 0.1f ? 1f : 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
anim?.SetFloat("Speed", 0f);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Combat
|
||||
|
||||
private void FindTarget()
|
||||
{
|
||||
// Use static TeamMember registry if available (zero-allocation)
|
||||
if (teamMember != null)
|
||||
{
|
||||
float nearestDistance = float.MaxValue;
|
||||
|
||||
foreach (TeamMember other in TeamMember.AllMembers)
|
||||
{
|
||||
if (other == null || other == teamMember) continue;
|
||||
if (other.GetTeam() == teamMember.GetTeam()) continue;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, other.transform.position);
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
currentTarget = other.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: find player by tag
|
||||
if (currentTarget == null)
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player != null && player != gameObject)
|
||||
{
|
||||
currentTarget = player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateDistanceToTarget()
|
||||
{
|
||||
if (currentTarget == null) return float.MaxValue;
|
||||
return Vector3.Distance(transform.position, currentTarget.transform.position);
|
||||
}
|
||||
|
||||
private bool CanAttack()
|
||||
{
|
||||
return Time.time >= lastAttackTime + attackCooldown + Random.Range(-randomAttackVariation, randomAttackVariation);
|
||||
}
|
||||
|
||||
private void PerformAttack()
|
||||
{
|
||||
if (isAttacking) return;
|
||||
|
||||
isAttacking = true;
|
||||
lastAttackTime = Time.time;
|
||||
|
||||
// Choose random attack type
|
||||
int attackType = Random.Range(0, 4);
|
||||
string animationName = "";
|
||||
|
||||
switch (attackType)
|
||||
{
|
||||
case 0:
|
||||
animationName = AnimationNames.Jab;
|
||||
break;
|
||||
case 1:
|
||||
animationName = AnimationNames.Right;
|
||||
break;
|
||||
case 2:
|
||||
animationName = AnimationNames.LowKick;
|
||||
break;
|
||||
case 3:
|
||||
animationName = AnimationNames.Haymaker;
|
||||
break;
|
||||
}
|
||||
|
||||
// Play animation
|
||||
PlayAnimation(animationName);
|
||||
|
||||
// Reset attack flag after animation
|
||||
StartCoroutine(ResetAttackFlag(0.8f));
|
||||
}
|
||||
|
||||
private IEnumerator ResetAttackFlag(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
isAttacking = false;
|
||||
}
|
||||
|
||||
private void PlayAnimation(string animationName)
|
||||
{
|
||||
if (animationManager != null)
|
||||
{
|
||||
animationManager.PlayAnimation(animationName);
|
||||
}
|
||||
else if (anim != null)
|
||||
{
|
||||
anim.Play(animationName);
|
||||
}
|
||||
}
|
||||
|
||||
private void FaceTarget()
|
||||
{
|
||||
if (currentTarget == null) return;
|
||||
|
||||
Vector3 direction = (currentTarget.transform.position - transform.position).normalized;
|
||||
direction.y = 0;
|
||||
|
||||
if (direction != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(direction);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Movement Helpers
|
||||
|
||||
private void MoveToTarget(Vector3 position)
|
||||
{
|
||||
if (navAgent == null || !navAgent.isActiveAndEnabled)
|
||||
{
|
||||
DevLog.LogWarning($"[CharacterAIController] {gameObject.name}: NavMeshAgent null or disabled, cannot move!");
|
||||
return;
|
||||
}
|
||||
|
||||
// If agent fell off NavMesh, try to re-warp it
|
||||
if (!navAgent.isOnNavMesh)
|
||||
{
|
||||
DevLog.LogWarning($"[CharacterAIController] {gameObject.name}: Agent is OFF NavMesh! Attempting re-warp...");
|
||||
WarpToNavMesh();
|
||||
|
||||
// If still not on NavMesh after warp, give up this frame
|
||||
if (!navAgent.isOnNavMesh)
|
||||
{
|
||||
Debug.LogError($"[CharacterAIController] {gameObject.name}: Re-warp FAILED. No NavMesh at position {transform.position}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
navAgent.isStopped = false;
|
||||
navAgent.SetDestination(position);
|
||||
|
||||
if (anim != null)
|
||||
{
|
||||
float spd = navAgent.velocity.magnitude / Mathf.Max(navAgent.speed, 0.01f);
|
||||
anim.SetFloat("Speed", spd);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopMovement()
|
||||
{
|
||||
if (navAgent != null && navAgent.isActiveAndEnabled && navAgent.isOnNavMesh)
|
||||
{
|
||||
navAgent.isStopped = true;
|
||||
navAgent.velocity = Vector3.zero;
|
||||
}
|
||||
|
||||
if (anim != null)
|
||||
{
|
||||
anim.SetFloat("Speed", 0f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Character Stability
|
||||
|
||||
private bool IsGamePaused()
|
||||
{
|
||||
// Lazy-cache PlayerManagerNew
|
||||
if (cachedPlayerManager == null)
|
||||
{
|
||||
cachedPlayerManager = FindFirstObjectByType<PlayerManagerNew>();
|
||||
}
|
||||
return cachedPlayerManager != null && cachedPlayerManager.pause;
|
||||
}
|
||||
|
||||
private void EnsureUprightCharacter()
|
||||
{
|
||||
if (IsCharacterTilted())
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.Euler(0, transform.rotation.eulerAngles.y, 0);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCharacterTilted()
|
||||
{
|
||||
return Mathf.Abs(transform.rotation.eulerAngles.x) > 5f ||
|
||||
Mathf.Abs(transform.rotation.eulerAngles.z) > 5f;
|
||||
}
|
||||
|
||||
private void ApplyGravity()
|
||||
{
|
||||
if (jump) return;
|
||||
|
||||
// CRITICAL FIX: When NavMeshAgent is active and ON the NavMesh,
|
||||
// it handles Y-positioning automatically. Applying manual gravity
|
||||
// CONFLICTS with NavMeshAgent and causes jittering/floating.
|
||||
// Only apply manual gravity as a FALLBACK when the agent is off-mesh.
|
||||
if (navAgent != null && navAgent.isActiveAndEnabled && navAgent.isOnNavMesh)
|
||||
{
|
||||
// NavMeshAgent handles grounding — reset velocity and skip manual gravity
|
||||
gvelocity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
gvelocity += gravity * gravityScale * Time.deltaTime;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Fallback gravity: only runs when NavMeshAgent is NOT handling grounding
|
||||
float groundY = 0f;
|
||||
RaycastHit hit;
|
||||
if (Physics.Raycast(transform.position + Vector3.up * 0.5f, Vector3.down, out hit, 50f, groundLayerMask))
|
||||
{
|
||||
groundY = hit.point.y;
|
||||
}
|
||||
|
||||
if (transform.position.y > groundY + 0.1f && !jump)
|
||||
{
|
||||
transform.position = new Vector3(
|
||||
transform.position.x,
|
||||
Mathf.Max(groundY + 0.05f, transform.position.y + gvelocity * Time.deltaTime),
|
||||
transform.position.z
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
gvelocity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Set the current target for this AI
|
||||
/// </summary>
|
||||
public void SetTarget(GameObject target)
|
||||
{
|
||||
currentTarget = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current AI state
|
||||
/// </summary>
|
||||
public AIState GetState()
|
||||
{
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force the AI into a specific state
|
||||
/// </summary>
|
||||
public void SetState(AIState state)
|
||||
{
|
||||
if (currentState != state)
|
||||
{
|
||||
currentState = state;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the AI mode
|
||||
/// </summary>
|
||||
public void SetMode(AIMode mode)
|
||||
{
|
||||
currentMode = mode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger an attack (can be called by CashSystemAI)
|
||||
/// </summary>
|
||||
public void TriggerAttack()
|
||||
{
|
||||
if (CanAttack())
|
||||
{
|
||||
PerformAttack();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update NavMeshAgent speed at runtime (replaces EnemyNavigation.speed setter)
|
||||
/// </summary>
|
||||
public void SetSpeed(float newSpeed)
|
||||
{
|
||||
speed = newSpeed;
|
||||
if (navAgent != null)
|
||||
{
|
||||
navAgent.speed = speed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current movement speed
|
||||
/// </summary>
|
||||
public float GetSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to a world position (for CashSystemAI integration)
|
||||
/// </summary>
|
||||
public void NavigateTo(Vector3 position)
|
||||
{
|
||||
MoveToTarget(position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop all movement (for CashSystemAI integration)
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
StopMovement();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user