Minecraft Scripting API

Please note: Some examples on this page may be outdated or may not work as expected.

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:

  1. Call world.afterEvents.X.subscribe(callback) to register interest
  2. Server watches for that event
  3. When event fires, server calls your callback with details
  4. 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 it
  • afterEvents.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?

  • beforeEvents fires BEFORE everyone sees it → you can event.cancel = true to hide it
  • afterEvents fires 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.

Navigation