diff --git a/Scripts/Editor.meta b/Scripts/Editor.meta new file mode 100644 index 0000000..d51de57 --- /dev/null +++ b/Scripts/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5706044f5f8a1d9469a0d192ad6a6da8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools.meta b/Scripts/Editor/Design_Tools.meta new file mode 100644 index 0000000..60f6149 --- /dev/null +++ b/Scripts/Editor/Design_Tools.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5700364c75144e9418afa8c01c54ec6d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/Collections.meta b/Scripts/Editor/Design_Tools/Collections.meta new file mode 100644 index 0000000..af78fa1 --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 878d1e2621294df439f0cf7a06d20f32 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs new file mode 100644 index 0000000..a9282b3 --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs @@ -0,0 +1,937 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using OfficeOpenXml; +using ArtResource; +using Debug = UnityEngine.Debug; + +namespace DesignTools.Collections +{ + /// + /// 表情配置Editor工具 + /// 读取Docs/config/Emoji.xlsx,关联Art_SO/Collections/EmojiResource.asset + /// + public class EmojiConfigEditor : EditorWindow + { + private const string EMOJI_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections"; + private const string EMOJI_SO_NAME = "EmojiResource"; + private const string EMOJI_EXCEL_NAME = "Emoji.xlsx"; + private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx"; + private const string EMOJI_SHEET_NAME = "Emoji"; + private const string LANGUAGE_SHEET_NAME = "client"; + private const string DOCS_PATH_PREF_KEY = "EmojiConfigEditor_DocsPath"; + private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule"; + + private string docsRootPath = ""; + private List emojiDataList = new List(); + private ArtTableSO emojiTableSO; + private Dictionary> languageDict = new Dictionary>(); + private Vector2 scrollPosition; + private bool isDataLoaded = false; + private string pendingTooltipText = ""; + + [MenuItem("策划工具/收藏品/表情")] + public static void ShowWindow() + { + var window = GetWindow("表情配置"); + window.minSize = new Vector2(1000, 600); + window.Show(); + } + + private void OnEnable() + { + // 读取上次保存的路径 + if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY)) + { + docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY); + } + } + + private void OnGUI() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("表情配置工具", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + // Docs路径选择 + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + docsRootPath = EditorGUILayout.TextField("路径", docsRootPath); + if (GUILayout.Button("选择文件夹", GUILayout.Width(100))) + { + string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + docsRootPath = selectedPath; + EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath); + } + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("加载配置数据", GUILayout.Height(30))) + { + LoadData(); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(5); + + // 数据编辑区域 + if (isDataLoaded) + { + DrawDataEditor(); + } + else + { + EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info); + } + } + + /// + /// 加载配置数据 + /// + private void LoadData() + { + try + { + // 校验路径 + if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath)) + { + EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定"); + return; + } + + // 1. 检查Docs是否为Git仓库并更新 + if (!CheckAndUpdateDocsRepository()) + { + return; + } + + // 2. 检查Design_SubModule分支 + if (!CheckAndSwitchDesignSubModuleBranch()) + { + return; + } + + // 校验Emoji SO是否存在 + string emojiSOPath = Path.Combine(EMOJI_SO_PATH, $"{EMOJI_SO_NAME}.asset"); + emojiTableSO = AssetDatabase.LoadAssetAtPath(emojiSOPath); + + if (emojiTableSO == null) + { + EditorUtility.DisplayDialog("错误", + $"未找到表情资源配置\n路径: {emojiSOPath}\n\n请先在美术资源配置工具中创建", + "确定"); + return; + } + + if (emojiTableSO.Items == null || emojiTableSO.Items.Count == 0) + { + EditorUtility.DisplayDialog("错误", "表情资源配置数据为空", "确定"); + return; + } + + // 加载语言表 + LoadLanguageData(); + + // 加载Emoji.xlsx + LoadEmojiExcel(); + + isDataLoaded = true; + EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定"); + isDataLoaded = false; + } + } + + /// + /// 执行Git命令 + /// + private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success) + { + try + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + success = process.ExitCode == 0; + return success ? output : error; + } + } + catch (Exception e) + { + success = false; + return $"执行Git命令失败: {e.Message}"; + } + } + + /// + /// 检查并更新Docs仓库 + /// + private bool CheckAndUpdateDocsRepository() + { + string gitPath = Path.Combine(docsRootPath, ".git"); + if (!Directory.Exists(gitPath)) + { + EditorUtility.DisplayDialog("错误", + $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f); + + string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess); + + if (!fetchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Fetch失败", + $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f); + + string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess); + + if (!statusSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Status失败", + $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态", + "确定"); + return false; + } + + if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed")) + { + EditorUtility.ClearProgressBar(); + bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改", + "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)", + "继续(不推荐)", "取消,前往处理"); + + if (!proceed) + { + Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改"); + return false; + } + } + + if (statusResult.Contains("Your branch is behind")) + { + EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f); + + string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess); + + EditorUtility.ClearProgressBar(); + + if (!pullSuccess) + { + if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict")) + { + EditorUtility.DisplayDialog("拉取失败:存在冲突", + $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作", + "确定"); + } + else + { + EditorUtility.DisplayDialog("拉取失败", + $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题", + "确定"); + } + return false; + } + + Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}"); + EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Docs仓库已是最新版本"); + } + + return true; + } + + /// + /// 检查并切换Design_SubModule到main分支 + /// + private bool CheckAndSwitchDesignSubModuleBranch() + { + string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH); + designSubModulePath = Path.GetFullPath(designSubModulePath); + + if (!Directory.Exists(designSubModulePath)) + { + EditorUtility.DisplayDialog("错误", + $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f); + + string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess); + + if (!branchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Branch失败", + $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态", + "确定"); + return false; + } + + string currentBranch = branchResult.Trim(); + + if (currentBranch != "main") + { + EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f); + + string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess); + + EditorUtility.ClearProgressBar(); + + if (!checkoutSuccess) + { + EditorUtility.DisplayDialog("切换分支失败", + $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支", + "确定"); + return false; + } + + Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支"); + EditorUtility.DisplayDialog("分支切换成功", + $"Design_SubModule已从 {currentBranch} 切换到 main 分支", + "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Design_SubModule已在main分支"); + } + + return true; + } + + /// + /// 加载语言数据 + /// + private void LoadLanguageData() + { + languageDict.Clear(); + + string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME); + if (!File.Exists(languageExcelPath)) + { + Debug.LogWarning($"未找到语言表: {languageExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(languageExcelPath))) + { + var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}"); + return; + } + + // 查找Key列和语言列 + int keyColumnIndex = -1; + int zhCNColumnIndex = -1; + int enUSColumnIndex = -1; + int ptBRColumnIndex = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "key") + { + keyColumnIndex = col; + } + else if (header == "zh_CN") + { + zhCNColumnIndex = col; + } + else if (header == "en_US") + { + enUSColumnIndex = col; + } + else if (header == "pt_BR") + { + ptBRColumnIndex = col; + } + } + + if (keyColumnIndex < 0) + { + Debug.LogWarning("语言表中未找到Key列"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string key = worksheet.Cells[row, keyColumnIndex].Text; + if (string.IsNullOrEmpty(key)) continue; + + if (!languageDict.ContainsKey(key)) + { + languageDict[key] = new Dictionary(); + } + + if (zhCNColumnIndex > 0) + { + languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text; + } + if (enUSColumnIndex > 0) + { + languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text; + } + if (ptBRColumnIndex > 0) + { + languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text; + } + } + } + } + + /// + /// 加载Emoji.xlsx + /// + private void LoadEmojiExcel() + { + emojiDataList.Clear(); + + string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME); + if (!File.Exists(emojiExcelPath)) + { + throw new Exception($"未找到Emoji配置文件: {emojiExcelPath}"); + } + + using (var package = new ExcelPackage(new FileInfo(emojiExcelPath))) + { + var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME]; + if (worksheet == null) + { + throw new Exception($"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}"); + } + + // 读取表头(第1行) + int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + switch (header) + { + case "Id": + idCol = col; + break; + case "NameKey": + nameKeyCol = col; + break; + case "Init": + initCol = col; + break; + case "Icon": + iconCol = col; + break; + } + } + + if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0) + { + throw new Exception("Emoji.xlsx表结构不正确,缺少必要列(Id/NameKey/Init/Icon)"); + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + string nameKey = worksheet.Cells[row, nameKeyCol].Text; + string initText = worksheet.Cells[row, initCol].Text; + string iconText = worksheet.Cells[row, iconCol].Text; + + int init = 0; + if (!string.IsNullOrEmpty(initText)) + { + int.TryParse(initText, out init); + } + + int iconId = -1; + if (!string.IsNullOrEmpty(iconText)) + { + if (!int.TryParse(iconText, out iconId)) + { + iconId = -1; + } + } + + var emojiData = new EmojiData + { + Id = id, + NameKey = nameKey, + Init = init, + IconId = iconId + }; + + emojiDataList.Add(emojiData); + } + } + } + + /// + /// 绘制数据编辑器 + /// + private void DrawDataEditor() + { + pendingTooltipText = ""; + + EditorGUILayout.BeginVertical("box"); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"表情数据列表(共 {emojiDataList.Count} 条)", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.cyan; + if (GUILayout.Button("+ 添加表情", GUILayout.Height(25), GUILayout.Width(100))) + { + AddNewEmoji(); + } + GUI.backgroundColor = Color.white; + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(3); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + // 表头 + EditorGUILayout.BeginHorizontal("box"); + EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150)); + EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120)); + EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200)); + EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100)); + EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.EndHorizontal(); + + // 数据行 + for (int i = 0; i < emojiDataList.Count; i++) + { + DrawEmojiDataRow(emojiDataList[i]); + } + + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // 保存按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.green; + if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200))) + { + SaveData(); + } + GUI.backgroundColor = Color.white; + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + // 在所有内容绘制完后,绘制tooltip(确保在最上层) + if (!string.IsNullOrEmpty(pendingTooltipText)) + { + Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText)); + tooltipSize.x += 10; + tooltipSize.y += 10; + + Vector2 mousePos = Event.current.mousePosition; + Rect tooltipRect = new Rect( + mousePos.x + 1, + mousePos.y + 1, + tooltipSize.x, + tooltipSize.y + ); + + GUI.Box(tooltipRect, pendingTooltipText); + } + } + + /// + /// 绘制单行数据 + /// + private void DrawEmojiDataRow(EmojiData data) + { + EditorGUILayout.BeginHorizontal("box"); + + // Id(可编辑) + data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50)); + + // NameKey(可编辑) + data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150)); + + // 中文名称(只读预览,带即时多语言显示) + GUI.enabled = false; + string zhName = "未找到语言Key"; + + if (languageDict.ContainsKey(data.NameKey)) + { + var langs = languageDict[data.NameKey]; + if (langs.ContainsKey("zh_CN")) + { + zhName = langs["zh_CN"]; + } + } + + Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120)); + EditorGUI.TextField(nameRect, zhName); + + // 检测鼠标悬停并准备tooltip内容 + if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey)) + { + var langs = languageDict[data.NameKey]; + List tooltipLines = new List(); + + if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"])) + { + tooltipLines.Add($"[中文] {langs["zh_CN"]}"); + } + if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"])) + { + tooltipLines.Add($"[English] {langs["en_US"]}"); + } + if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"])) + { + tooltipLines.Add($"[Português] {langs["pt_BR"]}"); + } + + if (tooltipLines.Count > 0) + { + pendingTooltipText = string.Join("\n", tooltipLines); + Repaint(); + } + } + + GUI.enabled = true; + + // Init(Checkbox) + bool initChecked = data.Init == 1; + bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50)); + data.Init = newInitChecked ? 1 : 0; + + // Icon(下拉列表) + var iconItems = emojiTableSO.Items; + var iconNamesList = new List { "未选择" }; + iconNamesList.AddRange(iconItems.Select(x => x.Name)); + var iconNames = iconNamesList.ToArray(); + + int currentIndex = 0; + var currentItem = iconItems.Find(x => x.Id == data.IconId); + if (currentItem != null) + { + int itemIndex = iconItems.IndexOf(currentItem); + if (itemIndex >= 0) + { + currentIndex = itemIndex + 1; + } + } + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + if (newIndex == 0) + { + data.IconId = -1; + } + else if (newIndex > 0 && newIndex <= iconItems.Count) + { + data.IconId = iconItems[newIndex - 1].Id; + } + } + + // 预览 + if (data.IconId >= 0) + { + var item = iconItems.Find(x => x.Id == data.IconId); + if (item != null) + { + Sprite sprite = item.Sprite; + + if (sprite != null && sprite.texture != null) + { + Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + + EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f)); + + Rect texCoords = sprite.textureRect; + Texture2D tex = sprite.texture; + + Rect normalizedCoords = new Rect( + texCoords.x / tex.width, + texCoords.y / tex.height, + texCoords.width / tex.width, + texCoords.height / tex.height + ); + + float aspect = texCoords.width / texCoords.height; + Rect drawRect = previewRect; + if (aspect > 1f) + { + float height = drawRect.width / aspect; + drawRect.y += (drawRect.height - height) * 0.5f; + drawRect.height = height; + } + else + { + float width = drawRect.height * aspect; + drawRect.x += (drawRect.width - width) * 0.5f; + drawRect.width = width; + } + + GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true); + + if (!string.IsNullOrEmpty(item.Desc)) + { + GUI.Label(previewRect, new GUIContent("", item.Desc)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("无图片", GUILayout.Width(50)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("未选择", GUILayout.Width(50)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("未选择", GUILayout.Width(50)); + } + + // 删除按钮 + GUI.backgroundColor = Color.red; + if (GUILayout.Button("删除", GUILayout.Width(60))) + { + if (EditorUtility.DisplayDialog("确认删除", + $"确定要删除表情 {data.Id} ({data.NameKey}) 吗?", + "删除", "取消")) + { + emojiDataList.Remove(data); + } + } + GUI.backgroundColor = Color.white; + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 添加新表情 + /// + private void AddNewEmoji() + { + int newId = 1; + if (emojiDataList.Count > 0) + { + newId = emojiDataList.Max(x => x.Id) + 1; + } + + var newEmoji = new EmojiData + { + Id = newId, + NameKey = "", + Init = 0, + IconId = -1 + }; + + emojiDataList.Add(newEmoji); + + scrollPosition = new Vector2(0, float.MaxValue); + } + + /// + /// 保存数据到Excel + /// + private void SaveData() + { + try + { + string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME); + if (!File.Exists(emojiExcelPath)) + { + EditorUtility.DisplayDialog("错误", $"未找到Emoji配置文件: {emojiExcelPath}", "确定"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(emojiExcelPath))) + { + var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME]; + if (worksheet == null) + { + EditorUtility.DisplayDialog("错误", $"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}", "确定"); + return; + } + + // 查找列索引 + int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + switch (header) + { + case "Id": + idCol = col; + break; + case "NameKey": + nameKeyCol = col; + break; + case "Init": + initCol = col; + break; + case "Icon": + iconCol = col; + break; + } + } + + // 更新和删除数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + var processedIds = new HashSet(); + + // 第一遍:更新现有行或删除 + for (int row = rowCount; row >= 3; row--) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + var emojiData = emojiDataList.Find(x => x.Id == id); + if (emojiData != null) + { + worksheet.Cells[row, nameKeyCol].Value = emojiData.NameKey; + worksheet.Cells[row, initCol].Value = emojiData.Init; + + if (emojiData.IconId < 0) + { + worksheet.Cells[row, iconCol].Value = ""; + } + else + { + worksheet.Cells[row, iconCol].Value = emojiData.IconId; + } + + processedIds.Add(id); + } + else + { + worksheet.DeleteRow(row); + } + } + + // 第二遍:添加新行 + int currentRow = worksheet.Dimension?.Rows ?? 2; + foreach (var emojiData in emojiDataList) + { + if (!processedIds.Contains(emojiData.Id)) + { + currentRow++; + worksheet.Cells[currentRow, idCol].Value = emojiData.Id; + worksheet.Cells[currentRow, nameKeyCol].Value = emojiData.NameKey; + worksheet.Cells[currentRow, initCol].Value = emojiData.Init; + + if (emojiData.IconId < 0) + { + worksheet.Cells[currentRow, iconCol].Value = ""; + } + else + { + worksheet.Cells[currentRow, iconCol].Value = emojiData.IconId; + } + } + } + + package.Save(); + } + + // 提示成功并提醒推送 + bool understood = EditorUtility.DisplayDialog("保存成功", + "配置已保存到Excel文件!\n\n" + + "⚠️ 重要提醒:\n" + + "请及时在SourceTree或GitHubDesktop中:\n" + + "1. 提交(Commit)本次修改\n" + + "2. 推送(Push)到远程仓库\n\n" + + "避免与其他策划产生冲突!", + "我知道了"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定"); + } + } + + /// + /// 表情数据类 + /// + private class EmojiData + { + public int Id; + public string NameKey; + public int Init; + public int IconId; + } + } +} diff --git a/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs.meta new file mode 100644 index 0000000..d536e12 --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections/EmojiConfigEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9ae11b153bc3454cab7be603efdb90b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs new file mode 100644 index 0000000..5720632 --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs @@ -0,0 +1,987 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using OfficeOpenXml; +using ArtResource; +using Debug = UnityEngine.Debug; + +namespace DesignTools.Collections +{ + /// + /// 头像配置Editor工具 + /// 读取Docs/config/Face.xlsx,关联Art_SO/HeadResources.asset + /// + public class HeadConfigEditor : EditorWindow + { + private const string HEAD_SO_PATH = "Assets/Art_SubModule/Art_SO"; + private const string FACE_EXCEL_NAME = "Face.xlsx"; + private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx"; + private const string FACE_SHEET_NAME = "Face"; + private const string LANGUAGE_SHEET_NAME = "client"; + private const string DOCS_PATH_PREF_KEY = "HeadConfigEditor_DocsPath"; + private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule"; + + private string docsRootPath = ""; + private List faceDataList = new List(); + private ArtTableSO headTableSO; + private Dictionary> languageDict = new Dictionary>(); + private Vector2 scrollPosition; + private bool isDataLoaded = false; + private string pendingTooltipText = ""; + + [MenuItem("策划工具/收藏品/头像")] + public static void ShowWindow() + { + var window = GetWindow("头像配置"); + window.minSize = new Vector2(900, 600); + window.Show(); + } + + private void OnEnable() + { + // 设置EPPlus许可证 + // ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + + // 读取上次保存的路径 + if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY)) + { + docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY); + } + } + + private void OnGUI() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("头像配置工具", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + // Docs路径选择 + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + docsRootPath = EditorGUILayout.TextField("路径", docsRootPath); + if (GUILayout.Button("选择文件夹", GUILayout.Width(100))) + { + string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + docsRootPath = selectedPath; + EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath); + } + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("加载配置数据", GUILayout.Height(30))) + { + LoadData(); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(5); + + // 数据编辑区域 + if (isDataLoaded) + { + DrawDataEditor(); + } + else + { + EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info); + } + } + + /// + /// 加载配置数据 + /// + private void LoadData() + { + try + { + // 校验路径 + if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath)) + { + EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定"); + return; + } + + // 1. 检查Docs是否为Git仓库并更新 + if (!CheckAndUpdateDocsRepository()) + { + return; // 检查失败,提示已在方法内显示 + } + + // 2. 检查Design_SubModule分支 + if (!CheckAndSwitchDesignSubModuleBranch()) + { + return; // 检查失败,提示已在方法内显示 + } + + // 校验Head SO是否存在 + string[] soGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { HEAD_SO_PATH }); + if (soGuids.Length == 0) + { + EditorUtility.DisplayDialog("错误", $"未在 {HEAD_SO_PATH} 目录下找到头像资源配置(ArtTableSO)\n请先在美术资源配置工具中创建", "确定"); + return; + } + + // 查找名称为"HeadResource"的SO + ArtTableSO foundSO = null; + foreach (var guid in soGuids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + var so = AssetDatabase.LoadAssetAtPath(path); + if (so != null && so.TableName == "HeadResource") + { + foundSO = so; + break; + } + } + + if (foundSO == null) + { + EditorUtility.DisplayDialog("错误", "无法加载头像资源配置", "确定"); + return; + } + + headTableSO = foundSO; + + if (headTableSO.Items == null || headTableSO.Items.Count == 0) + { + EditorUtility.DisplayDialog("错误", "头像资源配置数据为空", "确定"); + return; + } + + // 加载语言表 + LoadLanguageData(); + + // 加载Face.xlsx + LoadFaceExcel(); + + isDataLoaded = true; + EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定"); + isDataLoaded = false; + } + } + + /// + /// 执行Git命令 + /// + private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success) + { + try + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + success = process.ExitCode == 0; + return success ? output : error; + } + } + catch (Exception e) + { + success = false; + return $"执行Git命令失败: {e.Message}"; + } + } + + /// + /// 检查并更新Docs仓库 + /// + private bool CheckAndUpdateDocsRepository() + { + // 检查是否是Git仓库 + string gitPath = Path.Combine(docsRootPath, ".git"); + if (!Directory.Exists(gitPath)) + { + EditorUtility.DisplayDialog("错误", + $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f); + + // 执行git fetch检查远程更新 + string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess); + + if (!fetchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Fetch失败", + $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f); + + // 检查本地和远程的差异 + string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess); + + if (!statusSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Status失败", + $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态", + "确定"); + return false; + } + + // 检查是否有未提交的更改 + if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed")) + { + EditorUtility.ClearProgressBar(); + bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改", + "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)", + "继续(不推荐)", "取消,前往处理"); + + if (!proceed) + { + Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改"); + return false; + } + } + + // 检查是否behind远程 + if (statusResult.Contains("Your branch is behind")) + { + EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f); + + string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess); + + EditorUtility.ClearProgressBar(); + + if (!pullSuccess) + { + // 检查是否是合并冲突 + if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict")) + { + EditorUtility.DisplayDialog("拉取失败:存在冲突", + $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作", + "确定"); + } + else + { + EditorUtility.DisplayDialog("拉取失败", + $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题", + "确定"); + } + return false; + } + + Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}"); + EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Docs仓库已是最新版本"); + } + + return true; + } + + /// + /// 检查并切换Design_SubModule到main分支 + /// + private bool CheckAndSwitchDesignSubModuleBranch() + { + string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH); + designSubModulePath = Path.GetFullPath(designSubModulePath); + + // 检查Design_SubModule是否存在 + if (!Directory.Exists(designSubModulePath)) + { + EditorUtility.DisplayDialog("错误", + $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f); + + // 检查当前分支 + string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess); + + if (!branchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Branch失败", + $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态", + "确定"); + return false; + } + + string currentBranch = branchResult.Trim(); + + if (currentBranch != "main") + { + EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f); + + string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess); + + EditorUtility.ClearProgressBar(); + + if (!checkoutSuccess) + { + EditorUtility.DisplayDialog("切换分支失败", + $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支", + "确定"); + return false; + } + + Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支"); + EditorUtility.DisplayDialog("分支切换成功", + $"Design_SubModule已从 {currentBranch} 切换到 main 分支", + "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Design_SubModule已在main分支"); + } + + return true; + } + + /// + /// 加载语言数据 + /// + private void LoadLanguageData() + { + languageDict.Clear(); + + string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME); + if (!File.Exists(languageExcelPath)) + { + Debug.LogWarning($"未找到语言表: {languageExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(languageExcelPath))) + { + var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}"); + return; + } + + // 查找Key列和语言列 + int keyColumnIndex = -1; + int zhCNColumnIndex = -1; + int enUSColumnIndex = -1; + int ptBRColumnIndex = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "key") + { + keyColumnIndex = col; + } + else if (header == "zh_CN") + { + zhCNColumnIndex = col; + } + else if (header == "en_US") + { + enUSColumnIndex = col; + } + else if (header == "pt_BR") + { + ptBRColumnIndex = col; + } + } + + if (keyColumnIndex < 0) + { + Debug.LogWarning("语言表中未找到Key列"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string key = worksheet.Cells[row, keyColumnIndex].Text; + if (string.IsNullOrEmpty(key)) continue; + + if (!languageDict.ContainsKey(key)) + { + languageDict[key] = new Dictionary(); + } + + if (zhCNColumnIndex > 0) + { + languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text; + } + if (enUSColumnIndex > 0) + { + languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text; + } + if (ptBRColumnIndex > 0) + { + languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text; + } + } + } + } + + /// + /// 加载Face.xlsx + /// + private void LoadFaceExcel() + { + faceDataList.Clear(); + + string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME); + if (!File.Exists(faceExcelPath)) + { + throw new Exception($"未找到Face配置文件: {faceExcelPath}"); + } + + using (var package = new ExcelPackage(new FileInfo(faceExcelPath))) + { + var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME]; + if (worksheet == null) + { + throw new Exception($"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}"); + } + + // 读取表头(第1行) + int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + switch (header) + { + case "Id": + idCol = col; + break; + case "NameKey": + nameKeyCol = col; + break; + case "Init": + initCol = col; + break; + case "Icon": + iconCol = col; + break; + } + } + + if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0) + { + throw new Exception("Face.xlsx表结构不正确,缺少必要列(Id/NameKey/Init/Icon)"); + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + string nameKey = worksheet.Cells[row, nameKeyCol].Text; + string initText = worksheet.Cells[row, initCol].Text; + string iconText = worksheet.Cells[row, iconCol].Text; + + int init = 0; + if (!string.IsNullOrEmpty(initText)) + { + int.TryParse(initText, out init); + } + + int iconId = -1; // 默认为未选择 + if (!string.IsNullOrEmpty(iconText)) + { + if (!int.TryParse(iconText, out iconId)) + { + iconId = -1; // 解析失败设为未选择 + } + } + + var faceData = new FaceData + { + Id = id, + NameKey = nameKey, + Init = init, + IconId = iconId + }; + + faceDataList.Add(faceData); + } + } + } + + /// + /// 绘制数据编辑器 + /// + private void DrawDataEditor() + { + pendingTooltipText = ""; + + EditorGUILayout.BeginVertical("box"); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"头像数据列表(共 {faceDataList.Count} 条)", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.cyan; + if (GUILayout.Button("+ 添加头像", GUILayout.Height(25), GUILayout.Width(100))) + { + AddNewFace(); + } + GUI.backgroundColor = Color.white; + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(3); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + // 表头 + EditorGUILayout.BeginHorizontal("box"); + EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150)); + EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120)); + EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200)); + EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100)); + EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.EndHorizontal(); + + // 数据行 + for (int i = 0; i < faceDataList.Count; i++) + { + DrawFaceDataRow(faceDataList[i]); + } + + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // 保存按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.green; + if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200))) + { + SaveData(); + } + GUI.backgroundColor = Color.white; + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + // 在所有内容绘制完后,绘制tooltip(确保在最上层) + if (!string.IsNullOrEmpty(pendingTooltipText)) + { + Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText)); + tooltipSize.x += 10; + tooltipSize.y += 10; + + // 使用当前鼠标位置,而不是之前记录的位置 + Vector2 mousePos = Event.current.mousePosition; + Rect tooltipRect = new Rect( + mousePos.x + 1, + mousePos.y + 1, + tooltipSize.x, + tooltipSize.y + ); + + GUI.Box(tooltipRect, pendingTooltipText); + } + } + + /// + /// 绘制单行数据 + /// + private void DrawFaceDataRow(FaceData data) + { + EditorGUILayout.BeginHorizontal("box"); + + // Id(只读) + GUI.enabled = false; + EditorGUILayout.IntField(data.Id, GUILayout.Width(50)); + GUI.enabled = true; + + // NameKey(可编辑) + data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150)); + + // 中文名称(只读预览,带即时多语言显示) + GUI.enabled = false; + string zhName = "未找到语言Key"; + + if (languageDict.ContainsKey(data.NameKey)) + { + var langs = languageDict[data.NameKey]; + if (langs.ContainsKey("zh_CN")) + { + zhName = langs["zh_CN"]; + } + } + + Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120)); + EditorGUI.TextField(nameRect, zhName); + + // 检测鼠标悬停并准备tooltip内容(不立即绘制) + if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey)) + { + var langs = languageDict[data.NameKey]; + List tooltipLines = new List(); + + if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"])) + { + tooltipLines.Add($"[中文] {langs["zh_CN"]}"); + } + if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"])) + { + tooltipLines.Add($"[English] {langs["en_US"]}"); + } + if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"])) + { + tooltipLines.Add($"[Português] {langs["pt_BR"]}"); + } + + if (tooltipLines.Count > 0) + { + pendingTooltipText = string.Join("\n", tooltipLines); + Repaint(); + } + } + + GUI.enabled = true; + + // Init(Checkbox) + bool initChecked = data.Init == 1; + bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50)); + data.Init = newInitChecked ? 1 : 0; + + // Icon(下拉列表,包含未选择选项) + var iconItems = headTableSO.Items; + var iconNamesList = new List { "未选择" }; + iconNamesList.AddRange(iconItems.Select(x => x.Name)); + var iconNames = iconNamesList.ToArray(); + + // 查找当前选中的索引 + int currentIndex = 0; // 默认为"未选择" + var currentItem = iconItems.Find(x => x.Id == data.IconId); + if (currentItem != null) + { + // 找到了对应的Item,索引需要+1(因为第0项是"未选择") + int itemIndex = iconItems.IndexOf(currentItem); + if (itemIndex >= 0) + { + currentIndex = itemIndex + 1; + } + } + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + if (newIndex == 0) + { + // 选择了"未选择" + data.IconId = -1; + } + else if (newIndex > 0 && newIndex <= iconItems.Count) + { + // 选择了具体的Icon(索引需要-1) + data.IconId = iconItems[newIndex - 1].Id; + } + } + + // 预览和Desc提示 + if (data.IconId >= 0) + { + var item = iconItems.Find(x => x.Id == data.IconId); + if (item != null) + { + // 直接使用Sprite引用 + Sprite sprite = item.Sprite; + + if (sprite != null && sprite.texture != null) + { + // 正确处理图集sprite的预览 + Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + + // 绘制背景 + EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f)); + + Rect texCoords = sprite.textureRect; + Texture2D tex = sprite.texture; + + // 归一化UV坐标 + Rect normalizedCoords = new Rect( + texCoords.x / tex.width, + texCoords.y / tex.height, + texCoords.width / tex.width, + texCoords.height / tex.height + ); + + // 计算保持宽高比的显示区域 + float aspect = texCoords.width / texCoords.height; + Rect drawRect = previewRect; + if (aspect > 1f) + { + float height = drawRect.width / aspect; + drawRect.y += (drawRect.height - height) * 0.5f; + drawRect.height = height; + } + else + { + float width = drawRect.height * aspect; + drawRect.x += (drawRect.width - width) * 0.5f; + drawRect.width = width; + } + + GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true); + + // 添加Tooltip + if (!string.IsNullOrEmpty(item.Desc)) + { + GUI.Label(previewRect, new GUIContent("", item.Desc)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("无图片", GUILayout.Width(50)); + } + } + else + { + // ID存在但找不到对应的Item + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("未选择", GUILayout.Width(50)); + } + } + else + { + // IconId < 0,未选择状态 + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("未选择", GUILayout.Width(50)); + } + + // 删除按钮 + GUI.backgroundColor = Color.red; + if (GUILayout.Button("删除", GUILayout.Width(60))) + { + if (EditorUtility.DisplayDialog("确认删除", + $"确定要删除头像 {data.Id} ({data.NameKey}) 吗?", + "删除", "取消")) + { + faceDataList.Remove(data); + } + } + GUI.backgroundColor = Color.white; + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 添加新头像 + /// + private void AddNewFace() + { + // 计算新ID(当前最大ID + 1) + int newId = 1; + if (faceDataList.Count > 0) + { + newId = faceDataList.Max(x => x.Id) + 1; + } + + var newFace = new FaceData + { + Id = newId, + NameKey = "", + Init = 0, + IconId = -1 + }; + + faceDataList.Add(newFace); + + // 滚动到底部 + scrollPosition = new Vector2(0, float.MaxValue); + } + + /// + /// 保存数据到Excel + /// + private void SaveData() + { + try + { + string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME); + if (!File.Exists(faceExcelPath)) + { + EditorUtility.DisplayDialog("错误", $"未找到Face配置文件: {faceExcelPath}", "确定"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(faceExcelPath))) + { + var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME]; + if (worksheet == null) + { + EditorUtility.DisplayDialog("错误", $"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}", "确定"); + return; + } + + // 查找列索引 + int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + switch (header) + { + case "Id": + idCol = col; + break; + case "NameKey": + nameKeyCol = col; + break; + case "Init": + initCol = col; + break; + case "Icon": + iconCol = col; + break; + } + } + + // 更新和删除数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + var processedIds = new HashSet(); + + // 第一遍:更新现有行或标记删除 + for (int row = rowCount; row >= 3; row--) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + // 查找对应的FaceData + var faceData = faceDataList.Find(x => x.Id == id); + if (faceData != null) + { + // 更新数据 + worksheet.Cells[row, nameKeyCol].Value = faceData.NameKey; + worksheet.Cells[row, initCol].Value = faceData.Init; + + // IconId为-1时写入空字符串,否则写入实际值 + if (faceData.IconId < 0) + { + worksheet.Cells[row, iconCol].Value = ""; + } + else + { + worksheet.Cells[row, iconCol].Value = faceData.IconId; + } + + processedIds.Add(id); + } + else + { + // 删除行 + worksheet.DeleteRow(row); + } + } + + // 第二遍:添加新行 + int currentRow = worksheet.Dimension?.Rows ?? 2; + foreach (var faceData in faceDataList) + { + if (!processedIds.Contains(faceData.Id)) + { + // 这是新增的数据 + currentRow++; + worksheet.Cells[currentRow, idCol].Value = faceData.Id; + worksheet.Cells[currentRow, nameKeyCol].Value = faceData.NameKey; + worksheet.Cells[currentRow, initCol].Value = faceData.Init; + + if (faceData.IconId < 0) + { + worksheet.Cells[currentRow, iconCol].Value = ""; + } + else + { + worksheet.Cells[currentRow, iconCol].Value = faceData.IconId; + } + } + } + + // 保存文件 + package.Save(); + } + + // 提示成功并提醒推送 + bool understood = EditorUtility.DisplayDialog("保存成功", + "配置已保存到Excel文件!\n\n" + + "⚠️ 重要提醒:\n" + + "请及时在SourceTree或GitHubDesktop中:\n" + + "1. 提交(Commit)本次修改\n" + + "2. 推送(Push)到远程仓库\n\n" + + "避免与其他策划产生冲突!", + "我知道了"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定"); + } + } + + /// + /// 头像数据类 + /// + private class FaceData + { + public int Id; + public string NameKey; + public int Init; + public int IconId; + } + } +} diff --git a/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs.meta new file mode 100644 index 0000000..e550a2c --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections/HeadConfigEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3c6d1239cdbf0748b5fb75f163c3121 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs new file mode 100644 index 0000000..437ae57 --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs @@ -0,0 +1,965 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using OfficeOpenXml; +using ArtResource; +using Debug = UnityEngine.Debug; + +namespace DesignTools.Collections +{ + /// + /// 头像框配置Editor工具 + /// 读取Docs/config/Avatar.xlsx,关联Art_SO/Collections/HeadFrameResource.asset + /// + public class HeadFrameConfigEditor : EditorWindow + { + private const string HEADFRAME_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections"; + private const string HEADFRAME_SO_NAME = "HeadFrameResource"; + private const string AVATAR_EXCEL_NAME = "Avatar.xlsx"; + private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx"; + private const string AVATAR_SHEET_NAME = "Avatar"; + private const string LANGUAGE_SHEET_NAME = "client"; + private const string DOCS_PATH_PREF_KEY = "HeadFrameConfigEditor_DocsPath"; + private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule"; + + private string docsRootPath = ""; + private List headFrameDataList = new List(); + private ArtTableSO headFrameTableSO; + private Dictionary> languageDict = new Dictionary>(); + private Vector2 scrollPosition; + private bool isDataLoaded = false; + private string pendingTooltipText = ""; + + [MenuItem("策划工具/收藏品/头像框")] + public static void ShowWindow() + { + var window = GetWindow("头像框配置"); + window.minSize = new Vector2(1000, 600); + window.Show(); + } + + private void OnEnable() + { + // 读取上次保存的路径 + if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY)) + { + docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY); + } + } + + private void OnGUI() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("头像框配置工具", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + // Docs路径选择 + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + docsRootPath = EditorGUILayout.TextField("路径", docsRootPath); + if (GUILayout.Button("选择文件夹", GUILayout.Width(100))) + { + string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + docsRootPath = selectedPath; + EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath); + } + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("加载配置数据", GUILayout.Height(30))) + { + LoadData(); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(5); + + // 数据编辑区域 + if (isDataLoaded) + { + DrawDataEditor(); + } + else + { + EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info); + } + } + + /// + /// 加载配置数据 + /// + private void LoadData() + { + try + { + // 校验路径 + if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath)) + { + EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定"); + return; + } + + // 1. 检查Docs是否为Git仓库并更新 + if (!CheckAndUpdateDocsRepository()) + { + return; + } + + // 2. 检查Design_SubModule分支 + if (!CheckAndSwitchDesignSubModuleBranch()) + { + return; + } + + // 校验HeadFrame SO是否存在 + string headFrameSOPath = Path.Combine(HEADFRAME_SO_PATH, $"{HEADFRAME_SO_NAME}.asset"); + headFrameTableSO = AssetDatabase.LoadAssetAtPath(headFrameSOPath); + + if (headFrameTableSO == null) + { + EditorUtility.DisplayDialog("错误", + $"未找到头像框资源配置\n路径: {headFrameSOPath}\n\n请先在美术资源配置工具中创建", + "确定"); + return; + } + + if (headFrameTableSO.Items == null || headFrameTableSO.Items.Count == 0) + { + EditorUtility.DisplayDialog("错误", "头像框资源配置数据为空", "确定"); + return; + } + + // 加载语言表 + LoadLanguageData(); + + // 加载Avatar.xlsx + LoadAvatarExcel(); + + isDataLoaded = true; + EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定"); + isDataLoaded = false; + } + } + + /// + /// 执行Git命令 + /// + private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success) + { + try + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + success = process.ExitCode == 0; + return success ? output : error; + } + } + catch (Exception e) + { + success = false; + return $"执行Git命令失败: {e.Message}"; + } + } + + /// + /// 检查并更新Docs仓库 + /// + private bool CheckAndUpdateDocsRepository() + { + string gitPath = Path.Combine(docsRootPath, ".git"); + if (!Directory.Exists(gitPath)) + { + EditorUtility.DisplayDialog("错误", + $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f); + + string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess); + + if (!fetchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Fetch失败", + $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f); + + string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess); + + if (!statusSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Status失败", + $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态", + "确定"); + return false; + } + + if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed")) + { + EditorUtility.ClearProgressBar(); + bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改", + "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)", + "继续(不推荐)", "取消,前往处理"); + + if (!proceed) + { + Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改"); + return false; + } + } + + if (statusResult.Contains("Your branch is behind")) + { + EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f); + + string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess); + + EditorUtility.ClearProgressBar(); + + if (!pullSuccess) + { + if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict")) + { + EditorUtility.DisplayDialog("拉取失败:存在冲突", + $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作", + "确定"); + } + else + { + EditorUtility.DisplayDialog("拉取失败", + $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题", + "确定"); + } + return false; + } + + Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}"); + EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Docs仓库已是最新版本"); + } + + return true; + } + + /// + /// 检查并切换Design_SubModule到main分支 + /// + private bool CheckAndSwitchDesignSubModuleBranch() + { + string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH); + designSubModulePath = Path.GetFullPath(designSubModulePath); + + if (!Directory.Exists(designSubModulePath)) + { + EditorUtility.DisplayDialog("错误", + $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f); + + string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess); + + if (!branchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Branch失败", + $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态", + "确定"); + return false; + } + + string currentBranch = branchResult.Trim(); + + if (currentBranch != "main") + { + EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f); + + string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess); + + EditorUtility.ClearProgressBar(); + + if (!checkoutSuccess) + { + EditorUtility.DisplayDialog("切换分支失败", + $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支", + "确定"); + return false; + } + + Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支"); + EditorUtility.DisplayDialog("分支切换成功", + $"Design_SubModule已从 {currentBranch} 切换到 main 分支", + "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Design_SubModule已在main分支"); + } + + return true; + } + + /// + /// 加载语言数据 + /// + private void LoadLanguageData() + { + languageDict.Clear(); + + string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME); + if (!File.Exists(languageExcelPath)) + { + Debug.LogWarning($"未找到语言表: {languageExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(languageExcelPath))) + { + var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}"); + return; + } + + // 查找Key列和语言列 + int keyColumnIndex = -1; + int zhCNColumnIndex = -1; + int enUSColumnIndex = -1; + int ptBRColumnIndex = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "key") + { + keyColumnIndex = col; + } + else if (header == "zh_CN") + { + zhCNColumnIndex = col; + } + else if (header == "en_US") + { + enUSColumnIndex = col; + } + else if (header == "pt_BR") + { + ptBRColumnIndex = col; + } + } + + if (keyColumnIndex < 0) + { + Debug.LogWarning("语言表中未找到Key列"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string key = worksheet.Cells[row, keyColumnIndex].Text; + if (string.IsNullOrEmpty(key)) continue; + + if (!languageDict.ContainsKey(key)) + { + languageDict[key] = new Dictionary(); + } + + if (zhCNColumnIndex > 0) + { + languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text; + } + if (enUSColumnIndex > 0) + { + languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text; + } + if (ptBRColumnIndex > 0) + { + languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text; + } + } + } + } + + /// + /// 加载Avatar.xlsx + /// + private void LoadAvatarExcel() + { + headFrameDataList.Clear(); + + string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME); + if (!File.Exists(avatarExcelPath)) + { + throw new Exception($"未找到Avatar配置文件: {avatarExcelPath}"); + } + + using (var package = new ExcelPackage(new FileInfo(avatarExcelPath))) + { + var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME]; + if (worksheet == null) + { + throw new Exception($"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}"); + } + + // 读取表头(第1行) + int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + switch (header) + { + case "Id": + idCol = col; + break; + case "NameKey": + nameKeyCol = col; + break; + case "Init": + initCol = col; + break; + case "Icon": + iconCol = col; + break; + case "FrameImageScale": + frameImageScaleCol = col; + break; + } + } + + if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0 || frameImageScaleCol < 0) + { + throw new Exception("Avatar.xlsx表结构不正确,缺少必要列(Id/NameKey/Init/Icon/FrameImageScale)"); + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + string nameKey = worksheet.Cells[row, nameKeyCol].Text; + string initText = worksheet.Cells[row, initCol].Text; + string iconText = worksheet.Cells[row, iconCol].Text; + string frameImageScaleText = worksheet.Cells[row, frameImageScaleCol].Text; + + int init = 0; + if (!string.IsNullOrEmpty(initText)) + { + int.TryParse(initText, out init); + } + + int iconId = -1; + if (!string.IsNullOrEmpty(iconText)) + { + if (!int.TryParse(iconText, out iconId)) + { + iconId = -1; + } + } + + float frameImageScale = 1.0f; + if (!string.IsNullOrEmpty(frameImageScaleText)) + { + if (!float.TryParse(frameImageScaleText, out frameImageScale)) + { + frameImageScale = 1.0f; + } + } + + var frameData = new HeadFrameData + { + Id = id, + NameKey = nameKey, + Init = init, + IconId = iconId, + FrameImageScale = frameImageScale + }; + + headFrameDataList.Add(frameData); + } + } + } + + /// + /// 绘制数据编辑器 + /// + private void DrawDataEditor() + { + pendingTooltipText = ""; + + EditorGUILayout.BeginVertical("box"); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"头像框数据列表(共 {headFrameDataList.Count} 条)", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.cyan; + if (GUILayout.Button("+ 添加头像框", GUILayout.Height(25), GUILayout.Width(100))) + { + AddNewHeadFrame(); + } + GUI.backgroundColor = Color.white; + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(3); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + // 表头 + EditorGUILayout.BeginHorizontal("box"); + EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150)); + EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120)); + EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200)); + EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100)); + EditorGUILayout.LabelField("缩放", EditorStyles.boldLabel, GUILayout.Width(80)); + EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.EndHorizontal(); + + // 数据行 + for (int i = 0; i < headFrameDataList.Count; i++) + { + DrawHeadFrameDataRow(headFrameDataList[i]); + } + + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // 保存按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.green; + if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200))) + { + SaveData(); + } + GUI.backgroundColor = Color.white; + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + // 在所有内容绘制完后,绘制tooltip(确保在最上层) + if (!string.IsNullOrEmpty(pendingTooltipText)) + { + Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText)); + tooltipSize.x += 10; + tooltipSize.y += 10; + + // 使用当前鼠标位置,而不是之前记录的位置 + Vector2 mousePos = Event.current.mousePosition; + Rect tooltipRect = new Rect( + mousePos.x + 1, + mousePos.y + 1, + tooltipSize.x, + tooltipSize.y + ); + + GUI.Box(tooltipRect, pendingTooltipText); + } + } + + /// + /// 绘制单行数据 + /// + private void DrawHeadFrameDataRow(HeadFrameData data) + { + EditorGUILayout.BeginHorizontal("box"); + + // Id(可编辑) + data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50)); + + // NameKey(可编辑) + data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150)); + + // 中文名称(只读预览,带即时多语言显示) + GUI.enabled = false; + string zhName = "未找到语言Key"; + + if (languageDict.ContainsKey(data.NameKey)) + { + var langs = languageDict[data.NameKey]; + if (langs.ContainsKey("zh_CN")) + { + zhName = langs["zh_CN"]; + } + } + + Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120)); + EditorGUI.TextField(nameRect, zhName); + + // 检测鼠标悬停并准备tooltip内容(不立即绘制) + if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey)) + { + var langs = languageDict[data.NameKey]; + List tooltipLines = new List(); + + if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"])) + { + tooltipLines.Add($"[中文] {langs["zh_CN"]}"); + } + if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"])) + { + tooltipLines.Add($"[English] {langs["en_US"]}"); + } + if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"])) + { + tooltipLines.Add($"[Português] {langs["pt_BR"]}"); + } + + if (tooltipLines.Count > 0) + { + pendingTooltipText = string.Join("\n", tooltipLines); + Repaint(); + } + } + + GUI.enabled = true; + + // Init(Checkbox) + bool initChecked = data.Init == 1; + bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50)); + data.Init = newInitChecked ? 1 : 0; + + // Icon(下拉列表) + var iconItems = headFrameTableSO.Items; + var iconNamesList = new List { "未选择" }; + iconNamesList.AddRange(iconItems.Select(x => x.Name)); + var iconNames = iconNamesList.ToArray(); + + int currentIndex = 0; + var currentItem = iconItems.Find(x => x.Id == data.IconId); + if (currentItem != null) + { + int itemIndex = iconItems.IndexOf(currentItem); + if (itemIndex >= 0) + { + currentIndex = itemIndex + 1; + } + } + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + if (newIndex == 0) + { + data.IconId = -1; + } + else if (newIndex > 0 && newIndex <= iconItems.Count) + { + data.IconId = iconItems[newIndex - 1].Id; + } + } + + // 预览 + if (data.IconId >= 0) + { + var item = iconItems.Find(x => x.Id == data.IconId); + if (item != null) + { + Sprite sprite = item.Sprite; + + if (sprite != null && sprite.texture != null) + { + Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + + EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f)); + + Rect texCoords = sprite.textureRect; + Texture2D tex = sprite.texture; + + Rect normalizedCoords = new Rect( + texCoords.x / tex.width, + texCoords.y / tex.height, + texCoords.width / tex.width, + texCoords.height / tex.height + ); + + float aspect = texCoords.width / texCoords.height; + Rect drawRect = previewRect; + if (aspect > 1f) + { + float height = drawRect.width / aspect; + drawRect.y += (drawRect.height - height) * 0.5f; + drawRect.height = height; + } + else + { + float width = drawRect.height * aspect; + drawRect.x += (drawRect.width - width) * 0.5f; + drawRect.width = width; + } + + GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true); + + if (!string.IsNullOrEmpty(item.Desc)) + { + GUI.Label(previewRect, new GUIContent("", item.Desc)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("无图片", GUILayout.Width(50)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("未选择", GUILayout.Width(50)); + } + } + else + { + GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUILayout.Space(-50); + EditorGUILayout.LabelField("未选择", GUILayout.Width(50)); + } + + // FrameImageScale(float输入框) + data.FrameImageScale = EditorGUILayout.FloatField(data.FrameImageScale, GUILayout.Width(80)); + + // 删除按钮 + GUI.backgroundColor = Color.red; + if (GUILayout.Button("删除", GUILayout.Width(60))) + { + if (EditorUtility.DisplayDialog("确认删除", + $"确定要删除头像框 {data.Id} ({data.NameKey}) 吗?", + "删除", "取消")) + { + headFrameDataList.Remove(data); + } + } + GUI.backgroundColor = Color.white; + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 添加新头像框 + /// + private void AddNewHeadFrame() + { + int newId = 1; + if (headFrameDataList.Count > 0) + { + newId = headFrameDataList.Max(x => x.Id) + 1; + } + + var newFrame = new HeadFrameData + { + Id = newId, + NameKey = "", + Init = 0, + IconId = -1, + FrameImageScale = 1.0f + }; + + headFrameDataList.Add(newFrame); + + scrollPosition = new Vector2(0, float.MaxValue); + } + + /// + /// 保存数据到Excel + /// + private void SaveData() + { + try + { + string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME); + if (!File.Exists(avatarExcelPath)) + { + EditorUtility.DisplayDialog("错误", $"未找到Avatar配置文件: {avatarExcelPath}", "确定"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(avatarExcelPath))) + { + var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME]; + if (worksheet == null) + { + EditorUtility.DisplayDialog("错误", $"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}", "确定"); + return; + } + + // 查找列索引 + int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + switch (header) + { + case "Id": + idCol = col; + break; + case "NameKey": + nameKeyCol = col; + break; + case "Init": + initCol = col; + break; + case "Icon": + iconCol = col; + break; + case "FrameImageScale": + frameImageScaleCol = col; + break; + } + } + + // 更新和删除数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + var processedIds = new HashSet(); + + // 第一遍:更新现有行或删除 + for (int row = rowCount; row >= 3; row--) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + var frameData = headFrameDataList.Find(x => x.Id == id); + if (frameData != null) + { + worksheet.Cells[row, nameKeyCol].Value = frameData.NameKey; + worksheet.Cells[row, initCol].Value = frameData.Init; + + if (frameData.IconId < 0) + { + worksheet.Cells[row, iconCol].Value = ""; + } + else + { + worksheet.Cells[row, iconCol].Value = frameData.IconId; + } + + worksheet.Cells[row, frameImageScaleCol].Value = frameData.FrameImageScale; + + processedIds.Add(id); + } + else + { + worksheet.DeleteRow(row); + } + } + + // 第二遍:添加新行 + int currentRow = worksheet.Dimension?.Rows ?? 2; + foreach (var frameData in headFrameDataList) + { + if (!processedIds.Contains(frameData.Id)) + { + currentRow++; + worksheet.Cells[currentRow, idCol].Value = frameData.Id; + worksheet.Cells[currentRow, nameKeyCol].Value = frameData.NameKey; + worksheet.Cells[currentRow, initCol].Value = frameData.Init; + + if (frameData.IconId < 0) + { + worksheet.Cells[currentRow, iconCol].Value = ""; + } + else + { + worksheet.Cells[currentRow, iconCol].Value = frameData.IconId; + } + + worksheet.Cells[currentRow, frameImageScaleCol].Value = frameData.FrameImageScale; + } + } + + package.Save(); + } + + // 提示成功并提醒推送 + bool understood = EditorUtility.DisplayDialog("保存成功", + "配置已保存到Excel文件!\n\n" + + "⚠️ 重要提醒:\n" + + "请及时在SourceTree或GitHubDesktop中:\n" + + "1. 提交(Commit)本次修改\n" + + "2. 推送(Push)到远程仓库\n\n" + + "避免与其他策划产生冲突!", + "我知道了"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定"); + } + } + + /// + /// 头像框数据类 + /// + private class HeadFrameData + { + public int Id; + public string NameKey; + public int Init; + public int IconId; + public float FrameImageScale; + } + } +} diff --git a/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs.meta new file mode 100644 index 0000000..380181f --- /dev/null +++ b/Scripts/Editor/Design_Tools/Collections/HeadFrameConfigEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 370961b2539ed9d49ade3ec27aedd25e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/ItemConfigEditor.cs b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs new file mode 100644 index 0000000..41bdd8a --- /dev/null +++ b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs @@ -0,0 +1,1806 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using OfficeOpenXml; +using ArtResource; +using Debug = UnityEngine.Debug; + +namespace DesignTools +{ + /// + /// Item配置Editor工具 + /// 读取Docs/config/Item.xlsx,关联所有Art_SO资源 + /// + public class ItemConfigEditor : EditorWindow + { + private const string ART_SO_PATH = "Assets/Art_SubModule/Art_SO"; + private const string ITEM_EXCEL_NAME = "Item.xlsx"; + private const string LIMITED_TIME_EVENT_EXCEL_NAME = "LimitedTimeEvent.xlsx"; + private const string AVATAR_EXCEL_NAME = "Avatar.xlsx"; + private const string FACE_EXCEL_NAME = "Face.xlsx"; + private const string EMOJI_EXCEL_NAME = "Emoji.xlsx"; + private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx"; + private const string ITEM_SHEET_NAME = "Item"; + private const string EVENT_SHEET_NAME = "Event"; + private const string AVATAR_SHEET_NAME = "Avatar"; + private const string FACE_SHEET_NAME = "Face"; + private const string EMOJI_SHEET_NAME = "Emoji"; + private const string LANGUAGE_SHEET_NAME = "client"; + private const string DOCS_PATH_PREF_KEY = "ItemConfigEditor_DocsPath"; + private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule"; + + // IType类型定义 + private static readonly Dictionary ITEM_TYPES = new Dictionary + { + { 1, "能量" }, + { 2, "星星" }, + { 3, "钻石" }, + { 97, "Playroom宠物道具" }, + { 98, "卡牌" }, + { 99, "背包道具" }, + { 100, "棋子" }, + { 101, "卡包" }, + { 102, "限时事件" }, + { 103, "小猪存钱罐" }, + { 104, "万能卡" }, + { 105, "头像框" }, + { 106, "活动代币" }, + { 107, "竞赛游戏代币" }, + { 108, "Pet Playroom拜访道具" }, + { 109, "表情" }, + { 110, "头像" }, + { 111, "Playroom装饰" }, + { 112, "Playroom服装" }, + { 113, "Playroom装饰套装" }, + { 114, "Playroom服装套装" }, + { 115, "Playroom道具宝箱" }, + { 116, "活动通行证代币道具" } + }; + + private string docsRootPath = ""; + private List allItemDataList = new List(); + private List filteredItemDataList = new List(); + private Dictionary limitedTimeEventDict = new Dictionary(); // Id -> Name + private Dictionary avatarDict = new Dictionary(); // Id -> AvatarData + private Dictionary faceDict = new Dictionary(); // Id -> FaceData + private Dictionary emojiDict = new Dictionary(); // Id -> EmojiData + private Dictionary> languageDict = new Dictionary>(); + private List allArtTables = new List(); + private Dictionary> artTablesByFolder = new Dictionary>(); // 按文件夹分组的表格 + + private ArtTableSO headFrameResourceSO; + private ArtTableSO headResourceSO; + private ArtTableSO emojiResourceSO; + + // Res选择状态缓存 (ItemData的Id -> 选择的组名) + private Dictionary resSelectedFolderCache = new Dictionary(); + + private Vector2 scrollPosition; + private bool isDataLoaded = false; + + // 筛选和分页 + private int selectedIType = -1; + private int currentPage = 0; + private int itemsPerPage = 20; + private int totalPages = 0; + + [MenuItem("策划工具/Item配置")] + public static void ShowWindow() + { + var window = GetWindow("Item配置"); + window.minSize = new Vector2(1200, 700); + window.Show(); + } + + private void OnEnable() + { + // 读取上次保存的路径 + if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY)) + { + docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY); + } + } + + private void OnGUI() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("Item配置工具", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + // Docs路径选择 + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + docsRootPath = EditorGUILayout.TextField("路径", docsRootPath); + if (GUILayout.Button("选择文件夹", GUILayout.Width(100))) + { + string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + docsRootPath = selectedPath; + EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath); + } + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("加载配置数据", GUILayout.Height(30))) + { + LoadData(); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(5); + + // 数据编辑区域 + if (isDataLoaded) + { + DrawFilterAndDataEditor(); + } + else + { + EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info); + } + } + + /// + /// 加载配置数据 + /// + private void LoadData() + { + try + { + // 校验路径 + if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath)) + { + EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定"); + return; + } + + // 1. 检查Docs是否为Git仓库并更新 + if (!CheckAndUpdateDocsRepository()) + { + return; + } + + // 2. 检查Design_SubModule分支 + if (!CheckAndSwitchDesignSubModuleBranch()) + { + return; + } + + // 3. 加载所有Art_SO资源 + LoadAllArtTableSO(); + + // 4. 加载语言表 + LoadLanguageData(); + + // 5. 加载LimitedTimeEvent表 + LoadLimitedTimeEventData(); + + // 6. 加载Avatar表 + LoadAvatarData(); + + // 7. 加载Face表 + LoadFaceData(); + + // 8. 加载Emoji表 + LoadEmojiData(); + + // 9. 加载Item表 + LoadItemExcel(); + + isDataLoaded = true; + EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定"); + isDataLoaded = false; + } + } + + /// + /// 执行Git命令 + /// + private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success) + { + try + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = Process.Start(startInfo)) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + success = process.ExitCode == 0; + return success ? output : error; + } + } + catch (Exception e) + { + success = false; + return $"执行Git命令失败: {e.Message}"; + } + } + + /// + /// 检查并更新Docs仓库 + /// + private bool CheckAndUpdateDocsRepository() + { + string gitPath = Path.Combine(docsRootPath, ".git"); + if (!Directory.Exists(gitPath)) + { + EditorUtility.DisplayDialog("错误", + $"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f); + + string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess); + + if (!fetchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Fetch失败", + $"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f); + + string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess); + + if (!statusSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Status失败", + $"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态", + "确定"); + return false; + } + + if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed")) + { + EditorUtility.ClearProgressBar(); + bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改", + "Docs仓库中有未提交的更改,这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置?(不推荐)", + "继续(不推荐)", "取消,前往处理"); + + if (!proceed) + { + Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改"); + return false; + } + } + + if (statusResult.Contains("Your branch is behind")) + { + EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f); + + string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess); + + EditorUtility.ClearProgressBar(); + + if (!pullSuccess) + { + if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict")) + { + EditorUtility.DisplayDialog("拉取失败:存在冲突", + $"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作", + "确定"); + } + else + { + EditorUtility.DisplayDialog("拉取失败", + $"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题", + "确定"); + } + return false; + } + + Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}"); + EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Docs仓库已是最新版本"); + } + + return true; + } + + /// + /// 检查并切换Design_SubModule到main分支 + /// + private bool CheckAndSwitchDesignSubModuleBranch() + { + string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH); + designSubModulePath = Path.GetFullPath(designSubModulePath); + + if (!Directory.Exists(designSubModulePath)) + { + EditorUtility.DisplayDialog("错误", + $"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化", + "确定"); + return false; + } + + EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f); + + string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess); + + if (!branchSuccess) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Git Branch失败", + $"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态", + "确定"); + return false; + } + + string currentBranch = branchResult.Trim(); + + if (currentBranch != "main") + { + EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f); + + string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess); + + EditorUtility.ClearProgressBar(); + + if (!checkoutSuccess) + { + EditorUtility.DisplayDialog("切换分支失败", + $"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支", + "确定"); + return false; + } + + Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支"); + } + else + { + EditorUtility.ClearProgressBar(); + Debug.Log("Design_SubModule已在main分支"); + } + + return true; + } + + /// + /// 加载所有Art_SO资源 + /// + private void LoadAllArtTableSO() + { + allArtTables.Clear(); + artTablesByFolder.Clear(); + + string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { ART_SO_PATH }); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + ArtTableSO tableSO = AssetDatabase.LoadAssetAtPath(path); + if (tableSO != null) + { + allArtTables.Add(tableSO); + + // 按文件夹分组 + string folder = GetRelativeFolder(path); + if (!artTablesByFolder.ContainsKey(folder)) + { + artTablesByFolder[folder] = new List(); + } + artTablesByFolder[folder].Add(tableSO); + } + } + + // 加载特定资源 + headFrameResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "HeadFrameResource"); + headResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "HeadResource"); + emojiResourceSO = allArtTables.FirstOrDefault(x => x.TableName == "EmojiResource"); + + Debug.Log($"加载了 {allArtTables.Count} 个ArtTableSO资源,分为 {artTablesByFolder.Count} 个组"); + } + + /// + /// 获取相对文件夹路径 + /// + private string GetRelativeFolder(string assetPath) + { + string relativePath = assetPath.Replace(ART_SO_PATH + "/", ""); + int lastSlash = relativePath.LastIndexOf('/'); + if (lastSlash >= 0) + { + return relativePath.Substring(0, lastSlash); + } + return "根目录"; + } + + /// + /// 加载语言数据 + /// + private void LoadLanguageData() + { + languageDict.Clear(); + + string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME); + if (!File.Exists(languageExcelPath)) + { + Debug.LogWarning($"未找到语言表: {languageExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(languageExcelPath))) + { + var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}"); + return; + } + + // 查找Key列和语言列 + int keyColumnIndex = -1; + int zhCNColumnIndex = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "key") + { + keyColumnIndex = col; + } + else if (header == "zh_CN") + { + zhCNColumnIndex = col; + } + } + + if (keyColumnIndex < 0) + { + Debug.LogWarning("语言表中未找到Key列"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string key = worksheet.Cells[row, keyColumnIndex].Text; + if (string.IsNullOrEmpty(key)) continue; + + if (!languageDict.ContainsKey(key)) + { + languageDict[key] = new Dictionary(); + } + + if (zhCNColumnIndex > 0) + { + languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text; + } + } + } + } + + /// + /// 加载限时事件数据 + /// + private void LoadLimitedTimeEventData() + { + limitedTimeEventDict.Clear(); + + string eventExcelPath = Path.Combine(docsRootPath, "config", LIMITED_TIME_EVENT_EXCEL_NAME); + if (!File.Exists(eventExcelPath)) + { + Debug.LogWarning($"未找到限时事件表: {eventExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(eventExcelPath))) + { + var worksheet = package.Workbook.Worksheets[EVENT_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"限时事件表中未找到Sheet: {EVENT_SHEET_NAME}"); + return; + } + + // 查找Id和Name列 + int idCol = -1, nameCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "Name") nameCol = col; + } + + if (idCol < 0 || nameCol < 0) + { + Debug.LogWarning("限时事件表结构不正确"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (int.TryParse(idText, out int id)) + { + string name = worksheet.Cells[row, nameCol].Text; + limitedTimeEventDict[id] = name; + } + } + } + } + + /// + /// 加载Avatar数据 + /// + private void LoadAvatarData() + { + avatarDict.Clear(); + + string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME); + if (!File.Exists(avatarExcelPath)) + { + Debug.LogWarning($"未找到Avatar表: {avatarExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(avatarExcelPath))) + { + var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"Avatar表中未找到Sheet: {AVATAR_SHEET_NAME}"); + return; + } + + // 查找列 + int idCol = -1, nameKeyCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "NameKey") nameKeyCol = col; + else if (header == "Icon") iconCol = col; + } + + if (idCol < 0 || nameKeyCol < 0 || iconCol < 0) + { + Debug.LogWarning("Avatar表结构不正确"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (int.TryParse(idText, out int id)) + { + string nameKey = worksheet.Cells[row, nameKeyCol].Text; + string iconText = worksheet.Cells[row, iconCol].Text; + int.TryParse(iconText, out int iconId); + + avatarDict[id] = new AvatarData + { + Id = id, + NameKey = nameKey, + IconId = iconId + }; + } + } + } + } + + /// + /// 加载Face数据 + /// + private void LoadFaceData() + { + faceDict.Clear(); + + string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME); + if (!File.Exists(faceExcelPath)) + { + Debug.LogWarning($"未找到Face表: {faceExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(faceExcelPath))) + { + var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"Face表中未找到Sheet: {FACE_SHEET_NAME}"); + return; + } + + // 查找列 + int idCol = -1, nameKeyCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "NameKey") nameKeyCol = col; + else if (header == "Icon") iconCol = col; + } + + if (idCol < 0 || nameKeyCol < 0 || iconCol < 0) + { + Debug.LogWarning("Face表结构不正确"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (int.TryParse(idText, out int id)) + { + string nameKey = worksheet.Cells[row, nameKeyCol].Text; + string iconText = worksheet.Cells[row, iconCol].Text; + int.TryParse(iconText, out int iconId); + + faceDict[id] = new FaceData + { + Id = id, + NameKey = nameKey, + IconId = iconId + }; + } + } + } + } + + /// + /// 加载Emoji数据 + /// + private void LoadEmojiData() + { + emojiDict.Clear(); + + string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME); + if (!File.Exists(emojiExcelPath)) + { + Debug.LogWarning($"未找到Emoji表: {emojiExcelPath}"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(emojiExcelPath))) + { + var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME]; + if (worksheet == null) + { + Debug.LogWarning($"Emoji表中未找到Sheet: {EMOJI_SHEET_NAME}"); + return; + } + + // 查找列 + int idCol = -1, nameKeyCol = -1, iconCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "NameKey") nameKeyCol = col; + else if (header == "Icon") iconCol = col; + } + + if (idCol < 0 || nameKeyCol < 0 || iconCol < 0) + { + Debug.LogWarning("Emoji表结构不正确"); + return; + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (int.TryParse(idText, out int id)) + { + string nameKey = worksheet.Cells[row, nameKeyCol].Text; + string iconText = worksheet.Cells[row, iconCol].Text; + int.TryParse(iconText, out int iconId); + + emojiDict[id] = new EmojiData + { + Id = id, + NameKey = nameKey, + IconId = iconId + }; + } + } + } + } + + /// + /// 加载Item.xlsx + /// + private void LoadItemExcel() + { + allItemDataList.Clear(); + + string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME); + if (!File.Exists(itemExcelPath)) + { + throw new Exception($"未找到Item配置文件: {itemExcelPath}"); + } + + using (var package = new ExcelPackage(new FileInfo(itemExcelPath))) + { + var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME]; + if (worksheet == null) + { + throw new Exception($"Item.xlsx中未找到Sheet: {ITEM_SHEET_NAME}"); + } + + // 读取表头(第1行) + int idCol = -1, nameCol = -1, iTypeCol = -1, effectCol = -1, resCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "Name") nameCol = col; + else if (header == "IType") iTypeCol = col; + else if (header == "Effect") effectCol = col; + else if (header == "Res") resCol = col; + } + + if (idCol < 0 || nameCol < 0 || iTypeCol < 0) + { + throw new Exception("Item.xlsx表结构不正确,缺少必要列(Id/Name/IType)"); + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + string name = worksheet.Cells[row, nameCol].Text; + string iTypeText = worksheet.Cells[row, iTypeCol].Text; + string effect = effectCol > 0 ? worksheet.Cells[row, effectCol].Text : ""; + string res = resCol > 0 ? worksheet.Cells[row, resCol].Text : ""; + + if (!int.TryParse(iTypeText, out int iType)) continue; + + var itemData = new ItemData + { + Id = id, + Name = name, + IType = iType, + Effect = effect, + Res = res + }; + + allItemDataList.Add(itemData); + } + } + } + + /// + /// 绘制筛选和数据编辑器 + /// + private void DrawFilterAndDataEditor() + { + EditorGUILayout.BeginVertical("box"); + + // IType筛选 + EditorGUILayout.LabelField("Item类型筛选", EditorStyles.boldLabel); + EditorGUILayout.BeginHorizontal(); + + // 创建下拉列表选项 + var typeOptions = new List { "请选择类型..." }; + var typeValues = new List { -1 }; + + // 排除103、104、108和97、98、111-115 + foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key)) + { + // 跳过不需要显示的类型 + if (kvp.Key == 103 || kvp.Key == 104 || kvp.Key == 108 || + kvp.Key == 97 || kvp.Key == 98 || + (kvp.Key >= 111 && kvp.Key <= 115)) + { + continue; + } + + typeOptions.Add(kvp.Value); + typeValues.Add(kvp.Key); + } + + int currentIndex = typeValues.IndexOf(selectedIType); + if (currentIndex < 0) currentIndex = 0; + + int newIndex = EditorGUILayout.Popup("选择类型", currentIndex, typeOptions.ToArray()); + if (newIndex != currentIndex) + { + selectedIType = typeValues[newIndex]; + FilterItemsByType(); + currentPage = 0; + } + + if (selectedIType > 0) + { + EditorGUILayout.LabelField($"当前类型ID: {selectedIType}", GUILayout.Width(150)); + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(5); + + // 如果已选择类型,显示数据列表 + if (selectedIType > 0) + { + DrawItemDataList(); + } + else + { + EditorGUILayout.HelpBox("请先选择Item类型进行筛选", MessageType.Info); + } + } + + /// + /// 根据IType筛选数据 + /// + private void FilterItemsByType() + { + filteredItemDataList = allItemDataList.Where(x => x.IType == selectedIType).ToList(); + totalPages = Mathf.CeilToInt((float)filteredItemDataList.Count / itemsPerPage); + } + + /// + /// 绘制Item数据列表 + /// + private void DrawItemDataList() + { + EditorGUILayout.BeginVertical("box"); + + // 顶部工具栏 + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"数据列表(共 {filteredItemDataList.Count} 条)", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.cyan; + if (GUILayout.Button("+ 添加Item", GUILayout.Height(25), GUILayout.Width(100))) + { + AddNewItem(); + } + GUI.backgroundColor = Color.white; + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(3); + + // 分页控制 + DrawPagination(); + + EditorGUILayout.Space(3); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(400)); + + // 表头 + DrawTableHeader(); + + // 数据行 + int startIndex = currentPage * itemsPerPage; + int endIndex = Mathf.Min(startIndex + itemsPerPage, filteredItemDataList.Count); + + for (int i = startIndex; i < endIndex; i++) + { + DrawItemDataRow(filteredItemDataList[i]); + } + + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(5); + + // 分页控制(底部) + DrawPagination(); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // 保存按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + GUI.backgroundColor = Color.green; + if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200))) + { + SaveData(); + } + GUI.backgroundColor = Color.white; + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + + /// + /// 绘制分页控制 + /// + private void DrawPagination() + { + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.LabelField("每页显示:", GUILayout.Width(70)); + itemsPerPage = EditorGUILayout.IntField(itemsPerPage, GUILayout.Width(50)); + itemsPerPage = Mathf.Max(1, itemsPerPage); + + GUILayout.Space(20); + + GUI.enabled = currentPage > 0; + if (GUILayout.Button("第一页", GUILayout.Width(60))) + { + currentPage = 0; + } + if (GUILayout.Button("上一页", GUILayout.Width(60))) + { + currentPage--; + } + GUI.enabled = true; + + EditorGUILayout.LabelField($"第 {currentPage + 1} / {Mathf.Max(1, totalPages)} 页", GUILayout.Width(100)); + + int newPage = EditorGUILayout.IntField(currentPage + 1, GUILayout.Width(50)) - 1; + if (newPage != currentPage && newPage >= 0 && newPage < totalPages) + { + currentPage = newPage; + } + + GUI.enabled = currentPage < totalPages - 1; + if (GUILayout.Button("下一页", GUILayout.Width(60))) + { + currentPage++; + } + if (GUILayout.Button("最后一页", GUILayout.Width(70))) + { + currentPage = totalPages - 1; + } + GUI.enabled = true; + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + + /// + /// 绘制表头 + /// + private void DrawTableHeader() + { + EditorGUILayout.BeginHorizontal("box"); + EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("Name", EditorStyles.boldLabel, GUILayout.Width(120)); + EditorGUILayout.LabelField("IType", EditorStyles.boldLabel, GUILayout.Width(50)); + EditorGUILayout.LabelField("Effect", EditorStyles.boldLabel, GUILayout.Width(200)); + EditorGUILayout.LabelField("Res", EditorStyles.boldLabel, GUILayout.Width(200)); + EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100)); + EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.EndHorizontal(); + } + + /// + /// 绘制单行数据 + /// + private void DrawItemDataRow(ItemData data) + { + EditorGUILayout.BeginHorizontal("box"); + + // Id(可编辑) + data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50)); + + // Name(可编辑) + data.Name = EditorGUILayout.TextField(data.Name, GUILayout.Width(120)); + + // IType(只读) + GUI.enabled = false; + EditorGUILayout.IntField(data.IType, GUILayout.Width(50)); + GUI.enabled = true; + + // Effect(根据IType不同显示不同内容) + DrawEffectField(data); + + // Res(资源选择) + DrawResField(data); + + // 预览 + DrawResPreview(data); + + // 删除按钮 + GUI.backgroundColor = Color.red; + if (GUILayout.Button("删除", GUILayout.Width(60))) + { + if (EditorUtility.DisplayDialog("确认删除", $"确定要删除ID为{data.Id}的Item吗?", "删除", "取消")) + { + filteredItemDataList.Remove(data); + allItemDataList.Remove(data); + FilterItemsByType(); // 重新计算分页 + } + } + GUI.backgroundColor = Color.white; + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 绘制Effect字段(统一文本编辑) + /// + private void DrawEffectField(ItemData data) + { + EditorGUILayout.BeginVertical(GUILayout.Width(200)); + + // 当前使用统一的文本编辑框处理所有类型 + EditorGUI.BeginChangeCheck(); + string newEffect = EditorGUILayout.TextField(data.Effect ?? "", GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + data.Effect = newEffect; + } + + /* + // 以下是之前按类型特殊处理的代码,已注释保留,方便之后还原特殊处理 + switch (data.IType) + { + case 100: // 棋子 - 不显示 + case 1: // 能量 + case 2: // 星星 + case 3: // 钻石 + EditorGUILayout.LabelField("-", GUILayout.Width(200)); + break; + + case 101: // 卡包 - 只读 + case 106: // 活动代币 - 没有Effect + case 107: // 竞赛游戏代币 - 没有Effect + case 99: // 背包道具 - 没有Effect + EditorGUILayout.LabelField(string.IsNullOrEmpty(data.Effect) ? "-" : data.Effect, GUILayout.Width(200)); + break; + + case 102: // 限时事件 + DrawLimitedTimeEventEffect(data); + break; + + case 105: // 头像框 - 已改用Res字段 + EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200)); + break; + + case 110: // 头像 - 已改用Res字段 + EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200)); + break; + + case 109: // 表情 - 已改用Res字段 + EditorGUILayout.LabelField("请使用Res字段配置", GUILayout.Width(200)); + break; + + default: + data.Effect = EditorGUILayout.TextField(data.Effect, GUILayout.Width(200)); + break; + } + */ + + EditorGUILayout.EndVertical(); + } + + /// + /// 绘制限时事件Effect + /// + private void DrawLimitedTimeEventEffect(ItemData data) + { + // Effect格式: "eventId,seconds" + string[] parts = data.Effect.Split(','); + int eventId = 0; + int seconds = 0; + + if (parts.Length >= 1) int.TryParse(parts[0], out eventId); + if (parts.Length >= 2) int.TryParse(parts[1], out seconds); + + EditorGUILayout.BeginHorizontal(); + + // 事件选择下拉框 + var eventOptions = new List { "请选择..." }; + var eventIds = new List { 0 }; + + foreach (var kvp in limitedTimeEventDict.OrderBy(x => x.Key)) + { + eventOptions.Add(kvp.Value); + eventIds.Add(kvp.Key); + } + + int currentIndex = eventIds.IndexOf(eventId); + if (currentIndex < 0) currentIndex = 0; + + int newIndex = EditorGUILayout.Popup(currentIndex, eventOptions.ToArray(), GUILayout.Width(120)); + int newEventId = eventIds[newIndex]; + + // 秒数输入 + int newSeconds = EditorGUILayout.IntField(seconds, GUILayout.Width(60)); + + // 显示时间格式 + int minutes = newSeconds / 60; + int secs = newSeconds % 60; + GUI.enabled = false; + EditorGUILayout.TextField($"{minutes:D2}:{secs:D2}", GUILayout.Width(50)); + GUI.enabled = true; + + EditorGUILayout.EndHorizontal(); + + // 验证提示 + if (newEventId > 0 && !limitedTimeEventDict.ContainsKey(newEventId)) + { + EditorGUILayout.HelpBox("没有这个限时事件", MessageType.Error); + } + + // 更新数据 + data.Effect = $"{newEventId},{newSeconds}"; + } + + /// + /// 绘制头像框Effect + /// + private void DrawHeadFrameEffect(ItemData data) + { + // Effect格式: "avatarId,0" + string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(','); + int avatarId = -1; + + if (parts.Length >= 1) int.TryParse(parts[0], out avatarId); + + // 创建下拉选项 + var options = new List { "请选择..." }; + var ids = new List { -1 }; + + foreach (var kvp in avatarDict.OrderBy(x => x.Key)) + { + string name = kvp.Value.NameKey; + if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN")) + { + name = languageDict[kvp.Value.NameKey]["zh_CN"]; + } + options.Add($"{name} (ID:{kvp.Key})"); + ids.Add(kvp.Key); + } + + int currentIndex = ids.IndexOf(avatarId); + if (currentIndex < 0) currentIndex = 0; + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + int newAvatarId = ids[newIndex]; + data.Effect = $"{newAvatarId},0"; + } + } + + /// + /// 绘制头像Effect + /// + private void DrawFaceEffect(ItemData data) + { + // Effect格式: "faceId,0" + string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(','); + int faceId = -1; + + if (parts.Length >= 1) int.TryParse(parts[0], out faceId); + + // 创建下拉选项 + var options = new List { "请选择..." }; + var ids = new List { -1 }; + + foreach (var kvp in faceDict.OrderBy(x => x.Key)) + { + string name = kvp.Value.NameKey; + if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN")) + { + name = languageDict[kvp.Value.NameKey]["zh_CN"]; + } + options.Add($"{name} (ID:{kvp.Key})"); + ids.Add(kvp.Key); + } + + int currentIndex = ids.IndexOf(faceId); + if (currentIndex < 0) currentIndex = 0; + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + int newFaceId = ids[newIndex]; + data.Effect = $"{newFaceId},0"; + } + } + + /// + /// 绘制表情Effect + /// + private void DrawEmojiEffect(ItemData data) + { + // Effect格式: "emojiId,0" + string[] parts = string.IsNullOrEmpty(data.Effect) ? new string[0] : data.Effect.Split(','); + int emojiId = -1; + + if (parts.Length >= 1) int.TryParse(parts[0], out emojiId); + + // 创建下拉选项 + var options = new List { "请选择..." }; + var ids = new List { -1 }; + + foreach (var kvp in emojiDict.OrderBy(x => x.Key)) + { + string name = kvp.Value.NameKey; + if (languageDict.ContainsKey(kvp.Value.NameKey) && languageDict[kvp.Value.NameKey].ContainsKey("zh_CN")) + { + name = languageDict[kvp.Value.NameKey]["zh_CN"]; + } + options.Add($"{name} (ID:{kvp.Key})"); + ids.Add(kvp.Key); + } + + int currentIndex = ids.IndexOf(emojiId); + if (currentIndex < 0) currentIndex = 0; + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup(currentIndex, options.ToArray(), GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + int newEmojiId = ids[newIndex]; + data.Effect = $"{newEmojiId},0"; + } + } + + /// + /// 绘制Res字段(三级选择:组->表->Item) + /// + private void DrawResField(ItemData data) + { + EditorGUILayout.BeginVertical(GUILayout.Width(200)); + + // Res格式: "TableId,ItemId" + string[] parts = string.IsNullOrEmpty(data.Res) ? new string[0] : data.Res.Split(','); + int tableId = -1; + int itemId = -1; + + if (parts.Length > 0) int.TryParse(parts[0], out tableId); + if (parts.Length > 1) int.TryParse(parts[1], out itemId); + + // 查找当前表格和所属文件夹 + ArtTableSO currentTable = null; + string currentFolder = ""; + + // 优先使用缓存的组选择 + if (resSelectedFolderCache.ContainsKey(data.Id)) + { + currentFolder = resSelectedFolderCache[data.Id]; + } + + // 如果有Res数据,从Res推导组和表 + if (tableId >= 0) + { + currentTable = allArtTables.FirstOrDefault(x => x.TableId == tableId); + if (currentTable != null) + { + string tablePath = AssetDatabase.GetAssetPath(currentTable); + string folderFromRes = GetRelativeFolder(tablePath); + // 如果没有缓存或缓存不一致,更新缓存 + if (string.IsNullOrEmpty(currentFolder) || currentFolder != folderFromRes) + { + currentFolder = folderFromRes; + resSelectedFolderCache[data.Id] = currentFolder; + } + } + } + + // 第一级:选择组 + var folderOptions = new List { "未选择" }; + folderOptions.AddRange(artTablesByFolder.Keys.OrderBy(k => k)); + + int currentFolderIndex = 0; + if (!string.IsNullOrEmpty(currentFolder)) + { + currentFolderIndex = folderOptions.IndexOf(currentFolder); + if (currentFolderIndex < 0) currentFolderIndex = 0; + } + + EditorGUI.BeginChangeCheck(); + int newFolderIndex = EditorGUILayout.Popup(currentFolderIndex, folderOptions.ToArray(), GUILayout.Width(200)); + bool folderChanged = EditorGUI.EndChangeCheck(); + + if (folderChanged) + { + if (newFolderIndex > 0 && newFolderIndex < folderOptions.Count) + { + currentFolder = folderOptions[newFolderIndex]; + // 保存到缓存 + resSelectedFolderCache[data.Id] = currentFolder; + // 组改变时,清空表和Item选择,并清空Res数据 + currentTable = null; + tableId = -1; + itemId = -1; + data.Res = ""; // 清空Res,避免旧数据干扰 + } + else if (newFolderIndex == 0) + { + // 选择"未选择",清空所有 + currentFolder = ""; + if (resSelectedFolderCache.ContainsKey(data.Id)) + { + resSelectedFolderCache.Remove(data.Id); + } + currentTable = null; + data.Res = ""; + EditorGUILayout.EndVertical(); + return; + } + } + else if (newFolderIndex > 0 && newFolderIndex < folderOptions.Count) + { + currentFolder = folderOptions[newFolderIndex]; + } + else if (newFolderIndex == 0) + { + // 当前是"未选择"状态 + EditorGUILayout.EndVertical(); + return; + } + + // 第二级:选择表 + List tablesInFolder = artTablesByFolder.ContainsKey(currentFolder) + ? artTablesByFolder[currentFolder] + : new List(); + + var tableOptions = new List { "未选择" }; + tableOptions.AddRange(tablesInFolder.Select(t => $"{t.TableName}(ID:{t.TableId})")); + + int currentTableIndex = 0; + if (currentTable != null && !folderChanged) + { + int foundIndex = tablesInFolder.FindIndex(t => t.TableId == currentTable.TableId); + if (foundIndex >= 0) + { + currentTableIndex = foundIndex + 1; // +1 因为有"未选择"选项 + } + } + + EditorGUI.BeginChangeCheck(); + int newTableIndex = EditorGUILayout.Popup(currentTableIndex, tableOptions.ToArray(), GUILayout.Width(200)); + bool tableSelectionChanged = EditorGUI.EndChangeCheck(); + + if (tableSelectionChanged) + { + if (newTableIndex > 0 && newTableIndex <= tablesInFolder.Count) + { + currentTable = tablesInFolder[newTableIndex - 1]; // -1 因为有"未选择"选项 + // 表格改变时,清空Item选择 + data.Res = $"{currentTable.TableId},-1"; + itemId = -1; + } + else if (newTableIndex == 0) + { + // 选择"未选择" + currentTable = null; + data.Res = ""; + EditorGUILayout.EndVertical(); + return; + } + } + else if (newTableIndex > 0 && newTableIndex <= tablesInFolder.Count) + { + currentTable = tablesInFolder[newTableIndex - 1]; + } + else + { + // 当前表是"未选择"状态 + EditorGUILayout.EndVertical(); + return; + } + + // 第三级:选择Item + if (currentTable != null && currentTable.Items != null && currentTable.Items.Count > 0) + { + var itemOptions = new List { "未选择" }; + itemOptions.AddRange(currentTable.Items.Select(item => $"{item.Name}(ID:{item.Id})")); + + int currentItemIndex = 0; + if (itemId >= 0 && !tableSelectionChanged) + { + int foundIndex = currentTable.Items.FindIndex(item => item.Id == itemId); + if (foundIndex >= 0) + { + currentItemIndex = foundIndex + 1; // +1 因为有"未选择"选项 + } + } + + EditorGUI.BeginChangeCheck(); + int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(200)); + if (EditorGUI.EndChangeCheck()) + { + if (newItemIndex > 0 && newItemIndex <= currentTable.Items.Count) + { + data.Res = $"{currentTable.TableId},{currentTable.Items[newItemIndex - 1].Id}"; // -1 因为有"未选择"选项 + } + else if (newItemIndex == 0) + { + // 选择"未选择" + data.Res = $"{currentTable.TableId},-1"; + } + } + } + + EditorGUILayout.EndVertical(); + } + + /// + /// 绘制资源预览(统一使用Res字段) + /// + private void DrawResPreview(ItemData data) + { + Sprite previewSprite = null; + string tipMessage = ""; + + // 统一从Res字段获取预览 + previewSprite = GetResPreview(data); + + if (previewSprite != null) + { + Rect rect = GUILayoutUtility.GetRect(80, 80, GUILayout.Width(100)); + GUI.DrawTexture(rect, previewSprite.texture, ScaleMode.ScaleToFit); + } + else + { + if (!string.IsNullOrEmpty(tipMessage)) + { + GUILayout.Box(tipMessage, GUILayout.Width(100), GUILayout.Height(80)); + } + else + { + GUILayout.Box("无预览", GUILayout.Width(100), GUILayout.Height(80)); + } + } + } + + /// + /// 获取头像框预览 + /// + private Sprite GetHeadFramePreview(ItemData data) + { + if (headFrameResourceSO == null) return null; + if (string.IsNullOrEmpty(data.Effect)) return null; + + string[] parts = data.Effect.Split(','); + if (parts.Length < 1 || !int.TryParse(parts[0], out int avatarId)) return null; + if (avatarId < 0) return null; + + if (!avatarDict.ContainsKey(avatarId)) return null; + + int iconId = avatarDict[avatarId].IconId; + var item = headFrameResourceSO.Items.FirstOrDefault(x => x.Id == iconId); + + return item?.Sprite; + } + + /// + /// 获取头像预览 + /// + private Sprite GetFacePreview(ItemData data) + { + if (headResourceSO == null) return null; + if (string.IsNullOrEmpty(data.Effect)) return null; + + string[] parts = data.Effect.Split(','); + if (parts.Length < 1 || !int.TryParse(parts[0], out int faceId)) return null; + if (faceId < 0) return null; + + if (!faceDict.ContainsKey(faceId)) return null; + + int iconId = faceDict[faceId].IconId; + var item = headResourceSO.Items.FirstOrDefault(x => x.Id == iconId); + + return item?.Sprite; + } + + /// + /// 获取表情预览 + /// + private Sprite GetEmojiPreview(ItemData data) + { + if (emojiResourceSO == null) return null; + if (string.IsNullOrEmpty(data.Effect)) return null; + + string[] parts = data.Effect.Split(','); + if (parts.Length < 1 || !int.TryParse(parts[0], out int emojiId)) return null; + if (emojiId < 0) return null; + + if (!emojiDict.ContainsKey(emojiId)) return null; + + int iconId = emojiDict[emojiId].IconId; + var item = emojiResourceSO.Items.FirstOrDefault(x => x.Id == iconId); + + return item?.Sprite; + } + + /// + /// 获取Res资源预览 + /// + private Sprite GetResPreview(ItemData data) + { + if (string.IsNullOrEmpty(data.Res)) return null; + + string[] parts = data.Res.Split(','); + if (parts.Length < 2) return null; + + if (!int.TryParse(parts[0], out int tableId)) return null; + if (!int.TryParse(parts[1], out int itemId)) return null; + + var table = allArtTables.FirstOrDefault(x => x.TableId == tableId); + if (table == null) return null; + + var item = table.Items.FirstOrDefault(x => x.Id == itemId); + return item?.Sprite; + } + + /// + /// 添加新Item + /// + private void AddNewItem() + { + // 找到当前IType的最大ID + int maxId = 0; + foreach (var item in allItemDataList.Where(x => x.IType == selectedIType)) + { + if (item.Id > maxId) maxId = item.Id; + } + + // 自增ID并检查重复 + int newId = maxId + 1; + while (allItemDataList.Any(x => x.Id == newId)) + { + newId++; + } + + var newItem = new ItemData + { + Id = newId, + Name = $"新Item_{newId}", + IType = selectedIType, + Effect = "", + Res = "" + }; + + allItemDataList.Add(newItem); + FilterItemsByType(); + + // 跳转到最后一页 + currentPage = totalPages - 1; + } + + /// + /// 保存数据到Excel + /// + private void SaveData() + { + try + { + // ID重复检查 + var duplicateIds = allItemDataList.GroupBy(x => x.Id) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + if (duplicateIds.Count > 0) + { + EditorUtility.DisplayDialog("错误", + $"存在重复的ID: {string.Join(", ", duplicateIds)}\n请修正后再保存", + "确定"); + return; + } + + // 验证限时事件 + foreach (var item in allItemDataList.Where(x => x.IType == 102)) + { + if (!string.IsNullOrEmpty(item.Effect)) + { + string[] parts = item.Effect.Split(','); + if (parts.Length >= 1 && int.TryParse(parts[0], out int eventId)) + { + if (eventId > 0 && !limitedTimeEventDict.ContainsKey(eventId)) + { + EditorUtility.DisplayDialog("错误", + $"ID {item.Id} 的限时事件ID {eventId} 不存在\n请修正后再保存", + "确定"); + return; + } + } + } + } + + string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME); + + using (var package = new ExcelPackage(new FileInfo(itemExcelPath))) + { + var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME]; + + // 找到列索引 + int idCol = -1, nameCol = -1, iTypeCol = -1, effectCol = -1, resCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "Name") nameCol = col; + else if (header == "IType") iTypeCol = col; + else if (header == "Effect") effectCol = col; + else if (header == "Res") resCol = col; + } + + // 创建Id到行号的映射(读取现有数据) + var existingRowMap = new Dictionary(); + int existingRowCount = worksheet.Dimension.Rows; + + for (int row = 3; row <= existingRowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (!string.IsNullOrEmpty(idText) && int.TryParse(idText, out int id)) + { + existingRowMap[id] = row; + } + } + + // 更新或新增数据 + int nextNewRow = existingRowCount + 1; + + foreach (var item in allItemDataList.OrderBy(x => x.Id)) + { + int targetRow; + + // 如果该ID已存在,更新对应行;否则添加新行 + if (existingRowMap.ContainsKey(item.Id)) + { + targetRow = existingRowMap[item.Id]; + } + else + { + targetRow = nextNewRow; + nextNewRow++; + } + + // 只更新我们管理的列 + worksheet.Cells[targetRow, idCol].Value = item.Id; + worksheet.Cells[targetRow, nameCol].Value = item.Name; + worksheet.Cells[targetRow, iTypeCol].Value = item.IType; + + if (effectCol > 0) + { + worksheet.Cells[targetRow, effectCol].Value = item.Effect; + } + + if (resCol > 0) + { + worksheet.Cells[targetRow, resCol].Value = item.Res; + } + } + + // 删除已不存在的数据行(只清空我们管理的列) + var currentIds = allItemDataList.Select(x => x.Id).ToHashSet(); + for (int row = 3; row <= existingRowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (!string.IsNullOrEmpty(idText) && int.TryParse(idText, out int id)) + { + if (!currentIds.Contains(id)) + { + // 只清空我们管理的列,保留其他列 + worksheet.Cells[row, idCol].Value = null; + worksheet.Cells[row, nameCol].Value = null; + worksheet.Cells[row, iTypeCol].Value = null; + if (effectCol > 0) worksheet.Cells[row, effectCol].Value = null; + if (resCol > 0) worksheet.Cells[row, resCol].Value = null; + } + } + } + + package.Save(); + } + + EditorUtility.DisplayDialog("成功", "配置已保存到Excel文件", "确定"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"保存失败: {e.Message}\n{e.StackTrace}", "确定"); + } + } + + /// + /// Item数据类 + /// + private class ItemData + { + public int Id; + public string Name; + public int IType; + public string Effect; + public string Res; + } + + /// + /// Avatar数据类 + /// + private class AvatarData + { + public int Id; + public string NameKey; + public int IconId; + } + + /// + /// Face数据类 + /// + private class FaceData + { + public int Id; + public string NameKey; + public int IconId; + } + + /// + /// Emoji数据类 + /// + private class EmojiData + { + public int Id; + public string NameKey; + public int IconId; + } + } +} diff --git a/Scripts/Editor/Design_Tools/ItemConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs.meta new file mode 100644 index 0000000..873c7ac --- /dev/null +++ b/Scripts/Editor/Design_Tools/ItemConfigEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2cbfd5e054af7604e837e9ececb2a15c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/Scene.meta b/Scripts/Editor/Design_Tools/Scene.meta new file mode 100644 index 0000000..beb91bc --- /dev/null +++ b/Scripts/Editor/Design_Tools/Scene.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0f94b023b059a5349a6ecd1811ad9334 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs new file mode 100644 index 0000000..bd3e9df --- /dev/null +++ b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs @@ -0,0 +1,1585 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEngine; +using OfficeOpenXml; +using ArtResource; +using Debug = UnityEngine.Debug; + +namespace DesignTools.Scene +{ + /// + /// 场景进度奖励配置Editor工具 + /// 读取Docs/config/IndoorProgress.xlsx和Item.xlsx + /// + public class IndoorProgressEditor : EditorWindow + { + private const string ART_SO_PATH = "Assets/Art_SubModule/Art_SO"; + private const string INDOOR_PROGRESS_EXCEL_NAME = "IndoorProgress.xlsx"; + private const string ITEM_EXCEL_NAME = "Item.xlsx"; + private const string INDOOR_PROGRESS_SHEET_NAME = "IndoorProgress"; + private const string ITEM_SHEET_NAME = "Item"; + private const string DOCS_PATH_PREF_KEY = "IndoorProgressEditor_DocsPath"; + private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule"; + + // IType类型定义(从ItemConfigEditor复制) + private static readonly Dictionary ITEM_TYPES = new Dictionary + { + { 1, "能量" }, + { 2, "星星" }, + { 3, "钻石" }, + { 97, "Playroom宠物道具" }, + { 98, "卡牌" }, + { 99, "背包道具" }, + { 100, "棋子" }, + { 101, "卡包" }, + { 102, "限时事件" }, + { 103, "小猪存钱罐" }, + { 104, "万能卡" }, + { 105, "头像框" }, + { 106, "活动代币" }, + { 107, "竞赛游戏代币" }, + { 108, "Pet Playroom拜访道具" }, + { 109, "表情" }, + { 110, "头像" }, + { 111, "Playroom装饰" }, + { 112, "Playroom服装" }, + { 113, "Playroom装饰套装" }, + { 114, "Playroom服装套装" }, + { 115, "Playroom道具宝箱" }, + { 116, "活动通行证代币道具" } + }; + + // 特殊Item的映射关系 + private static readonly Dictionary MY_ITEM_DICT = new Dictionary() + { + [100001] = "Energy", + [100002] = "Star", + [100003] = "Diamond", + [100004] = "Cardpack1", + [100005] = "Cardpack2", + [100006] = "Cardpack3", + [100007] = "Cardpack4", + [100008] = "Cardpack5", + [100011] = "LimitEvent4", + [100012] = "LimitEvent2", + [100013] = "LimitEvent3", + [100014] = "LimitEvent1", + [100015] = "LimitEvent5", + [100016] = "LimitEvent6", + [100017] = "LimitEvent7", + [100018] = "LimitEvent9", + [100019] = "LimitEvent10", + [100020] = "LimitEvent8", + [100022] = "PurplePig", + [101149] = "LimitEvent2", + [101150] = "LimitEvent3", + [101151] = "LimitEvent4", + [1021] = "705", + [1006] = "702", + [1007] = "703", + [100035] = "Battery", + [101445] = "BagItemBox1", + [101446] = "BagItemBox2", + [101447] = "BagItemBox3", + }; + + private string docsRootPath = ""; + private List allProgressDataList = new List(); + private List filteredProgressDataList = new List(); + private Dictionary itemDict = new Dictionary(); // Id -> ItemData + private List allArtTables = new List(); + + private Vector2 scrollPosition; + private bool isDataLoaded = false; + private int selectedScene = -1; + private string sceneInputText = ""; + + // 道具奖励临时编辑状态(按数据ID存储) + private Dictionary tempItemRewardTypeIndex = new Dictionary(); + + // 区域奖励临时编辑状态(按数据ID存储) + private Dictionary tempAreaRewardTypeIndex = new Dictionary(); + private Dictionary tempAreaRewardItemId = new Dictionary(); + private Dictionary tempAreaRewardNum = new Dictionary(); + + [MenuItem("策划工具/场景/场景进度奖励")] + public static void ShowWindow() + { + var window = GetWindow("场景进度奖励配置"); + window.minSize = new Vector2(1400, 700); + window.Show(); + } + + private void OnEnable() + { + // 读取上次保存的路径 + if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY)) + { + docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY); + } + } + + private void OnGUI() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("场景进度奖励配置工具", EditorStyles.boldLabel); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + // Docs路径选择 + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + docsRootPath = EditorGUILayout.TextField("路径", docsRootPath); + if (GUILayout.Button("选择文件夹", GUILayout.Width(100))) + { + string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + docsRootPath = selectedPath; + EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath); + } + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("加载配置数据", GUILayout.Height(30))) + { + LoadData(); + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(5); + + // 数据编辑区域 + if (isDataLoaded) + { + DrawSceneFilterAndDataEditor(); + } + else + { + EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info); + } + } + + /// + /// 加载配置数据 + /// + private void LoadData() + { + if (string.IsNullOrEmpty(docsRootPath)) + { + EditorUtility.DisplayDialog("错误", "请先选择Docs根目录", "确定"); + return; + } + + try + { + // 检查并更新Docs仓库 + if (!CheckAndUpdateDocsRepository()) + { + return; + } + + // 检查并切换Design_SubModule到main分支 + if (!CheckAndSwitchDesignSubModuleBranch()) + { + return; + } + + // 加载所有Art_SO资源 + LoadAllArtTableSO(); + + // 加载Item数据 + LoadItemData(); + + // 加载IndoorProgress数据 + LoadIndoorProgressData(); + + isDataLoaded = true; + selectedScene = -1; + sceneInputText = ""; + filteredProgressDataList.Clear(); + + EditorUtility.DisplayDialog("成功", "配置数据加载完成!", "确定"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定"); + isDataLoaded = false; + } + } + + /// + /// 执行Git命令 + /// + private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success) + { + try + { + var processInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(processInfo)) + { + string output = process.StandardOutput.ReadToEnd(); + string error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + success = process.ExitCode == 0; + return success ? output : error; + } + } + catch (Exception e) + { + success = false; + return $"执行Git命令失败: {e.Message}"; + } + } + + /// + /// 检查并更新Docs仓库 + /// + private bool CheckAndUpdateDocsRepository() + { + string gitPath = Path.Combine(docsRootPath, ".git"); + if (!Directory.Exists(gitPath)) + { + EditorUtility.DisplayDialog("错误", + $"所选目录不是Git仓库:{docsRootPath}\n请确保选择的是Docs项目的根目录。", + "确定"); + return false; + } + + string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess); + + if (!fetchSuccess) + { + bool continueAnyway = EditorUtility.DisplayDialog("警告", + $"Git fetch失败:{fetchResult}\n\n是否继续?", + "继续", "取消"); + return continueAnyway; + } + + string statusResult = ExecuteGitCommand(docsRootPath, "status", out bool statusSuccess); + + if (!statusSuccess) + { + EditorUtility.DisplayDialog("错误", + $"Git status检查失败:{statusResult}", + "确定"); + return false; + } + + if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed")) + { + bool proceed = EditorUtility.DisplayDialog("提示", + "Docs仓库有未提交的修改。\n\n建议先提交或暂存这些修改。\n\n是否继续?", + "继续", "取消"); + + if (!proceed) + { + return false; + } + } + + if (statusResult.Contains("Your branch is behind")) + { + bool doPull = EditorUtility.DisplayDialog("提示", + "Docs仓库有远程更新。\n\n是否执行git pull拉取最新代码?\n(建议执行以获取最新配置)", + "执行Pull", "跳过"); + + if (doPull) + { + string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess); + + if (!pullSuccess) + { + if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict")) + { + EditorUtility.DisplayDialog("错误", + $"Git pull遇到冲突:\n{pullResult}\n\n请先在SourceTree或其他Git工具中解决冲突。", + "确定"); + } + else + { + EditorUtility.DisplayDialog("错误", + $"Git pull失败:{pullResult}", + "确定"); + } + return false; + } + + EditorUtility.DisplayDialog("成功", "已拉取最新代码!", "确定"); + } + } + else + { + Debug.Log("Docs仓库已是最新"); + } + + return true; + } + + /// + /// 检查并切换Design_SubModule到main分支 + /// + private bool CheckAndSwitchDesignSubModuleBranch() + { + string designSubModulePath = Path.Combine(Application.dataPath.Replace("/Assets", ""), DESIGN_SUBMODULE_PATH); + designSubModulePath = designSubModulePath.Replace("\\", "/"); + + if (!Directory.Exists(designSubModulePath)) + { + EditorUtility.DisplayDialog("错误", + $"Design_SubModule目录不存在:{designSubModulePath}", + "确定"); + return false; + } + + string branchResult = ExecuteGitCommand(designSubModulePath, "rev-parse --abbrev-ref HEAD", out bool branchSuccess); + + if (!branchSuccess) + { + EditorUtility.DisplayDialog("错误", + $"无法获取Design_SubModule当前分支:{branchResult}", + "确定"); + return false; + } + + string currentBranch = branchResult.Trim(); + + if (currentBranch != "main") + { + bool doCheckout = EditorUtility.DisplayDialog("提示", + $"Design_SubModule当前在 '{currentBranch}' 分支。\n\n是否切换到main分支?", + "切换到main", "取消"); + + if (doCheckout) + { + string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess); + + if (!checkoutSuccess) + { + EditorUtility.DisplayDialog("错误", + $"切换到main分支失败:{checkoutResult}", + "确定"); + return false; + } + + EditorUtility.DisplayDialog("成功", "已切换到main分支!", "确定"); + } + else + { + return false; + } + } + else + { + Debug.Log("Design_SubModule已在main分支"); + } + + return true; + } + + /// + /// 加载所有Art_SO资源 + /// + private void LoadAllArtTableSO() + { + allArtTables.Clear(); + + string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { ART_SO_PATH }); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + ArtTableSO artTable = AssetDatabase.LoadAssetAtPath(path); + if (artTable != null) + { + allArtTables.Add(artTable); + } + } + + Debug.Log($"加载了 {allArtTables.Count} 个ArtTableSO资源"); + } + + /// + /// 加载Item数据 + /// + private void LoadItemData() + { + itemDict.Clear(); + + string itemExcelPath = Path.Combine(docsRootPath, "config", ITEM_EXCEL_NAME); + if (!File.Exists(itemExcelPath)) + { + throw new Exception($"未找到Item配置文件: {itemExcelPath}"); + } + + using (var package = new ExcelPackage(new FileInfo(itemExcelPath))) + { + var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME]; + if (worksheet == null) + { + throw new Exception($"Item.xlsx中未找到Sheet: {ITEM_SHEET_NAME}"); + } + + // 读取表头(第1行) + int idCol = -1, nameCol = -1, iTypeCol = -1, resCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "Name") nameCol = col; + else if (header == "IType") iTypeCol = col; + else if (header == "Res") resCol = col; + } + + if (idCol < 0 || nameCol < 0 || iTypeCol < 0) + { + throw new Exception("Item.xlsx表结构不正确,缺少必要列(Id/Name/IType)"); + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + string name = worksheet.Cells[row, nameCol].Text; + string iTypeText = worksheet.Cells[row, iTypeCol].Text; + string res = resCol > 0 ? worksheet.Cells[row, resCol].Text : ""; + + if (!int.TryParse(iTypeText, out int iType)) continue; + + itemDict[id] = new ItemData + { + Id = id, + Name = name, + IType = iType, + Res = res + }; + } + } + + Debug.Log($"加载了 {itemDict.Count} 个Item数据"); + } + + /// + /// 加载IndoorProgress数据 + /// + private void LoadIndoorProgressData() + { + allProgressDataList.Clear(); + + string progressExcelPath = Path.Combine(docsRootPath, "config", INDOOR_PROGRESS_EXCEL_NAME); + if (!File.Exists(progressExcelPath)) + { + throw new Exception($"未找到IndoorProgress配置文件: {progressExcelPath}"); + } + + using (var package = new ExcelPackage(new FileInfo(progressExcelPath))) + { + var worksheet = package.Workbook.Worksheets[INDOOR_PROGRESS_SHEET_NAME]; + if (worksheet == null) + { + throw new Exception($"IndoorProgress.xlsx中未找到Sheet: {INDOOR_PROGRESS_SHEET_NAME}"); + } + + // 读取表头(第1行) + int idCol = -1, sceneCol = -1, lvCol = -1, itemCol = -1, emitCol = -1, + rewardCol = -1, bigRewardCol = -1, areaRewardCol = -1, partCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "Scene") sceneCol = col; + else if (header == "Lv") lvCol = col; + else if (header == "Item") itemCol = col; + else if (header == "Emit") emitCol = col; + else if (header == "Reward") rewardCol = col; + else if (header == "BigReward") bigRewardCol = col; + else if (header == "AreaReward") areaRewardCol = col; + else if (header == "Part") partCol = col; + } + + if (idCol < 0 || sceneCol < 0 || lvCol < 0) + { + throw new Exception("IndoorProgress.xlsx表结构不正确,缺少必要列(Id/Scene/Lv)"); + } + + // 读取数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + for (int row = 3; row <= rowCount; row++) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + string sceneText = worksheet.Cells[row, sceneCol].Text; + string lvText = worksheet.Cells[row, lvCol].Text; + + if (!int.TryParse(sceneText, out int scene)) continue; + if (!int.TryParse(lvText, out int lv)) continue; + + string item = itemCol > 0 ? worksheet.Cells[row, itemCol].Text : ""; + string emit = emitCol > 0 ? worksheet.Cells[row, emitCol].Text : ""; + string reward = rewardCol > 0 ? worksheet.Cells[row, rewardCol].Text : ""; + string bigReward = bigRewardCol > 0 ? worksheet.Cells[row, bigRewardCol].Text : ""; + string areaReward = areaRewardCol > 0 ? worksheet.Cells[row, areaRewardCol].Text : ""; + string partText = partCol > 0 ? worksheet.Cells[row, partCol].Text : ""; + + int.TryParse(partText, out int part); + + var progressData = new IndoorProgressData + { + Id = id, + Scene = scene, + Lv = lv, + Item = item, + Emit = emit, + Reward = reward, + BigReward = bigReward, + AreaReward = areaReward, + Part = part + }; + + allProgressDataList.Add(progressData); + } + } + + Debug.Log($"加载了 {allProgressDataList.Count} 条IndoorProgress数据"); + } + + /// + /// 绘制场景筛选和数据编辑器 + /// + private void DrawSceneFilterAndDataEditor() + { + EditorGUILayout.BeginVertical("box"); + + // 场景筛选 + EditorGUILayout.LabelField("场景筛选", EditorStyles.boldLabel); + EditorGUILayout.BeginHorizontal(); + + sceneInputText = EditorGUILayout.TextField("输入场景ID", sceneInputText, GUILayout.Width(200)); + + if (GUILayout.Button("确认加载场景", GUILayout.Width(120))) + { + if (int.TryParse(sceneInputText, out int sceneId)) + { + selectedScene = sceneId; + FilterProgressByScene(); + } + else + { + EditorUtility.DisplayDialog("错误", "请输入有效的场景ID(数字)", "确定"); + } + } + + if (selectedScene > 0) + { + EditorGUILayout.LabelField($"当前场景: {selectedScene}", EditorStyles.boldLabel, GUILayout.Width(150)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + if (selectedScene > 0 && filteredProgressDataList.Count > 0) + { + EditorGUILayout.Space(10); + + // 新增场景按钮 + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("+ 新增场景", GUILayout.Height(30), GUILayout.Width(150))) + { + AddNewScene(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + // 数据列表 + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + EditorGUILayout.BeginVertical("box"); + DrawProgressDataList(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(10); + + // 保存按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("保存配置到Excel", GUILayout.Height(40), GUILayout.Width(200))) + { + SaveData(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + else if (selectedScene > 0) + { + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox($"场景 {selectedScene} 没有数据", MessageType.Warning); + + // 新增场景按钮 + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("+ 新增场景", GUILayout.Height(30), GUILayout.Width(150))) + { + AddNewScene(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + else + { + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox("请输入场景ID并点击确认加载", MessageType.Info); + } + } + + /// + /// 根据场景筛选数据 + /// + private void FilterProgressByScene() + { + filteredProgressDataList = allProgressDataList + .Where(x => x.Scene == selectedScene) + .OrderBy(x => x.Lv) + .ToList(); + } + + /// + /// 绘制进度数据列表 + /// + private void DrawProgressDataList() + { + // 表头 + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField("ID", EditorStyles.boldLabel, GUILayout.Width(40)); + EditorGUILayout.LabelField("Lv", EditorStyles.boldLabel, GUILayout.Width(40)); + EditorGUILayout.LabelField("道具奖励", EditorStyles.boldLabel, GUILayout.Width(450)); + EditorGUILayout.LabelField("区域奖励", EditorStyles.boldLabel, GUILayout.Width(450)); + EditorGUILayout.LabelField("零件", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.EndHorizontal(); + + // 数据行 + for (int i = 0; i < filteredProgressDataList.Count; i++) + { + DrawProgressDataRow(filteredProgressDataList[i], i); + } + } + + /// + /// 绘制单行进度数据 + /// + private void DrawProgressDataRow(IndoorProgressData data, int index) + { + EditorGUILayout.BeginHorizontal("box"); + + // ID(不可编辑) + EditorGUILayout.LabelField(data.Id.ToString(), GUILayout.Width(40)); + + // Lv(可编辑) + EditorGUI.BeginChangeCheck(); + int newLv = EditorGUILayout.IntField(data.Lv, GUILayout.Width(40)); + if (EditorGUI.EndChangeCheck()) + { + data.Lv = newLv; + } + + // 道具奖励(Item/Emit/Reward一体) + EditorGUILayout.BeginVertical(GUILayout.Width(450)); + DrawItemRewardEditor(data); + EditorGUILayout.EndVertical(); + + // 区域奖励(AreaReward/BigReward) + EditorGUILayout.BeginVertical(GUILayout.Width(450)); + DrawAreaRewardEditor(data); + EditorGUILayout.EndVertical(); + + // 零件数量 + EditorGUI.BeginChangeCheck(); + int newPart = EditorGUILayout.IntField(data.Part, GUILayout.Width(60)); + if (EditorGUI.EndChangeCheck()) + { + data.Part = newPart; + } + + // 操作按钮 + if (GUILayout.Button("删除", GUILayout.Width(60))) + { + if (EditorUtility.DisplayDialog("确认删除", + $"确定要删除ID {data.Id} (场景{data.Scene}-Lv{data.Lv}) 的数据吗?", + "删除", "取消")) + { + filteredProgressDataList.RemoveAt(index); + allProgressDataList.Remove(data); + } + } + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 绘制道具奖励编辑器(Item/Emit/Reward) + /// + private void DrawItemRewardEditor(IndoorProgressData data) + { + // 解析当前数据 + var itemReward = ParseItemReward(data.Item); + + EditorGUILayout.BeginHorizontal(); + + // 选择IType + var typeOptions = new List { "无" }; + var typeValues = new List { -1 }; + + foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key)) + { + typeOptions.Add($"{kvp.Value} ({kvp.Key})"); + typeValues.Add(kvp.Key); + } + + // 初始化临时状态:如果有数据就用数据的类型,如果没数据就用临时保存的选择 + if (!tempItemRewardTypeIndex.ContainsKey(data.Id)) + { + if (itemReward.HasValue) + { + int itemType = GetItemType(itemReward.Value.Id); + tempItemRewardTypeIndex[data.Id] = typeValues.IndexOf(itemType); + } + else + { + tempItemRewardTypeIndex[data.Id] = 0; // 默认"无" + } + } + else if (itemReward.HasValue) + { + // 如果有数据,同步临时状态 + int itemType = GetItemType(itemReward.Value.Id); + int typeIndex = typeValues.IndexOf(itemType); + if (typeIndex >= 0) + { + tempItemRewardTypeIndex[data.Id] = typeIndex; + } + } + + int currentTypeIndex = tempItemRewardTypeIndex[data.Id]; + if (currentTypeIndex < 0) currentTypeIndex = 0; + + EditorGUI.BeginChangeCheck(); + int newTypeIndex = EditorGUILayout.Popup(currentTypeIndex, typeOptions.ToArray(), GUILayout.Width(120)); + bool typeChanged = EditorGUI.EndChangeCheck(); + + if (typeChanged) + { + tempItemRewardTypeIndex[data.Id] = newTypeIndex; + } + + if (newTypeIndex == 0) + { + // 选择"无" + if (itemReward.HasValue) + { + data.Item = ""; + data.Emit = ""; + data.Reward = ""; + } + } + else + { + int selectedType = typeValues[newTypeIndex]; + + // 如果类型改变,且当前有数据但类型不匹配,清空数据 + if (typeChanged && itemReward.HasValue && GetItemType(itemReward.Value.Id) != selectedType) + { + data.Item = ""; + data.Emit = ""; + data.Reward = ""; + itemReward = null; + } + + // 根据类型筛选Item + var itemsOfType = itemDict.Values.Where(x => x.IType == selectedType).ToList(); + + if (itemsOfType.Count > 0) + { + // IType=100(棋子)使用输入框+刷新按钮,其他使用下拉菜单 + if (selectedType == 100) + { + // 输入ID + EditorGUILayout.LabelField("ID:", GUILayout.Width(25)); + + // 当前ID:如果类型改变,重置为0;否则使用现有值 + int inputId = 0; + if (!typeChanged && itemReward.HasValue && GetItemType(itemReward.Value.Id) == 100) + { + inputId = itemReward.Value.Id; + } + + EditorGUI.BeginChangeCheck(); + inputId = EditorGUILayout.IntField(inputId, GUILayout.Width(60)); + bool idChanged = EditorGUI.EndChangeCheck(); + + // 数量 + EditorGUILayout.LabelField("数量:", GUILayout.Width(35)); + int num = itemReward.HasValue ? itemReward.Value.Num : 1; + EditorGUI.BeginChangeCheck(); + num = EditorGUILayout.IntField(num, GUILayout.Width(40)); + bool numChanged = EditorGUI.EndChangeCheck(); + + // 刷新按钮 + if (GUILayout.Button("刷新", GUILayout.Width(50))) + { + if (inputId > 0 && itemDict.ContainsKey(inputId) && itemDict[inputId].IType == 100) + { + UpdateItemReward(data, inputId, num); + } + else + { + EditorUtility.DisplayDialog("错误", $"ID {inputId} 不存在或不是棋子类型", "确定"); + } + } + + // 如果ID或数量改变,自动更新 + if (idChanged || numChanged) + { + if (inputId > 0 && itemDict.ContainsKey(inputId) && itemDict[inputId].IType == 100) + { + UpdateItemReward(data, inputId, num); + } + } + + // 显示预览图 + if (inputId > 0 && itemDict.TryGetValue(inputId, out var item) && item.IType == 100) + { + DrawItemPreview(item); + } + else + { + EditorGUILayout.LabelField("暂无预览", GUILayout.Width(60)); + } + } + else + { + // 其他类型使用下拉菜单 + var itemOptions = itemsOfType.Select(x => $"{x.Name} ({x.Id})").ToList(); + var itemIds = itemsOfType.Select(x => x.Id).ToList(); + + int currentItemIndex = 0; + if (!typeChanged && itemReward.HasValue) + { + currentItemIndex = itemIds.IndexOf(itemReward.Value.Id); + if (currentItemIndex < 0) currentItemIndex = 0; + } + + EditorGUI.BeginChangeCheck(); + int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(180)); + int selectedItemId = itemIds[newItemIndex]; + + // 数量 + int num = itemReward.HasValue ? itemReward.Value.Num : 1; + num = EditorGUILayout.IntField(num, GUILayout.Width(40)); + + // 更新数据 + if (EditorGUI.EndChangeCheck() || typeChanged || !itemReward.HasValue) + { + UpdateItemReward(data, selectedItemId, num); + } + + // 显示预览图 + if (itemDict.TryGetValue(selectedItemId, out var item2)) + { + DrawItemPreview(item2); + } + } + } + else + { + EditorGUILayout.LabelField("该类型无可用Item", GUILayout.Width(150)); + } + } + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 绘制区域奖励编辑器(AreaReward/BigReward) + /// + private void DrawAreaRewardEditor(IndoorProgressData data) + { + // 判断当前Lv是否可以配置AreaReward + bool canConfigArea = CanConfigAreaReward(data.Scene, data.Lv); + + if (!canConfigArea) + { + // 不显示任何内容,保持空白 + return; + } + + // 解析当前AreaReward + var areaRewards = ParseAreaReward(data.AreaReward); + + EditorGUILayout.BeginVertical(); + + // 显示现有奖励 + for (int i = 0; i < areaRewards.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + var reward = areaRewards[i]; + + // 显示Item信息和图片 + if (itemDict.TryGetValue(reward.Id, out var rewardItem)) + { + EditorGUILayout.LabelField($"{rewardItem.Name}", GUILayout.Width(100)); + + // 显示预览图 + DrawItemPreview(rewardItem); + } + else + { + EditorGUILayout.LabelField($"ID:{reward.Id}", GUILayout.Width(100)); + EditorGUILayout.LabelField("无图", GUILayout.Width(50)); + } + + // 数量 + EditorGUILayout.LabelField("x", GUILayout.Width(10)); + EditorGUI.BeginChangeCheck(); + int newNum = EditorGUILayout.IntField(reward.Num, GUILayout.Width(40)); + if (EditorGUI.EndChangeCheck()) + { + areaRewards[i] = new ItemReward { Id = reward.Id, Num = newNum }; + UpdateAreaReward(data, areaRewards); + } + + // 删除按钮 + if (GUILayout.Button("-", GUILayout.Width(30))) + { + areaRewards.RemoveAt(i); + UpdateAreaReward(data, areaRewards); + break; + } + + EditorGUILayout.EndHorizontal(); + } + + // 添加新奖励UI + DrawAddAreaRewardUI(data, areaRewards); + + EditorGUILayout.EndVertical(); + } + + /// + /// 判断是否可以配置AreaReward + /// + private bool CanConfigAreaReward(int scene, int lv) + { + if (scene == 1) + { + // 场景1: 20, 29, 36, 44 + return lv == 20 || lv == 29 || lv == 36 || lv == 44; + } + else if (scene >= 2 && scene <= 5) + { + // 场景2-5: 20 + return lv == 20; + } + else + { + // 其他场景: 25 + return lv == 25; + } + } + + /// + /// 绘制添加区域奖励UI + /// + private void DrawAddAreaRewardUI(IndoorProgressData data, List areaRewards) + { + EditorGUILayout.BeginHorizontal(); + + // 选择IType + var typeOptions = new List { "选择类型..." }; + var typeValues = new List { -1 }; + + foreach (var kvp in ITEM_TYPES.OrderBy(x => x.Key)) + { + typeOptions.Add($"{kvp.Value} ({kvp.Key})"); + typeValues.Add(kvp.Key); + } + + // 使用临时变量存储当前选择 + if (!tempAreaRewardTypeIndex.ContainsKey(data.Id)) + { + tempAreaRewardTypeIndex[data.Id] = 0; + } + if (!tempAreaRewardItemId.ContainsKey(data.Id)) + { + tempAreaRewardItemId[data.Id] = 0; + } + if (!tempAreaRewardNum.ContainsKey(data.Id)) + { + tempAreaRewardNum[data.Id] = 1; + } + + int currentTypeIndex = tempAreaRewardTypeIndex[data.Id]; + + EditorGUI.BeginChangeCheck(); + int newTypeIndex = EditorGUILayout.Popup(currentTypeIndex, typeOptions.ToArray(), GUILayout.Width(120)); + bool typeChanged = EditorGUI.EndChangeCheck(); + + if (typeChanged) + { + tempAreaRewardTypeIndex[data.Id] = newTypeIndex; + tempAreaRewardItemId[data.Id] = 0; // 重置ItemId + } + + if (newTypeIndex > 0) + { + int selectedType = typeValues[newTypeIndex]; + + // 根据类型筛选Item + var itemsOfType = itemDict.Values.Where(x => x.IType == selectedType).ToList(); + + if (itemsOfType.Count > 0) + { + // IType=100(棋子)使用输入框+添加按钮 + if (selectedType == 100) + { + EditorGUILayout.LabelField("ID:", GUILayout.Width(25)); + int inputId = tempAreaRewardItemId[data.Id]; + inputId = EditorGUILayout.IntField(inputId, GUILayout.Width(60)); + tempAreaRewardItemId[data.Id] = inputId; + + EditorGUILayout.LabelField("数量:", GUILayout.Width(35)); + int num = tempAreaRewardNum[data.Id]; + num = EditorGUILayout.IntField(num, GUILayout.Width(40)); + tempAreaRewardNum[data.Id] = num; + + if (GUILayout.Button("添加", GUILayout.Width(50))) + { + if (itemDict.ContainsKey(inputId) && itemDict[inputId].IType == 100) + { + areaRewards.Add(new ItemReward { Id = inputId, Num = num }); + UpdateAreaReward(data, areaRewards); + // 重置 + tempAreaRewardTypeIndex[data.Id] = 0; + tempAreaRewardItemId[data.Id] = 0; + tempAreaRewardNum[data.Id] = 1; + } + else + { + EditorUtility.DisplayDialog("错误", $"ID {inputId} 不存在或不是棋子类型", "确定"); + } + } + + // 显示预览图 + if (itemDict.TryGetValue(inputId, out var previewItem) && previewItem.IType == 100) + { + DrawItemPreview(previewItem); + } + else + { + EditorGUILayout.LabelField("暂无预览", GUILayout.Width(60)); + } + } + else + { + // 其他类型使用下拉菜单 + var itemOptions = itemsOfType.Select(x => $"{x.Name} ({x.Id})").ToList(); + var itemIds = itemsOfType.Select(x => x.Id).ToList(); + + int savedItemId = tempAreaRewardItemId[data.Id]; + int currentItemIndex = itemIds.IndexOf(savedItemId); + if (currentItemIndex < 0) currentItemIndex = 0; + + EditorGUI.BeginChangeCheck(); + int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions.ToArray(), GUILayout.Width(150)); + if (EditorGUI.EndChangeCheck()) + { + tempAreaRewardItemId[data.Id] = itemIds[newItemIndex]; + } + + int selectedItemId = itemIds[newItemIndex]; + + // 数量 + int num = tempAreaRewardNum[data.Id]; + num = EditorGUILayout.IntField(num, GUILayout.Width(40)); + tempAreaRewardNum[data.Id] = num; + + if (GUILayout.Button("添加", GUILayout.Width(50))) + { + areaRewards.Add(new ItemReward { Id = selectedItemId, Num = num }); + UpdateAreaReward(data, areaRewards); + // 重置 + tempAreaRewardTypeIndex[data.Id] = 0; + tempAreaRewardItemId[data.Id] = 0; + tempAreaRewardNum[data.Id] = 1; + } + + // 显示预览图 + if (itemDict.TryGetValue(selectedItemId, out var previewItem2)) + { + DrawItemPreview(previewItem2); + } + } + } + else + { + EditorGUILayout.LabelField("该类型无可用Item", GUILayout.Width(120)); + } + } + + EditorGUILayout.EndHorizontal(); + } + + /// + /// 解析Item奖励 + /// + private ItemReward? ParseItemReward(string itemJson) + { + if (string.IsNullOrEmpty(itemJson)) return null; + + try + { + // 格式: [{"Id":83,"Num":1}] + itemJson = itemJson.Trim(); + if (!itemJson.StartsWith("[") || !itemJson.EndsWith("]")) + return null; + + itemJson = itemJson.Substring(1, itemJson.Length - 2); // 去掉[] + + // 简单解析 + var idMatch = System.Text.RegularExpressions.Regex.Match(itemJson, @"""Id"":(\d+)"); + var numMatch = System.Text.RegularExpressions.Regex.Match(itemJson, @"""Num"":(\d+)"); + + if (idMatch.Success && numMatch.Success) + { + return new ItemReward + { + Id = int.Parse(idMatch.Groups[1].Value), + Num = int.Parse(numMatch.Groups[1].Value) + }; + } + } + catch { } + + return null; + } + + /// + /// 解析区域奖励 + /// + private List ParseAreaReward(string areaRewardJson) + { + var result = new List(); + + if (string.IsNullOrEmpty(areaRewardJson)) return result; + + try + { + // 格式: [{"Id":100001,"Num":25}] 或 [{"Id":100001,"Num":50},{"Id":100021,"Num":1}] + var matches = System.Text.RegularExpressions.Regex.Matches(areaRewardJson, @"\{""Id"":(\d+),""Num"":(\d+)\}"); + + foreach (System.Text.RegularExpressions.Match match in matches) + { + result.Add(new ItemReward + { + Id = int.Parse(match.Groups[1].Value), + Num = int.Parse(match.Groups[2].Value) + }); + } + } + catch { } + + return result; + } + + /// + /// 获取Item的类型 + /// + private int GetItemType(int itemId) + { + if (itemDict.TryGetValue(itemId, out var item)) + { + return item.IType; + } + return -1; + } + + /// + /// 更新Item奖励 + /// + private void UpdateItemReward(IndoorProgressData data, int itemId, int num) + { + // Item格式: [{"Id":83,"Num":1}] + data.Item = $"[{{\"Id\":{itemId},\"Num\":{num}}}]"; + + // Emit格式: 83 + data.Emit = itemId.ToString(); + + // Reward格式: 83=1 + data.Reward = $"{itemId}={num}"; + } + + /// + /// 更新区域奖励 + /// + private void UpdateAreaReward(IndoorProgressData data, List areaRewards) + { + if (areaRewards.Count == 0) + { + data.AreaReward = ""; + data.BigReward = ""; + + // 如果这是最后一个AreaReward位置,需要清空Lv=1的BigReward + if (IsLastAreaRewardPosition(data.Scene, data.Lv)) + { + var lv1Data = allProgressDataList.FirstOrDefault(x => x.Scene == data.Scene && x.Lv == 1); + if (lv1Data != null) + { + lv1Data.BigReward = ""; + } + } + return; + } + + // AreaReward格式: [{"Id":100001,"Num":25}] 或 [{"Id":100001,"Num":50},{"Id":100021,"Num":1}] + var areaRewardItems = areaRewards.Select(r => $"{{\"Id\":{r.Id},\"Num\":{r.Num}}}"); + data.AreaReward = $"[{string.Join(",", areaRewardItems)}]"; + + // 当前行的BigReward清空(只有Lv=1才有BigReward) + data.BigReward = ""; + + // 如果这是最后一个AreaReward位置,更新Lv=1的BigReward + if (IsLastAreaRewardPosition(data.Scene, data.Lv)) + { + var lv1Data = allProgressDataList.FirstOrDefault(x => x.Scene == data.Scene && x.Lv == 1); + if (lv1Data != null) + { + // BigReward格式: Energy=25 或 Energy=50,PurplePig=1,101451=1 + var bigRewardItems = new List(); + bool hasUnmappedItem = false; + + foreach (var reward in areaRewards) + { + if (MY_ITEM_DICT.TryGetValue(reward.Id, out string mappedName)) + { + bigRewardItems.Add($"{mappedName}={reward.Num}"); + } + else + { + bigRewardItems.Add($"{reward.Id}={reward.Num}"); + hasUnmappedItem = true; + } + } + + lv1Data.BigReward = string.Join(",", bigRewardItems); + + // 如果有未映射的Item,显示警告 + if (hasUnmappedItem) + { + Debug.LogWarning($"场景{data.Scene}的AreaReward包含未映射的ItemId,可能会出问题!"); + } + } + } + } + + /// + /// 判断当前Lv是否为最后一个AreaReward位置 + /// + private bool IsLastAreaRewardPosition(int scene, int lv) + { + if (scene == 1) + { + // 场景1的最后一个是44 + return lv == 44; + } + else if (scene >= 2 && scene <= 5) + { + // 场景2-5的最后一个是20 + return lv == 20; + } + else + { + // 其他场景的最后一个是25 + return lv == 25; + } + } + + /// + /// 绘制Item预览 + /// + private void DrawItemPreview(ItemData item) + { + if (string.IsNullOrEmpty(item.Res)) + { + EditorGUILayout.LabelField("无图", GUILayout.Width(50)); + return; + } + + // 解析Res格式: "tableId,itemId" + var parts = item.Res.Split(','); + if (parts.Length != 2) + { + EditorGUILayout.LabelField("无图", GUILayout.Width(50)); + return; + } + + if (!int.TryParse(parts[0], out int tableId)) return; + if (!int.TryParse(parts[1], out int itemId)) return; + + // 查找对应的ArtTableSO + var artTable = allArtTables.FirstOrDefault(x => x.TableId == tableId); + if (artTable == null) + { + EditorGUILayout.LabelField("无图", GUILayout.Width(50)); + return; + } + + var artItem = artTable.Items.FirstOrDefault(x => x.Id == itemId); + if (artItem == null || artItem.Sprite == null) + { + EditorGUILayout.LabelField("无图", GUILayout.Width(50)); + return; + } + + // 显示预览图 + Rect rect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50)); + GUI.DrawTexture(rect, artItem.Sprite.texture, ScaleMode.ScaleToFit); + } + + /// + /// 新增场景 + /// + private void AddNewScene() + { + // 找到最大的Scene ID + int maxScene = allProgressDataList.Count > 0 ? allProgressDataList.Max(x => x.Scene) : 0; + int newScene = maxScene + 1; + + // 找到最大的ID + int maxId = allProgressDataList.Count > 0 ? allProgressDataList.Max(x => x.Id) : 0; + + // 创建25个新记录 + for (int lv = 1; lv <= 25; lv++) + { + maxId++; + + var newData = new IndoorProgressData + { + Id = maxId, + Scene = newScene, + Lv = lv, + Item = "", + Emit = "", + Reward = "", + BigReward = "", + AreaReward = "", + Part = 0 + }; + + // 如果是Lv25,添加默认的AreaReward + if (lv == 25) + { + // 默认给25能量 + newData.AreaReward = "[{\"Id\":100001,\"Num\":25}]"; + newData.BigReward = "Energy=25"; + } + + allProgressDataList.Add(newData); + } + + // 切换到新场景 + selectedScene = newScene; + sceneInputText = newScene.ToString(); + FilterProgressByScene(); + + EditorUtility.DisplayDialog("成功", $"已创建场景 {newScene},包含25个步骤(Lv 1-25)", "确定"); + } + + /// + /// 保存数据 + /// + private void SaveData() + { + try + { + string progressExcelPath = Path.Combine(docsRootPath, "config", INDOOR_PROGRESS_EXCEL_NAME); + if (!File.Exists(progressExcelPath)) + { + EditorUtility.DisplayDialog("错误", $"未找到IndoorProgress配置文件: {progressExcelPath}", "确定"); + return; + } + + using (var package = new ExcelPackage(new FileInfo(progressExcelPath))) + { + var worksheet = package.Workbook.Worksheets[INDOOR_PROGRESS_SHEET_NAME]; + if (worksheet == null) + { + EditorUtility.DisplayDialog("错误", $"IndoorProgress.xlsx中未找到Sheet: {INDOOR_PROGRESS_SHEET_NAME}", "确定"); + return; + } + + // 查找列索引 + int idCol = -1, sceneCol = -1, lvCol = -1, itemCol = -1, emitCol = -1, + rewardCol = -1, bigRewardCol = -1, areaRewardCol = -1, partCol = -1; + int columnCount = worksheet.Dimension.Columns; + + for (int col = 1; col <= columnCount; col++) + { + string header = worksheet.Cells[1, col].Text; + if (header == "Id") idCol = col; + else if (header == "Scene") sceneCol = col; + else if (header == "Lv") lvCol = col; + else if (header == "Item") itemCol = col; + else if (header == "Emit") emitCol = col; + else if (header == "Reward") rewardCol = col; + else if (header == "BigReward") bigRewardCol = col; + else if (header == "AreaReward") areaRewardCol = col; + else if (header == "Part") partCol = col; + } + + // 更新和删除数据(从第3行开始) + int rowCount = worksheet.Dimension.Rows; + var processedIds = new HashSet(); + + // 第一遍:更新现有行或标记删除 + for (int row = rowCount; row >= 3; row--) + { + string idText = worksheet.Cells[row, idCol].Text; + if (string.IsNullOrEmpty(idText)) continue; + + if (!int.TryParse(idText, out int id)) continue; + + // 查找对应的数据 + var progressData = allProgressDataList.Find(x => x.Id == id); + if (progressData != null) + { + // 更新数据 + worksheet.Cells[row, sceneCol].Value = progressData.Scene; + worksheet.Cells[row, lvCol].Value = progressData.Lv; + worksheet.Cells[row, itemCol].Value = progressData.Item; + worksheet.Cells[row, emitCol].Value = progressData.Emit; + worksheet.Cells[row, rewardCol].Value = progressData.Reward; + worksheet.Cells[row, bigRewardCol].Value = progressData.BigReward; + worksheet.Cells[row, areaRewardCol].Value = progressData.AreaReward; + worksheet.Cells[row, partCol].Value = progressData.Part > 0 ? progressData.Part.ToString() : ""; + + processedIds.Add(id); + } + else + { + // 删除行 + worksheet.DeleteRow(row); + } + } + + // 第二遍:添加新行 + int currentRow = worksheet.Dimension?.Rows ?? 2; + foreach (var progressData in allProgressDataList.OrderBy(x => x.Id)) + { + if (!processedIds.Contains(progressData.Id)) + { + // 这是新增的数据 + currentRow++; + worksheet.Cells[currentRow, idCol].Value = progressData.Id; + worksheet.Cells[currentRow, sceneCol].Value = progressData.Scene; + worksheet.Cells[currentRow, lvCol].Value = progressData.Lv; + worksheet.Cells[currentRow, itemCol].Value = progressData.Item; + worksheet.Cells[currentRow, emitCol].Value = progressData.Emit; + worksheet.Cells[currentRow, rewardCol].Value = progressData.Reward; + worksheet.Cells[currentRow, bigRewardCol].Value = progressData.BigReward; + worksheet.Cells[currentRow, areaRewardCol].Value = progressData.AreaReward; + worksheet.Cells[currentRow, partCol].Value = progressData.Part > 0 ? progressData.Part.ToString() : ""; + } + } + + // 保存文件 + package.Save(); + } + + // 提示成功并提醒推送 + EditorUtility.DisplayDialog("保存成功", + "配置已保存到Excel文件!\n\n" + + "⚠️ 重要提醒:\n" + + "请及时在SourceTree或GitHubDesktop中:\n" + + "1. 提交(Commit)本次修改\n" + + "2. 推送(Push)到远程仓库\n\n" + + "避免与其他策划产生冲突!", + "我知道了"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定"); + } + } + + /// + /// 场景进度数据类 + /// + private class IndoorProgressData + { + public int Id; + public int Scene; + public int Lv; + public string Item; + public string Emit; + public string Reward; + public string BigReward; + public string AreaReward; + public int Part; + } + + /// + /// Item数据类 + /// + private class ItemData + { + public int Id; + public string Name; + public int IType; + public string Res; + } + + /// + /// Item奖励结构 + /// + private struct ItemReward + { + public int Id; + public int Num; + } + } +} diff --git a/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs.meta b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs.meta new file mode 100644 index 0000000..bb67e48 --- /dev/null +++ b/Scripts/Editor/Design_Tools/Scene/IndoorProgressEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 15c94f7123a92774084366377ef07164 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: