Подробный список изменений: - Перенесены эффекты частиц из C#-кода в готовые префабы-ассеты (Vfx Prefabs) в папке Assets/Resources/Vfx/ с автоматической генерацией материалов в Editor. - Исправлены ошибки duration на играющих системах частиц и розовые текстуры в GameVfxService.cs. - Исправлена навигация и фокус кнопок на оверлеях ContinueGameOverlay и PrizeResultOverlay с автоматическим подсвечиванием кнопок лимонно-зеленым цветом при фокусе и проигрыванием звука. - Добавлена проверка настроек в GameplayHudController.cs для скрытия ContinueGameOverlay, если продолжение игры отключено. - Расширен сбор статистики: создана структура GameplaySessionRecord, добавлена история до 50 раундов в PrizeSessionStateService и JsonPrizeSessionStateRepository, выведен расчет средних показателей и список 5 последних раундов в сервисной статистике. - Повышена надежность COM-транспорта: добавлен sleep (80мс) в Dispose для graceful shutdown потоков, реализован цикл переподключения до 10 попыток при обрыве связи. - Интегрированы настройки громкости с микшером: изменен шаг изменения громкости на 5% (0.05) для удобства, добавлен статус-чек и логи в ApplyServiceVolumes, расширены юнит-тесты на GetFloat параметры. - Созданы новые архитектурные гайды по аппаратному COM-транспорту и призовой экономике автомата в папке GUIDES/architecture/. - Оптимизирован и обновлен RAG-индекс проекта.
670 lines
24 KiB
C#
670 lines
24 KiB
C#
using System;
|
||
using System.Threading;
|
||
using Cysharp.Threading.Tasks;
|
||
using DanroJump.Audio;
|
||
using DanroJump.Gameplay;
|
||
using DanroJump.Hardware.Com;
|
||
using DanroJump.Prizes;
|
||
using DanroJump.Prizes.Qr;
|
||
using DanroJump.SceneFlow;
|
||
using DanroJump.Settings;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
using Zenject;
|
||
|
||
namespace DanroJump.UI.Gameplay
|
||
{
|
||
/// <summary>
|
||
/// Отображает runtime-показатели игрового забега в UI Toolkit HUD.
|
||
/// </summary>
|
||
public sealed class GameplayHudController : MonoBehaviour
|
||
{
|
||
[SerializeField] private VisualTreeAsset visualTreeAsset;
|
||
[SerializeField] private PanelSettings panelSettings;
|
||
[SerializeField] private int sortingOrder = 10;
|
||
[SerializeField] private string scoreLabelName = "ScoreValue";
|
||
[SerializeField] private string heightLabelName = "HeightValue";
|
||
[SerializeField] private string bestHeightLabelName = "BestHeightValue";
|
||
[SerializeField] private string livesLabelName = "LivesValue";
|
||
[SerializeField] private string coinsLabelName = "CoinsValue";
|
||
[SerializeField] private string deathsLabelName = "DeathsValue";
|
||
[SerializeField] private string stateLabelName = "StateValue";
|
||
[SerializeField] private string developmentStatsRootName = "RightStats";
|
||
|
||
private UIDocument document;
|
||
private GameplaySession session;
|
||
private IPrizeFlowService prizeFlow;
|
||
private IPrizeSessionStateService prizeSessionState;
|
||
private IPrizeQrService prizeQrService;
|
||
private ISceneFlowService sceneFlow;
|
||
private IReadOnlyServiceSettings serviceSettings;
|
||
private IGameAudioService audioService;
|
||
private IComSystem comSystem;
|
||
private PrizeResultOverlayWindow prizeResultOverlay;
|
||
private ContinueGameOverlayWindow continueGameOverlay;
|
||
private VisualElement prizeOverlayRoot;
|
||
private VisualElement continueOverlayRoot;
|
||
private VisualElement developmentStatsRoot;
|
||
private Label scoreLabel;
|
||
private Label heightLabel;
|
||
private Label bestHeightLabel;
|
||
private Label livesLabel;
|
||
private Label coinsLabel;
|
||
private Label deathsLabel;
|
||
private Label stateLabel;
|
||
private CancellationTokenSource refreshCancellation;
|
||
private CancellationTokenSource qrTimeoutCancellation;
|
||
private CancellationTokenSource physicalDispenseCancellation;
|
||
private bool isSubscribedToSession;
|
||
private bool isSubscribedToPrizeFlow;
|
||
|
||
/// <summary>
|
||
/// Получает игровую сессию из DI-контейнера сцены.
|
||
/// </summary>
|
||
[Inject]
|
||
public void Construct(
|
||
GameplaySession injectedSession,
|
||
[InjectOptional] IPrizeFlowService injectedPrizeFlow = null,
|
||
[InjectOptional] IPrizeSessionStateService injectedPrizeSessionState = null,
|
||
[InjectOptional] IPrizeQrService injectedPrizeQrService = null,
|
||
[InjectOptional] ISceneFlowService injectedSceneFlow = null,
|
||
[InjectOptional] IReadOnlyServiceSettings injectedServiceSettings = null,
|
||
[InjectOptional] IGameAudioService injectedAudioService = null,
|
||
[InjectOptional] IComSystem injectedComSystem = null)
|
||
{
|
||
prizeFlow = injectedPrizeFlow;
|
||
prizeSessionState = injectedPrizeSessionState;
|
||
prizeQrService = injectedPrizeQrService;
|
||
sceneFlow = injectedSceneFlow;
|
||
serviceSettings = injectedServiceSettings;
|
||
audioService = injectedAudioService;
|
||
comSystem = injectedComSystem;
|
||
SetSession(injectedSession);
|
||
SubscribeToPrizeFlow();
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
EnsureDocument();
|
||
BindLabels();
|
||
BindPrizeOverlay();
|
||
BindContinueOverlay();
|
||
ShowLastPrizeResultIfNeeded();
|
||
Refresh();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Подписывается на изменения сессии и запускает кадровую подстраховку обновления HUD.
|
||
/// </summary>
|
||
private void OnEnable()
|
||
{
|
||
SubscribeToSession();
|
||
SubscribeToPrizeFlow();
|
||
|
||
if (refreshCancellation == null)
|
||
{
|
||
refreshCancellation = new CancellationTokenSource();
|
||
RefreshContinuouslyAsync(refreshCancellation.Token).Forget(Debug.LogException);
|
||
}
|
||
|
||
Refresh();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Останавливает обновление HUD и снимает подписку с игровой сессии.
|
||
/// </summary>
|
||
private void OnDisable()
|
||
{
|
||
if (refreshCancellation != null)
|
||
{
|
||
refreshCancellation.Cancel();
|
||
refreshCancellation.Dispose();
|
||
refreshCancellation = null;
|
||
}
|
||
|
||
if (qrTimeoutCancellation != null)
|
||
{
|
||
qrTimeoutCancellation.Cancel();
|
||
qrTimeoutCancellation.Dispose();
|
||
qrTimeoutCancellation = null;
|
||
}
|
||
|
||
if (physicalDispenseCancellation != null)
|
||
{
|
||
physicalDispenseCancellation.Cancel();
|
||
physicalDispenseCancellation.Dispose();
|
||
physicalDispenseCancellation = null;
|
||
}
|
||
|
||
UnsubscribeFromSession();
|
||
UnsubscribeFromPrizeFlow();
|
||
prizeResultOverlay?.Dispose();
|
||
prizeResultOverlay = null;
|
||
prizeOverlayRoot = null;
|
||
continueGameOverlay?.Dispose();
|
||
continueGameOverlay = null;
|
||
continueOverlayRoot = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Создает или настраивает UIDocument, чтобы HUD мог жить на отдельном GameObject без ручной сборки.
|
||
/// </summary>
|
||
private void EnsureDocument()
|
||
{
|
||
if (document == null && !TryGetComponent(out document))
|
||
{
|
||
document = gameObject.AddComponent<UIDocument>();
|
||
}
|
||
|
||
if (document == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
document.visualTreeAsset = visualTreeAsset;
|
||
document.panelSettings = panelSettings;
|
||
document.sortingOrder = sortingOrder;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Находит текстовые поля в UXML по именам, заданным в инспекторе.
|
||
/// </summary>
|
||
private void BindLabels()
|
||
{
|
||
var root = document != null ? document.rootVisualElement : null;
|
||
if (root == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
developmentStatsRoot = root.Q<VisualElement>(developmentStatsRootName);
|
||
scoreLabel = FindLabel(root, scoreLabelName);
|
||
heightLabel = FindLabel(root, heightLabelName);
|
||
bestHeightLabel = FindLabel(root, bestHeightLabelName);
|
||
livesLabel = FindLabel(root, livesLabelName);
|
||
coinsLabel = FindLabel(root, coinsLabelName);
|
||
deathsLabel = FindLabel(root, deathsLabelName);
|
||
stateLabel = FindLabel(root, stateLabelName);
|
||
}
|
||
|
||
private void BindPrizeOverlay()
|
||
{
|
||
var root = document != null ? document.rootVisualElement : null;
|
||
var overlayRoot = root?.Q<VisualElement>("PrizeResultOverlay");
|
||
if (overlayRoot == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (prizeResultOverlay != null && prizeOverlayRoot == overlayRoot)
|
||
{
|
||
return;
|
||
}
|
||
|
||
prizeResultOverlay?.Dispose();
|
||
prizeOverlayRoot = overlayRoot;
|
||
prizeResultOverlay = new PrizeResultOverlayWindow(overlayRoot, audioService);
|
||
prizeResultOverlay.ReturnRequested += HandlePrizeReturnRequested;
|
||
}
|
||
|
||
private void BindContinueOverlay()
|
||
{
|
||
var root = document != null ? document.rootVisualElement : null;
|
||
var overlayRoot = root?.Q<VisualElement>("ContinueGameOverlay");
|
||
if (overlayRoot == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (continueGameOverlay != null && continueOverlayRoot == overlayRoot)
|
||
{
|
||
return;
|
||
}
|
||
|
||
continueGameOverlay?.Dispose();
|
||
continueOverlayRoot = overlayRoot;
|
||
continueGameOverlay = new ContinueGameOverlayWindow(overlayRoot, audioService);
|
||
continueGameOverlay.ContinueRequested += HandleContinueRequested;
|
||
continueGameOverlay.MenuRequested += HandleContinueMenuRequested;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновляет все значения HUD из текущей игровой сессии.
|
||
/// </summary>
|
||
private void Refresh()
|
||
{
|
||
RefreshDevelopmentStatsVisibility();
|
||
|
||
if (session == null)
|
||
{
|
||
SetLabel(stateLabel, "WAITING");
|
||
return;
|
||
}
|
||
|
||
SetLabel(scoreLabel, session.Score.ToString());
|
||
SetLabel(heightLabel, $"{session.MaxRunHeight:0} м");
|
||
SetLabel(bestHeightLabel, $"{session.RecordHeight:0} м");
|
||
SetLabel(livesLabel, session.RemainingLives.ToString());
|
||
SetLabel(coinsLabel, session.Coins.ToString());
|
||
SetLabel(deathsLabel, session.DeathCount.ToString());
|
||
SetLabel(stateLabel, FormatState(session.State));
|
||
RefreshContinueOverlay();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Безопасно обновляет текст label, если элемент найден в UXML.
|
||
/// </summary>
|
||
private static void SetLabel(Label label, string value)
|
||
{
|
||
if (label != null)
|
||
{
|
||
label.text = value;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ищет Label по имени, заданному в настройках контроллера.
|
||
/// </summary>
|
||
private static Label FindLabel(VisualElement root, string labelName)
|
||
{
|
||
return root != null && !string.IsNullOrEmpty(labelName)
|
||
? root.Q<Label>(labelName)
|
||
: null;
|
||
}
|
||
|
||
private void RefreshDevelopmentStatsVisibility()
|
||
{
|
||
if (developmentStatsRoot == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
developmentStatsRoot.style.display = ShouldShowDevelopmentStats
|
||
? DisplayStyle.Flex
|
||
: DisplayStyle.None;
|
||
}
|
||
|
||
private static bool ShouldShowDevelopmentStats => Debug.isDebugBuild || Application.isEditor;
|
||
|
||
/// <summary>
|
||
/// Подстраховывает HUD от событий, которые могли прийти до подписки.
|
||
/// </summary>
|
||
private async UniTask RefreshContinuouslyAsync(CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
while (enabled && !cancellationToken.IsCancellationRequested)
|
||
{
|
||
Refresh();
|
||
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
|
||
}
|
||
}
|
||
catch (System.OperationCanceledException)
|
||
{
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Преобразует состояние сессии в короткий текст для HUD.
|
||
/// </summary>
|
||
private static string FormatState(GameplaySessionState state)
|
||
{
|
||
return state switch
|
||
{
|
||
GameplaySessionState.Running => "RUN",
|
||
GameplaySessionState.Respawning => "RESPAWN",
|
||
GameplaySessionState.GameOver => "GAME OVER",
|
||
_ => "WAITING",
|
||
};
|
||
}
|
||
|
||
private void SetSession(GameplaySession injectedSession)
|
||
{
|
||
if (session == injectedSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UnsubscribeFromSession();
|
||
session = injectedSession;
|
||
SubscribeToSession();
|
||
Refresh();
|
||
}
|
||
|
||
private void SubscribeToSession()
|
||
{
|
||
if (!isActiveAndEnabled || session == null || isSubscribedToSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
session.Changed += Refresh;
|
||
isSubscribedToSession = true;
|
||
}
|
||
|
||
private void UnsubscribeFromSession()
|
||
{
|
||
if (session == null || !isSubscribedToSession)
|
||
{
|
||
return;
|
||
}
|
||
|
||
session.Changed -= Refresh;
|
||
isSubscribedToSession = false;
|
||
}
|
||
|
||
private void SubscribeToPrizeFlow()
|
||
{
|
||
if (!isActiveAndEnabled || prizeFlow == null || isSubscribedToPrizeFlow)
|
||
{
|
||
return;
|
||
}
|
||
|
||
prizeFlow.PrizeFlowStarted += HandlePrizeFlowStarted;
|
||
isSubscribedToPrizeFlow = true;
|
||
}
|
||
|
||
private void UnsubscribeFromPrizeFlow()
|
||
{
|
||
if (prizeFlow == null || !isSubscribedToPrizeFlow)
|
||
{
|
||
return;
|
||
}
|
||
|
||
prizeFlow.PrizeFlowStarted -= HandlePrizeFlowStarted;
|
||
isSubscribedToPrizeFlow = false;
|
||
}
|
||
|
||
private void HandlePrizeFlowStarted(PrizeFlowResult result)
|
||
{
|
||
if (!result.HasPrize)
|
||
{
|
||
return;
|
||
}
|
||
|
||
audioService?.Play(result.RewardKind == PrizeRewardKind.QRCode
|
||
? GameAudioEvent.PrizeQrStarted
|
||
: GameAudioEvent.PrizePhysicalStarted);
|
||
audioService?.Play(GameAudioEvent.PrizeStarted);
|
||
audioService?.PlayMusic(GameMusicCue.Prize);
|
||
EnsureDocument();
|
||
BindPrizeOverlay();
|
||
|
||
PrizeQrTicket qrTicket = null;
|
||
var qrError = string.Empty;
|
||
if (result.RewardKind == PrizeRewardKind.QRCode && prizeQrService != null)
|
||
{
|
||
prizeQrService.TryCreateTicket(result, out qrTicket, out qrError);
|
||
if (qrTicket == null)
|
||
{
|
||
prizeSessionState?.FailActiveSession(string.IsNullOrWhiteSpace(qrError)
|
||
? "Не удалось подготовить QR-приз."
|
||
: qrError);
|
||
}
|
||
}
|
||
|
||
prizeResultOverlay?.SetResult(
|
||
result,
|
||
serviceSettings?.GetString(ServiceSettingsKeys.BigPrize),
|
||
serviceSettings?.GetString(ServiceSettingsKeys.PrizePhoneNumber),
|
||
qrTicket,
|
||
qrError);
|
||
prizeResultOverlay?.Open();
|
||
if (qrTicket != null || result.RewardKind != PrizeRewardKind.QRCode)
|
||
{
|
||
prizeSessionState?.MarkActiveSessionShown();
|
||
}
|
||
|
||
if (result.RewardKind == PrizeRewardKind.QRCode)
|
||
{
|
||
prizeSessionState?.RecordQrShow();
|
||
if (qrTimeoutCancellation != null)
|
||
{
|
||
qrTimeoutCancellation.Cancel();
|
||
qrTimeoutCancellation.Dispose();
|
||
}
|
||
qrTimeoutCancellation = new CancellationTokenSource();
|
||
StartQrTimeoutAsync(qrTimeoutCancellation.Token).Forget(Debug.LogException);
|
||
}
|
||
else if (result.RewardKind == PrizeRewardKind.Hopper || result.RewardKind == PrizeRewardKind.PrizeStand)
|
||
{
|
||
if (physicalDispenseCancellation != null)
|
||
{
|
||
physicalDispenseCancellation.Cancel();
|
||
physicalDispenseCancellation.Dispose();
|
||
}
|
||
physicalDispenseCancellation = new CancellationTokenSource();
|
||
DispensePhysicalPrizeAsync(result, physicalDispenseCancellation.Token).Forget(Debug.LogException);
|
||
}
|
||
}
|
||
|
||
private void HandlePrizeReturnRequested()
|
||
{
|
||
audioService?.Play(GameAudioEvent.UiBack);
|
||
if (qrTimeoutCancellation != null)
|
||
{
|
||
qrTimeoutCancellation.Cancel();
|
||
qrTimeoutCancellation.Dispose();
|
||
qrTimeoutCancellation = null;
|
||
}
|
||
if (physicalDispenseCancellation != null)
|
||
{
|
||
physicalDispenseCancellation.Cancel();
|
||
physicalDispenseCancellation.Dispose();
|
||
physicalDispenseCancellation = null;
|
||
}
|
||
prizeSessionState?.CompleteActiveSession("Оператор закрыл экран выдачи приза.");
|
||
prizeResultOverlay?.Close();
|
||
sceneFlow?.OpenMainMenu();
|
||
}
|
||
|
||
private void ShowLastPrizeResultIfNeeded()
|
||
{
|
||
if (prizeFlow == null || !prizeFlow.LastResult.HasPrize)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HandlePrizeFlowStarted(prizeFlow.LastResult);
|
||
}
|
||
|
||
private void RefreshContinueOverlay()
|
||
{
|
||
if (session == null || session.State != GameplaySessionState.GameOver || prizeResultOverlay != null && prizeResultOverlay.IsOpen)
|
||
{
|
||
continueGameOverlay?.Close();
|
||
return;
|
||
}
|
||
|
||
if (!session.IsContinueEnabled())
|
||
{
|
||
continueGameOverlay?.Close();
|
||
return;
|
||
}
|
||
|
||
EnsureDocument();
|
||
BindContinueOverlay();
|
||
|
||
if (session.CanContinueGame)
|
||
{
|
||
continueGameOverlay?.SetOffer(session.ContinueGameCost, session.ContinueGameLives);
|
||
}
|
||
else
|
||
{
|
||
continueGameOverlay?.SetUnavailable();
|
||
}
|
||
|
||
if (continueGameOverlay != null && !continueGameOverlay.IsOpen)
|
||
{
|
||
continueGameOverlay.Open();
|
||
}
|
||
}
|
||
|
||
private void HandleContinueRequested()
|
||
{
|
||
if (session != null && session.TryContinueGame())
|
||
{
|
||
audioService?.Play(GameAudioEvent.UiSubmit);
|
||
continueGameOverlay?.Close();
|
||
return;
|
||
}
|
||
|
||
audioService?.Play(GameAudioEvent.UiError);
|
||
continueGameOverlay?.SetUnavailable();
|
||
}
|
||
|
||
private void HandleContinueMenuRequested()
|
||
{
|
||
audioService?.Play(GameAudioEvent.UiBack);
|
||
continueGameOverlay?.Close();
|
||
sceneFlow?.OpenMainMenu();
|
||
}
|
||
|
||
private async UniTask StartQrTimeoutAsync(CancellationToken token)
|
||
{
|
||
var timeoutSeconds = 30;
|
||
if (serviceSettings != null)
|
||
{
|
||
var val = serviceSettings.GetInt(ServiceSettingsKeys.InactivityTimeout);
|
||
if (val > 0)
|
||
{
|
||
timeoutSeconds = val;
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
await UniTask.Delay(TimeSpan.FromSeconds(timeoutSeconds), cancellationToken: token);
|
||
if (prizeSessionState != null)
|
||
{
|
||
prizeSessionState.CompleteActiveSession("QR-код закрыт автоматически по таймауту неактивности.");
|
||
}
|
||
prizeResultOverlay?.Close();
|
||
sceneFlow?.OpenMainMenu();
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
}
|
||
}
|
||
|
||
private async UniTask DispensePhysicalPrizeAsync(PrizeFlowResult result, CancellationToken token)
|
||
{
|
||
if (prizeSessionState == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (comSystem == null || !comSystem.IsRunning)
|
||
{
|
||
var err = "Ошибка: COM-система не запущена.";
|
||
prizeResultOverlay?.SetSubtitle(err);
|
||
prizeSessionState.RecordPhysicalDispense(false);
|
||
prizeSessionState.FailActiveSession(err);
|
||
return;
|
||
}
|
||
|
||
prizeResultOverlay?.SetSubtitle("Выдача приза...");
|
||
var success = false;
|
||
var detailMessage = string.Empty;
|
||
|
||
if (result.RewardKind == PrizeRewardKind.Hopper)
|
||
{
|
||
success = await DispenseHopperAsync(comSystem, token);
|
||
detailMessage = success
|
||
? "Капсула выдана успешно!"
|
||
: "Ошибка выдачи: датчик хоппера не сработал за 10 секунд.";
|
||
}
|
||
else if (result.RewardKind == PrizeRewardKind.PrizeStand)
|
||
{
|
||
var statuses = await comSystem.GetPrizeBoxStatusesAsync(token);
|
||
prizeResultOverlay?.SetPrizeStandStatus(statuses);
|
||
|
||
var readyBoxes = await comSystem.GetReadyPrizeBoxesAsync(token);
|
||
if (readyBoxes == null || readyBoxes.Length == 0)
|
||
{
|
||
success = false;
|
||
detailMessage = "Ошибка выдачи: нет доступных ячеек на витрине.";
|
||
}
|
||
else
|
||
{
|
||
var boxNumber = readyBoxes[0];
|
||
prizeResultOverlay?.SetPrizeStandStatus(statuses, openingBox: boxNumber);
|
||
prizeResultOverlay?.SetSubtitle($"Открытие ячейки {boxNumber}...");
|
||
var openResult = await comSystem.OpenPrizeBoxAsync(boxNumber, token);
|
||
success = openResult.IsSuccess;
|
||
detailMessage = success
|
||
? $"Ячейка {boxNumber} открыта!"
|
||
: $"Ошибка открытия ячейки {boxNumber}: {openResult.Message}";
|
||
|
||
if (success)
|
||
{
|
||
prizeResultOverlay?.SetPrizeStandStatus(statuses, openedBox: boxNumber);
|
||
}
|
||
else
|
||
{
|
||
prizeResultOverlay?.SetPrizeStandStatus(statuses, errorBox: boxNumber);
|
||
}
|
||
}
|
||
}
|
||
|
||
prizeResultOverlay?.SetSubtitle(detailMessage);
|
||
prizeSessionState.RecordPhysicalDispense(success);
|
||
if (success)
|
||
{
|
||
prizeSessionState.CompleteActiveSession(detailMessage);
|
||
}
|
||
else
|
||
{
|
||
prizeSessionState.FailActiveSession(detailMessage);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
prizeResultOverlay?.SetSubtitle("Выдача приза отменена.");
|
||
prizeSessionState.RecordPhysicalDispense(false);
|
||
prizeSessionState.FailActiveSession("Выдача приза отменена.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var err = $"Внутренняя ошибка выдачи: {ex.Message}";
|
||
prizeResultOverlay?.SetSubtitle(err);
|
||
prizeSessionState.RecordPhysicalDispense(false);
|
||
prizeSessionState.FailActiveSession(err);
|
||
}
|
||
}
|
||
|
||
private async UniTask<bool> DispenseHopperAsync(IComSystem com, CancellationToken token)
|
||
{
|
||
if (com == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!com.SendToMain("301"))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var tcs = new UniTaskCompletionSource();
|
||
UnityEngine.Events.UnityAction onTriggered = () => tcs.TrySetResult();
|
||
com.HopperSensorTriggered.AddListener(onTriggered);
|
||
|
||
try
|
||
{
|
||
await UniTask.WhenAny(
|
||
tcs.Task,
|
||
UniTask.Delay(TimeSpan.FromSeconds(10), cancellationToken: token)
|
||
);
|
||
return tcs.Task.Status == UniTaskStatus.Succeeded;
|
||
}
|
||
finally
|
||
{
|
||
com.HopperSensorTriggered.RemoveListener(onTriggered);
|
||
com.SendToMain("300");
|
||
}
|
||
}
|
||
}
|
||
}
|