diff --git a/Assets/Scripts/AI NPC/AnimatorAI.cs b/Assets/Scripts/AI NPC/AnimatorAI.cs index 12657a1c..c7dc4663 100644 --- a/Assets/Scripts/AI NPC/AnimatorAI.cs +++ b/Assets/Scripts/AI NPC/AnimatorAI.cs @@ -1,307 +1,199 @@ using UnityEngine; using UnityEngine.AI; using Invector; +using Invector.vEventSystems; using System.Collections; /// -/// AnimatorAI: Đồng bộ hóa trạng thái của EnemyAI với Animator. -/// Tích hợp Simulation Mode để giả lập animation khi chưa có logic di chuyển hoàn chỉnh. +/// AnimatorAI: Advanced Invector-style Animator synchronization for EnemyAI. +/// Resolves T-pose by ensuring all core Invector parameters are correctly synced. /// -public class AnimatorAI : MonoBehaviour +public class AnimatorAI : MonoBehaviour, vIAnimatorStateInfoController { protected Animator animator; protected EnemyAI enemyAI; protected NavMeshAgent agent; protected Rigidbody rb; - - [Header("Debug Settings")] - public bool debugMode = true; - public Color debugColor = Color.cyan; - - [Header("Simulation Mode (Giả lập)")] - public bool useSimulation = false; // Tích chọn để dùng thông số giả lập bên dưới - public bool autoCycleSpeed = false; // Tự động chạy/đi bộ/đứng im theo vòng lặp - [Range(0, 1)] public float simVerticalVelocity = 0f; - public bool simIsSprinting = false; - public bool simIsAiming = false; - public int simMoveSetID = 0; + protected vHealthController healthController; [Header("Movement Settings")] public float sprintThreshold = 0.8f; public float dampTime = 0.1f; + public float groundDistanceValue = 0.05f; + + public vAnimatorStateInfos animatorStateInfos { get; protected set; } #region Animator Parameters (Invector Style) protected vAnimatorParameter isDead; protected vAnimatorParameter isGrounded; - protected vAnimatorParameter isCrouching; protected vAnimatorParameter isStrafing; - protected vAnimatorParameter isSliding; protected vAnimatorParameter isSprinting; protected vAnimatorParameter isAiming; - protected vAnimatorParameter canAim; - protected vAnimatorParameter flipAnimation; - protected vAnimatorParameter flipEquip; - protected vAnimatorParameter groundDistance; - protected vAnimatorParameter groundAngle; protected vAnimatorParameter verticalVelocity; + protected vAnimatorParameter horizontalVelocity; + protected vAnimatorParameter groundDistance; protected vAnimatorParameter moveSet_ID; - protected vAnimatorParameter upperBody_ID; - protected vAnimatorParameter idleRandom; - protected vAnimatorParameter idleRandomTrigger; - protected vAnimatorParameter randomAttack; - protected vAnimatorParameter weakAttack; - protected vAnimatorParameter strongAttack; - protected vAnimatorParameter isBlocking; protected vAnimatorParameter attackID; - protected vAnimatorParameter defenseID; - protected vAnimatorParameter recoilID; - protected vAnimatorParameter reactionID; - protected vAnimatorParameter triggerRecoil; - protected vAnimatorParameter triggerReaction; protected vAnimatorParameter hitDirection; + protected vAnimatorParameter reactionID; + protected vAnimatorParameter triggerReaction; protected vAnimatorParameter resetState; - protected vAnimatorParameter reload; - protected vAnimatorParameter cancelReload; - protected vAnimatorParameter reloadID; - protected vAnimatorParameter shoot; - protected vAnimatorParameter shot_ID; - protected vAnimatorParameter powerCharger; #endregion - [Header("Nuclear Diagnostic (Siêu chẩn đoán)")] - public string forcePlayState = ""; // Gõ tên State vào đây để ép nó chạy (vd: Idle) - public bool showLayerWeights = false; + protected Vector3 lastPosition; + protected float currentV; + protected float currentH; - protected virtual void Start() + protected virtual void Awake() { animator = GetComponentInChildren(); enemyAI = GetComponent(); agent = GetComponent(); rb = GetComponent(); + healthController = GetComponent(); - if (animator == null) + if (animator) { - Debug.LogError($"[AnimatorAI] KHÔNG tìm thấy Animator trên {gameObject.name} hoặc các con của nó!"); - return; + animatorStateInfos = new vAnimatorStateInfos(animator); + InitializeParameters(); } - // Chạy chẩn đoán hạt nhân - StartCoroutine(NuclearDiagnosticRoutine()); - - InitializeParameters(); + lastPosition = transform.position; } - private IEnumerator NuclearDiagnosticRoutine() + protected virtual void OnEnable() { - while (true) - { - yield return new WaitForSeconds(1f); + this.Register(); + if (healthController) healthController.onReceiveDamage.AddListener(OnReceiveDamage); + } - if (animator == null) yield break; - - // 1. Kiểm tra Enabled - if (!animator.enabled) - Debug.LogError($"[NUCLEAR ALERT] Component Animator đang bị TẮT (enabled = false)!"); - - // 2. Kiểm tra Rig Type - if (animator.isHuman) - Debug.Log($"[Rig Info] Rig là Humanoid. Hãy chắc chắn các Animation cũng là Humanoid."); - else - Debug.LogWarning($"[Rig Info] Rig là Generic/Legacy. Nếu bạn dùng Animation Humanoid nó sẽ không chạy."); - - // 3. Kiểm tra Layer Weight - if (showLayerWeights) - { - for (int i = 0; i < animator.layerCount; i++) - { - Debug.Log($"Layer {i} ({animator.GetLayerName(i)}): Weight = {animator.GetLayerWeight(i)}"); - } - } - - // 4. Ép chạy State nếu có yêu cầu - if (!string.IsNullOrEmpty(forcePlayState)) - { - Debug.Log($"[Nuclear Force] ÉP CHẠY STATE: {forcePlayState}"); - animator.Play(forcePlayState); - forcePlayState = ""; // Reset sau khi chạy - } - - // 5. Kiểm tra vị trí xương (Nếu T-pose thì thường xương không đổi vị trí) - Vector3 handPos = animator.GetBoneTransform(HumanBodyBones.RightHand) ? animator.GetBoneTransform(HumanBodyBones.RightHand).position : Vector3.zero; - yield return new WaitForSeconds(0.1f); - if (handPos != Vector3.zero && Vector3.Distance(handPos, animator.GetBoneTransform(HumanBodyBones.RightHand).position) < 0.001f && animator.speed > 0) - { - // Nếu xương không nhúc nhích dù tốc độ > 0 - Debug.LogWarning($"[NUCLEAR ALERT] CẢNH BÁO: Xương nhân vật không nhúc nhích! Có thể do Rig lỗi hoặc bị script khác khóa xương."); - } - - if (!useSimulation) yield break; - } + protected virtual void OnDisable() + { + this.UnRegister(); + if (healthController) healthController.onReceiveDamage.RemoveListener(OnReceiveDamage); } protected virtual void InitializeParameters() { - if (animator == null) return; - - // Khởi tạo và kiểm tra từng tham số quan trọng - isDead = ValidateAndInit("isDead"); - isGrounded = ValidateAndInit("IsGrounded"); - isCrouching = ValidateAndInit("IsCrouching"); - isStrafing = ValidateAndInit("IsStrafing"); - isSliding = ValidateAndInit("IsSliding"); - isSprinting = ValidateAndInit("IsSprinting"); - isAiming = ValidateAndInit("IsAiming"); - canAim = ValidateAndInit("CanAim"); - flipAnimation = ValidateAndInit("FlipAnimation"); - flipEquip = ValidateAndInit("FlipEquip"); - groundDistance = ValidateAndInit("GroundDistance"); - groundAngle = ValidateAndInit("GroundAngle"); - verticalVelocity = ValidateAndInit("VerticalVelocity"); - moveSet_ID = ValidateAndInit("MoveSet_ID"); - upperBody_ID = ValidateAndInit("UpperBody_ID"); - idleRandom = ValidateAndInit("IdleRandom"); - idleRandomTrigger = ValidateAndInit("IdleRandomTrigger"); - randomAttack = ValidateAndInit("RandomAttack"); - weakAttack = ValidateAndInit("WeakAttack"); - strongAttack = ValidateAndInit("StrongAttack"); - isBlocking = ValidateAndInit("IsBlocking"); - attackID = ValidateAndInit("AttackID"); - defenseID = ValidateAndInit("DefenseID"); - recoilID = ValidateAndInit("RecoilID"); - reactionID = ValidateAndInit("ReactionID"); - triggerRecoil = ValidateAndInit("TriggerRecoil"); - triggerReaction = ValidateAndInit("TriggerReaction"); - hitDirection = ValidateAndInit("HitDirection"); - resetState = ValidateAndInit("ResetState"); - reload = ValidateAndInit("Reload"); - cancelReload = ValidateAndInit("CancelReload"); - reloadID = ValidateAndInit("ReloadID"); - shoot = ValidateAndInit("Shoot"); - shot_ID = ValidateAndInit("Shot_ID"); - powerCharger = ValidateAndInit("PowerCharger"); - } - - private vAnimatorParameter ValidateAndInit(string paramName) - { - vAnimatorParameter p = new vAnimatorParameter(animator, paramName); - if (!p.isValid && debugMode) - { - // Chỉ cảnh báo những biến cốt lõi nếu thiếu - if (paramName == "VerticalVelocity" || paramName == "IsGrounded" || paramName == "IsAiming") - Debug.LogWarning($"[AnimatorAI] Cảnh báo: Controller thiếu biến quan trọng: {paramName}"); - } - return p; + isDead = new vAnimatorParameter(animator, "isDead"); + isGrounded = new vAnimatorParameter(animator, "IsGrounded"); + isStrafing = new vAnimatorParameter(animator, "IsStrafing"); + isSprinting = new vAnimatorParameter(animator, "IsSprinting"); + isAiming = new vAnimatorParameter(animator, "IsAiming"); + verticalVelocity = new vAnimatorParameter(animator, "VerticalVelocity"); + horizontalVelocity = new vAnimatorParameter(animator, "HorizontalVelocity"); + groundDistance = new vAnimatorParameter(animator, "GroundDistance"); + moveSet_ID = new vAnimatorParameter(animator, "MoveSet_ID"); + attackID = new vAnimatorParameter(animator, "AttackID"); + hitDirection = new vAnimatorParameter(animator, "HitDirection"); + reactionID = new vAnimatorParameter(animator, "ReactionID"); + triggerReaction = new vAnimatorParameter(animator, "TriggerReaction"); + resetState = new vAnimatorParameter(animator, "ResetState"); } protected virtual void Update() { - if (animator == null) return; + if (animator == null || enemyAI == null || agent == null) return; - if (useSimulation) - { - RunSimulation(); - } - else - { - if (enemyAI == null || agent == null) return; - UpdateMovementParameters(); - UpdateCombatParameters(); - } - } - - protected virtual void RunSimulation() - { - // 1. Giả lập tốc độ di chuyển - if (autoCycleSpeed) - { - // Tạo vòng lặp tốc độ từ 0 đến 1 dùng hàm Sin - simVerticalVelocity = Mathf.Abs(Mathf.Sin(Time.time * 0.5f)); - simIsSprinting = simVerticalVelocity > sprintThreshold; - } - - SetFloat(verticalVelocity, simVerticalVelocity, "SIM: VerticalVelocity"); - SetBool(isSprinting, simIsSprinting, "SIM: IsSprinting"); - SetBool(isGrounded, true, "SIM: IsGrounded"); // Luôn giả lập trên mặt đất - - // 2. Giả lập chiến đấu - SetBool(isAiming, simIsAiming, "SIM: IsAiming"); - SetInt(moveSet_ID, simMoveSetID, "SIM: MoveSet_ID"); - SetBool(canAim, simIsAiming, "SIM: CanAim"); + UpdateMovementParameters(); + UpdateCombatParameters(); } protected virtual void UpdateMovementParameters() { + // 1. Grounded & GroundDistance (Critical for T-pose prevention) bool grounded = agent.isOnNavMesh || agent.enabled; - SetBool(isGrounded, grounded, "IsGrounded"); + SetBool(isGrounded, grounded); + SetFloat(groundDistance, grounded ? 0f : groundDistanceValue); - float speed = agent.velocity.magnitude / enemyAI.moveSpeed; - SetFloat(verticalVelocity, speed, "VerticalVelocity"); + // 2. Responsive Velocity Calculation + // Use a mix of agent velocity and position delta for better responsiveness + Vector3 worldVelocity = (transform.position - lastPosition) / Time.deltaTime; + lastPosition = transform.position; - bool sprinting = agent.velocity.magnitude > (enemyAI.moveSpeed * sprintThreshold); - SetBool(isSprinting, sprinting, "IsSprinting"); + if (worldVelocity.magnitude < 0.01f) worldVelocity = Vector3.zero; - bool isDodging = !agent.enabled && !rb.isKinematic; - SetBool(flipAnimation, isDodging, "FlipAnimation (Dodge)"); + Vector3 localVelocity = transform.InverseTransformDirection(worldVelocity); + + float targetV = localVelocity.z / enemyAI.moveSpeed; + float targetH = localVelocity.x / enemyAI.moveSpeed; + + // Smooth velocity values + currentV = Mathf.Lerp(currentV, targetV, 10f * Time.deltaTime); + currentH = Mathf.Lerp(currentH, targetH, 10f * Time.deltaTime); + + SetFloat(verticalVelocity, currentV); + SetFloat(horizontalVelocity, currentH); + + // 3. Sprinting + bool sprinting = worldVelocity.magnitude > (enemyAI.moveSpeed * sprintThreshold); + SetBool(isSprinting, sprinting); + + // 4. Strafing + SetBool(isStrafing, enemyAI.playerHasArtifact); } protected virtual void UpdateCombatParameters() { - bool aiming = enemyAI.playerHasArtifact && agent.isStopped; - SetBool(isAiming, aiming, "IsAiming"); + SetBool(isAiming, enemyAI.playerHasArtifact); + SetInt(moveSet_ID, enemyAI.playerHasArtifact ? 1 : 0); - int moveID = enemyAI.playerHasArtifact ? 1 : 0; - SetInt(moveSet_ID, moveID, "MoveSet_ID"); + // Shooting burst + if (enemyAI.IsShootingBurst) + SetInt(attackID, 1); + else + SetInt(attackID, 0); - SetBool(canAim, enemyAI.playerHasArtifact, "CanAim"); - } - - #region Optimized Setters with Debug - - protected void SetBool(vAnimatorParameter param, bool value, string name) - { - if (param.isValid) + // Dodge logic + if (enemyAI.IsDodging) { - bool current = animator.GetBool(param); - if (current != value) - { - animator.SetBool(param, value); - if (debugMode) Debug.Log($"[AnimDebug] {gameObject.name}: {name} -> {value}"); - } + // In Invector, dodges are often handled via triggers or specific IDs + SetAnimatorTrigger(triggerReaction); } + + // Death state + if (healthController) SetBool(isDead, healthController.isDead); } - protected void SetFloat(vAnimatorParameter param, float value, string name) + protected virtual void OnReceiveDamage(vDamage damage) + { + if (animator == null || !animator.enabled) return; + + // Sync damage parameters for hit reactions + if (hitDirection.isValid && damage.sender) + { + float angle = transform.HitAngle(damage.sender.position); + animator.SetInteger(hitDirection, (int)angle); + } + + if (reactionID.isValid) animator.SetInteger(reactionID, damage.reaction_id); + + SetAnimatorTrigger(triggerReaction); + SetAnimatorTrigger(resetState); + } + + #region Helpers + protected void SetBool(vAnimatorParameter param, bool value) + { + if (param.isValid && animator.GetBool(param) != value) + animator.SetBool(param, value); + } + + protected void SetFloat(vAnimatorParameter param, float value) { if (param.isValid) - { animator.SetFloat(param, value, dampTime, Time.deltaTime); - } } - protected void SetInt(vAnimatorParameter param, int value, string name) + protected void SetInt(vAnimatorParameter param, int value) { - if (param.isValid) - { - int current = animator.GetInteger(param); - if (current != value) - { - animator.SetInteger(param, value); - if (debugMode) Debug.Log($"[AnimDebug] {gameObject.name}: {name} -> {value}"); - } - } + if (param.isValid && animator.GetInteger(param) != value) + animator.SetInteger(param, value); } - #endregion - - #region Helper Methods (Triggers) - - public virtual void SetAnimatorTrigger(vAnimatorParameter trigger, string name = "Trigger") + public void SetAnimatorTrigger(vAnimatorParameter trigger) { - if (trigger.isValid) - { - if (debugMode) Debug.Log($"[AnimDebug] {gameObject.name}: Kích hoạt {name}"); - StartCoroutine(SetTriggerRoutine(trigger)); - } + if (trigger.isValid) StartCoroutine(SetTriggerRoutine(trigger)); } private IEnumerator SetTriggerRoutine(int targetHash) @@ -310,6 +202,5 @@ public class AnimatorAI : MonoBehaviour yield return new WaitForSeconds(0.1f); animator.ResetTrigger(targetHash); } - #endregion } diff --git a/Assets/Scripts/AI NPC/AutoDestroy.cs b/Assets/Scripts/AI NPC/AutoDestroy.cs index fa9d1645..643b35d5 100644 --- a/Assets/Scripts/AI NPC/AutoDestroy.cs +++ b/Assets/Scripts/AI NPC/AutoDestroy.cs @@ -1,16 +1,47 @@ +using Invector; using UnityEngine; public class AutoDestroy : MonoBehaviour { - // Start is called once before the first execution of Update after the MonoBehaviour is created + public int damageAmount = 30; void Start() { - + Destroy(gameObject,2f); } // Update is called once per frame - void Update() + private void OnTriggerEnter(Collider other) { + // Debug: Log tên và tag của bất cứ thứ gì đạn chạm vào + Debug.Log( + $"Laser collided with: {other.name} | Tag: {other.tag} | Layer: {LayerMask.LayerToName(other.gameObject.layer)}"); + + // Kiểm tra nếu trúng Player + if (other.CompareTag("Player") || other.GetComponentInParent() != null) + { + var healthController = other.GetComponentInParent(); + + if (healthController != null) + { + Debug.Log( + $"HIT PLAYER! Found health controller on {healthController.gameObject.name}. Applying {damageAmount} damage."); + var damage = new vDamage(damageAmount); + damage.sender = transform; + damage.hitPosition = transform.position; + healthController.TakeDamage(damage); + } + + // Luôn phá hủy đạn khi trúng Player + Impact(); + + } + } + + private void Impact() + { + + // Phá hủy đạn ngay lập tức + Destroy(gameObject); } } diff --git a/Assets/Scripts/AI NPC/EnemyAI.cs b/Assets/Scripts/AI NPC/EnemyAI.cs index ffe02250..194e95ce 100644 --- a/Assets/Scripts/AI NPC/EnemyAI.cs +++ b/Assets/Scripts/AI NPC/EnemyAI.cs @@ -10,7 +10,7 @@ using Random = UnityEngine.Random; [Serializable] public class DialogueResult { public string text; public float speedMod; public float suspicionMod; } -// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần +// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact / Sound Aggro) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Rigidbody))] public class EnemyAI : MonoBehaviour @@ -26,20 +26,18 @@ public class EnemyAI : MonoBehaviour public float rotateSpeed = 10f; [Header("Patrol Settings")] - public float patrolWaitTime = 2f; private float currentWaitTime = 0f; public float patrolSpeed = 2.5f; - - public float patrolRadius = 12f; // Bán kính của khu vực tuần tra ngẫu nhiên - - + public float patrolRadius = 12f; private Vector3 startPosition; + [Header("Combat State")] public bool playerHasArtifact; + public bool isAggroedBySound; // <-- MỚI: Trạng thái bị đánh động bởi âm thanh public GameObject laserPrefab; public Transform firePoint; - public float minShootDelay = 1.5f; // Delay giữa các LOẠT BẮN + public float minShootDelay = 1.5f; public float maxShootDelay = 3.5f; private float nextShootTime; @@ -59,11 +57,12 @@ public class EnemyAI : MonoBehaviour public float approachWeight = 0.35f; public float minCombatDistance = 5.0f; - - private float nextStrafeChangeTime; - private int strafeDirectionSign = 1; // -1: Trái, 1: Phải, 0: Đứng im bắn - private bool isShootingBurst = false; // Khóa chống trùng lặp loạt bắn + private int strafeDirectionSign = 1; + private bool isShootingBurst = false; + + public bool IsDodging => isDodging; + public bool IsShootingBurst => isShootingBurst; [Header("Conversation Settings")] public string npcName = "Guard"; @@ -71,7 +70,7 @@ public class EnemyAI : MonoBehaviour public float talkRange = 12f; public float talkCooldown = 60f; private float lastTalkTime; - public bool isTalking; // Public để debug + public bool isTalking; private EnemyAI talkingPartner; private Hallucinate.UI.ChatBubble chatBubble; @@ -92,6 +91,7 @@ public class EnemyAI : MonoBehaviour rb.isKinematic = true; rb.freezeRotation = true; + startPosition = transform.position; if (player == null) { @@ -105,7 +105,10 @@ public class EnemyAI : MonoBehaviour void InitTree() { var dodgeSequence = new Sequence(new List { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) }); - var laserSequence = new Sequence(new List { new TaskNode(CheckHasArtifact), new TaskNode(ActionFocusAndShoot) }); + + // Đổi hàm CheckHasArtifact thành CheckCombatConditions để dùng chung cho cả âm thanh + var laserSequence = new Sequence(new List { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) }); + var chaseSequence = new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionChasePlayer) }); var investigateSequence = new Sequence(new List { new TaskNode(CheckHasInvestigateTarget), new TaskNode(ActionInvestigate) }); var talkSequence = new Sequence(new List { new TaskNode(CheckCanTalkToNPC), new TaskNode(ActionTalk) }); @@ -128,9 +131,14 @@ public class EnemyAI : MonoBehaviour if (!agent.isOnNavMesh) return; - // Decay suspicion suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f); + // Bình tĩnh lại và tắt chế độ bắn dồn dập khi mức độ nghi ngờ về 0 + if (suspicionLevel <= 0f) + { + isAggroedBySound = false; + } + if (!isTalking && !isDodging && agent.isStopped) agent.isStopped = false; @@ -141,7 +149,7 @@ public class EnemyAI : MonoBehaviour private NodeState CheckDodgeConditions() { - if (playerHasArtifact) return NodeState.Failure; // Có cổ vật -> Không Dash né nữa + if (playerHasArtifact || isAggroedBySound) return NodeState.Failure; if (isDodging) return NodeState.Success; if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed) @@ -149,10 +157,12 @@ public class EnemyAI : MonoBehaviour return NodeState.Failure; } - private NodeState CheckHasArtifact() + // Node này thay thế cho CheckHasArtifact cũ + private NodeState CheckCombatConditions() { - if (playerHasArtifact) StopConversation(); - return playerHasArtifact ? NodeState.Success : NodeState.Failure; + bool shouldCombat = playerHasArtifact || isAggroedBySound; + if (shouldCombat) StopConversation(); + return shouldCombat ? NodeState.Success : NodeState.Failure; } private NodeState CheckCanSeePlayer() @@ -176,7 +186,7 @@ public class EnemyAI : MonoBehaviour private NodeState CheckCanTalkToNPC() { - if (playerHasArtifact || (fov != null && fov.canSeePlayer)) return NodeState.Failure; + if (playerHasArtifact || isAggroedBySound || (fov != null && fov.canSeePlayer)) return NodeState.Failure; if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure; if (isTalking) return NodeState.Success; @@ -216,6 +226,13 @@ public class EnemyAI : MonoBehaviour { suspicionLevel += volume * 15f; if (fov != null) fov.lastKnownPlayerPosition = location; + + // CẬP NHẬT: Nếu AI nghe thấy tiếng động làm mức nghi ngờ vượt mốc điều tra, chuyển sang trạng thái chiến đấu (bắn bồi) + if (suspicionLevel >= investigationThreshold) + { + isAggroedBySound = true; + } + if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(); StopConversation(); Debug.Log($"[AI {npcName}] Heard noise! Suspicion: {suspicionLevel}"); @@ -279,31 +296,25 @@ public class EnemyAI : MonoBehaviour private NodeState ActionPatrol() { - Debug.Log("Wandering randomly..."); agent.isStopped = false; agent.speed = patrolSpeed; - // Kiểm tra xem NPC đã đi đến điểm ngẫu nhiên hiện tại chưa if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) { currentWaitTime += Time.deltaTime; - // Đứng đợi hết thời gian quy định rồi mới tìm đường mới if (currentWaitTime >= patrolWaitTime) { - // 1. Lấy một điểm ngẫu nhiên trong không gian hình cầu dựa trên bán kính Vector3 randomDirection = Random.insideUnitSphere * patrolRadius; - randomDirection += startPosition; // Cộng với tâm ban đầu để giới hạn khu vực + randomDirection += startPosition; NavMeshHit hit; - // 2. Ép tọa độ ngẫu nhiên đó phải nằm TRÊN bề mặt xanh của NavMesh (tránh kẹt tường) - // Số 1 ở cuối là Area Mask (thường là Walkable) if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1)) { agent.SetDestination(hit.position); } - currentWaitTime = 0f; // Reset thời gian chờ + currentWaitTime = 0f; } } @@ -339,13 +350,30 @@ public class EnemyAI : MonoBehaviour private NodeState ActionFocusAndShoot() { - if (player == null) return NodeState.Failure; + if (player == null) return NodeState.Failure; if (agent.hasPath) agent.ResetPath(); agent.isStopped = false; - // 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ PLAYER - Vector3 bodyDir = player.position - transform.position; + // CẢI TIẾN: Xác định mục tiêu linh hoạt để tránh Wallhack. + Vector3 targetPos = player.position; // Mặc định khóa chặt người chơi + + // Nếu không cầm Artifact và cũng chưa bị nhìn thấy, AI chỉ nhắm vào MỐC ÂM THANH + if (!playerHasArtifact && fov != null && !fov.canSeePlayer && fov.lastKnownPlayerPosition != Vector3.zero) + { + targetPos = fov.lastKnownPlayerPosition; + + // Nếu AI tiến hành áp sát và xả đạn vào nơi phát ra tiếng mà không thấy ai, ngưng bắn + if (Vector3.Distance(transform.position, targetPos) < 2f) + { + isAggroedBySound = false; + suspicionLevel *= 0.5f; + return NodeState.Success; + } + } + + // 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ MỤC TIÊU (Người chơi hoặc Tiếng động) + Vector3 bodyDir = targetPos - transform.position; bodyDir.y = 0f; Vector3 bodyDirNormal = bodyDir.normalized; if (bodyDir != Vector3.zero) @@ -365,32 +393,28 @@ public class EnemyAI : MonoBehaviour // 3. TÍNH TOÁN TRỘN VECTOR: CHỈ TIẾN LÊN KHI ĐANG ĐI NGANG TRÁI/PHẢI Vector3 finalMovementVector = Vector3.zero; - // Kiểm tra xem AI có đang di chuyển ngang không (strafeDirectionSign khác 0) if (strafeDirectionSign != 0 && bodyDir != Vector3.zero) { - // Vector đi ngang trái/phải finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign; - // Tiến tới tịnh tiến dần dần nếu chưa đạt khoảng cách tối thiểu - float currentDistance = Vector3.Distance(transform.position, player.position); + float currentDistance = Vector3.Distance(transform.position, targetPos); if (currentDistance > minCombatDistance) { finalMovementVector += bodyDirNormal * approachWeight; } } - // Áp dụng di chuyển thực tế bằng Agent.Move if (finalMovementVector != Vector3.zero) { - finalMovementVector.Normalize(); // Chuẩn hóa vector + finalMovementVector.Normalize(); agent.speed = moveSpeed * 0.75f; agent.Move(finalMovementVector * agent.speed * Time.deltaTime); } - // 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO NGƯỜI PLAYER + // 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO MỤC TIÊU if (firePoint != null) { - Vector3 targetCenter = player.position + Vector3.up * 1f; + Vector3 targetCenter = targetPos + Vector3.up * 1f; Vector3 aimDir = targetCenter - firePoint.position; if (aimDir != Vector3.zero) { @@ -409,7 +433,6 @@ public class EnemyAI : MonoBehaviour return NodeState.Running; } - // Coroutine xử lý bắn loạt đạn kết hợp RANDOM ĐỘ LỆCH (Spread) private IEnumerator ShootBurstRoutine(int bulletCount) { isShootingBurst = true; @@ -418,19 +441,15 @@ public class EnemyAI : MonoBehaviour { if (laserPrefab == null || firePoint == null) break; - // Tính toán độ lệch ngẫu nhiên (Xoay quanh trục X và Y của họng súng để tạo độ "lệch tâm thông minh") float randomX = Random.Range(-maxSpreadAngle, maxSpreadAngle); float randomY = Random.Range(-maxSpreadAngle, maxSpreadAngle); Quaternion spreadRotation = Quaternion.Euler(randomX, randomY, 0f); - // Nhân góc xoay gốc của họng súng với góc lệch ngẫu nhiên Quaternion finalBulletRotation = firePoint.rotation * spreadRotation; - // Sinh đạn Instantiate(laserPrefab, firePoint.position, finalBulletRotation); Debug.Log($"[AI Burst] Viên thứ {i + 1}/{bulletCount} | Độ lệch X:{randomX:F1}, Y:{randomY:F1}"); - // Nếu còn đạn trong loạt, đợi một khoảng ngắn (burstInterval) rồi mới bắn tiếp viên sau if (i < bulletCount - 1) { yield return new WaitForSeconds(burstInterval); @@ -440,7 +459,7 @@ public class EnemyAI : MonoBehaviour isShootingBurst = false; } - private void ShootLaser() { } // Hàm cũ không dùng nữa, đã có Burst lo + private void ShootLaser() { } private NodeState ActionDodge() {