- удалены 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>
338 lines
19 KiB
C#
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);
|
|
}
|
|
}
|