架构大更
This commit is contained in:
@@ -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
|
||||
}
|
||||
18
Assets/Scripts/MainGame/Card/CardAttributes.cs
Normal file
18
Assets/Scripts/MainGame/Card/CardAttributes.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/Card/CardAttributes.cs.meta
Normal file
2
Assets/Scripts/MainGame/Card/CardAttributes.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3065cb6a46856c4c8eabd7e2e3fae9f
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,设置Attr,BaseAttrOffset=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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
78
Assets/Scripts/MainGame/Card/CardKeywords.cs
Normal file
78
Assets/Scripts/MainGame/Card/CardKeywords.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/Card/CardKeywords.cs.meta
Normal file
2
Assets/Scripts/MainGame/Card/CardKeywords.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bea2951ce4b62464d974c95b94ed9085
|
||||
@@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 会自动接管 CardData(ScriptableObject)的 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 端字段名仍为 endowingCurrentAttributes(CardData 端已用 [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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
626
Assets/Scripts/MainGame/Card/Editor/CardPreviewer.cs
Normal file
626
Assets/Scripts/MainGame/Card/Editor/CardPreviewer.cs
Normal 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
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3941b9ef1fdf4f545bbe372206c6dce3
|
||||
@@ -27,6 +27,9 @@ namespace Continentis.MainGame.Card
|
||||
card.SetVariableAttribute("Damage", damageOffset, additive, originalDamage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认伤害计算,仅使用卡牌基础伤害(无任何属性加成)
|
||||
/// </summary>
|
||||
public void SetDamage_Default()
|
||||
{
|
||||
SetDamage(0);
|
||||
|
||||
Reference in New Issue
Block a user