Skip to content

Prediction ​

TSUKI has a built-in prediction engine for projectile leading and motion tracking.

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.

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-1min(stability, straightness) * decel
centervec3For strafing targets, the centroid of the oscillation in the horizontal plane; only x/y are meaningful - z is always 0
swing_radiusnumberMax distance from the centroid (orbit radius, in game units)

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.