@longstoryshort/vtt-sdk 0.3.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 +63 -14
- package/dist/adapters/owlbear/index.js +57 -68
- package/dist/{types-w2_82sqo.d.ts → formatRoll-BhFkInCu.d.ts} +7 -50
- package/dist/index.d.ts +11 -29
- package/dist/index.js +25 -72
- 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,16 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as SheetEvent, N as NotifyVariant } from '../../formatRoll-BhFkInCu.js';
|
|
2
|
+
import * as _owlbear_rodeo_sdk from '@owlbear-rodeo/sdk';
|
|
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
|
+
}
|
|
2
22
|
|
|
3
23
|
/**
|
|
4
|
-
* Owlbear Rodeo
|
|
24
|
+
* Owlbear Rodeo bridge helper.
|
|
5
25
|
*
|
|
6
26
|
* Owlbear exposes no public dice API (its 3D roller is first-party and closed),
|
|
7
|
-
* so rendering a roll is
|
|
8
|
-
* 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
|
|
9
29
|
* everyone at the table sees the floating number. All scene work is best-effort
|
|
10
30
|
* and degrades silently; the broadcast/notification path is the guaranteed core.
|
|
11
31
|
*/
|
|
12
|
-
declare class OwlbearAdapter implements
|
|
13
|
-
private sdk;
|
|
32
|
+
declare class OwlbearAdapter implements ObrAdapter {
|
|
14
33
|
private obr;
|
|
15
34
|
private user;
|
|
16
35
|
private sessionId;
|
|
@@ -19,13 +38,13 @@ declare class OwlbearAdapter implements VTTAdapter {
|
|
|
19
38
|
get isAvailable(): boolean;
|
|
20
39
|
ready(): Promise<boolean>;
|
|
21
40
|
private init;
|
|
22
|
-
private loadSdk;
|
|
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,9 +62,39 @@ 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
|
-
|
|
49
|
-
|
|
65
|
+
/** OBR room metadata key — false when Vortex is connected (logger active), true otherwise. */
|
|
66
|
+
declare const NOTIFY_ROLLS_KEY = "rodeo.lss/notify-rolls";
|
|
67
|
+
|
|
68
|
+
interface ObrLike {
|
|
69
|
+
onReady(cb: () => void): void;
|
|
70
|
+
}
|
|
71
|
+
/** Promisifies `OBR.onReady` — resolves once the OBR_READY handshake completes. */
|
|
72
|
+
declare function whenObrReady(obr: ObrLike): Promise<void>;
|
|
73
|
+
|
|
74
|
+
type OwlbearSdk = typeof _owlbear_rodeo_sdk;
|
|
75
|
+
/**
|
|
76
|
+
* The window shape expected by the SDK preload contract.
|
|
77
|
+
* Set `__lssObrSdk` at the app entry (before any navigation) so
|
|
78
|
+
* `loadObrSdk` can reuse the already-imported module and avoid missing
|
|
79
|
+
* the one-shot OBR_READY handshake.
|
|
80
|
+
*/
|
|
81
|
+
interface ObrPreloadWindow {
|
|
82
|
+
__lssObrSdk?: OwlbearSdk;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Call this at the bridge entry module — before any client-side navigation —
|
|
86
|
+
* to stash the already-imported OBR SDK so `loadObrSdk` can find it.
|
|
87
|
+
*/
|
|
88
|
+
declare function preloadObrSdk(sdk: OwlbearSdk): void;
|
|
89
|
+
/**
|
|
90
|
+
* Retrieves the OBR SDK for use inside an OBR extension frame.
|
|
91
|
+
*
|
|
92
|
+
* Prefers the copy stashed by `preloadObrSdk` (imported early enough to catch
|
|
93
|
+
* the one-shot OBR_READY handshake). Falls back to a dynamic import if the
|
|
94
|
+
* stash is absent, with a warning — the dynamic path may miss OBR_READY.
|
|
95
|
+
*
|
|
96
|
+
* Returns `null` when not running in a browser or when the import fails.
|
|
97
|
+
*/
|
|
98
|
+
declare function loadObrSdk(): Promise<OwlbearSdk | null>;
|
|
50
99
|
|
|
51
|
-
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";
|
|
@@ -44,8 +43,44 @@ function syncObrref() {
|
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
// src/adapters/owlbear/
|
|
46
|
+
// src/adapters/owlbear/loadObrSdk.ts
|
|
48
47
|
var DEV2 = process.env["NODE_ENV"] !== "production";
|
|
48
|
+
function preloadObrSdk(sdk) {
|
|
49
|
+
window.__lssObrSdk = sdk;
|
|
50
|
+
}
|
|
51
|
+
async function loadObrSdk() {
|
|
52
|
+
if (typeof window === "undefined") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const host = window;
|
|
56
|
+
for (let i = 0; i < 10 && !host.__lssObrSdk; i += 1) {
|
|
57
|
+
await new Promise((resolve) => {
|
|
58
|
+
window.setTimeout(resolve, 50);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (host.__lssObrSdk) {
|
|
62
|
+
return host.__lssObrSdk;
|
|
63
|
+
}
|
|
64
|
+
if (DEV2) {
|
|
65
|
+
console.warn("[LSS/OBR] no preloaded SDK \u2014 importing now (may miss OBR_READY)");
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return await import('@owlbear-rodeo/sdk');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("[LSS/OBR] SDK import failed:", error);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/adapters/owlbear/whenObrReady.ts
|
|
76
|
+
function whenObrReady(obr) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
obr.onReady(resolve);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/adapters/owlbear/OwlbearAdapter.ts
|
|
83
|
+
var DEV3 = process.env["NODE_ENV"] !== "production";
|
|
49
84
|
var NOTIFY_VARIANT = {
|
|
50
85
|
info: "INFO",
|
|
51
86
|
success: "SUCCESS",
|
|
@@ -54,7 +89,6 @@ var NOTIFY_VARIANT = {
|
|
|
54
89
|
};
|
|
55
90
|
var OwlbearAdapter = class {
|
|
56
91
|
constructor() {
|
|
57
|
-
this.sdk = null;
|
|
58
92
|
this.obr = null;
|
|
59
93
|
this.readyPromise = null;
|
|
60
94
|
this.disposed = false;
|
|
@@ -73,13 +107,12 @@ var OwlbearAdapter = class {
|
|
|
73
107
|
return false;
|
|
74
108
|
}
|
|
75
109
|
syncObrref();
|
|
76
|
-
const sdk = await
|
|
110
|
+
const sdk = await loadObrSdk();
|
|
77
111
|
if (!sdk) {
|
|
78
112
|
return false;
|
|
79
113
|
}
|
|
80
|
-
this.sdk = sdk;
|
|
81
114
|
this.obr = sdk.default;
|
|
82
|
-
if (
|
|
115
|
+
if (DEV3) {
|
|
83
116
|
console.info(
|
|
84
117
|
"[LSS/OBR] init \u2014 isAvailable:",
|
|
85
118
|
this.obr.isAvailable,
|
|
@@ -88,18 +121,16 @@ var OwlbearAdapter = class {
|
|
|
88
121
|
);
|
|
89
122
|
}
|
|
90
123
|
if (!this.obr.isAvailable) {
|
|
91
|
-
if (
|
|
124
|
+
if (DEV3) {
|
|
92
125
|
console.warn("[LSS/OBR] not embedded (origin empty) \u2014 obrref missing at SDK load.");
|
|
93
126
|
}
|
|
94
127
|
return false;
|
|
95
128
|
}
|
|
96
|
-
if (
|
|
129
|
+
if (DEV3) {
|
|
97
130
|
console.info("[LSS/OBR] awaiting onReady (isReady =", this.obr.isReady, ")");
|
|
98
131
|
}
|
|
99
|
-
await
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
if (DEV2) {
|
|
132
|
+
await whenObrReady(this.obr);
|
|
133
|
+
if (DEV3) {
|
|
103
134
|
console.info("[LSS/OBR] onReady fired \u2014 fetching player\u2026");
|
|
104
135
|
}
|
|
105
136
|
if (this.disposed) {
|
|
@@ -112,31 +143,11 @@ var OwlbearAdapter = class {
|
|
|
112
143
|
this.obr.player.getRole()
|
|
113
144
|
]);
|
|
114
145
|
this.user = { id, name, role: role === "GM" ? "gm" : "player" };
|
|
115
|
-
if (
|
|
146
|
+
if (DEV3) {
|
|
116
147
|
console.info("[LSS/OBR] ready \u2014 room:", this.sessionId, "user:", this.user);
|
|
117
148
|
}
|
|
118
149
|
return true;
|
|
119
150
|
}
|
|
120
|
-
async loadSdk() {
|
|
121
|
-
const host = window;
|
|
122
|
-
for (let i = 0; i < 10 && !host.__lssObrSdk; i += 1) {
|
|
123
|
-
await new Promise((resolve) => {
|
|
124
|
-
window.setTimeout(resolve, 50);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
if (host.__lssObrSdk) {
|
|
128
|
-
return host.__lssObrSdk;
|
|
129
|
-
}
|
|
130
|
-
if (DEV2) {
|
|
131
|
-
console.warn("[LSS/OBR] no preloaded SDK \u2014 importing now (may miss OBR_READY)");
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
return await import('@owlbear-rodeo/sdk');
|
|
135
|
-
} catch (error) {
|
|
136
|
-
console.error("[LSS/OBR] SDK import failed:", error);
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
151
|
getSessionId() {
|
|
141
152
|
return this.sessionId;
|
|
142
153
|
}
|
|
@@ -160,43 +171,21 @@ var OwlbearAdapter = class {
|
|
|
160
171
|
void this.obr?.notification.show(message, NOTIFY_VARIANT[variant]).catch(() => {
|
|
161
172
|
});
|
|
162
173
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
const [token] = await obr.scene.items.getItems(selection);
|
|
177
|
-
if (!token) {
|
|
178
|
-
if (DEV2) {
|
|
179
|
-
console.warn("[OwlbearAdapter] label skipped \u2014 selected item not found in scene");
|
|
180
|
-
}
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
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();
|
|
184
|
-
await obr.scene.items.addItems([label]);
|
|
185
|
-
window.setTimeout(() => {
|
|
186
|
-
void obr.scene.items.deleteItems([label.id]).catch(() => {
|
|
187
|
-
});
|
|
188
|
-
}, ttlMs);
|
|
189
|
-
return true;
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if (DEV2) {
|
|
192
|
-
console.warn("[OwlbearAdapter] label error:", error);
|
|
193
|
-
}
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
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);
|
|
196
185
|
}
|
|
197
186
|
dispose() {
|
|
198
187
|
this.disposed = true;
|
|
199
188
|
}
|
|
200
189
|
};
|
|
201
190
|
|
|
202
|
-
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,22 +1,12 @@
|
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - a roll on the sheet → local toast for the roller + broadcast to other
|
|
9
|
-
* clients + a transient label over the roller's selected token;
|
|
10
|
-
* - a roll broadcast by another client → local toast.
|
|
11
|
-
*
|
|
12
|
-
* Inbound capability wiring (`dnd:command`, `dnd:manifest`, `dnd:health`) is
|
|
13
|
-
* deliberately out of scope here — wire those directly via
|
|
14
|
-
* {@link BridgeSheetSource.onEvent} when your bridge needs them.
|
|
15
|
-
*
|
|
16
|
-
* Returns a dispose fn that tears down every subscription it created. The
|
|
17
|
-
* adapter is left untouched — it may be shared and longer-lived than the bridge.
|
|
5
|
+
* Minimum sandbox tokens required on the sheet iframe.
|
|
6
|
+
* `allow-same-origin` is essential — without it the sheet gets an opaque origin
|
|
7
|
+
* and auth (cookies / localStorage) breaks entirely.
|
|
18
8
|
*/
|
|
19
|
-
declare
|
|
9
|
+
declare const SHEET_IFRAME_SANDBOX = "allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals";
|
|
20
10
|
|
|
21
11
|
/**
|
|
22
12
|
* Sheet-side half of the postMessage transport. The character sheet (running in
|
|
@@ -46,11 +36,9 @@ interface BridgeSheetSourceOptions {
|
|
|
46
36
|
/** Origin to post inbound commands to. Default `'*'`. */
|
|
47
37
|
targetOrigin?: string;
|
|
48
38
|
}
|
|
49
|
-
/**
|
|
39
|
+
/** Bridge-side source: `onRoll` convenience + full `onEvent` access + inbound `send`. */
|
|
50
40
|
interface BridgeSheetSource extends SheetSource {
|
|
51
|
-
/** Subscribe to
|
|
52
|
-
onManifest(handler: (manifest: CapabilityManifest) => void): () => void;
|
|
53
|
-
/** Subscribe to every event coming from the sheet (not only rolls). */
|
|
41
|
+
/** Subscribe to every event coming from the sheet. Returns an unsubscribe fn. */
|
|
54
42
|
onEvent(handler: (event: SheetEvent) => void): () => void;
|
|
55
43
|
/** Post an inbound command to the sheet (e.g. `dnd:command`). */
|
|
56
44
|
send(event: SheetEvent): void;
|
|
@@ -58,18 +46,12 @@ interface BridgeSheetSource extends SheetSource {
|
|
|
58
46
|
}
|
|
59
47
|
/**
|
|
60
48
|
* Bridge-side half of the postMessage transport. Runs in the bridge frame (the
|
|
61
|
-
* VTT extension), listens to the embedded sheet iframe, and
|
|
62
|
-
* `
|
|
63
|
-
* 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.
|
|
64
51
|
*
|
|
65
52
|
* `contentWindow` is read live on every message/send, so it survives the sheet
|
|
66
53
|
* iframe navigating or reloading (e.g. Gatsby dev's full-reload on navigation).
|
|
67
54
|
*/
|
|
68
55
|
declare function createBridgeSheetSource(options: BridgeSheetSourceOptions): BridgeSheetSource;
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
declare function formatRollMessage(payload: DiceRollPayload): string;
|
|
72
|
-
/** Maps a roll's crit state onto a toast variant. */
|
|
73
|
-
declare function rollVariant(payload: DiceRollPayload): NotifyVariant;
|
|
74
|
-
|
|
75
|
-
export { type BridgeSheetSource, type BridgeSheetSourceOptions, CapabilityManifest, DiceRollPayload, MessageHost, NotifyVariant, RollBridgeOptions, 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,63 +1,5 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
let crit = "";
|
|
4
|
-
if (payload.isCrit) {
|
|
5
|
-
if (payload.critKind === "success") {
|
|
6
|
-
crit = " \u{1F4A5}";
|
|
7
|
-
} else if (payload.critKind === "failure") {
|
|
8
|
-
crit = " \u{1F480}";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return `\u{1F3B2} ${payload.characterName}: ${payload.title} \u2014 ${payload.total}${crit}`;
|
|
12
|
-
}
|
|
13
|
-
function rollVariant(payload) {
|
|
14
|
-
if (payload.isCrit && payload.critKind === "success") {
|
|
15
|
-
return "success";
|
|
16
|
-
}
|
|
17
|
-
if (payload.isCrit && payload.critKind === "failure") {
|
|
18
|
-
return "warning";
|
|
19
|
-
}
|
|
20
|
-
return "info";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// src/createRollBridge.ts
|
|
24
|
-
var DEFAULT_MESSAGES = {
|
|
25
|
-
connected: "\u{1F3B2} Sheet connected to the table",
|
|
26
|
-
labelHint: "No label placed \u2014 select exactly one of your tokens on the map"
|
|
27
|
-
};
|
|
28
|
-
function createRollBridge(source, adapter, options = {}) {
|
|
29
|
-
const messages = { ...DEFAULT_MESSAGES, ...options.messages };
|
|
30
|
-
const cleanups = [];
|
|
31
|
-
let cancelled = false;
|
|
32
|
-
cleanups.push(source.onRoll((roll) => {
|
|
33
|
-
if (!adapter.isAvailable) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
adapter.notify(formatRollMessage(roll), rollVariant(roll));
|
|
37
|
-
adapter.broadcast({ type: "dnd:roll", payload: roll });
|
|
38
|
-
void adapter.labelOverSelection(roll.total).then((placed) => {
|
|
39
|
-
if (!placed) {
|
|
40
|
-
adapter.notify(messages.labelHint, "warning");
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}));
|
|
44
|
-
void adapter.ready().then((available) => {
|
|
45
|
-
if (cancelled || !available) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
adapter.notify(messages.connected, "success");
|
|
49
|
-
cleanups.push(adapter.onEvent((event) => {
|
|
50
|
-
if (event.type !== "dnd:roll") {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
adapter.notify(formatRollMessage(event.payload), rollVariant(event.payload));
|
|
54
|
-
}));
|
|
55
|
-
});
|
|
56
|
-
return () => {
|
|
57
|
-
cancelled = true;
|
|
58
|
-
cleanups.forEach((cleanup) => cleanup());
|
|
59
|
-
};
|
|
60
|
-
}
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var SHEET_IFRAME_SANDBOX = "allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals";
|
|
61
3
|
|
|
62
4
|
// src/postMessageProtocol.ts
|
|
63
5
|
var MARKER = "__lssSheetSdk";
|
|
@@ -156,17 +98,6 @@ function createBridgeSheetSource(options) {
|
|
|
156
98
|
handlers.delete(wrapped);
|
|
157
99
|
};
|
|
158
100
|
},
|
|
159
|
-
onManifest(handler) {
|
|
160
|
-
const wrapped = (event) => {
|
|
161
|
-
if (event.type === "dnd:manifest") {
|
|
162
|
-
handler(event.payload);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
handlers.add(wrapped);
|
|
166
|
-
return () => {
|
|
167
|
-
handlers.delete(wrapped);
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
101
|
onEvent(handler) {
|
|
171
102
|
handlers.add(handler);
|
|
172
103
|
return () => {
|
|
@@ -185,4 +116,26 @@ function createBridgeSheetSource(options) {
|
|
|
185
116
|
};
|
|
186
117
|
}
|
|
187
118
|
|
|
188
|
-
|
|
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 };
|