using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using System.Linq; 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 Transform[] patrolWaypoints; public int currentWaypointIndex = 0; public float patrolWaitTime = 2f; private float currentWaitTime = 0f; [Header("Combat State")] public bool playerHasArtifact; public GameObject laserPrefab; public Transform firePoint; public float minShootDelay = 1f; public float maxShootDelay = 3f; 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("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 (isDodging) return NodeState.Success; if (fov != null && fov.canSeePlayer && Input.GetMouseButtonDown(0) && Time.time >= nextDodgeTime) 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) { // Randomly decide to check or stay on patrol 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) { // Kiểm tra khoảng cách thực tế giữa 2 NPC 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; // Nếu bạn diễn đi quá xa trong khi đang nói, ngắt hội thoại 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); // Apply minor stat mods moveSpeed += result.speedMod; suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100); lastTalkTime = Time.time; Debug.Log($"[AI {npcName}] Conv result: {result.text} | SpeedMod: {result.speedMod}"); } 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() { if (patrolWaypoints == null || patrolWaypoints.Length == 0) return NodeState.Failure; agent.isStopped = false; agent.speed = moveSpeed * (suspicionLevel > 20 ? 0.7f : 0.5f); // Walk faster if suspicious var target = patrolWaypoints[currentWaypointIndex]; agent.SetDestination(target.position); if (Vector3.Distance(transform.position, target.position) < 1.5f) { currentWaitTime += Time.deltaTime; if (currentWaitTime >= patrolWaitTime) { currentWaypointIndex = (currentWaypointIndex + 1) % patrolWaypoints.Length; 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) // Look around for 3 seconds { fov.lastKnownPlayerPosition = Vector3.zero; suspicionLevel *= 0.5f; // Decrease suspicion after check return NodeState.Success; } } return NodeState.Running; } private NodeState ActionFocusAndShoot() { agent.isStopped = true; FaceTarget(player.position); if (Time.time >= nextShootTime) { if (laserPrefab && firePoint) Instantiate(laserPrefab, firePoint.position, firePoint.rotation); nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); } return NodeState.Running; } 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() { // Vẽ vùng nói chuyện (Xanh lá) Gizmos.color = Color.green; Gizmos.DrawWireSphere(transform.position, talkRange); // Vẽ đường nối tới bạn diễn nếu đang nói if (isTalking && talkingPartner != null) { Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up); } } }