跳到主要内容

Lua 脚本:入门指南

webapp_banner.jpg

你将学到什么​

本页将教你编写 EliteMobs 的第一个 Lua 能力,从空白文件一直到可用的 Boss 技能。结束时你将理解钩子、上下文、冷却时间以及每个 Lua 能力文件的一般结构。

熟悉基础知识后,继续阅读以下配套页面:

  • 钩子与生命周期 -- 钩子名称、触发顺序和上下文可用性
  • NPC 脚本 -- NPC 生命周期、交互和邻近钩子
  • Boss 与实体 -- context.boss、context.player、context.players 和 context.entities API
  • 世界与环境 -- 粒子、声音、闪电、生成和 context.world API
  • 区域与目标 -- 原生 Lua 区域、脚本工具目标和 context.zones / context.script API
  • 示例与模式 -- 可学习和改编的完整工作能力
  • 枚举与值 -- Particle、Material、PotionEffectType 等字符串常量的 Spigot Javadoc 链接
  • 故障排除 -- 常见错误、调试技巧和 QC 清单
  • 行为程序 -- 用 Lua 编写、通过 Boss 的 behavior 设置选用的怪物 AI
仍在演进中

EliteMobs 的大多数内置能力使用 Lua,但辅助方法的名称和行为可能随版本变化。自定义能力投入生产前应在预发布服务器上检查,更新后也应重新查阅参考文档。

与 EliteScript 的关系

Lua 不会替代现有的 eliteScript: YAML 系统,而且 EliteScript 也没有被弃用。

  • 当你需要使用现有的动作、目标、区域、条件、冷却时间和相对向量页面进行声明式 YAML 驱动脚本时,请使用 EliteScript。
  • 当你需要变量、循环、随机选择、可复用辅助函数、每个 Boss 的持久状态和更传统的编程流程时,请使用 Lua 能力文件。

脚本工具 API(context.script)把规格表传给 EliteScript 的目标、区域、相对向量和粒子类。它们的解析器和限制仍然适用,但独立的 Lua 句柄并不具备 YAML 动作的所有上下文。继承目标和落地位置需要这些句柄缺少的上下文;Lua 辅助方法也不会自动应用 YAML 动作的等待、条件或目标缓存。区域与目标页面解释了这些差异。


什么是 Lua 能力​

Lua 能力是独立的 .lua 文件,位于 EliteMobs 正常的 powers 目录树中,引用方式与普通能力文件完全相同。

Boss 能力 vs. NPC 脚本

本页讲的是 Boss Lua 能力。NPC Lua 脚本是另一项独立功能:它们位于 plugins/EliteMobs/npc_scripts/,通过 NPC 配置中的 scripts: 列表挂接,并使用诸如 on_npc_proximity_enter 的 NPC 专属钩子。参见 NPC 脚本。

Lua 能力的优势​

Lua 能力在以下情况下表现出色:

  • 攻击轮换和随机技能选择
  • 使用 context.state 在钩子间保持持久状态
  • 无需使用 YAML 等待构建一切的延迟和重复动作
  • 在单个文件内共享自定义辅助函数
  • 纯 EliteScript 中难以处理的复杂分支
  • 仍然想复用 EliteScript 风格目标和区域定义的 Boss 逻辑

如果你的能力主要是"触发事件,执行几个脚本动作",现有的 EliteScript 文档仍然是最快最清晰的构建方式。如果你的能力需要真正的程序流程,Lua 就是合适的工具。


本页面面向的读者​

本页面为三类读者编写:

  • 已经了解 EliteScript,想学习 Lua 但不想一次学会"真正的编程"的人
  • EliteMobs 脚本新手,需要包含精确名称的完整参考的人
  • 使用 AI 起草能力,需要足够的细节来判断 AI 何时编造了虚假信息的人

在编写有用的能力之前,你不需要成为完整的 Lua 开发者。对于大多数实用的 EliteMobs 能力,你真正需要的只是:

  • 如何在返回的表中放置有效的钩子
  • 如何从 context 读取值
  • 如何使用 if ... then return end 提前退出
  • 如何正确调用几个辅助方法
  • 如何从现有 EliteScript 文档中复制正确的目标和区域规范

心智模型:EliteScript vs. Lua​

如果你了解 EliteScript,这个对比是理解 Lua 能力的最快方式:

如果你用 EliteScript 术语思考在 Lua 中通常意味着
Events钩子名称如 on_spawn 或 on_boss_damaged_by_player
Cooldownscontext.cooldowns(见下方 context.cooldowns)
Actions直接方法调用如 context.world:spawn_particle_at_location(...) 或 context.script:damage(...)
Targetscontext.script:target({...}) -- 使用 EliteScript Target 字段名
Zonescontext.script:zone({...}) 或原生 context.zones 辅助 -- 见区域与目标
Relative vectorscontext.script:relative_vector({...}) 或 context.vectors
Script flow你自己的 Lua if 语句、辅助函数、定时器和状态

最大的区别是:

  • EliteScript 在 YAML 中描述应该发生什么。
  • Lua 让你用代码决定何时、为什么以及哪个分支应该执行。

如果 EliteScript 感觉像"配置",那么 Lua 感觉像"配置加决策"。


Lua 极简入门​

多数能力使用几个 Lua 概念:变量(local x = 5)、函数(function foo() end)、if 检查(if x then ... end)、表({key = value})和 nil(没有值)。分号可选。表使用花括号,函数和控制流程块使用 end 结束。

关于带示例的完整讲解,请参阅 MagmaCore Lua 脚本引擎 —— Lua 极简入门。该入门内容在所有 Nightbreak 插件之间共享,学一次即可处处适用。


你的第一个工作能力,逐步构建​

如果你是完全的新手,这是通往"第一次成功"的最佳路径。

步骤 1 之前:保存并附加到 Boss​

将 Lua 文件保存在 EliteMobs 的正常能力文件夹中,例如:

plugins/
EliteMobs/
powers/
first_test.lua

然后使用正常的 powers: 列表将该文件名添加到 Boss 配置中:

powers:
- first_test.lua

完整的新手流程是:

  1. 将文件保存到 plugins/EliteMobs/powers/
  2. 将 .lua 文件名添加到 Boss 的 powers: 列表
  3. 执行 /em reload 加载修改的文件,再生成一个新的测试 Boss。重载影响插件及其活动内容,因此请使用预发布服务器。
  4. 触发钩子,检查可见结果和控制台。只保存文件,或不重载就生成另一个 Boss,都不会更新已加载的定义。

如果需要更多关于 Boss 文件、能力列表或自定义 Boss 结构的背景,请参阅创建自定义 Boss。那个页面详细介绍了 Boss 的搭建,包括 powers: 列表是如何工作的。

步骤 1:让文件加载​

return {
api_version = 1,

on_spawn = function(context)
end
}

加载器会验证 Lua 语法、返回表和钩子名称。空钩子没有可见结果,因此仅仅没有报错,不能证明文件已被发现或附加。添加步骤 2 的声音并生成一个新 Boss,验证整个过程。

步骤 2:让 Boss 做一件可见的事​

return {
api_version = 1,

on_spawn = function(context)
context.boss:play_sound_at_self("entity.blaze.ambient", 1.0, 1.0)
end
}

现在你有了最重要的新手证明:你的钩子正在触发。

步骤 3:响应玩家的攻击​

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
if context.player == nil then
return
end

context.player:send_message("&eYou hit the boss.")
end
}

这教授三个核心概念:

  • on_boss_damaged_by_player 是钩子名称
  • context.player 是参与该钩子的玩家
  • return 在缺少所需数据时提前退出

步骤 4:使用冷却时间防止刷屏​

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
if context.player == nil then
return
end

if not context.cooldowns:local_ready("hello_message") then
return
end

context.player:send_message("&eYou woke up the boss.")
context.cooldowns:set_local(60, "hello_message")
end
}

本地冷却的每刻表示实际经过的 50 毫秒:即使服务器 TPS 下降,60 也始终是三秒。这个命名冷却在同一 Boss 的能力之间共享,不在不同 Boss 之间共享。组合自定义能力时,请给键加上能力名称前缀。本例只检查自身的本地键,不检查 Boss 的全局冷却。

步骤 5:添加一个真实效果​

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
if context.player == nil then
return
end

if not context.cooldowns:local_ready("shock") then
return
end

context.cooldowns:set_local(100, "shock")
context.player:send_message("&cStatic jumps from the boss into your armor!")
context.world:strike_lightning_at_location(context.player.current_location)
end
}

这会在世界中产生真实闪电,不是纯视觉闪光。先设置冷却再执行效果,使效果触发的事件也能看到该冷却。这个 100 刻的本地冷却表示实际经过的五秒。


第一个真实工作流程​

从头构建新的 Lua 能力时,使用以下顺序:

  1. 创建文件并让 on_spawn 工作。
  2. 切换到你实际需要的钩子。
  3. 确认钩子具有你期望的数据,如 context.player 或 context.event。
  4. 先添加消息或声音,在伤害或粒子之前。
  5. 添加冷却时间。
  6. 添加一个游戏效果。
  7. 之后才添加辅助函数、状态、调度器逻辑或区域。

这个顺序使调试大大简化,因为每次只改变一件事。


Lua 文件的位置​

将 .lua 文件放在与普通能力 .yml 文件相同的文件夹树中:

plugins/
EliteMobs/
powers/
mycoolpower.lua
attack_push.yml
subfolder/
myotherpower.lua

Lua 能力会从 EliteMobs 已经加载的能力目录中自动发现。

可选 YAML 元数据​

自定义 mycoolpower.lua 可以在同一目录旁边配一个已加载的 mycoolpower.yml 或 mycoolpower.yaml。发现过程会读取该 YAML 的 isEnabled、effect、powerType、powerCooldown 和 globalCooldown;isEnabled: false 会阻止该自定义 Lua 文件注册。没有匹配的元数据时,不使用效果,能力类型为 MISCELLANEOUS。Lua 钩子仍自行控制攻击时机,并且必须自行检查冷却。

放在内置 Lua 能力旁边、已加载的同名 YAML 文件会对该能力应用相同的字段。没有这样的文件时,内置能力保留其随插件提供的元数据。各字段见创建能力。

Boss 如何引用 Lua 能力​

Boss 文件仍然使用普通的 powers: 列表:

powers:
- attack_push.yml
- mycoolpower.lua

不需要特殊字段。Lua 能力不通过 eliteScript: 加载。

文件命名规则​

  • Boss 通过文件名引用 Lua 能力,而非文件夹路径。
  • EliteMobs 目前仅通过基本名称注册发现的 Lua 能力。
  • 避免重复的名称如 powers/fire.lua 和 powers/bosses/fire.lua。EliteMobs 只会加载完整路径按字母顺序排在前面的那个文件,并对被跳过的副本记录警告,除非两个文件完全相同。

预制 Lua 能力​

大多数内置能力使用 Lua。当前源码定义了 68 个具体的预制 Lua 能力,在文件缺失时安装到 plugins/EliteMobs/powers/,包括 attack_fire.lua、frost_cone.lua、meteor_shower.lua 和 shield_wall.lua。可以阅读这些文件学习,但编辑过的本地副本可能不同于插件自带版本。

预制文件只在缺失时写入,因此修改在重启后仍保留。当某次更新修改了内置能力时,启动时会将未编辑过的旧版随附副本移至 plugins/MagmaCore/outdated files/EliteMobs/,并在原处写入当前版本;编辑过的副本会保留。删除一个内置文件后,下次插件初始化会重新生成自带版本;替换前请保留仍然需要的自定义修改。

共享源码片段​

Boss Lua 能力可以用独占一行的注释指令包含辅助文件:

-- @include helpers/common.inc

加载器在验证能力前插入引用文件的文本。路径相对于包含指令的文件,并且必须保持在最外层能力文件的目录内,解析符号链接后也不例外。循环包含、超过 16 层的嵌套、大于 4 MiB 的文件,以及超过 4,194,304 字符的展开源码都会被拒绝。

辅助片段请使用 .inc 等非 .lua 扩展名,避免发现过程也将其当作独立能力加载。在能力最终的 return 表之前声明辅助函数。该指令仅适用于 Boss 能力加载,不会启用 Lua require 或文件系统访问,NPC 脚本也不会处理它。

旧的 .yml 名称仍然可以解析​

引用 attack_fire.yml 的 Boss 配置仍然有效。对于 .yml 或 .yaml 引用,EliteMobs 先查找相同基本名称的已加载 Lua 能力,再回退到原名称。验证失败或被禁用的文件不是已加载的替代能力。请保持基本名称的拼写和大小写一致。要修改内置 Lua 能力,请编辑其现有文件并重载;在其他目录建立重复名称不是可靠的覆盖方式。


最小文件约定​

每个 Lua 能力必须用 return 返回一个表。该表有意设计得很严格。

必需和可选的顶级字段​

字段必需类型备注
api_version是Number使用整数 1
priority否Number使用整数。较小的值先执行,默认为 0。YAML 能力的优先级为 0
支持的钩子键否Function必须使用钩子参考中列出的确切钩子名称之一

验证规则​

  • 文件必须返回一个表。
  • api_version 是必需的,当前必须为 1。
  • priority 如果存在必须是数字。
  • 每个额外的顶级键必须是支持的钩子名称。
  • 每个钩子键必须指向一个函数。
  • 未知的顶级键会被拒绝。

这意味着辅助函数和局部常量应该放在最终 return 的上方,而非放在返回的表内部,除非它们是实际的钩子。

文件主体在验证时执行一次,并在每个 Boss 能力运行实例的独立 Lua 环境中再次执行。context 只作为钩子或回调参数存在,不是顶级全局变量。局部变量可以在该运行实例中保留,但验证时的值不会传入它。运行环境移除了文件、操作系统、Java 访问和 require;源码片段请使用包含指令。未捕获的钩子或回调错误会关闭该运行实例并取消其任务,下次受到攻击时不会重试。修正文件并重载后再试。


复制粘贴入门模板​

最小的有效 Lua 能力​

return {
api_version = 1,

on_spawn = function(context)
end
}
local ATTACK_COOLDOWN = "my_attack"

local function can_run_attack(context)
return context.cooldowns:local_ready(ATTACK_COOLDOWN)
and context.cooldowns:global_ready()
end

local function run_attack(context)
context.boss:play_sound_at_self("entity.blaze.shoot", 1.0, 1.0)
context.cooldowns:set_local(100, ATTACK_COOLDOWN)
context.cooldowns:set_global(20)
end

return {
api_version = 1,
priority = 0,

on_boss_damaged_by_player = function(context)
if context.player == nil then
return
end

if not can_run_attack(context) then
return
end

run_attack(context)
end
}

大型文件布局​

local CONSTANT_NAME = "value"

local function helper_function(context)
end

local function another_helper(context, value)
end

return {
api_version = 1,
priority = 0,

on_spawn = function(context)
end,

on_enter_combat = function(context)
end,

on_boss_damaged_by_player = function(context)
end,

on_exit_combat = function(context)
end
}

钩子​

钩子是你返回的表中具有特殊名称的函数。EliteMobs 在发生某些事情时调用它们 -- Boss 生成、受到伤害、进入战斗等。你已经在上面的教程中看到了 on_spawn 和 on_boss_damaged_by_player。

最常用的入门钩子是 on_spawn、on_boss_damaged_by_player、on_enter_combat 和 on_exit_combat。所有钩子的完整列表、每个钩子中可用的上下文键以及执行顺序的工作方式,请参阅钩子与生命周期。


什么是 context?​

每个钩子函数都接收一个名为 context 的参数。把它想象成 EliteMobs 每次发生事情时递给你的工具箱 -- 它包含了你与 Boss、玩家、世界、冷却时间等交互所需的一切。

on_boss_damaged_by_player = function(context)
-- context.boss = the boss that was hit
-- context.player = the player who hit it
-- context.world = tools for spawning particles, sounds, etc.
-- context.cooldowns = cooldown management
-- context.state = your own persistent storage
end

你不需要自己创建 context -- EliteMobs 创建它并传递给你的钩子。你只需从中读取并调用方法。

信息

每个钩子接收新的 context。其中的 context.state 表属于此 Boss 上此能力的运行实例,可为之后的钩子和回调保存数据。其他能力有独立状态;关闭或重载运行实例会丢弃状态。


关键 context API​

以下是你将从 context 使用的最重要的 API:

  • context.state -- 为此 Boss 上此能力的运行实例保留的普通 Lua 表。用它在钩子间保存阶段编号、任务 ID 和标志。运行实例移除或重载后不保留。

  • context.log -- 使用 log:info(msg)、log:warn(msg) 和 log:debug(msg) 记录控制台日志。需要区分多个能力的输出时,请在消息里写上能力名称。

  • context.cooldowns -- Boss 的各个能力共享命名冷却和全局冷却。显式键共享 Boss 范围内的命名空间,因此请加上能力名称前缀。nil 键选择当前 Lua 文件名的默认冷却。cooldowns:check_local(key, ticks) 在一次调用中检查并设置本地冷却,不检查也不设置全局冷却。完整 API 见钩子与生命周期。

  • context.scheduler -- scheduler:run_after(ticks, callback) 延迟一次回调。scheduler:run_every(ticks, callback) 重复执行,首次不等待一个间隔;需要明确的首次延迟时使用 scheduler:run_repeating(delay, interval, callback)。回调接收不含触发事件或玩家的新上下文。使用这个回调参数,并主动保存所需玩家 UUID 或位置;不要假定延迟后仍有 ctx.player。仅限战斗的重复任务应在 on_exit_combat 中取消,否则所属任务会一直运行,直到取消或运行实例关闭。见钩子与生命周期。

  • context.boss / context.player -- 参与当前事件的 Boss 和玩家。所有字段和方法请参阅 Boss 与实体。

  • context.world -- 生成粒子、实体、声音、闪电、方块。请参阅世界与环境。

  • context.zones / context.script -- 区域几何、目标、伤害、粒子。请参阅区域与目标。


方法语法:: 与 .​

关于 : 与 .,请参阅 MagmaCore Lua 脚本引擎。实体方法和共享服务接受两种形式,但 context.event.set_damage_amount(value) 与 context.event.multiply_damage_amount(multiplier) 必须使用点号;冒号会把事件表作为第一个参数传入。context.event:transfer_damage(target, amount) 接受冒号。em.create_location(...) 等全局构造函数使用点号。


后续步骤​

现在你已经了解了基础知识,探索 Lua 脚本文档的其余部分:

  • 钩子与生命周期 -- 钩子名称、执行顺序以及每个钩子中可用的上下文键
  • NPC 脚本 -- NPC 文件位置、钩子、邻近事件以及 context.npc
  • Boss 与实体 -- context.boss、context.player、context.players、context.entities 和 context.event
  • 世界与环境 -- 粒子、声音、闪电、生成和 context.world
  • 区域与目标 -- 原生 Lua 区域、脚本工具的目标/区域/粒子和相对向量
  • 示例与模式 -- 带讲解的完整工作能力
  • 枚举与值 -- Particle、Material、PotionEffectType 等的 Spigot Javadoc 链接
  • 故障排除 -- 常见错误、调试技巧和 QC 清单

对于基于 YAML 的脚本,EliteScript 页面仍然是权威参考: