using System; namespace GAS.Runtime { public abstract class AbilitySpec { /// /// 获取激活能力时传递给能力的参数。 /// 在技能过程中不应该修改, 考虑使用UserData /// /// /// 旧版本为 object[] 类型, 有内存分配和装箱/拆箱问题, 应自行创建一个数据结构(推荐record)来传参. /// public object AbilityArgument { get; private set; } /// /// 仅限GrantedAbility, 激活能力时传递给能力的效果规格。 /// 可以通过给gameplayEffectSpec添加自定义数据(UserData)来传递更多信息。 /// public EntityRef GameplayEffectSpec { get; private set; } /// /// 获取或设置与能力关联的自定义数据。 /// /// /// 此属性用于存储能力的自定义信息,以便在能力的不同任务之间共享数据。 /// 例如,可以在一个技能的任务(AbilityTask)中设置此数据,然后在同一个技能的另一个任务(AbilityTask)中检索和使用该数据。 /// 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 _onActivateResult; protected event Action _onEndAbility; protected event Action _onCancelAbility; public void RegisterActivateResult(Action onActivateResult) { _onActivateResult += onActivateResult; } public void UnregisterActivateResult(Action 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); } /// /// 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. /// 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 : AbilitySpec where T : AbstractAbility { public T Data { get; private set; } protected AbilitySpec(T ability, AbilitySystemComponent owner) : base(ability, owner) { Data = ability; } } }