NEOLearn

The World API

What a script's World class gives you, state, emit, query, spawn, rng and time, and how entity objects behave in JavaScript and Python.

Updated October 17, 2026

On this page

Every script's class extends World. Inside a handler, this (JavaScript) or self (Python) is the script's world, and it is the only way a script reaches the game. This page covers each member and the entity objects that query and events give you. For signatures and parameter tables, see The neo module.

Members at a glance#

MemberKindWhat it does
statepropertyThe script's saved state, kept by the world.
emit(event, payload)methodSends an event to the game's Rust systems.
query(kind)methodReturns every visible entity of a kind.
spawn(kind, fields)methodCreates an entity of a spawnable kind.
rngpropertyThe world-seeded random stream: rng.random().
timepropertySimulation time in seconds.

The names are the same in both languages.

state#

this.state (Python: self.state) holds the values your script keeps. Declare initial values as a class field, then read and write keys directly:

JavaScript
state = { opened: 0, lastOpener: null };
// in a handler
this.state.opened += 1;

Reads give you values, not live references: nested lists and objects arrive as frozen copies, so you write a whole new list instead of pushing into one. Hot reload keeps state. All the rules are in Script state.

emit(event, payload)#

Sends an event out of the script. The game's Rust systems read it after the scripts run (as a ScriptEmitted message with the script's name, the event name and the payload).

JavaScript
this.emit("puzzle_solved", { by: this.state.lastOpener });
  • payload is optional; leaving it out sends null.
  • The payload must be plain data: numbers, strings, booleans, null/None, lists, objects or dicts with string keys, and entity objects. Functions, symbols and Python sets can't be sent.
  • Emitted events go to the game, not to other scripts' @on handlers.

query(kind)#

Returns a list of every entity of kind that your script may see, in id order:

JavaScript
for (const lamp of this.query("Lamp")) lamp.intensity = 0.5;
const platform = this.query("Platform")[0];

A kind that isn't exposed to your script returns an empty list: as far as the script can tell, it doesn't exist. query takes only a kind name; filter the list yourself.

spawn(kind, fields)#

Creates an entity and returns its entity object right away:

JavaScript
const cube = this.spawn("Cube", { size: 0.5, position: { x: 0, y: 3, z: 0 } });
this.emit("cube_spawned", { id: cube.id });
  • The kind must be spawnable for your script (spawn = true in neo.toml, or a Kind:spawn grant for players), and the game must register a spawner for it. Otherwise spawn throws (TypeError: Cube isn't spawnable by scripts).
  • You can only set fields your script may write, and numeric fields must be inside their declared range.
  • Ids are never reused, so an id you saved can't later point at a different entity.

rng#

this.rng.random() returns a float in [0, 1) from the world's random stream. In JavaScript, Math.random() draws from the same stream. The stream is seeded from the world seed and the script's name, so the same game replays the same numbers.

JavaScript
if (this.rng.random() < 0.25) this.state.rolls += 1;

time#

this.time is the simulation time in seconds, as a float. It follows the game's fixed timestep, never the wall clock. See Time, sleep and async.

Entity objects#

query, spawn and event arguments give you entity objects. Each one stands for one game entity:

You writeWhat happens
door.idAlways readable: the entity's id (a number in JavaScript, an int in Python).
lamp.intensityReads an exposed field. Nested values (such as position) are frozen copies.
lamp.intensity = 0.5Writes an exposed, writable field. The game applies it right after your script's turn.
door.open()Calls an exposed method.
"open" in doortrue for fields and methods your script may use.
JSON.stringify(door){"id":1}: only the id.

Some details that matter:

  • Same entity, same object. Two references to one entity are the same object (a === b in JavaScript).
  • Positions are { x, y, z }. A kind declared with a transform has a position field. Write a whole new object: platform.position = { x: 4, y: 1, z: 0 }. Changing platform.position.x on the copy you read fails because the copy is frozen.
  • Method results. In a running game, an exposed method runs right after your script's turn ends, so the call itself returns null (Python: None). If the Rust method returns an error, it is recorded as your script's fault.
  • Names. JavaScript sees camelCase (displayName); Python sees the snake_case name (display_name).

When access is denied#

Anything your script isn't allowed to use fails loudly instead of doing nothing:

ActionJavaScriptPython
Read a field that isn't readableTypeError: Platform.boosts isn't readable by scriptsPermissionError
Write a read-only fieldTypeError: Door.open isn't writable by scriptsPermissionError
Write a number outside the field's rangeTypeError: Lamp.intensity must be in 0..=1TypeError
Call a method more often than its rateTypeError: ... is limited to 2 calls per secondTypeError
Query a kind that isn't exposedan empty listan empty list

You can catch these errors. An uncaught one ends the handler with a fault. See Exposure and permissions.

Python equivalents#

The Python names are the same; only the syntax changes:

JavaScriptPython
this.state.lastOpenerself.state.last_opener
this.emit("go", { at: this.time })self.emit("go", {"at": self.time})
this.query("Door")self.query("Door")
this.spawn("Cube", { size: 2 })self.spawn("Cube", {"size": 2})
this.rng.random() or Math.random()self.rng.random()
nullNone

Event payloads that are maps behave as dicts in Python and can also be read as attributes (player.id).

See also#