Files
MVP1/Assets/Scripts/City3D/City3DController.cs
T

702 lines
32 KiB
C#
Raw Normal View History

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), здесь только показ и ввод.
//
// Пока НЕ подключено (этапы II-III): цена и таймер стройки, Экономика и награда за расчистку, сохранение,
// дороги, настоящие модели. Кнопки -- временные (IMGUI); окна согласуются с Володей.
public class City3DController : MonoBehaviour
{
// Тип здания для прототипа: максимальный габарит (клеток) с первого уровня, высота куба, цвет
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;
// ---------------- Построение сцены ----------------
void Start()
{
cam = Camera.main;
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();
// Материал URP по умолчанию берём у примитива: шейдер точно есть в сборке
var probe = GameObject.CreatePrimitive(PrimitiveType.Cube);
baseMaterial = new Material(probe.GetComponent<Renderer>().sharedMaterial);
Destroy(probe);
layout = City3DLayout.Create();
map = layout.Map;
BuildGround();
BuildObstacles();
BuildGrass();
// Ратуша стоит в центре с самого начала
int c = City3DLayout.Size / 2;
PlaceNew(0, c - 2, c - 2, 0);
RefreshGround();
}
Material Mat(Color color)
{
var m = new Material(baseMaterial) { color = color };
return m;
}
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
};
var mat = Mat(Color.white);
mat.mainTexture = groundTexture;
go.AddComponent<MeshRenderer>().sharedMaterial = mat;
go.AddComponent<MeshCollider>().sharedMesh = mesh;
// Вода -- плоскость над руслом
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);
water.GetComponent<Renderer>().sharedMaterial = Mat(new Color(0.25f, 0.5f, 0.85f));
}
void BuildObstacles()
{
var trunk = Mat(new Color(0.45f, 0.3f, 0.18f));
var leaves = Mat(new Color(0.18f, 0.48f, 0.2f));
var stone = Mat(new Color(0.55f, 0.55f, 0.58f));
foreach (var o in layout.Obstacles)
{
var root = new GameObject(o.Id);
root.transform.SetParent(transform, false);
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;
float h = City3DLayout.HeightAt(center.x, center.y);
root.transform.position = new Vector3(center.x, h, center.y);
if (o.IsTree)
{
Part(root, PrimitiveType.Cylinder, new Vector3(0, 0.5f, 0), new Vector3(0.25f, 0.5f, 0.25f), trunk);
Part(root, PrimitiveType.Sphere, new Vector3(0, 1.4f, 0), new Vector3(1.1f, 1.3f, 1.1f), leaves);
}
else
{
float s = o.Cells.Count > 1 ? 1.8f : 0.9f;
Part(root, PrimitiveType.Sphere, new Vector3(0, s * 0.3f, 0), new Vector3(s, s * 0.7f, s * 0.9f), stone);
}
obstacleViews[o.Id] = root;
}
}
void BuildGrass()
{
var grass = Mat(new Color(0.35f, 0.65f, 0.3f));
foreach (var p in layout.Grass)
{
var g = GameObject.CreatePrimitive(PrimitiveType.Cube);
Destroy(g.GetComponent<Collider>());
g.name = "Grass";
g.transform.SetParent(transform, false);
g.transform.position = new Vector3(p.x, City3DLayout.HeightAt(p.x, p.y) + 0.08f, p.y);
g.transform.localScale = new Vector3(0.25f, 0.16f, 0.25f);
g.transform.rotation = Quaternion.Euler(0, p.x * 37f, 0);
g.GetComponent<Renderer>().sharedMaterial = grass;
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;
bool grid = mode != Mode.View;
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);
Color c = k == CellKind.Water ? new Color(0.3f, 0.45f, 0.7f)
: k == CellKind.Mountain ? new Color(0.5f, 0.48f, 0.45f)
: Color.Lerp(new Color(0.35f, 0.58f, 0.3f), new Color(0.42f, 0.66f, 0.34f), Mathf.PerlinNoise(x * 0.3f, y * 0.3f));
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;
if (grid && (ix == 0 || iy == 0)) p *= 0.85f; // линии клеток
// граница открытой зоны -- светлая линия
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);
UpdateBuildingView(id);
UpdateGrass();
return true;
}
GameObject CreateBuildingView(string id, int type)
{
var t = Types[type];
var root = new GameObject(id + "_" + t.Name);
root.transform.SetParent(transform, false);
var body = GameObject.CreatePrimitive(PrimitiveType.Cube);
body.name = "Body";
body.transform.SetParent(root.transform, false);
body.GetComponent<Renderer>().sharedMaterial = Mat(t.Color);
var door = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.25f, 0.18f, 0.12f)));
door.name = "Door"; // показывает, куда повёрнуто здание
var foundation = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.6f, 0.57f, 0.5f)));
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;
}
// Положение, поворот, фундамент по данным карты (или призрака при переносе)
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);
var body = root.transform.Find("Body");
body.localScale = new Vector3(w - 0.3f, t.Height, h - 0.3f);
body.localPosition = new Vector3(0, t.Height / 2f, 0);
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);
EnsureFootprint();
RefreshGround();
UpdateGhost();
}
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;
CancelGhost();
if (PlaceNew(type, x, y, rot)) Say(Types[type].Name + " построено (прототип: без цены и таймера)");
}
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()
{
if (cam == null) return;
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)
{
pointerOverGui = OverGui(pos);
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);
else cameraRig.Pan(pos);
}
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; }
selectedBuilding = building;
selectedObstacle = building == null ? map.ObstacleAt(cx, cy) : null;
}
// Клетка под пальцем. 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; }
}
void ChangeTownHall(int delta)
{
int before = townHallLevel;
townHallLevel = Mathf.Clamp(townHallLevel + delta, 1, 5);
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;
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();
// Верх: режим, Ратуша (тест), редактирование
Box(new Rect(10, 10, 330, 44), $"3D-поселение (прототип) • Ратуша {townHallLevel} ур.");
if (Button(new Rect(350, 10, 60, 44), "−")) ChangeTownHall(-1);
if (Button(new Rect(415, 10, 60, 44), "+")) ChangeTownHall(+1);
if (mode == Mode.View || mode == Mode.Edit)
{
if (Button(new Rect(w - 250, 10, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать"))
{
mode = mode == Mode.Edit ? Mode.View : Mode.Edit;
selectedBuilding = selectedObstacle = null;
RefreshGround();
if (mode == Mode.Edit) Say("Режим редактирования: нажмите здание, чтобы перенести");
}
}
// Слева: что построить
if (mode == Mode.View || mode == Mode.Edit)
{
float y = 64;
for (int i = 1; i < Types.Length; i++)
{
if (Button(new Rect(10, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) StartPlacing(i);
y += 44;
}
}
// Низ: призрак -- поворот, поставить, отмена
if (mode == Mode.Place || mode == Mode.Move)
{
string status = ghostError == PlaceError.None
? (ghostFoundation > 0 ? $"Можно ставить (фундамент {ghostFoundation / 1000f:0.0} м)" : "Можно ставить")
: SettlementMap.ErrorText(ghostError);
Box(new Rect(w / 2 - 300, h - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status);
if (Button(new Rect(w / 2 - 300, h - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); }
if (Button(new Rect(w / 2 - 95, h - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost();
if (Button(new Rect(w / 2 + 110, h - 70, 190, 60), "Отмена")) CancelGhost();
}
// Меню здания: перенос
if (mode == Mode.View && selectedBuilding != null && map.TryGetBuilding(selectedBuilding, out var b))
{
Box(new Rect(w / 2 - 250, h - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°");
if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); }
if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedBuilding = null;
}
// Меню дерева / валуна: расчистка без цены и ожидания
if (mode == Mode.View && selectedObstacle != null)
{
bool tree = selectedObstacle.StartsWith("tree");
Box(new Rect(w / 2 - 250, h - 120, 500, 44), tree ? "Дерево" : "Валун");
if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Расчистить"))
{
if (map.ClearObstacle(selectedObstacle) && obstacleViews.TryGetValue(selectedObstacle, out var view))
{
Destroy(view);
Say((tree ? "Дерево" : "Валун") + " расчищено. Награда " + (tree ? "деревом" : "камнем") +
" -- после подключения сохранений (этап III)");
}
selectedObstacle = null;
}
if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedObstacle = null;
}
if (Time.unscaledTime < messageUntil)
GUI.Label(new Rect(w / 2 - 350, 64, 700, 60), message, label);
}
}