# `$.prefab` — prefab, наследование сцен и сериализация узлов

Подсистема сохраняет любой узел со всем поддеревом в обычные
JSON-совместимые данные и создаёт по ним новые узлы. Это аналог
`PackedScene`/`instantiate()` и inherited scene из Godot 4, закрывающий
авторинг из данных: `$.scene` умеет менять сцены, но не описывать
дерево узлов.

```js
$.ready(() => {
    // Прототип: враг с ребёнком-полоской здоровья.
    const proto = $('<enemy>', { class: 'goblin' })
        .at(0, 0).size(28, 40).appendTo($.world);
    $('<ui.bar>').at(0, -26).size(28, 4).appendTo(proto);

    // Сохранили под именем и убрали прототип.
    $.prefab.register('goblin', $.prefab.save(proto));
    proto.remove();

    // Наследник: те же узлы, но другие свойства и свой шлем.
    $.prefab.register('goblin-boss', {
        extend: 'goblin',
        overrides: { hp: 300, size: [48, 64], speed: 60 },
        add: [{ tag: 'rect', class: 'helmet', w: 40, h: 12, y: -36 }],
    });

    // Инстанцируем пачку.
    $.prefab.instantiate('goblin', { count: 5, x: 400, y: 200, parent: $.world })
        .addClass('wave-1');
    $.prefab.instantiate('goblin-boss', { id: 'boss', x: 700, y: 200, parent: $.world });
});
```

Три правила, из которых растёт весь модуль:

1. **Только данные.** В результат `save()` не попадают ни функции, ни ссылки
   на живые объекты: `sanitize()` выбрасывает функции, разрывает циклы,
   превращает `Set`/`Map` в массивы и объекты.
2. **Описания, а не id.** Числовой id тела Box2D и спрайта после загрузки
   будет другим, поэтому `body` хранится строкой (`'dynamic'`/`'static'`/
   `'kinematic'`/`false`), а `sprite` — путём или спецификацией листа.
3. **Идемпотентность.** `save → load → save` даёт побайтово одинаковый JSON:
   `nodeToData()` всегда пишет один и тот же набор полей в одном порядке.

---

## `$.prefab`

### Сохранение и загрузка

| Метод | Возвращает | Смысл |
|---|---|---|
| `$.prefab.save(nodeOrSelector)` | объект или массив | узел со всем поддеревом → данные |
| `$.prefab.load(data, parent?)` | обёртка | данные → новые узлы |
| `$.prefab.clone(nodeOrSelector)` | обёртка | глубокая копия поддерева **рядом** с оригиналом |

`nodeOrSelector` — узел, обёртка или CSS-селектор. `parent` — узел, обёртка,
селектор; по умолчанию мир. `save()` от одного узла возвращает объект, от
нескольких (или от массива) — массив; `load()`/`instantiate()` принимают и то
и другое.

```js
const g = $.prefab.instantiate('goblin', { x: 400, y: 200 });
const data = g.toData();                 // снимок уже живого узла
$.prefab.load(data, $('#cave'));         // копия внутрь другого узла
$.prefab.clone('#boss');                 // копия рядом с '#boss'
```

### Именованные prefab

| Метод | Смысл |
|---|---|
| `$.prefab.register(name, spec)` | зарегистрировать данные или функцию `(opts) => data` |
| `$.prefab.instantiate(nameOrData, opts)` | создать узлы по имени или данным |
| `$.prefab.get(name)` | разрешённые данные (с учётом `extend`) или `null` |
| `$.prefab.has(name)` / `list()` / `remove(name)` / `clear()` | реестр |

`opts` у `instantiate`:

| Поле | Смысл |
|---|---|
| `x`, `y` | позиция первого корня |
| `parent` | родитель (по умолчанию мир) |
| `id` | id первого корня; если занят — получит суффикс |
| `class` | классы первого корня (дополняются) |
| `overrides` | переопределения свойств (см. ниже) |
| `count` | сколько копий создать (по умолчанию `1`) |

```js
$.prefab.instantiate('goblin', { count: 10, x: 100, y: 0, class: 'wave' });
// → обёртка из 10 корней, у каждого свой uid, id уникальны
```

### Наследование (`extend`)

Ребёнок наследует дерево родителя и переопределяет свойства — это
inherited scene:

```js
$.prefab.register('base', data);
$.prefab.register('child', {
    extend: 'base',
    overrides: { hp: 50, size: [40, 60] },   // свойства корня
    add: [{ tag: 'rect', class: 'cape' }],   // новые дети корня
});
```

`overrides` — короткие формы и обычные поля:

| Ключ | Что делает |
|---|---|
| `size: [w, h]` | задаёт `w`/`h` |
| `pos: [x, y]` | задаёт `x`/`y` |
| `class: 'a b'` | **дополняет** классы |
| `tags: [...]` | **дополняет** теги |
| `hp`, `maxHp`, `color`, `alpha`, `speed`, … | переопределяют поле или `attrs` |

Поля, которых нет в формате данных (например `speed`), попадают в `attrs` и
применяются через обычный `.attr()` (это метод обёртки: `$.attr()` не
существует). `extend` разрешается рекурсивно; цикл
не роняет игру — в журнал уходит предупреждение, а `instantiate` вернёт
пустую обёртку.

> Переопределения действуют на **корень** prefab. Чтобы настроить конкретного
> ребёнка, добавьте его через `add` или правьте данные вручную.

### Файлы и сцены

| Метод | Смысл |
|---|---|
| `$.prefab.saveTo(name, nodeOrSelector)` | сохранить узел в `$.store` (`prefab:<name>`) и на диск |
| `$.prefab.loadFrom(name, opts)` | взять prefab из `$.store` (или `prefabs/<name>.json`) |
| `$.prefab.saveScene(name)` | сохранить весь мир (`scene:<name>`) |
| `$.prefab.loadScene(name, opts)` | восстановить мир; `opts: { clear: true }` — сначала очистить |
| `$.prefab.toJSON(data)` / `$.prefab.fromJSON(text)` | строка JSON и обратно |

```js
$.store.file('build/level1.json');       // куда писать
$.prefab.saveTo('hero', '#hero');
$.prefab.loadFrom('hero', { x: 800, y: 200 });

$.prefab.saveScene('level1');
$('.junk').remove();
$.prefab.loadScene('level1', { clear: true });   // мир как был
```

`saveScene()` пишет корневые узлы (у кого нет родителя) вместе с поддеревом;
UI-узлы и `world-bound` тоже попадают в сцену — это буквально весь мир.

### Методы узла

| Метод | Возвращает | Смысл |
|---|---|---|
| `.toData()` | объект/массив | данные узла (у одного — объект, у многих — массив) |
| `.clone()` | обёртка | копия поддерева рядом с оригиналом |
| `.prefabClone()` | обёртка | то же; псевдоним на случай, если имя `clone` займут |
| `.savePrefab(name)` | `this` | `register(name, save(this))` |
| `.prefab()` | строка или `null` | имя prefab, из которого создан узел |

```js
$('#hero').savePrefab('hero');
$('#hero').prefab();        // 'hero'
$('#hero').clone();         // копия с уникальным id
```

---

## Формат данных

`save()` возвращает объект с фиксированным набором ключей:

| Поле | Тип | Смысл |
|---|---|---|
| `tag` | строка | тег узла |
| `id` | строка или `null` | id |
| `class` | строка | классы через пробел |
| `tags` | массив | дополнительные теги (`.addTag()`) |
| `data` | объект | `data_store` (`.data()`) |
| `x`, `y`, `w`, `h`, `angle`, `scaleX`, `scaleY` | число | геометрия |
| `alpha`, `visible`, `layer`, `depth` | число/булево | порядок и прозрачность |
| `color`, `hoverColor`, `textColor`, `fillColor` | `'#rrggbbaa'` или `null` | цвета |
| `text`, `fontSize`, `value`, `max`, `radius`, `intensity`, `r` | число/строка | текст и параметры тегов |
| `team`, `hp`, `maxHp` | число | здоровье и команда |
| `body` | `'dynamic' \| 'static' \| 'kinematic' \| false \| null` | тело: тип, «выключено», «как в теге» |
| `gravity`, `hitbox`, `collisionMask` | булево/массив/число | физика |
| `sprite` | строка, `{src,cols,rows,cw,ch}`, массив или `null` | картинка |
| `frame` | число | кадр листа |
| `attrs` | объект | прочие атрибуты (`.attr()`) |
| `children` | массив | дети, рекурсивно |

Числовой id тела и спрайта **не** сохраняется. Тело восстанавливается по
`body` + `attrs` (плотность, трение, `fixedRotation`), спрайт — по
`sprite`. Путь спрайта модуль запоминает в момент вызова
`.sprite('путь.png')` (хук на `Node.prototype.setSprite`), поэтому
`save → load → save` не теряет картинку.

---

## Чистые функции (для тестов и инструментов)

Экспортируются из `src/highlevel/prefab.js` и не требуют движка:

| Функция | Что делает |
|---|---|
| `nodeToData(node)` | узел → данные (без функций и живых ссылок) |
| `applyData(data, parent)` | данные → узел с детьми |
| `dataToSpec(value)` | глубокая JSON-безопасная копия данных |
| `sanitize(value, seen?)` | приводит значение к JSON-совместимому виду |
| `applyOverrides(data, overrides)` | применяет `overrides` к данным |
| `mergeSpec(base, child)` | наследование: база + `overrides` + `add` |
| `uniquifyIds(data, used?)` | делает уникальными id всего поддерева |
| `resolveParent(parent)` | узел/обёртка/селектор/мир → Node или `null` |
| `prefabOf(node)` | имя prefab узла или `null` |

Проверка без сборки движка:

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

---

## Ограничения и особенности

* **`save()` пишет только данные.** Функции в `attrs`/`data` молча
  выбрасываются: обработчики событий (`.on()`), `script`-функции и замыкания
  в prefab не переносятся — подпишитесь заново после `instantiate()`.
* **`extend` переопределяет только корень.** Дерево наследуется целиком,
  точечных переопределений конкретных детей (как `%Node` в Godot) нет — для
  этого есть `add` и `overrides`.
* **`clone`/`.clone()`** не занято ядром (`_CONTRACT.md` §5), но рядом всегда
  есть псевдоним `.prefabClone()`: если имя `clone` когда-нибудь займут,
  используйте его.
* **id не дублируются.** Если при `instantiate`/`load`/`clone` id уже занят
  живым узлом, копия получает суффикс (`hero` → `hero2`), а оригинал остаётся
  доступен по своему id. `opts.id` — это пожелание, а не гарантия точного id.
* **Тела Box2D пересоздаются** при загрузке: физическое состояние (скорость,
  угловая скорость, сон) не сохраняется, только позиция, угол и параметры.
  Транзитные визуальные поля (`tint`, `shake_timer`) в данные не входят.
* **`loadScene({ clear: true })`** убирает узлы, но не трогает таймеры, твины
  и настройки мира — этим занимается `$.scene`. Если нужен полный сброс,
  очищайте мир через смену сцены.
* **`loadFrom`/`loadScene`** ищут данные в памяти `$.store`, затем делают
  `$.store.load()` (читает `save.json`), затем пробуют
  `prefabs/<name>.json` / `scenes/<name>.json`. Держите `$.store.file(...)`
  настроенным заранее, чтобы не спутать prefab с игровым сохранением.
* **Спрайт, созданный только `resolveSprite` числа** (например `.frame(n)`),
  сохранить нельзя: источника у готового id нет. Задавайте картинку через
  `.sprite('путь')`, `.frames({src, ...})` или `attrs.src`.
* Имя `$.prefab` и методы `.toData()`, `.clone()`, `.savePrefab()`,
  `.prefab()` не пересекаются с именами ядра.
