Справочник SDK
Эта страница — подробный справочник по @longstoryshort/vtt-sdk: транспортному слою и
типам протокола. Она применима к любой архитектуре стола — веб-приложению, десктопному
приложению, расширению браузера. Обзорное введение — на странице встраивания; если стол загружает интеграции как расширения по URL, читайте
ещё Bridge guide про статическую страницу-мост.
Как это устроено
Заголовок раздела «Как это устроено»Лист и ваш стол работают на разных origin. Общение идёт только через
window.postMessage — ваш код никогда не читает DOM листа, его куки или токен
авторизации.
Ваша страница (ваш origin) └── iframe с листом (longstoryshort.app) └── postMessage ──→ ваша страницаSDK берёт на себя формат конверта, согласование версии протокола и фильтрацию по origin. Наружу отдаются типизированные события.
Установка
Заголовок раздела «Установка»npm install @longstoryshort/vtt-sdkВстраивание листа
Заголовок раздела «Встраивание листа»import { SHEET_IFRAME_SANDBOX } from '@longstoryshort/vtt-sdk';
const iframe = document.createElement('iframe');iframe.src = 'https://longstoryshort.app/iframe/characters/list/';iframe.title = 'LSS Character Sheet';iframe.setAttribute('sandbox', SHEET_IFRAME_SANDBOX);iframe.setAttribute('allow', 'clipboard-write');iframe.style.cssText = 'border:none;width:100%;height:100vh;display:block';document.body.appendChild(iframe);SHEET_IFRAME_SANDBOX разворачивается в:
allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modalsФлаги sandbox
Заголовок раздела «Флаги sandbox»| Флаг | Зачем нужен |
|---|---|
allow-same-origin |
Лист читает куку авторизации и localStorage своего origin. Без флага origin становится непрозрачным и логин ломается. |
allow-scripts |
Лист — это JS-приложение. |
allow-popups |
Окна OAuth-редиректа. |
allow-popups-to-escape-sandbox |
Окно авторизации должно открыться в обычном контексте, а не унаследовать sandbox. |
allow-forms |
Отправка форм внутри листа. |
allow-modals |
Нативные диалоговые окна. |
clipboard-write — это флаг Permissions Policy, он задаётся атрибутом allow, а не sandbox.
Про allow-same-origin
Заголовок раздела «Про allow-same-origin»На первый взгляд флаг настораживает. Опасность реальна только тогда, когда iframe и
внешняя страница находятся на одном origin — тогда allow-same-origin позволил бы
iframe выйти из песочницы и добраться до кук родителя. Здесь это не так: лист живёт на
longstoryshort.app, ваша страница — на вашем собственном origin. Границу браузер
держит независимо от sandbox, так что флаг даёт листу доступ только к его собственным
данным авторизации — ровно то, что ему нужно.
Приём событий
Заголовок раздела «Приём событий»import { createBridgeSheetSource } from '@longstoryshort/vtt-sdk';
const source = createBridgeSheetSource({ iframe, // Учитываются только сообщения с origin листа. allowedOrigins: ['https://longstoryshort.app'],});createBridgeSheetSource вешает слушатель message на window и пропускает только
конверты, которые пришли с разрешённого origin, несут текущую версию протокола и
отправлены именно из iframe.contentWindow. Всё остальное молча игнорируется.
Броски костей
Заголовок раздела «Броски костей»import { formatRollMessage, rollVariant } from '@longstoryshort/vtt-sdk';
source.onRoll((roll) => { const message = formatRollMessage(roll); // "🎲 Alice: Longsword Attack — 18 💥" const severity = rollVariant(roll); // 'info' | 'success' | 'warning'
// Дальше — как удобно вашему столу: уведомление, чат, рассылка и т. д. myVTT.notification.show(message, severity);});Все события
Заголовок раздела «Все события»source.onEvent((event) => { if (event.type === 'dnd:roll') { // event.payload: DiceRollPayload } if (event.type === 'dnd:manifest') { // event.payload: CapabilityManifest (экспериментально) } if (event.type === 'dnd:health') { // event.payload: HealthChangedPayload (экспериментально) }});onRoll — сокращение для самого частого случая, onEvent отдаёт всё, включая
экспериментальные события.
Состояние листа: снапшот и повтор
Заголовок раздела «Состояние листа: снапшот и повтор»Есть два вида событий, и ведут они себя по-разному.
Броски — это факты в моменте. Подписались позже — прошлые броски не придут, и это правильно: показывать вчерашний бросок как новый ни к чему.
Состояние (dnd:manifest, dnd:health) лист присылает сам, никого не спрашивая:
манифест — сразу при загрузке, HP — как только персонаж загрузился, и дальше при каждом
изменении, откуда бы оно ни пришло. Благодаря этому токен на столе не остаётся пустым до
первой команды и не «залипает», когда игрок правит HP прямо в листе.
Но лист загружается раньше, чем ваш стол успевает доинициализироваться, поэтому SDK запоминает последнее событие каждого вида состояния и отдаёт его новому обработчику сразу при подписке. То есть можно спокойно писать так:
await myVTT.ready(); // лист уже мог прислать HP
source.onEvent((event) => { if (event.type === 'dnd:health') { // Придёт немедленно — это запомненный снапшот, а не новое изменение. myVTT.token.setHealth(event.payload.current, event.payload.max); }});Повтор происходит один раз на подписку и только для последнего значения каждого типа.
Очистка
Заголовок раздела «Очистка»source.dispose(); // снимает слушатель messageОтправка команд в лист
Заголовок раздела «Отправка команд в лист»// Исходящая команда (экспериментально)const delivered = source.send({ type: 'dnd:command', payload: { op: 'adjust', capabilityId: 'hp', delta: -5 },});
if (!delivered) { myVTT.notification.show('Откройте панель с листом, чтобы применить изменение', 'warning');}send адресована вашему iframe и возвращает boolean:
| Значение | Что произошло |
|---|---|
true |
Сообщение отправлено в окно листа. Это не гарантия, что лист его обработал. |
false |
Отправлять некуда: панель закрыта, iframe удалён или ещё не загрузился. |
Очереди нет — команду в закрытое окошко никто не досылает. Явный false нужен именно для
того, чтобы стол мог сказать об этом пользователю, а не терять команду молча.
Справочник протокола
Заголовок раздела «Справочник протокола»| Тип события | Статус | Направление | Тип payload |
|---|---|---|---|
dnd:roll |
✅ стабильное | лист → хост | DiceRollPayload |
dnd:manifest |
🧪 зарезервировано | лист → хост | CapabilityManifest |
dnd:health |
🧪 зарезервировано | лист → хост | HealthChangedPayload |
dnd:command |
🧪 зарезервировано | хост → лист | CapabilityOperation |
Зарезервированные события типизированы и проведены сквозь лист, но API на стороне моста считается экспериментальным и может измениться до стабилизации.
Утилиты
Заголовок раздела «Утилиты»formatRollMessage(roll: DiceRollPayload): string
Заголовок раздела «formatRollMessage(roll: DiceRollPayload): string»Человекочитаемая строка броска для уведомлений или чата:
| Результат броска | Вывод |
|---|---|
| Обычный | 🎲 Alice: Longsword Attack — 18 |
| Крит успеха | 🎲 Alice: Longsword Attack — 20 💥 |
| Крит провала | 🎲 Alice: Longsword Attack — 1 💀 |
rollVariant(roll: DiceRollPayload): NotifyVariant
Заголовок раздела «rollVariant(roll: DiceRollPayload): NotifyVariant»Отображает состояние крита в серьёзность уведомления, чтобы красить тосты по цвету:
| Условие | Возвращает |
|---|---|
| Крит успеха | 'success' |
| Крит провала | 'warning' |
| Обычный бросок | 'info' |
NotifyVariant — это 'info' \| 'success' \| 'warning' \| 'error'.
Сторона листа
Заголовок раздела «Сторона листа»Если вы разрабатываете сам лист (а не встраивающий его стол), используйте
createSheetClient для отправки событий и приёма входящих команд:
import { createSheetClient } from '@longstoryshort/vtt-sdk';
const client = createSheetClient();
// Отправить бросок во встраивающую страницуclient.send({ type: 'dnd:roll', payload: { ... } });
// Принять входящие команды от мостаconst unsub = client.onEvent((event) => { if (event.type === 'dnd:command') { // применить операцию к персонажу }});
// Очисткаunsub();client.dispose();createSheetClient по умолчанию отправляет в window.parent (встраивающий мост) и
слушает window.
