Pular para o conteúdo principal

Scripting Lua: Exemplos e padrões

Esta página contém exemplos completos e funcionais de poderes Lua do EliteMobs, além de padrões práticos, melhores práticas e dicas. Cada exemplo inclui uma explicação do que faz e por quê.

Se você é novo em poderes Lua, comece com Primeiros passos. Para detalhes completos da API, consulte a Referência da API, Boss e entidades, Mundo e ambiente, Zonas e alvos e Enums.

webapp_banner.jpg


Exemplo: Seleção de alvos baseada em zonas com utilitários de script

O que este exemplo ensina: Como usar context.script para criar geometria de zona estilo EliteScript a partir de Lua, gerar partículas e causar dano a entidades na zona.

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

on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = {
targetType = "SELF",
offset = "0,1,0"
},
Target2 = {
targetType = "NEARBY_PLAYERS",
range = 20
},
radius = 5
})

context.script:spawn_particles(
cone:full_target(0.4),
{
particle = "FLAME",
amount = 1,
speed = 0.05
}
)

context.script:damage(cone:full_target(), 1.0, 1.5)
end
}

Explicação

  1. Criação de zona -- context.script:zone(...) cria uma forma de cone usando os mesmos nomes de campo que as Zonas do EliteScript. Target define a origem do cone (o próprio boss, deslocado 1 bloco acima), e Target2 define o destino (jogadores mais próximos dentro de 20 blocos). radius controla a abertura do cone.

  2. Geração de partículas -- cone:full_target(0.4) retorna um handle de alvo que resolve para todas as localizações dentro do cone com 40% de cobertura.

  3. Dano -- context.script:damage(cone:full_target(), 1.0, 1.5) atinge todas as entidades vivas dentro do cone completo. O primeiro número (1.0) é a quantidade de dano base, e o segundo (1.5) é o multiplicador de dano aplicado aos jogadores.

Nomes de campo do EliteScript

As tabelas de zona e alvo passadas a context.script usam nomes de campo do EliteScript (targetType, shape, Target, Target2, range, offset, coverage). Para a lista completa, consulte Zonas do EliteScript e Alvos do EliteScript.


Exemplo: Loop de ataque com estado e scheduler

O que este exemplo ensina: Uso de context.state para rastrear estado em tempo de execução, context.scheduler para tarefas repetitivas e ciclo de vida adequado de entrada/saída de combate.

Arquivo de poder completo (clique para expandir)
local function pick_action(context)
local roll = math.random(1, 2)

if roll == 1 then
context.boss:play_model_animation("slam")
else
context.boss:play_model_animation("roar")
end
end

return {
api_version = 1,

on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,

on_enter_combat = function(context)
if context.state.started then
return
end

context.state.started = true

context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then
pick_action(loop_context)
end
end)
end,

on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}

Explicação

  1. Inicialização de estado -- on_spawn define context.state.started como false e context.state.loop_task_id como nil. A tabela state persiste durante toda a vida útil desta instância do boss.
  2. Guarda de combate -- on_enter_combat verifica context.state.started antes de iniciar o loop, prevenindo loops múltiplos sobrepostos.
  3. Padrão de scheduler -- context.scheduler:run_every(100, callback) executa o callback a cada 100 ticks (5 segundos). O callback recebe um contexto fresco.
  4. Limpeza na saída -- on_exit_combat cancela a tarefa repetitiva e reinicia o estado.
Sempre cancele tarefas do scheduler

Se você iniciar uma tarefa repetitiva em on_enter_combat, sempre cancele-a em on_exit_combat. Esquecer de cancelar deixa uma tarefa em segundo plano executando até o boss desaparecer.


Exemplo: Efeito de fogo ao atingir

O que este exemplo ensina: Um simples poder de "ao atingir aplicar efeito" -- o padrão mais comum para poderes de combate.

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

on_player_damaged_by_boss = function(context)
-- Guard: the player may be nil in edge cases
if context.player == nil then
return
end

-- Check and set a 60-tick (3 second) local cooldown in one call
if not context.cooldowns:check_local("fire_touch", 60) then
return
end

-- Set the player on fire for 60 ticks (3 seconds)
context.player:set_fire_ticks(60)

-- Visual feedback: spawn flame particles at the player's location
context.world:spawn_particle_at_location(
context.player.current_location,
{ particle = "FLAME", amount = 20, speed = 0.1 }
)

-- Tell the player what happened
context.player:send_message("&cThe boss's touch burns!")

-- Set the global power cooldown so other powers on this boss
-- don't all fire at the same instant
context.cooldowns:set_global(40)
end
}

Explicação

  1. Guarda nil -- context.player pode ser nil em casos extremos raros. Sempre verifique antes de usar.
  2. Cooldown local -- context.cooldowns:check_local("fire_touch", 60) verifica e define atomicamente o cooldown.
  3. Ticks de fogo -- context.player:set_fire_ticks(60) coloca o jogador em chamas por 60 ticks (3 segundos).
  4. Partículas -- context.world:spawn_particle_at_location(location, spec) gera partículas em uma localização.
  5. Mensagem -- context.player:send_message(text) envia uma mensagem de chat com códigos de cor.
  6. Cooldown global -- context.cooldowns:set_global(40) coloca todos os poderes deste boss em cooldown de 40 ticks.

Exemplo: Poder AoE baseado em zonas usando zonas nativas Lua

O que este exemplo ensina: Criar e consultar zonas nativas Lua para causar dano a jogadores em uma área.

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

on_spawn = function(context)
context.state.aoe_task_id = nil
end,

on_enter_combat = function(context)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end

-- Check a local cooldown so this doesn't stack with other effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end

-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}

-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

-- Damage and show particles on each player found
for i = 1, #victims do
local victim = victims[i]
victim:deal_custom_damage(4.0)

tick_context.world:spawn_particle_at_location(
victim.current_location,
{ particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 }
)
end

-- Spawn visual ring particles at the boss
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)

-- Set global cooldown
tick_context.cooldowns:set_global(60)
end)
end,

on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}

Explicação

  1. Configuração de estado -- on_spawn inicializa aoe_task_id como nil.
  2. Ataque repetitivo -- on_enter_combat inicia uma tarefa repetitiva a cada 60 ticks (3 segundos).
  3. Definição de zona -- A tabela zone_def usa a sintaxe de zona nativa Lua com kind, radius e origin.
  4. Consulta de entidades -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" }) retorna um array Lua com todas as tabelas de jogador dentro da esfera. A opção filter aceita "players", "elites", "mobs" ou "living" (padrão).
  5. Causar dano -- victim:deal_custom_damage(4.0) causa 4 pontos de dano atribuídos ao boss.
  6. Feedback de partículas -- Partículas de poeira roxa em cada vítima, partículas ambientais na posição do boss.
  7. Limpeza -- on_exit_combat cancela a tarefa e limpa o estado.
Zonas nativas vs. zonas de utilitários de script

Este exemplo usa zonas nativas Lua (context.zones:get_entities_in_zone()). Os utilitários de script (context.script:zone(...)) usam nomes de campo do EliteScript como shape, Target e Target2. Ambos funcionam -- use zonas nativas para formas simples e context.script para resolução avançada de alvos.


Exemplo: Mecânica de boss multifase

O que este exemplo ensina: Uso de estado para rastrear fases do boss e mudar comportamento em limiares de saúde.

Arquivo de poder completo (clique para expandir)
local function phase_one_attack(context)
-- Slow, heavy slam
context.boss:play_model_animation("slam")

local zone_def = {
kind = "sphere",
radius = 5,
origin = context.boss:get_location()
}

local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(3.0)
end

context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "EXPLOSION", amount = 3, speed = 0 }
)
end

local function phase_two_attack(context)
-- Fast, frantic multi-hit
context.boss:play_model_animation("frenzy")

local zone_def = {
kind = "sphere",
radius = 8,
origin = context.boss:get_location()
}

local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end

context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 }
)

context.world:play_sound_at_location(
context.boss:get_location(),
"entity.wither.ambient",
1.0, 1.5
)
end

local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
end

return {
api_version = 1,

on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
context.state.phase_switched = false
end,

on_enter_combat = function(context)
start_attack_loop(context)
end,

on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end

-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end

-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health

if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2
context.state.phase_switched = true

context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")

-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end

-- Play transition effects
context.boss:play_model_animation("transform")

-- Announce the phase change to nearby players
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end

start_attack_loop(context)
end
end,

on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}

Explicação

  1. Inicialização de estado -- on_spawn define phase como 1, attack_task_id como nil e phase_switched como false.
  2. Ciclo de ataque -- on_enter_combat retoma a fase atual através de start_attack_loop: a fase 1 ataca a cada 100 ticks e a fase 2 a cada 40. Sair e voltar a entrar em combate mantém a fase. O ataque da fase 1 cria uma esfera de 5 blocos, causa dano aos jogadores no interior e reproduz uma animação de golpe com partículas de explosão.
  3. Verificação de fase em on_game_tick -- on_game_tick termina imediatamente fora de combate e depois verifica um cooldown de 20 ticks ("phase_check"). Se o chefe já estiver na fase 2, também termina antecipadamente.
  4. Limiar de saúde -- context.boss.health / context.boss.maximum_health dá a saúde atual como fração. A 50% ou menos, a transição começa.
  5. Transição de fase -- O loop antigo é cancelado, animação de transformação é reproduzida, jogadores próximos recebem mensagem e título, e um novo loop mais rápido inicia (a cada 40 ticks em vez de 100).
  6. Ataques fase 2 -- Esfera maior (8 blocos), dano ligeiramente menor por golpe mas muito mais frequente, aplica efeito de lentidão, usa partículas de poeira vermelha e som de wither.
  7. Registro -- context.log:info(...) escreve no console do servidor.
  8. Limpeza -- on_exit_combat cancela qualquer loop de ataque ativo, independentemente da fase.
Mantenha on_game_tick leve

on_game_tick é executado a cada tick do servidor (20 vezes por segundo). Limite as operações dispendiosas com um cooldown, como check_local("phase_check", 20). Exceder o orçamento de execução desativa o poder.


Dicas para geração com IA

Se quer que a IA gere poderes Lua de forma confiável, certifique-se de que o prompt inclua:

  • Nome exato do hook -- por exemplo, on_player_damaged_by_boss, e não "quando o boss acerta um jogador".
  • Zonas nativas Lua ou utilitários de script -- especifique qual. Zonas nativas usam context.zones:get_entities_in_zone(zone_def, opts) com kind, radius, origin. Utilitários de script usam context.script:zone(...) com nomes de campo do EliteScript como shape, Target, Target2.
  • Cooldowns locais e globais -- especifique o nome da chave de cooldown e a duração em ticks. context.cooldowns:check_local(key, ticks) para cooldowns por boss, context.cooldowns:set_global(ticks) para o cooldown de poder compartilhado.
  • Nomes de animação de modelo personalizado -- por exemplo, context.boss:play_model_animation("slam"). A IA não consegue adivinhar nomes de animação; forneça-os.
  • Seleção de alvo -- especifique se o poder mira context.player (o jogador do evento), context.players.nearby_players(range) ou uma consulta de zona.
  • Tipo de efeito -- dano (deal_custom_damage), poção (apply_potion_effect), fogo (set_fire_ticks), velocidade (set_velocity_vector, apply_push_vector), etc.
  • Use apenas nomes de método documentados -- se não está na página de Referência da API, não existe.
  • Especificações de utilitários de script usam nomes de campo do EliteScript -- targetType, shape, Target, Target2, range, offset, coverage, e não nomes em estilo Lua.
  • Callbacks do scheduler recebem um contexto novo -- o parâmetro do callback é um contexto novo, não o externo. Sempre use tick_context (ou o nome que você der ao parâmetro) dentro dos callbacks.

Bom exemplo de prompt

Escreva um poder Lua que use on_enter_combat para iniciar uma tarefa repetitiva a cada 80 ticks. A cada tick, crie uma zona de esfera nativa Lua (raio 6) centrada no boss, consulte os jogadores dentro e cause 2.0 de dano personalizado a cada um. Use uma chave de cooldown local "pulse" com duração 80. Cancele a tarefa em on_exit_combat. Gere partículas DUST (red=0, green=255, blue=100) em cada vítima.

Restrições extras a incluir

  • "Retorne uma única tabela com api_version = 1."
  • "Inicialize todos os campos de estado em on_spawn."
  • "Proteja os callbacks do scheduler com if not tick_context.boss.exists then return end."
  • "Cancele todas as tarefas repetitivas em on_exit_combat."
  • "Não invente nomes de método -- use apenas métodos da página de Referência da API."
  • "Use context.cooldowns:check_local(key, ticks) para checar e definir de uma vez."

Lista de verificação QC

Use esta lista para verificar um poder Lua antes de implantá-lo:

  1. O arquivo retorna exatamente uma tabela com api_version = 1.
  2. Cada nome de hook coincide exatamente com uma entrada da lista de hooks (por exemplo, on_player_damaged_by_boss, e não on_player_hit).
  3. context.player é protegido com == nil antes do uso, nos hooks em que pode ser nil.
  4. Campos de context.state são inicializados em on_spawn.
  5. Cada chamada context.scheduler:run_every(...) tem um context.scheduler:cancel_task(...) correspondente em on_exit_combat.
  6. Callbacks do scheduler usam o parâmetro de contexto do próprio callback, não o context externo.
  7. Chaves de cooldown são strings descritivas (por exemplo, "fire_pulse") e durações estão em ticks.
  8. Hooks on_game_tick protegem trabalho pesado atrás de verificação de cooldown.
  9. Todos os nomes de métodos existem na Referência da API -- sem aliases inventados.
  10. Tabelas de utilitários de script usam nomes de campo do EliteScript (targetType, shape, Target, etc.), e não nomes em estilo Lua.
  11. Definições de zona nativas usam kind, radius, origin, destination, etc.
  12. O poder não chama operações bloqueantes ou de longa duração dentro de um hook ou callback.
  13. Especificações de partículas usam nomes de enum de partículas Bukkit válidos em UPPER_CASE (por exemplo, "FLAME", "DUST", "EXPLOSION").

Melhores práticas

  • Comece com um hook pequeno e verifique. Escreva um unico on_spawn que envie uma mensagem de log. Confirme que dispara. Depois construa a partir dai.

  • Mantenha funcoes auxiliares locais. Declare helpers como local function pick_action(context) acima da tabela de retorno. Isso os mantem fora do escopo global e evita colisoes com outros poderes Lua carregados no mesmo runtime.

  • Coloque geometria nos utilitarios de script. Se voce precisa de cones, raios rotativos, raios em translacao ou zonas animadas, use context.script:zone(...) com nomes de campo do EliteScript. Os utilitarios de script reutilizam o motor de zonas testado em batalha do EliteScript.

  • Use context.state para estado em tempo de execucao. Nao use variaveis globais Lua. context.state e limitado a uma unica instancia de boss e persiste entre hooks durante a vida desse boss.

  • Use chaves de cooldown locais nomeadas. Em vez de numeros simples, use chaves descritivas como "fire_touch" ou "aoe_pulse". Isso facilita a depuracao e previne colisoes acidentais entre diferentes cooldowns no mesmo poder.

  • Mantenha on_game_tick leve. Limite as operações dispendiosas com um cooldown e fique bem abaixo do orçamento de execução.

  • Cancele tarefas repetitivas quando terminar. Cada run_every deve ter um cancel_task correspondente em on_exit_combat (e possivelmente on_death). Tarefas nao canceladas desperdicam CPU e podem causar erros de referencia nula apos o boss desaparecer.

  • Use contextos frescos nos callbacks do scheduler. Callbacks do scheduler (run_every, run_after) recebem um contexto fresco como parametro. Sempre use esse parametro -- nao o context externo -- porque o contexto externo pode conter snapshots obsoletos.

  • Registre transicoes de estado com context.log:info(). Durante o desenvolvimento, adicione logging para mudancas de fase, inicios de cooldown e inicios/paradas do scheduler. Remova ou mude para context.log:debug() antes do deploy.

  • Reutilize documentacao existente do EliteScript. As paginas de Zonas, Alvos, Vetores relativos e Condicoes documentam os mesmos nomes de campo que os utilitarios de script aceitam. Nao duplique essa informacao no seu poder Lua -- simplesmente faca referencia a ela.


Erros comuns de iniciante

  • Usar o context externo dentro de um callback do scheduler. O contexto externo captura um snapshot no momento em que o hook foi executado. Dentro de um callback run_every ou run_after, sempre use o parametro proprio do callback (ex., tick_context), que lhe da um snapshot fresco.

  • Esquecer de cancelar tarefas repetitivas. Se voce inicia um run_every em on_enter_combat mas nunca o cancela, a tarefa continua rodando ate o boss ser removido do servidor, mesmo apos o combate terminar.

  • Nao inicializar estado em on_spawn. Se voce le context.state.phase em on_game_tick mas nunca o define em on_spawn, sera nil e suas comparacoes se comportarao inesperadamente.

  • Verificar context.player sem guarda nil. Em hooks como on_player_damaged_by_boss, o jogador esta quase sempre disponivel -- mas "quase sempre" nao e "sempre". Um unico guarda nil faltando pode crashar o poder.

  • Usar nomes de campo estilo Lua em chamadas de utilitarios de script. Os utilitarios de script esperam targetType, Target, Target2, shape -- nao target_type, target, target2, zone_shape. Nomes nao correspondentes silenciosamente nao produzem resultados.

  • Executar logica pesada em on_game_tick sem gate de cooldown. Este hook dispara a cada tick do servidor. Mesmo aritmetica simples repetida 20 vezes por segundo em muitos bosses se acumula.

  • Inventar nomes de metodos. Se um metodo nao esta listado na Referencia da API, nao existe. Erros comuns incluem escrever entity:teleport(loc) em vez de entity:teleport_to_location(loc), ou player:set_velocity(vec) em vez de player:set_velocity_vector(vec).

  • Usar context.boss.health para definir saude. context.boss.health e um snapshot somente leitura. Para curar o boss, use context.boss:restore_health(amount).

  • Esquecer api_version = 1. A tabela retornada deve incluir este campo, ou EliteMobs nao carregara o poder.


Próximos passos