Свой код
Поставьте SDK из npm и используйте его прямо в своём приложении. Минимальный пример
Лист персонажа Long Story Short можно встроить в стороннее приложение — виртуальный стол, менеджер кампании и так далее — и затем общаться с ним через механизм postMessage. Через этот механизм уже работают наши собственные расширения для Owlbear Rodeo: это открытый протокол, доступный любому разработчику.
Встроенное приложение открывается только с доменов из белого списка — добавить туда
localhost нельзя, нужен именно ваш рабочий домен. Пришлите его
@shakusky в Telegram, shakusky.lss в Discord или напишите в
сообщения сообщества VK — и мы добавим домен в список.
Свой код
Поставьте SDK из npm и используйте его прямо в своём приложении. Минимальный пример
Расширение VTT
Стол умеет только открыть URL? Понадобится статическая страница-мост. Мосты для VTT
Права на листы
Своя система прав не нужна: лист остаётся под управлением игрока, а вы получаете броски и состояние. Что берёт на себя стол
Лист остаётся на longstoryshort.app и открывается у вас в iframe. Общение идёт через window.postMessage, и вашему коду нет необходимости читать DOM листа, управлять его куками и токенами авторизации.
Ваша страница (ваш origin) └── iframe с листом (longstoryshort.app) └── postMessage ──→ ваша страницаSDK берёт на себя формат конверта, согласование версии протокола и фильтрацию по origin, отдавая наружу типизированные события:
npm install @longstoryshort/vtt-sdkПакет ставится из публичного npm (лицензия MIT), исходники — tldrpg/lss-vtt-sdk.
import { createBridgeSheetSource, SHEET_IFRAME_SANDBOX, formatRollMessage, rollVariant,} from '@longstoryshort/vtt-sdk';
const iframe = document.createElement('iframe');iframe.src = 'https://longstoryshort.app/iframe/characters/list/';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);
const source = createBridgeSheetSource({ iframe, allowedOrigins: ['https://longstoryshort.app'],});
source.onRoll((roll) => { // formatRollMessage → "🎲 Alice: Longsword Attack — 18 💥" // rollVariant → 'info' | 'success' | 'warning' myVTT.notification.show(formatRollMessage(roll), rollVariant(roll));});createBridgeSheetSource вешает слушатель message на window и пропускает только
те сообщения, которые пришли с разрешённого origin, несут текущую версию протокола и
отправлены именно из iframe.contentWindow. Всё остальное молча игнорируется.
Когда встраивание больше не нужно — source.dispose() снимает слушатель.
SHEET_IFRAME_SANDBOX разворачивается в набор флагов, без которых лист не заработает:
allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals| Флаг | Назначение |
|---|---|
allow-same-origin |
Лист читает куку авторизации и localStorage своего origin |
allow-scripts |
Лист — это JS-приложение |
allow-popups |
Окна OAuth-редиректа |
allow-popups-to-escape-sandbox |
Окно авторизации должно открыться в обычном контексте, а не унаследовать sandbox |
allow-forms |
Отправка форм внутри листа |
allow-modals |
Нативные диалоговые окна |
clipboard-write — это флаг Permissions Policy, он задаётся атрибутом allow, а не sandbox.
Точка входа — список персонажей, дальше пользователь выбирает лист сам:
https://longstoryshort.app/iframe/characters/list/Существуют и прямые адреса конкретного листа и конструктора
(/iframe/characters/digital/<id>/, /iframe/characters/builder/<id>/). Если ваше приложение само
управляет выбранным персонажем, можно открывать iframe сразу на его странице по id. В противном случае стоит
открывать /characters/list/, чтобы дать пользователю выбрать персонажа из списка.
| Событие | Статус | Направление | Что это |
|---|---|---|---|
dnd:roll |
✅ стабильное | лист → хост | Результат броска |
dnd:manifest |
🧪 зарезервировано | лист → хост | Возможности листа при рукопожатии |
dnd:command |
🧪 зарезервировано | хост → лист | Входящие команды (изменить HP, состояние…) |
dnd:health |
🧪 зарезервировано | лист → хост | Текущие HP |
onRoll — сокращение для самого частого случая, onEvent отдаёт всё, включая
экспериментальные события. Зарезервированные события типизированы и проведены сквозь
лист, но их API на стороне хоста может измениться до стабилизации.
Броски приходят по мере того, как игрок их делает. Состояние (dnd:manifest,
dnd:health) лист присылает сам: манифест — при загрузке, HP — как только персонаж
загрузился, и дальше при каждом изменении, включая правки прямо в листе. Токен на столе
не остаётся пустым до первой команды и не расходится с листом.
Подписаться можно и позже — SDK запоминает последнее событие каждого вида состояния и отдаёт его новому обработчику сразу. Броски не повторяются: показывать прошедший бросок как новый незачем.
source.onEvent((event) => { if (event.type === 'dnd:health') { // Придёт сразу при подписке, даже если лист прислал HP раньше. myVTT.token.setHealth(event.payload.current, event.payload.max); }});Обратное направление — source.send(...). Команда адресована вашему iframe и
возвращает boolean: false означает, что отправлять некуда — панель с листом закрыта
или ещё не загрузилась. Очереди нет, поэтому стоит сказать об этом пользователю, а не
терять команду молча.
const delivered = source.send({ type: 'dnd:command', payload: { op: 'adjust', capabilityId: 'hp', delta: -5 },});
if (!delivered) { myVTT.notification.show('Откройте панель с листом, чтобы применить изменение', 'warning');}Встраивание листа не требует от вас системы прав. Каждый лист открыт под аккаунтом своего игрока и шлёт события только своему iframe — вы собираете их у себя и рисуете на токенах. Ни кодов, ни приглашений, ни проверок доступа с нашей стороны в этом сценарии нет вообще.
Из этого следуют две вещи, о которых стоит знать заранее:
Если ваш стол — обычное веб- или десктоп-приложение, где вы полностью управляете кодом, этого достаточно: ставьте SDK как npm-пакет и используйте прямо в своём коде.
Если стол загружает интеграции как расширения, которые умеют только открыть URL (так устроен Owlbear Rodeo), понадобится отдельная статическая страница-мост — читайте про мосты.