Danro/Assets/Scripts/UI/FX/QRCodeTierHighlight.cs
Danil Iarosh e8000ffdca
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(vfx, ui, com): add custom vfx, polish ui and update backlight protocol
- Интегрирована система кастомных визуальных эффектов (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 для предотвращения рассинхронизации буфера обмена платы.
2026-07-07 16:52:05 +03:00

230 lines
7.8 KiB
C#

using RR.Common.Prizes;
using RR.Common.Utility;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace RR.Common.UI.FX
{
/// <summary>
/// Добавляет QR-коду цветную подсветку, соответствующую рангу приза.
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("RR/UI/FX/QR Code Tier Highlight")]
public sealed class QRCodeTierHighlight : MonoBehaviour
{
[SerializeField] private QRTier tier;
[SerializeField] private Color fallbackColor = new(0.15f, 0.55f, 1f, 1f);
[SerializeField, Range(0f, 1f)] private float glowAlpha = 0.55f;
[SerializeField, Min(0f)] private float outlineDistance = 4f;
[SerializeField, Range(4, 16)] private int outlineSamples = 8;
[SerializeField, Range(1, 4)] private int blurLayers = 2;
[SerializeField, Min(0f)] private float blurSpread = 2f;
[SerializeField, Min(0f)] private float pulseScale = 0.035f;
[SerializeField, Min(0.01f)] private float pulseDuration = 1.35f;
[SerializeField] private bool pulse = true;
private Image _targetImage;
private RectTransform _highlightRect;
private GameObject _highlightObject;
private readonly List<Image> _highlightLayers = new();
public static QRCodeTierHighlight Apply(GameObject qrObject, QRTier qrTier)
{
if (qrObject == null)
{
return null;
}
QRCodeTierHighlight highlight = qrObject.GetComponent<QRCodeTierHighlight>();
if (highlight == null)
{
highlight = qrObject.AddComponent<QRCodeTierHighlight>();
}
highlight.SetTier(qrTier);
return highlight;
}
private void Awake()
{
ResolveReferences();
EnsureView();
ApplyColor();
}
private void OnEnable()
{
ResolveReferences();
EnsureView();
ApplyColor();
SyncHighlightRect(1f);
}
private void Update()
{
if (!pulse || _highlightRect == null)
{
return;
}
float wave = (Mathf.Sin(Time.unscaledTime / pulseDuration * Mathf.PI * 2f) + 1f) * 0.5f;
float scale = 1f + wave * pulseScale;
SyncHighlightRect(scale);
}
public void SetTier(QRTier qrTier)
{
tier = qrTier;
EnsureView();
ApplyColor();
}
private void EnsureView()
{
if (_highlightLayers.Count > 0)
{
return;
}
Transform highlightParent = transform.parent != null ? transform.parent : transform;
_highlightObject = new GameObject($"{name} QR Tier Highlight", typeof(RectTransform), typeof(LayoutElement));
_highlightObject.transform.SetParent(highlightParent, false);
_highlightObject.GetComponent<LayoutElement>().ignoreLayout = true;
_highlightRect = _highlightObject.GetComponent<RectTransform>();
_highlightObject.transform.SetSiblingIndex(transform.GetSiblingIndex());
SyncHighlightRect(1f);
RebuildHighlightLayers();
}
private void OnDestroy()
{
if (_highlightObject != null)
{
Destroy(_highlightObject);
}
}
private void ResolveReferences()
{
if (_targetImage == null)
{
_targetImage = GetComponent<Image>();
}
}
private void RebuildHighlightLayers()
{
_highlightLayers.Clear();
int samples = Mathf.Max(4, outlineSamples);
int layerIndex = 0;
for (int blur = 0; blur < Mathf.Max(1, blurLayers); blur++)
{
float distance = outlineDistance + blur * blurSpread;
for (int sample = 0; sample < samples; sample++)
{
GameObject layerObject = new($"QR Tier Outline {layerIndex + 1}", typeof(RectTransform), typeof(Image));
layerObject.transform.SetParent(_highlightRect, false);
layerObject.transform.SetSiblingIndex(layerIndex);
float angle = sample / (float)samples * Mathf.PI * 2f;
Vector2 offset = new(Mathf.Cos(angle) * distance, Mathf.Sin(angle) * distance);
RectTransform layerRect = layerObject.GetComponent<RectTransform>();
layerRect.anchorMin = Vector2.zero;
layerRect.anchorMax = Vector2.one;
layerRect.offsetMin = offset;
layerRect.offsetMax = offset;
Image layerImage = layerObject.GetComponent<Image>();
ApplyTargetSprite(layerImage);
layerImage.raycastTarget = false;
_highlightLayers.Add(layerImage);
layerIndex++;
}
}
}
private void ApplyTargetSprite(Image image)
{
if (image == null || _targetImage == null)
{
return;
}
image.sprite = _targetImage.sprite;
image.type = _targetImage.type;
image.preserveAspect = _targetImage.preserveAspect;
image.fillCenter = true;
image.fillMethod = _targetImage.fillMethod;
image.fillOrigin = _targetImage.fillOrigin;
image.fillAmount = _targetImage.fillAmount;
image.fillClockwise = _targetImage.fillClockwise;
image.pixelsPerUnitMultiplier = _targetImage.pixelsPerUnitMultiplier;
}
private void ApplyColor()
{
if (_highlightLayers.Count == 0)
{
return;
}
for (int i = 0; i < _highlightLayers.Count; i++)
{
Image layer = _highlightLayers[i];
if (layer == null)
{
continue;
}
int samples = Mathf.Max(4, outlineSamples);
int blurIndex = i / samples;
float layerT = Mathf.Max(1, blurLayers) <= 1 ? 0f : blurIndex / (float)(Mathf.Max(1, blurLayers) - 1);
Color color = ResolveTierColor(tier);
color.a = glowAlpha * Mathf.Lerp(0.36f, 0.12f, layerT);
layer.color = color;
}
}
private Color ResolveTierColor(QRTier qrTier)
{
if (qrTier == QRTier.None)
{
return fallbackColor;
}
Vector3 rgb = QRCodeInfo.GetTierDarkColorVector(qrTier);
return new Color(rgb.x, rgb.y, rgb.z, 1f);
}
private void SyncHighlightRect(float scale)
{
if (_highlightRect == null)
{
return;
}
RectTransform targetRect = transform as RectTransform;
if (targetRect == null)
{
return;
}
_highlightRect.anchorMin = targetRect.anchorMin;
_highlightRect.anchorMax = targetRect.anchorMax;
_highlightRect.pivot = targetRect.pivot;
_highlightRect.anchoredPosition = targetRect.anchoredPosition;
_highlightRect.sizeDelta = targetRect.sizeDelta;
_highlightRect.localRotation = targetRect.localRotation;
_highlightRect.localScale = targetRect.localScale * scale;
if (_highlightRect.parent == targetRect.parent)
{
_highlightRect.SetSiblingIndex(Mathf.Max(0, targetRect.GetSiblingIndex() - 1));
}
}
}
}