Advanced Attack

This is the Player's attack state which has combos and separate animations for ground, air, and while aiming upwards. It could be used by other characters, but none of the enemies have more than one attack animation and their Brains would also need to be changed to make use of combos and jumping.

Fields

The fields are all arrays of AttackTransitions, though the Ground Animations are the only group with multiple animations to perform a combo:

public sealed class AdvancedAttackState : AttackState
{
    const string TooltipPrefix = "The animation combo used when the character is";

    [SerializeField]
    [Tooltip(TooltipPrefix + "grounded")]
    private AttackTransition[] _GroundAnimations;

    [SerializeField]
    [Tooltip(TooltipPrefix + "grounded and trying to move upwards")]
    private AttackTransition[] _GroundUpAnimations;

    [SerializeField]
    [Tooltip(TooltipPrefix + "airborne")]
    private AttackTransition[] _AirAnimations;

    [SerializeField]
    [Tooltip(TooltipPrefix + "airborne and trying to move upwards")]
    private AttackTransition[] _AirUpAnimations;

Initialization

All of the animations use the same OnAnimationEnd method to handle Combos:

    private void Awake()
    {
        Action onEnd = OnAnimationEnd;

        for (int i = 0; i < _GroundAnimations.Length; i++)
            _GroundAnimations[i].Events.OnEnd += onEnd;

        for (int i = 0; i < _GroundUpAnimations.Length; i++)
            _GroundUpAnimations[i].Events.OnEnd += onEnd;

        for (int i = 0; i < _AirAnimations.Length; i++)
            _AirAnimations[i].Events.OnEnd += onEnd;

        for (int i = 0; i < _AirUpAnimations.Length; i++)
            _AirUpAnimations[i].Events.OnEnd += onEnd;

It also registers a callback to the CharacterBody2D.OnGroundedChanged event so that the attack will be cancelled if the character lands during an air attack or becomes airbourne during a ground attack:

        Character.Body.OnGroundedChanged += OnGroundedChanged;
    }
    
    private void OnGroundedChanged(bool isGrounded)
    {
        if (Character.StateMachine.CurrentState == this)
            Character.StateMachine.ForceSetDefaultState();
    }

State Entry

Entering this state resets the current attack index to start with the first animation in whichever group ends up getting played and also clears the Combo flag before playing an animation:

    private int _CurrentIndex;
    private bool _Combo;

    public override void OnEnterState()
    {
        base.OnEnterState();
        _CurrentIndex = 0;
        _Combo = false;
        PlayAnimation();
    }

To play an animation, it first clears any active Hit Boxes then determines which group of animations it should be using and plays the one at the current index:

    private void PlayAnimation()
    {
        Character.Animancer.EndHitSequence();
        var animation = CurrentAnimations[_CurrentIndex];
        Character.Animancer.Play(animation);
    }

Determining which animation group to use is simply based on the whether or not the character is currently on the ground and aiming upwards:

    private AttackTransition[] CurrentAnimations
    {
        get
        {
            if (Character.Body.IsGrounded)
            {
                if (Character.MovementDirectionY > 0.5f && _GroundUpAnimations.Length > 0)
                    return _GroundUpAnimations;
                else
                    return _GroundAnimations;
            }
            else
            {
                if (Character.MovementDirectionY > 0.5f && _AirUpAnimations.Length > 0)
                    return _AirUpAnimations;
                else
                    return _AirAnimations;
            }
        }
    }

Combos

While in this state, if the Character Brain attempts to enter it again, it sets the _Combo flag but still returns false to finish the current attack:

    public override bool CanExitState
    {
        get
        {
            if (Character.StateMachine.NextState == this)
                _Combo = true;

            return false;
        }
    }

If the _Combo flag was set when the animation ends, the current attack index is incremented and if there is still another animation remaining in the current group it is played:

    private void OnAnimationEnd()
    {
        if (_Combo)
        {
            _Combo = false;
            _CurrentIndex++;

            if (_CurrentIndex < CurrentAnimations.Length)
            {
                PlayAnimation();
                return;
            }
        }

Otherwise the character returns to their Idle state normally:

        Character.StateMachine.ForceSetDefaultState();
    }
}