Scripting Lua: Ejemplos y Patrones
Esta página contiene ejemplos completos y funcionales de scripts de props de FreeMinecraftModels, además de patrones prácticos y buenas prácticas. Cada ejemplo incluye una explicación de qué hace y por qué.
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 enseña este ejemplo: El script útil más simple -- cancelar el daño 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
}
Explicación Paso a Paso
-
Elección del hook --
on_left_clickse activa cuando un jugador golpea (clic izquierdo) el prop. Internamente, se trata de unEntityDamageByEntityEventen el armor stand subyacente del prop. -
Verificación del evento --
context.eventsiempre debería estar presente en este hook, pero la verificación es una buena práctica. -
Cancelación --
context.event.cancel()cancela el evento de daño, lo que evita que el armor stand reciba daño y sea destruido.
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)
-- 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
}
Explicación Paso a Paso
-
Constantes a nivel de archivo --
OPEN_ANIMATIONyCLOSE_ANIMATIONse definen encima de la tabla return. Esto facilita cambiarlas para diferentes archivos de modelo que puedan usar nombres de animación distintos. -
Inicialización del estado --
on_spawnestablececontext.state.is_open = false. El estado persiste a través de todos los hooks para esta instancia del prop. -
Invulnerabilidad -- El hook
on_left_clickcancela el daño para que la puerta no pueda romperse accidentalmente. -
Lógica de alternancia --
on_right_clickverificacontext.state.is_open, detiene cualquier animación actual, reproduce la animación correspondiente, cambia el estado y reproduce un sonido. La llamada astop_animation()antes deplay_animation()asegura transiciones limpias. -
Retroalimentación sonora --
context.world:play_sound()reproduce el nombre del enum Sound de Bukkit en la ubicación del prop. Los nombres de sonido deben ser nombres de enum en UPPER_CASE.
Los nombres de animación como "open" y "close" deben coincidir con lo definido en el archivo del modelo. Si la animación no se encuentra, play_animation() devuelve false y no sucede nada. Verifica tu archivo de modelo para los nombres exactos de animación.
Ejemplo: Activador de Proximidad con Partículas
Lo que enseña este ejemplo: Creación de zonas, vigilancia 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
-- 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
}
Explicación Paso a Paso
-
Constantes --
ZONE_RADIUSyPARTICLE_INTERVALestán a nivel de archivo para facilitar su ajuste. -
Inicialización del estado --
on_spawnestablece todos los campos de estado anil/0antes de hacer cualquier otra cosa. -
Creación de zona --
context.zones:create_sphere()crea una zona esférica centrada en el prop. El handle devuelto es un ID numérico usado para referenciar esta zona posteriormente. -
Vigilancia de zona --
context.zones:watch()registra callbacks para la entrada y salida de jugadores. Los callbacks incrementan y decrementan un contador almacenado encontext.state. -
Bucle de partículas -- Una tarea repetitiva genera partículas en un anillo alrededor del prop cada medio segundo. El tipo de partícula cambia según si hay jugadores en la zona.
-
Limpieza --
on_destroycancela la tarea repetitiva y deja de vigilar la zona. Aunque ambos se limpian automáticamente cuando el prop se elimina, la limpieza explícita es una buena práctica.
Generar muchas partículas en cada tick puede afectar el rendimiento. Usa un intervalo razonable (10-20 ticks) y mantén los conteos de partículas bajos. El ejemplo anterior usa PARTICLE_INTERVAL = 10 (dos veces por segundo) con solo 12 partículas por anillo.
Ejemplo: Prop Emisor de Sonido
Lo que enseña este ejemplo: Reproducir sonidos en la interacción, comportamiento tipo cooldown usando state y scheduler, y prevención de interacciones rápidas 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
}
Explicación Paso a Paso
-
Patrón de cooldown -- Dado que los scripts de props de FMM no tienen una API
context.cooldownsintegrada como EliteMobs, el ejemplo implementa un cooldown simple usandocontext.state.on_cooldownyscheduler:run_later(). La bandera se establece entruecuando se reproduce el sonido, y una tarea retrasada la restablece después deCOOLDOWN_TICKS. -
Reproducción de sonido --
context.world:play_sound()recibe el nombre del enum Sound de Bukkit en UPPER_CASE, coordenadas, volumen y tono. -
Retroalimentación con partículas -- Partículas de notas aparecen sobre el prop cuando se reproduce el sonido, proporcionando una señal visual.
-
Invulnerabilidad -- El hook
on_left_clickcancela el daño como de costumbre. -
Contexto del callback del scheduler -- El callback de
run_laterrecibelater_context, un contexto nuevo. Usamoslater_context.state(nocontext.state) para restablecer la bandera de cooldown. Como el estado es compartido, ambos apuntan a la misma tabla -- pero usar el parámetro de contexto del callback es el hábito correcto.
Los scripts de props de FMM no tienen la API context.cooldowns de EliteMobs. Usa el patrón mostrado aquí: una bandera booleana en context.state combinada con scheduler:run_later() para restablecerla. Esto te da control total sobre la duración y el comportamiento del cooldown.
Ejemplo: Prop Ambiental Animado
Lo que enseña este ejemplo: Iniciar una animación en bucle al aparecer, 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
-
Inicio automático de animación --
on_spawnreproduce inmediatamente una animación"idle"en bucle. Elfalsepara blend significa que comienza desde cero sin mezclar con una animación anterior. Eltruepara loop significa que se repite indefinidamente. -
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.
-
Limpieza --
on_destroycancela 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, desmontarlo con clic izquierdo, y usar context.event.player para obtener el jugador que interactúa.
Archivo de script completo (clic para expandir)
return {
api_version = 1,
on_right_click = function(context)
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 damage so the chair is invulnerable
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
-
Clic derecho para sentarse --
on_right_clickobtiene el jugador decontext.event.player, verifica si ya es un pasajero (para evitar doble montaje) y llama acontext.prop:mount(player)para sentarlo en el armor stand del prop. -
Clic izquierdo para levantarse --
on_left_clickcancela el evento de daño (invulnerabilidad), luego verifica si el jugador que golpea es actualmente un pasajero. Si es así,context.prop:dismount(player)lo expulsa. -
Verificación de pasajeros --
context.prop:get_passengers()devuelve un array de tablas de entidades. Comparamos UUIDs para encontrar al jugador que interactúa en la lista. -
Retroalimentación sonora -- Un sonido de colocación de madera se reproduce al sentarse y un sonido de rotura de madera al levantarse, proporcionando retroalimentación táctil.
Ejemplo: Santuario de Bendición
Lo que enseña este ejemplo: Verificar el ítem sostenido del jugador, consumir ítems, aplicar efectos de poción aleatorios, gestión de cooldowns y retroalimentación con 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)
-- Make invulnerable
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
-
Verificación de ítem --
player:get_held_item()devuelve una tabla contype,amountydisplay_namepara el ítem de la mano principal (onilsi está vacía). Comparamosheld.typecon"gold_ingot"(nombre del material en minúsculas). -
Consumo de ítem --
player:consume_held_item(1)elimina un ítem de la pila de la mano principal del jugador. -
Buff aleatorio -- La tabla
BLESSINGSa nivel de archivo lista los efectos positivos disponibles.math.random(#BLESSINGS)elige uno al azar.player:add_potion_effect(effect, duration, amplifier)lo aplica --600ticks son 30 segundos, amplificador1es nivel II. -
Cooldown -- El mismo patrón de bandera booleana más scheduler del ejemplo de Prop Emisor de Sonido. Un cooldown de 30 segundos previene el spam del santuario.
-
Retroalimentación -- Partículas de encantamiento y aldeano feliz más un sonido de activación de beacon 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)
-- Make invulnerable
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
-
Ramificación por ofrenda -- El script verifica
player:get_held_item()y toma uno de dos caminos: castigo si el jugador no tiene oro, o recompensa si lo tiene. -
Rayo --
context.world:strike_lightning(x, y, z)invoca un rayo real (con daño) en la ubicación del jugador. La posición del jugador se lee deplayer.current_location. -
Generación de zombis --
context.world:spawn_entity("zombie", x, y, z)genera zombis vanilla. El bucle los distribuye uniformemente en un círculo alrededor del santuario usando trigonometría. -
Efectos de poción negativos --
player:add_potion_effect("poison", 400, 1)aplica 20 segundos de Veneno II. Los nombres de efectos son strings en minúsculas que coinciden con los nombres dePotionEffectTypede Bukkit. -
Camino de recompensa -- Cuando se ofrece oro, el santuario consume un lingote y aplica un efecto positivo aleatorio, reflejando el comportamiento del Santuario de Bendición.
-
Cooldown -- Un cooldown de 30 segundos se aplica independientemente de qué rama se tomó, previniendo castigo o recompensa rápidos.
Generar múltiples entidades a la vez puede impactar el rendimiento del servidor. Mantén el conteo bajo (4-6) y considera agregar 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 en la interacción, programar la detención de una 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)
-- Make invulnerable
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
-
Guardia de estado --
context.state.is_spinningpreviene múltiples solicitudes de giro superpuestas. La bandera se establece cuando comienza el giro y se limpia cuando se dispara la detención programada. -
Animación temporizada --
play_animation(SPIN_ANIMATION, true, false)reproduce la animación una vez (sin bucle). La llamadascheduler:run_later(100, ...)detiene la animación después de exactamente 5 segundos, en caso de que la animación misma sea más larga o esté en bucle. -
Sonidos mecánicos --
BLOCK_CHAIN_PLACEda un sonido de clic/mecánico al inicio;BLOCK_CHAIN_FALLda un sonido de desaceleración al detenerse. Ajusta el tono a tu gusto. -
Contexto del callback -- El callback de
run_laterusalater_context(nocontext) para todo el acceso a state y world. Este es el patrón correcto para callbacks del scheduler.
El nombre de animación "spin" debe coincidir con lo definido en tu archivo de modelo. Si tu modelo usa un nombre diferente (ej. "rotate", "turn"), actualiza la constante SPIN_ANIMATION correspondientemente.
Ejemplo: Prop de Susto
Lo que enseña este ejemplo: Disparadores de zona de proximidad, efectos de susto de un solo uso con un cooldown largo, y combinación de sonido/partículas/animación para 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
context.zones:watch(
handle,
function(player)
-- on_enter: trigger the scare
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,
nil -- no on_leave callback needed
)
end,
on_left_click = function(context)
-- Make invulnerable
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
-
Zona de proximidad --
context.zones:create_sphere()crea una zona de radio de 3 bloques.context.zones:watch()registra un callback de entrada que se dispara cuando cualquier jugador entra. -
Efectos de susto -- El callback de entrada reproduce una animación
"jumpscare", un grito de ghast y genera partículas de humo de fogata. La combinación crea un efecto repentino y sobresaltante. -
Cooldown de 60 segundos -- El patrón de bandera booleana previene que el susto se active repetidamente. Una vez activado, el prop queda en silencio por 60 segundos (
COOLDOWN_TICKS = 1200), luego se rearma. -
Sin callback de salida -- El argumento
nilcomo segundo argumento decontext.zones:watch()significa que no nos importa cuándo los jugadores salen de la zona. -
Limpieza --
on_destroydeja de vigilar la zona. Aunque las zonas se limpian automáticamente cuando el prop se elimina, la limpieza explícita es una buena práctica.
Para el mejor efecto de susto, esconde el prop detrás de una esquina o en un área oscura. El radio de 3 bloques asegura que el jugador esté cerca antes de que se active el susto. Ajusta SCARE_RADIUS y COOLDOWN_TICKS a tu gusto.
Ejemplo: Prop Generador de Goblins
Lo que enseña este ejemplo: Usar prop:spawn_elitemobs_boss() para generar un jefe personalizado desde una interacción con prop, con una alternativa 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)
-- Make invulnerable
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
-
Generación de jefe --
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.ymlen la carpetacustombossesde EliteMobs. -
Alternativa elegante --
spawn_elitemobs_boss()devuelvenilsi EliteMobs no está instalado o el archivo de jefe no existe. El script maneja esto con un mensaje de log de advertencia, un efecto de partículas de chisporroteo y un mensaje al jugador explicando el fallo. -
Desplazamiento de spawn -- El jefe se genera en
loc.y + 1(un bloque por encima del prop) para evitar que el jefe se sobreponga con el prop o el suelo. -
Cooldown -- Un cooldown de 10 segundos previene que los jugadores inunden el área con guerreros goblin. Ajusta
COOLDOWN_TICKSsegún tus necesidades de juego. -
Distinción visual/auditiva -- El éxito usa partículas de llama y un sonido de invocación de evoker para un spawn dramático. El fallo usa humo y extinción de fuego para un efecto claro de "chisporroteo", para que el jugador sepa que algo salió mal sin revisar la consola.
El nombre de archivo del jefe (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.
Buenas Prácticas
-
Empieza con un hook pequeño y verifica. Escribe un solo
on_spawnque envíe un mensaje de log. Confirma que se activa. Luego construye a partir de ahí. -
Mantén las funciones auxiliares locales. Declara los helpers como
local function toggle_door(context)encima de la tabla return. Esto los mantiene fuera del ámbito global. -
Inicializa todo el estado en
on_spawn. Si leescontext.state.is_openenon_right_clickpero nunca lo estableces enon_spawn, seránily tus comparaciones pueden comportarse inesperadamente. -
Cancela las tareas repetitivas cuando terminen. Cada
run_repeatingdebe tener uncancelcorrespondiente enon_destroy. Las tareas sin cancelar desperdician CPU. -
Usa contextos frescos de callbacks del scheduler. Los callbacks del scheduler reciben un parámetro de contexto nuevo. Siempre usa ese parámetro dentro del callback, no el
contextexterno. -
Mantén
on_game_tickligero. Si defines este hook, se ejecuta en cada tick del servidor (20 veces por segundo). Protege el trabajo costoso detrás de una verificación de cooldown basada en estado. -
Haz los props invulnerables por defecto. A menos que quieras que el prop sea destruible, incluye la cancelación de daño de
on_left_clicken cada script. -
Usa UPPER_CASE para enums de Bukkit. Los nombres de sonidos y partículas deben usar el formato de constante enum de Bukkit (por ejemplo,
"FLAME", no"flame").
Errores Comunes de Principiantes
-
Usar el
contextexterno dentro de un callback del scheduler. El contexto externo captura una instantánea del momento en que se ejecutó el hook. Dentro de los callbacks, siempre usa el parámetro propio del callback. -
Olvidar cancelar tareas repetitivas. Si inicias un
run_repeatingenon_spawnpero nunca lo cancelas, la tarea se ejecuta hasta que el prop sea eliminado. -
No inicializar el estado en
on_spawn. Leercontext.state.xantes de establecerlo devuelvenil, lo que puede romper tu lógica silenciosamente. -
Nombres de animación incorrectos. Si
play_animation("open")devuelvefalse, el nombre de la animación no coincide con el del archivo del modelo. Verifica el modelo para los nombres exactos. -
Nombres de sonido/partículas en minúsculas.
"flame"no funciona -- usa"FLAME". La API convierte a UPPER_CASE internamente para partículas, pero los nombres de enum de Sound deben ser exactos. -
Olvidar
api_version = 1. La tabla devuelta debe incluir este campo, o FMM no cargará el script. -
Poner funciones dentro de la tabla devuelta que no son hooks. Las funciones auxiliares deben declararse encima de la instrucción
return. Solo los nombres de hooks (on_spawn,on_right_click, etc.) están permitidos como claves en la tabla devuelta.
Lista de Verificación QC
Usa esta lista de verificación para verificar un script de prop antes de desplegarlo:
- El archivo devuelve exactamente una tabla con
api_version = 1. - Cada nombre de hook coincide exactamente con una entrada en la lista de hooks.
context.eventse verifica conif context.event thenantes de llamar acancel().- Los campos de
context.statese inicializan enon_spawn. - Cada llamada a
scheduler:run_repeating(...)tiene unscheduler:cancel(...)correspondiente enon_destroy. - Los callbacks del scheduler usan el parámetro de contexto propio del callback, no el
contextexterno. - Los hooks
on_game_tickprotegen el trabajo costoso detrás de una verificación. - Todos los nombres de métodos existen en la referencia de la API de Props -- sin aliases inventados.
- Los nombres de sonidos y partículas usan nombres de enum UPPER_CASE de Bukkit.
- El script no llama a operaciones bloqueantes o de larga duración dentro de un hook o callback.
Consejos para Generación con IA
Si quieres que una IA genere scripts de props de manera confiable, asegúrate 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 animación del archivo del modelo -- la IA no puede adivinarlos; proporciónaselos.
- Nombres de enum de sonido -- por ejemplo,
"BLOCK_NOTE_BLOCK_HARP", no "sonido de arpa". - Nombres de enum de partículas -- por ejemplo,
"FLAME", no "partículas de fuego". - Si el prop debe ser invulnerable -- si es así, incluir
on_left_clickconcontext.event.cancel(). - Usar solo nombres de métodos documentados -- si no está en la página de la API de Props, no existe.
Buen ejemplo de prompt
Escribe un script de prop FMM que reproduzca la animación "activate" al hacer clic derecho, haga el prop invulnerable, genere partículas FLAME en la ubicación 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.
Próximos Pasos
- Primeros Pasos -- estructura de archivos, hooks, explicación del primer script, plantillas
- API de Props -- referencia completa de la API para todas las tablas de contexto
- Solución de Problemas -- problemas comunes, consejos de depuración