Danil Iarosh 77186697f8 Prepare v0.1 gameplay and hardware foundation
Подробные изменения:

- Расширен основной gameplay loop: джетпак, новые платформы, оборудование, враги, прогрессия сложности и ограничения взаимодействий игрока.

- Добавлены Bootstrap-сервисы запуска приложения, защита, фокус окна, управление курсором и настройки сервисов через DI.

- Добавлена COM-интеграция с технической схемой Input System, обработкой кнопок/ADC, подсветкой и консольной диагностикой.

- Добавлен отдельный debug/logging модуль с каналами, обработчиками консоли и файловым выводом.

- Добавлены asmdef, link.xml, EditMode/PlayMode тесты и обновлена дорожная карта проекта.

Проверки:

- dotnet build .\\DanroJump.Runtime.csproj --no-restore

- dotnet build .\\DanroJump.Tests.EditMode.csproj --no-restore

- dotnet build .\\DanroJump.Tests.PlayMode.csproj --no-restore
2026-06-08 14:22:48 +03:00

275 lines
9.1 KiB
C#
Raw 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;
using UnityEngine.U2D.Animation;
namespace DanroJump.Gameplay
{
/// <summary>
/// Базовый класс противника с обработкой trigger- и collision-контакта с игроком.
/// </summary>
[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 Collider2D enemyCollider;
private SpriteSkin[] spriteSkins;
private Renderer[] visualRenderers;
private bool visualComponentsCached;
private bool visualWarningLogged;
/// <summary>
/// Тип мест, где этот prefab врага разрешено создавать.
/// </summary>
public EnemySpawnKind SpawnKind => spawnKind;
/// <summary>
/// True, если врага можно создавать на точках платформ.
/// </summary>
public bool CanSpawnOnPlatform => spawnKind == EnemySpawnKind.GroundedOnly || spawnKind == EnemySpawnKind.Universal;
/// <summary>
/// True, если врага можно создавать процедурно в поле, отдельно от платформ.
/// </summary>
public bool CanSpawnProcedurally => spawnKind == EnemySpawnKind.FlyingOnly || spawnKind == EnemySpawnKind.Universal;
/// <summary>
/// True, если противник сейчас должен реагировать на игрока.
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// Получает игровую сессию для начисления очков за stomp.
/// </summary>
[Zenject.Inject]
public void Construct(GameplaySession injectedSession)
{
session = injectedSession;
}
/// <summary>
/// Активирует противника при включении объекта.
/// </summary>
protected virtual void OnEnable()
{
ConfigureRuntimeVisuals();
ActivateEnemy();
}
/// <summary>
/// Деактивирует противника при выключении объекта.
/// </summary>
protected virtual void OnDisable()
{
DeactivateEnemy();
}
/// <summary>
/// Передает trigger-контакт с игроком в конкретную реализацию.
/// </summary>
private void OnTriggerEnter2D(Collider2D other)
{
if (PlayerContactUtility.TryGetAlivePlayer(other, out var player))
{
HandlePlayerContact(player, this);
}
}
/// <summary>
/// Передает физическое столкновение с игроком в конкретную реализацию.
/// </summary>
private void OnCollisionEnter2D(Collision2D collision)
{
if (PlayerContactUtility.TryGetAlivePlayer(collision.collider, out var player))
{
HandlePlayerContact(player, this);
}
}
/// <summary>
/// Включает противника.
/// </summary>
public virtual void ActivateEnemy()
{
IsActive = true;
}
/// <summary>
/// Выключает противника.
/// </summary>
public virtual void DeactivateEnemy()
{
IsActive = false;
}
/// <summary>
/// Сбрасывает противника к активному состоянию.
/// </summary>
public virtual void ResetEnemy()
{
ConfigureRuntimeVisuals();
ActivateEnemy();
}
/// <summary>
/// Обрабатывает stomp сверху или убивает игрока при обычном контакте.
/// </summary>
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;
}
player.Kill();
}
/// <summary>
/// Реакция врага на успешное уничтожение прыжком сверху.
/// </summary>
protected virtual void HandleStomp(JumpPlayerController player)
{
session?.AddScore(stompScore);
player.Bounce(player.AutoJumpVelocity);
DeactivateEnemy();
gameObject.SetActive(false);
}
/// <summary>
/// Проверяет, что игрок падает и его GroundCheck находится выше верхней части противника.
/// </summary>
protected virtual bool IsStomp(JumpPlayerController player)
{
enemyCollider ??= GetComponent<Collider2D>();
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;
}
/// <summary>
/// Приводит 2D Animation-визуал врага к предсказуемому runtime-режиму для Player build.
/// </summary>
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<SpriteSkin>(true);
visualRenderers = GetComponentsInChildren<Renderer>(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<Collider2D>(true);
foreach (var targetCollider in colliders)
{
if (targetCollider != null)
{
targetCollider.enabled = false;
}
}
}
}
}