diff --git a/Assets/Scripts/AI NPC/EnemyAI.cs b/Assets/Scripts/AI NPC/EnemyAI.cs index ae55a78d..a288bf71 100644 --- a/Assets/Scripts/AI NPC/EnemyAI.cs +++ b/Assets/Scripts/AI NPC/EnemyAI.cs @@ -1,140 +1,157 @@ using System.Collections; using System.Collections.Generic; using UnityEngine; -using UnityEngine.AI; -using System.Linq; +using UnityEngine.AI; +// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact) --> Đuổi theo (Vector) --> Điều tra --> Đi tuần [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(FieldOfView))] public class EnemyAI : MonoBehaviour { - [Header("References")] + [Header("Tham chieu he thong")] public Transform player; private NavMeshAgent agent; private Rigidbody rb; private FieldOfView fov; - [Header("Movement & Rotation")] + [Header("Toc do & Di chuyen")] public float moveSpeed = 3f; - public float rotateSpeed = 10f; // Tốc độ xoay mượt mờ khi rượt đuổi + public float rotateSpeed = 10f; - [Header("Patrol Waypoints")] - public Transform[] patrolPoints; - public float patrolWaitTime = 2f; - private int currentPatrolIndex = 0; - private float currentWaitTime; - - [Header("Artifact State")] + [Header("He thong di tuan (Waypoints)")] + public Transform[] patrolWaypoints; + public int currentWaypointIndex = 0; + public float patrolWaitTime = 2f; + private float currentWaitTime = 0f; + + [Header("Trang thai Co vat")] public bool playerHasArtifact; - [Header("Laser Weapon")] + [Header("Vu khi Laser")] public GameObject laserPrefab; public Transform firePoint; public float minShootDelay = 1f; public float maxShootDelay = 3f; private float nextShootTime; - [Header("Dodge Settings (Rigidbody)")] + [Header("Co che Ne don (Vat ly)")] public float dodgeForce = 10f; public float dodgeDuration = 0.2f; public float dodgeCooldown = 1.2f; private bool isDodging = false; - private float nextDodgeTime; + private float nextDodgeTime = 0f; - // Root của Behavior Tree - public Node behaviorTreeRoot; + [Header("Conversation")] + public string npcName = "Guard"; + public string persona = "You are a grumpy guard protecting gold."; + public float talkRange = 4f; + public float talkCooldown = 30f; + private float lastTalkTime; + private bool isTalking; + private EnemyAI talkingPartner; + private Hallucinate.UI.ChatBubble chatBubble; + + public Node rootNode; - private void Start() + void Start() { agent = GetComponent(); rb = GetComponent(); fov = GetComponent(); - agent.speed = moveSpeed; - - // Tự động tìm tất cả điểm PatrolPoint có trên Map - patrolPoints = GameObject.FindGameObjectsWithTag("PatrolPoint") - .Select(go => go.transform).ToArray(); - - // Setup Rigidbody ban đầu ở chế độ Kinematic để tránh lỗi xung đột NavMesh thường ngày - rb.isKinematic = true; + // Thiết lập Rigidbody chuẩn Unity 6 để sẵn sàng nhận lực né + rb.isKinematic = true; rb.freezeRotation = true; - FindPlayer(); - InitBehaviorTree(); + if (player == null) + { + GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); + if (playerObj != null) player = playerObj.transform; + } + chatBubble = GetComponentInChildren(true); + InitTree(); } - private void Update() + void InitTree() { - if (player == null) FindPlayer(); + // 1. Nhánh Né đòn (Khi player bấm chuột trái tấn công) + var dodgeSequence = new Sequence(new List + { + new TaskNode(CheckDodgeConditions), + new TaskNode(ActionDodge) + }); - // Luôn cập nhật cây hành vi - behaviorTreeRoot?.Evaluate(); - } - - private void FindPlayer() - { - GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); - if (playerObj != null) player = playerObj.transform; - } - - private void InitBehaviorTree() - { - // 1. Ưu tiên cao nhất: Kiểm tra né đòn - var dodgeNode = new TaskNode(CheckAndActionDodge); - - // 2. Ưu tiên hai: Player lấy được cổ vật -> Đứng lại nhắm bắn + // 2. Nhánh Tấn công Laser (Khi player lấy được cổ vật) var laserSequence = new Sequence(new List { new TaskNode(CheckHasArtifact), new TaskNode(ActionFocusAndShoot) }); - // 3. Ưu tiên ba: Tương tác đuổi theo bằng Vector hoặc Đi điều tra vết tích cuối cùng - var trackingSelector = new Selector(new List + // 3. Nhánh Đuổi theo (Sử dụng agent.Move di chuyển thẳng bằng Vector) + var chaseSequence = new Sequence(new List { -new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionChasePlayer) }), - new Sequence(new List { new TaskNode(CheckHasInvestigateTarget), new TaskNode(ActionInvestigate) }) + new TaskNode(CheckCanSeePlayer), + new TaskNode(ActionChasePlayer) }); - // 4. Mặc định: Đi tuần tra theo các Waypoint - var patrolNode = new TaskNode(ActionPatrol); - - // Gom tất cả vào bộ Selector gốc - behaviorTreeRoot = new Selector(new List + // 4. Nhánh Điều tra (Khi mất dấu, đi kiểm tra vị trí cuối cùng) + var investigateSequence = new Sequence(new List { - dodgeNode, + new TaskNode(CheckHasInvestigateTarget), + new TaskNode(ActionInvestigate) + }); + var talkSequence = new Sequence(new List + { + new TaskNode(CheckCanTalkToNPC), + new TaskNode(ActionTalk) + }); + // 5. Nhánh Đi tuần mặc định + var patrolAction = new TaskNode(ActionPatrol); + + // Xây dựng cây tổng hợp theo thứ tự ưu tiên từ trên xuống dưới + rootNode = new Selector(new List + { + dodgeSequence, laserSequence, - trackingSelector, - patrolNode + chaseSequence, + investigateSequence, + talkSequence, + patrolAction }); } - #region CONDITIONS - - private NodeState CheckAndActionDodge() + void Update() { - if (isDodging) return NodeState.Running; + if (player == null) return; + rootNode?.Evaluate(); + } - // Chỉ né khi: Thấy Player VÀ Player click chuột trái VÀ đã hồi chiêu né + #region CONDITIONS (CAC HAM KIEM TRA) + + private NodeState CheckDodgeConditions() + { + // Nếu đang trong quá trình né thì luôn cho phép chạy tiếp hành động né + if (isDodging) return NodeState.Success; + + // Điều kiện kích hoạt né: Thấy player + Player nhấn chuột trái + Hết cooldown if (fov.canSeePlayer && Input.GetMouseButtonDown(0) && Time.time >= nextDodgeTime) { - StartCoroutine(DodgeRollRoutine()); - nextDodgeTime = Time.time + dodgeCooldown; - return NodeState.Running; + return NodeState.Success; } - return NodeState.Failure; } private NodeState CheckHasArtifact() { + if (playerHasArtifact) StopConversation(); return playerHasArtifact ? NodeState.Success : NodeState.Failure; } private NodeState CheckCanSeePlayer() { + return fov.canSeePlayer ? NodeState.Success : NodeState.Failure; } @@ -145,26 +162,36 @@ new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(Acti #endregion - #region ACTIONS + #region ACTIONS (CAC HAM HANH DONG) + + private NodeState ActionDodge() + { + if (!isDodging) + { + StartCoroutine(DodgeRollRoutine()); + nextDodgeTime = Time.time + dodgeCooldown; + } + return NodeState.Running; + } private IEnumerator DodgeRollRoutine() { isDodging = true; - agent.enabled = false; // Tắt tạm thời Agent để giải phóng vật lý - rb.isKinematic = false; // Bật chế độ tương tác vật lý thực tế + agent.enabled = false; // Tắt định vị để nhường quyền cho Vật lý + rb.isKinematic = false; // Bật chế độ vật lý động - // Tính toán hướng né: Vuông góc góc nhìn đối diện với Player (Né sang trái hoặc phải) + // Tính hướng né vuông góc với Player Vector3 directionToPlayer = (player.position - transform.position).normalized; Vector3 perpendicularDir = new Vector3(-directionToPlayer.z, 0, directionToPlayer.x); Vector3 dodgeDirection = (Random.Range(0, 2) == 0 ? perpendicularDir : -perpendicularDir).normalized; - // Búng vật lý bằng lực Impulse + // Đẩy bằng lực Impulse vật lý thực tế rb.AddForce(dodgeDirection * dodgeForce, ForceMode.Impulse); yield return new WaitForSeconds(dodgeDuration); - // Trả lại trạng thái cũ cho NavMesh - rb.linearVelocity = Vector3.zero; // Cú pháp Unity 6 + // Trả lại quyền cho NavMeshAgent sau khi né xong + rb.linearVelocity = Vector3.zero; // Cú pháp chuẩn của Unity 6 rb.isKinematic = true; agent.enabled = true; isDodging = false; @@ -172,20 +199,19 @@ new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(Acti private NodeState ActionChasePlayer() { - if (player == null) return NodeState.Failure; - - Debug.Log("Chasing Player via Direct Vector!"); - agent.isStopped = false; + agent.isStopped = false; + agent.speed = moveSpeed; // 1. Tính toán hướng đi thẳng tắp (Bỏ trục Y) Vector3 dir = player.position - transform.position; - dir.y = 0f; + dir.y = 0f; dir.Normalize(); -// 2. Di chuyển tịnh tiến bằng Vector tích hợp qua Agent.Move để cản xuyên tường - Vector3 movement = dir * moveSpeed * Time.deltaTime; - agent.Move(movement); - // 3. Tự xoay mượt mà theo hướng di chuyển + // 2. Di chuyển tịnh tiến bằng Vector tích hợp qua Agent.Move + Vector3 movement = dir * moveSpeed * Time.deltaTime; + agent.Move(movement); + + // 3. Xoay mượt mà theo hướng di chuyển của Vector đúng ý bạn if (dir != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(dir); @@ -194,45 +220,107 @@ new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(Acti return NodeState.Running; } - - private NodeState ActionPatrol() + private NodeState CheckCanTalkToNPC() { - if (patrolPoints.Length == 0) return NodeState.Failure; + + if (playerHasArtifact || fov.canSeePlayer) return NodeState.Failure; + if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure; + if (isTalking) return NodeState.Success; - agent.isStopped = false; - agent.speed = moveSpeed * 0.5f; - agent.SetDestination(patrolPoints[currentPatrolIndex].position); - - if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) + // Tìm NPC gần nhất + Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange); + foreach (var hit in hitColliders) { - currentWaitTime += Time.deltaTime; - if (currentWaitTime >= patrolWaitTime) + if (hit.gameObject != gameObject && hit.CompareTag("Enemy")) { - currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; - currentWaitTime = 0f; + EnemyAI other = hit.GetComponent(); + if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown) + { + talkingPartner = other; + return NodeState.Success; + } } } + + return NodeState.Failure; + } + private NodeState ActionTalk() + { + if (talkingPartner == null) return NodeState.Failure; + + if (!isTalking) + { + isTalking = true; + agent.isStopped = true; + + // Xoay về phía bạn + FaceTarget(talkingPartner.transform.position); + + // Bắt đầu hội thoại qua Gemini + StartNPCConversation(); + } + return NodeState.Running; } - private NodeState ActionInvestigate() + private void StartNPCConversation() { - agent.isStopped = false; - agent.speed = moveSpeed * 0.7f; - agent.SetDestination(fov.lastKnownPlayerPosition); + string prompt = $"You are {npcName}. You are talking to your fellow guard {talkingPartner.npcName}. " + + "Keep it short (1 sentence). Topic: gold security or complaining about work."; - if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) + Hallucinate.AI.GeminiService.Instance.GetResponse(persona, prompt, (response) => { + if (chatBubble != null) chatBubble.Show(response); + + // Hẹn giờ kết thúc hội thoại + Invoke(nameof(EndConversation), 5f); + }); + + // Thông báo cho bạn diễn cũng dừng lại để "nghe" + talkingPartner.OnPartnerTalked(this); + } + + public void OnPartnerTalked(EnemyAI partner) + { + isTalking = true; + talkingPartner = partner; + agent.isStopped = true; + FaceTarget(partner.transform.position); + + // Chờ bạn nói xong mới phản hồi + Invoke(nameof(EndConversation), 6f); + } + + private void EndConversation() + { + isTalking = false; + lastTalkTime = Time.time; + if (agent != null) agent.isStopped = false; + talkingPartner = null; + } + + private void StopConversation() + { + if (!isTalking) return; + CancelInvoke(nameof(EndConversation)); + EndConversation(); + if (chatBubble != null) chatBubble.Show("Suỵt! Có gì đó không ổn...", 2f); + } + + private void FaceTarget(Vector3 targetPos) + { + Vector3 dir = targetPos - transform.position; + dir.y = 0; + if (dir != Vector3.zero) { - fov.lastKnownPlayerPosition = Vector3.zero; // Không thấy ai nữa thì hủy điểm nghi vấn - return NodeState.Success; + transform.rotation = Quaternion.LookRotation(dir); } - return NodeState.Running; } private NodeState ActionFocusAndShoot() { - agent.isStopped = true; + agent.isStopped = true; // Đứng im bắn cố định khi có cổ vật + // Tập trung xoay người nhìn về phía Player Vector3 dir = player.position - transform.position; dir.y = 0f; if (dir != Vector3.zero) @@ -241,6 +329,7 @@ new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(Acti transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); } + // Cơ chế đếm ngược bắn Laser ngẫu nhiên giống code cũ của bạn if (Time.time >= nextShootTime) { ShootLaser(); @@ -254,6 +343,51 @@ new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(Acti { if (laserPrefab == null || firePoint == null) return; Instantiate(laserPrefab, firePoint.position, firePoint.rotation); + Debug.Log("Laser Shot!"); + } + + private NodeState ActionInvestigate() + { + agent.isStopped = false; + agent.speed = moveSpeed * 0.7f; + agent.SetDestination(fov.lastKnownPlayerPosition); + + // Đi tới vị trí cuối cùng nhìn thấy Player + if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) + { + fov.lastKnownPlayerPosition = Vector3.zero; // Xóa vị trí nghi vấn để thoát trạng thái + return NodeState.Success; + } + return NodeState.Running; + } + + private NodeState ActionPatrol() + { + if (patrolWaypoints == null || patrolWaypoints.Length == 0) + { + return NodeState.Failure; + } + + agent.isStopped = false; + agent.speed = moveSpeed * 0.5f; // Đi tuần chậm rãi + + var targetWaypoint = patrolWaypoints[currentWaypointIndex]; + var distanceToTarget = Vector3.Distance(transform.position, targetWaypoint.position); + + // Nếu đã đến gần điểm tuần tra الحالي + if (distanceToTarget < 1f) + { + currentWaitTime += Time.deltaTime; + // Chờ một khoảng thời gian trước khi chuyển sang điểm tiếp theo + if (currentWaitTime >= patrolWaitTime) + { + currentWaypointIndex = (currentWaypointIndex + 1) % patrolWaypoints.Length; + currentWaitTime = 0f; + } + } + + agent.SetDestination(patrolWaypoints[currentWaypointIndex].position); + return NodeState.Running; } #endregion