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 players not replicated yet. Works for enemies |
p:get_steam_id() | integer | Alias for p:steam_id() |
p:get_player_name() | string | The player's name. "" until it has been read |
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). It is the same check the aimbot uses, see trace.source(). Always true on the local player |
p:is_on_screen() | boolean | Within screen bounds. Always true on the local player |
p:is_scoped() | boolean | Currently scoped or zoomed. Local player only: always false on other handles |
p:is_in_reload() | boolean | Currently reloading. Local player only: always false on other handles |
p:get_reload_progress() | number | Reload progress 0.0 to 1.0. Local player only: always 0.0 on other handles |
p:is_primary_weapon_active() | boolean | Not implemented. Always false. |
p:is_in_melee_attack() | boolean | Not implemented. Always false. |
p:is_targetable() | boolean | On 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() | number | Effective gun projectile speed (u/s), including the velocity item multiplier. Returns 0 until the gun resolves. Local player only |
p:get_level() | integer | nil | Hero level. Returns nil (never 0) before the value has been read |
p:get_souls() | integer | Total souls (net worth) |
p:get_unsecured_souls() | integer | Unsecured souls (lost on death) |
p:get_scoreboard() | table | nil | Kills, 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.
| Field | Meaning |
|---|---|
kills, deaths, assists | Hero kills, deaths and assists |
last_hits, denies | Last hits and denies |
kill_streak | Current kill streak |
hero_damage, objective_damage | Damage dealt to heroes and to objectives this match |
net_worth | Souls (the same number as p:get_souls()) |
ap_net_worth | Ability points |
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
endget_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
| Function | Returns | Description |
|---|---|---|
p:get_position() | vec3 | World position (feet) |
p:get_head_world() | vec3 | nil | Head 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() | vec3 | Movement velocity vector |
p:get_view_angles() | vec2 | Pitch 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 | nil | World 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 | nil | Legacy alias for p:bone_pos. It is the same function |
p:bone_names() | table | All available bone names for this player's current model. Empty table until the bones have been read |
p:is_bone_visible(bone) | boolean | Runs 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() | table | The model's hitbox capsules in world space: the same shapes the triggerbot tests and the engine traces for damage |
p:get_distance([other]) | number | Metres 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 returnsnilbone_pos("head")and every other name fall back to the origin, which is the feet positionbone_names()still lists names, but none of them resolve to a positionis_bone_visible(index)is alwaysfalse. The name form traces to the feethitboxes()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:
| Field | Type | Description |
|---|---|---|
bone | string | Bone this capsule hangs off |
a | vec3 | Capsule segment start, world space, game units |
b | vec3 | Capsule segment end (a == b means the capsule is a sphere) |
radius | number | This capsule's own radius, game units |
group | integer | Engine 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.
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)
endStamina
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 or nil 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_ammo() | integer | Rounds 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() | number | Game clock time of the last shot fired. Compare it with game.server_time(), not game_time(). See Timestamps |
p:get_hero_damage() | integer | Cumulative hero damage dealt this match. Includes DoT. For other players, use hero_damage from p:get_scoreboard() |
p:get_aim_target() | integer | Entity 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 | nil | The 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 | nil | What 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.
| Field | Meaning |
|---|---|
spirit_power, spirit_power_pct | Spirit power added, flat and percent |
weapon_power, weapon_power_pct | Weapon power added, flat and percent |
bullet_resist, spirit_resist | Resist added |
fire_rate, reload_speed, bullet_damage | Gun bonuses |
cooldown_reduction_pct | Cooldown reduction |
ability_damage_pct | Ability damage bonus |
ability_range, ability_range_pct | Ability range bonus, flat and percent |
ability_radius, ability_radius_pct | Ability radius bonus, flat and percent |
raw | Every stat the screen lists, as {[stat id] = value} |
-- 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) endCrouch
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 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.
| Function | Returns | Description |
|---|---|---|
p:has_modifier_flag(flag) | boolean | Check a single modifier_flag bit. 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_() raises an error.
-- 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 Modifier Flags Reference for the full flag list.
Modifiers (full data)
Full modifier data. Use these when flag checks are not enough.
| Function | Returns | Description |
|---|---|---|
p:has_modifier(name_or_token) | boolean | True 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 | 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 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() | 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 | Game clock time when the effect ends, 0 if permanent (see Timestamps). Use remaining for time left |
is_active | boolean | Has duration and hasn't expired |
ability_name | string | Linked ability class name. Empty if the ability belongs to another player |
ability_cd | number | Ability cooldown remaining. 0 if the ability belongs to another player |
ability_cooling | boolean | Ability is on cooldown. false if the ability belongs to another player |
serial | integer | Unique instance ID (distinguishes duplicate tokens) |
stacks | integer | Stack count, 0 for a modifier that does not stack |
max_stacks | integer | Maximum stack count, 0 when there is none |
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. false on anyone but you |
p:is_ability_ready(slot_or_name) | boolean | Slot 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 | nil | Full ability data for one slot (see field list below) |
p:get_abilities() | table | Short 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 | nil | The 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() | table | Upgrade state of every ability, for any player. See Ability upgrades |
p:get_ability_points(slot_or_name) | integer | nil | Upgrade 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 | nil | A 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() | table | The cooldown of each of the four abilities, for any player. Estimated for other players. See Ability cooldowns |
-- these are equivalent for Haze
if lp:is_ability_ready(slot.ability1) then ... end
if lp:is_ability_ready("sleep_dagger") then ... endOther 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:
| Field | Type | Meaning |
|---|---|---|
name | string | Ability 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 |
slot | integer | Slot, 0 to 3 (Silver is different, see below) |
points | integer | Upgrade tiers bought, 0 to 3. Also 0 before the ability is learned |
learned | boolean | true once the ability has been learned |
upgrade_bits | integer | The 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.
for s, u in pairs(enemy:get_ability_upgrades()) do
print(s, u.name, u.points, u.learned)
endAbility 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.
| Field | Type | Meaning |
|---|---|---|
slot | integer | 0 to 3 |
learned | boolean | The ability has been learned |
points | integer | nil | Upgrade tiers bought, 0 to 3 |
known | boolean | The timer is known. When false, ready, remaining, total and charges are nil |
ready | boolean | nil | Off cooldown, with a charge left if it uses charges |
remaining | number | nil | Seconds until it is ready, 0 when ready |
total | number | nil | Length of the running cooldown |
charges | integer | nil | Charges left, for abilities with charges |
estimated | boolean | The timer is an estimate from a cast TSUKI saw |
assumed | boolean | No cast has been seen yet, so ready is assumed |
may_end_early | boolean | The ability can reset or refund its cooldown, so it may be ready sooner |
stale | boolean | The 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.
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
endSlot 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 |
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:
| Field | Type | Description |
|---|---|---|
name | string | RTTI name, e.g. "citadel_ability_tengu_airlift" |
slot | integer | Slot number |
points | integer | Upgrade 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 |
learned | boolean | Always true for every ability the table returns, so it is never a useful check. Use points >= 0 |
cooldown | number | Seconds remaining |
is_ready | boolean | Off cooldown and not casting. Also true for an unlearned ability, see below |
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 | The 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_speed | number | Base projectile speed in u/s (VData, not item-boosted). 0 = no projectile |
range | number | Cast 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_start | number | nil | Game clock time when the cast delay (windup) began, or nil when not active |
channel_start | number | nil | Game clock time when channeling began, or nil when not active |
cooldown_start | number | nil | Game clock time when cooldown began, or nil when not active |
cooldown_end | number | nil | Game clock time when cooldown ends, or nil when not active |
cast_completed | number | nil | Game clock time when the cast finished, or nil when not active |
scope_start | number | nil | Game 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.
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) endOwned Items
All purchased items. 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 | Always present, e.g. 0x5230D219 |
name | string | nil | Display name, e.g. "Metal Skin". The key is missing when the token has no known display name, so check it before formatting |
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 "?"))
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 |
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:
| Field | Type | Description |
|---|---|---|
name | string | RTTI class name |
subclass | string | Subclass token as hex text, e.g. "0x9C02E614", not a readable name. Empty string ("") when the item has no subclass token |
slot | integer | 4-7 |
bucket | integer | Always 0 for now. The game no longer sends it |
cooldown | number | Seconds remaining |
is_ready | boolean | Off cooldown, not in cast delay, not channeling |
local me = local_player()
if me:has_active_item("metal_skin") then
local mskin = me: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 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.
| 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). Also works for 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)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.
| Method | What 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_FieldName | Reads one schema field by name. p.m_FieldName = value writes it. Setting any other key on a player raises an error |