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)
if context.player == nil then return end
if not context.cooldowns:check_local("fire_touch", 60) then return end
context.player:set_fire_ticks(60)
context.world:spawn_particle_at_location(context.player.current_location, { particle = "FLAME", amount = 20, speed = 0.1 })
context.player:send_message("&cThe boss's touch burns!")
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)
if context.state.aoe_task_id ~= nil then return end
context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then return end
local zone_def = { kind = "sphere", radius = 8, origin = tick_context.boss:get_location() }
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #victims do
victims[i]:deal_custom_damage(4.0)
tick_context.world:spawn_particle_at_location(victims[i].current_location, { particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 })
end
tick_context.world:spawn_particle_at_location(tick_context.boss:get_location(), { particle = "SPELL_MOB", amount = 40, speed = 0.1 })
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 -- Retorna um array Lua de todos os jogadores dentro da esfera.
  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)
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)
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

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)
if context.state.attack_task_id ~= nil then return end
context.state.attack_task_id = context.scheduler:run_every(100, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 100) then return end
phase_one_attack(tick_context)
end)
end,
on_game_tick = function(context)
if not context.cooldowns:check_local("phase_check", 20) then return end
if context.state.phase ~= 1 then return end
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
context.state.phase = 2
context.state.phase_switched = true
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
context.boss:play_model_animation("transform")
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
context.state.attack_task_id = context.scheduler:run_every(40, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 40) then return end
phase_two_attack(tick_context)
end)
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. Loop de ataque fase 1 -- on_enter_combat inicia uma tarefa repetitiva que executa phase_one_attack a cada 100 ticks.
  3. Verificação de fase em on_game_tick -- Verifica um cooldown de 20 ticks para evitar lógica pesada a cada tick.
  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 executa a cada tick do servidor (20 vezes por segundo). Sempre proteja trabalho pesado atrás de uma verificação de cooldown. Se seu hook exceder 50ms, o EliteMobs desativará automaticamente 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, zonas nativas Lua ou utilitários de script (especifique qual), cooldowns locais e globais com chave e duração em ticks, nomes de animação de modelo personalizado, seleção de alvo, tipo de efeito, e apenas nomes de métodos documentados.

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.


Lista de verificação QC

  1. O arquivo retorna exatamente uma tabela com api_version = 1.
  2. Nomes de hook coincidem exatamente com a lista de hooks.
  3. context.player é protegido com == nil antes do uso.
  4. Campos de context.state são inicializados em on_spawn.
  5. Cada run_every tem um cancel_task correspondente em on_exit_combat.
  6. Callbacks do scheduler usam o parâmetro de contexto do callback, não o context externo.
  7. Chaves de cooldown são strings descritivas 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.
  10. Tabelas de utilitários de script usam nomes de campo do EliteScript.
  11. Definições de zona nativas usam kind, radius, origin, destination, etc.
  12. O poder não chama operações bloqueantes dentro de hooks ou callbacks.
  13. Especificações de partículas usam nomes de enum de partículas Bukkit válidos em UPPER_CASE.

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. Sempre proteja atras de uma verificacao de cooldown. Se sua logica executa a cada tick, deve completar em bem menos de 50ms ou o poder sera desativado.

  • 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