Skip to content

Targeting

Target selection helpers and aim math. All functions are in the targeting.* namespace.

Functions

FunctionReturnsDescription
targeting.find_closest_by_fov(max_fov, max_dist)player | nilNearest target within max_fov degrees of crosshair (measured to the chest bone) AND within max_dist meters
targeting.find_closest_by_distance(meters)player | nilNearest target within meters
targeting.find_all_in_range(meters)tableAll players within meters
targeting.get_fov(player, bone)numberDegrees 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])booleanTrue 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])vec2Aim delta in mouse counts, with projectile lead
targeting.smoothness_compensation(speed)numberConverts 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-corrected

Argument 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.

lua
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
end

Pass 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 default 0 for 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:

  1. Presses the key bound to the given slot.
  2. Continues aiming at the target's bone for hold_time milliseconds.
  3. If the target dies during the hold, releases early.
  4. Yields each tick during the hold, call this from on_tick with a while true + coroutine.yield() pattern.

Example:

lua
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
end

Notes:

  • Yields each tick for hold_time milliseconds; 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() or is_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:

lua
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
end

get_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.

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.