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. Local player only: always false on other handles
p:is_in_reload()booleanCurrently reloading. Local player only: always false on other handles
p:get_reload_progress()numberReload progress 0.0 to 1.0. Local player only: always 0.0 on other handles
p:is_primary_weapon_active()booleanNot implemented. Always false. The backing field is never written by the reader
p:is_in_melee_attack()booleanNot implemented. Always false. The backing field is never written by the reader
p:is_targetable()booleanNon-local handles: alive and valid and visible and not untargetable. It folds in the vischeck, so an enemy behind a wall reports false even when the game would let you target them. Always false on the local player
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_level()integer | nilHero level. nil, never 0, before the value has been read
p:get_souls()integerTotal souls (net worth)
p:get_unsecured_souls()integerUnsecured souls (lost on death)

An ability has no level of its own. Its upgrade tier is get_ability(slot).points, in the Abilities section below.

p:get_max_health() reads the controller's max health field. p:get_property("m_iMaxHealth") reads the pawn entity's own field. The two differ on the same pawn by design, and which of them matches the health bar has not been confirmed.

Position and Movement

FunctionReturnsDescription
p:get_position()vec3World position (feet)
p:get_head_world()vec3 | nilHead position in world space, falling back to position.z + 78 when the head bone is unset. Returns nil when the handle no longer resolves to a snapshot. get_position() and get_velocity() return a zero vector instead, so guard this one before indexing .x
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", …) or a bone.* constant; raw integer indexes work as legacy. An unknown name does not return nil. The resolver falls back to the head bone, then to the player's origin, so p:bone_pos("not_a_bone") hands back the feet position. nil comes back only from the integer form with an out-of-range index, or when even the fallback resolves to (0,0,0). Compare the name against p:bone_names() if you need to know it really resolved
p:get_bone_position(...)vec3 | nilLegacy alias for p:bone_pos, literally the same function. 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 the camera to the named/indexed bone. Accepts the same argument as bone_pos. Always a boolean, never nil: false for a stale handle or a bone that resolves to (0,0,0). An unknown bone name is not false. It resolves the same way bone_pos does and traces to the player's feet. The integer form is bounds-checked against the bone count, which is 0 on the local player, so any index returns false there
p:hitboxes()tableThe model's hitbox capsules in world space: the same shapes the triggerbot tests and the engine traces for damage
p:get_distance([other])numberMetres to the local player, or to other if given. Returns the sentinel 9999 (not a distance) when this handle is stale, when there is no local player, or when other is a stale handle

No skeleton on the local player

The local player's snapshot carries no bone data. The bone array, the named bones and the hitbox set are all left empty. On local_player():

  • bone_pos(index) always returns nil
  • bone_pos("head") and every other name falls through to the origin, i.e. the feet position
  • bone_names() still lists names, but none of them resolve to a position
  • is_bone_visible(index) is always false; the name form traces to the feet
  • hitboxes() returns an empty table

get_head_world() and screen_head() are unaffected. They use their own position.z + 78 fallback, which is why they disagree with bone_pos("head") on your own player.

Hitboxes

p:hitboxes() returns the model's real hitbox capsule set in world space: the same shapes the triggerbot tests and targeting.is_on_target ray-tests. Use it when you want your own hit logic instead of rebuilding a body out of bone positions.

Each entry of the 1-based array has:

FieldTypeDescription
bonestringBone this capsule hangs off
avec3Capsule segment start, world space, game units
bvec3Capsule segment end (a == b means the capsule is a sphere)
radiusnumberThis capsule's own radius, game units
groupintegerEngine hitgroup id

Always a table, never nil. You get an empty one when the handle is stale or the model carries no hitbox set, so #p:hitboxes() is always safe. At most 40 capsules are exposed.

The local player has no hitbox data: local_player():hitboxes() is always empty. Only other players are populated.

lua
local boxes = target:hitboxes()
if #boxes == 0 then
    -- no hitbox set for this model (or it's the local player):
    -- targeting.is_on_target falls back to its bone-pair sweep
end
for _, h in ipairs(boxes) do
    print(h.bone, h.radius, h.group)
end

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_ammo()integerRounds currently in the primary weapon's magazine (m_iClip). Drops per shot, resets on reload. 0 both for an empty magazine and before the weapon pointer resolves, so 0 on its own is ambiguous. Pair it with get_shot_number() if you need to tell the two apart
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
p:get_aim_target_player()player | nilThe same target as a player object, or nil when there is none or it is not a player. Local player only. Toggles the same way, so latch it

Resists, shields and barrier are not readable this way. They come from p:get_stats(), which needs the internal.

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. screen_box() also returns nil when the projected box is under 3 pixels tall. That happens for very distant but perfectly on-screen players, so a nil box is not proof the player is off-screen.

The box is height-driven: w is always h * 0.45. Both corners are anchored to the head bone's world XY (the bottom swaps in the origin's Z), so it does not jitter laterally as the model animates.

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 statep: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 modifier names, 1:1 with get_modifier_count(). An entry whose RTTI name didn't resolve is emitted as its hex token instead, e.g. "0x9C02E614". Prefix or substring matching has to tolerate "0x…" 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_or_name)booleanSlot is ready to cast. Accepts a slot integer or an ability class-name substring (case-insensitive, first match wins). false when nothing matches
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)
p:get_selected_ability()table | nilThe ability or active item you are holding right now, as {slot, name}. nil when the gun is out. Local player only
lua
-- these are equivalent for Haze
if lp:is_ability_ready(slot.ability1) then ... end
if lp:is_ability_ready("sleep_dagger") then ... end

Slot Constants

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

Weapon slots are local player only, and nearly empty

Weapon slots (20-22) resolve only for the local player. Items (4-7) come back through the active-item API rather than get_ability, and item tables carry no projectile_speed field at all.

A weapon-slot ability table fills in only name, slot, projectile_speed (the base gun speed, which is why the slot is exposed), plus learned = true and is_ready = true, both hardcoded. Everything else stays at its default: cooldown 0, points 0, range 0, remaining_charges -1, and none of the phase timestamps are present.

So p:is_ability_ready(slot.weapon_primary) is true whenever the slot has resolved, and cannot be used as a fire gate.

Each ability table has:

FieldTypeDescription
namestringRTTI name, e.g. "citadel_ability_tengu_airlift"
slotintegerSlot number
pointsintegerUpgrade points spent: popcount(upgrade_bits >> 16) - 1. -1 means nothing has been spent. This, not learned, is the skilled/unskilled test
learnedbooleanHardcoded true for every ability the table returns, so it is never a useful check. Use points >= 0
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 from the ability's VData (AbilityCastRange), in game units. Multiply by 0.0254 for metres. 0 when the ability configures no cast range (self-cast, passive) or its VData hasn't resolved yet. Works on any player handle; weapon slots (20-22) always leave it 0
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

is_ready does not imply skilled

is_ready is computed as learned and not cooling down and not in cast delay and not channeling, and learned is always true. So an unskilled ability that happens to be off cooldown reports is_ready = true. Gate on points >= 0 as well before casting.

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
tokenintegerAlways present, e.g. 0x5230D219
namestring | nilDisplay name, e.g. "Metal Skin". The key is omitted when the token has no known display name, so test it before formatting
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 or "?"))
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 token rendered as hex text, e.g. "0x9C02E614", not a readable name. Empty string ("") when the item carries no subclass token
slotinteger4-7
bucketintegerItem bucket
cooldownnumberSeconds remaining
is_readybooleanOff cooldown, not in cast delay, not channeling. Populated for enemy and teammate handles too, not just the local player
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)

Internal feeds

Five methods need the internal running and needs_internal = true in script_info. Without the flag they return nil. See Internal API Reference.

MethodWhat it returns
p:get_stats()Resolved combat stats: resists, shields, barrier, and your own weapon numbers
p:get_ability_value(slot, name)The resolved value of one ability property, local player only
p:get_hit_events([since_seq])New damage events, the newest sequence number, and how many you missed
p:get_hit_stats()Running hit, headshot and damage counters for hits you dealt
p:get_property(name)One schema field on this player's entity, typed as the game declares it