AzSequence

Chaining animations with AzSequence

AzSequence describes an ordered chain of animations, like an attack's windup, strike and recovery, as a single reusable value. It is immutable, so it is safe to store in a static final field and share between entities.

Building a sequence

Chain style, where every call returns a new sequence:

AzSequence attack = AzSequence.create()
.then("attack_windup", AzPlayBehaviors.PLAY_ONCE)
.then("attack", AzPlayBehaviors.PLAY_ONCE)
.then("idle", AzPlayBehaviors.LOOP);

Builder style, which avoids intermediate copies:

private static final AzSequence EXECUTION = AzSequence.builder()
.play("execute_start")
.play("execute")
.play("execute_end")
.then("idle", AzPlayBehaviors.LOOP)
.build();
MethodBehavior
play(name)Plays once, then continues to the next stage (or stops if it is the last).
loop(name)Loops until something else is played.
hold(name)Plays once and freezes on the final frame.
then(name, behavior)Any AzPlayBehavior. An overload takes a UnaryOperator<AzAnimationStageProperties> for extra stage properties.
event(name, tick)A named event tick game ticks after the sequence starts.

loop, hold and FREEZE_ON_FRAME stages never finish, so adding a stage after one throws an IllegalStateException instead of silently creating a stage that can never play.

Playing a sequence

A sequence converts to a normal AzCommand:

EXECUTION.toCommand("base").sendForEntity(this);
// or
AzCommand.create("base", EXECUTION).sendForEntity(this);

Timed events

private static final AzSequence SLAM = AzSequence.builder()
.play("windup")
.event("damage", 12)
.play("strike")
.hold("recovery")
.build();

Event ticks are counted from the start of the sequence, not from the stage they follow; their position in the chain is only for readability. Events are not sent to the client. They are delivered by an AzSequencePlayer, which counts ticks on whichever side ticks it, so they can drive server-side logic such as damage:

private final AzSequencePlayer attackPlayer = AzSequencePlayer.forEntity(this, "base", (sequence, event) -> {
if (event.is("damage")) {
dealSlamDamage();
}
});
public void startSlam() {
attackPlayer.play(SLAM); // dispatches the animation and starts the event clock
}
@Override
public void tick() {
super.tick();
if (!level().isClientSide()) {
attackPlayer.tick();
}
}

play cancels the controller first, so replaying a sequence that is still running restarts it on the client too and keeps the events in step. If another animation interrupts the sequence (a hurt or death animation, for example), call attackPlayer.cancel() so pending events do not fire.