using System; using System.Collections.Generic; using EntityStates; using HG; using JetBrains.Annotations; using RoR2.Skills; using UnityEngine; namespace RoR2; [RequireComponent(typeof(CharacterBody))] public sealed class GenericSkill : MonoBehaviour, ILifeBehavior, IManagedMonoBehaviour { public class SkillOverride : IEquatable { public readonly object source; public readonly SkillDef skillDef; public readonly SkillOverridePriority priority; public int stock; public float rechargeStopwatch; public SkillOverride(object source, SkillDef skillDef, SkillOverridePriority priority) { this.source = source; this.skillDef = skillDef; this.priority = priority; stock = 0; rechargeStopwatch = 0f; } public bool Equals(SkillOverride other) { if (object.Equals(source, other.source) && object.Equals(skillDef, other.skillDef)) { return priority == other.priority; } return false; } public override bool Equals(object obj) { if (obj is SkillOverride other) { return Equals(other); } return false; } public override int GetHashCode() { return (((((source != null) ? source.GetHashCode() : 0) * 397) ^ ((skillDef != null) ? skillDef.GetHashCode() : 0)) * 397) ^ (int)priority; } } public enum SkillOverridePriority { Default, Loadout, Upgrade, Replacement, Contextual, Network } public class SkillOverrideHandle : IDisposable { public readonly object source; public readonly SkillDef skill; public readonly GenericSkill skillSlot; public readonly SkillOverridePriority priority; public void Dispose() { } } public delegate void StateMachineResolver(GenericSkill genericSkill, SkillDef skillDef, ref EntityStateMachine targetStateMachine); [SerializeField] private SkillFamily _skillFamily; public string skillName; public bool hideInCharacterSelect; private static readonly List stateMachineLookupBuffer = new List(); private SkillOverride[] skillOverrides = Array.Empty(); private int currentSkillOverride = -1; private int bonusStockFromBody; public int baseStock; private float finalRechargeInterval; private float _cooldownScale = 1f; private float _flatCooldownReduction = 1f; private float _temporaryCooldownPenalty; public float baseRechargeStopwatch; [HideInInspector] public bool hasExecutedSuccessfully; public SkillDef skillDef { get; private set; } public SkillFamily skillFamily => _skillFamily; public SkillDef baseSkill { get; private set; } public string skillNameToken => skillDef.GetCurrentNameToken(this); public string skillDescriptionToken => skillDef.GetCurrentDescriptionToken(this); public float baseRechargeInterval => skillDef.GetRechargeInterval(this); public int rechargeStock => skillDef.GetRechargeStock(this); public bool beginSkillCooldownOnSkillEnd => skillDef.beginSkillCooldownOnSkillEnd; public SerializableEntityStateType activationState => skillDef.activationState; public InterruptPriority interruptPriority => skillDef.interruptPriority; public bool isCombatSkill => skillDef.isCombatSkill; public bool mustKeyPress => skillDef.mustKeyPress; public Sprite icon => skillDef.GetCurrentIcon(this); [CanBeNull] public EntityStateMachine stateMachine { get; private set; } [CanBeNull] public SkillDef.BaseSkillInstanceData skillInstanceData { get; set; } public CharacterBody characterBody { get; private set; } public SkillDef defaultSkillDef { get; private set; } public int maxStock { get; private set; } = 1; public int stock { get { if (currentSkillOverride >= 0 && currentSkillOverride < skillOverrides.Length) { return skillOverrides[currentSkillOverride].stock; } return baseStock; } set { if (currentSkillOverride >= 0 && currentSkillOverride < skillOverrides.Length) { skillOverrides[currentSkillOverride].stock = value; } else { baseStock = value; } } } public float cooldownScale { get { return _cooldownScale; } set { if (_cooldownScale != value) { _cooldownScale = value; RecalculateFinalRechargeInterval(); } } } public float flatCooldownReduction { get { return _flatCooldownReduction; } set { if (_flatCooldownReduction != value) { _flatCooldownReduction = value; RecalculateFinalRechargeInterval(); } } } public float temporaryCooldownPenalty { get { return _temporaryCooldownPenalty; } set { if (_temporaryCooldownPenalty != value) { _temporaryCooldownPenalty = value; RecalculateFinalRechargeInterval(); } } } public float rechargeStopwatch { get { if (currentSkillOverride >= 0 && currentSkillOverride < skillOverrides.Length) { return skillOverrides[currentSkillOverride].rechargeStopwatch; } return baseRechargeStopwatch; } set { if (currentSkillOverride >= 0 && currentSkillOverride < skillOverrides.Length) { skillOverrides[currentSkillOverride].rechargeStopwatch = value; } else { baseRechargeStopwatch = value; } } } public float cooldownRemaining { get { if (stock != maxStock && rechargeStock != 0) { return finalRechargeInterval - rechargeStopwatch; } return 0f; } } public event Action onSkillChanged; private event StateMachineResolver _customStateMachineResolver; public event StateMachineResolver customStateMachineResolver { add { _customStateMachineResolver += value; PickTargetStateMachine(); } remove { _customStateMachineResolver -= value; PickTargetStateMachine(); } } private int FindSkillOverrideIndex(ref SkillOverride skillOverride) { for (int i = 0; i < skillOverrides.Length; i++) { if (skillOverrides[i].Equals(skillOverride)) { return i; } } return -1; } public void SetSkillOverride(object source, SkillDef skillDef, SkillOverridePriority priority) { SkillOverride skillOverride = new SkillOverride(source, skillDef, priority); if (FindSkillOverrideIndex(ref skillOverride) == -1) { ArrayUtils.ArrayAppend(ref skillOverrides, in skillOverride); PickCurrentOverride(); } } public void UnsetSkillOverride(object source, SkillDef skillDef, SkillOverridePriority priority) { SkillOverride skillOverride = new SkillOverride(source, skillDef, priority); int num = FindSkillOverrideIndex(ref skillOverride); if (num != -1) { ArrayUtils.ArrayRemoveAtAndResize(ref skillOverrides, num); PickCurrentOverride(); } } public bool HasSkillOverrideOfPriority(SkillOverridePriority priority) { for (int i = 0; i < skillOverrides.Length; i++) { if (priority == skillOverrides[i].priority) { return true; } } return false; } private void PickCurrentOverride() { currentSkillOverride = -1; SkillOverridePriority skillOverridePriority = SkillOverridePriority.Default; for (int i = 0; i < skillOverrides.Length; i++) { SkillOverridePriority priority = skillOverrides[i].priority; if (skillOverridePriority <= priority) { currentSkillOverride = i; skillOverridePriority = priority; } } if (currentSkillOverride == -1) { SetSkillInternal(baseSkill); } else { SetSkillInternal(skillOverrides[currentSkillOverride].skillDef); } } private void SetSkillInternal(SkillDef newSkillDef) { if (!(skillDef == newSkillDef)) { UnassignSkill(); AssignSkill(newSkillDef); this.onSkillChanged?.Invoke(this); } } public void SetBaseSkill(SkillDef newSkillDef) { baseSkill = newSkillDef; PickCurrentOverride(); } private void UnassignSkill() { if ((object)skillDef != null) { skillDef.OnUnassigned(this); skillInstanceData = null; skillDef = null; } } private void AssignSkill(SkillDef newSkillDef) { skillDef = newSkillDef; if ((object)skillDef != null) { PickTargetStateMachine(); RecalculateMaxStock(); if (skillDef.fullRestockOnAssign && stock < maxStock) { stock = maxStock; } if (skillDef.dontAllowPastMaxStocks) { stock = Mathf.Min(maxStock, stock); } skillInstanceData = skillDef.OnAssigned(this); RecalculateFinalRechargeInterval(); } } public void SetBonusStockFromBody(int newBonusStockFromBody) { bonusStockFromBody = newBonusStockFromBody; RecalculateMaxStock(); } private void RecalculateMaxStock() { if (skillDef != null) { maxStock = skillDef.GetMaxStock(this) + bonusStockFromBody; } else { maxStock = 1; } } private void Awake() { defaultSkillDef = skillFamily.defaultSkillDef; baseSkill = defaultSkillDef; characterBody = GetComponent(); AssignSkill(defaultSkillDef); } private void OnDestroy() { UnassignSkill(); } private void Start() { RecalculateMaxStock(); stock = maxStock; } private void FixedUpdate() { ManagedFixedUpdate(Time.fixedDeltaTime); } public void ManagedFixedUpdate(float deltaTime) { skillDef?.OnFixedUpdate(this, deltaTime); } public void OnDeathStart() { base.enabled = false; } public bool CanExecute() { return skillDef?.CanExecute(this) ?? false; } public bool IsReady() { return skillDef?.IsReady(this) ?? false; } public bool ExecuteIfReady() { hasExecutedSuccessfully = CanExecute(); if (hasExecutedSuccessfully) { OnExecute(); return true; } return false; } public void RunRecharge(float dt) { if (stock < maxStock) { if (!beginSkillCooldownOnSkillEnd || !(stateMachine.state.GetType() == activationState.stateType)) { rechargeStopwatch += dt; } if (skillDef != null && skillDef.attackSpeedBuffsRestockSpeed) { RecalculateFinalRechargeInterval(); } if (rechargeStopwatch >= finalRechargeInterval) { temporaryCooldownPenalty = 0f; RestockSteplike(); } } } public void Reset() { rechargeStopwatch = 0f; stock = maxStock; } public bool CanApplyAmmoPack() { if (stock < maxStock) { return true; } return false; } public void ApplyAmmoPack() { if (stock < maxStock) { stock += rechargeStock; if (stock > maxStock) { stock = maxStock; } } } public void AddOneStock() { int num = stock + 1; stock = num; rechargeStopwatch = 0f; } public void RemoveAllStocks() { stock = 0; rechargeStopwatch = 0f; } public void DeductStock(int count) { stock = Mathf.Max(0, stock - count); } private void OnExecute() { skillDef.OnExecute(this); } public void RechargeBaseSkill(float dt) { if (baseSkill == null) { return; } float rechargeInterval = baseSkill.GetRechargeInterval(this); float num = Mathf.Min(rechargeInterval, Mathf.Max(0.5f, rechargeInterval * cooldownScale - flatCooldownReduction)) + temporaryCooldownPenalty; int baseMaxStock = GetBaseMaxStock(); if (num == 0f) { baseStock = baseMaxStock; baseRechargeStopwatch = 0f; return; } baseRechargeStopwatch += dt; int num2 = baseSkill.GetRechargeStock(this); int num3 = Mathf.FloorToInt(baseRechargeStopwatch / num * (float)num2); baseStock += num3; if (baseStock >= baseMaxStock) { baseStock = baseMaxStock; baseRechargeStopwatch = 0f; } else { baseRechargeStopwatch -= (float)num3 * num; } } public int GetBaseStock() { return baseStock; } public int GetBaseMaxStock() { if (baseSkill == null) { return 1; } return baseSkill.GetMaxStock(this) + bonusStockFromBody; } private void RestockContinuous() { if (finalRechargeInterval == 0f) { stock = maxStock; rechargeStopwatch = 0f; return; } int num = Mathf.FloorToInt(rechargeStopwatch / finalRechargeInterval * (float)rechargeStock); stock += num; if (stock >= maxStock) { stock = maxStock; rechargeStopwatch = 0f; } else { rechargeStopwatch -= (float)num * finalRechargeInterval; } } public void RestockSteplike() { stock += rechargeStock; if (stock >= maxStock) { stock = maxStock; } rechargeStopwatch = 0f; } public float CalculateFinalRechargeInterval() { return Mathf.Min(baseRechargeInterval, Mathf.Max(0.5f, baseRechargeInterval * cooldownScale - flatCooldownReduction)) + temporaryCooldownPenalty; } private void RecalculateFinalRechargeInterval() { finalRechargeInterval = CalculateFinalRechargeInterval(); } public void RecalculateValues() { RecalculateMaxStock(); RecalculateFinalRechargeInterval(); } private void PickTargetStateMachine() { EntityStateMachine targetStateMachine = stateMachine; if (!string.Equals(stateMachine?.customName, skillDef.activationStateMachineName, StringComparison.Ordinal)) { targetStateMachine = EntityStateMachine.FindByCustomName(base.gameObject, skillDef.activationStateMachineName); } this._customStateMachineResolver?.Invoke(this, skillDef, ref targetStateMachine); stateMachine = targetStateMachine; } [AssetCheck(typeof(GenericSkill))] private static void CheckGenericSkillStateMachine(AssetCheckArgs args) { if (((GenericSkill)args.asset).stateMachine.customName == string.Empty) { args.LogError("Unnamed state machine.", ((GenericSkill)args.asset).gameObject); } } }