using System; using System.Collections.Generic; using System.Text; // МОДУЛЬ КРУГА: объединение игроков -- создать, вступить, выйти, роли главы и участника. // Документ: Docs/Modules/Circle.md // // Без Unity: когда появится сервер, список Кругов и состав будут приходить с него, // а этот класс заменится сетевой реализацией ICircle. // Пока других игроков нет -- Круги из рейтинга заполнены выдуманными участниками. public sealed class CircleService : ICircle, ISaveParticipant { public const int MaxMembersPerCircle = 30; public const int MinNameLength = 3; public const int MaxNameLength = 24; public int MaxMembers => MaxMembersPerCircle; private readonly List circles = new List(); private CircleInfo current; public CircleInfo Current => current; public bool IsInCircle => current != null; public bool IsLeader => current != null && FindMember(current, LocalPlayer.Id)?.role == CircleRole.Leader; public CircleService() { CreateDemoCircles(); } public IReadOnlyList GetCircles() { return circles; } // ---------------- Действия игрока ---------------- public bool TryCreate(string name, out string error) { name = name?.Trim() ?? ""; if (IsInCircle) { error = "Сначала выйдите из своего Круга"; return false; } if (!IsValidName(name, out error)) return false; if (circles.Exists(c => string.Equals(c.name, name, StringComparison.OrdinalIgnoreCase))) { error = "Круг с таким названием уже есть"; return false; } var circle = new CircleInfo { id = "player_" + Guid.NewGuid().ToString("N").Substring(0, 8), name = name }; circle.members.Add(new CircleMember { playerId = LocalPlayer.Id, name = LocalPlayer.Name, role = CircleRole.Leader }); circles.Insert(0, circle); current = circle; EventBus.Publish(new CircleJoinedEvent { CircleId = circle.id, CircleName = circle.name, Created = true }); error = null; return true; } public bool TryJoin(string circleId, out string error) { if (IsInCircle) { error = "Вы уже состоите в Круге. Сначала выйдите из него"; return false; } CircleInfo circle = circles.Find(c => c.id == circleId); if (circle == null) { error = "Такого Круга нет"; return false; } if (circle.members.Count >= MaxMembersPerCircle) { error = "В Круге нет свободных мест"; return false; } circle.members.Add(new CircleMember { playerId = LocalPlayer.Id, name = LocalPlayer.Name, role = CircleRole.Member }); current = circle; EventBus.Publish(new CircleJoinedEvent { CircleId = circle.id, CircleName = circle.name, Created = false }); error = null; return true; } public void Leave() { if (current == null) return; CircleInfo circle = current; CircleMember me = FindMember(circle, LocalPlayer.Id); circle.members.Remove(me); current = null; bool disbanded = circle.members.Count == 0; if (disbanded) { circles.Remove(circle); } else if (me != null && me.role == CircleRole.Leader) { circle.members[0].role = CircleRole.Leader; // главой становится старейший участник } EventBus.Publish(new CircleLeftEvent { CircleId = circle.id, CircleName = circle.name, Disbanded = disbanded }); } public static bool IsValidName(string name, out string error) { name = name?.Trim() ?? ""; if (name.Length < MinNameLength) { error = $"Название -- не короче {MinNameLength} символов"; return false; } if (name.Length > MaxNameLength) { error = $"Название -- не длиннее {MaxNameLength} символов"; return false; } if (name.IndexOfAny(new[] { '\n', '\r', '|', '<', '>' }) >= 0) { error = "В названии есть недопустимые символы"; return false; } error = null; return true; } static CircleMember FindMember(CircleInfo circle, string playerId) { return circle.members.Find(m => m.playerId == playerId); } // ---------------- Сохранение (секция "circle") ---------------- // Модуль без Unity, поэтому формат -- простые строки, а не JSON: // joined= // created=|<название>|<роль игрока: Leader/Member> -- Круги, созданные игроком // Демо-Круги не сохраняются: они одинаковые при каждом запуске. public string SaveKey => "circle"; public string Capture() { var sb = new StringBuilder(); if (current != null) sb.Append("joined=").Append(current.id).Append('\n'); foreach (var c in circles) { if (!c.id.StartsWith("player_")) continue; var me = FindMember(c, LocalPlayer.Id); if (me == null) continue; // свой Круг, из которого игрок вышел, уже распущен sb.Append("created=").Append(c.id).Append('|').Append(c.name).Append('|').Append(me.role).Append('\n'); } return sb.ToString(); } public void Restore(string data) { ResetState(); if (string.IsNullOrEmpty(data)) return; string joinedId = null; foreach (var raw in data.Split('\n')) { string line = raw.Trim(); if (line.StartsWith("joined=")) joinedId = line.Substring("joined=".Length); else if (line.StartsWith("created=")) { string[] parts = line.Substring("created=".Length).Split('|'); if (parts.Length < 2) continue; var circle = new CircleInfo { id = parts[0], name = parts[1] }; CircleRole role = parts.Length > 2 && parts[2] == nameof(CircleRole.Member) ? CircleRole.Member : CircleRole.Leader; circle.members.Add(new CircleMember { playerId = LocalPlayer.Id, name = LocalPlayer.Name, role = role }); circles.Insert(0, circle); } } if (joinedId == null) return; CircleInfo joined = circles.Find(c => c.id == joinedId); if (joined == null) return; // Круга больше нет if (FindMember(joined, LocalPlayer.Id) == null) joined.members.Add(new CircleMember { playerId = LocalPlayer.Id, name = LocalPlayer.Name, role = CircleRole.Member }); current = joined; } // Новая игра: игрок ни в каком Круге, свои Круги распущены public void ResetState() { current = null; circles.Clear(); CreateDemoCircles(); } // ---------------- Демо-Круги (до появления сервера) ---------------- static readonly string[] DemoCircleNames = { "Круг Перуна", "Стальные Волки", "Сыны Руси", "Богатыри", "Северный Страж", "Вольные", "Златые Земли", "Серые Горны", "Речные", "Перунов Круг" }; static readonly string[] DemoPlayerNames = { "Велес", "Север", "Тайрон", "DreadWolf", "Мороз", "Леший", "Князь", "Варяг", "Сокол", "Ратибор", "Светозар", "Добромир", "Велимир", "Ярополк", "Всеслав", "Мстислав", "Остромир", "Богдан", "Любомир", "Твердислав", "Радим", "Гостомысл", "Вышеслав", "Путята", "Ставр", "Чурила", "Микула", "Василиса", "Забава", "IronBear" }; // Сколько участников в каждом демо-Круге (последний -- полный, чтобы было видно "мест нет") static readonly int[] DemoMemberCounts = { 28, 24, 22, 19, 17, 14, 12, 9, 6, MaxMembersPerCircle }; void CreateDemoCircles() { for (int i = 0; i < DemoCircleNames.Length; i++) { var circle = new CircleInfo { id = "circle_" + (i + 1), name = DemoCircleNames[i] }; for (int k = 0; k < DemoMemberCounts[i]; k++) { string name = DemoPlayerNames[(i + k) % DemoPlayerNames.Length]; circle.members.Add(new CircleMember { playerId = $"npc_{i + 1}_{k + 1}", name = k < DemoPlayerNames.Length ? name : name + " " + (k + 1), role = k == 0 ? CircleRole.Leader : CircleRole.Member }); } circles.Add(circle); } } }