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

Встраивание листа персонажа

Лист персонажа Long Story Short можно встроить в стороннее приложение — виртуальный стол, менеджер кампании и так далее — и затем общаться с ним через механизм postMessage. Через этот механизм уже работают наши собственные расширения для Owlbear Rodeo: это открытый протокол, доступный любому разработчику.

Встроенное приложение открывается только с доменов из белого списка — добавить туда localhost нельзя, нужен именно ваш рабочий домен. Пришлите его @shakusky в Telegram, shakusky.lss в Discord или напишите в сообщения сообщества VK — и мы добавим домен в список.

Расширение 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 — вы собираете их у себя и рисуете на токенах. Ни кодов, ни приглашений, ни проверок доступа с нашей стороны в этом сценарии нет вообще.

Из этого следуют две вещи, о которых стоит знать заранее:

  • Каждый игрок открывает свой лист сам. У вас нет способа открыть чужой лист за игрока — это сознательное ограничение, а не пробел. Все данные, которые вы видите, игрок передал вам, открыв лист у себя.
  • Общее состояние стола — ваше. Мы не храним, кто за каким столом сидит. Если нужно показать HP одного игрока другому, это делает ваш стол своими средствами: события пришли к вам, дальше вы решаете, кому их разослать.

Если ваш стол — обычное веб- или десктоп-приложение, где вы полностью управляете кодом, этого достаточно: ставьте SDK как npm-пакет и используйте прямо в своём коде.

Если стол загружает интеграции как расширения, которые умеют только открыть URL (так устроен Owlbear Rodeo), понадобится отдельная статическая страница-мост — читайте про мосты.