Appearance
Guide: Bedwars
You'll build: Bedwars as a game mod on the Team Minigame runtime: two sky islands, beds, generators and a shop, in about 60 lines of code (plus comments) and a map as data. You'll use: a mod package, the runtime's api.ts, @inventery/minecraft (a map as data, generators, a shop, block events) and the base game's rules. Status: Built (headless; the full sample also in the plots dev stack on an ASDFGHJKL monument).
The finished code: examples/bedwars (plot.json, map.json, logic.ts). The same model at full size is the sample plot/samples/bedwars (four teams, diamonds and emeralds), next to the runtime (its README lists the extension points) and a second game on it, Spleef.
Runtime and mod
A plot runs one runtime (how the plot is run) and any number of mods (register #94; see Runtimes and mods). Team Minigame is a reusable runtime: it runs the round of any team game. A game is a mod that tells it the rest.
| Team Minigame (the runtime) does | The Bedwars mod adds |
|---|---|
lobbies of N (its manifest's lobbies.size), stashed items | the map: islands, a middle, a perch for the dead, the lobby box |
| teams balanced as players arrive, a scoreboard | the team names and each team's status ("BED", then "X") |
| a full lobby locked, one start countdown (everyone protected) | the kit (a wooden sword) and the round's titles |
| every team on its spawn with the kit, the void under the map | generators: iron on each island, gold in the middle |
| a death as a respawn after 3 s, or as out (the game says which, per team) | beds: a team respawns while its bed stands |
| the last team standing wins, then the round's block edits undone, drops cleared, everyone back in the lobby, the next countdown | a shop: its buttons, and what a press buys |
| refuses breaking, placing and hitting outside a round, by players who are out, and between teammates | which blocks may be broken (enemy beds, what was placed this round) and placed (team wool) |
Neither writes melee, drops, pickup or bed halves: the base game runs inside the plot (register #111). Beds are the base game's (two halves, broken whole), and so are swords, knockback, items dropped and picked up by walking over them. The runtime and the mod only filter the base rules (return false to refuse; any refusal wins) and hear what they did.
Install both: a monument's entry in the plots config (or a dev box's) lists the runtime and its mods.
json
{ "dir": "plot/samples/team-minigame", "mods": ["docs/developers/examples/bedwars"] }The mod's manifest is small. kind: "mod" means it can't use the runtime's calls (ui.setButton, lobbies.*); depends makes the plot load Team Minigame first.
json
{
"kind": "mod",
"id": "ana.bedwars",
"namespace": "bedwars",
"name": "Bedwars",
"version": "2.0.0",
"apiVersion": 1,
"code": "dist/logic.js",
"depends": ["inventery.team-minigame"],
"tags": ["pvp", "teams", "minigame"]
}The runtime and the mod are separate sandboxed components, so they talk through events, as data (mods.sendEvent → onModEvent). The runtime's api.ts wraps that: import it, and re-export its onModEvent so the events reach it.
ts
import { teamGame as game } from "../../../../plot/samples/team-minigame/api";
import MAP from "./map.json";
// The runtime and this mod are separate sandboxed components: they talk through events, as data (mods.sendEvent ->
// onModEvent). api.ts's onModEvent turns the runtime's events into game.onMap, game.onRoundStart and the others.
export { onModEvent } from "../../../../plot/samples/team-minigame/api";
// The base game's block events reach a package through these hooks; events.blockBreak and the others listen to them.
export { onBlockBreak, onBlockPlace, onBlockBroken, onBlockPlaced } from "@inventery/minecraft";1. The map
map.json is a blueprint: pieces (layers bottom-up, rows north to south), keys (a character to a block, or a mark), and where each piece goes. The runtime builds it into the plot's shared base, from the plot's corner on the ground.
json
"place": [
{ "piece": "island", "at": [2, 20, 13], "tag": "red", "keys": { "W": "red_wool" } },
{ "piece": "island", "at": [25, 20, 13], "rotate": 2, "tag": "blue", "keys": { "W": "blue_wool" } },
{ "piece": "middle", "at": [14, 20, 14], "tag": "middle" },
{ "piece": "watch", "at": [14, 32, 14], "tag": "watch" },
{ "piece": "box", "at": [12, -1, 2], "tag": "lobby" }
]The runtime reads three marks: spawn under each team's tag (red, blue), under lobby (where everyone waits between rounds) and under watch (where the dead wait). The other marks are the game's: bed on both halves of each team's bed, iron on each island, gold in the middle. The blue island is turned twice, and its bed turns with it.
2. Register the game
ts
game.register({
name: "Bedwars",
map: MAP, // built by the runtime into the plot's shared base
teams: TEAMS,
kit: [{ item: "inventery:wooden_sword", count: 1 }], // given at every spawn
status: "BED", // each team's scoreboard status when the round starts
buttons: SHOP.buttons(),
goal: "Protect your bed", // the round's opening subtitle
outText: "Your bed is gone", // the subtitle when you're out
});register runs at the top level, so at every load. Everything in it is data (a GameDef, typed in api.ts).
3. Generators and the shop
ts
const SHOP = shop([
{ button: "wool", item: (p) => `inventery:${game.teamOf(p)}_wool`, count: 16, price: [IRON, 4], label: "16 WOOL" },
{ button: "stone_sword", item: "inventery:stone_sword", price: [IRON, 10], label: "SWORD 2" },
{ button: "iron_sword", item: "inventery:iron_sword", price: [GOLD, 4], label: "SWORD 3" },
]);Only a runtime may put buttons on screen (ui.setButton is runtime-only), so the mod hands the runtime SHOP.buttons() in register and the runtime passes presses back.
ts
let marks: Marks = {};
game.onMap((m) => {
marks = m;
generators.add({ at: structures.find(m, "iron"), item: IRON, every: 1, cap: 32 });
generators.add({ at: structures.find(m, "gold"), item: GOLD, every: 6, cap: 8 });
});
// A round started in this lobby: everyone is on their team's spawn with the kit. The runtime undid the last round's
// block edits itself, so this mod forgets its own record of them (edits.original starts afresh) and starts dropping.
game.onRoundStart(() => {
edits.forget();
generators.start();
});
game.onRoundEnd(() => generators.stop()); // clears what's still lying there
game.onButton((p, button) => SHOP.buy(p, button));
export function tickLobby() {
generators.tick();
}A generator drops the base game's iron with world.dropItem: it lies on the ground, onlookers see it, and players pick it up by walking over it, the base way. SHOP.buy takes the price from the player's items and gives the item.
4. Beds: who respawns
ts
const same = (a: Vec3, b: Vec3) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
// Which team's bed a cell is (either half), if any: the map marks both halves "bed" on each team's island.
const bedOf = (pos: Vec3) => TEAMS.find((t) => marks[t]?.bed?.some((c) => same(c, pos)));
// Before the base game breaks a block. The runtime already refuses breaking outside a round and by players who are
// out; this refusal comes on top. In the round you may break the other team's bed and blocks placed this round.
events.blockBreak.on((ev) => {
if (!game.playing(ev.by)) return false;
const bed = bedOf(ev.pos);
if (bed) return bed !== game.teamOf(ev.by); // not your own bed
return edits.original(ev.pos) === AIR; // a bridge, not the islands
});
// After the base game broke a bed (both halves are gone): that team respawns no more.
events.blockBroken.on((ev) => {
const bed = bedOf(ev.pos);
if (!bed) return;
game.setRespawn(bed, false); // canRespawn(team): from now that team is out when it dies
game.setStatus(bed, "X"); // the scoreboard row: "BED 1" becomes "X 1"
ui.showTitle("lobby", `${bed.toUpperCase()} bed destroyed!`, { color: bed });
});
// Before the base game places a block: only team wool, by players in the round.
events.blockPlace.on((ev) => game.playing(ev.by) && blocks.oneOf(ev, TEAMS.map((t) => `${t}_wool`)));events.blockBreak is a filter round the base game's breaking, events.blockBroken a listener after it (with every cell it changed: both halves of the bed). "Can this team respawn?" never crosses as a function: the mod keeps the runtime's answer current with setRespawn, and the runtime decides at each death whether it's a respawn or out.
Beds drop nothing in this plot (the base game would drop a bed item):
ts
for (const facing of ["", "_south", "_west", "_east"]) {
for (const half of ["bed", "bed_head"]) world.setDrops(world.blockId(half + facing), []);
}Run it
Build the runtime and the mod, then play a round headless with two players in a lobby of two:
sh
node plot/sdk/build.mjs plot/samples/team-minigame
node plot/sdk/build.mjs docs/developers/examples/bedwars
node docs/developers/examples/run.mjs plot/samples/team-minigame --mods docs/developers/examples/bedwars \
--lobby 2 --players 2 --break 28,21,13 --hitrun.mjs loads the runtime with its mods (--mods, the plots config's mods list), walks ana and bob in, waits 12 s (the entry protection and the start countdown), then ana breaks blue's bed (its foot is at 28,21,13: the blue island's corner plus the turned bed's cell) and hits bob with the kit's sword until he dies. Bob has no bed, so he's out, and red is the last team standing. Abridged:
text
break 28,21,13: true
hit bob: 4 swings
lobby l1: 2 players of 2, locked
ana: at 26,21,16, inside in lobby l1, plot health 20
inventory: [{"item":"inventery:wooden_sword","count":1}]
UI {"op":"title","title":"Bedwars","opts":{"subtitle":"You're on RED","color":"red"}}
UI {"op":"button","def":{"id":"wool","side":"right","priority":3,"label":"16 WOOL","banner":"16 WOOL 4 IRON"}}
UI {"op":"scoreboard","title":"BEDWARS ROUND 1","rows":[{"label":"RED","color":"red","value":"BED 1"},{"label":"BLUE","color":"blue","value":"BED 1"}]}
UI {"op":"title","title":"Round starts","opts":{"subtitle":"Protect your bed","color":"gold"}}
UI {"op":"title","title":"BLUE bed destroyed!","opts":{"color":"blue"}}
UI {"op":"scoreboard","title":"BEDWARS ROUND 1","rows":[{"label":"RED","color":"red","value":"BED 1"},{"label":"BLUE","color":"blue","value":"X 1"}]}
CHAT {"scope":"lobby","from":"Team Minigame","text":"bob was killed by ana"}
UI {"op":"title","title":"RED wins!","opts":{"color":"red","subtitle":"Again in 5 s"}}
bob: at 16,33,16, inside in lobby l1, plot health 20
inventory: []
UI {"op":"title","title":"Eliminated","opts":{"subtitle":"Your bed is gone","color":"gray"}}Breaking your own bed is refused: --break 3,21,17 (red's bed) prints break 3,21,17: false.
The samples' tests play whole rounds through the harness (four players, bridges, the shop, melee, the reset and the next countdown): node --test plot/test/samples-bedwars.test.mjs plot/test/samples-team-minigame.test.mjs.


Switch the plot to Spleef
The game is the mods list. Change it and restart the plot:
json
{ "dir": "plot/samples/team-minigame", "mods": ["plot/samples/spleef"] }Spleef registers its own map with the same runtime; the runtime clears the islands it built from the shared base and builds Spleef's snow floor. Nothing in Team Minigame changes. Headless (Spleef's map needs a 48-block plot):
sh
node plot/sdk/build.mjs plot/samples/spleef
node docs/developers/examples/run.mjs plot/samples/team-minigame --mods plot/samples/spleef \
--lobby 2 --size 48,96,48 --players 2 --break 31,30,16Ana digs out the snow under bob, he falls through (bob fell into the void, "Eliminated", "You fell through") and red wins. The switch itself, across a restart with the islands cleared, is in plot/test/samples-team-minigame.test.mjs.
The other way: your own runtime
Want the round flow itself to be yours (no teams, a puzzle room for four, a custom camera)? Write a runtime: declare lobbies and stashItems in a kind: "runtime" manifest, and handle entry, the start countdown (lobbies.startCountdown), deaths and the win with @inventery/lobby. The dungeon and arena examples are runtimes. Prefer a game mod when a reusable runtime fits: the round, the reset and the next game come for free.