Souljack/Assets/_Project/Scripts/Enemy/EnemyAI.cs

212 lines
7.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Rigidbody>();
_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<LineRenderer>();
_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);
}
}
}
}