DEMIENGINEDOCUMENTATION Everything is free
Lua API reference · Create

Vectors, seeded RNG, debug lines, and profiling.

Math & debug

Small helpers for moving numbers around and for seeing what your script thinks it is doing. Four modules: demi.math (vector and scalar math plus seeded randomness), demi.debug (on-screen lines), demi.profile (timing scopes), and the text helpers in demi.text and demi.regex.

Why a math module

Lua has no built-in vector type. The math services speak in Vec2/Vec3 numeric arrays — fixed-length tables indexed from 1 — and return single numbers for lengths and dot products, so results drop straight into specs and hit tables without repacking.

local Vector3 = require("demi.math.vector3")

local sum = Vector3.add({ 1, 2, 3 }, { 0.5, 0, 0 })
-- sum: { 1.5, 2, 3 } — a 3-number array

local len = Vector3.length({ 3, 4, 0 })            -- 5
local n = Vector3.normalized({ 0, 2, 0 })          -- { 0, 1, 0 } (unit length)
Call (vector2 / vector3)TakesReturns
addTwo vector arraysComponent-wise sum as one array
subtractTwo vector arraysComponent-wise difference as one array
scaleOne vector array plus one numberScaled vector array
lengthOne vector arraySingle number: the magnitude
normalizedOne vector arrayUnit-length vector array
dotTwo vector arraysSingle number: the dot product
crossTwo vector arrays (vector3 only)Cross product as one array
lerpTwo vector arrays and a t number 0…1Interpolated vector array

vector2 takes and returns two-number arrays; vector3 takes and returns three-number arrays. A 3D example: Vector3.add({ 1, 0, 0 }, { 0, 1, 0 }) returns { 1, 1, 0 }. Read arrays with v[1], v[2], v[3] (Lua indexes from 1).

local Vector3 = require("demi.math.vector3")
local Scalar = require("demi.math.scalar")

local t = Scalar.smoothstep(0.2, 0.8, 0.5)   -- eased 0…1, clamped to the range
local m = Vector3.lerp({ 0, 0, 0 }, { 10, 0, 0 }, t)
  • Scalar.smoothstep(edge0, edge1, x) → one number. Maps x into a smooth 0…1 ramp between the two edges, clamped outside them. Use it for eased fades and camera blends.
  • Vector3.lerp(a, b, t) → interpolated array; t = 0 is a, t = 1 is b.

Seeded randomness

Unseeded math.random cannot be reproduced. The random service keeps an explicit seed so replays and tests match.

local Random = require("demi.math.random")

Random.seed(20260926)           -- restart the sequence from a known seed
local snap = Random.state()     -- exact state string of the current position
print(Random.value())           -- float in [0, 1)
print(Random.range(-2, 2))      -- float between -2 and 2
print(Random.integer(1, 6))     -- integer in [1, 6] inclusive

Random.restore(snap)            -- rewind to a saved state
  • Random.seed(n) — starts a new sequence from integer n. Returns nothing.
  • Random.state() → exact unsigned 64-bit state string, safe to keep in JSON saves and pass to restore.
  • Random.restore(state) — rewinds to that state string. Returns true when the state was accepted.
  • Random.value() → float in [0, 1).
  • Random.range(min, max) → float between the two numbers.
  • Random.integer(min, max) → integer, both ends included.

Snapshots let you branch (“roll both outcomes and pick one”) and still continue the main sequence. See Replay & profiler for whole-run reproduction.

On-screen debug output

While iterating on Skyforge you often want information on screen without building UI. Debug output is temporary developer tooling, not player-facing HUD.

local Debug = require("demi.debug")

Debug.log("wave 3, enemies left: 2")   -- log channel (console/overlay)
Debug.line(0, 0, 4, 2, 1, 0, 0, 1)     -- draw a world-space segment this frame
Debug.clear_lines()                    -- wipe drawn segments
  • Debug.log(message) — records a message to the log. Returns nothing.
  • Debug.line(x1, y1, x2, y2, r?, g?, b?, a?, width?) — draws one line segment between two world points this frame; color channels and width are optional. Returns nothing.
  • Debug.clear_lines() — removes drawn segments. Returns nothing.

Profiling scopes

When a frame stutters, measure instead of guessing. A scope wraps a function and reports how long it took.

local Profile = require("demi.profile")

print(Profile.enabled())    -- true while profiling is active

Profile.scope("skyforge_enemy_ai", function()
    update_all_enemies()
end)
  • Profile.enabled() — function returning true while profiling is on (CLI: --profiler).
  • Profile.scope(name, fn) — runs fn and records its duration under name. Returns true when the scope ran and was recorded.

Aggregate and per-frame reports come from the CLI (--profile-report, --profile-frames); see Replay & profiler.

Text and patterns

Characters are not always one byte: an emoji or accented letter can be several. These helpers count and slice by user-visible characters (graphemes) so a player name is never cut in half.

local Text = require("demi.text")
local Regex = require("demi.regex")

print(Text.grapheme_count("Skýforge"))            -- 7 visible characters
local piece = Text.grapheme_slice("Skýforge", 1, 3)
-- piece: first 3 graphemes as a string (one-based start index)

local laid = Text.layout("Ready?", 120, 16)
-- laid: table with width, height, grapheme_count, truncated, lines

local rich = Text.parse_rich("[b]Ace[/b]")
-- rich: { text, spans, diagnostics } describing styled runs

print(Regex.is_valid("[0-9]+"))            -- true (pattern compiles)
print(Regex.matches("wave 3", "[0-9]+"))   -- true
  • Text.grapheme_count(s) → integer number of visible characters.
  • Text.grapheme_slice(s, first, count) → string of that many graphemes starting at the one-based first index, or nil when out of range.
  • Text.layout(s, width, font_size, max_lines?) → table with width, height, grapheme_count, truncated, valid_utf8, and per-line lines.
  • Text.parse_rich(s, strict?) → table with the plain text, styled spans, and diagnostics.
  • Regex.is_valid(pattern) → boolean, false for a pattern that does not compile.
  • Regex.matches(value, pattern, case_sensitive?) → boolean; the value comes first, the pattern second. Patterns use ECMAScript regular-expression syntax.

Next steps