Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Интегрирована система кастомных визуальных эффектов (VFX): - Добавлена новая утилита MvpVFXUtility и настройки MvpVFXSpawnSettings для спавна эффектов. - Реализован пул эффектов VFXPool с поддержкой отслеживания версий спавна, сброса оверрайдов и анимации масштаба по времени. - Добавлены эффекты прыжка и приземления в PlayerAnimationEvents. - Интегрирован эффект DeathVignetteController для плавного затемнения экрана при смерти игрока. - Добавлены частицы stompImpactVFX при уничтожении врагов прыжком сверху (EnemyController). - Реализован эффект конфетти finishVFX при успешном завершении уровня (LevelFinish). - Интегрированы эффекты искр при сборе монеток, кристаллов и других собираемых предметов (Collectible, EnvironmentalCollectibleBase). - Улучшен и визуально доработан пользовательский интерфейс (UI): - Добавлен скрипт QRCodeTierHighlight для цветовой подсветки QR-кодов в зависимости от тира приза. - Реализована пульсация блока приза в баннере победы (WinBannerController). - Обновлено обозначение валюты в BalanceDisplay с "₽" на "руб.". - Добавлены новые префабы, сцены, ассеты тайлсетов и шейдеров для настройки визуального стиля. - Обновлён протокол и инициализация подсветки COM-платы: - Изменен префикс команд подсветки с 85/86 на 8550, а диапазон эффектов обновлен с 40...52 на 10...22 (LightController, LightMode, LightCycleDurations). - Исправлена хардкод-команда сброса подсветки при инициализации в ComDeviceClient с устаревшей 8600 на новую 8550 для предотвращения рассинхронизации буфера обмена платы.
384 lines
15 KiB
C#
384 lines
15 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
|
||
using RR.Common.Controls;
|
||
using RR.Common.Managers;
|
||
using RR.Common.Prizes;
|
||
using RR.Common.Settings;
|
||
using RR.Common.UI.FX;
|
||
using RR.Game2D.Core.Managers;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
/// <summary>
|
||
/// Контроллер баннера выигрыша.
|
||
/// Модальное окно, которое отображается при победе игрока.
|
||
/// </summary>
|
||
public class WinBannerController : Modal
|
||
{
|
||
[Header("Элементы баннера выигрыша")]
|
||
[SerializeField] private TextMeshProUGUI winText;
|
||
[SerializeField] private Button mainMenuButton;
|
||
|
||
[Header("Опциональная разметка режимов")]
|
||
[Tooltip("Корневой объект обычной победной плашки. Если не задан, используется только winText.")]
|
||
[SerializeField] private GameObject winContentRoot;
|
||
[Tooltip("Корневой объект призовой плашки. Если не задан, используется только winText.")]
|
||
[SerializeField] private GameObject prizeContentRoot;
|
||
[SerializeField] private TextMeshProUGUI prizeTitleText;
|
||
[SerializeField] private TextMeshProUGUI prizeDetailsText;
|
||
|
||
[Header("QR-коды")]
|
||
[Tooltip("Контейнер для размещения QR-кодов. QR-коды создаются динамически.")]
|
||
[SerializeField] private RectTransform qrCodesContainer;
|
||
[Tooltip("Корневой объект секции QR. Если не задан, используется объект контейнера QR.")]
|
||
[SerializeField] private GameObject qrSectionRoot;
|
||
|
||
[Header("FX")]
|
||
[SerializeField] private bool pulsePrizeContent = true;
|
||
[SerializeField] private float prizePulseScale = 1.08f;
|
||
[SerializeField] private float prizePulseDuration = 1.2f;
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое при закрытии баннера выигрыша.
|
||
/// </summary>
|
||
public event Action WinBannerDismissed;
|
||
|
||
private readonly List<GameObject> _createdQRCodeObjects = new();
|
||
private Coroutine _prizePulseCoroutine;
|
||
private Transform _prizePulseTarget;
|
||
private Vector3 _prizePulseOriginalScale;
|
||
|
||
protected override void Awake()
|
||
{
|
||
base.Awake();
|
||
|
||
if (mainMenuButton)
|
||
{
|
||
mainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Показывает баннер выигрыша или призовой баннер для последнего уровня.
|
||
/// </summary>
|
||
/// <param name="gameTime">Время игры в секундах.</param>
|
||
/// <param name="prizeConfig">Конфиг приза последнего уровня.</param>
|
||
/// <param name="finalScore">Итоговый счёт за игру.</param>
|
||
/// <param name="showFinalScore">Нужно ли показывать итоговый счёт в баннере.</param>
|
||
public void ShowWin(float gameTime, LevelPrizeConfig prizeConfig, int finalScore, bool showFinalScore)
|
||
{
|
||
ApplyBannerState(ResolveBannerState(gameTime, prizeConfig ?? new LevelPrizeConfig(), finalScore, showFinalScore));
|
||
Open();
|
||
}
|
||
|
||
private void ApplyBannerState(BannerState state)
|
||
{
|
||
if (winContentRoot != null)
|
||
winContentRoot.SetActive(!state.IsPrizeBanner);
|
||
|
||
if (prizeContentRoot != null)
|
||
prizeContentRoot.SetActive(state.IsPrizeBanner);
|
||
|
||
if (winText)
|
||
winText.text = state.FallbackWinText;
|
||
|
||
if (prizeTitleText)
|
||
prizeTitleText.text = state.PrizeTitle;
|
||
|
||
if (prizeDetailsText)
|
||
prizeDetailsText.text = state.PrizeDetails;
|
||
|
||
if ((prizeTitleText == null || prizeDetailsText == null) && winText)
|
||
winText.text = state.LegacyCombinedText;
|
||
|
||
DisplayQRCodes(state.ShowQRCodes);
|
||
StartPrizeHighlight(state.IsPrizeBanner);
|
||
}
|
||
|
||
private BannerState ResolveBannerState(float gameTime, LevelPrizeConfig prizeConfig, int finalScore, bool showFinalScore)
|
||
{
|
||
string formattedTime = TimeFormatHelper.FormatTime(gameTime);
|
||
string scoreLine = showFinalScore ? $"\nОчки за игру: {finalScore}" : string.Empty;
|
||
bool canShowQRCodes = prizeConfig.type == PrizeType.QRCode && CanDisplayBigPrizeQRCodes() && HasQRCodes(prizeConfig.qrTier);
|
||
string qrTierName = GetQRTierName(prizeConfig.qrTier);
|
||
string prizeStandSummary = GetPrizeStandSummary(finalScore, showFinalScore);
|
||
|
||
return prizeConfig.type switch
|
||
{
|
||
PrizeType.QRCode when canShowQRCodes => CreatePrizeState(
|
||
"Ваш приз",
|
||
$"Отсканируйте {qrTierName} QR-код, чтобы получить приз.\nВремя игры: {formattedTime}{scoreLine}",
|
||
$"Ваш приз: {qrTierName} QR-код\nВремя игры: {formattedTime}{scoreLine}",
|
||
showQRCodes: true),
|
||
PrizeType.Hopper => CreatePrizeState(
|
||
"Ваш приз",
|
||
$"Капсулы: {prizeConfig.hopperCapsuleCount}\nВремя игры: {formattedTime}{scoreLine}",
|
||
$"Ваш приз: капсулы\nКоличество: {prizeConfig.hopperCapsuleCount}\nВремя игры: {formattedTime}{scoreLine}"),
|
||
PrizeType.PrizeStand => CreatePrizeState(
|
||
"Ваш приз",
|
||
$"{prizeStandSummary}\nВремя игры: {formattedTime}{scoreLine}",
|
||
$"Ваш приз: витрина\n{prizeStandSummary}\nВремя игры: {formattedTime}{scoreLine}"),
|
||
_ => CreateWinState(formattedTime, finalScore, showFinalScore)
|
||
};
|
||
}
|
||
|
||
private static BannerState CreatePrizeState(string title, string details, string legacyCombinedText, bool showQRCodes = false)
|
||
{
|
||
return new BannerState
|
||
{
|
||
IsPrizeBanner = true,
|
||
PrizeTitle = title,
|
||
PrizeDetails = details,
|
||
LegacyCombinedText = legacyCombinedText,
|
||
ShowQRCodes = showQRCodes,
|
||
FallbackWinText = string.Empty
|
||
};
|
||
}
|
||
|
||
private static BannerState CreateWinState(string formattedTime, int finalScore, bool showFinalScore)
|
||
{
|
||
string winText = showFinalScore
|
||
? $"Время игры: {formattedTime}\nОчки за игру: {finalScore}"
|
||
: $"Время игры: {formattedTime}";
|
||
|
||
return new BannerState
|
||
{
|
||
IsPrizeBanner = false,
|
||
PrizeTitle = string.Empty,
|
||
PrizeDetails = string.Empty,
|
||
LegacyCombinedText = winText,
|
||
FallbackWinText = winText,
|
||
ShowQRCodes = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отображает QR-коды из PrizeSystemManager, если для финального уровня назначен QR-приз.
|
||
/// </summary>
|
||
private void DisplayQRCodes(bool shouldDisplay)
|
||
{
|
||
ClearQRCodes();
|
||
|
||
if (qrCodesContainer == null)
|
||
return;
|
||
|
||
GetQRSectionRoot().SetActive(shouldDisplay);
|
||
if (!shouldDisplay || PrizeSystemManager.Instance == null)
|
||
return;
|
||
|
||
QRTier tier = PrizeSystemManager.Instance.GetPrizeConfigForCurrentLevel().qrTier;
|
||
Texture2D[] qrCodes = PrizeSystemManager.Instance.GetQRCodes(tier);
|
||
if (qrCodes == null || qrCodes.Length == 0)
|
||
{
|
||
GetQRSectionRoot().SetActive(false);
|
||
return;
|
||
}
|
||
|
||
foreach (Texture2D qrTexture in qrCodes)
|
||
{
|
||
if (qrTexture == null) continue;
|
||
|
||
GameObject qrObject = new GameObject("QRCode", typeof(RectTransform), typeof(Image));
|
||
qrObject.transform.SetParent(qrCodesContainer, false);
|
||
|
||
Sprite qrSprite = Sprite.Create(
|
||
qrTexture,
|
||
new Rect(0, 0, qrTexture.width, qrTexture.height),
|
||
new Vector2(0.5f, 0.5f)
|
||
);
|
||
|
||
Image image = qrObject.GetComponent<Image>();
|
||
image.sprite = qrSprite;
|
||
image.raycastTarget = false;
|
||
QRCodeTierHighlight.Apply(qrObject, tier);
|
||
|
||
_createdQRCodeObjects.Add(qrObject);
|
||
}
|
||
}
|
||
|
||
private void StartPrizeHighlight(bool isPrizeBanner)
|
||
{
|
||
StopPrizeHighlight();
|
||
|
||
if (!pulsePrizeContent || !isPrizeBanner)
|
||
return;
|
||
|
||
GameObject target = prizeContentRoot != null
|
||
? prizeContentRoot
|
||
: qrSectionRoot != null
|
||
? qrSectionRoot
|
||
: qrCodesContainer != null
|
||
? qrCodesContainer.gameObject
|
||
: null;
|
||
if (target == null || !target.activeInHierarchy)
|
||
return;
|
||
|
||
_prizePulseTarget = target.transform;
|
||
_prizePulseOriginalScale = _prizePulseTarget.localScale;
|
||
_prizePulseCoroutine = StartCoroutine(PulsePrizeContent());
|
||
}
|
||
|
||
private IEnumerator PulsePrizeContent()
|
||
{
|
||
float elapsed = 0f;
|
||
|
||
while (elapsed < prizePulseDuration && _prizePulseTarget != null)
|
||
{
|
||
elapsed += Time.unscaledDeltaTime;
|
||
float normalized = Mathf.Clamp01(elapsed / Mathf.Max(0.01f, prizePulseDuration));
|
||
float pulse = Mathf.Sin(normalized * Mathf.PI * 2f);
|
||
float scale = Mathf.Lerp(1f, prizePulseScale, Mathf.Abs(pulse));
|
||
_prizePulseTarget.localScale = _prizePulseOriginalScale * scale;
|
||
yield return null;
|
||
}
|
||
|
||
if (_prizePulseTarget != null)
|
||
_prizePulseTarget.localScale = _prizePulseOriginalScale;
|
||
|
||
_prizePulseCoroutine = null;
|
||
_prizePulseTarget = null;
|
||
}
|
||
|
||
private void StopPrizeHighlight()
|
||
{
|
||
if (_prizePulseCoroutine != null)
|
||
{
|
||
StopCoroutine(_prizePulseCoroutine);
|
||
_prizePulseCoroutine = null;
|
||
}
|
||
|
||
if (_prizePulseTarget != null)
|
||
_prizePulseTarget.localScale = _prizePulseOriginalScale;
|
||
|
||
_prizePulseTarget = null;
|
||
}
|
||
|
||
private static bool CanDisplayBigPrizeQRCodes()
|
||
{
|
||
return SettingsManager.Instance != null && SettingsManager.Instance.GetBool("prizes.giveBigPrize");
|
||
}
|
||
|
||
private bool HasQRCodes(QRTier tier)
|
||
{
|
||
if (PrizeSystemManager.Instance == null)
|
||
return false;
|
||
|
||
Texture2D[] qrCodes = PrizeSystemManager.Instance.GetQRCodes(tier);
|
||
if (qrCodes == null || qrCodes.Length == 0)
|
||
return false;
|
||
|
||
foreach (Texture2D qrCode in qrCodes)
|
||
{
|
||
if (qrCode != null)
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private GameObject GetQRSectionRoot()
|
||
{
|
||
return qrSectionRoot != null ? qrSectionRoot : qrCodesContainer.gameObject;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Очищает созданные QR-код объекты.
|
||
/// </summary>
|
||
private void ClearQRCodes()
|
||
{
|
||
foreach (GameObject obj in _createdQRCodeObjects)
|
||
{
|
||
if (obj != null)
|
||
{
|
||
Destroy(obj);
|
||
}
|
||
}
|
||
_createdQRCodeObjects.Clear();
|
||
}
|
||
|
||
private void OnMainMenuClicked()
|
||
{
|
||
Close();
|
||
WinBannerDismissed?.Invoke();
|
||
|
||
if (GameManager.Instance)
|
||
{
|
||
GameManager.Instance.SetPause(false);
|
||
GameManager.Instance.LoadScene("MainMenu", false);
|
||
}
|
||
}
|
||
|
||
protected override void OnDisable()
|
||
{
|
||
base.OnDisable();
|
||
StopPrizeHighlight();
|
||
ClearQRCodes();
|
||
if (qrCodesContainer != null)
|
||
GetQRSectionRoot().SetActive(true);
|
||
if (winContentRoot != null)
|
||
winContentRoot.SetActive(true);
|
||
if (prizeContentRoot != null)
|
||
prizeContentRoot.SetActive(true);
|
||
}
|
||
|
||
private static string GetQRTierName(QRTier tier)
|
||
{
|
||
return tier switch
|
||
{
|
||
QRTier.Bronze => "бронзовый",
|
||
QRTier.Silver => "серебряный",
|
||
QRTier.Gold => "золотой",
|
||
_ => "обычный"
|
||
};
|
||
}
|
||
|
||
private static string GetPrizeStandSummary(int finalScore, bool showFinalScore)
|
||
{
|
||
PrizeSystemManager manager = PrizeSystemManager.Instance;
|
||
if (manager == null)
|
||
return "Витрина недоступна";
|
||
|
||
PrizeStandConnectionInfo connection = manager.GetPrizeStandConnectionInfo();
|
||
PrizeStandSettings settings = manager.GetPrizeStandSettings();
|
||
int unlockedCount = 0;
|
||
|
||
if (settings?.boxes != null)
|
||
{
|
||
for (int i = 0; i < settings.boxes.Count; i++)
|
||
{
|
||
PrizeStandBoxConfig box = settings.boxes[i];
|
||
if (box == null)
|
||
continue;
|
||
|
||
bool isConnected = connection.IsConnected
|
||
&& (connection.ConnectedBoxesCount <= 0 || box.boxNumber <= connection.ConnectedBoxesCount);
|
||
bool isUnlocked = !showFinalScore || finalScore >= box.requiredScore;
|
||
if (isConnected && isUnlocked)
|
||
unlockedCount++;
|
||
}
|
||
}
|
||
|
||
string connectionText = connection.IsConnected
|
||
? $"Витрина подключена, ячеек на устройстве: {connection.ConnectedBoxesCount}"
|
||
: "Витрина не подключена";
|
||
|
||
return $"{connectionText}\nЯчеек доступно по очкам: {unlockedCount}";
|
||
}
|
||
|
||
private struct BannerState
|
||
{
|
||
public bool IsPrizeBanner;
|
||
public bool ShowQRCodes;
|
||
public string PrizeTitle;
|
||
public string PrizeDetails;
|
||
public string LegacyCombinedText;
|
||
public string FallbackWinText;
|
||
}
|
||
}
|
||
}
|