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 / 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 { [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; 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; 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)")] 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; private float nextStrafeChangeTime; private int strafeDirectionSign = 1; private bool isShootingBurst = false; [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; 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; startPosition = transform.position; 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) }); // Đổ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) }); 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; 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; rootNode?.Evaluate(); } #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) return NodeState.Success; return NodeState.Failure; } // Node này thay thế cho CheckHasArtifact cũ private NodeState CheckCombatConditions() { bool shouldCombat = playerHasArtifact || isAggroedBySound; if (shouldCombat) StopConversation(); return shouldCombat ? 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 || 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; 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; // 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}"); } 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() { 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; 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); } } }