Overview
This example demonstrates:- Multi-phase boss system with state transitions
- Health-based animation changes
- Custom hitbox interactions
- Skill system with animations
- Boss bar integration
Boss Entity Structure
Core Boss Class
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.animation.AnimationModifier;
import kr.toxicity.model.api.entity.BaseEntity;
import kr.toxicity.model.api.tracker.EntityTracker;
import kr.toxicity.model.api.tracker.TrackerUpdateAction;
import kr.toxicity.model.api.event.hitbox.HitBoxInteractEvent;
import kr.toxicity.model.api.nms.HitBoxListener;
import kr.toxicity.model.api.util.function.BonePredicate;
import org.bukkit.Location;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Zombie;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class DynamicBoss {
private final String bossId;
private final EntityTracker tracker;
private final LivingEntity entity;
private final BossBar bossBar;
private BossPhase currentPhase = BossPhase.PHASE_1;
private final Map<UUID, Long> lastHitTime = new ConcurrentHashMap<>();
private final Set<UUID> playersInRange = ConcurrentHashMap.newKeySet();
private double maxHealth;
private boolean isEnraged = false;
public DynamicBoss(String bossId, Location location, String modelName) {
this.bossId = bossId;
// Spawn entity
this.entity = location.getWorld().spawn(location, Zombie.class, zombie -> {
zombie.setCustomName("§c§lDemon Lord");
zombie.setCustomNameVisible(false);
zombie.setHealth(500);
zombie.getAttribute(org.bukkit.attribute.Attribute.GENERIC_MAX_HEALTH).setBaseValue(500);
});
this.maxHealth = entity.getMaxHealth();
// Create boss bar
this.bossBar = org.bukkit.Bukkit.createBossBar(
"§c§lDemon Lord",
BarColor.RED,
BarStyle.SEGMENTED_10
);
// Create model
this.tracker = BetterModel.model(modelName)
.map(renderer -> renderer.getOrCreate(BaseEntity.of(entity)))
.orElse(null);
if (tracker != null) {
setupBoss();
}
}
private void setupBoss() {
// Setup hitbox interactions
tracker.createHitBox(
BaseEntity.of(entity),
HitBoxListener.builder()
.interact(this::handleHit)
.build(),
BonePredicate.name("body").or(BonePredicate.name("head"))
);
// Start phase 1 animation
enterPhase(BossPhase.PHASE_1);
// Health monitoring
tracker.tick(5, (t, bundler) -> {
updateHealth();
checkPhaseTransition();
updateBossBar();
});
// Range detection for players
tracker.tick(10, (t, bundler) -> {
updatePlayersInRange();
});
// Periodic skills
scheduleSkills();
}
private void handleHit(HitBoxInteractEvent event) {
Player player = event.player().player();
if (player == null) return;
UUID playerId = player.getUniqueId();
long currentTime = System.currentTimeMillis();
// Cooldown check (500ms)
Long lastHit = lastHitTime.get(playerId);
if (lastHit != null && currentTime - lastHit < 500) return;
lastHitTime.put(playerId, currentTime);
// Apply damage
entity.damage(10, player);
// Play hit reaction
if (Math.random() < 0.3) { // 30% chance
playSkill(BossSkill.COUNTER_ATTACK);
}
// Damage animation
tracker.damageTint();
}
private void updateHealth() {
double healthPercent = entity.getHealth() / maxHealth;
// Check for enrage at 25% health
if (healthPercent <= 0.25 && !isEnraged) {
enterEnrage();
}
}
private void checkPhaseTransition() {
double healthPercent = entity.getHealth() / maxHealth;
BossPhase newPhase = null;
if (healthPercent <= 0.3 && currentPhase != BossPhase.PHASE_3) {
newPhase = BossPhase.PHASE_3;
} else if (healthPercent <= 0.6 && currentPhase == BossPhase.PHASE_1) {
newPhase = BossPhase.PHASE_2;
}
if (newPhase != null) {
enterPhase(newPhase);
}
}
private void enterPhase(BossPhase phase) {
if (currentPhase == phase) return;
currentPhase = phase;
// Stop current animations
tracker.stopAnimation("idle");
tracker.stopAnimation("walk");
// Play phase transition
tracker.animate("phase_transition", AnimationModifier.builder()
.type(AnimationIterator.Type.PLAY_ONCE)
.build(),
() -> {
// Start phase idle
tracker.animate("phase_" + phase.getId() + "_idle");
}
);
// Visual effects based on phase
switch (phase) {
case PHASE_1 -> {
tracker.update(TrackerUpdateAction.tint(0xFFFFFF));
bossBar.setColor(BarColor.RED);
}
case PHASE_2 -> {
tracker.update(TrackerUpdateAction.composite(
TrackerUpdateAction.tint(0xFF8800),
TrackerUpdateAction.glow(true),
TrackerUpdateAction.glowColor(0xFF4400)
));
bossBar.setColor(BarColor.YELLOW);
}
case PHASE_3 -> {
tracker.update(TrackerUpdateAction.composite(
TrackerUpdateAction.tint(0xFF0000),
TrackerUpdateAction.glow(true),
TrackerUpdateAction.glowColor(0xFF0000),
TrackerUpdateAction.brightness(15, 15)
));
bossBar.setColor(BarColor.RED);
}
}
// Announce phase change
playersInRange.forEach(uuid -> {
Player player = org.bukkit.Bukkit.getPlayer(uuid);
if (player != null) {
player.sendMessage("§c§lDemon Lord entered " + phase.getName() + "!");
}
});
}
private void enterEnrage() {
isEnraged = true;
tracker.animate("enrage", AnimationModifier.builder()
.type(AnimationIterator.Type.PLAY_ONCE)
.build(),
() -> {
// Faster animations in enraged state
tracker.animate(currentPhase.getIdleAnimation(), AnimationModifier.builder()
.speed(1.5F)
.build()
);
}
);
// Red pulsing effect
tracker.update(TrackerUpdateAction.composite(
TrackerUpdateAction.tint(0xFF0000),
TrackerUpdateAction.glow(true),
TrackerUpdateAction.glowColor(0xFF0000)
));
bossBar.setColor(BarColor.RED);
bossBar.setTitle("§c§lDemon Lord §f[§4ENRAGED§f]");
}
private void scheduleSkills() {
// Fireball every 5 seconds
tracker.tick(100, (t, bundler) -> {
if (currentPhase.ordinal() >= BossPhase.PHASE_2.ordinal()) {
playSkill(BossSkill.FIREBALL);
}
});
// Ground slam every 8 seconds
tracker.tick(160, (t, bundler) -> {
playSkill(BossSkill.GROUND_SLAM);
});
// Summon minions in phase 3
tracker.tick(300, (t, bundler) -> {
if (currentPhase == BossPhase.PHASE_3) {
playSkill(BossSkill.SUMMON_MINIONS);
}
});
}
private void playSkill(BossSkill skill) {
tracker.animate(skill.getAnimation(), AnimationModifier.builder()
.type(AnimationIterator.Type.PLAY_ONCE)
.speed(isEnraged ? 1.5F : 1.0F)
.build(),
() -> {
// Return to idle
tracker.animate(currentPhase.getIdleAnimation());
}
);
// Execute skill logic
skill.execute(this);
}
private void updatePlayersInRange() {
playersInRange.clear();
entity.getWorld().getNearbyPlayers(entity.getLocation(), 50)
.forEach(player -> {
playersInRange.add(player.getUniqueId());
bossBar.addPlayer(player);
});
// Remove players out of range
new HashSet<>(bossBar.getPlayers()).forEach(player -> {
if (!playersInRange.contains(player.getUniqueId())) {
bossBar.removePlayer(player);
}
});
}
private void updateBossBar() {
double progress = entity.getHealth() / maxHealth;
bossBar.setProgress(Math.max(0, Math.min(1, progress)));
}
public void remove() {
if (tracker != null) {
tracker.close();
}
bossBar.removeAll();
entity.remove();
}
public EntityTracker getTracker() {
return tracker;
}
public LivingEntity getEntity() {
return entity;
}
public enum BossPhase {
PHASE_1(1, "Phase 1", "phase_1_idle"),
PHASE_2(2, "Phase 2", "phase_2_idle"),
PHASE_3(3, "Phase 3", "phase_3_idle");
private final int id;
private final String name;
private final String idleAnimation;
BossPhase(int id, String name, String idleAnimation) {
this.id = id;
this.name = name;
this.idleAnimation = idleAnimation;
}
public int getId() { return id; }
public String getName() { return name; }
public String getIdleAnimation() { return idleAnimation; }
}
public enum BossSkill {
FIREBALL("skill_fireball"),
GROUND_SLAM("skill_slam"),
COUNTER_ATTACK("skill_counter"),
SUMMON_MINIONS("skill_summon");
private final String animation;
BossSkill(String animation) {
this.animation = animation;
}
public String getAnimation() {
return animation;
}
public void execute(DynamicBoss boss) {
// Implement skill logic here
switch (this) {
case FIREBALL -> boss.executeFireball();
case GROUND_SLAM -> boss.executeGroundSlam();
case COUNTER_ATTACK -> boss.executeCounter();
case SUMMON_MINIONS -> boss.executeSummon();
}
}
}
private void executeFireball() {
// Launch fireball projectile
}
private void executeGroundSlam() {
// AoE damage around boss
entity.getWorld().getNearbyPlayers(entity.getLocation(), 5)
.forEach(player -> {
player.damage(15);
player.setVelocity(player.getLocation().toVector()
.subtract(entity.getLocation().toVector())
.normalize()
.multiply(1.5)
.setY(0.5));
});
}
private void executeCounter() {
// Quick counter attack
}
private void executeSummon() {
// Spawn minion entities
}
}
Boss Manager
Managing Multiple Bosses
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class BossManager {
private final Map<String, DynamicBoss> bosses = new ConcurrentHashMap<>();
public DynamicBoss spawnBoss(String id, Location location, String modelName) {
// Remove existing boss with same ID
removeBoss(id);
DynamicBoss boss = new DynamicBoss(id, location, modelName);
bosses.put(id, boss);
return boss;
}
public void removeBoss(String id) {
DynamicBoss boss = bosses.remove(id);
if (boss != null) {
boss.remove();
}
}
public DynamicBoss getBoss(String id) {
return bosses.get(id);
}
public void removeAllBosses() {
bosses.values().forEach(DynamicBoss::remove);
bosses.clear();
}
}
Best Practices
- Use phase-based animations to keep boss fights dynamic
- Implement cooldowns on hitbox interactions to prevent spam
- Update boss bars regularly for real-time health feedback
- Use TrackerUpdateAction.composite() for complex visual effects
- Cache player references for performance
- Always remove boss bars when boss is defeated
- Close trackers properly to prevent memory leaks
- Test phase transitions thoroughly
- Limit the number of simultaneous skill animations
Next Steps
Custom Events
Learn more about hitbox events
Multi-part Entities
Create bosses with multiple body parts
Conditional Animations
Advanced animation state machines
Performance Optimization
Optimize boss performance
