- Добавлен асинхронный процесс аппаратной выдачи физических призов (хоппер и витрина) в GameplayHudController. - Реализована блокировка выдачи по 10-секундному таймауту датчика хоппера с гарантированной отправкой команды отключения двигателя '300'. - Реализован автоматический таймаут неактивности на экране QR-кода с последующим переходом в главное меню. - Расширен сервис статистики PrizeSessionStateService новыми долгосрочными счетчиками (сыгранные игры, очки, монеты, сбои связи COM, физ. выдачи и ошибки). - Обновлен построитель сводки статистики в меню настроек для отображения новых счетчиков и списка последних 5 сессий. - Написаны EditMode тесты для проверки инкремента счетчиков и запуска выдачи призов.
223 lines
7.0 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|