using UnityEngine; namespace _Project.Scripts.PlayerUtils { [RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))] public class EnemyAI : MonoBehaviour, IDamageable { [Header("Класс и Профиль Врага")] public BodyClass enemyClass = BodyClass.Trooper; [Tooltip("Если не назначен, сгенерируется автоматически под выбранный класс")] public BodyProfile profile; [Header("Настройки ИИ")] [SerializeField] private float detectionRange = 30f; [SerializeField] private float eyeHeight = 1.5f; [SerializeField] private LayerMask obstacleMask = ~0; public float CurrentHealth { get; private set; } public bool IsDead { get; private set; } // Можно вселяться в любого живого врага (или если у него осталось мало ХП — на твой выбор) public bool CanBePossessed => !IsDead; private Rigidbody _rb; private Transform _playerTransform; private float _fireTimer; private float _strafeTimer; private int _strafeDir = 1; private LineRenderer _sniperLaser; private void Awake() { _rb = GetComponent(); _rb.freezeRotation = true; } private void Start() { if (Player.Instance != null) _playerTransform = Player.Instance.transform; if (profile == null || profile.bodyClass != enemyClass) { profile = BodyProfile.CreateDefault(enemyClass); } CurrentHealth = profile.maxHealth; if (enemyClass == BodyClass.Sniper && _sniperLaser == null) { SetupSniperLaser(); } } private void SetupSniperLaser() { _sniperLaser = gameObject.AddComponent(); _sniperLaser.startWidth = 0.02f; _sniperLaser.endWidth = 0.02f; _sniperLaser.material = new Material(Shader.Find("Sprites/Default")); _sniperLaser.startColor = new Color(1f, 0f, 0f, 0.7f); _sniperLaser.endColor = new Color(1f, 0f, 0f, 0.1f); _sniperLaser.enabled = false; } private void Update() { if (IsDead || _playerTransform == null) return; _fireTimer -= Time.deltaTime; _strafeTimer -= Time.deltaTime; if (_strafeTimer <= 0f) { _strafeDir = Random.value > 0.5f ? 1 : -1; _strafeTimer = Random.Range(0.6f, 1.5f); } Vector3 eyePos = transform.position + Vector3.up * eyeHeight; Vector3 targetPos = _playerTransform.position + Vector3.up * 1.0f; Vector3 toPlayer = targetPos - eyePos; float dist = toPlayer.magnitude; if (dist > detectionRange || !HasLineOfSight(eyePos, targetPos, dist)) { if (_sniperLaser != null) _sniperLaser.enabled = false; return; } // Поворот лицом к игроку по горизонтали Vector3 flatDir = new Vector3(toPlayer.x, 0f, toPlayer.z).normalized; if (flatDir.sqrMagnitude > 0.001f) { transform.rotation = Quaternion.Slerp( transform.rotation, Quaternion.LookRotation(flatDir), Time.deltaTime * 10f); } // Лазер снайпера виден за 0.5 сек до выстрела if (_sniperLaser != null) { _sniperLaser.enabled = _fireTimer < 0.5f; if (_sniperLaser.enabled) { _sniperLaser.SetPosition(0, eyePos); _sniperLaser.SetPosition(1, targetPos); } } // Стрельба по игроку if (dist <= profile.weaponRange && _fireTimer <= 0f) { ShootAtPlayer(eyePos, toPlayer.normalized); } } private void FixedUpdate() { if (IsDead || _playerTransform == null) return; Vector3 eyePos = transform.position + Vector3.up * eyeHeight; Vector3 targetPos = _playerTransform.position + Vector3.up * 1.0f; Vector3 toPlayer = targetPos - eyePos; float dist = toPlayer.magnitude; if (dist > detectionRange || !HasLineOfSight(eyePos, targetPos, dist)) { _rb.linearVelocity = new Vector3(0f, _rb.linearVelocity.y, 0f); return; } Vector3 forward = new Vector3(toPlayer.x, 0f, toPlayer.z).normalized; Vector3 right = Vector3.Cross(Vector3.up, forward); Vector3 moveDir = Vector3.zero; // Тактика передвижения зависит от класса! switch (enemyClass) { case BodyClass.Scout: // Агрессивно сближается зигзагами if (dist > 3.5f) moveDir = (forward + right * (_strafeDir * 0.65f)).normalized; else moveDir = right * _strafeDir; break; case BodyClass.Trooper: // Держит среднюю дистанцию (8-14м) и стрейфит if (dist > 14f) moveDir = forward + right * (_strafeDir * 0.3f); else if (dist < 7f) moveDir = -forward + right * (_strafeDir * 0.5f); else moveDir = right * _strafeDir; moveDir.Normalize(); break; case BodyClass.Juggernaut: // Медленно и неотвратимо идёт напролом if (dist > 4f) moveDir = forward; break; case BodyClass.Sniper: // Держится далеко (отступает, если игрок ближе 15м) if (dist < 15f) moveDir = -forward + right * (_strafeDir * 0.25f); else if (dist > 28f) moveDir = forward; moveDir.Normalize(); break; } Vector3 vel = moveDir * (profile.moveSpeed * 0.8f); _rb.linearVelocity = new Vector3(vel.x, _rb.linearVelocity.y, vel.z); } private bool HasLineOfSight(Vector3 from, Vector3 to, float dist) { Vector3 dir = (to - from).normalized; if (Physics.Raycast(from, dir, out RaycastHit hit, dist, obstacleMask, QueryTriggerInteraction.Ignore)) { if (hit.transform == _playerTransform || hit.transform.root == _playerTransform) return true; return false; } return true; } private void ShootAtPlayer(Vector3 origin, Vector3 baseDir) { _fireTimer = profile.fireRate * 1.6f; int pellets = Mathf.Max(1, profile.pelletsPerShot); float spread = Mathf.Max(1.5f, profile.spreadAngle + 1.5f); // Сдвигаем точку вылета чуть вперёд от глаз врага Vector3 muzzlePos = origin + baseDir * 0.6f; for (int i = 0; i < pellets; i++) { Vector3 dir = Quaternion.Euler( Random.Range(-spread, spread), Random.Range(-spread, spread), 0f) * baseDir; Projectile.Spawn(muzzlePos, dir, profile, false, gameObject, obstacleMask); } } public void TakeDamage(float amount) { if (IsDead) return; CurrentHealth -= amount; if (CurrentHealth <= 0f) { IsDead = true; Destroy(gameObject); } } } }