Danro-Jump/Assets/Scripts/Runtime/UI/Gameplay/PrizeResultOverlayWindow.cs
Danil Iarosh d39d160d92 Implement physical prize dispensing, service stats, and QR screen timeout
- Добавлен асинхронный процесс аппаратной выдачи физических призов (хоппер и витрина) в GameplayHudController.
- Реализована блокировка выдачи по 10-секундному таймауту датчика хоппера с гарантированной отправкой команды отключения двигателя '300'.
- Реализован автоматический таймаут неактивности на экране QR-кода с последующим переходом в главное меню.
- Расширен сервис статистики PrizeSessionStateService новыми долгосрочными счетчиками (сыгранные игры, очки, монеты, сбои связи COM, физ. выдачи и ошибки).
- Обновлен построитель сводки статистики в меню настроек для отображения новых счетчиков и списка последних 5 сессий.
- Написаны EditMode тесты для проверки инкремента счетчиков и запуска выдачи призов.
2026-06-23 10:36:13 +03:00

223 lines
7.0 KiB
C#

using System;
using DanroJump.Prizes;
using DanroJump.Prizes.Qr;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanroJump.UI.Gameplay
{
/// <summary>
/// Полноэкранный результат выдачи приза поверх gameplay HUD.
/// </summary>
public sealed class PrizeResultOverlayWindow : IDisposable
{
private const float OpenInputGuardSeconds = 0.45f;
private readonly VisualElement root;
private readonly Label titleLabel;
private readonly Label subtitleLabel;
private readonly Label detailsLabel;
private readonly VisualElement qrContainer;
private readonly Image qrImage;
private readonly Label qrPhoneLabel;
private readonly Label qrMessageLabel;
private readonly Button returnButton;
private float inputEnabledAtTime = float.NegativeInfinity;
public PrizeResultOverlayWindow(VisualElement root)
{
this.root = root;
if (this.root != null)
{
this.root.focusable = true;
}
titleLabel = root?.Q<Label>("PrizeResultTitle");
subtitleLabel = root?.Q<Label>("PrizeResultSubtitle");
detailsLabel = root?.Q<Label>("PrizeResultDetails");
qrContainer = root?.Q<VisualElement>("PrizeQrContainer");
qrImage = root?.Q<Image>("PrizeQrImage");
qrPhoneLabel = root?.Q<Label>("PrizeQrPhone");
qrMessageLabel = root?.Q<Label>("PrizeQrMessage");
returnButton = root?.Q<Button>("PrizeResultReturnButton");
if (returnButton != null)
{
returnButton.clicked += HandleReturnClicked;
}
}
public event Action ReturnRequested;
public bool IsOpen { get; private set; }
public void SetResult(
PrizeFlowResult result,
string bigPrizeName = null,
string phoneNumber = null,
PrizeQrTicket qrTicket = null,
string qrError = null)
{
SetLabel(titleLabel, ResolveTitle(result, bigPrizeName));
SetLabel(subtitleLabel, ResolveSubtitle(result, phoneNumber, qrTicket, qrError));
SetLabel(detailsLabel, $"Очки: {result.Score}. Монеты: {result.Coins}. Высота: {result.Height:0} м.");
RefreshQrBlock(result, qrTicket, qrError);
}
public void SetSubtitle(string text)
{
SetLabel(subtitleLabel, text);
}
public void Open()
{
if (root == null)
{
return;
}
root.style.display = DisplayStyle.Flex;
root.SetEnabled(true);
IsOpen = true;
inputEnabledAtTime = Time.unscaledTime + OpenInputGuardSeconds;
root.Focus();
}
public void Close()
{
if (root == null)
{
return;
}
root.style.display = DisplayStyle.None;
root.SetEnabled(false);
IsOpen = false;
}
public void Dispose()
{
if (returnButton != null)
{
returnButton.clicked -= HandleReturnClicked;
}
}
private static string ResolveTitle(PrizeFlowResult result, string bigPrizeName)
{
return result.RewardKind switch
{
PrizeRewardKind.QRCode => "QR-приз!",
PrizeRewardKind.Hopper => "Приз из хоппера!",
PrizeRewardKind.PrizeStand => "Приз на витрине!",
PrizeRewardKind.Big => string.IsNullOrWhiteSpace(bigPrizeName)
? "Большой приз!"
: $"Большой приз: {bigPrizeName}",
PrizeRewardKind.Small => "Малый приз!",
_ => "Игра завершена",
};
}
private static string ResolveSubtitle(
PrizeFlowResult result,
string phoneNumber,
PrizeQrTicket qrTicket,
string qrError)
{
if (!result.HasPrize)
{
return "Попробуйте ещё раз.";
}
if (result.RewardKind == PrizeRewardKind.QRCode)
{
if (qrTicket != null)
{
return "Отсканируйте QR-код для связи в Telegram.";
}
return string.IsNullOrWhiteSpace(qrError)
? "QR-приз готовится. Обратитесь к оператору."
: qrError;
}
return string.IsNullOrWhiteSpace(phoneNumber)
? "Обратитесь к оператору для выдачи приза."
: $"Обратитесь к оператору: {phoneNumber}";
}
private void RefreshQrBlock(PrizeFlowResult result, PrizeQrTicket qrTicket, string qrError)
{
if (qrContainer == null)
{
return;
}
var shouldShow = result.RewardKind == PrizeRewardKind.QRCode;
qrContainer.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None;
if (!shouldShow)
{
if (qrImage != null)
{
qrImage.image = null;
}
SetLabel(qrPhoneLabel, string.Empty);
SetLabel(qrMessageLabel, string.Empty);
return;
}
if (qrImage != null)
{
qrImage.image = qrTicket?.Texture;
qrImage.style.display = qrTicket?.Texture != null ? DisplayStyle.Flex : DisplayStyle.None;
}
var phone = qrTicket != null
? FormatDisplayPhone(qrTicket)
: "Телефон не задан";
var message = qrTicket != null
? qrTicket.Message
: qrError;
SetLabel(qrPhoneLabel, phone);
SetLabel(qrMessageLabel, message);
}
private static string FormatDisplayPhone(PrizeQrTicket qrTicket)
{
if (!string.IsNullOrWhiteSpace(qrTicket.OriginalPhone))
{
return qrTicket.OriginalPhone;
}
return string.IsNullOrWhiteSpace(qrTicket.PhoneDigits)
? "Телефон не задан"
: $"+{qrTicket.PhoneDigits}";
}
private static void SetLabel(Label label, string value)
{
if (label != null)
{
label.text = value;
}
}
private void HandleReturnClicked()
{
if (IsInputGuardActive())
{
return;
}
ReturnRequested?.Invoke();
}
private bool IsInputGuardActive()
{
return Time.unscaledTime < inputEnabledAtTime;
}
}
}