@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
@@ -26,6 +26,11 @@ export type RoomCode = string;
26
26
  * is enforced by the server's genericRelay handler. Payloads exceeding this
27
27
  * limit will be silently dropped by the server without error notification.
28
28
  *
29
+ * **Reserved Fields:** The field names `playerIndex` and `targetPlayerIndex` are reserved
30
+ * by the SDK for internal routing. Using these as custom data field names will cause
31
+ * a compile-time error. The SDK automatically extracts `playerIndex` to identify the sender
32
+ * and uses `targetPlayerIndex` for targeted message delivery.
33
+ *
29
34
  * **Type Safety Note:** Without providing an explicit generic type parameter,
30
35
  * `createScreen()` and `createController()` default to the empty `EventMap`,
31
36
  * which means `send()`, `broadcast()`, and `on()` accept any string as an event
@@ -46,7 +51,7 @@ export type RoomCode = string;
46
51
  *
47
52
  * // With explicit generic -- full type safety
48
53
  * const screen = createScreen<MyGameEvents>({ debug: true });
49
- * screen.on('tap', (pi, data) => { ... });
54
+ * screen.on('tap', (playerIndex, data) => { ... });
50
55
  * await screen.ready;
51
56
  *
52
57
  * // Without generic -- no type safety (not recommended)
@@ -64,7 +69,10 @@ export type RoomCode = string;
64
69
  * ```
65
70
  */
66
71
  export interface EventMap {
67
- [key: string]: unknown;
72
+ [key: string]: Record<string, unknown> & {
73
+ playerIndex?: never;
74
+ targetPlayerIndex?: never;
75
+ };
68
76
  }
69
77
  /**
70
78
  * Extract event names from an event map.
@@ -93,7 +101,7 @@ export interface CharacterAppearance {
93
101
  /** Character style preset identifier */
94
102
  style: string;
95
103
  /** Additional character customization options */
96
- options: Record<string, any>;
104
+ options: Record<string, unknown>;
97
105
  }
98
106
  /**
99
107
  * Information about a connected controller (player).
@@ -124,6 +132,100 @@ export interface ControllerInfo {
124
132
  */
125
133
  readonly appearance?: CharacterAppearance | null;
126
134
  }
135
+ /**
136
+ * Transport interface for custom communication implementations.
137
+ * Implement this to use a custom transport instead of the default PostMessageTransport.
138
+ */
139
+ export interface Transport {
140
+ emit(event: string, ...args: unknown[]): void;
141
+ on(event: string, handler: (...args: unknown[]) => void): void;
142
+ off(event: string, handler?: (...args: unknown[]) => void): void;
143
+ destroy(): void;
144
+ }
145
+ /**
146
+ * Error codes for SDK errors.
147
+ */
148
+ export type SmoreErrorCode = 'TIMEOUT' | 'NOT_READY' | 'DESTROYED' | 'INVALID_EVENT' | 'INVALID_PLAYER' | 'CONNECTION_LOST' | 'INIT_FAILED' | 'RATE_LIMITED' | 'PAYLOAD_TOO_LARGE' | 'UNKNOWN';
149
+ /**
150
+ * Structured error type for SDK errors.
151
+ */
152
+ export interface SmoreError {
153
+ /** Error code for programmatic handling */
154
+ code: SmoreErrorCode;
155
+ /** Human-readable error message */
156
+ message: string;
157
+ /** Original error if available */
158
+ cause?: Error;
159
+ /** Additional context */
160
+ details?: Record<string, unknown>;
161
+ }
162
+ /**
163
+ * Game results structure for gameOver().
164
+ * Flexible to accommodate different game types.
165
+ */
166
+ export interface GameResults {
167
+ /** Player scores indexed by player index */
168
+ scores?: Record<PlayerIndex, number>;
169
+ /** Winner's player index (or -1 for no winner/tie) */
170
+ winner?: PlayerIndex;
171
+ /** Ranked player indices from first to last */
172
+ rankings?: PlayerIndex[];
173
+ /** Additional custom game-specific data */
174
+ custom?: Record<string, unknown>;
175
+ }
176
+ /**
177
+ * Lifecycle event names for subscribing via `on()`.
178
+ *
179
+ * These `$`-prefixed event names allow lifecycle events to be registered
180
+ * through the same `on()` method used for game events. The `$` prefix is
181
+ * reserved by the SDK and cannot be used for user-defined events.
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * // These are equivalent:
186
+ * screen.onControllerJoin((pi, info) => { ... });
187
+ * screen.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });
188
+ * ```
189
+ */
190
+ export declare const LifecycleEvent: {
191
+ readonly ALL_READY: "$all-ready";
192
+ readonly CONTROLLER_JOIN: "$controller-join";
193
+ readonly CONTROLLER_LEAVE: "$controller-leave";
194
+ readonly CONTROLLER_DISCONNECT: "$controller-disconnect";
195
+ readonly CONTROLLER_RECONNECT: "$controller-reconnect";
196
+ readonly CHARACTER_UPDATED: "$character-updated";
197
+ readonly ERROR: "$error";
198
+ readonly GAME_OVER: "$game-over";
199
+ readonly CONNECTION_CHANGE: "$connection-change";
200
+ };
201
+ /** Union of all lifecycle event name strings. */
202
+ export type LifecycleEventName = typeof LifecycleEvent[keyof typeof LifecycleEvent];
203
+ /**
204
+ * Handler signatures for Screen lifecycle events.
205
+ * Used by `on()` overloads to provide type-safe lifecycle event subscription.
206
+ */
207
+ export interface ScreenLifecycleHandlers {
208
+ '$all-ready': () => void;
209
+ '$controller-join': (playerIndex: PlayerIndex, info: ControllerInfo) => void;
210
+ '$controller-leave': (playerIndex: PlayerIndex) => void;
211
+ '$controller-disconnect': (playerIndex: PlayerIndex) => void;
212
+ '$controller-reconnect': (playerIndex: PlayerIndex, info: ControllerInfo) => void;
213
+ '$character-updated': (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void;
214
+ '$error': (error: SmoreError) => void;
215
+ '$connection-change': (connected: boolean) => void;
216
+ }
217
+ /** Screen lifecycle event name union. */
218
+ export type ScreenLifecycleEvent = keyof ScreenLifecycleHandlers;
219
+ /**
220
+ * Handler signatures for Controller lifecycle events.
221
+ * Extends Screen lifecycle events with Controller-specific events.
222
+ */
223
+ export interface ControllerLifecycleHandlers extends ScreenLifecycleHandlers {
224
+ '$game-over': (results?: GameResults) => void;
225
+ '$state-recovery': (states: Record<number, Record<string, any>>) => void;
226
+ }
227
+ /** Controller lifecycle event name union. */
228
+ export type ControllerLifecycleEvent = keyof ControllerLifecycleHandlers;
127
229
  /**
128
230
  * Handler for events received from controllers.
129
231
  * Receives the player index and event data.
@@ -144,12 +246,12 @@ export type ScreenEventHandler<TData = unknown> = (playerIndex: PlayerIndex, dat
144
246
  *
145
247
  * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));
146
248
  * screen.onAllReady(() => startCountdown());
147
- * screen.onControllerJoin((pi, info) => console.log('Joined:', pi));
249
+ * screen.onControllerJoin((playerIndex, info) => console.log('Joined:', playerIndex));
148
250
  *
149
251
  * await screen.ready;
150
252
  * ```
151
253
  */
152
- export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
254
+ export interface ScreenConfig {
153
255
  /**
154
256
  * Enable debug mode for verbose logging.
155
257
  * @default false
@@ -166,6 +268,19 @@ export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
166
268
  * @default 10000
167
269
  */
168
270
  timeout?: number;
271
+ /**
272
+ * Automatically signal ready after initialization.
273
+ * When true (default), the SDK calls signalReady() automatically after init completes.
274
+ * Set to false if your game needs to load resources before signaling ready.
275
+ * @default true
276
+ */
277
+ autoReady?: boolean;
278
+ /**
279
+ * Custom transport implementation.
280
+ * If provided, uses this instead of the default PostMessageTransport.
281
+ * The bridge handshake (_bridge:init) still occurs via postMessage.
282
+ */
283
+ transport?: Transport;
169
284
  }
170
285
  /**
171
286
  * Screen instance - the main interface for the host/TV side of your game.
@@ -179,9 +294,9 @@ export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
179
294
  * ```ts
180
295
  * const screen = createScreen<MyEvents>({ debug: true });
181
296
  *
182
- * screen.on('tap', (pi, data) => handleTap(pi, data));
297
+ * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));
183
298
  * screen.onAllReady(() => startGame());
184
- * screen.onControllerJoin((pi, info) => addPlayer(pi, info));
299
+ * screen.onControllerJoin((playerIndex, info) => addPlayer(playerIndex, info));
185
300
  *
186
301
  * await screen.ready;
187
302
  * screen.broadcast('phase-update', { phase: 'playing' });
@@ -203,6 +318,10 @@ export interface Screen<TEvents extends EventMap = EventMap> {
203
318
  readonly isReady: boolean;
204
319
  /** Whether the screen has been destroyed. */
205
320
  readonly isDestroyed: boolean;
321
+ /** Whether the connection to the server is active. */
322
+ readonly isConnected: boolean;
323
+ /** Protocol version negotiated with the parent frame. */
324
+ readonly protocolVersion: number;
206
325
  /**
207
326
  * A Promise that resolves when the screen is initialized and ready.
208
327
  * Use this to await readiness after creating a screen synchronously.
@@ -259,13 +378,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
259
378
  * @returns Unsubscribe function to remove the callback
260
379
  */
261
380
  onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void;
262
- /**
263
- * Register a callback for when the server rate-limits an event.
264
- *
265
- * @param callback - Called with the rate-limited event name
266
- * @returns Unsubscribe function to remove the callback
267
- */
268
- onRateLimited(callback: (event: string) => void): () => void;
269
381
  /**
270
382
  * Register a callback for when an error occurs.
271
383
  * If no error callback is registered, errors are logged to console in debug mode.
@@ -274,6 +386,14 @@ export interface Screen<TEvents extends EventMap = EventMap> {
274
386
  * @returns Unsubscribe function to remove the callback
275
387
  */
276
388
  onError(callback: (error: SmoreError) => void): () => void;
389
+ /**
390
+ * Register a callback for when the connection status changes.
391
+ * Called when the connection to the server is lost or restored.
392
+ *
393
+ * @param callback - Called with true (connected) or false (disconnected)
394
+ * @returns Unsubscribe function to remove the callback
395
+ */
396
+ onConnectionChange(callback: (connected: boolean) => void): () => void;
277
397
  /**
278
398
  * Broadcast an event to all connected controllers.
279
399
  *
@@ -288,11 +408,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
288
408
  * ```
289
409
  */
290
410
  broadcast<K extends EventNames<TEvents>>(event: K, data: EventData<TEvents, K>): void;
291
- /**
292
- * Broadcast an event with any data (bypasses type checking).
293
- * Use sparingly - prefer the typed broadcast() method.
294
- */
295
- broadcastRaw(event: string, data?: unknown): void;
296
411
  /**
297
412
  * Send an event to a specific controller.
298
413
  *
@@ -311,10 +426,24 @@ export interface Screen<TEvents extends EventMap = EventMap> {
311
426
  */
312
427
  sendToController<K extends EventNames<TEvents>>(playerIndex: PlayerIndex, event: K, data: EventData<TEvents, K>): void;
313
428
  /**
314
- * Send to controller with any data (bypasses type checking).
315
- * Use sparingly - prefer the typed sendToController() method.
429
+ * Get a specific controller's cached custom state.
430
+ * Returns undefined if no state has been set for this controller.
431
+ *
432
+ * @param playerIndex - The controller's player index
316
433
  */
317
- sendToControllerRaw(playerIndex: PlayerIndex, event: string, data?: unknown): void;
434
+ getControllerState(playerIndex: number): Record<string, any> | undefined;
435
+ /**
436
+ * Get all controllers' cached custom states.
437
+ * Returns a record mapping player index to state.
438
+ */
439
+ getAllControllerStates(): Record<number, Record<string, any>>;
440
+ /**
441
+ * Register a listener for custom state changes from any controller.
442
+ *
443
+ * @param listener - Called with the player index and the new state
444
+ * @returns Unsubscribe function
445
+ */
446
+ onCustomStateChange(listener: (playerIndex: number, state: Record<string, any>) => void): () => void;
318
447
  /**
319
448
  * Signal that the game is over and send results.
320
449
  * This will broadcast a game-over event to all controllers.
@@ -345,6 +474,17 @@ export interface Screen<TEvents extends EventMap = EventMap> {
345
474
  * ```
346
475
  */
347
476
  signalReady(): void;
477
+ /**
478
+ * Subscribe to a lifecycle event or a user-defined game event.
479
+ *
480
+ * Lifecycle events use `$`-prefixed names. Use `LifecycleEvent` constants
481
+ * for type-safe subscription:
482
+ * ```ts
483
+ * screen.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });
484
+ * screen.on('tap', (pi, data) => { ... }); // user event
485
+ * ```
486
+ */
487
+ on<K extends ScreenLifecycleEvent>(event: K, handler: ScreenLifecycleHandlers[K]): () => void;
348
488
  /**
349
489
  * Add a listener for a specific event after construction.
350
490
  * Can be called before the screen is ready -- handlers are queued
@@ -365,6 +505,7 @@ export interface Screen<TEvents extends EventMap = EventMap> {
365
505
  * ```
366
506
  */
367
507
  on<K extends EventNames<TEvents>>(event: K, handler: ScreenEventHandler<EventData<TEvents, K>>): () => void;
508
+ once<K extends ScreenLifecycleEvent>(event: K, handler: ScreenLifecycleHandlers[K]): () => void;
368
509
  /**
369
510
  * Add a one-time listener that auto-removes after first call.
370
511
  *
@@ -372,10 +513,19 @@ export interface Screen<TEvents extends EventMap = EventMap> {
372
513
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
373
514
  */
374
515
  once<K extends EventNames<TEvents>>(event: K, handler: ScreenEventHandler<EventData<TEvents, K>>): () => void;
516
+ off<K extends ScreenLifecycleEvent>(event: K, handler?: ScreenLifecycleHandlers[K]): void;
375
517
  /**
376
518
  * Remove a specific listener or all listeners for an event.
377
519
  */
378
520
  off<K extends EventNames<TEvents>>(event: K, handler?: ScreenEventHandler<EventData<TEvents, K>>): void;
521
+ /**
522
+ * Remove all event listeners, or all listeners for a specific event.
523
+ * Only removes user event listeners registered via on()/once().
524
+ * Lifecycle callbacks (onControllerJoin, onAllReady, etc.) are not affected.
525
+ *
526
+ * @param event - Optional event name. If omitted, removes ALL user event listeners.
527
+ */
528
+ removeAllListeners(event?: string): void;
379
529
  /**
380
530
  * Get a specific controller by player index.
381
531
  * Returns undefined if not found.
@@ -385,14 +535,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
385
535
  * Get the number of connected controllers.
386
536
  */
387
537
  getControllerCount(): number;
388
- /**
389
- * Check if there is at least one connected controller.
390
- * Useful for detecting when all players have disconnected
391
- * (e.g., to pause the game or show a waiting screen).
392
- *
393
- * Screen-only method. Controller instances can check their own connection status directly.
394
- */
395
- hasAnyConnectedControllers(): boolean;
396
538
  /**
397
539
  * Clean up all resources and disconnect.
398
540
  * Call this when unmounting/destroying your game.
@@ -424,7 +566,7 @@ export type ControllerEventHandler<TData = unknown> = (data: TData) => void;
424
566
  * controller.send('tap', { x: 100, y: 200 });
425
567
  * ```
426
568
  */
427
- export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
569
+ export interface ControllerConfig {
428
570
  /**
429
571
  * Enable debug mode for verbose logging.
430
572
  * @default false
@@ -440,6 +582,19 @@ export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
440
582
  * @default 10000
441
583
  */
442
584
  timeout?: number;
585
+ /**
586
+ * Automatically signal ready after initialization.
587
+ * When true (default), the SDK calls signalReady() automatically after init completes.
588
+ * Set to false if your game needs to load resources before signaling ready.
589
+ * @default true
590
+ */
591
+ autoReady?: boolean;
592
+ /**
593
+ * Custom transport implementation.
594
+ * If provided, uses this instead of the default PostMessageTransport.
595
+ * The bridge handshake (_bridge:init) still occurs via postMessage.
596
+ */
597
+ transport?: Transport;
443
598
  }
444
599
  /**
445
600
  * Controller instance - the main interface for the player/phone side.
@@ -470,13 +625,19 @@ export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
470
625
  */
471
626
  export interface Controller<TEvents extends EventMap = EventMap> {
472
627
  /** My player index (0, 1, 2, ...). */
473
- readonly myIndex: PlayerIndex;
628
+ readonly myPlayerIndex: PlayerIndex;
629
+ /** My own controller info. undefined before initialization. */
630
+ readonly me: ControllerInfo | undefined;
474
631
  /** The room code for this game session. */
475
632
  readonly roomCode: RoomCode;
476
633
  /** Whether the controller is initialized and ready. */
477
634
  readonly isReady: boolean;
478
635
  /** Whether the controller has been destroyed. */
479
636
  readonly isDestroyed: boolean;
637
+ /** Whether the connection to the server is active. */
638
+ readonly isConnected: boolean;
639
+ /** Protocol version negotiated with the parent frame. */
640
+ readonly protocolVersion: number;
480
641
  /**
481
642
  * Read-only list of all known controllers (players) in the room.
482
643
  * Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
@@ -542,13 +703,6 @@ export interface Controller<TEvents extends EventMap = EventMap> {
542
703
  * @returns Unsubscribe function to remove the callback
543
704
  */
544
705
  onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void;
545
- /**
546
- * Register a callback for when the server rate-limits an event.
547
- *
548
- * @param callback - Called with the rate-limited event name
549
- * @returns Unsubscribe function to remove the callback
550
- */
551
- onRateLimited(callback: (event: string) => void): () => void;
552
706
  /**
553
707
  * Register a callback for when an error occurs.
554
708
  * If no error callback is registered, errors are logged to console in debug mode.
@@ -557,10 +711,50 @@ export interface Controller<TEvents extends EventMap = EventMap> {
557
711
  * @returns Unsubscribe function to remove the callback
558
712
  */
559
713
  onError(callback: (error: SmoreError) => void): () => void;
714
+ /**
715
+ * Register a callback for when the game ends.
716
+ * Called when the Screen calls gameOver().
717
+ *
718
+ * @param callback - Called with optional game results
719
+ * @returns Unsubscribe function to remove the callback
720
+ */
721
+ onGameOver(callback: (results?: GameResults) => void): () => void;
722
+ /**
723
+ * Register a callback for when the connection status changes.
724
+ * Called when the connection to the server is lost or restored.
725
+ *
726
+ * @param callback - Called with true (connected) or false (disconnected)
727
+ * @returns Unsubscribe function to remove the callback
728
+ */
729
+ onConnectionChange(callback: (connected: boolean) => void): () => void;
560
730
  /**
561
731
  * Returns the number of currently connected players.
562
732
  */
563
733
  getControllerCount(): number;
734
+ /**
735
+ * Get a specific controller by player index.
736
+ * Returns undefined if not found.
737
+ */
738
+ getController(playerIndex: PlayerIndex): ControllerInfo | undefined;
739
+ /**
740
+ * Set custom state for this controller. State is merged with existing state on the server.
741
+ * State persists only for the duration of the game session.
742
+ *
743
+ * @param state - Key-value state to merge
744
+ */
745
+ setState(state: Record<string, any>): void;
746
+ /**
747
+ * Get this controller's cached custom state.
748
+ * Returns undefined if no state has been set.
749
+ */
750
+ getMyState(): Record<string, any> | undefined;
751
+ /**
752
+ * Register a listener for custom state changes from any controller.
753
+ *
754
+ * @param listener - Called with the player index and the new state
755
+ * @returns Unsubscribe function
756
+ */
757
+ onCustomStateChange(listener: (playerIndex: number, state: Record<string, any>) => void): () => void;
564
758
  /**
565
759
  * Send an event to the screen.
566
760
  *
@@ -577,11 +771,6 @@ export interface Controller<TEvents extends EventMap = EventMap> {
577
771
  * ```
578
772
  */
579
773
  send<K extends EventNames<TEvents>>(event: K, data: EventData<TEvents, K>): void;
580
- /**
581
- * Send with any data (bypasses type checking).
582
- * Use sparingly - prefer the typed send() method.
583
- */
584
- sendRaw(event: string, data?: unknown): void;
585
774
  /**
586
775
  * Signal that this controller has finished loading resources and is ready to start.
587
776
  *
@@ -596,6 +785,17 @@ export interface Controller<TEvents extends EventMap = EventMap> {
596
785
  * ```
597
786
  */
598
787
  signalReady(): void;
788
+ /**
789
+ * Subscribe to a lifecycle event or a user-defined game event.
790
+ *
791
+ * Lifecycle events use `$`-prefixed names. Use `LifecycleEvent` constants
792
+ * for type-safe subscription:
793
+ * ```ts
794
+ * controller.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });
795
+ * controller.on('phase-update', (data) => { ... }); // user event
796
+ * ```
797
+ */
798
+ on<K extends ControllerLifecycleEvent>(event: K, handler: ControllerLifecycleHandlers[K]): () => void;
599
799
  /**
600
800
  * Add a listener for a specific event after construction.
601
801
  * Can be called before the controller is ready -- handlers are queued
@@ -621,6 +821,7 @@ export interface Controller<TEvents extends EventMap = EventMap> {
621
821
  * ```
622
822
  */
623
823
  on<K extends EventNames<TEvents>>(event: K, handler: ControllerEventHandler<EventData<TEvents, K>>): () => void;
824
+ once<K extends ControllerLifecycleEvent>(event: K, handler: ControllerLifecycleHandlers[K]): () => void;
624
825
  /**
625
826
  * Add a one-time listener that auto-removes after first call.
626
827
  *
@@ -628,47 +829,25 @@ export interface Controller<TEvents extends EventMap = EventMap> {
628
829
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
629
830
  */
630
831
  once<K extends EventNames<TEvents>>(event: K, handler: ControllerEventHandler<EventData<TEvents, K>>): () => void;
832
+ off<K extends ControllerLifecycleEvent>(event: K, handler?: ControllerLifecycleHandlers[K]): void;
631
833
  /**
632
834
  * Remove a specific listener or all listeners for an event.
633
835
  */
634
836
  off<K extends EventNames<TEvents>>(event: K, handler?: ControllerEventHandler<EventData<TEvents, K>>): void;
837
+ /**
838
+ * Remove all event listeners, or all listeners for a specific event.
839
+ * Only removes user event listeners registered via on()/once().
840
+ * Lifecycle callbacks (onControllerJoin, onAllReady, etc.) are not affected.
841
+ *
842
+ * @param event - Optional event name. If omitted, removes ALL user event listeners.
843
+ */
844
+ removeAllListeners(event?: string): void;
635
845
  /**
636
846
  * Clean up all resources and disconnect.
637
847
  * Call this when unmounting/destroying your game.
638
848
  */
639
849
  destroy(): void;
640
850
  }
641
- /**
642
- * Game results structure for gameOver().
643
- * Flexible to accommodate different game types.
644
- */
645
- export interface GameResults {
646
- /** Player scores indexed by player index */
647
- scores?: Record<PlayerIndex, number>;
648
- /** Winner's player index (or -1 for no winner/tie) */
649
- winner?: PlayerIndex;
650
- /** Ranked player indices from first to last */
651
- rankings?: PlayerIndex[];
652
- /** Additional custom data */
653
- [key: string]: unknown;
654
- }
655
- /**
656
- * Error codes for SDK errors.
657
- */
658
- export type SmoreErrorCode = 'TIMEOUT' | 'NOT_READY' | 'DESTROYED' | 'INVALID_EVENT' | 'INVALID_PLAYER' | 'CONNECTION_LOST' | 'INIT_FAILED' | 'UNKNOWN';
659
- /**
660
- * Structured error type for SDK errors.
661
- */
662
- export interface SmoreError {
663
- /** Error code for programmatic handling */
664
- code: SmoreErrorCode;
665
- /** Human-readable error message */
666
- message: string;
667
- /** Original error if available */
668
- cause?: Error;
669
- /** Additional context */
670
- details?: Record<string, unknown>;
671
- }
672
851
  /**
673
852
  * Log levels for debug output.
674
853
  */
@@ -692,274 +871,4 @@ export interface DebugOptions {
692
871
  /** Custom logger function */
693
872
  logger?: (level: LogLevel, message: string, data?: unknown) => void;
694
873
  }
695
- /**
696
- * Create a Screen instance for the host/TV side of your game.
697
- *
698
- * Returns a Screen instance synchronously. The screen begins listening
699
- * for the bridge init message immediately. Use the `.ready` promise
700
- * to await full initialization.
701
- *
702
- * @template TEvents - Event map type for type-safe events
703
- * @param config - Screen configuration (debug, parentOrigin, timeout)
704
- * @returns Screen instance (synchronous)
705
- *
706
- * @example
707
- * ```ts
708
- * const screen = createScreen<MyEvents>({ debug: true });
709
- *
710
- * screen.on('tap', (playerIndex, data) => {
711
- * screen.broadcast('round-result', { ... });
712
- * });
713
- *
714
- * screen.onAllReady(() => startGame());
715
- * screen.onControllerJoin((pi, info) => addPlayer(pi, info));
716
- *
717
- * await screen.ready;
718
- * ```
719
- */
720
- export declare function createScreen<TEvents extends EventMap = EventMap>(config?: ScreenConfig<TEvents>): Screen<TEvents>;
721
- /**
722
- * Create a Controller instance for the player/phone side of your game.
723
- *
724
- * Returns a Controller instance synchronously. The controller begins listening
725
- * for the bridge init message immediately. Use the `.ready` promise
726
- * to await full initialization.
727
- *
728
- * @template TEvents - Event map type for type-safe events
729
- * @param config - Controller configuration (debug, parentOrigin, timeout)
730
- * @returns Controller instance (synchronous)
731
- *
732
- * @example
733
- * ```ts
734
- * const controller = createController<MyEvents>({ debug: true });
735
- *
736
- * controller.on('phase-update', (data) => setPhase(data.phase));
737
- * controller.onAllReady(() => console.log('Ready!'));
738
- *
739
- * await controller.ready;
740
- * controller.send('tap', { x: 100, y: 200 });
741
- * ```
742
- */
743
- export declare function createController<TEvents extends EventMap = EventMap>(config?: ControllerConfig<TEvents>): Controller<TEvents>;
744
- /**
745
- * Options for creating mock instances.
746
- */
747
- export interface MockOptions {
748
- /** Initial room code */
749
- roomCode?: RoomCode;
750
- /** Initial controllers for Screen mock */
751
- controllers?: ControllerInfo[];
752
- /** My index for Controller mock */
753
- myIndex?: PlayerIndex;
754
- /** Auto-trigger ready state */
755
- autoReady?: boolean;
756
- }
757
- /**
758
- * Mock Screen for testing.
759
- * Extends Screen with additional test utilities.
760
- */
761
- export interface MockScreen<TEvents extends EventMap = EventMap> extends Screen<TEvents> {
762
- /**
763
- * Simulate receiving an event from a controller.
764
- * Triggers the corresponding listener.
765
- */
766
- simulateEvent<K extends EventNames<TEvents>>(playerIndex: PlayerIndex, event: K, data: EventData<TEvents, K>): void;
767
- /**
768
- * Simulate a controller joining.
769
- * Triggers onControllerJoin callbacks.
770
- */
771
- simulateControllerJoin(info: ControllerInfo): void;
772
- /**
773
- * Simulate a controller leaving.
774
- * Triggers onControllerLeave callbacks.
775
- */
776
- simulateControllerLeave(playerIndex: PlayerIndex): void;
777
- /**
778
- * Simulate a controller disconnecting temporarily.
779
- * Triggers onControllerDisconnect callbacks and marks player as disconnected.
780
- */
781
- simulateControllerDisconnect(playerIndex: PlayerIndex): void;
782
- /**
783
- * Simulate a controller reconnecting after a disconnect.
784
- * Triggers onControllerReconnect callbacks and marks player as connected.
785
- */
786
- simulateControllerReconnect(playerIndex: PlayerIndex): void;
787
- /**
788
- * Get all events that were broadcast.
789
- * Returns array of { event, data } objects.
790
- */
791
- getBroadcasts(): Array<{
792
- event: string;
793
- data: unknown;
794
- }>;
795
- /**
796
- * Get all events sent to a specific controller.
797
- */
798
- getSentToController(playerIndex: PlayerIndex): Array<{
799
- event: string;
800
- data: unknown;
801
- }>;
802
- /**
803
- * Clear recorded events.
804
- */
805
- clearRecordedEvents(): void;
806
- /**
807
- * Manually trigger the ready state.
808
- */
809
- triggerReady(): void;
810
- /**
811
- * Simulate a player's character appearance being updated.
812
- * Triggers onCharacterUpdated callbacks and updates the controller's appearance.
813
- */
814
- simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void;
815
- /**
816
- * Simulate the server rate-limiting an event.
817
- * Triggers onRateLimited callbacks.
818
- */
819
- simulateRateLimited(event: string): void;
820
- /**
821
- * Simulate the all-ready event (all participants signaled ready).
822
- * Triggers onAllReady callbacks.
823
- */
824
- simulateAllReady(): void;
825
- /** Simulate an error. Triggers onError callbacks. */
826
- simulateError(error: any): void;
827
- }
828
- /**
829
- * Mock Controller for testing.
830
- * Extends Controller with additional test utilities.
831
- */
832
- export interface MockController<TEvents extends EventMap = EventMap> extends Controller<TEvents> {
833
- /**
834
- * Simulate receiving an event from the screen.
835
- * Triggers the corresponding listener.
836
- */
837
- simulateEvent<K extends EventNames<TEvents>>(event: K, data: EventData<TEvents, K>): void;
838
- /**
839
- * Get all events that were sent to the screen.
840
- * Returns array of { event, data } objects.
841
- */
842
- getSentEvents(): Array<{
843
- event: string;
844
- data: unknown;
845
- }>;
846
- /**
847
- * Clear recorded events.
848
- */
849
- clearRecordedEvents(): void;
850
- /**
851
- * Manually trigger the ready state.
852
- */
853
- triggerReady(): void;
854
- /**
855
- * Simulate another player joining the room.
856
- * Triggers onControllerJoin callbacks.
857
- */
858
- simulatePlayerJoin(playerIndex: PlayerIndex, info: ControllerInfo): void;
859
- /**
860
- * Simulate another player leaving the room.
861
- * Triggers onControllerLeave callbacks.
862
- */
863
- simulatePlayerLeave(playerIndex: PlayerIndex): void;
864
- /**
865
- * Simulate another player disconnecting temporarily.
866
- * Triggers onControllerDisconnect callbacks.
867
- */
868
- simulatePlayerDisconnect(playerIndex: PlayerIndex): void;
869
- /**
870
- * Simulate another player reconnecting after a disconnect.
871
- * Triggers onControllerReconnect callbacks.
872
- */
873
- simulatePlayerReconnect(playerIndex: PlayerIndex, info: ControllerInfo): void;
874
- /**
875
- * Simulate a player's character appearance being updated.
876
- * Triggers onCharacterUpdated callbacks and updates the controller's appearance.
877
- */
878
- simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void;
879
- /**
880
- * Simulate the server rate-limiting an event.
881
- * Triggers onRateLimited callbacks.
882
- */
883
- simulateRateLimited(event: string): void;
884
- /**
885
- * Simulate the all-ready event (all participants signaled ready).
886
- * Triggers onAllReady callbacks.
887
- */
888
- simulateAllReady(): void;
889
- /** Simulate an error. Triggers onError callbacks. */
890
- simulateError(error: any): void;
891
- }
892
- /**
893
- * Create a mock Screen for testing.
894
- *
895
- * @example
896
- * ```ts
897
- * const mockScreen = createMockScreen<MyEvents>({
898
- * controllers: [
899
- * { playerIndex: 0, nickname: 'Player 1', connected: true },
900
- * { playerIndex: 1, nickname: 'Player 2', connected: true },
901
- * ],
902
- * });
903
- *
904
- * mockScreen.on('tap', (pi, data) => handleTap(pi, data));
905
- * mockScreen.onAllReady(() => startGame());
906
- *
907
- * // Simulate player input
908
- * mockScreen.simulateEvent(0, 'tap', { x: 100, y: 200 });
909
- *
910
- * // Check what was broadcast
911
- * expect(mockScreen.getBroadcasts()).toContainEqual({
912
- * event: 'score-update',
913
- * data: { scores: { 0: 10 } },
914
- * });
915
- * ```
916
- */
917
- export declare function createMockScreen<TEvents extends EventMap = EventMap>(options?: MockOptions): MockScreen<TEvents>;
918
- /**
919
- * Create a mock Controller for testing.
920
- *
921
- * @example
922
- * ```ts
923
- * const mockController = createMockController<MyEvents>({
924
- * myIndex: 0,
925
- * });
926
- *
927
- * mockController.on('your-turn', (data) => handleTurn(data));
928
- * mockController.onAllReady(() => console.log('Ready!'));
929
- *
930
- * // Simulate receiving from screen
931
- * mockController.simulateEvent('your-turn', { timeLimit: 30 });
932
- *
933
- * // Check what was sent
934
- * expect(mockController.getSentEvents()).toContainEqual({
935
- * event: 'answer',
936
- * data: { choice: 2 },
937
- * });
938
- * ```
939
- */
940
- export declare function createMockController<TEvents extends EventMap = EventMap>(options?: MockOptions): MockController<TEvents>;
941
- /**
942
- * Game metadata from game.json manifest.
943
- * SYNC: Also defined in game-project/types/src/types.ts - keep in sync.
944
- */
945
- export interface GameMetadata {
946
- /** Unique game identifier */
947
- id: string;
948
- /** Display title */
949
- title: string;
950
- /** Game description */
951
- description: string;
952
- /** Minimum players required */
953
- minPlayers: number;
954
- /** Maximum players supported */
955
- maxPlayers: number;
956
- /** Game categories/tags */
957
- categories: string[];
958
- /** Thumbnail image URL */
959
- thumbnail?: string;
960
- /** Author/developer name */
961
- author?: string;
962
- /** Game version */
963
- version?: string;
964
- }
965
874
  //# sourceMappingURL=types.d.ts.map