# 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`.

- `playAreaRect` is the destination where the current lobby or map is placed.
- `lobby.source.rect` and each `map.source.rect` are 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:

```js
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:

```ts
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:

```ts
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:

```ts
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.
- `SessionBasedGame` exposes the current `gamePhase`, selected `map`, `getPlayer`, `getPlayers`, `getStartingInText`, `giveColouredArmourToEveryone`, `checkWin`, and `makeTeamWin`.
- `SessionBasedPlayer` exposes `playerId`, `game`, `team`, `requestedTeam`, `isSpectating`, `startPlaying`, `startSpectating`, `onMessage`, `onRespawnRequest`, and `getPositionInPlayArea`.
- `BaseTeamInfo`, `BaseMapInfo`, and `SessionBasedGameSetupOptions` are 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.

```js
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 `tick` and 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. `onStart` resets 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 personal `kills` count. The plugin refreshes this HUD once a second.
- `makeTeamWin` changes the phase before calling `onFinish`, 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, then `onStart` clears 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.
