Lua 脚本:示例与模式
本页包含 EliteMobs Lua 能力的完整工作示例,以及实用模式、最佳实践和技巧。每个示例都附有其功能和原因的说明。
如果你是 Lua 能力的新手,请从入门指南开始。完整 API 详情请参阅 API 参考、Boss 与实体、世界与环境、区域与目标选择和枚举。
示例:使用脚本工具进行基于区域的目标选择
本示例教授: 如何使用 context.script 从 Lua 创建 EliteScript 风格的区域几何,生成粒子并对区域内的实体造成伤害。
完整能力文件(点击展开)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = { targetType = "SELF", offset = "0,1,0" },
Target2 = { targetType = "NEARBY_PLAYERS", range = 20 },
radius = 5
})
context.script:spawn_particles(cone:full_target(0.4), { particle = "FLAME", amount = 1, speed = 0.05 })
context.script:damage(cone:full_target(), 1.0, 1.5)
end
}
说明
- 区域创建 --
context.script:zone(...)使用与 EliteScript 区域相同的字段名创建锥形。Target设置锥形原点(Boss 本身,向上偏移1格),Target2设置目的地(20格内最近的玩家)。radius控制锥形的张开程度。 - 粒子生成 --
cone:full_target(0.4)返回一个以 40% 覆盖率解析为锥形内所有位置的目标句柄。 - 伤害 --
context.script:damage(cone:full_target(), 1.0, 1.5)命中完整锥形内的所有生物实体。第一个数字(1.0)是基础伤害量,第二个(1.5)是应用于玩家的伤害倍率。
传递给 context.script 的区域和目标表使用 EliteScript 字段名(targetType、shape、Target、Target2、range、offset、coverage)。完整列表请参阅 EliteScript 区域和 EliteScript 目标。
示例:状态 + 调度器攻击循环
本示例教授: 使用 context.state 跟踪运行时状态,context.scheduler 执行重复任务,以及正确的战斗进入/退出生命周期。
完整能力文件(点击展开)
local function pick_action(context)
local roll = math.random(1, 2)
if roll == 1 then context.boss:play_model_animation("slam")
else context.boss:play_model_animation("roar") end
end
return {
api_version = 1,
on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,
on_enter_combat = function(context)
if context.state.started then return end
context.state.started = true
context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then pick_action(loop_context) end
end)
end,
on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}
说明
- 状态初始化 --
on_spawn将context.state.started设为false,context.state.loop_task_id设为nil。state表在此 Boss 实例的整个生命周期中持续存在。 - 战斗守卫 --
on_enter_combat在启动循环前检查context.state.started,防止多个重叠循环。 - 调度器模式 --
context.scheduler:run_every(100, callback)每 100 tick(5秒)执行回调。回调接收新鲜上下文。 - 退出时清理 --
on_exit_combat取消重复任务并重置状态。
如果在 on_enter_combat 中启动了重复任务,务必在 on_exit_combat 中取消它。忘记取消会留下一个后台任务,直到 Boss 消失才停止运行。
示例:命中时的火焰效果
本示例教授: 简单的"命中时应用效果"能力 -- 战斗能力最常见的模式。
完整能力文件(点击展开)
return {
api_version = 1,
on_player_damaged_by_boss = function(context)
if context.player == nil then return end
if not context.cooldowns:check_local("fire_touch", 60) then return end
context.player:set_fire_ticks(60)
context.world:spawn_particle_at_location(context.player.current_location, { particle = "FLAME", amount = 20, speed = 0.1 })
context.player:send_message("&cThe boss's touch burns!")
context.cooldowns:set_global(40)
end
}
说明
- Nil 守卫 --
context.player在极少数边缘情况下可能为nil。使用前务必检查。 - 本地冷却 --
context.cooldowns:check_local("fire_touch", 60)原子性地检查和设置冷却。 - 燃烧 tick --
context.player:set_fire_ticks(60)让玩家燃烧 60 tick(3秒)。 - 粒子 --
context.world:spawn_particle_at_location(location, spec)在位置生成粒子。 - 消息 --
context.player:send_message(text)发送带颜色代码的聊天消息。 - 全局冷却 --
context.cooldowns:set_global(40)将此 Boss 的所有能力设为 40 tick 冷却。
示例:使用原生 Lua 区域的基于区域的 AoE 能力
本示例教授: 创建和查询原生 Lua 区域,对区域内的玩家造成伤害。
完整能力文件(点击展开)
return {
api_version = 1,
on_spawn = function(context) context.state.aoe_task_id = nil end,
on_enter_combat = function(context)
if context.state.aoe_task_id ~= nil then return end
context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then return end
local zone_def = { kind = "sphere", radius = 8, origin = tick_context.boss:get_location() }
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #victims do
victims[i]:deal_custom_damage(4.0)
tick_context.world:spawn_particle_at_location(victims[i].current_location, { particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 })
end
tick_context.world:spawn_particle_at_location(tick_context.boss:get_location(), { particle = "SPELL_MOB", amount = 40, speed = 0.1 })
tick_context.cooldowns:set_global(60)
end)
end,
on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}
说明
- 状态设置 --
on_spawn将aoe_task_id初始化为nil。 - 重复攻击 --
on_enter_combat每 60 tick(3秒)启动一个重复任务。 - 区域定义 --
zone_def表使用原生 Lua 区域语法,包含kind、radius和origin。 - 实体查询 -- 返回球体内所有玩家表的 Lua 数组。
- 造成伤害 --
victim:deal_custom_damage(4.0)造成 4 点归属于 Boss 的伤害。 - 粒子反馈 -- 每个受害者显示紫色尘埃粒子,Boss 位置显示环境粒子。
- 清理 --
on_exit_combat取消任务并清理状态。
此示例使用原生 Lua 区域(context.zones:get_entities_in_zone())。脚本工具(context.script:zone(...))使用 EliteScript 字段名如 shape、Target 和 Target2。两者都有效 -- 简单形状使用原生区域,需要 EliteScript 高级目标解析时使用 context.script。
示例:多阶段 Boss 机制
本示例教授: 使用状态跟踪 Boss 阶段并在生命值阈值处切换行为。
完整能力文件(点击展开)
local function phase_one_attack(context)
context.boss:play_model_animation("slam")
local zone_def = { kind = "sphere", radius = 5, origin = context.boss:get_location() }
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do targets[i]:deal_custom_damage(3.0) end
context.world:spawn_particle_at_location(context.boss:get_location(), { particle = "EXPLOSION", amount = 3, speed = 0 })
end
local function phase_two_attack(context)
context.boss:play_model_animation("frenzy")
local zone_def = { kind = "sphere", radius = 8, origin = context.boss:get_location() }
local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end
context.world:spawn_particle_at_location(context.boss:get_location(), { particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 })
context.world:play_sound_at_location(context.boss:get_location(), "entity.wither.ambient", 1.0, 1.5)
end
return {
api_version = 1,
on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
context.state.phase_switched = false
end,
on_enter_combat = function(context)
if context.state.attack_task_id ~= nil then return end
context.state.attack_task_id = context.scheduler:run_every(100, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 100) then return end
phase_one_attack(tick_context)
end)
end,
on_game_tick = function(context)
if not context.cooldowns:check_local("phase_check", 20) then return end
if context.state.phase ~= 1 then return end
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
context.state.phase = 2
context.state.phase_switched = true
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
context.boss:play_model_animation("transform")
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end
context.state.attack_task_id = context.scheduler:run_every(40, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 40) then return end
phase_two_attack(tick_context)
end)
end
end,
on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}
说明
- 状态初始化 --
on_spawn将phase设为1,attack_task_id设为nil,phase_switched设为false。 - 阶段1攻击循环 --
on_enter_combat启动每 100 tick 执行phase_one_attack的重复任务。 - on_game_tick 中的阶段检查 -- 检查 20 tick 的冷却以避免每 tick 执行重逻辑。
- 生命值阈值 -- 降至 50% 或以下时开始过渡。
- 阶段过渡 -- 取消旧循环,播放变身动画,附近玩家收到消息和标题,启动更快的新循环(每 40 tick 而非 100)。
- 阶段2攻击 -- 更大的球体(8格),每次伤害略低但频率更高,应用缓慢药水效果,使用红色尘埃粒子和凋灵声音。
- 日志 --
context.log:info(...)写入服务器控制台。 - 清理 --
on_exit_combat取消当前活跃的攻击循环,无论 Boss 处于哪个阶段。
on_game_tick 每个服务器 tick 执行一次(每秒20次)。始终用冷却检查保护重逻辑。如果你的钩子超过 50ms,EliteMobs 将自动禁用该能力。
AI 生成提示
要让 AI 可靠地生成 Lua 能力,确保提示包含:精确的钩子名称,原生 Lua 区域或脚本工具(指明哪个),带键名和 tick 持续时间的本地和全局冷却,自定义模型动画名称,目标选择,效果类型,以及仅使用已文档化的方法名称。
良好的提示示例
编写一个 Lua 能力,使用
on_enter_combat每 80 tick 启动一个重复任务。每次 tick,创建一个以 Boss 为中心的原生 Lua 球体区域(半径6),查询其中的玩家,并对每个玩家造成 2.0 自定义伤害。使用本地冷却键"pulse",持续时间 80。在on_exit_combat中取消任务。在每个受害者位置生成 DUST 粒子(red=0, green=255, blue=100)。
QC 检查清单
- 文件返回恰好一个包含
api_version = 1的表。 - 钩子名称与钩子列表中的条目精确匹配。
context.player在使用前用== nil保护。context.state字段在on_spawn中初始化。- 每个
run_every在on_exit_combat中有对应的cancel_task。 - 调度器回调使用回调自身的上下文参数,而非外部
context。 - 冷却键是描述性字符串,持续时间以 tick 为单位。
on_game_tick钩子用冷却检查保护重逻辑。- 所有方法名存在于 API 参考中。
- 脚本工具表使用 EliteScript 字段名。
- 原生区域定义使用
kind、radius、origin、destination等。 - 能力不在钩子或回调中调用阻塞操作。
- 粒子规格使用有效的 Bukkit 粒子枚举名(UPPER_CASE)。
最佳实践
-
从小钩子开始并验证。 写一个发送日志消息的
on_spawn。确认它触发后再继续构建。 -
保持辅助函数为局部。 在返回表上方声明如
local function pick_action(context)的辅助函数。这使它们不在全局作用域中,避免与同一运行时中加载的其他 Lua 能力冲突。 -
将几何放入脚本工具。 如果需要锥体、旋转射线、平移射线或动画区域,使用带有 EliteScript 字段名的
context.script:zone(...)。脚本工具重用了经过实战检验的 EliteScript 区域引擎。 -
使用
context.state存储运行时状态。 不要使用 Lua 全局变量。context.state限定于单个 Boss 实例,并在该 Boss 的生命周期内跨钩子持久化。 -
使用命名的本地冷却键。 不要使用简单数字,使用描述性键如
"fire_touch"或"aoe_pulse"。这使调试更容易,并防止同一能力中不同冷却之间的意外冲突。 -
保持
on_game_tick轻量。 始终用冷却检查保护。如果你的逻辑每个 tick 都运行,必须在远少于 50ms 内完成,否则能力将被禁用。 -
完成时取消重复任务。 每个
run_every必须在on_exit_combat(可能还有on_death)中有对应的cancel_task。泄漏的任务浪费 CPU 并可能在 Boss 消失后导致空引用错误。 -
在调度器回调中使用新鲜上下文。 调度器回调(
run_every、run_after)接收新鲜上下文作为参数。始终使用该参数 -- 而非外部context-- 因为外部上下文可能包含过时的快照。 -
用
context.log:info()记录状态转换。 在开发期间,为阶段切换、冷却开始和调度器启动/停止添加日志。部署前删除或更改为context.log:debug()。 -
重用现有的 EliteScript 文档。 区域、目标、相对向量 和 条件 页面记录了脚本工具接受的相同字段名。不要在你的 Lua 能力中重复该信息 -- 只需引用它。
常见初学者错误
-
在调度器回调中使用外部
context。 外部上下文捕获了钩子运行时的快照。在run_every或run_after回调内,始终使用回调自身的参数(例如tick_context),它会给你一个新鲜的快照。 -
忘记取消重复任务。 如果你在
on_enter_combat中启动了run_every但从未取消,任务会一直运行直到 Boss 从服务器移除,即使战斗已结束。 -
不在
on_spawn中初始化状态。 如果你在on_game_tick中读取context.state.phase但从未在on_spawn中设置它,它将是nil,你的比较将表现异常。 -
没有 nil 守卫就检查
context.player。 在on_player_damaged_by_boss等钩子中,玩家几乎总是可用的 -- 但"几乎总是"不是"总是"。一个缺失的 nil 守卫可以使能力崩溃。 -
在脚本工具调用中使用 Lua 风格字段名。 脚本工具期望
targetType、Target、Target2、shape-- 而非target_type、target、target2、zone_shape。不匹配的名称会悄无声息地不产生结果。 -
在
on_game_tick中没有冷却门控就运行重逻辑。 这个钩子每个服务器 tick 都会触发。即使是在许多 Boss 上每秒重复 20 次的简单算术也会积累。 -
编造方法名。 如果方法未在 API 参考中列出,则不存在。常见错误包括写
entity:teleport(loc)而非entity:teleport_to_location(loc),或player:set_velocity(vec)而非player:set_velocity_vector(vec)。 -
使用
context.boss.health设置生命值。context.boss.health是只读快照。要治愈 Boss,使用context.boss:restore_health(amount)。 -
忘记
api_version = 1。 返回的表必须包含此字段,否则 EliteMobs 不会加载该能力。
