using System; using System.Collections.Generic; namespace Bogatyri.World { public enum MapNodeType { PlayerCity, // город игрока NeighborCity, // соседний город -- другой игрок BossCastle // Большой замок с боссом в центре карты } [Serializable] public class RewardEntry { public string resource; // имя из ResourceType (Gold, Food, ...) -- логика карты про экономику не знает public int amount; } // Точка на карте мира. Координаты -- целые "милли-единицы" (1000 = 1 клетка Unity), // чтобы расчёты были одинаковыми на клиенте и на сервере. [Serializable] public class MapNode { public string id; public string ownerId; // id игрока-владельца (у замка -- пусто) public string displayName; public MapNodeType type; public int x; public int y; public bool canBeAttacked; public int garrison; // солдат в обороне public List rewards = new List(); } // Раскладка карты: замок с боссом в центре, города игроков разбросаны вокруг хаотично. // Раскладка строится из seed, поэтому одинакова при каждом запуске и совпадёт с серверной. // В мультиплеере карту и реальных соседей будет присылать сервер. public static class WorldMapLayout { public const string PlayerCityId = "player_city"; public const string BossCastleId = "boss_castle"; public const int DefaultSeed = 20260925; public const int NeighborCount = 20; // Границы карты (от центра) public const int HalfWidth = 20000; public const int HalfHeight = 14000; const int EdgeMargin = 1500; // города не липнут к краю карты const int CastleClearRadius = 4500; // вокруг замка свободная зона const int MinCitySpacing = 3600; // минимальное расстояние между городами const int MaxPlacementAttempts = 1000; // Пока соседи -- заглушки с именами. Потом это будут ники реальных игроков. static readonly string[] NeighborNames = { "Ратибор", "Светозар", "Добромир", "Велимир", "Ярополк", "Всеслав", "Мстислав", "Остромир", "Богдан", "Любомир", "Твердислав", "Радим", "Гостомысл", "Вышеслав", "Путята", "Ставр", "Чурила", "Микула", "Василиса", "Забава" }; public static List CreateDefault() { return Generate(DefaultSeed, NeighborCount); } public static List Generate(int seed, int neighborCount) { var rng = new DeterministicRandom(seed); var nodes = new List(); var castle = new MapNode { id = BossCastleId, displayName = "Замок Змея Горыныча", type = MapNodeType.BossCastle, x = 0, y = 0, canBeAttacked = true, garrison = 20 // требует балансировки }; // Небольшие числа -- по концепции экономики castle.rewards.Add(new RewardEntry { resource = "Gold", amount = 20 }); castle.rewards.Add(new RewardEntry { resource = "Food", amount = 40 }); castle.rewards.Add(new RewardEntry { resource = "Iron", amount = 10 }); nodes.Add(castle); if (TryFindFreeSpot(rng, nodes, out int px, out int py)) { nodes.Add(new MapNode { id = PlayerCityId, ownerId = "local_player", displayName = "Твой город", type = MapNodeType.PlayerCity, x = px, y = py }); } for (int i = 0; i < neighborCount; i++) { if (!TryFindFreeSpot(rng, nodes, out int x, out int y)) break; // карта переполнена int garrison = rng.Range(8, 41); var city = new MapNode { id = $"city_{i + 1}", ownerId = $"npc_{i + 1}", displayName = NeighborNames[i % NeighborNames.Length], type = MapNodeType.NeighborCity, x = x, y = y, canBeAttacked = true, garrison = garrison }; // Добыча растёт вместе с гарнизоном: сильный сосед -- богаче, но опаснее city.rewards.Add(new RewardEntry { resource = "Gold", amount = 5 + garrison / 3 }); city.rewards.Add(new RewardEntry { resource = "Food", amount = 10 + garrison }); city.rewards.Add(new RewardEntry { resource = "Wood", amount = 10 + garrison / 2 }); if (garrison >= 20) city.rewards.Add(new RewardEntry { resource = "Iron", amount = garrison / 4 }); nodes.Add(city); } return nodes; } // Случайная точка, не слишком близко к замку и к другим городам static bool TryFindFreeSpot(DeterministicRandom rng, List nodes, out int x, out int y) { for (int attempt = 0; attempt < MaxPlacementAttempts; attempt++) { x = rng.Range(-HalfWidth + EdgeMargin, HalfWidth - EdgeMargin + 1); y = rng.Range(-HalfHeight + EdgeMargin, HalfHeight - EdgeMargin + 1); bool free = true; foreach (var n in nodes) { int minDistance = n.type == MapNodeType.BossCastle ? CastleClearRadius : MinCitySpacing; if (DistanceSquared(n.x, n.y, x, y) < (long)minDistance * minDistance) { free = false; break; } } if (free) return true; } x = 0; y = 0; return false; } public static MapNode Find(List nodes, string id) { foreach (var n in nodes) { if (n.id == id) return n; } return null; } static long DistanceSquared(int x1, int y1, int x2, int y2) { long dx = x2 - x1; long dy = y2 - y1; return dx * dx + dy * dy; } // Целочисленный квадратный корень (метод Ньютона) -- одинаков на любой платформе public static int Distance(MapNode a, MapNode b) { long value = DistanceSquared(a.x, a.y, b.x, b.y); if (value <= 0) return 0; long r = value; long next = (r + 1) / 2; while (next < r) { r = next; next = (r + value / r) / 2; } return (int)r; } } // Детерминированный генератор случайных чисел (xorshift32). // System.Random не годится: его реализация может отличаться между платформами и версиями .NET, // а карта у клиента и сервера должна получиться одинаковой. public sealed class DeterministicRandom { private uint state; public DeterministicRandom(int seed) { state = (uint)seed; if (state == 0) state = 0x9E3779B9; // xorshift не работает с нулевым состоянием } public uint NextUInt() { uint x = state; x ^= x << 13; x ^= x >> 17; x ^= x << 5; state = x; return x; } // Число в диапазоне [minInclusive, maxExclusive) public int Range(int minInclusive, int maxExclusive) { uint span = (uint)(maxExclusive - minInclusive); return minInclusive + (int)(NextUInt() % span); } } }