using System; using System.Collections.Generic; namespace Bogatyri.World { // Ресурсная точка на карте (регламент 31.10, раздел 7; решение №46: состояние точки ведёт World). // Состояние меняется со временем, поэтому хранится в сохранении, а не строится из seed, как замок и города. [Serializable] public class ResourcePoint { public string id; public int kind; // ResourcePointRules.Resources: 0 дерево, 1 камень, 2 металл, 3 золото public int size; // 0 малая, 1 средняя, 2 крупная public int x; public int y; public int stock; // сколько осталось public int stockMax; public long spawnMs; // появилась (реальное время часов GameClock) public long expireMs; // исчезнет public int occupiedBy; // номер похода, который сейчас собирает (0 -- свободна) public bool active; // false -- истощена или исчезла (остаётся, пока к ней идёт или с неё возвращается войско) } // Числа ресурсных точек (регламент 31.10, раздел 7, ТЕСТ). В игре -- из таблицы WorldResourceConfig. public sealed class ResourcePointRules { // Имена ресурсов -- как в ResourceType (Мир про экономику не знает, зачисляет её модуль по имени) public static readonly string[] Resources = { "Wood", "Stone", "Iron", "Gold" }; public static readonly string[] KindNames = { "Лес", "Каменоломня", "Рудник", "Золотая жила" }; public static readonly string[] SizeNames = { "малая", "средняя", "крупная" }; // Запас: [ресурс][размер] -- дерево 200/600/1500, камень 150/450/1000, металл 80/250/600, золото 30/100/250 public int[][] Stock = { new[] { 200, 600, 1500 }, new[] { 150, 450, 1000 }, new[] { 80, 250, 600 }, new[] { 30, 100, 250 } }; // Добыча одного воина за ИГРОВОЙ час, в тысячных долях: 2 / 1,5 / 1 / 0,5 public int[] MiningMilliPerGameHour = { 2000, 1500, 1000, 500 }; // Груз одного воина: 12 / 10 / 8 / 5. Осадные машины груза не несут (регламент) public int[] CargoPerSoldier = { 12, 10, 8, 5 }; // Срок жизни точки в РЕАЛЬНЫХ часах с момента появления: малая 12, средняя 24, крупная 48 public int[] LifetimeRealHours = { 12, 24, 48 }; // 1 игровой час = 5 реальных минут (регламент 31.10, раздел 2) public long GameHourMs = 5 * 60 * 1000; // Не задано регламентом -- ТЕСТ, решает команда: public int PointsPerResource = 2; // сколько точек каждого ресурса одновременно на карте public int Spacing = 2200; // не ближе 2,2 клетки к другим точкам карты public static ResourcePointRules Default => new ResourcePointRules(); // Сбор с начала startMs: сколько войско унесёт и когда закончит. // Конец -- груз полон, точка истощена или исчезла (expireMs), что раньше. public void Gather(int kind, int[] units, int stock, long startMs, long expireMs, out int amount, out long endMs) { int miners = 0, carriers = 0; for (int i = 0; units != null && i < units.Length; i++) { if (units[i] <= 0) continue; miners += units[i]; if (i != TroopType.Siege) carriers += units[i]; } long target = Math.Min(Math.Max(0, stock), (long)carriers * CargoPerSoldier[kind]); long rate = (long)miners * MiningMilliPerGameHour[kind]; // тысячных долей за игровой час if (target <= 0 || rate <= 0 || expireMs <= startMs) { amount = 0; endMs = startMs; return; } long full = (target * 1000 * GameHourMs + rate - 1) / rate; // время до полного груза (вверх) if (startMs + full <= expireMs) { amount = (int)target; endMs = startMs + full; } else { endMs = expireMs; // точка исчезла раньше -- уносят, что успели amount = (int)Math.Min(target, rate * (expireMs - startMs) / (1000 * GameHourMs)); } } } // Состояние ресурсных точек для сохранения (секция "world") [Serializable] public class ResourcePointsState { public List points = new List(); public int nextPointId = 1; public int generation; // сколько точек уже появилось -- для детерминированного места следующей public bool initialized; // первые точки уже выставлены } public sealed partial class WorldSimulation { private readonly ResourcePointRules pointRules; private readonly int pointSeed; private readonly List points = new List(); private int nextPointId = 1; private int pointGeneration; private bool pointsInitialized; public ResourcePointRules PointRules => pointRules; public IReadOnlyList ResourcePoints => points; public ResourcePoint GetPoint(string id) { return points.Find(p => p.id == id); } // Первые точки: по PointsPerResource каждого ресурса, появляются в момент первого расчёта мира void EnsureResourcePoints(long nowMs) { if (pointsInitialized) return; pointsInitialized = true; for (int kind = 0; kind < ResourcePointRules.Resources.Length; kind++) for (int k = 0; k < pointRules.PointsPerResource; k++) SpawnPoint(kind, nowMs); } // Новая точка того же ресурса в другом месте. Место и размер -- из seed и номера появления (одинаково на сервере) ResourcePoint SpawnPoint(int kind, long nowMs) { pointGeneration++; var rng = new DeterministicRandom(pointSeed ^ 0x2F0E1D ^ pointGeneration * 7919); if (!WorldMapLayout.TryFindFreeSpot(rng, nodes, out int x, out int y, pointRules.Spacing)) return null; int size = rng.Range(0, 3); // размер равновероятно -- регламент не задаёт (ТЕСТ) var p = new ResourcePoint { id = "res_" + nextPointId++, kind = kind, size = size, x = x, y = y, stock = pointRules.Stock[kind][size], stockMax = pointRules.Stock[kind][size], spawnMs = nowMs, expireMs = nowMs + pointRules.LifetimeRealHours[size] * 3_600_000L, active = true }; points.Add(p); nodes.Add(ToNode(p)); return p; } static MapNode ToNode(ResourcePoint p) { return new MapNode { id = p.id, displayName = $"{ResourcePointRules.KindNames[p.kind]} ({ResourcePointRules.SizeNames[p.size]})", type = MapNodeType.ResourcePoint, x = p.x, y = p.y, canBeAttacked = true, // «можно отправить войско» level = p.size + 1 }; } // Точка истощена или исчезла: на её месте больше нечего собирать, новая появляется в другом месте void RetirePoint(ResourcePoint p, long atMs) { if (!p.active) return; p.active = false; p.occupiedBy = 0; SpawnPoint(p.kind, atMs); } // Исчезнувшие точки убираются с карты, когда к ним уже никто не идёт и с них никто не возвращается void PurgeRetiredPoints() { points.RemoveAll(p => { if (p.active || marches.Exists(m => m.targetNodeId == p.id)) return false; nodes.RemoveAll(n => n.id == p.id); return true; }); } public bool CanGatherAt(string pointId, long nowMs, out string error) { ResourcePoint p = GetPoint(pointId); if (p == null || !p.active || nowMs >= p.expireMs) { error = "Этой ресурсной точки больше нет"; return false; } if (p.occupiedBy != 0) { error = "Точку уже занимает войско"; return false; } if (marches.Exists(m => m.targetNodeId == pointId && m.state == MarchState.Outbound)) { error = "К этой точке уже идёт войско"; return false; } error = null; return true; } // Прогноз сбора для окна отправки: сколько унесут и сколько продлится сбор, если дойдут в arriveMs public void PreviewGather(string pointId, int[] units, long arriveMs, out int amount, out long gatherMs) { ResourcePoint p = GetPoint(pointId); if (p == null) { amount = 0; gatherMs = 0; return; } pointRules.Gather(p.kind, units, p.stock, arriveMs, p.expireMs, out amount, out long endMs); gatherMs = endMs - arriveMs; } // Войско дошло до точки: начинает сбор. Точки нет или она занята -- возвращается без добычи void StartGather(MarchData m) { MapNode from = GetNode(m.fromNodeId); MapNode target = GetNode(m.targetNodeId); ResourcePoint p = GetPoint(m.targetNodeId); int[] units = HasUnits(m) ? m.units : new[] { m.soldiers, 0, 0, 0 }; m.victory = false; m.survivors = m.soldiers; // при сборе боя нет -- все живы m.survivorUnits = HasUnits(m) ? (int[])m.units.Clone() : null; m.loot = new List(); if (p == null || !p.active || p.occupiedBy != 0 || m.arriveMs >= p.expireMs) { m.targetEmpty = true; m.gatherEndMs = m.arriveMs; m.returnMs = m.arriveMs + TravelTimeMs(target, from, units); m.state = MarchState.Returning; return; } pointRules.Gather(p.kind, units, p.stock, m.arriveMs, p.expireMs, out int amount, out long endMs); m.cargo = amount; m.gatherEndMs = endMs; m.state = MarchState.Gathering; p.occupiedBy = m.id; } // Сбор закончен: груз -- в добычу похода (зачислит Экономика по возвращении), войско идёт домой void FinishGather(MarchData m) { MapNode from = GetNode(m.fromNodeId); MapNode target = GetNode(m.targetNodeId); ResourcePoint p = GetPoint(m.targetNodeId); int[] units = HasUnits(m) ? m.units : null; if (p != null) { p.stock = Math.Max(0, p.stock - m.cargo); if (p.occupiedBy == m.id) p.occupiedBy = 0; if (m.cargo > 0) m.loot.Add(new RewardEntry { resource = ResourcePointRules.Resources[p.kind], amount = m.cargo }); if (p.stock <= 0 || m.gatherEndMs >= p.expireMs) RetirePoint(p, m.gatherEndMs); } m.returnMs = m.gatherEndMs + TravelTimeMs(target, from, units); m.state = MarchState.Returning; } // ---------------- Сохранение ---------------- public ResourcePointsState GetPointsState() { var state = new ResourcePointsState { nextPointId = nextPointId, generation = pointGeneration, initialized = pointsInitialized }; foreach (var p in points) state.points.Add(p); return state; } // Загружать ДО походов: поход к точке восстанавливается, только если точка есть на карте public void LoadPointsState(ResourcePointsState state) { nodes.RemoveAll(n => n.type == MapNodeType.ResourcePoint); points.Clear(); nextPointId = state?.nextPointId ?? 1; pointGeneration = state?.generation ?? 0; pointsInitialized = state?.initialized ?? false; if (state?.points == null) return; foreach (var p in state.points) { if (p == null || p.kind < 0 || p.kind >= ResourcePointRules.Resources.Length || p.size < 0 || p.size > 2) continue; points.Add(p); nodes.Add(ToNode(p)); } } } }