# Мир — `$.world`

Мир владеет реестром узлов и синхронизацией с физикой: раз в кадр позиции тел
из C перекладываются в узлы, а удалённые тела убираются. Здесь же поиск,
лучи, границы и порядок отрисовки.

```js
$.world.gravity(0, 900).color('#1a1d24').bounds(0, 0, 4000, 800);
const hit = $.world.raycast({ x: 0, y: 0 }, { x: 300, y: 0 }, { mask: LAYER_SOLID });
$.world.spawn('enemy', 800, 200, { body: 'dynamic' });
$.world.sort((a, b) => a.y - b.y);          // порядок отрисовки по глубине
```

---

## 1. Мир и физика

| Вызов | Смысл |
|---|---|
| `gravity(x, y?)` / `color(c)` / `bounds(x, y, w, h)` | параметры мира |
| `clearBounds()` | убрать стены, поставленные `bounds()` |
| `pause()` / `resume()` / `freeze()` / `thaw()` / `isPaused()` | остановка физики |
| `timeScale(value?)` / `getTimeScale()` | скорость мира |
| `spawn(tag, x, y, attrs?)` / `all()` / `count(sel?)` | создание и перечисление |

**`all()` отдаёт ВСЕ узлы контекста, включая интерфейс.** Для «убрать мир и
начать заново» он не годится: `$.world.all().remove()` сносит и HUD — в срезе
игры после рестарта пропадали полоса здоровья и счётчик патронов (`$('#hud-hp')`
давал 0 узлов). Держите список узлов забега руками и удаляйте только его.
Ещё одно: `remove()` есть у обёртки — у сырого узла из `.nodes` его нет, и
`for (const n of $.world.all().nodes) n.remove()` падает молча (ошибку видно
только в stderr движка).
| `sync(dt)` | перенести трансформы из физики (движок зовёт сам) |
| `bodyAt(x, y, opts?)` / `bodiesIn(x, y, w, h, opts?)` | поиск тел |
| `contacts()` | события контакта за кадр |
| `bullet(what, on?)` / `isBullet(what)` | CCD для узла (пуля не проскакивает стену) |

## 1.1. Границы мира

```js
$.world.bounds(0, 0, 4000, 2000);      // четыре невидимые стены по краям
$.world.bounds(0, 0, 640, 360);        // ПРЕЖНИЕ стены убираются, ставятся новые
$.world.clearBounds();                 // убрать стены совсем
```

`bounds()` **заменяет** прежние стены, а не добавляет новые. Это важно и было
дефектом: каждый вызов добавлял ещё четыре, а старые оставались, и второе
`bounds()` (смена уровня, другой тест) оставляло невидимые стены от первого —
тела упирались в воздух. Нашлось на перетаскивании: тело замирало на `x = 628`
при полосе мира `0..4000` — это была стена предыдущей сцены.

Стены — обычные статические узлы с классом `world-bound` (толщина 64, `opts
.thickness`), поэтому их видно в `$('.world-bound')` и в `$.world.count()` они
не считаются. `bounds(..., { solid: false })` оставляет границы только как
числа в `state.bounds` (без физики).

## 2. Лучи и формы

| Вызов | Смысл |
|---|---|
| `raycast(from, to, opts?)` | первый луч |
| `raycastAll(from, to, opts?)` | все пересечения |
| `castShape(from, to, opts?)` | фигурный свип (луч «толщиной») |
| `lineOfSight(from, to, opts?)` | есть ли прямая видимость |
| `particlesAt(x, y)` / `particlesIn(x, y, w, h)` | частицы под точкой и в прямоугольнике |

Порядок аргументов у свипа — **сначала путь, потом форма**: `from` и `to` — точка,
узел, обёртка или селектор, а форма задаётся в `opts` — `w`/`h` (прямоугольник),
`radius` (круг), `capsule: [радиус, половина отрезка]` или явно (`shape` +
`halfW`/`halfH`/`radius`); `angle` поворачивает форму, `mask` и `ignore` работают
как у луча. Возвращает `{ hit, point, normal, distance, fraction, body, node, self }`
или `null`; `fraction = 0` значит «объём уже перекрывается с препятствием».

```js
// Пролезет ли ящик 48×48 в проём: у луча и у объёма ответы разные.
const hit = $.world.castShape({ x: 0, y: 0 }, { x: 200, y: 0 }, { w: 48, h: 48, mask: 0x1 });
```

### 2.3. Зоны

Зона — **вторая форма** на теле узла («голова», «ноги», «щит»): она не заменяет
основной хитбокс, а добавляется к нему, поэтому в контакте видно, **куда** попали
(`contactBetween().tagA`/`tagB`).

| Вызов | Смысл |
|---|---|
| `$('#hero').zone({ type, w, h, x, y, tag })` | добавить форму-зону → индекс (0 — основная) |
| `$('#hero').zoneCount()` | сколько форм у тела |
| `$.world.zone(what, opts)` | то же из скрипта → индекс формы или `-1` |
| `$.world.zoneTag(what, index)` | имя зоны по индексу формы (`null`, если имени нет) |
| `$.world.zoneCount(what)` | сколько форм у тела |
| `$.world.zonesTouching(what, other)` | имена **всех** зон, которых касается другой узел |

`what` — узел, обёртка или селектор (как у лучей). `opts`: `type`
(`'box'`/`'circle'`/`'capsule'`/`'polygon'`), `w`/`h` (по умолчанию
32×32), `radius`, `x`/`y` — **смещение зоны от центра тела** (голова выше, ноги
ниже), `tag` — имя зоны для игры, плюс `sensor`, `contacts`, `density`, `friction`,
`restitution`, `layer`, `mask`, `group`. Предел — 8 форм на тело
(`R2D_MAX_SHAPES_PER_BODY`), лишние не добавляются.

```js
$('#hero').zone({ type: 'box', w: 40, h: 24, y: -34, tag: 'head' });
$('#hero').zoneCount();                          // 2: основная форма + голова

const hit = $.world.contactBetween('#hero', '#spike');
if (hit && hit.tagA === 'head') headshot();
// Зоны перекрываются, и contactBetween отдаёт первую: все разом — через zonesTouching.
if ($.world.zonesTouching('#hero', '#spike').includes('head')) headshot();
```

### 2.4. Видимость: `lineOfSight` и `ignore`

```js
// «Видит ли враг игрока»: расстояние + свободный путь.
const sees = dist < 600
    && $.world.lineOfSight(enemy.pos(), hero.pos(), { ignore: [hero, enemyNode] });
```

**Исключайте ОБА тела.** Луч пускается из собственного тела и без `ignore`
упирается в него же: `lineOfSight` вернёт `false` **всегда**, и враг окажется
«слепым» у вас на глазах. Второе тело — цель: если её не исключить, луч тоже
попадёт в неё и «видимости не будет».

На это легко потратить час: снаружи (в отладке) тот же вызов без `ignore`
может вернуть `true` — смотря где стояли тела. Если враг «не видит», первым
делом смотрите `$.world.raycast` (он вернёт, во **что** попал луч: `hit.node.tag`).

`opts.ignore` принимает узел, обёртку, селектор или массив из них.

**`lineOfSight` — это `raycast(...) === null`.** Препятствием считается любое
тело: тайлы, ящик, который игрок толкает перед собой. Это правильное поведение,
но неожиданное, когда «враг перестал видеть» из-за собственного груза.

## 3. Соединения (joints)

| Вызов | Смысл |
|---|---|
| `joint(a, b, opts)` | создать соединение; возвращает id |
| `destroyJoint(id)` / `joint(id)` / `jointAlive(id)` / `jointCount()` | управление |

Виды суставов (`opts.type`):

| Тип | Смысл |
|---|---|
| `revolute` (по умолчанию) | шарнир: вращение вокруг точки; `limit`, `motor` |
| `distance` | стержень фиксированной длины; `length`, границы длины |
| `weld` | сварка: тела держатся жёстко |
| `prismatic` | направляющая: едет по оси и не вращается; `axis`, `limit`, `motor` |
| `wheel` | колесо/подвеска: крутится вокруг оси и ходит вдоль неё; `axis`, `motor` |
| `filter` | запрет столкновений конкретной пары тел (надёжнее масок) |

Перетаскивание — **не сустав**: `$.world.tug(узел, x, y, opts)` тянет тело
скоростью (пружинный контроллер), поэтому столкновение может перебить тягу и
никого не телепортирует.

```js
$.world.tug('#crate', $.input.mouseWorld().x, $.input.mouseWorld().y);
```

```js
// Направляющая вдоль X: тело ездит по рельсу и не вращается.
$.world.joint('#rail', '#slider', { type: 'prismatic', axis: [1, 0],
                                    a: [300, 200], b: [300, 200] });
```

Ось (`axis`) задаётся в мировых координатах и нужна только `prismatic` и `wheel`;
остальным видам она безразлична. Незнакомый тип не подменяется молча: движок
пишет предупреждение и берёт `revolute`.

### Чего нет и почему

* **`pulley` и `gear` сделать нельзя**: в Box2D v3 этих суставов **нет** —
  в `b2JointType` остались distance, filter, motor, mouse, prismatic, revolute,
  weld, wheel. Обещание про блок и зубчатую передачу было устаревшим: оно
  пришло из Box2D v2. Вместо них появились `filter` и `motor` (второго у нас
  пока нет).
* **у `mouse` не проверена тяга**: сустав создаётся, цель читается и
  переставляется, но тело к цели **не поехало** — ни в тесте, ни в отдельной
  пробе. Параметры проверены: тело A статическое, B динамическое, масса 0.56 кг,
  сила 500000 Н, цель задана. Причину найти не удалось, поэтому обещать
  «перетаскивание мышью» нельзя. Для перетаскивания пока используйте
  `castShape`/`bodyAt` и `applyImpulse`.

## 3.1. Перетаскивание

```js
$.world.tug('#crate', mouseWorld.x, mouseWorld.y);          // тянем к точке
$.world.tug('#crate', x, y, { speed: 400, snap: 4 });       // медленнее и мягче
```

Скорость задаётся **постоянной** по направлению к цели (`speed`, по умолчанию
900; пол `minSpeed`, чтобы тяга не затухала у цели — иначе тело застревает, не
доехав). Позицию не телепортируем: тело физическое, и **столкновение тягу
перебивает** — если путь закрыт, тело останавливается у препятствия.

Спящее тело тянется так же: сеттер скорости в движке **будит** тело (Box2D
засыпает неподвижные, а у спящего `SetLinearVelocity` не оживляет тело — это
был настоящий дефект, из-за которого «`setVelocity` перестал работать»).

## 4. Порядок и слои

| Вызов | Смысл |
|---|---|
| `sort(fn)` / `sortWith(...)` | порядок отрисовки |
| `background(path, opts?)` / `getBackground()` / `clearBackground()` | фон-картинка (`parallax`, `scale`, `y`, `color`) |
| `query(x, y, r?)` | узлы, чьи границы накрывают точку (или центр в радиусе `r`) |

Цвет фона задаётся не `background()`, а `$.world.color(c)` (цвет очистки кадра).
`query()` — **пространственный** запрос по координатам, а не поиск по селектору:
селектор — это `$(...)` или `$.find(sel)`, счётчик — `$.count(sel)` /
`$.world.count(sel)`.

`$.world.bsp` — порядок отрезков «от дальних к ближним» (см. [bsp.md](highlevel/bsp)),
`$.world.ignoreBodies` — тела, которых не касается мир.

## 5. Ограничения

* **CCD есть, но поведенческой проверки нет**: `$.world.bullet('#p', true)`,
  `.bullet(on)` у узла и тег `<bullet>` включают непрерывную проверку в Box2D
  (флаг доходит до физики — это проверено), однако ВОСПРОИЗВЕСТИ разницу в
  поведении на стенде не удалось: обе пули останавливались у стены и с CCD, и
  без. Нужен отдельный тест с более тонкой стеной и подшагом мельче 1/60;
* **события контакта копятся за все подшаги кадра**, до чтения JS;
  список остаётся ограничен native лимитом буфера;
* **BSP не упорядочивает спрайты**: только отрезки; спрайты сортируются по
  расстоянию (`sort`);
* **сетка навигации отдельно**: `$.nav` строит свой граф, `$.world` его не знает;
* **ось сустава — в мировых координатах**, не в локальных телу;
* **mouse-сустава нет осознанно**: он создаётся, но тело к цели не тянет
  (проверено пробами при силах 10…500000 и при выключенном сне). Мёртвый код
  убран, вместо него `$.world.tug`. У `tug` тяга тоже не доводит тело до цели —
  оно доезжает примерно на 270 px из 400 и замирает; причина не выяснена,
  в тесте проверяется ровно то, что работает.
