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; public float aggressionMod; // Ảnh hưởng delay bắn (0.1 -> 1.0) public float braveryMod; // Ảnh hưởng ngưỡng Panic public float healthMod; // Hồi máu hoặc mất máu tâm lý } [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.8f; // Tăng nhẹ delay để đỡ khó public float maxShootDelay = 4.0f; private float nextShootTime; [Header("Dodge Settings")] public float dodgeForce = 8f; public float dodgeDuration = 0.2f; public float dodgeCooldown = 2.0f; // Tăng cooldown né đòn private bool isDodging = false; private float nextDodgeTime = 0f; [Header("Advanced AI States")] public bool isPanicking = false; public bool isEnraged = false; public float panicHealthThreshold = 50f; // Chạy ngược lại khi < 50% máu public float regenRate = 1.5f; public float regenDelay = 5f; private float lastDamageTime; [Header("Personality (Randomized)")] private float personalApproachWeight; private float personalMinCombatDistance; private float personalBurstMax; private float personalStrafeIntensity; [Header("Artifact Combat Upgrades")] public float minStrafeDuration = 0.5f; public float maxStrafeDuration = 2.2f; public float maxSpreadAngle = 7f; public float burstInterval = 0.15f; 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."; 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 = 30f; public Node rootNode; void Start() { agent = GetComponent(); rb = GetComponent(); fov = GetComponent(); chatBubble = GetComponentInChildren(true); mainCollider = GetComponent(); health = GetComponent(); // RANDOM TÍNH CÁCH personalApproachWeight = Random.Range(0.2f, 0.7f); personalMinCombatDistance = Random.Range(3f, 8f); personalBurstMax = Random.Range(2, 5); personalStrafeIntensity = Random.Range(0.5f, 1.5f); health.onReceiveDamage.AddListener(OnReceiveDamage); health.onDead.AddListener(OnDead); if (gameObject.layer == LayerMask.NameToLayer("Default")) { int enemyLayer = LayerMask.NameToLayer("Enemy"); if (enemyLayer != -1) { gameObject.layer = enemyLayer; Debug.Log($"[AI {npcName}] Đã chuyển sang Layer: Enemy"); } else { Debug.LogWarning($"[AI {npcName}] CẢNH BÁO: Không tìm thấy Layer 'Enemy' trong Project! Hãy tạo Layer 'Enemy' để súng có thể bắn trúng."); } } 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) }); var panicSequence = new Sequence(new List { new TaskNode(CheckPanicConditions), new TaskNode(ActionRetreat) }); // Thay Panic bằng Retreat 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, panicSequence, laserSequence, chaseSequence, investigateSequence, talkSequence, patrolAction }); } void Update() { if (player == null || health.isDead) return; 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) { float currentRegenSpeed = regenRate; float healthPercent = (float)health.currentHealth / health.maxHealth; // Tăng tốc hồi máu khi máu cực thấp (< 25%) if (healthPercent < 0.25f) currentRegenSpeed *= 4f; else if (healthPercent < 0.5f) currentRegenSpeed *= 2f; health.AddHealth((int)(currentRegenSpeed * Time.deltaTime)); } } #region HEALTH EVENTS private void OnReceiveDamage(vDamage damage) { lastDamageTime = Time.time; isAggroedBySound = true; suspicionLevel = 100f; StopConversation(); // PHẢN ỨNG TỨC THÌ: Alert toàn bộ lân cận AlertNeighbors(damage.hitPosition); // PHẢN ỨNG TỨC THÌ: Reset delay bắn để phản công nhanh hoặc né nextShootTime = Time.time + 0.5f; // Né đòn Elden Ring (Tăng tỉ lệ né khi trúng dame) if (Time.time > nextDodgeTime && !isDodging && Random.value < 0.7f) { StartCoroutine(DodgeRollRoutine()); } // Tự động Enrage nếu bị dồn vào đường cùng (nhưng đồng thời vẫn có thể bỏ chạy nếu không phẫn nộ) if (health.currentHealth < health.maxHealth * 0.2f && !isEnraged) { EnterEnrageMode(); } } private void OnDead(GameObject killer) { Debug.Log($"[AI {npcName}] DIED."); // 1. Vô hiệu hóa va chạm và di chuyển ngay lập tức if (mainCollider != null) mainCollider.enabled = false; agent.enabled = false; // 2. Kích hoạt Enrage cho đồng đội xung quanh Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange); foreach (var hit in hitColliders) { EnemyAI ally = hit.GetComponentInParent(); if (ally != null && ally != this) ally.EnterEnrageMode(); } // 3. Tự hủy sau 3 giây (để kịp chạy animation chết hoặc hiệu ứng) Destroy(gameObject, 3f); this.enabled = false; } public void EnterEnrageMode() { if (isEnraged) return; isEnraged = true; isPanicking = false; moveSpeed *= 1.3f; // Giảm nhẹ buff speed minShootDelay *= 0.6f; maxShootDelay *= 0.6f; if (chatBubble != null) chatBubble.Show("I'LL TAKE YOU DOWN WITH ME!", 2f); } #endregion #region CONDITIONS private NodeState CheckDodgeConditions() { if (isDodging) return NodeState.Success; // Tự né khi thấy Player đ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ợ, chiến đến chết // Nếu máu dưới ngưỡng thiết lập (ví dụ 50%), kích hoạt trạng thái tháo chạy if (health.currentHealth < (health.maxHealth * (panicHealthThreshold / 100f))) { return NodeState.Success; } isPanicking = false; 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 * 20f; 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(); } // Hành động tháo chạy khi máu thấp private NodeState ActionRetreat() { isPanicking = true; agent.isStopped = false; agent.speed = moveSpeed * 1.3f; // Chạy nhanh hơn bình thường để thoát thân // Tính toán hướng ngược lại với Player Vector3 retreatDir = (transform.position - player.position).normalized; Vector3 targetPos = transform.position + retreatDir * 15f + Random.insideUnitSphere * 5f; if (!agent.pathPending && agent.remainingDistance < 1f) { NavMeshHit hit; if (NavMesh.SamplePosition(targetPos, out hit, 10f, 1)) { agent.SetDestination(hit.position); } } if (chatBubble != null && Random.value < 0.005f) chatBubble.Show("I NEED TO RECOVER!", 1.5f); // Khi máu đã hồi phục trên 50%, dừng chạy và quay lại tấn công if (health.currentHealth >= health.maxHealth * 0.5f) { isPanicking = false; return NodeState.Success; } 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; Vector3 bodyDir = (targetPos - transform.position); bodyDir.y = 0f; if (bodyDir != Vector3.zero) transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(bodyDir), rotateSpeed * Time.deltaTime); 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 * personalStrafeIntensity; float dist = Vector3.Distance(transform.position, targetPos); if (dist > personalMinCombatDistance) moveDir += normal * personalApproachWeight; } 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) { int burstCount = Random.Range(1, (int)personalBurstMax + (isEnraged ? 2 : 0)); StartCoroutine(ShootBurstRoutine(burstCount)); 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.3f : dodgeForce), ForceMode.Impulse); yield return new WaitForSeconds(dodgeDuration); rb.linearVelocity = Vector3.zero; rb.isKinematic = true; agent.enabled = true; nextDodgeTime = Time.time + (isEnraged ? dodgeCooldown * 0.6f : dodgeCooldown); isDodging = false; } public void ProcessDialogueResult(string json) { try { DialogueResult result = JsonUtility.FromJson(json); if (chatBubble != null) chatBubble.Show(result.text); // Áp dụng modifiers từ Gemini moveSpeed = Mathf.Clamp(moveSpeed + result.speedMod, 1f, 8f); suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100); // Modifier hung hãn: giảm delay bắn (max 50%) minShootDelay = Mathf.Clamp(minShootDelay * (1f - result.aggressionMod), 0.5f, 5f); maxShootDelay = Mathf.Clamp(maxShootDelay * (1f - result.aggressionMod), 1f, 10f); // Modifier can đảm: thay đổi ngưỡng panic panicHealthThreshold = Mathf.Clamp(panicHealthThreshold - result.braveryMod, 0, 80); // Modifier máu if (result.healthMod != 0) { health.AddHealth((int)result.healthMod); } lastTalkTime = Time.time; Debug.Log($"[AI {npcName}] Gemini influence: Speed:{result.speedMod}, Aggro:{result.aggressionMod}, Bravery:{result.braveryMod}"); } 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); } }