架构大更

This commit is contained in:
SoulliesOfficial
2026-03-20 11:56:50 -04:00
parent e60ef64d01
commit d09b58fd80
3663 changed files with 15232012 additions and 105579 deletions

View File

@@ -1,16 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Character;
using Continentis.MainGame.Commands;
using NUnit.Framework;
using SLSFramework.General;
using SLSFramework.UModAssistance;
using UnityEngine;
namespace Continentis.MainGame.Card
{
#region Fundamental
public partial class CardInstance
{
public List<string> GetElementalKeywords(List<string> overrideKeywords = null)
@@ -18,80 +12,84 @@ namespace Continentis.MainGame.Card
overrideKeywords ??= contentSubmodule.keywords;
return overrideKeywords.Filtered(kw => MainGameManager.Instance.elementTags.Contains(kw));
}
public bool HasKeyword(string keyword)
{
return contentSubmodule.keywords.Contains(keyword);
}
}
#endregion
#region Attributes
#region Attributes
public partial class CardInstance
{
/// <summary>
/// 设置可变属性值
/// 设置可变属性值
/// </summary>
/// <param name="attributeName">属性名,通常为</param>
/// <param name="additive">是否为叠加true为叠加false为覆盖true时originalValue为外部传入值</param>
/// <param name="originalValue">原始伤害值仅在additive为true时有效否则此值被覆盖为BaseAttribute</param>
/// <param name="baseOffset">伤害增量</param>
public void SetVariableAttribute(string attributeName, int baseOffset, bool additive = false, int originalValue = 0)
public void SetVariableAttribute(string attributeName, int baseOffset, bool additive = false,
int originalValue = 0)
{
string baseName = "Base" + attributeName;
string baseOffsetName = baseName + "Offset";
var baseName = "Base" + attributeName;
var baseOffsetName = baseName + "Offset";
if (!additive) originalValue = GetAttribute(baseName);
SetAttribute(attributeName, originalValue);
SetAttribute(baseOffsetName, baseOffset);
ModifyAttribute(attributeName, baseOffset);
}
/// <summary>
/// 检查卡牌是否具有某属性
/// 检查卡牌是否具有某属性
/// </summary>
public bool HasAttribute(string attributeName)
{
return attributeSubmodule.attributeGroup.current.ContainsKey(attributeName);
}
/// <summary>
/// 获取卡牌的属性值
/// 获取卡牌的属性值
/// </summary>
public int GetAttribute(string attributeName, int defaultValue = 0)
{
return attributeSubmodule.GetRoundCurrentAttribute(attributeName, defaultValue);
}
public float GetRawAttribute(string attributeName, float defaultValue = 0)
{
return attributeSubmodule.GetCurrentAttribute(attributeName, defaultValue);
}
/// <summary>
/// 设置卡牌的属性值
/// 设置卡牌的属性值
/// </summary>
public void SetAttribute(string attributeName, int value)
{
attributeSubmodule.attributeGroup.current[attributeName] = value;
}
/// <summary>
/// 设置卡牌的属性值
/// 设置卡牌的属性值
/// </summary>
public void SetAttribute(string attributeName, float value)
{
attributeSubmodule.attributeGroup.current[attributeName] = value;
}
/// <summary>
/// 修改卡牌的属性值
/// 修改卡牌的属性值
/// </summary>
public void ModifyAttribute(string attributeName, int delta)
{
attributeSubmodule.attributeGroup.current[attributeName] += delta;
}
}
#endregion
}

View File

@@ -0,0 +1,18 @@
namespace Continentis.MainGame.Card
{
/// <summary>
/// 基于 EditorBaseCollection 自动生成的卡牌属性常量字典。
/// 包含所有配置的 Key以防止手写出现 Typo。
/// </summary>
public static class CardAttributes
{
/// <summary> 体力值消耗 </summary>
public const string StaminaCost = "StaminaCost";
/// <summary> 魔力值消耗 </summary>
public const string ManaCost = "ManaCost";
/// <summary> 目标数量0为自身-1为全体 </summary>
public const string TargetCount = "TargetCount";
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3065cb6a46856c4c8eabd7e2e3fae9f

View File

@@ -1,99 +1,97 @@
using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Card;
using Continentis.MainGame.Character;
using SLSFramework.General;
using UnityEngine;
namespace Continentis.MainGame.Card
{
public partial class CardCombatBuffBase : CardBuffBase
public abstract partial class CardCombatBuffBase : CardBuffBase
{
public CardLogicBase sourceCard;
public CountSubmodule combatActionTimeSubmodule;
public CountSubmodule combatRoundTimeSubmodule;
public CardLogicBase sourceCard;
}
public partial class CardCombatBuffBase
{
/// <summary>
/// 基类的泛型签名由此层密封并桥接到强类型版本,子类请实现
/// <see cref="OnBuffApply(out CardCombatBuffBase)" />。
/// </summary>
public sealed override bool OnBuffApply(out BuffBase<CardInstance> existingBuff)
{
throw new System.NotImplementedException("请使用类型约束更强的OnBuffApply方法");
var result = OnBuffApply(out var typed);
existingBuff = typed;
return result;
}
public virtual bool OnBuffApply(out CardCombatBuffBase existingBuff)
{
throw new System.NotImplementedException(); //需要在子类中实现
}
/// <summary>
/// Buff被尝试添加到卡牌时调用。
/// 返回 true 表示这是全新 Buff返回 false 表示已有同类 Buff 存在(通过 out 参数返回)。
/// </summary>
public abstract bool OnBuffApply(out CardCombatBuffBase existingBuff);
public override void OnAfterFirstApply()
{
}
}
public override void OnBuffRemove()
{
//attachedCard.combatBuffSubmodule.buffList.For(buff => buff.eventSubmodule.onOtherBuffRemoved.Invoke(this));
attributeSubmodule?.RefreshAllModifiedAttributes();
}
public virtual void OnUsage()
{
}
public virtual void OnRoundStart()
{
}
public virtual void OnRoundEnd()
{
}
public virtual void OnActionStart()
{
}
public virtual void OnActionEnd()
{
}
}
public partial class CardCombatBuffBase
{
protected bool FindExistingSameBuff<T>(out T existingBuff) where T : CardBuffBase
{
bool result = FindExistingSameBuffs(out List<T> existingBuffs, attachedCard.combatBuffSubmodule.buffList);
var result = FindExistingSameBuffs(out List<T> existingBuffs, attachedCard.combatBuffSubmodule.buffList);
existingBuff = result ? existingBuffs[0] : null;
return result;
}
public override void Apply(CardInstance attachedCard, CharacterBase sourceCharacter = null)
{
this.Apply(attachedCard, sourceCharacter, null);
Apply(attachedCard, sourceCharacter);
}
public void Apply(CardInstance attachedCard, CharacterBase sourceCharacter = null, CardLogicBase sourceCard = null)
public void Apply(CardInstance attachedCard, CharacterBase sourceCharacter = null,
CardLogicBase sourceCard = null)
{
this.attachedCard = attachedCard;
this.sourceCharacter = sourceCharacter;
this.sourceCard = sourceCard;
if (OnBuffApply(out CardCombatBuffBase existingBuff))
if (OnBuffApply(out var existingBuff))
{
this.attachedCard.combatBuffSubmodule.buffList.Add(this);
OnAfterFirstApply();
}
attributeSubmodule?.RefreshAllModifiedAttributes();
attachedCard.contentSubmodule.dirtyMark = true;
Debug.Log(base.attachedCard.contentSubmodule.interpretedFunctionText);
Debug.Log(this.attachedCard.contentSubmodule.interpretedFunctionText);
}
public override void Remove()
@@ -105,7 +103,7 @@ namespace Continentis.MainGame.Card
public override void UntriggerRemove()
{
this.attachedCard.combatBuffSubmodule.buffList.Remove(this);
attachedCard.combatBuffSubmodule.buffList.Remove(this);
}
}
}

View File

@@ -1,92 +1,188 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Continentis.MainGame.Character;
using SLSFramework.General;
using UnityEngine;
using NaughtyAttributes;
using Sirenix.OdinInspector;
using SLSFramework.UModAssistance;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Continentis.MainGame.Card
{
public enum CardType
{
Attack = 0,
Skill = 10,
Power = 20,
Skill = 10,
Power = 20,
Status = 30,
Curse = 40,
Item = 50,
Curse = 40,
Item = 50,
}
// ─────────────────────────────────────────────────────────────────────────
// CardData — ScriptableObject 数据定义
// ─────────────────────────────────────────────────────────────────────────
[CreateAssetMenu(menuName = "Continentis/MainGame/Card/CardData", fileName = "CardData")]
public partial class CardData : ScriptableObject
{
[Header("Fundamental")]
public string modName;
public string categoryName;
// ── Fundamental ───────────────────────────────────────────────────────
[BoxGroup("Fundamental"), PropertyOrder(0)]
[ValueDropdown("@CardData.GetCardLogicDropdownItems()", AppendNextDrawer = true, DisableGUIInAppendedDrawer = true)]
[LabelText("逻辑类名"), OnValueChanged("OnCardLogicClassSelected")]
public string className;
[BoxGroup("Fundamental"), PropertyOrder(1)]
[ReadOnly, LabelText("Mod 名称")]
public string modName;
[BoxGroup("Fundamental"), PropertyOrder(2)]
[ReadOnly, LabelText("分类名称")]
public string categoryName;
[BoxGroup("Fundamental"), PropertyOrder(3)]
[ReadOnly, LabelText("显示名称 Key")]
public string displayName;
[BoxGroup("Fundamental"), PropertyOrder(4)]
[LabelText("稀有度")]
public Rarity cardRarity;
[BoxGroup("Fundamental"), PropertyOrder(5)]
[LabelText("卡牌类型")]
public CardType cardType;
[BoxGroup("Fundamental"), PropertyOrder(6)]
[ListDrawerSettings(ShowIndexLabels = false, DraggableItems = true)]
[ValueDropdown("GetAvailableKeywords", AppendNextDrawer = true)]
[LabelText("关键词")]
public List<string> keywords;
// ── Display ───────────────────────────────────────────────────────────
[BoxGroup("Display"), PropertyOrder(7)]
[PreviewField(80, ObjectFieldAlignment.Left)]
[LabelText("卡牌图片")]
public Sprite cardSprite;
[BoxGroup("Display"), PropertyOrder(8)]
[LabelText("功能文本 Key")]
public string functionText;
[BoxGroup("Display"), PropertyOrder(9)]
[LabelText("卡牌描述 Key")]
public string cardDescription;
[BoxGroup("Display"), PropertyOrder(10)]
[ListDrawerSettings(ShowIndexLabels = false), LabelText("布局标签")]
public List<string> cardLayoutTags;
public string functionText;
public string cardDescription;
// ── Attributes ────────────────────────────────────────────────────────
[Header("Intention")]
public List<string> intentionIconKeys;
public List<string> intentionValueNames;
public string intentionTextOverride;
public float baseWeight = 0f;
[Header("Attributes")] [Tooltip("可变属性这个属性会自动设置BaseAttr进入Original设置AttrBaseAttrOffset=0以及DisplayAttr进入Current")]
[TabGroup("Data", "属性"), PropertyOrder(20)]
[DictionaryDrawerSettings(KeyLabel = "属性名", ValueLabel = "属性值")]
[Tooltip("可变属性:自动设置 BaseAttr → Original设置 Attr / BaseAttrOffset=0 / DisplayAttr → Current")]
[LabelText("可变属性 (Variable)")]
public SerializableDictionary<string, float> variableAttributes = new SerializableDictionary<string, float>();
[Tooltip("基础属性,不会改变,通常情况下不会直接使用")]
[TabGroup("Data", "属性"), PropertyOrder(21)]
[DictionaryDrawerSettings(KeyLabel = "属性名", ValueLabel = "属性值")]
[Tooltip("基础属性,运行时不会改变,通常不直接使用。")]
[LabelText("基础属性 (Original)")]
public SerializableDictionary<string, float> originalAttributes = new SerializableDictionary<string, float>();
[TabGroup("Data", "属性"), PropertyOrder(22)]
[DictionaryDrawerSettings(KeyLabel = "属性名", ValueLabel = "初始值表达式")]
[FormerlySerializedAs("endowingCurrentAttributes")]
[Tooltip("初始化时赋予给CurrentAttributes的属性第一栏是属性名第二栏是初始化时使用对应名称的OriginalAttributes的留空则默认为0如果是float数字则直接使用该数字")]
[Tooltip("运行时当前属性Value 填写对应 OriginalAttributes 的 Key 名,或直接填浮点数,留空则默认为 0。")]
[LabelText("运行时当前属性 (Runtime Current)")]
public SerializableDictionary<string, string> runtimeCurrentAttributes = new SerializableDictionary<string, string>();
[Header("Upgrade")] public CardUpgradeNode upgradeNode;
// ── Upgrade ───────────────────────────────────────────────────────────
[Header("References")] public List<string> prefabRefs = new List<string>();
[TabGroup("Data", "升级"), PropertyOrder(40)]
[HideLabel]
public CardUpgradeNode upgradeNode;
// ── References ────────────────────────────────────────────────────────
[TabGroup("Data", "引用"), PropertyOrder(50)]
[ListDrawerSettings(ShowIndexLabels = false, DraggableItems = false)]
[ValueDropdown("GetAvailablePrefabs", AppendNextDrawer = true)]
[LabelText("Prefab 引用")]
public List<string> prefabRefs = new List<string>();
[TabGroup("Data", "引用"), PropertyOrder(51)]
[ListDrawerSettings(ShowIndexLabels = false, DraggableItems = false)]
[ValueDropdown("GetAvailableCardData", AppendNextDrawer = true)]
[LabelText("衍生卡牌数据引用")]
public List<string> derivativeCardDataRefs = new List<string>();
[TabGroup("Data", "引用"), PropertyOrder(52)]
[ListDrawerSettings(ShowIndexLabels = false, DraggableItems = false)]
[ValueDropdown("GetAvailableCharacterData", AppendNextDrawer = true)]
[LabelText("衍生角色数据引用")]
public List<string> derivativeCharacterDataRefs = new List<string>();
// ── Intention ────────────────────────────────────────────────────────
[TabGroup("Data", "意图"), PropertyOrder(60)]
[ListDrawerSettings(ShowIndexLabels = false)]
[ValueDropdown("GetAvailableIntentionIcons", AppendNextDrawer = true)]
[LabelText("意图图标 Keys")]
public List<string> intentionIconKeys;
[TabGroup("Data", "意图"), PropertyOrder(61)]
[ListDrawerSettings(ShowIndexLabels = false)]
[ValueDropdown("GetVariableAttributeKeys", AppendNextDrawer = true)]
[LabelText("意图数值名")]
public List<string> intentionValueNames;
[TabGroup("Data", "意图"), PropertyOrder(62)]
[LabelText("意图文本覆盖")]
public string intentionTextOverride;
[FormerlySerializedAs("baseWeight")]
[TabGroup("Data", "意图"), PropertyOrder(63)]
[LabelText("基础权重"), Range(0f, 100f)]
public float intentionBaseWeight = 0f;
}
// ─────────────────────────────────────────────────────────────────────────
// Runtime: 关键词查询
// ─────────────────────────────────────────────────────────────────────────
public partial class CardData
{
/// <summary>
/// 检查此卡牌是否包含指定关键词。
/// </summary>
public bool HasKeyword(string keyword)
{
return keywords.Contains(keyword);
}
}
// ─────────────────────────────────────────────────────────────────────────
// Runtime: 数据库查询
// ─────────────────────────────────────────────────────────────────────────
public partial class CardData
{
/// <summary>
/// 通过 DataID 从 ModManager 数据库查找 CardData。
/// </summary>
public static CardData Get(string dataID)
{
return ModManager.GetData<CardData>(dataID);
}
}
// ─────────────────────────────────────────────────────────────────────────
// Runtime: 衍生数据访问
// ─────────────────────────────────────────────────────────────────────────
public partial class CardData
{
/// <summary>
/// 通过索引获取衍生卡牌数据
/// 通过索引获取衍生卡牌数据
/// </summary>
public CardData GetDerivativeCardData(int index)
{
@@ -94,13 +190,11 @@ namespace Continentis.MainGame.Card
}
/// <summary>
/// 通过索引获取衍生角色数据
/// 通过索引获取衍生角色数据
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public CharacterData GetDerivativeCharacterData(int index)
{
return ModManager.GetData<CharacterData>(derivativeCharacterDataRefs[index]);
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Continentis.MainGame.Base;
using Continentis.MainGame.Character;
using Continentis.MainGame.Combat;
using Continentis.MainGame.UI;
@@ -194,7 +195,11 @@ namespace Continentis.MainGame.Card
if (cardData.intentionIconKeys.Count > 0)
{
string iconMarkKey = cardData.intentionIconKeys[0];
iconSprite = MainGameManager.Instance.basePrefabs.intentionMarkIcons[iconMarkKey];
Sprite modSprite = EditorBaseCollection.GetIntentionIcon(iconMarkKey);
if (modSprite != null)
iconSprite = modSprite;
else
Debug.LogWarning($"[CardInstance] 意图图标 '{iconMarkKey}' 未在任何 EditorBaseCollection 中找到,使用默认 Unknown 图标。");
}
if (string.IsNullOrEmpty(cardData.intentionTextOverride))

View File

@@ -0,0 +1,78 @@
namespace Continentis.MainGame.Card
{
/// <summary>
/// 基于 EditorBaseCollection 自动生成的卡牌关键词常量字典。
/// 包含所有配置的 Key以防止手写出现 Typo。
/// </summary>
public static class CardKeywords
{
/// <summary> 可选目标仅自身 </summary>
public const string TargetSelf = "TargetSelf";
/// <summary> 可选目标为敌人 </summary>
public const string TargetEnemies = "TargetEnemies";
/// <summary> 可选目标为友方(不包括自己) </summary>
public const string TargetAllies = "TargetAllies";
/// <summary> 可选目标为全体角色 </summary>
public const string TargetAll = "TargetAll";
/// <summary> 物理 </summary>
public const string Physics = "Physics";
/// <summary> 魔法 </summary>
public const string Magic = "Magic";
/// <summary> 打击(力量) </summary>
public const string Strike = "Strike";
/// <summary> 突刺(敏捷) </summary>
public const string Prick = "Prick";
/// <summary> 斩击(力量&敏捷) </summary>
public const string Slash = "Slash";
/// <summary> 奥术(智力) </summary>
public const string Arcane = "Arcane";
/// <summary> 邪术(魅力) </summary>
public const string Sorcery = "Sorcery";
/// <summary> 迅捷(敏捷) </summary>
public const string Swiftness = "Swiftness";
/// <summary> 坚韧(体质) </summary>
public const string Fortitude = "Fortitude";
/// <summary> 预判(感知) </summary>
public const string Prediction = "Prediction";
/// <summary> 消耗(打出后移至消耗堆) </summary>
public const string Exhaust = "Exhaust";
/// <summary> 消耗性(使用若干次后消耗) </summary>
public const string Exhaustible = "Exhaustible";
/// <summary> 瞬发(抽到后立刻打出) </summary>
public const string Instant = "Instant";
/// <summary> 不能主动从手牌中打出 </summary>
public const string Unplayable = "Unplayable";
/// <summary> 固有(战斗开始时必定抽到) </summary>
public const string Innate = "Innate";
/// <summary> 迟缓(战斗开始时放入弃牌堆) </summary>
public const string Tardy = "Tardy";
/// <summary> 保留(行动结束时不丢弃) </summary>
public const string Retain = "Retain";
/// <summary> 虚无(行动结束时如在手牌中则消耗) </summary>
public const string Ethereal = "Ethereal";
/// <summary> 复用(打出后回到手牌) </summary>
public const string Reuse = "Reuse";
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bea2951ce4b62464d974c95b94ed9085

View File

@@ -5,42 +5,37 @@ using Continentis.MainGame.Character;
using Continentis.MainGame.Equipment;
using SLSFramework.General;
using SLSFramework.UModAssistance;
using UniRx;
using UnityEngine;
namespace Continentis.MainGame.Card
{
public abstract partial class CardLogicBase
{
[Header("Reference")]
[Header("Reference")]
public CardInstance card;
public CardData cardData => card.cardData;
public CharacterBase user => card.user;
public HashSet<CardLogicComponentBase> logicComponents { get; private set; }
/// <summary>
/// 生成卡牌逻辑实例
/// </summary>
/// <summary>生成卡牌逻辑实例。</summary>
public static CardLogicBase GenerateCardLogic(CardData data)
{
string typeID = ModManager.GetTypeID(data.modName, "Cards", data.categoryName, data.className);
Type logicType = ModManager.GetType(typeID);
if(logicType == null)
if (logicType == null)
{
Debug.LogError($"Card class '{typeID}' not found in assemblies.");
return null;
}
if (Activator.CreateInstance(logicType) is CardLogicBase cardLogic)
{
return cardLogic;
}
Debug.LogError($"Card class '{typeID}' not found or could not be instantiated.");
return null;
}
public virtual void Initialize(CardInstance cardInstance)
{
card = cardInstance;
@@ -49,11 +44,8 @@ namespace Continentis.MainGame.Card
card.eventSubmodule.onUntargeting += UntargetingEffect;
}
public virtual void SetUpLogicComponents()
{
}
public virtual void SetUpLogicComponents() { }
public T AddLogicComponent<T>() where T : CardLogicComponentBase, new()
{
if (logicComponents.Any(component => component is T))
@@ -61,178 +53,171 @@ namespace Continentis.MainGame.Card
Debug.LogWarning($"Card {card.cardData.className} already has component of type {typeof(T)}, cannot add duplicate.");
return null;
}
else
{
T component = new T();
component.Initialize(this);
logicComponents.Add(component);
return component;
}
T component = new T();
component.Initialize(this);
logicComponents.Add(component);
return component;
}
public T LogicComponent<T>() where T : CardLogicComponentBase
{
return logicComponents.OfType<T>().FirstOrDefault();
}
public virtual void ApplyAttributeChangesByCard()
public virtual void ApplyAttributeChangesByCard() { }
/// <summary>卡牌出牌效果,返回一个 CommandGroup 供队列执行。</summary>
public virtual CommandGroup PlayEffect(List<CharacterBase> targetList)
{
}
public virtual List<CommandBase> PlayEffect(List<CharacterBase> targetList)
{
return null;
return new CommandGroup();
}
public virtual void AfterPlayEffect(List<CharacterBase> targetList)
{
}
public virtual void AfterPlayEffect(List<CharacterBase> targetList) { }
}
#region Attributes
#region Attributes
public partial class CardLogicBase
{
/// <summary>
/// 设置可变属性值
/// </summary>
/// <param name="attributeName">属性名,通常为</param>
/// <param name="additive">是否为叠加true为叠加false为覆盖true时originalValue为外部传入值</param>
/// <param name="originalValue">原始伤害值仅在additive为true时有效否则此值被覆盖为BaseAttribute</param>
/// <param name="baseOffset">伤害增量</param>
/// <summary>设置可变属性值。</summary>
public void SetVariableAttribute(string attributeName, int baseOffset, bool additive = false, int originalValue = 0)
{
card.SetVariableAttribute(attributeName, baseOffset, additive, originalValue);
}
/// <summary>
/// 检查卡牌是否具有某属性
/// </summary>
public bool HasAttribute(string attributeName)
{
return card.HasAttribute(attributeName);
}
/// <summary>
/// 获取卡牌的属性值
/// </summary>
public int GetAttribute(string attributeName, int defaultValue = 0)
{
return card.GetAttribute(attributeName, defaultValue);
}
public float GetRawAttribute(string attributeName, float defaultValue = 0)
{
return card.GetRawAttribute(attributeName, defaultValue);
}
/// <summary>
/// 设置卡牌的属性值
/// </summary>
public void SetAttribute(string attributeName, int value)
{
card.SetAttribute(attributeName, value);
}
/// <summary>
/// 设置卡牌的属性值
/// </summary>
public void SetAttribute(string attributeName, float value)
{
card.SetAttribute(attributeName, value);
}
/// <summary>
/// 修改卡牌的属性值
/// </summary>
public void ModifyAttribute(string attributeName, int delta)
{
card.ModifyAttribute(attributeName, delta);
card.SetVariableAttribute(attributeName, baseOffset, additive, originalValue);
}
/// <summary>检查卡牌是否具有某属性。</summary>
public bool HasAttribute(string attributeName) => card.HasAttribute(attributeName);
/// <summary>获取卡牌的属性值。</summary>
public int GetAttribute(string attributeName, int defaultValue = 0) => card.GetAttribute(attributeName, defaultValue);
public float GetRawAttribute(string attributeName, float defaultValue = 0) => card.GetRawAttribute(attributeName, defaultValue);
/// <summary>设置卡牌的属性值int。</summary>
public void SetAttribute(string attributeName, int value) => card.SetAttribute(attributeName, value);
/// <summary>设置卡牌的属性值float。</summary>
public void SetAttribute(string attributeName, float value) => card.SetAttribute(attributeName, value);
/// <summary>修改卡牌的属性值。</summary>
public void ModifyAttribute(string attributeName, int delta) => card.ModifyAttribute(attributeName, delta);
}
#endregion
#region Command
#region Command
public partial class CardLogicBase
{
// ── 新 API闭包工厂推荐Mod 制作者优先使用) ─────────────────────
/// <summary>
/// 创建一个命令组,组内命令按顺序执行
/// 对 targetList 中的每个目标调用工厂 lambda生成的命令按 mainExecutionMode 组合。
/// </summary>
protected CommandGroup ForEachTarget(
List<CharacterBase> targetList,
Func<CharacterBase, CommandBase> factory,
ExecutionMode mainExecutionMode = ExecutionMode.Sequential)
{
var group = new CommandGroup(mainExecutionMode);
foreach (CharacterBase target in targetList)
{
CharacterBase captured = target;
group.AddCommand(factory(captured));
}
return group;
}
/// <summary>
/// 对 targetList 中的每个目标调用工厂 lambda生成的 CommandGroup 按 mainExecutionMode 组合。
/// </summary>
protected CommandGroup ForEachTarget(
List<CharacterBase> targetList,
Func<CharacterBase, CommandGroup> factory,
ExecutionMode mainExecutionMode = ExecutionMode.Sequential)
{
var group = new CommandGroup(mainExecutionMode);
foreach (CharacterBase target in targetList)
{
CharacterBase captured = target;
group.AddCommand(factory(captured));
}
return group;
}
// ── 旧 API模板 Clone 模式(保留供向后兼容,后续迁移完成后移除) ──
/// <summary>
/// 克隆命令模板列表,组合为单个并行 CommandGroup。
/// </summary>
/// <param name="commands">命令模板</param>
protected CommandGroup SingleCommandGroup(params CommandBase[] commands)
{
return SingleCommandGroup(ExecutionMode.Parallel, commands);
}
/// <summary>
/// 创建一个命令组,组内命令按指定顺序执行
/// 克隆命令模板列表,按 executionMode 组合为单个 CommandGroup。
/// </summary>
/// <param name="executionMode">执行模式,顺序或并行</param>
/// <param name="commands">命令模板</param>
protected virtual CommandGroup SingleCommandGroup(
ExecutionMode executionMode = ExecutionMode.Parallel, params CommandBase[] commands)
{
CommandGroup singleGroup = new CommandGroup(executionMode);
var group = new CommandGroup(executionMode);
foreach (CommandBase template in commands)
{
singleGroup.AddCommand(template.Clone());
}
return singleGroup;
group.AddCommand(template.Clone());
return group;
}
/// <summary>
/// 对目标列表中的每个目标依次执行一组有参函数命令每组命令的参数为targetList中的个体主体Group顺序执行单体Group并行执行
/// 对目标列表中的每个目标克隆命令模板并注入 Target生成嵌套 CommandGroup。
/// 新代码请改用 <see cref="ForEachTarget"/> 闭包工厂模式。
/// </summary>
/// <param name="targetList">目标列表</param>
/// <param name="singleCommands">单体命令模板</param>
protected CommandGroup TargetListCommandGroup(List<CharacterBase> targetList,
[Obsolete("请改用 ForEachTarget(targetList, target => ...) 闭包工厂模式。")]
protected CommandGroup TargetListCommandGroup(
List<CharacterBase> targetList,
params CommandBase[] singleCommands)
{
return TargetListCommandGroup(targetList, ExecutionMode.Sequential, ExecutionMode.Parallel,
singleCommands);
return TemplateTargetGroup(targetList, ExecutionMode.Sequential, ExecutionMode.Parallel, singleCommands);
}
/// <summary>
/// 对目标列表中的每个目标依次执行一组有参函数命令每组命令的参数为targetList中的个体
/// 对目标列表中的每个目标克隆命令模板并注入 Target生成嵌套 CommandGroup
/// 新代码请改用 <see cref="ForEachTarget"/> 闭包工厂模式。
/// </summary>
/// <param name="targetList">目标列表</param>
/// <param name="mainExecutionMode">主体Group各个目标的执行顺序</param>
/// <param name="singleExecutionMode">单体Group一个目标中指令的执行顺序</param>
/// <param name="singleCommands">单体命令模板</param>
[Obsolete("请改用 ForEachTarget(targetList, target => ...) 闭包工厂模式。")]
protected virtual CommandGroup TargetListCommandGroup(
List<CharacterBase> targetList, ExecutionMode mainExecutionMode = ExecutionMode.Sequential,
List<CharacterBase> targetList,
ExecutionMode mainExecutionMode = ExecutionMode.Sequential,
ExecutionMode singleExecutionMode = ExecutionMode.Parallel,
params CommandBase[] singleCommands)
{
CommandGroup mainGroup = new CommandGroup(mainExecutionMode);
return TemplateTargetGroup(targetList, mainExecutionMode, singleExecutionMode, singleCommands);
}
/// <summary>
/// 模板 Clone 模式的底层实现,供旧代码向后兼容。
/// 克隆每条模板命令并向所有子命令的 selfContext 注入 Target。
/// </summary>
protected virtual CommandGroup TemplateTargetGroup(
List<CharacterBase> targetList,
ExecutionMode mainExecutionMode = ExecutionMode.Sequential,
ExecutionMode singleExecutionMode = ExecutionMode.Parallel,
params CommandBase[] singleCommands)
{
var mainGroup = new CommandGroup(mainExecutionMode);
foreach (CharacterBase target in targetList)
{
CommandGroup singleGroup = new CommandGroup(singleExecutionMode);
var singleGroup = new CommandGroup(singleExecutionMode);
foreach (CommandBase template in singleCommands)
{
CommandBase clone = template.Clone();
List<CommandBase> allCommands = new List<CommandBase>();
if (clone is CommandGroup group)
{
allCommands.AddRange(group.GetAllCommands(true));
}
else
{
allCommands.Add(clone);
}
// 收集所有子命令(含嵌套组内的命令)并注入 Target
var allCommands = clone is CommandGroup group
? group.GetAllCommands(true)
: new List<CommandBase> { clone };
foreach (CommandBase cmd in allCommands)
{
cmd.selfContext.context["Target"] = target;
}
cmd.selfContext.Set(CommandContextKeys.Target, target);
singleGroup.AddCommand(clone);
}
@@ -244,76 +229,55 @@ namespace Continentis.MainGame.Card
}
}
#endregion
#region Attack
public partial class CardLogicBase
{
/// <summary>
/// 获取最终伤害
/// </summary>
/// <param name="target">目标</param>
/// <param name="elementalTags">元素标签若为null则使用卡牌的元素标签</param>
/// <summary>获取对指定目标的最终伤害值。</summary>
public virtual int GetTargetedFinalDamage(CharacterBase target, List<string> elementalTags = null)
{
return GetFinalDamage(target, elementalTags, out _, out _, out _, out _);
}
/// <summary>
/// 获取最终伤害
/// </summary>
/// <param name="elementalTags">元素标签若为null则使用卡牌的元素标签</param>
/// <summary>获取无目标时的最终伤害值。</summary>
public virtual int GetNoTargetFinalDamage(List<string> elementalTags = null)
{
return GetFinalDamage(null, elementalTags, out _, out _, out _, out _);
}
protected virtual int GetFinalDamage(CharacterBase target, List<string> elementalTags,
out float baseDamageAfterOffset, out float elementalMultiplier,
protected virtual int GetFinalDamage(
CharacterBase target, List<string> elementalTags,
out float baseDamageAfterOffset, out float elementalMultiplier,
out float magicMultiplier, out float finalMultiplier)
{
bool haveTarget = target != null;
elementalTags ??= card.GetElementalKeywords();
//----计算基础伤害增量----
int physicsOffset = 0;
if (card.HasKeyword("Physics") || card.HasKeyword("Slash") || card.HasKeyword("Prick") || card.HasKeyword("Strike"))
{
physicsOffset = user.GetAttribute("PhysicsDamageDealtOffset"); //物理伤害基础增量
}
physicsOffset = user.GetAttribute("PhysicsDamageDealtOffset");
int magicOffset = 0;
if (card.HasKeyword("Magic") || card.HasKeyword("Arcane") || card.HasKeyword("Sorcery"))
{
magicOffset = user.GetAttribute("MagicDamageDealtOffset"); //魔法伤害基础增量
}
magicOffset = user.GetAttribute("MagicDamageDealtOffset");
//----计算伤害因数----
//计算元素伤害加成注意“物理Physics”也是一种元素因此下方没有“通用物理伤害加成”的计算
elementalMultiplier = 1;
elementalMultiplier = 1f;
foreach (string element in elementalTags)
{
float targetGain = haveTarget ? target.GetRawAttribute(element + "DamageGainMultiplier", 1) : 1;
elementalMultiplier *= user.GetRawAttribute(element + "DamageDealtMultiplier", 1) * targetGain;
float targetGain = haveTarget ? target.GetRawAttribute(element + "DamageGainMultiplier", 1f) : 1f;
elementalMultiplier *= user.GetRawAttribute(element + "DamageDealtMultiplier", 1f) * targetGain;
}
//计算通用的魔法伤害加成
magicMultiplier = 1;
magicMultiplier = 1f;
if (card.HasKeyword("Magic") || card.HasKeyword("Arcane") || card.HasKeyword("Sorcery"))
{
float targetGain = haveTarget ? target.GetRawAttribute("MagicDamageGainMultiplier", 1) : 1;
magicMultiplier = user.GetRawAttribute("MagicDamageDealtMultiplier", 1) * targetGain;
float targetGain = haveTarget ? target.GetRawAttribute("MagicDamageGainMultiplier", 1f) : 1f;
magicMultiplier = user.GetRawAttribute("MagicDamageDealtMultiplier", 1f) * targetGain;
}
//计算最终伤害加成
float targetFinalGain = haveTarget ? target.GetRawAttribute("FinalDamageGainMultiplier", 1) : 1;
finalMultiplier = user.GetRawAttribute("FinalDamageDealtMultiplier", 1) * targetFinalGain;
//----计算最终伤害----
float targetFinalGain = haveTarget ? target.GetRawAttribute("FinalDamageGainMultiplier", 1f) : 1f;
finalMultiplier = user.GetRawAttribute("FinalDamageDealtMultiplier", 1f) * targetFinalGain;
baseDamageAfterOffset = card.attributeSubmodule.GetCurrentAttribute("Damage") + physicsOffset + magicOffset;
float finalDamage = baseDamageAfterOffset * elementalMultiplier * magicMultiplier * finalMultiplier;
@@ -325,123 +289,85 @@ namespace Continentis.MainGame.Card
#region Buffs
public partial class CardLogicBase
{
/// <summary>
/// 创建一个角色战斗Buff实例
/// 注意此函数依赖ModManager的类型注册功能请确保在Mod加载时已注册对应Buff类型
/// 此函数中的T并不是原型参数而是获取Mod中注册的类型用的
/// </summary>
public T CreateCharacterBuff<T>(params object[] parameters) where T :CharacterCombatBuffBase
/// <summary>创建一个角色战斗 Buff 实例(通过 ModManager 类型注册)。</summary>
public T CreateCharacterBuff<T>(params object[] parameters) where T : CharacterCombatBuffBase
{
string buffTypeID = ModManager.GetTypeID(typeof(T));
if (string.IsNullOrEmpty(buffTypeID))
{
Debug.LogError($"Failed to get buff name for type {typeof(T).FullName}");
return null;
}
return ModManager.CreateInstance<T>(buffTypeID, parameters);
}
public T CreateCharacterBuff<T>(string buffTypeID, params object[] parameters) where T :CharacterCombatBuffBase
public T CreateCharacterBuff<T>(string buffTypeID, params object[] parameters) where T : CharacterCombatBuffBase
{
if (string.IsNullOrEmpty(buffTypeID))
{
Debug.LogError($"Failed to get buff name for type {typeof(T).FullName}");
return null;
}
return ModManager.CreateInstance<T>(buffTypeID, parameters);
}
/// <summary>
/// 创建一个卡牌战斗Buff实例
/// 注意此函数依赖ModManager的类型注册功能请确保在Mod加载时已注册对应Buff类型
/// 此函数中的T并不是原型参数而是获取Mod中注册的类型用的
/// </summary>
public T CreateCardBuff<T>(params object[] parameters) where T :CardBuffBase
/// <summary>创建一个卡牌战斗 Buff 实例(通过 ModManager 类型注册)。</summary>
public T CreateCardBuff<T>(params object[] parameters) where T : CardBuffBase
{
string buffTypeID = ModManager.GetTypeID(typeof(T));
if (string.IsNullOrEmpty(buffTypeID))
{
Debug.LogError($"Failed to get buff name for type {typeof(T).FullName}");
return null;
}
return ModManager.CreateInstance<T>(buffTypeID, parameters);
}
public T CreateCardBuff<T>(string buffTypeID, params object[] parameters) where T :CardBuffBase
public T CreateCardBuff<T>(string buffTypeID, params object[] parameters) where T : CardBuffBase
{
if (string.IsNullOrEmpty(buffTypeID))
{
Debug.LogError($"Failed to get buff name for type {typeof(T).FullName}");
return null;
}
return ModManager.CreateInstance<T>(buffTypeID, parameters);
}
}
#endregion
public abstract partial class CardLogicBase
{
/// <summary>
/// 获取衍生卡牌数据
/// </summary>
/// <summary>获取衍生卡牌数据(按索引)。</summary>
public CardData GetDerivativeCardData(int index)
{
return ModManager.GetData<CardData>(cardData.derivativeCardDataRefs[index]);
}
/// <summary>
/// 获取衍生卡牌数据
/// </summary>
/// <summary>获取衍生卡牌数据(按名称)。</summary>
public CardData GetDerivativeCardData(string dataName)
{
if (cardData.derivativeCardDataRefs.Contains(dataName))
{
return ModManager.GetData<CardData>(dataName);
}
Debug.LogError($"Card {cardData.className} does not contain derivative card data '{dataName}'.");
return null;
}
/// <summary>
/// 选中目标时触发的效果效果在所有逻辑组件的Targeting之前执行在SetUp函数生成EventSubmodule的时候
/// 如果必须需要在逻辑组件之后执行请重写Initialize函数。
/// </summary>
public virtual void TargetingEffect(CharacterBase target)
{
}
/// <summary>
/// 取消选中目标时触发的效果效果在所有逻辑组件的Untargeting之前执行在SetUp函数生成EventSubmodule的时候
/// 如果必须需要在逻辑组件之后执行请重写Initialize函数。
/// </summary>
public virtual void UntargetingEffect()
{
}
/// <summary>选中目标时触发的效果(在逻辑组件的 Targeting 之前执行)。</summary>
public virtual void TargetingEffect(CharacterBase target) { }
/// <summary>取消选中目标时触发的效果(在逻辑组件的 Untargeting 之前执行)。</summary>
public virtual void UntargetingEffect() { }
}
/// <summary>
/// 卡牌逻辑组件基类接口
/// 注意,所有的子接口需要实现的函数:
/// ComponentTargetingEffect此牌瞄准目标时调用
/// ComponentUntargetingEffect此牌取消瞄准目标时调用
/// </summary>
/// <summary>卡牌逻辑组件基类。</summary>
public abstract class CardLogicComponentBase
{
protected CardLogicBase mainLogic;
protected CardInstance card => mainLogic.card;
protected CharacterBase user => card.user;
protected CombatTeam usingTeam => card.usingTeam;
public virtual void Initialize(CardLogicBase card)
{
this.mainLogic = card;
@@ -449,14 +375,8 @@ namespace Continentis.MainGame.Card
this.card.eventSubmodule.onUntargeting += UntargetingEffect;
}
protected virtual void TargetingEffect(CharacterBase target)
{
}
protected virtual void TargetingEffect(CharacterBase target) { }
protected virtual void UntargetingEffect()
{
}
protected virtual void UntargetingEffect() { }
}
}
}

View File

@@ -209,8 +209,8 @@ namespace Continentis.MainGame.Card
});
}));
CommandQueueManager.Instance.AddCommands(PlayEffect(targetList));
CommandQueueManager.Instance.AddCommands(cardLogic.PlayEffect(targetList));
CommandQueueManager.Instance.AddCommand(PlayEffect(targetList));
CommandQueueManager.Instance.AddCommand(cardLogic.PlayEffect(targetList));
CommandQueueManager.Instance.AddCommand(new Cmd_Function(() =>
{
eventSubmodule.onAfterPlay.Invoke(targetList);
@@ -241,9 +241,9 @@ namespace Continentis.MainGame.Card
}
}
protected virtual List<CommandBase> PlayEffect(List<CharacterBase> targetList)
protected virtual CommandGroup PlayEffect(List<CharacterBase> targetList)
{
return new List<CommandBase>();
return new CommandGroup();
}
protected virtual void AfterPlayEffect(List<CharacterBase> targetList)

View File

@@ -14,7 +14,7 @@ namespace Continentis.MainGame.Card
{
this.forceUse = false;
this.forceIgnore = false;
this.baseWeight = owner.cardData.baseWeight;
this.baseWeight = owner.cardData.intentionBaseWeight;
this.currentWeight = baseWeight;
}

View File

@@ -5,9 +5,9 @@ namespace Continentis.MainGame.Card
{
public abstract partial class CardViewBase
{
Tweener hintShadowTweener;
Tweener selectShadowTweener;
private Tweener hintShadowTweener;
private Tweener selectShadowTweener;
public void ClearShadows()
{
hintShadowTweener?.Kill(true);
@@ -21,7 +21,7 @@ namespace Continentis.MainGame.Card
DisableHintShadow(immediately);
DisableSelectShadow(immediately);
}
public void EnableHintShadow(Color shadowColor)
{
hintShadow.gameObject.SetActive(true);
@@ -38,7 +38,8 @@ namespace Continentis.MainGame.Card
}
else
{
hintShadowTweener = hintShadow.DOColor(Color.clear, 0.2f).OnComplete(() => { hintShadow.gameObject.SetActive(false); }).Play();
hintShadowTweener = hintShadow.DOColor(Color.clear, 0.2f)
.OnComplete(() => { hintShadow.gameObject.SetActive(false); }).Play();
}
}
@@ -58,8 +59,9 @@ namespace Continentis.MainGame.Card
}
else
{
selectShadowTweener = selectShadow.DOColor(Color.clear, 0.2f).OnComplete(() => { selectShadow.gameObject.SetActive(false); }).Play();
selectShadowTweener = selectShadow.DOColor(Color.clear, 0.2f)
.OnComplete(() => { selectShadow.gameObject.SetActive(false); }).Play();
}
}
}
}
}

View File

@@ -1,158 +1,299 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Continentis.MainGame.Base;
using Sirenix.OdinInspector;
using SLSFramework.General;
using UnityEditor;
using UnityEngine;
using SLSFramework.UModAssistance;
using Continentis.MainGame.Character;
using Continentis.Mods;
namespace Continentis.MainGame.Card
{
[CustomEditor(typeof(CardData))]
public partial class CardDataEditor : DataEditor
// =========================================================================
// CardDataEditor
//
// 注意:此类已不再使用 [CustomEditor] 注解。
// Odin Inspector 会自动接管 CardDataScriptableObject的 Inspector 渲染,
// 无需任何自定义 Editor 类。本文件现主要用于承载 CardData 的编辑器扩展分部类。
// 如需将来添加 OdinEditorWindow 工具或其他编辑器工具类,可在此文件中继续扩展。
// =========================================================================
internal static class CardDataEditorPlaceholder
{
// 存储我们需要自定义绘制的属性的引用
private SerializedProperty modNameProp;
private SerializedProperty categoryNameProp;
private SerializedProperty classNameProp;
private SerializedProperty displayNameProp;
private SerializedProperty cardRarityProp;
private SerializedProperty cardTypeProp;
private SerializedProperty keywordsProp;
private SerializedProperty cardSpriteProp;
private SerializedProperty cardLayoutTagsProp;
private SerializedProperty functionTextProp;
private SerializedProperty cardDescriptionProp;
private SerializedProperty intentionIconKeysProp;
private SerializedProperty intentionValueNamesProp;
private SerializedProperty intentionTextOverrideProp;
private SerializedProperty baseWeightProp;
private SerializedProperty variableAttributesProp;
private SerializedProperty originalAttributesProp;
private SerializedProperty runtimeCurrentAttributesProp;
private SerializedProperty upgradeNodeProp;
private SerializedProperty prefabsProp;
private SerializedProperty derivativeCardsProp;
private SerializedProperty derivativeCharactersProp;
protected override void OnEnable()
// 保留此类以维持文件存在意义,可在此添加未来的编辑器工具方法。
}
// =========================================================================
// partial class CardData — 编辑器专属扩展
//
// 包含:
// 1. Odin ValueDropdown 数据提供方法
// 2. OnValueChanged 回调(自动填充 modName / categoryName / 文本Key
// 3. DrawCardSpriteField() — 16:9 自定义预览绘制器
// 4. PasteDefaultAttributes() — 粘贴默认属性模板按钮
// 5. OpenCardPreviewer() — 打开 CardPreviewer 工具窗口按钮
// 6. 内部共享辅助方法AssetDatabase 查询)
// =========================================================================
public partial class CardData
{
// ── 1. 卡牌逻辑类选择器 ───────────────────────────────────────────────
// 替代旧 CardDataEditor 中的 DrawSearchableTypeSelector()
// 配合 CardData.cs 中字段上的 [ValueDropdown("@CardData.GetCardLogicDropdownItems()")]
/// <summary>
/// 为 [ValueDropdown] 提供所有 CardLogicBase 子类的层级下拉项。
/// 路径格式为 "ModName/Category/ClassName",与旧 TypeSelectorWindow 分组逻辑一致。
/// 此方法为 static故在 [ValueDropdown] 中使用 @ 表达式语法引用。
/// </summary>
public static IEnumerable<ValueDropdownItem<string>> GetCardLogicDropdownItems()
{
base.OnEnable();
// 在启用时根据我们修改后的字段名找到对应的SerializedProperty
modNameProp = serializedObject.FindProperty("modName");
classNameProp = serializedObject.FindProperty("className");
categoryNameProp = serializedObject.FindProperty("categoryName");
displayNameProp = serializedObject.FindProperty("displayName");
cardRarityProp = serializedObject.FindProperty("cardRarity");
cardTypeProp = serializedObject.FindProperty("cardType");
keywordsProp = serializedObject.FindProperty("keywords");
cardSpriteProp = serializedObject.FindProperty("cardSprite");
cardLayoutTagsProp = serializedObject.FindProperty("cardLayoutTags");
functionTextProp = serializedObject.FindProperty("functionText");
cardDescriptionProp = serializedObject.FindProperty("cardDescription");
intentionIconKeysProp = serializedObject.FindProperty("intentionIconKeys");
intentionValueNamesProp = serializedObject.FindProperty("intentionValueNames");
intentionTextOverrideProp = serializedObject.FindProperty("intentionTextOverride");
baseWeightProp = serializedObject.FindProperty("baseWeight");
variableAttributesProp = serializedObject.FindProperty("variableAttributes");
originalAttributesProp = serializedObject.FindProperty("originalAttributes");
runtimeCurrentAttributesProp = serializedObject.FindProperty("runtimeCurrentAttributes");
upgradeNodeProp = serializedObject.FindProperty("upgradeNode");
prefabsProp = serializedObject.FindProperty("prefabRefs");
derivativeCardsProp = serializedObject.FindProperty("derivativeCardDataRefs");
derivativeCharactersProp = serializedObject.FindProperty("derivativeCharacterDataRefs");
const string namespacePrefix = "Continentis.Mods";
const string namespaceToRemove = "Cards";
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a =>
{
try
{
return a.GetTypes();
}
catch
{
return Type.EmptyTypes;
}
})
.Where(t => typeof(CardLogicBase).IsAssignableFrom(t)
&& !t.IsAbstract
&& !t.IsInterface
&& t != typeof(CardLogicBase));
foreach (var type in types.OrderBy(t => t.FullName))
{
string path;
if (type.Namespace != null && type.Namespace.StartsWith(namespacePrefix))
{
// 去掉公共前缀,拆分剩余命名空间段落
var segments = type.Namespace
.Substring(namespacePrefix.Length)
.Split('.')
.Where(s => !string.IsNullOrEmpty(s) && s != namespaceToRemove)
.ToList();
path = segments.Count > 0
? string.Join("/", segments) + "/" + type.Name
: type.Name;
}
else
{
path = "Uncategorized/" + type.Name;
}
yield return new ValueDropdownItem<string>(path, type.Name);
}
}
public override void OnInspectorGUI()
/// <summary>
/// 当 className 通过下拉菜单改变后,自动填充 modName / categoryName / displayName / functionText。
/// 等价于旧 CardDataEditor 中 DrawSearchableTypeSelector 的 onTypeSelected 回调。
/// 被 CardData.cs 中字段上的 [OnValueChanged("OnCardLogicClassSelected")] 触发。
/// </summary>
private void OnCardLogicClassSelected()
{
serializedObject.Update();
// --- 绘制自定义的Type选择器 ---
// 我们把它从所有自动绘制的属性中分离出来,放在最前面或最后面,让布局更清晰
EditorGUILayout.Space(); // 增加一点间距
EditorGUILayout.LabelField("Fundamental", EditorStyles.boldLabel);
DrawSearchableTypeSelector(
classNameProp, // 1. 存储类名的字符串属性
"Card Logic Class", // 2. 显示的标签
typeof(CardLogicBase), // 3. 要搜索的基类
(outType) => // 4. 【关键】当用户选择后执行的回调
{
string className = outType.Name;
string modName = outType.Namespace!.Replace("Continentis.Mods.", "").Split('.')[0];
string categoryName = outType.Namespace!.Replace("Continentis.Mods." + modName + ".Cards", "");
if (!string.IsNullOrEmpty(categoryName))
{
categoryName = outType.Namespace!.Replace("Continentis.Mods." + modName + ".Cards", "").Substring(1); // 去掉开头的点
}
string displayName = "Card_" + modName + "_" + className + "_DisplayName";
string functionTextName = "Card_" + modName + "_" + className + "_FunctionText";
classNameProp.stringValue = className;
modNameProp.stringValue = modName;
categoryNameProp.stringValue = categoryName;
displayNameProp.stringValue = displayName;
functionTextProp.stringValue = functionTextName;
Debug.Log(outType.FullName);
Debug.Log($"modName: {modName}, categoryName: {categoryName}, className: {className}, displayName: {displayName}, functionText: {functionTextName}");
serializedObject.ApplyModifiedProperties();
},
"Continentis.Mods", // 5. 你的 namespacePrefix
"Cards" // 6. 你的 namespaceToRemove (注意:根据你的代码,这里不应包含".")
);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.PropertyField(modNameProp);
EditorGUILayout.PropertyField(categoryNameProp);
EditorGUILayout.PropertyField(classNameProp);
EditorGUILayout.PropertyField(displayNameProp);
EditorGUI.EndDisabledGroup();
EditorGUILayout.PropertyField(cardRarityProp);
EditorGUILayout.PropertyField(cardTypeProp);
DrawListWithEditRefSelector(keywordsProp, "CardKeywords");
EditorGUILayout.PropertyField(cardSpriteProp);
EditorGUILayout.PropertyField(cardLayoutTagsProp, true);
EditorGUILayout.PropertyField(functionTextProp);
EditorGUILayout.PropertyField(cardDescriptionProp);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Attributes", EditorStyles.boldLabel);
DrawListWithEditRefSelector(intentionIconKeysProp, "IntentionIconKeys");
DrawListWithLocalSelector(intentionValueNamesProp, "variableAttributes");
EditorGUILayout.PropertyField(intentionTextOverrideProp);
EditorGUILayout.PropertyField(baseWeightProp);
EditorGUILayout.PropertyField(variableAttributesProp, true);
EditorGUILayout.PropertyField(originalAttributesProp, true);
EditorGUILayout.PropertyField(runtimeCurrentAttributesProp, true);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Upgrade", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(upgradeNodeProp);
// --- 绘制自定义的引用列表 ---
EditorGUILayout.Space();
EditorGUILayout.LabelField("References", EditorStyles.boldLabel);
DrawCharacterListGUI<GameObject>(prefabsProp);
DrawCharacterListGUI<CardData>(derivativeCardsProp);
DrawCharacterListGUI<CharacterData>(derivativeCharactersProp);
if (string.IsNullOrEmpty(className)) return;
HandleObjectPicker();
serializedObject.ApplyModifiedProperties();
// 根据 className 反查对应 Type仅首个匹配
var selectedType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a =>
{
try
{
return a.GetTypes();
}
catch
{
return Type.EmptyTypes;
}
})
.FirstOrDefault(t =>
t.Name == className
&& typeof(CardLogicBase).IsAssignableFrom(t)
&& !t.IsAbstract);
if (selectedType?.Namespace == null) return;
const string prefix = "Continentis.Mods.";
var ns = selectedType.Namespace;
if (!ns.StartsWith(prefix)) return;
// 解析 Mod 名与分类名
// 例ns = "Continentis.Mods.Basic.Cards.Assassin"
// → afterPrefix = "Basic.Cards.Assassin"
// → parts = ["Basic", "Cards", "Assassin"]
var afterPrefix = ns.Substring(prefix.Length);
var parts = afterPrefix.Split('.');
var resolvedMod = parts.Length > 0 ? parts[0] : string.Empty;
// 去掉 "{ModName}.Cards" 后的剩余部分即为 categoryName
var cardsSegment = resolvedMod + ".Cards";
var resolvedCategory = ns.Contains(cardsSegment)
? ns.Replace(prefix + cardsSegment, "").TrimStart('.')
: string.Empty;
modName = resolvedMod;
categoryName = resolvedCategory;
displayName = $"Card_{resolvedMod}_{className}_DisplayName";
functionText = $"Card_{resolvedMod}_{className}_FunctionText";
EditorUtility.SetDirty(this);
Debug.Log($"[CardData] 已自动填充 → modName: {modName}, categoryName: {categoryName}, " +
$"className: {className}, displayName: {displayName}, functionText: {functionText}");
}
// ── 2. 关键词下拉 ─────────────────────────────────────────────────────
// 从所有 EditorBaseCollection 的 cardKeywords 字典聚合 Key 列表
private IEnumerable<ValueDropdownItem<string>> GetAvailableKeywords()
{
return EditorBaseCollection.GetCardKeywordsDropdown();
}
// ── 3. 意图图标下拉 ───────────────────────────────────────────────────
// 从所有 EditorBaseCollection 的 intentionIcons 字典聚合 Key 列表
private IEnumerable<ValueDropdownItem<string>> GetAvailableIntentionIcons()
{
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var coll in EditorBaseCollection.GetAllCollections())
{
if (coll.intentionIcons == null) continue;
foreach (var key in coll.intentionIcons.Keys)
if (seen.Add(key))
yield return new ValueDropdownItem<string>(key, key);
}
}
// ── 4. intentionValueNames 候选(来自 variableAttributes 的 Key────────
// 替代旧 DrawListWithLocalSelector(intentionValueNamesProp, "variableAttributes")
private IEnumerable<ValueDropdownItem<string>> GetVariableAttributeKeys()
{
if (variableAttributes == null) yield break;
foreach (var key in variableAttributes.Keys)
yield return new ValueDropdownItem<string>(key, key);
}
// ── 5. References 列表的资产名称选择器 ──────────────────────────────────
// 替代旧 DrawCharacterListGUI<T>(prop)
private IEnumerable<ValueDropdownItem<string>> GetAvailablePrefabs()
{
return GetAssetNameDropdown("t:Prefab");
}
private IEnumerable<ValueDropdownItem<string>> GetAvailableCardData()
{
return GetAssetNameDropdown("t:CardData");
}
private IEnumerable<ValueDropdownItem<string>> GetAvailableCharacterData()
{
return GetAssetNameDropdown("t:CharacterData");
}
[BoxGroup("Display")]
[PropertyOrder(11)]
[Button("✨ 预览卡牌效果", ButtonSizes.Medium)]
[GUIColor(0.5f, 0.8f, 1f)]
private void OpenCardPreviewer()
{
CardPreviewer.OpenWith(this);
}
[BoxGroup("Data/属性/工具")]
[PropertyOrder(35)]
[Button("粘贴默认属性", ButtonSizes.Medium)]
[GUIColor(0.7f, 1f, 0.7f)]
private void PasteDefaultAttributes()
{
var targetCollections = new List<CardAttributesDefaultCollection>();
var guids = AssetDatabase.FindAssets("t:CardAttributesDefaultCollection");
if (guids.Length == 0)
{
Debug.LogWarning("[CardData] 未找到任何 CardAttributesDefaultCollection 资产。");
return;
}
if (guids.Length > 1) Debug.Log("[CardData] 找到多个 CardAttributesDefaultCollection将合并后导入。");
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var directory = Path.GetDirectoryName(path)?.Replace('\\', '/');
var collection =
AssetDatabase.LoadAssetAtPath<CardAttributesDefaultCollection>(path);
if (collection == null) continue;
// 只收集位于 Assets/Mods/{ModName}/Cards/DefaultCollections/ 路径下的资产
if (!string.IsNullOrEmpty(directory)
&& directory.StartsWith("Assets/Mods/")
&& directory.EndsWith("/Cards/DefaultCollections"))
{
targetCollections.Add(collection);
Debug.Log($"[CardData] 已加载默认集合:{path}");
}
}
if (targetCollections.Count == 0)
{
Debug.LogWarning("[CardData] 未在 Assets/Mods/{ModName}/Cards/DefaultCollections/ " +
"路径下找到有效的 CardAttributesDefaultCollection 资产。");
return;
}
// 合并各 Collection 中的数据
var merged_variable = new Dictionary<string, float>();
var merged_original = new Dictionary<string, float>();
// Collection 端字段名仍为 endowingCurrentAttributesCardData 端已用 [FormerlySerializedAs] 改名)
var merged_runtime = new Dictionary<string, string>();
foreach (var collection in targetCollections)
{
collection.variableAttributes?.PasteDictionary(merged_variable);
collection.originalAttributes?.PasteDictionary(merged_original);
collection.endowingCurrentAttributes?.PasteDictionary(merged_runtime);
}
// 写入当前 CardData
merged_variable.PasteDictionary(variableAttributes);
merged_original.PasteDictionary(originalAttributes);
merged_runtime.PasteDictionary(runtimeCurrentAttributes);
EditorUtility.SetDirty(this);
Debug.Log($"[CardData] 已将默认属性粘贴至 '{name}'。");
}
// ── 内部共享辅助方法 ─────────────────────────────────────────────────
/// <summary>
/// 通过 AssetDatabase 搜索特定类型/标签的资产,将文件名(无扩展名)作为下拉项返回。
/// 用于 References 列表的字符串引用选择。
/// </summary>
private static IEnumerable<ValueDropdownItem<string>> GetAssetNameDropdown(string searchFilter)
{
var guids = AssetDatabase.FindAssets(searchFilter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var name = Path.GetFileNameWithoutExtension(path);
yield return new ValueDropdownItem<string>(name, name);
}
}
}
}

View File

@@ -0,0 +1,626 @@
#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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3941b9ef1fdf4f545bbe372206c6dce3

View File

@@ -27,6 +27,9 @@ namespace Continentis.MainGame.Card
card.SetVariableAttribute("Damage", damageOffset, additive, originalDamage);
}
/// <summary>
/// 默认伤害计算,仅使用卡牌基础伤害(无任何属性加成)
/// </summary>
public void SetDamage_Default()
{
SetDamage(0);