Globals
Any script can call the top-level functions on this page and the game, tsuki, storage, json and timer namespaces it covers.
entities has its own page, Entities. aim, trace and internal are in the Internal API Reference.
Player Access
| Function | Returns | Description |
|---|---|---|
local_player() | player | nil | The local player object. nil when there is no local player, for example in the main menu |
get_players() | table | Enemy players. Teammates are in get_teammates() |
get_all_players() | table | All valid players except the local player |
get_teammates() | table | Teammate players only |
get_dead_players() | table | Players who are dead right now, with their respawn timer. See below |
get_dormant_players() | table | Living players your client is not receiving updates for, with where and when they were last seen. See below |
local me = local_player()
for _, p in ipairs(get_players()) do
if p:is_enemy() and p:is_alive() then
-- ...
end
endDead and dormant players
get_players() and the other lists only hold living players your client is receiving. Two more lists cover the rest. Both return an array of plain tables (not player objects), and do not include you.
| Field | Both lists | Description |
|---|---|---|
hero_name, hero_id | yes | The hero |
team, is_enemy | yes | Team number, and whether it is the other team |
player_name, steam_id | yes | The player |
score | yes | The scoreboard table, when it has been read |
respawn_in | dead only | Seconds until the player respawns. Absent when unknown |
last_position | dormant only | vec3 where the player was last seen. Absent if they were never seen this match |
last_seen | dormant only | Seconds since then. Absent when last_position is |
health, max_health | dormant only | The last health the game sent |
A dormant player is one the game stopped sending you, usually because nobody on your team can see them. Their position is not updated, which is why they are not in get_players().
for _, d in ipairs(get_dead_players()) do
if d.is_enemy and d.respawn_in then
print(string.format("%s respawns in %.0f s", d.hero_name, d.respawn_in))
end
endWorld Entities
| Function | Returns | Description |
|---|---|---|
get_world_entities([type]) | WorldEntity[] | All tracked world entities. Optional type string filters by coarse type name (e.g. "sinner", "tower") or exact engine class name (e.g. "citadel_breakableprop") |
Each WorldEntity has:
| Field | Type | Description |
|---|---|---|
type | string | Coarse type name ("sinner", "tower", "trooper_boss", "unknown", ...) |
class_name | string | Exact engine class name |
x, y, z | number | World position |
health, max_health | number | Current and maximum health |
team | integer | Team id |
distance | number | Distance from the local player, in metres (already converted) |
subclass | integer | Separates subtypes that share one class_name, e.g. golden statue vs soul crate |
currency_value | integer | Soul value of the pickup. 0 for non-soul rows |
active | boolean | Whether the pickup is available (false while on respawn cooldown) |
pickup_name | nil | Always nil. Tell pickups apart by type and subclass, as Powerup ESP does |
rejuv_in | number | rejuv rows only: seconds until it can be taken. It goes to 0 and below once it can be taken, and is -1 when unknown. -1 on every other row |
rejuv_taken | boolean | Rejuvenator rows only: true once it has been taken. false on every other row |
rejuv_hits | integer | rejuv rows only: punches the crystal has left (3 to 0), or -1 when unknown. Other rows always read 3, so check e.type == "rejuv" first |
-- Print every Sinner's Sacrifice
for _, e in ipairs(get_world_entities("sinner")) do
print(string.format("sinner at %.0f,%.0f hp=%d/%d", e.x, e.y, e.health, e.max_health))
end
-- Check for breakable props regardless of sub-type
for _, e in ipairs(get_world_entities("citadel_breakableprop")) do
print(e.class_name, e.subclass)
endTime
| Function | Returns | Description |
|---|---|---|
clock() | number | High-resolution time in seconds. Use for deltatime and debouncing |
game_time() | number | Match clock time in seconds (pause-aware HUD timer) |
get_match_time() | number | Alias for game_time() |
is_paused() | boolean | True while the match clock is frozen (a team pause, or the 3s unpause countdown) |
sim_time() | number | Raw client SimTime. Keeps advancing through pauses and pre-game |
game.time() | number | Same as game_time() |
game.sim_time() | number | Same as sim_time() |
game.tick_count() | integer or nil | The game's current tick number. nil, never 0, until the game clock is running |
game.tick_interval() | number or nil | Seconds per game tick: 0.015625 on a 64 tick server. nil until it has been checked. Keeps its value through a pause |
game.latency() | number or nil | Your network latency in milliseconds. nil until it has been read |
game.start_time() | number or nil | SimTime the match clock started. nil before the horn |
game.frame_dt() | number | Seconds since the previous tick, capped at 1. Use it to drive animation |
game.server_time() | number | "Now" on the clock the game's time stamps use. Stops during pauses. See below |
game.state() | integer, string | The match phase: 7, "in_progress" while the match runs. See below |
game.match_id() | integer | nil | The match id. nil outside a match and in bot matches |
clock() counts real seconds since the engine started and never pauses. game_time() and its alias get_match_time() follow the match clock and freeze while is_paused() is true. sim_time() is the raw client SimTime and keeps running through pauses and pre-game.
Do not diff absolute timestamps against sim_time()
The game's absolute timestamps run on the match clock. These are cast_delay_start, cooldown_start / cooldown_end, channel_start, cast_completed, and a modifier's expires_at. They freeze during pauses. If you subtract them from sim_time(), the result is too large by all the pause and pre-game time so far.
For "how much is left", use the pre-computed fields ability.cooldown and modifier.remaining. Both already handle pauses. Use sim_time() only for deltas between two sim_time() samples.
If you do need "now" on the same clock as those timestamps, use game.server_time():
local ab = local_player():get_ability(slot.ability1)
if ab and ab.cast_delay_start then
local since_cast = game.server_time() - ab.cast_delay_start
end-- Correct: read the pre-computed remaining time (already pause-aware)
local ab = local_player():get_ability(slot.ability1)
if ab and ab.cooldown > 0 then
print(string.format("Ability 1: %.1fs left", ab.cooldown))
end
-- Correct: sim_time is fine for a delta between two sim_time samples
local started = sim_time()
-- ... some ticks later ...
local elapsed = sim_time() - startedgame.state() returns the phase as a number and a name: 0 "invalid", 1 "init", 2 "waiting_for_players", 3 "hero_selection", 4 "match_intro", 5 "loading_map", 6 "pregame", 7 "in_progress", 8 "post_game", 9 "play_of_the_game", 10 "abandoned", 11 "end". Any other number comes with the name "unknown".
game.tick_count() is the game's tick number. It is nil for about the first second of a match and while the match is paused, never 0. Game ticks are not the same rate as on_tick.
game.tick_interval() is the length of one game tick in seconds. Use it instead of writing 1/64 into your script. It is nil when it cannot be confirmed, keeps its value through a pause, and is nil again on a new map until it has been checked.
Output
| Function | Returns | Description |
|---|---|---|
print(...) | - | Writes to the app console, the Windows console, and the client's tsuki.log. Lines are prefixed [script_name] in the app console and the log file, and [Lua:script_name] in the Windows console. Arguments are converted to strings and joined with tabs, as in standard Lua. The log file keeps at most 30 lines per script every 10 seconds (see below) |
toast(text, [duration], [style]) | - | Centered HUD toast. Optional per-call styling. See below |
notify(text, [duration], [color]) | - | Notification in the top-left corner, Standard rendering modes only. Default duration 3.0, color white. See below |
play_sound(filename) | - | Play a WAV file from assets\sounds\. Pass the filename only, no path separators |
toast("ready")
notify("warning", 5.0)
play_sound("alert.wav") -- plays assets\sounds\alert.wavLog lines look like [2026-08-23 21:04:11.882] [LUA] [my_script] ready. They stay after the session ends, so the log tells a script that never ran from one that ran quietly.
The log file keeps only the first 30 lines each script prints in every 10 seconds, so a script that prints every tick cannot fill it. The next time the script prints after those 10 seconds, the log gets one line saying how many were left out, for example [my_script] (212 more print lines in the last 10 s were not logged). The app console still shows every line.
toast(text, [duration], [style])
A centered HUD toast notification.
duration: seconds to display. Leave it out or pass0to use the global default from the menu settings.style: optional table of per-call overrides. Any key you leave out uses the global default set in the menu:
| Key | Type | Notes |
|---|---|---|
bg | {r, g, b, a} | Background fill |
text | {r, g, b, a} | Text color |
outline | {r, g, b, a} | Outline / drop-shadow color |
scale | number | Size multiplier (1.0 = default) |
Toasts are deduplicated by text only. If the same string is already showing or queued, the call is dropped, even with a different style table, so toast("X") called every tick in on_tick shows once.
toast("Hit", 2, {bg = {r = 255, g = 0, b = 0}}) -- queued
toast("Hit", 2, {bg = {r = 0, g = 255, b = 0}}) -- dropped (same text)
toast("Hit!", 2) -- different text: replaces "Hit"The style colors must be {r, g, b, a} tables. A packed draw.color() number is ignored and the global default is used instead.
Only one toast shows at a time. A toast with different text replaces the one on screen straight away, and when several arrive before the overlay's next frame, only the newest shows. TSUKI's own parry toasts use the same spot.
toast("Target locked", 2, {
bg = {r = 40, g = 15, b = 50, a = 220},
text = {r = 200, g = 140, b = 255},
scale = 1.2
})A demo script where each button shows a different style preset:
-- Toast Style Demo
-- Click buttons to see different toast styles
script_info = {
name = "Toast Style Demo",
description = "Showcases custom toast styling",
}
ui.new_tab("toast_demo", "Toast Demo")
ui.button("toast_demo", "styles", "Default Toast", function()
toast("Default style, uses your global settings", 3)
end)
ui.button("toast_demo", "styles", "Success (Green)", function()
toast("Parry successful!", 3, {
bg = {r=20, g=60, b=20, a=220},
text = {r=100, g=255, b=100},
outline = {r=80, g=200, b=80, a=120},
})
end)
ui.button("toast_demo", "styles", "Danger (Red)", function()
toast("LOW HP, retreat!", 4, {
bg = {r=80, g=10, b=10, a=230},
text = {r=255, g=80, b=80},
outline = {r=255, g=60, b=60, a=150},
scale = 1.3,
})
end)
ui.button("toast_demo", "styles", "Info (Blue)", function()
toast("Cooldown ready", 2, {
bg = {r=15, g=25, b=60, a=220},
text = {r=120, g=180, b=255},
outline = {r=80, g=140, b=220, a=100},
})
end)
ui.button("toast_demo", "styles", "Big Gold", function()
toast("ULTRA KILL!", 5, {
bg = {r=40, g=30, b=5, a=240},
text = {r=255, g=215, b=0},
outline = {r=255, g=180, b=0, a=160},
scale = 1.8,
})
end)notify(text, [duration], [color])
A notification in the top-left corner of the screen. Default duration 3.0, color white. It only shows when Rendering Mode in the TSUKI menu is Standard (GPU) or Standard (CPU). In the other modes it draws nothing, so use toast for a message every player sees. Unlike toast, the same text is not merged. At most 10 show at once, and an 11th removes the oldest.
colormust be a packed integer, the value thatdraw.color(r, g, b, [a])returns.- Unlike
toast,notifydoes not accept an{r, g, b, a}table. A table, or a float such as3.0, is ignored without an error. The notification falls back to white.
notify("boss up", 5.0, draw.color(255, 80, 80)) -- correct
notify("boss up", 5.0, { r = 255, g = 80, b = 80 }) -- ignored, shows whiteInput shortcuts
| Function | Returns | Description |
|---|---|---|
slot_to_key(slot) | integer | VK code for an ability, item, jump, slide or weapon_melee slot. Returns 0, not nil, for any other slot. See Game Keybinds |
item_slot_to_key(config_slot) | integer | VK code for a 0-based item config slot (0-3). Equivalent to slot_to_key(config_slot + 4) |
left_click() | - | Simulate a left mouse click. The button is held for 32 ms (two game ticks) so the game always sees it |
right_click() | - | Simulate a right mouse click, held the same way |
A click sent while the previous one is still held waits for it to finish. Only one click waits at a time, so more clicks sent in the meantime are merged into it. For other buttons or a longer hold use input.click. For finer-grained input control (key down, key up, mouse movement), see the Input namespace.
Game state
is_position_visible(pos)
Casts a ray from the camera to a world-space position. Returns true if nothing blocks the path. The map and the chess pieces a hero can place block it. Players do not. It also returns true when TSUKI has no collision data for the map yet.
Pass a second position, is_position_visible(pos, from), to check the line between two points instead of from the camera.
local pos = some_player:get_position()
if is_position_visible(pos) then
-- position has line-of-sight from camera
endThis checks a point in the world. To check whether a player is visible, use player:is_visible() instead.
is_in_menu()
Returns true when the game cursor is visible, meaning the player is in a menu, shop, scoreboard, or settings screen.
if is_in_menu() then return endAll input actions from Lua scripts (key presses, mouse movement) are blocked automatically while the player is in a menu, so you don't need to check is_in_menu() for safety. Use it to skip expensive logic like targeting when the player isn't actively playing.
is_game_focused()
Returns true when the game window is in the foreground (active and focused).
if not is_game_focused() then return endUse this to skip logic when the player has alt-tabbed or is in another window.
Spectators
These list the dead players who are spectating you. They update every 2 seconds, and only while the Spectator Panel is on in the TSUKI menu.
| Function | Returns | Description |
|---|---|---|
game.get_spectators() | table | Hero names of the players spectating you, for example {"Haze", "Abrams"} |
game.get_spectator_count() | integer | Number of spectators, without building the name table |
if game.get_spectator_count() > 0 then
local specs = game.get_spectators()
print(#specs .. " spectators: " .. table.concat(specs, ", "))
endItem data
| Function | Returns | Description |
|---|---|---|
game.item_info(name) | table | nil | An item's tier, shop slot, price override and base values, by name |
name is the item's name with or without the upgrade_ prefix, for example "extra_spirit" or "upgrade_extra_spirit". Case does not matter. It returns nil for a name that is not an item, and when the game's item data cannot be read yet. The table has:
| Field | Type | Description |
|---|---|---|
name | string | The full name, with upgrade_ in front |
tier | integer | 1 to 5 |
slot | string | nil | The shop category: "weapon", "vitality", "spirit", and a few special ones. nil when unknown |
price_override | integer | A special price for this item, or 0 when it costs the normal price for its tier |
properties | table | The item's base values by property name, as {[name] = value}, in the game's own units |
The properties are the values from the game files, before a player's items or upgrades change them, the same kind of numbers p:get_ability_properties gives for an ability.
local info = game.item_info("extra_spirit")
if info then
print(info.name, "tier", info.tier, info.slot)
for k, v in pairs(info.properties) do print(k, v) end
endTokens
A modifier's token, an item's token and a damage message's ability_id are numbers that stand for a name. These two functions convert between the number and the name.
| Function | Returns | Description |
|---|---|---|
game.token(name) | integer | The token for a name. Case does not matter |
game.token_name(token) | string | nil | The name behind a token: any hero's ability, any item, and most other game data. nil when it is unknown |
for _, m in ipairs(local_player():get_modifiers()) do
print(m.name ~= "" and m.name or game.token_name(m.token) or "?")
endTsuki Config
Read/write TSUKI's own settings (ESP toggles, aimbot FOV, etc.).
| Function | Returns | Description |
|---|---|---|
tsuki.get_bool(key, default) | boolean | Read a boolean setting |
tsuki.get_int(key, default) | integer | Read an integer setting |
tsuki.get_float(key, default) | number | Read a float setting |
tsuki.set_bool(key, value) | - | Write a boolean setting |
tsuki.set_int(key, value) | - | Write an integer setting |
tsuki.set_float(key, value) | - | Write a float setting |
tsuki.get_color(key, default) | Color | Read a color {r,g,b,a} (0-255) |
tsuki.set_color(key, value) | - | Write a color {r,g,b,a} (0-255) |
-- React to the user's configured aimbot FOV
local fov = tsuki.get_float("aimbot_fov", 3.0)
local target = targeting.find_closest_by_fov(fov, 30.0)
-- Temporarily override a setting and restore it
local prev = tsuki.get_bool("show_teammate_esp", false)
tsuki.set_bool("show_teammate_esp", false)
-- ... do something ...
tsuki.set_bool("show_teammate_esp", prev)Colors are stored as [r,g,b,a] arrays, each value 0-255. get_color and set_color use a matching {r,g,b,a} table:
-- Tint the enemy box fill red while a condition holds, restore otherwise
local KEY = "enemy_box_fill_vis_color"
local base = tsuki.get_color(KEY, {r=255, g=255, b=255, a=40}) -- cache the real color ONCE
if condition then
tsuki.set_color(KEY, {r=255, g=0, b=0, a=90})
else
tsuki.set_color(KEY, base) -- restore
endset_color writes the LIVE setting
set_color changes the user's real saved value, and the change is written to their config. For a temporary or per-modifier tint, cache the original with get_color and restore it when the condition clears. Restore it in on_disable and on_unload too, so turning the script off or reloading it does not leave the tint behind. A tint that is still on when the game or TSUKI closes is saved.
Setting keys match the IDs TSUKI uses internally. For new script-defined settings, prefer the UI namespace. It creates a labeled widget and saves its value automatically.
The API registry
Every registered name, in a table your script can read.
| Function | Returns | Description |
|---|---|---|
tsuki.api() | table | An array of {name, kind, host, needs_internal, since}, one entry per registered name |
tsuki.api_info(name) | table or nil | One entry, or nil when nothing matches exactly |
The fields on an entry:
| Field | Notes |
|---|---|
name | Namespaced functions are ns.fn, player methods are p:method, callbacks and globals are bare |
kind | One of function, method, global, callback or table |
host | external or internal |
needs_internal | Whether your script_info has to declare the flag |
since | The build the name arrived in |
tsuki.api_info is the reliable way to check whether a call exists on this build.
The name must match exactly, including the prefix: "p:get_stats", not "get_stats". tsuki.api() builds a new table on each call, so do not call it every tick.
local i = tsuki.api_info("p:get_stats")
if i then
print(i.name, i.host, i.needs_internal) -- p:get_stats internal true
end
local n_internal = 0
for _, e in ipairs(tsuki.api()) do
if e.needs_internal then n_internal = n_internal + 1 end
end
print(n_internal .. " calls need needs_internal = true")The Lua helpers further down this page (sleep, debounce, press_ability, snap_to_target and the rest) and print are not in the table, but they still work.
For what the host and needs_internal tags mean, see the Internal API Reference.
Cross-Script Modules
Scripts can share code with export and import. A library script calls export at the top level to register a table of functions under a name. Consumer scripts call import from on_tick to fetch it.
| Function | Returns | Description |
|---|---|---|
export(name, module_table) | - | Register a table under name so other scripts can import it. Call at top level |
import(name) | table | nil | Fetch a module exported by another loaded script. Returns nil if the library is not loaded |
WARNING
The sandbox removes eight base-library globals: require, dofile, loadfile, rawget, rawset, rawlen, rawequal, and collectgarbage. Calling any of them fails with attempt to call a nil value. export/import is the only supported way to share code between scripts.
The sandbox loads only _G (base), math, string, table, coroutine and utf8. There is no os, io or debug. Library code ported from elsewhere that calls rawget, rawset or collectgarbage("count") must drop those calls before it runs here.
-- mylib.lua: the library script
script_info = {
name = "mylib",
description = "Shared utilities",
}
export("mylib", {
clamp = function(v, lo, hi) return math.max(lo, math.min(hi, v)) end,
sign = function(v) return v >= 0 and 1 or -1 end,
})-- consumer.lua
script_info = {
name = "consumer",
description = "Uses mylib",
}
local lib = nil
function on_tick()
if not lib then lib = import("mylib") end
if not lib then return end -- library not loaded yet
local clamped = lib.clamp(150, 0, 100) -- 100
endStorage
Persistent key-value storage, separate for each script. Values survive script reloads, game restarts, and PC restarts. Each script has its own file at scripts\storage\<script_name>.json, and one script cannot see another script's keys.
| Function | Returns | Description |
|---|---|---|
storage.set(key, value) | - | Save a value under a key. Passing nil as the value deletes the key. |
storage.get(key) | any | nil | Load a saved value, or nil if not set |
storage.keys() | table | List of saved keys for this script |
Values can be strings, numbers or booleans. nil deletes the key. Any other type, or a missing value (storage.set("k")), raises storage.set: unsupported value type. Tables are not supported. Store each field under its own key, or encode the table as a string with json.encode.
Numeric strings do not round-trip
Any string that Lua can convert to a number is stored as a number. storage.set("k", "42") stores the number 42.0, and storage.get("k") returns 42.0, not "42". The same happens with "3.14", "1e3" and "0x1F". If a value must stay a string, give it a non-numeric prefix.
Writes are cached in memory and flushed to disk about every 5 seconds while the engine is ticking. One more flush runs when the Lua VM shuts down (app exit or a script reload). storage.set() does not flush to disk right away, and disabling a script does not flush.
Don't write storage from on_unload
The shutdown flush runs before on_unload is called, and the cache is discarded right after. Anything on_unload saves never reaches disk. Save state as it changes instead.
-- Track cumulative kills across game sessions
local function in_dead_list(t)
for _, d in ipairs(get_dead_players()) do
if d.hero_name == t.hero_name and d.team == t.team then return true end
end
return false
end
function on_kill(player)
local me = local_player()
if type(player) == "table" then -- left the player list: dead, or only out of sight
if not me or player.team == me:get_team() or not in_dead_list(player) then return end
elseif not player:is_enemy() then return end
local total = (storage.get("total_kills") or 0) + 1
storage.set("total_kills", total)
print("Total kills: " .. total)
end
-- Store structured data as individual keys...
storage.set("loadout_slot1", "burst_fire")
storage.set("loadout_slot2", "trooper_bounty")
local slot1 = storage.get("loadout_slot1")
-- ...or as one JSON string (see JSON below)
storage.set("loadout", json.encode({ "burst_fire", "trooper_bounty" }))
local loadout = json.decode(storage.get("loadout") or "[]")
-- Delete a key
storage.set("old_data", nil)JSON
json.encode and json.decode turn Lua values into JSON text and back. Use them for HTTP bodies, or to store a table with storage.set.
| Function | Returns | Description |
|---|---|---|
json.encode(value, [pretty]) | string | JSON text. Pass true as pretty for indented output. Raises an error for a value JSON cannot hold (a function, NaN, or tables nested deeper than 64, which also catches a table that contains itself) |
json.decode(text) | value | nil, error | The decoded value, or nil and a short reason when the text is not valid JSON. It does not raise on bad text, so it is safe on whatever a server sends back. Passing nil or a table still raises an error |
json.null | value | Stands for JSON null |
How values map:
- A table whose keys are exactly 1 to n is an array. An empty table
{}encodes as[]. Any other table is an object, and number keys become strings. - To force an object, for example an empty
{}, usesetmetatable(t, { __jsontype = "object" }). - Integers stay integers.
- JSON
nulldecodes tojson.null, notnil, so arrays keep their positions. Test it withv == json.null. json.decoderefuses text over 16 MB or nested deeper than 256, and returnsniland the reason.- In a string, bytes that are not valid UTF-8 are encoded as the replacement character, so binary data does not survive a round trip.
local body = json.encode({ name = "tsuki", hits = 3, tags = { "a", "b" } })
-- {"hits":3,"name":"tsuki","tags":["a","b"]}
local data, err = json.decode(body)
if data then print(data.name, #data.tags) else print("bad json:", err) endTimers
timer runs a function later, or again and again, without a coroutine.
| Function | Returns | Description |
|---|---|---|
timer.after(seconds, fn) | integer | Calls fn once after seconds. Returns the timer's id |
timer.every(seconds, fn) | integer | Calls fn every seconds until it is removed. Returns the timer's id |
timer.remove(id) | boolean | Cancels one of your script's timers. true when it was still pending |
seconds can be 0 (the script's next tick) up to 1e9. Anything else raises an error. Adding a timer also raises an error when 4096 timers are already waiting across all scripts.
Timers count real time, so they keep counting through a match pause. A timer belongs to its script and fires on that script's tick, right before its events and on_tick, so it never fires faster than the script ticks. While the script is paused (is_enabled() returns false, or you are playing another hero) its timers wait and fire on its next tick. They are removed when the script is turned off, errors or reloads. An error inside a timer is reported like any other callback error, and a timer.every keeps running.
local beat = timer.every(1, function() print("one second") end)
timer.after(10, function() timer.remove(beat) end)Helpers
These coroutine-friendly helpers for sleeping, debouncing, key management, and aim control are available in every script. sleep, wait_*, hold_key, hold_ability, aim_at_target and fire_and_hold yield, so call them only from on_tick. In an event callback or a timer they raise an error.
Timing and debugging
| Function | Returns | Description |
|---|---|---|
sleep(ms) | - | Yield for approximately ms milliseconds |
debounce(key, cooldown_ms) | boolean | Return true at most once per cooldown_ms window for a given key. Keys are shared by all scripts, so make yours unique, for example by prefixing the script name |
dump(value, [depth]) | string | Any value as readable text. Tables come out one key per line, sorted, nested up to depth levels (default 6). A table that contains itself is marked instead of looping |
print(dump(local_player():get_scoreboard()))function on_tick()
if not debounce("my_feature", 500) then return end
-- runs at most twice per second
endKey and Ability Shortcuts
| Function | Returns | Description |
|---|---|---|
press_ability(slot) | - | Press (down + up) the key bound to the given ability slot |
hold_key(key, duration?) | - | Hold a VK key for duration ms then release. Default 500 ms |
hold_ability(slot, duration?) | - | Hold the key bound to an ability slot. Default 500 ms |
press_ability(slot.ability1) -- tap ability 1
hold_key(VK.SPACE, 200) -- hold Space for 200 ms
hold_ability(slot.ability3, 300) -- hold ability 3 for 300 msAim Helpers
| Function | Returns | Description |
|---|---|---|
snap_to_target(target, opts?) | boolean | Move aim toward target with input.snap_to. Each call moves part of the way, so call it every tick. Returns false if target is invalid or dead |
aim_at_target(target, opts?) | boolean | Smooth aim at target until within threshold FOV or timeout. Returns true on success, and false at the timeout or as soon as target:is_targetable() is false, for example behind a wall |
lock_aim(target, opts?) | - | Aim at target every tick, after on_tick, until unlock_aim() or the target dies. All scripts share one lock. A lock_aim or unlock_aim from any script replaces it, and it moves the mouse once after each running script's on_tick. Turning your script off does not clear it, so call unlock_aim() in on_disable |
unlock_aim() | - | Cancel any active aim lock |
fire_and_hold(target, slot, opts?) | - | Press ability then continuously aim at target for hold_time ms |
opts table for aim helpers:
| Key | Type | Default | Notes |
|---|---|---|---|
bone | string | integer | spine_1 (index 4) for snap_to_target, aim_at_target and lock_aim. bone.chest for fire_and_hold | Bone to aim at |
projectile_velocity | number | 0 | Projectile speed for lead compensation |
hold_time | number | 200 | fire_and_hold only. Milliseconds to keep aiming after pressing |
speed | number | 0.5 | aim_at_target and lock_aim. Smoothing speed passed to input.move_mouse |
threshold | number | 0.5 | aim_at_target only. Success FOV in degrees |
timeout | number | 2000 | aim_at_target only. Maximum ms before returning false |
To soften a continuous lock, set speed on lock_aim instead of calling aim_at_target in a loop:
lock_aim(target, { bone = bone.head, speed = 0.2 }) -- slow, sticky trackingsnap_to_target and fire_and_hold ignore speed. They aim through input.snap_to, which does its own smoothing.
-- Aim, and fire once the crosshair is on the head
if snap_to_target(target, { bone = bone.head })
and targeting.get_fov(target, bone.head) < 1.0 then
press_ability(slot.ability1)
end
-- Smooth aim with timeout
if aim_at_target(target, { speed = 0.3, threshold = 1.5, timeout = 500 }) then
left_click()
end
-- Fire then hold aim on the target
fire_and_hold(target, slot.ability2, { hold_time = 300, bone = bone.chest })
-- Continuous tracking per tick
lock_aim(target, { bone = bone.chest })
-- later, when done:
unlock_aim()Wait Helpers
All wait_* helpers yield the coroutine until a condition is met, then return true. They return false if the timeout elapses first (default 3000 ms).
| Function | Returns | Description |
|---|---|---|
wait_for_cast_delay(slot, timeout?) | boolean | Yield until the ability is out of its cast delay. Returns true at once if it is not in one, so give a press time to reach the game first |
wait_for_cooling_down(slot, timeout?) | boolean | Yield until the ability has STARTED cooling down (is_cooling_down becomes true). Returns true at that point, not when the cooldown finishes |
wait_until_in_range(target, distance, timeout?) | boolean | Yield until target is within distance metres. Returns false early if the target dies |
function on_tick()
press_ability(slot.ability1)
sleep(100) -- give the press time to reach the game
if not wait_for_cast_delay(slot.ability1, 1000) then return end -- wait up to 1s
-- cast delay finished, ability is active
if not wait_for_cooling_down(slot.ability1, 5000) then return end -- wait for CD
-- the cooldown has started
endTargeting Helper
| Function | Returns | Description |
|---|---|---|
find_closest_by_predicate(predicate, max_dist) | player | nil | Find the closest enemy within max_dist metres that satisfies predicate(player) == true |
-- Find the closest enemy that has a specific item
local target = find_closest_by_predicate(function(p)
return p:has_item("Phantom Strike") and p:is_alive()
end, 30.0)