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
