Skip to main content

Lua Scripting: Getting Started

webapp_banner.jpg

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, and context.entities APIs
  • World & Environment -- particles, sounds, lightning, spawning, and the context.world API
  • Zones & Targeting -- native Lua zones, Script Utilities targeting, and the context.zones / context.script APIs
  • 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 behavior setting
Still evolving

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.

Relationship To EliteScript

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.

Boss powers vs. NPC scripts

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 termsIn Lua that usually means
EventsHook names such as on_spawn or on_boss_damaged_by_player
Cooldownscontext.cooldowns (see context.cooldowns below)
ActionsDirect method calls such as context.world:spawn_particle_at_location(...) or context.script:damage(...)
Targetscontext.script:target({...}) -- uses EliteScript Target field names
Zonescontext.script:zone({...}) or native context.zones helpers -- see Zones & Targeting
Relative vectorscontext.script:relative_vector({...}) or context.vectors
Script flowYour 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:

  1. Save the file in plugins/EliteMobs/powers/
  2. Add the .lua filename to the boss powers: list
  3. Run /em reload to load changed files, then spawn a fresh test boss. Reload affects the plugin and its active content, so use a staging server.
  4. 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_player is the hook name
  • context.player is the player involved in that hook
  • return exits 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:

  1. Create the file and make on_spawn work.
  2. Change to the actual hook you want.
  3. Confirm the hook has the data you expect, such as context.player or context.event.
  4. Add a message or sound first, before damage or particles.
  5. Add cooldowns.
  6. Add one gameplay effect.
  7. 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.lua and powers/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​

FieldRequiredTypeNotes
api_versionYesNumberUse integer 1
priorityNoNumberUse an integer. Lower values run first; defaults to 0. YAML powers use priority 0
supported hook keysNoFunctionMust use one of the exact hook names listed in the Hook Reference

Validation Rules​

  • The file must return a table.
  • api_version is required and must currently be 1.
  • priority must 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
}
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.

info

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 with log:info(msg), log:warn(msg), and log: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. A nil key 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; use scheduler: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 assume ctx.player survives a delay. Cancel combat-only repeats in on_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, and context.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: