Design_SubModule/Scripts/Editor/Design_Tools/Scene/LevelLauncherDataEditor.cs
2026-04-15 15:16:07 +08:00

604 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
namespace DesignTools.Scene
{
/// <summary>
/// 发射器插图名称和资源设置工具
/// 读取 Docs/config/NetAssetData.xlsx 的 LevelLauncherData Sheet
/// 关联 ArtTableSO "LauncherMergeStoryPic" 提供预览图选择
/// </summary>
public class LevelLauncherDataEditor : BaseDesignToolEditor
{
#region
private const string EXCEL_NAME = "NetAssetData.xlsx";
private const string SHEET_NAME = "LevelLauncherData";
private const string RESOURCES_PATH_PREFIX = "Assets/Art_SubModule/GameMain/UI/UISprites/Area/";
private const string PIC_TABLE_NAME = "LauncherMergeStoryPic";
#endregion
#region
private List<LauncherRowData> dataList = new List<LauncherRowData>();
private ArtTableSO picTableSO;
private Dictionary<string, Sprite> spriteCache = new Dictionary<string, Sprite>();
private Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>();
#endregion
#region
[MenuItem("策划工具/场景/发射器插图名称和资源设置")]
public static void ShowWindow()
{
var window = GetWindow<LevelLauncherDataEditor>("发射器插图名称和资源设置");
window.minSize = window.GetMinWindowSize();
window.Show();
}
#endregion
#region BaseDesignToolEditor
protected override string GetDocsPathPrefKey() => "LevelLauncherDataEditor_DocsPath";
protected override string GetWindowTitle() => "发射器插图名称和资源设置";
protected override Vector2 GetMinWindowSize() => new Vector2(1200, 600);
protected override void LoadConfigData()
{
spriteCache.Clear();
textureCache.Clear();
LoadPicTableSO();
LoadExcelData();
}
protected override void DrawDataEditor()
{
if (picTableSO == null)
{
EditorGUILayout.HelpBox(
$"未找到 ArtTableSO \"{PIC_TABLE_NAME}\",预览图选择功能不可用。\n请先在美术资源工具中创建名为 \"{PIC_TABLE_NAME}\" 的资源表。",
MessageType.Warning);
}
EditorGUILayout.BeginVertical("box");
// 标题 + 新增按钮
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"发射器数据列表(共 {dataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 新增行", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewRow();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 表头
DrawTableHeader();
// 数据行
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
LauncherRowData toDelete = null;
foreach (var data in dataList)
{
DrawDataRow(data, ref toDelete);
}
if (toDelete != null)
{
dataList.Remove(toDelete);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
protected override void SaveDataToExcel()
{
try
{
string excelPath = GetDocsConfigFilePath(EXCEL_NAME);
if (!File.Exists(excelPath))
{
EditorUtility.DisplayDialog("错误", $"文件不存在: {excelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(excelPath)))
{
var ws = package.Workbook.Worksheets[SHEET_NAME];
if (ws == null)
{
EditorUtility.DisplayDialog("错误", $"未找到 Sheet: {SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, lvCol = -1, langKeyCol = -1, picCol = -1, resPathCol = -1;
int colCount = ws.Dimension?.Columns ?? 0;
for (int c = 1; c <= colCount; c++)
{
string h = ws.Cells[1, c].Text;
switch (h)
{
case "Id": idCol = c; break;
case "Lv": lvCol = c; break;
case "LanguageKey": langKeyCol = c; break;
case "Picture": picCol = c; break;
case "ResourcesPath": resPathCol = c; break;
}
}
if (idCol < 0)
{
EditorUtility.DisplayDialog("错误", "表结构不正确,缺少 Id 列", "确定");
return;
}
// 清除现有数据行(从底部向上删除,保留前两行表头)
int existingRows = ws.Dimension?.Rows ?? 2;
for (int row = existingRows; row >= 3; row--)
{
ws.DeleteRow(row);
}
// 写入所有数据
for (int i = 0; i < dataList.Count; i++)
{
int row = i + 3;
var data = dataList[i];
ws.Cells[row, idCol].Value = data.Id;
if (lvCol > 0) ws.Cells[row, lvCol].Value = data.Lv;
if (langKeyCol > 0) ws.Cells[row, langKeyCol].Value = GetLanguageKey(data.Lv);
if (picCol > 0) ws.Cells[row, picCol].Value = data.Picture ?? "";
if (resPathCol > 0) ws.Cells[row, resPathCol].Value = GetResourcesPath(data.Picture);
}
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}", "确定");
}
}
#endregion
#region
private void LoadPicTableSO()
{
picTableSO = null;
string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (so != null && so.TableName == PIC_TABLE_NAME)
{
picTableSO = so;
break;
}
}
if (picTableSO == null)
{
Debug.LogWarning($"未找到 ArtTableSO: {PIC_TABLE_NAME},预览图选择功能将不可用");
}
}
private void LoadExcelData()
{
dataList.Clear();
string excelPath = GetDocsConfigFilePath(EXCEL_NAME);
if (!File.Exists(excelPath))
{
throw new Exception($"未找到文件: {excelPath}");
}
using (var package = new ExcelPackage(new FileInfo(excelPath)))
{
var ws = package.Workbook.Worksheets[SHEET_NAME];
if (ws == null)
{
throw new Exception($"未找到 Sheet: {SHEET_NAME}");
}
// 查找列
int idCol = -1, lvCol = -1, picCol = -1;
int colCount = ws.Dimension?.Columns ?? 0;
for (int c = 1; c <= colCount; c++)
{
string h = ws.Cells[1, c].Text;
if (h == "Id") idCol = c;
else if (h == "Lv") lvCol = c;
else if (h == "Picture") picCol = c;
}
if (idCol < 0)
{
throw new Exception("表结构不正确,缺少 Id 列");
}
int rowCount = ws.Dimension?.Rows ?? 0;
for (int row = 3; row <= rowCount; row++)
{
string idText = ws.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
int lv = 0;
if (lvCol > 0)
{
int.TryParse(ws.Cells[row, lvCol].Text, out lv);
}
dataList.Add(new LauncherRowData
{
Id = id,
Lv = lv,
Picture = picCol > 0 ? (ws.Cells[row, picCol].Text ?? "") : "",
});
}
}
}
#endregion
#region UI
private void DrawTableHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("Id", EditorStyles.miniLabel, GUILayout.Width(35));
EditorGUILayout.LabelField("Lv", EditorStyles.miniLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("LanguageKey", EditorStyles.miniLabel, GUILayout.Width(260));
EditorGUILayout.LabelField("Picture", EditorStyles.miniLabel, GUILayout.Width(188));
EditorGUILayout.LabelField("预览", EditorStyles.miniLabel, GUILayout.Width(55));
EditorGUILayout.LabelField("ResourcesPath", EditorStyles.miniLabel);
EditorGUILayout.LabelField("", GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
}
private void DrawDataRow(LauncherRowData data, ref LauncherRowData toDelete)
{
EditorGUILayout.BeginHorizontal();
// Id只读
EditorGUILayout.LabelField(data.Id.ToString(), GUILayout.Width(35));
// Lv可编辑
data.Lv = EditorGUILayout.IntField(data.Lv, GUILayout.Width(50));
// LanguageKey自动只读
EditorGUILayout.LabelField(GetLanguageKey(data.Lv), GUILayout.Width(260));
// Picture + 设置按钮
EditorGUILayout.BeginHorizontal(GUILayout.Width(188));
data.Picture = EditorGUILayout.TextField(data.Picture, GUILayout.Width(120));
GUI.enabled = picTableSO != null;
if (GUILayout.Button("设置预览图", EditorStyles.miniButton, GUILayout.Width(64)))
{
var capturedData = data;
LauncherPicPickerWindow.Show(picTableSO, "选择发射器预览图", name =>
{
capturedData.Picture = name;
spriteCache.Remove(name);
textureCache.Remove(name);
Repaint();
});
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
// Picture 预览
DrawSpritePreview(data.Picture, 50);
// ResourcesPath自动只读
EditorGUILayout.LabelField(GetResourcesPath(data.Picture), EditorStyles.miniLabel);
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除 Id={data.Id} (Lv={data.Lv}) 的行吗?", "删除", "取消"))
{
toDelete = data;
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
private void DrawSpritePreview(string spriteName, float size)
{
Rect previewRect = GUILayoutUtility.GetRect(size, size, GUILayout.Width(size), GUILayout.Height(size));
if (string.IsNullOrEmpty(spriteName))
{
EditorGUI.DrawRect(previewRect, new Color(0.3f, 0.3f, 0.3f, 1f));
return;
}
// 优先从 ArtTableSO 获取 Sprite
Sprite sprite = GetCachedSprite(spriteName);
if (sprite != null && sprite.texture != null)
{
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 > 1)
{
float newHeight = previewRect.height / aspect;
drawRect.y += (previewRect.height - newHeight) * 0.5f;
drawRect.height = newHeight;
}
else
{
float newWidth = previewRect.width * aspect;
drawRect.x += (previewRect.width - newWidth) * 0.5f;
drawRect.width = newWidth;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords);
return;
}
// 回退:尝试直接从路径加载 Texture2D
Texture2D fallbackTex = GetCachedTexture(spriteName);
if (fallbackTex != null)
{
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
GUI.DrawTexture(previewRect, fallbackTex, ScaleMode.ScaleToFit);
}
else
{
EditorGUI.DrawRect(previewRect, new Color(0.3f, 0.3f, 0.3f, 1f));
}
}
#endregion
#region
private void AddNewRow()
{
int newId = 1;
if (dataList.Count > 0)
{
newId = dataList.Max(x => x.Id) + 1;
}
dataList.Add(new LauncherRowData
{
Id = newId,
Lv = 0,
Picture = "",
});
scrollPosition = new Vector2(0, float.MaxValue);
}
#endregion
#region
private static string GetLanguageKey(int lv) => $"UI_MainLvPanel_chapterTip_lv_{lv}";
private static string GetResourcesPath(string picture)
{
if (string.IsNullOrEmpty(picture)) return "";
return $"{RESOURCES_PATH_PREFIX}{picture}.png";
}
#endregion
#region Sprite
private Sprite GetCachedSprite(string name)
{
if (string.IsNullOrEmpty(name)) return null;
if (spriteCache.ContainsKey(name)) return spriteCache[name];
Sprite result = null;
if (picTableSO != null)
{
var item = picTableSO.Items.FirstOrDefault(x => x.Name == name);
if (item != null) result = item.Sprite;
}
spriteCache[name] = result;
return result;
}
private Texture2D GetCachedTexture(string name)
{
if (string.IsNullOrEmpty(name)) return null;
if (textureCache.ContainsKey(name)) return textureCache[name];
string path = GetResourcesPath(name);
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
textureCache[name] = tex;
return tex;
}
#endregion
#region
private class LauncherRowData
{
public int Id;
public int Lv;
public string Picture = "";
}
#endregion
}
/// <summary>
/// 发射器预览图选择弹窗
/// 展示 LauncherMergeStoryPic ArtTableSO 中的所有图片,点击选择后回调
/// </summary>
public class LauncherPicPickerWindow : EditorWindow
{
private ArtTableSO tableSO;
private Action<string> onSelect;
private string searchFilter = "";
private Vector2 scrollPos;
private const float CELL_WIDTH = 120;
private const float CELL_HEIGHT = 130;
private const float IMAGE_SIZE = 80;
public static void Show(ArtTableSO tableSO, string title, Action<string> onSelect)
{
if (tableSO == null || tableSO.Items == null || tableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", $"{title}\n资源表为空或不存在", "确定");
return;
}
var window = CreateInstance<LauncherPicPickerWindow>();
window.titleContent = new GUIContent(title);
window.tableSO = tableSO;
window.onSelect = onSelect;
window.minSize = new Vector2(650, 450);
window.ShowUtility();
}
private void OnGUI()
{
if (tableSO == null)
{
EditorGUILayout.HelpBox("资源表未加载", MessageType.Error);
return;
}
// 搜索栏
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("搜索:", GUILayout.Width(40));
searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField);
if (GUILayout.Button("×", EditorStyles.miniButton, GUILayout.Width(20)))
{
searchFilter = "";
GUI.FocusControl(null);
}
EditorGUILayout.EndHorizontal();
// 过滤
var items = tableSO.Items;
if (!string.IsNullOrEmpty(searchFilter))
{
string filter = searchFilter.ToLower();
items = items.Where(x => x.Name != null && x.Name.ToLower().Contains(filter)).ToList();
}
EditorGUILayout.LabelField($"共 {items.Count} 个资源(点击选择)", EditorStyles.miniLabel);
// 网格展示
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
float windowWidth = position.width - 30;
int columns = Mathf.Max(1, Mathf.FloorToInt(windowWidth / CELL_WIDTH));
for (int i = 0; i < items.Count; i += columns)
{
EditorGUILayout.BeginHorizontal();
for (int j = 0; j < columns && (i + j) < items.Count; j++)
{
DrawItemCell(items[i + j]);
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
private void DrawItemCell(ArtItemData item)
{
EditorGUILayout.BeginVertical("box", GUILayout.Width(CELL_WIDTH), GUILayout.Height(CELL_HEIGHT));
// 图片预览
Rect imageRect = GUILayoutUtility.GetRect(IMAGE_SIZE, IMAGE_SIZE,
GUILayout.Width(IMAGE_SIZE), GUILayout.Height(IMAGE_SIZE));
if (item.Sprite != null && item.Sprite.texture != null)
{
Sprite sprite = item.Sprite;
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
);
EditorGUI.DrawRect(imageRect, new Color(0.4f, 0.4f, 0.4f, 1f));
GUI.DrawTextureWithTexCoords(imageRect, tex, normalizedCoords);
}
else
{
EditorGUI.DrawRect(imageRect, new Color(0.3f, 0.3f, 0.3f, 1f));
GUI.Label(imageRect, "无图片", EditorStyles.centeredGreyMiniLabel);
}
// 名称按钮(点击选择)
string displayName = item.Name ?? "???";
if (displayName.Length > 16)
{
displayName = displayName.Substring(0, 14) + "..";
}
if (GUILayout.Button(displayName, EditorStyles.miniButton))
{
onSelect?.Invoke(item.Name);
Close();
}
EditorGUILayout.EndVertical();
}
}
}