2026-11-04 14:50:22 +01:00
|
|
|
|
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), здесь только показ и ввод.
|
|
|
|
|
|
//
|
2026-11-05 14:18:22 +03:00
|
|
|
|
// Общий HUD (05.11, Валдемар): в сцене стоит префаб HUD_Canvas -- тот же, что в 2D-городе. Нажатия по HUD
|
|
|
|
|
|
// в сцену не проходят; кнопка "СТРОЙКА" HUD открывает список зданий (BuildModeController.ExternalToggle);
|
|
|
|
|
|
// временные кнопки этой сцены сдвинуты, чтобы не налезать на HUD.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Здания игры (05.11, Валдемар, City3DGameplay): у каждого 3D-здания те же компоненты, что у 2D-префабов --
|
|
|
|
|
|
// тап открывает окно здания (рабочие, добыча, улучшение по таймеру), стройка -- с ценой, лимитом, строителями и
|
|
|
|
|
|
// таймером (BuildTimer), Ратуша -- настоящая (TownHall, её уровень открывает зоны). Город сохраняется (City3DSave).
|
|
|
|
|
|
//
|
2026-11-04 14:50:22 +01:00
|
|
|
|
// Пока НЕ подключено (этапы II-III): цена и таймер стройки, Экономика и награда за расчистку, сохранение,
|
|
|
|
|
|
// дороги, настоящие модели. Кнопки -- временные (IMGUI); окна согласуются с Володей.
|
2026-11-05 14:18:22 +03:00
|
|
|
|
public class City3DController : MonoBehaviour, City3DSave.ILive
|
2026-11-04 14:50:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
// Тип здания для прототипа: максимальный габарит (клеток) с первого уровня, высота куба, цвет
|
|
|
|
|
|
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<string, GameObject> buildingViews = new Dictionary<string, GameObject>();
|
|
|
|
|
|
readonly Dictionary<string, int> buildingType = new Dictionary<string, int>();
|
|
|
|
|
|
readonly Dictionary<string, GameObject> obstacleViews = new Dictionary<string, GameObject>();
|
|
|
|
|
|
readonly List<GameObject> grassViews = new List<GameObject>();
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Построение сцены ----------------
|
|
|
|
|
|
|
2026-11-05 14:18:22 +03:00
|
|
|
|
bool buildMenuOpen; // список "что построить" открывается кнопкой "СТРОЙКА" общего HUD
|
|
|
|
|
|
|
|
|
|
|
|
// HUD закрывает верх (ресурсы) и низ (кнопки) экрана -- временные кнопки сцены ставим между ними (в единицах 720)
|
|
|
|
|
|
const float HudTop = 150f, HudBottom = 150f, HudLeft = 160f;
|
|
|
|
|
|
|
|
|
|
|
|
void ToggleBuildMenu()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (mode == Mode.Place || mode == Mode.Move) { CancelGhost(); buildMenuOpen = false; return; }
|
|
|
|
|
|
buildMenuOpen = !buildMenuOpen;
|
|
|
|
|
|
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 15:12:19 +03:00
|
|
|
|
// Отладочная сетка (05.11, просьба Миши): видна всегда, ярче, каждая 5-я линия жёлтая -- примерять референсы.
|
|
|
|
|
|
// Включается кнопкой "Сетка" в панели DEV.
|
|
|
|
|
|
public static bool DebugGrid { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
public static void SetDebugGrid(bool on)
|
|
|
|
|
|
{
|
|
|
|
|
|
DebugGrid = on;
|
|
|
|
|
|
var city = FindAnyObjectByType<City3DController>();
|
|
|
|
|
|
if (city != null && city.map != null) city.RefreshGround();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 14:18:22 +03:00
|
|
|
|
bool stateApplied; // сохранённый город уже поставлен (в первом кадре, после загрузки сохранения в Start)
|
|
|
|
|
|
|
|
|
|
|
|
void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
BuildModeController.ExternalToggle = ToggleBuildMenu;
|
|
|
|
|
|
BuildModeController.ExternalMove = MoveFromWindow;
|
|
|
|
|
|
CitySync.ExternalCity = true;
|
|
|
|
|
|
City3DSave.Live = this;
|
|
|
|
|
|
EventBus.Subscribe<TownHallLevelChangedEvent>(OnTownHallLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (BuildModeController.ExternalToggle == (System.Action)ToggleBuildMenu) BuildModeController.ExternalToggle = null;
|
|
|
|
|
|
if (BuildModeController.ExternalMove == (System.Action<GameObject>)MoveFromWindow) BuildModeController.ExternalMove = null;
|
|
|
|
|
|
CitySync.ExternalCity = false;
|
|
|
|
|
|
// Сохраняемся до ухода из сцены (SceneNavigator, выход из игры) -- здесь здания могут быть уже убраны
|
|
|
|
|
|
if (City3DSave.Live == (City3DSave.ILive)this) City3DSave.Live = null;
|
|
|
|
|
|
EventBus.Unsubscribe<TownHallLevelChangedEvent>(OnTownHallLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Сохранение (City3DSave) ----------------
|
|
|
|
|
|
|
|
|
|
|
|
public City3DSave.Data CaptureState()
|
|
|
|
|
|
{
|
2026-11-05 16:22:23 +01:00
|
|
|
|
// Сохранённый город ещё не поставлен (сохранение в первом кадре -- например, поход вернулся офлайн):
|
|
|
|
|
|
// отдаём то, что было загружено, иначе пустая сцена затёрла бы весь 3D-город
|
|
|
|
|
|
if (!stateApplied || map == null) return City3DSave.Cached;
|
2026-11-05 14:18:22 +03:00
|
|
|
|
var d = new City3DSave.Data { nextId = nextBuildingId };
|
|
|
|
|
|
foreach (var b in map.Buildings)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!buildingViews.TryGetValue(b.Id, out var view) || view == null) continue;
|
|
|
|
|
|
var e = new City3DSave.Entry { id = b.Id, type = b.Type, x = b.X, y = b.Y, rot = b.Rotation, level = City3DGameplay.LevelOf(view) };
|
|
|
|
|
|
City3DSave.WriteTimer(e, view);
|
|
|
|
|
|
d.buildings.Add(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
d.cleared.AddRange(map.Cleared);
|
|
|
|
|
|
return d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузили другое сохранение или сбросили -- город уже стоит, перестраиваем сцену целиком
|
|
|
|
|
|
public void OnSaveLoaded()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (stateApplied) UnityEngine.SceneManagement.SceneManager.LoadScene(gameObject.scene.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void ApplySaved()
|
|
|
|
|
|
{
|
|
|
|
|
|
stateApplied = true;
|
|
|
|
|
|
var d = City3DSave.Cached;
|
|
|
|
|
|
int c = City3DLayout.Size / 2;
|
|
|
|
|
|
bool hasTownHall = false;
|
|
|
|
|
|
if (d != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
nextBuildingId = Mathf.Max(1, d.nextId);
|
|
|
|
|
|
map.RestoreCleared(d.cleared);
|
|
|
|
|
|
foreach (var id in d.cleared)
|
|
|
|
|
|
if (obstacleViews.TryGetValue(id, out var ov)) { Destroy(ov); obstacleViews.Remove(id); }
|
|
|
|
|
|
// Ратуша первой: её уровень открывает зоны
|
|
|
|
|
|
var list = new List<City3DSave.Entry>(d.buildings);
|
|
|
|
|
|
list.Sort((a, b) => (b.type == "Ратуша").CompareTo(a.type == "Ратуша"));
|
|
|
|
|
|
foreach (var e in list)
|
|
|
|
|
|
{
|
|
|
|
|
|
int type = System.Array.FindIndex(Types, t => t.Name == e.type);
|
|
|
|
|
|
if (type < 0) continue;
|
|
|
|
|
|
var t = Types[type];
|
|
|
|
|
|
var rect = new PlacedRect { Id = e.id, Type = t.Name, X = e.x, Y = e.y, SizeW = t.W, SizeH = t.H, Rotation = e.rot };
|
|
|
|
|
|
if (!map.Restore(rect)) continue;
|
|
|
|
|
|
buildingType[e.id] = type;
|
|
|
|
|
|
buildingViews[e.id] = CreateBuildingView(e.id, type);
|
|
|
|
|
|
City3DGameplay.Attach(buildingViews[e.id], t.Name, e.level);
|
|
|
|
|
|
City3DSave.ReadTimer(e, buildingViews[e.id]);
|
|
|
|
|
|
UpdateBuildingView(e.id);
|
|
|
|
|
|
if (type == 0) { hasTownHall = true; townHallLevel = Mathf.Clamp(e.level, 1, 5); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!hasTownHall) PlaceNew(0, c - 2, c - 2, 0); // новый город: Ратуша в центре
|
|
|
|
|
|
UpdateGrass();
|
|
|
|
|
|
RefreshGround();
|
|
|
|
|
|
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Кнопка "Перенести" в окне здания
|
|
|
|
|
|
void MoveFromWindow(GameObject view)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var pair in buildingViews)
|
|
|
|
|
|
if (pair.Value == view)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (mode == Mode.Place || mode == Mode.Move) CancelGhost();
|
|
|
|
|
|
buildMenuOpen = false;
|
|
|
|
|
|
StartMoving(pair.Key);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnTownHallLevel(TownHallLevelChangedEvent e) => ApplyTownHallLevel(e.Level);
|
|
|
|
|
|
|
2026-11-05 17:05:59 +01:00
|
|
|
|
// Сцена 3D-города открыта (для окон HUD: «Построить Казармы» ставит призрак здесь, а не в 2D-режиме стройки)
|
|
|
|
|
|
public static City3DController Instance { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Instance == this) Instance = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Начать ставить здание по имени 3D-типа ("Казармы"). false -- такого типа нет
|
|
|
|
|
|
public bool PlaceBuilding(string type3d)
|
|
|
|
|
|
{
|
|
|
|
|
|
int type = System.Array.FindIndex(Types, t => t.Name == type3d);
|
|
|
|
|
|
if (type <= 0) return false;
|
|
|
|
|
|
buildMenuOpen = false;
|
|
|
|
|
|
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
|
|
|
|
|
|
string error = City3DGameplay.CanBuild(type3d);
|
|
|
|
|
|
if (error != null) Say(error);
|
|
|
|
|
|
StartPlacing(type);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:50:22 +01:00
|
|
|
|
void Start()
|
|
|
|
|
|
{
|
2026-11-05 17:05:59 +01:00
|
|
|
|
Instance = this;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
cam = Camera.main;
|
2026-11-04 14:55:41 +01:00
|
|
|
|
Map3D.UseRenderer3D(cam); // обычный 3D-рендерер URP (на случай, если сцену собрали вручную)
|
2026-11-04 14:50:22 +01:00
|
|
|
|
cameraRig = cam.GetComponent<City3DCamera>() ?? cam.gameObject.AddComponent<City3DCamera>();
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2026-11-04 15:03:16 +01:00
|
|
|
|
Map3D.SetUpLook(cam, 30f, 85f); // солнце, небо, дымка вдали
|
2026-11-05 14:18:22 +03:00
|
|
|
|
City3DGameplay.PutHudInFront(cam); // общий HUD -- поверх холмов и деревьев
|
2026-11-04 14:50:22 +01:00
|
|
|
|
|
|
|
|
|
|
layout = City3DLayout.Create();
|
|
|
|
|
|
map = layout.Map;
|
|
|
|
|
|
BuildGround();
|
|
|
|
|
|
BuildObstacles();
|
|
|
|
|
|
BuildGrass();
|
|
|
|
|
|
|
2026-11-05 14:18:22 +03:00
|
|
|
|
// Ратуша и остальные здания -- из сохранения (или новая Ратуша в центре): в первом кадре, когда сохранение
|
|
|
|
|
|
// уже загружено (оно грузится в Start), см. ApplySaved
|
2026-11-04 14:50:22 +01:00
|
|
|
|
RefreshGround();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:03:16 +01:00
|
|
|
|
static Material Mat(Color color) => Map3D.Mat(color);
|
2026-11-04 14:50:22 +01:00
|
|
|
|
|
|
|
|
|
|
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<MeshFilter>().sharedMesh = mesh;
|
|
|
|
|
|
groundTexture = new Texture2D(n * PixelsPerCell, n * PixelsPerCell, TextureFormat.RGBA32, false)
|
|
|
|
|
|
{
|
|
|
|
|
|
filterMode = FilterMode.Point,
|
|
|
|
|
|
wrapMode = TextureWrapMode.Clamp
|
|
|
|
|
|
};
|
2026-11-04 15:03:16 +01:00
|
|
|
|
go.AddComponent<MeshRenderer>().sharedMaterial = Map3D.TexturedMat(groundTexture);
|
2026-11-04 14:50:22 +01:00
|
|
|
|
go.AddComponent<MeshCollider>().sharedMesh = mesh;
|
|
|
|
|
|
|
2026-11-04 15:07:28 +01:00
|
|
|
|
// Лес и луга за краем участка: камера не видит пустого неба
|
|
|
|
|
|
Map3D.Block(transform, "OuterLand", PrimitiveType.Cube, new Color(0.24f, 0.4f, 0.22f), new Vector3(n / 2f, -0.7f, n / 2f),
|
|
|
|
|
|
new Vector2(n + 200f, n + 200f), 0.1f);
|
|
|
|
|
|
var forest = new System.Random(7);
|
|
|
|
|
|
for (int i = 0; i < 140; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
// кольцо деревьев вокруг участка
|
|
|
|
|
|
float angle = (float)forest.NextDouble() * Mathf.PI * 2f, r = n * 0.72f + (float)forest.NextDouble() * 14f;
|
|
|
|
|
|
var p = new Vector3(n / 2f + Mathf.Cos(angle) * r, -0.6f, n / 2f + Mathf.Sin(angle) * r);
|
|
|
|
|
|
if (p.x > 0 && p.x < n && p.z > 0 && p.z < n) continue; // внутри участка -- свои деревья
|
|
|
|
|
|
Map3D.Tree(transform, "ForestTree", p, 1.4f + (float)forest.NextDouble() * 0.8f, forest.Next(6));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:50:22 +01:00
|
|
|
|
// Вода -- плоскость над руслом
|
|
|
|
|
|
var water = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
|
|
|
|
water.name = "River";
|
|
|
|
|
|
Destroy(water.GetComponent<Collider>());
|
|
|
|
|
|
water.transform.SetParent(transform, false);
|
|
|
|
|
|
water.transform.position = new Vector3(37f, -0.25f, n / 2f);
|
|
|
|
|
|
water.transform.localScale = new Vector3(9f, 0.05f, n);
|
2026-11-04 15:03:16 +01:00
|
|
|
|
water.GetComponent<Renderer>().sharedMaterial = Mat(new Color(0.3f, 0.58f, 0.85f));
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void BuildObstacles()
|
|
|
|
|
|
{
|
2026-11-04 15:03:16 +01:00
|
|
|
|
int i = 0;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
foreach (var o in layout.Obstacles)
|
|
|
|
|
|
{
|
|
|
|
|
|
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;
|
2026-11-04 15:03:16 +01:00
|
|
|
|
var ground = new Vector3(center.x, City3DLayout.HeightAt(center.x, center.y), center.y);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
var view = o.IsTree
|
|
|
|
|
|
? Map3D.Tree(transform, o.Id, ground, 1.0f + (i * 37 % 10) * 0.05f, i) // ели и лиственные вперемешку
|
|
|
|
|
|
: Map3D.Rock(transform, o.Id, ground, o.Cells.Count > 1 ? 1.7f : 0.85f, i);
|
|
|
|
|
|
obstacleViews[o.Id] = view.gameObject;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:03:16 +01:00
|
|
|
|
// Трава кустиками -- только украшение: прячется под зданием и возвращается
|
2026-11-04 14:50:22 +01:00
|
|
|
|
void BuildGrass()
|
|
|
|
|
|
{
|
2026-11-04 15:03:16 +01:00
|
|
|
|
Color[] greens = { new Color(0.3f, 0.6f, 0.25f), new Color(0.38f, 0.66f, 0.28f), new Color(0.45f, 0.68f, 0.3f) };
|
|
|
|
|
|
for (int i = 0; i < layout.Grass.Count; i++)
|
2026-11-04 14:50:22 +01:00
|
|
|
|
{
|
2026-11-04 15:03:16 +01:00
|
|
|
|
Vector2 p = layout.Grass[i];
|
|
|
|
|
|
var g = Map3D.Shape(transform, "Grass", Map3D.Cone, greens[i % greens.Length],
|
|
|
|
|
|
new Vector3(p.x, City3DLayout.HeightAt(p.x, p.y), p.y), Vector2.one * 0.28f, 0.22f + (i % 3) * 0.06f).gameObject;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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<Collider>());
|
|
|
|
|
|
p.transform.SetParent(parent.transform, false);
|
|
|
|
|
|
p.transform.localPosition = localPos;
|
|
|
|
|
|
p.transform.localScale = scale;
|
|
|
|
|
|
p.GetComponent<Renderer>().sharedMaterial = mat;
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Текстура земли: обычная -- трава, скалы, русло; в режиме стройки -- клетки, открытая зона и следующая
|
|
|
|
|
|
void RefreshGround()
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = City3DLayout.Size, px = PixelsPerCell;
|
2026-11-05 15:12:19 +03:00
|
|
|
|
bool grid = mode != Mode.View || DebugGrid;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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);
|
2026-11-04 15:03:16 +01:00
|
|
|
|
bool shore = k == CellKind.Ground && (map.KindAt(x - 1, y) == CellKind.Water || map.KindAt(x + 1, y) == CellKind.Water
|
|
|
|
|
|
|| map.KindAt(x, y - 1) == CellKind.Water || map.KindAt(x, y + 1) == CellKind.Water);
|
|
|
|
|
|
float hm = map.HeightAt(x, y) / 1000f;
|
|
|
|
|
|
Color c = k == CellKind.Water ? new Color(0.32f, 0.5f, 0.72f)
|
|
|
|
|
|
: k == CellKind.Mountain ? Color.Lerp(new Color(0.48f, 0.45f, 0.42f), new Color(0.85f, 0.86f, 0.88f), Mathf.Clamp01((hm - 3.2f) / 1.5f)) // снег на вершинах
|
|
|
|
|
|
: shore ? new Color(0.78f, 0.72f, 0.52f) // песчаный берег
|
|
|
|
|
|
: Color.Lerp(new Color(0.33f, 0.56f, 0.27f), new Color(0.45f, 0.66f, 0.32f), Mathf.PerlinNoise(x * 0.25f, y * 0.25f));
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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;
|
2026-11-05 15:12:19 +03:00
|
|
|
|
if (grid && (ix == 0 || iy == 0)) p *= DebugGrid ? 0.55f : 0.85f; // линии клеток
|
|
|
|
|
|
if (DebugGrid && ((ix == 0 && x % 5 == 0) || (iy == 0 && y % 5 == 0))) p = new Color(1f, 0.85f, 0.2f); // каждая 5-я
|
2026-11-04 14:50:22 +01:00
|
|
|
|
// граница открытой зоны -- светлая линия
|
|
|
|
|
|
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);
|
2026-11-05 14:18:22 +03:00
|
|
|
|
City3DGameplay.Attach(buildingViews[id], t.Name, 1); // здание игры: окно, рабочие, уровни (без цены и таймера)
|
2026-11-04 14:50:22 +01:00
|
|
|
|
UpdateBuildingView(id);
|
|
|
|
|
|
UpdateGrass();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:03:16 +01:00
|
|
|
|
// Кровля по типу здания
|
|
|
|
|
|
static Color RoofFor(int type)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (Types[type].Name)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "Ратуша": return new Color(0.65f, 0.15f, 0.12f);
|
|
|
|
|
|
case "Госпиталь": return new Color(0.45f, 0.55f, 0.65f);
|
|
|
|
|
|
case "Казармы": return new Color(0.4f, 0.15f, 0.12f);
|
|
|
|
|
|
case "Каменоломня":
|
|
|
|
|
|
case "Шахта": return new Color(0.35f, 0.35f, 0.38f);
|
|
|
|
|
|
default: return Color.Lerp(Types[type].Color, new Color(0.3f, 0.15f, 0.1f), 0.55f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Здание на примитивах: стены и двускатная крыша в «единичном» объёме Body (растягивается под габарит и высоту),
|
|
|
|
|
|
// у некоторых -- свои приметы: башня Ратуши, флаг Казарм, крест Госпиталя, тёмный вход Шахты
|
2026-11-04 14:50:22 +01:00
|
|
|
|
GameObject CreateBuildingView(string id, int type)
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = Types[type];
|
|
|
|
|
|
var root = new GameObject(id + "_" + t.Name);
|
|
|
|
|
|
root.transform.SetParent(transform, false);
|
2026-11-04 15:03:16 +01:00
|
|
|
|
var body = new GameObject("Body").transform;
|
|
|
|
|
|
body.SetParent(root.transform, false);
|
|
|
|
|
|
var hit = body.gameObject.AddComponent<BoxCollider>(); // тап по зданию попадает в него целиком
|
|
|
|
|
|
hit.center = new Vector3(0, 0.5f, 0);
|
|
|
|
|
|
hit.size = Vector3.one;
|
|
|
|
|
|
|
|
|
|
|
|
Color walls = Color.Lerp(t.Color, Color.white, 0.3f), roofColor = RoofFor(type);
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(0, 0.31f, 0), new Vector3(1f, 0.62f, 1f), walls);
|
|
|
|
|
|
var top = Map3D.Shape(body, "Roof", Map3D.Roof, roofColor, Vector3.zero, new Vector2(1.08f, 1.12f), 0.38f).transform;
|
|
|
|
|
|
top.localPosition = new Vector3(0, 0.62f, 0);
|
|
|
|
|
|
switch (t.Name)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "Ратуша":
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(0.3f, 0.7f, 0.3f), new Vector3(0.28f, 1.4f, 0.28f), walls);
|
|
|
|
|
|
var spire = Map3D.Shape(body, "Spire", Map3D.Pyramid, roofColor, Vector3.zero, Vector2.one * 0.4f, 0.4f, 45f).transform;
|
|
|
|
|
|
spire.localPosition = new Vector3(0.3f, 1.4f, 0.3f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "Казармы":
|
|
|
|
|
|
Unit(body, PrimitiveType.Cylinder, new Vector3(-0.4f, 0.75f, 0.4f), new Vector3(0.03f, 0.75f, 0.03f), new Color(0.35f, 0.25f, 0.15f));
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(-0.32f, 1.35f, 0.4f), new Vector3(0.16f, 0.12f, 0.01f), new Color(0.8f, 0.15f, 0.15f));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "Госпиталь":
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(0, 0.38f, -0.505f), new Vector3(0.08f, 0.28f, 0.01f), new Color(0.85f, 0.1f, 0.1f));
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(0, 0.38f, -0.505f), new Vector3(0.24f, 0.08f, 0.01f), new Color(0.85f, 0.1f, 0.1f));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "Шахта":
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(0, 0.2f, -0.505f), new Vector3(0.35f, 0.4f, 0.02f), new Color(0.1f, 0.1f, 0.12f));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "Ферма":
|
|
|
|
|
|
// грядки перед амбаром
|
|
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
|
|
|
|
Unit(body, PrimitiveType.Cube, new Vector3(-0.3f + i * 0.3f, 0.02f, -0.62f), new Vector3(0.22f, 0.04f, 0.2f), new Color(0.55f, 0.75f, 0.25f));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var door = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.28f, 0.18f, 0.1f)));
|
2026-11-04 14:50:22 +01:00
|
|
|
|
door.name = "Door"; // показывает, куда повёрнуто здание
|
2026-11-04 15:03:16 +01:00
|
|
|
|
var foundation = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.62f, 0.58f, 0.52f)));
|
2026-11-04 14:50:22 +01:00
|
|
|
|
foundation.name = "Foundation";
|
|
|
|
|
|
|
|
|
|
|
|
var labelGo = new GameObject("Label");
|
|
|
|
|
|
labelGo.transform.SetParent(root.transform, false);
|
|
|
|
|
|
var label = labelGo.AddComponent<TextMeshPro>();
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:03:16 +01:00
|
|
|
|
// Деталь в единичном объёме здания (Body растягивается под габарит и высоту)
|
|
|
|
|
|
static void Unit(Transform body, PrimitiveType type, Vector3 localPos, Vector3 scale, Color color)
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = GameObject.CreatePrimitive(type);
|
|
|
|
|
|
Destroy(p.GetComponent<Collider>());
|
|
|
|
|
|
p.transform.SetParent(body, false);
|
|
|
|
|
|
p.transform.localPosition = localPos;
|
|
|
|
|
|
p.transform.localScale = scale;
|
|
|
|
|
|
p.GetComponent<Renderer>().sharedMaterial = Mat(color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:50:22 +01:00
|
|
|
|
// Положение, поворот, фундамент по данным карты (или призрака при переносе)
|
|
|
|
|
|
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);
|
2026-11-04 15:03:16 +01:00
|
|
|
|
// Body -- в размерах без поворота и повёрнут сам: конёк крыши и приметы поворачиваются вместе со зданием
|
2026-11-04 14:50:22 +01:00
|
|
|
|
var body = root.transform.Find("Body");
|
2026-11-04 15:03:16 +01:00
|
|
|
|
body.localScale = new Vector3(t.W - 0.3f, t.Height, t.H - 0.3f);
|
|
|
|
|
|
body.localPosition = Vector3.zero;
|
|
|
|
|
|
body.localRotation = Quaternion.Euler(0, -rot, 0);
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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);
|
2026-11-05 16:56:44 +01:00
|
|
|
|
// Не на Ратуше и не в чужом здании: ближайшее к центру экрана место, где здание можно поставить
|
|
|
|
|
|
if (FindFreeSpot(t, ghostX, ghostY, out int fx, out int fy, out int frot))
|
|
|
|
|
|
{
|
|
|
|
|
|
ghostX = fx; ghostY = fy; ghostRot = frot;
|
|
|
|
|
|
// камера плавно подъезжает к месту, не меняя приближения
|
|
|
|
|
|
float size = cameraRig.distance / 1.15f;
|
|
|
|
|
|
cameraRig.FocusOn(new Rect(fx + t.W / 2f - size / 2f, fy + t.H / 2f - size / 2f, size, size));
|
|
|
|
|
|
}
|
2026-11-04 14:50:22 +01:00
|
|
|
|
EnsureFootprint();
|
|
|
|
|
|
RefreshGround();
|
|
|
|
|
|
UpdateGhost();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 16:56:44 +01:00
|
|
|
|
// Ближайшее свободное место (по кольцам от x0, y0), сначала без поворота, потом с поворотом на 90
|
|
|
|
|
|
bool FindFreeSpot(BuildingType t, int x0, int y0, out int x, out int y, out int rot)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int r = 0; r < City3DLayout.Size; r++)
|
|
|
|
|
|
for (int dy = -r; dy <= r; dy++)
|
|
|
|
|
|
for (int dx = -r; dx <= r; dx++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Mathf.Max(Mathf.Abs(dx), Mathf.Abs(dy)) != r) continue; // только кольцо радиуса r
|
|
|
|
|
|
foreach (int tryRot in new[] { 0, 90 })
|
|
|
|
|
|
if (map.Validate(x0 + dx, y0 + dy, t.W, t.H, tryRot, townHallLevel, null, out _) == PlaceError.None)
|
|
|
|
|
|
{
|
|
|
|
|
|
x = x0 + dx; y = y0 + dy; rot = tryRot;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
x = x0; y = y0; rot = 0;
|
|
|
|
|
|
return false; // места нет -- призрак там, где камера, красный с причиной
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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<Collider>());
|
|
|
|
|
|
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<Renderer>().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;
|
2026-11-05 14:18:22 +03:00
|
|
|
|
string problem = City3DGameplay.CanBuild(Types[type].Name); // уровень Ратуши, лимит, строители, ресурсы
|
|
|
|
|
|
if (problem != null) { Say(problem); return; }
|
|
|
|
|
|
string id = "b_" + nextBuildingId;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
CancelGhost();
|
2026-11-05 14:18:22 +03:00
|
|
|
|
if (PlaceNew(type, x, y, rot))
|
|
|
|
|
|
{
|
|
|
|
|
|
City3DGameplay.StartConstruction(buildingViews[id]); // цена и стройка по времени (BuildTimer)
|
|
|
|
|
|
Say(Types[type].Name + ": стройка началась");
|
|
|
|
|
|
}
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
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()
|
|
|
|
|
|
{
|
2026-11-05 14:18:22 +03:00
|
|
|
|
if (!stateApplied && map != null) ApplySaved();
|
2026-11-04 14:50:22 +01:00
|
|
|
|
if (cam == null) return;
|
2026-11-05 17:05:59 +01:00
|
|
|
|
UpdateConstructionViews();
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-11-05 16:58:12 +01:00
|
|
|
|
pointerOverGui = OverGui(pos) || City3DInput.OverHud(pos); // и кнопки/окна общего HUD
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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);
|
2026-11-05 17:46:42 +03:00
|
|
|
|
else if (pointerMoved) cameraRig.Pan(pos); // карта едет только при настоящем перетаскивании
|
|
|
|
|
|
else cameraRig.Track(pos); // а мелкое дрожание пальца при тапе её не трогает
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
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; }
|
2026-11-05 14:18:22 +03:00
|
|
|
|
// Тап по зданию -- окно здания игры (как в 2D): рабочие, добыча, улучшение, перенос
|
|
|
|
|
|
if (building != null && buildingViews.TryGetValue(building, out var tapped) && tapped != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
selectedBuilding = selectedObstacle = null;
|
|
|
|
|
|
buildMenuOpen = false;
|
|
|
|
|
|
BuildingWindow.Open(tapped);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-11-04 14:50:22 +01:00
|
|
|
|
selectedBuilding = building;
|
|
|
|
|
|
selectedObstacle = building == null ? map.ObstacleAt(cx, cy) : null;
|
2026-11-05 17:46:42 +03:00
|
|
|
|
// Расчищать можно только в открытой зоне стройки (за её пределами -- после улучшения Ратуши) (05.11)
|
|
|
|
|
|
if (selectedObstacle != null && !map.IsOpen(cx, cy, townHallLevel))
|
|
|
|
|
|
{
|
|
|
|
|
|
selectedObstacle = null;
|
|
|
|
|
|
Say("Это за пределами зоны стройки -- откроется после улучшения Ратуши");
|
|
|
|
|
|
}
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Клетка под пальцем. 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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 14:18:22 +03:00
|
|
|
|
// Кнопки "Ратуша −/+" -- только для теста: меняют уровень настоящей Ратуши (TownHall)
|
2026-11-04 14:50:22 +01:00
|
|
|
|
void ChangeTownHall(int delta)
|
2026-11-05 14:18:22 +03:00
|
|
|
|
{
|
|
|
|
|
|
var th = TownHall.Instance;
|
|
|
|
|
|
int level = Mathf.Clamp((th != null ? th.currentLevel : townHallLevel) + delta, 1, th != null ? th.maxLevel : 5);
|
|
|
|
|
|
if (th != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
th.currentLevel = level;
|
|
|
|
|
|
EventBus.Publish(new TownHallLevelChangedEvent { Level = level }); // зоны обновит OnTownHallLevel
|
|
|
|
|
|
}
|
|
|
|
|
|
else ApplyTownHallLevel(level);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Уровень Ратуши изменился (улучшение в окне Ратуши или тест): открыть зону
|
|
|
|
|
|
void ApplyTownHallLevel(int level)
|
2026-11-04 14:50:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
int before = townHallLevel;
|
2026-11-05 14:18:22 +03:00
|
|
|
|
townHallLevel = Mathf.Clamp(level, 1, 5);
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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<Rect> guiRects = new List<Rect>();
|
|
|
|
|
|
float guiScale = 1f;
|
|
|
|
|
|
GUIStyle button, label, box;
|
|
|
|
|
|
|
2026-11-05 17:05:59 +01:00
|
|
|
|
// ---------------- Стройка по времени в 3D ----------------
|
|
|
|
|
|
// BuildTimer затемняет только 2D-спрайты, поэтому в 3D здание, которое ещё строится, выглядело готовым
|
|
|
|
|
|
// (Казармы «построены», а нанимать нельзя). Здесь: здание темнее и над ним -- «Стройка 0:45» (05.11)
|
|
|
|
|
|
|
|
|
|
|
|
readonly Dictionary<string, TextMeshPro> constructionLabels = new Dictionary<string, TextMeshPro>();
|
|
|
|
|
|
static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"), ColorId = Shader.PropertyToID("_Color");
|
|
|
|
|
|
MaterialPropertyBlock constructionBlock;
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateConstructionViews()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var pair in buildingViews)
|
|
|
|
|
|
{
|
|
|
|
|
|
var view = pair.Value;
|
|
|
|
|
|
if (view == null) continue;
|
|
|
|
|
|
var timer = BuildTimer.Of(view);
|
|
|
|
|
|
bool building = timer != null && BuildTimer.IsConstructing(view.transform);
|
|
|
|
|
|
constructionLabels.TryGetValue(pair.Key, out var label);
|
|
|
|
|
|
if (building)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (label == null) label = constructionLabels[pair.Key] = StartConstructionLook(view);
|
|
|
|
|
|
label.text = "Стройка " + GameData.FormatTime(timer.SecondsLeft);
|
|
|
|
|
|
label.transform.rotation = cam.transform.rotation;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (label != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Destroy(label.gameObject);
|
|
|
|
|
|
constructionLabels.Remove(pair.Key);
|
|
|
|
|
|
foreach (var r in view.GetComponentsInChildren<Renderer>()) if (!(r is TMPro.TMP_SubMesh)) r.SetPropertyBlock(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TextMeshPro StartConstructionLook(GameObject view)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (constructionBlock == null) constructionBlock = new MaterialPropertyBlock();
|
|
|
|
|
|
Bounds bounds = new Bounds(view.transform.position, Vector3.zero);
|
|
|
|
|
|
bool first = true;
|
|
|
|
|
|
foreach (var r in view.GetComponentsInChildren<Renderer>())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (first) { bounds = r.bounds; first = false; } else bounds.Encapsulate(r.bounds);
|
|
|
|
|
|
var m = r.sharedMaterial;
|
|
|
|
|
|
if (m == null) continue;
|
|
|
|
|
|
Color c = m.HasProperty(BaseColorId) ? m.GetColor(BaseColorId) : m.HasProperty(ColorId) ? m.GetColor(ColorId) : Color.white;
|
|
|
|
|
|
Color grey = Color.Lerp(c, new Color(0.45f, 0.42f, 0.38f), 0.6f) * 0.75f;
|
|
|
|
|
|
grey.a = 1f;
|
|
|
|
|
|
r.GetPropertyBlock(constructionBlock);
|
|
|
|
|
|
constructionBlock.SetColor(BaseColorId, grey);
|
|
|
|
|
|
constructionBlock.SetColor(ColorId, grey);
|
|
|
|
|
|
r.SetPropertyBlock(constructionBlock);
|
|
|
|
|
|
}
|
|
|
|
|
|
var go = new GameObject("ConstructionLabel");
|
|
|
|
|
|
go.transform.SetParent(view.transform, false);
|
|
|
|
|
|
go.transform.position = new Vector3(bounds.center.x, bounds.max.y + 0.7f, bounds.center.z);
|
|
|
|
|
|
var label = go.AddComponent<TextMeshPro>();
|
|
|
|
|
|
label.fontSize = 7f;
|
|
|
|
|
|
label.alignment = TextAlignmentOptions.Center;
|
|
|
|
|
|
label.rectTransform.sizeDelta = new Vector2(8f, 1.5f);
|
|
|
|
|
|
label.color = new Color(1f, 0.85f, 0.4f);
|
|
|
|
|
|
label.outlineWidth = 0.25f;
|
|
|
|
|
|
label.outlineColor = Color.black;
|
|
|
|
|
|
return label;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:50:22 +01:00
|
|
|
|
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();
|
|
|
|
|
|
|
2026-11-05 14:18:22 +03:00
|
|
|
|
// Окно HUD открыто -- временные кнопки сцены не показываем (и не ловим нажатия)
|
|
|
|
|
|
if (UIPanelManager.Instance != null && UIPanelManager.Instance.CurrentId != "") return;
|
|
|
|
|
|
|
2026-11-05 17:46:42 +03:00
|
|
|
|
// Под верхней панелью HUD, справа: Ратуша −/+ -- только в редакторе и отладочной сборке (тест зон), редактирование
|
|
|
|
|
|
if (Application.isEditor || Debug.isDebugBuild)
|
|
|
|
|
|
{
|
|
|
|
|
|
Box(new Rect(w - 480, HudTop, 290, 44), $"3D (тест) • Ратуша {townHallLevel} ур.");
|
|
|
|
|
|
if (Button(new Rect(w - 185, HudTop, 55, 44), "−")) ChangeTownHall(-1);
|
|
|
|
|
|
if (Button(new Rect(w - 125, HudTop, 55, 44), "+")) ChangeTownHall(+1);
|
|
|
|
|
|
}
|
2026-11-04 14:50:22 +01:00
|
|
|
|
if (mode == Mode.View || mode == Mode.Edit)
|
|
|
|
|
|
{
|
2026-11-05 14:18:22 +03:00
|
|
|
|
if (Button(new Rect(w - 250, HudTop + 50, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать"))
|
2026-11-04 14:50:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
mode = mode == Mode.Edit ? Mode.View : Mode.Edit;
|
|
|
|
|
|
selectedBuilding = selectedObstacle = null;
|
|
|
|
|
|
RefreshGround();
|
|
|
|
|
|
if (mode == Mode.Edit) Say("Режим редактирования: нажмите здание, чтобы перенести");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 17:05:59 +01:00
|
|
|
|
// Справа по центру: приближение / отдаление. На телефоне есть щипок двумя пальцами, но в редакторе
|
|
|
|
|
|
// (Device Simulator) колесо мыши не доходит до игры, а щипок мышью не сделать -- без кнопок карту не отдалить (05.11)
|
|
|
|
|
|
if (Button(new Rect(w - 70, h / 2 - 62, 60, 56), "+")) cameraRig.Zoom(0.2f);
|
|
|
|
|
|
if (Button(new Rect(w - 70, h / 2 + 2, 60, 56), "−")) cameraRig.Zoom(-0.25f);
|
|
|
|
|
|
|
2026-11-05 14:18:22 +03:00
|
|
|
|
// Слева, правее кнопок HUD: что построить -- открывается кнопкой "СТРОЙКА" общего HUD
|
|
|
|
|
|
if (buildMenuOpen && (mode == Mode.View || mode == Mode.Edit))
|
2026-11-04 14:50:22 +01:00
|
|
|
|
{
|
2026-11-05 14:18:22 +03:00
|
|
|
|
float y = HudTop;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
for (int i = 1; i < Types.Length; i++)
|
|
|
|
|
|
{
|
2026-11-05 14:18:22 +03:00
|
|
|
|
if (Button(new Rect(HudLeft, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) { buildMenuOpen = false; StartPlacing(i); }
|
2026-11-04 14:50:22 +01:00
|
|
|
|
y += 44;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Низ: призрак -- поворот, поставить, отмена
|
|
|
|
|
|
if (mode == Mode.Place || mode == Mode.Move)
|
|
|
|
|
|
{
|
|
|
|
|
|
string status = ghostError == PlaceError.None
|
|
|
|
|
|
? (ghostFoundation > 0 ? $"Можно ставить (фундамент {ghostFoundation / 1000f:0.0} м)" : "Можно ставить")
|
|
|
|
|
|
: SettlementMap.ErrorText(ghostError);
|
2026-11-05 14:18:22 +03:00
|
|
|
|
Box(new Rect(w / 2 - 300, h - HudBottom - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status);
|
|
|
|
|
|
if (Button(new Rect(w / 2 - 300, h - HudBottom - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); }
|
|
|
|
|
|
if (Button(new Rect(w / 2 - 95, h - HudBottom - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost();
|
|
|
|
|
|
if (Button(new Rect(w / 2 + 110, h - HudBottom - 70, 190, 60), "Отмена")) CancelGhost();
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Меню здания: перенос
|
|
|
|
|
|
if (mode == Mode.View && selectedBuilding != null && map.TryGetBuilding(selectedBuilding, out var b))
|
|
|
|
|
|
{
|
2026-11-05 14:18:22 +03:00
|
|
|
|
Box(new Rect(w / 2 - 250, h - HudBottom - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°");
|
|
|
|
|
|
if (Button(new Rect(w / 2 - 250, h - HudBottom - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); }
|
|
|
|
|
|
if (Button(new Rect(w / 2 + 5, h - HudBottom - 70, 245, 60), "Закрыть")) selectedBuilding = null;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Меню дерева / валуна: расчистка без цены и ожидания
|
|
|
|
|
|
if (mode == Mode.View && selectedObstacle != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool tree = selectedObstacle.StartsWith("tree");
|
2026-11-05 14:18:22 +03:00
|
|
|
|
Box(new Rect(w / 2 - 250, h - HudBottom - 120, 500, 44), tree ? "Дерево" : "Валун");
|
|
|
|
|
|
if (Button(new Rect(w / 2 - 250, h - HudBottom - 70, 245, 60), "Расчистить"))
|
2026-11-04 14:50:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
if (map.ClearObstacle(selectedObstacle) && obstacleViews.TryGetValue(selectedObstacle, out var view))
|
|
|
|
|
|
{
|
|
|
|
|
|
Destroy(view);
|
2026-11-05 14:18:22 +03:00
|
|
|
|
City3DGameplay.RewardClearing(tree); // +10 дерева или камня, один раз (решение 04.11)
|
|
|
|
|
|
Say((tree ? "Дерево" : "Валун") + " расчищено: +10 " + (tree ? "дерева" : "камня"));
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
selectedObstacle = null;
|
|
|
|
|
|
}
|
2026-11-05 14:18:22 +03:00
|
|
|
|
if (Button(new Rect(w / 2 + 5, h - HudBottom - 70, 245, 60), "Закрыть")) selectedObstacle = null;
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Time.unscaledTime < messageUntil)
|
2026-11-05 14:18:22 +03:00
|
|
|
|
GUI.Label(new Rect(w / 2 - 350, HudTop, 700, 60), message, label);
|
2026-11-04 14:50:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|