2026-06-05 21:24:41 +07:00
|
|
|
using System;
|
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-06-05 22:24:16 +07:00
|
|
|
using UnityEngine.InputSystem;
|
2026-06-05 21:24:41 +07:00
|
|
|
using Random = UnityEngine.Random;
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
public class DialogueResult { public string text; public float speedMod; public float suspicionMod; }
|
2026-05-30 17:41:31 +07:00
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact / Sound Aggro) --> Đ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 float patrolWaitTime = 2f;
|
|
|
|
|
private float currentWaitTime = 0f;
|
2026-06-05 22:24:16 +07:00
|
|
|
public float patrolSpeed = 2.5f;
|
2026-06-05 23:18:29 +07:00
|
|
|
public float patrolRadius = 12f;
|
2026-06-05 22:24:16 +07:00
|
|
|
private Vector3 startPosition;
|
2026-06-05 23:18:29 +07:00
|
|
|
|
2026-06-05 18:46:19 +07:00
|
|
|
[Header("Combat State")]
|
2026-06-03 13:42:09 +07:00
|
|
|
public bool playerHasArtifact;
|
2026-06-05 23:18:29 +07:00
|
|
|
public bool isAggroedBySound; // <-- MỚI: Trạng thái bị đánh động bởi âm thanh
|
2026-06-03 13:42:09 +07:00
|
|
|
public GameObject laserPrefab;
|
|
|
|
|
public Transform firePoint;
|
2026-06-05 23:18:29 +07:00
|
|
|
public float minShootDelay = 1.5f;
|
2026-06-05 22:24:16 +07:00
|
|
|
public float maxShootDelay = 3.5f;
|
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 22:24:16 +07:00
|
|
|
[Header("Artifact Combat Upgrades (New)")]
|
|
|
|
|
public float minStrafeDuration = 0.5f;
|
|
|
|
|
public float maxStrafeDuration = 2.2f;
|
|
|
|
|
public float maxSpreadAngle = 6f;
|
|
|
|
|
public float burstInterval = 0.12f;
|
|
|
|
|
|
2026-06-05 22:53:48 +07:00
|
|
|
public float approachWeight = 0.35f;
|
|
|
|
|
public float minCombatDistance = 5.0f;
|
|
|
|
|
|
2026-06-05 22:24:16 +07:00
|
|
|
private float nextStrafeChangeTime;
|
2026-06-05 23:18:29 +07:00
|
|
|
private int strafeDirectionSign = 1;
|
|
|
|
|
private bool isShootingBurst = false;
|
2026-06-05 22:24:16 +07:00
|
|
|
|
2026-06-05 23:25:56 +07:00
|
|
|
public bool IsDodging => isDodging;
|
|
|
|
|
public bool IsShootingBurst => isShootingBurst;
|
|
|
|
|
|
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 21:24:41 +07:00
|
|
|
[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;
|
2026-06-05 17:16:11 +07:00
|
|
|
private float lastTalkTime;
|
2026-06-05 23:18:29 +07:00
|
|
|
public bool isTalking;
|
2026-06-05 17:16:11 +07:00
|
|
|
private EnemyAI talkingPartner;
|
|
|
|
|
private Hallucinate.UI.ChatBubble chatBubble;
|
2026-06-05 21:24:41 +07:00
|
|
|
|
|
|
|
|
[Header("Suspicion Settings")]
|
|
|
|
|
public float suspicionLevel = 0f;
|
|
|
|
|
public float investigationThreshold = 30f;
|
|
|
|
|
public float alertNeighborsThreshold = 70f;
|
|
|
|
|
public float alertRange = 20f;
|
2026-06-05 17:16:11 +07:00
|
|
|
|
|
|
|
|
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 17:16:11 +07:00
|
|
|
rb.isKinematic = true;
|
2026-06-05 15:59:33 +07:00
|
|
|
rb.freezeRotation = true;
|
2026-06-05 23:18:29 +07:00
|
|
|
startPosition = transform.position;
|
2026-06-05 15:59:33 +07:00
|
|
|
|
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
|
|
|
|
2026-06-05 17:16:11 +07:00
|
|
|
InitTree();
|
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) });
|
2026-06-05 23:18:29 +07:00
|
|
|
|
|
|
|
|
// Đổi hàm CheckHasArtifact thành CheckCombatConditions để dùng chung cho cả âm thanh
|
|
|
|
|
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) });
|
|
|
|
|
|
2026-06-05 18:46:19 +07:00
|
|
|
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
|
|
|
|
2026-06-05 21:24:41 +07:00
|
|
|
if (!agent.isOnNavMesh) return;
|
|
|
|
|
|
|
|
|
|
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
|
2026-06-05 18:46:19 +07:00
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
// Bình tĩnh lại và tắt chế độ bắn dồn dập khi mức độ nghi ngờ về 0
|
|
|
|
|
if (suspicionLevel <= 0f)
|
|
|
|
|
{
|
|
|
|
|
isAggroedBySound = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 18:46:19 +07:00
|
|
|
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 23:18:29 +07:00
|
|
|
if (playerHasArtifact || isAggroedBySound) return NodeState.Failure;
|
2026-06-05 22:24:16 +07:00
|
|
|
|
2026-06-05 17:16:11 +07:00
|
|
|
if (isDodging) return NodeState.Success;
|
2026-06-05 22:24:16 +07:00
|
|
|
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed)
|
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
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
// Node này thay thế cho CheckHasArtifact cũ
|
|
|
|
|
private NodeState CheckCombatConditions()
|
2026-06-03 13:42:09 +07:00
|
|
|
{
|
2026-06-05 23:18:29 +07:00
|
|
|
bool shouldCombat = playerHasArtifact || isAggroedBySound;
|
|
|
|
|
if (shouldCombat) StopConversation();
|
|
|
|
|
return shouldCombat ? 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;
|
2026-06-05 21:24:41 +07:00
|
|
|
if (canSee) { StopConversation(); AlertNeighbors(); suspicionLevel = 100; }
|
2026-06-05 18:46:19 +07:00
|
|
|
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 21:24:41 +07:00
|
|
|
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero)
|
|
|
|
|
{
|
|
|
|
|
if (suspicionLevel > investigationThreshold)
|
|
|
|
|
{
|
|
|
|
|
if (Random.value < (suspicionLevel / 100f)) return NodeState.Success;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 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 23:18:29 +07:00
|
|
|
if (playerHasArtifact || isAggroedBySound || (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 21:24:41 +07:00
|
|
|
if (Hallucinate.AI.ConversationManager.Instance == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError($"[AI {npcName}] ConversationManager Instance is NULL!");
|
|
|
|
|
return NodeState.Failure;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure;
|
|
|
|
|
|
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 21:24:41 +07:00
|
|
|
float dist = Vector3.Distance(transform.position, other.transform.position);
|
|
|
|
|
if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
|
2026-06-05 17:16:11 +07:00
|
|
|
{
|
2026-06-05 21:24:41 +07:00
|
|
|
Debug.Log($"<color=green>[AI {npcName}]</color> Found partner: {other.npcName}. Starting conversation.");
|
|
|
|
|
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
|
2026-06-05 17:16:11 +07:00
|
|
|
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
|
|
|
|
|
|
2026-06-05 21:24:41 +07:00
|
|
|
public void HearNoise(Vector3 location, float volume)
|
|
|
|
|
{
|
|
|
|
|
suspicionLevel += volume * 15f;
|
|
|
|
|
if (fov != null) fov.lastKnownPlayerPosition = location;
|
2026-06-05 23:18:29 +07:00
|
|
|
|
|
|
|
|
// CẬP NHẬT: Nếu AI nghe thấy tiếng động làm mức nghi ngờ vượt mốc điều tra, chuyển sang trạng thái chiến đấu (bắn bồi)
|
|
|
|
|
if (suspicionLevel >= investigationThreshold)
|
|
|
|
|
{
|
|
|
|
|
isAggroedBySound = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 21:24:41 +07:00
|
|
|
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors();
|
|
|
|
|
StopConversation();
|
|
|
|
|
Debug.Log($"<color=orange>[AI {npcName}]</color> Heard noise! Suspicion: {suspicionLevel}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void AlertNeighbors()
|
2026-06-05 17:16:11 +07:00
|
|
|
{
|
2026-06-05 21:24:41 +07:00
|
|
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
|
|
|
|
|
foreach (var hit in hitColliders)
|
2026-06-05 17:16:11 +07:00
|
|
|
{
|
2026-06-05 21:24:41 +07:00
|
|
|
EnemyAI neighbor = hit.GetComponentInParent<EnemyAI>();
|
|
|
|
|
if (neighbor != null && neighbor != this)
|
|
|
|
|
{
|
|
|
|
|
neighbor.suspicionLevel = Mathf.Max(neighbor.suspicionLevel, 50f);
|
|
|
|
|
if (fov != null && neighbor.fov != null) neighbor.fov.lastKnownPlayerPosition = fov.lastKnownPlayerPosition;
|
|
|
|
|
}
|
2026-06-05 17:16:11 +07:00
|
|
|
}
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 21:24:41 +07:00
|
|
|
private NodeState ActionTalk()
|
2026-06-05 17:16:11 +07:00
|
|
|
{
|
2026-06-05 21:24:41 +07:00
|
|
|
if (isTalking)
|
|
|
|
|
{
|
|
|
|
|
agent.isStopped = true;
|
|
|
|
|
if (talkingPartner != null)
|
|
|
|
|
{
|
|
|
|
|
if (Vector3.Distance(transform.position, talkingPartner.transform.position) > talkRange + 2f)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log($"[AI {npcName}] Partner moved too far. Ending conversation.");
|
|
|
|
|
StopConversation();
|
|
|
|
|
return NodeState.Failure;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return NodeState.Running;
|
|
|
|
|
}
|
|
|
|
|
return NodeState.Failure;
|
2026-06-05 17:16:11 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 21:24:41 +07:00
|
|
|
public void ProcessDialogueResult(string json)
|
2026-06-05 17:16:11 +07:00
|
|
|
{
|
2026-06-05 21:24:41 +07:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
DialogueResult result = JsonUtility.FromJson<DialogueResult>(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); }
|
2026-06-05 17:16:11 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StopConversation()
|
|
|
|
|
{
|
2026-06-05 21:24:41 +07:00
|
|
|
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
|
|
|
|
|
{
|
|
|
|
|
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
|
|
|
|
|
if (chatBubble != null) chatBubble.Show("Wait, what was 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
|
|
|
agent.isStopped = false;
|
2026-06-05 22:24:16 +07:00
|
|
|
agent.speed = patrolSpeed;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-05 22:24:16 +07:00
|
|
|
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-05 18:46:19 +07:00
|
|
|
currentWaitTime += Time.deltaTime;
|
2026-06-05 22:24:16 +07:00
|
|
|
|
2026-06-05 18:46:19 +07:00
|
|
|
if (currentWaitTime >= patrolWaitTime)
|
|
|
|
|
{
|
2026-06-05 22:24:16 +07:00
|
|
|
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
|
2026-06-05 23:18:29 +07:00
|
|
|
randomDirection += startPosition;
|
2026-06-05 22:24:16 +07:00
|
|
|
|
|
|
|
|
NavMeshHit hit;
|
|
|
|
|
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
|
|
|
|
|
{
|
|
|
|
|
agent.SetDestination(hit.position);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
currentWaitTime = 0f;
|
2026-06-05 18:46:19 +07:00
|
|
|
}
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
2026-06-05 22:24:16 +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 21:24:41 +07:00
|
|
|
|
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 21:24:41 +07:00
|
|
|
currentWaitTime += Time.deltaTime;
|
2026-06-05 22:24:16 +07:00
|
|
|
if (currentWaitTime > 3f)
|
2026-06-05 21:24:41 +07:00
|
|
|
{
|
|
|
|
|
fov.lastKnownPlayerPosition = Vector3.zero;
|
2026-06-05 22:24:16 +07:00
|
|
|
suspicionLevel *= 0.5f;
|
2026-06-05 21:24:41 +07:00
|
|
|
return NodeState.Success;
|
|
|
|
|
}
|
2026-06-05 17:16:11 +07:00
|
|
|
}
|
|
|
|
|
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 21:43:41 +07:00
|
|
|
if (player == null) return NodeState.Failure;
|
|
|
|
|
|
2026-06-05 22:24:16 +07:00
|
|
|
if (agent.hasPath) agent.ResetPath();
|
|
|
|
|
agent.isStopped = false;
|
2026-06-05 21:43:41 +07:00
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
// CẢI TIẾN: Xác định mục tiêu linh hoạt để tránh Wallhack.
|
|
|
|
|
Vector3 targetPos = player.position; // Mặc định khóa chặt người chơi
|
|
|
|
|
|
|
|
|
|
// Nếu không cầm Artifact và cũng chưa bị nhìn thấy, AI chỉ nhắm vào MỐC ÂM THANH
|
|
|
|
|
if (!playerHasArtifact && fov != null && !fov.canSeePlayer && fov.lastKnownPlayerPosition != Vector3.zero)
|
|
|
|
|
{
|
|
|
|
|
targetPos = fov.lastKnownPlayerPosition;
|
|
|
|
|
|
|
|
|
|
// Nếu AI tiến hành áp sát và xả đạn vào nơi phát ra tiếng mà không thấy ai, ngưng bắn
|
|
|
|
|
if (Vector3.Distance(transform.position, targetPos) < 2f)
|
|
|
|
|
{
|
|
|
|
|
isAggroedBySound = false;
|
|
|
|
|
suspicionLevel *= 0.5f;
|
|
|
|
|
return NodeState.Success;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ MỤC TIÊU (Người chơi hoặc Tiếng động)
|
|
|
|
|
Vector3 bodyDir = targetPos - transform.position;
|
2026-06-05 22:24:16 +07:00
|
|
|
bodyDir.y = 0f;
|
|
|
|
|
Vector3 bodyDirNormal = bodyDir.normalized;
|
2026-06-05 21:43:41 +07:00
|
|
|
if (bodyDir != Vector3.zero)
|
|
|
|
|
{
|
2026-06-05 22:24:16 +07:00
|
|
|
Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal);
|
2026-06-05 21:43:41 +07:00
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 22:53:48 +07:00
|
|
|
// 2. RANDOM THỜI GIAN/KHOẢNG CÁCH DUY TRÌ HƯỚNG DI CHUYỂN
|
2026-06-05 22:24:16 +07:00
|
|
|
if (Time.time >= nextStrafeChangeTime)
|
|
|
|
|
{
|
|
|
|
|
int[] choices = { -1, 1, 0 }; // Trái, Phải, Đứng yên bắn
|
|
|
|
|
strafeDirectionSign = choices[Random.Range(0, choices.Length)];
|
|
|
|
|
nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 22:53:48 +07:00
|
|
|
// 3. TÍNH TOÁN TRỘN VECTOR: CHỈ TIẾN LÊN KHI ĐANG ĐI NGANG TRÁI/PHẢI
|
|
|
|
|
Vector3 finalMovementVector = Vector3.zero;
|
|
|
|
|
|
2026-06-05 22:24:16 +07:00
|
|
|
if (strafeDirectionSign != 0 && bodyDir != Vector3.zero)
|
|
|
|
|
{
|
2026-06-05 22:53:48 +07:00
|
|
|
finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign;
|
|
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
float currentDistance = Vector3.Distance(transform.position, targetPos);
|
2026-06-05 22:53:48 +07:00
|
|
|
if (currentDistance > minCombatDistance)
|
|
|
|
|
{
|
|
|
|
|
finalMovementVector += bodyDirNormal * approachWeight;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (finalMovementVector != Vector3.zero)
|
|
|
|
|
{
|
2026-06-05 23:18:29 +07:00
|
|
|
finalMovementVector.Normalize();
|
2026-06-05 22:24:16 +07:00
|
|
|
agent.speed = moveSpeed * 0.75f;
|
2026-06-05 22:53:48 +07:00
|
|
|
agent.Move(finalMovementVector * agent.speed * Time.deltaTime);
|
2026-06-05 22:24:16 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
// 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO MỤC TIÊU
|
2026-06-05 21:43:41 +07:00
|
|
|
if (firePoint != null)
|
|
|
|
|
{
|
2026-06-05 23:18:29 +07:00
|
|
|
Vector3 targetCenter = targetPos + Vector3.up * 1f;
|
2026-06-05 21:43:41 +07:00
|
|
|
Vector3 aimDir = targetCenter - firePoint.position;
|
|
|
|
|
if (aimDir != Vector3.zero)
|
|
|
|
|
{
|
|
|
|
|
firePoint.rotation = Quaternion.LookRotation(aimDir);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 22:53:48 +07:00
|
|
|
// 5. RANDOM SỐ ĐẠN & DELAY GIỮA CÁC ĐỢT BẮN
|
2026-06-05 22:24:16 +07:00
|
|
|
if (Time.time >= nextShootTime && !isShootingBurst)
|
2026-06-05 17:16:11 +07:00
|
|
|
{
|
2026-06-05 22:53:48 +07:00
|
|
|
int randomBulletCount = Random.Range(1, 4);
|
2026-06-05 22:24:16 +07:00
|
|
|
StartCoroutine(ShootBurstRoutine(randomBulletCount));
|
2026-06-05 18:46:19 +07:00
|
|
|
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
2026-06-05 17:16:11 +07:00
|
|
|
}
|
2026-06-05 21:43:41 +07:00
|
|
|
|
2026-06-05 18:46:19 +07:00
|
|
|
return NodeState.Running;
|
|
|
|
|
}
|
2026-06-05 22:24:16 +07:00
|
|
|
|
|
|
|
|
private IEnumerator ShootBurstRoutine(int bulletCount)
|
2026-06-05 21:43:41 +07:00
|
|
|
{
|
2026-06-05 22:24:16 +07:00
|
|
|
isShootingBurst = true;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < bulletCount; i++)
|
|
|
|
|
{
|
|
|
|
|
if (laserPrefab == null || firePoint == null) break;
|
|
|
|
|
|
|
|
|
|
float randomX = Random.Range(-maxSpreadAngle, maxSpreadAngle);
|
|
|
|
|
float randomY = Random.Range(-maxSpreadAngle, maxSpreadAngle);
|
|
|
|
|
Quaternion spreadRotation = Quaternion.Euler(randomX, randomY, 0f);
|
|
|
|
|
|
|
|
|
|
Quaternion finalBulletRotation = firePoint.rotation * spreadRotation;
|
|
|
|
|
|
|
|
|
|
Instantiate(laserPrefab, firePoint.position, finalBulletRotation);
|
|
|
|
|
Debug.Log($"<color=cyan>[AI Burst]</color> Viên thứ {i + 1}/{bulletCount} | Độ lệch X:{randomX:F1}, Y:{randomY:F1}");
|
|
|
|
|
|
|
|
|
|
if (i < bulletCount - 1)
|
|
|
|
|
{
|
|
|
|
|
yield return new WaitForSeconds(burstInterval);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isShootingBurst = false;
|
2026-06-05 21:43:41 +07:00
|
|
|
}
|
2026-06-05 22:24:16 +07:00
|
|
|
|
2026-06-05 23:18:29 +07:00
|
|
|
private void ShootLaser() { }
|
2026-06-05 22:24:16 +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 22:24:16 +07:00
|
|
|
public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; }
|
2026-06-05 21:24:41 +07:00
|
|
|
public void FaceTarget(Vector3 pos)
|
2026-06-05 18:46:19 +07:00
|
|
|
{
|
|
|
|
|
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()
|
|
|
|
|
{
|
|
|
|
|
Gizmos.color = Color.green;
|
|
|
|
|
Gizmos.DrawWireSphere(transform.position, talkRange);
|
|
|
|
|
|
|
|
|
|
if (isTalking && talkingPartner != null)
|
|
|
|
|
{
|
|
|
|
|
Gizmos.color = Color.yellow;
|
|
|
|
|
Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-05 22:24:16 +07:00
|
|
|
}
|