#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace Continentis.MainGame.Card
{
///
/// 卡牌成品预览窗口。
/// 通过 CardDataEditor 中的"预览卡牌"按钮打开,或通过菜单 Continentis / Mod Tools / 卡牌预览 进入。
/// 使用 IMGUI 多层合成方式还原游戏内卡牌的大致外观。
///
public class CardPreviewer : EditorWindow
{
// ─────────────────────────────────────────────────────────────────────
// 常量与路径
// ─────────────────────────────────────────────────────────────────────
private const string k_SpritesRoot = "Assets/Sprites/MainGame/CardView/GeneralCard";
private const string k_LocaleRoot = "Assets/Mods";
// CSV 列索引(对应 CSV 首行:Key, English, Simplified Chinese, ...)
private const int k_ColEN = 1;
private const int k_ColCN = 2;
// 卡牌宽高比(基于参考图:约 2:3)
private const float k_CardAspect = 2f / 3f; // width / height
// 卡牌内部各区域相对于卡牌高度的比例(基于参考截图目测)
private const float k_ArtTopRatio = 0.09f; // 图片区顶部
private const float k_ArtBotRatio = 0.53f; // 图片区底部(包含类型徽章重叠)
private const float k_NameTopRatio = 0.065f; // 名称文字中心
private const float k_TypeBadgeRatio = 0.525f; // 类型徽章中心
private const float k_DescTopRatio = 0.565f; // 描述文字区顶部
private const float k_DescBotRatio = 0.92f; // 描述文字区底部
// ─────────────────────────────────────────────────────────────────────
// 状态
// ─────────────────────────────────────────────────────────────────────
private CardData _cardData;
// 语言切换工具栏(0=English, 1=简体中文)
private int _langIndex = 0;
private readonly string[] _langLabels = { "English", "简体中文" };
// 加载的精灵层
private Texture2D _texBackground;
private Texture2D _texOuterFrame;
private Texture2D _texNameFrame;
private Texture2D _texTypeBg;
private Texture2D _texTypeMain;
// 本地化缓存:key → [EN, CN]
private Dictionary _localizationCache;
private bool _localeLoaded = false;
// 样式缓存(延迟初始化,避免 OnEnable 时 GUI skin 未就绪)
private GUIStyle _cardNameStyle;
private GUIStyle _descStyle;
private GUIStyle _typeStyle;
private bool _stylesInitialized = false;
// 滚动位置(描述文本过长时)
private Vector2 _scrollPos;
// ─────────────────────────────────────────────────────────────────────
// 静态入口
// ─────────────────────────────────────────────────────────────────────
[MenuItem("Continentis/Mod Tools/卡牌预览")]
public static void OpenEmpty()
{
GetOrCreateWindow(null);
}
///
/// 从 CardDataEditor 调用,附带要预览的 CardData。
///
public static void OpenWith(CardData cardData)
{
GetOrCreateWindow(cardData);
}
private static CardPreviewer GetOrCreateWindow(CardData cardData)
{
var window = GetWindow(false, "卡牌预览", true);
window.minSize = new Vector2(280, 480);
window.maxSize = new Vector2(500, 900);
if (cardData != null)
window.SetTarget(cardData);
return window;
}
// ─────────────────────────────────────────────────────────────────────
// 生命周期
// ─────────────────────────────────────────────────────────────────────
private void OnEnable()
{
LoadLayerSprites();
}
private void OnDisable()
{
// 不主动 Destroy —— AssetDatabase 加载的对象由 Unity 管理生命周期
}
// ─────────────────────────────────────────────────────────────────────
// 公开接口
// ─────────────────────────────────────────────────────────────────────
public void SetTarget(CardData cardData)
{
_cardData = cardData;
_localeLoaded = false; // 切换目标时清空缓存,触发重新扫描
Repaint();
}
// ─────────────────────────────────────────────────────────────────────
// OnGUI — 主绘制
// ─────────────────────────────────────────────────────────────────────
private void OnGUI()
{
EnsureStylesInitialized();
EnsureLocalizationLoaded();
DrawToolbar();
if (_cardData == null)
{
DrawEmptyState();
return;
}
DrawCardPreview();
}
// ─────────────────────────────────────────────────────────────────────
// 工具栏(语言切换 + 选择 CardData)
// ─────────────────────────────────────────────────────────────────────
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
// 语言切换
EditorGUILayout.LabelField("语言:", GUILayout.Width(36));
int newLang = GUILayout.Toolbar(_langIndex, _langLabels, EditorStyles.toolbarButton, GUILayout.Width(160));
if (newLang != _langIndex)
{
_langIndex = newLang;
Repaint();
}
GUILayout.FlexibleSpace();
// 手动选择 CardData
if (GUILayout.Button("选择 CardData...", EditorStyles.toolbarButton, GUILayout.Width(120)))
{
ShowCardDataPicker();
}
EditorGUILayout.EndHorizontal();
// CardData 对象字段(次行,方便拖拽)
EditorGUI.BeginChangeCheck();
var picked = (CardData)EditorGUILayout.ObjectField(
"CardData", _cardData, typeof(CardData), false);
if (EditorGUI.EndChangeCheck())
SetTarget(picked);
}
// ─────────────────────────────────────────────────────────────────────
// 空状态提示
// ─────────────────────────────────────────────────────────────────────
private void DrawEmptyState()
{
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(
"请将 CardData 拖入上方字段,\n或点击\"选择 CardData...\"按钮",
new GUIStyle(EditorStyles.centeredGreyMiniLabel) { fontSize = 12, wordWrap = true },
GUILayout.ExpandWidth(true));
GUILayout.FlexibleSpace();
}
// ─────────────────────────────────────────────────────────────────────
// 卡牌合成渲染
// ─────────────────────────────────────────────────────────────────────
private void DrawCardPreview()
{
// ── 计算卡牌绘制区域(居中,保持宽高比)──────────────────────────
float availW = position.width - 16f;
float availH = position.height - 60f; // 减去工具栏高度
float cardW, cardH;
if (availW / availH < k_CardAspect)
{
cardW = availW;
cardH = availW / k_CardAspect;
}
else
{
cardH = availH;
cardW = availH * k_CardAspect;
}
float offsetX = (availW - cardW) * 0.5f + 8f;
float offsetY = 58f; // 工具栏 + ObjectField 高度之和
Rect cardRect = new Rect(offsetX, offsetY, cardW, cardH);
// ── 层 1:卡牌深色背景 ────────────────────────────────────────────
if (_texBackground != null)
GUI.DrawTexture(cardRect, _texBackground, ScaleMode.StretchToFill, true);
else
EditorGUI.DrawRect(cardRect, new Color(0.08f, 0.08f, 0.12f));
// ── 层 2:卡牌图片(上半区域,内框范围内)────────────────────────
Rect artRect = RectFromRatios(cardRect,
padH: 0.04f, padV: k_ArtTopRatio,
widthRatio: 0.92f,
heightRatio: k_ArtBotRatio - k_ArtTopRatio);
if (_cardData.cardSprite != null)
{
GUI.DrawTexture(artRect, _cardData.cardSprite.texture, ScaleMode.ScaleToFit, true);
}
else
{
EditorGUI.DrawRect(artRect, new Color(0.15f, 0.15f, 0.2f));
GUI.Label(artRect, " 无卡牌图片",
new GUIStyle(EditorStyles.centeredGreyMiniLabel) { alignment = TextAnchor.MiddleCenter });
}
// ── 层 4:外框 ────────────────────────────────────────────────────
if (_texOuterFrame != null)
GUI.DrawTexture(cardRect, _texOuterFrame, ScaleMode.StretchToFill, true);
// ── 层 5:名称框背景 ──────────────────────────────────────────────
if (_texNameFrame != null)
{
Rect nameFrameRect = RectFromRatios(cardRect,
padH: 0.1f, padV: k_NameTopRatio - 0.036f,
widthRatio: 0.80f,
heightRatio: 0.072f);
GUI.DrawTexture(nameFrameRect, _texNameFrame, ScaleMode.StretchToFill, true);
}
// ── 层 6:卡牌名称文字 ────────────────────────────────────────────
string cardName = GetLocalizedText(_cardData.displayName);
Rect nameTextRect = new Rect(
cardRect.x + cardRect.width * 0.1f,
cardRect.y + cardRect.height * (k_NameTopRatio - 0.025f),
cardRect.width * 0.80f,
cardRect.height * 0.055f);
GUI.Label(nameTextRect, cardName, _cardNameStyle);
// ── 层 7:类型徽章 ────────────────────────────────────────────────
Rect typeBadgeRect = RectFromRatios(cardRect,
padH: 0.25f, padV: k_TypeBadgeRatio - 0.028f,
widthRatio: 0.50f,
heightRatio: 0.056f);
if (_texTypeBg != null)
GUI.DrawTexture(typeBadgeRect, _texTypeBg, ScaleMode.StretchToFill, true);
if (_texTypeMain != null)
GUI.DrawTexture(typeBadgeRect, _texTypeMain, ScaleMode.StretchToFill, true);
GUI.Label(typeBadgeRect, _cardData.cardType.ToString(), _typeStyle);
// ── 层 8:描述文字区(滚动) ──────────────────────────────────────
Rect descArea = new Rect(
cardRect.x + cardRect.width * 0.06f,
cardRect.y + cardRect.height * k_DescTopRatio,
cardRect.width * 0.88f,
cardRect.height * (k_DescBotRatio - k_DescTopRatio));
string rawFunction = GetLocalizedText(_cardData.functionText);
string displayDesc = StripMacros(rawFunction);
// 动态调整字号使文字适应区域
_descStyle.fontSize = CalculateFontSize(cardH);
// 用 Scroll View 防止超长文字溢出
_scrollPos = GUI.BeginScrollView(descArea, _scrollPos,
new Rect(0, 0, descArea.width - 10f,
Mathf.Max(descArea.height,
_descStyle.CalcHeight(new GUIContent(displayDesc), descArea.width - 14f))));
GUI.Label(new Rect(0, 4f, descArea.width - 14f, descArea.height + 200f),
displayDesc, _descStyle);
GUI.EndScrollView();
}
// ─────────────────────────────────────────────────────────────────────
// 资源加载
// ─────────────────────────────────────────────────────────────────────
private void LoadLayerSprites()
{
_texBackground = LoadTex("CardBackground_Default");
_texOuterFrame = LoadTex("CardOuterFrame");
_texNameFrame = LoadTex("CardNameFrame");
_texTypeBg = LoadTex("CardTypeBackground");
_texTypeMain = LoadTex("CardTypeMain");
}
private static Texture2D LoadTex(string fileName)
{
// 优先加载 PNG,再尝试其他扩展名
string path = $"{k_SpritesRoot}/{fileName}.png";
var spr = AssetDatabase.LoadAssetAtPath(path);
if (spr != null) return spr.texture;
// 降级:直接加载 Texture2D
return AssetDatabase.LoadAssetAtPath(path);
}
// ─────────────────────────────────────────────────────────────────────
// 本地化加载(惰性 + 扫描所有 CSV)
// ─────────────────────────────────────────────────────────────────────
private void EnsureLocalizationLoaded()
{
if (_localeLoaded) return;
_localizationCache = new Dictionary(StringComparer.Ordinal);
string[] guids = AssetDatabase.FindAssets("t:TextAsset Localization", new[] { k_LocaleRoot });
foreach (string guid in guids)
{
string csvPath = AssetDatabase.GUIDToAssetPath(guid);
if (!csvPath.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) continue;
try
{
ParseCsv(File.ReadAllText(csvPath));
}
catch (Exception e)
{
Debug.LogWarning($"[CardPreviewer] 解析本地化文件失败:{csvPath}\n{e.Message}");
}
}
_localeLoaded = true;
}
///
/// 解析单个 CSV 文件,将 Key → [EN, CN] 写入缓存。
/// 支持 RFC 4180 标准的双引号转义字段。
///
private void ParseCsv(string content)
{
// 按行拆分
string[] lines = content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
if (lines.Length < 2) return;
// 首行为列头,找到 English / Simplified Chinese 的列索引
string[] headers = SplitCsvRow(lines[0]);
int colEN = FindColumnIndex(headers, "English");
int colCN = FindColumnIndex(headers, "Simplified Chinese");
for (int i = 1; i < lines.Length; i++)
{
if (string.IsNullOrWhiteSpace(lines[i])) continue;
string[] cells = SplitCsvRow(lines[i]);
if (cells.Length == 0) continue;
string key = cells[0].Trim();
if (string.IsNullOrEmpty(key)) continue;
string en = colEN >= 0 && colEN < cells.Length ? cells[colEN] : string.Empty;
string cn = colCN >= 0 && colCN < cells.Length ? cells[colCN] : string.Empty;
_localizationCache[key] = new[] { en, cn };
}
}
private static int FindColumnIndex(string[] headers, string columnName)
{
for (int i = 0; i < headers.Length; i++)
if (string.Equals(headers[i].Trim(), columnName, StringComparison.OrdinalIgnoreCase))
return i;
return -1;
}
///
/// RFC 4180 兼容的 CSV 行拆分,处理双引号包围的字段及内部双引号转义。
///
private static string[] SplitCsvRow(string line)
{
var fields = new List();
bool inQuote = false;
var current = new System.Text.StringBuilder();
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (inQuote)
{
if (c == '"')
{
// 双引号转义 ("") → 单引号字符
if (i + 1 < line.Length && line[i + 1] == '"')
{
current.Append('"');
i++;
}
else
{
inQuote = false;
}
}
else
{
current.Append(c);
}
}
else
{
if (c == '"')
{
inQuote = true;
}
else if (c == ',')
{
fields.Add(current.ToString());
current.Clear();
}
else
{
current.Append(c);
}
}
}
fields.Add(current.ToString());
return fields.ToArray();
}
// ─────────────────────────────────────────────────────────────────────
// 文本处理
// ─────────────────────────────────────────────────────────────────────
///
/// 根据当前语言返回本地化文本。若未找到,退回显示 Key 本身。
///
private string GetLocalizedText(string key)
{
if (string.IsNullOrEmpty(key)) return string.Empty;
if (_localizationCache != null && _localizationCache.TryGetValue(key, out string[] texts))
{
string text = _langIndex == 0 ? texts[0] : texts[1];
return string.IsNullOrEmpty(text) ? $"[{key}]" : text;
}
return $"[{key}]"; // 未找到时显示 Key
}
///
/// 将功能文本中的宏替换为可读形式,用于在预览中近似展示。
/// $Keyword("xxx") → [xxx]
/// $Attribute("xxx") → {xxx}
///
private static string StripMacros(string text)
{
if (string.IsNullOrEmpty(text)) return text;
text = Regex.Replace(text, @"\$Keyword\(""([^""]+)""\)", m => $"[{m.Groups[1].Value}]");
text = Regex.Replace(text, @"\$Attribute\(""([^""]+)""\)", m => $"{{{m.Groups[1].Value}}}");
return text;
}
// ─────────────────────────────────────────────────────────────────────
// 样式初始化
// ─────────────────────────────────────────────────────────────────────
private void EnsureStylesInitialized()
{
if (_stylesInitialized) return;
_cardNameStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 16,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter,
wordWrap = false,
normal = { textColor = Color.white }
};
_typeStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 12,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter,
wordWrap = false,
// 全部状态一律使用相同颜色,防止 Unity GUI skin 默认的悬停高亮
normal = { textColor = new Color(0.2f, 0.55f, 0.2f) },
hover = { textColor = new Color(0.2f, 0.55f, 0.2f) },
active = { textColor = new Color(0.2f, 0.55f, 0.2f) },
focused = { textColor = new Color(0.2f, 0.55f, 0.2f) }
};
_descStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 13,
fontStyle = FontStyle.Normal,
alignment = TextAnchor.UpperLeft,
wordWrap = true,
richText = true,
normal = { textColor = new Color(0.88f, 0.88f, 0.88f) }
};
_stylesInitialized = true;
}
// ─────────────────────────────────────────────────────────────────────
// 工具方法
// ─────────────────────────────────────────────────────────────────────
///
/// 在 baseRect 内部按比例偏移创建子矩形。
/// padH / padV 是左右/上方的水平/垂直内边距(相对于卡牌宽/高的比值)。
///
private static Rect RectFromRatios(Rect baseRect, float padH, float padV,
float widthRatio, float heightRatio)
{
return new Rect(
baseRect.x + baseRect.width * padH,
baseRect.y + baseRect.height * padV,
baseRect.width * widthRatio,
baseRect.height * heightRatio);
}
///
/// 根据当前卡牌绘制高度动态计算字号,确保缩小窗口时文字不溢出。
///
private static int CalculateFontSize(float cardHeight)
{
// 字号随卡牌高度线性缩放,base 400px → 13pt
int size = Mathf.RoundToInt(cardHeight * 13f / 400f);
return Mathf.Clamp(size, 9, 16);
}
///
/// 打开 Unity 内置的 CardData 类型选择器(Object Picker)。
///
private void ShowCardDataPicker()
{
// 用 SearchService 或简单的 AssetDatabase 弹窗
string[] guids = AssetDatabase.FindAssets("t:CardData");
if (guids.Length == 0)
{
Debug.LogWarning("[CardPreviewer] 项目中未找到任何 CardData 资产。");
return;
}
// 收集所有 CardData 资产供选择
var items = new List();
var paths = new List();
foreach (string guid in guids)
{
string p = AssetDatabase.GUIDToAssetPath(guid);
items.Add(Path.GetFileNameWithoutExtension(p));
paths.Add(p);
}
// 弹出简易选择窗口
CardDataPickerWindow.Show(items, paths, selectedPath =>
{
var cd = AssetDatabase.LoadAssetAtPath(selectedPath);
SetTarget(cd);
});
}
// ─────────────────────────────────────────────────────────────────────
// 内部:CardData 选择弹窗
// ─────────────────────────────────────────────────────────────────────
private class CardDataPickerWindow : EditorWindow
{
private List _names;
private List _paths;
private Action _onSelect;
private string _search = "";
private Vector2 _scroll;
public static void Show(List names, List paths, Action onSelect)
{
var w = GetWindow(true, "选择 CardData", true);
w.minSize = new Vector2(260, 340);
w._names = names;
w._paths = paths;
w._onSelect = onSelect;
}
private void OnGUI()
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
_search = GUILayout.TextField(_search, GUI.skin.FindStyle("ToolbarSearchTextField") ?? EditorStyles.textField);
if (GUILayout.Button("", GUI.skin.FindStyle("ToolbarSearchCancelButton") ?? EditorStyles.miniButton))
_search = "";
GUILayout.EndHorizontal();
_scroll = EditorGUILayout.BeginScrollView(_scroll);
for (int i = 0; i < _names.Count; i++)
{
if (!string.IsNullOrEmpty(_search) &&
_names[i].IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0)
continue;
if (GUILayout.Button(_names[i], EditorStyles.label))
{
_onSelect?.Invoke(_paths[i]);
Close();
}
}
EditorGUILayout.EndScrollView();
}
}
}
}
#endif