Skip to main content
Learn how to add cosmetic models to players, including hats, wings, pets, and other accessories.

Overview

Player cosmetics allow you to:
  • Attach custom models to players
  • Create wearable accessories (hats, wings, capes)
  • Build companion pets that follow players
  • Add particle effects and animations
  • Support multiple cosmetics simultaneously

Basic Cosmetic System

Cosmetic Manager

import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.entity.BaseEntity;
import kr.toxicity.model.api.profile.ModelProfile;
import kr.toxicity.model.api.tracker.PlayerTracker;
import kr.toxicity.model.api.tracker.TrackerModifier;
import org.bukkit.entity.Player;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class CosmeticManager {
    
    private final Map<UUID, Map<CosmeticSlot, PlayerTracker>> playerCosmetics = new ConcurrentHashMap<>();
    
    public void equipCosmetic(Player player, CosmeticSlot slot, String modelName) {
        // Remove existing cosmetic in slot
        unequipCosmetic(player, slot);
        
        BetterModel.limb(modelName).ifPresent(renderer -> {
            PlayerTracker tracker = (PlayerTracker) renderer.create(
                BaseEntity.of(player),
                ModelProfile.of(player),
                TrackerModifier.builder()
                    .sightTrace(false)
                    .damageAnimation(false)
                    .damageTint(false)
                    .build()
            );
            
            // Store tracker
            playerCosmetics
                .computeIfAbsent(player.getUniqueId(), k -> new ConcurrentHashMap<>())
                .put(slot, tracker);
            
            // Start idle animation
            tracker.animate("idle");
        });
    }
    
    public void unequipCosmetic(Player player, CosmeticSlot slot) {
        Map<CosmeticSlot, PlayerTracker> cosmetics = playerCosmetics.get(player.getUniqueId());
        if (cosmetics == null) return;
        
        PlayerTracker tracker = cosmetics.remove(slot);
        if (tracker != null) {
            tracker.close();
        }
    }
    
    public void removeAllCosmetics(Player player) {
        Map<CosmeticSlot, PlayerTracker> cosmetics = playerCosmetics.remove(player.getUniqueId());
        if (cosmetics != null) {
            cosmetics.values().forEach(PlayerTracker::close);
        }
    }
    
    public Map<CosmeticSlot, PlayerTracker> getCosmetics(Player player) {
        return playerCosmetics.getOrDefault(player.getUniqueId(), Collections.emptyMap());
    }
    
    public enum CosmeticSlot {
        HAT,
        BACK,      // Wings, capes
        SHOULDER,  // Parrots, pets
        AURA,      // Particle effects
        COMPANION  // Following pets
    }
}

Animated Wings

Wings with Movement Animation

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

public class AnimatedWings {
    
    public void equipWings(Player player, CosmeticManager manager) {
        manager.equipCosmetic(player, CosmeticManager.CosmeticSlot.BACK, "angel_wings");
        
        PlayerTracker tracker = manager.getCosmetics(player)
            .get(CosmeticManager.CosmeticSlot.BACK);
        
        if (tracker != null) {
            setupWingAnimations(tracker, player);
        }
    }
    
    private void setupWingAnimations(PlayerTracker tracker, Player player) {
        // Idle wings
        tracker.animate("wings_idle");
        
        // Flap when flying
        tracker.tick(5, (t, bundler) -> {
            if (player.isGliding()) {
                t.stopAnimation("wings_idle");
                t.animate("wings_flap", AnimationModifier.builder()
                    .speed(1.5F)
                    .build()
                );
            } else if (!t.renderer().animations().containsKey("wings_idle")) {
                t.stopAnimation("wings_flap");
                t.animate("wings_idle");
            }
        });
    }
}

Companion Pets

Pet That Follows Player

import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.tracker.DummyTracker;
import kr.toxicity.model.api.tracker.ModelRotation;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;

public class CompanionPet {
    
    private final DummyTracker tracker;
    private final Player owner;
    private Location targetLocation;
    
    public CompanionPet(Player owner, String modelName) {
        this.owner = owner;
        this.targetLocation = owner.getLocation();
        
        this.tracker = BetterModel.model(modelName)
            .map(renderer -> renderer.create(owner.getLocation()))
            .orElse(null);
        
        if (tracker != null) {
            setupPetBehavior();
        }
    }
    
    private void setupPetBehavior() {
        // Start idle animation
        tracker.animate("pet_idle");
        
        // Follow player
        tracker.tick(2, (t, bundler) -> {
            Location ownerLoc = owner.getLocation();
            Location currentLoc = tracker.location().toBukkit();
            
            // Calculate offset behind player
            Vector direction = ownerLoc.getDirection().multiply(-1).normalize();
            targetLocation = ownerLoc.clone()
                .add(direction.multiply(2))
                .add(0, 0.5, 0);
            
            double distance = currentLoc.distance(targetLocation);
            
            // Move if too far
            if (distance > 0.5) {
                // Calculate movement
                Vector movement = targetLocation.toVector()
                    .subtract(currentLoc.toVector())
                    .normalize()
                    .multiply(Math.min(distance * 0.3, 0.5));
                
                Location newLoc = currentLoc.add(movement);
                
                // Update rotation to face movement direction
                float yaw = (float) Math.toDegrees(
                    Math.atan2(-movement.getX(), movement.getZ())
                );
                
                tracker.rotation(() -> new ModelRotation(0, yaw));
                tracker.location(newLoc);
                
                // Play walk animation if moving fast
                if (distance > 2) {
                    tracker.stopAnimation("pet_idle");
                    tracker.animate("pet_walk");
                }
            } else {
                // Play idle when close
                tracker.stopAnimation("pet_walk");
                tracker.animate("pet_idle");
            }
        });
    }
    
    public void remove() {
        if (tracker != null) {
            tracker.close();
        }
    }
    
    public DummyTracker getTracker() {
        return tracker;
    }
}

Cosmetic Menu System

GUI for Equipping Cosmetics

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;

import java.util.Arrays;

public class CosmeticMenu {
    
    private final CosmeticManager manager;
    
    public CosmeticMenu(CosmeticManager manager) {
        this.manager = manager;
    }
    
    public void openMenu(Player player) {
        Inventory inv = Bukkit.createInventory(null, 27, "§6Cosmetics");
        
        // Hat slot
        inv.setItem(10, createCosmeticItem(
            Material.GOLDEN_HELMET,
            "§6Crown",
            "golden_crown",
            CosmeticManager.CosmeticSlot.HAT
        ));
        
        // Wings slot
        inv.setItem(12, createCosmeticItem(
            Material.ELYTRA,
            "§fAngel Wings",
            "angel_wings",
            CosmeticManager.CosmeticSlot.BACK
        ));
        
        // Pet slot
        inv.setItem(14, createCosmeticItem(
            Material.BONE,
            "§7Ghost Pet",
            "ghost_pet",
            CosmeticManager.CosmeticSlot.COMPANION
        ));
        
        player.openInventory(inv);
    }
    
    private ItemStack createCosmeticItem(Material material, String name, 
                                         String modelName, CosmeticManager.CosmeticSlot slot) {
        ItemStack item = new ItemStack(material);
        ItemMeta meta = item.getItemMeta();
        
        meta.setDisplayName(name);
        meta.setLore(Arrays.asList(
            "§7Click to equip",
            "§8Model: " + modelName
        ));
        
        item.setItemMeta(meta);
        return item;
    }
    
    public void handleClick(Player player, ItemStack item) {
        if (item == null || !item.hasItemMeta()) return;
        
        ItemMeta meta = item.getItemMeta();
        if (meta.getLore() == null || meta.getLore().size() < 2) return;
        
        String modelLine = meta.getLore().get(1);
        String modelName = modelLine.replace("§8Model: ", "");
        
        // Determine slot based on item type
        CosmeticManager.CosmeticSlot slot = switch (item.getType()) {
            case GOLDEN_HELMET -> CosmeticManager.CosmeticSlot.HAT;
            case ELYTRA -> CosmeticManager.CosmeticSlot.BACK;
            case BONE -> CosmeticManager.CosmeticSlot.COMPANION;
            default -> null;
        };
        
        if (slot != null) {
            manager.equipCosmetic(player, slot, modelName);
            player.sendMessage("§aEquipped: " + meta.getDisplayName());
        }
    }
}

Best Practices

  • Use player models (limbs) for cosmetics attached to players
  • Disable damage animations and tints for cosmetic trackers
  • Use DummyTracker for companion pets that move independently
  • Cache cosmetic trackers for easy management
  • Update cosmetic animations based on player state
  • Always remove cosmetics when players disconnect
  • Limit the number of simultaneous cosmetics per player
  • Test cosmetic compatibility with player skins
  • Validate model existence before equipping

Next Steps

Custom Armor

Learn about custom armor models

Conditional Animations

State-based animation systems

Performance Optimization

Optimize cosmetic rendering

API Reference

Tracker API documentation

Build docs developers (and LLMs) love