Building plugins
A plugin is a small web app that runs next to the game in Phantom. It can add a panel beside a game, a tile on the canvas, a page in the sidebar and commands in the game's plugins menu. It can read the game, draw over it and, if it asks for permission, act in it.
Each plugin runs in its own sandboxed frame. It can't see the app, other plugins, the player's Phantom account, their Animal Jam sign-in, or any game it wasn't allowed on. ctx.phantom() gives it the same flash, aj and packets SDK Phantom uses, and every message it sends the game is checked against what the player allowed.
Create a plugin
npm create @phantomclient/plugin@latest
cd my-plugin
npm install
npm run devIt asks which starters you want:
- Overlay: draws over the game, with typed settings and a keyboard shortcut.
- Page: a Vue page of its own under Plugins, which sees every allowed game.
- Panel: a Vue panel docked beside a game.
- Command: an entry in a game's plugin menu, run on that game.
- Colour theme: colours only, no code.
In Phantom, turn on Developer mode in Plugins → Developer. Your plugin loads from its dev server, marked Development. Allow it on a game and open the game.
The template installs two packages: @phantomclient/plugin, the API you import, and @phantomclient/plugin-cli, which gives you the phantom-plugin command the package scripts run.
What's in a plugin
A package.json with the manifest in its phantom field, and an entry file that exports definePlugin.
{
"name": "phantom-plugin-hide-strangers",
"version": "1.0.0",
"phantom": {
"id": "you.hide-strangers",
"title": "Hide strangers",
"description": "Hides everyone in the room but you.",
"author": "You",
"api": "^0.2.0",
"runs": "each-game",
"requires": ["game"],
"permissions": { "game": "read" },
"config": {
"hideKey": {
"type": "keybind",
"name": "Show or hide them",
"default": "Ctrl+Shift+H"
}
},
"contributes": {
"commands": [{ "id": "where", "title": "Say where I am" }]
}
}
}import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const { flash, aj, keybinds } = ctx.phantom();
const me = (await aj.player.current())?.userName;
const rule = await flash.ui.rule('avatar.AvatarWorldView', (view) => view.hide(), {
unless: async (view) => (await view.userName.$get()) === me,
});
const key = keybinds.on(ctx.config.hideKey, () => ctx.toast('Pressed'));
return async () => {
await key();
await rule();
};
},
commands: {
where: async (_input, { aj }) => {
const room = await aj.room.current();
return { message: `You are in ${room?.name ?? 'no room yet'}`, tone: 'info' };
},
},
});runs: "each-game" starts the plugin once for each game it's allowed on. "single" starts it once for all of them. requires: ["game"] holds setup back until the client is in the world, and Phantom shows the player what the plugin is waiting for.
If setup throws, the error, its code and its stack show on the plugin and in its log. Throw NotReady (or the SDK's NOT_READY) to have Phantom run setup again later, with a backoff.
Permissions
permissions lists everything the plugin may do. The player sees each line before installing:
| Permission | What it allows |
|---|---|
"game": "read" | See the game, hear what happens in it, draw over it, hide, restyle |
"game": "act" | All of that, and act as the player: call, set, override, drive |
"packets": "read" | Hear the game's traffic |
"packets": "send" | Send it, and change or drop it on its way |
"network": ["host"] | ctx.fetch to those HTTPS hosts |
"clipboard", "notifications" | Copy text; show desktop notifications |
"microphone" | Hear the microphone, once the player turns it on; never the camera |
Each message is checked by what it does as it leaves the plugin's frame, so a read plugin can't act. A refusal names the permission it needed and the line to add. A person reviews act and send before the plugin is listed. See Permissions.
Plugins for the private server
Phantom runs its own private server, free for anyone with a Phantom account. A plugin made only for it says so in its manifest:
"phantom": {
"servers": ["private"]
}npm create @phantomclient/plugin -- --private-server starts a plugin with this already set.
Private server plugins are free for everyone: an account with no plan can find them in the store, install them, set them up and run them. Other plugins need a Phantom plan.
Phantom keeps a private server plugin out of every Animal Jam game, plan or not. ctx.phantom() only works with private server games, and a call that names any other game is refused with GAME_NOT_ALLOWED. It can't reach Animal Jam's web addresses either (animaljam.com, wildworks.com, ajcontent.akamaized.net, smartbombinteractive.com). Permissions, review, settings and the dev loop all work as for any other plugin, and developer mode runs it for a free account too.
A plugin that works on both can be free on the private server and on a plan on Animal Jam, in one listing, by naming both servers:
"phantom": {
"servers": ["private", "animal-jam"]
}servers | Where it runs | Who can run it |
|---|---|---|
| left out | Every game: Animal Jam and the private server. | Accounts with a plan. |
["private"] | Private server games only. | Everyone, free. Use it for plugins that never touch AJ. |
["private", "animal-jam"] | Every game. Free on the private server; a plan for Animal Jam. | Everyone on the private server; a plan for Animal Jam. |
["animal-jam"] | Animal Jam games only, never the private server. | Accounts with a plan. |
For an account with no plan, a plugin that names both is treated exactly like a private server plugin: it only runs in private server games, other games are refused with GAME_NOT_ALLOWED, and ctx.fetch can't reach Animal Jam's web addresses. Once the account has a plan it runs in Animal Jam games too, straight away, and stops again when the plan ends. Leaving servers out is different: that plugin needs a plan everywhere.
The store tags both kinds Private server and lists them on the private server's plugins page. See servers.
Settings
Put anything the player can set in the manifest's config. Each item has a type (boolean, number, string, select, colour or keybind), a name the player sees, and a default. Phantom stores the values, draws the form, checks each value and passes them to your code as ctx.config. phantom-plugin writes src/phantom-config.d.ts from package.json, so a misspelt name won't compile. ctx.onConfigChange fires on every change.
Develop
npm run dev serves the plugin with hot reload, for logic as well as components. A component reloads in place and keeps its state. When src/index.ts changes, the running setup's cleanup runs and is waited for, the SDK ends every hook, rule, overlay, watch and key the old code held, and setup runs again with the new code. You don't restart the app.
While it's served, Phantom adds a Dev tools panel beside the game. Click anything in the game to see its class, path, live members and the flash.ui.rule selector that matches it, and copy it as code. There's also a console with phantom bound to the running game. See Dev tools. Logs and errors print in the terminal too.
Build and publish
npm run validate # the manifest against src/index.ts, and the permissions it asks for
npm run build # dist/plugin.js and dist/plugin.css
npm run pack # the .tgz with only what the plugin ships
npx phantom-plugin login
npx phantom-plugin publishSee Publishing.