using System; using System.Collections.Generic; namespace Bogatyri.World { public enum MarchState { Outbound, // идут к цели Returning, // бой прошёл, возвращаются домой Finished // вернулись, солдаты и добыча зачислены } // Один поход. Всё время -- абсолютное (Unix-время в миллисекундах), а не "осталось N секунд": // поход продолжается, пока игра закрыта, и одинаково понимается клиентом и сервером. [Serializable] public class MarchData { public int id; public string ownerId; public string fromNodeId; public string targetNodeId; public int soldiers; // сколько ушло public int survivors; // сколько возвращается (известно после боя) public bool victory; public bool targetEmpty; // лагерь уже был разбит, когда войско пришло: боя нет, все возвращаются public int defenders; // гарнизон цели в момент боя public int defenderLosses; // сколько потерял защитник public int[] units; // состав похода по типам (TroopType); пусто -- старый поход, все считаются пехотой public int[] survivorUnits; // выжившие по типам (известно после боя) public List loot = new List(); public long departMs; public long arriveMs; public long returnMs; // известно после боя public MarchState state; } public static class MarchRules { // Скорость войска: 400 милли-единиц (0,4 клетки) в секунду. // Ближний сосед (~4 клетки) -- около 10 сек, замок (~10 клеток) -- около 25 сек, край карты -- до минуты. public const int SpeedPerSecond = 400; public static long GetTravelTimeMs(MapNode from, MapNode to) { return (long)WorldMapLayout.Distance(from, to) * 1000 / SpeedPerSecond; } } public static class CampRules { // Разбитый лагерь появляется снова через 30 минут (требует балансировки) public const long RespawnMs = 30L * 60 * 1000; } // Разбитый лагерь: до какого момента его нет на карте. Хранится в сохранении мира. [Serializable] public class CampState { public string id; public long respawnMs; } public enum WorldEventType { BattleResolved, // войско дошло, бой прошёл MarchReturned // войско вернулось домой } public struct WorldEvent { public WorldEventType Type; public MarchData March; } // Состояние мира: карта + походы. Не зависит от Unity -- в мультиплеере этот же код крутится на сервере, // а клиент только показывает то, что он вернул. public sealed class WorldSimulation { private readonly List nodes; private readonly IBattleSystem battleSystem; private readonly List marches = new List(); private readonly Dictionary campRespawn = new Dictionary(); // id лагеря -> когда появится private readonly List arrivals = new List(); private int nextMarchId = 1; public IReadOnlyList Nodes => nodes; public IReadOnlyList Marches => marches; // Бой считает модуль Боевой системы -- Мир получает его через контракт, а не знает формулу сам public WorldSimulation(List nodes, IBattleSystem battleSystem) { this.nodes = nodes; this.battleSystem = battleSystem; } public MapNode GetNode(string id) { return WorldMapLayout.Find(nodes, id); } // Восстановление походов из сохранения (или из ответа сервера) public void LoadMarches(List saved) { marches.Clear(); if (saved == null) return; foreach (var m in saved) { // Поход к точке, которой больше нет на карте (карта поменялась), восстановить нельзя if (GetNode(m.fromNodeId) == null || GetNode(m.targetNodeId) == null) continue; marches.Add(m); if (m.id >= nextMarchId) nextMarchId = m.id + 1; } } // ---------------- Лагеря ---------------- public bool IsCampCleared(string campId, long nowMs) { return campRespawn.TryGetValue(campId, out long at) && nowMs < at; } // Сколько миллисекунд до появления лагеря (0 -- лагерь на месте) public long CampRespawnInMs(string campId, long nowMs) { return campRespawn.TryGetValue(campId, out long at) && nowMs < at ? at - nowMs : 0; } public List GetCampStates() { var list = new List(); foreach (var c in campRespawn) list.Add(new CampState { id = c.Key, respawnMs = c.Value }); return list; } public void LoadCampStates(List saved) { campRespawn.Clear(); if (saved == null) return; foreach (var c in saved) if (c != null && GetNode(c.id) != null) campRespawn[c.id] = c.respawnMs; } // ---------------- Походы ---------------- public MarchData StartMarch(string ownerId, string fromId, string targetId, int soldiers, long nowMs, out string error) { MapNode from = GetNode(fromId); MapNode target = GetNode(targetId); if (from == null || target == null) { error = "Неизвестная точка на карте"; return null; } if (target == from || !target.canBeAttacked) { error = $"На {target.displayName} нападать нельзя"; return null; } if (target.type == MapNodeType.Camp && IsCampCleared(targetId, nowMs)) { error = $"{target.displayName} уже разбит. Появится снова через {(CampRespawnInMs(targetId, nowMs) + 59_999) / 60_000} мин"; return null; } if (soldiers <= 0) { error = "Нужно отправить хотя бы одного солдата"; return null; } var march = new MarchData { id = nextMarchId++, ownerId = ownerId, fromNodeId = fromId, targetNodeId = targetId, soldiers = soldiers, survivors = soldiers, departMs = nowMs, arriveMs = nowMs + MarchRules.GetTravelTimeMs(from, target), state = MarchState.Outbound }; marches.Add(march); error = null; return march; } // Продвинуть мир до момента nowMs. Если игра была закрыта долго, за один вызов поход // может и дойти, и вернуться -- события придут в правильном порядке. public void Advance(long nowMs, List events) { // Бои -- в порядке прихода войск: кто первым дошёл до лагеря, тот его и разбил arrivals.Clear(); foreach (var m in marches) if (m.state == MarchState.Outbound && nowMs >= m.arriveMs) arrivals.Add(m); arrivals.Sort((a, b) => a.arriveMs != b.arriveMs ? a.arriveMs.CompareTo(b.arriveMs) : a.id.CompareTo(b.id)); foreach (var m in arrivals) { ResolveBattle(m); events.Add(new WorldEvent { Type = WorldEventType.BattleResolved, March = m }); } foreach (var m in marches) { if (m.state == MarchState.Returning && nowMs >= m.returnMs) { m.state = MarchState.Finished; events.Add(new WorldEvent { Type = WorldEventType.MarchReturned, March = m }); } } marches.RemoveAll(m => m.state == MarchState.Finished); // Лагеря, которые уже появились снова, больше не нужно помнить expiredCamps.Clear(); foreach (var c in campRespawn) if (nowMs >= c.Value) expiredCamps.Add(c.Key); foreach (var id in expiredCamps) campRespawn.Remove(id); } private readonly List expiredCamps = new List(); void ResolveBattle(MarchData m) { MapNode from = GetNode(m.fromNodeId); MapNode target = GetNode(m.targetNodeId); // Обратный путь отсчитывается от момента боя, а не от "сейчас" -- важно для офлайна m.returnMs = m.arriveMs + MarchRules.GetTravelTimeMs(target, from); m.state = MarchState.Returning; m.loot = new List(); // Лагерь уже разбит (другим войском): боя нет, все идут домой if (target.type == MapNodeType.Camp && IsCampCleared(target.id, m.arriveMs)) { m.targetEmpty = true; m.victory = false; m.survivors = m.soldiers; m.survivorUnits = HasUnits(m) ? (int[])m.units.Clone() : null; m.defenders = 0; m.defenderLosses = 0; return; } BattleOutcome outcome = battleSystem.Resolve(new BattleRequest { AttackerUnits = HasUnits(m) ? m.units : null, Attackers = m.soldiers, DefenderUnits = WorldMapLayout.GarrisonUnits(target), Defenders = target.garrison, DefenderFortified = WorldMapLayout.IsFortified(target) // замок и города -- за стенами, лагерь -- в поле }); m.victory = outcome.Victory; m.survivors = outcome.Survivors; m.survivorUnits = HasUnits(m) ? outcome.AttackerSurvivorUnits : null; m.defenders = target.garrison; m.defenderLosses = outcome.DefenderLosses; // Гарнизон соседа пока не меняется: соседи -- заглушки. Когда они станут реальными игроками, // их потери будет списывать сервер из армии того игрока. if (outcome.Victory) { foreach (var r in target.rewards) m.loot.Add(new RewardEntry { resource = r.resource, amount = r.amount }); // Разбитый лагерь пропадает с карты и появляется снова -- отсчёт от момента боя if (target.type == MapNodeType.Camp) campRespawn[target.id] = m.arriveMs + CampRules.RespawnMs; } } // Состав похода известен и сходится с числом воинов (у старых походов его нет) static bool HasUnits(MarchData m) { if (m.units == null || m.units.Length != TroopType.Count) return false; int n = 0; foreach (var u in m.units) n += u; return n == m.soldiers && n > 0; } // Где сейчас войско (для отрисовки). Возвращает долю пути 0..1 и точки начала/конца отрезка. public void GetMarchSegment(MarchData m, long nowMs, out MapNode segmentFrom, out MapNode segmentTo, out double progress) { MapNode home = GetNode(m.fromNodeId); MapNode target = GetNode(m.targetNodeId); long start, end; if (m.state == MarchState.Outbound) { segmentFrom = home; segmentTo = target; start = m.departMs; end = m.arriveMs; } else { segmentFrom = target; segmentTo = home; start = m.arriveMs; end = m.returnMs; } progress = end > start ? Math.Min(1.0, Math.Max(0.0, (nowMs - start) / (double)(end - start))) : 1.0; } } }