Lua Scripting: Zones & Targeting
EliteMobs Lua powers offer two complementary approaches for defining spatial areas and resolving targets:
- Native zones (
context.zones) -- simple and direct. You build a zone definition as a plain Lua table and query it for entities or locations. Best for straightforward "is anything in this area?" checks. - Script utilities (
context.script) -- richer targeting, zone handles with watch/contains/entities methods, particle spawning, damage, and push actions. Uses the same field names as EliteScript Zones and EliteScript Targets for familiarity.
Both approaches are native Lua APIs. Pick whichever fits the complexity of your power.
Native Zones: context.zones
Native zones let you define zones as plain Lua tables and query them directly. No handles, no extra abstraction -- just a table describing a shape and a method call to query it.
Methods
| Method | Notes |
|---|---|
zones:get_entities_in_zone(zoneDef, options) | Returns an array of entity wrappers inside the zone |
zones:get_locations_in_zone(zoneDef, options) | Returns an array of location tables inside the zone |
zones:zone_contains(zoneDef, location[, "full"|"border"]) | Returns true if the location is inside the zone |
zones:watch_zone(zoneDef, callbacks, options) | Registers a persistent zone watcher that fires per tick |
Zone definition fields
| Field | Type | Default | Notes |
|---|---|---|---|
kind (or type) | string | — | "sphere", "dome", "cylinder", "cuboid", "cone", "static_ray", "rotating_ray", "translating_ray" |
radius | number | 0 | Zone radius (sphere, dome, cylinder, cone) |
height | number | 0 | Cylinder height |
origin | location | boss location | Center location |
destination | location | boss location | End point for rays and cones |
x, y, z | number | 0 | Cuboid half-extents |
thickness / point_radius / pointRadius | number | 0.5 | Ray thickness |
border_radius / borderRadius | number | 1 | Border width |
x_border, y_border, z_border | number | 1 | Cuboid border widths |
animation_duration | int | 0 | Animation length in ticks for animated rays |
pitch_pre_rotation, yaw_pre_rotation | number | 0 | Pre-rotation angles (rotating ray) |
pitch_rotation, yaw_rotation | number | 0 | Per-tick rotation angles (rotating ray) |
origin_end, destination_end | location | boss location | End positions for translating ray |
ignores_solid_blocks | boolean | true | Whether rays pass through solid blocks |
length | number | 0 | Only read when destination is omitted -- see below |
Every multi-word field also accepts camelCase (borderRadius, xBorder, animationDuration, pitchPreRotation, ignoresSolidBlocks, ...), so a spec copied from an EliteScript Zone: block mostly works as-is.
kind is case-sensitiveUnlike almost every other string in the Lua API, kind is matched exactly and must be lowercase. "SPHERE" or "Sphere" do not match anything, and the zone silently resolves to nothing -- queries just return an empty list with no console warning.
destination -- or a lengthorigin falls back to the boss's own location when omitted. destination falls back to origin projected length blocks along the facing direction stored in the origin location table.
That gives you two working ways to build a cone or ray:
-- Explicit endpoint
local explicit_ray = { kind = "static_ray", origin = context.boss:get_eye_location(),
destination = context.player:get_eye_location() }
-- Or a length along the origin's own facing
local forward_ray = { kind = "static_ray", origin = context.boss:get_eye_location(), length = 15 }
The length shorthand only aims somewhere useful when the origin table carries yaw/pitch. Location tables returned by the API (get_location(), get_eye_location(), current_location) do; a hand-written { x = .., y = .., z = .. } table does not, and neither does em.create_location(x, y, z) unless you pass its optional yaw and pitch.
Omit both and length defaults to 0, so the shape collapses to zero length and every query silently returns nothing.
Query options
| Key | Notes |
|---|---|
filter | "player" / "players", "elite" / "elites", "mob" / "mobs", "living" (default) |
mode | "full" (default) or "border" |
coverage | 0.0 to 1.0 -- fraction of locations to sample (default 1.0) |
Watch callbacks
| Key | Notes |
|---|---|
on_enter | function(entity) -- called when an entity enters the zone |
on_leave | function(entity) -- called when an entity leaves the zone |
Example: basic sphere query
Example
return {
api_version = 1,
on_spawn = function(context)
-- Store a zone definition for reuse
context.state.danger_zone = {
kind = "sphere",
origin = context.boss:get_location(),
radius = 10
}
end,
on_boss_damaged_by_player = function(context)
-- Update the origin to the boss's current position
context.state.danger_zone.origin = context.boss:get_location()
local players = context.zones:get_entities_in_zone(
context.state.danger_zone,
{ filter = "players" }
)
for _, player in ipairs(players) do
player:send_message("&cYou are in the danger zone!")
end
end
}
Example: zone watching with enter/leave
Example
return {
api_version = 1,
on_spawn = function(context)
context.zones:watch_zone(
{
kind = "sphere",
origin = context.boss:get_location(),
radius = 8
},
{
on_enter = function(entity)
entity:apply_potion_effect("SLOWNESS", 40, 1)
entity:send_message("&7You feel sluggish near the boss...")
end,
on_leave = function(entity)
entity:send_message("&aYou escape the slowing aura.")
end
},
{ filter = "players", mode = "full" }
)
end
}
Watcher notes:
- Watcher callbacks receive a single entity wrapper directly, not via
context.event - Watchers call their own
on_enter/on_leavecallbacks; they do not invoke the power's top-levelon_zone_enter/on_zone_leavehooks - Watchers are cleaned up automatically when the boss is removed
- Each watcher runs every tick, so keep callback logic lightweight
- The boss entity itself is excluded from zone queries
Script Utilities: context.script
The script utilities provide target resolution, zone handles, relative vectors, particle spawning, and combat actions.
context.script runs the EliteScript enginecontext.script is not merely "EliteScript-flavoured naming" -- the spec table you pass is converted to a plain Java map and handed straight to the same target, zone, relative-vector and particle classes that power YAML EliteScripts. There is no separate implementation.
What that means in practice:
- Every field documented on EliteScript Targets, EliteScript Zones and EliteScript Relative Vectors works in these spec tables, including any field this page does not list.
- Enum values are exact UPPER_SNAKE_CASE (
"NEARBY_PLAYERS","SPHERE","ZONE_FULL"), the same as YAML. - Field names are the YAML names, so
TargetandTarget2are capitalised whiletargetTypeandborderRadiusare camelCase. - YAML load-time behaviour applies too: an unparseable value logs an EliteScript warning and clears the field rather than falling back to its default.
This is the opposite of context.zones, which is a genuinely separate lightweight implementation using lowercase kind names.
Methods
| Method | Notes |
|---|---|
script:target(spec) | Creates a target handle from a target spec table |
script:zone(spec) | Creates a zone handle from a zone spec table |
script:relative_vector(spec[, actionLocation][, zoneHandle]) | Creates a relative-vector handle |
script:damage(targetHandle, amount[, multiplier]) | Deals damage to resolved targets |
script:push(targetHandle, vectorOrHandle[, additive]) | Pushes resolved targets |
script:set_facing(targetHandle, vectorOrHandle) | Sets facing direction for targets |
script:spawn_particles(targetHandle, particleSpec) | Spawns particles at resolved target locations |
Target handle methods
| Method | Notes |
|---|---|
handle:entities() | Returns array of entity wrappers |
handle:locations() | Returns array of location tables |
handle:first_entity() | Returns first entity or nil |
handle:first_location() | Returns first location or nil |
Target spec keys
| Key | Default | Notes |
|---|---|---|
targetType | "SELF" | Any EliteScript target type |
range | 20 | Range for nearby-type targets |
coverage | 1.0 | 0.0 to 1.0. Only honoured for zone target types; on any other type it is reset to 1.0 with a console warning |
offset | 0,0,0 | "x,y,z" string or { x = n, y = n, z = n } table |
relativeOffset | none | A relative-vector spec table, for an offset relative to the boss's facing |
location | none | Single location, for "LOCATION" |
locations | none | Location list, for "LOCATIONS" |
track | true | Whether to re-resolve moving targets |
All 17 EliteScript target types are accepted, not just the common ones: SELF, SELF_SPAWN, DIRECT_TARGET, NEARBY_PLAYERS, NEARBY_MOBS, NEARBY_ELITES, WORLD_PLAYERS, ALL_PLAYERS, LOCATION, LOCATIONS, ZONE_FULL, ZONE_BORDER, LANDING_LOCATION, ACTION_TARGET, INHERIT_SCRIPT_TARGET, INHERIT_SCRIPT_ZONE_FULL, INHERIT_SCRIPT_ZONE_BORDER. The ACTION_TARGET and INHERIT_* types only resolve to anything when the surrounding EliteScript context exists, so they are of little use from Lua.
Example: creating and using a target
Example
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
if not context.cooldowns:check_local("roar", 200) then return end
-- Find all players within 20 blocks
local nearby = context.script:target({
targetType = "NEARBY_PLAYERS",
range = 20
})
for _, player in ipairs(nearby:entities()) do
player:send_message("&eThe boss roars in fury!")
end
-- Single-entity access
local closest = nearby:first_entity()
if closest then
closest:show_title("&cRUN!", "&7The boss is targeting you")
end
end
}
Zone handle methods
| Method | Notes |
|---|---|
handle:full_target([coverage]) | Returns a target handle for the full zone volume |
handle:border_target([coverage]) | Returns a target handle for the zone border |
handle:full_locations([coverage]) | Returns locations in the full zone |
handle:border_locations([coverage]) | Returns locations on the zone border |
handle:full_entities() | Returns entities in the full zone |
handle:border_entities() | Returns entities on the zone border |
handle:contains(location[, "full"|"border"]) | Returns true if the location is inside the zone |
handle:watch(callbacks[, mode]) | Watches zone for enter/leave events, returns task ID |
Zone spec keys
| Key | Default | Notes |
|---|---|---|
shape | "CYLINDER" | "SPHERE", "DOME", "CYLINDER", "CUBOID", "CONE", "STATIC_RAY", "ROTATING_RAY", "TRANSLATING_RAY" |
radius | 5 | Sphere, dome, cylinder, cone |
height | 1 | Cylinder only |
x, y, z | 0 | Cuboid half-extents |
xBorder, yBorder, zBorder | 0 | Cuboid border widths |
borderRadius | 1 | Border width for sphere / dome / cylinder / cone |
pointRadius | 0.5 | Ray thickness |
animationDuration | 0 | Animation length in ticks for animated rays |
Target | {targetType = "SELF"} | Center target spec (table) -- uses the same target spec format |
Target2 | none | Second point. Required for cones and all ray shapes |
FinalTarget, FinalTarget2 | none | End positions, translating ray only |
filter | "PLAYER" | "PLAYER", "ELITE", "LIVING" -- note these are UPPERCASE here, unlike context.zones |
ignoresSolidBlocks | true | Rays only |
pitchPreRotation, yawPreRotation | 0 | Rotating ray only |
pitchRotation, yawRotation | 0 | Rotating ray only |
Keys that do not apply to the chosen shape are read without complaint and then ignored -- a height on a sphere does nothing.
Example: zone with damage and particles
Example
return {
api_version = 1,
on_enter_combat = function(context)
-- Create a sphere zone centered on the boss
if context.state.zone_task_id ~= nil then return end
local zone = context.script:zone({
shape = "SPHERE",
radius = 6,
Target = { targetType = "SELF" }
})
-- Spawn warning particles on the zone border
context.script:spawn_particles(
zone:border_target(0.3),
{ particle = "FLAME", amount = 1, speed = 0.02 }
)
-- Damage all players inside the zone
local targets = zone:full_target()
context.script:damage(targets, 5.0)
-- Repeat every 20 ticks
context.state.zone_task_id = context.scheduler:run_every(20, function(ctx)
local z = ctx.script:zone({
shape = "SPHERE",
radius = 6,
Target = { targetType = "SELF" }
})
ctx.script:spawn_particles(
z:border_target(0.3),
{ particle = "FLAME", amount = 1, speed = 0.02 }
)
ctx.script:damage(z:full_target(), 5.0)
end)
end,
on_exit_combat = function(context)
if context.state.zone_task_id ~= nil then
context.scheduler:cancel_task(context.state.zone_task_id)
context.state.zone_task_id = nil
end
end
}
Relative-vector spec keys
| Key | Notes |
|---|---|
SourceTarget | Source target spec (table) |
DestinationTarget | Destination target spec (table) |
normalize | boolean -- whether to normalize the resulting vector |
multiplier | Scale factor applied after normalization |
offset | "x,y,z" string or { x = n, y = n, z = n } table |
Relative-vector handle methods
| Method | Notes |
|---|---|
handle:resolve() | Returns the computed vector table |
Example: pushing targets with a relative vector
Example
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
if not context.cooldowns:check_local("knockback", 100) then return end
-- Build a vector from the boss toward the attacker
local vec = context.script:relative_vector({
SourceTarget = { targetType = "SELF" },
DestinationTarget = { targetType = "DIRECT_TARGET" },
normalize = true,
multiplier = 2.5
})
-- Push the attacker away
local target = context.script:target({
targetType = "DIRECT_TARGET"
})
context.script:push(target, vec)
end
}
Particle spec format
Particle specs can be a string, a single table, or an array of tables. Because this path runs the EliteScript particle engine, the keys are the EliteScript ones documented under SPAWN_PARTICLE:
| Key | Default | Notes |
|---|---|---|
particle | "FLAME" | Particle name (e.g. "FLAME", "DUST", "SMOKE") |
amount | 1 | Number of particles. 0 turns x/y/z into a velocity vector |
x, y, z | 0.01 | Offset/spread values, or velocity when amount is 0 |
speed | 0.01 | Particle speed |
red, green, blue | 255 | Colour for DUST, DUST_COLOR_TRANSITION, WITCH and other colour-data particles (0-255) |
toRed, toGreen, toBlue | 255 | Transition target colour for DUST_COLOR_TRANSITION |
material | "STONE" | Block or item shown by particles that carry block/item data (BLOCK, ITEM, FALLING_DUST, ...) |
relativeVector | none | A relative-vector spec. Setting it forces amount to 0 and overwrites x/y/z with the resolved direction |
These EliteScript keys are not the same as the ones context.world:spawn_particle_at_location(loc, spec) accepts. The world table has its own smaller reader with different defaults (amount 1, x/y/z/speed 0), no material and no relativeVector, and it additionally accepts to_red / to_green / to_blue in snake_case. See World & Environment.
For the full list of particle names, see the Enum Reference.
Important caveats
- Script utility handles are tied to the event context in which they were created. Do not store them in
context.statefor use in a later hook call. context.script:zone(...):watch(...)returns a task ID that you can cancel withcontext.scheduler:cancel_task().context.zones:watch_zone(...)returns nothing and is cleaned up automatically with the power.- Coverage values apply only to location-based resolution, not entity queries.
- All string values use the same UPPER_SNAKE_CASE enum names as EliteScript YAML (e.g.
"SELF","NEARBY_PLAYERS","SPHERE").
em Helper Namespace
The em namespace is available globally in all Lua power files. It provides convenience constructors for locations, vectors, and zone definitions.
Location and vector constructors
| Function | Notes |
|---|---|
em.create_location(x, y, z[, world][, yaw][, pitch]) | Returns a location table with an add(dx, dy, dz) method |
em.create_vector(x, y, z) | Returns a vector table |
Zone builder helpers
The em.zone sub-table provides builder functions that return zone definition tables compatible with context.zones. Each builder returns a table with chainable mutator methods.
| Function | Parameters | Mutators |
|---|---|---|
em.zone.create_sphere_zone(radius) | radius | :set_center(location) |
em.zone.create_dome_zone(radius) | radius | :set_center(location) |
em.zone.create_cylinder_zone(radius, height) | radius, height | :set_center(location) |
em.zone.create_cuboid_zone(x, y, z) | x, y, z (half-extents) | :set_center(location) |
em.zone.create_cone_zone(length, radius) | length, radius | :set_origin(location), :set_destination(location) |
em.zone.create_static_ray_zone(length, thickness) | length, thickness | :set_origin(location), :set_destination(location) |
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration) | length, point_radius, animation_duration | :set_origin(location), :set_destination(location) |
em.zone.create_translating_ray_zone(length, point_radius, animation_duration) | length, point_radius, animation_duration | :set_origin(location), :set_destination(location) |
length is only a fallbackThe cone and ray builders store length in the spec table, and context.zones reads it only when no destination is set -- the endpoint then becomes the origin projected length blocks along the origin's own facing direction.
So either chain :set_origin(...) and :set_destination(...), or chain :set_origin(...) with a location that carries yaw/pitch (such as context.boss:get_eye_location()) and let length do the work. Chaining neither leaves both at the boss's location with length unusable, and the shape has zero length.
Example
Example
return {
api_version = 1,
on_spawn = function(context)
-- Create a location offset from the boss
local boss_loc = context.boss:get_location()
local above = em.create_location(boss_loc.x, boss_loc.y + 5, boss_loc.z)
-- Create a sphere zone using the builder
local zone = em.zone.create_sphere_zone(10):set_center(boss_loc)
-- Use with native zone queries
local players = context.zones:get_entities_in_zone(zone, { filter = "players" })
for _, p in ipairs(players) do
p:send_message("&cYou are within the boss's aura!")
end
-- Create a directional vector
local push_vec = em.create_vector(0, 1.5, 0)
context.boss:set_velocity_vector(push_vec)
end
}
The em namespace is not per-instance -- all Lua power instances share the same em helper functions. The functions are pure constructors and do not carry any state.
Native Zones vs Script Utilities
Both systems work with the same underlying zone geometry. Here is when to use each:
| Use case | Recommended approach |
|---|---|
| Simple "is anything in this area?" check | Native zones (context.zones) |
| Quick entity query with a shape | Native zones |
NEARBY_PLAYERS, ZONE_FULL with coverage | Script utilities (context.script) |
| Animated zones (rotating/translating rays) | Either -- native zones support these shapes too |
Zone handles with watch/contains/entities methods | Script utilities |
| Particle spawning at zone locations | Script utilities (spawn_particles) |
| Damage and push actions tied to targeting | Script utilities (damage, push) |
| Relative vectors for directional effects | Script utilities (relative_vector) |
| Combining Lua control flow with rich targeting | Script utilities |
In practice, many powers use both. Native zones are great for the initial "are players nearby?" check in a cooldown guard, while script utilities handle the complex attack logic that follows.
Next Steps
- Examples & Patterns -- complete working powers you can study and adapt
- NPC Scripts -- NPC proximity hooks for simple approach/leave behavior
- Enum Reference -- valid values for Particle, Sound, Material, and other string constants
