@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,9 +1,28 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
2
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
3
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SmoreSDK = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
+
class DirectTransport {
|
|
8
|
+
constructor(socket) {
|
|
9
|
+
this.socket = socket;
|
|
10
|
+
}
|
|
11
|
+
emit(event, ...args) {
|
|
12
|
+
this.socket.emit(event, ...args);
|
|
13
|
+
}
|
|
14
|
+
on(event, handler) {
|
|
15
|
+
this.socket.on(event, handler);
|
|
16
|
+
}
|
|
17
|
+
off(event, handler) {
|
|
18
|
+
if (handler) {
|
|
19
|
+
this.socket.off(event, handler);
|
|
20
|
+
} else {
|
|
21
|
+
this.socket.off(event);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
const SMORE_MSG_PREFIX = "smore:";
|
|
8
27
|
function isSmoreMessage(data) {
|
|
9
28
|
return data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith(SMORE_MSG_PREFIX);
|
|
@@ -75,150 +94,557 @@
|
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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`
|
|
102
|
+
};
|
|
103
|
+
const EVENT_NAME_REGEX$1 = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
|
|
104
|
+
function validateEventName$1(event) {
|
|
105
|
+
if (!EVENT_NAME_REGEX$1.test(event)) {
|
|
106
|
+
throw new Error(
|
|
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 _)`
|
|
110
|
+
);
|
|
85
111
|
}
|
|
86
|
-
return true;
|
|
87
112
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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);
|
|
101
128
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
179
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
180
|
+
};
|
|
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);
|
|
114
202
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
203
|
+
});
|
|
204
|
+
this.registerHandler(SYSTEM_EVENTS$1.PLAYER_LEAVE, (data) => {
|
|
205
|
+
if (data.playerIndex !== void 0) {
|
|
206
|
+
this.config.onPlayerLeave?.(data.playerIndex);
|
|
119
207
|
}
|
|
120
|
-
|
|
121
|
-
|
|
208
|
+
});
|
|
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);
|
|
122
213
|
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!transport) {
|
|
129
|
-
console.warn("[HostBridge] Cannot broadcast before init");
|
|
130
|
-
return;
|
|
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);
|
|
131
219
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
});
|
|
139
230
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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);
|
|
146
348
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
349
|
+
};
|
|
350
|
+
if (this.transport) {
|
|
351
|
+
this.transport.on(event, wrappedHandler);
|
|
352
|
+
this.registeredHandlers.push({ event, handler: wrappedHandler });
|
|
353
|
+
}
|
|
354
|
+
return () => {
|
|
355
|
+
this.transport?.off(event, wrappedHandler);
|
|
356
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
357
|
+
(h) => h.event !== event || h.handler !== wrappedHandler
|
|
358
|
+
);
|
|
359
|
+
};
|
|
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
|
+
}
|
|
158
393
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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])?$/;
|
|
401
|
+
function validateEventName(event) {
|
|
402
|
+
if (!EVENT_NAME_REGEX.test(event)) {
|
|
403
|
+
throw new Error(
|
|
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 _)`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
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);
|
|
176
425
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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) ;
|
|
184
475
|
}
|
|
185
|
-
|
|
186
|
-
|
|
476
|
+
};
|
|
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);
|
|
187
485
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!transport) {
|
|
194
|
-
console.warn("[PlayerBridge] Cannot emit before init");
|
|
195
|
-
return;
|
|
486
|
+
});
|
|
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);
|
|
196
491
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
console.warn("[PlayerBridge] Cannot onEvent before init");
|
|
203
|
-
return () => {
|
|
204
|
-
};
|
|
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);
|
|
205
497
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
575
|
+
return () => {
|
|
576
|
+
this.transport?.off(event, handler);
|
|
577
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
578
|
+
(h) => h.event !== event || h.handler !== handler
|
|
579
|
+
);
|
|
580
|
+
};
|
|
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
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const SMORE_EVENTS = {
|
|
617
|
+
// 게임 lifecycle
|
|
618
|
+
READY: "smore:ready",
|
|
619
|
+
GAME_OVER: "smore:game-over",
|
|
620
|
+
RETURN_TO_LOBBY: "smore:return-to-lobby",
|
|
621
|
+
// 플레이어 관리
|
|
622
|
+
PLAYER_JOIN: "smore:player-join",
|
|
623
|
+
PLAYER_LEAVE: "smore:player-leave",
|
|
624
|
+
// 특정 플레이어에게 전송 (내부용)
|
|
625
|
+
SEND_TO_PLAYER: "smore:send-to-player",
|
|
626
|
+
// 초기화
|
|
627
|
+
INIT: "smore:init",
|
|
628
|
+
UPDATE: "smore:update"
|
|
629
|
+
};
|
|
630
|
+
function validateUserEvent(event) {
|
|
631
|
+
if (event.includes(":")) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Invalid event name "${event}": User events cannot contain ':'. Use '_' or '-' instead. System events use 'smore:' prefix.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function isSystemEvent(event) {
|
|
638
|
+
return event.startsWith("smore:");
|
|
218
639
|
}
|
|
219
640
|
|
|
220
|
-
exports.
|
|
221
|
-
exports.
|
|
641
|
+
exports.DirectTransport = DirectTransport;
|
|
642
|
+
exports.PostMessageTransport = PostMessageTransport;
|
|
643
|
+
exports.SMORE_EVENTS = SMORE_EVENTS;
|
|
644
|
+
exports.SmoreHost = SmoreHost;
|
|
645
|
+
exports.SmorePlayer = SmorePlayer;
|
|
646
|
+
exports.isSystemEvent = isSystemEvent;
|
|
647
|
+
exports.validateUserEvent = validateUserEvent;
|
|
222
648
|
|
|
223
649
|
}));
|
|
224
650
|
//# sourceMappingURL=smore-sdk-vanilla.umd.js.map
|