Skip to main content

Lua Scripting: NPC Scripts

webapp_banner.jpg

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.playerplus an NPC-specific context.npc table. Anything MagmaCore exposes to scripts is available here too.

Experimental Feature

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:

FieldTypeNotes
api_versionnumberRequired. Must be 1.
prioritynumberOptional. Lower values run first.
on_spawnfunctionRuns after the NPC spawns.
on_removefunctionRuns when the NPC is removed.
on_game_tickfunctionRuns every server tick while the NPC is valid. Keep this very light.
on_npc_interactfunctionRuns when a player interacts with the NPC.
on_npc_proximity_enterfunctionRuns once when a player enters this NPC's activation radius.
on_npc_proximity_leavefunctionRuns once when a player leaves this NPC's activation radius.
on_zone_enterfunctionRuns when an entity enters a zone this script is watching (see context.zones).
on_zone_leavefunctionRuns 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.

HookFires when
on_npc_proximity_enterA player moves from outside the NPC's activation radius to inside it.
on_npc_proximity_leaveA 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:

TableWhat it does
context.worldWorld 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.zonesCreate 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.schedulerrun_later(ticks, fn), run_repeating(delay, interval, fn), cancel(task_id).
context.cooldownsShared MagmaCore cooldowns: local_ready, local_remaining, check_local, set_local, global_ready, set_global.
context.loginfo(msg), warn(msg), error(msg) — writes to the server console.
context.eventThe current Bukkit event, when one is present. See below.
context.playerThe interacting/triggering player, when present. See below.
context.stateA 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

FieldTypeNotes
namestringNPC display name from the config.
filenamestringNPC config filename.
uuidstringRuntime NPC UUID.
activation_radiusnumberConfigured activation radius.
current_locationlocation tableSnapshot location when the backing entity exists.
entity_typestringBukkit entity type when the backing entity exists.

Methods

MethodArgsReturnsNotes
is_valid()-booleanWhether the NPC still has a valid backing entity.
get_location()-location tableCurrent NPC location, or spawn location if the entity is not available.
get_eye_location()-location tableCurrent eye location, or spawn location fallback.
get_activation_radius()-numberCurrent configured activation radius.
get_nearby_players(radius)numbertablePlayer wrappers within radius of the NPC.
face_direction_or_location(target)vector or locationnilFaces a direction vector or turns toward a location/player location.
say_greeting(player?)player, UUID, name, or nilnilSends a configured greeting. Defaults to the triggering player when available.
say_dialog(player?)player, UUID, name, or nilnilSends configured dialog. Defaults to the triggering player when available.
say_farewell(player?)player, UUID, name, or nilnilSends configured farewell text. Defaults to the triggering player when available.
play_model_animation(name)stringnilPlays 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 / MethodNotes
namePlayer name.
uuidPlayer 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 / MethodNotes
is_cancelledWhether 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.
playerThe 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:

MethodArgsNotes
run_later(ticks, callback) / run_after(ticks, callback)number, functionRuns once after a delay. Returns a task ID.
run_repeating(delay, interval, callback)number, number, functionRuns repeatedly after an initial delay. Returns a task ID.
run_every(interval, callback)number, functionRuns every interval ticks (initial delay 0). Returns a task ID.
cancel(task_id) / cancel_task(task_id)numberCancels 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:

MethodArgsReturnsNotes
local_ready(key?)stringbooleanTrue when the local cooldown has expired.
local_remaining(key?)stringnumberTicks remaining, or 0 when ready.
check_local(key?, duration)string, numberbooleanIf ready, starts the cooldown and returns true.
set_local(duration, key?)number, stringnilSets or resets the cooldown.
global_ready()-booleanTrue when the shared global cooldown is ready.
set_global(duration)numbernilStarts the global cooldown.
Unified cooldown API

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_tick hooks small. They run 20 times per second for every NPC script instance that defines them. (Scripts that don't declare on_game_tick are never ticked.)
  • Prefer on_npc_proximity_enter and on_npc_proximity_leave for 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.