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.
view: ViewWhat this player's view shows: its field of view, exposure, fog and crosshair.
highlight
Section titled “highlight”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.
| Name | Type | What it is |
|---|---|---|
rule | (entity: Entity) -> Highlight? | Needed. |
- Fails
- a value of the wrong type
To outline the zombies through walls
Section titled “To outline the zombies through walls”highlight(function(e) if e.kind == "npc" then return { outline = "2px #ff3333", through_walls = true, transition = { seconds = 0.2 } } end return nilend)To see in thermal
Section titled “To see in thermal”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 nilend)keyframes
Section titled “keyframes”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.
| Name | Type | What it is |
|---|---|---|
name | string | Needed. |
frames | ({[string]: Style})? | May be left out. |
- Fails
- a value of the wrong type
To pulse a warning
Section titled “To pulse a warning”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)shake(target, { amplitude?, frequency }, transition) → ShakeShakes 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.
| Name | Type | What it is |
|---|---|---|
target | "view" | "aim" | Needed. |
wave | table | A table of the options below. Those marked needed may not be left out. |
amplitude | Amplitude? | How far it turns each way on each axis. A table of the options below it. |
amplitude.yaw | number?0 to 3.141592653589793 | About the vertical, left and right. |
amplitude.pitch | number?0 to 3.141592653589793 | Up and down. |
amplitude.roll | number?0 to 3.141592653589793 | About the line of sight: the view's alone, not the aim's. |
frequency | number0 to 60 | How often it turns, a second, above 0: a smooth noise rather than a sine. Needed. |
transition | Transition | Needed. |
- 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
To shake the screen as a blast goes off
Section titled “To shake the screen as a blast goes off”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 })merge(defaults, props) → D & PGives 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.
| Name | Type | What it is |
|---|---|---|
defaults | D | Needed. |
props | P | Needed. |
- Returns
D & P- Fails
- a value of the wrong type
To make a button its caller may restyle
Section titled “To make a button its caller may restyle”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)omit(props, ...keys) → PGives a copy of props without the keys named, a component's own, which a Box refuses.
| Name | Type | What it is |
|---|---|---|
props | P | Needed. |
...keys | string | Any 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)store(first?) → TGives 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.
| Name | Type | What it is |
|---|---|---|
first | T? | May be left out. |
- Returns
T- Fails
- a value of the wrong type
To count this player’s kills
Section titled “To count this player’s kills”local banner = store { kills = 0 }on("killed", function(e) if e.killer == me then banner.kills += 1 endend)mount(function() return Box { `{banner.kills} kills` } end)Box(props) → NodeA 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.
| Name | Type | What it is |
|---|---|---|
props | BoxProps | Needed. |
- Returns
Node- Fails
- a value of the wrong type
To show a player’s health in a corner
Section titled “To show a player’s health in a corner”mount(function() return Box { `{me.health or 0} HP`, style = { right = 40, bottom = 40, fontSize = 32, color = "#ffcc33" }, }end)To light a button up under the pointer
Section titled “To light a button up under the pointer”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)To mark where a bomb was planted
Section titled “To mark where a bomb was planted”mount(function() return Box { "BOMB", at = vector.create(4, 1, -2), edge = true, style = { color = "#ff3333" } }end)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.
| Name | Type | What it is |
|---|---|---|
list | {T}? | May be left out. |
render | (item: T, index: number) -> Node? | Needed. |
- Returns
{Node}- Fails
- a value of the wrong type
To list the players by name
Section titled “To list the players by name”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)component
Section titled “component”component(render) → (props: P) -> NodeMakes 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? }.
| Name | Type | What it is |
|---|---|---|
render | (props: P) -> Node? | Needed. |
- Returns
(props: P) -> Node- Fails
- a value of the wrong type
To make each row its own boundary
Section titled “To make each row its own boundary”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)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.
| Name | Type | What it is |
|---|---|---|
component | (props: P) -> Node? | Needed. |
- Fails
- a value of the wrong type
To show the player’s name in a corner
Section titled “To show the player’s name in a corner”mount(function() return Box { me.name or "", style = { left = 40, top = 40 } }end)Shake:stop
Section titled “Shake:stop”Shake:stop(transition?)Fades the shake out from where it is, at once without a transition, a later stop replacing an earlier.
| Name | Type | What it is |
|---|---|---|
transition | Transition? | 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 })View:set
Section titled “View:set”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.
| Name | Type | What it is |
|---|---|---|
values | table | A 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. |
exposure | number?0 or more | How 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. |
crosshair | boolean? | Whether the engine's crosshair shows, fading. |
transition | Transition? | May be left out. |
- Fails
- a value of the wrong type
- an option it does not have
- a value outside its range
To zoom in through a scope, then back
Section titled “To zoom in through a scope, then back”view:set({ fov = 20 }, { seconds = 0.2, timing = "ease-out" })view:set({ fov = false }, { seconds = 0.2 })To fade into a thick fog
Section titled “To fade into a thick fog”view:set({ fog = { color = "#8a8f99", half_at = 12 }, crosshair = false }, { seconds = 3 })Style properties
Section titled “Style properties”What a Box’s style takes, on a client. A property it does not take fails.
| Name | Takes | What it is |
|---|---|---|
left | length | How far its left edge is from its parent's, or the screen's; or auto. |
top | length | How far its top edge is from its parent's, or the screen's; or auto. |
right | length | How far its right edge is from its parent's, or the screen's; or auto. |
bottom | length | How far its bottom edge is from its parent's, or the screen's; or auto. |
width | length | Its width, not negative; or auto, as its content needs. |
height | length | Its height, not negative; or auto, as its content needs. |
padding | lengths | The room inside its border: one to four lengths, as CSS's sides, no %. |
color | colour | Its text's colour; white without it. |
fontSize | length | Its text's size, 24 pixels without it; em and % are of the screen's 24. |
fontWeight | number | normal, bold or 1 to 1000, bold from 700. |
textAlign | left, center, right, start, end | Where its lines sit. |
lineHeight | number | The height of a line: a multiple of the font size, a length, % or normal. |
letterSpacing | length | The room between letters, or normal. |
textTransform | none, uppercase, lowercase, capitalize | Its text's case. |
fontStyle | normal, italic, oblique | Slanted, oblique 10deg under 90deg. |
overflow | visible, hidden, clip | Whether what passes its box shows. |
textOverflow | clip, ellipsis | How a line cut by overflow ends. |
background | CSS | Its background colour, or image, position and size, as CSS's shorthand. |
backgroundColor | colour | The colour behind its content. |
backgroundImage | image | An image the server named, or a gradient, drawn behind its content. |
backgroundPosition | position | Where its background image sits; at its top-left without it. |
backgroundSize | size | How big its background image is; its own size without it. |
border | CSS | A width, solid or none, and a colour, in any order. |
borderWidth | length | Its border's width, one for all sides, no %. |
borderStyle | solid, none, hidden | Whether its border is drawn. |
borderColor | colour | Its border's colour. |
borderRadius | lengths | How round its corners are: one to four lengths, as CSS's corners. |
boxShadow | CSS | One shadow: x and y, a blur and a spread if given, and a colour; or none. |
transform | CSS | CSS's transform functions, in order: translate(), rotate(), scale()… |
transformOrigin | position | The point it turns and scales about; its middle without it. |
transition | CSS | How 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. |
animation | CSS | Up to 4 animations of keyframes by name, as CSS's shorthand. |
opacity | number | How opaque it and all inside it are, 0 to 1 or a percentage. |
filter | CSS | Up 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. |
backdropFilter | CSS | Up to 8 of CSS's filter functions on what is behind it, within its border. |
mixBlendMode | normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity, plus-lighter | How it and all inside it blend with what is behind it. |
isolation | auto, isolate | Keeps what is inside it blending only with each other. |
maskImage | image | An image or a gradient whose opacity shows it, nothing outside it; nothing while its image loads. |
maskSize | size | How big its mask is. |
maskPosition | position | Where its mask sits. |
zIndex | whole number | Its order among its siblings, or auto; the engine's HUD is at 0. |
userSelect | none, auto, text | Whether its text may be selected. |
display | block, flex, none | How it lays its children out, or hides. |
position | static, relative, absolute | Whether it flows in its parent. |
flexDirection | row, row-reverse, column, column-reverse | Which way a flex box lays its children. |
flexWrap | nowrap, wrap, wrap-reverse | Whether a flex box's children wrap. |
justifyContent | normal, flex-start, flex-end, start, end, center, space-between, space-around, space-evenly | Where a flex box's children sit along its way. |
alignItems | normal, stretch, flex-start, flex-end, start, end, center | Where a flex box's children sit across its way. |
alignSelf | auto, normal, stretch, flex-start, flex-end, start, end, center | Where it sits across its flex parent's way, over its alignItems. |
gap | lengths | The room between a flex box's rows and columns: one for both, or two. |
rowGap | length | The room between a flex box's rows, or normal. |
columnGap | length | The room between a flex box's columns, or normal. |
flex | CSS | Grow and shrink factors and a basis, or none, auto or initial. |
flexGrow | number | How much of a flex box's room to spare it takes. |
flexShrink | number | How much it gives up when a flex box lacks room. |
flexBasis | length | Its size along its flex parent's way before growing, or auto or content. |
order | whole number | Its place among a flex box's children. |