using System.Collections.Generic; using Bogatyri.Settlement.Placement; using TMPro; using UnityEngine; // 3D-ПОСЕЛЕНИЕ, ЭТАП I -- ПРОТОТИП НА ПРИМИТИВАХ (ТЗ «Система строительства поселения» от 04.11.2026). // Сцена Assets/Scenes/City3D.unity (создаётся: Tools -> Bogatyri -> 3D-город: создать сцену City3D). // // Что умеет: // - местность с холмами, рекой, скалами, деревьями, валунами и травой (City3DLayout); // - невидимая сетка: видна только в режиме стройки / редактирования вместе с границей открытой зоны; // - новое здание: выбрать тип -> призрак-куб тянется пальцем, привязан к клеткам, зелёный / красный с причиной, // поворот 0/90/180/270, «Поставить» / «Отмена»; // - перенос: кнопка в меню здания или общий режим редактирования; то же здание (тот же id), подтверждение / отмена; // - расчистка дерева / валуна (без ожидания и цены); трава прячется под зданием и возвращается; // - зоны по уровню Ратуши (кнопки «Ратуша -/+» -- ТОЛЬКО ДЛЯ ТЕСТА), камера плавно показывает новую зону. // Правила места -- в модуле Поселения (Bogatyri.Settlement.Placement, тесты PlacementTests), здесь только показ и ввод. // // Пока НЕ подключено (этапы II-III): цена и таймер стройки, Экономика и награда за расчистку, сохранение, // дороги, настоящие модели. Кнопки -- временные (IMGUI); окна согласуются с Володей. public class City3DController : MonoBehaviour { // Тип здания для прототипа: максимальный габарит (клеток) с первого уровня, высота куба, цвет struct BuildingType { public string Name; public int W, H; public float Height; public Color Color; } static readonly BuildingType[] Types = { new BuildingType { Name = "Ратуша", W = 4, H = 4, Height = 3.0f, Color = new Color(0.85f, 0.75f, 0.35f) }, new BuildingType { Name = "Дом", W = 2, H = 2, Height = 1.4f, Color = new Color(0.85f, 0.55f, 0.35f) }, new BuildingType { Name = "Ферма", W = 3, H = 3, Height = 1.0f, Color = new Color(0.9f, 0.85f, 0.4f) }, new BuildingType { Name = "Лесопилка", W = 3, H = 2, Height = 1.5f, Color = new Color(0.55f, 0.4f, 0.25f) }, new BuildingType { Name = "Каменоломня", W = 3, H = 2, Height = 1.3f, Color = new Color(0.6f, 0.6f, 0.62f) }, new BuildingType { Name = "Шахта", W = 2, H = 2, Height = 1.5f, Color = new Color(0.4f, 0.45f, 0.55f) }, new BuildingType { Name = "Склад", W = 2, H = 3, Height = 1.8f, Color = new Color(0.65f, 0.5f, 0.3f) }, new BuildingType { Name = "Амбар", W = 2, H = 3, Height = 1.8f, Color = new Color(0.8f, 0.65f, 0.3f) }, new BuildingType { Name = "Казармы", W = 3, H = 3, Height = 2.0f, Color = new Color(0.75f, 0.3f, 0.3f) }, new BuildingType { Name = "Госпиталь", W = 3, H = 3, Height = 1.8f, Color = new Color(0.85f, 0.95f, 1f) }, }; enum Mode { View, Edit, Place, Move } City3DLayout layout; SettlementMap map; City3DCamera cameraRig; Camera cam; Material baseMaterial; Texture2D groundTexture; const int PixelsPerCell = 8; readonly Dictionary buildingViews = new Dictionary(); readonly Dictionary buildingType = new Dictionary(); readonly Dictionary obstacleViews = new Dictionary(); readonly List grassViews = new List(); int nextBuildingId = 1; int townHallLevel = 1; Mode mode = Mode.View; int placingType = -1; // Place: какой тип ставим string movingId; // Move: какое здание переносим int ghostX, ghostY, ghostRot; PlacedRect moveOriginal; // Move: последнее подтверждённое положение GameObject ghost, footprint; Material okMaterial, badMaterial; PlaceError ghostError; int ghostFoundation; bool draggingGhost; Vector3 pointerDown; bool pointerMoved, pointerOverGui; string selectedBuilding, selectedObstacle; // меню здания / препятствия (тап в обычном режиме) string message = ""; float messageUntil; // ---------------- Построение сцены ---------------- void Start() { cam = Camera.main; cameraRig = cam.GetComponent() ?? cam.gameObject.AddComponent(); cameraRig.bounds = new Rect(2, 2, City3DLayout.Size - 4, City3DLayout.Size - 4); cameraRig.target = new Vector3(City3DLayout.Size / 2f, 0, City3DLayout.Size / 2f); cameraRig.Apply(); // Материал URP по умолчанию берём у примитива: шейдер точно есть в сборке var probe = GameObject.CreatePrimitive(PrimitiveType.Cube); baseMaterial = new Material(probe.GetComponent().sharedMaterial); Destroy(probe); layout = City3DLayout.Create(); map = layout.Map; BuildGround(); BuildObstacles(); BuildGrass(); // Ратуша стоит в центре с самого начала int c = City3DLayout.Size / 2; PlaceNew(0, c - 2, c - 2, 0); RefreshGround(); } Material Mat(Color color) { var m = new Material(baseMaterial) { color = color }; return m; } void BuildGround() { int n = City3DLayout.Size; var verts = new Vector3[(n + 1) * (n + 1)]; var uvs = new Vector2[verts.Length]; for (int z = 0; z <= n; z++) for (int x = 0; x <= n; x++) { int i = z * (n + 1) + x; verts[i] = new Vector3(x, City3DLayout.HeightAt(x, z), z); uvs[i] = new Vector2(x / (float)n, z / (float)n); } var tris = new int[n * n * 6]; int t = 0; for (int z = 0; z < n; z++) for (int x = 0; x < n; x++) { int i = z * (n + 1) + x; tris[t++] = i; tris[t++] = i + n + 1; tris[t++] = i + 1; tris[t++] = i + 1; tris[t++] = i + n + 1; tris[t++] = i + n + 2; } var mesh = new Mesh { name = "City3D_Ground", indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 }; mesh.vertices = verts; mesh.uv = uvs; mesh.triangles = tris; mesh.RecalculateNormals(); var go = new GameObject("Ground"); go.transform.SetParent(transform, false); go.AddComponent().sharedMesh = mesh; groundTexture = new Texture2D(n * PixelsPerCell, n * PixelsPerCell, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; var mat = Mat(Color.white); mat.mainTexture = groundTexture; go.AddComponent().sharedMaterial = mat; go.AddComponent().sharedMesh = mesh; // Вода -- плоскость над руслом var water = GameObject.CreatePrimitive(PrimitiveType.Cube); water.name = "River"; Destroy(water.GetComponent()); water.transform.SetParent(transform, false); water.transform.position = new Vector3(37f, -0.25f, n / 2f); water.transform.localScale = new Vector3(9f, 0.05f, n); water.GetComponent().sharedMaterial = Mat(new Color(0.25f, 0.5f, 0.85f)); } void BuildObstacles() { var trunk = Mat(new Color(0.45f, 0.3f, 0.18f)); var leaves = Mat(new Color(0.18f, 0.48f, 0.2f)); var stone = Mat(new Color(0.55f, 0.55f, 0.58f)); foreach (var o in layout.Obstacles) { var root = new GameObject(o.Id); root.transform.SetParent(transform, false); Vector2 center = Vector2.zero; foreach (var c in o.Cells) center += new Vector2(c.x + 0.5f, c.y + 0.5f); center /= o.Cells.Count; float h = City3DLayout.HeightAt(center.x, center.y); root.transform.position = new Vector3(center.x, h, center.y); if (o.IsTree) { Part(root, PrimitiveType.Cylinder, new Vector3(0, 0.5f, 0), new Vector3(0.25f, 0.5f, 0.25f), trunk); Part(root, PrimitiveType.Sphere, new Vector3(0, 1.4f, 0), new Vector3(1.1f, 1.3f, 1.1f), leaves); } else { float s = o.Cells.Count > 1 ? 1.8f : 0.9f; Part(root, PrimitiveType.Sphere, new Vector3(0, s * 0.3f, 0), new Vector3(s, s * 0.7f, s * 0.9f), stone); } obstacleViews[o.Id] = root; } } void BuildGrass() { var grass = Mat(new Color(0.35f, 0.65f, 0.3f)); foreach (var p in layout.Grass) { var g = GameObject.CreatePrimitive(PrimitiveType.Cube); Destroy(g.GetComponent()); g.name = "Grass"; g.transform.SetParent(transform, false); g.transform.position = new Vector3(p.x, City3DLayout.HeightAt(p.x, p.y) + 0.08f, p.y); g.transform.localScale = new Vector3(0.25f, 0.16f, 0.25f); g.transform.rotation = Quaternion.Euler(0, p.x * 37f, 0); g.GetComponent().sharedMaterial = grass; grassViews.Add(g); } } static GameObject Part(GameObject parent, PrimitiveType type, Vector3 localPos, Vector3 scale, Material mat) { var p = GameObject.CreatePrimitive(type); Object.Destroy(p.GetComponent()); p.transform.SetParent(parent.transform, false); p.transform.localPosition = localPos; p.transform.localScale = scale; p.GetComponent().sharedMaterial = mat; return p; } // Текстура земли: обычная -- трава, скалы, русло; в режиме стройки -- клетки, открытая зона и следующая void RefreshGround() { int n = City3DLayout.Size, px = PixelsPerCell; bool grid = mode != Mode.View; var pixels = new Color32[n * px * n * px]; for (int y = 0; y < n; y++) for (int x = 0; x < n; x++) { CellKind k = map.KindAt(x, y); Color c = k == CellKind.Water ? new Color(0.3f, 0.45f, 0.7f) : k == CellKind.Mountain ? new Color(0.5f, 0.48f, 0.45f) : Color.Lerp(new Color(0.35f, 0.58f, 0.3f), new Color(0.42f, 0.66f, 0.34f), Mathf.PerlinNoise(x * 0.3f, y * 0.3f)); if (grid && k == CellKind.Ground) { int zone = map.ZoneAt(x, y); if (zone > 0 && zone <= townHallLevel) c = Color.Lerp(c, new Color(0.6f, 0.95f, 0.6f), 0.25f); // открыто else if (zone == townHallLevel + 1) c = Color.Lerp(c, new Color(0.95f, 0.85f, 0.4f), 0.3f); // откроется следующим else c *= 0.55f; // закрыто } bool open = map.IsOpen(x, y, townHallLevel); for (int iy = 0; iy < px; iy++) for (int ix = 0; ix < px; ix++) { Color p = c; if (grid && (ix == 0 || iy == 0)) p *= 0.85f; // линии клеток // граница открытой зоны -- светлая линия if (grid && open && (ix == 0 && !map.IsOpen(x - 1, y, townHallLevel) || ix == px - 1 && !map.IsOpen(x + 1, y, townHallLevel) || iy == 0 && !map.IsOpen(x, y - 1, townHallLevel) || iy == px - 1 && !map.IsOpen(x, y + 1, townHallLevel))) p = new Color(1f, 1f, 1f); p.a = 1; pixels[(y * px + iy) * n * px + x * px + ix] = p; } } groundTexture.SetPixels32(pixels); groundTexture.Apply(false); } // ---------------- Здания ---------------- bool PlaceNew(int type, int x, int y, int rot) { var t = Types[type]; string id = "b_" + nextBuildingId; PlaceError e = map.Place(id, t.Name, x, y, t.W, t.H, rot, townHallLevel); if (e != PlaceError.None) { Say(SettlementMap.ErrorText(e)); return false; } nextBuildingId++; buildingType[id] = type; buildingViews[id] = CreateBuildingView(id, type); UpdateBuildingView(id); UpdateGrass(); return true; } GameObject CreateBuildingView(string id, int type) { var t = Types[type]; var root = new GameObject(id + "_" + t.Name); root.transform.SetParent(transform, false); var body = GameObject.CreatePrimitive(PrimitiveType.Cube); body.name = "Body"; body.transform.SetParent(root.transform, false); body.GetComponent().sharedMaterial = Mat(t.Color); var door = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.25f, 0.18f, 0.12f))); door.name = "Door"; // показывает, куда повёрнуто здание var foundation = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.6f, 0.57f, 0.5f))); foundation.name = "Foundation"; var labelGo = new GameObject("Label"); labelGo.transform.SetParent(root.transform, false); var label = labelGo.AddComponent(); label.text = t.Name; label.fontSize = 4; label.alignment = TextAlignmentOptions.Center; label.color = Color.white; label.outlineWidth = 0.2f; label.outlineColor = Color.black; label.rectTransform.sizeDelta = new Vector2(8, 2); return root; } // Положение, поворот, фундамент по данным карты (или призрака при переносе) void UpdateBuildingView(string id) { if (map.TryGetBuilding(id, out var r)) LayoutView(buildingViews[id], buildingType[id], r.X, r.Y, r.Rotation); } void LayoutView(GameObject root, int type, int x, int y, int rot) { var t = Types[type]; bool swap = rot == 90 || rot == 270; int w = swap ? t.H : t.W, h = swap ? t.W : t.H; int min = int.MaxValue, max = int.MinValue; for (int cy = y; cy < y + h; cy++) for (int cx = x; cx < x + w; cx++) { int v = map.HeightAt(cx, cy); min = Mathf.Min(min, v); max = Mathf.Max(max, v); } if (min == int.MaxValue) { min = 0; max = 0; } float top = max / 1000f, bottom = min / 1000f - 0.3f; root.transform.position = new Vector3(x + w / 2f, top, y + h / 2f); var body = root.transform.Find("Body"); body.localScale = new Vector3(w - 0.3f, t.Height, h - 0.3f); body.localPosition = new Vector3(0, t.Height / 2f, 0); var foundation = root.transform.Find("Foundation"); foundation.localScale = new Vector3(w - 0.1f, top - bottom, h - 0.1f); foundation.localPosition = new Vector3(0, (bottom - top) / 2f, 0); // дверь -- на «переднюю» сторону: 0 -- юг, 90 -- запад, 180 -- север, 270 -- восток var door = root.transform.Find("Door"); Vector3 dir = Quaternion.Euler(0, -rot, 0) * Vector3.back; float halfDepth = (rot == 0 || rot == 180 ? h : w) / 2f - 0.15f; door.localPosition = dir * halfDepth + Vector3.up * 0.4f; door.localScale = rot == 0 || rot == 180 ? new Vector3(0.6f, 0.8f, 0.1f) : new Vector3(0.1f, 0.8f, 0.6f); var label = root.transform.Find("Label"); label.localPosition = new Vector3(0, t.Height + 0.8f, 0); } void UpdateGrass() { for (int i = 0; i < grassViews.Count; i++) { Vector2 p = layout.Grass[i]; bool hidden = map.BuildingAt((int)p.x, (int)p.y) != null; // и под призраком при переносе: прежнее место -- трава вернётся только после подтверждения if (!hidden && mode == Mode.Move && movingId != null && map.TryGetBuilding(movingId, out var r)) hidden = (int)p.x >= r.X && (int)p.x < r.X + r.W && (int)p.y >= r.Y && (int)p.y < r.Y + r.H; grassViews[i].SetActive(!hidden); } } // ---------------- Призрак: новое здание или перенос ---------------- void StartPlacing(int type) { CancelGhost(); mode = Mode.Place; placingType = type; var t = Types[type]; ghost = CreateBuildingView("ghost", type); ghostRot = 0; Vector3 c = cameraRig.target; ghostX = Mathf.Clamp(Mathf.RoundToInt(c.x - t.W / 2f), 0, City3DLayout.Size - t.W); ghostY = Mathf.Clamp(Mathf.RoundToInt(c.z - t.H / 2f), 0, City3DLayout.Size - t.H); EnsureFootprint(); RefreshGround(); UpdateGhost(); } void StartMoving(string id) { if (!map.TryGetBuilding(id, out var r)) return; CancelGhost(); mode = Mode.Move; movingId = id; moveOriginal = r; ghostX = r.X; ghostY = r.Y; ghostRot = r.Rotation; ghost = buildingViews[id]; // переносится ТО ЖЕ здание -- новое не создаётся EnsureFootprint(); RefreshGround(); UpdateGhost(); } void EnsureFootprint() { if (footprint != null) return; footprint = GameObject.CreatePrimitive(PrimitiveType.Cube); footprint.name = "Footprint"; Destroy(footprint.GetComponent()); footprint.transform.SetParent(transform, false); } int GhostType => mode == Mode.Move ? buildingType[movingId] : placingType; void UpdateGhost() { if (ghost == null) return; var t = Types[GhostType]; ghostError = map.Validate(ghostX, ghostY, t.W, t.H, ghostRot, townHallLevel, mode == Mode.Move ? movingId : null, out ghostFoundation); LayoutView(ghost, GhostType, ghostX, ghostY, ghostRot); bool swap = ghostRot == 90 || ghostRot == 270; int w = swap ? t.H : t.W, h = swap ? t.W : t.H; float top = ghost.transform.position.y; footprint.transform.position = new Vector3(ghostX + w / 2f, top + 0.03f, ghostY + h / 2f); footprint.transform.localScale = new Vector3(w, 0.06f, h); if (okMaterial == null) { okMaterial = Mat(new Color(0.3f, 0.95f, 0.35f)); badMaterial = Mat(new Color(0.95f, 0.25f, 0.2f)); } footprint.GetComponent().sharedMaterial = ghostError == PlaceError.None ? okMaterial : badMaterial; UpdateGrass(); } void ConfirmGhost() { if (ghostError != PlaceError.None) { Say(SettlementMap.ErrorText(ghostError)); return; } if (mode == Mode.Place) { int type = placingType; int x = ghostX, y = ghostY, rot = ghostRot; CancelGhost(); if (PlaceNew(type, x, y, rot)) Say(Types[type].Name + " построено (прототип: без цены и таймера)"); } else if (mode == Mode.Move) { // Подтверждение -- одно изменение: позиция и поворот того же здания; ошибка -- всё как было PlaceError e = map.Move(movingId, ghostX, ghostY, ghostRot, townHallLevel); string id = movingId; if (e != PlaceError.None) { Say(SettlementMap.ErrorText(e)); return; } movingId = null; ghost = null; mode = Mode.Edit; UpdateBuildingView(id); Say("Перенесено. Здание то же: уровень, рабочие и таймеры не меняются"); FinishGhost(); } } // Отмена: новое -- не создаётся; перенос -- здание возвращается на последнее подтверждённое место void CancelGhost() { if (mode == Mode.Place && ghost != null) Destroy(ghost); if (mode == Mode.Move && movingId != null) UpdateBuildingView(movingId); bool wasMove = mode == Mode.Move; ghost = null; movingId = null; placingType = -1; mode = wasMove ? Mode.Edit : Mode.View; FinishGhost(); } void FinishGhost() { if (footprint != null) { Destroy(footprint); footprint = null; } draggingGhost = false; cameraRig.AllowPan = true; UpdateGrass(); RefreshGround(); } // ---------------- Ввод ---------------- void Update() { if (cam == null) return; bool down, held, up; Vector3 pos; if (Input.touchCount > 0) { if (Input.touchCount > 1) { cameraRig.EndPan(); draggingGhost = false; return; } // два пальца -- зум камеры Touch touch = Input.GetTouch(0); pos = touch.position; down = touch.phase == TouchPhase.Began; up = touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled; held = !down && !up; } else { pos = Input.mousePosition; down = Input.GetMouseButtonDown(0); up = Input.GetMouseButtonUp(0); held = Input.GetMouseButton(0) && !down; } if (down) { pointerOverGui = OverGui(pos); if (pointerOverGui) return; pointerDown = pos; pointerMoved = false; draggingGhost = (mode == Mode.Place || mode == Mode.Move) && CellUnder(pos, true, out int cx, out int cy) && InsideGhost(cx, cy); cameraRig.AllowPan = !draggingGhost; cameraRig.BeginPan(pos); } else if (held && !pointerOverGui) { if ((pos - pointerDown).magnitude > Screen.height * 0.015f) pointerMoved = true; if (draggingGhost) DragGhost(pos); else cameraRig.Pan(pos); } else if (up && !pointerOverGui) { cameraRig.EndPan(); if (!pointerMoved && !draggingGhost) Tap(pos); draggingGhost = false; cameraRig.AllowPan = true; } } void DragGhost(Vector3 pos) { if (!CellUnder(pos, true, out int cx, out int cy)) return; var t = Types[GhostType]; bool swap = ghostRot == 90 || ghostRot == 270; int w = swap ? t.H : t.W, h = swap ? t.W : t.H; int nx = Mathf.Clamp(cx - w / 2, 0, City3DLayout.Size - w), ny = Mathf.Clamp(cy - h / 2, 0, City3DLayout.Size - h); if (nx == ghostX && ny == ghostY) return; ghostX = nx; ghostY = ny; UpdateGhost(); } bool InsideGhost(int cx, int cy) { if (ghost == null) return false; var t = Types[GhostType]; bool swap = ghostRot == 90 || ghostRot == 270; int w = swap ? t.H : t.W, h = swap ? t.W : t.H; return cx >= ghostX - 1 && cx < ghostX + w + 1 && cy >= ghostY - 1 && cy < ghostY + h + 1; // с запасом на палец } void Tap(Vector3 pos) { if (!CellUnder(pos, mode == Mode.Place || mode == Mode.Move, out int cx, out int cy)) return; if (mode == Mode.Place || mode == Mode.Move) { // тап по земле -- переставить призрак туда DragGhost(pos); return; } string building = map.BuildingAt(cx, cy); if (mode == Mode.Edit && building != null) { StartMoving(building); return; } selectedBuilding = building; selectedObstacle = building == null ? map.ObstacleAt(cx, cy) : null; } // Клетка под пальцем. groundOnly -- только земля (перетаскивание призрака); иначе здание под пальцем -- его клетка bool CellUnder(Vector3 screen, bool groundOnly, out int x, out int y) { x = y = -1; Ray ray = cam.ScreenPointToRay(screen); var hits = Physics.RaycastAll(ray, 500f); System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); foreach (var hit in hits) { Transform root = hit.collider.transform.parent; if (!groundOnly && root != null) foreach (var pair in buildingViews) if (pair.Value != null && pair.Value.transform == root && map.TryGetBuilding(pair.Key, out var r)) { x = r.X; y = r.Y; return true; } if (hit.collider.gameObject.name != "Ground") continue; x = Mathf.FloorToInt(hit.point.x); y = Mathf.FloorToInt(hit.point.z); return map.InMap(x, y); } return false; } // Подписи зданий всегда смотрят в камеру void LateUpdate() { if (cam == null) return; foreach (var v in buildingViews.Values) if (v != null) { var l = v.transform.Find("Label"); if (l != null) l.rotation = cam.transform.rotation; } if (ghost != null) { var l = ghost.transform.Find("Label"); if (l != null) l.rotation = cam.transform.rotation; } } void ChangeTownHall(int delta) { int before = townHallLevel; townHallLevel = Mathf.Clamp(townHallLevel + delta, 1, 5); if (townHallLevel == before) return; RefreshGround(); if (ghost != null) UpdateGhost(); if (townHallLevel > before) { // Открылась зона -- камера плавно отходит и показывает её; здания остаются на местах int half = City3DLayout.ZoneHalfSize(townHallLevel), c = City3DLayout.Size / 2; cameraRig.FocusOn(new Rect(c - half, c - half, half * 2, half * 2)); Say($"Ратуша {townHallLevel} ур.: открыта новая зона"); } } void Say(string text) { message = text; messageUntil = Time.unscaledTime + 4f; } // ---------------- Кнопки (временные, IMGUI) ---------------- readonly List guiRects = new List(); float guiScale = 1f; GUIStyle button, label, box; bool OverGui(Vector3 screen) { var p = new Vector2(screen.x / guiScale, (Screen.height - screen.y) / guiScale); foreach (var r in guiRects) if (r.Contains(p)) return true; return false; } bool Button(Rect r, string text) { guiRects.Add(r); return GUI.Button(r, text, button); } void Box(Rect r, string text) { guiRects.Add(r); GUI.Box(r, text, box); } void OnGUI() { guiScale = Screen.height / 720f; GUI.matrix = Matrix4x4.Scale(new Vector3(guiScale, guiScale, 1)); float w = Screen.width / guiScale, h = 720f; if (button == null) { button = new GUIStyle(GUI.skin.button) { fontSize = 20, wordWrap = true }; label = new GUIStyle(GUI.skin.label) { fontSize = 20, wordWrap = true, alignment = TextAnchor.MiddleCenter }; label.normal.textColor = Color.white; box = new GUIStyle(GUI.skin.box) { fontSize = 20, wordWrap = true, alignment = TextAnchor.MiddleCenter }; box.normal.textColor = Color.white; } if (Event.current.type == EventType.Layout) guiRects.Clear(); // Верх: режим, Ратуша (тест), редактирование Box(new Rect(10, 10, 330, 44), $"3D-поселение (прототип) • Ратуша {townHallLevel} ур."); if (Button(new Rect(350, 10, 60, 44), "−")) ChangeTownHall(-1); if (Button(new Rect(415, 10, 60, 44), "+")) ChangeTownHall(+1); if (mode == Mode.View || mode == Mode.Edit) { if (Button(new Rect(w - 250, 10, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать")) { mode = mode == Mode.Edit ? Mode.View : Mode.Edit; selectedBuilding = selectedObstacle = null; RefreshGround(); if (mode == Mode.Edit) Say("Режим редактирования: нажмите здание, чтобы перенести"); } } // Слева: что построить if (mode == Mode.View || mode == Mode.Edit) { float y = 64; for (int i = 1; i < Types.Length; i++) { if (Button(new Rect(10, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) StartPlacing(i); y += 44; } } // Низ: призрак -- поворот, поставить, отмена if (mode == Mode.Place || mode == Mode.Move) { string status = ghostError == PlaceError.None ? (ghostFoundation > 0 ? $"Можно ставить (фундамент {ghostFoundation / 1000f:0.0} м)" : "Можно ставить") : SettlementMap.ErrorText(ghostError); Box(new Rect(w / 2 - 300, h - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status); if (Button(new Rect(w / 2 - 300, h - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); } if (Button(new Rect(w / 2 - 95, h - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost(); if (Button(new Rect(w / 2 + 110, h - 70, 190, 60), "Отмена")) CancelGhost(); } // Меню здания: перенос if (mode == Mode.View && selectedBuilding != null && map.TryGetBuilding(selectedBuilding, out var b)) { Box(new Rect(w / 2 - 250, h - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°"); if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); } if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedBuilding = null; } // Меню дерева / валуна: расчистка без цены и ожидания if (mode == Mode.View && selectedObstacle != null) { bool tree = selectedObstacle.StartsWith("tree"); Box(new Rect(w / 2 - 250, h - 120, 500, 44), tree ? "Дерево" : "Валун"); if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Расчистить")) { if (map.ClearObstacle(selectedObstacle) && obstacleViews.TryGetValue(selectedObstacle, out var view)) { Destroy(view); Say((tree ? "Дерево" : "Валун") + " расчищено. Награда " + (tree ? "деревом" : "камнем") + " -- после подключения сохранений (этап III)"); } selectedObstacle = null; } if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedObstacle = null; } if (Time.unscaledTime < messageUntil) GUI.Label(new Rect(w / 2 - 350, 64, 700, 60), message, label); } }