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 accept plain Lua tables. Each query creates a new shape; a watcher retains one shape. Repeated queries of an animated ray therefore restart the animation rather than inspect the watcher's current shape. Use a script zone handle when several operations need the same animated shape.
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 | Shape origin: cylinder bottom, cone tip, or ray start |
destination | location | boss location | End point for rays and cones |
x, y, z | number | 0 | Cuboid dimensions; see the point/entity geometry limitation |
thickness / point_radius / pointRadius | number | 0.5 | Ray sample radius and spacing; use a positive value |
border_radius / borderRadius | number | 1 | Inner radius excluded from the outer shape |
x_border, y_border, z_border | number | 1 | Inner cuboid dimensions |
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 | Total rotation over the animation; the current animator applies positive increments only |
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 | Stored by builders, but an omitted destination currently resolves to the boss location before the length fallback can run; supply an explicit destination |
The numeric and boolean fields listed with underscores also accept camelCase (borderRadius, xBorder, animationDuration, pitchPreRotation, ignoresSolidBlocks). The location fields origin_end and destination_end require their underscored names. EliteScript Target tables cannot replace native location fields.
kind is case-sensitivekind 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.
An omitted origin or destination resolves to the boss's current location. The current location resolver prevents length from acting as a shorthand for an omitted destination. Supply distinct endpoints:
local explicit_ray = { kind = "static_ray", origin = context.boss:get_eye_location(),
destination = context.player:get_eye_location() }
For a translating ray, supply origin_end, destination_end, and a positive animation_duration to animate it. An omitted final endpoint stays at its starting position. Zero duration creates the static initial ray; negative duration is rejected. The final animation frame ends exactly at the requested endpoints.
Keep all endpoints and query locations in the boss's current world: native entity queries search that world. In a location table, omitted coordinates default to 0; an omitted world uses the boss's world, but an explicit world name must already be loaded. Dimensions must be finite and positive. Point tests, particle samples and entity tests are not interchangeable; the shared shape limitations include spherical entity checks for full domes, full entity-height checks for cylinders, and different cuboid bounds.
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) |
filter is case-insensitive; unknown values use living. mobs means non-player living entities, including elites. The boss itself is excluded, but players are not filtered by game mode. mode recognizes border case-insensitively; other values use full. Coverage randomly samples locations, not entities, and does not limit watcher membership. Use full for rays: their border locations are unsupported and can make a location query fail.
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
- A native watcher constructs its shape once. Its center does not follow the boss, and editing the original definition table does not move an existing watcher. Use fresh queries or a script zone with a tracked target for a moving area.
- Each watcher runs every tick, so keep callback logic lightweight
- The boss entity itself is excluded from zone queries
The first poll is one tick later; entities already inside trigger on_enter. Cancellation or cleanup does not send on_leave for remaining members. In this example the sphere stays at the spawn position. Slowness lasts 40 ticks after entry; staying inside does not refresh it, and leaving does not remove it early.
Script Utilities: context.script
The script utilities provide target resolution, zone handles, relative vectors, particle spawning, and combat actions.
context.script runs the EliteScript engineThe spec table is converted to a Java map and passed to the target, zone, relative-vector and particle classes used by YAML EliteScripts.
What that means in practice:
- The spec tables use the parsers documented on EliteScript Targets, EliteScript Zones and EliteScript Relative Vectors, including their current limitations. Accepting a field does not guarantee that the Lua handle has the action lifecycle or inherited context needed to use it.
- Enum values are exact UPPER_SNAKE_CASE (
"NEARBY_PLAYERS","SPHERE","ZONE_FULL"), the same as YAML. - Use the canonical YAML field names shown here:
Target,Target2,targetType,borderRadius. Their internal readers are case-insensitive. - Invalid values can produce warnings, fallback values or missing fields that fail later. Acceptance of the table does not validate the complete operation.
context.zones has a separate table reader with lowercase kind names, but uses the same geometry classes. Supply nonempty named spec tables: an empty Lua {} converts to a Java list, so even script:target({}) fails. Use script:target({targetType = "SELF"}) instead.
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 |
damage, push, set_facing and spawn_particles require a target handle, not an entity array, and return no result. They do not apply YAML action conditions, waits or filters. damage submits the amount through Bukkit damage with the boss as attacker; the optional multiplier applies to the player combat path, and final health loss depends on combat processing. push resolves targets and one vector now, then applies that vector to every target next tick. It replaces velocity unless additive = true, which adds to velocity at that tick. set_facing applies one direction to all targets immediately. Guard against zero or nonfinite directions.
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 |
Each method resolves again; first_* does not read a shared snapshot or promise the nearest result. DIRECT_TARGET refers to the event's captured target, not the boss's current combat target, and selects nothing when the event supplies none.
Target spec keys
| Key | Default | Notes |
|---|---|---|
targetType | "SELF" | Any EliteScript target type |
range | 20 | Half-width on each axis of the nearby-target search box; results are not sorted by distance |
coverage | 1.0 | 0.0 to 1.0. Only honoured for zone target types; values below 1.0 on other types are reset to 1.0 with a console warning |
offset | 0,0,0 | Comma-separated "x,y,z" string; a Lua vector table is not accepted by this EliteScript field |
relativeOffset | none | A relative-vector spec table; its source and destination targets determine the offset direction |
location | none | Serialized "world,x,y,z,yaw,pitch" string, for "LOCATION" |
locations | none | Array of serialized location strings, for "LOCATIONS" |
track | true | Whether to re-resolve moving targets |
The Lua handle path does not run the YAML action's target-caching step. Setting track = false on a static Lua target or zone therefore does not freeze its location. Static zone handles resolve their origins again when queried; animated zone handles cache their shapes when created. For a fixed area, supply a LOCATION target with explicit coordinates, or use a native zone with a captured origin. offset and relativeOffset move every location a handle resolves, including LOCATION, LOCATIONS, SELF_SPAWN, entity-based targets and sampled zone locations; they do not move the entities returned by entities(), and LANDING_LOCATION and INHERIT_SCRIPT_TARGET ignore them. A zone handle's full_target() and border_target() take no offset, so put offsets on the zone's Target and Target2 specs. See shared attributes.
All 17 EliteScript target names are accepted by the parser: 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. Standalone Lua handles do not supply inherited action data or a landing location. Avoid INHERIT_* and LANDING_LOCATION there; they can fail during resolution rather than returning useful targets. ACTION_TARGET is meaningful only in a compatible relative-vector/action context. See the inherited-zone limitations.
For ZONE_FULL and ZONE_BORDER, obtain the target from a zone handle; a standalone script:target(...) has no zone. Lua particle helpers keep the default coverage of 1.0; they do not apply the YAML SPAWN_PARTICLE action's default of 0.3.
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 along each axis
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
-- First resolved entity; this does not promise the nearest player.
local first = nearby:first_entity()
if first then
first: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; must fit the entity's full height for entity queries |
x, y, z | 0 | Cuboid dimensions; see the point/entity geometry limitation |
xBorder, yBorder, zBorder | 0 | Inner cuboid dimensions |
borderRadius | 1 | Inner radius for sphere / dome / cylinder / cone |
pointRadius | 0.5 | Positive ray sample radius and spacing |
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.
contains tests a point, not an entity bounding box. watch uses the same callback format and first-poll behavior as native watchers. Cancel its returned ID with context.scheduler:cancel_task(id); cancellation does not emit leave callbacks. A static script zone can follow SELF as it is resolved each tick; an animated handle retains its shapes. borderRadius is the inner radius, not border thickness. Use full targets for rays.
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)
-- First repeat after 20 ticks, then every 20 ticks.
context.state.zone_task_id = context.scheduler:run_repeating(20, 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
}
This example damages players immediately when showing particles; it does not provide a warning delay. Repeats start after 20 ticks and stop on combat exit. The default inner radius of 1 makes the border a thick shell inside radius 6, not a thin ring.
Relative-vector spec keys
| Key | Notes |
|---|---|
SourceTarget | Source target spec (table) |
DestinationTarget | Destination target spec (table) |
normalize | Boolean, default false; whether to normalize the resulting vector |
multiplier | Scale factor applied after normalization, default 1 |
offset | Comma-separated "x,y,z" string, default "0,0,0"; not a Lua vector table |
Relative-vector handle methods
| Method | Notes |
|---|---|
handle:resolve() | Returns the computed vector table |
Supply both target specs. Resolution uses the first location from each: destination minus source, optional normalization, multiplication, then a world-axis offset. Empty results or different worlds return zero before applying the offset; coincident points with normalization can produce a nonfinite vector. ACTION_TARGET uses the supplied actionLocation, defaulting to the boss location captured when the handle is created. It is not independently replaced for each pushed entity. Pass a zone handle as the third argument when the vector's targets require that zone.
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. With 0, directional behavior depends on the particle type |
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 and particles requiring Color data (0-255); WITCH does not use these values |
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 |
Dust size is fixed at 1. moveToTarget is parsed but does not move particles. Unsupported particle data types are skipped with a warning. The helper shifts zone and LOCATION / LOCATIONS particle positions by +0.5 on X and Z; the locations returned by handles are unshifted.
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.
- Target, shape and filter enum values use the same UPPER_SNAKE_CASE names as EliteScript YAML (e.g.
"SELF","NEARBY_PLAYERS","SPHERE"). Watch/contains modes remain"full"or"border".
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 (cuboid dimensions) | :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) |
The cone and ray builders store length, but the current native location resolver uses the boss location for an omitted destination. Chain both :set_origin(...) and :set_destination(...); do not rely on length to aim the shape. Translating rays also need origin_end, destination_end, and a positive animation_duration set on the returned table.
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, boss_loc.world)
-- 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
}
Each script instance has its own Lua environment and em namespace. These constructors return new tables; the location add(...) method and zone setters mutate the table they are called on.
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) | Script zone handles for repeated reads; native watchers can also retain an animated shape |
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 |
Both APIs can be used in one power. Choose based on the targeting and lifecycle needed: native queries default to living entities excluding the boss; script zones default to players.
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
