Skip to content

Boxes and styles

The HUD a client script draws: a tree of boxes, styled with CSS. A script mounts a function that returns its boxes; it draws again whenever something it read changes, and only what differs reaches the screen.

Styles are CSS as a browser has it, keys as JavaScript writes them: fontSize = 32, color = "#ffcc33", display = "flex", transitions and animations. A number is pixels of a 1080-line screen. The style properties are listed last. The killstreak guide draws one line.

The rest of the screen is this player’s too, at no network cost: view:set changes the field of view, the exposure, the fog and the crosshair, shake shakes the view or the aim, and highlight fills or outlines the bodies and props drawn.

On a client view: View

What this player's view shows: its field of view, exposure, fog and crosshair.

On a client highlight(rule)

Sets how the entities this player sees are drawn, at no network cost: rule gives each a highlight, or nil for none.

It is given at a script’s top, one rule a script, and asked of each body, NPC, prop, shape and part drawn as it appears, given its entity, and again only as what it read changes, as a render is. With several scripts’ rules, the last loaded that gives one wins. A highlight is {fill, filter, outline, through_walls, transition}: fill a CSS colour mixed over the entity by its alpha, "#ffffff" a flat silhouette; filter CSS’s functions but blur(); outline a width of up to 4 pixels and a colour, "2px #ffcc33"; through_walls shows the fill and the outline where the entity is hidden; transition eases it in and out, its seconds finite.

What rides an entity is drawn as it is, unless highlighted itself; the weapon before the eyes never is. Highlights are drawn as the HUD’s zIndex 0 starts, over a frame filter below it, so greying the world leaves highlighted bodies as they are. 16 different ones show at once among what the camera sees, past which the rest are drawn plain. A highlight shows only what the server sends this player.

A new highlight eases in by its own transition and the last out by its own; an entity that comes into view is shown highlighted at once.

NameTypeWhat it is
rule(entity: Entity) -> Highlight?Needed.
Fails
  • a value of the wrong type
highlight(function(e)
if e.kind == "npc" then
return { outline = "2px #ff3333", through_walls = true, transition = { seconds = 0.2 } }
end
return nil
end)
view:set({ exposure = 0.5 })
mount(function()
local filter = "grayscale(1) contrast(1.4)"
return Box { style = { left = 0, top = 0, right = 0, bottom = 0, zIndex = -1, backdropFilter = filter } }
end)
highlight(function(e)
if e.kind == "player" or e.kind == "npc" then
return { filter = "grayscale(1) brightness(3)", fill = "rgba(255, 255, 255, 0.3)" }
end
return nil
end)
On a client keyframes(name, frames?)

Defines keyframes, as CSS's @keyframes, which a style's animation plays by name; nil frames remove them.

frames is keyed "from", "to" or a percentage, a comma between several, each a style of the properties transition eases and its own animationTimingFunction, 8 keyframes at most. A name is up to 16 lowercase letters, digits and _, but no word animation takes, such as linear. An animation drawn again under its name plays on; one whose name went away starts over. A style plays 4 animations at most.

NameTypeWhat it is
namestringNeeded.
frames({[string]: Style})?May be left out.
Fails
  • a value of the wrong type
keyframes("pulse", {
from = { opacity = 1 },
["50%"] = { opacity = 0.3, animationTimingFunction = "ease-in" },
to = { opacity = 1 },
})
mount(function()
return Box { "LOW AMMO", style = { left = 40, top = 80, color = "#ff3333", animation = "pulse 1s infinite" } }
end)
On a client shake(target, { amplitude?, frequency }, transition) → Shake

Shakes this player's view, "view", the picture alone, or "aim", where the player aims and shoots too, and gives its handle, whose stop(transition) fades it out.

transition’s seconds are how long it lasts, above 0, math.huge until stopped, and its timing how it fades. Shakes add up, 32 at once, a new one past them ending the oldest. An aim shake reaches the server with the player’s aim, which a modified client could skip, as it can a weapon’s kick.

NameTypeWhat it is
target"view" | "aim"Needed.
wavetableA table of the options below. Those marked needed may not be left out.
amplitudeAmplitude?How far it turns each way on each axis. A table of the options below it.
amplitude.yawnumber?0 to 3.141592653589793About the vertical, left and right.
amplitude.pitchnumber?0 to 3.141592653589793Up and down.
amplitude.rollnumber?0 to 3.141592653589793About the line of sight: the view's alone, not the aim's.
frequencynumber0 to 60How often it turns, a second, above 0: a smooth noise rather than a sine. Needed.
transitionTransitionNeeded.
Returns
Shake
Fails
  • a value of the wrong type
  • an option it does not have
  • a key it needs left out
  • a value outside its range
local wave = { amplitude = { yaw = 0.02, pitch = 0.03, roll = 0.01 }, frequency = 8 }
shake("view", wave, { seconds = 1.5, timing = "ease-out" })

To sway the aim until the player steadies it

Section titled “To sway the aim until the player steadies it”
local sway = shake("aim", { amplitude = { yaw = 0.01, pitch = 0.01 }, frequency = 0.5 }, { seconds = math.huge })
sway:stop({ seconds = 1 })
On a client merge(defaults, props) → D & P

Gives a component's defaults with its props over them, the caller's winning, to pass on to a Box.

A style is merged property by property, a function of is among them; content given replaces the default’s.

NameTypeWhat it is
defaultsDNeeded.
propsPNeeded.
Returns
D & P
Fails
  • a value of the wrong type
local Button = component(function(props)
return Box(merge({ style = { padding = "4px 8px", color = "#ffffff" } }, props))
end)
mount(function() return Button { "Leave", style = { color = "#ff3333" } } end)
On a client omit(props, ...keys) → P

Gives a copy of props without the keys named, a component's own, which a Box refuses.

NameTypeWhat it is
propsPNeeded.
...keysstringAny number of them.
Returns
P
Fails
  • a value of the wrong type

To pass on all but a component’s own prop

Section titled “To pass on all but a component’s own prop”
local Badge = component(function(props)
return Box(merge({ if props.danger then "!" else "" }, omit(props, "danger")))
end)
mount(function() return Badge { danger = true, style = { color = "#ff3333" } } end)
On a client store(first?) → T

Gives a table of the script's own data, which a render reads as it reads the game: one that read a key renders again as it changes.

It is read and written as a table, banner.count += 1, gone over with for key, value in banner, and keyed by any key a table takes. Once a frame, after the handlers, each store of a script that ran is compared deeply with its copy, on the script’s time. A list goes in a field, store { items = {} }: table.insert, table.remove and table.sort write past a store, and what they write there is dropped and said.

NameTypeWhat it is
firstT?May be left out.
Returns
T
Fails
  • a value of the wrong type
local banner = store { kills = 0 }
on("killed", function(e)
if e.killer == me then banner.kills += 1 end
end)
mount(function() return Box { `{banner.kills} kills` } end)
On a client Box(props) → Node

A box: its content, unnamed and first, then its props, named.

Its content is its text, strings and numbers joined, and its children: Boxes, components’ calls and lists of them, flattened, nil and false skipped. Every element of a list needs a key, unique among its siblings; a new key is a new box, its animations started again. A style table is read where it is written, so a property the HUD lacks fails at that line; a function of is runs as its box renders, and again as hovered, pressed or disabled flips. No style is inherited.

A root box is placed on the screen as CSS’s position: absolute; its children flow in it. at and follow lay a box out from a point in the world, keeping its size at any distance and showing through walls: left and top measure from where the point shows, right and bottom back from it. It hides while the point is behind the eye or what it follows is not drawn, unless edge. A text is up to 256 bytes, \n between lines, shown as written, braces and all. A box under more than 8 parents is not drawn. Past 8 frames’ worth of pictures, or 32 boxes reading what is behind them, a frame draws boxes without their effects, and masked ones not at all.

NameTypeWhat it is
propsBoxPropsNeeded.
Returns
Node
Fails
  • a value of the wrong type
mount(function()
return Box {
`{me.health or 0} HP`,
style = { right = 40, bottom = 40, fontSize = 32, color = "#ffcc33" },
}
end)
mount(function()
return Box {
"Ready",
style = function(is)
return { padding = "4px 8px", background = if is.hovered then "#ffcc33" else "#333333" }
end,
onClick = function() print("ready") end,
}
end)
mount(function()
return Box { "BOMB", at = vector.create(4, 1, -2), edge = true, style = { color = "#ff3333" } }
end)
On a client Each(list?, render) → {Node}

Renders each item of a list, render(item, index) of its 1 to #list, each its own redraw boundary.

Each element it gives needs a key, unique among them; an item giving nil shows nothing. An item renders again only as what it read changes, so a long list costs only what changed.

NameTypeWhat it is
list{T}?May be left out.
render(item: T, index: number) -> Node?Needed.
Returns
{Node}
Fails
  • a value of the wrong type
mount(function()
local listed = {}
for _, p in players do
table.insert(listed, p)
end
return Box {
Each(listed, function(p) return Box { p.name, key = p.id } end),
style = { display = "flex", flexDirection = "column" },
}
end)
On a client component(render) → (props: P) -> Node

Makes a component of a function of props: a call of it, Row { key = id, … }, renders as its own redraw boundary.

It renders again only as what it read changes, or as its parent gives it props that differ, functions all alike. A component’s props extend BoxProps, as BoxProps & { danger: boolean? }.

NameTypeWhat it is
render(props: P) -> Node?Needed.
Returns
(props: P) -> Node
Fails
  • a value of the wrong type
local Row = component(function(props)
return Box { props.name, style = { color = if props.dead then "#888888" else "#ffffff" } }
end)
mount(function()
return Box { Row { key = 1, name = "Ann", dead = false }, Row { key = 2, name = "Bo", dead = true } }
end)
On a client mount(component)

Shows a component, or a function of no props, from the first frame its match plays until its script loads again or is stopped.

A render gives one Box, a component’s call or nil, and renders again as what it read changes: match, server, me, players, entities, a store and now(step); only what differs reaches the screen. Mounts are drawn in the order mounted, zIndex ordering them, and never take the pointer. The engine’s own HUD is drawn at zIndex 0: a box below it is drawn under it. For a HUD in several corners, mount each part, or give the root left = 0, top = 0, right = 0, bottom = 0 and its parts position = "absolute".

What the scripts keep on screen, their boxes and keyframes, takes 8 MB a script and 64 MB in all, each counting 1 KB beside its bytes; past it, the script is stopped and its trees go. A frame makes 4096 changes to the screen at most.

NameTypeWhat it is
component(props: P) -> Node?Needed.
Fails
  • a value of the wrong type
mount(function()
return Box { me.name or "", style = { left = 40, top = 40 } }
end)
On a client Shake:stop(transition?)

Fades the shake out from where it is, at once without a transition, a later stop replacing an earlier.

NameTypeWhat it is
transitionTransition?May be left out.
Fails
  • a value of the wrong type

To sway the aim until the player steadies it

Section titled “To sway the aim until the player steadies it”
local sway = shake("aim", { amplitude = { yaw = 0.01, pitch = 0.01 }, frequency = 0.5 }, { seconds = math.huge })
sway:stop({ seconds = 1 })
On a client View:set(values, transition?)

Changes how this player's view looks, at no network cost, each value given eased by transition, at once without one; the rest stay as they are, the last set winning.

view.fov, view.exposure, view.fog, view.listener and view.crosshair read back what was last set, fov and fog nil while the player’s and the map’s own. A fog is exponential: half_at metres away, half of what lies there is hidden; {half_at = math.huge} is none. A frame takes 64 changes to the view at most.

NameTypeWhat it is
valuestableA table of the options below. Each may be left out.
fov(number | false)?The field of view, in degrees across a 4:3 screen as the player's fov setting has them, over the setting and the weapon's sights; false for the player's own.
exposurenumber?0 or moreHow bright the scene is before tone mapping, 1 as without it; the HUD is as bright as ever.
fog(Fog | false)?A fog over the map's, the sky fading into it; false for the map's own.
listener("camera" | "body")?Where the sound is heard: at the view, wherever the mod's camera puts it, or at the eyes of the body the view is of; at once, whatever the transition.
crosshairboolean?Whether the engine's crosshair shows, fading.
transitionTransition?May be left out.
Fails
  • a value of the wrong type
  • an option it does not have
  • a value outside its range
view:set({ fov = 20 }, { seconds = 0.2, timing = "ease-out" })
view:set({ fov = false }, { seconds = 0.2 })
view:set({ fog = { color = "#8a8f99", half_at = 12 }, crosshair = false }, { seconds = 3 })

What a Box’s style takes, on a client. A property it does not take fails.

NameTakesWhat it is
leftlengthHow far its left edge is from its parent's, or the screen's; or auto.
toplengthHow far its top edge is from its parent's, or the screen's; or auto.
rightlengthHow far its right edge is from its parent's, or the screen's; or auto.
bottomlengthHow far its bottom edge is from its parent's, or the screen's; or auto.
widthlengthIts width, not negative; or auto, as its content needs.
heightlengthIts height, not negative; or auto, as its content needs.
paddinglengthsThe room inside its border: one to four lengths, as CSS's sides, no %.
colorcolourIts text's colour; white without it.
fontSizelengthIts text's size, 24 pixels without it; em and % are of the screen's 24.
fontWeightnumbernormal, bold or 1 to 1000, bold from 700.
textAlignleft, center, right, start, endWhere its lines sit.
lineHeightnumberThe height of a line: a multiple of the font size, a length, % or normal.
letterSpacinglengthThe room between letters, or normal.
textTransformnone, uppercase, lowercase, capitalizeIts text's case.
fontStylenormal, italic, obliqueSlanted, oblique 10deg under 90deg.
overflowvisible, hidden, clipWhether what passes its box shows.
textOverflowclip, ellipsisHow a line cut by overflow ends.
backgroundCSSIts background colour, or image, position and size, as CSS's shorthand.
backgroundColorcolourThe colour behind its content.
backgroundImageimageAn image the server named, or a gradient, drawn behind its content.
backgroundPositionpositionWhere its background image sits; at its top-left without it.
backgroundSizesizeHow big its background image is; its own size without it.
borderCSSA width, solid or none, and a colour, in any order.
borderWidthlengthIts border's width, one for all sides, no %.
borderStylesolid, none, hiddenWhether its border is drawn.
borderColorcolourIts border's colour.
borderRadiuslengthsHow round its corners are: one to four lengths, as CSS's corners.
boxShadowCSSOne shadow: x and y, a blur and a spread if given, and a colour; or none.
transformCSSCSS's transform functions, in order: translate(), rotate(), scale()…
transformOriginpositionThe point it turns and scales about; its middle without it.
transitionCSSHow a change to its properties eases: a property, seconds, a timing, a delay. It eases the place and the size, padding, colours, text sizes and spacing, backgrounds, borders, shadows, transforms, opacity, filters and masks, or all.
animationCSSUp to 4 animations of keyframes by name, as CSS's shorthand.
opacitynumberHow opaque it and all inside it are, 0 to 1 or a percentage.
filterCSSUp to 8 of CSS's filter functions on it and all inside it, as one: blur, brightness, contrast, grayscale, hue-rotate, invert, opacity, saturate and sepia.
backdropFilterCSSUp to 8 of CSS's filter functions on what is behind it, within its border.
mixBlendModenormal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity, plus-lighterHow it and all inside it blend with what is behind it.
isolationauto, isolateKeeps what is inside it blending only with each other.
maskImageimageAn image or a gradient whose opacity shows it, nothing outside it; nothing while its image loads.
maskSizesizeHow big its mask is.
maskPositionpositionWhere its mask sits.
zIndexwhole numberIts order among its siblings, or auto; the engine's HUD is at 0.
userSelectnone, auto, textWhether its text may be selected.
displayblock, flex, noneHow it lays its children out, or hides.
positionstatic, relative, absoluteWhether it flows in its parent.
flexDirectionrow, row-reverse, column, column-reverseWhich way a flex box lays its children.
flexWrapnowrap, wrap, wrap-reverseWhether a flex box's children wrap.
justifyContentnormal, flex-start, flex-end, start, end, center, space-between, space-around, space-evenlyWhere a flex box's children sit along its way.
alignItemsnormal, stretch, flex-start, flex-end, start, end, centerWhere a flex box's children sit across its way.
alignSelfauto, normal, stretch, flex-start, flex-end, start, end, centerWhere it sits across its flex parent's way, over its alignItems.
gaplengthsThe room between a flex box's rows and columns: one for both, or two.
rowGaplengthThe room between a flex box's rows, or normal.
columnGaplengthThe room between a flex box's columns, or normal.
flexCSSGrow and shrink factors and a basis, or none, auto or initial.
flexGrownumberHow much of a flex box's room to spare it takes.
flexShrinknumberHow much it gives up when a flex box lacks room.
flexBasislengthIts size along its flex parent's way before growing, or auto or content.
orderwhole numberIts place among a flex box's children.