# Пул объектов — `$.pool`

Пул переиспользует узлы: пуля, частица-объект или враг создаются один раз, а
дальше по кругу выдаются и возвращаются. Это убирает мусор от частых
`$('<bullet>')` и, что важнее, не плодит тела Box2D: освобождённый узел теряет
тело и уходит из мира, но сам объект остаётся в памяти.

```js
$.ready(() => {
    $.pool.create({
        name: 'bullets',
        tag: 'bullet',
        max: 64,
        speed: 600,
        color: '#ffd34d',
        onRelease: (node) => { node.attrs.hit = false; },
    });

    // выстрел
    $.pool.spawn('bullets', { x: 100, y: 200 })
        .velocity(1, 0)
        .on('collision', (e) => e.self.release());
});
```

---

## 1. Жизненный цикл узла

| Состояние | Что с узлом |
|---|---|
| создан (`create`/`initial`) | лежит в списке свободных, невидим, в `ctx.nodes` его нет |
| выдан (`spawn`) | попадает в `ctx.nodes`, `visible = true`, получает тело (своё же, включённое обратно) |
| возвращён (`release`) | тело **выключено** (`engine.setBodyEnabled(body, false)`) и живёт до следующего `spawn`, `visible = false`, убран из `ctx.nodes` и селекторов |
| уничтожен (`clear`) | `node.destroy()` — узел нельзя выдать снова |

Пока узел свободен, его не видит отрисовка, не находит `$('#id')` и не считает
`$.world.count()`. На следующем `spawn` узел возвращается к состоянию шаблона
(координаты, цвет, размер, классы), поверх накладываются `opts`.

---

## 2. Создание пула

```js
const bullets = $.pool.create({
    name: 'bullets',     // обязательно: имя для spawn/get
    tag: 'bullet',       // тег узла, по умолчанию 'rect'
    max: 64,             // жёсткий потолок роста, по умолчанию 128
    initial: 16,         // предсоздать узлы заранее (алиас prewarm)
    parent: $.world,     // необязательный родитель; $.world = корень мира
    speed: 600,          // все прочие поля — атрибуты узла (шаблон)
    onAcquire: (node, opts) => {},
    onRelease: (node) => {},
});
```

| Поле | Тип | По умолчанию | Смысл |
|---|---|---|---|
| `name` | строка | — | имя пула; без него `create()` вернёт `null` |
| `tag` | строка | `'rect'` | тег создаваемых узлов; должен существовать в `TAGS` |
| `max` | целое | `128` | потолок числа созданных узлов (не выданных) |
| `initial` / `prewarm` | целое | `0` | сколько узлов создать сразу, обрезается по `max` |
| `parent` | узел/`$.world` | мир | куда прикреплять выданные узлы |
| `onAcquire` | функция | — | `(node, opts)` после выдачи узла |
| `onRelease` | функция | — | `(node)` перед возвратом, узел ещё в мире |
| остальные поля | — | — | атрибуты узла: `w`, `h`, `color`, `speed`, `class`… |

`create()` возвращает **дескриптор пула** (см. ниже). Повторный `create()` с
тем же `name` не перезаписывает пул, а возвращает существующий и пишет
предупреждение в журнал.

---

## 3. Пространство имён `$.pool`

| Функция | Назначение |
|---|---|
| `$.pool.create(spec)` | зарегистрировать пул; вернуть дескриптор или `null` |
| `$.pool.get(name)` | дескриптор пула или `null` |
| `$.pool.has(name)` | есть ли такой пул |
| `$.pool.spawn(name, opts)` | выдать узел: обёртка `$` (пустая, если места нет) |
| `$.pool.release(node)` | вернуть узел или обёртку; `false`, если узел не из пула |
| `$.pool.releaseAll(name?)` | вернуть всех; без имени — по всем пулам; число возвратов |
| `$.pool.stats()` | сводка по всем пулам (см. §5) |
| `$.pool.clear(name?)` | уничтожить пул(ы) вместе с узлами; без имени — все |

`spawn` возвращает обычную обёртку, поэтому работают цепочки:

```js
$.pool.spawn('bullets', { x, y }).color('#ff0').velocity(vx, vy);
```

Если `max` исчерпан, `spawn` не создаёт узел и возвращает **пустую обёртку** —
цепочка не падает, а `stats().skipped` растёт. Освободите узел, чтобы место
появилось снова.

---

## 4. Дескриптор пула и метод узла

```js
const p = $.pool.get('bullets');
p.spawn({ x: 0, y: 0 });   // то же, что $.pool.spawn('bullets', …)
p.release(node);
p.releaseAll();
p.clear();                 // уничтожить узлы, пул остаётся зарегистрированным
p.nodes();                 // обёртка со всеми выданными узлами
p.stats();

p.created;  // всего создано узлов
p.active;   // выдано сейчас
p.free;     // свободно
p.max;      // потолок
p.spawned;  // успешных выдач за всё время
p.released; // возвратов в пул
p.skipped;  // spawn не нашёл места (лимит max)
```

У выданной обёртки есть метод `.release()` — вернуть узел в свой пул:

```js
$('#bullet').on('collision', (e) => e.self.release());
```

Вызов на узле, который не выдавал `$.pool.spawn()`, пишет подсказку в журнал и
ничего не делает.

---

## 5. Статистика

`$.pool.stats()` возвращает общую сводку и разбивку по именам:

```js
{
    pools: 2, created: 80, active: 12, free: 68,
    spawned: 340, released: 328, skipped: 0,
    names: ['bullets', 'sparks'],
    by_name: {
        bullets: { name, tag, max, created, active, free, spawned, released, skipped },
        sparks:  { … },
    },
}
```

Те же поля (кроме `by_name`) отдаёт `p.stats()` для одного пула.

---

## 6. Счётчики подсистем в `$.debug`

`$.pool.tickPool()` обновляет снимок счётчиков на каждом кадре; читают его
через `$.debug`:

```js
$.debug.counters();
// { nodes, world_nodes, ui_nodes, bodies, particles, tweens, zones,
//   pools, pool_created, pool_active, pool_free }

$.debug.stats().counters;   // тот же объект внутри общей сводки кадра
```

| Поле | Что считает |
|---|---|
| `nodes` | все узлы в `ctx.nodes` |
| `world_nodes` | игровые узлы без интерфейса и стен `bounds()` |
| `ui_nodes` | узлы интерфейса |
| `bodies` | узлы с живым телом Box2D |
| `particles` | живые частицы всех `<particles>` |
| `tweens` | активные твины |
| `zones` | узлы `<trigger>` и `<area>` |
| `pools`, `pool_created`, `pool_active`, `pool_free` | состояние пулов |

`counters()` считает значения заново при каждом вызове, поэтому верен даже до
первого кадра.

---

## 7. Ограничения и правила

* Пул — общий на процесс: смена сцены узлы пула не уничтожает. Чистите явно
  (`$.pool.clear()`), если пул больше не нужен.
* `onAcquire`/`onRelease` получают **сырой узел** (не обёртку) — так в кадре не
  создаётся лишних объектов. Для цепочек оберните его: `$(node).at(x, y)`.
* Подписки и иерархия (`on`, `parent`) применяются один раз при создании узла;
  `opts` при повторной выдаче их не переподписывают. Для разовых обработчиков
  используйте `onAcquire`.
* Между выдачами состояние узла сбрасывается к шаблону, поэтому всё, что должно
  переживать `release`, храните в `node.attrs` внутри `onRelease`/`onAcquire`
  или во внешнем объекте.
* Узел, уничтоженный игрой через `.remove()`, в пул не возвращается —
  следующий `spawn` создаст вместо него новый (если есть место по `max`).
* Пул не заменяет `$.particles`: частицы эмиттера живут своим внутренним пулом
  и в `$.pool` не нуждаются.

---

## 8. Тесты

```bash
build/_deps/quickjs-build/qjs tests/js/pool_test.mjs
```

Юнит-тест проверяет переиспользование объекта узла, сброс состояния между
выдачами, лимит роста и предсоздание, `release`/`releaseAll`/`clear`, работу
с телами Box2D, обработчики `onAcquire`/`onRelease` и счётчики `$.debug`.

Интеграционный прогон в движке — `tests/agent/highlevel_pool_test.py`
(фикстура `tests/fixtures/pool/`); его запускает интегратор после сборки.

