Files
MVP1/Assets/Editor/HUDPanelsBuilder.cs
T
AlexanderandClaude Opus 5.5 d570fddbc8 Одна карта мира: удалена маленькая карта Meta/World
- удалены WorldSystem, WorldMapPanel, WorldData, WorldLine и окно Panel_World из сцены города
- кнопка «МИР» в городе открывает большую карту (сцена WorldMap) через UIPanelManager.Open("World")
- перенос сохранений: солдаты из походов старой карты возвращаются в город при загрузке
  (отправленные, а если отряд шёл назад — выжившие); добыча таких походов не переносится
- ArmyBridge.TroopsInMarches: панель «Войска» показывает воинов в походах большой карты
- HUDPanelsBuilder: кнопка «МИР» без отдельной панели
- документы World, Army, README обновлены

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:27:34 +02:00

338 lines
19 KiB
C#

using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using UnityEditor.Events;
using UnityEditor.SceneManagement;
using TMPro;
// Кладётся в Assets/Editor/.
// Tools -> Build HUD Panels:
// 1) находит HUD_Canvas,
// 2) создаёт для каждой кнопки свою панель (заголовок + крестик + область Content),
// 3) вешает на панель её скрипт (QuestsPanel, ShopPanel...) -- он сам рисует содержимое в игре,
// 4) вешает UIPanelManager и подключает onClick ВСЕХ кнопок по их именам,
// 5) добавляет красные значки (HUDBadge) на кнопки, где бывает "что забрать".
// Можно запускать повторно -- старые панели пересоздаются.
public class HUDPanelsBuilder
{
class Entry
{
public string button, id, title; public Type panel; public bool badge;
public Entry(string button, string id, string title, Type panel, bool badge)
{ this.button = button; this.id = id; this.title = title; this.panel = panel; this.badge = badge; }
}
// имя кнопки в иерархии -> id панели, заголовок, скрипт панели, нужен ли значок
static readonly Entry[] Map =
{
new Entry("RightBtn_Quests", "Quests", "ЗАДАНИЯ", typeof(QuestsPanel), true),
new Entry("RightBtn_Events", "Events", "СОБЫТИЯ", typeof(EventsPanel), true),
new Entry("RightBtn_Rewards", "Rewards", "НАГРАДЫ", typeof(RewardsPanel), true),
new Entry("NavBtn_Mail", "Mail", "ПОЧТА", typeof(MailPanel), true),
new Entry("SettingsButton", "Settings", "НАСТРОЙКИ", typeof(SettingsPanel), false),
new Entry("PlayerAvatar", "Profile", "ПРОФИЛЬ", typeof(ProfilePanel), false),
new Entry("Nav_Hero", "Hero", "ГЕРОЙ", typeof(HeroPanel), true),
new Entry("Nav_Battle", "Battle", "БОЙ", typeof(BattlePanel), false),
new Entry("Nav_Arena", "Arena", "АРЕНА", typeof(ArenaPanel), true),
new Entry("Nav_Troops", "Troops", "ВОЙСКА", typeof(TroopsPanel), false),
new Entry("Nav_Shop", "Shop", "МАГАЗИН", typeof(ShopPanel), true),
new Entry("Nav_World", "World", null, null, false), // кнопка "МИР" -> сцена большой карты (UIPanelManager.Open)
new Entry("Nav_Home", "Home", null, null, false), // без панели: закрывает всё
};
[MenuItem("Tools/Build HUD Panels")]
public static void Build()
{
GameObject canvasGO = GameObject.Find("HUD_Canvas");
if (canvasGO == null) { Debug.LogError("Не найден объект HUD_Canvas на сцене."); return; }
// казармы: префаб + данные здания + в список стройки
EnsureBarracks();
// менеджер
UIPanelManager manager = canvasGO.GetComponent<UIPanelManager>();
if (manager == null) manager = canvasGO.AddComponent<UIPanelManager>();
manager.panels.Clear();
// корень для панелей (пересоздаём)
Transform old = canvasGO.transform.Find("Panels");
if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject);
GameObject root = new GameObject("Panels", typeof(RectTransform));
root.transform.SetParent(canvasGO.transform, false);
Stretch(root.GetComponent<RectTransform>(), Vector4.zero);
// Корень сам не блокирует клики (нет Image), поэтому нижняя/верхняя панели HUD остаются живыми.
int wired = 0;
string missing = "";
foreach (var m in Map)
{
if (m.title != null)
{
// карте мира нужно больше места -- её окно почти на весь экран между верхней и нижней панелями
Vector4 offsets = m.id == "World" ? new Vector4(16, 145, 16, 195) : new Vector4(30, 250, 30, 230);
GameObject panel = CreatePanel(root.transform, m.id, m.title, manager, offsets);
if (m.panel != null) panel.AddComponent(m.panel);
manager.panels.Add(new UIPanelManager.PanelEntry { id = m.id, panel = panel });
}
Button btn = FindButton(canvasGO.transform, m.button);
if (btn == null && m.id == "World") btn = CreateWorldButton(canvasGO.transform, root.transform);
if (btn == null) { Debug.LogWarning("Кнопка не найдена: " + m.button); missing += " " + m.button; continue; }
// убираем старые подписки из Inspector и вешаем свою
for (int i = btn.onClick.GetPersistentEventCount() - 1; i >= 0; i--)
UnityEventTools.RemovePersistentListener(btn.onClick, i);
UnityEventTools.AddStringPersistentListener(btn.onClick, manager.Open, m.id);
// красный значок "есть что забрать"
HUDBadge badge = btn.GetComponent<HUDBadge>();
if (m.badge)
{
if (badge == null) badge = btn.gameObject.AddComponent<HUDBadge>();
badge.panelId = m.id;
}
else if (badge != null) UnityEngine.Object.DestroyImmediate(badge);
wired++;
}
// Кнопка "СТРОЙКА": включает/выключает режим стройки (тап по участку -> выбор здания).
// Старое меню стройки прячем: при запуске игры оно больше не открывается.
GameObject buildMenu = null;
foreach (var t in canvasGO.GetComponentsInChildren<Transform>(true))
if (t.name == "BuildMenuPanel") { buildMenu = t.gameObject; break; }
if (buildMenu != null) buildMenu.SetActive(false);
BuildModeController buildMode = canvasGO.GetComponent<BuildModeController>();
if (buildMode == null) buildMode = canvasGO.AddComponent<BuildModeController>();
Button buildBtn = FindButton(canvasGO.transform, "Nav_Build");
if (buildBtn == null)
buildBtn = CreateSideButton(canvasGO.transform, root.transform, "Nav_Build", "СТРОЙКА",
"Assets/UI/HUD v2/Рюкзак/v1.png", new Vector2(30, 400), new Color(0.55f, 0.4f, 0.2f));
for (int i = buildBtn.onClick.GetPersistentEventCount() - 1; i >= 0; i--)
UnityEventTools.RemovePersistentListener(buildBtn.onClick, i);
UnityEventTools.AddVoidPersistentListener(buildBtn.onClick, buildMode.ToggleMode);
buildMode.modeButton = buildBtn;
buildMode.legacyBuildMenu = buildMenu;
EditorUtility.SetDirty(buildMode);
// Меню стройки растянуто на весь экран и закрывало все кнопки HUD (ловило клики и белило экран).
// Делаем его фон прозрачным и "сквозным" -- кнопки зданий внутри продолжают работать.
foreach (string see in new[] { "BuildMenuPanel", "ButtonContainer" })
{
foreach (var t in canvasGO.GetComponentsInChildren<Transform>(true))
{
if (t.name != see) continue;
var img = t.GetComponent<Image>();
if (img == null) continue;
var c = img.color; c.a = 0f; img.color = c;
img.raycastTarget = false;
EditorUtility.SetDirty(img);
}
}
EditorUtility.SetDirty(manager);
EditorSceneManager.MarkSceneDirty(canvasGO.scene);
Debug.Log("Готово: панелей " + manager.panels.Count + ", подключено кнопок " + wired + " из " + Map.Length +
(missing.Length > 0 ? ". Не найдены:" + missing : "") + ". Не забудь сохранить сцену (Ctrl+S).");
}
// ===================== КАЗАРМЫ =====================
const string BarracksPrefabPath = "Assets/Prefab/Buildings/Barracks.prefab";
const string BarracksDataPath = "Assets/Prefab/Buildings/Data_Barracks.asset";
[MenuItem("Tools/Create Barracks")]
public static void EnsureBarracks()
{
Sprite icon = LoadSprite("Assets/UI/HUD v2/Войска/v1.png");
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(BarracksPrefabPath);
if (prefab == null)
{
// берём готовое здание как шаблон: тот же масштаб, слой, материал, коллайдер
string template = "Assets/Prefab/Buildings/Food.prefab";
if (AssetDatabase.LoadAssetAtPath<GameObject>(template) == null)
{
var bmT = UnityEngine.Object.FindAnyObjectByType<BuildManager>();
if (bmT != null)
foreach (var b in bmT.allBuildings)
if (b != null && b.prefab != null) { template = AssetDatabase.GetAssetPath(b.prefab); break; }
}
if (!AssetDatabase.CopyAsset(template, BarracksPrefabPath))
{
Debug.LogError("Не удалось создать префаб казарм из " + template);
return;
}
GameObject root = PrefabUtility.LoadPrefabContents(BarracksPrefabPath);
root.name = "Barracks";
foreach (var c in root.GetComponents<ResourceCollector>()) UnityEngine.Object.DestroyImmediate(c);
foreach (var c in root.GetComponents<ResourceProducer>()) UnityEngine.Object.DestroyImmediate(c);
foreach (var c in root.GetComponents<BuildingUI>()) UnityEngine.Object.DestroyImmediate(c);
var kids = new System.Collections.Generic.List<GameObject>();
foreach (Transform ch in root.transform) kids.Add(ch.gameObject); // полоска запаса ресурса -- не нужна
foreach (var k in kids) UnityEngine.Object.DestroyImmediate(k);
var sr = root.GetComponent<SpriteRenderer>();
if (sr != null)
{
if (icon != null)
{
Vector2 oldSize = sr.sprite != null ? (Vector2)sr.sprite.bounds.size : Vector2.one;
sr.sprite = icon;
sr.color = Color.white;
float k = Mathf.Min(oldSize.x / icon.bounds.size.x, oldSize.y / icon.bounds.size.y);
root.transform.localScale = Vector3.Scale(root.transform.localScale, new Vector3(k, k, 1));
}
else sr.color = new Color(1f, 0.6f, 0.6f); // нет иконки -- хотя бы отличаем цветом
var box = root.GetComponent<BoxCollider2D>();
if (box != null && sr.sprite != null) { box.size = sr.sprite.bounds.size; box.offset = sr.sprite.bounds.center; }
}
if (root.GetComponent<Barracks>() == null) root.AddComponent<Barracks>();
PrefabUtility.SaveAsPrefabAsset(root, BarracksPrefabPath);
PrefabUtility.UnloadPrefabContents(root);
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(BarracksPrefabPath);
Debug.Log("Создан префаб казарм: " + BarracksPrefabPath);
}
BuildingData data = AssetDatabase.LoadAssetAtPath<BuildingData>(BarracksDataPath);
if (data == null)
{
data = ScriptableObject.CreateInstance<BuildingData>();
data.buildingName = "Казармы";
data.icon = icon;
data.prefab = prefab;
data.requiredTownHallLevel = 1;
data.cost = new[]
{
new ResourceCost { type = ResourceType.Wood, amount = 300 },
new ResourceCost { type = ResourceType.Stone, amount = 200 },
};
AssetDatabase.CreateAsset(data, BarracksDataPath);
AssetDatabase.SaveAssets();
Debug.Log("Созданы данные здания: " + BarracksDataPath);
}
var bm = UnityEngine.Object.FindAnyObjectByType<BuildManager>();
if (bm != null && !bm.allBuildings.Contains(data))
{
bm.allBuildings.Add(data);
EditorUtility.SetDirty(bm);
EditorSceneManager.MarkSceneDirty(bm.gameObject.scene);
Debug.Log("Казармы добавлены в список стройки (BuildManager)");
}
}
static Sprite LoadSprite(string path)
{
foreach (var a in AssetDatabase.LoadAllAssetsAtPath(path))
if (a is Sprite) return (Sprite)a;
return null;
}
// ищем объект по имени среди всех детей (включая выключенные); Button может быть на нём или внутри
static Button FindButton(Transform root, string name)
{
foreach (var t in root.GetComponentsInChildren<Transform>(true))
{
if (t.name != name) continue;
Button b = t.GetComponent<Button>();
if (b == null) b = t.GetComponentInChildren<Button>(true);
if (b == null) b = t.gameObject.AddComponent<Button>(); // на случай если там просто Image
return b;
}
return null;
}
// Кнопка "МИР" слева над нижней панелью (если дизайнер ещё не сделал свою с именем Nav_World)
static Button CreateWorldButton(Transform canvas, Transform panelsRoot)
{
return CreateSideButton(canvas, panelsRoot, "Nav_World", "МИР", "Assets/UI/HUD v2/Мир/v1.png",
new Vector2(30, 210), new Color(0.2f, 0.45f, 0.3f));
}
static Button CreateSideButton(Transform canvas, Transform panelsRoot, string objName, string label, string spritePath, Vector2 pos, Color fallback)
{
GameObject go = new GameObject(objName, typeof(RectTransform), typeof(Image), typeof(Button));
go.transform.SetParent(canvas, false);
go.transform.SetSiblingIndex(panelsRoot.GetSiblingIndex()); // под панелями, чтобы открытая панель её закрывала
RectTransform rt = go.GetComponent<RectTransform>();
rt.anchorMin = rt.anchorMax = rt.pivot = new Vector2(0, 0);
rt.sizeDelta = new Vector2(150, 150);
rt.anchoredPosition = pos;
var img = go.GetComponent<Image>();
Sprite sprite = null;
foreach (var a in AssetDatabase.LoadAllAssetsAtPath(spritePath))
if (a is Sprite) { sprite = (Sprite)a; break; }
if (sprite != null) { img.sprite = sprite; img.preserveAspect = true; }
else img.color = fallback;
GameObject t = new GameObject("Label", typeof(RectTransform), typeof(TextMeshProUGUI));
t.transform.SetParent(go.transform, false);
var tmp = t.GetComponent<TextMeshProUGUI>();
tmp.text = label; tmp.fontSize = 30; tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center; tmp.color = Color.white; tmp.raycastTarget = false;
tmp.outlineWidth = 0.25f; tmp.outlineColor = Color.black;
RectTransform tr = t.GetComponent<RectTransform>();
tr.anchorMin = new Vector2(0, 0); tr.anchorMax = new Vector2(1, 0); tr.pivot = new Vector2(0.5f, 1);
tr.sizeDelta = new Vector2(0, 40); tr.anchoredPosition = new Vector2(0, 8);
return go.GetComponent<Button>();
}
static GameObject CreatePanel(Transform parent, string id, string title, UIPanelManager manager, Vector4 offsets)
{
// окно оставляет сверху и снизу место под TopPanel и BottomNavBar, чтобы они были кликабельны
GameObject go = new GameObject("Panel_" + id, typeof(RectTransform), typeof(Image));
go.transform.SetParent(parent, false);
go.GetComponent<Image>().color = new Color(0.1f, 0.12f, 0.15f, 0.97f);
Stretch(go.GetComponent<RectTransform>(), offsets); // left, top, right, bottom
// заголовок
GameObject t = new GameObject("Title", typeof(RectTransform), typeof(TextMeshProUGUI));
t.transform.SetParent(go.transform, false);
var tmp = t.GetComponent<TextMeshProUGUI>();
tmp.text = title; tmp.fontSize = 52; tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center; tmp.color = new Color(0.95f, 0.75f, 0.25f);
tmp.raycastTarget = false;
RectTransform tr = t.GetComponent<RectTransform>();
tr.anchorMin = new Vector2(0, 1); tr.anchorMax = new Vector2(1, 1); tr.pivot = new Vector2(0.5f, 1);
tr.sizeDelta = new Vector2(0, 110); tr.anchoredPosition = Vector2.zero;
// область содержимого: сюда скрипт панели рисует списки, кнопки и т.д.
GameObject content = new GameObject("Content", typeof(RectTransform));
content.transform.SetParent(go.transform, false);
Stretch(content.GetComponent<RectTransform>(), new Vector4(20, 120, 20, 20));
// крестик
GameObject c = new GameObject("CloseButton", typeof(RectTransform), typeof(Image), typeof(Button));
c.transform.SetParent(go.transform, false);
c.GetComponent<Image>().color = new Color(0.6f, 0.15f, 0.15f);
RectTransform cr = c.GetComponent<RectTransform>();
cr.anchorMin = cr.anchorMax = cr.pivot = new Vector2(1, 1);
cr.sizeDelta = new Vector2(90, 90); cr.anchoredPosition = new Vector2(-12, -10);
GameObject x = new GameObject("X", typeof(RectTransform), typeof(TextMeshProUGUI));
x.transform.SetParent(c.transform, false);
var xt = x.GetComponent<TextMeshProUGUI>();
xt.text = "X"; xt.fontSize = 44; xt.alignment = TextAlignmentOptions.Center; xt.color = Color.white;
xt.raycastTarget = false;
Stretch(x.GetComponent<RectTransform>(), Vector4.zero);
UnityEventTools.AddVoidPersistentListener(c.GetComponent<Button>().onClick, manager.CloseAll);
go.SetActive(false);
return go;
}
// left, top, right, bottom -- отступы от краёв родителя
static void Stretch(RectTransform rt, Vector4 ltrb)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = new Vector2(ltrb.x, ltrb.w);
rt.offsetMax = new Vector2(-ltrb.z, -ltrb.y);
}
}