@smoregg/sdk 2.0.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 +193 -115
  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 +185 -130
  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 +125 -74
  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 +195 -117
  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 +187 -132
  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 +125 -74
  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 +10 -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 +63 -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 +4 -0
  57. package/dist/types/transport/protocol.d.ts.map +1 -1
  58. package/dist/types/types.d.ts +215 -347
  59. package/dist/types/types.d.ts.map +1 -1
  60. package/dist/umd/smore-sdk.umd.js +442 -787
  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,99 @@ 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
+ }
226
+ /** Controller lifecycle event name union. */
227
+ export type ControllerLifecycleEvent = keyof ControllerLifecycleHandlers;
127
228
  /**
128
229
  * Handler for events received from controllers.
129
230
  * Receives the player index and event data.
@@ -144,12 +245,12 @@ export type ScreenEventHandler<TData = unknown> = (playerIndex: PlayerIndex, dat
144
245
  *
145
246
  * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));
146
247
  * screen.onAllReady(() => startCountdown());
147
- * screen.onControllerJoin((pi, info) => console.log('Joined:', pi));
248
+ * screen.onControllerJoin((playerIndex, info) => console.log('Joined:', playerIndex));
148
249
  *
149
250
  * await screen.ready;
150
251
  * ```
151
252
  */
152
- export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
253
+ export interface ScreenConfig {
153
254
  /**
154
255
  * Enable debug mode for verbose logging.
155
256
  * @default false
@@ -166,6 +267,19 @@ export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
166
267
  * @default 10000
167
268
  */
168
269
  timeout?: number;
270
+ /**
271
+ * Automatically signal ready after initialization.
272
+ * When true (default), the SDK calls signalReady() automatically after init completes.
273
+ * Set to false if your game needs to load resources before signaling ready.
274
+ * @default true
275
+ */
276
+ autoReady?: boolean;
277
+ /**
278
+ * Custom transport implementation.
279
+ * If provided, uses this instead of the default PostMessageTransport.
280
+ * The bridge handshake (_bridge:init) still occurs via postMessage.
281
+ */
282
+ transport?: Transport;
169
283
  }
170
284
  /**
171
285
  * Screen instance - the main interface for the host/TV side of your game.
@@ -179,9 +293,9 @@ export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
179
293
  * ```ts
180
294
  * const screen = createScreen<MyEvents>({ debug: true });
181
295
  *
182
- * screen.on('tap', (pi, data) => handleTap(pi, data));
296
+ * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));
183
297
  * screen.onAllReady(() => startGame());
184
- * screen.onControllerJoin((pi, info) => addPlayer(pi, info));
298
+ * screen.onControllerJoin((playerIndex, info) => addPlayer(playerIndex, info));
185
299
  *
186
300
  * await screen.ready;
187
301
  * screen.broadcast('phase-update', { phase: 'playing' });
@@ -203,6 +317,10 @@ export interface Screen<TEvents extends EventMap = EventMap> {
203
317
  readonly isReady: boolean;
204
318
  /** Whether the screen has been destroyed. */
205
319
  readonly isDestroyed: boolean;
320
+ /** Whether the connection to the server is active. */
321
+ readonly isConnected: boolean;
322
+ /** Protocol version negotiated with the parent frame. */
323
+ readonly protocolVersion: number;
206
324
  /**
207
325
  * A Promise that resolves when the screen is initialized and ready.
208
326
  * Use this to await readiness after creating a screen synchronously.
@@ -259,13 +377,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
259
377
  * @returns Unsubscribe function to remove the callback
260
378
  */
261
379
  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
380
  /**
270
381
  * Register a callback for when an error occurs.
271
382
  * If no error callback is registered, errors are logged to console in debug mode.
@@ -274,6 +385,14 @@ export interface Screen<TEvents extends EventMap = EventMap> {
274
385
  * @returns Unsubscribe function to remove the callback
275
386
  */
276
387
  onError(callback: (error: SmoreError) => void): () => void;
388
+ /**
389
+ * Register a callback for when the connection status changes.
390
+ * Called when the connection to the server is lost or restored.
391
+ *
392
+ * @param callback - Called with true (connected) or false (disconnected)
393
+ * @returns Unsubscribe function to remove the callback
394
+ */
395
+ onConnectionChange(callback: (connected: boolean) => void): () => void;
277
396
  /**
278
397
  * Broadcast an event to all connected controllers.
279
398
  *
@@ -288,11 +407,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
288
407
  * ```
289
408
  */
290
409
  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
410
  /**
297
411
  * Send an event to a specific controller.
298
412
  *
@@ -310,11 +424,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
310
424
  * ```
311
425
  */
312
426
  sendToController<K extends EventNames<TEvents>>(playerIndex: PlayerIndex, event: K, data: EventData<TEvents, K>): void;
313
- /**
314
- * Send to controller with any data (bypasses type checking).
315
- * Use sparingly - prefer the typed sendToController() method.
316
- */
317
- sendToControllerRaw(playerIndex: PlayerIndex, event: string, data?: unknown): void;
318
427
  /**
319
428
  * Signal that the game is over and send results.
320
429
  * This will broadcast a game-over event to all controllers.
@@ -345,6 +454,17 @@ export interface Screen<TEvents extends EventMap = EventMap> {
345
454
  * ```
346
455
  */
347
456
  signalReady(): void;
457
+ /**
458
+ * Subscribe to a lifecycle event or a user-defined game event.
459
+ *
460
+ * Lifecycle events use `$`-prefixed names. Use `LifecycleEvent` constants
461
+ * for type-safe subscription:
462
+ * ```ts
463
+ * screen.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });
464
+ * screen.on('tap', (pi, data) => { ... }); // user event
465
+ * ```
466
+ */
467
+ on<K extends ScreenLifecycleEvent>(event: K, handler: ScreenLifecycleHandlers[K]): () => void;
348
468
  /**
349
469
  * Add a listener for a specific event after construction.
350
470
  * Can be called before the screen is ready -- handlers are queued
@@ -365,6 +485,7 @@ export interface Screen<TEvents extends EventMap = EventMap> {
365
485
  * ```
366
486
  */
367
487
  on<K extends EventNames<TEvents>>(event: K, handler: ScreenEventHandler<EventData<TEvents, K>>): () => void;
488
+ once<K extends ScreenLifecycleEvent>(event: K, handler: ScreenLifecycleHandlers[K]): () => void;
368
489
  /**
369
490
  * Add a one-time listener that auto-removes after first call.
370
491
  *
@@ -372,10 +493,19 @@ export interface Screen<TEvents extends EventMap = EventMap> {
372
493
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
373
494
  */
374
495
  once<K extends EventNames<TEvents>>(event: K, handler: ScreenEventHandler<EventData<TEvents, K>>): () => void;
496
+ off<K extends ScreenLifecycleEvent>(event: K, handler?: ScreenLifecycleHandlers[K]): void;
375
497
  /**
376
498
  * Remove a specific listener or all listeners for an event.
377
499
  */
378
500
  off<K extends EventNames<TEvents>>(event: K, handler?: ScreenEventHandler<EventData<TEvents, K>>): void;
501
+ /**
502
+ * Remove all event listeners, or all listeners for a specific event.
503
+ * Only removes user event listeners registered via on()/once().
504
+ * Lifecycle callbacks (onControllerJoin, onAllReady, etc.) are not affected.
505
+ *
506
+ * @param event - Optional event name. If omitted, removes ALL user event listeners.
507
+ */
508
+ removeAllListeners(event?: string): void;
379
509
  /**
380
510
  * Get a specific controller by player index.
381
511
  * Returns undefined if not found.
@@ -385,14 +515,6 @@ export interface Screen<TEvents extends EventMap = EventMap> {
385
515
  * Get the number of connected controllers.
386
516
  */
387
517
  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
518
  /**
397
519
  * Clean up all resources and disconnect.
398
520
  * Call this when unmounting/destroying your game.
@@ -424,7 +546,7 @@ export type ControllerEventHandler<TData = unknown> = (data: TData) => void;
424
546
  * controller.send('tap', { x: 100, y: 200 });
425
547
  * ```
426
548
  */
427
- export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
549
+ export interface ControllerConfig {
428
550
  /**
429
551
  * Enable debug mode for verbose logging.
430
552
  * @default false
@@ -440,6 +562,19 @@ export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
440
562
  * @default 10000
441
563
  */
442
564
  timeout?: number;
565
+ /**
566
+ * Automatically signal ready after initialization.
567
+ * When true (default), the SDK calls signalReady() automatically after init completes.
568
+ * Set to false if your game needs to load resources before signaling ready.
569
+ * @default true
570
+ */
571
+ autoReady?: boolean;
572
+ /**
573
+ * Custom transport implementation.
574
+ * If provided, uses this instead of the default PostMessageTransport.
575
+ * The bridge handshake (_bridge:init) still occurs via postMessage.
576
+ */
577
+ transport?: Transport;
443
578
  }
444
579
  /**
445
580
  * Controller instance - the main interface for the player/phone side.
@@ -470,13 +605,17 @@ export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
470
605
  */
471
606
  export interface Controller<TEvents extends EventMap = EventMap> {
472
607
  /** My player index (0, 1, 2, ...). */
473
- readonly myIndex: PlayerIndex;
608
+ readonly myPlayerIndex: PlayerIndex;
474
609
  /** The room code for this game session. */
475
610
  readonly roomCode: RoomCode;
476
611
  /** Whether the controller is initialized and ready. */
477
612
  readonly isReady: boolean;
478
613
  /** Whether the controller has been destroyed. */
479
614
  readonly isDestroyed: boolean;
615
+ /** Whether the connection to the server is active. */
616
+ readonly isConnected: boolean;
617
+ /** Protocol version negotiated with the parent frame. */
618
+ readonly protocolVersion: number;
480
619
  /**
481
620
  * Read-only list of all known controllers (players) in the room.
482
621
  * Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
@@ -542,13 +681,6 @@ export interface Controller<TEvents extends EventMap = EventMap> {
542
681
  * @returns Unsubscribe function to remove the callback
543
682
  */
544
683
  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
684
  /**
553
685
  * Register a callback for when an error occurs.
554
686
  * If no error callback is registered, errors are logged to console in debug mode.
@@ -557,10 +689,31 @@ export interface Controller<TEvents extends EventMap = EventMap> {
557
689
  * @returns Unsubscribe function to remove the callback
558
690
  */
559
691
  onError(callback: (error: SmoreError) => void): () => void;
692
+ /**
693
+ * Register a callback for when the game ends.
694
+ * Called when the Screen calls gameOver().
695
+ *
696
+ * @param callback - Called with optional game results
697
+ * @returns Unsubscribe function to remove the callback
698
+ */
699
+ onGameOver(callback: (results?: GameResults) => void): () => void;
700
+ /**
701
+ * Register a callback for when the connection status changes.
702
+ * Called when the connection to the server is lost or restored.
703
+ *
704
+ * @param callback - Called with true (connected) or false (disconnected)
705
+ * @returns Unsubscribe function to remove the callback
706
+ */
707
+ onConnectionChange(callback: (connected: boolean) => void): () => void;
560
708
  /**
561
709
  * Returns the number of currently connected players.
562
710
  */
563
711
  getControllerCount(): number;
712
+ /**
713
+ * Get a specific controller by player index.
714
+ * Returns undefined if not found.
715
+ */
716
+ getController(playerIndex: PlayerIndex): ControllerInfo | undefined;
564
717
  /**
565
718
  * Send an event to the screen.
566
719
  *
@@ -577,11 +730,6 @@ export interface Controller<TEvents extends EventMap = EventMap> {
577
730
  * ```
578
731
  */
579
732
  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
733
  /**
586
734
  * Signal that this controller has finished loading resources and is ready to start.
587
735
  *
@@ -596,6 +744,17 @@ export interface Controller<TEvents extends EventMap = EventMap> {
596
744
  * ```
597
745
  */
598
746
  signalReady(): void;
747
+ /**
748
+ * Subscribe to a lifecycle event or a user-defined game event.
749
+ *
750
+ * Lifecycle events use `$`-prefixed names. Use `LifecycleEvent` constants
751
+ * for type-safe subscription:
752
+ * ```ts
753
+ * controller.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });
754
+ * controller.on('phase-update', (data) => { ... }); // user event
755
+ * ```
756
+ */
757
+ on<K extends ControllerLifecycleEvent>(event: K, handler: ControllerLifecycleHandlers[K]): () => void;
599
758
  /**
600
759
  * Add a listener for a specific event after construction.
601
760
  * Can be called before the controller is ready -- handlers are queued
@@ -621,6 +780,7 @@ export interface Controller<TEvents extends EventMap = EventMap> {
621
780
  * ```
622
781
  */
623
782
  on<K extends EventNames<TEvents>>(event: K, handler: ControllerEventHandler<EventData<TEvents, K>>): () => void;
783
+ once<K extends ControllerLifecycleEvent>(event: K, handler: ControllerLifecycleHandlers[K]): () => void;
624
784
  /**
625
785
  * Add a one-time listener that auto-removes after first call.
626
786
  *
@@ -628,47 +788,25 @@ export interface Controller<TEvents extends EventMap = EventMap> {
628
788
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
629
789
  */
630
790
  once<K extends EventNames<TEvents>>(event: K, handler: ControllerEventHandler<EventData<TEvents, K>>): () => void;
791
+ off<K extends ControllerLifecycleEvent>(event: K, handler?: ControllerLifecycleHandlers[K]): void;
631
792
  /**
632
793
  * Remove a specific listener or all listeners for an event.
633
794
  */
634
795
  off<K extends EventNames<TEvents>>(event: K, handler?: ControllerEventHandler<EventData<TEvents, K>>): void;
796
+ /**
797
+ * Remove all event listeners, or all listeners for a specific event.
798
+ * Only removes user event listeners registered via on()/once().
799
+ * Lifecycle callbacks (onControllerJoin, onAllReady, etc.) are not affected.
800
+ *
801
+ * @param event - Optional event name. If omitted, removes ALL user event listeners.
802
+ */
803
+ removeAllListeners(event?: string): void;
635
804
  /**
636
805
  * Clean up all resources and disconnect.
637
806
  * Call this when unmounting/destroying your game.
638
807
  */
639
808
  destroy(): void;
640
809
  }
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
810
  /**
673
811
  * Log levels for debug output.
674
812
  */
@@ -692,274 +830,4 @@ export interface DebugOptions {
692
830
  /** Custom logger function */
693
831
  logger?: (level: LogLevel, message: string, data?: unknown) => void;
694
832
  }
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
833
  //# sourceMappingURL=types.d.ts.map