starlit

movement

Build your own movement features on a virtual keyboard: a stick with a direction, plus jump, duck and walk, applied where the player's own keys are.

movement is a virtual keyboard for a script. It holds one intent at a time: a stick, and the jump, duck and walk buttons. The game applies the intent exactly where the player's own keys are applied, so the anti-aim, the jump features, the edge bug, the ladder helper and the ragebot treat it as the player's input. Use it for auto-peek steps, stop and slide helpers, crouch and hop helpers, and for steering the built-in strafe along a path.

The world is read with the entity and game modules. movement itself returns nothing but true or false.

The intent

An intent is a table passed to movement.intent. There are two ways to describe the stick.

  • Stick axes. forward and side are numbers from -1 to 1. A positive forward is ahead and a positive side is to the right. Values are clamped and the stick is cut to a length of 1.
  • Heading. heading is a world yaw in degrees, and speed is 0 to 1. The stick is steered to that heading relative to the view of the command it is applied to, so it stays correct while the view turns. Do not combine a heading with forward or side.
fieldtypemeaning
forward, sidenumberthe stick axes, -1 to 1, default 0
headingnumbera world yaw in degrees. Any finite number is accepted and wrapped.
speednumber0 to 1 with a heading, default 1
jump, duck, walkbooleanthe buttons, default false
modestring"add" (default), "set" or "idle", see below
ttlnumberhow many ticks the intent lives, 1 to 6, default 3
modewhat happens
"add"your stick is added to the player's, each axis clamped
"set"your stick replaces the player's, zero included, so the player holds still when you send zero
"idle"the whole intent applies only while the player's own stick is zero

ttl counts ticks of 16 ms, so an intent lives 16 to 96 ms. A fraction is cut off and other values are clamped.

How the calls behave

  • Refreshing. Call movement.intent every frame you want the keys. A script that stops calling, stops with an error, is unloaded, or sees the level change hands the keys back within 96 ms.
  • Allowance. A script may have 4 accepted movement.intent calls per frame. A refused call does not use the allowance. The intent table may hold at most 16 entries.
  • Errors. An argument that is not a table, a field that does not exist, a value of the wrong type, an unknown mode, or a heading mixed with forward or side raises an error with a fixed text.
  • Several scripts. The newest call wins. When it lapses, or its script goes, the newest intent of the others that is still alive takes over.
  • The ragebot. The ragebot keeps its priority: it moves the player itself when it stops or strafes. The player's own keys return as soon as the ttl runs out.
  • Timing. The intent is published on the render frame and applied on the next game command. That is about one frame (3 ms at 300 fps, 17 ms at 60) plus up to one tick behind what the script saw. It suits steps, stops, slides, crouch and hop helpers and steering the built-in strafe. It is not tick-exact.

Cost 40 instructions for movement.intent, 10 for movement.release.

Functions

movement.intent

movement.intent{ forward =, side =, heading =, speed =, jump =, duck =, walk =, mode =, ttl = } -> boolean

Returns true when the intent was taken. It returns false for a number that is not finite, and after 4 accepted calls in one frame.

events.on_paint(function()
  movement.intent{ forward = 1, ttl = 2 }
end)

movement.release

movement.release()

Gives the keys back now, without waiting for the ttl. It returns nothing.

Example: auto-peek

Walk one step out while a key is held, and walk back to the start when it is released. The script remembers where the player stood, then steers home with a heading until it is within 4 units.

local key = ui.keybind("peek key", "hold")
local home

events.on_paint(function()
  local me = entity.local_player()
  if not me or not me:is_alive() then return end

  if key:get() then
    if not home then
      local x, y = me:origin()
      home = { x = x, y = y }
    end
    movement.intent{ forward = 1, ttl = 2 }
  elseif home then
    local x, y = me:origin()
    local dx, dy = home.x - x, home.y - y
    if dx * dx + dy * dy < 16 then
      home = nil
      movement.release()
      return
    end
    movement.intent{ heading = math.deg(math.atan2(dy, dx)), mode = "set", ttl = 2 }
  end
end)

Example: slide stop

When the player lets go of the keys, push against the sliding direction. The "idle" mode keeps the helper out of the way while the player steers.

events.on_paint(function()
  local me = entity.local_player()
  if not me or not me:is_alive() then return end

  local vx, vy = me:velocity()
  if not vx or vx * vx + vy * vy < 100 then return end

  movement.intent{ heading = math.deg(math.atan2(-vy, -vx)), speed = 0.6, mode = "idle", ttl = 2 }
end)

Example: steer along a path

While a key is held, add a stick at a heading you choose to whatever the player is already pressing.

local path = ui.slider("path yaw", -180, 180, 90)
local on = ui.keybind("steer", "hold")

events.on_paint(function()
  if on:get() then
    movement.intent{ heading = path:get(), mode = "add", ttl = 2 }
  end
end)

previous: settings ids · next: visuals