Skip to content

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) doesThe Bedwars mod adds
lobbies of N (its manifest's lobbies.size), stashed itemsthe map: islands, a middle, a perch for the dead, the lobby box
teams balanced as players arrive, a scoreboardthe 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 mapgenerators: 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 countdowna shop: its buttons, and what a press buys
refuses breaking, placing and hitting outside a round, by players who are out, and between teammateswhich 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 --hit

run.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.

Bedwars mid-round from outside: a red wool bridge to the blue island

Bedwars in the plots dev page at phone size: the shop's buttons on the right, the scoreboard, a bed

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,16

Ana 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.

Spleef on the same runtime: a hole dug under another player

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.