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 do FreeMinecraftModels, além de padrões práticos e boas práticas. Cada exemplo inclui uma explicação detalhada do que faz e porquê.

Se você é novo no scripting de props, comece com Primeiros Passos. Para detalhes completos da API, consulte a API de Props.


Exemplo: Prop Invulnerável

O que este exemplo ensina: O script útil mais simples -- cancelar dano para que o prop não possa ser destruído.

Este é o script pré-definido que acompanha 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 passo a passo

  1. Escolha do hook -- on_left_click dispara quando um jogador ataca (clique esquerdo) o prop. Internamente, trata-se de um EntityDamageByEntityEvent no armor stand subjacente do prop.

  2. Verificação do evento -- context.event deveria estar sempre presente neste hook, mas a verificação é uma boa prática.

  3. Cancelamento -- context.event.cancel() cancela o evento de dano, impedindo que o armor stand sofra dano e seja destruído.

Uso

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

isEnabled: true
scripts:
- invulnerable.lua

Exemplo: Porta Interativa

O que este exemplo ensina: Alternar estado com clique 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)
-- 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
}

Explicação passo a passo

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

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

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

  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 a 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 posição do prop. Os nomes de sons devem ser nomes de enum em UPPER_CASE.

Nomes de animação

Nomes de animação como "open" e "close" devem corresponder ao que está definido no arquivo do 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 este exemplo 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 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
}

Explicação passo a passo

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

  2. Inicialização do estado -- on_spawn define 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 posteriormente.

  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 gera partículas em anel ao redor do prop a cada meio segundo. O tipo de partícula muda conforme haja jogadores na zona ou não.

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

Desempenho de partículas

Gerar muitas partículas a cada tick pode impactar o desempenho. Use um intervalo razoável (10-20 ticks) e mantenha as quantidades de partículas baixas. 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 este exemplo ensina: Reproduzir sons na interação, comportamento tipo cooldown usando state e scheduler, e prevenção de interações rápidas repetidas.

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

Explicação passo a passo

  1. Padrão de cooldown -- Como os scripts de props do FMM não possuem uma API context.cooldowns integrada 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 é reproduzido, e uma tarefa atrasada a redefine após COOLDOWN_TICKS.

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

  3. Feedback com partículas -- Partículas de nota aparecem acima do prop quando o som é reproduzido, fornecendo uma indicação visual.

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

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

Implementação de cooldown

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


Exemplo: Prop Ambiental Animado

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

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

Explicação passo a passo

  1. Início automático da animação -- on_spawn reproduz imediatamente uma animação "idle" em loop. O false para blend significa que começa do zero sem misturar com uma animação anterior. O true para loop significa que se repete indefinidamente.

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

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


Boas Práticas

  • Comece com um hook pequeno e verifique. Escreva um único on_spawn que envie uma mensagem de log. Confirme que ele dispara. Depois construa a partir daí.

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

  • Inicialize todo o estado em on_spawn. Se você lê context.state.is_open em on_right_click mas nunca o define em on_spawn, ele 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 não canceladas desperdiçam CPU.

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

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

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

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


Erros Comuns de Iniciantes

  • Usar o context externo dentro de um callback do scheduler. O contexto externo captura um snapshot do momento em que o hook foi executado. Dentro dos 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 executa até o prop ser removido.

  • Não inicializar estado 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 do modelo. Verifique o modelo para os nomes exatos.

  • Nomes de sons/partículas em minúsculas. "flame" não funciona -- use "FLAME". A API converte para UPPER_CASE 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 da instrução return. Apenas nomes de hooks (on_spawn, on_right_click, etc.) são permitidos como chaves na tabela retornada.


Checklist 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. Cada nome de hook corresponde exatamente a uma entrada na lista de hooks.
  3. context.event é verificado com if context.event then antes de chamar cancel().
  4. Os campos de context.state são inicializados em on_spawn.
  5. Cada chamada a scheduler:run_repeating(...) tem um scheduler:cancel(...) correspondente em on_destroy.
  6. Callbacks do scheduler usam o parâmetro de contexto próprio do callback, não o context externo.
  7. Hooks on_game_tick protegem trabalho custoso atrás de uma verificação.
  8. Todos os nomes de métodos existem na referência da API de Props -- sem aliases inventados.
  9. Nomes de sons e partículas usam nomes de enum UPPER_CASE do Bukkit.
  10. O script não chama operações bloqueantes ou de longa duração dentro de um hook ou callback.

Dicas para Geração com IA

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

  • Nome exato do hook -- ex: on_right_click, não "quando o jogador clica no prop".
  • Nomes de animação do arquivo do modelo -- a IA não consegue adivinhar esses nomes; forneça-os.
  • Nomes de enum de som -- ex: "BLOCK_NOTE_BLOCK_HARP", não "som de harpa".
  • Nomes de enum de partículas -- ex: "FLAME", não "partículas de fogo".
  • Se o prop deve ser invulnerável -- se sim, incluir on_left_click com context.event.cancel().
  • Usar apenas nomes de métodos 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 reproduza a animação "activate" ao clicar com botão direito, torne o prop invulnerável, gere partículas FLAME na posição do prop ao clicar, reproduza o som BLOCK_LEVER_CLICK e tenha um cooldown de 2 segundos entre cliques usando context.state e scheduler:run_later.


Próximos Passos