Skip to content

Events ​

Event callbacks fire when the engine detects specific game events. Define them as global functions; the engine looks them up by name. You don't have to define any of them.

on_tick ​

Called every reader tick - the main loop for continuous logic.

lua
function on_tick()
    if not local_player():is_alive() then return end
    -- ...
end

on_unload ​

Called before the script's Lua environment is torn down. Fires on reload, app shutdown, and env reset (re-enable). Use it to clean up any global or main-config state the script set. Takes no arguments.

lua
function on_unload()
    -- reset any state set outside this script's env
end

on_kill ​

Fires when a tracked player dies. There are two cases:

  • Health drops to 0 while the player is still in the snapshot - player is a full player object.
  • Player disappears from the snapshot while previously alive - player is a plain table {hero_name, team, is_alive=false} (the live object is gone; only the cached fields are available).

Always check before calling player methods:

lua
function on_kill(player)
    if type(player) == "table" then
        -- player left the snapshot before we could read them
        toast(player.hero_name .. " died (left snapshot)")
        return
    end
    if player:is_local() then
        toast("we died")
    end
end

on_modifier_added ​

Fires when a modifier instance appears on a tracked player. Serial-based tracking detects duplicate tokens (e.g. owning AND being hit by the same item).

lua
function on_modifier_added(player, modifier)
    -- modifier fields available; see modifier table in Player reference
    if not player:is_local() then return end
    local caster = modifier.caster
    if caster and caster:is_enemy() then
        toast(modifier.name .. " from " .. caster:hero_name())
    end
end

The modifier argument is a reduced shape - it does not include expires_at. If you need an expiry time, compute it as modifier.remaining + sim_time(). All other fields from player:get_modifiers() are present. See Player > Modifiers.

on_modifier_removed ​

Fires when a modifier disappears from a tracked player. Only token and name are populated - all other fields are absent because the modifier is no longer in memory. name may be an empty string and token may be 0 if the data was not cached before removal.

lua
function on_modifier_removed(player, modifier)
    -- only modifier.name (may be "") and modifier.token (may be 0) are present
    if modifier.name ~= "" then
        print(player:hero_name() .. " lost modifier: " .. modifier.name)
    end
end

on_ability_start ​

Fires when a hero ability (slots 0-3) begins its cast-delay / windup phase. This is the earliest possible reaction point - before the cooldown starts and before the cast commits. info.phase is always "start".

Use this for parry or dodge reactions that need maximum reaction time.

FieldTypeDescription
namestringRTTI name, e.g. "ability_vampirebat_batswarm"
slotinteger0-3
cooldownnumberInitial cooldown in seconds (may be 0 before commit)
phasestringAlways "start"
cast_delay_startnumber?sim_time() when cast-delay began (nil if absent)
channel_startnumber?sim_time() when channel began (nil if not yet started)
cooldown_startnumber?sim_time() when cooldown began (nil if not yet started)
cooldown_endnumber?sim_time() when cooldown ends (nil if not yet started)
cast_completednumber?sim_time() the cast finished (nil if not yet completed)
chargesinteger?Remaining charges at event time (nil if ability has no charges)
upgrade_bitsintegerBitmask of purchased upgrade tiers
lua
function on_ability_start(player, info)
    if player:is_enemy() then
        print(player:hero_name() .. " started cast on slot " .. info.slot)
    end
end

on_ability_cast ​

Fires when a hero ability (slots 0-3) commits - the cooldown rises above 0.1 seconds (ab.cooldown > 0.1f). Abilities whose cooldown lands at or below 0.1 s do not trigger this event. info.phase is always "cast".

FieldTypeDescription
namestringRTTI name, e.g. "ability_vampirebat_batswarm"
slotinteger0-3
cooldownnumberInitial cooldown in seconds
phasestringAlways "cast"
cast_delay_startnumber?sim_time() when cast-delay began (nil if absent)
channel_startnumber?sim_time() when channel began (nil if not channeled)
cooldown_startnumber?sim_time() when cooldown began (nil if absent)
cooldown_endnumber?sim_time() when cooldown ends (nil if absent)
cast_completednumber?sim_time() the cast finished (nil if absent)
chargesinteger?Remaining charges at event time (nil if ability has no charges)
upgrade_bitsintegerBitmask of purchased upgrade tiers
lua
function on_ability_cast(player, info)
    if player:is_enemy() then
        print(player:hero_name() .. " cast slot " .. info.slot)
    end
end

on_ability_channel ​

Fires when a hero ability (slots 0-3) begins its channel phase. Only fires for abilities that actually channel; not every ability has this phase. info.phase is always "channel".

FieldTypeDescription
namestringRTTI name, e.g. "ability_vampirebat_batswarm"
slotinteger0-3
cooldownnumberInitial cooldown in seconds
phasestringAlways "channel"
cast_delay_startnumber?sim_time() when cast-delay began (nil if absent)
channel_startnumbersim_time() when channel began (always present for this event)
cooldown_startnumber?sim_time() when cooldown began (nil if not yet on cooldown)
cooldown_endnumber?sim_time() when cooldown ends (nil if not yet on cooldown)
cast_completednumber?sim_time() the cast finished (nil if not yet completed)
chargesinteger?Remaining charges at event time (nil if ability has no charges)
upgrade_bitsintegerBitmask of purchased upgrade tiers
lua
function on_ability_channel(player, info)
    if player:is_enemy() then
        print(player:hero_name() .. " channeling slot " .. info.slot)
    end
end

on_item_used ​

Fires when an active item (slots 4-7) goes on cooldown.

FieldTypeDescription
namestringRTTI name, e.g. "upgrade_metal_skin"
slotinteger4-7
cooldownnumberInitial cooldown in seconds
lua
function on_item_used(player, item)
    if item.name:find("metal_skin") and player:is_enemy() then
        toast(player:hero_name() .. " popped Metal Skin!")
    end
end

The player argument is always the owner/user of the item - the player whose item slot went on cooldown. There is no targeting information in this event.

on_particle_create ​

Fires when a particle effect appears on a tracked player. By default, particle events only include ability-related effects (projectiles, impacts, casts) for the local player, all enemies, and all teammates. Set needs_all_particles = true at the top of your script to also receive events for cosmetic, environmental, tower, and ability-spawned particles. This increases processing overhead - leave it off unless your script specifically requires those event types.

The engine scans the game's internal particle linked list at 100Hz, detects new and removed particles via diffing, and delivers events to Lua. Particle names are full VPF paths like "particles/abilities/melee/melee_heavy_activate_charge.vpcf".

FieldTypeDescription
namestringFull particle path, e.g. "particles/abilities/melee/melee_heavy_activate_charge.vpcf"
is_enemybooleanParticle belongs to an enemy player
is_localbooleanParticle belongs to the local player
is_teammatebooleanParticle belongs to a teammate
owner_labelstringHuman-readable owner tag, e.g. "(enemy t2)", "(local)", "(ally t3)"
owner_pawnintegerRaw pawn pointer of the particle owner
ownerplayer or nilResolved player table (same shape as get_players() returns). nil if the owner couldn't be matched to a known player, happens for ability-spawned particles when needs_all_particles is enabled, since those are owned by ability entities rather than player pawns
positiontable{x, y, z} world position from m_vSortOrigin. May be (0, 0, 0) for entity-attached particles (use control points instead)
has_control_pointsbooleantrue if control point data was successfully read. Only populated for particles whose path contains "abilities/" or "melee"
cp0table{x, y, z} control point 0 (typically bounding box min)
cp1table{x, y, z} control point 1 (typically bounding box max)
cp2table{x, y, z} control point 2, effect center / AoE target position. Usually the most useful CP for gameplay logic
cp3table{x, y, z} control point 3 (adjusted center)
lua
function on_particle_create(ev)
    if ev.is_enemy and ev.name:find("melee_heavy_activate_charge") then
        toast("Enemy charging heavy melee!")
    end
end

Notes on control points ​

  • CP2 is typically the "target center" or AoE landing position for ability particles.
  • For some abilities (Celeste's Luminous Strike cast particle unicorn_flux_strike_cast for example), the CPs are relative to the caster and do not change with the target location. The actual AoE landing particle (unicorn_daggers_target) is ability-spawned and requires needs_all_particles = true to detect.
  • For melee particles, CPs represent the swing bounding box.
  • Not all particles have meaningful CPs, check has_control_points before reading them.

on_particle_destroy ​

Fires when a particle effect is removed from the game. Same fields as on_particle_create, but position and control points will be zeroed (the particle no longer exists in memory).

lua
function on_particle_destroy(ev)
    if ev.is_enemy and ev.name:find("melee_heavy_activate_charge") then
        print("Enemy melee charge ended")
    end
end

Common particle names ​

Reference list of particle paths commonly used in scripting. The Particle Scanner (labelled 'Particle Logger' in the UI) is in Dev Studio > Tools, opened from the bug icon on the top navbar. The old Scripts → Dev Tools sub-tab no longer exists. See Debugging > Discovering particle names.

Melee ​

ParticleMeaning
melee_heavy_activate_chargeEnemy starts charging a heavy melee (the wind-up)
melee_heavy_activateHeavy melee transitions from charge to swing
melee_swing_heavyThe actual heavy melee swing
melee_parryA parry was activated
melee_parry_successA parry successfully blocked an attack
melee_parry_debuffParry debuff applied to the stunned attacker

Hero-specific melee ​

ParticleHero
unicorn_anim_heavy_melee_startCeleste (Unicorn) heavy melee wind-up
unicorn_anim_heavy_meleeCeleste heavy melee swing animation

Movement ​

ParticleMeaning
generic/sprintPlayer is sprinting
generic/slidePlayer is sliding
generic/air_dashPlayer used air dash
generic/bridge_buffPlayer has the bridge zipline buff

Combat ​

ParticleMeaning
generic/headshotHeadshot indicator
modifiers/stunnedPlayer is stunned
weapon_fx/*/muzzle_flashWeapon muzzle flash (hero-specific subfolder)
weapon_fx/*/tracerBullet tracer (hero-specific subfolder)

Ability-spawned (requires needs_all_particles) ​

ParticleMeaning
unicorn_daggers_targetCeleste Luminous Strike AoE landing indicator
unicorn_flux_rainbowCeleste Luminous Strike rainbow effect
npc/npc_melee_swingTower / NPC melee swing
npc/npc_healthbarNPC healthbar particle

Matching modifier and item names ​

Modifier name and item name are RTTI class strings. Use string.find for substring matches because the engine reports the full name including prefixes:

lua
if modifier.name:find("stunned") then ... end
if modifier.name:find("glitch") then ... end
if modifier.name:find("sleep") then ... end

If you want exact matching, use the token (an integer) instead:

lua
if player:has_modifier(0x9C02E614) then ... end

Not affiliated with Valve Corporation.