Danil Iarosh 79b66b2d17
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: add controller disconnect/reconnect handling, MAC identification, lighting pause/resume system
- Рефакторинг системы подсветки: вместо длительности в секундах теперь
  используется количество циклов (float). Добавлена таблица длительности
  циклов эффектов `LightCycleDurations` для пересчёта циклов в секунды.
  Префикс команд подсветки изменён с `85` на `86`.
- `LightSequence`: добавлены `Pause()` / `Resume()` с запоминанием текущего
  шага последовательности; возобновление продолжает с сохранённой позиции.
- `LightController`: добавлены `PauseAll()` / `ResumeAll()` для
  приостановки и восстановления подсветки и мигания шляпы при
  отключении/переподключении платы. Свойства `IsSequencePaused`,
  `IsBlinkPaused`.
- `ComDeviceClient`: после успешной авторизации отправляются команды
  `890003` и `8600`. Добавлен запрос MAC-адреса через команду `06`
  (ответ — 12 hex-символов без префикса). Новое событие `ConnectionLost`
  при начале KeepAlive-реконнекта.
- `ComHub`: очередь `_disconnectedQueue` с типом `DeviceDisconnectMessage`
  (DeviceId + Role + Mac) — сохраняет идентификатор устройства до его
  удаления из словаря. Подписка на `ConnectionLost`.
  Добавлен `GetDeviceMac()`.
- `ComSystemManager`: при инициализации, если плата не обнаружена,
  подсветка сразу ставится на паузу. При отвале дефолтного устройства —
  `PauseAll()`, при переподключении — сравнение по MAC и роли через
  `IsSameDevice()`: та же плата → `ResumeAll()`, другая → `Blink()` с нуля.
  Добавлены событие `OnDeviceDisconnected` и метод `GetDeviceMac()`.
- `COMInput`: при событии `OnDeviceDisconnected` сбрасываются все состояния
  кнопок через `ResetAllButtons()`, генерируя `buttonUp` для зажатых кнопок.
  Устраняет баг залипания управления после переподключения платы.
- `GameManager`: расширена последовательность световых эффектов в меню
  (добавлены `DiagonalWave3`, `WaveUp`, `OrangeWave`); вызовы `SetLight`
  для game over/won переведены на float-циклы.
- Обновлены `ARCHITECTURE.md` и `README.md`: переписана документация
  на актуальную структуру проекта и текущие возможности игры.
2026-03-04 13:55:08 +03:00

132 lines
4.9 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;
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;
// 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)
yield return new WaitForSecondsRealtime(waitSeconds);
}
_currentStepIndex = 0;
}
while (_looping);
_coroutine = null;
}
}
}