The avatar
p2.avatar() says who is playing, to dress your character with: their name and level, their critter, and what they wear. It never says their email or wallets, as nothing in the SDK does.
const me = await p2.avatar();
// {
// id: "Xq3...", handle: "amber", name: "Amber", level: 7, picture: null,
// critter: { face: "amber", size: 18, tile: "#1c1a26",
// pixels: [[6, 3, "#f2c14e"], [7, 3, "hsl(40, 85%, 62%)"], ...],
// model: "https://playertwo.lol/api/v1/critters/amber.glb" },
// worn: [],
// }- The critter is drawn from its
face(their handle, unless they picked another): the same name always gives the same critter, on every page of Player Two and in every game. Itspixelssit in an 18 by 18 box, from its top left; colours are CSS. wornis what they wear, earned or bought on Player Two: a hat, a trail, a badge, each a model and a picture. It is empty for now.- Ask once a session: the critter only changes when the player picks another face.
Drawing the pixels
// the critter on a canvas, crisp at any size: one rectangle a pixel
function drawCritter(ctx, critter, x, y, size) {
const u = size / critter.size;
for (const [px, py, colour] of critter.pixels) {
ctx.fillStyle = colour;
ctx.fillRect(Math.round(x + px * u), Math.round(y + py * u), Math.ceil(u), Math.ceil(u));
}
}
drawCritter(ctx, me.critter, 24, 24, 72);The 3D model
critter.model is the same critter as the Plaza draws it, in 3D: a glTF binary (.glb), each pixel a cube 7 cm a side, standing on the origin and facing +Z, coloured by its vertices, a few thousand triangles. Anyone may fetch it, from anywhere, and it never changes, so cache it as long as you like.
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const { scene: critter } = await new GLTFLoader().loadAsync(me.critter.model);
critter.traverse((o) => { if (o.isMesh) o.material.vertexColors = true; });
critter.position.set(0, 0, 0); // it stands on the origin, facing +Z, about 0.9 m tall
world.add(critter);Travel
Through a door
p2.travel.to(slug, { params, place }) sends the player to another game by its address on Player Two (playertwo.lol/game/slug). p2.travel.home() sends them back to the Plaza. Each resolves as the player goes: Player Two’s player sends your game’s last saves up, closes it, and opens the next game in its place.
// a door in your lobby: the player goes on to Hoop It, and their party with them
doorButton.onclick = () => p2.travel.to("hoop-it", { params: { mode: "pickup" }, place: "court" });
// back to the Plaza, where every game on Player Two has its screen
homeButton.onclick = () => p2.travel.home();- The game gone to plays as it would from its page: a free game they don’t have yet asks to be added, and a paid one they don’t own offers its demo, or Buy.
paramsare invite params: up to 16 strings, 512 bytes in all, withfromandplace.- Refused:
no-game(there is none at that address),not-out,not-playable(it isn’t played in Player Two’s player),same-game,bad-params, andno-travelwhere there is nowhere to send the player (below).
Arriving
The game reached gets your params as p2.params, with from, your game’s address, and place, if you named one. A game without the SDK finds them in its address’s query, as it finds a join link’s.
const p2 = await PlayerTwo.init();
if (p2.params?.from) {
// came through another game's door: p2.params.from is its address on Player Two
toast(`Welcome from ${p2.params.from}`);
if (p2.params.place === "court") goToCourt();
}A door menu
p2.travel.links() fills a menu of doors: the Plaza, then up to 12 games, in this order: where the party’s leader is, the games the player’s friends are in now, the ones they played lately, then the busiest. Only games played in Player Two’s player, out and listed, never yours, never one for adults unless the player said they are 18 or older, nor one with a label they hide.
const { home, links } = await p2.travel.links();
// home: { title: "The Plaza", url }
// links: [{ slug, title, tagline, art, players, friends: [{ handle, name }], why, free, owned }, ...]
menu.add(home.title, () => p2.travel.home());
for (const d of links) menu.add(d.title, () => p2.travel.to(d.slug), { art: d.art, note: d.friends.length ? `${d.friends[0].name} is here` : `${d.players} playing` });The party
A party moves as one, as it does in the Plaza. When its leader goes through a door, every member playing with them is asked, at once, in Player Two’s own bar over the game: “Amber went into Hoop It · Follow · Not now”. Following, they land where the leader is, with what the leader set friends to join them with there (p2.presence.set(text, { join })), and p2.params.from their own last game. Members elsewhere on Player Two are called to the game, as the leader’s “Join my game” calls them. Nothing in your game is needed for this.
Where it works
- In Player Two’s player on the web, for a game Player Two hosts or serves, or one on its own site with Connect on.
- In the Player Two app on a computer, and anywhere else:
travel.toandtravel.homereject withno-travel. Show your doors only whenPlayerTwo.envis"p2", and hide them if one is refused. - On localhost, the stand-in says where it would have gone and stays.
Played with recently
A player’s profile lists who they played with in the last 14 days, to find them again. Your game adds to it by being played:
- Rooms: players in the same room for a minute or more (p2.rooms). The SDK says who, once a minute; only players in the same game now count.
- Parties: members of the same party in your game at the same time.
- Your own server’s matches: report them (below).
A player who keeps their activity private is never shown on anyone’s list; blocked players are never shown to each other; guests are never counted.
Match reports
A game with its own servers reports a match when it ends, with its game key (your studio: Game keys). Name each player by their id for your game (the identity’s sub, as you name them for trophies: a player of your game in the last six hours), or by the identity their client sent you (p2.identity(), taken up to six hours past its time), or by their play session’s token. Each one’s list gets the others.
// your game's server, when a match ends: who was in it, by their ids for your game (the identity's sub)
await fetch("https://playertwo.lol/api/v1/sdk/server/match", {
method: "POST",
headers: { authorization: `Bearer ${process.env.P2_GAME_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
id: match.id, // counted once
minutes: Math.ceil(match.ms / 60000),
players: match.players.map((p) => ({ id: p.p2 })), // or { token }: an identity a client sent you
}),
});
// { counted: 6, players: [{ id, counted: true }, { id, counted: false, why: "guest" }, ...] }Trophies across games
Two of Player Two’s own trophies are earned across games, and count toward the player’s level like every other. They show on a profile once earned, never before.
| Trophy | How |
|---|---|
| Through five portals | Go through a door (p2.travel.to), one of the Plaza's screens, or after the party's leader, into five different games. |
| Three games with one party | Play three different games with the same party, at the same time as another of its members. |
Reference
| Call | Request | Answer |
|---|---|---|
| p2.avatar() | GET /api/v1/sdk/avatar | { id, handle, name, level, picture, critter, worn } |
| p2.travel.to(slug, { params, place }) | POST /api/v1/sdk/travel { to, params, place } | { to, params, unlocked } |
| p2.travel.home() | POST /api/v1/sdk/travel { home: true } | { to } |
| p2.travel.links() | GET /api/v1/sdk/travel | { home, links } |
| p2.params.from, .place | In the player's answer to the game's hello | Strings, or none |
| A critter's model | GET /api/v1/critters/{name}.glb | model/gltf-binary |
| Your server: report a match | POST /api/v1/sdk/server/match { players, minutes, id } | { counted, players } |
Every route is in the API spec.