Lua Scripting: Getting Started
What You'll Learn
This page teaches you to write your first Lua power for EliteMobs, from an empty file all the way to a working boss ability. By the end you will understand hooks, context, cooldowns, and the general shape of every Lua power file.
Once you are comfortable with the basics, continue with the companion pages:
- Hooks & Lifecycle -- hook names, firing order, and context availability
- NPC Scripts -- NPC lifecycle, interaction, and proximity hooks
- Boss & Entities -- the
context.boss,context.player,context.players, andcontext.entitiesAPIs - World & Environment -- particles, sounds, lightning, spawning, and the
context.worldAPI - Zones & Targeting -- native Lua zones, Script Utilities targeting, and the
context.zones/context.scriptAPIs - Examples & Patterns -- complete working powers you can study and adapt
- Enums & Values -- links to Spigot Javadocs for Particle, Material, PotionEffectType, and other string constants
- Troubleshooting -- common errors, debugging tips, and the QC checklist
- Behavior Programs -- Lua-authored mob AI selected with a boss's
behaviorsetting
Most bundled EliteMobs powers use Lua, but helper names and behavior can change between releases. Check custom powers on a staging server before using them in production, and revisit the reference after updating.
Lua does not replace the existing eliteScript: YAML system, and EliteScript is not deprecated.
- Use EliteScript when you want declarative, YAML-driven scripting with the existing Actions, Targets, Zones, Conditions, Cooldowns, and Relative Vectors pages.
- Use Lua power files when you need variables, loops, random selection, reusable helper functions, persistent per-boss state, and more traditional programming flow.
The Script Utilities API (context.script) passes your spec tables to the EliteScript target, zone, relative-vector and particle classes. Their parsers and limitations carry over, but standalone Lua handles do not provide every YAML action context. Inherited targets and landing locations need context that those handles lack; Lua helpers also do not automatically apply YAML action waits, conditions or target caching. The zones and targeting companion page explains these differences.
What Lua Powers Are
Lua powers are standalone .lua files that live in the normal EliteMobs powers tree and are referenced exactly like normal power files.
This page teaches boss Lua powers. NPC Lua scripts are a separate feature: they live in plugins/EliteMobs/npc_scripts/, attach through an NPC config scripts: list, and use NPC-specific hooks such as on_npc_proximity_enter. See NPC Scripts.
What Lua Powers Are Good At
Lua powers shine when you need:
- Attack rotations and random move pickers
- Persistent state between hooks with
context.state - Delayed and repeating actions without building everything out of YAML waits
- Custom helper functions shared across one file
- Complex branching that would be awkward in pure EliteScript
- Boss logic that still wants to reuse EliteScript-style targeting and zone definitions
If your power is mostly "trigger event, do a few scripted actions", the existing EliteScript docs are still the fastest and clearest way to build it. If your power needs real program flow, Lua is the tool for that job.
Who This Page Is For
This page is written for three kinds of readers:
- Someone who already knows EliteScript and wants to learn Lua without learning "real programming" all at once
- Someone who is new to EliteMobs scripting and needs a complete exact-name reference
- Someone using AI to draft powers and needing enough detail to tell when the AI invented something fake
You do not need to become a full Lua developer before writing useful powers. For most practical EliteMobs powers, the only things you really need are:
- How to place a valid hook in the returned table
- How to read values from
context - How to stop early with
if ... then return end - How to call a few helper methods exactly
- How to copy the right target and zone specs from the existing EliteScript docs
Mental Model: EliteScript vs Lua
If you know EliteScript, this comparison is the fastest way to understand Lua powers:
| If you think in EliteScript terms | In Lua that usually means |
|---|---|
| Events | Hook names such as on_spawn or on_boss_damaged_by_player |
| Cooldowns | context.cooldowns (see context.cooldowns below) |
| Actions | Direct method calls such as context.world:spawn_particle_at_location(...) or context.script:damage(...) |
| Targets | context.script:target({...}) -- uses EliteScript Target field names |
| Zones | context.script:zone({...}) or native context.zones helpers -- see Zones & Targeting |
| Relative vectors | context.script:relative_vector({...}) or context.vectors |
| Script flow | Your own Lua if, helper functions, timers, and state |
The biggest difference is this:
- EliteScript says what should happen in YAML.
- Lua lets you decide when, why, and which branch should happen using code.
So if EliteScript feels like "configuration", Lua feels like "configuration plus decision-making".
Tiny Lua Primer
Most powers use a few Lua concepts: variables (local x = 5), functions (function foo() end), if checks (if x then ... end), tables ({key = value}), and nil (no value). Semicolons are optional. Tables use braces; functions and control-flow blocks close with end.
For a complete walkthrough with examples, see the MagmaCore Lua Scripting Engine — Tiny Lua Primer. That primer is shared across all Nightbreak plugins, so learning it once applies everywhere.
Your First Working Power, Built Slowly
If you are completely new, this is the best "first win" progression.
Before Step 1: Save It and Attach It to a Boss
Save the Lua file in the normal EliteMobs powers folder, for example:
plugins/
EliteMobs/
powers/
first_test.lua
Then add that filename to a boss config using the normal powers: list:
powers:
- first_test.lua
So the full beginner loop is:
- Save the file in
plugins/EliteMobs/powers/ - Add the
.luafilename to the bosspowers:list - Run
/em reloadto load changed files, then spawn a fresh test boss. Reload affects the plugin and its active content, so use a staging server. - Trigger the hook and check both its visible result and the console. Saving the file or spawning another boss without reloading does not refresh an already loaded definition.
If you need more background on boss files, power lists, or how custom bosses are structured, see Creating Custom Bosses. That page covers boss setup in detail, including how the powers: list works.
Step 1: Make the File Load
return {
api_version = 1,
on_spawn = function(context)
end
}
The loader validates Lua syntax, the returned table and hook names. An empty hook has no visible result, so silence alone does not prove the file was discovered or attached. Add the sound in Step 2 and spawn a fresh boss to check that path.
Step 2: Make the Boss Do One Visible Thing
return {
api_version = 1,
on_spawn = function(context)
context.boss:play_sound_at_self("entity.blaze.ambient", 1.0, 1.0)
end
}
Now you have the most important beginner proof: your hook is firing.
Step 3: React to a Player Hit
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
}
This teaches three core ideas:
on_boss_damaged_by_playeris the hook namecontext.playeris the player involved in that hookreturnexits early when the data you need is missing
Step 4: Prevent Spam With a Cooldown
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
}
Local cooldown ticks represent 50 milliseconds of elapsed time: 60 is three seconds even when server TPS drops. This named cooldown is shared across powers on this boss, not between different bosses. Prefix keys with your power's name when combining custom powers. This example checks only its local key, not the boss's global cooldown.
Step 5: Add One Real Effect
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
}
This creates real world lightning, not a visual-only flash. The cooldown is set before the effect so events triggered by that effect see it. This local cooldown of 100 ticks is five seconds of elapsed time.
First Real Workflow
When building a brand-new Lua power, use this order:
- Create the file and make
on_spawnwork. - Change to the actual hook you want.
- Confirm the hook has the data you expect, such as
context.playerorcontext.event. - Add a message or sound first, before damage or particles.
- Add cooldowns.
- Add one gameplay effect.
- Only after that, add helpers, state, scheduler logic, or zones.
That order makes debugging dramatically easier because only one thing changes at a time.
Where Lua Files Live
Place .lua files in the same folder tree as normal power .yml files:
plugins/
EliteMobs/
powers/
mycoolpower.lua
attack_push.yml
subfolder/
myotherpower.lua
Lua powers are auto-discovered from the power directories EliteMobs already loads.
Optional YAML metadata
A custom mycoolpower.lua can have a loaded mycoolpower.yml or mycoolpower.yaml beside it in the same directory. Discovery reads that YAML's isEnabled, effect, powerType, powerCooldown, and globalCooldown; isEnabled: false prevents that custom Lua file from registering. With no matching metadata, it uses no effect and the MISCELLANEOUS power type. Lua hooks still control when attacks run and must perform their own cooldown checks.
A loaded same-stem YAML file beside a bundled Lua power applies the same fields to that power. Without one, the bundled power keeps its shipped metadata. See Creating Powers for the metadata fields.
How Bosses Reference Lua Powers
Boss files still use the normal powers: list:
powers:
- attack_push.yml
- mycoolpower.lua
No special field is required. Lua powers are not loaded through eliteScript:.
File Naming Rules
- Bosses reference Lua powers by filename, not by folder path.
- EliteMobs currently registers discovered Lua powers by basename only.
- Avoid duplicate names like
powers/fire.luaandpowers/bosses/fire.lua. EliteMobs loads only the file whose full path sorts first alphabetically and logs a warning about the skipped copy unless both files are identical.
Premade Lua Powers
Most bundled powers are written in Lua. The current source defines 68 concrete premade Lua powers, installed under plugins/EliteMobs/powers/ when missing, including attack_fire.lua, frost_cone.lua, meteor_shower.lua and shield_wall.lua. Read these files for examples, bearing in mind that an edited local copy may differ from the version shipped with your plugin.
Premade files are only written if missing, so edits survive restarts. When an update changes a bundled power, an unedited copy of an earlier shipped version is moved to plugins/MagmaCore/outdated files/EliteMobs/ at startup and the current version is written in its place; edited copies stay. Removing a bundled file lets the next plugin initialization regenerate the shipped version; preserve any custom edits you still need before replacing it.
Shared source fragments
Boss Lua powers can include a helper file with a standalone comment directive:
-- @include helpers/common.inc
The loader inserts the referenced file's text before validating the power. Paths are relative to the file containing the directive and must stay inside the top-level power file's directory, including when symbolic links are resolved. Circular includes, nesting beyond 16 levels, files larger than 4 MiB, and expanded source larger than 4,194,304 characters are rejected.
Use a non-.lua extension such as .inc for helper fragments so discovery does not also try to load them as standalone powers. Declare helper functions above the power's final return table. This directive is specific to boss power loading; it does not enable Lua require or filesystem access, and NPC scripts do not process it.
Legacy .yml names still resolve
Boss configs referencing attack_fire.yml keep working. For a .yml or .yaml reference, EliteMobs first checks for a loaded Lua power with the same basename, then falls back to the literal name. A file that failed validation or was disabled is not a loaded replacement. Keep basename spelling and case consistent. To customize a bundled Lua power, edit its existing file and reload; creating duplicate basenames in other directories is not a reliable override mechanism.
Minimal File Contract
Every Lua power must return a table. That table is intentionally strict.
Required and Optional Top-Level Fields
| Field | Required | Type | Notes |
|---|---|---|---|
api_version | Yes | Number | Use integer 1 |
priority | No | Number | Use an integer. Lower values run first; defaults to 0. YAML powers use priority 0 |
| supported hook keys | No | Function | Must use one of the exact hook names listed in the Hook Reference |
Validation Rules
- The file must return a table.
api_versionis required and must currently be1.prioritymust be numeric if present.- Every extra top-level key must be a supported hook name.
- Every hook key must point to a function.
- Unknown top-level keys are rejected.
This means helper functions and local constants should live above the final return, not inside the returned table unless they are actual hooks.
The file body is evaluated during validation and again in a separate Lua environment for each boss power runtime. context exists only as a hook or callback argument, not as a top-level global. Local variables can persist for that runtime, but validation-time values are not carried into it. The runtime removes file/OS/Java access and require; use the include directive for source fragments. An uncaught hook or callback error closes that runtime and cancels its owned tasks; it is not retried on the next hit. Correct the file and reload before trying again.
Copy-Paste Starter Templates
Smallest Valid Lua Power
return {
api_version = 1,
on_spawn = function(context)
end
}
Recommended Starter Template
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
}
Larger File Layout
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
}
Hooks
Hooks are specially named functions in the table you return. EliteMobs calls them when something happens — the boss spawns, takes damage, enters combat, etc. You already saw on_spawn and on_boss_damaged_by_player in the tutorial above.
The most common hooks to start with are on_spawn, on_boss_damaged_by_player, on_enter_combat, and on_exit_combat. For the full list of all hooks, which context keys are available in each, and how execution order works, see Hooks & Lifecycle.
What Is context?
Every hook function receives one argument called context. Think of it as a toolbox that EliteMobs hands you each time something happens — it contains everything you need to interact with the boss, the player, the world, cooldowns, and more.
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
You don't create context yourself — EliteMobs creates it and passes it to your hook. You just read from it and call methods on it.
Each hook receives a new context. Its context.state table belongs to this power's runtime on this boss and can store data for later hooks and callbacks. Other powers have separate state; closing or reloading the runtime discards it.
Key context APIs
Here are the most important APIs you'll use from context:
-
context.state— A plain Lua table retained for this power's runtime on this boss. Use it to store phase numbers, task IDs, and flags between hooks. It is not saved across runtime removal or reload. -
context.log— Console logging withlog:info(msg),log:warn(msg), andlog:debug(msg). Include a power name in your message when you need to distinguish several powers' output. -
context.cooldowns— Named cooldowns and a global cooldown shared by the boss's powers. Explicit keys share one boss-wide namespace, so prefix them with your power's name. Anilkey selects this Lua filename's default cooldown.cooldowns:check_local(key, ticks)checks readiness and sets the local cooldown in one call; it does not check or set the global cooldown. See Hooks & Lifecycle for the full API. -
context.scheduler—scheduler:run_after(ticks, callback)delays one callback.scheduler:run_every(ticks, callback)repeats with no initial interval delay; usescheduler:run_repeating(delay, interval, callback)for an explicit first delay. Callbacks receive a fresh context without the triggering event or player. Use that callback argument, and deliberately save any needed player UUID or position; do not assumectx.playersurvives a delay. Cancel combat-only repeats inon_exit_combat; otherwise owned tasks continue until cancelled or the runtime closes. See Hooks & Lifecycle. -
context.boss/context.player— The boss and player involved in the current event. See Boss & Entities for all fields and methods. -
context.world— Spawning particles, entities, sounds, lightning, blocks. See World & Environment. -
context.zones/context.script— Zone geometry, targeting, damage, particles. See Zones & Targeting.
Method Syntax: : vs .
For an explanation of : vs . method syntax in Lua, see the MagmaCore Lua Scripting Engine page. Entity methods and shared context services accept both forms. Event setters context.event.set_damage_amount(value) and context.event.multiply_damage_amount(multiplier) require dot syntax; a colon passes the event table as the first argument. context.event:transfer_damage(target, amount) does accept a colon. Global constructors such as em.create_location(...) use a dot.
Next Steps
Now that you know the basics, explore the rest of the Lua scripting documentation:
- Hooks & Lifecycle -- hook names, firing order, and which context keys are available in each hook
- NPC Scripts -- NPC file location, hooks, proximity events, and
context.npc - Boss & Entities --
context.boss,context.player,context.players,context.entities, andcontext.event - World & Environment -- particles, sounds, lightning, spawning, and
context.world - Zones & Targeting -- native Lua zones, Script Utilities targeting/zones/particles, and relative vectors
- Examples & Patterns -- complete working powers with walkthroughs
- Enums & Values -- links to Spigot Javadocs for Particle, Material, PotionEffectType, and more
- Troubleshooting -- common errors, debugging tips, and a QC checklist
For YAML-based scripting, the EliteScript pages remain the canonical reference:
