@smoregg/sdk 2.0.0 → 2.2.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 +260 -113
  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 +26 -3
  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 +244 -128
  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 +181 -73
  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 +262 -115
  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 +25 -4
  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 +246 -130
  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 +181 -73
  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 +1 -1
  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 +14 -1
  45. package/dist/types/events.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +4 -8
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/screen.d.ts +3 -3
  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 +65 -4
  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 +5 -0
  57. package/dist/types/transport/protocol.d.ts.map +1 -1
  58. package/dist/types/types.d.ts +254 -345
  59. package/dist/types/types.d.ts.map +1 -1
  60. package/dist/umd/smore-sdk.umd.js +575 -784
  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,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var events = require('./events.cjs');
4
+ var errors = require('./errors.cjs');
4
5
 
5
6
  function createMockScreen(options = {}) {
6
7
  const {
@@ -10,6 +11,7 @@ function createMockScreen(options = {}) {
10
11
  } = options;
11
12
  let _controllers = [...initialControllers];
12
13
  let _isReady = false;
14
+ let _isConnected = false;
13
15
  let _isDestroyed = false;
14
16
  const listeners = /* @__PURE__ */ new Map();
15
17
  const _onAllReadyCallbacks = /* @__PURE__ */ new Set();
@@ -18,13 +20,15 @@ function createMockScreen(options = {}) {
18
20
  const _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
19
21
  const _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
20
22
  const _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
21
- const _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
22
23
  const _onErrorCallbacks = /* @__PURE__ */ new Set();
24
+ const _onConnectionChangeCallbacks = /* @__PURE__ */ new Set();
23
25
  let _allReadyFired = false;
24
26
  let _readyResolve;
25
27
  const _readyPromise = new Promise((resolve) => {
26
28
  _readyResolve = resolve;
27
29
  });
30
+ const _customStates = /* @__PURE__ */ new Map();
31
+ const _stateChangeListeners = /* @__PURE__ */ new Set();
28
32
  const broadcasts = [];
29
33
  const sends = [];
30
34
  const screen = {
@@ -41,6 +45,12 @@ function createMockScreen(options = {}) {
41
45
  get isDestroyed() {
42
46
  return _isDestroyed;
43
47
  },
48
+ get isConnected() {
49
+ return _isConnected;
50
+ },
51
+ get protocolVersion() {
52
+ return 1;
53
+ },
44
54
  get ready() {
45
55
  return _readyPromise;
46
56
  },
@@ -84,87 +94,107 @@ function createMockScreen(options = {}) {
84
94
  _onCharacterUpdatedCallbacks.delete(callback);
85
95
  };
86
96
  },
87
- onRateLimited(callback) {
88
- _onRateLimitedCallbacks.add(callback);
89
- return () => {
90
- _onRateLimitedCallbacks.delete(callback);
91
- };
92
- },
93
97
  onError(callback) {
94
98
  _onErrorCallbacks.add(callback);
95
99
  return () => {
96
100
  _onErrorCallbacks.delete(callback);
97
101
  };
98
102
  },
103
+ onConnectionChange(callback) {
104
+ _onConnectionChangeCallbacks.add(callback);
105
+ return () => {
106
+ _onConnectionChangeCallbacks.delete(callback);
107
+ };
108
+ },
99
109
  // === Communication Methods ===
100
110
  broadcast(event, data) {
101
111
  if (_isDestroyed) {
102
- throw new Error("Cannot broadcast: screen is destroyed");
103
- }
104
- if (!_isReady) {
105
- throw new Error("Cannot broadcast: screen is not ready");
106
- }
107
- events.validateEventName(event);
108
- broadcasts.push({ event, data });
109
- },
110
- broadcastRaw(event, data) {
111
- if (_isDestroyed) {
112
- throw new Error("Cannot broadcast: screen is destroyed");
112
+ throw new errors.SmoreSDKError("DESTROYED", "Cannot call broadcast() after destroy()");
113
113
  }
114
114
  if (!_isReady) {
115
- throw new Error("Cannot broadcast: screen is not ready");
115
+ throw new errors.SmoreSDKError("NOT_READY", "Cannot call broadcast() before screen is ready");
116
116
  }
117
117
  events.validateEventName(event);
118
118
  broadcasts.push({ event, data });
119
119
  },
120
120
  sendToController(playerIndex, event, data) {
121
121
  if (_isDestroyed) {
122
- throw new Error("Cannot send: screen is destroyed");
122
+ throw new errors.SmoreSDKError("DESTROYED", "Cannot call sendToController() after destroy()");
123
123
  }
124
124
  if (!_isReady) {
125
- throw new Error("Cannot send: screen is not ready");
125
+ throw new errors.SmoreSDKError("NOT_READY", "Cannot call sendToController() before screen is ready");
126
126
  }
127
127
  events.validateEventName(event);
128
128
  if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
129
- throw new Error(`Invalid player index: ${playerIndex}`);
129
+ throw new errors.SmoreSDKError("INVALID_PLAYER", `No controller found with player index ${playerIndex}`);
130
130
  }
131
131
  sends.push({ playerIndex, event, data });
132
132
  },
133
- sendToControllerRaw(playerIndex, event, data) {
134
- if (_isDestroyed) {
135
- throw new Error("Cannot send: screen is destroyed");
136
- }
137
- if (!_isReady) {
138
- throw new Error("Cannot send: screen is not ready");
139
- }
140
- events.validateEventName(event);
141
- if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
142
- throw new Error(`Invalid player index: ${playerIndex}`);
143
- }
144
- sends.push({ playerIndex, event, data });
133
+ // === Custom State Methods ===
134
+ getControllerState(playerIndex) {
135
+ return _customStates.get(playerIndex);
136
+ },
137
+ getAllControllerStates() {
138
+ const result = {};
139
+ _customStates.forEach((state, playerIndex) => {
140
+ result[playerIndex] = state;
141
+ });
142
+ return result;
143
+ },
144
+ onCustomStateChange(listener) {
145
+ _stateChangeListeners.add(listener);
146
+ return () => {
147
+ _stateChangeListeners.delete(listener);
148
+ };
149
+ },
150
+ simulateStateChange(playerIndex, state) {
151
+ const existing = _customStates.get(playerIndex) ?? {};
152
+ const next = { ...existing, ...state };
153
+ _customStates.set(playerIndex, next);
154
+ _stateChangeListeners.forEach((cb) => cb(playerIndex, next));
145
155
  },
146
156
  // === Game Lifecycle ===
147
157
  gameOver(results) {
148
158
  if (_isDestroyed) {
149
- throw new Error("Cannot call gameOver: screen is destroyed");
159
+ throw new errors.SmoreSDKError("DESTROYED", "Cannot call gameOver() after destroy()");
150
160
  }
151
161
  if (!_isReady) {
152
- throw new Error("Cannot call gameOver: screen is not ready");
162
+ throw new errors.SmoreSDKError("NOT_READY", "Cannot call gameOver() before screen is ready");
153
163
  }
154
164
  broadcasts.push({ event: "smore:game-over", data: { results } });
155
165
  },
156
166
  signalReady() {
157
167
  if (_isDestroyed) {
158
- throw new Error("Cannot call signalReady: screen is destroyed");
168
+ throw new errors.SmoreSDKError("DESTROYED", "Cannot call signalReady() after destroy()");
159
169
  }
160
170
  if (!_isReady) {
161
- throw new Error("Cannot call signalReady: screen is not ready");
171
+ throw new errors.SmoreSDKError("NOT_READY", "Cannot call signalReady() before screen is ready");
162
172
  }
163
173
  },
164
174
  // === Event Subscription ===
165
175
  on(event, handler) {
166
- events.validateEventName(event);
167
176
  const eventStr = event;
177
+ if (eventStr.startsWith("$")) {
178
+ switch (eventStr) {
179
+ case "$controller-join":
180
+ return screen.onControllerJoin(handler);
181
+ case "$controller-leave":
182
+ return screen.onControllerLeave(handler);
183
+ case "$controller-disconnect":
184
+ return screen.onControllerDisconnect(handler);
185
+ case "$controller-reconnect":
186
+ return screen.onControllerReconnect(handler);
187
+ case "$character-updated":
188
+ return screen.onCharacterUpdated(handler);
189
+ case "$all-ready":
190
+ return screen.onAllReady(handler);
191
+ case "$error":
192
+ return screen.onError(handler);
193
+ case "$connection-change":
194
+ return screen.onConnectionChange(handler);
195
+ }
196
+ }
197
+ events.validateEventName(event);
168
198
  if (!listeners.has(eventStr)) {
169
199
  listeners.set(eventStr, /* @__PURE__ */ new Set());
170
200
  }
@@ -190,6 +220,13 @@ function createMockScreen(options = {}) {
190
220
  listeners.get(eventStr)?.delete(handler);
191
221
  }
192
222
  },
223
+ removeAllListeners(event) {
224
+ if (event) {
225
+ listeners.delete(event);
226
+ } else {
227
+ listeners.clear();
228
+ }
229
+ },
193
230
  // === Utilities ===
194
231
  getController(playerIndex) {
195
232
  return _controllers.find((c) => c.playerIndex === playerIndex);
@@ -197,9 +234,6 @@ function createMockScreen(options = {}) {
197
234
  getControllerCount() {
198
235
  return _controllers.filter((c) => c.connected).length;
199
236
  },
200
- hasAnyConnectedControllers() {
201
- return _controllers.some((c) => c.connected);
202
- },
203
237
  // === Cleanup ===
204
238
  /**
205
239
  * Note: destroy() clears recorded broadcast/event arrays. Call getBroadcasts() before destroy() if assertions are needed.
@@ -235,7 +269,7 @@ function createMockScreen(options = {}) {
235
269
  simulateControllerDisconnect(playerIndex) {
236
270
  const controller = _controllers.find((c) => c.playerIndex === playerIndex);
237
271
  if (!controller) {
238
- throw new Error(`Controller ${playerIndex} not found`);
272
+ throw new errors.SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
239
273
  }
240
274
  _controllers = _controllers.map(
241
275
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
@@ -248,7 +282,7 @@ function createMockScreen(options = {}) {
248
282
  simulateControllerReconnect(playerIndex) {
249
283
  const controller = _controllers.find((c) => c.playerIndex === playerIndex);
250
284
  if (!controller) {
251
- throw new Error(`Controller ${playerIndex} not found`);
285
+ throw new errors.SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
252
286
  }
253
287
  const reconnectedController = { ...controller, connected: true };
254
288
  _controllers = _controllers.map(
@@ -259,16 +293,13 @@ function createMockScreen(options = {}) {
259
293
  simulateCharacterUpdate(playerIndex, appearance) {
260
294
  const controller = _controllers.find((c) => c.playerIndex === playerIndex);
261
295
  if (!controller) {
262
- throw new Error(`Controller ${playerIndex} not found`);
296
+ throw new errors.SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
263
297
  }
264
298
  _controllers = _controllers.map(
265
299
  (c) => c.playerIndex === playerIndex ? { ...c, appearance } : c
266
300
  );
267
301
  _onCharacterUpdatedCallbacks.forEach((cb) => cb(playerIndex, appearance));
268
302
  },
269
- simulateRateLimited(event) {
270
- _onRateLimitedCallbacks.forEach((cb) => cb(event));
271
- },
272
303
  simulateAllReady() {
273
304
  _allReadyFired = true;
274
305
  _onAllReadyCallbacks.forEach((cb) => cb());
@@ -276,6 +307,10 @@ function createMockScreen(options = {}) {
276
307
  simulateError(error) {
277
308
  _onErrorCallbacks.forEach((cb) => cb(error));
278
309
  },
310
+ simulateConnectionChange(connected) {
311
+ _isConnected = connected;
312
+ _onConnectionChangeCallbacks.forEach((cb) => cb(connected));
313
+ },
279
314
  getBroadcasts() {
280
315
  return [...broadcasts];
281
316
  },
@@ -288,6 +323,7 @@ function createMockScreen(options = {}) {
288
323
  },
289
324
  triggerReady() {
290
325
  _isReady = true;
326
+ _isConnected = true;
291
327
  _readyResolve();
292
328
  }
293
329
  };
@@ -299,10 +335,11 @@ function createMockScreen(options = {}) {
299
335
  function createMockController(options = {}) {
300
336
  const {
301
337
  roomCode = "TEST",
302
- myIndex = 0,
338
+ myPlayerIndex = 0,
303
339
  autoReady = true
304
340
  } = options;
305
341
  let _isReady = false;
342
+ let _isConnected = false;
306
343
  let _isDestroyed = false;
307
344
  let _controllers = options.controllers ?? [];
308
345
  const listeners = /* @__PURE__ */ new Map();
@@ -312,18 +349,21 @@ function createMockController(options = {}) {
312
349
  const _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
313
350
  const _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
314
351
  const _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
315
- const _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
316
352
  const _onErrorCallbacks = /* @__PURE__ */ new Set();
353
+ const _onGameOverCallbacks = /* @__PURE__ */ new Set();
354
+ const _onConnectionChangeCallbacks = /* @__PURE__ */ new Set();
317
355
  let _allReadyFired = false;
318
356
  let _readyResolve;
319
357
  const _readyPromise = new Promise((resolve) => {
320
358
  _readyResolve = resolve;
321
359
  });
360
+ const _customStates = /* @__PURE__ */ new Map();
361
+ const _stateChangeListeners = /* @__PURE__ */ new Set();
322
362
  const sentEvents = [];
323
363
  const controller = {
324
364
  // === Properties ===
325
- get myIndex() {
326
- return myIndex;
365
+ get myPlayerIndex() {
366
+ return myPlayerIndex;
327
367
  },
328
368
  get roomCode() {
329
369
  return roomCode;
@@ -334,6 +374,12 @@ function createMockController(options = {}) {
334
374
  get isDestroyed() {
335
375
  return _isDestroyed;
336
376
  },
377
+ get isConnected() {
378
+ return _isConnected;
379
+ },
380
+ get protocolVersion() {
381
+ return 1;
382
+ },
337
383
  get controllers() {
338
384
  return [..._controllers];
339
385
  },
@@ -380,48 +426,92 @@ function createMockController(options = {}) {
380
426
  _onCharacterUpdatedCallbacks.delete(callback);
381
427
  };
382
428
  },
383
- onRateLimited(callback) {
384
- _onRateLimitedCallbacks.add(callback);
385
- return () => {
386
- _onRateLimitedCallbacks.delete(callback);
387
- };
388
- },
389
429
  onError(callback) {
390
430
  _onErrorCallbacks.add(callback);
391
431
  return () => {
392
432
  _onErrorCallbacks.delete(callback);
393
433
  };
394
434
  },
435
+ onConnectionChange(callback) {
436
+ _onConnectionChangeCallbacks.add(callback);
437
+ return () => {
438
+ _onConnectionChangeCallbacks.delete(callback);
439
+ };
440
+ },
441
+ onGameOver(callback) {
442
+ _onGameOverCallbacks.add(callback);
443
+ return () => {
444
+ _onGameOverCallbacks.delete(callback);
445
+ };
446
+ },
395
447
  getControllerCount() {
396
448
  return _controllers.filter((c) => c.connected).length;
397
449
  },
450
+ getController(playerIndex) {
451
+ return _controllers.find((c) => c.playerIndex === playerIndex);
452
+ },
453
+ get me() {
454
+ return _controllers.find((c) => c.playerIndex === myPlayerIndex);
455
+ },
456
+ setState(state) {
457
+ const existing = _customStates.get(myPlayerIndex) ?? {};
458
+ const next = { ...existing, ...state };
459
+ _customStates.set(myPlayerIndex, next);
460
+ _stateChangeListeners.forEach((cb) => cb(myPlayerIndex, next));
461
+ },
462
+ getMyState() {
463
+ return _customStates.get(myPlayerIndex);
464
+ },
465
+ onCustomStateChange(listener) {
466
+ _stateChangeListeners.add(listener);
467
+ return () => {
468
+ _stateChangeListeners.delete(listener);
469
+ };
470
+ },
398
471
  // === Communication Methods ===
399
472
  send(event, data) {
400
473
  if (_isDestroyed) {
401
- throw new Error("Cannot send: controller is destroyed");
402
- }
403
- events.validateEventName(event);
404
- sentEvents.push({ event, data });
405
- },
406
- sendRaw(event, data) {
407
- if (_isDestroyed) {
408
- throw new Error("Cannot send: controller is destroyed");
474
+ throw new errors.SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
409
475
  }
410
476
  events.validateEventName(event);
411
477
  sentEvents.push({ event, data });
412
478
  },
413
479
  signalReady() {
414
480
  if (_isDestroyed) {
415
- throw new Error("Cannot call signalReady: controller is destroyed");
481
+ throw new errors.SmoreSDKError("DESTROYED", "Cannot call signalReady() after destroy()");
416
482
  }
417
483
  if (!_isReady) {
418
- throw new Error("Cannot call signalReady: controller is not ready");
484
+ throw new errors.SmoreSDKError("NOT_READY", "Cannot call signalReady() before controller is ready");
419
485
  }
420
486
  },
421
487
  // === Event Subscription ===
422
488
  on(event, handler) {
423
- events.validateEventName(event);
424
489
  const eventStr = event;
490
+ if (eventStr.startsWith("$")) {
491
+ switch (eventStr) {
492
+ case "$controller-join":
493
+ return controller.onControllerJoin(handler);
494
+ case "$controller-leave":
495
+ return controller.onControllerLeave(handler);
496
+ case "$controller-disconnect":
497
+ return controller.onControllerDisconnect(handler);
498
+ case "$controller-reconnect":
499
+ return controller.onControllerReconnect(handler);
500
+ case "$character-updated":
501
+ return controller.onCharacterUpdated(handler);
502
+ case "$all-ready":
503
+ return controller.onAllReady(handler);
504
+ case "$error":
505
+ return controller.onError(handler);
506
+ case "$game-over":
507
+ return controller.onGameOver(handler);
508
+ case "$state-recovery":
509
+ return controller.onCustomStateChange(handler);
510
+ case "$connection-change":
511
+ return controller.onConnectionChange(handler);
512
+ }
513
+ }
514
+ events.validateEventName(event);
425
515
  if (!listeners.has(eventStr)) {
426
516
  listeners.set(eventStr, /* @__PURE__ */ new Set());
427
517
  }
@@ -447,6 +537,13 @@ function createMockController(options = {}) {
447
537
  listeners.get(eventStr)?.delete(handler);
448
538
  }
449
539
  },
540
+ removeAllListeners(event) {
541
+ if (event) {
542
+ listeners.delete(event);
543
+ } else {
544
+ listeners.clear();
545
+ }
546
+ },
450
547
  // === Cleanup ===
451
548
  destroy() {
452
549
  _isDestroyed = true;
@@ -472,6 +569,7 @@ function createMockController(options = {}) {
472
569
  },
473
570
  triggerReady() {
474
571
  _isReady = true;
572
+ _isConnected = true;
475
573
  _readyResolve();
476
574
  },
477
575
  /**
@@ -512,22 +610,32 @@ function createMockController(options = {}) {
512
610
  simulateCharacterUpdate(playerIndex, appearance) {
513
611
  const ctrl = _controllers.find((c) => c.playerIndex === playerIndex);
514
612
  if (!ctrl) {
515
- throw new Error(`Controller ${playerIndex} not found`);
613
+ throw new errors.SmoreSDKError("INVALID_PLAYER", `Controller ${playerIndex} not found`);
516
614
  }
517
615
  _controllers = _controllers.map(
518
616
  (c) => c.playerIndex === playerIndex ? { ...c, appearance } : c
519
617
  );
520
618
  _onCharacterUpdatedCallbacks.forEach((cb) => cb(playerIndex, appearance));
521
619
  },
522
- simulateRateLimited(event) {
523
- _onRateLimitedCallbacks.forEach((cb) => cb(event));
524
- },
525
620
  simulateAllReady() {
526
621
  _allReadyFired = true;
527
622
  _onAllReadyCallbacks.forEach((cb) => cb());
528
623
  },
529
624
  simulateError(error) {
530
625
  _onErrorCallbacks.forEach((cb) => cb(error));
626
+ },
627
+ simulateGameOver(results) {
628
+ _onGameOverCallbacks.forEach((cb) => cb(results));
629
+ },
630
+ simulateConnectionChange(connected) {
631
+ _isConnected = connected;
632
+ _onConnectionChangeCallbacks.forEach((cb) => cb(connected));
633
+ },
634
+ simulateStateChange(playerIndex, state) {
635
+ const existing = _customStates.get(playerIndex) ?? {};
636
+ const next = { ...existing, ...state };
637
+ _customStates.set(playerIndex, next);
638
+ _stateChangeListeners.forEach((cb) => cb(playerIndex, next));
531
639
  }
532
640
  };
533
641
  if (autoReady) {