Files
BABA_YAGA/Assets/Scripts/AI NPC/EnemyAI.cs

326 lines
10 KiB
C#
Raw Normal View History

2026-06-05 14:10:16 +07:00
using System.Collections;
2026-05-30 17:41:31 +07:00
using System.Collections.Generic;
using UnityEngine;
2026-06-05 17:16:11 +07:00
using UnityEngine.AI;
2026-06-05 18:46:19 +07:00
using System.Linq;
2026-05-30 17:41:31 +07:00
2026-06-05 18:46:19 +07:00
// 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
2026-06-05 14:10:16 +07:00
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Rigidbody))]
2026-05-30 17:41:31 +07:00
public class EnemyAI : MonoBehaviour
{
2026-06-05 18:46:19 +07:00
[Header("References")]
2026-05-30 17:41:31 +07:00
public Transform player;
2026-06-05 15:59:33 +07:00
private NavMeshAgent agent;
private Rigidbody rb;
private FieldOfView fov;
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
[Header("Movement Settings")]
2026-06-05 15:59:33 +07:00
public float moveSpeed = 3f;
2026-06-05 17:16:11 +07:00
public float rotateSpeed = 10f;
2026-06-05 18:46:19 +07:00
[Header("Patrol Settings")]
2026-06-05 17:16:11 +07:00
public Transform[] patrolWaypoints;
public int currentWaypointIndex = 0;
public float patrolWaitTime = 2f;
private float currentWaitTime = 0f;
2026-06-05 18:46:19 +07:00
[Header("Combat State")]
2026-06-03 13:42:09 +07:00
public bool playerHasArtifact;
public GameObject laserPrefab;
public Transform firePoint;
public float minShootDelay = 1f;
public float maxShootDelay = 3f;
2026-06-05 15:59:33 +07:00
private float nextShootTime;
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
[Header("Dodge Settings")]
2026-06-05 16:06:59 +07:00
public float dodgeForce = 10f;
public float dodgeDuration = 0.2f;
public float dodgeCooldown = 1.2f;
2026-06-05 14:10:16 +07:00
private bool isDodging = false;
2026-06-05 17:16:11 +07:00
private float nextDodgeTime = 0f;
2026-06-05 18:46:19 +07:00
[Header("Conversation Settings")]
2026-06-05 17:16:11 +07:00
public string npcName = "Guard";
2026-06-05 18:46:19 +07:00
[TextArea] public string persona = "You are a grumpy guard protecting gold.";
public float talkRange = 10f;
public float talkCooldown = 15f;
2026-06-05 17:16:11 +07:00
private float lastTalkTime;
2026-06-05 18:46:19 +07:00
public bool isTalking; // Public để debug
2026-06-05 17:16:11 +07:00
private EnemyAI talkingPartner;
private Hallucinate.UI.ChatBubble chatBubble;
public Node rootNode;
void Start()
2026-05-30 17:41:31 +07:00
{
2026-06-04 15:41:01 +07:00
agent = GetComponent<NavMeshAgent>();
2026-06-05 14:10:16 +07:00
rb = GetComponent<Rigidbody>();
2026-06-05 15:59:33 +07:00
fov = GetComponent<FieldOfView>();
2026-06-05 18:46:19 +07:00
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
2026-06-04 23:01:39 +07:00
2026-06-05 18:46:19 +07:00
// Rigidbody setup cho Unity 6
2026-06-05 17:16:11 +07:00
rb.isKinematic = true;
2026-06-05 15:59:33 +07:00
rb.freezeRotation = true;
2026-06-05 17:16:11 +07:00
if (player == null)
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null) player = playerObj.transform;
}
2026-06-05 18:46:19 +07:00
/*
// Tạm thời comment đoạn này để tránh lỗi Tag chưa định nghĩa
if (patrolWaypoints == null || patrolWaypoints.Length == 0)
{
patrolWaypoints = GameObject.FindGameObjectsWithTag("PatrolPoint")
.Select(go => go.transform).ToArray();
}
*/
2026-06-05 17:16:11 +07:00
InitTree();
2026-06-05 18:46:19 +07:00
Debug.Log($"<color=white>[AI {npcName}] Init complete. Waypoints: {patrolWaypoints.Length}</color>");
2026-06-05 14:10:16 +07:00
}
2026-06-05 17:16:11 +07:00
void InitTree()
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
var dodgeSequence = new Sequence(new List<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckHasArtifact), 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) });
2026-06-05 17:16:11 +07:00
var patrolAction = new TaskNode(ActionPatrol);
2026-06-03 13:42:09 +07:00
2026-06-05 17:16:11 +07:00
rootNode = new Selector(new List<Node>
2026-05-30 17:41:31 +07:00
{
2026-06-05 17:16:11 +07:00
dodgeSequence,
2026-06-03 13:42:09 +07:00
laserSequence,
2026-06-05 17:16:11 +07:00
chaseSequence,
investigateSequence,
talkSequence,
patrolAction
2026-05-30 17:41:31 +07:00
});
}
2026-06-05 17:16:11 +07:00
void Update()
{
if (player == null) return;
2026-06-05 18:46:19 +07:00
// An toàn cho NavMeshAgent
if (!agent.isOnNavMesh)
{
Debug.LogWarning($"[AI {npcName}] NPC is NOT on NavMesh!");
return;
}
if (!isTalking && !isDodging && agent.isStopped)
agent.isStopped = false;
2026-06-05 17:16:11 +07:00
rootNode?.Evaluate();
}
2026-06-05 15:59:33 +07:00
2026-06-05 18:46:19 +07:00
#region CONDITIONS
2026-06-05 17:16:11 +07:00
private NodeState CheckDodgeConditions()
2026-06-05 15:59:33 +07:00
{
2026-06-05 17:16:11 +07:00
if (isDodging) return NodeState.Success;
2026-06-05 18:46:19 +07:00
if (fov != null && fov.canSeePlayer && Input.GetMouseButtonDown(0) && Time.time >= nextDodgeTime)
2026-06-05 17:16:11 +07:00
return NodeState.Success;
2026-06-05 15:59:33 +07:00
return NodeState.Failure;
}
2026-06-03 13:42:09 +07:00
private NodeState CheckHasArtifact()
{
2026-06-05 17:16:11 +07:00
if (playerHasArtifact) StopConversation();
2026-06-04 15:41:01 +07:00
return playerHasArtifact ? NodeState.Success : NodeState.Failure;
2026-06-03 13:42:09 +07:00
}
private NodeState CheckCanSeePlayer()
{
2026-06-05 18:46:19 +07:00
bool canSee = fov != null && fov.canSeePlayer;
if (canSee) StopConversation();
return canSee ? NodeState.Success : NodeState.Failure;
2026-06-05 14:10:16 +07:00
}
2026-06-03 13:42:09 +07:00
2026-06-05 15:59:33 +07:00
private NodeState CheckHasInvestigateTarget()
2026-06-05 14:10:16 +07:00
{
2026-06-05 18:46:19 +07:00
return (fov != null && fov.lastKnownPlayerPosition != Vector3.zero) ? NodeState.Success : NodeState.Failure;
2026-06-05 17:16:11 +07:00
}
2026-06-03 13:42:09 +07:00
2026-06-05 17:16:11 +07:00
private NodeState CheckCanTalkToNPC()
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
if (playerHasArtifact || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
2026-06-05 17:16:11 +07:00
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
if (isTalking) return NodeState.Success;
2026-06-05 18:46:19 +07:00
// Quét tìm NPC
2026-06-05 17:16:11 +07:00
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
foreach (var hit in hitColliders)
2026-06-05 14:10:16 +07:00
{
2026-06-05 18:46:19 +07:00
if (hit.gameObject == gameObject) continue;
EnemyAI other = hit.GetComponentInParent<EnemyAI>();
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
2026-06-05 15:59:33 +07:00
{
2026-06-05 18:46:19 +07:00
// Chỉ ID nhỏ hơn gọi để tránh trùng
if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
2026-06-05 17:16:11 +07:00
{
talkingPartner = other;
return NodeState.Success;
}
2026-06-05 15:59:33 +07:00
}
2026-06-04 15:41:01 +07:00
}
2026-06-05 17:16:11 +07:00
return NodeState.Failure;
}
2026-06-05 18:46:19 +07:00
#endregion
#region ACTIONS
private NodeState ActionTalk()
2026-06-05 17:16:11 +07:00
{
if (talkingPartner == null) return NodeState.Failure;
if (!isTalking)
{
isTalking = true;
agent.isStopped = true;
FaceTarget(talkingPartner.transform.position);
2026-06-05 18:46:19 +07:00
Debug.Log($"<color=yellow>[AI {npcName}] Talking to {talkingPartner.npcName}</color>");
string prompt = $"You are {npcName}. Speak 1 short sentence in English to your colleague {talkingPartner.npcName} about the shift.";
Hallucinate.AI.GeminiService.Instance.GetResponse(persona, prompt, (response) => {
if (chatBubble != null) chatBubble.Show(response);
Invoke(nameof(EndConversation), 5f);
});
talkingPartner.OnPartnerTalked(this);
2026-06-05 17:16:11 +07:00
}
2026-05-30 17:41:31 +07:00
return NodeState.Running;
}
2026-06-05 17:16:11 +07:00
public void OnPartnerTalked(EnemyAI partner)
{
isTalking = true;
talkingPartner = partner;
agent.isStopped = true;
FaceTarget(partner.transform.position);
Invoke(nameof(EndConversation), 6f);
}
private void EndConversation()
{
isTalking = false;
lastTalkTime = Time.time;
2026-06-05 18:46:19 +07:00
if (agent != null && agent.isOnNavMesh) agent.isStopped = false;
2026-06-05 17:16:11 +07:00
talkingPartner = null;
}
private void StopConversation()
{
if (!isTalking) return;
CancelInvoke(nameof(EndConversation));
EndConversation();
2026-06-05 18:46:19 +07:00
if (chatBubble != null) chatBubble.Show("Wait, what's that?!", 2f);
2026-06-05 17:16:11 +07:00
}
2026-06-05 18:46:19 +07:00
private NodeState ActionPatrol()
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
if (patrolWaypoints == null || patrolWaypoints.Length == 0) return NodeState.Failure;
2026-06-04 21:26:19 +07:00
2026-06-05 18:46:19 +07:00
agent.isStopped = false;
agent.speed = moveSpeed * 0.5f;
var target = patrolWaypoints[currentWaypointIndex];
agent.SetDestination(target.position);
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
if (Vector3.Distance(transform.position, target.position) < 1.5f)
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
currentWaitTime += Time.deltaTime;
if (currentWaitTime >= patrolWaitTime)
{
currentWaypointIndex = (currentWaypointIndex + 1) % patrolWaypoints.Length;
currentWaitTime = 0f;
}
2026-05-30 17:41:31 +07:00
}
2026-06-03 13:42:09 +07:00
return NodeState.Running;
2026-05-30 17:41:31 +07:00
}
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
private NodeState ActionChasePlayer()
2026-06-03 13:42:09 +07:00
{
2026-06-05 18:46:19 +07:00
agent.isStopped = false;
agent.speed = moveSpeed;
agent.SetDestination(player.position);
return NodeState.Running;
2026-06-05 17:16:11 +07:00
}
private NodeState ActionInvestigate()
{
agent.isStopped = false;
agent.speed = moveSpeed * 0.7f;
agent.SetDestination(fov.lastKnownPlayerPosition);
2026-06-05 18:46:19 +07:00
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
2026-06-05 17:16:11 +07:00
{
2026-06-05 18:46:19 +07:00
fov.lastKnownPlayerPosition = Vector3.zero;
2026-06-05 17:16:11 +07:00
return NodeState.Success;
}
return NodeState.Running;
}
2026-06-05 18:46:19 +07:00
private NodeState ActionFocusAndShoot()
2026-06-05 17:16:11 +07:00
{
2026-06-05 18:46:19 +07:00
agent.isStopped = true;
FaceTarget(player.position);
if (Time.time >= nextShootTime)
2026-06-05 17:16:11 +07:00
{
2026-06-05 18:46:19 +07:00
if (laserPrefab && firePoint) Instantiate(laserPrefab, firePoint.position, firePoint.rotation);
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
2026-06-05 17:16:11 +07:00
}
2026-06-05 18:46:19 +07:00
return NodeState.Running;
}
2026-06-05 17:16:11 +07:00
2026-06-05 18:46:19 +07:00
private NodeState ActionDodge()
{
if (!isDodging) StartCoroutine(DodgeRollRoutine());
return NodeState.Running;
}
2026-06-05 17:16:11 +07:00
2026-06-05 18:46:19 +07:00
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;
}
2026-06-05 17:16:11 +07:00
2026-06-05 18:46:19 +07:00
private void FaceTarget(Vector3 pos)
{
Vector3 dir = (pos - transform.position);
dir.y = 0;
if (dir != Vector3.zero) transform.rotation = Quaternion.LookRotation(dir);
2026-06-03 13:42:09 +07:00
}
#endregion
2026-06-05 18:46:19 +07:00
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);
}
}
}