Skip to content

Example: arena ​

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

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

plot.json ​

json
{
  "kind": "runtime",
  "namespace": "arena",
  "id": "ana.arena",
  "version": "1.0.0",
  "name": "Arena",
  "apiVersion": 1,
  "code": "dist/logic.js",
  "stashItems": true,
  "lobbies": { "size": 2 },
  "tickRate": 30,
  "rating": "teen",
  "tags": ["shooter", "pvp"]
}

logic.ts ​

ts
import { input, lobbies, network, players, scene, time, ui } from "@inventery/host";
import type { PlayerId, Vec3 } from "@inventery/host";

const RANGE = 100, DAMAGE = 5, COOLDOWN = 0.25;
const lastShot = new Map<PlayerId, number>(); // memory is fine for this: losing it on a restart is harmless

export function onPlayerEnter(p: PlayerId) {
  ui.chat(p, "Tap to fire.");
  const lobby = lobbies.current();
  if (lobby.players.length >= lobby.size) {
    lobbies.lock();                                    // a duel: nobody else walks in mid-fight
    lobbies.startCountdown({ protectEveryone: true }); // an equal start: nobody can be hit before it ends
  }
}

export function onCountdownEnd() {
  for (const p of players.list()) players.setCamera(p, { mode: "first" });
}

export function onPlayerLeave() {
  if (players.list().length === 0) lobbies.unlock();  // empty again: open for the next pair
}

// Every tick, read this lobby's inputs. The room is the referee.
export function tickLobby() {
  for (const ev of input.read()) {
    if (ev.action !== "fire" || !ev.aimRay) continue;
    const now = time.now();
    if (now - (lastShot.get(ev.player) ?? -1) < COOLDOWN) continue;   // the server decides the fire rate
    lastShot.set(ev.player, now);

    // Lag compensation: check the shot against where everyone was when the shooter saw them.
    // The ray starts inside the shooter's own body, so it ignores the shooter.
    const seenAt = ev.renderTime ?? now;
    const hit = network.raycastAtTime(seenAt, ev.aimRay, { maxDistance: RANGE, players: true, blocks: true, ignore: [ev.player] });
    if (hit?.player) players.damage(hit.player, DAMAGE, { by: ev.player, cause: "shot" });

    // The tracer is in the scene, so onlookers outside see the fight too.
    const end: Vec3 = hit?.point ?? along(ev.aimRay.origin, ev.aimRay.dir, RANGE);
    scene.create({
      Transform: { pos: ev.aimRay.origin },
      Emitter: { particles: "inventery:spark", burst: 6, to: end, color: "#ffe08a" },
      Lifetime: { seconds: 0.2 },
    });
  }
}

function along(o: Vec3, d: Vec3, t: number): Vec3 {
  return [o[0] + d[0] * t, o[1] + d[1] * t, o[2] + d[2] * t];
}