Feature/3d hud #18

Merged
Alexander merged 59 commits from feature/3d-hud into develop 2026-11-06 18:09:33 +03:00
3 changed files with 133 additions and 5 deletions
Showing only changes of commit 5502489c07 - Show all commits
+91
View File
@@ -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))
{
+41 -5
View File
@@ -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++)
{
+1
View File
@@ -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`. |