Danil Iarosh 6b02c76ef3
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
refactor: LightController + LightSequence, протокол 85XXYY, namespace
LightController / LightMode:
- Новый LightMode.cs: enum эффектов под протокол 85XXYY (13 эффектов + Off)
  значения enum = коды команд XX, Off=-1 генерирует просто '85'
- Новый LightController.cs: управление подсветкой платы
  * SetLight(mode, duration) → строит команду 85XXYY, возвращает LightSequence
  * Blink() / StopBlink() / SetBlinkInterval() — мигание шляпы
  * StopSequence() / StopAllEffects() — остановка эффектов
  * Все параметры (hatOn/hatOff, blinkInterval, defaultMode/Duration) в инспекторе
- Вложенный класс LightSequence — builder цепочек эффектов:
  SetLight(A,3).Then(B,3).Then(C,3).Loop()
  SetLight(A,3).Then(LightMode.Off)
  SetLight(A,5) — одиночный эффект
GameManager:
- Добавлено свойство Light (→ Plate.Light)
- GameWin   → SetLight(FinalWave, 30)
- GameOver  → SetLight(BlueWave, 30)
- NewGame   → SetLight(Rainbow, 99)
- LoadScene MainMenu → SetLight(Off) + Blink()
namespace (добавлены в файлы без namespace):
- Editor/MouseWorldPositionOverlay.cs  → RR.Common.InspectorOverrides
- UI/StatsWindowUIController.cs        → RR.Common.UI
- Utilities/AdvancedLogging_Test.cs    → RR.Common.Utility
- Utilities/DebugSettings.cs           → RR.Common.Utility
2026-02-19 16:52:59 +03:00

266 lines
12 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 System.Collections.Generic;
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("Время эффекта по умолчанию (секунды, 0–99)")]
[SerializeField] [Range(0, 99)] private int defaultDuration = 3;
// ───────────────────────── Приватное состояние ─────────────────────────
private Coroutine _blinkingCoroutine;
private LightSequence _activeSequence;
private const string EffectPrefix = "85";
// ───────────────────────── Свойства ─────────────────────────
/// <summary>Время эффекта по умолчанию (используется в <see cref="LightSequence.Then(LightMode)"/>).</summary>
public int DefaultDuration => defaultDuration;
// ===========================start: PUBLIC API===========================
/// <summary>
/// Включает эффект по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// </summary>
public LightSequence SetLight() => SetLight(defaultLightMode, defaultDuration);
/// <summary>
/// Включает эффект на время по умолчанию и возвращает <see cref="LightSequence"/> для построения цепочки.
/// </summary>
public LightSequence SetLight(LightMode mode) => SetLight(mode, defaultDuration);
/// <summary>
/// Включает световой эффект на указанное время и возвращает <see cref="LightSequence"/> для построения цепочки.
/// <para>Если вызвать без <c>.Then()</c> / <c>.Loop()</c> — эффект запустится немедленно как одиночный.</para>
/// </summary>
/// <param name="mode">Эффект подсветки</param>
/// <param name="durationSeconds">Время эффекта (0–99 секунд)</param>
public LightSequence SetLight(LightMode mode, int durationSeconds)
{
StopSequence();
_activeSequence = new LightSequence(this, mode, durationSeconds);
_activeSequence.Play();
return _activeSequence;
}
/// <summary>
/// Останавливает текущую активную последовательность эффектов и отключает рендеринг.
/// </summary>
public void StopSequence()
{
_activeSequence?.Stop();
_activeSequence = null;
}
/// <summary>
/// Останавливает все эффекты и отключает рендеринг платы (<c>85</c>).
/// </summary>
public void StopAllEffects()
{
StopSequence();
ComWrapper.Send(EffectPrefix);
}
/// <summary>
/// Запускает мигание шляпы с настроенным интервалом.
/// </summary>
public void Blink() => Blink(blinkInterval);
/// <summary>
/// Запускает мигание шляпы с явно указанным интервалом.
/// </summary>
public void Blink(float interval)
{
StopBlink(enableHat: false);
_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;
}
ComWrapper.Send(enableHat ? hatOnCommand : hatOffCommand);
}
/// <summary>Возвращает <c>true</c>, если мигание шляпы сейчас активно.</summary>
public bool IsBlinking => _blinkingCoroutine != null;
/// <summary>Возвращает <c>true</c>, если последовательность эффектов сейчас активна.</summary>
public bool IsSequencePlaying => _activeSequence?.IsPlaying ?? false;
// ===========================end: PUBLIC API===========================
/// <summary>
/// Отправляет COM-команду для одного шага последовательности.
/// Используется из <see cref="LightSequence"/>.
/// </summary>
public void SendCommand(LightMode mode, int durationSeconds)
{
ComWrapper.Send(BuildCommand(mode, durationSeconds));
}
// ReSharper disable once IteratorNeverReturns — корутина намеренно бесконечная
private IEnumerator BlinkingCoroutine(float interval)
{
while (true)
{
ComWrapper.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 WaitForSeconds(wait);
}
}
private static string BuildCommand(LightMode mode, int durationSeconds)
{
if (mode == LightMode.Off)
return EffectPrefix;
int xx = (int)mode;
int yy = Mathf.Clamp(durationSeconds, 0, 99);
return $"{EffectPrefix}{xx:D2}{yy:D2}";
}
/// <summary>
/// Строитель последовательности световых эффектов.
/// Создаётся через <see cref="LightController.SetLight(LightMode,int)"/>.
/// <example><code>
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop();
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.Off);
/// SetLight(LightMode.Rainbow, 5);
/// </code></example>
/// </summary>
public class LightSequence
{
private readonly struct Step
{
public readonly LightMode Mode;
public readonly int DurationSeconds;
public Step(LightMode mode, int durationSeconds) { Mode = mode; DurationSeconds = durationSeconds; }
}
private readonly List<Step> _steps = new();
private readonly LightController _controller;
private bool _looping;
private Coroutine _coroutine;
internal LightSequence(LightController controller, LightMode firstMode, int firstDuration)
{
_controller = controller;
_steps.Add(new Step(firstMode, firstDuration));
}
/// <summary>Добавляет следующий эффект с временем по умолчанию.</summary>
public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultDuration);
/// <summary>Добавляет следующий эффект с указанным временем.</summary>
public LightSequence Then(LightMode mode, int durationSeconds)
{
_steps.Add(new Step(mode, durationSeconds));
return this;
}
/// <summary>
/// Зацикливает последовательность. Для остановки — <see cref="LightController.StopSequence"/>.
/// </summary>
public LightSequence Loop()
{
_looping = true;
Play();
return this;
}
/// <summary>Запускает последовательность один раз.</summary>
public LightSequence Play()
{
Stop();
_coroutine = _controller.StartCoroutine(RunSequence());
return this;
}
/// <summary>Останавливает выполнение последовательности.</summary>
public void Stop()
{
if (_coroutine == null) return;
_controller.StopCoroutine(_coroutine);
_coroutine = null;
}
/// <summary>Возвращает <c>true</c>, если последовательность сейчас выполняется.</summary>
public bool IsPlaying => _coroutine != null;
// ReSharper disable once IteratorNeverReturns — цикл намеренно бесконечный при _looping=true
private IEnumerator RunSequence()
{
do
{
foreach (Step step in _steps)
{
_controller.SendCommand(step.Mode, step.DurationSeconds);
if (step.Mode != LightMode.Off && step.DurationSeconds > 0)
yield return new WaitForSeconds(step.DurationSeconds);
}
}
while (_looping);
_coroutine = null;
}
}
}
}