Scripting Lua: Hooks e Ciclo de Vida
Esta página cobre todos os hooks que um poder Lua pode definir, a ordem por que os hooks são executados, como cada boss obtém o seu próprio runtime isolado e quais as funções da biblioteca padrão disponíveis dentro da sandbox.
Se ainda não escreveu um poder Lua, comece primeiro por Como começar.
Esta página documenta os hooks para poderes Lua de boss em plugins/EliteMobs/powers/. Os scripts Lua de NPC usam a sua própria pasta plugins/EliteMobs/npc_scripts/ e hooks específicos de NPC, como on_npc_interact e on_npc_proximity_enter; consulte Scripts de NPC.
Referência de hooks
Cada ficheiro de poder Lua devolve uma tabela. Cada chave nessa tabela (além de api_version e priority) deve ser um dos hooks listados abaixo. O runtime chama a função correspondente sempre que o evento de jogo respetivo é acionado.
| Hook | Aciona-se quando | context.player disponível? |
|---|---|---|
on_spawn | O elite mob surge (spawn) | Não |
on_game_tick | Uma vez por tick com o relógio do runtime ativo e sem pausa; normalmente a cada 50 ms a 20 TPS | Não |
on_boss_damaged | O boss sofre dano de qualquer fonte | Não |
on_boss_damaged_by_player | O boss sofre dano de um jogador | Sim |
on_reinforcement_damaged_by_player | Um reforço invocado por este boss sofre dano de um jogador; o hook corre nos poderes do invocador | Sim |
on_boss_damaged_by_elite | O boss sofre dano de outro elite mob | Não |
on_player_damaged_by_boss | Um jogador sofre dano deste boss | Sim |
on_enter_combat | O boss entra em combate | Sim |
on_exit_combat | O boss sai de combate | Não |
on_heal | O boss cura-se | Não |
on_boss_target_changed | O EliteMobs comunica um evento de alvo jogador, incluindo a aquisição inicial; não cobre todas as mudanças ou perdas de alvo | Sim |
on_death | O boss morre | Não |
on_phase_switch | Um boss de fases muda para uma nova fase | Não |
on_mind_action | Um Mind nativo pede uma ação aos seus poderes; consulte o contexto da ação | Não |
on_zone_enter | Uma entidade entra numa zona vigiada | Sim (se a entidade for um jogador) |
on_zone_leave | Uma entidade sai de uma zona vigiada | Sim (se a entidade for um jogador) |
Quando context.player está listado como "Não", aceder-lhe devolve nil. Verifique sempre se é nil antes de o usar.
on_boss_damaged e os hooks específicos de dano seguem caminhos de eventos separados. Um golpe pode acionar ambos; não são mutuamente exclusivos. O hook de alvo não permite cancelar o evento através desta tabela Lua nem comunica o alvo anterior.
on_zone_enter / on_zone_leaveO carregador aceita e valida estes hooks, mas um poder Lua sozinho não os consegue acionar. Só o listener de zona do EliteScript os envia. Este inicia quando um eliteScript: YAML no mesmo boss inclui ZoneEnterEvent ou ZoneLeaveEvent em Events:. Um boss apenas com poderes Lua carrega estes hooks sem erro, mas nunca os chama.
Os vigias Lua são independentes. context.zones:watch_zone(...) e context.script:zone(...):watch(...) chamam diretamente os callbacks on_enter / on_leave fornecidos, sem passar por estes hooks de nível superior. Use os vigias, salvo se estiver a combinar deliberadamente um poder Lua com um script de zona YAML.
Os eventos de zona YAML chegam a todos os poderes correspondentes do boss. Incluem a entidade que entra ou sai, mas não identificam a zona nem o script de origem. Vários listeners YAML podem, por isso, entregar vários eventos para a mesma entidade.
Os scripts Lua de NPC funcionam de forma diferente. O seu context.zones pertence ao MagmaCore, cujo watch aciona on_zone_enter / on_zone_leave. Consulte Scripts de NPC.
Poder típico com múltiplos hooks
Um único poder Lua pode definir tantos hooks quantos precisar. Abaixo está um esqueleto que usa três hooks em conjunto:
return {
api_version = 1,
on_enter_combat = function(context)
-- Initialize per-fight state when combat begins
context.state.hit_count = 0
context.log:info("Combat started!")
end,
on_boss_damaged_by_player = function(context)
-- Track hits and trigger an ability every 5th hit
context.state.hit_count = (context.state.hit_count or 0) + 1
if context.state.hit_count % 5 ~= 0 then
return
end
if not context.cooldowns:check_local("counter_attack", 100) then
return
end
-- Fire a projectile back at the player
local origin = context.boss:get_location()
origin:add(0, 1, 0)
context.boss:summon_projectile(
"SMALL_FIREBALL", origin, context.player:get_location(), 1.5
)
end,
on_death = function(context)
-- Spawn an explosion particle on death
context.world:spawn_particle_at_location(
context.boss:get_location(), "EXPLOSION_EMITTER", 1
)
end
}
Dados do evento (context.event)
Alguns hooks recebem uma tabela context.event que expõe dados sobre o evento de jogo que acionou o hook. Os campos disponíveis dependem do hook que está a ser executado.
Hooks de dano
Aplica-se a on_boss_damaged, on_boss_damaged_by_player, on_reinforcement_damaged_by_player, on_boss_damaged_by_elite e on_player_damaged_by_boss.
| Campo / Método | Tipo | Descrição |
|---|---|---|
event.damage_amount | double | Valor do dano no momento de criação desta tabela de evento |
event.get_damage_amount() | double | Lê o dano atual após alterações anteriores |
event.damage_cause | string | Nome de DamageCause do Spigot (por exemplo, "ENTITY_ATTACK", "PROJECTILE") |
event.damager | tabela de entidade | Atacante Bukkit original quando o evento subjacente é dano por entidade, incluindo em on_boss_damaged. Num impacto de projétil, é o projétil e não quem o disparou. |
event.projectile | tabela de entidade | A entidade projétil, se o causador do dano foi um projétil. |
event.set_damage_amount(n) | — | Sobrepor o dano para um valor fixo |
event.multiply_damage_amount(n) | — | Multiplicar o dano atual por n |
event.cancel_event() | — | Cancelar totalmente o evento de dano |
on_boss_damaged_by_player = function(context)
-- Halve all projectile damage
if context.event.damage_cause == "PROJECTILE" then
context.event.multiply_damage_amount(0.5)
end
end
Chame event.set_damage_amount(n) e event.multiply_damage_amount(n) com ponto. Estes setters leem diretamente o primeiro argumento e não removem o self de uma chamada com dois pontos. event:transfer_damage(target, amount) aceita dois pontos.
Nos hooks de dano de jogador a elite, event.entity identifica a elite atingida. Em on_reinforcement_damaged_by_player, context.boss continua a ser o invocador e context.player é o jogador atacante. O hook recebe o evento de dano original e modificável do reforço.
Estes hooks também expõem event.is_damage_transfer e event:transfer_damage(target, amount). O alvo deve ser outra elite viva que o boss proprietário possa afetar. A quantidade tem de ser finita e não negativa. Um evento cancelado, uma quantidade zero ou uma transferência aninhada devolve false. Uma chamada válida envia um golpe separado atribuído ao jogador e devolve true; não subtrai dano ao golpe original nem garante que outro listener permita o novo golpe. Use event.set_damage_amount(...) para repartir dano em vez de o duplicar. As transferências não podem voltar a transferir recursivamente.
Hook de spawn
Aplica-se a on_spawn.
| Campo / Método | Tipo | Descrição |
|---|---|---|
event.spawn_reason | string | Nome de SpawnReason do Spigot |
event.cancel_event() | — | Cancelar o spawn |
Hook de morte
Aplica-se a on_death.
| Campo / Método | Tipo | Descrição |
|---|---|---|
event.entity | tabela de entidade | A entidade que está a morrer |
Hooks de zona
Aplica-se a on_zone_enter e on_zone_leave.
| Campo / Método | Tipo | Descrição |
|---|---|---|
event.entity | tabela de entidade | A entidade que entra ou sai da zona |
Os vigias de zona criados em Lua não preenchem context.event; passam diretamente a entidade que entra/sai para o seu callback.
Eventos canceláveis (geral)
Os hooks com um evento subjacente cancelável expõem event.cancel_event(). context.event é nil em on_game_tick, on_mind_action e nos callbacks normais do agendador. Hooks como on_heal têm um evento subjacente, mas esta tabela não oferece campos específicos de cura. Verifique os membros documentados antes de os chamar.
A tabela de evento específica do boss não oferece cancel(), uncancel() ou is_cancelled do wrapper partilhado. Cancelar um evento interrompe a sua entrega aos poderes seguintes do mesmo boss, mas não sai da função Lua atual. Acrescente return quando o resto da função não deva correr.
Para os campos completos da tabela de entidade, consulte Boss e Entidades. Para os valores de causa de dano e de motivo de spawn, consulte Enums e Valores.
Ordem de execução dos hooks
O distribuidor de eventos do boss percorre os poderes de script associados pela ordem de execução. O campo priority de um poder Lua determina a sua posição:
- Valores mais baixos são executados primeiro (a predefinição é
0). - Prioridades iguais preservam a ordem de inserção dos poderes do boss. Não dependa de uma ordem específica entre diferentes caminhos de carregamento.
return {
api_version = 1,
priority = -10, -- runs before most other powers
on_boss_damaged_by_player = function(context)
-- Record this power's last attacker before its later hooks run
context.state.last_attacker = context.player.uuid
end
}
Os poderes YAML EliteScript participam no mesmo distribuidor com prioridade 0. Uma prioridade Lua negativa corre antes; uma positiva, depois. Isto ordena um evento num boss, não eventos Bukkit separados ou tarefas agendadas. Eventos já cancelados interrompem a distribuição, pelo que nem todos os poderes recebem necessariamente o evento. Depois de correrem os poderes próprios de um reforço, o invocador direto recebe o hook de dano ao reforço apenas se o evento continuar sem estar cancelado.
Modelo de runtime
Um runtime por poder e boss
Cada poder Lua associado tem o seu próprio runtime para cada boss. Ao iniciar, o EliteMobs avalia o código carregado numa sandbox nova e guarda a tabela devolvida. Quando o boss desaparece ou é removido, o runtime encerra. Um boss com dois poderes Lua tem, assim, duas tabelas de estado e dois ambientes independentes.
Isto significa:
- As variáveis e funções locais ao ficheiro, bem como os globais Lua que definir, são privados do runtime desse poder nesse boss.
- As funções de hook da tabela devolvida nunca são partilhadas entre bosses.
O carregador também avalia o ficheiro uma vez para validar a tabela devolvida. A inicialização ao nível do ficheiro deve ser independente do contexto dos hooks. Um poder só de ticks não precisa de on_spawn para iniciar o relógio. A inicialização do runtime e o callback de spawn são passos separados.
Isolamento de estado
Cada runtime tem a sua própria tabela context.state, separada dos outros poderes do mesmo boss e do mesmo poder noutros bosses. Use-a para guardar contadores, indicadores, temporizadores e outros dados do poder entre hooks. O estado dura enquanto o runtime estiver ativo; não é guardado em disco entre despawns ou reinícios.
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Each boss tracks its own enrage counter independently
context.state.enrage_hits = (context.state.enrage_hits or 0) + 1
if context.state.enrage_hits >= 20 then
context.boss:apply_potion_effect("SPEED", 200, 2)
end
end
}
Propriedade das tarefas agendadas
Todas as tarefas criadas através de context.scheduler pertencem ao runtime que as criou. Quando um boss desaparece (despawn):
- O runtime chama
shutdown(). - Todas as tarefas que lhe pertencem -- tanto as de execução única (
run_after) como as repetidas (run_every) -- são automaticamente canceladas. - Todos os vigias de zona são limpos.
- As alterações de IA, perceção, escala e invulnerabilidade que este poder ainda mantém são repostas, a cura que definiu é limpa e o seu controlador de bolas de fogo de perseguição termina.
O runtime limpa as suas tarefas agendadas quando o boss é removido. Algumas funcionalidades Java de jogo iniciam tarefas separadas com durações próprias; consulte a respetiva documentação. Cancele tarefas repetidas quando deixarem de ser necessárias durante o jogo:
return {
api_version = 1,
on_enter_combat = function(context)
local pulse_count = 0
local task_id
task_id = context.scheduler:run_every(20, function(tick_context)
pulse_count = pulse_count + 1
if pulse_count > 10 or not tick_context.boss.exists then
tick_context.scheduler:cancel_task(task_id)
return
end
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
end)
end
}
Comportamento do relógio por tick
O relógio de tick interno de uma instância de poder Lua só é executado quando o poder define um hook on_game_tick. Os vigias de zona criados através de context.zones:watch_zone(...) ou context.script:zone(...):watch(...) criam as suas próprias tarefas repetidas próprias, em vez de ativarem o hook on_game_tick de nível superior do poder.
Sem on_game_tick nem vigias de zona, o runtime não regista esse relógio de ticks. As tarefas explícitas do agendador e as iniciadas por funcionalidades de jogo continuam segundo os seus próprios horários. O trabalho por tick e as tarefas de vigilância pertencentes ao runtime são cancelados quando o boss desaparece ou o runtime encerra.
Pausar um runtime preserva o estado Lua e as tarefas, mas omite o respetivo trabalho. O caminho de eventos do boss também verifica a supressão de poderes. Remover o boss ou fechar o runtime termina o trabalho que lhe pertence, em vez de apenas o pausar.
Comportamento de erros e desempenho
O EliteMobs impõe limites estritos de erro e de desempenho aos poderes Lua:
Exceções
Se uma função de hook ou um callback agendado lançar um erro Lua (ou se uma exceção Java surgir de uma chamada à API), o poder é imediatamente desativado para essa instância de boss. O runtime é encerrado e todas as tarefas que lhe pertencem são canceladas.
A instância que falhou permanece fechada; hooks de dano posteriores não a recriam silenciosamente. Corrija e recarregue a definição e crie um runtime novo. Um encerramento deliberado pelo proprietário pode limpar a referência antiga para reconstruir o corpo mais tarde.
O erro é registado com o ficheiro do poder e o hook em execução. Inclui um número de linha quando disponível:
[Lua] Error in 'frost_cone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> ...explanation of what went wrong...
[Lua] -> Script has been disabled for this entity to prevent further errors.
Orçamento de execução
Os poderes Lua correm sob um orçamento rígido de execução imposto dentro da VM Lua, e não apenas medido à posteriori. Cada hook, cada callback agendado e até a avaliação inicial do ficheiro correm dentro desse orçamento:
| Limite | Valor |
|---|---|
| Tempo de CPU da thread por ponto de entrada | 50 ms |
| Tempo decorrido alternativo se não for possível medir a CPU | 250 ms |
| Instruções de bytecode por ponto de entrada | 250,000 |
A VM conta as instruções e verifica o tempo a cada 1024 instruções. O erro identifica o limite excedido:
Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)
Um ciclo Lua infinito é interrompido durante a execução. As chamadas aninhadas partilham o orçamento do ponto de entrada. A VM não consegue interromper uma chamada Java em curso, e não existe um segundo limite de 50 ms de tempo decorrido após o retorno. O orçamento não torna seguras as operações dispendiosas sobre o mundo.
Para se manter dentro do orçamento:
- Evite ciclos sem limites dentro dos hooks. Use
context.scheduler:run_every(...)para distribuir o trabalho ao longo dos ticks. - Mantenha os handlers de
on_game_tickleves -- são executados em cada tick. - Mova a inicialização pesada para
on_spawnouon_enter_combatem vez de a repetir a cada tick. - Ciclos de partículas em zonas grandes podem consumir rapidamente 250 000 instruções. Use
coveragepara processar menos localizações.
Sandbox Lua
Os poderes Lua são executados dentro de um ambiente LuaJ em sandbox. Vários globais que poderiam aceder ao sistema de ficheiros ou ao runtime Java são removidos.
Globais removidos
Os seguintes globais padrão do Lua são definidos como nil e não podem ser usados:
| Removido | Motivo |
|---|---|
debug | Expõe o estado interno da VM |
dofile | Acesso ao sistema de ficheiros |
io | Acesso ao sistema de ficheiros |
load | Carregamento de código arbitrário |
loadfile | Acesso ao sistema de ficheiros |
luajava | Acesso direto a classes Java |
module | Sistema de módulos (não necessário) |
os | Acesso ao sistema operativo |
package | Sistema de módulos (não necessário) |
require | Sistema de módulos / acesso ao sistema de ficheiros |
Os hooks de depuração da VM continuam instalados internamente para contar as instruções do orçamento de execução. A tabela debug é definida como nil imediatamente a seguir, impedindo o acesso pelos scripts.
Biblioteca padrão disponível
A sandbox mantém as bibliotecas padrão do LuaJ, incluindo estas funções comuns:
| Categoria | Funções |
|---|---|
| Math | math.abs, math.ceil, math.floor, math.max, math.min, math.random, math.sin, math.cos, math.sqrt, math.pi, e todas as outras funções math.* |
| String | string.byte, string.char, string.find, string.format, string.gsub, string.len, string.lower, string.match, string.rep, string.sub, string.upper, e todas as outras funções string.* |
| Table | table.insert, table.remove, table.sort, table.concat, e todas as outras funções table.* |
| Iteradores | pairs, ipairs, next |
| Tipo | type, tostring, tonumber, select, unpack |
| Tratamento de erros | pcall, xpcall, error, assert |
| Outros | print, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable |
As strings partilham uma única metatabela bloqueada. getmetatable("") devolve false, e as chamadas de métodos em strings, como ("text"):upper(), usam sempre as funções de string padrão, mesmo depois de um script acrescentar ou substituir funções na sua própria tabela string.
print escreve na consola, mas prefira context.log:info(msg) ou context.log:warn(msg). Os registos dos bosses não incluem automaticamente o ficheiro do poder. Acrescente-o à mensagem quando vários poderes puderem escrever ao mesmo tempo.
Namespace auxiliar em
A tabela em está disponível no momento do carregamento do ficheiro (antes de qualquer hook ser executado). Fornece construtores auxiliares para criar tabelas de localização, tabelas de vetor e definições de zona usadas em toda a API.
| Função | Finalidade |
|---|---|
em.create_location(x, y, z [, world, yaw, pitch]) | Criar uma tabela de localização com nome de mundo, yaw e pitch opcionais |
em.create_vector(x, y, z) | Criar uma tabela de vetor |
em.zone.create_sphere_zone(radius) | Criar uma definição de zona esférica |
em.zone.create_dome_zone(radius) | Criar uma definição de zona em cúpula (dome) |
em.zone.create_cylinder_zone(radius, height) | Criar uma definição de zona cilíndrica |
em.zone.create_cuboid_zone(x, y, z) | Criar uma definição de zona cuboide |
em.zone.create_cone_zone(length, radius) | Criar uma definição de zona cónica |
em.zone.create_static_ray_zone(length, thickness) | Criar uma definição de zona de raio estático |
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration) | Criar uma definição de zona de raio rotativo |
em.zone.create_translating_ray_zone(length, point_radius, animation_duration) | Criar uma definição de zona de raio com translação |
O em também inclui em.location, com consultas de mundo partilhadas só de leitura: is_in_dungeon, is_protected, owned_by, owners, kinds_at e has_kind. Recebem uma tabela de localização como primeiro argumento. Uma localização ausente ou que não seja uma tabela, um mundo ausente ou não carregado devolve false ou uma tabela vazia. Coordenadas ausentes valem zero; tipos de coordenada inválidos podem causar erros. Indique o mundo explicitamente ao construir localizações para estas consultas. Consulte as assinaturas e os resultados na Referência da API Lua.
-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end
Não existe binding Lua para verificações de permissão de construção. em.location.is_protected diz se algum plugin de proteção reivindica a localização; não resolve direitos de construção por jogador.
Os construtores de zona devolvem tabelas encadeáveis (chainable) com :set_center(loc) (ou :set_origin(loc) / :set_destination(loc), dependendo do tipo de zona). Estes foram concebidos para serem usados no topo de um ficheiro ou dentro de hooks:
-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Anchor the zone to the boss's current location at call time
blast_zone:set_center(context.boss:get_location())
local entities = context.zones:get_entities_in_zone(blast_zone)
for i = 1, #entities do
if entities[i].is_player then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}
Para uma análise completa das formas de zona, filtros, vigias e padrões de visar alvos, consulte Zonas e Alvos.
Próximos passos
- Boss e Entidades --
context.boss,context.player, wrappers de entidade - Mundo e Ambiente -- partículas, sons, spawning,
context.world - Zonas e Alvos -- zonas nativas, utilitários de script,
context.zones/context.script - Exemplos e Padrões -- poderes completos e funcionais que pode estudar e adaptar
- Enums e Valores -- ligações para o Javadoc do Spigot para todas as constantes de string
