Design_SubModule/Scripts/Editor/Design_Tools/GuideConfigEditor.cs
2026-05-27 18:40:34 +08:00

545 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
/// <summary>
/// GuideConfig.txt 可视化编辑器
/// 菜单路径: 程序工具/策划工具/引导配置编辑器
/// </summary>
public class GuideConfigEditor : EditorWindow
{
#region
[Serializable]
public class GuideConditions
{
public List<string> Satisfy = new List<string>();
public int CheckAgain;
public List<string> Trigger = new List<string>();
public string End = "";
public float Delay;
public int Limit;
}
[Serializable]
public class GuideShowUI
{
public string LangText = "";
public string RoleExpression = "";
public string DialogPos = "";
public int OkBtn;
public int ClickSg;
public string ClickPos = "";
public string FocusPos = "";
public int Phone;
public int ClickToSkip;
}
[Serializable]
public class GuideEntry
{
public int Id;
public string Name = "";
public int Priority;
public GuideConditions Conditions = new GuideConditions();
public GuideShowUI ShowUI = new GuideShowUI();
}
#endregion
#region
private static readonly string ConfigPath = "Assets/Scripts/Preload/GuideConfig.txt";
private List<GuideEntry> _entries = new List<GuideEntry>();
private Vector2 _listScroll;
private Vector2 _detailScroll;
private int _selectedIndex = -1;
private string _searchText = "";
private string _searchId = "";
private bool _isDirty;
private bool _showConditions = true;
private bool _showShowUI = true;
private bool _showSatisfy = true;
private bool _showTrigger = true;
// 列表排序
private enum SortMode { Id, Priority, Name }
private SortMode _sortMode = SortMode.Id;
// 样式缓存
private GUIStyle _headerStyle;
private GUIStyle _selectedStyle;
private GUIStyle _entryStyle;
private bool _stylesInited;
#endregion
[MenuItem("策划工具/引导配置编辑器")]
public static void ShowWindow()
{
var win = GetWindow<GuideConfigEditor>("引导配置编辑器");
win.minSize = new Vector2(900, 500);
win.LoadConfig();
}
private void OnEnable()
{
LoadConfig();
}
private void InitStyles()
{
if (_stylesInited) return;
_headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 13 };
_selectedStyle = new GUIStyle("TV Selection");
_entryStyle = new GUIStyle(EditorStyles.label) { richText = true };
_stylesInited = true;
}
#region /
private void LoadConfig()
{
string fullPath = Path.Combine(Application.dataPath, "..", ConfigPath);
if (!File.Exists(fullPath))
{
Debug.LogError($"[GuideConfigEditor] 文件不存在: {fullPath}");
return;
}
string json = File.ReadAllText(fullPath);
_entries = ParseJsonArray(json);
_isDirty = false;
_selectedIndex = -1;
Debug.Log($"[GuideConfigEditor] 已加载 {_entries.Count} 条引导配置");
}
private void SaveConfig()
{
string fullPath = Path.Combine(Application.dataPath, "..", ConfigPath);
string json = SerializeToJson(_entries);
File.WriteAllText(fullPath, json);
_isDirty = false;
AssetDatabase.Refresh();
Debug.Log($"[GuideConfigEditor] 已保存 {_entries.Count} 条引导配置到 {ConfigPath}");
}
#endregion
#region GUI
private void OnGUI()
{
InitStyles();
DrawToolbar();
EditorGUILayout.BeginHorizontal();
{
// 左侧列表
EditorGUILayout.BeginVertical(GUILayout.Width(320));
DrawList();
EditorGUILayout.EndVertical();
// 分割线
GUILayout.Box("", GUILayout.Width(2), GUILayout.ExpandHeight(true));
// 右侧详情
EditorGUILayout.BeginVertical();
DrawDetail();
EditorGUILayout.EndVertical();
}
EditorGUILayout.EndHorizontal();
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
{
if (GUILayout.Button("重新加载", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
if (!_isDirty || EditorUtility.DisplayDialog("确认", "有未保存的修改,确定重新加载?", "确定", "取消"))
LoadConfig();
}
GUI.backgroundColor = _isDirty ? Color.yellow : Color.white;
if (GUILayout.Button(_isDirty ? "保存 *" : "保存", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
SaveConfig();
}
GUI.backgroundColor = Color.white;
GUILayout.Space(10);
if (GUILayout.Button("+ 新建", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
AddNewEntry();
}
if (GUILayout.Button("复制当前", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
DuplicateSelected();
}
if (GUILayout.Button("删除当前", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
DeleteSelected();
}
GUILayout.FlexibleSpace();
// 排序
GUILayout.Label("排序:", EditorStyles.toolbarButton, GUILayout.Width(30));
var newSort = (SortMode)EditorGUILayout.EnumPopup(_sortMode, EditorStyles.toolbarPopup, GUILayout.Width(70));
if (newSort != _sortMode)
{
_sortMode = newSort;
}
GUILayout.Space(5);
GUILayout.Label("搜索:", EditorStyles.toolbarButton, GUILayout.Width(30));
_searchText = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarSearchField, GUILayout.Width(120));
GUILayout.Label("ID:", EditorStyles.toolbarButton, GUILayout.Width(20));
_searchId = EditorGUILayout.TextField(_searchId, EditorStyles.toolbarSearchField, GUILayout.Width(60));
}
EditorGUILayout.EndHorizontal();
}
private void DrawList()
{
_listScroll = EditorGUILayout.BeginScrollView(_listScroll);
var filtered = GetFilteredAndSorted();
for (int i = 0; i < filtered.Count; i++)
{
var entry = filtered[i];
int realIndex = _entries.IndexOf(entry);
bool isSelected = realIndex == _selectedIndex;
Rect rect = EditorGUILayout.BeginHorizontal(isSelected ? _selectedStyle : GUIStyle.none, GUILayout.Height(22));
{
string label = $"<color=#888888>[{entry.Id}]</color> {entry.Name}";
if (entry.Priority > 0)
label += $" <color=#cc8800>P{entry.Priority}</color>";
GUILayout.Label(label, _entryStyle);
}
EditorGUILayout.EndHorizontal();
if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition))
{
_selectedIndex = realIndex;
GUI.FocusControl(null);
Repaint();
}
}
EditorGUILayout.EndScrollView();
// 底部统计
EditorGUILayout.LabelField($"共 {_entries.Count} 条 | 显示 {filtered.Count} 条", EditorStyles.centeredGreyMiniLabel);
}
private void DrawDetail()
{
if (_selectedIndex < 0 || _selectedIndex >= _entries.Count)
{
EditorGUILayout.HelpBox("← 请在左侧列表中选择一条引导配置", MessageType.Info);
return;
}
var entry = _entries[_selectedIndex];
_detailScroll = EditorGUILayout.BeginScrollView(_detailScroll);
EditorGUILayout.LabelField("基本信息", _headerStyle);
EditorGUI.indentLevel++;
DrawField("Id", ref entry.Id);
DrawField("Name (名称)", ref entry.Name);
DrawField("Priority (优先级)", ref entry.Priority);
EditorGUI.indentLevel--;
EditorGUILayout.Space(8);
// Conditions
_showConditions = EditorGUILayout.Foldout(_showConditions, "Conditions (触发条件)", true);
if (_showConditions)
{
EditorGUI.indentLevel++;
DrawStringList("Satisfy (满足条件)", ref entry.Conditions.Satisfy, ref _showSatisfy);
DrawField("CheckAgain", ref entry.Conditions.CheckAgain);
DrawStringList("Trigger (触发器)", ref entry.Conditions.Trigger, ref _showTrigger);
DrawField("End (结束条件)", ref entry.Conditions.End);
DrawField("Delay (延迟秒)", ref entry.Conditions.Delay);
DrawField("Limit (限制次数)", ref entry.Conditions.Limit);
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(8);
// ShowUI
_showShowUI = EditorGUILayout.Foldout(_showShowUI, "ShowUI (显示配置)", true);
if (_showShowUI)
{
EditorGUI.indentLevel++;
DrawField("LangText (多语言Key)", ref entry.ShowUI.LangText);
DrawField("RoleExpression (角色表情)", ref entry.ShowUI.RoleExpression);
DrawField("DialogPos (对话位置)", ref entry.ShowUI.DialogPos);
DrawField("OkBtn (确认按钮)", ref entry.ShowUI.OkBtn);
DrawField("ClickSg (点击信号)", ref entry.ShowUI.ClickSg);
DrawField("ClickPos (点击位置)", ref entry.ShowUI.ClickPos);
DrawField("FocusPos (聚焦位置)", ref entry.ShowUI.FocusPos);
DrawField("Phone (手机引导)", ref entry.ShowUI.Phone);
DrawField("ClickToSkip (点击跳过)", ref entry.ShowUI.ClickToSkip);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndScrollView();
}
#endregion
#region
private void DrawField(string label, ref int value)
{
int newVal = EditorGUILayout.IntField(label, value);
if (newVal != value) { value = newVal; _isDirty = true; }
}
private void DrawField(string label, ref float value)
{
float newVal = EditorGUILayout.FloatField(label, value);
if (!Mathf.Approximately(newVal, value)) { value = newVal; _isDirty = true; }
}
private void DrawField(string label, ref string value)
{
string newVal = EditorGUILayout.TextField(label, value ?? "");
if (newVal != (value ?? "")) { value = newVal; _isDirty = true; }
}
private void DrawStringList(string label, ref List<string> list, ref bool foldout)
{
EditorGUILayout.BeginHorizontal();
foldout = EditorGUILayout.Foldout(foldout, $"{label} ({list.Count})", true);
if (GUILayout.Button("+", GUILayout.Width(20)))
{
list.Add("");
_isDirty = true;
}
EditorGUILayout.EndHorizontal();
if (!foldout) return;
EditorGUI.indentLevel++;
for (int i = 0; i < list.Count; i++)
{
EditorGUILayout.BeginHorizontal();
string newVal = EditorGUILayout.TextField($"[{i}]", list[i] ?? "");
if (newVal != (list[i] ?? "")) { list[i] = newVal; _isDirty = true; }
if (GUILayout.Button("↑", GUILayout.Width(20)) && i > 0)
{
(list[i - 1], list[i]) = (list[i], list[i - 1]);
_isDirty = true;
}
if (GUILayout.Button("↓", GUILayout.Width(20)) && i < list.Count - 1)
{
(list[i], list[i + 1]) = (list[i + 1], list[i]);
_isDirty = true;
}
if (GUILayout.Button("×", GUILayout.Width(20)))
{
list.RemoveAt(i);
_isDirty = true;
break;
}
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
}
#endregion
#region
private List<GuideEntry> GetFilteredAndSorted()
{
IEnumerable<GuideEntry> result = _entries;
// 按名称搜索
if (!string.IsNullOrEmpty(_searchText))
{
string lower = _searchText.ToLower();
result = result.Where(e => (e.Name ?? "").ToLower().Contains(lower));
}
// 按ID搜索
if (!string.IsNullOrEmpty(_searchId) && int.TryParse(_searchId, out int searchIdVal))
{
result = result.Where(e => e.Id.ToString().Contains(_searchId));
}
// 排序
switch (_sortMode)
{
case SortMode.Id:
result = result.OrderBy(e => e.Id);
break;
case SortMode.Priority:
result = result.OrderByDescending(e => e.Priority).ThenBy(e => e.Id);
break;
case SortMode.Name:
result = result.OrderBy(e => e.Name ?? "");
break;
}
return result.ToList();
}
private void AddNewEntry()
{
int maxId = _entries.Count > 0 ? _entries.Max(e => e.Id) : 0;
var newEntry = new GuideEntry
{
Id = maxId + 1,
Name = "新引导步骤",
};
_entries.Add(newEntry);
_selectedIndex = _entries.Count - 1;
_isDirty = true;
}
private void DuplicateSelected()
{
if (_selectedIndex < 0 || _selectedIndex >= _entries.Count) return;
var src = _entries[_selectedIndex];
string json = JsonUtility.ToJson(src);
var copy = JsonUtility.FromJson<GuideEntry>(json);
// 深拷贝列表字段 (JsonUtility 会处理)
copy.Id = _entries.Max(e => e.Id) + 1;
copy.Name = src.Name + "_副本";
_entries.Insert(_selectedIndex + 1, copy);
_selectedIndex = _selectedIndex + 1;
_isDirty = true;
}
private void DeleteSelected()
{
if (_selectedIndex < 0 || _selectedIndex >= _entries.Count) return;
var entry = _entries[_selectedIndex];
if (EditorUtility.DisplayDialog("确认删除", $"确定删除引导 [{entry.Id}] {entry.Name}", "删除", "取消"))
{
_entries.RemoveAt(_selectedIndex);
_selectedIndex = Mathf.Min(_selectedIndex, _entries.Count - 1);
_isDirty = true;
}
}
#endregion
#region JSON / ()
/// <summary>
/// 使用 Unity 内置 JsonUtility 无法直接解析顶层数组,这里包装一下。
/// 原文件存在尾逗号等不规范 JSON需要预处理。
/// </summary>
private List<GuideEntry> ParseJsonArray(string json)
{
// 预处理:移除尾逗号 (trailing commas before ] or })
json = Regex.Replace(json, @",(\s*[\]\}])", "$1");
// 包装成对象让 JsonUtility 能解析
string wrapped = "{\"items\":" + json + "}";
var wrapper = JsonUtility.FromJson<GuideEntryArrayWrapper>(wrapped);
return wrapper != null && wrapper.items != null ? wrapper.items : new List<GuideEntry>();
}
[Serializable]
private class GuideEntryArrayWrapper
{
public List<GuideEntry> items;
}
/// <summary>
/// 序列化回原始格式(带缩进的 JSON 数组,保持和原文件一致的风格)
/// </summary>
private string SerializeToJson(List<GuideEntry> entries)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("[");
for (int i = 0; i < entries.Count; i++)
{
var e = entries[i];
sb.AppendLine(" {");
sb.AppendLine($" \"Id\": {e.Id},");
sb.AppendLine($" \"Name\":\"{EscapeJson(e.Name)}\",");
sb.AppendLine($" \"Priority\":{e.Priority},");
// Conditions
sb.AppendLine(" \"Conditions\":{");
sb.AppendLine(" \"Satisfy\":[");
for (int s = 0; s < e.Conditions.Satisfy.Count; s++)
{
string comma = s < e.Conditions.Satisfy.Count - 1 ? "," : "";
sb.AppendLine($" \"{EscapeJson(e.Conditions.Satisfy[s])}\"{comma}");
}
sb.AppendLine(" ],");
sb.AppendLine($" \"CheckAgain\":{e.Conditions.CheckAgain},");
sb.AppendLine(" \"Trigger\":[");
for (int t = 0; t < e.Conditions.Trigger.Count; t++)
{
string comma = t < e.Conditions.Trigger.Count - 1 ? "," : "";
sb.AppendLine($" \"{EscapeJson(e.Conditions.Trigger[t])}\"{comma}");
}
sb.AppendLine(" ],");
sb.AppendLine($" \"End\":\"{EscapeJson(e.Conditions.End)}\",");
sb.AppendLine($" \"Delay\":{FormatFloat(e.Conditions.Delay)},");
sb.AppendLine($" \"Limit\":{e.Conditions.Limit}");
sb.AppendLine(" },");
// ShowUI
sb.AppendLine(" \"ShowUI\": {");
sb.AppendLine($" \"LangText\":\"{EscapeJson(e.ShowUI.LangText)}\",");
sb.AppendLine($" \"RoleExpression\":\"{EscapeJson(e.ShowUI.RoleExpression)}\",");
sb.AppendLine($" \"DialogPos\":\"{EscapeJson(e.ShowUI.DialogPos)}\",");
sb.AppendLine($" \"OkBtn\":{e.ShowUI.OkBtn},");
sb.AppendLine($" \"ClickSg\":{e.ShowUI.ClickSg},");
sb.AppendLine($" \"ClickPos\":\"{EscapeJson(e.ShowUI.ClickPos)}\",");
sb.AppendLine($" \"FocusPos\":\"{EscapeJson(e.ShowUI.FocusPos)}\",");
sb.AppendLine($" \"Phone\":{e.ShowUI.Phone},");
sb.AppendLine($" \"ClickToSkip\":{e.ShowUI.ClickToSkip}");
sb.AppendLine(" }");
string entryComma = i < entries.Count - 1 ? "," : "";
sb.AppendLine($" }}{entryComma}");
}
sb.Append("]");
return sb.ToString();
}
private string FormatFloat(float val)
{
// 如果是整数值,输出不带小数点的格式(和原文件保持一致)
if (Mathf.Approximately(val, Mathf.Round(val)))
return ((int)val).ToString();
return val.ToString("G");
}
private string EscapeJson(string s)
{
if (string.IsNullOrEmpty(s)) return "";
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r").Replace("\t", "\\t");
}
#endregion
}