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(); if (_canvasGroup == null) _canvasGroup = gameObject.AddComponent(); } _panelOpenedScale = _panel.localScale; VerticalLayoutGroup oldLayout = _viewport.GetComponent(); if (oldLayout != null) oldLayout.enabled = false; } private void BuildRating() { ClearGeneratedRows(); PlayerStatsService statsService = GameManager.Instance?.StatsService; List records = statsService?.GetTopPlayers(_maxRecords) ?? new List(); 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(); _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(); 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(); _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(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() : null; return rowRect != null && rowRect.rect.height > 0f ? rowRect.rect.height : 66f; } private float GetRowWidth() { RectTransform rowRect = _rowPrefab != null ? _rowPrefab.GetComponent() : 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}"; } }