Saltar al contenido principal

Scripts Lua: ejemplos y patrones

Esta página contiene ejemplos de scripts de elementos decorativos de FreeMinecraftModels con explicaciones. Proporciona las animaciones y el contenido referenciados y verifica cada ejemplo en tu servidor antes de usarlo en un mapa.

Si estás empezando con los scripts, comienza con Primeros pasos. Para los detalles completos de la API, consulta la API de elementos decorativos.


Ejemplo: elemento decorativo invulnerable​

Lo que enseña este ejemplo: El script útil más sencillo: cancelar el daño predeterminado del clic izquierdo.

Guarda este script como invulnerable.lua en la carpeta de scripts y recarga antes de asignarlo a un elemento.

Archivo de script completo (clic para expandir)
return {
api_version = 1,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}

Explicación paso a paso​

  1. Elección del evento -- on_left_click se activa cuando un jugador golpea el elemento con clic izquierdo. El evento Lua es la marca de cancelación de FMM para el daño predeterminado del elemento.

  2. Verificación del evento -- context.event siempre debería estar presente en este hook, pero la verificación es una buena práctica.

  3. Cancelación -- context.event.cancel() impide que FMM aplique el daño predeterminado de ese clic. No establece invulnerabilidad general frente a todas las fuentes de daño.

Uso​

Agrega lo siguiente a la configuración .yml de tu prop:

isEnabled: true
scripts:
- invulnerable.lua

Ejemplo: puerta interactiva​

Lo que enseña este ejemplo: Alternar estado con clic derecho, reproducir y detener animaciones, y usar context.state para rastrear si la puerta está abierta o cerrada.

Archivo de script completo (clic para expandir)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"

return {
api_version = 1,

on_spawn = function(context)
context.state.is_open = false
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local loc = context.prop.current_location

if context.state.is_open then
-- Close the door
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false

if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Open the door
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true

if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}

Explicación paso a paso​

  1. Constantes a nivel de archivo -- OPEN_ANIMATION y CLOSE_ANIMATION se definen encima de la tabla de retorno. Esto facilita cambiarlas para distintos archivos de modelo que puedan usar nombres de animación diferentes.

  2. Inicialización del estado -- on_spawn establece context.state.is_open = false. El estado persiste entre los hooks de esta asignación de script a este elemento. Los demás scripts del mismo elemento tienen su propio estado.

  3. Protección frente a clics -- on_left_click cancela el daño predeterminado de ese clic.

  4. Lógica de alternancia -- on_right_click solicita detener la animación y reproducir la correspondiente, cambia la marca y emite un sonido. La marca registra el estado solicitado, no la reproducción confirmada. blend = true pone la animación en cola; detenerla no anula una animación de muerte activa del motor compartido. Consulta Animaciones. El ejemplo anima el modelo, pero no cambia la colisión de las barreras ni garantiza que la abertura sea transitable.

  5. Los sonidos aceptan nombres enum de Bukkit sin distinguir mayúsculas o claves válidas como minecraft:block.note_block.harp y claves de packs personalizados. El cliente debe disponer del sonido en su pack de recursos.

Nombres de animación

Los nombres como "open" y "close" deben coincidir con el modelo. Una animación inexistente devuelve false; este ejemplo sigue cambiando la marca y emitiendo el sonido. Comprueba el resultado si esos efectos deben depender de la aceptación y recuerda que true no demuestra una reproducción visible.


Ejemplo: activador de proximidad con partículas​

Lo que enseña este ejemplo: Creación de zonas, observación de zonas para eventos de entrada/salida, efectos de partículas y limpieza.

Archivo de script completo (clic para expandir)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle bursts

return {
api_version = 1,

on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0

local loc = context.prop.current_location
if loc == nil then return end

-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle

-- Pass placeholder functions to enable on_zone_enter and on_zone_leave.
-- The actual logic lives in the hooks below.
context.zones:watch(handle, function() end, function() end)

-- Start a repeating particle effect at the zone boundary
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end

-- Spawn particles in a ring at the zone boundary
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS

if (tick_context.state.players_in_zone or 0) > 0 then
-- Flame particles when players are inside
tick_context.world:spawn_particle("FLAME", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_zone_enter = function(context)
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,

on_zone_leave = function(context)
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end,

on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Explicación paso a paso​

  1. Constantes -- ZONE_RADIUS y PARTICLE_INTERVAL están a nivel de archivo para facilitar el ajuste.

  2. Inicialización del estado -- on_spawn configura todos los campos de estado a nil / 0 antes de hacer cualquier otra cosa.

  3. Creación de la zona -- context.zones:create_sphere() crea una esfera en la posición inicial del elemento. El identificador numérico referencia esa zona fija. El anillo de partículas consulta la posición actual, así que usa un elemento inmóvil o actualiza la zona cuando lo muevas.

  4. Observación de la zona -- context.zones:watch(handle, function() end, function() end) habilita los hooks on_zone_enter y on_zone_leave. Esos hooks incrementan y decrementan un contador almacenado en context.state; también reciben al jugador que cruza como context.player / context.event.player si quieres un comportamiento específico por jugador.

  5. Bucle de partículas -- Una tarea genera el anillo cada 10 ticks del servidor, medio segundo a 20 TPS. El tipo de partícula cambia según haya jugadores en la zona.

  6. Limpieza -- on_destroy cancela la tarea repetitiva y deja de observar la zona. Aunque ambas se limpian automáticamente cuando se elimina el prop, la limpieza explícita es una buena práctica.

Rendimiento de las partículas

Generar muchas partículas en cada tick puede afectar al rendimiento. Usa un intervalo razonable (10-20 ticks) y mantén bajos los conteos de partículas. El ejemplo de arriba usa PARTICLE_INTERVAL = 10 (dos veces por segundo) con solo 12 partículas por anillo.


Ejemplo: elemento decorativo que emite sonido​

Lo que enseña este ejemplo: Reproducir sonidos al interactuar, usar context.cooldowns y prevenir interacciones en ráfaga.

Archivo de script completo (clic para expandir)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds

return {
api_version = 1,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Prevent spam
if not context.cooldowns:check_local("sound", COOLDOWN_TICKS) then
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)

-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
end
}

Explicación paso a paso​

  1. Tiempo de reutilización -- context.cooldowns:check_local("sound", COOLDOWN_TICKS) comprueba y, si está listo, inicia el tiempo de espera inmediatamente. Lo comparten todos los jugadores que usan esta asignación de script en el elemento. Cada tick de este tiempo representa 50 ms transcurridos; los retrasos del programador cuentan ticks reales del servidor.

  2. Los sonidos aceptan nombres enum de Bukkit sin distinguir mayúsculas o claves válidas como minecraft:block.note_block.harp y claves de packs personalizados. El cliente debe disponer del sonido en su pack de recursos.

  3. Retroalimentación de partículas -- Las partículas de nota aparecen sobre el prop cuando se reproduce el sonido, dando una señal visual.

  4. Protección frente a clics -- on_left_click cancela el daño predeterminado de ese clic.

Implementación del cooldown

Usa context.cooldowns para cooldowns normales. Reserva context.state más scheduler:run_later() para casos en los que necesites cambios de estado personalizados al final del retardo.


Ejemplo: elemento ambiental animado​

Lo que enseña este ejemplo: Iniciar una animación en bucle al generar, con un emisor de partículas basado en ticks.

Archivo de script completo (clic para expandir)
return {
api_version = 1,

on_spawn = function(context)
-- Start the idle animation immediately, looping
context.prop:play_animation("idle", false, true)

-- Emit ambient particles every 40 ticks (2 seconds)
context.state.ambient_task = context.scheduler:run_repeating(0, 40, function(tick_context)
local loc = tick_context.prop.current_location
if loc == nil then return end

tick_context.world:spawn_particle(
"ENCHANT",
loc.x, loc.y + 1, loc.z,
10, 0.5, 0.5, 0.5, 0.05
)
end)
end,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
if context.state.ambient_task then
context.scheduler:cancel(context.state.ambient_task)
end
end
}

Explicación paso a paso​

  1. Inicio automático de animación -- on_spawn reproduce inmediatamente "idle" en bucle. El valor false de blend cambia de estado inmediatamente. El estado integrado idle se repite independientemente del argumento loop suministrado.

  2. Partículas ambientales -- Una tarea repetitiva genera partículas de mesa de encantamientos sobre el prop cada 2 segundos, creando un efecto ambiental mágico.

  3. Limpieza -- on_destroy cancela la tarea de partículas.


Ejemplo: silla para sentarse​

Lo que enseña este ejemplo: Montar a un jugador en un prop con clic derecho, desmontar con clic izquierdo y usar context.event.player para obtener al jugador que interactúa.

Archivo de script completo (clic para expandir)
return {
api_version = 1,

on_right_click = function(context)
-- Handle seating here instead of also using the default mount fallback
if context.event then
context.event.cancel()
end

local player = context.event and context.event.player
if not player then return end

-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end

-- Mount the player on the chair
context.prop:mount(player)

local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,

on_left_click = function(context)
-- Cancel default left-click damage
if context.event then
context.event.cancel()
end

local player = context.event and context.event.player
if not player then return end

-- Check if the player is sitting and dismount them
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)

local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}

Explicación paso a paso​

  1. Clic derecho para sentarse -- on_right_click obtiene al jugador de context.event.player, comprueba si ya es pasajero para evitar montarlo dos veces y llama a context.prop:mount(player) para solicitar el primer asiento m_ disponible.

  2. Clic izquierdo para levantarse -- on_left_click cancela el daño predeterminado de ese clic y comprueba si el jugador que golpea es pasajero. Si lo es, context.prop:dismount(player) lo baja del asiento.

  3. Comprobación de pasajeros -- context.prop:get_passengers() devuelve un array de tablas de entidad. Comparamos los UUID para encontrar al jugador que interactúa en la lista.

  4. Sonidos -- Acompañan las solicitudes en cola de sentarse y levantarse, no cambios de asiento confirmados. El hook de clic derecho cancela la alternativa de montaje predeterminada de FMM para que no intente sentar también al jugador. El modelo necesita un punto m_ libre.


Ejemplo: santuario de bendiciones​

Lo que enseña este ejemplo: Comprobar el objeto que sostiene el jugador, consumir objetos, aplicar efectos de poción aleatorios, gestión de cooldown y retroalimentación de partículas/sonido.

Archivo de script completo (clic para expandir)
local COOLDOWN_TICKS = 600  -- 30 seconds between uses

local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end

-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end

-- Consume one gold ingot
player:consume_held_item(1)

-- Play blessing effects
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end

-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")

-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Explicación paso a paso​

  1. Comprobación del objeto -- player:get_held_item() devuelve una tabla con type, amount y display_name para el objeto de la mano principal (o nil si está vacía). held.type es el nombre del material Bukkit en mayúsculas, así que lo comparamos con "GOLD_INGOT".

  2. Consumo del objeto -- player:consume_held_item(1) pone en cola la retirada de la mano principal. Lee esa mano al ejecutarse la tarea y no devuelve un resultado de pago. Esta demostración no es un intercambio atómico: cambiar de objeto en la mano puede alterar lo que se consume, mientras la bendición se aplica inmediatamente.

  3. Mejora aleatoria -- La tabla BLESSINGS a nivel de archivo lista los efectos positivos disponibles. math.random(#BLESSINGS) elige uno al azar. player:add_potion_effect(effect, duration, amplifier) lo aplica -- 600 ticks son 30 segundos, el amplificador 1 es nivel II.

  4. Tiempo de reutilización -- La marca compartida por esta asignación de script se restablece tras 600 ticks del servidor, 30 segundos a 20 TPS. Todos los jugadores de este santuario la comparten. Para medir tiempo transcurrido, usa context.cooldowns como en el ejemplo del elemento que emite sonido.

  5. Retroalimentación -- Las partículas de encantamiento y de aldeano feliz más un sonido de activación de baliza crean una sensación de "bendición divina".


Ejemplo: santuario maldito​

Lo que enseña este ejemplo: Efectos de poción negativos, rayos, generación de entidades y lógica de ramificación basada en las ofrendas del jugador.

Archivo de script completo (clic para expandir)
local COOLDOWN_TICKS = 600  -- 30 seconds between uses

local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}

local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}

local ZOMBIE_COUNT = 4

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")

-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end

-- Spawn a horde of zombies around the shrine
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end

-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")

-- Ominous effects
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold offered -- reward the player
player:consume_held_item(1)

-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")

-- Positive feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end

-- Set cooldown regardless of path
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Explicación paso a paso​

  1. Ramificación según la ofrenda -- El script comprueba player:get_held_item() y toma uno de dos caminos: castigo si el jugador no tiene oro, o recompensa si lo tiene.

  2. Caída de rayo -- context.world:strike_lightning(x, y, z) invoca un rayo real (que hace daño) en la ubicación del jugador. La posición del jugador se lee de player.current_location.

  3. Generación de zombis -- context.world:spawn_entity("zombie", x, y, z) genera zombis normales en un círculo alrededor del santuario. Eliminar el elemento no elimina esos monstruos. El ejemplo no comprueba el terreno ni limita cuántos siguen vivos.

  4. Efectos de poción negativos -- player:add_potion_effect("poison", 400, 1) aplica 20 segundos de Veneno II. Los nombres de efecto son strings en minúscula que coinciden con los nombres de PotionEffectType de Bukkit.

  5. Recompensa -- El santuario pone en cola el consumo de la mano principal y aplica un efecto positivo aleatorio. Como el santuario de bendiciones, es una demostración, no un intercambio atómico de pago y recompensa.

  6. Tiempo de reutilización -- Ambas ramas activan la misma marca para todos los jugadores que usan esta asignación de script. Se restablece tras 600 ticks del servidor, 30 segundos a 20 TPS.

Rendimiento de la generación de entidades

Generar múltiples entidades a la vez puede afectar al rendimiento del servidor. Mantén bajo el conteo (4-6) y considera añadir un cooldown por jugador si muchos jugadores usan el santuario simultáneamente.


Ejemplo: globo giratorio​

Lo que enseña este ejemplo: Reproducir una animación temporizada al interactuar, programar una detención de la animación y efectos de sonido mecánicos.

Archivo de script completo (clic para expandir)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 seconds in ticks

return {
api_version = 1,

on_spawn = function(context)
context.state.is_spinning = false
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Prevent starting a new spin while already spinning
if context.state.is_spinning then
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Start the spin animation (non-looping)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true

-- Play a mechanical clicking sound
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)

-- Schedule the animation to stop after 5 seconds
context.scheduler:run_later(SPIN_DURATION, function(later_context)
later_context.prop:stop_animation()
later_context.state.is_spinning = false

local stop_loc = later_context.prop.current_location
if stop_loc then
later_context.world:play_sound("BLOCK_CHAIN_FALL", stop_loc.x, stop_loc.y, stop_loc.z, 1.0, 0.8)
end
end)
end
}

Explicación paso a paso​

  1. Protección de estado -- context.state.is_spinning evita múltiples solicitudes de giro superpuestas. La bandera se establece cuando empieza el giro y se borra cuando se dispara la detención programada.

  2. Animación temporizada -- play_animation(SPIN_ANIMATION, true, false) solicita una animación sin bucle mediante la cola. La solicitud de detención ocurre 100 ticks del servidor después del clic, no del inicio de la reproducción. Una cola ocupada puede acortar o impedir el giro visible, y el motor compartido no detiene una animación de muerte activa. El ejemplo activa la marca incluso si la solicitud devuelve false.

  3. Sonidos mecánicos -- BLOCK_CHAIN_PLACE da un sonido de inicio mecánico/de clic; BLOCK_CHAIN_FALL da un sonido de parada de desaceleración. Ajusta el tono a tu gusto.

  4. Contexto del callback -- El callback de run_later usa later_context (no context) para todo el acceso a estado y mundo. Este es el patrón correcto para los callbacks del scheduler.

Nombres de animación

El nombre de animación "spin" debe coincidir con lo definido en tu archivo de modelo. Si tu modelo usa un nombre diferente (p. ej. "rotate", "turn"), actualiza la constante SPIN_ANIMATION en consecuencia.


Ejemplo: elemento decorativo de susto repentino​

Lo que enseña este ejemplo: Disparadores de zona de proximidad, efectos de susto de un solo disparo con un cooldown largo, y combinación de sonido/partículas/animación para un impacto dramático.

Archivo de script completo (clic para expandir)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
local SCARE_ANIMATION = "jumpscare"

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil

local loc = context.prop.current_location
if loc == nil then return end

-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle

-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,

on_zone_enter = function(context)
if context.state.on_cooldown then
return
end

local scare_loc = context.prop.current_location
if scare_loc == nil then return end

-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)

-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)

-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)

-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Explicación paso a paso​

  1. Zona de proximidad -- context.zones:create_sphere() crea una zona fija de 3 bloques de radio en la posición inicial. context.zones:watch(handle, function() end, nil) habilita on_zone_enter. Mueve o vuelve a crear la zona si desplazas el elemento.

  2. Efectos de susto -- El hook on_zone_enter reproduce una animación "jumpscare", un sonido de grito de ghast y genera partículas de humo de hoguera. La combinación crea un efecto repentino y sobresaltante.

  3. Tiempo de reutilización -- Todos los jugadores comparten la marca de esta asignación de script. Se restablece tras 1200 ticks del servidor, 60 segundos a 20 TPS; entonces la siguiente entrada en la zona puede provocar otro susto.

  4. Sin hook de salida -- El tercer argumento nil de context.zones:watch() significa que no nos importa cuándo los jugadores salen de la zona.

  5. Limpieza -- on_destroy deja de observar la zona. Aunque las zonas se limpian automáticamente cuando se elimina el prop, la limpieza explícita es una buena práctica.

Diseño del susto

Para el mejor efecto de susto, esconde el prop tras una esquina o en una zona oscura. El radio de 3 bloques asegura que el jugador esté cerca antes de que se dispare el susto. Ajusta SCARE_RADIUS y COOLDOWN_TICKS a tu gusto.


Ejemplo: generador de goblins​

Lo que enseña este ejemplo: Usar prop:spawn_elitemobs_boss() para generar un jefe personalizado a partir de la interacción con un prop, con un fallback elegante si EliteMobs no está instalado.

Archivo de script completo (clic para expandir)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 seconds between spawns

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)

if boss then
-- Success -- play spawn effects
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")

-- Fizzle particles as visual feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end

-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Explicación paso a paso​

  1. Generación de jefes -- context.prop:spawn_elitemobs_boss(filename, x, y, z) genera un jefe personalizado de EliteMobs en las coordenadas dadas. El nombre de archivo debe coincidir con un archivo .yml en la carpeta custombosses de EliteMobs.

  2. Información del fallo -- spawn_elitemobs_boss() devuelve nil si la integración no puede devolver un jefe vivo, por ejemplo si falta el plugin o la configuración, o falla la generación. El mensaje del ejemplo sugiere comprobar EliteMobs, pero no identifica la causa exacta.

  3. Desplazamiento de generación -- La posición solicitada está un bloque por encima del elemento. No comprueba terreno ni colisiones; elige una zona despejada apropiada para el jefe.

  4. Tiempo de reutilización -- Esta asignación de script permite un intento cada 200 ticks del servidor, 10 segundos a 20 TPS, compartido por todos los jugadores. No limita los jefes vivos ni los elimina cuando desaparece el elemento. Añade límites al encuentro si lo usas como actividad repetible.

  5. Distinción visual/auditiva -- El éxito usa partículas de llama y un sonido de invocación de evoker para una generación dramática. El fallo usa humo y extinción de fuego para un claro efecto de "fallo", de modo que el jugador sepa que algo salió mal sin revisar la consola.

Integración con EliteMobs

El nombre de archivo del jefe (p. ej. "goblin_warrior.yml") debe corresponder a una configuración de jefe personalizado existente en EliteMobs. Si estás distribuyendo un mapa o mazmorra que usa este script, incluye el archivo de configuración del jefe y documenta la dependencia de EliteMobs.


Efectos de objetos​

Los ejemplos de scripts de objetos equipados se han retirado. Usa definiciones de objetos personalizados y encantamientos personalizados para los efectos actuales de objetos.

Buenas prácticas​

  • Empieza con un hook diminuto y verifica. Escribe un único on_spawn que envíe un mensaje al log. Confirma que se dispara. Luego construye a partir de ahí.

  • Mantén las funciones auxiliares locales. Declara los auxiliares como local function toggle_door(context) encima de la tabla de retorno. Esto los mantiene fuera del ámbito global.

  • Inicializa todo el estado en on_spawn. Si lees context.state.is_open en on_right_click pero nunca lo estableces en on_spawn, será nil y tus comparaciones podrían comportarse de forma inesperada.

  • Cancela las tareas repetitivas cuando termines. Cada run_repeating debería tener un cancel correspondiente en on_destroy. Las tareas filtradas desperdician CPU.

  • Usa contextos frescos de callback del scheduler. Los callbacks del scheduler reciben un parámetro de contexto fresco. Usa siempre ese parámetro dentro del callback, no el context exterior.

  • Mantén on_game_tick ligero. Se ejecuta cada tick del servidor, 20 veces por segundo a 20 TPS. Limita el trabajo costoso mediante context.cooldowns o un intervalo del programador.

  • Controla deliberadamente el daño del clic izquierdo. Incluye su cancelación en on_left_click cuando los golpes no deban causar el daño predeterminado del elemento; no bloquea todas las fuentes de daño.

  • Los nombres de partículas aceptan FLAME y flame; la API normaliza las mayúsculas antes de buscar la partícula de Bukkit. Los sonidos aceptan nombres enum de Bukkit sin distinguir mayúsculas o claves válidas como minecraft:block.note_block.harp y claves de packs personalizados. El cliente debe disponer del sonido en su pack de recursos.


Errores habituales de principiantes​

  • Usar el context exterior dentro de un callback del scheduler. El contexto exterior captura una instantánea del momento en que se ejecutó el hook. Dentro de los callbacks, usa siempre el propio parámetro del callback.

  • Olvidar cancelar tareas repetitivas. Si inicias un run_repeating en on_spawn pero nunca lo cancelas, la tarea se ejecuta hasta que se elimina el prop.

  • No inicializar el estado en on_spawn. Leer context.state.x antes de establecerlo devuelve nil, lo que puede romper tu lógica silenciosamente.

  • Nombres de animación incorrectos. Si play_animation("open") devuelve false, el nombre de la animación no coincide con lo que hay en el archivo del modelo. Comprueba el modelo para conocer los nombres exactos.

  • Los nombres de partículas aceptan FLAME y flame; la API normaliza las mayúsculas antes de buscar la partícula de Bukkit. Los sonidos aceptan nombres enum de Bukkit sin distinguir mayúsculas o claves válidas como minecraft:block.note_block.harp y claves de packs personalizados. El cliente debe disponer del sonido en su pack de recursos.

  • Olvidar api_version = 1. La tabla devuelta debe incluir este campo, o FMM no cargará el script.

  • Poner auxiliares dentro de la tabla devuelta. Aparte de api_version y el priority opcional, la tabla solo acepta nombres de hooks compatibles con valores de función. Define los auxiliares como funciones locales fuera de ella. FMM ejecuta los scripts en el orden de la lista configurada; priority no los reordena.


Lista de control de calidad​

Usa esta lista de verificación para validar un script de prop antes de desplegarlo:

  1. El archivo devuelve exactamente una tabla con api_version = 1.
  2. Cada nombre de evento coincide exactamente con una entrada de la lista de eventos de elementos decorativos. Los eventos de encantamientos pertenecen a otro motor.
  3. Comprueba tanto context.event como context.event.cancel antes de cancelar fuera de un evento de clic conocido.
  4. Los campos de context.state se inicializan en on_spawn.
  5. Cada llamada a scheduler:run_repeating(...) tiene un scheduler:cancel(...) correspondiente en on_destroy.
  6. Los callbacks del scheduler usan el propio parámetro de contexto del callback, no el context exterior.
  7. Los hooks on_game_tick limitan el trabajo costoso detrás de una comprobación.
  8. Todos los nombres de método existen en la referencia de la API de elementos decorativos, sin alias inventados.
  9. Los nombres de partículas aceptan FLAME y flame; la API normaliza las mayúsculas antes de buscar la partícula de Bukkit. Los sonidos aceptan nombres enum de Bukkit sin distinguir mayúsculas o claves válidas como minecraft:block.note_block.harp y claves de packs personalizados. El cliente debe disponer del sonido en su pack de recursos.
  10. El script no llama a ninguna operación bloqueante o de larga duración dentro de un hook o callback.

Consejos para generar scripts con IA​

Si quieres que la IA genere scripts de prop de forma fiable, asegúrate de que el prompt incluya:

  • Nombre exacto del hook -- p. ej., on_right_click, no "cuando el jugador hace clic en el prop".
  • Nombres de animación del archivo del modelo -- la IA no puede adivinarlos; proporciónalos.
  • Nombres de enum Sound -- p. ej., "BLOCK_NOTE_BLOCK_HARP", no "sonido de arpa".
  • Nombres de enum de partículas -- p. ej., "FLAME", no "partículas de fuego".
  • Si los clics izquierdos deben causar el daño predeterminado -- cancela ese comportamiento mediante on_left_click y context.event.cancel().
  • Usa nombres de método verificados -- consulta primero la API de elementos decorativos. Si una operación necesaria no figura, comprueba el código actual del motor en lugar de inventar un alias.

Ejemplo de buen prompt​

Escribe un script de elemento decorativo de FMM que reproduzca la animación "activate" al hacer clic derecho, cancele el daño predeterminado del clic izquierdo, genere partículas FLAME en la ubicación del elemento al hacer clic, reproduzca el sonido BLOCK_LEVER_CLICK y tenga un tiempo de reutilización de 2 segundos entre clics usando context.cooldowns:check_local("activate", 40).


Compatibilidad del contenido de objetos​

Los paquetes antiguos de objetos que usan scripts: necesitan definiciones actualizadas y contenido de encantamientos. FMM omite esas definiciones retiradas y deja los archivos intactos. Añadir un campo material: ya no activa un script de objeto.

Siguientes pasos​