@smoregg/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/dist/cjs/SmoreHost.cjs +306 -0
- package/dist/cjs/SmoreHost.cjs.map +1 -0
- package/dist/cjs/SmorePlayer.cjs +229 -0
- package/dist/cjs/SmorePlayer.cjs.map +1 -0
- package/dist/cjs/components/IframeGameBridge.cjs +115 -0
- package/dist/cjs/components/IframeGameBridge.cjs.map +1 -0
- package/dist/cjs/context/RoomProvider.cjs +3 -3
- package/dist/cjs/context/RoomProvider.cjs.map +1 -1
- package/dist/cjs/hooks/useGameHost.cjs +91 -14
- package/dist/cjs/hooks/useGameHost.cjs.map +1 -1
- package/dist/cjs/hooks/useGamePlayer.cjs +65 -6
- package/dist/cjs/hooks/useGamePlayer.cjs.map +1 -1
- package/dist/cjs/iframe/index.cjs +58 -315
- package/dist/cjs/iframe/index.cjs.map +1 -1
- package/dist/cjs/index.cjs +4 -22
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/utils/connectionMonitor.cjs +77 -0
- package/dist/cjs/utils/connectionMonitor.cjs.map +1 -0
- package/dist/cjs/utils/preloadAssets.cjs +66 -0
- package/dist/cjs/utils/preloadAssets.cjs.map +1 -0
- package/dist/cjs/utils/serverTime.cjs +43 -0
- package/dist/cjs/utils/serverTime.cjs.map +1 -0
- package/dist/esm/SmoreHost.js +304 -0
- package/dist/esm/SmoreHost.js.map +1 -0
- package/dist/esm/SmorePlayer.js +227 -0
- package/dist/esm/SmorePlayer.js.map +1 -0
- package/dist/esm/components/IframeGameBridge.js +113 -0
- package/dist/esm/components/IframeGameBridge.js.map +1 -0
- package/dist/esm/context/RoomProvider.js +3 -3
- package/dist/esm/context/RoomProvider.js.map +1 -1
- package/dist/esm/hooks/useGameHost.js +92 -15
- package/dist/esm/hooks/useGameHost.js.map +1 -1
- package/dist/esm/hooks/useGamePlayer.js +66 -7
- package/dist/esm/hooks/useGamePlayer.js.map +1 -1
- package/dist/esm/iframe/index.js +59 -313
- package/dist/esm/iframe/index.js.map +1 -1
- package/dist/esm/index.js +2 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/utils/connectionMonitor.js +75 -0
- package/dist/esm/utils/connectionMonitor.js.map +1 -0
- package/dist/esm/utils/preloadAssets.js +63 -0
- package/dist/esm/utils/preloadAssets.js.map +1 -0
- package/dist/esm/utils/serverTime.js +41 -0
- package/dist/esm/utils/serverTime.js.map +1 -0
- package/dist/types/SmoreHost.d.ts +187 -0
- package/dist/types/SmoreHost.d.ts.map +1 -0
- package/dist/types/SmorePlayer.d.ts +146 -0
- package/dist/types/SmorePlayer.d.ts.map +1 -0
- package/dist/types/components/IframeGameBridge.d.ts +2 -2
- package/dist/types/components/IframeGameBridge.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +2 -4
- package/dist/types/components/index.d.ts.map +1 -1
- package/dist/types/context/RoomProvider.d.ts +3 -3
- package/dist/types/context/RoomProvider.d.ts.map +1 -1
- package/dist/types/hooks/useGameHost.d.ts +33 -7
- package/dist/types/hooks/useGameHost.d.ts.map +1 -1
- package/dist/types/hooks/useGamePlayer.d.ts +29 -3
- package/dist/types/hooks/useGamePlayer.d.ts.map +1 -1
- package/dist/types/iframe/index.d.ts +10 -10
- package/dist/types/iframe/index.d.ts.map +1 -1
- package/dist/types/iframe/vanilla.d.ts +12 -4
- package/dist/types/iframe/vanilla.d.ts.map +1 -1
- package/dist/types/index.d.ts +36 -20
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +1 -1
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/utils/connectionMonitor.d.ts +57 -0
- package/dist/types/utils/connectionMonitor.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +7 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/preloadAssets.d.ts +29 -0
- package/dist/types/utils/preloadAssets.d.ts.map +1 -0
- package/dist/types/utils/serverTime.d.ts +28 -0
- package/dist/types/utils/serverTime.d.ts.map +1 -0
- package/dist/umd/smore-sdk-iframe.umd.js +62 -316
- package/dist/umd/smore-sdk-iframe.umd.js.map +1 -1
- package/dist/umd/smore-sdk-iframe.umd.min.js +1 -1
- package/dist/umd/smore-sdk-iframe.umd.min.js.map +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.js +553 -127
- package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
- package/dist/umd/smore-sdk.umd.js +496 -577
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +1 -26
|
@@ -1,260 +1,8 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports'
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SmoreSDK = {}
|
|
5
|
-
})(this, (function (exports
|
|
6
|
-
|
|
7
|
-
function styleInject(css, ref) {
|
|
8
|
-
if ( ref === void 0 ) ref = {};
|
|
9
|
-
var insertAt = ref.insertAt;
|
|
10
|
-
|
|
11
|
-
if (!css || typeof document === 'undefined') { return; }
|
|
12
|
-
|
|
13
|
-
var head = document.head || document.getElementsByTagName('head')[0];
|
|
14
|
-
var style = document.createElement('style');
|
|
15
|
-
style.type = 'text/css';
|
|
16
|
-
|
|
17
|
-
if (insertAt === 'top') {
|
|
18
|
-
if (head.firstChild) {
|
|
19
|
-
head.insertBefore(style, head.firstChild);
|
|
20
|
-
} else {
|
|
21
|
-
head.appendChild(style);
|
|
22
|
-
}
|
|
23
|
-
} else {
|
|
24
|
-
head.appendChild(style);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (style.styleSheet) {
|
|
28
|
-
style.styleSheet.cssText = css;
|
|
29
|
-
} else {
|
|
30
|
-
style.appendChild(document.createTextNode(css));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
var css_248z$3 = ".TapButton-module_tapButton__dqJr9{-webkit-tap-highlight-color:transparent;align-items:center;background:var(--color-primary,#6366f1);border:none;border-radius:16px;color:#fff;cursor:pointer;display:flex;font-size:18px;font-weight:700;justify-content:center;min-height:80px;min-width:80px;padding:16px 24px;touch-action:manipulation;transition:transform .1s ease,background .1s ease;user-select:none;-webkit-user-select:none}.TapButton-module_pressed__Z2nfR,.TapButton-module_tapButton__dqJr9:active{background:var(--color-primary-dark,#4f46e5);transform:scale(.95)}.TapButton-module_disabled__3bR6q{cursor:not-allowed;opacity:.5}.TapButton-module_disabled__3bR6q.TapButton-module_pressed__Z2nfR,.TapButton-module_disabled__3bR6q:active{background:var(--color-primary,#6366f1);transform:none}";
|
|
35
|
-
var styles$3 = {"tapButton":"TapButton-module_tapButton__dqJr9","pressed":"TapButton-module_pressed__Z2nfR","disabled":"TapButton-module_disabled__3bR6q"};
|
|
36
|
-
styleInject(css_248z$3);
|
|
37
|
-
|
|
38
|
-
function TapButton({
|
|
39
|
-
onTap,
|
|
40
|
-
children,
|
|
41
|
-
className,
|
|
42
|
-
disabled = false
|
|
43
|
-
}) {
|
|
44
|
-
const isPressed = react.useRef(false);
|
|
45
|
-
const buttonRef = react.useRef(null);
|
|
46
|
-
const handleTouchStart = react.useCallback((e) => {
|
|
47
|
-
if (disabled) return;
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
isPressed.current = true;
|
|
50
|
-
buttonRef.current?.classList.add(styles$3.pressed);
|
|
51
|
-
if (navigator.vibrate) {
|
|
52
|
-
navigator.vibrate(10);
|
|
53
|
-
}
|
|
54
|
-
onTap();
|
|
55
|
-
}, [onTap, disabled]);
|
|
56
|
-
const handleTouchEnd = react.useCallback((e) => {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
isPressed.current = false;
|
|
59
|
-
buttonRef.current?.classList.remove(styles$3.pressed);
|
|
60
|
-
}, []);
|
|
61
|
-
const handleMouseDown = react.useCallback(() => {
|
|
62
|
-
if (disabled) return;
|
|
63
|
-
isPressed.current = true;
|
|
64
|
-
buttonRef.current?.classList.add(styles$3.pressed);
|
|
65
|
-
onTap();
|
|
66
|
-
}, [onTap, disabled]);
|
|
67
|
-
const handleMouseUp = react.useCallback(() => {
|
|
68
|
-
isPressed.current = false;
|
|
69
|
-
buttonRef.current?.classList.remove(styles$3.pressed);
|
|
70
|
-
}, []);
|
|
71
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
72
|
-
"button",
|
|
73
|
-
{
|
|
74
|
-
ref: buttonRef,
|
|
75
|
-
className: `${styles$3.tapButton} ${className || ""} ${disabled ? styles$3.disabled : ""}`,
|
|
76
|
-
onTouchStart: handleTouchStart,
|
|
77
|
-
onTouchEnd: handleTouchEnd,
|
|
78
|
-
onTouchCancel: handleTouchEnd,
|
|
79
|
-
onMouseDown: handleMouseDown,
|
|
80
|
-
onMouseUp: handleMouseUp,
|
|
81
|
-
onMouseLeave: handleMouseUp,
|
|
82
|
-
disabled,
|
|
83
|
-
children
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
var css_248z$2 = ".HoldButton-module_holdButton__tu4mi{-webkit-tap-highlight-color:transparent;align-items:center;background:var(--color-secondary,#f59e0b);border:none;border-radius:16px;color:#fff;cursor:pointer;display:flex;font-size:18px;font-weight:700;justify-content:center;min-height:80px;min-width:80px;padding:16px 24px;touch-action:manipulation;transition:transform .1s ease,background .1s ease;user-select:none;-webkit-user-select:none}.HoldButton-module_holdButton__tu4mi:active,.HoldButton-module_pressed__JWIRG{background:var(--color-secondary-dark,#d97706);transform:scale(.95)}.HoldButton-module_disabled__4QwwT{cursor:not-allowed;opacity:.5}.HoldButton-module_disabled__4QwwT.HoldButton-module_pressed__JWIRG,.HoldButton-module_disabled__4QwwT:active{background:var(--color-secondary,#f59e0b);transform:none}";
|
|
89
|
-
var styles$2 = {"holdButton":"HoldButton-module_holdButton__tu4mi","pressed":"HoldButton-module_pressed__JWIRG","disabled":"HoldButton-module_disabled__4QwwT"};
|
|
90
|
-
styleInject(css_248z$2);
|
|
91
|
-
|
|
92
|
-
function HoldButton({
|
|
93
|
-
onHoldStart,
|
|
94
|
-
onHoldEnd,
|
|
95
|
-
children,
|
|
96
|
-
className,
|
|
97
|
-
disabled = false
|
|
98
|
-
}) {
|
|
99
|
-
const isHolding = react.useRef(false);
|
|
100
|
-
const buttonRef = react.useRef(null);
|
|
101
|
-
const startHold = react.useCallback(() => {
|
|
102
|
-
if (disabled || isHolding.current) return;
|
|
103
|
-
isHolding.current = true;
|
|
104
|
-
buttonRef.current?.classList.add(styles$2.pressed);
|
|
105
|
-
if (navigator.vibrate) {
|
|
106
|
-
navigator.vibrate(10);
|
|
107
|
-
}
|
|
108
|
-
onHoldStart();
|
|
109
|
-
}, [onHoldStart, disabled]);
|
|
110
|
-
const endHold = react.useCallback(() => {
|
|
111
|
-
if (!isHolding.current) return;
|
|
112
|
-
isHolding.current = false;
|
|
113
|
-
buttonRef.current?.classList.remove(styles$2.pressed);
|
|
114
|
-
onHoldEnd();
|
|
115
|
-
}, [onHoldEnd]);
|
|
116
|
-
const handleTouchStart = react.useCallback((e) => {
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
startHold();
|
|
119
|
-
}, [startHold]);
|
|
120
|
-
const handleTouchEnd = react.useCallback((e) => {
|
|
121
|
-
e.preventDefault();
|
|
122
|
-
endHold();
|
|
123
|
-
}, [endHold]);
|
|
124
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
125
|
-
"button",
|
|
126
|
-
{
|
|
127
|
-
ref: buttonRef,
|
|
128
|
-
className: `${styles$2.holdButton} ${className || ""} ${disabled ? styles$2.disabled : ""}`,
|
|
129
|
-
onTouchStart: handleTouchStart,
|
|
130
|
-
onTouchEnd: handleTouchEnd,
|
|
131
|
-
onTouchCancel: handleTouchEnd,
|
|
132
|
-
onMouseDown: startHold,
|
|
133
|
-
onMouseUp: endHold,
|
|
134
|
-
onMouseLeave: endHold,
|
|
135
|
-
disabled,
|
|
136
|
-
children
|
|
137
|
-
}
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
var css_248z$1 = ".DirectionPad-module_padContainer__iL-rh{align-items:center;display:flex;flex-direction:column;gap:8px;user-select:none;-webkit-user-select:none}.DirectionPad-module_horizontal__kI4j7{flex-direction:row;gap:16px}.DirectionPad-module_vertical__b8Xec{flex-direction:column;gap:16px}.DirectionPad-module_row__mzuUr{display:flex;gap:8px}.DirectionPad-module_dirButton__QCCHz{-webkit-tap-highlight-color:transparent;align-items:center;background:var(--color-surface,#374151);border:none;border-radius:12px;color:#fff;cursor:pointer;display:flex;font-size:24px;height:70px;justify-content:center;touch-action:manipulation;transition:transform .1s ease,background .1s ease;width:70px}.DirectionPad-module_dirButton__QCCHz:active{background:var(--color-primary,#6366f1);transform:scale(.9)}.DirectionPad-module_horizontal__kI4j7 .DirectionPad-module_dirButton__QCCHz,.DirectionPad-module_vertical__b8Xec .DirectionPad-module_dirButton__QCCHz{font-size:32px;height:100px;width:100px}";
|
|
142
|
-
var styles$1 = {"padContainer":"DirectionPad-module_padContainer__iL-rh","horizontal":"DirectionPad-module_horizontal__kI4j7","vertical":"DirectionPad-module_vertical__b8Xec","row":"DirectionPad-module_row__mzuUr","dirButton":"DirectionPad-module_dirButton__QCCHz"};
|
|
143
|
-
styleInject(css_248z$1);
|
|
144
|
-
|
|
145
|
-
function DirectionPad({
|
|
146
|
-
onDirection,
|
|
147
|
-
leftRightOnly = false,
|
|
148
|
-
upDownOnly = false,
|
|
149
|
-
className
|
|
150
|
-
}) {
|
|
151
|
-
const pressedRef = react.useRef(/* @__PURE__ */ new Set());
|
|
152
|
-
const handlePress = react.useCallback((direction) => {
|
|
153
|
-
if (pressedRef.current.has(direction)) return;
|
|
154
|
-
pressedRef.current.add(direction);
|
|
155
|
-
if (navigator.vibrate) {
|
|
156
|
-
navigator.vibrate(10);
|
|
157
|
-
}
|
|
158
|
-
onDirection(direction);
|
|
159
|
-
}, [onDirection]);
|
|
160
|
-
const handleRelease = react.useCallback((direction) => {
|
|
161
|
-
pressedRef.current.delete(direction);
|
|
162
|
-
}, []);
|
|
163
|
-
const createButton = (direction, label) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
164
|
-
"button",
|
|
165
|
-
{
|
|
166
|
-
className: `${styles$1.dirButton} ${styles$1[direction]}`,
|
|
167
|
-
onTouchStart: (e) => {
|
|
168
|
-
e.preventDefault();
|
|
169
|
-
handlePress(direction);
|
|
170
|
-
},
|
|
171
|
-
onTouchEnd: (e) => {
|
|
172
|
-
e.preventDefault();
|
|
173
|
-
handleRelease(direction);
|
|
174
|
-
},
|
|
175
|
-
onTouchCancel: () => handleRelease(direction),
|
|
176
|
-
onMouseDown: () => handlePress(direction),
|
|
177
|
-
onMouseUp: () => handleRelease(direction),
|
|
178
|
-
onMouseLeave: () => handleRelease(direction),
|
|
179
|
-
children: label
|
|
180
|
-
},
|
|
181
|
-
direction
|
|
182
|
-
);
|
|
183
|
-
if (leftRightOnly) {
|
|
184
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${styles$1.padContainer} ${styles$1.horizontal} ${className || ""}`, children: [
|
|
185
|
-
createButton("left", "\u25C0"),
|
|
186
|
-
createButton("right", "\u25B6")
|
|
187
|
-
] });
|
|
188
|
-
}
|
|
189
|
-
if (upDownOnly) {
|
|
190
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${styles$1.padContainer} ${styles$1.vertical} ${className || ""}`, children: [
|
|
191
|
-
createButton("up", "\u25B2"),
|
|
192
|
-
createButton("down", "\u25BC")
|
|
193
|
-
] });
|
|
194
|
-
}
|
|
195
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${styles$1.padContainer} ${styles$1.full} ${className || ""}`, children: [
|
|
196
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles$1.row, children: createButton("up", "\u25B2") }),
|
|
197
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles$1.row, children: [
|
|
198
|
-
createButton("left", "\u25C0"),
|
|
199
|
-
createButton("right", "\u25B6")
|
|
200
|
-
] }),
|
|
201
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: styles$1.row, children: createButton("down", "\u25BC") })
|
|
202
|
-
] });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
var css_248z = ".SwipeArea-module_swipeArea__yob7L{height:100%;touch-action:none;user-select:none;-webkit-user-select:none;width:100%}";
|
|
206
|
-
var styles = {"swipeArea":"SwipeArea-module_swipeArea__yob7L"};
|
|
207
|
-
styleInject(css_248z);
|
|
208
|
-
|
|
209
|
-
function SwipeArea({
|
|
210
|
-
onSwipe,
|
|
211
|
-
threshold = 50,
|
|
212
|
-
children,
|
|
213
|
-
className
|
|
214
|
-
}) {
|
|
215
|
-
const touchStart = react.useRef(null);
|
|
216
|
-
const handleTouchStart = react.useCallback((e) => {
|
|
217
|
-
const touch = e.touches[0];
|
|
218
|
-
if (!touch) return;
|
|
219
|
-
touchStart.current = { x: touch.clientX, y: touch.clientY };
|
|
220
|
-
}, []);
|
|
221
|
-
const handleTouchEnd = react.useCallback((e) => {
|
|
222
|
-
if (!touchStart.current) return;
|
|
223
|
-
const touch = e.changedTouches[0];
|
|
224
|
-
if (!touch) return;
|
|
225
|
-
const deltaX = touch.clientX - touchStart.current.x;
|
|
226
|
-
const deltaY = touch.clientY - touchStart.current.y;
|
|
227
|
-
const absX = Math.abs(deltaX);
|
|
228
|
-
const absY = Math.abs(deltaY);
|
|
229
|
-
if (absX < threshold && absY < threshold) {
|
|
230
|
-
touchStart.current = null;
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
let direction;
|
|
234
|
-
if (absX > absY) {
|
|
235
|
-
direction = deltaX > 0 ? "right" : "left";
|
|
236
|
-
} else {
|
|
237
|
-
direction = deltaY > 0 ? "down" : "up";
|
|
238
|
-
}
|
|
239
|
-
if (navigator.vibrate) {
|
|
240
|
-
navigator.vibrate(15);
|
|
241
|
-
}
|
|
242
|
-
onSwipe(direction);
|
|
243
|
-
touchStart.current = null;
|
|
244
|
-
}, [onSwipe, threshold]);
|
|
245
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
246
|
-
"div",
|
|
247
|
-
{
|
|
248
|
-
className: `${styles.swipeArea} ${className || ""}`,
|
|
249
|
-
onTouchStart: handleTouchStart,
|
|
250
|
-
onTouchEnd: handleTouchEnd,
|
|
251
|
-
onTouchCancel: () => {
|
|
252
|
-
touchStart.current = null;
|
|
253
|
-
},
|
|
254
|
-
children
|
|
255
|
-
}
|
|
256
|
-
);
|
|
257
|
-
}
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SmoreSDK = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
258
6
|
|
|
259
7
|
class DirectTransport {
|
|
260
8
|
constructor(socket) {
|
|
@@ -275,109 +23,6 @@
|
|
|
275
23
|
}
|
|
276
24
|
}
|
|
277
25
|
|
|
278
|
-
const TransportContext = react.createContext(null);
|
|
279
|
-
function useTransport() {
|
|
280
|
-
const transport = react.useContext(TransportContext);
|
|
281
|
-
if (!transport) {
|
|
282
|
-
throw new Error("useTransport must be used within a RoomProvider that supplies a Transport");
|
|
283
|
-
}
|
|
284
|
-
return transport;
|
|
285
|
-
}
|
|
286
|
-
const RoomContext = react.createContext(null);
|
|
287
|
-
const HostRoomProvider = ({
|
|
288
|
-
roomCode,
|
|
289
|
-
players,
|
|
290
|
-
leaderId,
|
|
291
|
-
socket,
|
|
292
|
-
children
|
|
293
|
-
}) => {
|
|
294
|
-
const connectedPlayers = react.useMemo(
|
|
295
|
-
() => players.filter((p) => p.connected !== false),
|
|
296
|
-
[players]
|
|
297
|
-
);
|
|
298
|
-
const hostState = react.useMemo(
|
|
299
|
-
() => ({ roomCode, players, connectedPlayers, leaderId, socket }),
|
|
300
|
-
[roomCode, players, connectedPlayers, leaderId, socket]
|
|
301
|
-
);
|
|
302
|
-
const value = react.useMemo(
|
|
303
|
-
() => ({
|
|
304
|
-
roomCode,
|
|
305
|
-
players,
|
|
306
|
-
connectedPlayers,
|
|
307
|
-
leaderId,
|
|
308
|
-
side: "host",
|
|
309
|
-
host: hostState,
|
|
310
|
-
player: null
|
|
311
|
-
}),
|
|
312
|
-
[roomCode, players, connectedPlayers, leaderId, hostState]
|
|
313
|
-
);
|
|
314
|
-
const transport = react.useMemo(() => new DirectTransport(socket), [socket]);
|
|
315
|
-
return /* @__PURE__ */ jsxRuntime.jsx(TransportContext.Provider, { value: transport, children: /* @__PURE__ */ jsxRuntime.jsx(RoomContext.Provider, { value, children }) });
|
|
316
|
-
};
|
|
317
|
-
const PlayerRoomProvider = ({
|
|
318
|
-
roomCode,
|
|
319
|
-
players,
|
|
320
|
-
leaderId,
|
|
321
|
-
mySessionId,
|
|
322
|
-
isLeader,
|
|
323
|
-
socket,
|
|
324
|
-
isConnected,
|
|
325
|
-
children
|
|
326
|
-
}) => {
|
|
327
|
-
const connectedPlayers = react.useMemo(
|
|
328
|
-
() => players.filter((p) => p.connected !== false),
|
|
329
|
-
[players]
|
|
330
|
-
);
|
|
331
|
-
const playerState = react.useMemo(
|
|
332
|
-
() => ({
|
|
333
|
-
roomCode,
|
|
334
|
-
players,
|
|
335
|
-
connectedPlayers,
|
|
336
|
-
leaderId,
|
|
337
|
-
mySessionId,
|
|
338
|
-
isLeader,
|
|
339
|
-
socket,
|
|
340
|
-
isConnected
|
|
341
|
-
}),
|
|
342
|
-
[roomCode, players, connectedPlayers, leaderId, mySessionId, isLeader, socket, isConnected]
|
|
343
|
-
);
|
|
344
|
-
const value = react.useMemo(
|
|
345
|
-
() => ({
|
|
346
|
-
roomCode,
|
|
347
|
-
players,
|
|
348
|
-
connectedPlayers,
|
|
349
|
-
leaderId,
|
|
350
|
-
side: "player",
|
|
351
|
-
host: null,
|
|
352
|
-
player: playerState
|
|
353
|
-
}),
|
|
354
|
-
[roomCode, players, connectedPlayers, leaderId, playerState]
|
|
355
|
-
);
|
|
356
|
-
const transport = react.useMemo(() => new DirectTransport(socket), [socket]);
|
|
357
|
-
return /* @__PURE__ */ jsxRuntime.jsx(TransportContext.Provider, { value: transport, children: /* @__PURE__ */ jsxRuntime.jsx(RoomContext.Provider, { value, children }) });
|
|
358
|
-
};
|
|
359
|
-
function useRoom() {
|
|
360
|
-
const context = react.useContext(RoomContext);
|
|
361
|
-
if (!context) {
|
|
362
|
-
throw new Error("useRoom must be used within HostRoomProvider or PlayerRoomProvider");
|
|
363
|
-
}
|
|
364
|
-
return context;
|
|
365
|
-
}
|
|
366
|
-
function useHostRoom() {
|
|
367
|
-
const context = useRoom();
|
|
368
|
-
if (context.side !== "host" || !context.host) {
|
|
369
|
-
throw new Error("useHostRoom must be used within HostRoomProvider");
|
|
370
|
-
}
|
|
371
|
-
return context.host;
|
|
372
|
-
}
|
|
373
|
-
function usePlayerRoom() {
|
|
374
|
-
const context = useRoom();
|
|
375
|
-
if (context.side !== "player" || !context.player) {
|
|
376
|
-
throw new Error("usePlayerRoom must be used within PlayerRoomProvider");
|
|
377
|
-
}
|
|
378
|
-
return context.player;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
26
|
const SMORE_MSG_PREFIX = "smore:";
|
|
382
27
|
function isSmoreMessage(data) {
|
|
383
28
|
return data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith(SMORE_MSG_PREFIX);
|
|
@@ -449,237 +94,523 @@
|
|
|
449
94
|
}
|
|
450
95
|
}
|
|
451
96
|
|
|
452
|
-
const SYSTEM_PREFIX = "smore:";
|
|
453
|
-
const SYSTEM_EVENTS = {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
GAME_OVER: `${SYSTEM_PREFIX}game-over`,
|
|
458
|
-
RETURN_TO_LOBBY: `${SYSTEM_PREFIX}return-to-lobby`,
|
|
459
|
-
SEND_TO_PLAYER: `${SYSTEM_PREFIX}send-to-player`,
|
|
460
|
-
BROADCAST: `${SYSTEM_PREFIX}broadcast`
|
|
97
|
+
const SYSTEM_PREFIX$1 = "smore:";
|
|
98
|
+
const SYSTEM_EVENTS$1 = {
|
|
99
|
+
PLAYER_JOIN: `${SYSTEM_PREFIX$1}player-join`,
|
|
100
|
+
PLAYER_LEAVE: `${SYSTEM_PREFIX$1}player-leave`,
|
|
101
|
+
GAME_OVER: `${SYSTEM_PREFIX$1}game-over`
|
|
461
102
|
};
|
|
103
|
+
const EVENT_NAME_REGEX$1 = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
|
|
462
104
|
function validateEventName$1(event) {
|
|
463
|
-
if (
|
|
105
|
+
if (!EVENT_NAME_REGEX$1.test(event)) {
|
|
464
106
|
throw new Error(
|
|
465
|
-
`[
|
|
107
|
+
`[SmoreHost] Invalid event name "${event}". Event names must:
|
|
108
|
+
- Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
|
|
109
|
+
- Start and end with a letter (no leading/trailing - or _)`
|
|
466
110
|
);
|
|
467
111
|
}
|
|
468
112
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
113
|
+
class SmoreHost {
|
|
114
|
+
transport = null;
|
|
115
|
+
config;
|
|
116
|
+
_players = [];
|
|
117
|
+
_roomCode = "";
|
|
118
|
+
_leaderIndex = -1;
|
|
119
|
+
_isReady = false;
|
|
120
|
+
_isDestroyed = false;
|
|
121
|
+
boundMessageHandler = null;
|
|
122
|
+
registeredHandlers = [];
|
|
123
|
+
constructor(config = {}) {
|
|
124
|
+
this.config = config;
|
|
125
|
+
if (config.listeners) {
|
|
126
|
+
for (const event of Object.keys(config.listeners)) {
|
|
127
|
+
validateEventName$1(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (config.socket) {
|
|
131
|
+
this.initBundled(config);
|
|
132
|
+
} else {
|
|
133
|
+
this.initIframe(config);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Initialization
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
initBundled(config) {
|
|
140
|
+
if (!config.socket) {
|
|
141
|
+
throw new Error("[SmoreHost] socket is required for bundled games");
|
|
142
|
+
}
|
|
143
|
+
this.transport = new DirectTransport(config.socket);
|
|
144
|
+
this._roomCode = config.roomCode || "";
|
|
145
|
+
this._players = config.players || [];
|
|
146
|
+
this._leaderIndex = config.leaderIndex ?? -1;
|
|
147
|
+
this.setupEventHandlers();
|
|
148
|
+
this._isReady = true;
|
|
149
|
+
this.config.onReady?.();
|
|
150
|
+
}
|
|
151
|
+
initIframe(config) {
|
|
152
|
+
const parentOrigin = config.parentOrigin || "*";
|
|
153
|
+
window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
|
|
154
|
+
this.boundMessageHandler = (e) => {
|
|
155
|
+
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
156
|
+
const msg = e.data;
|
|
157
|
+
if (!isSmoreMessage(msg)) return;
|
|
158
|
+
if (msg.type === "smore:init") {
|
|
159
|
+
const initData = msg.payload;
|
|
160
|
+
if (initData.side !== "host") {
|
|
161
|
+
console.error("[SmoreHost] Received init for wrong side:", initData.side);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.transport = new PostMessageTransport(parentOrigin);
|
|
165
|
+
this._roomCode = initData.roomCode;
|
|
166
|
+
this._players = this.mapPlayersFromInit(initData.players);
|
|
167
|
+
this._leaderIndex = this.findLeaderIndex(initData.players, initData.leaderId);
|
|
168
|
+
this.setupEventHandlers();
|
|
169
|
+
this._isReady = true;
|
|
170
|
+
this.config.onReady?.();
|
|
171
|
+
} else if (msg.type === "smore:update") {
|
|
172
|
+
const updateData = msg.payload;
|
|
173
|
+
if (updateData.players) {
|
|
174
|
+
this._players = this.mapPlayersFromInit(updateData.players);
|
|
175
|
+
}
|
|
176
|
+
if (updateData.leaderId !== void 0) {
|
|
177
|
+
this._leaderIndex = this.findLeaderIndex(this._players, updateData.leaderId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
499
180
|
};
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
181
|
+
window.addEventListener("message", this.boundMessageHandler);
|
|
182
|
+
}
|
|
183
|
+
mapPlayersFromInit(players) {
|
|
184
|
+
return players.map((p, index) => ({
|
|
185
|
+
playerIndex: p.playerIndex ?? index,
|
|
186
|
+
nickname: p.nickname || `Player ${index + 1}`,
|
|
187
|
+
connected: p.connected !== false,
|
|
188
|
+
appearance: p.appearance
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
findLeaderIndex(players, leaderId) {
|
|
192
|
+
if (!leaderId) return -1;
|
|
193
|
+
const idx = players.findIndex((p) => p.sessionId === leaderId);
|
|
194
|
+
return idx >= 0 ? idx : -1;
|
|
195
|
+
}
|
|
196
|
+
setupEventHandlers() {
|
|
197
|
+
if (!this.transport) return;
|
|
198
|
+
this.registerHandler(SYSTEM_EVENTS$1.PLAYER_JOIN, (data) => {
|
|
199
|
+
const playerIndex = data.player?.playerIndex;
|
|
200
|
+
if (playerIndex !== void 0) {
|
|
201
|
+
this.config.onPlayerJoin?.(playerIndex);
|
|
202
|
+
}
|
|
505
203
|
});
|
|
506
|
-
|
|
507
|
-
if (data
|
|
508
|
-
|
|
204
|
+
this.registerHandler(SYSTEM_EVENTS$1.PLAYER_LEAVE, (data) => {
|
|
205
|
+
if (data.playerIndex !== void 0) {
|
|
206
|
+
this.config.onPlayerLeave?.(data.playerIndex);
|
|
509
207
|
}
|
|
510
208
|
});
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
209
|
+
this.registerHandler("room:player-joined", (data) => {
|
|
210
|
+
const playerIndex = data?.player?.playerIndex ?? data?.playerIndex;
|
|
211
|
+
if (playerIndex !== void 0) {
|
|
212
|
+
this.config.onPlayerJoin?.(playerIndex);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
this.registerHandler("room:player-left", (data) => {
|
|
216
|
+
const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;
|
|
217
|
+
if (playerIndex !== void 0) {
|
|
218
|
+
this.config.onPlayerLeave?.(playerIndex);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
if (this.config.listeners) {
|
|
222
|
+
for (const [event, handler] of Object.entries(this.config.listeners)) {
|
|
223
|
+
if (!handler) continue;
|
|
224
|
+
this.registerHandler(event, (data) => {
|
|
225
|
+
const { playerIndex, ...rest } = data;
|
|
226
|
+
if (playerIndex !== void 0) {
|
|
227
|
+
handler(playerIndex, rest);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
registerHandler(event, handler) {
|
|
234
|
+
if (!this.transport) return;
|
|
235
|
+
this.transport.on(event, handler);
|
|
236
|
+
this.registeredHandlers.push({ event, handler });
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Public Properties
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
/**
|
|
242
|
+
* Get all players in the room.
|
|
243
|
+
* Returns a copy to prevent external mutation.
|
|
244
|
+
*/
|
|
245
|
+
get players() {
|
|
246
|
+
return [...this._players];
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get the room code.
|
|
250
|
+
*/
|
|
251
|
+
get roomCode() {
|
|
252
|
+
return this._roomCode;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get the leader's player index (-1 if no leader).
|
|
256
|
+
*/
|
|
257
|
+
get leaderIndex() {
|
|
258
|
+
return this._leaderIndex;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check if the host is initialized and ready.
|
|
262
|
+
*/
|
|
263
|
+
get isReady() {
|
|
264
|
+
return this._isReady;
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Public Methods
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
/**
|
|
270
|
+
* Broadcast an event to all players.
|
|
271
|
+
*
|
|
272
|
+
* @param event - Event name (no colons allowed)
|
|
273
|
+
* @param data - Optional data payload
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* host.broadcast('phase-update', { phase: 'playing' });
|
|
278
|
+
* host.broadcast('timer-tick', { remaining: 30 });
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
broadcast(event, data) {
|
|
282
|
+
this.ensureReady("broadcast");
|
|
283
|
+
validateEventName$1(event);
|
|
284
|
+
this.transport.emit(event, data);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Send an event to a specific player.
|
|
288
|
+
*
|
|
289
|
+
* @param playerIndex - Target player index (0, 1, 2, ...)
|
|
290
|
+
* @param event - Event name (no colons allowed)
|
|
291
|
+
* @param data - Optional data payload
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* host.sendToPlayer(0, 'your-turn', { timeLimit: 30 });
|
|
296
|
+
* host.sendToPlayer(1, 'wait', { message: 'Not your turn' });
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
sendToPlayer(playerIndex, event, data) {
|
|
300
|
+
this.ensureReady("sendToPlayer");
|
|
301
|
+
validateEventName$1(event);
|
|
302
|
+
this.transport.emit(event, {
|
|
303
|
+
targetPlayerIndex: playerIndex,
|
|
304
|
+
...data && typeof data === "object" ? data : { data }
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Signal game over with results.
|
|
309
|
+
* This will broadcast the game over event to all players.
|
|
310
|
+
*
|
|
311
|
+
* @param results - Game results (scores, winner, etc.)
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```ts
|
|
315
|
+
* host.gameOver({
|
|
316
|
+
* scores: { 0: 100, 1: 75, 2: 50 },
|
|
317
|
+
* winner: 0,
|
|
318
|
+
* });
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
gameOver(results) {
|
|
322
|
+
this.ensureReady("gameOver");
|
|
323
|
+
this.transport.emit(SYSTEM_EVENTS$1.GAME_OVER, { results });
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Add a listener for a specific event after construction.
|
|
327
|
+
*
|
|
328
|
+
* @param event - Event name (no colons allowed)
|
|
329
|
+
* @param handler - Handler function (playerIndex, data) => void
|
|
330
|
+
* @returns Cleanup function to remove the listener
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```ts
|
|
334
|
+
* const cleanup = host.on('tap', (playerIndex, data) => {
|
|
335
|
+
* console.log(`Player ${playerIndex} tapped`);
|
|
336
|
+
* });
|
|
337
|
+
*
|
|
338
|
+
* // Later
|
|
339
|
+
* cleanup();
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
on(event, handler) {
|
|
343
|
+
validateEventName$1(event);
|
|
344
|
+
const wrappedHandler = (data) => {
|
|
345
|
+
const { playerIndex, ...rest } = data;
|
|
346
|
+
if (playerIndex !== void 0) {
|
|
347
|
+
handler(playerIndex, rest);
|
|
348
|
+
}
|
|
517
349
|
};
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const entries = Object.entries(listeners);
|
|
522
|
-
const cleanups = [];
|
|
523
|
-
for (const [event, handler] of entries) {
|
|
524
|
-
if (!handler) continue;
|
|
525
|
-
validateEventName$1(event);
|
|
526
|
-
const wrappedHandler = (data) => {
|
|
527
|
-
handler(data.playerId, data.payload);
|
|
528
|
-
};
|
|
529
|
-
transport.on(event, wrappedHandler);
|
|
530
|
-
cleanups.push(() => transport.off(event, wrappedHandler));
|
|
350
|
+
if (this.transport) {
|
|
351
|
+
this.transport.on(event, wrappedHandler);
|
|
352
|
+
this.registeredHandlers.push({ event, handler: wrappedHandler });
|
|
531
353
|
}
|
|
532
354
|
return () => {
|
|
533
|
-
|
|
355
|
+
this.transport?.off(event, wrappedHandler);
|
|
356
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
357
|
+
(h) => h.event !== event || h.handler !== wrappedHandler
|
|
358
|
+
);
|
|
534
359
|
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
sendToPlayer,
|
|
569
|
-
gameOver,
|
|
570
|
-
returnToLobby
|
|
571
|
-
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Clean up all resources.
|
|
363
|
+
* Call this when unmounting/destroying the game.
|
|
364
|
+
*/
|
|
365
|
+
destroy() {
|
|
366
|
+
if (this._isDestroyed) return;
|
|
367
|
+
this._isDestroyed = true;
|
|
368
|
+
this._isReady = false;
|
|
369
|
+
for (const { event, handler } of this.registeredHandlers) {
|
|
370
|
+
this.transport?.off(event, handler);
|
|
371
|
+
}
|
|
372
|
+
this.registeredHandlers = [];
|
|
373
|
+
if (this.transport instanceof PostMessageTransport) {
|
|
374
|
+
this.transport.destroy();
|
|
375
|
+
}
|
|
376
|
+
this.transport = null;
|
|
377
|
+
if (this.boundMessageHandler) {
|
|
378
|
+
window.removeEventListener("message", this.boundMessageHandler);
|
|
379
|
+
this.boundMessageHandler = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Private Helpers
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
ensureReady(method) {
|
|
386
|
+
if (!this._isReady || !this.transport) {
|
|
387
|
+
throw new Error(`[SmoreHost] Cannot call ${method}() before host is ready. Wait for onReady callback.`);
|
|
388
|
+
}
|
|
389
|
+
if (this._isDestroyed) {
|
|
390
|
+
throw new Error(`[SmoreHost] Cannot call ${method}() after destroy()`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
572
393
|
}
|
|
573
394
|
|
|
395
|
+
const SYSTEM_PREFIX = "smore:";
|
|
396
|
+
const SYSTEM_EVENTS = {
|
|
397
|
+
PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
|
|
398
|
+
PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`
|
|
399
|
+
};
|
|
400
|
+
const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
|
|
574
401
|
function validateEventName(event) {
|
|
575
|
-
if (
|
|
402
|
+
if (!EVENT_NAME_REGEX.test(event)) {
|
|
576
403
|
throw new Error(
|
|
577
|
-
`[
|
|
404
|
+
`[SmorePlayer] Invalid event name "${event}". Event names must:
|
|
405
|
+
- Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
|
|
406
|
+
- Start and end with a letter (no leading/trailing - or _)`
|
|
578
407
|
);
|
|
579
408
|
}
|
|
580
409
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
410
|
+
class SmorePlayer {
|
|
411
|
+
transport = null;
|
|
412
|
+
config;
|
|
413
|
+
_roomCode = "";
|
|
414
|
+
_myIndex = -1;
|
|
415
|
+
_isLeader = false;
|
|
416
|
+
_isReady = false;
|
|
417
|
+
_isDestroyed = false;
|
|
418
|
+
boundMessageHandler = null;
|
|
419
|
+
registeredHandlers = [];
|
|
420
|
+
constructor(config = {}) {
|
|
421
|
+
this.config = config;
|
|
422
|
+
if (config.listeners) {
|
|
423
|
+
for (const event of Object.keys(config.listeners)) {
|
|
424
|
+
validateEventName(event);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (config.socket) {
|
|
428
|
+
this.initBundled(config);
|
|
429
|
+
} else {
|
|
430
|
+
this.initIframe(config);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Initialization
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
initBundled(config) {
|
|
437
|
+
if (!config.socket) {
|
|
438
|
+
throw new Error("[SmorePlayer] socket is required for bundled games");
|
|
439
|
+
}
|
|
440
|
+
this.transport = new DirectTransport(config.socket);
|
|
441
|
+
this._roomCode = config.roomCode || "";
|
|
442
|
+
this._myIndex = config.myIndex ?? -1;
|
|
443
|
+
this._isLeader = config.isLeader ?? false;
|
|
444
|
+
this.setupEventHandlers();
|
|
445
|
+
this._isReady = true;
|
|
446
|
+
this.config.onReady?.();
|
|
447
|
+
}
|
|
448
|
+
initIframe(config) {
|
|
449
|
+
const parentOrigin = config.parentOrigin || "*";
|
|
450
|
+
window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
|
|
451
|
+
this.boundMessageHandler = (e) => {
|
|
452
|
+
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
453
|
+
const msg = e.data;
|
|
454
|
+
if (!isSmoreMessage(msg)) return;
|
|
455
|
+
if (msg.type === "smore:init") {
|
|
456
|
+
const initData = msg.payload;
|
|
457
|
+
if (initData.side !== "player") {
|
|
458
|
+
console.error("[SmorePlayer] Received init for wrong side:", initData.side);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (initData.myIndex === void 0) {
|
|
462
|
+
console.error("[SmorePlayer] Missing myIndex in init payload");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.transport = new PostMessageTransport(parentOrigin);
|
|
466
|
+
this._roomCode = initData.roomCode;
|
|
467
|
+
this._myIndex = initData.myIndex;
|
|
468
|
+
this._isLeader = initData.isLeader ?? false;
|
|
469
|
+
this.setupEventHandlers();
|
|
470
|
+
this._isReady = true;
|
|
471
|
+
this.config.onReady?.();
|
|
472
|
+
} else if (msg.type === "smore:update") {
|
|
473
|
+
const updateData = msg.payload;
|
|
474
|
+
if (updateData.leaderId !== void 0) ;
|
|
475
|
+
}
|
|
611
476
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
477
|
+
window.addEventListener("message", this.boundMessageHandler);
|
|
478
|
+
}
|
|
479
|
+
setupEventHandlers() {
|
|
480
|
+
if (!this.transport) return;
|
|
481
|
+
this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data) => {
|
|
482
|
+
const playerIndex = data.player?.playerIndex ?? data.playerIndex;
|
|
483
|
+
if (playerIndex !== void 0) {
|
|
484
|
+
this.config.onPlayerJoin?.(playerIndex);
|
|
620
485
|
}
|
|
621
486
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
validateEventName(event);
|
|
628
|
-
transport.emit(event, data);
|
|
629
|
-
},
|
|
630
|
-
[transport]
|
|
631
|
-
);
|
|
632
|
-
return {
|
|
633
|
-
players: room.players,
|
|
634
|
-
leaderId: room.leaderId,
|
|
635
|
-
roomCode: room.roomCode,
|
|
636
|
-
myPlayerId: room.mySessionId,
|
|
637
|
-
isLeader: room.isLeader,
|
|
638
|
-
emit
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function useExternalGames(config) {
|
|
643
|
-
const { serverUrl, enabled = true } = config;
|
|
644
|
-
const [games, setGames] = react.useState([]);
|
|
645
|
-
const [loading, setLoading] = react.useState(false);
|
|
646
|
-
const [error, setError] = react.useState(null);
|
|
647
|
-
const [refreshKey, setRefreshKey] = react.useState(0);
|
|
648
|
-
react.useEffect(() => {
|
|
649
|
-
if (!enabled) return;
|
|
650
|
-
let cancelled = false;
|
|
651
|
-
setLoading(true);
|
|
652
|
-
fetch(`${serverUrl}/api/games`).then((res) => res.json()).then((data) => {
|
|
653
|
-
if (cancelled) return;
|
|
654
|
-
const external = (data.games || []).map((g) => ({
|
|
655
|
-
id: g.id,
|
|
656
|
-
title: g.title,
|
|
657
|
-
description: g.description || "",
|
|
658
|
-
minPlayers: g.minPlayers || 2,
|
|
659
|
-
maxPlayers: g.maxPlayers || 8,
|
|
660
|
-
thumbnail: g.thumbnail || "/thumbnails/g8.jpeg",
|
|
661
|
-
categories: g.categories || ["party"],
|
|
662
|
-
type: "external",
|
|
663
|
-
hostUrl: g.hostUrl,
|
|
664
|
-
playerUrl: g.playerUrl,
|
|
665
|
-
available: !!(g.hostUrl && g.playerUrl),
|
|
666
|
-
rating: 4,
|
|
667
|
-
heroRequired: false
|
|
668
|
-
}));
|
|
669
|
-
setGames(external);
|
|
670
|
-
setError(null);
|
|
671
|
-
}).catch((err) => {
|
|
672
|
-
if (cancelled) return;
|
|
673
|
-
setError(err.message);
|
|
674
|
-
}).finally(() => {
|
|
675
|
-
if (!cancelled) setLoading(false);
|
|
487
|
+
this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data) => {
|
|
488
|
+
const playerIndex = data.player?.playerIndex ?? data.playerIndex;
|
|
489
|
+
if (playerIndex !== void 0) {
|
|
490
|
+
this.config.onPlayerLeave?.(playerIndex);
|
|
491
|
+
}
|
|
676
492
|
});
|
|
493
|
+
if (this.config.listeners) {
|
|
494
|
+
for (const [event, handler] of Object.entries(this.config.listeners)) {
|
|
495
|
+
if (!handler) continue;
|
|
496
|
+
this.registerHandler(event, handler);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
registerHandler(event, handler) {
|
|
501
|
+
if (!this.transport) return;
|
|
502
|
+
this.transport.on(event, handler);
|
|
503
|
+
this.registeredHandlers.push({ event, handler });
|
|
504
|
+
}
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Public Properties
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
/**
|
|
509
|
+
* Get my player index (0, 1, 2, ...).
|
|
510
|
+
*/
|
|
511
|
+
get myIndex() {
|
|
512
|
+
return this._myIndex;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Check if I am the room leader.
|
|
516
|
+
*/
|
|
517
|
+
get isLeader() {
|
|
518
|
+
return this._isLeader;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Get the room code.
|
|
522
|
+
*/
|
|
523
|
+
get roomCode() {
|
|
524
|
+
return this._roomCode;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Check if the player is initialized and ready.
|
|
528
|
+
*/
|
|
529
|
+
get isReady() {
|
|
530
|
+
return this._isReady;
|
|
531
|
+
}
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Public Methods
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
/**
|
|
536
|
+
* Send an event to the host.
|
|
537
|
+
*
|
|
538
|
+
* @param event - Event name (no colons allowed)
|
|
539
|
+
* @param data - Optional data payload
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* player.send('tap', { timestamp: Date.now() });
|
|
544
|
+
* player.send('answer', { choice: 2 });
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
547
|
+
send(event, data) {
|
|
548
|
+
this.ensureReady("send");
|
|
549
|
+
validateEventName(event);
|
|
550
|
+
this.transport.emit(event, data);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Add a listener for a specific event after construction.
|
|
554
|
+
*
|
|
555
|
+
* @param event - Event name (no colons allowed)
|
|
556
|
+
* @param handler - Handler function (data) => void
|
|
557
|
+
* @returns Cleanup function to remove the listener
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* const cleanup = player.on('phase-update', (data) => {
|
|
562
|
+
* console.log('New phase:', data.phase);
|
|
563
|
+
* });
|
|
564
|
+
*
|
|
565
|
+
* // Later
|
|
566
|
+
* cleanup();
|
|
567
|
+
* ```
|
|
568
|
+
*/
|
|
569
|
+
on(event, handler) {
|
|
570
|
+
validateEventName(event);
|
|
571
|
+
if (this.transport) {
|
|
572
|
+
this.transport.on(event, handler);
|
|
573
|
+
this.registeredHandlers.push({ event, handler });
|
|
574
|
+
}
|
|
677
575
|
return () => {
|
|
678
|
-
|
|
576
|
+
this.transport?.off(event, handler);
|
|
577
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
578
|
+
(h) => h.event !== event || h.handler !== handler
|
|
579
|
+
);
|
|
679
580
|
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Clean up all resources.
|
|
584
|
+
* Call this when unmounting/destroying the game.
|
|
585
|
+
*/
|
|
586
|
+
destroy() {
|
|
587
|
+
if (this._isDestroyed) return;
|
|
588
|
+
this._isDestroyed = true;
|
|
589
|
+
this._isReady = false;
|
|
590
|
+
for (const { event, handler } of this.registeredHandlers) {
|
|
591
|
+
this.transport?.off(event, handler);
|
|
592
|
+
}
|
|
593
|
+
this.registeredHandlers = [];
|
|
594
|
+
if (this.transport instanceof PostMessageTransport) {
|
|
595
|
+
this.transport.destroy();
|
|
596
|
+
}
|
|
597
|
+
this.transport = null;
|
|
598
|
+
if (this.boundMessageHandler) {
|
|
599
|
+
window.removeEventListener("message", this.boundMessageHandler);
|
|
600
|
+
this.boundMessageHandler = null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Private Helpers
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
ensureReady(method) {
|
|
607
|
+
if (!this._isReady || !this.transport) {
|
|
608
|
+
throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);
|
|
609
|
+
}
|
|
610
|
+
if (this._isDestroyed) {
|
|
611
|
+
throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
683
614
|
}
|
|
684
615
|
|
|
685
616
|
const SMORE_EVENTS = {
|
|
@@ -708,23 +639,11 @@
|
|
|
708
639
|
}
|
|
709
640
|
|
|
710
641
|
exports.DirectTransport = DirectTransport;
|
|
711
|
-
exports.DirectionPad = DirectionPad;
|
|
712
|
-
exports.HoldButton = HoldButton;
|
|
713
|
-
exports.HostRoomProvider = HostRoomProvider;
|
|
714
|
-
exports.PlayerRoomProvider = PlayerRoomProvider;
|
|
715
642
|
exports.PostMessageTransport = PostMessageTransport;
|
|
716
643
|
exports.SMORE_EVENTS = SMORE_EVENTS;
|
|
717
|
-
exports.
|
|
718
|
-
exports.
|
|
719
|
-
exports.TransportContext = TransportContext;
|
|
644
|
+
exports.SmoreHost = SmoreHost;
|
|
645
|
+
exports.SmorePlayer = SmorePlayer;
|
|
720
646
|
exports.isSystemEvent = isSystemEvent;
|
|
721
|
-
exports.useExternalGames = useExternalGames;
|
|
722
|
-
exports.useGameHost = useGameHost;
|
|
723
|
-
exports.useGamePlayer = useGamePlayer;
|
|
724
|
-
exports.useHostRoom = useHostRoom;
|
|
725
|
-
exports.usePlayerRoom = usePlayerRoom;
|
|
726
|
-
exports.useRoom = useRoom;
|
|
727
|
-
exports.useTransport = useTransport;
|
|
728
647
|
exports.validateUserEvent = validateUserEvent;
|
|
729
648
|
|
|
730
649
|
}));
|