How do I do that?
Common tasks and the call for each. Every call is in the SDK reference, by area, with the permission it needs.
The snippets are written as scripts, where phantom is a global. In a plugin it's ctx.phantom() in your logic or usePhantom() in a component. The calls are the same, except a plugin reads a property by calling it: a script's phantom.aj.room.players.all is await phantom.aj.room.players.all() in a plugin (Building plugins).
Write a line into the chat box
To add a line to the chat history panel (a local message, a system message, a note to yourself), use phantom.aj.chat.log. Nothing is sent to Animal Jam and nobody else sees it.
phantom.aj.chat.log.writeAs(name, text) writes a line as that name, and phantom.aj.chat.log.write(text) writes it as the player's own line. notice(text) is a line of your own, in any colour.
await phantom.aj.chat.log.writeAs('Phantom', 'following localhost');
await phantom.aj.chat.log.write('that went to the log, not to the room');phantom.aj.chat.log.clear() empties the panel. See the chat reference.
Put a name, or a label, over somebody's animal
Use phantom.aj.ui.tag(target, text, options). The target is a player's user id, something from phantom.flash.entities, or any place in the client. The label follows the animal: the loader moves it every frame inside the game, so nothing crosses the bridge while they walk, and it hides while they're off screen.
for (const player of await phantom.aj.room.players.all()) {
await phantom.aj.ui.tag(player.userId, player.userName, {
colour: 0x22c55e,
seconds: 10,
});
}Each player gets one tag, so labelling a room again replaces the labels rather than stacking them. See the ui reference.
For a speech balloon instead, phantom.aj.chat.balloons.show(userId, text) puts the game's own balloon over that animal, in your own colour if you like, and it fades after seconds. Nothing is sent to Animal Jam.
Draw a panel that remembers where it was dragged
phantom.aj.ui.window gives you a box with a title bar to drag it by, in Animal Jam's own look. Saving the position is up to you: onMove fires on each drag, and you can save it in ctx.storage so the next session opens it in the same place.
import { definePlugin } from '@phantomclient/plugin';
type Spot = { x: number; y: number };
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const kept = await ctx.storage.get<Spot>('spot');
const window = await phantom.aj.ui.window({
title: 'Trades',
width: 240,
height: 160,
x: kept?.x ?? 40,
y: kept?.y ?? 40,
});
window.onMove((spot) => void ctx.storage.set('spot', spot));
return () => void window.remove();
},
});Hear when the player joins a room, and when somebody walks in
phantom.aj.room.onJoin(listener)fires when the player arrives in a new room, with the room.phantom.aj.room.players.onEnter(listener)fires when somebody else walks into the player's room, andonLeavewhen they go.
const stopJoining = phantom.aj.room.onJoin((room) => console.log('now in', room.name));
const stopArriving = phantom.aj.room.players.onEnter((player) =>
console.log(player.userName, 'walked in')
);Both return a function that stops them, which a plugin can return from setup. See the room reference.
Hear the player click an item, or hover over one
phantom.aj.guiItems.onPress fires on a press on an item in any of the game's windows: the avatar editor, the den editor, the pet list, the buddy list, the trade window, the inbox. onItemOver and onItemOut fire when the mouse arrives and leaves. Each gives you the window, the slot, and the item with its definition id.
const stop = phantom.aj.guiItems.onPress(({ screen, item }) => {
if (item) console.log('pressed', item.name, 'in', screen, 'definition', item.defId);
});You don't need to wait for the window or re-add the listener after it closes. See Hearing the player.
Find out what a button is called
Run this in a script and click the button. The path is what phantom.aj.gui.click, text and on take.
phantom.aj.gui.onPress(({ path, className }) => console.log(path, className));Keep a listener working after the player changes room
If a listener stops firing after the player changes room, it's because the client built a new HUD and your listener is on the old button.
keep on phantom.aj.gui.on follows the button: the listener moves to each new one the client builds. It also waits for a path that isn't there yet, so you can set it up before the HUD is drawn.
const stop = phantom.aj.gui.on('hud.denBtn', 'click', () => console.log('off home'), {
keep: true,
});phantom.aj.gui.onText and onVisible take it too. For anything that isn't a listener, phantom.aj.gui.onBuilt(path, listener) fires for each new element so you can redo your work. See Timing.
Wait for the room, a window or a value, without guessing
Don't use setTimeout after onJoin. onJoin fires on the server's message, before the client has built the room.
phantom.aj.room.onLoaded(listener)andawait phantom.aj.room.loaded(): the room is built and shown.phantom.aj.screens.waitFor(name),phantom.aj.popups.waitFor(match)andphantom.aj.gui.waitFor(query): a window the client is opening.phantom.flash.until(place)waits for any value in the client, andphantom.flash.frames(count)waits a number of the game's frames.
if (await phantom.aj.room.loaded()) {
console.log((await phantom.aj.room.scene.layers()).length, 'layers in this room');
}See Timing.
Measure text before you draw it
phantom.flash.overlay.measure(text, options) measures text the way Flash would draw it, without drawing anything, so you can size a box to fit.
const font = { size: 14, bold: true };
const size = await phantom.flash.overlay.measure('1200 gems', { font, padding: 4 });
const box = await phantom.flash.overlay.rect({
x: 20,
y: 20,
width: size.width,
height: size.height,
fill: { color: 0x000000, alpha: 0.6 },
});
await phantom.flash.overlay.text('1200 gems', { parent: box, x: 4, y: 4, font });For a node that's already drawn, node.bounds() gives its size, and overlay.row and overlay.column lay nodes out in a line by their sizes. See Drawing over the game.
Draw a checkbox, a dropdown, tabs or a slider
phantom.flash.overlay.checkbox, select, tabs, scroller and slider. Each is one call that returns a node, with onChange for its value.
const sort = await phantom.flash.overlay.select({
x: 20,
y: 20,
width: 160,
value: 'newest',
choices: [
{ value: 'newest', label: 'Newest first' },
{ value: 'price', label: 'Cheapest first' },
],
});
sort.onChange((value) => console.log('sorting by', value));phantom.aj.ui has the same controls in Animal Jam's own look, which is usually what you want for a settings panel in the game. These are the plain versions. See Drawing over the game.
Keep something in the middle of the screen
Don't calculate from the stage's size, because the window can be resized. align puts a node against an edge or the middle of whatever it's drawn in, and keeps it there when that changes size.
await phantom.flash.overlay.text('paused', {
align: { x: 'center', y: 'middle' },
font: { size: 24, bold: true },
outline: 0x000000,
centered: true,
});Inside a panel or group it aligns to that instead, so a Close button sits in the corner of its own window and stays there when the window is resized.
Let the player drag something you drew
Set drag on any node, not just a panel. Flash follows the mouse itself, so nothing crosses the bridge during the drag, and on('move') gets where it was dropped.
const knob = await phantom.flash.overlay.circle({
x: 40,
y: 400,
radius: 7,
fill: 0xffffff,
hand: true,
drag: { axis: 'x', left: 40, right: 200, step: 4, live: true },
});
knob.on('move', ({ x }) => console.log('set to', Math.round(((x - 40) / 160) * 100)));axis, left, right, top, bottom and step keep a slider on its track, and live reports each step rather than only where it stopped.
Follow the mouse over something you drew
on('hover') fires as the mouse moves inside a node, and on('wheel') when the wheel turns over it, with how far. Hover doesn't capture the mouse, so the game still gets hover events underneath. Every other mouse event on a node is captured, so a click on your button doesn't also walk the player across the room.
const board = await phantom.flash.overlay.rect({
x: 0,
y: 0,
width: 200,
height: 200,
fill: { color: 0x000000, alpha: 0.2 },
});
board.on('hover', ({ x, y }) => console.log('brush at', x, y));
board.on('wheel', ({ delta }) => console.log('wheel', delta));Change a value in the game and have it stay changed
$set writes a field once, and the client often writes its own value back a frame later: on every frame, on a redraw, or on an object it rebuilds for the next room.
$keep writes it every frame, inside the game, until you call the function it returns, which puts the old value back.
const hud = phantom.flash.use('gui.GuiManager').mainHud;
const faded = await hud.alpha.$keep(0.5);It follows the place rather than the object. It waits while the object isn't there yet, and when the client rebuilds it (the HUD for the next room, an avatar redrawn) the new one gets the value too. phantom.flash.hide, recolor and style work the same way. $keep is phantom.flash.hold on one field, and takes the same options.
Add a menu of your own
phantom.aj.ui.menuButton is a button that opens a menu. Put it on the HUD with phantom.aj.ui.attach and it stays there in every room.
const tools = await phantom.aj.ui.menuButton(
'Tools',
[
{ label: 'Go home', onSelect: () => void phantom.aj.dens.joinOwn() },
{ label: 'Wave', value: 'wave' },
'-',
{ label: 'Coming soon', disabled: true },
],
{ skin: 'game', width: 70, height: 24 }
);
await phantom.aj.ui.attach('hud', tools);
tools.onSelect((value) => console.log('picked', value));phantom.aj.ui.menu(items) opens a menu on its own at the mouse, and resolves with what was picked, or null if the player clicked away. The items can be a function, called each time the menu opens, for a menu that changes with the game.
Tell the player something, without a window
phantom.aj.ui.notice(text) shows a line on screen that goes away by itself. To ask a question, use the game's own popups, phantom.aj.ui.popup.okCancel, yesNo and the rest, or phantom.aj.ui.prompt to ask for text (in the game skin, with the client's own popup and text field; see Asking the player something).
await phantom.aj.ui.notice('your shop sold 3 things');
if (await phantom.aj.ui.popup.yesNo('Follow them?'))
await phantom.aj.player.avatar.walkTo({ x: 400, y: 300 });Put my own thing inside one of the game's screens
Overlay nodes are drawn above the whole game. That's fine for a label over an animal, but not for a badge on a HUD button or a shadow on the floor.
phantom.flash.overlay.attach(place, node)draws it inside one of the client's own containers, so it moves, scales and hides with it.phantom.flash.overlay.replace(place, node)hides one of the client's display objects and puts your node where it was.phantom.aj.ui.attach,replaceandplacesdo the same with the client's screens already named for you.
Places are looked up again from time to time, so a HUD the client rebuilds is found again. See Drawing over the game.
Run something for hours, and have it survive a reconnect
Don't start a loop in setup. The game's page reloads when the game switches server or a dropped connection reconnects, and a loop in setup usually catches that as a failure and never starts again.
Declare an automation instead. Phantom starts it when the game is ready, cancels it when the game goes, starts it again when the game comes back (with a growing gap between tries), and gives back its saved notes so it carries on.
import { defineAutomation, definePlugin } from '@phantomclient/plugin';
export default definePlugin({
automations: {
adventure: defineAutomation({
requires: ['game', 'room'],
notes: () => ({ runs: 0 }),
async run(task, phantom) {
while (true) {
task.step(`Run ${task.notes.runs + 1}`);
await phantom.aj.adventures.lobby.join(23, { autoStart: true });
task.save({ runs: task.notes.runs + 1 });
await task.sleep(4_000);
}
},
}),
},
});Every wait on the task throws as soon as the run should stop, so your loop unwinds through its own finally blocks. See Automations, and Keeping state for the same saved state without an automation.
Not listed here
- The SDK reference has every call, by area, generated from the SDK itself.
- Reaching the game client covers
phantom.flash: any class, method or field in the running game, private ones included. If you had to use it for something the SDK should cover, tell us.
If you searched for something and it isn't here, say so in the community and we'll add it.