#if UNITY_EDITOR || DEVELOPMENT_BUILD using TMPro; using UnityEngine; using UnityEngine.UI; // ОТЛАДОЧНАЯ ПАНЕЛЬ для проверки баланса: кнопка "DEV" слева на экране. // // Есть только в редакторе и в Development Build -- в релизную сборку этот файл не попадает совсем. // "+1 час" не имеет своей логики: он двигает GameClock, и модули досчитывают время по своим обычным правилам, // ровно как при возвращении игрока в игру. Поэтому проверяешь настоящие правила, а не кнопку. // // Перемотка действует на то, что берёт время из GameClock (сейчас -- Население и походы). Обучение // войск пока идёт по реальным часам. public class DebugPanel : MonoBehaviour { static DebugPanel instance; GameObject panel; TextMeshProUGUI info; UnityEngine.UI.Button gridButton; float nextRefresh; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Create() { if (instance != null) return; var go = new GameObject("DebugPanel", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); DontDestroyOnLoad(go); var canvas = go.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 1500; // поверх HUD и окон var scaler = go.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.matchWidthOrHeight = 0.5f; instance = go.AddComponent(); instance.Build(); } void Build() { // Панель -- внутри безопасной зоны, чтобы не уходила под вырез камеры var safe = new GameObject("SafeArea", typeof(RectTransform)).GetComponent(); safe.SetParent(transform, false); safe.anchorMin = Vector2.zero; safe.anchorMax = Vector2.one; safe.offsetMin = safe.offsetMax = Vector2.zero; safe.gameObject.AddComponent(); // Кнопка-переключатель слева посередине: там нет кнопок HUD var toggle = UIKit.Button(transform, "DEV", new Color(0.8f, 0.2f, 0.8f, 1f), Toggle, 110, 64, 28); var trt = (RectTransform)toggle.transform; trt.anchorMin = trt.anchorMax = new Vector2(0, 0.5f); trt.pivot = new Vector2(0, 0.5f); trt.anchoredPosition = new Vector2(12, 0); trt.sizeDelta = new Vector2(110, 64); var card = UIKit.Card(safe, new Color(0.08f, 0.08f, 0.11f, 0.95f), 20, 10); card.anchorMin = card.anchorMax = new Vector2(0, 0.5f); card.pivot = new Vector2(0, 0.5f); card.anchoredPosition = new Vector2(135, 0); card.sizeDelta = new Vector2(620, 0); card.gameObject.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; panel = card.gameObject; var title = UIKit.Text(card, "ОТЛАДКА БАЛАНСА", 30, UIKit.Theme.Gold, TextAlignmentOptions.Center); title.fontStyle = FontStyles.Bold; info = UIKit.Text(card, "", 24, UIKit.Theme.Text); var row1 = UIKit.Row(card, 70, 10); Btn(row1, "+1 час", UIKit.Theme.Blue, () => GameClock.Advance(3600)); Btn(row1, "+10 часов", UIKit.Theme.Blue, () => GameClock.Advance(36000)); var row2 = UIKit.Row(card, 70, 10); Btn(row2, "+1000 всех ресурсов", UIKit.Theme.Green, AddAllResources); Btn(row2, "+1000 еды", UIKit.Theme.Green, () => AddResource(ResourceType.Food, 1000)); Btn(row2, "Кристаллы = макс", UIKit.Theme.Blue, FillGems); var row3 = UIKit.Row(card, 70, 10); Btn(row3, "Еда = 0", UIKit.Theme.Red, ZeroFood); Btn(row3, "Сбросить перемотку", UIKit.Theme.Gray, () => { GameClock.ResetOffset(); Refresh(); }); var row4 = UIKit.Row(card, 70, 10); Btn(row4, "Рабочие", UIKit.Theme.Blue, () => BuildingWindow.Open(null)); gridButton = UIKit.Button(row4, GridLabel, UIKit.Theme.Blue, ToggleGrid, -1, 70, 24); UIKit.Size(gridButton, -1, 70, 1); Btn(row4, "Камера", UIKit.Theme.Blue, ToggleCameraCard); Btn(row4, "Закрыть", UIKit.Theme.Gray, () => { panel.SetActive(false); if (cameraCard != null) cameraCard.SetActive(false); }); panel.SetActive(false); } // ---------------- Камера 3D-поселения (решение 05.11: перспектива, числа подбираем в Unity и на телефоне) ---------------- GameObject cameraCard; TextMeshProUGUI cameraInfo; // Ползунок: подпись, значение, диапазон. get/set -- поле камеры struct CamSlider { public string Label, Format; public float Min, Max; public System.Func Get; public System.Action Set; public string Pref; } static readonly CamSlider[] CamSliders = { new CamSlider { Label = "Наклон", Format = "0°", Min = 25, Max = 85, Pref = City3DCamera.PrefPitch, Get = c => c.pitch, Set = (c, v) => c.pitch = v }, new CamSlider { Label = "Перспектива", Format = "0°", Min = 15, Max = 75, Pref = City3DCamera.PrefFov, Get = c => c.fieldOfView, Set = (c, v) => c.SetFieldOfView(v) }, new CamSlider { Label = "Ближе всего", Format = "0", Min = 3, Max = 30, Pref = City3DCamera.PrefMin, Get = c => c.minDistance, Set = (c, v) => { c.minDistance = Mathf.Min(v, c.maxDistance - 1); c.distance = Mathf.Max(c.distance, c.minDistance); } }, new CamSlider { Label = "Дальше всего", Format = "0", Min = 20, Max = 120, Pref = City3DCamera.PrefMax, Get = c => c.maxDistance, Set = (c, v) => { c.maxDistance = Mathf.Max(v, c.minDistance + 1); c.distance = Mathf.Min(c.distance, c.maxDistance); } }, new CamSlider { Label = "Сейчас", Format = "0", Min = 3, Max = 120, Pref = null, Get = c => c.distance, Set = (c, v) => c.distance = Mathf.Clamp(v, c.minDistance, c.maxDistance) }, }; static City3DCamera Cam3D => Object.FindAnyObjectByType(); void ToggleCameraCard() { if (cameraCard != null) { bool show = !cameraCard.activeSelf; if (show) BuildCameraCard(); else cameraCard.SetActive(false); return; } BuildCameraCard(); } void BuildCameraCard() { if (cameraCard != null) Destroy(cameraCard); var cam = Cam3D; var safe = panel.transform.parent; var card = UIKit.Card(safe, new Color(0.08f, 0.08f, 0.11f, 0.95f), 20, 10); card.anchorMin = card.anchorMax = new Vector2(0, 0.5f); card.pivot = new Vector2(0, 0.5f); card.anchoredPosition = new Vector2(135 + 640, 0); card.sizeDelta = new Vector2(640, 0); card.gameObject.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; cameraCard = card.gameObject; var title = UIKit.Text(card, "КАМЕРА 3D-ПОСЕЛЕНИЯ (ТЕСТ)", 28, UIKit.Theme.Gold, TextAlignmentOptions.Center); title.fontStyle = FontStyles.Bold; if (cam == null) { UIKit.Text(card, "Откройте 3D-город (сцена City3D)", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); } else { foreach (var def in CamSliders) { var d = def; var row = UIKit.Row(card, 60, 12); var label = UIKit.Text(row, d.Label, 24); UIKit.Size(label, 170); var value = UIKit.Text(row, d.Get(cam).ToString(d.Format), 24, UIKit.Theme.TextDim, TextAlignmentOptions.Right); UIKit.Slider(row, Mathf.InverseLerp(d.Min, d.Max, d.Get(cam)), t => { var c = Cam3D; if (c == null) return; d.Set(c, Mathf.Lerp(d.Min, d.Max, t)); value.text = d.Get(c).ToString(d.Format); if (d.Pref != null) PlayerPrefs.SetFloat(d.Pref, d.Get(c)); // запомнить на этом устройстве c.Apply(); UpdateCameraInfo(); }); value.transform.SetAsLastSibling(); UIKit.Size(value, 80); } } cameraInfo = UIKit.Text(card, "", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); UpdateCameraInfo(); var row2 = UIKit.Row(card, 64, 10); Btn(row2, "Сбросить", UIKit.Theme.Gray, ResetCamera); Btn(row2, "Закрыть", UIKit.Theme.Gray, () => cameraCard.SetActive(false)); } // Строка, которую удобно переписать в отчёт / в сцену void UpdateCameraInfo() { var c = Cam3D; if (cameraInfo == null) return; cameraInfo.text = c == null ? "" : $"pitch {c.pitch:0} • fieldOfView {c.fieldOfView:0} • minDistance {c.minDistance:0} • maxDistance {c.maxDistance:0} • distance {c.distance:0}\n" + "Подобранное запоминается на этом устройстве. В сцену (для всех) -- вписать числа в Main Camera → City3DCamera"; } void ResetCamera() { foreach (var d in CamSliders) if (d.Pref != null) PlayerPrefs.DeleteKey(d.Pref); var c = Cam3D; if (c != null) { c.pitch = c.DefaultPitch; c.SetFieldOfView(c.DefaultFov); c.minDistance = c.DefaultMin; c.maxDistance = c.DefaultMax; c.distance = Mathf.Clamp(c.distance, c.minDistance, c.maxDistance); c.Apply(); } BuildCameraCard(); } // Отладочная сетка стройки (3D-поселение и 2D-город): видна всегда, ярче, каждая 5-я линия жёлтая (05.11, просьба Миши) void ToggleGrid() { bool on = !(City3DController.DebugGrid || BuildGrid.DebugVisible); City3DController.SetDebugGrid(on); bool building = BuildModeController.Instance != null && BuildModeController.Instance.IsActive; BuildGrid.SetDebugVisible(on, building); if (gridButton != null) UIKit.SetButton(gridButton, GridLabel, true); } static string GridLabel => City3DController.DebugGrid || BuildGrid.DebugVisible ? "Сетка: вкл" : "Сетка: выкл"; static void Btn(Transform row, string label, Color color, System.Action onClick) { var b = UIKit.Button(row, label, color, onClick, -1, 70, 24); UIKit.Size(b, -1, 70, 1); } void Toggle() { panel.SetActive(!panel.activeSelf); if (panel.activeSelf) Refresh(); } void Update() { if (panel == null || !panel.activeSelf || Time.unscaledTime < nextRefresh) return; nextRefresh = Time.unscaledTime + 0.5f; Refresh(); } void Refresh() { if (info == null) return; var sb = new System.Text.StringBuilder(); if (GameServices.TryGet(out IPopulation p)) { sb.Append($"Население: {p.Total} / {p.HousingCap}\n"); sb.Append($" жители {p.Civilians}: на работе {p.Workers}, свободно {p.FreeWorkers} из {p.Workforce}; воины {p.Soldiers}\n"); sb.Append($"Еда в час: -{p.FoodPerHour:0.#} прирост в час: +{p.GrowthPerHour:0.#}\n"); if (p.IsStarving) sb.Append("ГОЛОД: еды не хватает, прирост остановлен\n"); } else sb.Append("Модуль Населения не зарегистрирован\n"); if (GameServices.TryGet(out IProduction prod)) sb.Append($"Добыча в час: еда +{prod.RatePerHour(ResourceType.Food):0.#}, дерево +{prod.RatePerHour(ResourceType.Wood):0.#}, " + $"камень +{prod.RatePerHour(ResourceType.Stone):0.#}, металл +{prod.RatePerHour(ResourceType.Iron):0.#}, золото +{prod.RatePerHour(ResourceType.Gold):0.#}\n"); if (GameServices.TryGet(out IEconomy e)) sb.Append($"Склад: еда {e.Get(ResourceType.Food):0}/{Cap(e, ResourceType.Food)} дерево {e.Get(ResourceType.Wood):0}/{Cap(e, ResourceType.Wood)} " + $"камень {e.Get(ResourceType.Stone):0}/{Cap(e, ResourceType.Stone)}\n"); else sb.Append("Экономика не найдена (нет ResourceManager на сцене)\n"); long off = GameClock.OffsetSeconds; sb.Append(off > 0 ? $"Перемотано: +{off / 3600f:0.#} ч" : "Время: реальное"); info.text = sb.ToString(); } static string Cap(IEconomy e, ResourceType t) { float c = e.GetCapacity(t); return float.IsInfinity(c) ? "∞" : c.ToString("0"); } void AddAllResources() { ResourceType[] types = { ResourceType.Wood, ResourceType.Food, ResourceType.Stone, ResourceType.Iron, ResourceType.Gold, ResourceType.Gems }; foreach (var t in types) AddResource(t, 1000); // лишнее обрежет лимит склада } // Кристаллы до предела склада (лимит 50 -- решение 19.10, предел не меняется) void FillGems() { var rm = ResourceManager.Instance; if (rm == null) { UIKit.Toast("Нет склада (ResourceManager)"); return; } float cap = rm.GetCapacity(ResourceType.Gems); if (float.IsInfinity(cap)) cap = EconomyConfig.Load().storage.crystalCap; rm.SetResource(ResourceType.Gems, cap); Refresh(); } void AddResource(ResourceType type, float amount) { if (GameServices.TryGet(out IEconomy e)) e.Add(type, amount); Refresh(); } void ZeroFood() { if (GameServices.TryGet(out IEconomy e)) e.Add(ResourceType.Food, -e.Get(ResourceType.Food)); Refresh(); } void OnDestroy() { if (instance == this) instance = null; } } #endif