This commit is contained in:
2026-06-05 23:26:49 +07:00
7 changed files with 192 additions and 1814 deletions

View File

@@ -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);
}
}

View File

@@ -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;
@@ -51,17 +49,17 @@ public class EnemyAI : MonoBehaviour
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;
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;
@@ -72,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;
@@ -93,6 +91,7 @@ public class EnemyAI : MonoBehaviour
rb.isKinematic = true;
rb.freezeRotation = true;
startPosition = transform.position;
if (player == null)
{
@@ -106,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) });
@@ -129,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;
@@ -142,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)
@@ -150,10 +157,12 @@ public class EnemyAI : MonoBehaviour
return NodeState.Failure;
}
private NodeState CheckHasArtifact()
// Node này thay thế cho CheckHasArtifact
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()
@@ -177,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;
@@ -217,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}");
@@ -280,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;
}
}
@@ -345,8 +355,25 @@ public class EnemyAI : MonoBehaviour
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)
@@ -355,27 +382,39 @@ public class EnemyAI : MonoBehaviour
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
}
// 2. RANDOM KHOẢNG CÁCH DI CHUYỂN TRÁI/PHẢI (Tính theo thời gian duy trì)
// 2. RANDOM THỜI GIAN/KHOẢNG CÁCH DUY TRÌ HƯỚNG DI CHUYỂN
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);
}
// 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;
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);
finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign;
float currentDistance = Vector3.Distance(transform.position, targetPos);
if (currentDistance > minCombatDistance)
{
finalMovementVector += bodyDirNormal * approachWeight;
}
}
// 3. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO NGƯỜI PLAYER
if (finalMovementVector != Vector3.zero)
{
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 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)
{
@@ -383,20 +422,17 @@ public class EnemyAI : MonoBehaviour
}
}
// 4. RANDOM SỐ ĐẠN (1-3) & RANDOM DELAY GIỮA CÁC ĐỢT BẮN
// 5. RANDOM SỐ ĐẠN & DELAY GIỮA CÁC ĐỢT BẮN
if (Time.time >= nextShootTime && !isShootingBurst)
{
int randomBulletCount = Random.Range(1, 4); // Trả về ngẫu nhiên 1, 2, hoặc 3 viên
int randomBulletCount = Random.Range(1, 4);
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;
}
// Coroutine xử lý bắn loạt đạn kết hợp RANDOM ĐỘ LỆCH (Spread)
private IEnumerator ShootBurstRoutine(int bulletCount)
{
isShootingBurst = true;
@@ -405,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);
@@ -427,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()
{

View File

@@ -28,29 +28,27 @@ public class LaserProjectile : MonoBehaviour
// 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)
// 1. Kiểm tra nếu trúng Player hoặc đối tượng có Health
var healthController = other.GetComponentInParent<vIHealthController>();
if (other.CompareTag("Player") || healthController != 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.");
Debug.Log($"<color=red>HIT PLAYER!</color> 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();
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 an obstacle (Wall/Floor).");
Debug.Log($"Laser hit solid object: {other.name} (Ground/Obstacle). Destroying.");
Impact();
}
}

View File

@@ -13,19 +13,24 @@ Material:
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _COLOROVERLAY_ON
- _DISTORTION_ON
- _FADING_ON
- _FLIPBOOKBLENDING_ON
- _SOFTPARTICLES_ON
- _SURFACE_TYPE_TRANSPARENT
m_InvalidKeywords:
- EFFECT_BUMP
- _ALPHABLEND_ON
- _FLIPBOOKBLENDING_OFF
- _REQUIRE_UV2
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
m_CustomRenderQueue: 3000
stringTagMap:
RenderType: Opaque
disabledShaderPasses: []
RenderType: Transparent
disabledShaderPasses:
- DepthOnly
- SHADOWCASTER
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
@@ -88,8 +93,8 @@ Material:
- _DistortionEnabled: 1
- _DistortionStrength: 1
- _DistortionStrengthScaled: 0.1
- _DstBlend: 0
- _DstBlendAlpha: 0
- _DstBlend: 10
- _DstBlendAlpha: 10
- _EmissionEnabled: 1
- _FlipbookBlending: 1
- _FlipbookMode: 1
@@ -108,11 +113,11 @@ Material:
- _SoftParticlesFarFadeDistance: 1
- _SoftParticlesNearFadeDistance: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _SrcBlend: 5
- _SrcBlendAlpha: 1
- _Surface: 1
- _UVSec: 0
- _ZWrite: 1
- _ZWrite: 0
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _BaseColorAddSubDiff: {r: 0, g: 0, b: 0, a: 0}
@@ -121,7 +126,7 @@ Material:
- _ColorAddSubDiff: {r: 1, g: 0, b: 0, a: 0}
- _EmisColor: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0.31132078, g: 0.31132078, b: 0.31132078, a: 1}
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
- _SoftParticleFadeParams: {r: 0, g: 1, b: 0, a: 0}
- _TintColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1