using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using DanroJump.Audio;
using UnityEngine;
namespace DanroJump.Gameplay
{
///
/// Пружинная площадка, которая усиливает прыжок игрока и проигрывает squash/stretch-анимацию.
///
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider2D))]
public sealed class JumpPad : PlatformEquipmentBase, ILandingEquipment
{
[SerializeField] [Min(1f)] private float jumpHeightMultiplier = 3f;
[SerializeField] private float landingVelocityThreshold = 0.05f;
[SerializeField] private float landingTopTolerance = 0.2f;
[SerializeField] private float activationCooldown = 0.08f;
[SerializeField] private Transform visualRoot;
[SerializeField] private string visualRootName = "Visual";
[SerializeField] private Vector2 stretchScale = new(0.9f, 1.35f);
[SerializeField] [Min(0f)] private float stretchOutDuration = 0.06f;
[SerializeField] [Min(0f)] private float recoverDuration = 0.14f;
private Collider2D jumpPadCollider;
private IGameAudioService audioService;
private CancellationTokenSource stretchCancellation;
private Vector3 visualBaseScale = Vector3.one;
private float lastActivationTime = -1f;
private bool hasVisualBaseScale;
///
/// Во сколько раз пружина усиливает стандартный прыжок игрока.
///
public float JumpHeightMultiplier => jumpHeightMultiplier;
[Zenject.Inject]
public void Construct([Zenject.InjectOptional] IGameAudioService injectedAudioService = null)
{
audioService = injectedAudioService;
}
///
/// Подготавливает trigger-коллайдер и визуальную часть пружины.
///
private void Awake()
{
EnsureTriggerCollider();
ResolveVisualRoot();
CacheVisualBaseScale();
}
///
/// Проверяет контакт при первом входе игрока в trigger.
///
private void OnTriggerEnter2D(Collider2D other)
{
TryHandlePlayerContact(other);
}
///
/// Повторно проверяет контакт, если игрок уже находится внутри trigger.
///
private void OnTriggerStay2D(Collider2D other)
{
TryHandlePlayerContact(other);
}
///
/// Преобразует Collider2D в игрока и передает контакт в общий handler.
///
private void TryHandlePlayerContact(Collider2D other)
{
if (other.TryGetComponent(out JumpPlayerController player) && !player.IgnoresEnvironmentInteractions)
{
HandlePlayerContact(player, this);
}
}
///
/// Сбрасывает cooldown, коллайдер и визуальный scale при переиспользовании.
///
public override void ResetEquipment()
{
lastActivationTime = -1f;
EnsureTriggerCollider();
ResolveVisualRoot();
CacheVisualBaseScale();
ResetVisualScale();
}
///
/// Пробует активировать пружину от контакта игрока.
///
public override void HandlePlayerContact(JumpPlayerController player, Component source)
{
TryActivate(player);
}
///
/// Активирует усиленный прыжок, если игрок действительно приземляется сверху.
///
/// True, если пружина сработала.
public bool TryActivate(JumpPlayerController player)
{
if (!CanActivate(player))
{
return false;
}
NotifyPlatformLandingHandlers(player);
audioService?.Play(GameAudioEvent.JumpPadActivated, transform.position);
player.BounceWithEnemyPassThrough(CalculateBoostVelocity(player));
lastActivationTime = Time.time;
PlayStretchAnimation();
return true;
}
public bool TryActivateFromLanding(JumpPlayerController player)
{
return TryActivate(player);
}
///
/// Проверяет направление движения, cooldown и высоту GroundCheck относительно верха пружины.
///
private bool CanActivate(JumpPlayerController player)
{
if (player == null || player.IgnoresEnvironmentInteractions || Time.time - lastActivationTime < activationCooldown)
{
return false;
}
var isLanding = player.VerticalVelocity <= landingVelocityThreshold ||
player.PreviousVerticalVelocity <= landingVelocityThreshold;
if (!isLanding)
{
return false;
}
if (jumpPadCollider == null)
{
EnsureTriggerCollider();
}
if (jumpPadCollider == null)
{
return false;
}
var landingLineY = jumpPadCollider.bounds.max.y - landingTopTolerance;
return player.GroundCheckY >= landingLineY || player.PreviousGroundCheckY >= landingLineY;
}
///
/// Переводит множитель высоты в скорость прыжка через квадратный корень.
///
private float CalculateBoostVelocity(JumpPlayerController player)
{
return player.AutoJumpVelocity * Mathf.Sqrt(jumpHeightMultiplier);
}
///
/// Гарантирует, что коллайдер пружины работает как trigger.
///
private void EnsureTriggerCollider()
{
jumpPadCollider = GetComponent();
if (jumpPadCollider != null)
{
jumpPadCollider.isTrigger = true;
}
}
///
/// Находит визуальный корень пружины для stretch-анимации.
///
private void ResolveVisualRoot()
{
if (visualRoot != null)
{
return;
}
visualRoot = transform.Find(visualRootName);
if (visualRoot == null && TryGetComponent(out SpriteRenderer spriteRenderer))
{
visualRoot = spriteRenderer.transform;
}
}
///
/// Запоминает исходный scale визуала, чтобы корректно восстановить его после анимации.
///
private void CacheVisualBaseScale()
{
if (visualRoot == null || hasVisualBaseScale)
{
return;
}
visualBaseScale = visualRoot.localScale;
hasVisualBaseScale = true;
}
///
/// Запускает короткую анимацию растяжения пружины.
///
private void PlayStretchAnimation()
{
ResolveVisualRoot();
CacheVisualBaseScale();
if (visualRoot == null)
{
return;
}
CancelStretchAnimation();
stretchCancellation = new CancellationTokenSource();
StretchVisualAsync(stretchCancellation).Forget(Debug.LogException);
}
///
/// Останавливает активную stretch-анимацию и возвращает базовый scale.
///
private void ResetVisualScale()
{
CancelStretchAnimation();
if (visualRoot != null && hasVisualBaseScale)
{
visualRoot.localScale = visualBaseScale;
}
}
private void OnDestroy()
{
CancelStretchAnimation();
}
private void CancelStretchAnimation()
{
if (stretchCancellation == null)
{
return;
}
try
{
stretchCancellation.Cancel();
}
catch
{
// ignored
}
stretchCancellation.Dispose();
stretchCancellation = null;
}
///
/// Последовательно растягивает визуал и возвращает его к исходному размеру.
///
private async UniTask StretchVisualAsync(CancellationTokenSource cancellationSource)
{
var stretchedScale = new Vector3(
visualBaseScale.x * stretchScale.x,
visualBaseScale.y * stretchScale.y,
visualBaseScale.z);
var token = cancellationSource.Token;
try
{
await AnimateVisualScaleAsync(visualBaseScale, stretchedScale, stretchOutDuration, token);
await AnimateVisualScaleAsync(stretchedScale, visualBaseScale, recoverDuration, token);
if (!token.IsCancellationRequested && visualRoot != null)
{
visualRoot.localScale = visualBaseScale;
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
// ignored
}
finally
{
if (stretchCancellation == cancellationSource)
{
stretchCancellation = null;
cancellationSource.Dispose();
}
}
}
///
/// Интерполирует scale визуального корня за указанное время.
///
private async UniTask AnimateVisualScaleAsync(Vector3 from, Vector3 to, float duration, CancellationToken cancellationToken)
{
if (duration <= 0f)
{
if (visualRoot != null)
{
visualRoot.localScale = to;
}
return;
}
var elapsed = 0f;
while (elapsed < duration)
{
cancellationToken.ThrowIfCancellationRequested();
if (visualRoot == null)
{
return;
}
elapsed += Time.deltaTime;
var t = Mathf.Clamp01(elapsed / duration);
var easedT = 1f - (1f - t) * (1f - t);
visualRoot.localScale = Vector3.LerpUnclamped(from, to, easedT);
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
if (visualRoot != null)
{
visualRoot.localScale = to;
}
}
}
}