Engine API
The engine registers host function modules that scripts use to interact with
graphics, audio, input, events, persistence, windowing, and more. Each module is
imported with access <name> and exposes functions via dot notation
(e.g. gfx.fill_screen).
sys
access <sys>
System-level operations.
| Function | Args | Returns | Description |
|---|---|---|---|
sys.warp(target) | symbol or text | void | Request a scene/mode transition. Symbols become "sym:id". The target is placed into side effects for the engine or runner to act on. |
sys.exit() | – | void | Request engine shutdown. The game loop exits after the current tick completes. |
sys.is_headless() | – | bool | active if the process was built with EngineConfig::headless, else dormant. Same source of truth as the headless / tick-only driver (no separate ns-network dependency). |
sys.runtime_role() | – | symbol | :offline_singleplayer, :client, or :server, from EngineConfig::runtime_role at startup; immutable for the process. |
Example
access <sys>
when game_over {
sys.warp(:title_screen)
}
when should_quit {
sys.exit()
}
log
access <log>
Logging to stderr with severity prefixes.
| Function | Args | Returns | Description |
|---|---|---|---|
log.info(...) | variadic | void | Log with [ep:info] prefix |
log.warn(...) | variadic | void | Log with [ep:warn] prefix |
log.error(...) | variadic | void | Log with [ep:error] prefix |
log.debug(...) | variadic | void | Log with [ep:debug] prefix |
Arguments are formatted and joined with spaces.
Example
access <log>
log.info("Player HP:", hp, "/ ", max_hp)
log.warn("Low health!")
log.error("Failed to load asset:", path)
input
access <input>
Input handling via a command queue. The engine pushes input events as symbols each tick. Scripts consume them by polling.
| Function | Args | Returns | Description |
|---|---|---|---|
input.poll() | – | symbol or void | Pop the next input command from the queue. Returns void if empty. |
input.peek() | – | symbol or void | View the next command without consuming it. |
input.held(key) | text | bool | Whether key is currently held down. |
input.count() | – | int | Number of queued commands. |
input.bind(key, command) | text, text | void | Bind a key name to a command name. |
input.unbind(key) | text | void | Remove a key binding. |
Example
access <input>
phase tick(dt) {
sustain cmd = input.poll() {
inspect cmd {
:left => { x = x - speed * dt }
:right => { x = x + speed * dt }
:confirm => { do_action() }
}
}
when input.held("shift") {
speed = run_speed
}
}
Key Bindings
Key bindings map physical key names to command symbols. They can be set in
game.toml under [keys] or at runtime via input.bind:
input.bind("ArrowLeft", "left")
input.bind("ArrowRight", "right")
input.bind("z", "confirm")
input.bind("x", "cancel")
Input vs event queues
The input queue is filled with key-edge commands and cleared by the engine after each tick batch so edges do not carry across frames. Use it only for device-derived commands.
The event queue is a separate FIFO for game or engine signals. It stores
full values—usually symbols, optionally with a metadata payload (the same
Symbol(id, payload) shape as in the language). Entries stay queued until a
script polls them or calls event.clear(); the engine does not
flush this queue at tick boundaries.
From Rust, call EpitaphRuntime::push_event, push_event_value,
push_event_payload, and clear_event_queue (see
crates/ns-core/src/scripting.rs).
event
access <event>
Game/engine event mailbox: a FIFO of values (typically :tag or :tag(data)).
Unlike input, this queue is not cleared automatically each tick.
| Function | Args | Returns | Description |
|---|---|---|---|
event.poll() | – | value or void | Remove and return the next event. void if empty. |
event.peek() | – | value or void | Next event without removing it. |
event.count() | – | int | Number of queued events. |
event.clear() | – | void | Discard all queued events. |
event.emit(tag) | symbol | void | Enqueue that symbol (including any payload on the literal, e.g. :err("msg")). |
event.emit(tag, meta) | symbol, any | void | Enqueue tag with meta as the symbol payload (second form overwrites a literal payload). |
Unknown symbol names passed to Rust push_event / push_event_payload are
silently ignored, matching input command behavior.
Example
access <event>
phase tick(dt) {
sustain ev = event.poll() {
inspect ev {
:door_open(id) => { open_door(id) }
:quest_update => { refresh_quest_ui() }
_ => {}
}
}
}
mouse
access <mouse>
Pointer state for the current outer display frame (one snapshot per loop
iteration, before the tick batch). Coordinates are in buffer pixels (the
same space as the framebuffer passed to the shell). Logical coordinates
match gfx / engine.screen_*: mouse.logical_x() ≈ mouse.x() / engine.scale().
| Function | Args | Returns | Description |
|---|---|---|---|
mouse.x() | – | float | Horizontal position in buffer pixels. |
mouse.y() | – | float | Vertical position in buffer pixels. |
mouse.logical_x() | – | float | X in logical screen units. |
mouse.logical_y() | – | float | Y in logical screen units. |
mouse.inside() | – | bool | Whether the pointer maps inside the buffer rectangle. |
mouse.down(name) | text | bool | "left", "right", or "middle" held. |
mouse.pressed(name) | text | bool | Same names; true only on the frame the button went down. |
mouse.scroll_x() | – | float | Horizontal scroll delta this frame (platform units). |
mouse.scroll_y() | – | float | Vertical scroll delta this frame. |
The reference ns-shell-minifb shell maps the OS
cursor into buffer space using the window size vs buffer size.
gamepad
access <gamepad>
Connected pads appear in stable gamepad-id order; index i runs from 0
to gamepad.count() - 1 when using ns-shell-minifb with the gamepad Cargo
feature enabled. Without that feature, gamepad.count() is always 0.
| Function | Args | Returns | Description |
|---|---|---|---|
gamepad.count() | – | int | Number of gamepad slots in the current snapshot. |
gamepad.connected(i) | int | bool | Whether slot i has a connected controller. |
gamepad.axis(i, name) | int, text | float | Stick/trigger axis: lx, ly, rx, ry, lt, rt (triggers 0..1). |
gamepad.down(i, name) | int, text | bool | Named button held (see table below). |
gamepad.pressed(i, name) | int, text | bool | Button edge this frame. |
Button names (gilrs / Xbox-style layout): a, b, x, y, lb, rb,
lt_btn, rt_btn, select, start, guide, lstick, rstick, up, down,
left, right.
Enable gamepads in your game Cargo.toml:
ns-shell-minifb = { path = "../ns/crates/ns-shell-minifb", features = ["gamepad"] }
gfx
access <gfx>
Graphics rendering. All draw calls queue render commands that are executed
during game.draw. Draw calls made during game.init or game.tick are
not rendered.
Color Arguments
Many gfx functions accept colors in two forms:
- Color record: A record with
r,g,b,afields (fromcolor.rgb,color.rgba, orcolor.hex). - Three integers:
r, g, bas separate arguments with alpha defaulting to 255.
Functions
| Function | Args | Returns | Description |
|---|---|---|---|
gfx.fill_screen(color) | color record, or r, g, b | void | Fill the entire screen with a solid color. |
gfx.fill_rect(x, y, w, h, color) | ints + color record, or x, y, w, h, r, g, b | void | Fill a rectangle. |
gfx.draw_text(text, x, y, color?, font?, scale?) | see below | void | Draw text at position. Optional scale is dot size in buffer pixels (int or float, may be fractional). |
gfx.draw_image(handle, x, y, img_scale?) | int, int, int, optional int/float | void | Draw a loaded image at logical (x, y). Optional img_scale: buffer pixels per source pixel for this draw only (default: engine scale). Clamped to 1..64. Logical placement still uses engine scale; a smaller img_scale draws a smaller bitmap (e.g. engine scale 4 and img_scale 2 halves pixel footprint vs omitting the argument). |
gfx.draw_image_crop(handle, x, y, sx, sy, sw, sh, img_scale?) | 7 ints + optional int/float | void | Cropped blit; optional 8th img_scale same semantics as draw_image. |
gfx.push_clip(x, y, w, h) | 4 ints | void | Push a clipping rectangle onto the stack (logical coords, same as fill_rect; multiplied by engine scale to buffer pixels). Intersects with the current clip. |
gfx.pop_clip() | – | void | Pop the most recent clipping rectangle. On flush, the active clip limits fill_rect, draw_text, draw_image, draw_image_crop, and draw_tilemap; fill_screen is not clipped. |
gfx.screen_width() | – | int | Logical screen width. |
gfx.screen_height() | – | int | Logical screen height. |
gfx.set_font(font) | symbol | void | Set the default font. :small for small font, :normal for normal. |
gfx.load_font(path) | string | bool | Load a .epf packed font from the raw asset store (mount .epf files or register bytes). Lookup matches assets.load_image–style lazy resolution: exact path, then path + ".epf" when path does not already end in .epf. Returns true on success. Returns false (falsy) if the argument is missing, not a string, empty, no raw entry matches, or decode fails (decode errors are logged); the active font is unchanged in those cases. |
gfx.reset_font() | – | void | Clear the loaded .epf font and use the built-in bitmap font again. |
gfx.set_text_outline(color?) | color record, r,g,b, or false | void | Enable a 1-pixel cardinal outline (shadow) behind all subsequent draw_text calls. Pass false or no arguments to disable. The outline is drawn at 4 offsets (up/down/left/right) in the given color before the foreground glyph. |
gfx.set_text_edges(mode?) | symbol or nothing | void | Controls how alpha .epf glyphs are drawn (1-bit fonts are always crisp). :crisp — threshold alpha (≥128) to solid pixels, no soft fringe. :smooth — alpha blending (default behavior for alpha fonts). :auto or no arguments — follow the font (alpha → smooth, 1-bit → crisp). Requires symbols crisp, smooth, and auto to be registered (use :crisp, etc. in scripts). |
Custom .epf fonts affect gfx.draw_text glyph rendering and advances. Kerning and complex scripts are not supported—monospace-style bitmap cells only.
Scripting: gfx is a host global (gfx.load_font, …), not a script-backed access module. Avoid a fragment gfx or a top-level phase gfx.… whose name collides with a host call (e.g. phase gfx.load_font): the compiler always binds gfx.load_font(...) to the engine host so calls do not disappear as “void” or wrong chunks.
Build .epf files with the ns:font-builder built-in plugin (ns build rules or ns asset --plugin ns:font-builder). See docs/build-system.md and docs/font-epf.md.
draw_text Details
gfx.draw_text(text, x, y) -- white text, default font
gfx.draw_text(text, x, y, 255, 200, 0) -- colored text (r, g, b)
gfx.draw_text(text, x, y, my_color) -- color record
gfx.draw_text(text, x, y, 255, 255, 255, :small) -- with font symbol
gfx.draw_text(text, x, y, 255, 255, 255, :normal, 2) -- integer dot size
gfx.draw_text(text, x, y, 255, 255, 255, :normal, 1.5) -- fractional dot size
Arguments after position are optional:
- color: record or three ints (default: 255, 255, 255)
- font:
:smallor:normalsymbol (default: lastset_fontor normal) - scale: positive int or float — dot size in buffer pixels per glyph cell (default: engine UI scale). Fractional values (e.g.
1.5) are supported.
Example
access <gfx>
access <color>
phase draw() {
gfx.fill_screen(20, 20, 40)
gfx.fill_rect(10, 10, 100, 20, color.Red)
gfx.draw_text("Score: " + score, 12, 12, color.White)
gfx.draw_text("GAME OVER", 100, 100, 255, 50, 50, :normal, 2)
}
audio
access <audio>
Audio playback provided by the ns-audio crate. Two pieces must be wired in
your main.rs:
- Backend — an [
AudioSink] implementation that decodes and plays sound.ns-audioships a ready-made rodio backend behind therodiofeature (on by default). Pass it to the engine withengine.set_audio(...). If no backend is set the engine defaults to silent no-op. - Script plugin —
AudioRuntimeregisters theaudio.*andassets.load_soundhost functions. Install it withengine.rt.install_audio(AudioRuntime::new()).
The platform shell has no audio knowledge — audio is entirely owned by the engine.
#![allow(unused)]
fn main() {
// in main.rs
engine.set_audio(ns_audio::AudioBackend::open()); // rodio backend
engine.rt.install_audio(ns_audio::AudioRuntime::new()); // script API
}
Sound files placed in your asset directories (.wav, .ogg, etc.) are
automatically available by path after mount_assets — no manual load_data
step required.
| Function | Args | Returns | Description |
|---|---|---|---|
audio.play_sfx(src, loop?) | int handle or string path, optional bool | int | Play a sound effect. Accepts a handle from assets.load_sound or a path string. Returns a playback handle for later control. |
audio.play_bgm(src, loop?) | int handle or string path, optional bool | void | Play background music. loop defaults to true. |
audio.play(src, loop?) | int handle or string path, optional bool | int | Convenience alias — plays a sound effect. Returns a playback handle. |
audio.stop_bgm() | – | void | Stop background music. |
audio.stop(handle) | int | void | Stop a specific sound by playback handle. |
audio.stop_all() | – | void | Stop all playing sounds. |
audio.set_volume(level) | float | void | Set master volume (0.0 – 1.0). |
Loading sounds
When the audio plugin is installed, it adds load_sound to the assets record:
| Function | Args | Returns | Description |
|---|---|---|---|
assets.load_sound(path) | string | int | Load a sound from the mounted raw assets and return a handle. Returns 0 if the path is not found. Subsequent calls with the same path return the cached handle. |
Example
access <assets>
access <audio>
let bgm = assets.load_sound("sound/overworld.wav")
let sfx = assets.load_sound("sound/hit.wav")
phase init() {
audio.play_bgm(bgm)
}
phase tick(dt) {
when attack_landed {
audio.play_sfx(sfx)
}
// Or play directly by path (convenience)
// audio.play("sound/hit.wav")
}
assets
access <assets>
Asset loading from the mounted content providers (PAK files or directories).
Engine::mount_assets (see crates/ns-core/src/engine.rs) pre-registers known extensions:
.png / .qoi as textures, .epd / .json / .toml as structured data (same keys as paths with the suffix stripped), and everything else as opaque bytes in the raw store.
Format resolution (load_image / load_data)
When you pass an optional second format argument (text, or a symbol whose name was fixed at the last cache_symbols / finalize pass), it always wins over the path extension and over magic-byte detection.
Otherwise the engine uses, in order:
- File extension on
path(e.g..png,.qoi,.epd,.json,.toml). - Magic sniff — images only: PNG signature and QOI
qoif. There is no JSON sniff; use.jsonorload_data(path, "json").
If extension and magic disagree and you did not pass an explicit format, image decode fails (handle 0). That avoids guessing.
Lazy loading: If an image or data entry was not pre-registered at mount, load_image / load_data read from the raw store. They try the exact path, then helpful suffixes: images also try path + ".png" / path + ".qoi"; data tries path + ".epd" / path + ".json" / path + ".toml".
| Function | Args | Returns | Description |
|---|---|---|---|
assets.load_data(path, format?) | text, optional format | value or void | Return structured data for path if already mounted or decode lazily from raw bytes. Built-in formats: epd, json, toml. Returns void if missing or decode fails. |
assets.load_image(path, format?) | text, optional format | int | Return a texture handle: fast path if the image was mounted at startup; otherwise decode from raw bytes (PNG, QOI, or a custom format registered from Rust). Returns 0 if missing or decode fails. |
assets.image_width(handle) | int | int | Width of a loaded image in pixels. Returns 0 for invalid handles. |
assets.image_height(handle) | int | int | Height of a loaded image in pixels. Returns 0 for invalid handles. |
Custom decoders (Rust)
Games can register named decoders that scripts select with the second argument:
EpitaphRuntime::register_data_decoder("myfmt", |bytes| { ... })→assets.load_data(path, "myfmt")EpitaphRuntime::register_image_decoder("myimg", |bytes| { ... })(with theimagefeature) →assets.load_image(path, "myimg")
Format names are compared case-insensitively.
Example
access <assets>
access <gfx>
let bg_handle = 0
phase init() {
bg_handle = assets.load_image("images/background.png")
}
phase draw() {
gfx.draw_image(bg_handle, 0, 0)
-- Smaller on-screen footprint than default (per-draw `img_scale`; engine scale unchanged):
-- gfx.draw_image(bg_handle, 0, 0, 2)
}
save
access <save>
Structured persistence using named slots. Requires a SaveProvider to be
installed in the engine (e.g. file-based or in-memory).
| Function | Args | Returns | Description |
|---|---|---|---|
save.write(slot, ...values) | text, variadic | void | Save values to a named slot. |
save.read(slot) | text | list | Load saved values. Returns empty list if slot doesn’t exist. |
save.delete(slot) | text | void | Delete a saved slot. |
save.exists(slot) | text | bool | Check if a slot has saved data. |
Example
access <save>
phase save_game() {
save.write("slot1", player.hp, player.level, player.name)
}
phase load_game() {
when save.exists("slot1") {
let data = save.read("slot1")
player.hp = data[0]
player.level = data[1]
player.name = data[2]
}
}
window
access <window>
Window management for the platform shell.
Control Functions
| Function | Args | Returns | Description |
|---|---|---|---|
window.open(w, h, title) | int, int, text | void | Open/resize the window. |
window.close() | – | void | Close the window. |
window.set_size(w, h) | int, int | void | Resize the window. |
window.set_title(title) | text | void | Set the window title. |
window.set_fullscreen(on?) | bool | void | Toggle fullscreen. |
window.set_vsync(on?) | bool | void | Toggle vsync. |
window.set_resizable(on?) | bool | void | Toggle resizable. |
window.set_frame_rates(tick_hz, render_hz) | int, int | void | Set simulation tick rate and display refresh hint together (one shell update). Same effect as engine.set_tick_rate + engine.set_render_rate. |
Query Functions
| Function | Returns | Description |
|---|---|---|
window.width() | int | Current window width. |
window.height() | int | Current window height. |
window.is_fullscreen() | bool | Whether the window is fullscreen. |
window.is_focused() | bool | Whether the window has focus. |
window.was_resized() | bool | Whether the window was resized this frame. |
Events
| Function | Returns | Description |
|---|---|---|
window.poll_event() | symbol, text, or bool | Pop one window event. Returns dormant if no events. Events: :resize, :focus, :blur, :close_requested. |
Example
access <window>
phase init() {
window.open(800, 600, "My Game")
window.set_resizable(active)
}
phase tick(dt) {
sustain evt = window.poll_event() {
inspect evt {
:close_requested => { sys.exit() }
:resize => { log.info("Resized to", window.width(), "x", window.height()) }
}
}
}
engine
access <engine>
Engine configuration for tick rate, render rate, scale, and screen size.
VM stack limits (Rust EngineConfig only)
When you build Engine::new with EngineConfig, set vm_stack_max and vm_frames_max for the Epitaph VM operand stack and call-frame depth. Defaults match the historical caps (256 stack slots, 64 frames). Values are clamped to safe ranges via VmLimits::new (stack roughly 64..1 048 576, frames 8..4096). The stack Vec is pre-reserved to the stack cap (size_of::<Value>() × cap) — raise the cap only if scripts need it and memory is acceptable.
Headless / tick-only driver (Rust Engine only)
For servers, CI, or batch simulation you often want fixed game.tick steps without a window, minifb, or full-size framebuffer.
- Set
EngineConfig.headless: true.Engine::newthen allocates no pixel buffer (bufferis empty;tw/thstay 0). Logicalscreen_w/screen_hin config are unchanged for scripts (and forengine.screen_width()/engine.screen_height()from Epitaph once hosts are registered). - Use
NoopHeadlessShell(or your ownPlatformShellstub) withEngine::run: the main loop still accumulates real time, but it never runsgame.draw, never flushes draw lists to the buffer, and never callspresent_buffer. - For exactly N ticks without a wall-clock outer loop, call
Engine::run_headless_ticksafterload_entry. It runs the same bootstrap asrun(on_init,game.init,on_ready), then N times: input poll (empty onNoopHeadlessShell), at most one coroutine resume, onegame.tick, side-effect dispatch, and audio drain — still no draw. It returnsErrifheadlessis not set. Engine::bootstrap_for_runispubif you need init-only from Rust without enteringrun.
Alternative (fully custom): drive EpitaphRuntime yourself: mount scripts, finalize, then call run_phase("game.tick", …) in your own loop and register any hosts your phases touch. The Engine path above keeps window/audio/input wiring consistent with the client loop.
Runtime role & single script tree (Rust EngineConfig + Epitaph)
For one gameplay codebase shared by a headed client and a headless server, keep a single script mount per process and vary only the binary’s Rust config (see networking.md):
EngineConfig.runtime_role—RuntimeRole:OfflineSingleplayer(default, unchanged single-player),Client, orServer. Set when constructingEngineConfigbeforeEngine::new; not meant to change at runtime.EngineConfig.headless— composes with the section above; scripts read it viasys.is_headless()(see sys).
Epitaph exposes both on the sys module (always installed on EpitaphRuntime; see the sys table for sys.is_headless() and sys.runtime_role()). That keeps process profile separate from ns-network, which only registers net.* when you install it.
Advanced: EpitaphRuntime::with_vm_limits_and_options takes RuntimeBuildOptions (headless + runtime_role) if you construct the runtime without Engine.
Assets: point both binaries at the same asset directory or PAK when simulation and client must agree on paths — Engine::mount_assets is keyed by path string; divergent mounts mean missing textures or desynced data on one side.
Optional pattern: per-binary [constants] in game.toml (still one script tree, two [[bin]] targets) if you prefer compile-time flags over runtime_role; avoid inventing ad-hoc private host names for the same branching.
Control Functions
| Function | Args | Returns | Description |
|---|---|---|---|
engine.set_tick_rate(hz) | int | void | Set the simulation tick rate (default 60). |
engine.set_render_rate(hz) | int | void | Set the render frame rate. |
engine.set_scale(n) | int | void | Set the pixel scale factor. |
engine.set_screen_size(w, h) | int, int | void | Set the logical screen dimensions. |
To change tick and render rate in one call from scripts, prefer
window.set_frame_rates(tick_hz, render_hz); the engine.set_tick_rate /
engine.set_render_rate pair remains for changing a single value.
Query Functions
| Function | Returns | Description |
|---|---|---|
engine.tick_rate() | int | Current tick rate in Hz. |
engine.render_rate() | int | Current render rate in Hz. |
engine.scale() | int | Current pixel scale. |
engine.screen_width() | int | Logical screen width. |
engine.screen_height() | int | Logical screen height. |
Example
access <engine>
phase init() {
engine.set_screen_size(320, 240)
engine.set_scale(2)
engine.set_tick_rate(30)
}
tilemap
access <tilemap>
Hex/tile grid rendering and queries.
Setup
The collision/tile grid is a square N×N cells (default 40). Call tilemap.set_grid_size(n) before load_grid_from_asset when your asset is a different size; resizing clears the grid to zeros. Query the current edge length with tilemap.grid_size(). Maximum n is 1024 (values above that are clamped).
Tile texture helpers (load_atlas, set_atlas_item) are only available when the engine is built with the image feature (default in ns-core).
| Function | Args | Returns | Description |
|---|---|---|---|
tilemap.set_grid_size(n) | int / float | void | Set map edge length to n cells (clamped to 1..1024). Reallocates storage to n×n and zeros it. |
tilemap.grid_size() | – | int | Current map edge length in cells. |
tilemap.load_grid_from_asset(path, flush?) | text, optional bool / symbol / text | bool | Load a grid from raw asset bytes (row-major, grid_size × grid_size bytes). Optional second argument selects flush (zero grid before copy): use active, symbol :flush (name flush in the symbol table), or text "flush" / "active" / "true" / "1". Omit, dormant, or any other symbol name → overlay only (legacy). Other truthy values (e.g. non-zero int) still count as flush. Returns whether the asset existed. |
tilemap.load_atlas(prefix, count) | text, int | int | Bind tile indices 0..count-1 to textures. Resolves each slot in order: existing image handle for {prefix}/{i}.png, .qoi, or exact {prefix}/{i}; if missing, lazy-decodes from the raw store (same rules as assets.load_image: .png / .qoi / extensionless + magic). Decoded tiles are registered under {prefix}/{i}.png for caching. Missing or bad slots get handle 0 (skipped when drawing). Returns count. |
tilemap.set_atlas_item(id, src) | int, int or text | void | Set atlas slot id without rebuilding the whole atlas. src is either a texture handle from assets.load_image (including 0 to clear the slot), or an asset path string resolved like a single load_atlas entry (path.png / .qoi / extensionless + raw + decode). Grows atlas_handles with leading 0s if id is past the end. |
tilemap.set_cell_size(px) | int | void | Set cell size in pixels. |
tilemap.set_view_tiles(n) | int | void | Set view radius in tiles. |
tilemap.set_terrain_cap(cap) | int | void | Set terrain passability threshold. |
Camera
| Function | Args | Returns | Description |
|---|---|---|---|
tilemap.snap_camera(q, r) | int/float, int/float | void | Instantly move camera to cell. |
tilemap.follow_camera(q, r, dt) | int/float, int/float, float | void | Smoothly move camera toward cell. |
Queries
| Function | Args | Returns | Description |
|---|---|---|---|
tilemap.is_passable(q, r) | int, int | bool | Whether a cell is passable. |
tilemap.get_cell(q, r) | int, int | int | Tile index at cell. |
tilemap.cell_to_screen_x(q) | int/float | int | Screen X for cell coordinate. |
tilemap.cell_to_screen_y(r) | int/float | int | Screen Y for cell coordinate. |
Rendering
| Function | Args | Returns | Description |
|---|---|---|---|
tilemap.draw(ox, oy) | int, int | void | Draw the tilemap with pixel offset. |
Example
access <tilemap>
access <gfx>
phase init() {
tilemap.set_grid_size(64)
tilemap.load_grid_from_asset("maps/world.grid", :flush)
tilemap.load_atlas("tiles/terrain", 16)
tilemap.set_cell_size(32)
tilemap.set_view_tiles(8)
}
phase tick(dt) {
tilemap.follow_camera(player_q, player_r, dt)
}
phase draw() {
gfx.fill_screen(0, 0, 0)
tilemap.draw(0, 0)
}
ecs
Entity Component System for managing game objects. Host functions are registered
but access <ecs> may not be fully wired in all engine versions – call the
functions directly by name if needed.
| Function | Args | Returns | Description |
|---|---|---|---|
ecs.spawn() | – | int | Create a new entity. Returns entity ID, or -1 if no ECS adapter. |
ecs.destroy(id) | int | void | Remove an entity. |
ecs.set(id, comp, value?) | int, text, value | void | Set a named component on an entity. Value defaults to void. |
ecs.get(id, comp) | int, text | value | Get a component value. Returns void if not found. |
ecs.remove(id, comp) | int, text | void | Remove a component from an entity. |
ecs.query(mask) | text | list | Query entities matching a component mask. Returns list of entity IDs. |
Example
let player_id = ecs.spawn()
ecs.set(player_id, "position", {"x": 100, "y": 50})
ecs.set(player_id, "health", 100)
let pos = ecs.get(player_id, "position")
log.info("Player at", pos["x"], pos["y"])
let entities = ecs.query("health")
traverse entities as id {
let hp = ecs.get(id, "health")
when hp <= 0 {
ecs.destroy(id)
}
}
config
access <config>
Runtime constants loaded from game.toml’s [constants] section. The config
global is a record whose fields match the TOML keys.
game.toml
[constants]
title = "My Game"
version = 3
debug = false
Usage
access <config>
phase init() {
window.set_title(config.title)
when config.debug {
log.info("Debug mode enabled, version", config.version)
}
}