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_statsworks on any player. Your row hasbullet_damage. The enemy's row has resists, shield and barrier.- Two checks. Either read can be
nilwhile the other one works, so each is checked on its own. - Missing keys.
bullet_damageis missing on other players' rows, so check it withif not ms.bullet_damageinstead of comparing it with 0. - Colors are packed integers, the same values
draw.color(...)returns.draw.texttakes 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
endNotes
- Falloff is not modelled, so past
falloff_minthe number is optimistic. The orange line warns you when this happens. - The
dmg_taken_incanddmg_taken_redterms are usually zero, so they rarely change the result. - The
pcallwrappers are only a safety net. A player object is only good for the tick you got it, so to follow a player across ticks, storep: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.