Feature/3d hud #18
@@ -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<City3DCamera>() ?? cam.gameObject.AddComponent<City3DCamera>();
|
||||
@@ -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<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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -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<PlacedBuildingId>();
|
||||
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++)
|
||||
{
|
||||
|
||||
@@ -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`. |
|
||||
|
||||
Reference in New Issue
Block a user