Files
Continentis/Assets/Scripts/MainGame/Card/CardLogicBase.cs
SoulliesOfficial ac98ec3aef 更新
2026-04-17 12:01:50 -04:00

534 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using Continentis.MainGame.Character;
using Continentis.MainGame.Equipment;
using SoftCircuits.Collections;
using SLSUtilities.General;
using SLSUtilities.UModAssistance;
using UnityEngine;
namespace Continentis.MainGame.Card
{
public abstract partial class CardLogicBase
{
[Header("Reference")]
public CardInstance card;
public CardData cardData => card.cardData;
public CharacterBase user => card.user;
public HashSet<CardLogicComponentBase> logicComponents { get; private set; }
/// <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)
{
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;
}
// 存储所有通过 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()
{
if (logicComponents.Any(component => component is T))
{
Debug.LogWarning($"Card {card.cardData.className} already has component of type {typeof(T)}, cannot add duplicate.");
return null;
}
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() { }
/// <summary>卡牌出牌效果,返回一个 CommandGroup 供队列执行。</summary>
public virtual CommandGroup PlayEffect(List<CharacterBase> targetList)
{
return new CommandGroup();
}
public virtual void AfterPlayEffect(List<CharacterBase> targetList) { }
}
#region Attributes
public partial class CardLogicBase
{
/// <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) => 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
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>
protected CommandGroup SingleCommandGroup(params CommandBase[] commands)
{
return SingleCommandGroup(ExecutionMode.Parallel, commands);
}
/// <summary>
/// 克隆命令模板列表,按 executionMode 组合为单个 CommandGroup。
/// </summary>
protected virtual CommandGroup SingleCommandGroup(
ExecutionMode executionMode = ExecutionMode.Parallel, params CommandBase[] commands)
{
var group = new CommandGroup(executionMode);
foreach (CommandBase template in commands)
group.AddCommand(template.Clone());
return group;
}
/// <summary>
/// 对目标列表中的每个目标克隆命令模板并注入 Target生成嵌套 CommandGroup。
/// 新代码请改用 <see cref="ForEachTarget"/> 闭包工厂模式。
/// </summary>
[Obsolete("请改用 ForEachTarget(targetList, target => ...) 闭包工厂模式。")]
protected CommandGroup TargetListCommandGroup(
List<CharacterBase> targetList,
params CommandBase[] singleCommands)
{
return TemplateTargetGroup(targetList, ExecutionMode.Sequential, ExecutionMode.Parallel, singleCommands);
}
/// <summary>
/// 对目标列表中的每个目标克隆命令模板并注入 Target生成嵌套 CommandGroup。
/// 新代码请改用 <see cref="ForEachTarget"/> 闭包工厂模式。
/// </summary>
[Obsolete("请改用 ForEachTarget(targetList, target => ...) 闭包工厂模式。")]
protected virtual CommandGroup TargetListCommandGroup(
List<CharacterBase> targetList,
ExecutionMode mainExecutionMode = ExecutionMode.Sequential,
ExecutionMode singleExecutionMode = ExecutionMode.Parallel,
params CommandBase[] singleCommands)
{
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)
{
var singleGroup = new CommandGroup(singleExecutionMode);
foreach (CommandBase template in singleCommands)
{
CommandBase clone = template.Clone();
// 收集所有子命令(含嵌套组内的命令)并注入 Target
var allCommands = clone is CommandGroup group
? group.GetAllCommands(true)
: new List<CommandBase> { clone };
foreach (CommandBase cmd in allCommands)
cmd.selfContext.Set(CommandContextKeys.Target, target);
singleGroup.AddCommand(clone);
}
mainGroup.AddCommand(singleGroup);
}
return mainGroup;
}
}
#endregion
#region Attack
public partial class CardLogicBase
{
/// <summary>
/// 以当前卡牌作为来源,对目标发动攻击。
/// 内部自动构建携带 sourceCard 的 AttackContext确保 Buff 层能正确识别来源卡牌信息。
/// 卡牌脚本中所有的攻击调用都应优先使用此方法,而非直接调用 user.Attack()。
/// </summary>
protected AttackResult AttackTarget(CharacterBase target, int damage, AttackContext ctx = null)
{
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(AttackContext ctx = null)
{
return GetFinalDamage(null, ctx, out _, out _, out _, out _);
}
protected virtual int GetFinalDamage(
CharacterBase target, AttackContext ctx,
out float baseDamageAfterOffset, out float elementalMultiplier,
out float magicMultiplier, out float finalMultiplier)
{
bool haveTarget = target != null;
// 从 AttackContext 中读取伤害关键词和属性名null 时回退到卡牌默认值
List<string> damageKeywords = ctx?.damageKeywords ?? card.GetElementalKeywords();
string baseDamageAttr = ctx?.baseDamageAttributeName;
// Physics / Magic offset 由 damageKeywords 驱动,与卡牌标记关键词无关
int physicsOffset = 0;
if (damageKeywords.Contains("Physics"))
physicsOffset = user.GetAttribute(CharacterAttributes.PhysicsDamageDealtOffset);
int magicOffset = 0;
if (damageKeywords.Contains("Magic"))
magicOffset = user.GetAttribute(CharacterAttributes.MagicDamageDealtOffset);
// 元素乘区:遍历 damageKeywords 中属于 elementTags 的部分
elementalMultiplier = 1f;
foreach (string keyword in damageKeywords)
{
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 (damageKeywords.Contains("Magic") || damageKeywords.Contains("Arcane") || damageKeywords.Contains("Sorcery"))
{
float targetGain = haveTarget ? target.GetRawAttribute("MagicDamageGainMultiplier", 1f) : 1f;
magicMultiplier = user.GetRawAttribute("MagicDamageDealtMultiplier", 1f) * targetGain;
}
float targetFinalGain = haveTarget ? target.GetRawAttribute("FinalDamageGainMultiplier", 1f) : 1f;
finalMultiplier = user.GetRawAttribute("FinalDamageDealtMultiplier", 1f) * targetFinalGain;
string damageAttr = string.IsNullOrEmpty(baseDamageAttr) ? "Damage" : baseDamageAttr;
baseDamageAfterOffset = card.attributeSubmodule.GetCurrentAttribute(damageAttr) + physicsOffset + magicOffset;
float finalDamage = baseDamageAfterOffset * elementalMultiplier * magicMultiplier * finalMultiplier;
return Mathf.RoundToInt(finalDamage);
}
}
#endregion
#region Buffs
public partial class CardLogicBase
{
/// <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
{
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 类型注册)。</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
{
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>
public CardData GetDerivativeCardData(int index)
{
return ModManager.GetData<CardData>(cardData.derivativeCardDataRefs[index]);
}
/// <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 之前执行)。</summary>
public virtual void TargetingEffect(CharacterBase target) { }
/// <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>
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;
this.card.eventSubmodule.onTargeting += TargetingEffect;
this.card.eventSubmodule.onUntargeting += UntargetingEffect;
}
protected virtual void TargetingEffect(CharacterBase target) { }
protected virtual void UntargetingEffect() { }
}
}