Skip to content

The Minecraft layer and the lobby helper ​

Two optional libraries over the host API. Everything in them is a few host calls, so you can drop to the raw API at any point. Status: Built.

@inventery/minecraft ​

The layer is thin because the base game already runs inside your plot (manifest baseRules, every category on by default): breaking and placing with tools and drops, beds and doors as two halves, PvP with weapons, knockback and armour, items lying on the ground picked up by walking over them. The layer gives you registries, world events that filter and hear those rules, and a few generic minigame pieces. Nothing here re-does the base rules.

ts
import { blocks, items, recipes, entities, structures, worldgen, events } from "@inventery/minecraft";
// re-export the hooks whose events you use:
export { onBlockBreak, onBlockPlace, onBlockBroken, onBlockPlaced, onUse, onStructureComplete, onEntityDeath, onModEvent } from "@inventery/minecraft";

Add content at the top level of logic.ts (or in init). Registries are plot-level and run again after every restart; the same name always gets the same id.

Registries ​

CallReturnsHost call
blocks.add(name, { model?, texture?, color?, hardness?, solid?, drops? })BlockId ("<namespace>:<name>")content.addBlock
blocks.id(name)BlockId of a base ("stone") or namespaced ("bedwars:bed") blockworld.blockId
items.add(name, { model?, texture?, stack?, places? })the item id "<namespace>:<name>"content.addItem
recipes.add({ pattern?, keys?, ingredients?, result })content.addRecipe
entities.add(name, { model, health?, collider?, drops? })the type idcontent.addEntityType
entities.spawn(type, pos, opts?)EntityId (lobby context)content.spawnEntity
worldgen.feature(name, kind, data)content.addWorldgenFeature
structures.pattern(name, layout, onMatch)content.addStructurePattern
ts
recipes.add({ pattern: ["WWW", "PPP"], keys: { W: "inventery:red_wool", P: "inventery:oak_planks" },
              result: { item: "furniture:red_bench", count: 1 } });   // shaped
recipes.add({ ingredients: ["inventery:obsidian", "inventery:gold_ingot"],
              result: { item: "portals:key", count: 1 } });             // shapeless

const BOSS = entities.add("golem", { model: "models/golem.glb", health: 200 });  // a glTF boss is just a type

Entity types get the base game's mob behaviour from ai: "hostile" (a zombie's), "ranged" (a skeleton's), "neutral" (a wolf's), "passive" (a pig's), or "none" (stays where you put it); damage and speed tune it. Players' swings are the base melee on every entity with health (onEntityDamage can refuse one), and a mob's hit on a player reaches onPlayerDamage with the entity as its 4th argument. At 0 health an entity dies: onEntityDeath, and its drops lie on the ground. A boss with its own moves has no ai: move it yourself (the base melee still hurts it), and hurt anything yourself with content.damageEntity. content.spawnEntity(type, pos, { health }) scales one. The runtime's baseRules: { "mobs": false } turns all of it off.

World events ​

ts
events.blockBreak.on(BED, (ev) => (ev.by && isOwnBed(ev) ? false : undefined));  // before: false refuses
events.blockBroken.on(BED, (ev) => { /* after: ev.changes is every cell (both halves) */ });
events.blockPlace.on((ev) => blocks.oneOf(ev, ["red_wool", "end_stone"]));       // only these can be placed
events.use.on(LEVER, (ev) => { /* tapped */ });
events.entityDeath.on((ev) => { /* a spawned entity died */ });
events.modEvent.on(({ event, data }) => { /* another package's mods.sendEvent */ });
events.send("minigame:status", { team: "red", text: "X" });
  • blockBreak, blockPlace and use are filters: a handler returning false refuses; anything else allows.
  • blockBroken and blockPlaced are listeners: what the base game did, with every cell it changed.
  • Block events can filter by one block id: on(BED, handler).
  • With baseRules.blocks off, nothing takes a placed block from the player's items: your placement filter does it (players.take).

Structure patterns ​

Build a shape in the world and something happens: the classic "obsidian frame becomes a portal", or "four iron blocks and a pumpkin become a golem".

ts
structures.pattern("frame", {
  layers: [["OOO", "O O", "OOO"]],          // bottom-up; each layer is rows (z) of characters (x)
  keys: { O: "obsidian" },                  // " " matches anything
}, (m) => {
  scene.create({ Transform: { pos: m.origin }, Emitter: { particles: "inventery:spell", rate: 6, color: "#b060ff" },
                 Light: { color: "purple", intensity: 2, range: 6 } });
});
export { onStructureComplete } from "@inventery/minecraft";
  • It matches in any of the four horizontal rotations.
  • It's checked when a block is placed (by a player or plot code), only against patterns containing that block.
  • The callback runs in the lobby where it was built, with { name, origin, blocks, by }.
  • anchor (a key character) sets which cell origin is.

Minigame pieces ​

Generic pieces the Team Minigame runtime and the Bedwars and Spleef mods are built from (plot/samples/):

CallDoes
structures.build(blueprint, origin, { once?, perTick?, version?, clear? })stamps a map as data (pieces, keys, mixes, marks; rotated with its directional blocks) and returns its marks. At plot level it writes the shared base. once makes it idempotent across restarts; perTick builds a big map over many ticks (call structures.tick() from tickPlot; structures.built(once) says when it's done); clear leaves boxes alone (the monument)
structures.find(marks, name)every position of one mark ("iron" on every island)
generators.add({ at, item, every, cap? })base items dropped on a timer at a spot (world.dropItem), picked up the base way by walking over them. Plot level; then generators.start() / stop() per lobby and generators.tick() from tickLobby
shop(offers)buy items with items through on-screen buttons: show(to), hide(to), buy(p, button) from a button press; buttons() hands the definitions to a runtime that places buttons for a mod
combat.voidBelow(y, who?)players below y die (call from tickLobby). PvP itself is the base game's; onPlayerDamage filters it
edits.reset(), edits.original(pos), edits.forget()undo what a lobby's players broke and placed (remembered from onBlockBroken / onBlockPlaced, so re-export those), for the next round; edits.set, setMany and fill for blocks your code writes in a lobby

@inventery/lobby ​

Write a game for one lobby; the helper keeps one state object per lobby.

ts
import { defineLobbyGame } from "@inventery/lobby";

interface State { round: number; alive: string[] }

const game = defineLobbyGame<State>({
  setup: (lobby) => ({ round: 0, alive: [] }),          // a fresh lobby
  onLobbyLoad(s) { /* after the state is built or loaded */ },
  onPlayerEnter(s, p) { s.alive.push(p); },
  tickLobby(s, dt) { /* ... */ },
  onBlockBreak(s, ev) { return true; },                 // every lobby hook gets the state first
  saveOnTick: false,                                    // save the tick's changes with the next other hook
});
export const { onLobbyLoad, onPlayerEnter, tickAllLobbies, onBlockBreak } = game;   // what you defined, plus onLobbyLoad
  • The state is JSON in the lobby's storage (key "lobby", or key), saved after every hook. It survives restarts, suspends and resumes.
  • If a hook throws, the host rolls back its writes and the helper reloads the last saved state.
  • Keep it small: it's written every hook (and every tick, unless saveOnTick: false).
  • Your tickLobby is exported two ways: tickAllLobbies (one call a tick for all your lobbies: the cheap path when there are many; saveOnTick defaults to false there) and tickLobby (one call per lobby). Export one.
  • In @inventery/minecraft event handlers, read the state with game.state(), and export const onBlockBreak = game.wrap(mcOnBlockBreak) so it's saved after the handler.
  • Don't also storage.set("lobby", ...) yourself; let the helper own that key.