@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.
Files changed (89) hide show
  1. package/dist/cjs/SmoreHost.cjs +306 -0
  2. package/dist/cjs/SmoreHost.cjs.map +1 -0
  3. package/dist/cjs/SmorePlayer.cjs +229 -0
  4. package/dist/cjs/SmorePlayer.cjs.map +1 -0
  5. package/dist/cjs/components/IframeGameBridge.cjs +115 -0
  6. package/dist/cjs/components/IframeGameBridge.cjs.map +1 -0
  7. package/dist/cjs/context/RoomProvider.cjs +3 -3
  8. package/dist/cjs/context/RoomProvider.cjs.map +1 -1
  9. package/dist/cjs/hooks/useGameHost.cjs +91 -14
  10. package/dist/cjs/hooks/useGameHost.cjs.map +1 -1
  11. package/dist/cjs/hooks/useGamePlayer.cjs +65 -6
  12. package/dist/cjs/hooks/useGamePlayer.cjs.map +1 -1
  13. package/dist/cjs/iframe/index.cjs +58 -315
  14. package/dist/cjs/iframe/index.cjs.map +1 -1
  15. package/dist/cjs/index.cjs +4 -22
  16. package/dist/cjs/index.cjs.map +1 -1
  17. package/dist/cjs/transport/protocol.cjs.map +1 -1
  18. package/dist/cjs/utils/connectionMonitor.cjs +77 -0
  19. package/dist/cjs/utils/connectionMonitor.cjs.map +1 -0
  20. package/dist/cjs/utils/preloadAssets.cjs +66 -0
  21. package/dist/cjs/utils/preloadAssets.cjs.map +1 -0
  22. package/dist/cjs/utils/serverTime.cjs +43 -0
  23. package/dist/cjs/utils/serverTime.cjs.map +1 -0
  24. package/dist/esm/SmoreHost.js +304 -0
  25. package/dist/esm/SmoreHost.js.map +1 -0
  26. package/dist/esm/SmorePlayer.js +227 -0
  27. package/dist/esm/SmorePlayer.js.map +1 -0
  28. package/dist/esm/components/IframeGameBridge.js +113 -0
  29. package/dist/esm/components/IframeGameBridge.js.map +1 -0
  30. package/dist/esm/context/RoomProvider.js +3 -3
  31. package/dist/esm/context/RoomProvider.js.map +1 -1
  32. package/dist/esm/hooks/useGameHost.js +92 -15
  33. package/dist/esm/hooks/useGameHost.js.map +1 -1
  34. package/dist/esm/hooks/useGamePlayer.js +66 -7
  35. package/dist/esm/hooks/useGamePlayer.js.map +1 -1
  36. package/dist/esm/iframe/index.js +59 -313
  37. package/dist/esm/iframe/index.js.map +1 -1
  38. package/dist/esm/index.js +2 -8
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/transport/protocol.js.map +1 -1
  41. package/dist/esm/utils/connectionMonitor.js +75 -0
  42. package/dist/esm/utils/connectionMonitor.js.map +1 -0
  43. package/dist/esm/utils/preloadAssets.js +63 -0
  44. package/dist/esm/utils/preloadAssets.js.map +1 -0
  45. package/dist/esm/utils/serverTime.js +41 -0
  46. package/dist/esm/utils/serverTime.js.map +1 -0
  47. package/dist/types/SmoreHost.d.ts +187 -0
  48. package/dist/types/SmoreHost.d.ts.map +1 -0
  49. package/dist/types/SmorePlayer.d.ts +146 -0
  50. package/dist/types/SmorePlayer.d.ts.map +1 -0
  51. package/dist/types/components/IframeGameBridge.d.ts +2 -2
  52. package/dist/types/components/IframeGameBridge.d.ts.map +1 -1
  53. package/dist/types/components/index.d.ts +2 -4
  54. package/dist/types/components/index.d.ts.map +1 -1
  55. package/dist/types/context/RoomProvider.d.ts +3 -3
  56. package/dist/types/context/RoomProvider.d.ts.map +1 -1
  57. package/dist/types/hooks/useGameHost.d.ts +33 -7
  58. package/dist/types/hooks/useGameHost.d.ts.map +1 -1
  59. package/dist/types/hooks/useGamePlayer.d.ts +29 -3
  60. package/dist/types/hooks/useGamePlayer.d.ts.map +1 -1
  61. package/dist/types/iframe/index.d.ts +10 -10
  62. package/dist/types/iframe/index.d.ts.map +1 -1
  63. package/dist/types/iframe/vanilla.d.ts +12 -4
  64. package/dist/types/iframe/vanilla.d.ts.map +1 -1
  65. package/dist/types/index.d.ts +36 -20
  66. package/dist/types/index.d.ts.map +1 -1
  67. package/dist/types/transport/protocol.d.ts +1 -1
  68. package/dist/types/transport/protocol.d.ts.map +1 -1
  69. package/dist/types/utils/connectionMonitor.d.ts +57 -0
  70. package/dist/types/utils/connectionMonitor.d.ts.map +1 -0
  71. package/dist/types/utils/index.d.ts +7 -0
  72. package/dist/types/utils/index.d.ts.map +1 -0
  73. package/dist/types/utils/preloadAssets.d.ts +29 -0
  74. package/dist/types/utils/preloadAssets.d.ts.map +1 -0
  75. package/dist/types/utils/serverTime.d.ts +28 -0
  76. package/dist/types/utils/serverTime.d.ts.map +1 -0
  77. package/dist/umd/smore-sdk-iframe.umd.js +62 -316
  78. package/dist/umd/smore-sdk-iframe.umd.js.map +1 -1
  79. package/dist/umd/smore-sdk-iframe.umd.min.js +1 -1
  80. package/dist/umd/smore-sdk-iframe.umd.min.js.map +1 -1
  81. package/dist/umd/smore-sdk-vanilla.umd.js +553 -127
  82. package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
  83. package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
  84. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
  85. package/dist/umd/smore-sdk.umd.js +496 -577
  86. package/dist/umd/smore-sdk.umd.js.map +1 -1
  87. package/dist/umd/smore-sdk.umd.min.js +1 -1
  88. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  89. 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.SmoreSDKIframe = {}));
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 RESERVED_PREFIXES = ["room:", "game:"];
79
- function validateEventName(event) {
80
- for (const prefix of RESERVED_PREFIXES) {
81
- if (event.startsWith(prefix)) {
82
- console.error(`[SDK] Event name "${event}" uses reserved prefix "${prefix}". Game events must not use system prefixes.`);
83
- return false;
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
- function createHostBridge(options) {
89
- const { parentOrigin = "*", gameId, listeners, onPlayerJoin, onPlayerLeave, onReady } = options;
90
- let transport = null;
91
- window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
92
- const messageHandler = (e) => {
93
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
94
- const msg = e.data;
95
- if (!isSmoreMessage(msg)) return;
96
- if (msg.type === "smore:init") {
97
- const { side, roomCode, players, leaderId } = msg.payload;
98
- if (side !== "host") {
99
- console.error("[HostBridge] Received init for wrong side:", side);
100
- return;
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
- transport = new PostMessageTransport(parentOrigin);
103
- if (listeners) {
104
- Object.keys(listeners).forEach((event) => {
105
- validateEventName(event);
106
- const handler = listeners[event];
107
- transport.on(event, handler);
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
- if (onPlayerJoin) {
111
- transport.on("player:joined", (payload) => {
112
- onPlayerJoin(payload.sessionId);
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
- if (onPlayerLeave) {
116
- transport.on("player:left", (payload) => {
117
- onPlayerLeave(payload.sessionId);
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
- if (onReady) {
121
- onReady({ roomCode, players, leaderId });
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
- window.addEventListener("message", messageHandler);
126
- return {
127
- broadcast: (event, data) => {
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
- validateEventName(event);
133
- transport.emit(event, data);
134
- },
135
- sendToPlayer: (playerId, event, data) => {
136
- if (!transport) {
137
- console.warn("[HostBridge] Cannot sendToPlayer before init");
138
- return;
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
- transport.emit(event, { targetSessionId: playerId, ...data });
141
- },
142
- emitGameOver: (results) => {
143
- if (!transport) {
144
- console.warn("[HostBridge] Cannot emitGameOver before init");
145
- return;
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
- transport.emit("game-over", results);
148
- },
149
- setLoaded: () => {
150
- window.parent.postMessage({ type: "smore:loaded" }, parentOrigin);
151
- },
152
- destroy: () => {
153
- window.removeEventListener("message", messageHandler);
154
- transport?.destroy();
155
- transport = null;
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
- function createPlayerBridge(options) {
160
- const { parentOrigin = "*", gameId, listeners, onReady } = options;
161
- let transport = null;
162
- window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
163
- const messageHandler = (e) => {
164
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
165
- const msg = e.data;
166
- if (!isSmoreMessage(msg)) return;
167
- if (msg.type === "smore:init") {
168
- const { side, roomCode, mySessionId, isLeader } = msg.payload;
169
- if (side !== "player") {
170
- console.error("[PlayerBridge] Received init for wrong side:", side);
171
- return;
172
- }
173
- if (!mySessionId) {
174
- console.error("[PlayerBridge] Missing mySessionId in init payload");
175
- return;
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
- transport = new PostMessageTransport(parentOrigin);
178
- if (listeners) {
179
- Object.keys(listeners).forEach((event) => {
180
- validateEventName(event);
181
- const handler = listeners[event];
182
- transport.on(event, handler);
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
- if (onReady) {
186
- onReady({ roomCode, isLeader: !!isLeader, mySessionId });
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
- window.addEventListener("message", messageHandler);
191
- return {
192
- emit: (event, data) => {
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
- validateEventName(event);
198
- transport.emit(event, data);
199
- },
200
- onEvent: (event, handler) => {
201
- if (!transport) {
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
- validateEventName(event);
207
- transport.on(event, handler);
208
- return () => {
209
- transport?.off(event, handler);
210
- };
211
- },
212
- destroy: () => {
213
- window.removeEventListener("message", messageHandler);
214
- transport?.destroy();
215
- transport = null;
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.createHostBridge = createHostBridge;
221
- exports.createPlayerBridge = createPlayerBridge;
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