using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using System.Linq; using UnityEngine.InputSystem; 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 [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Rigidbody))] public class EnemyAI : MonoBehaviour { [Header("References")] public Transform player; private NavMeshAgent agent; private Rigidbody rb; private FieldOfView fov; [Header("Movement Settings")] public float moveSpeed = 3f; 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 private Vector3 startPosition; [Header("Combat State")] public bool playerHasArtifact; public GameObject laserPrefab; public Transform firePoint; public float minShootDelay = 1.5f; // Delay giữa các LOẠT BẮN public float maxShootDelay = 3.5f; private float nextShootTime; [Header("Dodge Settings")] public float dodgeForce = 10f; public float dodgeDuration = 0.2f; public float dodgeCooldown = 1.2f; private bool isDodging = false; private float nextDodgeTime = 0f; [Header("Artifact Combat Upgrades (New)")] [Tooltip("Khoảng cách di chuyển trái/phải ngẫu nhiên qua thời gian duy trì")] public float minStrafeDuration = 0.5f; public float maxStrafeDuration = 2.2f; [Tooltip("Độ lệch tâm bắn (Độ). Số càng nhỏ bắn càng chuẩn, số lớn bắn càng lệch")] public float maxSpreadAngle = 6f; [Tooltip("Tốc độ bắn giữa các viên trong cùng 1 loạt đạn")] public float burstInterval = 0.12f; 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 public bool IsDodging => isDodging; public bool IsShootingBurst => isShootingBurst; [Header("Conversation Settings")] public string npcName = "Guard"; [TextArea] public string persona = "You are a bored security guard. You love coffee and hate night shifts."; public float talkRange = 12f; public float talkCooldown = 60f; private float lastTalkTime; public bool isTalking; // Public để debug private EnemyAI talkingPartner; private Hallucinate.UI.ChatBubble chatBubble; [Header("Suspicion Settings")] public float suspicionLevel = 0f; public float investigationThreshold = 30f; public float alertNeighborsThreshold = 70f; public float alertRange = 20f; public Node rootNode; void Start() { agent = GetComponent(); rb = GetComponent(); fov = GetComponent(); chatBubble = GetComponentInChildren(true); rb.isKinematic = true; rb.freezeRotation = true; if (player == null) { GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj != null) player = playerObj.transform; } InitTree(); } 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) }); 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) }); var patrolAction = new TaskNode(ActionPatrol); rootNode = new Selector(new List { dodgeSequence, laserSequence, chaseSequence, investigateSequence, talkSequence, patrolAction }); } void Update() { if (player == null) return; if (!agent.isOnNavMesh) return; // Decay suspicion suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f); if (!isTalking && !isDodging && agent.isStopped) agent.isStopped = false; rootNode?.Evaluate(); } #region CONDITIONS private NodeState CheckDodgeConditions() { if (playerHasArtifact) return NodeState.Failure; // Có cổ vật -> Không Dash né nữa if (isDodging) return NodeState.Success; if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed) return NodeState.Success; return NodeState.Failure; } private NodeState CheckHasArtifact() { if (playerHasArtifact) StopConversation(); return playerHasArtifact ? NodeState.Success : NodeState.Failure; } private NodeState CheckCanSeePlayer() { bool canSee = fov != null && fov.canSeePlayer; if (canSee) { StopConversation(); AlertNeighbors(); 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; } } return NodeState.Failure; } private NodeState CheckCanTalkToNPC() { if (playerHasArtifact || (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; 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) { float dist = Vector3.Distance(transform.position, other.transform.position); if (dist <= talkRange && 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; } } } return NodeState.Failure; } #endregion #region ACTIONS public void HearNoise(Vector3 location, float volume) { suspicionLevel += volume * 15f; if (fov != null) fov.lastKnownPlayerPosition = location; if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(); StopConversation(); Debug.Log($"[AI {npcName}] Heard noise! Suspicion: {suspicionLevel}"); } public void AlertNeighbors() { Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange); foreach (var hit in hitColliders) { 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; } } } 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; } return NodeState.Failure; } public void ProcessDialogueResult(string json) { try { DialogueResult result = JsonUtility.FromJson(json); if (chatBubble != null) chatBubble.Show(result.text); moveSpeed += result.speedMod; suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100); lastTalkTime = Time.time; } 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() { 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 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ờ } } 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; // 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ PLAYER Vector3 bodyDir = player.position - 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 KHOẢNG CÁCH DI CHUYỂN TRÁI/PHẢI (Tính theo thời gian duy trì) 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)]; // Ép khoảng cách di chuyển dài ngắn ngẫu nhiên bằng cách random thời gian đổi hướng nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration); } if (strafeDirectionSign != 0 && bodyDir != Vector3.zero) { Vector3 strafeDir = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign; agent.speed = moveSpeed * 0.75f; agent.Move(strafeDir * agent.speed * Time.deltaTime); } // 3. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO NGƯỜI PLAYER if (firePoint != null) { Vector3 targetCenter = player.position + Vector3.up * 1f; Vector3 aimDir = targetCenter - firePoint.position; if (aimDir != Vector3.zero) { firePoint.rotation = Quaternion.LookRotation(aimDir); } } // 4. RANDOM SỐ ĐẠN (1-3) & RANDOM DELAY GIỮA CÁC ĐỢT BẮN if (Time.time >= nextShootTime && !isShootingBurst) { int randomBulletCount = Random.Range(1, 4); // Trả về ngẫu nhiên 1, 2, hoặc 3 viên StartCoroutine(ShootBurstRoutine(randomBulletCount)); // Cập nhật thời gian chờ cho loạt đạn tiếp theo (Random delay) nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); } 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; for (int i = 0; i < bulletCount; i++) { 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); } } isShootingBurst = false; } private void ShootLaser() { } // Hàm cũ không dùng nữa, đã có Burst lo private NodeState ActionDodge() { if (!isDodging) StartCoroutine(DodgeRollRoutine()); return NodeState.Running; } private IEnumerator DodgeRollRoutine() { isDodging = true; agent.enabled = false; rb.isKinematic = false; 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) { Vector3 dir = (pos - transform.position); dir.y = 0; if (dir != Vector3.zero) transform.rotation = Quaternion.LookRotation(dir); } #endregion 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); } } }