# Onceworlds engine game

This is a browser game on the **Onceworlds engine**, published on **Onceworlds**, a platform for AI-made games. The engine is
the code you build on: worlds of entities, systems, 2D and 3D drawing, physics, input and touch, sound, UI, game feel, and the
platform's rooms, matches, saves and bots built in. The platform hosts the files and the engine. You write only the client:
HTML, JavaScript and assets, with no build step and no server code.

Read this guide first. It says what to reach for, in what order, and what to leave to the engine. The full reference (every
component, option and error) is the API reference: https://onceworlds.com/engine-api.md. The plain SDK guide, for what lies
below the engine (`window.onceworlds`, raw rooms and matches), is https://onceworlds.com/agents.md.

## Workflow

1. **Start from a kit.** `npx @onceworlds/cli new my-game --kit <kit>` makes a folder with a complete, working game of one
   kind (the table below), this guide as `AGENTS.md`, and an `onceworlds.json`. `--list` shows the kits. With the onceworlds
   MCP tools, `new_game` with `kit` does the same. A kit already has a lobby, bots, touch controls, sound, a finished round and
   tests: change it into your game rather than starting from an empty file.
2. **Change it.** Rules and numbers first, then art and sound, then the title and how it looks. Keep it playable after every step.
3. **Check it.** `npm install` once (for the tests and types), then `npx @onceworlds/cli check` before every push. It runs the
   game's tests, then plays simulated matches headless with a reloaded host, a dropped player, the host changing, a late joiner
   and garbage messages, and says what broke. It needs no browser and takes seconds. Fix what it reports before anything else.
4. **Try it.** `npx @onceworlds/cli dev` plays the folder on a local platform (when you work on the platform itself), or push a
   draft with `deploy_preview` and play it on onceworlds.com/create. Open a **private server** and its invite link in two or
   three windows to be several players.
5. **Make the store art.** `npx @onceworlds/cli posters` draws the game's `game.poster()` scenes into `store/` (below).
6. **Publish.** Push to GitHub (the Onceworlds workflow deploys every push) or publish the draft from the Create page. The
   platform guide's "Workflow" section has the details of repos, previews, visibility and review: https://onceworlds.com/agents.md.

### The files

```
index.html          loads main.js as a module; nothing else is needed
main.js             import { createGame } from '@onceworlds/engine'; import { config } from './game.config.js'; createGame(config).start();
game.config.js      export const config = { modules, systems, scene };  export const actions = { ... }   (no side effects)
src/ or *.js        components, systems, prefabs, UI views, bots
test/*.test.js      headless tests with @onceworlds/engine/test
store/              icon and thumbnails (made by `posters`)
onceworlds.json     { "slug", "name", "description", "engine": "1", "genre", "icon", "thumbnails", "controls", "badges" }
package.json        devDependencies: @onceworlds/engine (types and tests), vitest. Not part of the game.
```

- **Keep the game's `createGame` config in a module with no side effects** (`game.config.js` at the top or in `src/`) and
  export it as `config` (an object, or a function `(page) => config` when each player's page differs). `main.js` starts it;
  tests and `onceworlds check` import the same file, so what is tested is what players run. Export the action map as `actions`
  too and `check` compares "How to play" with it exactly.
- **`"engine": "1"` in `onceworlds.json`** makes the platform add an import map before your scripts, so
  `import ... from '@onceworlds/engine'` and `'@onceworlds/engine/modules'` work in a plain page and a zip upload. `"1"` is the
  newest 1.x (fixes and features arrive without a redeploy); `"1.2"` the newest 1.2.x; `"1.2.3"` exactly that version, forever.
  A deploy that names a version the platform doesn't have fails and says what to write. A game without the field gets no map (a
  game that bundles its own copy of the engine doesn't need one).
- The engine's files come from onceworlds.com/engine/<version>/, one address for every game, cached for a year per version, and
  the physics WebAssembly (about 1 MB, only when a game uses physics) is a separate file that never changes. You never copy,
  host or import it yourself. Do not write your own import map for `@onceworlds/engine`: the platform's comes first.
- **Games never import three.js or Rapier.** Everything is reached through the engine; the few members that expose a backend
  (`render3d.native`, `physics.raw`) are marked unstable: use them only when nothing else can do it.
- Plain JavaScript is the default: only `.js`, `.mjs`, HTML, CSS, JSON, images, audio, fonts, glTF, WebAssembly and shaders are
  deployed (a `.ts` file isn't). Write TypeScript if you like and build it to JavaScript with a build script: the project's
  `dir` in `onceworlds.json` is then the build's output (see the platform guide).

## Pick the default path

Each kind of game has one well-worn path. Start from its kit and keep its choices until something forces a change.

| Kind of game | Kit | Draws with | Moves with | Camera | Shell |
|---|---|---|---|---|---|
| Top-down arena, brawler, tank game, twin-stick shooter | `arena-2d` | `Render2D`, `Sprite` or `Shape2D`, `Tilemap` | `Physics2D`, `TopDown` (dash) | `CameraRig.fitAll` or `.follow` | `Flow.rounds`, bots fill seats |
| Platformer, runner, obstacle course | `platformer-2d` | `Render2D`, `SpriteAnim`, `Tilemap` | `Physics2D`, `Platformer2D` | `CameraRig.sideScroll` | `Flow.rounds` (race) or `Flow.coop` (stages) |
| Party and minigames, last-one-standing, hide and seek | `party-2d` | `Render2D`, `Shape2D`, `Text` | `TopDown` or your own systems | fixed `camera2D` | `Flow.rounds`, several short rounds |
| Third-person 3D arena, battle, adventure | `third-person-3d` | `Render3D`, `Model` or `Mesh`, `Light.sun`, `Sky`, `PostFx` | `Physics3D`, `Character3D` | `CameraRig.thirdPerson` | `Flow.rounds` or `Flow.dropIn` |
| Racing, driving, kart | `racing-3d` | `Render3D`, `Mesh` | `Physics3D`, `ArcadeVehicle` | `CameraRig.thirdPerson` or `.follow` | `Flow.rounds` (laps), bots drive |
| Tactics, board and card games, puzzles | `tactics-iso` | `Render3D` ortho or `Render2D` | `Grid`, `GridMover` (A*, ranges) | `CameraRig.isometric` | `Flow.turns` |

Games the table doesn't name, by the nearest path:
- **Survival and sandbox worlds, hangouts, social spaces**: the third-person or arena kit with `Flow.dropIn` (play at once, no
  rounds), `Net` for the shared world, `Save` for what stays between visits.
- **Shooters**: the third-person kit with a `Collider3D` per hitbox and `physics.raycast` for bullets (hit scan on the host), or the
  arena kit in 2D. Bullets are `rpc` calls to the host, which validates and applies them.
- **Co-op runs**: `Flow.coop` with checkpoints; late friends join at the next checkpoint.
- **Single-player games**: the same kits without `Net`; `Flow.none` or `Flow.dropIn`. Playing alone is being the only player.

A game is **modules** (what the engine adds: `Render2D`, `Physics3D`, `Input`, `Audio`, `UI`, `Feel`, `Net`, `Flow`, `Save`...),
**components** (plain data with a schema), **systems** (functions over queries that run in stages), and a **scene** that spawns
what exists at the start:

```js
import { createGame, defineComponent, defineSystem, t, Transform } from '@onceworlds/engine';
import { Render2D, Shape2D, Input, Audio, UI, Feel, Net, Flow, camera2D } from '@onceworlds/engine/modules';

const Coin = defineComponent('Coin', { value: t.u8(1) });

export const actions = {
  move: Input.axis2d({ keys: 'wasd arrows', stick: true, pad: 'left', label: 'Move' }),
  dash: Input.button({ keys: 'Space', touch: { label: 'Dash', big: true }, label: 'Dash' }),
};

const Collect = defineSystem({
  name: 'collect',
  stage: 'fixed',                          // 60 times a second, however fast the screen is
  query: [Coin, Transform],
  authority: 'host',                       // only the host's page runs it: the rules move with the host
  run({ query, world, feel }) { /* ... */ },
});

export const config = {
  space: '2d',
  modules: [Render2D(), Input({ actions }), Audio(), UI(), Feel(), Net({ components: [Coin] }), Flow.rounds({ /* title, lobby, roster, round, scoring, results */ })],
  systems: [Collect],
  scene(world) { world.spawn([Transform(), camera2D({ height: 12, clear: '#10131c' })]); },
};
```

## Go a layer down only when you must

Every default has a lower layer, and the lower layer is the same API the default is written with. Go down one step at a time and
only for the thing that needs it; the rest of the game stays on the defaults.

| You need | Default | Go down to |
|---|---|---|
| A controller that feels different | `Platformer2D`, `TopDown`, `Character3D`, `ArcadeVehicle` with their options | write a system that reads `Intent` and calls `physics.moveCharacter(entity, desired)`; then raw `Body` velocities and `applyImpulse` |
| A look no preset gives | `Material.standard/toon/unlit`, `PostFx.polished()`, `Sky.preset` | `Material.custom` (WGSL), `PostFx({ passes })`, then `render.addPass` |
| Your own screens | the standard Flow views (`flow.title`, `flow.results`...) with `UI({ theme })` | replace a view by its name, then `ui.draw.*` immediate mode, then `ui.overlay` HTML |
| Rules of who owns what | `Net({ components })`, `owner: 'host'`, `rpc`, `claim` | `net.send` and `net.onMessage` with your own validated messages |
| A different match shape | `Flow.rounds`, `.turns`, `.dropIn`, `.coop` with their options | `Flow.none()` and drive `room.startMatch`, `endMatch` yourself, following the platform guide's "Matches" |
| A sound that isn't there | `Audio` synthesized effects, `audio.play(name)`, buses | the Web Audio graph through `audio.context` |
| More than the engine does | everything above | `window.onceworlds` (the SDK) beside the engine; read https://onceworlds.com/agents.md |

When you do go down, the rules that kept the default safe still apply: simulation in `fixed` systems with `time.dt` and the seeded
`world.rng('name')` (never `Math.random` or `Date.now` in rules), drawing in `render`, host-only rules behind `authority: 'host'`,
and every message from another page checked before it is used.

## Make it fun

Players find games in a grid of thumbnails, often on a phone, and leave within seconds if they don't get it. Make games a teenager
and an adult both want to play, that a younger player still understands. These are defaults from games that worked and games that
didn't; the creator's idea always wins.

**The idea**
- **Familiar beats clever.** Tag, a race, an obstacle course, musical chairs, hide and seek, a color-matching floor: the name is the
  rules. Make it yours with a theme, a twist and polish, not new rules to learn.
- **The name says what you do** ("Pass the Bomb"), and the game fits one sentence a friend would repeat.

**The first minute**
- **One tap to play.** The title is the game's name, the game alive behind it (`title: { scene }`), and one Play button. Players
  move within 10 seconds: no tutorial screens, no settings first.
- **Show the goal, don't explain it.** An arrow, a glowing target, a flag. Two to five words at the moment they matter, then gone.
- **The core action is fun in 30 seconds**, on an empty map: action, instant feedback, a reward, the next decision, every few seconds.

**While playing**
- **Always know the goal and the score** from the screen: the goal, the time left, your place or score, in big shapes and numbers.
- **Short rounds, fast restarts**: 30 seconds to 3 minutes, a match of a few rounds with points, results, and the next match one tap
  away (`flow.dismissResults()` / the UI's Play again). A player who is out keeps doing something (spectate the leader, a ghost).
- **Simple controls**: move plus one or two buttons, the same everywhere. On phones the stick and at most three buttons.
- **Ramp the challenge**: easy at first, harder within each round; mistakes cost seconds; the one behind can catch up.
- **Never alone**: `roster: { fill, bot }` seats bots so a lone player gets the full game. Bots play like people (`defineBotSystem`,
  `botProfile(skill)`: a reaction time, aim error, mistakes, a personality), never perfectly.
- **Make winning a moment**: the countdown, a podium with the winners' avatars, a badge for a milestone. The standard Flow screens
  do it; the kits keep them.

**Juice: every action answers.** Within a tenth of a second every action gets a sound and motion: `feel.squash(entity)` on jumps
and landings, `feel.particles.burst('sparks', entity)`, `feel.floatText('+3', entity)`, eased `tween`s (never linear), `feel.shake`
and `feel.hitStop(80)` for knockouts (shake the camera, never the body), a synthesized sound for every action. Everything respects
the player's Reduce motion setting for you. Juice returns to rest and never blocks input.

**Screens anyone can read at a glance.** Buttons at least 56 px on phones, text at least 18 px, few words (labels and icons, no
paragraphs), saturated colors on a contrasting background with a symbol beside every color that matters, room to breathe, one
art direction and one font (the UI theme is one flat, bold style). Draw each player with their avatar and name (`NameTag`), mark
"You", give bots names and looks. The UI keeps clear of the platform's buttons and the thumbs by itself: don't fight it.

**A look for everyone.** Aim at the polish of games people already play: a confident style, modern type, real lighting in 3D
(`Light.sun({ shadows })`, `Sky.preset`, `PostFx.polished()`). Babyish blobs make a game look like it's for toddlers.

## What the platform requires, and what the engine already does

The platform's rules are in https://onceworlds.com/agents.md ("Rules of the platform", "Multiplayer rooms", "Matches"). With the
engine most of them are done for you. This is the list to check against, and the rules that are still yours.

**The engine does (keep the defaults):**
- **One server, chosen by the platform.** `Flow` and `game.join()` call `rooms.join` once as the game starts. Don't build Solo,
  Host, Join, Quick match, server codes or lists, invite links, friend lists, chat or volume controls: the platform has them.
- **Lobbies follow the rule: everyone readies up; a host starts private ones; joining never starts a game.** `Flow.rounds` shows the
  live lobby (players move about while the others get ready), the Ready/Start strip and the 3-2-1 countdown. Public lobbies start
  themselves.
- **Host changes and reloads.** The match phase lives in the room (`room.match`) plus one record the host keeps; a page that loads
  into a round resumes it, a new host continues from the newest state, and a paused match resumes under the new host. This holds
  only if the rules run behind `authority: 'host'` (or `net.acting()`) and the state that matters is replicated or in the room.
- **Ghost players.** A reload or a dropped connection holds the seat (15 and 30 s); the engine follows `connected` for you.
- **Touch controls.** The action map becomes the platform's stick and buttons while play is on, and goes away on every other screen.
  The mouse-look rules (lock only from a click in the world, panels free it, Esc opens your menu, a refused lock never pauses,
  touch never locks) are in `Input.pointer({ lock: 'while-playing' })` and `input.lock`.
- **Safe zones.** The UI stays out of the top left 130 x 56 (the platform's menu and chat), the lobby's Ready strip, the thumbs'
  corners and the notch. Don't put your own HUD there.
- **Reduce motion, Graphics quality, volume and mute** come from the platform's menu through `render.quality`,
  `render.reducedMotion` and the audio buses.
- **Saves and records.** `defineSave` / `Save` write through `onceworlds.save` after a pause and when the page hides, never every
  frame, with versions and migrations. Badges, leaderboards and the stats a match reports are declared once in `Flow` (`progress`).

**Still yours:**
- **`controls` in `onceworlds.json`** lists every key and button so "How to play" in the platform menu is right.
  `onceworlds doctor --fix` writes it from the action map (`controlsFromActions(actions)` is the exact function); keep the action
  map and the list in step.
- **Text from other players is untrusted.** Names, chat, room messages and shared state come from other browsers. Draw them as text
  (`Text`, `UI.label`, `ui.draw.text`, `ui.overlay.el({ text })`: there is no `innerHTML` to misuse), and validate every message
  you send with `net.send`: `rpc` and `claim` check their arguments against a schema for you, which is why they are the default.
- **Never ask for a password or personal details, never draw login or "session expired" screens.** Names that imitate Onceworlds,
  its staff or brands are refused. Players and games can be reported.
- **Network.** The game loads only its own files, the hosted engine, and the CDNs cdn.jsdelivr.net, cdnjs.cloudflare.com,
  unpkg.com, esm.sh, ga.jspm.io (and Google Fonts). Other APIs are blocked: use `onceworlds.fetch` with a secret for AI and media
  APIs, never a key in game code (see the platform guide, "Secrets").
- **The game frame is sandboxed.** `alert`, `confirm`, `prompt`, popups, navigation, clipboard writes and service workers are
  blocked; `localStorage` and cookies live in memory and vanish on reload (use `Save`); module workers don't work (classic ones do);
  no `SharedArrayBuffer`; `history.pushState` does nothing. The engine already avoids all of them.
- **Phones.** Guard browser APIs that phones lack (`navigator.vibrate`, `Notification`, `getGamepads`): feature-detect or
  `try/catch`. Keep text readable at 360 px, handle `resize` (the engine's renderers do), and play the game once in Safari on an
  iPhone (Xcode's Simulator is enough): a game that only ever ran in Chrome often draws nothing there. Import maps need Safari 16.4
  or newer, so an iPhone older than that can't run an engine game.
- **Limits.** 100 MB per game, 25 MB per file, 2,000 files, paths up to 200 characters, HTML up to 10 MB. Rooms seat at most 30
  players, and the more players, the less each may send (`room.budget`): the replication budget (`Net`) keeps a full room smooth.
- **Game page art before the first deploy** (below), and a real `genre`, `description` and `controls`.

## Multiplayer with the engine

- **An entity has an owner.** `world.spawn([...], { owner: me.id })` makes a player's page simulate it and write its components;
  `{ owner: 'host' }` is for the host's rules (bots, pickups, a ball). A component with `net: { owner: 'host' }` is written by the
  host even on a player's entity (health, score): clients can't award themselves anything.
- **Replicate what others must see, nothing else.** `defineComponent(name, schema, { net: { replicate: true } })` sends only changed
  fields, quantized by each field's `step`, batched inside the room's budget, drawn about 100 ms behind between two updates.
  Use `predict: true` on the owner's movement for instant feel; the host corrects it smoothly. Don't replicate what is derived.
- **Actions are calls.** `net.rpc('fire', { args: { angle: t.f32(...) }, to: 'host', rate: 10, validate, run })`: arguments are
  checked on arrival, a bad call is refused and never run, a player can only call so fast. Contested things ("two players reach one
  chair", a pickup) are `net.claim`s: the host settles them by the room's clock. Never trust a client's word about damage, score,
  position of someone else, or time.
- **The host is a page.** It can reload, drop or leave. Rules behind `authority: 'host'` follow the role at once; entities the host
  owns are kept in the room and come back with a new host. The engine tests exactly this (below): run `check`.
- **Time and randomness.** Rules use `time.dt` and `world.rng('stream')` (seeded from the match), shared deadlines use the match
  clock (`world.time.match`, paused with the match), never `Date.now()` or `Math.random()` in rules.
- **30 players.** Per-entity update rates of 10 to 20 Hz, interest by distance (`Net({ focus })`), `priority` on what matters.

## Assets

- Formats: glTF and GLB (Draco or Meshopt), KTX2, PNG and WebP, Opus and MP3, Tiled and LDtk maps, JSON, WebAssembly, WGSL.
  Preload per scene with `assets.add(name, { type, url })` and `await assets.load(...)`; show progress on the title.
- **No art files? Build it.** Procedural geometry (`Mesh.*`), gradient and pattern textures, noise, and the synthesized sound
  effects need no files and load instantly. A well-made flat style from `Shape2D` and `Mesh` beats borrowed art.
- Keep the first screen under 3 seconds on a phone: the title draws first, with the heavy assets loading behind it. Total under
  the platform's 100 MB; keep a 3D game under about 500 MB of memory.
- Other games' characters, logos and platform buttons are never fair game; look at the result at 128 px wide.

## Test it without a browser

The engine runs with no DOM and no GPU. `@onceworlds/engine/test` is how you (and the AI tool building the game) check a game in
seconds.

```js
// test/match.test.js
import { simulate } from '@onceworlds/engine/test';
import { expect, test } from 'vitest';
import { config } from '../game.config.js';

test('a full match ends with everyone ranked, through a reloaded host and a dropped player', async () => {
  const run = await simulate(config, { humans: 2, bots: 2, seed: 7, minutes: 3, chaos: ['reload-host', 'drop-player'], latency: [10, 60] });
  expect(run.errors).toEqual([]);
  expect(run.ranking).toHaveLength(4);
});
```

- **`onceworlds check` does the same for you** with a set of chaos runs, plus `npm test`. It is the gate before a push.
- **Chaos scenarios** are the bugs real games had: `reload-host`, `reload-player`, `drop-player`, `host-change`,
  `pause-during-host-change` (the host pauses the match, the role moves, the new host resumes it), `late-joiner`,
  `garbage-messages` (wrong shapes, other players' entities, floods, junk over shared records) and `host-leaves`. Same seed, same
  run: a failing seed is a bug report. Add your own: `{ name, at, run(env) }`.
- **Test the rules, not the pixels.** Step the game (`game.step(n)`, `game.run(seconds)`) and assert on components and
  resources: that a hit takes health, a pickup scores once, a round ends and ranks, a save round-trips, a bot reacts. Drive
  input with `input.setAxis('move', 1, 0)` and `input.tap('jump')`; replay a recorded run with `input.play(recording)`.
- Controllers have exact numbers (a jump reaches `jumpHeight`), the physics is deterministic, and UI layouts can be checked with
  `ui.preview(tree, { width: 390, height: 844, touch: true })` (buttons at least 56 px, nothing off screen).
- Before you ship: break it with three players (reload the host, close a window, let the match pause), and play it once alone
  from the title to the results. The platform guide's checklists ("Before you ship a multiplayer game", "Before you ship any
  game") still apply; `check` automates most of the first.

## Store art: posters

`game.poster(name, setup)` registers a staged scene for the game page. Opening the game with `?poster=cover` draws that scene
instead of the game, at an exact size and a pixel ratio of 1, frozen and deterministic. `npx @onceworlds/cli posters` opens your
latest draft that way and saves a picture of each into `store/<name>.png`, naming `icon` and the 16:9 pictures in
`onceworlds.json` when it has none.

```js
game.poster('cover', (p) => {                  // 1920 x 1080 by default; 'icon' is 512 x 512, 'badge-*' 256 x 256
  p.camera({ height: 10, clear: '#203040' });
  p.world.spawn([Transform(), Shape2D({ shape: 'circle', radius: 2, fill: '#ffd23f' })]);
  feelOf(p.world).particles.burst('confetti', { x: 0, y: -4 });
  p.settle(0.8);                               // simulate a little first, with the seed fixed
});
```

- **Icon:** one simple subject (the player, the main object) on a plain or softly shaded background, no words, nothing important
  in the outer 10%. **Thumbnails:** three to five, the first (the cover) the best picture of the game being played: real action
  with several things going on, the subject in the middle, any word short and large. Not the title screen, not a logo.
- Compose the scene on purpose: spawn the characters in a pose, set the camera, burst the particles, `settle` a moment. Look at
  the result at 128 px wide: if you can't tell what the game is, redo it.

## Mistakes the engine's errors will tell you about

The errors say what went wrong, where, and the line that fixes it. The usual ones:

| You wrote | What to do |
|---|---|
| `import ... from '@onceworlds/engine'` and the page fails to load | `"engine": "1"` in `onceworlds.json` (`doctor` warns about it) |
| `world.spawn([Health])` | components are called: `Health()` or `Health({ hp: 5 })` |
| `Math.random()` or `Date.now()` in a rule | `world.rng('name')` and `time.dt` / `time.match`: the page that replays or takes over must agree |
| a rule that changes shared state on every page | put `authority: 'host'` on the system, or spawn with `{ owner: 'host' }` |
| a system that changes physics bodies in `update` | do it in `fixed`, before `'physics2d'` / `'physics3d'` |
| shaking the player's transform | `feel.shake` moves the camera only |
| your own lobby, countdown or podium | use `Flow` and the standard views; replace one by name only for a different look |
| touch controls drawn by hand | `Input({ actions })` sets the platform's; branch on `input.device`, not on screen size |
| a message handler that trusts its input | use `rpc` or `claim` (schema checked), or validate every field |
| `localStorage` for progress | `defineSave` (the frame's storage is gone on reload) |

## What `onceworlds check`, `doctor` and the platform each look at

- `check`: the game's tests, simulated matches with chaos (needs `game.config.js` and `npm install`), and everything `doctor` finds.
- `doctor`: no viewport tag, text fields under 16 px, dialogs, `localStorage`, keys with no touch controls, a missing icon,
  thumbnails or `controls`, `Date.now()` for shared time, an engine import with no `"engine"` in `onceworlds.json`, and "How to
  play" out of step with the action map. `--fix` writes `controls`. `--strict` fails on likely bugs, for CI.
- A deploy checks the file types and limits, the `engine` version (and says which exist), the game page fields, and your repo.

## Gotchas found while building the kits

- `Input.pointer({ drag: 'none' })` never reports presses: use the default drag mode, or a button action, for taps.
- `net.isMine(entity)` is true for host entities on the host's page. Check `net.owner(entity) === me.id` when you mean "my own character".
- In `Flow.turns`, `FlowState.turn.me` is true on the host during bot turns (the host plays them). Check whose turn it is by id.
- Net only accepts a record from the page that created it. Spawn a player's own entities on that player's page, not on the host's.
- A kinematic body under a `Platformer2D` character blocks walking sideways. Make moving platforms colliders without bodies.
