243 lines
8.4 KiB
C#
Raw Normal View History

using System;
namespace GAS.Runtime
{
public abstract class AbilitySpec
{
/// <summary>
/// 获取激活能力时传递给能力的参数。
/// 在技能过程中不应该修改, 考虑使用UserData
/// </summary>
/// <remarks>
/// <para>旧版本为 object[] 类型, 有内存分配和装箱/拆箱问题, 应自行创建一个数据结构(推荐record)来传参.</para>
/// </remarks>
public object AbilityArgument { get; private set; }
/// <summary>
/// 仅限GrantedAbility, 激活能力时传递给能力的效果规格。
/// 可以通过给gameplayEffectSpec添加自定义数据(UserData)来传递更多信息。
/// </summary>
public EntityRef<GameplayEffectSpec> GameplayEffectSpec { get; private set; }
/// <summary>
/// 获取或设置与能力关联的自定义数据。
/// </summary>
/// <remarks>
/// <para>此属性用于存储能力的自定义信息,以便在能力的不同任务之间共享数据。</para>
/// <para>例如,可以在一个技能的任务(AbilityTask)中设置此数据,然后在同一个技能的另一个任务(AbilityTask)中检索和使用该数据。</para>
/// </remarks>
public object UserData { get; set; }
public AbilitySpec(AbstractAbility ability, AbilitySystemComponent owner)
{
Ability = ability;
Owner = owner;
}
public virtual void Dispose()
{
_onActivateResult = null;
_onEndAbility = null;
_onCancelAbility = null;
}
public AbstractAbility Ability { get; }
public AbilitySystemComponent Owner { get; protected set; }
public int Level { get; protected set; }
public bool IsActive { get; private set; }
public int ActiveCount { get; private set; }
protected event Action<AbilityActivateResult> _onActivateResult;
protected event Action _onEndAbility;
protected event Action _onCancelAbility;
public void RegisterActivateResult(Action<AbilityActivateResult> onActivateResult)
{
_onActivateResult += onActivateResult;
}
public void UnregisterActivateResult(Action<AbilityActivateResult> onActivateResult)
{
_onActivateResult -= onActivateResult;
}
public void RegisterEndAbility(Action onEndAbility)
{
_onEndAbility += onEndAbility;
}
public void UnregisterEndAbility(Action onEndAbility)
{
_onEndAbility -= onEndAbility;
}
public void RegisterCancelAbility(Action onCancelAbility)
{
_onCancelAbility += onCancelAbility;
}
public void UnregisterCancelAbility(Action onCancelAbility)
{
_onCancelAbility -= onCancelAbility;
}
public virtual void SetLevel(int level)
{
Level = level;
}
public virtual AbilityActivateResult CanActivate()
{
if (IsActive) return AbilityActivateResult.FailHasActivated;
if (!CheckGameplayTagsValidTpActivate()) return AbilityActivateResult.FailTagRequirement;
if (!CheckCost()) return AbilityActivateResult.FailCost;
if (CheckCooldown().TimeRemaining > 0) return AbilityActivateResult.FailCooldown;
return AbilityActivateResult.Success;
}
private bool CheckGameplayTagsValidTpActivate()
{
var hasAllTags = Owner.HasAllTags(Ability.Tag.ActivationRequiredTags);
var notHasAnyTags = !Owner.HasAnyTags(Ability.Tag.ActivationBlockedTags);
var notBlockedByOtherAbility = true;
foreach (var kv in Owner.AbilityContainer.AbilitySpecs())
{
var abilitySpec = kv.Value;
if (abilitySpec.IsActive)
if (Ability.Tag.AssetTag.HasAnyTags(abilitySpec.Ability.Tag.BlockAbilitiesWithTags))
{
notBlockedByOtherAbility = false;
break;
}
}
return hasAllTags && notHasAnyTags && notBlockedByOtherAbility;
}
protected virtual bool CheckCost()
{
if (Ability.Cost == null) return true;
var costSpec = Ability.Cost.CreateSpec(Owner, Owner, Level);
if (costSpec.Value == null) return false;
if (Ability.Cost.DurationPolicy != EffectsDurationPolicy.Instant) return true;
foreach (var modifier in Ability.Cost.Modifiers)
{
// 常规来说消耗是减法, 但是加一个负数也应该被视为减法
if (modifier.Operation != GEOperation.Add && modifier.Operation != GEOperation.Minus) continue;
var costValue = modifier.CalculateMagnitude(costSpec, modifier.ModiferMagnitude);
var attributeCurrentValue =
Owner.GetAttributeCurrentValue(modifier.AttributeSetName, modifier.AttributeShortName);
if (modifier.Operation == GEOperation.Add)
if (attributeCurrentValue + costValue < 0)
return false;
if (modifier.Operation == GEOperation.Minus)
if (attributeCurrentValue - costValue < 0)
return false;
}
return true;
}
protected virtual CooldownTimer CheckCooldown()
{
return Ability.Cooldown == null
? new CooldownTimer { TimeRemaining = 0, Duration = Ability.CooldownTime }
: Owner.CheckCooldownFromTags(Ability.Cooldown.TagContainer.GrantedTags);
}
/// <summary>
/// Some skills include wind-up and follow-through, where the wind-up may be interrupted, causing the skill not to be
/// successfully released.
/// Therefore, the actual timing and logic of skill release (triggering costs and initiating cooldown) should be
/// determined by developers within the AbilitySpec,
/// rather than being systematically standardized.
/// </summary>
public void DoCost()
{
if (Ability.Cost != null) Owner.ApplyGameplayEffectToSelf(Ability.Cost);
if (Ability.Cooldown != null)
{
var cdSpec = Owner.ApplyGameplayEffectToSelf(Ability.Cooldown);
cdSpec.Value.SetDuration(Ability.CooldownTime); // Actually, it should be set by the ability's cooldown time.
}
}
public virtual bool TryActivateAbility(object arg = null, GameplayEffectSpec gameplayEffectSpec = null)
{
AbilityArgument = arg;
GameplayEffectSpec = gameplayEffectSpec;
var result = CanActivate();
var success = result == AbilityActivateResult.Success;
if (success)
{
IsActive = true;
ActiveCount++;
Owner.GameplayTagAggregator.ApplyGameplayAbilityDynamicTag(this);
ActivateAbility();
}
_onActivateResult?.Invoke(result);
return success;
}
public virtual void TryEndAbility()
{
if (!IsActive) return;
IsActive = false;
Owner.GameplayTagAggregator.RestoreGameplayAbilityDynamicTags(this);
EndAbility();
_onEndAbility?.Invoke();
}
public virtual void TryCancelAbility()
{
if (!IsActive) return;
IsActive = false;
Owner.GameplayTagAggregator.RestoreGameplayAbilityDynamicTags(this);
CancelAbility();
_onCancelAbility?.Invoke();
}
public void Tick()
{
if (IsActive)
{
AbilityTick();
}
}
protected virtual void AbilityTick()
{
}
public abstract void ActivateAbility();
public abstract void CancelAbility();
public abstract void EndAbility();
}
public abstract class AbilitySpec<T> : AbilitySpec where T : AbstractAbility
{
public T Data { get; private set; }
protected AbilitySpec(T ability, AbilitySystemComponent owner) : base(ability, owner)
{
Data = ability;
}
}
}