using UnityEngine;
using UnityEngine.U2D.Animation;
using DanroJump.Audio;
namespace DanroJump.Gameplay
{
///
/// Базовый класс противника с обработкой trigger- и collision-контакта с игроком.
///
[RequireComponent(typeof(Collider2D))]
public abstract class EnemyBase : MonoBehaviour, IEnemy, IPlayerContactHandler
{
[SerializeField] private EnemySpawnKind spawnKind = EnemySpawnKind.GroundedOnly;
[SerializeField] [Min(0)] private int stompScore = 10;
[SerializeField] private float stompTopTolerance = 0.2f;
[SerializeField] [Range(0f, 1f)] private float stompMinimumHeightFactor = 0.55f;
private GameplaySession session;
private IGameAudioService audioService;
private Collider2D enemyCollider;
private SpriteSkin[] spriteSkins;
private Renderer[] visualRenderers;
private bool visualComponentsCached;
private bool visualWarningLogged;
///
/// Тип мест, где этот prefab врага разрешено создавать.
///
public EnemySpawnKind SpawnKind => spawnKind;
///
/// True, если врага можно создавать на точках платформ.
///
public bool CanSpawnOnPlatform => spawnKind == EnemySpawnKind.GroundedOnly || spawnKind == EnemySpawnKind.Universal;
///
/// True, если врага можно создавать процедурно в поле, отдельно от платформ.
///
public bool CanSpawnProcedurally => spawnKind == EnemySpawnKind.FlyingOnly || spawnKind == EnemySpawnKind.Universal;
///
/// True, если противник сейчас должен реагировать на игрока.
///
public bool IsActive { get; private set; }
///
/// Получает игровую сессию для начисления очков за stomp.
///
[Zenject.Inject]
public void Construct(
GameplaySession injectedSession,
[Zenject.InjectOptional] IGameAudioService injectedAudioService = null)
{
session = injectedSession;
audioService = injectedAudioService;
}
///
/// Активирует противника при включении объекта.
///
protected virtual void OnEnable()
{
ConfigureRuntimeVisuals();
ActivateEnemy();
}
///
/// Деактивирует противника при выключении объекта.
///
protected virtual void OnDisable()
{
DeactivateEnemy();
}
///
/// Передает trigger-контакт с игроком в конкретную реализацию.
///
private void OnTriggerEnter2D(Collider2D other)
{
if (PlayerContactUtility.TryGetAlivePlayer(other, out var player))
{
HandlePlayerContact(player, this);
}
}
///
/// Передает физическое столкновение с игроком в конкретную реализацию.
///
private void OnCollisionEnter2D(Collision2D collision)
{
if (PlayerContactUtility.TryGetAlivePlayer(collision.collider, out var player))
{
HandlePlayerContact(player, this);
}
}
///
/// Включает противника.
///
public virtual void ActivateEnemy()
{
IsActive = true;
audioService?.Play(GameAudioEvent.EnemySpawned, transform.position);
}
///
/// Выключает противника.
///
public virtual void DeactivateEnemy()
{
IsActive = false;
}
///
/// Сбрасывает противника к активному состоянию.
///
public virtual void ResetEnemy()
{
ConfigureRuntimeVisuals();
ActivateEnemy();
}
///
/// Обрабатывает stomp сверху или убивает игрока при обычном контакте.
///
public virtual void HandlePlayerContact(JumpPlayerController player, Component source)
{
if (!IsActive || player == null || player.IgnoresEnvironmentInteractions)
{
return;
}
if (player.TryIgnoreEnemy(this))
{
return;
}
if (IsStomp(player))
{
HandleStomp(player);
return;
}
audioService?.Play(GameAudioEvent.EnemyAttack, transform.position);
player.Kill();
}
///
/// Реакция врага на успешное уничтожение прыжком сверху.
///
protected virtual void HandleStomp(JumpPlayerController player)
{
session?.AddScore(stompScore);
player.Bounce(player.AutoJumpVelocity);
audioService?.Play(GameAudioEvent.EnemyDefeated, transform.position);
DeactivateEnemy();
gameObject.SetActive(false);
}
///
/// Проверяет, что игрок падает и его GroundCheck находится выше верхней части противника.
///
protected virtual bool IsStomp(JumpPlayerController player)
{
enemyCollider ??= GetComponent();
var bounds = enemyCollider != null
? enemyCollider.bounds
: new Bounds(transform.position, Vector3.one);
var groundCheck = player.GroundCheckPosition;
var isAboveEnemyHorizontally =
groundCheck.x >= bounds.min.x - stompTopTolerance &&
groundCheck.x <= bounds.max.x + stompTopTolerance;
var stompLineY = Mathf.Lerp(bounds.min.y, bounds.max.y, stompMinimumHeightFactor);
var isInUpperZone =
groundCheck.y >= stompLineY - stompTopTolerance ||
player.PreviousGroundCheckY >= bounds.max.y - stompTopTolerance;
return player.VerticalVelocity <= 0.05f &&
isAboveEnemyHorizontally &&
isInUpperZone;
}
///
/// Приводит 2D Animation-визуал врага к предсказуемому runtime-режиму для Player build.
///
private void ConfigureRuntimeVisuals()
{
CacheVisualComponentsIfNeeded();
if (spriteSkins != null)
{
foreach (var spriteSkin in spriteSkins)
{
if (spriteSkin == null)
{
continue;
}
spriteSkin.alwaysUpdate = true;
spriteSkin.forceCpuDeformation = true;
}
}
if (visualRenderers == null)
{
return;
}
foreach (var visualRenderer in visualRenderers)
{
if (visualRenderer != null)
{
visualRenderer.allowOcclusionWhenDynamic = false;
}
}
if (!HasUsableVisual())
{
DisableEnemyColliders();
if (!visualWarningLogged)
{
Debug.LogWarning($"{name} enemy has no usable visual renderer. Enemy colliders were disabled.", this);
visualWarningLogged = true;
}
}
}
private void CacheVisualComponentsIfNeeded()
{
if (visualComponentsCached)
{
return;
}
spriteSkins = GetComponentsInChildren(true);
visualRenderers = GetComponentsInChildren(true);
visualComponentsCached = true;
}
private bool HasUsableVisual()
{
if (visualRenderers == null || visualRenderers.Length == 0)
{
return false;
}
foreach (var visualRenderer in visualRenderers)
{
if (visualRenderer == null)
{
continue;
}
if (visualRenderer is SpriteRenderer spriteRenderer)
{
if (spriteRenderer.sprite != null)
{
return true;
}
continue;
}
return true;
}
return false;
}
private void DisableEnemyColliders()
{
var colliders = GetComponentsInChildren(true);
foreach (var targetCollider in colliders)
{
if (targetCollider != null)
{
targetCollider.enabled = false;
}
}
}
}
}