Files
BABA_YAGA/Assets/Scripts/AI NPC/EnemyAI.cs
2026-06-06 02:03:22 +07:00

603 lines
21 KiB
C#

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<NavMeshAgent>();
rb = GetComponent<Rigidbody>();
fov = GetComponent<FieldOfView>();
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
mainCollider = GetComponent<Collider>();
health = GetComponent<vHealthController>();
// 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<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
var panicSequence = new Sequence(new List<Node> { new TaskNode(CheckPanicConditions), new TaskNode(ActionRetreat) }); // Thay Panic bằng Retreat
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) });
var chaseSequence = new Sequence(new List<Node> { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionChasePlayer) });
var investigateSequence = new Sequence(new List<Node> { new TaskNode(CheckHasInvestigateTarget), new TaskNode(ActionInvestigate) });
var talkSequence = new Sequence(new List<Node> { new TaskNode(CheckCanTalkToNPC), new TaskNode(ActionTalk) });
var patrolAction = new TaskNode(ActionPatrol);
rootNode = new Selector(new List<Node>
{
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($"<color=black>[AI {npcName}] DIED.</color>");
// 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<EnemyAI>();
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<EnemyAI>();
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<EnemyAI>();
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<DialogueResult>(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);
}
}