300 lines
14 KiB
C#
300 lines
14 KiB
C#
#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>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = 1500; // поверх HUD и окон
|
|
var scaler = go.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920, 1080);
|
|
scaler.matchWidthOrHeight = 0.5f;
|
|
instance = go.AddComponent<DebugPanel>();
|
|
instance.Build();
|
|
}
|
|
|
|
void Build()
|
|
{
|
|
// Панель -- внутри безопасной зоны, чтобы не уходила под вырез камеры
|
|
var safe = new GameObject("SafeArea", typeof(RectTransform)).GetComponent<RectTransform>();
|
|
safe.SetParent(transform, false);
|
|
safe.anchorMin = Vector2.zero;
|
|
safe.anchorMax = Vector2.one;
|
|
safe.offsetMin = safe.offsetMax = Vector2.zero;
|
|
safe.gameObject.AddComponent<SafeArea>();
|
|
|
|
// Кнопка-переключатель слева посередине: там нет кнопок 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<ContentSizeFitter>().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<City3DCamera, float> Get; public System.Action<City3DCamera, float> 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<City3DCamera>();
|
|
|
|
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<ContentSizeFitter>().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($"Население: <b>{p.Total}</b> / {p.HousingCap}\n");
|
|
sb.Append($" жители {p.Civilians}: на работе {p.Workers}, свободно {p.FreeWorkers} из {p.Workforce}; воины {p.Soldiers}\n");
|
|
sb.Append($"Еда в час: <color=#ff8a80>-{p.FoodPerHour:0.#}</color> прирост в час: +{p.GrowthPerHour:0.#}\n");
|
|
if (p.IsStarving) sb.Append("<color=#ff5252><b>ГОЛОД: еды не хватает, прирост остановлен</b></color>\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
|