453 lines
21 KiB
C#
453 lines
21 KiB
C#
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; }
|
||
|
||
// 3D-поселение (сцена City3D): своей 2D-стройки там нет -- кнопка "СТРОЙКА" общего HUD
|
||
// открывает стройку 3D-сцены. Задаёт City3DController, пока сцена открыта (05.11).
|
||
public static System.Action ExternalToggle;
|
||
// То же для кнопки "Перенести" в окне здания: в 3D-сцене здание переносит сама сцена
|
||
public static System.Action<GameObject> ExternalMove;
|
||
static bool Has2DCity => BuildManager.Instance != null;
|
||
|
||
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()
|
||
{
|
||
if (!Has2DCity && ExternalToggle != null) { ExternalToggle(); return; } // 3D-сцена: своя стройка
|
||
SetMode(!IsActive);
|
||
}
|
||
|
||
public void SetMode(bool active)
|
||
{
|
||
if (!Has2DCity) active = false; // в 3D-сцене 2D-стройки нет (и сетку 2D не создаём)
|
||
IsActive = active;
|
||
CloseChooser();
|
||
CancelGhost();
|
||
UpdateButtonLabel();
|
||
if (Has2DCity) 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)
|
||
{
|
||
if (!Has2DCity && ExternalMove != null) { ExternalMove(building); return; } // 3D-сцена
|
||
var id = building != null ? building.GetComponent<PlacedBuildingId>() : 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<PlacedBuildingId>() : 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 ? "<color=#9be59b>место свободно</color>" : "<color=#ff7a7a>клетки заняты</color>";
|
||
if (moving != null)
|
||
{
|
||
placeInfo.text = "<b>Перенос: " + d.buildingName + "</b> " + 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 = "<b>" + d.buildingName + "</b> " + 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<ContentSizeFitter>();
|
||
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<BuildingData>(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 ? " <size=22><color=#aab>построено " + have + " / " + max + "</color></size>" : "";
|
||
UIKit.Text(info, d.buildingName + " <size=22><color=#aab>" + size.x + "×" + size.y + " кл.</color></size>" + 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<PlacedBuildingId>(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 ? "" : " <color=#ff7a7a>все строители заняты</color>";
|
||
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<string>();
|
||
foreach (var c in d.cost)
|
||
{
|
||
bool ok = ResourceManager.Instance != null && ResourceManager.Instance.HasEnough(c.type, c.amount);
|
||
parts.Add("<color=" + (ok ? "#9be59b" : "#ff7a7a") + ">" + GameData.Short(c.amount) + "</color> " + 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<TextMeshProUGUI>();
|
||
if (t != null) t.text = IsActive ? "ГОТОВО" : "СТРОЙКА";
|
||
}
|
||
}
|