Design_SubModule/Scripts/Editor/Design_Tools/Friends/FriendTreasureChestEditor.cs
2026-04-14 17:30:13 +08:00

723 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using ArtResource;
using DesignTools.Common;
using OfficeOpenXml;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace DesignTools.Friends
{
/// <summary>
/// 好友馈赠宝箱奖励配置工具
/// 读取 Docs/config/FriendTreasure.xlsx 的 Chest Sheet
/// Items 的道具从 Item.xlsx 获取,支持查看图片、编辑数量与概率、增删行
/// </summary>
public class FriendTreasureChestEditor : BaseDesignToolEditor
{
private const string FRIEND_TREASURE_EXCEL_NAME = "FriendTreasure.xlsx";
private const string FRIEND_TREASURE_SHEET_NAME = "Chest";
private const string ITEM_EXCEL_NAME = "Item.xlsx";
private const string ITEM_SHEET_NAME = "Item";
private const string ART_SO_PATH = "Assets/Art_SubModule/Art_SO";
private const int COL_ID = 1;
private const int COL_ITEMS = 2;
private const int COL_PROB = 3;
private const int DATA_START_ROW = 3;
private const int PROB_TOTAL = 1000;
private readonly List<FriendTreasureChestRowData> chestRewardList = new List<FriendTreasureChestRowData>();
private readonly Dictionary<int, FriendTreasureItemData> itemDict = new Dictionary<int, FriendTreasureItemData>();
private readonly List<ArtTableSO> allArtTables = new List<ArtTableSO>();
private readonly List<int> allItemTypes = new List<int>();
[MenuItem("策划工具/好友/好友馈赠/宝箱奖励及概率")]
public static void ShowWindow()
{
var window = GetWindow<FriendTreasureChestEditor>("宝箱奖励及概率");
window.minSize = window.GetMinWindowSize();
window.Show();
}
protected override string GetDocsPathPrefKey()
{
return "FriendTreasureChestEditor_DocsPath";
}
protected override string GetWindowTitle()
{
return "好友馈赠宝箱奖励及概率";
}
protected override Vector2 GetMinWindowSize()
{
return new Vector2(1200, 680);
}
protected override void LoadConfigData()
{
LoadAllArtTableSO();
LoadItemData();
LoadChestData();
}
protected override void DrawDataEditor()
{
DrawTipsAndToolbar();
EditorGUILayout.Space(6);
DrawChestRewardTable();
}
protected override void SaveDataToExcel()
{
SaveToExcel();
}
private void DrawTipsAndToolbar()
{
EditorGUILayout.BeginVertical("box");
// EditorGUILayout.HelpBox("保存时只会写回 FriendTreasure.xlsx 的 Chest Sheet不会改动其它 Sheet。", MessageType.Info);
// EditorGUILayout.HelpBox("新增只能追加到最后;删除任意一行后,后续 Id 会自动前移并从 1 开始重新编号。", MessageType.Info);
int totalProb = chestRewardList.Sum(x => x.Prob);
float totalPercent = totalProb / 10f;
MessageType messageType = totalProb == PROB_TOTAL ? MessageType.Info : MessageType.Error;
EditorGUILayout.HelpBox($"当前总概率:{totalProb}/{PROB_TOTAL}{totalPercent:F1}%", messageType);
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = Color.green;
if (GUILayout.Button(" 新增一行", GUILayout.Width(120), GUILayout.Height(28)))
{
AddNewRow();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawChestRewardTable()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("道具类型", EditorStyles.boldLabel, GUILayout.Width(130));
EditorGUILayout.LabelField("奖励道具可输ID / 搜索)", EditorStyles.boldLabel, GUILayout.Width(470));
EditorGUILayout.LabelField("数量", EditorStyles.boldLabel, GUILayout.Width(80));
EditorGUILayout.LabelField("概率", EditorStyles.boldLabel, GUILayout.Width(80));
EditorGUILayout.LabelField("百分比", EditorStyles.boldLabel, GUILayout.Width(90));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(70));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(70));
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
for (int i = 0; i < chestRewardList.Count; i++)
{
FriendTreasureChestRowData rowData = chestRewardList[i];
DrawSingleRow(i, rowData);
EditorGUILayout.Space(4);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void DrawSingleRow(int index, FriendTreasureChestRowData rowData)
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(rowData.Id.ToString(), GUILayout.Width(50));
DrawItemSelector(rowData);
rowData.Num = EditorGUILayout.IntField(rowData.Num, GUILayout.Width(80));
rowData.Prob = EditorGUILayout.IntField(rowData.Prob, GUILayout.Width(80));
EditorGUILayout.LabelField(GetPercentText(rowData.Prob), GUILayout.Width(90));
DrawItemPreview(rowData.RewardItemId);
GUI.backgroundColor = new Color(1f, 0.55f, 0.55f);
if (GUILayout.Button("删除", GUILayout.Width(60), GUILayout.Height(22)))
{
RemoveRow(index);
GUI.backgroundColor = Color.white;
GUIUtility.ExitGUI();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
if (itemDict.TryGetValue(rowData.RewardItemId, out FriendTreasureItemData itemData))
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(54);
EditorGUILayout.LabelField($"当前道具:{itemData.Name} ({itemData.Id})", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
}
private void DrawItemSelector(FriendTreasureChestRowData rowData)
{
EnsureInputItemId(rowData);
int currentType = GetCurrentItemType(rowData.RewardItemId);
if (currentType < 0 && allItemTypes.Count > 0)
{
currentType = allItemTypes[0];
}
int currentTypeIndex = Mathf.Max(0, allItemTypes.IndexOf(currentType));
string[] typeOptions = allItemTypes.Select(GetItemTypeDisplayName).ToArray();
if (typeOptions.Length == 0)
{
EditorGUILayout.LabelField("无可用类型", GUILayout.Width(130));
EditorGUILayout.LabelField("无可用道具", GUILayout.Width(470));
return;
}
int newTypeIndex = EditorGUILayout.Popup(currentTypeIndex, typeOptions, GUILayout.Width(130));
int selectedType = allItemTypes[newTypeIndex];
List<FriendTreasureItemData> itemsOfType = itemDict.Values
.Where(x => x.IType == selectedType)
.OrderBy(x => x.Id)
.ToList();
if (itemsOfType.Count == 0)
{
EditorGUILayout.LabelField("该类型无道具", GUILayout.Width(470));
return;
}
EditorGUILayout.BeginVertical(GUILayout.Width(470));
EditorGUILayout.BeginHorizontal();
int newInputItemId = EditorGUILayout.IntField(rowData.InputItemId, GUILayout.Width(90));
if (newInputItemId != rowData.InputItemId)
{
rowData.InputItemId = newInputItemId;
if (itemDict.ContainsKey(newInputItemId))
{
SetRowItem(rowData, newInputItemId);
currentType = GetCurrentItemType(rowData.RewardItemId);
selectedType = currentType;
itemsOfType = itemDict.Values
.Where(x => x.IType == selectedType)
.OrderBy(x => x.Id)
.ToList();
}
}
GUI.backgroundColor = new Color(0.75f, 0.9f, 1f);
if (GUILayout.Button("查找", GUILayout.Width(55), GUILayout.Height(20)))
{
EditorApplication.delayCall += () => OpenItemPickerWindow(rowData);
GUI.backgroundColor = Color.white;
GUIUtility.ExitGUI();
}
GUI.backgroundColor = Color.white;
if (itemDict.TryGetValue(rowData.InputItemId, out FriendTreasureItemData inputItemData))
{
EditorGUILayout.LabelField($"{inputItemData.Name} ({inputItemData.Id})", EditorStyles.miniLabel, GUILayout.Width(300));
}
else
{
Color oldColor = GUI.color;
GUI.color = new Color(1f, 0.35f, 0.35f);
EditorGUILayout.LabelField("未找到该 ItemId", EditorStyles.miniLabel, GUILayout.Width(300));
GUI.color = oldColor;
}
EditorGUILayout.EndHorizontal();
int currentItemIndex = itemsOfType.FindIndex(x => x.Id == rowData.RewardItemId);
if (currentItemIndex < 0)
{
currentItemIndex = 0;
SetRowItem(rowData, itemsOfType[0].Id);
}
string[] itemOptions = itemsOfType
.Select(x => $"{x.Name} ({x.Id})")
.ToArray();
int newItemIndex = EditorGUILayout.Popup(currentItemIndex, itemOptions, GUILayout.Width(460));
SetRowItem(rowData, itemsOfType[newItemIndex].Id);
EditorGUILayout.EndVertical();
}
private void EnsureInputItemId(FriendTreasureChestRowData rowData)
{
if (rowData.InputItemId <= 0)
{
rowData.InputItemId = rowData.RewardItemId;
}
}
private void SetRowItem(FriendTreasureChestRowData rowData, int itemId)
{
rowData.RewardItemId = itemId;
rowData.InputItemId = itemId;
}
private void OpenItemPickerWindow(FriendTreasureChestRowData rowData)
{
int currentType = GetCurrentItemType(rowData.RewardItemId);
DesignToolItemPickerWindow.ShowWindow(
itemDict.Values
.Select(x => new DesignToolItemPickerItem
{
Id = x.Id,
Name = x.Name,
IType = x.IType
})
.ToList(),
allItemTypes,
ITEM_TYPE_NAMES,
currentType,
rowData.RewardItemId,
position,
selectedItemId =>
{
SetRowItem(rowData, selectedItemId);
Repaint();
});
}
private void AddNewRow()
{
int defaultItemId = itemDict.Count > 0 ? itemDict.Values.OrderBy(x => x.Id).First().Id : 0;
chestRewardList.Add(new FriendTreasureChestRowData
{
Id = chestRewardList.Count + 1,
RewardItemId = defaultItemId,
InputItemId = defaultItemId,
Num = 1,
Prob = 0
});
}
private void RemoveRow(int index)
{
if (index < 0 || index >= chestRewardList.Count)
{
return;
}
chestRewardList.RemoveAt(index);
ReassignIds();
}
private void ReassignIds()
{
for (int i = 0; i < chestRewardList.Count; i++)
{
chestRewardList[i].Id = i + 1;
}
}
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<ArtTableSO>(path);
if (artTable != null)
{
allArtTables.Add(artTable);
}
}
Debug.Log($"加载了 {allArtTables.Count} 个 ArtTableSO 资源");
}
private void LoadItemData()
{
itemDict.Clear();
allItemTypes.Clear();
string itemExcelPath = GetDocsConfigFilePath(ITEM_EXCEL_NAME);
if (!File.Exists(itemExcelPath))
{
throw new FileNotFoundException($"未找到 {ITEM_EXCEL_NAME}", itemExcelPath);
}
using (var package = new ExcelPackage(new FileInfo(itemExcelPath)))
{
var worksheet = package.Workbook.Worksheets[ITEM_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"{ITEM_EXCEL_NAME} 中未找到 Sheet: {ITEM_SHEET_NAME}");
}
int idCol = -1;
int nameCol = -1;
int iTypeCol = -1;
int resCol = -1;
int columnCount = worksheet.Dimension?.Columns ?? 0;
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 列");
}
int rowCount = worksheet.Dimension?.Rows ?? 0;
for (int row = DATA_START_ROW; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text.Trim();
if (string.IsNullOrEmpty(idText))
{
continue;
}
if (!int.TryParse(idText, out int id))
{
continue;
}
string name = worksheet.Cells[row, nameCol].Text.Trim();
string iTypeText = worksheet.Cells[row, iTypeCol].Text.Trim();
string res = resCol > 0 ? worksheet.Cells[row, resCol].Text.Trim() : string.Empty;
if (!int.TryParse(iTypeText, out int iType))
{
continue;
}
itemDict[id] = new FriendTreasureItemData
{
Id = id,
Name = name,
IType = iType,
Res = res
};
}
}
foreach (int itemType in itemDict.Values.Select(x => x.IType).Distinct().OrderBy(x => x))
{
allItemTypes.Add(itemType);
}
Debug.Log($"加载了 {itemDict.Count} 个 Item 数据");
}
private void LoadChestData()
{
chestRewardList.Clear();
string excelPath = GetDocsConfigFilePath(FRIEND_TREASURE_EXCEL_NAME);
if (!File.Exists(excelPath))
{
throw new FileNotFoundException($"未找到 {FRIEND_TREASURE_EXCEL_NAME}", excelPath);
}
using (var package = new ExcelPackage(new FileInfo(excelPath)))
{
var worksheet = package.Workbook.Worksheets[FRIEND_TREASURE_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"未找到 Sheet: {FRIEND_TREASURE_SHEET_NAME}");
}
int rowCount = worksheet.Dimension?.Rows ?? 0;
for (int row = DATA_START_ROW; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, COL_ID].Text.Trim();
if (string.IsNullOrEmpty(idText))
{
continue;
}
if (!int.TryParse(idText, out int id))
{
continue;
}
string itemsText = worksheet.Cells[row, COL_ITEMS].Text.Trim();
int prob = ParseInt(worksheet.Cells[row, COL_PROB].Text.Trim());
if (!TryParseItems(itemsText, out int itemId, out int num))
{
Debug.LogWarning($"{FRIEND_TREASURE_EXCEL_NAME}/{FRIEND_TREASURE_SHEET_NAME} 第 {row} 行 Items 格式异常,已使用默认值");
itemId = itemDict.Count > 0 ? itemDict.Keys.Min() : 0;
num = 1;
}
chestRewardList.Add(new FriendTreasureChestRowData
{
Id = id,
RewardItemId = itemId,
InputItemId = itemId,
Num = num,
Prob = prob,
RowIndex = row
});
}
}
ReassignIds();
}
private void SaveToExcel()
{
ReassignIds();
if (!ValidateBeforeSave(out string errorMessage))
{
EditorUtility.DisplayDialog("校验失败", errorMessage, "确定");
return;
}
try
{
string excelPath = GetDocsConfigFilePath(FRIEND_TREASURE_EXCEL_NAME);
using (var package = new ExcelPackage(new FileInfo(excelPath)))
{
var worksheet = package.Workbook.Worksheets[FRIEND_TREASURE_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"未找到 Sheet: {FRIEND_TREASURE_SHEET_NAME}", "确定");
return;
}
int rowCount = worksheet.Dimension?.Rows ?? 0;
for (int i = 0; i < chestRewardList.Count; i++)
{
FriendTreasureChestRowData rowData = chestRewardList[i];
int row = DATA_START_ROW + i;
worksheet.Cells[row, COL_ID].Value = rowData.Id;
worksheet.Cells[row, COL_ITEMS].Value = BuildItemsJson(rowData.RewardItemId, rowData.Num);
worksheet.Cells[row, COL_PROB].Value = rowData.Prob;
}
for (int row = DATA_START_ROW + chestRewardList.Count; row <= rowCount; row++)
{
worksheet.Cells[row, COL_ID].Value = null;
worksheet.Cells[row, COL_ITEMS].Value = null;
worksheet.Cells[row, COL_PROB].Value = null;
}
package.Save();
}
EditorUtility.DisplayDialog("成功", "宝箱奖励及概率已保存到 Chest Sheet", "确定");
Debug.Log("宝箱奖励及概率已保存");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存失败: {e.Message}", "确定");
Debug.LogError($"保存宝箱奖励及概率失败: {e}");
}
}
private bool ValidateBeforeSave(out string errorMessage)
{
if (chestRewardList.Count == 0)
{
errorMessage = "至少需要保留一条宝箱奖励数据。";
return false;
}
for (int i = 0; i < chestRewardList.Count; i++)
{
FriendTreasureChestRowData rowData = chestRewardList[i];
if (!itemDict.ContainsKey(rowData.RewardItemId))
{
errorMessage = $"第 {i + 1} 行选择的 ItemId 不存在于 Item.xlsx 中。";
return false;
}
if (rowData.Num < 1)
{
errorMessage = $"第 {i + 1} 行的数量必须大于等于 1。";
return false;
}
if (rowData.Prob < 0)
{
errorMessage = $"第 {i + 1} 行的概率不能小于 0。";
return false;
}
}
int totalProb = chestRewardList.Sum(x => x.Prob);
if (totalProb != PROB_TOTAL)
{
errorMessage = $"所有概率之和必须等于 {PROB_TOTAL},当前为 {totalProb}。";
return false;
}
errorMessage = string.Empty;
return true;
}
private bool TryParseItems(string itemsText, out int itemId, out int num)
{
itemId = 0;
num = 0;
if (string.IsNullOrWhiteSpace(itemsText))
{
return false;
}
Match idMatch = Regex.Match(itemsText, @"""Id""\s*:\s*(\d+)");
Match numMatch = Regex.Match(itemsText, @"""Num""\s*:\s*(-?\d+)");
if (!idMatch.Success || !numMatch.Success)
{
return false;
}
return int.TryParse(idMatch.Groups[1].Value, out itemId) &&
int.TryParse(numMatch.Groups[1].Value, out num);
}
private string BuildItemsJson(int itemId, int num)
{
return $"[{{\"Id\":{itemId},\"Num\":{num}}}]";
}
private int ParseInt(string text)
{
return int.TryParse(text, out int value) ? value : 0;
}
private string GetPercentText(int prob)
{
return $"{prob / 10f:F1}%";
}
private int GetCurrentItemType(int itemId)
{
return itemDict.TryGetValue(itemId, out FriendTreasureItemData itemData) ? itemData.IType : -1;
}
private string GetItemTypeDisplayName(int iType)
{
return ITEM_TYPE_NAMES.TryGetValue(iType, out string typeName)
? $"{typeName} ({iType})"
: $"类型 {iType}";
}
private void DrawItemPreview(int itemId)
{
if (!itemDict.TryGetValue(itemId, out FriendTreasureItemData itemData) || string.IsNullOrEmpty(itemData.Res))
{
EditorGUILayout.LabelField("无图", GUILayout.Width(70));
return;
}
string[] parts = itemData.Res.Split(',');
if (parts.Length != 2)
{
EditorGUILayout.LabelField("无图", GUILayout.Width(70));
return;
}
string tableName = parts[0];
string artItemName = parts[1];
if (string.IsNullOrEmpty(tableName) || string.IsNullOrEmpty(artItemName))
{
EditorGUILayout.LabelField("无图", GUILayout.Width(70));
return;
}
ArtTableSO artTable = allArtTables.FirstOrDefault(x => x.TableName == tableName);
if (artTable == null)
{
EditorGUILayout.LabelField("无图", GUILayout.Width(70));
return;
}
var artItem = artTable.Items.FirstOrDefault(x => x.Name == artItemName);
if (artItem == null || artItem.Sprite == null)
{
EditorGUILayout.LabelField("无图", GUILayout.Width(70));
return;
}
Rect rect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUI.DrawTexture(rect, artItem.Sprite.texture, ScaleMode.ScaleToFit);
}
private static readonly Dictionary<int, string> ITEM_TYPE_NAMES = new Dictionary<int, string>
{
{ 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, "活动通行证代币道具" }
};
}
[Serializable]
public class FriendTreasureChestRowData
{
public int Id;
public int RewardItemId;
public int InputItemId;
public int Num;
public int Prob;
public int RowIndex;
}
[Serializable]
public class FriendTreasureItemData
{
public int Id;
public string Name;
public int IType;
public string Res;
}
}