328 lines
11 KiB
C#

using DG.Tweening;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class RatingModal : MonoBehaviour
{
public event Action Closed;
[Header("Строки")]
[SerializeField] private GameObject _rowPrefab;
[SerializeField] private GameObject _latestRowPrefab;
[SerializeField] private int _maxRecords = 100;
[SerializeField] private float _rowSpacing = 4f;
[Header("Прокрутка")]
[SerializeField] private float _scrollAcceleration = 3600f;
[SerializeField] private float _scrollDeceleration = 4800f;
[SerializeField] private float _maxScrollSpeed = 1800f;
[SerializeField] private float _minimumScrollbarHandleHeight = 30f;
[Header("Модальное окно")]
[SerializeField] private float _inputTimeout = 20f;
[SerializeField] private float _transitionDuration = 0.25f;
private const string DateFormat = "dd.MM.yy HH:mm";
private RectTransform _panel;
private RectTransform _viewport;
private RectTransform _content;
private RectTransform _scrollbarTrack;
private RectTransform _scrollbarHandle;
private RectTransform _latestRow;
private CanvasGroup _canvasGroup;
private Transform _originalParent;
private Sequence _transition;
private Vector3 _panelOpenedScale;
private float _lastInputTime;
private float _scrollOffset;
private float _scrollSpeed;
private float _maxScrollOffset;
private bool _isTransitioning;
public bool IsOpen { get; private set; }
private void Update()
{
if (!IsOpen || _isTransitioning)
return;
bool hasInput = In.AnyButton();
if (hasInput)
_lastInputTime = Time.unscaledTime;
if (Time.unscaledTime - _lastInputTime >= _inputTimeout)
{
Close();
return;
}
UpdateScrolling();
}
public void Open(Transform modalParent)
{
if (IsOpen || modalParent == null)
return;
CacheReferences();
BuildRating();
_originalParent = transform.parent;
transform.SetParent(modalParent, false);
transform.SetAsLastSibling();
RectTransform root = (RectTransform)transform;
root.anchorMin = Vector2.zero;
root.anchorMax = Vector2.one;
root.offsetMin = Vector2.zero;
root.offsetMax = Vector2.zero;
root.localScale = Vector3.one;
gameObject.SetActive(true);
_canvasGroup.alpha = 0f;
_panel.localScale = _panelOpenedScale * 0.9f;
_lastInputTime = Time.unscaledTime;
_isTransitioning = true;
IsOpen = true;
_transition?.Kill();
_transition = DOTween.Sequence().SetUpdate(true);
_transition.Join(_canvasGroup.DOFade(1f, _transitionDuration));
_transition.Join(_panel.DOScale(_panelOpenedScale, _transitionDuration).SetEase(Ease.OutBack));
_transition.OnComplete(() => _isTransitioning = false);
_transition.PlayForward();
}
public void Close()
{
if (!IsOpen)
return;
_isTransitioning = true;
_transition?.Kill();
_transition = DOTween.Sequence().SetUpdate(true);
_transition.Join(_canvasGroup.DOFade(0f, _transitionDuration));
_transition.Join(_panel.DOScale(_panelOpenedScale * 0.9f, _transitionDuration).SetEase(Ease.InBack));
_transition.OnComplete(CompleteClose);
_transition.PlayForward();
}
private void CacheReferences()
{
_panel ??= transform.Find("Panel") as RectTransform;
_viewport ??= transform.Find("Panel/Table VLG") as RectTransform;
_scrollbarTrack ??= transform.Find("Panel/scrollbar") as RectTransform;
_scrollbarHandle ??= transform.Find("Panel/scrollbar/Image") as RectTransform;
if (_canvasGroup == null)
{
_canvasGroup = GetComponent<CanvasGroup>();
if (_canvasGroup == null)
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
_panelOpenedScale = _panel.localScale;
VerticalLayoutGroup oldLayout = _viewport.GetComponent<VerticalLayoutGroup>();
if (oldLayout != null)
oldLayout.enabled = false;
}
private void BuildRating()
{
ClearGeneratedRows();
PlayerStatsService statsService = GameManager.Instance?.StatsService;
List<PlayerStatsRecord> records = statsService?.GetTopPlayers(_maxRecords)
?? new List<PlayerStatsRecord>();
float rowHeight = GetRowHeight();
float rowStep = rowHeight + _rowSpacing;
float contentHeight = Mathf.Max(_viewport.rect.height, records.Count * rowStep - _rowSpacing);
GameObject contentObject = new("Rating Content", typeof(RectTransform));
_content = contentObject.GetComponent<RectTransform>();
_content.SetParent(_viewport, false);
_content.anchorMin = new Vector2(0.5f, 1f);
_content.anchorMax = new Vector2(0.5f, 1f);
_content.pivot = new Vector2(0.5f, 1f);
_content.anchoredPosition = Vector2.zero;
_content.sizeDelta = new Vector2(_viewport.rect.width, contentHeight);
for (int i = 0; i < records.Count; i++)
{
RectTransform row = Instantiate(_rowPrefab, _content).GetComponent<RectTransform>();
ConfigureRowTransform(row, i, rowHeight, rowStep);
FillRow(row, i + 1, records[i]);
}
CreateLatestRow(statsService, rowHeight);
_scrollOffset = 0f;
_scrollSpeed = 0f;
_maxScrollOffset = Mathf.Max(0f, contentHeight - _viewport.rect.height);
UpdateContentPosition();
UpdateScrollbar();
}
private void ClearGeneratedRows()
{
for (int i = _viewport.childCount - 1; i >= 0; i--)
{
GameObject child = _viewport.GetChild(i).gameObject;
child.SetActive(false);
Destroy(child);
}
if (_latestRow != null)
{
_latestRow.gameObject.SetActive(false);
Destroy(_latestRow.gameObject);
_latestRow = null;
}
}
private void CreateLatestRow(PlayerStatsService statsService, float rowHeight)
{
PlayerStatsRecord latestRecord = statsService?.GetLatestRecord();
if (latestRecord == null)
return;
_latestRow = Instantiate(_latestRowPrefab, _panel).GetComponent<RectTransform>();
_latestRow.anchorMin = new Vector2(0.5f, 0.5f);
_latestRow.anchorMax = new Vector2(0.5f, 0.5f);
_latestRow.pivot = new Vector2(0.5f, 0.5f);
_latestRow.anchoredPosition = new Vector2(_viewport.anchoredPosition.x, -284f);
ApplyRowSize(_latestRow, rowHeight);
int rank = statsService.GetRecordRank(latestRecord);
FillRow(_latestRow, rank, latestRecord);
}
private void ConfigureRowTransform(RectTransform row, int index, float rowHeight, float rowStep)
{
row.anchorMin = new Vector2(0.5f, 1f);
row.anchorMax = new Vector2(0.5f, 1f);
row.pivot = new Vector2(0.5f, 1f);
row.anchoredPosition = new Vector2(0f, -index * rowStep);
ApplyRowSize(row, rowHeight);
}
private void ApplyRowSize(RectTransform row, float rowHeight)
{
float rowWidth = GetRowWidth();
float horizontalScale = rowWidth > 0f ? _viewport.rect.width / rowWidth : 1f;
row.sizeDelta = new Vector2(rowWidth, rowHeight);
row.localScale = new Vector3(horizontalScale, 1f, 1f);
}
private void FillRow(RectTransform row, int rank, PlayerStatsRecord record)
{
TextMeshProUGUI[] cells = row.GetComponentsInChildren<TextMeshProUGUI>(true);
if (cells.Length < 5)
return;
cells[0].text = rank.ToString();
cells[1].text = record.PlayerName;
cells[2].text = FormatTime(record.CompletionTimeSec);
cells[3].text = record.MovesCount.ToString();
cells[4].text = record.CompletedAt.ToString(DateFormat);
if (row == _latestRow)
cells[4].fontSize = 19f;
}
private void UpdateScrolling()
{
int direction = In.GetAxis(JoystickSide.Both, InteractMode.Continious).y;
float targetSpeed = -direction * _maxScrollSpeed;
float acceleration = direction == 0 ? _scrollDeceleration : _scrollAcceleration;
_scrollSpeed = Mathf.MoveTowards(
_scrollSpeed,
targetSpeed,
acceleration * Time.unscaledDeltaTime);
if (Mathf.Approximately(_scrollSpeed, 0f))
return;
float previousOffset = _scrollOffset;
_scrollOffset = Mathf.Clamp(
_scrollOffset + _scrollSpeed * Time.unscaledDeltaTime,
0f,
_maxScrollOffset);
if (Mathf.Approximately(previousOffset, _scrollOffset))
_scrollSpeed = 0f;
UpdateContentPosition();
UpdateScrollbar();
}
private void UpdateContentPosition()
{
if (_content != null)
_content.anchoredPosition = Vector2.up * _scrollOffset;
}
private void UpdateScrollbar()
{
float trackHeight = _scrollbarTrack.rect.height;
float contentHeight = _content.rect.height;
float handleHeight = contentHeight <= 0f
? trackHeight
: Mathf.Clamp(
trackHeight * _viewport.rect.height / contentHeight,
_minimumScrollbarHandleHeight,
trackHeight);
_scrollbarHandle.anchorMin = new Vector2(0.5f, 1f);
_scrollbarHandle.anchorMax = new Vector2(0.5f, 1f);
_scrollbarHandle.pivot = new Vector2(0.5f, 1f);
_scrollbarHandle.sizeDelta = new Vector2(_scrollbarHandle.sizeDelta.x, handleHeight);
float normalized = _maxScrollOffset <= 0f ? 0f : _scrollOffset / _maxScrollOffset;
_scrollbarHandle.anchoredPosition = new Vector2(
0f,
-normalized * (trackHeight - handleHeight));
}
private void CompleteClose()
{
IsOpen = false;
_isTransitioning = false;
_scrollSpeed = 0f;
_canvasGroup.alpha = 1f;
_panel.localScale = _panelOpenedScale;
transform.SetParent(_originalParent, false);
gameObject.SetActive(false);
_transition = null;
Closed?.Invoke();
}
private float GetRowHeight()
{
RectTransform rowRect = _rowPrefab != null
? _rowPrefab.GetComponent<RectTransform>()
: null;
return rowRect != null && rowRect.rect.height > 0f ? rowRect.rect.height : 66f;
}
private float GetRowWidth()
{
RectTransform rowRect = _rowPrefab != null
? _rowPrefab.GetComponent<RectTransform>()
: null;
return rowRect != null && rowRect.rect.width > 0f ? rowRect.rect.width : _viewport.rect.width;
}
private string FormatTime(float seconds)
{
int totalSeconds = Mathf.Max(0, Mathf.RoundToInt(seconds));
return $"{totalSeconds / 60:D2}:{totalSeconds % 60:D2}";
}
}