FluxWareScripting
Back to site

Cookbook

Worked examples for the things people actually write. Each one is complete and runs as-is.

A HUD element

Text and a panel in the corner. render2d shapes are batched, so a handful of them per frame costs nothing measurable.

slug: coords
local m = module {
  name = 'Coords',
  category = 'render',
}

local size  = m:setting('number', 'Text size', 9, 6, 16)
local tint  = m:setting('color',  'Colour', 0xFF22D3EE)

m:on('render2d', function()
  local x, y, z = player.pos()
  if not x then return end

  local text = ('%.1f  %.1f  %.1f'):format(x, y, z)
  local w = render2d.textWidth(text, size:get())

  render2d.roundedRect(8, 8, w + 16, size:get() + 12, 6, 0x99000000)
  render2d.text(text, 16, 14, size:get(), tint:get())
end)

Highlighting entities

A box around every player, drawn in the world. Note the entity.isValid check is unnecessary here because the handles come from a query made in this same frame; it becomes necessary the moment you keep one.

slug: player-boxes
local m = module {
  name = 'Player Boxes',
  category = 'render',
}

local fill    = m:setting('color', 'Fill', 0x302563EB)
local outline = m:setting('color', 'Outline', 0xFF3B82F6)

m:on('render3d', function()
  for _, e in ipairs(entities.players()) do
    if not e:isFriend() then
      render3d.entityBox(e, fill:get(), outline:get())
    end
  end
end)
Zero alpha skips the pass
Passing a colour with an alpha of 0 to a render3d function skips that draw entirely rather than drawing something invisible. Set the fill to 0 for outline-only boxes.

Reacting to health

Combining a world-space query with a screen-space overlay: render3d.worldToScreen is callable from a render2d handler, which is where you usually want the answer.

slug: low-health-tags
local m = module {
  name = 'Low Health Tags',
  category = 'render',
}

local threshold = m:setting('number', 'Show under', 12, 1, 20)

m:on('render2d', function()
  for _, e in ipairs(entities.players()) do
    local hp = e:health()
    if hp and hp < threshold:get() then
      local x, y, z = e:pos()
      local sx, sy = render3d.worldToScreen(x, y + 2.2, z)
      if sx then
        local label = ('%.0f'):format(hp)
        local w = render2d.textWidth(label, 9)
        render2d.text(label, sx - w / 2, sy, 9, 0xFFFF3B30)
      end
    end
  end
end)

Changing movement

The move event hands you the delta about to be applied and takes a replacement. Scripts compose: if two movement scripts are on, the second sees what the first returned.

slug: sprint-boost
local m = module {
  name = 'Sprint Boost',
  category = 'movement',
}

local factor = m:setting('number', 'Multiplier', 1.15, 1, 2)

m:on('move', function(x, y, z)
  if not player.isSprinting() or not player.isOnGround() then return end
  return x * factor:get(), y, z * factor:get()
end)
This is the part servers notice
Movement is the most heavily checked thing a client does. A multiplier that looks modest here is very visible to an anticheat. See the sandbox page.

Doing something on an interval

There is no sleep and no timer: a script cannot block the game thread. Compare timestamps instead.

slug: announce
local m = module {
  name = 'Announce',
  category = 'misc',
}

local every   = m:setting('number', 'Seconds', 30, 5, 300)
local message = m:setting('string', 'Message', 'gg')

local lastSent = 0

m:on('enable', function()
  -- Reset on enable, so switching the module on does not immediately fire.
  lastSent = util.time()
end)

m:on('tick', function()
  local now = util.time()
  if now - lastSent < every:get() * 1000 then return end
  lastSent = now
  player.sendChat(message:get())
end)

Patterns worth copying

Bail early on nil. Every getter returns nil with no world. One if not x then return end at the top of a handler is cheaper than a nil check on every line, and it is what stops a script throwing on the title screen.

Reset in the enable handler. Anything you accumulate across ticks should be cleared there. A module switched off and on again otherwise resumes mid-thought.

Do not keep entity handles. They go stale. If you must keep one across ticks, guard every use with e:isValid().

Query once, use many times. entities.players() walks the world; calling it three times in one handler walks it three times. Hoist it into a local.