Custom rooms
A custom room is a room you make yourself, and the player walks into it like any other Animal Jam room. You supply the art (a background and a picture for each prop) and say where avatars can walk, where the player appears and how they get out. Phantom builds the room file, serves it to the game client and moves the player in. The client draws it the same way as its own rooms, so avatars, chat balloons, emotes and dances all work.
Animal Jam's servers never see it. As far as they know, the player is still in the room they came from, and when the player leaves, that room is put back as it is now.
A room can be private or shared. In a shared room, everyone who entered the same room from your plugin meets up. Phantom draws the other players, passes their chat along and applies the Phantom community rules. Only plugins from the Phantom store can share a room.
Quick example
import { definePlugin } from '@phantomclient/plugin';
import background from '../art/garden.png?inline';
import moon from '../art/moon.png?inline';
import pond from '../art/pond.png?inline';
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const garden = await phantom.aj.customRooms.define({
name: 'Moon Garden',
width: 900,
height: 550,
background,
walkable: [{ rect: { x: 20, y: 320, width: 860, height: 210 } }],
spawn: { x: 450, y: 460 },
props: [
{
id: 'moon',
art: moon,
x: 390,
y: 30,
layer: 'floor',
clickable: true,
motion: { pulse: { seconds: 4, min: 0.75 } },
},
{ id: 'pond', art: pond, x: 640, y: 380, layer: 'floor', nearDistance: 60 },
],
exits: [{ id: 'gate', x: 65, y: 470, radius: 35 }],
});
const stops = [
phantom.aj.customRooms.onEnter(({ name }) => ctx.log(`Welcome to ${name}`)),
phantom.aj.customRooms.onPropClick(({ prop }) => ctx.log(`Clicked the ${prop}`)),
phantom.aj.customRooms.onPropNear(({ prop }) => ctx.log(`Close to the ${prop}`)),
phantom.aj.customRooms.onLeave(({ reason }) => ctx.log(`Left: ${reason}`)),
];
await phantom.aj.customRooms.enter(garden);
return () => stops.forEach((stop) => stop());
},
});Each picture is a PNG in the plugin's folder. Importing it with ?inline makes the bundler build it into the plugin as a data: URL. You can pass bytes instead if you generate a picture in code. Entering a room takes over what the game shows, so the manifest asks to act in the game.
"permissions": { "game": "act" }The full Moon Garden, pictures included, is in Phantom's repo at plugins/examples/custom-room.
Defining once
define is slow, because the pictures are sent to the game, so do it once. If your plugin has one room and a command that goes to it, phantom.aj.customRooms.open(spec) does define and enter together and remembers the id, so the second visit builds nothing.
const garden = {
name: 'Moon Garden',
width: 900,
height: 550,
background,
spawn: { x: 450, y: 460 },
};
export const visit = () => phantom.aj.customRooms.open(garden, { shared: true });Pass the same object each time. A description that fails to build is forgotten, so a corrected one builds on the next call. A room you take away with remove isn't built again; use define and enter for it.
Making the art
A room is one background and a picture for each prop. The background is stretched to fill the room, so paint it at the room's size. A 900 by 550 background in a 900 by 550 room is drawn pixel for pixel. Props are drawn at their picture's own size unless you set width and height. Paint them at the size you want them to appear.
Transparency
Props are PNGs with a transparent background. If a prop isn't a rectangle (a bush, a lamp, a glowing moon), make the space around it fully transparent. A soft glow can fade out into that space. The background doesn't need transparency and can be a JPEG if it's a photo or a painting with no hard edges.
Layers
Each prop sits on one of the client's own four layers.
layer | Where it's drawn |
|---|---|
floor | Under all avatars. Use it for rugs, stages, ponds and anything on the back wall. |
standing | The default. Drawn among the avatars. If someone's feet are lower on screen than the prop's bottom edge they're drawn in front of it, otherwise behind. Paint the foot of a tree or sofa at the bottom of its picture. |
front | Over all avatars, like a beam of light or a branch hanging into the room. Keep these faint or small. |
top | Over everything the room draws at all. A haze, a vignette, weather over the whole scene. |
standing is the only layer that sorts, and sortHeight tunes it: how far above the prop's lowest point it counts as standing, in pixels. A player whose feet are below that point is drawn in front of the prop, and one above it behind. Without it the client uses the whole box, which suits a tree but not a low wall you want to stand behind.
scaleX and scaleY scale the picture inside its box; 1 is the box itself. The box stays put whatever the scale, so what avatars walk round and what the player clicks don't move.
SWF art
art and background take a SWF as well as a PNG or JPEG. Animal Jam's own room art is SWF, and the client loads all three the same way. A SWF is drawn at its stage size unless width and height say otherwise, and plays whatever is on its timeline.
A SWF can't be used where Phantom draws the picture itself: changeable, motion and frames. Phantom has no Flash player to draw it with, so use a PNG or JPEG for those.
The floor
Phantom doesn't work out the floor from your picture. You set it with walkable, and it has to match what you painted. Measure where the floor starts in your background (the Moon Garden lawn starts at y 300) and make walkable cover the floor and nothing else. Use a rect for a plain floor and a polygon for a path or a floor seen at an angle. Cut out walls, water and furniture with blocked, or make a prop solid so avatars walk round it. For a standing prop, give it an area covering just its base so people can walk behind the top half.
The game's grid has four kinds of cell, not two, and the client decides which ones block a given avatar as it loads the room. landWalls are areas an animal on the ground can't enter but one in the air can, which is what water is. airWalls are the reverse. An area in both is a wall to everyone. They're laid over what's walkable, so a cell has to be walkable before either applies.
Leave a bit of margin at the edges. The game moves avatars between points 24 pixels apart, and an avatar standing right on the edge of the floor looks odd.
It helps to keep a layout layer over the background while you paint, with the floor line, the walkable area, each prop's box and the spawn point marked on it.
Describing a room
All measurements are in pixels, with 0, 0 at the room's top left corner.
| Field | What it is |
|---|---|
name | The room name the game shows, up to 40 characters. |
width | 900 (one screen) to 3000. If it's wider than a screen, the view follows the player. |
height | 550 (one screen) to 2000. |
background | The art behind everything, stretched over the whole room. A PNG, a JPEG or a SWF. |
walkable | Areas avatars can walk in. Defaults to the whole room minus a 40 pixel margin. |
blocked | Areas cut out of walkable, like walls or anything every animal walks round. |
landWalls | Areas an animal on the ground can't enter but one in the air can. Water is this. |
airWalls | Areas an animal in the air can't enter but one on the ground can. |
spawn | Where the player appears. It has to be somewhere they can stand. |
props | What's in the room, up to 60. |
exits | Circles that take the player back to where they came from, up to 8. |
characters | People in the room who aren't players, up to 8. |
music | One of Animal Jam's own music tracks, by its .mp3 file name, at a volume from 0 to 100. Silent if left out. |
spawns | Other places a player can appear, each named, up to 16. See Ways in and ways on. |
doorways | Circles that take the player to another room, up to 8. See Ways in and ways on. |
volumes | Named areas the game can find by name, up to 32. See Named areas. |
scroll | How far the view scrolls. Defaults to the whole room. See How far the view scrolls. |
An area is a rect, a circle or a polygon of [x, y] points.
Props
x and y are the top left corner of the prop's picture.
| Field | What it does |
|---|---|
art | The art, a PNG, JPEG or SWF as a data: URL or bytes. |
width | Drawn width. Defaults to the picture's width (one frame's width for a strip). |
height | Drawn height. Defaults to the picture's height. |
layer | floor, standing (the default), front or top. |
sortHeight | standing only: how far above its lowest point it counts as standing, for sorting. |
scaleX | Scales the picture inside its box; 1 by default. The box doesn't move with it. |
scaleY | The same, down. |
motion | Animates the picture on a loop. See below. |
frames | Plays the picture as a strip of frames. See below. |
solid | Avatars walk round its box, or round its area if it has one. |
area | The area used for walking onto it, clicking and nearness. Defaults to its box. |
clickable | onPropClick fires when the player clicks it. |
nearDistance | onPropNear fires when the player comes within this many pixels of its area. |
bounce | Avatars standing in its area bounce, using the game's own bounce. One per room. |
changeable | Phantom draws it, so you can move, hide and repicture it while the player is in the room. See below. |
Props that move
motion animates a prop's picture on a loop. You can combine these and they run together.
pulse: { seconds, min }fades it down tominopacity (0.35 by default) and back.spin: { seconds }turns it once round its middle. Use a negative number to spin the other way.bob: { seconds, height }lifts itheightpixels and drops it back.
A loop can be up to 8 seconds long. Spinning happens around the picture's centre, so centre the thing that spins (a disco ball, a wheel) in its picture.
For frame animation, like a flickering torch or a turning sign, put the frames side by side in one PNG, all the same size, and give the prop frames: { count, fps }. A four frame torch with 30 by 50 frames is a single 120 by 50 picture.
import torch from './art/torch.png?inline';
await phantom.aj.customRooms.define({
name: 'Great Hall',
width: 1200,
height: 700,
background: hall,
spawn: { x: 600, y: 560 },
props: [{ id: 'torch', art: torch, x: 180, y: 220, frames: { count: 4, fps: 8 } }],
});A strip can have up to 64 frames and play at up to 24 frames a second. You can use motion on it too.
Props that change
Everything above is built into the room's file, which is made and loaded once, so none of it can change while the player is in the room. Give a prop changeable: true and Phantom draws it itself, in the room's own layers, so your plugin can move it, hide it and change its picture while the player is there.
const id = await phantom.aj.customRooms.define({
name: 'Sword Cave',
width: 900,
height: 550,
background: cave,
spawn: { x: 450, y: 450 },
props: [
{ id: 'sword', art: swordArt, x: 400, y: 180, clickable: true, changeable: true },
],
});
await phantom.aj.customRooms.enter(id);
const sword = phantom.aj.customRooms.prop('sword');
phantom.aj.customRooms.onPropClick(async ({ prop }) => {
if (prop !== 'sword') return;
await sword.art(glowingSword);
await sword.move(120, 340);
});prop(id) is only a name. Making one asks Phantom nothing and keeps nothing alive, so you can make it once in setup and keep it.
| Call | What it does |
|---|---|
move(x, y) | Where the picture's top left corner goes, in the room's pixels. |
hide() | Takes it out of the room, without taking it out of the room's description. |
show() | Puts it back. |
art(picture) | Another picture, as a data: URL or bytes; null for the one the room gives. |
change({ … }) | Several of those at once, so nothing is drawn halfway between the two. |
reset() | Back where the room describes it, with its own picture, shown. |
now() | Where it stands now: x, y, shown, and whether anything has changed it. |
A picture passed to art is checked like the room's own pictures (a PNG or JPEG, up to 4 MB, 4096 pixels on a side). It isn't part of the room, so it doesn't count towards the room's 12 MB picture limit and doesn't change the room's hash.
Each call returns where the prop stands afterwards, or null once the room is gone (the player walked out, or you removed it), so a handle kept past its room goes quiet instead of throwing. Naming a prop the room hasn't got, or one the room draws itself, does throw, because that's a bug in your code.
By default a prop belongs to the room the player is in now, which suits a plugin with one room. Pass the room's id as the second argument (prop('sword', roomId)) for a room they aren't in yet. The change is kept and drawn when they walk in, so a sword can be hidden before anyone sees it. Where a prop stands belongs to the room, not the visit, so it's still there when they come back. reset() forgets it.
Moving a prop doesn't change the room
A shared room's instance is keyed by the hash of the room's whole description, which is how two people describing the same room meet in it. Where a prop stands now isn't part of that description, so moving one never splits your shared room.
It also means changing a prop is local. It happens on this player's screen, and others in a shared room still see it where the room describes it. For something everyone in the room should agree on, give it shared: true to make it an object, whose state Phantom's server holds.
What a changeable prop can't have
motion, frames, solid and bounce are refused, because each is built into the room or its walking map, which can't change while the player is in it. To animate a changeable prop, put a motion prop next to it, or draw frames into the pictures you swap between.
Everything else follows it. clickable and nearDistance work wherever it has moved to, and a hidden prop isn't there at all: standing where it was does nothing, and onPropLeave fires as it goes. Clicking a changeable prop doesn't walk the player over to it.
Because Phantom draws it rather than the room, it sits on top of its layer instead of being sorted into it, so a standing one is drawn over the avatars. Use floor for something they should walk in front of.
Shared objects
A changeable prop only exists on this player's screen. If two people in the same shared room click the same sword at the same moment and both plugins call move, both of them walk away with it, each seeing a sword nobody else can see.
Give the prop shared: true and it becomes an object. Its state lives on Phantom's server for the shared room instance, and the server decides once: one player gets the sword and the other gets a refusal.
import { objectErrorOf } from '@phantomclient/plugin/sdk';
const id = await phantom.aj.customRooms.define({
name: 'Sword Cave',
width: 900,
height: 550,
background: cave,
spawn: { x: 450, y: 450 },
props: [
{
id: 'sword',
art: swordArt,
x: 400,
y: 180,
clickable: true,
changeable: true,
shared: true,
},
],
});
await phantom.aj.customRooms.enter(id, { shared: true });
const sword = phantom.aj.customRooms.object('sword');
sword.onTaken(({ yours }) => console.log(yours ? 'I have it' : 'somebody else has it'));
phantom.aj.customRooms.onPropClick(async ({ prop }) => {
if (prop !== 'sword') return;
try {
await sword.take({ shown: false });
} catch (error) {
console.log(objectErrorOf(error).message);
}
});Like prop(id), object(id) is only a name and asks Phantom nothing.
| Call | What it does |
|---|---|
take({ … }) | Takes it, with anything else to do in the same breath. Refused if taken. |
drop({ … }) | Gives it up where it stands, after anything else in the change. |
move(x, y) | Where the picture's top left corner goes. Needs it held. |
hide(), show() | Out of the room and back. Needs it held. |
mark(word) | Keeps a short word on it (open, lit, a score); null takes it off. |
reset() | Back where the room describes it, shown and unmarked. Needs it held. |
change({ … }) | All of the above at once, made whole or refused whole. |
now() | How it stands: holder, mine, x, y, shown, tag, changed. |
onTaken, onDropped, onChanged | Hears it happen, to anybody in the room. |
phantom.aj.customRooms.objects() gives every object something has been done to, and phantom.aj.customRooms.onObject hears all of them at once.
Who decides
Phantom's server decides, once, for the whole room instance. Your plugin doesn't arbitrate: a rule in the plugin would differ between plugins, and two in the same room would disagree.
- Anyone in the room can take anything. There's no owner.
- Everything except
takeneeds this player to hold the object. Move a sword you haven't taken and you getnot-held. That stops two people changing one thing at once. - A refusal is an error, not
false.takerejects with aCustomRoomObjectErrorwhosereasonisheld,not-held,too-many,badorgone.objectErrorOfreads one back in a plugin frame. Losing a race is normal, so catch it. - A change is all or nothing.
take({ take: true, tag: 'on', drop: true })is one step: a lever someone flips without keeping hold of it. - Whoever leaves drops what they had, where it stands. The server decides this, not whichever plugin noticed. Anything they moved stays moved.
holder is an id that only means something inside the room instance. It's never an account, player or screen name, and it changes the next time the same person walks in. mine says whether it's this player. Your plugin still only hears its own player, as with everything else in a shared room.
What an object holds
A room holds 32 objects (phantom.aj.customRooms.limits.objects), and each holds very little: who has it, where it is, whether it's drawn, and one word of up to 64 bytes. Each change sends that record, not the whole room.
There's no art. Shared state is no place to send pictures to thirty people, and your art lives in your plugin anyway. For something with two looks, use two props and hide one.
Objects last as long as someone is in the room. When the last person leaves, they're gone, and the next instance of the room starts with everything where the room describes it. Carrying a sword out of the room and keeping it until next week isn't supported.
Objects also work in a room the player entered alone, with nobody to lose to, so the same code covers both. There they're forgotten when the player leaves.
prop('sword') refuses an object, and object('anvil') refuses a prop that isn't one. Where a shared thing stands is the room's decision, not this player's, and the two calls would disagree. An object also needs changeable: true, and define tells you if it's missing.
Characters
A room can have people in it who aren't players: a keeper by the door, a guide at the crossroads, someone sweeping. Describe them with the room, so every plugin gets the same thing.
await phantom.aj.customRooms.define({
name: 'Great Hall',
width: 1200,
height: 700,
background: hall,
spawn: { x: 600, y: 560 },
characters: [
{
id: 'keeper',
name: 'Keeper',
x: 520,
y: 540,
greeting: 'Welcome in. Give me a click.',
says: [
'This hall is your plugin’s own. Animal Jam has never heard of it.',
'Talk in the chat bar down there. It never leaves this room.',
'That lamp has been burning since long before you walked in.',
],
path: [
{ x: 700, y: 540 },
{ x: 700, y: 600 },
],
pace: 5,
},
],
});| Field | What it is |
|---|---|
id | Your own name for it, which its events carry. Letters, digits, - and _. |
name | The name over its head, 1 to 20 characters. |
x, y | Where it stands. It has to be somewhere avatars can walk. |
flipped | Facing left. |
look | What it looks like. See below. The player's own animal if you leave it out. |
greeting | What it says once, as the player walks in. |
says | What it says when clicked, one line to a click, starting again at the first. Up to 20 lines. |
path | A round it walks for ever: each point in turn, then back to where it started. Up to 12 points. |
pace | How long it takes over each step of path, in whole seconds. 4 by default. |
clickable | false and clicking it does nothing. The click is still taken either way, see below. |
The game client draws a character from the same packets that draw everyone else, so it has Animal Jam's own name bar, walk and chat balloon. Nothing about it is sent to Animal Jam, and only the player sees it. A room can have up to 8.
What a character looks like
await phantom.aj.customRooms.define({
name: 'Great Hall',
width: 1200,
height: 700,
background: hall,
spawn: { x: 600, y: 560 },
characters: [
{
id: 'keeper',
name: 'Keeper',
x: 520,
y: 540,
look: { animal: 13, colors: [3, 9, 1], wearing: [{ defId: 1234 }], member: true },
},
],
});| Field | What it is |
|---|---|
animal | The animal, by its def id. The player's own animal if you leave it out. |
customAnimal | A recycled animal's id, for one of those. |
colors | Its three colours, as the client packs them. |
eyes, pattern | Its eyes and its pattern, by def id. |
wearing | What it has on, each by def id and colour, up to 24 things. Nothing by default. |
member | Drawn with a member's name bar. |
phantom | Drawn as one of Animal Jam's own Phantoms, over the animal underneath. |
tint | A tint pulled over its colours, 0xAARRGGBB. |
Everything except phantom and tint is free: it goes in the packet that brings the character in, and the client loads the art it needs as it would for a stranger walking into Jamaa. phantom and tint are applied once the character is standing there, so they take a moment to appear and are skipped if the client never gets that far.
Anything you leave out is the player's own: their animal, colours, eyes and pattern. That's the one animal the client is sure to have art for, and it fits in a room only the player sees. Name an animal for a shared room, or everyone sees a character that looks like themselves.
phantom.aj.player.animals.kinds.playedBy() gives the player's own animal and its def id, which is the easiest way to find numbers to use while you build a room.
Clicking a character
Clicking a character says its next line and fires onCharacterClick. The click never reaches Animal Jam or the game's own handler, so no card opens for someone who doesn't exist. That's true for every character, including ones with clickable: false and ones with nothing to say.
A character with no says takes the click and says nothing, leaving the reply to your plugin.
const keeper = phantom.aj.customRooms.character('keeper');
let asked = 0;
phantom.aj.customRooms.onCharacterClick(async ({ character, said }) => {
if (character !== 'keeper' || said) return;
asked += 1;
await keeper.say(
asked === 1 ? 'Welcome in.' : `That is ${asked} times you have asked.`
);
});Controlling a character
phantom.aj.customRooms.character(id) is a handle, made without asking Phantom anything, for things the room's description can't know in advance. Every call returns where the character is afterwards, or null once the player has left the room.
| Call | What it does |
|---|---|
say(text) | Says it in its own balloon. |
walk(x, y) | Walks it there, with its own walk. |
put(x, y) | Puts it there at once. |
hide() | Takes it out of the room, without taking it out of the description. |
show() | Puts it back where it stands. |
change(change) | Several of those at once. |
now() | Where it is, and how many lines it has said this visit. |
const keeper = phantom.aj.customRooms.character('keeper');
phantom.aj.customRooms.onEnter(async () => {
await keeper.walk(700, 540);
await keeper.say('Over here.');
});
for (const character of await phantom.aj.customRooms.characters()) {
console.log(character.name, 'is at', character.x, character.y);
}Whether a character is hidden is remembered while the player is elsewhere, so you can remove one before anyone sees it. Everything else resets each visit to what the room describes: it stands where the room put it and has said nothing.
Characters don't follow the player
This is on purpose. A round is one packet per step, seconds apart, and doesn't read anything about the player. Following would mean reading the player's position several times a second for as long as they're in the room, which is why Phantom Lobby's greeter stopped walking. To bring a character to the player, walk it yourself on onPropNear or onMove, so it only costs a walk when something happens.
Doorways and spawns
An exit means "back where you came from". A doorway says where it goes, so a plugin can have several rooms.
const walledGarden = await phantom.aj.customRooms.define({
name: 'Walled Garden',
width: 900,
height: 550,
background: garden,
spawn: { x: 450, y: 400 },
spawns: [{ name: 'gate', x: 760, y: 470, radius: 20, walkTo: 'default' }],
doorways: [{ id: 'back', x: 820, y: 470, radius: 40, to: { room: 'hall' } }],
});
await phantom.aj.customRooms.define({
name: 'Great Hall',
width: 1200,
height: 700,
background: hall,
spawn: { x: 600, y: 560 },
doorways: [
{
id: 'garden',
x: 1100,
y: 500,
radius: 40,
to: { room: walledGarden },
spawn: 'gate',
},
{
id: 'town',
x: 80,
y: 500,
radius: 40,
to: { jamaa: 'jamaa_township.room_main' },
},
],
});A doorway's to is one of two things:
{ room }: another room your plugin defined, by the iddefinereturned. Define it first, because the doorway is written into the room's file, so the room it leads to has to exist when the room holding it is built. Phantom handles this itself and Animal Jam hears nothing.{ jamaa }: one of Animal Jam's own rooms, by the namephantom.aj.room.jointakes, likejamaa_township.room_main.phantom.aj.world.roomDefinitions()lists every room the client knows, with that name as itspathName, so read it from there rather than guessing. This is a real join: the player leaves the custom room, and your plugin hearsonLeavewithelsewhere.
spawn names which spawn the player arrives at in the room it leads to. Without it they arrive at that room's default, its spawn: { x, y }. A spawn the room doesn't have counts as none, so a doorway never leaves anyone standing nowhere.
spawns are the room's other spawns. Each needs a name of letters, digits, - and _. radius is how far from the point a player may appear (10 by default, the same as spawn), and walkTo names another spawn in the same room to walk to on arrival, so the player walks in through the door rather than appearing inside.
You can't use default as a name in spawns, because that's the room's own spawn.
A doorway to a room this game doesn't have, or to another plugin's room, does nothing: the player stays where they are. Define rooms in the order their doorways need.
A doorway to your own room also does nothing from a shared room. Walking from the shared room into one the player would be in alone would take them out of the instance without asking, so that's left to you: give a shared room an exit, or listen to onMove and enter the next room with shared yourself. A doorway to one of Animal Jam's rooms works from a shared room as anywhere, because it really leaves.
Named areas
volumes are areas of the room the game can find by name. Bouncing and clickable props are built from the same thing; use volumes for areas that aren't props.
await phantom.aj.customRooms.define({
name: 'Great Hall',
width: 1200,
height: 700,
background: hall,
spawn: { x: 600, y: 560 },
volumes: [
{ name: 'bouncevolume', area: { circle: { x: 600, y: 400, radius: 90 } } },
{ name: 'stage', area: { rect: { x: 900, y: 300, width: 200, height: 120 } } },
],
});Each needs a name (letters, digits, -, _ and :) and an area. kind is the game's own volume type, 0 (a plain named area) if you leave it out; message is the message that kind of volume carries; and walkTo: true makes clicking in it walk the player there.
bouncevolume is the one name the game acts on by itself: every avatar standing in it bounces. Only the first one in a room works, so define refuses a second. A prop with bounce: true creates one and counts towards this. Those are written first, so a room with a bouncing prop keeps that one.
How far the view scrolls
In a room larger than one screen, the view follows the player. scroll limits how far: the four numbers are where the view's top left corner can go, so the player can see from xmin to xmax + 900 across and ymin to ymax + 550 down.
Without it, the view covers the whole room and nothing beyond: xmin: 0, xmax: width - 900, ymin: 0, ymax: height - 550.
await phantom.aj.customRooms.define({
name: 'Great Hall',
width: 1200,
height: 700,
background: hall,
spawn: { x: 600, y: 560 },
scroll: { xmin: 40, xmax: 260, ymin: 40, ymax: 110 },
});define checks them against the room's size, so the view can't go off the edge.
Shared rooms
Pass shared: true to put the player in with everyone else who entered the same room from your plugin.
try {
await phantom.aj.customRooms.enter(garden, { shared: true });
} catch (error) {
console.log((error as Error).message);
}People only meet if they're in the same room. Phantom identifies a shared room by your plugin's id and a hash of the whole description: name, size, walkable areas, all the props and the bytes of all the pictures. Change any of that and it's a different room with different people. A room holds 30 people. Once it's full, another copy opens and the next person goes there.
Who can share a room
Rooms are only shared for plugins installed from the Phantom store, and only for a version that's live there. Because the plugin asks for customRooms, someone has reviewed that version. Phantom checks with the store each time someone enters, and if the check fails, enter rejects with a message explaining why.
In Developer mode you can share your plugin's rooms to test them, but only with others running it in Developer mode. An unreviewed room never mixes with the public.
What players see
Other players appear as their own avatars, under their Animal Jam screen names, and their chat shows in the game's balloons.
What your plugin hears
Only the local player, as in a private room: their walking, their chat and the props they touch. Your plugin doesn't get a list of who else is there or hear what they say. Phantom draws the others and relays their chat.
For a list of who's there, and to talk to their copies of your plugin, join a room channel with { scope: 'room' }. It follows the player into the room, and everyone in the same instance is in it. In a room the player entered alone it's a channel of one, so the same panel works either way.
Listing shared rooms
shared() lists the store's shared rooms that have people in them right now, busiest first, across all plugins. You get each room's name, its plugin's title and how many people are in it, but not who they are. Rooms from plugins in Developer mode aren't listed. It needs customRooms at read. Use it to build a room browser.
for (const { name, title, people } of await phantom.aj.customRooms.shared()) {
console.log(`${name} (${title}): ${people} here`);
}Safety
Shared rooms follow the Phantom community rules.
- The player has to be signed in to a Phantom account and playing as an Animal Jam player linked to it. Accounts barred from the community are refused. The ban is checked each time someone enters, not just at sign in.
- Chat is cleaned. Links are dropped, the server masks slurs for everyone, and Phantom masks swearing before it draws a balloon.
- Walking, chat and emotes each have a rate limit.
- Phantom's moderators watch shared rooms live, labelled by room and plugin. They can disconnect someone or bar their account.
Your plugin can let players hide and report people they meet, with phantom.aj.community (the community permission). Hiding is two-way and silent: once hidden, neither player sees or hears the other in any shared room or real-time channel. A report sends Phantom's moderators the last thing that person said in a shared room that reached the player.
await phantom.aj.community.hide('localhost');
await phantom.aj.community.report('localhost', 'Asked for my password');
phantom.aj.community.onHidden((hidden) => {
console.log('hidden:', hidden.map((player) => player.screenName).join(', '));
});
await phantom.aj.community.unhide('localhost');Events
| Event | When |
|---|---|
onEnter | The player arrived in one of your rooms. shared tells you if it's a shared room. |
onLeave | They left. The reason is left (you called leave), exit, elsewhere (map or buddy) or closed. |
onMove | They started walking. Includes where to. |
onChat | They said something. Words, a SafeChat phrase, an emote or a dance, by the game's chat kind. |
onPropEnter | They stepped into a prop's area. |
onPropLeave | They stepped out of it. |
onPropNear | They came within a prop's nearDistance. Fires once each time they approach. |
onPropClick | They clicked a clickable prop, wherever a changeable one has been moved to. |
onCharacterClick | They clicked one of the room's characters, with the line it answered with. |
onObject | The room's objects changed hands or moved, as Phantom's server settled it. |
You only hear events from your own rooms. In a private room, the player's chat shows in their own balloon and goes nowhere else. In a shared room, the others see it too.
Where you can enter from
The player has to be in a normal Animal Jam room, on land, playing a land animal. Dens, parties and adventures are refused because they're hard to restore afterwards. enter rejects with a message explaining why, which is worth showing to the player.
Limits
| What | Limit |
|---|---|
| A picture | PNG or JPEG, 4 MB, 4096 pixels on a side |
| All pictures in a room | 12 MB altogether (a picture used twice counts once) |
| Props, exits | 60 props, 8 exits |
| Doorways, spawns | 8 doorways, 16 named spawns beside spawn |
| Named areas | 32, one of them a bouncevolume |
| A motion's loop | 8 seconds |
| A strip of frames | 2 to 64 frames, 1 to 24 a second |
| A shared room | 30 people, with another copy opening as it fills |
| Objects in a room | 32, each with one word of up to 64 bytes on it |
| Characters in a room | 8, each with up to 20 lines of up to 200 characters |
| A character's round | 12 points, 1 to 60 seconds a step |
define checks all of this, including the bytes of each picture, before anything reaches the game. If something's wrong it rejects and says what, for example a picture that isn't a PNG or JPEG, a damaged or oversized picture, or a spawn point nobody can stand on.
Not supported yet
- Text in a room. Paint it into a picture instead.
- More than one bouncing area per room. The game only bounces avatars in the first one, so
definerefuses a second. - A character that follows the player or wanders. It stands where it's put and walks the round the room gives it (see Characters).
- A character that hands something over, or knows what players have done to the room's objects. Listen to
onCharacterClickand do it in your plugin. - A character that dances, sits or sleeps. It stands and walks.
- Changing the room itself while the player is in it: its background, walls, walkable area, exits and doorways, named areas, scrolling or music. Those are what people meet in. Props are what changes (see Props that change).
- Adding or removing a prop while the player is in the room. Describe it and hide it instead.
- Showing others in a shared room where a plain
changeableprop has moved to. Moving one is local to the player who did it. Give itshared: trueto make it an object everyone agrees on. - Carrying an object out of the room and keeping it. Objects last as long as someone is in the room.
- Giving an object a different picture. Use two props.
- Letting your plugin see or talk to the others in a shared room. Phantom handles that, under the community rules.