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 a cada tick do servidor (50 ms) enquanto o relógio do runtime estiver ativo | 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_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 boss muda 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_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.
Os hooks de nível superior on_zone_enter e on_zone_leave são acionados por eventos EliteScript/ScriptZone. Os vigias (watchers) criados em Lua a partir de context.zones:watch_zone(...) e context.script:zone(...):watch(...) chamam diretamente os seus callbacks on_enter / on_leave em vez de invocarem estes hooks de nível superior.
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 a firework 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_boss_damaged_by_elite e on_player_damaged_by_boss.
| Campo / Método | Tipo | Descrição |
|---|---|---|
event.damage_amount | double | Valor de dano em bruto |
event.damage_cause | string | Nome de DamageCause do Spigot (por exemplo, "ENTITY_ATTACK", "PROJECTILE") |
event.damager | tabela de entidade | Entidade que causou o dano. Só presente em hooks de dano por entidade. |
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
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)
Qualquer hook cujo evento de jogo subjacente seja cancelável expõe event.cancel_event(). Se context.event for nil para um dado hook (por exemplo, on_game_tick, on_heal), não há nenhum evento subjacente com que interagir.
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
Quando um boss tem múltiplos poderes Lua associados, o hook de cada poder é chamado para o mesmo evento. A ordem é determinada pelo campo priority:
- Valores mais baixos são executados primeiro (a predefinição é
0). - Poderes com a mesma prioridade são executados pela ordem de carregamento (efetivamente não especificada).
return {
api_version = 1,
priority = -10, -- runs before most other powers
on_boss_damaged_by_player = function(context)
-- This runs early, so other powers see any state changes we make
context.state.last_attacker = context.player.uuid
end
}
A prioridade só afeta a ordenação entre poderes Lua no mesmo boss. Não interage com a ordem de execução do EliteScript.
Modelo de runtime
Um runtime por boss
Cada entidade de boss obtém a sua própria instância de runtime Lua independente. Quando o boss surge, o EliteMobs carrega o código-fonte Lua, avalia-o num ambiente de sandbox novo e armazena a tabela devolvida. Quando o boss desaparece (despawn) ou é removido, o runtime é encerrado.
Isto significa:
- As variáveis globais Lua definidas durante a avaliação do ficheiro (como funções auxiliares com
local function) são privadas desse boss. - As funções de hook da tabela devolvida nunca são partilhadas entre bosses.
Isolamento de estado
Cada runtime tem a sua própria tabela context.state. O estado de um boss é completamente invisível para qualquer outro boss, mesmo que partilhem o mesmo ficheiro de poder Lua. Use context.state para armazenar contadores, flags, temporizadores ou quaisquer dados por boss que precise entre hooks.
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.
Nunca precisa de limpar manualmente as tarefas agendadas aquando da remoção do boss. No entanto, deve mesmo assim cancelar as tarefas repetidas quando já não forem necessárias durante a jogabilidade normal, para evitar trabalho desnecessário:
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.
Se um poder não tiver nem on_game_tick nem vigias de zona, não é gerado qualquer trabalho por tick. O trabalho por tick e as tarefas dos vigias de zona são canceladas automaticamente quando o boss desaparece (despawn) ou o runtime é encerrado.
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.
O erro é registado na consola do servidor juntamente com o nome do ficheiro do poder, o número da linha e o hook que estava a ser executado:
[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
Cada invocação de hook e cada invocação de callback é cronometrada. Se uma única chamada demorar mais do que 50 milissegundos, o poder é desativado com um aviso na consola:
[Lua] my_power.lua took 73ms in 'on_game_tick' (limit: 50ms) — script disabled to prevent lag.
Isto evita que scripts descontrolados congelem o servidor. 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.
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 |
Biblioteca padrão disponível
Tudo o resto da biblioteca padrão do Lua funciona normalmente:
| 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 |
print escreve na consola do servidor, mas prefira context.log:info(msg) ou context.log:warn(msg) para a saída. Estes têm como prefixo o nome do poder, facilitando a identificação de qual poder produziu a mensagem.
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 |
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].type == "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
