Danil Iarosh c1d8cb89c9
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: add UI animator, game price display, improve lighting reliability
Добавлены новые UI-компоненты и исправлена надёжность подсветки

- Добавлен UIElementAnimator — универсальный аниматор UI-элементов (покачивание, движение вверх-вниз, пульсация масштаба)
- Добавлен GamePriceDisplay — отображение стоимости игры из настроек (payment.startGameCost, payment.currency)
- Добавлен метод StartIdleEffects() в GameManager для гарантированного запуска мигания шапки и последовательности эффектов
- Исправлена подсветка, не включавшаяся при переходе в главное меню: OnSceneLoaded теперь вызывает StartIdleEffects()
- Исправлено восстановление подсветки при переподключении платы: ComSystemManager использует StartIdleEffects() для MainMenu
- Добавлены LightController.IsHatOn и ResendHatState() для корректного отслеживания состояния шапки
- Исправлено зависание спецэффектов при мигании шапки: LightSequence теперь переотправляет команду эффекта каждые 2 сек
- Убраны дублирующиеся вызовы Blink/SetLight из LoadScene (теперь обрабатывается в OnSceneLoaded)
- Обновлена сцена MainMenu, ассеты шрифтов, удалён старый префаб Coin
2026-03-05 17:03:03 +03:00

252 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using UnityEngine;
using Random = UnityEngine.Random;
namespace RR.Common.Controls
{
/// <summary>
/// Управляет подсветкой периферийной платы по протоколу <c>85XXYY</c>:
/// <list type="bullet">
/// <item><c>XX</c> — код эффекта (<see cref="LightMode"/>)</item>
/// <item><c>YY</c> — время в секундах (00–99)</item>
/// </list>
/// Команда <c>85</c> (без параметров) отключает рендеринг и освобождает память.
/// <para>Поддерживает цепочки эффектов через <see cref="LightSequence"/>:</para>
/// <code>
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop();
/// </code>
/// </summary>
public class LightController : MonoBehaviour
{
// ───────────────────────── Инспектор ─────────────────────────
[Header("Команды шляпы")]
[Tooltip("COM-команда для включения шляпы")]
[SerializeField] private string hatOnCommand = "2131";
[Tooltip("COM-команда для выключения шляпы")]
[SerializeField] private string hatOffCommand = "2130";
[Header("Мигание")]
[Tooltip("Интервал между переключениями при мигании (секунды)")]
[SerializeField] [Range(0.05f, 5f)] private float blinkInterval = 0.5f;
[Tooltip("Случайный разброс интервала (±секунды). 0 = строго равномерно")]
[SerializeField] [Range(0f, 2f)] private float blinkIntervalVariance;
[Header("Световые эффекты")]
[Tooltip("Эффект по умолчанию при вызове SetLight без аргументов")]
[SerializeField] private LightMode defaultLightMode = LightMode.Rainbow;
[Tooltip("Количество циклов эффекта по умолчанию")]
[SerializeField] private float defaultCycles = 1f;
// ───────────────────────── Приватное состояние ─────────────────────────
private Coroutine _blinkingCoroutine;
private LightSequence _activeSequence;
private bool _blinkWasPaused;
private float _pausedBlinkInterval;
private bool _hatOn;
private const string EffectPrefix = "86";
// ───────────────────────── Свойства ─────────────────────────
/// <summary>Количество циклов по умолчанию (используется в <see cref="LightSequence.Then(LightMode)"/>).</summary>
public float DefaultCycles => defaultCycles;
// ===========================start: PUBLIC API===========================
/// <summary>
/// Включает эффект по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// </summary>
public LightSequence SetLight() => SetLight(defaultLightMode, defaultCycles);
/// <summary>
/// Включает эффект с количеством циклов по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// </summary>
public LightSequence SetLight(LightMode mode) => SetLight(mode, defaultCycles);
/// <summary>
/// Включает световой эффект на указанное количество циклов и возвращает <see cref="LightSequence"/> для построения цепочки.
/// <para>Если вызвать без <c>.Then()</c> / <c>.Loop()</c> — эффект запустится немедленно как одиночный.</para>
/// </summary>
/// <param name="mode">Эффект подсветки</param>
/// <param name="cycles">Количество циклов (дробное: 0.5 = половина цикла)</param>
public LightSequence SetLight(LightMode mode, float cycles)
{
StopSequence();
_activeSequence = new LightSequence(this, mode, cycles);
_activeSequence.Play();
return _activeSequence;
}
/// <summary>
/// Останавливает текущую активную последовательность эффектов и отключает рендеринг.
/// </summary>
public void StopSequence()
{
_activeSequence?.Stop();
_activeSequence = null;
}
/// <summary>
/// Останавливает все эффекты и отключает рендеринг платы (<c>85</c>).
/// </summary>
public void StopAllEffects()
{
StopSequence();
ComSystemManager.Send(EffectPrefix + "00");
}
/// <summary>
/// Запускает мигание шляпы с настроенным интервалом.
/// </summary>
public void Blink() => Blink(blinkInterval);
/// <summary>
/// Запускает мигание шляпы с явно указанным интервалом.
/// </summary>
public void Blink(float interval)
{
StopBlink(enableHat: false);
_hatOn = true;
_blinkingCoroutine = StartCoroutine(BlinkingCoroutine(interval));
}
/// <summary>
/// Меняет интервал мигания на лету; перезапускает мигание, если оно активно.
/// </summary>
public void SetBlinkInterval(float interval)
{
blinkInterval = Mathf.Max(0.05f, interval);
if (_blinkingCoroutine != null) Blink(blinkInterval);
}
/// <summary>
/// Останавливает мигание шляпы.
/// </summary>
/// <param name="enableHat">Оставить шляпу включённой после остановки</param>
public void StopBlink(bool enableHat = true)
{
if (_blinkingCoroutine != null)
{
StopCoroutine(_blinkingCoroutine);
_blinkingCoroutine = null;
}
_hatOn = enableHat;
ComSystemManager.Send(enableHat ? hatOnCommand : hatOffCommand);
}
/// <summary>Возвращает <c>true</c>, если мигание шляпы сейчас активно.</summary>
public bool IsBlinking => _blinkingCoroutine != null;
/// <summary>Возвращает <c>true</c>, если шляпа включена (постоянно, без мигания).</summary>
public bool IsHatOn => _hatOn && _blinkingCoroutine == null;
/// <summary>Возвращает <c>true</c>, если последовательность эффектов сейчас активна.</summary>
public bool IsSequencePlaying => _activeSequence?.IsPlaying ?? false;
/// <summary>Возвращает <c>true</c>, если последовательность эффектов приостановлена.</summary>
public bool IsSequencePaused => _activeSequence?.IsPaused ?? false;
/// <summary>Возвращает <c>true</c>, если мигание было приостановлено.</summary>
public bool IsBlinkPaused => _blinkWasPaused;
/// <summary>
/// Повторно отправляет текущее состояние шляпы на плату.
/// Используется при переподключении, когда шляпа была просто включена (не моргала).
/// </summary>
public void ResendHatState()
{
ComSystemManager.Send(_hatOn ? hatOnCommand : hatOffCommand);
}
/// <summary>
/// Приостанавливает все активные эффекты (подсветку и мигание).
/// Вызывается при отвале платы контроллера.
/// </summary>
public void PauseAll()
{
if (_blinkingCoroutine != null)
{
_blinkWasPaused = true;
_pausedBlinkInterval = blinkInterval;
StopCoroutine(_blinkingCoroutine);
_blinkingCoroutine = null;
}
else
{
_blinkWasPaused = false;
}
// _hatOn сохраняется как есть — при Resume будет восстановлено
_activeSequence?.Pause();
}
/// <summary>
/// Возобновляет приостановленные эффекты (подсветку и мигание).
/// Вызывается при переподключении платы контроллера.
/// </summary>
public void ResumeAll()
{
if (_blinkWasPaused)
{
_blinkWasPaused = false;
_blinkingCoroutine = StartCoroutine(BlinkingCoroutine(_pausedBlinkInterval));
}
else if (_hatOn)
{
// Шляпа была просто включена (не моргала) — включаем обратно
ComSystemManager.Send(hatOnCommand);
}
if (_activeSequence != null && _activeSequence.IsPaused)
{
_activeSequence.Resume();
}
}
// ===========================end: PUBLIC API===========================
/// <summary>
/// Отправляет COM-команду для одного шага последовательности.
/// Используется из <see cref="LightSequence"/>.
/// </summary>
public void SendCommand(LightMode mode, int durationSeconds)
{
ComSystemManager.Send(BuildCommand(mode, durationSeconds));
}
// ReSharper disable once IteratorNeverReturns — корутина намеренно бесконечная
private IEnumerator BlinkingCoroutine(float interval)
{
while (true)
{
ComSystemManager.Send(Random.Range(0f, 1f) < 0.5f ? hatOnCommand : hatOffCommand);
float wait = blinkIntervalVariance > 0f
? Mathf.Max(0.05f, interval + Random.Range(-blinkIntervalVariance, blinkIntervalVariance))
: interval;
yield return new WaitForSecondsRealtime(wait);
}
}
private static string BuildCommand(LightMode mode, int durationSeconds)
{
if (mode == LightMode.Off)
return EffectPrefix;
int xx = (int)mode;
if (durationSeconds == -1)
return $"{EffectPrefix}{xx:D2}";
int yy = Mathf.Clamp(durationSeconds, 0, 99);
return $"{EffectPrefix}{xx:D2}{yy:D2}";
}
}
}