Skip to main content
Triggers define when HUDs and popups should appear or update. BetterHud provides a powerful event-based trigger system that works across all platforms.

How Triggers Work

Triggers listen to game events and fire update events that refresh HUD elements and popups:
# In a popup configuration
triggers:
  1:
    class: entity_attack # Trigger name
When the trigger fires, BetterHud:
  1. Creates an UpdateEvent with context
  2. Evaluates placeholders with the event context
  3. Updates the display for affected players

Built-in Triggers

Bukkit Triggers

Bukkit platform provides these built-in triggers:
  • entity_attack - Player attacks an entity
  • entity_damage - Player takes damage from an entity
  • player_join - Player joins the server
  • player_quit - Player leaves the server
  • player_death - Player dies
  • player_respawn - Player respawns
  • block_break - Player breaks a block
  • block_place - Player places a block
  • player_interact - Player interacts with something

Fabric Triggers

Fabric platform provides these built-in triggers:
  • entity_attack - Player attacks an entity
  • entity_damage - Player takes damage
  • entity_kill - Player kills an entity
  • player_death - Player dies

Universal Triggers

These triggers work on all platforms:
  • health - Player health changes
  • food - Player food level changes
  • armor - Player armor value changes
  • air - Player air level changes
  • level - Player level changes

Configuring Triggers

Basic Trigger

my_popup:
  triggers:
    1:
      class: entity_attack
  duration: 20 # Ticks to display
  layouts:
    # ... layout configuration

Multiple Triggers

my_popup:
  triggers:
    1:
      class: entity_attack
    2:
      class: entity_damage
    3:
      class: player_death
  # Popup shows on any of these events

Creating Custom Triggers

Bukkit Custom Trigger

Create a trigger that listens to any Bukkit event:
import kr.toxicity.hud.api.bukkit.trigger.HudBukkitEventTrigger;
import kr.toxicity.hud.api.update.UpdateEvent;
import kr.toxicity.hud.api.BetterHudAPI;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

public class InteractTrigger implements HudBukkitEventTrigger<PlayerInteractEvent>, Listener {
    
    private BiConsumer<UUID, UpdateEvent> eventConsumer;
    
    @Override
    public @NotNull Class<PlayerInteractEvent> getEventClass() {
        return PlayerInteractEvent.class;
    }
    
    @Override
    public @NotNull Object getKey(PlayerInteractEvent event) {
        return event.getPlayer().getUniqueId();
    }
    
    @Override
    public void registerEvent(@NotNull BiConsumer<UUID, UpdateEvent> eventConsumer) {
        this.eventConsumer = eventConsumer;
    }
    
    @EventHandler
    public void onInteract(PlayerInteractEvent event) {
        // Create update event from Bukkit event
        UpdateEvent updateEvent = new UpdateEvent() {
            @Override
            public @NotNull UpdateReason getType() {
                return UpdateReason.EMPTY; // Or create custom reason
            }
            
            @Override
            public @NotNull Object getKey() {
                return event.getPlayer().getUniqueId();
            }
        };
        
        eventConsumer.accept(event.getPlayer().getUniqueId(), updateEvent);
    }
}
Register the trigger:
import org.bukkit.plugin.java.JavaPlugin;

public class MyPlugin extends JavaPlugin {
    @Override
    public void onEnable() {
        InteractTrigger trigger = new InteractTrigger();
        getServer().getPluginManager().registerEvents(trigger, this);
        
        BetterHudAPI.inst().getTriggerManager().addTrigger(
            "player_interact",
            trigger
        );
    }
}

Fabric Custom Trigger

Create a trigger using Fabric’s event system:
import kr.toxicity.hud.api.fabric.trigger.HudFabricEventTrigger;
import kr.toxicity.hud.api.fabric.event.EventRegistry;
import kr.toxicity.hud.api.fabric.event.entity.PlayerAttackEntityEvent;
import kr.toxicity.hud.api.update.UpdateEvent;
import kr.toxicity.hud.api.BetterHudAPI;

public class FabricAttackTrigger implements HudFabricEventTrigger<PlayerAttackEntityEvent> {
    
    @Override
    public @NotNull EventRegistry<PlayerAttackEntityEvent> registry() {
        return PlayerAttackEntityEvent.REGISTRY;
    }
    
    @Override
    public @NotNull Object getKey(PlayerAttackEntityEvent event) {
        return event.player().getUUID();
    }
    
    @Override
    public void registerEvent(@NotNull BiConsumer<UUID, UpdateEvent> eventConsumer) {
        PlayerAttackEntityEvent.REGISTRY.register(event -> {
            UpdateEvent updateEvent = createUpdateEvent(event);
            eventConsumer.accept(event.player().getUUID(), updateEvent);
        });
    }
    
    private UpdateEvent createUpdateEvent(PlayerAttackEntityEvent event) {
        return new UpdateEvent() {
            @Override
            public @NotNull UpdateReason getType() {
                return UpdateReason.EMPTY;
            }
            
            @Override
            public @NotNull Object getKey() {
                return event.player().getUUID();
            }
        };
    }
}
Register the trigger:
import net.fabricmc.api.ModInitializer;

public class MyMod implements ModInitializer {
    @Override
    public void onInitialize() {
        BetterHudAPI.inst().getTriggerManager().addTrigger(
            "fabric_attack",
            new FabricAttackTrigger()
        );
    }
}

Update Events

UpdateEvent Interface

Update events provide context for placeholder evaluation:
public interface UpdateEvent {
    @NotNull UpdateReason getType();
    @NotNull Object getKey();
    default @NotNull UpdateEvent source() {
        return this;
    }
}

Update Reasons

Update reasons categorize why an update occurred:
public enum UpdateReason {
    EMPTY,           // No specific reason
    ENTITY_ATTACK,   // Entity attack
    ENTITY_DAMAGE,   // Entity damage
    PLAYER_JOIN,     // Player joined
    PLAYER_QUIT,     // Player quit
    HEALTH_CHANGE,   // Health changed
    FOOD_CHANGE,     // Food changed
    // ... and more
}

Custom Update Events

Create custom update events with additional data:
import kr.toxicity.hud.api.update.UpdateEvent;
import kr.toxicity.hud.api.update.UpdateReason;

public class CustomDamageEvent implements UpdateEvent {
    private final UUID playerUuid;
    private final double damage;
    private final String damageType;
    
    public CustomDamageEvent(UUID playerUuid, double damage, String damageType) {
        this.playerUuid = playerUuid;
        this.damage = damage;
        this.damageType = damageType;
    }
    
    @Override
    public @NotNull UpdateReason getType() {
        return UpdateReason.ENTITY_DAMAGE;
    }
    
    @Override
    public @NotNull Object getKey() {
        return playerUuid;
    }
    
    public double getDamage() {
        return damage;
    }
    
    public String getDamageType() {
        return damageType;
    }
}
Use in placeholders:
HudPlaceholder.<Number>builder()
    .function((args, event) -> {
        if (event instanceof CustomDamageEvent damageEvent) {
            return player -> damageEvent.getDamage();
        }
        return player -> 0.0;
    })
    .build()
    .add("last_damage", numberContainer);

Trigger Context

Event Variables

Popups can access event variables:
entity_damage_popup:
  triggers:
    1:
      class: entity_damage
  layouts:
    1:
      name: damage_text
  # Variables from event context
  variables:
    damage_amount: "<event_damage>"
    attacker_name: "<event_attacker>"

Key Mapping

Control how popups are grouped by key:
my_popup:
  key-mapping: true # Same key = replace existing popup
  triggers:
    1:
      class: entity_attack
With key-mapping: false, multiple popups can stack.

Trigger Timing

Update Intervals

Control how often triggers check for updates:
# config.yml
tick-speed: 1 # Check every tick (20 times per second)
Set how long popups stay visible after triggering:
my_popup:
  duration: 40 # Ticks (2 seconds)
  triggers:
    1:
      class: entity_attack

Always Check Condition

Keep checking conditions even after the popup is shown:
my_popup:
  always-check-condition: true
  condition: "<boolean:is_in_combat>"
  triggers:
    1:
      class: entity_attack

Advanced Trigger Patterns

Combo System

Track consecutive triggers:
public class ComboTrigger implements HudBukkitEventTrigger<EntityDamageByEntityEvent> {
    private final Map<UUID, Integer> combos = new HashMap<>();
    private final Map<UUID, Long> lastHit = new HashMap<>();
    
    @EventHandler
    public void onHit(EntityDamageByEntityEvent event) {
        if (!(event.getDamager() instanceof Player player)) return;
        
        UUID uuid = player.getUniqueId();
        long now = System.currentTimeMillis();
        long last = lastHit.getOrDefault(uuid, 0L);
        
        if (now - last > 3000) {
            combos.put(uuid, 1);
        } else {
            combos.put(uuid, combos.getOrDefault(uuid, 0) + 1);
        }
        
        lastHit.put(uuid, now);
        
        // Fire trigger with combo count
        UpdateEvent updateEvent = new ComboUpdateEvent(uuid, combos.get(uuid));
        eventConsumer.accept(uuid, updateEvent);
    }
}

Cooldown Trigger

Prevent trigger spam:
public class CooldownTrigger implements HudBukkitEventTrigger<PlayerInteractEvent> {
    private final Map<UUID, Long> cooldowns = new HashMap<>();
    private final long cooldownMs = 1000; // 1 second
    
    @EventHandler
    public void onInteract(PlayerInteractEvent event) {
        UUID uuid = event.getPlayer().getUniqueId();
        long now = System.currentTimeMillis();
        long lastTrigger = cooldowns.getOrDefault(uuid, 0L);
        
        if (now - lastTrigger < cooldownMs) {
            return; // On cooldown
        }
        
        cooldowns.put(uuid, now);
        UpdateEvent updateEvent = createUpdateEvent(event);
        eventConsumer.accept(uuid, updateEvent);
    }
}

Best Practices

Performance

  1. Use specific triggers instead of checking every tick
  2. Avoid expensive operations in trigger handlers
  3. Clean up data structures (remove old entries)
  4. Use appropriate update intervals

Organization

  1. Group related triggers in dedicated classes
  2. Use descriptive trigger names
  3. Document custom trigger behavior
  4. Share triggers between popups when possible

Testing

  1. Test triggers with debug logging enabled
  2. Verify trigger keys are unique when needed
  3. Check trigger timing and cooldowns
  4. Test with multiple players

Examples

Entity Health Popup

entity_health_popup:
  triggers:
    1:
      class: entity_attack
  duration: 60 # 3 seconds
  push: true # Stack multiple popups
  key-mapping: true # One per entity
  layouts:
    1:
      name: entity_health_bar

Kill Feed

kill_feed:
  triggers:
    1:
      class: entity_kill
  duration: 100 # 5 seconds  
  push: true # Stack recent kills
  layouts:
    1:
      name: kill_notification

Health Warning

low_health_warning:
  triggers:
    1:
      class: health # Updates on health change
  always-check-condition: true
  condition: "<boolean:is_low_health>"
  layouts:
    1:
      name: warning_indicator

Next Steps

Placeholders

Use placeholders in triggers

Conditions

Add conditional logic

Animations

Animate triggered popups

Bukkit Platform

Bukkit-specific triggers

Build docs developers (and LLMs) love