@smoregg/sdk 1.3.0 → 2.1.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 (70) hide show
  1. package/dist/cjs/controller.cjs +342 -193
  2. package/dist/cjs/controller.cjs.map +1 -1
  3. package/dist/cjs/errors.cjs +1 -0
  4. package/dist/cjs/errors.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +19 -2
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/index.cjs +2 -7
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/screen.cjs +336 -238
  10. package/dist/cjs/screen.cjs.map +1 -1
  11. package/dist/cjs/shared.cjs +34 -0
  12. package/dist/cjs/shared.cjs.map +1 -0
  13. package/dist/cjs/testing.cjs +269 -197
  14. package/dist/cjs/testing.cjs.map +1 -1
  15. package/dist/cjs/transport/PostMessageTransport.cjs +12 -0
  16. package/dist/cjs/transport/PostMessageTransport.cjs.map +1 -1
  17. package/dist/cjs/transport/protocol.cjs +2 -0
  18. package/dist/cjs/transport/protocol.cjs.map +1 -1
  19. package/dist/cjs/types.cjs +16 -0
  20. package/dist/cjs/types.cjs.map +1 -0
  21. package/dist/esm/controller.js +344 -195
  22. package/dist/esm/controller.js.map +1 -1
  23. package/dist/esm/errors.js +1 -0
  24. package/dist/esm/errors.js.map +1 -1
  25. package/dist/esm/events.js +18 -3
  26. package/dist/esm/events.js.map +1 -1
  27. package/dist/esm/index.js +1 -3
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/screen.js +338 -240
  30. package/dist/esm/screen.js.map +1 -1
  31. package/dist/esm/shared.js +30 -0
  32. package/dist/esm/shared.js.map +1 -0
  33. package/dist/esm/testing.js +269 -197
  34. package/dist/esm/testing.js.map +1 -1
  35. package/dist/esm/transport/PostMessageTransport.js +12 -0
  36. package/dist/esm/transport/PostMessageTransport.js.map +1 -1
  37. package/dist/esm/transport/protocol.js +2 -1
  38. package/dist/esm/transport/protocol.js.map +1 -1
  39. package/dist/esm/types.js +14 -0
  40. package/dist/esm/types.js.map +1 -0
  41. package/dist/types/controller.d.ts +22 -43
  42. package/dist/types/controller.d.ts.map +1 -1
  43. package/dist/types/errors.d.ts.map +1 -1
  44. package/dist/types/events.d.ts +10 -1
  45. package/dist/types/events.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +15 -19
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/screen.d.ts +26 -37
  49. package/dist/types/screen.d.ts.map +1 -1
  50. package/dist/types/shared.d.ts +21 -0
  51. package/dist/types/shared.d.ts.map +1 -0
  52. package/dist/types/testing.d.ts +78 -3
  53. package/dist/types/testing.d.ts.map +1 -1
  54. package/dist/types/transport/PostMessageTransport.d.ts +1 -0
  55. package/dist/types/transport/PostMessageTransport.d.ts.map +1 -1
  56. package/dist/types/transport/protocol.d.ts +4 -0
  57. package/dist/types/transport/protocol.d.ts.map +1 -1
  58. package/dist/types/types.d.ts +391 -540
  59. package/dist/types/types.d.ts.map +1 -1
  60. package/dist/umd/smore-sdk.umd.js +742 -952
  61. package/dist/umd/smore-sdk.umd.js.map +1 -1
  62. package/dist/umd/smore-sdk.umd.min.js +1 -1
  63. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  64. package/package.json +7 -1
  65. package/dist/cjs/config.cjs +0 -13
  66. package/dist/cjs/config.cjs.map +0 -1
  67. package/dist/esm/config.js +0 -10
  68. package/dist/esm/config.js.map +0 -1
  69. package/dist/types/config.d.ts +0 -35
  70. package/dist/types/config.d.ts.map +0 -1
@@ -1,39 +1,30 @@
1
1
  import { validateEventName } from './events.js';
2
+ import { SmoreSDKError } from './errors.js';
2
3
 
3
4
  function createMockScreen(options = {}) {
4
5
  const {
5
6
  roomCode = "TEST",
6
7
  controllers: initialControllers = [],
7
- autoReady = true,
8
- onControllerJoin: onJoinCb,
9
- onControllerLeave: onLeaveCb,
10
- onControllerDisconnect: onDisconnectCb,
11
- onControllerReconnect: onReconnectCb,
12
- onCharacterUpdated: onCharacterUpdatedCb,
13
- onRateLimited: onRateLimitedCb,
14
- onReady: onReadyCb,
15
- onError: onErrorCb
8
+ autoReady = true
16
9
  } = options;
17
10
  let _controllers = [...initialControllers];
18
11
  let _isReady = false;
12
+ let _isConnected = false;
19
13
  let _isDestroyed = false;
20
14
  const listeners = /* @__PURE__ */ new Map();
21
- let onControllerJoinCallback;
22
- let onControllerLeaveCallback;
23
- let onControllerDisconnectCallback;
24
- let onControllerReconnectCallback;
25
- let onCharacterUpdatedCallback;
26
- let onRateLimitedCallback;
27
- let onReadyCallback;
28
- let onErrorCallback;
29
- onControllerJoinCallback = onJoinCb;
30
- onControllerLeaveCallback = onLeaveCb;
31
- onControllerDisconnectCallback = onDisconnectCb;
32
- onControllerReconnectCallback = onReconnectCb;
33
- onCharacterUpdatedCallback = onCharacterUpdatedCb;
34
- onRateLimitedCallback = onRateLimitedCb;
35
- onReadyCallback = onReadyCb;
36
- onErrorCallback = onErrorCb;
15
+ const _onAllReadyCallbacks = /* @__PURE__ */ new Set();
16
+ const _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
17
+ const _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
18
+ const _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
19
+ const _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
20
+ const _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
21
+ const _onErrorCallbacks = /* @__PURE__ */ new Set();
22
+ const _onConnectionChangeCallbacks = /* @__PURE__ */ new Set();
23
+ let _allReadyFired = false;
24
+ let _readyResolve;
25
+ const _readyPromise = new Promise((resolve) => {
26
+ _readyResolve = resolve;
27
+ });
37
28
  const broadcasts = [];
38
29
  const sends = [];
39
30
  const screen = {
@@ -50,75 +41,131 @@ function createMockScreen(options = {}) {
50
41
  get isDestroyed() {
51
42
  return _isDestroyed;
52
43
  },
53
- // === Communication Methods ===
54
- broadcast(event, data) {
55
- if (_isDestroyed) {
56
- throw new Error("Cannot broadcast: screen is destroyed");
57
- }
58
- if (!_isReady) {
59
- throw new Error("Cannot broadcast: screen is not ready");
44
+ get isConnected() {
45
+ return _isConnected;
46
+ },
47
+ get protocolVersion() {
48
+ return 1;
49
+ },
50
+ get ready() {
51
+ return _readyPromise;
52
+ },
53
+ // === Lifecycle Methods ===
54
+ onAllReady(callback) {
55
+ if (_allReadyFired) {
56
+ callback();
60
57
  }
61
- validateEventName(event);
62
- broadcasts.push({ event, data });
58
+ _onAllReadyCallbacks.add(callback);
59
+ return () => {
60
+ _onAllReadyCallbacks.delete(callback);
61
+ };
62
+ },
63
+ onControllerJoin(callback) {
64
+ _onControllerJoinCallbacks.add(callback);
65
+ return () => {
66
+ _onControllerJoinCallbacks.delete(callback);
67
+ };
68
+ },
69
+ onControllerLeave(callback) {
70
+ _onControllerLeaveCallbacks.add(callback);
71
+ return () => {
72
+ _onControllerLeaveCallbacks.delete(callback);
73
+ };
74
+ },
75
+ onControllerDisconnect(callback) {
76
+ _onControllerDisconnectCallbacks.add(callback);
77
+ return () => {
78
+ _onControllerDisconnectCallbacks.delete(callback);
79
+ };
80
+ },
81
+ onControllerReconnect(callback) {
82
+ _onControllerReconnectCallbacks.add(callback);
83
+ return () => {
84
+ _onControllerReconnectCallbacks.delete(callback);
85
+ };
86
+ },
87
+ onCharacterUpdated(callback) {
88
+ _onCharacterUpdatedCallbacks.add(callback);
89
+ return () => {
90
+ _onCharacterUpdatedCallbacks.delete(callback);
91
+ };
92
+ },
93
+ onError(callback) {
94
+ _onErrorCallbacks.add(callback);
95
+ return () => {
96
+ _onErrorCallbacks.delete(callback);
97
+ };
63
98
  },
64
- broadcastRaw(event, data) {
99
+ onConnectionChange(callback) {
100
+ _onConnectionChangeCallbacks.add(callback);
101
+ return () => {
102
+ _onConnectionChangeCallbacks.delete(callback);
103
+ };
104
+ },
105
+ // === Communication Methods ===
106
+ broadcast(event, data) {
65
107
  if (_isDestroyed) {
66
- throw new Error("Cannot broadcast: screen is destroyed");
108
+ throw new SmoreSDKError("DESTROYED", "Cannot call broadcast() after destroy()");
67
109
  }
68
110
  if (!_isReady) {
69
- throw new Error("Cannot broadcast: screen is not ready");
111
+ throw new SmoreSDKError("NOT_READY", "Cannot call broadcast() before screen is ready");
70
112
  }
71
113
  validateEventName(event);
72
114
  broadcasts.push({ event, data });
73
115
  },
74
116
  sendToController(playerIndex, event, data) {
75
117
  if (_isDestroyed) {
76
- throw new Error("Cannot send: screen is destroyed");
118
+ throw new SmoreSDKError("DESTROYED", "Cannot call sendToController() after destroy()");
77
119
  }
78
120
  if (!_isReady) {
79
- throw new Error("Cannot send: screen is not ready");
121
+ throw new SmoreSDKError("NOT_READY", "Cannot call sendToController() before screen is ready");
80
122
  }
81
123
  validateEventName(event);
82
124
  if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
83
- throw new Error(`Invalid player index: ${playerIndex}`);
84
- }
85
- sends.push({ playerIndex, event, data });
86
- },
87
- sendToControllerRaw(playerIndex, event, data) {
88
- if (_isDestroyed) {
89
- throw new Error("Cannot send: screen is destroyed");
90
- }
91
- if (!_isReady) {
92
- throw new Error("Cannot send: screen is not ready");
93
- }
94
- validateEventName(event);
95
- if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
96
- throw new Error(`Invalid player index: ${playerIndex}`);
125
+ throw new SmoreSDKError("INVALID_PLAYER", `No controller found with player index ${playerIndex}`);
97
126
  }
98
127
  sends.push({ playerIndex, event, data });
99
128
  },
100
129
  // === Game Lifecycle ===
101
130
  gameOver(results) {
102
131
  if (_isDestroyed) {
103
- throw new Error("Cannot call gameOver: screen is destroyed");
132
+ throw new SmoreSDKError("DESTROYED", "Cannot call gameOver() after destroy()");
104
133
  }
105
134
  if (!_isReady) {
106
- throw new Error("Cannot call gameOver: screen is not ready");
135
+ throw new SmoreSDKError("NOT_READY", "Cannot call gameOver() before screen is ready");
107
136
  }
108
137
  broadcasts.push({ event: "smore:game-over", data: { results } });
109
138
  },
110
139
  signalReady() {
111
140
  if (_isDestroyed) {
112
- throw new Error("Cannot call signalReady: screen is destroyed");
141
+ throw new SmoreSDKError("DESTROYED", "Cannot call signalReady() after destroy()");
113
142
  }
114
143
  if (!_isReady) {
115
- throw new Error("Cannot call signalReady: screen is not ready");
144
+ throw new SmoreSDKError("NOT_READY", "Cannot call signalReady() before screen is ready");
116
145
  }
117
146
  },
118
147
  // === Event Subscription ===
119
148
  on(event, handler) {
120
- validateEventName(event);
121
149
  const eventStr = event;
150
+ if (eventStr.startsWith("$")) {
151
+ switch (eventStr) {
152
+ case "$controller-join":
153
+ return screen.onControllerJoin(handler);
154
+ case "$controller-leave":
155
+ return screen.onControllerLeave(handler);
156
+ case "$controller-disconnect":
157
+ return screen.onControllerDisconnect(handler);
158
+ case "$controller-reconnect":
159
+ return screen.onControllerReconnect(handler);
160
+ case "$character-updated":
161
+ return screen.onCharacterUpdated(handler);
162
+ case "$all-ready":
163
+ return screen.onAllReady(handler);
164
+ case "$error":
165
+ return screen.onError(handler);
166
+ }
167
+ }
168
+ validateEventName(event);
122
169
  if (!listeners.has(eventStr)) {
123
170
  listeners.set(eventStr, /* @__PURE__ */ new Set());
124
171
  }
@@ -144,6 +191,13 @@ function createMockScreen(options = {}) {
144
191
  listeners.get(eventStr)?.delete(handler);
145
192
  }
146
193
  },
194
+ removeAllListeners(event) {
195
+ if (event) {
196
+ listeners.delete(event);
197
+ } else {
198
+ listeners.clear();
199
+ }
200
+ },
147
201
  // === Utilities ===
148
202
  getController(playerIndex) {
149
203
  return _controllers.find((c) => c.playerIndex === playerIndex);
@@ -151,9 +205,6 @@ function createMockScreen(options = {}) {
151
205
  getControllerCount() {
152
206
  return _controllers.filter((c) => c.connected).length;
153
207
  },
154
- hasAnyConnectedControllers() {
155
- return _controllers.some((c) => c.connected);
156
- },
157
208
  // === Cleanup ===
158
209
  /**
159
210
  * Note: destroy() clears recorded broadcast/event arrays. Call getBroadcasts() before destroy() if assertions are needed.
@@ -177,86 +228,59 @@ function createMockScreen(options = {}) {
177
228
  },
178
229
  simulateControllerJoin(info) {
179
230
  _controllers.push(info);
180
- if (onControllerJoinCallback) {
181
- onControllerJoinCallback(info.playerIndex, info);
182
- }
231
+ _onControllerJoinCallbacks.forEach((cb) => cb(info.playerIndex, info));
183
232
  },
184
233
  simulateControllerLeave(playerIndex) {
185
234
  _controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);
186
- if (onControllerLeaveCallback) {
187
- onControllerLeaveCallback(playerIndex);
188
- }
235
+ _onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
189
236
  },
190
237
  /**
191
238
  * Simulate a controller network disconnect (player still in room but unreachable).
192
- *
193
- * @example
194
- * ```ts
195
- * screen.simulateControllerDisconnect(0);
196
- * expect(screen.getController(0)?.connected).toBe(false);
197
- * ```
198
239
  */
199
240
  simulateControllerDisconnect(playerIndex) {
200
241
  const controller = _controllers.find((c) => c.playerIndex === playerIndex);
201
242
  if (!controller) {
202
- throw new Error(`Controller ${playerIndex} not found`);
243
+ throw new SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
203
244
  }
204
245
  _controllers = _controllers.map(
205
246
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
206
247
  );
207
- if (onControllerDisconnectCallback) {
208
- onControllerDisconnectCallback(playerIndex);
209
- }
248
+ _onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
210
249
  },
211
250
  /**
212
251
  * Simulate a controller network reconnect after disconnect.
213
- *
214
- * @example
215
- * ```ts
216
- * screen.simulateControllerDisconnect(0);
217
- * screen.simulateControllerReconnect(0);
218
- * expect(screen.getController(0)?.connected).toBe(true);
219
- * ```
220
252
  */
221
253
  simulateControllerReconnect(playerIndex) {
222
254
  const controller = _controllers.find((c) => c.playerIndex === playerIndex);
223
255
  if (!controller) {
224
- throw new Error(`Controller ${playerIndex} not found`);
256
+ throw new SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
225
257
  }
226
258
  const reconnectedController = { ...controller, connected: true };
227
259
  _controllers = _controllers.map(
228
260
  (c) => c.playerIndex === playerIndex ? reconnectedController : c
229
261
  );
230
- if (onControllerReconnectCallback) {
231
- onControllerReconnectCallback(playerIndex, reconnectedController);
232
- }
262
+ _onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, reconnectedController));
233
263
  },
234
264
  simulateCharacterUpdate(playerIndex, appearance) {
235
265
  const controller = _controllers.find((c) => c.playerIndex === playerIndex);
236
266
  if (!controller) {
237
- throw new Error(`Controller ${playerIndex} not found`);
267
+ throw new SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
238
268
  }
239
269
  _controllers = _controllers.map(
240
270
  (c) => c.playerIndex === playerIndex ? { ...c, appearance } : c
241
271
  );
242
- if (onCharacterUpdatedCallback) {
243
- onCharacterUpdatedCallback(playerIndex, appearance);
244
- }
245
- },
246
- simulateRateLimited(event) {
247
- if (onRateLimitedCallback) {
248
- onRateLimitedCallback(event);
249
- }
272
+ _onCharacterUpdatedCallbacks.forEach((cb) => cb(playerIndex, appearance));
250
273
  },
251
274
  simulateAllReady() {
252
- if (onReadyCallback) {
253
- onReadyCallback();
254
- }
275
+ _allReadyFired = true;
276
+ _onAllReadyCallbacks.forEach((cb) => cb());
255
277
  },
256
278
  simulateError(error) {
257
- if (onErrorCallback) {
258
- onErrorCallback(error);
259
- }
279
+ _onErrorCallbacks.forEach((cb) => cb(error));
280
+ },
281
+ simulateConnectionChange(connected) {
282
+ _isConnected = connected;
283
+ _onConnectionChangeCallbacks.forEach((cb) => cb(connected));
260
284
  },
261
285
  getBroadcasts() {
262
286
  return [...broadcasts];
@@ -270,6 +294,8 @@ function createMockScreen(options = {}) {
270
294
  },
271
295
  triggerReady() {
272
296
  _isReady = true;
297
+ _isConnected = true;
298
+ _readyResolve();
273
299
  }
274
300
  };
275
301
  if (autoReady) {
@@ -280,42 +306,33 @@ function createMockScreen(options = {}) {
280
306
  function createMockController(options = {}) {
281
307
  const {
282
308
  roomCode = "TEST",
283
- myIndex = 0,
284
- autoReady = true,
285
- onControllerJoin: onJoinCb,
286
- onControllerLeave: onLeaveCb,
287
- onControllerDisconnect: onDisconnectCb,
288
- onControllerReconnect: onReconnectCb,
289
- onCharacterUpdated: onCharacterUpdatedCb,
290
- onRateLimited: onRateLimitedCb,
291
- onReady: onReadyCb,
292
- onError: onErrorCb
309
+ myPlayerIndex = 0,
310
+ autoReady = true
293
311
  } = options;
294
312
  let _isReady = false;
313
+ let _isConnected = false;
295
314
  let _isDestroyed = false;
296
315
  let _controllers = options.controllers ?? [];
297
316
  const listeners = /* @__PURE__ */ new Map();
298
- let onControllerJoinCallback;
299
- let onControllerLeaveCallback;
300
- let onControllerDisconnectCallback;
301
- let onControllerReconnectCallback;
302
- let onCharacterUpdatedCallback;
303
- let onRateLimitedCallback;
304
- let onReadyCallback;
305
- let onErrorCallback;
306
- onControllerJoinCallback = onJoinCb;
307
- onControllerLeaveCallback = onLeaveCb;
308
- onControllerDisconnectCallback = onDisconnectCb;
309
- onControllerReconnectCallback = onReconnectCb;
310
- onCharacterUpdatedCallback = onCharacterUpdatedCb;
311
- onRateLimitedCallback = onRateLimitedCb;
312
- onReadyCallback = onReadyCb;
313
- onErrorCallback = onErrorCb;
317
+ const _onAllReadyCallbacks = /* @__PURE__ */ new Set();
318
+ const _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
319
+ const _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
320
+ const _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
321
+ const _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
322
+ const _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
323
+ const _onErrorCallbacks = /* @__PURE__ */ new Set();
324
+ const _onGameOverCallbacks = /* @__PURE__ */ new Set();
325
+ const _onConnectionChangeCallbacks = /* @__PURE__ */ new Set();
326
+ let _allReadyFired = false;
327
+ let _readyResolve;
328
+ const _readyPromise = new Promise((resolve) => {
329
+ _readyResolve = resolve;
330
+ });
314
331
  const sentEvents = [];
315
332
  const controller = {
316
333
  // === Properties ===
317
- get myIndex() {
318
- return myIndex;
334
+ get myPlayerIndex() {
335
+ return myPlayerIndex;
319
336
  },
320
337
  get roomCode() {
321
338
  return roomCode;
@@ -326,39 +343,122 @@ function createMockController(options = {}) {
326
343
  get isDestroyed() {
327
344
  return _isDestroyed;
328
345
  },
346
+ get isConnected() {
347
+ return _isConnected;
348
+ },
349
+ get protocolVersion() {
350
+ return 1;
351
+ },
329
352
  get controllers() {
330
353
  return [..._controllers];
331
354
  },
355
+ get ready() {
356
+ return _readyPromise;
357
+ },
358
+ // === Lifecycle Methods ===
359
+ onAllReady(callback) {
360
+ if (_allReadyFired) {
361
+ callback();
362
+ }
363
+ _onAllReadyCallbacks.add(callback);
364
+ return () => {
365
+ _onAllReadyCallbacks.delete(callback);
366
+ };
367
+ },
368
+ onControllerJoin(callback) {
369
+ _onControllerJoinCallbacks.add(callback);
370
+ return () => {
371
+ _onControllerJoinCallbacks.delete(callback);
372
+ };
373
+ },
374
+ onControllerLeave(callback) {
375
+ _onControllerLeaveCallbacks.add(callback);
376
+ return () => {
377
+ _onControllerLeaveCallbacks.delete(callback);
378
+ };
379
+ },
380
+ onControllerDisconnect(callback) {
381
+ _onControllerDisconnectCallbacks.add(callback);
382
+ return () => {
383
+ _onControllerDisconnectCallbacks.delete(callback);
384
+ };
385
+ },
386
+ onControllerReconnect(callback) {
387
+ _onControllerReconnectCallbacks.add(callback);
388
+ return () => {
389
+ _onControllerReconnectCallbacks.delete(callback);
390
+ };
391
+ },
392
+ onCharacterUpdated(callback) {
393
+ _onCharacterUpdatedCallbacks.add(callback);
394
+ return () => {
395
+ _onCharacterUpdatedCallbacks.delete(callback);
396
+ };
397
+ },
398
+ onError(callback) {
399
+ _onErrorCallbacks.add(callback);
400
+ return () => {
401
+ _onErrorCallbacks.delete(callback);
402
+ };
403
+ },
404
+ onConnectionChange(callback) {
405
+ _onConnectionChangeCallbacks.add(callback);
406
+ return () => {
407
+ _onConnectionChangeCallbacks.delete(callback);
408
+ };
409
+ },
410
+ onGameOver(callback) {
411
+ _onGameOverCallbacks.add(callback);
412
+ return () => {
413
+ _onGameOverCallbacks.delete(callback);
414
+ };
415
+ },
332
416
  getControllerCount() {
333
417
  return _controllers.filter((c) => c.connected).length;
334
418
  },
419
+ getController(playerIndex) {
420
+ return _controllers.find((c) => c.playerIndex === playerIndex);
421
+ },
335
422
  // === Communication Methods ===
336
423
  send(event, data) {
337
424
  if (_isDestroyed) {
338
- throw new Error("Cannot send: controller is destroyed");
339
- }
340
- validateEventName(event);
341
- sentEvents.push({ event, data });
342
- },
343
- sendRaw(event, data) {
344
- if (_isDestroyed) {
345
- throw new Error("Cannot send: controller is destroyed");
425
+ throw new SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
346
426
  }
347
427
  validateEventName(event);
348
428
  sentEvents.push({ event, data });
349
429
  },
350
430
  signalReady() {
351
431
  if (_isDestroyed) {
352
- throw new Error("Cannot call signalReady: controller is destroyed");
432
+ throw new SmoreSDKError("DESTROYED", "Cannot call signalReady() after destroy()");
353
433
  }
354
434
  if (!_isReady) {
355
- throw new Error("Cannot call signalReady: controller is not ready");
435
+ throw new SmoreSDKError("NOT_READY", "Cannot call signalReady() before controller is ready");
356
436
  }
357
437
  },
358
438
  // === Event Subscription ===
359
439
  on(event, handler) {
360
- validateEventName(event);
361
440
  const eventStr = event;
441
+ if (eventStr.startsWith("$")) {
442
+ switch (eventStr) {
443
+ case "$controller-join":
444
+ return controller.onControllerJoin(handler);
445
+ case "$controller-leave":
446
+ return controller.onControllerLeave(handler);
447
+ case "$controller-disconnect":
448
+ return controller.onControllerDisconnect(handler);
449
+ case "$controller-reconnect":
450
+ return controller.onControllerReconnect(handler);
451
+ case "$character-updated":
452
+ return controller.onCharacterUpdated(handler);
453
+ case "$all-ready":
454
+ return controller.onAllReady(handler);
455
+ case "$error":
456
+ return controller.onError(handler);
457
+ case "$game-over":
458
+ return controller.onGameOver(handler);
459
+ }
460
+ }
461
+ validateEventName(event);
362
462
  if (!listeners.has(eventStr)) {
363
463
  listeners.set(eventStr, /* @__PURE__ */ new Set());
364
464
  }
@@ -384,6 +484,13 @@ function createMockController(options = {}) {
384
484
  listeners.get(eventStr)?.delete(handler);
385
485
  }
386
486
  },
487
+ removeAllListeners(event) {
488
+ if (event) {
489
+ listeners.delete(event);
490
+ } else {
491
+ listeners.clear();
492
+ }
493
+ },
387
494
  // === Cleanup ===
388
495
  destroy() {
389
496
  _isDestroyed = true;
@@ -409,102 +516,67 @@ function createMockController(options = {}) {
409
516
  },
410
517
  triggerReady() {
411
518
  _isReady = true;
519
+ _isConnected = true;
520
+ _readyResolve();
412
521
  },
413
522
  /**
414
523
  * Simulate a new player joining the room.
415
524
  * Stores full ControllerInfo (nickname, appearance, etc.) for later retrieval.
416
- *
417
- * @example
418
- * ```ts
419
- * controller.simulatePlayerJoin(2, { playerIndex: 2, nickname: 'Alice', connected: true });
420
- * expect(controller.getControllerCount()).toBe(3);
421
- * expect(controller.controllers.find(c => c.playerIndex === 2)?.nickname).toBe('Alice');
422
- * ```
423
525
  */
424
526
  simulatePlayerJoin(playerIndex, info) {
425
527
  if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
426
528
  _controllers = [..._controllers, { ...info, connected: info.connected ?? true }];
427
529
  }
428
- if (onControllerJoinCallback) {
429
- onControllerJoinCallback(playerIndex, info);
430
- }
530
+ _onControllerJoinCallbacks.forEach((cb) => cb(playerIndex, info));
431
531
  },
432
532
  /**
433
533
  * Simulate a player leaving the room (fully removed).
434
- *
435
- * @example
436
- * ```ts
437
- * controller.simulatePlayerLeave(1);
438
- * expect(controller.controllers.some(c => c.playerIndex === 1)).toBe(false);
439
- * ```
440
534
  */
441
535
  simulatePlayerLeave(playerIndex) {
442
536
  _controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);
443
- if (onControllerLeaveCallback) {
444
- onControllerLeaveCallback(playerIndex);
445
- }
537
+ _onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
446
538
  },
447
539
  /**
448
540
  * Simulate a player network disconnect (player still in room but unreachable).
449
- *
450
- * @example
451
- * ```ts
452
- * controller.simulatePlayerDisconnect(1);
453
- * expect(controller.controllers.find(c => c.playerIndex === 1)?.connected).toBe(false);
454
- * ```
455
541
  */
456
542
  simulatePlayerDisconnect(playerIndex) {
457
543
  _controllers = _controllers.map(
458
544
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
459
545
  );
460
- if (onControllerDisconnectCallback) {
461
- onControllerDisconnectCallback(playerIndex);
462
- }
546
+ _onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
463
547
  },
464
548
  /**
465
549
  * Simulate a player network reconnect after disconnect.
466
- *
467
- * @example
468
- * ```ts
469
- * controller.simulatePlayerDisconnect(1);
470
- * controller.simulatePlayerReconnect(1, { playerIndex: 1, nickname: 'Bob', connected: true });
471
- * expect(controller.controllers.find(c => c.playerIndex === 1)?.connected).toBe(true);
472
- * ```
473
550
  */
474
551
  simulatePlayerReconnect(playerIndex, info) {
475
552
  _controllers = _controllers.map(
476
553
  (c) => c.playerIndex === playerIndex ? { ...info, connected: true } : c
477
554
  );
478
- if (onControllerReconnectCallback) {
479
- onControllerReconnectCallback(playerIndex, info);
480
- }
555
+ _onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, info));
481
556
  },
482
557
  simulateCharacterUpdate(playerIndex, appearance) {
483
- const controller2 = _controllers.find((c) => c.playerIndex === playerIndex);
484
- if (!controller2) {
485
- throw new Error(`Controller ${playerIndex} not found`);
558
+ const ctrl = _controllers.find((c) => c.playerIndex === playerIndex);
559
+ if (!ctrl) {
560
+ throw new SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
486
561
  }
487
562
  _controllers = _controllers.map(
488
563
  (c) => c.playerIndex === playerIndex ? { ...c, appearance } : c
489
564
  );
490
- if (onCharacterUpdatedCallback) {
491
- onCharacterUpdatedCallback(playerIndex, appearance);
492
- }
493
- },
494
- simulateRateLimited(event) {
495
- if (onRateLimitedCallback) {
496
- onRateLimitedCallback(event);
497
- }
565
+ _onCharacterUpdatedCallbacks.forEach((cb) => cb(playerIndex, appearance));
498
566
  },
499
567
  simulateAllReady() {
500
- if (onReadyCallback) {
501
- onReadyCallback();
502
- }
568
+ _allReadyFired = true;
569
+ _onAllReadyCallbacks.forEach((cb) => cb());
503
570
  },
504
571
  simulateError(error) {
505
- if (onErrorCallback) {
506
- onErrorCallback(error);
507
- }
572
+ _onErrorCallbacks.forEach((cb) => cb(error));
573
+ },
574
+ simulateGameOver(results) {
575
+ _onGameOverCallbacks.forEach((cb) => cb(results));
576
+ },
577
+ simulateConnectionChange(connected) {
578
+ _isConnected = connected;
579
+ _onConnectionChangeCallbacks.forEach((cb) => cb(connected));
508
580
  }
509
581
  };
510
582
  if (autoReady) {