Skip to content

Example: dungeon ​

From the runnable examples. Lint, build and run it from servers/inventery/:

sh
node plot/sdk/lint.mjs docs/developers/examples/dungeon
node plot/sdk/build.mjs docs/developers/examples/dungeon
node docs/developers/examples/run.mjs docs/developers/examples/dungeon

plot.json ​

json
{
  "kind": "runtime",
  "namespace": "crypt",
  "id": "ana.crypt",
  "version": "1.0.0",
  "name": "The Crypt",
  "apiVersion": 1,
  "code": "dist/logic.js",
  "stashItems": true,
  "lobbies": { "size": 4, "area": { "min": [0, 0, 0], "max": [8, 6, 8] } },
  "rating": "everyone",
  "tags": ["story", "adventure", "co-op"]
}

logic.ts ​

ts
import { content, lobbies, players, scene, storage, time, ui, world } from "@inventery/host";
import type { EntityId, PlayerId, UseEvent } from "@inventery/host";
import { entities } from "@inventery/minecraft";
import { defineLobbyGame } from "@inventery/lobby";

const DOOR: [number, number, number] = [8, 1, 4];
// No base mob AI runs in plots: a plot moves and fights with its entities itself. This boss just stands guard.
const SKELETON = entities.add("skeleton", { model: "inventery:skeleton", health: 10 });

interface State { keeper: EntityId; boss: EntityId; doorOpen: boolean; bossDown: boolean }

const game = defineLobbyGame<State>({
  // A new party's copy of the crypt: its own NPC, door and boss.
  setup: () => ({ keeper: 0, boss: 0, doorOpen: false, bossDown: false }),

  onLobbyLoad(s) {
    if (!s.keeper) {
      s.keeper = scene.create({
        Transform: { pos: [4, 1, 6] },
        Model: { asset: "inventery:villager" },
        Nametag: { text: "The Keeper", color: "gold" },
      });
    }
  },

  onPlayerEnter(s, p: PlayerId) {
    // Their progress is this plot's own data, kept across visits and parties.
    const chapter = storage.getPlayerProfile<number | null>(p, "chapter") ?? 1;
    ui.showTitle(p, "The Crypt", { subtitle: `Chapter ${chapter}`, color: "gold" });
  },

  onUse(s, ev: UseEvent) {
    if (ev.entity === s.boss && !s.bossDown) {
      content.damageEntity(ev.entity, 2, { by: ev.by });  // a tap is a hit: hurting plot entities is the plot's
      return true;
    }
    if (ev.entity === s.keeper) {
      ui.showDialogue(ev.by, s.keeper, "The door opens for those who listen.", { name: "The Keeper", color: "gold" });
      if (!s.doorOpen) {
        s.doorOpen = true;
        lobbies.lock();                                   // the party is set: nobody new joins mid-story
        world.setBlock(DOOR, world.blockId("air"));       // this party's copy only
        time.startTimer(2, "boss");
      }
    }
    return true;
  },

  onTimer(s, name) {
    if (name === "boss") s.boss = entities.spawn(SKELETON, [12, 1, 4]);
  },

  onEntityDeath(s, ev) {
    if (ev.type !== SKELETON || s.bossDown) return;
    s.bossDown = true;
    for (const p of players.list()) {
      storage.setPlayerProfile(p, "chapter", 2);
      ui.showToast(p, { text: "Chapter 1 complete", icon: "star" });
    }
  },
});

export const { onLobbyLoad, onPlayerEnter, onUse, onTimer, onEntityDeath } = game;