Jamango! API Documentation#
Introduction#
What is a script?#
A script in Jamango! is a piece of code that runs in the game world, allowing you to define behaviors, interactions, and game mechanics. Scripts are written in TypeScript and can use the Jamango API to interact with the game world.
Entry Point Scripts#
Jamango uses two entry point scripts to organize your code:
client.ts- Code that runs only on the client (players' devices). Use for UI updates, input handling, and client-specific visual effects.server.ts- Code that runs only on the dedicated server. Use for game logic, world state management, and authoritative operations like chat messages.
Both files can import other TypeScript files in your project using relative imports. Only these entry points are executed automatically - other files must be imported to run.
Importing the API#
In order to use the Jamango API, start by importing the functions. There are two possible ways for you to do this:
Import every function and use them via J:
tsimport * as J from "jamango";
Import specific functions and use them directly:
tsimport { onGameStart, spawnProp } from "jamango";
Core Concepts#
Positions and velocities are typically expressed as arrays of numbers. A x=1, y=5, z=3 vector can simply be expressed as [1, 5, 3].
The vec3 library contains functions that operate on vectors:
tsimport * as J from "jamango"; // Create vectors A and B const a = [10, 5, 3]; const b = [2, 0, -1]; // Get the distance between the points A and B const distance = J.vec3.distance(a, b);
Get the asset ID of anything you've built in the world editor by using the assets object:
tsconst coinAsset = J.assets.props.Coin.id
Spawn instances of entities by using your asset ID. The spawn functions will return an entity ID:
tsconst coin = J.spawnProp(coinAsset)
With the entity ID of your coin, you can now make it go flying!
tsJ.setEntityVelocity(coin, [0, 10, 0])
Using Jamango! with AI Code Assistants#
If you want to code Jamango! in your own local IDE, or use AI code-assist tools such as Cursor or Windsurf, you can sync your world's scripts to a folder on your computer straight from the browser editor — no extra tooling to install.
Step 1: Open the script editor in your Jamango! world#
Load your world in creator mode and press J to open the script editor.
Step 2: Click "Connect folder"#
In the editor's top bar, click Connect folder. Choose an initial sync direction (Browser → Folder to copy your current scripts out to an empty folder), then pick a local folder and grant read/write access when your browser asks.
Step 3: Edit locally with your IDE and AI tools#
Your local folder is now the source of truth, and the browser editor becomes read-only — edit the files in your IDE and changes sync back to the game automatically. The editor writes everything an AI assistant needs into the folder alongside your scripts: the jamango API types under node_modules/, this README, and rules files (CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, .windsurfrules), so Cursor, Windsurf, Claude, and friends have the full API context out of the box.
AI Code Generation First Steps#
If you are using Cursor or Windsurf, here is an example prompt to test the AI's capability with Jamango!
AI Prompt Example: Dice spawning server script#
Please write the following test script:
- Using the Jamango API - please spawn a prop with the id: prop#6CA2D1B994904AFBB3699EB2AA67D62B every 1 second in a different location in the sky (Y: 40) near spawn (0,0,0).
- When the player interacts with this prop, please add upward velocity (and some forward velocity facing away from the player) to the prop like the player is tossing the dice in the air.
- Please also give the dice some angular velocity to make the effect interesting when the player 'tosses' the dice.
- Please limit this to 30 dice in the scene total.
- Please send a chat message on every successful dice toss.
- Please review the Jamango API to consider the most appropriate implementation.
Cursor/Windsurf will automatically write code for you to review and accept or reject.
Schema System (J.schema)#
Jamango's schema system provides a type-safe way to define and validate data structures. It's particularly useful for defining traits, network messages, and configuration objects.
Basic Types#
1. Primitives#
ts// Boolean J.schema.boolean({ defaultValue: true, label: "Is Active", description: "Whether the feature is enabled" }); // Number with range and step J.schema.number({ defaultValue: 10,
2. Vectors and Matrices#
ts// 2D position J.schema.vec2({ defaultValue: [0, 0], control: "position" }); // 3D rotation J.schema.vec3({ defaultValue: [0, 1, 0], control: "direction"
Complex Types#
3. Objects and Collections#
ts// Object with nested properties J.schema.object({ position: J.schema.vec3(), rotation: J.schema.quat(), scale: J.schema.number({ defaultValue: 1 }) }); // List of items J.schema.list( J.schema.string(),
4. Union Types#
ts// Discriminated union J.schema.union("type", [ J.schema.object({ type: J.schema.literal("weapon"), damage: J.schema.number() }), J.schema.object({ type: J.schema.literal("potion"), healAmount: J.schema.number() })
Real-world Examples#
Player Trait#
tsconst PlayerTrait = J.defineTrait("Player", J.schema.object({ health: J.schema.number({ min: 0, max: 100, defaultValue: 100 }), level: J.schema.number({ min: 1, defaultValue: 1 }),
Moving Platform Trait#
tsconst MovingPlatformTrait = J.defineTrait("MovingPlatform", J.schema.object({ speed: J.schema.vec3({ label: "Movement Speed", defaultValue: [1, 0, 0] }), range: J.schema.vec3({ label: "Movement Range", defaultValue: [5, 0, 0] }), pauseDuration: J.schema.number({
Asset-based Trait Example#
- You can use
"prop" | "avatar" | "block" | "audio" | "particleSystem" | "environmentPreset" | "animation"values when identifying the asset type for the asset function parameter.
ts// Define a trait for a prop that can change its appearance const PropTrait = J.defineTrait( "Prop", J.schema.object({ // Required asset reference (e.g., "prop:wooden_crate") assetId: J.schema.asset({ label: "3D Model", description: "The 3D model to use for this prop", assetTypes: ["prop"], // Restrict to prop assets defaultValue: "prop:wooden_crate", // Default prop
Common Schema Options#
All schema types support these common options:
label?: string- Display name for the fielddescription?: string- Description of the fieldoptional?: boolean- Makes the field optionalnullable?: boolean- Allows null valueshidden?: boolean- Hides the field from UI
We will release more guides in the near future on best approaches and warnings on how best to use AI code assist in Jamango!
Tutorial - Your First Collection Mechanic#
We're going to build a simple script that spawns 10 dice at random positions that can be picked up.
Before we get going there are only two pre-requisite steps to complete this tutorial:
- Create a new World. Let's use the "Flat" world to start.
- Good news: every world comes with a built-in "Dice" prop, so you don't need to create one! Just make sure you have it.
If there's no dice in your world, you can create your own prop and just replace the name in the code with your own prop name later. To create a prop simply make a selection with the pencil, right-click and choose "Create Prop".
Part 1 – Spawning Dice#
You can now press H and open the script editor. All of the code you add today will be written in server.ts just for the purposes of this tutorial. However it is possible to write code in client.ts or make your own scripts. You just need to make sure to properly import these scripts in both execution scripts.
server.ts
ts// import all of the Jamango! API, which will below be accessible with the "J" variable. import * as J from "jamango"; // create a "on game start" hook J.onGameStart(() => { // store the id of the Dice prop (built-in to every world) const propAsset = J.assets.props["Dice"].id; // i is the current iteration, 10 is the amount of dice to spawn for (let i = 0; i < 10; i++) {
Part 2: Making Dice Collectable#
Now that dice are randomly spawning in the world, let's make them collectable. You can define Traits to attach logic to entities, or simply to identify them. We can pick any name - let's define a Player trait for players, and a Collectable trait for our dice.
We can then use these traits to create a collision listener that activates when a Player touches the Collectable.
tsimport * as J from "jamango"; // define a "player" and a "collectable" trait const PlayerTrait = J.defineTrait("Player"); const CollectableTrait = J.defineTrait("Collectable"); // when players join the game, give them the "player" trait J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait); });
Part 3: Tracking Collection Count#
Traits are very useful for many game mechanics - this is because they can also contain data.
We can use the Player trait to store a "dice" value, and increment it every time we pick up a die. Let's update the trait definition for players with this new information:
tsconst PlayerTrait = J.defineTrait("Player", J.schema.object({ dice: J.schema.number() }));
Now, when a player joins - we can set their initial dice value. We're going to set it to zero for now. Let's update the code:
ts// when players join the game, give them the "player" trait J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait, { dice: 0 }); });
Next, let's increase the player's dice counter when dice are collected. To do this, we get the trait data from the player, and increase the value. We can do this right inside the collision listener:
tsJ.onEntityCollisionStart( { source: [CollectableTrait], target: [PlayerTrait], }, (sourceId, targetId) => { // remove the die J.removeEntity(sourceId); // increase the players trait value
Part 4: Final Complete Script#
Now you've put it all together – great job! There's lots more you can do now – whether you want to add a sound effect when you pick up a coin, or update the behaviour of the dice to make them float up and down, or make them rotate - the world is your oyster with Traits and the Jamango! API.
Here's a final version of the script:
tsimport * as J from "jamango"; // define a "player" and a "collectable" trait const PlayerTrait = J.defineTrait("Player", J.schema.object({ dice: J.schema.number() })); const CollectableTrait = J.defineTrait("Collectable"); // when players join the game, give them the "player" trait with dice counter J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait, { dice: 0 }); });
Part 5: Querying All Players#
Another useful function is getAllWithTraits. For example - when the game ends, we could easily get all players, and send a chat message with their dice count:
tsconst players = J.getAllWithTraits([PlayerTrait]); for (const [id, player] of players) { const name = J.getPlayerUsername(id); J.sendChatMessage(`${name} has ${player.dice} dice!`); }
Attaching Traits to Entities with the Wrench#
In this tutorial you learned how to programmatically spawn entities and attach traits to them. However, you can also attach traits to props or characters that you have placed in edit mode using the wrench.
In order to see your traits when using the wrench, you should add some display metadata to the trait definition. We can also change our trait to contain some data, such as a "value" that could control how much score it gives a player.
tsconst CollectableTrait = J.defineTrait( "Collectable", J.schema.object({ value: J.schema.number({ label: "Value" }) }), { name: "Collectable", description: "Makes an entity collectable", color: "#ffe100", icon: "🎲", }, );
To now add this trait onto something in-world:
- Press E and drag a "Dice" prop into your action bar.
- Select the Dice and place it in the world by left clicking.
- Activate the wrench and press "Inspect" on the prop.
- Find the "Collectable" trait you defined and click on it
- Fill out the form generated from your schema to set the value.
We hope this helps you understand traits! Here are some more examples for simple game mechanics:
Examples / Snippets#
Bouncy Entity Collision#
Here's how you could make a player fly upwards after colliding with any prop or area that has the "Bouncy" trait.
tsconst BouncyTrait = J.defineTrait("Bouncy"); const PlayerTrait = J.defineTrait("Player"); J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait); }); J.onEntityCollisionStart( { source: [BouncyTrait],
Bouncy Block Type Collision#
tsconst BouncyTrait = J.defineTrait("bouncy"); const PlayerTrait = J.defineTrait("player"); J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait); }); J.onGameStart(() => { J.setTrait(J.assets.blocks["My Bouncy Block Type"].id, BouncyTrait); });
Obby Checkpoints and Hazards#
This is a script allowing you to store player progression in an obby - and reset the player if they collide with a hazard!
tsimport * as J from "jamango"; // define a trait to store checkpoints for players const PlayerCheckpointTrait = J.defineTrait("playerCheckpoint", J.schema.object({ spawn: J.schema.vec3(), checkpoint: J.schema.number(), })); // define a trait for checkpoint areas. the trait value is the checkpoint level, so that // walking over previous checkpoints does nothing.
Respawning Trapdoor Block#
Blocks can contain traits as well - let any "TrapBlock" disappear after a player collision, and respawn after some time!
tsconst PlayerTrait = J.defineTrait("Player"); const TrapdoorBlockTrait = J.defineTrait("TrapBlock"); J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait); }); J.onGameStart(() => { J.setTrait(J.assets.blocks.Trapdoor.id, TrapdoorBlockTrait); });
Moving Platform (all directions)#
tsimport * as J from "jamango"; const MovingPlatform = J.defineTrait("MovingPlatform", J.schema.object({ speed: J.schema.vec3(), range: J.schema.vec3(), goBack: J.schema.boolean(), pauseDuration: J.schema.number(), permanentRemove: J.schema.boolean({ optional: true }), }));
Swimming (Global Water Level)#
tsconst PlayerTrait = J.defineTrait("PlayerTrait"); J.onPlayerJoin((playerId) => { J.setTrait(playerId, PlayerTrait); }); const WATER_LEVEL = -1; J.onGameTick(() => { for (const [playerId] of J.getAllWithTraits([PlayerTrait])) {
Balloons Tower Defence Balloon Movement Script#
tsimport * as J from "jamango"; export const PATH: J.Vec3[] = [ [-45, 8, -264], [-52, 8, -264], [-51, 8, -272], [-47, 8, -272], [-47, 8, -283], ];
Best Practices#
Code Execution#
Jamango code runs in both edit mode and play mode. In edit mode, J.on* hooks are not called, but code is still executed so that traits defined with J.defineTrait are understood by the editor.
To ensure your code runs correctly, follow these guidelines:
- Put all gameplay logic inside hooks (e.g.,
J.onGameStart) - Never store data on globals e.g.
globalThis,document - Always use
J.uiElementfor custom UI, this is reset properly when changing between edit and play mode - Don't use
setTimeout/setInterval, prefer checkingJ.getWorldTime()inJ.onGameTickinstead - Constants and variables in module scope are ok!
ts// ❌ BAD - using setInterval, can't be properly cleaned up const SPAWN_INTERVAL_MS = 1000; setInterval(() => { J.spawnProp(J.assets.props["Dice"].id); // ... }, SPAWN_INTERVAL_MS);
ts// ✅ GOOD - runs only in play mode const SPAWN_INTERVAL = 1; // seconds let lastSpawnTime = 0; J.onGameTick((_, time) => { if (time - lastSpawnTime > SPAWN_INTERVAL) { J.spawnProp(J.assets.props["Dice"].id); // ...
API Reference#
J.net.isServer: boolean#
Shared modules can inspect whether they are running in the dedicated server or browser client bundle:
tsif (J.net.isServer) { // Server-only behavior } else { // Client-only behavior }
Networking Commands#
J.net.defineCommand<TData>(name: string) => Command<TData>#
- Environment: both
Defines a custom network command for sending data between clients and server. Returns a command object that can be used with send, sendToAll, and listen.
ts// Define a command with TypeScript data const MyCommand = J.net.defineCommand<{ value: number; message: string }>("myCommand");
J.net.send<TCommand>(command: TCommand, data: TCommand["_data"], target?: EntityId | EntityId[]) => void#
- Environment: both
Sends a command to specific targets (clients or server). If no target is specified, sends to the server.
ts// Send to server from client J.net.send(MyCommand, { value: 10, message: "Hello" }); // Send to specific player from server J.net.send(MyCommand, { value: 20, message: "Response" }, playerId); // Send to multiple players J.net.send(MyCommand, { value: 30, message: "Broadcast" }, [playerId1, playerId2]);
J.net.sendToAll<TCommand>(command: TCommand, data: TCommand["_data"], except?: EntityId) => void#
- Environment: both
Sends a command to all clients except optionally one (usually the sender).
ts// Send to all clients J.net.sendToAll(MyCommand, { value: 100, message: "Everyone" }); // Send to all except one player J.net.sendToAll(MyCommand, { value: 200, message: "Others" }, excludedPlayerId);
J.net.listen<TCommand>(command: TCommand, handler: (data: TCommand["_data"], playerId?: EntityId) => void) => () => void#
- Environment: both
Listens for a command and returns an unsubscribe function. The handler receives the data and optionally the sender's player ID.
ts// Listen for a command const unsubscribe = J.net.listen(MyCommand, (data, playerId) => { console.log("Received command:", data); if (playerId) { console.log("From player:", playerId); } }); // Stop listening when done unsubscribe();
Scene Tree#
J.sceneTree: Record<string, SceneTreeNode>#
- Environment: both
Access to scene tree nodes defined in the world editor. Each node contains position, rotation, scale, and type information.
ts// Access a scene tree node by name const node = J.sceneTree["MySpawnPoint"]; if (node) { console.log("Node position:", node.position); console.log("Node type:", node.type); // SCENE_NODE_TYPE_PROP, SCENE_NODE_TYPE_CHARACTER, or SCENE_NODE_TYPE_AREA }
J.getSceneTreeNodeEntity(sceneTreeNodeId: string) => EntityId | undefined#
- Environment: both
Gets the entity ID for a scene tree node. Returns undefined if the node doesn't exist or hasn't spawned an entity.
ts// Get the entity for a scene tree node const entityId = J.getSceneTreeNodeEntity("MySpawnPoint"); if (entityId) { // Use the entity ID J.setEntityPosition(entityId, [0, 10, 0]); }
getWorldTime() => number#
- Environment: both
Returns the current world time in seconds since the game started.
getWorldBounds() => WorldBounds#
- Environment: both
Returns the world bounds that define the playable area. The bounds are an axis-aligned bounding box (AABB) in world coordinates.
isInWorldBounds(pos: Vec3) => boolean#
- Environment: both
Checks if a position is within the world bounds. Returns true if the position is inside the playable area, false otherwise.
tsJ.onGameTick((dt, t) => { // get the global world respawn position const spawn = J.getWorldSpawn(); const entities = J.getAllCharacters(); for (const id of entities) { // set any character back to spawn in case they fall out of bounds if (!J.isInWorldBounds(J.getEntityPosition(id))) { J.setEntityPosition(id, spawn.position, true); } }
getWorldSpawn() => WorldSpawn#
- Environment: both
Returns the world's spawn point configuration, including the spawn position and rotation angle.
CharacterItem type#
tstype CharacterItem = { asset: string; transform: { position: Vec3; quaternion: Quat; scale: number; }; animations: ItemAnimations; }
ItemAnimations enum#
tsenum ItemAnimations { ONE_HANDED_TOOL, ONE_HANDED_MELEE, ONE_HANDED_RANGED_WEAPON, TWO_HANDED_RANGED_WEAPON }
getCharacterItem(id: EntityId) => CharacterItem | undefined#
Gets the item equipped by a character entity.
setCharacterItem(id: EntityId, item: CharacterItem | undefined) => void#
Sets the item equipped by a character entity. Pass undefined to unequip the item.
tsJ.onPlayerJoin((playerId) => { const swordItem: J.CharacterItem = { asset: "prop#7E8C391CCEB04E30BECDC9DBFAC5B5B1", //your prop id goes here transform: { position: [0, 0, 0], quaternion: [0, 0, 0, 1], scale: 1, }, animations: J.ItemAnimations.ONE_HANDED_MELEE };
setFirstPersonItemTransform(position: Vec3, quaternion: Quat, scale: number, lerpSpeed?: number) => void#
- Environment: client
This is used to set the transform of the first-person item (the item that appears in front of the camera when in first-person view). You can use this to create custom first-person item animations, like drawing back a bow, swinging a sword, reloading a gun, etc.
While a transform is set, you are in complete control of the first-person item position, rotation, and scale. The builtin item bobbing effects will be disabled.
clearFirstPersonItemTransform() => void#
- Environment: client
Resets the first-person item transform, and resumes the builtin item bobbing effects.
setWorldSettings(settings: Partial<WorldSettings>) => void#
- Environment: client and server
Sets world settings such as chat enabled/disabled, others in future.
tsJ.onGameStart(() => { J.setWorldSettings({ chatEnabled: false }); });
getWorldSettings() => WorldSettings#
- Environment: client and server
Returns whether the chat is enabled in the world.
setUISettings(options: Partial<UISettings>) => void#
- Environment: client
Sets the UI settings for the client. You can pass a partial object to only update specific options.
ts// hide all builtin UI elements J.setUISettings({ mobileRightButtons: false, mobileInteractButton: false, mobileEmoteButton: false, });
getUISettings() => UISettings#
- Environment: client
Returns the current UI settings for the client.
getRenderSettings() => RenderSettings#
- Environment: client
Returns the local player's current render settings:
tstype RenderSettings = { renderDistance: number; entityRenderDistance: number; pixelRatio: number; fxaa: boolean; shadows: boolean; simpleWater: boolean; };
These settings are local to each player. Use J.net if the server needs a client to report its settings.
setRenderSettings(settings: Partial<RenderSettings>) => void#
- Environment: client
Overrides the local player's render settings for the current game session. Only the supplied fields are changed, and the player's saved settings are not modified.
tsconst settings = J.getRenderSettings(); if (!settings.shadows) { J.setRenderSettings({ simpleWater: true, }); }
setCrosshair(visible?: boolean, spread?: number) => void#
-
Environment: client Controls the crosshair display and spread offset. This function only works on the client side.
-
visible- Controls whether the crosshair is visible or hidden. Default is true (visible). -
spread- Controls the crosshair spread (how far apart the crosshair lines are). Default is 0 (no spread).
ts// Hide the crosshair J.setCrosshair(false); // Show crosshair with some spread J.setCrosshair(true, 0.2);
setBlockSelector(visible?: boolean) => void#
- Environment: client
Controls the block selector display. The block selector is the box that highlights which block you are aiming at. This function only works on the client side.
visible- Controls whether the block selector is visible or hidden. Default is true (visible).
Ribbons#
Ribbons are client-only visual geometry for paths, directional markers, and rings. A ribbon is not an entity: it has no position, traits, physics, scene-tree node, or networking. Its numeric RibbonId is only valid with the ribbon functions below. Every client that should display a ribbon must create it from client.ts.
Ribbons render procedural stripes without textures. Pattern spacing follows world distance along the ribbon, including around bends. The RibbonStyle values:
backgroundColor/stripeColor- RGBA values from 0 to 1.stripeAngle- normalized from 0 to 1.0: flat, perpendicular stripes.0.5: approximately 45° stripes, or a 90° tip withstripeArrow: true.1: maximally angled, internally capped before becoming infinite.
stripeWidth- normalized from 0 to 1.0.1: stripe occupies 10% of each repetition.1: stripe color completely covers the background.
stripeArrow-false: straight angled stripes.true: mirrors the stripe angle across the ribbon center, producing chevrons/arrows.stripeSpacing- repeat distance in world units.scrollSpeed- world units per second along the point order;0is stationary, negative values reverse. Scrolling runs entirely in the fragment shader.occludedOpacity- multiplies both alpha values when the ribbon is behind opaque geometry; use0to hide occluded parts.
ts// Flat warning stripes: | | | | stripeAngle: 0, stripeArrow: false, // Diagonal warning stripes: / / / / stripeAngle: 0.5, stripeArrow: false, // Arrow pattern: > > > > stripeAngle: 0.5,
createRibbon(sectionCapacity: number, geometry: RibbonGeometry, style: RibbonStyle) => RibbonId#
- Environment: client
Creates a visible ribbon. sectionCapacity reserves the maximum number of path points or explicit sections that later geometry updates may use, up to 512 per ribbon. At most 64 ribbons and 4096 reserved sections can exist at the same time.
The high-level path geometry expands center points into a fixed-width band using the required normal. Rebuild and set the geometry each frame to keep a ribbon attached to a moving entity:
tslet playerPathRibbon: J.RibbonId; function getPlayerPathGeometry(): J.RibbonGeometry { const playerId = J.getLocalPlayer(); const position = J.getEntityInterpolatedPosition(playerId)!; return { kind: "path", points: [position, RECTANGLE_CENTER], thickness: 1,
The low-level sections geometry accepts exact left and right world-space vertices. Use it for ribbons conforming to uneven surfaces or changing width:
tsconst exactRibbon = J.createRibbon( 3, { kind: "sections", sections: [ { left: [-1, 0.02, 0], right: [1, 0.02, 0] }, { left: [-1, 0.2, 2], right: [1, 0.2, 2] }, { left: [0, 0.02, 4], right: [2, 0.02, 4] }, ], closed: false,
Set closed: true to connect the last section back to the first. Closed ribbons require at least three sections.
setRibbonGeometry(ribbonId: RibbonId, geometry: RibbonGeometry) => void#
- Environment: client
Replaces the ribbon's world-space geometry. The new geometry must fit its creation-time sectionCapacity. Updating points is how a ribbon follows an entity; entity transform functions do not apply to ribbons.
setRibbonStyle(ribbonId: RibbonId, style: RibbonStyle) => void#
- Environment: client
Replaces all stripe pattern, color, scrolling, and occlusion values. Style changes update shader data without changing ribbon positions.
setRibbonVisible(ribbonId: RibbonId, visible: boolean) => void#
- Environment: client
Shows or hides a ribbon without releasing its reserved geometry.
removeRibbon(ribbonId: RibbonId) => void#
- Environment: client
Removes the ribbon and releases its renderer allocation. Ribbon IDs become invalid after removal and all ribbons are removed when client game code reloads.
getCamera() => { position: Vec3; quaternion: Quat; fov: number; aspect: number; distance: number; }#
- Environment: client
Returns the current camera position, quaternion, vertical field of view in degrees, aspect ratio (width / height of the render viewport), and distance — how far behind the character the camera is set to sit, in world units, where 0 means first person. distance is the configured value, not the shortened one the camera falls back to when a wall is in the way.
setCameraFree(position: Vec3, rotation: Quat) => void#
- Environment: client
Sets the camera to a free-floating position with custom rotation, detached from the local player. This allows you to create cinematic camera movements, security cameras, or other custom camera behaviors.
position- The 3D world position where the camera should be placedrotation- The quaternion rotation for the camera orientation
ts// Set a fixed camera position looking at a specific area J.setCameraFree([10, 5, 10], [0, 0, 0, 1]); // Create a camera that follows an entity const cameraOffset = J.vec3.fromValues(0, 4, 10); const cameraLocalMatrix = J.mat4.fromTranslation(J.mat4.create(), cameraOffset); const rot = J.quat.create(); const pos = J.vec3.create(); const _scale = J.vec3.create(); const cameraWorldMatrix = J.mat4.create();
setLocalPlayerCamera() => void#
- Environment: client
Reattaches the camera to the local player, undoing setCameraFree. It does not change the distance, range or offset, so a camera you configured before going free comes back the way you left it.
ts// Return to normal player camera after using free camera J.setLocalPlayerCamera();
setCameraDistance(distance: number) => void#
- Environment: client
Moves the camera to a distance behind the character, in world units. There is no separate first/third person setting: 0 is first person, and any other value is third person. Values between 0 and 1 are pushed out to 1 so the camera never ends up inside the character's head.
The distance is clamped to the range set by setCameraDistanceRange, and the player's own scrolling moves it within that range afterwards, so this sets the current zoom rather than locking it.
ts// drop into first person J.setCameraDistance(0); // pull back to a wide third-person view J.setCameraDistance(8);
setCameraDistanceRange(min: number, max: number) => void#
- Environment: client
Limits how far the player may zoom with the scroll wheel (pinch on touch). Both values follow the same rule as setCameraDistance: 0 means first person, anything else is pushed out to at least 1. The current distance is clamped into the new range immediately.
min- closest the player may zoom in.0lets them reach first personmax- furthest the player may zoom out
ts// first person only, zooming disabled J.setCameraDistanceRange(0, 0); // third person only, locked at a fixed distance J.setCameraDistanceRange(5, 5); // let the player zoom across the full range (the default) J.setCameraDistanceRange(0, 20);
setCameraOffset(right: number, up: number) => void#
- Environment: client
Shifts the point the camera orbits, in world units. Use it for an over-the-shoulder camera, or to raise the view above the character's eyes.
right follows the look direction, so a positive value keeps the camera over the character's right shoulder as they turn. up stacks on top of the character's eye height. Both apply in first and third person, and the offset is included in the wall collision check, so an offset camera still gets pushed in rather than clipping through geometry.
ts// over-the-shoulder third person J.setCameraDistance(3); J.setCameraOffset(0.8, 0.3); // back to centred J.setCameraOffset(0, 0);
getPlayerUsername(playerId: number) => string | undefined#
- Environment: both
Returns the username for a player entity ID, or undefined if the entity is not a player or the entity is not in the world.
tsJ.onPlayerJoin((playerId) => { const username = J.getPlayerUsername(playerId)!; J.sendChatMessage(`Welcome to the game, ${username}!`); });
getPlayerPhotoUrl(playerId: number) => string | undefined#
- Environment: both
Returns the photo url for a player entity ID, or undefined if the entity is not a player or the entity is not in the world.
setCharacterNameplate(characterId: number, visible?: boolean, textColor?: Vec3, text?: string) => void#
- Environment: client
Sets the nameplate's options for a character entity ID (players or NPCs). Nameplates are visible by default. It only works on Client.
The text parameter allows overriding the displayed nameplate text. If null or not provided, players will show their peer username automatically, while NPCs will have no nameplate.
ts// client.ts J.onPlayerJoin((playerId) => { // enable nameplate and set green color J.setCharacterNameplate(playerId, true, [0, 1, 0]); // override player nameplate text J.setCharacterNameplate(playerId, true, undefined, "🎮 Player"); }); // Set custom nameplate for NPC
getWorldPeerCount() => number#
- Environment: both
Returns the number of peers connected to the world.
tsJ.onPlayerJoin(() => { const peerCount = J.getWorldPeerCount(); J.sendChatMessage(`player joined! ${peerCount} players in game!`); });
sendChatMessage(message: string, textColor?: string, toPlayerIds?: number[]) => void#
- Environment: both
Sends a chat message to all players or specific players. If toPlayerIds is not provided, the message is sent to all players. Called from client.ts, the message is only shown locally to that player and toPlayerIds is ignored.
ts// broadcast a message to all players when a player joins J.onPlayerJoin((playerId) => { const username = J.getPlayerUsername(playerId)!; J.sendChatMessage(`Player ${username} has joined the game!`, "#00FF00"); }); // send a private message to a specific player J.onPlayerJoin((playerId) => { J.sendChatMessage(`Psst, welcome to the game!`, "#0000FF", [playerId]); });
setPlayerPermissions: (playerId: EntityId, permissions: Partial<PlayerPermissions> => void#
- Environment: both
Sets a player's permissions.
ts// player setup J.onPlayerJoin((playerId) => { // set permissions J.setPlayerPermissions(playerId, { canFly: false, canForceRespawn: false, canUseIndividualBlocks: false, canInteract: true, }); });
getPlayerPermissions: (playerId: EntityId) => PlayerPermissions | undefined#
- Environment: both
Gets a player's permissions.
setCharacterMovementProperties: (id: EntityId, properties: Partial<CharacterMovementProperties>) => void#
Sets the movement properties for a character entity.
tsJ.onPlayerJoin((playerId) => { // set custom movement properties for the character J.setCharacterMovementProperties(playerId, { walkSpeed: 6, }); });
getCharacterMovementProperties: (id: EntityId) => CharacterMovementProperties | undefined#
- Environment: both
Gets the movement properties for a character entity. Returns undefined if the entity doesn't exist or is not a character.
setBlock(position: Vec3, shape: number, type: string) => void#
Sets the block at the specified coordinates with the given shape and type.
ts// clear the block at (0, 0, 0) J.setBlock([0, 0, 0], J.BLOCK_SHAPE_AIR, "block#ABCDEFGHIJKLMNOPQRSTUVWX"); // set a block at (0, 0, 0) J.setBlock([0, 0, 0], J.BLOCK_SHAPE_CUBE, "block#ABCDEFGHIJKLMNOPQRSTUVWX");
setBlocks(positions: Vec3[], shapes: number[], types: string[]) => void#
Sets many blocks at once, given as three parallel arrays (entry i of each array describes one block). Much faster than calling setBlock in a loop: the whole batch is applied and sent over the network as a single update. Use this whenever you change more than a handful of blocks (walls, floors, generated structures).
ts// build a 10x10 stone floor at y = 0 const positions = []; const shapes = []; const types = []; for (let x = 0; x < 10; x++) { for (let z = 0; z < 10; z++) { positions.push([x, 0, z]); shapes.push(J.BLOCK_SHAPE_CUBE); types.push("block#ABCDEFGHIJKLMNOPQRSTUVWX"); }
getBlockShape(position: Vec3) => number#
- Environment: both
Gets the shape of the block at the specified coordinates.
getBlockType(position: Vec3) => string#
- Environment: both
Gets the type of the block at the specified coordinates.
getEntityPosition(id: number) => Vec3 | undefined#
- Environment: both
Gets the position of the entity with the given ID.
setEntityPosition(id: number, position: Vec3, teleport?: boolean) => void#
- Environment: both
Sets the position of the entity with the given ID.
teleport (default true) marks the move as a discontinuous jump: the entity snaps to the new position locally and every other client snaps as well instead of interpolating towards it. This is what you want for spawn placement, respawns and teleports. Pass false for continuous movement you drive every tick (moving platforms, custom motion), so clients keep interpolating smoothly between updates.
Teleporting a player character also overrides the position that player's own client reports until it has applied the teleport.
tsconst prop = J.spawnProp(visual); J.setEntityPosition(prop, pos); // snap into place J.onGameTick(() => J.setEntityPosition(platform, nextPos, false)); // smooth per-tick motion
getEntityQuaternion(id: number) => Quat | undefined#
- Environment: both
Gets the rotation (quaternion) of the specified entity.
setEntityQuaternion(id: number, quaternion: Quat, teleport?: boolean) => void#
- Environment: both
Sets the rotation (quaternion) of the specified entity. teleport behaves exactly as in setEntityPosition: the default true snaps the rotation everywhere, false lets clients interpolate towards it for continuous per-tick rotation.
getEntityMatrix(entityId: EntityId) => Mat4 | undefined#
- Environment: both
Gets the complete transformation matrix of an entity, combining position, rotation, and scale. Returns undefined if the entity doesn't exist.
tsconst matrix = J.getEntityMatrix(entityId); if (matrix) { // Use matrix for advanced transformations const position = J.mat4.getTranslation([0, 0, 0], matrix); const rotation = J.mat4.getRotation([0, 0, 0, 1], matrix); }
Character mesh attachments#
- Environment: client — not networked. Every client that should see the attachment must call these functions.
Attach a mesh to one of the character's body slots (addCharacterMeshAttachmentProp / addCharacterMeshAttachmentGlb), or to the local first-person view (addFirstPersonMeshAttachmentProp / addFirstPersonMeshAttachmentGlb, camera-relative, shown only while the local camera is first-person on that character). Prop variants take a prop, block-structure or block id; Glb variants take an inline { id, colors? } spec. Each returns a handle for setCharacterMeshAttachment / updateFirstPersonMeshAttachment and remove…, or -1 on failure.
Slots apply per-rig retargeting so the same offset works across all rigs (defaultMale, gnome, ogre, …). Available slots: "head" | "chest" | "waist" | "armLeft" | "armRight" | "handLeft" | "handRight" | "legLeft" | "legRight" | "footLeft" | "footRight"
After scale, attachments take optional tint, glow, outline (color) and outlineSize, with the same meaning as setEntityTint, setEntityGlow and setEntityOutline. Omit them for none; attachments don't take on the character's values.
Full equip/unequip pattern#
tsconst rot = J.quat.setAxisAngle(J.quat.create(), [1, 0, 0], -1.5); let handle3p = -1; let handleFp = -1; function equip(characterId: J.EntityId) { // yellow outline, no tint or glow handle3p = J.addCharacterMeshAttachmentGlb(characterId, { id: "item_smg" }, "handRight", [0, 0, 0], rot, 1.3, undefined, 0, [1, 0.8, 0, 1], 0.05); if (characterId === J.getLocalPlayer()) { handleFp = J.addFirstPersonMeshAttachmentGlb(characterId, { id: "item_smg" }, [0.5, -0.7, -0.7], [0, 0, 0, 1], 1); }
Available item GLB ids: item_smg, item_sword, item_medShield — any id from the items_grp in the character GLB. Avatar component ids (e.g. nova_chest, aero_head) also work.
getCharacterSlotWorldMatrix(characterId: EntityId, slot: CharacterSlotId, localMatrix: Mat4) => Mat4 | undefined#
- Environment: client
Resolves a slot-local matrix into world space using the same per-rig retargeting attachments use. Returns undefined if the character has no visual or the slot is not bound on the active rig.
ts// muzzle is a 10cm forward offset on the right hand const muzzleLocal = J.mat4.fromTranslation(J.mat4.create(), [0, 0, 0.1]); const muzzleWorld = J.getCharacterSlotWorldMatrix(characterId, "handRight", muzzleLocal); if (muzzleWorld) { const origin = J.mat4.getTranslation([0, 0, 0], muzzleWorld); J.spawnProjectile("#ff0", 50, characterId, null, origin, aimDirection); }
getCharacterFirstPersonWorldMatrix(characterId: EntityId, localMatrix: Mat4) => Mat4 | undefined#
- Environment: client
Resolves a camera-local matrix into world space. Returns undefined when the local view is not first-person on that character.
First-person muzzle position#
ts// Muzzle position for shell ejection / muzzle flash (first-person only) const fpLocal = J.mat4.fromTranslation(J.mat4.create(), [0.5, -0.3, -1.8]); J.mat4.rotateY(fpLocal, fpLocal, -Math.PI / 2); const m = J.getCharacterFirstPersonWorldMatrix(characterId, fpLocal); if (m) { const pos: J.Vec3 = [m[12], m[13], m[14]]; // world position from matrix column 3 const flash = J.spawnProp(FLASH_ASSET); J.setEntityPosition(flash, pos, false); }
getEntityInterpolatedPosition(entityId: EntityId) => Vec3 | undefined#
- Environment: both
Gets the interpolated position (smoothed for rendering) of an entity. This differs from getEntityPosition as it includes client-side smoothing for visual continuity.
tsconst interpolatedPos = J.getEntityInterpolatedPosition(entityId); if (interpolatedPos) { // Use for smooth visual effects J.spawnParticlesAtPosition(interpolatedPos, "particle#12345"); }
getEntityInterpolatedQuaternion(entityId: EntityId) => Quat | undefined#
- Environment: both
Gets the interpolated quaternion (smoothed for rendering) of an entity. This provides smooth rotation values for visual effects.
tsconst interpolatedQuat = J.getEntityInterpolatedQuaternion(entityId); if (interpolatedQuat) { // Use for smooth visual calculations const forward = J.vec3.transformQuat([0, 0, 1], [0, 0, 1], interpolatedQuat); }
getEntityVelocity(id: number) => Vec3 | undefined#
- Environment: both
Gets the current velocity of the entity.
addEntityVelocity(id: number, velocity: Vec3) => void#
- Environment: both
Adds to the entity's current velocity.
setEntityVelocity(id: number, velocity: Vec3) => void#
- Environment: both
Sets the velocity of the entity.
getEntityAngularVelocity(id: number) => Vec3 | undefined#
- Environment: both
Gets the angular velocity of the entity.
addEntityAngularVelocity(id: number, angularVelocity: Vec3) => void#
- Environment: both
Adds to the entity's angular velocity.
setEntityAngularVelocity(id: number, angularVelocity: Vec3) => void#
- Environment: both
Sets the angular velocity of the entity.
moveKinematicEntity(id: number, targetPosition: Vec3, targetQuaternion: Quat) => void#
- Environment: both
Sets the linear and angular velocity of a kinematic entity such that it reaches the target position and quaternion in the next physics tick.
This API is intended to be called every tick to set new velocities. If you stop calling this method and want an entity to stop moving entirely, you should call setEntityVelocity and setEntityAngularVelocity with zero vectors.
addEntityImpulse(id: number, impulse: Vec3) => void#
- Environment: both
Applies an impulse to the entity. Supports props.
addEntityImpulseAtPoint(id: number, impulse: Vec3, point: Vec3) => void#
- Environment: both
Applies an impulse to the entity at a specific point. Supports props.
addEntityAngularImpulse(id: number, angularImpulse: Vec3) => void#
- Environment: both
Applies an angular impulse to the entity. Supports props.
getEntityMass(id: number) => number | undefined#
- Environment: both
Gets the mass of the entity. Supports characters and props.
getEntityVelocityAtPoint(id: number, point: Vec3) => Vec3 | undefined#
- Environment: both
Gets the velocity of the entity at a specific point in world space. Supports props.
addEntityForce(entityId: EntityId, force: Vec3, point: Vec3) => void#
- Environment: both
Applies a continuous force to an entity at a specific point. Unlike impulses, forces are applied continuously over time. Use this for constant effects like gravity wells, wind, or pushing objects.
ts// Apply upward force at the center of a prop J.addEntityForce(propId, [0, 50, 0], [0, 0, 0]); // Apply force at an offset point to create torque J.addEntityForce(propId, [100, 0, 0], [0, 2, 0]);
getEntityInverseInertia(entityId: EntityId) => Mat4 | undefined#
- Environment: both
Gets the inverse inertia tensor matrix of an entity. This represents how resistant the entity is to rotational acceleration. Returns undefined if the entity doesn't exist or doesn't have physics.
tsconst inertia = J.getEntityInverseInertia(propId); if (inertia) { // Use the inertia matrix for physics calculations }
getEntityBodyActive(id: number) => boolean | undefined#
- Environment: both
Gets whether the entity's physics body is currently active. Supports props.
An entity may become "inactive" or "go to sleep" if it has been stationary for a while.
Usually applying an impulse, or changing the entity's velocity will automatically reactivate the body. In rare cases you may want to manually activate the body using activateEntityBody however.
activateEntityBody(id: number) => void#
- Environment: both
Activates the entity's physics body. Supports props.
getEntityScale(id: number) => number | undefined#
- Environment: both
Gets the scale of the entity.
getEntityBlockCollision(id: number) => Vec3[]#
- Environment: both
Gets the positions of blocks that the entity is currently colliding with. Returns an empty array if the entity doesn't exist or is not a physical entity.
ts// spawn a prop that can collide with blocks const propId = J.spawnProp("prop#ABCDE12345"); // check which blocks the entity is colliding with const collisions = J.getEntityBlockCollision(propId); collisions.forEach((position) => { console.log(`Entity is colliding with block at (${position[0]}, ${position[1]}, ${position[2]})`); // change each colliding block to a red block J.setBlock(position, J.BLOCK_SHAPE_CUBE, "block#ABCDE12345");
setEntityGravityFactor(entityId: EntityId, gravityFactor: number) => void#
- Environment: both
Sets how much gravity affects an entity. 0 = no gravity, 1 = normal gravity, 2 = double gravity. Use this for anti-gravity zones, moon physics, or custom gravity effects.
ts// Make an entity float (no gravity) J.setEntityGravityFactor(propId, 0); // Make an entity heavy (double gravity) J.setEntityGravityFactor(propId, 2); // Moon gravity (1/6 Earth gravity) J.setEntityGravityFactor(propId, 0.166);
setEntityScale(id: number, scale: number) => void#
- Environment: both
Sets the scale of the entity.
setEntityTint(id: number, tint: Vec4) => void#
- Environment: client
Sets the tint color for a prop or character entity. The tint is applied as a color overlay that is mixed with the final color based on the alpha component. Works on prop and character (creature/player) entities; other entity kinds are ignored. On characters and GLB-component props the tint color is quantized to 4 bits per channel (fine for flashes/overlays).
ts// set a R = 50%, G = 30%, B = 20% tint, mixed in by 50% J.setEntityTint(propId, [0.5, 0.3, 0.2, 0.5]); // make an entity appear completely blue (full blue with 100% mix) J.setEntityTint(entityId, [0.0, 0.0, 1.0, 1.0]); // remove tinting (color is irrelevant, as it is mixed by 0%) J.setEntityTint(entityId, [1.0, 1.0, 1.0, 0.0]);
setEntityGlow(id: number, glow: number) => void#
- Environment: client
Sets the glow multiplier for a prop or character entity. The glow value is a float from 0 to 1 that controls how much the entity ignores darkness and shadows. Changing this can allow your entity to visually stand out. Works on prop and character (creature/player) entities; other entity kinds are ignored. Default is 0.
ts// make an entity have no glow (fully affected by lighting) J.setEntityGlow(entityId, 0.0); // make the entity glow fully (ignore lighting completely) J.setEntityGlow(entityId, 1.0);
setEntityOutline(id: number, size: number, color: Vec4) => void#
- Environment: client
Sets the outline size and color for a prop or character entity. size is clamped from 0 to 1, and the color components are clamped from 0 to 1 when rendered. A size of 0.25 draws a thick outline; larger sizes are only useful on small objects. Set the size or color alpha to 0 to remove the outline. Outlines are local visual state, so each client that should see one must set it from client.ts.
ts// add a blue outline J.setEntityOutline(entityId, 0.035, [0.15, 0.75, 1.0, 1.0]); // remove the outline J.setEntityOutline(entityId, 0, [0, 0, 0, 0]);
setEntityVisible(id: number, isVisible: boolean) => void#
- Environment: client
Sets the visibility of an entity. When set to false, the entity will be hidden from view. This is a client-side only concept - the entity still exists and functions normally on the server, but is not rendered on the client.
ts// spawn entity const propId = J.spawnProp(J.assets.props.MyCoin.id); // hide entity J.setEntityVisible(propId, false);
setEntityVisualOffset(id: number, offset: Vec3) => void#
- Environment: client
Applies a visual-only position offset to an entity's rendered position. This offset is purely cosmetic and does not affect physics, collisions, or networking. Perfect for juice effects like floating, bobbing, or screen shake.
ts// make a prop float up and down J.onGameRender(() => { const bobAmount = Math.sin(J.getWorldTime() * 0.003) * 0.2; J.setEntityVisualOffset(propId, [0, bobAmount, 0]); }); // offset a character slightly to the right J.setEntityVisualOffset(characterId, [0.5, 0, 0]); // clear the offset
getEntityVisualOffset(id: number) => Vec3 | undefined#
- Environment: client
Gets the current visual offset applied to an entity. Returns undefined if the entity does not exist.
tsconst offset = J.getEntityVisualOffset(propId); if (offset) { console.log(`Offset: x=${offset[0]}, y=${offset[1]}, z=${offset[2]}`); }
setEntityVisualQuaternion(id: number, quaternion: Quat) => void#
- Environment: client
Applies a visual-only rotation (as a quaternion) on top of the entity's actual rotation. This rotation is purely cosmetic and does not affect physics, collisions, or networking. Perfect for spin effects, wobbles, or other visual rotations.
The visual quaternion is relative, and composed in the entity's local space:
renderedRotation = entityRotation x visualQuaternion
so it turns the entity around its own axes, not the world's. To face a fixed world direction, pass inverse(entityRotation) x desiredWorldRotation.
It stays applied every frame until you change it. Set it back to identity [0, 0, 0, 1] to turn it off; an effect that blends out to identity releases itself, no cleanup call needed.
ts// create a spinning effect (you'll need to update this each frame) J.onGameRender(() => { const rotation = (J.getWorldTime() * 0.001) % (Math.PI * 2); const halfRotation = rotation * 0.5; J.setEntityVisualQuaternion(propId, [0, Math.sin(halfRotation), 0, Math.cos(halfRotation)]); }); // apply a 45-degree tilt const halfTilt = Math.PI / 8; J.setEntityVisualQuaternion(propId, [0, 0, Math.sin(halfTilt), Math.cos(halfTilt)]);
getEntityVisualQuaternion(id: number) => Quat | undefined#
- Environment: client
Gets the current visual quaternion applied to an entity. Returns undefined if the entity does not exist.
tsconst quat = J.getEntityVisualQuaternion(propId); if (quat) { console.log(`Rotation: x=${quat[0]}, y=${quat[1]}, z=${quat[2]}, w=${quat[3]}`); }
setEntityVisualScale(id: number, scale: Vec3) => void#
- Environment: client
Applies a visual-only scale multiplier to an entity. The scale is applied component-wise (x, y, z) on top of the entity's actual scale. This is purely cosmetic and does not affect physics, collisions, or networking. Perfect for squash & stretch effects, impact animations, or other visual feedback.
ts// make a prop appear 50% larger visually (without affecting physics) J.setEntityVisualScale(propId, [1.5, 1.5, 1.5]); // non-uniform scale for a pancake effect J.setEntityVisualScale(propId, [1.5, 0.2, 1.5]); // clear the visual scale J.setEntityVisualScale(propId, [1, 1, 1]);
getEntityVisualScale(id: number) => Vec3 | undefined#
- Environment: client
Gets the current visual scale applied to an entity. Returns undefined if the entity does not exist.
tsconst scale = J.getEntityVisualScale(propId); if (scale) { console.log(`Scale: x=${scale[0]}, y=${scale[1]}, z=${scale[2]}`); }
entityLookAt(id: number, target: Vec3) => void#
- Environment: both
Makes an entity look at a specific position. For character entities, uses spherical coordinates for camera control. For other entities, rotates them to face the target point.
ts// spawn entities const characterId = J.spawnCharacter("avatar#ABCDE12345"); const propId = J.spawnProp("prop#ABCDE12345"); // make the character look at a specific position (uses camera control) J.entityLookAt(characterId, [10, 5, 20]); // make a prop face a specific position (rotates the prop) J.entityLookAt(propId, [0, 0, 10]);
characterJump(characterId: EntityId, velocity: number, force: boolean, teleport: boolean) => void#
- Environment: both
Makes a character entity jump with the specified velocity. Only works on character entities (players and NPCs). With teleport the new velocity is applied to a player even if that player's client reports otherwise; without it a player's own simulation may override the jump.
ts// make all players jump with high velocity const players = J.getAllPlayers(); players.forEach((playerId) => { J.characterJump(playerId, 15.0, true, false); });
characterUse: (id: EntityId, primary: boolean, secondary: boolean) => void#
- Environment: both
Makes a character entity use an item or perform an action. The primary and secondary parameters indicate which action to perform.
tsJ.onGameStart(() => { // spawn a character const characterId = J.spawnCharacter(J.assets.avatars.MyAvatar.id); // make the character perform a primary use action J.characterUse(characterId, true, false); // make the character perform a secondary use action J.characterUse(characterId, false, true); });
getCharacterCrouching: (characterId: EntityId) => boolean#
- Environment: both
Returns whether the character is currently crouching.
setCharacterCrouching: (characterId: EntityId, crouching: boolean) => void#
- Environment: both
Sets whether the character is crouching. This only works on NPC character entities.
setCharacterMoveTarget: (id: EntityId, targets: Vec3[]) => void#
- Environment: both
Sets the movement targets for a character entity. The character will move towards these targets in order.
tsJ.onGameStart(() => { // spawn a character const characterId = J.spawnCharacter(J.assets.avatars.MyAvatar.id); // set movement targets for the character const targets: J.Vec3[] = [ [10, 0, 10], [20, 0, 20], [30, 0, 30], ];
clearCharacterMoveTarget: (id: EntityId) => void#
- Environment: both
Clears the movement targets for a character entity. The character will stop moving towards any targets.
ts// clear the movement targets for a character J.clearCharacterMoveTarget(characterId);
setCharacterLookAtTarget: (id: EntityId, target: Vec3) => void#
- Environment: both
Sets the "look at" target for a character.
If a character also has a "move target" it will continue looking at the "look at target" while moving to the next "move target".
clearCharacterLookAtTarget: (id: EntityId) => void#
- Environment: both
Clears the "look at" target for a character.
characterPlayAnimation: (id: EntityId, clip: string, opts?: { mode?: "loop" | "once" | "hold"; speed?: number; fadeIn?: number; cancelOnMove?: boolean }) => void#
- Environment: both
Plays a GLB animation on a character controlled by the current runtime. Clients control their local player and client-created characters; the server controls server-created NPCs. Calling this for a replayed character throws. The controller's animation state replicates to every observer, and replaying the same clip starts it again. The clip's playback mode is derived from the clip name:
- clips with no suffix are base clips — they occupy the character's single base slot, replacing whichever locomotion or emote clip would otherwise play. Playing a new base clip automatically clears any previous base clip.
- clips ending in
_addare additive — they blend on top of the base (and other additives) as a delta from the bind pose. Useful for hit reactions, lean, impulses, etc. - clips ending in
_overare override — they play concurrently with the base at full weight._overclips are authored with partial-bone tracks, so e.g.twoHanded_idle_overonly affects arms while legs keep following locomotion.
Every clip name present in the GLB is available via J.assets.animations.<name>.id (locomotion, emotes, _add, and _over — anything the mixer can resolve).
Options:
-
mode— what the clip does when it reaches its end:"loop"— restart from the beginning, indefinitely."once"— play through once, then clear the clip. A base clip returns the base slot to locomotion; additive/override layers stop."hold"— play through once, then freeze on the final frame until the clip is explicitly stopped (characterStopAnimation/clearCharacterAnimations) or replaced. Ideal for death poses, knockouts, etc.
Defaults to
"loop"for base/override clips and"once"for additive clips. -
speed— playback rate multiplier (default1). Negative values start at the end and play backwards;0pauses the animation. -
fadeIn— crossfade-in duration when the clip starts. Defaults to0.1sfor base clips,0for additive/override. -
cancelOnMove— whentrue, the controller drops the animation when its resolved locomotion leaves idle. Useful for emote-style base clips.
ts// server.ts can animate a server-controlled NPC. const npcId = J.spawnCharacter("avatar#example"); // looping dance emote, auto-cancel when the player moves J.characterPlayAnimation(npcId, J.assets.animations.emotes_danceBasic.id, { cancelOnMove: true, }); // one-shot hit reaction on top of whatever locomotion is playing J.characterPlayAnimation(npcId, "locomotion_default_hit_add");
characterSetAnimationSpeed: (id: EntityId, clip: string, speed: number) => void#
- Environment: both
Changes the playback speed of an active animation without restarting it. Use a negative speed to reverse direction, 0 to pause, and a positive speed to play forwards. Has no effect if the clip is not active.
tsJ.characterSetAnimationSpeed(npcId, J.assets.animations.emotes_danceBasic.id, -1);
characterStopAnimation: (id: EntityId, clip: string, opts?: { fadeOut?: number }) => void#
- Environment: both
Stops a specific animation clip on a character. Has no effect if the clip isn't currently playing. For base clips, stopping the clip returns the base slot to whatever locomotion resolves to next frame.
tsJ.characterStopAnimation(playerId, "twoHanded_idle_over", { fadeOut: 0.2 });
clearCharacterAnimations: (id: EntityId) => void#
- Environment: both
Stops every animation currently playing on the character — base, additive, and override. Locomotion takes over the base slot again on the next frame.
tsJ.clearCharacterAnimations(playerId);
getCharacterViewRay(id: number) => { origin: Vec3; direction: Vec3; hitEntityId: number | undefined; hitBlock: Vec3 | undefined } | undefined#
- Environment: both
Gets the current view ray of the character.
getCharacterInput: (id: EntityId) => object | undefined#
- Environment: both
Gets the input object for a character entity. The input object contains all current input states including movement, camera orientation, and action states.
tsconst playerId = J.getLocalPlayer(); const input = J.getCharacterInput(playerId)!; console.log("Player input:", input); // access input properties like input.nipple, input.camera, etc.
getPointer: () => object#
- Environment: client
Returns the screen-space pointer state — the mouse on desktop, the primary touch on mobile: { x, y, isDown, justPressed, justReleased, locked, touch }. Positions are CSS pixels with a top-left origin, the same coordinate space as the onGameUiCanvas context and getScreenPosition. justPressed and justReleased are true only on the frame the press or release happened, and on a release frame x/y still hold the release position, so release-based click checks work. While locked (pointer lock, i.e. gameplay mouselook) there is no cursor, so skip hit-testing. On mobile touch is true and x/y are -1, -1 while no touch is active.
This is primarily relevant for interactive canvas UI drawn with onGameUiCanvas — poll it every frame and hit-test your rects against it. HTML-based UI usually doesn't need it: elements under uiElement receive normal DOM pointer events directly.
tsJ.onGameUiCanvas((ctx, width, height) => { const p = J.getPointer(); const x = width - 120, y = height - 60, w = 100, h = 40; const hover = !p.locked && p.x >= x && p.x < x + w && p.y >= y && p.y < y + h; ctx.fillStyle = hover ? "#3d8f27" : "#2e6e1c"; ctx.fillRect(x, y, w, h); if (hover && p.justPressed) buyItem(); });
setPointerLock: (state: "auto" | "unlocked" | "locked") => void#
- Environment: client
Controls pointer lock (gameplay mouselook). "auto" is the default: a click on the game canvas grabs pointer lock. "unlocked" releases lock now and suppresses the automatic re-lock, so clicks no longer grab lock and mouse movement no longer drives the camera — getPointer().locked and getCharacterInput().pointerLocked stay false while the cursor keeps updating. "locked" grabs lock now and resumes normal mouselook. Those two flags diverge under setMouseMode("pointerFree"): the cursor is free so getPointer().locked is false, but pointerLocked stays true because gameplay input is still running. No-op on touch / mobile devices. Prefer this over document.exitPointerLock() / element.requestPointerLock() so the engine's own re-lock stays in sync. The state resets to "auto" on world load.
ts// free the cursor for an interactive canvas shop, then restore gameplay J.setPointerLock("unlocked"); J.setCursor("default"); // ...later, when the shop closes: J.setPointerLock("auto");
setMouseMode: (mode: "pointerLocked" | "pointerFree") => void#
- Environment: client
Chooses how the mouse is handled during gameplay. "pointerLocked" is the default shooter handling: the first click grabs pointer lock and the mouse steers the camera from then on, with no cursor on screen and the right button free for the held item. "pointerFree" leaves the player a usable cursor and only steers the camera while the right mouse button is held — movement, items and the left button stay live throughout, but the right button belongs to the camera and no longer fires the item's secondary action. Desktop only; touch devices have no cursor. The mode resets to "pointerLocked" on world load.
This is orthogonal to setPointerLock, which stays the way to hand the pointer to interactive UI: setPointerLock("unlocked") disables gameplay input in either mouse mode.
ts// point-and-click game: the player keeps a cursor and right-drags to look around J.onGameStart(() => { J.setMouseMode("pointerFree"); });
setCursor: (css: string) => void#
- Environment: client
Sets the CSS cursor shown over the game area (any valid cursor value — "default", "pointer", "grab", "grabbing", …). Safe to call every frame. No effect while the pointer is locked, since the cursor is hidden during mouselook.
tsJ.onGameUiCanvas((ctx, width, height) => { const p = J.getPointer(); const hover = !p.locked && p.x < 120 && p.y < 40; J.setCursor(hover ? "pointer" : "default"); });
setCharacterMovementInputEnabled: (id: EntityId, enabled: boolean) => void#
- Environment: both
Enables or disables movement input processing for a character entity. When disabled (false), the character will not respond to movement input but other input (like camera) will still work. This is useful for taking control of player input to drive other entities like platforms or vehicles.
ts// disable player movement input J.setCharacterMovementInputEnabled(playerId, false) // use player's movement input to control a platform const input = J.getCharacterInput(playerId); const platformSpeed = 5; const velocity = [ input.nipple.x * platformSpeed, // left/right movement 0, // no vertical movement
setRagdoll: (id: EntityId, enabled: boolean) => void#
- Environment: both
Enables ragdoll physics for a character. Disabling it restores movement and blends back to animation.
Call ragdoll functions from the runtime that simulates the character.
addRagdollImpulse: (id: EntityId, origin: Vec3, radius: number, impulse: number) => void#
- Environment: both
Pushes an enabled ragdoll away from a world-space origin, with strength fading to zero at radius.
tsJ.setRagdoll(characterId, true); J.addRagdollImpulse(characterId, impactOrigin, 8, 700);
setCharacterVisualFacingMode: (id: EntityId, mode: "movement" | "camera") => void#
- Environment: client
Sets how a character's rendered body decides which way to face. This is a local rendering preference and is never networked — each client controls it independently.
"movement"(default) — the body faces the steering/movement direction, so strafing or back-pedaling turns the body toward where the character is actually moving."camera"— the body faces the camera/look angle, staying aligned with where the character is aiming regardless of movement direction.
ts// lock the body to face the aim direction (e.g. while drawing a bow or aiming down iron sights, scoping in) J.setCharacterVisualFacingMode(playerId, "camera"); // restore the default steering-based facing J.setCharacterVisualFacingMode(playerId, "movement");
getCharacterPhotoUrl(entityId: EntityId) => string | undefined#
- Environment: both
Returns the photo url for a character entity ID, or undefined if the entity is not a character.
getCharacterAvatar(entityId: EntityId) => CharacterAvatarConfig | undefined#
- Environment: both
Returns the character's current avatar config (with all modifications), or undefined if the entity isn't a character.
tsconst avatar = J.getCharacterAvatar(playerId); if (avatar) { console.log("Rig:", avatar.rigId); console.log("Skin:", avatar.skinColorPrimary); console.log("Hair:", avatar.hairColor); console.log("Head components:", avatar.components.head.map(c => c.id)); }
getPlayerBaseAvatar(entityId: EntityId) => CharacterAvatarConfig | undefined#
- Environment: both
Returns the player's original avatar, the one stored when they joined the server, before any script modifications. Returns undefined if the entity isn't a player character. Use this to reset a player back to their base look.
ts// Reset a player back to their base look after a costume swap. const base = J.getPlayerBaseAvatar(playerId); if (base) J.setCharacterAvatarByConfig(playerId, base);
setCharacterAvatarById(entityId: EntityId, avatarAssetId: string) => void#
- Environment: both
Loads a pre-made avatar asset by ID and sets it as the character's avatar.
tsJ.setCharacterAvatarById(playerId, J.assets.avatars.CoolAvatar.id);
setCharacterAvatarByConfig(entityId: EntityId, avatarConfig: CharacterAvatarConfig) => void#
- Environment: both
setCharacterAvatarByConfig(entityId: EntityId, avatarConfig: CharacterAvatarConfig) => void#
- Environment: both
Replaces the character's full avatar config. Use this to set top-level values like hair color, skin color, and face features. Components are replaced entirely—use addCharacterAvatarConfig for additive layering instead.
Warning: Assigning avatar.components.<slot> = [...] fully overwrites that slot and removes default components. Use J.addCharacterAvatarConfig to add costumes or equipment without losing base components.
ts// Change hair and eye color, keep other parts const avatar = J.getCharacterAvatar(entityId); avatar.hairColor = "#ff0000"; avatar.face.eyeId = "Eye_Glow.png"; avatar.anim.idle = J.assets.animations.locomotion_happy_idle.id; J.setCharacterAvatarByConfig(entityId, avatar); // Reset player to their original platform avatar const baseAvatar = J.getPlayerBaseAvatar(entityId); J.setCharacterAvatarByConfig(entityId, baseAvatar);
addCharacterAvatarConfig(entityId: EntityId, config: PartialCharacterAvatarConfig) => void#
- Environment: both
Layers a partial config on top of the character's current avatar.
- Omitted = untouched. Fields you don't pass stay as-is, at every level.
- Components stack per slot.
components.chest: [x]addsxalongside what's already there (doesn't replace). Duplicateids per slot are skipped.
Everything else (rigId, colors, face fields) simply overwrites when you pass it.
Animation overrides currently support anim.idle, anim.run, anim.walk, and anim.crouch; set any of them to a GLB clip name or null to use the default rig animation.
ts// Character starts with the player's base avatar (e.g. chest slot = [default_chest]) J.onPlayerJoin((playerId) => { J.addCharacterAvatarConfig(playerId, { hairColor: "#000000", // overwrites hair face: { eyeId: "Eye_Editor.png" }, // overwrites just eyeId; other face fields stay components: { chest: [{ id: "aero_chest", colors: { "01": "#ffffff" } }], // chest slot → [default_chest, aero_chest] legLeft: [{ id: "aero_legLeft", colors: { "01": "#fff333", "02": "#FFFFFF" } }], legRight: [{ id: "aero_legRight", colors: {} }], },
ts// Zombie mode, change skin + face, components untouched J.addCharacterAvatarConfig(playerId, { skinColorPrimary: "#00ff00", anim: { run: J.assets.animations.locomotion_zombie_run.id }, face: { eyeId: "Eye_M_Void.png" }, }); // Change a player's locomotion style. Use J.assets.animations for autocomplete // over every GLB animation clip available in the character rig. J.addCharacterAvatarConfig(playerId, {
getCharacterAlive(characterId: EntityId) => boolean | undefined#
- Environment: both
Checks if a character is alive. Returns undefined if the character doesn't exist.
tsconst alive = J.getCharacterAlive(playerId); if (alive === false) { // Character is dead, respawn them J.respawnCharacter(playerId); }
setCharacterAlive(characterId: EntityId, alive: boolean) => void#
- Environment: both
Sets whether a character is alive. Set to false to "kill" a character, true to revive them.
ts// Kill a character J.setCharacterAlive(playerId, false); // Revive a character J.setCharacterAlive(playerId, true);
getCharacterSprinting(characterId: EntityId) => boolean#
- Environment: both
Gets whether a character is currently sprinting (running faster than normal).
tsconst isSprinting = J.getCharacterSprinting(playerId); if (isSprinting) { // Apply stamina drain drainStamina(playerId); }
setCharacterSprinting(characterId: EntityId, sprinting: boolean) => void#
- Environment: both
Sets whether a character should be sprinting. Use this for custom sprint mechanics or forced movement states.
ts// Force character to sprint J.setCharacterSprinting(playerId, true); // Stop character from sprinting J.setCharacterSprinting(playerId, false);
raycast: (origin: Vec3, direction: Vec3, distance: number, ignoreEntityIds?: EntityId[]) => { hit: boolean, hitBlock: Vec3 | undefined, hitEntityId: EntityId | undefined, hitDistance: number, hitPosition: Vec3 | undefined, hitNormal: Vec3 | undefined }#
- Environment: both
Performs a raycast from the given origin in the specified direction for a certain distance. Returns an object with details about the hit.
tsJ.onGameStart(() => { const origin = [0, 0, 0]; // starting point of the ray const direction = [1, 0, 0]; // direction the ray should travel in const distance = 10; // the maximum distance the ray should travel const ignoreEntityIds = []; // optional array of entity IDs to ignore in the raycast, e.g. a player's own character const result = J.raycast(origin, direction, distance, ignoreEntityIds); console.log( "Raycast result:",
spawnCharacter(avatarAssetId: string) => number#
- Environment: both
Spawns a new character using the given asset.
spawnProp(propAssetId: string) => number#
- Environment: both
Spawns a new prop using the given asset.
updatePropAsset(entityId: EntityId, assetId: string | undefined) => void#
- Environment: both
Updates the asset of an existing prop entity. Only works on entities that are props.
ts// spawn a prop const propId = J.spawnProp("prop#ABCDE12345"); // change the prop's asset to a different one J.updatePropAsset(propId, "prop#FGHIJ67890"); // make the prop invisible by setting asset to undefined J.updatePropAsset(propId, undefined);
setEntityPhysicsEnabled(entityId: EntityId, enabled: boolean) => void#
- Environment: both
Enables or disables physics for the given entity. Supports props and characters.
Note that disabling physics will prevent it from being hit by raycasts or being detected by sensors. Use this when an entity should be entirely non-physical.
ts// spawn a prop const propId = J.spawnProp("prop#ABCDE12345"); // disable physics for the prop J.setEntityPhysicsEnabled(propId, false);
getEntityPhysicsEnabled(entityId: EntityId) => boolean | undefined#
- Environment: both
Returns whether physics is enabled for the given entity. Returns undefined if the entity doesn't exist.
getPropAsset(entityId: EntityId) => string | undefined#
- Environment: both
Returns the current prop asset for the given prop entity.
updatePropPhysicsProperties(entityId: EntityId, properties: { motionType?: MotionType; sensor?: boolean; scale?: number; mass?: number; friction?: number; restitution?: number; gravityFactor?: number; }) => void#
- Environment: both
Updates the physics properties of an existing prop entity. Only works on entities that are props. All properties in the object are optional.
ts// spawn a prop const propId = J.spawnProp("prop#ABCDE12345"); // make the prop dynamic and bouncy J.updatePropPhysicsProperties(propId, { motionType: J.MOTION_TYPE_DYNAMIC, sensor: false, scale: 1.0, mass: 5.0, friction: 0.1,
getPropPhysicsProperties(entityId: EntityId) => { motionType: MotionType; sensor: boolean; scale: number; mass: number; friction: number; restitution: number; gravityFactor: number; } | undefined#
- Environment: both
Gets the current physics properties of a prop entity. Returns undefined if the entity doesn't exist or is not a prop. The returned object is a shallow copy that can be modified and passed to updatePropPhysicsProperties.
ts// spawn a prop const propId = J.spawnProp("prop#ABCDE12345"); // get current physics properties const properties = J.getPropPhysicsProperties(propId); if (properties) { console.log("Current properties:", properties); // modify properties and update (get -> edit -> set pattern) properties.mass *= 2; // double the mass
spawnParticles(particlesAssetId: string) => number#
- Environment: client
Spawns a particle effect using the given asset.
spawnBoxArea(sizeX: number, sizeY: number, sizeZ: number) => number#
- Environment: both
Spawns a box-shaped area with the given dimensions.
spawnSphereArea(radius: number) => number#
- Environment: both
Spawns a spherical area with the given radius.
removeEntity(id: number) => void#
- Environment: both
Removes the specified entity.
getAreaEntityDesc: (id: EntityId) => undefined | { type: "box", extents: Vec3 } | { type: "sphere", radius: number }#
- Environment: both
Gets an area entity's description, which includes its shape type and shape settings. Returns undefined if the entity is not an area entity.
ts// spawn a box area const areaId = J.spawnBoxArea(5, 5, 5); // get the area entity description const areaDesc = J.getAreaEntityDesc(areaId); console.log("Area Description:", areaDesc); // { type: "box", extents: [5, 5, 5] }
setAreaEntityDesc: (id: EntityId, desc: { type: "box", extents: Vec3 } | { type: "sphere", radius: number }) => void#
- Environment: both
ts// spawn a box area const areaId = J.spawnBoxArea(5, 5, 5); // later, resize the area to different dimensions J.setAreaEntityDesc(areaId, = { type: "box", extents: [10, 10, 10], }); // change the box area to a sphere with a radius of 12
setEntitySphereArea(entityId: number, radius: number) => void#
- Environment: both
Changes an existing area entity to a sphere shape with the specified radius.
ts// spawn a sphere area const areaId = J.spawnSphereArea(5); // later, resize the sphere to a different radius J.setEntitySphereArea(areaId, 12);
getBlocksInSphere(center: Vec3, radius: number) => Vec3[]#
- Environment: both
Returns all block positions within a spherical area centered at the given position.
tsconst blocks = J.getBlocksInSphere([0, 0, 0], 5); blocks.forEach((position) => { J.setBlock(position, J.BLOCK_SHAPE_CUBE, "block#ABCDE12345"); });
getBlocksInBox(min: Vec3, max: Vec3) => Vec3[]#
- Environment: both
Returns all block positions within a box area between the min and max coordinates.
tsconst blocks = J.getBlocksInBox([-5, 0, -5], [5, 10, 5]); blocks.forEach((position) => { J.setBlock(position, J.BLOCK_SHAPE_AIR, "block#ABCDE12345"); });
getEntitiesInSphere(center: Vec3, radius: number) => number[]#
- Environment: both
Gets all entities within a given spherical area.
defineTrait(name: string, schema?: Schema, meta?: any) => string & { __T: T }#
- Environment: both
Defines a new trait with optional properties. The trait can be registered only once with a specific name.
setTrait(target: string | number, trait: T, value?: T["__T"]) => void#
- Environment: both
Sets a trait on a target entity or object.
getTrait(target: string | number, trait: T) => T["__T"] | null#
- Environment: both
Gets the value of a trait from a target.
removeTrait(target: string | number, trait: T) => void#
- Environment: both
Removes a trait from a target.
getAllWithTraits(traits: T[]) => Array<[id: any, ...traits: T["__T"][]]>#
- Environment: both
Gets all entities with the specified traits.
playSound(audioAssetId: string, options: { volume?: number; detune?: number; fadeInDuration?: number; loop?: boolean }) => number | undefined#
- Environment: client
Plays a sound using the specified asset and options.
ts// play a sound globally J.playSound(J.assets.audio.MySound.id, { volume: 0.5, }); // play looping background music J.playSound(J.assets.audio.BackgroundMusic.id, { volume: 0.3, loop: true, });
playSoundAtPosition(audioAssetId: string, position: Vec3, options: { volume?: number; detune?: number; fadeInDuration?: number; loop?: boolean }) => number | undefined#
- Environment: client
Plays a spatial sound at a given position using the specified asset and options.
ts// play a sound at a specific position in the world J.playSoundAtPosition(J.assets.audio.MySound.id, [10, 5, -10], { volume: 0.5, loop: false, }); // play looping ambient noise at a specific position J.playSoundAtPosition(J.assets.audio.AmbientNoise.id, [0, 0, 0], { volume: 0.2, loop: true,
playSoundAtEntity(audioAssetId: string, options: { volume?: number; detune?: number; fadeInDuration?: number; loop?: boolean }) => number | undefined#
- Environment: client
Plays a spatial sound at the position of a specific entity using the specified asset and options.
ts// play a sound at the position of a specific entity J.playSoundAtEntity(J.assets.audio.Whistle.id, entityId, { volume: 0.5, loop: false, });
setSoundVolume(soundId: number, volume: number, fadeDuration?: number) => void#
- Environment: client
Sets the volume of a playing sound, optionally fading to it over fadeDuration seconds.
tsconst musicId = J.playSound(J.assets.audio.BackgroundMusic.id, { loop: true, fadeInDuration: 2 }); // duck the music over 1.5 seconds, e.g. while a dialog plays J.setSoundVolume(musicId, 0.1, 1.5);
setSoundFilter(soundId: number, type: "lowpass" | "highpass", frequency: number, fadeDuration?: number) => void#
- Environment: client
Applies a lowpass or highpass filter to a playing sound, sweeping the cutoff frequency to frequency (in Hz) over fadeDuration seconds. Calling it again retargets the sweep, so filters can transition as the game plays.
ts// muffle the music while the player is underwater J.setSoundFilter(musicId, "lowpass", 400, 0.5); // back to (almost) unfiltered J.setSoundFilter(musicId, "lowpass", 24000, 0.5);
clearSoundFilter(soundId: number) => void#
- Environment: client
Removes the filter from a playing sound immediately. For a smooth exit, sweep the filter to an inaudible frequency first (24000 Hz for lowpass, 10 Hz for highpass).
stopSound(soundId: number) => void#
- Environment: client
Stops a playing sound.
restartGame() => void#
- Environment: server
Resets the world to it's initial state, and triggers onPlayerJoin for all players.
getWorldOptions() => Record<string, string | number | boolean>#
- Environment: both
Returns the creator-defined world options that were set at room creation time. Returns a copy of the options, so mutations don't affect the world. Use this when you need to read world options outside of onGameStart.
tsJ.onGameTick(() => { const options = J.getWorldOptions(); if (options.gameMode === "deathmatch") { // ... } });
onGameStart(fn: (worldOptions: Record<string, string | number | boolean>) => void) => void#
- Environment: both
Registers a handler for when the game starts. The handler receives the creator-defined world options that were set at room creation time.
onGameTick(fn: (deltaTime: number, time: number) => void) => void#
- Environment: both
Registers a handler for each game tick.
onGameRender(fn: (deltaTime: number, time: number) => void) => void#
- Environment: client
Registers a handler for each game render. This only runs on the client side. Runs at the client's render framerate and receives the render-frame deltaTime and world time.
This is typically used with J.getScreenPosition to position UI relative to the 3D world at the same FPS as the game's rendering.
onGameUiCanvas(fn: (ctx: CanvasRenderingContext2D, width: number, height: number, deltaTime: number, time: number) => void) => void#
- Environment: client
Registers a handler for drawing 2D overlay UI (HUDs, debug text, minimaps) with the canvas 2D API. The handler runs once per render frame, after the 3D scene has rendered, and receives a CanvasRenderingContext2D for an engine-managed 2D canvas. After all handlers have drawn, the engine composites the canvas into the main game canvas, so the overlay is part of the final visible frame and is included in screenshots and recordings of the game canvas.
You draw in CSS pixels: width / height are the logical canvas size, and the engine handles device pixel ratio for you — text and shapes are always rendered at native resolution, no devicePixelRatio handling needed. This is the same coordinate space as getScreenPosition (draw at its output directly) and getPointer (hit-test against its x/y directly).
The canvas is cleared by the engine at the start of every frame, so draw everything you need each frame. Registering multiple handlers is fine — they all draw into the same canvas, in registration order.
tsJ.onGameUiCanvas((ctx, width, height) => { ctx.fillStyle = "white"; ctx.font = "24px sans-serif"; ctx.fillText(`Score: ${score}`, 32, height - 32); });
onBeforePhysicsTick(fn: (deltaTime: number, time: number) => void) => void#
- Environment: both
Registers a handler before each physics tick.
You can use this to read inputs, and modify entity positions and velocities before the physics simulation runs, so the changes take effect in the current tick.
onPlayerJoin(fn: (playerId: number, playerPersistentData: JsonSerialisable | undefined) => void) => void#
- Environment: both
Registers a handler for when a player joins.
onPlayerLeave(fn: (playerId: number) => void) => void#
- Environment: both
Registers a handler for when a player leaves.
onCharacterUse(traitFilters: OnCharacterUseTraitFilters, fn: (characterId: EntityId, primary: boolean, secondary: boolean) => void) => void#
- Environment: both
Registers a handler for when a character uses an item or ability. primary / secondary correspond to the two use actions (typically left / right click).
ts// Fires for every character on every use J.onCharacterUse({}, (characterId, primary, secondary) => { if (primary) console.log("primary used by", characterId); if (secondary) console.log("secondary used by", characterId); });
onCharacterUnuse(traitFilters: OnCharacterUseTraitFilters, fn: (characterId: EntityId) => void) => void#
- Environment: both
Registers a handler for when a character stops using an item or ability.
onChat(fn: (playerId: EntityId, message: string) => void) => void#
- Environment: both
Registers a handler for chat messages.
onControlPress(control: string, fn: (playerId: EntityId, combo: string, press: string) => void) => void#
- Environment: both
Registers a handler for a key press.
control is a physical KeyboardEvent.code like "KeyQ" or "Space", or a + combo like "Control+KeyB".
A code is a key position on a US QWERTY keyboard, not a letter. "KeyQ" is the key where Q sits on US QWERTY, whatever your own layout prints there. So one code plays the same on QWERTY, AZERTY, QWERTZ, and the rest.
Common codes (event.code values):
- Letters:
KeyAtoKeyZ - Digits (number row):
Digit0toDigit9 - Function:
F1toF12 - Arrows:
ArrowUp,ArrowDown,ArrowLeft,ArrowRight - Modifiers:
Shift,Control,Alt,Meta(left and right fold together) - Whitespace and edit:
Space,Enter,Tab,Escape,Backspace,Delete - Punctuation:
Backquote,Minus,Equal,BracketLeft,BracketRight,Semicolon,Quote,Comma,Period,Slash,Backslash - Numpad:
Numpad0toNumpad9, plusNumpadAdd,NumpadEnter, etc. - Navigation:
Home,End,PageUp,PageDown,Insert - Locks:
CapsLock,NumLock,ScrollLock
tsJ.onControlPress("KeyQ", (playerId) => { if (playerId !== J.getLocalPlayer()) return; useAbility(); });
- Legacy names still work. Old
event.keynames ("q","ctrl","space") map to codes but are deprecated, so prefer codes. - Fires for every player, on client and host. To react only to the presser's own client, guard with
if (playerId !== J.getLocalPlayer()) return;.
onControlRelease(control: string, fn: (playerId: EntityId, combo: string, press: string) => void) => void#
- Environment: both
Registers a handler for a key release. Same control codes and local-player guard as onControlPress.
onBlockSpawn(traitFilters: TargetTraitFilters, fn: (position: Vec3) => void) => void#
- Environment: both
Registers a handler for when a block is spawned.
onBlockBreak(traitFilters: TargetTraitFilters, fn: (position: Vec3, characterId: EntityId | undefined) => void) => void#
- Environment: both
Registers a handler for when a block is broken.
onBlockBuild(traitFilters: TargetTraitFilters, fn: (position: Vec3, characterId: EntityId | undefined) => void) => void#
- Environment: both
Registers a handler for when a block is built.
onBlockSculpt(traitFilters: TargetTraitFilters, fn: (position: Vec3, characterId: EntityId | undefined) => void) => void#
- Environment: both
Registers a handler for when a block is sculpted.
onInteractWithBlock(traitFilters: TargetTraitFilters, fn: (position: Vec3, triggeredBy: EntityId) => void) => void#
- Environment: both
Registers a handler for when a block is interacted with.
onInteractWithEntity(traitFilters: TargetTraitFilters, fn: (entity: EntityId, triggeredBy: EntityId) => void) => void#
- Environment: both
Registers a handler for when an entity is interacted with.
onProjectileHitBlock(traitFilters: ProjectileTraitFilters, fn: (shooterId: EntityId | undefined, position: Vec3, hitPosition: Vec3, hitNormal: Vec3, distance: number) => void) => void#
- Environment: both
Registers a handler for when a projectile hits a block.
onProjectileHitEntity(traitFilters: ProjectileTraitFilters, fn: (entityId: EntityId, shooterId: EntityId | undefined, distance: number, crit: boolean, hitPosition: Vec3, hitNormal: Vec3) => void) => void#
- Environment: both
Registers a handler for when a projectile hits an entity.
onEntityCollisionStart(traitFilters: CollisionTraitFilters, fn: (sourceId: EntityId, targetId: EntityId, contactNormal: Vec3) => void) => void#
- Environment: both
Registers a handler for when an entity collision starts.
onEntityCollisionPersisted(traitFilters: CollisionTraitFilters, fn: (sourceId: EntityId, targetId: EntityId, contactNormal: Vec3) => void) => void#
- Environment: both
Registers a handler for ongoing entity collisions.
onEntityCollisionEnd(traitFilters: CollisionTraitFilters, fn: (sourceId: EntityId, targetId: EntityId) => void) => void#
- Environment: both
Registers a handler for when an entity collision ends.
onBlockCollisionStart(traitFilters: CollisionTraitFilters, fn: (position: Vec3, normal: Vec3, blockType: string, entityId: EntityId) => void) => void#
- Environment: both
Registers a handler for when an entity starts colliding with a block.
onBlockCollisionEnd(traitFilters: CollisionTraitFilters, fn: (position: Vec3, normal: Vec3, blockType: string, entityId: EntityId) => void) => void#
- Environment: both
Registers a handler for when an entity stops colliding with a block.
createFixedConstraint: (entityA: EntityId, entityB: EntityId) => number#
- Environment: both
Constraints two entities bodies together in a fixed way.
getAllPlayers() => EntityId[]#
- Environment: both
Returns an array of entity IDs for all players currently connected to the world.
ts// get list of all players and send them a message const players = J.getAllPlayers(); players.forEach((playerId) => { const username = J.getPlayerUsername(playerId); J.sendChatMessage(`Hello ${username}!`, "#00FF00", [playerId]); });
getLocalPlayer() => EntityId#
- Environment: client
Returns the entity ID of the local player when called on the client side.
getAllNPCs() => EntityId[]#
Returns an array of entity IDs for all NPCs in the world.
ts// make all NPCs look at a specific position const npcs = J.getAllNPCs(); npcs.forEach((npcId) => { J.entityLookAt(npcId, [0, 0, 0]); // look at world origin });
getAllProps() => EntityId[]#
- Environment: both
Returns an array of entity IDs for all props in the world.
getAllCharacters() => EntityId[]#
- Environment: both
Returns an array of entity IDs for all characters in the world.
getAllEntities() => EntityId[]#
- Environment: both
Returns an array of entity IDs for all entities in the world, including players, NPCs, props, areas, and particle systems.
getAllParticleSystems() => EntityId[]#
- Environment: client
Returns an array of entity IDs for all particle systems in the world.
getAllAreas() => EntityId[]#
- Environment: both
Returns an array of entity IDs for all areas in the world.
setEntityDegreesOfFreedom(entityId: EntityId, translationX: boolean, translationY: boolean, translationZ: boolean, rotationX: boolean, rotationY: boolean, rotationZ: boolean) => void#
- Environment: both
Constrains a prop entity's movement by setting which translation and rotation axes are allowed. Only works on prop entities.
tsconst propId = J.spawnProp("prop#ABCDE12345"); // movement X, Y, Z, rotation X, Y, Z // only allow movement along Y axis J.setEntityDegreesOfFreedom(propId, false, true, false, false, false, false);
spawnProjectile(color: string, velocity: number, shooterId: EntityId, itemAssetId: string | null, origin: Vec3, direction: Vec3) => void#
- Environment: both
Spawns a projectile with the specified properties. The projectile will be fired from the origin position in the given direction.
tsconst viewRay = J.getCharacterViewRay(playerId); if (viewRay) { J.spawnProjectile( "#FF0000", 20, playerId, null, viewRay.origin, viewRay.direction );
spawnText3D(text: string, fontSize: number, maxVisibleDistance: number, faceCamera: boolean, position: Vec3, rotation: Vec3) => EntityId#
- Environment: both
Spawns text using the engine's fixed font and outline style. The position is the text's bottom-center anchor.
tsconst textId = J.spawnText3D("Hello World!", 32, 50, true, [0, 5, 0], [0, 0, 0]);
updateText3D(entityId: EntityId, text?: string, fontSize?: number, position?: Vec3, rotation?: Vec3) => void#
- Environment: both
Updates the content, size, or transform of an existing Text3D entity. Position updates set the bottom-center anchor directly.
tsJ.updateText3D(textId, "Updated text!", 48); J.updateText3D(textId, undefined, undefined, [10, 5, 10]);
findPath: (start: Vec3, goal: Vec3, searchSize: number ) => { success: boolean; path: Vec3[]; }#
- Environment: both
Finds a path from the start position to the goal position using a voxel grid-based search. The searchSize determines how many search iterations to perform, with larger values allowing for more complex paths but taking longer to compute.
tsconst start: J.Vec3 = [0, 0, 0]; const goal: J.Vec3 = [10, 0, 10]; const maxSearchSize = 100; const result = J.findPath(start, goal, maxSearchSize); // console.log(result.success, result.path);
findWalkableBlocksInRadius: (start: Vec3, radius: number, searchSize: number) => Vec3[]#
- Environment: both
Finds all walkable blocks within a specified radius from the start position. The searchSize determines how many search iterations to perform, with larger values allowing for more complex searches but taking longer to compute.
tsconst start: J.Vec3 = [0, 0, 0]; const radius = 10; const maxSearchSize = 100; const walkableBlocks = J.findWalkableBlocksInRadius(start, radius, maxSearchSize); console.log("Walkable blocks:", walkableBlocks);
getPlayerPersistentData: (playerId: EntityId) => JsonSerialisable | undefined#
- Environment: server
Gets the persistent data associated with a player. This data persists across game sessions and can be used to store player progress, preferences, or other persistent state.
Note that play mode and edit mode have separate persistent data storage - data saved in play mode is not accessible in edit mode and vice versa.
setPlayerPersistentData: (playerId: EntityId, data: JsonSerialisable) => void#
- Environment: server
Sets the persistent data associated with a player. This data persists across game sessions and can be used to store player progress, preferences, or other persistent state.
Note that play mode and edit mode have separate persistent data storage - data saved in play mode is not accessible in edit mode and vice versa.
tsJ.onPlayerJoin((playerId, data) => { // If player doesn't have anything, add 100 coins if (data === undefined) { data = { coins: 100 }; J.setPlayerPersistentData(playerId, data); } J.sendChatMessage( `Player ${playerId} has joined with ${data.coins} coins`, "#00FF00",
getScreenPosition: (out: Vec2, worldPosition: Vec3) => Vec2#
- Environment: client
Converts a 3D world position to a 2D screen position. Returns the screen coordinates where the world position would appear on the player's screen.
The returned Vec2 contains x and y coordinates in screen space, where (0,0) is typically the top-left corner of the screen. If the world position is behind the camera or outside the view frustum, the returned coordinates may be outside the visible screen area.
setEnvironmentPreset: (assetId: string) => void#
- Environment: both
Sets the current environment preset for the world using the specified asset ID. This will change lighting, fog, sky, and other environmental settings. Environment presets can be created and managed through the world editor. It is recommended to avoid changing this frequently.
ts// Day time J.setEnvironmentPreset(J.assets.environmentPresets.DayPreset.id); // Night time J.setEnvironmentPreset(J.assets.environmentPresets.NightPreset.id);
showWorldPortal: (worldShortCode: string) => void#
- Environment: client
Client-only function that presents a world portal to the player.
tsJ.showWorldPortal("XIMXA");
getPropAssetIcon: (assetId: string) => string | undefined#
- Environment: client
Returns the prop's asset icon as a base64-encoded string url, or undefined if no icon is available. This can be used to display visual representations of props in UI elements.
leaderboards.defineLeaderboard: (leaderboardId: string, options: { mode: "lowest" | "highest" }) => LeaderboardId#
- Environment: both
Defines a new leaderboard with the specified ID and scoring mode. This should typically be called in a shared file so both client and server have access to the same leaderboard definition.
leaderboardId- A unique string identifier for the leaderboardoptions.mode- Either "lowest" (for time-based competitions) or "highest" (for score-based competitions)
ts// shared.ts - Define leaderboards that both client and server can access export const RaceLeaderboard = J.leaderboards.defineLeaderboard("race_times", { mode: "lowest" }); export const ScoreLeaderboard = J.leaderboards.defineLeaderboard("high_scores", { mode: "highest" });
leaderboards.getPlayerScore: (leaderboard: LeaderboardId, playerId: EntityId) => Promise<LeaderboardEntry | null>#
- Environment: both
Gets a specific player's score entry from the leaderboard. Returns null if the player has no score recorded.
ts// client.ts - Display player's current ranking const playerScore = await J.leaderboards.getPlayerScore(RaceLeaderboard, playerId); if (playerScore) { J.sendChatMessage(`Your best time: ${playerScore.score}s (Rank #${playerScore.place})`); } else { J.sendChatMessage("You haven't set a time yet!"); }
leaderboards.setPlayerScore: (leaderboard: LeaderboardId, playerId: EntityId, score: number) => void#
- Environment: server
Sets a player's score on the leaderboard. The score will only be updated if it's better than their previous score (lower for "lowest" mode, higher for "highest" mode).
ts// server.ts - Record a new lap time const lapTime = 45.23; J.leaderboards.setPlayerScore(RaceLeaderboard, playerId, lapTime); J.sendChatMessage(`New lap time recorded: ${lapTime}s!`);
leaderboards.getTopScores: (leaderboard: LeaderboardId) => Promise<LeaderboardEntry[]>#
- Environment: both
Gets the top scores from the leaderboard, ordered by rank. Returns an array of LeaderboardEntry objects containing player information and scores.
ts// client.ts - Display top 10 players const topScores = await J.leaderboards.getTopScores(RaceLeaderboard); topScores.slice(0, 10).forEach((entry, index) => { J.sendChatMessage(`#${entry.place}: ${entry.playerUsername} - ${entry.score}s`); });
leaderboards.getScoresForPlayers: (leaderboard: LeaderboardId) => Promise<LeaderboardEntry[]>#
- Environment: both
Gets leaderboard entries for all currently connected players. Useful for displaying rankings of active players only.
ts// client.ts - Show rankings for current players const playerScores = await J.leaderboards.getScoresForPlayers(ScoreLeaderboard); playerScores.forEach((entry) => { if (entry.playerId) { J.sendChatMessage(`${entry.playerUsername}: ${entry.score} points (Rank #${entry.place})`); } });
Changelog#
24 Sep 2026#
- Mesh attachment add/set/update functions now take optional
tint?, glow?, outline?, outlineSize?parameters afterscale, giving each attachment its own tint, glow and outline instead of the character's. Theget…MeshAttachmentsfunctions return them too.
26 Aug 2026#
- Breaking: the
predictable?: booleanparameter ofJ.setEntityPosition,J.setEntityQuaternion,J.setEntityVelocity,J.addEntityVelocity,J.setEntityAngularVelocity,J.addEntityAngularVelocityandJ.characterJumpis nowteleport?: booleanand defaults totrue. Calls without the argument now snap (teleport) instead of interpolating; passfalsefor continuous per-tick movement. Scripts that passedpredictable: falseshould drop the argument; scripts that relied on the old default for per-tick movement must passfalse.
20 Aug 2026#
- Added
J.setMouseMode(mode: "pointerLocked" | "pointerFree")— chooses how the mouse is handled during gameplay."pointerLocked"is the default (click once, mouse steers the camera from then on);"pointerFree"leaves the player a cursor and only steers while the right mouse button is held, which also means the right button no longer fires the item's secondary action. Desktop only, client-only. - Added
J.setCameraDistance(distance)andJ.setCameraDistanceRange(min, max)— the camera is now described by a single distance in world units, where0is first person and anything else is third person, so there is no first/third person mode to pick any more. The range is what the player may scroll between; passing the same value for both pins the camera and disables zooming. Client-only. - Added
J.setCameraOffset(right, up)— shifts the point the camera orbits, for over-the-shoulder or raised views.rightfollows the look direction; the offset is included in the wall collision check. Client-only. J.getCameranow also returnsdistance(the configured distance behind the character,0in first person) alongsideposition,quaternion,fov, andaspect.- Breaking:
J.setLocalPlayerCamerano longer takes any arguments — it only reattaches the camera to the local player. The front-facing"selfie"camera is gone, and"firstPerson"/"thirdPerson"are replaced byJ.setCameraDistanceRange: pass(0, 0)for first person only,(1, 20)for third person only,(0, 20)for the full range. - Breaking: the
mobileCameraButtonUI setting is gone along with the mobile camera-toggle button it controlled. - Breaking:
J.getCharacterInput().camerano longer has aradiusfield. It was always1and never did anything; useJ.getCamera().distancefor the camera distance.
3 Sep 2026#
- Animation speeds may now be negative to play clips backwards.
J.characterSetAnimationSpeed(id, clip, speed)changes an active animation's direction or speed without restarting it; use0to pause.
14 Aug 2026#
J.setEntityOutlinenow accepts sizes up to 1 instead of 0.25.
29 Jul 2026#
- Added
J.getRenderSettings() => RenderSettingsandJ.setRenderSettings(settings: Partial<RenderSettings>) => void— reads and session-overrides the local player's render settings without modifying saved preferences. Client-only. - Added
J.setEntityOutline(id, size, color)— sets a client-only outline on prop and character entities; use a zero size or alpha to remove it. J.spawnText3DandJ.updateText3Dnow use the engine's fixed font and outline style, with positions treated as the text's bottom-center anchor.
10 Jul 2026#
- Added client-only procedural stripe ribbons:
J.createRibbon,J.setRibbonGeometry,J.setRibbonStyle,J.setRibbonVisible, andJ.removeRibbon.
4 Jul 2026#
- Added
J.setPointerLock(state: "auto" | "unlocked" | "locked")— controls gameplay mouselook."unlocked"frees the cursor and suppresses the engine's click-to-lock (use it for interactive UI panels),"auto"restores the default,"locked"grabs lock now. No-op on mobile. Client-only. - Added
J.setCursor(css: string)— sets the CSS cursor shown over the game area ("default", "pointer", "grab", …). Client-only. - Added
J.onGameUiCanvas(fn: (ctx: CanvasRenderingContext2D, width: number, height: number, deltaTime: number, time: number) => void)— draws 2D overlay UI onto an engine-managed canvas that is composited into the main game canvas each frame. Coordinates are CSS pixels (device pixel ratio is handled by the engine), the same space asgetScreenPositionandgetPointer. Client-only. - Added
J.getPointer() => { x, y, isDown, justPressed, justReleased, locked, touch }— polls the screen-space pointer (mouse on desktop, primary touch on mobile) for canvas UI hit-testing. Client-only. J.onGameRendercallbacks now receive(deltaTime: number, time: number)(previously no arguments).J.getCameranow also returnsaspect(width / height of the render viewport) alongsideposition,quaternion, andfov.
3 Jul 2026#
- Added
J.setSoundVolume(soundId, volume, fadeDuration?)— sets a playing sound's volume, optionally fading to it. - Added
J.setSoundFilter(soundId, type: "lowpass" | "highpass", frequency, fadeDuration?)— applies a filter to a playing sound, sweeping the cutoff tofrequencyHz; calling it again retargets the sweep. - Added
J.clearSoundFilter(soundId)— removes a sound's filter immediately.
8 Jun 2026#
- Added
J.setCharacterVisualFacingMode(id, mode: "movement" | "camera")— sets whether a character's rendered body faces its movement direction (default) or the camera/look angle. Local rendering preference, never networked. Client-only. J.setEntityQuaternionnow takes an optionalpredictable?: booleanparameter. For player characters, passpredictable: false, otherwise the call has no effect, since a character's orientation is driven by its camera.
3 Jun 2026#
- Added the
animkey to character avatar configs (J.addCharacterAvatarConfigand friends) — per-character locomotion animation overrides. Supportsanim.idle,anim.run,anim.walk, andanim.crouch; set each to a GLB clip name ornullto restore the default rig animation.
29 May 2026#
J.characterPlayAnimation,J.characterStopAnimation, andJ.clearCharacterAnimationswrite only characters controlled by the current runtime. Controller-authored animation state replicates unchanged; writes to replayed characters fail fast.
21 May 2026#
J.characterPlayAnimationnow takesmode: "loop" | "once" | "hold"instead ofloop: boolean."loop"replacesloop: true,"once"replacesloop: false, and the new"hold"mode plays the clip once then freezes it on the final frame until it's stopped or replaced — useful for death poses and knockouts. Defaults are unchanged ("loop"for base/override,"once"for additive). Replace any{ loop: true }with{ mode: "loop" }and{ loop: false }with{ mode: "once" }.
12 May 2026#
- Added
J.getWorldOptions() => Record<string, string | number | boolean>— returns the world options object configured in the editor (same value passed toonGameStartcallbacks). - Changed
J.onGameStartcallback signature: the callback now receivesworldOptions: Record<string, string | number | boolean>as its first argument.J.onGameStart(() => { ... })still works if you don't need the options. - Added
J.join(worldOptions?, playerOptions?) => void— triggers a join from the client. Both arguments are optional key-value maps passed to the server at join time.
7 May 2026#
- Renamed
J.addCharacterMeshAttachment→J.addCharacterMeshAttachmentProp. - Added
J.addCharacterMeshAttachmentGlb(characterId, spec, slot, position, quaternion, scale) => number— attaches an inline GLB mesh spec ({ id, colors? }) instead of a prop asset id. - Renamed
J.addFirstPersonMeshAttachment→J.addFirstPersonMeshAttachmentProp. - Added
J.addFirstPersonMeshAttachmentGlb(characterId, spec, position, quaternion, scale) => number— FP equivalent of the Glb variant above. - Added
J.spawnPropFromSpec(spec: GlbMeshSpec, physics?: SpecPropPhysics) => EntityId— spawns a prop entity from an inline GLB spec ({ id, colors? }). Client-only; spec is not replicated.
30 Apr 2026#
- Added
J.getCharacterSlotWorldMatrix(characterId, slot, localMatrix)— resolves a slot-local matrix into world space, matching the compositionaddCharacterMeshAttachmentPropapplies. Use it to resolve attachment offsets (gun muzzles, holster sockets, etc.) into world space. - Added
J.getCharacterFirstPersonWorldMatrix(characterId, localMatrix)— multiplies the local first-person camera's world matrix bylocalMatrix, matching the compositionaddFirstPersonMeshAttachmentPropapplies. Returnsundefinedunless the local view is currently first-person on the given character.
28 Apr 2026#
- Removed
J.getCharacterItem,J.setCharacterItem,J.ItemAnimationsenum, andJ.CharacterItemtype — replaced by the mesh attachment system (see below). - Removed
J.setFirstPersonItemTransformandJ.clearFirstPersonItemTransform— replaced byaddFirstPersonMeshAttachmentProp. - Added character mesh attachment system (client-only, not networked — call on every client that should see the attachment):
J.addCharacterMeshAttachmentProp(characterId, propAssetId, slot, position, quaternion, scale) => number— attaches a prop mesh at a character slot. Returns a handle.J.addCharacterMeshAttachmentGlb(characterId, spec, slot, position, quaternion, scale) => number— attaches an inline GLB spec.J.setCharacterMeshAttachment(characterId, handle, position?, quaternion?, scale?, slot?) => void— updates an existing attachment's transform or slot.J.removeCharacterMeshAttachment(characterId, handle) => void— removes by handle.J.clearCharacterMeshAttachments(characterId) => void— removes all attachments.J.getCharacterMeshAttachments(characterId) => CharacterMeshAttachmentInfo[]
- Added first-person mesh attachment system (client-only, transforms are camera-relative):
J.addFirstPersonMeshAttachmentProp(characterId, propAssetId, position, quaternion, scale) => numberJ.addFirstPersonMeshAttachmentGlb(characterId, spec, position, quaternion, scale) => numberJ.updateFirstPersonMeshAttachment(characterId, handle, position?, quaternion?, scale?) => voidJ.removeFirstPersonMeshAttachment(characterId, handle) => voidJ.clearFirstPersonMeshAttachments(characterId) => voidJ.getFirstPersonMeshAttachments(characterId) => FirstPersonMeshAttachmentInfo[]
- Removed
J.assets.avatarComponentsand the"avatarComponent"value for theassetTypesschema option. Legacy avatar components are now marketplace products; use the character avatar APIs (J.getCharacterAvatar,J.setCharacterAvatarById,J.addCharacterAvatarConfig) instead.
23 Apr 2026#
- Replaced
J.characterPlayEmote/J.characterStopEmotewith a generalized animation API:J.characterPlayAnimation(id, clip, opts?)— plays any GLB clip. Mode (base / additive / override) is derived from the clip's name suffix (_add→ additive,_over→ override, otherwise → base). Options:loop,speed,fadeIn,cancelOnMove.J.characterStopAnimation(id, clip, opts?)— stops a specific clip.opts.fadeOutcontrols the fade.J.clearCharacterAnimations(id)— stops every animation currently playing on the character.
- Base, additive, and override layers coexist: the character can hold an additive pose, run an override clip, and still animate locomotion underneath. Playing a new base clip auto-replaces any prior base clip.
- Renamed
J.assets.emotes→J.assets.animations. The registry still lists every clip in the GLB (locomotion, emotes,_add,_over). - Schema asset type
"emote"renamed to"animation". If you hadJ.schema.asset({ assetTypes: ["emote"] }), change it to["animation"].
21 Apr 2026#
J.assets.emotesis now sourced directly from GLB animation clips. Available emotes:danceBasic,danceWorm,hello,usainBolt,zen. Old names (Wave,CowgirlDance,MangoDance,Salute,FistPump,ThumbsUp,RaisedArms,Talk,Think,Beckon,Measure,Show,Freeze,JumpingJacks,LayDown,PushUps,Sit,Stretch,Dance,DjDance,HopakDance,Flying,Energy,Death,Fly,Zombie,Clap) are removed.
17 Apr 2026#
- Added
J.CHARACTER_RIG_IDS: readonly CharacterRigId[],J.CHARACTER_SLOT_IDS: readonly CharacterSlotId[], andJ.MESH_SET_IDS: readonly MeshSetId[]— typed constant arrays listing all valid rig, slot, and mesh-set identifiers.
16 Apr 2026#
- Replaced
J.equipAvatarComponent/J.unequipAvatarComponentwith additive config layering:J.addCharacterAvatarConfig(merge partial config onto current avatar) andJ.getPlayerBaseAvatar(read original platform avatar for reset).
9 Dec 2025#
- Added
J.getWorldSettings() => WorldSettingsandJ.setWorldSettings(settings: Partial<WorldSettings>) => void— read and write world-level settings (e.g.chatEnabled). - Added
J.getUISettings() => UISettingsandJ.setUISettings(settings: Partial<UISettings>) => void— read and write per-client UI visibility settings (mobile buttons, emote button, etc.). Previously documented asgetUIOptions/setUIOptions; the actual function names aregetUISettings/setUISettings.
21 Nov 2025#
- Added
J.schemadocumentation
20 Nov 2025#
- Added more Physics API
J.getEntityPhysicsEnabled/J.setEntityPhysicsEnabledJ.getEntityMassJ.addEntityImpulseJ.addEntityImpulseAtPointJ.getEntityInverseInertiaJ.getEntityVelocityAtPointJ.getEntityInterpolatedPositionJ.setEntityInterpolatedPositionJ.getEntityBodyActiveJ.activateEntityBodyJ.onBeforePhysicsTickJ.getCamera
19 Nov 2025#
- Added networking function documentation
J.net.defineCommandJ.net.sendJ.net.sendToAllJ.net.listen
- Added
J.sceneTreeandJ.getSceneTreeNodeEntitydocumentation example - Added
J.getEntityMatrix,J.getEntityInterpolatedQuaternion,J.addEntityForceandJ.setEntityGravityFactor - Added
J.getCharacterAlive,J.setCharacterAlive,J.getCharacterSprintingandJ.setCharacterSprinting - Added
J.getEntityVisualOffset,J.setEntityVisualOffset,J.getEntityVisualQuaternion,J.setEntityVisualQuaternion,J.getEntityVisualScale, andJ.setEntityVisualScale
4 Nov 2025#
- Changed
net.sendToAllexceptargument type fromexcept?: EntityId | EntityId[]toexcept?: EntityId
22nd Oct 2025#
-
Added
J.assets.avatarComponents -
Added
J.getCharacterAvatar,J.setCharacterAvatarByIdandJ.setCharacterAvatarByConfig
1 Oct 2025#
- Added
J.getPlayerPhotoUrl - Added
J.getCharacterPhotoUrl
29 Sep 2025#
-
Added
J.setPlayerNameplate -
Changed
J.characterPlayEmoteto no longer take aloopargument -
Fix: Hide footstep effect for invisible characters
-
List of emotes was changed:
- Use
J.assets.emotes.Flyinstead ofFlyXXX - Use
J.assets.emotes.Deathinstead ofDeathXXX
- Use
1 Sep 2025#
-
Introduced
J.Eulertype,J.euler.*math utilities -
Changed
J.quat.fromEulerto take aEulerobject -
Moved
J.remap,J.remapClampAPIs toJ.math.remap,J.math.remapClamp -
Added
J.math.clamp -
Added
J.math.degreesToRadians,J.math.radiansToDegrees -
Introduced
J.schemaAPI for defining the shape of data, and annotating it with metadata so that UIs can be generated for inspecting and editing- This will in future be used for typing and improving performance of network commands
-
Changed
J.defineTraitto no longer take a type generic argument. This API now takes aJ.Schema
Before
tsconst LaunchpadTrait = J.defineTrait<{ velocity: J.Vec3 }>("Launchpad");
After
tsconst LaunchpadTrait = J.defineTrait("Launchpad", { velocity: J.schema.vec3() }); // or const LaunchpadTrait = J.defineTrait("Launchpad", J.schema.any<{ velocity: J.Vec3 }>());