@smoregg/sdk 1.2.0 → 2.0.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 (179) hide show
  1. package/dist/cjs/config.cjs.map +1 -1
  2. package/dist/cjs/controller.cjs +215 -145
  3. package/dist/cjs/controller.cjs.map +1 -1
  4. package/dist/cjs/screen.cjs +220 -178
  5. package/dist/cjs/screen.cjs.map +1 -1
  6. package/dist/cjs/testing.cjs +160 -151
  7. package/dist/cjs/testing.cjs.map +1 -1
  8. package/dist/esm/config.js.map +1 -1
  9. package/dist/esm/controller.js +216 -146
  10. package/dist/esm/controller.js.map +1 -1
  11. package/dist/esm/screen.js +221 -179
  12. package/dist/esm/screen.js.map +1 -1
  13. package/dist/esm/testing.js +160 -151
  14. package/dist/esm/testing.js.map +1 -1
  15. package/dist/types/config.d.ts +1 -2
  16. package/dist/types/config.d.ts.map +1 -1
  17. package/dist/types/controller.d.ts +22 -43
  18. package/dist/types/controller.d.ts.map +1 -1
  19. package/dist/types/index.d.ts +14 -14
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/screen.d.ts +26 -37
  22. package/dist/types/screen.d.ts.map +1 -1
  23. package/dist/types/testing.d.ts +16 -0
  24. package/dist/types/testing.d.ts.map +1 -1
  25. package/dist/types/types.d.ts +244 -338
  26. package/dist/types/types.d.ts.map +1 -1
  27. package/dist/umd/smore-sdk.umd.js +595 -474
  28. package/dist/umd/smore-sdk.umd.js.map +1 -1
  29. package/dist/umd/smore-sdk.umd.min.js +1 -1
  30. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  31. package/package.json +1 -1
  32. package/dist/cjs/SmoreHost.cjs +0 -306
  33. package/dist/cjs/SmoreHost.cjs.map +0 -1
  34. package/dist/cjs/SmorePlayer.cjs +0 -229
  35. package/dist/cjs/SmorePlayer.cjs.map +0 -1
  36. package/dist/cjs/components/DirectionPad.cjs +0 -68
  37. package/dist/cjs/components/DirectionPad.cjs.map +0 -1
  38. package/dist/cjs/components/DirectionPad.module.css.cjs +0 -12
  39. package/dist/cjs/components/DirectionPad.module.css.cjs.map +0 -1
  40. package/dist/cjs/components/HoldButton.cjs +0 -57
  41. package/dist/cjs/components/HoldButton.cjs.map +0 -1
  42. package/dist/cjs/components/HoldButton.module.css.cjs +0 -12
  43. package/dist/cjs/components/HoldButton.module.css.cjs.map +0 -1
  44. package/dist/cjs/components/IframeGameBridge.cjs +0 -115
  45. package/dist/cjs/components/IframeGameBridge.cjs.map +0 -1
  46. package/dist/cjs/components/SwipeArea.cjs +0 -58
  47. package/dist/cjs/components/SwipeArea.cjs.map +0 -1
  48. package/dist/cjs/components/SwipeArea.module.css.cjs +0 -12
  49. package/dist/cjs/components/SwipeArea.module.css.cjs.map +0 -1
  50. package/dist/cjs/components/TapButton.cjs +0 -58
  51. package/dist/cjs/components/TapButton.cjs.map +0 -1
  52. package/dist/cjs/components/TapButton.module.css.cjs +0 -12
  53. package/dist/cjs/components/TapButton.module.css.cjs.map +0 -1
  54. package/dist/cjs/context/RoomProvider.cjs +0 -118
  55. package/dist/cjs/context/RoomProvider.cjs.map +0 -1
  56. package/dist/cjs/hooks/useExternalGames.cjs +0 -49
  57. package/dist/cjs/hooks/useExternalGames.cjs.map +0 -1
  58. package/dist/cjs/hooks/useGameHost.cjs +0 -206
  59. package/dist/cjs/hooks/useGameHost.cjs.map +0 -1
  60. package/dist/cjs/hooks/useGamePlayer.cjs +0 -134
  61. package/dist/cjs/hooks/useGamePlayer.cjs.map +0 -1
  62. package/dist/cjs/iframe/index.cjs +0 -260
  63. package/dist/cjs/iframe/index.cjs.map +0 -1
  64. package/dist/cjs/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.cjs +0 -33
  65. package/dist/cjs/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.cjs.map +0 -1
  66. package/dist/cjs/server/index.cjs +0 -45
  67. package/dist/cjs/server/index.cjs.map +0 -1
  68. package/dist/cjs/transport/DirectTransport.cjs +0 -23
  69. package/dist/cjs/transport/DirectTransport.cjs.map +0 -1
  70. package/dist/cjs/utils/connectionMonitor.cjs +0 -77
  71. package/dist/cjs/utils/connectionMonitor.cjs.map +0 -1
  72. package/dist/cjs/utils/preloadAssets.cjs +0 -66
  73. package/dist/cjs/utils/preloadAssets.cjs.map +0 -1
  74. package/dist/cjs/utils/serverTime.cjs +0 -43
  75. package/dist/cjs/utils/serverTime.cjs.map +0 -1
  76. package/dist/esm/SmoreHost.js +0 -304
  77. package/dist/esm/SmoreHost.js.map +0 -1
  78. package/dist/esm/SmorePlayer.js +0 -227
  79. package/dist/esm/SmorePlayer.js.map +0 -1
  80. package/dist/esm/components/DirectionPad.js +0 -66
  81. package/dist/esm/components/DirectionPad.js.map +0 -1
  82. package/dist/esm/components/DirectionPad.module.css.js +0 -8
  83. package/dist/esm/components/DirectionPad.module.css.js.map +0 -1
  84. package/dist/esm/components/HoldButton.js +0 -55
  85. package/dist/esm/components/HoldButton.js.map +0 -1
  86. package/dist/esm/components/HoldButton.module.css.js +0 -8
  87. package/dist/esm/components/HoldButton.module.css.js.map +0 -1
  88. package/dist/esm/components/IframeGameBridge.js +0 -113
  89. package/dist/esm/components/IframeGameBridge.js.map +0 -1
  90. package/dist/esm/components/SwipeArea.js +0 -56
  91. package/dist/esm/components/SwipeArea.js.map +0 -1
  92. package/dist/esm/components/SwipeArea.module.css.js +0 -8
  93. package/dist/esm/components/SwipeArea.module.css.js.map +0 -1
  94. package/dist/esm/components/TapButton.js +0 -56
  95. package/dist/esm/components/TapButton.js.map +0 -1
  96. package/dist/esm/components/TapButton.module.css.js +0 -8
  97. package/dist/esm/components/TapButton.module.css.js.map +0 -1
  98. package/dist/esm/context/RoomProvider.js +0 -109
  99. package/dist/esm/context/RoomProvider.js.map +0 -1
  100. package/dist/esm/hooks/useExternalGames.js +0 -47
  101. package/dist/esm/hooks/useExternalGames.js.map +0 -1
  102. package/dist/esm/hooks/useGameHost.js +0 -204
  103. package/dist/esm/hooks/useGameHost.js.map +0 -1
  104. package/dist/esm/hooks/useGamePlayer.js +0 -132
  105. package/dist/esm/hooks/useGamePlayer.js.map +0 -1
  106. package/dist/esm/iframe/index.js +0 -257
  107. package/dist/esm/iframe/index.js.map +0 -1
  108. package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js +0 -29
  109. package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js.map +0 -1
  110. package/dist/esm/server/index.js +0 -43
  111. package/dist/esm/server/index.js.map +0 -1
  112. package/dist/esm/transport/DirectTransport.js +0 -21
  113. package/dist/esm/transport/DirectTransport.js.map +0 -1
  114. package/dist/esm/utils/connectionMonitor.js +0 -75
  115. package/dist/esm/utils/connectionMonitor.js.map +0 -1
  116. package/dist/esm/utils/preloadAssets.js +0 -63
  117. package/dist/esm/utils/preloadAssets.js.map +0 -1
  118. package/dist/esm/utils/serverTime.js +0 -41
  119. package/dist/esm/utils/serverTime.js.map +0 -1
  120. package/dist/types/SmoreHost.d.ts +0 -187
  121. package/dist/types/SmoreHost.d.ts.map +0 -1
  122. package/dist/types/SmorePlayer.d.ts +0 -146
  123. package/dist/types/SmorePlayer.d.ts.map +0 -1
  124. package/dist/types/components/DirectionPad.d.ts +0 -21
  125. package/dist/types/components/DirectionPad.d.ts.map +0 -1
  126. package/dist/types/components/HoldButton.d.ts +0 -22
  127. package/dist/types/components/HoldButton.d.ts.map +0 -1
  128. package/dist/types/components/IframeGameBridge.d.ts +0 -38
  129. package/dist/types/components/IframeGameBridge.d.ts.map +0 -1
  130. package/dist/types/components/SwipeArea.d.ts +0 -19
  131. package/dist/types/components/SwipeArea.d.ts.map +0 -1
  132. package/dist/types/components/TapButton.d.ts +0 -19
  133. package/dist/types/components/TapButton.d.ts.map +0 -1
  134. package/dist/types/components/index.d.ts +0 -6
  135. package/dist/types/components/index.d.ts.map +0 -1
  136. package/dist/types/context/RoomProvider.d.ts +0 -69
  137. package/dist/types/context/RoomProvider.d.ts.map +0 -1
  138. package/dist/types/context/index.d.ts +0 -3
  139. package/dist/types/context/index.d.ts.map +0 -1
  140. package/dist/types/dev/DevSimulator.d.ts +0 -31
  141. package/dist/types/dev/DevSimulator.d.ts.map +0 -1
  142. package/dist/types/dev/index.d.ts +0 -2
  143. package/dist/types/dev/index.d.ts.map +0 -1
  144. package/dist/types/hooks/index.d.ts +0 -7
  145. package/dist/types/hooks/index.d.ts.map +0 -1
  146. package/dist/types/hooks/useExternalGames.d.ts +0 -32
  147. package/dist/types/hooks/useExternalGames.d.ts.map +0 -1
  148. package/dist/types/hooks/useGameHost.d.ts +0 -67
  149. package/dist/types/hooks/useGameHost.d.ts.map +0 -1
  150. package/dist/types/hooks/useGamePlayer.d.ts +0 -55
  151. package/dist/types/hooks/useGamePlayer.d.ts.map +0 -1
  152. package/dist/types/iframe/IframeRoomProvider.d.ts +0 -31
  153. package/dist/types/iframe/IframeRoomProvider.d.ts.map +0 -1
  154. package/dist/types/iframe/index.d.ts +0 -18
  155. package/dist/types/iframe/index.d.ts.map +0 -1
  156. package/dist/types/iframe/vanilla-entry.d.ts +0 -7
  157. package/dist/types/iframe/vanilla-entry.d.ts.map +0 -1
  158. package/dist/types/iframe/vanilla.d.ts +0 -49
  159. package/dist/types/iframe/vanilla.d.ts.map +0 -1
  160. package/dist/types/server/createGameRelay.d.ts +0 -26
  161. package/dist/types/server/createGameRelay.d.ts.map +0 -1
  162. package/dist/types/server/index.d.ts +0 -3
  163. package/dist/types/server/index.d.ts.map +0 -1
  164. package/dist/types/utils/connectionMonitor.d.ts +0 -57
  165. package/dist/types/utils/connectionMonitor.d.ts.map +0 -1
  166. package/dist/types/utils/index.d.ts +0 -7
  167. package/dist/types/utils/index.d.ts.map +0 -1
  168. package/dist/types/utils/preloadAssets.d.ts +0 -29
  169. package/dist/types/utils/preloadAssets.d.ts.map +0 -1
  170. package/dist/types/utils/serverTime.d.ts +0 -28
  171. package/dist/types/utils/serverTime.d.ts.map +0 -1
  172. package/dist/umd/smore-sdk-iframe.umd.js +0 -266
  173. package/dist/umd/smore-sdk-iframe.umd.js.map +0 -1
  174. package/dist/umd/smore-sdk-iframe.umd.min.js +0 -2
  175. package/dist/umd/smore-sdk-iframe.umd.min.js.map +0 -1
  176. package/dist/umd/smore-sdk-vanilla.umd.js +0 -1275
  177. package/dist/umd/smore-sdk-vanilla.umd.js.map +0 -1
  178. package/dist/umd/smore-sdk-vanilla.umd.min.js +0 -2
  179. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +0 -1
@@ -45,12 +45,12 @@ export type RoomCode = string;
45
45
  * }
46
46
  *
47
47
  * // With explicit generic -- full type safety
48
- * const screen = await createScreen<MyGameEvents>({ ... });
49
- * screen.broadcast('tap', { x: 1, y: 2 }); // Type-checked
48
+ * const screen = createScreen<MyGameEvents>({ debug: true });
49
+ * screen.on('tap', (pi, data) => { ... });
50
+ * await screen.ready;
50
51
  *
51
52
  * // Without generic -- no type safety (not recommended)
52
- * const screen = await createScreen({ ... });
53
- * screen.broadcastRaw('anything', 'any data'); // No checking
53
+ * const screen = createScreen();
54
54
  * ```
55
55
  *
56
56
  * @example Event naming conventions
@@ -64,6 +64,7 @@ export type RoomCode = string;
64
64
  * ```
65
65
  */
66
66
  export interface EventMap {
67
+ [key: string]: unknown;
67
68
  }
68
69
  /**
69
70
  * Extract event names from an event map.
@@ -128,124 +129,27 @@ export interface ControllerInfo {
128
129
  * Receives the player index and event data.
129
130
  */
130
131
  export type ScreenEventHandler<TData = unknown> = (playerIndex: PlayerIndex, data: TData) => void;
131
- /**
132
- * Map of event listeners for Screen.
133
- * Each handler receives (playerIndex, data).
134
- */
135
- export type ScreenListeners<TEvents extends EventMap> = {
136
- [K in EventNames<TEvents>]?: ScreenEventHandler<EventData<TEvents, K>>;
137
- };
138
132
  /**
139
133
  * Configuration options for creating a Screen instance.
140
134
  *
141
- * @template TEvents - Event map type for type-safe events
135
+ * In the event emitter pattern, only static options are passed via config.
136
+ * All lifecycle callbacks and event listeners are registered via methods
137
+ * on the returned Screen instance.
142
138
  *
143
- * @example Callback-based (immediate)
144
- * ```ts
145
- * const screen = createScreen<MyEvents>({
146
- * onReady: () => {
147
- * console.log('Screen ready!');
148
- * },
149
- * listeners: {
150
- * tap: (playerIndex, data) => handleTap(playerIndex, data),
151
- * },
152
- * });
153
- * ```
139
+ * @template TEvents - Event map type for type-safe events
154
140
  *
155
- * @example Promise-based (await)
141
+ * @example
156
142
  * ```ts
157
- * const screen = await createScreen<MyEvents>({
158
- * listeners: {
159
- * tap: (playerIndex, data) => handleTap(playerIndex, data),
160
- * },
161
- * });
162
- * // Screen is ready here
163
- * ```
143
+ * const screen = createScreen<MyEvents>({ debug: true });
164
144
  *
165
- * ## Best Practices: Callbacks vs Events
145
+ * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));
146
+ * screen.onAllReady(() => startCountdown());
147
+ * screen.onControllerJoin((pi, info) => console.log('Joined:', pi));
166
148
  *
167
- * - **Config callbacks** (`onReady`, `onControllerJoin`, etc.): Use for essential lifecycle events
168
- * that should always be handled. These are registered once at creation and cannot be removed.
169
- *
170
- * - **`on()`/`off()` methods**: Use for dynamic event handlers that may need to be added/removed
171
- * during gameplay. These support multiple handlers per event and can be cleaned up with `off()`.
172
- *
173
- * For most games, config callbacks are sufficient. Use `on()`/`off()` only when you need
174
- * dynamic handler management (e.g., different handlers per game phase).
149
+ * await screen.ready;
150
+ * ```
175
151
  */
176
- export interface ScreenConfig<TEvents extends EventMap = EventMap> {
177
- /**
178
- * Called when the screen is initialized and ready.
179
- * If not provided, use the Promise returned by createScreen().
180
- */
181
- onReady?: () => void;
182
- /**
183
- * Called when a new controller (player) joins the room.
184
- */
185
- onControllerJoin?: (playerIndex: PlayerIndex, info: ControllerInfo) => void;
186
- /**
187
- * Called when a controller (player) leaves the room.
188
- */
189
- onControllerLeave?: (playerIndex: PlayerIndex) => void;
190
- /**
191
- * Called when a controller temporarily disconnects (e.g., lost WiFi).
192
- * The player may reconnect — see onControllerReconnect.
193
- */
194
- onControllerDisconnect?: (playerIndex: PlayerIndex) => void;
195
- /**
196
- * Called when a controller reconnects after a disconnect.
197
- */
198
- onControllerReconnect?: (playerIndex: PlayerIndex, info: ControllerInfo) => void;
199
- /**
200
- * Called when a player's character appearance is updated.
201
- *
202
- * @param playerIndex - The player whose character changed
203
- * @param appearance - The new character appearance, or null if removed/reset
204
- *
205
- * @note Sending an empty/null character update from the platform resets the
206
- * player's appearance to null (default state). This allows players to clear
207
- * their character selection.
208
- */
209
- onCharacterUpdated?: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void;
210
- /**
211
- * Called when the server rate-limits an event from this client.
212
- *
213
- * @param event - The event name that was rate-limited
214
- */
215
- onRateLimited?: (event: string) => void;
216
- /**
217
- * Called when all participants (screen + all connected controllers) have signaled ready.
218
- * Use this as the trigger to start your game's countdown or gameplay.
219
- *
220
- * The ready/start synchronization flow:
221
- * 1. Game loads resources (Phaser assets, images, etc.)
222
- * 2. Game calls `screen.signalReady()` when loading is complete
223
- * 3. Each controller also calls `controller.signalReady()` when ready
224
- * 4. Server broadcasts all-ready when everyone has signaled
225
- * 5. This callback fires on all devices simultaneously
226
- */
227
- onAllReady?: () => void;
228
- /**
229
- * Event listeners for messages from controllers.
230
- * Keys are event names, values are handler functions.
231
- *
232
- * **Relationship with on() method:**
233
- * - Listeners passed in config are registered during initialization
234
- * - Listeners added via on() are registered dynamically after initialization
235
- * - Both types of listeners coexist and are called in registration order
236
- *
237
- * **Important:** Lifecycle listeners (onReady, onControllerJoin, etc.) passed
238
- * in config are NOT removable via off(). They are permanent for the lifetime
239
- * of the Screen instance. Only listeners added via on() can be removed via off().
240
- *
241
- * @see Best Practices section above for guidance on when to use `listeners` config vs `on()` method.
242
- */
243
- listeners?: ScreenListeners<TEvents>;
244
- /**
245
- * Called when an error occurs.
246
- * If not provided, errors are logged to console in debug mode.
247
- */
248
- onError?: (error: SmoreError) => void;
152
+ export interface ScreenConfig<_TEvents extends EventMap = EventMap> {
249
153
  /**
250
154
  * Enable debug mode for verbose logging.
251
155
  * @default false
@@ -262,32 +166,25 @@ export interface ScreenConfig<TEvents extends EventMap = EventMap> {
262
166
  * @default 10000
263
167
  */
264
168
  timeout?: number;
265
- /**
266
- * Automatically signal ready after initialization.
267
- * When true (default), the SDK calls signalReady() automatically after init completes.
268
- * Set to false if your game needs to load resources before signaling ready
269
- * (e.g., Phaser assets, large images, audio files).
270
- *
271
- * @default true
272
- */
273
- autoReady?: boolean;
274
169
  }
275
170
  /**
276
171
  * Screen instance - the main interface for the host/TV side of your game.
277
172
  *
173
+ * Uses an event emitter pattern: lifecycle callbacks and event listeners
174
+ * are registered via methods on the instance, not via config.
175
+ *
278
176
  * @template TEvents - Event map type for type-safe events
279
177
  *
280
178
  * @example
281
179
  * ```ts
282
- * const screen = await createScreen<MyEvents>({ ... });
180
+ * const screen = createScreen<MyEvents>({ debug: true });
283
181
  *
284
- * // Broadcast to all controllers
285
- * screen.broadcast('phase-update', { phase: 'playing' });
286
- *
287
- * // Send to specific controller
288
- * screen.sendToController(0, 'your-turn', { timeLimit: 30 });
182
+ * screen.on('tap', (pi, data) => handleTap(pi, data));
183
+ * screen.onAllReady(() => startGame());
184
+ * screen.onControllerJoin((pi, info) => addPlayer(pi, info));
289
185
  *
290
- * // End the game
186
+ * await screen.ready;
187
+ * screen.broadcast('phase-update', { phase: 'playing' });
291
188
  * screen.gameOver({ scores: { 0: 100, 1: 75 } });
292
189
  * ```
293
190
  */
@@ -306,6 +203,77 @@ export interface Screen<TEvents extends EventMap = EventMap> {
306
203
  readonly isReady: boolean;
307
204
  /** Whether the screen has been destroyed. */
308
205
  readonly isDestroyed: boolean;
206
+ /**
207
+ * A Promise that resolves when the screen is initialized and ready.
208
+ * Use this to await readiness after creating a screen synchronously.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const screen = createScreen<MyEvents>();
213
+ * screen.on('tap', handler);
214
+ * await screen.ready;
215
+ * screen.broadcast('start', {});
216
+ * ```
217
+ */
218
+ readonly ready: Promise<void>;
219
+ /**
220
+ * Register a callback for when all participants are ready (all-ready event).
221
+ * If the all-ready event has already fired when called, the callback fires immediately.
222
+ *
223
+ * @param callback - Called when all participants signal ready
224
+ * @returns Unsubscribe function to remove the callback
225
+ */
226
+ onAllReady(callback: () => void): () => void;
227
+ /**
228
+ * Register a callback for when a controller (player) joins the room.
229
+ *
230
+ * @param callback - Called with player index and controller info
231
+ * @returns Unsubscribe function to remove the callback
232
+ */
233
+ onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;
234
+ /**
235
+ * Register a callback for when a controller (player) leaves the room.
236
+ *
237
+ * @param callback - Called with the leaving player's index
238
+ * @returns Unsubscribe function to remove the callback
239
+ */
240
+ onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void;
241
+ /**
242
+ * Register a callback for when a controller temporarily disconnects.
243
+ *
244
+ * @param callback - Called with the disconnected player's index
245
+ * @returns Unsubscribe function to remove the callback
246
+ */
247
+ onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void;
248
+ /**
249
+ * Register a callback for when a controller reconnects after a disconnect.
250
+ *
251
+ * @param callback - Called with player index and updated controller info
252
+ * @returns Unsubscribe function to remove the callback
253
+ */
254
+ onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;
255
+ /**
256
+ * Register a callback for when a player's character appearance is updated.
257
+ *
258
+ * @param callback - Called with player index and new appearance (or null if reset)
259
+ * @returns Unsubscribe function to remove the callback
260
+ */
261
+ 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
+ /**
270
+ * Register a callback for when an error occurs.
271
+ * If no error callback is registered, errors are logged to console in debug mode.
272
+ *
273
+ * @param callback - Called with the error object
274
+ * @returns Unsubscribe function to remove the callback
275
+ */
276
+ onError(callback: (error: SmoreError) => void): () => void;
309
277
  /**
310
278
  * Broadcast an event to all connected controllers.
311
279
  *
@@ -328,7 +296,7 @@ export interface Screen<TEvents extends EventMap = EventMap> {
328
296
  /**
329
297
  * Send an event to a specific controller.
330
298
  *
331
- * Screen Controller direction only. For Controller Screen, see `send()`.
299
+ * Screen -> Controller direction only. For Controller -> Screen, see `send()`.
332
300
  *
333
301
  * @param playerIndex - Target controller's player index
334
302
  * @param event - Event name (must match TEvents keys)
@@ -379,6 +347,8 @@ export interface Screen<TEvents extends EventMap = EventMap> {
379
347
  signalReady(): void;
380
348
  /**
381
349
  * Add a listener for a specific event after construction.
350
+ * Can be called before the screen is ready -- handlers are queued
351
+ * and activated when the transport becomes available.
382
352
  *
383
353
  * @param event - Event name
384
354
  * @param handler - Handler function (playerIndex, data) => void
@@ -434,122 +404,27 @@ export interface Screen<TEvents extends EventMap = EventMap> {
434
404
  * Receives only the event data (no player index needed).
435
405
  */
436
406
  export type ControllerEventHandler<TData = unknown> = (data: TData) => void;
437
- /**
438
- * Map of event listeners for Controller.
439
- * Each handler receives (data) only.
440
- */
441
- export type ControllerListeners<TEvents extends EventMap> = {
442
- [K in EventNames<TEvents>]?: ControllerEventHandler<EventData<TEvents, K>>;
443
- };
444
407
  /**
445
408
  * Configuration options for creating a Controller instance.
446
409
  *
410
+ * In the event emitter pattern, only static options are passed via config.
411
+ * All lifecycle callbacks and event listeners are registered via methods
412
+ * on the returned Controller instance.
413
+ *
447
414
  * @template TEvents - Event map type for type-safe events
448
415
  *
449
- * @example Callback-based (immediate)
416
+ * @example
450
417
  * ```ts
451
- * const controller = createController<MyEvents>({
452
- * onReady: () => {
453
- * console.log(`Ready! My index: ${controller.myIndex}`);
454
- * },
455
- * listeners: {
456
- * 'phase-update': (data) => handlePhase(data.phase),
457
- * },
458
- * });
459
- * ```
418
+ * const controller = createController<MyEvents>({ debug: true });
460
419
  *
461
- * @example Promise-based (await)
462
- * ```ts
463
- * const controller = await createController<MyEvents>({
464
- * listeners: {
465
- * 'phase-update': (data) => handlePhase(data.phase),
466
- * },
467
- * });
468
- * console.log(`Ready! My index: ${controller.myIndex}`);
420
+ * controller.on('phase-update', (data) => handlePhase(data.phase));
421
+ * controller.onAllReady(() => console.log('Ready!'));
422
+ *
423
+ * await controller.ready;
424
+ * controller.send('tap', { x: 100, y: 200 });
469
425
  * ```
470
426
  */
471
- export interface ControllerConfig<TEvents extends EventMap = EventMap> {
472
- /**
473
- * Called when the controller is initialized and ready.
474
- * If not provided, use the Promise returned by createController().
475
- */
476
- onReady?: () => void;
477
- /**
478
- * Called when another player joins the room.
479
- */
480
- onControllerJoin?: (playerIndex: PlayerIndex, info: ControllerInfo) => void;
481
- /**
482
- * Called when another player leaves the room.
483
- */
484
- onControllerLeave?: (playerIndex: PlayerIndex) => void;
485
- /**
486
- * Called when another player temporarily disconnects.
487
- */
488
- onControllerDisconnect?: (playerIndex: PlayerIndex) => void;
489
- /**
490
- * Called when another player reconnects after a disconnect.
491
- */
492
- onControllerReconnect?: (playerIndex: PlayerIndex, info: ControllerInfo) => void;
493
- /**
494
- * Called when a player's character appearance is updated.
495
- *
496
- * @param playerIndex - The player whose character changed
497
- * @param appearance - The new character appearance, or null if removed/reset
498
- *
499
- * @note Sending an empty/null character update from the platform resets the
500
- * player's appearance to null (default state). This allows players to clear
501
- * their character selection.
502
- */
503
- onCharacterUpdated?: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void;
504
- /**
505
- * Called when the server rate-limits an event from this client.
506
- *
507
- * @param event - The event name that was rate-limited
508
- */
509
- onRateLimited?: (event: string) => void;
510
- /**
511
- * Called when all participants (screen + all connected controllers) have signaled ready.
512
- * Use this as the trigger to start your game's countdown or gameplay.
513
- */
514
- onAllReady?: () => void;
515
- /**
516
- * Called when the host/screen disconnects.
517
- * The game may become unresponsive until the host reconnects.
518
- *
519
- * @experimental This callback is reserved for future use and currently non-functional.
520
- * The server emits `smore:host-disconnected` but it is not yet forwarded to SDK games
521
- * through the IframeGameBridge GAME_FACING_EVENTS allowlist. Setting this callback
522
- * will produce a runtime warning. It will become functional in a future SDK release.
523
- */
524
- onHostDisconnect?: () => void;
525
- /**
526
- * Called when the host/screen reconnects after a disconnect.
527
- *
528
- * @experimental This callback is reserved for future use and currently non-functional.
529
- * The server emits `smore:host-reconnected` but it is not yet forwarded to SDK games
530
- * through the IframeGameBridge GAME_FACING_EVENTS allowlist. Setting this callback
531
- * will produce a runtime warning. It will become functional in a future SDK release.
532
- */
533
- onHostReconnect?: () => void;
534
- /**
535
- * Event listeners for messages from the screen.
536
- * Keys are event names, values are handler functions.
537
- *
538
- * **Relationship with on() method:**
539
- * - Listeners passed in config are registered during initialization
540
- * - Listeners added via on() are registered dynamically after initialization
541
- * - Both types of listeners coexist and are called in registration order
542
- *
543
- * **Important:** Lifecycle listeners (onReady, onControllerJoin, etc.) passed
544
- * in config are NOT removable via off(). They are permanent for the lifetime
545
- * of the Controller instance. Only listeners added via on() can be removed via off().
546
- */
547
- listeners?: ControllerListeners<TEvents>;
548
- /**
549
- * Called when an error occurs.
550
- * If not provided, errors are logged to console in debug mode.
551
- */
552
- onError?: (error: SmoreError) => void;
427
+ export interface ControllerConfig<_TEvents extends EventMap = EventMap> {
553
428
  /**
554
429
  * Enable debug mode for verbose logging.
555
430
  * @default false
@@ -565,26 +440,23 @@ export interface ControllerConfig<TEvents extends EventMap = EventMap> {
565
440
  * @default 10000
566
441
  */
567
442
  timeout?: number;
568
- /**
569
- * Automatically signal ready after initialization.
570
- * When true (default), the SDK calls signalReady() automatically after init completes.
571
- * Set to false if your game needs to load resources before signaling ready
572
- * (e.g., Phaser assets, large images, audio files).
573
- *
574
- * @default true
575
- */
576
- autoReady?: boolean;
577
443
  }
578
444
  /**
579
445
  * Controller instance - the main interface for the player/phone side.
580
446
  *
447
+ * Uses an event emitter pattern: lifecycle callbacks and event listeners
448
+ * are registered via methods on the instance, not via config.
449
+ *
581
450
  * @template TEvents - Event map type for type-safe events
582
451
  *
583
452
  * @example
584
453
  * ```ts
585
- * const controller = await createController<MyEvents>({ ... });
454
+ * const controller = createController<MyEvents>({ debug: true });
455
+ *
456
+ * controller.on('phase-update', (data) => setPhase(data.phase));
457
+ * controller.onAllReady(() => console.log('Game starting!'));
586
458
  *
587
- * // Send input to screen
459
+ * await controller.ready;
588
460
  * controller.send('tap', { x: 100, y: 200 });
589
461
  * ```
590
462
  *
@@ -594,7 +466,7 @@ export interface ControllerConfig<TEvents extends EventMap = EventMap> {
594
466
  * This is intentional: Controller-to-Controller (C2C) communication is not supported.
595
467
  * All coordination between controllers must go through the Screen.
596
468
  *
597
- * Flow: Controller.send() Screen receives Screen.broadcast() or Screen.sendToController()
469
+ * Flow: Controller.send() -> Screen receives -> Screen.broadcast() or Screen.sendToController()
598
470
  */
599
471
  export interface Controller<TEvents extends EventMap = EventMap> {
600
472
  /** My player index (0, 1, 2, ...). */
@@ -614,6 +486,77 @@ export interface Controller<TEvents extends EventMap = EventMap> {
614
486
  * Cache the result in a local variable if you need to access it repeatedly.
615
487
  */
616
488
  readonly controllers: readonly ControllerInfo[];
489
+ /**
490
+ * A Promise that resolves when the controller is initialized and ready.
491
+ * Use this to await readiness after creating a controller synchronously.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * const controller = createController<MyEvents>();
496
+ * controller.on('phase-update', handler);
497
+ * await controller.ready;
498
+ * controller.send('tap', { x: 0, y: 0 });
499
+ * ```
500
+ */
501
+ readonly ready: Promise<void>;
502
+ /**
503
+ * Register a callback for when all participants are ready (all-ready event).
504
+ * If the all-ready event has already fired when called, the callback fires immediately.
505
+ *
506
+ * @param callback - Called when all participants signal ready
507
+ * @returns Unsubscribe function to remove the callback
508
+ */
509
+ onAllReady(callback: () => void): () => void;
510
+ /**
511
+ * Register a callback for when another player joins the room.
512
+ *
513
+ * @param callback - Called with player index and controller info
514
+ * @returns Unsubscribe function to remove the callback
515
+ */
516
+ onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;
517
+ /**
518
+ * Register a callback for when another player leaves the room.
519
+ *
520
+ * @param callback - Called with the leaving player's index
521
+ * @returns Unsubscribe function to remove the callback
522
+ */
523
+ onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void;
524
+ /**
525
+ * Register a callback for when another player temporarily disconnects.
526
+ *
527
+ * @param callback - Called with the disconnected player's index
528
+ * @returns Unsubscribe function to remove the callback
529
+ */
530
+ onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void;
531
+ /**
532
+ * Register a callback for when another player reconnects after a disconnect.
533
+ *
534
+ * @param callback - Called with player index and updated controller info
535
+ * @returns Unsubscribe function to remove the callback
536
+ */
537
+ onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;
538
+ /**
539
+ * Register a callback for when a player's character appearance is updated.
540
+ *
541
+ * @param callback - Called with player index and new appearance (or null if reset)
542
+ * @returns Unsubscribe function to remove the callback
543
+ */
544
+ 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
+ /**
553
+ * Register a callback for when an error occurs.
554
+ * If no error callback is registered, errors are logged to console in debug mode.
555
+ *
556
+ * @param callback - Called with the error object
557
+ * @returns Unsubscribe function to remove the callback
558
+ */
559
+ onError(callback: (error: SmoreError) => void): () => void;
617
560
  /**
618
561
  * Returns the number of currently connected players.
619
562
  */
@@ -621,7 +564,7 @@ export interface Controller<TEvents extends EventMap = EventMap> {
621
564
  /**
622
565
  * Send an event to the screen.
623
566
  *
624
- * Controller Screen direction only. For Screen Controller, see `sendToController()`.
567
+ * Controller -> Screen direction only. For Screen -> Controller, see `sendToController()`.
625
568
  *
626
569
  * @param event - Event name (must match TEvents keys)
627
570
  * @param data - Event data (type-safe based on event name)
@@ -655,6 +598,8 @@ export interface Controller<TEvents extends EventMap = EventMap> {
655
598
  signalReady(): void;
656
599
  /**
657
600
  * Add a listener for a specific event after construction.
601
+ * Can be called before the controller is ready -- handlers are queued
602
+ * and activated when the transport becomes available.
658
603
  *
659
604
  * @param event - Event name
660
605
  * @param handler - Handler function (data) => void
@@ -750,79 +695,52 @@ export interface DebugOptions {
750
695
  /**
751
696
  * Create a Screen instance for the host/TV side of your game.
752
697
  *
753
- * Returns a Promise that resolves when the screen is ready.
754
- * You can also use the onReady callback in config for immediate instantiation.
755
- *
756
- * The return type is a Promise with an `instance` property for dual access:
757
- * - Await the Promise to get the instance after initialization completes
758
- * - Access `instance` property for synchronous access before initialization
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.
759
701
  *
760
702
  * @template TEvents - Event map type for type-safe events
761
- * @param config - Screen configuration
762
- * @returns Promise that resolves to the Screen instance when ready
703
+ * @param config - Screen configuration (debug, parentOrigin, timeout)
704
+ * @returns Screen instance (synchronous)
763
705
  *
764
- * @example Promise-based (recommended)
706
+ * @example
765
707
  * ```ts
766
- * const screen = await createScreen<MyEvents>({
767
- * listeners: {
768
- * tap: (playerIndex, data) => handleTap(playerIndex, data),
769
- * },
708
+ * const screen = createScreen<MyEvents>({ debug: true });
709
+ *
710
+ * screen.on('tap', (playerIndex, data) => {
711
+ * screen.broadcast('round-result', { ... });
770
712
  * });
771
713
  *
772
- * screen.broadcast('game-start', { countdown: 3 });
773
- * ```
714
+ * screen.onAllReady(() => startGame());
715
+ * screen.onControllerJoin((pi, info) => addPlayer(pi, info));
774
716
  *
775
- * @example Callback-based
776
- * ```ts
777
- * const screen = createScreen<MyEvents>({
778
- * onReady: () => {
779
- * screen.broadcast('game-start', { countdown: 3 });
780
- * },
781
- * listeners: { ... },
782
- * });
717
+ * await screen.ready;
783
718
  * ```
784
719
  */
785
- export declare function createScreen<TEvents extends EventMap = EventMap>(config?: ScreenConfig<TEvents>): Promise<Screen<TEvents>> & {
786
- instance: Screen<TEvents>;
787
- };
720
+ export declare function createScreen<TEvents extends EventMap = EventMap>(config?: ScreenConfig<TEvents>): Screen<TEvents>;
788
721
  /**
789
722
  * Create a Controller instance for the player/phone side of your game.
790
723
  *
791
- * Returns a Promise that resolves when the controller is ready.
792
- * You can also use the onReady callback in config for immediate instantiation.
793
- *
794
- * The return type is a Promise with an `instance` property for dual access:
795
- * - Await the Promise to get the instance after initialization completes
796
- * - Access `instance` property for synchronous access before initialization
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.
797
727
  *
798
728
  * @template TEvents - Event map type for type-safe events
799
- * @param config - Controller configuration
800
- * @returns Promise that resolves to the Controller instance when ready
729
+ * @param config - Controller configuration (debug, parentOrigin, timeout)
730
+ * @returns Controller instance (synchronous)
801
731
  *
802
- * @example Promise-based (recommended)
732
+ * @example
803
733
  * ```ts
804
- * const controller = await createController<MyEvents>({
805
- * listeners: {
806
- * 'phase-update': (data) => setPhase(data.phase),
807
- * },
808
- * });
734
+ * const controller = createController<MyEvents>({ debug: true });
809
735
  *
810
- * controller.send('tap', { x: 100, y: 200 });
811
- * ```
736
+ * controller.on('phase-update', (data) => setPhase(data.phase));
737
+ * controller.onAllReady(() => console.log('Ready!'));
812
738
  *
813
- * @example Callback-based
814
- * ```ts
815
- * const controller = createController<MyEvents>({
816
- * onReady: () => {
817
- * controller.send('ready', {});
818
- * },
819
- * listeners: { ... },
820
- * });
739
+ * await controller.ready;
740
+ * controller.send('tap', { x: 100, y: 200 });
821
741
  * ```
822
742
  */
823
- export declare function createController<TEvents extends EventMap = EventMap>(config?: ControllerConfig<TEvents>): Promise<Controller<TEvents>> & {
824
- instance: Controller<TEvents>;
825
- };
743
+ export declare function createController<TEvents extends EventMap = EventMap>(config?: ControllerConfig<TEvents>): Controller<TEvents>;
826
744
  /**
827
745
  * Options for creating mock instances.
828
746
  */
@@ -833,26 +751,8 @@ export interface MockOptions {
833
751
  controllers?: ControllerInfo[];
834
752
  /** My index for Controller mock */
835
753
  myIndex?: PlayerIndex;
836
- /** Auto-trigger onReady */
754
+ /** Auto-trigger ready state */
837
755
  autoReady?: boolean;
838
- /** Called when ready is triggered */
839
- onReady?: () => void;
840
- /** Called when a controller joins */
841
- onControllerJoin?: (playerIndex: PlayerIndex, info: ControllerInfo) => void;
842
- /** Called when a controller leaves */
843
- onControllerLeave?: (playerIndex: PlayerIndex) => void;
844
- /** Called when a controller disconnects */
845
- onControllerDisconnect?: (playerIndex: PlayerIndex) => void;
846
- /** Called when a controller reconnects */
847
- onControllerReconnect?: (playerIndex: PlayerIndex, info: ControllerInfo) => void;
848
- /** Called when a player's character appearance is updated */
849
- onCharacterUpdated?: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void;
850
- /** Called when the server rate-limits an event from this client */
851
- onRateLimited?: (event: string) => void;
852
- /** Called when all participants are ready */
853
- onAllReady?: () => void;
854
- /** Called when an error occurs */
855
- onError?: (error: SmoreError) => void;
856
756
  }
857
757
  /**
858
758
  * Mock Screen for testing.
@@ -866,22 +766,22 @@ export interface MockScreen<TEvents extends EventMap = EventMap> extends Screen<
866
766
  simulateEvent<K extends EventNames<TEvents>>(playerIndex: PlayerIndex, event: K, data: EventData<TEvents, K>): void;
867
767
  /**
868
768
  * Simulate a controller joining.
869
- * Triggers onControllerJoin callback.
769
+ * Triggers onControllerJoin callbacks.
870
770
  */
871
771
  simulateControllerJoin(info: ControllerInfo): void;
872
772
  /**
873
773
  * Simulate a controller leaving.
874
- * Triggers onControllerLeave callback.
774
+ * Triggers onControllerLeave callbacks.
875
775
  */
876
776
  simulateControllerLeave(playerIndex: PlayerIndex): void;
877
777
  /**
878
778
  * Simulate a controller disconnecting temporarily.
879
- * Triggers onControllerDisconnect callback and marks player as disconnected.
779
+ * Triggers onControllerDisconnect callbacks and marks player as disconnected.
880
780
  */
881
781
  simulateControllerDisconnect(playerIndex: PlayerIndex): void;
882
782
  /**
883
783
  * Simulate a controller reconnecting after a disconnect.
884
- * Triggers onControllerReconnect callback and marks player as connected.
784
+ * Triggers onControllerReconnect callbacks and marks player as connected.
885
785
  */
886
786
  simulateControllerReconnect(playerIndex: PlayerIndex): void;
887
787
  /**
@@ -909,20 +809,20 @@ export interface MockScreen<TEvents extends EventMap = EventMap> extends Screen<
909
809
  triggerReady(): void;
910
810
  /**
911
811
  * Simulate a player's character appearance being updated.
912
- * Triggers onCharacterUpdated callback and updates the controller's appearance.
812
+ * Triggers onCharacterUpdated callbacks and updates the controller's appearance.
913
813
  */
914
814
  simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void;
915
815
  /**
916
816
  * Simulate the server rate-limiting an event.
917
- * Triggers onRateLimited callback.
817
+ * Triggers onRateLimited callbacks.
918
818
  */
919
819
  simulateRateLimited(event: string): void;
920
820
  /**
921
821
  * Simulate the all-ready event (all participants signaled ready).
922
- * Triggers onAllReady callback.
822
+ * Triggers onAllReady callbacks.
923
823
  */
924
824
  simulateAllReady(): void;
925
- /** Simulate an error. Triggers onError callback. */
825
+ /** Simulate an error. Triggers onError callbacks. */
926
826
  simulateError(error: any): void;
927
827
  }
928
828
  /**
@@ -953,40 +853,40 @@ export interface MockController<TEvents extends EventMap = EventMap> extends Con
953
853
  triggerReady(): void;
954
854
  /**
955
855
  * Simulate another player joining the room.
956
- * Triggers onControllerJoin callback.
856
+ * Triggers onControllerJoin callbacks.
957
857
  */
958
858
  simulatePlayerJoin(playerIndex: PlayerIndex, info: ControllerInfo): void;
959
859
  /**
960
860
  * Simulate another player leaving the room.
961
- * Triggers onControllerLeave callback.
861
+ * Triggers onControllerLeave callbacks.
962
862
  */
963
863
  simulatePlayerLeave(playerIndex: PlayerIndex): void;
964
864
  /**
965
865
  * Simulate another player disconnecting temporarily.
966
- * Triggers onControllerDisconnect callback.
866
+ * Triggers onControllerDisconnect callbacks.
967
867
  */
968
868
  simulatePlayerDisconnect(playerIndex: PlayerIndex): void;
969
869
  /**
970
870
  * Simulate another player reconnecting after a disconnect.
971
- * Triggers onControllerReconnect callback.
871
+ * Triggers onControllerReconnect callbacks.
972
872
  */
973
873
  simulatePlayerReconnect(playerIndex: PlayerIndex, info: ControllerInfo): void;
974
874
  /**
975
875
  * Simulate a player's character appearance being updated.
976
- * Triggers onCharacterUpdated callback and updates the controller's appearance.
876
+ * Triggers onCharacterUpdated callbacks and updates the controller's appearance.
977
877
  */
978
878
  simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void;
979
879
  /**
980
880
  * Simulate the server rate-limiting an event.
981
- * Triggers onRateLimited callback.
881
+ * Triggers onRateLimited callbacks.
982
882
  */
983
883
  simulateRateLimited(event: string): void;
984
884
  /**
985
885
  * Simulate the all-ready event (all participants signaled ready).
986
- * Triggers onAllReady callback.
886
+ * Triggers onAllReady callbacks.
987
887
  */
988
888
  simulateAllReady(): void;
989
- /** Simulate an error. Triggers onError callback. */
889
+ /** Simulate an error. Triggers onError callbacks. */
990
890
  simulateError(error: any): void;
991
891
  }
992
892
  /**
@@ -1001,6 +901,9 @@ export interface MockController<TEvents extends EventMap = EventMap> extends Con
1001
901
  * ],
1002
902
  * });
1003
903
  *
904
+ * mockScreen.on('tap', (pi, data) => handleTap(pi, data));
905
+ * mockScreen.onAllReady(() => startGame());
906
+ *
1004
907
  * // Simulate player input
1005
908
  * mockScreen.simulateEvent(0, 'tap', { x: 100, y: 200 });
1006
909
  *
@@ -1021,6 +924,9 @@ export declare function createMockScreen<TEvents extends EventMap = EventMap>(op
1021
924
  * myIndex: 0,
1022
925
  * });
1023
926
  *
927
+ * mockController.on('your-turn', (data) => handleTurn(data));
928
+ * mockController.onAllReady(() => console.log('Ready!'));
929
+ *
1024
930
  * // Simulate receiving from screen
1025
931
  * mockController.simulateEvent('your-turn', { timeLimit: 30 });
1026
932
  *