Skip to content

Script Structure

A Lua script is a .lua file in the scripts folder. No fields are strictly required. An empty file is a valid script. Define only what you need.

Your most basic script will probably look something like this:

lua
name = "Aim Assist"

function on_tick()
    if is_in_menu() then return end
    if not input.is_key_held(VK.XBUTTON2) then return end

    local target = targeting.find_closest_by_fov(15, 50)
    if target and target:is_alive() then
        snap_to_target(target, { bone = bone.chest })
    end
end

on_tick runs every ~4ms (the rate at which TSUKI reads memory) and is what 90%+ of scripts will use as their main loop. It runs as a coroutine, return to start fresh next tick, or call coroutine.yield() to resume from the same line preserving all local state.

Event-only scripts

on_tick isn't required. If your script only needs to react to game events (kills, modifiers, ability casts), you can skip it entirely. This is a complete event-only script:

lua
name = "Kill Logger"

function on_kill(player)
    toast(player:hero_name() .. " died!", 2)
end

The full list of available event callbacks is in the table below.

Optional globals

FieldDefaultDeclare wherePurpose
namefilename (without .lua)eitherDisplay name in menu
idhero_id.any (all heroes)top-level onlyHero filter: hero_id.haze, etc.
settingsnonetop-level onlyConfig table rendered in the menu
description""script_info onlyTagline shown in the menu
version""script_info onlyVersion string shown on the script record
author""script_info onlyAuthor name shown on the script record
needs_all_particlesfalsescript_info onlyReceive all particle events, not just ability-related ones
needs_sound_eventsfalsescript_info onlyEnables the on_sound_event callback
needs_internalfalsescript_info onlyPermission to call the internal feeds. Without it those calls return nil

needs_internal unlocks p:get_stats, p:get_ability_value, p:get_property, p:get_hit_events, p:get_hit_stats, entities.row:get, trace.line and trace.get. Without it those return nil and log once. Nothing else changes.

The two styles are not interchangeable

Only name is read from both places. Everything else lives in exactly one place. id and settings are read only as top-level globals. Every other field is read only from inside script_info. Putting needs_all_particles = true at the top level, or settings inside script_info, is silently ignored. You get no error. The feature never turns on.

A typical header uses both:

lua
id = hero_id.haze                -- top-level only
settings = { ... }               -- top-level only

script_info = {                  -- everything else
    name        = "Aim Assist",
    description = "Locks onto the closest enemy in FOV",
    version     = "1.2",
    author      = "you",
    needs_internal = true,       -- only if the script calls an internal feed
}

Optional callbacks

Define only the callbacks you need.

CallbackWhen it fires
on_tick()Every ~4ms, runs as a coroutine
is_enabled()Checked before every tick; return false to skip this tick
on_unload()Before the script environment is torn down: a reload, a re-enable, or app close. Not on a plain disable
on_kill(player)A player dies
on_modifier_added(player, mod)A modifier is applied to any player
on_modifier_removed(player, mod)A modifier is removed from any player
on_ability_start(player, info)An ability (slots 0-3) enters its cast-delay / windup phase
on_ability_cast(player, info)An ability (slots 0-3) went on cooldown
on_ability_channel(player, info)A channeled ability (slots 0-3) began its channel phase
on_item_used(player, info)An active item (slots 4-7) went on cooldown
on_shot(player, info)The local player fired. One event per trigger pull, so a shotgun blast is one event
on_entity_added(row)An entity appeared in a class you are polling with entities.by_class. One argument, no player
on_entity_removed(row)An entity disappeared from a class you are polling. One argument, no player
on_particle_create(event)A particle system spawned
on_particle_destroy(event)A particle system destroyed
on_sound_event(event)A game sound was played. Requires needs_sound_events = true. Takes a single event table and no player argument

Parameter details for each callback are in the Events reference.

If is_enabled() returns false, the on_tick coroutine is paused at its current coroutine.yield(), not killed, and resumes with its local state intact when it returns true. Wall-clock time keeps running while paused, so a helper like fire_and_hold may find its hold window already elapsed.

Reloading

Saving a .lua file reloads every script about 200 ms later. The Reload button does the same; toggling one script off and on rebuilds only that one. All three rebuild the Lua state, so on_unload runs first and top-level local variables reset. Use Storage for state that should survive.

Toggling a script off only stops it ticking. on_unload does not run until you toggle it back on.

Only the top level of the scripts folder is scanned. A library you import() must sit there, not in a subfolder, and its top-level code runs at load even while disabled.

Next steps

  • Sandbox - which standard libraries are available and which aren't
  • Script Settings - declarative settings and the config namespace
  • Events - full reference for each callback
  • Globals - top-level functions like clock, toast, print
  • Player - everything you can ask about a player
  • Internal API Reference - what needs_internal gives you and when a feed answers nil