Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/toxicity188/BetterModel/llms.txt

Use this file to discover all available pages before exploring further.

Build epic boss encounters with custom 3D models, dynamic animations, phase transitions, and interactive mechanics.

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

Build docs developers (and LLMs) love