Skip to content

Shots to Kill ​

Prints the nearest enemy's name and how many body and head shots your gun needs to kill them. It also shows the per hit damage, the health pool behind that number, and a warning when the target is beyond your falloff range.

What this shows ​

  • get_stats works on any player. Your row has bullet_damage. The enemy's row has resists, shield and barrier.
  • Two checks. Either read can be nil while the other one works, so each is checked on its own.
  • Missing keys. bullet_damage is missing on other players' rows, so check it with if not ms.bullet_damage instead of comparing it with 0.
  • Colors are packed integers, the same values draw.color(...) returns. draw.text takes a scale as its fifth argument.

The formula ​

per_hit = bullet_damage * (1 - bullet_resist/100) * (1 + taken_inc/100) * (1 - taken_red/100)
pool    = health + bullet_shield + barrier
shots   = ceil(pool / per_hit)
head    = ceil(pool / (per_hit * 1.65))

The script uses a fixed head multiplier of 1.65. For the real one, use head_mult from p:get_shot_damage(), which also handles pellets and falloff. See Damage preview. p:get_distance() is in metres and falloff_min is in game units, 39.37 per metre.

Script ​

lua
-- shots_to_kill.lua
-- Bullets to kill on the nearest enemy, from the internal stat feed.
-- Falloff is ignored past falloff_min, so the number reads optimistic at range.

script_info = {
    name         = "shots_to_kill",
    author       = "tsuki",
    version      = "1.0",
    description  = "bullets-to-kill readout for the nearest enemy (needs the internal)",
    needs_internal = true,
}

local WHITE, DIM, GREEN, ORANGE = 0xFFFFFFFF, 0xFFA8A8A8, 0xFF7FE07F, 0xFF3C96FF
local HEAD_MULT = 1.65

local function nearest_enemy()
    local best, bd = nil, 1e12
    local list = get_players and get_players() or nil
    if type(list) ~= "table" then return nil end
    for _, p in ipairs(list) do
        local ok_e, is_e = pcall(function() return p:is_enemy() end)
        local ok_a, is_a = pcall(function() return p:is_alive() end)
        if ok_e and is_e and ok_a and is_a then
            local ok_d, d = pcall(function() return p:get_distance() end)
            d = (ok_d and type(d) == "number") and d or 1e11
            if d < bd then bd, best = d, p end
        end
    end
    return best, bd
end

function on_tick()
    local x, y = 40, 460
    local function line(t, c, sc) draw.text(x, y, t, c or WHITE, sc or 1.15); y = y + 18 end

    if not internal.is_loaded() then line("shots_to_kill: internal not loaded", DIM); return end
    local me = local_player and local_player() or nil
    local ms = me and me:get_stats() or nil
    if not ms or not ms.bullet_damage then line("shots_to_kill: waiting for local stats", DIM); return end

    local e, d = nearest_enemy()
    local es = e and e:get_stats() or nil
    if not e or not es then line("shots_to_kill: no enemy stats yet", DIM); return end

    local B   = ms.bullet_damage
    local R   = (es.bullet_resist or 0) / 100
    local inc = ((es.dmg_taken_inc or 0) + (es.bullet_dmg_taken_inc or 0)) / 100
    local red = ((es.dmg_taken_red or 0) + (es.bullet_dmg_red_pct or 0)) / 100
    local per = B * (1 - R) * (1 + inc) * (1 - red)
    if per < 0.1 then line("shots_to_kill: per-hit damage ~0", DIM); return end

    local ok_h, hp = pcall(function() return e:get_health() end)
    hp = (ok_h and type(hp) == "number") and hp or (es.max_health or 0)
    local pool  = hp + (es.bullet_shield or 0) + (es.barrier or 0)
    local shots = math.ceil(pool / per)
    local head  = math.ceil(pool / (per * HEAD_MULT))
    local ok_n, name = pcall(function() return e:hero_name() end)
    name = (ok_n and name) or "enemy"
    local past = (ms.falloff_min or 0) > 0 and d and (d * 39.37) > ms.falloff_min

    line(string.format("%s  %d body / %d head", name, shots, head), GREEN, 1.4)
    line(string.format("%.1f per hit  (%.1f x (1-%.0f%%))   pool %.0f = hp %.0f + shield %.0f + barrier %.0f",
        per, B, R * 100, pool, hp, es.bullet_shield or 0, es.barrier or 0), DIM)
    if past then line(string.format("beyond falloff (%.0fm > %.0fm): optimistic", d, ms.falloff_min / 39.37), ORANGE) end
end

Notes ​

  • Falloff is not modelled, so past falloff_min the number is optimistic. The orange line warns you when this happens.
  • The dmg_taken_inc and dmg_taken_red terms are usually zero, so they rarely change the result.
  • The pcall wrappers are only a safety net. A player object is only good for the tick you got it, so to follow a player across ticks, store p:entity_ptr() and find them again next tick.
  • To compare the prediction with the hits that landed, see Hit Events. Stats lists every stat key and its unit.