Files
AlexanderandClaude Opus 5.5 15b06b8f42 Модули Чата и Круга, разворот чата на весь экран, кнопка «Почта» в Safe Area
Модуль Чата (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>
2026-10-19 19:07:45 +02:00

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];
}
}