Targeting
Target selection helpers and aim math. All functions are in the targeting.* namespace.
Functions
| Function | Returns | Description |
|---|---|---|
targeting.find_closest_by_fov(max_fov, max_dist) | player | nil | Nearest target within max_fov degrees of crosshair (measured to the chest bone) AND within max_dist meters |
targeting.find_closest_by_distance(meters) | player | nil | Nearest target within meters |
targeting.find_all_in_range(meters) | table | All players within meters |
targeting.get_fov(player, bone) | number | Degrees from crosshair to that bone. bone may be a name string or a raw bone index. The two differ on a bad bone (see below) |
targeting.is_on_target(player, [extra_radius]) | boolean | True if the crosshair ray intersects any of the player's real hitbox capsules. extra_radius (game units, default 0) is ADDED to each capsule's own radius. It widens the test; it is not the test radius |
targeting.compute_aim_delta(player, bone_or_pos, [proj_speed]) | vec2 | Aim delta in mouse counts, with projectile lead |
targeting.smoothness_compensation(speed) | number | Converts a smoothing speed in (0, 1] to a compensation factor. The input speed is clamped to [0.01, 1] before dividing, so the returned 1/speed ranges [1, 100] |
get_fov
bone.* constants are strings (bone.chest == "chest"). On the string path an unknown bone name does not return 9999: it falls back to the hero's named bones, then head_world for "head", then the player's origin, so you get a real angle measured to the feet. 9999 only comes back for a stale handle, no local player, or a position of exactly (0,0,0).
An integer bone indexes the raw skeleton array and does return 9999 when the index is out of range. Any other argument type returns 9999.
is_on_target
is_on_target ray-tests the camera against every capsule in player:hitboxes(), the same shapes the triggerbot uses, so with no second argument it answers "would this shot hit". A negative extra_radius shrinks the capsules; the effective radius is floored at 0.
The older bone-pair sweep runs only when a model carries no hitbox set (#player:hitboxes() == 0). There the second argument keeps its old meaning as the radius, defaulting to 6.0. Passing the old 6 on a normal model still works. It widens the test slightly.
compute_aim_delta
compute_aim_delta is overloaded; argument 2 accepts a bone name, a bone index, or a world position:
targeting.compute_aim_delta(world_pos) -- vec3 aim point, no lead possible
targeting.compute_aim_delta(player, bone_name, proj_speed) -- string bone name
targeting.compute_aim_delta(player, bone_index, proj_speed) -- integer index into the raw bone array
targeting.compute_aim_delta(player, world_pos, proj_speed) -- vec3 aim point, still lead-correctedArgument 2 is tested as vec3, then integer, then string. A vec3 is used as an absolute aim point, still led by the player's velocity (unlike the one-argument compute_aim_delta(world_pos)). Any other type, an out-of-range bone index, or a bone that resolves to (0,0,0) returns vec2(0, 0) instead of raising, so check delta:empty().
Returns a vec2 of raw mouse counts, not pixels, to aim at the target with linear velocity lead (no gravity). The angular error in degrees is divided by the configured sensitivity (0.022 degrees per count when uncalibrated), so the same error is a different number on a different sensitivity. Pass it to input.move_mouse or input.snap_to; it is not a screen offset.
proj_speed defaults to instant (no leading) if omitted.
local target = targeting.find_closest_by_fov(8.0, 30.0)
if target and target:is_visible() then
local proj_speed = local_player():get_active_projectile_speed()
local delta = targeting.compute_aim_delta(target, "head", proj_speed)
input.move_mouse(delta) -- pass the vec2 itself
-- input.move_mouse(delta, 0.25) -- optional second arg smooths the move
endPass the vec2 itself - input.move_mouse's second argument is speed, not delta.y. Splitting it into two numbers raises bad argument #1 to 'move_mouse' (vec2 expected, got number).
For "snap to target" behavior in one call, use input.snap_to instead. It calls compute_aim_delta internally and moves the mouse in one step.
fire_and_hold
Fires an ability or item at a target and maintains aim lock for a duration after firing. Handles the common "fire then hold lock" pattern as a coroutine, yields internally, so local variables and script state are preserved across the hold.
Use for abilities where cast-time aim matters more than key-press-time aim - Haze's Sleep Dagger is the canonical case: pressing the key doesn't release the dagger immediately, and if your aim drifts during the throw animation the projectile misses. fire_and_hold keeps the snap locked through the animation. See the Haze Auto Dagger example for complete usage.
Parameters:
target(player). The player to aim at.slot(number). The ability or item slot to fire (slot.ability1-slot.ability4,slot.item1-slot.item4).opts(table, optional). Configuration:bone. Bone to aim at. Default:bone.chest.hold_time(number). Milliseconds to maintain aim after firing. Default:200.projectile_velocity(number). Projectile speed in game units per second, for lead prediction. Set for projectile-based abilities (Vindicta stake, Grey Talon arrow, etc.). Leave at the default0for hitscan abilities. No leading is applied.
Returns: nothing (nil). If the target dies mid-hold, the function returns early but does not signal this to the caller. Check target:is_alive() after the call if your script needs to react to the early break.
Behavior:
- Presses the key bound to the given slot.
- Continues aiming at the target's bone for
hold_timemilliseconds. - If the target dies during the hold, releases early.
- Yields each tick during the hold, call this from
on_tickwith awhile true+coroutine.yield()pattern.
Example:
function on_tick()
while true do
local target = targeting.find_closest_by_fov(10, 30)
if target and target:is_targetable() then
local angle = targeting.get_fov(target, bone.chest)
if angle and angle < 1.5 then
fire_and_hold(target, slot.ability1, {
bone = bone.chest,
hold_time = 200
})
else
snap_to_target(target, { bone = bone.chest })
end
end
coroutine.yield()
end
endNotes:
- Yields each tick for
hold_timemilliseconds; code after it resumes on the first tick after the hold. - Works with both abilities (
slot.ability1-slot.ability4) and items (slot.item1-slot.item4). - Pair with
is_ability_ready()oris_item_ready()to avoid firing on cooldown.
Custom filtering
The targeting helpers (find_closest_by_fov, find_closest_by_distance, find_all_in_range) already filter for alive, valid enemies. You only need to write your own loop if you want extra conditions on top of that, like requiring the target be visible or targetable:
local function find_enemy(max_fov)
local best, best_fov = nil, max_fov
for _, p in ipairs(get_players()) do
if p:is_alive() and p:is_targetable() and p:is_visible() then
local fov = targeting.get_fov(p, "head")
if fov < best_fov then
best, best_fov = p, fov
end
end
end
return best
endget_players() already returns only enemies, so you don't need to call is_enemy() here. Teammates live on a separate accessor, get_teammates(), and are always populated.
Related
aim.can_reach(target, [bone], [detail]) answers whether a shot at a point can be made from where you are standing, and it works with or without the internal running. It is documented with the rest of the aim path in Aim.