using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; // СВОБОДНАЯ СТРОЙКА НА СЕТКЕ (игрок сам выбирает место): // 1. Кнопка "СТРОЙКА" включает режим: на земле появляется сетка (красные клетки заняты), сверху подсказка, // и сразу открывается список зданий. // 2. Выбрали здание -> появляется призрак: полупрозрачное здание и пятно его клеток. // Зелёное пятно -- можно ставить, красное -- занято или за границей сетки. // 3. Призрак двигают пальцем (тащить за само пятно) или тапом по земле -- он прыгает туда. // Камера в это время не ездит. Размер у зданий разный (BuildingData.footprint). // 4. Внизу панель: "Поставить" (если место свободно и хватает ресурсов), "Другое здание", "Отмена". // 5. После постройки режим остаётся включённым -- можно строить дальше. "Выйти" -- сетка прячется. // // 6. Лимит зданий по уровню Ратуши (EconomyConfig, ТЕСТ): в списке видно "2 / 3", сверх лимита строить нельзя. // 7. Стройка идёт по времени и занимает строителя (BuildTimer): все заняты -- поставить нельзя. // 8. Перенос (кнопка "Перенести" в окне здания): тот же призрак, но здание переезжает бесплатно. // // Вешается билдером (Tools -> Build HUD Panels) на HUD_Canvas, кнопка Nav_Build вызывает ToggleMode(). [DefaultExecutionOrder(-50)] // раньше камеры: захват пальца призраком должен быть виден ей в том же кадре public class BuildModeController : MonoBehaviour { public static BuildModeController Instance { get; private set; } [Tooltip("Кнопка HUD, которая включает/выключает режим (надпись меняется сама)")] public Button modeButton; [Tooltip("Старое меню стройки -- прячем, оно больше не используется")] public GameObject legacyBuildMenu; public bool IsActive { get; private set; } GameObject banner; GameObject chooser; GameObject placePanel; TextMeshProUGUI placeInfo; Button placeButton; BuildGhost ghost; GameObject moving; // здание, которое переносим (null -- строим новое) bool dragging; Vector2 grabOffset; Vector2 downPos; bool downOverUI; bool downOnGhost; float nextPanelRefresh; const float TapThreshold = 25f; void Awake() { Instance = this; } void OnDestroy() { if (Instance == this) Instance = null; UIBlocker.WorldInputCaptured = false; } void Start() { if (legacyBuildMenu != null) legacyBuildMenu.SetActive(false); SetMode(false); } public void ToggleMode() { SetMode(!IsActive); } public void SetMode(bool active) { IsActive = active; CloseChooser(); CancelGhost(); UpdateButtonLabel(); BuildGrid.Instance.ShowOverlay(active); if (banner != null) { Destroy(banner); banner = null; } if (active) { if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll(); banner = CreateBanner(); OpenChooser(); } } void Update() { if (!IsActive) return; // открыли любую панель HUD -- выходим из режима стройки if (UIPanelManager.Instance != null && UIPanelManager.Instance.CurrentId != "") { SetMode(false); return; } if (chooser != null || ghost == null) { StopDrag(); return; } HandlePointer(); if (Time.unscaledTime >= nextPanelRefresh) { nextPanelRefresh = Time.unscaledTime + 0.3f; ghost.Recheck(); // клетки могли заняться (загрузка сохранения) или ресурсы -- прийти RefreshPlacePanel(); } } // ---------------- палец: тащим призрак или тапаем по земле ---------------- void HandlePointer() { Vector2 world = PointerWorld(); if (Input.GetMouseButtonDown(0)) { downPos = Input.mousePosition; downOverUI = UIBlocker.IsPointerOverUI(); downOnGhost = !downOverUI && ghost.Contains(world); if (downOnGhost) { dragging = true; grabOffset = (Vector2)ghost.transform.position - world; UIBlocker.WorldInputCaptured = true; // камера не поедет } } if (dragging && Input.GetMouseButton(0)) { var before = ghost.Cell; ghost.MoveNear(world + grabOffset); if (ghost.Cell != before) RefreshPlacePanel(); } if (Input.GetMouseButtonUp(0)) { bool wasDragging = dragging; StopDrag(); if (wasDragging || downOverUI || UIBlocker.IsPointerOverUI()) return; if (((Vector2)Input.mousePosition - downPos).magnitude > TapThreshold) return; // тащили камеру ghost.MoveNear(world); // тап по земле -- призрак прыгает сюда RefreshPlacePanel(); } } void StopDrag() { dragging = false; UIBlocker.WorldInputCaptured = false; } static Vector2 PointerWorld() { var cam = Camera.main; if (cam == null) return Vector2.zero; Vector3 w = cam.ScreenToWorldPoint(Input.mousePosition); return new Vector2(w.x, w.y); } // ---------------- призрак ---------------- // Перенос построенного здания: призрак на его месте, "Сюда" -- переезжает бесплатно public void StartMove(GameObject building) { var id = building != null ? building.GetComponent() : null; var data = id != null && BuildManager.Instance != null ? BuildManager.Instance.FindData(id.buildingName) : null; if (data == null) { UIKit.Toast("Это здание перенести нельзя"); return; } if (!IsActive) { IsActive = true; UpdateButtonLabel(); BuildGrid.Instance.ShowOverlay(true); if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll(); if (banner == null) banner = CreateBanner(); } CloseChooser(); CancelGhost(); moving = building; BuildGrid.IgnoredBuilding = building; // его клетки считаются свободными, пока выбираем новое место BuildGrid.Instance.Refresh(); ghost = BuildGhost.Create(data); ghost.MoveNear((Vector2)building.transform.position + ghost.Footprint.anchorOffset); CreatePlacePanel(); } void StartGhost(BuildingData data) { CancelGhost(); CloseChooser(); ghost = BuildGhost.Create(data); // ставим в центр экрана, а если там занято -- на ближайшее свободное место var cam = Camera.main; Vector2 center = cam != null ? (Vector2)cam.transform.position : Vector2.zero; if (BuildGrid.Instance.FindFreeNear(center, ghost.Footprint.size, out var cell)) ghost.MoveToCell(cell); else { ghost.MoveNear(center); UIKit.Toast("Свободного места под это здание нет"); } CreatePlacePanel(); } void CancelGhost() { StopDrag(); if (moving != null || BuildGrid.IgnoredBuilding != null) { moving = null; BuildGrid.IgnoredBuilding = null; // здание остаётся на старом месте if (BuildGrid.Instance != null) BuildGrid.Instance.Refresh(); } if (ghost != null) Destroy(ghost.gameObject); ghost = null; if (placePanel != null) Destroy(placePanel); placePanel = null; } void Place() { if (ghost == null || BuildManager.Instance == null) return; var d = ghost.Data; ghost.Recheck(); if (!ghost.CellsFree) { UIKit.Toast("Здесь строить нельзя: клетки заняты"); return; } if (moving != null) { var building = moving; var id = building != null ? building.GetComponent() : null; if (id == null || string.IsNullOrEmpty(id.buildingName)) { CancelGhost(); return; } // здания уже нет // Перенос бесплатный: меняется только место. Уровень, стройка и улучшение остаются у того же здания, // ресурсы не тратятся и не возвращаются (решения 04.11) building.transform.position = ghost.PrefabPosition; CancelGhost(); // снимает "игнор" клеток -- здание уже на новом месте UIKit.Toast("Перенесено: " + d.buildingName); return; } string limit = LimitProblem(d); if (limit != null) { UIKit.Toast(limit); return; } if (BuildTimer.DurationMs(false, 1) > 0 && BuildTimer.FreeBuilders == 0) { UIKit.Toast("Все строители заняты"); return; } if (!d.CanAfford()) { UIKit.Toast(GameProgress.Instance.MissingText(ToRewards(d.cost))); return; } if (BuildManager.Instance.BuildAt(d, ghost.Cell) != null) { UIKit.Toast(BuildTimer.DurationMs(false, 1) > 0 ? "Стройка началась: " + d.buildingName : "Построено: " + d.buildingName); CancelGhost(); // режим остаётся: "Здания" в подсказке сверху -- строить дальше } else UIKit.Toast("Не получилось построить"); } // ---------------- нижняя панель: Поставить / Другое здание / Отмена ---------------- void CreatePlacePanel() { if (placePanel != null) Destroy(placePanel); var bar = UIKit.Row(UIKit.Overlay, -1, 14, new Color(0.08f, 0.1f, 0.14f, 0.94f), 14); bar.name = "BuildPlacePanel"; bar.anchorMin = new Vector2(0.04f, 0); bar.anchorMax = new Vector2(0.96f, 0); bar.pivot = new Vector2(0.5f, 0); bar.sizeDelta = new Vector2(0, 130); bar.anchoredPosition = new Vector2(0, 40); placePanel = bar.gameObject; placeInfo = UIKit.Text(bar, "", 26, Color.white, TextAlignmentOptions.Left); UIKit.Size(placeInfo, -1, -1, 1); placeButton = UIKit.Button(bar, moving != null ? "Сюда" : "Поставить", UIKit.Theme.Green, Place, 230, 90, 28); if (moving == null) UIKit.Button(bar, "Другое", UIKit.Theme.Blue, OpenChooser, 180, 90, 26); UIKit.Button(bar, "Отмена", UIKit.Theme.Gray, CancelGhost, 180, 90, 26); RefreshPlacePanel(); } void RefreshPlacePanel() { if (ghost == null || placeInfo == null) return; var d = ghost.Data; string where = ghost.CellsFree ? "место свободно" : "клетки заняты"; if (moving != null) { placeInfo.text = "Перенос: " + d.buildingName + " " + ghost.Footprint.size.x + "×" + ghost.Footprint.size.y + " кл. • " + where + "\nБесплатно"; UIKit.SetButton(placeButton, ghost.CellsFree ? "Сюда" : "Занято", ghost.CellsFree); return; } bool afford = d.CanAfford(); string limit = LimitProblem(d); bool builders = BuildTimer.DurationMs(false, 1) <= 0 || BuildTimer.FreeBuilders > 0; placeInfo.text = "" + d.buildingName + " " + ghost.Footprint.size.x + "×" + ghost.Footprint.size.y + " кл. • " + where + "\n" + CostText(d) + BuildTimeText(); bool ok = ghost.CellsFree && afford && limit == null && builders; string label = ok ? "Поставить" : !ghost.CellsFree ? "Занято" : limit != null ? "Лимит" : !builders ? "Строители заняты" : "Мало ресурсов"; UIKit.SetButton(placeButton, label, ok); } // ---------------- окно выбора здания ---------------- void OpenChooser() { CloseChooser(); StopDrag(); var bm = BuildManager.Instance; if (bm == null) return; GameObject root; var card = UIKit.Modal("Что построить?", out root); chooser = root; // окно фиксированного размера с прокруткой, чтобы влезало на любом экране var fit = card.GetComponent(); if (fit != null) DestroyImmediate(fit); card.anchorMin = new Vector2(0.08f, 0.08f); card.anchorMax = new Vector2(0.92f, 0.92f); card.offsetMin = Vector2.zero; card.offsetMax = Vector2.zero; int th = TownHall.Instance != null ? TownHall.Instance.currentLevel : 1; UIKit.Text(card, "Ратуша " + th + " ур. • строителей свободно: " + BuildTimer.FreeBuilders + " из " + BuildTimer.Builders + "\nвыберите здание, потом место на сетке", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); var holder = UIKit.Make("ListHolder", card); UIKit.Size(holder, -1, -1).flexibleHeight = 1; ScrollRect scroll; var list = UIKit.ScrollList(holder, out scroll, 10); // доступные сверху, закрытые по уровню Ратуши -- ниже var all = new List(bm.allBuildings); all.RemoveAll(b => b == null || b.prefab == null); all.Sort((a, b) => a.requiredTownHallLevel.CompareTo(b.requiredTownHallLevel)); foreach (var data in all) { var d = data; int max = EconomyConfig.Load().BuildingLimitFor(d.buildingName, th); int have = CountBuilt(d.buildingName); bool full = max >= 0 && have >= max; bool locked = d.requiredTownHallLevel > th || full; bool afford = !locked && d.CanAfford(); var size = BuildFootprint.Of(d).size; var row = UIKit.Row(list, 120, 14, locked ? UIKit.Theme.CardDark : UIKit.Theme.Card, 10); var icon = UIKit.Box(row, d.icon != null ? Color.white : UIKit.Theme.Gray); if (d.icon != null) { icon.sprite = d.icon; icon.preserveAspect = true; } UIKit.Size(icon, 100, 100); var info = UIKit.Card(row, new Color(0, 0, 0, 0), 0, 4); UIKit.Size(info, -1, -1, 1); string count = max >= 0 ? " построено " + have + " / " + max + "" : ""; UIKit.Text(info, d.buildingName + " " + size.x + "×" + size.y + " кл." + count, 30, locked ? UIKit.Theme.TextDim : UIKit.Theme.Text).fontStyle = FontStyles.Bold; string why = d.requiredTownHallLevel > th ? "Откроется на Ратуше " + d.requiredTownHallLevel + " уровня" : full ? NextLimitText(d.buildingName, th, have) : CostText(d); UIKit.Text(info, why, 24, locked ? UIKit.Theme.Gold : UIKit.Theme.TextDim); // выбрать можно и без ресурсов: место присмотреть, а "Поставить" загорится, когда хватит string label = d.requiredTownHallLevel > th ? "Закрыто" : full ? "Лимит" : "Выбрать"; var b = UIKit.Button(row, label, afford ? UIKit.Theme.Green : UIKit.Theme.Gray, () => StartGhost(d), 230, 90, 26); b.interactable = !locked; } UIKit.Button(card, "Закрыть", UIKit.Theme.Gray, CloseChooser, -1, 90, 30); } // ---------------- лимит зданий по Ратуше (EconomyConfig, ТЕСТ) ---------------- static int CountBuilt(string buildingName) { int n = 0; foreach (var id in FindObjectsByType(FindObjectsSortMode.None)) if (id.buildingName == buildingName) n++; return n; } // null -- лимит позволяет; иначе текст для игрока static string LimitProblem(BuildingData d) { int th = TownHall.Instance != null ? TownHall.Instance.currentLevel : 1; int max = EconomyConfig.Load().BuildingLimitFor(d.buildingName, th); int have = CountBuilt(d.buildingName); if (max < 0 || have < max) return null; return NextLimitText(d.buildingName, th, have); } static string NextLimitText(string buildingName, int th, int have) { var config = EconomyConfig.Load(); for (int lvl = th + 1; lvl <= 10; lvl++) if (config.BuildingLimitFor(buildingName, lvl) > have) return "Лимит " + have + " / " + config.BuildingLimitFor(buildingName, th) + ": больше — на Ратуше " + lvl + " ур."; return "Достигнут лимит: " + have; } static string BuildTimeText() { long ms = BuildTimer.DurationMs(false, 1); if (ms <= 0) return ""; string free = BuildTimer.FreeBuilders > 0 ? "" : " все строители заняты"; return " • стройка " + GameData.FormatTime(ms / 1000.0) + free; } void CloseChooser() { if (chooser != null) Destroy(chooser); chooser = null; } static RewardItem[] ToRewards(ResourceCost[] cost) { if (cost == null) return new RewardItem[0]; var r = new RewardItem[cost.Length]; for (int i = 0; i < cost.Length; i++) r[i] = new RewardItem(cost[i].type, Mathf.CeilToInt(cost[i].amount)); return r; } // Цена по-русски, красным -- чего не хватает static string CostText(BuildingData d) { if (d.cost == null || d.cost.Length == 0) return "Бесплатно"; var parts = new List(); foreach (var c in d.cost) { bool ok = ResourceManager.Instance != null && ResourceManager.Instance.HasEnough(c.type, c.amount); parts.Add("" + GameData.Short(c.amount) + " " + GameData.ResName(c.type)); } return string.Join(", ", parts.ToArray()); } // ---------------- подсказка сверху и кнопка ---------------- GameObject CreateBanner() { // полоска сверху экрана (не модальная: остальной экран остаётся кликабельным) var bar = UIKit.Row(UIKit.Overlay, -1, 16, new Color(0.1f, 0.25f, 0.12f, 0.92f), 14); bar.name = "BuildModeBanner"; bar.anchorMin = new Vector2(0.1f, 1); bar.anchorMax = new Vector2(0.9f, 1); bar.pivot = new Vector2(0.5f, 1); bar.sizeDelta = new Vector2(0, 110); bar.anchoredPosition = new Vector2(0, -150); var t = UIKit.Text(bar, "РЕЖИМ СТРОЙКИ: двигайте здание по сетке, красное -- занято", 26, Color.white, TextAlignmentOptions.Left); UIKit.Size(t, -1, -1, 1); UIKit.Button(bar, "Здания", UIKit.Theme.Green, OpenChooser, 180, 80, 28); UIKit.Button(bar, "Выйти", UIKit.Theme.Red, () => SetMode(false), 160, 80, 28); return bar.gameObject; } void UpdateButtonLabel() { if (modeButton == null) return; var t = modeButton.GetComponentInChildren(); if (t != null) t.text = IsActive ? "ГОТОВО" : "СТРОЙКА"; } }