Merge branch 'main' of https://scove-vault.duckdns.org/scove/HALLUCINATION
This commit is contained in:
@@ -1,307 +1,199 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using Invector;
|
||||
using Invector.vEventSystems;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Animator>();
|
||||
enemyAI = GetComponent<EnemyAI>();
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
rb = GetComponent<Rigidbody>();
|
||||
healthController = GetComponent<vHealthController>();
|
||||
|
||||
if (animator == null)
|
||||
if (animator)
|
||||
{
|
||||
Debug.LogError($"<color=red>[AnimatorAI]</color> 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($"<color=red>[NUCLEAR ALERT]</color> Component Animator đang bị TẮT (enabled = false)!");
|
||||
|
||||
// 2. Kiểm tra Rig Type
|
||||
if (animator.isHuman)
|
||||
Debug.Log($"<color=white>[Rig Info]</color> Rig là Humanoid. Hãy chắc chắn các Animation cũng là Humanoid.");
|
||||
else
|
||||
Debug.LogWarning($"<color=yellow>[Rig Info]</color> 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($"<color=magenta>[Nuclear Force]</color> É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($"<color=red>[NUCLEAR ALERT]</color> 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($"<color=yellow>[AnimatorAI]</color> Cảnh báo: Controller thiếu biến quan trọng: <b>{paramName}</b>");
|
||||
}
|
||||
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($"<color=cyan>[AnimDebug]</color> {gameObject.name}: <b>{name}</b> -> {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($"<color=orange>[AnimDebug]</color> {gameObject.name}: <b>{name}</b> -> {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($"<color=yellow>[AnimDebug]</color> {gameObject.name}: Kích hoạt <b>{name}</b>");
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<vIHealthController>() != null)
|
||||
{
|
||||
var healthController = other.GetComponentInParent<vIHealthController>();
|
||||
|
||||
if (healthController != null)
|
||||
{
|
||||
Debug.Log(
|
||||
$"<color=red>HIT PLAYER!</color> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
|
||||
var laserSequence = new Sequence(new List<Node> { 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<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) });
|
||||
@@ -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($"<color=orange>[AI {npcName}]</color> 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($"<color=cyan>[AI Burst]</color> 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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user