Население — часть Поселения: найм из жителей, история голода; стройка на сетке

This commit is contained in:
2026-10-28 22:11:36 +03:00
parent 6567b78ecf
commit 10b58fee22
27 changed files with 1067 additions and 109 deletions
+161 -63
View File
@@ -3,14 +3,18 @@ using UnityEngine;
using UnityEngine.UI;
using TMPro;
// НОВАЯ СХЕМА СТРОЙКИ (как в Rise of Kingdoms):
// 1. Кнопка "СТРОЙКА" включает режим строительства -- появляются свободные участки и подсказка сверху.
// 2. Тап по свободному участку -> окно со зданиями (цена, что доступно по уровню Ратуши).
// 3. "Построить" -> здание встаёт на участок. Режим остаётся включённым, можно строить дальше.
// 4. Кнопка "ВЫЙТИ" (та же кнопка или в подсказке) -- выключает режим, участки снова прячутся.
// Вне режима пустые участки не видны и не мешают, при запуске игры ничего не открывается.
// СВОБОДНАЯ СТРОЙКА НА СЕТКЕ (игрок сам выбирает место):
// 1. Кнопка "СТРОЙКА" включает режим: на земле появляется сетка (красные клетки заняты), сверху подсказка,
// и сразу открывается список зданий.
// 2. Выбрали здание -> появляется призрак: полупрозрачное здание и пятно его клеток.
// Зелёное пятно -- можно ставить, красное -- занято или за границей сетки.
// 3. Призрак двигают пальцем (тащить за само пятно) или тапом по земле -- он прыгает туда.
// Камера в это время не ездит. Размер у зданий разный (BuildingData.footprint).
// 4. Внизу панель: "Поставить" (если место свободно и хватает ресурсов), "Другое здание", "Отмена".
// 5. После постройки режим остаётся включённым -- можно строить дальше. "Выйти" -- сетка прячется.
//
// Вешается билдером (Tools -> Build HUD Panels) на HUD_Canvas, кнопка Nav_Build вызывает ToggleMode().
[DefaultExecutionOrder(-50)] // раньше камеры: захват пальца призраком должен быть виден ей в том же кадре
public class BuildModeController : MonoBehaviour
{
public static BuildModeController Instance { get; private set; }
@@ -24,14 +28,27 @@ public class BuildModeController : MonoBehaviour
GameObject banner;
GameObject chooser;
GameObject placePanel;
TextMeshProUGUI placeInfo;
Button placeButton;
BuildGhost ghost;
bool dragging;
Vector2 grabOffset;
Vector2 downPos;
bool downOverUI;
float nextVisualRefresh;
bool downOnGhost;
float nextPanelRefresh;
static readonly Color SlotColor = new Color(0.3f, 1f, 0.4f, 0.75f);
const float TapThreshold = 25f;
void Awake() { Instance = this; }
void OnDestroy() { if (Instance == this) Instance = null; }
void OnDestroy()
{
if (Instance == this) Instance = null;
UIBlocker.WorldInputCaptured = false;
}
void Start()
{
@@ -45,85 +62,172 @@ public class BuildModeController : MonoBehaviour
{
IsActive = active;
CloseChooser();
ApplySlotVisuals();
CancelGhost();
UpdateButtonLabel();
BuildGrid.Instance.ShowOverlay(active);
if (banner != null) { Destroy(banner); banner = null; }
if (active)
{
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
banner = CreateBanner();
if (FreeSlots().Count == 0) UIKit.Toast("Свободных участков нет. Улучшайте Ратушу и сносите старое.");
OpenChooser();
}
}
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 (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))
{
Vector2 up = Input.mousePosition;
if (downOverUI || UIBlocker.IsPointerOverUI() || (up - downPos).magnitude > 25f) return;
HandleTap(up);
bool wasDragging = dragging;
StopDrag();
if (wasDragging || downOverUI || UIBlocker.IsPointerOverUI()) return;
if (((Vector2)Input.mousePosition - downPos).magnitude > TapThreshold) return; // тащили камеру
ghost.MoveNear(world); // тап по земле -- призрак прыгает сюда
RefreshPlacePanel();
}
}
void HandleTap(Vector2 screenPos)
void StopDrag()
{
if (BuildManager.Instance == null) return;
var slot = BuildManager.Instance.SlotAtScreen(screenPos);
if (slot == null) return;
if (slot.isOccupied)
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);
}
// ---------------- призрак ----------------
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
{
var id = slot.occupyingBuilding != null ? slot.occupyingBuilding.GetComponent<PlacedBuildingId>() : null;
UIKit.Toast("Участок занят" + (id != null ? ": " + id.buildingName : ""));
return;
ghost.MoveNear(center);
UIKit.Toast("Свободного места под это здание нет");
}
OpenChooser(slot);
CreatePlacePanel();
}
// ---------------- участки ----------------
List<BuildSlot> FreeSlots()
void CancelGhost()
{
var list = new List<BuildSlot>();
foreach (var s in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None)) if (!s.isOccupied) list.Add(s);
return list;
StopDrag();
if (ghost != null) Destroy(ghost.gameObject);
ghost = null;
if (placePanel != null) Destroy(placePanel);
placePanel = null;
}
void ApplySlotVisuals()
void Place()
{
foreach (var s in FindObjectsByType<BuildSlot>(FindObjectsSortMode.None))
if (ghost == null || BuildManager.Instance == null) return;
var d = ghost.Data;
ghost.Recheck();
if (!ghost.CellsFree) { UIKit.Toast("Здесь строить нельзя: клетки заняты"); return; }
if (!d.CanAfford()) { UIKit.Toast(GameProgress.Instance.MissingText(ToRewards(d.cost))); return; }
if (BuildManager.Instance.BuildAt(d, ghost.Cell) != null)
{
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;
UIKit.Toast("Построено: " + 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, "Поставить", UIKit.Theme.Green, Place, 230, 90, 28);
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;
bool afford = d.CanAfford();
string where = ghost.CellsFree ? "<color=#9be59b>место свободно</color>" : "<color=#ff7a7a>клетки заняты</color>";
placeInfo.text = "<b>" + d.buildingName + "</b> " + ghost.Footprint.size.x + "×" + ghost.Footprint.size.y +
" кл. • " + where + "\n" + CostText(d);
bool ok = ghost.CellsFree && afford;
UIKit.SetButton(placeButton, ok ? "Поставить" : (ghost.CellsFree ? "Мало ресурсов" : "Занято"), ok);
}
// ---------------- окно выбора здания ----------------
void OpenChooser(BuildSlot slot)
void OpenChooser()
{
CloseChooser();
StopDrag();
var bm = BuildManager.Instance;
if (bm == null) return;
GameObject root;
var card = UIKit.Modal("Что построить?", out root);
chooser = root;
@@ -135,7 +239,7 @@ public class BuildModeController : MonoBehaviour
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);
UIKit.Text(card, "Ратуша " + th + " ур. • выберите здание, потом место на сетке", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
var holder = UIKit.Make("ListHolder", card);
UIKit.Size(holder, -1, -1).flexibleHeight = 1;
@@ -144,7 +248,7 @@ public class BuildModeController : MonoBehaviour
// доступные сверху, закрытые по уровню Ратуши -- ниже
var all = new List<BuildingData>(bm.allBuildings);
all.RemoveAll(b => b == null);
all.RemoveAll(b => b == null || b.prefab == null);
all.Sort((a, b) => a.requiredTownHallLevel.CompareTo(b.requiredTownHallLevel));
foreach (var data in all)
@@ -152,6 +256,7 @@ public class BuildModeController : MonoBehaviour
var d = data;
bool locked = d.requiredTownHallLevel > th;
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);
@@ -160,26 +265,18 @@ public class BuildModeController : MonoBehaviour
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, d.buildingName + " <size=22><color=#aab>" + size.x + "×" + size.y + " кл.</color></size>", 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);
// выбрать можно и без ресурсов: место присмотреть, а "Поставить" загорится, когда хватит
string label = locked ? "Закрыто" : "Выбрать";
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);
UIKit.Button(card, "Закрыть", UIKit.Theme.Gray, CloseChooser, -1, 90, 30);
}
void CloseChooser()
@@ -216,12 +313,13 @@ public class BuildModeController : MonoBehaviour
// полоска сверху экрана (не модальная: остальной экран остаётся кликабельным)
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.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, "РЕЖИМ СТРОЙКИ: нажмите на зелёный участок", 30, Color.white, TextAlignmentOptions.Left);
var t = UIKit.Text(bar, "РЕЖИМ СТРОЙКИ: двигайте здание по сетке, красное -- занято", 26, Color.white, TextAlignmentOptions.Left);
UIKit.Size(t, -1, -1, 1);
UIKit.Button(bar, "Выйти", UIKit.Theme.Red, () => SetMode(false), 180, 80, 28);
UIKit.Button(bar, "Здания", UIKit.Theme.Green, OpenChooser, 180, 80, 28);
UIKit.Button(bar, "Выйти", UIKit.Theme.Red, () => SetMode(false), 160, 80, 28);
return bar.gameObject;
}