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

Справочник 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
Флаг Зачем нужен
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.

На первый взгляд флаг настораживает. Опасность реальна только тогда, когда 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 на стороне моста считается экспериментальным и может измениться до стабилизации.

Человекочитаемая строка броска для уведомлений или чата:

Результат броска Вывод
Обычный 🎲 Alice: Longsword Attack — 18
Крит успеха 🎲 Alice: Longsword Attack — 20 💥
Крит провала 🎲 Alice: Longsword Attack — 1 💀

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

Условие Возвращает
Крит успеха '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.