using System; using System.Collections.Generic; using System.Globalization; using TMPro; using UnityEngine; using UnityEngine.UI; // Одна строка таблицы рейтинга (ссылки на её элементы; заполняет RatingPanel) [Serializable] public class RatingRowView { public GameObject root; public Image background; public Image medal; public TMP_Text placeText; public Image avatar; public TMP_Text avatarLetter; public TMP_Text nameText; public Image circleBanner; public TMP_Text circleText; public TMP_Text powerText; public Button inspectButton; } // Панель "Рейтинг": разделы слева, таблица "Место / Игрок / Круг / Мощь", страницы внизу. public class RatingPanel : MonoBehaviour { public const int PageSize = 10; [Header("Общее")] public Button closeButton; public TMP_Text statusText; [Header("Разделы (порядок как в RatingCategory)")] public Button[] categoryButtons; [Header("Таблица")] public TMP_Text nameColumnHeader; public TMP_Text circleColumnHeader; public RatingRowView[] rows; public TMP_Text emptyText; [Header("Страницы")] public Button prevPageButton; public Button nextPageButton; public Button[] pageButtons; static readonly Color GoldRow = new Color(0.45f, 0.32f, 0.1f, 0.95f); static readonly Color SilverRow = new Color(0.28f, 0.31f, 0.36f, 0.95f); static readonly Color BronzeRow = new Color(0.36f, 0.22f, 0.12f, 0.95f); static readonly Color EvenRow = new Color(0.07f, 0.09f, 0.13f, 0.95f); static readonly Color OddRow = new Color(0.1f, 0.12f, 0.17f, 0.95f); static readonly Color[] MedalColors = { new Color(0.95f, 0.75f, 0.3f), new Color(0.8f, 0.83f, 0.88f), new Color(0.8f, 0.5f, 0.28f) }; static readonly Color ActiveButton = new Color(0.16f, 0.35f, 0.7f); static readonly Color InactiveButton = new Color(0.1f, 0.12f, 0.17f); static readonly Color[] BannerColors = { new Color(0.15f, 0.35f, 0.75f), new Color(0.45f, 0.2f, 0.18f), new Color(0.7f, 0.15f, 0.15f), new Color(0.2f, 0.45f, 0.2f), new Color(0.75f, 0.5f, 0.1f), new Color(0.35f, 0.35f, 0.4f) }; private RatingCategory category = RatingCategory.Players; private int pageIndex; void Awake() { closeButton.onClick.AddListener(Close); prevPageButton.onClick.AddListener(() => ShowPage(pageIndex - 1)); nextPageButton.onClick.AddListener(() => ShowPage(pageIndex + 1)); for (int i = 0; i < categoryButtons.Length; i++) { RatingCategory c = (RatingCategory)i; categoryButtons[i].onClick.AddListener(() => SelectCategory(c)); } foreach (var row in rows) { // Круглая медаль -- спрайт генерируется в коде, поэтому ставим его здесь, а не в сборщике сцены row.medal.sprite = PrimitiveSprites.Circle; } } public void Open() { gameObject.SetActive(true); SelectCategory(category); } public void Close() { gameObject.SetActive(false); } void SelectCategory(RatingCategory newCategory) { category = newCategory; for (int i = 0; i < categoryButtons.Length; i++) categoryButtons[i].image.color = i == (int)category ? ActiveButton : InactiveButton; bool circles = category == RatingCategory.Circles; nameColumnHeader.text = circles ? "Круг" : "Игрок"; circleColumnHeader.text = circles ? "Глава" : "Круг"; ShowPage(0); } int PageCount => Mathf.Max(1, Mathf.CeilToInt(RatingService.Instance.GetTotalCount(category) / (float)PageSize)); void ShowPage(int newPageIndex) { pageIndex = Mathf.Clamp(newPageIndex, 0, PageCount - 1); statusText.text = ""; bool available = RatingService.Instance.IsAvailable(category); emptyText.gameObject.SetActive(!available); emptyText.text = "Раздел в разработке"; List entries = available ? RatingService.Instance.GetPage(category, pageIndex, PageSize) : new List(); for (int i = 0; i < rows.Length; i++) { if (i < entries.Count) FillRow(rows[i], entries[i], i); else rows[i].root.SetActive(false); } UpdatePagination(available); } void FillRow(RatingRowView row, RatingEntry entry, int indexOnPage) { row.root.SetActive(true); bool top3 = entry.place <= 3; row.background.color = entry.place == 1 ? GoldRow : entry.place == 2 ? SilverRow : entry.place == 3 ? BronzeRow : indexOnPage % 2 == 0 ? EvenRow : OddRow; row.medal.gameObject.SetActive(top3); if (top3) row.medal.color = MedalColors[entry.place - 1]; row.placeText.text = entry.place.ToString(); row.placeText.color = top3 ? new Color(0.2f, 0.12f, 0.05f) : Color.white; // Аватар-заглушка: цветной квадрат с первой буквой имени row.avatar.color = Color.Lerp(BannerColors[StableHash(entry.name) % BannerColors.Length], Color.black, 0.25f); row.avatarLetter.text = entry.name.Substring(0, 1).ToUpper(); row.nameText.text = entry.name; row.circleText.text = entry.circleName; row.circleBanner.color = BannerColors[StableHash(category == RatingCategory.Circles ? entry.name : entry.circleName) % BannerColors.Length]; row.powerText.text = FormatPower(entry.power); string name = entry.name; row.inspectButton.onClick.RemoveAllListeners(); row.inspectButton.onClick.AddListener(() => statusText.text = $"Профиль «{name}» пока недоступен"); } void UpdatePagination(bool available) { int pageCount = PageCount; prevPageButton.interactable = available && pageIndex > 0; nextPageButton.interactable = available && pageIndex < pageCount - 1; // Окно из 5 номеров вокруг текущей страницы int first = Mathf.Clamp(pageIndex - pageButtons.Length / 2, 0, Mathf.Max(0, pageCount - pageButtons.Length)); for (int i = 0; i < pageButtons.Length; i++) { int page = first + i; Button button = pageButtons[i]; bool visible = available && page < pageCount; button.gameObject.SetActive(visible); if (!visible) continue; button.GetComponentInChildren().text = (page + 1).ToString(); button.image.color = page == pageIndex ? ActiveButton : InactiveButton; button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => ShowPage(page)); } } // 128530000 -> "128 530 000" static string FormatPower(long power) { return power.ToString("N0", CultureInfo.InvariantCulture).Replace(',', ' '); } // string.GetHashCode в Unity может меняться между запусками -- для цвета баннера нужен стабильный static int StableHash(string s) { int hash = 17; foreach (char c in s) hash = hash * 31 + c; return hash & int.MaxValue; } }