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