Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UICharmScaffold.cs
2026-06-08 11:26:17 +08:00

310 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.UI;
using BaseGames.UI.Theme;
using BaseGames.Core.Events;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 护符面板脚手架:生成 <c>UI_CharmPanel</c> 预制件CharmEquipPanel + 已装备区 + 收藏目录 + 凹槽进度条 + 卡片模板)。
/// 数据来源运行期 ServiceLocator → IEquipmentServiceEquipmentManager变更经 EVT_EquipmentChanged 反应式重建。
///
/// 该面板既可 standalone 打开PanelId.CharmPanel也由 InventoryHub 内嵌为「护符」Tab脚手架内嵌时中和 Canvas 链 + 隐藏 chrome
/// 美术 → 改 UI_CharmPanel / 卡片样式;策划 → 改护符数据(CharmSO/CharmCatalogSO)。
/// 菜单BaseGames/UI/控件库/生成护符面板(预制件)
/// </summary>
public static class UICharmScaffold
{
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
private const string PrefabName = "UI_CharmPanel";
[MenuItem("BaseGames/UI/控件库/生成护符面板(预制件)")]
public static void GeneratePrefab()
{
EnsureFolder(PrefabDir);
var report = new List<string>();
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
SeedLocalization(report);
BuildPrefab(theme, report);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UICharm] 护符面板已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("运行期读 IEquipmentServiceEquipmentManager重跑「Scaffold Inventory Hub」把它收编为护符 Tab。");
Debug.Log(sb.ToString());
}
private static void SeedLocalization(List<string> report)
{
var zh = new Dictionary<string, string>
{
{ "MENU_CHARM", "护符" }, { "CHARM_TITLE", "护符" },
{ "CHARM_EQUIPPED", "已装备" }, { "CHARM_CATALOG", "收藏" }, { "CHARM_NOTCHES", "凹槽" },
};
var en = new Dictionary<string, string>
{
{ "MENU_CHARM", "Charms" }, { "CHARM_TITLE", "Charms" },
{ "CHARM_EQUIPPED", "Equipped" }, { "CHARM_CATALOG", "Collected" }, { "CHARM_NOTCHES", "Notches" },
};
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
report.Add($"本地化新增 {added} 条(已存在不覆盖)。");
}
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
{
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
int added = 0;
foreach (var p in kv)
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
return added;
}
// ── 预制件 ───────────────────────────────────────────────────────────
private static void BuildPrefab(UIThemeSO theme, List<string> report)
{
var root = new GameObject(PrefabName,
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
Stretch((RectTransform)root.transform);
SetupCanvas(root, 61);
var panel = root.AddComponent<CharmEquipPanel>();
var applier = root.AddComponent<UIThemeApplier>();
if (theme != null) AssignRef(applier, "_theme", theme);
// 全屏遮罩chrome内嵌时隐藏
var overlayGo = NewUIChild(root.transform, "Overlay", out var overlayRt);
Stretch(overlayRt);
var overlay = overlayGo.AddComponent<Image>(); overlay.color = new Color(0f, 0f, 0f, 0.6f);
// 主框
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
SetRect(boxRt, C(0.5f), C(0.5f), C(0.5f), Vector2.zero, new Vector2(1280f, 720f));
var box = boxGo.AddComponent<Image>();
box.sprite = Std(); box.type = Image.Type.Sliced; box.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
SetEnum(boxGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
// 标题
var titleGo = NewUIChild(boxGo.transform, "Title", out var titleRt);
SetRect(titleRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -50f), new Vector2(600f, 60f));
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
titleGo.AddComponent<LocalizedText>();
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "CHARM_TITLE");
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
// 凹槽进度条(已用/总数)
var notchGo = NewUIChild(boxGo.transform, "NotchBar", out var notchRt);
SetRect(notchRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -110f), new Vector2(420f, 34f));
var notchBg = notchGo.AddComponent<Image>(); notchBg.color = new Color(1f, 1f, 1f, 0.08f);
notchBg.sprite = Std(); notchBg.type = Image.Type.Sliced;
var fillGo = NewUIChild(notchGo.transform, "Fill", out var fillRt);
Stretch(fillRt);
var notchFill = fillGo.AddComponent<Image>();
notchFill.color = new Color(0.85f, 0.80f, 0.55f, 0.85f);
notchFill.sprite = Std(); notchFill.type = Image.Type.Filled;
notchFill.fillMethod = Image.FillMethod.Horizontal; notchFill.fillOrigin = 0; notchFill.fillAmount = 0f;
var notchTextGo = NewUIChild(notchGo.transform, "NotchText", out var notchTextRt);
Stretch(notchTextRt);
var notchText = notchTextGo.AddComponent<TextMeshProUGUI>();
notchText.alignment = TextAlignmentOptions.Center; notchText.fontSize = 20f; notchText.raycastTarget = false;
SetEnum(notchTextGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
// 已装备区(左)
var equippedLabel = MakeSectionLabel(boxGo.transform, "EquippedLabel", "CHARM_EQUIPPED",
C(0f, 1f), new Vector2(60f, -160f), new Vector2(420f, 34f));
var equippedGo = NewUIChild(boxGo.transform, "EquippedContainer", out var equippedRt);
SetRect(equippedRt, C(0f, 1f), C(0f, 1f), C(0f, 1f), new Vector2(40f, -200f), new Vector2(440f, 460f));
var eqGrid = equippedGo.AddComponent<GridLayoutGroup>();
eqGrid.cellSize = new Vector2(200f, 96f); eqGrid.spacing = new Vector2(8f, 8f);
eqGrid.childAlignment = TextAnchor.UpperLeft; eqGrid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
eqGrid.constraintCount = 2;
// 收藏目录(右,滚动)
var catalogLabel = MakeSectionLabel(boxGo.transform, "CatalogLabel", "CHARM_CATALOG",
C(1f, 1f), new Vector2(-490f, -160f), new Vector2(440f, 34f));
var scrollGo = NewUIChild(boxGo.transform, "CatalogScroll", out var scrollRt);
SetRect(scrollRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-40f, -200f), new Vector2(680f, 460f));
var scrollImg = scrollGo.AddComponent<Image>(); scrollImg.color = new Color(1f, 1f, 1f, 0.03f);
var scroll = scrollGo.AddComponent<ScrollRect>();
scroll.horizontal = false; scroll.vertical = true;
var viewportGo = NewUIChild(scrollGo.transform, "Viewport", out var viewportRt);
Stretch(viewportRt);
var vpImg = viewportGo.AddComponent<Image>(); vpImg.color = new Color(1f, 1f, 1f, 0.01f);
viewportGo.AddComponent<RectMask2D>();
var catalogGo = NewUIChild(viewportGo.transform, "CatalogContainer", out var catalogRt);
SetRect(catalogRt, C(0f, 1f), C(1f, 1f), C(0.5f, 1f), Vector2.zero, new Vector2(0f, 0f));
var catGrid = catalogGo.AddComponent<GridLayoutGroup>();
catGrid.cellSize = new Vector2(200f, 96f); catGrid.spacing = new Vector2(8f, 8f);
catGrid.childAlignment = TextAnchor.UpperLeft; catGrid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
catGrid.constraintCount = 3; catGrid.padding = new RectOffset(8, 8, 8, 8);
var catFitter = catalogGo.AddComponent<ContentSizeFitter>();
catFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
AssignRef(scroll, "m_Content", catalogRt);
AssignRef(scroll, "m_Viewport", viewportRt);
// 卡片模板CharmCardViewinactive
var cardGo = BuildCardTemplate(boxGo.transform);
// 关闭按钮chrome内嵌时隐藏
var closeGo = NewUIChild(boxGo.transform, "Btn_Close", out var closeRt);
SetRect(closeRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 36f), new Vector2(240f, 56f));
var closeImg = closeGo.AddComponent<Image>(); closeImg.sprite = Std(); closeImg.type = Image.Type.Sliced;
closeImg.color = new Color(1f, 1f, 1f, 0.06f);
var closeBtn = closeGo.AddComponent<Button>(); closeBtn.targetGraphic = closeImg;
var closeLblGo = NewUIChild(closeGo.transform, "Label", out var closeLblRt);
Stretch(closeLblRt);
var closeLbl = closeLblGo.AddComponent<TextMeshProUGUI>();
closeLbl.alignment = TextAlignmentOptions.Center; closeLbl.fontSize = 26f;
closeLblGo.AddComponent<LocalizedText>();
SetString(closeLblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
SetEnum(closeLblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
// 绑定 CharmEquipPanel
AssignRef(panel, "_notchText", notchText);
AssignRef(panel, "_notchBarFill", notchFill);
AssignRef(panel, "_equippedContainer", equippedGo.transform);
AssignRef(panel, "_catalogContainer", catalogGo.transform);
AssignRef(panel, "_charmCardTemplate", cardGo);
AssignRef(panel, "_btnClose", closeBtn);
AssignRef(panel, "_onEquipmentChanged", FindAsset<VoidEventChannelSO>("EVT_EquipmentChanged"));
if (FindAsset<VoidEventChannelSO>("EVT_EquipmentChanged") == null)
report.Add("EVT_EquipmentChanged 未找到_onEquipmentChanged 留空(运行 BaseGames ▸ Events ▸ Create Event Channels。");
root.SetActive(false);
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PrefabName}.prefab");
Object.DestroyImmediate(root);
report.Add($"{PrefabDir}/{PrefabName}.prefab");
}
// 护符卡片模板CharmCardView + icon/name/notchCost/desc/badge/button整卡为按钮
private static GameObject BuildCardTemplate(Transform parent)
{
var cardGo = NewUIChild(parent, "CharmCardTemplate", out var cardRt);
cardRt.sizeDelta = new Vector2(200f, 96f);
var bg = cardGo.AddComponent<Image>(); bg.color = new Color(1f, 1f, 1f, 0.05f);
bg.sprite = Std(); bg.type = Image.Type.Sliced;
var btn = cardGo.AddComponent<Button>(); btn.targetGraphic = bg;
var view = cardGo.AddComponent<CharmCardView>();
var iconGo = NewUIChild(cardGo.transform, "Icon", out var iconRt);
SetRect(iconRt, C(0f, 0.5f), C(0f, 0.5f), C(0f, 0.5f), new Vector2(12f, 0f), new Vector2(64f, 64f));
var icon = iconGo.AddComponent<Image>(); icon.preserveAspect = true; icon.raycastTarget = false;
var nameGo = NewUIChild(cardGo.transform, "Name", out var nameRt);
SetRect(nameRt, C(0f, 1f), C(1f, 1f), C(0f, 1f), new Vector2(88f, -10f), new Vector2(-96f, 28f));
var nameTxt = nameGo.AddComponent<TextMeshProUGUI>();
nameTxt.fontSize = 20f; nameTxt.raycastTarget = false; nameTxt.alignment = TextAlignmentOptions.TopLeft;
SetEnum(nameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
var costGo = NewUIChild(cardGo.transform, "NotchCost", out var costRt);
SetRect(costRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-10f, -10f), new Vector2(40f, 28f));
var costTxt = costGo.AddComponent<TextMeshProUGUI>();
costTxt.fontSize = 18f; costTxt.alignment = TextAlignmentOptions.TopRight; costTxt.raycastTarget = false;
SetEnum(costGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
var descGo = NewUIChild(cardGo.transform, "Desc", out var descRt);
SetRect(descRt, C(0f, 0f), C(1f, 0f), C(0f, 0f), new Vector2(88f, 8f), new Vector2(-96f, 44f));
var descTxt = descGo.AddComponent<TextMeshProUGUI>();
descTxt.fontSize = 15f; descTxt.alignment = TextAlignmentOptions.TopLeft; descTxt.raycastTarget = false;
descTxt.enableWordWrapping = true;
SetEnum(descGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
var badgeGo = NewUIChild(cardGo.transform, "Badge", out var badgeRt);
SetRect(badgeRt, C(1f, 0f), C(1f, 0f), C(1f, 0f), new Vector2(-12f, 10f), new Vector2(28f, 28f));
var badgeTxt = badgeGo.AddComponent<TextMeshProUGUI>();
badgeTxt.fontSize = 22f; badgeTxt.alignment = TextAlignmentOptions.Center; badgeTxt.raycastTarget = false;
SetEnum(badgeGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Accent);
// 绑定 CharmCardView 字段
AssignRef(view, "_icon", icon);
AssignRef(view, "_iconRoot", iconGo);
AssignRef(view, "_nameText", nameTxt);
AssignRef(view, "_notchCostText", costTxt);
AssignRef(view, "_descriptionText", descTxt);
AssignRef(view, "_equipBadgeText", badgeTxt);
AssignRef(view, "_actionButton", btn);
cardGo.SetActive(false);
return cardGo;
}
private static TMP_Text MakeSectionLabel(Transform parent, string name, string locKey,
Vector2 anchor, Vector2 pos, Vector2 size)
{
var go = NewUIChild(parent, name, out var rt);
SetRect(rt, anchor, anchor, anchor, pos, size);
var t = go.AddComponent<TextMeshProUGUI>();
t.fontSize = 24f; t.alignment = TextAlignmentOptions.Left; t.raycastTarget = false;
go.AddComponent<LocalizedText>();
SetString(go.GetComponent<LocalizedText>(), "_key", locKey);
SetEnum(go.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
return t;
}
// ── 助手(对照 UIFormSkillScaffold────────────────────────────────────
private static Vector2 C(float x = 0.5f, float y = 0.5f) => new Vector2(x, y);
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
{
var go = new GameObject(name, typeof(RectTransform));
rt = (RectTransform)go.transform; rt.SetParent(parent, false);
return go;
}
private static void Stretch(RectTransform rt)
{ rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; }
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
{ rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot; rt.anchoredPosition = pos; rt.sizeDelta = size; }
private static void SetEnum(Component c, string prop, int value)
{ var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static void SetString(Component c, string prop, string value)
{ if (c == null) return; var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static void AssignRef(Object target, string prop, Object value)
{ var so = new SerializedObject(target); var p = so.FindProperty(prop); if (p != null) { p.objectReferenceValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static T FindAsset<T>(string name) where T : Object
{
foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}"))
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var a = AssetDatabase.LoadAssetAtPath<T>(path);
if (a != null && a.name == name) return a;
}
return null;
}
private static void SetupCanvas(GameObject go, int sortOrder)
{
var canvas = go.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = sortOrder;
var scaler = go.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
}
private static Sprite Std() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
private static void EnsureFolder(string dir)
{
string[] parts = dir.Split('/'); string cur = parts[0];
for (int i = 1; i < parts.Length; i++) { string n = $"{cur}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(n)) AssetDatabase.CreateFolder(cur, parts[i]); cur = n; }
}
}
}