# Туториал: как добавить новое scripting API

Скрипты пользователей в Рублоксе работают через **песочницу `game.*`**. Все вызовы вида `game.player.damage(10)`, `game.scene.spawn(...)`, `game.ui.set(...)` — это методы которые ты можешь расширять.

Этот туториал — как добавить **новый метод** в `game.*`. На примере: добавим `game.player.jump()` чтобы скрипт мог программно прыгнуть.

## Архитектура (важно понять до кода)

```
┌──────────────────────────────────────────────────────┐
│  Скрипт юзера (его JS-код)                           │
│  game.player.jump();                                 │
└────────────────────┬─────────────────────────────────┘
                     │ postMessage({cmd:'player.jump'})
                     ▼
┌──────────────────────────────────────────────────────┐
│  ScriptSandboxWorker.js  (Web Worker, изолирован)    │
│  Прокси-объект game.* — обёртки которые шлют         │
│  postMessage в основной поток                        │
└────────────────────┬─────────────────────────────────┘
                     │ Worker.onmessage
                     ▼
┌──────────────────────────────────────────────────────┐
│  GameRuntime.js  (главный поток)                     │
│  Обработчик _handleScriptCommand(cmd, payload):      │
│    if (cmd === 'player.jump') { ... }                │
│  Здесь — реальный вызов BabylonScene / PlayerCtrl    │
└────────────────────┬─────────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────────┐
│  PlayerController + BabylonScene                     │
│  Изменение velocity, проигрывание анимации, и т.д.   │
└──────────────────────────────────────────────────────┘
```

**Почему так сложно:** скрипты юзеров — недоверенный код. Они в Worker'е без доступа к DOM, `window`, `fetch`. Единственный канал коммуникации — `postMessage`. Это безопасность.

## Шаг 1. Реализуй фичу в движке

Открой `src/editor/engine/PlayerController.js`. Найди где обрабатывается прыжок (по клавише Space):

```javascript
// Где-то в PlayerController.js
_handleInput() {
    if (this._keys.space && this._isGrounded) {
        this._jump();
    }
}

_jump() {
    this._velocity.y = this.jumpForce;
    this._isGrounded = false;
}
```

Если у тебя есть метод `_jump()` — отлично. Если нет — добавь его на основе того кода что обрабатывает Space.

Сделай его **публичным** (без подчёркивания):

```javascript
jump() {
    if (!this._isGrounded) return;  // нельзя прыгнуть в воздухе
    this._velocity.y = this.jumpForce;
    this._isGrounded = false;
    // опционально: проиграть звук
    this.scene3d.audio?.play('jump');
}
```

## Шаг 2. Добавь обработчик в GameRuntime

Открой `src/editor/engine/GameRuntime.js`. Найди функцию `_handleScriptCommand(cmd, payload)` — это длинный switch по `cmd`.

Добавь свой блок (рядом с другими `player.*`):

```javascript
if (cmd === 'player.jump') {
    try {
        this.playerCtrl?.jump();
    } catch (e) {
        console.warn('[script] player.jump failed:', e);
    }
    return;
}
```

Соглашение:
- `cmd` строкой через точку: `<namespace>.<action>`
- Всё оборачивай в `try/catch` — скрипт юзера не должен крашить движок
- Если что-то возвращаешь — отвечай через `this._postBack(msgId, result)`

## Шаг 3. Добавь прокси в ScriptSandboxWorker

Открой `src/editor/engine/ScriptSandboxWorker.js`. Найди объект `game.player`:

```javascript
const game = {
    player: {
        damage(amount) { postCmd('player.damage', { amount }); },
        heal(amount)   { postCmd('player.heal',   { amount }); },
        kill()         { postCmd('player.kill'); },
        // ...
    },
};
```

Добавь свой метод:

```javascript
player: {
    damage(amount) { postCmd('player.damage', { amount }); },
    heal(amount)   { postCmd('player.heal',   { amount }); },
    kill()         { postCmd('player.kill'); },
    jump()         { postCmd('player.jump'); },   // ← новое
},
```

Готово — скрипт юзера теперь может звать `game.player.jump()`.

## Шаг 4. Добавь TypeScript-декларацию (для Monaco autocomplete)

Открой `src/editor/engine/types/player.d.ts`. Найди где описаны другие методы игрока:

```typescript
declare namespace game.player {
    /** Снять HP игроку. */
    function damage(amount: number): void;

    /** Восстановить HP. */
    function heal(amount: number): void;
    // ...
}
```

Добавь свой метод **со строгой типизацией и JSDoc-комментарием**:

```typescript
/**
 * Заставить игрока прыгнуть программно (как нажатие Space).
 * Игнорируется если игрок в воздухе.
 *
 * @example
 *   game.player.jump();  // прыжок
 */
function jump(): void;
```

JSDoc-комментарий важен — Monaco показывает его как подсказку когда юзер пишет `game.player.jump(`.

## Шаг 5. Пересобери Monaco-бандл деклараций

`.d.ts` файлы склеиваются в один `bundle.js`, который Monaco грузит при старте:

```bash
cd src/editor/engine/types
python _build_bundle.py
```

Скрипт пройдёт по всем `*.d.ts`, склеит, обновит `bundle.js`. **Закоммить и `.d.ts` и `bundle.js`** — иначе у других контрибьюторов автокомплит будет показывать твою новую функцию, а в runtime получит `undefined`.

## Шаг 6. Протестируй вручную

1. Запусти dev-сервер: `npm run dev`
2. Открой студию: http://localhost:5174
3. Создай новый проект (или открой существующий)
4. Открой панель «Скрипты», создай новый скрипт с кодом:

```javascript
game.onKey('e', () => {
    game.player.jump();
});
```

5. Жми Play. В игре нажми `E` — игрок должен прыгнуть.

**Проверки на отказ:**
- Не падает если игрок в воздухе (просто игнорится)
- Не крашит движок если PlayerController ещё не создан
- Не крашит если скрипт юзера зовёт `jump()` 100 раз/сек

## Шаг 7. Запиши в [CHANGELOG.md](../CHANGELOG.md)

В секции `[Unreleased]`:

```markdown
## [Unreleased]

### Added
- `game.player.jump()` — программный прыжок игрока ([#XX](https://git.rublox.pro/rublox/studio/pulls/XX))
```

## Шаг 8. PR

Открой PR с описанием:

```markdown
## Что сделано
Новое scripting API: `game.player.jump()` — программный прыжок игрока.

## Зачем
Юзеры часто хотят сделать «авто-прыжок» в определённой зоне (платформер,
GD-уровни). Раньше нужно было хак через эмуляцию нажатия Space.

## Как протестировать
1. Создай проект, добавь скрипт:
   ```js
   game.onKey('e', () => game.player.jump());
   ```
2. Запусти, нажми E в игре.
3. Игрок прыгает.

## Файлы
- `src/editor/engine/PlayerController.js` — публичный метод `jump()`
- `src/editor/engine/GameRuntime.js` — обработчик `cmd === 'player.jump'`
- `src/editor/engine/ScriptSandboxWorker.js` — прокси `game.player.jump`
- `src/editor/engine/types/player.d.ts` — TypeScript-декларация
- `src/editor/engine/types/bundle.js` — пересобран
- `CHANGELOG.md` — запись в Unreleased
```

## Best practices

### ✅ Делай

- **Try/catch на всё в `GameRuntime`** — скрипт юзера не должен крашить
- **Throttle/debounce на UI-обновления** — `game.ui.set` 60 раз/сек убьёт FPS
- **JSDoc с `@example`** — Monaco покажет пример
- **Имя через точку**: `namespace.action` (`player.jump`, `scene.spawn`, `ui.set`)
- **Возвращай примитивы**: number, string, boolean. Объекты — только простые `{x,y,z}`

### ❌ Не делай

- **Не возвращай Babylon-объекты в Worker** — они не сериализуются через postMessage
- **Не создавай новые Worker'ы внутри скрипта** — у нас один Worker на скрипт
- **Не давай доступ к `fetch`, `XMLHttpRequest`, `WebSocket`** — это XSS-вектор
- **Не позволяй вечный цикл** в синхронной обработке `_handleScriptCommand` — повесит главный поток
- **Не добавляй deprecated API в `.d.ts`** — пусть устаревшие методы пропадут с автокомплита

## Что почитать дальше

- [src/editor/engine/README.md](../src/editor/engine/README.md) — обзор архитектуры движка
- [src/editor/engine/types/README.md](../src/editor/engine/types/README.md) — система типов для скриптов
- `ScriptSandboxWorker.js` — изучи как сделаны существующие 100+ методов

## Вопросы

Канал `#разработка` на https://team.rublox.pro
