Модуль Чата (Bogatyri.Chat, без Unity): - чат вынесен из Social в отдельный модуль; контракт IChat в ядре, событие ChatMessageReceivedEvent, LocalPlayer (id и имя игрока) - панель чата работает только через контракт и события - разворот чата на весь экран: кнопка «Развернуть/Свернуть» или Esc Модуль Круга (Bogatyri.Circle, без Unity): - основать Круг, вступить, выйти; роли главы и участника; 30 мест - уход главы передаёт главенство, последний участник распускает Круг - секция сохранения "circle", участвует в сбросе сохранений - окно «Круг» (кнопка внизу карты мира), демо-Круги до появления сервера - канал «Круг» в чате — только для участников Круга Прочее: - кнопка «Почта» привязана к правому краю нижней панели (в безопасной зоне она уезжала за экран) - тесты Чата и Круга; документы Chat.md, Circle.md, обновлены README, Core, Save Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
6.3 KiB
C#
148 lines
6.3 KiB
C#
using System;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
// Окно "Круг" (слой приложения): вступить, основать, состав, покинуть.
|
|
// Сам Круг -- модуль Круга: только через контракт ICircle и события CircleJoinedEvent / CircleLeftEvent.
|
|
// Документ: Docs/Modules/Circle.md
|
|
//
|
|
// Открыть: CircleWindow.Open() -- кнопка "Круг" внизу карты мира.
|
|
public static class CircleWindow
|
|
{
|
|
const float ListHeight = 620f;
|
|
|
|
static GameObject root;
|
|
|
|
static ICircle Circle => GameServices.Get<ICircle>();
|
|
|
|
public static bool IsOpen => root != null;
|
|
|
|
public static void Open()
|
|
{
|
|
Close();
|
|
RectTransform card = UIKit.Modal(Circle.IsInCircle ? Circle.Current.name : "Круг", out root);
|
|
if (Circle.IsInCircle) BuildMyCircle(card);
|
|
else BuildChooseCircle(card);
|
|
|
|
var close = UIKit.Button(card, "Закрыть", UIKit.Theme.Gray, Close, -1, 90, 30);
|
|
UIKit.Size(close, -1, 90);
|
|
}
|
|
|
|
public static void Close()
|
|
{
|
|
if (root != null) UnityEngine.Object.Destroy(root);
|
|
root = null;
|
|
}
|
|
|
|
// ---------------- Игрок не в Круге ----------------
|
|
|
|
static void BuildChooseCircle(RectTransform card)
|
|
{
|
|
UIKit.Text(card, "Вы не состоите в Круге. Вступите в один из Кругов или основайте свой — " +
|
|
"вместе проще брать крепости и держать территорию.", 28, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
|
|
|
UIKit.Button(card, "Основать свой Круг", UIKit.Theme.Gold, AskNameAndCreate, -1, 90, 30);
|
|
|
|
var list = ScrollArea(card);
|
|
foreach (var c in Circle.GetCircles())
|
|
{
|
|
CircleInfo circle = c;
|
|
bool full = circle.members.Count >= Circle.MaxMembers;
|
|
|
|
var row = UIKit.Row(list, 100, 16, UIKit.Theme.Card, 14);
|
|
UIKit.Icon(row, BannerColor(circle.name), circle.name.Substring(0, 1), 72);
|
|
|
|
var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 2);
|
|
UIKit.Size(info, -1, -1, 1);
|
|
UIKit.Text(info, circle.name, 30, UIKit.Theme.Text).fontStyle = FontStyles.Bold;
|
|
UIKit.Text(info, $"Глава: {circle.LeaderName} • участников {circle.members.Count} / {Circle.MaxMembers}",
|
|
22, UIKit.Theme.TextDim);
|
|
|
|
var join = UIKit.Button(row, full ? "Мест нет" : "Вступить", full ? UIKit.Theme.Gray : UIKit.Theme.Green,
|
|
() => Join(circle), -1, 76, 26);
|
|
UIKit.Size(join, 210, 76);
|
|
join.interactable = !full;
|
|
}
|
|
}
|
|
|
|
static void AskNameAndCreate()
|
|
{
|
|
UIKit.InputDialog("Название Круга", "", CircleService.MaxNameLength, name =>
|
|
{
|
|
if (Circle.TryCreate(name, out string error))
|
|
{
|
|
UIKit.Toast($"Круг «{name}» основан. Вы -- глава");
|
|
Open();
|
|
}
|
|
else UIKit.Toast(error);
|
|
});
|
|
}
|
|
|
|
static void Join(CircleInfo circle)
|
|
{
|
|
if (Circle.TryJoin(circle.id, out string error))
|
|
{
|
|
UIKit.Toast($"Вы вступили в Круг «{circle.name}»");
|
|
Open();
|
|
}
|
|
else UIKit.Toast(error);
|
|
}
|
|
|
|
// ---------------- Игрок в Круге ----------------
|
|
|
|
static void BuildMyCircle(RectTransform card)
|
|
{
|
|
CircleInfo circle = Circle.Current;
|
|
string role = Circle.IsLeader ? "Глава" : "Участник";
|
|
UIKit.Text(card, $"Ваша роль: {role} • участников {circle.members.Count} / {Circle.MaxMembers}",
|
|
28, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
|
|
|
var list = ScrollArea(card);
|
|
foreach (var m in circle.members)
|
|
{
|
|
bool me = m.playerId == LocalPlayer.Id;
|
|
var row = UIKit.Row(list, 80, 14, me ? new Color(0.24f, 0.24f, 0.3f) : UIKit.Theme.Card, 12);
|
|
UIKit.Icon(row, BannerColor(m.name), m.name.Substring(0, 1), 56);
|
|
var name = UIKit.Text(row, me ? m.name + " (вы)" : m.name, 28, me ? UIKit.Theme.Gold : UIKit.Theme.Text);
|
|
UIKit.Size(name, -1, -1, 1);
|
|
UIKit.Size(UIKit.Text(row, m.role == CircleRole.Leader ? "Глава" : "Участник", 24,
|
|
m.role == CircleRole.Leader ? UIKit.Theme.Gold : UIKit.Theme.TextDim, TextAlignmentOptions.Right), 180);
|
|
}
|
|
|
|
string leaveText = circle.members.Count == 1
|
|
? "Вы в Круге один -- если выйти, Круг будет распущен."
|
|
: Circle.IsLeader ? "Главой станет старейший участник Круга." : "Вернуться можно в любой момент, если будут места.";
|
|
UIKit.Button(card, "Покинуть Круг", UIKit.Theme.Red, () =>
|
|
UIKit.Confirm("Покинуть Круг?", leaveText, "Покинуть", () =>
|
|
{
|
|
Circle.Leave();
|
|
UIKit.Toast("Вы покинули Круг");
|
|
Open();
|
|
}), -1, 90, 30);
|
|
}
|
|
|
|
// ---------------- Помощники ----------------
|
|
|
|
// Список фиксированной высоты: окно растягивается по содержимому, а Кругов может быть много
|
|
static RectTransform ScrollArea(RectTransform card)
|
|
{
|
|
var holder = UIKit.Make("ListHolder", card);
|
|
UIKit.Size(holder, -1, ListHeight);
|
|
return UIKit.ScrollList(holder, out ScrollRect _, 10);
|
|
}
|
|
|
|
// Один и тот же Круг -- всегда один цвет (string.GetHashCode в Unity меняется между запусками)
|
|
static Color BannerColor(string name)
|
|
{
|
|
Color[] colors =
|
|
{
|
|
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)
|
|
};
|
|
int hash = 17;
|
|
foreach (char ch in name) hash = hash * 31 + ch;
|
|
return colors[(hash & int.MaxValue) % colors.Length];
|
|
}
|
|
}
|