diff --git a/Assets/Scripts/AI NPC/AnimatorAI.cs b/Assets/Scripts/AI NPC/AnimatorAI.cs
index 12657a1c..c7dc4663 100644
--- a/Assets/Scripts/AI NPC/AnimatorAI.cs
+++ b/Assets/Scripts/AI NPC/AnimatorAI.cs
@@ -1,307 +1,199 @@
using UnityEngine;
using UnityEngine.AI;
using Invector;
+using Invector.vEventSystems;
using System.Collections;
///
-/// AnimatorAI: Đồng bộ hóa trạng thái của EnemyAI với Animator.
-/// Tích hợp Simulation Mode để giả lập animation khi chưa có logic di chuyển hoàn chỉnh.
+/// AnimatorAI: Advanced Invector-style Animator synchronization for EnemyAI.
+/// Resolves T-pose by ensuring all core Invector parameters are correctly synced.
///
-public class AnimatorAI : MonoBehaviour
+public class AnimatorAI : MonoBehaviour, vIAnimatorStateInfoController
{
protected Animator animator;
protected EnemyAI enemyAI;
protected NavMeshAgent agent;
protected Rigidbody rb;
-
- [Header("Debug Settings")]
- public bool debugMode = true;
- public Color debugColor = Color.cyan;
-
- [Header("Simulation Mode (Giả lập)")]
- public bool useSimulation = false; // Tích chọn để dùng thông số giả lập bên dưới
- public bool autoCycleSpeed = false; // Tự động chạy/đi bộ/đứng im theo vòng lặp
- [Range(0, 1)] public float simVerticalVelocity = 0f;
- public bool simIsSprinting = false;
- public bool simIsAiming = false;
- public int simMoveSetID = 0;
+ protected vHealthController healthController;
[Header("Movement Settings")]
public float sprintThreshold = 0.8f;
public float dampTime = 0.1f;
+ public float groundDistanceValue = 0.05f;
+
+ public vAnimatorStateInfos animatorStateInfos { get; protected set; }
#region Animator Parameters (Invector Style)
protected vAnimatorParameter isDead;
protected vAnimatorParameter isGrounded;
- protected vAnimatorParameter isCrouching;
protected vAnimatorParameter isStrafing;
- protected vAnimatorParameter isSliding;
protected vAnimatorParameter isSprinting;
protected vAnimatorParameter isAiming;
- protected vAnimatorParameter canAim;
- protected vAnimatorParameter flipAnimation;
- protected vAnimatorParameter flipEquip;
- protected vAnimatorParameter groundDistance;
- protected vAnimatorParameter groundAngle;
protected vAnimatorParameter verticalVelocity;
+ protected vAnimatorParameter horizontalVelocity;
+ protected vAnimatorParameter groundDistance;
protected vAnimatorParameter moveSet_ID;
- protected vAnimatorParameter upperBody_ID;
- protected vAnimatorParameter idleRandom;
- protected vAnimatorParameter idleRandomTrigger;
- protected vAnimatorParameter randomAttack;
- protected vAnimatorParameter weakAttack;
- protected vAnimatorParameter strongAttack;
- protected vAnimatorParameter isBlocking;
protected vAnimatorParameter attackID;
- protected vAnimatorParameter defenseID;
- protected vAnimatorParameter recoilID;
- protected vAnimatorParameter reactionID;
- protected vAnimatorParameter triggerRecoil;
- protected vAnimatorParameter triggerReaction;
protected vAnimatorParameter hitDirection;
+ protected vAnimatorParameter reactionID;
+ protected vAnimatorParameter triggerReaction;
protected vAnimatorParameter resetState;
- protected vAnimatorParameter reload;
- protected vAnimatorParameter cancelReload;
- protected vAnimatorParameter reloadID;
- protected vAnimatorParameter shoot;
- protected vAnimatorParameter shot_ID;
- protected vAnimatorParameter powerCharger;
#endregion
- [Header("Nuclear Diagnostic (Siêu chẩn đoán)")]
- public string forcePlayState = ""; // Gõ tên State vào đây để ép nó chạy (vd: Idle)
- public bool showLayerWeights = false;
+ protected Vector3 lastPosition;
+ protected float currentV;
+ protected float currentH;
- protected virtual void Start()
+ protected virtual void Awake()
{
animator = GetComponentInChildren();
enemyAI = GetComponent();
agent = GetComponent();
rb = GetComponent();
+ healthController = GetComponent();
- if (animator == null)
+ if (animator)
{
- Debug.LogError($"[AnimatorAI] KHÔNG tìm thấy Animator trên {gameObject.name} hoặc các con của nó!");
- return;
+ animatorStateInfos = new vAnimatorStateInfos(animator);
+ InitializeParameters();
}
- // Chạy chẩn đoán hạt nhân
- StartCoroutine(NuclearDiagnosticRoutine());
-
- InitializeParameters();
+ lastPosition = transform.position;
}
- private IEnumerator NuclearDiagnosticRoutine()
+ protected virtual void OnEnable()
{
- while (true)
- {
- yield return new WaitForSeconds(1f);
+ this.Register();
+ if (healthController) healthController.onReceiveDamage.AddListener(OnReceiveDamage);
+ }
- if (animator == null) yield break;
-
- // 1. Kiểm tra Enabled
- if (!animator.enabled)
- Debug.LogError($"[NUCLEAR ALERT] Component Animator đang bị TẮT (enabled = false)!");
-
- // 2. Kiểm tra Rig Type
- if (animator.isHuman)
- Debug.Log($"[Rig Info] Rig là Humanoid. Hãy chắc chắn các Animation cũng là Humanoid.");
- else
- Debug.LogWarning($"[Rig Info] Rig là Generic/Legacy. Nếu bạn dùng Animation Humanoid nó sẽ không chạy.");
-
- // 3. Kiểm tra Layer Weight
- if (showLayerWeights)
- {
- for (int i = 0; i < animator.layerCount; i++)
- {
- Debug.Log($"Layer {i} ({animator.GetLayerName(i)}): Weight = {animator.GetLayerWeight(i)}");
- }
- }
-
- // 4. Ép chạy State nếu có yêu cầu
- if (!string.IsNullOrEmpty(forcePlayState))
- {
- Debug.Log($"[Nuclear Force] ÉP CHẠY STATE: {forcePlayState}");
- animator.Play(forcePlayState);
- forcePlayState = ""; // Reset sau khi chạy
- }
-
- // 5. Kiểm tra vị trí xương (Nếu T-pose thì thường xương không đổi vị trí)
- Vector3 handPos = animator.GetBoneTransform(HumanBodyBones.RightHand) ? animator.GetBoneTransform(HumanBodyBones.RightHand).position : Vector3.zero;
- yield return new WaitForSeconds(0.1f);
- if (handPos != Vector3.zero && Vector3.Distance(handPos, animator.GetBoneTransform(HumanBodyBones.RightHand).position) < 0.001f && animator.speed > 0)
- {
- // Nếu xương không nhúc nhích dù tốc độ > 0
- Debug.LogWarning($"[NUCLEAR ALERT] CẢNH BÁO: Xương nhân vật không nhúc nhích! Có thể do Rig lỗi hoặc bị script khác khóa xương.");
- }
-
- if (!useSimulation) yield break;
- }
+ protected virtual void OnDisable()
+ {
+ this.UnRegister();
+ if (healthController) healthController.onReceiveDamage.RemoveListener(OnReceiveDamage);
}
protected virtual void InitializeParameters()
{
- if (animator == null) return;
-
- // Khởi tạo và kiểm tra từng tham số quan trọng
- isDead = ValidateAndInit("isDead");
- isGrounded = ValidateAndInit("IsGrounded");
- isCrouching = ValidateAndInit("IsCrouching");
- isStrafing = ValidateAndInit("IsStrafing");
- isSliding = ValidateAndInit("IsSliding");
- isSprinting = ValidateAndInit("IsSprinting");
- isAiming = ValidateAndInit("IsAiming");
- canAim = ValidateAndInit("CanAim");
- flipAnimation = ValidateAndInit("FlipAnimation");
- flipEquip = ValidateAndInit("FlipEquip");
- groundDistance = ValidateAndInit("GroundDistance");
- groundAngle = ValidateAndInit("GroundAngle");
- verticalVelocity = ValidateAndInit("VerticalVelocity");
- moveSet_ID = ValidateAndInit("MoveSet_ID");
- upperBody_ID = ValidateAndInit("UpperBody_ID");
- idleRandom = ValidateAndInit("IdleRandom");
- idleRandomTrigger = ValidateAndInit("IdleRandomTrigger");
- randomAttack = ValidateAndInit("RandomAttack");
- weakAttack = ValidateAndInit("WeakAttack");
- strongAttack = ValidateAndInit("StrongAttack");
- isBlocking = ValidateAndInit("IsBlocking");
- attackID = ValidateAndInit("AttackID");
- defenseID = ValidateAndInit("DefenseID");
- recoilID = ValidateAndInit("RecoilID");
- reactionID = ValidateAndInit("ReactionID");
- triggerRecoil = ValidateAndInit("TriggerRecoil");
- triggerReaction = ValidateAndInit("TriggerReaction");
- hitDirection = ValidateAndInit("HitDirection");
- resetState = ValidateAndInit("ResetState");
- reload = ValidateAndInit("Reload");
- cancelReload = ValidateAndInit("CancelReload");
- reloadID = ValidateAndInit("ReloadID");
- shoot = ValidateAndInit("Shoot");
- shot_ID = ValidateAndInit("Shot_ID");
- powerCharger = ValidateAndInit("PowerCharger");
- }
-
- private vAnimatorParameter ValidateAndInit(string paramName)
- {
- vAnimatorParameter p = new vAnimatorParameter(animator, paramName);
- if (!p.isValid && debugMode)
- {
- // Chỉ cảnh báo những biến cốt lõi nếu thiếu
- if (paramName == "VerticalVelocity" || paramName == "IsGrounded" || paramName == "IsAiming")
- Debug.LogWarning($"[AnimatorAI] Cảnh báo: Controller thiếu biến quan trọng: {paramName}");
- }
- return p;
+ isDead = new vAnimatorParameter(animator, "isDead");
+ isGrounded = new vAnimatorParameter(animator, "IsGrounded");
+ isStrafing = new vAnimatorParameter(animator, "IsStrafing");
+ isSprinting = new vAnimatorParameter(animator, "IsSprinting");
+ isAiming = new vAnimatorParameter(animator, "IsAiming");
+ verticalVelocity = new vAnimatorParameter(animator, "VerticalVelocity");
+ horizontalVelocity = new vAnimatorParameter(animator, "HorizontalVelocity");
+ groundDistance = new vAnimatorParameter(animator, "GroundDistance");
+ moveSet_ID = new vAnimatorParameter(animator, "MoveSet_ID");
+ attackID = new vAnimatorParameter(animator, "AttackID");
+ hitDirection = new vAnimatorParameter(animator, "HitDirection");
+ reactionID = new vAnimatorParameter(animator, "ReactionID");
+ triggerReaction = new vAnimatorParameter(animator, "TriggerReaction");
+ resetState = new vAnimatorParameter(animator, "ResetState");
}
protected virtual void Update()
{
- if (animator == null) return;
+ if (animator == null || enemyAI == null || agent == null) return;
- if (useSimulation)
- {
- RunSimulation();
- }
- else
- {
- if (enemyAI == null || agent == null) return;
- UpdateMovementParameters();
- UpdateCombatParameters();
- }
- }
-
- protected virtual void RunSimulation()
- {
- // 1. Giả lập tốc độ di chuyển
- if (autoCycleSpeed)
- {
- // Tạo vòng lặp tốc độ từ 0 đến 1 dùng hàm Sin
- simVerticalVelocity = Mathf.Abs(Mathf.Sin(Time.time * 0.5f));
- simIsSprinting = simVerticalVelocity > sprintThreshold;
- }
-
- SetFloat(verticalVelocity, simVerticalVelocity, "SIM: VerticalVelocity");
- SetBool(isSprinting, simIsSprinting, "SIM: IsSprinting");
- SetBool(isGrounded, true, "SIM: IsGrounded"); // Luôn giả lập trên mặt đất
-
- // 2. Giả lập chiến đấu
- SetBool(isAiming, simIsAiming, "SIM: IsAiming");
- SetInt(moveSet_ID, simMoveSetID, "SIM: MoveSet_ID");
- SetBool(canAim, simIsAiming, "SIM: CanAim");
+ UpdateMovementParameters();
+ UpdateCombatParameters();
}
protected virtual void UpdateMovementParameters()
{
+ // 1. Grounded & GroundDistance (Critical for T-pose prevention)
bool grounded = agent.isOnNavMesh || agent.enabled;
- SetBool(isGrounded, grounded, "IsGrounded");
+ SetBool(isGrounded, grounded);
+ SetFloat(groundDistance, grounded ? 0f : groundDistanceValue);
- float speed = agent.velocity.magnitude / enemyAI.moveSpeed;
- SetFloat(verticalVelocity, speed, "VerticalVelocity");
+ // 2. Responsive Velocity Calculation
+ // Use a mix of agent velocity and position delta for better responsiveness
+ Vector3 worldVelocity = (transform.position - lastPosition) / Time.deltaTime;
+ lastPosition = transform.position;
- bool sprinting = agent.velocity.magnitude > (enemyAI.moveSpeed * sprintThreshold);
- SetBool(isSprinting, sprinting, "IsSprinting");
+ if (worldVelocity.magnitude < 0.01f) worldVelocity = Vector3.zero;
- bool isDodging = !agent.enabled && !rb.isKinematic;
- SetBool(flipAnimation, isDodging, "FlipAnimation (Dodge)");
+ Vector3 localVelocity = transform.InverseTransformDirection(worldVelocity);
+
+ float targetV = localVelocity.z / enemyAI.moveSpeed;
+ float targetH = localVelocity.x / enemyAI.moveSpeed;
+
+ // Smooth velocity values
+ currentV = Mathf.Lerp(currentV, targetV, 10f * Time.deltaTime);
+ currentH = Mathf.Lerp(currentH, targetH, 10f * Time.deltaTime);
+
+ SetFloat(verticalVelocity, currentV);
+ SetFloat(horizontalVelocity, currentH);
+
+ // 3. Sprinting
+ bool sprinting = worldVelocity.magnitude > (enemyAI.moveSpeed * sprintThreshold);
+ SetBool(isSprinting, sprinting);
+
+ // 4. Strafing
+ SetBool(isStrafing, enemyAI.playerHasArtifact);
}
protected virtual void UpdateCombatParameters()
{
- bool aiming = enemyAI.playerHasArtifact && agent.isStopped;
- SetBool(isAiming, aiming, "IsAiming");
+ SetBool(isAiming, enemyAI.playerHasArtifact);
+ SetInt(moveSet_ID, enemyAI.playerHasArtifact ? 1 : 0);
- int moveID = enemyAI.playerHasArtifact ? 1 : 0;
- SetInt(moveSet_ID, moveID, "MoveSet_ID");
+ // Shooting burst
+ if (enemyAI.IsShootingBurst)
+ SetInt(attackID, 1);
+ else
+ SetInt(attackID, 0);
- SetBool(canAim, enemyAI.playerHasArtifact, "CanAim");
- }
-
- #region Optimized Setters with Debug
-
- protected void SetBool(vAnimatorParameter param, bool value, string name)
- {
- if (param.isValid)
+ // Dodge logic
+ if (enemyAI.IsDodging)
{
- bool current = animator.GetBool(param);
- if (current != value)
- {
- animator.SetBool(param, value);
- if (debugMode) Debug.Log($"[AnimDebug] {gameObject.name}: {name} -> {value}");
- }
+ // In Invector, dodges are often handled via triggers or specific IDs
+ SetAnimatorTrigger(triggerReaction);
}
+
+ // Death state
+ if (healthController) SetBool(isDead, healthController.isDead);
}
- protected void SetFloat(vAnimatorParameter param, float value, string name)
+ protected virtual void OnReceiveDamage(vDamage damage)
+ {
+ if (animator == null || !animator.enabled) return;
+
+ // Sync damage parameters for hit reactions
+ if (hitDirection.isValid && damage.sender)
+ {
+ float angle = transform.HitAngle(damage.sender.position);
+ animator.SetInteger(hitDirection, (int)angle);
+ }
+
+ if (reactionID.isValid) animator.SetInteger(reactionID, damage.reaction_id);
+
+ SetAnimatorTrigger(triggerReaction);
+ SetAnimatorTrigger(resetState);
+ }
+
+ #region Helpers
+ protected void SetBool(vAnimatorParameter param, bool value)
+ {
+ if (param.isValid && animator.GetBool(param) != value)
+ animator.SetBool(param, value);
+ }
+
+ protected void SetFloat(vAnimatorParameter param, float value)
{
if (param.isValid)
- {
animator.SetFloat(param, value, dampTime, Time.deltaTime);
- }
}
- protected void SetInt(vAnimatorParameter param, int value, string name)
+ protected void SetInt(vAnimatorParameter param, int value)
{
- if (param.isValid)
- {
- int current = animator.GetInteger(param);
- if (current != value)
- {
- animator.SetInteger(param, value);
- if (debugMode) Debug.Log($"[AnimDebug] {gameObject.name}: {name} -> {value}");
- }
- }
+ if (param.isValid && animator.GetInteger(param) != value)
+ animator.SetInteger(param, value);
}
- #endregion
-
- #region Helper Methods (Triggers)
-
- public virtual void SetAnimatorTrigger(vAnimatorParameter trigger, string name = "Trigger")
+ public void SetAnimatorTrigger(vAnimatorParameter trigger)
{
- if (trigger.isValid)
- {
- if (debugMode) Debug.Log($"[AnimDebug] {gameObject.name}: Kích hoạt {name}");
- StartCoroutine(SetTriggerRoutine(trigger));
- }
+ if (trigger.isValid) StartCoroutine(SetTriggerRoutine(trigger));
}
private IEnumerator SetTriggerRoutine(int targetHash)
@@ -310,6 +202,5 @@ public class AnimatorAI : MonoBehaviour
yield return new WaitForSeconds(0.1f);
animator.ResetTrigger(targetHash);
}
-
#endregion
}
diff --git a/Assets/Scripts/AI NPC/AutoDestroy.cs b/Assets/Scripts/AI NPC/AutoDestroy.cs
index fa9d1645..643b35d5 100644
--- a/Assets/Scripts/AI NPC/AutoDestroy.cs
+++ b/Assets/Scripts/AI NPC/AutoDestroy.cs
@@ -1,16 +1,47 @@
+using Invector;
using UnityEngine;
public class AutoDestroy : MonoBehaviour
{
- // Start is called once before the first execution of Update after the MonoBehaviour is created
+ public int damageAmount = 30;
void Start()
{
-
+ Destroy(gameObject,2f);
}
// Update is called once per frame
- void Update()
+ private void OnTriggerEnter(Collider other)
{
+ // Debug: Log tên và tag của bất cứ thứ gì đạn chạm vào
+ Debug.Log(
+ $"Laser collided with: {other.name} | Tag: {other.tag} | Layer: {LayerMask.LayerToName(other.gameObject.layer)}");
+
+ // Kiểm tra nếu trúng Player
+ if (other.CompareTag("Player") || other.GetComponentInParent() != null)
+ {
+ var healthController = other.GetComponentInParent();
+
+ if (healthController != null)
+ {
+ Debug.Log(
+ $"HIT PLAYER! Found health controller on {healthController.gameObject.name}. Applying {damageAmount} damage.");
+ var damage = new vDamage(damageAmount);
+ damage.sender = transform;
+ damage.hitPosition = transform.position;
+ healthController.TakeDamage(damage);
+ }
+
+ // Luôn phá hủy đạn khi trúng Player
+ Impact();
+
+ }
+ }
+
+ private void Impact()
+ {
+
+ // Phá hủy đạn ngay lập tức
+ Destroy(gameObject);
}
}
diff --git a/Assets/Scripts/AI NPC/EnemyAI.cs b/Assets/Scripts/AI NPC/EnemyAI.cs
index ffe02250..194e95ce 100644
--- a/Assets/Scripts/AI NPC/EnemyAI.cs
+++ b/Assets/Scripts/AI NPC/EnemyAI.cs
@@ -10,7 +10,7 @@ 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ắn hạ (Artifact) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần
+// 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
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Rigidbody))]
public class EnemyAI : MonoBehaviour
@@ -26,20 +26,18 @@ public class EnemyAI : MonoBehaviour
public float rotateSpeed = 10f;
[Header("Patrol Settings")]
-
public float patrolWaitTime = 2f;
private float currentWaitTime = 0f;
public float patrolSpeed = 2.5f;
-
- public float patrolRadius = 12f; // Bán kính của khu vực tuần tra ngẫu nhiên
-
-
+ public float patrolRadius = 12f;
private Vector3 startPosition;
+
[Header("Combat State")]
public bool playerHasArtifact;
+ public bool isAggroedBySound; // <-- MỚI: Trạng thái bị đánh động bởi âm thanh
public GameObject laserPrefab;
public Transform firePoint;
- public float minShootDelay = 1.5f; // Delay giữa các LOẠT BẮN
+ public float minShootDelay = 1.5f;
public float maxShootDelay = 3.5f;
private float nextShootTime;
@@ -59,11 +57,12 @@ public class EnemyAI : MonoBehaviour
public float approachWeight = 0.35f;
public float minCombatDistance = 5.0f;
-
-
private float nextStrafeChangeTime;
- private int strafeDirectionSign = 1; // -1: Trái, 1: Phải, 0: Đứng im bắn
- private bool isShootingBurst = false; // Khóa chống trùng lặp loạt bắn
+ private int strafeDirectionSign = 1;
+ private bool isShootingBurst = false;
+
+ public bool IsDodging => isDodging;
+ public bool IsShootingBurst => isShootingBurst;
[Header("Conversation Settings")]
public string npcName = "Guard";
@@ -71,7 +70,7 @@ public class EnemyAI : MonoBehaviour
public float talkRange = 12f;
public float talkCooldown = 60f;
private float lastTalkTime;
- public bool isTalking; // Public để debug
+ public bool isTalking;
private EnemyAI talkingPartner;
private Hallucinate.UI.ChatBubble chatBubble;
@@ -92,6 +91,7 @@ public class EnemyAI : MonoBehaviour
rb.isKinematic = true;
rb.freezeRotation = true;
+ startPosition = transform.position;
if (player == null)
{
@@ -105,7 +105,10 @@ public class EnemyAI : MonoBehaviour
void InitTree()
{
var dodgeSequence = new Sequence(new List { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
- var laserSequence = new Sequence(new List { new TaskNode(CheckHasArtifact), new TaskNode(ActionFocusAndShoot) });
+
+ // Đổi hàm CheckHasArtifact thành CheckCombatConditions để dùng chung cho cả âm thanh
+ 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) });
@@ -128,9 +131,14 @@ public class EnemyAI : MonoBehaviour
if (!agent.isOnNavMesh) return;
- // Decay suspicion
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
+ // 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;
+ }
+
if (!isTalking && !isDodging && agent.isStopped)
agent.isStopped = false;
@@ -141,7 +149,7 @@ public class EnemyAI : MonoBehaviour
private NodeState CheckDodgeConditions()
{
- if (playerHasArtifact) return NodeState.Failure; // Có cổ vật -> Không Dash né nữa
+ if (playerHasArtifact || isAggroedBySound) return NodeState.Failure;
if (isDodging) return NodeState.Success;
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed)
@@ -149,10 +157,12 @@ public class EnemyAI : MonoBehaviour
return NodeState.Failure;
}
- private NodeState CheckHasArtifact()
+ // Node này thay thế cho CheckHasArtifact cũ
+ private NodeState CheckCombatConditions()
{
- if (playerHasArtifact) StopConversation();
- return playerHasArtifact ? NodeState.Success : NodeState.Failure;
+ bool shouldCombat = playerHasArtifact || isAggroedBySound;
+ if (shouldCombat) StopConversation();
+ return shouldCombat ? NodeState.Success : NodeState.Failure;
}
private NodeState CheckCanSeePlayer()
@@ -176,7 +186,7 @@ public class EnemyAI : MonoBehaviour
private NodeState CheckCanTalkToNPC()
{
- if (playerHasArtifact || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
+ if (playerHasArtifact || isAggroedBySound || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
if (isTalking) return NodeState.Success;
@@ -216,6 +226,13 @@ public class EnemyAI : MonoBehaviour
{
suspicionLevel += volume * 15f;
if (fov != null) fov.lastKnownPlayerPosition = location;
+
+ // 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;
+ }
+
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors();
StopConversation();
Debug.Log($"[AI {npcName}] Heard noise! Suspicion: {suspicionLevel}");
@@ -279,31 +296,25 @@ public class EnemyAI : MonoBehaviour
private NodeState ActionPatrol()
{
- Debug.Log("Wandering randomly...");
agent.isStopped = false;
agent.speed = patrolSpeed;
- // Kiểm tra xem NPC đã đi đến điểm ngẫu nhiên hiện tại chưa
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
{
currentWaitTime += Time.deltaTime;
- // Đứng đợi hết thời gian quy định rồi mới tìm đường mới
if (currentWaitTime >= patrolWaitTime)
{
- // 1. Lấy một điểm ngẫu nhiên trong không gian hình cầu dựa trên bán kính
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
- randomDirection += startPosition; // Cộng với tâm ban đầu để giới hạn khu vực
+ randomDirection += startPosition;
NavMeshHit hit;
- // 2. Ép tọa độ ngẫu nhiên đó phải nằm TRÊN bề mặt xanh của NavMesh (tránh kẹt tường)
- // Số 1 ở cuối là Area Mask (thường là Walkable)
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
{
agent.SetDestination(hit.position);
}
- currentWaitTime = 0f; // Reset thời gian chờ
+ currentWaitTime = 0f;
}
}
@@ -339,13 +350,30 @@ public class EnemyAI : MonoBehaviour
private NodeState ActionFocusAndShoot()
{
- if (player == null) return NodeState.Failure;
+ if (player == null) return NodeState.Failure;
if (agent.hasPath) agent.ResetPath();
agent.isStopped = false;
- // 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ PLAYER
- Vector3 bodyDir = player.position - transform.position;
+ // 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;
bodyDir.y = 0f;
Vector3 bodyDirNormal = bodyDir.normalized;
if (bodyDir != Vector3.zero)
@@ -365,32 +393,28 @@ public class EnemyAI : MonoBehaviour
// 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;
- // Kiểm tra xem AI có đang di chuyển ngang không (strafeDirectionSign khác 0)
if (strafeDirectionSign != 0 && bodyDir != Vector3.zero)
{
- // Vector đi ngang trái/phải
finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign;
- // Tiến tới tịnh tiến dần dần nếu chưa đạt khoảng cách tối thiểu
- float currentDistance = Vector3.Distance(transform.position, player.position);
+ float currentDistance = Vector3.Distance(transform.position, targetPos);
if (currentDistance > minCombatDistance)
{
finalMovementVector += bodyDirNormal * approachWeight;
}
}
- // Áp dụng di chuyển thực tế bằng Agent.Move
if (finalMovementVector != Vector3.zero)
{
- finalMovementVector.Normalize(); // Chuẩn hóa vector
+ finalMovementVector.Normalize();
agent.speed = moveSpeed * 0.75f;
agent.Move(finalMovementVector * agent.speed * Time.deltaTime);
}
- // 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO NGƯỜI PLAYER
+ // 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO MỤC TIÊU
if (firePoint != null)
{
- Vector3 targetCenter = player.position + Vector3.up * 1f;
+ Vector3 targetCenter = targetPos + Vector3.up * 1f;
Vector3 aimDir = targetCenter - firePoint.position;
if (aimDir != Vector3.zero)
{
@@ -409,7 +433,6 @@ public class EnemyAI : MonoBehaviour
return NodeState.Running;
}
- // Coroutine xử lý bắn loạt đạn kết hợp RANDOM ĐỘ LỆCH (Spread)
private IEnumerator ShootBurstRoutine(int bulletCount)
{
isShootingBurst = true;
@@ -418,19 +441,15 @@ public class EnemyAI : MonoBehaviour
{
if (laserPrefab == null || firePoint == null) break;
- // Tính toán độ lệch ngẫu nhiên (Xoay quanh trục X và Y của họng súng để tạo độ "lệch tâm thông minh")
float randomX = Random.Range(-maxSpreadAngle, maxSpreadAngle);
float randomY = Random.Range(-maxSpreadAngle, maxSpreadAngle);
Quaternion spreadRotation = Quaternion.Euler(randomX, randomY, 0f);
- // Nhân góc xoay gốc của họng súng với góc lệch ngẫu nhiên
Quaternion finalBulletRotation = firePoint.rotation * spreadRotation;
- // Sinh đạn
Instantiate(laserPrefab, firePoint.position, finalBulletRotation);
Debug.Log($"[AI Burst] Viên thứ {i + 1}/{bulletCount} | Độ lệch X:{randomX:F1}, Y:{randomY:F1}");
- // Nếu còn đạn trong loạt, đợi một khoảng ngắn (burstInterval) rồi mới bắn tiếp viên sau
if (i < bulletCount - 1)
{
yield return new WaitForSeconds(burstInterval);
@@ -440,7 +459,7 @@ public class EnemyAI : MonoBehaviour
isShootingBurst = false;
}
- private void ShootLaser() { } // Hàm cũ không dùng nữa, đã có Burst lo
+ private void ShootLaser() { }
private NodeState ActionDodge()
{