# Writing scripts for multiplayer

**Multiplayer is coming; it isn't in this build.** You can't turn on a networked game yet. These are
the rules your scripts will run under once it arrives: write them this way now and they will be ready.
A script that follows them runs unchanged in single-player today.

In a networked game every player's machine runs the same simulation, tick by tick, and the machines
only stay in step if every behaviour does exactly the same thing on each of them. So a networked game
runs your scripts under a few extra rules. This page is the short version, with the fix for each one.
The full rules are in the script API reference (`docs/API.md`, the networked-session list under
"Conventions") and in [NETCODE.md](NETCODE.md).

## Do

**Wait with `ctx.time.after`, not `async` or promises.** A module that uses `async`, `await`,
`Promise` or `queueMicrotask` is refused when it loads, because a promise's code would run at a
different moment on each machine. Raise an event later on the tick clock instead:

```ts
export default {
  init(ctx) {
    // not: await sleep(2); explode()
    ctx.time.after(2, "fuse-done", { power: 3 })
  },
  onEvent(ctx, ev) {
    if (ev.name === "fuse-done") explode(ctx, (ev.data as { power: number }).power)
  },
} satisfies Behavior
```

**Write `Math.pow(x, y)`, not `x ** y`.** In a networked game `Math` gives the same bits on every
machine and operating system, but the `**` operator bypasses it, so a module that uses `**` is refused
when it loads. `Math.sqrt`, `Math.abs`, `Math.floor`, `Math.min` and the like are fine as they are.

```ts
const falloff = Math.pow(1 - d / radius, 2)   // not (1 - d / radius) ** 2
```

**Send an event to the one entity it is for with `ctx.events.emitTo`.** Only that entity's behaviours
hear it, and a timer can do the same with `{ to }`:

```ts
ctx.events.emitTo(target, "hit", { damage: 10 })
ctx.time.after(0.5, "stun-over", {}, { to: target })
```

**Let each entity change itself.** When a shot hits something, send the hit to the target and let
the target's own handler apply it, rather than writing into the other entity from the shooter. Add up
things that add (damage, knock-back) so the order of the hits doesn't matter, and settle anything only
one player can win (the kill, the pickup) by a fixed rule on entity ids, such as the lowest id, never by
which handler happened to run first. "Projectiles in a game that must agree across machines" in
`docs/API.md` walks through it for shots.

**Order by id yourself, not by which behaviour updates first.** Behaviours update in the order they were added
(the scene's order when it loads, then spawns as they happen), not sorted by id. That order is the
same on every machine, but a small change to the scene changes it, so a result that depends on it
breaks easily. When you need a fixed order, sort by `ctx.entity.id` yourself: ids compare as plain
strings, so `"crawler#10"` comes before `"crawler#2"`, and plain `<` gives that order:

```ts
const winner = claims.reduce((a, b) => (b.id < a.id ? b : a))   // the lowest id wins
```

**Keep game state in `ctx.state`, and finish a tick's work by the next tick.** A save or a rollback
holds `ctx.state`; anything kept elsewhere, or a hit you carry for several ticks, comes back wrong.

**Use the clocks a behaviour already has.** `ctx.time.now`, `performance.now()` and `Date.now()` all
read the game's own clock, so they agree on every machine. `Math.random()` is fine too: each
behaviour draws from its own stream, seeded from the game, so every machine draws the same numbers.

**Step many entities in one call: `updateAll`.** A networked game re-runs recent ticks whenever a
late input arrives, often eight or more times a frame, so a behaviour that every player or unit
carries costs its `update` × instances × re-runs. Give the module `updateAll` instead. The host calls
it once a tick with every instance, and the batch queries answer for all of them at once:

```ts
export default {
  updateAll(ctx, dt, batch) {
    const at = ctx.world.positions(batch.entities)            // Float32Array, 3 per entity
    const enemies = ctx.world.query({ tag: "enemy" })
    const near = ctx.world.nearestMany(at, ctx.world.positions(enemies), 10) // Int32Array; -1 = none
    for (let k = 0; k < batch.count; k++) {
      const st = batch.states[k]                                // that instance's ctx.state
      st.target = near[k] === -1 ? null : enemies[near[k]].id
    }
  },
}
```

Keep per-instance data in `batch.states[k]`, never in variables at the top of the module: a re-run
puts `ctx.state` back, but nothing puts a module variable back. `nearestMany` gives equal distances
to the lower index, so every machine picks the same target. (A point never skips
itself: when `to` holds `from`'s own points, each one's nearest is itself.) The exact rules are in
`docs/API.md` (Changes: "One call for every instance").

**Mark what must happen once: `confirmedOnly: true`.** A networked game guesses ahead: it runs the
next few ticks before every player's inputs have arrived, then runs them again when they do. For
most scripts that is invisible. But a goal scored on a guess can be un-scored a moment later, and a
shop that sells on a guess can sell twice. Give such a module `confirmedOnly: true`: it then runs
only on ticks every machine agrees on, a few ticks (about 130 ms) late, and never takes anything back.

```ts
export default {
  confirmedOnly: true,
  onEvent(ctx, ev) {
    if (ev.name === "goal") ctx.state.score[ev.data.team] += 1
  },
}
```

Use it for:
- **Scoring:** points, kills, laps, objectives.
- **Match flow:** round start and end, the winner, the timer running out, respawn waves.
- **AI directors:** what spawns where and when, difficulty changes, and any decision every machine
  must make the same way.
- **Economy:** purchases, rewards, inventory changes and currency.

Leave movement, aiming, the camera, your own shots and hit effects as they are: they must answer at
once, and the rollback corrects them. A module with `confirmedOnly` does nothing different in
single-player.

## Don't

**Don't recurse deeply.** A networked game stops any callback more than 256 calls deep, with a
`RangeError` ("call depth exceeded"). A flood fill or a tree walk that recurses can pass that easily
(a 32 x 32 grid is 1,024 calls deep). Use a list you push to and pop from instead:

```ts
const todo = [start]
while (todo.length > 0) {
  const cell = todo.pop()!
  // ...visit cell, push its unvisited neighbours onto todo
}
```

**Don't change the built-ins.** `Math`, `Array.prototype` and the `ctx` handles are frozen for every
behaviour (networked or not): patching one throws a `TypeError`.

**Don't rely on a member the browser doesn't have yet** if browser players can join the match. In a
mixed session (browser and desktop players together) a desktop machine gets only what the browser
supports, and a behaviour that calls anything else stops with `WebUnsupportedError` on every machine.
The table under "Conventions" in `docs/API.md` lists what is missing and when it comes.

**Don't time gameplay with a real clock.** A cooldown on `ctx.time.now` is fine. `ctx.debug.wallMs()`
is real time, for measuring how long your own code takes; a value from it kept in `ctx.state`, or a
branch taken on it, differs between machines.

## If something goes wrong

- **"module '...' uses `async` (line 12): this session refuses promises, async and await"** (or
  "uses `**`"): the message names the module and the line; change that line as above.
- **"call depth exceeded"**: a recursion is too deep; use a list, as above.
- **`WebUnsupportedError`**: the behaviour calls something browser players can't run yet; leave it
  out of a mixed match, or wait for it to come to the browser.

