235 lines
10 KiB
C#
235 lines
10 KiB
C#
using System.Collections.Generic;
|
|||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.UI;
|
||
|
|
using TMPro;
|
||
|
|
|
||
|
|
// НОВАЯ СХЕМА СТРОЙКИ (как в Rise of Kingdoms):
|
||
|
|
// 1. Кнопка "СТРОЙКА" включает режим строительства -- появляются свободные участки и подсказка сверху.
|
||
|
|
// 2. Тап по свободному участку -> окно со зданиями (цена, что доступно по уровню Ратуши).
|
||
|
|
// 3. "Построить" -> здание встаёт на участок. Режим остаётся включённым, можно строить дальше.
|
||
|
|
// 4. Кнопка "ВЫЙТИ" (та же кнопка или в подсказке) -- выключает режим, участки снова прячутся.
|
||
|
|
// Вне режима пустые участки не видны и не мешают, при запуске игры ничего не открывается.
|
||
|
|
//
|
||
|
|
// Вешается билдером (Tools -> Build HUD Panels) на HUD_Canvas, кнопка Nav_Build вызывает ToggleMode().
|
||
|
|
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;
|
||
|
|
Vector2 downPos;
|
||
|
|
bool downOverUI;
|
||
|
|
float nextVisualRefresh;
|
||
|
|
|
||
|
|
static readonly Color SlotColor = new Color(0.3f, 1f, 0.4f, 0.75f);
|
||
|
|
|
||
|
|
void Awake() { Instance = this; }
|
||
|
|
void OnDestroy() { if (Instance == this) Instance = null; }
|
||
|
|
|
||
|
|
void Start()
|
||
|
|
{
|
||
|
|
if (legacyBuildMenu != null) legacyBuildMenu.SetActive(false);
|
||
|
|
SetMode(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void ToggleMode() { SetMode(!IsActive); }
|
||
|
|
|
||
|
|
public void SetMode(bool active)
|
||
|
|
{
|
||
|
|
IsActive = active;
|
||
|
|
CloseChooser();
|
||
|
|
ApplySlotVisuals();
|
||
|
|
UpdateButtonLabel();
|
||
|
|
|
||
|
|
if (banner != null) { Destroy(banner); banner = null; }
|
||
|
|
if (active)
|
||
|
|
{
|
||
|
|
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
|
||
|
|
banner = CreateBanner();
|
||
|
|
if (FreeSlots().Count == 0) UIKit.Toast("Свободных участков нет. Улучшайте Ратушу и сносите старое.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void Update()
|
||
|
|
{
|
||
|
|
if (Time.unscaledTime >= nextVisualRefresh)
|
||
|
|
{
|
||
|
|
nextVisualRefresh = Time.unscaledTime + 0.3f;
|
||
|
|
ApplySlotVisuals(); // участки могли освободиться/заняться (снос, загрузка сохранения)
|
||
|
|
}
|
||
|
|
if (!IsActive) return;
|
||
|
|
// открыли любую панель HUD -- выходим из режима стройки
|
||
|
|
if (UIPanelManager.Instance != null && UIPanelManager.Instance.CurrentId != "") { SetMode(false); return; }
|
||
|
|
if (chooser != null) return;
|
||
|
|
|
||
|
|
// Тап = нажали и отпустили почти на том же месте (чтобы перетаскивание камеры не считалось тапом)
|
||
|
|
if (Input.GetMouseButtonDown(0))
|
||
|
|
{
|
||
|
|
downPos = Input.mousePosition;
|
||
|
|
downOverUI = UIBlocker.IsPointerOverUI();
|
||
|
|
}
|
||
|
|
if (Input.GetMouseButtonUp(0))
|
||
|
|
{
|
||
|
|
Vector2 up = Input.mousePosition;
|
||
|
|
if (downOverUI || UIBlocker.IsPointerOverUI() || (up - downPos).magnitude > 25f) return;
|
||
|
|
HandleTap(up);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void HandleTap(Vector2 screenPos)
|
||
|
|
{
|
||
|
|
if (BuildManager.Instance == null) return;
|
||
|
|
var slot = BuildManager.Instance.SlotAtScreen(screenPos);
|
||
|
|
if (slot == null) return;
|
||
|
|
if (slot.isOccupied)
|
||
|
|
{
|
||
|
|
var id = slot.occupyingBuilding != null ? slot.occupyingBuilding.GetComponent<PlacedBuildingId>() : null;
|
||
|
|
UIKit.Toast("Участок занят" + (id != null ? ": " + id.buildingName : ""));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
OpenChooser(slot);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------- участки ----------------
|
||
|
|
|
||
|
|
List<BuildSlot> FreeSlots()
|
||
|
|
{
|
||
|
|
var list = new List<BuildSlot>();
|
||
|
|
foreach (var s in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None)) if (!s.isOccupied) list.Add(s);
|
||
|
|
return list;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ApplySlotVisuals()
|
||
|
|
{
|
||
|
|
foreach (var s in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None))
|
||
|
|
{
|
||
|
|
var sr = s.GetComponent<SpriteRenderer>();
|
||
|
|
if (sr == null) continue;
|
||
|
|
bool show = IsActive && !s.isOccupied;
|
||
|
|
if (sr.enabled != show) sr.enabled = show;
|
||
|
|
if (show) sr.color = SlotColor;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------- окно выбора здания ----------------
|
||
|
|
|
||
|
|
void OpenChooser(BuildSlot slot)
|
||
|
|
{
|
||
|
|
CloseChooser();
|
||
|
|
var bm = BuildManager.Instance;
|
||
|
|
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, "Участок: " + slot.name + " • Ратуша " + th + " ур.", 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);
|
||
|
|
all.Sort((a, b) => a.requiredTownHallLevel.CompareTo(b.requiredTownHallLevel));
|
||
|
|
|
||
|
|
foreach (var data in all)
|
||
|
|
{
|
||
|
|
var d = data;
|
||
|
|
bool locked = d.requiredTownHallLevel > th;
|
||
|
|
bool afford = !locked && d.CanAfford();
|
||
|
|
|
||
|
|
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);
|
||
|
|
UIKit.Text(info, d.buildingName, 30, locked ? UIKit.Theme.TextDim : UIKit.Theme.Text).fontStyle = FontStyles.Bold;
|
||
|
|
UIKit.Text(info, locked ? "Откроется на Ратуше " + d.requiredTownHallLevel + " уровня" : CostText(d), 24,
|
||
|
|
locked ? UIKit.Theme.Gold : UIKit.Theme.TextDim);
|
||
|
|
|
||
|
|
string label = locked ? "Закрыто" : (afford ? "Построить" : "Мало ресурсов");
|
||
|
|
var b = UIKit.Button(row, label, afford ? UIKit.Theme.Green : UIKit.Theme.Gray, () =>
|
||
|
|
{
|
||
|
|
if (BuildManager.Instance.BuildOnSlot(d, slot))
|
||
|
|
{
|
||
|
|
UIKit.Toast("Построено: " + d.buildingName);
|
||
|
|
CloseChooser();
|
||
|
|
ApplySlotVisuals();
|
||
|
|
if (FreeSlots().Count == 0) SetMode(false);
|
||
|
|
}
|
||
|
|
else UIKit.Toast(GameProgress.Instance.MissingText(ToRewards(d.cost)));
|
||
|
|
}, 230, 90, 26);
|
||
|
|
b.interactable = !locked;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIKit.Button(card, "Отмена", UIKit.Theme.Gray, CloseChooser, -1, 90, 30);
|
||
|
|
}
|
||
|
|
|
||
|
|
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.15f, 1); bar.anchorMax = new Vector2(0.85f, 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, "РЕЖИМ СТРОЙКИ: нажмите на зелёный участок", 30, Color.white, TextAlignmentOptions.Left);
|
||
|
|
UIKit.Size(t, -1, -1, 1);
|
||
|
|
UIKit.Button(bar, "Выйти", UIKit.Theme.Red, () => SetMode(false), 180, 80, 28);
|
||
|
|
return bar.gameObject;
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateButtonLabel()
|
||
|
|
{
|
||
|
|
if (modeButton == null) return;
|
||
|
|
var t = modeButton.GetComponentInChildren<TextMeshProUGUI>();
|
||
|
|
if (t != null) t.text = IsActive ? "ГОТОВО" : "СТРОЙКА";
|
||
|
|
}
|
||
|
|
}
|