Skip to main content
Learn how to create sophisticated animation systems that respond to game state, player actions, and custom conditions.

Overview

Conditional animations allow you to:
  • Create state machines for entity behavior
  • Transition between animations based on conditions
  • Use predicates to control animation playback
  • Build reactive animation systems

Animation State Machine

Basic State System

import kr.toxicity.model.api.animation.AnimationModifier;
import kr.toxicity.model.api.tracker.EntityTracker;
import org.bukkit.entity.LivingEntity;

import java.util.function.BooleanSupplier;

public class AnimationStateMachine {
    
    private final EntityTracker tracker;
    private final LivingEntity entity;
    private AnimationState currentState = AnimationState.IDLE;
    
    public AnimationStateMachine(EntityTracker tracker, LivingEntity entity) {
        this.tracker = tracker;
        this.entity = entity;
        setupStateMachine();
    }
    
    private void setupStateMachine() {
        // Check state every tick
        tracker.tick((t, bundler) -> {
            AnimationState newState = determineState();
            
            if (newState != currentState) {
                transitionTo(newState);
            }
        });
    }
    
    private AnimationState determineState() {
        // Priority-based state determination
        if (entity.isDead()) return AnimationState.DEATH;
        if (entity.getHealth() < entity.getMaxHealth() * 0.3) return AnimationState.HURT;
        if (entity.getVelocity().lengthSquared() > 0.01) return AnimationState.WALKING;
        if (entity.isInWater()) return AnimationState.SWIMMING;
        
        return AnimationState.IDLE;
    }
    
    private void transitionTo(AnimationState newState) {
        // Stop current animation
        tracker.stopAnimation(currentState.getAnimation());
        
        // Play new animation
        tracker.animate(
            newState.getAnimation(),
            newState.getModifier()
        );
        
        currentState = newState;
    }
    
    public enum AnimationState {
        IDLE("idle", AnimationModifier.DEFAULT),
        WALKING("walk", AnimationModifier.DEFAULT),
        RUNNING("run", AnimationModifier.builder().speed(1.5F).build()),
        HURT("hurt", AnimationModifier.builder().speed(0.8F).build()),
        SWIMMING("swim", AnimationModifier.DEFAULT),
        DEATH("death", AnimationModifier.DEFAULT_WITH_PLAY_ONCE);
        
        private final String animation;
        private final AnimationModifier modifier;
        
        AnimationState(String animation, AnimationModifier modifier) {
            this.animation = animation;
            this.modifier = modifier;
        }
        
        public String getAnimation() { return animation; }
        public AnimationModifier getModifier() { return modifier; }
    }
}

Predicate-Based Animations

Using Animation Predicates

import kr.toxicity.model.api.animation.AnimationModifier;
import kr.toxicity.model.api.tracker.Tracker;
import org.bukkit.entity.LivingEntity;

import java.util.function.BooleanSupplier;

public class ConditionalAnimations {
    
    public void setupConditionalAnimation(Tracker tracker, LivingEntity entity) {
        // Play animation only when entity has potion effect
        BooleanSupplier hasPotionEffect = () -> 
            entity.getActivePotionEffects().size() > 0;
        
        tracker.animate("potion_effect", AnimationModifier.builder()
            .predicate(hasPotionEffect)
            .start(5)
            .end(5)
            .build()
        );
    }
    
    public void setupHealthBasedAnimation(Tracker tracker, LivingEntity entity) {
        // Different animations based on health percentage
        tracker.tick((t, bundler) -> {
            double healthPercent = entity.getHealth() / entity.getMaxHealth();
            
            if (healthPercent > 0.75) {
                // Healthy - play normal idle
                t.animate("idle");
            } else if (healthPercent > 0.5) {
                // Slightly damaged - slower animation
                t.animate("idle", AnimationModifier.builder()
                    .speed(0.9F)
                    .build()
                );
            } else if (healthPercent > 0.25) {
                // Damaged - play hurt idle
                t.animate("idle_hurt");
            } else {
                // Critical - play critical animation
                t.animate("idle_critical", AnimationModifier.builder()
                    .speed(1.2F)
                    .build()
                );
            }
        });
    }
}

Animation Layering

Multiple Animation Layers

import kr.toxicity.model.api.util.function.BonePredicate;
import kr.toxicity.model.api.animation.AnimationModifier;

public class AnimationLayers {
    
    public void setupLayeredAnimations(EntityTracker tracker, LivingEntity entity) {
        // Base layer - body animations
        tracker.animate("idle");
        
        // Upper body layer - attack animations
        tracker.tick((t, bundler) -> {
            if (entity.getAttackCooldown() == 1.0F) {
                // Play attack only on upper body bones
                t.animate("attack_upper", AnimationModifier.builder()
                    .type(AnimationIterator.Type.PLAY_ONCE)
                    .build()
                );
            }
        });
        
        // Head layer - look around
        tracker.tick(20, (t, bundler) -> {
            if (Math.random() < 0.3) {
                t.animate("look_around", AnimationModifier.builder()
                    .type(AnimationIterator.Type.PLAY_ONCE)
                    .build()
                );
            }
        });
    }
}

Per-Player Animations

Player-Specific Animation States

import kr.toxicity.model.api.animation.AnimationModifier;
import kr.toxicity.model.api.tracker.EntityTracker;
import org.bukkit.entity.Player;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class PerPlayerAnimations {
    
    private final Map<UUID, AnimationState> playerStates = new ConcurrentHashMap<>();
    
    public void setupPerPlayerAnimation(EntityTracker tracker, Player player) {
        UUID playerId = player.getUniqueId();
        
        // Different animation based on player relationship
        tracker.tick((t, bundler) -> {
            AnimationState state = determineStateForPlayer(player);
            AnimationState currentState = playerStates.get(playerId);
            
            if (state != currentState) {
                playerStates.put(playerId, state);
                
                // Play per-player animation
                t.animate(state.getAnimation(), AnimationModifier.builder()
                    .player(player)
                    .build()
                );
            }
        });
    }
    
    private AnimationState determineStateForPlayer(Player player) {
        // Example: Different animation based on player team
        if (isTeammate(player)) {
            return AnimationState.FRIENDLY;
        } else if (isEnemy(player)) {
            return AnimationState.HOSTILE;
        }
        return AnimationState.NEUTRAL;
    }
    
    private boolean isTeammate(Player player) {
        // Your team check logic
        return false;
    }
    
    private boolean isEnemy(Player player) {
        // Your enemy check logic
        return false;
    }
    
    enum AnimationState {
        FRIENDLY("wave"),
        NEUTRAL("idle"),
        HOSTILE("aggressive");
        
        private final String animation;
        
        AnimationState(String animation) {
            this.animation = animation;
        }
        
        public String getAnimation() {
            return animation;
        }
    }
}

Time-Based Animations

Day/Night Cycle Animations

import org.bukkit.World;

public class TimeBasedAnimations {
    
    public void setupDayNightAnimations(EntityTracker tracker, World world) {
        tracker.tick(20, (t, bundler) -> {
            long time = world.getTime();
            
            if (time >= 0 && time < 12000) {
                // Day - active animation
                t.animate("active");
            } else if (time >= 12000 && time < 13000) {
                // Sunset - transition
                t.animate("sleep_transition", AnimationModifier.builder()
                    .type(AnimationIterator.Type.PLAY_ONCE)
                    .build(),
                    () -> t.animate("sleeping")
                );
            } else {
                // Night - sleeping
                t.animate("sleeping");
            }
        });
    }
}

Combat State Animations

Advanced Combat System

import kr.toxicity.model.api.animation.AnimationModifier;
import kr.toxicity.model.api.tracker.EntityTracker;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class CombatAnimations {
    
    private final Map<UUID, CombatState> combatStates = new HashMap<>();
    private final Map<UUID, Long> lastDamageTime = new HashMap<>();
    
    public void setupCombatAnimations(EntityTracker tracker, LivingEntity entity) {
        tracker.tick((t, bundler) -> {
            UUID entityId = entity.getUniqueId();
            long currentTime = System.currentTimeMillis();
            
            // Check if entity was recently damaged
            Long lastDamage = lastDamageTime.get(entityId);
            boolean inCombat = lastDamage != null && 
                (currentTime - lastDamage) < 5000; // 5 second combat timer
            
            CombatState state = determineComba tState(entity, inCombat);
            CombatState currentState = combatStates.get(entityId);
            
            if (state != currentState) {
                combatStates.put(entityId, state);
                playCombatAnimation(t, state);
            }
        });
    }
    
    private CombatState determineCombatState(LivingEntity entity, boolean inCombat) {
        if (!inCombat) return CombatState.IDLE;
        
        double healthPercent = entity.getHealth() / entity.getMaxHealth();
        
        if (healthPercent < 0.3) {
            return CombatState.DEFENSIVE;
        } else if (entity.getAttackCooldown() > 0.8F) {
            return CombatState.ATTACKING;
        } else {
            return CombatState.COMBAT_READY;
        }
    }
    
    private void playCombatAnimation(EntityTracker tracker, CombatState state) {
        switch (state) {
            case IDLE -> tracker.animate("idle");
            case COMBAT_READY -> tracker.animate("combat_idle");
            case ATTACKING -> tracker.animate("attack", AnimationModifier.builder()
                .type(AnimationIterator.Type.PLAY_ONCE)
                .build()
            );
            case DEFENSIVE -> tracker.animate("defend");
        }
    }
    
    public void onEntityDamaged(UUID entityId) {
        lastDamageTime.put(entityId, System.currentTimeMillis());
    }
    
    enum CombatState {
        IDLE,
        COMBAT_READY,
        ATTACKING,
        DEFENSIVE
    }
}

Best Practices

  • Use state machines for complex animation logic
  • Implement smooth transitions between animations with lerp times
  • Cache animation states to avoid redundant updates
  • Use predicates for animations that should play conditionally
  • Combine multiple conditions with priority-based logic
  • Avoid changing animations every tick - use throttling
  • Don’t create circular animation dependencies
  • Test state transitions thoroughly
  • Monitor performance with complex state systems

Next Steps

Custom Events

Trigger animations from custom events

Dynamic Boss

Apply state machines to boss entities

Performance Optimization

Optimize animation systems

API Reference

AnimationModifier API documentation

Build docs developers (and LLMs) love