feat(stdlib): namespaced standard library (issue #2)

Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2):
156 namespace methods across Math, Text, List, Ease, Collide, World, Net,
Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time.
All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds).

Compiler (selfhost/):
- Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude —
  bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus
  hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/
  move_toward/smoothstep/lerp/remap/sign/floor/ceil/round.
- Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace,
  and the libc-backed queries.
- List.* (complete): insert/remove_at/remove/sort plus the earlier ops.
- Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/
  circles/rect_circle).
- Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte-
  identical IR) and Mem.* (bytes/words/copy/fill/peek/poke).
- Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new
  runtime primitives; sprite/sprite_scaled aliases), Color.* functions,
  Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new
  game-loop frame counter).
- Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed.

Docs & tooling:
- 129 new per-symbol doc pages; gen.py made data-driven (namespaces
  discovered from the docs, no hardcoded list); new check-impl.py enforces
  that every implemented namespace method / keyword / type / phase has a
  doc page, wired into `x test-tools`. Document the previously-undocumented
  keywords (break/continue/where/entry/new/public + and/or/not tokens).
- LSP: namespaced signature help (ns_method_sig) covering every namespace.

Tests: 12 new self-host/regression tests + a golden render for the drawing
primitives. All suites green (selfhost 21, regression 45, tools 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 00:26:19 +03:00
parent ff15c4e01d
commit a38195128f
235 changed files with 24676 additions and 7762 deletions

View file

@ -0,0 +1,7 @@
---
id: math
title: Math
order: 6
---
Deterministic fixed-point math. Every function is computed in Q16.16 with plain integer arithmetic, so results are bit-identical on every platform and every run — the same guarantee the rest of the runtime gives. Arguments are positional.

View file

@ -0,0 +1,28 @@
---
id: math-abs
name: Math.abs
category: math
kind: namespace-method
tokens: Math.abs
sig: Math.abs(x) -> int
tip: The magnitude of a value, dropping its sign.
order: 2
ns: Math
member: abs
---
Returns the absolute value of <code>x</code> — its distance from zero, always non-negative. It preserves the type of its argument, so <code>Math.abs</code> of a <code>fixed</code> stays <code>fixed</code>. Reach for it when you care about how far apart two things are regardless of direction, such as the gap between a target and current position, or the speed behind a signed velocity. The bare form <code>abs(x)</code> is kept as a convenience alias.
Parameters:
- `x` — the value whose magnitude you want
```ludic
program ChaseDistance {
var target: int = 40
var here: int = 12
handler Report phase Update {
let gap = Math.abs(target - here) # 28, regardless of which is larger
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-acos
name: Math.acos
category: math
kind: namespace-method
tokens: Math.acos
sig: Math.acos(x) -> fixed
tip: The arccosine of a value, in radians.
order: 28
ns: Math
member: acos
---
Returns the angle in radians whose cosine is <code>x</code>, in <code>0</code>..<code>pi</code>. The input should be in <code>-1.0</code>..<code>1.0</code>. Handy for recovering the angle between two normalized directions from their dot product.
```ludic
program Demo {
handler Step phase Update {
let between = Math.acos(dot)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-asin
name: Math.asin
category: math
kind: namespace-method
tokens: Math.asin
sig: Math.asin(x) -> fixed
tip: The arcsine of a value, in radians.
order: 27
ns: Math
member: asin
---
Returns the angle in radians whose sine is <code>x</code>, in <code>-pi/2</code>..<code>pi/2</code>. The input should be in <code>-1.0</code>..<code>1.0</code>. Built from <code>Math.atan2</code> and <code>Math.sqrt</code>, so it is deterministic.
```ludic
program Demo {
handler Step phase Update {
let angle = Math.asin(height / radius)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-atan2
name: Math.atan2
category: math
kind: namespace-method
tokens: Math.atan2
sig: Math.atan2(y, x) -> fixed
tip: The angle of the vector (x, y), in radians.
order: 26
ns: Math
member: atan2
---
Returns the angle in radians from the positive x-axis to the point <code>(x, y)</code>, in the full range <code>-pi</code>..<code>pi</code> — the standard way to get a heading from a direction vector. Note the argument order is <code>y</code> then <code>x</code>. Deterministic fixed-point, accurate to within about a degree.
```ludic
program Demo {
handler Step phase Update {
let heading = Math.atan2(target_y - y, target_x - x)
}
}
```

View file

@ -0,0 +1,29 @@
---
id: math-ceil
name: Math.ceil
category: math
kind: namespace-method
tokens: Math.ceil
sig: Math.ceil(x) -> int
tip: Round a fixed value up to the nearest whole int.
order: 6
ns: Math
member: ceil
---
Takes a <code>fixed</code> value and returns the smallest <code>int</code> not less than it — rounding toward positive infinity, so <code>Math.ceil(2.1)</code> is <code>3</code> and <code>Math.ceil(4.0)</code> stays <code>4</code>. Use it when you need to cover a fractional amount with whole units: how many full tiles a span touches, or how many rows a variable-height list needs.
Parameters:
- `x` — the `fixed` value to round up
```ludic
program RowsNeeded {
const ROW_HEIGHT: fixed = 12.0
var content_height: fixed = 30.0
handler Layout phase Update {
let rows = Math.ceil(content_height / ROW_HEIGHT) # 3
}
}
```

View file

@ -0,0 +1,31 @@
---
id: math-clamp
name: Math.clamp
category: math
kind: namespace-method
tokens: Math.clamp
sig: Math.clamp(v, lo, hi) -> int
tip: Constrain a value to the range [lo, hi].
order: 3
ns: Math
member: clamp
---
Returns <code>v</code> held inside the inclusive range <code>lo</code>..<code>hi</code>: below <code>lo</code> it returns <code>lo</code>, above <code>hi</code> it returns <code>hi</code>, otherwise <code>v</code> unchanged. It is the single-call form of <code>Math.max(lo, Math.min(v, hi))</code> and works for <code>int</code> and <code>fixed</code> alike. Use it to keep a camera inside the world, a slider within its track, or a stat within its legal bounds. The bare form <code>clamp(v, lo, hi)</code> is kept as a convenience alias.
Parameters:
- `v` — the value to constrain
- `lo` — the lowest allowed result (inclusive)
- `hi` — the highest allowed result (inclusive)
```ludic
program CameraBounds {
const WORLD_WIDTH: int = 320
var camera_x: int = 0
handler Follow phase Update {
camera_x = Math.clamp(camera_x, 0, WORLD_WIDTH - 1)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-cos
name: Math.cos
category: math
kind: namespace-method
tokens: Math.cos
sig: Math.cos(radians) -> fixed
tip: Cosine of an angle in radians.
order: 13
ns: Math
member: cos
---
Returns the cosine of <code>radians</code> as a <code>fixed</code> in <code>-1.0</code>..<code>1.0</code>, computed as <code>Math.sin(radians + pi/2)</code> off the same deterministic table. Use <code>Math.cos</code> and <code>Math.sin</code> together to convert an angle into an <code>(x, y)</code> direction — for orbits, aiming, or steering.
```ludic
program Demo {
handler Step phase Update {
let x = center_x + Math.cos(angle) * radius
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-deg_to_rad
name: Math.deg_to_rad
category: math
kind: namespace-method
tokens: Math.deg_to_rad
sig: Math.deg_to_rad(degrees) -> fixed
tip: Convert degrees to radians.
order: 18
ns: Math
member: deg_to_rad
---
Converts an angle from degrees to radians (multiplying by <code>pi/180</code>), since <code>Math.sin</code>/<code>Math.cos</code>/<code>Math.tan</code> all take radians. Use it when your data or design speaks in degrees — a 90-degree turn, a 45-degree cone — and you need to feed it to the trig functions.
```ludic
program Demo {
handler Step phase Update {
let r = Math.sin(Math.deg_to_rad(90.0)) # ~1.0
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-dist
name: Math.dist
category: math
kind: namespace-method
tokens: Math.dist
sig: Math.dist(x0, y0, x1, y1) -> fixed
tip: Distance between two points.
order: 16
ns: Math
member: dist
---
Returns the straight-line distance between <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>sqrt(dx*dx + dy*dy)</code> in fixed-point. Use it for proximity and range checks. When you only need to compare distances (which is nearer? within radius?), prefer <code>Math.dist2</code>, which skips the square root.
```ludic
program Demo {
handler Step phase Update {
if Math.dist(px, py, ex, ey) < attack_range { strike() }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: math-dist2
name: Math.dist2
category: math
kind: namespace-method
tokens: Math.dist2
sig: Math.dist2(x0, y0, x1, y1) -> fixed
tip: Squared distance between two points.
order: 17
ns: Math
member: dist2
---
Returns the squared distance between <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>dx*dx + dy*dy</code>, with no square root. Comparing squared distances gives the same ordering as comparing real distances, so this is the cheaper choice for "nearest" and "within radius" tests: compare against <code>radius * radius</code> instead of calling <code>Math.dist</code>.
```ludic
program Demo {
handler Step phase Update {
let r2 = radius * radius
if Math.dist2(px, py, ex, ey) < r2 { in_range() }
}
}
```

View file

@ -0,0 +1,29 @@
---
id: math-floor
name: Math.floor
category: math
kind: namespace-method
tokens: Math.floor
sig: Math.floor(x) -> int
tip: Round a fixed value down to the nearest whole int.
order: 5
ns: Math
member: floor
---
Takes a <code>fixed</code> value and returns the largest <code>int</code> not greater than it — rounding toward negative infinity, so <code>Math.floor(2.7)</code> is <code>2</code> and <code>Math.floor(-0.2)</code> is <code>-1</code>. This is the fixed-to-int conversion you reach for when turning a smooth position into a whole tile or pixel index. It is the same operation as the bare <code>flr(x)</code>.
Parameters:
- `x` — the `fixed` value to round down
```ludic
program TileIndex {
const TILE_SIZE: fixed = 16.0
var world_x: fixed = 40.5
handler Locate phase Update {
let column = Math.floor(world_x / TILE_SIZE) # 2
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-hypot
name: Math.hypot
category: math
kind: namespace-method
tokens: Math.hypot
sig: Math.hypot(x, y) -> fixed
tip: Length of the vector (x, y).
order: 15
ns: Math
member: hypot
---
Returns <code>sqrt(x*x + y*y)</code> — the length of the 2D vector <code>(x, y)</code>, or equivalently the hypotenuse of a right triangle with those legs. It is the one-call form for a magnitude and reads more clearly than spelling out the squares and root. To normalize a vector, divide each component by its <code>Math.hypot</code>.
```ludic
program Demo {
handler Step phase Update {
let length = Math.hypot(velocity_x, velocity_y)
}
}
```

View file

@ -0,0 +1,31 @@
---
id: math-inverse_lerp
name: Math.inverse_lerp
category: math
kind: namespace-method
tokens: Math.inverse_lerp
sig: Math.inverse_lerp(a, b, v) -> fixed
tip: Find where a value sits between two endpoints as a 0..1 fraction.
order: 9
ns: Math
member: inverse_lerp
---
The inverse of <code>Math.lerp</code>: given endpoints <code>a</code> and <code>b</code> and a value <code>v</code>, it returns the fraction <code>(v - a) / (b - a)</code> — <code>0.0</code> when <code>v</code> equals <code>a</code>, <code>1.0</code> when it equals <code>b</code>. Use it to turn a raw quantity into a normalized amount: how full a health bar is, how far a timer has run, or the <code>t</code> to feed into another <code>Math.lerp</code>. The endpoints must differ, since equal ones would divide by zero.
Parameters:
- `a` — the endpoint that maps to `0.0`
- `b` — the endpoint that maps to `1.0`
- `v` — the value to locate between them
```ludic
program HealthFraction {
const MAX_HP: fixed = 200.0
var hp: fixed = 150.0
handler Draw phase Render {
let fraction = Math.inverse_lerp(0.0, MAX_HP, hp) # 0.75
}
}
```

View file

@ -0,0 +1,30 @@
---
id: math-lerp
name: Math.lerp
category: math
kind: namespace-method
tokens: Math.lerp
sig: Math.lerp(a, b, t) -> fixed
tip: Blend between two values by a 0..1 amount.
order: 8
ns: Math
member: lerp
---
Linear interpolation: returns <code>a</code> when <code>t</code> is <code>0.0</code>, <code>b</code> when <code>t</code> is <code>1.0</code>, and a proportional blend in between — the exact value <code>a + (b - a) * t</code>. All three arguments are <code>fixed</code> and the result is <code>fixed</code>. It is the workhorse behind smooth motion: ease a camera toward a target, fade a value over time, or sample a gradient. Values of <code>t</code> outside <code>0..1</code> extrapolate past the endpoints.
Parameters:
- `a` — the value returned at `t = 0.0`
- `b` — the value returned at `t = 1.0`
- `t` — the blend amount, normally `0.0`..`1.0`
```ludic
program EaseCamera {
var camera_x: fixed = 0.0
var target_x: fixed = 100.0
handler Follow phase Update {
camera_x = Math.lerp(camera_x, target_x, 0.1) # glides a tenth of the way each frame
}
}
```

View file

@ -0,0 +1,28 @@
---
id: math-max
name: Math.max
category: math
kind: namespace-method
tokens: Math.max
sig: Math.max(a, b) -> int
tip: The larger of two values.
order: 1
ns: Math
member: max
---
Returns whichever of <code>a</code> and <code>b</code> is larger. Like <code>Math.min</code>, it works for both <code>int</code> and <code>fixed</code> operands. Use it to hold a value at a floor — for example keeping a countdown at zero instead of going negative, or a health bar from dropping below the minimum. The bare form <code>max(a, b)</code> is kept as a convenience alias.
Parameters:
- `a` — the first value
- `b` — the second value
```ludic
program Countdown {
var timer: int = 60
handler Tick phase Update {
timer = Math.max(timer - 1, 0) # stops at 0, never negative
}
}
```

View file

@ -0,0 +1,28 @@
---
id: math-min
name: Math.min
category: math
kind: namespace-method
tokens: Math.min
sig: Math.min(a, b) -> int
tip: The smaller of two values.
order: 0
ns: Math
member: min
---
Returns whichever of <code>a</code> and <code>b</code> is smaller. It works on plain <code>int</code> values and on <code>fixed</code> values alike — both are stored as signed 32-bit words, so the comparison keeps their order either way. Use it to cap a value at a ceiling, keep a cursor inside a list, or take the nearer of two distances. The bare form <code>min(a, b)</code> is kept as a convenience alias.
Parameters:
- `a` — the first value
- `b` — the second value
```ludic
program ClampScore {
var score: int = 0
handler AddPoint phase Update {
score = Math.min(score + 1, 100) # never climbs past 100
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-move_toward
name: Math.move_toward
category: math
kind: namespace-method
tokens: Math.move_toward
sig: Math.move_toward(from, to, delta) -> fixed
tip: Step from one value toward another by at most delta.
order: 24
ns: Math
member: move_toward
---
Moves <code>from</code> toward <code>to</code> by at most <code>delta</code>, never overshooting — when the gap is smaller than <code>delta</code> it returns exactly <code>to</code>. Unlike <code>Math.lerp</code>, which eases by a fraction of the remaining distance, this moves at a constant rate, which is what you want for turrets, meters, and UI values that should approach a target steadily and then stop.
```ludic
program Demo {
handler Step phase Update {
health_shown = Math.move_toward(health_shown, health, 2.0)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-ping_pong
name: Math.ping_pong
category: math
kind: namespace-method
tokens: Math.ping_pong
sig: Math.ping_pong(t, len) -> int
tip: Bounce a counter back and forth in [0, len].
order: 22
ns: Math
member: ping_pong
---
Maps an ever-increasing counter <code>t</code> to a value that rises from <code>0</code> to <code>len</code> and back, bouncing forever — the triangle wave to <code>Math.wrap</code>'s sawtooth. Feed it a frame counter to drive a patrol, a pulsing highlight, or a back-and-forth sweep without tracking direction yourself.
```ludic
program Demo {
handler Step phase Update {
let phase = Math.ping_pong(frame, 30)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-posmod
name: Math.posmod
category: math
kind: namespace-method
tokens: Math.posmod
sig: Math.posmod(a, m) -> int
tip: Modulo that is always non-negative.
order: 20
ns: Math
member: posmod
---
Returns <code>a</code> modulo <code>m</code>, but always in the range <code>0</code>..<code>m-1</code> — unlike the <code>%</code> operator, which keeps the sign of <code>a</code> (so <code>-1 % 5</code> is <code>-1</code>, while <code>Math.posmod(-1, 5)</code> is <code>4</code>). It is what you want for wrapping indices and grid coordinates that can go negative.
```ludic
program Demo {
handler Step phase Update {
let col = Math.posmod(x, grid_width) # never negative
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-rad_to_deg
name: Math.rad_to_deg
category: math
kind: namespace-method
tokens: Math.rad_to_deg
sig: Math.rad_to_deg(radians) -> fixed
tip: Convert radians to degrees.
order: 19
ns: Math
member: rad_to_deg
---
Converts an angle from radians to degrees (multiplying by <code>180/pi</code>) — the inverse of <code>Math.deg_to_rad</code>. Use it to present an internally-radians angle in the degrees a player or designer expects, for a HUD readout or a debug overlay.
```ludic
program Demo {
handler Step phase Update {
let heading_deg = Math.rad_to_deg(heading)
}
}
```

View file

@ -0,0 +1,34 @@
---
id: math-remap
name: Math.remap
category: math
kind: namespace-method
tokens: Math.remap
sig: Math.remap(v, in0, in1, out0, out1) -> fixed
tip: Rescale a value from one range into another.
order: 10
ns: Math
member: remap
---
Maps <code>v</code> from the input range <code>in0</code>..<code>in1</code> onto the output range <code>out0</code>..<code>out1</code>, keeping its relative position — equivalent to <code>Math.lerp(out0, out1, Math.inverse_lerp(in0, in1, v))</code>. All arguments are <code>fixed</code>. It is the one call that converts between units: a health value into a bar width, a noise sample in <code>-1..1</code> into a <code>0..255</code> shade, or a mouse position into a world coordinate. Swap the output ends to invert the mapping.
Parameters:
- `v` — the value to rescale
- `in0` — the start of the input range
- `in1` — the end of the input range
- `out0` — the start of the output range
- `out1` — the end of the output range
```ludic
program HealthBar {
const MAX_HP: fixed = 200.0
const BAR_WIDTH: fixed = 64.0
var hp: fixed = 150.0
handler Draw phase Render {
let width = Math.remap(hp, 0.0, MAX_HP, 0.0, BAR_WIDTH) # 48.0
}
}
```

View file

@ -0,0 +1,27 @@
---
id: math-round
name: Math.round
category: math
kind: namespace-method
tokens: Math.round
sig: Math.round(x) -> int
tip: Round a fixed value to the nearest whole int.
order: 7
ns: Math
member: round
---
Takes a <code>fixed</code> value and returns the nearest <code>int</code>, with halves rounding up: <code>Math.round(2.5)</code> is <code>3</code> and <code>Math.round(2.4)</code> is <code>2</code>. Choose it over <code>Math.floor</code> when you want the closest whole value rather than always the lower one — snapping a smoothly-moving sprite to a pixel, or reporting a fractional total as a clean number.
Parameters:
- `x` — the `fixed` value to round to nearest
```ludic
program SnapToPixel {
var smooth_x: fixed = 10.5
handler Draw phase Render {
let px = Math.round(smooth_x) # 11
}
}
```

View file

@ -0,0 +1,28 @@
---
id: math-sign
name: Math.sign
category: math
kind: namespace-method
tokens: Math.sign
sig: Math.sign(x) -> int
tip: The sign of a value as -1, 0, or 1.
order: 4
ns: Math
member: sign
---
Returns <code>-1</code> when <code>x</code> is negative, <code>1</code> when it is positive, and <code>0</code> when it is exactly zero. It works on both <code>int</code> and <code>fixed</code> values, since zero and sign are the same in Q16.16. Multiply a step by <code>Math.sign(dx)</code> to move one unit toward a target, or read the sign of a velocity to decide which way a sprite faces.
Parameters:
- `x` — the value to test
```ludic
program FaceTarget {
var x: int = 8
var target: int = 20
handler StepToward phase Update {
x = x + Math.sign(target - x) # +1 while target is to the right
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-sin
name: Math.sin
category: math
kind: namespace-method
tokens: Math.sin
sig: Math.sin(radians) -> fixed
tip: Sine of an angle in radians.
order: 12
ns: Math
member: sin
---
Returns the sine of <code>radians</code> as a <code>fixed</code> in <code>-1.0</code>..<code>1.0</code>. It is table-driven (a 256-entry Q16.16 sine table with linear interpolation) so it is fast and fully deterministic. Angles wrap, so any value works — no need to reduce into a range first. Pair it with <code>Math.cos</code> to turn an angle into a direction.
```ludic
program Demo {
handler Step phase Update {
let y = center_y + Math.sin(angle) * radius
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-smoothstep
name: Math.smoothstep
category: math
kind: namespace-method
tokens: Math.smoothstep
sig: Math.smoothstep(e0, e1, x) -> fixed
tip: A smooth 0..1 ramp between two edges.
order: 25
ns: Math
member: smoothstep
---
Returns <code>0.0</code> when <code>x</code> is at or below <code>e0</code>, <code>1.0</code> at or above <code>e1</code>, and a smooth S-curve (ease-in and ease-out) in between — the classic <code>t*t*(3 - 2t)</code> Hermite ramp. Use it for fades, dissolves, and easing a normalized amount before feeding it to <code>Math.lerp</code>, when a straight linear ramp looks too abrupt.
```ludic
program Demo {
handler Step phase Update {
let a = Math.smoothstep(0.0, 1.0, fade_t)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-snapped
name: Math.snapped
category: math
kind: namespace-method
tokens: Math.snapped
sig: Math.snapped(v, step) -> fixed
tip: Round a value to the nearest multiple of step.
order: 23
ns: Math
member: snapped
---
Rounds <code>v</code> to the nearest multiple of <code>step</code> (both <code>fixed</code>) — for snapping a position to a grid, quantizing an angle to 8 directions, or rounding a value to a tidy increment. <code>Math.snapped(x, 16.0)</code> snaps <code>x</code> to a 16-unit grid.
```ludic
program Demo {
handler Step phase Update {
let gx = Math.snapped(world_x, 16.0)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-sqrt
name: Math.sqrt
category: math
kind: namespace-method
tokens: Math.sqrt
sig: Math.sqrt(x) -> fixed
tip: The square root of a fixed value.
order: 11
ns: Math
member: sqrt
---
Returns the non-negative square root of <code>x</code>, computed in Q16.16 by a deterministic integer algorithm — so it is bit-identical on every platform, the same guarantee the rest of the runtime gives. Negative inputs return <code>0</code>. Use it for lengths and distances; when you have two legs of a right triangle reach for <code>Math.hypot</code>, and when you only need to compare magnitudes prefer the cheaper squared distance <code>Math.dist2</code>.
```ludic
program Demo {
handler Step phase Update {
let speed = Math.sqrt(vx * vx + vy * vy)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-tan
name: Math.tan
category: math
kind: namespace-method
tokens: Math.tan
sig: Math.tan(radians) -> fixed
tip: Tangent of an angle in radians.
order: 14
ns: Math
member: tan
---
Returns the tangent of <code>radians</code>, computed as <code>Math.sin(radians) / Math.cos(radians)</code> in fixed-point. Near odd multiples of <code>pi/2</code> the cosine approaches zero and the result grows without bound, so guard those angles. For most gameplay, <code>Math.sin</code>/<code>Math.cos</code> are what you want; <code>tan</code> is here for slopes and field-of-view math.
```ludic
program Demo {
handler Step phase Update {
let slope = Math.tan(pitch)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-wrap
name: Math.wrap
category: math
kind: namespace-method
tokens: Math.wrap
sig: Math.wrap(v, lo, hi) -> int
tip: Wrap a value into the range [lo, hi).
order: 21
ns: Math
member: wrap
---
Wraps <code>v</code> into the half-open range <code>lo</code>..<code>hi</code>, so values that run off one end reappear at the other — the toroidal counterpart to <code>Math.clamp</code>. Use it for wrap-around movement, cycling through a menu, or keeping a scrolling offset in bounds.
```ludic
program Demo {
handler Step phase Update {
let x = Math.wrap(player_x, 0, world_width)
}
}
```