Skip to content

Draw

2D and 3D drawing primitives. Draw calls accumulate in a per-tick buffer. That buffer is cleared at the top of every tick and handed to the overlay at the end of it. Whatever you draw lives for exactly one tick. To keep something on screen, draw it again every tick from on_tick.

The buffer holds 1024 shapes and 128 text draws per tick, shared by every loaded script. A call past that budget draws nothing and raises nothing. draw.box3d costs 12 of the shape slots and draw.sphere3d costs three per segment, so 36 at its default segment count.

Event callbacks (on_kill, on_ability_cast, ...) run inside the same tick as on_tick, so drawing from one works. The drawing flashes for that single tick unless you record the state and re-draw it from on_tick. ui.button callbacks are the exception. They run before the buffer is cleared, so draw calls made there are discarded.

Colors

Colors are IM_COL32 packed uint32 in 0xAABBGGRR format (alpha in the high byte, red in the low byte).

lua
local WHITE  = 0xFFFFFFFF
local RED    = 0xFF0000FF  -- red in the low byte, FF alpha
local GREEN  = 0xFF00FF00
local BLUE   = 0xFFFF0000
local YELLOW = 0xFF00FFFF

Use draw.color to build colors from RGBA components without hand-packing:

lua
local muted = draw.color(180, 180, 180)        -- alpha defaults to 255
local fade  = draw.color(255, 255, 255, 80)

draw.color also accepts a single {r, g, b, a} table as its first argument, so the result of a color picker can be passed directly:

lua
local col = draw.color(ui.get(my_color_picker))

Byte order gotcha

This format is ABGR, not ARGB. 0xFFFF0000 is solid blue, not red, most other engines and CSS use 0xAARRGGBB. If you're copying a hex color from elsewhere, the red and blue channels need to swap. draw.color(r, g, b, a) handles the packing for you.

2D primitives

FunctionDescription
draw.line(x1, y1, x2, y2, color, [thickness], [glow])Line between two points. glow = true draws a soft glow under the line
draw.rect(x, y, w, h, color, [thickness], [rounding], [glow])Outlined rectangle - w/h are width and height, not corner coords. glow = true draws a soft glow under the outline
draw.rect_filled(x, y, w, h, color, [rounding], [glow])Filled rectangle - w/h are width and height, not corner coords. glow = true draws a soft glow underneath
draw.circle(x, y, radius, color, [thickness], [segments], [glow])Outlined circle. glow = true draws a soft glow underneath
draw.circle_filled(x, y, radius, color, [segments], [glow])Filled circle. glow = true draws a soft glow underneath
draw.rect_gradient(x, y, w, h, color_a, color_b, [vertical])Rectangle filled with a two-color gradient. w/h are width and height

The optional glow boolean exists only on the first five 2D primitives. draw.rect_gradient, the 3D primitives and all text functions ignore any extra argument.

draw.rect_gradient requires both colors, unlike most optional-looking pairs in this namespace. A truthy vertical puts color_a along the top edge and color_b along the bottom. Omitted or false runs color_a on the left to color_b on the right. There is no rounding argument.

Text

Text functions accept an optional font name OR scale parameter. Pass a string for a font name, or a number for scale on the default font.

FunctionDescription
draw.text(x, y, text, color, [font_or_size], [size])Text with shadow
draw.text_raw(x, y, text, color, [font_or_size], [size])Text without shadow
draw.text3d(pos, text, color, [font_or_size], [size])Text anchored to a world position (vec3)
lua
draw.text(100, 100, "Hello", 0xFFFFFFFF)              -- default font, scale 1.0
draw.text(100, 120, "Hello", 0xFFFFFFFF, 1.5)          -- default font, scale 1.5
draw.text(100, 140, "Hello", 0xFFFFFFFF, "hud")        -- custom font, scale 1.0
draw.text(100, 160, "Hello", 0xFFFFFFFF, "hud", 1.5)   -- custom font, scale 1.5

All text functions return nothing.

draw.text, draw.text_raw and draw.text3d cut the string at 47 bytes. A longer label truncates with no warning and no error, so build long readouts as two lines rather than one.

Fonts

Scripts can use custom fonts. Either pre-load fonts via fonts.lua or let users pick from system fonts via the font picker setting.

FunctionReturnsDescription
draw.set_font(name)-Set the per-script default font for subsequent draw.text calls. Pass nil, "", or "default" to reset to the overlay default
draw.get_font()stringCurrent per-script default font name
draw.get_fonts()tableArray of all loaded custom font names

Font state is per-script. Calling draw.set_font("Arial") in Script A doesn't affect Script B.

If the font name isn't loaded yet, draw.set_font triggers dynamic loading. The engine searches in this order:

  1. C:\TSUKI\Deadlock\assets\fonts\ (custom fonts you add)
  2. C:\Windows\Fonts\ (installed system fonts)

The first frame may render with the default font while the atlas rebuilds.

fonts.lua (optional pre-loading)

Pre-load fonts at specific sizes into the atlas at startup. Drop a fonts.lua next to your scripts. Not required; the font picker dynamically loads system fonts on demand.

lua
-- C:\TSUKI\Deadlock\scripts\fonts.lua
return {
    hud   = { file = "roboto.ttf",   size = 16 },
    small = { file = "consolas.ttf", size = 10 },
    big   = { file = "inter.ttf",    size = 24 },
}

.ttf and .otf files go in C:\TSUKI\Deadlock\assets\fonts\. The keys (hud, small, etc.) become the names you pass to draw.set_font.

Font priority

Highest precedence wins:

  1. Explicit font passed to draw.text(..., font_name)
  2. Per-script default set via draw.set_font(name)
  3. Overlay default font

Images

An image is loaded once into a handle, then drawn into a screen rectangle as often as you like. Both loaders return an integer handle, or nil on failure.

FunctionReturnsDescription
draw.load_image(file_name)integer | nilLoad an image by bare file name from the scripts image folder
draw.load_image_data(base64)integer | nilLoad an image from base64 bytes embedded in the script
draw.image(handle, x, y, w, h, [alpha])-Draw a loaded image into a screen rectangle
draw.image_size(handle)width, height | nilPixel size of a loaded image

draw.load_image takes a bare file name under C:\TSUKI\Deadlock\scripts\images\. Slashes, colons and .. are rejected.

draw.load_image_data takes the file's bytes as base64, at most 256 KB decoded. Identical bytes share one handle.

PNG and JPG, up to 4096 pixels a side, at most 64 images across all scripts. A second draw.load_image for the same name returns the same handle. A failing name logs once.

Load from on_tick. At script load time the call usually returns nil. Keep the handle in a local and retry until it lands.

Argument order gotcha

draw.image takes the handle first. Arguments 4 and 5 are width and height, not a second corner. alpha is 0 to 1. A stale handle draws nothing.

draw.image_size returns two values. nil for a handle that used to work means the texture was dropped; load it again.

lua
local icon

function on_tick()
    icon = icon or draw.load_image("icon.png")   -- nil at load time, so ask every tick until it lands
    if icon then
        local w, h = draw.image_size(icon)
        draw.image(icon, 40, 40, 32, 32 * (h / w), 0.9)
    end
end

3D primitives

3D primitives are reprojected per frame.

FunctionDescription
draw.line3d(p1, p2, color, [thickness])Line between two vec3 points
draw.circle3d(center, radius_px, color, [thickness], [segments])Circle outline anchored to a world position. radius_px is a screen-pixel radius, not world units. Only center is reprojected, so the circle keeps the same on-screen size at any distance
draw.circle3d_filled(center, radius_px, color, [segments])Same, filled
draw.box3d(center, width, depth, height, color, [thickness])Axis-aligned box centered at center (vec3) with dimensions in world units along X (width), Y (depth), and Z (height)
draw.sphere3d(center, radius, color, [thickness], [segments])Wireframe sphere (3 great circles); segments clamped 4-32, default 12

The radius units differ between the two circle families and the names do not say so. draw.circle3d and draw.circle3d_filled take a radius in screen pixels, while draw.sphere3d takes one in world units. Only draw.box3d and draw.sphere3d take world-unit dimensions; the 3D circles take pixels.

Projection and screen size

FunctionReturnsDescription
draw.world_to_screen(vec3)vec2 | nilPixel position as a vec2 with .x and .y, or nil if behind the camera
draw.screen_size()vec2Screen dimensions as vec2 with .x (width) and .y (height)
draw.measure_text(text, [font_or_scale], [size])vec2Rendered pixel size of text as vec2 with .x (width) and .y (height). Same font/scale resolution as draw.text

world_to_screen is also available as camera.world_to_screen. Same function.

draw.screen_size() is also available as camera.get_screen_size(). Same function.

Example

lua
local WHITE = 0xFFFFFFFF
local RED   = 0xFF3232FF

function on_tick()
    for _, p in ipairs(get_players()) do
        if p:is_enemy() and p:is_alive() then
            local box = p:screen_box()
            if box then
                draw.rect(box.x, box.y, box.w, box.h, RED, 2)
                draw.text(box.x, box.y - 14, p:hero_name(), WHITE)
            end
        end
    end
end