Saltar al contenido principal

Scripting Lua: Ejemplos y patrones

Esta pagina contiene ejemplos completos y funcionales de scripts de props de FreeMinecraftModels, ademas de patrones practicos y buenas practicas. Cada ejemplo incluye una explicacion de que hace y por que.

Si eres nuevo en el scripting de props, comienza con Primeros pasos. Para detalles completos de la API, consulta la API de Props.


Ejemplo: Prop invulnerable

Lo que ensena este ejemplo: El script util mas simple -- cancelar el dano para que el prop no pueda ser destruido.

Este es el script predefinido que viene con FreeMinecraftModels.

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
}

Explicacion paso a paso

  1. Eleccion del hook -- on_left_click se activa cuando un jugador golpea (clic izquierdo) el prop. Internamente, se trata de un EntityDamageByEntityEvent en el armor stand subyacente del prop.

  2. Verificacion del evento -- context.event siempre deberia estar presente en este hook, pero la verificacion es una buena practica.

  3. Cancelacion -- context.event.cancel() cancela el evento de dano, lo que evita que el armor stand reciba dano y sea destruido.

Uso

Agrega lo siguiente a la configuracion .yml de tu prop:

isEnabled: true
scripts:
- invulnerable.lua

Ejemplo: Puerta interactiva

Lo que ensena este ejemplo: Alternar estado con clic derecho, reproducir y detener animaciones, y usar context.state para rastrear si la puerta esta 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)
-- Make the door invulnerable
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
}

Explicacion paso a paso

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

  2. Inicializacion del estado -- on_spawn establece context.state.is_open = false. El estado persiste a traves de todos los hooks para esta instancia del prop.

  3. Invulnerabilidad -- El hook on_left_click cancela el dano para que la puerta no pueda romperse accidentalmente.

  4. Logica de alternancia -- on_right_click verifica context.state.is_open, detiene cualquier animacion actual, reproduce la animacion correspondiente, cambia el estado y reproduce un sonido. La llamada a stop_animation() antes de play_animation() asegura transiciones limpias.

  5. Retroalimentacion sonora -- context.world:play_sound() reproduce el nombre del enum Sound de Bukkit en la ubicacion del prop. Los nombres de sonido deben ser nombres de enum en UPPER_CASE.

Nombres de animacion

Los nombres de animacion como "open" y "close" deben coincidir con lo definido en el archivo del modelo. Si la animacion no se encuentra, play_animation() devuelve false y no sucede nada. Verifica tu archivo de modelo para los nombres exactos de animacion.


Ejemplo: Activador de proximidad con particulas

Lo que ensena este ejemplo: Creacion de zonas, vigilancia de zonas para eventos de entrada/salida, efectos de particulas 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

-- Watch for enter/leave
context.zones:watch(
handle,
function(player)
-- on_enter
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
function(player)
-- on_leave
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
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
-- Red particles when players are inside
tick_context.world:spawn_particle("DUST", 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)
-- Make invulnerable
if context.event then
context.event.cancel()
end
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
}

Explicacion paso a paso

  1. Constantes -- ZONE_RADIUS y PARTICLE_INTERVAL estan a nivel de archivo para facilitar su ajuste.

  2. Inicializacion del estado -- on_spawn establece todos los campos de estado a nil / 0 antes de hacer cualquier otra cosa.

  3. Creacion de zona -- context.zones:create_sphere() crea una zona esferica centrada en el prop. El handle devuelto es un ID numerico usado para referenciar esta zona posteriormente.

  4. Vigilancia de zona -- context.zones:watch() registra callbacks para la entrada y salida de jugadores. Los callbacks incrementan y decrementan un contador almacenado en context.state.

  5. Bucle de particulas -- Una tarea repetitiva genera particulas en un anillo alrededor del prop cada medio segundo. El tipo de particula cambia segun si hay jugadores en la zona.

  6. Limpieza -- on_destroy cancela la tarea repetitiva y deja de vigilar la zona. Aunque ambos se limpian automaticamente cuando el prop se elimina, la limpieza explicita es una buena practica.

Rendimiento de particulas

Generar muchas particulas en cada tick puede afectar el rendimiento. Usa un intervalo razonable (10-20 ticks) y mantiene los conteos de particulas bajos. El ejemplo anterior usa PARTICLE_INTERVAL = 10 (dos veces por segundo) con solo 12 particulas por anillo.


Ejemplo: Prop emisor de sonido

Lo que ensena este ejemplo: Reproducir sonidos en la interaccion, comportamiento tipo cooldown usando state y scheduler, y prevencion de interacciones rapidas repetidas.

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_spawn = function(context)
context.state.on_cooldown = false
end,

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

on_right_click = function(context)
-- Prevent spam
if context.state.on_cooldown 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)

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

Explicacion paso a paso

  1. Patron de cooldown -- Dado que los scripts de props de FMM no tienen una API context.cooldowns integrada como EliteMobs, el ejemplo implementa un cooldown simple usando context.state.on_cooldown y scheduler:run_later(). La bandera se establece en true cuando se reproduce el sonido, y una tarea retrasada la restablece despues de COOLDOWN_TICKS.

  2. Reproduccion de sonido -- context.world:play_sound() recibe el nombre del enum Sound de Bukkit en UPPER_CASE, coordenadas, volumen y tono.

  3. Retroalimentacion con particulas -- Particulas de notas aparecen sobre el prop cuando se reproduce el sonido, proporcionando una senal visual.

  4. Invulnerabilidad -- El hook on_left_click cancela el dano como de costumbre.

  5. Contexto del callback del scheduler -- El callback de run_later recibe later_context, un contexto nuevo. Usamos later_context.state (no context.state) para restablecer la bandera de cooldown. Como el estado es compartido, ambos apuntan a la misma tabla -- pero usar el parametro de contexto del callback es el habito correcto.

Implementacion de cooldown

Los scripts de props de FMM no tienen la API context.cooldowns de EliteMobs. Usa el patron mostrado aqui: una bandera booleana en context.state combinada con scheduler:run_later() para restablecerla. Esto te da control total sobre la duracion y el comportamiento del cooldown.


Ejemplo: Prop ambiental animado

Lo que ensena este ejemplo: Iniciar una animacion en bucle al aparecer, con un emisor de particulas 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
}

Explicacion paso a paso

  1. Inicio automatico de animacion -- on_spawn reproduce inmediatamente una animacion "idle" en bucle. El false para blend significa que comienza desde cero sin mezclar con una animacion anterior. El true para loop significa que se repite indefinidamente.

  2. Particulas ambientales -- Una tarea repetitiva genera particulas de mesa de encantamientos sobre el prop cada 2 segundos, creando un efecto ambiental magico.

  3. Limpieza -- on_destroy cancela la tarea de particulas.


Buenas practicas

  • Empieza con un hook pequeno y verifica. Escribe un solo on_spawn que envie un mensaje de log. Confirma que se activa. Luego construye a partir de ahi.

  • Mantiene las funciones auxiliares locales. Declara los helpers como local function toggle_door(context) encima de la tabla return. Esto los mantiene fuera del ambito 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, sera nil y tus comparaciones pueden comportarse inesperadamente.

  • Cancela las tareas repetitivas cuando terminen. Cada run_repeating debe tener un cancel correspondiente en on_destroy. Las tareas sin cancelar desperdician CPU.

  • Usa contextos frescos de callbacks del scheduler. Los callbacks del scheduler reciben un parametro de contexto nuevo. Siempre usa ese parametro dentro del callback, no el context externo.

  • Mantiene on_game_tick ligero. Si defines este hook, se ejecuta en cada tick del servidor (20 veces por segundo). Protege el trabajo costoso detras de una verificacion de cooldown basada en estado.

  • Haz los props invulnerables por defecto. A menos que quieras que el prop sea destruible, incluye la cancelacion de dano de on_left_click en cada script.

  • Usa UPPER_CASE para enums de Bukkit. Los nombres de sonidos y particulas deben usar el formato de constante enum de Bukkit (por ejemplo, "FLAME", no "flame").


Errores comunes de principiantes

  • Usar el context externo dentro de un callback del scheduler. El contexto externo captura una instantanea del momento en que se ejecuto el hook. Dentro de los callbacks, siempre usa el parametro propio del callback.

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

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

  • Nombres de animacion incorrectos. Si play_animation("open") devuelve false, el nombre de la animacion no coincide con el del archivo del modelo. Verifica el modelo para los nombres exactos.

  • Nombres de sonido/particulas en minusculas. "flame" no funciona -- usa "FLAME". La API convierte a UPPER_CASE internamente para particulas, pero los nombres de enum de Sound deben ser exactos.

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

  • Poner funciones dentro de la tabla devuelta que no son hooks. Las funciones auxiliares deben declararse encima de la instruccion return. Solo los nombres de hooks (on_spawn, on_right_click, etc.) estan permitidos como claves en la tabla devuelta.


Lista de verificacion QC

Usa esta lista de verificacion para verificar un script de prop antes de desplegarlo:

  1. El archivo devuelve exactamente una tabla con api_version = 1.
  2. Cada nombre de hook coincide exactamente con una entrada en la lista de hooks.
  3. context.event se verifica con if context.event then antes de llamar a cancel().
  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 parametro de contexto propio del callback, no el context externo.
  7. Los hooks on_game_tick protegen el trabajo costoso detras de una verificacion.
  8. Todos los nombres de metodos existen en la referencia de la API de Props -- sin aliases inventados.
  9. Los nombres de sonidos y particulas usan nombres de enum UPPER_CASE de Bukkit.
  10. El script no llama a operaciones bloqueantes o de larga duracion dentro de un hook o callback.

Consejos para generacion con IA

Si quieres que una IA genere scripts de props de manera confiable, asegurate de que el prompt incluya:

  • Nombre exacto del hook -- por ejemplo, on_right_click, no "cuando el jugador hace clic en el prop".
  • Nombres de animacion del archivo del modelo -- la IA no puede adivinarlos; proporcionarlos.
  • Nombres de enum de sonido -- por ejemplo, "BLOCK_NOTE_BLOCK_HARP", no "sonido de arpa".
  • Nombres de enum de particulas -- por ejemplo, "FLAME", no "particulas de fuego".
  • Si el prop debe ser invulnerable -- si es asi, incluir on_left_click con context.event.cancel().
  • Usar solo nombres de metodos documentados -- si no esta en la pagina de la API de Props, no existe.

Buen ejemplo de prompt

Escribe un script de prop FMM que reproduzca la animacion "activate" al hacer clic derecho, haga el prop invulnerable, genere particulas FLAME en la ubicacion del prop al hacer clic, reproduzca el sonido BLOCK_LEVER_CLICK y tenga un cooldown de 2 segundos entre clics usando context.state y scheduler:run_later.


Proximos pasos

  • Primeros pasos -- estructura de archivos, hooks, explicacion del primer script, plantillas
  • API de Props -- referencia completa de la API para todas las tablas de contexto
  • Solucion de problemas -- problemas comunes, consejos de depuracion