diff --git a/Assets/Animation/Shooter/Animator/Invector@ShooterMelee.controller b/Assets/Animation/Shooter/Animator/Invector@ShooterMelee.controller index 11183ac9..cfdb0848 100644 --- a/Assets/Animation/Shooter/Animator/Invector@ShooterMelee.controller +++ b/Assets/Animation/Shooter/Animator/Invector@ShooterMelee.controller @@ -14716,7 +14716,10 @@ AnimatorStateMachine: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: Base Layer - m_ChildStates: [] + m_ChildStates: + - serializedVersion: 1 + m_State: {fileID: 4416801500611958025} + m_Position: {x: -358.64734, y: -43.860413, z: 0} m_ChildStateMachines: - serializedVersion: 1 m_StateMachine: {fileID: 1107191697213795204} @@ -36256,6 +36259,32 @@ AnimatorStateTransition: m_InterruptionSource: 0 m_OrderedInterruption: 1 m_CanTransitionToSelf: 1 +--- !u!1102 &4416801500611958025 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: TestRun + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: [] + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400012, guid: 37c6cfe59f56e8a4799011397a870a8b, type: 3} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: --- !u!1101 &4602781940129309487 AnimatorStateTransition: m_ObjectHideFlags: 1 diff --git a/Assets/Scenes/Cho môn AI/Only AI.unity b/Assets/Scenes/Cho môn AI/Only AI.unity index 4a1e9f7c..824ec507 100644 --- a/Assets/Scenes/Cho môn AI/Only AI.unity +++ b/Assets/Scenes/Cho môn AI/Only AI.unity @@ -90874,6 +90874,22 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 6469822191588635990, guid: 15df559ce497e104a81254e0adf3107e, type: 3} + propertyPath: useSimulation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6469822191588635990, guid: 15df559ce497e104a81254e0adf3107e, type: 3} + propertyPath: autoCycleSpeed + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6469822191588635990, guid: 15df559ce497e104a81254e0adf3107e, type: 3} + propertyPath: forcePlayState + value: Free Movement + objectReference: {fileID: 0} + - target: {fileID: 6469822191588635990, guid: 15df559ce497e104a81254e0adf3107e, type: 3} + propertyPath: showLayerWeights + value: 1 + objectReference: {fileID: 0} - target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3} propertyPath: m_Name value: xNPC diff --git a/Assets/Scripts/AI NPC/AnimatorAI.cs b/Assets/Scripts/AI NPC/AnimatorAI.cs index 8d938d97..12657a1c 100644 --- a/Assets/Scripts/AI NPC/AnimatorAI.cs +++ b/Assets/Scripts/AI NPC/AnimatorAI.cs @@ -68,6 +68,10 @@ public class AnimatorAI : MonoBehaviour 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 virtual void Start() { animator = GetComponentInChildren(); @@ -81,45 +85,57 @@ public class AnimatorAI : MonoBehaviour return; } - // CHẨN ĐOÁN SÂU: - if (debugMode) StartCoroutine(DeepDiagnosticRoutine()); + // Chạy chẩn đoán hạt nhân + StartCoroutine(NuclearDiagnosticRoutine()); InitializeParameters(); } - private IEnumerator DeepDiagnosticRoutine() + private IEnumerator NuclearDiagnosticRoutine() { while (true) { - yield return new WaitForSeconds(2f); // Kiểm tra mỗi 2 giây + yield return new WaitForSeconds(1f); if (animator == null) yield break; - // 1. Kiểm tra Avatar - if (animator.avatar == null) - Debug.LogError($"[T-POSE ALERT] {animator.gameObject.name} KHÔNG CÓ AVATAR! Đây là lý do bị T-Pose. Hãy kéo Avatar vào component Animator."); - - // 2. Kiểm tra Speed - if (animator.speed <= 0) - Debug.LogWarning($"[T-POSE ALERT] Tốc độ Animator đang bằng {animator.speed}. Nhân vật sẽ không cử động."); + // 1. Kiểm tra Enabled + if (!animator.enabled) + Debug.LogError($"[NUCLEAR ALERT] Component Animator đang bị TẮT (enabled = false)!"); - // 3. Kiểm tra Animation Clips - var stateInfo = animator.GetCurrentAnimatorStateInfo(0); - var clipInfo = animator.GetCurrentAnimatorClipInfo(0); - - if (clipInfo.Length == 0) + // 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) { - Debug.LogError($"[T-POSE ALERT] State hiện tại ({stateInfo.fullPathHash}) KHÔNG CÓ Clip animation nào! Hãy kéo file .anim vào ô Motion của State trong Animator Controller."); - } - - // 4. Kiểm tra Culling - if (animator.cullingMode == AnimatorCullingMode.CullCompletely) - { - // Đôi khi Unity tự tắt animation nếu camera không nhìn thấy - Debug.Log($"[Info] Culling Mode đang là CullCompletely. Nếu nhân vật ở xa có thể sẽ dừng animation."); + for (int i = 0; i < animator.layerCount; i++) + { + Debug.Log($"Layer {i} ({animator.GetLayerName(i)}): Weight = {animator.GetLayerWeight(i)}"); + } } - if (!useSimulation) yield break; // Nếu không dùng simulation thì chỉ check 1 lần rồi thô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; } } diff --git a/Assets/Scripts/AI NPC/EnemyAI.cs b/Assets/Scripts/AI NPC/EnemyAI.cs index 2b53ca7c..8906a533 100644 --- a/Assets/Scripts/AI NPC/EnemyAI.cs +++ b/Assets/Scripts/AI NPC/EnemyAI.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using System.Linq; +using UnityEngine.InputSystem; using Random = UnityEngine.Random; [Serializable] @@ -25,17 +26,21 @@ public class EnemyAI : MonoBehaviour public float rotateSpeed = 10f; [Header("Patrol Settings")] - public Transform[] patrolWaypoints; - public int currentWaypointIndex = 0; + 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 + + + private Vector3 startPosition; [Header("Combat State")] public bool playerHasArtifact; public GameObject laserPrefab; public Transform firePoint; - public float minShootDelay = 1f; - public float maxShootDelay = 3f; + public float minShootDelay = 1.5f; // Delay giữa các LOẠT BẮN + public float maxShootDelay = 3.5f; private float nextShootTime; [Header("Dodge Settings")] @@ -45,6 +50,19 @@ public class EnemyAI : MonoBehaviour private bool isDodging = false; private float nextDodgeTime = 0f; + [Header("Artifact Combat Upgrades (New)")] + [Tooltip("Khoảng cách di chuyển trái/phải ngẫu nhiên qua thời gian duy trì")] + public float minStrafeDuration = 0.5f; + public float maxStrafeDuration = 2.2f; + [Tooltip("Độ lệch tâm bắn (Độ). Số càng nhỏ bắn càng chuẩn, số lớn bắn càng lệch")] + public float maxSpreadAngle = 6f; + [Tooltip("Tốc độ bắn giữa các viên trong cùng 1 loạt đạn")] + public float burstInterval = 0.12f; + + 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 + [Header("Conversation Settings")] public string npcName = "Guard"; [TextArea] public string persona = "You are a bored security guard. You love coffee and hate night shifts."; @@ -121,8 +139,10 @@ public class EnemyAI : MonoBehaviour private NodeState CheckDodgeConditions() { + if (playerHasArtifact) return NodeState.Failure; // Có cổ vật -> Không Dash né nữa + if (isDodging) return NodeState.Success; - if (fov != null && fov.canSeePlayer && Input.GetMouseButtonDown(0) && Time.time >= nextDodgeTime) + if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed) return NodeState.Success; return NodeState.Failure; } @@ -146,7 +166,6 @@ public class EnemyAI : MonoBehaviour { if (suspicionLevel > investigationThreshold) { - // Randomly decide to check or stay on patrol if (Random.value < (suspicionLevel / 100f)) return NodeState.Success; } } @@ -175,7 +194,6 @@ public class EnemyAI : MonoBehaviour EnemyAI other = hit.GetComponentInParent(); if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown) { - // Kiểm tra khoảng cách thực tế giữa 2 NPC float dist = Vector3.Distance(transform.position, other.transform.position); if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID()) { @@ -220,8 +238,6 @@ public class EnemyAI : MonoBehaviour if (isTalking) { agent.isStopped = true; - - // Nếu bạn diễn đi quá xa trong khi đang nói, ngắt hội thoại if (talkingPartner != null) { if (Vector3.Distance(transform.position, talkingPartner.transform.position) > talkRange + 2f) @@ -231,7 +247,6 @@ public class EnemyAI : MonoBehaviour return NodeState.Failure; } } - return NodeState.Running; } return NodeState.Failure; @@ -244,12 +259,9 @@ public class EnemyAI : MonoBehaviour DialogueResult result = JsonUtility.FromJson(json); if (chatBubble != null) chatBubble.Show(result.text); - // Apply minor stat mods moveSpeed += result.speedMod; suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100); lastTalkTime = Time.time; - - Debug.Log($"[AI {npcName}] Conv result: {result.text} | SpeedMod: {result.speedMod}"); } catch { if (chatBubble != null) chatBubble.Show(json); } } @@ -265,23 +277,34 @@ public class EnemyAI : MonoBehaviour private NodeState ActionPatrol() { - if (patrolWaypoints == null || patrolWaypoints.Length == 0) return NodeState.Failure; - + Debug.Log("Wandering randomly..."); agent.isStopped = false; - agent.speed = moveSpeed * (suspicionLevel > 20 ? 0.7f : 0.5f); // Walk faster if suspicious - - var target = patrolWaypoints[currentWaypointIndex]; - agent.SetDestination(target.position); + agent.speed = patrolSpeed; - if (Vector3.Distance(transform.position, target.position) < 1.5f) + // 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) { - currentWaypointIndex = (currentWaypointIndex + 1) % patrolWaypoints.Length; - currentWaitTime = 0f; + // 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 + + 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ờ } } + return NodeState.Running; } @@ -302,10 +325,10 @@ public class EnemyAI : MonoBehaviour if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f) { currentWaitTime += Time.deltaTime; - if (currentWaitTime > 3f) // Look around for 3 seconds + if (currentWaitTime > 3f) { fov.lastKnownPlayerPosition = Vector3.zero; - suspicionLevel *= 0.5f; // Decrease suspicion after check + suspicionLevel *= 0.5f; return NodeState.Success; } } @@ -316,46 +339,93 @@ public class EnemyAI : MonoBehaviour { if (player == null) return NodeState.Failure; - agent.isStopped = true; // Đứng im bắn cố định khi có cổ vật + if (agent.hasPath) agent.ResetPath(); + agent.isStopped = false; - // 1. XOAY THÂN THEO TRỤC NGANG (Giúp NPC luôn đứng thẳng lưng, không bị đổ người) + // 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ PLAYER Vector3 bodyDir = player.position - transform.position; - bodyDir.y = 0f; // Khóa trục dọc của thân + bodyDir.y = 0f; + Vector3 bodyDirNormal = bodyDir.normalized; if (bodyDir != Vector3.zero) { - Quaternion targetRotation = Quaternion.LookRotation(bodyDir); + Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); } - // 2. XOAY HỌNG SÚNG THEO TRỤC DỌC (Chúi xuống hoặc ngước lên thẳng vào Player) + // 2. RANDOM KHOẢNG CÁCH DI CHUYỂN TRÁI/PHẢI (Tính theo thời gian duy trì) + 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)]; + + // Ép khoảng cách di chuyển dài ngắn ngẫu nhiên bằng cách random thời gian đổi hướng + nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration); + } + + if (strafeDirectionSign != 0 && bodyDir != Vector3.zero) + { + Vector3 strafeDir = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign; + agent.speed = moveSpeed * 0.75f; + agent.Move(strafeDir * agent.speed * Time.deltaTime); + } + + // 3. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO NGƯỜI PLAYER if (firePoint != null) { - // Cộng thêm Vector3.up * 1f để họng súng nhắm vào NGỰC/BỤNG player, không bị bắn chúi xuống đất (bắn vào chân) Vector3 targetCenter = player.position + Vector3.up * 1f; Vector3 aimDir = targetCenter - firePoint.position; - if (aimDir != Vector3.zero) { - // Họng súng nhìn thẳng hoàn toàn vào Player (bao gồm cả trục Y chéo xuống) firePoint.rotation = Quaternion.LookRotation(aimDir); } } - // 3. BẮN LASER (Laser sinh ra sẽ tự động lấy rotation chéo của firePoint) - if (Time.time >= nextShootTime) + // 4. RANDOM SỐ ĐẠN (1-3) & RANDOM DELAY GIỮA CÁC ĐỢT BẮN + if (Time.time >= nextShootTime && !isShootingBurst) { - ShootLaser(); + int randomBulletCount = Random.Range(1, 4); // Trả về ngẫu nhiên 1, 2, hoặc 3 viên + StartCoroutine(ShootBurstRoutine(randomBulletCount)); + + // Cập nhật thời gian chờ cho loạt đạn tiếp theo (Random delay) nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); } return NodeState.Running; } - private void ShootLaser() + + // Coroutine xử lý bắn loạt đạn kết hợp RANDOM ĐỘ LỆCH (Spread) + private IEnumerator ShootBurstRoutine(int bulletCount) { - if (laserPrefab == null || firePoint == null) return; - Instantiate(laserPrefab, firePoint.position, firePoint.rotation); - Debug.Log($"[AI {npcName}] Fired chéo Laser downwards/upwards at Player!"); + isShootingBurst = true; + + for (int i = 0; i < bulletCount; i++) + { + 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); + } + } + + isShootingBurst = false; } + + private void ShootLaser() { } // Hàm cũ không dùng nữa, đã có Burst lo + private NodeState ActionDodge() { if (!isDodging) StartCoroutine(DodgeRollRoutine()); @@ -377,11 +447,7 @@ public class EnemyAI : MonoBehaviour isDodging = false; } - public void SetTalkingPartner(EnemyAI partner) - { - talkingPartner = partner; - } - + public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; } public void FaceTarget(Vector3 pos) { Vector3 dir = (pos - transform.position); @@ -393,15 +459,13 @@ public class EnemyAI : MonoBehaviour 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); } } -} +} \ No newline at end of file