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
-
Escolha do hook --
on_left_clickdispara quando um jogador soca (clica com botão esquerdo) o prop. Por baixo dos panos, este é umEntityDamageByEntityEventno armor stand de suporte do prop. -
Guarda de evento --
context.eventdeveria estar sempre presente neste hook, mas a guarda é boa prática. -
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
-
Constantes no escopo do arquivo --
OPEN_ANIMATIONeCLOSE_ANIMATIONsã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. -
Inicialização de estado --
on_spawndefinecontext.state.is_open = false. O estado persiste entre todos os hooks para esta instância de prop. -
Invulnerabilidade -- O hook
on_left_clickcancela dano para que a porta não possa ser acidentalmente quebrada. -
Lógica de alternância --
on_right_clickverificacontext.state.is_open, para qualquer animação atual, reproduz a animação apropriada, inverte o estado e reproduz um som. A chamadastop_animation()antes deplay_animation()garante transições limpas. -
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 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
-
Constantes --
ZONE_RADIUSePARTICLE_INTERVALestão no escopo do arquivo para fácil ajuste. -
Inicialização de estado --
on_spawnconfigura todos os campos de estado comonil/0antes de fazer qualquer outra coisa. -
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. -
Monitoramento de zona --
context.zones:watch()registra callbacks para entrada e saída de jogadores. Os callbacks incrementam e decrementam um contador armazenado emcontext.state. -
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.
-
Limpeza --
on_destroycancela 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.
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
-
Padrão de cooldown -- Como scripts de props do FMM não têm uma API embutida
context.cooldownscomo o EliteMobs, o exemplo implementa um cooldown simples usandocontext.state.on_cooldownescheduler:run_later(). A flag é definida comotruequando o som toca, e uma tarefa atrasada a reseta apósCOOLDOWN_TICKS. -
Reprodução de som --
context.world:play_sound()recebe o nome do enum Sound do Bukkit em MAIÚSCULAS, coordenadas, volume e tom. -
Feedback de partículas -- Partículas de nota aparecem acima do prop quando o som toca, dando uma dica visual.
-
Invulnerabilidade -- O hook
on_left_clickcancela dano como de costume. -
Context do callback do scheduler -- O callback
run_laterrecebelater_context, um context novo. Usamoslater_context.state(nãocontext.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.
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
-
Animação automática ao iniciar --
on_spawnimediatamente reproduz uma animação"idle"em loop. Ofalsepara blend significa que inicia do zero sem blending de uma animação anterior. Otruepara loop significa que repete indefinidamente. -
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.
-
Limpeza --
on_destroycancela 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
-
Clique direito para sentar --
on_right_clickobtém o jogador decontext.event.player, verifica se ele já é passageiro (para evitar montagem dupla) e chamacontext.prop:mount(player)para sentá-lo no armor stand do prop. -
Clique esquerdo para levantar --
on_left_clickcancela o evento de dano (invulnerabilidade), depois verifica se o jogador que socou é atualmente um passageiro. Se sim,context.prop:dismount(player)o ejeta. -
Verificação de passageiros --
context.prop:get_passengers()retorna um array de tabelas de entidade. Comparamos UUIDs para encontrar o jogador interagindo na lista. -
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
-
Verificação de item --
player:get_held_item()retorna uma tabela comtype,amountedisplay_namepara o item na mão principal (ounilse vazia). Comparamosheld.typecontra"gold_ingot"(nome de material em minúsculas). -
Consumo de item --
player:consume_held_item(1)remove um item do stack da mão principal do jogador. -
Buff aleatório -- A tabela
BLESSINGSno escopo do arquivo lista efeitos positivos disponíveis.math.random(#BLESSINGS)escolhe um aleatoriamente.player:add_potion_effect(effect, duration, amplifier)o aplica --600ticks são 30 segundos, amplificador1é nível II. -
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.
-
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
-
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. -
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 deplayer.current_location. -
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. -
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 doPotionEffectTypedo Bukkit. -
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.
-
Cooldown -- Um cooldown de 30 segundos se aplica independente de qual ramificação foi tomada, prevenindo punição ou recompensa rápida repetida.
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
-
Guarda de estado --
context.state.is_spinningprevine múltiplas requisições de giro sobrepostas. A flag é definida quando o giro começa e limpa quando a parada agendada dispara. -
Animação cronometrada --
play_animation(SPIN_ANIMATION, true, false)reproduz a animação uma vez (sem loop). A chamadascheduler:run_later(100, ...)para a animação após exatamente 5 segundos, caso a própria animação seja mais longa ou em loop. -
Sons mecânicos --
BLOCK_CHAIN_PLACEdá um som mecânico de clique ao iniciar;BLOCK_CHAIN_FALLdá um som de desaceleração ao parar. Ajuste o tom a gosto. -
Context do callback -- O callback
run_laterusalater_context(nãocontext) para todos os acessos de state e mundo. Este é o padrão correto para callbacks de scheduler.
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
-
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. -
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. -
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. -
Sem callback de saída -- O segundo argumento
nilparacontext.zones:watch()significa que não nos importamos quando jogadores saem da zona. -
Limpeza --
on_destroypara de monitorar a zona. Embora zonas sejam limpas automaticamente quando o prop é removido, limpeza explícita é uma boa prática.
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
-
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.ymlna pastacustombossesdo EliteMobs. -
Fallback gracioso --
spawn_elitemobs_boss()retornanilse 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. -
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. -
Cooldown -- Um cooldown de 10 segundos previne jogadores de inundar a área com guerreiros goblin. Ajuste
COOLDOWN_TICKSbaseado nas suas necessidades de gameplay. -
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.
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
-
Inicialização de estado --
on_equipdefinewave_active = falsepara prevenir múltiplas ondas de choque sobrepostas. Este hook dispara quando o jogador equipa a espada. -
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. -
Onda em expansão --
scheduler:run_repeating()roda a cada 2 ticks. A cada tick, o raio da onda cresce porWAVE_SPEED. Partículas são spawnadas em um anel, mas apenas dentro do ângulo do cone. -
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 tabeladestroyed_blocksprevine processamento duplicado. -
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 guardaif entity.damage thené crítica --get_nearby_entities()retorna TODAS as entidades (armor stands, itens, etc.), não apenas as vivas. -
Efeitos de combate -- Entidades atingidas recebem dano, são empurradas na direção da onda e recebem 3 segundos de Lentidão II.
-
Feedback para o jogador --
show_action_bar()exibe "Frost Shockwave!" por 40 ticks. Sons de vidro quebrando avançam com a frente da onda. -
Limpeza -- Quando o raio excede
WAVE_MAX_RADIUS, a tarefa repetitiva se cancela e reseta a flagwave_active.
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
-
Padrão de cooldown -- Mesma abordagem de flag booleana usada em scripts de props.
on_equipinicializa a flag,on_right_clicka verifica, escheduler:run_later()a reseta após o cooldown. -
Sistema de cargas --
context.item:get_uses()lê um contador de uso customizado do PDC do item. Cada uso o decrementa comset_uses(). Quando as cargas chegam a 0, a varinha reproduz um som de falha e se recusa a ativar. -
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.
-
Cancelamento de evento --
context.event:cancel()previne o clique direito de interagir com blocos próximos (colocar blocos, abrir portas, etc.). -
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.
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_spawnque 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_openemon_right_clickmas nunca o define emon_spawn, seránile suas comparações podem se comportar inesperadamente. -
Cancele tarefas repetitivas quando terminar. Cada
run_repeatingdeve ter umcancelcorrespondente emon_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
contextexterno. -
Mantenha
on_game_tickleve. 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_clickem 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
contextexterno 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_repeatingemon_spawnmas nunca o cancela, a tarefa roda até o prop ser removido. -
Não inicializar state em
on_spawn. Lercontext.state.xantes de defini-lo retornanil, o que pode quebrar sua lógica silenciosamente. -
Nomes de animação errados. Se
play_animation("open")retornafalse, 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:
- O arquivo retorna exatamente uma tabela com
api_version = 1. - Todo nome de hook corresponde a uma entrada na lista de hooks de props ou lista de hooks de itens exatamente.
context.eventé protegido comif context.event thenantes de chamarcancel().- Campos de
context.statesão inicializados emon_spawn. - Toda chamada
scheduler:run_repeating(...)tem umscheduler:cancel(...)correspondente emon_destroy. - Callbacks de scheduler usam o parâmetro de context próprio do callback, não o
contextexterno. - Hooks
on_game_tickbloqueiam trabalho pesado atrás de uma verificação. - Todos os nomes de método existem na referência da API de Props e Itens -- sem aliases inventados.
- Nomes de som e partícula usam nomes de enum Bukkit em MAIÚSCULAS.
- 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_clickcomcontext.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.
| Script | Item | Efeito |
|---|---|---|
goblin_golden_sword.lua | Golden Sword | 25% de chance de varredura -- causa dano e empurra todos os inimigos próximos |
goblin_iron_axe.lua | Iron Axe | 20% de chance de espinho -- lança o alvo para cima com visual de dripstone |
goblin_bow.lua | Bow | Puxa o mob atingido em direção ao jogador |
goblin_crossbow.lua | Crossbow | 33% de chance de barragem de fogos de artifício |
goblin_trident.lua | Trident | Domo de gelo encapsula o alvo por 3s |
goblin_iron_hoe.lua | Iron Hoe | 15% de chance de nuvens de veneno |
goblin_mace.lua | Mace | Meteoros caem do céu (clique direito) |
goblin_spear.lua | Spear | Feixe de fogo na direção da mira (shift+clique direito) |
goblin_shield.lua | Shield | Domo de vidro ao bloquear (reativo) |
goblin_iron_sword.lua | Iron Sword | Anel de escuridão carregando (shift+clique direito) |
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