The room the client has on screen
phantom.aj.room.current() gives you the room the player is in: its name, its id and whose den it is. Everything else about the room is held by the classes the world's room manager is built on:
room.RoomManagerBaseholds the room itself: the scene it was loaded from, the actor lists that scene gave it, how far it reaches, where the view has scrolled, the walk grid and the music.room.RoomManagerViewholds this computer's mouse and keyboard, and the walk they add up to.room.RoomManagerWorldis the one the game actually runs. It holds the room as a place: which shard, whose den, which minigame opens next, what the camera is doing.room.VolumeManagerholds the room's shapes, androom.RoomObjectthe small animations they drive.room.RoomGridis the grid and the pathfinder.
The SDK covers them in three places: phantom.aj.room (with room.scene, room.volumes, room.camera and room.music), phantom.aj.navigation (the grid and walking) and phantom.aj.dens (a den being laid out). For anything else, use the typed client: phantom.flash.use('room.RoomManagerWorld').
Most of this is never sent to the server. It changes what this screen shows, and the next room load works most of it out again.
What the client knows about the room
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const { aj } = ctx.phantom();
const room = await aj.room.current();
if (!room) return;
console.log(`room ${room.defId} (${room.name})`);
if (room.den?.mine) console.log('this is the player’s own den');
const bounds = await aj.room.scene.bounds();
if (bounds) console.log('it reaches from', bounds);
},
});room.scene reads what the scene was loaded with: its layers and art, spawns, portals, paths and action points, its minimap and, in a dark room, the shapes it's masked with.
The walk grid and the pathfinder
navigation.cellAt gives the client's own collision test for a point, the check it makes on every step. navigation.plan works out a walk over the grid without moving, and navigation.walkTo walks it.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const { aj } = ctx.phantom();
const me = await aj.navigation.position();
if (!me) return;
const walk = await aj.navigation.plan({ x: me.x + 400, y: me.y });
console.log(
walk.verdict === 'reachable' ? `${walk.hops.length} hops` : 'no way through'
);
for (const point of walk.hops) console.log('through', point.x, point.y);
},
});navigation.reach says whether a point can be walked to at all, navigation.distances how far each of several points is on foot, and navigation.tour puts points in the order that covers them with the least walking.
Volumes and the art they drive
room.volumes.all() reads the scene's shapes as the client holds them: the polygon in room coordinates, and whether the client still checks the volume.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const { aj } = ctx.phantom();
for (const volume of await aj.room.volumes.all()) {
console.log(volume.name, volume.kind, volume.enabled ? 'on' : 'off');
}
const me = await aj.navigation.position();
if (!me) return;
const tests = await aj.room.volumes.tests(me);
if (tests.splash) console.log('splashing about in', tests.splash);
},
});A volume can drive a piece of the room's art, like a lamp that lights when the cursor crosses it, a gem that vanishes when clicked, or art that fills up as animals gather. room.volumes.objects() reads them.
Room events
| Listener | What it hears |
|---|---|
room.onJoin | each room the player joins, as the server's answer describes it |
room.onLoaded | the room built: its spawns, portals, volumes, grid and layers there |
room.onLeave | the room taken down, before the next one loads |
room.music.onChange | the room's music changing, whoever changed it |
room.music.onSound | the sounds the room sets off |
room.camera.onChange | the camera moving |
room.players.onChange | who is drawn in the room changing |
room.worldItems.onTaken | a world item's gift taken, whoever took it |
For a room manager method with no listener, hook it on the typed client (see Hooks). Leave the per-frame methods alone: heartbeat, scrollRoom, updateLayerActors and portalCollisionTest run twenty-four times a second, and a hook on any of them sends a message every frame for as long as the room is up.
What not to call
Some of the room manager is reachable but shouldn't be used:
forceInvisModeis set when a join is sent and cleared when the server answers. Writing it between joins changes nothing and desyncs the next one. Useaj.room.join(name, { invisible: true })for an invisible join.- The per-frame methods above take numbers the client has just worked out. Calling one puts the room a frame out of step with itself.
convertPathToWorldSpacemoves every point of every path in the scene, in place, once per load. Calling it again moves them all a second time, with no record of the first.init,destroy, and the volume manager'ssetSceneandreleasebuild and tear down the room. Called at the wrong moment they leave a room on screen that nothing is listening to._bIsMyDenis what shows a den's editing HUD. Set it in somebody else's den and the client offers an edit the server will refuse.
What you can change
These last until you call the function they return: the room's doors (room.scene.blockPortals), its action points (room.scene.blockActionPoints), a kind of volume (room.volumes.block('splash')), a layer or a piece of art (room.scene.hideLayer, room.scene.hideArt), and the camera (room.camera.hold). The camera can also be pointed somewhere and come back by itself (room.camera.focus), kept within your own bounds (room.camera.setBounds) and zoomed (room.camera.zoom).
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const { aj } = ctx.phantom();
const portals = await aj.room.scene.blockPortals();
const splashes = await aj.room.volumes.block('splash');
return async () => {
await portals();
await splashes();
};
},
});The den being laid out
A den's items aren't part of the room's art. They're held by room.DenItemHolder, which is where an edit in progress lives. aj.dens.editor.holder() reads it, pickUp and dragTo move an item the way the mouse does, and onStep hears each step the holder takes. Nothing is sent until aj.dens.items.save().
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const { aj } = ctx.phantom();
const at = await aj.room.toScreen({ x: 400, y: 300 });
if (!at) return;
await aj.dens.editor.pickUp(at);
const holder = await aj.dens.editor.holder();
console.log('holding', holder.selectedInvIdx, 'at', holder.dragPoint);
},
});