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

158 lines
6.5 KiB
C#
Raw Permalink 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 System.Collections.Generic;
using UnityEngine;
namespace RR.Common.Controls
{
/// <summary>
/// Строитель последовательности световых эффектов.
/// Создаётся через <see cref="LightController"/>.
/// <example><code>
/// SetLight(LightMode.VerticalWave, 3f).Then(LightMode.DiagonalWave, 1.5f).Loop();
/// SetLight(LightMode.VerticalWave, 3f).Then(LightMode.Off);
/// SetLight(LightMode.Rainbow, 2f);
/// </code></example>
/// </summary>
public class LightSequence
{
private readonly struct Step
{
public readonly LightMode Mode;
public readonly float Cycles;
public Step(LightMode mode, float cycles) { Mode = mode; Cycles = cycles; }
}
private readonly List<Step> _steps = new();
private readonly LightController _controller;
private bool _looping;
private Coroutine _coroutine;
private int _currentStepIndex;
private bool _paused;
internal LightSequence(LightController controller, LightMode firstMode, float firstCycles)
{
_controller = controller;
_steps.Add(new Step(firstMode, firstCycles));
}
/// <summary>Добавляет следующий эффект с количеством циклов по умолчанию.</summary>
public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultCycles);
/// <summary>Добавляет следующий эффект с указанным количеством циклов.</summary>
public LightSequence Then(LightMode mode, float cycles)
{
_steps.Add(new Step(mode, cycles));
return this;
}
/// <summary>
/// Зацикливает последовательность. Для остановки — <see cref="LightController.StopSequence"/>.
/// </summary>
public LightSequence Loop()
{
_looping = true;
Play();
return this;
}
/// <summary>Запускает последовательность один раз.</summary>
public LightSequence Play()
{
Stop();
_currentStepIndex = 0;
_paused = false;
_coroutine = _controller.StartCoroutine(RunSequence());
return this;
}
/// <summary>Останавливает выполнение последовательности.</summary>
public void Stop()
{
if (_coroutine == null) return;
_controller.StopCoroutine(_coroutine);
_coroutine = null;
_paused = false;
}
/// <summary>
/// Приостанавливает последовательность с сохранением текущей позиции.
/// </summary>
public void Pause()
{
if (_coroutine == null) return;
_controller.StopCoroutine(_coroutine);
_coroutine = null;
_paused = true;
}
/// <summary>
/// Возобновляет приостановленную последовательность с сохранённой позиции.
/// </summary>
public void Resume()
{
if (!_paused) return;
_paused = false;
_coroutine = _controller.StartCoroutine(RunSequence());
}
/// <summary>Возвращает <c>true</c>, если последовательность сейчас выполняется.</summary>
public bool IsPlaying => _coroutine != null;
/// <summary>Возвращает <c>true</c>, если последовательность приостановлена.</summary>
public bool IsPaused => _paused;
/// <summary>
/// Интервал переотправки команды эффекта (секунды).
/// Нужно, чтобы команды шляпы (мигание) не «заглушали» длительные эффекты:
/// плата получает напоминание об активном эффекте каждые N секунд.
/// </summary>
private const float ResendInterval = 2f;
// ReSharper disable once IteratorNeverReturns — цикл намеренно бесконечный при _looping=true
private IEnumerator RunSequence()
{
do
{
for (int i = _currentStepIndex; i < _steps.Count; i++)
{
_currentStepIndex = i;
Step step = _steps[i];
int commandSeconds = LightCycleDurations.CyclesToCommandSeconds(step.Mode, step.Cycles);
_controller.SendCommand(step.Mode, commandSeconds);
float waitSeconds = LightCycleDurations.GetCycleSeconds(step.Mode) * step.Cycles;
if (step.Mode != LightMode.Off && waitSeconds > 0f)
{
// Разбиваем ожидание на короткие интервалы и периодически
// переотправляем команду эффекта, чтобы параллельные команды
// шляпы не сбрасывали текущий эффект на плате.
float elapsed = 0f;
while (elapsed < waitSeconds)
{
float chunk = Mathf.Min(ResendInterval, waitSeconds - elapsed);
yield return new WaitForSecondsRealtime(chunk);
elapsed += chunk;
// Переотправляем, если ещё не дождались конца шага
if (elapsed < waitSeconds)
{
int remainSeconds = Mathf.CeilToInt(waitSeconds - elapsed);
if (remainSeconds > 0)
_controller.SendCommand(step.Mode, remainSeconds);
}
}
}
}
_currentStepIndex = 0;
}
while (_looping);
_coroutine = null;
}
}
}