Перейти к основному содержимому

Как написать плагин

Плагин — это папка на вашем диске. Вы правите её в своей IDE, она работает в редакторе без шага сборки, и вы публикуете её, когда готовы.

С чего начать

В панели Plugins нажмите Create. Studio попросит папку и запишет в неё шаблон:

my-plugin/
meta.json манифест
main.html разметка вашей панели
main.js ваш код — .ts, .tsx и .jsx тоже работают
plugin.d.ts сгенерированные типы всего API
tsconfig.json чтобы IDE эти типы находила
.wasignore что не загружать при публикации

Правьте файлы, нажимайте Reload from disk — изменения сразу в деле. Ни бандлера, ни установки.

.ts, .tsx и .jsx компилируются при чтении

Можно писать на TypeScript и JSX напрямую. Типы берутся из сгенерированного plugin.d.ts, так что редактор автодополняет весь API.

Манифест

{
"id": "acme.shape-spawner",
"name": "Shape Spawner",
"version": "1.0.0",
"entry": "main.tsx",
"permissions": ["scene:read", "scene:write"],
"panels": [{ "id": "main", "title": "Shapes", "entry": "main.tsx" }],
"icon": "data:image/webp;base64,…"
}

Иконка — data URL, а не путь к файлу, поэтому локальная папка и опубликованный плагин выглядят одинаково везде, где плагин появляется: в панели инструментов, в шапке панели, на карточке в библиотеке. Форма публикации сгенерирует её из любого изображения.

Ваша панель

Простейшая панель — это HTML-файл и скрипт:

<div id="root">Loading…</div>
<script type="module">
import { signal } from '@was/signals';

const clicks = signal(0);

init(async () => {
// здесь editor, world и meta уже готовы
document.getElementById('root').textContent = `${meta.name}${world.getEntities().length} entities`;
});
</script>

Либо обойдитесь без HTML вовсе и укажите в entry файл .tsx — рантайм смонтирует его экспорт по умолчанию, а панель можно собрать из собственной библиотеки компонентов редактора:

// main.tsx
import { useState } from 'react';
import { Button } from '@was/ui';

export default function Panel() {
const [clicks, setClicks] = useState(0);
return <Button onClick={() => setClicks(clicks + 1)}>{clicks}</Button>;
}
init() ждёт, пока мир действительно появится

Он выполняется, когда соединение с редактором открыто и синхронизированная копия сцены готова, так что опрашивать ничего не нужно.

Четыре глобальных объекта доступны всегда: editor (API редактора), world (живая синхронизированная копия сцены в виде сущностей и компонентов), meta (ваш манифест) и init.

Импортировать можно @was/ecs, @was/signals, @was/engine, @was/editor-api, @was/ui, @was/icons, react и react-dom/client. Всё остальное падает с ошибкой, а не разрешается молча в пустоту.

Набор стилей фиксирован

Панели на @was/ui используют заранее собранную таблицу стилей, поэтому существуют только те классы, которые в ней есть. Произвольных значений вроде text-[13px] среди них нет — для размеров вне шкалы используйте инлайновый style.

Работа со сценой

Собирать сущности руками через world можно, но для сцен и spaces есть путь лучше — попросить редактор, и вы получите его собственные умолчания, нумерацию и защиты:

await editor.scenes.list();
await editor.scenes.create({ name: 'Chapter 2', trigger: { type: 'image', imageId } });
await editor.scenes.setTrigger(sceneId, { type: 'surface', bindingType: 'wall' });
await editor.scenes.delete(sceneId);
await editor.spaces.create({ name: 'Lobby' });

trigger — это просто данные якоря, проверяемые той же схемой, что и в редакторе, так что новые типы триггеров работают без изменений протокола плагинов. Триггер по изображению сам вычислит физический размер из картинки, если вы его не указали.

Как потом найти своё

Плагину, который генерирует сцены, надо уметь их потом находить. Для этого есть tags — они автоматически ограничены пространством имён вашего плагина, так что два плагина никогда не затрут пометки друг друга, а редактор их не показывает и не трогает:

tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');

Общее состояние сессии

Часть состояния принадлежит встрече, а не документу: идущий таймер, открытое голосование, поднятая рука. Отменять такое, публиковать или хранить в проекте — всё это было бы неправильно.

Комната проекта — небольшое общее хранилище «ключ — значение», которое видят все, кто сейчас в проекте:

const { now, entries } = await editor.room.get();
await editor.room.set('timer', { running: true, endsAt: now + 60_000 });
editor.on('room.changed', (state) => render(state.entries.timer));
await editor.room.delete('timer');

Она переживает перезагрузку страницы, истекает через двенадцать часов и вмещает до 64 ключей по 8 КБ.

now — это часы сервера, и в этом весь смысл

Компьютеры двух людей могут расходиться на минуты. Храните абсолютное время окончания по серверным часам и дайте каждому клиенту отсчитывать самому — никогда не храните «осталось столько-то секунд», иначе идущий таймер означал бы запись в комнату каждую секунду для всех, кто в проекте.

Сервер также записывает, кто записал каждый ключ — именно это делает возможным честное голосование: голос засчитывается, только если ключ vote/<userId> действительно записан этим пользователем.

Оверлей во вьюпорте

Панель приватна и её можно закрыть — это не годится для того, что должны видеть все. Панель, объявленная с "surface": "hud", рисуется небольшим оверлеем поверх сцены:

editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // оверлей может вызвать собственную панель

Он стартует скрытым и показывается, когда ему есть что показать: невидимая прозрачная рамка над сценой глотала бы клики. Размер ограничен 640×400.

Оверлею копия сцены не достаётся

Он открыт у всех и всё время, поэтому намеренно дёшев: всё, что ему нужно показать, панель кладёт в комнату.

Публикация

Форма публикации принимает иконку, название, до 12 тегов, описание до 500 символов, до 4 скриншотов и флаг «только по подписке».

Ограничения: 512 КБ на файл, 64 файла, 50 плагинов на аккаунт.

Повторная публикация обновляет ту же запись. Опубликованный плагин можно скачать для правки — его файлы запишутся в выбранную вами папку.

Ассеты работают только после публикации

Файлы опубликованного плагина отдаются как положено, так что ./icon.png разрешается. Локальная папка разработки инлайнит только .ts, .js и .json — изображения, на которые ссылаются путём, не появятся, пока вы не опубликуете.

Разрешения

В манифесте перечисляется то, что плагину нужно: scene:read, scene:write, resources:write, spaces:write, collaboration:read, collaboration:write, editor:panels и другие. Неизвестные сервер отвергает.

Объявляйте только необходимое

Именно разрешения читают проверяющие и по ним судят о вашем плагине. Просите самый узкий набор, который делает дело.

А устанавливая чужой плагин, исходите из того, что он дотянется до всего проекта: ставьте только то, чему у вас есть основания доверять. → Доверие и проверка


Дальше: Расширения-компоненты