@longstoryshort/vtt-sdk 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -55
- package/dist/adapters/owlbear/index.d.ts +30 -13
- package/dist/adapters/owlbear/index.js +13 -38
- package/dist/{types-w2_82sqo.d.ts → formatRoll-BhFkInCu.d.ts} +7 -50
- package/dist/index.d.ts +7 -32
- package/dist/index.js +23 -73
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# @longstoryshort/vtt-sdk
|
|
2
2
|
|
|
3
|
-
Embed a [longstoryshort.app](https://longstoryshort.app) character sheet in any virtual tabletop
|
|
3
|
+
Embed a [longstoryshort.app](https://longstoryshort.app) character sheet in any virtual tabletop via iframe and postMessage.
|
|
4
4
|
|
|
5
5
|
## How it works
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The sheet and your VTT run at different origins. They communicate only via `window.postMessage` — your code never reads the sheet's DOM, cookies, or auth token.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Your page (your origin)
|
|
11
|
+
└── LSS sheet iframe (longstoryshort.app)
|
|
12
|
+
└── postMessage ──→ your page
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
@@ -18,7 +18,7 @@ OBR / Foundry / … ──► [bridge page, FOREIGN origin]
|
|
|
18
18
|
npm install @longstoryshort/vtt-sdk
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
For Owlbear Rodeo, also install the peer dependency:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
24
|
npm install @owlbear-rodeo/sdk
|
|
@@ -28,82 +28,64 @@ npm install @owlbear-rodeo/sdk
|
|
|
28
28
|
|
|
29
29
|
| Import | Contents |
|
|
30
30
|
|--------|----------|
|
|
31
|
-
| `@longstoryshort/vtt-sdk` | Core: types, `
|
|
32
|
-
| `@longstoryshort/vtt-sdk/owlbear` | `OwlbearAdapter`, `
|
|
31
|
+
| `@longstoryshort/vtt-sdk` | Core: protocol types, `createBridgeSheetSource`, `createSheetClient`, `formatRollMessage`, `SHEET_IFRAME_SANDBOX` |
|
|
32
|
+
| `@longstoryshort/vtt-sdk/owlbear` | `OwlbearAdapter`, `ObrAdapter`, OBR bootstrap helpers |
|
|
33
33
|
|
|
34
|
-
## Quick start
|
|
34
|
+
## Quick start
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
|
-
import {
|
|
38
|
-
import { OwlbearAdapter } from '@longstoryshort/vtt-sdk/owlbear';
|
|
37
|
+
import { createBridgeSheetSource, SHEET_IFRAME_SANDBOX, formatRollMessage, rollVariant } from '@longstoryshort/vtt-sdk';
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
// Embed the sheet
|
|
40
|
+
const iframe = document.createElement('iframe');
|
|
41
|
+
iframe.src = 'https://longstoryshort.app/iframe/characters/list/';
|
|
42
|
+
iframe.setAttribute('sandbox', SHEET_IFRAME_SANDBOX);
|
|
43
|
+
iframe.setAttribute('allow', 'clipboard-write');
|
|
44
|
+
iframe.style.cssText = 'border:none;width:100%;height:100vh;display:block';
|
|
45
|
+
document.body.appendChild(iframe);
|
|
46
|
+
|
|
47
|
+
// Receive rolls
|
|
41
48
|
const source = createBridgeSheetSource({
|
|
42
|
-
iframe
|
|
49
|
+
iframe,
|
|
43
50
|
allowedOrigins: ['https://longstoryshort.app'],
|
|
44
51
|
});
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
labelHint: 'Select exactly one token to place a roll label',
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// later:
|
|
54
|
-
dispose();
|
|
55
|
-
source.dispose();
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Quick start — sheet side
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import { createSheetClient } from '@longstoryshort/vtt-sdk';
|
|
62
|
-
|
|
63
|
-
const client = createSheetClient();
|
|
64
|
-
|
|
65
|
-
// emit a roll to the bridge
|
|
66
|
-
client.send({ type: 'dnd:roll', payload: { ... } });
|
|
67
|
-
|
|
68
|
-
// receive inbound commands from the bridge
|
|
69
|
-
const unsub = client.onEvent((event) => {
|
|
70
|
-
if (event.type === 'dnd:command') { /* handle damage, conditions, … */ }
|
|
53
|
+
source.onRoll((roll) => {
|
|
54
|
+
// wire to your VTT — notification, chat, peer broadcast, etc.
|
|
55
|
+
console.log(formatRollMessage(roll), rollVariant(roll));
|
|
71
56
|
});
|
|
72
57
|
|
|
73
58
|
// cleanup
|
|
74
|
-
|
|
59
|
+
source.dispose();
|
|
75
60
|
```
|
|
76
61
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
The bridge must embed the sheet iframe with at least these sandbox tokens:
|
|
62
|
+
## Documentation
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
`allow-same-origin` is required so the sheet can read its auth cookie and access localStorage. Without it the sheet gets an opaque origin and auth breaks.
|
|
64
|
+
- [SDK guide](docs/sdk-guide.md) — sandbox requirements, receiving events, protocol reference, utilities. Start here regardless of your VTT architecture.
|
|
65
|
+
- [Bridge guide](docs/bridge-guide.md) — for VTTs that use an extension/plugin model and need a separate static bridge page. Includes the full annotated OBR bridge and `OwlbearAdapter` reference.
|
|
86
66
|
|
|
87
67
|
## Reference bridge — Owlbear Rodeo (D&D 5e)
|
|
88
68
|
|
|
89
|
-
A deployable
|
|
69
|
+
A deployable bridge for Owlbear Rodeo lives in [`bridges/dnd/`](bridges/dnd/). Deployed automatically to GitHub Pages on every push to `master`.
|
|
90
70
|
|
|
91
71
|
**Live manifest:** `https://bridge.longstoryshort.app/dnd/obr/manifest.json`
|
|
92
72
|
|
|
93
|
-
To install
|
|
73
|
+
To install in OBR: Extensions → Add extension → paste the manifest URL above.
|
|
74
|
+
|
|
75
|
+
## Bridge template
|
|
94
76
|
|
|
95
|
-
|
|
77
|
+
[`bridges/_template/`](bridges/_template/) is a minimal copy-and-modify skeleton — iframe embed + `createBridgeSheetSource` + `TODO` comments for your VTT's APIs, ready to build with Vite.
|
|
96
78
|
|
|
97
79
|
## Protocol events
|
|
98
80
|
|
|
99
81
|
| Type | Status | Direction | Description |
|
|
100
82
|
|------|--------|-----------|-------------|
|
|
101
|
-
| `dnd:roll`
|
|
102
|
-
| `dnd:manifest` | 🧪 reserved
|
|
103
|
-
| `dnd:health`
|
|
104
|
-
| `dnd:command`
|
|
83
|
+
| `dnd:roll` | ✅ stable | sheet → host | A roll result |
|
|
84
|
+
| `dnd:manifest` | 🧪 reserved | sheet → host | Sheet capabilities at handshake |
|
|
85
|
+
| `dnd:health` | 🧪 reserved | sheet → host | HP after an adjust/set |
|
|
86
|
+
| `dnd:command` | 🧪 reserved | host → sheet | Inbound ops (adjust HP, toggle condition, …) |
|
|
105
87
|
|
|
106
|
-
Reserved events are typed and
|
|
88
|
+
Reserved events are typed and wired end-to-end, but the bridge-side API is experimental. Subscribe via `source.onEvent` directly.
|
|
107
89
|
|
|
108
90
|
## License
|
|
109
91
|
|
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as SheetEvent, N as NotifyVariant } from '../../formatRoll-BhFkInCu.js';
|
|
2
2
|
import * as _owlbear_rodeo_sdk from '@owlbear-rodeo/sdk';
|
|
3
3
|
|
|
4
|
+
interface ObrPlayer {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
role: 'gm' | 'player';
|
|
8
|
+
}
|
|
9
|
+
/** Public contract of {@link OwlbearAdapter} — use this type when you need to reference the adapter without importing the class. */
|
|
10
|
+
interface ObrAdapter {
|
|
11
|
+
readonly isAvailable: boolean;
|
|
12
|
+
ready(): Promise<boolean>;
|
|
13
|
+
getSessionId(): string | undefined;
|
|
14
|
+
getCurrentUser(): ObrPlayer | undefined;
|
|
15
|
+
notify(message: string, variant?: 'info' | 'success' | 'warning' | 'error'): void;
|
|
16
|
+
broadcast(event: SheetEvent): void;
|
|
17
|
+
onEvent(handler: (event: SheetEvent) => void): () => void;
|
|
18
|
+
getRoomMetadata(): Promise<Record<string, unknown>>;
|
|
19
|
+
onRoomMetadataChange(handler: () => void): () => void;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
|
-
* Owlbear Rodeo
|
|
24
|
+
* Owlbear Rodeo bridge helper.
|
|
6
25
|
*
|
|
7
26
|
* Owlbear exposes no public dice API (its 3D roller is first-party and closed),
|
|
8
|
-
* so rendering a roll is
|
|
9
|
-
* a transient label item over the roller's token — scene items are shared, so
|
|
27
|
+
* so rendering a roll is the bridge's job: broadcast a result for toasts/logs and
|
|
28
|
+
* add a transient label item over the roller's token — scene items are shared, so
|
|
10
29
|
* everyone at the table sees the floating number. All scene work is best-effort
|
|
11
30
|
* and degrades silently; the broadcast/notification path is the guaranteed core.
|
|
12
31
|
*/
|
|
13
|
-
declare class OwlbearAdapter implements
|
|
14
|
-
private sdk;
|
|
32
|
+
declare class OwlbearAdapter implements ObrAdapter {
|
|
15
33
|
private obr;
|
|
16
34
|
private user;
|
|
17
35
|
private sessionId;
|
|
@@ -21,11 +39,12 @@ declare class OwlbearAdapter implements VTTAdapter {
|
|
|
21
39
|
ready(): Promise<boolean>;
|
|
22
40
|
private init;
|
|
23
41
|
getSessionId(): string | undefined;
|
|
24
|
-
getCurrentUser():
|
|
42
|
+
getCurrentUser(): ObrPlayer | undefined;
|
|
25
43
|
broadcast(event: SheetEvent): void;
|
|
26
44
|
onEvent(handler: (event: SheetEvent) => void): () => void;
|
|
27
45
|
notify(message: string, variant?: NotifyVariant): void;
|
|
28
|
-
|
|
46
|
+
getRoomMetadata(): Promise<Record<string, unknown>>;
|
|
47
|
+
onRoomMetadataChange(handler: () => void): () => void;
|
|
29
48
|
dispose(): void;
|
|
30
49
|
}
|
|
31
50
|
|
|
@@ -43,10 +62,8 @@ declare function syncObrref(): void;
|
|
|
43
62
|
|
|
44
63
|
/** Room-wide pub/sub channel for sheet events. Namespaced to avoid collisions. */
|
|
45
64
|
declare const BROADCAST_CHANNEL = "rodeo.lss/sheet-events";
|
|
46
|
-
/**
|
|
47
|
-
declare const
|
|
48
|
-
/** How long a roll label floats over a token before it is removed. */
|
|
49
|
-
declare const DEFAULT_LABEL_TTL_MS = 4000;
|
|
65
|
+
/** OBR room metadata key — false when Vortex is connected (logger active), true otherwise. */
|
|
66
|
+
declare const NOTIFY_ROLLS_KEY = "rodeo.lss/notify-rolls";
|
|
50
67
|
|
|
51
68
|
interface ObrLike {
|
|
52
69
|
onReady(cb: () => void): void;
|
|
@@ -80,4 +97,4 @@ declare function preloadObrSdk(sdk: OwlbearSdk): void;
|
|
|
80
97
|
*/
|
|
81
98
|
declare function loadObrSdk(): Promise<OwlbearSdk | null>;
|
|
82
99
|
|
|
83
|
-
export { BROADCAST_CHANNEL,
|
|
100
|
+
export { BROADCAST_CHANNEL, NOTIFY_ROLLS_KEY, type ObrAdapter, type ObrPlayer, type ObrPreloadWindow, OwlbearAdapter, loadObrSdk, preloadObrSdk, syncObrref, whenObrReady };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// src/adapters/owlbear/constants.ts
|
|
2
2
|
var BROADCAST_CHANNEL = "rodeo.lss/sheet-events";
|
|
3
|
-
var
|
|
4
|
-
var DEFAULT_LABEL_TTL_MS = 4e3;
|
|
3
|
+
var NOTIFY_ROLLS_KEY = "rodeo.lss/notify-rolls";
|
|
5
4
|
|
|
6
5
|
// src/adapters/owlbear/obrref.ts
|
|
7
6
|
var PARAM = "obrref";
|
|
@@ -90,7 +89,6 @@ var NOTIFY_VARIANT = {
|
|
|
90
89
|
};
|
|
91
90
|
var OwlbearAdapter = class {
|
|
92
91
|
constructor() {
|
|
93
|
-
this.sdk = null;
|
|
94
92
|
this.obr = null;
|
|
95
93
|
this.readyPromise = null;
|
|
96
94
|
this.disposed = false;
|
|
@@ -113,7 +111,6 @@ var OwlbearAdapter = class {
|
|
|
113
111
|
if (!sdk) {
|
|
114
112
|
return false;
|
|
115
113
|
}
|
|
116
|
-
this.sdk = sdk;
|
|
117
114
|
this.obr = sdk.default;
|
|
118
115
|
if (DEV3) {
|
|
119
116
|
console.info(
|
|
@@ -174,43 +171,21 @@ var OwlbearAdapter = class {
|
|
|
174
171
|
void this.obr?.notification.show(message, NOTIFY_VARIANT[variant]).catch(() => {
|
|
175
172
|
});
|
|
176
173
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
const [token] = await obr.scene.items.getItems(selection);
|
|
191
|
-
if (!token) {
|
|
192
|
-
if (DEV3) {
|
|
193
|
-
console.warn("[OwlbearAdapter] label skipped \u2014 selected item not found in scene");
|
|
194
|
-
}
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
const label = sdk.buildLabel().plainText(text).position(token.position).attachedTo(token.id).pointerHeight(0).disableHit(true).locked(true).layer("TEXT").metadata({ [LABEL_METADATA_KEY]: true }).build();
|
|
198
|
-
await obr.scene.items.addItems([label]);
|
|
199
|
-
window.setTimeout(() => {
|
|
200
|
-
void obr.scene.items.deleteItems([label.id]).catch(() => {
|
|
201
|
-
});
|
|
202
|
-
}, ttlMs);
|
|
203
|
-
return true;
|
|
204
|
-
} catch (error) {
|
|
205
|
-
if (DEV3) {
|
|
206
|
-
console.warn("[OwlbearAdapter] label error:", error);
|
|
207
|
-
}
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
174
|
+
getRoomMetadata() {
|
|
175
|
+
if (!this.obr) return Promise.resolve({});
|
|
176
|
+
return this.obr.room.getMetadata().then(
|
|
177
|
+
(meta) => meta,
|
|
178
|
+
() => ({})
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
onRoomMetadataChange(handler) {
|
|
182
|
+
if (!this.obr) return () => {
|
|
183
|
+
};
|
|
184
|
+
return this.obr.room.onMetadataChange(handler);
|
|
210
185
|
}
|
|
211
186
|
dispose() {
|
|
212
187
|
this.disposed = true;
|
|
213
188
|
}
|
|
214
189
|
};
|
|
215
190
|
|
|
216
|
-
export { BROADCAST_CHANNEL,
|
|
191
|
+
export { BROADCAST_CHANNEL, NOTIFY_ROLLS_KEY, OwlbearAdapter, loadObrSdk, preloadObrSdk, syncObrref, whenObrReady };
|
|
@@ -4,13 +4,6 @@
|
|
|
4
4
|
* This file must not import from any project outside this SDK directory —
|
|
5
5
|
* only from other SDK files and external dependencies.
|
|
6
6
|
*/
|
|
7
|
-
type VTTUserRole = 'gm' | 'player';
|
|
8
|
-
interface VTTUser {
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
role: VTTUserRole;
|
|
12
|
-
}
|
|
13
|
-
type NotifyVariant = 'info' | 'success' | 'warning' | 'error';
|
|
14
7
|
/** A single dice roll made on a character sheet, normalized for any VTT. */
|
|
15
8
|
interface DiceRollPayload {
|
|
16
9
|
characterId: string;
|
|
@@ -140,48 +133,6 @@ interface SheetSource {
|
|
|
140
133
|
/** Subscribe to rolls made on the sheet. Returns an unsubscribe fn. */
|
|
141
134
|
onRoll(handler: (roll: DiceRollPayload) => void): () => void;
|
|
142
135
|
}
|
|
143
|
-
/** Human-facing strings surfaced by the bridge — override to localize. */
|
|
144
|
-
interface RollBridgeMessages {
|
|
145
|
-
/** Toast shown once the sheet connects to the table. */
|
|
146
|
-
connected: string;
|
|
147
|
-
/** Toast shown to the roller when a token label could not be placed. */
|
|
148
|
-
labelHint: string;
|
|
149
|
-
}
|
|
150
|
-
interface RollBridgeOptions {
|
|
151
|
-
messages?: Partial<RollBridgeMessages>;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* The seam every VTT implements. The sheet talks only to this interface; each
|
|
155
|
-
* table (Owlbear, Foundry, …) ships a thin adapter that maps its own SDK onto
|
|
156
|
-
* these methods.
|
|
157
|
-
*/
|
|
158
|
-
interface VTTAdapter {
|
|
159
|
-
/** True only when the page actually runs inside this VTT. */
|
|
160
|
-
readonly isAvailable: boolean;
|
|
161
|
-
/**
|
|
162
|
-
* Loads and handshakes with the VTT SDK. Resolves `true` once ready, or
|
|
163
|
-
* `false` if the page is not running inside this VTT. Safe to call multiple
|
|
164
|
-
* times — the work happens once.
|
|
165
|
-
*/
|
|
166
|
-
ready(): Promise<boolean>;
|
|
167
|
-
getSessionId(): string | undefined;
|
|
168
|
-
getCurrentUser(): VTTUser | undefined;
|
|
169
|
-
/** Send an event to every other client in the room (sender excluded). */
|
|
170
|
-
broadcast(event: SheetEvent): void;
|
|
171
|
-
/** Subscribe to events broadcast by other clients. Returns an unsubscribe fn. */
|
|
172
|
-
onEvent(handler: (event: SheetEvent) => void): () => void;
|
|
173
|
-
/** Local toast on this client. */
|
|
174
|
-
notify(message: string, variant?: NotifyVariant): void;
|
|
175
|
-
/**
|
|
176
|
-
* Float a transient text label over the player's currently selected token.
|
|
177
|
-
* Scene items are shared, so the label is visible to everyone at the table.
|
|
178
|
-
* Resolves `true` if a label was placed, `false` when there isn't exactly
|
|
179
|
-
* one token selected or the scene write was rejected (e.g. no permission).
|
|
180
|
-
*/
|
|
181
|
-
labelOverSelection(text: string, ttlMs?: number): Promise<boolean>;
|
|
182
|
-
/** Tear down listeners / SDK handlers. */
|
|
183
|
-
dispose(): void;
|
|
184
|
-
}
|
|
185
136
|
/** Minimal "listen for messages" surface — the real `window` satisfies it. */
|
|
186
137
|
interface MessageHost {
|
|
187
138
|
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
@@ -218,4 +169,10 @@ interface SheetClient {
|
|
|
218
169
|
dispose(): void;
|
|
219
170
|
}
|
|
220
171
|
|
|
221
|
-
|
|
172
|
+
type NotifyVariant = 'info' | 'success' | 'warning' | 'error';
|
|
173
|
+
/** Toast text for a roll, e.g. "🎲 Alice: Longsword Attack — 18 💥". */
|
|
174
|
+
declare function formatRollMessage(payload: DiceRollPayload): string;
|
|
175
|
+
/** Maps a roll's crit state onto a toast variant. */
|
|
176
|
+
declare function rollVariant(payload: DiceRollPayload): NotifyVariant;
|
|
177
|
+
|
|
178
|
+
export { type CapabilityDescriptor as C, type DiceRollPayload as D, type HealthChangedPayload as H, type MessageHost as M, type NotifyVariant as N, type SheetEvent as S, type SheetClientOptions as a, type SheetClient as b, type SheetSource as c, type CapabilityManifest as d, type CapabilityOpAddTag as e, type CapabilityOpAdjust as f, type CapabilityOpName as g, type CapabilityOpRemoveTag as h, type CapabilityOpRequestRoll as i, type CapabilityOpSet as j, type CapabilityOpToggle as k, type CapabilityOperation as l, type MessageTarget as m, formatRollMessage as n, rollVariant as r };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { a as SheetClientOptions, b as SheetClient, c as SheetSource, S as SheetEvent, M as MessageHost } from './formatRoll-BhFkInCu.js';
|
|
2
|
+
export { C as CapabilityDescriptor, d as CapabilityManifest, e as CapabilityOpAddTag, f as CapabilityOpAdjust, g as CapabilityOpName, h as CapabilityOpRemoveTag, i as CapabilityOpRequestRoll, j as CapabilityOpSet, k as CapabilityOpToggle, l as CapabilityOperation, D as DiceRollPayload, H as HealthChangedPayload, m as MessageTarget, N as NotifyVariant, n as formatRollMessage, r as rollVariant } from './formatRoll-BhFkInCu.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Minimum sandbox tokens required on the sheet iframe.
|
|
@@ -8,23 +8,6 @@ export { e as CapabilityDescriptor, f as CapabilityOpAddTag, g as CapabilityOpAd
|
|
|
8
8
|
*/
|
|
9
9
|
declare const SHEET_IFRAME_SANDBOX = "allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals";
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* The default roll bridge — one opinionated policy, not the full protocol.
|
|
13
|
-
*
|
|
14
|
-
* Wires {@link SheetSource} → {@link VTTAdapter} for the `dnd:roll` event only:
|
|
15
|
-
* - a roll on the sheet → local toast for the roller + broadcast to other
|
|
16
|
-
* clients + a transient label over the roller's selected token;
|
|
17
|
-
* - a roll broadcast by another client → local toast.
|
|
18
|
-
*
|
|
19
|
-
* Inbound capability wiring (`dnd:command`, `dnd:manifest`, `dnd:health`) is
|
|
20
|
-
* deliberately out of scope here — wire those directly via
|
|
21
|
-
* {@link BridgeSheetSource.onEvent} when your bridge needs them.
|
|
22
|
-
*
|
|
23
|
-
* Returns a dispose fn that tears down every subscription it created. The
|
|
24
|
-
* adapter is left untouched — it may be shared and longer-lived than the bridge.
|
|
25
|
-
*/
|
|
26
|
-
declare function createRollBridge(source: SheetSource, adapter: VTTAdapter, options?: RollBridgeOptions): () => void;
|
|
27
|
-
|
|
28
11
|
/**
|
|
29
12
|
* Sheet-side half of the postMessage transport. The character sheet (running in
|
|
30
13
|
* its own origin, embedded by a VTT bridge in a foreign origin) uses this to push
|
|
@@ -53,11 +36,9 @@ interface BridgeSheetSourceOptions {
|
|
|
53
36
|
/** Origin to post inbound commands to. Default `'*'`. */
|
|
54
37
|
targetOrigin?: string;
|
|
55
38
|
}
|
|
56
|
-
/**
|
|
39
|
+
/** Bridge-side source: `onRoll` convenience + full `onEvent` access + inbound `send`. */
|
|
57
40
|
interface BridgeSheetSource extends SheetSource {
|
|
58
|
-
/** Subscribe to
|
|
59
|
-
onManifest(handler: (manifest: CapabilityManifest) => void): () => void;
|
|
60
|
-
/** Subscribe to every event coming from the sheet (not only rolls). */
|
|
41
|
+
/** Subscribe to every event coming from the sheet. Returns an unsubscribe fn. */
|
|
61
42
|
onEvent(handler: (event: SheetEvent) => void): () => void;
|
|
62
43
|
/** Post an inbound command to the sheet (e.g. `dnd:command`). */
|
|
63
44
|
send(event: SheetEvent): void;
|
|
@@ -65,18 +46,12 @@ interface BridgeSheetSource extends SheetSource {
|
|
|
65
46
|
}
|
|
66
47
|
/**
|
|
67
48
|
* Bridge-side half of the postMessage transport. Runs in the bridge frame (the
|
|
68
|
-
* VTT extension), listens to the embedded sheet iframe, and
|
|
69
|
-
* `
|
|
70
|
-
* bridge ever touching the sheet's internals.
|
|
49
|
+
* VTT extension), listens to the embedded sheet iframe, and delivers typed
|
|
50
|
+
* `SheetEvent`s — without the bridge ever touching the sheet's internals.
|
|
71
51
|
*
|
|
72
52
|
* `contentWindow` is read live on every message/send, so it survives the sheet
|
|
73
53
|
* iframe navigating or reloading (e.g. Gatsby dev's full-reload on navigation).
|
|
74
54
|
*/
|
|
75
55
|
declare function createBridgeSheetSource(options: BridgeSheetSourceOptions): BridgeSheetSource;
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
declare function formatRollMessage(payload: DiceRollPayload): string;
|
|
79
|
-
/** Maps a roll's crit state onto a toast variant. */
|
|
80
|
-
declare function rollVariant(payload: DiceRollPayload): NotifyVariant;
|
|
81
|
-
|
|
82
|
-
export { type BridgeSheetSource, type BridgeSheetSourceOptions, CapabilityManifest, DiceRollPayload, MessageHost, NotifyVariant, RollBridgeOptions, SHEET_IFRAME_SANDBOX, SheetClient, SheetClientOptions, SheetEvent, type SheetFrameRef, SheetSource, VTTAdapter, createBridgeSheetSource, createRollBridge, createSheetClient, formatRollMessage, rollVariant };
|
|
57
|
+
export { type BridgeSheetSource, type BridgeSheetSourceOptions, MessageHost, SHEET_IFRAME_SANDBOX, SheetClient, SheetClientOptions, SheetEvent, type SheetFrameRef, SheetSource, createBridgeSheetSource, createSheetClient };
|
package/dist/index.js
CHANGED
|
@@ -1,67 +1,6 @@
|
|
|
1
1
|
// src/constants.ts
|
|
2
2
|
var SHEET_IFRAME_SANDBOX = "allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals";
|
|
3
3
|
|
|
4
|
-
// src/formatRoll.ts
|
|
5
|
-
function formatRollMessage(payload) {
|
|
6
|
-
let crit = "";
|
|
7
|
-
if (payload.isCrit) {
|
|
8
|
-
if (payload.critKind === "success") {
|
|
9
|
-
crit = " \u{1F4A5}";
|
|
10
|
-
} else if (payload.critKind === "failure") {
|
|
11
|
-
crit = " \u{1F480}";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return `\u{1F3B2} ${payload.characterName}: ${payload.title} \u2014 ${payload.total}${crit}`;
|
|
15
|
-
}
|
|
16
|
-
function rollVariant(payload) {
|
|
17
|
-
if (payload.isCrit && payload.critKind === "success") {
|
|
18
|
-
return "success";
|
|
19
|
-
}
|
|
20
|
-
if (payload.isCrit && payload.critKind === "failure") {
|
|
21
|
-
return "warning";
|
|
22
|
-
}
|
|
23
|
-
return "info";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// src/createRollBridge.ts
|
|
27
|
-
var DEFAULT_MESSAGES = {
|
|
28
|
-
connected: "\u{1F3B2} Sheet connected to the table",
|
|
29
|
-
labelHint: "No label placed \u2014 select exactly one of your tokens on the map"
|
|
30
|
-
};
|
|
31
|
-
function createRollBridge(source, adapter, options = {}) {
|
|
32
|
-
const messages = { ...DEFAULT_MESSAGES, ...options.messages };
|
|
33
|
-
const cleanups = [];
|
|
34
|
-
let cancelled = false;
|
|
35
|
-
cleanups.push(source.onRoll((roll) => {
|
|
36
|
-
if (!adapter.isAvailable) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
adapter.notify(formatRollMessage(roll), rollVariant(roll));
|
|
40
|
-
adapter.broadcast({ type: "dnd:roll", payload: roll });
|
|
41
|
-
void adapter.labelOverSelection(roll.total).then((placed) => {
|
|
42
|
-
if (!placed) {
|
|
43
|
-
adapter.notify(messages.labelHint, "warning");
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}));
|
|
47
|
-
void adapter.ready().then((available) => {
|
|
48
|
-
if (cancelled || !available) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
adapter.notify(messages.connected, "success");
|
|
52
|
-
cleanups.push(adapter.onEvent((event) => {
|
|
53
|
-
if (event.type !== "dnd:roll") {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
adapter.notify(formatRollMessage(event.payload), rollVariant(event.payload));
|
|
57
|
-
}));
|
|
58
|
-
});
|
|
59
|
-
return () => {
|
|
60
|
-
cancelled = true;
|
|
61
|
-
cleanups.forEach((cleanup) => cleanup());
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
4
|
// src/postMessageProtocol.ts
|
|
66
5
|
var MARKER = "__lssSheetSdk";
|
|
67
6
|
var VERSION = 2;
|
|
@@ -159,17 +98,6 @@ function createBridgeSheetSource(options) {
|
|
|
159
98
|
handlers.delete(wrapped);
|
|
160
99
|
};
|
|
161
100
|
},
|
|
162
|
-
onManifest(handler) {
|
|
163
|
-
const wrapped = (event) => {
|
|
164
|
-
if (event.type === "dnd:manifest") {
|
|
165
|
-
handler(event.payload);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
handlers.add(wrapped);
|
|
169
|
-
return () => {
|
|
170
|
-
handlers.delete(wrapped);
|
|
171
|
-
};
|
|
172
|
-
},
|
|
173
101
|
onEvent(handler) {
|
|
174
102
|
handlers.add(handler);
|
|
175
103
|
return () => {
|
|
@@ -188,4 +116,26 @@ function createBridgeSheetSource(options) {
|
|
|
188
116
|
};
|
|
189
117
|
}
|
|
190
118
|
|
|
191
|
-
|
|
119
|
+
// src/formatRoll.ts
|
|
120
|
+
function formatRollMessage(payload) {
|
|
121
|
+
let crit = "";
|
|
122
|
+
if (payload.isCrit) {
|
|
123
|
+
if (payload.critKind === "success") {
|
|
124
|
+
crit = " \u{1F4A5}";
|
|
125
|
+
} else if (payload.critKind === "failure") {
|
|
126
|
+
crit = " \u{1F480}";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return `\u{1F3B2} ${payload.characterName}: ${payload.title} \u2014 ${payload.total}${crit}`;
|
|
130
|
+
}
|
|
131
|
+
function rollVariant(payload) {
|
|
132
|
+
if (payload.isCrit && payload.critKind === "success") {
|
|
133
|
+
return "success";
|
|
134
|
+
}
|
|
135
|
+
if (payload.isCrit && payload.critKind === "failure") {
|
|
136
|
+
return "warning";
|
|
137
|
+
}
|
|
138
|
+
return "info";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { SHEET_IFRAME_SANDBOX, createBridgeSheetSource, createSheetClient, formatRollMessage, rollVariant };
|