API Reference
Every keyword, type, phase, builtin, namespace method, annotation and color in Ludic — each on its own page. Search, or browse by section. In any code sample across this site, hover a token for a summary and click to jump to its page.
Program structure
The shape of a Ludic program: one program block holding declarations.
programThe top-level unit — one program compiles to one native game.propertyA named record of typed fields — a per-model component, or a plain heap record.modelA named kind of thing — a fixed bundle of properties you spawn by one name.handlerA named block the engine runs each frame during phase P.phaseNames which stage of the frame a handler runs in.constA compile-time constant, folded into the code with no storage.varA mutable binding — at program scope, your game's persistent named state.letAn immutable binding — the default choice for a value that never changes.functionA function — reusable logic called positionally or with named arguments.fnA top-level function named as a value - the entry point handed to Job.parallel_for.prefabA model with preset component fields — spawn it, override what varies.returnHand a value back from a function and stop running it.entryThe program's main block — runs once, top to bottom.importSplice another Ludic file's declarations into this program.publicExpose a declaration's lifecycle on the public event bus.externBind a name to an external native symbol — the seam for platform and library calls.newAllocate a property record or an empty slice on the heap.namespaceGroup functions into a Name.* namespace and control its public surface.enumA named set of integer constants — names for a magic-number space.uiDeclare a retained widget tree as data; the engine lays it out and draws it.Entities & the ECS
Entities are ids; properties are their data; queries walk them.
spawnCreate a model instance carrying the listed properties, seeding their fields.despawnRemove a model instance from the world, firing any @OnDespawn hooks.queryMatch every model instance carrying all the listed properties.selfThe model instance currently bound by the enclosing query loop.enableRe-activate a disabled property, model, or handler — its data is intact.disableDeactivate a property, model, or handler without destroying its data.attachStructurally add a property to a live model instance, seeding its fields.detachStructurally remove a property from a live model instance.world_attach_dynAttach a property to an instance by numeric id at runtime (dynamic ECS).world_countThe total number of live model instances in the world.world_detach_dynDetach a property from an instance by numeric id at runtime (dynamic ECS).world_field_idResolve a field name within a property to its numeric index.world_getRead one field of a model instance by numeric id (reflection ABI).world_hasTest whether a model instance currently carries a property.world_kindThe model id of an instance — which kind of thing it is.world_loadRestore the whole world from a serialized byte buffer.world_model_idResolve a model's name to its stable numeric id.world_prop_idResolve a property's name to its stable numeric id.world_query_nextStep to the next instance carrying a property, walking the world by id.world_register_propRegister a brand-new property at runtime and get its id (dynamic ECS).world_saveSerialize the whole world into a buffer; returns the number of bytes written.world_setWrite one field of a model instance by numeric id (reflection ABI).world_sizeThe number of live model instances in the world.world_spawnSpawn an instance of a model chosen by numeric id at runtime.Control flow
Branches, loops, and state machines.
if / elseA branch — run one block when a condition holds, another when it doesn't.whileLoop as long as a condition holds, re-checking it before each pass.for … inRange loop — iterate the half-open range from a up to but not including b.inThe part of a for loop that names what to iterate — a range or a query.matchMulti-way branch on one value, matching one or more literals per arm.machineAn explicit state machine over an int var — dispatches on the store's value.stateOne state of a machine — its body runs while the machine sits in it.becomeTransition to another state of the enclosing machine, or to another scene.breakLeave the enclosing loop immediately.continueSkip to the next iteration of the loop.whereFilter a query loop to entities that satisfy a condition.tryRecover a fallible result as a value, with a fallback — no exceptions, no unwinding.Scenes & layers
One active scene at a time, each grouping handlers into layers.
sceneA mutually-exclusive game state — a title screen, the overworld, a battle.layerA named group of handlers inside a scene; layers render in declaration order.on enter / on exitScene lifecycle hooks that fire as a scene becomes active or is left.enterThe scene-entry hook — runs once as a scene becomes active.exitThe scene-exit hook — runs once as a scene is left.startMarks the one scene the game begins in.Events
Decoupled, named messages between handlers.
Screen — drawing
The 2D drawing surface. Every call takes named arguments; draw during the Render phase, then Screen.show().
Screen.clearFill the whole framebuffer with one color to start a fresh frame.Screen.fill_rectangleDraw a solid, filled rectangle at a pixel position.Screen.draw_rectangleDraw a one-pixel-thick rectangle outline (not filled).Screen.put_pixelSet a single pixel at a pixel coordinate to one color.Screen.draw_textDraw a string with the built-in font at an integer scale.Screen.draw_numberDraw an integer directly, with no string allocation or conversion.Screen.showPresent the finished frame — copy everything drawn this frame to the window.Screen.widthThe framebuffer width in pixels.Screen.heightThe framebuffer height in pixels.Screen.statusSet the persistent one-line status/HUD string shown by the runtime.Screen.lineDraw a straight line between two points.Screen.circleDraw a circle outline.Screen.fill_circleDraw a filled disc.Screen.triangleDraw a triangle outline.Screen.fill_triangleDraw a filled triangle.Screen.spriteBlit a sprite at its natural size.Screen.sprite_scaledBlit a sprite at an integer scale.Screen.ovalDraw an axis-aligned ellipse outline.Screen.cameraSet the world-space camera offset for the draw path.Screen.clipRestrict drawing to a screen-space rectangle.Screen.clip_resetLift the clip rectangle (draw to the whole screen again).Screen.blend_modeChoose replace or additive pixel blending.Screen.measure_textThe pixel advance width of text in the built-in font.Screen.pixelRead a framebuffer pixel (0x00RRGGBB), or 0 if off-screen.Screen.barA filled meter: value of max over a track.Clock
A game-controlled simulated clock: a single seconds counter the game owns. Unlike Time.now / Time.since, it never reads the wall clock, so any gameplay that reads Clock.now() is deterministic and replay-safe. Set it outright, or advance it by a Duration each tick to run time at whatever rate the simulation wants. The clock is a plain instant (seconds since 1970), so all the DateTime.* readers work on it directly.
Collision
2D overlap tests on integer coordinates (pixels or tiles). Rectangles are (x, y, w, h) from the top-left; circles are (x, y, r). Each returns a bool. Squared distances are computed in 64-bit so large coordinates never overflow.
These are static boolean tests. For a *moving* body with real collision resolution, the engine ships a physics-lite movement system built on the same integer math — declare the well-known components and the engine advances them for you each frame, no handler wired:
- property Body { vx, vy, gravity, max_fall, rx, ry, policy, on_ground, hit_wall, hit_ceiling } — velocity/accel state in Q16.16 fixed-point (vx/vy/gravity/max_fall), engine-owned sub-pixel accumulators (rx/ry), an integration policy (0 platformer with gravity, 1 top-down), and the derived contact flags the engine sets each frame.
- property Collider { w, h, offx, offy, is_trigger, one_way, layer, mask, hit, entered, exited } — an AABB shape offset from the entity's Position { x, y }, with layer/mask filtering, one-way-platform and trigger flags, and per-frame trigger outputs.
- property Solids { tile, wall, oneway } — an optional single config entity that turns on the tile-grid broadphase over the Map tilemap (tile px size, the solid wall glyph, and an optional one-way oneway glyph).
The engine-owned esys_move system (Update phase) integrates velocity and gravity into a tentative move, then resolves it with a per-axis **swept AABB** — against both solid Collider entities and the tile grid — so a fast body never tunnels through a wall. It handles one-way platforms (which block only a downward landing), reports trigger/sensor overlaps without resolving them, and sets on_ground / hit_wall / hit_ceiling for a controller to read. Everything is integer and deterministic, so movement reproduces exactly under replay, lockstep and world_save snapshots. Contact is surfaced as polled flags (the SpriteAnim.event_fired shape) rather than engine-emitted events, so a game raises its own CollisionResolved / TriggerEntered events from its handler with no coupling. This is the shared foundation the built-in gameplay controllers build on. Related: Grid, Map, Motion, @EngineSystem.
Color functions
Building and blending colors at runtime. Colors are 0x00RRGGBB ints; these pack channels and transform an existing color. The named palette constants (Color.Charcoal, …) are a separate compile-time set.
Crypto
Secure, test-vector-backed hashing for the handful of security-sensitive things a game actually does: signing a save or leaderboard payload so casual tampering is detectable, and verifying that a network message or token was not forged by someone who does not hold the key. This is the deliberate counterpart to the fast Hash library — same idea, opposite trade-off. Hash is fast and reversible and must never guard anything; Crypto is SHA-256 and HMAC-SHA256 implemented to the standard, so the algorithms are the ones with published known-answer tests rather than anything home-grown.
Digests are returned as lowercase hex strings, not raw bytes — a str is null-terminated and a raw digest can contain a zero byte, so hex is the form you can print, store, and compare directly.
Alongside hashing, this library exposes the OS cryptographically-secure random generator — random_bytes, random_hex, and random_u32 — for tokens, nonces, and Uuid generation, plus base64 for moving bytes through text-only channels. The secure-random helpers are deliberately non-deterministic and must never seed the lockstep simulation RNG (Random).
What this is not: it is not DRM and it is not unbeatable anti-cheat. A client-side game cannot keep a secret from the machine it runs on — a determined owner can always read the key out of the binary. Use it to make *casual* tampering detectable and to authenticate messages between parties who share a key. To verify a MAC always use Crypto.verify_hmac (a constant-time check), never ==, which leaks how much of a guessed MAC was correct.
Crypto.sha256SHA-256 of a string, as 64 hex characters.Crypto.hmac_sha256Sign a message with a shared secret key.Crypto.verify_hmacConstant-time check that a MAC matches.Crypto.hexLowercase hex of a string's bytes.Crypto.ct_equalConstant-time string equality for secrets.Crypto.random_bytesn bytes from the OS CSPRNG, as a 2n-character hex string.Crypto.random_hexAlias for random_bytes — n secure bytes as a 2n-char hex string.Crypto.random_u32One CSPRNG-drawn 32-bit integer.Crypto.base64Standard base64 (RFC 4648) of a string's bytes.Date
Calendar days on the proleptic Gregorian calendar (UTC). A Date is stored as a plain int: the count of days since 1970-01-01, so shifting a date by whole days is ordinary integer arithmetic and two dates subtract to a day count. Build one with Date.new, read its parts with year/month/day/weekday. Every operation is deterministic integer math — no leap seconds, no timezone, no floating point.
Date.newA calendar day as days-since-1970.Date.yearThe calendar year of a date.Date.monthThe month (1-12) of a date.Date.dayThe day of the month (1-31) of a date.Date.weekdayDay of the week, 0=Sunday..6=Saturday.Date.is_leapTrue if the year is a leap year.Date.days_in_monthNumber of days in a given month.Date.to_epochMidnight UTC of the day, as a DateTime.Date.add_daysThe date n days later (n may be negative).Date.diff_daysWhole days from b to a (a - b).DateTime
Instants on the wall-clock timeline (UTC), stored as a plain int: the count of seconds since 1970-01-01, matching Time.now. Build one with DateTime.from, shift it by a Duration with DateTime.add, and pull it apart with the year/month/day/hour/minute/second readers. Deterministic integer math throughout; v1 is UTC-only with no leap seconds, and assumes instants at or after 1970.
DateTime.fromBuild an instant from its calendar parts.DateTime.dateThe calendar day the instant falls on.DateTime.addThe instant `span` seconds later.DateTime.yearThe calendar year of an instant.DateTime.monthThe month (1-12) of an instant.DateTime.dayThe day of the month (1-31) of an instant.DateTime.weekdayDay of the week, 0=Sunday..6=Saturday.DateTime.hourThe hour of day (0-23) of an instant.DateTime.minuteThe minute of the hour (0-59) of an instant.DateTime.secondThe second of the minute (0-59) of an instant.DateTime.formatRender an instant as text using a token pattern.DateTime.parseParse text into an instant; -1 on failure.Duration
Spans of real time, measured in whole seconds and carried in a plain int — so a duration adds, subtracts and compares with the ordinary operators (Duration.minutes(5) + Duration.seconds(30), away > Duration.hours(3)). The constructors build a span from a unit; the as_* readers convert a span back to whole units (truncating toward zero). Integer-only, so no floating-point drift.
Duration.secondsA span of n whole seconds.Duration.minutesA span of n minutes, in seconds.Duration.hoursA span of n hours, in seconds.Duration.daysA span of n days, in seconds.Duration.as_secondsThe span in whole seconds.Duration.as_minutesThe span in whole minutes.Duration.as_hoursThe span in whole hours.Duration.as_daysThe span in whole days.Ease
Tween curves — the "juice" layer. Each takes a normalized amount t in 0.0..1.0 and returns an eased fixed, ready to feed to Math.lerp. All deterministic fixed-point.
Fs
The filesystem, wrapped into one safe, ergonomic API — no file descriptors, no byte buffers, no half-written files. The foundation for saves, config, mods, and asset loading: read and write whole files as text, check existence, make directories, copy, and list a folder. The bare file_* builtins remain underneath for streaming; Fs.* is the everyday layer.
Fallible calls surface failure as a value — null, false, or -1 — that you branch on, never a crash (a first-class try/else lands with the error-handling work). Writes are atomic (written to a temporary file and renamed into place), so a crash mid-write never corrupts the previous file. list returns entries sorted, so a directory walk is reproducible across runs and platforms.
Paths are /-separated over the native (macOS/BSD) filesystem — the fully supported target today; a sandboxed virtual filesystem for wasm and recursive directory copy are follow-ups. Text helpers are UTF-8 (they pair with Unicode); use Path to build the paths and Os for per-user locations.
Fs.existsDoes a path exist?Fs.is_dirIs a path a directory?Fs.read_textRead a whole file as a string.Fs.write_textWrite a string to a file (atomically).Fs.append_textAppend a string to a file.Fs.removeDelete a file.Fs.sizeFile size in bytes.Fs.mkdirCreate a directory and its parents.Fs.copyCopy a file byte-for-byte.Fs.listDirectory entries, sorted.Hash
Fast, non-cryptographic hashing for everyday game needs: turning string IDs into integer handles, mixing a few numbers into one deterministic seed, and checksumming data to catch corruption. Every function is plain 32-bit integer arithmetic with a defined byte order and fixed constants, so a given input hashes to exactly the same value on every platform and every run — the guarantee that makes it safe for procedural generation and lockstep networking. Results are 32-bit and print as signed integers. Arguments are positional. Not for security. These hashes are fast and reversible — never use them for passwords, tokens, or tamper-proofing. For secure hashing reach for the cryptography library instead.
Hash.ofThe fast default string hash.Hash.fnv1aFNV-1a 32-bit, named explicitly.Hash.crc32CRC-32 checksum for corruption detection.Hash.mixAvalanche one integer into a well-scrambled value.Hash.combineFold several ints into one deterministic seed.Hash.of64The fast default 64-bit string hash.Hash.fnv1a_64FNV-1a 64-bit, named explicitly.Hash.mix64Avalanche one 64-bit integer into a well-scrambled value.Input
Reading input. At the base, Input.key gives this frame's key and Input.poll is the single per-frame read that also drives deterministic record/replay. Over that sit named actions — Input.bind / Input.down / Input.pressed / Input.rebind — so gameplay reads rebindable actions, not physical keys.
The device layer adds everything past one key per frame: multiple simultaneous held keys (Input.key_down / key_pressed / key_released), analog Input.axis and normalized Input.vector, the mouse (position, delta, buttons, wheel), gamepads and touch. The held set is fed by the platform when windowed and by the Input.press / Input.set_* injection on every target — the same idea as Godot's action_press, and what a replay, an AI, or the network feeds. Everything is integer and deterministic, and record/replay snapshots the whole per-frame state.
Input.keyThe key pressed this frame as a character code (0 when nothing is pressed).Input.bindBind a physical key to a named action, so gameplay reads the action, not the key.Input.rebindRemap an action from one key to another at runtime (rebinding menus).Input.pollAdvance one frame of input; the single per-frame read behind actions and replay.Input.downIs a named action held on the frame last polled?Input.pressedDid a named action go down this frame (a one-shot edge)?Input.recordStart recording polled input each frame (for deterministic replay).Input.replayReplay recorded input; poll then reads the tape, not the device.Input.key_downIs a physical key held this frame?Input.key_pressedDid a key go down this frame (edge)?Input.key_releasedDid a key go up this frame (edge)?Input.key_labelThe name to show a player for a key - in their own keyboard layout.Input.pressInject a held key (AI, tutorial, testing, network).Input.releaseRelease an injected key.Input.axisA -1..+1 axis from two keys.Input.axis_iA -1/0/1 directional intent as a plain int, no bool->int glue.Input.vectorA normalized 2D vector from four keys.Input.strength0..1 strength of a named action.Input.mouse_xThe mouse x position this frame.Input.mouse_yThe mouse y position this frame.Input.mouse_dxMouse x movement since the last poll.Input.mouse_dyMouse y movement since the last poll.Input.mouse_downIs a mouse button held?Input.wheelScroll-wheel delta this frame.Input.set_mouseInject the mouse state (headless / AI / testing).Input.pad_connectedIs a gamepad connected?Input.pad_buttonIs a gamepad button held?Input.pad_axisA gamepad analog axis, -1..+1.Input.set_padInject a gamepad's whole state.Input.touch_countHow many touch points are active?Input.touch_xThe x of a touch point.Input.touch_yThe y of a touch point.Input.set_touchInject a touch point.Input.actionRegister a default key binding for an action (kept if already bound).Input.bind_padAlso fire a named action from a gamepad button (device-agnostic).Input.activeIs a named action active right now (any bound key held, or pad button down)?Input.just_pressedDid a named action go active this frame (the deterministic on-press)?Input.just_releasedDid a named action go inactive this frame (the on-release edge)?Input.cursor_modeHide / lock / confine the OS mouse cursor to the window (windowed).Input.move_iThe standard top-down movement intent, -1/0/1 per axis.List
Operations over []T slices — length, ends access, push/pop, swap, search, and in-place reverse. A slice is a shared growable buffer, so these mutate it in place and every holder sees the change. Arguments are positional.
List.lenThe number of elements in a slice.List.pushAppend an element to the end of a slice.List.clearRemove all elements, keeping capacity.List.firstThe first element of a slice.List.lastThe last element of a slice.List.popRemove and return the last element.List.swapExchange the elements at two indices.List.containsWhether a value appears in a slice.List.index_ofThe index of a value in a slice, or -1 if absent.List.reverseReverse the order of a slice in place.List.insertInsert an element at an index, shifting the rest up.List.remove_atRemove the element at an index, shifting the rest down.List.removeRemove the first element equal to a value.List.sortSort a slice in ascending order, in place.List.sort_bySort a slice ascending by a key each element maps to.List.sort_desc_bySort a slice descending by a key each element maps to.List.sort_withSort a slice with a full two-argument comparator.List.sampleRandom picks from an int slice, distinct while it can.Log
Levelled, structured logging — the default way to answer "what is my game doing?" and "why did that break?", and a real step up from scattering print calls through your code. Each message carries a level, from trace (most verbose) through debug, info, and warn to error, and a runtime threshold set with Log.set_level decides which ones actually appear — so a development build can be chatty and a release build quiet, without touching the call sites.
Messages go to standard error, kept separate from a program's real stdout, tagged with their level. Beyond the message you can pass **structured fields** as trailing key/value pairs — Log.warn("missing texture", "path", p, "id", n) prints [WARN] missing texture path=... id=... — cheap to write and easy to grep. Numeric values (int, long) are formatted for you; the level tag is chosen at compile time, so a filtered-out level costs only a threshold comparison at runtime.
Logging never touches the simulation — it writes to stderr and returns — so it has no effect on gameplay determinism or replays. This first version ships the console (stderr) sink; a rotating-file sink and an in-engine overlay sink are planned follow-ups.
Log.traceLog at the trace level (0) — the most verbose.Log.debugLog at the debug level (1) — development detail.Log.infoLog at the info level (2) — normal operation.Log.warnLog at the warn level (3) — something looks wrong.Log.errorLog at the error level (4) — a failure.Log.set_levelShow only messages at level n or above (0 = all).Log.levelThe current logging threshold.Math
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.
Given a float or double argument, the same functions compute in that type instead (using the platform's math library) and return it — floor, ceil and round included, which return an int only for fixed. sign returns an int either way. trunc and atan exist only for floats.
Math.minThe smaller of two values.Math.maxThe larger of two values.Math.absThe magnitude of a value, dropping its sign.Math.clampConstrain a value to the range [lo, hi].Math.signThe sign of a value as -1, 0, or 1.Math.floorRound a fixed value down to the nearest whole int.Math.ceilRound a fixed value up to the nearest whole int.Math.roundRound a fixed value to the nearest whole int.Math.lerpBlend between two values by a 0..1 amount.Math.inverse_lerpFind where a value sits between two endpoints as a 0..1 fraction.Math.remapRescale a value from one range into another.Math.sqrtThe square root of a fixed value.Math.sinSine of an angle in radians.Math.cosCosine of an angle in radians.Math.tanTangent of an angle in radians.Math.hypotLength of the vector (x, y).Math.distDistance between two points.Math.dist2Squared distance between two points.Math.deg_to_radConvert degrees to radians.Math.rad_to_degConvert radians to degrees.Math.posmodModulo that is always non-negative.Math.wrapWrap a value into the range [lo, hi).Math.ping_pongBounce a counter back and forth in [0, len].Math.snappedRound a value to the nearest multiple of step.Math.move_towardStep from one value toward another by at most delta.Math.smoothstepA smooth 0..1 ramp between two edges.Math.atan2The angle of the vector (x, y), in radians.Math.asinThe arcsine of a value, in radians.Math.acosThe arccosine of a value, in radians.Math.expe raised to the power x.Math.logThe natural logarithm of x.Math.powbase raised to a fixed exponent.Memory
Raw memory — allocate byte and word buffers, copy and fill regions, and peek/poke individual bytes. The low-level escape hatch.
Mime
Content-type detection for assets and downloads: map a filename to its MIME type, or refine the guess by peeking at a file's leading bytes. Useful for deciding how to load a dropped-in mod file, labelling an export, or picking a decoder.
of is a pure lookup by extension; sniff reads the first bytes and recognises a few well-known signatures (PNG, JPEG, GIF, PDF), falling back to the extension when nothing matches. The extension table is deliberately compact and documented; richer magic-byte coverage is a follow-up.
Network
The low-level networking seam — send and poll datagrams, serialize and apply entity state, and read ownership and role. Multiplayer is normally a compile-time property of the ECS, not glue you thread by hand: mark fields @Sync and models @Owned and the compiler generates the replication. These Network.* primitives are what that sugar lowers to, for when you drive the transport yourself. Offline they collapse to single-player defaults.
Network.sendPut a datagram on the wire.Network.pollRead an inbound datagram.Network.serializeSerialize an entity's synced state.Network.applyApply serialized state to an entity.Network.ownerThe peer that owns an entity.Network.set_ownerAssign ownership of an entity.Network.is_serverIs this peer the server?Network.is_ownerDoes this peer own the entity?Network.local_idThis peer's own id.Noise
Procedural noise — the primitive that terrain, caves, biomes, textures, clouds, wind, and object placement are built on. Every generator is implemented in Q16.16 **fixed point** over an integer permutation hash seeded from an explicit seed, so a given seed reproduces the *exact* same field on every platform and every run: native, headless, and (later) wasm all agree bit-for-bit. That is a real edge over float-based engines, whose worlds can drift subtly across CPUs and break shared-seed multiplayer or replays.
Coordinates are fixed values. The integer part of a coordinate selects a lattice cell and the fraction interpolates within it, so you scale feature size by sampling at a fractional *frequency* (e.g. multiply coordinates by 1/64). Outputs are fixed normalised to [-1, 1]; Noise.unit remaps that to [0, 1] when you want a height or a probability.
Pick a generator by feel: value2 is cheap and blocky; perlin2 is the classic gradient noise; simplex2 is the organic default with fewer directional artifacts; fbm2 stacks octaves of simplex for natural, detailed fields; and cellular2 (Worley) gives Voronoi-cell structure for stone, cracks, and biome boundaries. Every sampler is a pure function of (x, y, seed) — no global state, no allocation — so it is safe to call across a whole worldgen pass or a per-pixel fill.
Seed worldgen from its own seed (or a dedicated Random stream), kept separate from gameplay RNG, so generating the world never desyncs the simulation.
Noise.value22D value (lattice) noise, deterministic, in [-1, 1].Noise.perlin22D Perlin gradient noise, deterministic, in [-1, 1].Noise.simplex22D simplex noise, the organic default, in [-1, 1].Noise.fbm2Fractal Brownian motion — octaves of simplex, in [-1, 1].Noise.cellular2Worley (cellular) F1 distance to the nearest cell point.Noise.cellular2_idThe id of the nearest Worley cell — stable per cell.Noise.unitRemap a [-1,1] noise sample to [0,1].Os
The operating-system interface — the environment *around* the game rather than the game itself: the command line, environment variables, the standard streams, the process exit code, the host platform, and the per-user folders a game writes its saves, config, and cache into. It rounds the bare System builtins (arg, getenv, exit) into one coherent, Go-flavored namespace.
It is deliberately small and game-scoped: there is no process spawning, no signals, and no user/permission APIs. Reach for it in launchers, asset pipelines, and dev tools — parsing launch flags like --level 3, reading config from the environment, or resolving where a save file belongs — far more than in the simulation itself.
**Determinism:** args, the env family, and platform/arch are non-deterministic host input. Read them once at startup to configure the game, and keep them out of the replayable simulation so a shared seed still reproduces.
**Platform coverage:** this first version targets the native macOS/BSD host — the fully supported target today. platform is portable (the uname system name is available on every Unix); arch and the known-folder layout (save_dir/config_dir/cache_dir) follow the macOS conventions. Linux/Windows/wasm folder resolution and a target-aware arch are documented follow-ups.
Os.argsEvery command-line argument, as a list.Os.arg_countHow many command-line arguments there are.Os.argThe i-th command-line argument.Os.envRead an environment variable (null if unset).Os.env_orRead an environment variable, or a fallback.Os.has_envIs an environment variable set?Os.set_envSet an environment variable.Os.unset_envRemove an environment variable.Os.exitTerminate the process with a status code.Os.platformThe host platform id.Os.archThe host CPU architecture.Os.stdout_writeWrite a string to standard output.Os.stderr_writeWrite a string to standard error.Os.save_dirPer-user save directory for an app.Os.config_dirPer-user config directory for an app.Os.cache_dirPer-user cache directory for an app.Os.temp_dirThe system temporary directory.Os.pidThis process's id.Path
Path manipulation as pure string operations — no disk access, no allocation surprises, just the joining and splitting every save system, mod loader, and asset pipeline needs. Separate a filename from its folder, get an extension, join segments without worrying about doubled or missing separators, and collapse ./../duplicate slashes into a canonical form.
Paths use / as the separator, matching the native (macOS/BSD) filesystem — the fully supported target today; Windows-style separators are a follow-up. Pair these with Os (which supplies per-user directory *locations*) and Fs (which does the actual reading and writing).
Save
Whole-world save and restore — write a binary snapshot of the ECS world and read it back.
System
Low-level file and process access — raw file handles, reading a character, and running a shell command. Outside the deterministic simulation. For command-line arguments, environment variables, the exit code, and the standard streams, use the Os namespace.
System.runRun a shell command.System.read_charRead one byte from standard input.System.file_openOpen a file.System.file_readRead bytes from a file.System.file_writeWrite bytes to a file.System.file_seekMove a file's read/write position.System.file_tellThe current file position.System.file_closeClose a file.Text
String queries and building over string values — length, character access, substrings, search, prefix/suffix tests, and int conversion. Arguments are positional. Strings are byte sequences, so indices and lengths count bytes.
Text.lengthThe number of bytes in a string.Text.char_atThe byte value at an index, 0..255.Text.sliceA substring covering bytes [a, b).Text.starts_withWhether a string begins with a given prefix.Text.ends_withWhether a string ends with a given suffix.Text.containsWhether a substring appears anywhere in a string.Text.index_ofThe byte index of a substring, or -1 if absent.Text.equalsWhether two strings have identical bytes.Text.concatJoin two strings into a new one.Text.to_intParse a leading integer from a string.Text.from_intRender an integer as a string.Text.upperAn uppercased copy of a string.Text.lowerA lowercased copy of a string.Text.trimA copy with surrounding whitespace removed.Text.repeatA string repeated n times.Text.pad_leftPad with spaces on the left to a width.Text.pad_rightPad with spaces on the right to a width.Text.splitSplit a string on a separator into a list.Text.joinJoin a list of strings with a separator.Text.replaceReplace every occurrence of a substring.Time
The frame clock. frame/elapsed/delta are deterministic (driven by a per-frame counter at a fixed 60 fps), so a replay reproduces every value; now is the wall clock and is explicitly non-deterministic.
Unicode
Correct-by-default text over UTF-8. Player names, translated menus, and chat arrive as UTF-8 bytes, and counting *bytes* gets non-ASCII text wrong — the wrong length, and truncation that slices a character in half into mojibake. The Unicode namespace works in code points and (approximately) grapheme clusters instead, so measuring, indexing, truncating, and case-mapping behave for every language.
This is the correctness layer, not a replacement: the byte-oriented Text operations stay for speed on ASCII and for raw byte work. Reach for Unicode.* whenever the text came from a human — a name, a message, a localized string.
Three notions of "length" matter, and the API keeps them distinct: byte_len (storage), len (code points — Unicode scalar values), and grapheme_len (user-perceived characters, where a base letter plus its combining accent, or a ZWJ emoji sequence, count as one).
**Coverage (v1):** decoding and validation cover the full UTF-8 range. Case mapping covers ASCII and the Latin-1 letters — correct for Western-European text; wider scripts (Latin-Extended, Greek, Cyrillic), locale rules (Turkish i, German ß), and NFC normalization are follow-ups. grapheme_len approximates UAX#29 for the cases real player text hits — combining marks, variation selectors, ZWJ sequences, and flag pairs.
Unicode.lenNumber of code points in a string (not bytes).Unicode.byte_lenNumber of bytes in a string.Unicode.is_valid_utf8Is a string well-formed UTF-8?Unicode.char_atThe i-th code point of a string.Unicode.charsEvery code point of a string, in order.Unicode.upperUppercase a string.Unicode.lowerLowercase a string.Unicode.truncateFirst n code points of a string.Unicode.grapheme_lenNumber of user-perceived characters.Uuid
Universally-unique identifiers — stable IDs that don't collide, generated locally with no central authority handing out numbers. Reach for a UUID whenever something needs an identity that survives being saved, shared, or sent over a network: players and sessions in multiplayer, user-created content (levels, items, mods) that has to merge cleanly across installs, or a per-install / per-run ID for analytics and bug reports.
Two versions are provided. Uuid.new makes a **v4** (random) UUID — 122 bits of entropy, effectively never colliding. Uuid.new_v7 makes a **v7** (time-ordered) UUID whose leading bits are a millisecond timestamp, so a batch of v7 IDs sorts by creation time — friendly to database indexes and append logs. Both set the RFC 4122 version and variant bits correctly.
A UUID is represented as its canonical lowercase 36-character text form (8-4-4-4-12), the same shape you store, print, send, and compare — so there is no conversion at each boundary. Validate untrusted input with Uuid.is_valid or normalise it with Uuid.parse, and compare with Uuid.equals, which ignores case.
**Determinism caveat.** v4 and the random tail of v7 come from the OS cryptographically-secure RNG (Crypto), which is non-deterministic by design. Minting a UUID inside the lockstep simulation will desync replays and networked peers — generate IDs at the edges (on connect, on save, on spawn-from-input), never per tick in reproducible gameplay code.
Uuid.newA new random (v4) UUID as a canonical 36-char string.Uuid.v4Explicit alias for Uuid.new — a random (v4) UUID.Uuid.new_v7A new time-ordered (v7) UUID; sorts by creation time.Uuid.v7Explicit alias for Uuid.new_v7 — a time-ordered UUID.Uuid.parseNormalise an untrusted string to a lowercase UUID, or the nil UUID.Uuid.is_validIs s a well-formed UUID string?Uuid.to_textThe canonical text form of a UUID.Uuid.equalsCase-insensitive UUID equality.Uuid.nilThe all-zero UUID.World
Runtime reflection over the ECS world — read and write component fields by id, spawn and inspect entities, and register components at runtime. The introspection layer a mod uses.
World.getRead a component field by id.World.setWrite a component field by id.World.hasDoes an entity have a component?World.countThe number of live entities.World.sizeThe serialized size of the world.World.spawnSpawn an entity of a model by id.World.saveSerialize the world into a buffer.World.loadRestore the world from a buffer.World.prop_idLook up a component's id by name.World.field_idLook up a field's id by name.World.model_idLook up a model's id by name.World.kindThe model id of an entity.World.register_propRegister a new component at runtime.World.attachAttach a component to an entity at runtime.World.detachDetach a component from an entity at runtime.World.query_nextAdvance a reflective query.World.despawnDespawn an entity by id — runs its @OnDespawn hooks and frees the slot.Audio
Sound effects and music. A sound is loaded once with Audio.load into a handle; Audio.play fires it one-shot and Audio.play_music loops it on a single music channel. Playback is out-of-band — the audio device is real-time, not part of the deterministic simulation — but every trigger here is an ordinary frame-driven call, so a recorded run replays the same sounds at the same frames. Headless builds carry the whole API as no-ops (no audio device needed), so the same game code runs under the test harness.
Audio.loadLoad a sound file into a handle.Audio.playFire a one-shot sound.Audio.play_atFire a one-shot with its own gain, pitch and stereo position.Audio.play_soundFire a one-shot sound (alias of play).Audio.play_musicLoop a sound as background music.Audio.stopStop one sound.Audio.stop_musicStop the music channel.Audio.stop_allStop every sound.Audio.volumeSet the master volume.Audio.pitchSet the playback rate / pitch.Audio.is_playingIs this sound playing?Audio.defineLoad a sound into the bank under a name.Audio.namedThe handle registered under a name, or 0.Random
A seeded, deterministic RNG — same seed, same sequence, every run and every platform.
Random.rangeA random integer in the inclusive range [low, high].Random.chanceReturn true with the given percent probability.Random.seedSeed the random generator so runs are reproducible.Random.valueA random fixed value in [0, 1).Random.intA random integer in [0, max).Random.signA random +1 or -1.Random.weightedAn index drawn in proportion to its weight.Vector
2D vector math for positions, velocities, and directions. A Vector is a pair of Q16.16 fixed components packed into one value, so it is copied by value and never allocates. Every operation is deterministic integer fixed-point — bit-identical on every platform, the same guarantee the rest of the runtime gives. Arguments are positional; angles are in radians.
Vector.makeBuild a vector from x and y components.Vector.zeroThe zero vector, (0, 0).Vector.xThe x component of a vector.Vector.yThe y component of a vector.Vector.addComponent-wise sum a + b.Vector.subComponent-wise difference a - b.Vector.scaleScale a vector by a scalar.Vector.dotThe dot product of two vectors.Vector.lengthThe length (magnitude) of a vector.Vector.distanceThe distance between two points.Vector.normalizeA unit vector in the same direction.Vector.lerpLinear interpolation between two vectors.Vector.rotateRotate a vector by an angle in radians.Vector.angleThe angle of a vector in radians.Vector.from_angleA unit vector pointing at an angle.Http
A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with Http.get / Http.post (or Http.open to add headers first), then each frame call Http.poll — it returns -1 while pending, 0 on error, or the status code — so the frame never blocks. Read the reply with Http.status / ok / text / header, and pair it with Json.* for (de)serialization. TLS is the system's, on by default. A large download goes to a file instead of memory with Http.save_to, and Http.received / expected drive a progress bar while it is pending.
HTTP depends on the network and the wall clock, so — like Net.* and Time.now — it is out-of-band and must never feed the deterministic lockstep/replay simulation. The transport is NSURLConnection / NSURLSession on macOS and WinHTTP on Windows; the raw-response parser (Http.parse) is pure and portable.
Http.getStart an async GET request.Http.postStart an async POST with a body.Http.requestStart a request with any method.Http.openOpen a request to configure before sending.Http.setSet a request header before sending.Http.bodySet a string request body.Http.body_bytesSet a raw byte request body.Http.sendDispatch an opened request.Http.pollPoll a request; -1 pending, 0 error, else status.Http.statusThe response HTTP status code.Http.okWas the response status 2xx?Http.textThe response body as text.Http.body_lenThe response body length in bytes.Http.headerA response header value (case-insensitive).Http.freeRelease a request's resources.Http.parseParse a raw HTTP response into a handle.Http.save_toStream the response body to a file.Http.receivedBody bytes received so far.Http.expectedThe body length the server announced, or -1.IVec2
Integer 2D vector math for tile and grid coordinates, cell offsets, and integer sizes. An IVec2 is a pair of whole-number int components (x, y) packed into one value, so it is copied by value and never allocates. Every operation is exact integer arithmetic — no rounding, and bit-identical on every platform. Use it wherever a fractional part would be meaningless; reach for Vector when you need sub-pixel precision. Arguments are positional.
IVec2.makeBuild an integer vector from x and y components.IVec2.zeroThe origin cell, (0, 0).IVec2.xThe x component of an integer vector.IVec2.yThe y component of an integer vector.IVec2.addComponent-wise sum of two integer vectors.IVec2.subComponent-wise difference of two integer vectors.IVec2.scaleMultiply both components by an integer.IVec2.dotThe dot product ax*bx + ay*by.IVec2.equalTrue when both components match.IVec2.manhattanGrid distance |dx| + |dy|.IVec2.to_vectorWiden to a fixed-point Vector.IVec2.distance2The squared distance between two points.IVec2.withinAre two points within a radius of each other?IVec2.headingThe direction from one point to another, in degrees.IVec2.alongThe point a distance along a heading.IVec2.stepA -1/0/1 unit step along a heading.Map — tilemap
A character grid the game paints and reads.
Map.sizeSet the tilemap dimensions in cells before filling rows.Map.rowFill one row of the tilemap from a string of tile characters.Map.tileRead the tile character at a cell (out-of-bounds reads answer '#').Map.getThe glyph at a cell ('#' outside the map).Map.setWrite one cell in place.Map.fillEvery cell becomes the glyph.Map.rectFill a rectangle of cells.Map.borderThe outermost ring of cells.Map.random_cellA random cell holding the glyph (seeded RNG).Map.is_solidIs a cell solid, per the Solids config?Map.is_solid_atIs the cell under a pixel position solid?Map.widthThe map's width in cells.Map.heightThe map's height in cells.Map.random_cell_farA random cell with the glyph, at least a distance from a point.Map.to_tileThe tile under a pixel position.Rect
Axis-aligned rectangles for HUD layout boxes, hitboxes, and camera regions. A Rect is four Q16.16 fixed components — position (x, y) (its top-left corner) and size (w, h) — packed into a single value that is copied by value and never allocates. It offers fast point-in-rect and rectangle-overlap tests. Every operation is deterministic fixed-point, bit-identical on every platform. Arguments are positional.
Rect.makeBuild a rectangle from a corner and a size.Rect.xThe left edge (x position).Rect.yThe top edge (y position).Rect.wThe width.Rect.hThe height.Rect.rightThe right edge, x + w.Rect.bottomThe bottom edge, y + h.Rect.centerThe center point as a Vector.Rect.containsTrue when the point is inside.Rect.intersectsTrue when two rectangles overlap.Udp
Plain IPv4 datagrams, polled — the transport under a game's own netcode: peer-to-peer play, a LAN lobby, a STUN query. Open a socket with Udp.open, send with Udp.send, and each frame drain what arrived with Udp.recv, which returns 0 when nothing is waiting and never blocks. The sender of the last datagram read is Udp.from_ip / from_port.
An address is an int: a.b.c.d is (a << 24) | (b << 16) | (c << 8) | d, converted by Udp.ip and Udp.ip_text. Datagrams can be lost, repeated and reordered; the protocol a game builds on top says what to do about that. Like Http.* it is out-of-band and must never feed the deterministic lockstep/replay simulation directly. BSD sockets on macOS, Winsock on Windows.
Udp.openOpen a socket on a port.Udp.portThe port a socket is bound to.Udp.sendSend one datagram.Udp.recvRead the next waiting datagram.Udp.from_ipThe sender of the last datagram read.Udp.from_portThe sender's port.Udp.closeClose a socket.Udp.resolveLook a host name up.Udp.local_ipThis machine's address.Udp.ipParse a dotted address.Udp.ip_textFormat an address.BigInt
Arbitrary-precision integers with no upper bound, for idle/incremental counters, exact huge currencies, and score arithmetic that a 32- or 64-bit int would overflow. A BigInt is a sign-magnitude number stored as base-1e9 limbs, so every operation is exact and therefore deterministic — bit-identical on every platform, with no binary floating point. Build one with BigInt.from (an int) or BigInt.parse (decimal text), combine with add / sub / mul / pow / div / mod, compare with cmp / eq / is_zero, and render with str. Arguments are positional. The runtime is spliced in only when a program mentions BigInt.*.
BigInt.fromTurn a plain int into a BigInt.BigInt.parseParse decimal text into a BigInt.BigInt.addExact sum of two big integers.BigInt.subExact difference of two big integers.BigInt.mulExact product of two big integers.BigInt.negNegate a big integer.BigInt.powRaise a big integer to an int power.BigInt.divDivide a big integer by an int (toward zero).BigInt.modRemainder of a big integer divided by an int.BigInt.cmpCompare two big integers: -1, 0, or 1.BigInt.eqTrue when two big integers are equal.BigInt.is_zeroTrue when a big integer is zero.BigInt.to_intNarrow a big integer to a plain int (clamped).BigInt.strRender a big integer as decimal text.Color — the named palette
Color.Name lowers to a plain 0xRRGGBB integer at compile time — no runtime cost, identical to writing the hex by hand, but readable. 221 names are built in; a custom shade is any hex literal or a named const. The full palette:
Process
Child processes, started and polled — what a launcher is made of. Process.spawn starts a program directly, with no shell, and returns at once; each frame Process.poll answers -1 while the child runs and its exit code once it has ended, so a crash is a non-zero code the game can act on. Process.kill ends a child and Process.free lets a handle go. Pair it with App.window_hide to step aside while the child runs.
The child inherits the environment and the working directory. posix_spawn on macOS; CreateProcessW on Windows, with each argument quoted by the MSVC rules and no console window. Like Http.* it is out-of-band and must never feed the deterministic lockstep/replay simulation.
Decimal
Exact base-10 fixed-point numbers for game economies — prices, balances and taxes on values like 0.10 that binary floating point cannot represent, so they always add up exactly. A Decimal is a BigInt mantissa with a decimal scale (the number of digits after the point), giving unbounded range and exact add / sub / mul. Build one with Decimal.from (an int) or Decimal.parse (text like "19.99"), compare with cmp / eq, change precision with rescale (truncates toward zero), read the current precision with scale, and render with str. Every operation is exact and deterministic. The runtime is spliced in only when a program mentions Decimal.* (or BigInt.*).
Decimal.fromTurn a whole int into a Decimal.Decimal.parseParse decimal text like "19.99".Decimal.addExact sum of two decimals.Decimal.subExact difference of two decimals.Decimal.mulExact product of two decimals.Decimal.negNegate a decimal.Decimal.cmpCompare two decimals: -1, 0, or 1.Decimal.eqTrue when two decimals are equal in value.Decimal.scaleThe number of digits after the point.Decimal.rescaleChange precision (truncates toward zero).Decimal.strRender a decimal as text.Types
Ludic is statically typed; most code uses just int.
intThe default 32-bit signed integer — also how colors, keys, and tiles are carried.fixedQ16.16 fixed-point for deterministic fractional math — no floats.longA 64-bit signed integer for values that overflow a 32-bit int.boolA truth value — the result of comparisons and of and / or / not.floatA 32-bit IEEE floating-point number — what the GPU uses.stringAn immutable string — compared by content, sliceable, and interpolatable.doubleA 64-bit IEEE floating-point number for precise math.entityThe id/handle of a spawned model instance, as returned by self().pointerA raw address into memory — a byte buffer from bytes(n), or an FFI handle.wordsA raw buffer indexed as 32-bit words — each buffer[i] reads or writes an int.byteA raw buffer indexed one byte at a time — each buffer[i] reads or writes a byte.[]T (slices)A growable slice of T — new []T makes one, push appends, len counts, s[i] indexes.fixedsA buffer of fixed-point values — f[i] reads and writes a fixed.countdownAn int component field the engine steps toward 0 once per Update.floatsA buffer of floats (or doubles) — v[i] reads and writes one.pointersA buffer of pointers — p[i] reads and writes a pointer.VectorA 2D vector — two fixed components (x, y), copied by value.IVec2An integer 2D vector — two int components (x, y), copied by value.RectA rectangle — position (x, y) and size (w, h), copied by value.voidThe absence of a value — the return type of a function that returns nothing.Dict
A hash map from string keys to int values — the everyday lookup a game needs, like resource counts or an id registry by name. A Dict is backed by an open-addressing hash table (FNV-1a, linear probing, tombstone deletes, growing at load factor 0.7), so get/set/has are O(1) on average rather than the linear scan a list would give. Create one with Dict.new, then set / get / get_or / has / remove / size / clear / keys. Values are int (which also holds an entity or any small id); for richer values use the Value.* tree. Arguments are positional. The runtime is spliced in only when a program mentions Dict.* (or Set.*).
Dict.newCreate an empty string-keyed map.Dict.setInsert or update a key.Dict.getLook up a key (0 if absent).Dict.get_orLook up a key with an explicit default.Dict.hasIs a key present?Dict.removeDelete a key.Dict.sizeHow many keys are stored.Dict.clearRemove every key.Dict.keysEvery key, as a slice of strings.Operators & tokens
The symbols the grammar recognizes.
ArithmeticAdd, subtract, multiply, integer-divide, and remainder — on int and fixed.ComparisonCompare two values and yield a bool; on strings == compares contents.LogicalThe boolean combinators, spelled as words — never && or || or a bare !.BitwiseBit-level and, or, xor, not, and shifts — with Go-style precedence.RangeA half-open range for numeric for loops — from a up to but not including b.AssignmentStore into a var, a field, or an element — a statement, not an expression.Member & indexField access, element index, and string slice — usable as value or target.String interpolationA backtick string with {expr} holes, each stringified and concatenated.LiteralsInteger, hex, character, string, boolean, and null-pointer literals.CommentEverything after # on a line is a comment, ignored by the compiler.Builtin functions
Global functions available anywhere, beyond the namespaced APIs above.
printPrint an int or string followed by a newline — for headless tests and debugging.stringConvert an int, bool, or fixed value to text; a string passes through unchanged.quitStop the game loop cleanly after the current frame finishes.saveSerialize the whole world — every model instance, property, and program var — in one call.loadRestore the world from a snapshot previously written by save().lenThe number of elements in a slice, or the byte length of a string.pushAppend one element to the end of a growable slice.absThe absolute value of an integer (its magnitude, never negative).clampConstrain a value to the inclusive range [lo, hi].panicAbort with a located error message instead of crashing.assertAbort with a located message when an invariant is false.okWrap a success payload in a result — the happy half of try/else.errWrap a failure message in a result — the sad half, recovered by try/else.is_okTrue when a result carries a success payload.is_errTrue when a result carries a failure.someWrap a present value in an option.noneThe empty option — a missing value.is_someTrue when an option holds a value.is_noneTrue when an option is empty.unwrap_orThe option value, or a fallback if empty.exitTerminate the process immediately with a status code.runRun a string as a shell command.getenvRead an environment variable, returning empty when it is unset.read_charRead one byte from standard input, or -1 at end of input.argThe i-th command-line argument as a string.arg_countThe number of command-line arguments, counting the program name.bytesAllocate a raw buffer of n bytes and return a pointer to it.file_stderrThe standard-error stream handle for use with file_write.file_stdoutThe standard-output stream handle for use with file_write.file_writeWrite a run of raw bytes to a stream handle.fixedConvert an int (or a float) into a Q16.16 fixed-point value.floorFloor a fixed-point value down to the nearest integer.maxThe larger of two integers.minThe smaller of two integers.ui_buildBuild the declared UI tree so it can be opened and rendered.wordsAllocate a raw buffer of n 32-bit words, indexable with [i].floatConvert any number to a float (or double).intConvert a number to a whole number, truncating toward zero.float_bitsA float's IEEE bit pattern as an int, and back.Set
A set of string members — membership tests for tags, unlocked achievements, or visited tiles. A Set shares the same open-addressing hash table as Dict, so add / has / remove are O(1) on average and duplicates are ignored. Create one with Set.new, then add / has / remove / size / clear / members. Arguments are positional. The runtime is spliced in only when a program mentions Set.* (or Dict.*).
Annotations
@Name(...) decorators attach compile-time behavior to a handler, property, or function.
@QueriesDeclare the properties a handler operates on, binding their fields by name in the body.@ComputedA derived field expanded inline wherever it is read, never stored.@OnRegister a compile-time listener that runs whenever an event is emitted.@exportExpose a function as a native symbol so a host can call it.@SyncMark fields as replicated and models as owned so the compiler generates networking.@HandlesDeclarative hint naming the event or subsystem a handler is responsible for.@OnAttachRun a handler when a property is structurally attached to a live instance.@OnDespawnRun a handler when a model instance is torn down, optionally knowing why.@OnDetachRun a handler when a property is structurally detached from a live instance.@OnDisableRun a handler when a property is paused (disabled) on an instance.@OnEnableRun a handler when a paused property is re-enabled on an instance.@OnQuitRun a handler once at shutdown, after the last frame.@OnSpawnRun a handler once each time an instance of a model is spawned.@OnStartRun a handler once at boot instead of assigning it a frame phase.@OwnedGive a model a network owner slot so its instances can be assigned to a peer.@PredictedRun a control handler on the owning client speculatively and on the server authoritatively.@PublicPromote a lifecycle hook to a public event other modules can listen for.@ReadsDeclare that a handler reads a property — an analysis and scheduling hint.@ServerRun a handler only on the authority; clients receive the result via @Sync.@ToClientsA remote event broadcast from the server to clients — a server→clients notification.@ToServerA remote event sent from a client to the server — a client→server request.@WritesDeclare that a handler writes a property — an analysis and scheduling hint.@EngineSystemRegister a package's engine-owned system so the frame loop runs it each phase.@NamespaceLet a package provide a Name.method(…) namespace that dispatches to name_method."@ClearColor"Declare a clear colour so the Render phase auto-clears + auto-presents for you.@SystemRegister a prebuilt binary module's system with the host at load, per phase.Huge
Idle/incremental big numbers — magnitudes far past what a 32- or 64-bit integer can hold, for prestige currencies and exponential growth. A Huge is a normalized mantissa (a Q16.16 fixed in [1, 10)) times 10^exponent, so it can represent 10^100 or 10^1000 while staying one small value. It is a display-scale number (about four significant digits), not a lockstep-exact one — keep it out of the deterministic simulation and reach for BigInt / Decimal when exactness matters. Build one with Huge.from, combine with add / sub / mul / neg, compare with cmp / sign, read mantissa / exp, and render as 1.23e45 with str. Arguments are positional. Spliced in only when a program mentions Huge.*.
Huge.fromTurn an int into a Huge.Huge.addSum of two big numbers.Huge.subDifference of two big numbers.Huge.mulProduct of two big numbers.Huge.negNegate a big number.Huge.cmpCompare two big numbers: -1, 0, or 1.Huge.signThe sign: -1, 0, or 1.Huge.mantissaThe mantissa in [1, 10).Huge.expThe base-10 exponent.Huge.strRender as scientific text like 1.23e45.Angle
An auto-wrapping angle in radians, so you never juggle % TAU by hand. Every Angle.* result is normalized into [-pi, pi), which makes diff the shortest signed rotation between two headings and lerp turn the short way around. Build from degrees with Angle.from_degrees (read back with to_degrees), take sin / cos, combine with add, and normalize any raw value with wrap. It builds on the deterministic fixed-point Math.* trig, so results are bit-identical on every platform. Arguments are positional; angles are fixed radians. Spliced in only when a program mentions Angle.*.
Angle.from_degreesDegrees to a wrapped radian angle.Angle.to_degreesRadians back to degrees.Angle.wrapNormalize any radian value to [-pi, pi).Angle.sinSine of an angle.Angle.cosCosine of an angle.Angle.addAdd two angles (wrapped).Angle.diffShortest signed rotation from a to b.Angle.lerpInterpolate along the shortest arc.Angle.diff_degreesThe signed difference between two headings in whole degrees.Percent
A value clamped to [0, 1] — health fractions, volumes, and the t of an interpolation, without stray values slipping below 0 or above 1. Percent.clamp pins any fixed into range; Percent.of forms a clamped ratio num / den; Percent.lerp interpolates a..b by a clamped t; and Percent.apply scales a value by a clamped fraction. Every operation is deterministic fixed-point. Arguments are positional. Spliced in only when a program mentions Percent.*.
Regex
Regular expressions with PCRE/PECL-compatible syntax over a **linear-time** Thompson NFA (a Pike VM), so a bad pattern from a modder can never trigger catastrophic backtracking — matching is O(n·m), never exponential. Compile a pattern once and reuse it; an invalid pattern is an error value, never a crash. Supported: literals, ., classes [...] with ranges/negation and \d \w \s (and their negations), anchors ^ $, alternation |, capturing and (?:…) groups, and the quantifiers * + ? and {n,m} in greedy or lazy form. Backreferences and look-around are out of scope for a linear engine.
Regex.compileCompile a pattern once for reuse; returns an error value (null) if it is invalid.Regex.validTrue if the pattern is well-formed (compiles without error).Regex.matchesTrue if the pattern matches anywhere in the text (a search).Regex.testLike matches, but reuses an already-compiled Regex.Regex.findThe first match of the pattern in the text (a Match, or null if none).Regex.execLike find, but with an already-compiled Regex.Regex.nextThe next match at or after byte offset from — the basis of a find-all loop.Regex.replaceReplace every match; the replacement expands \0..\9 group references.Regex.groupThe text of capture group n (group 0 is the whole match); empty if unset.Regex.group_countHow many capture groups the match's pattern has.Regex.startThe start byte offset of group n, or -1 if it did not participate.Regex.endThe end byte offset of group n, or -1 if it did not participate.Regex.okTrue if the match succeeded (the Match is non-null).Grid
Tile geometry and pathfinding over the Map tilemap. A cell is passable unless it is out of bounds or holds the caller's wall tile (a char code, e.g. '#'), so any impassable glyph works. Everything is integer and deterministic — same map and query reproduce the same path every run. Grid.line/Grid.flood/Grid.a_star return Cell slices (index them with len / [i]; each cell has .x and .y).
Grid.lineEvery cell a straight line from (x0,y0) to (x1,y1) crosses (Bresenham).Grid.blockedTrue if the cell is out of bounds or holds the wall tile.Grid.line_of_sightTrue if the straight line between two cells crosses no wall.Grid.floodEvery passable cell reachable from (x,y), 4-connected, in BFS order.Grid.a_starThe shortest 4-connected path between two cells (A*), or an empty list.Anim
Spritesheet frame animation off the fixed frame clock. Store an elapsed timer (seconds, a fixed) on a component and each frame ask Anim.frame / Anim.once / Anim.pingpong which cell to draw; Anim.cell_x/Anim.cell_y turn a frame index into a source rectangle on the sheet. Everything is integer/fixed and deterministic — the same timer reproduces the same frame every run, so replays and lockstep netcode match exactly. For the ECS engine-owned SpriteAnim component there is also an ergonomic layer: register named clips with Anim.clip and play them by name with Anim.play, and arm frame events with Anim.on_frame / Anim.fired.
Anim.frameThe looping frame index for an elapsed timer at a given fps.Anim.onceA non-looping frame index that clamps on the last frame.Anim.pingpongA frame index that bounces 0..count-1..0 and repeats.Anim.finishedTrue once a one-shot clip has run past its last frame.Anim.durationSeconds for one full cycle of a clip: count / fps.Anim.cell_xThe source x (pixels) of a frame on a grid spritesheet.Anim.cell_yThe source y (pixels) of a frame on a grid spritesheet.Anim.playStart (or restart) a spritesheet clip on an entity in one call.Anim.clipRegister a named spritesheet clip so Anim.play can play it by name.Anim.on_frameArm a frame event — the engine flags the tick a clip lands on this frame.Anim.firedDid the entity's clip land on its armed frame event this tick?Motion
Ergonomic control of the engine-owned Motion component — the value tween the engine advances each tick (see the ECS engine-systems). Motion.to starts a tween on an entity from one value to another over a number of ticks with an easing curve, rewinding it so it plays from the start, instead of setting the component's from/to/dur/ease fields by hand. The tween is integer and deterministic, so motion reproduces exactly under replay and lockstep. For a standalone, sequenced tween not tied to a component, see the fluent Tween handles. Related: Anim, Ease, Tween.
Tween
Value interpolation over a timeline, off the fixed frame clock. The timeline helpers Tween.progress/Tween.loop/Tween.yoyo turn an elapsed timer and a duration into a normalized amount; Tween.ease shapes that amount through an easing curve (shared with Ease); and the typed blends Tween.number/Tween.round/Tween.point/Tween.tint interpolate a fixed, int, Vector, or color. All deterministic fixed-point, so a replay reproduces every eased value exactly. Beyond these pure interpolators there are fluent handles the engine advances for you: Tween.to starts one and returns a handle, Tween.chain and Tween.delay sequence more segments, and Tween.value / Tween.done / Tween.parallel / Tween.stop read and control them.
Tween.progressA one-shot 0..1 amount, clamped, for timer over duration.Tween.loopA repeating 0..1 sawtooth amount over duration.Tween.yoyoA repeating 0..1..0 triangle amount over duration.Tween.doneTrue once a one-shot tween's timer reaches its duration.Tween.easeShape a 0..1 amount through an easing curve chosen by a literal mode.Tween.numberLinear blend of two fixeds by amount t.Tween.roundLinear blend of two ints by amount t, rounded to the nearest int.Tween.pointComponent-wise blend of two Vectors by amount t.Tween.tintPer-channel blend of two colors by amount t.Tween.toStart a fluent, engine-advanced tween and return a handle.Tween.chainAppend a tween segment that runs after the handle's current queue.Tween.delayAppend a pause to a tween handle's sequence.Tween.valueThe current value of a fluent tween handle this frame.Tween.stopStop and dispose a tween handle, freezing its value.Tween.parallelAre two tween handles both finished? — a parallel completion query.Query
Spatial and set queries over the live entities that carry a property, built on the World reflection ABI. Query.count and Query.first ask how many bearers there are and which is first; Query.nearest and Query.within add a position — read from a coordinate property's two int fields — to find the closest entity to a point or every entity inside a radius. A property id comes from World.prop_id and a field id from World.field_id. Everything is integer and deterministic: the same world reproduces the same answers, entity order included, every run. The scan is linear over the entity table — ample for the entity counts Ludic targets — and a game that uses Query.* gets the reflection ABI emitted automatically, no @event required.
Reflect
Runtime reflection over the world schema — inspect properties, fields, and entity state by name and by index. It powers the conveniences game devs consume without writing reflection code: automatic save/load, data-driven tools, and debug/inspector overlays. Enumerate with Reflect.prop_count/Reflect.prop_name and Reflect.field_count/Reflect.field_name/Reflect.field_type; resolve ids with Reflect.prop/Reflect.field; read and write an entity's fields with Reflect.get/Reflect.set/Reflect.has; and identify its model with Reflect.kind/Reflect.model. The metadata tables are generated at compile time (the same reflection ABI a foreign mod binds), so introspection is a table walk, not heavy runtime machinery. This is an advanced/tooling surface — most developers get its benefits through built-in features. Related: World, Query.
Reflect.propThe property id for a property name, or -1 if unknown.Reflect.fieldThe field id of a named field within a property, or -1.Reflect.prop_countHow many properties the world schema defines.Reflect.prop_nameThe name of the property at an index (or "").Reflect.field_countHow many fields a property has.Reflect.field_nameThe name of the field at an index within a property.Reflect.field_typeThe type name of the field at an index within a property.Reflect.getRead one field of an entity by (prop, field) id.Reflect.setWrite one field of an entity by (prop, field) id.Reflect.hasDoes an entity carry a property?Reflect.kindThe model id of an entity.Reflect.modelThe model id for a model name, or -1 if unknown.Reflect.serializeWalk an entity's components into a value tree.Reflect.applyWrite a value tree's fields back into an entity.Camera
A world-space camera — a draw offset threaded through the render path (the same offset Screen.camera sets). Camera.set places it, Camera.follow eases it toward a target, and Camera.shake jitters it from the seeded RNG for impact and explosions. Everything is integer and deterministic — driven off the same seed and inputs, a replay reproduces the exact camera path, shake included. The camera moves everything drawn; reset it to (0, 0) to draw a fixed HUD.
Camera.setPlace the camera at a world-space offset.Camera.followEase the camera toward centring a target point.Camera.shakeJitter the camera by up to +/- amount pixels (seeded RNG).Camera.zoomScale the whole view about the screen centre by a Q16.16 factor (1.0 = none).Camera.shake_forShake for a number of frames, then stop, with no bookkeeping.Light
A software 2D light-accumulation pass over the framebuffer, run in a render phase after drawing the scene and before Screen.show. Light.ambient multiplies the whole scene toward a tint — the night/cave modulate that darkens everything so lights add mood back on top. Light.point accumulates a radial glow that falls off with distance and clamps per channel, and Light.occlude registers rectangles that block a light's rays to cast hard shadows (cleared each frame with Light.clear_occluders). Lighting is a rendering concern only — it never touches game state or replays — and it is fully deterministic (integer and Q16.16 fixed), so the same scene lights identically every run and in a headless render, keeping screenshots diffable. Colours are 0x00RRGGBB. Beyond the radial core, the pass carries the render-quality tiers: Light.spot cones, a Light.falloff exponent, Light.soft shadows (penumbra), Light.gel colour cookies, normal-mapped surfaces (Light.normal + Light.height) that shade by facing, and a Light.time_of_day day/night ramp — every one deterministic. This is the imperative surface; the engine also consumes Light2D/Occluder components automatically (a Light2D may carry optional direction/spread/falloff/softness/gel fields). Related: Screen, Color, Camera.
Light.ambientMultiply the whole scene by a tint — the night/cave modulate.Light.pointAdd a radial glow that falls off with distance.Light.occludeRegister a rectangle that blocks light — a hard shadow caster.Light.clear_occludersForget every occluder — call once per frame before re-registering.Light.spotA cone / flashlight light aimed at a direction with a half-angle spread.Light.falloffSet the brightness-ramp exponent for later lights (1 linear, 2 quadratic…).Light.softSoft shadows — an occluder edge fades through a penumbra of this radius.Light.gelA colour cookie — later lights gel from their centre colour to this rim colour.Light.clear_gelClear the gel — later lights are a flat single colour again.Light.normalStamp a surface normal over a rectangle so lights shade it by facing (N·L).Light.clear_normalsForget every stamped normal — call once per frame before re-stamping.Light.heightSet the virtual height of lights above the surface, for normal-map shading.Light.time_of_daySet the ambient tint from a 0..1 time-of-day — a day/night cycle in one value.Sprite
A namespaced spritesheet / atlas API. Sprite.sheet loads one image and remembers its cell grid; Sprite.cell addresses a cell by grid coords and Sprite.cell_span a sprite that spans more than one cell; Sprite.define / Sprite.named name and look up a cell; Sprite.draw / Sprite.draw_scaled blit it (through the camera / zoom / clip). It stands on the variable-size image loader, so a cell can be any size — not just 16x16. (Distinct from the Sprite engine component of the sprite-render system.)
Sprite.sheetLoad a spritesheet and remember its cell grid; returns a sheet handle.Sprite.cellOne cell of a sheet, by grid coords; returns a sprite id.Sprite.cell_spanA sprite spanning cols x rows cells (tall/wide art).Sprite.defineName a cell for later lookup; returns its sprite id.Sprite.namedLook up a named sprite's id, or -1.Sprite.drawBlit an atlas sprite at (x, y) through the camera/zoom/clip.Sprite.draw_scaledBlit an atlas sprite scaled by an integer factor.Sprite.widthThe atlas sprite's width in pixels.Sprite.heightThe atlas sprite's height in pixels.Sprite.stripA run of animation frames as consecutive sprite ids.Sprite.draw_meterA value as a row of full / half / empty icons.Assets
Loading and lookup for whole-file assets, paired with the Sprite.* atlas API. Assets.image (and its alias Assets.load) loads a whole image file as one sprite; Assets.get looks a named sprite up.
Assets.imageLoad a whole image file as one sprite; returns its id.Assets.loadAlias of Assets.image.Assets.getLook up a named sprite's id (alias of Sprite.named).Assets.enqueueQueue a named image file to preload later.Assets.pumpLoad up to max queued assets this frame; returns how many it loaded.Assets.totalHow many assets are enqueued.Assets.loadedHow many enqueued assets have loaded so far.Assets.ready1 once every enqueued asset has loaded.Assets.fontA font loaded through the preload queue, by name.Assets.progressLoading progress as a 0..100 percent.Assets.enqueue_dirEnqueue every file of a directory, named by its file name.Testing
A built-in testing framework in the spirit of Go's go test: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A test "name" { … } block is discovered automatically and run by a synthetic entry point that prints ok - name or FAIL - name for each, a == N passed, M failed == summary, and exits non-zero if anything failed (so CI and the bin/ludic runner catch it). Inside a test, the expect, expect_eq and expect_near assertions check a condition and, on failure, print file:line: … failed (got …, want …) and mark the test failed — without aborting, so one run reports every failure. expect_near takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: function, print.
Pool
Entity-pool statistics. Ludic's ECS is already pool-based: the allocator recycles freed entity slots through a freelist (a despawned slot is reused by the next spawn before any new slot is taken), and component storage is fixed per-entity arrays — so spawning and despawning many entities per frame does no per-spawn heap allocation and cannot fragment. Pool.live / Pool.free / Pool.reserved / Pool.capacity read those counters so a bullet-hell or horde game can watch reuse and budget against the cap.
Value
A generic, self-describing value tree — the node type reflection serializes into and JSON round-trips through. A node is one of null, int, fixed, bool, str, list, or object (see Value.kind). Build one with the constructors (Value.int, Value.object, …) and the builders Value.add/Value.put; read it with Value.get/Value.at/Value.count and the as_* accessors. Related: Json, Reflect.
Value.nullAn empty null node.Value.intWrap an int in a value node.Value.fixedWrap a fixed in a value node.Value.boolWrap a bool in a value node.Value.strWrap a string in a value node.Value.listAn empty list node.Value.objectAn empty object node.Value.addAppend an item to a list; returns the list.Value.putSet a key on an object; returns the object.Value.getRead a member of an object by key.Value.hasWhether an object has a member.Value.atRead a list item by index.Value.key_atThe key of an object member by position.Value.countNumber of items/members in a list or object.Value.kindThe node's kind tag.Value.as_intRead a scalar node as an int.Value.as_strRead a str node's text.Json
The text bridge over the Value tree: Json.encode turns a value tree into compact, stable JSON text and Json.parse reads it back. Together with Reflect.serialize/Reflect.apply this is a one-call, bit-exact save/load for entities, and a diffable on-disk format for tooling. Determinism holds: the same tree always encodes to the same bytes.
Job
Background work that stays out of the frame. A Job is a future — a handle to a result that lands later. Kick one off with Job.run (a background compute that advances a little each Job.pump and finishes after enough frames, so heavy work never hitches) or Job.defer (a future you resolve yourself with Job.fulfill / Job.fail). Poll it with done / ok / failed / cancelled, read result / error, and always collect on the main thread — a Job must never touch the ECS world directly. The scheduler is deterministic and cooperative, so the same jobs and the same budget reproduce byte-for-byte, every run and every target. For work that should use every core now, Job.parallel_for(count, fn work, ctx) runs work(i, ctx) across a pool of real OS threads and returns when all of it is done; a worker computes on what it was handed and never changes the world. Arguments are positional. Spliced in only when a program mentions Job.*.
Job.deferA future you resolve yourself later.Job.runStart a background compute job.Job.fulfillResolve a pending job with a value.Job.failResolve a pending job as failed.Job.cancelCancel a job before it finishes.Job.pumpAdvance background jobs; collect results.Job.doneHas the job resolved (any outcome)?Job.okDid the job succeed?Job.failedDid the job fail?Job.cancelledWas the job cancelled?Job.resultThe success value of a done job.Job.errorThe error code of a failed job.Job.pendingHow many jobs are still unresolved.Job.freeRelease a job slot back to the pool.Job.parallel_forRun work(i, ctx) for every i in [0, count) across all cores; returns when every call is done.Job.is_workerTrue on a Job.parallel_for pool thread, false on the main thread.Promise
Combine several Job futures and resolve the group on the main thread. Promise.all succeeds once every member has, Promise.race once the first does; both return an ordinary job handle you poll like any other. For a loading screen, Promise.count_done over the same handles is the bar's numerator and len the denominator, and Promise.all_done is the ready check. Ludic has no closures, so progress is polled rather than chained through a then callback. Build the handle list with new []int + push. Spliced in only when a program mentions Promise.*.
Sync
The advanced, opt-in tier — here be dragons. Raw building blocks for engine-level systems that pass data around: a mutex (cooperative lock), an atomic counter (get / set / add / cas) and a bounded channel (send / recv / can_recv / len). On today's single-threaded deterministic runtime these are cooperative — correct, ordered, replayable and impossible to deadlock — and exist so message-passing code reads the same now as it will when a preemptive OS-thread backend lands behind this same API. Beginners never need this; reach for Job.* / Promise.* instead. Spliced in only when a program mentions Sync.*.
Sync.mutexCreate a cooperative lock.Sync.lockTake the lock.Sync.unlockRelease the lock.Sync.try_lockTake the lock only if it is free.Sync.atomicCreate an atomic counter (starts at 0).Sync.getRead the counter.Sync.setStore a value in the counter.Sync.addAdd to the counter; return the new value.Sync.casCompare-and-set the counter.Sync.channelCreate a bounded int FIFO channel.Sync.sendEnqueue a value (false if full).Sync.recvDequeue the oldest value.Sync.can_recvIs there a value waiting?Sync.lenHow many values are queued.Sync.cpu_countThe machine's logical cores (1 to 64); Job.parallel_for uses one thread per core.Xml
A minimal, deterministic XML reader for the element/attribute/CDATA subset the native Tiled formats (TMX/TSX/TX) use. Xml.parse turns a document into an element tree; the accessors read a node's tag, text, attributes (Xml.attr/Xml.attr_int/Xml.has) and children (Xml.child/Xml.find/Xml.count). It expands the five predefined entities and numeric character references, skips the <?xml?> prolog, comments and <!DOCTYPE>, and is best-effort rather than validating — the same contract as Json. Spliced on demand when a program mentions Xml.*.
Xml.parseParse XML text into an element tree.Xml.tagThe element's tag name.Xml.textThe element's character data.Xml.attrAn attribute's value by name.Xml.attr_intAn attribute parsed as an integer.Xml.hasWhether an attribute is present.Xml.attr_countHow many attributes the element has.Xml.child_countHow many child elements.Xml.childA child element by index.Xml.findThe first child with a given tag.Xml.countHow many children have a given tag.Base64
Standard base64 (RFC 4648) — Base64.decode reads base64 text into the bytes it stands for, and Base64.encode is the inverse. The decoder ignores ASCII whitespace, so it reads the newline-wrapped base64 that Tiled writes inside a <data> element; that output then feeds the DEFLATE inflater for zlib/gzip-compressed layer data. Deterministic; spliced on demand when a program mentions Base64.*. (The security-sensitive encoder Crypto.base64 is a separate, hardened path.)
Tiled
Load and draw Tiled maps. Both native format families — TMX/TSX/TX (XML) and TMJ/TSJ/TJ (JSON) — read onto one intermediate, a Value tree in Tiled's JSON schema, with layer data decoded to a dense GID list; so a CSV .tmx and a base64+zlib .tmj of the same map read identically. Tiled.read returns that intermediate tree for a map file and Tiled.read_tsx for a tileset file. Spliced on demand when a program mentions Tiled.*, alongside the Xml, Base64 and Value runtimes it builds on.
Tiled.readRead a map file into the intermediate value tree.Tiled.read_tsxRead a tileset file into a value object.Tiled.loadLoad a map file into the runtime map model.Tiled.gidThe raw GID at a cell in a tile layer.Tiled.resolveDecode a GID into tileset, local id and flip flags.Tiled.widthThe map width in tiles.Tiled.heightThe map height in tiles.Tiled.layer_countHow many layers the map has.Tiled.layer_nameA layer's name by index.Tiled.drawDraw every visible tile layer.Tiled.projectProject a collision layer to the byte tilemap.Tiled.treeThe underlying intermediate value tree.Tiled.tile_propWhether a GID's tile carries a bool property.Tiled.collideDrive collision from per-tile metadata.Tiled.collision_kindClassify a GID's collision from its tile metadata.Tiled.tile_shapesWhether a GID's tile has a collision shape.Tiled.draw_animDraw with animated tiles advanced to a frame.Tiled.frame_gidThe current GID of an animated tile at a frame.Tiled.animatedWhether a GID's tile is animated.Tiled.object_countHow many objects an object layer holds.Tiled.objectAn object on an object layer by index.Tiled.object_shapeClassify an object's shape.Tiled.propA custom property's value, with class-default fallback.Tiled.prop_intA custom property parsed as an integer.Tiled.prop_typeA custom property's declared type.Tiled.load_typesLoad a project custom-type table.Tiled.templateRead a template file into an object value.Tiled.spawnSpawn a Ludic entity from an object.Tiled.spawn_layerSpawn every object on a layer.Tiled.cell_xThe screen x of a tile cell for the map orientation.Tiled.cell_yThe screen y of a tile cell for the map orientation.Tiled.layer_kindA layer's kind.Tiled.layer_opacityA layer's opacity.Tiled.layer_tintA layer's tint colour.Tiled.layer_offsetxA layer's horizontal offset.Tiled.layer_offsetyA layer's vertical offset.Tiled.worldRead a .world file.Tiled.world_countHow many maps a world stitches.Tiled.world_mapA world member map by index.Ui
The retained-mode menu API over a ui block. Ui.build constructs every declared widget tree once (after fonts and skins are loaded); Ui.open makes one menu active and Ui.close deactivates it; the frame loop ticks navigation on its own, an activation fires the UiClicked event, and Ui.clicked is the polled form; Ui.set_text updates a label or button, and Ui.render draws the active menu (call it from an Overlay handler so it paints over the world). Every id: Name in a ui block mints a UI_Name handle.
Ui.buildConstruct every declared ui block (loads skins, measures fonts).Ui.openMake one menu active and focus its first button.Ui.closeDeactivate the menu: no menu is open.Ui.tickAdvance navigation by one key (the frame loop does this for you).Ui.clickedWas this control activated this frame? (polled form of UiClicked)Ui.set_textReplace a label's or button's text.Ui.renderDraw the active menu.File
Raw file access over the C stdio calls, for programs that read and write their own formats. File.open returns a handle (or null), File.read / File.write move bytes through a bytes buffer, File.seek / File.tell position the cursor, and File.close releases the handle. For text and structured data prefer the higher-level Fs, Json and Prefs APIs.
File.openOpen a file (fopen); null when it cannot be opened.File.readRead up to n bytes into a buffer; returns the bytes read.File.writeWrite n bytes from a buffer; returns the bytes written.File.seekMove the file cursor (0 start, 1 current, 2 end).File.tellThe cursor position in bytes.File.closeClose a handle from File.open.Font
TrueType fonts for the retained UI and for Screen.draw_text-style drawing. Font.load reads a .ttf / .ttc file and returns a handle that a ui block's font: property or a text call takes.
Fx
Engine-owned transient effects. A game asks for a burst of sparks or a floating number and the engine owns the rest: it moves and ages them every Update, draws them every Render after the sprites (through the camera, shake and clip), and drops them when they expire. Nothing is an entity, so a hit effect needs no component, model, handler or draw call. Velocities and lifetimes come from the seeded RNG, so a replay produces the same sparks.
Prefab
A prefab is a model with preset component fields — prefab Grunt: Creature { Stats { hp: 30 }, Weapon { def_id: 1 } } — spawned with spawn Grunt { Position { x: 40 } }, where the spawn's own fields override the presets. Prefabs chain (prefab Grunt: Foe, where Foe is itself a prefab) so shared presets live once. spawn is also an expression yielding the new entity (let e = spawn Grunt { … }), and Prefab.spawn spawns a prefab chosen at runtime by name.
App
The running application, as distinct from its window. Today that is the boot splash: the runtime raises it before main from the game's asset pack, and App.splash_hide takes it down when the game has something to show instead. App.window_hide and App.window_show take the game's own window off the screen and bring it back without closing it — a launcher stepping aside while the game it started runs.
Gl
OpenGL for Ludic. Gl.* binds the whole OpenGL 4.1 core API: every gl* entry point of the platform gl3.h is a method named by its snake case (glBindBuffer → Gl.bind_buffer, glTexImage2D → Gl.tex_image2d, glDrawElementsInstanced → Gl.draw_elements_instanced), with every GL_* constant available as written. Parameters keep the header's names as labels (glVertexAttribPointer's pointer is called offset, program is prog); GLfloat/GLdouble parameters take fixed, pointer parameters take the raw bytes/words buffers Ludic already has, and 64-bit sizes take long (an int widens). The binding is generated by ludic-dev glgen from the header, with one ABI thunk per entry point.
A context comes from Gl.open(width, height, title): windowed, an NSOpenGLContext on the game's window at the display's backing resolution (2× on Retina — Gl.width()/Gl.height() are the drawable's pixels); headless, an offscreen context with a framebuffer standing in for the screen (Gl.screen_fbo()), so the same program renders and screenshots under the test harness. Gl.swap() presents, Gl.screenshot(path) writes the screen as a PPM, Gl.check(tag) prints any pending error.
The glue: Gl.program(vs, fs) / Gl.program5(vs, tcs, tes, gs, fs) compile and link (logs on failure), Gl.uniform(prog, name), Gl.vao(), Gl.buffer(), Gl.texture(), Gl.framebuffer() allocate, and float data is filled from Q16.16 with Gl.floats(n) / Gl.put(buf, i, v) / Gl.bytes_of(n) or from IEEE bits with Gl.put_bits; Gl.f32(v) and Gl.fixed(bits) convert single values. The bare f_add/f_mul/… helpers do IEEE single-precision arithmetic on those bit patterns for programs that want real floats on the CPU.
Using Gl.* links gl.ll, the thunks and OpenGL.framework; a program that does not is byte-identical to before. The ludic.render3d package is a physically based 3D renderer written on this surface (see examples/rendering/smooth.ludic here, and Maroon Lake for a game built on it).
``ludic
program Triangle {
property Marker { on: int = 1 }
model Anchor { Marker }
var prog: int = 0
var vao: int = 0
handler Boot phase Start {
spawn Anchor {}
if not Gl.open(width: 640, height: 360, title: "GL") { quit() }
prog = Gl.program(vs: "#version 410 core\nvoid main(){ gl_Position = vec4(float(gl_VertexID == 1) * 2.0 - 0.5, float(gl_VertexID == 2) * 2.0 - 0.5, 0.0, 1.0); }\n",
fs: "#version 410 core\nout vec4 o; void main(){ o = vec4(1.0, 0.5, 0.2, 1.0); }\n")
vao = Gl.vao()
}
handler Draw phase Render {
Gl.clear_color(red: 0.1, green: 0.1, blue: 0.15, alpha: 1.0)
Gl.clear(mask: GL_COLOR_BUFFER_BIT)
Gl.use_program(prog: prog)
Gl.bind_vertex_array(array: vao)
Gl.draw_arrays(mode: GL_TRIANGLES, first: 0, count: 3)
Gl.swap()
}
}
``
Vk
Vulkan for Ludic. Vk.* binds Vulkan 1.0–1.4 and the extensions a modern renderer is built around — swapchain and HDR colour spaces, ray query and acceleration structures, opacity micromaps, mesh shaders, variable rate shading, memory budget, pipeline libraries, NVIDIA low latency, and portability enumeration for MoltenVK. Every command is a method named by its snake case (vkCreateInstance → Vk.create_instance, vkCmdDrawIndexedIndirectCount → Vk.cmd_draw_indexed_indirect_count), every VK_* constant is available as written, and every struct has its size and field offsets as constants: VkDeviceCreateInfo_sizeof, VkDeviceCreateInfo_queueCreateInfoCount. The binding is generated by ludic-dev vkgen from the Vulkan registry (vk.xml), and every size and offset is checked against the SDK's C headers.
A struct is plain memory filled by field name: bytes(Vk…_sizeof), Vk.zero(p, n), then Vk.put_i32 / Vk.put_i64 / Vk.put_ptr(p, offset, value), read back with Vk.get_i32 / get_i64 / get_ptr, and Vk.at(p, offset) for a nested struct or an inline array. Numbers follow the C types: a float is its IEEE bit pattern in an int, and uint64_t, VkDeviceSize and non-dispatchable handles are long. A negative long has to come from a long variable — an int literal passed straight to a long parameter is zero-extended.
The loader is opened at run time by Vk.open(), never linked: vulkan-1.dll on Windows, libvulkan.1.dylib (the LunarG loader and MoltenVK) on macOS — beside the executable, in the library paths, or under $VULKAN_SDK. It returns 0 on a machine without Vulkan, and the program carries on; Vk.has(name) says whether one command is there. Using Vk.* links the generated thunks and the loader; a program that does not is unchanged. See examples/rendering/vk_probe.ludic (what a machine's Vulkan can do) and vk_compute.ludic (a Slang compute shader, dispatched and read back).
``ludic
program Probe {
property Marker { on: int = 1 }
model Anchor { Marker }
handler Boot phase Start {
spawn Anchor {}
if Vk.open() == 0 { print("no Vulkan here"); quit() }
let app = bytes(VkApplicationInfo_sizeof)
Vk.zero(app, VkApplicationInfo_sizeof)
Vk.put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO)
Vk.put_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12))
let ci = bytes(VkInstanceCreateInfo_sizeof)
Vk.zero(ci, VkInstanceCreateInfo_sizeof)
Vk.put_i32(ci, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO)
Vk.put_ptr(ci, VkInstanceCreateInfo_pApplicationInfo, app)
let out = bytes(8)
if Vk.create_instance(ci, null, out) == VK_SUCCESS {
print("a Vulkan instance")
Vk.destroy_instance(Vk.get_ptr(out, 0), null)
}
quit()
}
}
``
Phases
StartRuns once at boot, before the game loop — the place to set up initial state.InputThe first per-frame phase — read the keyboard and record the player's intent.FixedUpdateThe deterministic simulation step — physics and gameplay meant to be reproducible.UpdateThe ordinary per-frame game-logic step, run after Input and FixedUpdate.LateUpdateRuns each frame after Update and before Render.RenderThe last per-frame phase — draw the world, then call Screen.show() once.OverlayThe HUD pass — runs after Render and after every engine-owned drawing system.