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ị 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")] public Transform player; private NavMeshAgent agent; private Rigidbody rb; private FieldOfView fov; private Collider mainCollider; private vHealthController health; [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; 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("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; private float nextStrafeChangeTime; private int strafeDirectionSign = 1; private bool isShootingBurst = false; 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; 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 = 25f; // Tăng tầm gọi hội public Node rootNode; void Start() { agent = GetComponent(); rb = GetComponent(); fov = GetComponent(); chatBubble = GetComponentInChildren(true); mainCollider = GetComponent(); health = GetComponent(); // 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"); 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() { // 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) }); // 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) }); var patrolAction = new TaskNode(ActionPatrol); rootNode = new Selector(new List { dodgeSequence, panicSequence, laserSequence, chaseSequence, investigateSequence, talkSequence, patrolAction }); } void Update() { if (player == null || health.isDead) return; // Đảm bảo Collider luôn bật if (mainCollider != null && !mainCollider.enabled) mainCollider.enabled = true; HandleHealthRegen(); suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f); if (suspicionLevel <= 0f && !isEnraged) isAggroedBySound = false; 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 (isDodging) return NodeState.Success; // 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; } 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 || isEnraged; if (shouldCombat) StopConversation(); return shouldCombat ? NodeState.Success : NodeState.Failure; } private NodeState CheckCanSeePlayer() { bool canSee = fov != null && fov.canSeePlayer; if (canSee) { StopConversation(); AlertNeighbors(transform.position); suspicionLevel = 100; } return canSee ? NodeState.Success : NodeState.Failure; } private NodeState CheckHasInvestigateTarget() { if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero && suspicionLevel > investigationThreshold) return NodeState.Success; return NodeState.Failure; } private NodeState CheckCanTalkToNPC() { 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 && !other.isEnraged) { if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID()) { 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 >= investigationThreshold) isAggroedBySound = true; if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(location); StopConversation(); } public void AlertNeighbors(Vector3 threatPos) { Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange); foreach (var hit in hitColliders) { EnemyAI neighbor = hit.GetComponentInParent(); if (neighbor != null && neighbor != this) { 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; 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 { 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); } } 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 = isEnraged ? Color.red : Color.green; Gizmos.DrawWireSphere(transform.position, alertRange); } }