From 5502489c0701ac3f61ecc46072c7d21db1067338 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 5 Nov 2026 17:05:59 +0100 Subject: [PATCH] =?UTF-8?q?3D:=20=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B8=D0=B4=D0=BD=D0=B0=20(=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20+=20?= =?UTF-8?q?=C2=AB=D0=A1=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=200:45=C2=BB),?= =?UTF-8?q?=20=C2=AB=D0=92=D0=BE=D0=B9=D1=81=D0=BA=D0=B0=C2=BB=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=8E=D1=82=20=D0=BE=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D1=8F=D1=89=D0=B8=D1=85=D1=81=D1=8F=20=D0=9A=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=85,=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B7=D1=83=D0=BC=D0=B0=20+/=E2=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BuildTimer затемнял только 2D-спрайты: в 3D строящиеся Казармы выглядели готовыми, а окно «Войска» писало «казарм: 0» - «Войска»: «Казармы строятся, осталось …»; перерисовка, когда достроились; «Построить Казармы» в 3D ставит призрак - «+ / −» справа: в Device Simulator колесо мыши не доходит до игры Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/City3D/City3DController.cs | 91 +++++++++++++++++++++++ Assets/Scripts/Meta/Panels/TroopsPanel.cs | 46 ++++++++++-- Docs/Modules/Settlement.md | 1 + 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/City3D/City3DController.cs b/Assets/Scripts/City3D/City3DController.cs index c0da680..2c38e37 100644 --- a/Assets/Scripts/City3D/City3DController.cs +++ b/Assets/Scripts/City3D/City3DController.cs @@ -207,8 +207,30 @@ public class City3DController : MonoBehaviour, City3DSave.ILive void OnTownHallLevel(TownHallLevelChangedEvent e) => ApplyTownHallLevel(e.Level); + // Сцена 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; + } + void Start() { + Instance = this; cam = Camera.main; Map3D.UseRenderer3D(cam); // обычный 3D-рендерер URP (на случай, если сцену собрали вручную) cameraRig = cam.GetComponent() ?? cam.gameObject.AddComponent(); @@ -674,6 +696,7 @@ public class City3DController : MonoBehaviour, City3DSave.ILive { if (!stateApplied && map != null) ApplySaved(); if (cam == null) return; + UpdateConstructionViews(); bool down, held, up; Vector3 pos; if (Input.touchCount > 0) @@ -845,6 +868,69 @@ public class City3DController : MonoBehaviour, City3DSave.ILive float guiScale = 1f; GUIStyle button, label, box; + // ---------------- Стройка по времени в 3D ---------------- + // BuildTimer затемняет только 2D-спрайты, поэтому в 3D здание, которое ещё строится, выглядело готовым + // (Казармы «построены», а нанимать нельзя). Здесь: здание темнее и над ним -- «Стройка 0:45» (05.11) + + readonly Dictionary constructionLabels = new Dictionary(); + 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()) 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()) + { + 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(); + 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; + } + bool OverGui(Vector3 screen) { var p = new Vector2(screen.x / guiScale, (Screen.height - screen.y) / guiScale); @@ -900,6 +986,11 @@ public class City3DController : MonoBehaviour, City3DSave.ILive } } + // Справа по центру: приближение / отдаление. На телефоне есть щипок двумя пальцами, но в редакторе + // (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); + // Слева, правее кнопок HUD: что построить -- открывается кнопкой "СТРОЙКА" общего HUD if (buildMenuOpen && (mode == Mode.View || mode == Mode.Edit)) { diff --git a/Assets/Scripts/Meta/Panels/TroopsPanel.cs b/Assets/Scripts/Meta/Panels/TroopsPanel.cs index d78026f..2362030 100644 --- a/Assets/Scripts/Meta/Panels/TroopsPanel.cs +++ b/Assets/Scripts/Meta/Panels/TroopsPanel.cs @@ -14,6 +14,23 @@ public class TroopsPanel : BasePanel static readonly int[] Amounts = { 1, 10, 50 }; + int shownBarracks; // сколько Казарм было, когда окно рисовалось: достроились -- перерисовать + TextMeshProUGUI barracksBuildingLeft; // «Осталось: 0:45», пока Казармы строятся + + // Казармы, которые ещё строятся: сколько секунд до ближайшего конца; -1 -- таких нет + static double BarracksConstructionSecondsLeft() + { + double best = -1; + foreach (var t in BuildTimer.All) + { + if (t == null || !t.IsConstruction) continue; + var id = t.GetComponent(); + if (id == null || id.buildingName != BarracksBootstrap.BarracksName) continue; + if (best < 0 || t.SecondsLeft < best) best = t.SecondsLeft; + } + return best; + } + protected override void Build(RectTransform root) { var d = GP.Data; @@ -73,15 +90,28 @@ public class TroopsPanel : BasePanel GarrisonButton(gRow, troopsArmy, "Все", troopsArmy.SoldiersAtHome); // --- Нет казарм -- нанимать негде --- + shownBarracks = Barracks.Count; + barracksBuildingLeft = null; if (Barracks.Count == 0) { var warn = UIKit.Card(list, new Color(0.35f, 0.18f, 0.12f)); - UIKit.Text(warn, "Нанимать войска можно только в Казармах. Постройте их на свободном участке.", 28, UIKit.Theme.Text); - UIKit.Button(warn, "ПОСТРОИТЬ КАЗАРМЫ", UIKit.Theme.Green, () => + double left = BarracksConstructionSecondsLeft(); + if (left >= 0) { - if (BuildModeController.Instance != null) BuildModeController.Instance.SetMode(true); - else UIKit.Toast("Нажмите кнопку СТРОЙКА"); - }, -1, 96, 32); + // Казармы поставлены, но ещё строятся (05.11: в 3D их было не отличить от готовых) + UIKit.Text(warn, "Казармы строятся. Когда стройка закончится, здесь можно будет нанимать войска.", 28, UIKit.Theme.Text); + barracksBuildingLeft = UIKit.Text(warn, "Осталось: " + GameData.FormatTime(left), 28, UIKit.Theme.Text); + } + else + { + UIKit.Text(warn, "Нанимать войска можно только в Казармах. Постройте их на свободном участке.", 28, UIKit.Theme.Text); + UIKit.Button(warn, "ПОСТРОИТЬ КАЗАРМЫ", UIKit.Theme.Green, () => + { + if (City3DController.Instance != null) City3DController.Instance.PlaceBuilding("Казармы"); // 3D-город + else if (BuildModeController.Instance != null) BuildModeController.Instance.SetMode(true); + else UIKit.Toast("Нажмите кнопку СТРОЙКА"); + }, -1, 96, 32); + } } // --- Очередь обучения --- @@ -249,6 +279,12 @@ public class TroopsPanel : BasePanel protected override void Tick() { + if (Barracks.Count != shownBarracks) { RequestRebuild(); return; } // Казармы достроились (или снесены) + if (barracksBuildingLeft != null) + { + double left = BarracksConstructionSecondsLeft(); + barracksBuildingLeft.text = "Осталось: " + GameData.FormatTime(System.Math.Max(0, left)); + } var training = GP.Training; for (int i = 0; i < queueBars.Count && i < training.Count; i++) { diff --git a/Docs/Modules/Settlement.md b/Docs/Modules/Settlement.md index d502fcb..91f1330 100644 --- a/Docs/Modules/Settlement.md +++ b/Docs/Modules/Settlement.md @@ -119,6 +119,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 05.11.2026 | Claude (Саша) | 3D: здание, которое ещё строится, теперь видно — оно серее, над ним «Стройка 0:45» (`BuildTimer` затемнял только 2D-спрайты, поэтому Казармы выглядели готовыми, а «Войска» писали «казарм: 0»). Окно «Войска»: пока Казармы строятся — «Казармы строятся, осталось …», после стройки окно перерисовывается само; «Построить Казармы» в 3D ставит призрак Казарм (`City3DController.PlaceBuilding`). Кнопки «+ / −» справа — приближение камеры (в Device Simulator колесо мыши не доходит до игры). | | 05.11.2026 | Claude (Саша) | Интеграция 3D: (1) снимок 3D-города для сохранения до расстановки сохранённых зданий отдаёт загруженное состояние — сохранение в первом кадре (поход вернулся офлайн) больше не затирает 3D-город пустым; (2) `ISettlement.TownHallLevel` вне города — уровень Ратуши последнего открытого города (2D или 3D), а не всегда 2D-сохранения: на карте мира предел жилья, резерв и открытие войск считаются по 3D-Ратуше. | | 04.11.2026 | Claude | Решения 04.11: снос убран, остался бесплатный перенос; офлайн по временным меткам — стройка, закончившаяся офлайн, вступает в силу в момент завершения внутри расчёта добычи (`ConstructionClock`, `GameTimeSimulatedEvent`, `BuildTimer.MayComplete`); проверки таймеров из сохранения; уровень при завершении только растёт. | | 01.11.2026 | Claude | Стройка и улучшение по времени (`BuildTimer`), 2 строителя, ускорение за кристаллы, офлайн; лимит зданий по уровню Ратуши; перенос и снос зданий из окна. Сохранение: поля таймера у зданий и Ратуши (старые сохранения читаются). Терем богатыря (`HeroCastle`) и гуляющий герой. Числа — ТЕСТ, `EconomyConfig`. |