Player Events & Interactions
Player events are the core of interactive add-ons. Every action a player takes - joining, dying, chatting, moving - triggers an event that your code can listen to and respond to instantly. Events are how you build everything: custom commands, leveling systems, PvP tracking, chat filters, economy systems.
Understanding Player Events
Events are notifications from the server when something happens. Instead of constantly checking "did the player join yet?", you tell the server "call me when a player joins". This is much more efficient and makes your code event-driven rather than poll-driven.
The subscription pattern:
- Call
world.afterEvents.X.subscribe(callback)to register interest - Server watches for that event
- When event fires, server calls your callback with details
- You receive an event object with properties (player, damage amount, message text, etc.)
Why is this better than polling? Polling checks every frame (30+ times per second) whether something happened. Events only call your code when something actually happens. For a server with 50 players, polling is 50x × 30 = 1,500 wasted checks per second. Events cut this to only what matters.
Key concept: beforeEvents vs afterEvents
beforeEvents.chatSend- fires BEFORE the message appears, you can cancel itafterEvents.playerJoin- fires AFTER the event already happened, you can't prevent it but you can react
Player Join & Leave
Join fires when a player enters the world - perfect for initialization (load their data, set their properties, send welcome message). Leave fires when they disconnect - used to save data, log activity, or clean up player-specific state.
import { world } from "@minecraft/server";
// **Player Join** - called when player enters the world
// Use this to: initialize player data, load from database, send welcome message, check permissions
world.afterEvents.playerJoin.subscribe((event) => {
const player = event.player;
// Send welcome message (visible to this player only)
player.sendMessage("§6Welcome to the realm!");
// Get player's unique ID (won't change if they rename their account)
const playerId = player.id;
console.log(`Player joined: ${player.nameTag} (${playerId})`);
// This is where you'd load their data from a database:
// const playerData = playerDB.get(playerId);
// if (!playerData) playerDB.set(playerId, { level: 1, money: 0 });
});
// **Player Leave** - called when player disconnects
// Use this to: save data, log activity, clean up references, stop animations
world.afterEvents.playerLeave.subscribe((event) => {
const player = event.player;
console.log(`Player left: ${player.nameTag}`);
// Save their current state before they disconnect
// const playerData = playerDB.get(player.id);
// if (playerData) {
// playerData.lastLogout = Date.now();
// playerData.lastKnownLocation = player.location;
// playerDB.set(player.id, playerData);
// }
});
Player Death Event
Fires when a player dies. Use this for: tracking kills/deaths stats, preventing item loss, giving resurrection items, teleporting to spawn, or triggering events in PvP systems.
world.afterEvents.playerDie.subscribe((event) => {
const player = event.deadPlayer; // The player who died
const damageSource = event.damageSource; // What killed them (fall, mob, player, etc.)
// Send death message only to this player
player.sendMessage("§cYou died!");
// Log who died and why (useful for debugging or moderating)
console.log(`${player.nameTag} died from ${damageSource.cause}`);
// **Common use case: prevent item loss**
// In survival, players lose items on death. Give them back:
const item = new ItemStack("golden_apple", 1);
player.container.addItem(item);
// **Tracking deaths in a leveling system**
// const playerData = playerDB.get(player.id);
// playerData.deaths = (playerData.deaths || 0) + 1;
// playerDB.set(player.id, playerData);
// **If killed by a player, track the killer:**
// if (damageSource.damagingEntity?.typeId === "minecraft:player") {
// const killer = damageSource.damagingEntity;
// const killerData = playerDB.get(killer.id);
// killerData.kills = (killerData.kills || 0) + 1;
// playerDB.set(killer.id, killerData);
// }
});
Real-world example: A PvP arena uses playerDie to track kills/deaths, give rewards to the killer, and respawn the player at spawn.
Chat Events
Chat events fire every time a player types. This is how custom commands work - you use beforeEvents.chatSend to intercept the message BEFORE it displays, allowing you to cancel it or process it.
// **beforeEvents** fires BEFORE the message is sent to all players
// This is where you cancel/filter messages or detect commands
world.beforeEvents.chatSend.subscribe((event) => {
const player = event.sender; // Who typed the message
const message = event.message; // What they typed
// **Chat filtering** - block certain words
if (message.includes("forbidden")) {
event.cancel = true; // Prevent the message from being sent at all
player.sendMessage("§cThat word is not allowed!");
return; // Stop processing, don't check for commands
}
// **Custom commands** - intercept messages starting with !
if (message.startsWith("!help")) {
event.cancel = true; // Hide the raw command from chat
// Now send a formatted response only the player sees
player.sendMessage("§6=== Custom Commands ===");
player.sendMessage("§e!help §r- Show this message");
player.sendMessage("§e!home §r- Teleport home");
return;
}
// If we didn't cancel, the message goes to global chat normally
// This is what happens if the player typed a regular message
});
Why beforeEvents.chatSend instead of afterEvents.chatSend?
beforeEventsfires BEFORE everyone sees it → you canevent.cancel = trueto hide itafterEventsfires AFTER it's already sent → too late to hide it
So for commands and filtering, always use beforeEvents.
Equipment Changes
Detect when players equip or remove armor/items. Useful for enforcing dress codes, tracking gear, or triggering buffs when wearing specific items.
// Store last known equipment for each player
const lastEquipment = new Map();
world.afterEvents.playerSpawn.subscribe((event) => {
const player = event.player;
// Check equipment every time a player loads/spawns
const equippable = player.getComponent("equippable");
if (!equippable) return; // Entity doesn't have equipment component
// Get current equipment
const head = equippable.getEquipment(EquipmentSlot.Head);
const chest = equippable.getEquipment(EquipmentSlot.Chest);
const legs = equippable.getEquipment(EquipmentSlot.Legs);
const feet = equippable.getEquipment(EquipmentSlot.Feet);
// Get what they wore last time we checked
const lastKnown = lastEquipment.get(player.id) || {};
// **Detect head equipment changes**
if (head?.typeId !== lastKnown.head) {
player.sendMessage(`§aHead equipment changed to ${head?.typeId || "none"}`);
}
// **Detect full armor sets** (useful for checking if player has diamond armor)
const hasFullDiamond = head?.typeId === "diamond_helmet" &&
chest?.typeId === "diamond_chestplate" &&
legs?.typeId === "diamond_leggings" &&
feet?.typeId === "diamond_boots";
if (hasFullDiamond && !lastKnown.fullDiamond) {
player.sendMessage("§bYou have equipped full Diamond armor!");
// Could apply a buff effect, unlock achievement, etc.
}
// Store current equipment as the "last known" for next time
lastEquipment.set(player.id, { head: head?.typeId, chest: chest?.typeId, legs: legs?.typeId, feet: feet?.typeId, fullDiamond: hasFullDiamond });
});
Player Damage
Fires every time any entity takes damage. Filter for players, then use the damage info to track hits, modify damage, or trigger effects.
world.afterEvents.entityHurt.subscribe((event) => {
const entity = event.hurtEntity; // Thing that got hurt (could be player, mob, anything)
const damage = event.damage; // How much damage (10 = 5 hearts)
const damageSource = event.damageSource;
// **Filter for players only** - we don't care about mobs getting hurt
if (entity.typeId !== "minecraft:player") return;
const player = entity;
const cause = damageSource.cause; // What caused damage: "fall", "fire", "explosion", etc.
// Send player a message about damage
player.sendMessage(`§cYou took ${damage} damage from ${cause}`);
// **Track damage in a leveling system**
// const playerData = playerDB.get(player.id);
// playerData.totalDamageTaken = (playerData.totalDamageTaken || 0) + damage;
// playerDB.set(player.id, playerData);
// **If hit by another player, notify them**
// if (damageSource.damagingEntity?.typeId === "minecraft:player") {
// const attacker = damageSource.damagingEntity;
// attacker.sendMessage(`§cYou hit ${player.nameTag} for ${damage} damage`);
//
// // Track PvP stats
// const attackerData = playerDB.get(attacker.id);
// attackerData.damageDealt = (attackerData.damageDealt || 0) + damage;
// playerDB.set(attacker.id, attackerData);
// }
// **Prevent specific damage types** (e.g., prevent fall damage in a parkour arena)
// if (cause === "fall") {
// event.damage = 0; // Reduce damage to 0 (doesn't prevent death, just reduces damage)
// }
});
Health & Hunger Monitoring
Check health/hunger every tick to detect changes, give warnings, or apply effects. Note: entityTick fires 20 times per second for every entity, so this is expensive - use cooldowns.
// Track when we last warned each player (prevent spam)
const lastWarning = new Map();
world.afterEvents.entityTick.subscribe((event) => {
// **Filter for players only** - we don't want to check every mob
if (event.entity.typeId !== "minecraft:player") return;
const player = event.entity;
const health = player.getComponent("health");
if (!health) return; // Entity doesn't have health (shouldn't happen for players, but be safe)
// **Monitor low health** - warn player when they're low
// health.currentValue is 0-20 (1 health = 0.5 hearts)
// 4 health = 2 hearts = low and dangerous
if (health.currentValue <= 4) {
// Don't spam warnings every tick - add a cooldown
const now = Date.now();
const lastWarnTime = lastWarning.get(player.id) || 0;
if (now - lastWarnTime > 2000) { // Only warn once every 2 seconds
player.sendMessage("§cLow health warning!");
lastWarning.set(player.id, now);
}
}
// **Monitor hunger** (for survival mode)
// const hunger = player.getComponent("hunger");
// if (hunger?.currentValue <= 2) {
// player.sendMessage("§eYou are starving!");
// }
// **Apply effects when health is critically low**
// if (health.currentValue <= 1) {
// // Apply blindness effect so player can't see as they're dying
// player.addEffect(EntityEffectType.Blindness, 20, { amplifier: 0 });
// }
});
Performance tip: entityTick runs 20 times per second on EVERY entity. If you're checking all players here, that's 20 × player_count checks per second. For most cases, use a slower interval instead of entityTick.