更新
This commit is contained in:
131
Assets/Scripts/MainGame/Base/AttackContext.cs
Normal file
131
Assets/Scripts/MainGame/Base/AttackContext.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Continentis.MainGame.Card;
|
||||
|
||||
namespace Continentis.MainGame
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击上下文,携带来源卡牌和标签信息。
|
||||
/// 替代 Attack() 方法中零散的 boolean 参数,提供可扩展的伤害标记体系。
|
||||
/// </summary>
|
||||
public class AttackContext
|
||||
{
|
||||
/// <summary>触发此次攻击的来源卡牌(可为 null)。</summary>
|
||||
public CardInstance sourceCard;
|
||||
|
||||
/// <summary>攻击标签集合,用于控制伤害计算与事件触发行为。</summary>
|
||||
public HashSet<string> tags;
|
||||
|
||||
/// <summary>
|
||||
/// 伤害属性关键词(如 "Physics"、"Fire"),驱动 offset 和元素乘区计算。
|
||||
/// 为 null 时回退到卡牌元素关键词(向后兼容)。
|
||||
/// </summary>
|
||||
public List<string> damageKeywords;
|
||||
|
||||
/// <summary>
|
||||
/// 基础伤害读取的属性名(如 "Damage_Physics")。
|
||||
/// 为 null 或空时默认读取 "Damage"。
|
||||
/// </summary>
|
||||
public string baseDamageAttributeName;
|
||||
|
||||
public AttackContext()
|
||||
{
|
||||
tags = new HashSet<string>();
|
||||
}
|
||||
|
||||
public AttackContext(CardInstance sourceCard) : this()
|
||||
{
|
||||
this.sourceCard = sourceCard;
|
||||
}
|
||||
|
||||
/// <summary>检查是否包含指定标签。</summary>
|
||||
public bool HasTag(string tag)
|
||||
{
|
||||
return tags != null && tags.Contains(tag);
|
||||
}
|
||||
|
||||
/// <summary>检查是否包含任意一个指定标签。</summary>
|
||||
public bool HasAnyTag(params string[] checkTags)
|
||||
{
|
||||
if (tags == null || tags.Count == 0) return false;
|
||||
return checkTags.Any(t => tags.Contains(t));
|
||||
}
|
||||
|
||||
/// <summary>链式添加标签。</summary>
|
||||
public AttackContext WithTag(string tag)
|
||||
{
|
||||
tags ??= new HashSet<string>();
|
||||
tags.Add(tag);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>链式添加多个标签。</summary>
|
||||
public AttackContext WithTags(params string[] newTags)
|
||||
{
|
||||
tags ??= new HashSet<string>();
|
||||
foreach (string tag in newTags)
|
||||
{
|
||||
tags.Add(tag);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>链式设置伤害属性关键词。</summary>
|
||||
public AttackContext WithDamageKeywords(params string[] keywords)
|
||||
{
|
||||
damageKeywords = new List<string>(keywords);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>链式设置基础伤害属性名。</summary>
|
||||
public AttackContext WithBaseDamageAttribute(string attributeName)
|
||||
{
|
||||
baseDamageAttributeName = attributeName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>创建一个携带来源卡牌的默认上下文。</summary>
|
||||
public static AttackContext Default(CardInstance card = null)
|
||||
{
|
||||
return new AttackContext(card);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预定义的攻击标签常量。
|
||||
/// Mod 开发者可以使用自定义字符串扩展标签体系,无需修改此类。
|
||||
/// </summary>
|
||||
public static class AttackTags
|
||||
{
|
||||
/// <summary>
|
||||
/// 完全静默攻击:不触发任何攻击事件(角色 EventSubmodule + Buff 层均跳过)。
|
||||
/// 等价于旧 API 的 triggerAttackEvent = false。
|
||||
/// </summary>
|
||||
public const string Silent = "Silent";
|
||||
|
||||
/// <summary>
|
||||
/// 响应式攻击:由 Buff 被动效果产生的附带攻击。
|
||||
/// 触发角色 EventSubmodule 事件,但跳过 Buff 层的 onDealAttack/onGetAttacked,
|
||||
/// 从机制上杜绝 Buff 触发链的无限递归。
|
||||
/// </summary>
|
||||
public const string Reactive = "Reactive";
|
||||
|
||||
/// <summary>
|
||||
/// 生命移除:无视闪避、格挡、护盾,不触发任何事件。
|
||||
/// 直接对目标造成生命值扣减,类似 Dota 2 的 HP Removal。
|
||||
/// </summary>
|
||||
public const string HpRemoval = "HpRemoval";
|
||||
|
||||
/// <summary>
|
||||
/// 反弹伤害:由反弹/反伤机制产生的伤害。
|
||||
/// Buff 可检查此标签以避免"反弹的反弹"导致无限循环。
|
||||
/// </summary>
|
||||
public const string Reflected = "Reflected";
|
||||
|
||||
/// <summary>必中,无视闪避。</summary>
|
||||
public const string GuaranteedHit = "GuaranteedHit";
|
||||
|
||||
/// <summary>穿透,无视格挡。</summary>
|
||||
public const string Penetrating = "Penetrating";
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/Base/AttackContext.cs.meta
Normal file
2
Assets/Scripts/MainGame/Base/AttackContext.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9deb152fc38db9458198d27672d8a30
|
||||
@@ -30,6 +30,9 @@ namespace Continentis.MainGame
|
||||
public GameObject inspectionCardObject;
|
||||
public SerializableDictionary<string, Sprite> intentionMarkIcons;
|
||||
public SerializableDictionary<string, CardViewCollection> cardViewCollections;
|
||||
public Sprite defaultCardImage;
|
||||
|
||||
[Header("Buffs")] public Sprite defaultBuffIcon;
|
||||
|
||||
[Header("GeneralUI")] public GameObject customImage;
|
||||
public GameObject informationBox;
|
||||
@@ -61,7 +64,7 @@ namespace Continentis.MainGame
|
||||
|
||||
public partial class BasePrefabs
|
||||
{
|
||||
public DamageNumber GenerateHurtText(int amount, CombatCharacterViewBase characterView)
|
||||
public DamageNumber GenerateHurtText(int amount, CombatCharacterViewBase characterView, Color color)
|
||||
{
|
||||
Vector3 position = characterView.transform.position + Vector3.up * 0.5f;
|
||||
|
||||
@@ -70,14 +73,15 @@ namespace Continentis.MainGame
|
||||
position = characterView.numbersPivot.position;
|
||||
}
|
||||
|
||||
DamageNumber infoText = GenerateHurtText(amount, position);
|
||||
DamageNumber infoText = GenerateHurtText(amount, position, color);
|
||||
return infoText;
|
||||
}
|
||||
|
||||
public DamageNumber GenerateHurtText(int amount, Vector3 position)
|
||||
public DamageNumber GenerateHurtText(int amount, Vector3 position, Color color)
|
||||
{
|
||||
DamageNumber hurtText = GenerateCombatText(hurtDamageNumber, position);
|
||||
hurtText.number = amount;
|
||||
hurtText.SetColor(color);
|
||||
return hurtText;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,16 +33,16 @@ namespace Continentis.MainGame
|
||||
public const string ManaRecoverPerAction = "ManaRecoverPerAction";
|
||||
|
||||
// ── 防御 ────────────────────────────────────
|
||||
public const string Block = "Block";
|
||||
public const string Shield = "Shield";
|
||||
public const string Dodge = "Dodge";
|
||||
public const string Block = "Block";
|
||||
public const string TemporaryHealth = "TemporaryHealth";
|
||||
public const string Dodge = "Dodge";
|
||||
|
||||
public const string BlockGainOffset = "BlockGainOffset";
|
||||
public const string BlockGainMultiplier = "BlockGainMultiplier";
|
||||
public const string DodgeGainOffset = "DodgeGainOffset";
|
||||
public const string DodgeGainMultiplier = "DodgeGainMultiplier";
|
||||
public const string ShieldGainOffset = "ShieldGainOffset";
|
||||
public const string ShieldGainMultiplier = "ShieldGainMultiplier";
|
||||
public const string BlockGainOffset = "BlockGainOffset";
|
||||
public const string BlockGainMultiplier = "BlockGainMultiplier";
|
||||
public const string DodgeGainOffset = "DodgeGainOffset";
|
||||
public const string DodgeGainMultiplier = "DodgeGainMultiplier";
|
||||
public const string TemporaryHealthGainOffset = "TemporaryHealthGainOffset";
|
||||
public const string TemporaryHealthGainMultiplier = "TemporaryHealthGainMultiplier";
|
||||
|
||||
public const string KeepBlockOnActionStart = "KeepBlockOnActionStart";
|
||||
public const string KeepDodgeOnActionStart = "KeepDodgeOnActionStart";
|
||||
|
||||
@@ -33,9 +33,12 @@ namespace Continentis.MainGame
|
||||
TextInterpreter.SetFunction("Attribute", new Func<string, bool, string>((name, high) => GetAttribute(card, name, high, false)));
|
||||
TextInterpreter.SetFunction("Attribute", new Func<string, bool, bool, string>((name, high, percent) => GetAttribute(card, name, high, percent)));
|
||||
|
||||
string result = DynamicTextInterpreter.Parse(TextInterpreter, textToInterpret);
|
||||
// 第一阶段:根据卡牌上下文(持有者、目标等)裁剪条件标签块
|
||||
string resolved = CardTextTagResolver.Resolve(card, textToInterpret);
|
||||
|
||||
// 第二阶段:Dynamic Expresso 求值 $Attribute / $Keyword 等表达式
|
||||
string result = DynamicTextInterpreter.Parse(TextInterpreter, resolved);
|
||||
|
||||
Debug.Log($"Interpreted Text: {result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
144
Assets/Scripts/MainGame/Base/Interpreters/CardTextTagResolver.cs
Normal file
144
Assets/Scripts/MainGame/Base/Interpreters/CardTextTagResolver.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Continentis.MainGame.Card;
|
||||
using Continentis.MainGame.Character;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame
|
||||
{
|
||||
/// <summary>
|
||||
/// 卡牌文本条件标签解析器。
|
||||
/// 在 Dynamic Expresso 求值之前,根据卡牌的上下文(持有者、目标等)裁剪条件标签块。
|
||||
///
|
||||
/// 支持的标签:
|
||||
/// [Showcase]...[/Showcase] 仅在展示界面(无持有者)显示
|
||||
/// [Owner: PlayerHero]...[/Owner] 持有者为 PlayerHero 时显示
|
||||
/// [Owner: CombatNPC]...[/Owner] 持有者为 CombatNPC 时显示
|
||||
/// [Target: PlayerHero]...[/Target] 目标为 PlayerHero 时显示
|
||||
/// [Target: CombatNPC]...[/Target] 目标为 CombatNPC 时显示
|
||||
///
|
||||
/// 解析规则:
|
||||
/// - 文本无任何标签 → 原样返回
|
||||
/// - 无持有者(展示界面) → 仅显示 [Showcase],剥离 [Owner] / [Target]
|
||||
/// - 有持有者,无目标 → 按持有者类型反推默认目标(PlayerHero → CombatNPC, 反之亦然)
|
||||
/// - 有持有者,有目标 → 按实际目标类型匹配 [Target]
|
||||
/// </summary>
|
||||
public static class CardTextTagResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 匹配条件标签块。支持两种格式:
|
||||
/// 带参数:[Category: Value]content[/Category]
|
||||
/// 无参数:[Showcase]content[/Showcase]
|
||||
/// </summary>
|
||||
private static readonly Regex TagPattern = new Regex(
|
||||
@"\[(?<category>Showcase|Owner|Target)(?:\s*:\s*(?<value>\w+))?\](?<content>.*?)\[/\k<category>\]",
|
||||
RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 解析文本中的条件标签块,根据卡牌上下文保留或移除。
|
||||
/// 如果文本中没有任何标签,原样返回。
|
||||
/// </summary>
|
||||
public static string Resolve(CardInstance card, string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
if (!TagPattern.IsMatch(text)) return text;
|
||||
|
||||
CharacterBase owner = card.owner as CharacterBase;
|
||||
CharacterBase target = card.currentTextTarget;
|
||||
bool isShowcase = (owner == null);
|
||||
|
||||
return TagPattern.Replace(text, match =>
|
||||
{
|
||||
string category = match.Groups["category"].Value;
|
||||
string value = match.Groups["value"].Success ? match.Groups["value"].Value : null;
|
||||
string content = match.Groups["content"].Value;
|
||||
|
||||
switch (category.ToLower())
|
||||
{
|
||||
case "showcase":
|
||||
return ResolveShowcase(isShowcase, content);
|
||||
case "owner":
|
||||
return ResolveOwner(owner, value, content);
|
||||
case "target":
|
||||
return ResolveTarget(owner, target, value, content);
|
||||
default:
|
||||
Debug.LogWarning($"[CardTextTagResolver] Unknown tag category: '{category}'.");
|
||||
return string.Empty;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Showcase] 标签:仅在展示界面(无持有者)显示。
|
||||
/// </summary>
|
||||
private static string ResolveShowcase(bool isShowcase, string content)
|
||||
{
|
||||
return isShowcase ? content : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Owner: X] 标签:根据持有者类型匹配。展示界面下不显示。
|
||||
/// </summary>
|
||||
private static string ResolveOwner(CharacterBase owner, string value, string content)
|
||||
{
|
||||
if (owner == null) return string.Empty;
|
||||
return MatchesCharacterType(owner, value) ? content : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Target: X] 标签:根据目标类型匹配。
|
||||
/// - 展示界面:不显示
|
||||
/// - 有明确目标:按实际目标匹配
|
||||
/// - 无明确目标:按持有者反推默认目标(PlayerHero → CombatNPC,反之亦然)
|
||||
/// </summary>
|
||||
private static string ResolveTarget(CharacterBase owner, CharacterBase target, string value, string content)
|
||||
{
|
||||
if (owner == null) return string.Empty;
|
||||
|
||||
// 有明确目标时,按实际类型匹配
|
||||
if (target != null)
|
||||
{
|
||||
return MatchesCharacterType(target, value) ? content : string.Empty;
|
||||
}
|
||||
|
||||
// 无明确目标时,按持有者类型反推默认目标
|
||||
if (owner is PlayerHero)
|
||||
{
|
||||
return string.Equals(value, "CombatNPC", StringComparison.OrdinalIgnoreCase) ? content : string.Empty;
|
||||
}
|
||||
|
||||
if (owner is CombatNPC)
|
||||
{
|
||||
return string.Equals(value, "PlayerHero", StringComparison.OrdinalIgnoreCase) ? content : string.Empty;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[CardTextTagResolver] Cannot infer default target for owner type '{owner.GetType().Name}'.");
|
||||
return content; // 未知持有者类型,保留内容作为兜底
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查角色是否匹配指定的类型名。
|
||||
/// </summary>
|
||||
private static bool MatchesCharacterType(CharacterBase character, string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
Debug.LogWarning("[CardTextTagResolver] Tag value is empty for Owner/Target tag.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeName.ToLower() switch
|
||||
{
|
||||
"playerhero" => character is PlayerHero,
|
||||
"combatnpc" => character is CombatNPC,
|
||||
_ => LogUnknownType(typeName)
|
||||
};
|
||||
}
|
||||
|
||||
private static bool LogUnknownType(string typeName)
|
||||
{
|
||||
Debug.LogWarning($"[CardTextTagResolver] Unknown character type: '{typeName}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec0a58018fe36a1469ea1ee568a662e0
|
||||
Reference in New Issue
Block a user