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:

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.

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

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:

AreaLowest blockHighest 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

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.

Logo