docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s

Rebuild the API Reference around one page per symbol and richer, verified content.

Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
  instead of a single scrolling page; namespace overview pages (ns-screen …
  ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
  never hidden, plus a flash highlight on the scrolled-to target.

Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
  `Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).

Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
  the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
  page), all 22 annotations, namespace methods with parameter docs, builtins,
  the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
  descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
  delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
  world_query_next(property, cursor) arg order, event fields bind by name; dropped
  `when` and `module` (not in the self-hosted parser).

Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
  token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 17:53:22 +03:00
parent 25f987e30d
commit 3c7ec9b016
172 changed files with 5240 additions and 895 deletions

View file

@ -5,8 +5,34 @@ category: builtins
kind: builtin
tokens: abs
sig: abs(a) -> int
tip: Absolute value.
tip: The absolute value of an integer (its magnitude, never negative).
order: 8
---
Absolute value.
Returns the magnitude of an integer, dropping its sign — so <code>abs(-7)</code> and <code>abs(7)</code> both give <code>7</code>. It is handy for distance-style checks where direction does not matter, such as measuring how far two grid cells are apart along one axis before deciding whether something is "close enough". Combine two axis distances to build a simple proximity test. It takes a single integer and returns an integer.
Parameters:
- `a` — the integer whose magnitude you want
```ludic
program Proximity {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
model Enemy { Position }
handler SpawnActors phase Start {
spawn Player { Position { column: 5, row: 5 } }
spawn Enemy { Position { column: 8, row: 6 } }
}
handler CheckAdjacency phase Update {
for (PlayerPosition) in query [Position, {Player}] {
for (EnemyPosition) in query [Position, {Enemy}] {
let column_gap = abs(PlayerPosition.column - EnemyPosition.column)
let row_gap = abs(PlayerPosition.row - EnemyPosition.row)
if column_gap + row_gap <= 1 { print(1) }
}
}
}
}
```

View file

@ -1,12 +1,27 @@
---
id: fn-arg
name: arg_count / arg
name: arg
category: builtins
kind: builtin
tokens: arg_count arg
sig: arg_count() -> int arg(i) -> str
tip: The command line; argv[0] is included.
order: 20
tokens: arg
sig: arg(i) -> str
tip: The i-th command-line argument as a string.
order: 50
---
Read the process command line: <code>arg_count()</code> is the number of arguments (including the program name at index 0) and <code>arg(i)</code> returns the <code>i</code>th as a string.
Returns the command-line argument at index <code>i</code> as a string. Index <code>0</code> is conventionally the program's own name or path, so the first user-supplied argument is at index <code>1</code>. Use it together with <code>arg_count</code> to write configurable command-line tools in Ludic — reading an input filename, a mode flag, or a numeric option. Guard the index against <code>arg_count()</code> before reading so you never ask for an argument that was not passed.
Parameters:
- `i` — the argument index (`0` is the program name)
```ludic
program EchoFirstArg {
handler ShowArg phase Start {
if arg_count() > 1 {
print(arg(1))
} else {
print("no argument")
}
}
}
```

View file

@ -0,0 +1,23 @@
---
id: fn-arg_count
name: arg_count
category: builtins
kind: builtin
tokens: arg_count
sig: arg_count() -> int
tip: The number of command-line arguments, counting the program name.
order: 50
---
Returns how many command-line arguments the program received, including the program name at index <code>0</code>. So a program run with no user arguments reports <code>1</code>, and each added argument raises the count by one. Use it to validate that the required arguments were supplied and to bound the index you pass to <code>arg</code>. It takes no arguments and is the natural companion to <code>arg</code> when building command-line tools.
```ludic
program PrintAllArgs {
handler ListArgs phase Start {
print(arg_count())
for index in 0 .. arg_count() {
print(arg(index))
}
}
}
```

View file

@ -1,12 +1,29 @@
---
id: fn-bytes
name: bytes / words
name: bytes
category: builtins
kind: builtin
tokens: bytes words
sig: bytes(n) -> ptr / words(n) -> words
tip: Allocate a raw buffer of n bytes / n 32-bit words.
order: 11
tokens: bytes
sig: bytes(n) -> ptr
tip: Allocate a raw buffer of n bytes and return a pointer to it.
order: 50
---
Allocate a raw buffer of n bytes / n 32-bit words.
Allocates a raw, byte-addressable buffer of <code>n</code> bytes and returns a pointer to its start. It is a low-level primitive used for I/O and networking scratch space — for example a buffer to serialize the world into, or to receive bytes from a socket. Size it to what you need; a common pattern is <code>bytes(world_size())</code> so the buffer is exactly large enough for a full world snapshot. For a buffer of 32-bit words rather than individual bytes, use <code>words</code> instead.
Parameters:
- `n` — the number of bytes to allocate
```ludic
program SnapshotBuffer {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
handler Snapshot phase Start {
spawn Player { Position { column: 3, row: 4 } }
let buffer = bytes(world_size())
let written = world_save(buffer)
print(written)
}
}
```

View file

@ -5,8 +5,50 @@ category: builtins
kind: builtin
tokens: clamp
sig: clamp(v, lo, hi) -> int
tip: Constrain v to [lo, hi].
tip: Constrain a value to the inclusive range [lo, hi].
order: 9
---
Constrain v to [lo, hi].
Constrains a value to a range: it returns <code>lo</code> if <code>v</code> is below the range, <code>hi</code> if it is above, and <code>v</code> unchanged when it already lies within <code>[lo, hi]</code>. The classic use is keeping a moving object on screen or inside a grid, so its position can never run past the walls no matter how fast it moves. It saves you writing a pair of <code>if</code> checks by hand. Make sure <code>lo</code> is not greater than <code>hi</code>.
Parameters:
- `v` — the value to constrain
- `lo` — the lowest allowed value (inclusive)
- `hi` — the highest allowed value (inclusive)
```ludic
program ClampToGrid {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
const GRID_WIDTH: int = 20
const GRID_HEIGHT: int = 15
const TILE_SIZE: int = 16
handler SpawnPlayer phase Start {
spawn Player { Position { column: 5, row: 5 } }
}
handler ReadInput phase Input {
let pressed = Input.key()
for (Position) in query [Position, {Player}] {
if pressed == 'd' { Position.column = Position.column + 1 }
if pressed == 'a' { Position.column = Position.column - 1 }
Position.column = clamp(Position.column, 0, GRID_WIDTH - 1)
}
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
for (Position) in query [Position, {Player}] {
Screen.fill_rectangle(
x: Position.column * TILE_SIZE,
y: Position.row * TILE_SIZE,
width: TILE_SIZE,
height: TILE_SIZE,
color: Color.LimeGreen)
}
Screen.show()
}
}
```

View file

@ -5,8 +5,24 @@ category: builtins
kind: builtin
tokens: exit
sig: exit(code)
tip: Exit the process immediately with a status code.
tip: Terminate the process immediately with a status code.
order: 21
---
Terminate the process now with the given status code. Unlike <code>quit()</code>, which ends the game loop cleanly after the current frame, <code>exit</code> stops the program at once.
Terminates the process at once with the given status code, without waiting for the current frame to finish. This is a harder stop than <code>quit</code>, which ends the game loop cleanly after the frame completes; use <code>exit</code> for command-line tools and tests that need to signal success or failure to the shell, where <code>0</code> conventionally means success and any non-zero value means an error. Because it stops immediately, drawing queued for the current frame is not presented.
Parameters:
- `code` — the process exit status (`0` for success, non-zero for failure)
```ludic
program RequireArgument {
handler CheckArgs phase Start {
if arg_count() < 2 {
print(0)
exit(1)
}
print(1)
exit(0)
}
}
```

View file

@ -1,12 +0,0 @@
---
id: fn-file
name: file_write / file_stdout / file_stderr
category: builtins
kind: builtin
tokens: file_write file_stdout file_stderr
sig: file_stdout() -> ptr file_stderr() -> ptr file_write(handle, buf, len)
tip: Low-level output handles and raw writing.
order: 25
---
<code>file_stdout()</code> and <code>file_stderr()</code> return the standard stream handles; <code>file_write(handle, buf, len)</code> writes <code>len</code> raw bytes to one. Most code uses <code>print</code> instead — these are the primitive underneath.

View file

@ -0,0 +1,26 @@
---
id: fn-file_stderr
name: file_stderr
category: builtins
kind: builtin
tokens: file_stderr
sig: file_stderr() -> ptr
tip: The standard-error stream handle for use with file_write.
order: 50
---
Returns the handle for the standard-error stream, which you pass to <code>file_write</code> to emit raw bytes. Writing diagnostics and error messages here keeps them separate from normal program output on standard out, so a tool's real results are not mixed with its warnings. It takes no arguments and always refers to the process's standard error. Use it for the error side of command-line tools written in Ludic.
```ludic
program WarnToStderr {
handler CheckArgs phase Start {
if arg_count() < 2 {
let err = file_stderr()
let message = "missing argument\n"
file_write(err, message, len(message))
exit(1)
}
print("ok")
}
}
```

View file

@ -0,0 +1,22 @@
---
id: fn-file_stdout
name: file_stdout
category: builtins
kind: builtin
tokens: file_stdout
sig: file_stdout() -> ptr
tip: The standard-output stream handle for use with file_write.
order: 50
---
Returns the handle for the standard-output stream, which you pass to <code>file_write</code> to emit raw bytes. Unlike <code>print</code>, which always appends a newline and handles conversion, writing through this handle gives you exact control over what bytes go out and when — useful for tooling and code generators that must emit precise text. It takes no arguments and always refers to the process's standard output. Pair it with <code>file_stderr</code> when you want to separate normal output from diagnostics.
```ludic
program WriteLine {
handler Emit phase Start {
let out = file_stdout()
let message = "built\n"
file_write(out, message, len(message))
}
}
```

View file

@ -0,0 +1,29 @@
---
id: fn-file_write
name: file_write
category: builtins
kind: builtin
tokens: file_write
sig: file_write(handle, buf, len)
tip: Write a run of raw bytes to a stream handle.
order: 50
---
Writes <code>len</code> bytes from <code>buf</code> to the given stream <code>handle</code>. Get the handle from <code>file_stdout</code> or <code>file_stderr</code>; the buffer is typically a string (whose length you pass with <code>len(buf)</code>) or a raw buffer from <code>bytes</code>. This is the precise, no-frills output primitive that underpins Ludic's own tooling and code generators, where exact bytes matter and the automatic newline of <code>print</code> would get in the way. Pass a byte count that does not exceed the buffer's size.
Parameters:
- `handle` — a stream handle from `file_stdout` or `file_stderr`
- `buf` — the bytes to write (a string or a raw buffer)
- `len` — how many bytes to write
```ludic
program WriteTwice {
handler Emit phase Start {
let out = file_stdout()
let heading = "SCORE: "
let value = "250\n"
file_write(out, heading, len(heading))
file_write(out, value, len(value))
}
}
```

View file

@ -0,0 +1,29 @@
---
id: fn-flr
name: flr
category: builtins
kind: builtin
tokens: flr
sig: flr(x) -> int
tip: Floor a fixed-point value down to the nearest integer.
order: 50
---
Converts a <code>fixed</code> (Q16.16) value back to an <code>int</code> by discarding the fractional part, rounding toward negative infinity. It is the counterpart to <code>fx</code>: you accumulate motion in fixed-point for sub-pixel smoothness, then <code>flr</code> the result to get the whole-pixel column or row to draw at. Because it floors rather than rounds, <code>flr(fx(3) / fx(2))</code> is <code>1</code>, not <code>2</code>. Use it wherever a fixed value must become an integer coordinate, count, or index.
Parameters:
- `x` — the fixed-point value to floor
```ludic
program FixedToPixels {
const TILE_SIZE: int = 16
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
let smooth_column = fx(5) + fx(1) / fx(2)
let pixel_x = flr(smooth_column) * TILE_SIZE
Screen.fill_rectangle(x: pixel_x, y: 32, width: TILE_SIZE, height: TILE_SIZE, color: Color.LimeGreen)
Screen.show()
}
}
```

View file

@ -1,12 +0,0 @@
---
id: fn-font_load
name: font_load
category: builtins
kind: builtin
tokens: font_load
sig: font_load(path) -> int
tip: Load a TrueType font and return a handle for the UI.
order: 12
---
Load a TrueType font and return a handle for the UI.

View file

@ -1,12 +1,26 @@
---
id: fn-fx
name: fx / flr
name: fx
category: builtins
kind: builtin
tokens: fx flr
sig: fx(n) -> fixed / flr(x) -> int
tip: Convert between int and fixed-point.
order: 10
tokens: fx
sig: fx(n) -> fixed
tip: Lift an integer into a Q16.16 fixed-point value.
order: 50
---
Convert between int and fixed-point.
Converts an integer into a <code>fixed</code> value (Ludic's Q16.16 fixed-point type), so it can take part in fractional arithmetic. Ludic has no floating point; <code>fixed</code> is how you carry sub-pixel precision for smooth movement and physics-like accumulation. Use <code>fx</code> when you need to combine an <code>int</code> with fixed-point values or divide to get a fraction — for example <code>fx(1) / fx(4)</code> is <code>0.25</code>. Convert back to a whole number for drawing with <code>flr</code>.
Parameters:
- `n` — the integer to lift into fixed-point
```ludic
program SmoothAccumulate {
handler ComputeStep phase Start {
let full_speed = fx(3)
let half_speed = full_speed / fx(2)
let two_steps = half_speed + half_speed
print(flr(two_steps))
}
}
```

View file

@ -5,8 +5,24 @@ category: builtins
kind: builtin
tokens: getenv
sig: getenv(name) -> str
tip: Read an environment variable.
tip: Read an environment variable, returning empty when it is unset.
order: 23
---
Return the value of environment variable <code>name</code> (empty when it is unset).
Returns the value of the named environment variable, or an empty string when that variable is not set. Use it to make command-line tools and headless runs configurable without recompiling — reading a data directory, a log level, or a seed from the environment. Check for the empty string to detect an unset variable and fall back to a default. Like the other system builtins, it is aimed at tooling rather than a shipped game loop.
Parameters:
- `name` — the environment variable name to read
```ludic
program ReadConfig {
handler ShowConfig phase Start {
let level = getenv("LUDIC_LEVEL")
if len(level) == 0 {
print("default")
} else {
print(level)
}
}
}
```

View file

@ -5,8 +5,23 @@ category: builtins
kind: builtin
tokens: len
sig: len(x) -> int
tip: Length of a slice or string.
tip: The number of elements in a slice, or the byte length of a string.
order: 5
---
Length of a slice or string.
Returns the length of its argument: the element count of a slice, or the byte length of a string. Use it to iterate a slice with <code>for index in 0 .. len(items)</code>, to check whether a collection is empty, or to compute string slice bounds such as trimming a suffix. It reads the current length, so after you <code>push</code> onto a slice the value it returns grows accordingly. For strings, note it counts bytes, not visual characters.
Parameters:
- `x` — a slice or string to measure
```ludic
program CountEnemies {
handler ReportCount phase Start {
let wave_sizes = new []int
push(wave_sizes, 3)
push(wave_sizes, 5)
push(wave_sizes, 8)
print(len(wave_sizes))
}
}
```

View file

@ -5,8 +5,32 @@ category: builtins
kind: builtin
tokens: load
sig: load() -> bool
tip: Restore a snapshot written by save().
tip: Restore the world from a snapshot previously written by save().
order: 4
---
Restore a snapshot written by <code>save()</code>.
Restores the entire world from the snapshot most recently written by <code>save()</code> — every model instance, property, and program <code>var</code> returns to its saved state. It returns a <code>bool</code>: <code>true</code> when a snapshot existed and was restored, <code>false</code> when there was nothing to load, so you can guard the call and avoid clobbering the current world by mistake. Use it for quick-load, respawning at a checkpoint, or resetting a test to a known state. It takes no arguments.
```ludic
program QuickLoad {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
handler SpawnPlayer phase Start {
spawn Player { Position { column: 5, row: 5 } }
save()
}
handler ReadInput phase Input {
if Input.key() == 'r' {
let restored = load()
if restored { print(1) }
}
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.show()
}
}
```

View file

@ -0,0 +1,35 @@
---
id: fn-max
name: max
category: builtins
kind: builtin
tokens: max
sig: max(a, b) -> int
tip: The larger of two integers.
order: 50
---
Returns whichever of its two integer arguments is larger. Use it to enforce a lower bound (for example clamping a countdown so it never drops below zero with <code>max(remaining, 0)</code>), to track a running high score, or to pick the greater of two candidate values. For the smaller of two values use <code>min</code>, and to bound a value on both sides at once use <code>clamp</code>.
Parameters:
- `a` — the first value
- `b` — the second value
```ludic
program HighScore {
var score: int = 0
var best_score: int = 0
handler EarnPoints phase Update {
score = score + 5
best_score = max(best_score, score)
if score >= 20 { quit() }
}
handler ReportBest phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 8, y: 8, value: best_score, color: Color.Gold, scale: 1)
Screen.show()
}
}
```

View file

@ -1,12 +1,41 @@
---
id: fn-min
name: min / max
name: min
category: builtins
kind: builtin
tokens: min max
sig: min(a, b) / max(a, b) -> int
tip: The smaller / larger of two ints.
order: 7
tokens: min
sig: min(a, b) -> int
tip: The smaller of two integers.
order: 50
---
The smaller / larger of two ints.
Returns whichever of its two integer arguments is smaller. Use it to enforce an upper bound — for instance capping healing so it never exceeds a maximum with <code>min(current + heal, maximum)</code> — or to pick the lesser of two candidate values. For the larger of two values use <code>max</code>, and to bound a value between a low and a high at once use <code>clamp</code>.
Parameters:
- `a` — the first value
- `b` — the second value
```ludic
program CappedHealing {
property Health { current: int = 100, maximum: int = 100 }
model Player { Health }
handler SpawnPlayer phase Start {
spawn Player { Health { current: 90, maximum: 100 } }
}
handler ApplyHeal phase Update {
for (Health) in query [Health, {Player}] {
Health.current = min(Health.current + 25, Health.maximum)
}
}
handler ReportHealth phase Render {
Screen.clear(Color.MidnightBlue)
for (Health) in query [Health, {Player}] {
Screen.draw_number(x: 8, y: 8, value: Health.current, color: Color.LimeGreen, scale: 1)
}
Screen.show()
}
}
```

View file

@ -5,8 +5,23 @@ category: builtins
kind: builtin
tokens: print
sig: print(x)
tip: Print an int or string, followed by a newline — for headless tests and debugging.
tip: Print an int or string followed by a newline — for headless tests and debugging.
order: 0
---
Print an int or string, followed by a newline — for headless tests and debugging.
Writes its argument to standard output followed by a newline. It accepts either an integer or a string, so it is the go-to tool for quick debugging and for headless test programs that emit numbers a test harness can check. It is a diagnostic channel, separate from anything drawn on screen with the <code>Screen</code> API, and is most useful in <code>Start</code>- or <code>Update</code>-phase handlers while you are iterating. For interpolated messages, build the string first with a backtick template and pass that.
Parameters:
- `x` — the value to print, an `int` or a string
```ludic
program PrintScore {
var score: int = 0
handler CountUp phase Update {
score = score + 10
print(score)
if score >= 30 { quit() }
}
}
```

View file

@ -5,8 +5,25 @@ category: builtins
kind: builtin
tokens: push
sig: push(slice, x)
tip: Append to a slice.
tip: Append one element to the end of a growable slice.
order: 6
---
Append to a slice.
Appends a value to the end of a slice, growing it by one element. Create the slice first with <code>new []T</code>, then <code>push</code> items onto it; afterward <code>len</code> reflects the new count and the element is reachable by index. Use it to build up lists at runtime — a queue of spawn requests, collected scores, parsed tokens — where you don't know the size in advance. The value's type must match the slice's element type.
Parameters:
- `slice` — the growable slice to append to
- `x` — the element to append
```ludic
program BuildWaveList {
handler PlanWaves phase Start {
let wave_sizes = new []int
push(wave_sizes, 4)
push(wave_sizes, 6)
for index in 0 .. len(wave_sizes) {
print(wave_sizes[index])
}
}
}
```

View file

@ -5,8 +5,22 @@ category: builtins
kind: builtin
tokens: quit
sig: quit()
tip: Stop the game loop after this frame.
tip: Stop the game loop cleanly after the current frame finishes.
order: 2
---
Stop the game loop after this frame.
Signals the runtime to stop the game loop after the current frame completes, ending the program in an orderly way. Because it lets the in-progress frame finish, any drawing you have already queued for this frame is still presented. Use it for a "quit to desktop" action, to end a headless test once its work is done, or to stop on a win/lose condition. For an immediate, mid-frame stop with a status code instead, use <code>exit</code>. It takes no arguments.
```ludic
program QuitOnKey {
handler ReadInput phase Input {
if Input.key() == 'q' { quit() }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_text(x: 8, y: 8, text: "PRESS Q TO QUIT", color: Color.White, scale: 1)
Screen.show()
}
}
```

View file

@ -5,8 +5,22 @@ category: builtins
kind: builtin
tokens: read_char
sig: read_char() -> int
tip: Read one byte from standard input (-1 at end of input).
tip: Read one byte from standard input, or -1 at end of input.
order: 24
---
Read a single byte from standard input, returning its value or <code>-1</code> at end of input — the basis for headless, pipe-driven runs.
Reads a single byte from standard input and returns its value, or <code>-1</code> when the input has ended. It is the foundation for headless, pipe-driven runs: a test or tool can feed the program a stream of bytes and process them one at a time, looping until <code>read_char</code> returns <code>-1</code>. Compare the byte against character literals such as <code>'a'</code> to interpret it. It takes no arguments and advances through the input on each call.
```ludic
program CountInputBytes {
handler DrainInput phase Start {
var byte_count: int = 0
var next_byte = read_char()
while next_byte != -1 {
byte_count = byte_count + 1
next_byte = read_char()
}
print(byte_count)
}
}
```

View file

@ -5,8 +5,20 @@ category: builtins
kind: builtin
tokens: run
sig: run(cmd)
tip: Run a shell command.
tip: Run a string as a shell command.
order: 22
---
Run <code>cmd</code> as a shell command — handy for build steps and tooling written in Ludic.
Executes its string argument as a shell command on the host. It exists so build steps and developer tooling can be written in Ludic itself — invoking the compiler, moving files, or chaining tools — rather than in a separate shell script. It is a tooling and scripting facility, not something a shipped game loop would normally use. Treat the command string carefully, since it runs with the same privileges as the program.
Parameters:
- `cmd` — the shell command line to run
```ludic
program BuildStep {
handler RunTool phase Start {
run("mkdir -p build")
run("echo built")
}
}
```

View file

@ -5,8 +5,30 @@ category: builtins
kind: builtin
tokens: save
sig: save()
tip: Serialize the entire world — every entity, property and program var — to a snapshot in one call.
tip: Serialize the whole world — every model instance, property, and program var — in one call.
order: 3
---
Serialize the entire world — every entity, property and program <code>var</code> — to a snapshot in one call.
Captures a complete snapshot of the running world in a single call: every spawned model instance and its properties, plus every program-level <code>var</code>. The compiler generates the serialization for you from your declarations, so you never write it by hand. Pair it with <code>load</code> to implement quick-save / quick-load, checkpoints, or deterministic test fixtures. It takes no arguments and writes to the runtime's snapshot slot; calling it again overwrites the previous snapshot.
```ludic
program QuickSave {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
handler SpawnPlayer phase Start {
spawn Player { Position { column: 5, row: 5 } }
}
handler ReadInput phase Input {
let pressed = Input.key()
if pressed == 's' { save() }
if pressed == 'l' { load() }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.show()
}
}
```

View file

@ -5,8 +5,22 @@ category: builtins
kind: builtin
tokens: str
sig: str(x) -> str
tip: Convert an int/bool/fixed to text; a string passes through.
tip: Convert an int, bool, or fixed value to text; a string passes through unchanged.
order: 1
---
Convert an int/bool/fixed to text; a string passes through. Also used by <code>`{…}`</code> interpolation.
Converts a value to its textual form: an <code>int</code>, <code>bool</code>, or <code>fixed</code> becomes a string, and a value that is already a string is returned unchanged. This is what backtick <code>`{…}`</code> interpolation calls under the hood, so most of the time you can interpolate directly instead of calling <code>str</code> yourself. Reach for the explicit form when you need to store or pass around the text, or build a string in pieces. The result can be printed, drawn with <code>Screen.draw_text</code>, or concatenated.
Parameters:
- `x` — the value to convert to text
```ludic
program LabelValue {
var score: int = 250
handler ReportScore phase Start {
let label = str(score)
print(label)
}
}
```

View file

@ -0,0 +1,43 @@
---
id: fn-ui_build
name: ui_build
category: builtins
kind: builtin
tokens: ui_build
sig: ui_build()
tip: Build the declared UI tree so it can be opened and rendered.
order: 50
---
Constructs the retained-UI tree you declared in a <code>ui</code> block, laying out its panels, labels, and buttons and loading any skins or images they reference. Call it once during a <code>Start</code>-phase handler, after loading any fonts the UI needs, and before you open a screen with <code>ui_open</code> or draw it with <code>ui_render</code>. Because the UI is declared as data and built in one step, the game code only has to build it, open it, and read clicks. It takes no arguments.
```ludic
program TitleMenu {
var title_font: int = 0
ui MainMenu {
panel id: Root w: 240 pad: 16 gap: 6 bg: 0x1a1a2c align: center {
label text: "CHRONORIFT" font: title_font size: 24 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 200
button id: Quit text: "Quit" font: title_font size: 16 w: 200
}
}
handler Boot phase Start {
title_font = font_load("/System/Library/Fonts/Supplemental/Arial.ttf")
ui_build()
ui_open(UI_MainMenu)
}
handler Navigate phase Update {
ui_tick(Input.key())
if ui_clicked(UI_Quit) { quit() }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
ui_render()
Screen.show()
}
}
```

View file

@ -0,0 +1,31 @@
---
id: fn-words
name: words
category: builtins
kind: builtin
tokens: words
sig: words(n) -> words
tip: Allocate a raw buffer of n 32-bit words, indexable with [i].
order: 50
---
Allocates a raw buffer of <code>n</code> 32-bit words and returns a <code>words</code> value you can index with <code>buffer[i]</code> to read or write each word as an <code>int</code>. Use it when you want a flat, fixed-size numeric scratch array — a lookup table, a small ring buffer, or per-slot counters — without declaring a model. It is lower-level than a growable <code>[]int</code> slice: the size is chosen up front and there is no <code>push</code>. For byte-granular storage use <code>bytes</code> instead.
Parameters:
- `n` — the number of 32-bit words to allocate
```ludic
program WordScratch {
handler SumScores phase Start {
let scores = words(3)
scores[0] = 10
scores[1] = 20
scores[2] = 30
var total: int = 0
for index in 0 .. 3 {
total = total + scores[index]
}
print(total)
}
}
```