Подробные изменения: - Добавлены сцены 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 из репозитория.
207 lines
6.0 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|