Changing the game
The areas of phantom.aj can change things as well as read them: a value on the HUD, a pet's size, the time left in a minigame, the room's zoom. Calls that change something come in three kinds:
| Call | What it does | What it gives back |
|---|---|---|
setX | Makes the change once, the way the game itself would make it | What it changed, or it throws |
keepX | Makes the change and holds it, however often the game puts its own value back | A handle that puts it back |
| an action | Does what a click would: trick, whack, showTab, turnAhead and many more | What happened, once the game shows it |
Each one was written against the game's ActionScript and goes through the same method the game uses for that change, so the screen updates. An action that doesn't happen throws a PhantomError (REFUSED, TIMEOUT, NOT_READY) rather than returning false. They all need "game": "act".
Set or keep
The game resets a lot of its own values. Some every frame (a speed, a timer, a scale), some every redraw, and some whenever it rebuilds a screen, which for the HUD is every room change. A change made once is gone a moment later and looks like it never worked. keep holds it.
const zoomed = await phantom.aj.view.keepZoom(150);
const told = await phantom.aj.hud.keepLabel(
'objective',
'Find the chest by the waterfall'
);
await zoomed();
await told();A keep runs inside the game, so nothing is sent between your code and the game while it lasts. It follows the thing rather than one copy of it: it waits while the thing doesn't exist yet, and when the game rebuilds it, the new one is changed too. The handle it returns puts the game's own value back.
Use set for a one-off change and keep for one that should stay. Areas only offer keep where the game would otherwise undo the change.
Your pet
await phantom.aj.player.pets.world.trick('dance');
const big = await phantom.aj.player.pets.world.keep({ scale: 2 });The client walks the player's pet a frame at a time towards whichever side of the animal it has picked. phantom.aj.player.pets.world.walk() reads that walk, and world.keep({ side }) holds the pet on one side, overriding the client's choice:
const walk = await phantom.aj.player.pets.world.walk();
if (walk) {
const held = await phantom.aj.player.pets.world.keep({ side: walk.limits.maxX });
}One keep takes the side, the scale and whether it's shown, and one handle puts them all back. world.drawLook draws a look straight into the pet's art, world.still() holds it on its current frame, and world.poke and world.sparkle are the click hop and the sparkle. Those last two report whether the room sees them in seenBy.
Other animals
phantom.aj.avatars is every animal in the room as this screen draws it, and most of what it changes is only on this screen. The room still hears where the player's own animal ends up, because the client sends that itself, but it's never told about a splash, a held animation or a tint.
const me = (await phantom.aj.player.current())?.userId;
if (me !== undefined) {
const muddy = await phantom.aj.avatars.drawing.splash(me, 'mud');
const pinned = await phantom.aj.avatars.motion.holdAnimation(me, 23);
}phantom.aj.avatars.drawing.tint refuses the player's own animal, as the client's own call does. The player's tint is worked out every frame from the splash volume they're in, so a write would be undone on the next frame. phantom.aj.avatars.drawing.reset runs the client's own calls for refreshing a stale piece of drawing:
await phantom.aj.avatars.drawing.reset('nameBars');
await phantom.aj.avatars.drawing.reset('positionThrottle');Floating, bouncing and the rest of how the client moves an animal aren't wrapped. They're the view's own calls (setBounce, doBounce, setPath), one step away through phantom.flash.
Clothing and the server
phantom.aj.player.looks.putOn dresses an animal properly: it works out what the player owns, sends it and waits for the answer. phantom.aj.player.looks.hideShown only stops the client drawing clothing. The client clears each piece's in-use flag as it hides it, so until something puts them back, the inventory shows them as off while the server still has them on.
await phantom.aj.player.looks.hideShown('clothing');
const me = (await phantom.aj.player.current())?.userId;
if (me !== undefined) await phantom.aj.avatars.drawing.reloadClothing(me);The rest of phantom.aj.player.looks works the same way. drawColors repaints the animal the client holds, so every view of it updates and nothing is sent; phantom.aj.player.avatar.colors.paint changes the colours for real. combatValues is read-only on purpose. The client adds up what an animal wears to draw a health bar, but the server does its own sum and never sends it, so a bigger attack written there would show on screen and change nothing in an adventure.
The fitting room
phantom.aj.player.looks.tryOn puts a piece on the animal that the player doesn't own and may never be able to buy. It builds the item from the client's own definition table and adds it to the record the client draws from, so it shows straight away. The server is never told, nothing is bought, and nobody else sees it.
await phantom.aj.player.looks.tryOn(222);
console.log(await phantom.aj.player.looks.tryingOn());
await phantom.aj.player.looks.takeOff(222);The client counts a tried-on piece as worn while it's on, so combatValues changes with it. That's harmless for the reason above, but surprising if you're reading those numbers. A server switch takes everything off, because the client doing the drawing is gone; put pieces back from phantom.flash.onReload if they should stay on. Anything that can't be drawn on an animal is refused, including den items, pets and ids from a def pack this client never loaded.
takeOff only removes what tryOn put on. Pieces the player owns come off through Animal Jam.
Minigames
Most minigames have their own area under phantom.aj.minigames, named after the game: fastFoodies, whackPhantom, microCrane, bowling, ticTacToe and the rest. They all share a core: state(), onState, whenState, set and keep for writable fields, client() for the game's own class, and leave. Each also has the actions the player takes in that game.
const game = await phantom.aj.minigames.fastFoodies.state();
if (game) console.log('stage', game.stage, 'done', game.stageDone);
const time = await phantom.aj.minigames.whackPhantom.keepSecondsLeft(30);state() reads the game once. To follow it, onState reads the game's members inside the game every few frames and calls you only when one changes, with the previous state. It's tied to the minigame manager rather than one game instance, so it survives the game being left and reloaded: it reports null while another game or none is on screen, and picks up the next one by itself.
const stop = phantom.aj.minigames.touchPool.onState(
(pool) => {
if (pool) console.log(pool.gems, pool.touched);
},
{ everyFrames: 6 }
);Watching suits numbers that change gradually. For one-off moments, the busier games hook the client's methods instead, so your listener runs at the same moment as the game: onDialog for each card and its buttons, plus one per game for what that game does to its board, water, stage or rails.
const judged = phantom.aj.minigames.trueFalse.onJudged((judged) => console.log(judged));
const cards = phantom.aj.minigames.trueFalse.onDialog((what) => console.log(what));A game rebuilds everything from scratch when it's left and reloaded, so handles and holds from the last round go stale. When onState goes to null and back, drop them and set them up again.
Anything the area doesn't cover is in client(), the game's own class, typed and reached through the manager each time. Sounds are a good example: every game loads its sound cues by name and plays one by reading the name back, so client() is how you play one or point it at another file.
const twister = phantom.aj.minigames.twister.client();
console.log(await twister.$describe());The minigames reference lists every game and its calls. phantom.aj.minigames.play, send and onMessage work for any game, whether it has its own area or not.
Game events
Some things aren't values to watch: a shot fired, a hit landed, a tower placed, a card filled in. Those are the game's own methods running, so the busier areas hook them and give you typed events. Each listener removes its hook when you stop it, and reads what it needs inside the call, before the game moves on.
const stop = phantom.aj.minigames.fortSmasher.onShot(({ pull, angle, fruit }) => {
console.log(`fruit ${fruit} at ${angle} with ${pull}`);
});
const hits = phantom.aj.minigames.fortSmasher.onHit(({ impulse }) =>
console.log(impulse)
);
const frames = phantom.aj.minigames.towerDefense.onState(
(state) => {
if (state) console.log(state.wave, state.lives);
},
{ everyFrames: 30 }
);Result cards are listen-only. Fort Smasher's endLevel, Tower Defense's two game over cards and Best Dressed's results all end in addGemsToBalance, so onResult hears them, but no call shows one.
What you can't write
Areas follow the client's own line. A clock, a speed, a paddle or a turn timer is worked out by the client, so it can be held. A gem count or an account value isn't: the server checks or keeps that number, so writing it gets the round refused rather than won. set and keep only accept fields the game's definition marks writable, and throw REFUSED for the rest. The comment on each area says which is which.
Turning off the client's background checks
Not every change is a value. The client runs its own timers, and one can get stuck asking a question the server has already refused. The Jammer Wall is the clearest case: while it's closed, the client checks your wall every sixty seconds so the HUD button can glow when someone posts. If the wall service answers unavailable, it shows a popup and keeps checking. On an account the service keeps refusing, that's a popup and a request you never asked for.
phantom.aj.playerWall.checks exposes the check as a reader, writers and a listener, so a plugin can see it, hear the refusal and stop the checking instead of only hiding the popup.
const quiet = await phantom.aj.playerWall.checks.quietWhenUnavailable();
quiet.onQuiet(({ userName, reason }) => {
console.log(`stopped checking ${userName}'s wall: ${reason}`);
});
const check = await phantom.aj.playerWall.checks.background();
if (check.running) await phantom.aj.playerWall.checks.setEvery(15 * 60 * 1000);Tell the player what they lose. While the check is off, the client never hears about new posts on their wall, so the HUD's wall button stops glowing and its count stops going up until the wall is opened. A wall that's unavailable now may become available later, when a membership starts or the service comes back, so turn the check back on rather than leaving it off for good. setEvery is a middle ground: every fifteen minutes instead of every minute.
There's no packet for any of this, because the wall uses its own HTTP requests. The timer stopping is detected inside the game rather than polled from your code, so a wall that's being checked normally costs nothing:
const watching = phantom.aj.playerWall.checks.onBackgroundChange((check) => {
if (check.present && !check.running) console.log('the client has stopped checking');
});Setting a window's controls
You can set a window the player has open the same way its own controls do. The areas run the handler the client's button runs, or write the same field that button writes, so the window carries on as normal.
await phantom.aj.screens.museum.setAmount(100);
await phantom.aj.screens.museum.setCause(2);None of this is sent to Animal Jam. The donate button still sends the donation, the report button still sends the report, and the pet inventory still asks to switch pets as it closes. That's why phantom.aj.player.pets.inventory.setSwitchOnClose(false) lets you browse the pets without changing the one that's out.
Values a window shows from the server's answers can be read but not written. The gems the museum donation counted, a switcher's counter, the likes under a masterpiece and the gifts the startup popups hold all belong to the server. Writing them would only change the number the player sees.
Mostly only your screen
Most of these change what the game shows on this computer. Animal Jam's server isn't told, so nobody else sees it, and the server still decides what's real. A keep on a minigame's clock gives you longer to play, but the score is still checked when it's sent. Each call's reference entry says whether it stays on your screen or sends something.
Any field
When no area has what you want, the client's own fields are available through phantom.flash. $set writes one once, and $keep holds it, following the thing like every keep above.
const hud = phantom.flash.use('gui.GuiManager').mainHud;
const faded = await hud.alpha.$keep(0.5);If you're doing this for something a player would expect an area to have, report it as a gap in the SDK. See Reaching the game client.
The room on screen
The room the client has on screen isn't the room Animal Jam says you're in. It's the scene this computer loaded, the camera, the keys and mouse it walks by, and some lists it keeps as it goes. None of that is sent, so phantom.aj.room writes the room manager's own fields where the client reads them later, and nowhere else.
The camera is a good example. Scrolling moves the room, and the client clamps that to the bounds the scene gave it, so to look at the art outside a room you have to move the bounds first:
const inside = await phantom.aj.room.scene.bounds();
if (inside) {
await phantom.aj.room.camera.setBounds({
min: { x: inside.min.x - 800, y: inside.min.y - 800 },
max: { x: inside.max.x + 800, y: inside.max.y + 800 },
});
const held = await phantom.aj.room.camera.hold();
await phantom.aj.room.camera.scrollTo({ x: -1400, y: -900 });
}camera.focus is the client's own move out, hold and return, the one cutscenes use.
Walking goes through phantom.aj.navigation. steer holds arrow keys down as the room hears them, so a side-scrolling adventure reads it exactly like a real key. plan works out the route over the room's grid before a step is taken, and walkTo walks it:
const plan = await phantom.aj.navigation.plan({ x: 900, y: 500 });
if (plan.verdict === 'reachable')
await phantom.aj.navigation.walkTo({ x: 900, y: 500 });
else console.log('no way through');The walk itself is sent, as walking always is. The keys, the plan and the camera aren't.
A room is also full of things the client tests the player against every frame: doors, action points, splash volumes, social art. Each is a list, and emptying one lets a tool cross a room without a shop opening or a door taking it somewhere:
const doors = await phantom.aj.room.scene.blockPortals();
const shops = await phantom.aj.room.scene.blockActionPoints();
await doors();
await shops();Putting a list back reads it from the scene again, so it comes back as it was. Joining a room rebuilds all of it anyway.
What the room believes the server said stays read-only: the room's def id, its environment type, the zoom it was given and the last room a join succeeded in are the client's copies of Animal Jam's answers. Writing one would only change what you read back. See The room the client has on screen.
Adventures
In an adventure, the client decides every frame whether a phantom has noticed you, whether it's in a wall and where its next step lands, so writes there are gone within forty milliseconds. What the adventure set on an actor once, and the client never writes again, can be changed, and it's most of what makes a phantom dangerous.
const actor = await phantom.aj.adventures.actors.place('phantom_1');
if (actor) {
await actor._actorDefaultInnerRadius.$set(40);
await actor._setSeekVelocity.$set(30);
await actor._targetRangeMax.$set(0);
}actors.place gives the client's own QuestActor, typed. One of these does reach Animal Jam, through the walk: for an actor the client walks itself, it reports the speed it used in its position updates, so a slowed phantom is slower for everyone in the room. Everything else stays on this screen.
What the server decides stays read-only: the crystal, gem and experience totals on the reward screen, the seed counts in the satchel, what a chest holds. The server keeps its own count and checks every pick, so writing one only changes what the player sees. Automating an adventure has the rest.
Animals and pets: drawing versus state
The animal and pet classes are the deepest in the client, and almost everything in them is one of three things: a value the frame writes, a value the client sends, or the client's cached copy of Animal Jam's answer. What's left is small, and it's all about drawing.
Two of the client's tables are worth knowing. Every action in the game, for animals and pets, is matched against a static array of names and animation ids, so changing one row changes what :dance: does for the whole room on this screen. Each returns a handle that restores the table:
const sleepy = await phantom.aj.avatars.motion.setAction(':sleep:', {
land: 23,
water: 38,
});
const petSleep = await phantom.aj.player.pets.world.setTrickAnimation('sleep', 33);The sparkle on the player's pet is a field on the avatar manager, read when it draws:
await phantom.flash.use('avatar.AvatarManager')._playerPetSparkleId.$set(1);
await phantom.aj.player.pets.world.redraw();A den mannequin is half sent and half not. The den save sends Animal Jam the three colours, the eyes and pattern, and the five inventory ids (DenXtCommManager.requestDenStateChange), so none of those can be written. The five definition ids and colours alongside them are what the client builds the clothing from for a mannequin that isn't the player's, and they're never sent:
const [dummy] = await phantom.aj.player.looks.mannequins.all();
if (dummy) {
await phantom.aj.player.looks.mannequins.dress(dummy.invId, {
head: { defId: 600, color: 0x11111111 },
});
}Three things look writable but aren't:
- The player's own opacity and splash colour on
AvatarManagerare sent, on a twelve-frame throttle, so writing one tells the room.phantom.aj.avatars.drawing.opacitywrites the view's own alpha instead, which isn't sent. - The width the room tests an animal against is only read by the portal test, so widening it makes the client join a room as soon as the player walks near a door rather than onto it.
- The account flags cached on
UserInfoCachedon't decide anything. Membership, the safe chat level, the privacy settings and the buddy count are Animal Jam's answers, and writing one only shows the player a button the server will refuse.
Minigames: what the gems decide
Each minigame's set and keep accept values the game reads later and Animal Jam never hears about, which is mostly what's drawn and how the course is built.
await phantom.aj.minigames.windRider.set({
background: 0,
foreground: 0,
countdown: 0.001,
});
const climb = await phantom.aj.minigames.windRider.state();
if (climb) await phantom.aj.minigames.windRider.set({ course: climb.course + 1 });
await phantom.aj.minigames.towerDefense.set({ endless: true, speed: 4 });set doesn't check the game's own limits, so read state() first when a number has a range, like the course.
Where a game keeps a table of numbers rather than a field, the area reads the table, merges in your values and writes the whole thing back. Fort Smasher weighs every piece of fort and every fruit from _materials and _projectiles:
await phantom.aj.minigames.fortSmasher.setMaterial(3, { strength: 0.1 });
await phantom.aj.minigames.towerDefense.setTowerRange('plant', 5);Scroll numbers, arrow flags, course rows and the pools behind them are read every frame but only worked out when a game starts, so one write lasts for the rest of the game. The same goes for timers that only gate a game's own cards: Sky High's ready countdown, the popcorn machine's "ready, set, go" and the claw machine's "go".
Anything the server calculates a reward from stays read-only. The ticket games hash their score with two serials the server issued and send it as gr. Every addGemsToBalance folds its amount into a hashed mg packet, so a gem purse is read-only. Every requestSetUserVar sends its value as it is, so the counters behind them are read-only too: the touch pool's log book is two bit fields sent as user vars 299 and 300. Each one's reference entry names the line that sends it.
Windows, shops and system managers
These classes are mostly copies of something Animal Jam owns: a shop's stock, a den's eco score, a masterpiece's maker. What's writable is what's only on screen: the text in a box, which tab is open, a remembered index, and flags that only affect the client's own drawing.
The item picker used by trades, Jam-a-Gram gifts and den shops can be set the way its own tabs, sort and search box set it:
await phantom.aj.trades.itemPicker.showTab('den');
await phantom.aj.trades.itemPicker.setSort(3);
await phantom.aj.trades.itemPicker.clearSearch();The avatar editor has two similar numbers, both statics it reads when building a list: which list the trade view opens on, and how far down the list starts. Everything else in that window is the player's wardrobe or what a save will send.
const editor = phantom.flash.use('gui.AvatarEditor');
await editor._tradeListViewType.$set(0);
await editor._mainScrollYPosition.$set(0);The open book turns the way its corners turn it. A page turn is only the client drawing paper, and tells Animal Jam nothing:
await phantom.aj.journeyBook.book.turnAhead();
await phantom.aj.journeyBook.book.turnTo(4, { flip: true });What the book counts a page by stays read-only. A Journey Book gift is kept or discarded against the page the book thinks it's on, so writing the page would claim a gift for a page nobody read.
The theatre's player can loop instead of closing. That changes the film in front of the player, not what the server sent:
await phantom.aj.movies.selector.play(0);
await phantom.aj.movies.player.setRepeat(true);The HUD lets you set the number on the den button and the room label along the top. The den count also controls the doorbell: the client only rings for a count that has gone up, so setting it above the real one stops the next visitor ringing.
await phantom.aj.hud.setDenVisitorCount(0);
await phantom.aj.hud.setRoomLabel('Jamaa Township');The chat bar's approved-words typing shows three completion balloons, underlines unapproved words in red and won't send a message containing one. Its word list, validation and send check stay the client's. You can read its settings, add a word through the client's own method, reset it, and choose which part of the list it predicts from.
const typist = await phantom.aj.chat.bar.predictive();
if (typist && typist.dictionaryLoaded) {
await phantom.aj.chat.bar.setUseType('chat');
await phantom.aj.chat.bar.addWord('awesome');
}The grid drawing studio has a pan and zoom the client was built for but never put on screen, and the painting studio's six brush widths can be changed.
await phantom.aj.artStudio.grid.setView({ scrollX: 8, scrollY: 8, zoom: 2 });
await phantom.aj.artStudio.canvas.setBrushWidths([1, 2, 4, 8, 16, 32]);Across all of these, anything a request carries or triggers stays read-only. The startup popups' waiting gifts are the clearest case: the gift is credited when Animal Jam allows it, so writing the number only misleads the player. A masterpiece's maker, picture id and frame version are sent with a like, and the maker's name with a report. The museum's donation amounts are what requestDonateGems sends. The facts and polls managers have nothing writable at all, because each cached definition holds the stat a vote or found fact is written to.