Saltar 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 por tick com o relógio do runtime ativo e sem pausa; normalmente a cada 50 ms a 20 TPSNão
on_boss_damagedO boss sofre dano de qualquer fonteNão
on_boss_damaged_by_playerO boss sofre dano de um jogadorSim
on_reinforcement_damaged_by_playerUm reforço invocado por este boss sofre dano de um jogador; o hook corre nos poderes do invocadorSim
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 EliteMobs comunica um evento de alvo jogador, incluindo a aquisição inicial; não cobre todas as mudanças ou perdas de alvoSim
on_deathO boss morreNão
on_phase_switchUm boss de fases muda para uma nova faseNão
on_mind_actionUm Mind nativo pede uma ação aos seus poderes; consulte o contexto da açãoNã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.

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.

As zonas Lua não acionam on_zone_enter / on_zone_leave

O 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étodoTipoDescrição
event.damage_amountdoubleValor do dano no momento de criação desta tabela de evento
event.get_damage_amount()doubleLê o dano atual após alterações anteriores
event.damage_causestringNome de DamageCause do Spigot (por exemplo, "ENTITY_ATTACK", "PROJECTILE")
event.damagertabela de entidadeAtacante 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.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

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é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)​

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):

  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.
  4. 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:

LimiteValor
Tempo de CPU da thread por ponto de entrada50 ms
Tempo decorrido alternativo se não for possível medir a CPU250 ms
Instruções de bytecode por ponto de entrada250,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_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.
  • Ciclos de partículas em zonas grandes podem consumir rapidamente 250 000 instruções. Use coverage para 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:

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

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:

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

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.

dica

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

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​