using UnityEngine;
using Zenject;
namespace DanroJump.Gameplay
{
///
/// Кастомная one-way логика платформы.
/// Игнорирует столкновение, если GroundCheck игрока ниже верхней границы платформы.
///
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider2D))]
public sealed class OneWayPlatformCollider : MonoBehaviour
{
[SerializeField] private JumpPlayerController player;
[SerializeField] private float topBoundaryPadding;
[SerializeField] private float landingVelocityThreshold = 0.05f;
[SerializeField] private float crossingGraceDistance = 0.8f;
private Collider2D platformCollider;
private BoxCollider2D boxPlatformCollider;
private Collider2D playerCollider;
private float cachedTopY;
private bool isIgnoringPlayerCollision;
///
/// Y-координата верхней границы коллайдера платформы (кэшированная).
///
public float TopY => cachedTopY;
///
/// Вычисляет и сохраняет Y-координату верхней границы платформы.
///
public void UpdateCachedTopY()
{
if (boxPlatformCollider != null)
{
var localTop = boxPlatformCollider.offset + Vector2.up * (boxPlatformCollider.size.y * 0.5f);
cachedTopY = boxPlatformCollider.transform.TransformPoint(localTop).y;
}
else
{
cachedTopY = platformCollider != null ? platformCollider.bounds.max.y : transform.position.y;
}
}
///
/// Получает игрока через DI, чтобы не искать его по сцене на каждой платформе.
///
[Inject]
public void Construct(JumpPlayerController injectedPlayer)
{
player = injectedPlayer;
ResolvePlayerCollider();
}
///
/// Подготавливает Collider2D и отключает стандартный PlatformEffector2D, если он остался на prefab.
///
private void Awake()
{
platformCollider = GetComponent();
boxPlatformCollider = platformCollider as BoxCollider2D;
platformCollider.usedByEffector = false;
if (TryGetComponent(out PlatformEffector2D platformEffector))
{
platformEffector.enabled = false;
}
UpdateCachedTopY();
RefreshColliderState();
}
///
/// После повторного включения сразу синхронизирует IgnoreCollision, не ожидая следующего FixedUpdate.
///
private void OnEnable()
{
UpdateCachedTopY();
RefreshColliderState();
}
///
/// При выключении платформы обязательно возвращает столкновение с игроком.
///
private void OnDisable()
{
SetPlayerCollisionIgnored(false);
}
///
/// Синхронизирует состояние IgnoreCollision с текущей позицией GroundCheck игрока.
///
private void FixedUpdate()
{
if (player == null && !ResolvePlayer())
{
return;
}
// Оптимизация: если игрок взлетает вверх, коллизия гарантированно игнорируется.
// Это избавляет от выполнения сложных вычислений расстояний для всех платформ.
var isFalling = player.VerticalVelocity <= landingVelocityThreshold ||
player.PreviousVerticalVelocity <= landingVelocityThreshold;
if (!isFalling)
{
SetPlayerCollisionIgnored(true);
return;
}
RefreshColliderState();
}
///
/// Обновляет режим столкновения между платформой и игроком.
///
private void RefreshColliderState()
{
if (playerCollider == null && !ResolvePlayerCollider())
{
return;
}
SetPlayerCollisionIgnored(!ShouldCollideWith(player));
}
///
/// Проверяет, должен ли игрок столкнуться с платформой в текущем физическом шаге.
///
/// Игрок, для которого проверяется столкновение.
/// True, если игрок падает сверху или только что пересек верх платформы сверху вниз.
public bool ShouldCollideWith(JumpPlayerController targetPlayer)
{
if (targetPlayer == null)
{
return false;
}
var topY = cachedTopY + topBoundaryPadding;
if (targetPlayer.GroundCheckY >= topY)
{
return true;
}
// Grace-зона закрывает быстрые кадры, когда GroundCheck успел проскочить немного ниже верхней границы.
var crossedFromAbove = targetPlayer.PreviousGroundCheckY >= topY &&
targetPlayer.GroundCheckY >= topY - crossingGraceDistance;
return crossedFromAbove;
}
///
/// Возвращает true, если ссылка на игрока уже получена.
///
private bool ResolvePlayer()
{
return player != null;
}
///
/// Кэширует коллайдер игрока для Physics2D.IgnoreCollision.
///
private bool ResolvePlayerCollider()
{
if (player == null)
{
return false;
}
return player.TryGetComponent(out playerCollider);
}
///
/// Включает или выключает физическое столкновение пары игрок-платформа.
///
private void SetPlayerCollisionIgnored(bool shouldIgnore)
{
if (platformCollider == null || playerCollider == null || isIgnoringPlayerCollision == shouldIgnore)
{
return;
}
Physics2D.IgnoreCollision(platformCollider, playerCollider, shouldIgnore);
isIgnoringPlayerCollision = shouldIgnore;
}
}
}