841 lines
30 KiB
C#
841 lines
30 KiB
C#
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 List<string> _satisfyPresets = new List<string>();
|
||
private List<string> _triggerPresets = new List<string>();
|
||
private List<string> _endPresets = new List<string>();
|
||
private List<string> _roleExpressionPresets = new List<string>();
|
||
private List<string> _dialogPosPresets = new List<string>();
|
||
private List<string> _clickPosPresets = new List<string>();
|
||
private List<string> _focusPosPresets = new List<string>();
|
||
private bool _presetsDirty = true;
|
||
|
||
// 样式缓存
|
||
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;
|
||
_presetsDirty = true;
|
||
Debug.Log($"[GuideConfigEditor] 已加载 {_entries.Count} 条引导配置");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从所有现有条目中收集去重的预设值,用于快速选择
|
||
/// </summary>
|
||
private void RebuildPresets()
|
||
{
|
||
if (!_presetsDirty) return;
|
||
_presetsDirty = false;
|
||
|
||
var satisfySet = new HashSet<string>();
|
||
var triggerSet = new HashSet<string>();
|
||
var endSet = new HashSet<string>();
|
||
var roleExprSet = new HashSet<string>();
|
||
var dialogPosSet = new HashSet<string>();
|
||
var clickPosSet = new HashSet<string>();
|
||
var focusPosSet = new HashSet<string>();
|
||
|
||
foreach (var e in _entries)
|
||
{
|
||
if (e.Conditions.Satisfy != null)
|
||
foreach (var s in e.Conditions.Satisfy)
|
||
if (!string.IsNullOrEmpty(s)) satisfySet.Add(s);
|
||
|
||
if (e.Conditions.Trigger != null)
|
||
foreach (var t in e.Conditions.Trigger)
|
||
if (!string.IsNullOrEmpty(t)) triggerSet.Add(t);
|
||
|
||
if (!string.IsNullOrEmpty(e.Conditions.End))
|
||
endSet.Add(e.Conditions.End);
|
||
|
||
if (!string.IsNullOrEmpty(e.ShowUI.RoleExpression))
|
||
roleExprSet.Add(e.ShowUI.RoleExpression);
|
||
|
||
if (!string.IsNullOrEmpty(e.ShowUI.DialogPos))
|
||
dialogPosSet.Add(e.ShowUI.DialogPos);
|
||
|
||
if (!string.IsNullOrEmpty(e.ShowUI.ClickPos))
|
||
clickPosSet.Add(e.ShowUI.ClickPos);
|
||
|
||
if (!string.IsNullOrEmpty(e.ShowUI.FocusPos))
|
||
focusPosSet.Add(e.ShowUI.FocusPos);
|
||
}
|
||
|
||
_satisfyPresets = satisfySet.OrderBy(s => s).ToList();
|
||
_triggerPresets = triggerSet.OrderBy(s => s).ToList();
|
||
_endPresets = endSet.OrderBy(s => s).ToList();
|
||
_roleExpressionPresets = roleExprSet.OrderBy(s => s).ToList();
|
||
_dialogPosPresets = dialogPosSet.OrderBy(s => s).ToList();
|
||
_clickPosPresets = clickPosSet.OrderBy(s => s).ToList();
|
||
_focusPosPresets = focusPosSet.OrderBy(s => s).ToList();
|
||
}
|
||
|
||
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();
|
||
RebuildPresets();
|
||
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(50));
|
||
_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++;
|
||
DrawStringListWithPresets("Satisfy (满足条件)", ref entry.Conditions.Satisfy, ref _showSatisfy, _satisfyPresets);
|
||
DrawField("CheckAgain", ref entry.Conditions.CheckAgain);
|
||
DrawStringListWithPresets("Trigger (触发器)", ref entry.Conditions.Trigger, ref _showTrigger, _triggerPresets);
|
||
DrawFieldWithPresets("End (结束条件)", ref entry.Conditions.End, _endPresets);
|
||
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);
|
||
DrawFieldWithPresets("RoleExpression (角色表情)", ref entry.ShowUI.RoleExpression, _roleExpressionPresets);
|
||
DrawFieldWithPresets("DialogPos (对话位置)", ref entry.ShowUI.DialogPos, _dialogPosPresets);
|
||
DrawField("OkBtn (确认按钮)", ref entry.ShowUI.OkBtn);
|
||
DrawField("ClickSg (点击信号)", ref entry.ShowUI.ClickSg);
|
||
DrawFieldWithPresets("ClickPos (点击位置)", ref entry.ShowUI.ClickPos, _clickPosPresets);
|
||
DrawFieldWithPresets("FocusPos (聚焦位置)", ref entry.ShowUI.FocusPos, _focusPosPresets);
|
||
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; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 带预设下拉的文本字段:点击▼按钮弹出已有值列表快速选择
|
||
/// </summary>
|
||
private void DrawFieldWithPresets(string label, ref string value, List<string> presets)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
string newVal = EditorGUILayout.TextField(label, value ?? "");
|
||
if (newVal != (value ?? "")) { value = newVal; _isDirty = true; }
|
||
|
||
if (presets != null && presets.Count > 0)
|
||
{
|
||
if (GUILayout.Button("▼", GUILayout.Width(20)))
|
||
{
|
||
// 记录当前选中条目和字段信息,在回调中通过索引赋值
|
||
int entryIdx = _selectedIndex;
|
||
string fieldLabel = label;
|
||
ShowPresetMenu(presets, selected =>
|
||
{
|
||
ApplyPresetToField(entryIdx, fieldLabel, selected);
|
||
});
|
||
}
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 预设回调:根据字段标签将选中值写回对应字段
|
||
/// </summary>
|
||
private void ApplyPresetToField(int entryIdx, string fieldLabel, string selected)
|
||
{
|
||
if (entryIdx < 0 || entryIdx >= _entries.Count) return;
|
||
var entry = _entries[entryIdx];
|
||
|
||
if (fieldLabel.StartsWith("End"))
|
||
entry.Conditions.End = selected;
|
||
else if (fieldLabel.StartsWith("RoleExpression"))
|
||
entry.ShowUI.RoleExpression = selected;
|
||
else if (fieldLabel.StartsWith("DialogPos"))
|
||
entry.ShowUI.DialogPos = selected;
|
||
else if (fieldLabel.StartsWith("ClickPos"))
|
||
entry.ShowUI.ClickPos = selected;
|
||
else if (fieldLabel.StartsWith("FocusPos"))
|
||
entry.ShowUI.FocusPos = selected;
|
||
|
||
_isDirty = true;
|
||
_presetsDirty = true;
|
||
Repaint();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 带预设快速选择的字符串列表(Satisfy / Trigger)
|
||
/// 点击 "+" 新增空行,点击 "▼" 从已有值中选择并新增
|
||
/// </summary>
|
||
private void DrawStringListWithPresets(string label, ref List<string> list, ref bool foldout, List<string> presets)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
foldout = EditorGUILayout.Foldout(foldout, $"{label} ({list.Count})", true);
|
||
if (GUILayout.Button("+", GUILayout.Width(20)))
|
||
{
|
||
list.Add("");
|
||
_isDirty = true;
|
||
}
|
||
// 预设快速添加按钮
|
||
if (presets != null && presets.Count > 0)
|
||
{
|
||
if (GUILayout.Button("▼选择", GUILayout.Width(45)))
|
||
{
|
||
var listRef = list;
|
||
ShowPresetMenu(presets, selected =>
|
||
{
|
||
listRef.Add(selected);
|
||
_isDirty = true;
|
||
_presetsDirty = true;
|
||
Repaint();
|
||
});
|
||
}
|
||
}
|
||
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 (presets != null && presets.Count > 0)
|
||
{
|
||
int idx = i;
|
||
if (GUILayout.Button("▼", GUILayout.Width(20)))
|
||
{
|
||
var listRef = list;
|
||
ShowPresetMenu(presets, selected =>
|
||
{
|
||
if (idx < listRef.Count)
|
||
{
|
||
listRef[idx] = selected;
|
||
_isDirty = true;
|
||
_presetsDirty = true;
|
||
Repaint();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
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--;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示预设值的下拉菜单(GenericMenu),按前缀分组
|
||
/// </summary>
|
||
private void ShowPresetMenu(List<string> presets, Action<string> onSelect)
|
||
{
|
||
var menu = new GenericMenu();
|
||
// 按前缀分组显示
|
||
var groups = new Dictionary<string, List<string>>();
|
||
foreach (var p in presets)
|
||
{
|
||
string prefix = ExtractPrefix(p);
|
||
if (!groups.ContainsKey(prefix))
|
||
groups[prefix] = new List<string>();
|
||
groups[prefix].Add(p);
|
||
}
|
||
|
||
// 如果分组数 > 1 且有明显前缀,按分组显示
|
||
if (groups.Count > 1 && groups.Keys.Any(k => k != ""))
|
||
{
|
||
foreach (var kvp in groups.OrderBy(g => g.Key))
|
||
{
|
||
string groupLabel = string.IsNullOrEmpty(kvp.Key) ? "其他" : kvp.Key;
|
||
foreach (var item in kvp.Value)
|
||
{
|
||
// 用 \u2215 (除号斜杠) 替换 / 避免被 GenericMenu 当作子菜单分隔符
|
||
string safeItem = item.Replace("/", "\u2215");
|
||
string menuPath = $"{groupLabel}/{safeItem}";
|
||
string captured = item;
|
||
menu.AddItem(new GUIContent(menuPath), false, () => onSelect(captured));
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 数量少或无明显前缀,平铺显示
|
||
foreach (var item in presets)
|
||
{
|
||
string safeItem = item.Replace("/", "\u2215");
|
||
string captured = item;
|
||
menu.AddItem(new GUIContent(safeItem), false, () => onSelect(captured));
|
||
}
|
||
}
|
||
|
||
menu.ShowAsContext();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提取条件字符串的前缀(如 "LevelEqual_1" → "LevelEqual","UIPanel_MainHomeUI_1" → "UIPanel")
|
||
/// </summary>
|
||
private string ExtractPrefix(string value)
|
||
{
|
||
if (string.IsNullOrEmpty(value)) return "";
|
||
int idx = value.IndexOf('_');
|
||
if (idx > 0 && idx < value.Length - 1)
|
||
return value.Substring(0, idx);
|
||
return "";
|
||
}
|
||
|
||
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;
|
||
|
||
// 全字段搜索(Name、Conditions、ShowUI 所有文本字段)
|
||
if (!string.IsNullOrEmpty(_searchText))
|
||
{
|
||
string lower = _searchText.ToLower();
|
||
result = result.Where(e => EntryMatchesSearch(e, lower));
|
||
}
|
||
|
||
// 按ID搜索
|
||
if (!string.IsNullOrEmpty(_searchId))
|
||
{
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全字段匹配:搜索 Name、Conditions(Satisfy/Trigger/End)、ShowUI 所有文本字段
|
||
/// </summary>
|
||
private bool EntryMatchesSearch(GuideEntry e, string lower)
|
||
{
|
||
// 基本信息
|
||
if ((e.Name ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
|
||
// Conditions
|
||
if (e.Conditions.Satisfy != null && e.Conditions.Satisfy.Any(s => (s ?? "").ToLower().Contains(lower)))
|
||
return true;
|
||
if (e.Conditions.Trigger != null && e.Conditions.Trigger.Any(s => (s ?? "").ToLower().Contains(lower)))
|
||
return true;
|
||
if ((e.Conditions.End ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
if (e.Conditions.CheckAgain.ToString().Contains(lower))
|
||
return true;
|
||
if (e.Conditions.Delay.ToString("G").ToLower().Contains(lower))
|
||
return true;
|
||
if (e.Conditions.Limit.ToString().Contains(lower))
|
||
return true;
|
||
|
||
// ShowUI
|
||
if ((e.ShowUI.LangText ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
if ((e.ShowUI.RoleExpression ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
if ((e.ShowUI.DialogPos ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
if ((e.ShowUI.ClickPos ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
if ((e.ShowUI.FocusPos ?? "").ToLower().Contains(lower))
|
||
return true;
|
||
if (e.ShowUI.OkBtn.ToString().Contains(lower))
|
||
return true;
|
||
if (e.ShowUI.ClickSg.ToString().Contains(lower))
|
||
return true;
|
||
if (e.ShowUI.Phone.ToString().Contains(lower))
|
||
return true;
|
||
if (e.ShowUI.ClickToSkip.ToString().Contains(lower))
|
||
return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
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
|
||
}
|