using System.Collections.Generic; using UnityEngine; namespace Invector.vShooter { using Invector.vEventSystems; [vClassHeader("Projectile Control", "The damage value is changed from minDamage, maxDamage, DropOffStart, DropOffEnd of the ShooterWeapon", openClose = false)] public class vProjectileControl : vMonoBehaviour { public vBulletLifeSettings bulletLifeSettings; public int bulletLife = 100; public bool debugTrajetory; public bool debugHittedObject; public vDamage damage; public float forceMultiplier = 1; public bool destroyOnCast = true; [Tooltip("Control Trail renderer")] public TrailRenderer trail; public ProjectilePassDamage onPassDamage; public ProjectileCastColliderEvent onCastCollider; public ProjectileCastColliderEvent onDestroyProjectile; public vProjectileInstantiateData instantiateData; internal bool damageByDistance; internal float velocity = 580; internal int minDamage; internal int maxDamage; internal float minDamageDistance = 8f; internal float maxDamageDistance = 50f; internal Vector3 startPosition; internal LayerMask hitLayer = -1; internal List ignoreTags = new List(); internal Transform shooterTransform; protected Vector3 previousPosition; protected Rigidbody _rigidBody; protected Color debugColor = Color.green; protected int debugLife; protected float castDist; protected List trajectoryPositions = new List(); protected virtual void Start() { transform.SetParent(vObjectContainer.root, true); debugLife = bulletLife; _rigidBody = GetComponent(); startPosition = transform.position; previousPosition = transform.position - transform.forward * 0.1f; if (trail) { AddTrailPosition(); } // Log diagnostic: Kiểm tra Layer mà đạn có thể bắn trúng Debug.Log($"PROJECTILE SPAWNED: HitLayer Mask: {hitLayer.value}. Đảm bảo Layer của Enemy nằm trong mask này."); } protected virtual void Update() { RaycastHit hitInfo; if (_rigidBody.linearVelocity.magnitude > 1) { 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) { return; } var dist = Vector3.Distance(startPosition, transform.position) + castDist; if (!(ignoreTags.Contains(hitInfo.collider.gameObject.tag) || (shooterTransform != null && hitInfo.collider.transform.IsChildOf(shooterTransform)))) { if (debugHittedObject) { Debug.Log(hitInfo.collider.gameObject.name, hitInfo.collider); } onCastCollider.Invoke(hitInfo); damage.damageValue = maxDamage; if (damageByDistance) { var result = 0f; var damageDifence = maxDamage - minDamage; //Calc damage per distance if (dist - minDamageDistance >= 0) { int percentComplete = (int)System.Math.Round((double)(100 * (dist - minDamageDistance)) / (maxDamageDistance - minDamageDistance)); result = Mathf.Clamp(percentComplete * 0.01f, 0, 1f); damage.damageValue = maxDamage - (int)(damageDifence * result); } else { damage.damageValue = maxDamage; } } damage.hitPosition = hitInfo.point; damage.receiver = hitInfo.collider.transform; damage.force = transform.forward * damage.damageValue * forceMultiplier; if (damage.damageValue > 0) { onPassDamage.Invoke(damage); // 1. Log khi trúng bất cứ thứ gì Debug.Log($"PROJECTILE HIT: {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(); if (damageReceiver != null) { if (hitInfo.collider.CompareTag("Enemy") || damageReceiver.gameObject.CompareTag("Enemy")) { Debug.Log($"APPLYING DAMAGE TO ENEMY: {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() : null); } else { Debug.LogWarning($"NO DAMAGE RECEIVER FOUND on {hitInfo.collider.name} or its parents. Đảm bảo Enemy có component vHealthController."); } } var rigb = hitInfo.collider.gameObject.GetComponent(); if (rigb && !rigb.isKinematic) { // GIẢM LỰC ĐẨY: Chỉ dùng 10% lực để NPC không bị bay quá xa float realisticForce = (damage.damageValue * forceMultiplier) * 0.1f; rigb.AddForce(transform.forward * realisticForce, ForceMode.Impulse); } startPosition = transform.position; castDist = dist; if (destroyOnCast) { if (bulletLifeSettings) { var bulletLifeInfo = bulletLifeSettings.GetReduceLife(hitInfo.collider.gameObject.tag, hitInfo.collider.gameObject.layer); bulletLife -= bulletLifeInfo.lostLife; if (debugTrajetory) { DrawHitPoint(hitInfo.point); } var crossed = false; if (bulletLife > 0 && !bulletLifeInfo.ricochet) { var position = transform.position = hitInfo.point + transform.forward * 0.001f; if (trail) { trail.AddPosition(transform.position); } if (debugTrajetory) { Debug.DrawLine(transform.position, previousPosition, debugColor, 10f); } for (float i = 0; i <= bulletLifeInfo.maxThicknessToCross; i += 0.01f) { var pointToCheck = position + transform.forward * (i); if (Physics.Linecast(pointToCheck, position)) { hitInfo.point = pointToCheck; hitInfo.normal = transform.forward; onCastCollider.Invoke(hitInfo); crossed = true; break; } } if (crossed) { if (trail) { AddTrailPosition(); } } } if (!crossed && !bulletLifeInfo.ricochet) { bulletLife = 0; transform.position = hitInfo.point; if (debugTrajetory) { Debug.DrawLine(transform.position, previousPosition, debugColor, 10f); } onDestroyProjectile.Invoke(hitInfo); if (trail && trail.gameObject != this.gameObject) { if (trail) { AddTrailPosition(); } trail.transform.SetParent(vObjectContainer.root); } Destroy(gameObject); return; } maxDamage -= (maxDamage) - ((maxDamage * bulletLifeInfo.lostDamage) / 100); minDamage -= (minDamage) - ((minDamage * bulletLifeInfo.lostDamage) / 100); if (maxDamage < 0) { maxDamage = 0; } if (minDamage < 0) { minDamage = 0; } var x = Random.Range(bulletLifeInfo.minChangeTrajectory, bulletLifeInfo.maxChangeTrajectory) * (Random.Range(-1, 1) >= 0 ? 1 : -1); var y = Random.Range(bulletLifeInfo.minChangeTrajectory, bulletLifeInfo.maxChangeTrajectory) * (Random.Range(-1, 1) >= 0 ? 1 : -1); if (y > 60 || y < -60) { x = Mathf.Clamp(x, -15, 15); } if (x != 0 || y != 0) { var dir = Quaternion.Euler(x, y, 0) * _rigidBody.linearVelocity; if (dir != Vector3.zero) { _rigidBody.linearVelocity = dir * (bulletLifeInfo.ricochet ? -1 : 1); transform.forward = dir * (bulletLifeInfo.ricochet ? -1 : 1); } } if (debugTrajetory) { var lostedLifePercent = (bulletLife / (float)debugLife) * 100f; debugColor = lostedLifePercent > 76 ? Color.green : lostedLifePercent > 51 ? Color.yellow : lostedLifePercent > 26 ? new Color(1, .5f, 0) : Color.red; debugColor.a = 0.5f; } } else { bulletLife = 0; } if (bulletLife <= 0 || bulletLifeSettings == null) { transform.position = hitInfo.point; if (debugTrajetory) { Debug.DrawLine(transform.position, previousPosition, debugColor, 10f); } onDestroyProjectile.Invoke(hitInfo); if (trail && trail.gameObject != this.gameObject) { if (trail) { AddTrailPosition(); } trail.transform.SetParent(vObjectContainer.root); } Destroy(gameObject); return; } } } else { transform.position = hitInfo.point + transform.forward * 0.001f; if (trail && trail.gameObject != this.gameObject) { if (trail) { AddTrailPosition(); } } if (debugTrajetory) { Debug.DrawLine(transform.position, previousPosition, debugColor, 10f); } } } else { // DIAGNOSTIC X-RAY: Kiểm tra xem có trúng cái gì mà bị HitLayer cấm không? RaycastHit diagHit; if (Physics.Linecast(previousPosition, transform.position + transform.forward * 0.5f, out diagHit, ~0)) // ~0 là tất cả layer { if (diagHit.collider.CompareTag("Enemy") || diagHit.collider.name.Contains("Guard") || diagHit.collider.GetComponentInParent() != null) { Debug.LogError($"LAYER BLOCK DETECTED! Đạn vừa bay xuyên qua {diagHit.collider.name}. " + $"Đối tượng này ở Layer: {LayerMask.LayerToName(diagHit.collider.gameObject.layer)} ({diagHit.collider.gameObject.layer}). " + $"NHƯNG súng của bạn đang dùng HitLayer Mask: {hitLayer.value}, không bao gồm layer này!"); } } if (debugTrajetory) { Debug.DrawLine(transform.position, previousPosition, debugColor, 10f); } } previousPosition = transform.position; } private void AddTrailPosition() { if (trajectoryPositions.Count > 0) { var lastPosition = trajectoryPositions[trajectoryPositions.Count - 1]; var distance = Vector3.Distance(lastPosition, transform.position); var dir = transform.position - lastPosition; var count = (int)(distance / 0.5f); for (int i = 0; i < count; i++) { trajectoryPositions.Add(lastPosition + dir.normalized * 0.5f); if (debugTrajetory) { Debug.DrawRay(lastPosition, Vector3.up * .1f, Color.red, 10); } lastPosition = lastPosition + dir.normalized * 0.5f; } } else { trajectoryPositions.Add(transform.position); } trail.Clear(); var position = trajectoryPositions.ToArray(); trail.AddPositions(position); } void DrawHitPoint(Vector3 point) { Debug.DrawRay(point, -transform.forward * 0.1f, Color.red, 10f); Debug.DrawRay(point, transform.right * 0.1f, Color.red, 10f); Debug.DrawRay(point, -transform.right * 0.1f, Color.red, 10f); Debug.DrawRay(point, transform.up * 0.1f, Color.red, 10f); Debug.DrawRay(point, -transform.up * 0.1f, Color.red, 10f); } public void RemoveParentOfOther(Transform other) { other.SetParent(vObjectContainer.root, true); } [System.Serializable] public class ProjectileCastColliderEvent : UnityEngine.Events.UnityEvent { } [System.Serializable] public class ProjectilePassDamage : UnityEngine.Events.UnityEvent { } } }