328 lines
11 KiB
C#
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}";
|
|
}
|
|
}
|