Danil Iarosh d39d160d92 Implement physical prize dispensing, service stats, and QR screen timeout
- Добавлен асинхронный процесс аппаратной выдачи физических призов (хоппер и витрина) в GameplayHudController.
- Реализована блокировка выдачи по 10-секундному таймауту датчика хоппера с гарантированной отправкой команды отключения двигателя '300'.
- Реализован автоматический таймаут неактивности на экране QR-кода с последующим переходом в главное меню.
- Расширен сервис статистики PrizeSessionStateService новыми долгосрочными счетчиками (сыгранные игры, очки, монеты, сбои связи COM, физ. выдачи и ошибки).
- Обновлен построитель сводки статистики в меню настроек для отображения новых счетчиков и списка последних 5 сессий.
- Написаны EditMode тесты для проверки инкремента счетчиков и запуска выдачи призов.
2026-06-23 10:36:13 +03:00

119 lines
4.0 KiB
C#

using UnityEngine;
using DanroJump.Audio;
namespace DanroJump.Gameplay
{
/// <summary>
/// Простая пружина платформы, которая задает игроку фиксированный вертикальный отскок.
/// </summary>
public sealed class SpringEquipment : PlatformEquipmentBase, ILandingEquipment
{
[SerializeField] private float bounceVelocity = 22f;
[SerializeField] private float landingVelocityThreshold = 0.05f;
[SerializeField] private float landingTopTolerance = 0.2f;
[SerializeField] private float activationCooldown = 0.08f;
private Collider2D springCollider;
private IGameAudioService audioService;
private float lastActivationTime = -1f;
/// <summary>
/// Вертикальная скорость, которую пружина задает игроку.
/// </summary>
public float BounceVelocity => bounceVelocity;
[Zenject.Inject]
public void Construct([Zenject.InjectOptional] IGameAudioService injectedAudioService = null)
{
audioService = injectedAudioService;
}
/// <summary>
/// Сбрасывает cooldown и кэширует trigger-коллайдер.
/// </summary>
public override void ResetEquipment()
{
lastActivationTime = -1f;
EnsureTriggerCollider();
}
/// <summary>
/// Отталкивает игрока вверх только при падении сверху.
/// </summary>
public override void HandlePlayerContact(JumpPlayerController player, Component source)
{
TryActivate(player);
}
/// <summary>
/// Пробует активировать пружину от контакта игрока.
/// </summary>
public bool TryActivate(JumpPlayerController player)
{
if (player == null || player.IgnoresEnvironmentInteractions)
{
return false;
}
if (!CanActivate(player))
{
return false;
}
NotifyPlatformLandingHandlers(player);
audioService?.Play(GameAudioEvent.SpringActivated, transform.position);
player.BounceWithEnemyPassThrough(bounceVelocity);
lastActivationTime = Time.time;
return true;
}
public bool TryActivateFromLanding(JumpPlayerController player)
{
return TryActivate(player);
}
/// <summary>
/// Проверяет направление движения, cooldown и высоту GroundCheck относительно верха пружины.
/// </summary>
private bool CanActivate(JumpPlayerController player)
{
if (Time.time - lastActivationTime < activationCooldown)
{
return false;
}
var isLanding = player.VerticalVelocity <= landingVelocityThreshold ||
player.PreviousVerticalVelocity <= landingVelocityThreshold;
if (!isLanding)
{
return false;
}
if (springCollider == null)
{
EnsureTriggerCollider();
}
if (springCollider == null)
{
return false;
}
var landingLineY = springCollider.bounds.max.y - landingTopTolerance;
return player.GroundCheckY >= landingLineY || player.PreviousGroundCheckY >= landingLineY;
}
/// <summary>
/// Гарантирует, что коллайдер пружины работает как trigger.
/// </summary>
private void EnsureTriggerCollider()
{
springCollider = GetComponent<Collider2D>();
if (springCollider != null)
{
springCollider.isTrigger = true;
}
}
}
}