310 lines
18 KiB
C#
310 lines
18 KiB
C#
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 → IEquipmentService(EquipmentManager);变更经 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("运行期读 IEquipmentService(EquipmentManager);重跑「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);
|
||
|
||
// 卡片模板(CharmCardView,inactive)
|
||
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; }
|
||
}
|
||
}
|
||
}
|