# Hawkes Play scripting

A game is a world (`scene.json`, see WORLD.md) plus scripts. The runtime (Babylon.js underneath) runs every `.js`
file in `behaviors/` and then `scripts/`, in name order (or the order in `game.json` `"scripts"`), with one global:
**`game`**. Scripts share data through `game.state`. Units are meters and seconds. +y is up, +z is forward,
rotations are in degrees. Saving a script updates the running game at once (no reload; the player stays put).

Every game already has a **first-person player** (WASD, mouse look, space to jump, shift to run) and catches anyone
who falls out of the world. Put the things in the world with scene.json; use scripts for the rules (scores, timers,
waves, winning) and for things that come and go.

## game.json (optional)

```json
{
  "background": "#9fd3f0",
  "gravity": -24,
  "ambient": 0.7,
  "sun": 0.9,
  "shadows": true,
  "pixelated": false,
  "fog": { "color": "#9fd3f0", "start": 40, "end": 120 },
  "look": true,
  "sky": true,
  "player": true,
  "controls": true,
  "ui": { "font": "Georgia, serif", "accent": "#f4c430", "panel": "rgba(30,20,10,.85)", "radius": 4 },
  "scripts": ["scripts/world.js", "scripts/player.js"]
}
```

Everything is optional. `"lights": false` turns off the default sky light and sun; `"look": false` the game look
(glow, vignette, filmic color); `"sky": false` the gradient sky; `"player": false` the built-in player (make your
own camera); `"controls": false` keeps the player but lets your scripts move it. `"ui"` themes every screen the game
makes (below).

## Lifecycle

| Call | What |
|---|---|
| `game.onStart(fn)` | After every script has run. |
| `game.onUpdate((dt, time) => {})` | Every step (60 a second). `dt` is seconds since the last one. |
| `game.after(seconds, fn)` | Once, later. Returns a function that cancels it. |
| `game.every(seconds, fn)` | Repeats. Returns a function that cancels it. |
| `game.time` | Seconds since the start (stops while paused). |
| `game.now` | Seconds on a clock every player shares: use it for things that must line up between players. |
| `game.pause()`, `game.resume()`, `game.paused` | Pause: updates, timers and physics stop. |
| `game.restart()` | Starts the game over (the world as built, the scripts again). |
| `game.over(text?)` | Pauses and shows a message with a Play again button. |
| `game.mode` | `'build'` in the editor while building, `'play'` when playing. |
| `game.state` | A plain object for your data. Empty after a restart. |
| `game.save(key, value)`, `game.load(key, fallback)` | Kept across restarts while the page is open. |

## Events

`game.on(name, (data) => {})` hears the engine's events and your own (`game.emit(name, data)`):
`'kill'` (you destroyed something: `{ entity }`), `'death'` (anything destroyed, the player too: `{ entity, by }`),
`'hurt'`, `'pickup'`, `'checkpoint'`, `'finish'`, `'gate'` (`{ entity, points }`), `'waypoint'`.

`game.damage(target, amount, from)` hurts anything: the player, another player (over the network), or a thing with
health (the `health` or `enemy` behavior), which is destroyed at 0.

## The Overseer, the sky and the planet (`game.overseer`, `game.sky`)

```js
// A beam from the Overseer onto the crate (things you create are beamed in by themselves).
game.overseer.beam(game.find('Crate'), { radius: 2 });
// Beam something in: it grows out of the light.
const gift = game.box({ x: 0, y: 0.5, z: 6, color: '#ffd166' });
game.overseer.materialize(gift);
// Arrivals: things made by spawnMany come down by themselves (dropped, grown or beamed, by what they are).
game.spawnMany('car', 1, { at: [0, 0, 10] });              // out of the sky onto that spot (on the roof if there is one)
game.spawnMany('cow', 20, { at: game.player, area: 10 });  // a herd rains down one by one
game.spawnMany('crate', 4, { arrive: 'beam' });            // or say how: 'drop', 'grow', 'beam', 'none'
// Things a script makes with game.box, game.model... just appear; bring one in from the sky yourself:
const prize = game.box({ x: 0, y: 0.5, z: 6, color: '#ffd166', physics: 'dynamic' });
game.arrive(prize, 'drop');
game.on('landed', ({ thing, at, weight }) => { if (weight > 0.6) game.ui.message('THUD', 1); });
// The Overseer speaks: a line in the game's chat window, never a caption over the picture.
game.overseer.say('You found the key. Do with it what you will.', 4);
// The time of day.
game.sky.hour = 'night';              // or 21.5
game.sky.time = { hour: 6, minutes: 8 }; // a running clock
if (game.sky.hour > 20) game.ui.message('Night falls');
// How high you are, and whether you've reached space.
game.every(1, () => { if (game.sky.space > 0.9) game.ui.message('Space!', 1); });
game.on('docked', () => game.ui.message('Welcome aboard'));
// While the Overseer builds something someone asked for (its beams, the lit-up area, the work pill):
game.on('workStart', ({ at }) => {});          // it started (at: where)
game.on('workBeat', ({ n, title }) => {});     // a new beat of the build ("The stalls go up")
game.on('workEnd', ({ ok, ops }) => {});       // it finished (ops: how many changes landed)
game.on('terrainStep', ({ step, of }) => {});  // the ground it is reshaping moved another step
```

| | |
|---|---|
| `game.overseer.beam(target, opts)` | A beam from the emitter to a thing or `[x, y, z]`. `opts`: `radius` (m, at the bottom), `seconds` (1.6), `hold: true` (stays until `.end()`), `color`, `entity` (grows in), `ring`, `sound`. Returns `{ end(), move(target) }` |
| `game.overseer.materialize(thing or [things], opts)` | Beams it in (one wide beam for several) |
| `game.overseer.say(text, seconds, opts)` | Words from the Overseer: a line in the game's chat, through the speech desk (below). `seconds` is the least time it holds the desk; `opts.priority`: `'low'`, `'normal'` (default) or `'important'` |
| `game.overseer.speak(text, voice, seconds)` | The same, spoken in its voice: `voice` is one of its lines (`HawkesPlay.overseer.VOICES`: the intro, the mirror's questions and remarks, `overseer-welcome-aboard.mp3`...) or a sound asset of the game's. A new line needs a voice made first (ask J). Same `opts` as `say` |
| `game.speech` / `HawkesPlay.speech` | The speech desk, below |
| `game.on('overseer-speech', ({ state, text, voice, caption, voiced, priority, seconds, interrupted }) => ...)` | A line starts (`state: 'start'`: `caption` says whether its words are shown, `voiced` whether its clip plays) or ends (`'end'`), or the settings change (`'settings'`) |
| `game.arrive(thing or [things], how)` | Brings it in from the Overseer where it is now: `'drop'` (from high in the beam; real physics while playing; lands on a roof over it), `'grow'` (up out of the ground), `'beam'`, `'auto'` (by what it is). Several drop one by one |
| `game.spawnMany(what, n, { arrive })` | `arrive`: `'drop'`, `'grow'`, `'beam'`, `'none'`, or `{ how, delay, gap }` (seconds before the first, between each). Default: by what it is. Online the host's copies fall for real and everyone sees the same |
| `game.on('landed', ({ thing, at, weight, on }) => ...)` | A dropped thing came down (`weight` 0-1: how heavy it felt; `on`: what it landed on) |
| `game.overseer.intro()` | Plays the intro now (a promise: `{ skipped }`) |
| `game.overseer.position`, `.radius` | Where it is (`null` when the game has none) |
| `game.sky(colorOrAsset)` | As before: a sky picture or colour |
| `game.sky.hour` | The clock's hour (0-24; `null` with no clock). Set it (a number or `"night"`, `"dawn"`...) to jump there |
| `game.sky.time = value` | Like `config.time` (WORLD.md) |
| `game.sky.day` | Daylight now, 0.12 (night) to 1 |
| `game.sky.altitude`, `game.sky.space` | The camera's height above sea level, and 0 (in the air) to 1 (in space) |
| `game.on('intro', ({ state, skipped }) => ...)` | The intro starts (`state: 'start'`) and ends (`'end'`) |
| `game.on('beam', ({ at, radius, entity }) => ...)` | Every beam |
| `game.on('docked')`, `game.on('undocked')`, `game.on('docked-pad', ({ vehicle }))` | In the Overseer's hangar; landed on its pad |
| `game.on('observatory', ({ state, first }))` | The Observatory (the round room inside the ship, up the hangar's lift): `state` is `'enter'`, `'scene'` (the first arrival's scene starts), `'gift'`, `'ready'` (the scene or the quick line is done) or `'leave'`; `first` is true on the first arrival |
| `game.on('observatory-gift', ({ gift, kind, label }))` | The Overseer's gift (an accessory id, now in the player's look), once per device |
| `H.overseer.observatory` | `{ on, inside, state ('bay' \| 'riding' \| 'inside'), first, scene, lift, liftRoom, gift, pearl, level, music, places, go(), leave(), skip() }`; `H.overseer.interior(on)` hides the hull and hangar's look (the Observatory does); `H.overseer.bay` gives the bay's numbers |
| `H.overseer.dockShip(vehicle, out)` | Settles a ship on the hangar's pad by the dock path (the Docked moment and `docked-pad`, as a flown ship does); `out` puts the player on foot beside it. For tests and playtests. In the hangar on foot the sandbox's quest line reads "The Overseer's lift" |
| `game.on('bay-catch', ({ distance }))` | The catch took hold of a rocket or ship coming in (`H.overseer.assist = false` turns it off) |
| `game.on('ascent-failed', (d))` | A rocket/ship climb failed: `d.why` is `'wrecked'`, `'fuel'` (out of fuel high up) or `'fell'` (back on the ground after a climb), `d.vehicle`, `d.maxAltitude`. Set `d.handled = true` to say your own line (and give a new rocket); else the Overseer speaks |
| `H.overseer.approach` | While a rocket/ship flies: `{ distance, speed, closing, align, lateral, height, allowed, tooFast, state, advice, catching }` toward the pad, else null |

**A round planet** (`config.planet.sphere`, new games): scripts work in the world around the player, as on flat ground:
up is +y where the player is, positions near the player are small numbers, `game.box({ y: 1 })` near the start is on
the ground. Farther round, use `game.planet`:

| | |
|---|---|
| `game.planet.round`, `.radius` | Whether the world is a round planet; its radius (m) |
| `game.planet.up([x, y, z])` | Which way is up there (away from the middle; `[0, 1, 0]` on a flat world) |
| `game.planet.height([x, y, z])` | How high a point is over the ground |
| `game.planet.toPlanet(p)`, `.toLocal(p)` | A point here → where it is on the planet (scene.json's numbers), and back |

```js
// A beacon 20 m over the ground wherever the player is, standing up there.
const up = game.planet.up(game.player.position);
game.cylinder({ position: game.player.position.map((v, i) => v + up[i] * 20), radius: 0.3, height: 2, emissive: '#8fd8ff' });
```

Modules (round worlds): `H.planet.round`, `H.planet.heightOf(p)`, `H.planet.up(p)`, `H.planet.center`, and
`H.planet.frame` (round.js): the engine runs in a frame around the player that moves under them every ~100 m round;
`frame.onRebase((tr) => ...)` turns what a module keeps itself (`tr.point(p)`, `tr.dir(v)`, `tr.quat(q)`: Living things'
goals and vehicles' own state do this; entities, bodies, characters, the camera and the Overseer are moved for you),
`frame.toPlanet(p)` / `toLocal(p)`, `frame.fromScene(transform)` / `toScene(ops)` (scene.json keeps things on the
planet; edits made here are converted by `H.applyOps(ops, { local: true })` and the editor). Gravity pulls bodies to the
middle (physics.js); dynamic things and characters more than 1.5 km off rest (`e._far`) until the player comes back.

Modules: `H.sky` (the one owner of the clock and the light: report what you want with `H.sky.set(owner, { dark, flash,
weather })`, never write the lights yourself; `H.sky.bg` is the far layer, `H.sky.onBg(fn)` places far things each
frame), `H.planet` (`air(y)`, `altitude(y)`, `drop(d)`, `space`, `radius`, `surface`; every standard/PBR material curves
with the planet; a custom shader adds `y -= |xz - camera.xz|² / 2R` itself), `H.overseer` (`beam`, `materialize`,
`emitter()`, `flare(k)`, `hangar`, `docked`, `intro`; a beam's `pulse(k)`), `H.arrive` (`bring(things, { how, exact })`,
`how(entity)`, `plan(specs)` (building: a drop's spot, roof and all, before the op), `landing(at, { lift, half })`,
`quake(at, { radius, rise, seconds })` (the ground moving), `down(at)` / `up(at)`, `of(entity)`, `active`).

The Overseer's first-time lines (`lines.js`): `H.lines.say(id)`, `.seen(id)`, `.blocked()`, `.reset()`; ids `build`, `fish`, `death`, `dock`, `space`, `boom`, `friend`, `return`. Each is said once per device, in the chat (`game.on('overseer-line', ({ id, text, voice }))` hears it), and waits while the intro, the mirror or a tour moment is on or another Overseer line is being read.

The Overseer's free phrasebook (`talk.js`): in every game, published ones too, Enter (or the 💬 button on a phone) opens the chat box and plain talk is answered in the Overseer's voice at no cost (no model: keyword rules, several variants each). Where the builder's assistant is attached (the editor, the signed-in sandbox) typing goes to it instead. Requests to make or change things are explained, with a Get started link; questions about its motives are turned away kindly. `H.talk.reply(text)` gives `{ intent, text, action? }`, `H.talk.say(text)` says it in the chat, `game.on('overseer-talk', ({ intent, said, text }))` hears it. With other players here, typing also goes to them and the Overseer answers only when its name is said.

**The Overseer's speech desk** (`HawkesPlay.speech`, `modules/overseer/speech.js`): every Overseer line, whoever asks (`say`, `speak`, its first-time lines, the mirror, the tour, talk), goes through one desk, one line at a time.

- Lines never overlap. A later line of the same or lower priority waits its turn (a `low` one gives up after 20 s, a `normal` one after 45 s); an `important` one (the answer to what you typed, the mirror's questions, the intro) cuts in gently: the voice fades in a quarter of a second, the words showing finish at once, then it speaks.
- A line lasts as long as its voice clip, else as long as it takes to read (about a second and 0.3 s a word). Its words type out in the chat over that time.
- **Captions.** With captions on (the default) every line is shown. Off, a line is hidden only while its voice is really heard (it has a clip, the voice is up, sound is running): with sound off, the voice at 0 or no clip, it is always shown. Sound off never loses a line.
- **Settings** (kept per device; the chat's ⚙, the editor's menu): captions on/off, the Overseer's voice volume and the music volume, each apart (`audio.voiceVolume`, `audio.musicVolume`). The chat's ⟲ (the editor's menu) replays the last line.
- The editor's assistant replies are captions in its chat. They are spoken only when the Overseer has a clip with those very words; never the browser's voice.

```js
const s = HawkesPlay.speech;
s.say({ text: 'The tide is in.', priority: 'normal' });          // queues behind what is being said
s.say({ text: 'Look up.', voice: 'overseer-ask.mp3', priority: 'important' }); // cuts in gently
s.set({ captions: false, voice: 0.6, music: 0.3 });             // the player's choices
s.replay();                                                      // the last line again
s.current; s.waiting; s.busy; s.log;                             // what is said, what waits, what was said
```

### The journal (the sandbox island; `H.memory.journal`)

The Overseer's memory also keeps which neighbours the player has met and which lore they have found, as ids (at most 16 each, `[a-z0-9-]`, kept in the account when signed in, on the device otherwise, merged by union).

```js
const j = HawkesPlay.memory.journal;
j.get();            // { met: ['pip', 'dolores'], lore: ['lamp'] }
j.meet('rolo');     // true when it was new; saved
j.find('tally');    // true when it was new; saved
```

The sandbox's `journal.js` is the panel (a book button top right, **N**, or the editor's pause menu "Journal"). Events: `game.on('met', {id})`, `game.on('lore-found', {id, n, title})`, `game.on('journal', {open, from})`. Funnel: `sandbox_journal_open {from, quests, met, lore}` and `sandbox_lore_found {id, n, found}` (add, never rename).
### The travel board (inside the Observatory)

```js
const ob = H.overseer.observatory;
ob.board;                  // [{ id, name, line, count, at }]: the featured games as planets (count: players there now, null until known)
ob.travel('platform-run'); // what E under a planet does: the Overseer speaks, fade, then navigate to /play/t/<id>?from=observatory&look=...
ob.beamIn();               // back from a game (?return=observatory does it on load): at the board, with a line
ob.traveling;              // { game, phase, href } while a trip lasts
game.on('observatory', ({ state }) => {}); // + 'travel', 'beam-down', 'return', 'travel-failed'
```
A featured game page opened with `?from=observatory` has a corner button and a pause menu (Esc twice) with "Return to the Observatory".

### It is alive (presence)

The ship reacts, with the same meshes (no draw calls added): while the speech desk has a line, the halo brightens and the rings turn about three times as fast; before a beam opens the emitter flares (the shaft follows 0.28 s later); at night the seams glow much stronger; when the player asks it something (the chat, or `game.emit('asked')`) it turns its bay toward them for 14 s (seen from afar, where the ship is drawn in the sky; close up the real hangar stays put); after 40 s of nothing it idles with slow, deeper breathing. A low hum (two sines and a soft triangle, through the background so a voice ducks it and the music slider sets it) is silent on the ground and rises as the player climbs and the ship gets near.

```js
game.overseer.presence();   // { speaking, flare, idle, quiet, attending, turn, halo: {alpha, scale}, rings: {speed}, seams: {glow}, night, hum: {level, playing, altitude, distance}, beams }
HawkesPlay.overseer.attend(seconds);   // face the player, as a question does
```

## The player

`game.player` is you: a capsule, 1.8 m tall, that walks, jumps, flies while building and is caught if it falls.

| | What |
|---|---|
| `game.player.health` | 0 to 100. At 0 the player goes back to the start. `game.hurt(amount)` takes some. |
| `game.player.controls` | `false` stops the built-in walking (move it yourself with `walk`, `move`, `velocity`). |
| `game.player.gravity` | `0` floats, `1` normal, `0.5` moon jump. |
| `game.player.flying` | True while flying. |
| `game.player.view` | `'first'`, `'third'` (see your character) or `'top'`. Settable. |
| `game.player.speed`, `.jumpHeight`, `.doubleJump`, `.canFly` | Walking speed, jump height in meters, a second jump in the air, flying while playing. (The world's `config.player` sets the start values: WORLD.md.) |
| `game.state.waypoints` | How many waypoints (things with `data.role` "waypoint") the player has reached. |
| `game.spawn(role = 'player')` | Puts the player on a spawn point (a `spawn` thing with that `data.role` or tag) and returns them. |
| `game.respawn()` | Back to the start (or the last checkpoint). `game.checkpoint([x, y, z])` sets where. |
| `game.player.tool` | What the player holds: a gun (`'rifle'`, or `{ type: 'rifle', damage: 20 }`) or a module's tool. `weapon` is the same. |
| `game.player.control(mode, options)` | Switches how the player moves: `'walk'`, `'flight'`, or a module's (driving, swimming). `game.player.controller` is the current one. |

## Things from the world

`game.entity(id)` is a thing from scene.json by its id; `game.find(name)` by name; `game.all(tag)` every thing with
that tag (or name); `game.nearest(tag, from)` the closest. They are entities (below), like the ones scripts make.

## Making things

Every maker returns an **entity**. Things made by scripts last while the game runs (they aren't saved in the world).

```js
const crate = game.box({ size: 1, y: 3, color: '#b07a45', physics: 'dynamic' });
const coin = game.cylinder({ x: 3, y: 1, radius: 0.4, height: 0.1, rotation: [90, 0, 0], color: '#f4c430', physics: 'trigger', tag: 'coin' });
const tree = game.model('tree.glb', { x: -4, size: 3 });      // an asset; size = height in meters
const hero = game.sprite('hero.png', { y: 1, size: 2 });        // a 2D image that faces the camera
const sign = game.text('Danger!', { y: 2, size: 0.4 });         // floating words
```

| Maker | Shape options |
|---|---|
| `game.box(opts)` | `size: [w, h, d]` or a number |
| `game.sphere(opts)` | `radius` |
| `game.capsule(opts)` | `radius`, `height` |
| `game.cylinder(opts)` | `radius`, `height` |
| `game.cone(opts)` | `radius`, `height` |
| `game.torus(opts)` | `radius`, `thickness` |
| `game.plane(opts)` | `size: [w, d]`: flat on the ground |
| `game.model(name, opts)` | A `.glb` asset. `size` scales it to that height. `entity.ready` is a promise. |
| `game.sprite(name, opts)` | An image asset. `size` is its height. `frames: { cols, rows, fps }` for a sprite sheet (`fps` plays it on a loop; `e.animate(from, to, fps)` picks frames). |
| `game.text(words, opts)` | `size` (letter height), `color`, `billboard: false` keeps it flat |
| `game.light(opts)` | `type: 'point' / 'spot' / 'sun'`, `color`, `intensity`, `range`, `position` |

Options every maker takes: `x, y, z` (or `position: [x, y, z]`), `rotation` (degrees around y, or `[x, y, z]`),
`scale`, `color: '#rrggbb'`, `texture: 'name.png'` with `tile: [u, v]`, `emissive: '#rrggbb'` (glows), `opacity`,
`physics`, `bounce`, `friction`, `mass`, `tag`, `name`, `shadow` (default true), `visible`, `data` (your fields).

**physics:** `'static'` (stays put, solid; the default), `'dynamic'` (falls, bounces, gets pushed and thrown),
`'kinematic'` (solid, moves only when you move it; things on it ride along), `'trigger'` (not solid, reports `hit`),
`'none'` (decoration). Things collide by their real shape (a ball rolls, a crate tumbles, a ring can be flown through) and turn as they fall.

## Entities

| Property / method | What |
|---|---|
| `e.x, e.y, e.z`, `e.position` (`[x, y, z]`) | Where it is (its center). Settable. |
| `e.rotation` | Turn around y in degrees. Settable. `e.turn(degrees)` adds; `e.rotate(x, y, z)` adds on every axis. |
| `e.scale`, `e.size` | Scale (settable); size in meters `[w, h, d]`. |
| `e.vx, e.vy, e.vz`, `e.velocity` | Velocity (dynamic things use it every step). `e.push([x, y, z])` adds to it. |
| `e.move(dx, dy, dz)` | Moves by that much, stopping at solid things. |
| `e.walk(direction, speed)` | Sets horizontal velocity from a direction (`{x, z}`, e.g. `game.input.direction()`) and turns to face it. |
| `e.jump(speed)` | Jumps if it is standing on something. Returns true if it jumped. |
| `e.grounded`, `e.standing` | Standing on something; the thing it stands on. |
| `e.lookAt(target)`, `e.forward`, `e.distanceTo(other)` | Facing, the way it faces (`{x, z}`), meters to another. |
| `e.touching(tagOrEntity)` | True if it is touching one. |
| `e.on('hit', other => {})`, `e.on('leave', other => {})`, `e.on('click', point => {})` | Touch starts, ends; clicked or tapped. |
| `e.color`, `e.emissive`, `e.opacity`, `e.visible` | Settable. |
| `e.say(text, seconds)` | A speech bubble over it. |
| `e.play(name, loop)` | Plays a model's animation (`e.animations` lists them). `e.animate(from, to, fps)` plays sprite frames. |
| `e.destroy()`, `e.alive` | Removes it. |
| `e.tag`, `e.name`, `e.data`, `e.sceneId` | Labels, your fields, its id in scene.json (if it came from there). |
| `e.mesh` | The Babylon node, for anything not covered here. |

## Behaviors of your own

A file in `behaviors/` defines one; any thing in the world can then use it with `{ "use": "name", ...options }`
(see WORLD.md for the built-in ones). Each function gets `(thing, ..., options, memory, game)`; `memory` is that
thing's own scratch object.

```js
// behaviors/wobble.js
game.behavior('wobble', {
  defaults: { amount: 10, speed: 3 },
  update(thing, dt, o, memory, game) { thing.node.rotation.z = Math.sin(game.now * o.speed) * o.amount * Math.PI / 180; },
  hit(thing, other, o, memory, game) { if (other === game.player) thing.say('Hey!'); },
});
```

`start(thing, o, memory, game)` runs once, `update(thing, dt, ...)` every step, `hit(thing, other, ...)` on touch,
`stop(thing, ...)` when it goes.


**Many at once:** `game.many(100, (i) => ({ type: 'box', size: 0.8, x: i % 10, z: Math.floor(i / 10), y: 3, physics: 'dynamic', color: '#ffffff' }))`
makes copies of one mesh (cheap: one draw call for all of them), each with its own position, color and physics. Use
it for herds, piles, crowds and debris. A world keeps itself playable: past a few thousand things the oldest ones
scripts made make room, the farthest moving things rest until you come near, and only the nearest things cast
shadows (`game.json` `"budget": { "entities": 2500, "bodies": 400, "shadows": 160 }` changes the limits; phones get less).
The budget also has `textureMB` (texture memory: textures are sharp near you and small far away, within it),
`maxTexture`, `drawDistance` (meters of draw distance per meter of a thing's size), `shadowDistance` and `pixelRatio`
(WORLD.md "Keeping it fast"). Give a thing `e.noCull = true` to draw it at any distance.

## The mirror (`game.mirror`)

Who players are: a `mirror` thing's questions (WORLD.md, "The mirror"). For scripts:

| | |
|---|---|
| `game.mirror.open(mirror?)` | opens the questions at a mirror (the thing, its name, or the nearest one) |
| `game.mirror.dress()` | the same questions anywhere, without a mirror (a character screen); `dress({ steps: ['what'], whats: ['animals'] })` asks less |
| `game.mirror.close()` | closes them (what they chose so far stays) |
| `game.mirror.isOpen`, `game.mirror.step` | whether it's open, and which question is on screen (`what`, `look`, `name`, `view`) |
| `game.mirror.pending` | a mirror will open by itself in a moment (after the intro) or is open: wait before greeting |
| `game.on('mirror', ({ state, what, name, step }) => ...)` | `open`, `step`, `pick` (they picked a `what`), `done` or `skip` (with `what` and `name`) |
| `game.presence.look` | what they chose: `{ what, name, body, accent, head, shape, hat, face, extras, eyeColor, accessory, accessoryColor }` |

```js
// The game starts once the player has been to the mirror.
game.on('mirror', ({ state, name }) => {
  if (state !== 'done' && state !== 'skip') return;
  game.ui.message(name ? `Go, ${name}!` : 'Go!');
  startRace();
});
// A shoe gets a speed boost (what you are can matter).
game.on('mirror', ({ state, what }) => { if (state === 'done' && what === 'shoe') game.player.speed = 9; });
```

## Blocks (`game.blocks`)

With `config.blocks` on (World: Blocks). Block coordinates are whole numbers from 0 at the world's corner; `y` up.

| | |
|---|---|
| `game.blocks.get(x, y, z)` | The block's name (`'air'` for nothing). |
| `game.blocks.set(x, y, z, type)` | Changes one block for everyone (`type` a name, or `'air'`). |
| `game.blocks.fill([x, y, z], [x, y, z], type)` | A box of blocks (at most 20,000). Returns how many. |
| `game.blocks.mine(x, y, z)`, `place(x, y, z, type)` | As a player would (the `mine` / `place` events), from no bag. |
| `game.blocks.top(x, z)` | The highest solid block's y in a column (-1: none). |
| `game.blocks.at([x, y, z])` | The block a world point is in. `game.blocks.toWorld(x, y, z)`: a block's middle in the world. |
| `game.blocks.aim()` | What the crosshair is on: `{ x, y, z, n: the face, type }` or null. |
| `game.blocks.bag`, `give(type, n)`, `take(type, n)`, `count(type)` | The player's bag. |
| `game.blocks.select(type)`, `picked` | The block R places. |
| `game.blocks.craft(name)`, `recipes` | Crafting. |
| `game.blocks.edits()` | The changes from the seed's world, as `config.blocks.edits`. |

```js
// A tower of gold where the player starts, and a reward for every diamond.
const [x, , z] = game.blocks.at(game.player.position);
game.blocks.fill([x + 3, game.blocks.top(x + 3, z) + 1, z], [x + 3, game.blocks.top(x + 3, z) + 6, z], 'gold');
let found = 0;
game.on('mine', (d) => { if (d.type === 'diamond' && ++found === 5) game.over('Five diamonds! You win'); });
```

## Looking, aiming, hitting

`game.raycast(from, direction, distance)` is the first thing a ray hits: `{ entity, point, distance, normal }` or
null. `game.aim(distance)` is what the crosshair is on. `game.camera.forward` and `game.camera.position` are the view.


## Physics

Things with `physics: "dynamic"` fall, tumble, roll and stack; the rest stay put or move only when moved. Scripts can
push them around:

| Call | What |
|---|---|
| `e.push([x, y, z])` | Adds to its velocity (m/s). |
| `e.impulse([x, y, z])` | A kick: heavier things (`mass`) move less. |
| `e.force([x, y, z])` | A steady push for this step: call it every step (thrusters, wind). |
| `e.angularVelocity = [x, y, z]` | Spins it (degrees a second). |
| `e.upright = true` | It never tips over (characters, animals). |
| `game.physics.explode([x, y, z], { radius: 5, force: 15, damage: 40 })` | Throws everything nearby away and hurts what has health. Every explosion (this, `game.explode`, rockets, barrels) sends one `'explosion'` event: `game.on('explosion', ({ position, radius, force, damage, authoritative }) => ...)` (`authoritative`: decided here; make lasting changes only then). |
| `game.physics.overlap(game.physics.shapes.sphere(3), [x, y, z])` | The things within 3 m. |
| `game.physics.joint('hinge', door, frame, { axis: [0, 1, 0], pivotA: [-0.5, 0, 0] })` | Joins two things: `fixed`, `hinge`, `ball`, `slider`, `distance`, `spring`. |
| `game.physics.gravity = -9.8` | The world's gravity (m/s², default -24, which feels like a game). |

## Items and money (`game.items`)

| | |
|---|---|
| `game.items.give(item, n)`, `take(item, n)` | The player's bag (online through the host). |
| `game.items.has(item, n)`, `count(item)`, `bag`, `money` | What they carry. |
| `game.items.use(item)` | Uses it (its `use`), as the Use button does. |
| `game.items.buy(shop, item)`, `sell(shop, item)`, `craft(make)` | As the panels do. |
| `game.items.unlock(door)` | As E at a `locked` thing. |
| `game.items.about(item)` | `{ name, icon, about, price, usable }`. |
| `game.items.open('bag' / 'bench')` | Opens the panel. |

```js
// A bounty: 10 coins for every crate broken, a key after five.
let n = 0;
game.on('death', (d) => { if (d.entity && d.entity.name === 'Crate') { game.items.give('coins', 10); if (++n === 5) game.items.give('key'); } });
```

## Terrain (`game.terrain`)

When the world has `config.terrain` (WORLD.md "Terrain"), `game.terrain` reads and reshapes the ground. (Without one,
`game.terrain.active` is false and reads give null.) Modules use the same thing as `HawkesPlay.terrain`.

| Call | What |
|---|---|
| `game.terrain.heightAt(x, z)` | The ground's height (world y) there, or null off the terrain. |
| `game.terrain.normalAt(x, z)` / `slopeAt(x, z)` | The ground's up direction `[nx, ny, nz]` / its steepness in degrees. |
| `game.terrain.materialAt(x, z)` | `'grass'`, `'dirt'`, `'rock'`, `'snow'` or `'sand'`. |
| `game.terrain.place(thing, lift = 0)` | Puts a thing on the ground below it (and returns the height). |
| `game.terrain.randomPoint({ near: [x, z], radius, maxSlope: 30 })` | A spot on dry, walkable ground `[x, y, z]` (spawn animals over the hills). |
| `game.terrain.dig(x, z, radius = 3, depth = 2, { pile, share })` | A hole (flat bottom from radius 4). `dig(x, z, 20, 15)` is a giant one. `pile: true` heaps the dirt beside it. |
| `game.terrain.raise / lower(x, z, radius, meters)` | A smooth bump or dip. |
| `game.terrain.flatten(x, z, radius, height?)` | Levels the ground (at `height`, or the height in the middle): a building site, a landing pad. |
| `game.terrain.smooth(x, z, radius, strength = 0.6)` | Softens bumps. |
| `game.terrain.paint(x, z, radius, 'dirt')` | Paints a layer: `grass`, `dirt` (roads, paths), `rock`, `snow`, `sand`, or `erase`. |
| `game.terrain.crater(x, z, radius, depth?)` | A crater with a rim, scorched, and the trees in it gone. |
| `game.terrain.mountain(x, z, { radius, height, road })`, `hill(x, z, radius, height)` | Adds a shape to the live world. |
| `game.terrain.road` | The mountain road: `[x, z]` points from its foot to the top (shape `mountain`). |
| `game.terrain.info` | `{ center, size, min, max, shape, cell }`. |
| `game.on('terrain', ({ edits, box }) => ...)` | The ground changed (dug, cratered, brushed). |

**Who sees an edit.** Scripts run on every player's machine, so an edit a script makes at the start (or on a shared
event) happens everywhere by itself. An edit from one player's action (a key, a click) needs `{ share: true }` as the
last argument so everyone gets it: `game.terrain.dig(x, z, 3, 2, { share: true })`. The shovel and explosions share
theirs already. Script edits aren't saved in the world (they're part of playing); stamps and brush strokes are.

**Explosions** (the engine's `'explosion'` event: `game.explode`, `game.physics.explode`, rockets, barrels, grenades)
leave a crater sized by the radius where the blast was decided (`authoritative`; everyone gets the edit), unless
`config.terrain.craters` is false or the blast was high in the air.

**The shovel** is a tool: `game.player.tool = 'shovel'` (or `{ type: 'shovel', radius: 2.5, depth: 1.2 }` for a big
one). Click digs where you look and throws the dirt beside the hole; shift+click (or R) piles dirt; holding G or R keeps going. On a
phone: hold the Dig and Pile buttons.

```js
// Dig a giant hole in front of the player when they press H, for everyone.
game.input.onKey('h', () => {
  const p = game.player, f = p.forward;
  game.terrain.dig(p.x + f.x * 25, p.z + f.z * 25, 20, 15, { share: true });
});

// 30 sheep on the hills (keep them off steep ground and out of the sea).
for (let i = 0; i < 30; i++) {
  const at = game.terrain.randomPoint({ near: [0, 0], radius: 120, maxSlope: 25 });
  if (at) game.box({ x: at[0], y: at[1] + 0.6, z: at[2], size: [1.2, 1, 0.8], color: '#f4f1ea', physics: 'dynamic', tag: 'sheep' });
}

// A flag on the summit of the mountain road.
const top = game.terrain.road.at(-1);
game.cylinder({ x: top[0], z: top[1], y: game.terrain.heightAt(top[0], top[1]) + 2, radius: 0.08, height: 4, color: '#dddddd' });
```

For modules: `HawkesPlay.terrain` is the same object (`heightAt`, `normalAt`, `dig` are the shared contract), plus
`field` (the heights, read only), `core` (the math), and `entity` (the ground's thing). The ground is one static
Havok heightfield body (tree trunks a second one), rebuilt at most four times a second after edits.

## Races (`game.race`, `game.on('race')`)

The world's courses are run by the Race module (WORLD.md, "Races"); a script reads and listens.

```js
game.on('race', (d) => {
  if (d.state === 'countdown') game.ui.message(String(d.n), 0.8);           // 3, 2, 1
  if (d.state === 'go') game.ui.message('GO!', 1);
  if (d.state === 'gate') console.log(`gate ${d.index} of ${d.of}`, d.ms, d.delta);   // d.delta: ms against your best run's gate (null the first time)
  if (d.state === 'finish') game.ui.message(d.isBest ? 'New best!' : 'Finished', 2);   // d.ms, d.best, d.previous, d.beatGhost, d.beatPar
});
```

| Call | What |
|---|---|
| `game.on('race', fn)` | `state`: `countdown` (`n`: 3, 2, 1), `go` (`ghost`: whether one is racing), `gate` (`index`, `of`, `lap`, `ms`, `delta`), `lap` (`lap`, `laps`, `ms`, `delta`), `finish` (`ms`, `isBest`, `previous`, `best`, `beatGhost`, `par`, `beatPar`, `rival`), `abort` (`why`: `left`, `wrecked`, `far`, `rescue`). Every one has `course` (the id). |
| `game.race.courses` | The courses as set in the world: `id`, `name`, `vehicle`, `laps`, `width`, `par`, `rival`, `gates` (`x`, `y`, `z`, `dx`, `dz`: the way it faces). |
| `game.race.state` | `{ phase: 'idle' \| 'countdown' \| 'running', course, lap, laps, ms, next, checkpoint, of }`. |
| `game.race.best(id)` | The best time in ms (0: none yet), from the account's memory and this device. |
| `game.race.ghost(id)`, `game.race.board(id)` | The kept ghost (`{ ms, step, p }`, decimeters) and the board rows (`{ rank, name, ms, rival }`). |
| `game.race.showBoard(id)` | Opens the board card. |
| `game.race.abort(why)` | Calls the race off. |
| `game.race.setLaps(id, n)` | Changes a course's laps (not while it is being raced). |

Times are milliseconds of game time from GO. A script that wants the island's Pip to cheer, a prize at a best time or a
race of its own kind (on foot?) listens to `race` and does it; the clock, gates, ghost and board stay the module's.

## Camera

The player's camera is first person. For other kinds of games:

| Call | What |
|---|---|
| `game.camera.follow(entity, { distance: 10, height: 5, behind: false, smooth: 0.1 })` | Follows from a fixed angle, or from behind it. |
| `game.camera.firstPerson(entity, { height })` | Mouse look from an entity (`height` from its feet for the player). |
| `game.camera.orbit(target, { distance: 12 })` | Drag to look around a point or an entity. |
| `game.camera.topDown(entity, { height: 18 })` | Straight down from above. |
| `game.camera.fixed([x, y, z], [lookX, lookY, lookZ])` | Stays put. |
| `game.camera.shake(strength = 0.3, seconds = 0.3)` | Shakes. |

## Input

| Call | What |
|---|---|
| `game.input.key('w')` | Held down. Names: letters, digits, `space`, `shift`, `enter`, `escape`, `arrowup`... |
| `game.input.pressed('space')` | Went down this step. `game.input.released(k)`: went up. |
| `game.input.axis()` | `{x, y}` from WASD, the arrows or the touch stick, each -1 to 1. |
| `game.input.direction()` | `{x, z}`: the same, turned to match the camera. Feed it to `walk`. |
| `game.input.mouse` | `{x, y, down, pressed}` in pixels. |
| `game.input.pick()` | What's under the mouse (or the crosshair): `{ entity, point }` or null. For clicking on things: strategy games, building, shops. |
| `game.input.onKey(key, fn)` | Calls fn when the key goes down. |

On phones there is a stick, dragging looks around, and two buttons: **⤒** is `space` and **B** is `e`.

## Vehicles (`game.car()`, `game.vehicles`)

Every vehicle kind has a maker that returns the vehicle (an entity): `game.car(opts)`, `game.truck(opts)` (the 4x4),
`game.bicycle`, `game.motorbike`, `game.boat`, `game.submarine`, `game.airplane`, `game.helicopter`, `game.rocket`, `game.spaceship`. Options are the
spec's: `x, y, z` (or `position`; no `y` puts it on the floor at 0), `rotation`, `color`, `name`, `scale`, `speed`,
`power`, `grip`, `handling`, `mass`, `health`, `model`, `data`.

```js
const car = game.car({ x: 3, z: 8, color: '#2266ff', name: 'Blue' });
const jeep = game.vehicles.make('4x4', { x: -4, z: 8 });     // any type or name: car, truck, 4x4, jeep, bike, plane, heli...
game.vehicles.enter(car);                                     // you get in (the driver's seat if it's free)
car.on('enter', (d) => game.ui.message(`${d.me ? 'You' : 'Someone'} got in`));
game.on('exit', (d) => { if (d.vehicle === car && d.me) game.ui.message('Walking again'); });
```

| Call | What |
|---|---|
| `game.vehicles.enter(v, { seat, passenger })` | Puts you in (seat 0 drives). Returns false if it can't (parked prop, wrecked, full). |
| `game.vehicles.exit()` | Gets you out (beside a door, moving as it was). |
| `game.vehicles.current`, `game.vehicles.seat` | The vehicle you're in (or null) and your seat. |
| `game.vehicles.list(type?)`, `game.vehicles.nearest(from?, maxDistance = 50)` | Vehicles in the world; the closest one with a free seat. |
| `game.vehicles.make(type, opts)` | Makes one by type or name. |
| `game.vehicles.drive(v, { speed, steer, throttle, up, down, boost, brake, hover })` | Drives it from code (`null` stops): `speed` m/s or `throttle` -1..1, `steer` -1..1, helicopters `hover` (meters up). |
| `game.vehicles.board(v, thing, { driver })` / `unboard(thing)` | Seats a non-player (an AI friend, an animal, a dummy) and takes it out again. With `driver: true` and `drive()` it drives you around. |
| `game.vehicles.repair(v)`, `flip(v)`, `honk(v, seconds)`, `blast(at, { radius, force, damage })` | Fix it, put it back on its wheels, honk, throw and hurt vehicles near a point. |
| `v.speed`, `v.velocity`, `v.driver`, `v.riders`, `v.health`, `v.wrecked`, `v.lights`, `v.autopilot` | What it's doing (`lights`, `health`, `autopilot` are settable). |
| `v.enter()`, `v.exit()`, `v.honk()`, `v.flip()`, `v.repair()`, `v.push([x, y, z])`, `v.impulse([x, y, z])` | The same, on the vehicle. |
| `game.on('enter' / 'exit', ({ vehicle, player, id, seat, driver, me }) => {})`, `v.on('enter' / 'exit', fn)` | Someone got in or out (you: `me`). |
| `game.on('crash', ({ vehicle, speed, other, at }) => {})`, `game.on('wrecked', ({ vehicle }) => {})` | A hard hit; out of health. |

Vehicles are real physics bodies: everything in "Physics" works on them, and `game.physics.explode` (or the Mayhem
module's explosions) throws and hurts them. A vehicle made by a script exists on every player's machine (scripts run
everywhere) and its driver keeps the others in step.

```js
// A taxi: an AI friend drives you in circles until you press T.
const taxi = game.car({ z: 10, color: '#f4c430', name: 'Taxi' });
const driver = game.capsule({ x: 2, z: 10, color: '#8b5a2b', name: 'Driver' });
game.vehicles.board(taxi, driver, { driver: true });
game.vehicles.drive(taxi, { speed: 9, steer: 0.4 });
game.input.onKey('t', () => game.vehicles.drive(taxi, null));
```

**Modules** (`HawkesPlay.vehicles`): `types`, `kinds`, `list()`, `near(from, radius)`, `nearest(from, radius)`,
`enter`, `exit`, `board(v, entity, { driver })`, `unboard`, `drive`, `blast`, `useOffer(player)` (what E does here:
`{ label, act }`), `waterLevel(x, z)`, `simulatedByMe(v)`, `current`, `seat`. A vehicle's own state is `e.vehicle`.

**How driving feels** (`HawkesPlay.vehicles.feel`, `vehicles-feel.js`; looks and sounds only, no forces): the view widens with speed
(and boost, and a drift), the chase camera trails farther back, the body leans in bends and dips under braking, dust and spray
come off the wheels by surface (sand and dirt a lot, paving hardly), the engine climbs through gears and is higher under load,
the handbrake or a slide squeals, a landing dips the view with dust and a thud, bumps shake it a little, a drift swings the
camera toward the slide, and headlights come on at night (the L key still wins). Phones: Juice's level halves (reduced) or
mostly stops (minimal) the particles; "reduce motion" turns the camera effects well down. `feel.math` has the pure numbers
(`fovKick`, `camLag`, `roll`, `pitch`, `engineRpm`, `dustAmount`, `skid`, `landing`, `bump`, `lightsWanted`); `feel.sample()`
gives what the local vehicle is doing now (`fov`, `roll`, `pitch`, `rpm`, `dip`, `skid`, `puffs`, `landed`).

## Living things (`HawkesPlay.living`)

Makers: `game.creature({ species, x, z, ...shape })` (with `count` > 1 it makes a herd), `game.herd({ species, count,
position })`, `game.friend({ name, personality, species, x, z })`. Every creature is an entity (push it, hurt it,
`e.say('Moo')`), with `e.data.species` and `e.hp`. A herd's animals are `herd.members`. `game.living` is also
`HawkesPlay.living`: write `const living = HawkesPlay.living;` at the top of a script (`game.living` is the same thing,
but only once the world is running: in updates, events and timers, not at the top of a script or in `onStart`).

| | |
|---|---|
| `living.spawn(species, opts)` | one creature (`x`, `z`, `y`, `follow`, `temper`, `size`, `color`...), or a herd with `count` (and `at`, `radius`) |
| `living.all(species?)`, `living.near(at, radius, species?)` | creatures |
| `living.command(e, what, opts)` | `'follow'` (`target`: `'player'`, an entity or a name), `'stay'`, `'sit'`, `'sleep'`, `'come'` (`target`), `'goto'` (`at: [x, z]`), `'dance'`, `'cheer'`, `'fetch'` (`target`), `'carry'` (`thing`), `'drop'`, `'wander'`, `'attack'` (`target`), `'flee'` (`target`); `seconds` ends it |
| `living.follow(e, target)`, `living.stay(e)`, `living.goTo(e, [x, z])`, `living.wander(e)` | the same, shorter |
| `living.startle(at, { radius, strength })` | scares the creatures near a point: they run (herds stampede). Explosions (the engine's `'explosion'` event: any blast) and gunfire do it by themselves; the closest are knocked over |
| `living.status(e)` | `{ species, state, act, hunger, energy, fear, following, carrying, riding }` (state: idle walk run eat sleep sit dance knocked attack cheer fly swim flop) |
| `living.ride(e)`, `living.dismount()` | puts the player on a creature and off it (the `ride` controller) |
| `living.path(from, to)` | a way around obstacles: `[[x, z], ...]` or null |
| `living.pickUp(e, thing)`, `living.drop(e)` | a creature carries something (in its mouth or its arms) |
| `living.talk(friend, text)` | says something to an AI friend: a promise of `{ say, action }` (the action happens). Friends answer from a free phrasebook (personality, species voice); there is no paid AI talk |
| `living.act(friend, action, say)` | makes a friend do and say something: `living.act(max, 'dance', 'Watch this!')` |
| `living.openChat(friend)` | opens the chat with a friend (`null` closes it) |
| `living.chat.addChip({ label, show(friend), act(friend) })` | a button of your own in friends' chats (shown when `show` says so); returns a function that removes it |
| `living.species.add(name, def)` | a new species: `living.species.add('goat', { ...living.species.get('sheep'), colors: { body: '#eee', head: '#eee', legs: '#555', accent: '#555' } })` |
| `living.count`, `living.max` | how many creatures there are, and the world's limit |
| `living.on('startle' / 'hurt' / 'act', fn)` | hear about it |

The **`ball` tool** (`game.player.tool = 'ball'`): click (or G, or the Throw button on phones) throws it; dogs fetch
it; walk over it to pick it up. The **`ride` controller**: `game.player.control('ride', { mount: horse })`.

Playing together: the host runs the brains (a rider runs their mount) and herds send their animals' places a few
times a second; what a friend says and does reaches everyone.

```js
// A dog that follows you, and a ball to throw for it.
game.onStart(() => {
  const rex = game.creature({ species: 'dog', name: 'Rex', x: 2, z: 2, follow: 'player' });
  game.player.tool = 'ball';
});

// 50 sheep; a wolf shows up after 30 seconds.
game.onStart(() => {
  game.herd({ species: 'sheep', count: 50, position: [0, 0, 20] });
  game.after(30, () => game.creature({ species: 'wolf', x: 40, z: 40 }).living.hunger = 1);
});

// A shepherd friend who comes when you score.
const max = game.friend({ name: 'Max', personality: 'a calm old shepherd', follow: false, x: -3, z: 4 });
game.on('score', () => HawkesPlay.living.act(max, 'cheer', 'Well done!'));

// Stampede on a key.
game.input.onKey('k', () => HawkesPlay.living.startle(game.player.position, { radius: 30 }));
```

## Screens: text, bars, menus, popups

Everything a game puts on the screen. Theme it all with `game.ui.theme({ font, color, accent, panel, border, radius,
blur })` (or `"ui"` in game.json), and add your own CSS with `game.ui.style(css)` for classes you pass as `class`.

| Call | What |
|---|---|
| `game.ui.text(id, text, { x: 'left', y: 'top', size: 22, color })` | A line of text. `x`: left, center, right. `y`: top, middle, bottom. |
| `game.ui.bar(id, value, { label, color, x, y })` | A bar from 0 to 1 (health, time). |
| `game.ui.button(id, label, fn, { x, y })` | A button. |
| `game.ui.message(text, seconds = 2)` | A big message in the middle. |
| `game.ui.panel(id, { title, text or html, buttons: [{ label, onClick }], x, y, width, closable, class })` | A box on screen: an inventory, a quest log, a shop. Returns `{ close, update }`. |
| `game.ui.menu(id, { title, text, items: [{ label, onClick, primary }], pause, closable, class })` | A menu over the game: a pause menu, a level select. It pauses the game (unless `pause: false`); arrows and Enter work. |
| `await game.ui.popup(text, { title, buttons: ['Yes', 'No'] })` | A question; resolves to the label picked. |
| `await game.ui.ask(question, { placeholder })` | Asks for words; resolves to what they typed (or null). |
| `game.ui.attach(id, entity, { text or html, offset })` | A label that stays on a thing in the world (names, health, prices). |
| `game.ui.html(id, html, { x, y, class })` | Your own HTML, for anything else. `game.ui.el(id)` is its element. |
| `game.ui.remove(id)`, `game.ui.clear()` | Removes them. |

```js
// A pause menu on P, in the game's own style.
game.ui.theme({ font: '"Trebuchet MS", sans-serif', accent: '#ff7a1a', panel: 'rgba(20,12,4,.9)', radius: 2 });
game.input.onKey('p', () => game.ui.menu('pause', {
  title: 'Paused',
  items: [
    { label: 'Resume', primary: true },
    { label: 'Restart', onClick: () => game.restart() },
  ],
}));

// A shopkeeper who asks before selling.
const shop = game.find('Shopkeeper');
game.ui.attach('shop_label', shop, { text: 'Shop' });
shop.on('click', async () => {
  const pick = await game.ui.popup('A sword for 10 coins?', { title: 'Shop', buttons: ['Buy', 'No thanks'] });
  if (pick === 'Buy' && (game.state.score || 0) >= 10) { game.state.score -= 10; game.ui.message('Sold!'); }
});
```

### A game's own screens: how they fit together

- **Pressing Play starts the game from the top**: the world as built, every script again, the player at the start.
  Show a title screen only when `game.mode === 'play'` (building shouldn't stop at it), from `game.onStart`.
- **The mouse:** a game with no menu at the start gets the mouse captured when Play is pressed. A menu's Start
  button calls `game.input.capture()` (browsers only capture on a click); open menus with `game.input.release()` first
  if the player was looking around. `game.input.captured` says which.
- **Once only:** `game.save('seen', true)` / `game.load('seen')` remember across "Play again" (`game.restart()`).
- **Endings:** a game that listens for `'finish'` shows its own results instead of the engine's "You made it!".
- **Pausing:** menus pause the game while open; bind a key that isn't Esc for a pause menu (`P`): in the editor Esc
  opens the editor's menu.
- **Style:** `game.ui.theme(...)` once, then `game.ui.style(css)` with classes you pass as `class`. Fonts that are
  always there: Orbitron (display), Rajdhani, Inter.

The three Overseer games (Bot Arena, Sky Racer, Leap) are complete examples: a title menu, a pause menu or countdown,
a HUD panel and a results screen, each a short script on top of the world.

## Water

`game.water` is the water in the world (the sea, water things, rain) and what you can do with it. WORLD.md "Water" has
the settings; this is for scripts.

| | What |
|---|---|
| `game.water.surfaceAt(x, z)` | Height of the water's surface there (waves included), or `null` where there's no water. |
| `game.water.inWater(pos)` | True if a point `[x, y, z]` (or a thing's middle) is under water. `depthAt(pos)`: how far under (negative: above). |
| `game.water.at(pos)` | `{ body, surface, depth }` or null: which water (`body.kind` is `"ocean"` or `"box"`, `body.entity` the water thing). |
| `game.water.bodies()` | Every body of water. `game.water.sea` is the sea (or null); `game.water.level` its level (settable). |
| `game.water({ size: [w, h, d], x, y, z, basin, waves, current, fish, color })` | Makes a water thing (like `game.box`). |
| `game.water.rise(level, seconds)` | Moves the sea's level smoothly: floods, tides, a draining lake. |
| `game.water.dump(target, { volume, height })` | Water falls on a thing, a player (by name: `'Sam'`; `'me'`) or a point `[x, y, z]`: it splashes, knocks people over, soaks them and puts out fire. `volume`: 1 a bucket, 10 a bathtub, 50 a water tower. Online the host decides what it pushes. |
| `game.water.splash(pos, size)` | Just the splash (spray, rings, sound): `size` 0.2 a pebble to 3 a cannonball. `ripple(x, z, size)`: rings only. |
| `game.water.wet(thing, seconds)` | Makes it wet (drips; puts out fire). `isWet(thing)`, `dry(thing)`. |
| `game.water.buoyancy(thing, { density, drag, points })` | Floats a thing for this step: call it every step from `game.physics.beforeStep` for your own hulls (vehicles do). Returns `{ submerged, surface, depth }`. Everything dynamic already floats by `data.density`; calling this takes over for that thing. |
| `game.water.swimming()` | True while you swim. `game.water.underwater`: your head is under. `game.water.rain`: how hard it rains (0 to 1). |
| `game.water.sound(name, { at, volume })` | `'splash'`, `'plop'`, `'bite'`, `'reel'`, `'catch'`, `'snap'`, `'drip'` (the game's own sound of that name if it has one). |

### Fish

| | What |
|---|---|
| `game.water.fish.list()` / `get(id)` | Every species: `{ id, name, rarity, water, kg: [min, max], cm: [min, max], color, strength }`. |
| `game.water.fish.add({ id, name, rarity, water, kg, cm, color, strength, points, chance, model })` | A species of your own (`rarity`: common, uncommon, rare, legendary, junk; `water`: fresh, salt, any; `strength` 0 to 1: how it fights; `model`: an asset name). `remove(id)`. |
| `game.water.fish.in(pos)` | The species living in the water there. `roll(pos)`: a random catch from there. |
| `game.water.fish.give(id, { weight })` | Counts as a catch for this player (the card, the log, the `catch` event for everyone). |
| `game.water.fish.journal()` | This player's catch log: `{ fish: { trout: { count, best, longest } }, total, points }`. `log()` opens it on screen (species caught with their best, the rest as silhouettes; J, or 📖 on phones), `clear()` empties it. Kept with the Overseer's memory: in the account when signed in, on this device otherwise. |
| `game.water.fish.odds(pos, { hour, depth, luck })` | `[{ id, p }]`: the chance of each species there. A species has `hours: [from, to]` (it bites 2.5x likelier then, 0.3x otherwise; wraps past midnight) and `depth: "shallow" \| "mid" \| "deep"` (1.8x / 0.4x; a longer cast reaches deeper: under 3.5 m shallow, under 6 mid). Nothing is ever ruled out. `hour` is the world's clock (`game.sky.hour`). |
| `game.water.fish.assist(on?)` | A first catch is made easy: until the player has caught anything (or while `assist(true)` is on) the first bite comes 2.5 to 4.5 s after the bobber lands (never past 5 s), it is a common fish, the bite waits 3 s, and the line cannot snap while the reel is held. `assist(true)` is the hook for scripts and the playtest bot to make every cast like that; `assist(false)` goes back to the normal tables; no argument says whether it is on. Turn the player's help off with the rod's option `firstCatch: false`. `phase()` says what the rod is doing (`ready`, `charge`, `flying`, `waiting`, `bite`, `reel`, `retract`, `show`, or `null` with no rod in hand). |
| `game.water.fish.board()` | Rewrites the catch boards: any thing with `data: { catchBoard: true }` shows your species count and three biggest, and E near it opens the log. |

### Events (`game.on`)

| event | data |
|---|---|
| `'catch'` | `{ fish: { id, name, rarity, weight, length, points }, by: { id, name }, mine, entity }`: on every player's screen, for every catch (contests) |
| `'species'` | `{ fish: { id, name, rarity, weight, length }, entity }`: this player's first catch of a species (not junk): a banner and confetti, and the Overseer notes it. `'catch'` data has `fish.first` |
| `'cast'`, `'bite'`, `'hooked'`, `'escape'`, `'snap'` | the fishing steps, on the fisher's screen |
| `'splash'` | `{ at, size, entity }`: something went into the water (or water landed: `bucket: true`) |
| `'soaked'` | `{ at, radius, entities, volume }`: dumped water landed |
| `'extinguish'` | `{ at, radius, entity, entities, by }`: water put out what is there (fire listens: Mayhem) |
| `'wet'`, `'knocked'`, `'swim'` (`{ swimming }`), `'underwater'` (`{ under }`), `'weather'`, `'rain'` (every 2 s while raining), `'lightning'`, `'bucket'` | |

### Examples

```js
// "Dump a bucket of water on Sam" (anyone in the game by name; a big one with volume: 20).
game.water.dump('Sam');

// "Every 30 seconds, drop a huge wave of water on a random player."
game.every(30, () => { const p = game.pick(game.all('player')); if (p) game.water.dump(p, { volume: 25 }); });

// "A fishing contest: 3 minutes, the heaviest catch wins."
const best = {};
game.ui.message('Fishing contest! 3 minutes', 2);
game.on('catch', (c) => {
  const who = c.by.name || 'You';
  best[who] = Math.max(best[who] || 0, c.fish.weight);
  game.ui.text('board', Object.entries(best).sort((a, b) => b[1] - a[1]).map(([n, kg]) => `${n}: ${kg} kg`).join('\n'), { x: 'right' });
});
game.after(180, () => { const top = Object.entries(best).sort((a, b) => b[1] - a[1])[0]; game.over(top ? `${top[0]} wins with ${top[1]} kg!` : 'No fish caught'); });

// "Flood the island slowly, then drain it."
game.water.rise(3, 20); game.after(40, () => game.water.rise(-0.6, 20));

// "Make the boat float" (a hull of your own: lift at four corners, so it rocks and rights itself).
const hull = game.find('Boat');
game.physics.beforeStep(() => game.water.buoyancy(hull, { density: 0.35, points: [[-1, -0.4, -2], [1, -0.4, -2], [-1, -0.4, 2], [1, -0.4, 2]] }));

// "Add a rare eel to the fish" (your own species).
game.water.fish.add({ id: 'night-eel', name: 'Night eel', rarity: 'rare', water: 'any', kg: [1, 4], color: '#2b2d6e', strength: 0.8 });

// "Put out the fire when it rains."
game.on('rain', () => { for (const f of game.all('fire')) f.destroy(); });
```

## Planets (`game.planets`)

| | |
|---|---|
| `game.planets.list()` | `[{ id, name, biome, gravity, home }]` (home first). |
| `game.planets.here` | The planet you're on (`'home'` for the world as written). |
| `game.planets.travel(id, { fx: true })` | Everyone goes there (with the warp; `fx: false` at once). False while building or already going. |
| `game.planets.canTravel`, `open()` | Whether the board can open here; opens it. |

```js
// Chapter two begins on Ember.
game.on('quest', (q) => { if (q.id === 'chapter-1' && q.done) game.after(3, () => game.planets.travel('ember')); });
game.on('arrive', (p) => { if (p.planet === 'ember') game.ui.message('Ember. It is hot here.', 3); });
```

## Homes (`game.homes`)

Houses, furniture and screens are things like any other (`game.house({...})`, `game.sofa({...})`, `game.tv({...})`
make them; WORLD.md "Homes" lists their options). `game.homes` is how scripts use them and build with them.

| Call | What |
|---|---|
| `game.homes.house({ rooms, floors, style, x, z, rotation, ... })` | A whole furnished house (the same options as the `house` thing). Returns it. |
| `game.homes.room({ type: 'kitchen', size: [6, 4], x, z })`, `game.homes.wall({ length, openings, x, z, rotation })` | One room; one wall with doors and windows. |
| `game.homes.plan(options)` | The plan of a house without making it: `{ size, floors, rooms: [{ name, type, floor, x, z }], parts }`. |
| `game.homes.find(house, what)`, `findAll(house, what)` | A house's pieces: `'front-door'`, a kind (`'fridge'`, `'tv'`, `'lamp'`), or a room's name (`'Kitchen'`). |
| `game.homes.use(thing, action?)` | Uses it as if a player pressed E (`action` picks one by its words: `'open'`, `'sit'`, `'flush'`, `'eat the apple'`). False if it can't now. |
| `game.homes.target` | What the player is looking at that they can use: `{ thing, action }` or null. |
| `game.homes.usable(thing, label, fn, at?)` | Makes anything usable with E: `game.homes.usable(npc, 'talk', () => npc.say('Hi!'))`. `fn(ctx)` gets `{ player, thing, set }`. |
| `game.homes.state(thing)` | Its shared state: `{ open }`, `{ on }`, `{ on, channel }`, `{ taken }`, `{ flushes }`... |
| `game.homes.set(thing, patch)` | Changes it for everyone: `game.homes.set(door, { open: true })`. |
| `game.homes.open(thing, open = true)`, `game.homes.power(thing, on?)` | Doors, fridges, cupboards; TVs, radios, lamps, taps, ovens (no `on`: flips it). |
| `game.homes.channel(tv, which)` | A channel by number (0 is the first) or by name (`'news'`), for everyone. |
| `game.homes.channels(tv, list)` | Its channel list (scripts run for everyone, so everyone gets it). |
| `game.homes.show(tv, channel)` | Shows a channel now, just here (null goes back): `{ text: () => 'Score: ' + game.state.score }` is a live scoreboard. |
| `game.homes.sit(thing)`, `game.homes.stand()` | Sits the player down on it (or lies them in a bed); gets them up. |
| `game.homes.note(n, { voice, volume })` | Plays a note (60 is middle C; voice piano, organ, synth, bell). |

Things you can use hear it: `thing.on('use', ({ action, player }) => {})`, and `thing.on('change', (state, changed) => {})`
when its state changes (from anyone). The game hears `'eat'` (`{ item, from }`), `'take'`, `'sleep'` (`{ bed }`),
`'shower'`, and `'note'` (`{ note, from }`, piano notes).

Tools (`game.player.tool = 'remote'`): **`remote`** (click: next channel of the TV you point at, R: on/off, Q: put
away) and **`phone`** (its screen shows the clock, every camera and the shows; click: next, Q: put away). Picking up a
`remote` or `phone` thing gives them. The **`seat`** controller is sitting and lying (`game.player.controller ===
'seat'` while seated).

```js
// A game show: the TV in the living room becomes a scoreboard, the fridge gives points.
const house = game.entity('house');
const tv = game.homes.find(house, 'tv');
game.state.score = 0;
game.homes.show(tv, { text: () => `Snacks eaten: ${game.state.score}` });
game.on('eat', () => { game.state.score++; });

// Lock the front door until the player finds the key.
const door = game.homes.find(house, 'front-door');
door.data.locked = true;
game.find('Key').on('hit', (who) => { if (who === game.player) { door.data.locked = false; game.ui.message('The door is open!'); } });

// Lights out at night: every lamp in the house.
game.after(60, () => { for (const lamp of game.homes.findAll(house, 'lamp')) game.homes.power(lamp, false); });

// Make an NPC you talk to with E.
const bob = game.capsule({ x: 3, y: 0.9, color: '#e07a5f' });
game.homes.usable(bob, 'talk to Bob', () => bob.say('Nice house you have!'));
```

Modules: `H.homes` has the same and more: `usable(e, points)`, `track(e, initial, apply, key)` (shared state),
`sit(e, at, yaw, { pose })`, `furniture(kind, shape, opts, make)` (a kind made of parts, wearing a model by name),
`tune(screen, channel)`, `screen(e, mesh)`, `loop({ asset, synth, thing })` (a sound near a thing), `assemble(root, parts)`
(pieces that follow a thing). A vehicle or creature module can make its things usable with `H.homes.usable(e, [{ label:
'get in', at: [x, y, z], use(ctx) {} }])` to share the E prompt.

## Mayhem (`game.mayhem`, `game.spawnMany`, `game.explode`)

| Call | What |
|---|---|
| `game.spawnMany(what, count, { at, area, pattern, spacing, size, height, velocity, data })` | Makes `count` things at once and returns them. `what`: a word (`'cow'`, `'cows'`, `'barrel'`, `'crate'`, `'ball'`, `'box'`, any animal or noun), a kind, an asset (`'tree.glb'`), or a scene spec (`{ type: 'sphere', shape: { radius: 0.3 }, material: { color: '#ff0000' } }`). `at` (default: in front of the player, on the ground), `area` (a radius, or `[width, depth]`), `pattern`: `'scatter'` (default), `'grid'`, `'circle'`, `'line'`, `'pile'`, `'stack'`, `'rain'` (dropped from the sky; `height` adds more). Stays within `config.mayhem.maxSpawn` (the oldest go first). Online, everyone gets the same things and the host moves them |
| `game.explode([x, y, z], { radius: 5, force: 18, damage: 40, fire: false, color, sound })` | A real explosion: things thrown and spun (heavier ones less), damage falling off with distance, breakables broken, barrels set off, `fire: true` lights things, a fireball, shockwave, smoke, debris, a shake and a boom. Online the host decides it once for everyone  A blast nobody set off (a meteor, a volcano): `by: 'world'` (no one's score or combo). |
| `game.mayhem.shatter(thing, { pieces, at, force, respawn })` | Breaks a thing into pieces now |
| `game.mayhem.ignite(thing, { seconds })` / `extinguish(thing)` / `isBurning(thing)` | Fire. Returns false for things that can't burn |
| `game.mayhem.tumble(thing, strength)` | Spins it like a ragdoll; creatures get back up when they land |
| `game.mayhem.rope(a, b, length)` | Ties two things (or a thing and a point `[x, y, z]`) with a rope; `cutRopes(true)` cuts them all |
| `game.mayhem.giveTool(name, options)` | Puts a tool in the player's hands and on the belt (`game.mayhem.belt` lists it) |
| `game.mayhem.clear()` | Removes everything spawned and every broken piece |
| `game.mayhem.slowMotion(seconds)` | Slows the game for a moment (playing alone) |
| `game.mayhem.sound(name, { at, volume })` | A mayhem sound: `'boom'`, `'crash'`, `'whoosh'`, `'pop'`, `'beep'`, `'fire'`, `'throw'`, `'zap'`, `'rope'`, `'moo'` (the game's asset by that name, or synthesized) |
| `game.mayhem.score`, `game.state.mayhem` | The MAYHEM score (things hit, broken, burnt, killed, times the combo) |
| `game.barrel({ x, y, z, power })`, `game.crate({ x, y, z, size })`, `game['tool-pickup']('lasso', { x, y, z })` | Makers for the kinds |

Events (`game.on`): `'explosion'` `{ position, radius, force, damage, fire, authoritative, by, chain, mayhem }` (the
engine's one explosion event, once per blast for every player; `authoritative` is true where it was decided: terrain
digs craters there), `'break'` `{ entity, pieces }`,
`'ignite'` `{ entity }`, `'spawned'` `{ things, what, count }`, `'landed'` `{ entity }` (an animal back on its feet),
`'mayhem'` `{ what, points, total, combo }`.

```js
// "Spawn 100 cows and give me a rocket launcher"
game.spawnMany('cow', 100, { area: 20 });
game.player.tool = 'rocket-launcher';

// "Rain barrels from the sky every 10 seconds"
game.every(10, () => game.spawnMany('barrel', 12, { pattern: 'rain', area: 15 }));

// "A tower of crates and a bomb under it"
game.spawnMany('crate', 8, { pattern: 'stack', at: [0, 0, 6] });
game.after(3, () => game.explode([0, 0.5, 6], { radius: 7, force: 30, fire: true }));

// "When a cow dies, spawn two more" (a hydra farm)
game.on('death', ({ entity }) => { if (entity.data.species === 'cow') game.spawnMany('cow', 2, { at: entity.position }); });

// "Points for every explosion"
game.on('explosion', (e) => { if (e.authoritative) game.state.score = (game.state.score || 0) + 10; });
```

**How the launcher and blasts feel** (`HawkesPlay.mayhem.feel`, `mayhem-feel.js`; looks and sounds only): a rocket leaves with a muzzle flash, a puff and a backblast, the launcher model kicks back and the view nudges up; its trail is a flame that fades into smoke left hanging behind. Barrels hurt by a blast at the same moment go off one after another along the shock (about 16 m/s, never closer than 0.12 s apart, the whole roll within 2.4 s), nearest first, instead of all at once. Each blast also throws embers, and its boom is three layers (crack, boom, rumble) that arrive later and duller the farther you are. Hit-stop, slow motion on big blasts, the shake that falls off with distance, the screen flash and scorch marks come from Juice. Budget: later blasts of a chain make less (fewer particles, no light, no embers) and a phone makes less again, so a ten-barrel chain stays inside `H.budget`. `feel.sample()` gives what happened (`chains`, `rolled`, `lastDelays`, `embers`, `muzzle`, `layers`, `maxLive`); the pure numbers are `HawkesPlay.mayhemMath` (`soundDelay`, `chainSchedule`, `detail`, `boomLayers`, `emberCount`, `launcherKick`).

## Presence (`game.presence`)

Name tags, chat, emotes, looks and spectating for scripts. `game.presence` is the same as `HawkesPlay.presence` (write
`const presence = HawkesPlay.presence;` at the top of a script if you need it before the world runs).

| | |
|---|---|
| `presence.say(text)` | says something in game chat as this player: a bubble over your head for everyone, in the chat log. False if too soon (a few lines per 10 s) |
| `presence.say(thing, text)` | a bubble over any thing (the same as `thing.say(text)`) |
| `presence.emote(name)` | plays an emote on your avatar for everyone: `wave laugh cheer shrug dance point sit sleep clap thumbsup heart` (also "hi", "lol", "thumbs up"...) |
| `presence.emote(name, 'all')` | everyone in the room does it (players, AI friends and pets) |
| `presence.emote(name, thing)` | a thing does it (AI friends and creatures act it out; scene things are synced) |
| `presence.feel(event, data, thing?)` | gives a body a feeling for a moment: `'land'` `{ speed }`, `'explosion'` `{ dist, radius }`, `'hurt'` `{ amount }`, `'catch'`, `'talk'` `{ seconds }` (the engine does these itself) |
| `presence.face(thing?)` | what a face shows now: `{ mood, emote, shows, talking, gaze }` |
| `presence.emotes()` | the list: `[{ name, icon, label, loop }]` |
| `presence.tag(thing, text, { color, kind })` | a name tag on anything (`kind`: npc, pet, ai, thing); `presence.tag(thing, null)` removes it |
| `presence.players()` | everyone here, you first then friends: `[{ id, name, me, friend, host, typing, muted, user, entity }]` |
| `presence.float(true/false)` | the Overseer's eye: a floating camera with no character (WASD, Q/E, drag; others see no body); `/play/<slug>?spectate=1` starts in it, "Join the game" ends it. `presence.floating()` |
| `presence.spectate(who)` | watch a player: `'next'`, `'prev'`, a player id, or `null` to stop. `presence.spectating()` is the one watched |
| `presence.mute(id, on)`, `presence.muted(id)` | mute a player's chat and emotes (for you only) |
| `presence.look`, `presence.setLook({ ... })`, `presence.randomLook()` | your look: `{ body, head, accent: '#hex', shape, hat, face, extras: [], model, hair, hairColor, bottom, shoes, eyeColor: '#hex', accessory, accessoryColor: '#hex', headSize, bodySize }` (`headSize` 0.5-3 and `bodySize` 0.3-4: times normal, for "a big head and a little body": `presence.setLook({ ...presence.look, headSize: 2.2, bodySize: 0.6 })`` (`accessory`: none, headphones, goggles, monocle, mustache, eyepatch, flower, antenna, medal, bell, jetpack, bandana, earrings; `core.ACCESSORIES` lists them with icon, label and anchor) (the person's hair: short, long, ponytail, curly, spiky, bun; `body` is the top, `bottom` the trousers, `shoes` the shoes, `head` the skin) (saved to your account when signed in) |
| `presence.goTo(id)` | go and stand next to a player (the Overseer's beam where you leave and where you arrive); the ✦ Go button and a tap on their name in Who's here do this. `presence.markers` is what is on screen this frame: `[{ id, name, mode: 'on' \| 'edge', x, y, angle, distance }]`: a marker over a player too far for a name tag, an arrow on the screen's edge toward one who is off screen (tap it to go) |
| `presence.report(id, reason)` | report a player (`'name'`, `'chat'`, `'other'`) and mute them for you; counted as a `presence_report` analytics event (no name, no text) |
| `presence.customize()`, `presence.showPeople()`, `presence.openChat()`, `presence.openEmotes()` | open the panels |
| `presence.on('say' / 'emote' / 'spectate' / 'people' / 'look', fn)` | hear about it (`say`: `{ name, text, me, friend, npc }`) |
| `presence.filter = (text) => text` | change chat lines before they show (a word filter) |
| `game.on('chat', fn)` | every chat line as a game event: `{ name, text, me, player }` |

Playing together: each player sends their own look, typing, health and host flag (`H.sync.state('presence:<id>')`);
lines and emotes go through the host (`H.sync.action('presence:say' / 'presence:emote')`) so everyone sees each once,
and a player can only speak or emote for themselves. Signed-in players show a signed badge; `/v1/presence/who` turns it
into their platform name and whether they're your friend.

```js
// A dance floor: step on it and everyone dances.
game.entity('floor').on('hit', (who) => { if (who === game.player) game.presence.emote('dance', 'all'); });

// A greeter who answers when someone says hello in chat.
const greeter = game.entity('greeter');
game.presence.tag(greeter, 'Greeter', { color: '#22c55e', kind: 'npc' });
game.on('chat', (m) => { if (!m.me && /\b(hi|hello)\b/i.test(m.text)) greeter.say(`Welcome, ${m.name}!`); });

// Out of lives: watch the others until the round ends.
game.on('death', () => { if (game.state.lives <= 0) game.presence.spectate('next'); });
game.on('round', () => game.presence.spectate(null));
```

`presence.anim`: a model's clips as data (`public/play/anim/<class>.json`, class mixamo, quadruped or object): `anim.load(cls)`, `anim.validate(cls, file)`, `anim.has(rig, clip)`, `anim.on((name, rig, event) => ...)` for a clip's events (footstep, hop). The pose of a rig can be forced to a clip at a moment with `rig.forceClip = { name, t }`.

## Sound

`game.sound('jump.mp3', { volume: 1 })` plays once. `game.music('theme.mp3', { volume: 0.5 })` loops;
`game.stopMusic()`. Sound starts after the player's first click or key (browsers require it).

## Look and effects

`game.sky('#9fd3f0')` sets the sky color; `game.sky('sky.jpg')` puts a panorama image around the world.
`game.burst([x, y, z], { color: '#ffcc00', count: 30, size: 0.2, speed: 4 })` throws out sparks.

## Assets by id (`game.assets`)

Every function that loads an asset (`game.model`, `game.sprite`, `game.sound`, `game.music`, `game.sky`, textures,
videos) takes an asset id (`a_...`) or a name. To find the game's assets for a word, ask:

| | |
|---|---|
| `game.assets.for('cow', { kind: 'model' })` | The ids for a word, in order: `config.assets.cow` first, then every asset labelled "cow" (plurals and other words too), then names (`cow.glb`, `cow-2.glb`). `kind`: `model`, `image` (texture, sprite, sky), `sound` (sfx, music, voice), `video` |
| `game.assets.pick('cow', { kind: 'model', seed: thing.id })` | The one for this thing: the default, or (when the word mixes) a variant picked by `seed`: the same every time for the same seed |
| `game.assets.get(ref)` | `{ id, name, url, type, title, words }` for an id or a name (or null); `game.assets.url(ref)`, `game.assets.has(ref)` |
| `game.assets.list()` | Every asset of the game |
| `game.assets.words()` | Each word and the ids it uses (`{ cow: { ids, mix } }`) |

```js
// "A tree from the game's own trees at every corner" (a different one each, the same every time).
for (const [i, [x, z]] of [[-10, -10], [10, -10], [-10, 10], [10, 10]].entries()) {
  const id = game.assets.pick('tree', { kind: 'model', seed: `corner${i}`, mix: true });
  if (id) game.model(id, { x, z, size: 6 }); else game.cone({ x, z, y: 2, radius: 1.5, height: 4, color: '#3d6b35' });
}
```

## Events and quests from scripts (`game.events`)

The rules of WORLD.md "Events" work from scripts too, and scripts can add their own actions and conditions. Rules
added by a script last while the game runs (the world's `config.events` is saved). Scripts run on every player's
machine; rules run on the host, so adding the same rule everywhere is right (the others forward their triggers).

| Call | What |
|---|---|
| `game.events.add(rule)` | Adds a rule (`{ id, when, if, do }`, or a list); returns its id. Same id replaces |
| `game.events.remove(id)`, `enable(id, on)`, `disable(id)`, `get(id)`, `list()` | Manage rules (`get` has `count`: how often it ran) |
| `game.events.run(id, data)` | Runs a rule now (its `if` too). Host only |
| `game.events.do([actions])` | Runs actions now, on the host (on a guest it does nothing: the host does it for everyone) |
| `game.events.value(name, playerId?)`, `set(name, value, { per, player })`, `change(name, by, { per })`, `declare(name, value, per)` | Variables (the host's; everyone sees them). `game.events.vars` is the globals |
| `game.events.standings(name)` | A per-player variable, best first: `[{ id, name, value }]` |
| `game.events.has(item, n)`, `items(playerId?)`, `give(item, n)` | The player's bag |
| `game.events.hour` (settable), `clock` (`{ hour, day, text }`), `dark(0.8, fade, seconds)` | The time of day; the dark |
| `game.events.disaster(name, opts)`, `stop(name or 'all')`, `disasters` | Starts a disaster on this screen (use the `disaster` action in rules to reach everyone) |
| `game.events.action(name, fn(a, ctx), { scope })` | A new action: `{ "confetti": true, ... }` then works in rules. `scope`: `'all'` (every screen, default), `'host'` (once), `'player'` (who triggered it). `ctx.who` is `{ id, name }`, `ctx.data` the trigger's data |
| `game.events.condition(name, fn(c, ctx))` | A new condition |
| `game.events.history` | The last rules that ran (`{ rule, t, who }`): for checking a game works |
| `game.events.banner(title, sub, seconds)`, `game.events.sound(name)` | The big title, a made sound |

**Quests:** `game.events.quests`

| Call | What |
|---|---|
| `add(quest)` | Adds a quest (WORLD.md "Quests") |
| `start(id)`, `complete(id)`, `fail(id)`, `advance(id)` | Moves it on |
| `state(id)` | `'locked'`, `'available'`, `'active'`, `'done'`, `'failed'` |
| `step(id)`, `progress(id)` | The current step's id; `{ stepId: { goalIndex: n } }` |
| `talk(id)`, `choose(id, i)` | Does a `talk` goal / picks a `choice` (what the giver's dialog does) |
| `open()`, `close()`, `talkTo(thing)`, `track(id)`, `list()` | The quest log, a giver's dialog, the tracked quest |

```js
// "Confetti" as an action anyone's rules can use.
game.events.action('confetti', (a, ctx) => { game.burst(game.player.position, { color: a.color || '#ff5ad1', count: 80, speed: 8 }); });
game.events.add({ id: 'party', when: { event: 'finish' }, do: [{ confetti: true }, { win: '{player} made it!' }] });

// A wave every 30 seconds, more each time.
game.events.declare('wave', 0);
game.events.add({ id: 'waves', when: { every: 30 }, do: [{ var: 'wave', add: 1 }, { banner: 'Wave {wave}' }, { spawn: 'zombie', count: 5, at: { at: 'player', radius: 25 } }] });

// A script's own event triggers rules (and quests' event goals).
game.input.onKey('h', () => game.emit('honk', { by: game.player }));
game.events.add({ when: { event: 'honk' }, cooldown: 2, do: [{ sound: 'horn' }] });

// A quest from code.
game.events.quests.add({ id: 'hello', title: 'Say hi', giver: 'Max', steps: [{ goal: { talk: 'Max' } }], reward: { score: 5 } });
```

For modules: `HawkesPlay.events` (`defineAction`, `defineCondition`, `defineDisaster(name, { seconds, start(h), step(h, dt) })`,
`things(ref)`, `posOf(ref)`, `inside(point, zone)`, `perform(action, ctx)`, `addRule`, `quests`). Any event a module
sends with `game.emit(name, data)` can trigger rules by name; put `mine: true` in the data when it's about this
player only (or `me`), so a guest's machine forwards it to the host.

## Playing together

Everyone in a game runs the scripts. Things in the world, players and what they hold are kept in step for you; for
anything else your scripts decide, tell the others:

| Call | What |
|---|---|
| `game.net.send(name, data)` | Tells everyone else. `game.net.everyone(name, data)` includes you. |
| `game.net.on(name, (data, from) => {})` | Hears it. Also `'join'` and `'leave'` (`{ id, name }`). |
| `game.net.isHost` | True for one player (the one who simulates the world): spawn shared enemies here. |
| `game.net.players` | The others: `[{ id, name, color, entity }]`. `game.net.me`, `game.net.online`. |
| `game.rewind(seconds)` | Takes the world back (10 s at most; alone at once, together only the host's counts). `game.resetWorld()` starts it over as it loaded. |

What players see of each other is a smooth replay of the data, a little behind (about a tenth of a second plus the
network); their own player and whatever they drive or carry answer at once. Don't send positions yourself: things in
the world, players and vehicles are kept in step for you.

## Strategy (`game.rts`)

With `config.rts` (World: Strategy).

| | |
|---|---|
| `game.rts.units(team?)`, `buildings(team?)`, `resources()` | The living ones. |
| `game.rts.selected`, `select(things, add)` | The player's chosen units. |
| `game.rts.order(units, { type, to, target })` | `type` move, amove (fight on the way), attack, gather, stop, hold (on the host, or offline). |
| `game.rts.command(type, { to, target })` | As the player: the chosen units (online, through the host). |
| `game.rts.train(building, name)` | Trains a unit if its side can pay. |
| `game.rts.unlock(building, name)` | It learns to train another unit (a name, or `{ name, cost, time, spec }`): progression. |
| `game.rts.spawn(name, team, [x, y, z])` | A unit at once, free (a story's reinforcements). |
| `game.rts.resource(team?, kind?)`, `give(team, n, kind?)` | A side's resources. |

```js
// Progression: after the third wave the station learns to build frigates.
game.on('wave', (w) => {
  if (w.n === 3) { game.rts.unlock(game.entity('station'), 'frigate'); game.ui.message('Frigates unlocked', 3); }
});
```

## Cinematic camera, photos and clips (`game.cinematic`, `game.photo`)

Shots take the camera, hide the HUD, put up letterbox bars, then give everything back as it was. Each call returns a
promise that resolves when the shot ends: `{ done, skipped, clip }`. Shots are described in WORLD.md (`config.cinematics`).

```js
// The intro saved in config.cinematics, then the game begins.
game.onStart(async () => {
  await game.cinematic.play('intro');
  game.ui.message('Go!');
});

// An orbit round the volcano, 8 seconds, half way round, with a title.
game.cinematic.orbit('Volcano', { seconds: 8, degrees: 180, title: 'Mount Doom' });

// A camera car alongside the 4x4 you're driving (you keep driving), until stopped.
game.cinematic.dolly('vehicle', { side: 7, height: 2 });
game.after(20, () => game.cinematic.stop());

// Through keyframes: positions and what to look at (a point or a thing's name), with an ease.
game.cinematic.path([
  { pos: [0, 30, -80], look: 'Island' },
  { pos: [20, 12, -30], look: 'Island', ease: 'out' },
  { pos: [4, 3, -8], look: [0, 1, 0], hold: 1.5 },
], { seconds: 10, fade: 1, title: 'Fire Island' });

// Cows stampede from a rocket, in slow motion, filmed from a fixed place and recorded to a clip.
game.cinematic.record({ type: 'fixed', pos: [20, 4, -12], target: 'Rocket', seconds: 6, slowmo: 0.35 })
  .then(({ clip }) => { /* the clip opens for review: trim, download, or share a link */ });

// Bullet time for a moment (real seconds).
game.cinematic.slowmo(0.2, 1.5);   // e.g. where your script blows something up
```

| Call | What |
|---|---|
| `game.cinematic.play(nameOrShot, opts?)` | A shot from `config.cinematics` by name, or a shot object (WORLD.md); `opts` override its fields |
| `game.cinematic.path(keys, opts)`, `.orbit(target, opts)`, `.follow(target, opts)`, `.dolly(target, opts)`, `.fixed(pos, target, opts)`, `.sequence(shots, opts)` | The same, per kind of shot. Targets: a thing, its name, `'player'`, `'vehicle'`, or `[x, y, z]` |
| `game.cinematic.record(nameOrShot, opts?)` | Plays it and records what the player sees (letterbox, titles and fades included); the clip opens for review. Resolves with `{ clip: { blob, mime, seconds } }` |
| `game.cinematic.stop()`, `.skip()` | Ends it now (the camera comes back) |
| `game.cinematic.slowmo(scale, seconds?)` | The world at `scale` speed (0.25 = a quarter) for `seconds` of real time, or until `slowmo(1)`. The camera moves in real time |
| `game.cinematic.letterbox(on, aspect?)`, `.caption(title, subtitle?)`, `.fade(to, seconds)` | The pieces by hand: bars, words over the picture (`caption(null)` clears), fade to black (1) and back (0) |
| `game.cinematic.define(name, shot)`, `.list()`, `.get(name)` | Shots for this run (config.cinematics keeps them in the game) |
| `game.cinematic.playing`, `game.cinematic.current` | Whether a shot plays, and which |
| `game.photo.open()`, `.close()`, `.take()`, `.on` | Photo mode from a script (a "Photo" button in your menu) |
| `game.photo.clip()` | "Clip that": saves the last 15 seconds and opens it for review |

Players (when the world turns the keys on, `config.share.keys`): **P** photo mode (WASD, Q/E down and up, Shift faster, drag or mouse to look, wheel zooms, Z/X tilt, Space takes
it, H hides the panel, Esc goes back; phones get a stick and up/down buttons; gamepads: sticks, triggers, A takes, B
goes back). **F8** (or 🎬) clip that; right-click 🎬 turns the rolling clip off or on. Photos and clips are downloaded or
shared from a small window on hawkesgen.ai: sharing needs an account (free), downloading doesn't.

**Moments:** `game.on('moment', fn)` hears the chip's life: `{ state, kind, caption }` with `state` `chip` (shown), `tap`, `kept`
(the clip or photo is ready), `link` (`url`, `id`), `shared` / `copied`, `window` (the sandboxed game's share window opened),
`declined` (the guest put the sheet away), `failed`, `gone`. `kind` is `boom`, `catch`, `jump` or `dock`. In a script,
`HawkesPlay.share.moments.trigger('boom')` offers one by hand (the same rules apply) and `.hide()` puts it away.

## Looks: choosing how a thing looks (`game.looks`)

A thing has a function (its kind: a truck drives, a chair seats, a cow grazes) and a look (the model it wears). The
looks chooser changes only the look: the camera turns round the thing, the player picks a look from the community
library or the game's own models (shown on the thing at once), or has a new one made with Hawkes Gen. A truck wearing
a teapot still drives like a truck. It opens only while building, or on the player's own island; never in a published
game.

```js
game.looks.open(game.find('Blue truck'));     // the chooser, on that thing
game.on('looks', ({ state, id, look }) => {}); // 'open', 'done' (look: the asset it wears now), 'cancel'
```

Where a look lives: `shape.model` for the kinds that wear one (vehicles, creatures, furniture), `shape.asset` for a
model thing; a plain shape (a box) becomes a model thing of its size. The Overseer opens it with a `look` line.

## Juice: feel from scripts (`game.juice`, `game.ragdoll`)

Everything happens by itself (impacts, blasts, landings, crashes). Scripts can add more:

| call | what it does |
|---|---|
| `game.ragdoll(thing, { velocity: [x, y, z], spin })` | knocks a player or creature down limp (6-8 boxes on joints) and throws it; it gets back up in a moment. Null if it can't (no physics, budget, turned off). `game.juice.ragdoll.end(thing)` gets it up now, `.is(thing)`, `.active` |
| `game.juice.shake(0.6)` | shakes the camera (0-1; it adds up and fades). `game.camera.shake(strength, seconds)` goes through it too. `{ at: [x, y, z] }` makes it weaker farther away |
| `game.juice.hitStop(0.08)` | freezes the world for a moment (at most 0.15 s; playing alone) |
| `game.juice.slowmo(0.3, 1.5)` | slow motion: 30% speed for 1.5 real seconds, easing back (playing alone) |
| `game.juice.flash(0.7)` | a flash over the screen (reduce motion dims it) |
| `game.juice.rumble(strong, weak, ms)` | controller rumble (a phone vibrates) |
| `game.juice.recoil(0.4)` | kicks the view up (guns do this by themselves) |
| `game.juice.sound.play({ material: 'metal', size: 'crash', loudness: 0.8, at: [x, y, z] })` | an impact sound (the game's sound for "metal crash" if it has one) |
| `game.juice.sound.footstep('snow', { at })`, `game.juice.sound.whoosh(at, speed)` | a step, a whoosh |
| `game.juice.fx.burst('sparks', [x, y, z], { count: 30 })` | particles: `dust`, `sparks`, `splinters`, `chips`, `glass`, `spray` (`color`, `speed`, `dir`) |
| `game.juice.fx.decal('scorch', [x, y, z], { size: 3 })` | a mark on the ground: `scorch`, `tire`, `print` (`yaw`, `normal`, `life` seconds) |
| `game.juice.level`, `game.juice.deviceLevel = 'reduced'`, `game.juice.reduceMotion = true` | the juice level now; this device's choice (kept in the browser) |
| `game.juice.openSettings()` | the Feel menu: level and reduce motion for this device |

Events: `game.on('ragdoll', ({ entity, on }) => ...)` when something goes limp (`on: true`) and gets back up (`on: false`).
`game.juice.feel.on('land', ({ speed, surface }) => ...)` when you land (m/s).

```js
// A trampoline that launches you limp:
game.find('Trampoline').on('hit', (who) => { if (who === game.player) game.ragdoll(who, { velocity: [0, 18, 0] }); });
// A boss stomp: shake, a beat of freeze, dust.
game.on('stomp', (d) => { game.juice.shake(0.8); game.juice.hitStop(0.06); game.juice.fx.burst('dust', d.at, { count: 60 }); });
```

## Progress (`H.progress`, `game.on('progress')`)

```js
// Events: every earn, every level, every unlock, and the Overseer's moment.
game.on('progress', ({ state, id, pts, total, level, key, text }) => { /* state: 'earn' | 'level' | 'unlock' | 'moment' */ });
// A game's own deeds can earn by the table (ids in progress-core.js EARN):
HawkesPlay.progress.earn('quest');        // a quest done
HawkesPlay.progress.did('help');          // a friend helped: its points, and the first time the first-time points
HawkesPlay.progress.state;                // { pts, level, bar: { into, span, frac }, name: { one, many }, firsts, meters }
HawkesPlay.progress.has('tool:grenade');  // is it theirs yet
game.emit('helped', {});                  // the one event a script can send for "helped a friend"
```

Adding an unlock is one line in `progress-core.js` `UNLOCKS`: `[level, kind, id, label, line?]` with kind `acc` (an accessory id
from `presence-core.js`), `col` (a hex color without #) or `tool` (an `H.tools` name). The line is the Overseer's own words when
given; otherwise its kind's line is used. Points are earned only after a world has settled (3 s) and the Overseer's intro and
mirror are over.

The level pill moves in the frame of an award: it counts up to the new total over about half a second and the bar flashes.
A level-up's Overseer line is said in the same call unless another line is being spoken; then it follows that line
within one frame (`H.progress.waiting` is how many wait). `H.progress.hud` gives `{ shown, target, flashes }`.

## The spawn wheel from scripts (`game.spawnWheel`)

```js
game.spawnWheel.open();                    // opens the wheel (the sheet on phones), if the player may spawn now
game.spawnWheel.close();
game.spawnWheel.allowed();                 // building, or config.spawn lets players spawn while playing
await game.spawnWheel.spawn('cow', 10);    // as the wheel would: by word or id ("4x4", "species:cow", "tool:lasso")
game.spawnWheel.search('boat');            // [{ id, name, type, category }], best first
game.on('spawn', ({ item, count, ids }) => { /* someone used the wheel: item { id, name, type, category } */ });
game.on('spawn-wheel', ({ state, as }) => { /* the player opened it (state 'open'; as 'wheel', or 'sheet' on phones) */ });
```

- While playing, `spawn` makes runtime things (not saved; shared online through Mayhem's spawning) and only when the
  game allows it (`config.spawn`). While building it makes saved scene things (one undo).
- For spawning by script without the wheel's rules, use `game.spawnMany(what, count, opts)` (Mayhem).

Modules: `H.spawn` holds the model (`types()`, `categories(type)`, `items(type, category)`, `load(...)`, `search(q)`,
`place(spec, { count, floats })`, `spawn(item, { count })`, `recent`, `favorites`). A kind registered with
`editor.hotbar` entries or a `codex.spec` shows up in the wheel by itself (its `codex.group` picks the type);
`editor: { spawn: false }` keeps a kind out of it.

## The daily wish (`H.wish`, `game.on('wish')`)

```js
game.on('wish', ({ state, id, n, of, pts, streak, gift, text }) => { /* 'offer' (spoken on arrival) | 'progress' | 'done' | 'day' (midnight UTC passed) | 'line' (an Overseer line) */ });
HawkesPlay.wish.state;   // { day: '2026-10-25', wish: 'fish-one', hud, n, of, done, streak, best, loaded }
HawkesPlay.wishCore.wishFor('2026-10-25');   // the wish of a day: a pure function of the UTC date
```

A wish is one line of the table in `wish-core.js`: `[id, kind, n, hud, say, pts, extra?]`. `kind` says what counts (fish, different
fish, things blown up, blown up from a vehicle, meters, races, a race under a time, dock, quest, lore, neighbours, things made,
asks, help, space, standing still, swimming, a photo of the planet, at dawn, a friend at the hut); `say` is the Overseer's words,
spoken once; `extra` is `{ needs, course, under, gift }`. The table has 40, each day takes the next of a shuffle seeded by the
40-day cycle (a wish does not come twice in a cycle, nor on two days running). The streak: a wish done on the day after the last
one adds one; any other gap starts at one; the HUD shows a streak only while it can still be carried on. The record (`last`,
`st`, `best`, `day`, `n`, `set`, `said`) is `H.memory.wish`. The game's own events move a wish on (`catch`, `explosion`,
`race` finish, `docked`, `quest`, `lore-found`, `met`, `spawned`, `asked`, `helped`, `swim`, and `photo`, which the photo
mode emits with the sky's `space` and `hour`).

## Helpers

`game.random(min, max)`, `game.randomInt(min, max)`, `game.pick(array)`, `game.clamp(v, min, max)`,
`game.lerp(a, b, t)`, `game.asset(name)` (an asset's URL), `game.hasAsset(name)`, `game.log(...)` (prints in the
editor). `game.scene`, `game.engine` and `game.BABYLON` are Babylon itself, for anything else.

## A whole game (rules on top of a world)

```js
// scripts/main.js: the world has coins (behaviors: collectible) and a door with the "opens" behavior.
game.state.score = 0;
game.ui.text('goal', 'Collect 5 coins to open the gate', { x: 'center', y: 'top' });
game.onUpdate(() => {
  game.ui.bar('hp', game.player.health / 100, { label: 'Health', y: 'bottom' });
  if (game.state.score >= 5 && !game.state.open) {
    game.state.open = true;
    game.entity('gate').destroy();
    game.ui.message('The gate is open!');
  }
});
```

## Controls (`game.controls`)

Every game has a **Controls** button (on a computer `?` opens it too): the keys that work right now (moving, the camera,
E, Q spawns, K changes how the thing you point at looks, L your own look, I the bag, B emotes, Enter talks, Esc twice
pauses), plus what the moment adds (driving, a tool in hand, building). On a phone it is a sheet of big buttons that do
those things (Spawn, Change look, My look, Bag, Emotes, Journal, Quests, Talk, Camera, Stuck?).

| | |
|---|---|
| `game.controls.add({ group, keys, what, when, act, icon, tap })` | Your game's own line: `keys` as shown ("G"), `what` it does, `when()` (shown only then), and for phones `tap` (the button's word), `icon` and `act()` (what the button does). Returns a remover. |
| `game.controls.open()`, `close()`, `list()` | Open it, close it, what it shows now. |
| `game.controls.changeLook()` | As K: the look chooser on what the player points at (or the vehicle they're in), where looks can be chosen. |

```js
// A game with its own keys: they show in Controls, and on phones as a button.
game.controls.add({ group: 'This game', keys: 'G', what: 'throw a snowball', tap: 'Throw', icon: '❄️', act: () => throwSnowball() });
```
