Skip to content

Player Object ​

Returned by local_player(), get_players(), event callbacks, and modifier.caster.

All methods are colon-style: p:method().

Identity ​

FunctionReturnsDescription
p:hero_name()stringDisplay name: "Ivy", "Vindicta", "Viscous", etc.
p:get_hero_id()integerNumeric hero ID (compare with hero_id enum)
p:get_team()integerTeam number (typically 2 or 3)
p:is_local()booleanTrue only for the local player
p:is_enemy()booleanTrue if on the opposing team
p:is_valid()booleanTrue if player data is currently readable
p:entity_ptr()integerRaw pawn pointer (advanced use)
p:steam_id()integerSteamID64; 0 for bots, SourceTV, or not-yet-replicated. Works for enemies
p:get_steam_id()integerAlias for p:steam_id()

State ​

FunctionReturnsDescription
p:is_alive()booleanHealth > 0
p:get_health()numberCurrent health
p:get_max_health()numberMaximum health
p:get_health_percentage()numberHealth as a percentage (0 to 100)
p:is_visible()booleanVisible to local player (vischeck)
p:is_on_screen()booleanWithin screen bounds
p:is_scoped()booleanCurrently scoped or zoomed
p:is_in_reload()booleanCurrently reloading
p:get_reload_progress()numberReload progress 0.0 to 1.0
p:is_primary_weapon_active()booleanPrimary weapon is the active slot
p:is_in_melee_attack()booleanCurrently performing a melee attack
p:is_targetable()booleanCan be targeted (not invulnerable)
p:get_active_projectile_speed()numberEffective gun projectile speed (u/s), includes velocity item multiplier. Returns 0 until the gun resolves. Local player only
p:get_souls()integerTotal souls (net worth)
p:get_unsecured_souls()integerUnsecured souls (lost on death)

Position and Movement ​

FunctionReturnsDescription
p:get_position()vec3World position (feet)
p:get_head_world()vec3Head position in world space
p:get_velocity()vec3Movement velocity vector
p:get_view_angles()vec2Pitch and yaw in degrees (no roll)
p:bone_pos(name_or_index)vec3 | nilWorld position of a skeleton bone. Accepts a name string ("head", "arm_upper_l", etc.) or bone.* constant. Raw integer indexes work as legacy. Returns nil if the bone isn't found
p:get_bone_position(...)vec3 | nilLegacy alias for p:bone_pos. Identical behavior
p:bone_names()tableAll available bone names for this player's current model. Empty table if bones haven't been probed yet
p:is_bone_visible(bone)booleanOn-demand BVH ray trace from camera to the named/indexed bone. Accepts the same argument as bone_pos
p:get_distance([other])numberMeters to local player, or to other if provided

Stamina ​

Local player only

Stamina is not replicated to other clients. These functions return 0 when called on enemy or teammate handles.

FunctionReturnsDescription
p:get_stamina()numberCurrent stamina (e.g. 3.0)
p:get_max_stamina()numberMaximum stamina (e.g. 4.0)
lua
local me = local_player()
local pct = me:get_stamina() / me:get_max_stamina() * 100
print(string.format("Stamina: %.0f%%", pct))

Combat Stats ​

Local player only

These values are not replicated. All functions return 0 / -1 when called on a non-local player handle.

FunctionReturnsDescription
p:get_recoil_angles()vec3Spray-climb recoil accumulator {x=pitch, y=yaw, z=roll} in degrees
p:get_aim_punch()vec3Per-shot view kick {x=pitch, y=yaw, z=roll} in degrees
p:get_shot_number()integerMonotonic shots-fired counter, ammo-independent
p:get_last_attack_time()numberGameTime of the last shot fired
p:get_hero_damage()integerCumulative hero damage dealt this match. Includes DoT
p:get_aim_target()integerEntity index of the enemy the game considers you to be aiming at, or -1. Toggles rapidly - latch for ~0.5-1s

Crouch ​

Local player only

Crouch state is not replicated to other clients. Returns 0 / false for non-local players.

FunctionReturnsDescription
p:get_crouch_fraction()number0.0 = standing, 1.0 = fully crouched. Values between indicate a transition
p:is_crouched()booleantrue when crouch fraction > 0.5
lua
local me = local_player()
if me:is_crouched() then
    print("Crouching: " .. string.format("%.0f%%", me:get_crouch_fraction() * 100))
end

Ground Normal / Slope ​

Local player only

Ground normal requires local movement data. Returns {0, 0, 0} / 0 for non-local players.

FunctionReturnsDescription
p:get_ground_normal()vec3Surface normal of the ground. {0, 0, 1} on flat ground
p:get_slope_angle()numberSlope angle in degrees. 0 = flat, 45 = steep, 90 = wall

The z component of the ground normal indicates steepness:

z valueMeaning
1.0Flat ground
0.87~30° slope
0.71~45° slope
0.0Vertical wall
lua
local me = local_player()
local slope = me:get_slope_angle()
if slope > 30 then
    print("Steep slope: " .. string.format("%.1f°", slope))
end

local gn = me:get_ground_normal()
print(string.format("Surface: (%.2f, %.2f, %.2f)", gn.x, gn.y, gn.z))

Screen Space ​

FunctionReturnsDescription
p:screen_box()table | nil{x, y, w, h} bounding box on screen
p:screen_head()vec2 | nil{x, y} head position on screen
p:screen_origin()vec2 | nil{x, y} feet position on screen

Returns nil if the player is off-screen or behind the camera.

Modifier Flags ​

Fast bitmask check for common status effects.

FunctionReturnsDescription
p:has_modifier_flag(flag)booleanCheck a single modifier_flag bit (EModifierState). Use for status effects: stunned, silenced, etc.
p:is_(flag)booleanCheck a single player_flag bit (movement / ground state). Takes a player_flag integer, e.g. p:is_(player_flag.onground)

Different enums

p:has_modifier_flag and p:is_ take values from different constant tables.

  • Status effects (stunned, silenced, rooted …) → p:has_modifier_flag(modifier_flag.STUNNED)
  • Movement / ground state → p:is_(player_flag.onground)

Passing a string to p:is_() is incorrect and will not work.

lua
-- Status effect check
if player:has_modifier_flag(modifier_flag.STUNNED) then
    print("target is stunned")
end

-- Ground-state check
if player:is_(player_flag.onground) then
    print("player is on the ground")
end

See Types & Constants for the full flag list.

Modifiers (full data) ​

Full modifier data - use when flag checks are insufficient:

FunctionReturnsDescription
p:has_modifier(name_or_token)booleanTrue if any modifier matches. String arg = substring match on RTTI name; integer arg = exact token match
p:get_modifier(name_or_token)table | nilFirst matching modifier, or nil. Same matching rules as has_modifier
p:get_modifier_count()integerNumber of active modifiers
p:get_modifier_names()tableArray of RTTI class name strings
p:get_modifiers()tableArray of all modifier tables
lua
-- Substring match: catches modifier_stunned, modifier_delayed_stun, etc.
if player:has_modifier("stun") then ... end

-- Exact token match
if player:has_modifier(0x9C02E614) then ... end

Each modifier table (returned by get_modifier and as entries in get_modifiers) has these fields:

FieldTypeDescription
namestringRTTI class name, e.g. "modifier_glitch"
tokenintegerAbility subclass ID, e.g. 0x9C02E614
durationnumber-1 if permanent, > 0 if temporary
remainingnumberSeconds left (0 if permanent or expired)
expires_atnumbersim_time() when effect ends. Compare with sim_time(), not game_time()
is_activebooleanHas duration and hasn't expired
ability_namestringLinked ability class name
ability_cdnumberAbility cooldown remaining
ability_coolingbooleanAbility is on cooldown
serialintegerUnique instance ID (distinguishes duplicate tokens)
casterplayer | nilWho applied the modifier (nil if unknown)
lua
local m = player:get_modifier("stunned")
if m and m.is_active then
    print(m.remaining, "seconds left, applied by", m.caster and m.caster:hero_name())
end

for _, m in ipairs(player:get_modifiers()) do
    if m.is_active and m.caster and m.caster:is_enemy() then
        print(m.name, m.remaining)
    end
end

Ultimate ​

FunctionReturnsDescription
p:is_ult_trained()booleanUltimate has been skilled
p:is_ult_ready()booleanTrained AND off cooldown
p:get_ult_cooldown()numberSeconds remaining (-1 = not trained, 0 = ready)

Abilities (slots 0-3) & Weapon Slots ​

Hero abilities, active items, and weapon slots.

FunctionReturnsDescription
p:has_abilities()booleanPlayer has any abilities loaded
p:is_ability_ready(slot)booleanSlot is ready to cast
p:get_ability(slot)table | nilFull ability data for one slot (see field list below)
p:get_abilities()tableLightweight summary of all abilities keyed by slot index - each entry has only {name, slot, points, is_ready, cooldown}. For the full field set use get_ability(slot)

Slot Constants ​

slot.*ValueMeaning
slot.ability1 … slot.ability40-3Hero abilities (signature 1-4)
slot.item1 … slot.item44-7Active items
slot.weapon_secondary20Secondary weapon / alt-fire
slot.weapon_primary21Primary gun
slot.weapon_melee22Melee

Local player only

Weapon slots (20-22) are local player only. Items (4-7) return as items, not abilities. They don't populate projectile_speed.

Each ability table has:

FieldTypeDescription
namestringRTTI name, e.g. "citadel_ability_tengu_airlift"
slotintegerSlot number
pointsintegerUpgrade points spent
learnedbooleanHas been skilled
cooldownnumberSeconds remaining
is_readybooleanLearned, not on cd, not casting
is_cooling_downbooleanOn cooldown
is_castingbooleanMid-cast
is_channelingbooleanCurrently channeling
is_in_cast_delaybooleanIn pre-cast windup
remaining_chargesintegerRemaining charges (-1 = not charge-based)
upgrade_bitsintegerBitmask of purchased upgrade tiers
projectile_speednumberBase projectile speed in u/s (VData, not item-boosted). 0 = no projectile
rangenumberCast range (currently always 0, unimplemented)
cast_delay_startnumber | nilsim_time() when the cast-delay (windup) began; nil when not active
channel_startnumber | nilsim_time() when channeling began; nil when not active
cooldown_startnumber | nilsim_time() when cooldown began; nil when not active
cooldown_endnumber | nilsim_time() when cooldown ends; nil when not active
cast_completednumber | nilsim_time() the cast finished; nil when not active

sim_time domain

All phase timestamps (cast_delay_start, channel_start, cooldown_start, cooldown_end, cast_completed) are in sim_time() domain. To compute elapsed time use sim_time() - ability.cast_delay_start, not game_time().

Projectile Speed ​

get_ability(slot).projectile_speed returns the base (VData) speed. For the gun's effective speed including velocity items, use get_active_projectile_speed() instead.

lua
local me = local_player()

-- Ability projectile speed (base)
local dagger = me:get_ability(slot.ability1)
if dagger then print(dagger.name, dagger.projectile_speed) end

-- Gun base vs effective
local gun_base = me:get_ability(slot.weapon_primary)
local effective = me:get_active_projectile_speed()
-- effective = base × (1 + 0.6 × velocity_item_count)

Owned Items ​

All purchased items (m_vecUpgrades). Accepts a case-insensitive display name or hex token.

FunctionReturnsDescription
p:get_item_count()integerNumber of owned items
p:has_item(name_or_token)boolean"Cursed Relic", "cursed relic", or 0x9C02E614 all work
p:get_item(name_or_token)table | nilItem table or nil if not owned
p:get_items()tableAll owned items

Each item table has:

FieldTypeDescription
tokenintegere.g. 0x5230D219
namestringDisplay name, e.g. "Metal Skin"
lua
if player:has_item("Cursed Relic") then
    -- ...
end

for _, it in ipairs(player:get_items()) do
    print(string.format("0x%08X  %s", it.token, it.name))
end

Active Items (slots 4-7) ​

Active items equipped in slots 4-7, each with cooldown state. Match by RTTI class-name substring (lowercase snake_case, e.g. "metal_skin").

FunctionReturnsDescription
p:get_active_item_count()integerNumber of active items equipped
p:has_active_item(substring)booleanRTTI substring match
p:get_active_item(substring)table | nilSingle active item data
p:get_active_items()tableArray of all active items
p:is_item_ready(slot_or_name)booleanTrue if the item in the given slot index or name substring is off cooldown and usable

Each active item table has:

FieldTypeDescription
namestringRTTI class name
subclassstringSubclass name
slotinteger4-7
bucketintegerItem bucket
cooldownnumberSeconds remaining
lua
if player:has_active_item("metal_skin") then
    local mskin = player:get_active_item("metal_skin")
    print("cd:", mskin.cooldown)
end

VData Ability Properties ​

Read raw KV3 property values from an ability's CitadelAbilityVData. Values are in game units (multiply by 0.0254 for metres). Slot-based reads work on any player handle; name-based variants are local player only.

Static data

VData properties are the base tuning values baked into the game files. They are not affected by items or modifiers. Read them once; polling is unnecessary.

FunctionReturnsDescription
p:read_ability_property(slot, prop)number | nilSingle property value for the ability in slot. nil if not found
p:get_ability_properties(slot)tableAll properties for the ability in slot as {[name]=value}. Empty table if not resolved
p:read_ability_property_by_name(class_substr, prop)number | nilSingle property for an ability matched by class-name substring (case-insensitive). Reaches innate abilities ("melee_parry", "jump", "dash", etc.). Local player only
p:get_ability_properties_by_name(class_substr)tableAll properties for the ability matched by class-name substring. Local player only
lua
local me = local_player()

-- Read a known property by slot
local dmg = me:read_ability_property(slot.ability1, "Damage")
if dmg then print("Ability 1 damage:", dmg) end

-- Dump all properties for a slot
for name, val in pairs(me:get_ability_properties(slot.ability2)) do
    print(name, val)
end

-- Reach an innate ability by class-name substring (local player only)
local parry_dur = me:read_ability_property_by_name("melee_parry", "ParryDuration")
print("Parry window:", parry_dur)

Not affiliated with Valve Corporation.