Saltar para o conteúdo principal

Scripts Lua: exemplos e padrões

Esta página apresenta exemplos de scripts de elementos decorativos do FreeMinecraftModels, com explicações e boas práticas. Forneça os modelos e as animações referidos e verifique o comportamento no servidor antes de os usar num mapa.

Se está a começar a criar scripts, consulte Primeiros passos. Para conhecer a API em detalhe, consulte a API de elementos decorativos.


Exemplo: elemento decorativo invulnerável​

O que este exemplo ensina: O script útil mais simples: cancelar o dano predefinido do clique esquerdo.

Guarde este exemplo como invulnerable.lua na pasta de scripts e recarregue os scripts antes de o associar a um elemento decorativo.

Ficheiro 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 evento — on_left_click é executado quando um jogador dá um soco no elemento decorativo, clicando com o botão esquerdo. O evento Lua representa o sinalizador de cancelamento usado pelo FMM para o seu tratamento predefinido de dano em elementos decorativos.

  2. Verificação do evento — context.event deve estar sempre presente neste evento, mas verificá-lo é uma boa prática.

  3. Cancelamento — context.event.cancel() impede que o FMM aplique o dano predefinido deste clique. Não concede invulnerabilidade geral contra todas as fontes de dano.

Utilização​

Adicione o seguinte à configuração .yml do elemento decorativo:

isEnabled: true
scripts:
- invulnerable.lua

Exemplo: porta interativa​

O que este exemplo ensina: Alternar o estado com o clique direito, reproduzir e parar animações e usar context.state para registar se a porta está aberta ou fechada.

Ficheiro 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)
-- Prevent default left-click damage
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​

  1. Constantes ao nível do ficheiro — OPEN_ANIMATION e CLOSE_ANIMATION são definidas antes da tabela devolvida. Assim, é fácil adaptá-las a ficheiros de modelos que usem outros nomes de animação.

  2. Inicialização do estado — on_spawn define context.state.is_open = false. O estado mantém-se entre os eventos desta associação de script ao elemento decorativo. Outros scripts associados ao mesmo elemento têm o seu próprio estado.

  3. Proteção contra cliques — O evento on_left_click cancela o dano predefinido desse clique.

  4. Alternância — on_right_click verifica context.state.is_open, pede a paragem e a animação adequada, inverte o sinalizador e reproduz um som. O sinalizador regista o pedido, não confirma a reprodução. Com blend = true, o pedido entra na fila; stop_animation() não interrompe o estado terminal death. Consulte Animações. Este exemplo não altera barreiras nem colisões e não garante que se possa atravessar a porta.

  5. Os sons aceitam nomes de enumerações Bukkit, independentemente de maiúsculas e minúsculas, ou chaves de som válidas como minecraft:block.note_block.harp e chaves personalizadas de pacotes de recursos. O cliente tem de ter o som no seu pacote de recursos.

Nomes das animações

Os nomes "open" e "close" têm de corresponder às definições no modelo. Se não existirem, play_animation() devolve false, mas este exemplo continua a inverter o sinalizador e a reproduzir o som. Verifique o resultado se o estado depender da aceitação do pedido. Mesmo true não confirma a reprodução visível.


Exemplo: ativação por proximidade com partículas​

O que este exemplo ensina: Criar zonas, observar as entradas e saídas dessas zonas, produzir efeitos de partículas e libertar os recursos utilizados.

Ficheiro 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

-- Pass placeholder functions to enable on_zone_enter and on_zone_leave.
-- The actual logic lives in the hooks below.
context.zones:watch(handle, function() end, function() 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
-- Flame particles when players are inside
tick_context.world:spawn_particle("FLAME", 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)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_zone_enter = function(context)
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,

on_zone_leave = function(context)
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
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​

  1. Constantes — ZONE_RADIUS e PARTICLE_INTERVAL ficam ao nível do ficheiro para facilitar os ajustes.

  2. Inicialização do estado — Antes de executar qualquer outra operação, on_spawn inicializa todos os campos de estado com nil ou 0.

  3. Criação da zona — context.zones:create_sphere() cria uma zona esférica centrada na posição do elemento no momento de on_spawn. O identificador numérico referencia essa zona fixa. As partículas consultam a posição atual do elemento, mas a zona não o acompanha. Use um elemento imóvel ou atualize a zona quando o mover.

  4. Observação da zona — context.zones:watch(handle, function() end, function() end) ativa os eventos on_zone_enter e on_zone_leave. Estes eventos incrementam e decrementam um contador guardado em context.state; também recebem o jogador que atravessa o limite em context.player / context.event.player, caso pretenda um comportamento específico por jogador.

  5. Ciclo de partículas — Uma tarefa repetida cria um anel de partículas a cada 10 ticks do servidor, meio segundo a 20 TPS. O tipo de partícula varia consoante existam jogadores dentro da zona.

  6. Limpeza — on_destroy cancela a tarefa repetida e deixa de observar a zona. Embora ambos sejam limpos automaticamente quando o elemento decorativo é removido, a limpeza explícita é uma boa prática.

Desempenho das partículas

Criar muitas partículas em cada tick pode afetar o desempenho. Use um intervalo razoável de 10 a 20 ticks e mantenha o número de partículas baixo. O exemplo usa PARTICLE_INTERVAL = 10, duas vezes por segundo a 20 TPS, com 12 partículas por anel.


Exemplo: elemento decorativo que emite som​

O que este exemplo ensina: Reproduzir sons ao interagir, usar context.cooldowns e impedir interações demasiado rápidas.

Ficheiro 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_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Prevent spam
if not context.cooldowns:check_local("sound", COOLDOWN_TICKS) 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)
end
}

Explicação​

  1. Intervalo entre utilizações — context.cooldowns:check_local("sound", COOLDOWN_TICKS) verifica o intervalo e inicia-o imediatamente se estiver disponível. Todos os jogadores partilham este intervalo na mesma associação de script. Usa tempo decorrido, com 50 ms por tick, ao contrário dos ticks efetivos do servidor usados pelo agendador.

  2. Os sons aceitam nomes de enumerações Bukkit, independentemente de maiúsculas e minúsculas, ou chaves de som válidas como minecraft:block.note_block.harp e chaves personalizadas de pacotes de recursos. O cliente tem de ter o som no seu pacote de recursos.

  3. Resposta visual — Quando o som é reproduzido, surgem partículas de notas por cima do elemento decorativo para sinalizar a interação.

  4. Proteção contra cliques — O evento on_left_click cancela o dano predefinido desse clique.

Implementação dos intervalos

Use context.cooldowns para intervalos de espera normais. Reserve context.state em conjunto com scheduler:run_later() para os casos em que precisa de alterar o estado de uma forma específica quando a espera termina.


Exemplo: elemento decorativo com animação ambiente​

O que este exemplo ensina: Iniciar uma animação em ciclo quando o elemento é criado e emitir partículas a intervalos medidos em ticks.

Ficheiro 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​

  1. Início automático da animação — on_spawn reproduz imediatamente a animação "idle" em ciclo. O valor false do parâmetro de transição faz a mudança de imediato. O estado integrado idle repete-se independentemente do argumento de repetição fornecido.

  2. Partículas ambiente — Uma tarefa repetida cria partículas de mesa de encantamentos por cima do elemento decorativo a cada 2 segundos, produzindo um ambiente mágico.

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


Exemplo: cadeira onde os jogadores se podem sentar​

O que este exemplo ensina: Montar um jogador num elemento decorativo com o clique direito, desmontá-lo com o clique esquerdo e usar context.event.player para obter o jogador que está a interagir.

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

on_right_click = function(context)
-- Handle seating here instead of also using the default mount fallback
if context.event then
context.event.cancel()
end

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

-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end

-- Mount the player on the chair
context.prop:mount(player)

local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,

on_left_click = function(context)
-- Cancel default left-click damage
if context.event then
context.event.cancel()
end

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

-- Check if the player is sitting and dismount them
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)

local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}

Explicação​

  1. Clique direito para sentar — on_right_click obtém o jogador através de context.event.player, verifica se já é passageiro para evitar uma segunda montagem e chama context.prop:mount(player) para pedir o primeiro assento disponível nos pontos de montagem m_.

  2. Clique esquerdo para levantar — on_left_click cancela o dano predefinido desse clique e verifica se o jogador que deu o soco é passageiro. Se for, context.prop:dismount(player) retira-o do assento.

  3. Verificação dos passageiros — context.prop:get_passengers() devolve uma lista de tabelas de entidades. A comparação dos UUIDs permite encontrar o jogador na lista.

  4. Resposta sonora — Os sons acompanham os pedidos de montagem e desmontagem colocados na fila; não confirmam que tenham sido executados. O script cancela o comportamento predefinido de montagem para evitar uma segunda tentativa. É necessário um ponto de montagem m_ livre.


Exemplo: santuário de bênçãos​

O que este exemplo ensina: Verificar o item que o jogador segura, consumir itens, aplicar efeitos de poção aleatórios, gerir intervalos de espera e produzir respostas visuais e sonoras.

Ficheiro de script completo (clique para expandir)
local COOLDOWN_TICKS = 600  -- 30 seconds between uses

local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}

return {
api_version = 1,

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

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end

-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end

-- Consume one gold ingot
player:consume_held_item(1)

-- Play blessing effects
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end

-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")

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

Explicação​

  1. Verificação do item — player:get_held_item() devolve uma tabela com type, amount e display_name para o item na mão principal, ou nil se estiver vazia. held.type é o nome do material Bukkit em maiúsculas, pelo que o comparamos com "GOLD_INGOT".

  2. Consumo do item — player:consume_held_item(1) coloca a remoção da mão principal numa fila. A mão é consultada quando a tarefa é executada e a chamada não devolve um resultado de pagamento. Esta demonstração não é uma troca atómica: mudar o item na mão pode alterar o que é consumido, enquanto a bênção é aplicada imediatamente.

  3. Efeito positivo aleatório — A tabela BLESSINGS, definida ao nível do ficheiro, lista os efeitos positivos disponíveis. math.random(#BLESSINGS) escolhe um deles aleatoriamente. player:add_potion_effect(effect, duration, amplifier) aplica-o: 600 ticks correspondem a 30 segundos e o amplificador 1 corresponde ao nível II.

  4. Intervalo de espera — Todos os jogadores partilham o sinalizador desta associação de script. A função agendada repõe-no após 600 ticks do servidor, 30 segundos a 20 TPS. Para medir tempo decorrido, use context.cooldowns, como no exemplo do som.

  5. Resposta visual e sonora — As partículas de encantamento e de aldeão feliz, juntamente com o som de ativação de um sinalizador, dão a sensação de uma bênção divina.


Exemplo: santuário amaldiçoado​

O que este exemplo ensina: Aplicar efeitos de poção negativos, lançar raios, criar entidades e escolher comportamentos consoante a oferenda do jogador.

Ficheiro de script completo (clique para expandir)
local COOLDOWN_TICKS = 600  -- 30 seconds between uses

local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}

local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}

local ZOMBIE_COUNT = 4

return {
api_version = 1,

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

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end

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

-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")

-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end

-- Spawn a horde of zombies around the shrine
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end

-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")

-- Ominous effects
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold offered -- reward the player
player:consume_held_item(1)

-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")

-- Positive feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end

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

Explicação​

  1. Decisão com base na oferenda — O script consulta player:get_held_item() e segue um de dois caminhos: castiga o jogador se não tiver ouro ou recompensa-o se tiver.

  2. Raio — context.world:strike_lightning(x, y, z) lança um raio real, que causa dano, na localização do jogador. A posição do jogador é obtida através de player.current_location.

  3. Criação de zombies — context.world:spawn_entity("zombie", x, y, z) cria zombies normais, distribuídos num círculo à volta do santuário. Não verifica o terreno nem limita quantos continuam vivos. Remover o elemento decorativo não remove estes zombies.

  4. Efeitos de poção negativos — player:add_potion_effect("poison", 400, 1) aplica Veneno II durante 20 segundos. Os nomes dos efeitos são cadeias de texto em minúsculas que correspondem aos nomes de PotionEffectType do Bukkit.

  5. Recompensa — Quando é oferecido ouro, o santuário coloca o consumo da mão principal na fila e aplica um efeito positivo aleatório. Tal como no santuário de bênçãos, esta demonstração não é uma troca atómica.

  6. Intervalo de espera — Ambos os caminhos usam o mesmo sinalizador, partilhado por todos os jogadores nesta associação de script. É reposto após 600 ticks do servidor, 30 segundos a 20 TPS.

Desempenho da criação de entidades

Criar várias entidades ao mesmo tempo pode afetar o desempenho do servidor. Mantenha a quantidade baixa, entre 4 e 6, e considere acrescentar um intervalo por jogador se muitos jogadores usarem o santuário em simultâneo.


Exemplo: globo giratório​

O que este exemplo ensina: Reproduzir uma animação com duração limitada ao interagir, agendar a sua paragem e usar efeitos sonoros mecânicos.

Ficheiro de script completo (clique para expandir)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 seconds in ticks

return {
api_version = 1,

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

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Prevent starting a new spin while already spinning
if context.state.is_spinning then
return
end

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

-- Start the spin animation (non-looping)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true

-- Play a mechanical clicking sound
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)

-- Schedule the animation to stop after 5 seconds
context.scheduler:run_later(SPIN_DURATION, function(later_context)
later_context.prop:stop_animation()
later_context.state.is_spinning = false

local stop_loc = later_context.prop.current_location
if stop_loc then
later_context.world:play_sound("BLOCK_CHAIN_FALL", stop_loc.x, stop_loc.y, stop_loc.z, 1.0, 0.8)
end
end)
end
}

Explicação​

  1. Verificação do estado — context.state.is_spinning impede pedidos de rotação sobrepostos. O sinalizador é ativado quando a rotação começa e desativado quando a paragem agendada é executada.

  2. Animação com duração limitada — play_animation(SPIN_ANIMATION, true, false) coloca na fila uma animação sem repetição. A paragem agendada ocorre 100 ticks do servidor após o clique, 5 segundos a 20 TPS, e não após o início da reprodução. Uma fila ocupada pode fazer a paragem cortar ou impedir a animação. O estado terminal death não pode ser parado desta forma. O exemplo ativa o sinalizador mesmo que o pedido devolva false.

  3. Sons mecânicos — BLOCK_CHAIN_PLACE produz um som de clique mecânico no início; BLOCK_CHAIN_FALL produz um som de desaceleração no fim. Ajuste a altura do som conforme pretendido.

  4. Contexto da função agendada — A função passada a run_later usa later_context, em vez de context, para todos os acessos ao estado e ao mundo. Este é o padrão correto para funções agendadas.

Nomes das animações

O nome da animação "spin" tem de corresponder ao definido no ficheiro do modelo. Se o modelo usar outro nome, como "rotate" ou "turn", atualize a constante SPIN_ANIMATION em conformidade.


Exemplo: elemento decorativo que assusta​

O que este exemplo ensina: Ativar comportamentos por zonas de proximidade, produzir um susto com um intervalo de espera longo e combinar som, partículas e animação para criar um efeito marcante.

Ficheiro de script completo (clique para expandir)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
local SCARE_ANIMATION = "jumpscare"

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil

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

-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle

-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,

on_zone_enter = function(context)
if context.state.on_cooldown then
return
end

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

-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)

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

-- Burst of smoke particles
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)

-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Explicação​

  1. Zona de proximidade — context.zones:create_sphere() cria uma zona fixa com 3 blocos de raio na posição do elemento em on_spawn. context.zones:watch(handle, function() end, nil) ativa on_zone_enter quando um jogador entra. Se mover o elemento, mova ou recrie a zona.

  2. Efeitos do susto — O evento on_zone_enter reproduz uma animação "jumpscare" e um grito de ghast, e cria partículas de fumo de fogueira. A combinação produz um efeito súbito que surpreende o jogador.

  3. Intervalo de 60 segundos — Todos os jogadores partilham o sinalizador desta associação de script. É reposto após 1200 ticks do servidor, 60 segundos a 20 TPS. Depois disso, uma nova entrada na zona pode ativar o susto.

  4. Sem evento de saída — Passar nil como terceiro argumento de context.zones:watch() indica que a saída dos jogadores da zona não precisa de ser tratada.

  5. Limpeza — on_destroy deixa de observar a zona. Embora as zonas sejam limpas automaticamente quando o elemento decorativo é removido, a limpeza explícita é uma boa prática.

Preparar o susto

Para obter o melhor efeito, esconda o elemento decorativo atrás de uma esquina ou numa zona escura. O raio de 3 blocos garante que o jogador está perto antes da ativação. Ajuste SCARE_RADIUS e COOLDOWN_TICKS conforme pretendido.


Exemplo: elemento decorativo que cria goblins​

O que este exemplo ensina: Usar prop:spawn_elitemobs_boss() para criar um boss personalizado ao interagir com um elemento decorativo e tratar a ausência do EliteMobs de forma controlada.

Ficheiro de script completo (clique para expandir)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 seconds between spawns

return {
api_version = 1,

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

on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end

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

-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)

if boss then
-- Success -- play spawn effects
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")

-- Fizzle particles as visual feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end

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

Explicação​

  1. Criação do boss — context.prop:spawn_elitemobs_boss(filename, x, y, z) cria um boss personalizado do EliteMobs nas coordenadas indicadas. O nome do ficheiro tem de corresponder a um ficheiro .yml na pasta custombosses do EliteMobs.

  2. Tratamento da falha — spawn_elitemobs_boss() devolve nil quando a integração não consegue devolver um boss vivo, incluindo plugin indisponível, configuração em falta ou falha na criação. O script regista um aviso e produz partículas e uma mensagem. A mensagem sugere verificar a instalação ou o ficheiro; não identifica a causa exata.

  3. Deslocamento da posição — loc.y + 1 cria o boss um bloco acima da origem do elemento. Isto não verifica terreno nem colisões. Escolha um ponto de criação com espaço livre.

  4. Intervalo de espera — Todos os jogadores partilham uma tentativa a cada 200 ticks do servidor nesta associação de script, 10 segundos a 20 TPS. Isto não limita os bosses vivos nem os remove quando o elemento desaparece. Acrescente os limites necessários ao encontro.

  5. Distinção visual e sonora — Em caso de sucesso, as partículas de chama e o som de invocação do evocador destacam a criação do boss. Em caso de falha, o fumo e o som de apagar fogo deixam claro que algo correu mal, sem que o jogador precise de consultar a consola.

Integração com EliteMobs

O nome do ficheiro do boss, por exemplo "goblin_warrior.yml", tem de corresponder a uma configuração de boss personalizado existente no EliteMobs. Se distribuir um mapa ou uma masmorra que use este script, inclua o ficheiro de configuração do boss e documente a dependência do EliteMobs.


Efeitos de itens​

Os exemplos de scripts de itens equipados foram retirados. Para os efeitos de itens atuais, use definições de itens personalizados e encantamentos personalizados.

Boas práticas​

  • Comece por um evento pequeno e verifique-o. Escreva apenas um on_spawn que envie uma mensagem para o registo. Confirme que é executado e depois desenvolva o script a partir daí.

  • Mantenha as funções auxiliares locais. Declare funções como local function toggle_door(context) antes da tabela devolvida, para as manter fora do âmbito global.

  • Inicialize todo o estado em on_spawn. Se consultar context.state.is_open em on_right_click sem o definir em on_spawn, o valor será nil e as comparações podem ter resultados inesperados.

  • Cancele as tarefas repetidas quando terminarem. Cada run_repeating deve ter um cancel correspondente em on_destroy. As tarefas que ficam a executar sem necessidade desperdiçam CPU.

  • Use o contexto atualizado da função agendada. As funções agendadas recebem um novo parâmetro de contexto. Use sempre esse parâmetro dentro da função, em vez do context exterior.

  • Mantenha on_game_tick leve. Se definir este evento, ele será executado em cada tick do servidor, 20 vezes por segundo a 20 TPS. Limite o trabalho dispendioso com context.cooldowns ou com um intervalo do agendador.

  • Controle o dano do clique esquerdo de forma explícita. Inclua o cancelamento em on_left_click quando os socos não devam causar o dano predefinido no elemento decorativo; isto não bloqueia todas as fontes de dano.

  • Os nomes de partículas aceitam FLAME ou flame; a API normaliza maiúsculas e minúsculas antes de procurar a partícula Bukkit. Os sons aceitam nomes de enumerações Bukkit, independentemente de maiúsculas e minúsculas, ou chaves de som válidas como minecraft:block.note_block.harp e chaves personalizadas de pacotes de recursos. O cliente tem de ter o som no seu pacote de recursos.


Erros comuns de principiantes​

  • Usar o context exterior dentro de uma função agendada. O contexto exterior contém uma imagem do estado no momento em que o evento foi executado. Dentro das funções agendadas, use sempre o respetivo parâmetro de contexto.

  • Esquecer-se de cancelar tarefas repetidas. Se iniciar um run_repeating em on_spawn e nunca o cancelar, a tarefa continuará a executar até o elemento decorativo ser removido.

  • Não inicializar o estado em on_spawn. Ler context.state.x antes de o definir devolve nil, o que pode quebrar a lógica sem produzir um erro evidente.

  • Usar nomes de animação incorretos. Se play_animation("open") devolver false, o nome da animação não corresponde ao que existe no ficheiro do modelo. Consulte o modelo para obter os nomes exatos.

  • Os nomes de partículas aceitam FLAME ou flame; a API normaliza maiúsculas e minúsculas antes de procurar a partícula Bukkit. Os sons aceitam nomes de enumerações Bukkit, independentemente de maiúsculas e minúsculas, ou chaves de som válidas como minecraft:block.note_block.harp e chaves personalizadas de pacotes de recursos. O cliente tem de ter o som no seu pacote de recursos.

  • Esquecer-se de api_version = 1. A tabela devolvida tem de incluir este campo; caso contrário, o FMM não carrega o script.

  • Colocar funções auxiliares na tabela devolvida. Declare-as como funções locais antes de return. Além de api_version e do campo opcional priority, a tabela só aceita funções de eventos suportados. O FMM executa os scripts pela ordem da lista configurada; priority não altera essa ordem.


Lista de verificação de qualidade​

Use esta lista para verificar um script de elemento decorativo antes de o colocar em produção:

  1. O ficheiro devolve exatamente uma tabela com api_version = 1.
  2. O nome de cada evento corresponde exatamente a uma entrada na lista de eventos de elementos decorativos. Os eventos de encantamentos pertencem a um sistema de execução separado.
  3. Verifique context.event e context.event.cancel antes de cancelar fora de um evento de clique conhecido.
  4. Os campos de context.state são inicializados em on_spawn.
  5. Cada chamada scheduler:run_repeating(...) tem um scheduler:cancel(...) correspondente em on_destroy.
  6. As funções agendadas usam o seu próprio parâmetro de contexto, em vez do context exterior.
  7. Os eventos on_game_tick condicionam o trabalho dispendioso a uma verificação.
  8. Todos os nomes de métodos existem na referência da API de elementos decorativos; não são usados nomes alternativos inventados.
  9. Os nomes de partículas aceitam FLAME ou flame; a API normaliza maiúsculas e minúsculas antes de procurar a partícula Bukkit. Os sons aceitam nomes de enumerações Bukkit, independentemente de maiúsculas e minúsculas, ou chaves de som válidas como minecraft:block.note_block.harp e chaves personalizadas de pacotes de recursos. O cliente tem de ter o som no seu pacote de recursos.
  10. O script não executa operações bloqueantes ou demoradas dentro de um evento ou de uma função agendada.

Dicas para gerar scripts com IA​

Para que uma IA gere scripts de elementos decorativos de forma fiável, inclua no pedido:

  • O nome exato do evento — Por exemplo, on_right_click, em vez de apenas «quando o jogador clica no elemento decorativo».
  • Os nomes das animações do ficheiro do modelo — A IA não os pode adivinhar; forneça-os.
  • Os nomes das enumerações dos sons — Por exemplo, "BLOCK_NOTE_BLOCK_HARP", em vez de «som de harpa».
  • Os nomes das enumerações das partículas — Por exemplo, "FLAME", em vez de «partículas de fogo».
  • Se os cliques esquerdos devem causar o dano predefinido — Cancele esse tratamento com on_left_click e context.event.cancel().
  • A exigência de usar métodos verificados — Prefira os métodos da referência da API de elementos decorativos. Se um método não estiver listado, verifique a sua implementação antes de o usar; não invente nomes alternativos.

Exemplo de um bom pedido​

Escreve um script de elemento decorativo FMM que reproduza a animação "activate" com o clique direito, cancele o dano predefinido do clique esquerdo, crie partículas FLAME na localização do elemento ao clicar, reproduza o som BLOCK_LEVER_CLICK e imponha um intervalo de 2 segundos entre cliques através de context.cooldowns:check_local("activate", 40).


Compatibilidade do conteúdo de itens​

Os pacotes de itens antigos que usam scripts: precisam de definições de itens e conteúdo de encantamentos atualizados. O FMM ignora essas definições retiradas e deixa os ficheiros inalterados. Adicionar um campo material: já não ativa um script de item.

Próximos passos​