627 lines
29 KiB
C#
627 lines
29 KiB
C#
|
|
#if UNITY_EDITOR
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Text.RegularExpressions;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
namespace Continentis.MainGame.Card
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 卡牌成品预览窗口。
|
|||
|
|
/// 通过 CardDataEditor 中的"预览卡牌"按钮打开,或通过菜单 Continentis / Mod Tools / 卡牌预览 进入。
|
|||
|
|
/// 使用 IMGUI 多层合成方式还原游戏内卡牌的大致外观。
|
|||
|
|
/// </summary>
|
|||
|
|
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<string, string[]> _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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 从 CardDataEditor 调用,附带要预览的 CardData。
|
|||
|
|
/// </summary>
|
|||
|
|
public static void OpenWith(CardData cardData)
|
|||
|
|
{
|
|||
|
|
GetOrCreateWindow(cardData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static CardPreviewer GetOrCreateWindow(CardData cardData)
|
|||
|
|
{
|
|||
|
|
var window = GetWindow<CardPreviewer>(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<Sprite>(path);
|
|||
|
|
if (spr != null) return spr.texture;
|
|||
|
|
|
|||
|
|
// 降级:直接加载 Texture2D
|
|||
|
|
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
// 本地化加载(惰性 + 扫描所有 CSV)
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void EnsureLocalizationLoaded()
|
|||
|
|
{
|
|||
|
|
if (_localeLoaded) return;
|
|||
|
|
_localizationCache = new Dictionary<string, string[]>(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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 解析单个 CSV 文件,将 Key → [EN, CN] 写入缓存。
|
|||
|
|
/// 支持 RFC 4180 标准的双引号转义字段。
|
|||
|
|
/// </summary>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// RFC 4180 兼容的 CSV 行拆分,处理双引号包围的字段及内部双引号转义。
|
|||
|
|
/// </summary>
|
|||
|
|
private static string[] SplitCsvRow(string line)
|
|||
|
|
{
|
|||
|
|
var fields = new List<string>();
|
|||
|
|
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();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
// 文本处理
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 根据当前语言返回本地化文本。若未找到,退回显示 Key 本身。
|
|||
|
|
/// </summary>
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 将功能文本中的宏替换为可读形式,用于在预览中近似展示。
|
|||
|
|
/// $Keyword("xxx") → [xxx]
|
|||
|
|
/// $Attribute("xxx") → {xxx}
|
|||
|
|
/// </summary>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
// 工具方法
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 在 baseRect 内部按比例偏移创建子矩形。
|
|||
|
|
/// padH / padV 是左右/上方的水平/垂直内边距(相对于卡牌宽/高的比值)。
|
|||
|
|
/// </summary>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 根据当前卡牌绘制高度动态计算字号,确保缩小窗口时文字不溢出。
|
|||
|
|
/// </summary>
|
|||
|
|
private static int CalculateFontSize(float cardHeight)
|
|||
|
|
{
|
|||
|
|
// 字号随卡牌高度线性缩放,base 400px → 13pt
|
|||
|
|
int size = Mathf.RoundToInt(cardHeight * 13f / 400f);
|
|||
|
|
return Mathf.Clamp(size, 9, 16);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 打开 Unity 内置的 CardData 类型选择器(Object Picker)。
|
|||
|
|
/// </summary>
|
|||
|
|
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<string>();
|
|||
|
|
var paths = new List<string>();
|
|||
|
|
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<CardData>(selectedPath);
|
|||
|
|
SetTarget(cd);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
// 内部:CardData 选择弹窗
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private class CardDataPickerWindow : EditorWindow
|
|||
|
|
{
|
|||
|
|
private List<string> _names;
|
|||
|
|
private List<string> _paths;
|
|||
|
|
private Action<string> _onSelect;
|
|||
|
|
private string _search = "";
|
|||
|
|
private Vector2 _scroll;
|
|||
|
|
|
|||
|
|
public static void Show(List<string> names, List<string> paths, Action<string> onSelect)
|
|||
|
|
{
|
|||
|
|
var w = GetWindow<CardDataPickerWindow>(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
|