This commit is contained in:
SoulliesOfficial
2026-04-01 12:23:27 -04:00
parent aff7ac0e03
commit c3b1561375
933 changed files with 114333 additions and 119360 deletions

View File

@@ -17,6 +17,32 @@ namespace Continentis.MainGame.Card
{
return contentSubmodule.keywords.Contains(keyword);
}
public bool HasAnyKeyword(params string[] keywords)
{
bool hasAny = false;
foreach (var keyword in keywords)
{
hasAny = HasKeyword(keyword);
if (hasAny) break;
}
return hasAny;
}
public bool HasAllKeywords(params string[] keywords)
{
bool hasAll = true;
foreach (var keyword in keywords)
{
hasAll = HasKeyword(keyword);
if (!hasAll) break;
}
return hasAll;
}
}
#endregion

View File

@@ -7,12 +7,12 @@
public static class CardAttributes
{
/// <summary> 体力值消耗 </summary>
public const string StaminaCost = "StaminaCost";
public const string StaminaCost = "Stamina_Cost";
/// <summary> 魔力值消耗 </summary>
public const string ManaCost = "ManaCost";
public const string ManaCost = "Mana_Cost";
/// <summary> 目标数量0为自身-1为全体 </summary>
public const string TargetCount = "TargetCount";
public const string TargetCount = "Target_Count";
}
}

View File

@@ -69,9 +69,10 @@ namespace Continentis.MainGame.Card
[LabelText("功能文本 Key")]
public string functionText;
[FormerlySerializedAs("cardDescription")]
[BoxGroup("Display"), PropertyOrder(9)]
[LabelText("卡牌描述 Key")]
public string cardDescription;
public string descriptionText;
[BoxGroup("Display"), PropertyOrder(10)]
[ListDrawerSettings(ShowIndexLabels = false), LabelText("布局标签")]

View File

@@ -26,6 +26,12 @@ namespace Continentis.MainGame.Card
public CharacterBase user;
public CombatTeam usingTeam;
/// <summary>
/// 当前用于文本条件标签解析的目标角色。
/// 玩家拖拽卡牌时由 Targeting/Untargeting 设置NPC 意图创建时由意图系统设置。
/// </summary>
public CharacterBase currentTextTarget;
public CardLogicBase cardLogic;
public int upgradeLevel;
@@ -117,18 +123,8 @@ namespace Continentis.MainGame.Card
.UpdateTeamPileText(CombatMainManager.Instance.characterController.playerTeam);
}
//下面的部分后续放入CardLogic的初始化函数中
card.RefreshCardAttributes();
if (card.HasKeyword("Instant")) //如果是“瞬发”牌,添加抽牌后立刻打出的事件
{
card.eventSubmodule.onDraw.InsertByPriority("Instant", new PrioritizedAction(() =>
{
card.DetectTargetsValidity(out List<CharacterBase> valid, out _, out _);
card.Play(card.SetRandomTargets(valid), card.user);
}, 99));
}
return card;
}
@@ -250,4 +246,4 @@ namespace Continentis.MainGame.Card
this.index = index;
}
}
}
}

View File

@@ -17,6 +17,9 @@
/// <summary> 可选目标为全体角色 </summary>
public const string TargetAll = "TargetAll";
/// <summary> 多目标选择时允许复选 </summary>
public const string AllowDuplicateTargets = "AllowDuplicateTargets";
/// <summary> 物理 </summary>
public const string Physics = "Physics";
@@ -24,6 +27,27 @@
/// <summary> 魔法 </summary>
public const string Magic = "Magic";
/// <summary> 风 </summary>
public const string Wind = "Wind";
/// <summary> 火 </summary>
public const string Fire = "Fire";
/// <summary> 冰 </summary>
public const string Ice = "Ice";
/// <summary> 土 </summary>
public const string Earth = "Earth";
/// <summary> 雷 </summary>
public const string Storm = "Storm";
/// <summary> 光 </summary>
public const string Light = "Light";
/// <summary> 暗 </summary>
public const string Darkness = "Darkness";
/// <summary> 打击(力量) </summary>
public const string Strike = "Strike";
@@ -74,5 +98,8 @@
/// <summary> 复用(打出后回到手牌) </summary>
public const string Reuse = "Reuse";
/// <summary> 先决(在手牌中时,必须先打出此牌) </summary>
public const string Prerequisite = "Prerequisite";
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Character;
using Continentis.MainGame.Equipment;
using SoftCircuits.Collections;
using SLSFramework.General;
using SLSFramework.UModAssistance;
using UnityEngine;
@@ -36,14 +37,129 @@ namespace Continentis.MainGame.Card
return null;
}
// 存储所有通过 SubscribeCombatEvent 注册的订阅的移除委托
private readonly List<Action> _managedUnsubscribers = new List<Action>();
public virtual void Initialize(CardInstance cardInstance)
{
card = cardInstance;
logicComponents = new HashSet<CardLogicComponentBase>();
card.eventSubmodule.onTargeting += TargetingEffect;
card.eventSubmodule.onUntargeting += UntargetingEffect;
// 自动将卡牌事件子模块的生命周期钩子接入虚方法
card.eventSubmodule.onDraw.InsertByPriority(
$"{GetType().Name}_OnDraw_{GetHashCode()}",
new PrioritizedAction(OnDraw));
card.eventSubmodule.onCombatStart.InsertByPriority(
$"{GetType().Name}_OnCombatStart_{GetHashCode()}",
new PrioritizedAction(OnCombatStart));
card.eventSubmodule.onCombatEnd.InsertByPriority(
$"{GetType().Name}_OnCombatEnd_{GetHashCode()}",
new PrioritizedAction(OnCombatEnd));
card.eventSubmodule.onRoundStart.InsertByPriority(
$"{GetType().Name}_OnRoundStart_{GetHashCode()}",
new PrioritizedAction(OnRoundStart));
card.eventSubmodule.onRoundEnd.InsertByPriority(
$"{GetType().Name}_OnRoundEnd_{GetHashCode()}",
new PrioritizedAction(OnRoundEnd));
card.eventSubmodule.onActionStart.InsertByPriority(
$"{GetType().Name}_OnActionStart_{GetHashCode()}",
new PrioritizedAction(OnActionStart));
card.eventSubmodule.onActionEnd.InsertByPriority(
$"{GetType().Name}_OnActionEnd_{GetHashCode()}",
new PrioritizedAction(OnActionEnd));
// 关键词驱动的行为统一在此处注册
if (card.HasKeyword("Instant"))
{
//含有Instant关键词抽到后直接打出
card.eventSubmodule.onDraw.InsertByPriority("Instant", new PrioritizedAction(() =>
{
card.DetectTargetsValidity(out List<CharacterBase> valid, out _, out _);
card.Play(card.SetRandomTargets(valid), card.user);
}, 99));
}
}
/// <summary>
/// 向战斗全局事件字典注册一个托管订阅。
/// Dispose() 时无需手动取消——基类会自动移除所有通过此方法注册的订阅。
/// </summary>
protected void SubscribeCombatEvent(
OrderedDictionary<string, PrioritizedAction> eventDict,
PrioritizedAction action,
int priority = 0)
{
string key = $"{GetType().Name}_{GetHashCode()}_{_managedUnsubscribers.Count}";
action.Priority = priority;
eventDict.InsertByPriority(key, action);
_managedUnsubscribers.Add(() => eventDict.Remove(key));
}
/// <summary>
/// 向战斗全局事件字典注册一个带参数的托管订阅。
/// Dispose() 时无需手动取消——基类会自动移除所有通过此方法注册的订阅。
/// </summary>
protected void SubscribeCombatEvent<T>(
OrderedDictionary<string, PrioritizedAction<T>> eventDict,
PrioritizedAction<T> action,
int priority = 0)
{
string key = $"{GetType().Name}_{GetHashCode()}_{_managedUnsubscribers.Count}";
action.Priority = priority;
eventDict.InsertByPriority(key, action);
_managedUnsubscribers.Add(() => eventDict.Remove(key));
}
/// <summary>
/// 卡牌销毁时调用(打出、弃牌、消耗)。
/// 自动清理所有通过 SubscribeCombatEvent 注册的托管订阅。
/// 子类重写时无需调用 base.Dispose(),除非有额外资源需要释放。
/// </summary>
public void Dispose()
{
foreach (Action unsubscribe in _managedUnsubscribers)
unsubscribe();
_managedUnsubscribers.Clear();
OnDispose();
}
// ── 生命周期虚方法 ─────────────────────────────────────────────────────
/// <summary>抽到此卡牌时调用。</summary>
protected virtual void OnDraw() { }
/// <summary>战斗开始时调用。</summary>
protected virtual void OnCombatStart() { }
/// <summary>战斗结束时调用。</summary>
protected virtual void OnCombatEnd() { }
/// <summary>每回合开始时调用。</summary>
protected virtual void OnRoundStart() { }
/// <summary>每回合结束时调用。</summary>
protected virtual void OnRoundEnd() { }
/// <summary>每次行动开始时调用。</summary>
protected virtual void OnActionStart() { }
/// <summary>每次行动结束时调用。</summary>
protected virtual void OnActionEnd() { }
/// <summary>
/// 卡牌销毁时的扩展清理钩子。
/// 子类有额外资源需要释放时重写此方法,无需处理 SubscribeCombatEvent 的取消订阅。
/// </summary>
protected virtual void OnDispose() { }
public virtual void SetUpLogicComponents() { }
public T AddLogicComponent<T>() where T : CardLogicComponentBase, new()
@@ -233,43 +349,66 @@ namespace Continentis.MainGame.Card
#region Attack
public partial class CardLogicBase
{
/// <summary>获取对指定目标的最终伤害值。</summary>
public virtual int GetTargetedFinalDamage(CharacterBase target, List<string> elementalTags = null)
/// <summary>
/// 以当前卡牌作为来源,对目标发动攻击。
/// 内部自动构建携带 sourceCard 的 AttackContext确保 Buff 层能正确识别来源卡牌信息。
/// 卡牌脚本中所有的攻击调用都应优先使用此方法,而非直接调用 user.Attack()。
/// </summary>
protected AttackResult AttackTarget(CharacterBase target, int damage, AttackContext ctx = null)
{
return GetFinalDamage(target, elementalTags, out _, out _, out _, out _);
ctx ??= new AttackContext(card);
if (ctx.sourceCard == null) ctx.sourceCard = card;
return user.Attack(target, damage, ctx);
}
/// <summary>
/// 获取对指定目标的最终伤害值。
/// ctx 中的 damageKeywords 驱动 offset 和元素乘区计算baseDamageAttributeName 指定基础伤害属性名。
/// ctx 为 null 时回退到卡牌元素关键词和默认 "Damage" 属性(向后兼容)。
/// </summary>
public virtual int GetTargetedFinalDamage(CharacterBase target, AttackContext ctx = null)
{
return GetFinalDamage(target, ctx, out _, out _, out _, out _);
}
/// <summary>获取无目标时的最终伤害值。</summary>
public virtual int GetNoTargetFinalDamage(List<string> elementalTags = null)
public virtual int GetNoTargetFinalDamage(AttackContext ctx = null)
{
return GetFinalDamage(null, elementalTags, out _, out _, out _, out _);
return GetFinalDamage(null, ctx, out _, out _, out _, out _);
}
protected virtual int GetFinalDamage(
CharacterBase target, List<string> elementalTags,
CharacterBase target, AttackContext ctx,
out float baseDamageAfterOffset, out float elementalMultiplier,
out float magicMultiplier, out float finalMultiplier)
{
bool haveTarget = target != null;
elementalTags ??= card.GetElementalKeywords();
// 从 AttackContext 中读取伤害关键词和属性名null 时回退到卡牌默认值
List<string> damageKeywords = ctx?.damageKeywords ?? card.GetElementalKeywords();
string baseDamageAttr = ctx?.baseDamageAttributeName;
// Physics / Magic offset 由 damageKeywords 驱动,与卡牌标记关键词无关
int physicsOffset = 0;
if (card.HasKeyword("Physics") || card.HasKeyword("Slash") || card.HasKeyword("Prick") || card.HasKeyword("Strike"))
physicsOffset = user.GetAttribute("PhysicsDamageDealtOffset");
if (damageKeywords.Contains("Physics"))
physicsOffset = user.GetAttribute(CharacterAttributes.PhysicsDamageDealtOffset);
int magicOffset = 0;
if (card.HasKeyword("Magic") || card.HasKeyword("Arcane") || card.HasKeyword("Sorcery"))
magicOffset = user.GetAttribute("MagicDamageDealtOffset");
if (damageKeywords.Contains("Magic"))
magicOffset = user.GetAttribute(CharacterAttributes.MagicDamageDealtOffset);
// 元素乘区:遍历 damageKeywords 中属于 elementTags 的部分
elementalMultiplier = 1f;
foreach (string element in elementalTags)
foreach (string keyword in damageKeywords)
{
float targetGain = haveTarget ? target.GetRawAttribute(element + "DamageGainMultiplier", 1f) : 1f;
elementalMultiplier *= user.GetRawAttribute(element + "DamageDealtMultiplier", 1f) * targetGain;
if (!MainGameManager.Instance.elementTags.Contains(keyword)) continue;
float targetGain = haveTarget ? target.GetRawAttribute(keyword + "DamageGainMultiplier", 1f) : 1f;
elementalMultiplier *= user.GetRawAttribute(keyword + "DamageDealtMultiplier", 1f) * targetGain;
}
// 魔法乘区:由 damageKeywords 中含 Magic/Arcane/Sorcery 时触发
magicMultiplier = 1f;
if (card.HasKeyword("Magic") || card.HasKeyword("Arcane") || card.HasKeyword("Sorcery"))
if (damageKeywords.Contains("Magic") || damageKeywords.Contains("Arcane") || damageKeywords.Contains("Sorcery"))
{
float targetGain = haveTarget ? target.GetRawAttribute("MagicDamageGainMultiplier", 1f) : 1f;
magicMultiplier = user.GetRawAttribute("MagicDamageDealtMultiplier", 1f) * targetGain;
@@ -278,7 +417,8 @@ namespace Continentis.MainGame.Card
float targetFinalGain = haveTarget ? target.GetRawAttribute("FinalDamageGainMultiplier", 1f) : 1f;
finalMultiplier = user.GetRawAttribute("FinalDamageDealtMultiplier", 1f) * targetFinalGain;
baseDamageAfterOffset = card.attributeSubmodule.GetCurrentAttribute("Damage") + physicsOffset + magicOffset;
string damageAttr = string.IsNullOrEmpty(baseDamageAttr) ? "Damage" : baseDamageAttr;
baseDamageAfterOffset = card.attributeSubmodule.GetCurrentAttribute(damageAttr) + physicsOffset + magicOffset;
float finalDamage = baseDamageAfterOffset * elementalMultiplier * magicMultiplier * finalMultiplier;
return Mathf.RoundToInt(finalDamage);
@@ -358,6 +498,17 @@ namespace Continentis.MainGame.Card
/// <summary>取消选中目标时触发的效果(在逻辑组件的 Untargeting 之前执行)。</summary>
public virtual void UntargetingEffect() { }
/// <summary>
/// 标记 hint shadow 在下一帧刷新,不触发文本重解析。
/// 子类在战场状态变化时调用此方法,而非直接操作 dirtyMark。
/// </summary>
protected void InvalidateHint() => card.contentSubmodule.hintDirtyMark = true;
/// 返回 null 表示不显示提示阴影;返回具体颜色则启用对应颜色的 hintShadow。
/// 此方法在 ContentSubmodule.RefreshContent() 时自动调用,
/// 子类可重写以实现"有可用目标时绿色/无可用目标时红色"等动态提示。
/// </summary>
public virtual Color? GetHintColor() => null;
}
/// <summary>卡牌逻辑组件基类。</summary>

View File

@@ -20,6 +20,7 @@ namespace Continentis.MainGame.Card
/// </summary>
public virtual void Targeting(CharacterBase target)
{
currentTextTarget = target;
eventSubmodule.onTargeting.Invoke(target);
}
@@ -29,6 +30,7 @@ namespace Continentis.MainGame.Card
/// </summary>
public virtual void Untargeting()
{
currentTextTarget = null;
eventSubmodule.onUntargeting.Invoke();
}
}
@@ -136,11 +138,26 @@ namespace Continentis.MainGame.Card
}
else
{
while (targets.Count < maximumTargets && valid.Count > 0)
bool allowDuplicate = HasKeyword(CardKeywords.AllowDuplicateTargets);
if (allowDuplicate)
{
CharacterBase target = valid[Random.Range(0, valid.Count)];
valid.Remove(target);
targets.Add(target);
// 放回抽样:可重复选中同一目标
for (int i = 0; i < maximumTargets; i++)
{
targets.Add(valid[Random.Range(0, valid.Count)]);
}
}
else
{
// 不放回抽样(原逻辑)
List<CharacterBase> pool = new List<CharacterBase>(valid);
while (targets.Count < maximumTargets && pool.Count > 0)
{
CharacterBase target = pool[Random.Range(0, pool.Count)];
pool.Remove(target);
targets.Add(target);
}
}
}
@@ -149,13 +166,13 @@ namespace Continentis.MainGame.Card
public virtual bool CheckBeforePlay()
{
if (!user.CheckEnoughStamina(GetAttribute("StaminaCost")))
if (!user.CheckEnoughStamina(GetAttribute(CardAttributes.StaminaCost)))
{
MainGameManager.Instance.basePrefabs.GenerateInfoText("Not Enough Stamina", user.characterView);
return false;
}
if (!user.CheckEnoughMana(GetAttribute("ManaCost")))
if (!user.CheckEnoughMana(GetAttribute(CardAttributes.ManaCost)))
{
MainGameManager.Instance.basePrefabs.GenerateInfoText("Not Enough Mana", user.characterView);
return false;
@@ -192,13 +209,13 @@ namespace Continentis.MainGame.Card
{
if (!noConsumption)
{
this.user.ModifyStamina(-GetAttribute("StaminaCost"));
this.user.ModifyMana(-GetAttribute("ManaCost"));
this.user.ModifyStamina(-GetAttribute(CardAttributes.StaminaCost));
this.user.ModifyMana(-GetAttribute(CardAttributes.ManaCost));
}
Debug.Log($"Starting to play card: {contentSubmodule.cardName}");
CommandQueueManager.Instance.AddCommand(new Cmd_Function(() =>
CommandQueueManager.Instance.AddCommand(Cmd.Do(() =>
{
playSubmodule.isDuringPlayEffect = true;
eventSubmodule.onBeforePlay.Invoke(targetList);
@@ -211,7 +228,7 @@ namespace Continentis.MainGame.Card
CommandQueueManager.Instance.AddCommand(PlayEffect(targetList));
CommandQueueManager.Instance.AddCommand(cardLogic.PlayEffect(targetList));
CommandQueueManager.Instance.AddCommand(new Cmd_Function(() =>
CommandQueueManager.Instance.AddCommand(Cmd.Do(() =>
{
eventSubmodule.onAfterPlay.Invoke(targetList);
combatBuffSubmodule.buffList.For(buff => buff.usageSubmodule?.UpdateModule());
@@ -308,6 +325,8 @@ namespace Continentis.MainGame.Card
KeyValuePair<string, List<CardInstance>> currentPile = deck.GetCardLocation(this, out int index);
if (!cardData.upgradeNode.isTerminalNode)
{
// 先 Dispose 旧 Logic再替换避免旧 Logic 的托管订阅泄漏
cardLogic?.Dispose();
DestroyHandCardView();
CardData newData = cardData.upgradeNode.upgradeCards[0]; //后续可改为选择升级方向

View File

@@ -111,6 +111,6 @@ namespace Continentis.MainGame.Card
public partial class AttributeSubmodule
{
public int targetCount => GetRoundCurrentAttribute("TargetCount", -2);
public int targetCount => GetRoundCurrentAttribute(CardAttributes.TargetCount, -2);
}
}

View File

@@ -22,15 +22,21 @@ namespace Continentis.MainGame.Card
/// </summary>
public bool dirtyMark;
/// <summary>
/// 标记hint shadow 颜色需要刷新,不触发文本重解析
/// </summary>
public bool hintDirtyMark;
public ContentSubmodule(CardInstance card) : base(card)
{
keywords = card.cardData.keywords;
cardName = card.cardData.displayName.Localize();
cardSprite = card.cardData.cardSprite;
cardSprite = card.cardData.cardSprite ?? MainGameManager.Instance.basePrefabs.defaultCardImage;
originalFunctionText = card.cardData.functionText.Localize();
cardRarity = card.cardData.cardRarity;
cardType = card.cardData.cardType;
dirtyMark = false;
hintDirtyMark = false;
Observable.EveryLateUpdate().Subscribe(_ =>
{
@@ -39,11 +45,12 @@ namespace Continentis.MainGame.Card
RefreshContent();
dirtyMark = false;
}
if (hintDirtyMark)
{
RefreshHintShadow();
hintDirtyMark = false;
}
}).AddTo(card.disposables);
//CardDescriptionInterpreter.InterpretDescription(card);
//keywords = CardDescriptionInterpreter.GetKeywords(card.cardData.cardDescription);
//Debug.Log($"Extracted Keywords: {string.Join(", ", keywords)}");
}
public void RefreshContent()
@@ -51,6 +58,19 @@ namespace Continentis.MainGame.Card
CardTextInterpreter.InterpretText(owner);
owner.handCardView?.Setup();
owner.intentionCardView?.Setup();
// 文本刷新后hint 也需要同步更新
hintDirtyMark = true;
}
/// <summary>
/// 仅刷新 hint shadow 颜色,不触发文本重解析。
/// </summary>
public void RefreshHintShadow()
{
if (owner.handCardView == null || owner.handCardView.isSelecting) return;
Color? hintColor = owner.cardLogic?.GetHintColor();
owner.handCardView.UpdateHintShadow(hintColor);
}
}
}

View File

@@ -42,7 +42,7 @@ namespace Continentis.MainGame.Card
public bool isDuringPlaying;
private void Update()
protected virtual void Update()
{
if (isHovering)
{
@@ -110,9 +110,9 @@ namespace Continentis.MainGame.Card
cardTypeText.text = this.card.contentSubmodule.cardType.ToString();
staminaCostText.rectTransform.parent.gameObject.SetActive(true);
staminaCostText.text = this.card.attributeSubmodule.GetRoundCurrentAttribute("StaminaCost").ToString();
staminaCostText.text = this.card.attributeSubmodule.GetRoundCurrentAttribute(CardAttributes.StaminaCost).ToString();
int manaCost = this.card.attributeSubmodule.GetRoundCurrentAttribute("ManaCost");
int manaCost = this.card.attributeSubmodule.GetRoundCurrentAttribute(CardAttributes.ManaCost);
manaCostText.rectTransform.parent.gameObject.SetActive(manaCost > 0);
manaCostText.text = manaCost.ToString();

View File

@@ -43,6 +43,46 @@ namespace Continentis.MainGame.Card
}
}
/// <summary>
/// 根据颜色智能更新提示阴影null 关闭,非 null 启用对应颜色。
/// 避免相同颜色重复 tween 导致闪烁。
/// </summary>
public void UpdateHintShadow(Color? color)
{
if (color == null)
{
if (hintShadow.gameObject.activeSelf)
{
DisableHintShadow();
}
return;
}
Color targetColor = color.Value;
if (hintShadow.gameObject.activeSelf)
{
// 已启用:仅在颜色差异足够大时 tween避免每帧闪烁
if (!ApproximatelyEqualColor(hintShadow.color, targetColor))
{
hintShadowTweener?.Kill();
hintShadowTweener = hintShadow.DOColor(targetColor, 0.2f).Play();
}
}
else
{
EnableHintShadow(targetColor);
}
}
private static bool ApproximatelyEqualColor(Color a, Color b, float tolerance = 0.01f)
{
return Mathf.Abs(a.r - b.r) < tolerance
&& Mathf.Abs(a.g - b.g) < tolerance
&& Mathf.Abs(a.b - b.b) < tolerance
&& Mathf.Abs(a.a - b.a) < tolerance;
}
public void EnableSelectShadow()
{
selectShadow.gameObject.SetActive(true);

View File

@@ -6,6 +6,7 @@ using Continentis.MainGame.UI;
using SLSFramework.General;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace Continentis.MainGame.Card
{
@@ -17,6 +18,32 @@ namespace Continentis.MainGame.Card
public List<CharacterBase> conditionNotMetTargets = new List<CharacterBase>();
public List<CharacterBase> invalidTargets = new List<CharacterBase>();
#region Multi-Target Selection State
/// <summary>是否正处于多目标逐次点击选择模式。</summary>
private bool isMultiTargetSelecting;
/// <summary>多目标模式下已选中的目标列表(可含重复)。</summary>
private readonly List<CharacterBase> multiTargetSelectedList = new List<CharacterBase>();
/// <summary>多目标模式需要的总选择次数。</summary>
private int multiTargetRequired;
/// <summary>多目标模式下卡牌固定显示的位置(箭头起点)。</summary>
private Vector3 multiTargetCardAnchor;
#endregion
protected override void Update()
{
base.Update();
if (isMultiTargetSelecting)
{
HandleMultiTargetInput();
}
}
public override void OnPointerEnter(PointerEventData eventData)
{
base.OnPointerEnter(eventData);
@@ -78,11 +105,23 @@ namespace Continentis.MainGame.Card
card.user = CombatMainManager.Instance.currentCharacter;
card.DetectTargetsValidity(out validTargets, out conditionNotMetTargets, out invalidTargets);
if (card.attributeSubmodule.targetCount == 1)
int targetCount = card.attributeSubmodule.targetCount;
if (targetCount > 1)
{
// 多目标模式:先执行通用的可打出校验
if (!CheckCanStartPlay())
{
ResetSelectionState();
return;
}
EnterMultiTargetMode(targetCount);
}
else if (targetCount == 1)
{
CombatUIManager.Instance.arrowsPage.GeneratePointerArrow(cardTransform.position, cardTransform.position, true);
}
else if (card.attributeSubmodule.targetCount == -1)
else if (targetCount == -1)
{
CombatUIManager.Instance.arrowsPage.GeneratePointerArrow(cardTransform.position, cardTransform.position, true);
@@ -95,6 +134,9 @@ namespace Continentis.MainGame.Card
public void OnDrag(PointerEventData eventData)
{
// 多目标模式下拖拽无效,交互由 Update 中的点击处理
if (isMultiTargetSelecting) return;
RectTransform arrowCanvasRect = CombatUIManager.Instance.arrowsPage.rectTransform;
Camera uiCamera = CombatUIManager.Instance.uiCamera;
Camera worldCamera = CombatUIManager.Instance.combatCamera;
@@ -117,7 +159,7 @@ namespace Continentis.MainGame.Card
card.contentSubmodule.dirtyMark = true;
}
Vector3 startPosition = cardTransform.position; //+ new Vector3(0, cardTransform.rect.height * cardTransform.lossyScale.y / 2, 0);
Vector3 startPosition = cardTransform.position;
Vector3 endPosition = SpaceConverter.ScreenPointToUIPoint(arrowCanvasRect, eventData.position, uiCamera);
PointerArrow mainPointerArrow = CombatUIManager.Instance.arrowsPage.mainPointerArrow;
@@ -222,6 +264,9 @@ namespace Continentis.MainGame.Card
public void OnEndDrag(PointerEventData eventData)
{
// 多目标模式下,拖拽结束只是从拖拽过渡到点击模式,不做打出判定
if (isMultiTargetSelecting) return;
CombatCharacterViewBase hoveringCharacterView = CombatUIManager.Instance.hoveringCharacterView;
CharacterBase hoveringCharacter = hoveringCharacterView != null ? hoveringCharacterView.character : null;
Camera uiCamera = CombatUIManager.Instance.uiCamera;
@@ -242,11 +287,7 @@ namespace Continentis.MainGame.Card
return;
}
if (card.HasKeyword("Unplayable")) // 如果有“不能打出”关键词,直接返回
{
return;
}
if (!CheckCanStartPlay()) return;
if (!card.HasKeyword("TargetSelf"))
{
@@ -290,5 +331,211 @@ namespace Continentis.MainGame.Card
}
}
}
#region Play Validation
/// <summary>
/// 检查卡牌是否可以被打出Unplayable / Prerequisite 校验)。
/// 校验失败时会生成提示文本。
/// </summary>
private bool CheckCanStartPlay()
{
if (card.HasKeyword("Unplayable"))
{
return false;
}
if (!card.HasKeyword("Prerequisite"))
{
CharacterBase currentCharacter = CombatMainManager.Instance.currentCharacter;
List<CardInstance> handPile = currentCharacter.deckSubmodule.HandPile;
for (int i = 0; i < handPile.Count; i++)
{
if (handPile[i].HasKeyword("Prerequisite"))
{
MainGameManager.Instance.basePrefabs.GenerateInfoText(
"Keyword_Prerequisite_Warning".Localize(), currentCharacter.characterView);
return false;
}
}
}
return true;
}
/// <summary>重置选择状态(不触发 Untargeting 事件)。</summary>
private void ResetSelectionState()
{
isSelecting = false;
isHovering = false;
CombatUIManager.Instance.selectingCardView = null;
canvas.overrideSorting = false;
canvas.sortingOrder = 0;
}
#endregion
#region Multi-Target Selection
/// <summary>进入多目标逐次选择模式。</summary>
private void EnterMultiTargetMode(int required)
{
isMultiTargetSelecting = true;
multiTargetRequired = required;
multiTargetSelectedList.Clear();
// 记录卡牌当前位置作为箭头起点锚点
multiTargetCardAnchor = cardTransform.position;
UpdateMultiTargetHint();
}
/// <summary>多目标模式下的输入处理,在 Update 中调用。</summary>
private void HandleMultiTargetInput()
{
// Escape 取消全部
if (Keyboard.current.escapeKey.wasPressedThisFrame)
{
ExitMultiTargetMode();
return;
}
// 右键撤销上一步
if (Mouse.current.rightButton.wasPressedThisFrame)
{
UndoLastMultiTarget();
return;
}
// 左键选择目标
if (Mouse.current.leftButton.wasPressedThisFrame)
{
CombatCharacterViewBase hoveringView = CombatUIManager.Instance.hoveringCharacterView;
if (hoveringView == null) return;
CharacterBase hoveringCharacter = hoveringView.character;
// 判断是否为有效目标
if (!validTargets.Contains(hoveringCharacter)) return;
// 判断是否允许重复选择
if (!card.HasKeyword(CardKeywords.AllowDuplicateTargets)
&& multiTargetSelectedList.Contains(hoveringCharacter))
{
return;
}
// 选中目标
multiTargetSelectedList.Add(hoveringCharacter);
// 生成固定箭头指向该目标
RectTransform arrowCanvasRect = CombatUIManager.Instance.arrowsPage.rectTransform;
Camera worldCamera = CombatUIManager.Instance.combatCamera;
Camera uiCamera = CombatUIManager.Instance.uiCamera;
Vector3 targetWorldPos = hoveringView.transform.position;
Vector3 targetUIPos = SpaceConverter.WorldPointToUIPoint(
arrowCanvasRect, targetWorldPos, worldCamera, uiCamera);
// 第一支箭头为 main后续为 other
bool isMain = multiTargetSelectedList.Count == 1;
CombatUIManager.Instance.arrowsPage.GeneratePointerArrow(
multiTargetCardAnchor, targetUIPos, isMain);
// 触发 TargetingEffect 更新伤害预览
card.Targeting(hoveringCharacter);
card.contentSubmodule.dirtyMark = true;
UpdateMultiTargetHint();
// 检查是否选满
if (multiTargetSelectedList.Count >= multiTargetRequired)
{
ConfirmMultiTargetPlay();
}
}
}
/// <summary>撤销多目标模式下的上一步选择。</summary>
private void UndoLastMultiTarget()
{
if (multiTargetSelectedList.Count > 0)
{
multiTargetSelectedList.RemoveAt(multiTargetSelectedList.Count - 1);
CombatUIManager.Instance.arrowsPage.RemoveLastPointerArrow();
// 如果还有已选目标,触发最后一个目标的 TargetingEffect
if (multiTargetSelectedList.Count > 0)
{
card.Targeting(multiTargetSelectedList[^1]);
}
else
{
card.Untargeting();
}
card.contentSubmodule.dirtyMark = true;
UpdateMultiTargetHint();
}
// 已选归零 → 取消选择,卡牌归位
if (multiTargetSelectedList.Count == 0)
{
ExitMultiTargetMode();
}
}
/// <summary>选满后自动打出。</summary>
private void ConfirmMultiTargetPlay()
{
List<CharacterBase> targets = new List<CharacterBase>(multiTargetSelectedList);
// 清理多目标状态
isMultiTargetSelecting = false;
multiTargetSelectedList.Clear();
multiTargetRequired = 0;
CombatUIManager.Instance.arrowsPage.ClearPointerArrows();
isSelecting = false;
isHovering = false;
CombatUIManager.Instance.selectingCardView = null;
canvas.overrideSorting = false;
canvas.sortingOrder = 0;
// 打出卡牌
if (!card.Play(targets))
{
card.eventSubmodule.onUntargeting();
card.contentSubmodule.dirtyMark = true;
}
}
/// <summary>退出多目标选择模式(取消操作),清理所有状态,卡牌归位。</summary>
private void ExitMultiTargetMode()
{
isMultiTargetSelecting = false;
multiTargetSelectedList.Clear();
multiTargetRequired = 0;
CombatUIManager.Instance.arrowsPage.ClearPointerArrows();
isSelecting = false;
isHovering = false;
CombatUIManager.Instance.selectingCardView = null;
canvas.overrideSorting = false;
canvas.sortingOrder = 0;
card.eventSubmodule.onUntargeting();
card.contentSubmodule.dirtyMark = true;
}
/// <summary>更新多目标选择的计数提示。</summary>
private void UpdateMultiTargetHint()
{
string hint = $"{multiTargetSelectedList.Count} / {multiTargetRequired}";
MainGameManager.Instance.basePrefabs.GenerateInfoText(
hint, card.user.characterView, Color.cyan);
}
#endregion
}
}

View File

@@ -145,6 +145,7 @@ namespace Continentis.MainGame.Card
categoryName = resolvedCategory;
displayName = $"Card_{resolvedMod}_{className}_DisplayName";
functionText = $"Card_{resolvedMod}_{className}_FunctionText";
descriptionText = $"Card_{resolvedMod}_{className}_Description";
EditorUtility.SetDirty(this);

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Continentis.MainGame.Character;
using Continentis.Mods.Basic.Cards;
using UnityEngine;
@@ -19,66 +20,67 @@ namespace Continentis.MainGame.Card
/// <summary>
/// 设置伤害值
/// </summary>
/// <param name="additive">是否为叠加true为叠加false为覆盖</param>
/// <param name="originalDamage">原始伤害值仅在additive为true时有效否则被覆盖为BaseDamage</param>
/// <param name="damageAttributeName">要写入的属性名,默认为 "Damage"</param>
/// <param name="damageOffset">伤害增量</param>
public void SetDamage(int damageOffset, bool additive = false, int originalDamage = 0)
/// <param name="additive">是否为叠加true为叠加false为覆盖</param>
/// <param name="originalDamage">原始伤害值仅在additive为true时有效</param>
public void SetDamage(int damageOffset, string damageAttributeName = "Damage", bool additive = false, int originalDamage = 0)
{
card.SetVariableAttribute("Damage", damageOffset, additive, originalDamage);
card.SetVariableAttribute(damageAttributeName, damageOffset, additive, originalDamage);
}
/// <summary>
/// 默认伤害计算,仅使用卡牌基础伤害(无任何属性加成)
/// </summary>
public void SetDamage_Default()
public void SetDamage_Default(string damageAttributeName = "Damage")
{
SetDamage(0);
SetDamage(0, damageAttributeName);
}
/// <summary>
/// 斩击伤害计算,伤害=基础伤害+(力量加成+敏捷加成)/2
/// </summary>
public void SetDamage_Slash(bool additive = false, int originalDamage = 0)
public void SetDamage_Slash(string damageAttributeName = "Damage", bool additive = false, int originalDamage = 0)
{
float rawDamageOffsetFromStrength = user.GetRawAttribute("OffsetFromStrength");
float rawDamageOffsetFromAgility = user.GetRawAttribute("OffsetFromAgility");
SetDamage(Mathf.RoundToInt((rawDamageOffsetFromStrength + rawDamageOffsetFromAgility) / 2f), additive, originalDamage);
SetDamage(Mathf.RoundToInt((rawDamageOffsetFromStrength + rawDamageOffsetFromAgility) / 2f), damageAttributeName, additive, originalDamage);
}
/// <summary>
/// 打击伤害计算,伤害=基础伤害+力量加成
/// </summary>
public void SetDamage_Strike(bool additive = false, int originalDamage = 0)
public void SetDamage_Strike(string damageAttributeName = "Damage", bool additive = false, int originalDamage = 0)
{
int damageOffset = user.GetAttribute("OffsetFromStrength");
SetDamage(damageOffset, additive, originalDamage);
SetDamage(damageOffset, damageAttributeName, additive, originalDamage);
}
/// <summary>
/// 突刺伤害计算,伤害=基础伤害+敏捷加成
/// </summary>
public void SetDamage_Prick(bool additive = false, int originalDamage = 0)
public void SetDamage_Prick(string damageAttributeName = "Damage", bool additive = false, int originalDamage = 0)
{
int damageOffset = user.GetAttribute("OffsetFromAgility");
SetDamage(damageOffset, additive, originalDamage);
SetDamage(damageOffset, damageAttributeName, additive, originalDamage);
}
/// <summary>
/// 奥术伤害计算,伤害=基础伤害+智力加成
/// </summary>
public void SetDamage_Arcane(bool additive = false, int originalDamage = 0)
public void SetDamage_Arcane(string damageAttributeName = "Damage", bool additive = false, int originalDamage = 0)
{
int damageOffset = user.GetAttribute("OffsetFromIntelligence");
SetDamage(damageOffset, additive, originalDamage);
SetDamage(damageOffset, damageAttributeName, additive, originalDamage);
}
/// <summary>
/// 契术伤害计算,伤害=基础伤害+魅力加成
/// </summary>
public void SetDamage_Sorcery(bool additive = false, int originalDamage = 0)
public void SetDamage_Sorcery(string damageAttributeName = "Damage", bool additive = false, int originalDamage = 0)
{
int damageOffset = user.GetAttribute("OffsetFromCharisma");
SetDamage(damageOffset, additive, originalDamage);
SetDamage(damageOffset, damageAttributeName, additive, originalDamage);
}
}
}

View File

@@ -16,9 +16,9 @@ namespace Continentis.MainGame.Card
{
card.SetAttribute("DisplayDodge", card.GetAttribute("Dodge"));
}
else if(card.HasAttribute("Shield"))
else if(card.HasAttribute("TemporaryHealth"))
{
card.SetAttribute("DisplayShield", card.GetAttribute("Shield"));
card.SetAttribute("DisplayTemporaryHealth", card.GetAttribute("TemporaryHealth"));
}
}
@@ -32,9 +32,9 @@ namespace Continentis.MainGame.Card
{
card.SetAttribute("DisplayDodge", card.GetAttribute("Dodge"));
}
else if(card.HasAttribute("Shield"))
else if(card.HasAttribute("TemporaryHealth"))
{
card.SetAttribute("DisplayShield", card.GetAttribute("Shield"));
card.SetAttribute("DisplayTemporaryHealth", card.GetAttribute("TemporaryHealth"));
}
}
@@ -102,11 +102,11 @@ namespace Continentis.MainGame.Card
}
/// <summary>
/// 设置护盾值,默认无加成
/// 设置临时生命值,默认无加成
/// </summary>
public void SetShield(bool additive = false, int originalShield = 0)
{
card.SetVariableAttribute("Shield", 0, additive, originalShield);
card.SetVariableAttribute("TemporaryHealth", 0, additive, originalShield);
}
}
}