/* * @Author: 白哉 * @Description: WikiData 自定义编辑器 * @Date: 2026年04月01日 星期三 11:04:20 * @Modify: */ using System.Collections.Generic; using Logic.Multilingual; using TH1_Logic.Config; using TH1_Logic.MatchConfig; using UI.HintUI; using UnityEditor; using UnityEngine; [CustomEditor(typeof(WikiData))] public class WikiDataEditor : Editor { // ── 状态 ───────────────────────────────────────────────────────────────── private string _searchText = ""; private Vector2 _scrollPos; // 以 ItemId 为 key,避免增删后 index 偏移导致折叠状态错乱 private readonly Dictionary _foldouts = new Dictionary(); private WikiData Target => (WikiData)target; // ── 样式(延迟初始化) ─────────────────────────────────────────────────── private GUIStyle _itemBoxStyle; private GUIStyle _descBoxStyle; private GUIStyle _hintBoxStyle; private GUIStyle _headerLabelStyle; private bool _stylesReady; private void EnsureStyles() { if (_stylesReady) return; _stylesReady = true; _itemBoxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(10, 10, 6, 8), margin = new RectOffset(0, 0, 2, 2), }; _descBoxStyle = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(8, 8, 6, 6), margin = new RectOffset(0, 0, 2, 4), }; // Hint 子面板:比 descBox 多一层视觉缩进 _hintBoxStyle = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(10, 8, 6, 6), margin = new RectOffset(0, 0, 3, 2), }; _headerLabelStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12, }; } // ── 搜索匹配(不区分大小写子串匹配 Name + 所有 GetDesc())────────────── private bool Matches(WikiItem item) { if (string.IsNullOrEmpty(_searchText)) return true; var keyword = _searchText.ToLower(); if (!string.IsNullOrEmpty(item.Name) && item.Name.ToLower().Contains(keyword)) return true; foreach (var d in item.DescItems) { try { // UseHint 时 GetDesc() 走 HintProvider.GetHintText(),需要运行时单例; // 编辑器非运行时会抛异常,catch 后安全跳过 var text = d.GetDesc(); if (!string.IsNullOrEmpty(text) && text.ToLower().Contains(keyword)) return true; } catch { /* 运行时单例未就绪,忽略 */ } } return false; } // ── 主入口 ─────────────────────────────────────────────────────────────── public override void OnInspectorGUI() { MultilingualManager.Instance.SetMultilingualType(MultilingualType.ZH); if (Table.Instance == null) new Table(); EnsureStyles(); // Update / ApplyModifiedProperties 保证 PropertyField(HintProvider 子字段)正确工作 serializedObject.Update(); DrawSearchBar(); EditorGUILayout.Space(6); DrawItemList(); EditorGUILayout.Space(10); DrawAddItemButton(); serializedObject.ApplyModifiedProperties(); } // ── 搜索栏 ─────────────────────────────────────────────────────────────── private void DrawSearchBar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); EditorGUILayout.LabelField("搜索", GUILayout.Width(36)); GUI.SetNextControlName("SearchField"); var newText = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarSearchField); if (newText != _searchText) _searchText = newText; GUI.enabled = !string.IsNullOrEmpty(_searchText); if (GUILayout.Button("清空", EditorStyles.toolbarButton, GUILayout.Width(42))) { _searchText = ""; GUI.FocusControl(null); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); } // ── 词条列表 ───────────────────────────────────────────────────────────── private void DrawItemList() { var items = Target.Items; int total = items.Count; int matched = 0; foreach (var item in items) if (Matches(item)) matched++; string titleText = string.IsNullOrEmpty(_searchText) ? $"词条列表 ({total} 条)" : $"词条列表 (匹配 {matched} / 共 {total} 条)"; EditorGUILayout.LabelField(titleText, _headerLabelStyle); var line = EditorGUILayout.GetControlRect(false, 1f); EditorGUI.DrawRect(line, new Color(0.45f, 0.45f, 0.45f, 0.6f)); EditorGUILayout.Space(4); _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.MaxHeight(520f)); int deleteIndex = -1; for (int i = 0; i < items.Count; i++) { if (!Matches(items[i])) continue; if (DrawItem(items[i], i)) // i = 在 Items 列表中的真实下标 deleteIndex = i; EditorGUILayout.Space(3); } if (total == 0 || (matched == 0 && !string.IsNullOrEmpty(_searchText))) { EditorGUILayout.HelpBox( matched == 0 && !string.IsNullOrEmpty(_searchText) ? "没有找到匹配的词条。" : "暂无词条,点击下方按钮添加。", MessageType.Info); } EditorGUILayout.EndScrollView(); if (deleteIndex >= 0) { Undo.RecordObject(target, "Delete Wiki Item"); Target.Items.RemoveAt(deleteIndex); EditorUtility.SetDirty(target); } } // ── 单条词条 ───────────────────────────────────────────────────────────── /// true = 需要删除 private bool DrawItem(WikiItem item, int listIndex) { if (!_foldouts.ContainsKey(item.Id)) _foldouts[item.Id] = true; bool doDelete = false; EditorGUILayout.BeginVertical(_itemBoxStyle); // —— 标题行 —— EditorGUILayout.BeginHorizontal(); string displayName = string.IsNullOrEmpty(item.Name) ? $"<未命名> [id:{item.Id}]" : $"{item.Name} [id:{item.Id}]"; _foldouts[item.Id] = EditorGUILayout.Foldout( _foldouts[item.Id], displayName, true, EditorStyles.foldoutHeader); GUILayout.FlexibleSpace(); var prevBg = GUI.backgroundColor; GUI.backgroundColor = new Color(1f, 0.42f, 0.42f); if (GUILayout.Button("删 除", GUILayout.Width(54))) { if (EditorUtility.DisplayDialog( "确认删除", $"确定要删除词条「{displayName}」吗?此操作不可撤销。", "删除", "取消")) doDelete = true; } GUI.backgroundColor = prevBg; EditorGUILayout.EndHorizontal(); // —— 折叠内容 —— if (_foldouts[item.Id]) { EditorGUILayout.Space(4); DrawItemName(item); EditorGUILayout.Space(3); DrawItemTypes(item); EditorGUILayout.Space(3); DrawItemDescs(item, listIndex); } EditorGUILayout.EndVertical(); return doDelete; } // ── 名称字段 ───────────────────────────────────────────────────────────── private void DrawItemName(WikiItem item) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("名 称", GUILayout.Width(40)); var newName = EditorGUILayout.TextField(item.Name ?? ""); if (newName != item.Name) { Undo.RecordObject(target, "Edit Wiki Item Name"); item.Name = newName; EditorUtility.SetDirty(target); } EditorGUILayout.EndHorizontal(); } // ── 类型列表 ───────────────────────────────────────────────────────────── private void DrawItemTypes(WikiItem item) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("类 型", GUILayout.Width(40)); int removeIdx = -1; for (int i = 0; i < item.Types.Count; i++) { var newType = (WikiType)EditorGUILayout.EnumPopup(item.Types[i], GUILayout.Width(90)); if (newType != item.Types[i]) { bool duplicate = false; for (int j = 0; j < item.Types.Count; j++) if (j != i && item.Types[j] == newType) { duplicate = true; break; } if (!duplicate) { Undo.RecordObject(target, "Edit Wiki Item Type"); item.Types[i] = newType; EditorUtility.SetDirty(target); } } var prevBg = GUI.backgroundColor; GUI.backgroundColor = new Color(1f, 0.42f, 0.42f); if (GUILayout.Button("x", GUILayout.Width(22))) removeIdx = i; GUI.backgroundColor = prevBg; GUILayout.Space(2); } var firstUnused = FirstUnusedType(item); GUI.enabled = firstUnused.HasValue; var addBg = GUI.backgroundColor; GUI.backgroundColor = firstUnused.HasValue ? new Color(0.48f, 0.85f, 0.48f) : Color.gray; if (GUILayout.Button("+ 添加", GUILayout.Width(60)) && firstUnused.HasValue) { Undo.RecordObject(target, "Add Wiki Item Type"); item.Types.Add(firstUnused.Value); EditorUtility.SetDirty(target); } GUI.backgroundColor = addBg; GUI.enabled = true; EditorGUILayout.EndHorizontal(); if (removeIdx >= 0) { Undo.RecordObject(target, "Remove Wiki Item Type"); item.Types.RemoveAt(removeIdx); EditorUtility.SetDirty(target); } } // ── 描述列表 ───────────────────────────────────────────────────────────── private void DrawItemDescs(WikiItem item, int listIndex) { EditorGUILayout.LabelField("描 述", EditorStyles.boldLabel); // 预先导航到此 item 的 DescItems 数组属性,HintProvider 子字段绘制时需要 var descItemsArrayProp = serializedObject .FindProperty("Items") .GetArrayElementAtIndex(listIndex) .FindPropertyRelative("DescItems"); int removeIdx = -1; for (int i = 0; i < item.DescItems.Count; i++) { var desc = item.DescItems[i]; EditorGUILayout.BeginVertical(_descBoxStyle); // —— 头部行:DescType | 模式切换 | 删除 —— EditorGUILayout.BeginHorizontal(); // DescType(去重校验) var newDescType = (WikiDescType)EditorGUILayout.EnumPopup(desc.DescType, GUILayout.Width(110)); if (newDescType != desc.DescType) { bool duplicate = false; for (int j = 0; j < item.DescItems.Count; j++) if (j != i && item.DescItems[j].DescType == newDescType) { duplicate = true; break; } if (!duplicate) { Undo.RecordObject(target, "Edit Desc Type"); desc.DescType = newDescType; EditorUtility.SetDirty(target); } } GUILayout.Space(6); // 模式切换按钮 var modeBg = GUI.backgroundColor; GUI.backgroundColor = desc.UseHint ? new Color(1f, 0.82f, 0.28f) // 黄:Hint 模式 : new Color(0.55f, 0.85f, 1f); // 蓝:文本模式 if (GUILayout.Button(desc.UseHint ? "Hint 模式" : "文本模式", GUILayout.Width(76))) { Undo.RecordObject(target, "Toggle Desc UseHint"); desc.UseHint = !desc.UseHint; // 切换到 Hint 时确保 HintProvider 已初始化 if (desc.UseHint && desc.HintProvider == null) desc.HintProvider = new HintDataProvider(); EditorUtility.SetDirty(target); } GUI.backgroundColor = modeBg; GUILayout.FlexibleSpace(); var prevBg = GUI.backgroundColor; GUI.backgroundColor = new Color(1f, 0.42f, 0.42f); if (GUILayout.Button("删除", GUILayout.Width(50))) removeIdx = i; GUI.backgroundColor = prevBg; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(3); if (desc.UseHint) { // ── Hint 模式:绘制 HintDataProvider ── if (desc.HintProvider == null) { Undo.RecordObject(target, "Init HintProvider"); desc.HintProvider = new HintDataProvider(); EditorUtility.SetDirty(target); } var hintProp = descItemsArrayProp .GetArrayElementAtIndex(i) .FindPropertyRelative("HintProvider"); DrawHintProvider(desc.HintProvider, hintProp); // 只读预览:显示 GetDesc() 的实际输出 // 非运行时单例未就绪时会抛异常,显示占位文字 EditorGUILayout.Space(2); EditorGUILayout.LabelField("GetDesc() 预览(运行时)", EditorStyles.centeredGreyMiniLabel); string preview; try { preview = desc.GetDesc(); } catch { preview = "(仅运行时可预览,当前单例未就绪)"; } GUI.enabled = false; EditorGUILayout.TextArea(preview, GUILayout.MinHeight(40), GUILayout.ExpandWidth(true)); GUI.enabled = true; } else { // ── 文本模式:直接编辑 Desc 字符串 ── var newDesc = EditorGUILayout.TextArea( desc.Desc ?? "", GUILayout.MinHeight(54), GUILayout.ExpandWidth(true)); if (newDesc != desc.Desc) { Undo.RecordObject(target, "Edit Desc Text"); desc.Desc = newDesc; EditorUtility.SetDirty(target); } } EditorGUILayout.EndVertical(); } if (removeIdx >= 0) { Undo.RecordObject(target, "Remove Desc Item"); item.DescItems.RemoveAt(removeIdx); EditorUtility.SetDirty(target); } // 添加描述按钮 var firstUnusedDesc = FirstUnusedDescType(item); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUI.enabled = firstUnusedDesc.HasValue; var addBg = GUI.backgroundColor; GUI.backgroundColor = firstUnusedDesc.HasValue ? new Color(0.48f, 0.85f, 0.48f) : Color.gray; if (GUILayout.Button("+ 添加描述", GUILayout.Width(100)) && firstUnusedDesc.HasValue) { Undo.RecordObject(target, "Add Desc Item"); item.DescItems.Add(new DescItem { DescType = firstUnusedDesc.Value }); EditorUtility.SetDirty(target); } GUI.backgroundColor = addBg; GUI.enabled = true; EditorGUILayout.EndHorizontal(); } // ── HintDataProvider 编辑器 ─────────────────────────────────────────────── // 简单字段(枚举 / string / bool)直接操作 provider 对象; // 复合类型(UnitFullType、CommonActionId、SkillBase、List)内部结构未知,保留 PropertyField。 private void DrawHintProvider(HintDataProvider provider, SerializedProperty hintProp) { EditorGUILayout.BeginVertical(_hintBoxStyle); // —— HintDataType —— var newHintType = (HintDataType)EditorGUILayout.EnumPopup("Hint 类型", provider.HintDataType); if (newHintType != provider.HintDataType) { Undo.RecordObject(target, "Edit HintDataType"); provider.HintDataType = newHintType; EditorUtility.SetDirty(target); } EditorGUILayout.Space(3); switch (provider.HintDataType) { // SkillDataAssets.GetSkillNameAndDesc_DECODE(SkillTypeData, UnitFullType, SkillParam, ...) case HintDataType.SkillHintData: { var v = (SkillType)EditorGUILayout.EnumPopup("技能类型", provider.SkillTypeData); if (v != provider.SkillTypeData) { Undo.RecordObject(target, "Edit SkillTypeData"); provider.SkillTypeData = v; EditorUtility.SetDirty(target); } EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("UnitFullType"), new GUIContent("单位类型")); break; } // ActionDataAssets.GetActionDesc(ActionIdData, locked, out ret) case HintDataType.ActionHintData: { EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("ActionIdData"), new GUIContent("行动 ID")); var v = EditorGUILayout.Toggle("锁定", provider.locked); if (v != provider.locked) { Undo.RecordObject(target, "Edit locked"); provider.locked = v; EditorUtility.SetDirty(target); } break; } // TechDataAssets.GetTechDesc(TechTypeData, out ret) case HintDataType.TechHintData: { var v = (TechType)EditorGUILayout.EnumPopup("科技类型", provider.TechTypeData); if (v != provider.TechTypeData) { Undo.RecordObject(target, "Edit TechTypeData"); provider.TechTypeData = v; EditorUtility.SetDirty(target); } break; } // 未实现,留空 case HintDataType.CityUpgradeHintData: EditorGUILayout.HelpBox("CityUpgrade 类型暂未实现。", MessageType.Info); break; // UnitTypeDataAssets.GetUnitDesc(UnitFullType, out ret) case HintDataType.UnitHintData: EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("UnitFullType"), new GUIContent("单位类型")); break; // TextDataAssets.GetGiantUpgradeText_DECODE(UnitFullType.GiantType, out ret) case HintDataType.TextDataGiantUpgrate: EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("UnitFullType"), new GUIContent("单位类型(取 GiantType)")); break; // TechDataAssets.GetTechAtomInfo(TechAtom, out info) case HintDataType.TechAtomHintData: EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("TechAtom"), new GUIContent("科技")); break; // TextData.PlayerActionType == FinishHeroTask → 特殊逻辑;否则直接用 Text 静态文本 case HintDataType.TextData: { EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("TextData"), new GUIContent("行动 ID", "PlayerActionType == FinishHeroTask 时走特殊逻辑")); var v = EditorGUILayout.TextArea(provider.Text ?? "", GUILayout.MinHeight(40)); if (v != provider.Text) { Undo.RecordObject(target, "Edit HintProvider Text"); provider.Text = v; EditorUtility.SetDirty(target); } EditorGUILayout.LabelField("↑ 静态文本(其余情况显示,已为 DECODE 格式)", EditorStyles.centeredGreyMiniLabel); break; } // 遍历 GeoIdList,逐条拼接地理名称 + 描述 case HintDataType.GeoData: EditorGUILayout.PropertyField(hintProp.FindPropertyRelative("GeoIdList"), new GUIContent("地理 ID 列表"), true); break; // PlayerTaskDataAssets.GetPlayerTaskData(PlayerTaskType, out data) case HintDataType.PlayerTaskData: { var v = (PlayerTaskType)EditorGUILayout.EnumPopup("玩家任务类型", provider.PlayerTaskType); if (v != provider.PlayerTaskType) { Undo.RecordObject(target, "Edit PlayerTaskType"); provider.PlayerTaskType = v; EditorUtility.SetDirty(target); } break; } } EditorGUILayout.EndVertical(); } // ── 辅助:首个未使用的 WikiType,全部用完则返回 null ──────────────────── private static WikiType? FirstUnusedType(WikiItem item) { foreach (WikiType t in (WikiType[])System.Enum.GetValues(typeof(WikiType))) if (!item.Types.Contains(t)) return t; return null; } // ── 辅助:首个未使用的 WikiDescType,全部用完则返回 null ──────────────── private static WikiDescType? FirstUnusedDescType(WikiItem item) { foreach (WikiDescType dt in (WikiDescType[])System.Enum.GetValues(typeof(WikiDescType))) { bool used = false; foreach (var d in item.DescItems) if (d.DescType == dt) { used = true; break; } if (!used) return dt; } return null; } // ── 底部添加词条按钮 ───────────────────────────────────────────────────── private void DrawAddItemButton() { EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); var prevBg = GUI.backgroundColor; GUI.backgroundColor = new Color(0.38f, 0.72f, 1f); if (GUILayout.Button("+ 添加词条", GUILayout.Height(30), GUILayout.Width(150))) { Undo.RecordObject(target, "Add Wiki Item"); Target.AddWikiItem("新词条"); var newest = Target.Items[^1]; _foldouts[newest.Id] = true; EditorUtility.SetDirty(target); } GUI.backgroundColor = prevBg; GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } }