Examples
Two ready-to-run plugins. Both live in the examples/ folder of the repo — copy, build, drop into scripts/, done.
hello — the smallest plugin
One command, one join listener. This is exactly what paperscript init scaffolds:
examples/hello/src/index.ts
ps.onEnable(() => {
ps.logger.info('Hello from a TypeScript plugin!');
ps.commands.register(
'hello',
(ctx) => {
ctx.sender.sendMessage('Hello, ' + ctx.sender.name + '!');
},
'Say hello',
'/hello'
);
ps.events.onPlayerJoin((event) => {
event.player.sendMessage('Welcome, ' + event.player.name + '!');
});
});
ps.onDisable(() => {
ps.logger.info('Hello plugin disabled.');
});essentials — a real command suite
A full Essentials-style plugin in ~370 lines of TypeScript:
/spawn /setspawn /tp /tpa /tpaccept /heal /feed /fly /gamemode /sethome /home /delhome
Chat theme & helpers
Every message goes through one gradient prefix and a few MiniMessage helpers, so the whole plugin looks consistent:
examples/essentials/src/index.ts
// One chat theme for the whole plugin — MiniMessage.
const PREFIX = "<dark_gray>[<gradient:#ff7a18:#af52de>Essentials</gradient>]</dark_gray> ";
const ok = (t: string) => `<green>${t}</green>`;
const fail = (t: string) => `<red>${t}</red>`;
const info = (t: string) => `<gray>${t}</gray>`;
const hi = (t: string) => `<gold>${t}</gold>`;
const aqua = (t: string) => `<aqua>${t}</aqua>`;
interface MessageTarget { sendMessage(msg: string): void; }
function tell(target: MessageTarget, text: string): void {
target.sendMessage(PREFIX + text);
}
// Resolve the sender as a Player, or tell them why not.
function self(ctx: CommandContext): Player | null {
if (!ctx.sender.player) {
tell(ctx.sender, fail("Players only."));
return null;
}
const p = ps.players.get(ctx.sender.name);
if (!p) tell(ctx.sender, fail("Could not find your player."));
return p;
}
// Find an online player by name, with a themed error if missing.
function findPlayer(name: string | null, ctx: CommandContext): Player | null {
if (!name) { tell(ctx.sender, info("Specify a player name.")); return null; }
const p = ps.players.get(name);
if (!p) tell(ctx.sender, fail(`Player ${hi(name)} is not online.`));
return p;
}
function needOp(ctx: CommandContext): boolean {
if (!ctx.sender.op) { tell(ctx.sender, fail("Not enough permissions.")); return false; }
return true;
}/spawn
examples/essentials/src/index.ts
ps.commands.register(
"spawn",
(ctx) => {
const p = self(ctx);
if (!p) return;
const w = ps.worlds.get(p.location.world) ?? ps.worlds.all()[0];
if (!w) { tell(ctx.sender, fail("World not found.")); return; }
p.teleport(w.spawnLocation);
tell(ctx.sender, ok("Teleported to spawn."));
},
"Teleport to spawn",
"/spawn"
);/tp — overloads & op checks
examples/essentials/src/index.ts
ps.commands.register(
"tp",
(ctx) => {
const a = ctx.arg(0);
const b = ctx.arg(1);
if (a && b) {
// /tp <from> <to> — operators only
if (!needOp(ctx)) return;
const from = findPlayer(a, ctx);
const to = findPlayer(b, ctx);
if (!from || !to) return;
from.teleport(to.location);
tell(ctx.sender, ok(`${hi(from.name)} teleported to ${hi(to.name)}.`));
} else if (a) {
// /tp <to> — self teleport
const p = self(ctx);
if (!p) return;
const to = findPlayer(a, ctx);
if (!to) return;
p.teleport(to.location);
tell(ctx.sender, ok(`Teleported to ${hi(to.name)}.`));
} else {
tell(ctx.sender, info("Usage: /tp <player> or /tp <a> <b>"));
}
},
"Teleportation",
"/tp <player>"
);/tpa — scheduler for expiring requests
examples/essentials/src/index.ts
interface TpaRequest { from: string; task: number; }
const pending = new Map<string, TpaRequest>(); // key = target name
ps.commands.register("tpa", (ctx) => {
const p = self(ctx);
if (!p) return;
const t = findPlayer(ctx.arg(0), ctx);
if (!t) return;
const prev = pending.get(t.name);
if (prev) ps.scheduler.cancelTask(prev.task);
// auto-expire after 60 seconds
const task = ps.scheduler.runTaskLater(() => pending.delete(t.name), 20 * 60);
pending.set(t.name, { from: p.name, task });
tell(ctx.sender, ok(`Teleport request sent to ${hi(t.name)}.`));
t.sendMessage(PREFIX + info(`${hi(p.name)} wants to teleport to you. `)
+ aqua(`/tpaccept ${p.name}`) + info(" (60s)."));
}, "Request a teleport", "/tpa <player>");/sethome + /home — persistent storage
examples/essentials/src/index.ts
type HomeMap = Record<string, Location>;
function loadHomes(): HomeMap {
const raw = ps.storage.get("homes");
if (!raw) return {};
try { return JSON.parse(raw) as HomeMap; } catch { return {}; }
}
function saveHomes(map: HomeMap): void {
ps.storage.set("homes", JSON.stringify(map));
}
ps.commands.register("sethome", (ctx) => {
const p = self(ctx);
if (!p) return;
const name = ctx.arg(0) ?? "home";
const homes = loadHomes();
homes[name] = p.location; // Location is a plain JSON-safe object
saveHomes(homes);
tell(ctx.sender, ok(`Home ${aqua(name)} saved.`));
}, "Save a home point", "/sethome [name]");
ps.commands.register("home", (ctx) => {
const p = self(ctx);
if (!p) return;
const name = ctx.arg(0) ?? "home";
const loc = loadHomes()[name];
if (!loc) { tell(ctx.sender, fail(`Home ${aqua(name)} not found.`)); return; }
p.teleport(loc);
tell(ctx.sender, ok(`Welcome home (${aqua(name)}).`));
}, "Teleport home", "/home [name]");Bundling for legacy (1.12.2 / 1.16.5)
The same source runs on legacy hosts — the only change is the bundle target in plugin.json, so the SDK emits ES2015 for the Nashorn engine:
plugin.json
{
"name": "essentials",
"version": "1.0.0",
"main": "dist/index.js",
"apiVersion": "1",
"target": "es2015"
}Replace MiniMessage strings with &-color codes (&a, &l…) and deploy to plugins/PaperScriptLegacy/scripts/ instead.