diff --git a/.gemini-workspace-history/session-2026-06-06-00-13.json.gz b/.gemini-workspace-history/session-2026-06-06-00-13.json.gz new file mode 100644 index 00000000..42f4890c Binary files /dev/null and b/.gemini-workspace-history/session-2026-06-06-00-13.json.gz differ diff --git a/.idea/.idea.HALLUCINATE/.idea/workspace.xml b/.idea/.idea.HALLUCINATE/.idea/workspace.xml index bd84ceb1..df922906 100644 --- a/.idea/.idea.HALLUCINATE/.idea/workspace.xml +++ b/.idea/.idea.HALLUCINATE/.idea/workspace.xml @@ -6,11 +6,8 @@ - - - diff --git a/Assets/Prefabs/NPC/xNPC.prefab b/Assets/Prefabs/NPC/xNPC.prefab index 6419b6c4..749d97d4 100644 --- a/Assets/Prefabs/NPC/xNPC.prefab +++ b/Assets/Prefabs/NPC/xNPC.prefab @@ -225,7 +225,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 0 + m_IsActive: 1 --- !u!4 &5863061020199015852 Transform: m_ObjectHideFlags: 0 @@ -235,7 +235,7 @@ Transform: m_GameObject: {fileID: 5687887011233860168} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0.041, y: 1.5, z: -0.007} + m_LocalPosition: {x: 0.002, y: 1.5, z: 0.514} m_LocalScale: {x: 0.05, y: 0.05, z: 0.05} m_ConstrainProportionsScale: 1 m_Children: [] @@ -525,6 +525,11 @@ MonoBehaviour: dodgeForce: 8 dodgeDuration: 0.5 dodgeCooldown: 3 + isPanicking: 0 + isEnraged: 0 + panicHealthThreshold: 40 + regenRate: 2 + regenDelay: 5 minStrafeDuration: 0.5 maxStrafeDuration: 2.2 maxSpreadAngle: 3 @@ -641,14 +646,14 @@ CapsuleCollider: serializedVersion: 2 m_Bits: 0 m_LayerOverridePriority: 0 - m_IsTrigger: 1 + m_IsTrigger: 0 m_ProvidesContacts: 0 m_Enabled: 1 serializedVersion: 2 - m_Radius: 0.37279892 + m_Radius: 0.41056824 m_Height: 1.8474874 m_Direction: 1 - m_Center: {x: 0, y: 0.92911196, z: -0.24853802} + m_Center: {x: 0, y: 0.92911196, z: 0.061820984} --- !u!114 &204793640880232070 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/projectile/_LazerPrọectile.prefab b/Assets/Prefabs/projectile/_LazerPrọectile.prefab index 64fc3b19..845feaa2 100644 --- a/Assets/Prefabs/projectile/_LazerPrọectile.prefab +++ b/Assets/Prefabs/projectile/_LazerPrọectile.prefab @@ -246,8 +246,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 4e4f602386d4d484ea7a2a3b0c19ac21, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::LaserProjectile - speed: 100 - lifeTime: 5 + speed: 70 + lifeTime: 2 damageAmount: 10 hitSound: Laser_Hit --- !u!54 &-8021594009672363794 diff --git a/Assets/Scripts/AI NPC/EnemyAI.cs b/Assets/Scripts/AI NPC/EnemyAI.cs index d6e976dc..e6657fe6 100644 --- a/Assets/Scripts/AI NPC/EnemyAI.cs +++ b/Assets/Scripts/AI NPC/EnemyAI.cs @@ -1,18 +1,21 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using System.Linq; using UnityEngine.InputSystem; +using Invector; +using Invector.vCharacterController; 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 / Sound Aggro) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần +// Quy trình ưu tiên: Né đòn (Bị bắn) --> Panic (Thấp máu) --> Bắn hạ (Chiến đấu) --> Đuổi theo --> Điều tra --> Nói chuyện --> Đi tuần [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Rigidbody))] +[RequireComponent(typeof(vHealthController))] public class EnemyAI : MonoBehaviour { [Header("References")] @@ -21,6 +24,7 @@ public class EnemyAI : MonoBehaviour private Rigidbody rb; private FieldOfView fov; private Collider mainCollider; + private vHealthController health; [Header("Movement Settings")] public float moveSpeed = 3f; @@ -35,7 +39,7 @@ public class EnemyAI : MonoBehaviour [Header("Combat State")] public bool playerHasArtifact; - public bool isAggroedBySound; // <-- MỚI: Trạng thái bị đánh động bởi âm thanh + public bool isAggroedBySound; public GameObject laserPrefab; public Transform firePoint; public float minShootDelay = 1.5f; @@ -49,12 +53,20 @@ public class EnemyAI : MonoBehaviour private bool isDodging = false; private float nextDodgeTime = 0f; - [Header("Artifact Combat Upgrades (New)")] + [Header("Advanced AI States (New)")] + public bool isPanicking = false; + public bool isEnraged = false; + public float panicHealthThreshold = 40f; // Dưới 40% máu sẽ panic + public float regenRate = 2f; // Hồi 2 máu mỗi giây + public float regenDelay = 5f; // Cần 5s không nhận dame để bắt đầu hồi + private float lastDamageTime; + + [Header("Artifact Combat Upgrades")] public float minStrafeDuration = 0.5f; public float maxStrafeDuration = 2.2f; public float maxSpreadAngle = 6f; public float burstInterval = 0.12f; - + public float approachWeight = 0.35f; public float minCombatDistance = 5.0f; @@ -79,8 +91,8 @@ public class EnemyAI : MonoBehaviour public float suspicionLevel = 0f; public float investigationThreshold = 30f; public float alertNeighborsThreshold = 70f; - public float alertRange = 20f; - + public float alertRange = 25f; // Tăng tầm gọi hội + public Node rootNode; void Start() @@ -90,18 +102,15 @@ public class EnemyAI : MonoBehaviour fov = GetComponent(); chatBubble = GetComponentInChildren(true); mainCollider = GetComponent(); + health = GetComponent(); - // Tự động gán Layer Enemy nếu chưa có + // Thiết lập Invector Health + health.onReceiveDamage.AddListener(OnReceiveDamage); + health.onDead.AddListener(OnDead); + + // Tự động gán Layer Enemy if (gameObject.layer == LayerMask.NameToLayer("Default")) - { gameObject.layer = LayerMask.NameToLayer("Enemy"); - Debug.Log($"[AI {npcName}] Tự động chuyển Layer sang Enemy để súng có thể bắn trúng."); - } - - if (mainCollider == null) - { - Debug.LogError($"[AI {npcName}] THIẾU COLLIDER! NPC này sẽ không thể bị bắn trúng."); - } rb.isKinematic = true; rb.freezeRotation = true; @@ -118,11 +127,16 @@ public class EnemyAI : MonoBehaviour void InitTree() { + // 1. Phản xạ tức thì (Dodge) var dodgeSequence = new Sequence(new List { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) }); + + // 2. Hoảng loạn (Panic) + var panicSequence = new Sequence(new List { new TaskNode(CheckPanicConditions), new TaskNode(ActionPanic) }); - // Đổi hàm CheckHasArtifact thành CheckCombatConditions để dùng chung cho cả âm thanh + // 3. Tấn công (Laser) var laserSequence = new Sequence(new List { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) }); - + + // 4. Các hành vi khác 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) }); @@ -131,6 +145,7 @@ public class EnemyAI : MonoBehaviour rootNode = new Selector(new List { dodgeSequence, + panicSequence, laserSequence, chaseSequence, investigateSequence, @@ -141,47 +156,115 @@ public class EnemyAI : MonoBehaviour void Update() { - if (player == null) return; + if (player == null || health.isDead) return; // Đảm bảo Collider luôn bật - if (mainCollider != null && !mainCollider.enabled) - { - mainCollider.enabled = true; - Debug.LogWarning($"[AI {npcName}] Collider bị tắt trái phép! Đã tự động bật lại."); - } + if (mainCollider != null && !mainCollider.enabled) mainCollider.enabled = true; - if (!agent.isOnNavMesh) return; + HandleHealthRegen(); 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 (suspicionLevel <= 0f && !isEnraged) isAggroedBySound = false; - if (!isTalking && !isDodging && agent.isStopped) + if (!isTalking && !isDodging && !isPanicking && agent.isStopped) agent.isStopped = false; rootNode?.Evaluate(); } + private void HandleHealthRegen() + { + if (Time.time > lastDamageTime + regenDelay && health.currentHealth < health.maxHealth) + { + health.AddHealth((int)(regenRate * Time.deltaTime)); + } + } + + #region HEALTH EVENTS + + private void OnReceiveDamage(vDamage damage) + { + lastDamageTime = Time.time; + isAggroedBySound = true; + suspicionLevel = 100f; + StopConversation(); + + // Gọi hội ngay khi bị bắn + AlertNeighbors(damage.hitPosition); + + // Né đòn ngay lập tức khi bị trúng dame (phản xạ Elden Ring) + if (Time.time > nextDodgeTime && !isDodging) + { + StartCoroutine(DodgeRollRoutine()); + } + + // Kiểm tra Enrage (Phase 2) + if (health.currentHealth < health.maxHealth * 0.5f && !isEnraged) + { + EnterEnrageMode(); + } + } + + private void OnDead(GameObject killer) + { + Debug.Log($"[AI {npcName}] DIED."); + // Khi chết, làm những con xung quanh Enraged gắt hơn + Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange); + foreach (var hit in hitColliders) + { + EnemyAI ally = hit.GetComponentInParent(); + if (ally != null && ally != this) ally.EnterEnrageMode(); + } + + agent.enabled = false; + this.enabled = false; + } + + public void EnterEnrageMode() + { + if (isEnraged) return; + isEnraged = true; + isPanicking = false; // Hết sợ, chuyển sang liều mạng + + // Buff stats mạnh mẽ như Elden Ring boss phase 2 + moveSpeed *= 1.5f; + minShootDelay *= 0.5f; + maxShootDelay *= 0.5f; + maxSpreadAngle *= 0.6f; // Bắn chính xác hơn + approachWeight = 0.8f; // Áp sát cực gắt + minCombatDistance = 2.0f; // Đứng sát mặt Player để bắn + + if (chatBubble != null) chatBubble.Show("FOR THE BROTHERHOOD! DIE!", 3f); + Debug.Log($"[AI {npcName}] ENRAGED! Stats boosted."); + } + + #endregion + #region CONDITIONS private NodeState CheckDodgeConditions() { - if (playerHasArtifact || isAggroedBySound) return NodeState.Failure; - if (isDodging) return NodeState.Success; - if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed) + // Tự né khi thấy Player click chuột trái (đang bắn) + if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed && Time.time > nextDodgeTime) return NodeState.Success; return NodeState.Failure; } - // Node này thay thế cho CheckHasArtifact cũ + private NodeState CheckPanicConditions() + { + if (isEnraged) return NodeState.Failure; // Đang điên thì không sợ + if (health.currentHealth < (health.maxHealth * (panicHealthThreshold / 100f))) + { + return NodeState.Success; + } + return NodeState.Failure; + } + private NodeState CheckCombatConditions() { - bool shouldCombat = playerHasArtifact || isAggroedBySound; + bool shouldCombat = playerHasArtifact || isAggroedBySound || isEnraged; if (shouldCombat) StopConversation(); return shouldCombat ? NodeState.Success : NodeState.Failure; } @@ -189,48 +272,32 @@ public class EnemyAI : MonoBehaviour private NodeState CheckCanSeePlayer() { bool canSee = fov != null && fov.canSeePlayer; - if (canSee) { StopConversation(); AlertNeighbors(); suspicionLevel = 100; } + if (canSee) { StopConversation(); AlertNeighbors(transform.position); suspicionLevel = 100; } return canSee ? NodeState.Success : NodeState.Failure; } private NodeState CheckHasInvestigateTarget() { - if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero) - { - if (suspicionLevel > investigationThreshold) - { - if (Random.value < (suspicionLevel / 100f)) return NodeState.Success; - } - } + if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero && suspicionLevel > investigationThreshold) + return NodeState.Success; return NodeState.Failure; } private NodeState CheckCanTalkToNPC() { - if (playerHasArtifact || isAggroedBySound || (fov != null && fov.canSeePlayer)) return NodeState.Failure; - if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure; - if (isTalking) return NodeState.Success; - - if (Hallucinate.AI.ConversationManager.Instance == null) - { - Debug.LogError($"[AI {npcName}] ConversationManager Instance is NULL!"); - return NodeState.Failure; - } - - if (!Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure; + if (playerHasArtifact || isAggroedBySound || isEnraged || (fov != null && fov.canSeePlayer)) return NodeState.Failure; + if (Time.time < lastTalkTime + talkCooldown || isTalking) return NodeState.Failure; + if (Hallucinate.AI.ConversationManager.Instance == null || !Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure; Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange); foreach (var hit in hitColliders) { if (hit.gameObject == gameObject) continue; - EnemyAI other = hit.GetComponentInParent(); - if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown) + if (other != null && !other.isTalking && !other.isEnraged) { - float dist = Vector3.Distance(transform.position, other.transform.position); - if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID()) + if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID()) { - Debug.Log($"[AI {npcName}] Found partner: {other.npcName}. Starting conversation."); Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other); return NodeState.Success; } @@ -247,29 +314,12 @@ public class EnemyAI : MonoBehaviour { suspicionLevel += volume * 15f; if (fov != null) fov.lastKnownPlayerPosition = location; - - // 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 - if (suspicionLevel >= investigationThreshold) - { - isAggroedBySound = true; - } - - if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(); + if (suspicionLevel >= investigationThreshold) isAggroedBySound = true; + if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(location); StopConversation(); - Debug.Log($"[AI {npcName}] Heard noise! Suspicion: {suspicionLevel}"); } - public void TriggerCombatAlert(Vector3 sourceLocation) - { - suspicionLevel = 100f; - isAggroedBySound = true; // Ép vào trạng thái tấn công - if (fov != null) fov.lastKnownPlayerPosition = sourceLocation; - StopConversation(); - AlertNeighbors(); - Debug.Log($"[AI {npcName}] GUNFIRE DETECTED! Entering combat mode."); - } - - public void AlertNeighbors() + public void AlertNeighbors(Vector3 threatPos) { Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange); foreach (var hit in hitColliders) @@ -277,31 +327,185 @@ public class EnemyAI : MonoBehaviour EnemyAI neighbor = hit.GetComponentInParent(); if (neighbor != null && neighbor != this) { - neighbor.suspicionLevel = Mathf.Max(neighbor.suspicionLevel, 50f); - if (fov != null && neighbor.fov != null) neighbor.fov.lastKnownPlayerPosition = fov.lastKnownPlayerPosition; + neighbor.TriggerCombatAlert(threatPos); } } } + public void TriggerCombatAlert(Vector3 sourceLocation) + { + if (isEnraged) return; + suspicionLevel = 100f; + isAggroedBySound = true; + if (fov != null) fov.lastKnownPlayerPosition = sourceLocation; + StopConversation(); + } + + private NodeState ActionPanic() + { + isPanicking = true; + agent.speed = moveSpeed * 1.3f; + + // Chạy trốn khỏi Player đến một điểm ngẫu nhiên xa Player + if (!agent.pathPending && agent.remainingDistance < 1f) + { + Vector3 runDir = (transform.position - player.position).normalized; + Vector3 escapePoint = transform.position + runDir * 10f + Random.insideUnitSphere * 5f; + + NavMeshHit hit; + if (NavMesh.SamplePosition(escapePoint, out hit, 10f, 1)) + agent.SetDestination(hit.position); + } + + if (chatBubble != null && Random.value < 0.01f) chatBubble.Show("HELP! HE'S KILLING US!", 1f); + + return NodeState.Running; + } + private NodeState ActionTalk() { - if (isTalking) - { - agent.isStopped = true; - if (talkingPartner != null) - { - if (Vector3.Distance(transform.position, talkingPartner.transform.position) > talkRange + 2f) - { - Debug.Log($"[AI {npcName}] Partner moved too far. Ending conversation."); - StopConversation(); - return NodeState.Failure; - } - } - return NodeState.Running; - } + if (isTalking) { agent.isStopped = true; return NodeState.Running; } return NodeState.Failure; } + private void StopConversation() + { + if (isTalking && Hallucinate.AI.ConversationManager.Instance != null) + { + Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this); + } + } + + private NodeState ActionPatrol() + { + isPanicking = false; + agent.isStopped = false; + agent.speed = patrolSpeed; + if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) + { + currentWaitTime += Time.deltaTime; + if (currentWaitTime >= patrolWaitTime) + { + Vector3 randomDest = startPosition + Random.insideUnitSphere * patrolRadius; + NavMeshHit hit; + if (NavMesh.SamplePosition(randomDest, out hit, patrolRadius, 1)) agent.SetDestination(hit.position); + currentWaitTime = 0f; + } + } + return NodeState.Running; + } + + private NodeState ActionChasePlayer() + { + isPanicking = false; + agent.isStopped = false; + agent.speed = moveSpeed; + agent.SetDestination(player.position); + return NodeState.Running; + } + + private NodeState ActionInvestigate() + { + agent.isStopped = false; + agent.speed = moveSpeed * 0.7f; + agent.SetDestination(fov.lastKnownPlayerPosition); + if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f) + { + currentWaitTime += Time.deltaTime; + if (currentWaitTime > 3f) { fov.lastKnownPlayerPosition = Vector3.zero; return NodeState.Success; } + } + return NodeState.Running; + } + + private NodeState ActionFocusAndShoot() + { + isPanicking = false; + if (player == null) return NodeState.Failure; + if (agent.hasPath) agent.ResetPath(); + agent.isStopped = false; + + Vector3 targetPos = player.position; + if (!playerHasArtifact && fov != null && !fov.canSeePlayer && fov.lastKnownPlayerPosition != Vector3.zero) + targetPos = fov.lastKnownPlayerPosition; + + // Xoay về phía mục tiêu + Vector3 bodyDir = (targetPos - transform.position); + bodyDir.y = 0f; + if (bodyDir != Vector3.zero) + transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(bodyDir), rotateSpeed * Time.deltaTime); + + // Strafe di chuyển linh hoạt + if (Time.time >= nextStrafeChangeTime) + { + strafeDirectionSign = new int[] { -1, 1, 0 }[Random.Range(0, 3)]; + nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration); + } + + Vector3 moveDir = Vector3.zero; + if (strafeDirectionSign != 0 && bodyDir != Vector3.zero) + { + Vector3 normal = bodyDir.normalized; + moveDir = new Vector3(-normal.z, 0, normal.x) * strafeDirectionSign; + if (Vector3.Distance(transform.position, targetPos) > minCombatDistance) moveDir += normal * approachWeight; + } + + if (moveDir != Vector3.zero) + { + agent.speed = moveSpeed * (isEnraged ? 1f : 0.75f); + agent.Move(moveDir.normalized * agent.speed * Time.deltaTime); + } + + if (firePoint != null) + firePoint.rotation = Quaternion.LookRotation((targetPos + Vector3.up * 1f) - firePoint.position); + + if (Time.time >= nextShootTime && !isShootingBurst) + { + StartCoroutine(ShootBurstRoutine(Random.Range(1, isEnraged ? 6 : 4))); + nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); + } + + return NodeState.Running; + } + + private IEnumerator ShootBurstRoutine(int bulletCount) + { + isShootingBurst = true; + for (int i = 0; i < bulletCount; i++) + { + if (laserPrefab == null || firePoint == null) break; + float spread = isEnraged ? maxSpreadAngle * 0.5f : maxSpreadAngle; + Quaternion rot = firePoint.rotation * Quaternion.Euler(Random.Range(-spread, spread), Random.Range(-spread, spread), 0f); + Instantiate(laserPrefab, firePoint.position, rot); + yield return new WaitForSeconds(isEnraged ? burstInterval * 0.7f : burstInterval); + } + isShootingBurst = false; + } + + private NodeState ActionDodge() + { + if (!isDodging) StartCoroutine(DodgeRollRoutine()); + return NodeState.Running; + } + + private IEnumerator DodgeRollRoutine() + { + isDodging = true; + agent.enabled = false; + rb.isKinematic = false; + if (mainCollider != null) mainCollider.enabled = true; + + Vector3 dir = (player.position - transform.position).normalized; + Vector3 perp = new Vector3(-dir.z, 0, dir.x) * (Random.value > 0.5f ? 1 : -1); + rb.AddForce(perp * (isEnraged ? dodgeForce * 1.5f : dodgeForce), ForceMode.Impulse); + + yield return new WaitForSeconds(dodgeDuration); + rb.linearVelocity = Vector3.zero; + rb.isKinematic = true; + agent.enabled = true; + nextDodgeTime = Time.time + (isEnraged ? dodgeCooldown * 0.5f : dodgeCooldown); + isDodging = false; + } + public void ProcessDialogueResult(string json) { try @@ -316,209 +520,6 @@ public class EnemyAI : MonoBehaviour catch { if (chatBubble != null) chatBubble.Show(json); } } - private void StopConversation() - { - if (isTalking && Hallucinate.AI.ConversationManager.Instance != null) - { - Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this); - if (chatBubble != null) chatBubble.Show("Wait, what was that?!", 2f); - } - } - - private NodeState ActionPatrol() - { - agent.isStopped = false; - agent.speed = patrolSpeed; - - if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) - { - currentWaitTime += Time.deltaTime; - - if (currentWaitTime >= patrolWaitTime) - { - Vector3 randomDirection = Random.insideUnitSphere * patrolRadius; - randomDirection += startPosition; - - NavMeshHit hit; - if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1)) - { - agent.SetDestination(hit.position); - } - - currentWaitTime = 0f; - } - } - - return NodeState.Running; - } - - private NodeState ActionChasePlayer() - { - agent.isStopped = false; - agent.speed = moveSpeed; - agent.SetDestination(player.position); - return NodeState.Running; - } - - private NodeState ActionInvestigate() - { - agent.isStopped = false; - agent.speed = moveSpeed * 0.7f; - agent.SetDestination(fov.lastKnownPlayerPosition); - - if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f) - { - currentWaitTime += Time.deltaTime; - if (currentWaitTime > 3f) - { - fov.lastKnownPlayerPosition = Vector3.zero; - suspicionLevel *= 0.5f; - return NodeState.Success; - } - } - return NodeState.Running; - } - - private NodeState ActionFocusAndShoot() - { - if (player == null) return NodeState.Failure; - - if (agent.hasPath) agent.ResetPath(); - agent.isStopped = false; - - // 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) - { - Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal); - transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); - } - - // 2. RANDOM THỜI GIAN/KHOẢNG CÁCH DUY TRÌ HƯỚNG DI CHUYỂN - if (Time.time >= nextStrafeChangeTime) - { - int[] choices = { -1, 1, 0 }; // Trái, Phải, Đứng yên bắn - strafeDirectionSign = choices[Random.Range(0, choices.Length)]; - nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration); - } - - // 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; - - if (strafeDirectionSign != 0 && bodyDir != Vector3.zero) - { - finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign; - - float currentDistance = Vector3.Distance(transform.position, targetPos); - if (currentDistance > minCombatDistance) - { - finalMovementVector += bodyDirNormal * approachWeight; - } - } - - if (finalMovementVector != Vector3.zero) - { - 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 MỤC TIÊU - if (firePoint != null) - { - Vector3 targetCenter = targetPos + Vector3.up * 1f; - Vector3 aimDir = targetCenter - firePoint.position; - if (aimDir != Vector3.zero) - { - firePoint.rotation = Quaternion.LookRotation(aimDir); - } - } - - // 5. RANDOM SỐ ĐẠN & DELAY GIỮA CÁC ĐỢT BẮN - if (Time.time >= nextShootTime && !isShootingBurst) - { - int randomBulletCount = Random.Range(1, 4); - StartCoroutine(ShootBurstRoutine(randomBulletCount)); - nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); - } - - return NodeState.Running; - } - - private IEnumerator ShootBurstRoutine(int bulletCount) - { - isShootingBurst = true; - - for (int i = 0; i < bulletCount; i++) - { - if (laserPrefab == null || firePoint == null) break; - - float randomX = Random.Range(-maxSpreadAngle, maxSpreadAngle); - float randomY = Random.Range(-maxSpreadAngle, maxSpreadAngle); - Quaternion spreadRotation = Quaternion.Euler(randomX, randomY, 0f); - - Quaternion finalBulletRotation = firePoint.rotation * spreadRotation; - - Instantiate(laserPrefab, firePoint.position, finalBulletRotation); - Debug.Log($"[AI Burst] Viên thứ {i + 1}/{bulletCount} | Độ lệch X:{randomX:F1}, Y:{randomY:F1}"); - - if (i < bulletCount - 1) - { - yield return new WaitForSeconds(burstInterval); - } - } - - isShootingBurst = false; - } - - private void ShootLaser() { } - - private NodeState ActionDodge() - { - if (!isDodging) StartCoroutine(DodgeRollRoutine()); - return NodeState.Running; - } - - private IEnumerator DodgeRollRoutine() - { - isDodging = true; - agent.enabled = false; - rb.isKinematic = false; - - // GIỮ COLLIDER LUÔN BẬT KHI NÉ - if (mainCollider != null) mainCollider.enabled = true; - - Vector3 dir = (player.position - transform.position).normalized; - Vector3 perp = new Vector3(-dir.z, 0, dir.x); - rb.AddForce((Random.value > 0.5f ? perp : -perp) * dodgeForce, ForceMode.Impulse); - - yield return new WaitForSeconds(dodgeDuration); - - rb.linearVelocity = Vector3.zero; - rb.isKinematic = true; - agent.enabled = true; - isDodging = false; - } - public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; } public void FaceTarget(Vector3 pos) { @@ -531,13 +532,7 @@ public class EnemyAI : MonoBehaviour private void OnDrawGizmos() { - Gizmos.color = Color.green; - Gizmos.DrawWireSphere(transform.position, talkRange); - - if (isTalking && talkingPartner != null) - { - Gizmos.color = Color.yellow; - Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up); - } + Gizmos.color = isEnraged ? Color.red : Color.green; + Gizmos.DrawWireSphere(transform.position, alertRange); } } \ No newline at end of file