PaviWeb DocsMinecraft Bedrock
Utilidades

GUIA PRACTICA

Desarrollo de addons para Minecraft Bedrock

Patrones para construir behavior packs, sistemas de eventos y minijuegos mantenibles con JavaScript. Los ejemplos fueron redactados desde cero a partir de las tecnicas recurrentes encontradas en los packs locales.

635suscripciones a eventos
1,467tareas programadas
604usos de formularios
409usos de scoreboard

Basico

Estructura del behavior pack

Organiza el contenido para que Minecraft encuentre cada recurso y el proyecto pueda crecer.

mi_addon/ ├── manifest.json ├── pack_icon.png ├── scripts/ │ ├── main.js │ ├── config.js │ └── systems/ ├── entities/ ├── items/ ├── blocks/ ├── functions/ └── structures/
  • Usa main.js solo para iniciar modulos y suscripciones globales.
  • Separa cada minijuego o sistema en su propio archivo.
  • Centraliza IDs, tags y objetivos en config.js.
  • No incluyas backups ni archivos de desarrollo en el pack publicado.
Basico

Manifest y dependencias

Declara el modulo de datos, el punto de entrada JavaScript y las APIs necesarias.

{
  "format_version": 2,
  "header": {
    "name": "Mi Addon",
    "description": "Sistema de ejemplo",
    "uuid": "UUID-UNICO-DEL-HEADER",
    "version": [1, 0, 0],
    "min_engine_version": [1, 21, 0]
  },
  "modules": [
    {
      "type": "data",
      "uuid": "UUID-UNICO-DATA",
      "version": [1, 0, 0]
    },
    {
      "type": "script",
      "language": "javascript",
      "entry": "scripts/main.js",
      "uuid": "UUID-UNICO-SCRIPT",
      "version": [1, 0, 0]
    }
  ],
  "dependencies": [
    { "module_name": "@minecraft/server", "version": "2.0.0" },
    { "module_name": "@minecraft/server-ui", "version": "2.0.0" }
  ]
}
Basico

Punto de entrada del script

Mantiene el arranque legible y evita registrar dos veces el mismo evento.

import { world } from "@minecraft/server";
import { registerAdminMenu } from "./systems/admin-menu.js";
import { registerGameEvents } from "./systems/game-events.js";

let initialized = false;

function initialize() {
  if (initialized) return;
  initialized = true;
  registerAdminMenu();
  registerGameEvents();
  console.warn("[Mi Addon] Sistemas iniciados.");
}

world.afterEvents.worldLoad.subscribe(initialize);

Si tu version no expone worldLoad, inicia los modulos al importarlos y conserva una bandera para impedir registros duplicados durante pruebas.

Intermedio

Organizacion modular

Divide responsabilidades sin convertir cada funcion en un archivo distinto.

Modulos recomendados

  • config.js: IDs y valores configurables.
  • state.js: estado temporal.
  • storage.js: datos persistentes.
  • permissions.js: roles.
  • game.js: reglas principales.

Evita

  • Un main.js con miles de lineas.
  • Tags escritos diferente en cada archivo.
  • Importaciones circulares.
  • Estados globales duplicados.
Basico

Eventos del mundo

Reacciona a acciones reales y reduce la necesidad de comprobar todo en cada tick.

import { world } from "@minecraft/server";

world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => {
  if (!initialSpawn) return;
  player.sendMessage("Bienvenido al evento.");
});

world.afterEvents.itemUse.subscribe(({ source, itemStack }) => {
  if (itemStack.typeId !== "pavi:menu") return;
  openMainMenu(source);
});

world.afterEvents.entityDie.subscribe(({ deadEntity, damageSource }) => {
  if (deadEntity.typeId !== "minecraft:player") return;
  registerDeath(deadEntity, damageSource.damagingEntity);
});

En los packs analizados, itemUse, entityHitEntity, playerSpawn y entityDie son los eventos mas frecuentes.

Basico

Temporizadores y ticks

Minecraft intenta ejecutar 20 ticks por segundo. Programa pensando en ese presupuesto.

import { system } from "@minecraft/server";

const countdownId = system.runInterval(() => {
  game.secondsLeft -= 1;
  updateTimerHud(game.secondsLeft);

  if (game.secondsLeft <= 0) {
    system.clearRun(countdownId);
    finishRound();
  }
}, 20);

system.runTimeout(startRound, 60); // 3 segundos
  • Guarda el ID retornado para cancelar intervalos.
  • No abras un intervalo nuevo por cada jugador.
  • Reparte el trabajo cuando proceses listas grandes.
Basico

Jugadores y entidades

Filtra desde la consulta para evitar recorrer entidades innecesarias.

import { world } from "@minecraft/server";

const overworld = world.getDimension("overworld");
const activePlayers = world.getPlayers({ tags: ["event:playing"] });
const markers = overworld.getEntities({
  type: "pavi:zone_marker",
  location: { x: 0, y: 64, z: 0 },
  maxDistance: 40
});

for (const player of activePlayers) {
  if (!player.isValid) continue;
  player.onScreenDisplay.setActionBar("Partida activa");
}
Basico

Tags y roles

Funcionan bien para estados visibles y temporales, pero no deben ser la unica seguridad.

const ROLE_TAGS = {
  owner: "role:owner",
  admin: "role:admin",
  player: "role:player"
};

function hasRole(player, role) {
  return player.hasTag(ROLE_TAGS[role]);
}

function canOpenAdminMenu(player) {
  return hasRole(player, "owner") || hasRole(player, "admin");
}
Intermedio

Scoreboards

Ideales para puntuaciones visibles, monedas y valores consultados por comandos.

import { world } from "@minecraft/server";

function getObjective(id, displayName = id) {
  return world.scoreboard.getObjective(id)
    ?? world.scoreboard.addObjective(id, displayName);
}

function setCoins(player, amount) {
  getObjective("mikucoins", "MikuCoins")
    .setScore(player, Math.max(0, Math.floor(amount)));
}

function getCoins(player) {
  return getObjective("mikucoins").getScore(player) ?? 0;
}
Intermedio

Propiedades dinamicas

Guarda configuraciones y estado persistente que no necesita mostrarse en un scoreboard.

import { world } from "@minecraft/server";

const KEY = "pavi:event_state";

export function saveEventState(state) {
  world.setDynamicProperty(KEY, JSON.stringify(state));
}

export function loadEventState() {
  const raw = world.getDynamicProperty(KEY);
  if (typeof raw !== "string") return null;
  try {
    return JSON.parse(raw);
  } catch {
    return null;
  }
}
  • Versiona el formato si puede cambiar con futuras actualizaciones.
  • No escribas el mismo JSON cada tick.
  • Valida tipos y valores al cargar.
Intermedio

Comandos y dimensiones

Usa la API directa cuando exista y reserva comandos para operaciones sin equivalente claro.

import { world } from "@minecraft/server";

const nether = world.getDimension("nether");

function movePlayer(player) {
  player.teleport(
    { x: 12.5, y: 72, z: -8.5 },
    { dimension: nether, facingLocation: { x: 12.5, y: 72, z: 0 } }
  );
}

function clearNearbyItems(dimension) {
  try {
    dimension.runCommand("kill @e[type=item,r=12]");
  } catch (error) {
    console.warn(`No se pudieron limpiar items: ${error}`);
  }
}
Intermedio

Maquina de estados para minijuegos

Evita booleanos contradictorios durante lobby, cuenta regresiva y partida.

const Phase = Object.freeze({
  LOBBY: "lobby",
  COUNTDOWN: "countdown",
  PLAYING: "playing",
  RESULTS: "results"
});

const game = { phase: Phase.LOBBY, round: 0, players: new Set() };

function transitionTo(nextPhase) {
  const allowed = {
    [Phase.LOBBY]: [Phase.COUNTDOWN],
    [Phase.COUNTDOWN]: [Phase.PLAYING, Phase.LOBBY],
    [Phase.PLAYING]: [Phase.RESULTS],
    [Phase.RESULTS]: [Phase.LOBBY]
  };
  if (!allowed[game.phase].includes(nextPhase)) return false;
  game.phase = nextPhase;
  return true;
}
Basico

Formularios con server-ui

Construye menus claros y vuelve a validar permisos despues de la respuesta.

import { ActionFormData } from "@minecraft/server-ui";

async function openAdminMenu(player) {
  if (!canOpenAdminMenu(player)) return;

  const response = await new ActionFormData()
    .title("Panel del evento")
    .body("Selecciona una accion.")
    .button("Iniciar partida")
    .button("Cancelar partida")
    .show(player);

  if (response.canceled || !canOpenAdminMenu(player)) return;
  if (response.selection === 0) startGame();
  if (response.selection === 1) cancelGame();
}
Intermedio

Inventarios e items

Comprueba que el componente y el contenedor existan antes de leer slots.

function countItem(player, typeId) {
  const inventory = player.getComponent("minecraft:inventory");
  const container = inventory?.container;
  if (!container) return 0;

  let total = 0;
  for (let slot = 0; slot < container.size; slot += 1) {
    const item = container.getItem(slot);
    if (item?.typeId === typeId) total += item.amount;
  }
  return total;
}
Intermedio

Combate, muerte y vidas

Separa la muerte de Minecraft de la eliminacion definitiva del evento.

world.afterEvents.entityDie.subscribe(({ deadEntity, damageSource }) => {
  if (deadEntity.typeId !== "minecraft:player") return;

  const remaining = Math.max(0, getLives(deadEntity) - 1);
  setLives(deadEntity, remaining);

  const killer = damageSource.damagingEntity;
  if (killer?.typeId === "minecraft:player") addKill(killer);

  if (remaining === 0) eliminatePlayer(deadEntity);
  else scheduleRespawn(deadEntity, remaining);
});
  • Registra el ultimo atacante con expiracion para asistencias.
  • No otorgues dos kills desde eventos diferentes.
  • Haz idempotente la eliminacion.
Intermedio

Zonas y deteccion

Comprueba volumenes simples y dispara acciones solo al entrar o salir.

function isInsideBox(location, min, max) {
  return location.x >= min.x && location.x <= max.x
    && location.y >= min.y && location.y <= max.y
    && location.z >= min.z && location.z <= max.z;
}

const inside = new Set();

function updateZone(players, zone) {
  for (const player of players) {
    const nowInside = isInsideBox(player.location, zone.min, zone.max);
    const wasInside = inside.has(player.id);
    if (nowInside && !wasInside) onZoneEnter(player);
    if (!nowInside && wasInside) onZoneLeave(player);
    nowInside ? inside.add(player.id) : inside.delete(player.id);
  }
}
Basico

Sonidos y particulas

Usa efectos como respuesta a acciones concretas y limita su alcance.

function playRoundStart(player) {
  player.playSound("pavi.round_start", {
    location: player.location,
    volume: 0.8,
    pitch: 1
  });

  player.dimension.spawnParticle("pavi:round_flash", {
    x: player.location.x,
    y: player.location.y + 1,
    z: player.location.z
  });
}
Avanzado

Componentes personalizados

Conecta items o bloques JSON con funciones JavaScript reutilizables.

import { system } from "@minecraft/server";

system.beforeEvents.startup.subscribe(({ itemComponentRegistry }) => {
  itemComponentRegistry.registerCustomComponent("pavi:open_menu", {
    onUse({ source }) {
      if (source?.typeId !== "minecraft:player") return;
      openMainMenu(source);
    }
  });
});
{
  "minecraft:item": {
    "description": { "identifier": "pavi:menu_item" },
    "components": { "pavi:open_menu": {} }
  }
}
Intermedio

Script events

Comunica funciones, command blocks y scripts mediante IDs con namespace.

system.afterEvents.scriptEventReceive.subscribe((event) => {
  if (event.id !== "pavi:event") return;

  const [action, value = ""] = event.message.trim().split(/\s+/, 2);
  if (action === "start") startGame();
  if (action === "round") setRound(Number(value));
}, { namespaces: ["pavi"] });

Valida el origen y los argumentos. No conviertas un mensaje recibido directamente en un comando.

Avanzado

ServerNet y servicios web

Sincroniza datos desde Bedrock Dedicated Server con una API controlada.

import {
  http, HttpHeader, HttpRequest, HttpRequestMethod
} from "@minecraft/server-net";

async function sendSnapshot(payload, token) {
  const request = new HttpRequest("https://api.example.com/event/live");
  request.method = HttpRequestMethod.Post;
  request.headers = [
    new HttpHeader("content-type", "application/json"),
    new HttpHeader("authorization", `Bearer ${token}`)
  ];
  request.body = JSON.stringify(payload);
  request.timeout = 10;

  const response = await http.request(request);
  if (response.status < 200 || response.status >= 300) {
    throw new Error(`HTTP ${response.status}`);
  }
}
  • Nunca incluyas la service_role de Supabase en el pack.
  • Usa un token limitado y rotatorio.
  • Envia cambios agrupados, no una solicitud por jugador cada tick.
  • Implementa timeout y reintentos limitados.
Avanzado

Rendimiento y watchdog

Reduce el trabajo por tick y evita ciclos o transformaciones costosas.

Usa eventos

No recorras jugadores si ya existe un evento.

Baja la frecuencia

HUD y zonas suelen funcionar cada 5, 10 o 20 ticks.

Procesa por lotes

Divide listas grandes entre varios ticks.

Evita duplicados

Registra y limpia intervalos una sola vez.

const queue = [];

system.runInterval(() => {
  for (let index = 0; index < 10 && queue.length; index += 1) {
    const task = queue.shift();
    try {
      task();
    } catch (error) {
      console.warn(`[Queue] ${error}`);
    }
  }
}, 1);
Avanzado

Seguridad y permisos

La ofuscacion retrasa la lectura; la proteccion real depende de validaciones consistentes.

  1. Valida el rol al mostrar el menu y al ejecutar cada accion.
  2. Centraliza permisos para evitar reglas contradictorias.
  3. No confies solo en tags agregables manualmente.
  4. No almacenes secretos administrativos en el cliente.
  5. Comprueba rangos, IDs y estado en toda entrada externa.
  6. Registra acciones administrativas importantes.
function assertGameAction(player, action) {
  if (!player?.isValid) return false;
  if (!game.players.has(player.id)) return false;
  if (game.phase !== "playing") return false;
  if (game.turnPlayerId !== player.id) return false;
  if (!ALLOWED_ACTIONS.has(action)) return false;
  return true;
}
Intermedio

Depuracion y publicacion

Prueba sistemas por separado y deja mensajes utiles sin revelar datos sensibles.

function safeRun(label, callback) {
  try {
    return callback();
  } catch (error) {
    console.warn(`[${label}] ${error?.stack ?? error}`);
    return undefined;
  }
}