Pular para o conteúdo principal

Scripting Lua: Exemplos e Padrões

Esta página contém exemplos completos e funcionais de scripts de props e itens do FreeMinecraftModels, além de padrões práticos e boas práticas. Cada exemplo inclui uma explicação do que faz e por quê.

Se você é novo em scripting, comece com Primeiros Passos. Para detalhes completos da API, veja a API de Props e Itens.


Exemplo: Prop Invulnerável

O que isso ensina: O script útil mais simples -- cancelar dano para que o prop não possa ser quebrado.

Este é o script pré-fabricado que vem com o FreeMinecraftModels.

Arquivo de script completo (clique para expandir)
return {
api_version = 1,

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

Explicação

  1. Escolha do hook -- on_left_click dispara quando um jogador soca (clica com botão esquerdo) o prop. Por baixo dos panos, este é um EntityDamageByEntityEvent no armor stand de suporte do prop.

  2. Guarda de evento -- context.event deveria estar sempre presente neste hook, mas a guarda é boa prática.

  3. Cancelamento -- context.event.cancel() cancela o evento de dano, o que previne o armor stand de receber dano e ser destruído.

Uso

Adicione à configuração .yml do seu prop:

isEnabled: true
scripts:
- invulnerable.lua

Exemplo: Porta Interativa

O que isso ensina: Alternar estado ao clicar com botão direito, reproduzir e parar animações, e usar context.state para rastrear se a porta está aberta ou fechada.

Arquivo de script completo (clique 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)
-- Tornar a porta invulnerável
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
-- Fechar a porta
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
-- Abrir a porta
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
}

Explicação

  1. Constantes no escopo do arquivo -- OPEN_ANIMATION e CLOSE_ANIMATION são definidas acima da tabela de retorno. Isso facilita a alteração para diferentes arquivos de modelo que podem usar nomes de animação diferentes.

  2. Inicialização de estado -- on_spawn define context.state.is_open = false. O estado persiste entre todos os hooks para esta instância de prop.

  3. Invulnerabilidade -- O hook on_left_click cancela dano para que a porta não possa ser acidentalmente quebrada.

  4. Lógica de alternância -- on_right_click verifica context.state.is_open, para qualquer animação atual, reproduz a animação apropriada, inverte o estado e reproduz um som. A chamada stop_animation() antes de play_animation() garante transições limpas.

  5. Feedback sonoro -- context.world:play_sound() reproduz o nome do enum Sound do Bukkit na localização do prop. Nomes de som devem ser nomes de enum em MAIÚSCULAS.

Nomes de animação

Nomes de animação como "open" e "close" devem corresponder ao que está definido no arquivo de modelo. Se a animação não for encontrada, play_animation() retorna false e nada acontece. Verifique seu arquivo de modelo para os nomes exatos de animação.


Exemplo: Gatilho de Proximidade com Partículas

O que isso ensina: Criação de zonas, monitoramento de zonas para eventos de entrada/saída, efeitos de partículas e limpeza.

Arquivo de script completo (clique para expandir)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks entre rajadas de partículas

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

-- Criar uma zona esférica ao redor do prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle

-- Monitorar entrada/saída
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
)

-- Iniciar efeito de partículas repetitivo no limite da zona
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

-- Spawnar partículas em um anel no limite da zona
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
-- Partículas vermelhas quando jogadores estão dentro
tick_context.world:spawn_particle("DUST", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Partículas verdes quando a zona está vazia
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)
-- Tornar invulnerável
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Limpar a tarefa repetitiva
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Limpar o monitoramento de zona
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Explicação

  1. Constantes -- ZONE_RADIUS e PARTICLE_INTERVAL estão no escopo do arquivo para fácil ajuste.

  2. Inicialização de estado -- on_spawn configura todos os campos de estado como nil / 0 antes de fazer qualquer outra coisa.

  3. Criação de zona -- context.zones:create_sphere() cria uma zona esférica centrada no prop. O handle retornado é um ID numérico usado para referenciar esta zona depois.

  4. Monitoramento de zona -- context.zones:watch() registra callbacks para entrada e saída de jogadores. Os callbacks incrementam e decrementam um contador armazenado em context.state.

  5. Loop de partículas -- Uma tarefa repetitiva spawna partículas em um anel ao redor do prop a cada meio segundo. O tipo de partícula muda com base em se há jogadores na zona.

  6. Limpeza -- on_destroy cancela a tarefa repetitiva e para de monitorar a zona. Embora ambos sejam limpos automaticamente quando o prop é removido, limpeza explícita é uma boa prática.

Performance de partículas

Spawnar muitas partículas a cada tick pode impactar a performance. Use um intervalo razoável (10-20 ticks) e mantenha a contagem de partículas baixa. O exemplo acima usa PARTICLE_INTERVAL = 10 (duas vezes por segundo) com apenas 12 partículas por anel.


Exemplo: Prop Emissor de Som

O que isso ensina: Reproduzir sons em interação, comportamento tipo cooldown usando state e scheduler, e prevenir interações rápidas repetidas.

Arquivo de script completo (clique para expandir)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 segundos entre sons

return {
api_version = 1,

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

on_left_click = function(context)
-- Tornar invulnerável
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Prevenir spam
if context.state.on_cooldown then
return
end

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

-- Reproduzir o som
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)

-- Mostrar algumas partículas
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)

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

Explicação

  1. Padrão de cooldown -- Como scripts de props do FMM não têm uma API embutida context.cooldowns como o EliteMobs, o exemplo implementa um cooldown simples usando context.state.on_cooldown e scheduler:run_later(). A flag é definida como true quando o som toca, e uma tarefa atrasada a reseta após COOLDOWN_TICKS.

  2. Reprodução de som -- context.world:play_sound() recebe o nome do enum Sound do Bukkit em MAIÚSCULAS, coordenadas, volume e tom.

  3. Feedback de partículas -- Partículas de nota aparecem acima do prop quando o som toca, dando uma dica visual.

  4. Invulnerabilidade -- O hook on_left_click cancela dano como de costume.

  5. Context do callback do scheduler -- O callback run_later recebe later_context, um context novo. Usamos later_context.state (não context.state) para resetar a flag de cooldown. Como o state é compartilhado, ambos apontam para a mesma tabela -- mas usar o parâmetro de context do callback é o hábito correto.

Implementação de cooldown

Scripts de props do FMM não têm a API context.cooldowns do EliteMobs. Use o padrão mostrado aqui: uma flag booleana em context.state combinada com scheduler:run_later() para resetá-la. Isso te dá controle total sobre duração e comportamento do cooldown.


Exemplo: Prop Ambiente Animado

O que isso ensina: Iniciar uma animação em loop ao spawnar, com um emissor de partículas baseado em ticks.

Arquivo de script completo (clique para expandir)
return {
api_version = 1,

on_spawn = function(context)
-- Iniciar a animação idle imediatamente, em loop
context.prop:play_animation("idle", false, true)

-- Emitir partículas ambiente a cada 40 ticks (2 segundos)
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
}

Explicação

  1. Animação automática ao iniciar -- on_spawn imediatamente reproduz uma animação "idle" em loop. O false para blend significa que inicia do zero sem blending de uma animação anterior. O true para loop significa que repete indefinidamente.

  2. Partículas ambiente -- Uma tarefa repetitiva spawna partículas de mesa de encantamento acima do prop a cada 2 segundos, criando um efeito mágico ambiente.

  3. Limpeza -- on_destroy cancela a tarefa de partículas.


Exemplo: Cadeira Sentável

O que isso ensina: Montar um jogador em um prop com clique direito, desmontar com clique esquerdo e usar context.event.player para obter o jogador interagindo.

Arquivo de script completo (clique 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

-- Verificar se o jogador já está sentado neste prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Jogador já está sentado, não fazer nada no clique direito
return
end
end

-- Montar o jogador na cadeira
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)
-- Cancelar dano para que a cadeira seja invulnerável
if context.event then
context.event.cancel()
end

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

-- Verificar se o jogador está sentado e desmontá-lo
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
}

Explicação

  1. Clique direito para sentar -- on_right_click obtém o jogador de context.event.player, verifica se ele já é passageiro (para evitar montagem dupla) e chama context.prop:mount(player) para sentá-lo no armor stand do prop.

  2. Clique esquerdo para levantar -- on_left_click cancela o evento de dano (invulnerabilidade), depois verifica se o jogador que socou é atualmente um passageiro. Se sim, context.prop:dismount(player) o ejeta.

  3. Verificação de passageiros -- context.prop:get_passengers() retorna um array de tabelas de entidade. Comparamos UUIDs para encontrar o jogador interagindo na lista.

  4. Feedback sonoro -- Um som de colocação de madeira toca ao sentar e um som de quebra de madeira ao levantar, dando feedback tátil.


Exemplo: Santuário de Bênção

O que isso ensina: Verificar o item na mão do jogador, consumir itens, aplicar efeitos de poção aleatórios, gerenciamento de cooldown e feedback de partículas/som.

Arquivo de script completo (clique para expandir)
local COOLDOWN_TICKS = 600  -- 30 segundos entre usos

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)
-- Tornar invulnerável
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

-- Verificar cooldown
if context.state.on_cooldown then
player:send_message("&eO santuário está recarregando... Por favor aguarde.")
return
end

-- Verificar se o jogador está segurando uma barra de ouro
local held = player:get_held_item()
if not held or held.type ~= "gold_ingot" then
player:send_message("&eO santuário exige uma oferenda de ouro...")
return
end

-- Consumir uma barra de ouro
player:consume_held_item(1)

-- Reproduzir efeitos de bênção
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

-- Aplicar uma bênção aleatória
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 segundos, nível II
player:send_message("&aO santuário te abençoa com " .. chosen.name .. "!")

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

Explicação

  1. Verificação de item -- player:get_held_item() retorna uma tabela com type, amount e display_name para o item na mão principal (ou nil se vazia). Comparamos held.type contra "gold_ingot" (nome de material em minúsculas).

  2. Consumo de item -- player:consume_held_item(1) remove um item do stack da mão principal do jogador.

  3. Buff aleatório -- A tabela BLESSINGS no escopo do arquivo lista efeitos positivos disponíveis. math.random(#BLESSINGS) escolhe um aleatoriamente. player:add_potion_effect(effect, duration, amplifier) o aplica -- 600 ticks são 30 segundos, amplificador 1 é nível II.

  4. Cooldown -- O mesmo padrão de flag booleana mais scheduler do exemplo do Prop Emissor de Som. Um cooldown de 30 segundos previne spam no santuário.

  5. Feedback -- Partículas de encantamento e happy-villager mais um som de ativação de beacon criam uma sensação de "bênção divina".


Exemplo: Santuário Amaldiçoado

O que isso ensina: Efeitos negativos de poção, raios, spawn de entidades e lógica de ramificação baseada em oferendas do jogador.

Arquivo de script completo (clique para expandir)
local COOLDOWN_TICKS = 600  -- 30 segundos entre usos

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)
-- Tornar invulnerável
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

-- Verificar cooldown
if context.state.on_cooldown then
player:send_message("&7O santuário sombrio pulsa com energia residual...")
return
end

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

-- Verificar se o jogador está segurando uma barra de ouro
local held = player:get_held_item()
if not held or held.type ~= "gold_ingot" then
-- Sem oferenda -- punir o jogador!
player:send_message("&4O santuário exige tributo! Você ousa se aproximar de mãos vazias?!")

-- Raio no jogador
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end

-- Spawnar uma horda de zumbis ao redor do santuário
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

-- Aplicar uma maldição aleatória
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 segundos, nível II
player:send_message("&cO santuário te amaldiçoa com " .. chosen_curse.name .. "!")

-- Efeitos sinistros
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
-- Ouro oferecido -- recompensar o jogador
player:consume_held_item(1)

-- Aplicar um buff aleatório
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 segundos, nível II
player:send_message("&aO santuário sombrio aceita sua oferenda. Você é abençoado com " .. chosen_buff.name .. "!")

-- Feedback positivo
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

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

Explicação

  1. Ramificação na oferenda -- O script verifica player:get_held_item() e segue um de dois caminhos: punição se o jogador não tem ouro, ou recompensa se tem.

  2. Raio -- context.world:strike_lightning(x, y, z) causa um raio real (com dano) na localização do jogador. A posição do jogador é lida de player.current_location.

  3. Spawn de zumbis -- context.world:spawn_entity("zombie", x, y, z) spawna zumbis vanilla. O loop os distribui uniformemente em um círculo ao redor do santuário usando trigonometria.

  4. Efeitos negativos de poção -- player:add_potion_effect("poison", 400, 1) aplica 20 segundos de Veneno II. Nomes de efeitos são strings em minúsculas correspondendo aos nomes do PotionEffectType do Bukkit.

  5. Caminho de recompensa -- Quando ouro é oferecido, o santuário consome um lingote e aplica um efeito positivo aleatório, espelhando o comportamento do Santuário de Bênção.

  6. Cooldown -- Um cooldown de 30 segundos se aplica independente de qual ramificação foi tomada, prevenindo punição ou recompensa rápida repetida.

Performance de spawn de entidades

Spawnar múltiplas entidades de uma vez pode impactar a performance do servidor. Mantenha a contagem baixa (4-6) e considere adicionar um cooldown por jogador se muitos jogadores usarem o santuário simultaneamente.


Exemplo: Globo Giratório

O que isso ensina: Reproduzir uma animação cronometrada em interação, agendar uma parada de animação e efeitos sonoros mecânicos.

Arquivo de script completo (clique para expandir)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 segundos em ticks

return {
api_version = 1,

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

on_left_click = function(context)
-- Tornar invulnerável
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Prevenir iniciar um novo giro enquanto já está girando
if context.state.is_spinning then
return
end

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

-- Iniciar a animação de giro (sem loop)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true

-- Reproduzir um som de clique mecânico
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)

-- Agendar a animação para parar após 5 segundos
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
}

Explicação

  1. Guarda de estado -- context.state.is_spinning previne múltiplas requisições de giro sobrepostas. A flag é definida quando o giro começa e limpa quando a parada agendada dispara.

  2. Animação cronometrada -- play_animation(SPIN_ANIMATION, true, false) reproduz a animação uma vez (sem loop). A chamada scheduler:run_later(100, ...) para a animação após exatamente 5 segundos, caso a própria animação seja mais longa ou em loop.

  3. Sons mecânicos -- BLOCK_CHAIN_PLACE dá um som mecânico de clique ao iniciar; BLOCK_CHAIN_FALL dá um som de desaceleração ao parar. Ajuste o tom a gosto.

  4. Context do callback -- O callback run_later usa later_context (não context) para todos os acessos de state e mundo. Este é o padrão correto para callbacks de scheduler.

Nomes de animação

O nome de animação "spin" deve corresponder ao que está definido no seu arquivo de modelo. Se seu modelo usa um nome diferente (ex: "rotate", "turn"), atualize a constante SPIN_ANIMATION de acordo.


Exemplo: Prop Susto

O que isso ensina: Gatilhos de zona de proximidade, efeitos de susto de uso único com cooldown longo e combinação de som/partículas/animação para impacto dramático.

Arquivo de script completo (clique para expandir)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 segundos entre sustos
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

-- Criar uma zona esférica pequena ao redor do prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle

-- Monitorar jogadores entrando na zona
context.zones:watch(
handle,
function(player)
-- on_enter: acionar o susto
if context.state.on_cooldown then
return
end

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

-- Reproduzir a animação de susto
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)

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

-- Rajada de partículas de fumaça
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
)

-- Definir cooldown para não acionar novamente imediatamente
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
nil -- sem callback on_leave necessário
)
end,

on_left_click = function(context)
-- Tornar invulnerável
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Limpar o monitoramento de zona
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Explicação

  1. Zona de proximidade -- context.zones:create_sphere() cria uma zona de raio de 3 blocos. context.zones:watch() registra um callback on-enter que dispara quando qualquer jogador entra.

  2. Efeitos de susto -- O callback on-enter reproduz uma animação "jumpscare", um grito de ghast e spawna partículas de fumaça de fogueira. A combinação cria um efeito súbito e assustador.

  3. Cooldown de 60 segundos -- O padrão de flag booleana previne o susto de acionar repetidamente. Uma vez acionado, o prop fica silencioso por 60 segundos (COOLDOWN_TICKS = 1200), depois rearma.

  4. Sem callback de saída -- O segundo argumento nil para context.zones:watch() significa que não nos importamos quando jogadores saem da zona.

  5. Limpeza -- on_destroy para de monitorar a zona. Embora zonas sejam limpas automaticamente quando o prop é removido, limpeza explícita é uma boa prática.

Design de susto

Para melhor efeito de susto, esconda o prop em uma esquina ou em uma área escura. O raio de 3 blocos garante que o jogador está perto antes do susto acionar. Ajuste SCARE_RADIUS e COOLDOWN_TICKS a gosto.


Exemplo: Prop Spawner de Goblin

O que isso ensina: Usar prop:spawn_elitemobs_boss() para spawnar um boss customizado a partir de uma interação de prop, com fallback gracioso se o EliteMobs não estiver instalado.

Arquivo de script completo (clique para expandir)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 segundos entre spawns

return {
api_version = 1,

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

on_left_click = function(context)
-- Tornar invulnerável
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

-- Prevenir spam de spawn
if context.state.on_cooldown then
player:send_message("&7O spawner está recarregando...")
return
end

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

-- Tentar spawnar o boss EliteMobs
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)

if boss then
-- Sucesso -- reproduzir efeitos de spawn
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("&cUm guerreiro goblin emerge!")
else
-- EliteMobs não está instalado ou o arquivo do boss não foi encontrado
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7O spawner falha... (EliteMobs não disponível)")

-- Partículas de falha como feedback visual
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

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

Explicação

  1. Spawn de boss -- context.prop:spawn_elitemobs_boss(filename, x, y, z) spawna um boss customizado do EliteMobs nas coordenadas dadas. O nome do arquivo deve corresponder a um arquivo .yml na pasta custombosses do EliteMobs.

  2. Fallback gracioso -- spawn_elitemobs_boss() retorna nil se o EliteMobs não estiver instalado ou o arquivo do boss não existir. O script lida com isso com uma mensagem de aviso no log, um efeito de partícula de falha e uma mensagem para o jogador explicando a falha.

  3. Offset de spawn -- O boss é spawnado em loc.y + 1 (um bloco acima do prop) para prevenir que o boss fique preso no prop ou no chão.

  4. Cooldown -- Um cooldown de 10 segundos previne jogadores de inundar a área com guerreiros goblin. Ajuste COOLDOWN_TICKS baseado nas suas necessidades de gameplay.

  5. Distinção visual/sonora -- Sucesso usa partículas de chama e um som de invocação de evoker para um spawn dramático. Falha usa fumaça e extinção de fogo para um efeito claro de "falha", para que o jogador saiba que algo deu errado sem verificar o console.

Integração com EliteMobs

O nome do arquivo do boss (ex: "goblin_warrior.yml") deve corresponder a uma configuração de boss customizado existente no EliteMobs. Se você está distribuindo um mapa ou dungeon que usa este script, inclua o arquivo de configuração do boss e documente a dependência do EliteMobs.


Exemplo: Espada de Onda de Gelo (Script de Item)

O que isso ensina: Um script de item completo demonstrando inicialização de estado em on_equip, on_shift_right_click para acionar uma habilidade, scheduler:run_repeating() para uma onda em expansão, get_nearby_entities() para encontrar alvos, métodos de combate de entidades, efeitos de partículas/som, modificação de terreno com get_highest_block_y(), show_action_bar() para feedback de UI, e proteção contra entidades não-vivas.

Arquivo de script completo (clique para expandir)
-- Frost Shockwave Sword
-- Shift + Right-click to release a horizontal frost wave

local WAVE_MAX_RADIUS = 12
local WAVE_SPEED = 2
local WAVE_TICK_INTERVAL = 2
local WAVE_DAMAGE = 4.0
local WAVE_KNOCKBACK = 1.2
local WAVE_CONE_ANGLE = 70

return {
api_version = 1,

on_equip = function(context)
context.state.wave_active = false
end,

on_shift_right_click = function(context)
if context.state.wave_active then return end
local loc = context.player.current_location
local yaw_rad = math.rad(loc.yaw)
local dir_x = -math.sin(yaw_rad)
local dir_z = math.cos(yaw_rad)

context.event:cancel()
context.state.wave_active = true
context.state.hit_entities = {}
context.state.destroyed_blocks = {}

local current_radius = 1
local origin_x, origin_y, origin_z = loc.x, loc.y, loc.z

context.player:show_action_bar("&b&lFrost Shockwave!", 40)
context.world:play_sound("ENTITY_PLAYER_ATTACK_SWEEP", origin_x, origin_y, origin_z, 1.5, 0.5)

local task_id = context.scheduler:run_repeating(0, WAVE_TICK_INTERVAL, function()
if current_radius > WAVE_MAX_RADIUS then
context.state.wave_active = false
context.scheduler:cancel(context.state.wave_task)
return
end

local half_angle = math.rad(WAVE_CONE_ANGLE / 2)
local steps = math.floor(current_radius * 8)
for i = 0, steps do
local angle = (i / steps) * 2 * math.pi
local px = origin_x + math.cos(angle) * current_radius
local pz = origin_z + math.sin(angle) * current_radius
local to_x, to_z = px - origin_x, pz - origin_z
local to_len = math.sqrt(to_x * to_x + to_z * to_z)
if to_len > 0 then
local dot = (to_x * dir_x + to_z * dir_z) / to_len
if dot >= math.cos(half_angle) then
local ground_y = context.world:get_highest_block_y(math.floor(px), math.floor(pz))
context.world:spawn_particle("SNOWFLAKE", px, ground_y + 1.0, pz, 3, 0.3, 0.2, 0.3, 0.02)
local bx, bz = math.floor(px), math.floor(pz)
local block_key = bx .. "," .. bz
if not context.state.destroyed_blocks[block_key] then
context.state.destroyed_blocks[block_key] = true
local block_type = context.world:get_block_at(bx, ground_y, bz)
if block_type ~= "bedrock" and block_type ~= "obsidian" and block_type ~= "barrier" then
context.world:set_block_at(bx, ground_y, bz, "AIR")
end
end
end
end
end

local entities = context.world:get_nearby_entities(origin_x, origin_y, origin_z, current_radius + 1)
for _, entity in ipairs(entities) do
if entity.uuid ~= context.player.uuid and not context.state.hit_entities[entity.uuid] then
local eloc = entity.current_location
local dx, dz = eloc.x - origin_x, eloc.z - origin_z
local dist = math.sqrt(dx * dx + dz * dz)
if dist >= current_radius - 2 and dist <= current_radius + 1 and dist > 0 then
local dot = (dx * dir_x + dz * dir_z) / dist
if dot >= math.cos(half_angle) and entity.damage then
context.state.hit_entities[entity.uuid] = true
entity:damage(WAVE_DAMAGE)
entity:push((dx/dist)*WAVE_KNOCKBACK, 0.3, (dz/dist)*WAVE_KNOCKBACK)
if entity.is_alive then entity:add_potion_effect("SLOWNESS", 60, 1) end
context.world:spawn_particle("SNOWFLAKE", eloc.x, eloc.y+1, eloc.z, 10, 0.5, 0.5, 0.5, 0.1)
end
end
end
end

context.world:play_sound("BLOCK_GLASS_BREAK",
origin_x + dir_x * current_radius, origin_y,
origin_z + dir_z * current_radius, 0.8, 1.5)
current_radius = current_radius + WAVE_SPEED
end)

context.state.wave_task = task_id
end
}

Explicação

  1. Inicialização de estado -- on_equip define wave_active = false para prevenir múltiplas ondas de choque sobrepostas. Este hook dispara quando o jogador equipa a espada.

  2. Cone direcional -- O yaw do jogador é convertido em um vetor de direção (dir_x, dir_z). Um cone de 70 graus na frente do jogador determina a propagação da onda.

  3. Onda em expansão -- scheduler:run_repeating() roda a cada 2 ticks. A cada tick, o raio da onda cresce por WAVE_SPEED. Partículas são spawnadas em um anel, mas apenas dentro do ângulo do cone.

  4. Destruição de terreno -- get_highest_block_y() encontra o nível do chão em cada ponto. Blocos de superfície são destruídos (definidos como AIR), com bedrock/obsidian/barreira excluídos. Uma tabela destroyed_blocks previne processamento duplicado.

  5. Seleção de alvos -- get_nearby_entities() encontra todas as entidades próximas da origem. O script verifica que cada entidade está dentro da faixa de onda atual, dentro do cone, e ainda não foi atingida. A guarda if entity.damage then é crítica -- get_nearby_entities() retorna TODAS as entidades (armor stands, itens, etc.), não apenas as vivas.

  6. Efeitos de combate -- Entidades atingidas recebem dano, são empurradas na direção da onda e recebem 3 segundos de Lentidão II.

  7. Feedback para o jogador -- show_action_bar() exibe "Frost Shockwave!" por 40 ticks. Sons de vidro quebrando avançam com a frente da onda.

  8. Limpeza -- Quando o raio excede WAVE_MAX_RADIUS, a tarefa repetitiva se cancela e reseta a flag wave_active.

Segurança de tipo de entidade

Sempre verifique if entity.damage then antes de chamar entity:damage(), entity:push() ou entity:add_potion_effect(). O método get_nearby_entities() retorna todas as entidades no alcance, incluindo não-vivas como armor stands, itens dropados e orbes de experiência que não possuem esses métodos.


Exemplo: Varinha de Cura (Script de Item)

O que isso ensina: Um script de item mais simples mostrando ativação por clique direito, gerenciamento de cooldown, o método item:consume() para usos limitados, e cura com efeitos de poção.

Arquivo de script completo (clique para expandir)
-- Healing Wand
-- Right-click to heal yourself. Consumes one charge per use.

local HEAL_AMOUNT = 6.0 -- 3 hearts
local COOLDOWN_TICKS = 60 -- 3 seconds
local REGEN_DURATION = 40 -- 2 seconds of regen
local REGEN_AMPLIFIER = 1 -- Regeneration II

return {
api_version = 1,

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

on_right_click = function(context)
-- Check cooldown
if context.state.on_cooldown then
context.player:show_action_bar("&cWand recharging...", 20)
return
end

-- Check remaining charges
local uses = context.item:get_uses()
if uses <= 0 then
context.player:show_action_bar("&c&lOut of charges!", 40)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH",
context.player.current_location.x,
context.player.current_location.y,
context.player.current_location.z,
0.8, 1.5)
return
end

-- Cancel the event so we don't interact with blocks
if context.event then
context.event:cancel()
end

-- Consume one charge
context.item:set_uses(uses - 1)

-- Heal the player
local loc = context.player.current_location
context.player:add_potion_effect("REGENERATION", REGEN_DURATION, REGEN_AMPLIFIER)

-- Visual and audio feedback
context.world:spawn_particle("HEART", loc.x, loc.y + 1.5, loc.z, 8, 0.4, 0.3, 0.4, 0)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 15, 0.5, 0.5, 0.5, 0)
context.world:play_sound("ENTITY_PLAYER_LEVELUP", loc.x, loc.y, loc.z, 0.7, 1.8)

-- UI feedback
context.player:show_action_bar("&a&lHealed! &7(" .. (uses - 1) .. " charges left)", 40)

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

Explicação

  1. Padrão de cooldown -- Mesma abordagem de flag booleana usada em scripts de props. on_equip inicializa a flag, on_right_click a verifica, e scheduler:run_later() a reseta após o cooldown.

  2. Sistema de cargas -- context.item:get_uses() lê um contador de uso customizado do PDC do item. Cada uso o decrementa com set_uses(). Quando as cargas chegam a 0, a varinha reproduz um som de falha e se recusa a ativar.

  3. Cura -- Em vez de definir a saúde diretamente, o script aplica Regeneração II por 2 segundos. Isso funciona mais naturalmente com o sistema de saúde do Minecraft e empilha corretamente com outros efeitos.

  4. Cancelamento de evento -- context.event:cancel() previne o clique direito de interagir com blocos próximos (colocar blocos, abrir portas, etc.).

  5. Feedback de UI -- show_action_bar() informa o jogador do resultado da ação e cargas restantes. Mensagens diferentes para cooldown, sem cargas e cura bem-sucedida fazem o item parecer responsivo.

Obtendo itens customizados

Use /fmm giveitem <id> para obter um item customizado com as tags corretas. Itens criados por outros meios podem não ter a chave PDC fmm_item_id, o que significa que os scripts não serão ativados para eles.


Boas Práticas

  • Comece com um hook pequeno e verifique. Escreva um único on_spawn que envia uma mensagem de log. Confirme que dispara. Então construa a partir daí.

  • Mantenha funções auxiliares locais. Declare auxiliares como local function toggle_door(context) acima da tabela de retorno. Isso os mantém fora do escopo global.

  • Inicialize todo o state em on_spawn. Se você lê context.state.is_open em on_right_click mas nunca o define em on_spawn, será nil e suas comparações podem se comportar inesperadamente.

  • Cancele tarefas repetitivas quando terminar. Cada run_repeating deve ter um cancel correspondente em on_destroy. Tarefas vazadas desperdiçam CPU.

  • Use contexts novos nos callbacks de scheduler. Callbacks de scheduler recebem um parâmetro de context novo. Sempre use esse parâmetro dentro do callback, não o context externo.

  • Mantenha on_game_tick leve. Se você define este hook, ele roda a cada tick do servidor (20 vezes por segundo). Bloqueie trabalho pesado atrás de uma verificação de cooldown baseada em estado.

  • Torne props invulneráveis por padrão. A menos que queira que o prop seja quebrável, inclua o cancelamento de dano de on_left_click em todo script.

  • Use MAIÚSCULAS para enums do Bukkit. Nomes de sons e partículas devem usar o formato de constante de enum do Bukkit (ex: "FLAME", não "flame").


Erros Comuns de Iniciantes

  • Usar o context externo dentro de um callback de scheduler. O context externo captura um snapshot do momento em que o hook rodou. Dentro de callbacks, sempre use o parâmetro próprio do callback.

  • Esquecer de cancelar tarefas repetitivas. Se você inicia um run_repeating em on_spawn mas nunca o cancela, a tarefa roda até o prop ser removido.

  • Não inicializar state em on_spawn. Ler context.state.x antes de defini-lo retorna nil, o que pode quebrar sua lógica silenciosamente.

  • Nomes de animação errados. Se play_animation("open") retorna false, o nome da animação não corresponde ao que está no arquivo de modelo. Verifique o modelo para os nomes exatos.

  • Nomes de som/partícula em minúsculas. "flame" não funciona -- use "FLAME". A API converte para MAIÚSCULAS internamente para partículas, mas nomes de enum de Sound devem ser exatos.

  • Esquecer api_version = 1. A tabela retornada deve incluir este campo, ou o FMM não carregará o script.

  • Colocar funções dentro da tabela retornada que não são hooks. Funções auxiliares devem ser declaradas acima do return. Apenas nomes de hooks (on_spawn, on_right_click, etc.) são permitidos como chaves na tabela retornada.


Checklist de QC

Use esta checklist para verificar um script de prop antes de implantá-lo:

  1. O arquivo retorna exatamente uma tabela com api_version = 1.
  2. Todo nome de hook corresponde a uma entrada na lista de hooks de props ou lista de hooks de itens exatamente.
  3. context.event é protegido com if context.event then antes de chamar cancel().
  4. Campos de context.state são inicializados em on_spawn.
  5. Toda chamada scheduler:run_repeating(...) tem um scheduler:cancel(...) correspondente em on_destroy.
  6. Callbacks de scheduler usam o parâmetro de context próprio do callback, não o context externo.
  7. Hooks on_game_tick bloqueiam trabalho pesado atrás de uma verificação.
  8. Todos os nomes de método existem na referência da API de Props e Itens -- sem aliases inventados.
  9. Nomes de som e partícula usam nomes de enum Bukkit em MAIÚSCULAS.
  10. O script não chama operações bloqueantes ou de longa duração dentro de um hook ou callback.

Dicas para Geração por IA

Se você quer que uma IA gere scripts de props de forma confiável, certifique-se de que o prompt inclui:

  • Nome exato do hook -- ex: on_right_click, não "quando o jogador clica no prop".
  • Nomes de animação do arquivo de modelo -- a IA não pode adivinhar; forneça-os.
  • Nomes de enum de som -- ex: "BLOCK_NOTE_BLOCK_HARP", não "som de harpa".
  • Nomes de enum de partícula -- ex: "FLAME", não "partículas de fogo".
  • Se o prop deve ser invulnerável -- se sim, inclua on_left_click com context.event.cancel().
  • Use apenas nomes de método documentados -- se não está na página da API de Props, não existe.

Bom exemplo de prompt

Escreva um script de prop FMM que reproduz a animação "activate" quando clicado com botão direito, torna o prop invulnerável, spawna partículas FLAME na localização do prop ao clicar, reproduz o som BLOCK_LEVER_CLICK, e tem um cooldown de 2 segundos entre cliques usando context.state e scheduler:run_later.


Coleção de Itens Goblin

A pasta scripts/ vem com um conjunto de 10 scripts de item de exemplo com tema de armas goblin. Cada script demonstra diferentes mecânicas de combate, efeitos de partículas e padrões de uso da API.

ScriptItemEfeito
goblin_golden_sword.luaGolden Sword25% de chance de varredura -- causa dano e empurra todos os inimigos próximos
goblin_iron_axe.luaIron Axe20% de chance de espinho -- lança o alvo para cima com visual de dripstone
goblin_bow.luaBowPuxa o mob atingido em direção ao jogador
goblin_crossbow.luaCrossbow33% de chance de barragem de fogos de artifício
goblin_trident.luaTridentDomo de gelo encapsula o alvo por 3s
goblin_iron_hoe.luaIron Hoe15% de chance de nuvens de veneno
goblin_mace.luaMaceMeteoros caem do céu (clique direito)
goblin_spear.luaSpearFeixe de fogo na direção da mira (shift+clique direito)
goblin_shield.luaShieldDomo de vidro ao bloquear (reativo)
goblin_iron_sword.luaIron SwordAnel de escuridão carregando (shift+clique direito)
Nenhum modelo necessário

Estes scripts funcionam com itens de aparência vanilla. Cada um precisa apenas de uma configuração YML com um campo material: -- nenhum arquivo .bbmodel é necessário. Por exemplo, para ativar o script da espada dourada, crie uma configuração YML como:

isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua

Próximos Passos