Particle Melee Parry ​
Auto-parries enemy heavy melee attacks using particle detection. Demonstrates on_particle_create, owner resolution, distance/visibility/facing gating, and clock()-based debouncing.
-- Particle Melee Parry
-- Detects enemy heavy melee charge via particles and auto-parries.
name = "Particle Melee Parry"
description = "Auto-parry enemy heavy melee via particle detection"
settings = {
{ key = "enabled", label = "Enable", type = "bool", default = true },
{ key = "max_dist", label = "Max Distance (m)", type = "float", default = 12.0, min = 3.0, max = 25.0 },
{ key = "require_vis", label = "Require Visible", type = "bool", default = true },
{ key = "require_facing", label = "Require Facing Us", type = "bool", default = true },
{ key = "facing_angle", label = "Facing Angle (deg)", type = "float", default = 90.0, min = 30.0, max = 180.0 },
{ key = "show_toast", label = "Show Toast", type = "bool", default = true },
{ key = "cooldown_ms", label = "Cooldown (ms)", type = "float", default = 400.0, min = 100.0, max = 2000.0 },
}
local last_parry_time = 0
function on_particle_create(ev)
if not ev.is_enemy then return end
if not config.get_bool("enabled", true) then return end
if not ev.name:find("melee_heavy_activate_charge") then return end
local owner = ev.owner
if not owner then return end
local me = local_player()
if not me or not me:is_alive() then return end
-- Visibility
if config.get_bool("require_vis", true) and not owner:is_visible() then return end
-- Distance (game units are in inches; divide by 39.37 for meters)
local my_pos = me:get_position()
local e_pos = owner:get_position()
local dx, dy, dz = my_pos.x - e_pos.x, my_pos.y - e_pos.y, my_pos.z - e_pos.z
local dist = math.sqrt(dx*dx + dy*dy + dz*dz) / 39.37
if dist > config.get_float("max_dist", 12.0) then return end
-- Facing check: is the enemy looking toward us?
if config.get_bool("require_facing", true) then
local angles = owner:get_view_angles()
if angles then
local len2d = math.sqrt(dx*dx + dy*dy)
if len2d > 1.0 then
local yaw = math.rad(angles.y)
local dot = (math.cos(yaw) * dx + math.sin(yaw) * dy) / len2d
local threshold = math.cos(math.rad(config.get_float("facing_angle", 90.0) / 2))
if dot < threshold then return end
end
end
end
-- Cooldown, prevents double-fire across scan frames on the same melee
local now = clock()
if (now - last_parry_time) < config.get_float("cooldown_ms", 400.0) / 1000.0 then return end
last_parry_time = now
-- Fire parry
input.press_key(keybinds.parry_key())
if config.get_bool("show_toast", true) then
toast(string.format("Parried %s (%.0fm)", owner:hero_name() or "?", dist))
end
endKey patterns ​
Early filtering. Check ev.is_enemy and ev.name:find(...) before doing any expensive work. Most particles aren't relevant, return immediately for non-matches. Most particles fire frequently; early returns keep per-event cost minimal.
Owner resolution. ev.owner is a full player table (same shape get_players() returns). Use it for :is_visible(), :get_position(), :get_view_angles(), :hero_name(), etc. It can be nil if the owner pawn doesn't match any known player, always nil-check before using.
Distance in meters. Game units are in inches. Divide by 39.37 to convert to meters for human-readable settings.
Facing check. Compute the dot product between the enemy's forward vector (derived from their view-angle yaw) and the direction from them to us. A positive dot means they're facing us. The threshold is derived from the configured cone angle: cos(angle / 2).
Debounce via clock(). Prevents the parry from firing multiple times on the same melee charge. The particle can be detected across several 10ms scan frames. clock() returns seconds elapsed since the Lua scripting engine was first initialized - specifically time_since_epoch() - g_clock_start, where g_clock_start is a static double (lua_engine.h line 79) set exactly once via if (g_clock_start == 0) at the time luaL_newstate() is called (lua_engine.h lines 5091-5093). It is NOT set at game or overlay process startup.
See also ​
- Events > on_particle_create, full event field reference
- Events > Common particle names, curated list of particle paths
- Debugging > Discovering particle names. How to find paths for other abilities