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 | Notes |
|---|
kind | string | "sphere", "dome", "cylinder", "cuboid", "cone", "static_ray", "rotating_ray", "translating_ray" |
radius | number | Zone radius (sphere, dome, cylinder, cone) |
height | number | Cylinder height |
origin | location | Center location (defaults to boss location if omitted) |
destination | location | End point for rays and cones |
x, y, z | number | Cuboid half-extents |
length | number | Length for cones and rays |
thickness / point_radius | number | Ray thickness |
border_radius | number | Border width (default 1) |
x_border, y_border, z_border | number | Cuboid border widths (default 1) |
animation_duration | int | Animation length in ticks for animated rays |
pitch_pre_rotation, yaw_pre_rotation | number | Pre-rotation angles (rotating ray) |
pitch_rotation, yaw_rotation | number | Per-tick rotation angles (rotating ray) |
origin_end, destination_end | location | End positions for translating ray |
ignores_solid_blocks | boolean | Whether rays pass through solid blocks (default true) |
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 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. They use the same field names as EliteScript Zones and EliteScript Targets for familiarity -- the enum names (like "NEARBY_PLAYERS", "SPHERE", "ZONE_FULL") are identical so that authors already comfortable with EliteScript can transfer that knowledge directly.
context.script is a native Lua API. It is not a bridge to YAML or to the EliteScript runtime. It simply reuses the same naming conventions so you do not have to learn a second set of enum 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 | Notes |
|---|
targetType | "SELF", "DIRECT_TARGET", "NEARBY_PLAYERS", "NEARBY_MOBS", "NEARBY_ELITES", "ZONE_FULL", "ZONE_BORDER", "ALL_PLAYERS", "WORLD_PLAYERS", "SELF_SPAWN", "LOCATION" |
range | Range for nearby-type targets |
coverage | 0.0 to 1.0 for zone targets |
offset | "x,y,z" string or { x = n, y = n, z = n } table |
track | Whether to re-resolve moving targets |
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 | Notes |
|---|
shape | "SPHERE", "DOME", "CYLINDER", "CUBOID", "CONE", "STATIC_RAY", "ROTATING_RAY", "TRANSLATING_RAY" |
radius | Zone radius |
height | Cylinder height |
x, y, z | Cuboid half-extents |
borderRadius | Border width |
pointRadius | Ray thickness |
animationDuration | Animation length in ticks for animated rays |
Target | Center target spec (table) -- uses the same target spec format |
Target2 | Second point for rays and cones |
filter | Entity filter |
ignoresSolidBlocks | boolean |
pitchPreRotation, yawPreRotation | Pre-rotation angles |
pitchRotation, yawRotation | Per-tick rotation angles |
Example: zone with damage and particles
Example
return {
api_version = 1,
on_enter_combat = function(context)
-- Create a sphere zone centered on the boss
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.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
}
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:
| Key | Notes |
|---|
particle | Particle name (e.g. "FLAME", "DUST", "SMOKE") |
amount | Number of particles |
x, y, z | Offset/spread values |
speed | Particle speed |
red, green, blue | Color for DUST / DUST_COLOR_TRANSITION (0-255) |
toRed, toGreen, toBlue | Transition target color for DUST_COLOR_TRANSITION |
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.state for use in a later hook call.
- Zone
watch() returns a task ID that you can cancel with context.scheduler:cancel_task().
- 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) |
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