Player Object â
Returned by local_player(), get_players(), event callbacks, and modifier.caster.
All methods are colon-style: p:method().
Identity â
| Function | Returns | Description |
|---|---|---|
p:hero_name() | string | Display name: "Ivy", "Vindicta", "Viscous", etc. |
p:get_hero_id() | integer | Numeric hero ID (compare with hero_id enum) |
p:get_team() | integer | Team number (typically 2 or 3) |
p:is_local() | boolean | True only for the local player |
p:is_enemy() | boolean | True if on the opposing team |
p:is_valid() | boolean | True if player data is currently readable |
p:entity_ptr() | integer | Raw pawn pointer (advanced use) |
p:steam_id() | integer | SteamID64; 0 for bots, SourceTV, or not-yet-replicated. Works for enemies |
p:get_steam_id() | integer | Alias for p:steam_id() |
State â
| Function | Returns | Description |
|---|---|---|
p:is_alive() | boolean | Health > 0 |
p:get_health() | number | Current health |
p:get_max_health() | number | Maximum health |
p:get_health_percentage() | number | Health as a percentage (0 to 100) |
p:is_visible() | boolean | Visible to local player (vischeck) |
p:is_on_screen() | boolean | Within screen bounds |
p:is_scoped() | boolean | Currently scoped or zoomed |
p:is_in_reload() | boolean | Currently reloading |
p:get_reload_progress() | number | Reload progress 0.0 to 1.0 |
p:is_primary_weapon_active() | boolean | Primary weapon is the active slot |
p:is_in_melee_attack() | boolean | Currently performing a melee attack |
p:is_targetable() | boolean | Can be targeted (not invulnerable) |
p:get_active_projectile_speed() | number | Effective gun projectile speed (u/s), includes velocity item multiplier. Returns 0 until the gun resolves. Local player only |
p:get_souls() | integer | Total souls (net worth) |
p:get_unsecured_souls() | integer | Unsecured souls (lost on death) |
Position and Movement â
| Function | Returns | Description |
|---|---|---|
p:get_position() | vec3 | World position (feet) |
p:get_head_world() | vec3 | Head position in world space |
p:get_velocity() | vec3 | Movement velocity vector |
p:get_view_angles() | vec2 | Pitch and yaw in degrees (no roll) |
p:bone_pos(name_or_index) | vec3 | nil | World 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 | nil | Legacy alias for p:bone_pos. Identical behavior |
p:bone_names() | table | All available bone names for this player's current model. Empty table if bones haven't been probed yet |
p:is_bone_visible(bone) | boolean | On-demand BVH ray trace from camera to the named/indexed bone. Accepts the same argument as bone_pos |
p:get_distance([other]) | number | Meters 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.
| Function | Returns | Description |
|---|---|---|
p:get_stamina() | number | Current stamina (e.g. 3.0) |
p:get_max_stamina() | number | Maximum stamina (e.g. 4.0) |
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.
| Function | Returns | Description |
|---|---|---|
p:get_recoil_angles() | vec3 | Spray-climb recoil accumulator {x=pitch, y=yaw, z=roll} in degrees |
p:get_aim_punch() | vec3 | Per-shot view kick {x=pitch, y=yaw, z=roll} in degrees |
p:get_shot_number() | integer | Monotonic shots-fired counter, ammo-independent |
p:get_last_attack_time() | number | GameTime of the last shot fired |
p:get_hero_damage() | integer | Cumulative hero damage dealt this match. Includes DoT |
p:get_aim_target() | integer | Entity 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.
| Function | Returns | Description |
|---|---|---|
p:get_crouch_fraction() | number | 0.0 = standing, 1.0 = fully crouched. Values between indicate a transition |
p:is_crouched() | boolean | true when crouch fraction > 0.5 |
local me = local_player()
if me:is_crouched() then
print("Crouching: " .. string.format("%.0f%%", me:get_crouch_fraction() * 100))
endGround Normal / Slope â
Local player only
Ground normal requires local movement data. Returns {0, 0, 0} / 0 for non-local players.
| Function | Returns | Description |
|---|---|---|
p:get_ground_normal() | vec3 | Surface normal of the ground. {0, 0, 1} on flat ground |
p:get_slope_angle() | number | Slope angle in degrees. 0 = flat, 45 = steep, 90 = wall |
The z component of the ground normal indicates steepness:
z value | Meaning |
|---|---|
1.0 | Flat ground |
0.87 | ~30° slope |
0.71 | ~45° slope |
0.0 | Vertical wall |
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 â
| Function | Returns | Description |
|---|---|---|
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.
| Function | Returns | Description |
|---|---|---|
p:has_modifier_flag(flag) | boolean | Check a single modifier_flag bit (EModifierState). Use for status effects: stunned, silenced, etc. |
p:is_(flag) | boolean | Check 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.
-- 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")
endSee Types & Constants for the full flag list.
Modifiers (full data) â
Full modifier data - use when flag checks are insufficient:
| Function | Returns | Description |
|---|---|---|
p:has_modifier(name_or_token) | boolean | True if any modifier matches. String arg = substring match on RTTI name; integer arg = exact token match |
p:get_modifier(name_or_token) | table | nil | First matching modifier, or nil. Same matching rules as has_modifier |
p:get_modifier_count() | integer | Number of active modifiers |
p:get_modifier_names() | table | Array of RTTI class name strings |
p:get_modifiers() | table | Array of all modifier tables |
-- 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 ... endEach modifier table (returned by get_modifier and as entries in get_modifiers) has these fields:
| Field | Type | Description |
|---|---|---|
name | string | RTTI class name, e.g. "modifier_glitch" |
token | integer | Ability subclass ID, e.g. 0x9C02E614 |
duration | number | -1 if permanent, > 0 if temporary |
remaining | number | Seconds left (0 if permanent or expired) |
expires_at | number | sim_time() when effect ends. Compare with sim_time(), not game_time() |
is_active | boolean | Has duration and hasn't expired |
ability_name | string | Linked ability class name |
ability_cd | number | Ability cooldown remaining |
ability_cooling | boolean | Ability is on cooldown |
serial | integer | Unique instance ID (distinguishes duplicate tokens) |
caster | player | nil | Who applied the modifier (nil if unknown) |
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
endUltimate â
| Function | Returns | Description |
|---|---|---|
p:is_ult_trained() | boolean | Ultimate has been skilled |
p:is_ult_ready() | boolean | Trained AND off cooldown |
p:get_ult_cooldown() | number | Seconds remaining (-1 = not trained, 0 = ready) |
Abilities (slots 0-3) & Weapon Slots â
Hero abilities, active items, and weapon slots.
| Function | Returns | Description |
|---|---|---|
p:has_abilities() | boolean | Player has any abilities loaded |
p:is_ability_ready(slot) | boolean | Slot is ready to cast |
p:get_ability(slot) | table | nil | Full ability data for one slot (see field list below) |
p:get_abilities() | table | Lightweight 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.* | Value | Meaning |
|---|---|---|
slot.ability1 ⌠slot.ability4 | 0-3 | Hero abilities (signature 1-4) |
slot.item1 ⌠slot.item4 | 4-7 | Active items |
slot.weapon_secondary | 20 | Secondary weapon / alt-fire |
slot.weapon_primary | 21 | Primary gun |
slot.weapon_melee | 22 | Melee |
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:
| Field | Type | Description |
|---|---|---|
name | string | RTTI name, e.g. "citadel_ability_tengu_airlift" |
slot | integer | Slot number |
points | integer | Upgrade points spent |
learned | boolean | Has been skilled |
cooldown | number | Seconds remaining |
is_ready | boolean | Learned, not on cd, not casting |
is_cooling_down | boolean | On cooldown |
is_casting | boolean | Mid-cast |
is_channeling | boolean | Currently channeling |
is_in_cast_delay | boolean | In pre-cast windup |
remaining_charges | integer | Remaining charges (-1 = not charge-based) |
upgrade_bits | integer | Bitmask of purchased upgrade tiers |
projectile_speed | number | Base projectile speed in u/s (VData, not item-boosted). 0 = no projectile |
range | number | Cast range (currently always 0, unimplemented) |
cast_delay_start | number | nil | sim_time() when the cast-delay (windup) began; nil when not active |
channel_start | number | nil | sim_time() when channeling began; nil when not active |
cooldown_start | number | nil | sim_time() when cooldown began; nil when not active |
cooldown_end | number | nil | sim_time() when cooldown ends; nil when not active |
cast_completed | number | nil | sim_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.
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.
| Function | Returns | Description |
|---|---|---|
p:get_item_count() | integer | Number 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 | nil | Item table or nil if not owned |
p:get_items() | table | All owned items |
Each item table has:
| Field | Type | Description |
|---|---|---|
token | integer | e.g. 0x5230D219 |
name | string | Display name, e.g. "Metal Skin" |
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))
endActive 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").
| Function | Returns | Description |
|---|---|---|
p:get_active_item_count() | integer | Number of active items equipped |
p:has_active_item(substring) | boolean | RTTI substring match |
p:get_active_item(substring) | table | nil | Single active item data |
p:get_active_items() | table | Array of all active items |
p:is_item_ready(slot_or_name) | boolean | True if the item in the given slot index or name substring is off cooldown and usable |
Each active item table has:
| Field | Type | Description |
|---|---|---|
name | string | RTTI class name |
subclass | string | Subclass name |
slot | integer | 4-7 |
bucket | integer | Item bucket |
cooldown | number | Seconds remaining |
if player:has_active_item("metal_skin") then
local mskin = player:get_active_item("metal_skin")
print("cd:", mskin.cooldown)
endVData 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.
| Function | Returns | Description |
|---|---|---|
p:read_ability_property(slot, prop) | number | nil | Single property value for the ability in slot. nil if not found |
p:get_ability_properties(slot) | table | All properties for the ability in slot as {[name]=value}. Empty table if not resolved |
p:read_ability_property_by_name(class_substr, prop) | number | nil | Single 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) | table | All properties for the ability matched by class-name substring. Local player only |
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)