# `$.math` — математика для игровой логики

Набор чистых функций, которых обычно не хватает в игре: ограничение и
интерполяция чисел, сглаживание, работа с углами, векторы и прямоугольники.
Аналог `@GlobalScope`-функций Godot (`clamp`, `lerp`, `move_toward`,
`smoothstep`, `wrapf`, `pingpong`, `snapped`, `angle_difference`) плюс
минимум векторной арифметики.

Подсистема не обращается к движку: это **чистые функции**. Всё, что ниже,
можно вызвать из игры (`$.math.clamp(...)`), а можно импортировать из
`src/highlevel/mathx.js` и проверить без сборки движка:

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

```js
$.math.clamp(hp, 0, maxHp);
$.math.approach(camera.x, target.x, 8, dt);          // плавно, без рывков
$.math.moveTowards(angle, targetAngle, 3 * dt);      // поворот с ограничением
const dir = $.math.vecNormalize($.math.vecSub(hero.pos(), enemy.pos()));
```

Соглашения:

* углы — в радианах; на экране ось Y смотрит вниз, поэтому положительный
  поворот идёт **по часовой стрелке**;
* функции не меняют переданные объекты, а возвращают новые;
* вектор — обычный объект `{ x, y }`, прямоугольник — `{ x, y, w, h }`
  (левый верхний угол + размеры — как у `$.grid` и `$.nav`), так что их
  можно класть в JSON и передавать в методы узлов как есть.

---

## 1. Числа

| Функция | Назначение |
|---|---|
| `clamp(v, lo, hi) → number` | Ограничить значение диапазоном `[lo, hi]` |
| `lerp(a, b, t) → number` | Линейная интерполяция; `t` может выходить за `[0,1]` |
| `inverseLerp(a, b, v) → number` | Доля пути от `a` к `b`; при `a === b` → `0` |
| `remap(v, inMin, inMax, outMin, outMax) → number` | Пересчёт значения из одного диапазона в другой |
| `moveTowards(cur, target, maxDelta) → number` | Шаг к цели не больше `maxDelta`, без перелёта |
| `smoothstep(edge0, edge1, x) → number` | S-кривая 0…1 между границами |
| `approach(cur, target, rate, dt) → number` | Экспоненциальное сглаживание, не зависящее от FPS |
| `wrap(v, min, max) → number` | Завернуть в `[min, max)` |
| `pingPong(v, len) → number` | «Туда-обратно» 0…len…0 с периодом `2*len` |
| `snap(v, step) → number` | Притянуть к шагу сетки (`snap(37, 16)` → `32`) |
| `angleDiff(from, to) → number` | Кратчайшая разница углов в `[-π, π]` |
| `deg(radians) → number` | Радианы → градусы |
| `rad(degrees) → number` | Градусы → радианы |
| `sign(v) → number` | `-1`, `0` или `1` |
| `roundTo(v, digits) → number` | Округлить до `digits` знаков (`digits < 0` — до десятков) |

Особые случаи, на которые опираются тесты:

* `clamp` терпит перепутанные границы (`clamp(5, 10, 0)` → `5`);
* `inverseLerp` и `remap` не делят на ноль при нулевом диапазоне;
* `wrap` с `max <= min` возвращает `min`; `snap` с `step <= 0` — значение
  без изменений;
* ровно половина шага в `snap` округляется вверх (`snap(40, 16)` → `48`),
  потому что внутри `Math.round`;
* `angleDiff(from, to)` для разворота ровно на π даёт `-π`: `+π` и `-π` —
  один и тот же поворот, выбрано одно соглашение.

```js
// Полоска здоровья: 100 → 0 превращается в 0 → 1 для шейдера/альфы.
const k = $.math.remap(hp, 0, maxHp, 0, 1);

// Прицел «догоняет» курсор, скорость не зависит от частоты кадров.
cam.x = $.math.approach(cam.x, mouse.x, 12, $.time.delta());

// Плавное появление: t идёт 0 → 1, анимация — по S-кривой.
const fade = $.math.smoothstep(0, 0.4, t);
```

## 2. Векторы

| Функция | Назначение |
|---|---|
| `vec2(x, y) → {x,y}` | Вектор из двух чисел |
| `vecLength(v) → number` | Длина |
| `vecLengthSq(v) → number` | Квадрат длины — дешевле для сравнений |
| `vecNormalize(v) → {x,y}` | Единичный вектор; нулевой остаётся нулевым |
| `vecAdd(a, b)`, `vecSub(a, b) → {x,y}` | Сумма и разность (`a - b`) |
| `vecScale(v, s) → {x,y}` | Умножение на число |
| `vecDot(a, b) → number` | Скалярное произведение |
| `vecDist(a, b) → number` | Расстояние между точками |
| `vecLerp(a, b, t) → {x,y}` | Интерполяция между векторами |
| `vecRotate(v, angle) → {x,y}` | Поворот на угол (радианы) |
| `vecFromAngle(angle, length) → {x,y}` | Вектор из угла, длина по умолчанию `1` |
| `vecAngle(v) → number` | Угол вектора, `atan2(y, x)` |

```js
const toHero = $.math.vecSub(hero.pos(), turret.pos());
if ($.math.vecLength(toHero) < 300 && $.math.vecDot(aim, $.math.vecNormalize(toHero)) > 0.7) {
    shoot($.math.vecAngle(toHero));
}
```

## 3. Прямоугольники

| Функция | Назначение |
|---|---|
| `rect(x, y, w, h) → {x,y,w,h}` | Прямоугольник по левому верхнему углу |
| `rectContains(r, point)` / `rectContains(r, x, y) → bool` | Точка внутри; границы включительно |
| `rectOverlap(a, b) → bool` | Пересекаются ли (касание краями — нет) |
| `rectIntersect(a, b) → {x,y,w,h}` \| `null` | Пересечение или `null` |
| `rectCenter(r) → {x,y}` | Центр |
| `rectGrow(r, amount) → {x,y,w,h}` | Расширить (`amount < 0` — сжать) |

```js
const view = $.math.rect(cam.x - cam.w / 2, cam.y - cam.h / 2, cam.w, cam.h);
if ($.math.rectOverlap(view, $.math.rect(node.x, node.y, 32, 32))) {
    draw(node);                       // рисуем только то, что видно
}
```

## 4. Установка

```js
import { installMath } from './mathx.js';
installMath($);          // $.math = { …все функции выше… }
```

`$.math` — те же самые функции, без обёрток и копий (`$.math.clamp === clamp`),
поэтому накладных расходов на вызов нет. Отдельного `tick` у подсистемы нет:
это библиотека, а не служба.

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

| Чего нет | Почему / что делать |
|---|---|
| Матриц, кватернионов, 3D | Движок двумерный; для сложной линейной алгебры считайте вручную или через `$.gfx`-трансформации |
| Кривых Безье и сплайнов | Есть `lerp` и `smoothstep`; для плавных траекторий — `$.tween` или своя функция |
| Методов у векторов (`v.add()`) | Вектор намеренно оставлен «просто данными»: JSON, сравнение, передача в любой метод узла |
| Перегрузок по типу аргумента | Всегда порядок `(a, b, t)`, а не «умный» разбор аргументов |
| Оптимизации на `Float32Array` | Функции работают с обычными объектами; для тысяч частиц считайте пачками сами |
