Skip to content

Prediction ​

TSUKI has a built-in prediction engine for projectile leading and motion tracking. If you're not sure how to implement prediction yourself, start here.

Units ​

All spatial values are in game units (Source inches). Speeds are game-units/second (u/s). Multiply by 0.0254 to convert to meters / m/s.

Projectile Intercept ​

prediction.solve_linear(target, [source_pos,] speed, [bone], [extra_time]) ​

Solves straight-line projectile intercept: where to aim so a projectile of speed hits target, accounting for velocity and travel time.

ParamTypeDescription
targetplayerPlayer object to intercept
source_posvec3Projectile origin. Defaults to local player's eye position if omitted
speednumberProjectile speed in u/s
bonestringAim bone (e.g. "head"). Default = origin
extra_timenumberExtra lead time in seconds for cast windup + latency

bone and extra_time are order-flexible: a string is read as the bone, a number as extra_time.

Returns aim_pos: vec3, flight_time: number. Returns nil, nil if unreachable.

lua
local me = local_player()
local speed = me:get_active_projectile_speed()
if speed <= 0 then return end

local aim, flight = prediction.solve_linear(target, speed, "head", 0.06)
if aim then
    -- aim = world position to put the crosshair on
    -- flight = seconds of travel
end

With explicit source position:

lua
local origin = me:get_head_world()
local aim, flight = prediction.solve_linear(target, origin, speed, "head")

prediction.solve_ballistic(target, [source_pos,] speed, gravity, up_speed, [bone], [extra_time]) ​

Work in progress

Arced/gravity projectile variant. Currently untested.

Solves an arced projectile intercept accounting for gravity. Same return signature as solve_linear.

Raw Prediction ​

prediction.predict(target, time, [bone]) ​

Raw position extrapolation: where target will be in time seconds. Returns a vec3. Returns (0,0,0) if unavailable.

lua
local future_pos = prediction.predict(target, 0.5, "chest")

prediction.get_velocity(target) ​

Returns the target's tracked velocity and acceleration as two vec3 values (u/s and u/s²).

lua
local vel, accel = prediction.get_velocity(target)
local speed = vel:distance(vec3(0, 0, 0))  -- scalar speed

prediction.get_state(target) ​

Movement-state snapshot. Returns a table:

FieldTypeDescription
groundedboolOn the ground
airborneboolNot grounded (jumping / falling / in the air)
stationaryboolGrounded and barely moving
speednumberTotal velocity magnitude (u/s)
speed2dnumberHorizontal velocity magnitude (u/s)
vel_znumberVertical velocity (u/s, negative = falling)

Confidence ​

get_confidence tells you how predictable a target's motion is. When confidence is high, the prediction is reliable. When it's low (target is juking, stopping, reversing), you should fall back to aiming at the current position instead of trusting the lead.

prediction.get_confidence(target, [ramp_seconds]) ​

Returns a table:

FieldTypeDescription
stability0-1How steady the velocity is. Low = erratic
straightness0-1How straight the path is. Low = turning / strafing
decel0-1Deceleration factor. Low = stopping
overall0-1Combined confidence score
centervec3For strafing targets, the center of the oscillation
swing_radiusnumberHalf-width of side-to-side swing (u/s)

ramp_seconds (optional): how quickly confidence ramps up after motion stabilizes.

lua
local c = prediction.get_confidence(target)

if c.overall >= 0.90 then
    aim = predicted_pos       -- confident: full lead
elseif c.overall < 0.55 then
    aim = current_pos         -- uncertain: aim where they are
end

prediction.reset_confidence(target) ​

Resets one target's motion history (keeps the velocity tracker). Useful after displacing a target (e.g. a hook reel).

prediction.reset() ​

Clears all tracking state for every target.

Example ​

lua
function on_tick()
    local me = local_player()
    if not me then return end

    local speed = me:get_active_projectile_speed()
    if speed <= 0 then return end

    local target = targeting.find_closest_by_distance(60)
    if not target or not target:is_alive() then return end

    local origin = me:get_head_world()
    local aim, flight = prediction.solve_linear(target, origin, speed, "head", 0.06)
    if not aim then return end

    local c = prediction.get_confidence(target)
    local final
    if c.overall >= 0.90 then
        final = aim
    elseif c.overall < 0.55 then
        final = target:bone_pos("head") or target:get_position()
    else
        return  -- hysteresis gap: hold
    end

    -- convert final (world vec3) to a view angle and apply
end

For ability projectiles (e.g. Haze sleep dagger):

lua
local dagger = me:get_ability(slot.ability1)
if dagger and dagger.projectile_speed > 0 then
    local aim, t = prediction.solve_linear(target, dagger.projectile_speed, "chest")
end

Function Index ​

FunctionReturns
prediction.solve_linear(target, [source_pos,] speed, [bone], [extra])vec3, number | nil, nil
prediction.solve_ballistic(target, [source_pos,] speed, gravity, up_speed, [bone], [extra])vec3, number | nil, nil
prediction.predict(target, time, [bone])vec3
prediction.get_velocity(target)vec3 vel, vec3 accel
prediction.get_state(target)table
prediction.get_confidence(target, [ramp_seconds])table
prediction.reset_confidence(target)—
prediction.reset()—

Not affiliated with Valve Corporation.