Keyword Reference

Language version 1.57.0 · docs generated 2026-08-13 · 411 keywords

Complete reference for the Crash BASIC language: every keyword, statement, and built-in function.

This file is auto-generated from docs/api-docs.json by scripts/generate-api-markdown.py. Do not edit by hand.

Contents

Control Flow

ASYNC · BOOM · BYREF · BYVAL · CALL · CALL EVENT · COOLDOWN · DEBUGGER · DECLARE · DELAY · DO · END · EVENT · EXIT · FOR · FOR EACH · FUNCTION · IF · INSTATE$ · RESOLVE · SELECT CASE · SET STATE · SET YIELD · STOP RENDER · STOP THREAD · STOP TIMER · SUB · WHILE · WITH · YIELD

Structured Programming

BYVAL · DECLARE · FUNCTION · IMPORT · NAMESPACE · SUB

Variables

Array Literal · BEGIN ENUM · BEGIN SORT · CLONE · CONST · DATA · DIM · ERASE · FALSE · MUTATION · MUTATIONCOUNT · MUTATIONPOINTX · MUTATIONPOINTY · OPEN DATA · Object Literal · PUSH · QUEUE · READ · REDIM · RESTORE · SELF · SET MUTATIONPOINT · TRUE · TYPE

Functions

ASARRAY · ASC · CHR$ · COUNT · DATE$ · DEQUEUE · EXISTS · FPS · HEX$ · INSTR · JOIN$ · LCASE$ · LEFT$ · LEN · LTRIM$ · MATCH · MID$ · OCT$ · POP · REGEX · REPLACE$ · REVERSE$ · RIGHT$ · RTRIM$ · SPACE$ · SPLIT$ · STR$ · STRING$ · TIME$ · TIMER · TRIM$ · UCASE$ · VAL · VERSION$

Math

ABS · ACOS · ASIN · ATN · ATN2 · CEIL · CLAMP · COS · EXP · FIX · FIXED · FLOOR · INT · LBOUND · LERP · LOG · LOG10 · MAG · MAX · MIN · MOD · PI · POW · RANDOMIZE · RND · ROUND · SGN · SIN · SQR · TAN · UBOUND

Bitwise

BITAND · BITCLEAR · BITNOT · BITOR · BITSET · BITTEST · BITXOR · SHL · SHR

Input

DRAGX · DRAGY · GAMEPAD · GAMEPAD ALPHA · GAMEPAD BUTTON · GAMEPAD BUTTON AT · GAMEPAD SETIMAGE · INKEY$ · INPUT · ISCONSOLE · ISMOBILE · JOYAXIS · JOYBUTTON · JOYCONNECTED · KEYDOWN · MOUSEDOWN · MOUSEX · MOUSEY · PAUSE · RUMBLE · TAP · TOUCHX · TOUCHY

Graphics

ALPHA · ASIMAGE · BEGIN FRAME · BEGIN MEMORYBUFFER · BEGIN SCENE · CIRCLE · CLS · COLOR · CREATE LAYER · CREATE MAP · DESTROY POLYGON · END FRAME · FONT · GETIMAGE · GETSCROLL · IMAGEHEIGHT · IMAGEWIDTH · LINE · LOADFONT · LOCATE · MAPTILE · MAPTILEX · MAPTILEY · MAPWORLDTILEX · MAPWORLDTILEY · NEWIMAGE · OFFSET · PALETTE · POINT · POINTRGB · POLYGON · PRINT · PSET · REGION · RESET LAYERS · ROTATE · SCALE · SCALETO · SCREEN · SCREEN TEXT · SCREENHEIGHT · SCREENWIDTH · SET CURSOR · SET IMAGE · SET LAYER · SET PALETTE · SET POLYGON · SET SCROLL · SETFONT · SHADER · TEXTTOIMAGE · TILEMAP · TOSPRITESHEET · TRANSITION · TRANSITION$ · TYPING · UPDATE MAP · WINDOW

Sprites

DISABLE SPRITE · EMITPARTICLE · ENABLE SPRITE · FOR COLLISIONS · FOR SPRITES · FRAMECOUNT · GETSPRITEMETA · HIDE SPRITE · IMAGEBBOX · MOVE SPRITE · NEXTFREESPRITE · RESERVE SPRITE · SET SPRITE · SHOW SPRITE · SPRITE RESET · SPRITEBBOX · SPRITECOLLISIONID · SPRITECOUNT · SPRITEDIRECTION · SPRITEDIST · SPRITEELEVATION · SPRITEENABLED · SPRITEHEIGHT · SPRITETAG$ · SPRITEVISIBLE · SPRITEWIDTH · SPRITEX · SPRITEY

Behaviors

AFTERCOLLISION · BEHAVIOR · HASBEHAVIOR · HASPATTERN · ONCOLLISION · PATTERN · SAVE BEHAVIOR · SAVE RIG · SET BEHAVIOR RATE · SET MUTATION BEHAVIOR · SPRITEBEHAVIORS$ · SPRITEPATTERN$ · SPRITEVELOCITYX · SPRITEVELOCITYY · THISSPRITE · VARIANCE

Collisions

BOUNDARY ADD · BOUNDARY ADD FROM · BOUNDARY CLEAR · BOUNDARY DELETE · COLLISION · ENCLOSURE (boundary modifier) · SOLID (boundary modifier) · SPRITECOLLISION · SPRITETOUCHING$ · SPRITETOUCHINGX · SPRITETOUCHINGY

Maps

FOR MAPDATA · MAPHEIGHT · MAPMETA$ · MAPWIDTH

Sound

ADD ARRANGEMENT · APPLY MUSIC · ARRANGEMENTCOUNT · ARRANGEMENTLAYER$ · ARRANGEMENTLAYERCOUNT · ARRANGEMENTPATTERN$ · ARRANGEMENTREPEAT · BEEP · CREATE MUSIC PATTERN · CREATE TRACK · DELETE ARRANGEMENT · DELETE MUSIC · DELETE MUSIC PATTERN · DELETE TRACK · FADEMUSIC · GENERATOR · INSERT ARRANGEMENT · INSTRUMENTMODE · LIPSYNC · LOADMUSIC · MUSIC · MUSICBEATS · MUSICBEATUNIT · MUSICEXISTS · MUSICLENGTH · MUSICLOOP · MUSICPATTERNCOUNT · MUSICPATTERNLENGTH · MUSICPATTERNNAME$ · MUSICPLAYING · MUSICPOSITION · MUSICTEMPO · MUTE TRACK · Music File Format · PATTERNAUDIO$ · PATTERNAUDIOVOLUME · PAUSEMUSIC · PLAY · PLAYMUSIC · RESUMEMUSIC · SET ARRANGEMENT · SET AUDIO · SET MUSIC · SET MUSIC PATTERN · SET TRACK · SETINSTRUMENTWAV · SOLO TRACK · SONGCOUNT · SONGNAME$ · SOUND · STOP PLAY · STOPMUSIC · TRACKCHANNEL · TRACKCOUNT · TRACKGENERATOR$ · TRACKINSTRUMENT · TRACKMUTED · TRACKNAME$ · TRACKNOTES$ · TRACKPAN · TRACKREPEAT · TRACKSOLOED · TRACKVOLUME

Video

LOADVIDEO · PAUSEVIDEO · PLAYVIDEO · RESUMEVIDEO · SEEKVIDEO · STOPVIDEO · WAITVIDEO

File I/O

CALL SQL · CLOSE · DESERIALIZE$ · EOF · FETCH · FILEEXISTS · FILES · ISOPEN · JMES · LINE INPUT · LOADJSON · LOADTEXT · LOC · LOF · OPEN · OPEN DATABASE · PRINT # · SAVEJSON · SAVETEXT · SERIALIZE$ · SQL

Resources

BASE64TOIMG · BASE64TOMP3 · BASE64TOWAV · FADEMP3 · FREE · GETIMAGE · LOADIMG · LOADMP3 · LOADPNG · LOADWAV · PLAYMP3 · PLAYWAV · PUTIMAGE · STOPMP3 · STOPWAV

Multiplayer

CALL HOST · CALL PLAYER · CRASHNET · CRASHNET (object) · GAMECONTEXT$ · HOST DATA · ME · OnServerBroadcast · PLAYER DATA · PLAYERS · SET ROOM · STOP HOST · STOP PLAYER · SYNC

In-App Purchase

FOR PURCHASE · PURCHASE · PURCHASE NEW · PURCHASE RESTORE

AI

ASTAR · CHOOSE · DISTANCE · FLEE · FLOODFILL · MAZE · NEAREST · NEIGHBORS · RAYCAST · SEEK · WANDER · WEIGHTED

String

FIXED$


Control Flow

ASYNC

Control Flow · since 1.30.0

Prefix keyword that spawns a worker thread to run the function and immediately returns a Promise value. Arguments are evaluated eagerly in the caller's scope, so changes to caller-local variables after the spawn don't leak into the worker. Works on BASIC FUNCTIONs, BASIC SUBs, and native built-ins.

The promise is consumed by RESOLVE(). Re-resolving the same promise returns the cached value (single-write, many-read). Wrong-arity / undefined-function errors halt at the ASYNC site, not deep inside a never-resolved promise.

Errors inside an async body halt the program — ASYNC is for parallelism, not error isolation.

Syntax

promise = ASYNC funcName(args)

Parameters

  • funcName - Any FUNCTION, SUB, or native function name
  • args - Argument expressions, evaluated eagerly in the caller's scope

Examples

FUNCTION Double(n)
  Double = n * 2
END FUNCTION

p = ASYNC Double(21)
result = RESOLVE(p)  ' 42
REM Three loads in parallel; total wall-clock = max(durations)
p1 = ASYNC LoadLevel(1)
p2 = ASYNC LoadLevel(2)
p3 = ASYNC LoadLevel(3)
levels = RESOLVE(p1, p2, p3)  ' array of three values, in order
REM Parallel network calls
p = ASYNC FETCH("https://cdn.example.com/sprite.png", "GET")
CALL DoOtherStuff()
r = RESOLVE(p)
ok = BASE64TOIMG(r.MESSAGE, "sprite")

See also: RESOLVE, FETCH, CALL THREAD, FUNCTION

↑ Back to top

BOOM

Control Flow · since 1.27.0

Inline guard that halts the program with a fatal runtime error when the condition is truthy. If the condition is falsy, BOOM is a silent no-op. Use it to bail out at the exact line where an impossible state is detected, instead of letting the program continue and fail somewhere downstream that's harder to trace. The message is a full expression evaluated only when BOOM fires (when the condition is falsy, the message is not evaluated, so it's safe to put expensive string-builders there). The error surfaces through the normal runtime-error path — same UI, same logs, same halt semantics — there is no catch / recover / on-error mechanism.

Syntax

BOOM <condition>, <message>

Parameters

  • condition - Any expression. Truthy fires BOOM; falsy is a no-op (TRUE = 1, FALSE = 0; non-zero values are truthy).
  • message - Any expression, stringified at runtime. Shows up as the runtime-error message.

Examples

' Guard against impossible state
BOOM gamePhase < 0, "gamePhase went negative"
' Validate inputs at function entry
SUB ApplyDamage(amount)
  BOOM amount < 0, "ApplyDamage got a negative amount"
  hp = hp - amount
END SUB
' Inline message composition
BOOM playerSlot > MAX_PLAYERS, "slot " + STR$(playerSlot) + " exceeds max " + STR$(MAX_PLAYERS)
' Single-line IF style
IF host.gamePercentageDone > 100 THEN BOOM 1, "percentage overflowed"

See also: DEBUGGER, END, STOP

↑ Back to top

BYREF

Control Flow · since 1.0.3

Pass parameter by reference to SUB. This is the default behavior for SUB parameters when neither BYVAL nor BYREF is specified. Changes to the parameter inside the subroutine will modify the caller's original variable. BYREF needs a caller's variable to write back into, so it applies only when the argument IS a plain variable. A literal, an expression, an array element (rooms(1)) or a function result has nothing to write back to and is passed by value regardless of how the parameter was declared. Not allowed in FUNCTION definitions (use SUB if you need to modify caller's variables).

Syntax

BYREF parameter AS datatype

Parameters

  • parameter - Parameter name
  • datatype - INTEGER, STRING, etc.

Examples

' BYREF allows modifying the caller's variable
SUB DoubleIt(BYREF n AS INTEGER)
  n = n * 2
END SUB

value = 5
CALL DoubleIt(value)
PRINT value  ' Prints 10
' Classic swap example using BYREF
SUB Swap(BYREF a AS INTEGER, BYREF b AS INTEGER)
  DIM temp AS INTEGER
  temp = a
  a = b
  b = temp
END SUB

x = 10
y = 20
CALL Swap(x, y)
PRINT x; y  ' Prints 20 10
' Mix BYVAL and BYREF in the same SUB
SUB UpdateScore(BYVAL points AS INTEGER, BYREF total AS INTEGER)
  total = total + points  ' Modifies caller's total
  points = 0  ' Only affects local copy
END SUB

score = 100
bonus = 50
CALL UpdateScore(bonus, score)
PRINT score  ' Prints 150
PRINT bonus  ' Still 50
' BYREF with string variables
SUB AppendWorld(BYREF text AS STRING)
  text = text + " World"
END SUB

msg$ = "Hello"
CALL AppendWorld(msg$)
PRINT msg$  ' Prints "Hello World"

See also: BYVAL, SUB

↑ Back to top

BYVAL

Control Flow · since 0.23.0

Pass parameter by value (copy). In SUB, use BYVAL to override the default BYREF behavior. In FUNCTION, all parameters are implicitly BYVAL. Changes to the parameter inside the subroutine/function do not affect the caller's variable. For a reference type (an array or object), an explicit BYVAL parameter is a genuinely independent deep copy — writing to an element through the parameter cannot reach the caller's data. Note that this is the effect of writing BYVAL, not of a BYREF parameter that fell back to by-value because its argument was not a plain variable: in that case a reference-typed argument still aliases the caller's data.

Syntax

BYVAL parameter AS datatype

Parameters

  • parameter - Parameter name
  • datatype - INTEGER, STRING, etc.

Examples

SUB Test(BYVAL x AS INTEGER)
  x = x + 1  ' Changes local copy only
END SUB

a = 5
CALL Test(a)
PRINT a  ' Still prints 5
' SUB defaults to BYREF, use BYVAL to prevent modification:
SUB ProcessValue(BYVAL n AS INTEGER)
  n = n + 1  ' Only affects local copy
END SUB

See also: BYREF, SUB, FUNCTION

↑ Back to top

CALL

Control Flow · since 0.23.0

Calls subroutines directly, schedules them for timer execution, runs them in background threads, or executes them synchronized with the behavior engine tick.

CALL RENDER is particularly important for game loops that need to be synchronized with sprite movement. Unlike CALL TIMER which runs on an independent timer, CALL RENDER callbacks execute at the end of each behavior engine tick (60 FPS), immediately after sprite positions are updated by behaviors and patterns. This eliminates timing desynchronization that can cause jitter in features like camera tracking or scroll position calculations.

Method receivers. A type-bound SUB is called on the instance it belongs to, and the receiver may be any path to that instance — a variable (CALL p.Take(3)), an array element (CALL enemies(i).Update()), a field of a type (CALL squad.leader.Hit(5)), or any depth of those combined (CALL army.squads(0).members(2).Hit(5)). The trailing argument list belongs to the method, never to the receiver. Inside the body SELF refers to the instance itself rather than a copy, so writes are visible through the same path afterwards. The async forms (TIMER / THREAD / RENDER / EVENT) take the same receivers, evaluated once at registration and captured as SELF. A FUNCTION method is called in expression position instead (IF army.squads(0).members(2).IsDead() THEN ...), with the same receiver rules.

A two-part name with no index is read as a namespaced sub first (CALL Utils.Double(5)), falling back to a method on a variable of that name.

The optional GUARD(flags) modifier restricts where the call/registration runs and/or guarantees non-overlapping invocations. Flags break down into two orthogonal kinds:

  • Context flags (combinable, OR-matched): SINGLE (only fires in solo / single-player launches), HOST (only when ME.ISHOST), PLAYER (only when ME.ISSERVER AND NOT ME.ISHOST), CLIENT (only when ME.ISCLIENT). List any subset; the handler fires when the current interpreter matches ANY listed context. Empty context list = no restriction.

  • Invocation lock: ONCE (skip overlapping invocations of this AS-id). Orthogonal to the context flags — combinable with any subset, or stand-alone.

Without FAILS, a guard violation is a silent no-op (skip the call/registration, or drop the overlapping invocation). With FAILS, the same violation halts the program with a runtime error.

Strict context matching (no solo short-circuit): SINGLE is its own context. GUARD(HOST) does NOT fire in solo — solo and multiplayer-host are different things, and writing a Free-tier solo flow that accidentally fires host-only logic was the motivating regression. To cover both solo and host, list both: GUARD(HOST, SINGLE). The intended pattern is "write a complete game using context-targeted GUARDs throughout, deploy to solo or multiplayer; the same .bas runs by adding SINGLE wherever the handler should also fire solo."

ONCE semantics by call kind:

  • plain CALL — keyed by SUB name (shared across threads). Re-entrant / recursive calls into the same SUB while it is in flight are skipped (or fail).
  • CALL THREAD — registration-time check: refuse to respawn if a thread with this ID is already running.
  • CALL TIMER / RENDER / EVENT / PLAYER / HOST — per-invocation lock keyed by AS-id; overlapping ticks / firings / handler invocations are dropped.

The parentheses are part of the call, empty or not: CALL Restart(), not CALL Restart. This is true of every form — plain, method, TIMER, THREAD and RENDER.

Syntax

CALL subroutine_name (arguments) [GUARD(flags) [FAILS]]
CALL receiver.Method (arguments) [GUARD(flags) [FAILS]]
CALL TIMER subroutine_name (arguments) [GUARD(flags) [FAILS]] EVERY time_interval time_unit [LOCAL] AS "timer_id"
CALL TIMER subroutine_name (arguments) [GUARD(flags) [FAILS]] AFTER time_interval time_unit [LOCAL] AS "timer_id"
CALL THREAD subroutine_name (arguments) [GUARD(flags) [FAILS]] [LOCAL] AS "thread_id"
CALL RENDER subroutine_name (arguments) [GUARD(flags) [FAILS]] [LOCAL] AS "render_id"

Parameters

  • subroutine_name - Name of SUB to call
  • arguments - Values to pass to subroutine parameters. The parentheses are required even when there are none: CALL DoThing()
  • time_interval - Numeric value for timer delay/interval
  • time_unit - SECOND, SECONDS, MILLISECONDS, or MINUTES
  • timer_id - String identifier for managing the timer
  • thread_id - String identifier for managing the thread
  • render_id - String identifier for managing the render callback
  • GUARD(flags) - Optional registration / invocation guard. Context flags (combinable, OR-matched): SINGLE (solo only), HOST, PLAYER, CLIENT. Lock flag: ONCE (no-overlap invocation lock). Combine with comma: GUARD(HOST, SINGLE) or GUARD(HOST, ONCE) or GUARD(HOST, SINGLE, ONCE).
  • FAILS - Optional. Turns a guard violation into a runtime error instead of a silent no-op.

Examples

CALL PrintMessage("Hello World")
CALL DrawBox(10, 10, 50, 50, 15)
' Plain CALL with GUARD(HOST, SINGLE) — fires both on the multiplayer host AND in solo, but never on player-server or client.
CALL InitWorldState() GUARD(HOST, SINGLE)
' Plain CALL with GUARD(ONCE) FAILS — refuse re-entrant SaveGame and halt loudly if it happens.
CALL SaveGame() GUARD(ONCE) FAILS
CALL TIMER GameUpdate() EVERY 1 SECOND AS "gameLoop"
CALL TIMER ShowMessage() AFTER 30 SECONDS AS "reminder"
CALL THREAD LoadResources() AS "loader"
CALL RENDER DrawHUD(playerHealth, score) AS "hud"
' Use CALL RENDER for camera tracking to avoid jitter:
CALL RENDER GameLoop() AS "gameloop"

SUB GameLoop()
  ' This runs synchronized with sprite movement
  scrollX = INT(SPRITEX(0) - 320)
  IF scrollX < 0 THEN scrollX = 0
  CALL RenderGame()
END SUB
' GUARD(HOST, SINGLE, ONCE) FAILS — fires on the multiplayer host AND in solo, with an invocation lock that halts loudly if a tick overruns.
CALL TIMER WorldStep() GUARD(HOST, SINGLE, ONCE) FAILS EVERY 100 MILLISECONDS AS "world"
' GUARD(CLIENT, SINGLE) — render only on the local client interpreter (multiplayer) or in solo. Player-server and host don't render here.
CALL RENDER DrawHUD() GUARD(CLIENT, SINGLE) AS "hud"
' Type-bound methods: the receiver can be any path to the instance.
TYPE Unit
    hp AS INTEGER
END TYPE

SUB Unit::Hit(n)
    SELF.hp = SELF.hp - n
END SUB

FUNCTION Unit::IsDead()
    IsDead = SELF.hp <= 0
END FUNCTION

TYPE Squad
    members(0 TO 2) AS Unit
    leader AS Unit
END TYPE

DIM squad AS Squad
squad.leader.hp = 10
squad.members(2).hp = 10

CALL squad.leader.Hit(3)            ' field of a type
CALL squad.members(2).Hit(10)       ' array field element

PRINT squad.leader.hp               ' 7 — SELF wrote through the path
IF squad.members(2).IsDead() THEN PRINT "down"

See also: SUB, STOP TIMER, STOP THREAD, STOP RENDER, DECLARE, BEHAVIOR, PATTERN, ME, CRASHNET

↑ Back to top

CALL EVENT

Control Flow · since 1.0.4

Registers a subroutine to be executed when a named event is triggered. Events enable decoupled communication between different parts of your program. If called with empty parentheses, the handler accepts dynamic arguments from the event source (e.g., behavior frame events pass sprite_id and frame_number). Event handlers run asynchronously in their own threads, so they do not block the main program or tick loop. Multiple events of the same type can run concurrently. Use STOP THREAD "event_name" to stop all running handlers for that event.

In CrashNet multiplayer, the same registration mechanism handles both in-process EVENT triggers AND cross-the-wire EVENT SERVER "name"(args) / EVENT HOST "name"(args) signals from clients. When invoked via a wire signal, the handler's parameter count must match the args sent — mismatch is a fatal runtime error. Register the handler on the side that should receive it: a player-server handler registered in the player-server interpreter receives EVENT SERVER from that player's client; a host-side handler receives EVENT HOST.

The optional GUARD(flags) modifier (see CALL) restricts where the handler is registered (HOST/PLAYER/CLIENT) and/or guarantees that overlapping firings of the same event don't run in parallel (ONCE — per-event invocation lock). Without FAILS, a guard violation is a silent no-op; with FAILS it halts the program with a runtime error.

Syntax

CALL EVENT subroutine_name (arguments) [GUARD(flags) [FAILS]] [LOCAL] AS "event_name"

Parameters

  • subroutine_name - Name of SUB to execute when event is triggered
  • arguments - Values to pass to the subroutine (captured at registration time). If empty parens (), accepts dynamic args from event source
  • event_name - String identifier for the event

Examples

DECLARE SUB OnPlayerWin()

CALL EVENT OnPlayerWin() AS "player_victory"

' Later in the game...
IF score >= 1000 THEN
  EVENT "player_victory"
END IF

SUB OnPlayerWin()
  PRINT "Congratulations! You won!"
  PLAY "victory.wav"
END SUB
DECLARE SUB ShowMessage(text AS STRING)

CALL EVENT ShowMessage("Game Over!") AS "game_end"
CALL EVENT ShowMessage("Level Complete!") AS "level_win"

IF lives <= 0 THEN
  EVENT "game_end"
ELSE
  EVENT "level_win"
END IF

SUB ShowMessage(text AS STRING)
  PRINT text
END SUB
' Multiple events can trigger the same handler
DECLARE SUB UpdateDisplay()

CALL EVENT UpdateDisplay() AS "score_changed"
CALL EVENT UpdateDisplay() AS "lives_changed"
CALL EVENT UpdateDisplay() AS "level_changed"

' Trigger from anywhere in your program
score = score + 100
EVENT "score_changed"

SUB UpdateDisplay()
  LOCATE 1, 1
  PRINT "Score:"; score; " Lives:"; lives
END SUB
' Dynamic args from behavior frame events
SUB OnAnimFrame(sprite_id, frame)
  PRINT "Sprite"; sprite_id; "hit frame"; frame
END SUB

CALL EVENT OnAnimFrame() AS "PLAYER_FRAME"  ' Empty parens = accept dynamic args from ANIMATE "player" EVENTSON
' CrashNet: same handler receives client EVENT SERVER signals
' Register on the player-server side. The captured arg values are placeholders;
' actual values come from the client EVENT SERVER call.
CALL EVENT OnFire(0, 0) AS "Fire"

SUB OnFire(x, y)
  ' Authoritative: validate range, apply damage, etc.
END SUB

' Then on the client (in IF ME.ISCLIENT):
' EVENT SERVER "Fire" (MOUSEX, MOUSEY)
' Stop all running handlers for an event
STOP THREAD "PLAYER_FRAME"  ' Stops all PLAYER_FRAME event handlers

See also: EVENT, SUB, DECLARE, BEHAVIOR, STOP THREAD, CRASHNET, ME

↑ Back to top

COOLDOWN

Control Flow · since 1.18.1

Executes the enclosed code at most once per ticks / resolution seconds of wall-clock time, rate-limiting code inside a fast game loop. resolution is optional and defaults to 60, so COOLDOWN N means once per N/60 seconds (one tick = 1/60 s) — the same as before. Pass a larger resolution for finer, sub-tick pacing: COOLDOWN 1, 240 runs at most once per 1/240 second (about 240 Hz). The first execution always runs immediately. Each COOLDOWN block is tracked independently by its position in the source. Timing is measured against a monotonic wall clock, so a block paces the same regardless of how fast the surrounding loop spins.

Syntax

COOLDOWN ticks [, resolution]
  statements
END COOLDOWN

Parameters

  • ticks - How many resolution-units to wait before the block may run again
  • resolution - Optional units-per-second (default 60). The block runs at most once per ticks/resolution seconds

Examples

' Fire bullets at most once per second
DO
  COOLDOWN 60
    CALL FireBullet()
  END COOLDOWN
  YIELD
LOOP
' Spawn enemies every 3 seconds
DO
  COOLDOWN 180
    CALL SpawnEnemy()
  END COOLDOWN
  CALL UpdateGame()
  YIELD
LOOP
' Pace a render block to ~120 Hz from a loop that spins much faster
DO
  COOLDOWN 1, 120
    CALL DrawFrame()
  END COOLDOWN
  CALL UpdateSimulation()
  YIELD
LOOP
' Multiple cooldowns in same loop
DO
  COOLDOWN 30  ' 2x per second
    CALL UpdateAnimation()
  END COOLDOWN
  
  COOLDOWN 60  ' 1x per second
    CALL CheckCollisions()
  END COOLDOWN
  YIELD
LOOP

See also: DELAY, YIELD

↑ Back to top

DEBUGGER

Control Flow · since 1.0.0

Triggers a breakpoint in the debugger, similar to JavaScript's debugger statement. When a debugger is attached and the program reaches this statement, execution will pause and enter debug mode, allowing inspection of variables and stepping through code. If no debugger is attached, this statement is a no-op and execution continues normally.

Syntax

DEBUGGER

Examples

' Break unconditionally for inspection
x = CalculateValue()
DEBUGGER
PRINT x
' Conditional debugging
IF score < 0 THEN
  DEBUGGER  ' Something went wrong
END IF
' Debug a loop iteration
FOR i = 1 TO 100
  IF i = 50 THEN DEBUGGER  ' Inspect at iteration 50
NEXT

See also: STOP, END

↑ Back to top

DECLARE

Control Flow · since 0.23.0

Forward declares a SUB or FUNCTION so its signature is known up front. It is optional for ordinary calls: module-level FUNCTION/SUB definitions are auto-hoisted, so you can call them before they appear in the source — including mutual recursion and a 'main at the top, helpers below' layout — with or without DECLARE. DECLARE remains useful for documenting an interface and for QuickBASIC compatibility. Only module-level definitions (top level and inside NAMESPACE blocks) are hoisted; a FUNCTION/SUB nested inside a control-flow block (IF/WHILE/FOR/DO) registers when execution reaches it, so define those before use.

The parentheses are required, empty or not: DECLARE SUB Restart(), not DECLARE SUB Restart.

Syntax

DECLARE SUB name(parameters)
DECLARE FUNCTION name(parameters) [AS datatype]

Parameters

  • name - Subroutine or function name
  • parameters - Parameter list matching the actual definition. The parentheses are required even for a routine that takes none: DECLARE SUB Restart()

Example

DECLARE FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
DECLARE SUB PrintMessage(BYVAL msg AS STRING)

PRINT Add(5, 3)
CALL PrintMessage("Test")

FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
  Add = a + b
END FUNCTION

SUB PrintMessage(BYVAL msg AS STRING)
  PRINT msg
END SUB

See also: SUB, FUNCTION, CALL

↑ Back to top

DELAY

Control Flow · since 1.0.0

Pauses program execution until a time or event condition is met. With a numeric argument, waits that many milliseconds. With UNTIL event$, blocks until the next time that named event fires via the EVENT statement (broadcast wake — every thread waiting on the same event resumes). Only firings that occur after the DELAY UNTIL begins count; earlier firings are not buffered. An optional TIMEOUT clause resumes execution after the given number of milliseconds even if the event never fires.

Syntax

DELAY milliseconds
DELAY UNTIL event$ [TIMEOUT milliseconds]

Parameters

  • milliseconds - Number of milliseconds to pause (must be >= 0)
  • event$ - Name of an event that will be fired by an EVENT statement
  • TIMEOUT milliseconds - Optional maximum wait time for DELAY UNTIL

Examples

PRINT "Starting..."
DELAY 1000
PRINT "Done!"
' Animation with delays
FOR frame = 1 TO 10
  CIRCLE (100 + frame * 10, 100), 5, 14
  DELAY 100  ' 100ms between frames
NEXT frame
' Sync to a pattern event
DELAY UNTIL "beat"
PRINT "Hit on the beat!"
' Wait for event, but cap the wait at 500ms
DELAY UNTIL "playerReady" TIMEOUT 500
PRINT "Continuing either way."

See also: YIELD, CALL TIMER, EVENT, CALL EVENT, FOR

↑ Back to top

DO

Control Flow · since 0.23.0

Loop with flexible condition testing at start or end.

Syntax

DO [WHILE condition]
  statements
LOOP [WHILE condition]

DO [UNTIL condition]
  statements
LOOP [UNTIL condition]

Parameters

  • condition - Expression to test for loop continuation

Examples

DO WHILE x < 10
  PRINT x
  x = x + 1
LOOP
DO
  INPUT "Continue? (Y/N): ", ans$
LOOP UNTIL UCASE$(ans$) = "N"

See also: WHILE, FOR, EXIT

↑ Back to top

END

Control Flow · since 0.23.0

Terminates program execution cleanly without error dialog.

Syntax

END

Example

PRINT "Program finished"
END

See also: STOP, SYSTEM

↑ Back to top

EVENT

Control Flow · since 0.23.0

Trigger a registered event. The optional target tells the runtime who runs the handler — and that "who" cascades into where authority lives, which routing the call takes, and which interpreter context the handler executes in.

In-process (the default)

EVENT "button_click"

Fires the matching CALL EVENT handler in the current interpreter, asynchronously on a background thread. No network involvement, no targeting — the right tool for single-interpreter dispatch, state-machine transitions, internal event buses, debounced UI, etc.

Client → server-side (only callable from a local-client interpreter)

Form Where it fires
EVENT SERVER "name" (args) this player's player-server (their own slot)
EVENT HOST "name" (args) the host (slot 0)

Use these when the client wants the server to make a decision. Client says "I pressed jump"; the server-side handler runs the authoritative reaction — position update, sound trigger, score change.

Server → server-side (callable from host or player-server)

Form Target Authority
EVENT SERVER n "name" (args) slot n's player-server host can target any n; a player-server can only target its own slot
EVENT SERVERS "name" (args) every player-server host-only
EVENT SERVERS ROOM "R" "name" (args) every player-server whose player is in room R host-only

For server-side fan-out — round-end ticks, per-room game-state changes, cross-slot coordination.

Server → client (callable from host or player-server)

Form Target Authority
EVENT CLIENT n "name" (args) slot n's local-client host can target any n; a player-server can only target its own slot
EVENT CLIENTS "name" (args) every connected client host-only
EVENT CLIENTS ROOM "R" "name" (args) every client whose player is in room R host-only

Server tells clients to do client-side things — render an animation, play a sound, show a notification. This is the right channel for anything in the "server wants the player to see X" bucket. Rendering and audio are client-side, period.

Authority

host > player-server > local-client. Cross-slot writes from a non-host server interpreter are a fatal runtime error pointing you at EVENT HOST (the right channel for client→host coordination). Broadcast and room-scoped variants from non-host interpreters are also fatal. The runtime won't let you accidentally write code that works on the host and errors elsewhere.

Arity must match

The matching CALL EVENT handler's parameter count must match the call exactly. A handler registered as CALL EVENT MyHandler(0, 0) AS "Foo" expects exactly two args on any EVENT ... "Foo" — extras or shortfalls are a fatal runtime error.

Args that travel over the wire

Cross-the-wire EVENTs (every form except the in-process default) accept these arg types: Integer, Float, String, Coordinate, Mutation (serialized as a coordinate sequence), Array (1D only), and Object of those types. Pass anything else and the runtime fails loudly with the offending argument's index — function values, multi-dimensional arrays, and Range are the usual suspects.

Single-player passthrough

Cross-the-wire EVENTs are a silent no-op in single-player — there's no other interpreter to deliver to. Intentional: the same .bas runs in both modes, so you can author the network-aware path without sprinkling IF ME.ISHOST THEN everywhere just to dodge runtime errors at startup.

Syntax

EVENT [<target>] "event_name" [(arg1, arg2, ...)]
  target ::= SERVER | SERVER n | SERVERS | SERVERS ROOM "name"
           | HOST
           | CLIENT n | CLIENTS | CLIENTS ROOM "name"

Parameters

  • - Optional. See description for each form's routing + authority. Absent = in-process synchronous-style dispatch in the calling interpreter.
  • n - Slot number (0..255). Required for EVENT SERVER n / EVENT CLIENT n. The host is slot 0.
  • "R" - Room name (string expression). For ROOM variants, only player-servers / clients whose ME.ROOM$ matches receive the event.
  • event_name - String identifier of the event to trigger (must match a registered CALL EVENT handler in the target context).
  • (arg1, arg2, ...) - Optional positional arguments. For in-process EVENT, passed as dynamic args to a handler registered with empty parens. For cross-the-wire forms, the handler's arity must match exactly.

Examples

' In-process — single-interpreter dispatch (the default)
DECLARE SUB PlaySound()
CALL EVENT PlaySound() AS "button_click"
EVENT "button_click"
SUB PlaySound()
  BEEP
END SUB
' Client → its own player-server (CrashNet)
IF ME.ISCLIENT AND KEYDOWN("SPACE") THEN
  EVENT SERVER "Jump" (MOUSEX, MOUSEY)
END IF
IF ME.ISSERVER AND NOT ME.ISHOST THEN
  CALL EVENT OnJump(0, 0) AS "Jump"
END IF
SUB OnJump(x, y)
  ' Authoritative reaction on the player-server
END SUB
' Client → host (authoritative)
IF ME.ISCLIENT AND KEYDOWN("B") THEN
  EVENT HOST "Build" ("barracks", MOUSEX, MOUSEY)
END IF
IF ME.ISHOST THEN
  CALL EVENT OnBuild("", 0, 0) AS "Build"
END IF
SUB OnBuild(kind$, x, y)
  ' Host-side validation + state mutation
  HOST.buildings = HOST.buildings + 1
END SUB
' Host → all clients (broadcast — system message overlay)
IF ME.ISHOST THEN
  EVENT CLIENTS "GameStart" ("Round 1 — fight!")
END IF
SUB OnGameStart(message$)
  IF ME.ISCLIENT THEN LOCATE 1, 1: PRINT message$
END SUB
CALL EVENT OnGameStart("") AS "GameStart"
' Host → one specific client (private notification)
IF ME.ISHOST AND playerWon THEN
  EVENT CLIENT winnerSlot "Victory" ("You win!")
END IF
' Host → all clients in a room (room-scoped announcement)
IF ME.ISHOST THEN
  EVENT CLIENTS ROOM "dungeon" "BossSpawn" ("Dragon appears!")
END IF
' Host → all player-servers (server-side fan-out)
IF ME.ISHOST THEN
  EVENT SERVERS "RoundEnd" (HOST.roundNumber)
END IF
IF ME.ISSERVER AND NOT ME.ISHOST THEN
  CALL EVENT OnRoundEnd(0) AS "RoundEnd"
END IF
SUB OnRoundEnd(round)
  ' Each player-server runs its own end-of-round logic
END SUB

See also: CALL EVENT, SUB, STOP THREAD, CRASHNET, ME

↑ Back to top

EXIT

Control Flow · since 0.23.0

Exits early from loops or procedures.

Syntax

EXIT FOR
EXIT DO
EXIT WHILE
EXIT SUB
EXIT FUNCTION

Examples

FOR I = 1 TO 100
  IF I = 50 THEN EXIT FOR
  PRINT I
NEXT
DO
  INPUT "Enter number: ", n
  IF n = 0 THEN EXIT DO
LOOP

See also: FOR, DO, WHILE, SUB, FUNCTION

↑ Back to top

FOR

Control Flow · since 0.23.0

Repeats a block of code a specific number of times.

Syntax

FOR variable = start TO end [STEP increment]
  statements
NEXT [variable]

Parameters

  • variable - Loop counter
  • start - Initial value
  • end - Final value
  • increment - Step amount (default: 1)

Examples

FOR I = 1 TO 10
  PRINT I
NEXT I
FOR X = 0 TO 320 STEP 10
  PSET (X, 100), 15
NEXT

See also: FOR EACH, WHILE, DO, EXIT

↑ Back to top

FOR EACH

Control Flow · since 1.12.0

Iterates over all elements of an array or all entries of an object. The single-variable form sets the loop variable to each value. The two-variable form sets the first variable to the key (string for objects, 0-based index for arrays) and the second to the value. The collection is snapshot at loop start, so modifications during iteration are safe. Supports EXIT FOR to break out early.

Syntax

FOR EACH value IN collection
  statements
NEXT

FOR EACH key, value IN collection
  statements
NEXT

Parameters

  • value - Loop variable set to each element value
  • key - (Optional) Loop variable set to the key (object property name or array index)
  • collection - Array or object expression to iterate over

Examples

DIM scores(3) AS INTEGER
scores(0) = 10
scores(1) = 20
scores(2) = 30
total = 0
FOR EACH s IN scores
  total = total + s
NEXT
PRINT total  ' 60
names = ["Alice", "Bob", "Charlie"]
FOR EACH name IN names
  PRINT name
NEXT
' Iterate object values
player = {"HP": 100, "MP": 50, "STR": 12}
FOR EACH stat IN player
  PRINT stat
NEXT
' Iterate object key-value pairs
player = {"HP": 100, "MP": 50}
FOR EACH name, value IN player
  PRINT name; " = "; value
NEXT
' Array with index
scores = [100, 85, 92]
FOR EACH i, score IN scores
  PRINT "Player"; i; ": "; score
NEXT

See also: FOR, EXIT, DIM

↑ Back to top

FUNCTION

Control Flow · since 0.23.0

Defines a function that returns a value when called. All parameters are always pass-by-value (BYVAL) to prevent side effects. BYREF is not allowed in FUNCTION — use SUB if you need to modify the caller's variables.

Type-bound methods. Qualify the name with Type:: to bind the function as a method of a user TYPE. Inside the body, SELF is an implied reference to the instance, and you return as usual (Method = value or RETURN value). Call a function method in expression position: x = obj.Method(), IF obj.Method() THEN ..., or on array elements (x = enemies(i).GetHP()).

Syntax

FUNCTION name [(parameters)] [AS datatype]
  statements
  name = return_value
END FUNCTION

FUNCTION Type::Method [(parameters)] [AS datatype]   ' type-bound method
  Method = return_value
END FUNCTION

Parameters

  • name - Function name
  • parameters - Optional parameter list (BYVAL only, BYREF not allowed)
  • datatype - Return type (INTEGER, STRING, etc.)

Examples

FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
  Add = a + b
END FUNCTION

result = Add(5, 3)
FUNCTION GetPlayerName$() AS STRING
  INPUT "Enter name: ", n$
  GetPlayerName$ = n$
END FUNCTION
' Parameters are always passed by value in FUNCTION
FUNCTION IncrementAndReturn(n AS INTEGER) AS INTEGER
  n = n + 1  ' Only affects local copy
  IncrementAndReturn = n
END FUNCTION

value = 5
result = IncrementAndReturn(value)
PRINT value   ' Still 5 (not modified)
PRINT result  ' Prints 6

See also: SUB, DECLARE, BYVAL, TYPE, SELF

↑ Back to top

IF

Control Flow · since 0.23.0

Executes different code blocks based on conditions.

Syntax

IF condition THEN statement [ELSE statement]
IF condition THEN
  statements
[ELSE
  statements]
END IF

Parameters

  • condition - Expression that evaluates to true/false
  • statement - Code to execute

Examples

IF x > 10 THEN PRINT "Large"
IF score >= 90 THEN
  PRINT "Grade: A"
ELSE
  PRINT "Grade: B"
END IF

See also: ELSEIF, SELECT CASE

↑ Back to top

INSTATE$

Control Flow · since 0.23.0

Returns 1 if the current state should continue executing, 0 if the state needs to advance and the subroutine should willingly exit loops and return. Takes no arguments.

Syntax

INSTATE$

Examples

SUB GameLoop()
  DO
    CALL UpdateGame()
    CALL DrawScreen()
    IF NOT INSTATE$ THEN EXIT DO  ' Exit if state should advance
  LOOP
END SUB
FOR i = 1 TO 1000
  CALL ProcessItem(i)
  IF NOT INSTATE$ THEN EXIT FOR  ' Allow state transition
NEXT i
SUB LongRunningTask()
  FOR step = 1 TO 10000
    CALL DoWork(step)
    ' Check if state should advance
    IF NOT INSTATE$ THEN
      CALL Cleanup()
      EXIT FOR
    END IF
  NEXT step
END SUB

See also: SET STATE, EXIT

↑ Back to top

RESOLVE

Control Flow · since 1.30.0

Blocks the current thread until the promise (or every promise) completes, then returns the resolved value(s). One promise → the value as-is (an async that returned an array stays an array). Multiple promises → an array of values in declaration order. Re-resolving the same promise returns the cached value, so you can ask twice without re-running the async work.

Fatal runtime error if any argument isn't a Promise, or if no arguments are given. If an async function inside the chain failed with a runtime error, the program halts (async errors are not isolated to the promise — see ASYNC).

Syntax

value = RESOLVE(promise)
values = RESOLVE(p1, p2, ..., pN)

Parameters

  • promise(s) - One or more Promise values returned by ASYNC. Other types halt loud.

Examples

p = ASYNC ComputeStuff()
result = RESOLVE(p)
REM Multiple promises → array result
p1 = ASYNC FetchA()
p2 = ASYNC FetchB()
results = RESOLVE(p1, p2)
a = results(0)
b = results(1)
REM Re-resolve returns the cached value
p = ASYNC Slow()
first = RESOLVE(p)
second = RESOLVE(p)  ' same value, no second wait

See also: ASYNC, FETCH

↑ Back to top

SELECT CASE

Control Flow · since 0.23.0

Multi-way branching based on expression value. Each CASE clause can match a single value, multiple comma-separated values, a range (value TO value), or a comparison (IS followed by a comparison operator and value).

Syntax

SELECT CASE expression
  CASE value1, value2, ...
    statements
  CASE value3 TO value4
    statements
  CASE IS > value5
    statements
  CASE ELSE
    statements
END SELECT

Parameters

  • expression - Value to compare against cases
  • CASE value - Matches a single value
  • CASE value1, value2, ... - Matches any of the listed values
  • CASE value1 TO value2 - Matches any value in the range (inclusive)
  • CASE IS value - Matches using a comparison operator (=, <, >, <=, >=, <>)

Examples

SELECT CASE grade
  CASE 90 TO 100
    PRINT "A"
  CASE 80 TO 89
    PRINT "B"
  CASE ELSE
    PRINT "C"
END SELECT
SELECT CASE direction$
  CASE "N", "S"
    PRINT "Vertical"
  CASE "E", "W"
    PRINT "Horizontal"
  CASE ELSE
    PRINT "Unknown"
END SELECT
SELECT CASE score
  CASE IS >= 100
    PRINT "High score!"
  CASE IS > 50
    PRINT "Not bad"
  CASE ELSE
    PRINT "Try again"
END SELECT

See also: IF, CASE

↑ Back to top

SET STATE

Control Flow · since 0.23.0

Changes the current program state for state machine-based control flow. The transition to the next state occurs when the current state SUB exits. If no state is queued, program flow continues normally after the SUB completes.

Syntax

SET STATE "state_name"

Parameters

  • state_name - String identifier matching an existing SUB name

Examples

SET STATE "MenuState"
SET STATE "PlayingState"
SET STATE "GameOverState"
' State-based game with matching SUB names
DECLARE SUB MenuState()
DECLARE SUB PlayingState()
DECLARE SUB GameOverState()

' Start with menu state
SET STATE "MenuState"

SUB MenuState()
  DO
    PRINT "Main Menu - Press S to start"
    keyPressed$ = INKEY$
    IF keyPressed$ = "s" THEN
      SET STATE "PlayingState"
      EXIT DO
    END IF
    IF INSTATE$=0 THEN EXIT DO
    DELAY 1
  LOOP
END SUB

SUB PlayingState()
  DO
    PRINT "Playing... Health:"; playerHealth
    IF playerHealth <= 0 THEN
      SET STATE "GameOverState"
      EXIT DO
    END IF
    IF INSTATE$=0 THEN EXIT DO
    DELAY 1
  LOOP
END SUB

SUB GameOverState()
  PRINT "Game Over!"
END SUB

See also: INSTATE$

↑ Back to top

SET YIELD

Control Flow

Sets how long YIELD pauses, in milliseconds. The default is 16 (~60fps), which keeps an unpaced game loop from free-running and flooding the graphics command stream on devices whose renderer can't drain it that fast. SET YIELD 1 (or 0) frees the loop to run as fast as the machine allows — do this only when you pace your own rendering, for example with COOLDOWN, or motion may run far faster than intended. The setting persists until changed and applies to every subsequent YIELD.

Syntax

SET YIELD milliseconds

Parameters

  • milliseconds - How long YIELD pauses. 16 is the default (~60fps); 1 is ultrafast/near-free-running

Examples

' Opt into a free-running loop, but pace rendering with COOLDOWN
SET YIELD 1
DO
  COOLDOWN 1, 60
    CALL DrawFrame()
  END COOLDOWN
  CALL UpdateSimulation()
  YIELD
LOOP
' Restore the default ~60fps cadence
SET YIELD 16

See also: YIELD, COOLDOWN, DELAY

↑ Back to top

STOP RENDER

Control Flow · since 0.23.0

Stops render callback execution. Without render_id, stops the current render callback.

Syntax

STOP RENDER ["render_id"]

Parameters

  • render_id - String identifier of render callback to stop (optional)

Examples

STOP RENDER "hud"
STOP RENDER  ' Stop current render context

See also: CALL RENDER, STOP TIMER, STOP THREAD

↑ Back to top

STOP THREAD

Control Flow · since 0.23.0

Terminates background thread execution. Without thread_id, stops the current thread. Also stops event handlers registered via CALL EVENT - use the event name directly (e.g., STOP THREAD "PLAYER_FRAME" stops all running PLAYER_FRAME handlers). When multiple handlers of the same event are running concurrently, this stops all of them.

Syntax

STOP THREAD ["thread_id"]

Parameters

  • thread_id - String identifier of thread or event to stop (optional)

Examples

STOP THREAD "loader"
STOP THREAD  ' Stop current thread context
STOP THREAD "PLAYER_FRAME"  ' Stop all running PLAYER_FRAME event handlers

See also: CALL THREAD, CALL EVENT, STOP TIMER, STOP RENDER

↑ Back to top

STOP TIMER

Control Flow · since 0.23.0

Stops scheduled timer execution. Without timer_id, stops the current timer.

Syntax

STOP TIMER ["timer_id"]

Parameters

  • timer_id - String identifier of timer to stop (optional)

Examples

STOP TIMER "gameLoop"
STOP TIMER  ' Stop current timer context

See also: CALL TIMER, STOP THREAD, STOP RENDER

↑ Back to top

SUB

Control Flow · since 0.23.0

Defines a subroutine that can be called with CALL statement. Parameters default to BYREF (pass by reference) unless BYVAL is specified. Literals are always passed by value regardless of mode. Use BYVAL when you want to protect the caller's variables from modification.

Type-bound methods. Qualify the name with Type:: to bind the sub as a method of a user TYPE. Inside the body, SELF is an implied reference to the instance. Invoke it with CALL obj.Method(args) — including on array elements (CALL enemies(i).Method()) and in the async forms (CALL THREAD obj.Method() AS "id"). A method behaves exactly like any other sub otherwise; the only addition is SELF.

Syntax

SUB name [(parameters)]
  statements
END SUB

SUB Type::Method [(parameters)]   ' type-bound method
  statements
END SUB

Parameters

  • name - Subroutine name
  • parameters - Optional parameter list with BYVAL/BYREF modifiers

Examples

SUB PrintMessage(BYVAL msg AS STRING)
  PRINT "Message: "; msg
END SUB

CALL PrintMessage("Hello")
SUB DrawBox(x1, y1, x2, y2, color)
  LINE (x1, y1)-(x2, y2), color, B
END SUB
score = 100  ' Global variable

SUB UpdateScore(points)
  score = score + points  ' Accesses global
  PRINT "New score:"; score
END SUB

CALL UpdateScore(50)
PRINT "Final score:"; score  ' Prints 150
' Using BYREF to modify caller's variable
SUB Increment(BYREF n AS INTEGER)
  n = n + 1
END SUB

counter = 0
CALL Increment(counter)
PRINT counter  ' Prints 1
' Swap two values using BYREF
SUB Swap(BYREF a AS INTEGER, BYREF b AS INTEGER)
  DIM temp AS INTEGER
  temp = a: a = b: b = temp
END SUB

x = 10: y = 20
CALL Swap(x, y)
PRINT x; y  ' Prints 20 10

See also: FUNCTION, DECLARE, CALL, BYVAL, BYREF, TYPE, SELF

↑ Back to top

WHILE

Control Flow · since 0.23.0

Repeats code while a condition remains true.

Syntax

WHILE condition
  statements
WEND

Parameters

  • condition - Expression to test each iteration

Example

INPUT "Enter number: ", num
WHILE num < 1 OR num > 10
  PRINT "Invalid!"
  INPUT "Enter number: ", num
WEND

See also: DO, FOR

↑ Back to top

WITH

Control Flow · since 1.29.0

WITH ... END WITH is the context primitive — step into a temporary scope, do some stuff, and the moment you leave the block (normal exit, EXIT FOR/DO, RETURN, or even a halting error) everything snaps back to how it was. Four flavors, split into two camps:

Ambient context (LAYER, ROOM) — pushes a frame onto an internal stack, the body reads from it, the frame pops on exit. Pure save-and-restore.

Compile-time hoisting (PLAYER, CLIENT) — the AST builder lifts the body out into a synthetic top-level SUB, auto-registers it as an event handler via CALL EVENT, and replaces the WITH site with the appropriate EVENT fire. WITH PLAYER becomes EVENT SERVER <slot> (ships to that player's PLAYER-SERVER interpreter); WITH CLIENT becomes EVENT CLIENTS-with-contextual-routing (broadcast from host, own-slot from player-server). Same .crashcart runs on every interpreter, so the synthetic SUB is already registered everywhere by the time the event lands.

`WITH LAYER n`

Block-scoped SET LAYER. The current draw layer becomes n for the body of the block, then snaps back to whatever it was before. No more save-the-old-value / do-the-work / restore-it boilerplate. Negative values clamp to 0 (matching what SET LAYER already tolerates).

WITH LAYER 3
    CLS 0
    PRINT "HUD"
END WITH
' previous layer restored automatically

`WITH ROOM "name"`

Every tilemap, sprite, and layer registration inside the block gets tagged with that room. The host's CREATE MAP / UPDATE MAP ships broadcasts that only reach clients whose ME.ROOM matches. Late-join replay walks the per-room snapshot. Single-player gets level-switching for free — WITH ROOM "level1" to set things up, later SET ROOM "level1" and the engine auto-mounts everything tagged level1. No manual destroy-and-recreate.

`WITH PLAYER n SEND (args)`

Ship the block body to player n's PLAYER-SERVER interpreter (the slot-N server-side runner) for one-shot execution. Server-authoritative state changes — SET ROOM, mutating PLAYERS(n).<field>, calling HOST.<field>, etc. — are legal inside because player-server is the right interpreter to make those changes "as that player." The compiler wraps the block in a synthetic SUB __with_anon_N(args), registers it as an event handler, and replaces the WITH site with EVENT SERVER n "__with_anon_N" (args). The args get evaluated BYVAL in the host's current scope at the WITH site and travel with the event.

' Host-side: handler fires when a new player joins, ships SET ROOM to
' the player-server so the new player lands in the entrance.
SUB OnPlayerJoin(p)
    WITH PLAYER p.slot SEND (p)
        ENABLE SPRITE p.slot
        SET SPRITE p.slot, 32, 32, "player", 0
        SET SPRITE p.slot SYNC("Entrance")
        SET ROOM "Entrance"
    END WITH
END SUB
CALL PLAYER OnPlayerJoin() GUARD(HOST) AS "PlayerHandler"

The SEND (args) clause is optional. Skip it for a zero-arg block.

For client-side rendering dispatch (overlays on a single player's screen), use raw EVENT CLIENT n — there's no block-form equivalent because WITH PLAYER is now firmly server-side.

`WITH PLAYER n SYNC (args)`

Same shape as SEND, but the caller waits for the remote body to finish before continuing past END WITH. Use this when ordering matters — "run this player-server setup, then continue with host bookkeeping that depends on it being done."

The contract is ack-on-complete, no return value. The body just signals "done" — whether it ran cleanly or halted with a runtime error, the caller resumes. If the player goes away mid-body, the player-server interpreter is torn down by disconnect cleanup and the host's wait exits.

' Wait for the player-server to finish populating its initial state
' before announcing the join to other players.
SUB OnPlayerJoin(p)
    WITH PLAYER p.slot SYNC (p)
        ENABLE SPRITE p.slot
        SET SPRITE p.slot SYNC("Lobby")
        SET ROOM "Lobby"
    END WITH
    ' At this point the new player is fully set up.
    EVENT SERVERS "PlayerReady" (p.slot, p.name$)
END SUB

SYNC is host-only for WITH PLAYER. A player-server interpreter doing WITH PLAYER ME.SLOT SYNC on itself would deadlock its own pattern-tick (the thread that needs to dispatch the body is the one doing the waiting). Use SEND for own-slot dispatch from a player-server. If you genuinely need cross-slot SYNC from a player-server, route through EVENT HOST + a CALL HOST handler on the host that does the WITH PLAYER n SYNC for you.

While the host is waiting, its pattern-tick is parked — other host-side timers / CALL RENDER / OnPlayerJoin for additional incoming players are paused until the wait resolves. By design; that's what "wait" means. Use SEND if you want concurrent fire-and-forget.

`WITH CLIENT SEND (args)`

Same shape as WITH PLAYER, but the target depends on which server-side context you're calling from:

  • From the host: broadcasts to every connected client (same as EVENT CLIENTS). Game-start countdowns, lobby chat, "the boss is here, scatter" announcements.
  • From a player-server: fires on that player's own client only (same as EVENT CLIENT ME.SLOT). Useful when shared code wants "tell the client this server-side context is for, no broadcasting required."
  • From a multiplayer local-client: fatal runtime error. Clients don't have authority to fire client-side handlers through the network — the design constraint exists so client code can't impersonate the server.
  • In single-player: collapses to in-process dispatch (the synthetic SUB runs locally). Same .crashcart runs in solo and multiplayer without gating.
WITH CLIENT SEND (countdown)
    PRINT "Starting in " + STR$(countdown) + "..."
END WITH

Writing WITH CLIENT from shared code that runs in both host and player-server contexts is the intended use — the routing collapses to "the right thing" without IF ME.ISHOST gating.

`WITH CLIENT SYNC (args)`

Player-server waits for the body to finish on its own client before continuing. Useful for client-side animations / transitions that have to complete before server-side state advances.

' Player-server side: wait for the client's fade-to-black to finish
' before swapping rooms server-authoritatively.
WITH CLIENT SYNC
    WITH LAYER 1
        CLS 0 EFFECT(DISSOLVE) SPEED 30
        WHILE TRANSITION() <> ""
            YIELD
        WEND
    END WITH
END WITH
SET ROOM "Lobby"

SYNC is player-server-only for WITH CLIENT. It targets the calling player-server's own client. Host-side WITH CLIENT SYNC is fatal — broadcasting to all clients and waiting for every one to ack has no coherent semantic (slowest client dictates the wait, single disconnect blocks the host). If you need confirmation from every client after a broadcast, fire async (WITH CLIENT SEND) and have each client's handler write a PLAYER DATA flag the host polls.

Nesting

LAYER and ROOM compose orthogonally — they're different scoping axes, so an outer WITH LAYER 3 and an inner WITH ROOM "lobby" both apply inside the inner block. Same-kind nesting shadows: inner wins, and when the inner block exits, the outer's value comes back.

PLAYER and CLIENT can't be meaningfully nested inside each other — they're one-shot fires, not ambient contexts.

Defaults outside any `WITH` block

  • LAYER resolves to whatever SET LAYER last set (default 0).
  • ROOM falls back to the _global sentinel on the host (visible to every connected player). Everywhere else, it falls back to ME.ROOM.

Why `SEND` / `SYNC`?

The keyword separates the slot expression from the args list because BASIC's expression parser eagerly consumes trailing parens as subscripts. Without it, WITH PLAYER 2 (host_score) would parse as WITH PLAYER 2(host_score) — a function call on the literal 2. The keyword also doubles as the async-vs-sync selector: SEND = fire-and-forget, SYNC = wait for ack. Omitting the keyword entirely is equivalent to SEND (zero-arg async).

Args are BYVAL

Full stop. The block body executes on a different interpreter context — sometimes a different machine. It can't mutate host state remotely, and there's no sync-back path even with SYNC (which is ack-only, not return-value). If you need to mutate server state, write to PLAYERS(n).field directly; the existing PLAYER DATA machinery handles propagation.

Syntax

WITH LAYER <expr>
    statements
END WITH

WITH ROOM <expr>
    statements
END WITH

WITH PLAYER <slot> [SEND|SYNC (arg1, arg2, ...)]
    statements
END WITH

WITH CLIENT [SEND|SYNC (arg1, arg2, ...)]
    statements
END WITH

Parameters

  • LAYER expr - WITH LAYER variant: numeric layer ID (0-99) for the block-scoped active draw layer.
  • ROOM expr - WITH ROOM variant: string room name. Tilemap commands inside the block tag with this room for replication/filter purposes.
  • PLAYER slot - WITH PLAYER variant: numeric slot expression for the target player. The block ships to that player's player-server interpreter for server-authoritative execution.
  • CLIENT - WITH CLIENT variant: no slot expression — broadcasts to every connected client.
  • SEND (args) - Optional. Comma-separated identifier list. Each name binds a host-scope value (looked up via Expr::Var) to a same-named BYVAL parameter on the synthetic SUB. Omit to ship a zero-arg block.

Examples

REM Block-scoped draw layer — replaces the SET LAYER save/restore dance
WITH LAYER 3        ' HUD layer
    CLS 0
    PRINT "Score: " + STR$(score)
END WITH            ' previous layer auto-restored
REM Host sets up four levels' worth of tilemap data, room-tagged
IF ME.ISHOST THEN
    FOR i = 1 TO 4
        WITH ROOM "level" + STR$(i)
            TILEMAP "lvl" + STR$(i)
                TILESET "grass.png"
                TILESIZE 32, 32
                SIZE 100, 60
                TILES 0 FROM tiles
            END TILEMAP
            CREATE MAP "lvl" + STR$(i) AS LAYER 1
        END WITH
    NEXT i
END IF
REM Composing two orthogonal scopes
WITH ROOM "lobby"
    WITH LAYER 2
        CLS 0
        PRINT "Welcome to the lobby"
    END WITH
END WITH
REM Single-player level switching (with the same code)
WITH ROOM "level1"
    CREATE MAP "level1" AS LAYER 1
END WITH
SET ROOM "level1"  ' mounts everything tagged level1 automatically
REM Per-player state setup on join — block runs on player 2's player-server, where SET ROOM and PLAYERS(n) writes are authoritative
SUB OnPlayerJoin(p)
    WITH PLAYER p.slot SEND (p)
        ENABLE SPRITE p.slot
        SET SPRITE p.slot, 32, 32, "player", 0
        SET SPRITE p.slot SYNC("Entrance")
        SET ROOM "Entrance"
    END WITH
END SUB
CALL PLAYER OnPlayerJoin() GUARD(HOST) AS "PlayerHandler"
REM Broadcast game-start countdown to every connected client
countdown = 3
WITH CLIENT SEND (countdown)
    PRINT "Game starts in " + STR$(countdown) + "..."
END WITH
REM Zero-arg form — no SEND clause, the block ships as-is
WITH PLAYER 1
    PRINT "Welcome, you're the first to arrive."
END WITH

See also: SET LAYER, SET ROOM, TILEMAP, CREATE MAP, UPDATE MAP, EVENT, CALL EVENT

↑ Back to top

YIELD

Control Flow · since 1.0.3

Yields execution so the renderer, audio, behavior animations, and input each get a slice, then resumes — a cooperative yield point. By default YIELD pauses about 16ms (~60fps), which keeps an unpaced game loop from free-running and flooding the graphics command stream on devices whose renderer can't drain it that fast. Tune the pause with SET YIELD <ms>: SET YIELD 1 frees the loop to run as fast as the machine allows — pair that with COOLDOWN so you pace your own rendering. Use YIELD in tight loops — especially game loops — to stay responsive; a loop with no YIELD trips the yield-starvation guard.

Express motion as a rate over time (e.g. pixels per second scaled by elapsed time) rather than a fixed step per iteration, so gameplay speed stays consistent regardless of the YIELD rate.

Syntax

YIELD

Examples

' Basic game loop
DO
  ' Update game logic
  YIELD
LOOP
' Smooth sprite animation with behaviors
DO
  IF KEYDOWN("LEFT") THEN playerX = playerX - 2
  IF KEYDOWN("RIGHT") THEN playerX = playerX + 2
  MOVE SPRITE 0, playerX, playerY
  YIELD  ' Allow behaviors to animate
LOOP
' Polling loop
WHILE NOT done
  IF INKEY$ <> "" THEN done = 1
  YIELD
WEND

See also: DELAY, BEHAVIOR

↑ Back to top

Structured Programming

BYVAL

Structured Programming · since 0.23.0

Pass parameter by value (copy) to SUB or FUNCTION.

Syntax

BYVAL parameter AS datatype

Parameters

  • parameter - Parameter name
  • datatype - INTEGER, STRING, etc.

Example

SUB Test(BYVAL x AS INTEGER)
  x = x + 1  ' Changes local copy only
END SUB

a = 5
CALL Test(a)
PRINT a  ' Still prints 5

See also: BYREF, SUB, FUNCTION

↑ Back to top

DECLARE

Structured Programming · since 0.23.0

Forward declares a SUB or FUNCTION so its signature is known up front. It is optional for ordinary calls: module-level FUNCTION/SUB definitions are auto-hoisted, so you can call them before they appear in the source — including mutual recursion and a 'main at the top, helpers below' layout — with or without DECLARE. DECLARE remains useful for documenting an interface and for QuickBASIC compatibility. Only module-level definitions (top level and inside NAMESPACE blocks) are hoisted; a FUNCTION/SUB nested inside a control-flow block (IF/WHILE/FOR/DO) registers when execution reaches it, so define those before use.

The parentheses are required, empty or not: DECLARE SUB Restart(), not DECLARE SUB Restart.

Syntax

DECLARE SUB name(parameters)
DECLARE FUNCTION name(parameters) [AS datatype]

Parameters

  • name - Subroutine or function name
  • parameters - Parameter list matching the actual definition. The parentheses are required even for a routine that takes none: DECLARE SUB Restart()

Example

DECLARE FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
DECLARE SUB PrintMessage(BYVAL msg AS STRING)

PRINT Add(5, 3)
CALL PrintMessage("Test")

FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
  Add = a + b
END FUNCTION

SUB PrintMessage(BYVAL msg AS STRING)
  PRINT msg
END SUB

See also: SUB, FUNCTION, CALL

↑ Back to top

FUNCTION

Structured Programming · since 0.23.0

Defines a function that returns a value when called. All parameters are always pass-by-value (BYVAL). BYREF is not allowed.

Syntax

FUNCTION name [(parameters)] [AS datatype]
  statements
  name = return_value
END FUNCTION

Parameters

  • name - Function name
  • parameters - Optional parameter list (BYVAL only, BYREF not allowed)
  • datatype - Return type (INTEGER, STRING, etc.)

Examples

FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
  Add = a + b
END FUNCTION

result = Add(5, 3)
FUNCTION GetPlayerName$() AS STRING
  INPUT "Enter name: ", n$
  GetPlayerName$ = n$
END FUNCTION

See also: SUB, DECLARE, CALL

↑ Back to top

IMPORT

Structured Programming · since 0.23.0

Imports functions, subroutines, and type definitions from another BASIC file in the same bundle. IMPORT may appear anywhere in the file — the imported file's contents are spliced in at the IMPORT's exact position, so you can pull a reusable block of code straight into the middle of an app, not just declare modules at the top. Errors in an imported file are reported against that file's name and line. IMPORT is a plain text splice with no include-once guard, by design: importing the same file more than once splices it each time, which simply re-declares its functions/subs (harmless — later definitions win) and keeps IMPORT predictable and flexible. Circular imports are still detected and rejected.

Syntax

IMPORT "filename.bas"

Parameters

  • filename - Name of the .bas file to import (within current bundle)
  • .bas extension is optional and will be added automatically
  • Files must exist in the same resource bundle (no subfolders supported)

Examples

IMPORT "math.bas"
IMPORT "graphics.bas"

' Now use imported functions
result = Calculate(10, 20)
CALL DrawSprite(100, 100)
' Import utility modules
IMPORT "utils"
IMPORT "physics.bas"
' main.bas
IMPORT "player.bas"
IMPORT "enemies.bas"

SCREEN 13
CALL InitPlayer()
CALL SpawnEnemies()

See also: DECLARE, SUB, FUNCTION, TYPE

↑ Back to top

NAMESPACE

Structured Programming · since 1.12.0

Groups functions, subroutines, and variables under a named scope. Inside the block, members are accessed directly by name. Outside the block, use Namespace.Member syntax (e.g., Utils.Clamp()) for both reads and writes. Multiple namespaces can define functions with the same name without conflict. Cross-namespace access works — code in one namespace can read and write variables in another namespace using the full Namespace.var path. See docs/variable-scoping.md for the full scoping rules.

Syntax

NAMESPACE name
  statements
END NAMESPACE

Parameters

  • name - Namespace identifier

Examples

NAMESPACE Utils
  FUNCTION Clamp(value, lo, hi)
    IF value < lo THEN
      Clamp = lo
    ELSEIF value > hi THEN
      Clamp = hi
    ELSE
      Clamp = value
    END IF
  END FUNCTION
END NAMESPACE

x = Utils.Clamp(150, 0, 100)  ' Returns 100
REM Namespace variables are readable and writable from outside.
REM Use mutator subs for complex logic.
NAMESPACE Player
  health = 100

  SUB TakeDamage(amount)
    health = health - amount
    IF health < 0 THEN health = 0
  END SUB
END NAMESPACE

PRINT Player.health                 ' 100
Player.health = 75                  ' Direct write from outside
PRINT Player.health                 ' 75
CALL Player.TakeDamage(25)          ' Or use a mutator sub
PRINT Player.health                 ' 50

See also: FUNCTION, SUB, CALL

↑ Back to top

SUB

Structured Programming · since 0.23.0

Defines a subroutine that can be called with CALL statement.

Syntax

SUB name [(parameters)]
  statements
END SUB

Parameters

  • name - Subroutine name
  • parameters - Optional parameter list with BYVAL/BYREF modifiers

Examples

SUB PrintMessage(BYVAL msg AS STRING)
  PRINT "Message: "; msg
END SUB

CALL PrintMessage("Hello")
SUB DrawBox(x1, y1, x2, y2, color)
  LINE (x1, y1)-(x2, y2), color, B
END SUB
score = 100  ' Global variable

SUB UpdateScore(points)
  score = score + points  ' Accesses global
  PRINT "New score:"; score
END SUB

CALL UpdateScore(50)
PRINT "Final score:"; score  ' Prints 150

See also: FUNCTION, DECLARE, CALL, BYVAL

↑ Back to top

Variables

Array Literal

Variables · since 1.12.0

Creates an inline array from a list of expressions. Elements are 0-indexed. The resulting array supports subscript access and FOR EACH iteration. Can be nested inside object literals.

Syntax

[expr, expr, ...]

Parameters

  • expr - Any expression for each element

Examples

scores = [10, 20, 30]
PRINT scores(0)  ' 10
names = ["Alice", "Bob"]
FOR EACH n IN names
  PRINT n
NEXT
data = {"values": [1, 2, 3], "label": "test"}

See also: Object Literal, FOR EACH, DIM

↑ Back to top

BEGIN ENUM

Variables · since 1.35.0

Declare a group of named integer constants, auto-numbered from 0.

List one member per line between BEGIN ENUM and END ENUM. Each member becomes a global constant (exactly like CONST — read-only, case-insensitive), numbered in order starting at 0:

BEGIN ENUM
  IDLE        ' 0
  WALKING     ' 1
  JUMPING     ' 2
END ENUM

state = IDLE
IF state = JUMPING THEN ...

This is cleaner than a stack of CONST lines when you just want a sequence of related values — game states, directions, message types, menu items.

Explicit values

A member can pin its own value with = expr (any integer expression). Members after it continue counting from there:

BEGIN ENUM
  OK              ' 0
  WARN  = 10      ' 10
  ERROR           ' 11  (continues from 10)
  FATAL = 20      ' 20
END ENUM

The name is optional

You may give the block a name for readability — BEGIN ENUM Direction — but it's decorative: members are still bare global constants (NORTH, not Direction.NORTH). Choose member names that won't collide with other constants or variables.

Notes

Members are defined when the BEGIN ENUM statement runs, just like CONST, so declare your enums before you use them (top of the program is the usual spot). Use trailing comments (RED ' the danger colour) rather than standalone comment lines between members.

Members are constants, so the same rule applies: a member name already in use as a variable is a loud error, not a silent promotion.

Syntax

BEGIN ENUM [Name]
  MEMBER1
  MEMBER2 [= value]
  ...
END ENUM

Parameters

  • Name - Optional, decorative block name (does not namespace the members)
  • MEMBER - One identifier per line; becomes a global integer constant
  • = value - Optional explicit integer value for a member; later members continue from value+1

Examples

REM Auto-numbered game states
BEGIN ENUM
  TITLE
  PLAYING
  PAUSED
  GAMEOVER
END ENUM

mode = TITLE
IF mode = PLAYING THEN CALL UpdateWorld()
REM Named enum with explicit values
BEGIN ENUM Direction
  NORTH = 0
  EAST  = 90
  SOUTH = 180
  WEST  = 270
END ENUM

facing = EAST
REM Explicit value, then continue auto-numbering
BEGIN ENUM
  HTTP_OK        = 200
  HTTP_CREATED          ' 201
  HTTP_NOTFOUND  = 404
  HTTP_TEAPOT           ' 405
END ENUM

Notes

Each member is registered via the same mechanism as CONST (read-only, namespace-aware via the active NAMESPACE, case-insensitive). Numbering starts at 0 and increments per member; an explicit = expr (evaluated like a CONST value) sets the member and resets the running counter to value+1. The optional name is parsed but not stored as a namespace — members are bare globals. Members are one-per-line (like TYPE fields); trailing comments and blank lines between members are fine, standalone comment lines are not. Defined at runtime when the statement executes (no hoisting), so declare before use.

See also: CONST, DIM, SELECT CASE

↑ Back to top

BEGIN SORT

Variables · since 1.38.0

Sorts a one-dimensional array IN PLACE using a custom comparator block. For each pair being compared, the two elements are bound to the named variables (a, b) and the block runs; its RETURN value is read as a three-way sign: negative if 'a' should sort BEFORE 'b' (ascending), 0 if they are equal, positive if 'a' sorts AFTER 'b'. The sort is stable (equal elements keep their original order). A RETURN inside the block yields only that comparison — it does NOT return from the enclosing FUNCTION/SUB. Sorting a non-1-D array, or a value that isn't an array, is a runtime error.

Syntax

BEGIN SORT array AS a, b
  ' compare a and b; RETURN a three-way sign
END SORT

Parameters

  • array - The one-dimensional array to sort in place (a variable, or any expression that evaluates to an array)
  • a - Name bound to the first element of each compared pair
  • b - Name bound to the second element of each compared pair

Examples

BEGIN SORT scores AS a, b
  RETURN a - b          ' ascending
END SORT
BEGIN SORT scores AS a, b
  RETURN b - a          ' descending
END SORT
' Sort an array of objects by a field
BEGIN SORT enemies AS a, b
  RETURN a.hp - b.hp
END SORT
' Explicit three-way comparator (e.g. for strings)
BEGIN SORT names AS a, b
  IF a < b THEN
    RETURN -1
  ELSEIF a > b THEN
    RETURN 1
  END IF
  RETURN 0
END SORT

See also: DIM, FOR EACH, CLONE

↑ Back to top

CLONE

Variables · since 1.38.0

Returns a deep, independent copy of a value. Crash BASIC arrays and objects are reference types: 'b = a' makes b an ALIAS of a (they share storage), so mutating one is seen through the other. CLONE(a) breaks that link, recursively duplicating nested arrays and objects so later mutations never bleed across. For scalars (Integer/Float/String) there is no shared storage, so CLONE just returns an equal value.

Syntax

CLONE(value)

Parameters

  • value - Any value. Arrays and Objects are deep-copied (recursively); scalars pass through unchanged.

Examples

DIM original(3) AS INTEGER
original(0) = 10
copy = CLONE(original)   ' independent snapshot
copy(0) = 99
PRINT original(0)        ' 10 (without CLONE it would be 99)
snapshot = CLONE(gameState)   ' deep copy of an object

See also: DIM, LET

↑ Back to top

CONST

Variables · since 0.23.0

Defines a constant that cannot be changed during execution.

A name is one thing or the other. Declaring a constant over a name already in use as a variable is a loud error rather than a silent promotion — every earlier assignment would become retroactively meaningless, and every later one would fail with "cannot assign to constant" pointing at a line that looks perfectly reasonable. Pick a different name, or drop the CONST.

Syntax

CONST name = value

Parameters

  • name - Constant name
  • value - Constant value (number or string)

Example

CONST PI = 3.14159
CONST MAX_PLAYERS = 4
CONST GAME_TITLE$ = "My Game"

See also: DIM, LET, BEGIN ENUM

↑ Back to top

DATA

Variables · since 1.0.0

Defines a named block of constant data that can be read sequentially at runtime. DATA blocks support numbers, strings, and expressions. Multiple data values can span multiple lines within the block. Use OPEN DATA to access the block and READ to retrieve values.

Syntax

DATA "blockname"
  value1, value2, value3
  value4, value5, value6
END DATA

Parameters

  • blockname - String name to identify the data block
  • values - Comma-separated numbers, strings, or expressions

Examples

DATA "colors"
  255, 0, 0
  0, 255, 0
  0, 0, 255
END DATA

OPEN DATA "colors" AS #1
DIM r AS INTEGER, g AS INTEGER, b AS INTEGER
READ #1, r, g, b
PRINT "Red: R=" + STR$(r) + " G=" + STR$(g) + " B=" + STR$(b)
CLOSE #1
DATA "enemies"
  "Goblin", 10, 2
  "Orc", 25, 5
  "Dragon", 100, 20
END DATA

OPEN DATA "enemies" AS #1
DIM name AS STRING, hp AS INTEGER, damage AS INTEGER
READ #1, name, hp, damage
PRINT "Enemy: " + name + " HP=" + STR$(hp)
CLOSE #1
DATA "mixed"
  "Player1", 100, 3.5
END DATA

OPEN DATA "mixed" AS #1
DIM name AS STRING, score AS INTEGER, multiplier AS SINGLE
READ #1, name, score, multiplier
PRINT name + " scored " + STR$(score)
CLOSE #1

See also: OPEN DATA, READ, RESTORE, CLOSE, EOF

↑ Back to top

DIM

Variables · since 0.23.0

Declares variables and allocates memory. AS datatype is required — there is no untyped DIM, and no inline initialization (DIM x = 0 does not parse; declare, then assign). Supports basic types, arrays, custom types, and Objects. Arrays are ZERO-BASED: DIM arr(N) allocates N elements indexed 0..N-1. A range bound is allowed only with a lower bound of 0 (e.g. DIM arr(0 TO 9) = 10 elements, indices 0-9); a non-zero lower bound such as DIM arr(1 TO 10) is a loud runtime error, not a silent re-base.

Custom types. DIM x AS SomeType (where SomeType is declared with TYPE) creates an instance pre-populated with the declared fields at their type defaults and tagged with the type, so it can carry methods. The type must be declared before the DIM; an unknown type name is a loud error, not a silent untyped object. Arrays of a custom type (DIM enemies(8) AS Enemy) give each element its own independent, tagged instance.

Syntax

DIM variable AS datatype
DIM variable(size) AS datatype
DIM variable(0 TO upper) AS datatype
DIM variable AS customtype

Parameters

  • variable - Variable name
  • datatype - INTEGER, LONG, SINGLE, DOUBLE, STRING, Object, or custom type name
  • size - Array element count (indices 0..size-1)
  • upper - Upper index in a 0 TO upper range bound (lower bound must be 0)

Examples

DIM name AS STRING
DIM scores(10) AS INTEGER
DIM grid(0 TO 9) AS INTEGER
DIM config AS Object
DIM player AS PlayerType

See also: LET, AS, TYPE, Object

↑ Back to top

ERASE

Variables · since 0.23.0

Clears arrays and frees memory.

Syntax

ERASE array_name [, array_name, ...]

Parameters

  • array_name - Names of arrays to erase

Example

DIM scores(100) AS INTEGER, names$(50) AS STRING
ERASE scores, names$

See also: DIM, REDIM

↑ Back to top

FALSE

Variables · since 1.57.0

The integer 0. Crash BASIC uses modern truthiness: TRUE is 1, FALSE is 0, and any non-zero value is truthy.

A pseudo-variable, so it needs no parentheses and cannot be assigned to.

Syntax

FALSE

Example

running = FALSE
WHILE running = FALSE
  YIELD
WEND

See also: TRUE, IF

↑ Back to top

MUTATION

Variables · since 1.4.3

Declares a MUTATION variable for polygon-style game entities. Mutations hold lists of 2D coordinates that can have behaviors applied, causing all points to move together as a unit. Can be rendered with POLYGON command. An array of mutations (DIM rocks(24) AS MUTATION) can be indexed wherever a mutation is expected, which is how a game pools bound polygons.

Reading is free; writing takes over. While a behavior drives a mutation you can read its points as often as you like — MUTATIONPOINTX / MUTATIONPOINTY report where the behavior has got to, which makes a mutation a perfectly good position tracker even when nothing renders it. WRITING it — re-assigning the variable, or SET MUTATIONPOINT — ends the behavior driving it and hands control to your program. That is deliberate: the shape a behavior was animating is no longer the shape you asserted, so the behavior is removed rather than fighting you for the points.

The consequence worth planning for: a shape that must KEEP its behavior can only be moved through its sprite half — SET POLYGON n, LOCATION(x, y) ROTATE(a) — never by writing points every frame.

Syntax

DIM varName AS MUTATION
DIM varName(maxPoints) AS MUTATION
varName = MUTATION((x1, y1), (x2, y2), ...)

Parameters

  • varName - Variable name for the mutation
  • maxPoints - Optional maximum number of points (omit for unlimited)
  • (x, y) - Point coordinates for mutation literal

Examples

'Fixed-size mutation (3 points max)
DIM triangle(3) AS MUTATION
triangle = MUTATION((0, 0), (50, 0), (25, 43))
'Dynamic mutation (unlimited points)
DIM path AS MUTATION
path = MUTATION((10, 20), (30, 40), (50, 60), (70, 80))
'Combine mutations with + operator
DIM combined AS MUTATION
combined = triangle + path
'Render mutation as polygon
POLYGON triangle, 14, 12  ' outline yellow, fill red

See also: SET MUTATION BEHAVIOR, SET MUTATIONPOINT, MUTATIONPOINTX, MUTATIONPOINTY, MUTATIONCOUNT, POLYGON, DIM

↑ Back to top

MUTATIONCOUNT

Variables · since 1.4.3

Returns the number of points in a MUTATION variable. Returns 0 if the variable is not a mutation or is empty. Reading is non-destructive: a mutation driven by a behavior can be read as often as you like and keeps running. It is WRITING it (SET MUTATIONPOINT, re-assignment) that ends the behavior.

Syntax

MUTATIONCOUNT(mutation)

Parameters

  • mutation - MUTATION variable or variable name

Examples

'Get point count
DIM triangle AS MUTATION
triangle = MUTATION((0, 0), (50, 0), (25, 43))
count = MUTATIONCOUNT(triangle)  ' Returns 3
'Iterate over all points
FOR i = 0 TO MUTATIONCOUNT(shape) - 1
  x = MUTATIONPOINTX(shape, i)
  y = MUTATIONPOINTY(shape, i)
  PSET (x, y), 15
NEXT i
'Check if mutation is empty
IF MUTATIONCOUNT(path) = 0 THEN
  PRINT "No points in path"
END IF

See also: MUTATIONPOINTX, MUTATIONPOINTY, SET MUTATIONPOINT, MUTATION

↑ Back to top

MUTATIONPOINTX

Variables · since 1.4.3

Returns the X coordinate of a point in a MUTATION variable at the specified index. Returns 0 if the index is out of bounds or the variable is not a mutation. Reading is non-destructive: a mutation driven by a behavior can be read as often as you like and keeps running. It is WRITING it (SET MUTATIONPOINT, re-assignment) that ends the behavior.

Syntax

MUTATIONPOINTX(mutation, index)

Parameters

  • mutation - MUTATION variable or variable name
  • index - Zero-based index of the point

Examples

'Get X coordinate of first point
DIM path AS MUTATION
path = MUTATION((100, 50), (200, 150))
x = MUTATIONPOINTX(path, 0)  ' Returns 100
'Print all point coordinates
FOR i = 0 TO MUTATIONCOUNT(path) - 1
  PRINT "Point " + STR$(i) + ": " + STR$(MUTATIONPOINTX(path, i)) + ", " + STR$(MUTATIONPOINTY(path, i))
NEXT i

See also: MUTATIONPOINTY, MUTATIONCOUNT, SET MUTATIONPOINT, MUTATION

↑ Back to top

MUTATIONPOINTY

Variables · since 1.4.3

Returns the Y coordinate of a point in a MUTATION variable at the specified index. Returns 0 if the index is out of bounds or the variable is not a mutation. Reading is non-destructive: a mutation driven by a behavior can be read as often as you like and keeps running. It is WRITING it (SET MUTATIONPOINT, re-assignment) that ends the behavior.

Syntax

MUTATIONPOINTY(mutation, index)

Parameters

  • mutation - MUTATION variable or variable name
  • index - Zero-based index of the point

Examples

'Get Y coordinate of first point
DIM path AS MUTATION
path = MUTATION((100, 50), (200, 150))
y = MUTATIONPOINTY(path, 0)  ' Returns 50
'Calculate bounding box
minY = MUTATIONPOINTY(shape, 0)
maxY = minY
FOR i = 1 TO MUTATIONCOUNT(shape) - 1
  y = MUTATIONPOINTY(shape, i)
  IF y < minY THEN minY = y
  IF y > maxY THEN maxY = y
NEXT i

See also: MUTATIONPOINTX, MUTATIONCOUNT, SET MUTATIONPOINT, MUTATION

↑ Back to top

OPEN DATA

Variables · since 1.0.0

Opens a named DATA block for sequential reading. The file number is used with READ to retrieve values from the block. Multiple DATA blocks can be open simultaneously with different file numbers.

Syntax

OPEN DATA "blockname" AS #filenumber

Parameters

  • blockname - String name of the DATA block to open
  • filenumber - Numeric file handle (1-255) for accessing the data

Examples

DATA "scores"
  100, 200, 300
END DATA

OPEN DATA "scores" AS #1
DIM score AS INTEGER
READ #1, score
PRINT STR$(score)
CLOSE #1
' Open multiple DATA blocks simultaneously
DATA "block1"
  1, 2, 3
END DATA

DATA "block2"
  100, 200, 300
END DATA

OPEN DATA "block1" AS #1
OPEN DATA "block2" AS #2
DIM a AS INTEGER, b AS INTEGER
READ #1, a
READ #2, b
PRINT STR$(a) + " and " + STR$(b)
CLOSE #1
CLOSE #2

See also: DATA, READ, CLOSE, RESTORE

↑ Back to top

Object Literal

Variables · since 1.12.0

Creates an inline object with key-value pairs. Keys can be quoted strings or bare identifiers. Values can be any expression, including nested objects and array literals. The resulting object supports dot notation (obj.key) and subscript access (obj("key")).

Syntax

{key: value, key2: value2, ...}

Parameters

  • key - Property name (string or identifier)
  • value - Any expression

Examples

player = {"name": "Dan", "score": 100}
PRINT player.NAME
config = {x: 10, y: 20, size: 32}
PRINT config.X + config.Y
game = {"player": {"x": 100, "y": 200}, "level": 1}
PRINT game.PLAYER.X

See also: Array Literal, DIM

↑ Back to top

PUSH

Variables · since 0.23.0

Adds an element to the end of a 1D array. This is an alias for QUEUE, but typically used with POP for LIFO (Last In, First Out) stack operations. It always appends a new element to the end (the array grows by one each call; it does not fill any pre-DIMmed empty slots). Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error.

Syntax

PUSH array, value

Parameters

  • array - 1D array variable to push onto
  • value - Expression or value to add to the end of the array

Examples

DIM stack(10) AS INTEGER
PUSH stack, 10
PUSH stack, 20
PUSH stack, 30
' LIFO stack: use with POP
PUSH myStack, 100
PUSH myStack, 200
value = POP(myStack)  ' Returns 200 (last in)
DIM history(5) AS STRING
PUSH history, "Action 1"
PUSH history, "Action 2"

See also: QUEUE, POP, DIM, REDIM

↑ Back to top

QUEUE

Variables · since 0.23.0

Adds an element to the end of a 1D array. It always appends a new element to the end (the array grows by one each call; it does not fill any pre-DIMmed empty slots). Used for FIFO (First In, First Out) queue operations. Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error, since appending one element would leave the array's shape inconsistent with its data.

Syntax

QUEUE array, value

Parameters

  • array - 1D array variable to append to
  • value - Expression or value to add to the end of the array

Examples

DIM tasks(10) AS STRING
QUEUE tasks, "Process order"
QUEUE tasks, "Send email"
DIM numbers(5) AS INTEGER
FOR i = 1 TO 10
  QUEUE numbers, i * 2
NEXT
' FIFO queue: use with DEQUEUE
QUEUE myQueue, "first"
QUEUE myQueue, "second"
item$ = DEQUEUE(myQueue)  ' Returns "first"

See also: PUSH, DEQUEUE, DIM, REDIM

↑ Back to top

READ

Variables · since 1.0.0

Reads the next value(s) from an open handle into variables.

For a DATA block handle (opened with OPEN DATA), values are read sequentially from where the last READ left off — one DATA element per variable, preserving the original type.

For a file-stream handle (opened with OPEN ... FOR INPUT), one line of the file is consumed per variable and auto-parsed: integers go to INTEGER, decimals to FLOAT, anything else stays a STRING. To capture a line verbatim without auto-parsing, use LINE INPUT instead.

Reading past the end raises a runtime error — gate the loop on EOF.

Syntax

READ #filenumber, var1 [, var2, var3, ...]

Parameters

  • filenumber - Handle from OPEN DATA or OPEN ... FOR INPUT
  • var1, var2, ... - Variables to receive the values (one DATA element OR one file line each)

Examples

DATA "numbers"
  10, 20, 30, 40, 50, 60
END DATA

OPEN DATA "numbers" AS #1
DIM a AS INTEGER, b AS INTEGER, c AS INTEGER
READ #1, a, b, c
PRINT STR$(a) + " " + STR$(b) + " " + STR$(c)
READ #1, a, b, c
PRINT STR$(a) + " " + STR$(b) + " " + STR$(c)
CLOSE #1
' Read mixed data types from a DATA block
DATA "player"
  "Hero", 100, 5.5
END DATA

OPEN DATA "player" AS #1
DIM name AS STRING, health AS INTEGER, speed AS SINGLE
READ #1, name, health, speed
PRINT name + " - HP: " + STR$(health)
CLOSE #1
' Read from a file stream — one line per variable, auto-parsed
OPEN "player.txt" FOR INPUT AS #1
DIM name$ AS STRING, level AS INTEGER
READ #1, name$, level
CLOSE #1

See also: DATA, OPEN DATA, OPEN, LINE INPUT, RESTORE, EOF

↑ Back to top

REDIM

Variables · since 0.23.0

Resizes an existing array, always preserving the elements that still fit. There is no PRESERVE keyword and none is needed — preserving is the default and only behavior (a non-preserving resize is just DIM). Growing keeps every existing element and default-fills the new cells; shrinking keeps the elements that remain in range. The resize happens in place, so other references to the same array (a BYREF parameter, a second binding) see the new size too. Rank is fixed: you may change the size of each dimension but not the number of dimensions, and the last dimension is the safe one to grow on multi-dimensional arrays. Changing the dimension count is a runtime error. If the array doesn't exist yet, REDIM allocates it like DIM.

Syntax

REDIM array_name(dimensions) AS datatype

Parameters

  • array_name - Name of the array to resize
  • dimensions - New size for each dimension (same number of dimensions as the original)
  • datatype - Element type (INTEGER, FLOAT, STRING, etc.)

Examples

DIM scores(10) AS INTEGER
scores(0) = 99
REDIM scores(20) AS INTEGER  ' grows to 20, scores(0) still 99
DIM log$(100) AS STRING
REDIM log$(50) AS STRING      ' shrinks to 50, first 50 kept

See also: DIM, ERASE

↑ Back to top

RESTORE

Variables · since 1.0.0

Resets the read pointer for a DATA block back to the beginning. Without a file number, restores all open DATA blocks. This allows re-reading the same data multiple times.

The handle keeps its #, as it does everywhere else — RESTORE #1, not RESTORE 1. A bare number does not parse.

Syntax

RESTORE [#filenumber]

Parameters

  • #filenumber - Optional file handle to restore, written with the # (omit to restore all open DATA blocks)

Examples

DATA "test"
  1, 2, 3
END DATA

OPEN DATA "test" AS #1
DIM a AS INTEGER, b AS INTEGER
READ #1, a, b
RESTORE #1
READ #1, a
PRINT STR$(a)  ' Prints 1 again
CLOSE #1
' Re-read data for multiple passes
DATA "palette"
  255, 0, 0
  0, 255, 0
  0, 0, 255
END DATA

OPEN DATA "palette" AS #1
FOR pass = 1 TO 2
  RESTORE #1
  DIM r AS INTEGER, g AS INTEGER, b AS INTEGER
  READ #1, r, g, b
  PRINT "Pass " + STR$(pass) + ": R=" + STR$(r)
NEXT pass
CLOSE #1

See also: DATA, READ, OPEN DATA

↑ Back to top

SELF

Variables · since 1.44.0

Inside a type-bound method (SUB Type::Method or FUNCTION Type::Method), SELF is an implied reference to the instance the method was called on. Reading or assigning SELF.field reads or mutates that instance in place — a method changes the object it was called on, with no need to pass it in or return it. You can call other methods on the same instance with CALL SELF.Other() (sub) or x = SELF.Other() (function). SELF is only meaningful inside a method body.

Syntax

SELF.field
CALL SELF.Method(args)
x = SELF.Method(args)

Example

TYPE Counter
    n AS INTEGER
END TYPE

SUB Counter::Inc()
    SELF.n = SELF.n + 1
END SUB

SUB Counter::IncTwice()
    CALL SELF.Inc()
    CALL SELF.Inc()
END SUB

DIM c AS Counter
c.n = 0
CALL c.IncTwice()
PRINT c.n   ' 2

See also: TYPE, SUB, FUNCTION

↑ Back to top

SET MUTATIONPOINT

Variables · since 1.4.3

Sets the coordinates of a specific point in a MUTATION variable. The index is 0-based. If the index is beyond the current number of points, the mutation is automatically extended with (0, 0) points to accommodate.

Writing a point ends any behavior on that mutation. Reading is free — MUTATIONPOINTX / MUTATIONPOINTY report where a behavior has got to — but writing is the program taking control, so the behavior is removed and the points you wrote become the shape's new baseline (a pose derived from the old one is reset with it). Re-apply the behavior if you still want it. To move a shape WITHOUT ending its behavior, position it through its sprite half: SET POLYGON n, LOCATION(x, y).

Syntax

SET MUTATIONPOINT varName, index, x, y
SET MUTATIONPOINT varName, index, coord   (the point also accepts a coordinate: (x,y) or a coordinate variable)
SET MUTATIONPOINT mutationArray(slot), index, coord

Parameters

  • varName - Name of the MUTATION variable
  • index - Zero-based index of the point to set
  • x - New X coordinate for the point
  • y - New Y coordinate for the point

Examples

'Create and modify a triangle
DIM triangle AS MUTATION
triangle = MUTATION((0, 0), (50, 0), (25, 43))

'Move the top vertex
SET MUTATIONPOINT triangle, 2, 25, 30
'Build a square point by point
DIM square AS MUTATION
SET MUTATIONPOINT square, 0, 0, 0
SET MUTATIONPOINT square, 1, 50, 0
SET MUTATIONPOINT square, 2, 50, 50
SET MUTATIONPOINT square, 3, 0, 50
'Animate a point
FOR i = 0 TO 100
  SET MUTATIONPOINT path, 0, i, SIN(i * 0.1) * 20
  POLYGON path, 14
  DELAY 16
NEXT i

See also: MUTATION, MUTATIONPOINTX, MUTATIONPOINTY, MUTATIONCOUNT

↑ Back to top

TRUE

Variables · since 1.57.0

The integer 1. Crash BASIC uses modern truthiness: TRUE is 1, FALSE is 0, and any non-zero value is truthy.

A pseudo-variable, so it needs no parentheses and cannot be assigned to.

Syntax

TRUE

Examples

done = TRUE
IF done THEN PRINT "finished"
IF HASPATTERN(id) = TRUE THEN PRINT "running"

See also: FALSE, IF

↑ Back to top

TYPE

Variables · since 1.56.0

Defines a custom data type with named fields. DIM x AS typename creates an instance pre-populated with the declared fields at their type defaults, and tags the instance with the type — which is what lets it carry methods (see below). Fields are accessed with dot notation, and the object stays dynamic: you can add extra fields beyond those declared.

Define before use. A TYPE must appear before any DIM ... AS typename that uses it (same rule as defining a SUB before you CALL it). Using an undeclared type name in DIM is a loud error rather than a silent untyped object, so typos are caught.

Array fields. A field may declare array bounds exactly as DIM does — invaders(0 TO 4, 0 TO 9) AS SpaceInvader — in any number of dimensions, with either the bare-size (cells(8)) or explicit zero-based range (cells(0 TO 7)) form. Arrays are zero-based, so a non-zero lower bound is an error here just as it is in DIM. The bounds are evaluated once, when the TYPE statement runs, and every instance is allocated from those sizes; use CONSTs rather than variables that change later. Each instance gets its own array — two instances never share cells.

Fields of another type. A field declared AS another type name is pre-populated as a tagged instance of that type, so its own declared fields are already there and its methods dispatch on it: a SUB with CALL fleet.invaders(r, c).Hit(1) and a FUNCTION in expression position with IF fleet.invaders(r, c).IsDead() THEN .... SELF refers to that cell itself, so what a method writes is visible through the same path afterwards and no other cell moves. This applies to array fields cell by cell. A type that refers to itself (next AS Node) cannot be filled in all the way down, so the self-referring field starts as an empty instance of that type — still tagged, ready to be assigned into, which is how you build lists and trees.

Methods. Bind a method to a type by qualifying a SUB or FUNCTION name with typename::. Inside the body, SELF is an implied reference to the instance the method was called on. Call a SUB method with CALL obj.Method(args) and a FUNCTION method in expression position (x = obj.Method()); both also work on array elements (CALL enemies(i).Hit()). Field and method types resolve case-insensitively.

Field types can be INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, COORDINATE, MUTATION, OBJECT, or another type name.

Syntax

TYPE typename
    fieldname AS datatype
    fieldname(bounds) AS datatype
    ...
END TYPE

Parameters

  • typename - Name for the custom type
  • fieldname - Name of each field
  • bounds - Optional array bounds for the field, spelled as in DIM: (N) or (0 TO N), comma-separated for more dimensions
  • datatype - Type of each field (INTEGER, STRING, etc.)

Examples

TYPE Player
    name AS STRING
    score AS INTEGER
    lives AS INTEGER
END TYPE

DIM p AS Player
p.name = "Mario"
p.score = 0
p.lives = 3
' A type with methods. SELF is the instance.
TYPE Player
    health AS INTEGER
END TYPE

SUB Player::TakeDamage(amount)
    SELF.health = SELF.health - amount
END SUB

FUNCTION Player::IsDead()
    IsDead = SELF.health <= 0
END FUNCTION

DIM p AS Player
p.health = 100
CALL p.TakeDamage(30)
IF p.IsDead() THEN PRINT "game over"
' Methods on an array of typed objects
TYPE Enemy
    hp AS INTEGER
END TYPE
SUB Enemy::Hit(n)
    SELF.hp = SELF.hp - n
END SUB
DIM enemies(8) AS Enemy
DIM i AS INTEGER
FOR i = 0 TO 7
    CALL enemies(i).Hit(5)
NEXT
' Array fields, including arrays of another type.
CONST ROWS = 5
CONST COLS = 11

TYPE SpaceInvader
    hp AS INTEGER
    alive AS BOOLEAN
END TYPE

SUB SpaceInvader::Hit(n)
    SELF.hp = SELF.hp - n
    SELF.alive = SELF.hp > 0
END SUB

TYPE SpaceInvaderFleet
    invaders(0 TO ROWS - 1, 0 TO COLS - 1) AS SpaceInvader
    speed AS SINGLE
END TYPE

DIM fleet AS SpaceInvaderFleet
fleet.speed = 2.5

DIM r AS INTEGER
DIM c AS INTEGER
FOR r = 0 TO ROWS - 1
    FOR c = 0 TO COLS - 1
        fleet.invaders(r, c).hp = 3
        fleet.invaders(r, c).alive = TRUE
    NEXT
NEXT

CALL fleet.invaders(0, 0).Hit(1)
PRINT fleet.invaders(0, 0).hp

See also: DIM, SUB, FUNCTION, SELF, Object

↑ Back to top

Functions

ASARRAY

Functions · since 1.0.3

Creates an array from a DATA block with specified dimensions. Converts embedded DATA block values directly into a multi-dimensional array without requiring OPEN DATA, READ loops, or manual element assignment. Supports 1D, 2D, 3D or higher dimensional arrays. The total number of elements in the DATA block must match the array dimensions.

Syntax

array = ASARRAY(data_block_name$, dim1 [, dim2, ...])

Parameters

  • data_block_name$ - Name of the DATA block (as a string)
  • dim1 - First dimension upper bound (0 to dim1)
  • dim2 - Optional second dimension upper bound
  • ... - Additional dimensions as needed

Examples

REM Simple 1D array from DATA block
DATA "scores"
  100, 200, 300, 400, 500
END DATA

scores = ASARRAY("scores", 4)
PRINT STR$(scores(0))  ' 100
PRINT STR$(scores(4))  ' 500
REM 2D array for tile map (3x3 = 9 elements)
DATA "tilemap"
  0, 1, 0
  1, 2, 1
  0, 1, 0
END DATA

tileMap = ASARRAY("tilemap", 2, 2)
PRINT STR$(tileMap(1, 1))  ' Center tile: 2
REM Color palette (4 colors x 3 RGB values = 12 elements)
DATA "palette"
  255, 0, 0
  0, 255, 0
  0, 0, 255
  255, 255, 255
END DATA

colors = ASARRAY("palette", 3, 2)
PRINT "Red R: " + STR$(colors(0, 0))    ' 255
PRINT "White B: " + STR$(colors(3, 2))  ' 255
REM Mixed value types (strings and numbers)
DATA "enemies"
  "Goblin", 10, 2
  "Orc", 25, 5
  "Dragon", 100, 20
END DATA

enemies = ASARRAY("enemies", 2, 2)
PRINT enemies(0, 0)        ' Goblin
PRINT STR$(enemies(2, 1))  ' 100 (Dragon HP)
REM Use with FOR loop after creation
DATA "values"
  5, 10, 15, 20, 25
END DATA

nums = ASARRAY("values", 4)
DIM total AS INTEGER
total = 0
FOR i = 0 TO UBOUND(nums)
  total = total + nums(i)
NEXT
PRINT "Sum: " + STR$(total)  ' 75

See also: DATA, DIM, UBOUND, LBOUND, OPEN DATA, READ

↑ Back to top

ASC

Functions · since 0.23.0

Returns ASCII code of first character.

Syntax

ASC(string_expression)

Parameters

  • string_expression - String with at least one character

Examples

PRINT ASC("A")  ' Prints 65
code = ASC(key$)
IF ASC(char$) >= 65 AND ASC(char$) <= 90 THEN PRINT "Uppercase"

See also: CHR$, VAL

↑ Back to top

CHR$

Functions · since 0.23.0

Converts ASCII code to character.

Syntax

CHR$(ascii_code)

Parameters

  • ascii_code - ASCII value (0-255)

Examples

PRINT CHR$(65)  ' Prints "A"
PRINT CHR$(7)   ' Bell character
name$ = CHR$(74) + CHR$(111) + CHR$(104) + CHR$(110)

See also: ASC, STR$

↑ Back to top

COUNT

Functions · since 0.23.0

Counts the number of non-overlapping occurrences of a substring within a string.

Syntax

COUNT(string, search_for)

Parameters

  • string - String to search in
  • search_for - Substring to count

Examples

n = COUNT("Hello World", "l")  ' Returns 3
n = COUNT("banana", "an")  ' Returns 2
words = COUNT(text$, " ") + 1  ' Count words (spaces + 1)

See also: INSTR, LEN, REPLACE$

↑ Back to top

DATE$

Functions · since 0.23.0

Returns the current system date as a string in MM/DD/YYYY format.

Syntax

DATE$

Parameters

  • No parameters required

Examples

PRINT "Today is: "; DATE$
today$ = DATE$
PRINT "Program run on "; DATE$; " at "; TIME$

See also: TIME$

↑ Back to top

DEQUEUE

Functions · since 0.23.0

Removes and returns the first element from a 1D array (FIFO - First In, First Out). All remaining elements shift down by one position, and the array automatically shrinks. Use with QUEUE for queue operations. Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error.

Syntax

value = DEQUEUE(array)

Parameters

  • array - 1D array variable to dequeue from

Examples

DIM tasks(20) AS STRING
QUEUE tasks, "Task 1"
QUEUE tasks, "Task 2"
QUEUE tasks, "Task 3"
nextTask$ = DEQUEUE(tasks)  ' Returns "Task 1" (first in)
nextTask$ = DEQUEUE(tasks)  ' Returns "Task 2"
' Message queue system
DIM messages(100) AS STRING
QUEUE messages, "Login"
QUEUE messages, "Update"
QUEUE messages, "Logout"
WHILE UBOUND(messages) >= 0
  msg$ = DEQUEUE(messages)
  PRINT "Processing: "; msg$
WEND
' Event processing queue
DIM events(50) AS STRING
QUEUE events, "KeyPress"
QUEUE events, "MouseClick"
IF UBOUND(events) >= 0 THEN
  currentEvent$ = DEQUEUE(events)
  PRINT "Handling: "; currentEvent$
END IF

See also: QUEUE, POP, UBOUND, LBOUND

↑ Back to top

EXISTS

Functions · since 1.0.3

Checks if a resource exists in the resource manager. Returns 1 if the resource exists, 0 if it does not.

Syntax

EXISTS(resource_name)

Parameters

  • resource_name - Name of the resource to check (image, sound, music, data, or text)

Examples

IF EXISTS("player.png") = 1 THEN
  PRINT "Player image found!"
ELSE
  PRINT "Player image not found"
END IF
REM Check for optional save data before loading
IF EXISTS("savegame.json") THEN
  gameData$ = LOADTEXT("savegame.json")
  PRINT "Save data loaded"
END IF

See also: LOADIMG, LOADTEXT, LOADJSON, LOADWAV, LOADMP3

↑ Back to top

FPS

Functions · since 1.10.0

Returns the current frames per second as a floating-point number. Uses a smoothed average to avoid frame-to-frame jitter. Useful for adaptive performance tuning — games can check FPS and reduce visual complexity on slower devices.

Syntax

FPS

Parameters

  • No parameters required

Examples

PRINT "FPS: "; INT(FPS)
' Adaptive quality:
IF FPS < 30 THEN
  ' Reduce particle count or disable effects
  maxParticles = 50
ELSE
  maxParticles = 200
END IF
' Display FPS counter:
LOCATE 1, 1
PRINT FIXED$(FPS, 1)

See also: TIMER, YIELD, DELAY

↑ Back to top

HEX$

Functions · since 0.23.0

Converts a number to its hexadecimal string representation.

Syntax

HEX$(number)

Parameters

  • number - Numeric value to convert (integer part used)

Examples

hex$ = HEX$(255)     ' Returns "FF"
hex$ = HEX$(16)      ' Returns "10"
PRINT "Color: &H" + HEX$(color_value)

See also: OCT$, STR$, VAL

↑ Back to top

INSTR

Functions · since 0.23.0

Finds the 1-based position of the first occurrence of a substring within a string. Returns 0 if not found.

Syntax

INSTR(string1, string2)

Parameters

  • string1 - String to search in
  • string2 - Substring to find

Examples

pos = INSTR("Hello World", "World")
IF INSTR(filename$, ".BAS") > 0 THEN PRINT "Basic file"

See also: MID$, LEN, REPLACE$

↑ Back to top

JOIN$

Functions · since 0.23.0

Joins an array of strings into a single string using a delimiter.

Syntax

JOIN$(array, delimiter)

Parameters

  • array - Array of strings to join
  • delimiter - Character or string to join with

Examples

result$ = JOIN$(parts$, ",")  ' Join array with commas
sentence$ = JOIN$(words$, " ")  ' Join words with spaces
csv_line$ = JOIN$(fields$, ",")  ' Create CSV format

See also: SPLIT$, STRING$

↑ Back to top

LCASE$

Functions · since 0.23.0

Converts a string to lowercase letters.

Syntax

LCASE$(string)

Parameters

  • string - Input string to convert

Examples

result$ = LCASE$("Hello World")  ' Returns "hello world"
filename$ = LCASE$(input_file$)

See also: UCASE$

↑ Back to top

LEFT$

Functions · since 0.23.0

Returns the leftmost characters from a string.

Syntax

LEFT$(string_expression, count)

Parameters

  • string_expression - Source string
  • count - Number of characters to return

Examples

first3 = LEFT$("Hello", 3)
prefix$ = LEFT$(filename$, 8)
PRINT LEFT$("QBasic", 2)  ' Prints "QB"

See also: RIGHT$, MID$, LEN

↑ Back to top

LEN

Functions · since 0.23.0

Returns the length of a string.

Syntax

LEN(string_expression)

Parameters

  • string_expression - String to measure

Examples

length = LEN("Hello")
IF LEN(name$) > 10 THEN PRINT "Name too long"
FOR i = 1 TO LEN(text$): PRINT MID$(text$, i, 1): NEXT

See also: MID$, LEFT$, RIGHT$

↑ Back to top

LTRIM$

Functions · since 0.23.0

Removes leading spaces from the left side of a string.

Syntax

LTRIM$(string)

Parameters

  • string - Input string to trim

Examples

result$ = LTRIM$("  Hello World  ")  ' Returns "Hello World  "
cleaned$ = LTRIM$(user_input$)

See also: TRIM$, RTRIM$

↑ Back to top

MATCH

Functions · since 0.23.0

Pattern matching with wildcards. Returns 1 if match, 0 if not.

Syntax

MATCH(string, pattern)

Parameters

  • string - String to test
  • pattern - Pattern with wildcards (* = any sequence, ? = any single character)

Examples

IF MATCH(filename$, "*.BAS") THEN PRINT "Basic file"
IF MATCH(name$, "J?hn") THEN PRINT "John or similar"
IF MATCH(text$, "*error*") THEN PRINT "Contains error"

See also: REGEX, INSTR

↑ Back to top

MID$

Functions · since 0.23.0

Returns a substring from the middle of a string.

Syntax

MID$(string_expression, start [, length])

Parameters

  • string_expression - Source string
  • start - Starting position (1-based)
  • length - Number of characters (optional, defaults to rest of string)

Examples

middle = MID$("Hello", 2, 3)
rest$ = MID$(text$, 5)
char$ = MID$(alphabet$, i, 1)

See also: LEFT$, RIGHT$, INSTR

↑ Back to top

OCT$

Functions · since 0.23.0

Converts a number to its octal string representation.

Syntax

OCT$(number)

Parameters

  • number - Numeric value to convert (integer part used)

Examples

oct$ = OCT$(64)      ' Returns "100"
oct$ = OCT$(8)       ' Returns "10"
PRINT "Octal: " + OCT$(value)

See also: HEX$, STR$, VAL

↑ Back to top

POP

Functions · since 0.23.0

Removes and returns the last element from a 1D array (LIFO - Last In, First Out). The array automatically shrinks by one element. Use with PUSH for stack operations. Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error.

Syntax

value = POP(array)

Parameters

  • array - 1D array variable to pop from

Examples

DIM stack(10) AS INTEGER
PUSH stack, 10
PUSH stack, 20
PUSH stack, 30
value = POP(stack)  ' Returns 30 (last in)
value = POP(stack)  ' Returns 20
' Undo system using stack
DIM history(50) AS STRING
PUSH history, "Action 1"
PUSH history, "Action 2"
lastAction$ = POP(history)  ' Get most recent action
' Stack-based expression evaluator
DIM operands(100) AS INTEGER
PUSH operands, 5
PUSH operands, 3
b = POP(operands)
a = POP(operands)
result = a + b

See also: PUSH, DEQUEUE, UBOUND, LBOUND

↑ Back to top

REGEX

Functions · since 0.23.0

Advanced regular expression pattern matching. Returns 1 if the pattern matches, 0 if not. Matching is case-sensitive; embed an inline flag such as (?i) at the start of the pattern for case-insensitive matching.

Syntax

REGEX(string, pattern)

Parameters

  • string - String to test
  • pattern - Regular expression pattern

Examples

IF REGEX(email$, "^[\w.-]+@[\w.-]+\.[a-z]{2,}$") THEN PRINT "Valid email"
IF REGEX(phone$, "^\d{3}-\d{3}-\d{4}$") THEN PRINT "Valid phone"
IF REGEX(text$, "(?i)hello") THEN PRINT "Contains hello (any case)"

See also: MATCH, INSTR

↑ Back to top

REPLACE$

Functions · since 0.23.0

Replaces all occurrences of a substring with another string.

Syntax

REPLACE$(string, search_for, replace_with)

Parameters

  • string - Source string
  • search_for - Substring to find
  • replace_with - Replacement string

Examples

result$ = REPLACE$("Hello World", "World", "QBasic")
clean$ = REPLACE$(text$, " ", "_")
fixed$ = REPLACE$(data$, "old", "new")

See also: INSTR, MID$

↑ Back to top

REVERSE$

Functions · since 0.23.0

Reverses the order of characters in a string.

Syntax

REVERSE$(string)

Parameters

  • string - String to reverse

Examples

result$ = REVERSE$("Hello")  ' Returns "olleH"
result$ = REVERSE$("12345")  ' Returns "54321"
IF text$ = REVERSE$(text$) THEN PRINT "Palindrome!"

See also: LEFT$, RIGHT$, MID$

↑ Back to top

Functions · since 0.23.0

Returns the rightmost characters from a string.

Syntax

RIGHT$(string_expression, count)

Parameters

  • string_expression - Source string
  • count - Number of characters to return

Examples

last3 = RIGHT$("Hello", 3)
extension$ = RIGHT$(filename$, 3)
PRINT RIGHT$("QBasic", 5)  ' Prints "Basic"

See also: LEFT$, MID$, LEN

↑ Back to top

RTRIM$

Functions · since 0.23.0

Removes trailing spaces from the right side of a string.

Syntax

RTRIM$(string)

Parameters

  • string - Input string to trim

Examples

result$ = RTRIM$("  Hello World  ")  ' Returns "  Hello World"
cleaned$ = RTRIM$(line_data$)

See also: TRIM$, LTRIM$

↑ Back to top

SPACE$

Functions · since 0.23.0

Creates a string of spaces.

Syntax

SPACE$(count)

Parameters

  • count - Number of spaces to create

Examples

spaces$ = SPACE$(10)         ' Returns "          "
indent$ = SPACE$(level * 4)
PRINT "Name:" + SPACE$(5) + name$

See also: STRING$

↑ Back to top

SPLIT$

Functions · since 0.23.0

Splits a string into an array using a delimiter. Returns array of strings.

Syntax

SPLIT$(string, delimiter)

Parameters

  • string - String to split
  • delimiter - Character or string to split on

Examples

parts$ = SPLIT$("apple,banana,cherry", ",")  ' Returns ["apple", "banana", "cherry"]
words$ = SPLIT$("Hello World", " ")  ' Returns ["Hello", "World"]
lines$ = SPLIT$(text$, CHR$(10))  ' Split on newlines

See also: JOIN$, MID$, INSTR

↑ Back to top

STR$

Functions · since 0.23.0

Converts a number to its string representation. No leading space is added for positive numbers.

Syntax

STR$(number)

Parameters

  • number - Numeric value to convert

Examples

text$ = STR$(123)     ' Returns "123"
text$ = STR$(-45)    ' Returns "-45"
PRINT "Value:" + STR$(score)

See also: VAL, HEX$, OCT$

↑ Back to top

STRING$

Functions · since 0.23.0

Creates a string by repeating a character or ASCII code.

Syntax

STRING$(count, character)

Parameters

  • count - Number of repetitions
  • character - String character or ASCII code number

Examples

line$ = STRING$(40, "-")     ' Returns "----------------------------------------"
spaces$ = STRING$(10, 32)    ' Returns 10 spaces (ASCII 32)
border$ = STRING$(20, "*")

See also: SPACE$, CHR$

↑ Back to top

TIME$

Functions · since 0.23.0

Returns the current system time as a string in HH:MM:SS format.

Syntax

TIME$

Parameters

  • No parameters required

Examples

PRINT "Current time: "; TIME$
startTime$ = TIME$
IF TIME$ > "12:00:00" THEN PRINT "Good afternoon"

See also: DATE$, TIMER

↑ Back to top

TIMER

Functions · since 0.23.0

Returns the number of seconds since midnight as a floating-point number. Includes fractional seconds for high-precision timing. Useful for performance measurement, animation timing, and seeding random numbers. Range is 0.0 to 86399.999...

Syntax

TIMER

Parameters

  • No parameters required

Examples

PRINT "Seconds since midnight: "; TIMER
' Measure execution time:
startTime = TIMER
' ... do something ...
endTime = TIMER
PRINT "Elapsed: "; endTime - startTime; " seconds"
' Seed random number generator:
RANDOMIZE TIMER
' Animation timing:
frameStart = TIMER
' ... render frame ...
elapsed = TIMER - frameStart
IF elapsed < 0.016667 THEN DELAY (0.016667 - elapsed) * 1000
' Time-based game logic:
IF TIMER - lastShot > 0.5 THEN
  ' Allow shooting every 0.5 seconds
  CALL FireBullet()
  lastShot = TIMER
END IF

See also: TIME$, DATE$, RANDOMIZE, DELAY

↑ Back to top

TRIM$

Functions · since 0.23.0

Removes leading and trailing spaces from a string.

Syntax

TRIM$(string)

Parameters

  • string - Input string to trim

Examples

cleaned$ = TRIM$("  Hello World  ")  ' Returns "Hello World"
result$ = TRIM$(INPUT_TEXT$)

See also: LTRIM$, RTRIM$

↑ Back to top

UCASE$

Functions · since 0.23.0

Converts a string to uppercase letters.

Syntax

UCASE$(string)

Parameters

  • string - Input string to convert

Examples

result$ = UCASE$("Hello World")  ' Returns "HELLO WORLD"
command$ = UCASE$(user_input$)

See also: LCASE$

↑ Back to top

VAL

Functions · since 0.23.0

Converts a string to a number. The whole string must be one — surrounding spaces are fine, anything else is not.

A string that is not a number is a runtime error. It does not come back as 0, because 0 is indistinguishable from the string "0": a typo, an empty input box and a genuine zero would all read the same, and the number reaching the rest of the program would be a guess nobody made.

It does not read a number off the front of something else. VAL("10ABC") is an error, not 10. If you need the leading digits of a mixed string, cut them out yourself with LEFT$ / MID$ first.

Check the string first if it might not be a number. There is no sentinel return to test for afterwards — that is the point.

Syntax

VAL(string_expression)

Parameters

  • string_expression - A string that is entirely a number, such as "42", "-3.14" or " 7 ". Anything else is a runtime error.

Examples

num = VAL("123.45")    ' 123.45
PRINT VAL("  -7  ")    ' Prints -7
REM Guard user input rather than trusting it
IF entry$ <> "" THEN
  score = VAL(entry$)
END IF

See also: STR$, ASC, CHR$, FIXED$, LEFT$

↑ Back to top

VERSION$

Functions · since 1.10.0

Returns the CrashCore engine version as a string (e.g., "1.10.0"). Useful for displaying engine information or checking compatibility.

Syntax

VERSION$

Parameters

  • No parameters required

Examples

PRINT "CrashCore version: "; VERSION$
' Check minimum version:
IF VERSION$ < "1.10.0" THEN
  PRINT "This game requires CrashCore 1.10.0 or later"
  END
END IF

See also: DATE$, TIME$

↑ Back to top

Math

ABS

Math · since 0.23.0

Returns the absolute value of a number.

Syntax

ABS(number)

Parameters

  • number - Any numeric expression

Examples

PRINT ABS(-42)    ' Prints 42
PRINT ABS(42)     ' Prints 42
PRINT ABS(-3.14)  ' Prints 3.14

See also: SGN

↑ Back to top

ACOS

Math · since 1.0.3

Returns the arccosine (inverse cosine) of a number, in radians. The argument should be in the range -1 to 1.

Syntax

ACOS(number)

Parameters

  • number - A value in the range -1 to 1

Examples

PRINT ACOS(1)          ' Prints 0
PRINT ACOS(0)          ' Prints 1.570796 (PI/2)
degrees = ACOS(ratio) * 180 / PI

See also: ASIN, ATN, COS

↑ Back to top

ASIN

Math · since 1.0.3

Returns the arcsine (inverse sine) of a number, in radians. The argument should be in the range -1 to 1.

Syntax

ASIN(number)

Parameters

  • number - A value in the range -1 to 1

Examples

PRINT ASIN(0)          ' Prints 0
PRINT ASIN(1)          ' Prints 1.570796 (PI/2)
degrees = ASIN(ratio) * 180 / PI

See also: ACOS, ATN, SIN

↑ Back to top

ATN

Math · since 0.23.0

Returns the arctangent (inverse tangent) in radians.

Syntax

ATN(number)

Parameters

  • number - Any numeric expression

Examples

PRINT ATN(0)           ' Prints 0
PRINT ATN(1)           ' Prints 0.785398 (PI/4)
degrees = ATN(1) * 180 / 3.14159  ' Convert to degrees
PRINT degrees          ' Prints 45

See also: ATN2, SIN, COS, TAN

↑ Back to top

ATN2

Math · since 1.0.3

Returns the two-argument arctangent (atan2) in radians. Computes the angle from the positive X axis to the point (x, y), handling all four quadrants correctly. This is the preferred function for computing angles between two points or from velocity components.

Syntax

ATN2(y, x)

Parameters

  • y - The Y component (vertical delta or sine component)
  • x - The X component (horizontal delta or cosine component)

Examples

angle = ATN2(1, 0)             ' Returns PI/2 (90 degrees)
angle = ATN2(0, 1)             ' Returns 0 (0 degrees)
angle = ATN2(-1, 0)            ' Returns -PI/2 (-90 degrees)
' Compute angle between two sprites:
dx = SPRITEX(1, 0.5) - SPRITEX(0, 0.5)
dy = SPRITEY(1, 0.5) - SPRITEY(0, 0.5)
angle = ATN2(dy, dx)
' Convert to degrees:
degrees = ATN2(dy, dx) * 180 / PI

See also: ATN, SIN, COS

↑ Back to top

CEIL

Math · since 0.23.0

Returns the smallest integer greater than or equal to the number (rounds up).

Syntax

CEIL(number)

Parameters

  • number - Any numeric expression

Examples

PRINT CEIL(3.2)       ' Prints 4
PRINT CEIL(-2.8)      ' Prints -2
PRINT CEIL(42)        ' Prints 42
pages = CEIL(total_items / items_per_page)

See also: FLOOR, ROUND, INT

↑ Back to top

CLAMP

Math · since 1.0.3

Constrains a value to lie within a specified range. If value is less than min, returns min. If value is greater than max, returns max. Otherwise returns value unchanged.

Syntax

CLAMP(value, min, max)

Parameters

  • value - The value to clamp
  • min - The minimum bound
  • max - The maximum bound

Examples

x = CLAMP(x, 0, screenWidth)    ' Keep x within screen bounds
hp = CLAMP(hp - damage, 0, maxHp) ' Clamp health between 0 and max
alpha = CLAMP(fade, 0, 255)       ' Keep alpha in valid range
speed = CLAMP(speed + accel, -maxSpeed, maxSpeed)

See also: MIN, MAX, LERP

↑ Back to top

COS

Math · since 0.23.0

Returns the cosine of an angle in radians.

Results are full-precision floats, and PRINT shows all of it. TAN(PI/4) is 0.9999999999999999, not 1 — that is the arithmetic being honest, not a bug. Compare with a tolerance rather than = when it matters: IF ABS(x - 1) < 0.0001 THEN. Use FIXED$(value, places) to show a rounded number.

Syntax

COS(angle)

Parameters

  • angle - Angle in radians

Examples

PRINT COS(0)           ' Prints 1
PRINT COS(PI)          ' Prints -1
angle = 60 * PI / 180  ' Convert degrees to radians
PRINT COS(angle)       ' Cos of 60 degrees

See also: SIN, TAN, ATN

↑ Back to top

EXP

Math · since 0.23.0

Returns e raised to the power of number (e^x).

Syntax

EXP(number)

Parameters

  • number - Any numeric expression

Examples

PRINT EXP(0)          ' Prints 1
PRINT EXP(1)          ' Prints 2.718281828459045 (e)
PRINT EXP(2)          ' Prints 7.38905609893065 (e^2)
PRINT LOG(EXP(5))     ' Prints 5

See also: LOG, SQR

↑ Back to top

FIX

Math · since 0.23.0

Returns the integer part by truncating towards zero.

Syntax

FIX(number)

Parameters

  • number - Any numeric expression

Examples

PRINT FIX(3.7)        ' Prints 3
PRINT FIX(-3.7)       ' Prints -3
PRINT FIX(5)          ' Prints 5
' INT vs FIX for negative numbers:
PRINT INT(-2.5), FIX(-2.5)  ' Prints -3, -2

See also: INT, CINT

↑ Back to top

FIXED

Math · since 1.41.0

Rounds a number to a fixed number of decimal places and returns it as a number. Useful for smoothing noisy inputs such as joystick axis values. For exact textual display, use FIXED$ instead (it avoids floating-point representation artifacts).

Syntax

FIXED(value, places)

Parameters

  • value - Any numeric expression
  • places - Number of decimal places (0-15; negative values are treated as 0)

Examples

PRINT FIXED(3.14159, 2)    ' Prints 3.14
PRINT FIXED(-0.56789, 2)   ' Prints -0.57
smooth = FIXED(JOYAXIS(0), 2)

See also: FIXED$, ROUND, INT

↑ Back to top

FLOOR

Math · since 0.23.0

Returns the largest integer less than or equal to the number (rounds down).

Syntax

FLOOR(number)

Parameters

  • number - Any numeric expression

Examples

PRINT FLOOR(3.7)      ' Prints 3
PRINT FLOOR(-2.3)     ' Prints -3
PRINT FLOOR(42)       ' Prints 42
page = FLOOR(item / items_per_page)

See also: CEIL, ROUND, INT

↑ Back to top

INT

Math · since 0.23.0

Returns the largest integer less than or equal to number (floor function).

Syntax

INT(number)

Parameters

  • number - Any numeric expression

Examples

PRINT INT(3.7)        ' Prints 3
PRINT INT(-3.7)       ' Prints -4
PRINT INT(5)          ' Prints 5
dice = INT(RND * 6) + 1  ' Random die roll (1-6)

See also: FIX, CINT

↑ Back to top

LBOUND

Math · since 1.0.3

Returns the lower bound (lowest valid index) of a one-dimensional array. Always returns 0 since arrays use 0-based indexing.

Syntax

LBOUND(array)

Parameters

  • array - An array variable

Examples

DIM arr(10) AS INTEGER
PRINT LBOUND(arr)  ' Prints 0
FOR i = LBOUND(arr) TO UBOUND(arr)
  PRINT arr(i)
NEXT

See also: UBOUND, DIM, REDIM

↑ Back to top

LERP

Math · since 1.0.3

Linear interpolation between two values by factor t. When t=0 returns a, when t=1 returns b, when t=0.5 returns the midpoint. Works with both scalar values and coordinates for smooth 2D/3D interpolation.

Syntax

LERP(a, b, t)

Parameters

  • a - Start value (scalar or coordinate)
  • b - End value (scalar or coordinate)
  • t - Interpolation factor (typically 0 to 1)

Examples

' Smooth camera movement
camX = LERP(camX, targetX, 0.1)
' Fade between colors
r = LERP(startR, endR, progress)
' Smooth 2D position interpolation
pos = LERP((0, 0), (100, 200), 0.5)  ' Returns (50, 100)
' Ease-in movement with coordinate lerp
start = (playerX, playerY)
dest = (goalX, goalY)
current = LERP(start, dest, t)

See also: CLAMP, MAG

↑ Back to top

LOG

Math · since 0.23.0

Returns the natural logarithm (base e) of a number.

Syntax

LOG(number)

Parameters

  • number - Any positive numeric expression

Examples

PRINT LOG(1)          ' Prints 0
PRINT LOG(2.718282)   ' Prints 1 (log of e)
PRINT LOG(10)         ' Prints 2.302585
log10 = LOG(100) / LOG(10)  ' Base 10 logarithm

See also: EXP, SQR

↑ Back to top

LOG10

Math · since 1.0.3

Returns the base-10 logarithm of a number.

Syntax

LOG10(number)

Parameters

  • number - A positive numeric expression

Examples

PRINT LOG10(1)         ' Prints 0
PRINT LOG10(100)       ' Prints 2
PRINT LOG10(1000)      ' Prints 3

See also: LOG, EXP, POW

↑ Back to top

MAG

Math · since 1.0.3

Returns the magnitude (Euclidean length) of a coordinate vector. For scalar values, returns the absolute value. Use MAG(a - b) to compute the distance between two coordinates.

Syntax

MAG(value)

Parameters

  • value - A coordinate or scalar value

Examples

pos = (3, 4)
PRINT MAG(pos)          ' Prints 5 (3-4-5 triangle)
a = (10, 20)
b = (40, 60)
dist = MAG(a - b)       ' Distance between two points
vel = (vx, vy, vz)
speed = MAG(vel)        ' Speed from 3D velocity vector
PRINT MAG(-7)           ' Prints 7 (absolute value for scalars)

See also: SQR, ABS, SPRITEDIST

↑ Back to top

MAX

Math · since 1.0.3

Returns the larger of two values. Preserves the original value type (integer or float) of the result.

Syntax

MAX(a, b)

Parameters

  • a - First value to compare
  • b - Second value to compare

Examples

PRINT MAX(5, 3)        ' Prints 5
PRINT MAX(-1, 1)       ' Prints 1
scrollX = MAX(scrollX, 0)  ' Clamp to zero
health = MAX(health - damage, 0)  ' Don't go below zero

See also: MIN, ABS

↑ Back to top

MIN

Math · since 1.0.3

Returns the smaller of two values. Preserves the original value type (integer or float) of the result.

Syntax

MIN(a, b)

Parameters

  • a - First value to compare
  • b - Second value to compare

Examples

PRINT MIN(5, 3)        ' Prints 3
PRINT MIN(-1, 1)       ' Prints -1
speed = MIN(speed + 1, maxSpeed)  ' Clamp to max
scrollX = MIN(scrollX, mapWidth - SCREENWIDTH)  ' Clamp camera

See also: MAX, ABS

↑ Back to top

MOD

Math · since 1.0.3

Returns the remainder of dividing a by b (modulo operation). Also available as the MOD operator: a MOD b. A zero divisor is a runtime error (matching division by zero).

Syntax

MOD(a, b)

Parameters

  • a - The dividend
  • b - The divisor (a value of 0 is a runtime error)

Examples

PRINT MOD(10, 3)       ' Prints 1
PRINT 10 MOD 3         ' Prints 1 (operator form)
IF MOD(frame, 2) = 0 THEN PRINT "even frame"
tile = MOD(x, tileSize)  ' Position within tile

See also: INT, FIX

↑ Back to top

PI

Math · since 1.57.0

The mathematical constant pi, 3.141592653589793, as a Float. A pseudo-variable, not a function — no parentheses.

Useful for converting between degrees and radians: Crash BASIC's trigonometric functions take radians, while ROTATE, BROTATE and STARTANGLE take degrees.

Syntax

PI

Examples

radians = degrees * PI / 180
circumference = 2 * PI * radius
PRINT SIN(PI / 2)  ' 1

See also: E, SIN, COS, RAD

↑ Back to top

POW

Math · since 1.0.3

Returns base raised to the power of exponent. Also available as the ^ operator: base ^ exponent. The result is always a float, so fractional and negative exponents work. ^ binds tighter than every arithmetic operator including unary minus, so -2 ^ 2 is -(2 ^ 2) = -4; write (-2) ^ 2 to raise a negative base. It is right-associative, so 2 ^ 3 ^ 2 is 2 ^ (3 ^ 2) = 512.

Syntax

POW(base, exponent)

Parameters

  • base - The base number
  • exponent - The power to raise the base to

Examples

PRINT POW(2, 3)        ' Prints 8
PRINT 2 ^ 3            ' Prints 8 (operator form)
PRINT POW(9, 0.5)      ' Prints 3 (square root)
PRINT 2 ^ -1           ' Prints 0.5 (negative exponent)
PRINT POW(10, 0)       ' Prints 1
PRINT POW(2, 10)       ' Prints 1024
PRINT 2 * 3 ^ 2        ' Prints 18 (^ before *)
PRINT -2 ^ 2           ' Prints -4 (^ before unary minus)
PRINT 2 ^ 3 ^ 2        ' Prints 512 (right-associative)

See also: SQR, EXP, LOG

↑ Back to top

RANDOMIZE

Math · since 0.23.0

Seeds the random number generator for RND function.

Syntax

RANDOMIZE [seed]

Parameters

  • seed - Optional seed value (uses timer if omitted)

Examples

RANDOMIZE        ' Seed with current time
RANDOMIZE 12345  ' Seed with specific value
PRINT RND

See also: RND, TIMER

↑ Back to top

RND

Math · since 0.23.0

Returns a random number between 0 and 1 (exclusive of 1).

Syntax

RND[(n)]

Parameters

  • n - Optional argument (currently ignored)

Examples

PRINT RND             ' Random decimal 0 to 0.999...
dice = INT(RND * 6) + 1  ' Random integer 1-6
x = RND * 100         ' Random number 0-100
IF RND < 0.5 THEN PRINT "Heads" ELSE PRINT "Tails"

See also: RANDOMIZE, INT

↑ Back to top

ROUND

Math · since 0.23.0

Returns the number rounded to the nearest integer.

Syntax

ROUND(number)

Parameters

  • number - Any numeric expression

Examples

PRINT ROUND(3.7)      ' Prints 4
PRINT ROUND(3.2)      ' Prints 3
PRINT ROUND(-2.8)     ' Prints -3
rounded = ROUND(precise_value)

See also: FLOOR, CEIL, INT

↑ Back to top

SGN

Math · since 0.23.0

Returns the sign of a number: 1 if positive, -1 if negative, 0 if zero.

Syntax

SGN(number)

Parameters

  • number - Any numeric expression

Examples

PRINT SGN(42)         ' Prints 1
PRINT SGN(-42)        ' Prints -1
PRINT SGN(0)          ' Prints 0
direction = SGN(velocity)  ' Get movement direction

See also: ABS

↑ Back to top

SIN

Math · since 0.23.0

Returns the sine of an angle in radians.

Syntax

SIN(angle)

Parameters

  • angle - Angle in radians

Examples

PRINT SIN(0)           ' Prints 0
PRINT SIN(3.14159/2)   ' Prints 1 (sin of PI/2)
angle = 45 * 3.14159 / 180  ' Convert degrees to radians
PRINT SIN(angle)       ' Sin of 45 degrees

See also: COS, TAN, ATN

↑ Back to top

SQR

Math · since 0.23.0

Returns the square root of a number. A negative argument is a runtime error (the result would be NaN, which is not a representable value).

Syntax

SQR(number)

Parameters

  • number - A non-negative numeric expression (a negative value is a runtime error)

Examples

PRINT SQR(16)         ' Prints 4
PRINT SQR(2)          ' Prints 1.414213
hypotenuse = SQR(3^2 + 4^2)  ' Pythagorean theorem
PRINT hypotenuse      ' Prints 5

See also: ABS, EXP, LOG

↑ Back to top

TAN

Math · since 0.23.0

Returns the tangent of an angle in radians.

Results are full-precision floats, and PRINT shows all of it. TAN(PI/4) is 0.9999999999999999, not 1 — that is the arithmetic being honest, not a bug. Compare with a tolerance rather than = when it matters: IF ABS(x - 1) < 0.0001 THEN. Use FIXED$(value, places) to show a rounded number.

Syntax

TAN(angle)

Parameters

  • angle - Angle in radians

Examples

PRINT TAN(0)           ' Prints 0
PRINT TAN(PI/4)        ' Prints 0.9999999999999999
angle = 45 * PI / 180  ' Convert degrees to radians
PRINT TAN(angle)       ' Tan of 45 degrees

See also: SIN, COS, ATN

↑ Back to top

UBOUND

Math · since 1.0.3

Returns the upper bound (highest valid index) of a one-dimensional array. Returns -1 if the array is empty or the argument is not an array.

Syntax

UBOUND(array)

Parameters

  • array - An array variable

Examples

DIM arr(10) AS INTEGER
PRINT UBOUND(arr)  ' Prints 9 (indices 0-9)
FOR i = 0 TO UBOUND(names)
  PRINT names(i)
NEXT
IF i <= UBOUND(arr) THEN value = arr(i)  ' Bounds check

See also: LBOUND, DIM, REDIM

↑ Back to top

Bitwise

BITAND

Bitwise · since 1.33.0

Bitwise AND of two integers (operates on the 64-bit pattern). Distinct from the boolean AND operator: BITAND(12, 10) is 8, whereas (12 AND 10) is 1.

Syntax

BITAND(a, b)

Parameters

  • a - Integer (floats are truncated)
  • b - Integer (floats are truncated)

Examples

mask = BITAND(flags, 15)        ' low nibble
PRINT BITAND(12, 10)            ' 8

See also: BITOR, BITXOR, BITNOT, SHL, SHR

↑ Back to top

BITCLEAR

Bitwise · since 1.33.0

Returns a with bit number bit set to 0 (other bits unchanged). Bit 0 is the least significant. A negative index halts; an index >= 64 returns a unchanged.

Syntax

BITCLEAR(a, bit)

Parameters

  • a - Integer (floats are truncated)
  • bit - Bit index 0..63 (0 = least significant)

Examples

flags = BITCLEAR(flags, 3)      ' turn bit 3 off
bitboard = BITCLEAR(bitboard, square)

See also: BITSET, BITTEST, BITAND

↑ Back to top

BITNOT

Bitwise · since 1.33.0

One's complement (flips every bit) of an integer. BITNOT(0) is -1 because the value is a signed 64-bit integer.

Syntax

BITNOT(a)

Parameters

  • a - Integer (floats are truncated)

Examples

PRINT BITNOT(0)                 ' -1
PRINT BITNOT(5)                 ' -6

See also: BITAND, BITOR, BITXOR

↑ Back to top

BITOR

Bitwise · since 1.33.0

Bitwise OR of two integers (operates on the 64-bit pattern). Distinct from the boolean OR operator.

Syntax

BITOR(a, b)

Parameters

  • a - Integer (floats are truncated)
  • b - Integer (floats are truncated)

Examples

flags = BITOR(flags, 4)         ' set bit 2
PRINT BITOR(12, 10)            ' 14

See also: BITAND, BITXOR, BITNOT, SHL, SHR

↑ Back to top

BITSET

Bitwise · since 1.33.0

Returns a with bit number bit set to 1 (other bits unchanged). Bit 0 is the least significant. A negative index halts; an index >= 64 returns a unchanged.

Syntax

BITSET(a, bit)

Parameters

  • a - Integer (floats are truncated)
  • bit - Bit index 0..63 (0 = least significant)

Examples

flags = BITSET(flags, 3)        ' turn bit 3 on
bitboard = BITSET(bitboard, square)

See also: BITCLEAR, BITTEST, BITOR

↑ Back to top

BITTEST

Bitwise · since 1.33.0

Returns 1 if bit number bit of a is set, else 0. Bit 0 is the least significant bit. A negative index halts; an index >= 64 reads as 0 (no such bit in a 64-bit value).

Syntax

BITTEST(a, bit)

Parameters

  • a - Integer (floats are truncated)
  • bit - Bit index 0..63 (0 = least significant)

Examples

IF BITTEST(flags, 3) THEN PRINT "bit 3 set"
occupied = BITTEST(bitboard, square)

See also: BITSET, BITCLEAR, BITAND

↑ Back to top

BITXOR

Bitwise · since 1.33.0

Bitwise XOR (exclusive or) of two integers.

Syntax

BITXOR(a, b)

Parameters

  • a - Integer (floats are truncated)
  • b - Integer (floats are truncated)

Examples

flags = BITXOR(flags, 4)        ' toggle bit 2
PRINT BITXOR(12, 10)           ' 6

See also: BITAND, BITOR, BITNOT, SHL, SHR

↑ Back to top

SHL

Bitwise · since 1.33.0

Shift the bits of a left by n positions. n must be >= 0 (negative halts); shifting by >= 64 yields 0.

Syntax

SHL(a, n)

Parameters

  • a - Integer to shift (floats are truncated)
  • n - Number of bit positions, >= 0

Examples

PRINT SHL(1, 4)                 ' 16
bit = SHL(1, n)                 ' bitmask for bit n

See also: SHR, BITAND, BITOR

↑ Back to top

SHR

Bitwise · since 1.33.0

Logical (zero-fill) shift of the bits of a right by n positions. Treats the value as a 64-bit pattern, so the sign bit does not propagate (correct for masks/bitboards). n must be >= 0 (negative halts); shifting by >= 64 yields 0.

Syntax

SHR(a, n)

Parameters

  • a - Integer to shift (floats are truncated)
  • n - Number of bit positions, >= 0

Examples

PRINT SHR(16, 2)                ' 4
PRINT SHR(-1, 1)                ' 9223372036854775807 (logical, not arithmetic)

See also: SHL, BITAND, BITOR

↑ Back to top

Input

DRAGX

Input · since 0.23.0

Returns normalized horizontal drag value from -1 to 1 while actively dragging, 0 when not dragging. Works with both touch (mobile) and mouse (desktop). -1 is maximum left drag, 0 is no drag, 1 is maximum right drag. Values are normalized based on half the screen width. Automatically returns 0 when pointer is released.

Syntax

DRAGX()

Examples

REM Real-time camera pan
DO
  dx = DRAGX()
  IF dx <> 0 THEN cameraX = cameraX - dx * 10
  DELAY 16
LOOP
REM Swipe-based menu navigation
DO
  dx = DRAGX()
  IF dx < -0.5 THEN
    CALL NextPage()
    DELAY 300  ' Debounce
  END IF
  IF dx > 0.5 THEN
    CALL PrevPage()
    DELAY 300
  END IF
  DELAY 16
LOOP
REM Horizontal scrolling with momentum
velocity = 0
DO
  dx = DRAGX()
  IF dx <> 0 THEN
    velocity = dx * 15
  ELSE
    velocity = velocity * 0.9  ' Decay when released
  END IF
  scrollX = scrollX + velocity
  DELAY 16
LOOP

See also: DRAGY, TAP, MOUSEX, MOUSEY, MOUSEDOWN

↑ Back to top

DRAGY

Input · since 0.23.0

Returns normalized vertical drag value from -1 to 1 while actively dragging, 0 when not dragging. Works with both touch (mobile) and mouse (desktop). -1 is maximum up drag, 0 is no drag, 1 is maximum down drag. Values are normalized based on half the screen height. Automatically returns 0 when pointer is released.

Syntax

DRAGY()

Examples

REM Real-time vertical scrolling
DO
  dy = DRAGY()
  IF dy <> 0 THEN scrollY = scrollY - dy * 100
  DELAY 16
LOOP
REM Drag-to-aim mechanic
DO
  dx = DRAGX()
  dy = DRAGY()
  IF dx <> 0 OR dy <> 0 THEN
    aimAngle = ATN2(dy, dx)
    aimPower = SQR(dx * dx + dy * dy)
    PRINT "Aim:", aimAngle, "Power:", aimPower
  END IF
  DELAY 16
LOOP
REM Pull-down-to-refresh gesture
DO
  dy = DRAGY()
  IF dy > 0.7 AND scrollY = 0 THEN
    PRINT "Refreshing..."
    CALL RefreshContent()
  END IF
  DELAY 16
LOOP

See also: DRAGX, TAP, MOUSEX, MOUSEY, MOUSEDOWN

↑ Back to top

GAMEPAD

Input · since 1.4.8

Controls virtual and physical gamepad input. The virtual gamepad displays on-screen buttons (D-pad + A/B/X/Y + START) for mobile touch controls. Physical gamepad support enables input from USB/Bluetooth controllers. Both use the same GAMEPAD MAP configuration to inject virtual key presses, allowing existing KEYDOWN-based games to work with any input method. The D-pad can optionally be displayed as an analog joystick that supports diagonal movement.

tvOS: the virtual (touch) gamepad is never shown on tvOS — there's no touchscreen to drive it — so GAMEPAD ON is a no-op there. Gate your UI with ISMOBILE() (false on tvOS) and use GAMEPAD PHYSICAL ON for the Siri Remote / game controllers instead. Older carts that call GAMEPAD ON unconditionally are handled safely: the overlay simply won't appear.

GAMEPAD PHYSICAL ON is a gate, and it comes first. Physical controller support starts off. That statement is what initializes the controller backend and begins polling — so until it runs, no controller is read and none is reported. JOYCONNECTED, JOYBUTTON, JOYAXIS and RUMBLE all return nothing beforehand, however many pads are plugged in or paired.

Enable it early — next to SCREEN — not when you first want to read a pad. On desktop and the web a controller is discovered by polling, so it turns up on the first poll after you enable and the ordering rarely bites. On Apple TV, iOS and Android it is pushed: the system announces a controller once, when it pairs. A pad that was already paired before your program enabled physical input announced itself while the subsystem was switched off. Enabling re-announces controllers it already knows about, precisely to cover this — but anything you read before enabling still comes back empty, and on Apple TV a controller is almost always already paired when your game starts. That's the failure to expect: a game that works on your desktop and sees no controller on the TV.

GAMEPAD PHYSICAL OFF stops polling again.

Syntax

GAMEPAD ON | OFF | MAP "button", "key" | BUTTON "button" ON|OFF | BUTTON "button" AT x, y | RESET | PHYSICAL ON | PHYSICAL OFF | DPAD JOYSTICK | DPAD BUTTONS | DPAD DRAG | ALPHA value

Parameters

  • ON - Show the virtual gamepad overlay (mobile touch)
  • OFF - Hide the virtual gamepad overlay
  • MAP "button", "key" - Map a gamepad button to simulate a keyboard key (affects both virtual and physical)
  • BUTTON "button" ON - Show a specific virtual button (default)
  • BUTTON "button" OFF - Hide a specific virtual button so it is neither drawn nor touch-interactive
  • BUTTON "button" AT x, y - Move a specific virtual button (0.0-1.0 window %); hit box follows automatically
  • RESET - Clear all button-to-key mappings
  • PHYSICAL ON - Enable physical gamepad/joystick polling
  • PHYSICAL OFF - Disable physical gamepad polling
  • DPAD JOYSTICK - Display D-pad as analog joystick (supports diagonal movement)
  • DPAD BUTTONS - Display D-pad as 4 separate buttons (default)
  • DPAD DRAG - Tap and drag anywhere on screen for directional input; touch point becomes the center, drag direction maps to UP/DOWN/LEFT/RIGHT
  • ALPHA value - Set the transparency of the virtual gamepad buttons (0.0 = fully transparent, 1.0 = fully opaque)
  • "button" - One of: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", "START"

Examples

REM Enable virtual gamepad on mobile, physical on desktop
IF ISMOBILE() THEN
  GAMEPAD ON
ELSE
  GAMEPAD PHYSICAL ON
END IF
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD MAP "A", "SPACE"
GAMEPAD MAP "START", "ESCAPE"
REM Game loop works the same for keyboard, virtual, and physical gamepad
DO
  IF KEYDOWN("W") THEN playerY = playerY - 2
  IF KEYDOWN("S") THEN playerY = playerY + 2
  IF KEYDOWN("A") THEN playerX = playerX - 2
  IF KEYDOWN("D") THEN playerX = playerX + 2
  IF KEYDOWN("SPACE") THEN CALL PlayerJump()
  DELAY 16
LOOP
REM Use joystick mode for diagonal movement on mobile
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD ON
GAMEPAD DPAD JOYSTICK
REM Now players can move diagonally by dragging the virtual joystick!
REM Use analog stick for smooth movement (physical gamepad)
GAMEPAD PHYSICAL ON
DO
  lx = JOYAXIS(0)
  ly = JOYAXIS(1)
  REM lx/ly are left stick X/Y (-1 to 1)
  IF ABS(lx) > 0.2 THEN playerX = playerX + lx * 5
  IF ABS(ly) > 0.2 THEN playerY = playerY + ly * 5
  DELAY 16
LOOP
REM Check if controller is connected
IF JOYCONNECTED() THEN
  PRINT "Controller detected!"
  GAMEPAD PHYSICAL ON
ELSE
  PRINT "No controller - use keyboard"
END IF
REM Drag-to-move: tap anywhere and drag for directional input
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD MAP "A", "SPACE"
GAMEPAD DPAD DRAG
GAMEPAD ON
REM D-pad buttons are hidden; drag anywhere to move, tap A/B/X/Y for actions

See also: GAMEPAD SETIMAGE, GAMEPAD ALPHA, GAMEPAD BUTTON, GAMEPAD BUTTON AT, KEYDOWN, ISMOBILE, JOYAXIS, JOYCONNECTED, TAP, TOUCHX, TOUCHY

↑ Back to top

GAMEPAD ALPHA

Input · since 1.7.3

Sets the transparency of the virtual gamepad buttons. This allows you to make the on-screen controls more or less visible, useful for adjusting visibility based on the game's visuals or player preference. The alpha value affects both the button fills and labels.

Syntax

GAMEPAD ALPHA value

Parameters

  • value - Transparency level from 0.0 (fully transparent/invisible) to 1.0 (fully opaque). Values outside this range are clamped.

Examples

REM Make the gamepad semi-transparent
GAMEPAD ON
GAMEPAD ALPHA 0.5
REM Make the gamepad nearly invisible
GAMEPAD ON
GAMEPAD ALPHA 0.2
REM Restore full opacity
GAMEPAD ALPHA 1.0
REM Fade gamepad based on player preference
alpha = LOAD("gamepad_alpha")
IF alpha = "" THEN alpha = 0.5
GAMEPAD ALPHA VAL(alpha)

See also: GAMEPAD, GAMEPAD SETIMAGE

↑ Back to top

GAMEPAD BUTTON

Input · since 1.19.3

Enables or disables an individual virtual gamepad button. Disabled buttons are not drawn and do not respond to touch, letting games show only the controls they actually use (for example, a puzzle game with just a D-pad and an A button). Buttons are enabled by default. Turning a pressed button OFF releases any mapped key before hiding it so no input gets stuck down. Only affects the on-screen virtual gamepad; physical gamepad buttons are unaffected.

Syntax

GAMEPAD BUTTON button$ ON|OFF
GAMEPAD BUTTON button$ AT x, y   (the position also accepts a coordinate: AT (x,y) or AT coord)

Parameters

  • button$ - The button to toggle: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", or "START"
  • ON - Show and enable the button (default state)
  • OFF - Hide the button and disable touch hit-testing for it

Examples

REM Puzzle game: only D-pad and A
GAMEPAD BUTTON "B" OFF
GAMEPAD BUTTON "X" OFF
GAMEPAD BUTTON "Y" OFF
GAMEPAD BUTTON "START" OFF
GAMEPAD ON
REM Runner game: only A for jump
GAMEPAD BUTTON "UP" OFF
GAMEPAD BUTTON "DOWN" OFF
GAMEPAD BUTTON "LEFT" OFF
GAMEPAD BUTTON "RIGHT" OFF
GAMEPAD BUTTON "B" OFF
GAMEPAD BUTTON "X" OFF
GAMEPAD BUTTON "Y" OFF
GAMEPAD BUTTON "START" OFF
GAMEPAD MAP "A", "SPACE"
GAMEPAD ON
REM Re-enable a button later
GAMEPAD BUTTON "START" ON

See also: GAMEPAD, GAMEPAD BUTTON AT, GAMEPAD SETIMAGE, GAMEPAD ALPHA

↑ Back to top

GAMEPAD BUTTON AT

Input · since 1.19.3

Repositions a virtual gamepad button. Coordinates are expressed as a fraction of the window size (0.0-1.0), matching the default layout. Values are clamped to that range. The hit box follows automatically so touches still register where the button is drawn. Use this to custom-lay-out controls per game without having to rebuild the engine, for example moving the A button further from the D-pad for thumb comfort or relocating START out of the way of gameplay.

Syntax

GAMEPAD BUTTON button$ AT x, y

Parameters

  • button$ - The button to move: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", or "START"
  • x - Horizontal center as a fraction of window width (0.0 = left edge, 1.0 = right edge)
  • y - Vertical center as a fraction of window height (0.0 = top edge, 1.0 = bottom edge)

Examples

REM Push the A button toward the bottom-right thumb zone
GAMEPAD BUTTON "A" AT 0.92, 0.85
GAMEPAD ON
REM Compact single-button layout: only A, centered low on the right
GAMEPAD BUTTON "UP" OFF
GAMEPAD BUTTON "DOWN" OFF
GAMEPAD BUTTON "LEFT" OFF
GAMEPAD BUTTON "RIGHT" OFF
GAMEPAD BUTTON "B" OFF
GAMEPAD BUTTON "X" OFF
GAMEPAD BUTTON "Y" OFF
GAMEPAD BUTTON "START" OFF
GAMEPAD BUTTON "A" AT 0.85, 0.8
GAMEPAD MAP "A", "SPACE"
GAMEPAD ON
REM Move START out of the way in the top-center
GAMEPAD BUTTON "START" AT 0.5, 0.08

See also: GAMEPAD, GAMEPAD BUTTON, GAMEPAD SETIMAGE

↑ Back to top

GAMEPAD SETIMAGE

Input · since 1.7.0

Sets a custom image for a virtual gamepad button. This allows you to replace the default button appearance with your own graphics, useful for theming the gamepad to match your game's art style. The image must be previously loaded with LOADIMG. Use an empty string to reset a button to its default appearance.

Syntax

GAMEPAD SETIMAGE button$, image$

Parameters

  • button$ - The button to customize: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", or "START"
  • image$ - The name of a loaded image to use, or empty string "" to reset to default

Examples

REM Load custom button images
LOADIMG "sprites/btn_jump.png", "jump_btn"
LOADIMG "sprites/btn_attack.png", "attack_btn"

REM Apply custom images to A and B buttons
GAMEPAD SETIMAGE "A", "jump_btn"
GAMEPAD SETIMAGE "B", "attack_btn"

REM Show the gamepad
GAMEPAD ON
REM Reset a button to default appearance
GAMEPAD SETIMAGE "A", ""
REM Theme all buttons for a retro game
LOADIMG "ui/dpad_up.png", "dpad_u"
LOADIMG "ui/dpad_down.png", "dpad_d"
LOADIMG "ui/dpad_left.png", "dpad_l"
LOADIMG "ui/dpad_right.png", "dpad_r"
GAMEPAD SETIMAGE "UP", "dpad_u"
GAMEPAD SETIMAGE "DOWN", "dpad_d"
GAMEPAD SETIMAGE "LEFT", "dpad_l"
GAMEPAD SETIMAGE "RIGHT", "dpad_r"

See also: GAMEPAD, GAMEPAD ALPHA, LOADIMAGE

↑ Back to top

INKEY$

Input · since 0.23.0

Returns the currently held key as a string, respecting case. Pressing 'a' returns "a", pressing Shift+A returns "A". Returns an empty string when no key is held. The value clears when the key is released. For case-insensitive key checking, use KEYDOWN() instead.

Syntax

variable$ = INKEY$

Examples

key$ = INKEY$
IF key$ <> "" THEN PRINT key$
DO
  k$ = INKEY$
LOOP WHILE k$ = ""
IF INKEY$ = "q" THEN END  ' Quit on lowercase q only
IF UCASE$(INKEY$) = "Q" THEN END  ' Quit on Q or q

See also: INPUT, ASC, KEYDOWN, PAUSE

↑ Back to top

INPUT

Input · since 0.23.0

Gets input from the user. By default, displays a simple text input. Use the WITH CURSOR modifier for a themed input field with a blinking cursor that uses the current foreground color for text and border, matching the CrashBasic visual style. The cursor character can be customized (e.g., "_" or "█"). User can type and edit input, then press Enter to submit. Multiple variables can be entered separated by commas.

Syntax

INPUT [WITH CURSOR(char$)] [prompt ;|,] variable [, variable ...]

Parameters

  • WITH CURSOR(char$) - Optional modifier to display a themed input field with blinking cursor using the specified character
  • prompt - Optional text to display before the input field
  • variable - Variable(s) to store input (string or numeric)

Examples

INPUT name$
INPUT "Enter your name: "; name$
INPUT "Enter your name: ", name$
INPUT WITH CURSOR("_") "Enter your name: "; name$
INPUT WITH CURSOR("█") "Password: "; pass$
COLOR 14: INPUT WITH CURSOR("_") "Yellow prompt: "; value$
INPUT "X, Y coordinates: "; x, y

See also: PRINT, INKEY$, COLOR, LOCATE

↑ Back to top

ISCONSOLE

Input · since 1.56.1

Returns 1 on a living-room console — tvOS today, with Xbox and Switch as those platforms land — and 0 everywhere else.

A console is the third case beside mobile and desktop: no touchscreen, but no keyboard and mouse either. It is driven by a remote or a game controller on a screen across the room. Checking ISMOBILE() alone can't find it, and treating "not mobile" as "desktop" lands a cart on mouse and keyboard assumptions that do not hold there.

The three platform classes are mutually exclusive, so a cart can pick its control scheme directly:

Platform ISMOBILE() ISCONSOLE()
iOS, Android 1 0
tvOS 0 1
Desktop, web 0 0

Both report 0 until the frontend has reported a platform, so a program that asks before its first SCREEN reads as desktop.

Syntax

ISCONSOLE()

Examples

REM Pick a control scheme per platform class.
IF ISCONSOLE() THEN
  GAMEPAD PHYSICAL ON            ' Siri Remote / controller
  PRINT "Use the remote to steer"
ELSEIF ISMOBILE() THEN
  GAMEPAD ON                     ' on-screen touch pad
  PRINT "Tap to jump, swipe to move"
ELSE
  PRINT "SPACE to jump, WASD to move"
END IF
REM A console has no pointer, so drive a cursor with the stick instead.
IF ISCONSOLE() THEN
  cursorX = cursorX + JOYAXIS(0) * speed
  cursorY = cursorY + JOYAXIS(1) * speed
ELSE
  cursorX = MOUSEX()
  cursorY = MOUSEY()
END IF

See also: ISMOBILE, JOYCONNECTED, JOYAXIS, JOYBUTTON, GAMEPAD

↑ Back to top

ISMOBILE

Input · since 1.4.8

Returns 1 in the native iOS and Android players, 0 everywhere else — desktop, web and tvOS. Use it to gate touch-specific UI: an on-screen gamepad, a "tap to continue" prompt, touch control hints.

tvOS is not mobile. It is a television driven by a remote, so it reports 0 — touch UI shown there would be something nobody can press. GAMEPAD ON (the virtual, on-screen gamepad) is a no-op on tvOS for the same reason; use GAMEPAD PHYSICAL ON for the Siri Remote and controllers.

It answers for the BUILD, not the device. The flag is the platform the player was compiled for — iOS or Android — so it is 1 in the native iOS and Android apps and 0 in a web browser, including a browser on a phone. There is no user-agent sniffing and no touch-capability probe.

That is deliberate rather than an oversight: the handheld experience ships as the native iOS and Android players, which is where touch UI, the virtual gamepad and the store integrations live. The web build targets desktop browsers, so ISMOBILE() reporting 0 there is the answer a cart wants. If you specifically need to serve a phone browser, detect it outside the cart and pass it in — ISMOBILE() will not tell you.

Returns 0 on tvOS — use ISCONSOLE() to detect a console.

Syntax

ISMOBILE()

Examples

REM Enable the on-screen gamepad only where it can be touched.
REM (0 on tvOS, so the Siri Remote path is taken instead.)
IF ISMOBILE() THEN
  GAMEPAD MAP "UP", "W"
  GAMEPAD MAP "DOWN", "S"
  GAMEPAD MAP "LEFT", "A"
  GAMEPAD MAP "RIGHT", "D"
  GAMEPAD MAP "A", "SPACE"
  GAMEPAD ON
ELSE
  GAMEPAD PHYSICAL ON
END IF
REM Show different control hints
IF ISMOBILE() THEN
  PRINT "Tap to jump, swipe to move"
ELSE
  PRINT "SPACE to jump, WASD to move"
END IF
REM Use touch or mouse based on device
IF ISMOBILE() THEN
  x = TOUCHX()
  y = TOUCHY()
ELSE
  x = MOUSEX()
  y = MOUSEY()
END IF

See also: GAMEPAD, JOYCONNECTED, TOUCHX, TOUCHY, TAP, DRAGX, DRAGY

↑ Back to top

JOYAXIS

Input · since 1.4.8

Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.

Returns the value of a physical gamepad axis. Sticks range -1 to 1 (small deadzone applied); triggers range 0 to 1. Returns 0 if the controller is not connected or the axis is not available.

Axis indices:

0 Left stick X 1 Left stick Y
2 Right stick X 3 Right stick Y
4 D-pad X (-1 left, +1 right) 5 D-pad Y (-1 up, +1 down)
6 Left trigger (0..1) 7 Right trigger (0..1)

The d-pad axes (4/5) are derived from the d-pad buttons so they read the same on every platform (+X right, +Y down). On native desktop the analog stick Y sign follows the hardware convention, which can differ from the d-pad's screen-style sign.

The optional player index (0-based) selects which connected controller to read; it defaults to 0 (the first controller), so existing single-controller code is unchanged. Up to 4 controllers are tracked (players 0 through 3). An out-of-range player returns 0.

Use GAMEPAD PHYSICAL ON to enable physical gamepad polling.

Syntax

JOYAXIS(axis [, player])

Parameters

  • axis - Axis index 0-7: 0/1 left stick, 2/3 right stick, 4/5 d-pad, 6/7 triggers (see description)
  • player - Optional 0-based controller index (0-3). Defaults to 0.

Examples

REM Basic analog stick movement
GAMEPAD PHYSICAL ON
DO
  lx = JOYAXIS(0)
  ly = JOYAXIS(1)
  playerX = playerX + lx * speed
  playerY = playerY + ly * speed
  DELAY 16
LOOP
REM Right stick for aiming
aimX = JOYAXIS(2)
aimY = JOYAXIS(3)
IF ABS(aimX) > 0.2 OR ABS(aimY) > 0.2 THEN
  aimAngle = ATN2(aimY, aimX)
END IF
REM Dual stick controls
DO
  REM Left stick for movement
  moveX = JOYAXIS(0) * moveSpeed
  moveY = JOYAXIS(1) * moveSpeed
  
  REM Right stick for camera
  camX = camX + JOYAXIS(2) * camSpeed
  camY = camY + JOYAXIS(3) * camSpeed
  
  DELAY 16
LOOP

See also: JOYCONNECTED, GAMEPAD, KEYDOWN

↑ Back to top

JOYBUTTON

Input · since 1.41.0

Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.

Returns 1 if the given physical gamepad button is currently held, 0 otherwise. The optional player index (0-based) selects which connected controller to read; it defaults to 0 (the first controller). Up to 4 controllers are tracked (players 0 through 3). An out-of-range player returns 0.

Button indices use the standard gamepad layout (the same physical button maps to the same index on every platform):

0 A 1 B 2 X 3 Y
4 LB (left shoulder) 5 RB (right shoulder)
6 LT (left trigger) 7 RT (right trigger)
8 SELECT/Back 9 START
10 L3 (left stick click) 11 R3 (right stick click)
12 D-pad Up 13 D-pad Down 14 D-pad Left 15 D-pad Right
16 Guide/Xbox 17 Share/Capture

LT/RT (6/7) report as pressed past a threshold; for their analog pressure use JOYAXIS(6) / JOYAXIS(7). Guide (16) and Share (17) are reported where the controller and platform expose them (Share is not standardized on the web).

Use GAMEPAD PHYSICAL ON to enable physical gamepad polling.

Syntax

JOYBUTTON(button [, player])

Parameters

  • button - Standard button index (0-17, see description).
  • player - Optional 0-based controller index (0-3). Defaults to 0.

Examples

REM Jump when the A button is pressed
GAMEPAD PHYSICAL ON
IF JOYBUTTON(0) THEN CALL player.jump()
REM Read face buttons + start
IF JOYBUTTON(0) THEN CALL fire()
IF JOYBUTTON(1) THEN CALL dash()
IF JOYBUTTON(9) THEN CALL pauseGame()
REM Two players, each on their own pad
IF JOYBUTTON(0, 0) THEN CALL p1Fire()
IF JOYBUTTON(0, 1) THEN CALL p2Fire()

See also: JOYAXIS, JOYCONNECTED, GAMEPAD, KEYDOWN

↑ Back to top

JOYCONNECTED

Input · since 1.4.8

Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.

Reports physical gamepad/joystick connectivity. Called with no argument, it returns the count of connected controllers (0 to 4). Called with a player index (0-based), it returns 1 if that specific controller is connected, or 0 if not.

Because any non-zero count is truthy, IF JOYCONNECTED() THEN ... still works to detect "at least one controller". Use the count to support local multiplayer, and JOYCONNECTED(n) to check a specific player slot. Up to 4 controllers are tracked (players 0 through 3).

Syntax

JOYCONNECTED([player])

Parameters

  • player - Optional 0-based controller index (0-3). When omitted, returns the total connected count instead.

Examples

REM Check for controller on startup
IF JOYCONNECTED() THEN
  PRINT "Press START to begin"
  GAMEPAD PHYSICAL ON
ELSE
  PRINT "Press ENTER to begin"
END IF
REM Local multiplayer: how many pads are plugged in?
pads = JOYCONNECTED()
PRINT pads; " controller(s) connected"
FOR p = 0 TO pads - 1
  PRINT "Player "; p; " ready"
NEXT
REM Check a specific player slot
IF JOYCONNECTED(1) THEN
  PRINT "Player 2 has a controller"
END IF

See also: JOYAXIS, JOYBUTTON, GAMEPAD, ISMOBILE

↑ Back to top

KEYDOWN

Input · since 0.23.0

Returns 1 if the specified key is currently pressed, 0 if not. Ideal for real-time game input and smooth character movement.

Key names are the ones the input boundary reports: Escape, Enter, Space, Tab, Backspace, Delete, Insert, Up, Down, Left, Right, Home, End, PageUp, PageDown, Shift, Control, Alt, CapsLock, F1-F12, or a single character such as "A" or "7". Case does not matter.

A name that names no key is not an error — it answers 0, because a key that cannot be pressed is never down. Watch the spelling for that reason: the key is called Escape, so KEYDOWN("ESC") returns 0 forever rather than telling you why.

Syntax

KEYDOWN(key_name)

Parameters

  • key_name - String name of key to check (e.g., "SPACE", "LEFT", "RIGHT", "UP", "DOWN", "A", "ESCAPE")

Examples

IF KEYDOWN("SPACE") THEN PRINT "Space is pressed"
IF KEYDOWN("LEFT") THEN playerX = playerX - 2
IF KEYDOWN("RIGHT") THEN playerX = playerX + 2
IF KEYDOWN("ESCAPE") THEN END

See also: INKEY$, MOUSEDOWN

↑ Back to top

MOUSEDOWN

Input · since 0.23.0

Returns 1 if the specified mouse button is currently pressed, 0 if not. Button parameter is optional and defaults to left mouse button.

Syntax

MOUSEDOWN([button])

Parameters

  • button - Mouse button number (0=left, 1=middle, 2=right) - optional, defaults to 0

Examples

IF MOUSEDOWN() THEN PRINT "Left mouse button pressed"
IF MOUSEDOWN(0) THEN PRINT "Left button"
IF MOUSEDOWN(2) THEN PRINT "Right button"
IF MOUSEDOWN() THEN
  IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, buttonW, buttonH) THEN
    CALL HandleButtonClick()
  END IF
END IF

See also: MOUSEX, MOUSEY, KEYDOWN, COLLISION

↑ Back to top

MOUSEX

Input · since 0.23.0

Returns the current X coordinate of the mouse cursor relative to the game canvas.

Syntax

MOUSEX()

Examples

mouseX = MOUSEX()
PRINT "Mouse at X:", MOUSEX()
IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, buttonW, buttonH) THEN
  PRINT "Mouse over button"
END IF

See also: MOUSEY, MOUSEDOWN, COLLISION

↑ Back to top

MOUSEY

Input · since 0.23.0

Returns the current Y coordinate of the mouse cursor relative to the game canvas.

Syntax

MOUSEY()

Examples

mouseY = MOUSEY()
PRINT "Mouse at Y:", MOUSEY()
IF COLLISION("POINT", MOUSEX(), MOUSEY(), targetX, targetY, targetW, targetH) THEN
  PRINT "Mouse over target"
END IF

See also: MOUSEX, MOUSEDOWN, COLLISION

↑ Back to top

PAUSE

Input · since 1.4.6

Halts program execution until the user presses any key. Useful for displaying messages that the user needs to read before continuing, or for simple "press any key to continue" prompts.

Syntax

PAUSE

Examples

PRINT "Press any key to continue..."
PAUSE
PRINT "Game Over!"
PRINT "Final Score:"; score
PAUSE
END
SCREEN 13
PRINT "Loading complete!"
PAUSE
CLS

See also: INKEY$, INPUT, DELAY, KEYDOWN

↑ Back to top

RUMBLE

Input · since 1.41.0

Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.

Vibrates a physical controller. Controllers have two motors: a low-frequency "heavy" motor and a high-frequency "light" motor.

Forms:

RUMBLE STRENGTH s DURATION ms drives both motors at magnitude s.
RUMBLE LOW lo HIGH hi DURATION ms drives the heavy and light motors separately.
RUMBLE STOP cancels rumble immediately.

Magnitudes are 0.0 to 1.0 (clamped). DURATION is in milliseconds; the rumble auto-stops when it elapses. PLAYER p (0-based) targets a specific controller and defaults to 0. RUMBLE PLAYER p STOP stops a specific controller.

Rumble targets a physical controller that supports vibration (turn on GAMEPAD PHYSICAL ON so controllers are detected). Strength is delivered as faithfully as each platform's haptics allow.

On mobile (iOS/Android), when no physical controller is connected for that slot — for example with the on-screen virtual gamepad (GAMEPAD ON) or touch play — RUMBLE falls back to the device's own vibrator so haptics still reach the player (on hardware that has one; iPads have no Taptic engine). On desktop and web, rumble drives the controller only.

Syntax

RUMBLE [PLAYER p] (STRENGTH s | LOW lo HIGH hi) DURATION ms

Parameters

  • PLAYER p - Optional 0-based controller index (0-3). Defaults to 0.
  • STRENGTH s - Both motors at magnitude s (0.0 to 1.0).
  • LOW lo HIGH hi - Heavy (low-freq) and light (high-freq) motor magnitudes separately.
  • DURATION ms - How long to rumble, in milliseconds (required for the play forms).

Examples

REM Quick buzz on hit
GAMEPAD PHYSICAL ON
IF playerHit THEN RUMBLE STRENGTH 0.8 DURATION 150
REM Heavy engine rumble on player 2's pad
RUMBLE PLAYER 1 LOW 0.9 HIGH 0.2 DURATION 500
REM Stop all rumble when paused
IF paused THEN RUMBLE STOP

See also: JOYBUTTON, JOYAXIS, GAMEPAD

↑ Back to top

TAP

Input · since 0.23.0

Returns 1 while pointer/finger is held down (without dragging), 0 when released or when dragging starts. Works with both touch (mobile) and mouse (desktop) input. This is a state, not an event - it remains 1 continuously while tapping, making it easy to detect in game loops.

Syntax

TAP()

Examples

REM Continuous fire while tapping
DO
  IF TAP() THEN
    PRINT "Firing at:", MOUSEX(), MOUSEY()
    CALL FireWeapon()
  END IF
  DELAY 16
LOOP
REM Button detection with visual feedback
IF TAP() THEN
  IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, 64, 32) THEN
    PRINT "Button pressed!"  ' Shows continuously while held
  END IF
END IF
REM Charge-up mechanic
chargeTime = 0
DO
  IF TAP() THEN
    chargeTime = chargeTime + 1
    PRINT "Charging:", chargeTime
  ELSE
    IF chargeTime > 0 THEN CALL ReleaseAttack(chargeTime)
    chargeTime = 0
  END IF
  DELAY 16
LOOP

See also: DRAGX, DRAGY, MOUSEX, MOUSEY, MOUSEDOWN, COLLISION

↑ Back to top

TOUCHX

Input · since 1.4.8

Returns the current X coordinate of an active touch point in logical game coordinates. Returns -1 if there is no active touch (finger is not touching the screen). Unlike MOUSEX(), this only responds to touch input and ignores mouse input, making it useful for distinguishing between mouse and touch events.

Syntax

TOUCHX()

Examples

REM Check for active touch
touchX = TOUCHX()
IF touchX >= 0 THEN
  PRINT "Touching at X:", touchX
END IF
REM Track touch position for painting
DO
  x = TOUCHX()
  y = TOUCHY()
  IF x >= 0 AND y >= 0 THEN
    PSET (x, y), 15
  END IF
  DELAY 16
LOOP

See also: TOUCHY, MOUSEX, TAP, DRAGX, ISMOBILE

↑ Back to top

TOUCHY

Input · since 1.4.8

Returns the current Y coordinate of an active touch point in logical game coordinates. Returns -1 if there is no active touch (finger is not touching the screen). Unlike MOUSEY(), this only responds to touch input and ignores mouse input.

Syntax

TOUCHY()

Examples

REM Check for active touch
touchY = TOUCHY()
IF touchY >= 0 THEN
  PRINT "Touching at Y:", touchY
END IF
REM Draw line from touch start to current position
IF TOUCHX() >= 0 THEN
  LINE (startX, startY)-(TOUCHX(), TOUCHY()), 14
END IF

See also: TOUCHX, MOUSEY, TAP, DRAGY, ISMOBILE

↑ Back to top

Graphics

ALPHA

Graphics · since 0.23.0

Graphics modifier used with PUTIMAGE to control transparency and fade effects. Value ranges from 0 (fully transparent) to 1 (fully opaque). Creates smooth transparency blending for advanced visual effects.

Syntax

ALPHA(fade_value)

Parameters

  • fade_value - Transparency level (0.0-1.0, where 0=transparent, 1=opaque)

Examples

PUTIMAGE (100, 100), "ghost", ALPHA(0.5)  ' 50% transparent
PUTIMAGE (50, 50), "sprite", 2, ALPHA(0.8)  ' Frame 2, 80% opaque
' Fade in effect:
FOR i = 0 TO 10
  alpha = i / 10
  PUTIMAGE (200, 100), "item", ALPHA(alpha)
NEXT
' Fade out effect:
FOR i = 10 TO 0 STEP -1
  alpha = i / 10
  PUTIMAGE (200, 100), "item", ALPHA(alpha)
NEXT

See also: PUTIMAGE, LOADIMG, PALETTE, SCALE, REGION

↑ Back to top

ASIMAGE

Graphics · since 1.27.0

Builds a named image from a DATA block of palette color indices, one value per pixel in row-major order (left-to-right, top-to-bottom). The result is an ordinary image resource: display it with PUTIMAGE, use it as a sprite texture, or capture and combine it like any other image. Ideal for hand-authored pixel art, icons, and tiles embedded directly in source.

The DATA block must contain exactly width * height values. Each value is a palette color index (0 or greater) or -1 for a transparent pixel. There is no upper bound on the index — the palette is dynamically sized. A value below -1, a missing DATA block, a value count that does not match width * height, or a zero/negative dimension is a runtime error.

In CrashNet multiplayer, ASIMAGE runs on every interpreter (host, player-server, client) so the image's dimensions register on each one's collision engine, just like NEWIMAGE / LOADIMG. The pixels are only realized where there is a renderer (the user's CrashPlayer client).

Syntax

ASIMAGE dataBlock$, imageName$, width, height

Parameters

  • dataBlock$ - Name of the DATA block holding the pixel values
  • imageName$ - String name to identify the created image
  • width - Image width in pixels (must be positive)
  • height - Image height in pixels (must be positive)
  • Each DATA value is a palette index (0+) or -1 for transparent
  • DATA values are read row-major: left-to-right, top-to-bottom
  • The block must hold exactly width * height values

Examples

REM 8x8 tile: red border, yellow ring, cyan center
DATA "tile"
  4,4,4,4,4,4,4,4
  4,14,14,14,14,14,14,4
  4,14,11,11,11,11,14,4
  4,14,11,11,11,11,14,4
  4,14,11,11,11,11,14,4
  4,14,11,11,11,11,14,4
  4,14,14,14,14,14,14,4
  4,4,4,4,4,4,4,4
END DATA
ASIMAGE "tile", "box", 8, 8
PUTIMAGE (100, 100), "box", SCALE(8, 8)
REM Transparent pixels with -1 (a white plus on a clear background)
DATA "plus"
  -1,15,-1
  15,15,15
  -1,15,-1
END DATA
ASIMAGE "plus", "cursor", 3, 3
PUTIMAGE (50, 50), "cursor"

See also: NEWIMAGE, PUTIMAGE, GETIMAGE, SET IMAGE, DATA

↑ Back to top

BEGIN FRAME

Graphics · since 0.23.0

Starts frame buffering for optimized graphics performance. All graphics operations between BEGIN FRAME and END FRAME are buffered and displayed simultaneously when END FRAME is reached. Essential for smooth animation and eliminating flicker.

Syntax

BEGIN FRAME
  graphics commands
END FRAME

Examples

BEGIN FRAME
  FOR i = 0 TO 100
    PSET (i, 50), 15
  NEXT i
END FRAME
WHILE INKEY$ = ""
  BEGIN FRAME
    CLS
    CIRCLE (x, y), 10, 14
    x = x + 1
  END FRAME
WEND
REM Star field animation
WHILE running
  BEGIN FRAME
    CLS
    FOR i = 0 TO 49
      PSET (starX(i), starY(i)), 7
      starX(i) = starX(i) - 1
    NEXT
  END FRAME
WEND

See also: END FRAME, CLS

↑ Back to top

BEGIN MEMORYBUFFER

Graphics · since 1.35.0

Draw a bunch of primitives into an offscreen buffer and bake the result into a single texture — efficiently.

Everything you draw between BEGIN MEMORYBUFFER and END MEMORYBUFFER (PSET, PRESET, LINE, CIRCLE, POLYGON, CLS) is recorded, then rasterized once on the CPU into one RGBA texture named textureName and uploaded a single time at END. This is dramatically cheaper than drawing the same primitives to the screen one at a time — instead of N round-trips to the renderer (each PSET its own command), the whole batch travels and uploads as a single operation. Use it for procedural textures, static backgrounds, generated sprites, starfields, icons — anything you build out of primitives once and then reuse.

The resulting texture works like any loaded image: hand it to SET SPRITE, PUTIMAGE, particles, etc.

Size

If you omit width, height, the buffer matches the current SCREEN resolution (so a SCREEN must be active). Pass an explicit width, height for a specific texture size. Maximum is 8192×8192. With FROM, the buffer is sized to its source instead — passing explicit width, height alongside FROM is a fatal error.

Seeding from an existing texture (FROM)

BEGIN MEMORYBUFFER "out" FROM "source" starts the buffer as a copy of an existing texture (any loaded image — LOADIMG, TEXTTOIMAGE, NEWIMAGE, or another memory buffer) and sizes it to that source. Your draws composite on top, so you can recolor, outline, mask, or otherwise procedurally alter existing pixels — ideal for post-processing TEXTTOIMAGE output. A transparent PRESET erases seeded pixels, giving cut-out/masking for free. Reading and overwriting the same name (FROM "spr" into "spr") is allowed. FROM reads pixel data, so it is client-only in multiplayer — wrap it in IF ME.ISCLIENT THEN.

Reading pixels with POINT / POINTRGB

Inside a FROM block, POINT(x, y) returns the palette index of the source pixel at (x, y) — or -1 if transparent — and POINTRGB(x, y) returns its raw packed colour (0xAARRGGBB). Both sample the frozen source as it was at BEGIN, never your in-block draws, so a convolution (blur, edge-detect, outline) reads pristine neighbours while writing results to the buffer without smearing. Out-of-range coordinates read as transparent. POINT/POINTRGB outside a memory-buffer block are a fatal error.

What's allowed inside

Primitive drawing (PSET, PRESET, LINE, CIRCLE, POLYGON, CLS) plus control flow (IF, FOR, WHILE, DO), variable assignment, and SUB/FUNCTION calls (so a helper that only draws primitives composes fine). Anything else — sprites, PRINT, PUTIMAGE, resource loads, SETIMAGE, BEGIN FRAME, SCREEN, audio, a nested BEGIN MEMORYBUFFER — is a fatal runtime error. The buffer is for primitive rasterization only.

CLS [color] inside the block fills the whole buffer (no color = transparent). The buffer starts fully transparent. Later draws overdraw earlier ones, exactly like drawing to the screen.

Textured polygons stay on the GPU

A POLYGON with a texture argument is intentionally rejected inside a memory buffer — texture sampling is a GPU operation and always will be, so it has no place in a CPU-rasterized buffer. Solid-color and fill polygons work fine here. When you want a textured polygon, draw it directly to a layer or the screen (outside the buffer), where the GPU does the sampling.

Multiplayer

Like TEXTTOIMAGE, this is a computed resource: it runs on every interpreter and registers the texture name/size everywhere, but is not broadcast (no per-pixel bandwidth). Build it per-client, e.g. inside IF ME.ISCLIENT THEN. CPU rasterization is deterministic, so every client produces identical pixels.

Syntax

BEGIN MEMORYBUFFER "textureName" [FROM "source"] [, width, height]
  primitive draws...
  idx = POINT(x, y)   ' only inside a FROM block
END MEMORYBUFFER

Parameters

  • textureName - String expression naming the texture to create (case-insensitive, like all image handles)
  • source - Optional (FROM clause). Seed the buffer from this existing texture and size it to the source. Mutually exclusive with width/height. Client-only in multiplayer. Enables POINT/POINTRGB sampling.
  • width - Optional buffer width in pixels (defaults to the current SCREEN width; not allowed with FROM)
  • height - Optional buffer height in pixels (defaults to the current SCREEN height; not allowed with FROM)

Examples

REM Procedural starfield baked into one texture
SCREEN 320, 240
BEGIN MEMORYBUFFER "stars"
  FOR i = 1 TO 200
    PSET (RND * 320, RND * 240), 15
  NEXT
END MEMORYBUFFER
PUTIMAGE (0, 0), "stars"
REM A 32x32 icon, then used as a sprite
BEGIN MEMORYBUFFER "icon", 32, 32
  CLS 0
  CIRCLE (16, 16), 14, 2, 1
  LINE (8, 16)-(24, 16), 15
END MEMORYBUFFER
ENABLE SPRITE 0
SET SPRITE 0, 100, 100, "icon"
REM Multiplayer: build the texture on each client only
IF ME.ISCLIENT THEN
  BEGIN MEMORYBUFFER "grid", 256, 256
    FOR x = 0 TO 256 STEP 16
      LINE (x, 0)-(x, 256), 8
    NEXT
  END MEMORYBUFFER
END IF
REM Recolor a rendered label in place: turn its glyph pixels red
IF ME.ISCLIENT THEN
  TEXTTOIMAGE "HI", "label", 32
  BEGIN MEMORYBUFFER "label" FROM "label"
    FOR y = 0 TO 31
      FOR x = 0 TO 200
        IF POINT(x, y) <> -1 THEN PSET (x, y), 4
      NEXT
    NEXT
  END MEMORYBUFFER
END IF

Notes

Primitives in the block are recorded as a batch and flushed at END as a single RasterizeMemoryBuffer gfx command; the renderer CPU-rasterizes them into one RGBA8 texture (reusing the same drawing algorithms as the live primitive path) and uploads it once via the texture store — the same path TEXTTOIMAGE uses. Only primitives + control flow + variable ops + calls are permitted inside; the stmt-level guard rejects everything else with a located error. Size defaults to SCREEN resolution, capped at 8192x8192. Textured POLYGON is rejected by design — texture mapping is GPU-only and never CPU-rasterized; draw textured polygons directly to a layer/screen instead. Not broadcast in multiplayer; runs everywhere and self-registers texture dimensions. FROM seeds the work buffer renderer-side from the named source (seed_from on RasterizeMemoryBuffer — no pixel bytes cross the wire); the interpreter separately fetches the source's CPU pixels once via a ReadImageData → PixelsReady round-trip to hold as the frozen POINT/POINTRGB snapshot (no GPU readback). POINT reverse-looks-up the live palette (exact match, else nearest by RGB); transparent reads -1. FROM is a fatal error on a server-side interpreter (no pixel mirror) and when combined with explicit dimensions.

See also: PSET, LINE, CIRCLE, POLYGON, CLS, POINT, POINTRGB, TEXTTOIMAGE, NEWIMAGE, SETIMAGE, SET SPRITE, PUTIMAGE

↑ Back to top

BEGIN SCENE

Graphics · since 1.4.3

Save the whole world, mess with it, restore it. That's BEGIN SCENE.

At push, the entire game state gets snapshotted — sprite states (position, velocity, behaviors, patterns, visibility, solid, tags, metadata), boundaries, mutation-bound polygons, active pattern instances, layer metadata (dimensions, scroll, alpha, wrap), tilemap assignments, current drawing layer, per-layer text state, palette. During the scene, any resource you create gets tracked (threads, timers, callbacks, sprites, layers, images). At END SCENE, the scene's resources get cleaned up and the pre-scene snapshot is restored — sprites snap back to where they were, behaviors resume, bound polygons return to their pre-scene shape, layers and scroll come back, tilemaps re-mount.

Scenes are nestable. Use them for battle transitions, dungeon dives, shop overlays, anything where you want to do a bunch of work and have the world go back to exactly how it was when you're done.

Retention mode (default, 1.29.0+)

The renderer-side snapshot moves the live GPU texture handles into the snapshot at push, leaving the scene body with a blank slate. On END SCENE, the saved handles move back. Restoration is a single-frame reference swap — no destroy, no recreate, no re-bake of tilemap textures. Visually instant.

Tradeoff: the pre-scene textures sit in VRAM untouched for the duration of the scene (~58MB per scene level for a 1280×720 game with 8 layers, less for smaller resolutions). Deeply nested scenes stack VRAM cost. If you're memory-constrained, consider FAST for those branches.

`BEGIN SCENE FAST "name"` — opt out of retention

The snapshot stores metadata only (plus a CPU clone of tile data); the scene body inherits the live layer state and overwrites it freely. On END SCENE, layers are destroyed and recreated from metadata, and tilemap textures re-bake from the saved tile arrays. Lower VRAM, but the re-render is visible — a few frames where things repaint. Good for one-way transitions (title screens, game-over screens), deeply nested scene stacks, or low-end hardware.

Tile-array sync after `END SCENE` (option B)

After the renderer restores its pre-scene state, BASIC arrays bound to tilemaps still hold whatever values they had at END SCENE time — not the pre-scene values. The renderer and your arrays are temporarily out of sync. Call UPDATE MAP "name" after END SCENE if you want the renderer resynced to the array's current state.

This is intentional. Scene retention is a render checkpoint, not a full state checkpoint. If you wanted full sync you'd also have to track every array assignment during the scene and roll them back, which is a deeper kind of save/load than a scene block should be doing.

Bound polygons

A polygon bound with SET POLYGON is a world object, so it obeys the same promise a sprite does. Move its shape inside a scene and END SCENE puts the geometry back — the drawn shape, the MUTATION it is bound to, and its SOLID collider all return together, so it never ends up drawn in one place and hit in another.

A polygon created inside a scene belongs to the scene: at END SCENE it is destroyed, binding and collider with it.

What does not restore is a MUTATION variable that was never bound to a polygon. That is an ordinary variable, and scenes do not roll variables back (same rule as the tile arrays above). Bind it to a polygon and its geometry becomes scene state; leave it unbound and it keeps whatever the scene left in it.

Screen restoration

If SCREEN w, h runs inside the scene, the logical screen dimensions get restored on END SCENE. The physical window stays at whatever size it last reached — only the user's coordinate space reverts. So you can SCREEN 800, 600 inside a scene, draw a high-res cutscene, and come back to your SCREEN 320, 240 retro game cleanly.

Multiplayer

The host's NetState::room_tilemaps is pushed and popped in lockstep with the renderer snapshot — under retention mode only. Late-joiners who connect during a scene see the scene's tilemap state; late-joiners who connect after END SCENE see the pre-scene state. Multiplayer-coherent without any extra work from you. FAST mode doesn't push the network-state stack (matches its no-retention-on-renderer semantics).

What survives, what doesn't

  • Under retention: layer pixel contents from freehand drawing (LINE, PSET, PRINT) ARE preserved. The textures themselves come back intact.
  • Under FAST: those are NOT preserved. The renderer rebuilds layers from scratch, so anything you drew gets re-baked from tilemap data only — freehand pixels are gone.

Syntax

BEGIN SCENE [FAST] "sceneName"
  statements
END SCENE

Parameters

  • FAST - Optional modifier. Opt-out of retention mode; use the pre-1.29.0 metadata-only snapshot path. Lower VRAM, visible re-render latency on END SCENE.
  • sceneName - A string expression identifying the scene (e.g., "menu", "level1", "cutscene")

Examples

REM Battle transition — overworld is fully restored, instantly
BEGIN SCENE "battle"
  CLS
  ' Set up battle sprites, UI, boundaries...
  WHILE battleActive
    ' Battle loop
    YIELD
  WEND
END SCENE
' Overworld sprites, scroll, patterns, AND rendered layer textures all back exactly as they were
REM Nested scenes — each restores its own level
BEGIN SCENE "dungeon"
  CREATE MAP ("dungeon.map.json", 0) AS LAYER 0
  ' ... explore dungeon ...
  BEGIN SCENE "boss"
    ' Boss fight in a separate scene
  END SCENE
  ' Back in dungeon, everything restored
END SCENE
' Back in overworld
REM Shop menu overlay
BEGIN SCENE "shop"
  CREATE LAYER 5, 320, 240
  ' Draw shop UI on layer 5
  ' ... shop interaction ...
END SCENE
' Layer 5 destroyed, overworld layers and scroll restored
REM Memory-constrained / one-way transition — opt out of retention
BEGIN SCENE FAST "titleScreen"
  ' Title screen drawing
END SCENE      ' visible re-render of the previous state, but no VRAM tax
REM Resync renderer to mutated tilemap array after scene
BEGIN SCENE "editor"
  tiles(5, 5) = NEW_TILE_ID
  UPDATE MAP "level1"
END SCENE
UPDATE MAP "level1"   ' resync the restored renderer to the array's current contents

Notes

At BEGIN SCENE: full world state is snapshotted. During scene: new resources are tracked. At END SCENE: (1) cleanup scene-created resources (threads, timers, callbacks, behaviors, patterns, shaders, sprites, layers, images), (2) restore snapshotted sprites/boundaries/bound polygons/patterns to behavior engine and re-sync GFX — a bound polygon's coordinates are written back to its MUTATION variable, which is where the renderer reads its vertices from, (3) restore layer metadata/scroll/tilemaps to GFX engine, (4) restore interpreter layer state, (5) restore palette. Layer pixel contents (freehand LINE/PSET/PRINT) are NOT preserved — only structural state. Tile maps regenerate automatically.

See also: SCREEN, LOADIMG, SET SPRITE, CREATE LAYER, SET PALETTE, CALL THREAD, CALL TIMER, CALL RENDER, CALL EVENT

↑ Back to top

CIRCLE

Graphics · since 0.23.0

Draws circles with specified center and radius. Can draw both outlined and filled circles. For optimal performance with multiple circles, use BEGIN FRAME/END FRAME.

Syntax

CIRCLE (x, y), radius [, color] [, fillColor]

Parameters

  • x, y - Center coordinates
  • radius - Circle radius
  • color - Outline color
  • fillColor - Optional fill color (if provided, circle is filled)

Examples

CIRCLE (160, 100), 50, 15
CIRCLE (160, 100), 50, 15, 12  ' Filled circle
FOR R = 10 TO 100 STEP 10
  CIRCLE (160, 100), R, 14
NEXT
' High-performance multiple circles:
BEGIN FRAME
  FOR R = 10 TO 100 STEP 10
    CIRCLE (160, 100), R, 14
  NEXT
END FRAME

See also: LINE, POLYGON, BEGIN FRAME, END FRAME

↑ Back to top

CLS

Graphics · since 0.23.0

Clears the screen and optionally sets background color or transitions to an image. Use CLS -1 to clear to fully transparent (useful for overlay layers). The TOIMAGE modifier specifies a loaded image to transition to instead of a solid color. The EFFECT modifier adds GPU-accelerated shader-based screen transition effects that are non-blocking - program execution continues while the effect plays. Each layer can have its own active transition. When the transition completes, the layer is filled with the target color or image.

In multiplayer, the bare CLS [color] form is client-only and fatal-errors on a server-side interpreter — clearing a layer / screen has no useful semantic on the host. Use EVENT CLIENTS "RefreshHud" to drive client-side clears from the host. Note: CLS paired with a target-image inside a NEWIMAGE/SET IMAGE setup block is allowed everywhere, since procedural-image creation runs on every interpreter for dimension registration.

Syntax

CLS [color] [TOIMAGE("imageName")] [EFFECT(effectName) [SPEED speed] [SIZE blockSize]]

Parameters

  • color - Background color (0-255, or -1 for transparent)
  • TOIMAGE("imageName") - Name of a loaded image to transition to (alternative to solid color)
  • EFFECT(effectName) - Screen transition effect: built-in effects (MELT, FADE, DISSOLVE, WIPEX, WIPEY, BLOCK) or a custom shader name in quotes
  • SPEED speed - Effect speed in ticks per second (default: 60). SPEED(60) = 1 second transition, SPEED(30) = 2 seconds, SPEED(120) = 0.5 seconds
  • SIZE blockSize - Block size in pixels for BLOCK effect (default: 8)

Examples

CLS
CLS 1  ' Blue background
CLS -1  ' Clear to transparent (for overlay layers)
CLS EFFECT(FADE)  ' Fade to black
CLS 4 EFFECT(MELT) SPEED 30  ' Slow melt to red (Doom-style, 2 seconds)
CLS EFFECT(DISSOLVE) SPEED 120  ' Fast pixel dissolve (0.5 seconds)
CLS EFFECT(WIPEX)  ' Horizontal wipe left to right
CLS EFFECT(WIPEY)  ' Vertical wipe top to bottom
CLS EFFECT(BLOCK) SIZE 16  ' Random 16x16 block clearing
CLS EFFECT("my_custom_shader")  ' Use a custom shader by name
CLS TOIMAGE("titleScreen")  ' Immediately show loaded image
CLS TOIMAGE("scene2") EFFECT(FADE)  ' Fade transition to image
CLS TOIMAGE("background") EFFECT(MELT) SPEED 60  ' Melt to image
' Wait for transition to complete:
CLS EFFECT(FADE)
WHILE TRANSITION() <> ""
  ' Continue other tasks
WEND

See also: COLOR, SCREEN, BEGIN FRAME, TRANSITION, SET LAYER, DEFINE SHADER, LOADIMG

↑ Back to top

COLOR

Graphics · since 0.23.0

Sets text foreground and background colors. The foreground argument is required; background is optional. Values are palette indices (0-255); in direct-color mode (SET PALETTE OFF) a full 0xAARRGGBB literal is also accepted, and a background of -1 means "no background".

Layer-scoped: color is remembered per layer. Setting COLOR while one layer is active (SET LAYER) does not change the text color of another layer; each layer keeps its own (default white, no background). CLS preserves a layer's color.

In multiplayer, COLOR is client-only and halts with a runtime error if called from a server-side interpreter. Color state is per-client renderer; drive it from a GUARD(CLIENT) handler triggered by EVENT CLIENTS.

Syntax

COLOR foreground [, background]

Parameters

  • foreground - Text color (palette index 0-255; required)
  • background - Background color (palette index 0-255, or -1 for none; optional)

Examples

COLOR 14  ' Yellow text
COLOR 15, 1  ' White on blue
COLOR 12: PRINT "Red text"

See also: CLS, PRINT

↑ Back to top

CREATE LAYER

Graphics · since 0.23.0

Creates a new graphics layer for multi-layer rendering and scrolling effects. PRINT and drawing go to the layer chosen by SET LAYER, which defaults to layer 0 — so drawing works before any SET LAYER, and layer 0 is always the default. Creating a layer does not change which layer is the draw target.

Syntax

CREATE LAYER layer_id, width, height

Parameters

  • layer_id - Layer number. By convention 0-99 are user layers and 100+ are reserved for internal use, but this is not enforced (any value 0-255 is accepted).
  • width - Layer width in pixels
  • height - Layer height in pixels

Examples

CREATE LAYER 0, 320, 200  ' Main game layer (layer 0 is the default draw target)
PRINT "Text goes to Layer 0"  ' Layer 0 is the default target until SET LAYER
CREATE LAYER 1, 640, 200  ' Scrolling background
CREATE LAYER 2, 320, 200  ' UI overlay

See also: SET LAYER, SET SCROLL, BEGIN FRAME, CREATE MAP

↑ Back to top

CREATE MAP

Graphics · since 1.4.3

Three ways to put a tilemap on a screen layer — pick by where the data lives.

Terminology first (the word "layer" is overloaded)

  • Tile layer is a horizontal slice INSIDE the tilemap (background grass, foreground decor, etc.). Indexed 0..N within the TILEMAP block. Selected by LAYER ....
  • Screen layer is the GPU's render layer (0..99). Where the tilemap actually gets painted. Selected by AS LAYER ....

These are different things sharing one word. LAYER (no AS) is always a tile layer. AS LAYER is always a screen layer. Don't conflate them.

Form 1: file-loaded (the original)

CREATE MAP("level1.map.json") AS LAYER 0

Loads a .map.json produced by the Map Editor widget. The paren form predates the named and inline forms; still works, still supported.

Form 2: named (renders a `TILEMAP` DEF)

CREATE MAP "level1" AS LAYER 0                    ' tile layer 0 → screen 0
CREATE MAP "level1" LAYER "foreground" AS LAYER 2 ' named tile layer → screen 2
CREATE MAP "level1" LAYER 0 TO 2 AS LAYER 0 TO 2  ' splat layers 0..=2 onto screens 0..=2

Renders a tilemap defined earlier with a TILEMAP block. The LAYER selector picks one or more tile layers from the named tilemap. Omit it and tile layer 0 is used — or, when AS LAYER is a range, tile layers 0..=N auto-splat onto the screen range.

This is the canonical form for programmatic tilemaps. Use it with TILEMAP DEF (which sets up the bindings) and UPDATE MAP (which pushes array mutations to the GPU). Tilemap data lives in your BASIC arrays; the renderer is downstream of them. There's no SETMAPTILE statement and never will be — tileArr(x, y) = newIdx is the way.

Form 3: inline (anonymous from a BASIC array)

CREATE MAP FROM tileArr TILESET "grass.png" TILESIZE 32 AS LAYER 0

One-shot, no TILEMAP DEF needed. Optional META metaArr binds a string array as metadata. Dimensions are inferred from the tile array's shape unless you pass SIZE w, h explicitly.

What you can do with a rendered map layer

  • Scroll it via SET SCROLL.
  • Make it transparent via SET LAYER ALPHA.
  • Animate tiles automatically — each animation runs at the frameTime declared in the basemap (default ~200ms per frame). SPEED fps is an optional global override that forces every animation on the map onto a fixed cadence (SPEED 4 = 4 frames/sec); omit it and per-animation timing is preserved.
  • Query metadata via MAPMETA$() and FOR MAPDATA. Metadata is single characters or emojis at grid positions — handy for marking spawn points, items, triggers, anything you want to look up by tile coord. Works identically against all three forms once the map is rendered.

What you can't do

  • Draw on a map layer with PSET, LINE, etc. Map layers are read-only at the renderer.
  • CLS a map layer. That destroys the tilemap.
  • Go bigger than 512×512 tiles. That's the GPU-accelerated rendering limit (16384×16384 pixels at 32px tiles).
  • Use more than 500 animated tile definitions or 5 frames each. Tileset animation budget. (Bumped from 8 in 1.36.4 — the old cap was a habit, not a hardware limit; 500 is more animations than any tileset will ever have unique tiles to trigger them, so the practical ceiling is now "as many animations as your tileset has unique source tiles.")

Room-aware replication

When called from the host inside a WITH ROOM "name" block, the mount tags with that room. Outgoing broadcasts to clients filter on ME.ROOM = name, so only players in that room render it. Late-join replay (replay_room_snapshot_to_player) and SET ROOM transitions walk the same per-room snapshot. Outside any WITH ROOM block, the host falls back to a _global sentinel that's visible to every connected player. Non-host interpreters don't write to the snapshot store — the host is the sole authoritative author.

One array is one map layer

AS LAYER x TO y spreads a map's layers across a range of screen layers, so it belongs to the file-loaded and named forms. The inline FROM tiles_array form describes exactly one map layer — one array, one layer — so there is nothing for a range to spread, and writing one is a runtime error.

This is a constraint rather than an omission: map layers are not required to share dimensions. A multi-layer map binds each layer explicitly so each can declare its own size:

TILEMAP "level1"
  BASEMAP "level1.map.json"
  TILES 0 INTO ground
  TILES 1 INTO decor
END TILEMAP

CREATE MAP "level1" LAYER 0 TO 1 AS LAYER 0 TO 1

Syntax

CREATE MAP("map_file" [, map_layer_index [TO map_layer_end]]) AS LAYER layer_id [TO layer_end] [SPEED fps] [WRAPX] [WRAPY]
CREATE MAP "tilemap_name" [LAYER tile_layer_selector] AS LAYER layer_id [TO layer_end] [SPEED fps] [WRAPX] [WRAPY]
CREATE MAP FROM tiles_array [META meta_array] TILESET "image" TILESIZE w[, h] [SIZE w, h] AS LAYER layer_id [SPEED fps] [WRAPX] [WRAPY]

Parameters

  • map_file - File-loaded form: path to the .map.json file (created with Map Editor widget).
  • map_layer_index - File-loaded form: which tile layer from the map file to load (0-based). Defaults to 0.
  • tilemap_name - Named form: a TILEMAP defined earlier with TILEMAP "name" ... END TILEMAP. If the string ends with .map.json or .json AND no TILEMAP is registered under that name, falls back to file-loaded form.
  • tile_layer_selector - Named form: optional selector picking which tile layer in the named TILEMAP to render. Three shapes: a string AS-name (LAYER "foreground"), a numeric index (LAYER 1), or a numeric range (LAYER 0 TO 2). The range form must be paired with AS LAYER X TO Y of the same width and splats each tile layer onto consecutive screen layers. Defaults: when AS LAYER is a single layer, tile layer 0; when AS LAYER is a range (X TO Y), tile layers 0..=(Y-X) auto-splat onto X..=Y — equivalent to writing LAYER 0 TO (Y-X) explicitly.
  • tiles_array - Inline form: globally-scoped 2D INTEGER array holding tile indices [x][y].
  • meta_array - Inline form: optional globally-scoped 2D STRING array holding per-cell metadata.
  • TILESET / TILESIZE / SIZE - Inline form: tileset image name, per-tile pixel dimensions, optional explicit map size in tiles (default: array shape).
  • layer_id - Target screen layer number (0-99). Layers 100+ are reserved for internal use.
  • SPEED fps - Optional global override forcing every animation on the map onto a fixed cadence (SPEED 4 = 4 fps). Omit it and each animation plays at its basemap-authored frame timing (default ~200ms per frame, i.e. ~5 fps).
  • WRAPX / WRAPY - Optional: enable infinite scrolling by wrapping at map edges.
  • Metadata layers (file-loaded or via TILEMAP DEF) are automatically queryable via MAPMETA$() / FOR MAPDATA.

Examples

REM File-loaded form (original; still supported)
CREATE MAP("level1.map.json") AS LAYER 0
REM Named form — renders a TILEMAP DEF
DIM tiles(0 TO 99, 0 TO 59) AS INTEGER
TILEMAP "level1"
    TILESET "grass.png"
    TILESIZE 32, 32
    SIZE 100, 60
    TILES 0 FROM tiles
END TILEMAP
CREATE MAP "level1" AS LAYER 0
REM Named form with multi-layer tilemap, picked by name
CREATE MAP "level1" LAYER "background" AS LAYER 0
CREATE MAP "level1" LAYER "foreground" AS LAYER 2
REM Named form with multi-layer tilemap, picked by numeric index
CREATE MAP "level1" LAYER 0 AS LAYER 0
CREATE MAP "level1" LAYER 2 AS LAYER 2
REM Named form splatting a tile-layer range onto consecutive screen layers
CREATE MAP "level1" LAYER 0 TO 2 AS LAYER 0 TO 2  ' tile layer 0 → screen 0, 1 → 1, 2 → 2
REM AS LAYER range with no explicit LAYER selector — auto-splats from tile layer 0
CREATE MAP "level1" AS LAYER 1 TO 2  ' equivalent to LAYER 0 TO 1 AS LAYER 1 TO 2
REM Room-aware: inside WITH ROOM, the mount tags with that room
WITH ROOM "lobby"
    CREATE MAP "lobbyMap" AS LAYER 1   ' only players in "lobby" see it
END WITH
REM Inline form — anonymous one-shot, dimensions inferred
DIM proceduralTiles(0 TO 49, 0 TO 29) AS INTEGER
CREATE MAP FROM proceduralTiles TILESET "grass.png" TILESIZE 32 AS LAYER 0
REM Pathfinding pattern: same array drives renderer + AI logic
DIM tiles(0 TO 99, 0 TO 59) AS INTEGER
TILEMAP "level1"
    TILESET "grass.png"
    TILESIZE 16, 16
    SIZE 100, 60
    TILES 0 FROM tiles
END TILEMAP
CREATE MAP "level1" AS LAYER 0

' AStar reads the same array the GPU is rendering
DIM path AS COORDINATE
path = AStar(tiles, startX, startY, endX, endY)

' Mutate to open a door, push to GPU
tiles(doorX, doorY) = OPEN_DOOR_TILE
UPDATE MAP "level1"
REM Find spawn points from metadata
CREATE MAP("level1.map.json") AS LAYER 0
FOR MAPDATA (0, 0)-(mapWidth, mapHeight) AS x, y, m$
  IF m$ = "P" THEN
    playerX = x * 16
    playerY = y * 16
  END IF
NEXT MAPDATA

See also: TILEMAP, UPDATE MAP, CREATE LAYER, SET LAYER, SET SCROLL, RESET LAYERS, MAPMETA$, MAPWIDTH, MAPHEIGHT, FOR MAPDATA

↑ Back to top

DESTROY POLYGON

Graphics · since 1.43.0

Removes a mutation-bound polygon created with SET POLYGON, unbinding its mutation and freeing the slot. The id is the same 0-based handle passed to SET POLYGON. Like SET POLYGON, this is client-only in multiplayer — calling it server-side in a CrashNet game is a fatal error. The shape's sprite half goes with it — its collider, behavior and pose are removed together, so nothing is left behind to collide with. The MUTATION itself is untouched: unbinding a polygon does not destroy the shape's data, and the same mutation can be bound again.

Syntax

DESTROY POLYGON id

Parameters

  • id - 0-based polygon handle to destroy

Example

SET POLYGON 0 BIND blob FILL 4
REM ... later ...
DESTROY POLYGON 0

See also: SET POLYGON, MUTATION

↑ Back to top

END FRAME

Graphics · since 0.23.0

Ends frame buffering and displays all buffered graphics operations at once. This eliminates flickering and provides smooth animation by updating the entire screen simultaneously. Optimized for maximum performance.

Syntax

END FRAME

Examples

BEGIN FRAME
  CLS
  FOR i = 0 TO 319
    PSET (i, 100), RND * 16
  NEXT
END FRAME
REM High-performance animation loop
WHILE running
  BEGIN FRAME
    CLS
    REM Draw all objects here
    CIRCLE (playerX, playerY), 5, 15
    LINE (enemyX, enemyY)-(enemyX+10, enemyY+10), 12, BF
  END FRAME
WEND

See also: BEGIN FRAME, CLS

↑ Back to top

FONT

Graphics · since 0.23.0

Sets the font family and size for text rendering, or resets to default font. Built-in fonts "IBM BIOS" and "IBM EGA 8x8" are available without loading. Custom fonts can be loaded with LOADFONT. The CURRENT sentinel keeps whatever font is already active and only changes the size — useful in render loops that mix sizes of the same face. FONT and SETFONT are interchangeable keywords.

Layer-scoped: font is remembered per layer. FONT set while one layer is active (SET LAYER) does not change another layer's font; each layer keeps its own and defaults to "IBM EGA 8x8" at size 8. CLS preserves a layer's font. (CURRENT keeps the active layer's current font.)

Syntax

FONT fontname$, size
FONT CURRENT, size
FONT RESET
SETFONT fontname$, size
SETFONT CURRENT, size

Parameters

  • fontname$ - Font family name (built-in: "IBM BIOS", "IBM EGA 8x8", or custom loaded font)
  • CURRENT - Sentinel meaning "keep the current font, only change size"
  • size - Font size in pixels

Examples

FONT "IBM EGA 8x8", 16
PRINT "Retro pixel font"
FONT "IBM BIOS", 8
PRINT "Classic BIOS font"
' Keep the same font, just resize for a heading
FONT "MyBody", 12
PRINT "body text"
FONT CURRENT, 24
PRINT "HEADING"
FONT CURRENT, 12
PRINT "back to body"
FONT RESET  ' Return to default font

See also: LOADFONT, COLOR, PRINT, LOCATE

↑ Back to top

GETIMAGE

Graphics · since 0.23.0

Captures a rectangular region of the current layer and stores it as a named image that can be displayed with PUTIMAGE. The captured image becomes a reusable resource.

Syntax

GETIMAGE (x1, y1)-(x2, y2), imageName$

Parameters

  • x1, y1 - Top-left corner of capture region
  • x2, y2 - Opposite corner of the capture region (coordinates may be given in any order)
  • imageName$ - String name to identify the captured image

Examples

REM Capture a sprite from the screen
CIRCLE (50, 50), 25, 15
GETIMAGE (25, 25)-(75, 75), "circle"
REM Use the captured image
PUTIMAGE (100, 100), "circle"
PUTIMAGE (200, 100), "circle", SCALETO(64, 64)
REM Capture and reuse UI elements
LINE (10, 10)-(90, 40), 15, BF
LOCATE 2, 2: PRINT "BUTTON"
GETIMAGE (10, 10)-(90, 40), "button"
CLS
PUTIMAGE (50, 50), "button"
PUTIMAGE (50, 100), "button"
REM Capture gameplay screenshots
GETIMAGE (0, 0)-(319, 199), "screenshot"
SAVEJSON "game_state.json", screenshot$  ' Can serialize the name

See also: PUTIMAGE, NEWIMAGE, LOADIMG

↑ Back to top

GETSCROLL

Graphics · since 1.0.0

Returns the current scroll offset of a layer as a coordinate. If no layer is specified, returns the scroll offset of the current layer. The returned coordinate can be used directly or decomposed into X and Y components.

Syntax

GETSCROLL([layer])

Parameters

  • layer - Optional layer number. Defaults to the current layer if omitted

Examples

pos = GETSCROLL()           ' Get current layer scroll as coordinate
pos = GETSCROLL(1)          ' Get scroll offset of layer 1
' Use with coordinate decomposition:
scroll = GETSCROLL()
PRINT scroll.X, scroll.Y
' Save and restore scroll position:
saved = GETSCROLL()
SET SCROLL(0, 0)  ' Reset
' ... draw UI ...
SET SCROLL(saved.X, saved.Y)  ' Restore

See also: SET SCROLL, CREATE LAYER, SET LAYER

↑ Back to top

IMAGEHEIGHT

Graphics · since 1.36.2

Returns the base height in pixels of a loaded image texture identified by its string handle (the name passed to LOADIMG, NEWIMAGE, TEXTTOIMAGE, BASE64TOIMG, etc.). The result is the texture's intrinsic dimension, independent of any sprite that references it. Image handles are case-insensitive. Returns 0 if no image with that handle is loaded.

Syntax

IMAGEHEIGHT(name$)

Parameters

  • name$ - String handle of a loaded image texture

Examples

LOADIMG "player.png", "player"
h = IMAGEHEIGHT("player")  ' Height of the player texture
' Stack two images vertically:
PUTIMAGE 0, 0, "top"
PUTIMAGE 0, IMAGEHEIGHT("top"), "bottom"

See also: IMAGEWIDTH, SPRITEWIDTH, SPRITEHEIGHT, LOADIMG, NEWIMAGE, PUTIMAGE

↑ Back to top

IMAGEWIDTH

Graphics · since 1.36.2

Returns the base width in pixels of a loaded image texture identified by its string handle (the name passed to LOADIMG, NEWIMAGE, TEXTTOIMAGE, BASE64TOIMG, etc.). The result is the texture's intrinsic dimension, independent of any sprite that references it. Image handles are case-insensitive. Returns 0 if no image with that handle is loaded.

Syntax

IMAGEWIDTH(name$)

Parameters

  • name$ - String handle of a loaded image texture

Examples

LOADIMG "player.png", "player"
w = IMAGEWIDTH("player")  ' Width of the player texture
' Place an image flush against the right edge of the screen:
LOADIMG "hud.png", "hud"
PUTIMAGE SCREENWIDTH - IMAGEWIDTH("hud"), 0, "hud"
' Center an image on screen:
x = (SCREENWIDTH - IMAGEWIDTH("logo")) / 2
y = (SCREENHEIGHT - IMAGEHEIGHT("logo")) / 2
PUTIMAGE x, y, "logo"

See also: IMAGEHEIGHT, SPRITEWIDTH, SPRITEHEIGHT, LOADIMG, NEWIMAGE, PUTIMAGE

↑ Back to top

LINE

Graphics · since 0.23.0

Draws lines and rectangles. A color or a B/BF style is required — LINE (x1,y1)-(x2,y2) with no color and no style does not parse. For smooth animation with multiple lines, use BEGIN FRAME/END FRAME to prevent flicker.

Syntax

LINE (x1, y1)-(x2, y2), color [, B[F]]
LINE (x1, y1)-(x2, y2), B[F]

Parameters

  • x1, y1 - Start coordinates
  • x2, y2 - End coordinates
  • color - Line color (required unless a B/BF style is given instead)
  • B - Box mode
  • BF - Filled box

Examples

LINE (0, 0)-(100, 100), 15
LINE (50, 50)-(150, 100), 12, B
LINE (10, 10)-(90, 90), 4, BF
REM Animated wireframe
BEGIN FRAME
  CLS
  FOR i = 0 TO 10
    LINE (i*30, 0)-(i*30, 200), 14
  NEXT
END FRAME

See also: PSET, CIRCLE, BEGIN FRAME

↑ Back to top

LOADFONT

Graphics · since 1.2.0

Loads a custom font file (TTF, OTF, or WOFF2) from disk and registers it with the given name for use with FONT or SETFONT. The font is cached for the duration of the program. Built-in fonts like "IBM BIOS" and "IBM EGA 8x8" are available without loading.

In CrashNet multiplayer, LOADFONT runs on every interpreter so subsequent SETFONT / GPRINT / TEXTTOIMAGE references resolve consistently. The actual font bytes are dropped by the headless host's GfxClient (it doesn't render text), but the name registration happens locally on each interpreter — call LOADFONT once at top-level and host / player-server / client all know the font exists.

Syntax

LOADFONT filename$, name$

Parameters

  • filename$ - Path to the font file (TTF, OTF, or WOFF2 format)
  • name$ - Name to register the font as for later use

Examples

LOADFONT "fonts/custom.ttf", "MyFont"
FONT "MyFont", 16
PRINT "Custom font text"
REM Load multiple fonts
LOADFONT "assets/title.ttf", "TitleFont"
LOADFONT "assets/body.ttf", "BodyFont"
FONT "TitleFont", 32
PRINT "GAME TITLE"
FONT "BodyFont", 12
PRINT "Press any key to start"

See also: FONT, TEXTTOIMAGE

↑ Back to top

LOCATE

Graphics · since 0.23.0

Positions text cursor at specific coordinates with optional pixel-level offset. LOCATE RELATIVE offsets the cursor from its current position by the given number of rows and columns, allowing you to move backward to overwrite previously printed text.

In multiplayer, LOCATE is client-only — a server-side call halts with a runtime error. The text cursor lives in the client's renderer.

Syntax

LOCATE [row] [, column] [ORIGIN(x, y)]
LOCATE RELATIVE deltaRow [, deltaCol]
(ORIGIN also accepts a coordinate: ORIGIN(coord); ORIGIN alone defaults to (0,0))

Parameters

  • row - Row position (1-based)
  • column - Column position (1-based)
  • ORIGIN(x, y) - Pixel offset from column/row position
  • RELATIVE - Use relative positioning (delta from current cursor position)
  • deltaRow - Number of rows to offset (negative = up)
  • deltaCol - Number of columns to offset (negative = left)

Examples

LOCATE 10, 20
LOCATE 5, 1: PRINT "Top left"
LOCATE , 40: PRINT "Column 40"
LOCATE 10, 10 ORIGIN(20, 5)
LOCATE 15, 10 ORIGIN(40, 10)
' Overwrite previous text
LOCATE 1, 1: PRINT "Score: 0"
LOCATE RELATIVE 0, -1: PRINT "5"
' Move cursor back to rewrite a value
PRINT "HP: 100";
LOCATE RELATIVE 0, -3: PRINT "075";

See also: PRINT, COLOR

↑ Back to top

MAPTILE

Graphics · since 1.13.0

Returns the 0-based tile index (the source Tiled tile id minus 1) at the given map coordinates. Defaults to the current screen layer if you omit screenLayer. Returns -1 if no tile exists at the position or the queried layer isn't a map layer.

Need to know what's under the mouse? Use MAPTILEX / MAPTILEY to convert pixel coordinates to tile coordinates first.

Server-side lookup

Player-server interpreters have no renderer of their own and never receive CreateTilemap broadcasts — they ARE the broadcast source. So their local map cache is empty. To make them useful for collision / pathfinding / map-aware logic, MAPTILE falls back to the per-room tilemap snapshot on NetState when the local cache misses.

Which room's snapshot? Resolved in this order:

  1. The WITH ROOM context if one is active.
  2. The interpreter's ME.ROOM.
  3. The _global sentinel.

Limit: the snapshot fallback only handles tile_layer == 0. Multi-tile-layer tilemaps live as separate snapshots per (room, screen_layer) (see CREATE MAP's splat form), so querying a deeper tile layer through the snapshot route returns -1.

Syntax

MAPTILE(tileX, tileY [, screenLayer])

Parameters

  • tileX - Tile X coordinate (map space, not pixels)
  • tileY - Tile Y coordinate (map space, not pixels)
  • screenLayer - Optional: screen layer to query (defaults to the current layer)

Examples

' Check tile at map coordinates
tile = MAPTILE(5, 10)
IF tile = 5 THEN PRINT "Solid wall!"
' Check tile at mouse position (convert pixels to tiles first)
tx = MAPTILEX(MOUSEX(), 1)
ty = MAPTILEY(MOUSEY(), 1)
tile = MAPTILE(tx, ty, 1)
PRINT "Tile: " + STR$(tile)
' Collision detection using tile coordinates
tx = MAPTILEX(playerX + 8)
ty = MAPTILEY(playerY + 8)
IF MAPTILE(tx, ty) > 0 THEN
  playerBlocked = 1
END IF

See also: MAPTILEX, MAPTILEY, MAPMETA$, CREATE MAP, SET LAYER

↑ Back to top

MAPTILEX

Graphics · since 1.13.0

Converts a SCREEN pixel X coordinate to tile X coordinate by adding the layer's scroll offset. This accounts for scrolling so the result maps to the correct tile even when the map has scrolled. Use this for MOUSEX(), TOUCHX(), and other screen-relative coordinates. For world coordinates like SPRITEX() that are already in map space, use MAPWORLDTILEX instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: screen input (MOUSEX) → MAPTILEX, world position (SPRITEX) → MAPWORLDTILEX.

Syntax

MAPTILEX(screenPixelX [, layerNum])

Parameters

  • screenPixelX - Screen pixel X coordinate (e.g., MOUSEX)
  • layerNum - Optional: Layer number to query (defaults to current layer)

Examples

' Track mouse on a scrolled map
tx = MAPTILEX(MOUSEX())
ty = MAPTILEY(MOUSEY())
tile = MAPTILE(tx, ty)
IF tile > 0 THEN PRINT "Hovering tile " + STR$(tile)
' Tile-based click detection on scrolled map
IF MOUSEDOWN() THEN
  tx = MAPTILEX(MOUSEX(), 1)
  ty = MAPTILEY(MOUSEY(), 1)
  PRINT "Clicked tile (" + STR$(tx) + "," + STR$(ty) + ")"
END IF

See also: MAPTILEY, MAPWORLDTILEX, MAPWORLDTILEY, MAPTILE, MOUSEX

↑ Back to top

MAPTILEY

Graphics · since 1.13.0

Converts a SCREEN pixel Y coordinate to tile Y coordinate by adding the layer's scroll offset. This accounts for scrolling so the result maps to the correct tile even when the map has scrolled. Use this for MOUSEY(), TOUCHY(), and other screen-relative coordinates. For world coordinates like SPRITEY() that are already in map space, use MAPWORLDTILEY instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: screen input (MOUSEY) → MAPTILEY, world position (SPRITEY) → MAPWORLDTILEY.

Syntax

MAPTILEY(screenPixelY [, layerNum])

Parameters

  • screenPixelY - Screen pixel Y coordinate (e.g., MOUSEY)
  • layerNum - Optional: Layer number to query (defaults to current layer)

Examples

' Get tile row at mouse position
ty = MAPTILEY(MOUSEY(), 1)
PRINT "Mouse at tile row: " + STR$(ty)
' Check if mouse click is in specific tile row
IF MOUSEDOWN() AND MAPTILEY(MOUSEY()) = 5 THEN
  PRINT "Row 5 clicked!"
END IF

See also: MAPTILEX, MAPWORLDTILEX, MAPWORLDTILEY, MAPTILE, MOUSEY

↑ Back to top

MAPWORLDTILEX

Graphics · since 1.13.0

Converts a WORLD pixel X coordinate to tile X coordinate without adding scroll offset. Use this for SPRITEX() and other world/logical coordinates that are already in map space. For screen-relative coordinates like MOUSEX() that need scroll adjustment, use MAPTILEX instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: world position (SPRITEX) → MAPWORLDTILEX, screen input (MOUSEX) → MAPTILEX.

Syntax

MAPWORLDTILEX(worldPixelX [, layerNum])

Parameters

  • worldPixelX - World pixel X coordinate (e.g., SPRITEX)
  • layerNum - Optional: Layer number to query (defaults to current layer)

Examples

' Get tile column where sprite is located
tx = MAPWORLDTILEX(SPRITEX(0), 1)
PRINT "Sprite at tile column: " + STR$(tx)
' Check sprite collision with tile
tx = MAPWORLDTILEX(SPRITEX(playerSprite))
ty = MAPWORLDTILEY(SPRITEY(playerSprite))
IF MAPTILE(tx, ty) = SOLID_TILE THEN
  PRINT "Sprite hit solid tile!"
END IF
' Get tile under sprite center
centerX = SPRITEX(0) + 8  ' Assuming 16px sprite
tx = MAPWORLDTILEX(centerX)
PRINT "Sprite center at tile: " + STR$(tx)

See also: MAPWORLDTILEY, MAPTILEX, MAPTILEY, MAPTILE, SPRITEX

↑ Back to top

MAPWORLDTILEY

Graphics · since 1.13.0

Converts a WORLD pixel Y coordinate to tile Y coordinate without adding scroll offset. Use this for SPRITEY() and other world/logical coordinates that are already in map space. For screen-relative coordinates like MOUSEY() that need scroll adjustment, use MAPTILEY instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: world position (SPRITEY) → MAPWORLDTILEY, screen input (MOUSEY) → MAPTILEY.

Syntax

MAPWORLDTILEY(worldPixelY [, layerNum])

Parameters

  • worldPixelY - World pixel Y coordinate (e.g., SPRITEY)
  • layerNum - Optional: Layer number to query (defaults to current layer)

Examples

' Get tile row where sprite is located
ty = MAPWORLDTILEY(SPRITEY(0), 1)
PRINT "Sprite at tile row: " + STR$(ty)
' Platform collision detection
feetY = SPRITEY(playerSprite) + 16  ' Bottom of sprite
ty = MAPWORLDTILEY(feetY)
IF MAPTILE(tx, ty) > 0 THEN
  onGround = 1
END IF
' Check if sprite is above certain tile row
IF MAPWORLDTILEY(SPRITEY(0)) < 5 THEN
  PRINT "Sprite is in upper area!"
END IF

See also: MAPWORLDTILEX, MAPTILEX, MAPTILEY, MAPTILE, SPRITEY

↑ Back to top

NEWIMAGE

Graphics · since 0.23.0

Creates an offscreen image buffer with specified dimensions and background color. The buffer can be drawn to using SET IMAGE and displayed with PUTIMAGE. With AS SPRITESHEET, the image is automatically divided into frames for animation. Perfect for compositing complex graphics, preparing sprites, or creating procedural sprite sheets.

Syntax

NEWIMAGE width, height, backgroundColor, imageName$ [AS SPRITESHEET(frameWidth, frameHeight)]

Parameters

  • width - Width in pixels (must be positive; a zero or negative value is a runtime error)
  • height - Height in pixels (must be positive; a zero or negative value is a runtime error)
  • backgroundColor - Palette color index (0-255) to fill the buffer
  • imageName$ - String name to identify the image buffer
  • AS SPRITESHEET(frameWidth, frameHeight) - Optional: Divide image into animation frames
  • Frame numbering: Frames start at 0 (top-left), increment left-to-right, top-to-bottom

Examples

REM Create offscreen buffer for compositing
NEWIMAGE 100, 100, 0, "composite"
SET IMAGE "composite"
CIRCLE (50, 50), 40, 15
LINE (10, 10)-(90, 90), 14
SET LAYER 0
PUTIMAGE (100, 50), "composite"
REM Create procedural sprite sheet with 4 frames (32x32 each)
NEWIMAGE 128, 32, 0, "anim" AS SPRITESHEET(32, 32)
SET IMAGE "anim"
FOR i = 0 TO 3
  CIRCLE (i * 32 + 16, 16), 10 + i * 2, 15, 15
NEXT
SET LAYER 0
PUTIMAGE (100, 100), "anim", frame  ' Use frame 0-3
REM Create sprite atlas
NEWIMAGE 320, 200, 0, "sprites"
SET IMAGE "sprites"
FOR i = 0 TO 9
  CIRCLE (i * 32 + 16, 100), 12, i + 1, i + 1
NEXT
SET LAYER 0
' Now use sprites from atlas
GETIMAGE (0, 88)-(32, 112), "sprite0"
REM Prepare UI panels
NEWIMAGE 200, 100, 8, "panel"
SET IMAGE "panel"
LINE (0, 0)-(199, 99), 15, B
LOCATE 2, 5: PRINT "GAME MENU"
SET LAYER 0
PUTIMAGE (60, 50), "panel", ALPHA(0.8)
REM Multiple buffers for double buffering
NEWIMAGE 320, 200, 0, "buffer1"
NEWIMAGE 320, 200, 0, "buffer2"

See also: SET IMAGE, GETIMAGE, PUTIMAGE, CREATE LAYER, TOSPRITESHEET, FRAMECOUNT

↑ Back to top

OFFSET

Graphics · since 1.0.3

Graphics modifier used with POLYGON to translate (move) all vertices by specified x and y amounts. Applied after SCALE and ROTATE transformations.

Syntax

OFFSET(x, y)

Parameters

  • x - Horizontal offset in pixels (can be negative)
  • y - Vertical offset in pixels (can be negative)

Examples

' Move polygon to position:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, OFFSET(100, 100)
' Define shape at origin, position with OFFSET:
POLYGON ((-25, -25), (25, -25), (25, 25), (-25, 25)), 14, OFFSET(160, 100)
' Combined with rotation:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(angle), OFFSET(centerX, centerY)
' Animation using OFFSET:
FOR x = 0 TO 320 STEP 10
  CLS
  POLYGON ((0, 0), (30, 0), (15, 30)), 15, 12, OFFSET(x, 100)
  DELAY 50
NEXT

See also: POLYGON, ROTATE, SCALE

↑ Back to top

PALETTE

Graphics · since 0.23.0

Modifies color palette with RGB values and transparency. The palette is dynamically sized: any non-negative color_index is valid and the palette grows to fit (there is no 256-entry limit).

Syntax

PALETTE [color_index, rgb_value]
PALETTE [color_index, red, green, blue [, alpha]]

Parameters

  • color_index - Palette color number (0 or greater; palette grows to fit)
  • rgb_value - RGB color value
  • red, green, blue - RGB components (0-255)
  • alpha - Transparency (0-255, optional)

Examples

PALETTE 1, 255, 128, 64      ' Orange color
PSET (100, 100), 1
PALETTE 2, 0, 255, 0, 128    ' Semi-transparent green
PALETTE 300, &HFF112233      ' High index is fine (palette grows)

See also: COLOR, SCREEN, SET PALETTE

↑ Back to top

POINT

Graphics · since 1.35.0

Sample a pixel from a BEGIN MEMORYBUFFER ... FROM source and return its palette index (0-255), or -1 if the pixel is transparent.

Valid only inside a memory-buffer block — calling it elsewhere is a fatal error (reading a texture's pixels outside this context is unsupported). It reads the frozen snapshot of the FROM source captured at BEGIN, never the draws you've made inside the block, so a filter that reads neighbouring pixels and writes results won't read its own half-finished output.

An opaque colour that isn't an exact palette entry resolves to the nearest palette index, so POINT always yields a usable index for opaque pixels. For the exact colour of a non-palette pixel (e.g. from a photo), use POINTRGB. Inside a block with no FROM, the snapshot is transparent everywhere (POINT returns -1). Out-of-range coordinates read as transparent.

Syntax

POINT(x, y)
POINT(coord)   (also accepts a single coordinate value)

Parameters

  • x - X coordinate in the source/buffer (same coordinate space as PSET)
  • y - Y coordinate in the source/buffer

Examples

REM Outline: draw an edge around a rendered glyph
IF ME.ISCLIENT THEN
  TEXTTOIMAGE "PLAY", "label", 48
  BEGIN MEMORYBUFFER "outlined" FROM "label"
    FOR y = 1 TO 46
      FOR x = 1 TO 200
        IF POINT(x, y) = -1 THEN
          IF POINT(x-1, y) <> -1 OR POINT(x+1, y) <> -1 OR POINT(x, y-1) <> -1 OR POINT(x, y+1) <> -1 THEN
            PSET (x, y), 0
          END IF
        END IF
      NEXT
    NEXT
  END MEMORYBUFFER
END IF
REM Recolor: every non-transparent pixel becomes red
BEGIN MEMORYBUFFER "out" FROM "src"
  FOR y = 0 TO 31
    FOR x = 0 TO 31
      IF POINT(x, y) <> -1 THEN PSET (x, y), 4
    NEXT
  NEXT
END MEMORYBUFFER

Notes

Reads the interpreter-side snapshot of the FROM source (linear RGBA, captured once at BEGIN via ReadImageData → PixelsReady — no GPU readback). Transparent (alpha 0) returns -1; otherwise reverse-looks-up the live palette via Palette::nearest_index (first exact RGB match, else nearest by squared RGB distance). Fatal outside a MEMORYBUFFER block; out-of-range and no-FROM blocks read transparent. Coordinates share PSET's space 1:1.

See also: POINTRGB, BEGIN MEMORYBUFFER, PSET, PRESET, PALETTE

↑ Back to top

POINTRGB

Graphics · since 1.35.0

Sample a pixel from a BEGIN MEMORYBUFFER ... FROM source and return its raw packed colour as 0xAARRGGBB (alpha in the high byte), or 0 if transparent.

The companion to POINT: where POINT gives a palette index, POINTRGB gives the exact colour — use it for FROM'd photos and anything that isn't a palette colour. Like POINT, it samples the frozen FROM snapshot (not in-block draws) and is valid only inside a memory-buffer block (fatal error otherwise). Out-of-range coordinates read as 0 (transparent).

Pull individual channels with the bitwise helpers:

c = POINTRGB(x, y)
r = BITAND(SHR(c, 16), 255)
g = BITAND(SHR(c, 8), 255)
b = BITAND(c, 255)
a = BITAND(SHR(c, 24), 255)

Syntax

POINTRGB(x, y)
POINTRGB(coord)   (also accepts a single coordinate value)

Parameters

  • x - X coordinate in the source/buffer (same coordinate space as PSET)
  • y - Y coordinate in the source/buffer

Examples

REM Read the exact colour of a photo pixel
BEGIN MEMORYBUFFER "out" FROM "photo"
  c = POINTRGB(10, 10)
  r = BITAND(SHR(c, 16), 255)
END MEMORYBUFFER
REM Tint: halve the red channel of every opaque pixel
BEGIN MEMORYBUFFER "dim" FROM "photo"
  FOR y = 0 TO 63
    FOR x = 0 TO 63
      c = POINTRGB(x, y)
      IF BITAND(SHR(c, 24), 255) > 0 THEN
        r = BITAND(SHR(c, 16), 255) / 2
        g = BITAND(SHR(c, 8), 255)
        b = BITAND(c, 255)
        PSET (x, y), RGB(r, g, b)
      END IF
    NEXT
  NEXT
END MEMORYBUFFER

Notes

Returns the snapshot pixel's packed 0xAARRGGBB (alpha high byte) as an integer; transparent reads 0. Same frozen-snapshot, block-scoped semantics as POINT — fatal outside a MEMORYBUFFER block, out-of-range reads transparent. Composes with BITAND/SHR for channel extraction (0xRRGGBB packing convention shared with PALETTE and RGB).

See also: POINT, BEGIN MEMORYBUFFER, RGB, BITAND, SHR

↑ Back to top

POLYGON

Graphics · since 0.23.0

Draws polygons with any number of vertices using GPU-accelerated ear-clipping triangulation, so both convex and concave shapes fill correctly. Can draw outlined, filled, or textured polygons. Supports two syntaxes: inline point coordinates or an array of points. Array-based syntax is useful for dynamic polygons or large point sets. Supports OFFSET, ROTATE, SCALE, and SCALETO modifiers for transformation. For shader effects, use SET LAYER SHADER on the target layer. For textured polygons, UV coordinates specify texture mapping for each vertex.

Transforms use the shape's own centre as their origin by default. ROTATE(angle) spins the polygon about the average of its own vertices, and SCALE/SCALETO grow it about the same point. That is right for a shape that stands alone, and wrong for one piece of a composed object: a flame drawn behind a ship, authored around the ship's origin, spins about its own middle and never swings round to the correct side.

Give an explicit origin for that case — ROTATE(angle, 0, 0) rotates about the coordinate system the points were authored in, so several polygons sharing an origin rotate as one object. OFFSET is applied last, after any rotation or scale, so it moves the finished shape into place.

Syntax

POLYGON ((x1, y1) [UV (u1, v1)], (x2, y2) [UV (u2, v2)], ...), color [, fillColor] [, textureName] [, modifiers]
POLYGON arrayName, color [, fillColor] [, textureName] [, UV(uvArray | (u1,v1), (u2,v2), ...)] [, modifiers]
modifiers: ROTATE(angle [, originX, originY])  SCALE(sx, sy)  SCALETO(w, h)  OFFSET(x, y)
(SCALE, SCALETO and OFFSET each accept a coordinate: SCALE(coord), OFFSET(coord))

Parameters

  • (x, y) - Vertex coordinates (at least 3 vertices required)
  • UV (u, v) - Optional UV texture coordinates for each vertex (0.0-1.0, where 0,0 is top-left)
  • arrayName - A MUTATION, or an array of points: coordinate values ((x,y)), objects with .x/.y fields, or an Nx2 numeric array of [x, y] rows
  • color - Outline color (palette index 0-255, or -1 for no outline)
  • fillColor - Optional fill color (palette index 0-255, ignored when texture is specified)
  • textureName - Optional texture name for textured fill (requires UV coordinates on vertices)
  • OFFSET(x, y) - Translate polygon by x, y pixels
  • ROTATE(angle [, originX, originY]) - Rotate polygon by angle degrees (default: around centroid)
  • SCALE(scaleX, scaleY) - Scale polygon dimensions (applied around centroid)
  • SCALETO(width, height) - Scale polygon to fit target dimensions
  • UV(uvArray) - UV modifier: specify UV coordinates from a MUTATION or coordinate array (normalized 0.0-1.0, for array/mutation-based polygons)
  • UV((u1,v1), (u2,v2), ...) - UV modifier: specify inline UV coordinates (normalized 0.0-1.0, where 0,0 is top-left and 1,1 is bottom-right)
  • UV(..., SCALE(sx, sy)) - Scale UV coordinates by factors (applied around UV centroid)
  • UV(..., SCALETO(w, h)) - Scale UV coordinates to fit target dimensions
  • UV(..., ROTATE(angle)) - Rotate UV coordinates by angle degrees (around UV centroid)
  • UV(..., OFFSET(u, v)) - Translate UV coordinates by u, v amounts

Examples

POLYGON ((50, 50), (150, 50), (100, 150)), 15  ' Triangle outline
POLYGON ((50, 50), (150, 50), (100, 150)), 15, 12  ' Filled triangle
POLYGON ((10, 10), (90, 10), (90, 90), (10, 90)), 14, 10  ' Filled rectangle
' Star shape:
POLYGON ((160, 50), (170, 80), (200, 80), (175, 100), (185, 130), (160, 110), (135, 130), (145, 100), (120, 80), (150, 80)), 15
' Array-based polygon:
DIM points(2) AS OBJECT
points(0).x = 50: points(0).y = 50
points(1).x = 150: points(1).y = 50
points(2).x = 100: points(2).y = 150
POLYGON points, 15, 12
' Textured quad with UV mapping:
LOADIMG "beam.png", "beam"
POLYGON ((0, 0) UV (0, 0), (100, 0) UV (1, 0), (100, 20) UV (1, 1), (0, 20) UV (0, 1)), -1, 0, "beam"
' UV modifier with inline coordinates (for array/mutation polygons):
DIM quad AS MUTATION
quad = MUTATION((0, 0), (100, 0), (100, 100), (0, 100))
LOADIMG "tile.png", "tile"
POLYGON quad, -1, 0, "tile" UV((0, 0), (1, 0), (1, 1), (0, 1))
' UV modifier with array variable:
DIM uvCoords(3) AS OBJECT
uvCoords(0).u = 0: uvCoords(0).v = 0
uvCoords(1).u = 1: uvCoords(1).v = 0
uvCoords(2).u = 1: uvCoords(2).v = 1
uvCoords(3).u = 0: uvCoords(3).v = 1
POLYGON quad, -1, 0, "tile" UV(uvCoords)
' Translated polygon:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, OFFSET(100, 100)
' Rotated polygon:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45)
' Rotated around custom point:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45, 25, 25)
' Scaled polygon:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALE(2, 2)
' Combined modifiers:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, 12, SCALE(2, 2), ROTATE(angle), OFFSET(200, 200)

See also: LINE, CIRCLE, BEGIN FRAME, END FRAME, OFFSET, ROTATE, SCALE, SCALETO, SET LAYER, LOADIMG

↑ Back to top

PRINT

Graphics · since 0.23.0

Displays text and values on the screen. Use the WORDS modifier to enable word wrapping, which breaks text at word boundaries instead of mid-word when reaching the edge of the screen. Use RATE to create a typewriter effect with a delay between each character - this is non-blocking, meaning program execution continues while text is being typed.

In multiplayer, PRINT is client-only — calling it from a server-side interpreter (host or player-server) raises a fatal runtime error pointing at the EVENT CLIENTS pattern. Update server-authoritative state, then fire EVENT CLIENTS "RefreshHud" and have a GUARD(CLIENT) handler do the actual PRINT. State is server-authoritative; pixels are client-authoritative.

Syntax

PRINT [WORDS] [RATE(delayMs)] [expression [; | ,] [expression] ...]

Parameters

  • WORDS - Optional modifier for word-wrapped output
  • RATE(delayMs) - Optional typewriter effect with delay in milliseconds between characters
  • expression - Value to display
  • ; - Keep on same line
  • , - Tab to next column

Examples

PRINT "Hello, World!"
PRINT "Name:", name$
PRINT x; y; z
PRINT WORDS "This long text will wrap at word boundaries instead of breaking mid-word"
PRINT RATE(100) "Slow typewriter effect..."
PRINT WORDS RATE(30) "Fast typewriter with word wrap for dialog boxes"
' Wait for typing to complete:
PRINT RATE(50) "Loading..."
WHILE TYPING() = 1
  ' Continue other tasks
WEND

See also: LOCATE, TYPING, WINDOW

↑ Back to top

PSET

Graphics · since 0.23.0

Sets individual pixels at coordinates. For optimal performance when drawing many pixels in loops, use BEGIN FRAME/END FRAME to buffer operations and eliminate flicker.

Syntax

PSET (x, y) [, color]

Parameters

  • x, y - Pixel coordinates
  • color - Color value

Examples

PSET (100, 100), 15
FOR I = 0 TO 319
  PSET (I, 100), 14
NEXT
REM High-performance pixel drawing
BEGIN FRAME
  FOR I = 0 TO 319
    PSET (I, 100), 14
  NEXT
END FRAME

See also: LINE, CIRCLE, BEGIN FRAME

↑ Back to top

REGION

Graphics · since 0.23.0

Graphics modifier used with PUTIMAGE to copy only a specific rectangular region from the source image. Useful for extracting parts of tilesets, creating viewport effects, or implementing custom sprite clipping.

Syntax

REGION(x1, y1, x2, y2)

Parameters

  • x1, y1 - Top-left corner of the source region
  • x2, y2 - Bottom-right corner of the source region

Examples

PUTIMAGE (10, 10), "tileset", REGION(0, 0, 32, 32)  ' Copy 32x32 tile
PUTIMAGE (100, 100), "atlas", REGION(64, 0, 96, 32)  ' Extract specific sprite
PUTIMAGE (0, 0), "room", REGION(20, 20, 400, 400), SCALETO(640, 480)  ' Crop and scale to target size
' Viewport scrolling:
viewX = playerX - 160
viewY = playerY - 120
PUTIMAGE (0, 0), "level", REGION(viewX, viewY, viewX+320, viewY+240)
' Extract multiple tiles from tileset:
FOR row = 0 TO 3
  FOR col = 0 TO 7
    tileX = col * 32
    tileY = row * 32
    PUTIMAGE (col*32, row*32), "tiles", REGION(tileX, tileY, tileX+32, tileY+32)
  NEXT
NEXT

See also: PUTIMAGE, LOADIMG, SCALE, SCALETO, ALPHA

↑ Back to top

RESET LAYERS

Graphics · since 0.23.0

Destroys all custom layers and restores the graphics system to its default state with only layer 0. Useful for cleaning up complex multi-layer scenes or resetting the graphics environment. All layer data is cleared and layer 0 is recreated with default screen dimensions.

Syntax

RESET LAYERS

Examples

CREATE LAYER 1, 640, 200  ' Create custom layers
CREATE LAYER 2, 320, 200
RESET LAYERS  ' Clean up - back to layer 0 only
REM Reset graphics between game scenes
RESET LAYERS
SCREEN 13
CLS

See also: CREATE LAYER, CREATE MAP, SET LAYER, CLS

↑ Back to top

ROTATE

Graphics · since 0.23.0

Graphics modifier used with PUTIMAGE and POLYGON to rotate by a specified angle in degrees. Supports custom rotation origin point for precise control over the rotation center.

Syntax

ROTATE(angle [, originX, originY])

Parameters

  • angle - Rotation angle in degrees (0-360)
  • originX - Optional X origin for rotation. For PUTIMAGE: normalized 0-1 value. For POLYGON: pixel coordinates
  • originY - Optional Y origin for rotation. For PUTIMAGE: normalized 0-1 value. For POLYGON: pixel coordinates
  • For PUTIMAGE: (0, 0) = top-left, (0.5, 0.5) = center (default when omitted), (1, 1) = bottom-right
  • For POLYGON: rotation occurs around centroid by default, or custom pixel coordinates

Examples

PUTIMAGE (100, 100), "ship", ROTATE(45)  ' Rotate 45 degrees around center (default)
PUTIMAGE (x, y), "ship", ROTATE(45, 0.5, 0.5)  ' Rotate 45 degrees around center (explicit)
PUTIMAGE (200, 150), "wheel", 0, ROTATE(rotation)  ' Animated rotation with frame 0
' Spinning effect:
angle = (angle + 5) MOD 360
PUTIMAGE (centerX, centerY), "spinner", ROTATE(angle, 0.5, 0.5)
' Combined with other modifiers:
PUTIMAGE (x, y), "sprite", FLIPX, ROTATE(45, 0.5, 0.5)  ' Flip and rotate
PUTIMAGE (x, y), "player", ALPHA(0.8), SCALE(2, 2), ROTATE(angle, 0.5, 0.5)  ' Transparent, scaled, and rotated
' Polygon rotation:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45)  ' Rotate around centroid
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45, 25, 25)  ' Rotate around custom point

See also: PUTIMAGE, POLYGON, LOADIMG, ALPHA, SCALE, FLIPX, FLIPY, OFFSET

↑ Back to top

SCALE

Graphics · since 0.23.0

Graphics modifier used with PUTIMAGE, SET SPRITE, and POLYGON. Uniformly applies scale factors where 1.0 = original size, 0.5 = half size, 2.0 = double size. For POLYGON, scales vertices around the centroid. Use SCALETO for scaling to specific pixel dimensions.

Syntax

SCALE(scaleX, scaleY)

Parameters

  • scaleX - Horizontal scale factor (1.0 = original, 2.0 = double, 0.5 = half)
  • scaleY - Vertical scale factor (1.0 = original, 2.0 = double, 0.5 = half)

Examples

PUTIMAGE (10, 10), "icon", SCALE(2, 2)  ' Draw at double size
PUTIMAGE (100, 100), "player", 0, SCALE(0.5, 0.5)  ' Draw at half size
PUTIMAGE (50, 50), "enemy", ALPHA(0.7), SCALE(3, 3)  ' Transparent and 3x scaled
SET SPRITE 0, x, y, "player", SCALE(2, 2)  ' Double-size sprite
' Polygon scaling:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALE(2, 2)  ' Double size
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALE(0.5, 1)  ' Squash horizontally

See also: SCALETO, PUTIMAGE, POLYGON, SET SPRITE, LOADIMG, ALPHA, REGION, OFFSET, ROTATE

↑ Back to top

SCALETO

Graphics · since 0.30.0

Graphics modifier used with PUTIMAGE, SET SPRITE, and POLYGON. Scales to specific target pixel dimensions. For POLYGON, computes the bounding box and derives scale factors to fit the shape within the target dimensions. Use SCALE for factor-based scaling.

Syntax

SCALETO(width, height)

Parameters

  • width - Target width in pixels
  • height - Target height in pixels

Examples

PUTIMAGE (10, 10), "icon", SCALETO(64, 64)  ' Scale to exactly 64x64 pixels
PUTIMAGE (100, 100), "player", 0, SCALETO(128, 128)  ' Scale sprite frame to 128x128
PUTIMAGE (50, 50), "enemy", ALPHA(0.7), SCALETO(48, 48)  ' Transparent and scaled to 48x48
SET SPRITE 0, x, y, "player", SCALETO(64, 64)  ' Sprite scaled to 64x64
' Dynamic size effect:
FOR size = 16 TO 64 STEP 8
  PUTIMAGE (100, 100), "powerup", SCALETO(size, size)
NEXT
' Polygon scaled to target size:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALETO(100, 100)  ' Fit into 100x100

See also: SCALE, PUTIMAGE, POLYGON, SET SPRITE, LOADIMG, ALPHA, REGION, ROTATE

↑ Back to top

SCREEN

Graphics · since 0.23.0

Set the video display mode, a custom resolution, or hand control to the platform's native size. This is usually the first graphical statement in a program — it's what opens the window and gives LINE / PSET / PRINT / sprites somewhere to draw.

Classic screen modes

Pass a single integer for the QBasic-style preset:

Mode Resolution Notes
0 text character grid, not pixel-based
1 CGA classic 320×200 4-color
7 EGA 320×200 16-color
12 VGA 640×480
13 VGA 320×200 (the famous 256-color mode)

Custom resolutions

Pass two integers for an arbitrary pixel size (1–4096 per axis):

SCREEN 320, 240    ' retro
SCREEN 1920, 1080  ' Full HD

Add two more for explicit scale factors (0.1–10, default 1). Pixel-perfect upscaling for retro looks is exactly this:

SCREEN 160, 120, 2, 2  ' 160×120 art, doubled to 320×240 on-screen

`SCREEN NATIVE`

SCREEN NATIVE
SCREEN NATIVE, "My Game"

Use the platform's actual screen size. Especially useful on mobile, where you don't know the resolution ahead of time and SCREENWIDTH / SCREENHEIGHT are how you find out after the call.

Idempotent on repeat

Calling SCREEN with the same logical dimensions as the current screen is a no-op for layer state: the window itself updates (title, scale, fullscreen) and the viewport is recomputed, but existing layers, tilemaps, animations, and shader transitions all survive. This protects multiplayer / remote-client setups where the local CrashPlayer runs SCREEN concurrently with server-pushed CreateLayer / CreateTilemap frames — pre-1.29.0 the local SCREEN could race-nuke a map the server had already painted.

A SCREEN call with different dimensions still resets every layer (the prior render targets are the wrong size), same as before.

If you want "clear the screen, start fresh" mid-program, use BEGIN SCENE / END SCENE for transactional layer state, or CLS + DESTROY LAYER for explicit teardown. Don't lean on a second SCREEN to do it.

Syntax

SCREEN mode
SCREEN width, height
SCREEN width, height, scaleX, scaleY
SCREEN NATIVE [, "title"]

Parameters

  • Classic Screen Modes:
  • mode - Screen mode (0=text, 1=CGA, 7=EGA, 12=VGA 640x480, 13=VGA 320x200)
  • Custom Resolution:
  • width - Screen width in pixels (1-4096)
  • height - Screen height in pixels (1-4096)
  • scaleX - Horizontal scale factor (0.1-10 recommended; the value is passed through to the renderer and is not clamped; default: 1)
  • scaleY - Vertical scale factor (0.1-10 recommended; the value is passed through to the renderer and is not clamped; default: 1)
  • Native Mode:
  • NATIVE - Use the platform's native resolution (mobile-friendly)
  • title - Optional window title (string)

Examples

SCREEN 13           ' Classic VGA 320x200
SCREEN 12           ' Classic VGA 640x480
SCREEN 0            ' Text mode
SCREEN 800, 600     ' Custom 800x600 resolution
SCREEN 320, 240     ' Custom 320x240 resolution
SCREEN 160, 120, 2, 2  ' 160x120 scaled 2x (pixel-perfect)
SCREEN 640, 480, 0.5, 0.5  ' 640x480 scaled down 50%
SCREEN 1920, 1080   ' Full HD resolution
SCREEN NATIVE       ' Use native resolution (great for mobile)
SCREEN NATIVE, "My Game"  ' Native resolution with custom title
' Adaptive layout using native resolution
SCREEN NATIVE
PRINT "Screen size:"; SCREENWIDTH; "x"; SCREENHEIGHT

See also: CLS, PSET, SCREENWIDTH, SCREENHEIGHT

↑ Back to top

SCREEN TEXT

Graphics · since 0.23.0

Creates a text-only screen mode optimized for character display. Uses 8x8 character cells and supports both ASCII and modern Unicode characters including emojis.

Syntax

SCREEN TEXT columns, rows [, scaleX, scaleY] [WITH ASCII | WITH MODERN]

Parameters

  • columns - Number of character columns (1-200)
  • rows - Number of character rows (1-100)
  • scaleX - Horizontal scale factor (1-10, default: 1)
  • scaleY - Vertical scale factor (1-10, default: 1)
  • ASCII - Use classic ASCII character set (default)
  • MODERN - Enable modern Unicode characters and emojis

Examples

SCREEN TEXT 80, 25                    'Classic 80x25 text mode
SCREEN TEXT 40, 20, 2, 2 WITH ASCII   '40x20 with 2x scaling
SCREEN TEXT 60, 30 WITH MODERN        '60x30 with emoji support
PRINT "Hello World!"                  'Text displays normally
PRINT "👾 🎮 CrashBasic! 🚀"           'Emojis work in MODERN mode

See also: SCREEN, PRINT, LOCATE, COLOR

↑ Back to top

SCREENHEIGHT

Graphics · since 1.7.0

Returns the current screen height in pixels. Useful for adaptive layouts, especially when using SCREEN NATIVE on mobile devices.

Syntax

SCREENHEIGHT

Examples

SCREEN 320, 200
PRINT SCREENHEIGHT  ' Prints 200
SCREEN NATIVE
PRINT "Height:"; SCREENHEIGHT
' Position something at bottom of screen
y = SCREENHEIGHT - objectHeight - 10

See also: SCREENWIDTH, SCREEN

↑ Back to top

SCREENWIDTH

Graphics · since 1.7.0

Returns the current screen width in pixels. Useful for adaptive layouts, especially when using SCREEN NATIVE on mobile devices.

Syntax

SCREENWIDTH

Examples

SCREEN 320, 200
PRINT SCREENWIDTH  ' Prints 320
SCREEN NATIVE
PRINT "Width:"; SCREENWIDTH
' Center something on screen
x = (SCREENWIDTH - objectWidth) / 2

See also: SCREENHEIGHT, SCREEN

↑ Back to top

SET CURSOR

Graphics · since 0.23.0

Sets a custom mouse cursor using a previously loaded image, or restores the default cursor. The image should be loaded first using LOADIMG. Browser limits cursor size to 128x128 pixels.

Syntax

SET CURSOR image_name [, hotspot_x, hotspot_y] | SET CURSOR OFF

Parameters

  • image_name - Name of the loaded image to use as cursor
  • hotspot_x - Optional X coordinate of cursor hotspot (default: center)
  • hotspot_y - Optional Y coordinate of cursor hotspot (default: center)
  • OFF - Keyword to restore the default cursor

Examples

LOADIMG "pointer.png", "customPointer"
SET CURSOR "customPointer"  ' Use custom cursor with center hotspot
SET CURSOR "customPointer", 0, 0  ' Hotspot at top-left corner
SET CURSOR OFF  ' Restore default cursor

See also: LOADIMG, PUTIMAGE

↑ Back to top

SET IMAGE

Graphics · since 0.23.0

Switches the drawing target to a named image buffer created with NEWIMAGE or captured with GETIMAGE. All graphics commands (PSET, LINE, CIRCLE, etc.) will draw to the image instead of the screen until SET LAYER is used to return to layer drawing.

Syntax

SET IMAGE imageName$

Parameters

  • imageName$ - Name of image buffer to draw to (must exist)

Examples

REM Draw to offscreen buffer
NEWIMAGE 100, 100, 0, "buffer"
SET IMAGE "buffer"
CIRCLE (50, 50), 40, 15
LINE (10, 10)-(90, 90), 14

REM Return to screen and display buffer
SET LAYER 0
PUTIMAGE (100, 50), "buffer"
REM Prepare multiple sprites
NEWIMAGE 64, 64, 0, "player"
SET IMAGE "player"
CIRCLE (32, 32), 28, 14, 14
CIRCLE (24, 24), 4, 15, 15
CIRCLE (40, 24), 4, 15, 15

NEWIMAGE 64, 64, 0, "enemy"
SET IMAGE "enemy"
CIRCLE (32, 32), 28, 4, 4

SET LAYER 0
PUTIMAGE (50, 50), "player"
PUTIMAGE (200, 50), "enemy"
REM Complex compositing
CREATE LAYER 0, 320, 200
NEWIMAGE 320, 200, 0, "effect"

REM Draw base to layer
SET LAYER 0
CIRCLE (160, 100), 80, 12, 12

REM Draw effect to buffer
SET IMAGE "effect"
FOR r = 10 TO 100 STEP 5
  CIRCLE (160, 100), r, 14
NEXT

REM Composite effect over base
SET LAYER 0
PUTIMAGE (0, 0), "effect", ALPHA(0.5)
REM Switch between buffers
WHILE running
  SET IMAGE "backbuffer"
  CLS
  ' Draw game state to backbuffer
  CIRCLE (playerX, playerY), 10, 15
  
  SET LAYER 0
  PUTIMAGE (0, 0), "backbuffer"
WEND

See also: NEWIMAGE, SET LAYER, GETIMAGE, PUTIMAGE

↑ Back to top

SET LAYER

Graphics · since 0.23.0

Sets the active layer for subsequent graphics operations. All drawing commands (PSET, LINE, CIRCLE, etc.) will target the specified layer until changed. Optional ALPHA parameter sets the transparency of the entire layer (0.0=fully transparent, 1.0=fully opaque). Optional WRAPX/WRAPY parameters enable infinite scrolling (wrap-around) on the horizontal/vertical axis respectively - when enabled, content that scrolls off one edge reappears on the opposite edge. Optional SHADER parameter applies a post-processing shader to the entire layer for effects like CRT filters, vignettes, color grading, etc. NOTE: Use BEGIN FRAME/END FRAME for layer composition to work properly.

In multiplayer SET LAYER is dual-form: the bare SET LAYER n form (just switching draw target) is render-only and fatal-errors on a server-side interpreter — drive client-side draw via EVENT CLIENTS "RefreshHud" instead. Forms that carry layer attributes — SET LAYER n WRAPX/WRAPY, … ALPHA(v), … SHADER "x" — are layer configuration that runs everywhere; the SHADER form additionally broadcasts so all clients pick up the same post-processing.

Plane layers — AFFINE / PERSPECTIVE / FLAT

Reproject a layer (a drawn surface, a loaded image, or a tilemap) as a rotated/scaled or Mode 7-style receding plane. The camera is driven with SET SCROLL x, y ANGLE(deg) — x, y is the camera's world position and ANGLE is its heading in degrees (consistent with SIN/COS).

  • AFFINE [ZOOM(z)] — flat rotate/scale of the whole layer about the screen centre, no perspective. ZOOM (default 1.0): > 1 zooms out, < 1 zooms in. Ideal for spinning/scaling a tilemap or layer in pure 2D.
  • PERSPECTIVE [HORIZON(y)] [HEIGHT(h)] — a receding floor. HORIZON is the horizon line's screen-Y in pixels; everything above it is transparent. HEIGHT is the camera's height above the plane (larger = floor seen from higher up).
  • FLAT — turn plane mode off; the layer renders as normal 2D again.

Clipping a layer to a sub-region — CLIP

By default a layer (and a map layer in particular) fills the whole screen. CLIP (x1,y1)-(x2,y2) restricts that layer's rendering to a screen rectangle (a scissor rect), so a tilemap/map can occupy a sub-region and leave HUD/menu areas uncovered — compartmentalizing the screen. Coordinates are logical screen pixels; the rect is normalized (corners ordered, clamped to the screen) and mapped through any letterbox. The clip applies to the layer's final output, so it composes correctly with a layer SHADER and with AFFINE/PERSPECTIVE plane modes (the shaded/projected floor is clipped, not an intermediate). Sprites assigned to the layer clip with it. CLIP NONE removes the clip (full screen again). The clip persists across frames until changed — set it once at setup. CLIP does NOT apply to video playback (PLAYVIDEO): video is decoded through each platform's native pipeline and composites outside the layer system on some platforms, so a clip rectangle will not mask a playing video.

WRAPX/WRAPY make the plane tile infinitely toward the horizon. Sprites assigned to a plane layer are projected onto it (scaled by depth, painter-sorted far→near, culled at the camera/horizon). User shaders compose with plane layers: on a plain (drawn/image) plane layer the shader runs in map space and the result is then projected (shade-then-project); on a tilemap plane layer the shader runs in screen space on the projected floor (project-then-shade). SET LAYER ALPHA composes with a shader on any layer — layer alpha can fade a shadered layer all the way to transparent.

Syntax

SET LAYER layer_id [ALPHA(alpha)] [WRAPX [ON|OFF]] [WRAPY [ON|OFF]] [CLIP (x1,y1)-(x2,y2) | CLIP NONE] [AFFINE [ZOOM(z)] | PERSPECTIVE [HORIZON(y)] [HEIGHT(h)] | FLAT] [SHADER "name" [, @param=value, ...] | SHADER NONE]

Parameters

  • layer_id - Layer number to make active (0-99). Layers 100+ are reserved for internal use.
  • ALPHA(alpha) - Optional layer transparency (0.0-1.0). 0.0=invisible, 1.0=fully opaque
  • WRAPX [ON|OFF] - Enable or disable horizontal wrap-around scrolling. WRAPX alone enables it, WRAPX ON explicitly enables, WRAPX OFF disables
  • WRAPY [ON|OFF] - Enable or disable vertical wrap-around scrolling. WRAPY alone enables it, WRAPY ON explicitly enables, WRAPY OFF disables
  • AFFINE [ZOOM(z)] - Reproject the layer as a flat rotated/scaled plane about the screen centre. ZOOM (default 1.0): >1 zooms out, <1 zooms in. Camera driven by SET SCROLL ... ANGLE.
  • PERSPECTIVE [HORIZON(y)] [HEIGHT(h)] - Reproject the layer as a Mode 7-style receding floor. HORIZON = horizon line's screen-Y in pixels (above it is transparent); HEIGHT = camera height above the plane.
  • FLAT - Disable plane mode; the layer renders as normal 2D.
  • CLIP (x1,y1)-(x2,y2) - Restrict the layer's rendering to this screen rectangle (a scissor rect), so a map layer fills a sub-region and leaves HUD/menu areas uncovered. Composes with the layer shader and plane modes; sprites on the layer clip too. Does NOT affect video (PLAYVIDEO), which renders via the platform's native pipeline. CLIP NONE removes it (full screen).
  • SHADER "name" - Apply a shader to the entire layer (must be defined with SHADER block)
  • @param=value - Optional shader parameter overrides
  • SHADER NONE - Remove any shader from the layer

Examples

SET LAYER 0   ' Draw to main layer
CIRCLE (160, 100), 50, 15
SET LAYER 1   ' Switch to background layer
LINE (0, 0)-(320, 200), 12
SET LAYER 2 ALPHA(0.5)  ' Set UI layer to 50% transparent
SET LAYER 1 ALPHA(1.0)  ' Set sprites fully opaque
SET LAYER 0 ALPHA(0.0)  ' Make background invisible
SET LAYER 0 SHADER "CRT"  ' Apply CRT effect to main layer
SET LAYER 1 SHADER "Vignette", @intensity=0.8  ' Vignette with custom intensity
SET LAYER 0 SHADER NONE  ' Remove shader from layer
SET LAYER 1 WRAPX  ' Enable horizontal infinite scrolling
SET LAYER 1 WRAPX WRAPY  ' Enable both horizontal and vertical wrapping
' Compartmentalize a map: it fills the left region, the right is a HUD area
CREATE MAP FROM map TILESET "tiles" TILESIZE 32 AS LAYER 0
SET LAYER 0 CLIP (0,0)-(212,240)  ' map only renders in the left 212px
SET LAYER 0 CLIP NONE             ' later: back to full screen
SET LAYER 2 WRAPX ON WRAPY OFF  ' Explicit wrap control
SET LAYER 0 ALPHA(0.8) WRAPX  ' Combine transparency with wrapping
REM Mode 7 receding floor (drive the camera with SET SCROLL ... ANGLE)
SET LAYER 0 PERSPECTIVE HORIZON(110) HEIGHT(90) WRAPX WRAPY
SET SCROLL 256, cameraY ANGLE(heading)
SET LAYER 0 AFFINE ZOOM(1.0)  ' Flat rotate/scale plane (pure 2D)
SET LAYER 0 FLAT  ' Turn plane mode off — back to normal 2D

See also: CREATE LAYER, CREATE MAP, SET SCROLL, ALPHA, SHADER, SET SPRITE

↑ Back to top

SET PALETTE

Graphics · since 1.39.0

Switches between palette (indexed) and direct-color modes. ON (the default) means color values passed to drawing commands are palette indices. OFF (direct-color mode) means a color value is a literal 0xAARRGGBB color (alpha, red, green, blue), so colors can be specified directly without defining palette entries. In direct mode, transparency is expressed with alpha = 0 (the -1 transparent sentinel applies only in palette mode). Hex literals (&H...) pair naturally with this mode. The mode applies to all color arguments (PSET, LINE, CIRCLE, POLYGON, COLOR, text, fills, etc.).

Syntax

SET PALETTE ON | OFF

Parameters

  • ON - Color values are palette indices (default)
  • OFF - Color values are literal 0xAARRGGBB colors (direct-color mode)

Examples

SET PALETTE OFF
COLOR &HFFFF8800            ' opaque orange (A=FF R=FF G=88 B=00)
PSET (10, 10), &H8000FF00  ' 50% green
PSET (20, 20), &H00000000  ' transparent (alpha 0) in direct mode
SET PALETTE ON             ' back to palette indices

See also: PALETTE, COLOR

↑ Back to top

SET POLYGON

Graphics · since 1.43.0

Creates a retained, mutation-bound polygon — a polygon whose vertices are a MUTATION. Unlike POLYGON, which rasterizes into a layer every time you call it, a bound polygon lives above its assigned layer in the sprite z-order and is re-composited on the GPU every frame from the bound mutation's current coordinates. Because the geometry rides the same per-frame update path sprites use, a behavior-driven (deforming) polygon costs about a sprite draw per frame instead of a re-rasterize, and it never dirties the layer beneath it.

The id is a 0-based polygon handle in its own space, independent of sprite IDs. BIND mutationVar names the MUTATION whose points are the polygon's vertices — apply a behavior to that mutation with SET MUTATION ... BEHAVIOR and the polygon deforms automatically.

Modifiers: FILL sets the solid fill colour (palette index); COLOR sets an outline colour; TEXTURE names a loaded image for a textured polygon; UV binds the per-vertex texture coordinates (0..1) to a second MUTATION so the texture animates — apply a behavior to that UV mutation and the texture warps/scrolls on the polygon while its shape can move independently; LAYER chooses the layer it composites above; ZORDER sets draw order within that layer (shared with sprites). Without UV, a textured polygon auto-maps the image across its bounding box. Fills are correct for concave shapes (ear-clipping). Remove a bound polygon with DESTROY POLYGON id.

Multiplayer: bound polygons are client-only. The polygon's mutation and its behavior tick on each client's own engine and render there, so a polygon is drawn independently on every client with no per-frame network traffic. Calling SET POLYGON (or DESTROY POLYGON) from server-side code in a CrashNet game is a fatal error — wrap it in IF ME.ISCLIENT THEN ... END IF. To share a polygon's shape across clients, replicate the data that drives its mutation (for example via PLAYER DATA / HOST DATA) and let each client draw it locally.

Pooling. The mutation may be an array slot — rocks(i) — and the index is an expression, so one loop can create and drive a whole field of shapes. Without it every shape needed its own literally-named variable, which is why bound polygons were only ever used one at a time.

Collision. A bound polygon is geometry and does not collide until asked. SOLID gives it a collider that tracks the shape's live extent, which makes every existing collision facility work on it unchanged — SOLID resolution, ONCOLLISION/AFTERCOLLISION, FOR COLLISIONS, tags, boundaries, rooms and layers. GHOST removes the collider again, leaving the geometry.

SOLID on its own collides with boundaries. Sprite-to-sprite resolution is opt-in by tag, exactly as it is for a sprite: SOLID SPRITES("ship", "bullet") names what this shape treats as an obstacle, and without that list the shape and any sprite pass through each other. Collision reporting — ONCOLLISION, FOR COLLISIONS, tag queries — does not need the list.

The shape defaults to CIRCLE, inscribed in the extent. A circle suits the round-ish silhouettes vector games are built from, costs one distance check, and — unlike a box — does not breathe as the shape rotates: a spinning rock's AABB swells by up to 41% and back every revolution, which plays as a hitbox that pulses. BOX opts into the axis-aligned box a sprite uses. POLY tests the shape's actual outline — every edge, concave shapes included — which is what an irregular silhouette needs: a circle inscribed in the extent misses everything in the shape's bulges, and a box catches its empty corners. It costs a handful of segment tests, and only on pairs whose bounding boxes already overlap, so the shapes that were never going to touch cost nothing.

TAG names the collider for queries and handlers, exactly as on a sprite. BBOX takes the same relative 0-1 quad or Range a sprite takes, but measured against the shape's current extent and resolved every tick — because that extent moves, and a box baked to pixels once would be left behind.

An AABB overlap always gates the shape test, so a circle costs nothing on pairs that were never going to touch. Resolution still separates on an axis whatever the shape.

A bound polygon is world state, so BEGIN SCENE covers it: a shape moved inside a scene is restored at END SCENE, and a polygon created inside one is destroyed with it. Re-issuing SET POLYGON on a slot that predates the scene is a restyle, not a creation, and leaves the polygon standing.

A bound polygon is a sprite too

The slot a polygon occupies is a sprite slot. Its geometry comes from the bound MUTATION; everything else about it — where it is, whether it is visible, how solid it is — is ordinary sprite state.

Two ways to reach that state, and the difference matters. SET POLYGON id, ... takes the polygon's own 0-based handle and is what you write in ordinary code. SET SPRITE takes a raw sprite id, and a polygon's half does not live at the handle you gave it — so SET SPRITE 3 is sprite 3, not polygon 3. Where you have the SLOT itself, SET SPRITE works on it like any other sprite; a collision handler is where that happens, because index_a is that slot.

You will most often reach it from a collision handler, where index_a is that slot:

BEHAVIOR "asteroid"
    BVELOCITY @vx = 0, @vy = 0
    ONCOLLISION
        IF tag_b$ = "edge" THEN
            ' Move the sprite and the shape goes with it.
            SET SPRITE index_a LOCATION(1, SPRITEY(index_a))
        END IF
    END ONCOLLISION
END BEHAVIOR
What you write What it does to a bound polygon
LOCATION(x, y), MOVE SPRITE moves the SHAPE — its points travel with it, and the behaviour driving it carries on undisturbed
SCALE, ROTATE, FLIPX, FLIPY poses the shape: the transform is written into its points, so what is drawn, what collides and what the MUTATION reads back are the same thing
ORIGIN(u, v) the TRANSLATIONAL anchor — the point LOCATION places. The pivot a pose turns about is separate, and is named in the rotation call: ROTATE(deg, ox, oy)
ALPHA, LAYER, ZORDER, SHOW/HIDE SPRITE exactly what they do to any sprite — given the SLOT. From ordinary code use SET POLYGON id, ALPHA(a) / SHOW / HIDE, which take the handle
SOLID, GHOST, TAG, BBOX, SPRITES(...) ordinary collider settings, measured against the shape's live extent
DISABLE SPRITE destroys the polygon — geometry, binding and collider together

Writing the shape's points by hand is the thing to avoid here. Moving the shape — SET POLYGON id, LOCATION(x, y), or SET SPRITE index_a LOCATION(...) from a handler — lets the engine keep the shape, its collider and its behaviour in step. Writing the points instead ends the behaviour: the shape it was attached to is no longer the shape you asserted, so it is removed rather than left running against geometry it no longer describes, and you would have to re-apply it.

A shape's behaviour handlers fire on that slot. ONCOLLISION and AFTERCOLLISION inside a behaviour applied with SET MUTATION ... BEHAVIOR run when the polygon collides, because a collision is looked up from the sprite that collided and this is that sprite.

Modifier-only form. SET POLYGON id, ... with no BIND updates a shape that is already bound, exactly as SET SPRITE n, ... updates an existing sprite. Every modifier works in either form — appearance, pose, motion and the collider set alike, so a shape bound once at setup can be made SOLID, re-TAGged or GHOSTed later in response to what is happening. A modifier you leave out keeps its current value, so SET POLYGON 0, LAYER 1 moves the shape and keeps its colour and texture. BIND is the other way round — it rebuilds the polygon, so a modifier you leave out of a re-bind goes back to its default. The drawing modifiers (COLOR, FILL, TEXTURE, LAYER, ZORDER) need something to draw, so using one on an id that was never bound is an error; the pose, motion and collider modifiers work on an unbound id.

Address a polygon by its handle, with SET POLYGON. SET SPRITE 3, SHOW SPRITE 3 and HIDE SPRITE 3 address sprite 3 — they do not reach polygon 3, and they do not report an error about it. Use SET POLYGON 3, SHOW / HIDE and the modifiers above. (The sprite statements do work on the SLOT, which is what a collision handler's index_a holds — see above.)

Pose lives in the points. A shape has no quad for the renderer to transform, so ROTATE / SCALE / SCALETO / FLIPX / FLIPY are baked into the bound mutation's coordinates — which means a program reading that mutation back sees the posed shape, and the collider measures the posed extent. Each pose is computed from the shape as authored, not from the last pose, so scaling out and back returns the coordinates you wrote.

ORIGIN is the TRANSLATIONAL anchor, in relative 0-1 coordinates across the shape's extent: the point LOCATION places, a parent link measures from, and the collider rect subtracts. The point a shape TURNS about is a DIFFERENT anchor, named inside the rotation call because that is the only context a pivot means anything — ROTATE(deg, ox, oy) for a pose, BROTATE ... ORIGIN(u, v) for a behavior. Omit it and the pivot is (0, 0), the top-left of the shape as authored. Keeping them apart is what lets a shape turn about one point and be placed by another. The default ORIGIN(0, 0) places a shape by the top-left of its extent; ORIGIN(0.5, 0.5) places it by its centre. Give it before LOCATION in the same statement — it is what LOCATION means.

Motion comes from the sprite half. BEHAVIOR, PATTERN and PARENT move and turn the shape by writing its points, and MAXVELOCITY / MAXACCELERATION cap that motion the same way they cap a sprite's. Reading the bound mutation while a behavior drives it is free; WRITING it ends the behavior and hands control back to the program (see MUTATION), which is why a shape that must keep its behavior is positioned with LOCATION rather than by authoring points.

Syntax

SET POLYGON id BIND mutationVar | mutationArray(index) [modifiers]
SET POLYGON id [modifiers]
(appearance) TEXTURE name | UV uvMutation | FILL fillColor | COLOR outlineColor |
             SHADER "name" | LAYER n | ZORDER z | ALPHA(a) | SHOW | HIDE
(pose)       LOCATION(x, y) | ORIGIN(ox, oy) | ROTATE(angle [, ox, oy]) | SCALE(sx, sy) |
             SCALETO(w, h) | FLIPX | FLIPY
(motion)     BEHAVIOR "name" [, @param=value ...] | PATTERN "name" | PARENT(n) | PARENT POLYGON(n) |
             MAXVELOCITY(x, y) | MAXACCELERATION(x, y) | RESET
(collision)  SOLID [CIRCLE|BOX|POLY] [SPRITES("tag", ...)] | GHOST | TAG "name" |
             BBOX(x1,y1,x2,y2) | BBOX range
(network)    SYNC [("room")] | NOSYNC

Parameters

  • id - 0-based polygon handle (its own space, not a sprite ID)
  • mutationVar - name of the MUTATION holding the polygon's vertices
  • uvMutationVar - name of a MUTATION whose coords are the per-vertex UVs (0..1); animates the texture when a behavior drives it (UV modifier)
  • fillColor - solid fill palette index (FILL modifier)
  • outlineColor - outline palette index (COLOR modifier)
  • name - texture image name for a textured polygon (TEXTURE modifier)
  • n - layer to composite above (LAYER modifier)
  • z - z-order within the layer (ZORDER modifier)
  • SPRITES("tag", ...) - Optional after SOLID. Tags this collider treats as solid obstacles. Omit it and the shape resolves against boundaries only.
  • a - ALPHA opacity 0-1, applied to fill and outline alike
  • x, y - LOCATION target for the shape's anchor (accepts a Coordinate)
  • ORIGIN(ox, oy) - the TRANSLATIONAL anchor, relative 0-1 across the shape's extent: the point the shape is PLACED by (what LOCATION positions, what a parent link measures from, and what the collider rect subtracts). It is NOT the point the shape turns about — that is a separate anchor, named inside the rotation call itself as ROTATE(deg, ox, oy) or BROTATE ... ORIGIN(u, v). Two anchors, two jobs; a shape may want them at different points.
  • angle - ROTATE angle in degrees, turned about the shape's ROTATIONAL pivot — the optional ox, oy of this same ROTATE, NOT the translational ORIGIN. Omitted, the pivot is (0, 0): the top-left of the shape as authored.
  • sx, sy - SCALE factors (accepts a Coordinate); SCALETO takes a target size in pixels instead
  • "name" - BEHAVIOR or PATTERN to attach to the shape's sprite half
  • PARENT(n) [RIGID] - the sprite the shape rides. A shape has no draw-time transform stage, so the parent's motion is folded into its points once per tick. Without RIGID the link is translation-only: the child follows the parent's position and keeps its own rotation. With RIGID the parent's ROTATION comes too — the shape orbits the parent and inherits its spin, which is what makes it a bone rather than a tagalong.
  • PARENT POLYGON(polygonHandle) [RIGID] - Parent a BOUND POLYGON, named by its own 0-based handle rather than by a raw sprite id. A polygon's slot IS a sprite, so the link behaves exactly as PARENT(spriteNum) does — but the two number spaces are different and neither can be guessed from the value: PARENT(0) is sprite 0, PARENT POLYGON(0) is polygon 0. This is the only way to name another shape's slot from ordinary code; inside a collision handler index_a already IS the slot. RIGID and NONE work the same as for a sprite parent.
  • MAXVELOCITY / MAXACCELERATION - motion ceilings, same meaning as on a sprite (accept a Coordinate)
  • RESET - clear the shape's motion and detach its behaviors
  • SYNC / NOSYNC - replication control; SYNC("room") scopes it to one room

Examples

REM A deforming blob: wobble a mutation, bind a polygon to it
DIM blob AS MUTATION
blob = MUTATION((160, 70), (215, 120), (190, 190), (130, 190), (105, 120))
BEHAVIOR "Wobble"
    BOSCILLATE AMPLITUDE 14, 14 SPEED 3 PHASE 0 VARIANCE(0, 6.28)
END BEHAVIOR
SET MUTATION blob BEHAVIOR "Wobble"
SET POLYGON 0 BIND blob FILL 4 COLOR 15
REM Sprite-controlled placement: the shape keeps its behavior
DIM flame AS MUTATION
flame = MUTATION((-4, -4.5), (0, 4.5), (4, -4.5))
BEHAVIOR "flicker"
    BALPHA 1.0 TO 0.35 DURATION 8 LOOP
END BEHAVIOR
SET POLYGON 1 BIND flame COLOR 4 FILL 12
SET MUTATION flame BEHAVIOR "flicker"
REM Never write the points: that would end the flicker. Move it instead.
SET POLYGON 1, ORIGIN(0.5, 0.5) LOCATION(shipX, shipY) ROTATE(heading)

See also: DESTROY POLYGON, POLYGON, MUTATION, SET MUTATION, SET SPRITE

↑ Back to top

SET SCROLL

Graphics · since 0.23.0

Sets the scroll offset for the current layer. Enables camera movement and parallax scrolling effects for advanced 2D graphics. IMPORTANT: Requires BEGIN FRAME/END FRAME for layer composition to display properly.

The optional ANGLE(degrees) clause sets the camera heading for plane layers (SET LAYER ... AFFINE / PERSPECTIVE). On a plane layer, x, y is the camera's world position and ANGLE rotates the plane around it — degrees, consistent with SIN/COS. ANGLE has no visible effect on a normal (non-plane) layer.

Syntax

SET SCROLL x, y [ANGLE(degrees)]   (also accepts the coordinate form: SET SCROLL(x, y) [ANGLE(degrees)])

Parameters

  • x - Horizontal scroll offset in pixels (camera world X on a plane layer)
  • y - Vertical scroll offset in pixels (camera world Y on a plane layer)
  • ANGLE(degrees) - Optional camera heading in degrees for plane (AFFINE/PERSPECTIVE) layers; rotates the plane around the camera position. Degrees, like SIN/COS.

Examples

SET SCROLL(0, 0)      ' Reset scroll position
SET SCROLL(cameraX, cameraY)  ' Follow player
SET SCROLL(backgroundX / 2, 0)  ' Parallax effect
SET SCROLL 256, cameraY ANGLE(heading)  ' Plane camera: pan + rotate (degrees)

See also: GETSCROLL, CREATE LAYER, CREATE MAP, SET LAYER

↑ Back to top

SETFONT

Graphics · since 0.23.0

Synonym for FONT. Sets the font family and size for text rendering. The CURRENT sentinel keeps whatever font is already active and only changes the size. See FONT for full details, including FONT RESET (which has no SETFONT spelling) and the layer-scoped font behavior.

In multiplayer, SETFONT is client-only and halts with a runtime error if called from a server-side interpreter.

Syntax

SETFONT fontname$, size
SETFONT CURRENT, size

Parameters

  • fontname$ - Font family name (built-in: "IBM BIOS", "IBM EGA 8x8", or custom loaded font)
  • CURRENT - Sentinel meaning "keep the current font, only change size"
  • size - Font size in pixels

Examples

SETFONT "IBM EGA 8x8", 16
PRINT "Retro pixel font"
SETFONT CURRENT, 24  ' Resize without changing font face

See also: FONT, LOADFONT, COLOR, PRINT, LOCATE

↑ Back to top

SHADER

Graphics · since 1.3.2

Defines a custom fragment shader using a simplified DSL that compiles to WGSL for the wgpu renderer. The shader executes for every pixel of the sprite or layer it's applied to. All values in shader expressions use 0.0-1.0 range for colors (not 0-255). Shaders are globally scoped and can be applied to sprites with SET SPRITE SHADER or to layers with SET LAYER SHADER.

Syntax

SHADER "name"
  [USE NUMBER paramName [DEFAULT value]]
  [USE COLOR paramName [DEFAULT RGBA(r, g, b, a)]]
  [USE IMAGE paramName]
  WHEN DRAWING
    [variable = expression]
    SET COLOR = expression
  END WHEN
END SHADER

Parameters

  • name - String name for the shader (used with SET SPRITE SHADER or SET LAYER SHADER)
  • USE NUMBER paramName [DEFAULT value] - Declares a numeric parameter (f32). Default is 0.0 if not specified.
  • USE COLOR paramName [DEFAULT RGBA(r,g,b,a)] - Declares a color parameter (vec4). Default is RGBA(0,0,0,1) if not specified.
  • USE IMAGE paramName - Declares an additional texture input. When applying the shader, pass the image name as a string: @paramName="imageName". The image must be loaded with LOADIMG first. Use SAMPLE(paramName, uv) in the shader body to read from it.
  • WHEN DRAWING ... END WHEN - The shader body. Executes once per pixel.
  • variable = expression - Local variable assignment.
  • SET COLOR = expression - Sets the final output color. Must be a vec4.

Examples

' Greyscale with intensity control
SHADER "Greyscale"
  USE NUMBER intensity DEFAULT 1.0

  WHEN DRAWING
    c = IMAGE
    grey = (c.r + c.g + c.b) / 3.0
    result = MIX(c, RGBA(grey, grey, grey, c.a), intensity)
    SET COLOR = result
  END WHEN
END SHADER
' Rainbow cycle based on time
SHADER "Rainbow"
  USE NUMBER speed DEFAULT 1.0

  WHEN DRAWING
    c = IMAGE
    hue = FRACT(TIME * speed + UV.x)
    r = ABS(hue * 6.0 - 3.0) - 1.0
    g = 2.0 - ABS(hue * 6.0 - 2.0)
    b = 2.0 - ABS(hue * 6.0 - 4.0)
    rainbow = CLAMP(RGB(r, g, b), 0.0, 1.0)
    SET COLOR = RGBA(rainbow.r * c.r, rainbow.g * c.g, rainbow.b * c.b, c.a)
  END WHEN
END SHADER
' Damage flash (white pulse)
SHADER "Flash"
  WHEN DRAWING
    c = IMAGE
    flash = ABS(SIN(TICK * 0.5))
    SET COLOR = MIX(c, RGBA(1.0, 1.0, 1.0, c.a), flash)
  END WHEN
END SHADER
' Vignette (darken edges)
SHADER "Vignette"
  USE NUMBER strength DEFAULT 0.5

  WHEN DRAWING
    c = IMAGE
    center = VEC2(0.5, 0.5)
    dist = LENGTH(UV - center)
    vignette = 1.0 - SMOOTHSTEP(0.3, 0.7, dist) * strength
    SET COLOR = RGBA(c.r * vignette, c.g * vignette, c.b * vignette, c.a)
  END WHEN
END SHADER
' Pixelation effect
SHADER "Pixelate"
  USE NUMBER pixels DEFAULT 32.0

  WHEN DRAWING
    pixelUV = FLOOR(UV * pixels) / pixels
    c = OWNTEXTURE(pixelUV)
    SET COLOR = c
  END WHEN
END SHADER
' Color tint overlay
SHADER "Tint"
  USE NUMBER tintR DEFAULT 1.0
  USE NUMBER tintG DEFAULT 0.8
  USE NUMBER tintB DEFAULT 0.6
  USE NUMBER amount DEFAULT 0.3

  WHEN DRAWING
    c = IMAGE
    tint = RGB(tintR, tintG, tintB)
    SET COLOR = MIX(c, RGBA(tint.r, tint.g, tint.b, c.a), amount)
  END WHEN
END SHADER
' Dissolve transition (use with BSHADER for animation)
SHADER "Dissolve"
  USE NUMBER progress DEFAULT 0.0
  USE IMAGE noise

  WHEN DRAWING
    c = IMAGE
    n = SAMPLE(noise, UV)
    alpha = STEP(progress, n.r) * c.a
    SET COLOR = RGBA(c.r, c.g, c.b, alpha)
  END WHEN
END SHADER

Notes

  • Built-in variables
  • IMAGE (vec4) - Sprite texture color at current UV. Components: .r, .g, .b, .a (0.0-1.0).
  • UV (vec2) - Texture coordinates. .x/.y range 0.0 (top-left) to 1.0 (bottom-right).
  • TIME (f32) - Elapsed seconds since program started.
  • TICK (f32) - Frames since shader was assigned to sprite.
  • SCREEN (vec2) - Fragment position in screen pixels (like gl_FragCoord.xy). Useful for screen-space effects.
  • SCREEN_SIZE (vec2) - Screen dimensions in pixels (width, height). Use SCREEN / SCREEN_SIZE for normalized screen coords.
  • Color functions
  • RGBA(r, g, b, a) -> vec4 - Create color (0.0-1.0 range).
  • RGB(r, g, b) -> vec4 - Create opaque color (alpha=1.0).
  • Vector functions
  • VEC2(x, y), VEC3(x, y, z), VEC4(x, y, z, w) - Create vectors.
  • LENGTH(v) -> f32 - Vector magnitude.
  • NORMALIZE(v) -> vec - Unit vector.
  • DOT(a, b) -> f32 - Dot product.
  • Math functions
  • SIN(x), COS(x) -> f32 - Trig (radians).
  • ABS(x), FLOOR(x), FRACT(x), POW(x,y), SQRT(x) -> f32/vec
  • MIN(a,b), MAX(a,b), CLAMP(x,min,max) -> f32/vec
  • RND(x, y) or RND(seed) -> f32 - Pseudo-random 0-1 from seed values. Deterministic (same input = same output).
  • Interpolation
  • MIX(a, b, t) -> f32/vec - Linear interpolation (t=0 returns a, t=1 returns b).
  • STEP(edge, x) -> f32/vec - 0.0 if x < edge, else 1.0.
  • SMOOTHSTEP(edge0, edge1, x) -> f32/vec - Smooth interpolation.
  • Texture sampling
  • SAMPLE(textureName, uv) -> vec4 - Sample USE IMAGE texture.
  • OWNTEXTURE(uv) -> vec4 - Sample sprite's texture at arbitrary UV.
  • PX(x, y) -> vec2 - Pixel offset to UV offset.
  • OPERATORS: +, -, *, / (scalar and component-wise)
  • SWIZZLING: .r/.g/.b/.a or .x/.y/.z/.w - Access components. .rgb, .xy, .bgr etc.
  • All color values 0.0-1.0 (not 0-255). Up to 16 vec4 params (64 floats). Up to 4 USE IMAGE textures. Comments (') allowed. Case-insensitive variable names.
  • Limitations
  • Variable reassignment is NOT supported in shaders. Each variable name can only be assigned once because assignments compile to WGSL 'var' declarations. Use unique names for each step: instead of 'x = 1' then 'x = x + 1', use 'x = 1' then 'x2 = x + 1'.

See also: SET LAYER, BEHAVIOR, SET SPRITE

↑ Back to top

TEXTTOIMAGE

Graphics · since 1.0.3

Renders text (including emoji) to a new image that can be used as a sprite. The image is automatically sized to fit the text with some padding. Useful for creating emoji-based sprites, dynamic text labels, or any text that needs to be treated as a sprite for transformations like rotation or scaling.

Server-side dimension placeholder

In CrashNet multiplayer, TEXTTOIMAGE runs on every interpreter so the resulting image's dimensions register on each one's BehaviorEngine — the host's collision system needs them just like it needs LOADIMG / NEWIMAGE dims. Actual text rendering only happens on the user's CrashPlayer client (the host's headless gfx client drops the draw command; the player-server's render call is intercepted by the client's WS receiver, which acks without rendering since the client's LOCAL interpreter renders the same TEXTTOIMAGE line separately).

Because the server side has no font renderer, the dimensions it registers are a best-effort placeholder derived from the call arguments:

  • width = char_count × font_size
  • height = font_size

For monospace fonts and single-character emoji (the common cases), this lines up closely with what the local renderer actually produces. For proportional fonts, kerning, or text with significant character-width variation, the placeholder will be off — the visible rendered glyph differs from the bbox the server uses for SOLID sprite collision and for builtins like SPRITEWIDTH / SPRITEHEIGHT.

For collision-bearing text sprites where the placeholder hitbox is off, tighten or shift the collision region with SET SPRITE n BBOX(x1, y1, x2, y2) (relative coordinates, 0-1, where (0,0,1,1) is the full bounds) after the SET SPRITE call, so server-authoritative collision uses a hitbox you control rather than the placeholder bounds. Sprite-based PNG / LOADIMG textures don't have this issue — those dims are read directly from the source file on every interpreter.

Syntax

TEXTTOIMAGE text$, imageName$ [, fontSize] [, fontName$]

Parameters

  • text$ - The text to render (can include emoji characters like 🎮 👾 🚀)
  • imageName$ - Name to identify the created image
  • fontSize - Optional font size in pixels (default: 32)
  • fontName$ - Optional font family name. When omitted, the current font is used (default: "IBM EGA 8x8").

Examples

REM Create emoji sprite
TEXTTOIMAGE "🎮", "gamepad"
PUTIMAGE (100, 100), "gamepad"
REM Create with custom font size
TEXTTOIMAGE "💯", "score", 64
PUTIMAGE (160, 100), "score"
REM Create with custom font
TEXTTOIMAGE "GAME OVER", "gameover", 48, "Arial"
PUTIMAGE (80, 90), "gameover"
REM Use as hardware sprite
TEXTTOIMAGE "👾", "alien", 32
ENABLE SPRITE 0
SET SPRITE 0, 160, 100, "alien"
REM Animate emoji
TEXTTOIMAGE "⭐", "star", 48
FOR angle = 0 TO 360 STEP 5
  BEGIN FRAME
    CLS
    PUTIMAGE (160, 100), "star", ROTATE(angle, 0.5, 0.5)
  END FRAME
  DELAY 16
NEXT
REM Multiple emoji characters
TEXTTOIMAGE "🚀🌟✨", "effects", 32
PUTIMAGE (100, 50), "effects"
REM Family emoji (multi-character)
TEXTTOIMAGE "👨‍👩‍👧‍👦", "family", 64
PUTIMAGE (100, 100), "family"

See also: PUTIMAGE, NEWIMAGE, SET SPRITE, LOADIMG

↑ Back to top

TILEMAP

Graphics · since 1.27.0

Defines a named tilemap whose tile + metadata data live in your globally-scoped BASIC arrays. The TILEMAP block itself is bookkeeping — it tells the renderer which arrays compose this map. The arrays remain the single source of truth: pathfinding, AI, line-of-sight, anything that needs to read or write tile data goes straight at the array, and UPDATE MAP pushes the result to the GPU.

Bindings

  • TILES idx (FROM | INTO) ident — declare a tile layer at z-order index idx (0 = bottom).
    • FROM binds an existing globally-scoped INTEGER 2D array. The block doesn't allocate; it just registers the binding.
    • INTO auto-DIMs a new global of the right size and populates it from BASEMAP's tile layer at the same index.
    • Either way you end up with a user-mutable global after exec.
    • AS "name" gives the layer a readable handle for CREATE MAP "x" LAYER "name". When the block has 2+ TILES clauses, AS "name" is required on each.
  • META "name" (FROM | INTO) ident — same shape for metadata layers. STRING 2D arrays. No z-order (metadata isn't rendered, it's queryable via MAPMETA$ and FOR MAPDATA).
  • BASEMAP "file.map.json" — load an editor-drawn .map.json as the starting state. Block declarations override BASEMAP layers by index (TILES) or name (META). BASEMAP-supplied layers your block doesn't redeclare or expose via INTO stay as opaque cached data — still rendered, just not addressable as user arrays.
  • TILESET / TILESIZE / SIZE — block-level versions override whatever BASEMAP supplied. With no BASEMAP, all three are required.

Global scope (always, regardless of where the block lives)

Every binding produced by a TILEMAP block goes to global scope, regardless of where the block executes. INTO arrays auto-DIM as globals; FROM bindings require their referenced array to already be global (declared outside any SUB). This means TILEMAP "current" inside a SUB called from anywhere produces the same global namespace artifacts — the level-loader pattern works cleanly without you thinking about scope.

Re-execution replaces

Running TILEMAP "x" a second time replaces the registry entry, regenerates INTO globals from the new BASEMAP, and lets the next CREATE MAP / UPDATE MAP push fresh data. This is the explicit reload path — CALL LoadLevel(2) after CALL LoadLevel(1) overwrites cleanly.

Slicing into higher-D arrays

FROM accepts any expression that resolves to a 2D array — so you can use the general partial-indexing primitive to feed a slice of a higher-D global into the tilemap. A 3D rooms(R, X, Y) with TILES 0 FROM rooms(level) binds the 2D view at the chosen room.

DIM rooms(0 TO 9, 0 TO 99, 0 TO 59) AS INTEGER
FOR i = 0 TO 9
    WITH ROOM "level" + STR$(i)
        TILEMAP "lvl" + STR$(i)
            TILESET "grass.png"
            TILESIZE 32, 32
            SIZE 100, 60
            TILES 0 FROM rooms(i)
        END TILEMAP
        CREATE MAP "lvl" + STR$(i) AS LAYER 1
    END WITH
NEXT i

One global array, ten levels, no bazillion-tile-arrays bookkeeping. The slice is a live view — rooms(3, 5, 5) = WALL_TILE followed by UPDATE MAP "lvl3" does the right thing because the view shares storage with rooms. See the ARRAY type docs for the full story on partial indexing.

What works as a FROM expression:

  • Bare identifier — FROM tiles (the legacy form, re-resolved by name each tick).
  • Partial index of a higher-D array — FROM rooms(level).
  • Member access — FROM player.tiles (when player is an Object).
  • Anything else that evaluates to an array.

Limits + caveats:

  • Expression-form FROM is evaluated once at TILEMAP DEF registration time, then the resulting array reference is snapshotted. Mutations to the underlying storage stay live; reassigning the root variable does not update the binding — re-execute the TILEMAP DEF when you switch arrays.
  • Out-of-range partial indices fail loudly at registration.
  • INTO accepts the same two forms as FROM: bare identifier auto-DIMs a fresh global; a slice expression like INTO Rooms(0) writes BASEMAP data INTO the named slice of an existing higher-D array. The slice must already exist and match the TILEMAP SIZE (height × width). Writes go through the partial-index view so they land on the parent's storage; UPDATE MAP later reads from the same view.

Validation

These run at parse time + exec time. Violations halt with a runtime error pointing at the offending clause — we'd rather you see a clear failure than silently get a black-screen tilemap.

  • Tile-layer indexes must be unique, contiguous from 0, and non-negative.
  • With no BASEMAP, the lowest declared TILES index must be 0. TILES 1 ... without a TILES 0 and no BASEMAP is rejected — there's no source for layer 0, so CREATE MAP would silently render the empty default. Renumber, add a TILES 0, or add a BASEMAP that supplies the lower layers.
  • Tile-layer names must be unique within the block (and required when 2+ TILES clauses are present).
  • Metadata-layer names must be unique within the block.
  • INTO requires BASEMAP in the same block.
  • FROM arrays must exist in global scope and match the declared (or BASEMAP-supplied) SIZE.
  • With no BASEMAP, TILESET / TILESIZE / SIZE must all be supplied.

Syntax

TILEMAP "name"
    [BASEMAP "file.map.json"]
    [TILESET "image"]
    [TILESIZE w [, h]]
    [SIZE w, h]
    [TILES idx [AS "layer_name"] (FROM expression | INTO expression)]*
    [META "name" (FROM expression | INTO expression)]*
END TILEMAP

Parameters

  • name - String literal naming this tilemap. Used by CREATE MAP "name" / UPDATE MAP "name" to look it up.
  • BASEMAP "file" - Optional starting state from a Map Editor .map.json. Block declarations layer on top.
  • TILESET "image" - Tileset image (loaded with LOADIMG or carried over from BASEMAP).
  • TILESIZE w[, h] - Per-tile pixel size. Single value means square tiles.
  • SIZE w, h - Map size in tiles. Inherited from BASEMAP when absent.
  • TILES idx [AS "name"] FROM expression - Bind any expression that resolves to a 2D INTEGER array as tile layer idx. Bare identifier (re-resolved each tick) or a partial-index expression like rooms(level) (snapshotted once, view-aliased) both work.
  • TILES idx [AS "name"] INTO expression - Two forms: INTO ident auto-DIMs a new global of the right shape and populates it from BASEMAP's tile layer idx. INTO Rooms(0) (slice expression) writes BASEMAP data into an existing higher-D array's slice via the partial-indexing primitive — the slice must already exist in scope and match the TILEMAP SIZE.
  • META "name" FROM expression - Same expression-form FROM semantics for STRING 2D metadata arrays.
  • META "name" INTO expression - Same expression-form INTO semantics for STRING 2D metadata arrays. Bare ident = auto-DIM; slice expression = write-into-existing-slice.

Examples

REM Procedural single-layer tilemap
DIM tiles(0 TO 99, 0 TO 59) AS INTEGER
TILEMAP "level1"
    TILESET "grass.png"
    TILESIZE 32, 32
    SIZE 100, 60
    TILES 0 FROM tiles
END TILEMAP
CREATE MAP "level1" AS LAYER 0
REM Multi-layer with mixed FROM/INTO + metadata
TILEMAP "shop"
    BASEMAP "shop-base.map.json"
    TILES 0 AS "floor"     INTO floorTiles      ' loaded from BASEMAP, exposed as a global
    TILES 1 AS "walls"     INTO wallTiles
    TILES 2 AS "decor"     FROM proceduralDecor ' user-supplied procedural top layer
    META  "shopkeepers"    INTO npcGrid$        ' base meta exposed as a global
END TILEMAP
CREATE MAP "shop" LAYER "floor"  AS LAYER 0
CREATE MAP "shop" LAYER "walls"  AS LAYER 1
CREATE MAP "shop" LAYER "decor"  AS LAYER 2
REM Level-loader pattern — TILEMAP DEF inside a SUB still produces globals
SUB LoadLevel(n)
    TILEMAP "current"
        BASEMAP "level" + STR$(n) + ".map.json"
        TILES 0 AS "bg" INTO levelBg
        META  "doors"   INTO doorGrid$
    END TILEMAP
    CREATE MAP "current" AS LAYER 0
END SUB

CALL LoadLevel(1)
CALL LoadLevel(2)  ' replaces "current" + the levelBg + doorGrid$ globals
REM Room-aware level switching (1.29.0)
IF ME.ISHOST THEN
    FOR i = 1 TO 4
        WITH ROOM "level" + STR$(i)
            TILEMAP "lvl" + STR$(i)
                TILESET "grass.png"
                TILESIZE 32, 32
                SIZE 100, 60
                TILES 0 FROM tiles
            END TILEMAP
            CREATE MAP "lvl" + STR$(i) AS LAYER 1
        END WITH
    NEXT i
END IF

See also: CREATE MAP, UPDATE MAP, MAPMETA$, FOR MAPDATA, WITH

↑ Back to top

TOSPRITESHEET

Graphics · since 1.0.3

Combines multiple existing images into a single sprite sheet. All source images must have identical dimensions. The resulting sprite sheet arranges frames horizontally in a single row. Useful for creating animation sequences from individual frame images or combining captured regions.

The output sheet's per-frame dimensions are derived from the first source image — that source must already be loaded (via LOADIMG / NEWIMAGE / GETIMAGE) when TOSPRITESHEET runs, otherwise the call halts with a runtime error pointing at the missing handle. Frame dimensions are registered on every interpreter (host, player-server, client) so server-authoritative collision sees the right hitbox for any sprite that uses the resulting sheet.

Syntax

TOSPRITESHEET outputName$, sourceImage1$, sourceImage2$ [, ...]

Parameters

  • outputName$ - Name for the new combined sprite sheet
  • sourceImage1$, sourceImage2$, ... - Names of images to combine (minimum 2)
  • All source images must have the same width and height
  • Frames are arranged left-to-right in a single row
  • Output width = sourceWidth * numberOfFrames
  • Output height = sourceHeight

Examples

REM Combine captured frames into animation
GETIMAGE (0, 0)-(31, 31), "frame0"
GETIMAGE (32, 0)-(63, 31), "frame1"
GETIMAGE (64, 0)-(95, 31), "frame2"
TOSPRITESHEET "walk", "frame0", "frame1", "frame2"
PUTIMAGE (100, 100), "walk", currentFrame
REM Create procedural animation frames
NEWIMAGE 32, 32, 0, "f0"
SET IMAGE "f0"
CIRCLE (16, 16), 8, 15
SET LAYER 0

NEWIMAGE 32, 32, 0, "f1"
SET IMAGE "f1"
CIRCLE (16, 16), 12, 15
SET LAYER 0

TOSPRITESHEET "pulse", "f0", "f1"
REM Combine multiple enemy states
LOADIMG "enemy_idle.png", "idle"
LOADIMG "enemy_attack.png", "attack"
LOADIMG "enemy_hurt.png", "hurt"
TOSPRITESHEET "enemy_states", "idle", "attack", "hurt"

See also: NEWIMAGE, GETIMAGE, LOADIMG, PUTIMAGE, FRAMECOUNT

↑ Back to top

TRANSITION

Graphics · since 1.4.3

Returns the name of the currently active screen transition effect on a layer, or an empty string if no transition is running. Use this to wait for CLS EFFECT transitions to complete. Each layer can have its own independent transition.

Syntax

TRANSITION([layerId])

Parameters

  • layerId - Optional layer number to check (default: current layer)

Examples

' Check current layer transition:
IF TRANSITION() <> "" THEN PRINT "Transition in progress"
' Wait for fade to complete:
CLS EFFECT(FADE)
WHILE TRANSITION() <> ""
  ' Program continues running
WEND
PRINT "Fade complete!"
' Check specific layer:
result$ = TRANSITION(2)  ' Check layer 2
' Multiple layer transitions:
SET LAYER 0
CLS EFFECT(MELT)
SET LAYER 1
CLS EFFECT(FADE)
' Both run simultaneously

See also: CLS, SET LAYER

↑ Back to top

TRANSITION$

Graphics · since 1.57.0

Returns the name of the transition effect currently running on a layer — "FADE", "MELT", "BLOCK", "DISSOLVE", "WIPE" — or an empty string if none is running.

Use it to wait for a CLS EFFECT(...) to finish before moving on. Both spellings exist and behave identically: TRANSITION$ follows the string-function convention, and TRANSITION is accepted for convenience. With no argument it reports the current layer.

Syntax

TRANSITION$([layer])
TRANSITION([layer])

Parameters

  • layer - Optional layer ID. Defaults to the current layer.

Examples

WITH LAYER 0
  CLS EFFECT(FADE) SPEED 120
END WITH
WHILE TRANSITION()
  YIELD
WEND
PRINT "faded out"
IF TRANSITION$(1) = "" THEN PRINT "layer 1 is idle"

See also: CLS, SET LAYER

↑ Back to top

TYPING

Graphics · since 1.4.3

Returns 1 if a PRINT RATE typewriter effect is currently in progress, 0 if complete. Use this to wait for typewriter text to finish before continuing or to allow the user to skip the effect.

Syntax

TYPING()

Examples

' Check if typing is in progress:
IF TYPING() = 1 THEN PRINT "Still typing..."
' Wait for typing to complete:
PRINT RATE(50) "Loading game data..."
WHILE TYPING() = 1
  ' Can do other things here
WEND
PRINT ""
PRINT "Done!"
' Allow user to skip:
PRINT WORDS RATE(30) "Long dialog text..."
WHILE TYPING() = 1
  IF KEYDOWN(SPACE) THEN
    ' Could implement skip here
  END IF
WEND

See also: PRINT, WINDOW

↑ Back to top

UPDATE MAP

Graphics · since 1.27.0

Mutate your tile array. Call UPDATE MAP. The GPU catches up. The mental model: your BASIC arrays are always the truth; the renderer's tilemap texture is a stale-until-told cache, and UPDATE MAP is the cache flush.

Heads up: `LAYER` here is TILE LAYER, not screen layer

The LAYER selector references tile layers declared INSIDE the TILEMAP block (TILES 0, TILES 1, ...), not the screen layers the tilemap got mounted onto via AS LAYER .... So given:

TILEMAP "level1"
    TILES 0 AS "bg" FROM bgTiles    ' tile layer 0
    TILES 1 AS "fg" FROM fgTiles    ' tile layer 1
END TILEMAP
CREATE MAP "level1" AS LAYER 1 TO 2 ' tile 0 → screen 1, tile 1 → screen 2

…then UPDATE MAP "level1" LAYER 0 refreshes bg (tile layer 0), regardless of which screen layer it ended up on. LAYER "fg" refreshes by name. LAYER 0 TO 1 refreshes both. Mismatching this in your head is the #1 way to write an UPDATE MAP that does nothing visible.

Selector shapes

Mirrors CREATE MAP's named-form selector:

  • LAYER "name" — match the AS-name from the TILEMAP block.
  • LAYER N — match the tile layer at index N (0-based).
  • LAYER N TO M — match every mounted tile-layer index in that inclusive range.
  • No LAYER clause — refresh every mounted tile layer.

If the selector matches zero mounted tile layers, UPDATE MAP halts loudly. We don't silently no-op on bad selectors — that's how black-screen bugs hide.

Performance: don't call this per frame

Each call triggers a tilemap GPU reload. It's not a draw call — it's a full texture re-bake of the affected layers. Batch your mutations and call UPDATE MAP once at the end of the batch. For per-frame mutations (water animation, scrolling effects), you almost certainly want a shader or a TILESET animation, not UPDATE MAP.

What `UPDATE MAP` works on

Tilemaps registered via TILEMAP DEF (or the named CREATE MAP form). For file-loaded maps, just CREATE MAP the file again — the file IS the source of truth in that path, not an array.

Room-aware broadcast

The refresh broadcast uses the same room filter as CREATE MAP. Inside a WITH ROOM "name" block the update reaches only players in that room; outside any WITH ROOM block it broadcasts unfiltered.

Known foot-gun: when CREATE MAP runs inside WITH ROOM "lobby" but UPDATE MAP runs outside any block, the update broadcasts unfiltered while the mount was lobby-scoped. Wrap the UPDATE MAP in the same WITH ROOM as the original CREATE MAP to keep them aligned.

Syntax

UPDATE MAP <name> [LAYER <tile_layer_selector>]

Parameters

  • name - String naming a registered TILEMAP (must have been CREATE MAP'd at least once).
  • tile_layer_selector - Optional: filter the refresh to a subset of TILE layers (the layers declared inside the TILEMAP block — NOT screen layers). Three shapes: a string AS-name (LAYER "foreground"), a numeric tile-layer index (LAYER 1), or a numeric tile-layer range (LAYER 0 TO 2). Absent => refresh every mounted tile layer.

Examples

REM Mutate the array, then push to GPU
tiles(x, y) = newTileIdx
UPDATE MAP "level1"
REM Refresh only the foreground after a region update
FOR i = 0 TO 9
    decorTiles(spawnX + i, spawnY) = MUSHROOM_TILE
NEXT i
UPDATE MAP "shop" LAYER "decor"
REM Numeric-index selector — handy when layers don't carry AS-names
UPDATE MAP "level1" LAYER 1
REM Range selector — refresh tile layers 0..=2 in one statement
UPDATE MAP "level1" LAYER 0 TO 2
REM Open every door in the level (pathfinding-friendly: same array)
FOR MAPDATA (0, 0)-(mapW, mapH) AS x, y, m$
    IF m$ = "D" THEN tiles(x, y) = OPEN_DOOR_TILE
NEXT MAPDATA
UPDATE MAP "level1"
REM Room-aware refresh — wrap UPDATE MAP in the same WITH ROOM as the CREATE MAP
WITH ROOM "lobby"
    bg(0, 0) = 7
    UPDATE MAP "lobbyMap"   ' refresh broadcasts to lobby only
END WITH

See also: TILEMAP, CREATE MAP, MAPMETA$, FOR MAPDATA, WITH

↑ Back to top

WINDOW

Graphics · since 1.4.3

Defines a rectangular text window region in pixel coordinates. Text output will start at the window's top-left corner and word wrap at the right edge. The text cursor is reset to position (0, 0) within the window. The coordinate hierarchy is: WINDOW (base) → ORIGIN (offset) → LOCATE (cursor position).

Syntax

WINDOW (x1, y1) - (x2, y2)

Parameters

  • x1, y1 - Top-left corner of the text window in pixels
  • x2, y2 - Bottom-right corner of the text window in pixels

Examples

WINDOW (50, 50) - (270, 150)  ' Create centered text window
PRINT WORDS "Text wraps within the window boundaries"
' Dialog box with typewriter effect:
WINDOW (20, 140) - (300, 190)
PRINT WORDS RATE(30) "Welcome traveler! The adventure begins now."
' RPG-style text box:
LINE (18, 138)-(302, 192), 15, B  ' Draw border
WINDOW (20, 140) - (300, 190)
PRINT WORDS RATE(25) "You found a treasure chest! Inside you discover 100 gold coins and a mysterious key."

See also: PRINT, LOCATE, TYPING

↑ Back to top

Sprites

DISABLE SPRITE

Sprites · since 1.0.3

Disables a hardware sprite slot and destroys its associated GPU sprite. The sprite slot is reset to default state and can be reused.

Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.

Any PATTERN running on the sprite is removed with it. A pattern instance outliving its sprite keeps ticking against an id that no longer exists — and the slot is claimable again, so the next sprite to take that id would inherit a dead pattern mid-step.

Syntax

DISABLE SPRITE spriteNum

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

DISABLE SPRITE 0  ' Disable and clean up sprite 0
' Disable all enemy sprites:
FOR i = 0 TO 9
  DISABLE SPRITE i
NEXT
' Clean up sprites at game end:
FOR i = 0 TO maxSprites
  IF SPRITEENABLED(i) THEN DISABLE SPRITE i
NEXT

See also: ENABLE SPRITE, SET SPRITE, SPRITEENABLED

↑ Back to top

EMITPARTICLE

Sprites · since 1.6.0

Creates a system-managed particle sprite that automatically expires after a specified number of ticks. Particles are allocated from their own band of sprite IDs (100000+, separate from user sprites) and leverage the existing pattern/behavior system. When a particle expires, it is automatically disabled and removed. Particles integrate with all existing sprite operations including collisions, tags, and patterns. If no particle slots are available, the command silently does nothing. Supports two syntaxes: coordinate form (using a coordinate literal or variable) and separate x, y form for convenience. Parameters can be passed to the pattern using @paramName=value syntax, allowing each particle to have unique behavior (e.g., different target positions, speeds, or directions). If LAYER is not specified, the particle is created on the current layer set by SET LAYER.

Polygon particles

Pass a MUTATION where the image name goes and the particle is a shape rather than a sprite: a copy of that mutation, centred on the spawn point, drawn as a polygon and expiring on the same tick count. It is an ordinary particle underneath — same ID band, same lifetime, same reaping, same tag — with the shape bound to its slot, so nothing about pooling or expiry behaves differently.

It takes the polygon appearance modifiers, from the same grammar rule as SET POLYGON: COLOR, FILL, TEXTURE, UV, SHADER, LAYER, ZORDER, SOLID [CIRCLE|BOX|POLY] [SPRITES(...)], BBOX.

DIM shard AS MUTATION
shard = MUTATION((0, -6), (5, 4), (-5, 4))
EMITPARTICLE (x, y), shard, "debris", "tumble", 90, @spin = 240 COLOR 5 FILL 6 SOLID

One difference from the sprite form worth knowing: the polygon-only modifiers are a loud error on a particle emitted from an image name, rather than being quietly ignored — COLOR on a sprite particle is a mistake about which kind of particle you are emitting.

GHOST and TAG are rejected on either form. A particle is born rather than edited, so there is no collider to remove, and its tag is already the third argument.

Multiplayer: a polygon particle is a bound polygon, so it follows the same rule SET POLYGON does — client-only. Emitting one from server-side code in a CrashNet game is a fatal error; wrap it in IF ME.ISCLIENT THEN ... END IF. Sprite particles are unaffected.

What moves a particle

The fourth argument names the motion, and it may be either a BEHAVIOR or a PATTERN — for either kind of particle. The name says which it is. A name matching neither is a fatal error: a particle that was told to move and does not is a bug you would otherwise find by staring at a motionless particle.

BEHAVIOR "name" and PATTERN "name" are also accepted as modifiers, which is how you attach the kind the fourth argument did not name, or a second one alongside it.

Modifiers that shape a particle at birth

ALPHA(a), ROTATE(angle), SCALE(sx, sy), SCALETO(w, h), FLIPX, FLIPY and ORIGIN(ox, oy) apply to the particle as it is created, and MAXVELOCITY / MAXACCELERATION cap what its motion can build up to. This is what gives a burst per-particle variety without a behavior per variation:

EMITPARTICLE (x, y), rock, "debris", "tumble", 90 SCALE(0.4, 0.4) ROTATE(RND * 360) ALPHA(0.8)

What a particle refuses, loudly, because it has no answer for them: LOCATION (position is the first argument), SHOW / HIDE (a particle is visible for its whole life — end it sooner with a shorter ticksToLive), RESET (nothing to reset at birth), PARENT (a particle outlives the statement that emitted it and is reaped by the engine), and SYNC / NOSYNC (particle replication is a room setting — PARTICLES in the CRASHNET block).

Syntax

EMITPARTICLE coord, "image", "tag", "pattern", ticksToLive [, @param=value, ...] [SOLID [SPRITES("tag", ...)]] [LAYER n]
EMITPARTICLE x, y, "image", "tag", "pattern", ticksToLive [, @param=value, ...] [SOLID [SPRITES("tag", ...)]] [LAYER n]
EMITPARTICLE coord, mutationVar, "tag", "behavior", ticksToLive [, @param=value, ...] [SET POLYGON modifiers]

Parameters

  • coord - Initial position as a coordinate literal (x, y) or coordinate variable (first syntax)
  • x, y - Initial X and Y position in pixels (second syntax)
  • "image" | mutationVar - Name of a loaded image for a sprite particle, or a MUTATION for a polygon particle
  • "tag" - Tag string for collision detection and sprite queries
  • "pattern" - Name of a defined PATTERN to apply (empty string "" for no pattern). For a polygon particle this names a BEHAVIOR instead.
  • ticksToLive - Number of ticks before automatic expiration (60 ticks ≈ 1 second)
  • @param=value - Optional parameters passed to the pattern or behavior, which flow down to its commands. Values are evaluated at emission time.
  • SOLID - Optional modifier giving the particle a collider. On its own it resolves against boundaries; add SPRITES("tag", ...) to name the sprite tags it also treats as solid obstacles. On a polygon particle it also accepts CIRCLE (default), BOX or POLY.
  • LAYER n - Optional layer number. If omitted, uses the current layer from SET LAYER.
  • SET POLYGON modifiers - A polygon particle also accepts COLOR, FILL, TEXTURE, UV, SHADER, ZORDER and BBOX, with the same meanings they have on SET POLYGON.

Examples

' Simple particle with separate x, y
EMITPARTICLE 100, 100, "spark", "particle", "", 30
' Simple particle with coordinate literal
EMITPARTICLE (100, 100), "spark", "particle", "", 30
' Particle with fade-out pattern using coordinate variable
BEHAVIOR "fadeOut"
  BALPHA 1 TO 0 DURATION 60
END BEHAVIOR

PATTERN "sparkle"
  "fadeOut"
END PATTERN

DIM pos AS COORDINATE
pos = (playerX, playerY)
EMITPARTICLE pos, "spark", "effect", "sparkle", 60
' Emit particles on mouse click (using x, y syntax)
DO
  IF MOUSEBUTTON(0) THEN
    EMITPARTICLE MOUSEX, MOUSEY, "star", "particle", "fadeOut", 45
  END IF
  YIELD
LOOP
' Bullet casings with physics
BEHAVIOR "ejectCasing"
  BVELOCITY -2, -4
  BACCELERATION 0, 0.3
  BROTATE 360 DURATION 30
END BEHAVIOR

PATTERN "casing"
  "ejectCasing"
END PATTERN

EMITPARTICLE gunX, gunY, "casing", "debris", "casing", 90
' Solid particle that collides with boundaries
EMITPARTICLE (160, 100), "debris", "solid_particle", "fallAndFade", 120 SOLID
' Explosion effect spawning multiple particles
SUB Explosion(x, y)
  FOR i = 1 TO 20
    angle = RND * 360
    EMITPARTICLE x, y, "spark", "explosion", "expand", 30 + RND * 30
  NEXT i
END SUB
' Parameterized particle with unique target per emission
BEHAVIOR "moveToTarget"
  BMOVE @targetX=0, @targetY=0
END BEHAVIOR

PATTERN "homing"
  "moveToTarget" DURATION 60
END PATTERN

' Each particle moves toward a different target
EMITPARTICLE (100, 100), "missile", "projectile", "homing", 90, @targetX=enemyX, @targetY=enemyY
' Radial explosion with unique direction per particle
BEHAVIOR "radialMove"
  BMOVE @dx=0, @dy=0
  BALPHA 1 TO 0 DURATION 30
END BEHAVIOR

PATTERN "explode"
  "radialMove"
END PATTERN

SUB Explode(x, y)
  FOR i = 0 TO 7
    angle = i * 45 * 3.14159 / 180
    dx = COS(angle) * 3
    dy = SIN(angle) * 3
    EMITPARTICLE x, y, "spark", "debris", "explode", 30, @dx=dx, @dy=dy
  NEXT i
END SUB
' Emit particle on a specific layer (for foreground effects)
EMITPARTICLE (160, 100), "glow", "fx", "fadeOut", 60 LAYER 3
' Emit solid particle on layer 2
EMITPARTICLE playerX, playerY, "debris", "collision", "fall", 90 SOLID LAYER 2

See also: PATTERN, BEHAVIOR, SET SPRITE, ENABLE SPRITE, DISABLE SPRITE, FOR SPRITES

↑ Back to top

ENABLE SPRITE

Sprites · since 1.0.3

Enables a hardware sprite slot. Hardware sprites are GPU-native sprites that render between layers without being baked into layer textures. They are more efficient than PUTIMAGE for moving objects. Up to 100,000 sprite slots (0-99,999) are available.

Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.

Syntax

ENABLE SPRITE spriteNum

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

ENABLE SPRITE 0  ' Enable first sprite slot
ENABLE SPRITE player  ' Enable using variable
' Enable multiple sprites for enemies:
FOR i = 0 TO 9
  ENABLE SPRITE i
NEXT

See also: DISABLE SPRITE, SET SPRITE, MOVE SPRITE, PUTIMAGE

↑ Back to top

FOR COLLISIONS

Sprites · since 1.0.4

Iterates over all collision pairs. The two-variable form detects sprite-sprite collisions only. The three-variable form also detects sprite-boundary collisions: when a sprite collides with a boundary, spriteB is set to -1 and boundaryTag$ contains the boundary's tag name. Uses bounding box collision detection (honouring a sprite's BBOX and scale), and only SOLID sprites take part.

A pair is reported when either of two things is true:

  • They overlap right now. Adjacency is not a collision — two sprites that came to rest edge-to-edge were never pushed apart by anything and do not appear here.
  • The solid resolver corrected a collision between them on this tick. Resolution separates the pair, so a geometry-only test would miss an impact that provably happened. Such a collision is reportable for exactly the tick it occurred on and no longer.

A sprite pressing continuously into a wall is corrected every tick, so it produces a fresh collision every tick — that is not the same as one collision lingering.

To react to the moment of impact rather than poll for it, use a behavior's ONCOLLISION / AFTERCOLLISION handlers, which fire on collision edges and carry the contact details (including the pre-resolution impact velocity).

Syntax

FOR COLLISIONS a, b
  statements...
NEXT COLLISION

FOR COLLISIONS spriteA, spriteB, boundaryTag$
  statements...
NEXT COLLISION

Parameters

  • a/spriteA - Variable to receive first sprite index in collision pair
  • b/spriteB - Variable to receive second sprite index, or -1 for boundary collisions (three-variable form)
  • boundaryTag$ - (Optional, three-variable form) String variable to receive the tag of the boundary hit (empty for sprite-sprite collisions)

Examples

' Basic collision loop (sprite-sprite only)
FOR COLLISIONS a, b
  PRINT "Sprites"; a; "and"; b; "collided"
NEXT COLLISION
' Handle player-enemy collisions
FOR COLLISIONS a, b
  IF SPRITETAG$(a) = "player" AND SPRITETAG$(b) = "enemy" THEN
    playerHealth = playerHealth - 10
    DISABLE SPRITE b
  ELSEIF SPRITETAG$(b) = "player" AND SPRITETAG$(a) = "enemy" THEN
    playerHealth = playerHealth - 10
    DISABLE SPRITE a
  END IF
NEXT COLLISION
' Bullet collision detection
FOR COLLISIONS i, j
  IF SPRITETAG$(i) = "bullet" THEN
    IF SPRITETAG$(j) = "enemy" THEN
      score = score + 100
      DISABLE SPRITE i
      DISABLE SPRITE j
    END IF
  END IF
NEXT COLLISION
' Check all collisions including boundaries
FOR COLLISIONS a, b, tag$
  IF b = -1 THEN
    PRINT "Sprite"; a; "hit boundary:"; tag$
  ELSE
    PRINT "Sprite"; a; "hit sprite"; b
  END IF
NEXT COLLISION
' Handle player hitting walls
FOR COLLISIONS player, other, boundary$
  IF other = -1 THEN
    SELECT CASE boundary$
      CASE "floor"
        playerOnGround = 1
      CASE "spikes"
        playerHealth = playerHealth - 1
    END SELECT
  END IF
NEXT COLLISION

See also: ONCOLLISION, AFTERCOLLISION, SPRITETAG$, SET SPRITE (TAG), ENABLE SPRITE, DISABLE SPRITE, FOR SPRITES, BOUNDARY ADD

↑ Back to top

FOR SPRITES

Sprites · since 1.0.3

Iterates over all enabled sprites that have the specified tag. Each iteration sets the loop variable to the sprite index of a matching sprite. This provides an efficient way to process groups of related sprites without manually tracking sprite indices.

Syntax

FOR SPRITES spriteVar, tagExpr$
  statements...
NEXT SPRITE

Parameters

  • spriteVar - Variable to receive sprite index on each iteration
  • tagExpr$ - String expression for the tag to filter by (e.g., "enemy", "bullet")

Examples

' Move all enemies down
FOR SPRITES s, "enemy"
  MOVE SPRITE s, SPRITEX(s), SPRITEY(s) + 1
NEXT SPRITE
' Disable all bullets that went off screen
FOR SPRITES b, "bullet"
  IF SPRITEY(b) < 0 THEN
    DISABLE SPRITE b
  END IF
NEXT SPRITE
' Count active enemies
enemyCount = 0
FOR SPRITES e, "enemy"
  enemyCount = enemyCount + 1
NEXT SPRITE
PRINT "Enemies remaining:"; enemyCount
' Apply damage to all enemies in blast radius
FOR SPRITES e, "enemy"
  dx = SPRITEX(e) - blastX
  dy = SPRITEY(e) - blastY
  dist = SQR(dx * dx + dy * dy)
  IF dist < blastRadius THEN
    DISABLE SPRITE e
    score = score + 100
  END IF
NEXT SPRITE

See also: SET SPRITE (TAG), SPRITETAG$, FOR COLLISIONS, ENABLE SPRITE, DISABLE SPRITE, NEXTFREESPRITE

↑ Back to top

FRAMECOUNT

Sprites · since 0.23.0

Returns the number of frames in a sprite sheet loaded with LOADIMG AS SPRITESHEET.

Syntax

FRAMECOUNT(image_name)

Parameters

  • image_name - Name of sprite sheet image resource

Examples

LOADIMG "player.png", "player" AS SPRITESHEET(32, 32)
frames = FRAMECOUNT("player")
PRINT "Player has"; frames; "animation frames"
REM Use CALL TIMER for sprite animation instead of FOR loops
DECLARE SUB AnimateSprite()
CALL TIMER AnimateSprite() EVERY 100 MILLISECONDS AS "spriteAnim"

SUB AnimateSprite()
  frame = (frame + 1) MOD FRAMECOUNT("sprites")
  PUTIMAGE (x, y), "sprites", frame
END SUB

See also: LOADIMG, PUTIMAGE

↑ Back to top

GETSPRITEMETA

Sprites · since 1.0.3

Retrieves metadata previously stored on a sprite using SET SPRITE META. Supports both single-value and key-value retrieval. Returns the stored value (number, string, or object), or 0 if no metadata has been set for that key. Crash BASIC has no null: a missing key and a stored 0 are indistinguishable, so if 0 is a meaningful value for your metadata, store a sentinel (-1) or a separate "has" key rather than testing the value itself.

Syntax

GETSPRITEMETA(spriteNum)
GETSPRITEMETA(spriteNum, "key")

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • key - Optional string key to retrieve specific value (e.g., "health", "lives")

Examples

' Get key-value metadata
health = GETSPRITEMETA(0, "health")
lives = GETSPRITEMETA(0, "lives")
state$ = GETSPRITEMETA(0, "state")
PRINT "Health:"; health; " Lives:"; lives
' Damage calculation with key-value metadata
FOR SPRITES e, "enemy"
  health = GETSPRITEMETA(e, "health")
  health = health - playerDamage
  SET SPRITE e META "health", health
  IF health <= 0 THEN
    DISABLE SPRITE e
    score = score + 100
  END IF
NEXT SPRITE
' Check sprite type
FOR SPRITES s, "powerup"
  type$ = GETSPRITEMETA(s, "type")
  IF type$ = "powerup" THEN
    CALL CollectPowerup(s)
  END IF
NEXT SPRITE
' Retrieve object metadata (single-value mode)
DIM enemyStats AS EnemyData
enemyStats = GETSPRITEMETA(0)
IF enemyStats.health <= 0 THEN
  DISABLE SPRITE 0
END IF
' Check for metadata before using
DIM meta AS VARIANT
meta = GETSPRITEMETA(spriteNum, "health")
IF meta <> 0 THEN
  PRINT "Health:"; meta
END IF

See also: SET SPRITE (META), SPRITETAG$, FOR SPRITES

↑ Back to top

HIDE SPRITE

Sprites · since 1.0.3

Makes a hardware sprite invisible without disabling it. The sprite retains its configuration and can be shown again with SHOW SPRITE. Useful for temporary visibility toggling like invincibility flashing or pooled sprites.

Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.

Syntax

HIDE SPRITE spriteNum

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

HIDE SPRITE 0  ' Make sprite invisible
' Invincibility flashing effect:
FOR i = 0 TO 20
  IF i MOD 2 = 0 THEN HIDE SPRITE 0 ELSE SHOW SPRITE 0
  DELAY 100
NEXT
SHOW SPRITE 0
' Hide sprite when off-screen:
IF spriteX < 0 OR spriteX > 320 THEN HIDE SPRITE 0

See also: SHOW SPRITE, DISABLE SPRITE, SPRITEVISIBLE

↑ Back to top

IMAGEBBOX

Sprites · since 1.45.0

Returns a named Aseprite slice's bounding box on a given frame of a loaded .ase image, as a Range (x1,y1)-(x2,y2) in RELATIVE 0-1 coordinates. There's no sprite context, so no world position — combine the relative box with a position yourself, feed it straight into SET SPRITE id BBOX <range>, or use SPRITEBBOX when you want world coordinates for a placed sprite.

The slice and its per-frame keys come from the Aseprite file (slice keys are expanded per frame). Naming a slice the image doesn't have — or a frame no key covers — is a loud runtime error listing the available slice names. The image name is case-insensitive.

Syntax

IMAGEBBOX(image$, sliceName$, frame)

Parameters

  • image$ - Name an .ase image was loaded under with LOADIMG (case-insensitive)
  • sliceName$ - Aseprite slice name (case-insensitive)
  • frame - Absolute frame index (0-based)

Example

LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 64)
' Frame 3's "sword" slice, relative 0-1
b = IMAGEBBOX("HERO", "sword", 3)
SET SPRITE 0 BBOX b

See also: SPRITEBBOX, BEHAVIOR, SET SPRITE, LOADIMG

↑ Back to top

MOVE SPRITE

Sprites · since 1.0.3

Quickly updates only the position of a hardware sprite without recreating it. More efficient than SET SPRITE when only position changes. Use this for smooth sprite movement in game loops.

Syntax

MOVE SPRITE spriteNum, x, y
MOVE SPRITE spriteNum, coord   (coord = (x,y) literal or a coordinate variable/constant)

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • x, y - New position coordinates

Examples

MOVE SPRITE 0, newX, newY  ' Update position
' Game loop with sprite movement:
WHILE running
  IF KEYDOWN("LEFT") THEN playerX = playerX - 2
  IF KEYDOWN("RIGHT") THEN playerX = playerX + 2
  MOVE SPRITE 0, playerX, playerY
  DELAY 16
WEND
' Move multiple sprites:
FOR i = 0 TO numEnemies - 1
  enemyX(i) = enemyX(i) + enemyDX(i)
  MOVE SPRITE i + 10, enemyX(i), enemyY(i)
NEXT

See also: SET SPRITE, ENABLE SPRITE, SPRITEX, SPRITEY

↑ Back to top

NEXTFREESPRITE

Sprites · since 1.0.3

Returns the index of the next available (not enabled) sprite slot. Use this function to get a free sprite from the pool without manually tracking which sprites are in use. When a sprite is disabled, its slot becomes available again and can be returned by this function.

Not race-safe: on the server, two player threads can both get the same ID before either enables it. For multiplayer, use RESERVE SPRITE instead — it atomically reserves the slot so concurrent callers can never collide.

Syntax

NEXTFREESPRITE()

Examples

' Get a free sprite for a new bullet
bulletSprite = NEXTFREESPRITE()
IF bulletSprite >= 0 THEN
  ENABLE SPRITE bulletSprite
  SET SPRITE bulletSprite, playerX, playerY, "bullet" TAG "bullet"
END IF
' Spawn enemies using free slots
FOR i = 1 TO 5
  s = NEXTFREESPRITE()
  IF s >= 0 THEN
    ENABLE SPRITE s
    SET SPRITE s, RND * 600, 0, "enemy" TAG "enemy"
  END IF
NEXT
' Check if sprite pool is exhausted
IF NEXTFREESPRITE() = -1 THEN
  PRINT "No free sprites available!"
END IF

See also: ENABLE SPRITE, DISABLE SPRITE, RESERVE SPRITE, SPRITEENABLED, FOR SPRITES, SPRITECOUNT

↑ Back to top

RESERVE SPRITE

Sprites · since 1.21.0

Atomically reserve the next free sprite slot and write the reserved ID (or -1 if the pool is exhausted) into the named variable. Unlike NEXTFREESPRITE, the slot is immediately marked in use so concurrent callers on the CrashNet server can never race onto the same ID. The caller is expected to follow up with an ENABLE SPRITE / LOADIMG / SET SPRITE call to populate the slot; otherwise it stays reserved until explicitly disabled.

In CrashNet, the reserved sprite is tagged with the calling player's slot as its creator — on disconnect, every sprite that player created is automatically swept back into the free pool.

Syntax

RESERVE SPRITE var

Parameters

  • var - Integer variable that receives the reserved sprite ID (or -1)

Examples

' Race-safe version of NEXTFREESPRITE for multiplayer
RESERVE SPRITE newId
IF newId >= 0 THEN
  SET SPRITE newId, 100, 100, "bullet" TAG "bullet" SYNC
END IF
' Host-side enemy spawn — safe even with many concurrent player threads
SUB SpawnEnemy(x, y)
  RESERVE SPRITE e
  IF e >= 0 THEN
    SET SPRITE e, x, y, "enemy" TAG "enemy" SYNC
  END IF
END SUB

See also: NEXTFREESPRITE, ENABLE SPRITE, DISABLE SPRITE, SET SPRITE, CRASHNET

↑ Back to top

SET SPRITE

Sprites · since 1.0.3

Configures a hardware sprite with position, image, and optional modifiers. Supports two forms: (1) Full form with position and texture for initial setup, and (2) Modifier-only form for updating existing sprite properties. Modifiers can be combined freely and include visual transforms (ALPHA, SCALE, ROTATE, FLIP), positioning (LOCATION, PARENT, ORIGIN, LAYER, ZORDER), collision (SOLID, BBOX), physics caps (MAXVELOCITY, MAXACCELERATION), behaviors and patterns (BEHAVIOR, PATTERN), rigging constraints (IK, PIN, LIMIT), tags and metadata (TAG, META), and shaders (SHADER).

ORIGIN(ox, oy) sets the sprite's anchor in relative 0-1 coordinates — the single point the sprite is positioned at, parented at, and rotated around. Default is (0, 0) = top-left; (0.5, 0.5) = center; (1, 1) = bottom-right. It accepts an (ox, oy) pair or a Coordinate. The rotation pivot follows the origin by default, so the sprite rotates around its anchor; an explicit ROTATE(angle, px, py) pivot overrides only the rotation centre, leaving the positioning anchor intact. The collision box follows the origin too: the sprite's rect is placed at position - origin x size, the same place it is drawn, and BBOX fractions are measured from that drawn corner. So ORIGIN(.5, 1) anchors a sprite by the ground under its feet without shifting where it collides.

BEHAVIOR "name" [, @param=value ...] attaches a behavior. Re-issuing a behavior that is ALREADY active with the same parameters is deliberately a no-op, so a DO loop can re-assert it every frame without restarting BMOVE timing, BOSCILLATE phase or re-rolling VARIANCE. Re-issuing it with DIFFERENT parameters rebuilds the running instance with the new values — that is how a sprite reverses itself, e.g. SET SPRITE 0 BEHAVIOR "Move", @vx=-@vx from inside an AFTERCOLLISION handler.

Rigging constraints (IK / PIN / LIMIT) build articulated, constraint-driven sprites on top of the PARENT hierarchy and resolve per-tick (single-player and CrashNet alike). IK applies inverse kinematics to a chain ending at this sprite (the tip): SET SPRITE tip IK TARGET(x, y) LENGTH n rotates the tip plus its n PARENT ancestors so the tip reaches the target, using a FABRIK solver. Optional BEND CW|CCW picks which way interior joints bend, MIX m blends 0..1 between the FK pose and the solved pose, and ITERATIONS k caps solver passes. A bare TARGET on an already-configured tip just re-aims it (other settings persist); IK NONE removes the constraint. Declaring IK auto-sets RIGID on the chain links; give each chain member ORIGIN at its joint so it rotates around the correct point. PIN blends a sprite's resolved position toward a target each tick (SET SPRITE id PIN TARGET(x, y) [MIX m] / PIN NONE) — independent of the rotation chain. LIMIT clamps a child's local offset to a range or locks it to one axis for mechanical rigs (SET SPRITE id LIMIT [X min, max] [Y min, max] [AXIS X|Y] / LIMIT NONE).

Not for a bound polygon. A polygon created with SET POLYGON has a sprite half — it is where its visibility, alpha, layer, pose, behavior and collider live — but it sits in the polygon band of the sprite ID space, not at the raw id. SET SPRITE 3 addresses sprite 3 and does not reach polygon 3, silently. Every modifier here that means something for a shape is accepted by SET POLYGON itself; use that.

Modifiers are separated by spaces, not commas. The comma before the FIRST modifier is optional — SET SPRITE 0, x, y, "hero", 3, ALPHA(0.5) and ... 3 ALPHA(0.5) are both fine — but there is no comma between modifiers: ALPHA(0.5) SCALE(2, 2) ROTATE(45). Commas inside a modifier's own parentheses are its arguments and are unrelated.

Syntax

SET SPRITE n, x, y, imageName$ [, frame] [modifiers]
SET SPRITE n [modifiers]
(LOCATION, SCALE, MAXVELOCITY and MAXACCELERATION each accept a coordinate: e.g. LOCATION(x,y), LOCATION(coord))
ORIGIN(ox, oy)   ' anchor in relative 0-1 coords (also accepts a Coordinate)
IK TARGET(x, y) LENGTH n [BEND CW|CCW] [MIX m] [ITERATIONS k]   |   IK NONE
PIN TARGET(x, y) [MIX m]   |   PIN NONE
LIMIT [X min, max] [Y min, max] [AXIS X|Y]   |   LIMIT NONE

Parameters

  • n - Sprite slot number (0-99,999)
  • x, y - Position coordinates (relative to parent if PARENT is set)
  • imageName$ - Texture/image name (loaded with LOADIMG)
  • frame - Optional sprite sheet frame number (0-based)
  • Visual modifiers
  • ALPHA(value) - Transparency (0.0-1.0)
  • SCALE(sx, sy) - Scale factors (1.0 = original, 2.0 = double)
  • SCALETO(w, h) - Scale to target pixel dimensions
  • ROTATE(angle [, originX, originY]) - Rotation in degrees with optional origin (0-1, default 0.5,0.5)
  • FLIPX - Horizontal flip
  • FLIPY - Vertical flip
  • FRAME n - Show spritesheet cell n (0-based) without re-specifying position or texture. This is the canonical way to flip a sprite's frame: SET SPRITE 0 FRAME 2. Equivalent to the positional frame in the full form SET SPRITE n, x, y, image$, n, but usable on its own to animate an existing sprite.
  • RESET - Reset to defaults (alpha=1, scale=1,1, rotation=0, no flip), clears behaviors/patterns. Preserves location, frame, texture.
  • Positioning modifiers
  • LOCATION(x, y) - Update sprite position (modifier-only form). A 3-component coordinate LOCATION(x, y, z) also sets elevation (z = height above a plane layer); a 2D coordinate leaves elevation unchanged.
  • ELEVATION(z) - Height above the plane layer the sprite sits on (AFFINE/PERSPECTIVE), in world units; 0 = on the floor. Lifts the sprite's drawn position toward the camera (foreshortened by distance) while keeping its footprint and size; no visible effect on a flat layer. Animate with the BELEVATE behavior; query with SPRITEELEVATION(n). Cleared to 0 by RESET.
  • PARENT(spriteNum) [RIGID] - Parent sprite for relative positioning. Child moves with parent. By default the link is translation-only: the child follows the parent's position but keeps its own rotation (does NOT revolve around the parent when the parent rotates). Add RIGID to make it a rigid attachment — the child orbits the parent on the parent's rotation and inherits the parent's spin (a true bone/joint). Use PARENT NONE to detach. Scale is not inherited in either mode.
  • PARENT POLYGON(polygonHandle) [RIGID] - Parent a BOUND POLYGON, named by its own 0-based handle rather than by a raw sprite id. A polygon's slot IS a sprite, so the link behaves exactly as PARENT(spriteNum) does — but the two number spaces are different and neither can be guessed from the value: PARENT(0) is sprite 0, PARENT POLYGON(0) is polygon 0. This is the only way to name another shape's slot from ordinary code; inside a collision handler index_a already IS the slot. RIGID and NONE work the same as for a sprite parent.
  • PARENT NONE - Clear parent, make sprite independent
  • LAYER layerNum - Which layer the sprite renders above
  • ZORDER n - Z-order within same layer (higher = on top)
  • Collision modifiers
  • SOLID - Makes sprite interact with solid boundaries (pushback on collision)
  • SOLID SPRITES("tag1", "tag2", ...) - Boundaries + automatic pushback against sprites with matching tags. One-sided: this sprite stops, tagged sprites are immovable.
  • SOLID SPRITES() - Clear sprite-to-sprite collision tags (keep boundary solid)
  • GHOST - Clear all solid collision (boundary and sprite-to-sprite)
  • BBOX(x1, y1, x2, y2) - Custom collision box using relative coordinates (0-1). Default (0,0,1,1) = full bounds. Affects all collision: boundary pushback, sprite-to-sprite pushback, SPRITECOLLISION(), and SPRITETOUCHING$().
  • BBOX range - Same custom collision box supplied as a single relative-0–1 Range value instead of four numbers, e.g. BBOX boxes(curFrame) using a Range from LOADIMG ... SLICES. Equivalent to the four-argument form.
  • Physics cap modifiers
  • MAXVELOCITY(x, y) - Caps velocity to ±x and ±y per tick. Use MAXVELOCITY NONE to remove.
  • MAXACCELERATION(x, y) - Caps acceleration to ±x and ±y per tick. Use MAXACCELERATION NONE to remove.
  • Behavior modifiers
  • BEHAVIOR "name" [, @param=value, ...] - Add a behavior with optional parameter overrides
  • BEHAVIOR DELETE "name" - Remove a specific behavior by name
  • BEHAVIOR NONE - Clear all behaviors from sprite
  • Pattern modifiers
  • PATTERN "name" [, @param=value, ...] - Set a pattern (replaces any existing pattern)
  • PATTERN DELETE - Remove active pattern
  • PATTERN NONE - Same as DELETE
  • Tag & metadata modifiers
  • TAG "tagName" - Set sprite tag for identification (used in FOR SPRITES, FOR COLLISIONS)
  • META value - Store single metadata value
  • META "key", value - Store key-value metadata
  • Shader modifiers
  • SHADER "name" [, @param=value, ...] - Apply shader with optional parameters
  • SHADER NONE - Remove shader from sprite
  • CrashNet sync modifiers (multiplayer only, no-op in single-player)
  • SYNC - Mark sprite for cross-client broadcast (global, visible to every player and every room). The server sends this sprite's state updates to all connected clients every tick.
  • SYNC("RoomName") - Same as SYNC but room-scoped: only players whose ME.ROOM$ equals "RoomName" receive broadcasts for this sprite, AND the sprite only collides / interacts (BFOLLOW, BORBIT, sprite-vs-sprite SOLID) with other sprites in the same room (or with global, unscoped sprites). Use this for MMORPG-style multi-room servers where Room A's NPCs shouldn't drift into Room B.
  • NOSYNC - Stop broadcasting this sprite. The sprite remains on the server/creator; clients hold whatever state they last received. Leaves the sprite's room scope unchanged so a later SYNC re-uses it.

Examples

' Basic sprite setup
ENABLE SPRITE 0
SET SPRITE 0, 100, 100, "player"
' With frame and modifiers
SET SPRITE 0, x, y, "player", 3, ALPHA(0.5) SCALE(2, 2)
' Modifier-only updates
SET SPRITE 0 LOCATION(150, 200)
SET SPRITE 0 ALPHA(0.5) FLIPX
SET SPRITE 0 ROTATE(45)
' Flip an existing sprite's spritesheet cell (e.g. animating mouth shapes)
SET SPRITE 0 FRAME 2
' Parent-child hierarchy
SET SPRITE 1, 16, 0, "sword", PARENT(0)  ' Sword follows player (translation only)
SET SPRITE 1 PARENT NONE  ' Detach
' RIGID attachment: a forearm that swings around the shoulder as the
' upper arm rotates (orbit + inherited spin), the basis for articulated rigs.
SET SPRITE 2, 20, 0, "forearm"
SET SPRITE 2 PARENT(upperArm) RIGID
' Collision with boundaries
SET SPRITE 0 SOLID
SET SPRITE 0, x, y, "player" SOLID BBOX(0.2, 0.2, 0.8, 0.8)
' BBOX from an Aseprite slice Range (per-frame hitbox)
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32) SLICES boxes()
SET SPRITE 0, x, y, "HERO"
SET SPRITE 0 BBOX boxes(curFrame)
' Collision with sprites by tag (automatic pushback)
SET SPRITE player SOLID SPRITES("wall", "npc", "rock")
SET SPRITE npc TAG "npc"  ' This sprite acts as an obstacle
' Clear sprite collision but keep boundary collision
SET SPRITE player SOLID SPRITES()
' Clear all collision
SET SPRITE player GHOST
' Physics caps
SET SPRITE 0 MAXVELOCITY(5, 5) MAXACCELERATION(0.2, 0.5)
SET SPRITE 0 MAXVELOCITY NONE  ' Remove cap
' Behaviors
SET SPRITE 0 BEHAVIOR "walkRight"
SET SPRITE 0 BEHAVIOR "moveToPoint", @targetX=100, @targetY=200
SET SPRITE 0 BEHAVIOR DELETE "walkRight"
SET SPRITE 0 BEHAVIOR NONE  ' Clear all
' Patterns
SET SPRITE 0 PATTERN "walkCycle"
SET SPRITE 0 PATTERN "chase", @targetX=enemyX
SET SPRITE 0 PATTERN NONE
' Tags for collision handling
SET SPRITE 0, 100, 100, "hero" TAG "player"
SET SPRITE 1, 200, 100, "monster" TAG "enemy"
' Metadata for game stats
SET SPRITE 0 META "health", 100
SET SPRITE 0 META "lives", 3
' Shaders
SET SPRITE 0 SHADER "Greyscale", @intensity=0.8
SET SPRITE 0 SHADER NONE
' Full example combining many modifiers
SET SPRITE 0, 160, 100, "player", 1, ALPHA(0.9) SCALE(2, 2) ROTATE(angle) SOLID LAYER 1 TAG "player"
' ORIGIN: anchor the sprite at its center so it positions and rotates
' around its middle (instead of the default top-left corner).
SET SPRITE 0, 160, 120, "ball" ORIGIN(0.5, 0.5)
SET SPRITE 0 ROTATE(45)            ' spins around the center now
SET SPRITE 0 ORIGIN(centerCoord)  ' ORIGIN also accepts a Coordinate value
' IK chain: a tip ball driven to the mouse, the rest of the chain following.
SET SPRITE 0, 320, 240, "ball" ORIGIN(0.5, 0.5)   ' root anchor
FOR i = 1 TO 5
  SET SPRITE i, 320 + i * 40, 240, "ball" ORIGIN(0.5, 0.5)
  SET SPRITE i PARENT(i - 1) RIGID
NEXT i
SET SPRITE 5 IK TARGET(MOUSEX, MOUSEY) LENGTH 5 BEND CCW
' ...then each frame just re-aim the tip:
SET SPRITE 5 IK TARGET(MOUSEX, MOUSEY)
' PIN: an eye sprite that eases toward the cursor each tick.
SET SPRITE 7 PIN TARGET(MOUSEX, MOUSEY) MIX 0.3
SET SPRITE 7 PIN NONE             ' stop tracking

' LIMIT: a piston that slides only along its X axis between 0 and 20.
SET SPRITE 8 PARENT(0) RIGID
SET SPRITE 8 LIMIT X 0, 20 AXIS X

See also: ENABLE SPRITE, MOVE SPRITE, HIDE SPRITE, SHOW SPRITE, BEHAVIOR, PATTERN, SHADER, SPRITETAG$, GETSPRITEMETA, HASBEHAVIOR, HASPATTERN

↑ Back to top

SHOW SPRITE

Sprites · since 1.0.3

Makes a previously hidden hardware sprite visible again. Only works if the sprite is enabled.

Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.

Syntax

SHOW SPRITE spriteNum

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

SHOW SPRITE 0  ' Make sprite visible
' Spawn enemy at position:
ENABLE SPRITE enemySlot
SET SPRITE enemySlot, spawnX, spawnY, "enemy"
SHOW SPRITE enemySlot
' Toggle visibility:
IF SPRITEVISIBLE(0) THEN HIDE SPRITE 0 ELSE SHOW SPRITE 0

See also: HIDE SPRITE, ENABLE SPRITE, SPRITEVISIBLE

↑ Back to top

SPRITE RESET

Sprites · since 1.2.7

Resets a sprite's physics state and orientation, and clears all behaviors and patterns. Zeroes velocity and acceleration, removes velocity/acceleration caps, resets rotation to 0 and flip to false, and detaches all behaviors and patterns. Does NOT affect the sprite's position, texture, frame, alpha, scale, or visibility. Use this to stop a sprite's movement and clear automated behaviors while keeping it where it is.

Syntax

SPRITE spriteNum RESET

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

SPRITE 0 RESET  ' Stop all movement and clear behaviors
' Stop player when they die:
SPRITE playerSprite RESET
SET SPRITE playerSprite, SPRITEX(playerSprite), SPRITEY(playerSprite), "player_dead"
' Reset enemy before assigning new patrol:
SPRITE enemyId RESET
SET SPRITE enemyId BEHAVIOR "patrol"

See also: SET SPRITE, MAXVELOCITY, MAXACCELERATION, SPRITEVELOCITYX, SPRITEVELOCITYY

↑ Back to top

SPRITEBBOX

Sprites · since 1.45.0

Returns a sprite's bounding box in WORLD pixels as a Range (x1,y1)-(x2,y2), ready for manual hit-detection. With no slice name it returns the sprite's active collision box — its custom BBOX, or the full frame if none is set. With a slice name it returns that Aseprite slice's box at the sprite's CURRENT animation frame. World coordinates account for the sprite's position (including any parent), its scale and its ORIGIN. A bbox is relative to the IMAGE and ORIGIN does not reshape it — but ORIGIN moves the image, and the box travels with it, so the rectangle returned here is exactly the one collision tests.

The slice geometry comes from the Aseprite (.ase) file the sprite's image was loaded from. Naming a slice that doesn't exist on the image — or a frame that no slice key covers — is a loud runtime error listing the available slice names. Use this for secondary hit checks (a hurtbox vs. a hitbox) beyond the single automatic collision box that AUTOBBOX drives.

Syntax

SPRITEBBOX(id [, sliceName$])

Parameters

  • id - Sprite id
  • sliceName$ - Optional Aseprite slice name (case-insensitive). Omit for the sprite's active collision box.

Examples

' Active collision box of sprite 0, in world pixels
body = SPRITEBBOX(0)
' A named slice's box at the sprite's current frame (world space)
' for a secondary, manual hit check
hurt = SPRITEBBOX(player, "hurtbox")

See also: IMAGEBBOX, BEHAVIOR, SET SPRITE, LOADIMG

↑ Back to top

SPRITECOLLISIONID

Sprites · since 1.57.0

Returns the sprite ID of the first sprite tagged tag$ that spriteNum is colliding with, or -1 if it is colliding with none.

The difference from SPRITECOLLISION, which answers only yes or no: this tells you WHICH sprite was hit, so a handler can act on it — score it, destroy it, read its tag. Test against -1 rather than 0, because 0 is a valid sprite slot.

Syntax

SPRITECOLLISIONID(spriteNum, tag$)

Parameters

  • spriteNum - Sprite slot number to test
  • tag$ - Tag to match against. Matched case-insensitively.

Example

hitId = SPRITECOLLISIONID(playerId, "coin")
IF hitId <> -1 THEN
  DISABLE SPRITE hitId
  score = score + 10
END IF

See also: SPRITECOLLISION, SPRITETAG$, SET SPRITE

↑ Back to top

SPRITECOUNT

Sprites · since 1.4.5

Returns the number of currently enabled sprites that have the specified tag. This is useful for checking how many enemies remain, counting collectibles, or managing spawn limits. Only enabled sprites are counted.

Syntax

SPRITECOUNT("tagName")

Parameters

  • tagName - String expression for the tag to count (case-insensitive)

Examples

enemyCount = SPRITECOUNT("enemy")
PRINT "Enemies remaining:"; enemyCount
' Check for level completion
IF SPRITECOUNT("enemy") = 0 THEN
  PRINT "Level complete!"
  CALL NextLevel()
END IF
' Limit enemy spawn count
IF SPRITECOUNT("enemy") < 10 THEN
  CALL SpawnEnemy()
END IF
' Count multiple types
totalBaddies = SPRITECOUNT("enemy") + SPRITECOUNT("boss")
PRINT "Total threats:"; totalBaddies
' Game over check
IF SPRITECOUNT("player") = 0 THEN
  CALL GameOver()
END IF

See also: FOR SPRITES, SPRITETAG$, SET SPRITE (TAG), NEXTFREESPRITE

↑ Back to top

SPRITEDIRECTION

Sprites · since 1.0.3

Returns the last movement direction of a sprite in degrees. 0 = right, 90 = down, 180 = left, 270 = up. Direction is updated whenever the sprite moves via behaviors. Returns 0 if sprite has not moved.

Syntax

SPRITEDIRECTION(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

dir = SPRITEDIRECTION(0)  ' Get player's facing direction
' Choose animation based on direction:
IF dir < 45 OR dir >= 315 THEN
  ' Facing right
  SET SPRITE 0, x, y, "player_right"
ELSEIF dir >= 45 AND dir < 135 THEN
  ' Facing down
  SET SPRITE 0, x, y, "player_down"
ELSEIF dir >= 135 AND dir < 225 THEN
  ' Facing left
  SET SPRITE 0, x, y, "player_left"
ELSE
  ' Facing up
  SET SPRITE 0, x, y, "player_up"
END IF
' Point sprite in direction of movement:
SET SPRITE 0, x, y, "arrow", ROTATE(SPRITEDIRECTION(0))

See also: SPRITEVELOCITYX, SPRITEVELOCITYY, BEHAVIOR

↑ Back to top

SPRITEDIST

Sprites · since 1.57.0

Returns the straight-line distance in pixels between two sprites' positions, as a Float.

Measured between the sprites' anchors — the same point SPRITEX / SPRITEY report and LOCATION places — so ORIGIN moves what is measured. Returns 0 if either sprite does not exist.

Syntax

SPRITEDIST(spriteA, spriteB)

Parameters

  • spriteA - First sprite slot number
  • spriteB - Second sprite slot number

Examples

IF SPRITEDIST(playerId, enemyId) < 32 THEN
  PRINT "too close"
END IF
' Chase only when in range
IF SPRITEDIST(enemyId, playerId) < 200 THEN
  SET SPRITE enemyId BEHAVIOR "chase"
END IF

See also: SPRITEX, SPRITEY, SPRITECOLLISION, DISTANCE

↑ Back to top

SPRITEELEVATION

Sprites · since 1.40.0

Returns a sprite's current ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE), in world units; 0 means on the floor. Useful in behavior/pattern WHEN conditions, e.g. landing detection. Elevation is set with SET SPRITE id ELEVATION(z) or animated with the BELEVATE behavior.

Syntax

SPRITEELEVATION(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

z = SPRITEELEVATION(0)  ' How high is the player off the floor?
REM Landing detection inside a pattern (THISSPRITE = the running sprite)
PATTERN "Jumper" LOOP
  "Land" WHEN SPRITEELEVATION(THISSPRITE) <= 0
END PATTERN

See also: SET SPRITE, BEHAVIOR, SET LAYER, SPRITEX, SPRITEY

↑ Back to top

SPRITEENABLED

Sprites · since 1.0.3

Returns 1 if the sprite slot is enabled, 0 otherwise. An enabled sprite can be configured and rendered. Use this to check sprite slot availability.

Syntax

SPRITEENABLED(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

IF SPRITEENABLED(0) THEN PRINT "Sprite 0 is enabled"
' Find first available sprite slot:
FOR i = 0 TO 999
  IF NOT SPRITEENABLED(i) THEN
    freeSlot = i
    EXIT FOR
  END IF
NEXT
' Count active sprites:
active = 0
FOR i = 0 TO 999
  IF SPRITEENABLED(i) THEN active = active + 1
NEXT
PRINT "Active sprites:" + STR$(active)
' Safe cleanup:
FOR i = 0 TO 999
  IF SPRITEENABLED(i) THEN DISABLE SPRITE i
NEXT

See also: SPRITEVISIBLE, ENABLE SPRITE, DISABLE SPRITE

↑ Back to top

SPRITEHEIGHT

Sprites · since 1.0.3

Returns the current height of a hardware sprite in pixels. The returned value accounts for scaling (base texture height multiplied by the sprite's Y scale factor). Returns 0 if the sprite does not exist.

Syntax

SPRITEHEIGHT(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

h = SPRITEHEIGHT(0)  ' Get player sprite height
' Position sprite so its bottom touches the floor:
SET SPRITE 0, x, floorY - SPRITEHEIGHT(0), "player"
' Bottom edge of a sprite (equivalent to SPRITEY(id, 1)):
bottomEdge = SPRITEY(0) + SPRITEHEIGHT(0)

See also: SPRITEWIDTH, SPRITEX, SPRITEY, SET SPRITE

↑ Back to top

SPRITETAG$

Sprites · since 1.0.3

Returns the tag string associated with the specified sprite. Returns an empty string if no tag has been set.

Syntax

SPRITETAG$(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

PRINT SPRITETAG$(0)  ' Might print "player"
IF SPRITETAG$(a) = "enemy" THEN
  PRINT "Hit an enemy!"
END IF
' Collision handling by tag
FOR COLLISIONS a, b
  IF SPRITETAG$(a) = "player" AND SPRITETAG$(b) = "enemy" THEN
    PRINT "Player hit enemy!"
  END IF
NEXT COLLISION

See also: SET SPRITE (TAG), FOR COLLISIONS, FOR SPRITES

↑ Back to top

SPRITEVISIBLE

Sprites · since 1.0.3

Returns 1 if the sprite is both enabled AND visible, 0 otherwise. A sprite is not visible if it has been hidden with HIDE SPRITE or if it is not enabled.

Syntax

SPRITEVISIBLE(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

IF SPRITEVISIBLE(0) THEN PRINT "Sprite is visible"
' Count visible sprites:
visible = 0
FOR i = 0 TO 999
  IF SPRITEVISIBLE(i) THEN visible = visible + 1
NEXT
PRINT "Visible sprites:" + STR$(visible)
' Toggle visibility:
IF SPRITEVISIBLE(0) THEN HIDE SPRITE 0 ELSE SHOW SPRITE 0

See also: SPRITEENABLED, HIDE SPRITE, SHOW SPRITE

↑ Back to top

SPRITEWIDTH

Sprites · since 1.0.3

Returns the current width of a hardware sprite in pixels. The returned value accounts for scaling (base texture width multiplied by the sprite's X scale factor). Returns 0 if the sprite does not exist.

Syntax

SPRITEWIDTH(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

w = SPRITEWIDTH(0)  ' Get player sprite width
' Center a sprite horizontally on screen:
SET SPRITE 0, (SCREENWIDTH - SPRITEWIDTH(0)) / 2, y, "player"
' Check if sprite fits within a gap:
IF SPRITEWIDTH(0) < gapSize THEN canPass = 1
' Right edge of a sprite (equivalent to SPRITEX(id, 1)):
rightEdge = SPRITEX(0) + SPRITEWIDTH(0)

See also: SPRITEHEIGHT, SPRITEX, SPRITEY, SET SPRITE

↑ Back to top

SPRITEX

Sprites · since 1.0.3

Returns the X position of a hardware sprite. By default returns the left edge (x position). When relativeX is provided (0-1 range), returns the translated X coordinate at that relative position across the sprite's width. For example, 0.5 returns the horizontal center.

Bound polygons take a slot id, not a polygon id. This function reads the raw sprite id you pass, and a polygon's sprite half sits in the polygon band of that space — so SPRITEX(3) reports sprite 3, not polygon 3. Where you DO have the slot, this reports the shape's anchor: its extent's left edge offset by ORIGIN, the same point SET POLYGON n, LOCATION(...) places. A collision handler is the usual place that happens — index_a is the slot the shape occupies, so SPRITEX(index_a) reads the shape that was hit. Note that a shape's extent comes from its live points, so a ROTATING shape's bounding box grows and shrinks as it turns; anchor it at ORIGIN(0.5, 0.5) for a stable point.

Syntax

SPRITEX(spriteNum [, relativeX])

Parameters

  • spriteNum - Sprite slot number (0-999 for user sprites; emitted particles use 100,000+)
  • relativeX - Optional. Relative position (0-1) across sprite width. 0 = left edge (default), 0.5 = center, 1 = right edge

Examples

x = SPRITEX(0)  ' Get player sprite left edge X position
centerX = SPRITEX(0, 0.5)  ' Get sprite horizontal center
rightX = SPRITEX(0, 1)  ' Get sprite right edge
' Check if sprite center is on screen:
IF SPRITEX(0, 0.5) >= 0 AND SPRITEX(0, 0.5) < 320 THEN onScreen = 1
' Distance between sprite centers:
dx = SPRITEX(0, 0.5) - SPRITEX(1, 0.5)
dy = SPRITEY(0, 0.5) - SPRITEY(1, 0.5)
distance = SQR(dx * dx + dy * dy)
' Display sprite center position:
LOCATE 1, 1: PRINT "Center: " + STR$(SPRITEX(0, 0.5)) + "," + STR$(SPRITEY(0, 0.5))

Notes

Works for any live sprite, including emitted particles (IDs 100,000+). Querying a sprite that doesn't exist is a loud runtime error, not a silent 0 — a missing sprite would otherwise read as a valid coordinate at the origin. Guard with IF SPRITEENABLED(id) THEN ... first. A common cause is reading an emitted particle's position after its TTL has expired (e.g. from an async BEVENT handler); give the particle a longer TTL or fire the event with BEVENT "..." SYNC so the handler runs before cleanup.

See also: SPRITEY, SPRITEWIDTH, SPRITEHEIGHT, SPRITEVISIBLE, SPRITEENABLED, MOVE SPRITE

↑ Back to top

SPRITEY

Sprites · since 1.0.3

Returns the Y position of a hardware sprite. By default returns the top edge (y position). When relativeY is provided (0-1 range), returns the translated Y coordinate at that relative position across the sprite's height. For example, 0.5 returns the vertical center.

Bound polygons take a slot id, not a polygon id. This function reads the raw sprite id you pass, and a polygon's sprite half sits in the polygon band of that space — so SPRITEX(3) reports sprite 3, not polygon 3. Where you DO have the slot, this reports the shape's anchor: its extent's top edge offset by ORIGIN, the same point SET POLYGON n, LOCATION(...) places. A collision handler is the usual place that happens — index_a is the slot the shape occupies, so SPRITEX(index_a) reads the shape that was hit. Note that a shape's extent comes from its live points, so a ROTATING shape's bounding box grows and shrinks as it turns; anchor it at ORIGIN(0.5, 0.5) for a stable point.

Syntax

SPRITEY(spriteNum [, relativeY])

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • relativeY - Optional. Relative position (0-1) across sprite height. 0 = top edge (default), 0.5 = center, 1 = bottom edge

Examples

y = SPRITEY(0)  ' Get player sprite top edge Y position
centerY = SPRITEY(0, 0.5)  ' Get sprite vertical center
bottomY = SPRITEY(0, 1)  ' Get sprite bottom edge
' Check if sprite bottom hit floor:
IF SPRITEY(0, 1) > 180 THEN gameOver = 1
' Vertical collision check using centers:
IF ABS(SPRITEY(0, 0.5) - SPRITEY(1, 0.5)) < 16 THEN verticalOverlap = 1
' Check if sprite is fully on screen:
IF SPRITEY(0) >= 0 AND SPRITEY(0, 1) < 200 THEN fullyVisible = 1

Notes

Works for any live sprite, including emitted particles (IDs 100,000+). Querying a sprite that doesn't exist is a loud runtime error, not a silent 0 — a missing sprite would otherwise read as a valid coordinate at the origin. Guard with IF SPRITEENABLED(id) THEN ... first. A common cause is reading an emitted particle's position after its TTL has expired (e.g. from an async BEVENT handler); give the particle a longer TTL or fire the event with BEVENT "..." SYNC so the handler runs before cleanup.

See also: SPRITEX, SPRITEWIDTH, SPRITEHEIGHT, SPRITEVISIBLE, SPRITEENABLED, MOVE SPRITE

↑ Back to top

Behaviors

AFTERCOLLISION

Behaviors · since 1.53.0

Inside a BEHAVIOR block, runs inline BASIC after collision resolution — the closing half of ONCOLLISION. The block is hoisted to an anonymous handler and fired by the behavior engine.

A tick goes: contact, then resolution, then the next tick. ONCOLLISION runs on contact, before resolution, so impact_vx / impact_vy hold the velocity the sprite struck at. AFTERCOLLISION runs between resolution and the next tick, once the engine has stopped or reflected the sprite — which makes it the place to decide what to do about the hit, such as reversing direction by re-issuing the behavior.

Solid contacts always resolve, so both edges land on the tick of the hit and AFTERCOLLISION does NOT wait for the sprite to move away. That matters because a sprite driven by BVELOCITY never stops pressing — BVELOCITY re-applies every tick — so a sprite held against a wall would never separate at all.

A ghost contact has no resolution to follow, so its collision stays in effect for as long as the pair overlaps: a sprite can wander around inside an otherwise-solid block indefinitely. Its AFTERCOLLISION fires on the tick the overlap finally ends. Calling GHOST from inside ONCOLLISION switches the contact to this behavior, which is what makes phase-through work.

Edge-triggered either way, and it fires exactly once per contact — never per tick.

The scoped values describe the contact as it began — the same edge$, tags and impact velocity ONCOLLISION saw.

The same restrictions apply as ONCOLLISION: no blocking statements, changes take effect next tick, handler bodies scope like a SUB, and INCLUDES stacks handlers.

Syntax

AFTERCOLLISION
  statements
END AFTERCOLLISION

Parameters

  • index_a - (in scope) This sprite's ID. Always the sprite the behavior is running on.
  • index_b - (in scope) The other sprite's ID, or -1 when the contact was with a boundary.
  • tag_a$ - (in scope) This sprite's tag.
  • tag_b$ - (in scope) The other sprite's tag, or the boundary's tag when index_b is -1.
  • edge$ - (in scope) The face struck when the contact began.
  • impact_vx - (in scope) Horizontal velocity recorded when the contact began.
  • impact_vy - (in scope) Vertical velocity recorded when the contact began.
  • THISSPRITE is also set to index_a.

Examples

' Walking off a ledge: gravity comes back
BEHAVIOR "grounded"
  AFTERCOLLISION
    IF tag_b$ = "ground" THEN SET SPRITE index_a BEHAVIOR "falling"
  END AFTERCOLLISION
END BEHAVIOR
' Leaving a lava boundary stops the burn
BEHAVIOR "burnable"
  AFTERCOLLISION
    IF tag_b$ = "lava" THEN STOP TIMER "burnTick"
  END AFTERCOLLISION
END BEHAVIOR
' Become solid again once clear of whatever we phased through.
' SOLID SPRITES(...) rather than bare SOLID: GHOST cleared the sprite
' barrier tags, and plain SOLID restores only boundary solidity.
BEHAVIOR "phaser"
  ONCOLLISION
    SET SPRITE index_a GHOST
  END ONCOLLISION
  AFTERCOLLISION
    SET SPRITE index_a SOLID SPRITES("wall")
  END AFTERCOLLISION
END BEHAVIOR

See also: ONCOLLISION, BEHAVIOR, BOUNDARY ADD, SET SPRITE, FOR COLLISIONS

↑ Back to top

BEHAVIOR

Behaviors · since 1.0.5

Defines declarative sprite animation and movement behaviors. A single BEHAVIOR block can define one named behavior, or multiple named behaviors separated by labels. Behaviors are applied to hardware sprites using SET SPRITE n BEHAVIOR. Behavior commands can use parameterized values with @paramName=defaultValue syntax, allowing the same behavior to be reused with different values. Use INCLUDES to compose behaviors from other defined behaviors - included commands come first, followed by this behavior's commands.

Declaration-time control flow. A behavior body may contain IF/ELSEIF/ELSE/END IF and SELECT CASE/CASE/CASE ELSE/END SELECT to choose which commands are baked into the behavior. These are evaluated once, when the BEHAVIOR statement runs (not per tick) — the matching branch's commands are included in the registered behavior, the rest are skipped. Re-declaring the behavior with different variables re-evaluates the conditions and overwrites the definition. This is distinct from a pattern's runtime WHEN clause, which re-checks each tick. Both block and single-line IF cond THEN command forms are supported, and SELECT CASE supports the same CASE value, CASE a, b, CASE a TO b, CASE IS <op> v, and CASE ELSE forms as the statement-level SELECT.

A behavior can also carry collision handlers — ONCOLLISION and AFTERCOLLISION blocks of ordinary BASIC that the engine fires when this sprite starts and stops touching something. They are edge-triggered, so a character landing on the ground runs its handler once rather than every tick it stands there, and they receive the contact details (what was hit, which face, and how fast it was going at the moment of impact). Because they are ordinary blocks, declaration-time IF / SELECT CASE can choose between them.

Behavior names are case-insensitive. SET SPRITE n BEHAVIOR "Walk" finds the behavior defined as "walk", and the same holds everywhere one is named — INCLUDES, a PATTERN step, EMITPARTICLE, HASBEHAVIOR.

The commands

Every command below lives INSIDE a BEHAVIOR ... END BEHAVIOR block. None of them is a statement you can write on its own — that is why they are documented here rather than as separate keywords.

Motion

BVELOCITY

BVELOCITY vx, vy [, vz]

Inside a BEHAVIOR block, contributes a continuous velocity to the sprite. This is a standing movement intent, not a one-shot kick: the engine zeroes the BVELOCITY contribution at the start of every tick and each active BVELOCITY command adds its value back, so several behaviors carrying BVELOCITY are additive rather than last-one-wins. Combine with BACCELERATION (gravity, thrust) for arcs.

Because the contribution is rebuilt every tick, it has no inertia: BFRICTION does not decay it (there is nothing accumulated to decay), and a solid collision does not zero it — the sprite keeps pushing against whatever blocked it for as long as the behavior is active. For the same reason BBOUNCE cannot rebound it: the reflection is applied, then overwritten by the constant on the next tick, so a BVELOCITY-driven sprite presses against a wall rather than bouncing off it.

For a discrete kick — a jump, dash, recoil, or knockback that decays and can be blocked — use BIMPULSE, which seeds the accumulated velocity once and is subject to friction and collision zeroing.

The optional third component vz sets the sprite's vertical (elevation) velocity — height above a plane layer; pairs with BACCELERATION z (gravity) and the ground clamp for jump/fall physics.

  • vx - Horizontal velocity in pixels per tick, re-applied every tick while the behavior is active
  • vy - Vertical velocity in pixels per tick, re-applied every tick while the behavior is active (negative = up)
  • vz - (optional) Vertical (elevation) velocity in world units per tick. Height above a plane layer; gravity (BACCELERATION z) and the ground (elevation 0) act on it.

BMOVE

BMOVE [ABSOLUTEX] dx, [ABSOLUTEY] dy [, [ABSOLUTEZ] dz]

Inside a BEHAVIOR block, defines constant movement per tick. By default, all arguments are per-tick deltas applied to the sprite's current position. Multiple BMOVE behaviors stack additively per tick.

The optional ABSOLUTEX / ABSOLUTEY / ABSOLUTEZ modifiers (per-axis, applied independently) flip the corresponding argument's interpretation from delta to absolute target — instead of dx adding to the sprite's X each tick, the sprite's X is set to dx. This lets you mix idioms freely: snap one axis while leaving the others to drift, set a column on entry but allow free vertical motion, etc. Axes can be made absolute simultaneously to teleport-snap on every tick.

The optional third component dz moves the sprite's ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE), in world units. Only the resulting elevation affects rendering on a plane layer; on a flat layer it has no visible effect. Omit it for plain 2D movement.

Values can use @paramName=default syntax for parameterized behaviors. All axes accept VARIANCE for per-instance jitter.

  • dx - Horizontal movement in pixels per tick. With ABSOLUTEX, treated as the target X coordinate instead. Supports @param=default and VARIANCE.
  • dy - Vertical movement in pixels per tick. With ABSOLUTEY, treated as the target Y coordinate instead. Supports @param=default and VARIANCE.
  • dz - (optional) Elevation movement in world units per tick. With ABSOLUTEZ, treated as the target elevation instead. Supports @param=default and VARIANCE.
  • ABSOLUTEX (modifier) - Optional, prefixes the X argument. Treats dx as an absolute target X position rather than a per-tick delta.
  • ABSOLUTEY (modifier) - Optional, prefixes the Y argument. Treats dy as an absolute target Y position rather than a per-tick delta.
  • ABSOLUTEZ (modifier) - Optional, prefixes the Z argument. Treats dz as an absolute target elevation rather than a per-tick delta.

BACCELERATION

BACCELERATION dx, dy [, dz]

Inside a BEHAVIOR block, applies continuous acceleration to the sprite. Unlike BMOVE which applies constant velocity, BACCELERATION adds to velocity each tick, creating effects like gravity, thrust, or deceleration. Velocity accumulates over time and persists until the behavior is removed. Use with BVELOCITY for proper jump physics.

The optional third component dz accelerates the sprite's ELEVATION (height above a plane layer) — this is how gravity for the vertical axis works: BACCELERATION 0, 0, -1 pulls a sprite back down to the floor after a BIMPULSE 0, 0, z jump. The z axis integrates into elevation each tick and lands at the ground (elevation 0).

  • dx - Horizontal acceleration in pixels per tick per tick
  • dy - Vertical (screen-Y) acceleration in pixels per tick per tick (positive = down for in-plane gravity)
  • dz - (optional) Elevation acceleration in world units per tick per tick. Negative pulls toward the floor (gravity off a plane layer); pairs with BIMPULSE z for jumps.

BIMPULSE

BIMPULSE ix, iy [, iz]

Inside a BEHAVIOR block, applies a one-shot impulse: adds (ix, iy[, iz]) to the sprite's accumulated accel_velocity exactly once per behavior activation, then becomes a no-op for the rest of that activation. The seeded velocity persists in accel_velocity and decays through BFRICTION (if any) the same way BACCELERATION contributions decay. Honours max_accel_x/max_accel_y so an oversized impulse can't blow past a sprite's configured speed ceiling.

The optional third component iz kicks the sprite's ELEVATION — a true jump off a plane layer: BIMPULSE 0, 0, 16 paired with gravity (BACCELERATION 0, 0, -1) arcs up and lands back on the floor (elevation 0). The vertical (z) axis is a real physics axis: z-velocity integrates into elevation each tick, decelerated by gravity, clamped at the ground.

BIMPULSE is the right primitive for discrete kicks of momentum: jumps, dashes, recoil, knockback. Compare with the related primitives:

  • BACCELERATION is applied every tick (continuous force — gravity, thrust).
  • BVELOCITY overwrites bvelocity every tick (no inertia, ignored by friction — useful for direct movement intent).
  • BMOVE translates the sprite directly with no velocity at all.

To re-fire the impulse, the behavior must be deactivated and re-activated — typically driven from a pattern step or a WHEN trigger. On reactivation, a fresh internal state is built (tick = 0) and the impulse fires again. All axes accept VARIANCE for per-instance jitter, and support @paramName=default parameter references for pattern-driven values.

  • ix - Horizontal impulse in pixels per tick. Added to accel_velocity_x once on the activation tick.
  • iy - Vertical impulse in pixels per tick. Same semantics on the Y axis (positive = down).
  • iz - (optional) Elevation impulse in world units — a vertical jump kick off a plane layer. Added to the z-velocity once; gravity (BACCELERATION z) pulls it back to the ground.
  • All values support VARIANCE(low, high), VARIANCEINT(...), and @paramName=default parameter references.

BFRICTION

BFRICTION x_friction, y_friction [, z_friction]

Inside a BEHAVIOR block, applies universal per-axis friction every tick — independent of collisions. Multiplies the sprite's accumulated accel_velocity by (1 - friction) on each axis. 0 = frictionless (no decay), 1 = instant stop, in between = exponential decay (e.g. 0.1 = lose 10% of accumulated velocity per tick).

BFRICTION is the natural counterpart to BACCELERATION. They reach equilibrium at the terminal velocity ≈ acceleration / friction — useful for modelling air resistance, surface drag, or maximum-speed clamps via gentle decay rather than a hard cap.

The optional third coefficient z_friction decays vertical (ELEVATION) velocity each tick the same way — air resistance on jumps and falls, or a terminal fall speed when paired with BGRAVITY (terminal ≈ gravity / z_friction).

Does not affect BVELOCITY (which is reset and re-derived each tick from the BVELOCITY commands themselves, so friction on it would be a no-op) or BMOVE (constant-motion intent). All axes accept VARIANCE for per-instance jitter — useful when you want a swarm of objects to settle at slightly different speeds.

  • x_friction - Friction coefficient on the X axis. 0 = no decay, 1 = instant stop, between = exponential decay per tick.
  • y_friction - Friction coefficient on the Y axis. Same scale.
  • z_friction - (optional) Friction on the elevation axis — air resistance on vertical motion; pairs with BGRAVITY for a terminal fall speed.
  • All values support VARIANCE(low, high) and VARIANCEINT(...) for per-instance randomness.

BGRAVITY

BGRAVITY strength

Inside a BEHAVIOR block, applies a constant downward acceleration to the sprite's ELEVATION every tick — convenience sugar for the vertical component of BACCELERATION (0, 0, -strength). Use a positive strength; the sprite accelerates toward the ground (elevation 0). This is the everyday way to give plane-layer sprites weight: pair it with BIMPULSE z for jumps and BBOUNCE z for bouncing. strength supports VARIANCE and @paramName=default. Only meaningful on a PERSPECTIVE/AFFINE plane layer (where elevation is rendered); with no elevation it has no visible effect.

  • strength - Downward acceleration in elevation units per tick per tick. Positive pulls the sprite toward the ground. Supports VARIANCE(low, high) and @paramName=default.

BELEVATE

BELEVATE from TO to DURATION ticks [LOOP]

Inside a BEHAVIOR block, tweens a sprite's ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE), in world units — from from to to over ticks. Mirrors BALPHA/BSCALE/BROTATE. Sequence two BELEVATE behaviors in a looping PATTERN (rise then fall) for a continuous hop, or use a single LOOP for a repeating lift. Elevation only changes how the sprite is drawn on a plane layer (it rides toward the camera eye-height, foreshortened by distance); on a flat layer it has no visible effect. Query the live value with SPRITEELEVATION(id), e.g. in a WHEN condition for landing detection.

  • from - Starting elevation (world units; 0 = on the floor)
  • to - Ending elevation (world units)
  • ticks - Duration in ticks/frames
  • LOOP - Repeat the tween indefinitely

BBOUNCE

BBOUNCE x_restitution, y_restitution [, z_restitution]

Inside a BEHAVIOR block, applies bounce physics to the sprite when it collides with a solid boundary. Each axis takes a restitution coefficient: 0 = absorb (sprite stops, identical to the default zero-on-block behaviour), 1 = perfect elastic reflection, between 0 and 1 = partial energy loss per bounce.

BBOUNCE reflects the sprite's accumulated velocity — the momentum built up by BIMPULSE, BACCELERATION and BGRAVITY. That is the motion that carries across ticks, so it is what a bounce can meaningfully reverse: BIMPULSE 8, 0 + BBOUNCE 0.9, 0.9 sends a ball off a wall and back, losing 10% per hit.

BVELOCITY is not a useful bounce source. It is a constant re-applied every tick by design, so although the reflection is applied, the next tick rebuilds the original value and overwrites it — a sprite whose only motion is BVELOCITY does not rebound, it presses steadily against whatever it hit. Build bouncing things out of BIMPULSE (a one-shot kick) or BACCELERATION / BGRAVITY (continuous force).

For SLOPE boundaries, the velocity is reflected along the slope's normal vector and scaled by the average of x_restitution and y_restitution — so a ball hitting a 45° SE slope from above will bounce up-and-to-the-right cleanly. AABB walls reflect axis-by-axis: LEFT/RIGHT edges use x_restitution, TOP/BOTTOM edges use y_restitution. Both restitutions support VARIANCE for per-instance jitter (good for natural-feeling particle systems).

The optional third coefficient z_restitution bounces the ELEVATION axis off the ground (elevation 0) — a ball dropping under BGRAVITY (or BACCELERATION z) on a plane layer rebounds upward scaled by z_restitution, and with z < 1 loses energy each hit and settles. z_restitution 0 (or omitting it) = land and stop, the prior 2D behaviour.

When BBOUNCE is not active, sprites use the default behaviour: accel_velocity in the blocked direction is zeroed and BVELOCITY persists. Adding BBOUNCE 0, 0 explicitly is equivalent to the default for the blocked-axis component, and additionally zeroes the BVELOCITY component for the remainder of that tick (so SPRITEVELOCITYX / SPRITEVELOCITYY read zero) — though, as above, the BVELOCITY constant is rebuilt on the next tick. Durable absorption applies to accumulated velocity.

  • x_restitution - Bounce coefficient on the X axis (LEFT/RIGHT walls). 0 = absorb, 1 = perfect elastic.
  • y_restitution - Bounce coefficient on the Y axis (TOP/BOTTOM walls). 0 = absorb, 1 = perfect elastic.
  • z_restitution - (optional) Bounce coefficient for the ground (elevation 0) on a plane layer. 0 = land/stop, 1 = perfect elastic, between = damped bounce that settles. Needs vertical motion (BGRAVITY / BIMPULSE z) to matter.
  • All values support VARIANCE(low, high) and VARIANCEINT(...) for per-instance randomness.
  • SLOPE hits use (x_restitution + y_restitution) / 2 as the restitution along the slope normal.

BSTOP

BSTOP

Inside a BEHAVIOR block, performs a one-shot motion halt. On activation, zeros the sprite's bvelocity, accumulated accel_velocity, and the cached total velocity on ALL axes — including the vertical z (elevation) axis — exactly once, then becomes a no-op for the rest of that activation.

BSTOP fills a real gap in the motion model: pattern transitions (SET SPRITE n PATTERN ... and GOTOPATTERN) do not reset sprite motion. Contributions to accel_velocity from BACCELERATION and BIMPULSE survive across pattern changes unless something explicitly zeros them, and BFRICTION only decays them gradually. Without BSTOP there's no clean primitive for 'stop here, regardless of what the previous pattern was doing.'

Lifecycle matches BIMPULSE: fires on the activation tick, no-op after, re-fires on reactivation (pattern step entered again, WHEN trigger transitions back to true). Subsequent ticks of the same activation don't keep wrestling motion to zero — a 'stop' pattern that holds for several ticks halts once and then idles, leaving room for other behaviors in the same activation slot to operate.

  • (no parameters)

Paths and orbits

BORBIT

BORBIT (x, y[, z]) DISTANCE radius [SPEED degreesPerTick] [CLOCKWISE|COUNTERCLOCKWISE] [DECAY pixelsPerTick] [STARTANGLE angle] [AXIS (ax, ay, az)] [SEEKFIRST [SPEED seekSpeed]]
BORBIT SPRITE targetId DISTANCE radius [SPEED degreesPerTick] [CLOCKWISE|COUNTERCLOCKWISE] [DECAY pixelsPerTick] [STARTANGLE angle] [AXIS (ax, ay, az)] [SEEKFIRST [SPEED seekSpeed]]
(the (x, y[, z]) center also accepts a coordinate variable/constant)

Inside a BEHAVIOR block, makes the sprite orbit around a fixed coordinate or another sprite. The sprite maintains a constant distance (radius) from the center point while rotating around it. Without STARTANGLE, the starting angle is derived from the sprite's actual position relative to the orbit center. Use STARTANGLE to specify an explicit starting angle in degrees (supports VARIANCE for randomness). Two modes: coordinate-based (x, y) for fixed orbit center, or sprite-based for orbiting around a moving target. Use DECAY to create a spiral effect where the orbit radius shrinks over time. AXIS (ax, ay, az) makes the orbit 3D: it is the axis of rotation, i.e. the normal of the plane the sprite circles in. The default axis (0, 0, 1) is the flat floor orbit (x/y circle, constant elevation). AXIS (1, 0, 0) stands the ring up into a vertical wheel; any tilted vector gives a tilted ring, foreshortened by the plane projection. The center coordinate may itself be 3D — (x, y, z) — to lift the whole ring off the floor; a ring of sprites at staggered STARTANGLEs then traces a rotating 3D ring. AXIS only has a visible elevation effect on a PERSPECTIVE/AFFINE plane layer. A 3D orbit around a SPRITE center rides at that sprite's own elevation, so a ring around a jumping sprite rises and falls with it. Use SEEKFIRST to make the sprite smoothly approach its computed orbit start position before beginning to orbit.

DECAY requires SPEED. DECAY pulls the orbit inward and SPEED is what carries the sprite around it, so a decaying orbit with no speed is a sprite sliding along a fixed bearing. Writing DECAY without SPEED is a runtime error.

  • (x, y) - Fixed coordinate to orbit around
  • SPRITE targetId - Sprite slot number (0-99,999) to orbit around
  • DISTANCE radius - Orbital distance in pixels from center
  • SPEED n - Optional angular rate. The orbit advances n * 0.01 radians per tick (default: 1 — about 0.57 degrees per tick, a full revolution in roughly 628 ticks). This is a rate multiplier, not an angle in degrees.
  • CLOCKWISE - Orbit in clockwise direction (default)
  • COUNTERCLOCKWISE - Orbit in counterclockwise direction
  • DECAY pixelsPerTick - Optional decay rate to reduce orbital radius each tick (creates spiral inward)
  • STARTANGLE angle - Optional starting orbit angle in degrees (supports VARIANCE for randomness). Without STARTANGLE, the angle is derived from the sprite's actual position relative to the orbit center.
  • AXIS (ax, ay, az) - Optional 3D rotation axis (normal of the orbit plane). Default (0, 0, 1) = flat floor orbit. (1, 0, 0) = vertical wheel; tilted vectors = tilted rings. Requires a PERSPECTIVE/AFFINE plane layer for the elevation to be visible.
  • (x, y, z) center - The center coordinate may include a 3rd component to lift the orbit ring to that elevation.
  • SEEKFIRST [SPEED seekSpeed] - Optional modifier that causes the sprite to smoothly approach its computed orbit start position before beginning to orbit. Default seek speed is 2.0 pixels per tick.

BOSCILLATE

BOSCILLATE AMPLITUDE amplitudeX, amplitudeY [, amplitudeZ] [SPEED degreesPerTick] [DECAY pixelsPerTick] [PHASE radians] [PERSISTENT]

Inside a BEHAVIOR block, makes the sprite oscillate (move back and forth) around its starting position using a sine wave pattern. The sprite moves smoothly between -amplitude and +amplitude on each axis. Useful for floating, hovering, or wobbling effects.

The optional third amplitude amplitudeZ oscillates the sprite's ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE) — for hover/bob. It swings symmetrically (±amplitudeZ) around whatever elevation the sprite had when the behavior activated, so set a base height first (e.g. SET SPRITE n ELEVATION(30)) for a hover that stays above the floor. Only affects rendering on a plane layer.

Use DECAY to gradually reduce the oscillation amplitude over time until the sprite settles. Use PERSISTENT to keep the sprite at its current oscillation offset when the behavior ends (instead of snapping back to the starting position).

Use PHASE to set the initial sine angle (in radians). The default initial phase is 0 — every instance starts synchronized at the wave origin. Combine PHASE with VARIANCE(low, high) to scatter starting phases per-instance (each sprite or mutation point gets a different random phase, useful for swarms of bobbing objects so they don't all move in lockstep).

When transitioning between two behaviors that both use BOSCILLATE, the new behavior automatically inherits the previous behavior's current phase so there is no visual jerk at the join. The carryover applies for one transition only — any non-oscillating behavior between two oscillating ones breaks the chain. An explicit PHASE override on the new behavior wins over the inherited value.

  • amplitudeX - Maximum horizontal displacement in pixels from starting position
  • amplitudeY - Maximum vertical displacement in pixels from starting position
  • amplitudeZ - (optional) Maximum elevation displacement in world units; oscillates height above a plane layer around the base elevation at activation (hover/bob).
  • SPEED degreesPerTick - Optional oscillation speed in degrees per tick (default: 6, full cycle in ~60 ticks)
  • DECAY pixelsPerTick - Optional decay rate to reduce amplitude each tick (sprite settles to starting position)
  • PHASE radians - Optional initial sine angle in radians (default 0). Supports VARIANCE(low, high) for per-instance scatter — e.g. PHASE 0 VARIANCE(0, 6.28) gives each instance a random phase across a full cycle.
  • PERSISTENT - Optional modifier to keep the current offset when the behavior ends (sprite does not snap back)

BSHAKE

BSHAKE AMPLITUDE amplitude

Inside a BEHAVIOR block, makes the sprite shake by applying random position offsets each tick. The sprite jitters randomly within the amplitude range on both X and Y axes, creating a vibration or trembling effect. Useful for damage feedback, screen shake effects, or unstable objects.

  • amplitude - Maximum random offset in pixels (applies to both X and Y axes)

BFOLLOW

BFOLLOW targetSpriteId DISTANCE minDistance [SPEED pixelsPerTick] [ELEVATION offset] [SEEKFIRST [SPEED seekSpeed]]

Inside a BEHAVIOR block, makes the sprite automatically follow another sprite. The sprite will move toward the target sprite only when the distance between them exceeds the specified minimum distance. The follower stops when it reaches the minimum distance from the target. Use SEEKFIRST to make the sprite smoothly approach the target before beginning normal follow behavior.

The optional ELEVATION clause makes BFOLLOW 3D: the follower also tracks the target's elevation, offset by the given amount, easing toward target_elevation + offset at the follow speed each tick. ELEVATION 0 pins the follower to the target's height (a drone matching a jumping player); ELEVATION 20 hovers 20 units above; ELEVATION -elevationOfTarget can pin a shadow to the ground. Without the ELEVATION clause, BFOLLOW only matches x/y (and on non-plane games all elevations are 0, so it is a no-op).

  • targetSpriteId - The sprite slot number (0-99,999) of the sprite to follow
  • minDistance - Minimum distance to maintain from target sprite (in pixels)
  • SPEED pixelsPerTick - Optional movement speed in pixels per tick (default: 2)
  • ELEVATION offset - (optional) Track the target's elevation plus this offset (3D follow). Eases at the follow speed; requires a PERSPECTIVE/AFFINE plane layer to be visible.
  • SEEKFIRST [SPEED seekSpeed] - Optional modifier that causes the sprite to smoothly approach the target before beginning to follow. Default seek speed is 2.0 pixels per tick.

PATH

PATH [ABSOLUTE] (x1,y1[,z1]), (x2,y2[,z2]), ... [SPEED n] [LOOP] [PINGPONG] [SEEKFIRST [SPEED seekSpeed]]   (each waypoint may also be a coordinate variable: PATH (0,0), home, (100,100))

Inside a BEHAVIOR block, defines a waypoint path for sprite movement. By default, coordinates are relative to sprite's starting position. Use ABSOLUTE for screen coordinates. Absolute PATH without SEEKFIRST snaps the sprite to the first waypoint immediately. Use SEEKFIRST to make the sprite smoothly approach the first waypoint before following the path (this was the previous default behavior for absolute paths). Relative PATH is unaffected by this change.

Waypoints may include an optional third coordinate component = ELEVATION, the sprite's height above the plane layer it sits on (AFFINE/PERSPECTIVE). Elevation is interpolated along the path in lockstep with x/y, so a sprite can arc up and over a Mode-7 floor. Elevation is always absolute (no relative-origin offset). If every waypoint is 2D, the path leaves elevation untouched — fully backward compatible.

  • (x,y[,z]) - Waypoint coordinates. The optional third component z is elevation (height above a plane layer), interpolated along the path.
  • ABSOLUTE - Use absolute screen coordinates instead of relative (x/y only; elevation is always absolute)
  • SPEED n - Movement speed in pixels per tick (default: 2)
  • LOOP - Repeat path indefinitely
  • PINGPONG - Reverse direction at endpoints instead of jumping back
  • SEEKFIRST [SPEED seekSpeed] - Optional modifier that causes the sprite to smoothly approach the first waypoint before following the path. Default seek speed is 2.0 pixels per tick.

Appearance

BROTATE

BROTATE from TO to DURATION ticks [ORIGIN(x, y)] [LOOP]
BROTATE offset DURATION ticks [ORIGIN(x, y)] [LOOP]

Inside a BEHAVIOR block, defines rotation transition over time. Multiple BROTATE behaviors are additive. Use ORIGIN to specify the rotation pivot point. Two modes available: absolute (FROM to TO) and relative (single offset value rotates from current angle).

The pivot defaults to the sprite's top-left corner, not its centre. A behavior always sends the pivot it is configured with, and an unset ORIGIN is (0, 0) — write ORIGIN(0.5, 0.5) to spin about the centre.

  • Absolute mode (with TO):
  • from - Starting rotation in degrees
  • to - Ending rotation in degrees
  • Relative mode (without TO):
  • offset - Amount to rotate in degrees from current angle
  • ticks - Duration in ticks/frames
  • ORIGIN(x, y) - Rotation pivot point using normalized values (0-1), relative to the sprite's top-left corner. This is independent of BORIGIN (anchor point). Default is (0, 0) = the top-left corner.
    • (0, 0) = top-left corner (default)
    • (0.5, 0.5) = center
    • (1, 1) = bottom-right corner
  • LOOP - Repeat transition indefinitely

BSCALE

BSCALE fromX, fromY TO toX, toY DURATION ticks [LOOP]

Inside a BEHAVIOR block, defines scale transition over time.

  • fromX, fromY - Starting scale factors
  • toX, toY - Ending scale factors
  • ticks - Duration in ticks/frames
  • LOOP - Repeat transition indefinitely

BALPHA

BALPHA from TO to DURATION ticks [LOOP]

Inside a BEHAVIOR block, defines alpha (transparency) transition over time.

  • from - Starting alpha value (0-1)
  • to - Ending alpha value (0-1)
  • ticks - Duration in ticks/frames
  • LOOP - Repeat transition indefinitely
BBLINK rate

Inside a BEHAVIOR block, makes the sprite blink by toggling visibility at the specified rate. Use rate of 0 to disable blinking.

  • rate - Ticks between visibility toggles (0 = disable blinking)

BFLIPX

BFLIPX ON
BFLIPX OFF

Inside a BEHAVIOR block, sets horizontal flip state.

  • ON - Enable horizontal flip
  • OFF - Disable horizontal flip

BFLIPY

BFLIPY ON
BFLIPY OFF

Inside a BEHAVIOR block, sets vertical flip state.

  • ON - Enable vertical flip
  • OFF - Disable vertical flip

BORIGIN

BORIGIN ox, oy

Inside a BEHAVIOR block, sets the sprite's anchor in relative 0-1 coordinates — the single point the sprite is positioned at, parented at, and rotated around. The same anchor SET SPRITE ... ORIGIN(ox, oy) sets, moved from a behavior instead of a statement.

0, 0 is the top-left of the sprite, 0.5, 0.5 its centre, 1, 1 its bottom-right. Applied once, on the behavior's first tick.

Not the same as BROTATE ... ORIGIN(x, y). The anchor is where the sprite is; BROTATE's ORIGIN overrides the pivot for that one rotation. Setting an anchor does not start a rotation, so on its own BORIGIN has no visible effect — it changes where later positioning, parenting and rotation measure from.

Both values support VARIANCE and @paramName=default.

  • ox - Horizontal anchor, relative 0-1 across the sprite (0 = left, 1 = right)
  • oy - Vertical anchor, relative 0-1 down the sprite (0 = top, 1 = bottom)

BIMAGE

BIMAGE imageName [, frame]

Inside a BEHAVIOR block, changes the sprite's texture to the specified image when the behavior becomes active. The image is only changed if it differs from the current image (avoids redundant texture switches). This is useful for state-based sprite changes like different character states or transformations.

  • imageName - Name of a loaded image (via LOADIMG, LOADIMAGE, or TEXTTOIMAGE)
  • frame - Optional frame number for sprite sheets (0-based)

ANIMATE

ANIMATE imageName [, frame1, frame2, ...] [EVENTSON(frame1, frame2, ...)] [AUTOBBOX] FRAMERATE fps
ANIMATE imageName, startFrame TO endFrame [EVENTSON(...)] [AUTOBBOX] FRAMERATE OFSPRITE

Inside a BEHAVIOR block, defines animation frames to cycle through. If frame indices are omitted, all frames from the sprite sheet are used automatically. Frame indices can be listed individually or as a range using startFrame TO endFrame (e.g., 0 TO 18 expands to frames 0, 1, 2, ..., 18). Only one ANIMATE command can be active at a time per sprite (last wins). EVENTSON triggers events when specified frames become visible - these events run asynchronously and do not block the animation or tick loop. Multiple sprites can trigger the same event concurrently. EVENT NAMING: The event name is the animation image name UPPERCASED followed by '_FRAME'. For example, ANIMATE "player" fires events named 'PLAYER_FRAME', and ANIMATE "walkSprite" fires 'WALKSPRITE_FRAME'. This means multiple behaviors sharing the same animation image fire the same event. Register handlers with empty parens to receive dynamic args (sprite_id, frame_number). Use STOP THREAD "PLAYER_FRAME" to stop all running handlers.

FRAMERATE OFSPRITE drives the timing from the image's own Aseprite-authored per-frame durations instead of a single fps — each frame can be on screen for a different length of time, exactly as set on the Aseprite timeline. It requires an image loaded from an .ase/.aseprite file; using it on a plain PNG/sprite sheet (which has no per-frame durations) is a loud runtime error.

AUTOBBOX updates the sprite's bounding box every frame from the image's Aseprite per-frame slice geometry, so a hitbox that was animated in Aseprite tracks the animation automatically. Bare AUTOBBOX uses the file's first slice; AUTOBBOX "sliceName" selects a specific slice by name (case-insensitive) — useful when the file has several slices and the first isn't the collision box you want. It needs an .ase image with at least one slice (or a matching named slice); otherwise it is a loud runtime error. The sprite has a single collision box, so AUTOBBOX drives one slice; query other slices for manual checks with SPRITEBBOX(id, name$) / IMAGEBBOX(image$, name$, frame). AUTOBBOX is independent of OFSPRITE — combine it with either OFSPRITE or an explicit fps. Both options resolve identically on server and client in CrashNet, since the per-frame metadata registers on every interpreter at LOADIMG time.

  • imageName - Name of loaded sprite sheet
  • frame1, frame2, ... - Optional frame indices to cycle through (if omitted, uses all frames)
  • startFrame TO endFrame - Optional shorthand for a contiguous range of frames (e.g., 0 TO 18 expands to 0, 1, 2, ..., 18). If start > end, frames are generated in reverse order.
  • EVENTSON(frames) - Optional list of frame numbers that trigger events when displayed. Event name = UPPERCASED animation image name + '_FRAME' (e.g., ANIMATE "player" → event 'PLAYER_FRAME')
  • AUTOBBOX ["sliceName"] - Optional (Aseprite only): set the sprite's bounding box each frame from a slice's per-frame geometry. Bare = first slice; with a name = that slice.
  • FRAMERATE fps - Frames per second for the animation
  • FRAMERATE OFSPRITE - Use the .ase file's authored per-frame durations instead of a fixed fps (Aseprite only)

BSHADER

BSHADER "shaderName" [@param=from TO to DURATION ticks [LOOP]]

Behavior command that attaches a shader to a sprite and optionally animates one of its parameters over time. Use within a BEHAVIOR block to create animated shader effects like dissolves, fades, or pulsing. With the parameter clause, the value is linearly interpolated from 'from' to 'to' over the specified duration. The clause is optional — BSHADER "name" on its own just attaches the shader.

  • shaderName - Name of the shader (must be defined with SHADER block)
  • @param - The shader parameter to animate
  • from - Starting value for the parameter
  • to - Ending value for the parameter
  • ticks - Duration in behavior ticks (60 ticks/second)
  • LOOP - Optional: repeat the animation continuously

Syntax

BEHAVIOR "name" [INCLUDES("other1", "other2", ...)]
  commands...
  [ONCOLLISION
    statements...
  END ONCOLLISION]
  [AFTERCOLLISION
    statements...
  END AFTERCOLLISION]
END BEHAVIOR

BEHAVIOR
  "name1":
    commands...
  "name2":
    commands...
END BEHAVIOR

Parameters

  • name - String expression for behavior name (case-insensitive)
  • INCLUDES("name1", "name2", ...) - Optional. Include commands from other behaviors. Included commands come first, then this behavior's commands. Included behaviors must be defined before this one. Parameter defaults are also inherited (later includes and this behavior's params override earlier ones).
  • IF cond THEN / ELSEIF / ELSE / END IF, and SELECT CASE sel / CASE ... / CASE ELSE / END SELECT - Declaration-time control flow: chooses which commands are baked into the behavior when it is declared (evaluated once, not per tick). Single-line IF cond THEN command is also allowed.
  • Commands available inside BEHAVIOR blocks:
  • ONCOLLISION ... END ONCOLLISION - Inline BASIC run when this sprite BEGINS touching something. Edge-triggered: fires once when the contact starts, not every tick it lasts. In scope: index_a/tag_a$ (this sprite), index_b/tag_b$ (the other sprite, or -1 and the boundary tag), edge$, and impact_vx/impact_vy — the velocity at impact, before resolution zeroes it. Blocking statements (PAUSE, DELAY, DELAY UNTIL, YIELD) are rejected: handlers run inline on the behavior tick loop, so waiting there would freeze every sprite.
  • AFTERCOLLISION ... END AFTERCOLLISION - Inline BASIC run when a contact ENDS. Same scoped values, describing the contact as it began. A ghost overlap stays live until the sprites separate, so GHOST from ONCOLLISION defers this until the sprite is clear.
  • Collision handler bodies scope like a SUB: assigning a name never declared at program scope creates a local that dies with the handler. Declare variables you want to persist before the BEHAVIOR block. INCLUDES runs every inherited handler, in composition order.
  • Parameter references: Any numeric value in behavior commands can use @paramName=defaultValue syntax. The default value is used if no override is provided when the behavior is activated. Parameter values are evaluated once at assignment time.
  • Attribute persistence on deactivation
  • When a behavior is deactivated (via DELETE or NONE), some attributes revert while others persist:
  • REVERT: Visibility (BBLINK restores visible=true), Velocity (BVELOCITY zeros controlled axes if no other behavior controls them), Shaders (BSHADER removes the shader), Bounce (BBOUNCE clears the sprite's bounce profile if no other active behavior provides one)
  • PERSIST: Position (BMOVE, PATH, BFOLLOW, BORBIT, BOSCILLATE, BSHAKE changes remain), Alpha (BALPHA final value), Scale (BSCALE final values), Rotation (BROTATE final angle), Flip state (BFLIPX/BFLIPY), Image/Frame (ANIMATE, BIMAGE), Anchor point (BORIGIN)
  • SPECIAL: Acceleration velocity (BACCELERATION) persists when a single behavior is deactivated but clears when ALL behaviors are cleared (BEHAVIOR NONE)

Examples

BEHAVIOR "walkRight"
  ANIMATE "player" FRAMERATE 10  ' Uses all frames
  BMOVE 2, 0
END BEHAVIOR
BEHAVIOR "walkRight"
  ANIMATE "player", 0, 1, 2, 3 FRAMERATE 10  ' Explicit frames
  BMOVE 2, 0
END BEHAVIOR
' Declaration-time IF: bake in different commands per difficulty.
' Re-declare with a changed variable to swap the behavior.
BEHAVIOR "enemy"
  IF hardMode THEN
    BMOVE 4, 0
    BGRAVITY 0.5
  ELSE
    BMOVE 2, 0
  END IF
  IF hardMode THEN BSHAKE AMPLITUDE 2
END BEHAVIOR
' Declaration-time SELECT CASE on a level variable.
BEHAVIOR "enemy"
  SELECT CASE level
    CASE 1
      BMOVE 1, 0
    CASE 2, 3
      BMOVE 2, 0
    CASE IS > 5
      BMOVE 6, 0
    CASE ELSE
      BMOVE 3, 0
  END SELECT
END BEHAVIOR
BEHAVIOR "fadeIn"
  BALPHA 0 TO 1 DURATION 30
END BEHAVIOR
BEHAVIOR "bounce"
  PATH (0,0), (0,-20), (0,0) SPEED 3 LOOP
END BEHAVIOR
BEHAVIOR
  "idle":
    ANIMATE "player", 0, 1 FRAMERATE 5
  "walk":
    ANIMATE "player" FRAMERATE 12  ' Uses all frames
    BMOVE 3, 0
END BEHAVIOR
' EVENTSON triggers events when specific frames are displayed
BEHAVIOR "walk"
  ANIMATE "player", 0, 1, 2, 3 EVENTSON(1, 3) FRAMERATE 10
END BEHAVIOR

SUB OnWalkFrame(sprite_id, frame)
  IF frame = 1 THEN PLAYWAV "step1"
  IF frame = 3 THEN PLAYWAV "step2"
END SUB

CALL EVENT OnWalkFrame() AS "PLAYER_FRAME"
' Parameterized behavior with defaults
BEHAVIOR "moveToPoint"
  BMOVE @targetX=0, @targetY=0
END BEHAVIOR

' Use with overrides - values evaluated at assignment time
SET SPRITE 0 BEHAVIOR "moveToPoint", @targetX=100, @targetY=playerY

' Use with defaults (moves to 0,0)
SET SPRITE 1 BEHAVIOR "moveToPoint"
' Sprite positioned by center (BORIGIN) and rotates around center (BROTATE ORIGIN)
' Note: These are independent - BORIGIN affects positioning, BROTATE ORIGIN affects rotation pivot
BEHAVIOR "spinCenter"
  BORIGIN 0.5, 0.5
  BROTATE 0 TO 360 DURATION 60 ORIGIN(0.5, 0.5) LOOP
END BEHAVIOR
' Compose behaviors using INCLUDES
BEHAVIOR "moveRight"
  BVELOCITY 2, 0
END BEHAVIOR

BEHAVIOR "wobble"
  BOSCILLATE AMPLITUDE 3, 3 SPEED 0.1
END BEHAVIOR

' Combined behavior - gets moveRight + wobble commands, then adds fade
BEHAVIOR "wobblyMove" INCLUDES("moveRight", "wobble")
  BALPHA 1 TO 0.8 DURATION 30 LOOP
END BEHAVIOR
BEHAVIOR "driftRight"
  BVELOCITY 5, 0    ' Steady drift to the right
END BEHAVIOR
BEHAVIOR "thrust"
  BVELOCITY 0, -8   ' Steady upward thrust
END BEHAVIOR
BEHAVIOR "driftDiagonal"
  BVELOCITY 3, -5   ' Steady diagonal drift
END BEHAVIOR
' Rising arc: the standing -8 thrust is overcome by accumulating gravity
BEHAVIOR "lob"
  BVELOCITY 0, -8      ' Re-applied every tick, not a one-shot
  BACCELERATION 0, 0.5 ' Accumulates each tick until it wins
END BEHAVIOR
' Projectile with arc
BEHAVIOR "cannonball"
  BVELOCITY 5, -3      ' Constant thrust
  BACCELERATION 0, 0.2 ' Gravity arc
END BEHAVIOR
' Knockback belongs to BIMPULSE, not BVELOCITY: it must decay and
' stop when the sprite hits something.
BEHAVIOR "knockedBack"
  BIMPULSE -4, -2      ' One-shot kick into accumulated velocity
  BFRICTION 0.1, 0.1   ' Decays back to rest
END BEHAVIOR
BEHAVIOR "rise"
  BVELOCITY 0, 0, 4   ' Rise off a plane floor at constant vertical speed
END BEHAVIOR
BEHAVIOR "moveRight"
  BMOVE 2, 0                       ' Move right 2 pixels per tick
END BEHAVIOR
BEHAVIOR "moveDiagonal"
  BMOVE -1, 1                      ' Move diagonally
END BEHAVIOR
BEHAVIOR "moveBy"
  BMOVE @dx=0, @dy=0               ' Parameterized movement — pass values at activation
END BEHAVIOR
BEHAVIOR "snapX"
  BMOVE ABSOLUTEX 100, 0           ' Snap X to 100 (absolute), Y unchanged each tick
END BEHAVIOR
BEHAVIOR "snapY"
  BMOVE 0, ABSOLUTEY 50            ' Y snapped to 50 (absolute), X drifts freely
END BEHAVIOR
BEHAVIOR "snapTo"
  BMOVE ABSOLUTEX 160, ABSOLUTEY 120 ' Teleport-snap to (160, 120) each tick
END BEHAVIOR
BEHAVIOR "climb"
  BMOVE 0, 0, 0.7                  ' Rise off a plane layer (elevation +0.7/tick)
END BEHAVIOR
' Lane-locked horizontal mover — snap X, drift Y
BEHAVIOR "laneRight"
  BMOVE ABSOLUTEX 240, 1
END BEHAVIOR
BEHAVIOR "gravity"
  BACCELERATION 0, 0.5  ' Simulate gravity (accelerate downward)
END BEHAVIOR
BEHAVIOR "rocket"
  BACCELERATION 0, -0.3 ' Thrust upward (rocket)
END BEHAVIOR
BEHAVIOR "dragLeft"
  BACCELERATION -0.1, 0 ' Decelerate leftward (friction-like)
END BEHAVIOR
BEHAVIOR "fall"
  BACCELERATION 0, 0, -1 ' Gravity on the elevation axis (plane layers)
END BEHAVIOR
' Jumping with proper physics
BEHAVIOR "jump"
  BVELOCITY 0, -8      ' Initial upward velocity
  BACCELERATION 0, 0.5 ' Gravity pulls back down
END BEHAVIOR
' True 3D jump off a Mode-7 floor — vertical kick + elevation gravity
BEHAVIOR "hop"
  BIMPULSE 0, 0, 16
  BACCELERATION 0, 0, -1
END BEHAVIOR
BEHAVIOR "jump"
  BIMPULSE 0, -8           ' Classic jump — instant upward kick
END BEHAVIOR
BEHAVIOR "dash"
  BIMPULSE 4, 0            ' Dash to the right
END BEHAVIOR
' True jump off a plane floor — vertical kick + gravity, lands on its own
BEHAVIOR "jump"
  BIMPULSE 0, 0, 16
  BACCELERATION 0, 0, -1
END BEHAVIOR
BEHAVIOR "knockback"
  BIMPULSE -3 VARIANCE(-1, 1), -2  ' Slight horizontal scatter on knockback
END BEHAVIOR
' Jump with gravity — impulse fires once, gravity pulls back
BEHAVIOR "jump"
  BIMPULSE 0, -8
  BACCELERATION 0, 0.5
END BEHAVIOR
' Dash with friction-based decay — impulse, then ice-style settle
BEHAVIOR "dashRight"
  BIMPULSE 6, 0
  BFRICTION 0.1, 0
END BEHAVIOR
' Pattern-driven recoil — re-fires on every reactivation
PATTERN "shoot"
  "recoil" DURATION 8
  "idle" DURATION 12
END PATTERN

BEHAVIOR "recoil"
  BIMPULSE -2, 0
  BFRICTION 0.2, 0
END BEHAVIOR
' Parameterised — pattern can pass per-shot impulse
BEHAVIOR "kick"
  BIMPULSE @ix=0, @iy=-6
END BEHAVIOR
BEHAVIOR "ice"
  BFRICTION 0.05, 0.05      ' Ice — slow, even decay on both axes
END BEHAVIOR
BEHAVIOR "airDrag"
  BFRICTION 0, 0, 0.05      ' Air resistance on a jump/fall (elevation axis only)
END BEHAVIOR
BEHAVIOR "groundDrag"
  BFRICTION 0.2, 0          ' Ground drag (X only). Sprite slides but slows; falls untouched.
END BEHAVIOR
BEHAVIOR "fallDrag"
  BFRICTION 0, 0.05         ' Air resistance (Y only). Useful for falling objects with terminal velocity.
END BEHAVIOR
' Air-resistance + gravity → terminal velocity ≈ 1.0 / 0.1 = 10 px/tick
BEHAVIOR "airDrop"
  BACCELERATION 0, 1.0
  BFRICTION 0, 0.1
END BEHAVIOR
' Slippery ice with slight randomness per object
BEHAVIOR "icePuck"
  BFRICTION 0.02 VARIANCE(0, 0.03), 0.02 VARIANCE(0, 0.03)
END BEHAVIOR
' Hockey-style: friction decays accel from impacts
BEHAVIOR "slidingPuck"
  BFRICTION 0.04, 0.04
  BBOUNCE 0.85, 0.85
END BEHAVIOR
BEHAVIOR "weight"
  BGRAVITY 0.5             ' Standard weight on a plane layer
END BEHAVIOR
' Jump: kick up, gravity brings it back down
BEHAVIOR "jump"
  BIMPULSE 0, 0, 16
  BGRAVITY 1
END BEHAVIOR
' Bouncing ball on the floor
BEHAVIOR "ball"
  BGRAVITY 0.4
  BBOUNCE 0, 0, 0.7
END BEHAVIOR
' Capped fall speed (terminal velocity ~= 0.5 / 0.05 = 10)
BEHAVIOR "feather"
  BGRAVITY 0.5
  BFRICTION 0, 0, 0.05
END BEHAVIOR
BEHAVIOR "liftOff"
  BELEVATE 0 TO 60 DURATION 14  ' Rise 60 units over 14 ticks
END BEHAVIOR
REM A continuous hop: sequence rise + fall in a looping pattern
BEHAVIOR "RiseUp"
  BELEVATE 0 TO 60 DURATION 14
END BEHAVIOR
BEHAVIOR "FallDown"
  BELEVATE 60 TO 0 DURATION 14
END BEHAVIOR
PATTERN "Hop" LOOP
  "RiseUp" DURATION 14
  "FallDown" DURATION 14
END PATTERN
BEHAVIOR "rubberBall"
  BBOUNCE 0.9, 0.9          ' Rubber ball — almost-perfect bounce on both axes
END BEHAVIOR
BEHAVIOR "ballOnGround"
  BBOUNCE 0.9, 0.5          ' Bouncy side-to-side, dampened up-down (ball on grass)
END BEHAVIOR
BEHAVIOR "dribble"
  BBOUNCE 0.7, 0            ' Rolls along floor, no Y bounce (basketball dribble)
END BEHAVIOR
BEHAVIOR "superBall"
  BBOUNCE 1.2, 1.2          ' Energetic — gains speed each hit (rare; for power-ups)
END BEHAVIOR
' Bouncing ball on a plane floor (3D): drops, rebounds, settles
BEHAVIOR "ball3d"
  BGRAVITY 0.4
  BBOUNCE 0, 0, 0.7
END BEHAVIOR
' Particle system with natural jitter — BIMPULSE, so each spark's
' sideways motion survives a bounce
BEHAVIOR "sparkBounce"
  BBOUNCE 0.85 VARIANCE(-0.1, 0.05), 0.85 VARIANCE(-0.1, 0.05)
  BIMPULSE 4 VARIANCE(-3, 3), -6 VARIANCE(-2, 2)
  BACCELERATION 0, 0.3
END BEHAVIOR
' Classic bouncing ball with gravity
BEHAVIOR "ball"
  BBOUNCE 0.85, 0.85
  BIMPULSE 5, 0        ' one-shot kick; BVELOCITY would not rebound
  BACCELERATION 0, 0.4 ' gravity, bounces off the floor
END BEHAVIOR
' Projectile that ricochets off walls then dies
BEHAVIOR "ricochet"
  BBOUNCE 0.95, 0.95
  BIMPULSE 8, 0        ' accumulated momentum is what reflects
  BALPHA 1 TO 0 DURATION 120
END BEHAVIOR
BEHAVIOR "halt"
  BSTOP                          ' Hard halt — zeroes all velocity and acceleration state
END BEHAVIOR
' Stop pattern — typically transitioned into from another pattern
BEHAVIOR "halt"
  BSTOP
END BEHAVIOR

PATTERN "stop" LOOP
  "halt" WHEN 1=1
END PATTERN
' WHEN-triggered stop — kill all motion the moment a flag goes true
PATTERN "chase" LOOP
  "chasePlayer" WHEN player_visible = 1
  "halt" WHEN player_visible = 0
END PATTERN
' Use after a knockback impulse to clamp the recovery period
PATTERN "hit"
  "knockback" DURATION 8
  "freeze" DURATION 12
END PATTERN

BEHAVIOR "knockback"
  BIMPULSE -3, -2
END BEHAVIOR

BEHAVIOR "freeze"
  BSTOP
END BEHAVIOR
BEHAVIOR "orbitCentre"
  BORBIT (160, 120) DISTANCE 50          ' Orbit around screen center
END BEHAVIOR
BEHAVIOR "slowOrbit"
  BORBIT (160, 120) DISTANCE 40 SPEED 3  ' Slower orbit at 3 degrees/tick
END BEHAVIOR
BEHAVIOR "orbitPlayer"
  BORBIT SPRITE 0 DISTANCE 30            ' Orbit around sprite 0
END BEHAVIOR
BEHAVIOR "orbitBackwards"
  BORBIT SPRITE 0 DISTANCE 40 COUNTERCLOCKWISE  ' Orbit counterclockwise
END BEHAVIOR
BEHAVIOR "spiralIn"
  BORBIT (160, 120) DISTANCE 80 DECAY 0.5  ' Spiral inward
END BEHAVIOR
' Satellite orbiting planet
BEHAVIOR "satellite"
  ANIMATE "moon", 0, 1 FRAMERATE 5
  BORBIT SPRITE 0 DISTANCE 50 SPEED 2
END BEHAVIOR
' Shield effect orbiting player
BEHAVIOR "shield"
  BORBIT SPRITE 0 DISTANCE 32 SPEED 8 CLOCKWISE
  BROTATE 360 DURATION 60 LOOP
END BEHAVIOR
' Decaying orbit (spiraling into target)
BEHAVIOR "spiralIn"
  BORBIT SPRITE 0 DISTANCE 60 SPEED 6 DECAY 0.3
END BEHAVIOR
' Vertical 3D ring (wheel) on a plane layer (8 balls)
BEHAVIOR "Ring"
  BORBIT (256, 280, 60) DISTANCE 70 STARTANGLE @a=0 SPEED 3 AXIS (1, 0, 0)
END BEHAVIOR
FOR i = 0 TO 7
  SET SPRITE i, 256, 280, "guy", 0 LAYER 0
  SET SPRITE i BEHAVIOR "Ring", @a=(i * 45)
NEXT i
' Tilted ring (30deg off the floor)
BEHAVIOR "TiltRing"
  BORBIT (256, 280, 40) DISTANCE 70 SPEED 3 AXIS (0.4, 0, 1)
END BEHAVIOR
BEHAVIOR "wobble"
  BOSCILLATE AMPLITUDE 10, 5             ' Wobble horizontally and vertically
END BEHAVIOR
BEHAVIOR "hover"
  BOSCILLATE AMPLITUDE 0, 10             ' Vertical bob only (hovering)
END BEHAVIOR
' Hover above a plane floor: lift to a base height, then bob the elevation
SET SPRITE 0 ELEVATION(30)
BEHAVIOR "hover"
  BOSCILLATE AMPLITUDE 0, 0, 15 SPEED 4
END BEHAVIOR
BEHAVIOR "sway"
  BOSCILLATE AMPLITUDE 15, 0 SPEED 3     ' Slow horizontal sway
END BEHAVIOR
BEHAVIOR "settle"
  BOSCILLATE AMPLITUDE 20, 10 DECAY 0.2  ' Wobble that fades out
END BEHAVIOR
BEHAVIOR "bobAndStay"
  BOSCILLATE AMPLITUDE 0, 20 PERSISTENT  ' Keep position when behavior ends
END BEHAVIOR
BEHAVIOR "bobOffset"
  BOSCILLATE AMPLITUDE 0, 8 PHASE 1.5708 ' Start a quarter-cycle into the wave
END BEHAVIOR
' Scatter starting phases across a swarm of mutation points
BEHAVIOR "swarmBob"
  BOSCILLATE AMPLITUDE 0, 8 SPEED 4 PHASE 0 VARIANCE(0, 6.28)
END BEHAVIOR
' Floating coin effect
BEHAVIOR "float"
  ANIMATE "coin", 0, 1, 2, 3 FRAMERATE 8
  BOSCILLATE AMPLITUDE 0, 8
END BEHAVIOR
' Wobbling ghost enemy
BEHAVIOR "ghostWobble"
  BOSCILLATE AMPLITUDE 5, 3 SPEED 8
  BALPHA 0.7 TO 1 DURATION 30 LOOP
END BEHAVIOR
' Platform that moves up and down
BEHAVIOR "movingPlatform"
  BOSCILLATE AMPLITUDE 0, 50 SPEED 2
END BEHAVIOR
' Landing effect - bounce that settles
BEHAVIOR "landing"
  BOSCILLATE AMPLITUDE 0, 15 SPEED 10 DECAY 0.4
END BEHAVIOR
' Smooth transition between two oscillating idles — phase carries over
BEHAVIOR "idleSlow"
  BOSCILLATE AMPLITUDE 0, 6 SPEED 3
END BEHAVIOR
BEHAVIOR "idleFast"
  BOSCILLATE AMPLITUDE 0, 6 SPEED 8
END BEHAVIOR
' Switching idleSlow → idleFast resumes the wave from where idleSlow left it
BEHAVIOR "shiver"
  BSHAKE AMPLITUDE 3            ' Subtle shake
END BEHAVIOR
BEHAVIOR "quake"
  BSHAKE AMPLITUDE 8            ' Intense shake
END BEHAVIOR
' Damage feedback behavior
BEHAVIOR "damaged"
  BSHAKE AMPLITUDE 4
  BBLINK 5
END BEHAVIOR
' Unstable platform
BEHAVIOR "unstable"
  BSHAKE AMPLITUDE 2
END BEHAVIOR
' Explosion tremor
BEHAVIOR "tremor"
  BSHAKE AMPLITUDE 6
  BALPHA 1 TO 0.5 DURATION 30 LOOP
END BEHAVIOR
BEHAVIOR "followPlayer"
  BFOLLOW 0 DISTANCE 32                 ' Follow sprite 0, stay 32 pixels away
END BEHAVIOR
BEHAVIOR "chase"
  BFOLLOW 0 DISTANCE 50 SPEED 3         ' Follow faster at 3 pixels per tick
END BEHAVIOR
BEHAVIOR "droneEscort"
  BFOLLOW 0 DISTANCE 24 SPEED 3 ELEVATION 30  ' Drone hovering 30 units above the target
END BEHAVIOR
' Enemy following player behavior
BEHAVIOR "followPlayer"
  ANIMATE "enemy", 0, 1, 2 FRAMERATE 8
  BFOLLOW 0 DISTANCE 40 SPEED 2
END BEHAVIOR
' Companion behavior - follows and animates
BEHAVIOR "companion"
  BFOLLOW 0 DISTANCE 24 SPEED 1.5
  ANIMATE "pet", 0, 1, 2, 3 FRAMERATE 10
END BEHAVIOR
BEHAVIOR "hop"
  PATH (0,0), (0,-20), (0,0) SPEED 3 LOOP  ' Bounce effect
END BEHAVIOR
BEHAVIOR "patrol"
  PATH ABSOLUTE (100,100), (200,100) SPEED 2 LOOP PINGPONG  ' Patrol
END BEHAVIOR
BEHAVIOR "square"
  PATH (0,0), (50,0), (50,50), (0,50) LOOP  ' Square path
END BEHAVIOR
BEHAVIOR "arc"
  PATH ABSOLUTE (180,260,0), (256,260,70), (332,260,0) SPEED 3 PINGPONG  ' Arc up over a plane floor (3rd component = elevation)
END BEHAVIOR
BEHAVIOR "spin"
  BROTATE 0 TO 360 DURATION 60 LOOP  ' Absolute: spin from 0 to 360
END BEHAVIOR
BEHAVIOR "spinRelative"
  BROTATE 360 DURATION 60 LOOP       ' Relative: spin 360 degrees from current
END BEHAVIOR
BEHAVIOR "quarterTurn"
  BROTATE 90 DURATION 30             ' Relative: rotate 90 degrees from current
END BEHAVIOR
BEHAVIOR "spinCentred"
  BROTATE 0 TO 360 DURATION 60 ORIGIN(0.5, 0.5) LOOP  ' Spin around center
END BEHAVIOR
BEHAVIOR "tipBack"
  BROTATE -45 DURATION 15 ORIGIN(0, 1)  ' Rotate backwards around bottom-left
END BEHAVIOR
BEHAVIOR "grow"
  BSCALE 1, 1 TO 2, 2 DURATION 30  ' Grow to 2x over 30 ticks
END BEHAVIOR
BEHAVIOR "pulse"
  BSCALE 1, 1 TO 1.2, 1.2 DURATION 10 LOOP  ' Pulsing effect
END BEHAVIOR
BEHAVIOR "fadeIn"
  BALPHA 0 TO 1 DURATION 30  ' Fade in over 30 ticks
END BEHAVIOR
BEHAVIOR "pulseAlpha"
  BALPHA 1 TO 0.3 DURATION 15 LOOP  ' Pulsing blink effect
END BEHAVIOR
BEHAVIOR "blink"
  BBLINK 10              ' Toggle visibility every 10 ticks
END BEHAVIOR
BEHAVIOR "fastBlink"
  BBLINK 5               ' Fast blink
END BEHAVIOR
BEHAVIOR "stopBlink"
  BBLINK 0               ' Disable blinking
END BEHAVIOR
' Invincibility effect
BEHAVIOR "invincible"
  BBLINK 4
END BEHAVIOR
' Damaged enemy blinks
BEHAVIOR "damaged"
  BBLINK 8
  BALPHA 0.7 TO 1 DURATION 16 LOOP
END BEHAVIOR
BEHAVIOR "faceLeft"
  BFLIPX ON   ' Flip sprite horizontally
END BEHAVIOR
BEHAVIOR "faceRight"
  BFLIPX OFF  ' Normal orientation
END BEHAVIOR
BEHAVIOR "upsideDown"
  BFLIPY ON   ' Flip sprite vertically
END BEHAVIOR
BEHAVIOR "upright"
  BFLIPY OFF  ' Normal orientation
END BEHAVIOR
REM Swing from the top of the sprite rather than its middle.
BEHAVIOR "pendulum"
    BORIGIN 0.5, 0
    BROTATE -30 TO 30 DURATION 60 LOOP
END BEHAVIOR
REM Anchor a chain link at its joint so it turns about the right point.
BEHAVIOR "upperArm"
    BORIGIN 0, 0.5
END BEHAVIOR
BEHAVIOR "run"
  BIMAGE "playerRun"     ' Change to running sprite
END BEHAVIOR
BEHAVIOR "swapToEnemy"
  BIMAGE "enemy", 2      ' Change to enemy sprite, frame 2
END BEHAVIOR
' Walking behavior with different sprite
BEHAVIOR "walkRight"
  BIMAGE "playerWalk"
  ANIMATE "playerWalk", 0, 1, 2, 3 FRAMERATE 10
  BMOVE 2, 0
END BEHAVIOR
' State-based appearance
BEHAVIOR "powered"
  BIMAGE "playerPowered"
  BSCALE 1, 1 TO 1.2, 1.2 DURATION 30 LOOP
END BEHAVIOR
' Enemy transformation
BEHAVIOR "enraged"
  BIMAGE "enemyAngry"
  BALPHA 1 TO 0.8 DURATION 10 LOOP
END BEHAVIOR
BEHAVIOR "walkCycle"
  ANIMATE "player" FRAMERATE 10           ' Use all frames from sprite sheet
END BEHAVIOR
BEHAVIOR "walkFrames"
  ANIMATE "player", 0, 1, 2, 3 FRAMERATE 10  ' Explicit frames
END BEHAVIOR
BEHAVIOR "longWalk"
  ANIMATE "player", 0 TO 18 FRAMERATE 10     ' Frame range (expands to 0, 1, 2, ..., 18)
END BEHAVIOR
BEHAVIOR "enemyIdle"
  ANIMATE "enemy", 0, 1 FRAMERATE 5       ' Specific frames only
END BEHAVIOR
BEHAVIOR "attackWithCues"
  ANIMATE "player", 0 TO 8 EVENTSON(3, 6) FRAMERATE 10  ' Range with event triggers
END BEHAVIOR
' Aseprite: honor the file's own per-frame timing and per-frame hitboxes
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32)
BEHAVIOR "walk"
  ANIMATE "HERO", 0 TO 7 AUTOBBOX FRAMERATE OFSPRITE
END BEHAVIOR
' Frame events for sound effects
BEHAVIOR "walk"
  ANIMATE "player", 0, 1, 2, 3 EVENTSON(1, 3) FRAMERATE 10
END BEHAVIOR

SUB OnWalkFrame(sprite_id, frame)
  IF frame = 1 THEN PLAYWAV "step1"
  IF frame = 3 THEN PLAYWAV "step2"
END SUB

CALL EVENT OnWalkFrame() AS "PLAYER_FRAME"
' Define a dissolve behavior
BEHAVIOR "dissolve"
  BSHADER "Dissolve" @progress=0 TO 1 DURATION 60
END BEHAVIOR

' Apply to sprite
SET SPRITE 1 BEHAVIOR "dissolve"
' Looping pulse effect
BEHAVIOR "pulse"
  BSHADER "Pulse" @intensity=0.5 TO 1.0 DURATION 30 LOOP
END BEHAVIOR
' Combine with other behavior commands
BEHAVIOR "fadeAndDissolve"
  BALPHA 1.0 TO 0.5 DURATION 60
  BSHADER "Dissolve" @progress=0 TO 1 DURATION 60
END BEHAVIOR

See also: ONCOLLISION, AFTERCOLLISION, SET SPRITE, ENABLE SPRITE, HASBEHAVIOR, SPRITEBEHAVIORS$, BEHAVIOR, CALL EVENT

↑ Back to top

HASBEHAVIOR

Behaviors · since 1.0.3

Returns 1 if the specified behavior is currently active on the sprite, 0 otherwise. Behavior name matching is case-insensitive.

Syntax

HASBEHAVIOR(spriteNum, "behaviorName")

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • behaviorName - Name of behavior to check

Examples

IF HASBEHAVIOR(0, "walkRight") THEN PRINT "Walking"
IF NOT HASBEHAVIOR(0, "idle") THEN
  SET SPRITE 0 BEHAVIOR "idle"
END IF

See also: BEHAVIOR, SPRITEBEHAVIORS$, SET SPRITE

↑ Back to top

HASPATTERN

Behaviors · since 1.0.3

Returns 1 (TRUE) if the specified sprite has an active pattern, 0 (FALSE) otherwise. With a pattern name, returns 1 only if that is the pattern currently running.

Use the name form to test which pattern is running, rather than comparing SPRITEPATTERN$. Pattern names match case-insensitively everywhere they are used to find something, but = on strings is case-sensitive — so IF SPRITEPATTERN$(n) = "walk" fails against a PATTERN "Walk" block, while IF HASPATTERN(n, "walk") does what you meant. This mirrors HASBEHAVIOR.

Added in 1.57.0: the optional pattern name. HASPATTERN(n) is unchanged.

Syntax

HASPATTERN(spriteNum)
HASPATTERN(spriteNum, patternName$)

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • patternName$ - Optional. Ask about ONE pattern: returns 1 only if that is the pattern running. Matched case-insensitively, like every other pattern name.

Examples

IF HASPATTERN(0) THEN PRINT "Pattern active"
IF NOT HASPATTERN(0) THEN
  SET SPRITE 0 PATTERN "defaultPattern"
END IF
IF HASPATTERN(playerId, "walk") THEN
  PRINT "still walking"
END IF
REM Case does not matter — this finds PATTERN "Walk"
IF HASPATTERN(playerId, "WALK") THEN PRINT "yes"

See also: PATTERN, SPRITEPATTERN$, SET SPRITE (PATTERN), HASBEHAVIOR

↑ Back to top

ONCOLLISION

Behaviors · since 1.53.0

Inside a BEHAVIOR block, runs inline BASIC when a sprite carrying this behavior begins touching something. The block is hoisted to an anonymous handler and fired by the behavior engine.

Edge-triggered. It fires on the tick a contact starts, not every tick the contact lasts. A character landing on the ground fires ONCOLLISION once; standing there fires nothing further. The contact stays "active" for as long as the engine keeps resolving it (or, for a ghost, while the overlap persists), and only a fresh contact fires again.

ONCOLLISION observes the world before collision resolution, which is the point of it: impact_vx / impact_vy hold the velocity the sprite was travelling at when it hit, the value resolution is about to zero or reflect away. Use AFTERCOLLISION to see the world once resolution has run.

Both sprites in a sprite-to-sprite hit fire their own handler, each seeing itself as index_a — including a stationary sprite that something ran into. A sprite touching a boundary reports index_b = -1 with the boundary's tag in tag_b$; because a tag names a boundary TYPE ("wall", "lava", "water"), a wall built from many same-tagged segments is one surface and sliding across the seams does not re-fire.

Handlers run inline on the behavior tick loop, so blocking statements are rejected at definition time: PAUSE, DELAY, DELAY UNTIL and YIELD would freeze every sprite in the game, not just this one. Anything the handler changes — a behavior swap, GHOST, SOLID — takes effect from the next tick.

A handler body follows the same scoping rules as a SUB: assigning a name that was never declared at program scope creates a handler-local that vanishes when the handler returns. Declare the variables you want to persist before the BEHAVIOR block.

When a behavior is composed with INCLUDES, every inherited ONCOLLISION runs, in composition order.

Bound polygons fire these too. A behaviour attached with SET MUTATION ... BEHAVIOR is carried by the shape's sprite half, so a SET POLYGON shape reports its collisions exactly as a sprite does, and index_a is the slot it occupies.

A handler may destroy its own sprite. DISABLE SPRITE index_a is what a bullet does when it strikes, and it is safe: any handler still queued for a sprite that no longer exists is skipped rather than run against nothing. That covers the two ordinary ways it happens — a sprite carrying more than one ONCOLLISION (two hits in a tick, or a behaviour composed with INCLUDES), and a particle whose lifetime ends on the same tick its contact was recorded.

Syntax

ONCOLLISION
  statements
END ONCOLLISION

Parameters

  • index_a - (in scope) This sprite's ID. Always the sprite the behavior is running on.
  • index_b - (in scope) The other sprite's ID, or -1 when the contact is with a boundary.
  • tag_a$ - (in scope) This sprite's tag.
  • tag_b$ - (in scope) The other sprite's tag, or the boundary's tag when index_b is -1.
  • edge$ - (in scope) Which face of the other party was struck: LEFT, RIGHT, TOP, BOTTOM or SLOPE. The two sides of one contact report opposite edges.
  • impact_vx - (in scope) Horizontal velocity at the moment of impact, captured before resolution alters it.
  • impact_vy - (in scope) Vertical velocity at the moment of impact, captured before resolution alters it.
  • THISSPRITE is also set to index_a, matching the convention used by PATTERN steps.

Examples

' Score when the ball strikes a brick
score = 0
BEHAVIOR "ball"
  BIMPULSE 6, -4
  ONCOLLISION
    IF tag_b$ = "brick" THEN score = score + 10
  END ONCOLLISION
END BEHAVIOR
' Damage scaled by how hard the landing was — impact_vy is the
' pre-resolution speed, which resolution would otherwise destroy
health = 100
BEHAVIOR "faller"
  BGRAVITY 0.5
  ONCOLLISION
    IF edge$ = "TOP" AND impact_vy > 12 THEN
      health = health - (impact_vy - 12)
    END IF
  END ONCOLLISION
END BEHAVIOR
' Landing on the ground: swap to a behavior without gravity
BEHAVIOR "falling"
  BGRAVITY 0.5
  ONCOLLISION
    IF tag_b$ = "ground" THEN SET SPRITE index_a BEHAVIOR "grounded"
  END ONCOLLISION
END BEHAVIOR
' Phase through on contact; AFTERCOLLISION fires once clear.
' Note SOLID SPRITES(...), not bare SOLID: GHOST cleared the sprite
' barrier tags, and plain SOLID only restores boundary solidity.
BEHAVIOR "phaser"
  ONCOLLISION
    SET SPRITE index_a GHOST
  END ONCOLLISION
  AFTERCOLLISION
    SET SPRITE index_a SOLID SPRITES("wall")
  END AFTERCOLLISION
END BEHAVIOR
' A stationary hazard reacting to whatever ran into it
BEHAVIOR "spikes"
  ONCOLLISION
    IF tag_b$ = "player" THEN EVENT "playerHurt"
  END ONCOLLISION
END BEHAVIOR

See also: AFTERCOLLISION, BEHAVIOR, BOUNDARY ADD, SET SPRITE, FOR COLLISIONS, SPRITETOUCHING$

↑ Back to top

PATTERN

Behaviors · since 1.0.4

Defines a sequence of behaviors and commands to chain together over time. Patterns provide automatic behavior sequencing without program logic. Three step modes: DURATION (run for fixed ticks), completion mode (run until behavior completes), and WHEN (conditional). Special commands control visibility (SHOW, HIDE, DISABLE, FADE), collision (SOLID, GHOST), tagging (RETAG), pattern switching (GOTOPATTERN), and event firing (BEVENT). WAITFOR parks the pattern on the current step until a named event fires — other sprites' patterns keep running normally. Commands can also have WHEN clauses for conditional execution. Use LOOP to repeat. When a pattern is activated with parameters (via SET SPRITE n PATTERN "name", @param=value), those parameters are automatically passed to any behaviors the pattern activates.

Declaration-time control flow. A pattern body may contain IF/ELSEIF/ELSE/END IF and SELECT CASE/CASE/CASE ELSE/END SELECT to choose which steps are baked into the pattern. Like the BEHAVIOR equivalent, these are evaluated once, when the PATTERN statement runs (not per tick), and the matching branch's steps are included in the registered pattern. This is distinct from a step's runtime WHEN clause, which re-checks each tick: use IF/SELECT to decide what the pattern contains at declaration, and WHEN to decide when an included step fires at runtime. An included step may still carry its own WHEN.

Pattern names are case-insensitive, like behavior names. SET SPRITE n PATTERN "Walk" finds the pattern defined as "walk", and so do GOTOPATTERN, INCLUDES and EMITPARTICLE.

The steps

A pattern is a list of steps, run in order. Everything below lives INSIDE a PATTERN ... END PATTERN block — none of it is a statement you can write on its own, which is why it is documented here rather than as separate keywords.

A step is one of three shapes:

[STEP] <behaviorName> [DURATION ticks] [WHEN condition]   ' run a behavior
[STEP] <command> [WHEN condition]                         ' do something once
[STEP] WAITFOR <eventName> [TIMEOUT ticks]                ' block until an event fires

Every name here is an expression. GOTOPATTERN "attack" is the literal form of one; GOTOPATTERN "phase" + STR$(n) works exactly as well, and the same goes for a behavior name, a RETAG tag and a BEVENT event name.

WHEN steps run IN PARALLEL with the sequential steps, not after them — a WHEN can fire on the very first tick while step 0 is still running.

Running a behavior

DURATION

"behaviorName" DURATION ticks

Inside a PATTERN block, specifies how long a behavior step should run before transitioning to the next step. Duration is measured in ticks (1/60th of a second at 60 FPS). This is one of three step modes; omit DURATION for completion mode.

  • behaviorName - Name of a defined BEHAVIOR
  • ticks - Number of ticks to run this step (60 ticks = 1 second at 60 FPS)

WHEN

"behaviorName" WHEN condition

Inside a PATTERN block, creates a conditional step that activates when the condition is true. WHEN steps only become active after all sequential steps (DURATION and completion mode) have finished. Multiple WHEN steps can be active simultaneously when their conditions are true.

  • behaviorName - Name of a defined BEHAVIOR
  • condition - Boolean expression to check each tick

Commands

SHOW

[STEP] SHOW [WHEN condition]

Inside a PATTERN block, shows the sprite by setting its visibility to true. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. Useful for creating visibility sequences or conditional display.

  • WHEN condition - Optional: only execute when condition becomes true

HIDE

[STEP] HIDE [WHEN condition]

Inside a PATTERN block, hides the sprite by setting its visibility to false. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. The sprite still exists and can be shown again later.

  • WHEN condition - Optional: only execute when condition becomes true

DISABLE

[STEP] DISABLE [WHEN condition]

Inside a PATTERN block, disables and destroys the sprite. Without WHEN, this is a command step that executes immediately. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. The pattern instance is removed along with the sprite, so the step runs exactly once. Useful for conditional destruction based on game state.

  • WHEN condition - Optional: only execute when condition becomes true

SOLID

[STEP] SOLID [WHEN condition]

Inside a PATTERN block, marks the sprite as solid for collision detection. Solid sprites will interact with boundaries and stop at solid barriers. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running.

  • WHEN condition - Optional: only execute when condition becomes true

GHOST

[STEP] GHOST [WHEN condition]

Inside a PATTERN block, marks the sprite as not solid (ghost mode). Ghost sprites pass through other sprites and boundaries without collision. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running.

  • WHEN condition - Optional: only execute when condition becomes true

FADE

[STEP] FADE [SPEED speedMultiplier] [WHEN condition]

Inside a PATTERN block, animates the sprite's alpha from 1 to 0 over time. Unlike instant commands, FADE is a transition step that takes time to complete before advancing to the next step. The sprite fades out gradually, creating a smooth disappearance effect. Often followed by DISABLE or HIDE. Note: WHEN clause is parsed but conditional FADE is not yet fully supported at runtime.

  • SPEED speedMultiplier - Optional speed multiplier (default: 1)
    • SPEED 1: Fade completes in 60 ticks (~1 second)
    • SPEED 2: Fade completes in 30 ticks (~0.5 seconds)
    • SPEED 0.5: Fade completes in 120 ticks (~2 seconds)
  • WHEN condition - Optional: parsed but not yet supported at runtime

RETAG

[STEP] RETAG "newTag" [WHEN condition]

Inside a PATTERN block, changes the sprite's tag to the specified value. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. Useful for changing sprite type during gameplay based on conditions.

  • "newTag" - New tag string to assign to the sprite
  • WHEN condition - Optional: only execute when condition becomes true

GOTOPATTERN

[STEP] GOTOPATTERN "patternName" [WHEN condition]

Inside a PATTERN block, switches the sprite to a different pattern. This allows patterns to chain into other patterns based on conditions or at specific points in the sequence. When executed, the current pattern stops and the new pattern begins from its first step. Commonly used with WHEN for conditional pattern transitions like switching from patrol to attack when a player is detected.

  • "patternName" - Name of the pattern to switch to (case-insensitive)
  • WHEN condition - Optional: only switch patterns when condition is true

BEVENT

[STEP] BEVENT "eventName" [SYNC] [WHEN condition]

Inside a PATTERN block, fires a one-time event passing the current sprite ID as an argument. The event is fired once per pattern execution (or per WHEN trigger if conditional). Any event handler registered with CALL EVENT will receive the sprite ID as its first argument. By default, the handler runs asynchronously in a new thread. The SYNC modifier makes the handler run synchronously, blocking the pattern tick until it completes. This is useful when the handler sets variables or state that subsequent pattern steps depend on.

  • "eventName" - Name of the event to fire (case-insensitive, matches CALL EVENT registration)
  • SYNC - Optional: run the event handler synchronously (blocks pattern tick until handler returns)
  • WHEN condition - Optional: only fire event when condition is true

WAITFOR

[STEP] WAITFOR eventName [TIMEOUT ticks]

Blocks this sprite's pattern on the current step until the named event fires (via EVENT or BEVENT). Other sprites' patterns are unaffected — only this one waits.

It waits for a NEW firing. The event's fire count is snapshotted when the step is reached, and the wait ends when that count changes — so an event that already fired before the pattern got here does not satisfy it. The step also parks for at least one tick on arrival, and forgets it was waiting whenever the pattern leaves the step (advance, GOTOPATTERN, loop), so coming back starts a fresh wait rather than resuming a stale one.

TIMEOUT ticks advances the pattern anyway after that many ticks, so a pattern cannot be stranded by an event that never arrives. Without it, the step waits indefinitely.

Both the event name and the timeout are expressions.

  • eventName - Event to wait for. Any expression.
  • TIMEOUT ticks - Optional. Advance after this many ticks even if the event never fires.

Syntax

PATTERN "name" [INCLUDES("other1", "other2", ...)] [LOOP]
  [STEP] "behavior1" [DURATION ticks]
  [STEP] "behavior2" [WHEN condition]
  [STEP] SHOW | HIDE | DISABLE | SOLID | GHOST | FADE | RETAG | GOTOPATTERN | BEVENT [WHEN condition]
  [STEP] WAITFOR "eventName" [TIMEOUT ticks]
  ...
END PATTERN

Parameters

  • name - String expression for pattern name (case-insensitive)
  • INCLUDES("name1", "name2", ...) - Optional. Include steps from other patterns. Included steps come first, then this pattern's steps. Included patterns must be defined before this one.
  • LOOP - Optional modifier to repeat pattern after completion
  • Inside PATTERN blocks:
  • [STEP] "behaviorName" DURATION ticks - Run behavior for fixed duration
  • [STEP] "behaviorName" - Completion mode: run until behavior completes (path ends, animation cycles, transition finishes)
  • [STEP] "behaviorName" WHEN condition - Conditional: active while condition is true
  • [STEP] SHOW [WHEN condition] - Show the sprite (optionally conditional)
  • [STEP] HIDE [WHEN condition] - Hide the sprite (optionally conditional)
  • [STEP] DISABLE [WHEN condition] - Disable/destroy the sprite (optionally conditional)
  • [STEP] SOLID [WHEN condition] - Mark sprite as solid for collision (optionally conditional)
  • [STEP] GHOST [WHEN condition] - Mark sprite as not solid (optionally conditional)
  • [STEP] FADE [SPEED n] - Animate alpha to 0 over time (transition step)
  • [STEP] RETAG "tag" [WHEN condition] - Change sprite tag (optionally conditional)
  • [STEP] GOTOPATTERN "name" [WHEN condition] - Switch to a different pattern (optionally conditional)
  • [STEP] BEVENT "eventName" [SYNC] [WHEN condition] - Fire a one-time event with the current sprite as argument. SYNC makes it blocking. (optionally conditional)
  • [STEP] WAITFOR "eventName" [TIMEOUT ticks] - Park this sprite's pattern on the current step until the named event fires (broadcast from EVENT / BEVENT). Only firings that occur after the step starts count. Optional TIMEOUT (in pattern ticks) forces the pattern to advance after that many ticks even if the event never fires. Other sprites' patterns are unaffected. WAITFOR does not support WHEN.
  • STEP keyword is optional
  • Commands with WHEN execute once when condition becomes true, and can re-execute if condition cycles back to true

Examples

'Fixed duration steps
PATTERN "walkCycle" LOOP
  "walkRight" DURATION 120
  "idle" DURATION 60
END PATTERN
'Completion mode - waits for behavior to complete
PATTERN "fadeAndDie"
  "fadeOut"         ' Runs until alpha transition completes
  DISABLE           ' Then disable the sprite
END PATTERN
'Conditional steps (WHEN mode)
PATTERN "playerControl" LOOP
  "idle"                          ' Default behavior
  "walkRight" WHEN KEYDOWN("RIGHT")
  "walkLeft" WHEN KEYDOWN("LEFT")
END PATTERN
'Declaration-time IF / SELECT choose which steps the pattern contains
'(decided once when the PATTERN runs; WHEN still works on included steps).
PATTERN "ai" LOOP
  IF boss THEN
    "charge" DURATION 60
  ELSE
    "walk" DURATION 30
  END IF
  SELECT CASE mode
    CASE 1
      "patrol" DURATION 90
    CASE ELSE
      "idle" DURATION 30
  END SELECT
END PATTERN
'Commands with WHEN conditions
PATTERN "enemy" LOOP
  "patrol"
  RETAG "dead" WHEN health <= 0
  DISABLE WHEN health <= 0
END PATTERN
'Show/hide for blinking effect
PATTERN "spawning" LOOP
  SHOW
  "visible" DURATION 10
  HIDE
  "hidden" DURATION 10
END PATTERN
'Death sequence with ghost mode
PATTERN "death"
  GHOST               ' Stop colliding
  "hit" DURATION 10
  "fall"              ' Completion mode
  "fadeOut"           ' Completion mode
  DISABLE             ' Remove sprite
END PATTERN
' Parameters flow from pattern to behaviors
' Define behavior with parameters:
BEHAVIOR "moveToPoint"
  BMOVE @targetX=0, @targetY=0
END BEHAVIOR

' Pattern uses the parameterized behavior:
PATTERN "chase" LOOP
  "moveToPoint" DURATION 60
END PATTERN

' Activate with parameters (flow to behaviors):
SET SPRITE 0 PATTERN "chase", @targetX=enemyX, @targetY=enemyY
' Compose patterns using INCLUDES
PATTERN "intro"
  "fadeIn" DURATION 30
  "flash" DURATION 10
END PATTERN

' Combined pattern - gets intro steps first, then adds gameplay loop
PATTERN "fullGame" INCLUDES("intro") LOOP
  "idle"
  "walk" WHEN KEYDOWN("RIGHT")
END PATTERN
' Sync pattern steps to an external event
PATTERN "bossAttack" LOOP
  "windup"  DURATION 30
  WAITFOR   "drumHit"             ' Wait for the next drumHit before swinging
  "swing"   DURATION 20
  WAITFOR   "drumHit" TIMEOUT 120  ' Wait for next drumHit, but cap at 2 seconds
END PATTERN
PATTERN "walk"
  "walkRight" DURATION 120  ' Run walkRight for 2 seconds
END PATTERN
PATTERN "rest"
  "idle" DURATION 30        ' Run idle for 0.5 seconds
END PATTERN
PATTERN "combo"
  STEP "attack" DURATION 15 ' Optional STEP keyword
END PATTERN
PATTERN "moveOnKey"
  "moveLeft" WHEN KEYDOWN("LEFT")   ' Move when left key is pressed
END PATTERN
PATTERN "jumpOnKey"
  "jump" WHEN KEYDOWN("SPACE")      ' Jump when space is pressed
END PATTERN
' Player control pattern
PATTERN "player" LOOP
  "idle"
  "walkRight" WHEN KEYDOWN("RIGHT")
  "walkLeft" WHEN KEYDOWN("LEFT")
  "jump" WHEN KEYDOWN("SPACE")
END PATTERN
PATTERN "reveal"
  SHOW                         ' Make sprite visible immediately
END PATTERN
PATTERN "revealWhenNear"
  SHOW WHEN playerNearby       ' Show when player is nearby
END PATTERN
' Blinking spawn effect
PATTERN "spawning" LOOP
  SHOW
  "visible" DURATION 10
  HIDE
  "pause" DURATION 10
END PATTERN
' Conditional visibility
PATTERN "hiddenTrap" LOOP
  "idle"
  SHOW WHEN SPRITEDIST(THISSPRITE, 0) < 50
END PATTERN
PATTERN "vanish"
  HIDE                         ' Make sprite invisible immediately
END PATTERN
PATTERN "hideInStealth"
  HIDE WHEN stealthMode        ' Hide when stealth is activated
END PATTERN
' Disappear effect
PATTERN "vanish"
  "fadeOut"                   ' Completion mode
  HIDE
END PATTERN
' Conditional hiding
PATTERN "shyEnemy" LOOP
  "patrol"
  HIDE WHEN SPRITEDIST(THISSPRITE, 0) < 30
END PATTERN
PATTERN "destroy"
  DISABLE                      ' Disable sprite immediately
END PATTERN
PATTERN "destroyWhenDead"
  DISABLE WHEN health <= 0     ' Destroy when health depleted
END PATTERN
' Death sequence
PATTERN "death"
  "hit" DURATION 10
  "fall"                      ' Completion mode
  "fadeOut"                   ' Completion mode
  DISABLE                     ' Remove sprite
END PATTERN
' Conditional destruction
PATTERN "enemy" LOOP
  "patrol"
  DISABLE WHEN GETSPRITEMETA(THISSPRITE, "health") <= 0
END PATTERN
' Explosion effect
PATTERN "explode"
  "explosion"                 ' Play animation until done
  DISABLE                     ' Then remove
END PATTERN
PATTERN "makeSolid"
  SOLID                        ' Mark sprite as solid immediately
END PATTERN
PATTERN "solidOnGround"
  SOLID WHEN grounded          ' Become solid when touching ground
END PATTERN
' Landing sequence - become solid when landing
PATTERN "landing"
  "falling"                   ' Fall animation
  SOLID                       ' Become solid on landing
  "idle"
END PATTERN
' Conditional solidity
PATTERN "platformer" LOOP
  "idle"
  SOLID WHEN SPRITEY(THISSPRITE) > 100
END PATTERN
PATTERN "makeGhost"
  GHOST                        ' Mark sprite as not solid immediately
END PATTERN
PATTERN "ghostWhenInvincible"
  GHOST WHEN invincible        ' Become ghost when invincible
END PATTERN
' Invincibility power-up effect
PATTERN "invincible"
  GHOST                       ' Pass through enemies
  "flashing" DURATION 300     ' Flash for 5 seconds
  SOLID                       ' Return to normal
END PATTERN
' Conditional ghost mode
PATTERN "phaseAbility" LOOP
  "idle"
  GHOST WHEN KEYDOWN("SHIFT")
END PATTERN
' Death sequence - become ghost before falling
PATTERN "dying"
  GHOST                       ' Stop colliding
  "deathSpin" DURATION 60
  "fall"
  DISABLE
END PATTERN
PATTERN "fadeOut"
  FADE                          ' Fade out over 1 second
END PATTERN
PATTERN "quickFade"
  FADE SPEED 2                  ' Fast fade (0.5 seconds)
END PATTERN
PATTERN "slowFade"
  FADE SPEED 0.5                ' Slow fade (2 seconds)
END PATTERN
' Death sequence with fade
PATTERN "death"
  GHOST
  "hit" DURATION 15
  FADE SPEED 1.5
  DISABLE
END PATTERN
' Collectible pickup effect
PATTERN "collected"
  "sparkle" DURATION 20
  FADE SPEED 3
  DISABLE
END PATTERN
' Ghost enemy fade out
PATTERN "vanish"
  "spin" DURATION 30
  FADE
  HIDE
END PATTERN
' Slow dramatic fade
PATTERN "farewell"
  "wave" DURATION 60
  FADE SPEED 0.3
  DISABLE
END PATTERN
PATTERN "markDead"
  RETAG "dead"                  ' Change sprite tag immediately
END PATTERN
PATTERN "retagWhenDead"
  RETAG "enemy" WHEN health <= 0  ' Retag when health depleted
END PATTERN
' Enemy transforms when defeated
PATTERN "defeat"
  GHOST
  RETAG "collectible"
  "spin" DURATION 60
  DISABLE
END PATTERN
' Conditional retagging based on state
PATTERN "enemy" LOOP
  "patrol"
  RETAG "enraged" WHEN GETSPRITEMETA(THISSPRITE, "health") < 20
END PATTERN
' Boss phase transition
PATTERN "bossPhase2"
  RETAG "boss_phase2"
  "transform" DURATION 60
  SOLID
END PATTERN
PATTERN "toAttack"
  GOTOPATTERN "attack"           ' Switch to attack pattern
END PATTERN
PATTERN "toIdleWhenStill"
  GOTOPATTERN "idle" WHEN NOT moving
END PATTERN
' Enemy AI with pattern switching
PATTERN "patrol" LOOP
  "walkLeft" DURATION 60
  "walkRight" DURATION 60
  GOTOPATTERN "chase" WHEN playerNearby
END PATTERN
' Chase pattern returns to patrol
PATTERN "chase"
  "runToPlayer" DURATION 120
  GOTOPATTERN "attack" WHEN playerInRange
  GOTOPATTERN "patrol"          ' Return to patrol if no attack
END PATTERN
' State machine with patterns
PATTERN "idle" LOOP
  "stand"
  GOTOPATTERN "walk" WHEN KEYDOWN("RIGHT") OR KEYDOWN("LEFT")
  GOTOPATTERN "jump" WHEN KEYDOWN("SPACE")
END PATTERN
PATTERN "explode"
  BEVENT "explosion"           ' Fire explosion event asynchronously
END PATTERN
PATTERN "attackSync"
  BEVENT "attack" SYNC         ' Fire and wait for handler to complete
END PATTERN
PATTERN "attackWhenReady"
  BEVENT "attack" WHEN attacking
END PATTERN
PATTERN "damageWhenAttacking"
  BEVENT "calcDamage" SYNC WHEN attacking  ' Sync + conditional
END PATTERN
' Pattern fires event when animation reaches a key frame
PATTERN "attack" LOOP
  "windup" DURATION 15
  BEVENT "attackHit"       ' Fire hit event at attack frame
  "followThrough" DURATION 15
END PATTERN
' Synchronous event to set state before next step
PATTERN "boss" LOOP
  BEVENT "chooseAttack" SYNC  ' Handler sets ATTACKTYPE variable
  "windupA" WHEN ATTACKTYPE = 1
  "windupB" WHEN ATTACKTYPE = 2
END PATTERN

See also: BEHAVIOR, SET SPRITE (PATTERN), HASPATTERN, SPRITEPATTERN$, PATTERN, WHEN, EVENT, DELAY

↑ Back to top

SAVE BEHAVIOR

Behaviors · since 1.46.0

Serialize a registered behavior or pattern to a JSON file, and load it back. The unit of the foundation for a sprite rigger: author behaviors/patterns once, save them, and reuse across projects.

SAVE BEHAVIOR "name" TO file$ writes the behavior in its resolved form — declaration-time IF/SELECT is already flattened away, while @param defaults and VARIANCE are preserved. SAVE PATTERN "name" TO file$ writes the pattern's steps (keeping runtime WHEN conditions and durations intact) and its LOOP flag.

LOAD BEHAVIOR file$ / LOAD PATTERN file$ register every behavior/pattern in the file by its saved name; re-loading an existing name overwrites it, exactly like re-declaring the block. Saving an undefined name, or loading a missing/mismatched file, is a loud runtime error. The file is a single versioned JSON schema shared with rig files.

Syntax

SAVE BEHAVIOR "name" TO file$
LOAD BEHAVIOR file$
SAVE PATTERN "name" TO file$
LOAD PATTERN file$

Parameters

  • name - The registered behavior/pattern name to save
  • file$ - JSON file path (saved via the same VFS as SAVETEXT)

Examples

BEHAVIOR "walk"
  BMOVE @speed=2, 0
END BEHAVIOR
SAVE BEHAVIOR "walk" TO "walk.json"
' ...later, or in another project:
LOAD BEHAVIOR "walk.json"   ' re-registers "walk", @speed default intact
PATTERN "ai" LOOP
  "walk" DURATION 30
  "walk" WHEN KEYDOWN("RIGHT")
END PATTERN
SAVE PATTERN "ai" TO "ai.json"
LOAD PATTERN "ai.json"

See also: LOAD BEHAVIOR, SAVE RIG, BEHAVIOR, PATTERN

↑ Back to top

SAVE RIG

Behaviors · since 1.46.0

Save and load a sprite rig — a tree of parent/child sprites plus the behaviors and patterns they use — as a single JSON file. This is the unit a sprite rigger produces: a torso with a head, arms, and legs attached at offsets, each part carrying its own image, hitbox, and behavior.

SAVE RIG FROM SPRITE id TO file$ walks the parent/child tree rooted at id (any depth) and captures each node: image, relative offset from its parent, whether the attachment is RIGID (orbit + inherited spin), frame, bbox, scale, flip, visibility, solid flag, tag, alpha, rotation, origin, elevation, layer, and its active behavior(s) or pattern. Every behavior/pattern referenced anywhere in the tree is collected into the file once. The root's absolute position is NOT saved — placement is per-instance; child offsets ARE saved, since they are the rig's structure.

LOAD RIG file$ registers the file's behaviors and patterns only. LOAD RIG file$ ONTO SPRITE id additionally rebuilds the tree: the root's bindings are applied to id (kept at its current position), and each child is created as a fresh sprite at its relative offset, parented, and given its bindings + behavior/pattern — recursively. Loading a behavior/pattern-only file ONTO a sprite (no tree in it) is a loud error.

Syntax

SAVE RIG FROM SPRITE id TO file$
LOAD RIG file$
LOAD RIG file$ ONTO SPRITE id

Parameters

  • id (SAVE) - The root sprite of the rig to capture (its children are walked automatically)
  • id (LOAD ... ONTO) - The sprite to use as the rebuilt rig's root; children are allocated fresh ids
  • file$ - JSON rig file path

Examples

' Rig a two-part sprite, then save the whole thing.
SET SPRITE 0, 100, 100, "torso"
SET SPRITE 1, 100, 80, "head"
SET SPRITE 1 PARENT(0)
SET SPRITE 1 BEHAVIOR "bob"
SAVE RIG FROM SPRITE 0 TO "hero.rig.json"
' Spawn a fresh copy of the rig onto a new root sprite.
rootId = NEXTFREESPRITE()
SET SPRITE rootId, 200, 150, "torso"
LOAD RIG "hero.rig.json" ONTO SPRITE rootId  ' head re-attaches at its offset
' Register a rig's behaviors/patterns without spawning sprites.
LOAD RIG "hero.rig.json"

See also: SAVE BEHAVIOR, SET SPRITE, NEXTFREESPRITE, BEHAVIOR, PATTERN

↑ Back to top

SET BEHAVIOR RATE

Behaviors · since 1.57.0

Sets how many times per second the behavior engine ticks. The default is 60. Takes effect on the next tick, so a program can change pace between game states — a calm title screen, brisker gameplay — without restarting anything.

What scales and what doesn't. Anything authored in ticks scales with the rate; that is the point of the statement. Anything authored in real units is re-derived so it keeps meaning the same thing:

Authored in Examples On SET BEHAVIOR RATE 120
ticks BMOVE/BIMPULSE velocity (per tick), BFRICTION decay, BOSCILLATE speed, EMITPARTICLE ticksToLive, DURATION in ticks runs twice as fast
real time ANIMATE … FRAMERATE (frames per second), Aseprite millisecond frame durations, pattern SPEED (steps per second) unchanged

So doubling the rate makes sprites move twice as quickly while their animations still play at the authored frame rate.

Other timing systems keep their own clocks and are unaffected: CALL TIMER, DELAY, COOLDOWN and shader TIME are wall-clock, not tick-driven. The behavior engine is not the program's master clock.

Multiplayer: every interpreter runs the same program, so set the rate from code they all reach — not inside an IF ME.ISHOST branch — or the host and clients will simulate motion at different speeds.

Accepts 1 to 240. Outside that range is a runtime error rather than a silent clamp: 0 has no meaning (the tick interval divides by it) and an absurd rate is a bug worth hearing about.

Syntax

SET BEHAVIOR RATE ticksPerSecond

Parameters

  • ticksPerSecond - Behavior engine ticks per second (1-240, default 60)

Examples

REM Slow, drifting motion on the title screen; normal speed in play.
SET BEHAVIOR RATE 30
CALL ShowTitle()

SET BEHAVIOR RATE 60
CALL PlayGame()
REM A bullet-time effect: everything authored in ticks slows down,
REM while animations keep their authored frame rate.
SET BEHAVIOR RATE 20
DELAY 2000            ' DELAY is wall-clock, so this is still 2 real seconds
SET BEHAVIOR RATE 60
REM Rates can come from an expression, e.g. a settings value.
DIM smooth AS INTEGER
smooth = 120
SET BEHAVIOR RATE smooth

See also: BEHAVIOR, SET SPRITE, EMITPARTICLE, SET YIELD, COOLDOWN

↑ Back to top

SET MUTATION BEHAVIOR

Behaviors · since 1.4.3

Applies, removes, or clears behaviors on a MUTATION variable. When a behavior is applied, all points in the mutation move together as a unit. Multiple behaviors can be stacked. Behaviors are automatically cleared when the mutation goes out of scope.

Parameters. @param=value overrides are resolved exactly as they are for SET SPRITE ... BEHAVIOR: the definition's defaults merged with the overrides, baked into the commands at apply time. This is what lets one definition drive a pool — each slot gets its own speed, direction and spin from the same BEHAVIOR block.

Pooling. The target may be an array slot (rocks(i)), so a loop can bind a whole field of shapes to one behavior.

What a behavior can do to a mutation. Everything that describes motion or geometry: position (BMOVE, PATH, BFOLLOW, BORBIT, BOSCILLATE, BSHAKE), velocity and momentum (BVELOCITY, BACCELERATION, BIMPULSE, BFRICTION, BSTOP), and transforms (BROTATE, BSCALE, BFLIPX, BFLIPY) which are baked into the points about the shape's own extent. Appearance splits in two. BALPHA and BBLINK work on a shape bound to a polygon — they drive its sprite half, so a shape fades and blinks like anything else on screen (bind it to nothing and there is naturally nothing to fade). ANIMATE and BIMAGE do not apply at all: both select frames of an image, and a shape has no image — its colour, fill and texture come from SET POLYGON.

Poses do not drift. BROTATE, BSCALE and the flips are computed from the shape as you authored it, not from the shape as it was last posed, so scaling out and back returns the coordinates you wrote and a shape turned one degree at a time comes home after 360 of them. Writing the shape's points yourself (SET MUTATIONPOINT, re-assignment) makes that the new baseline and ends the behavior: a mutation is readable while a behavior drives it — that is how a program watches where a shape has got to — but writing it means the shape the behavior was attached to no longer exists, so the behavior is removed rather than left running against geometry it no longer describes. Re-apply it if you still want it. Behavior motion is not authoring and does not end anything.

A shape bound to a polygon is driven by its sprite half too. SET POLYGON n, BEHAVIOR "name" attaches to the same shape, and binding a polygon to a mutation that already carries a behavior copies it onto that half so its collision handlers have something to fire against. Position a shape that must KEEP its behavior with SET POLYGON n, LOCATION(x, y) rather than by writing its points.

Syntax

SET MUTATION varName BEHAVIOR "behaviorName" [, @param=value, ...]
SET MUTATION mutationArray(slot) BEHAVIOR "behaviorName" [, @param=value, ...]
SET MUTATION varName BEHAVIOR DELETE "behaviorName"
SET MUTATION varName BEHAVIOR NONE

Parameters

  • varName - Name of the MUTATION variable
  • behaviorName - Name of a defined BEHAVIOR BLOCK
  • DELETE - Removes a specific behavior from the mutation
  • NONE - Clears all behaviors from the mutation

Examples

'Define a movement behavior
BEHAVIOR "moveRight"
  BMOVE 2, 0
END BEHAVIOR

'Apply to mutation
DIM path AS MUTATION
path = MUTATION((0, 0), (10, 10), (20, 0))
SET MUTATION path BEHAVIOR "moveRight"
'Remove specific behavior
SET MUTATION path BEHAVIOR DELETE "moveRight"
'Clear all behaviors
SET MUTATION path BEHAVIOR NONE
'Stack multiple behaviors
BEHAVIOR "oscillate"
  BOSCILLATE AMPLITUDE 0, 10
END BEHAVIOR

SET MUTATION path BEHAVIOR "moveRight"
SET MUTATION path BEHAVIOR "oscillate"

See also: MUTATION, BEHAVIOR BLOCK, BEHAVIOR

↑ Back to top

SPRITEBEHAVIORS$

Behaviors · since 1.0.3

Returns a comma-separated string of all behavior names currently active on the specified sprite. Returns empty string if no behaviors are active.

Names come back spelled the way they were written when the behavior was attached. Attaching the same behavior again in different case does not list it twice — it is one behavior.

Syntax

SPRITEBEHAVIORS$(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

PRINT SPRITEBEHAVIORS$(0)  ' Might print "walkRight,fadeIn"
behaviors$ = SPRITEBEHAVIORS$(0)
IF behaviors$ = "" THEN
  PRINT "No active behaviors"
END IF

See also: BEHAVIOR, HASBEHAVIOR, SET SPRITE

↑ Back to top

SPRITEPATTERN$

Behaviors · since 1.0.3

Returns the name of the currently active pattern on the specified sprite, or an empty string if no pattern is active.

The name comes back spelled the way the PATTERN block declared it, not the way the statement that attached it happened to be capitalised — so comparing this against your own literal keeps working now that the lookup itself ignores case.

To test WHICH pattern is running, use HASPATTERN(n, name$) instead of comparing this string. Pattern names match case-insensitively, but = on strings does not, so SPRITEPATTERN$(n) = "walk" is false for a PATTERN "Walk" block. This function is for reporting the name — showing it, logging it, saving it — not for deciding on it.

Syntax

SPRITEPATTERN$(spriteNum)

Parameters

  • spriteNum - Sprite slot number (0-99,999)

Examples

PRINT SPRITEPATTERN$(0)  ' Might print "walkCycle"
currentPattern$ = SPRITEPATTERN$(0)
IF currentPattern$ <> "attackSequence" THEN
  SET SPRITE 0 PATTERN "attackSequence"
END IF

See also: PATTERN, HASPATTERN, SET SPRITE (PATTERN)

↑ Back to top

SPRITEVELOCITYX

Behaviors · since 1.0.3

Returns the sprite's current total X velocity (includes BVELOCITY, BACCELERATION, and any other velocity-affecting behaviors). Returns 0 if the sprite has no active velocity. Pass the optional literal "direction" as a second argument to get the sign of the velocity (-1, 0, or 1) instead of the raw value.

Syntax

SPRITEVELOCITYX(spriteNum [, "direction"])

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • "direction" - Optional literal string; returns the sign of the velocity (-1, 0, or 1) instead of the raw value.

Examples

vx = SPRITEVELOCITYX(0)  ' Get player's current X velocity
' Check if moving right:
IF SPRITEVELOCITYX(0) > 0 THEN movingRight = 1
' Cap maximum velocity:
IF ABS(SPRITEVELOCITYX(0)) > 10 THEN
  PRINT "Too fast!"
END IF
' Display velocity:
PRINT "VX:" + STR$(SPRITEVELOCITYX(0)) + " VY:" + STR$(SPRITEVELOCITYY(0))

See also: SPRITEVELOCITYY, SPRITEDIRECTION, BEHAVIOR

↑ Back to top

SPRITEVELOCITYY

Behaviors · since 1.0.3

Returns the sprite's current total Y velocity (includes BVELOCITY, BACCELERATION, and any other velocity-affecting behaviors). Returns 0 if the sprite has no active velocity. Positive values indicate downward movement. Pass the optional literal "direction" as a second argument to get the sign of the velocity (-1, 0, or 1) instead of the raw value.

Syntax

SPRITEVELOCITYY(spriteNum [, "direction"])

Parameters

  • spriteNum - Sprite slot number (0-99,999)
  • "direction" - Optional literal string; returns the sign of the velocity (-1, 0, or 1) instead of the raw value.

Examples

vy = SPRITEVELOCITYY(0)  ' Get player's current Y velocity
' Check if falling (positive Y = downward):
IF SPRITEVELOCITYY(0) > 0 THEN falling = 1
' Detect landing (was falling, now stopped):
IF wasFalling AND SPRITEVELOCITYY(0) <= 0 THEN
  PRINT "Landed!"
END IF
' Calculate total speed:
speed = SQR(SPRITEVELOCITYX(0)^2 + SPRITEVELOCITYY(0)^2)

See also: SPRITEVELOCITYX, SPRITEDIRECTION, BEHAVIOR

↑ Back to top

THISSPRITE

Behaviors · since 1.12.1

A special variable that returns the sprite ID of the sprite currently being evaluated in a PATTERN WHEN condition. Only valid inside WHEN condition expressions - allows patterns to reference metadata or properties of the specific sprite they are attached to. This enables writing generic patterns that work with any sprite.

Syntax

THISSPRITE

Parameters

  • Returns the sprite ID (0-99,999) of the sprite currently running the pattern

Examples

' Generic enemy pattern that checks its own metadata
PATTERN "enemyAI" LOOP
  "patrol" WHEN GETSPRITEMETA(THISSPRITE, "state") = "patrol"
  "chase" WHEN GETSPRITEMETA(THISSPRITE, "state") = "chase"
  "attack" WHEN GETSPRITEMETA(THISSPRITE, "state") = "attack"
END PATTERN
' Check collision with player using sprite's own ID
PATTERN "enemy" LOOP
  "idle"
  "attack" WHEN SPRITECOLLISION(THISSPRITE, 0)
END PATTERN
' Pattern that responds to sprite's own tag
PATTERN "tagBehavior" LOOP
  "normalMove" WHEN SPRITETAG$(THISSPRITE) = "normal"
  "fastMove" WHEN SPRITETAG$(THISSPRITE) = "powered"
END PATTERN
' Check if this sprite is near a specific position
PATTERN "zoneAware" LOOP
  "idle"
  "activate" WHEN SPRITEX(THISSPRITE) > 100 AND SPRITEX(THISSPRITE) < 200
END PATTERN

See also: PATTERN, GETSPRITEMETA, SPRITETAG$, SPRITEX, SPRITEY

↑ Back to top

VARIANCE

Behaviors · since 1.18.1

Adds randomness to numeric parameters in BEHAVIOR commands. When a behavior is assigned to a sprite, variance values are resolved once to create per-sprite variation. VARIANCE produces floating-point results, VARIANCEINT produces integer results. The NONZERO modifier ensures the result is never zero (useful for velocities where zero would mean no movement).

Syntax

value VARIANCE(low, high)
value VARIANCEINT(low, high)
value VARIANCEINT(low, high, NONZERO)

Parameters

  • value - Base value to add variance to
  • low - Minimum variance offset (can be negative)
  • high - Maximum variance offset
  • Result = value + random(low, high)
  • VARIANCE - Floating-point result
  • VARIANCEINT - Integer result (floored)
  • NONZERO - Only for VARIANCEINT: guarantees result is not zero

Examples

' Float variance on velocity
BEHAVIOR "spread"
  BVELOCITY 0 VARIANCE(-3, 3), -5 VARIANCE(-1, 1)
END BEHAVIOR
' Integer variance (useful for frame rates, pixel values)
BEHAVIOR "randomAnim"
  ANIMATE "spark" FRAMERATE 8 VARIANCEINT(-2, 4)
END BEHAVIOR
' NONZERO ensures movement (never zero velocity)
BEHAVIOR "scatter"
  BVELOCITY 0 VARIANCEINT(-5, 5, NONZERO), 0 VARIANCEINT(-5, 5, NONZERO)
END BEHAVIOR
' Variance on multiple parameters
BEHAVIOR "randomOrbit"
  BORBIT SPRITE 0 DISTANCE 50 VARIANCE(-10, 10) SPEED 3 VARIANCE(-1, 2)
END BEHAVIOR
' Particle spread effect
BEHAVIOR "explosion"
  BVELOCITY 0 VARIANCEINT(-8, 8, NONZERO), 0 VARIANCEINT(-8, 8, NONZERO)
  BACCELERATION 0, 0.2
  BALPHA 1 TO 0 DURATION 30 VARIANCEINT(-5, 10)
END BEHAVIOR
' Scatter oscillation phases so a swarm of bobbers doesn't move in lockstep
BEHAVIOR "swarmBob"
  BOSCILLATE AMPLITUDE 0, 6 SPEED 4 PHASE 0 VARIANCE(0, 6.28)
END BEHAVIOR

See also: BEHAVIOR

↑ Back to top

Collisions

BOUNDARY ADD

Collisions · since 1.24.0

Creates a named boundary region for collision detection. Boundaries are invisible regions that sprites can collide with, useful for walls, floors, trigger zones, hills, ramps, and level boundaries. Use FOR COLLISIONS with a boundary tag variable to detect sprite-boundary collisions. Normal boundaries trigger collisions when sprites ENTER the region. ENCLOSURE boundaries trigger collisions when sprites try to LEAVE the region (inverted boundaries).

By default the boundary is a rectangular AABB. Add the SLOPE modifier to turn it into a half-plane: the bounding rect is split along its diagonal and only the triangle anchored at the named compass corner is solid — the other half is empty. This lets you build hills, ramps, and corner cuts using the same tag-based collision API. Sprites colliding with a SOLID slope slide along the diagonal surface (velocity-projection physics) instead of being shoved perpendicular off it. Slope hits report "SLOPE" as the edge string in FOR COLLISIONS.

Syntax

BOUNDARY ADD "tag", (x1,y1)-(x2,y2) [, layer] [SOLID] [ENCLOSURE] [SLOPE direction]

Parameters

  • "tag" - String expression for boundary name (e.g., "left-wall", "floor", "hill")
  • (x1,y1) - Top-left corner coordinates of the boundary rectangle
  • (x2,y2) - Bottom-right corner coordinates of the boundary rectangle
  • layer - Optional layer number. By default, boundaries apply to ALL layers. Specify a layer number to limit collision to sprites on that specific layer only.
  • SOLID - Optional keyword. When specified, sprites with behaviors cannot pass through this boundary. Their movement will be stopped at the boundary edge (or slid along it for SLOPE boundaries).
  • ENCLOSURE - Optional keyword. Inverts the boundary behavior: collision triggers when sprites try to LEAVE the region instead of entering it. Useful for keeping sprites inside a play area.
  • SLOPE direction - Optional keyword. Turns the bounding rect into a half-plane where only the triangle anchored at the named compass corner is solid. The diagonal of the rect is the collision surface. Direction is one of NE / NW / SE / SW (or long forms NORTHEAST / NORTHWEST / SOUTHEAST / SOUTHWEST). Compass directions map to VISUAL screen direction in CrashGFX's Y-down coordinate system: NE = upper-right corner = (xmax, ymin). NE/SW share one diagonal (visually ""); NW/SE share the other (visually "/"). The named corner is the SOLID anchor, so picking the right direction depends on whether you want a hill (sprite walks UP onto it — solid is below the diagonal, so use SE for climbs-right and SW for climbs-left) or a ceiling/overhang (sprite hits the diagonal from below — solid is above, so use NE/NW). Example: SLOPE SE on a 32×32 rect makes the lower-right triangle solid; a sprite walking right enters at the rect's left edge (where the diagonal meets the floor) and rises up the slope until it exits the right edge at the rect's full height.

Examples

BOUNDARY ADD "left-wall", (0,0)-(10,480)
BOUNDARY ADD "floor", (0,470)-(640,480)
BOUNDARY ADD "platform", (100,300)-(200,310), 1
BOUNDARY ADD "solid-wall", (0,0)-(10,480) SOLID
BOUNDARY ADD "floor", (0,470)-(640,480), 0 SOLID
BOUNDARY ADD "play-area", (0,0)-(320,200) SOLID ENCLOSURE
' Create walls around a game area
BOUNDARY ADD "left", (0,0)-(10,480) SOLID
BOUNDARY ADD "right", (630,0)-(640,480) SOLID
BOUNDARY ADD "top", (0,0)-(640,10) SOLID
BOUNDARY ADD "bottom", (0,470)-(640,480) SOLID
' Hill the player climbs walking RIGHT — solid is the lower-right wedge
BOUNDARY ADD "hill-right", (200,400)-(264,432) SOLID SLOPE SE
' Hill the player climbs walking LEFT — solid is the lower-left wedge
BOUNDARY ADD "hill-left", (264,400)-(328,432) SOLID SLOPE SOUTHWEST
' Two ramps forming a peak — climb up the right-rising hill, then down the left-rising hill on the other side
BOUNDARY ADD "left-ramp",  (100,400)-(164,432) SOLID SLOPE SE
BOUNDARY ADD "right-ramp", (164,400)-(228,432) SOLID SLOPE SW
' Ceiling overhang — solid mass anchored at upper-left, sloping down to the right (sprites underneath bonk and slide along the diagonal)
BOUNDARY ADD "overhang", (50,0)-(150,80) SOLID SLOPE NW

See also: BOUNDARY DELETE, BOUNDARY CLEAR, FOR COLLISIONS, ENCLOSURE

↑ Back to top

BOUNDARY ADD FROM

Collisions · since 1.41.0

Loads boundaries from a JSON map file exported by the Crash BASIC IDE's map editor and registers each one exactly as a literal BOUNDARY ADD would. This lets you draw collision regions visually and consume them at runtime instead of hand-writing every BOUNDARY ADD.

The file is read from the game directory. Each entry carries a tag, a pixel rect, and the SOLID / ENCLOSURE / SLOPE / layer attributes, and is added under the current WITH ROOM scope (if any), matching BOUNDARY ADD.

The statement fails loudly: a missing file, malformed JSON, an invalid slope, or an entry that is both ENCLOSURE and SLOPE halts with an error rather than registering a partial set.

File format

{
  "boundaries": [
    { "tag": "floor", "rect": [0, 480, 640, 500], "solid": true, "enclosure": false, "slope": null, "layer": null },
    { "tag": "ramp", "rect": [100, 400, 200, 480], "solid": true, "slope": "NE", "layer": 1 }
  ]
}

rect is [x1, y1, x2, y2] in pixels. slope is one of NE / NW / SE / SW or null. layer is a layer number or null (all layers). Extra fields such as version, source, and generated are ignored.

Syntax

BOUNDARY ADD FROM("file.json")

Parameters

  • "file.json" - String expression naming the boundaries map file, relative to the game directory.

Examples

BOUNDARY ADD FROM("level1.boundaries.json")
' Load per-level collision drawn in the IDE map editor
level$ = "forest"
BOUNDARY ADD FROM(level$ + ".boundaries.json")

See also: BOUNDARY ADD, BOUNDARY DELETE, BOUNDARY CLEAR, FOR COLLISIONS

↑ Back to top

BOUNDARY CLEAR

Collisions · since 1.0.3

Removes all boundary regions. This is useful for level transitions or resetting the game state.

Syntax

BOUNDARY CLEAR

Examples

BOUNDARY CLEAR
' Level transition
BOUNDARY CLEAR
CALL LoadLevel(nextLevel)

See also: BOUNDARY ADD, BOUNDARY DELETE

↑ Back to top

BOUNDARY DELETE

Collisions · since 1.0.3

Removes a boundary region by its tag name. After deletion, the boundary will no longer be detected in collision checks.

Syntax

BOUNDARY DELETE "tag"

Parameters

  • "tag" - String expression for the boundary name to delete

Examples

BOUNDARY DELETE "left-wall"
BOUNDARY DELETE boundaryName$

See also: BOUNDARY ADD, BOUNDARY CLEAR

↑ Back to top

COLLISION

Collisions · since 0.23.0

Professional 2D collision detection with multiple algorithms. Returns 1 if collision detected, 0 if no collision. Each collision type uses only the parameters it logically needs. An unrecognized type, or too few arguments for the chosen type, is a runtime error (it does not silently return 0).

Syntax

COLLISION(type, ...) - Parameters vary by type

Parameters

  • BOX: COLLISION("BOX", x1, y1, w1, h1, x2, y2, w2, h2) - Rectangle vs rectangle
  • CIRCLE: COLLISION("CIRCLE", cx1, cy1, r1, cx2, cy2, r2) - Circle vs circle (centers and radii)
  • POINT: COLLISION("POINT", px, py, rx, ry, rw, rh) - Point in rectangle

Examples

REM Player vs enemy collision (fastest)
IF COLLISION("BOX", playerX, playerY, 32, 32, enemyX, enemyY, 24, 24) THEN
  PRINT "Player hit enemy!"
END IF
REM Smooth ball collision
DIM ball1X AS SINGLE, ball1Y AS SINGLE, ball1Radius AS SINGLE
DIM ball2X AS SINGLE, ball2Y AS SINGLE, ball2Radius AS SINGLE
ball1Radius = 8: ball2Radius = 10
IF COLLISION("CIRCLE", ball1X, ball1Y, ball1Radius, ball2X, ball2Y, ball2Radius) THEN
  ballSpeedY = -ballSpeedY  ' Bounce
END IF
REM Button click detection
IF MOUSEDOWN() THEN
  IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, buttonW, buttonH) THEN
    CALL HandleButtonClick()
  END IF
END IF
REM Performance optimization pattern
IF COLLISION("BOX", x1, y1, w1, h1, x2, y2, w2, h2) THEN  ' Fast check first
  IF COLLISION("CIRCLE", x1, y1, w1/2, x2, y2, w2/2) THEN  ' Then precise circle check
    REM Handle collision
  END IF
END IF

See also: KEYDOWN, MOUSEX, MOUSEY, PUTIMAGE, LOADIMG, SPRITECOLLISION

↑ Back to top

ENCLOSURE (boundary modifier)

Collisions · since 1.4.6

The ENCLOSURE keyword creates an inverted boundary that keeps sprites INSIDE the defined region. Unlike normal boundaries that trigger when sprites enter them, enclosures trigger when sprites try to LEAVE them. This is perfect for defining play areas, arenas, or any region where sprites should be contained. When combined with SOLID, sprites are physically prevented from leaving the enclosure. Without SOLID, the collision is detected but sprites can still pass through (useful for triggering events when sprites leave an area).

Syntax

BOUNDARY ADD "tag", (x1,y1)-(x2,y2) [, layer] [SOLID] ENCLOSURE

Parameters

  • ENCLOSURE - Add at the end of BOUNDARY ADD to invert the collision behavior
  • SOLID ENCLOSURE - Combine both to create an impassable container that sprites cannot escape

Examples

' Keep player inside the game screen
BOUNDARY ADD "screen", (0,0)-(320,200) SOLID ENCLOSURE
' Detect when enemy leaves patrol zone (but allow it)
BOUNDARY ADD "patrol-zone", (100,50)-(250,150) ENCLOSURE
' Arena boundary - sprites cannot escape
BOUNDARY ADD "arena", (20,20)-(300,180) SOLID ENCLOSURE
' Trigger event when player leaves safe zone
BOUNDARY ADD "safe-zone", (50,50)-(270,150) ENCLOSURE

FOR COLLISIONS a, b, tag$
  IF b = -1 AND tag$ = "safe-zone" THEN
    PRINT "Warning: Leaving safe zone!"
  END IF
NEXT COLLISION

See also: BOUNDARY ADD, SOLID, FOR COLLISIONS

↑ Back to top

SOLID (boundary modifier)

Collisions · since 1.0.3

The SOLID keyword marks a boundary as impassable. When a sprite with an active behavior (movement via BMOVE, PATH, BFOLLOW, etc.) attempts to move through a SOLID boundary, its movement is stopped at the boundary edge. This allows "sliding" along walls - if movement is blocked in one direction, movement in the perpendicular direction is still allowed. Non-solid boundaries (the default) can be used as trigger zones that sprites can pass through.

Syntax

BOUNDARY ADD "tag", (x1,y1)-(x2,y2) [, layer] SOLID

Parameters

  • SOLID - Add at the end of BOUNDARY ADD to make the boundary solid

Examples

' Solid wall that sprites cannot pass through
BOUNDARY ADD "wall", (0,0)-(10,480) SOLID
' Trigger zone that sprites pass through (non-solid, default)
BOUNDARY ADD "checkpoint", (100,100)-(150,150)
' Create a platformer level with solid platforms
BOUNDARY ADD "ground", (0,470)-(640,480), 0 SOLID
BOUNDARY ADD "platform1", (100,350)-(200,360), 0 SOLID
BOUNDARY ADD "platform2", (300,250)-(400,260), 0 SOLID

See also: BOUNDARY ADD, BEHAVIOR

↑ Back to top

SPRITECOLLISION

Collisions · since 1.15.0

Checks if a sprite is colliding with another sprite or any sprite with a specific tag. The second parameter can be a sprite index (number) or a tag string. Returns 1 (TRUE) if colliding, 0 (FALSE) if not. Uses custom BBOX if set via SET SPRITE, otherwise uses full sprite bounds. Only enabled sprites are considered for tag-based collision.

Syntax

SPRITECOLLISION(sprite, spriteOrTag)

Parameters

  • sprite - Sprite slot number (0-99,999)
  • spriteOrTag - Either a sprite index (number) or a tag string to match against

Examples

REM Basic collision check between two sprites
IF SPRITECOLLISION(playerSprite, enemySprite) THEN
  PRINT "Hit!"
END IF
REM Check player against all sprites tagged "enemy"
IF SPRITECOLLISION(0, "enemy") THEN
  lives = lives - 1
  GOTO GameOver
END IF
REM Tag-based collision for bullets hitting enemies
FOR i = 0 TO numBullets - 1
  IF SPRITECOLLISION(bulletSprite(i), "enemy") THEN
    CALL DestroyBullet(i)
    score = score + 100
  END IF
NEXT
REM With custom bounding boxes for precise collision
SET SPRITE 0, playerX, playerY, "player", BBOX(0.2, 0.3, 0.8, 0.9) TAG "player"
SET SPRITE 1, enemyX, enemyY, "enemy", BBOX(0.1, 0.1, 0.9, 0.9) TAG "enemy"
IF SPRITECOLLISION(0, "enemy") THEN
  REM Collided with any enemy-tagged sprite
END IF

See also: SET SPRITE, COLLISION, ENABLE SPRITE

↑ Back to top

SPRITETOUCHING$

Collisions · since 1.18.3

Returns the tag of the solid boundary or solid sprite that a sprite is touching at the specified edge. Checks boundaries first, then sprites registered via SOLID SPRITES(...). Returns an empty string if nothing is touching that edge. Use "ANY" to check all four edges at once. When the optional boundaryTag$ is provided, only checks boundaries/sprites with that tag. IMPORTANT: The sprite must have a movement behavior (BMOVE, BVELOCITY, etc.) applied for collision detection to be active.

Syntax

SPRITETOUCHING$(spriteId, edge$ [, boundaryTag$])

Parameters

  • spriteId - The sprite index to check
  • edge$ - Which edge of the BOUNDARY to check: "TOP", "BOTTOM", "LEFT", "RIGHT", or "ANY" (checks all edges, returns first match)
  • boundaryTag$ - (Optional) Only check the boundary with this tag name

Examples

' Platformer ground detection — sprite standing on boundary's TOP edge
IF SPRITETOUCHING$(player, "TOP") <> "" THEN
  onGround = 1
  canJump = 1
ELSE
  onGround = 0
END IF
' Check if sprite hit a wall to the right — touching boundary's LEFT edge
IF SPRITETOUCHING$(player, "LEFT", "walls") <> "" THEN
  PRINT "Hit wall!"
END IF
' Check all four boundary edges
topEdge$ = SPRITETOUCHING$(0, "TOP")     ' standing on it
bottomEdge$ = SPRITETOUCHING$(0, "BOTTOM")  ' bumped head
leftEdge$ = SPRITETOUCHING$(0, "LEFT")    ' hit wall moving right
rightEdge$ = SPRITETOUCHING$(0, "RIGHT")   ' hit wall moving left

See also: BOUNDARY ADD, SOLID, FOR COLLISIONS, SPRITEX, SPRITEY

↑ Back to top

SPRITETOUCHINGX

Collisions · since 1.12.0

Returns the X coordinate the sprite should be at to align with the boundary it's touching on the given edge. Useful for snapping sprites to walls. If not touching a boundary, returns the sprite's current X position. Supports optional boundary tag filter. Requires the sprite to have a movement behavior active for collision detection to work.

Syntax

SPRITETOUCHINGX(spriteId, edge$ [, boundaryTag$])

Parameters

  • spriteId - The sprite index to check
  • edge$ - Which edge of the BOUNDARY: "TOP", "BOTTOM", "LEFT", "RIGHT", or "ANY"
  • boundaryTag$ - (Optional) Only check the boundary with this tag

Example

' Snap to wall — sprite hit boundary's LEFT edge (moving right)
IF SPRITETOUCHING$(player, "LEFT", "walls") <> "" THEN
  SET SPRITE player, LOCATION(SPRITETOUCHINGX(player, "LEFT", "walls"), SPRITEY(player))
  velocityX = 0
END IF

See also: SPRITETOUCHINGY, SPRITETOUCHING$, SPRITEX

↑ Back to top

SPRITETOUCHINGY

Collisions · since 1.12.0

Returns the Y coordinate the sprite should be at to align with the boundary it's touching on the given edge. Useful for snapping sprites to floors/ceilings and disabling gravity when grounded. If not touching a boundary, returns the sprite's current Y position. Supports optional boundary tag filter. Requires the sprite to have a movement behavior active for collision detection to work.

Syntax

SPRITETOUCHINGY(spriteId, edge$ [, boundaryTag$])

Parameters

  • spriteId - The sprite index to check
  • edge$ - Which edge of the BOUNDARY: "TOP", "BOTTOM", "LEFT", "RIGHT", or "ANY"
  • boundaryTag$ - (Optional) Only check the boundary with this tag

Example

' Snap to floor (boundary's TOP edge) and disable gravity
IF SPRITETOUCHING$(player, "TOP") <> "" THEN
  SET SPRITE player, LOCATION(SPRITEX(player), SPRITETOUCHINGY(player, "TOP"))
  velocityY = 0
  onGround = 1
END IF

See also: SPRITETOUCHINGX, SPRITETOUCHING$, SPRITEY

↑ Back to top

Maps

FOR MAPDATA

Maps · since 1.4.3

Iterates over all non-empty metadata cells in the specified region of a map layer. Each iteration sets the loop variables to the coordinates, metadata character, and optionally the tile index of the next non-empty cell. Metadata is placed in the Map Editor using the Metadata layer. This is useful for finding spawn points, item locations, triggers, and other marked positions in a tile map.

Syntax

FOR MAPDATA [LAYER n] (x1, y1)-(x2, y2) AS xVar, yVar, metaVar$ [, indexVar]
  statements...
NEXT MAPDATA

Parameters

  • LAYER n - Optional. Specifies which map layer to scan (0-99). If omitted, uses the current layer.
  • x1, y1 - Top-left corner of the region to scan (in tile coordinates)
  • x2, y2 - Bottom-right corner of the region to scan (in tile coordinates)
  • xVar - Variable to receive the X tile coordinate of each metadata cell
  • yVar - Variable to receive the Y tile coordinate of each metadata cell
  • metaVar$ - String variable to receive the metadata character (single char or emoji)
  • indexVar - Optional. Variable to receive the tile index at each cell (0-based, -1 for empty)
  • Only non-empty cells are returned; empty cells are skipped automatically

Examples

' Find player spawn point (marked with "P" in map editor)
FOR MAPDATA (0, 0)-(mapWidth, mapHeight) AS x, y, m$
  IF m$ = "P" THEN
    playerX = x * 16
    playerY = y * 16
  END IF
NEXT MAPDATA
' Spawn entities based on metadata markers
FOR MAPDATA (0, 0)-(20, 15) AS x, y, m$
  IF m$ = "E" THEN
    CALL SpawnEnemy(x * 16, y * 16)
  ELSEIF m$ = "C" THEN
    CALL SpawnCoin(x * 16, y * 16)
  ELSEIF m$ = "💎" THEN
    CALL SpawnGem(x * 16, y * 16)
  END IF
NEXT MAPDATA
' Scan a specific layer with tile index
FOR MAPDATA LAYER 2 (0, 0)-(mapWidth, mapHeight) AS x, y, m$, tileIdx
  IF m$ = "T" THEN
    PRINT "Trigger at"; x; y; "tile:"; tileIdx
  END IF
NEXT MAPDATA
' Initialize game objects from map metadata
CREATE MAP("level1.map.json") AS LAYER 0
FOR MAPDATA (0, 0)-(100, 100) AS tx, ty, marker$
  SELECT CASE marker$
    CASE "P"
      playerStartX = tx: playerStartY = ty
    CASE "E"
      CALL AddEnemy(tx, ty)
    CASE "🚪"
      CALL AddDoor(tx, ty)
    CASE "🔑"
      CALL AddKey(tx, ty)
  END SELECT
NEXT MAPDATA

See also: MAPMETA$, CREATE MAP, FOR SPRITES, FOR COLLISIONS

↑ Back to top

MAPHEIGHT

Maps · since 1.12.0

Returns the height of a tilemap layer in tiles. Pair it with MAPWIDTH for full map dimensions — iteration bounds, boundary checks, centering math. Returns 0 if the specified layer isn't a map layer or doesn't exist.

Server-side lookup

Same snapshot-fallback semantics as MAPTILE and MAPWIDTH. When the renderer-local cache is empty — which happens on player-server interpreters that never get their own CreateTilemap broadcast — MAPHEIGHT consults the per-room tilemap snapshot on NetState. Room resolution: WITH ROOM if active, else ME.ROOM, else the _global sentinel.

Syntax

MAPHEIGHT(layer_id)

Parameters

  • layer_id - The layer number to query (must be a tilemap layer created with CREATE MAP)
  • Returns the height of the map in tiles, or 0 if not a map layer

Examples

' Get map dimensions
mapW = MAPWIDTH(0)
mapH = MAPHEIGHT(0)
PRINT "Map size:"; mapW; "x"; mapH; "tiles"
' Scan bottom row of map
y = MAPHEIGHT(1) - 1
FOR x = 0 TO MAPWIDTH(1) - 1
  IF MAPMETA$(x, y) = "E" THEN
    exitFound = 1
  END IF
NEXT x
' Check if position is within map bounds
IF y >= 0 AND y < MAPHEIGHT(0) THEN
  ' Safe to access map at this Y coordinate
END IF
' Center camera on map
cameraX = (MAPWIDTH(1) * 16) / 2 - screenWidth / 2
cameraY = (MAPHEIGHT(1) * 16) / 2 - screenHeight / 2

See also: MAPWIDTH, MAPMETA$, FOR MAPDATA, CREATE MAP

↑ Back to top

MAPMETA$

Maps · since 1.4.3

Returns the metadata character at the specified tile coordinates in the current map layer. Returns an empty string if no metadata exists at that position or if coordinates are out of bounds. Metadata is placed in the Map Editor using the Metadata layer. Use this to check specific tile positions for markers like spawn points, triggers, or item locations.

Syntax

MAPMETA$(x, y)
MAPMETA$(coord)   (also accepts a single coordinate value)

Parameters

  • x - X tile coordinate (0-based)
  • y - Y tile coordinate (0-based)
  • Returns a string containing the metadata character (single char or emoji), or empty string if none

Examples

' Check what's at a specific tile
marker$ = MAPMETA$(10, 5)
IF marker$ = "P" THEN
  PRINT "Player spawn at tile 10,5"
END IF
' Check tile under player
tileX = playerX \ 16
tileY = playerY \ 16
IF MAPMETA$(tileX, tileY) = "💀" THEN
  playerHealth = playerHealth - 1
  PRINT "Stepped on spikes!"
END IF
' Check for trigger zones
IF MAPMETA$(doorTileX, doorTileY) = "🚪" THEN
  IF hasKey THEN
    CALL OpenDoor()
  END IF
END IF
' Validate movement destination
newTileX = (playerX + dx) \ 16
newTileY = (playerY + dy) \ 16
IF MAPMETA$(newTileX, newTileY) <> "X" THEN
  ' "X" marks blocked tiles
  playerX = playerX + dx
  playerY = playerY + dy
END IF

See also: FOR MAPDATA, CREATE MAP, MAPWIDTH, MAPHEIGHT

↑ Back to top

MAPWIDTH

Maps · since 1.12.0

Returns the width of a tilemap layer in tiles. Useful for iteration bounds, boundary checks, dynamic camera centering, anything that needs to know how big the map is. Returns 0 if the specified layer isn't a map layer or doesn't exist.

Server-side lookup

Same snapshot-fallback semantics as MAPTILE. When the renderer-local cache is empty — which happens on player-server interpreters that never get their own CreateTilemap broadcast — MAPWIDTH consults the per-room tilemap snapshot on NetState. Room resolution: WITH ROOM if active, else ME.ROOM, else the _global sentinel.

Syntax

MAPWIDTH(layer_id)

Parameters

  • layer_id - The layer number to query (must be a tilemap layer created with CREATE MAP)
  • Returns the width of the map in tiles, or 0 if not a map layer

Examples

' Get map dimensions
mapW = MAPWIDTH(1)
mapH = MAPHEIGHT(1)
PRINT "Map size:"; mapW; "x"; mapH; "tiles"
' Iterate entire map with dynamic bounds
FOR MAPDATA (0, 0)-(MAPWIDTH(1) - 1, MAPHEIGHT(1) - 1) AS x, y, m$
  IF m$ = "P" THEN playerX = x * 16
NEXT MAPDATA
' Bounds checking before accessing map
IF tileX >= 0 AND tileX < MAPWIDTH(0) THEN
  marker$ = MAPMETA$(tileX, tileY)
END IF
' Calculate total tile count
totalTiles = MAPWIDTH(1) * MAPHEIGHT(1)
PRINT "Total tiles:"; totalTiles

See also: MAPHEIGHT, MAPMETA$, FOR MAPDATA, CREATE MAP

↑ Back to top

Sound

ADD ARRANGEMENT

Sound · 📱 Client-only · since 1.51.0

Appends one step to the end of a song's arrangement.

REPEAT(n) plays the pattern n times in a row (default 1). WITH(...) layers other patterns on top of it, playing simultaneously — that is how you stack a drum loop under a melody without merging them into one pattern.

Both WITH("a", "b") and WITH "a", "b" are accepted.

Syntax

ADD ARRANGEMENT song, pattern [REPEAT(n)] [WITH(other, ...)]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Pattern to play
  • REPEAT(n) - Number of consecutive plays (default 1)
  • WITH(other, ...) - Patterns layered on top, played at the same time

Examples

ADD ARRANGEMENT "battle", "chorus" REPEAT(2)
ADD ARRANGEMENT "battle", "verse" REPEAT(4) WITH("drums", "pad")

See also: SET ARRANGEMENT, INSERT ARRANGEMENT, DELETE ARRANGEMENT, MUSIC

↑ Back to top

APPLY MUSIC

Sound · 📱 Client-only · since 1.51.0

Pushes a song's pending edits to the audio engine now, and runs its pattern generators, without playing it. Use it to warm the render up ahead of the moment the song is actually needed — during a loading screen or scene transition — so the eventual PLAYMUSIC has nothing left to do.

You never have to call this: PLAYMUSIC, FADEMUSIC and MUSICLENGTH all push pending edits themselves. It only moves the work earlier.

Syntax

APPLY MUSIC song

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

MUSIC "boss"
  TEMPO 168
END MUSIC
APPLY MUSIC "boss"    ' render during the loading screen
' ...
PLAYMUSIC "boss"      ' instant

See also: PLAYMUSIC, MUSIC, GENERATOR

↑ Back to top

ARRANGEMENTCOUNT

Sound · since 1.51.0

Number of steps in a song's arrangement.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

ARRANGEMENTCOUNT(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

FOR i = 0 TO ARRANGEMENTCOUNT("battle") - 1
  PRINT ARRANGEMENTPATTERN$("battle", i); " x"; ARRANGEMENTREPEAT("battle", i)
NEXT

See also: ADD ARRANGEMENT, ARRANGEMENTPATTERN$

↑ Back to top

ARRANGEMENTLAYER$

Sound · since 1.51.0

One of the patterns an arrangement step layers on top of its main pattern.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

ARRANGEMENTLAYER$(song, step, layer)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • step - Zero-based arrangement step index
  • layer - Zero-based index into that step's WITH list

Example

name$ = ARRANGEMENTLAYER$("battle", 0, 0)

See also: ARRANGEMENTLAYERCOUNT, ADD ARRANGEMENT

↑ Back to top

ARRANGEMENTLAYERCOUNT

Sound · since 1.51.0

How many patterns one arrangement step layers on top of its main pattern (its WITH list).

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

ARRANGEMENTLAYERCOUNT(song, step)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • step - Zero-based arrangement step index

Example

FOR L = 0 TO ARRANGEMENTLAYERCOUNT("battle", 0) - 1
  PRINT ARRANGEMENTLAYER$("battle", 0, L)
NEXT

See also: ARRANGEMENTLAYER$, ADD ARRANGEMENT

↑ Back to top

ARRANGEMENTPATTERN$

Sound · since 1.51.0

The pattern played by one arrangement step.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

ARRANGEMENTPATTERN$(song, step)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • step - Zero-based arrangement step index

Example

p$ = ARRANGEMENTPATTERN$("battle", 0)

See also: ARRANGEMENTCOUNT, ARRANGEMENTREPEAT

↑ Back to top

ARRANGEMENTREPEAT

Sound · since 1.51.0

How many consecutive times one arrangement step plays its pattern.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

ARRANGEMENTREPEAT(song, step)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • step - Zero-based arrangement step index

Example

PRINT ARRANGEMENTREPEAT("battle", 1)

See also: ARRANGEMENTCOUNT, ADD ARRANGEMENT

↑ Back to top

BEEP

Sound · 📱 Client-only · since 0.23.0

Produces a system beep sound.

Syntax

BEEP

Examples

BEEP
IF error THEN BEEP: PRINT "Error occurred"

See also: SOUND, PLAY

↑ Back to top

CREATE MUSIC PATTERN

Sound · 📱 Client-only · since 1.51.0

Adds an empty pattern to a song. Add tracks to it with CREATE TRACK.

LENGTH is the pattern's length in beats; omit it to size the pattern automatically from its longest track. Creating a pattern that already exists is an error — use DELETE MUSIC PATTERN first, or a MUSIC block, which replaces.

Spelled MUSIC PATTERN rather than PATTERN because a bare PATTERN block already means a sprite behavior pattern.

Syntax

CREATE MUSIC PATTERN song, pattern [LENGTH(beats)]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • LENGTH(beats) - Pattern length in beats (optional)

Example

CREATE MUSIC PATTERN "battle", "bridge" LENGTH(16)
CREATE TRACK "battle", "bridge" CHANNEL(1) NOTES "o3 e g b"

See also: DELETE MUSIC PATTERN, SET MUSIC PATTERN, CREATE TRACK, MUSIC

↑ Back to top

CREATE TRACK

Sound · 📱 Client-only · since 1.51.0

Appends a track to a pattern and returns nothing; the new track's index is the previous TRACKCOUNT. Unmentioned properties start at their defaults: channel 1, instrument 0, volume 80, centred, no notes.

Track modifiers, all optional and in any order. Anything you don't mention keeps its current value.

  • CHANNEL(n) — MIDI channel 1 to 16 (10 is conventionally drums)
  • INSTRUMENT(n) — instrument number (built-ins are 0 to 66; .cbi instruments may use any number)
  • VOLUME(n) — 0 to 100
  • PAN(x) — -1.0 (left) to 1.0 (right)
  • NAME(s$) — a label for the track, shown in the Music Editor
  • NOTES(s$) — the track's MML
  • GENERATOR(s$) — name of a BASIC FUNCTION that supplies the MML instead (see GENERATOR)
  • REPEAT / NOREPEAT — whether the track tiles to fill the pattern's length

Every value modifier accepts both VOLUME(80) and VOLUME 80.

Syntax

CREATE TRACK song, pattern [modifiers...]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • modifiers - See SET TRACK

Examples

CREATE TRACK "battle", "verse" CHANNEL(3) INSTRUMENT(5) VOLUME(70) NOTES "o4 g a b"
' Add a drum track that tiles to fill the pattern
CREATE TRACK "battle", "verse" CHANNEL(10) REPEAT NOTES "c16 r16 c16 r16"

See also: SET TRACK, DELETE TRACK, TRACKCOUNT, MUSIC

↑ Back to top

DELETE ARRANGEMENT

Sound · 📱 Client-only · since 1.51.0

Removes one step from a song's arrangement. Later steps shift down one index, so delete from the highest index first when removing several.

Syntax

DELETE ARRANGEMENT song, index

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • index - Zero-based step index

Example

DELETE ARRANGEMENT "battle", 3

See also: ADD ARRANGEMENT, SET ARRANGEMENT, ARRANGEMENTCOUNT

↑ Back to top

DELETE MUSIC

Sound · 📱 Client-only · since 1.51.0

Removes a song from the interpreter's music bank. Queries and edit statements can no longer reach it.

The audio engine keeps whatever copy it already has, so a song that was played before being deleted can still be playing; STOPMUSIC to end it.

Syntax

DELETE MUSIC song

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

DELETE MUSIC "scratch"

See also: MUSIC, MUSICEXISTS, STOPMUSIC

↑ Back to top

DELETE MUSIC PATTERN

Sound · 📱 Client-only · since 1.51.0

Removes a pattern from a song, along with every arrangement step that played it and every WITH layer that referenced it. Leaving those behind would render as silence with no diagnostic, so they go with the pattern.

Syntax

DELETE MUSIC PATTERN song, pattern

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)

Example

DELETE MUSIC PATTERN "battle", "bridge"

See also: CREATE MUSIC PATTERN, DELETE ARRANGEMENT, MUSIC

↑ Back to top

DELETE TRACK

Sound · 📱 Client-only · since 1.51.0

Removes a track from a pattern. Tracks after it shift down one index, so delete from the highest index first when removing several.

Syntax

DELETE TRACK song, pattern, index

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

DELETE TRACK "battle", "verse", 2

See also: CREATE TRACK, MUTE TRACK, TRACKCOUNT

↑ Back to top

FADEMUSIC

Sound · 📱 Client-only · since 1.6.1

Fades synthesizer music in or out over the specified duration. FADEMUSIC IN starts the specified song (if not playing) and ramps volume from 0 to full. FADEMUSIC OUT ramps volume from current to 0 and stops the music when complete. Use this for smooth transitions between songs or for dramatic effect. In multiplayer FADEMUSIC is client-only — wrap in IF ME.ISCLIENT THEN ... END IF or trigger from the server with EVENT CLIENT "name". Fire-and-forget: FADEMUSIC does not block; engine errors (song not in bank, FADEMUSIC OUT on a song that isn't playing) surface in the log as [native] AUDIO ERROR: ... but do not halt. Pre-validate with BOOM at the call site if you want loud failure on a typo.

Syntax

FADEMUSIC IN|OUT song_name DURATION seconds

Parameters

  • IN|OUT - Fade direction (IN = fade in, OUT = fade out)
  • song_name - Name of the song to fade (from loaded music bank)
  • seconds - Duration of the fade in seconds

Examples

FADEMUSIC IN "Battle Theme" DURATION 2
FADEMUSIC OUT "Battle Theme" DURATION 5
' Start a song with fade-in (no PLAYMUSIC needed)
FADEMUSIC IN "Battle Theme" DURATION 3
' Crossfade between songs
FADEMUSIC OUT "Overworld" DURATION 2
FADEMUSIC IN "Battle Theme" DURATION 2

See also: PLAYMUSIC, STOPMUSIC, PAUSEMUSIC, FADEMP3

↑ Back to top

GENERATOR

Sound · 📱 Client-only · since 1.51.0

Binds a BASIC FUNCTION to a track so the track's notes are computed instead of written out — algorithmic music. The function returns a string of MML, exactly what you would otherwise put in NOTES.

The generator runs once per arrangement repeat, every time the song is played. That is the point: a song with a generator track produces a fresh take on each PLAYMUSIC, and a step written PLAY "chorus" REPEAT 4 gets four different passes rather than the same bar four times.

The function may declare 0, 1, 2 or 3 parameters and receives a prefix of (pattern$, track, repeat) — so FUNCTION Bass$() and FUNCTION Bass$(pattern$, track, repeat) are both valid. repeat is the zero-based pass number within the current arrangement step, which is what you vary on to build a fill on the last time round. Declaring more than three parameters is an error.

Because a generator returns text, it is normally declared with BASIC's string suffix. Both GENERATOR "MakeBass" and GENERATOR "MakeBass$" find FUNCTION MakeBass$.

GENERATOR "" clears the binding and turns the track back into an ordinary one.

Failures are loud: a generator that names a function that doesn't exist, returns a non-string, or returns an empty string (usually a missing RETURN) halts with an error rather than rendering a silent track.

Note that generators are resolved by the interpreter before the song reaches the audio engine, so they cost nothing at playback time — but a generative song is re-rendered on every play, where an ordinary song is rendered once and cached.

Syntax

TRACK ... GENERATOR function_name
SET TRACK song, pattern, index GENERATOR function_name

FUNCTION function_name$([pattern$[, track[, repeat]]])

Parameters

  • function_name - Name of a BASIC FUNCTION returning a string of MML
  • pattern$ - Name of the pattern being generated (first parameter, optional)
  • track - Zero-based index of the track being generated (second parameter, optional)
  • repeat - Zero-based pass number within the arrangement step (third parameter, optional)

Examples

MUSIC "ambient"
  PATTERN "drift"
    TRACK CHANNEL(1) INSTRUMENT(5) GENERATOR "MakeArp"
  END PATTERN
  ARRANGEMENT
    PLAY "drift" REPEAT 8
  END ARRANGEMENT
END MUSIC
PLAYMUSIC "ambient"

FUNCTION MakeArp$(pattern$, track, repeat)
  DIM notes(4) AS STRING
  notes(0) = "c"
  notes(1) = "e"
  notes(2) = "g"
  notes(3) = "b"
  seq$ = "o4 "
  FOR i = 0 TO 15
    seq$ = seq$ + notes(INT(RND * 4)) + "16"
  NEXT
  RETURN seq$
END FUNCTION
' Vary by pass: a fill on the last time round
FUNCTION MakeDrums$(pattern$, track, repeat)
  IF repeat = 3 THEN
    RETURN "c16 c16 c16 c16 c16 c16 c16 c16"
  END IF
  RETURN "c8 r8 c8 r8"
END FUNCTION
' Bind and unbind at run time
SET TRACK "ambient", "drift", 0 GENERATOR "MakeArp"
SET TRACK "ambient", "drift", 0 GENERATOR "" NOTES "o4 c e g"

See also: MUSIC, SET TRACK, TRACKGENERATOR$, PLAY, APPLY MUSIC

↑ Back to top

INSERT ARRANGEMENT

Sound · 📱 Client-only · since 1.51.0

Inserts a step at a position in the arrangement, shifting later steps along. index may equal ARRANGEMENTCOUNT to append; anything past that is an error.

Syntax

INSERT ARRANGEMENT song, index, pattern [REPEAT(n)] [WITH(other, ...)]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • index - Zero-based position to insert at (0 to ARRANGEMENTCOUNT)
  • pattern - Pattern to play
  • REPEAT(n) - Number of consecutive plays (default 1)
  • WITH(other, ...) - Patterns layered on top, played at the same time

Examples

INSERT ARRANGEMENT "battle", 0, "intro"
INSERT ARRANGEMENT "battle", 2, "bridge" REPEAT(2)

See also: ADD ARRANGEMENT, DELETE ARRANGEMENT, ARRANGEMENTCOUNT

↑ Back to top

INSTRUMENTMODE

Sound · 📱 Client-only · since 1.23.0

Engine-wide instrument playback mode. Modern (the default) uses linear sample interpolation, the full 32-voice polyphony pool, and no output filtering — clean, modern DSP. Retro switches to a SNES-style voicing: 4-tap Gaussian sample interpolation, an 8-voice global polyphony cap with oldest-released voice-stealing, and a one-pole low-pass at 15 kHz on the output mix. The cap and the Gaussian filter together produce the soft, compressed character associated with mid-90s sampled-instrument soundtracks. Only sampled instruments (bound via SETINSTRUMENTWAV) change character audibly; synthesized waveforms are mostly unaffected except for the output low-pass. Mode can be switched at any time.

Syntax

INSTRUMENTMODE "modern" | "retro"

Parameters

  • mode - "modern" (default) or "retro"

Examples

INSTRUMENTMODE "retro"  ' everything else plays SNES-ish from now on
' Opening title uses retro, gameplay uses modern
INSTRUMENTMODE "retro"
PLAYMUSIC "Title"
' ...after title screen...
INSTRUMENTMODE "modern"
PLAYMUSIC "Overworld"

See also: SETINSTRUMENTWAV, LOADWAV, PLAY

↑ Back to top

LIPSYNC

Sound · 🧪 Experimental · since 1.8.5

Analyzes a WAV or MP3 audio file and returns an array of timestamped mouth shapes (visemes) for lip sync animation. Each element is an object with TIME (absolute seconds from the start of the audio file, as a float) and SHAPE$ (viseme code). The optional sensitivity parameter (0.0 to 1.0, default 0.5) controls how much audio is detected: 1.0 picks up the quietest sounds, 0.0 picks up almost nothing. Increase sensitivity for quiet recordings; decrease it to ignore background noise. Uses FFT-based formant analysis to classify vowel and consonant mouth shapes.

Viseme codes:

  • X — closed / silence
  • A — open jaw ('ah')
  • B — rounded lips ('oh')
  • C — wide / flat ('ee' / 'eh')
  • D — lip tuck ('f' / 'v' / sibilants)
  • E — slightly open (neutral / 'th' / 'l')
  • F — tight pucker ('w' / 'oo')

IMPORTANT: For best results, the audio file should contain an isolated voice track (speech/dialogue only). Background music, drums, and sound effects will produce inaccurate mouth shapes. If your audio has music, use external tools (e.g. Audacity vocal isolation, AI stem separators like Demucs/UVR) to extract a clean voice track, then pass that to LIPSYNC while playing the original mix for audio. TIME values are absolute timestamps, not delays. To animate in sync with playback, compute the delay between consecutive entries (see examples).

Syntax

LIPSYNC(path$ [, sensitivity])

Parameters

  • path$ - Path to a WAV or MP3 audio file
  • sensitivity - Optional. Detection sensitivity from 0.0 (picks up nothing) to 1.0 (picks up everything). Default is 0.5; a missing or non-numeric value falls back to 0.5

Examples

lip = LIPSYNC("dialogue.wav")
FOR i = 1 TO UBOUND(lip)
  PRINT lip(i).TIME; " "; lip(i).SHAPE$
NEXT i
REM High sensitivity for quiet dialogue
lip = LIPSYNC("whisper.mp3", 0.9)
REM Animate mouth sprite based on audio.
REM Lay out the mouth spritesheet so the cells match this viseme order:
REM   cell 0 = X (closed), 1 = A, 2 = B, 3 = C, 4 = D, 5 = E, 6 = F
lip = LIPSYNC("speech.mp3")
PLAYWAV "speech.mp3", "voice"
prevTime = 0
FOR i = 1 TO UBOUND(lip)
  REM TIME is absolute seconds from start of file
  REM Delay by the delta between consecutive entries
  DELAY lip(i).TIME - prevTime
  prevTime = lip(i).TIME
  shape$ = lip(i).SHAPE$
  REM Map the viseme letter to its 0-based cell (X->0 closed, A->1 ... F->6)
  SET SPRITE 1 FRAME INSTR("XABCDEF", shape$) - 1
NEXT i

See also: LOADWAV, LOADMP3, PLAYWAV, PLAYMP3

↑ Back to top

LOADMUSIC

Sound · 📱 Client-only · since 1.51.0

Loads a .cbm music bank and renders it, so a later PLAYMUSIC starts instantly.

Rendering is the expensive half of multi-track music. A dense song — many tracks, sampled or wavetable instruments — takes seconds to render, and that has to happen somewhere. LOADMUSIC is how you choose where: put it behind a loading screen or a scene transition instead of letting it land on the first PLAYMUSIC.

It blocks until the render is finished. That is what makes it useful — when it returns, the music is ready. To keep drawing while it works, call it from an ASYNC function and RESOLVE the promise when you need it (see the examples).

All four forms do something different:

  • LOADMUSIC — the default music.cbm, every song in it
  • LOADMUSIC "levels/boss.cbm" — that bank, every song in it
  • LOADMUSIC SONG "battle" — one song from the bank already loaded
  • LOADMUSIC "levels/boss.cbm" SONG "battle" — that bank, one song from it

Banks merge. Loading a per-level bank adds to what is already there rather than replacing it, so the songs from music.cbm stay available. Re-stating a filename you have already loaded (the usual way to pre-render a second song from the same bank) does not re-read it, so it cannot undo SET MUSIC / SET TRACK edits you made since. A bank file whose contents actually changed does replace its songs.

Songs you built in code with a MUSIC block need no file: LOADMUSIC SONG "name" renders them the same way.

Calling it twice on the same song is cheap — an already-rendered song is not rendered again — so a loading screen can re-enter without penalty.

LOADMUSIC is an optimisation, never a prerequisite. A song you never pre-render still plays; PLAYMUSIC renders it first. Naming a song or a file that doesn't exist is a fatal error rather than a silent no-op that leaves a loading screen waiting forever.

Multiplayer: client-only, like every other resource load. Wrap in IF ME.ISCLIENT THEN ... END IF.

Syntax

LOADMUSIC [filename] [SONG song]

Parameters

  • filename - Path to a .cbm bank, relative to your game folder. Omit to use the bank already loaded (music.cbm is loaded for you at startup)
  • SONG song - Render only this song. Omit to render every song in the bank

Examples

' Pre-render one song before the scene that needs it
LOADMUSIC SONG "battle"
PLAYMUSIC "battle"    ' instant
' Load a per-level bank and render all of it
LOADMUSIC "levels/boss.cbm"
' Put the loading screen up, then render the music on a worker
' while the rest of the level loads.
CLS
PRINT "Loading..."
p = ASYNC PreloadMusic()

' ...other loading work happens here, overlapping the render...

ready = RESOLVE(p)    ' block only where the music is actually needed
PLAYMUSIC "battle"

FUNCTION PreloadMusic()
  LOADMUSIC "levels/boss.cbm" SONG "battle"
  RETURN 1
END FUNCTION
' Render a song that was built in code, not loaded from a file
MUSIC "victory"
  TEMPO 140
  PATTERN "p"
    TRACK CHANNEL(1) INSTRUMENT(12) VOLUME(80) NOTES "o4 c e g o5 c"
  END PATTERN
  ARRANGEMENT
    PLAY "p"
  END ARRANGEMENT
END MUSIC
LOADMUSIC SONG "victory"

See also: PLAYMUSIC, MUSIC, APPLY MUSIC, MUSICEXISTS, FADEMUSIC

↑ Back to top

MUSIC

Sound · 📱 Client-only · since 1.51.0

Declares or adjusts a multi-track song, laid out the way you would build it in the Music Editor. If the song already exists in the music bank the block edits it in place; if it doesn't, the block creates it.

Multi-track songs live in a music bank (a .cbm file, normally music.cbm, authored in the IDE's Music Editor). A song has a tempo, a loop flag, a time signature, a set of named patterns, and an arrangement that plays those patterns in order. A pattern holds tracks, and each track is one MML part on one channel with its own instrument, volume and pan. A pattern may also carry an AUDIO clip — a pre-recorded MP3 or WAV mixed in under whatever the tracks play.

Crash BASIC can build and adjust all of that at run time. Edits apply to the interpreter's copy of the bank and are sent to the audio engine automatically the next time the song is played, so you never have to think about when to push them. The .cbm file on disk is never rewritten — edits last for the run.

Only what you mention changes. A block that sets just TEMPO on an authored song keeps that song's patterns, arrangement and loop flag. A PATTERN block replaces that one pattern's track list outright, which makes re-running the same MUSIC block idempotent — a level restart won't double every track. An ARRANGEMENT block replaces the whole arrangement; an empty one clears it.

Patterns may be declared after the ARRANGEMENT block that names them; the arrangement is checked once the whole block has been applied. Arranging a pattern the song doesn't define is a fatal error rather than silent silence.

Mixing in a recording. AUDIO handle names a clip you already loaded with LOADMP3 or LOADWAV, and mixes it into the pattern alongside the tracks. The song refers to the handle, not the file — so loading a different file onto that handle changes what the song plays, without touching the song at all. Load the clip before LOADMUSIC or PLAYMUSIC; a handle with nothing on it is a fatal error naming the handle, not silence.

An audio pattern is as long as its clip, rounded up to a whole bar, so whatever plays next still lands on a downbeat. A pattern may hold tracks, an AUDIO clip, or both; layering a recording under a different pattern is what WITH in the arrangement is for. Like tracks, the clip is declared whole: a pattern block with no AUDIO line clears it.

The clip is baked into the song when it renders, which is what keeps it locked to the synth parts across a loop seam. Re-loading a handle re-renders the songs that use it, so a swap costs a render.

For per-property tweaks during play, use SET MUSIC and SET TRACK instead of re-declaring the song.

Syntax

MUSIC song
  [TEMPO n]
  [LOOP ON | LOOP OFF]
  [TIMESIG beats, unit]
  [PATTERN name [LENGTH beats]
    TRACK [modifiers...]
    [AUDIO handle [modifiers...]]
    ...
  END PATTERN]
  [ARRANGEMENT
    PLAY pattern [REPEAT n] [WITH other, ...]
    ...
  END ARRANGEMENT]
END MUSIC

Parameters

  • song - Name of the song to define or edit
  • TEMPO n - Beats per minute (default 120)
  • LOOP ON | OFF - Whether the song repeats when played
  • TIMESIG beats, unit - Time signature, e.g. 3, 4 for 3/4 (default 4, 4)
  • PATTERN name [LENGTH beats] - A named pattern; LENGTH is in beats, omit to auto-size from the longest track
  • TRACK [modifiers] - One MML part. Track modifiers, all optional and in any order. Anything you don't mention keeps its current value. - CHANNEL(n) — MIDI channel 1 to 16 (10 is conventionally drums) - INSTRUMENT(n) — instrument number (built-ins are 0 to 66; .cbi instruments may use any number) - VOLUME(n) — 0 to 100 - PAN(x) — -1.0 (left) to 1.0 (right) - NAME(s$) — a label for the track, shown in the Music Editor - NOTES(s$) — the track's MML - GENERATOR(s$) — name of a BASIC FUNCTION that supplies the MML instead (see GENERATOR) - REPEAT / NOREPEAT — whether the track tiles to fill the pattern's length Every value modifier accepts both VOLUME(80) and VOLUME 80.
  • AUDIO handle [modifiers] - Mixes a pre-recorded clip, loaded with LOADMP3 or LOADWAV under that handle, into the pattern. Modifiers are all optional and in any order; an omitted one takes its default rather than keeping a previous value. - VOLUME(n) — 0 to 100 (default 100) - PAN(x) — -1.0 (left) to 1.0 (right) - SECTION(start, end) — mix only this region of the clip, in seconds, so one long file can supply several patterns - REPEAT / NOREPEAT — tile the clip to fill the pattern (default NOREPEAT) - OFFSET(secs) — seconds into the pattern at which the clip starts, so a recording can enter late Seconds, not beats, for both SECTION and OFFSET — they describe positions in a recording. Every value modifier accepts both VOLUME(80) and VOLUME 80.
  • PLAY pattern [REPEAT n] [WITH other, ...] - One arrangement step. WITH layers other patterns on top simultaneously

Examples

MUSIC "battle"
  TEMPO 140
  LOOP ON
  PATTERN "verse"
    TRACK CHANNEL(1) INSTRUMENT(12) VOLUME(80) NOTES "o4 c d e f"
    TRACK CHANNEL(10) VOLUME(90) REPEAT NOTES "c8 c8 r8 c8"
  END PATTERN
  ARRANGEMENT
    PLAY "verse" REPEAT 4
  END ARRANGEMENT
END MUSIC
PLAYMUSIC "battle"
' Adjust a song the Music Editor authored, without redefining it
MUSIC "overworld"
  TEMPO 96
  LOOP ON
END MUSIC
' Layer two patterns in one step
MUSIC "boss"
  PATTERN "riff"
    TRACK CHANNEL(1) NOTES "o3 e e g e"
  END PATTERN
  PATTERN "drums"
    TRACK CHANNEL(10) REPEAT NOTES "c16 c16"
  END PATTERN
  ARRANGEMENT
    PLAY "riff" REPEAT 8 WITH "drums"
  END ARRANGEMENT
END MUSIC
' Mix a recorded track under the synth parts
LOADMP3 "vocals.mp3", "vox"
MUSIC "anthem"
  TEMPO 120
  PATTERN "chorus"
    AUDIO "vox" VOLUME 85
    TRACK CHANNEL(1) INSTRUMENT(12) NOTES "o4 c e g"
  END PATTERN
  ARRANGEMENT
    PLAY "chorus"
  END ARRANGEMENT
END MUSIC
LOADMUSIC SONG "anthem"
PLAYMUSIC "anthem"
' One file, two patterns, and a drum loop tiled underneath
LOADMP3 "session.mp3", "take"
LOADWAV "breakbeat.wav", "break"
MUSIC "jam"
  PATTERN "intro"
    AUDIO "take" SECTION(0, 8)
  END PATTERN
  PATTERN "verse"
    AUDIO "take" SECTION(8, 24) OFFSET 0.25
    AUDIO "break" REPEAT VOLUME 60
  END PATTERN
END MUSIC
' Swap the backing track without touching the song
LOADMP3 "day_theme.mp3", "bed"
MUSIC "level"
  PATTERN "main"
    AUDIO "bed"
  END PATTERN
END MUSIC
PLAYMUSIC "level"
' ...night falls
LOADMP3 "night_theme.mp3", "bed"   ' same handle, different file
PLAYMUSIC "level"                  ' re-renders with the new recording

See also: SET MUSIC, SET TRACK, GENERATOR, PLAYMUSIC, APPLY MUSIC

↑ Back to top

MUSICBEATS

Sound · since 1.51.0

Beats per measure — the top number of the song's time signature.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICBEATS(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

PRINT MUSICBEATS("waltz"); "/"; MUSICBEATUNIT("waltz")

See also: MUSICBEATUNIT, SET MUSIC

↑ Back to top

MUSICBEATUNIT

Sound · since 1.51.0

Beat unit — the bottom number of the song's time signature (4 = quarter notes, 8 = eighth notes).

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICBEATUNIT(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

PRINT MUSICBEATUNIT("jig")

See also: MUSICBEATS, SET MUSIC

↑ Back to top

MUSICEXISTS

Sound · since 1.51.0

Returns TRUE if the named song is in the music bank. The one query that never errors on an unknown name — use it to guard the others.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICEXISTS(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

IF MUSICEXISTS("battle") THEN PLAYMUSIC "battle"

See also: SONGCOUNT, MUSIC

↑ Back to top

MUSICLENGTH

Sound · since 1.7.2

Total length of a song in ticks (4 ticks per beat), measured from the last audible note of its fully expanded arrangement.

Works on any song in the bank, whether or not it has been pre-rendered or played. Pending SET MUSIC / SET TRACK edits are applied before measuring, so the number describes the song as your program has it now.

Returns 0 if the song has no notes.

Syntax

MUSICLENGTH(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Examples

ticks = MUSICLENGTH("battle")
' Length in seconds, from ticks and the song's own tempo
secs = MUSICLENGTH("battle") / 4 * 60 / MUSICTEMPO("battle")

See also: MUSICPLAYING, MUSICTEMPO, PLAYMUSIC

↑ Back to top

MUSICLOOP

Sound · since 1.51.0

TRUE if the song is set to loop when played.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICLOOP(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

IF MUSICLOOP("battle") THEN PRINT "loops"

See also: SET MUSIC, PLAYMUSIC

↑ Back to top

MUSICPATTERNCOUNT

Sound · since 1.51.0

Number of patterns defined in a song.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICPATTERNCOUNT(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

FOR i = 0 TO MUSICPATTERNCOUNT("battle") - 1
  PRINT MUSICPATTERNNAME$("battle", i)
NEXT

See also: MUSICPATTERNNAME$, CREATE MUSIC PATTERN

↑ Back to top

MUSICPATTERNLENGTH

Sound · since 1.51.0

A pattern's declared length in beats, or 0 if it sizes itself automatically from its longest track.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICPATTERNLENGTH(song, pattern)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)

Example

PRINT MUSICPATTERNLENGTH("battle", "verse")

See also: SET MUSIC PATTERN, CREATE MUSIC PATTERN

↑ Back to top

MUSICPATTERNNAME$

Sound · since 1.51.0

Name of one of a song's patterns. Patterns are ordered alphabetically ignoring case, so enumeration is stable from run to run.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICPATTERNNAME$(song, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • index - Zero-based pattern index

Example

p$ = MUSICPATTERNNAME$("battle", 0)

See also: MUSICPATTERNCOUNT, TRACKCOUNT

↑ Back to top

MUSICPLAYING

Sound · since 1.7.2

Returns whether multi-track music is currently playing. Returns 1 (TRUE) if music is playing, 0 (FALSE) if not playing or paused. (A non-looping song that finishes on its own still reads as playing until you call STOPMUSIC — the engine sends no end-of-song notification.)

Syntax

MUSICPLAYING()

Examples

IF MUSICPLAYING() THEN PRINT "Music is playing"
REM Only start music if not already playing
IF NOT MUSICPLAYING() THEN
  PLAYMUSIC "Background"
END IF
REM Check music state
playing = MUSICPLAYING()
IF playing THEN
  PRINT "Now playing..."
ELSE
  PRINT "Silence"
END IF

See also: PLAYMUSIC, STOPMUSIC, PAUSEMUSIC

↑ Back to top

MUSICPOSITION

Sound · since 1.7.2

Current playback position of the multi-track song, in ticks — the same unit MUSICLENGTH reports, so the two divide into a progress fraction.

There are 4 ticks per beat, so a tick's real duration follows the song's tempo: at 120 BPM a tick is 125ms. Returns 0 when no song is playing.

A looping song's position wraps at its loop point rather than growing without bound, so it stays in step with what you can hear. A one-shot song's position runs past the end of its music and stays there, which is what you want when timing something to an outro. A paused song holds its position — a pause consumes no song time.

Use it to drive a progress bar, or to fire something at a musical point without hand-maintaining a second clock.

Syntax

MUSICPOSITION()

Examples

pos = MUSICPOSITION()
' How far through the song, 0.0 to 1.0
total = MUSICLENGTH("battle")
IF total > 0 THEN progress = MUSICPOSITION() / total
' Fire once the song reaches the halfway mark
IF MUSICPOSITION() >= MUSICLENGTH("battle") / 2 AND NOT spawned THEN
  spawned = TRUE
  CALL SpawnBoss()
END IF

See also: MUSICPLAYING, MUSICLENGTH, PLAYMUSIC

↑ Back to top

MUSICTEMPO

Sound · since 1.51.0

A song's tempo in beats per minute.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

MUSICTEMPO(song)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)

Example

SET MUSIC "battle" TEMPO(MUSICTEMPO("battle") + 10)

See also: SET MUSIC, MUSICBEATS

↑ Back to top

MUTE TRACK

Sound · 📱 Client-only · since 1.51.0

Silences a single track without removing it, so it can be brought back later. Muted tracks are simply left out when the song is sent to the audio engine.

This is the natural way to build adaptive music: author every layer up front, then unmute them as the scene escalates.

Takes effect the next time the song is played. Read the state back with TRACKMUTED.

Syntax

MUTE TRACK song, pattern, index
UNMUTE TRACK song, pattern, index

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Examples

MUTE TRACK "battle", "verse", 2
' Bring the drums in when combat starts
UNMUTE TRACK "battle", "verse", 1
PLAYMUSIC "battle"

See also: SOLO TRACK, TRACKMUTED, SET TRACK

↑ Back to top

Music File Format

Sound · since 1.7.2

Multi-track music is stored in .cbm (CrashBasic Music) files, which are JSON files created in the Music Editor widget. Songs can contain multiple patterns with multiple tracks each, arranged into a complete composition. Tracks use MML notation for notes. Custom instruments can be created and edited using the Instrument Editor widget, which allows you to design synthesizer patches using waveforms, envelopes, and effects.

Syntax

.cbm JSON file format

Parameters

  • File Structure:
  • songs - Object containing song definitions keyed by name
  • Song Properties:
  • name - Song name (string)
  • tempo - Beats per minute (number, default 120)
  • loop - Whether to loop the song (boolean)
  • timeSignature - [beats, unit] like [4, 4] for 4/4 time
  • patterns - Object of pattern definitions keyed by name
  • arrangement - Array of arrangement steps
  • Pattern Properties:
  • tracks - Array of track definitions
  • length - Optional length in beats (auto-calculated if omitted)
  • Track Properties:
  • channel - MIDI channel 1-16 (10 for drums)
  • instrument - Instrument number (I0-I66, same as PLAY command)
  • volume - Volume 0-100
  • pan - Pan position -1.0 (left) to 1.0 (right)
  • notes - MML notation string
  • name - Optional track name
  • Arrangement Step:
  • pattern - Name of pattern to play
  • repeat - Times to repeat (default 1)
  • with - Additional patterns to layer

Example

REM Example .cbm file structure:
{
  "songs": {
    "My Song": {
      "name": "My Song",
      "tempo": 120,
      "loop": true,
      "patterns": {
        "intro": {
          "tracks": [
            {
              "channel": 1,
              "instrument": 1,
              "volume": 80,
              "notes": "L4 C E G >C"
            },
            {
              "channel": 2,
              "instrument": 10,
              "volume": 70,
              "notes": "L2 C <G"
            }
          ]
        },
        "verse": {
          "tracks": [
            {
              "channel": 1,
              "instrument": 1,
              "volume": 80,
              "notes": "L8 C D E F G A B >C"
            }
          ]
        }
      },
      "arrangement": [
        { "pattern": "intro" },
        { "pattern": "verse", "repeat": 2 }
      ]
    }
  }
}

See also: PLAYMUSIC, PLAY, Instruments

↑ Back to top

PATTERNAUDIO$

Sound · since 1.52.0

The LOADMP3 / LOADWAV handle of the recording a pattern mixes, or an empty string if the pattern has no AUDIO clip — so IF PATTERNAUDIO$(s$, p$) <> "" THEN is the has-a-recording test.

This is the handle the song names, not a filename. What that handle currently holds is whatever was last loaded onto it, which is what lets one song play different recordings.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song or pattern that doesn't exist is a fatal error rather than a silent zero.

Syntax

PATTERNAUDIO$(song, pattern)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)

Examples

PRINT PATTERNAUDIO$("anthem", "chorus")
IF PATTERNAUDIO$("anthem", "chorus") <> "" THEN PRINT "chorus has a backing track"

See also: MUSIC, PATTERNAUDIOVOLUME, LOADMP3, LOADWAV

↑ Back to top

PATTERNAUDIOVOLUME

Sound · since 1.52.0

The mix level of a pattern's AUDIO clip, 0 to 100.

Returns 0 for a pattern with no clip, which reads the same as a clip turned all the way down — pair it with PATTERNAUDIO$ when the difference matters.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song or pattern that doesn't exist is a fatal error rather than a silent zero.

Syntax

PATTERNAUDIOVOLUME(song, pattern)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)

Example

PRINT PATTERNAUDIOVOLUME("anthem", "chorus")

See also: MUSIC, PATTERNAUDIO$

↑ Back to top

PAUSEMUSIC

Sound · 📱 Client-only · since 1.7.2

Pauses the currently playing multi-track song. The song can be resumed from the same position with RESUMEMUSIC. Has no effect if no music is playing or if music is already paused.

Syntax

PAUSEMUSIC

Examples

PAUSEMUSIC
REM Pause music when game is paused
SUB PauseGame()
  gamePaused = 1
  PAUSEMUSIC
END SUB
REM Toggle pause
IF KEYDOWN("P") THEN
  IF gamePaused THEN
    RESUMEMUSIC
  ELSE
    PAUSEMUSIC
  END IF
  gamePaused = NOT gamePaused
END IF

See also: RESUMEMUSIC, PLAYMUSIC, STOPMUSIC

↑ Back to top

PLAY

Sound · 📱 Client-only · since 0.23.0

Plays musical sequences using Music Macro Language (MML). Use PLAY BACKGROUND for non-blocking music that continues while program runs. Supports notes (A-G), sharps (#/+), flats (-), octaves (O0-O6, <, >), note lengths (L1-L64), tempo (T32-T255), rests (R1-R64, also P1-P64), modes (MN/ML/MS), volume (V0-V15), numeric notes (N0-N116), instruments (I0-I66 including Futuristic and Gothic NES-style patches), dotted notes (.), ties (^) that extend the previous note or rest without re-triggering, pitch bends (~ for a semitone bend within a note, & for portamento sliding to the next note), chords using [notes] syntax, and track-level effects via $NAME:p[:q] (echo, reverb, chorus, flanger, phaser, tremolo, vibrato, drive, bitcrush, filter, highpass, pan) applied as a per-track DSP chain.

Syntax

PLAY [BACKGROUND] command_string

Parameters

  • BACKGROUND (optional) - Play music in background without blocking program execution
  • command_string - Musical commands in MML format
  • Music Macro Language Commands:
  • A, B, C, D, E, F, G - Musical notes
  • or + - Sharp (raise pitch by semitone)

      • Flat (lower pitch by semitone)
  • O0-O6 - Set octave (O0=lowest, O4=middle, O6=highest)
  • < - Down one octave
    • Up one octave
  • L1-L64 - Set note length (L1=whole, L2=half, L4=quarter, L8=eighth)
  • T32-T255 - Set tempo in beats per minute
  • R1-R64 - Rest/pause with duration (P1-P64 is an accepted alias)
  • MN - Normal mode (7/8 note length)
  • ML - Legato mode (full note length)
  • MS - Staccato mode (3/4 note length)
  • V0-V15 - Set volume (V0=silent, V15=maximum)
  • I0-I66 - Set instrument:
  • Basic Waveforms (I0-I9):
  • I0 - Basic Beep (pure sine tone)
  • I1 - Square 50% (hollow classic square)
  • I2 - Square 25% (brighter 25%-duty pulse)
  • I3 - Square 12.5% (thin, nasal 12.5%-duty pulse)
  • I4 - NES Triangle (mellow triangle with a near-instant attack; the classic chiptune bass)
  • I5 - Sawtooth (buzzy, bright timbre)
  • I6 - Square Vibrato (square with a 6 Hz pitch wobble)
  • I7 - Triangle Pad (soft triangle, slow attack and long release)
  • I8 - NES Noise (LFSR noise through a 3 kHz bandpass)
  • I9 - PWM (square with a slow 0.8 Hz pitch drift; despite the name the duty cycle is fixed at 50%)
  • Melodic Instruments (I10-I19):
  • I10 - Piano (triangle plus three sine partials, long decay)
  • I11 - Electric Piano (detuned sine stack, bell-like decay)
  • I12 - Guitar (saw, triangle and an octave sine through a 2.5 kHz lowpass, fast decay)
  • I13 - Bass Guitar (triangle plus octave sine through a 1.2 kHz lowpass)
  • I14 - Strings (four detuned saws, slow attack, gentle vibrato)
  • I15 - Brass (stacked saws and a square through a 3.5 kHz lowpass)
  • I16 - Flute (sine with a faint breath-noise layer and 5.5 Hz vibrato)
  • I17 - Organ (four-partial sine drawbar, instant attack)
  • I18 - Synth Lead (saw plus a detuned 25% pulse through a 3 kHz lowpass)
  • I19 - Synth Pad (detuned saws over a sub triangle, 0.5 s attack)
  • Advanced Synthesis (I20-I29):
  • I20 - FM Synth (sine plus octave sine with a deep 40-cent vibrato)
  • I21 - Distorted Square (square through a 2.5 kHz lowpass)
  • I22 - Brass Stab (two saws, short punchy decay)
  • I23 - String Ensemble (four widely detuned saws, 0.3 s swell)
  • I24 - Recorder (sine with a faint high partial, soft attack)
  • I25 - Choir Pad (detuned triangles plus a sine, slow swell)
  • I26 - Synth Bass (sub square plus triangle with a 40 ms pitch scoop up to pitch)
  • I27 - Acid Bass (saw through a 700 Hz lowpass, short decay)
  • I28 - PWM Synth (square with a very slow 0.3 Hz pitch drift; the duty cycle is fixed at 50%)
  • I29 - Vocoder (saw plus octave square through a 1.2 kHz bandpass)
  • Percussion & Drums (I30-I39):
  • I30 - Kick Drum (sine and triangle swept 165 Hz down to 45 Hz)
  • I31 - Snare Drum (noise plus a pitched triangle body, 800 Hz highpass)
  • I32 - Closed Hi-Hat (short noise burst, 8 kHz highpass)
  • I33 - Open Hi-Hat (sustained noise, 6.5 kHz highpass)
  • I34 - Tom High (sine swept 320 Hz down to 200 Hz)
  • I35 - Tom Low (sine swept 180 Hz down to 75 Hz)
  • I36 - Crash Cymbal (long noise wash, 7 kHz bandpass)
  • I37 - Ride Cymbal (medium noise decay, 5.5 kHz highpass)
  • I38 - Clap (noise burst through a 1.8 kHz bandpass)
  • I39 - Shaker (soft noise, 4.5 kHz highpass)
  • Drum pitch: I30, I34 and I35 sweep an absolute frequency and I32, I33, I36-I39 are fixed-pitch noise, so the played note does not change any of them. I31 keeps a fixed triangle body but its noise layer does brighten with the played note.
  • Special Effects (I40-I50):
  • I40 - Laser Zap (square swept 2 kHz down to 100 Hz; fixed pitch)
  • I41 - Explosion (noise and triangle swept 200 Hz down to 30 Hz; fixed pitch)
  • I42 - Power-up (two squares swept up four octaves; follows the played note)
  • I43 - Coin Pickup (square blip swept B5 up to E6; fixed pitch)
  • I44 - Jump Sound (square swept 150 Hz up to 500 Hz; fixed pitch)
  • I45 - Damage Sound (square and noise swept 500 Hz down to 80 Hz; fixed pitch)
  • I46 - Door Creak (sub saw through a 600 Hz bandpass with a deep slow vibrato)
  • I47 - Wind Sound (noise through a 400 Hz bandpass with a slow tremolo)
  • I48 - Water Drop (sine swept 1.5 kHz down to 400 Hz; fixed pitch)
  • I49 - Bubble Pop (sine swept 1.8 kHz down to 600 Hz; fixed pitch)
  • I50 - Alien Voice (saw with a wide 10 Hz, 150-cent vibrato)
  • Futuristic NES Patches (I51-I58):
  • I51 - Futuristic Lead (12.5%-duty pulse melody voice)
  • I52 - Futuristic Arpeggio (12.5%-duty pulse with a very fast decay, for arpeggio runs)
  • I53 - Futuristic Bass (bare NES triangle)
  • I54 - Futuristic Kick (triangle swept 185 Hz down to 52 Hz; fixed pitch)
  • I55 - Futuristic Pulse Bass (25%-duty pulse)
  • I56 - Futuristic Snare (noise plus a pitched triangle body, 900 Hz highpass)
  • I57 - Futuristic Echo Lead (two 25%-duty pulses 8 cents apart)
  • I58 - Futuristic Tom (triangle with a 1.5x-to-1x pitch drop; follows the played note)
  • Gothic NES Patches (I59-I66):
  • I59 - Gothic Lead (square with a 6.5 Hz vibrato)
  • I60 - Gothic Strings (two 25%-duty pulses 12 cents apart, soft attack)
  • I61 - Gothic Bass (triangle with an octave-up square)
  • I62 - Gothic Church Organ (five-partial sine drawbar including a sub octave)
  • I63 - Gothic Melody (25%-duty pulse with a gentle vibrato)
  • I64 - Gothic Bell (three sines with an inharmonic top partial, one-second decay)
  • I65 - Gothic Dark Pad (detuned 25% pulses over a sub triangle, 1.4 kHz lowpass)
  • I66 - Gothic Staccato (square with a 60 ms decay to silence)
  • N0-N116 - Play note by number (N0=rest, N1=C0, N84=B6, N116=G9). Covers the full engine pitch range, which is wider than the O0-O6 letter-note range. Values above N116 clamp to G9.
  • . (dot) - Dotted note (extends duration by 50%)
  • ^ - Tie (extend the previous note or rest by the following duration without re-attacking; e.g., C4^16 = one C attack held for quarter + sixteenth). Ties may chain (C^8^16) and tie default-length when no number follows.
  • ~ - Pitch bend the previous note (or chord) by a signed number of semitones over its duration, then hold; e.g., C42 bends up 2 semitones, C-3 bends down 3, [CEG]2 bends the whole chord up 2. Add ,length to bend over a shorter span first: C42,8 reaches +2 over an eighth then holds. Fractional semitones allowed.
  • & - Portamento: glide the previous note (or chord) to the pitch of the following note over its duration; e.g., C&E slides C up to E, E&C slides down. For a chord the whole chord glides in parallel so its root reaches the target: [CEG]&F shifts the chord up 5 semitones. The target is pitch-only (any length on it is ignored) and uses the current octave — for wider slides use ~ with a semitone count. (A bend after a rest is a no-op.)
  • [notes] - Chord (play multiple notes simultaneously, e.g., [CEG] for C major)
  • Track effects - $NAME:p[:q] declares a DSP effect applied to the whole track (e.g. $ECHO:25, $REVERB:40:60). p and q are 0-100 intensity unless noted; p=0 bypasses (except $PAN). Declared order = signal-chain order; track-global, last value wins. Effects ride inside the MML string, so they work identically in PLAY, PLAY BACKGROUND, and .cbm music tracks.
  • $ECHO:p[:q] - Tempo-synced delay. p = wet/feedback. q = note division (1/2/4/8/16/32, default 8)
  • $REVERB:p[:q] - Room reverb. p = wet mix. q = room size/decay (default 50)
  • $CHORUS:p[:q] - Detuned doubling (thickens). p = depth. q = LFO rate (default 30)
  • $FLANGER:p[:q] - Sweeping comb (jet). p = depth. q = sweep rate (default 30)
  • $PHASER:p[:q] - All-pass sweep. p = depth. q = sweep rate (default 30)
  • $TREMOLO:p[:q] - Amplitude wobble. p = depth. q = LFO rate (default 40)
  • $VIBRATO:p[:q] - Pitch wobble. p = depth. q = LFO rate (default 40)
  • $DRIVE:p[:q] - Soft saturation/overdrive. p = drive. q = post tone/brightness (default 70)
  • $BITCRUSH:p[:q] - Lo-fi crush. p = bit reduction. q = sample-rate reduction (default 0)
  • $FILTER:p[:q] - Lowpass. p = cutoff. q = resonance (default 10)
  • $HIGHPASS:p[:q] - Highpass. p = cutoff. q = resonance (default 10)
  • $PAN:p - Static placement. p: 0 = hard left, 50 = center, 100 = hard right

Examples

PLAY "C D E F G A B"
PLAY "T120 L4 C E G > C"
PLAY "I5 O4 C D E P4 F G A"
PLAY "I30 L4 C C C C"    ' Kick drum
PLAY "I43 C E G"        ' Coin pickup sound
PLAY "I22 O3 L2 C E G"  ' Brass-like chord
PLAY "L4 [CEG] [FAC] [GBD]"  ' Chord progression
PLAY "O4 [C E G] P4 [D F A]"  ' Chords with rest between
PLAY "L4 C^16 D E"           ' C held for quarter + sixteenth, then D E
PLAY "L8 C^^ D"              ' Two chained ties extend C by two more eighths
PLAY "L2 C~2 E~-1"           ' C bends up 2 semitones, E bends down 1
PLAY "L2 C&E G&C"            ' Portamento: C glides to E, G glides to C
PLAY "L4 C~12,8"             ' Bend up an octave over an eighth, then hold
PLAY "L2 [CEG]~2 [CEG]&F"    ' Whole chord bends up 2, then glides to F
PLAY "$ECHO:30:8 T120 L8 O5 C E G > C"   ' Lead with a tempo-synced echo
PLAY "$DRIVE:40 $REVERB:35 O3 L2 C E G"  ' Saturated, reverberant chord (drive then reverb)
PLAY BACKGROUND "$FILTER:55 $CHORUS:50 T110 L4 C G E G"  ' Filtered, chorused background pad

See also: SOUND

↑ Back to top

PLAYMUSIC

Sound · 📱 Client-only · since 1.24.0

Plays a multi-track song by name. Songs must be loaded first (typically from a .cbm music bank file included in your bundle, or created in the Music Editor). The song plays in the background, allowing your program to continue. Only one song can play at a time; calling PLAYMUSIC again stops the current song.

The optional LOOP / NOLOOP modifier overrides the loop flag declared for the song in its bank definition. Without the modifier, the song's declared default is used (typical for menu/overworld themes that loop and stinger/fanfare cues that don't). LOOP forces continuous looping regardless of the bank's setting; NOLOOP forces single playback.

Rendering: a song has to be rendered to audio before it can play, and for a dense song with sampled or wavetable instruments that takes seconds. PLAYMUSIC renders on demand, so it always works — but the render happens right then, and the song starts late.

Use LOADMUSIC to move that cost somewhere you control (a loading screen, a scene transition). After LOADMUSIC has pre-rendered a song, PLAYMUSIC on it is instant. Songs you never pre-render still play; they just pay the render at first play.

Changing instrument state (LOADINSTRUMENT, LOADINSTRUMENTBANK, SETINSTRUMENTWAV) invalidates every cached render, since a song rendered against the old instruments is now wrong. Songs you pre-rendered with LOADMUSIC are re-rendered immediately so they stay instant; everything else simply renders again on demand. Most programs only touch instrument state during init / scene setup, where this is invisible.

Prior to 1.51.0 every song in the bank was rendered eagerly when the bank loaded, which put the whole cost — sometimes many seconds — into startup whether the program played the music or not.

Multiplayer: PLAYMUSIC is client-only. Wrap in IF ME.ISCLIENT THEN ... END IF, or trigger from the server with EVENT CLIENT "name" so the client's handler runs PLAYMUSIC. Calling it on the host or a player-server raises a fatal runtime error.

Fire-and-forget: PLAYMUSIC does not block waiting for an engine acknowledgement (a looping song would never return). If the song name isn't in the music bank, the engine emits an error event that surfaces in the runtime log as [native] AUDIO ERROR: Song '...' not found in music bank but does not halt execution. To fail loud on a missing song, pre-validate with BOOM at the call site, e.g. BOOM NOT MUSICEXISTS("theme"), "theme not loaded".

Syntax

PLAYMUSIC song_name [LOOP | NOLOOP]

Parameters

  • song_name - String name of the song to play (as defined in the music file)
  • LOOP - Optional. Force the song to loop continuously, ignoring the bank's declared loop flag.
  • NOLOOP - Optional. Force single playback, ignoring the bank's declared loop flag.

Examples

PLAYMUSIC "Battle Theme"          ' Use the song's declared loop default
PLAYMUSIC "Battle Theme" LOOP     ' Force loop regardless of bank setting
PLAYMUSIC "Victory Fanfare" NOLOOP ' Stinger that plays once
PLAYMUSIC "Menu Music"
REM Play different music based on game state
IF inBattle THEN
  PLAYMUSIC "Battle Theme"
ELSE
  PLAYMUSIC "Overworld"
END IF

See also: LOADMUSIC, STOPMUSIC, PAUSEMUSIC, RESUMEMUSIC, FADEMUSIC, MUSICPLAYING

↑ Back to top

RESUMEMUSIC

Sound · 📱 Client-only · since 1.7.2

Resumes a paused multi-track song from where it was paused. Has no effect if music is not paused.

Syntax

RESUMEMUSIC

Examples

RESUMEMUSIC
REM Resume music when game is unpaused
SUB UnpauseGame()
  gamePaused = 0
  RESUMEMUSIC
END SUB

See also: PAUSEMUSIC, PLAYMUSIC, STOPMUSIC

↑ Back to top

SET ARRANGEMENT

Sound · 📱 Client-only · since 1.51.0

Replaces a song's whole arrangement with the given patterns, each played once. With no patterns at all, clears the arrangement.

For per-step control (repeat counts and layering) use ADD ARRANGEMENT or a MUSIC block's ARRANGEMENT section. Naming a pattern the song doesn't define is an error.

Syntax

SET ARRANGEMENT song [, pattern1, pattern2, ...]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern1, pattern2, ... - Patterns to play in order

Examples

SET ARRANGEMENT "battle", "intro", "verse", "verse", "chorus"
' Strip a song back to its loop
SET ARRANGEMENT "battle", "verse"

See also: ADD ARRANGEMENT, INSERT ARRANGEMENT, ARRANGEMENTCOUNT, MUSIC

↑ Back to top

SET AUDIO

Sound · 📱 Client-only · since 1.46.0

Live-updates the volume and/or stereo pan of a currently-playing sound by name — a WAV (PLAYWAV), an MP3 (PLAYMP3), or a multi-track song (PLAYMUSIC). Use it to duck background music under a cutscene, pan a positional sound as its source moves, or ride a sound's level over time.

The VOLUME and PAN modifiers may appear in any order, and at least one must be present. A modifier you omit leaves that parameter unchanged — SET AUDIO "music" PAN(0.5) adjusts only the pan and keeps the current volume. Setting a parameter on a sound that isn't currently playing is a harmless no-op.

VOLUME(v) takes v in 0.0 (silent) to 1.0 (full). PAN(x [, y]) takes x in -1.0 (hard left) to 1.0 (hard right), 0.0 = center; the optional y is a vertical/depth axis reserved for future 3D audio and is not yet mixed. PAN also accepts a single coordinate value (PAN(pos)) whose x and y fill both axes.

For multi-track music this targets the whole song. Multiplayer: SET AUDIO is client-only — it adjusts local playback and raises a fatal runtime error if called on the host or a player-server. Wrap in IF ME.ISCLIENT THEN ... END IF.

Syntax

SET AUDIO name VOLUME(v) PAN(x [, y])

Parameters

  • name - String name of the playing WAV, MP3, or song to adjust
  • VOLUME(v) - Optional. Set playback volume, v 0.0 (silent) to 1.0 (full)
  • PAN(x [, y]) - Optional. Set stereo pan: x -1.0 (left) to 1.0 (right), 0.0 = center. Optional y (depth) is reserved. A single coordinate fills both axes.

Examples

SET AUDIO "music" VOLUME(0.4)            ' Duck the background music
SET AUDIO "engine" PAN(0.6)             ' Pan a looping engine sound right
SET AUDIO "alarm" VOLUME(0.8) PAN(-0.5) ' Set both at once
REM Pan a sound to follow an on-screen object
SET AUDIO "engine" PAN(enemyX / 160.0 - 1.0)

See also: PLAYWAV, PLAYMP3, PLAYMUSIC, FADEMUSIC

↑ Back to top

SET MUSIC

Sound · 📱 Client-only · since 1.51.0

Adjusts a song's own properties without touching its patterns or arrangement. At least one modifier is required; they may appear in any order, and anything you don't mention is left alone.

The change takes effect the next time the song is played. To hear it immediately, call PLAYMUSIC again (music is pre-rendered, so a tempo change cannot be applied to a song already in flight).

Both TEMPO(140) and TEMPO 140 are accepted.

Syntax

SET MUSIC song [TEMPO(n)] [LOOP | NOLOOP] [TIMESIG(beats, unit)]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • TEMPO(n) - Beats per minute
  • LOOP | NOLOOP - Whether the song repeats when played
  • TIMESIG(beats, unit) - Time signature, e.g. TIMESIG(6, 8)

Examples

SET MUSIC "battle" TEMPO(160)
' Speed the music up as the timer runs down
SET MUSIC "battle" TEMPO(120 + (60 - secondsLeft))
PLAYMUSIC "battle"
SET MUSIC "fanfare" NOLOOP TIMESIG(3, 4)

See also: MUSIC, SET TRACK, MUSICTEMPO, PLAYMUSIC

↑ Back to top

SET MUSIC PATTERN

Sound · 📱 Client-only · since 1.51.0

Sets a pattern's length in beats. A pattern shorter than its longest track is extended to fit; tracks marked REPEAT tile to fill the length.

Syntax

SET MUSIC PATTERN song, pattern LENGTH(beats)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • LENGTH(beats) - Pattern length in beats

Example

SET MUSIC PATTERN "battle", "verse" LENGTH(32)

See also: CREATE MUSIC PATTERN, MUSICPATTERNLENGTH, SET TRACK

↑ Back to top

SET TRACK

Sound · 📱 Client-only · since 1.51.0

Changes one track's properties. At least one modifier is required, and this is a partial update: setting VOLUME leaves the notes, instrument and pan exactly as they were.

Track modifiers, all optional and in any order. Anything you don't mention keeps its current value.

  • CHANNEL(n) — MIDI channel 1 to 16 (10 is conventionally drums)
  • INSTRUMENT(n) — instrument number (built-ins are 0 to 66; .cbi instruments may use any number)
  • VOLUME(n) — 0 to 100
  • PAN(x) — -1.0 (left) to 1.0 (right)
  • NAME(s$) — a label for the track, shown in the Music Editor
  • NOTES(s$) — the track's MML
  • GENERATOR(s$) — name of a BASIC FUNCTION that supplies the MML instead (see GENERATOR)
  • REPEAT / NOREPEAT — whether the track tiles to fill the pattern's length

Every value modifier accepts both VOLUME(80) and VOLUME 80.

The change takes effect the next time the song is played.

Syntax

SET TRACK song, pattern, index [modifiers...]

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern
  • modifiers - Any combination of the track modifiers above

Examples

SET TRACK "battle", "verse", 1 VOLUME(20) PAN(-0.4)
' Swap a lead line for a different instrument mid-game
SET TRACK "battle", "chorus", 0 INSTRUMENT(24)
PLAYMUSIC "battle"
' Replace a track's notes outright
SET TRACK "battle", "verse", 0 NOTES "o2 c4 g4 c4 g4"

See also: CREATE TRACK, MUTE TRACK, GENERATOR, TRACKVOLUME, MUSIC

↑ Back to top

SETINSTRUMENTWAV

Sound · 📱 Client-only · since 1.23.0

Binds a WAV sample (previously loaded with LOADWAV) as the oscillator source for an instrument. The instrument's existing modulation — ADSR envelope, filter + filter envelope, vibrato, tremolo, FM — still applies; only the waveform source changes. This lets authors layer any .cbi instrument's modulation shell over a custom sampled waveform for sampled-instrument playback (similar in spirit to SNES sample-based instruments). Bindings persist across music bank (.cbm) loads — clear them by passing an empty handle. Without an explicit layer number, the binding applies to all layers of the instrument; specify a layer to bind only one. BASEPITCH sets the MIDI note the sample represents at its native rate (default A4); accepts either a note name ("C4", "A#5", "Bb3") or a raw MIDI number 0-127. LOOP sets forward loop points in sample frames; omitted means one-shot playback.

Syntax

SETINSTRUMENTWAV instrument, [layer,] handle [BASEPITCH note] [LOOP start, end]

Parameters

  • instrument - Instrument number (0-255) matching your PLAY/MML @N references
  • layer - (optional) 0-indexed layer to bind; omitted = all layers of the instrument
  • handle - String handle of a WAV loaded via LOADWAV; empty string clears the binding
  • note - (optional BASEPITCH) MIDI note the sample represents at native rate; default A4 (an unrecognized note name or out-of-range number also falls back to A4)
  • start, end - (optional LOOP) Forward loop points in sample frames

Examples

LOADWAV "samples/piano.wav", "piano"
SETINSTRUMENTWAV 1, "piano"
' Bind only to layer 0, leaving other layers as the original .cbi waveform
SETINSTRUMENTWAV 1, 0, "piano"
' Pitched + looped for sustained instruments
SETINSTRUMENTWAV 2, "strings" BASEPITCH "A4" LOOP 4800, 24000
' Clear a binding and revert to the original .cbi waveform
SETINSTRUMENTWAV 1, ""
' Typical workflow: set up once at game start, play indefinitely after
LOADWAV "samples/kick.wav", "kick"
LOADWAV "samples/snare.wav", "snare"
SETINSTRUMENTWAV 10, "kick" BASEPITCH 36
SETINSTRUMENTWAV 11, "snare" BASEPITCH 38
PLAY "I10 o3 l16 c c I11 e c"

See also: LOADWAV, PLAY, INSTRUMENTMODE, PLAYMUSIC

↑ Back to top

SOLO TRACK

Sound · 📱 Client-only · since 1.51.0

Plays only the soloed tracks of a song. Solo beats mute, as on any mixer: while anything in the song is soloed, every track that isn't soloed is silent, including un-muted ones in other patterns. UNSOLO on the last soloed track returns the song to normal mute behaviour.

Takes effect the next time the song is played. Read the state back with TRACKSOLOED.

Syntax

SOLO TRACK song, pattern, index
UNSOLO TRACK song, pattern, index

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Examples

SOLO TRACK "battle", "verse", 0
PLAYMUSIC "battle"
UNSOLO TRACK "battle", "verse", 0

See also: MUTE TRACK, TRACKSOLOED, SET TRACK

↑ Back to top

SONGCOUNT

Sound · since 1.51.0

Number of songs in the music bank.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

SONGCOUNT()

Example

FOR i = 0 TO SONGCOUNT() - 1
  PRINT SONGNAME$(i)
NEXT

See also: SONGNAME$, MUSICEXISTS

↑ Back to top

SONGNAME$

Sound · since 1.51.0

Name of a song in the bank. Songs are ordered alphabetically ignoring case, so enumeration is stable from run to run.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

SONGNAME$(index)

Parameters

  • index - Zero-based song index

Example

name$ = SONGNAME$(0)

See also: SONGCOUNT, MUSICEXISTS

↑ Back to top

SOUND

Sound · 📱 Client-only · since 0.23.0

Generates tones by frequency and duration. Non-blocking - program continues immediately while sound plays.

Syntax

SOUND frequency, duration

Parameters

  • frequency - Sound frequency in Hz
  • duration - Duration in ticks (18.2/sec)

Examples

SOUND 440, 18  ' A note for 1 second (non-blocking)
FOR F = 200 TO 2000 STEP 100
  SOUND F, 2  ' Quick beep sequence
NEXT
SOUND 1000, 5  ' Sound effect
PRINT "Sound playing in background"

See also: PLAY, BEEP

↑ Back to top

STOP PLAY

Sound · 📱 Client-only · since 0.23.0

Stops background music started with PLAY BACKGROUND. Does not affect foreground PLAY commands or SOUND effects.

Syntax

STOP PLAY

Examples

PLAY BACKGROUND "T120 C D E F G"
PRINT "Music playing..."
STOP PLAY
PRINT "Music stopped!"
REM Toggle background music
IF musicPlaying THEN
  STOP PLAY
  musicPlaying = 0
ELSE
  PLAY BACKGROUND "C E G C"
  musicPlaying = 1
END IF

See also: PLAY BACKGROUND, PLAY

↑ Back to top

STOPMUSIC

Sound · 📱 Client-only · since 1.7.2

Stops the currently playing multi-track song. Has no effect if no music is playing.

Syntax

STOPMUSIC

Examples

STOPMUSIC
REM Stop music when entering a cutscene
STOPMUSIC
PLAYMP3 "cutscene_audio"
REM Clean stop when game ends
SUB GameOver()
  STOPMUSIC
  PLAYMUSIC "Game Over"
END SUB

See also: PLAYMUSIC, PAUSEMUSIC, MUSICPLAYING

↑ Back to top

TRACKCHANNEL

Sound · since 1.51.0

A track's MIDI channel, 1 to 16.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKCHANNEL(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

IF TRACKCHANNEL("battle", "verse", t) = 10 THEN PRINT "drums"

See also: SET TRACK, TRACKINSTRUMENT

↑ Back to top

TRACKCOUNT

Sound · since 1.51.0

Number of tracks in a pattern. Valid track indices are 0 to TRACKCOUNT - 1.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKCOUNT(song, pattern)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)

Example

FOR t = 0 TO TRACKCOUNT("battle", "verse") - 1
  MUTE TRACK "battle", "verse", t
NEXT

See also: CREATE TRACK, SET TRACK

↑ Back to top

TRACKGENERATOR$

Sound · since 1.51.0

The name of the BASIC function bound to a track as its generator, or the empty string if it has none. Reads back exactly as it was written.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKGENERATOR$(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

IF TRACKGENERATOR$("ambient", "drift", 0) <> "" THEN PRINT "generative"

See also: GENERATOR, SET TRACK

↑ Back to top

TRACKINSTRUMENT

Sound · since 1.51.0

A track's instrument number.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKINSTRUMENT(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

PRINT TRACKINSTRUMENT("battle", "verse", 0)

See also: SET TRACK, TRACKCHANNEL

↑ Back to top

TRACKMUTED

Sound · since 1.51.0

TRUE if the track is muted. Lets a program read its own mixer state back instead of shadowing it in variables.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKMUTED(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

IF TRACKMUTED("battle", "verse", 1) THEN UNMUTE TRACK "battle", "verse", 1

See also: MUTE TRACK, TRACKSOLOED

↑ Back to top

TRACKNAME$

Sound · since 1.51.0

A track's label, or the empty string if it has none.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKNAME$(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

PRINT TRACKNAME$("battle", "verse", 0)

See also: SET TRACK, CREATE TRACK

↑ Back to top

TRACKNOTES$

Sound · since 1.51.0

A track's MML. For a generator track this is whatever the generator last produced, which is empty until the song has been played or applied.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKNOTES$(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

PRINT TRACKNOTES$("battle", "verse", 0)

See also: SET TRACK, GENERATOR, PLAY

↑ Back to top

TRACKPAN

Sound · since 1.51.0

A track's stereo position, -1.0 (left) to 1.0 (right).

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKPAN(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

PRINT TRACKPAN("battle", "verse", 0)

See also: SET TRACK, TRACKVOLUME

↑ Back to top

TRACKREPEAT

Sound · since 1.51.0

TRUE if the track tiles to fill its pattern's length.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKREPEAT(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

IF TRACKREPEAT("battle", "verse", 1) THEN PRINT "tiles"

See also: SET TRACK, MUSICPATTERNLENGTH

↑ Back to top

TRACKSOLOED

Sound · since 1.51.0

TRUE if the track is soloed.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKSOLOED(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

IF TRACKSOLOED("battle", "verse", 0) THEN UNSOLO TRACK "battle", "verse", 0

See also: SOLO TRACK, TRACKMUTED

↑ Back to top

TRACKVOLUME

Sound · since 1.51.0

A track's volume, 0 to 100.

Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.

Syntax

TRACKVOLUME(song, pattern, index)

Parameters

  • song - Name of the song in the music bank (matched ignoring case)
  • pattern - Name of a pattern within that song (matched ignoring case)
  • index - Zero-based track index within the pattern

Example

' Duck a track by half
v = TRACKVOLUME("battle", "verse", 0)
SET TRACK "battle", "verse", 0 VOLUME(v / 2)

See also: SET TRACK, TRACKPAN

↑ Back to top

Video

LOADVIDEO

Video · 🧪 Experimental · since 1.9.3

Loads a video file from local resources for playback. The video is decoded on a dedicated thread (native) or Web Worker (WASM). Supported formats are MP4 (H.264) and WebM (VP8/VP9). Loading is synchronous — execution pauses until the video metadata is read and the video is ready to play. Argument order matches LOADWAV/LOADMP3/LOADIMG: filename first, handle name second.

In CrashNet multiplayer, resource loads are client-only — calling LOADVIDEO from server-side code is a fatal runtime error. Each client loads its own copies inside an IF ME.ISCLIENT THEN block.

Syntax

LOADVIDEO filename$, name$

Parameters

  • filename$ - Path to the video file relative to the game directory
  • name$ - Reference name for the video (used in PLAYVIDEO, STOPVIDEO, etc.)
  • Supported formats: MP4 (H.264), WebM (VP8/VP9)

Examples

LOADVIDEO "cutscenes/intro.mp4", "intro"
LOADVIDEO "cutscenes/ending.webm", "ending"
REM Load and play a cutscene
LOADVIDEO "intro.mp4", "intro"
PLAYVIDEO "intro"
WAITVIDEO "intro"

See also: PLAYVIDEO, STOPVIDEO, WAITVIDEO, LOADMP3, LOADWAV

↑ Back to top

PAUSEVIDEO

Video · 🧪 Experimental · since 1.9.3

Pauses playback of an active video. The current frame remains displayed on the layer. Use RESUMEVIDEO to continue playback from where it was paused.

Syntax

PAUSEVIDEO name$

Parameters

  • name$ - Reference name of the video to pause

Examples

PAUSEVIDEO "cutscene"
REM Pause on user input
IF INKEY$ = " " THEN
  PAUSEVIDEO "cutscene"
END IF

See also: RESUMEVIDEO, PLAYVIDEO, STOPVIDEO

↑ Back to top

PLAYVIDEO

Video · 🧪 Experimental · since 1.9.3

Plays a previously loaded video. The decoded frames are rendered to the specified graphics layer. Playback runs asynchronously — execution continues immediately unless WAITVIDEO is used. The LOOP modifier restarts the video when it finishes. The MUTE modifier suppresses the video's audio track.

Layer effects do NOT reliably apply to video playback. To support hardware mp4 decode, each platform renders video through its own native pipeline (for example a hardware-accelerated HTML <video> element on web), which composites outside the layer system on some platforms. Do not rely on layer CLIP, ALPHA, SHADER, or plane (AFFINE/PERSPECTIVE) modes to mask or transform a playing video — treat video as exempt from layer compositing. Use the video's own target dimensions and position instead.

Syntax

PLAYVIDEO name$ [, layer] [LOOP] [MUTE]

Parameters

  • name$ - Reference name of a loaded video
  • layer - Optional graphics layer to render on (default 0). The video frames replace the layer's texture each frame.
  • LOOP - Optional modifier that restarts the video when it finishes
  • MUTE - Optional modifier that suppresses the video's audio track

Examples

PLAYVIDEO "intro"
PLAYVIDEO "intro", 2
PLAYVIDEO "intro" LOOP
PLAYVIDEO "intro" MUTE
PLAYVIDEO "background", 0 LOOP MUTE
REM Play a cutscene on layer 1 and wait for it
CREATE LAYER 1, 320, 200
LOADVIDEO "scene1.mp4", "cutscene"
PLAYVIDEO "cutscene", 1
WAITVIDEO "cutscene"
FREE LAYER 1

See also: LOADVIDEO, PAUSEVIDEO, RESUMEVIDEO, STOPVIDEO, WAITVIDEO, CREATE LAYER

↑ Back to top

RESUMEVIDEO

Video · 🧪 Experimental · since 1.9.3

Resumes playback of a paused video from where it was paused.

Syntax

RESUMEVIDEO name$

Parameters

  • name$ - Reference name of the video to resume

Examples

RESUMEVIDEO "cutscene"
REM Toggle pause with spacebar
IF INKEY$ = " " THEN
  IF videoPaused = 0 THEN
    PAUSEVIDEO "cutscene"
    videoPaused = 1
  ELSE
    RESUMEVIDEO "cutscene"
    videoPaused = 0
  END IF
END IF

See also: PAUSEVIDEO, PLAYVIDEO, STOPVIDEO

↑ Back to top

SEEKVIDEO

Video · 🧪 Experimental · since 1.9.3

Seeks to a specific time position in a loaded video. The position is specified in seconds (supports fractional values for sub-second precision).

Syntax

SEEKVIDEO name$, seconds

Parameters

  • name$ - Reference name of the video to seek
  • seconds - Target time position in seconds (e.g., 1.5 for one and a half seconds)

Examples

SEEKVIDEO "cutscene", 0
SEEKVIDEO "cutscene", 10.5
REM Skip ahead 5 seconds
SEEKVIDEO "intro", 5.0
PLAYVIDEO "intro"

See also: PLAYVIDEO, LOADVIDEO, PAUSEVIDEO

↑ Back to top

STOPVIDEO

Video · 🧪 Experimental · since 1.9.3

Stops playback and unloads a video, freeing its resources. The video must be reloaded with LOADVIDEO before it can be played again.

Stopping a video releases any WAITVIDEO blocked on it. That is what makes a skippable cutscene work: the program waits on the video while an event handler calls STOPVIDEO on a keypress. A stopped video reports no "finished" event — it did not finish — so WAITVIDEO ends on the stop itself.

Syntax

STOPVIDEO name$

Parameters

  • name$ - Reference name of the video to stop and unload

Examples

STOPVIDEO "cutscene"
REM Skip cutscene on key press
PLAYVIDEO "intro"
WHILE videoPlaying
  IF INKEY$ <> "" THEN
    STOPVIDEO "intro"
    videoPlaying = 0
  END IF
  BEGIN FRAME
  END FRAME
WEND

See also: PLAYVIDEO, LOADVIDEO, PAUSEVIDEO

↑ Back to top

WAITVIDEO

Video · 🧪 Experimental · since 1.9.3

Blocks execution until the specified video finishes playing or is stopped. This is the primary way to implement cutscenes that must complete before gameplay resumes. If the video is looping, WAITVIDEO will block indefinitely until the video is stopped by another thread or event handler.

Syntax

WAITVIDEO name$

Parameters

  • name$ - Reference name of the video to wait for

Examples

WAITVIDEO "intro"
REM Full cutscene pattern
LOADVIDEO "cutscenes/intro.mp4", "intro"
PLAYVIDEO "intro"
WAITVIDEO "intro"
PRINT "Cutscene finished!"
REM Skippable cutscene with event handler
DECLARE SUB SkipCutscene()
CALL EVENT SkipCutscene() AS "keypress"
LOADVIDEO "intro.mp4", "intro"
PLAYVIDEO "intro"
WAITVIDEO "intro"
STOP THREAD "keypress"

SUB SkipCutscene()
  STOPVIDEO "intro"
END SUB

See also: PLAYVIDEO, LOADVIDEO, STOPVIDEO, CALL EVENT

↑ Back to top

File I/O

CALL SQL

File I/O · since 1.56.0

Runs a declared SQL block against an open database. Arguments are passed exactly as a behavior's are — @name=value — and merge over the block's declared defaults.

A statement that returns rows binds INTO an array of objects, one per row, with the columns as named fields: rows(0).name, rows(0).score — object fields carry no $ suffix, whatever the column's type. A statement that changes the database instead reports through SQLROWSAFFECTED and SQLLASTID.

Syntax

CALL SQL "name" ON #n [(@param=value, ...)] [INTO variable]

Parameters

  • name - The SQL block to run.
  • #n - An open database handle.
  • @param=value - Values for the block's declared parameters. Anything the call site omits falls back to that parameter's DEFAULT.
  • INTO variable - Receives an array of row objects. Omit it for a statement that returns no rows.

Examples

CALL SQL "topScores" ON #1 (@minScore=1000, @limit=5) INTO rows

DIM i AS INTEGER
FOR i = 0 TO UBOUND(rows)
    PRINT rows(i).name; " "; rows(i).score
NEXT i
' A write reports what it changed
CALL SQL "addRun" ON #2 (@who$="Dan", @score=1200)
PRINT "rows: "; SQLROWSAFFECTED; " id: "; SQLLASTID

Notes

  • Results are objects. UBOUND(rows) gives the last index and rows(i).columnName reads a field, using the column name the query produced — name a computed column with AS to get a usable field. Object fields take no $ suffix, so a text column is rows(i).name, not rows(i).name$.
  • After a write, SQLROWSAFFECTED holds the number of changed rows and SQLLASTID the last inserted row id.
  • INTO on a statement that returns no rows is reported — it would otherwise look like an empty result rather than a mistake.
  • Blobs have no BASIC type; a blob column reads as its length in bytes.

See also: SQL, OPEN DATABASE, CLOSE

↑ Back to top

CLOSE

File I/O · since 1.0.0

Closes open file handles or DATA block handles. Always close handles when done to free resources. Can close multiple handles in one statement.

One handle, one thing. Files, DATA blocks and databases share the #n space so that CLOSE #n is unambiguous. Opening onto a number something else already holds is a runtime error naming what holds it — in either direction, for either kind. CLOSE frees the number for any of them again.

Syntax

CLOSE #filenumber [, #filenumber2, ...]

Parameters

  • filenumber - File or DATA block handle number to close

Examples

OPEN DATA "scores" AS #1
DIM score AS INTEGER
READ #1, score
CLOSE #1
' Close multiple handles
OPEN DATA "block1" AS #1
OPEN DATA "block2" AS #2
CLOSE #1, #2
' Close after reading all data
DATA "palette"
  255, 0, 0
  0, 255, 0
END DATA

OPEN DATA "palette" AS #1
WHILE NOT EOF(1)
  DIM r AS INTEGER, g AS INTEGER, b AS INTEGER
  READ #1, r, g, b
WEND
CLOSE #1

See also: OPEN DATA, DATA, READ, EOF

↑ Back to top

DESERIALIZE$

File I/O · since 0.23.0

Parses a JSON string back into its original data structure. Used to restore game state, player data, settings, and other complex data from storage. Accepts any valid JSON, including data from external APIs or files.

A rectangular nested JSON array is restored as a true multi-dimensional array, indexable as a(row, col) — so a matrix from another tool round-trips as a matrix. A ragged or mixed nesting (rows of differing lengths) has no single shape, so it stays an array of arrays, indexed a(row)(col). LOADJSON reconstructs arrays the same way.

Syntax

DESERIALIZE$(serializedData$)

Parameters

  • serializedData$ - A JSON string, typically from SERIALIZE$ or LOADTEXT

Examples

savedData$ = LOADTEXT("player.dat")
DIM player AS Object
player = DESERIALIZE$(savedData$)
PRINT "Welcome back, " + player.name
scoresData$ = LOADTEXT("highscores.dat")
DIM highScores(10) AS Number
highScores = DESERIALIZE$(scoresData$)
PRINT "High Score: " + STR$(highScores(0))
TYPE GameState
  level AS Number
  lives AS Number
END TYPE
savedGame$ = LOADTEXT("savegame.dat")
DIM state AS GameState
state = DESERIALIZE$(savedGame$)
PRINT "Resuming level " + STR$(state.level)
REM Load and apply game settings
configData$ = LOADTEXT("config.json")
DIM config AS Object
config = DESERIALIZE$(configData$)
IF config.sound THEN CALL enableSound()
CALL setGraphicsQuality(config.graphics)

See also: SERIALIZE$, LOADTEXT, LOADJSON

↑ Back to top

EOF

File I/O · since 1.0.0

Returns TRUE (1) if the handle is exhausted, FALSE (0) otherwise. Works for both DATA block handles (exhausted when every value has been READ) and file-stream handles opened FOR INPUT (exhausted when every line has been read). For a closed or invalid handle, EOF returns TRUE — so a stray WHILE NOT EOF(n) on a never-opened handle terminates immediately rather than spinning.

Syntax

EOF(filenumber)

Parameters

  • filenumber - File handle number (without the # symbol)

Examples

DATA "numbers"
  10, 20, 30, 40, 50
END DATA

OPEN DATA "numbers" AS #1
DIM num AS INTEGER
WHILE NOT EOF(1)
  READ #1, num
  PRINT STR$(num)
WEND
CLOSE #1
' Load all enemies from DATA block
DATA "enemies"
  "Goblin", 10
  "Orc", 25
  "Dragon", 100
END DATA

OPEN DATA "enemies" AS #1
DIM name AS STRING, hp AS INTEGER
DIM count AS INTEGER
count = 0
WHILE NOT EOF(1)
  READ #1, name, hp
  count = count + 1
  PRINT STR$(count) + ": " + name
WEND
CLOSE #1
' Read a config file line by line
OPEN "config.txt" FOR INPUT AS #1
DIM line$ AS STRING
WHILE NOT EOF(1)
  LINE INPUT #1, line$
  PRINT line$
WEND
CLOSE #1

See also: DATA, OPEN, OPEN DATA, READ, LINE INPUT, LOC, LOF, CLOSE

↑ Back to top

FETCH

File I/O · since 1.30.0

Synchronous HTTP client. Returns an Object with exactly two fields: STATUSCODE (Integer) and MESSAGE (String). On success, STATUSCODE is the HTTP status (200, 404, 500, ...) and MESSAGE is the response body as text. On failure to complete an HTTP exchange (DNS failure, connection refused, TLS error, invalid URL, invalid method, timeout), STATUSCODE is 0 and MESSAGE is a human-readable error description. Network errors do NOT halt the interpreter — the BASIC program detects them via STATUSCODE. A non-Object headers argument is handled the same way (STATUSCODE=0 with the error in MESSAGE; it does not halt). Only a wrong argument count halts.

Body types: String passes through verbatim. Object/Array auto-encode as JSON with Content-Type: application/json (unless overridden via headers). Anything else is stringified.

Headers: optional 4th arg is an Object of header names to values.

Native: blocking reqwest client with a 30-second timeout. WASM: synchronous XMLHttpRequest in the Worker the interpreter runs on. Cross-origin destinations on WASM are subject to normal browser CORS rules. Sync FETCH blocks the worker — for slow or parallel calls, wrap in ASYNC FETCH(...) and RESOLVE().

Syntax

result = FETCH(url$, method$ [, body [, headers]])

Parameters

  • url$ - Full URL of the resource
  • method$ - HTTP method (case-insensitive: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ...)
  • body - Optional body. String sent as-is. Object/Array auto-encode as JSON with Content-Type: application/json. Other types stringified.
  • headers - Optional 4th-arg Object of header names → values (e.g. {"Authorization": "Bearer xyz"})

Examples

REM Simple GET
r = FETCH("https://api.example.com/score", "GET")
IF r.STATUSCODE = 200 THEN
  PRINT r.MESSAGE
END IF
REM POST with auto-JSON body
DIM payload AS OBJECT
payload.player = "alice"
payload.score = 4200
r = FETCH("https://api.example.com/scores", "POST", payload)
REM Headers (4th arg)
DIM headers AS OBJECT
headers.Authorization = "Bearer " + token$
r = FETCH(url$, "GET", "", headers)
REM Parallel fetches via ASYNC
p1 = ASYNC FETCH("https://api.example.com/leaderboard", "GET")
p2 = ASYNC FETCH("https://api.example.com/news", "GET")
results = RESOLVE(p1, p2)
lb = results(0)
news = results(1)
REM Detect network failure (STATUSCODE = 0) vs HTTP error (4xx/5xx)
r = FETCH(url$, "GET")
IF r.STATUSCODE = 0 THEN
  PRINT "Network error: " + r.MESSAGE
ELSEIF r.STATUSCODE >= 400 THEN
  PRINT "HTTP "; r.STATUSCODE; ": "; r.MESSAGE
ELSE
  PRINT "Got: " + r.MESSAGE
END IF

See also: ASYNC, RESOLVE, BASE64TOIMG, BASE64TOWAV, BASE64TOMP3

↑ Back to top

FILEEXISTS

File I/O · since 1.0.0

Checks whether a file exists in the project's local storage (the same VFS that LOADTEXT, SAVETEXT, and OPEN use). Returns 1 if the file is present, 0 otherwise.

Distinct from EXISTS, which checks the in-memory resource manager (LOADIMG / LOADWAV / LOADMP3 / loaded text resources). FILEEXISTS is for raw filesystem paths — use it to gate LOADTEXT, LOADJSON, or OPEN against missing optional saves and configs without triggering a runtime error.

Syntax

result = FILEEXISTS(filename$)

Parameters

  • filename$ - Path to test, same form accepted by LOADTEXT / OPEN

Examples

REM Only restore if a save actually exists
IF FILEEXISTS("savegame.json") THEN
  saved = LOADJSON("savegame.json")
END IF
REM Decide between user settings and defaults
IF FILEEXISTS("settings.dat") THEN
  s$ = LOADTEXT("settings.dat")
ELSE
  s$ = LOADTEXT("defaults.dat")
END IF

See also: EXISTS, LOADTEXT, LOADJSON, SAVETEXT, OPEN

↑ Back to top

FILES

File I/O · since 1.36.4

Returns a 1D array of filenames in the bus root or in path$. Used to discover what's loadable / saved / available without hardcoding lists in BASIC.

Unified single- and multi-player behavior. FILES returns the union of cart-bundled assets AND user-saved files. The same call on the same logical content gives the same answer in solo play, on a host server, and on a player-server — a file written via SAVETEXT / SAVEJSON / OPEN FOR OUTPUT shows up in a subsequent FILES() call regardless of which interpreter wrote it. (Per-player saves on a server are scoped to that player; another player's saves are not visible.)

Shape:

  • Regular files only. Subdirectories are silently skipped — they don't appear as entries. Pass the subdirectory as path$ to enumerate inside.
  • Basenames only. The returned strings are just the filename, not the full path. Concatenate with the queried directory if you need full paths.
  • Sorted alphabetically (byte order). Deterministic across platforms and across native / WASM / multiplayer — important for replays and lockstep multiplayer.
  • Dotfiles excluded (.DS_Store, editor swap files, etc.).

Sandbox. path$ is resolved inside the bus's storage root. Attempts to escape (.., absolute paths to outside the sandbox) raise a fatal runtime error rather than silently returning an empty array — bugs in path construction fail loud.

No path$ (or empty "", ".", "/") lists the bus root.

Syntax

result = FILES([path$])

Parameters

  • path$ - Optional directory inside the bus root. Omit (or pass empty) for the root.

Examples

REM List every file in the project root
FOR EACH f IN FILES()
  PRINT f
NEXT
REM Enumerate save slots
saves = FILES("saves")
IF UBOUND(saves) >= 0 THEN
  PRINT "You have " + STR$(UBOUND(saves) + 1) + " saved games"
ELSE
  PRINT "No saves yet"
END IF
REM Round-trip — write a save, then it shows up in FILES()
SAVETEXT "slot01.dat", SERIALIZE$(player)
allFiles = FILES()
FOR EACH f IN allFiles
  IF RIGHT$(f, 4) = ".dat" THEN
    PRINT "Save: " + f
  END IF
NEXT
REM Multiplayer: same call works solo and on a server
mySaves = FILES("saves")
FOR EACH s IN mySaves
  PRINT ME.NAME$ + " has " + s
NEXT
REM Path traversal is a fatal error, not silent empty
' FILES("../etc")   ' would halt with a runtime error

See also: FILEEXISTS, LOADTEXT, LOADJSON, SAVETEXT, SAVEJSON, OPEN, UBOUND

↑ Back to top

ISOPEN

File I/O · since 1.0.0

Returns 1 if the given file or DATA block handle is currently open, 0 otherwise. Useful for cleanup paths that may run without knowing whether OPEN succeeded, and for guarding a CLOSE against an unopened handle (which would otherwise be a silent no-op rather than an error, but the guard reads more clearly).

Syntax

result = ISOPEN(filenumber)

Parameters

  • filenumber - Handle to test (1-255)

Examples

OPEN "data.txt" FOR INPUT AS #1
IF ISOPEN(1) THEN
  PRINT "Open"
END IF
CLOSE #1
IF ISOPEN(1) = 0 THEN
  PRINT "Closed"
END IF
REM Defensive close — only runs if OPEN succeeded
IF ISOPEN(1) THEN CLOSE #1

See also: OPEN, OPEN DATA, CLOSE, EOF

↑ Back to top

JMES

File I/O · since 1.41.0

Runs a JMESPath query against an array or object (or any value) and returns the result. JMESPath is a compact query language for JSON-like data — it reaches into nested fields, slices and filters arrays, projects values, and applies built-in functions such as max, length, and sort_by. The data is matched as if it were JSON, so objects, arrays, numbers, strings, and booleans all work.

The result is converted back to a Crash value: an object becomes an Object, an array becomes an Array, numbers and strings come back as themselves, and a no-match (JSON null) returns 0 — the same convention as DESERIALIZE$. An invalid query halts with an error.

See https://jmespath.org for the full query syntax.

Syntax

JMES(data, "query")

Parameters

  • data - An array, object, or any value to query (commonly the result of DESERIALIZE$ or LOADJSON)
  • "query" - A JMESPath query string

Examples

data = DESERIALIZE$(json$)
PRINT JMES(data, "players[0].name")
REM Highest score across all players
best = JMES(roster, "max(players[*].score)")
REM Filter, then take the first match
winner$ = JMES(roster, "players[?score > `100`].name | [0]")
REM Reach a nested field
title$ = JMES(config, "window.title")

See also: DESERIALIZE$, SERIALIZE$, LOADJSON

↑ Back to top

LINE INPUT

File I/O · since 1.0.0

Reads one full line from a file handle (opened with OPEN ... FOR INPUT) into a string variable. Unlike READ, LINE INPUT does not auto-parse — the entire line is stored verbatim, including embedded commas, quotes, and leading/trailing whitespace. When the file is exhausted, the variable is set to an empty string; check EOF before reading to distinguish a true empty line from end-of-file.

Syntax

LINE INPUT #filenumber, var$

Parameters

  • filenumber - File handle from OPEN ... FOR INPUT
  • var$ - String variable to receive the line

Examples

OPEN "dialog.txt" FOR INPUT AS #1
DIM line$ AS STRING
WHILE NOT EOF(1)
  LINE INPUT #1, line$
  PRINT line$
WEND
CLOSE #1
REM Skip a CSV header row, then process the rest verbatim
OPEN "data.csv" FOR INPUT AS #1
DIM header$ AS STRING, row$ AS STRING
LINE INPUT #1, header$
WHILE NOT EOF(1)
  LINE INPUT #1, row$
  PRINT row$
WEND
CLOSE #1

See also: OPEN, READ, EOF, CLOSE, INPUT

↑ Back to top

LOADJSON

File I/O · since 0.23.0

Loads and parses JSON data from files in the project bundle. Returns the parsed data as CrashBasic objects and arrays. Excellent for loading structured game data, level definitions, sprite configurations, and complex settings.

Syntax

result = LOADJSON(filename$)

Parameters

  • filename$ - Name of JSON file in project bundle (relative to project root)

Examples

DIM levelData AS Object
levelData = LOADJSON("levels/level1.json")
PRINT "Level: " + levelData.name
PRINT "Size: " + STR$(levelData.width) + "x" + STR$(levelData.height)
DIM sprites AS Object
sprites = LOADJSON("sprites/characters.json")
PRINT "Player sprite: " + sprites.player.filename
PRINT "Frames: " + STR$(sprites.player.frames)
REM Load game configuration
DIM config AS Object
config = LOADJSON("config.json")
SCREEN config.screen.width, config.screen.height
IF config.audio.enabled THEN CALL enableAudio()
REM Load enemy data
DIM enemies AS Array
enemies = LOADJSON("data/enemies.json")
FOR i = 0 TO UBOUND(enemies)
  PRINT enemies(i).name + " HP: " + STR$(enemies(i).health)
NEXT
REM Load dialog system
DIM dialogs AS Object
dialogs = LOADJSON("dialog/game.json")
PRINT dialogs.intro.character + ": " + dialogs.intro.text

See also: SAVEJSON, LOADTEXT, DESERIALIZE$

↑ Back to top

LOADTEXT

File I/O · since 0.23.0

Loads text content from files in the project bundle. Returns the complete file content as a string. Ideal for loading game data, configuration files, level definitions, dialog text, and any text-based resources.

Syntax

result$ = LOADTEXT(filename$)

Parameters

  • filename$ - Name of text file in project bundle (relative to project root)

Examples

levelData$ = LOADTEXT("levels/level1.txt")
PRINT "Level 1 loaded: " + LEN(levelData$) + " characters"
dialog$ = LOADTEXT("dialog/intro.txt")
PRINT dialog$
REM Load game configuration
configText$ = LOADTEXT("config.txt")
lines = SPLIT(configText$, CHR$(10))
FOR i = 0 TO UBOUND(lines)
  PRINT "Setting: " + lines(i)
NEXT
REM Load serialized game state
savedGame$ = LOADTEXT("autosave.dat")
IF LEN(savedGame$) > 0 THEN
  gameState = DESERIALIZE$(savedGame$)
  PRINT "Game restored from autosave"
END IF
REM Load high score names
scoreNames$ = LOADTEXT("scores.txt")
names = SPLIT(scoreNames$, ",")
FOR i = 0 TO UBOUND(names)
  PRINT (i + 1) + ". " + names(i)
NEXT

See also: SAVETEXT, LOADJSON, DESERIALIZE$

↑ Back to top

LOC

File I/O · since 1.0.0

Returns the current read/write position of an open handle. For a DATA block handle, this is the 0-based index of the next value to be read. For a file-stream handle (OPEN ... FOR INPUT / OUTPUT / APPEND), this is the 0-based line number — how many lines have been read or written so far. Returns -1 if the handle is closed or invalid.

Syntax

result = LOC(filenumber)

Parameters

  • filenumber - Handle from OPEN or OPEN DATA

Examples

OPEN "log.txt" FOR INPUT AS #1
DIM line$ AS STRING
LINE INPUT #1, line$
PRINT "After 1 line, LOC = " + STR$(LOC(1))  ' 1
CLOSE #1
DATA "nums"
  10, 20, 30
END DATA
OPEN DATA "nums" AS #1
DIM n AS INTEGER
READ #1, n
PRINT "After 1 READ, LOC = " + STR$(LOC(1))  ' 1
CLOSE #1

See also: LOF, EOF, ISOPEN, OPEN, OPEN DATA

↑ Back to top

LOF

File I/O · since 1.0.0

Returns the total size of the resource backing an open handle. For a DATA block handle, this is the total number of values in the block. For a file-stream handle, this is the total number of lines in the buffered file. Returns -1 if the handle is closed or invalid.

Combine with LOC to compute remaining content, or to drive a progress indicator over a long input file.

Syntax

result = LOF(filenumber)

Parameters

  • filenumber - Handle from OPEN or OPEN DATA

Examples

OPEN "level.txt" FOR INPUT AS #1
PRINT "File has " + STR$(LOF(1)) + " lines"
CLOSE #1
REM Show a progress indicator while parsing
OPEN "big.csv" FOR INPUT AS #1
DIM total AS INTEGER, row$ AS STRING
total = LOF(1)
WHILE NOT EOF(1)
  LINE INPUT #1, row$
  PRINT "Progress: " + STR$(INT(LOC(1) * 100 / total)) + "%"
WEND
CLOSE #1

See also: LOC, EOF, OPEN, OPEN DATA

↑ Back to top

OPEN

File I/O · since 1.0.0

Opens a text file for sequential line-by-line access. FOR INPUT reads from an existing file; FOR OUTPUT creates (or truncates) a file for writing; FOR APPEND opens a file for writing while preserving existing contents. Pair with LINE INPUT or READ to read, PRINT # to write, and CLOSE when done.

Files are buffered: FOR INPUT loads the whole file into memory at OPEN time, so subsequent reads do not hit the bundle/VFS. FOR OUTPUT and FOR APPEND accumulate writes in memory and flush to the bundle/VFS when CLOSE runs — drop the handle without CLOSE and writes are lost. EOF, LOC, LOF, and ISOPEN all work on file handles.

In multiplayer, FOR OUTPUT and FOR APPEND are server-side only — calling them from the local-client interpreter halts with a runtime error pointing at the EVENT HOST "Save" pattern (same policy as SAVETEXT/SAVEJSON). FOR INPUT is allowed on every interpreter because clients legitimately read level data, dialog, and configs from the cart.

This is the file-stream form. For named in-program data tables, use OPEN DATA instead.

One handle, one thing. Files, DATA blocks and databases share the #n space so that CLOSE #n is unambiguous. Opening onto a number something else already holds is a runtime error naming what holds it — in either direction, for either kind. CLOSE frees the number for any of them again.

Syntax

OPEN filename$ FOR INPUT|OUTPUT|APPEND AS #filenumber

Parameters

  • filename$ - Path to the file (any string expression, resolved against the project bundle / local storage)
  • FOR INPUT - Open for reading (file must exist)
  • FOR OUTPUT - Open for writing (truncates any existing file)
  • FOR APPEND - Open for writing, preserving existing contents
  • filenumber - Numeric handle (1-255) used by subsequent READ / LINE INPUT / PRINT # / CLOSE

Examples

REM Read a configuration file line by line
OPEN "config.txt" FOR INPUT AS #1
DIM line$ AS STRING
WHILE NOT EOF(1)
  LINE INPUT #1, line$
  PRINT line$
WEND
CLOSE #1
REM Write a high score table
OPEN "scores.txt" FOR OUTPUT AS #1
FOR i = 0 TO 9
  PRINT #1, names$(i) + "," + STR$(scores(i))
NEXT
CLOSE #1
REM Append a timestamped audit line
OPEN "events.log" FOR APPEND AS #1
PRINT #1, TIME$ + " player joined"
CLOSE #1
REM Multiplayer — only the host writes (clients halt on FOR OUTPUT)
IF ME.ISHOST THEN
  OPEN "world.dat" FOR OUTPUT AS #1
  PRINT #1, SERIALIZE$(world)
  CLOSE #1
END IF

See also: OPEN DATA, CLOSE, LINE INPUT, PRINT #, READ, EOF, LOC, LOF, ISOPEN, FILEEXISTS, SAVETEXT

↑ Back to top

OPEN DATABASE

File I/O · since 1.56.0

Opens a SQLite database on a file handle. A read resolves like any other read — a database bundled in the cart is found the same way an image is. FOR OUTPUT opens it for writing in whatever location the platform keeps save data, creating the database if it does not exist. Databases share the #n handle space with files and DATA blocks, so CLOSE covers all three and one handle cannot be two things at once — enforced on every OPEN, in both directions.

The database is held in memory while open and written back when the handle closes — the same point a buffered OPEN ... FOR OUTPUT file is flushed. A program that ends without CLOSE loses its changes, exactly as it would with a file.

Syntax

OPEN DATABASE path$ [FOR INPUT|OUTPUT] AS #n

Parameters

  • path$ - Database file. A read resolves through the cart; FOR OUTPUT resolves to the platform's save location.
  • FOR INPUT - Read-only, and the default when the clause is omitted. The file is never written back.
  • FOR OUTPUT - Writable. Created if missing, flushed on CLOSE.
  • #n - Handle number, shared with the file layer.

Example

' A database shipped inside the cart
OPEN DATABASE "content.db" AS #1

' The player's own save, in the platform's save location
OPEN DATABASE "player.db" FOR OUTPUT AS #2

' ... queries ...

CLOSE #1, #2

Notes

  • A missing database is an error for a read and a new empty database for a write — the same asymmetry OPEN ... FOR INPUT and FOR OUTPUT already have.
  • Read-only means read-only. A database opened without FOR OUTPUT is never written back, so a cart cannot be modified by the program reading it.
  • Multiplayer: opening FOR OUTPUT on a client is refused like any other client-side file write — the server owns the bytes.
  • Every platform. Desktop, iOS, tvOS, Android and the web player run the same SQLite through one implementation (rusqlite::ffi natively, sqlite-wasm-rs on wasm32 — the same C library compiled for each target). The file format is identical, so a database moves between them unchanged.

See also: SQL, CALL SQL, OPEN, CLOSE

↑ Back to top

PRINT

File I/O · since 1.0.0

Writes one line of text to a file handle (opened with OPEN ... FOR OUTPUT or FOR APPEND). Each expression is stringified and joined with single spaces; a newline is appended automatically. Output is buffered in memory until CLOSE flushes the file to the bundle/VFS.

Unlike console PRINT, the file form has no WORDS or RATE modifiers — this is plain text output, not a typewriter effect.

In multiplayer, the underlying handle is server-side only. The policy fires at OPEN time (see OPEN), so a PRINT # statement that the interpreter actually reaches always has a valid server-side handle behind it. For client-visible text output, use console PRINT inside a GUARD(CLIENT) handler.

Syntax

PRINT #filenumber, expression [, expression ...]

Parameters

  • filenumber - File handle from OPEN ... FOR OUTPUT or FOR APPEND
  • expression - One or more values; stringified and space-joined into a single line

Examples

OPEN "scores.txt" FOR OUTPUT AS #1
PRINT #1, "alice", 4200
PRINT #1, "bob", 3800
CLOSE #1
REM Append a timestamped log line
OPEN "events.log" FOR APPEND AS #1
PRINT #1, TIME$ + " " + event$
CLOSE #1
REM Round-trip — write then read
OPEN "player.txt" FOR OUTPUT AS #1
PRINT #1, playerName$
PRINT #1, STR$(playerLevel)
CLOSE #1

OPEN "player.txt" FOR INPUT AS #2
DIM name$ AS STRING, lvl$ AS STRING
LINE INPUT #2, name$
LINE INPUT #2, lvl$
CLOSE #2

See also: OPEN, PRINT, CLOSE, SAVETEXT

↑ Back to top

SAVEJSON

File I/O · since 0.23.0

Saves data as JSON format to local storage. Automatically converts CrashBasic objects, arrays, and basic data types to properly formatted JSON. Ideal for saving structured game data, settings, and player information in a readable format.

In multiplayer, SAVEJSON is server-side only — same policy as SAVETEXT. A client-side call halts with a runtime error pointing at EVENT HOST "Save" as the right pattern. The server is the authoritative writer.

Syntax

SAVEJSON filename$, data

Parameters

  • filename$ - Name for the saved JSON file (will be stored in local storage)
  • data - Any data structure (Object, Array, String, Number, Boolean, TYPE structure)

Examples

DIM player AS Object
player.name = "Hero"
player.level = 10
player.inventory = ["sword", "potion", "key"]
SAVEJSON "player.json", player
DIM gameSettings AS Object
gameSettings.difficulty = "hard"
gameSettings.graphics = {"quality": "high", "fullscreen": true}
gameSettings.audio = {"music": 0.8, "effects": 1.0}
SAVEJSON "settings.json", gameSettings
REM Save high scores with player data
DIM scores AS Array
FOR i = 0 TO 4
  DIM entry AS Object
  entry.name = playerNames(i)
  entry.score = highScores(i)
  entry.date = DATE$
  scores(i) = entry
NEXT
SAVEJSON "highscores.json", scores
REM Save level progress
TYPE LevelProgress
  completed AS Boolean
  bestTime AS Number
  starsEarned AS Number
END TYPE
DIM progress(20) AS LevelProgress
progress(0).completed = true
progress(0).bestTime = 45.2
progress(0).starsEarned = 3
SAVEJSON "progress.json", progress
REM Save custom level data
DIM levelEditor AS Object
levelEditor.tiles = tileArray
levelEditor.enemies = enemyPositions
levelEditor.metadata = {"author": "Player", "version": 1}
SAVEJSON "custom_level.json", levelEditor

See also: LOADJSON, SAVETEXT, SERIALIZE$

↑ Back to top

SAVETEXT

File I/O · since 0.23.0

Saves text content to local storage with the specified filename. Data persists between game sessions and can be loaded with LOADTEXT. Perfect for saving player progress, game settings, high scores, and user-generated content.

In multiplayer, SAVETEXT is server-side only. The host or a player-server is the authoritative writer; a call from the local-client interpreter halts with a runtime error pointing at EVENT HOST "Save" as the right pattern. The server writes once; clients read replicated state. Single-player allows it unconditionally (one interpreter, one filesystem).

Syntax

SAVETEXT filename$, content$

Parameters

  • filename$ - Name for the saved file (will be stored in local storage)
  • content$ - Text content to save

Examples

playerName$ = "Hero"
SAVETEXT "player.txt", playerName$
REM Save high scores
scoreList$ = "1000,800,650,500,300"
SAVETEXT "highscores.txt", scoreList$
REM Save game state
DIM gameState AS Object
gameState.level = 5
gameState.score = 2500
saveData$ = SERIALIZE$(gameState)
SAVETEXT "savegame.dat", saveData$
REM Save player preferences
DIM settings AS Object
settings.difficulty = "normal"
settings.music = true
settings.controls = {"up": "W", "down": "S"}
settingsText$ = SERIALIZE$(settings)
SAVETEXT "settings.dat", settingsText$
REM Autosave system
DECLARE SUB AutoSave()
CALL TIMER AutoSave() EVERY 30000 MILLISECONDS AS "autosave"

SUB AutoSave()
  currentState$ = SERIALIZE$(gameState)
  SAVETEXT "autosave.dat", currentState$
END SUB

See also: LOADTEXT, SAVEJSON, SERIALIZE$

↑ Back to top

SERIALIZE$

File I/O · since 0.23.0

Converts objects, arrays, or any data structure to a JSON string for storage or transmission. Essential for saving game state, player progress, settings, and complex data structures to persistent storage. The output is standard JSON, so it can be used with external tools or APIs that accept JSON.

Multi-dimensional arrays serialize as nested JSON arrays: a 2D array becomes [[1,2],[3,4]], a 3D array nests one level deeper, and so on. A matrix stays a matrix for tools that ingest the output. Objects and arrays that contain multi-dimensional arrays serialize their contents in full.

Syntax

SERIALIZE$(data)

Parameters

  • data - Any data type (Object, Array, String, Number, Boolean, TYPE structure)

Examples

DIM player AS Object
player.name = "Hero"
player.level = 5
player.score = 1000
saveData$ = SERIALIZE$(player)
DIM highScores(10) AS Number
FOR i = 0 TO 9
  highScores(i) = (10 - i) * 1000
NEXT
scoresData$ = SERIALIZE$(highScores)
TYPE GameState
  level AS Number
  lives AS Number
  powerups AS Object
END TYPE
DIM state AS GameState
state.level = 3
state.lives = 5
savedGame$ = SERIALIZE$(state)
REM Save complex game configuration
DIM config AS Object
config.graphics = "high"
config.sound = true
config.controls = {"jump": "SPACE", "fire": "CTRL"}
configData$ = SERIALIZE$(config)
SAVETEXT "config.json", configData$

See also: DESERIALIZE$, SAVETEXT, SAVEJSON

↑ Back to top

SQL

File I/O · since 1.56.0

Declares a named, parameterized SQL statement. Nothing runs until CALL SQL — the block registers the statement the way BEHAVIOR registers a behavior, and the name is case-insensitive.

The body is real SQL, kept verbatim. SQLite's named-parameter syntax is @name, the same sigil a behavior parameter uses, so the two notations coincide and nothing has to be rewritten or escaped. Every parameter binds as an actual SQLite parameter, which means a value can never turn into syntax — SQL injection is not filtered here, it is impossible.

One statement per block. A block is named, called, bound with one set of parameters and reported as one outcome, so it holds exactly one SQL statement. A second statement is a runtime error naming it — before 1.57.0 it was compiled away and silently never ran, which is how a table you thought you had created turned up missing. A trailing semicolon or comment is formatting and is fine.

Syntax

SQL "name"
  [USE NUMBER paramName [DEFAULT value]]
  [USE STRING paramName$ [DEFAULT "value"]]
  [NULLS column = value [, column = value ...]]
  <statement text>
END SQL

Parameters

  • name - String name for the block, used by CALL SQL.
  • USE NUMBER paramName [DEFAULT value] - Declares a numeric parameter. Without a DEFAULT the call site must supply it.
  • USE STRING paramName$ [DEFAULT "value"] - Declares a string parameter.
  • NULLS column = value - Declares what an absent value means for that column. Only columns named here may come back NULL.

Examples

SQL "topScores"
  USE NUMBER minScore DEFAULT 0
  USE NUMBER limit DEFAULT 10
  SELECT name, score FROM runs
   WHERE score >= @minScore
   ORDER BY score DESC LIMIT @limit
END SQL
' Declaring what an absent value means
SQL "allRuns"
  NULLS score = 0, name = "(unknown)"
  SELECT name, score FROM runs
END SQL
' Keeping absence and zero apart, in SQL
SQL "runsWithGaps"
  SELECT name,
         IFNULL(score, 0) AS score,
         score IS NULL     AS scoreMissing
    FROM runs
END SQL

Notes

  • NULL is never converted silently. Crash BASIC has no NULL, and mapping one to 0 would make it indistinguishable from a real zero ever after. A column that comes back NULL with no NULLS mapping halts with a message naming the block, the column and the row.
  • Telling absence from zero is done in SQL, which already has the vocabulary: SELECT IFNULL(score, 0) AS score, score IS NULL AS scoreMissing gives you both, and IS NULL arrives as an ordinary 1 or 0.
  • A parameter the statement never mentions is reported rather than ignored, as is an argument the block never declared — both are usually a typo.
  • Only scalars bind. A number or a string; an array, object, coordinate or mutation has no SQL equivalent and is refused by name.

See also: CALL SQL, OPEN DATABASE, BEHAVIOR, SHADER

↑ Back to top

Resources

BASE64TOIMG

Resources · since 1.30.0

Function-form analog of LOADIMG that takes a base64-encoded PNG payload instead of a file path. Designed to compose with FETCH — load an image over the network, then register it as a named texture in one more line. Accepts either raw base64 or a full data:image/png;base64,... URI; the data URI prefix is stripped automatically. Same dim-parsing and sprite-sheet semantics as LOADIMG. Errors halt with a clear runtime error — gate the call on FETCH's STATUSCODE if the source might fail.

Like all image handles, the image_name is case-insensitive — BASE64TOIMG(data$, "Player") registers it under "PLAYER" and PUTIMAGE looks it up the same way.

It is a function, not a statement. It returns 1 when the image was decoded and registered and 0 when it was not, so call it in an expression and check the result — ok = BASE64TOIMG(data$, "name"). A bare BASE64TOIMG(...) on its own line is a syntax error.

Syntax

ok = BASE64TOIMG(base64$, image_name)
ok = BASE64TOIMG(base64$, image_name, sheet_w, sheet_h)

Parameters

  • base64$ - Base64-encoded PNG payload, or a data:image/png;base64,... URI
  • image_name - Reference name for later use with PUTIMAGE (case-insensitive)
  • sheet_w, sheet_h - Optional frame dimensions to load as a sprite sheet (matches LOADIMG ... AS SPRITESHEET(w, h))

Examples

REM Pair with FETCH to load a remote image
r = FETCH("https://cdn.example.com/player.png", "GET")
IF r.STATUSCODE = 200 THEN
  ok = BASE64TOIMG(r.MESSAGE, "player")
  PUTIMAGE (100, 100), "player"
END IF
REM Sprite sheet variant — 16x16 frames
r = FETCH("https://cdn.example.com/tiles.png", "GET")
ok = BASE64TOIMG(r.MESSAGE, "tiles", 16, 16)
PUTIMAGE (0, 0), "tiles", 0
REM Accept a full data URI (e.g. embedded asset)
ok = BASE64TOIMG("data:image/png;base64,iVBOR...", "icon")

See also: LOADIMG, PUTIMAGE, FETCH, BASE64TOWAV, BASE64TOMP3

↑ Back to top

BASE64TOMP3

Resources · since 1.30.0

Function-form analog of LOADMP3 that takes a base64-encoded MP3 payload instead of a file path. Designed to compose with FETCH. Accepts either raw base64 or a full data:audio/mpeg;base64,... URI; the data URI prefix is stripped automatically. Client-only — calling from a server-side multiplayer interpreter is a fatal runtime error, matching LOADMP3's gating. The audio_name is case-insensitive — BASE64TOMP3(data$, "Theme") and PLAYMP3 "theme" agree.

It is a function, not a statement. It returns 1 when the MP3 was decoded and registered and 0 when it was not, so call it in an expression and check the result — ok = BASE64TOMP3(data$, "name"). A bare BASE64TOMP3(...) on its own line is a syntax error.

Syntax

ok = BASE64TOMP3(base64$, audio_name)

Parameters

  • base64$ - Base64-encoded MP3 payload, or a data:audio/mpeg;base64,... URI
  • audio_name - Reference name for later playback (case-insensitive)

Example

REM Pair with FETCH
r = FETCH("https://cdn.example.com/theme.mp3", "GET")
IF r.STATUSCODE = 200 THEN
  ok = BASE64TOMP3(r.MESSAGE, "theme")
  PLAYMP3 "theme" LOOP
END IF

See also: LOADMP3, PLAYMP3, FETCH, BASE64TOWAV, BASE64TOIMG

↑ Back to top

BASE64TOWAV

Resources · since 1.30.0

Function-form analog of LOADWAV that takes a base64-encoded WAV payload instead of a file path. Designed to compose with FETCH. Accepts either raw base64 or a full data:audio/wav;base64,... URI; the data URI prefix is stripped automatically. Client-only — calling from a server-side multiplayer interpreter is a fatal runtime error, matching LOADWAV's gating. The audio_name is case-insensitive — BASE64TOWAV(data$, "Boom") and PLAYWAV "boom" agree.

It is a function, not a statement. It returns 1 when the WAV was decoded and registered and 0 when it was not, so call it in an expression and check the result — ok = BASE64TOWAV(data$, "name"). A bare BASE64TOWAV(...) on its own line is a syntax error.

Syntax

ok = BASE64TOWAV(base64$, audio_name)

Parameters

  • base64$ - Base64-encoded WAV payload, or a data:audio/wav;base64,... URI
  • audio_name - Reference name for later playback (case-insensitive)

Example

REM Pair with FETCH
r = FETCH("https://cdn.example.com/explosion.wav", "GET")
IF r.STATUSCODE = 200 THEN
  ok = BASE64TOWAV(r.MESSAGE, "boom")
  PLAYWAV "boom"
END IF

See also: LOADWAV, PLAYWAV, FETCH, BASE64TOMP3, BASE64TOIMG

↑ Back to top

FADEMP3

Resources · 📱 Client-only · since 0.24.0

Fades an MP3 in or out over the specified duration. FADEMP3 IN starts the track (if not already playing) and ramps volume from 0 to master volume. FADEMP3 OUT ramps volume from the current volume to 0 and stops playback when complete. In multiplayer FADEMP3 is client-only — wrap in IF ME.ISCLIENT THEN ... END IF or trigger from the server with EVENT CLIENT "name".

Syntax

FADEMP3 IN|OUT audio_name DURATION seconds

Parameters

  • IN|OUT - Fade direction (IN = fade in, OUT = fade out)
  • audio_name - Reference name of a loaded MP3
  • seconds - Duration of the fade in seconds

Examples

FADEMP3 IN "bgmusic" DURATION 3
FADEMP3 OUT "bgmusic" DURATION 5
' Start background music with fade-in
FADEMP3 IN "bgm" DURATION 2
' Fade out background music over 30 seconds
FADEMP3 OUT "bgm" DURATION 30

See also: PLAYMP3, STOPMP3, LOADMP3

↑ Back to top

FREE

Resources · since 1.4.3

Frees a resource from memory. For audio resources (MP3/WAV), stops playback and unloads the sound data. For images, removes the texture from GPU memory and disables any sprites using that image. For layers, destroys the layer and disables sprites assigned to it. Layer 0 (default screen layer) cannot be freed.

Syntax

FREE MP3 name | FREE WAV name | FREE IMAGE name | FREE LAYER id

Parameters

  • MP3/WAV/IMAGE name - String expression identifying the resource to free
  • LAYER id - Numeric expression for the layer ID to free (1 or higher)

Examples

FREE MP3 "bgmusic"           ' Stop and unload MP3
FREE WAV "explosion"         ' Stop and unload WAV
FREE IMAGE "player_sprite"   ' Free image and disable sprites using it
FREE IMAGE imgName$           ' Free using variable name
FREE LAYER 2                  ' Free layer and disable sprites on it

See also: LOADWAV, LOADMP3, LOADIMG, CREATE LAYER, DISABLE SPRITE

↑ Back to top

GETIMAGE

Resources · since 0.23.0

Captures a rectangular area of the screen as an image resource that can be used with PUTIMAGE. Useful for creating dynamic sprites, saving screen regions, or implementing effects like trails.

Syntax

GETIMAGE (x1, y1)-(x2, y2), image_name

Parameters

  • x1, y1 - Top-left corner of capture area
  • x2, y2 - Bottom-right corner of capture area
  • image_name - Name to assign to captured image
  • Captured image includes all graphics in the specified region
  • Can capture across multiple layers if using layer system

Examples

GETIMAGE (0, 0)-(319, 199), "screenshot"          ' Capture entire screen
GETIMAGE (playerX-16, playerY-16)-(playerX+16, playerY+16), "player_backup"  ' Capture around player
GETIMAGE (100, 100)-(200, 150), "ui_section"      ' Capture UI area
' Create trail effect:
GETIMAGE (shipX-5, shipY-5)-(shipX+5, shipY+5), "trail"
PUTIMAGE (shipX-10, shipY), "trail", ALPHA(0.5)
' Save and restore screen area:
GETIMAGE (x, y)-(x+width, y+height), "backup"
' Draw temporary graphics...
PUTIMAGE (x, y), "backup"  ' Restore original

See also: PUTIMAGE, LOADIMG, Screen Capture

↑ Back to top

LOADIMG

Resources · since 0.23.0

Loads PNG, JPG, GIF, or Aseprite (.ase/.aseprite) images from bundle resources for use with PUTIMAGE. Supports transparency and can load images as sprite sheets for animation. Files must be uploaded to the project bundle via the Resource Manager. LOADPNG is a legacy alias for this command.

Both the filename and image_name are case-insensitive on native hosts. LOADIMG "HELLOWORLD.PNG", "player" finds HelloWorld.png on disk (the LocalBus does an exact-match pass then falls back to a case-insensitive component walk inside the sandbox). The image_name is case-insensitive at lookup time. WASM hosts SHOULD implement the same fallback in their VFS handler — see the IoBus trait docs in io.rs.

Aseprite files. A .ase/.aseprite file is flattened into a horizontal sprite sheet (one frame per timeline frame), so it loads exactly like a sprite sheet — the per-frame size is the canvas size unless you override it with AS SPRITESHEET(w, h). Aseprite files also carry two kinds of animation metadata that other formats can't, and LOADIMG reads both: the per-frame display durations authored on the timeline (used by ANIMATE ... FRAMERATE OFSPRITE) and the per-frame slice geometry (used by ANIMATE ... AUTOBBOX and by the SLICES clause).

SLICES arr() (Aseprite only) fills the array variable arr with one relative-0–1 bounding-box Range per frame, expanded from the file's first slice's keyframes (a slice key stays in effect until the next key, matching Aseprite). arr(frame) is (x1,y1)-(x2,y2) in 0–1 coordinates, ready to drop into SET SPRITE id BBOX arr(frame). Frames no slice covers fall back to the full frame (0,0)-(1,1), so the array is always dense. Using SLICES on a non-Aseprite file, or on an .ase with no slices, is a loud runtime error.

TAG("name") (Aseprite only) loads ONLY that animation tag's frames into the sheet, re-indexed from 0 — so ANIMATE image_name, 0 TO N plays exactly that tag, and the FRAMERATE OFSPRITE durations and AUTOBBOX/SLICES geometry are narrowed to the tag's frames to match. A .ase already implies a sprite sheet (each timeline frame is a cell), so no AS SPRITESHEET clause is needed. Tag names match case-insensitively; an unknown tag, or TAG on a non-Aseprite file, is a loud runtime error that lists the available tags. Want several tags from one file? Load it once per tag under different names.

LAYERS(...) (Aseprite only) composites ONLY the named or indexed source layers into the loaded image, dropping the rest. Each selector is a layer name (string, case-insensitive) or a 0-based layer index (number, where 0 is the bottom of the stack) — mix them freely: LAYERS("Turret", 0). Blend modes, opacity, and per-cel z-order are honored exactly as Aseprite renders them, and an explicitly-selected layer composites even if its eye icon is toggled off in the file. This is how you split one layered .ase into separate composited sprites: load it once per component under different names — LAYERS("Base", "Treads") for the hull, LAYERS("Turret") for the turret. Combines with TAG (that tag's frames, only these layers). An unknown layer name or an out-of-range index is a loud runtime error that lists the available layer names.

In CrashNet multiplayer, LOADIMG runs on every interpreter — including the host and player-servers — but the server-side execution is dimension-only. The image bytes are parsed (PNG/ASE headers give real width/height) and the dimensions register on each interpreter's BehaviorEngine so collision and pattern math work correctly. The Aseprite per-frame durations and slice bboxes register on every interpreter too, so OFSPRITE timing and AUTOBBOX hitboxes resolve identically on server and client. The actual texture data sent to the renderer is dropped by the headless host's GfxClient (it only forwards sprite commands), so there's no double-render or wire-bandwidth cost. Practically: just call LOADIMG once at top-level (no IF ME.ISCLIENT gate needed) and every interpreter will know the dims.

Syntax

LOADIMG filename, image_name [AS SPRITESHEET(width, height)] [TAG("name")] [SLICES arr()] [LAYERS(layer, ...)]

Parameters

  • filename - Image filename in project bundle (supports spaces in quotes)
  • image_name - Reference name for later use with PUTIMAGE
  • AS SPRITESHEET(width, height) - Optional: Load as sprite sheet with frame dimensions
  • TAG("name") - Optional (Aseprite only): load only the named animation tag's frames into the sheet, re-indexed from 0
  • SLICES arr() - Optional (Aseprite only): fill arr with one relative-0–1 bounding-box Range per frame, from the file's first slice
  • LAYERS(layer, ...) - Optional (Aseprite only): composite only these source layers (each a name string or a 0-based index) into the image; call again with different layers to split a layered file into separate sprites
  • Supported formats: PNG (with transparency), JPG, GIF, Aseprite (.ase/.aseprite)
  • Local files: Upload via Resource Manager, then reference by filename
  • Frame numbering: Sprite sheet frames start at 0 (top-left), increment left-to-right, top-to-bottom

Examples

LOADIMG "player.png", "player"                    ' Load single image
LOADIMG "Sprite Sheet.png", "sprites" AS SPRITESHEET(32, 32)  ' Load as 32x32 sprite sheet
LOADIMG "background.jpg", "bg"                    ' Load JPEG background
LOADIMG "icons.png", "ui" AS SPRITESHEET(16, 16)  ' Load UI icons
LOADIMG "animated character.gif", "hero"          ' GIF with spaces in filename
' Sprite sheet example with 8x8 frames:
' If image is 64x32, creates 8 frames (64/8=8 wide, 32/8=4 tall)
LOADIMG "tiles.png", "tiles" AS SPRITESHEET(8, 8)
' Aseprite: load with per-frame slice hitboxes into boxes()
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32) SLICES boxes()
SET SPRITE 0, 100, 100, "HERO"
SET SPRITE 0 BBOX boxes(0)            ' frame 0's slice as the bbox
' Aseprite: load just the "run" animation tag's frames
LOADIMG "hero.ase", "HERO_RUN" TAG("run")
BEHAVIOR "running"
  ANIMATE "HERO_RUN", 0 TO 5 FRAMERATE OFSPRITE
END BEHAVIOR
' Aseprite: split a layered tank into separate composited sprites
LOADIMG "tank.ase", "TANK_BASE"   LAYERS("Base", "Treads")
LOADIMG "tank.ase", "TANK_TURRET" LAYERS("Turret")
' now the turret can rotate independently of the hull
' Note: Upload files via Resources button first
' Then reference by exact filename in LOADIMG

See also: PUTIMAGE, FRAMECOUNT, BEHAVIOR, SET SPRITE, Resource Manager

↑ Back to top

LOADMP3

Resources · since 0.23.0

Loads MP3/WAV audio files from local resources or URLs for playback. Supports filenames with spaces.

Audio handles are case-insensitive — LOADMP3 "theme.mp3", "Theme" and PLAYMP3 "theme" agree. File paths are also case-insensitive on native hosts, so LOADMP3 "HELLOWORLD.MP3", "x" finds HelloWorld.mp3 on disk via the sandbox's component walk on miss.

Re-loading the same file onto a handle you already loaded it into is a no-op, so a LOADMP3 sitting in a game loop costs nothing after the first call. Loading a different file onto that handle swaps the clip — the old audio is freed and the handle plays the new file from then on. To drop a clip without replacing it, use FREE MP3 name.

In CrashNet multiplayer, resource loads are client-only — calling LOADMP3 from server-side code (host or player-server interpreter) is a fatal runtime error. Each client loads its own copies inside an IF ME.ISCLIENT THEN block; PLAYMP3 issued from the server (or host's broadcast) plays back on all clients using the resource each client already loaded.

Syntax

LOADMP3 filename_or_url, audio_name

Parameters

  • filename_or_url - Local filename or full URL to audio file
  • audio_name - Reference name for later playback
  • Supported formats: MP3, WAV, OGG
  • Local files: Upload via Resource Manager, then reference by filename
  • URLs: Full HTTP/HTTPS URLs for remote files

Examples

LOADMP3 "music.mp3", "bgmusic"        ' From local resources
LOADMP3 "Background Music.mp3", "bg"  ' Filename with spaces
LOADMP3 "coin pickup.wav", "coin"     ' WAV file with spaces
LOADMP3 "explosion.mp3", "boom"       ' From local resources
LOADMP3 "https://example.com/song.mp3", "remote"  ' From URL
' Note: Upload files via Resources button first
' Then reference by exact filename in LOADMP3

See also: PLAYMP3, STOPMP3, LOADWAV

↑ Back to top

LOADPNG

Resources · since 0.23.0

Legacy alias for LOADIMG. Loads PNG, JPG, or GIF images from bundle resources for use with PUTIMAGE.

In CrashNet multiplayer, resource loads are client-only — calling LOADPNG from server-side code is a fatal runtime error. See LOADIMG for the full pattern.

Syntax

LOADPNG filename, image_name [AS SPRITESHEET(width, height)]

Parameters

  • filename - Image filename in project bundle
  • image_name - Reference name for later use with PUTIMAGE
  • AS SPRITESHEET(width, height) - Optional: Load as sprite sheet with frame dimensions

Examples

LOADPNG "player.png", "player"  ' Works, but prefer LOADIMG
LOADIMG "player.png", "player"  ' Preferred

See also: LOADIMG, PUTIMAGE, FRAMECOUNT

↑ Back to top

LOADWAV

Resources · since 0.23.0

Loads WAV audio files from local resources or URLs for playback. Supports filenames with spaces.

Audio handles are case-insensitive — LOADWAV "shoot.wav", "Shoot" and PLAYWAV "shoot" agree. File paths are also case-insensitive on native hosts, so LOADWAV "HELLOWORLD.WAV", "x" finds HelloWorld.Wav on disk via the sandbox's component walk on miss.

Re-loading the same file onto a handle you already loaded it into is a no-op, so a LOADWAV sitting in a game loop costs nothing after the first call. Loading a different file onto that handle swaps the clip — the old audio is freed and the handle plays the new file from then on. To drop a clip without replacing it, use FREE WAV name.

In CrashNet multiplayer, resource loads are client-only — calling LOADWAV from server-side code (host or player-server interpreter) is a fatal runtime error. Each client loads its own copies inside an IF ME.ISCLIENT THEN block.

Syntax

LOADWAV filename_or_url, audio_name

Parameters

  • filename_or_url - Local filename or full URL to WAV file
  • audio_name - Reference name for later playback
  • Supported format: WAV (uncompressed audio)
  • Local files: Upload via Resource Manager, then reference by filename
  • URLs: Full HTTP/HTTPS URLs for remote files

Examples

LOADWAV "sound.wav", "effect"         ' From local resources
LOADWAV "Laser Sound.wav", "laser"    ' Filename with spaces
LOADWAV "explosion.wav", "boom"       ' From local resources
LOADWAV "https://example.com/beep.wav", "beep"  ' From URL
' Note: Upload files via Resources button first
' Then reference by exact filename in LOADWAV

See also: PLAYWAV, STOPWAV, LOADMP3

↑ Back to top

PLAYMP3

Resources · 📱 Client-only · since 1.5.0

Plays loaded audio files. Supports named modifiers for clarity, and they may appear in any order (EVENTSON, if present, comes last). LOOP enables continuous playback and VOLUME(n) sets volume (0.0-1.0). SECTION(start, end) plays or loops only that region of the file, in seconds — useful for a multi-part track where you want to repeat just one part; without LOOP it plays the region once, with LOOP it repeats just that region. CROSSFADE(secs) equal-power blends the loop seam of a looped section so it repeats without a click; it uses the audio just past the section end, so it applies to a looped SECTION (a whole-file loop has no tail to blend). The optional EVENTSON clause fires timed cue events during playback; with a SECTION its timestamps are relative to the section start. The audio_name lookup is case-insensitive — PLAYMP3 "theme" finds whatever was loaded under "Theme", "THEME", etc. In multiplayer this statement is client-only — wrap the call in IF ME.ISCLIENT THEN ... END IF, or trigger it from the server with EVENT CLIENT "name" so the client's handler runs the actual PLAYMP3. Calling it from a server-side interpreter raises a fatal runtime error.

Syntax

PLAYMP3 audio_name [SECTION(start, end)] [LOOP] [CROSSFADE(secs)] [VOLUME(n)] [EVENTSON(t1, t2, ...)]

Parameters

  • audio_name - Reference name of a loaded audio file
  • SECTION(start, end) - Optional modifier that limits playback to the region from start to end seconds (needs 0 <= start < end). With LOOP, only this region repeats; without LOOP, the region plays once.
  • LOOP - Optional modifier that enables looping playback
  • CROSSFADE(secs) - Optional modifier that equal-power blends a looped section's seam over secs seconds (default 0 = hard cut). Applies to a looped SECTION; ignored otherwise.
  • VOLUME(n) - Optional modifier that sets playback volume (0.0 to 1.0, default 1.0)
  • EVENTSON(t1, t2, ...) - Optional list of timestamps in seconds that trigger events when playback reaches them. Event name = UPPERCASED audio name + '_CUE' (e.g., PLAYMP3 "theme" → event 'THEME_CUE'). Callback receives the timestamp as an argument. Cue points re-fire on each loop iteration. With a SECTION, timestamps are relative to the section start.

Examples

PLAYMP3 "bgmusic"
PLAYMP3 "bgmusic" LOOP
PLAYMP3 "bgmusic" LOOP VOLUME(0.5)
PLAYMP3 "explosion" VOLUME(0.8)
' Loop just the second section of a multi-part track
PLAYMP3 "battletheme" SECTION(8.0, 24.0) LOOP
' Loop a section with a 150ms blend so the seam is seamless
PLAYMP3 "battletheme" SECTION(8.0, 24.0) LOOP CROSSFADE(0.15) VOLUME(0.8)
' Play one region once (no loop)
PLAYMP3 "stinger" SECTION(1.5, 3.0)
PLAYMP3 "theme" LOOP VOLUME(0.8) EVENTSON(1.5, 3.0, 7.2)
' Sync game events to music cue points
PLAYMP3 "theme" LOOP VOLUME(0.8) EVENTSON(1.5, 3.0, 7.2)

SUB OnCue(timestamp)
  IF timestamp = 1.5 THEN PRINT "Beat drop!"
  IF timestamp = 3.0 THEN PRINT "Chorus!"
END SUB

CALL EVENT OnCue() AS "THEME_CUE"
' Legacy positional syntax (deprecated, still supported)
PLAYMP3 "bgmusic", 1, 0.5

See also: LOADMP3, STOPMP3, FADEMP3, PLAYWAV

↑ Back to top

PLAYWAV

Resources · 📱 Client-only · since 0.23.0

Plays loaded WAV audio files with optional stereo positioning. The audio_name lookup is case-insensitive — PLAYWAV "shoot" finds whatever was loaded under "Shoot", "SHOOT", etc. In multiplayer this statement is client-only — wrap the call in IF ME.ISCLIENT THEN ... END IF, or trigger it from the server with EVENT CLIENT "name" so the client's handler runs the actual PLAYWAV. Calling it from a server-side interpreter (host or player-server) raises a fatal runtime error.

Syntax

PLAYWAV audio_name [, loop] [, volume] [AT(x, y)]   (AT also accepts a coordinate: AT(coord))

Parameters

  • audio_name - Reference name
  • loop - 1=loop, 0=once
  • volume - 0.0 to 1.0
  • AT(x, y) - Stereo position: x from -1.0 (left) to 1.0 (right), y reserved for future 3D audio

Examples

PLAYWAV "effect"
PLAYWAV "laser", 0, 1.0
PLAYWAV "ambient", 1, 0.3
PLAYWAV "explosion" AT(-0.5, 0.0)  ' Play from left side
PLAYWAV "shot", 0, 1.0 AT(1.0, 0.0)  ' Play from right side

See also: LOADWAV, STOPWAV, PLAYMP3

↑ Back to top

PUTIMAGE

Resources · since 0.23.0

Displays loaded images on screen with advanced rendering options. Supports sprite animation, transparency, scaling, region cropping, tile map rendering, flipping, and rotation. Use BEGIN FRAME/END FRAME for optimal performance when drawing multiple sprites.

Syntax

PUTIMAGE (x, y), image_name [, frame] [, ALPHA(a)] [, SCALE(sx, sy)] [, SCALETO(w, h)] [, REGION(x1, y1, x2, y2)] [, MAP(array, x1, y1, x2, y2)] [, FLIPX] [, FLIPY] [, ROTATE(angle [, originX, originY])]
(the (x, y) position and SCALE each also accept a coordinate value)

Parameters

  • x, y - Screen coordinates for image placement
  • image_name - Name of image loaded with LOADIMG (must be sprite sheet for MAP)
  • frame - Optional sprite sheet frame number (starts at 0)
  • ALPHA(a) - Optional transparency (0.0=transparent, 1.0=opaque)
  • SCALE(sx, sy) - Optional scale factors (1.0 = original, 2.0 = double, 0.5 = half)
  • SCALETO(w, h) - Optional scaling to specific pixel dimensions
  • REGION(x1, y1, x2, y2) - Optional source region to copy from the image
  • MAP(array, x1, y1, x2, y2) - Optional tile map rendering. Draws tiles from 2D array where values are sprite frame indices. Values ≤0 are skipped (transparent). Coordinates specify the region of the map array to draw.
  • FLIPX - Optional horizontal flip
  • FLIPY - Optional vertical flip
  • ROTATE(angle) - Optional rotation in degrees (0-360), rotates around center (0.5, 0.5) by default
  • ROTATE(angle, originX, originY) - Rotation with custom pivot point using normalized values (0-1), relative to the image's top-left corner:
    • (0, 0) = top-left corner of image
    • (0.5, 0.5) = center of image (default)
    • (1, 1) = bottom-right corner of image
  • Modifiers can be combined in any order
  • Coordinates can be expressions or variables

Examples

PUTIMAGE (100, 100), "player"                     ' Draw image at position
PUTIMAGE (x, y), "sprites", frame                 ' Draw specific sprite frame
PUTIMAGE (50, 50), "ghost", ALPHA(0.5)            ' 50% transparent image
PUTIMAGE (200, 150), "icon", SCALE(2, 2)          ' Draw at double size
PUTIMAGE (200, 150), "icon", SCALETO(64, 64)      ' Scale to exactly 64x64 pixels
PUTIMAGE (0, 0), "tileset", REGION(32, 0, 64, 32) ' Copy a 32x32 region from source
PUTIMAGE (playerX, playerY), "hero", 0, FLIPX     ' Flip horizontally
PUTIMAGE (enemyX, enemyY), "boss", FLIPX, FLIPY   ' Flip both directions
PUTIMAGE (shipX, shipY), "ship", ROTATE(45)       ' Rotate 45 degrees around center (default)
PUTIMAGE (shipX, shipY), "ship", ROTATE(45, 0.5, 0.5) ' Rotate 45 degrees around center
PUTIMAGE (doorX, doorY), "door", ROTATE(90, 0, 1) ' Rotate around bottom-left (hinge)
PUTIMAGE (itemX, itemY), "powerup", currentFrame, ALPHA(0.8), SCALE(1.5, 1.5)  ' Combined modifiers
PUTIMAGE (x, y), "sprite", FLIPX, ROTATE(angle)   ' Flip and rotate
PUTIMAGE (0, 0), "room", REGION(20, 20, 400, 400), SCALETO(640, 480)  ' Crop and scale to target size
' Sprite animation example:
frame = (frame + 1) MOD FRAMECOUNT("player")
PUTIMAGE (playerX, playerY), "player", frame
' Rotating sprite around center:
angle = (angle + 5) MOD 360
PUTIMAGE (centerX, centerY), "spinner", ROTATE(angle, 0.5, 0.5)
' Fade effect:
FOR i = 0 TO 10
  alpha = i / 10
  PUTIMAGE (x, y), "item", ALPHA(alpha)
NEXT
' Tile map rendering:
LOADIMG "tiles.png", "tileset" AS SPRITESHEET(16, 16)
DIM map(9, 9) AS INTEGER
map(0, 0) = 1  ' Wall
map(1, 1) = 2  ' Floor
PUTIMAGE (0, 0), "tileset", MAP(map, 0, 0, 9, 9)  ' Draw entire 10x10 map
PUTIMAGE (100, 0), "tiles", MAP(levelMap, 0, 0, 4, 4)  ' Draw 5x5 section of map
PUTIMAGE (0, 0), "tiles", MAP(map, 0, 0, 9, 9), ALPHA(0.5)  ' Transparent tile map
PUTIMAGE (200, 0), "tiles", MAP(map, 0, 0, 9, 9), SCALE(2, 2)  ' Draw tiles at double size

Notes

LIMITATION: When using MAP() with ROTATE() on very large tilesets (over 2048px in either dimension), rotation around a custom origin may produce incorrect positioning. This is due to an interaction between texture chunking and the rotation coordinate system. Workaround: Use smaller tileset images, or avoid ROTATE with custom origins on large MAP renders.

See also: LOADIMG, FRAMECOUNT, ALPHA, SCALE, SCALETO, REGION, ROTATE, MAP, BEGIN FRAME, END FRAME

↑ Back to top

STOPMP3

Resources · 📱 Client-only · since 0.23.0

Stops audio playback.

Syntax

STOPMP3 [audio_name]

Parameters

  • audio_name - Audio to stop (or all if omitted)

Examples

STOPMP3 "bgmusic"
STOPMP3  ' Stop all audio

See also: PLAYMP3, LOADMP3, STOPWAV

↑ Back to top

STOPWAV

Resources · 📱 Client-only · since 0.23.0

Stops WAV audio playback.

Syntax

STOPWAV [audio_name]

Parameters

  • audio_name - Audio to stop (or all if omitted)

Examples

STOPWAV "effect"
STOPWAV  ' Stop all WAV audio

See also: PLAYWAV, LOADWAV, STOPMP3

↑ Back to top

Multiplayer

CALL HOST

Multiplayer · 🚧 Pre-release · since 1.19.0

Registers a subroutine to handle host promotion events. The subroutine is called when this client becomes the host (either initially or due to host failover). In CrashNet, one player is always designated as the "host" and is authoritative for HOST SPRITES. If the host disconnects, the next player by join order becomes host.

The optional GUARD(flags) modifier (see CALL) can restrict registration by interpreter context and/or serialize handler invocations with ONCE. Without FAILS, a guard violation is a silent no-op; with FAILS it halts the program with a runtime error.

Syntax

CALL HOST SubName() [GUARD(flags) [FAILS]] AS "handlerId"

Parameters

  • SubName - Name of subroutine to call (no parameters)
  • "handlerId" - String identifier for this handler (used with STOP HOST)

Examples

'Register host event handler
CALL HOST OnBecameHost() AS "hostHandler"

SUB OnBecameHost()
  PRINT "You are now the host!"
  ' Initialize host-only NPCs, etc.
END SUB
'Host takes control of NPCs
CALL HOST InitHostSprites() AS "hostInit"

SUB InitHostSprites()
  ' Set up NPC behaviors - only runs on host
  FOR i = 40 TO 49
    SET SPRITE i BEHAVIOR "patrol"
  NEXT i
END SUB

See also: STOP HOST, CALL PLAYER, CRASHNET, ME.ISHOST

↑ Back to top

CALL PLAYER

Multiplayer · 🚧 Pre-release · since 1.19.0

Registers a subroutine to handle player join/leave events. The CALL PLAYER statement uses empty parens — the runtime automatically passes a single player object to the SUB at invocation time. The SUB declares one parameter to receive that object; field access on it gives .SLOT, .NAME$, .ISHOST, .CONNECTED, and .JOINED (1 = join, 0 = leave), plus any custom PLAYER DATA fields declared in the CRASHNET block. Both joins and leaves dispatch to the same handler; branch on .JOINED to differentiate. Handler ID is used with STOP PLAYER to unregister.

The optional GUARD(flags) modifier (see CALL) can restrict registration to a specific interpreter context (HOST/PLAYER/CLIENT) and/or serialize handler invocations with ONCE so back-to-back joins don't overlap. Without FAILS, a guard violation is a silent no-op; with FAILS it halts the program with a runtime error.

Syntax

CALL PLAYER SubName() [GUARD(flags) [FAILS]] AS "handlerId"

Parameters

  • SubName() - Name of the subroutine, with empty parens — the runtime supplies the player-object arg automatically; you do not pass anything in the call site.
  • "handlerId" - String identifier for this handler (used with STOP PLAYER)
  • (SUB parameter) - Your SUB declares one parameter to receive the player object. Fields: .SLOT (Integer), .NAME$ (String), .ISHOST (Integer 0/1), .CONNECTED (Integer 0/1), .JOINED (Integer 1=joined, 0=left), plus any PLAYER DATA fields you declared in the CRASHNET block.

Examples

'Register player event handler — empty parens on the CALL site;
'the runtime fills in the player arg.
CALL PLAYER OnPlayerJoin() AS "playerHandler"

SUB OnPlayerJoin(player)
  IF player.JOINED THEN
    PRINT player.NAME$ + " joined as player " + STR$(player.SLOT)
  ELSE
    PRINT player.NAME$ + " left the game"
  END IF
END SUB
'Track players in an array, indexed by slot
DIM playerNames$(4) AS STRING
CALL PLAYER TrackPlayers() AS "tracker"

SUB TrackPlayers(p)
  IF p.JOINED THEN
    playerNames$(p.SLOT) = p.NAME$
  ELSE
    playerNames$(p.SLOT) = ""
  END IF
END SUB
'Read a custom PLAYER DATA field on join
' (declared in CRASHNET block as: PLAYER DATA / team$ AS string / END PLAYER DATA)
CALL PLAYER OnJoin() AS "teamWatch"

SUB OnJoin(p)
  IF p.JOINED THEN PRINT p.NAME$ + " joined team " + p.team$
END SUB

See also: STOP PLAYER, CALL HOST, CRASHNET, ME, PLAYERS

↑ Back to top

CRASHNET

Multiplayer · 🚧 Pre-release · since 1.19.0

Configures CrashNet multiplayer settings. Must appear before the main game loop.

Sprite ownership is first-come-first-served: any sprite the host or a player enables (via SET SPRITE / RESERVE SPRITE / ENABLE SPRITE) is tagged with the creator's slot. There are no preallocated sprite ranges. By default no sprite is broadcast to other clients — use SET SPRITE n SYNC to opt a sprite in to cross-client replication, and SET SPRITE n NOSYNC to opt back out. On disconnect, every sprite the leaving player created is automatically swept back into the free pool.

Under the hybrid execution model, sprites that the local CrashPlayer client creates (typically inside IF ME.ISCLIENT THEN) live in a separate ID space (32768+) so they never collide with server-owned sprites; this happens transparently when you use NEXTFREESPRITE / RESERVE SPRITE.

PLAYER DATA defines per-player custom fields that each player can set and all can read. HOST DATA defines authoritative game state that only the host can modify.

STREAM controls which client input categories are forwarded to the player's server-side interpreter. Default is none — bandwidth-first. Server-side KEYDOWN, MOUSEX, MOUSEDOWN, etc. silently return empty/zero unless the corresponding category is opted in. For occasional discrete actions (jump, fire, build), prefer EVENT SERVER "name"(...) — it's far cheaper than streaming raw input.

Syntax

CRASHNET
  MAX PLAYERS n
  PLAYER DATA...END PLAYER DATA
  HOST DATA...END HOST DATA
  PARTICLES
  TICK RATE rate
  STREAM <kind>+
END CRASHNET

Parameters

  • MAX PLAYERS n - Maximum simultaneous players in the room (default 4).
  • PLAYER DATA...END PLAYER DATA - Declares custom fields each player owns (accessed via ME.field or PLAYERS(n).field).
  • HOST DATA...END HOST DATA - Declares host-authoritative variables (only host can write, all can read).
  • PARTICLES - Enables replication of EMITPARTICLE commands (sprites 1000+).
  • TICK RATE rate - Network ticks per second (1-60, default 20). Higher = smoother but more bandwidth. Outside that range is a runtime error, not a clamp: the server's tick loop divides by this value, so zero would take the room down rather than slow it. This is how often the server broadcasts state — for how fast behaviors run, see SET BEHAVIOR RATE.
  • STREAM KEYBOARD - Forward KeyDown / KeyUp events to the player-server interpreter so server-side KEYDOWN(key$) works.
  • STREAM MOUSE - Forward mouse position AND buttons. Position is the heavy one — every move generates an event.
  • STREAM MOUSE BUTTONS - Forward mouse clicks only (no movement spam). Use when the server only cares about clicks.

Examples

'Full multiplayer config with custom data
CRASHNET
  MAX PLAYERS 4
  
  PLAYER DATA
    score AS INTEGER
    health AS INTEGER
    team$ AS STRING
  END PLAYER DATA
  
  HOST DATA
    gameMode$ AS STRING
    roundNumber AS INTEGER
    timeRemaining AS INTEGER
  END HOST DATA
  
  TICK RATE 20
END CRASHNET
'Minimal config for 2-player game
CRASHNET
  MAX PLAYERS 2
END CRASHNET
'Action game — server needs to read keyboard authoritatively
CRASHNET
  TICK RATE 30
  STREAM KEYBOARD
END CRASHNET
'Strategy game — only clicks matter on the server, not every mouse pixel
CRASHNET
  STREAM MOUSE BUTTONS
END CRASHNET
'On player join, host spawns a cross-client synced avatar
SUB OnJoin(p)
  IF p.JOINED THEN
    RESERVE SPRITE avatar
    IF avatar >= 0 THEN
      SET SPRITE avatar, 100, 100, "player" TAG "avatar" SYNC
    END IF
  END IF
END SUB
CALL PLAYER OnJoin() AS "avatarHandler"

See also: SYNC, CALL PLAYER, CALL HOST, EVENT, CALL EVENT, OnServerBroadcast, ME, PLAYERS, PLAYER DATA, HOST DATA

↑ Back to top

CRASHNET (object)

Multiplayer · 🚧 Pre-release · since 1.19.0

Built-in object providing information about the current CrashNet session state. CONNECTED = 1 when networked, 0 otherwise.

In single-player launches the object is fully populated with sensible defaults (CONNECTED = 0, ROOMID$ = "", MAXPLAYERS = 1, TICKRATE = 0) so reads of CRASHNET.MAXPLAYERS, etc. work the same way they do in multiplayer — gating on CRASHNET.CONNECTED is enough; you don't have to first check that the field exists.

Syntax

CRASHNET.CONNECTED
CRASHNET.ROOMID$
CRASHNET.MAXPLAYERS
CRASHNET.TICKRATE
CRASHNET.PLAYERCOUNT
CRASHNET.TICK

Parameters

  • CRASHNET.CONNECTED - 1 if connected to CrashNet server, 0 otherwise
  • CRASHNET.ROOMID$ - String room ID for the current session
  • CRASHNET.MAXPLAYERS - Maximum players allowed in this room (from MAX PLAYERS in the CRASHNET block)
  • CRASHNET.TICKRATE - Network tick rate in Hz (from TICK RATE in the CRASHNET block)
  • CRASHNET.PLAYERCOUNT - Number of currently connected players (equivalent to PLAYERS.COUNT)
  • CRASHNET.TICK - Current server tick number — monotonically increasing counter that advances at TICKRATE Hz; useful for time-aligned logic

Examples

'Check connection status
IF CRASHNET.CONNECTED THEN
  PRINT "Connected to room: " + CRASHNET.ROOMID$
  PRINT "Players: " + STR$(CRASHNET.PLAYERCOUNT) + "/" + STR$(CRASHNET.MAXPLAYERS)
ELSE
  PRINT "Not connected"
END IF
'Display session info
PRINT "Tick Rate: " + STR$(CRASHNET.TICKRATE) + " Hz"
PRINT "Current tick: " + STR$(CRASHNET.TICK)
'Run something every 60 ticks (~3s at 20Hz)
IF CRASHNET.TICK MOD 60 = 0 THEN
  PRINT "Periodic tick: " + STR$(CRASHNET.TICK)
END IF

See also: CRASHNET, ME, PLAYERS

↑ Back to top

GAMECONTEXT$

Multiplayer · 🚧 Pre-release · since 1.27.0

Built-in pseudo-string that reports which of the four execution contexts this interpreter is running in. Returns one of:

  • "SINGLE" — single-player launch. CrashPlayer was started with a local game directory or .crashcart (no --remote flag, no crashnet:// descriptor). The interpreter is the only one running; nothing is connected over the network.

  • "HOST" — the headless host interpreter (slot 0, server-side) inside a CrashNet session. Authoritative for game state; broadcasts sprite frames; runs no renderer.

  • "PLAYER" — a player-server interpreter (slot 1+, server-side). One per connected player. Authoritative for sprites that player creates; sees STREAM-opted input from the client.

  • "CLIENT" — a local CrashPlayer interpreter connected to a CrashNet server. Renders broadcasts from the host, owns local-only sprites in the 32768+ ID space, forwards opted-in input.

Designed to make a single SELECT CASE do the work that a stack of nested ME.IS* checks used to do — same .bas runs in all four contexts and dispatches per-context behavior cleanly:

SELECT CASE GAMECONTEXT$
  CASE "SINGLE": ' solo path
  CASE "HOST":   ' authoritative state
  CASE "PLAYER": ' per-player server work
  CASE "CLIENT": ' local rendering / input
END SELECT

Values update live across host migrations: if a player gets promoted to host mid-session, their interpreter starts reporting "HOST" on the next read of GAMECONTEXT$.

GAMECONTEXT$ subsumes the deployment-shape question (single vs networked) AND the multiplayer-role question (host vs player-server vs client) into one pseudo-variable. ME.ISHOST / ME.ISSERVER / ME.ISCLIENT still exist and remain useful for individual boolean checks, but GAMECONTEXT$ is the recommended choice when you want a CASE-style dispatch.

Syntax

GAMECONTEXT$

Examples

' Four-way SELECT CASE — the canonical pattern
SELECT CASE GAMECONTEXT$
  CASE "SINGLE"
    PRINT "Single-player. Press SPACE to start."
  CASE "HOST"
    HOST.gameMode$ = "deathmatch"
  CASE "PLAYER"
    ' Per-player server-side setup (e.g. spawn the player's avatar)
  CASE "CLIENT"
    LOADIMG "player.ase", "avatar"
END SELECT
' Run setup work only in non-CLIENT contexts
IF GAMECONTEXT$ <> "CLIENT" AND GAMECONTEXT$ <> "SINGLE" THEN
  ' Server-side only — host or player-server
  CALL RegisterAuthHandlers()
END IF
' Resource loading is client-only; gate cleanly with GAMECONTEXT$
IF GAMECONTEXT$ = "SINGLE" OR GAMECONTEXT$ = "CLIENT" THEN
  CALL LoadResources()
END IF
' Only the host should mint test-driver state at boot
IF GAMECONTEXT$ = "HOST" THEN
  HOST.roundNumber = 0
  HOST.gameStarted = 0
END IF

See also: ME, PLAYERS, CRASHNET, SELECT CASE

↑ Back to top

HOST DATA

Multiplayer · 🚧 Pre-release · since 1.19.0

Declares host-authoritative game state in a CrashNet multiplayer session.

Access pattern: HOST.<field> (symmetric with ME.<field> / PLAYERS(n).<field> for PLAYER DATA). Reads work from any execution context (host / player-server / local-client) and reflect the current authoritative state.

Write authority is strict: only the headless host (slot 0, server-side, where ME.ISHOST = 1) may mutate HOST.. A write from any other context (player-server, local-client) is a fatal runtime error — use EVENT HOST to ask the host to perform the write.

If the host disconnects and a new host is promoted, the new host inherits write access. After every successful write, the server pushes a NetHostDataUpdate over the WebSocket so all clients see the new value within one tick.

Must be declared inside a CRASHNET block.

Single-player: HOST is also a usable central state container in solo runs — HOST.score = 0, HOST.gameMode$ = "deathmatch", etc. all persist across statements. Declared HOST DATA fields are zero-initialized when the CRASHNET block runs (so reads of declared-but-unwritten fields return the type-appropriate default), and the same code that drives multiplayer game state works in solo without a GAMECONTEXT$ guard. The defaults are install-once: re-running the CRASHNET block (e.g. via /dev/restart) preserves any values you've already written. There's no write-authority enforcement in solo (no host vs. client distinction), and undeclared fields can still be added on first write — they read back as Integer(0) until set.

Syntax

HOST DATA
  fieldName AS type
  ...
END HOST DATA

Parameters

  • fieldName AS INTEGER - Declares an integer field (default 0)
  • fieldName AS FLOAT - Declares a floating-point field (default 0.0)
  • fieldName$ AS STRING - Declares a string field (default empty string)
  • fieldName AS BOOLEAN - Declares a boolean field (default 0)
  • Supported field types are INTEGER, FLOAT, STRING, and BOOLEAN. SINGLE and DOUBLE are not supported here — use FLOAT. A field declared with an unsupported type is dropped from the schema, so a later write to it halts at runtime with a 'not declared' error.

Examples

'Declare host-controlled game state
CRASHNET
  HOST DATA
    gameMode$ AS STRING
    roundNumber AS INTEGER
    timeRemaining AS INTEGER
    gameStarted AS INTEGER
  END HOST DATA
END CRASHNET
'Host sets game state — gated on ME.ISHOST
IF ME.ISHOST THEN
  HOST.gameMode$ = "deathmatch"
  HOST.roundNumber = 1
  HOST.timeRemaining = 300
  HOST.gameStarted = 1
END IF
'Any context can read host data
PRINT "Mode: " + HOST.gameMode$
PRINT "Round " + STR$(HOST.roundNumber)
IF HOST.gameStarted THEN
  PRINT "Time left: " + STR$(HOST.timeRemaining)
END IF
'Client asks the host to start the round (write must originate on the host)
IF ME.ISCLIENT AND KEYDOWN("S") THEN
  EVENT HOST "StartRound"
END IF
SUB OnStartRound()
  IF ME.ISHOST THEN
    HOST.gameStarted = 1
    HOST.roundNumber = HOST.roundNumber + 1
  END IF
END SUB
IF ME.ISHOST THEN
  CALL EVENT OnStartRound() AS "StartRound"
END IF

See also: CRASHNET, ME.ISHOST, PLAYER DATA, SYNC, CALL HOST

↑ Back to top

ME

Multiplayer · 🚧 Pre-release · since 1.19.0

Built-in object representing the local player. In a CrashNet multiplayer session it provides identity (slot + name), role (host status), execution side (server vs client), and connection state.

In single-player launches ME is populated as a synthetic one-player CLIENT: SLOT = 1, NAME$ = "Player", ISCLIENT = 1, ISSERVER = 0, ISHOST = 0, CONNECTED = 1, ROOM$ = "". This means dual-mode (single + multiplayer) games can read ME.NAME$, ME.SLOT, etc. without gating every access on GAMECONTEXT$ = "SINGLE" — the same code that works against a real CrashNet client also works in solo. (GAMECONTEXT$ itself stays "SINGLE" so authors who want the shape distinction still have it.)

The same player slot in multiplayer has two interpreters running concurrently: one server-side (in CrashNet, authoritative) and one client-side (in the local CrashPlayer, for rendering and input). ISHOST/ISSERVER/ISCLIENT let a SUB tell which context it's running in so the same .bas file can branch on responsibility:

  • ME.ISHOST = 1 only on the headless host server (slot 0 inside CrashNet)
  • ME.ISSERVER = 1 on the host AND on every player-server-side interpreter
  • ME.ISCLIENT = 1 only inside the player's local CrashPlayer process (and in single-player)

Sprite ownership is no longer per-player — sprites are first-come-first-served via RESERVE SPRITE / NEXTFREESPRITE, and ownership is tracked per-sprite by the slot of the creator. The historic ME.SPRITESTART / ME.SPRITEEND / ME.SPRITECOUNT / ME.SPRITES(n) accessors were removed in the 1.19+ rewrite of the sprite model.

Syntax

ME.SLOT
ME.NAME$
ME.ISHOST
ME.ISSERVER
ME.ISCLIENT
ME.CONNECTED
ME.ROOM$

Parameters

  • ME.SLOT - Integer slot number assigned to this player (0 to MAX PLAYERS - 1)
  • ME.NAME$ - String display name of this player
  • ME.ISHOST - 1 if this interpreter is the headless host (slot 0 server-side), 0 otherwise
  • ME.ISSERVER - 1 if this interpreter is running server-side (host or any player-server interpreter), 0 if running in the local CrashPlayer
  • ME.ISCLIENT - 1 if this interpreter is running client-side in the local CrashPlayer, 0 if running server-side. Mutually exclusive with ISSERVER.
  • ME.CONNECTED - 1 if this player slot is currently connected, 0 otherwise
  • ME.ROOM$ - String name of the CrashNet room this player is currently in. Empty string = no room (default; sees only global / unscoped sprites). Set via SET ROOM "name". Always empty for the host (slot 0), which is omnipresent across rooms.

Examples

'Show player info
PRINT "You are player " + STR$(ME.SLOT)
PRINT "Name: " + ME.NAME$
IF ME.ISHOST THEN PRINT "You are the host!"
'Reserve and own a sprite (replaces the old per-player sprite range)
RESERVE SPRITE myAvatar
IF myAvatar >= 0 THEN
  SET SPRITE myAvatar, 100, 100, "player" SYNC
END IF
'Bail early if not in multiplayer
IF NOT ME.CONNECTED THEN
  PRINT "Single-player mode"
  EXIT SUB
END IF
'Render only on the local client; mutate state only on the server
IF ME.ISCLIENT THEN
  LOCATE 1, 1: PRINT "slot " + STR$(ME.SLOT)
END IF
IF ME.ISSERVER AND ME.ISHOST THEN
  ' authoritative game-state mutation goes here
END IF

See also: PLAYERS, CRASHNET, CALL PLAYER, RESERVE SPRITE, NEXTFREESPRITE

↑ Back to top

OnServerBroadcast

Multiplayer · 🚧 Pre-release · since 1.27.0

Built-in CrashNet event fired when a server operator sends a message from the CrashNet Server admin dashboard's "Broadcast message to all players" field.

The event fires in all three execution contexts at the same time:

  • The host (slot 0, server-side) — for game-state side effects (e.g. log to disk, update a sign).
  • Every player-server interpreter (slots 1+, server-side) — for authoritative responses (e.g. award currency to everyone).
  • Every connected local-client interpreter (in CrashPlayer) — for rendering the message to the player's screen.

A single SUB handles all three; gate with ME.ISCLIENT / ME.ISSERVER / ME.ISHOST for context-specific behavior. If no handler is registered, the broadcast is silently ignored — registering a handler is opt-in.

Arguments are positional: message$ is the operator-typed body, kind$ is a free-form category supplied by the dashboard (currently always "system" — reserved for future severity / channel routing like "warning", "chat", etc.).

Note: this is the ONLY way an operator-issued broadcast surfaces to a running game. Without a registered handler, broadcasts have no visible effect on players.

Syntax

SUB OnServerBroadcast(message$, kind$)
CALL EVENT OnServerBroadcast() AS "handlerId"

Parameters

  • message$ - String message body typed by the operator in the dashboard.
  • kind$ - String category ("system" by default; future-reserved for severities like "warning", "chat", etc.).

Examples

' Show admin broadcasts as a top-of-screen banner on every client
SUB OnServerBroadcast(message$, kind$)
  IF ME.ISCLIENT THEN
    LOCATE 1, 1
    COLOR 14
    PRINT "[" + kind$ + "] " + message$
  END IF
END SUB
CALL EVENT OnServerBroadcast() AS "adminMsg"
' Differentiate by kind — system messages popup, others go to chat log
SUB OnServerBroadcast(message$, kind$)
  IF NOT ME.ISCLIENT THEN EXIT SUB
  IF kind$ = "system" THEN
    SHOW SPRITE bannerSprite
    SET SPRITE bannerLabel, 100, 10, message$
  ELSE
    chatLog$ = chatLog$ + message$ + CHR$(10)
  END IF
END SUB
CALL EVENT OnServerBroadcast() AS "adminMsg"
' Host-side: log every broadcast to disk for moderation audit
SUB OnServerBroadcast(message$, kind$)
  IF ME.ISHOST THEN
    OPEN "broadcasts.log" FOR APPEND AS #1
    PRINT #1, TIME$ + " [" + kind$ + "] " + message$
    CLOSE #1
  END IF
END SUB
CALL EVENT OnServerBroadcast() AS "auditLog"

See also: CALL EVENT, ME, CRASHNET, EVENT

↑ Back to top

PLAYER DATA

Multiplayer · 🚧 Pre-release · since 1.19.0

Declares custom data fields that each player owns in a CrashNet multiplayer session.

Access pattern: ME.<field> for your own data, PLAYERS(n).<field> for any player's data. Reads work from any execution context (host / player-server / local-client) and reflect the current authoritative state.

Write authority — host > player-server > local-client:

  • Host (slot 0, server-side): may write any player's PLAYER DATA via PLAYERS(n)..
  • Player-server (slots 1+, server-side): may write its OWN PLAYER DATA via ME. (or equivalently PLAYERS(my_slot).). Writing to OTHER players' data is a fatal runtime error — only the host may cross slots. Use a CALL HOST handler if cross-player writes are needed.
  • Local-client (in CrashPlayer): read-only. Mutating ME. from a client interpreter is a fatal runtime error — fire EVENT SERVER and let the player-server interpreter perform the write authoritatively.

After every successful write, the server pushes a NetStateUpdate over the WebSocket so other clients' local NetState mirror picks up the new value within one tick. SYNC is a hint, not required for replication.

Must be declared inside a CRASHNET block.

Single-player: declared PLAYER DATA fields are zero-initialized on ME (and equivalently PLAYERS(1)) when the CRASHNET block runs, so dual-mode code that reads ME.score before the first write gets 0 instead of an undefined member. Solo writes through ME.<field> persist across statements (no authority gate fires when there's no NetState), and PLAYERS(1).<field> reads see the same data because both surface the same Object internally. The defaults are install-once — re-running the CRASHNET block preserves any values you've written.

Syntax

PLAYER DATA
  fieldName AS type
  ...
END PLAYER DATA

Parameters

  • fieldName AS INTEGER - Declares an integer field (default 0)
  • fieldName AS FLOAT - Declares a floating-point field (default 0.0)
  • fieldName$ AS STRING - Declares a string field (default empty string)
  • fieldName AS BOOLEAN - Declares a boolean field (default 0)
  • Supported field types are INTEGER, FLOAT, STRING, and BOOLEAN. SINGLE and DOUBLE are not supported here — use FLOAT. A field declared with an unsupported type is dropped from the schema, so a later write to it halts at runtime with a 'not declared' error.

Examples

'Declare player stats
CRASHNET
  PLAYER DATA
    score AS INTEGER
    health AS INTEGER
    team$ AS STRING
  END PLAYER DATA
END CRASHNET
'Set my player data
ME.score = 100
ME.health = 75
ME.team$ = "red"
'Read another player's data
IF PLAYERS(1).team$ = ME.team$ THEN
  PRINT "Same team!"
END IF
PRINT "Player 1 score: " + STR$(PLAYERS(1).score)

See also: CRASHNET, ME, PLAYERS, HOST DATA, SYNC

↑ Back to top

PLAYERS

Multiplayer · 🚧 Pre-release · since 1.19.0

Built-in array-like object providing access to all connected players. Use PLAYERS.COUNT for the count of connected players; index by slot number to access an individual player.

In single-player launches, PLAYERS.COUNT = 1 and PLAYERS(1) returns the synthetic solo player (same shape as the multiplayer case). Dual-mode games can iterate FOR i = 1 TO PLAYERS.COUNT ... PLAYERS(i).NAME$ without gating on GAMECONTEXT$.

The historic per-player sprite-range fields (PLAYERS(n).SPRITESTART / SPRITEEND / SPRITECOUNT / SPRITES(m)) were removed in the 1.19+ sprite-model rewrite — sprites are now first-come-first-served via RESERVE SPRITE / NEXTFREESPRITE, with ownership tracked per-sprite by the slot of the creator.

Syntax

PLAYERS.COUNT
PLAYERS(n).SLOT
PLAYERS(n).NAME$
PLAYERS(n).ISHOST
PLAYERS(n).CONNECTED
PLAYERS(n).ROOM$

Parameters

  • PLAYERS.COUNT - Total number of currently connected players
  • PLAYERS(n).SLOT - Slot number of player n
  • PLAYERS(n).NAME$ - Display name of player n
  • PLAYERS(n).ISHOST - 1 if player n is the host, 0 otherwise
  • PLAYERS(n).CONNECTED - 1 if player n is currently connected, 0 otherwise
  • PLAYERS(n).ROOM$ - String name of the CrashNet room player n is currently in. Empty string = no room. Useful for filtering the roster to only players in your room.

Examples

'List all players
FOR i = 0 TO PLAYERS.COUNT - 1
  PRINT PLAYERS(i).NAME$ + " (slot " + STR$(PLAYERS(i).SLOT) + ")"
  IF PLAYERS(i).ISHOST THEN PRINT " [HOST]"
NEXT i
'Find host player
FOR i = 0 TO PLAYERS.COUNT - 1
  IF PLAYERS(i).ISHOST THEN
    hostSlot = PLAYERS(i).SLOT
    EXIT FOR
  END IF
NEXT i
'Index by slot to find someone you know about
otherName$ = PLAYERS(1).NAME$

See also: ME, CRASHNET, CALL PLAYER

↑ Back to top

SET ROOM

Multiplayer · 🚧 Pre-release · since 1.27.0

Move the calling player into a CrashNet room. Rooms scope sprite visibility and cross-sprite interaction, so an MMORPG-style server can hold many concurrent areas in one process without Room A's sprites bleeding into Room B's renderer or colliding with Room B's sprites.

Pair with SET SPRITE n SYNC("RoomName") to author room-scoped sprites. Sprites without a room are global — visible to everyone in every room. Use SET ROOM "" to send the calling player back to the unscoped global view (where they see only the globals).

In single-player, SET ROOM is always legal. The solo interpreter is treated as authoritative for its own room state — it's effectively both "the host" and "the client" of its own session. Use it for level switching in solo games: pair with WITH ROOM "levelN" blocks and SET ROOM "levelN" auto-mounts everything tagged for that level.

In multiplayer, SET ROOM is legal only on a player-server interpreter (ME.ISSERVER AND NOT ME.ISHOST).

  • Calling on the host (slot 0) is a fatal runtime error. The host is omnipresent across all rooms; it doesn't belong to one.
  • Calling on the local-client interpreter is a fatal runtime error. Moving rooms is an authoritative state change — clients ask, the server decides.

What happens on execution

  1. The server sweeps every sprite this player owns, sending DisableSprite. That broadcasts naturally — players still in the old room see those sprites vanish.
  2. ME.ROOM$ updates.
  3. A per-recipient snapshot replays to this player's renderer: disable every sprite that lived in the old room (they're not visible to this player anymore), then re-enable and position every sprite that lives in the new room.
  4. You're expected to re-create the player's character / projectiles / whatever at the new room's spawn point. The old sprites are gone.

Tilemap transition

The same per-recipient replay path handles tilemaps too. On SET ROOM:

  • Old room's tilemaps tear down (one DestroyLayer per mounted screen-layer).
  • New room's tilemaps mount fresh — a CreateLayer + CreateTilemap pair per entry.
  • Tilemaps tagged _global are not touched — they stay visible across rooms.

Single-player gets level-switching for free. Author your tilemaps inside WITH ROOM "name" blocks ahead of time, then a single SET ROOM "name" auto-mounts everything tagged for that room. No manual CREATE MAP / RESET LAYERS choreography.

Syntax

SET ROOM expr

Parameters

  • expr - String expression naming the target room. Empty string returns the player to the global / unscoped view.

Examples

' Player teleports from town to dungeon
IF ME.ISSERVER AND NOT ME.ISHOST THEN
  SET ROOM "dungeon"
  ' Re-spawn this player's sprites at the dungeon entrance
  RESERVE SPRITE avatar
  IF avatar >= 0 THEN
    SET SPRITE avatar, 50, 50, "player" TAG "avatar" SYNC("dungeon")
  END IF
END IF
' Lobby → game room transition driven by a client EVENT
SUB OnEnterGame(roomName$)
  SET ROOM roomName$
END SUB
IF ME.ISSERVER AND NOT ME.ISHOST THEN
  CALL EVENT OnEnterGame("") AS "roomChange"
END IF
' Read your own room from anywhere
IF ME.ROOM$ = "dungeon" THEN
  PRINT "Welcome, brave adventurer."
END IF
' Roster filtered by room
FOR i = 0 TO PLAYERS.COUNT - 1
  IF PLAYERS(i).ROOM$ = ME.ROOM$ THEN
    PRINT PLAYERS(i).NAME$, " is here with you"
  END IF
NEXT i

See also: SET SPRITE, ME, PLAYERS, CRASHNET, WITH, CREATE MAP

↑ Back to top

STOP HOST

Multiplayer · 🚧 Pre-release · since 1.19.0

Unregisters a host event handler by its ID. After calling STOP HOST, the associated subroutine will no longer be called for host promotion events.

Syntax

STOP HOST "handlerId"

Parameters

  • "handlerId" - String identifier of handler to unregister (from CALL HOST AS)

Examples

'Unregister host handler
STOP HOST "hostHandler"
'Cleanup on game end
STOP HOST "hostInit"
STOP PLAYER "playerHandler"

See also: CALL HOST, STOP PLAYER

↑ Back to top

STOP PLAYER

Multiplayer · 🚧 Pre-release · since 1.19.0

Unregisters a player event handler by its ID. After calling STOP PLAYER, the associated subroutine will no longer be called for player join/leave events.

Syntax

STOP PLAYER "handlerId"

Parameters

  • "handlerId" - String identifier of handler to unregister (from CALL PLAYER AS)

Examples

'Unregister player handler
STOP PLAYER "playerHandler"
'Conditional unregister
IF gameOver THEN
  STOP PLAYER "tracker"
END IF

See also: CALL PLAYER, STOP HOST

↑ Back to top

SYNC

Multiplayer · 🚧 Pre-release · since 1.19.0

Network synchronization point. When executed, sends pending sprite state changes to the CrashNet server and processes incoming updates from other players. Should be called once per frame in the main game loop. Unlike YIELD (which just gives up CPU time), SYNC specifically handles network state exchange.

Syntax

SYNC

Examples

'Main game loop with network sync
DO
  ' Handle input
  ' Update game state
  SYNC  ' Exchange network state
LOOP
'With frame timing
DO
  ' Game logic here
  SYNC
  YIELD  ' Also yield CPU if needed
LOOP

See also: CRASHNET, YIELD, CALL PLAYER, CALL HOST

↑ Back to top

In-App Purchase

FOR PURCHASE

In-App Purchase · 🚧 Pre-release · since 1.7.0

Iterates over the locally cached list of in-app purchase products. Products are pushed by the platform when loaded from the app store - the loop executes immediately without blocking. Inside the loop, use PURCHASE.ID, PURCHASE.NAME, PURCHASE.PRICE, and PURCHASE.PURCHASED to access product information. Supports EXIT FOR to break out early. Note: The loop may iterate zero times if products haven't been loaded yet.

Syntax

FOR PURCHASE
  statements
NEXT PURCHASE

Examples

' Display all available products
FOR PURCHASE
  PRINT PURCHASE.NAME; " - "; PURCHASE.PRICE
  IF PURCHASE.PURCHASED THEN PRINT " (Owned)"
NEXT PURCHASE
' Find a specific product
FOR PURCHASE
  IF PURCHASE.ID = "premium" AND NOT PURCHASE.PURCHASED THEN
    PRINT "Premium available for "; PURCHASE.PRICE
    EXIT FOR
  END IF
NEXT PURCHASE

See also: PURCHASE, PURCHASE NEW, PURCHASE RESTORE

↑ Back to top

PURCHASE

In-App Purchase · 🚧 Pre-release · since 1.7.0

Pseudo-variable providing access to purchase product properties. AVAILABLE is always accessible and indicates whether IAP is connected to a provider (1) or offline/unavailable (0). Other properties (ID, NAME, DESCRIPTION, PRICE, PURCHASED) are populated during a FOR PURCHASE loop and read from the locally cached product list. Outside of a FOR PURCHASE loop, these loop-specific properties return empty/zero values. Note: PURCHASED only applies to non-consumable products (e.g., 'remove ads', 'premium upgrade'). Consumable products (e.g., coins, gems) always show PURCHASED=0 since they can be bought multiple times - use the purchase event to track consumable inventory in your own game variables.

Syntax

PURCHASE.ID
PURCHASE.NAME
PURCHASE.DESCRIPTION
PURCHASE.PRICE
PURCHASE.PURCHASED
PURCHASE.AVAILABLE

Parameters

  • PURCHASE.ID - Product identifier string (e.g., "com.example.coins100")
  • PURCHASE.NAME - Human-readable product name (e.g., "100 Coins")
  • PURCHASE.DESCRIPTION - Extended product description (e.g., "Get 100 in-game coins to spend in the shop.")
  • PURCHASE.PRICE - Formatted price string (e.g., "$0.99")
  • PURCHASE.PURCHASED - 1 if already purchased (non-consumables only), 0 otherwise
  • PURCHASE.AVAILABLE - (Global) 1 if connected to IAP provider, 0 if offline or unavailable

Examples

IF PURCHASE.AVAILABLE THEN
  FOR PURCHASE
    PRINT PURCHASE.NAME; " - "; PURCHASE.PRICE
    PRINT "  "; PURCHASE.DESCRIPTION
  NEXT PURCHASE
ELSE
  PRINT "Store unavailable"
END IF
FOR PURCHASE
  PRINT PURCHASE.NAME; ": "; PURCHASE.PRICE
  IF PURCHASE.PURCHASED THEN
    PRINT " [OWNED]"
  END IF
NEXT PURCHASE

See also: FOR PURCHASE, PURCHASE NEW, PURCHASE RESTORE

↑ Back to top

PURCHASE NEW

In-App Purchase · 🚧 Pre-release · since 1.7.0

Initiates a purchase for the specified product. This is an asynchronous operation - register an event handler with CALL EVENT to receive the result. The purchase event fires with parameters (productId$, success, errorMessage$). For consumable products (coins, gems, etc.), your game must track inventory in its own variables since consumables can be purchased multiple times. A purchase the store runs and refuses — a declined card, a cancelled dialog — is an ordinary result: the purchase event fires with success = 0 and the reason in errorMessage$. A purchase that can never start is not, and halts with an error instead: that means the store returned no checkout page, or the build has no way to open one. Today purchases run through a CrashNet IAP server, which is why PURCHASE is still pre-release; iOS and tvOS builds can also buy through StoreKit once the app is set up for it, and those never open a checkout page at all. Reaching the error means the provider in iap.json does not match the platform you are running on, which is a build problem rather than something a purchase handler can recover from.

Syntax

PURCHASE NEW productId

Parameters

  • productId - String expression containing the product identifier to purchase

Examples

' Simple purchase
PURCHASE NEW "com.example.removeads"
' Consumable purchase - game manages inventory
DIM coins AS INTEGER
coins = 0

SUB OnPurchase(productId$, success, errorMsg$)
  IF success THEN
    IF productId$ = "coins100" THEN coins = coins + 100
    IF productId$ = "coins500" THEN coins = coins + 500
  END IF
END SUB
CALL EVENT OnPurchase() AS "purchase"

PURCHASE NEW "coins100"

See also: FOR PURCHASE, PURCHASE RESTORE, CALL EVENT

↑ Back to top

PURCHASE RESTORE

In-App Purchase · 🚧 Pre-release · since 1.7.0

Restores previously purchased non-consumable products (e.g., after reinstalling the app or on a new device). Fires the purchase event for each restored product with success=1. If restore fails, fires a single event with success=0 and an error message. Note: Consumable purchases (coins, gems, etc.) cannot be restored - only non-consumables like 'remove ads' or 'premium upgrade' are restorable.

Syntax

PURCHASE RESTORE

Example

' Restore purchases with event handling
SUB OnPurchaseComplete(productId$, success, errorMsg$)
  IF success THEN
    PRINT "Restored: "; productId$
  ELSE
    PRINT "Restore failed: "; errorMsg$
  END IF
END SUB
CALL EVENT OnPurchaseComplete() AS "purchase"

PURCHASE RESTORE

See also: FOR PURCHASE, PURCHASE NEW, CALL EVENT

↑ Back to top

AI

ASTAR

AI · since 1.12.0

A* pathfinding on a 2D array. Returns an array of coordinate waypoints from start to end. Grid values of 0 are walkable, non-zero are blocked. Supports 8-directional movement with proper diagonal costs. Returns an empty array if no path exists.

Syntax

ASTAR(grid, start, end)

Parameters

  • grid - 2D array (DIM grid(w, h) AS INTEGER) where 0 = walkable
  • start - Starting coordinate (x, y)
  • end - Goal coordinate (x, y)

Example

DIM grid(10, 10) AS INTEGER
grid(5, 0) = 1
grid(5, 1) = 1
grid(5, 2) = 1
path = ASTAR(grid, (0, 0), (9, 9))
FOR EACH wp IN path
  PRINT wp.X; ","; wp.Y
NEXT

See also: RAYCAST, NEIGHBORS, DISTANCE

↑ Back to top

CHOOSE

AI · since 1.12.0

Returns a random element from an array. Returns 0 if the array is empty.

Syntax

CHOOSE(array)

Parameters

  • array - Array to pick from

Examples

greetings = ["Hello!", "Hi there!", "Hey!"]
PRINT CHOOSE(greetings)
direction = CHOOSE(["left", "right", "up", "down"])

See also: WEIGHTED, NEAREST

↑ Back to top

DISTANCE

AI · since 1.12.0

Returns the Euclidean distance between two coordinates.

Syntax

DISTANCE(a, b)

Parameters

  • a - First coordinate (x, y)
  • b - Second coordinate (x, y)

Examples

d = DISTANCE((0, 0), (3, 4))  ' Returns 5.0
IF DISTANCE(playerPos, enemyPos) < 50 THEN
  PRINT "Enemy nearby!"
END IF

See also: NEAREST, SEEK

↑ Back to top

FLEE

AI · since 1.12.0

Returns a velocity coordinate pointing away from the threat at the given speed. Opposite of SEEK. Returns (0, 0) if positions are identical.

Syntax

FLEE(pos, threat, speed)

Parameters

  • pos - Current position coordinate (x, y)
  • threat - Position to flee from (x, y)
  • speed - Movement speed (magnitude of result)

Example

velocity = FLEE(playerPos, enemyPos, 5)
player.X = player.X + velocity.X
player.Y = player.Y + velocity.Y

See also: SEEK, DISTANCE, WANDER

↑ Back to top

FLOODFILL

AI · since 1.12.0

Flood fills a 2D grid from the starting position, counting all connected cells that match the given value. Uses 4-directional connectivity. Returns the number of filled cells. Useful for checking area reachability.

It marks the grid. Every cell the fill reaches is set to -1 in the array you passed — the value is negative on purpose, so it cannot be mistaken for terrain by a later query. The return value is how many cells that was.

Syntax

FLOODFILL(grid, start, value)

Parameters

  • grid - 2D array to flood fill
  • start - Starting coordinate (x, y)
  • value - Cell value to match and fill (e.g., 0 for walkable cells)

Example

DIM grid(10, 10) AS INTEGER
grid(5, 0) = 1
grid(5, 1) = 1
grid(5, 2) = 1
reachable = FLOODFILL(grid, (0, 0), 0)
PRINT "Reachable cells:"; reachable

See also: ASTAR, NEIGHBORS

↑ Back to top

MAZE

AI · since 1.38.0

Procedurally generates a maze as a fresh 2D array where 1 = wall and 0 = floor. The array is exactly width x height (shape [height, width]); index it as maze(row, col). The output drops straight into ASTAR, FLOODFILL, RAYCAST and NEIGHBORS (a coordinate (x, y) maps to maze(y, x)), so you can generate a maze and pathfind across it in two lines, or read it back to build a tilemap. The outer border is always wall; even width/height values leave an extra wall strip on the far edge. width/height are clamped up to a minimum of 3. The trailing arguments are optional and matched by type, so MAZE(w, h), MAZE(w, h, "CAVE"), MAZE(w, h, 42) and MAZE(w, h, 42, "PRIM", 0.3) all work; when two numbers are supplied the first is the seed and the second is braid.

Turning a maze into a map. Write the maze into a tile array and hand that to CREATE MAP FROM. There is no SET TILE statement — to change tiles after the map is up, assign into the bound array (tiles(x, y) = idx) and call UPDATE MAP, which pushes just that change rather than rebuilding the map.

Syntax

MAZE(width, height [, seed] [, style$] [, braid])

Parameters

  • width - Number of columns in the returned array (minimum 3)
  • height - Number of rows in the returned array (minimum 3)
  • seed - Optional. 0 or omitted = a different maze each call; any nonzero value = reproducible
  • style$ - Optional. "BACKTRACKER" (winding corridors, default), "PRIM" (bushier, many short dead-ends), "DIVISION" (straight walls, room-like), or "CAVE" (organic cellular caverns)
  • braid - Optional, 0.0 to 1.0. 0.0 = perfect maze with exactly one path between any two cells (default); higher values remove that fraction of dead-ends, adding loops. Ignored for the CAVE style

Examples

' Generate a 31x31 maze and pathfind across it
maze = MAZE(31, 31)
path = ASTAR(maze, (1, 1), (29, 29))
' Build a tilemap from a reproducible, looping maze
maze = MAZE(41, 41, 12345, "PRIM", 0.3)
DIM tiles(41, 41) AS INTEGER
FOR row = 0 TO 40
  FOR col = 0 TO 40
    IF maze(row, col) = 1 THEN
      tiles(col, row) = 3        ' wall tile index
    ELSE
      tiles(col, row) = 0        ' floor tile index
    END IF
  NEXT
NEXT
CREATE MAP FROM tiles TILESET "dungeon.png" TILESIZE 16 AS LAYER 0
' Organic cave layout for a dungeon
cave = MAZE(60, 40, 0, "CAVE")

See also: ASTAR, FLOODFILL, NEIGHBORS, CHOOSE

↑ Back to top

NEAREST

AI · since 1.12.0

Returns the index (0-based) of the nearest coordinate in the array to the given position. Returns -1 if the array is empty.

Syntax

NEAREST(pos, points)

Parameters

  • pos - Reference coordinate (x, y)
  • points - Array of coordinates to search

Example

enemies = [(10, 20), (50, 50), (30, 15)]
closest = NEAREST(playerPos, enemies)
PRINT "Nearest enemy at index:"; closest

See also: DISTANCE, CHOOSE

↑ Back to top

NEIGHBORS

AI · since 1.12.0

Returns an array of coordinate positions for valid non-blocked neighbors of a position on a 2D grid. Uses 4-directional adjacency (up, right, down, left). Only includes cells where the grid value is 0.

Syntax

NEIGHBORS(grid, pos)

Parameters

  • grid - 2D array where 0 = walkable, non-zero = blocked
  • pos - Position to check neighbors of (x, y)

Example

DIM grid(10, 10) AS INTEGER
n = NEIGHBORS(grid, (5, 5))
FOR EACH cell IN n
  PRINT cell.X; ","; cell.Y
NEXT

See also: ASTAR, FLOODFILL

↑ Back to top

RAYCAST

AI · since 1.12.0

Line-of-sight check on a 2D grid using Bresenham's line algorithm. Returns 1 if the line from start to end is clear (no blocked cells), 0 if blocked. Start and end positions are not checked (only cells between them).

Syntax

RAYCAST(grid, start, end)

Parameters

  • grid - 2D array where 0 = clear, non-zero = blocked
  • start - Starting coordinate (x, y)
  • end - Target coordinate (x, y)

Example

DIM grid(10, 10) AS INTEGER
grid(5, 5) = 1
IF RAYCAST(grid, (0, 0), (9, 9)) THEN
  PRINT "Clear line of sight"
ELSE
  PRINT "Blocked!"
END IF

See also: ASTAR, DISTANCE

↑ Back to top

SEEK

AI · since 1.12.0

Returns a velocity coordinate pointing from pos toward target at the given speed. Useful for basic "move toward" AI behavior. Returns (0, 0) if already at target.

Syntax

SEEK(pos, target, speed)

Parameters

  • pos - Current position coordinate (x, y)
  • target - Target position coordinate (x, y)
  • speed - Movement speed (magnitude of result)

Example

velocity = SEEK(enemyPos, playerPos, 3)
enemy.X = enemy.X + velocity.X
enemy.Y = enemy.Y + velocity.Y

See also: FLEE, DISTANCE, WANDER

↑ Back to top

WANDER

AI · since 1.12.0

Returns a new position for gentle random wandering (Craig Reynolds style). Projects a circle at 'distance' ahead in the direction of 'angle' (degrees), then picks a random point on that circle. The angle is jittered randomly each call for natural movement.

Syntax

WANDER(pos, angle, radius, distance)

Parameters

  • pos - Current position coordinate (x, y)
  • angle - Current heading in degrees
  • radius - Wander circle radius (larger = more erratic)
  • distance - How far ahead the wander circle is projected

Example

heading = 0
pos = (100, 100)
pos = WANDER(pos, heading, 10, 20)

See also: SEEK, FLEE

↑ Back to top

WEIGHTED

AI · since 1.12.0

Returns a random element from the values array, weighted by the corresponding weights array. Higher weights mean higher probability of selection. Weights should be non-negative numbers.

Syntax

WEIGHTED(values, weights)

Parameters

  • values - Array of items to choose from
  • weights - Array of numeric weights (same length as values)

Example

loot = ["common", "rare", "epic"]
chances = [80, 15, 5]
drop = WEIGHTED(loot, chances)

See also: CHOOSE

↑ Back to top

String

FIXED$

String · since 1.41.0

Formats a number as text with exactly places decimal places (for example, FIXED$(-0.56789, 2) returns "-0.57"). Guarantees a clean display regardless of floating-point representation, making it ideal for showing noisy values such as joystick axes. Use FIXED for the rounded number form.

Syntax

FIXED$(value, places)

Parameters

  • value - Any numeric expression
  • places - Number of decimal places (0-15; negative values are treated as 0)

Examples

PRINT FIXED$(3.14159, 2)   ' Prints 3.14
PRINT FIXED$(1, 3)         ' Prints 1.000
LOCATE 2, 1 : PRINT "X: " + FIXED$(JOYAXIS(0), 2)

See also: FIXED

↑ Back to top