Danil Iarosh cefb2fd345 Build initial vertical platformer foundation
Подробные изменения:
- Добавлены сцены Bootstrap, MainMenu и Game с текущей вертикальной 9:16 структурой проекта.
- Добавлены runtime-скрипты gameplay-архитектуры: DI через Extenject, спавнер платформ, прогрессия этапов, one-way платформы, игрок, камера и оборудование JumpPad.
- Добавлены ScriptableObject-этапы сложности, weighted-настройки платформ, оборудования, противников и collectibles.
- Добавлены UI Toolkit ресурсы главного меню, preview-материалы, шрифты, спрайты, анимации и игровые prefab-ассеты.
- Перенесена и оформлена новая COM-система для работы с аппаратными портами и StreamingAssets.
- Обновлены проектные гайды, AGENTS.md, правила Git/Git LFS, .gitignore, Unity Packages и ProjectSettings.
- Удалён локальный skill start-required-mcp-servers из репозитория.
2026-05-11 07:41:14 +03:00

207 lines
6.0 KiB
C#

using System.Collections;
using UnityEngine;
namespace DanroJump.Gameplay
{
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider2D))]
public sealed class JumpPad : PlatformEquipmentBase
{
[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 Coroutine stretchRoutine;
private Vector3 visualBaseScale = Vector3.one;
private float lastActivationTime = -1f;
private bool hasVisualBaseScale;
public float JumpHeightMultiplier => jumpHeightMultiplier;
private void Awake()
{
EnsureTriggerCollider();
ResolveVisualRoot();
CacheVisualBaseScale();
}
private void OnTriggerEnter2D(Collider2D other)
{
TryHandlePlayerContact(other);
}
private void OnTriggerStay2D(Collider2D other)
{
TryHandlePlayerContact(other);
}
private void TryHandlePlayerContact(Collider2D other)
{
if (other.TryGetComponent(out JumpPlayerController player))
{
HandlePlayerContact(player, this);
}
}
public override void ResetEquipment()
{
lastActivationTime = -1f;
EnsureTriggerCollider();
ResolveVisualRoot();
CacheVisualBaseScale();
ResetVisualScale();
}
public override void HandlePlayerContact(JumpPlayerController player, Component source)
{
TryActivate(player);
}
public bool TryActivate(JumpPlayerController player)
{
if (!CanActivate(player))
{
return false;
}
player.Bounce(CalculateBoostVelocity(player));
lastActivationTime = Time.time;
PlayStretchAnimation();
return true;
}
private bool CanActivate(JumpPlayerController player)
{
if (player == null || Time.time - lastActivationTime < activationCooldown)
{
return false;
}
if (player.VerticalVelocity > landingVelocityThreshold)
{
return false;
}
if (jumpPadCollider == null)
{
EnsureTriggerCollider();
}
return jumpPadCollider != null && player.GroundCheckY >= jumpPadCollider.bounds.min.y - landingTopTolerance;
}
private float CalculateBoostVelocity(JumpPlayerController player)
{
return player.AutoJumpVelocity * Mathf.Sqrt(jumpHeightMultiplier);
}
private void EnsureTriggerCollider()
{
jumpPadCollider = GetComponent<Collider2D>();
if (jumpPadCollider != null)
{
jumpPadCollider.isTrigger = true;
}
}
private void ResolveVisualRoot()
{
if (visualRoot != null)
{
return;
}
visualRoot = transform.Find(visualRootName);
if (visualRoot == null && TryGetComponent(out SpriteRenderer spriteRenderer))
{
visualRoot = spriteRenderer.transform;
}
}
private void CacheVisualBaseScale()
{
if (visualRoot == null || hasVisualBaseScale)
{
return;
}
visualBaseScale = visualRoot.localScale;
hasVisualBaseScale = true;
}
private void PlayStretchAnimation()
{
ResolveVisualRoot();
CacheVisualBaseScale();
if (visualRoot == null)
{
return;
}
if (stretchRoutine != null)
{
StopCoroutine(stretchRoutine);
}
stretchRoutine = StartCoroutine(StretchVisualRoutine());
}
private void ResetVisualScale()
{
if (stretchRoutine != null)
{
StopCoroutine(stretchRoutine);
stretchRoutine = null;
}
if (visualRoot != null && hasVisualBaseScale)
{
visualRoot.localScale = visualBaseScale;
}
}
private IEnumerator StretchVisualRoutine()
{
var stretchedScale = new Vector3(
visualBaseScale.x * stretchScale.x,
visualBaseScale.y * stretchScale.y,
visualBaseScale.z);
yield return AnimateVisualScale(visualBaseScale, stretchedScale, stretchOutDuration);
yield return AnimateVisualScale(stretchedScale, visualBaseScale, recoverDuration);
visualRoot.localScale = visualBaseScale;
stretchRoutine = null;
}
private IEnumerator AnimateVisualScale(Vector3 from, Vector3 to, float duration)
{
if (duration <= 0f)
{
visualRoot.localScale = to;
yield break;
}
var elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
var t = Mathf.Clamp01(elapsed / duration);
var easedT = 1f - (1f - t) * (1f - t);
visualRoot.localScale = Vector3.LerpUnclamped(from, to, easedT);
yield return null;
}
visualRoot.localScale = to;
}
}
}