This commit is contained in:
2026-06-06 00:39:34 +07:00
18 changed files with 2501 additions and 542 deletions

View File

@@ -5,14 +5,17 @@ using UnityEngine;
using UnityEngine.AI;
using System.Linq;
using UnityEngine.InputSystem;
using Invector;
using Invector.vCharacterController;
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 / Sound Aggro) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần
// Quy trình ưu tiên: Né đòn (Bị bắn) --> Panic (Thấp máu) --> Bắn hạ (Chiến đấu) --> Đuổi theo --> Điều tra --> Nói chuyện --> Đi tuần
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(vHealthController))]
public class EnemyAI : MonoBehaviour
{
[Header("References")]
@@ -20,6 +23,8 @@ public class EnemyAI : MonoBehaviour
private NavMeshAgent agent;
private Rigidbody rb;
private FieldOfView fov;
private Collider mainCollider;
private vHealthController health;
[Header("Movement Settings")]
public float moveSpeed = 3f;
@@ -34,7 +39,7 @@ public class EnemyAI : MonoBehaviour
[Header("Combat State")]
public bool playerHasArtifact;
public bool isAggroedBySound; // <-- MỚI: Trạng thái bị đánh động bởi âm thanh
public bool isAggroedBySound;
public GameObject laserPrefab;
public Transform firePoint;
public float minShootDelay = 1.5f;
@@ -48,7 +53,15 @@ public class EnemyAI : MonoBehaviour
private bool isDodging = false;
private float nextDodgeTime = 0f;
[Header("Artifact Combat Upgrades (New)")]
[Header("Advanced AI States (New)")]
public bool isPanicking = false;
public bool isEnraged = false;
public float panicHealthThreshold = 40f; // Dưới 40% máu sẽ panic
public float regenRate = 2f; // Hồi 2 máu mỗi giây
public float regenDelay = 5f; // Cần 5s không nhận dame để bắt đầu hồi
private float lastDamageTime;
[Header("Artifact Combat Upgrades")]
public float minStrafeDuration = 0.5f;
public float maxStrafeDuration = 2.2f;
public float maxSpreadAngle = 6f;
@@ -78,7 +91,7 @@ public class EnemyAI : MonoBehaviour
public float suspicionLevel = 0f;
public float investigationThreshold = 30f;
public float alertNeighborsThreshold = 70f;
public float alertRange = 20f;
public float alertRange = 25f; // Tăng tầm gọi hội
public Node rootNode;
@@ -88,6 +101,16 @@ public class EnemyAI : MonoBehaviour
rb = GetComponent<Rigidbody>();
fov = GetComponent<FieldOfView>();
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
mainCollider = GetComponent<Collider>();
health = GetComponent<vHealthController>();
// Thiết lập Invector Health
health.onReceiveDamage.AddListener(OnReceiveDamage);
health.onDead.AddListener(OnDead);
// Tự động gán Layer Enemy
if (gameObject.layer == LayerMask.NameToLayer("Default"))
gameObject.layer = LayerMask.NameToLayer("Enemy");
rb.isKinematic = true;
rb.freezeRotation = true;
@@ -104,11 +127,16 @@ public class EnemyAI : MonoBehaviour
void InitTree()
{
// 1. Phản xạ tức thì (Dodge)
var dodgeSequence = new Sequence(new List<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
// Đổi hàm CheckHasArtifact thành CheckCombatConditions để dùng chung cho cả âm thanh
// 2. Hoảng loạn (Panic)
var panicSequence = new Sequence(new List<Node> { new TaskNode(CheckPanicConditions), new TaskNode(ActionPanic) });
// 3. Tấn công (Laser)
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) });
// 4. Các hành vi khác
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) });
@@ -117,6 +145,7 @@ public class EnemyAI : MonoBehaviour
rootNode = new Selector(new List<Node>
{
dodgeSequence,
panicSequence,
laserSequence,
chaseSequence,
investigateSequence,
@@ -127,40 +156,115 @@ public class EnemyAI : MonoBehaviour
void Update()
{
if (player == null) return;
if (player == null || health.isDead) return;
if (!agent.isOnNavMesh) return;
// Đảm bảo Collider luôn bật
if (mainCollider != null && !mainCollider.enabled) mainCollider.enabled = true;
HandleHealthRegen();
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 (suspicionLevel <= 0f && !isEnraged) isAggroedBySound = false;
if (!isTalking && !isDodging && agent.isStopped)
if (!isTalking && !isDodging && !isPanicking && agent.isStopped)
agent.isStopped = false;
rootNode?.Evaluate();
}
private void HandleHealthRegen()
{
if (Time.time > lastDamageTime + regenDelay && health.currentHealth < health.maxHealth)
{
health.AddHealth((int)(regenRate * Time.deltaTime));
}
}
#region HEALTH EVENTS
private void OnReceiveDamage(vDamage damage)
{
lastDamageTime = Time.time;
isAggroedBySound = true;
suspicionLevel = 100f;
StopConversation();
// Gọi hội ngay khi bị bắn
AlertNeighbors(damage.hitPosition);
// Né đòn ngay lập tức khi bị trúng dame (phản xạ Elden Ring)
if (Time.time > nextDodgeTime && !isDodging)
{
StartCoroutine(DodgeRollRoutine());
}
// Kiểm tra Enrage (Phase 2)
if (health.currentHealth < health.maxHealth * 0.5f && !isEnraged)
{
EnterEnrageMode();
}
}
private void OnDead(GameObject killer)
{
Debug.Log($"<color=black>[AI {npcName}] DIED.</color>");
// Khi chết, làm những con xung quanh Enraged gắt hơn
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
foreach (var hit in hitColliders)
{
EnemyAI ally = hit.GetComponentInParent<EnemyAI>();
if (ally != null && ally != this) ally.EnterEnrageMode();
}
agent.enabled = false;
this.enabled = false;
}
public void EnterEnrageMode()
{
if (isEnraged) return;
isEnraged = true;
isPanicking = false; // Hết sợ, chuyển sang liều mạng
// Buff stats mạnh mẽ như Elden Ring boss phase 2
moveSpeed *= 1.5f;
minShootDelay *= 0.5f;
maxShootDelay *= 0.5f;
maxSpreadAngle *= 0.6f; // Bắn chính xác hơn
approachWeight = 0.8f; // Áp sát cực gắt
minCombatDistance = 2.0f; // Đứng sát mặt Player để bắn
if (chatBubble != null) chatBubble.Show("FOR THE BROTHERHOOD! DIE!", 3f);
Debug.Log($"<color=red>[AI {npcName}] ENRAGED!</color> Stats boosted.");
}
#endregion
#region CONDITIONS
private NodeState CheckDodgeConditions()
{
if (playerHasArtifact || isAggroedBySound) return NodeState.Failure;
if (isDodging) return NodeState.Success;
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed)
// Tự né khi thấy Player click chuột trái (đang bắn)
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed && Time.time > nextDodgeTime)
return NodeState.Success;
return NodeState.Failure;
}
// Node này thay thế cho CheckHasArtifact cũ
private NodeState CheckPanicConditions()
{
if (isEnraged) return NodeState.Failure; // Đang điên thì không sợ
if (health.currentHealth < (health.maxHealth * (panicHealthThreshold / 100f)))
{
return NodeState.Success;
}
return NodeState.Failure;
}
private NodeState CheckCombatConditions()
{
bool shouldCombat = playerHasArtifact || isAggroedBySound;
bool shouldCombat = playerHasArtifact || isAggroedBySound || isEnraged;
if (shouldCombat) StopConversation();
return shouldCombat ? NodeState.Success : NodeState.Failure;
}
@@ -168,48 +272,32 @@ public class EnemyAI : MonoBehaviour
private NodeState CheckCanSeePlayer()
{
bool canSee = fov != null && fov.canSeePlayer;
if (canSee) { StopConversation(); AlertNeighbors(); suspicionLevel = 100; }
if (canSee) { StopConversation(); AlertNeighbors(transform.position); suspicionLevel = 100; }
return canSee ? NodeState.Success : NodeState.Failure;
}
private NodeState CheckHasInvestigateTarget()
{
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero)
{
if (suspicionLevel > investigationThreshold)
{
if (Random.value < (suspicionLevel / 100f)) return NodeState.Success;
}
}
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero && suspicionLevel > investigationThreshold)
return NodeState.Success;
return NodeState.Failure;
}
private NodeState CheckCanTalkToNPC()
{
if (playerHasArtifact || isAggroedBySound || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
if (isTalking) return NodeState.Success;
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;
if (playerHasArtifact || isAggroedBySound || isEnraged || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
if (Time.time < lastTalkTime + talkCooldown || isTalking) return NodeState.Failure;
if (Hallucinate.AI.ConversationManager.Instance == null || !Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure;
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
foreach (var hit in hitColliders)
{
if (hit.gameObject == gameObject) continue;
EnemyAI other = hit.GetComponentInParent<EnemyAI>();
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
if (other != null && !other.isTalking && !other.isEnraged)
{
float dist = Vector3.Distance(transform.position, other.transform.position);
if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
{
Debug.Log($"<color=green>[AI {npcName}]</color> Found partner: {other.npcName}. Starting conversation.");
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
return NodeState.Success;
}
@@ -226,19 +314,12 @@ 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();
if (suspicionLevel >= investigationThreshold) isAggroedBySound = true;
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(location);
StopConversation();
Debug.Log($"<color=orange>[AI {npcName}]</color> Heard noise! Suspicion: {suspicionLevel}");
}
public void AlertNeighbors()
public void AlertNeighbors(Vector3 threatPos)
{
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
foreach (var hit in hitColliders)
@@ -246,83 +327,77 @@ public class EnemyAI : MonoBehaviour
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;
neighbor.TriggerCombatAlert(threatPos);
}
}
}
public void TriggerCombatAlert(Vector3 sourceLocation)
{
if (isEnraged) return;
suspicionLevel = 100f;
isAggroedBySound = true;
if (fov != null) fov.lastKnownPlayerPosition = sourceLocation;
StopConversation();
}
private NodeState ActionPanic()
{
isPanicking = true;
agent.speed = moveSpeed * 1.3f;
// Chạy trốn khỏi Player đến một điểm ngẫu nhiên xa Player
if (!agent.pathPending && agent.remainingDistance < 1f)
{
Vector3 runDir = (transform.position - player.position).normalized;
Vector3 escapePoint = transform.position + runDir * 10f + Random.insideUnitSphere * 5f;
NavMeshHit hit;
if (NavMesh.SamplePosition(escapePoint, out hit, 10f, 1))
agent.SetDestination(hit.position);
}
if (chatBubble != null && Random.value < 0.01f) chatBubble.Show("HELP! HE'S KILLING US!", 1f);
return NodeState.Running;
}
private NodeState ActionTalk()
{
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;
}
if (isTalking) { agent.isStopped = true; return NodeState.Running; }
return NodeState.Failure;
}
public void ProcessDialogueResult(string json)
{
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); }
}
private void StopConversation()
{
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
{
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
if (chatBubble != null) chatBubble.Show("Wait, what was that?!", 2f);
}
}
private NodeState ActionPatrol()
{
isPanicking = false;
agent.isStopped = false;
agent.speed = patrolSpeed;
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
{
currentWaitTime += Time.deltaTime;
if (currentWaitTime >= patrolWaitTime)
{
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
randomDirection += startPosition;
Vector3 randomDest = startPosition + Random.insideUnitSphere * patrolRadius;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
{
agent.SetDestination(hit.position);
}
if (NavMesh.SamplePosition(randomDest, out hit, patrolRadius, 1)) agent.SetDestination(hit.position);
currentWaitTime = 0f;
}
}
return NodeState.Running;
}
private NodeState ActionChasePlayer()
{
isPanicking = false;
agent.isStopped = false;
agent.speed = moveSpeed;
agent.SetDestination(player.position);
@@ -334,99 +409,58 @@ public class EnemyAI : MonoBehaviour
agent.isStopped = false;
agent.speed = moveSpeed * 0.7f;
agent.SetDestination(fov.lastKnownPlayerPosition);
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
{
currentWaitTime += Time.deltaTime;
if (currentWaitTime > 3f)
{
fov.lastKnownPlayerPosition = Vector3.zero;
suspicionLevel *= 0.5f;
return NodeState.Success;
}
if (currentWaitTime > 3f) { fov.lastKnownPlayerPosition = Vector3.zero; return NodeState.Success; }
}
return NodeState.Running;
}
private NodeState ActionFocusAndShoot()
{
isPanicking = false;
if (player == null) return NodeState.Failure;
if (agent.hasPath) agent.ResetPath();
agent.isStopped = false;
// 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
Vector3 targetPos = player.position;
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;
// Xoay về phía mục tiêu
Vector3 bodyDir = (targetPos - transform.position);
bodyDir.y = 0f;
if (bodyDir != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
}
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(bodyDir), rotateSpeed * Time.deltaTime);
// 2. RANDOM THỜI GIAN/KHOẢNG CÁCH DUY TRÌ HƯỚNG DI CHUYỂN
// Strafe di chuyển linh hoạt
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)];
strafeDirectionSign = new int[] { -1, 1, 0 }[Random.Range(0, 3)];
nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration);
}
// 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;
Vector3 moveDir = Vector3.zero;
if (strafeDirectionSign != 0 && bodyDir != Vector3.zero)
{
finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign;
float currentDistance = Vector3.Distance(transform.position, targetPos);
if (currentDistance > minCombatDistance)
{
finalMovementVector += bodyDirNormal * approachWeight;
}
Vector3 normal = bodyDir.normalized;
moveDir = new Vector3(-normal.z, 0, normal.x) * strafeDirectionSign;
if (Vector3.Distance(transform.position, targetPos) > minCombatDistance) moveDir += normal * approachWeight;
}
if (finalMovementVector != Vector3.zero)
if (moveDir != Vector3.zero)
{
finalMovementVector.Normalize();
agent.speed = moveSpeed * 0.75f;
agent.Move(finalMovementVector * agent.speed * Time.deltaTime);
agent.speed = moveSpeed * (isEnraged ? 1f : 0.75f);
agent.Move(moveDir.normalized * agent.speed * Time.deltaTime);
}
// 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO MỤC TIÊU
if (firePoint != null)
{
Vector3 targetCenter = targetPos + Vector3.up * 1f;
Vector3 aimDir = targetCenter - firePoint.position;
if (aimDir != Vector3.zero)
{
firePoint.rotation = Quaternion.LookRotation(aimDir);
}
}
firePoint.rotation = Quaternion.LookRotation((targetPos + Vector3.up * 1f) - firePoint.position);
// 5. RANDOM SỐ ĐẠN & DELAY GIỮA CÁC ĐỢT BẮN
if (Time.time >= nextShootTime && !isShootingBurst)
{
int randomBulletCount = Random.Range(1, 4);
StartCoroutine(ShootBurstRoutine(randomBulletCount));
StartCoroutine(ShootBurstRoutine(Random.Range(1, isEnraged ? 6 : 4)));
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
}
@@ -436,31 +470,17 @@ public class EnemyAI : MonoBehaviour
private IEnumerator ShootBurstRoutine(int bulletCount)
{
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);
}
float spread = isEnraged ? maxSpreadAngle * 0.5f : maxSpreadAngle;
Quaternion rot = firePoint.rotation * Quaternion.Euler(Random.Range(-spread, spread), Random.Range(-spread, spread), 0f);
Instantiate(laserPrefab, firePoint.position, rot);
yield return new WaitForSeconds(isEnraged ? burstInterval * 0.7f : burstInterval);
}
isShootingBurst = false;
}
private void ShootLaser() { }
private NodeState ActionDodge()
{
if (!isDodging) StartCoroutine(DodgeRollRoutine());
@@ -472,16 +492,34 @@ public class EnemyAI : MonoBehaviour
isDodging = true;
agent.enabled = false;
rb.isKinematic = false;
if (mainCollider != null) mainCollider.enabled = true;
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);
Vector3 perp = new Vector3(-dir.z, 0, dir.x) * (Random.value > 0.5f ? 1 : -1);
rb.AddForce(perp * (isEnraged ? dodgeForce * 1.5f : dodgeForce), ForceMode.Impulse);
yield return new WaitForSeconds(dodgeDuration);
rb.linearVelocity = Vector3.zero;
rb.isKinematic = true;
agent.enabled = true;
nextDodgeTime = Time.time + (isEnraged ? dodgeCooldown * 0.5f : dodgeCooldown);
isDodging = false;
}
public void ProcessDialogueResult(string json)
{
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); }
}
public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; }
public void FaceTarget(Vector3 pos)
{
@@ -494,13 +532,7 @@ public class EnemyAI : MonoBehaviour
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);
}
Gizmos.color = isEnraged ? Color.red : Color.green;
Gizmos.DrawWireSphere(transform.position, alertRange);
}
}

View File

@@ -45,7 +45,18 @@ public class LaserProjectile : MonoBehaviour
return;
}
// KIỂM TRA LAYER "GROUND"
if (other.gameObject.layer == LayerMask.NameToLayer("Ground"))
{
Debug.Log("<color=yellow>Laser hit GROUND layer.</color>");
Impact();
return;
}
// Phá hủy đạn nếu trúng tường, sàn nhà (mọi thứ không phải trigger khác)
// 2. Phá hủy đạn nếu trúng Ground, Tường, hoặc bất kỳ vật thể đặc nào (không phải Trigger)
if (!other.isTrigger)
{
Debug.Log($"Laser hit solid object: {other.name} (Ground/Obstacle). Destroying.");

View File

@@ -1,20 +1,21 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
using Hallucinate.Audio; // Import namespace for AudioManager
using Hallucinate.Audio;
public class FinishGate : MonoBehaviour
{
[Header("Cài đặt UI Chính")]
public GameObject winPanel;
public GameObject warningUI; // Thông báo "Bạn chưa nhặt rương nào!"
public GameObject welcomeUI; // THÔNG BÁO 1: "Chào mừng!"
public GameObject warningUI; // THÔNG BÁO 2: "Bạn chưa nhặt rương nào!"
[Header("Cài đặt Sao trên HUD (Giao diện chính)")]
[Header("Cài đặt Sao trên HUD")]
public GameObject hudStar1;
public GameObject hudStar2;
public GameObject hudStar3;
[Header("Cài đặt Sao trên Bảng Win (Kết thúc)")]
[Header("Cài đặt Sao trên Bảng Win")]
public GameObject winStar1;
public GameObject winStar2;
public GameObject winStar3;
@@ -24,21 +25,26 @@ public class FinishGate : MonoBehaviour
public string warningSound = "UI_Warning";
public string clickSound = "UI_Click";
[Header("Cấu hình Tag")]
public string playerTag = "Player";
private bool hasEnteredOnce = false; // Theo dõi lần chạm cổng đầu tiên
private void Start()
{
Time.timeScale = 1f;
if (winPanel != null) winPanel.SetActive(false);
if (welcomeUI != null) welcomeUI.SetActive(false);
if (warningUI != null) warningUI.SetActive(false);
// Ẩn tất cả sao lúc bắt đầu
UpdateStarsUI(0);
UpdateWinStarsUI(0);
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Check"))
if (other.CompareTag(playerTag))
{
PlayerInventory player = other.GetComponentInChildren<PlayerInventory>();
if (player == null) player = other.GetComponentInParent<PlayerInventory>();
@@ -47,55 +53,68 @@ public class FinishGate : MonoBehaviour
{
if (player.treasuresCollected > 0)
{
Debug.Log($"<color=green>[Gate]</color> VỀ ĐÍCH! Kết thúc màn chơi với {player.treasuresCollected} sao.");
WinGame(player.treasuresCollected);
}
else
{
Debug.Log("<color=yellow>[Gate]</color> Bạn chưa nhặt rương nào, hãy đi tìm rương trước khi về.");
StopAllCoroutines();
StartCoroutine(ShowTempUI(warningUI));
// Nếu là lần đầu tiên -> Hiện Welcome. Nếu là lần sau -> Hiện Warning.
if (!hasEnteredOnce)
{
hasEnteredOnce = true;
StartCoroutine(ShowTempUI(welcomeUI));
}
else
{
StartCoroutine(ShowTempUI(warningUI));
}
}
}
}
}
// Hàm public để TreasureItem có thể gọi cập nhật HUD ngay khi nhặt
public void UpdateStarsUI(int count)
{
if (hudStar1) hudStar1.SetActive(count >= 1);
if (hudStar2) hudStar2.SetActive(count >= 2);
if (hudStar3) hudStar3.SetActive(count >= 3);
if (hudStar1 != null) hudStar1.SetActive(count >= 1);
if (hudStar2 != null) hudStar2.SetActive(count >= 2);
if (hudStar3 != null) hudStar3.SetActive(count >= 3);
}
void UpdateWinStarsUI(int count)
private void UpdateWinStarsUI(int count)
{
if (winStar1) winStar1.SetActive(count >= 1);
if (winStar2) winStar2.SetActive(count >= 2);
if (winStar3) winStar3.SetActive(count >= 3);
if (winStar1 != null) winStar1.SetActive(count >= 1);
if (winStar2 != null) winStar2.SetActive(count >= 2);
if (winStar3 != null) winStar3.SetActive(count >= 3);
}
void WinGame(int count)
private void WinGame(int count)
{
if (winPanel != null)
{
winPanel.SetActive(true);
UpdateWinStarsUI(count); // Hiện số sao tương ứng trên bảng kết thúc
UpdateWinStarsUI(count);
}
AudioManager.PlayGlobal(winSound); // Chạy âm thanh thắng cuộc
AudioManager.PlayGlobal(winSound);
Time.timeScale = 0f;
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
IEnumerator ShowTempUI(GameObject ui)
private IEnumerator ShowTempUI(GameObject ui)
{
if (ui == null) yield break;
// Tắt tất cả UI tạm thời khác trước khi bật cái mới để tránh đè chữ
if (welcomeUI != null) welcomeUI.SetActive(false);
if (warningUI != null) warningUI.SetActive(false);
ui.SetActive(true);
if (ui == warningUI) AudioManager.PlayGlobal(warningSound); // Chạy âm thanh cảnh báo
if (ui == warningUI)
{
AudioManager.PlayGlobal(warningSound);
}
yield return new WaitForSeconds(3f);
ui.SetActive(false);
@@ -103,17 +122,17 @@ public class FinishGate : MonoBehaviour
public void RestartGame()
{
AudioManager.PlayGlobal(clickSound); // Âm thanh click nút
AudioManager.PlayGlobal(clickSound);
Time.timeScale = 1f;
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public void QuitGame()
{
AudioManager.PlayGlobal(clickSound); // Âm thanh click nút
AudioManager.PlayGlobal(clickSound);
Application.Quit();
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#endif
}
}
}

View File

@@ -1,52 +1,55 @@
using UnityEngine;
using System.Collections;
using Hallucinate.Audio;
using Hallucinate.Audio;
public class TreasureItem : MonoBehaviour
{
[Header("Cài đặt UI thông báo")]
public GameObject notificationText; // Text "Đã nhặt Cổ vật"
[Tooltip("Kéo Text 'Đã nhặt được cổ vật hãy trốn thoát ra khỏi đây' vào đây")]
public GameObject notificationText;
[Header("Cài đặt Âm thanh")]
public string pickupSound = "Item_Pickup";
[Header("Cấu hình Tag")]
public string playerTag = "Player";
private bool isCollected = false;
private void Start()
{
if (notificationText != null)
{
notificationText.SetActive(false);
}
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
if (!isCollected && other.CompareTag(playerTag))
{
PlayerInventory player = other.GetComponentInChildren<PlayerInventory>();
if (player == null) player = other.GetComponentInParent<PlayerInventory>();
if (player != null)
{
// 1. Tăng số lượng rương đang giữ
isCollected = true;
player.treasuresCollected++;
Debug.Log($"<color=cyan>[Chest]</color> NHẶT THÀNH CÔNG! Số rương hiện tại: {player.treasuresCollected}");
// 2. Cập nhật sao trên HUD ngay lập tức (Tìm FinishGate để mượn hàm update)
FinishGate gate = Object.FindAnyObjectByType<FinishGate>();
if (gate != null)
{
gate.UpdateStarsUI(player.treasuresCollected);
}
// 3. Kích hoạt trạng thái truy đuổi cho toàn bộ Enemy AI
SetEnemiesAlertState(true);
// 4. Chạy âm thanh nhặt đồ
if (AudioManager.Instance != null)
{
AudioManager.Instance.Play(pickupSound, position: transform.position);
}
if (notificationText != null)
{
StopAllCoroutines();
StartCoroutine(ShowNotification());
}
// Biến mất rương
gameObject.SetActive(false);
StartCoroutine(HandlePickupRoutine());
}
}
}
@@ -56,14 +59,38 @@ public class TreasureItem : MonoBehaviour
EnemyAI[] allEnemies = Object.FindObjectsByType<EnemyAI>(FindObjectsSortMode.None);
foreach (EnemyAI enemy in allEnemies)
{
enemy.playerHasArtifact = state;
if (enemy != null) enemy.playerHasArtifact = state;
}
}
IEnumerator ShowNotification()
private IEnumerator HandlePickupRoutine()
{
notificationText.SetActive(true);
yield return new WaitForSeconds(2f);
notificationText.SetActive(false);
HideTreasureModel();
if (notificationText != null)
{
notificationText.SetActive(true);
}
yield return new WaitForSeconds(3f); // Tăng lên 3s để người chơi kịp đọc dòng chữ dài
if (notificationText != null)
{
notificationText.SetActive(false);
}
gameObject.SetActive(false);
}
}
private void HideTreasureModel()
{
Collider col = GetComponent<Collider>();
if (col != null) col.enabled = false;
MeshRenderer[] renderers = GetComponentsInChildren<MeshRenderer>();
foreach (MeshRenderer r in renderers)
{
r.enabled = false;
}
}
}

View File

@@ -51,6 +51,9 @@ namespace Invector.vShooter
{
AddTrailPosition();
}
// Log diagnostic: Kiểm tra Layer mà đạn có thể bắn trúng
Debug.Log($"<color=cyan>PROJECTILE SPAWNED:</color> HitLayer Mask: {hitLayer.value}. Đảm bảo Layer của Enemy nằm trong mask này.");
}
protected virtual void Update()
@@ -61,6 +64,7 @@ namespace Invector.vShooter
transform.rotation = Quaternion.LookRotation(_rigidBody.linearVelocity.normalized, transform.up);
}
// Thực hiện raycast để kiểm tra va chạm
if (Physics.Linecast(previousPosition, transform.position + transform.forward * 0.5f, out hitInfo, hitLayer))
{
if (!hitInfo.collider)
@@ -98,11 +102,31 @@ namespace Invector.vShooter
damage.hitPosition = hitInfo.point;
damage.receiver = hitInfo.collider.transform;
damage.force = transform.forward * damage.damageValue * forceMultiplier;
if (damage.damageValue > 0)
{
onPassDamage.Invoke(damage);
hitInfo.collider.gameObject.ApplyDamage(damage, damage.sender ? damage.sender.GetComponent<vIMeleeFighter>() : null);
// 1. Log khi trúng bất cứ thứ gì
Debug.Log($"<color=yellow>PROJECTILE HIT:</color> {hitInfo.collider.name} | Tag: {hitInfo.collider.tag} | Layer: {LayerMask.LayerToName(hitInfo.collider.gameObject.layer)}");
// 2. Tìm đối tượng nhận sát thương (ưu tiên tìm ở cha nếu trúng collider con)
var damageReceiver = hitInfo.collider.gameObject.GetComponentInParent<vIDamageReceiver>();
if (damageReceiver != null)
{
if (hitInfo.collider.CompareTag("Enemy") || damageReceiver.gameObject.CompareTag("Enemy"))
{
Debug.Log($"<color=green>APPLYING DAMAGE TO ENEMY:</color> {damageReceiver.gameObject.name}. Damage: {damage.damageValue}");
}
// Gửi sát thương đến đối tượng tìm thấy
damageReceiver.gameObject.ApplyDamage(damage, damage.sender ? damage.sender.GetComponent<vIMeleeFighter>() : null);
}
else
{
Debug.LogWarning($"<color=orange>NO DAMAGE RECEIVER FOUND</color> on {hitInfo.collider.name} or its parents. Đảm bảo Enemy có component vHealthController.");
}
}
var rigb = hitInfo.collider.gameObject.GetComponent<Rigidbody>();

View File

@@ -302,6 +302,15 @@ namespace Invector.vShooter
var rotation = Quaternion.LookRotation(dir);
GameObject bulletObject = null;
var velocityChanged = 0f;
if (projectile == null)
{
Debug.LogError($"<color=red>WEAPON ERROR:</color> No Projectile Prefab assigned to {gameObject.name}!");
return;
}
Debug.Log($"<color=white>WEAPON SHOOT:</color> Spawning projectile. HitLayer: {hitLayer.value}");
if (dispersion > 0 && projectile)
{
for (int i = 0; i < projectilesPerShot; i++)
@@ -311,6 +320,12 @@ namespace Invector.vShooter
bulletObject = Instantiate(projectile, startPoint, spreadRotation);
var pCtrl = bulletObject.GetComponent<vProjectileControl>();
if (pCtrl == null)
{
Debug.LogError($"<color=red>PROJECTILE ERROR:</color> {projectile.name} does not have vProjectileControl script!");
continue;
}
if (pCtrl.debugTrajetory && i == 0)
{
startPoint.DebugPoint(Color.red, 10, 0.1f);
@@ -338,6 +353,13 @@ namespace Invector.vShooter
{
bulletObject = Instantiate(projectile, startPoint, rotation);
var pCtrl = bulletObject.GetComponent<vProjectileControl>();
if (pCtrl == null)
{
Debug.LogError($"<color=red>PROJECTILE ERROR:</color> {projectile.name} does not have vProjectileControl script!");
return;
}
if (pCtrl.debugTrajetory)
{
startPoint.DebugPoint(Color.red, 10, 0.1f);