Skip to content

Shots to Kill

Prints the nearest enemy's name with the number of body and head shots your gun needs to kill them, the per hit damage and 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 carries bullet_damage; the enemy's carries resists, shield and barrier.
  • Two guards. Either read can be nil while the other answers, so each is checked on its own.
  • Absent, not zero. bullet_damage is missing on other players' rows, so the check is if not ms.bullet_damage, not a comparison 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))

1.65 is an observed head multiplier, not a value the API returns. 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. Past falloff_min the number reads optimistic; that is what the orange line is for.
  • The dmg_taken_inc and dmg_taken_red terms are rarely non zero. They stay in the formula but do not expect them to change the result.
  • The pcall wrappers exist because a player object is only valid for the current tick. Store p:entity_ptr() and look the player up again next tick.
  • To compare the prediction with what actually landed, see Hit Events. Every stat key and its unit is on Stats.