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 | The living enemy closest to the crosshair, within max_fov degrees of its 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 alive enemies within meters |
targeting.get_fov(player, bone) | number | Degrees from the crosshair to that bone. bone can be a name string or a raw bone index. The two forms behave differently on a bad bone (see below) |
targeting.is_on_target(player, [extra_radius]) | boolean | True if the crosshair ray hits any of the player's real hitbox capsules. extra_radius (game units, default 0) is added to each capsule's own radius, so it widens the test instead of setting the 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 is in [1, 100] |
get_fov
bone.* constants are strings (bone.chest == "chest"). A bone name the model does not have does not return 9999. "head" falls back to the head position and any other name to the player's origin, so you get the angle 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 is an index into the raw skeleton array and returns 9999 when the index is out of range. Any other argument type returns 9999.
is_on_target
is_on_target tests a ray from the camera against every capsule in player:hitboxes(). These are the same shapes the triggerbot uses, so with no second argument it tells you whether the shot would hit. A negative extra_radius shrinks the capsules, but the effective radius never goes below 0.
If a model has no hitboxes (#player:hitboxes() == 0), the test uses the skeleton instead, and the second argument is the capsule radius, default 6.0. On a normal model, passing 6 widens the test slightly.
compute_aim_delta
compute_aim_delta is overloaded. Argument 2 can be 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-correctedA vec3 is used as an absolute aim point and is 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 an error, so check delta:empty().
Returns a vec2 in raw mouse counts (not pixels) that aims at the target, with linear velocity lead and no gravity. The angular error in degrees is divided by the configured sensitivity (0.022 degrees per count when uncalibrated), so the same error gives a different number at a different sensitivity. Pass it to input.move_mouse. input.snap_to takes a player and a bone, not a delta. Do not use it as 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. The second argument of input.move_mouse is speed, not delta.y. Splitting it into two numbers raises a bad argument #1 to 'move_mouse' error.
To aim without computing the delta yourself, use input.snap_to. It calls compute_aim_delta for you and moves at most 12 counts per call, so call it every tick.
fire_and_hold
Fires an ability or item at a target and keeps the aim locked for a set time after firing. It runs as a coroutine and yields internally, so your local variables and script state are kept across the hold.
Use it for abilities that release after a windup, such as Haze's Sleep Dagger. If your aim drifts during the throw animation the projectile misses, so fire_and_hold keeps the aim on the target through it. 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 it for projectile abilities (Vindicta stake, Grey Talon arrow, etc.). For hitscan abilities, leave it at the default0, which applies no lead.
Returns: nothing (nil). If the target dies during the hold, the function returns early and does not tell the caller. If your script needs to react to that, check target:is_alive() after the call.
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, stops aiming and returns early.
- Yields each tick during the hold, so call it only from
on_tick. From an event callback or a timer it raises an error.
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 the call runs 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. Write your own loop only if you want extra conditions, such as requiring the target to 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() returns only enemies, so is_enemy() is not needed here. Teammates come from get_teammates().
Related
aim.can_reach(target, [bone], [detail]) tells you whether you can make a shot at a point from where you are standing. It works with or without the internal running. It is documented with the rest of the aim path in Aim.