HUD мира

This commit is contained in:
Alexander
2026-09-26 17:50:11 +02:00
parent 26c0a20520
commit c86f43a806
12 changed files with 23294 additions and 1979 deletions
+246 -6
View File
@@ -71,10 +71,26 @@ public static class WorldMapSceneBuilder
AnchorSide(myCity, left: false, new Vector2(280, 80), 20);
controller.myCityButton = myCity.GetComponent<Button>();
// ---- Нижняя панель: подсказка ----
RectTransform bottomBar = CreatePanel(canvasRect, "BottomBar", new Color(0.08f, 0.08f, 0.1f, 0.85f));
AnchorStretch(bottomBar, top: false, height: 100);
controller.hintText = CreateText(bottomBar, "Hint", "", 32);
// ---- Подсказка (над нижней панелью, правее чата) ----
RectTransform hintBar = CreatePanel(canvasRect, "HintBar", new Color(0.08f, 0.08f, 0.1f, 0.75f));
SetRect(hintBar, new Vector2(0, 0), new Vector2(1, 0), new Vector2(0.5f, 0), Vector2.zero, Vector2.zero);
hintBar.offsetMin = new Vector2(ChatWidth + 24, 172);
hintBar.offsetMax = new Vector2(-12, 222);
controller.hintText = CreateText(hintBar, "Hint", "", 28);
// ---- Чат слева внизу ----
BuildChat(canvasRect);
// ---- Нижняя панель: Карта мира | События | Круг | Поселение ----
BuildNavBar(canvasRect, controller);
// ---- Кнопка "Рейтинг" слева ----
RectTransform ratingBtn = CreateButton(canvasRect, "RatingButton", "Рейтинг", new Color(0.12f, 0.2f, 0.4f, 0.95f), out _);
SetRect(ratingBtn, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(12, -122), new Vector2(210, 110));
controller.ratingButton = ratingBtn.GetComponent<Button>();
// ---- Панель рейтинга (выше всего, кроме окна атаки и уведомлений) ----
controller.ratingPanel = BuildRatingPanel(canvasRect);
// ---- Окно атаки ----
RectTransform attack = CreateCenterPanel(canvasRect, "AttackPanel", new Vector2(900, 640));
@@ -134,6 +150,229 @@ public static class WorldMapSceneBuilder
EditorSceneManager.SaveScene(scene, WorldMapScenePath);
}
// ---------------- ЧАТ ----------------
const float ChatWidth = 620f;
static void BuildChat(RectTransform canvasRect)
{
RectTransform panel = CreatePanel(canvasRect, "ChatPanel", new Color(0.05f, 0.06f, 0.09f, 0.9f));
SetRect(panel, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(12, 12), new Vector2(ChatWidth, 350));
ChatPanel chat = panel.gameObject.AddComponent<ChatPanel>();
RectTransform worldTab = CreateButton(panel, "WorldTab", "Общий мир", new Color(0.16f, 0.35f, 0.7f), out _);
SetRect(worldTab, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(10, -10), new Vector2(220, 54));
chat.worldTabButton = worldTab.GetComponent<Button>();
RectTransform circleTab = CreateButton(panel, "CircleTab", "Круг", new Color(0.12f, 0.13f, 0.17f), out _);
SetRect(circleTab, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(240, -10), new Vector2(220, 54));
chat.circleTabButton = circleTab.GetComponent<Button>();
// Лента: текст прижат к низу, старые строки уходят вверх и обрезаются маской
var messagesGO = new GameObject("Messages", typeof(RectTransform), typeof(RectMask2D));
RectTransform messages = messagesGO.GetComponent<RectTransform>();
messages.SetParent(panel, false);
messages.anchorMin = Vector2.zero;
messages.anchorMax = Vector2.one;
messages.offsetMin = new Vector2(14, 86);
messages.offsetMax = new Vector2(-14, -74);
RectTransform messagesText = CreateTextRect(messages, "MessagesText", "", 24, TextAlignmentOptions.BottomLeft);
TMP_Text tmp = messagesText.GetComponent<TMP_Text>();
tmp.overflowMode = TextOverflowModes.Overflow;
chat.messagesText = tmp;
// Поле ввода
GameObject inputGO = TMP_DefaultControls.CreateInputField(new TMP_DefaultControls.Resources());
inputGO.name = "InputField";
RectTransform inputRect = inputGO.GetComponent<RectTransform>();
inputRect.SetParent(panel, false);
SetRect(inputRect, Vector2.zero, new Vector2(1, 0), new Vector2(0.5f, 0), Vector2.zero, Vector2.zero);
inputRect.offsetMin = new Vector2(12, 12);
inputRect.offsetMax = new Vector2(-124, 74);
inputGO.GetComponent<Image>().color = new Color(0.12f, 0.13f, 0.17f);
TMP_InputField input = inputGO.GetComponent<TMP_InputField>();
input.pointSize = 26;
input.textComponent.color = Color.white;
((TMP_Text)input.placeholder).text = "Введите сообщение...";
((TMP_Text)input.placeholder).color = new Color(1f, 1f, 1f, 0.35f);
chat.inputField = input;
RectTransform send = CreateButton(panel, "SendButton", ">", new Color(0.2f, 0.45f, 0.85f), out _);
SetRect(send, new Vector2(1, 0), new Vector2(1, 0), new Vector2(1, 0), new Vector2(-12, 12), new Vector2(100, 62));
chat.sendButton = send.GetComponent<Button>();
}
// ---------------- НИЖНЯЯ ПАНЕЛЬ ----------------
static void BuildNavBar(RectTransform canvasRect, WorldMapController controller)
{
RectTransform bar = CreatePanel(canvasRect, "BottomNavBar", new Color(0.06f, 0.07f, 0.1f, 0.92f));
SetRect(bar, Vector2.zero, new Vector2(1, 0), new Vector2(0.5f, 0), Vector2.zero, Vector2.zero);
bar.offsetMin = new Vector2(ChatWidth + 24, 12);
bar.offsetMax = new Vector2(-12, 162);
var layout = bar.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.padding = new RectOffset(10, 10, 10, 10);
layout.spacing = 10;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = true;
layout.childControlWidth = true;
layout.childControlHeight = true;
Color navColor = new Color(0.14f, 0.16f, 0.22f);
controller.worldMapNavButton = CreateButton(bar, "Nav_WorldMap", "Карта мира", navColor, out _).GetComponent<Button>();
controller.eventsNavButton = CreateButton(bar, "Nav_Events", "События", navColor, out _).GetComponent<Button>();
controller.circleNavButton = CreateButton(bar, "Nav_Circle", "Круг", navColor, out _).GetComponent<Button>();
controller.settlementNavButton = CreateButton(bar, "Nav_Settlement", "Поселение", new Color(0.18f, 0.25f, 0.45f), out _).GetComponent<Button>();
}
// ---------------- РЕЙТИНГ ----------------
static readonly Color RatingBg = new Color(0.04f, 0.06f, 0.1f, 0.97f);
static readonly Color RatingGold = new Color(0.95f, 0.8f, 0.5f);
const float RatingTableLeft = 340f;
const float RatingTableRight = 25f;
static RatingPanel BuildRatingPanel(RectTransform canvasRect)
{
RectTransform panel = CreatePanel(canvasRect, "RatingPanel", RatingBg);
SetRect(panel, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(1640, 920));
RatingPanel rating = panel.gameObject.AddComponent<RatingPanel>();
// Заголовок-баннер
RectTransform banner = CreatePanel(panel, "TitleBanner", new Color(0.12f, 0.25f, 0.55f));
SetRect(banner, new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -15), new Vector2(640, 90));
CreateText(banner, "Title", "Рейтинг", 60).color = RatingGold;
RectTransform close = CreateButton(panel, "CloseButton", "X", new Color(0.35f, 0.15f, 0.15f), out _);
SetRect(close, new Vector2(1, 1), new Vector2(1, 1), new Vector2(1, 1), new Vector2(-15, -15), new Vector2(80, 80));
rating.closeButton = close.GetComponent<Button>();
// Разделы слева -- порядок совпадает с RatingCategory
string[] categories = { "Игроки", "Круги", "Мощь", "PvP", "Развитие", "Сезон" };
rating.categoryButtons = new Button[categories.Length];
for (int i = 0; i < categories.Length; i++)
{
RectTransform tab = CreateButton(panel, "Category_" + i, categories[i], new Color(0.1f, 0.12f, 0.17f), out _);
SetRect(tab, new Vector2(0, 1), new Vector2(0, 1), new Vector2(0, 1), new Vector2(25, -130 - i * 112), new Vector2(290, 100));
rating.categoryButtons[i] = tab.GetComponent<Button>();
}
// Таблица
var tableGO = new GameObject("Table", typeof(RectTransform));
RectTransform table = tableGO.GetComponent<RectTransform>();
table.SetParent(panel, false);
table.anchorMin = Vector2.zero;
table.anchorMax = Vector2.one;
table.offsetMin = new Vector2(RatingTableLeft, 110);
table.offsetMax = new Vector2(-RatingTableRight, -130);
RectTransform header = CreatePanel(table, "Header", new Color(0.08f, 0.1f, 0.15f));
SetRect(header, new Vector2(0, 1), new Vector2(1, 1), new Vector2(0.5f, 1), Vector2.zero, new Vector2(0, 50));
Color headerColor = new Color(0.8f, 0.82f, 0.88f);
PlaceInRow(CreateTextRect(header, "Place", "Место", 26), 0, 140, 50).GetComponent<TMP_Text>().color = headerColor;
TMP_Text nameHeader = PlaceInRow(CreateTextRect(header, "Name", "Игрок", 26, TextAlignmentOptions.Left), 225, 330, 50).GetComponent<TMP_Text>();
nameHeader.color = headerColor;
rating.nameColumnHeader = nameHeader;
TMP_Text circleHeader = PlaceInRow(CreateTextRect(header, "Circle", "Круг", 26, TextAlignmentOptions.Left), 625, 320, 50).GetComponent<TMP_Text>();
circleHeader.color = headerColor;
rating.circleColumnHeader = circleHeader;
PlaceInRow(CreateTextRect(header, "Power", "Мощь", 26), 950, 220, 50).GetComponent<TMP_Text>().color = headerColor;
rating.rows = new RatingRowView[RatingPanel.PageSize];
for (int i = 0; i < rating.rows.Length; i++)
{
rating.rows[i] = BuildRatingRow(table, i);
}
rating.emptyText = CreateText(table, "EmptyText", "Раздел в разработке", 40);
rating.emptyText.gameObject.SetActive(false);
RectTransform status = CreateTextRect(panel, "StatusText", "", 26, TextAlignmentOptions.Left);
SetRect(status, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(RatingTableLeft, 30), new Vector2(520, 60));
rating.statusText = status.GetComponent<TMP_Text>();
// Страницы: < 1 2 3 4 5 > -- по центру под таблицей (а не под всей панелью)
float tableCenterX = (RatingTableLeft - RatingTableRight) / 2f;
Color pageColor = new Color(0.1f, 0.12f, 0.17f);
RectTransform prev = CreateButton(panel, "PrevPage", "<", pageColor, out _);
SetRect(prev, new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(tableCenterX - 240, 25), new Vector2(64, 64));
rating.prevPageButton = prev.GetComponent<Button>();
rating.pageButtons = new Button[5];
for (int i = 0; i < rating.pageButtons.Length; i++)
{
RectTransform page = CreateButton(panel, "Page_" + i, (i + 1).ToString(), pageColor, out _);
SetRect(page, new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(tableCenterX - 150 + i * 75, 25), new Vector2(64, 64));
rating.pageButtons[i] = page.GetComponent<Button>();
}
RectTransform next = CreateButton(panel, "NextPage", ">", pageColor, out _);
SetRect(next, new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(tableCenterX + 240, 25), new Vector2(64, 64));
rating.nextPageButton = next.GetComponent<Button>();
panel.gameObject.SetActive(false);
return rating;
}
static RatingRowView BuildRatingRow(RectTransform table, int index)
{
const float rowHeight = 54f;
var view = new RatingRowView();
RectTransform row = CreatePanel(table, "Row_" + (index + 1), new Color(0.07f, 0.09f, 0.13f));
SetRect(row, new Vector2(0, 1), new Vector2(1, 1), new Vector2(0.5f, 1), new Vector2(0, -56 - index * 58), new Vector2(0, rowHeight));
view.root = row.gameObject;
view.background = row.GetComponent<Image>();
RectTransform medal = CreatePanel(row, "Medal", Color.white);
PlaceInRow(medal, 45, 50, 50);
view.medal = medal.GetComponent<Image>();
view.placeText = PlaceInRow(CreateTextRect(row, "Place", "1", 30), 0, 140, rowHeight).GetComponent<TMP_Text>();
RectTransform avatar = CreatePanel(row, "Avatar", Color.gray);
PlaceInRow(avatar, 150, 48, 48);
view.avatar = avatar.GetComponent<Image>();
view.avatarLetter = CreateText(avatar, "Letter", "В", 28);
view.nameText = PlaceInRow(CreateTextRect(row, "Name", "Игрок", 30, TextAlignmentOptions.Left), 225, 330, rowHeight).GetComponent<TMP_Text>();
RectTransform circleBanner = CreatePanel(row, "CircleBanner", Color.blue);
PlaceInRow(circleBanner, 570, 36, 46);
view.circleBanner = circleBanner.GetComponent<Image>();
view.circleText = PlaceInRow(CreateTextRect(row, "Circle", "Круг", 28, TextAlignmentOptions.Left), 625, 320, rowHeight).GetComponent<TMP_Text>();
TMP_Text power = PlaceInRow(CreateTextRect(row, "Power", "0", 28), 950, 220, rowHeight).GetComponent<TMP_Text>();
power.color = RatingGold;
view.powerText = power;
RectTransform inspect = CreateButton(row, "InspectButton", "?", new Color(0.18f, 0.2f, 0.27f), out _);
PlaceInRow(inspect, 1190, 56, 44);
view.inspectButton = inspect.GetComponent<Button>();
return view;
}
// Элемент строки: прижат к левому краю строки с отступом x, по вертикали по центру
static RectTransform PlaceInRow(RectTransform rt, float x, float width, float height)
{
SetRect(rt, new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(x, 0), new Vector2(width, height));
return rt;
}
static void SetRect(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 position, Vector2 size)
{
rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax;
rt.pivot = pivot;
rt.sizeDelta = size;
rt.anchoredPosition = position;
}
// ---------------- BUILD SETTINGS ----------------
static void UpdateBuildSettings()
@@ -194,7 +433,8 @@ public static class WorldMapSceneBuilder
return rt;
}
static RectTransform CreateTextRect(RectTransform parent, string name, string content, int fontSize)
static RectTransform CreateTextRect(RectTransform parent, string name, string content, int fontSize,
TextAlignmentOptions alignment = TextAlignmentOptions.Center)
{
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
go.transform.SetParent(parent, false);
@@ -202,7 +442,7 @@ public static class WorldMapSceneBuilder
tmp.text = content;
tmp.fontSize = fontSize;
tmp.color = Color.white;
tmp.alignment = TextAlignmentOptions.Center;
tmp.alignment = alignment;
tmp.raycastTarget = false;
RectTransform rt = go.GetComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
+22492 -1972
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c2bd5411b154aeb4d94a3df600743672
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+97
View File
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
// Панель чата: вкладки "Общий мир" / "Круг", лента сообщений, поле ввода и кнопка отправки.
public class ChatPanel : MonoBehaviour
{
[Header("Вкладки")]
public Button worldTabButton;
public Button circleTabButton;
[Header("Сообщения")]
public TMP_Text messagesText;
[Header("Ввод")]
public TMP_InputField inputField;
public Button sendButton;
const int MaxShownMessages = 30; // старые всё равно уходят за верхний край ленты
static readonly Color ActiveTabColor = new Color(0.16f, 0.35f, 0.7f);
static readonly Color InactiveTabColor = new Color(0.12f, 0.13f, 0.17f);
static readonly string[] NameColors = { "#6EC6FF", "#FF8A65", "#C792EA", "#81C784", "#FFD54F", "#4DD0E1" };
const string OwnNameColor = "#FFC857";
private ChatChannel currentChannel = ChatChannel.World;
private readonly StringBuilder sb = new StringBuilder();
void Start()
{
worldTabButton.onClick.AddListener(() => SwitchChannel(ChatChannel.World));
circleTabButton.onClick.AddListener(() => SwitchChannel(ChatChannel.Circle));
sendButton.onClick.AddListener(Send);
inputField.onSubmit.AddListener(_ => Send()); // Enter на клавиатуре
inputField.characterLimit = ChatService.MaxMessageLength;
ChatService.Instance.MessageReceived += OnMessageReceived;
SwitchChannel(ChatChannel.World);
}
void OnDestroy()
{
ChatService.Instance.MessageReceived -= OnMessageReceived;
}
void SwitchChannel(ChatChannel channel)
{
currentChannel = channel;
worldTabButton.image.color = channel == ChatChannel.World ? ActiveTabColor : InactiveTabColor;
circleTabButton.image.color = channel == ChatChannel.Circle ? ActiveTabColor : InactiveTabColor;
Redraw();
}
void Send()
{
if (ChatService.Instance.Send(currentChannel, inputField.text))
{
inputField.text = "";
}
inputField.ActivateInputField(); // курсор остаётся в поле -- можно писать дальше
}
void OnMessageReceived(ChatMessage message)
{
if (message.channel == currentChannel) Redraw();
}
void Redraw()
{
IReadOnlyList<ChatMessage> messages = ChatService.Instance.GetMessages(currentChannel);
int start = Mathf.Max(0, messages.Count - MaxShownMessages);
sb.Clear();
for (int i = start; i < messages.Count; i++)
{
ChatMessage m = messages[i];
string time = DateTimeOffset.FromUnixTimeMilliseconds(m.timeMs).ToLocalTime().ToString("HH:mm");
string color = m.senderId == MarchManager.LocalPlayerId ? OwnNameColor : GetNameColor(m.senderId);
if (sb.Length > 0) sb.Append('\n');
// <noparse> -- чтобы игрок не мог вставить в сообщение теги оформления
sb.Append($"<color=#9AA3B5>[{time}]</color> <color={color}>{m.senderName}:</color> <noparse>{m.text}</noparse>");
}
messagesText.text = sb.ToString();
}
// Один и тот же игрок -- всегда один цвет
static string GetNameColor(string senderId)
{
int hash = 0;
foreach (char c in senderId) hash = hash * 31 + c;
return NameColors[(hash & int.MaxValue) % NameColors.Length];
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: baf21465e5a2a3f448d21131289986a9
+95
View File
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
public enum ChatChannel
{
World, // общий чат всего мира
Circle // чат своего Круга
}
[Serializable]
public class ChatMessage
{
public ChatChannel channel;
public string senderId;
public string senderName;
public string text;
public long timeMs; // Unix-время в миллисекундах
}
// История чата. Обычный класс со статическим экземпляром -- переживает смену сцен,
// поэтому переписка не теряется при переходе город <-> карта. Пока всё локально;
// когда появится сервер, Send будет отправлять сообщение туда, а входящие -- приходить через MessageReceived.
public class ChatService
{
public const int MaxMessageLength = 200;
public const int MaxStoredPerChannel = 100;
// Пока аккаунтов нет -- одно имя для локального игрока
public const string LocalPlayerName = "Богатырь";
private static ChatService instance;
public static ChatService Instance => instance ??= new ChatService(MarchManager.NowMs);
public event Action<ChatMessage> MessageReceived;
private readonly Dictionary<ChatChannel, List<ChatMessage>> history = new Dictionary<ChatChannel, List<ChatMessage>>
{
{ ChatChannel.World, new List<ChatMessage>() },
{ ChatChannel.Circle, new List<ChatMessage>() }
};
ChatService(long nowMs)
{
SeedDemoMessages(nowMs);
}
public IReadOnlyList<ChatMessage> GetMessages(ChatChannel channel)
{
return history[channel];
}
public bool Send(ChatChannel channel, string text)
{
text = text?.Trim();
if (string.IsNullOrEmpty(text)) return false;
if (text.Length > MaxMessageLength) text = text.Substring(0, MaxMessageLength);
Add(new ChatMessage
{
channel = channel,
senderId = MarchManager.LocalPlayerId,
senderName = LocalPlayerName,
text = text,
timeMs = MarchManager.NowMs
});
return true;
}
void Add(ChatMessage message)
{
List<ChatMessage> list = history[message.channel];
list.Add(message);
if (list.Count > MaxStoredPerChannel) list.RemoveAt(0);
MessageReceived?.Invoke(message);
}
// Несколько сообщений "от других игроков", чтобы чат не был пустым. Уберётся с приходом сервера.
void SeedDemoMessages(long nowMs)
{
const long minute = 60_000;
AddDemo(ChatChannel.World, "npc_sever", "Север", "Кто идёт на Змея Горыныча?", nowMs - 5 * minute);
AddDemo(ChatChannel.World, "npc_dreadwolf", "DreadWolf", "Набор в Круг, нужны активные игроки!", nowMs - 4 * minute);
AddDemo(ChatChannel.World, "npc_moroz", "Мороз", "На западе замечен Круг Стальные Волки.", nowMs - 3 * minute);
AddDemo(ChatChannel.World, "npc_tairon", "Тайрон", "Собираем отряд на древний храм.", nowMs - 2 * minute);
AddDemo(ChatChannel.World, "npc_veles", "Велес", "Всем удачной охоты!", nowMs - minute);
AddDemo(ChatChannel.Circle, "npc_ratibor", "Ратибор", "Братья, завтра вместе идём на замок.", nowMs - 10 * minute);
AddDemo(ChatChannel.Circle, "npc_svetozar", "Светозар", "Скиньте еды в казну Круга, кому не жалко.", nowMs - 6 * minute);
}
void AddDemo(ChatChannel channel, string id, string name, string text, long timeMs)
{
history[channel].Add(new ChatMessage { channel = channel, senderId = id, senderName = name, text = text, timeMs = timeMs });
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ee8544a4534237643ada27a572040574
+199
View File
@@ -0,0 +1,199 @@
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<RatingEntry> entries = available
? RatingService.Instance.GetPage(category, pageIndex, PageSize)
: new List<RatingEntry>();
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<TMP_Text>().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;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8c9bb8e0e4ad65a4d886aa354a85da52
+125
View File
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
// Разделы рейтинга (как вкладки слева на панели)
public enum RatingCategory
{
Players,
Circles,
Power,
PvP,
Development,
Season
}
[Serializable]
public class RatingEntry
{
public int place;
public string id; // id игрока или Круга
public string name; // имя игрока или название Круга
public string circleName; // Круг игрока (для рейтинга Кругов -- имя главы)
public long power;
}
// Рейтинг. Пока заглушка: 50 выдуманных игроков, числа -- как на референсе, для проверки вёрстки.
// Когда появится сервер, он будет считать рейтинг и отдавать его постранично через GetPage.
public class RatingService
{
private static RatingService instance;
public static RatingService Instance => instance ??= new RatingService();
private readonly List<RatingEntry> players = new List<RatingEntry>();
private readonly List<RatingEntry> circles = new List<RatingEntry>();
static readonly string[] PlayerNames =
{
"Велес", "Север", "Тайрон", "DreadWolf", "Мороз", "Леший", "Князь", "Варяг", "Сокол", "Ратибор",
"Светозар", "Добромир", "Велимир", "Ярополк", "Всеслав", "Мстислав", "Остромир", "Богдан", "Любомир", "Твердислав",
"Радим", "Гостомысл", "Вышеслав", "Путята", "Ставр", "Чурила", "Микула", "Василиса", "Забава", "IronBear",
"Ярослав", "Горазд", "Будимир", "Ждан", "Первуша", "Томир", "Вадим", "Олег", "Игорь", "Святослав",
"Рогволд", "Судислав", "Любава", "Милава", "Велеслава", "Snowfox", "Berserk", "Кузьма", "Потап", "Никита"
};
static readonly string[] CircleNames =
{
"Круг Перуна", "Стальные Волки", "Сыны Руси", "Богатыри", "Северный Страж",
"Вольные", "Златые Земли", "Серые Горны", "Речные", "Перунов Круг"
};
RatingService()
{
var rng = new Random(20260926);
long power = 128_530_000;
for (int i = 0; i < PlayerNames.Length; i++)
{
players.Add(new RatingEntry
{
place = i + 1,
id = $"player_{i + 1}",
name = PlayerNames[i],
circleName = CircleNames[i % CircleNames.Length],
power = power
});
// Каждый следующий слабее на 5-15%
power = power * rng.Next(85, 96) / 100;
}
BuildCircleRating();
}
void BuildCircleRating()
{
var totals = new Dictionary<string, long>();
var leaders = new Dictionary<string, string>();
foreach (var p in players)
{
totals.TryGetValue(p.circleName, out long sum);
totals[p.circleName] = sum + p.power;
if (!leaders.ContainsKey(p.circleName)) leaders[p.circleName] = p.name; // игроки уже по убыванию мощи
}
foreach (var name in CircleNames)
{
circles.Add(new RatingEntry { id = name, name = name, circleName = leaders[name], power = totals[name] });
}
circles.Sort((a, b) => b.power.CompareTo(a.power));
for (int i = 0; i < circles.Count; i++) circles[i].place = i + 1;
}
List<RatingEntry> GetList(RatingCategory category)
{
switch (category)
{
case RatingCategory.Players:
case RatingCategory.Power: // пока мощь = рейтинг игроков
return players;
case RatingCategory.Circles:
return circles;
default:
return null;
}
}
public bool IsAvailable(RatingCategory category)
{
return GetList(category) != null;
}
public int GetTotalCount(RatingCategory category)
{
return GetList(category)?.Count ?? 0;
}
public List<RatingEntry> GetPage(RatingCategory category, int pageIndex, int pageSize)
{
var result = new List<RatingEntry>();
List<RatingEntry> list = GetList(category);
if (list == null) return result;
int start = pageIndex * pageSize;
for (int i = start; i < start + pageSize && i < list.Count; i++) result.Add(list[i]);
return result;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cba4720c503471246a5941f50449bcf9
+24 -1
View File
@@ -16,9 +16,19 @@ public class WorldMapController : MonoBehaviour
public Button backButton;
public Button myCityButton;
[Header("Нижняя панель")]
[Header("Подсказка")]
public TMP_Text hintText;
[Header("Рейтинг")]
public Button ratingButton;
public RatingPanel ratingPanel;
[Header("Нижняя панель (пока без функционала)")]
public Button worldMapNavButton;
public Button eventsNavButton;
public Button circleNavButton;
public Button settlementNavButton;
[Header("Окно атаки")]
public GameObject attackPanel;
public TMP_Text attackTitleText;
@@ -60,6 +70,11 @@ public class WorldMapController : MonoBehaviour
backButton.onClick.AddListener(SceneNavigator.ReturnToCity);
if (myCityButton != null) myCityButton.onClick.AddListener(CenterOnMyCity);
if (ratingButton != null) ratingButton.onClick.AddListener(ratingPanel.Open);
HookUpComingSoon(worldMapNavButton, "Карта мира");
HookUpComingSoon(eventsNavButton, "События");
HookUpComingSoon(circleNavButton, "Круг");
HookUpComingSoon(settlementNavButton, "Поселение");
sendButton.onClick.AddListener(OnSendPressed);
cancelButton.onClick.AddListener(CloseAttackPanel);
popupOkButton.onClick.AddListener(ShowNextPopup);
@@ -67,6 +82,7 @@ public class WorldMapController : MonoBehaviour
attackPanel.SetActive(false);
popupPanel.SetActive(false);
if (ratingPanel != null) ratingPanel.Close();
hintText.text = DefaultHint;
if (ResourceManager.Instance == null)
@@ -95,6 +111,12 @@ public class WorldMapController : MonoBehaviour
soldiersText.text = $"Войска дома: {GetSoldiersAtHome()} В походе: {World.Marches.Count}";
}
void HookUpComingSoon(Button button, string sectionName)
{
if (button == null) return;
button.onClick.AddListener(() => hintText.text = $"Раздел «{sectionName}» пока в разработке");
}
// ---------------- КАРТА ----------------
void BuildMap()
@@ -188,6 +210,7 @@ public class WorldMapController : MonoBehaviour
void HandleMapTap()
{
if (attackPanel.activeSelf || popupPanel.activeSelf) return;
if (ratingPanel != null && ratingPanel.gameObject.activeSelf) return;
if (!mapCamera.TryGetTap(out Vector3 world)) return;
MapNode node = FindNodeAt(world);