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.
| Param | Type | Description |
|---|---|---|
target | player | Player object to intercept |
source_pos | vec3 | Projectile origin. Defaults to local player's eye position if omitted |
speed | number | Projectile speed in u/s |
bone | string | Aim bone (e.g. "head"). Default = origin |
extra_time | number | Extra 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.
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
endWith explicit source position:
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.
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²).
local vel, accel = prediction.get_velocity(target)
local speed = vel:distance(vec3(0, 0, 0)) -- scalar speedprediction.get_state(target) ā
Movement-state snapshot. Returns a table:
| Field | Type | Description |
|---|---|---|
grounded | bool | On the ground |
airborne | bool | Not grounded (jumping / falling / in the air) |
stationary | bool | Grounded and barely moving |
speed | number | Total velocity magnitude (u/s) |
speed2d | number | Horizontal velocity magnitude (u/s) |
vel_z | number | Vertical 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:
| Field | Type | Description |
|---|---|---|
stability | 0-1 | How steady the velocity is. Low = erratic |
straightness | 0-1 | How straight the path is. Low = turning / strafing |
decel | 0-1 | Deceleration factor. Low = stopping |
overall | 0-1 | Combined confidence score |
center | vec3 | For strafing targets, the center of the oscillation |
swing_radius | number | Half-width of side-to-side swing (u/s) |
ramp_seconds (optional): how quickly confidence ramps up after motion stabilizes.
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
endprediction.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 ā
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
endFor ability projectiles (e.g. Haze sleep dagger):
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")
endFunction Index ā
| Function | Returns |
|---|---|
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() | ā |