Lua Scripting: NPC Scripts
EliteMobs NPC Lua scripts are standalone .lua files that attach to NPC configs. They are separate from boss Lua powers: boss powers live in plugins/EliteMobs/powers/, while NPC scripts live in plugins/EliteMobs/npc_scripts/.
NPC scripts now run on the same unified MagmaCore scripting runtime as boss powers, FreeMinecraftModels props, and FMM items. That means an NPC script gets the full shared scripting surface — context.world (including strike_lightning), context.zones, context.scheduler, context.cooldowns, context.log, context.event, and context.player — plus an NPC-specific context.npc table. Anything MagmaCore exposes to scripts is available here too.
NPC Lua scripts are still experimental. The NPC-specific hooks and the context.npc helpers may change. The shared tables (context.world, context.zones, context.scheduler, context.cooldowns, context.log, context.event, context.player) are the same ones documented in the Scripting Engine and Lua API Reference.
File Location
Create NPC script files in:
plugins/
EliteMobs/
npc_scripts/
wave.lua
Script filenames are matched exactly, including the .lua extension. Subfolders are not scanned in this first version. If an NPC config references a missing script, EliteMobs logs a warning and the NPC still spawns.
Attaching Scripts To NPCs
Add a scripts: list to the NPC config:
scripts:
- wave.lua
Multiple scripts can be attached to one NPC:
scripts:
- wave.lua
- greeting_particles.lua
Scripts run in priority order. Lower priority values run first. If priority is omitted, it defaults to 0.
Script Shape
Every NPC script must return one table:
return {
api_version = 1,
priority = 0,
on_spawn = function(context)
context.state.spawned = true
context.npc:play_model_animation("idle")
end
}
Only these top-level fields are accepted:
| Field | Type | Notes |
|---|---|---|
api_version | number | Required. Must be 1. |
priority | number | Optional. Lower values run first. |
on_spawn | function | Runs after the NPC spawns. |
on_remove | function | Runs when the NPC is removed. |
on_game_tick | function | Runs every server tick while the NPC is valid. Keep this very light. |
on_npc_interact | function | Runs when a player interacts with the NPC. |
on_npc_proximity_enter | function | Runs once when a player enters this NPC's activation radius. |
on_npc_proximity_leave | function | Runs once when a player leaves this NPC's activation radius. |
on_zone_enter | function | Runs when an entity enters a zone this script is watching (see context.zones). |
on_zone_leave | function | Runs when an entity leaves a watched zone. |
Unknown top-level keys are rejected during script loading. Helper functions should be declared as local functions above the returned table.
Proximity Hooks
NPC proximity hooks use the NPC's activationRadius config value.
| Hook | Fires when |
|---|---|
on_npc_proximity_enter | A player moves from outside the NPC's activation radius to inside it. |
on_npc_proximity_leave | A player moves from inside the NPC's activation radius to outside it. |
These hooks are tracked per NPC and per player by the server-side proximity scanner. Standing near one NPC does not block another NPC from firing its own enter event, and staying inside the radius does not spam repeated enter events.
The normal greeting, dialog, and quest indicator behavior still runs. The Lua hook adds behavior on top of it.
Shared Scripting Surface
Because NPC scripts run on the unified runtime, every NPC hook also receives the shared MagmaCore context tables used by FreeMinecraftModels scripts. EliteMobs boss powers run on the same runtime but use boss-specific variants for several tables. Full method lists live in the Lua API Reference and the Scripting Engine:
| Table | What it does |
|---|---|
context.world | World effects and queries: strike_lightning, spawn_particle, play_sound, set_block_at, place_temporary_block, spawn_entity, spawn_firework, get_nearby_entities, get_nearby_players, raycast, and more. Both coordinate (strike_lightning(x, y, z)) and location-table (strike_lightning_at_location(loc)) forms are accepted. |
context.zones | Create spatial zones (create_sphere, create_cylinder, create_cuboid) and watch them for enter/leave callbacks, which fire your on_zone_enter / on_zone_leave hooks. |
context.scheduler | run_later(ticks, fn), run_repeating(delay, interval, fn), cancel(task_id). |
context.cooldowns | Shared MagmaCore cooldowns: local_ready, local_remaining, check_local, set_local, global_ready, set_global. |
context.log | info(msg), warn(msg), error(msg) — writes to the server console. |
context.event | The current Bukkit event, when one is present. See below. |
context.player | The interacting/triggering player, when present. See below. |
context.state | A plain Lua table that persists for this NPC script instance until the NPC is removed. |
Example: Smite On Interact
return {
api_version = 1,
on_npc_interact = function(context)
-- NPC scripts can now reach the full world API.
context.world:strike_lightning_at_location(context.npc:get_location())
end
}
context.npc
context.npc is available in every NPC hook.
Fields
| Field | Type | Notes |
|---|---|---|
name | string | NPC display name from the config. |
filename | string | NPC config filename. |
uuid | string | Runtime NPC UUID. |
activation_radius | number | Configured activation radius. |
current_location | location table | Snapshot location when the backing entity exists. |
entity_type | string | Bukkit entity type when the backing entity exists. |
Methods
| Method | Args | Returns | Notes |
|---|---|---|---|
is_valid() | - | boolean | Whether the NPC still has a valid backing entity. |
get_location() | - | location table | Current NPC location, or spawn location if the entity is not available. |
get_eye_location() | - | location table | Current eye location, or spawn location fallback. |
get_activation_radius() | - | number | Current configured activation radius. |
get_nearby_players(radius) | number | table | Player wrappers within radius of the NPC. |
face_direction_or_location(target) | vector or location | nil | Faces a direction vector or turns toward a location/player location. |
say_greeting(player?) | player, UUID, name, or nil | nil | Sends a configured greeting. Defaults to the triggering player when available. |
say_dialog(player?) | player, UUID, name, or nil | nil | Sends configured dialog. Defaults to the triggering player when available. |
say_farewell(player?) | player, UUID, name, or nil | nil | Sends configured farewell text. Defaults to the triggering player when available. |
play_model_animation(name) | string | nil | Plays a custom model animation if one exists. Safe no-op otherwise. |
context.player
context.player is available in on_npc_interact, on_npc_proximity_enter, and on_npc_proximity_leave. It is nil in lifecycle hooks that do not involve a player.
It is the shared MagmaCore player wrapper — the same full living-entity/player table boss powers and FMM scripts use, so it exposes far more than the basics (health, potion effects, send_message, show_title, show_action_bar, get_held_item, raycasting, and more). See the Lua API Reference for the complete list. Commonly used here:
| Field / Method | Notes |
|---|---|
name | Player name. |
uuid | Player UUID. |
get_location() | Current player location. |
get_eye_location() | Current player eye location. |
send_message(text) | Sends a chat message. Supports color codes. |
Always nil-check context.player before using it in shared helper functions.
context.event
context.event is nil when the hook has no Bukkit event. When present it is the shared MagmaCore event table:
| Field / Method | Notes |
|---|---|
is_cancelled | Whether the underlying event is cancelled (only meaningful for cancellable events). |
cancel() | Cancels the event, when it is cancellable. |
uncancel() | Un-cancels the event, when it is cancellable. |
player | The actor of the event (e.g. the interacting player), as a player wrapper, when present. |
For the interacting/proximity player, prefer context.player (it is set for those hooks).
State, Scheduler, And Cooldowns
context.state is a plain Lua table that persists for this NPC script instance until the NPC is removed.
context.scheduler is the shared MagmaCore scheduler. Both the MagmaCore names and the EliteMobs run_after / run_every names work — they are aliases for the same behavior:
| Method | Args | Notes |
|---|---|---|
run_later(ticks, callback) / run_after(ticks, callback) | number, function | Runs once after a delay. Returns a task ID. |
run_repeating(delay, interval, callback) | number, number, function | Runs repeatedly after an initial delay. Returns a task ID. |
run_every(interval, callback) | number, function | Runs every interval ticks (initial delay 0). Returns a task ID. |
cancel(task_id) / cancel_task(task_id) | number | Cancels an owned task. |
Scheduler callbacks receive a fresh context. They do not receive the original context.player or context.event. All owned tasks are cancelled automatically when the NPC is removed.
context.cooldowns is the shared MagmaCore cooldown table:
| Method | Args | Returns | Notes |
|---|---|---|---|
local_ready(key?) | string | boolean | True when the local cooldown has expired. |
local_remaining(key?) | string | number | Ticks remaining, or 0 when ready. |
check_local(key?, duration) | string, number | boolean | If ready, starts the cooldown and returns true. |
set_local(duration, key?) | number, string | nil | Sets or resets the cooldown. |
global_ready() | - | boolean | True when the shared global cooldown is ready. |
set_global(duration) | number | nil | Starts the global cooldown. |
NPC scripts now use the shared MagmaCore cooldown order (check_local(key?, duration)), the same as boss powers and FreeMinecraftModels scripts. Earlier experimental NPC builds used check_local(duration, key?) — update any old scripts to the shared order.
Example: Wave On Approach
This script makes the NPC face the entering player and play the wave custom model animation. Proximity enter already fires once per NPC/player pair while the player remains inside the radius; the cooldown keeps quick leave/re-enter cycles from replaying the animation too often.
return {
api_version = 1,
priority = 0,
on_npc_proximity_enter = function(context)
if context.player == nil then return end
if context.cooldowns:check_local("wave:" .. context.player.uuid, 60) then
context.npc:face_direction_or_location(context.player:get_location())
context.npc:play_model_animation("wave")
end
end
}
play_model_animation(name) safely does nothing when the NPC has no custom model or the model does not have that animation.
Performance Guidelines
- Keep
on_game_tickhooks small. They run 20 times per second for every NPC script instance that defines them. (Scripts that don't declareon_game_tickare never ticked.) - Prefer
on_npc_proximity_enterandon_npc_proximity_leavefor proximity behavior instead of polling nearby players every tick. - Use
context.cooldowns:check_local(...)to gate animations, sounds, and particle bursts. - Use
context.scheduler:run_repeating(...)with a reasonable interval when a behavior does not need to run every tick. - Avoid large searches in Lua.
context.npc:get_nearby_players(radius)is fine for small local checks, but broad scanning should stay in the plugin runtime.
Related Pages
- Creating NPCs -- NPC config fields, including
activationRadius - Lua Getting Started -- boss Lua powers
- Scripting Engine -- shared Lua concepts and the unified runtime
- Lua API Reference -- full method list for
context.world,context.player,context.zones, and more
