Files
MVP1/Assets/Scripts/Modules/Chat/ChatService.cs
T
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

119 lines
5.2 KiB
C#

using System;
using System.Collections.Generic;
// МОДУЛЬ ЧАТА: общий чат мира и чат Круга. Документ: Docs/Modules/Chat.md
// Без Unity: когда появится сервер, этот класс заменится сетевой реализацией IChat,
// а панель чата (слой приложения) менять не придётся.
//
// История живёт, пока открыта игра: модуль зарегистрирован в GameServices и переживает смену сцен.
// В файл сохранения чат не пишется -- переписку будет хранить сервер.
public sealed class ChatService : IChat
{
public const int MaxLength = 200;
public const int MaxStoredPerChannel = 100;
public int MaxMessageLength => MaxLength;
private readonly Dictionary<ChatChannel, List<ChatMessage>> history = new Dictionary<ChatChannel, List<ChatMessage>>
{
{ ChatChannel.World, new List<ChatMessage>() },
{ ChatChannel.Circle, new List<ChatMessage>() }
};
public ChatService(long nowMs)
{
SeedDemoMessages(nowMs);
// Канал "Круг" -- переписка текущего Круга игрока (модуль Круга сообщает о вступлении и выходе)
EventBus.Subscribe<CircleJoinedEvent>(OnCircleJoined);
EventBus.Subscribe<CircleLeftEvent>(OnCircleLeft);
EventBus.Subscribe<GameResetEvent>(_ => history[ChatChannel.Circle].Clear()); // новая игра -- без Круга
}
public const string SystemSenderId = "system";
// Писать в канал "Круг" можно, только состоя в Круге (если модуль Круга подключён)
public bool CanWrite(ChatChannel channel)
{
if (channel != ChatChannel.Circle) return true;
return !GameServices.TryGet(out ICircle circle) || circle.IsInCircle;
}
void OnCircleJoined(CircleJoinedEvent e)
{
history[ChatChannel.Circle].Clear();
string text = e.Created
? $"Вы основали Круг «{e.CircleName}». Зовите соратников!"
: $"Вы вступили в Круг «{e.CircleName}».";
Add(new ChatMessage { channel = ChatChannel.Circle, senderId = SystemSenderId, senderName = "Круг", text = text, timeMs = NowMs });
}
void OnCircleLeft(CircleLeftEvent e)
{
history[ChatChannel.Circle].Clear();
Add(new ChatMessage
{
channel = ChatChannel.Circle,
senderId = SystemSenderId,
senderName = "Круг",
text = e.Disbanded ? $"Круг «{e.CircleName}» распущен." : $"Вы покинули Круг «{e.CircleName}».",
timeMs = NowMs
});
}
static long NowMs => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
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 (!CanWrite(channel)) return false;
if (text.Length > MaxLength) text = text.Substring(0, MaxLength);
Add(new ChatMessage
{
channel = channel,
senderId = LocalPlayer.Id,
senderName = LocalPlayer.Name,
text = text,
timeMs = NowMs
});
return true;
}
// Входящее сообщение (сейчас -- только свои; позже сюда придут сообщения с сервера)
public void Receive(ChatMessage message)
{
Add(message);
}
void Add(ChatMessage message)
{
List<ChatMessage> list = history[message.channel];
list.Add(message);
if (list.Count > MaxStoredPerChannel) list.RemoveAt(0);
EventBus.Publish(new ChatMessageReceivedEvent { Message = 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);
}
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 });
}
}