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 players not replicated yet. Works for enemies
p:get_steam_id()integerAlias for p:steam_id()
p:get_player_name()stringThe player's name. "" until it has been read

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). It is the same check the aimbot uses, see trace.source(). Always true on the local player
p:is_on_screen()booleanWithin screen bounds. Always true on the local player
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.
p:is_in_melee_attack()booleanNot implemented. Always false.
p:is_targetable()booleanOn other players this is alive and valid and visible and not untargetable. It includes 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), including the velocity item multiplier. Returns 0 until the gun resolves. Local player only
p:get_level()integer | nilHero level. Returns 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)
p:get_scoreboard()table | nilKills, deaths, assists, damage and souls, for any player. See Scoreboard

An ability has no level of its own. Its upgrade tier is get_ability_points(slot) on any player, or get_ability(slot).points on yourself. Both are in the Abilities section below.

p:get_max_health() and p:get_property("m_iMaxHealth") read different fields, so they can return different values.

Scoreboard ​

p:get_scoreboard() works on every player. It returns nil until the player has been read.

FieldMeaning
kills, deaths, assistsHero kills, deaths and assists
last_hits, deniesLast hits and denies
kill_streakCurrent kill streak
hero_damage, objective_damageDamage dealt to heroes and to objectives this match
net_worthSouls (the same number as p:get_souls())
ap_net_worthAbility points
lua
for _, e in ipairs(get_players()) do
    local s = e:get_scoreboard()
    if s then print(e:get_player_name(), s.kills .. "/" .. s.deaths .. "/" .. s.assists) end
end

get_players() lists enemies only, and get_all_players() lists everyone but you. Dead and dormant players are in neither list. Their scoreboards are in get_dead_players() and get_dormant_players().

Position and Movement ​

FunctionReturnsDescription
p:get_position()vec3World position (feet)
p:get_head_world()vec3 | nilHead position in world space. Falls back to position.z + 78 when the head bone is unset. Returns nil when the handle is stale (get_position() and get_velocity() return a zero vector instead), so check for nil before indexing .x
p:get_velocity()vec3Movement velocity vector
p:get_view_angles()vec2Pitch and yaw in degrees (no roll). On other players only the yaw is known, so the pitch is always 0
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 also work (legacy). An unknown name does not return nil. It falls back to the player's origin, so p:bone_pos("not_a_bone") returns the feet position. You get nil from the integer form with an out-of-range index, when the fallback also resolves to (0,0,0), for a stale handle, or when the argument is neither a string nor an integer, such as a misspelled bone.* constant (which is nil). To check that a name resolved, compare it against p:bone_names()
p:get_bone_position(...)vec3 | nilLegacy alias for p:bone_pos. It is the same function
p:bone_names()tableAll available bone names for this player's current model. Empty table until the bones have been read
p:is_bone_visible(bone)booleanRuns a ray trace from the camera to the bone each time you call it. 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). true when TSUKI has no collision data for the map yet. 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 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 9999 (not a real 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 has no bone data. Its bone array, named bones and hitbox set are all empty. On local_player():

  • bone_pos(index) always returns nil
  • bone_pos("head") and every other name fall back to the origin, which is 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 not affected. They use their own position.z + 78 fallback, so on your own player they give a different head position from bone_pos("head").

Hitboxes ​

p:hitboxes() returns the model's real hitbox capsules in world space. These are the same shapes the triggerbot and targeting.is_on_target test against. Use it to write your own hit logic instead of building a body from 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. It is empty when the handle is stale or the model has no hitbox set, so #p:hitboxes() is always safe. At most 40 capsules are returned.

The local player has no hitbox data, so local_player():hitboxes() is always empty.

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 tests the skeleton instead
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 or nil 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 in the primary weapon's magazine. Drops with each shot and resets on reload. 0 means either an empty magazine or that the weapon has not resolved yet. Use get_shot_number() if you need to tell the two apart
p:get_last_attack_time()numberGame clock time of the last shot fired. Compare it with game.server_time(), not game_time(). See Timestamps
p:get_hero_damage()integerCumulative hero damage dealt this match. Includes DoT. For other players, use hero_damage from p:get_scoreboard()
p:get_aim_target()integerEntity index of the enemy the game thinks you are aiming at, or -1. Toggles rapidly, so latch it 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
p:get_stat_deltas()table | nilWhat your items and buffs add to each stat, read from the game's own stat screen. See Stat deltas. Local player only: nil on anyone else

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

Stat deltas ​

p:get_stat_deltas() works without the internal. Your first call starts the reads and returns nil. After that it refreshes 4 times a second while you keep calling it. On other players it is always nil.

These are bonuses, not totals. The hero's own base value is not included, so spirit_power is what your items and buffs add. For the exact total use p:get_stats().spirit_power, which needs the internal.

FieldMeaning
spirit_power, spirit_power_pctSpirit power added, flat and percent
weapon_power, weapon_power_pctWeapon power added, flat and percent
bullet_resist, spirit_resistResist added
fire_rate, reload_speed, bullet_damageGun bonuses
cooldown_reduction_pctCooldown reduction
ability_damage_pctAbility damage bonus
ability_range, ability_range_pctAbility range bonus, flat and percent
ability_radius, ability_radius_pctAbility radius bonus, flat and percent
rawEvery stat the screen lists, as {[stat id] = value}
lua
-- call it every tick (from on_tick): the first call returns nil
local d = local_player():get_stat_deltas()
if d then print("spirit power from items:", d.spirit_power) end

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 when the point is behind the camera. A player in front of you but outside the view still gets coordinates, which fall outside the screen, so check p:is_on_screen() first. screen_box() also returns nil when the projected box is under 3 pixels tall. This happens for very distant players that are still on screen, so a nil box does not mean the player is off-screen.

The box width w is always h * 0.45.

Modifier Flags ​

Fast bitmask check for common status effects.

FunctionReturnsDescription
p:has_modifier_flag(flag)booleanCheck a single modifier_flag bit. 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_() raises an error.

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 Modifier Flags Reference for the full flag list.

Modifiers (full data) ​

Full modifier data. Use these when flag checks are not enough.

FunctionReturnsDescription
p:has_modifier(name_or_token)booleanTrue if any modifier matches. A string does a case-sensitive substring match on the RTTI name. An integer does an 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 given as its hex token instead, e.g. "0x9C02E614", so prefix or substring matching must handle "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_atnumberGame clock time when the effect ends, 0 if permanent (see Timestamps). Use remaining for time left
is_activebooleanHas duration and hasn't expired
ability_namestringLinked ability class name. Empty if the ability belongs to another player
ability_cdnumberAbility cooldown remaining. 0 if the ability belongs to another player
ability_coolingbooleanAbility is on cooldown. false if the ability belongs to another player
serialintegerUnique instance ID (distinguishes duplicate tokens)
stacksintegerStack count, 0 for a modifier that does not stack
max_stacksintegerMaximum stack count, 0 when there is none
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. false on anyone but you
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()tableShort summary of all abilities, keyed by slot index. Each entry has only {name, slot, points, is_ready, cooldown} (plus scope_start where it applies). For all fields 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
p:get_ability_upgrades()tableUpgrade state of every ability, for any player. See Ability upgrades
p:get_ability_points(slot_or_name)integer | nilUpgrade tiers bought on one ability, 0 to 3, for any player. Takes a slot integer or part of the name from get_ability_upgrades() (any case). Also 0 before the ability is learned, never -1. nil if nothing matches
p:get_ability_token(slot)integer | nilA number for the ability in slot 0 to 3, for any player. It stays the same for that ability in every match, and works for heroes newer than your TSUKI build. nil if there's no data for that slot
p:get_ability_cooldowns()tableThe cooldown of each of the four abilities, for any player. Estimated for other players. See Ability cooldowns
lua
-- these are equivalent for Haze
if lp:is_ability_ready(slot.ability1) then ... end
if lp:is_ability_ready("sleep_dagger") then ... end

Other players' abilities

The game doesn't send other players' abilities or active items to your client, not even your teammates'. On anyone but you, get_ability returns nil, get_abilities returns an empty table, has_abilities and is_ability_ready return false, the slot-based property reads below return nothing, and the ability events don't fire. For other players, use get_ability_cooldowns, get_ability_upgrades, get_ability_points, get_ability_token and the Ultimate functions. With the internal, p:get_ability_value works on them too.

Ability upgrades (any player) ​

p:get_ability_upgrades() returns a table keyed by slot. Each entry has:

FieldTypeMeaning
namestringAbility designer name, for example "synth_affliction". You can pass it to p:get_ability_value_by_name. "" for an ability newer than your TSUKI build
slotintegerSlot, 0 to 3 (Silver is different, see below)
pointsintegerUpgrade tiers bought, 0 to 3. Also 0 before the ability is learned
learnedbooleantrue once the ability has been learned
upgrade_bitsintegerThe packed upgrade state. Bit 16 is set once the ability is learned, and each bought tier sets one more bit above it

It refreshes once a second. It is an empty table, never nil, until the first read and for a moment after you load in.

An ability newer than your TSUKI build keeps its slot, with name set to "". p:get_ability_name(slot) can name it with the internal.

Silver is the exception. Her wolf form abilities sit between her normal ones, so she has seven entries, and her four HUD abilities are slots 0, 2, 4 and 6. The same slots apply to get_ability_points and get_ability_token on her, and to p:get_ability_value when another player is Silver.

lua
for s, u in pairs(enemy:get_ability_upgrades()) do
    print(s, u.name, u.points, u.learned)
end

Ability cooldowns (any player) ​

p:get_ability_cooldowns() returns a table keyed by slot 0 to 3. On you the numbers are exact. Other players' ability timers are not sent to your client, so for them the cooldowns are estimated from the casts TSUKI sees. Their ultimate is the real timer.

FieldTypeMeaning
slotinteger0 to 3
learnedbooleanThe ability has been learned
pointsinteger | nilUpgrade tiers bought, 0 to 3
knownbooleanThe timer is known. When false, ready, remaining, total and charges are nil
readyboolean | nilOff cooldown, with a charge left if it uses charges
remainingnumber | nilSeconds until it is ready, 0 when ready
totalnumber | nilLength of the running cooldown
chargesinteger | nilCharges left, for abilities with charges
estimatedbooleanThe timer is an estimate from a cast TSUKI saw
assumedbooleanNo cast has been seen yet, so ready is assumed
may_end_earlybooleanThe ability can reset or refund its cooldown, so it may be ready sooner
stalebooleanThe player was out of sight since the cast

The estimates come from the same system as the Ability Cooldowns ESP. It runs while that ESP is on, or while a script keeps calling get_ability_cooldowns(). It only knows casts it saw after it started, so call it every tick while you need it, and the first call after a gap has known = false for other players.

lua
for _, e in ipairs(get_players()) do
    local cds = e:get_ability_cooldowns()
    local ult = cds[3]
    if ult.known and ult.ready then print(e:hero_name() .. " has ult up") end
end

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

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 instead of get_ability, and item tables have no projectile_speed field.

A weapon-slot ability table fills in only name, slot and projectile_speed (the base gun speed), plus learned = true and is_ready = true, which are always true. Everything else keeps its default: cooldown 0, points 0, range 0 and remaining_charges -1. None of the phase timestamps are present.

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

Each ability table has:

FieldTypeDescription
namestringRTTI name, e.g. "citadel_ability_tengu_airlift"
slotintegerSlot number
pointsintegerUpgrade tiers bought, 0 to 3. -1 means the ability is not learned yet. Use this field, not learned, to check whether the ability is skilled
learnedbooleanAlways true for every ability the table returns, so it is never a useful check. Use points >= 0
cooldownnumberSeconds remaining
is_readybooleanOff cooldown and not casting. Also true for an unlearned ability, see below
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_bitsintegerThe packed upgrade state, the same value as in get_ability_upgrades(). Bit 16 is set once the ability is learned, and each bought tier sets one more bit above it
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 has no cast range (self-cast, passive) or its VData hasn't resolved yet. Your own abilities only. Weapon slots (20-22) always leave it 0
cast_delay_startnumber | nilGame clock time when the cast delay (windup) began, or nil when not active
channel_startnumber | nilGame clock time when channeling began, or nil when not active
cooldown_startnumber | nilGame clock time when cooldown began, or nil when not active
cooldown_endnumber | nilGame clock time when cooldown ends, or nil when not active
cast_completednumber | nilGame clock time when the cast finished, or nil when not active
scope_startnumber | nilGame clock time when you started scoping. Only on Vindicta's sniper ability, absent everywhere else

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 is off cooldown reports is_ready = true. Check points >= 0 as well before casting.

Timestamps use the game clock ​

Game clock

All phase timestamps (cast_delay_start, channel_start, cooldown_start, cooldown_end, cast_completed, scope_start) and a modifier's expires_at use the game's own clock, which stops during pauses. "Now" on that clock is game.server_time().

sim_time() keeps running during pauses, so sim_time() - cast_delay_start comes out too large after a pause. Use game.server_time() - cast_delay_start instead. For time left, use cooldown or remaining, which already handle pauses. See 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 = me:get_ability(slot.weapon_primary)
local effective = me:get_active_projectile_speed()
if gun then print("base:", gun.projectile_speed, "effective:", effective) end

Owned Items ​

All purchased items. 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 missing when the token has no known display name, so check 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

These only work on your own items. On anyone else they return 0, false, nil or an empty table, because the game doesn't send other players' active items. p:get_items() still lists what anyone has bought.

Each active item table has:

FieldTypeDescription
namestringRTTI class name
subclassstringSubclass token as hex text, e.g. "0x9C02E614", not a readable name. Empty string ("") when the item has no subclass token
slotinteger4-7
bucketintegerAlways 0 for now. The game no longer sends it
cooldownnumberSeconds remaining
is_readybooleanOff cooldown, not in cast delay, not channeling
lua
local me = local_player()
if me:has_active_item("metal_skin") then
    local mskin = me: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 only work on your own abilities. On other players they return nil or an empty table. Name-based reads are local player only.

Static data

VData properties are the base tuning values from the game files. Items and modifiers do not change them. You only need to read them once.

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). Also works for 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 ​

These 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_shot_damage([distance_m])Your next gun shot on this player: body and head damage, and shots to kill
p:get_ability_damage(slot)What your ability would deal to this player, and casts to kill
p:get_ability_value(slot, name)The resolved value of one ability property, for any player
p:get_ability_value_by_name(ability, name)The same, with the ability picked by its designer name
p:get_ability_name(slot)The designer name of a player's ability, for any hero
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
p:get_ability_values(list)Several ability values in one call
p:set_property(name, value [, type])Writes one schema field on this player's entity, with a token to check it landed
p.m_FieldNameReads one schema field by name. p.m_FieldName = value writes it. Setting any other key on a player raises an error