Pular para o conteúdo principal

Scripting Lua: Hooks e Ciclo de Vida

webapp_banner.jpg

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.

Hooks de poderes de boss

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.

HookAciona-se quandocontext.player disponível?
on_spawnO elite mob surge (spawn)Não
on_game_tickUma vez a cada tick do servidor (50 ms) enquanto o relógio do runtime estiver ativoNão
on_boss_damagedO boss sofre dano de qualquer fonteNão
on_boss_damaged_by_playerO boss sofre dano de um jogadorSim
on_boss_damaged_by_eliteO boss sofre dano de outro elite mobNão
on_player_damaged_by_bossUm jogador sofre dano deste bossSim
on_enter_combatO boss entra em combateSim
on_exit_combatO boss sai de combateNão
on_healO boss cura-seNão
on_boss_target_changedO boss muda de alvoSim
on_deathO boss morreNão
on_phase_switchUm boss de fases muda para uma nova faseNão
on_zone_enterUma entidade entra numa zona vigiadaSim (se a entidade for um jogador)
on_zone_leaveUma entidade sai de uma zona vigiadaSim (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.

Origem dos hooks de zona

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étodoTipoDescrição
event.damage_amountdoubleValor de dano em bruto
event.damage_causestringNome de DamageCause do Spigot (por exemplo, "ENTITY_ATTACK", "PROJECTILE")
event.damagertabela de entidadeEntidade que causou o dano. Só presente em hooks de dano por entidade.
event.projectiletabela de entidadeA 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étodoTipoDescrição
event.spawn_reasonstringNome de SpawnReason do Spigot
event.cancel_event()Cancelar o spawn

Hook de morte

Aplica-se a on_death.

Campo / MétodoTipoDescrição
event.entitytabela de entidadeA entidade que está a morrer

Hooks de zona

Aplica-se a on_zone_enter e on_zone_leave.

Campo / MétodoTipoDescrição
event.entitytabela de entidadeA 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):

  1. O runtime chama shutdown().
  2. Todas as tarefas que lhe pertencem -- tanto as de execução única (run_after) como as repetidas (run_every) -- são automaticamente canceladas.
  3. 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_tick leves -- são executados em cada tick.
  • Mova a inicialização pesada para on_spawn ou on_enter_combat em 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:

RemovidoMotivo
debugExpõe o estado interno da VM
dofileAcesso ao sistema de ficheiros
ioAcesso ao sistema de ficheiros
loadCarregamento de código arbitrário
loadfileAcesso ao sistema de ficheiros
luajavaAcesso direto a classes Java
moduleSistema de módulos (não necessário)
osAcesso ao sistema operativo
packageSistema de módulos (não necessário)
requireSistema de módulos / acesso ao sistema de ficheiros

Biblioteca padrão disponível

Tudo o resto da biblioteca padrão do Lua funciona normalmente:

CategoriaFunções
Mathmath.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.*
Stringstring.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.*
Tabletable.insert, table.remove, table.sort, table.concat, e todas as outras funções table.*
Iteradorespairs, ipairs, next
Tipotype, tostring, tonumber, select, unpack
Tratamento de errospcall, xpcall, error, assert
Outrosprint, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable
dica

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çãoFinalidade
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