This page as Markdown: https://bloxd.io/docs/how-to-building-a-session-based-game.md. Every page of these docs, for AI agents: https://bloxd.io/docs/llms.txt
Building a SessionBasedGame
Use @plugins/sessionBasedGame for a team game that follows this loop:
- Load a lobby and wait for players.
- Optionally let players choose teams and vote for a map.
- Copy the selected map into the play area and assign teams.
- Run the game until one team remains or your code chooses a winner.
- Show the result, reset the lobby, and start the next countdown.
World layout
Build the lobby and each map in unused parts of the world. The plugin treats these areas as templates and copies one of them into playAreaRect.
playAreaRectis the destination where the current lobby or map is placed.lobby.source.rectand eachmap.source.rectare absolute world coordinates for template builds.- Lobby spawns, map team spawns, and team-pad rectangles are offsets from the lowest corner of
playAreaRect, not absolute world coordinates. - Source rectangles are rounded outwards to chunk boundaries. Keep template builds aligned to 32-block chunk boundaries so the copied area is predictable.
- Every lobby and map source must fit inside
playAreaRect. - At least two teams and one map are required.
Minimal setup
Create the game once, at the top level of your entry file:
import { createGame } from "@plugins/sessionBasedGame"
const teams = [
{ name: "Red", maxPlayers: 4, colour: "#ff5555" },
{ name: "Blue", maxPlayers: 4, colour: "#5555ff" },
]
const game = createGame({
teams,
maps: [
{
name: "Castle",
icon: "Stone Bricks",
source: {
rect: [
[320, 0, 0],
[351, 31, 31],
],
},
teams: {
Red: {
spawn: [6, 2, 16],
spawnFacing: [1, 0, 0],
},
Blue: {
spawn: [26, 2, 16],
spawnFacing: [-1, 0, 0],
},
},
},
],
lobby: {
source: {
rect: [
[256, 0, 0],
[287, 31, 31],
],
},
spawn: [16, 2, 16],
spawnFacing: [0, 0, 1],
},
playAreaRect: [
[0, 0, 0],
[31, 31, 31],
],
emptyLobbyStartInSecs: 60,
fullLobbyStartInSecs: 10,
gameResetInSecs: 10,
onStart: () => {
game.giveColouredArmourToEveryone()
},
})
The plugin supplies default values for the optional settings. With the setup above, players are assigned to the smallest available team when the game starts.
Eliminating players and ending the game
By default, players remain on their team after dying. For an elimination game, turn the victim into a spectator:
onPlayerKilledOtherPlayer = (_killerId, victimId) => {
game.getPlayer(victimId).startSpectating()
}
onMobKilledPlayer = (_mobId, playerId) => {
game.getPlayer(playerId).startSpectating()
}
startSpectating() checks whether only one team remains and finishes the game automatically. For games with another win condition, call game.makeTeamWin(team) when that condition is met.
Team choosing
Add teamPads to the lobby to let players request a team before the game starts:
lobby: {
source: {
rect: [
[256, 0, 0],
[287, 31, 31],
],
},
spawn: [16, 2, 16],
spawnFacing: [0, 0, 1],
teamPads: {
Red: {
rect: [
[4, 1, 14],
[6, 1, 16],
],
emptyBlock: "Red Concrete",
fullBlock: "Red Ceramic",
},
Blue: {
rect: [
[25, 1, 14],
[27, 1, 16],
],
emptyBlock: "Blue Concrete",
fullBlock: "Blue Ceramic",
},
},
}
Each key must exactly match a team name. The plugin changes a pad from emptyBlock to fullBlock when that team has enough requests.
Map voting
Set mapVotingEnabled: true and provide multiple maps. Before each game, the plugin opens a shop containing up to four randomly selected maps. The icon on each map is used as its shop image.
Custom player state and behaviour
Extend SessionBasedPlayer when each player needs additional state or customised lifecycle behaviour, then pass the class as playerConstructor. This example assumes teams, maps, lobby, and playAreaRect are top-level constants:
import { createGame, SessionBasedPlayer } from "@plugins/sessionBasedGame"
class GamePlayer extends SessionBasedPlayer<(typeof teams)[number], (typeof maps)[number]> {
kills = 0
startPlaying() {
super.startPlaying()
this.kills = 0
}
}
const game = createGame({
teams,
maps,
lobby,
playAreaRect,
playerConstructor: GamePlayer,
})
Call super.startPlaying() or super.startSpectating() when overriding those methods so the plugin still updates inventory, health, visibility, building permissions, and win detection.
Main session-based exports
createGame(options)creates and returns the single game instance.SessionBasedGameexposes the currentgamePhase, selectedmap,getPlayer,getPlayers,getStartingInText,giveColouredArmourToEveryone,checkWin, andmakeTeamWin.SessionBasedPlayerexposesplayerId,game,team,requestedTeam,isSpectating,startPlaying,startSpectating,onMessage,onRespawnRequest, andgetPositionInPlayArea.BaseTeamInfo,BaseMapInfo, andSessionBasedGameSetupOptionsare exported TypeScript types.
Complete example: team deathmatch
This combines team choosing, two-map voting, a custom player class, respawning equipment, personal kill counts, team scores, a round timer, sudden death, and automatic resets. Each enemy kill earns one team point. The first team to 10 points wins; after three minutes, the leading team wins. A tied round continues until a team takes the lead. Players who join mid-round spectate until the next round.
Build the templates first
Use these absolute world coordinates, or change the corresponding rectangles in the code:
| Area | Lowest block | Highest block |
|---|---|---|
| Play area (overwritten each round) | [0, 0, 0] | [31, 31, 31] |
| Lobby template | [256, 0, 0] | [287, 31, 31] |
| Castle template | [320, 0, 0] | [351, 31, 31] |
| Courtyard template | [384, 0, 0] | [415, 31, 31] |
Build a floor at y = 1 in each template, with walls to keep players inside and clear space above each spawn. In the lobby, leave room for the spawn at [272, 2, 16] and the team pads at [260, 1, 14]–[262, 1, 16] and [281, 1, 14]–[283, 1, 16]. The plugin colours the pads in the copied lobby. Castle's spawns are [326, 2, 16] and [346, 2, 16]; Courtyard's are [400, 2, 6] and [400, 2, 26]. Add cover between the opposing spawns.
Enable PvP and health, disable spawn protection, and save the builds in your world. The code copies your templates; it does not build them. All spawn and pad coordinates inside the code are offsets within the copied area, which is why Castle's Red spawn is [6, 2, 16], not [326, 2, 16].
Paste the whole entry file
Use index.js in the Code Editor. Replace the earlier examples with this entire block: it includes every constant and creates the game exactly once. Test with at least two players on opposing teams; team-pad requests are honoured, so choose different pads when testing.
import { createGame, SessionBasedPlayer } from "@plugins/sessionBasedGame"
/* Settings */
const SCORE_TO_WIN = 10
const ROUND_SECONDS = 180
const PLAYERS_PER_TEAM = 4
/* World layout */
/* This example's scoring and HUD are written for exactly two teams: Red and Blue. */
const teams = [
{ name: "Red", maxPlayers: PLAYERS_PER_TEAM, colour: "#ff5555" },
{ name: "Blue", maxPlayers: PLAYERS_PER_TEAM, colour: "#5555ff" },
]
const maps = [
{
name: "Castle",
icon: "Stone Bricks",
source: { rect: [[320, 0, 0], [351, 31, 31]] },
teams: {
Red: { spawn: [6, 2, 16], spawnFacing: [1, 0, 0] },
Blue: { spawn: [26, 2, 16], spawnFacing: [-1, 0, 0] },
},
},
{
name: "Courtyard",
icon: "Grass Block",
source: { rect: [[384, 0, 0], [415, 31, 31]] },
teams: {
Red: { spawn: [16, 2, 6], spawnFacing: [0, 0, 1] },
Blue: { spawn: [16, 2, 26], spawnFacing: [0, 0, -1] },
},
},
]
/* Round state */
const scores = { Red: 0, Blue: 0 }
let roundEndsAt = 0
let resultText = ""
class ArenaPlayer extends SessionBasedPlayer {
kills = 0
startPlaying() {
super.startPlaying()
this.kills = 0
api.setClientOptions(this.playerId, {
secsToRespawn: 3,
respawnButtonText: "Respawn",
usePlayAgainButton: false,
})
}
onRespawnRequest() {
const position = super.onRespawnRequest()
if (this.team !== null) {
/* Also runs at round start. Set slots so respawns cannot duplicate equipment. */
api.setItemSlot(this.playerId, 0, "Iron Sword")
api.setItemSlot(this.playerId, 46, `${this.team.name} Wood Helmet`)
api.setItemSlot(this.playerId, 47, `${this.team.name} Wood Chestplate`)
api.setItemSlot(this.playerId, 48, `${this.team.name} Wood Gauntlets`)
api.setItemSlot(this.playerId, 49, `${this.team.name} Wood Leggings`)
api.setItemSlot(this.playerId, 50, `${this.team.name} Wood Boots`)
}
return position
}
}
const game = createGame({
teams,
maps,
playerConstructor: ArenaPlayer,
playAreaRect: [[0, 0, 0], [31, 31, 31]],
lobby: {
source: { rect: [[256, 0, 0], [287, 31, 31]] },
spawn: [16, 2, 16],
spawnFacing: [0, 0, 1],
teamPads: {
Red: {
rect: [[4, 1, 14], [6, 1, 16]],
emptyBlock: "Red Concrete",
fullBlock: "Red Ceramic",
},
Blue: {
rect: [[25, 1, 14], [27, 1, 16]],
emptyBlock: "Blue Concrete",
fullBlock: "Blue Ceramic",
},
},
},
mapVotingEnabled: true,
maxSpectators: 4,
emptyLobbyStartInSecs: 30,
fullLobbyStartInSecs: 5,
gameResetInSecs: 10,
onStart: () => {
scores.Red = 0
scores.Blue = 0
resultText = ""
roundEndsAt = api.now() + ROUND_SECONDS * 1000
api.broadcastMessage(`First team to ${SCORE_TO_WIN} kills wins!`)
game.checkWin()
},
onFinish: (winningTeam) => {
resultText = `${winningTeam.name} wins! Red ${scores.Red} - Blue ${scores.Blue}`
api.broadcastMessage(resultText)
/* Disable combat and clear equipment during the result screen. */
for (const player of game.getPlayers()) {
player.startSpectating()
}
},
getRightInfoText: (player) => {
if (game.gamePhase === "beforeGame") {
return [
"TEAM DEATHMATCH\nChoose a team on the coloured pads.\nVote for a map in the shop.\n",
...game.getStartingInText(),
]
}
if (game.gamePhase === "gameFinished") {
return `${resultText}\nNext round soon!`
}
const secondsLeft = Math.max(0, Math.ceil((roundEndsAt - api.now()) / 1000))
const clock = secondsLeft > 0 ? `${secondsLeft}s left` : "Sudden death: next team lead wins!"
const role = player.team === null ? "Spectating until next round" : `${player.team.name} team`
return `TEAM DEATHMATCH\n${role}\nRed ${scores.Red} - Blue ${scores.Blue}\nTarget: ${SCORE_TO_WIN}\n${clock}\nYour kills: ${player.kills}`
},
})
function checkScoreWin() {
if (game.gamePhase !== "inGame") {
return
}
for (const team of teams) {
const otherName = team.name === "Red" ? "Blue" : "Red"
const reachedTarget = scores[team.name] >= SCORE_TO_WIN
const leadsAfterTime = api.now() >= roundEndsAt && scores[team.name] > scores[otherName]
if (reachedTarget || leadsAfterTime) {
game.makeTeamWin(team)
return
}
}
}
tick = () => {
checkScoreWin()
}
onPlayerKilledOtherPlayer = (killerId, victimId) => {
if (game.gamePhase === "inGame") {
const killer = game.getPlayer(killerId)
const victim = game.getPlayer(victimId)
if (killer.team !== null && victim.team !== null && killer.team !== victim.team) {
killer.kills++
scores[killer.team.name]++
checkScoreWin()
}
}
return "keepInventory"
}
onMobKilledPlayer = () => {
return "keepInventory"
}
onPlayerDropItem = () => {
return "preventDrop"
}
How the pieces connect
- Importing the plugin registers its join, leave, tick, team-pad, shop-voting, chat, and respawn callbacks. Do not call those plugin callbacks manually: your
tickand kill callbacks run after theirs automatically. startPlaying()runs once per round for each participant. It resets personal kills and calls the base implementation to restore health, team visibility, friendly-fire rules, and the spawn position.onStartresets shared scores and the deadline after players have started.- Deaths leave players on their teams, so the plugin respawns them at their map spawn. The player method
onRespawnRequest()restores equipment and returns the base spawn position. Returning that position is essential; a separate global respawn callback could accidentally override it. Mob and environmental deaths do not award points. getRightInfoText(player)builds the scoreboard for that player, including their personalkillscount. The plugin refreshes this HUD once a second.makeTeamWinchanges the phase before callingonFinish, so converting everyone to spectators there cannot trigger another win. Scores remain visible during the result screen; the plugin reloads the lobby after ten seconds, thenonStartclears them for the next match. The plugin also ends a round when only one team remains after disconnects.
To check the full loop, choose opposite pads, vote, score a kill, and confirm the victim respawns with a sword while the killer's team gains a point. Temporarily set SCORE_TO_WIN to 2 and ROUND_SECONDS to 15 to check a score victory, a timed victory, a tied round continuing into sudden death, and the next round starting with cleared scores. Join mid-round to check spectating.