@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.
- package/dist/cjs/controller.cjs +260 -113
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/errors.cjs +1 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/events.cjs +26 -3
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/index.cjs +2 -7
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/screen.cjs +244 -128
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/shared.cjs +34 -0
- package/dist/cjs/shared.cjs.map +1 -0
- package/dist/cjs/testing.cjs +181 -73
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/cjs/transport/PostMessageTransport.cjs +12 -0
- package/dist/cjs/transport/PostMessageTransport.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs +2 -0
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.cjs.map +1 -0
- package/dist/esm/controller.js +262 -115
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/errors.js +1 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/events.js +25 -4
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +1 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/screen.js +246 -130
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/shared.js +30 -0
- package/dist/esm/shared.js.map +1 -0
- package/dist/esm/testing.js +181 -73
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/transport/PostMessageTransport.js +12 -0
- package/dist/esm/transport/PostMessageTransport.js.map +1 -1
- package/dist/esm/transport/protocol.js +2 -1
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/types.js +14 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/controller.d.ts +1 -1
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/events.d.ts +14 -1
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts +3 -3
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/shared.d.ts +21 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/testing.d.ts +65 -4
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/transport/PostMessageTransport.d.ts +1 -0
- package/dist/types/transport/PostMessageTransport.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +5 -0
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +254 -345
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +575 -784
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +7 -1
- package/dist/cjs/config.cjs +0 -13
- package/dist/cjs/config.cjs.map +0 -1
- package/dist/esm/config.js +0 -10
- package/dist/esm/config.js.map +0 -1
- package/dist/types/config.d.ts +0 -35
- package/dist/types/config.d.ts.map +0 -1
package/dist/esm/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sources":["../../src/testing.ts"],"sourcesContent":["/**\n * @smoregg/sdk - Testing Utilities\n *\n * Mock implementations of Screen and Controller for unit testing.\n * All methods work synchronously for predictable test execution.\n *\n * Mocks use the same event emitter pattern as the real implementations:\n * lifecycle callbacks are registered via methods (onAllReady, onControllerJoin, etc.)\n * on the returned instance rather than via config options.\n *\n * @packageDocumentation\n */\n\nimport type {\n EventMap,\n EventNames,\n EventData,\n CharacterAppearance,\n ControllerInfo,\n PlayerIndex,\n GameResults,\n ScreenEventHandler,\n ControllerEventHandler,\n MockScreen,\n MockController,\n MockOptions,\n SmoreError,\n} from './types';\nimport { validateEventName } from './events';\n\n// =============================================================================\n// MOCK SCREEN IMPLEMENTATION\n// =============================================================================\n\ninterface RecordedBroadcast {\n event: string;\n data: unknown;\n}\n\ninterface RecordedSend {\n playerIndex: PlayerIndex;\n event: string;\n data: unknown;\n}\n\n/**\n * Create a mock Screen for testing game logic.\n *\n * All methods work synchronously. Events can be simulated and recorded\n * for assertions in unit tests.\n *\n * Uses the same event emitter pattern as real Screen: register lifecycle\n * callbacks via methods on the returned instance.\n *\n * @example\n * ```ts\n * const screen = createMockScreen<MyEvents>({\n * controllers: [\n * { playerIndex: 0, nickname: 'Player 1', connected: true },\n * { playerIndex: 1, nickname: 'Player 2', connected: true },\n * ],\n * });\n *\n * screen.on('tap', (pi, data) => handleTap(pi, data));\n * screen.onAllReady(() => startGame());\n *\n * // Simulate player input\n * screen.simulateEvent(0, 'tap', { x: 100, y: 200 });\n *\n * // Check what was broadcast\n * expect(screen.getBroadcasts()).toContainEqual({\n * event: 'score-update',\n * data: { scores: { 0: 10 } },\n * });\n * ```\n */\nexport function createMockScreen<TEvents extends EventMap = EventMap>(\n options: MockOptions = {},\n): MockScreen<TEvents> {\n const {\n roomCode = 'TEST',\n controllers: initialControllers = [],\n autoReady = true,\n } = options;\n\n // Internal state\n let _controllers: ControllerInfo[] = [...initialControllers];\n let _isReady = false;\n let _isDestroyed = false;\n\n // Event listeners\n const listeners = new Map<string, Set<ScreenEventHandler>>();\n\n // Lifecycle callback sets\n const _onAllReadyCallbacks = new Set<() => void>();\n const _onControllerJoinCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onControllerLeaveCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerDisconnectCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerReconnectCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onCharacterUpdatedCallbacks = new Set<(index: PlayerIndex, appearance: CharacterAppearance | null) => void>();\n const _onRateLimitedCallbacks = new Set<(event: string) => void>();\n const _onErrorCallbacks = new Set<(error: SmoreError) => void>();\n\n // Whether all-ready has fired\n let _allReadyFired = false;\n\n // Ready promise\n let _readyResolve: () => void;\n const _readyPromise = new Promise<void>((resolve) => {\n _readyResolve = resolve;\n });\n\n // Recorded events for testing\n const broadcasts: RecordedBroadcast[] = [];\n const sends: RecordedSend[] = [];\n\n // Screen implementation\n const screen: MockScreen<TEvents> = {\n // === Properties ===\n get controllers() {\n return [..._controllers];\n },\n get roomCode() {\n return roomCode;\n },\n get isReady() {\n return _isReady;\n },\n get isDestroyed() {\n return _isDestroyed;\n },\n get ready() {\n return _readyPromise;\n },\n\n // === Lifecycle Methods ===\n onAllReady(callback: () => void): () => void {\n if (_allReadyFired) {\n callback();\n }\n _onAllReadyCallbacks.add(callback);\n return () => { _onAllReadyCallbacks.delete(callback); };\n },\n\n onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerJoinCallbacks.add(callback);\n return () => { _onControllerJoinCallbacks.delete(callback); };\n },\n\n onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerLeaveCallbacks.add(callback);\n return () => { _onControllerLeaveCallbacks.delete(callback); };\n },\n\n onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerDisconnectCallbacks.add(callback);\n return () => { _onControllerDisconnectCallbacks.delete(callback); };\n },\n\n onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerReconnectCallbacks.add(callback);\n return () => { _onControllerReconnectCallbacks.delete(callback); };\n },\n\n onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void {\n _onCharacterUpdatedCallbacks.add(callback);\n return () => { _onCharacterUpdatedCallbacks.delete(callback); };\n },\n\n onRateLimited(callback: (event: string) => void): () => void {\n _onRateLimitedCallbacks.add(callback);\n return () => { _onRateLimitedCallbacks.delete(callback); };\n },\n\n onError(callback: (error: SmoreError) => void): () => void {\n _onErrorCallbacks.add(callback);\n return () => { _onErrorCallbacks.delete(callback); };\n },\n\n // === Communication Methods ===\n broadcast<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot broadcast: screen is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot broadcast: screen is not ready');\n }\n validateEventName(event as string);\n broadcasts.push({ event: event as string, data });\n },\n\n broadcastRaw(event: string, data?: unknown): void {\n if (_isDestroyed) {\n throw new Error('Cannot broadcast: screen is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot broadcast: screen is not ready');\n }\n validateEventName(event);\n broadcasts.push({ event, data });\n },\n\n sendToController<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: screen is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot send: screen is not ready');\n }\n validateEventName(event as string);\n if (!_controllers.some((c) => c.playerIndex === playerIndex)) {\n throw new Error(`Invalid player index: ${playerIndex}`);\n }\n sends.push({ playerIndex, event: event as string, data });\n },\n\n sendToControllerRaw(\n playerIndex: PlayerIndex,\n event: string,\n data?: unknown,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: screen is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot send: screen is not ready');\n }\n validateEventName(event);\n if (!_controllers.some((c) => c.playerIndex === playerIndex)) {\n throw new Error(`Invalid player index: ${playerIndex}`);\n }\n sends.push({ playerIndex, event, data });\n },\n\n // === Game Lifecycle ===\n gameOver(results?: GameResults): void {\n if (_isDestroyed) {\n throw new Error('Cannot call gameOver: screen is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot call gameOver: screen is not ready');\n }\n broadcasts.push({ event: 'smore:game-over', data: { results } });\n },\n\n signalReady(): void {\n if (_isDestroyed) {\n throw new Error('Cannot call signalReady: screen is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot call signalReady: screen is not ready');\n }\n // No-op in mock (real implementation emits to server)\n },\n\n // === Event Subscription ===\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void {\n validateEventName(event as string);\n const eventStr = event as string;\n if (!listeners.has(eventStr)) {\n listeners.set(eventStr, new Set());\n }\n listeners.get(eventStr)!.add(handler as ScreenEventHandler);\n\n return () => {\n listeners.get(eventStr)?.delete(handler as ScreenEventHandler);\n };\n },\n\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void {\n validateEventName(event as string);\n const wrapper: ScreenEventHandler<EventData<TEvents, K>> = (playerIndex, data) => {\n handler(playerIndex, data);\n screen.off(event, wrapper);\n };\n return screen.on(event, wrapper);\n },\n\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ScreenEventHandler<EventData<TEvents, K>>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n if (!handler) {\n listeners.delete(eventStr);\n } else {\n listeners.get(eventStr)?.delete(handler as ScreenEventHandler);\n }\n },\n\n // === Utilities ===\n getController(playerIndex: PlayerIndex): ControllerInfo | undefined {\n return _controllers.find((c) => c.playerIndex === playerIndex);\n },\n\n getControllerCount(): number {\n return _controllers.filter(c => c.connected).length;\n },\n\n hasAnyConnectedControllers(): boolean {\n return _controllers.some(c => c.connected);\n },\n\n // === Cleanup ===\n /**\n * Note: destroy() clears recorded broadcast/event arrays. Call getBroadcasts() before destroy() if assertions are needed.\n */\n destroy(): void {\n _isDestroyed = true;\n listeners.clear();\n broadcasts.length = 0;\n sends.length = 0;\n },\n\n // === Mock-specific methods ===\n simulateEvent<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n const handlers = listeners.get(eventStr);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(playerIndex, data);\n });\n }\n },\n\n simulateControllerJoin(info: ControllerInfo): void {\n _controllers.push(info);\n _onControllerJoinCallbacks.forEach(cb => cb(info.playerIndex, info));\n },\n\n simulateControllerLeave(playerIndex: PlayerIndex): void {\n _controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);\n _onControllerLeaveCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a controller network disconnect (player still in room but unreachable).\n */\n simulateControllerDisconnect(playerIndex: PlayerIndex): void {\n const controller = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!controller) {\n throw new Error(`Controller ${playerIndex} not found`);\n }\n // Mark as disconnected (need to create new object since ControllerInfo is readonly)\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex\n ? { ...c, connected: false }\n : c\n );\n _onControllerDisconnectCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a controller network reconnect after disconnect.\n */\n simulateControllerReconnect(playerIndex: PlayerIndex): void {\n const controller = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!controller) {\n throw new Error(`Controller ${playerIndex} not found`);\n }\n // Mark as connected (need to create new object since ControllerInfo is readonly)\n const reconnectedController = { ...controller, connected: true };\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex ? reconnectedController : c\n );\n _onControllerReconnectCallbacks.forEach(cb => cb(playerIndex, reconnectedController));\n },\n\n simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void {\n const controller = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!controller) {\n throw new Error(`Controller ${playerIndex} not found`);\n }\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex ? { ...c, appearance } : c\n );\n _onCharacterUpdatedCallbacks.forEach(cb => cb(playerIndex, appearance));\n },\n\n simulateRateLimited(event: string): void {\n _onRateLimitedCallbacks.forEach(cb => cb(event));\n },\n\n simulateAllReady(): void {\n _allReadyFired = true;\n _onAllReadyCallbacks.forEach(cb => cb());\n },\n\n simulateError(error: any): void {\n _onErrorCallbacks.forEach(cb => cb(error));\n },\n\n getBroadcasts(): Array<{ event: string; data: unknown }> {\n return [...broadcasts];\n },\n\n getSentToController(\n playerIndex: PlayerIndex,\n ): Array<{ event: string; data: unknown }> {\n return sends\n .filter((s) => s.playerIndex === playerIndex)\n .map((s) => ({ event: s.event, data: s.data }));\n },\n\n clearRecordedEvents(): void {\n broadcasts.length = 0;\n sends.length = 0;\n },\n\n triggerReady(): void {\n _isReady = true;\n _readyResolve();\n },\n };\n\n /**\n * Auto-trigger ready if enabled.\n *\n * **Note:** `autoReady` uses `setTimeout(0)` which fires asynchronously (next tick).\n * For synchronous test control, use `autoReady: false` and call `triggerReady()` manually.\n */\n if (autoReady) {\n setTimeout(() => screen.triggerReady(), 0);\n }\n\n return screen;\n}\n\n// =============================================================================\n// MOCK CONTROLLER IMPLEMENTATION\n// =============================================================================\n\ninterface RecordedEvent {\n event: string;\n data: unknown;\n}\n\n/**\n * Create a mock Controller for testing player input logic.\n *\n * All methods work synchronously. Events can be simulated and recorded\n * for assertions in unit tests.\n *\n * Uses the same event emitter pattern as real Controller: register lifecycle\n * callbacks via methods on the returned instance.\n *\n * @example\n * ```ts\n * const controller = createMockController<MyEvents>({\n * myIndex: 0,\n * });\n *\n * controller.on('your-turn', (data) => handleTurn(data));\n * controller.onAllReady(() => console.log('Ready!'));\n *\n * // Simulate receiving from screen\n * controller.simulateEvent('your-turn', { timeLimit: 30 });\n *\n * // Check what was sent\n * expect(controller.getSentEvents()).toContainEqual({\n * event: 'answer',\n * data: { choice: 2 },\n * });\n * ```\n */\nexport function createMockController<TEvents extends EventMap = EventMap>(\n options: MockOptions = {},\n): MockController<TEvents> {\n const {\n roomCode = 'TEST',\n myIndex = 0,\n autoReady = true,\n } = options;\n\n // Internal state -- uses a full ControllerInfo[] array (matching MockScreen pattern)\n // to preserve nickname/appearance data from simulatePlayerJoin()\n let _isReady = false;\n let _isDestroyed = false;\n let _controllers: ControllerInfo[] = options.controllers ?? [];\n\n // Event listeners\n const listeners = new Map<string, Set<ControllerEventHandler>>();\n\n // Lifecycle callback sets\n const _onAllReadyCallbacks = new Set<() => void>();\n const _onControllerJoinCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onControllerLeaveCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerDisconnectCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerReconnectCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onCharacterUpdatedCallbacks = new Set<(index: PlayerIndex, appearance: CharacterAppearance | null) => void>();\n const _onRateLimitedCallbacks = new Set<(event: string) => void>();\n const _onErrorCallbacks = new Set<(error: SmoreError) => void>();\n\n // Whether all-ready has fired\n let _allReadyFired = false;\n\n // Ready promise\n let _readyResolve: () => void;\n const _readyPromise = new Promise<void>((resolve) => {\n _readyResolve = resolve;\n });\n\n // Recorded events for testing\n const sentEvents: RecordedEvent[] = [];\n\n // Controller implementation\n const controller: MockController<TEvents> = {\n // === Properties ===\n get myIndex() {\n return myIndex;\n },\n get roomCode() {\n return roomCode;\n },\n get isReady() {\n return _isReady;\n },\n get isDestroyed() {\n return _isDestroyed;\n },\n\n get controllers(): readonly ControllerInfo[] {\n return [..._controllers];\n },\n\n get ready() {\n return _readyPromise;\n },\n\n // === Lifecycle Methods ===\n onAllReady(callback: () => void): () => void {\n if (_allReadyFired) {\n callback();\n }\n _onAllReadyCallbacks.add(callback);\n return () => { _onAllReadyCallbacks.delete(callback); };\n },\n\n onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerJoinCallbacks.add(callback);\n return () => { _onControllerJoinCallbacks.delete(callback); };\n },\n\n onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerLeaveCallbacks.add(callback);\n return () => { _onControllerLeaveCallbacks.delete(callback); };\n },\n\n onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerDisconnectCallbacks.add(callback);\n return () => { _onControllerDisconnectCallbacks.delete(callback); };\n },\n\n onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerReconnectCallbacks.add(callback);\n return () => { _onControllerReconnectCallbacks.delete(callback); };\n },\n\n onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void {\n _onCharacterUpdatedCallbacks.add(callback);\n return () => { _onCharacterUpdatedCallbacks.delete(callback); };\n },\n\n onRateLimited(callback: (event: string) => void): () => void {\n _onRateLimitedCallbacks.add(callback);\n return () => { _onRateLimitedCallbacks.delete(callback); };\n },\n\n onError(callback: (error: SmoreError) => void): () => void {\n _onErrorCallbacks.add(callback);\n return () => { _onErrorCallbacks.delete(callback); };\n },\n\n getControllerCount(): number {\n return _controllers.filter(c => c.connected).length;\n },\n\n // === Communication Methods ===\n send<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: controller is destroyed');\n }\n validateEventName(event as string);\n sentEvents.push({ event: event as string, data });\n },\n\n sendRaw(event: string, data?: unknown): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: controller is destroyed');\n }\n validateEventName(event);\n sentEvents.push({ event, data });\n },\n\n signalReady(): void {\n if (_isDestroyed) {\n throw new Error('Cannot call signalReady: controller is destroyed');\n }\n if (!_isReady) {\n throw new Error('Cannot call signalReady: controller is not ready');\n }\n // No-op in mock (real implementation emits to server)\n },\n\n // === Event Subscription ===\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void {\n validateEventName(event as string);\n const eventStr = event as string;\n if (!listeners.has(eventStr)) {\n listeners.set(eventStr, new Set());\n }\n listeners.get(eventStr)!.add(handler as ControllerEventHandler);\n\n return () => {\n listeners.get(eventStr)?.delete(handler as ControllerEventHandler);\n };\n },\n\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void {\n validateEventName(event as string);\n const wrapper: ControllerEventHandler<EventData<TEvents, K>> = (data) => {\n handler(data);\n controller.off(event, wrapper);\n };\n return controller.on(event, wrapper);\n },\n\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ControllerEventHandler<EventData<TEvents, K>>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n if (!handler) {\n listeners.delete(eventStr);\n } else {\n listeners.get(eventStr)?.delete(handler as ControllerEventHandler);\n }\n },\n\n // === Cleanup ===\n destroy(): void {\n _isDestroyed = true;\n listeners.clear();\n sentEvents.length = 0;\n },\n\n // === Mock-specific methods ===\n simulateEvent<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n const handlers = listeners.get(eventStr);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(data);\n });\n }\n },\n\n getSentEvents(): Array<{ event: string; data: unknown }> {\n return [...sentEvents];\n },\n\n clearRecordedEvents(): void {\n sentEvents.length = 0;\n },\n\n triggerReady(): void {\n _isReady = true;\n _readyResolve();\n },\n\n /**\n * Simulate a new player joining the room.\n * Stores full ControllerInfo (nickname, appearance, etc.) for later retrieval.\n */\n simulatePlayerJoin(playerIndex: PlayerIndex, info: ControllerInfo): void {\n if (!_controllers.some(c => c.playerIndex === playerIndex)) {\n _controllers = [..._controllers, { ...info, connected: info.connected ?? true }];\n }\n _onControllerJoinCallbacks.forEach(cb => cb(playerIndex, info));\n },\n\n /**\n * Simulate a player leaving the room (fully removed).\n */\n simulatePlayerLeave(playerIndex: PlayerIndex): void {\n _controllers = _controllers.filter(c => c.playerIndex !== playerIndex);\n _onControllerLeaveCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a player network disconnect (player still in room but unreachable).\n */\n simulatePlayerDisconnect(playerIndex: PlayerIndex): void {\n _controllers = _controllers.map(c =>\n c.playerIndex === playerIndex ? { ...c, connected: false } : c\n );\n _onControllerDisconnectCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a player network reconnect after disconnect.\n */\n simulatePlayerReconnect(playerIndex: PlayerIndex, info: ControllerInfo): void {\n _controllers = _controllers.map(c =>\n c.playerIndex === playerIndex ? { ...info, connected: true } : c\n );\n _onControllerReconnectCallbacks.forEach(cb => cb(playerIndex, info));\n },\n\n simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void {\n const ctrl = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!ctrl) {\n throw new Error(`Controller ${playerIndex} not found`);\n }\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex ? { ...c, appearance } : c\n );\n _onCharacterUpdatedCallbacks.forEach(cb => cb(playerIndex, appearance));\n },\n\n simulateRateLimited(event: string): void {\n _onRateLimitedCallbacks.forEach(cb => cb(event));\n },\n\n simulateAllReady(): void {\n _allReadyFired = true;\n _onAllReadyCallbacks.forEach(cb => cb());\n },\n\n simulateError(error: any): void {\n _onErrorCallbacks.forEach(cb => cb(error));\n },\n };\n\n /**\n * Auto-trigger ready if enabled.\n *\n * **Note:** `autoReady` uses `setTimeout(0)` which fires asynchronously (next tick).\n * For synchronous test control, use `autoReady: false` and call `triggerReady()` manually.\n */\n if (autoReady) {\n setTimeout(() => controller.triggerReady(), 0);\n }\n\n return controller;\n}\n\n// =============================================================================\n// EXPORTS\n// =============================================================================\n\nexport type { MockScreen, MockController, MockOptions };\n"],"names":[],"mappings":";;AA4EO,SAAS,gBAAA,CACd,OAAA,GAAuB,EAAC,EACH;AACrB,EAAA,MAAM;AAAA,IACJ,QAAA,GAAW,MAAA;AAAA,IACX,WAAA,EAAa,qBAAqB,EAAC;AAAA,IACnC,SAAA,GAAY;AAAA,GACd,GAAI,OAAA;AAGJ,EAAA,IAAI,YAAA,GAAiC,CAAC,GAAG,kBAAkB,CAAA;AAC3D,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,IAAI,YAAA,GAAe,KAAA;AAGnB,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAqC;AAG3D,EAAA,MAAM,oBAAA,uBAA2B,GAAA,EAAgB;AACjD,EAAA,MAAM,0BAAA,uBAAiC,GAAA,EAAwD;AAC/F,EAAA,MAAM,2BAAA,uBAAkC,GAAA,EAAkC;AAC1E,EAAA,MAAM,gCAAA,uBAAuC,GAAA,EAAkC;AAC/E,EAAA,MAAM,+BAAA,uBAAsC,GAAA,EAAwD;AACpG,EAAA,MAAM,4BAAA,uBAAmC,GAAA,EAA0E;AACnH,EAAA,MAAM,uBAAA,uBAA8B,GAAA,EAA6B;AACjE,EAAA,MAAM,iBAAA,uBAAwB,GAAA,EAAiC;AAG/D,EAAA,IAAI,cAAA,GAAiB,KAAA;AAGrB,EAAA,IAAI,aAAA;AACJ,EAAA,MAAM,aAAA,GAAgB,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnD,IAAA,aAAA,GAAgB,OAAA;AAAA,EAClB,CAAC,CAAA;AAGD,EAAA,MAAM,aAAkC,EAAC;AACzC,EAAA,MAAM,QAAwB,EAAC;AAG/B,EAAA,MAAM,MAAA,GAA8B;AAAA;AAAA,IAElC,IAAI,WAAA,GAAc;AAChB,MAAA,OAAO,CAAC,GAAG,YAAY,CAAA;AAAA,IACzB,CAAA;AAAA,IACA,IAAI,QAAA,GAAW;AACb,MAAA,OAAO,QAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,OAAA,GAAU;AACZ,MAAA,OAAO,QAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,WAAA,GAAc;AAChB,MAAA,OAAO,YAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,KAAA,GAAQ;AACV,MAAA,OAAO,aAAA;AAAA,IACT,CAAA;AAAA;AAAA,IAGA,WAAW,QAAA,EAAkC;AAC3C,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,QAAA,EAAS;AAAA,MACX;AACA,MAAA,oBAAA,CAAqB,IAAI,QAAQ,CAAA;AACjC,MAAA,OAAO,MAAM;AAAE,QAAA,oBAAA,CAAqB,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACxD,CAAA;AAAA,IAEA,iBAAiB,QAAA,EAAgF;AAC/F,MAAA,0BAAA,CAA2B,IAAI,QAAQ,CAAA;AACvC,MAAA,OAAO,MAAM;AAAE,QAAA,0BAAA,CAA2B,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAC9D,CAAA;AAAA,IAEA,kBAAkB,QAAA,EAA0D;AAC1E,MAAA,2BAAA,CAA4B,IAAI,QAAQ,CAAA;AACxC,MAAA,OAAO,MAAM;AAAE,QAAA,2BAAA,CAA4B,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAC/D,CAAA;AAAA,IAEA,uBAAuB,QAAA,EAA0D;AAC/E,MAAA,gCAAA,CAAiC,IAAI,QAAQ,CAAA;AAC7C,MAAA,OAAO,MAAM;AAAE,QAAA,gCAAA,CAAiC,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACpE,CAAA;AAAA,IAEA,sBAAsB,QAAA,EAAgF;AACpG,MAAA,+BAAA,CAAgC,IAAI,QAAQ,CAAA;AAC5C,MAAA,OAAO,MAAM;AAAE,QAAA,+BAAA,CAAgC,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACnE,CAAA;AAAA,IAEA,mBAAmB,QAAA,EAAkG;AACnH,MAAA,4BAAA,CAA6B,IAAI,QAAQ,CAAA;AACzC,MAAA,OAAO,MAAM;AAAE,QAAA,4BAAA,CAA6B,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAChE,CAAA;AAAA,IAEA,cAAc,QAAA,EAA+C;AAC3D,MAAA,uBAAA,CAAwB,IAAI,QAAQ,CAAA;AACpC,MAAA,OAAO,MAAM;AAAE,QAAA,uBAAA,CAAwB,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAC3D,CAAA;AAAA,IAEA,QAAQ,QAAA,EAAmD;AACzD,MAAA,iBAAA,CAAkB,IAAI,QAAQ,CAAA;AAC9B,MAAA,OAAO,MAAM;AAAE,QAAA,iBAAA,CAAkB,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACrD,CAAA;AAAA;AAAA,IAGA,SAAA,CACE,OACA,IAAA,EACM;AACN,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,MACzD;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,MACzD;AACA,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,UAAA,CAAW,IAAA,CAAK,EAAE,KAAA,EAAwB,IAAA,EAAM,CAAA;AAAA,IAClD,CAAA;AAAA,IAEA,YAAA,CAAa,OAAe,IAAA,EAAsB;AAChD,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,MACzD;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,MACzD;AACA,MAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,MAAA,UAAA,CAAW,IAAA,CAAK,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,IACjC,CAAA;AAAA,IAEA,gBAAA,CACE,WAAA,EACA,KAAA,EACA,IAAA,EACM;AACN,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,MACpD;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,MACpD;AACA,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,IAAI,CAAC,aAAa,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,WAAA,KAAgB,WAAW,CAAA,EAAG;AAC5D,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,WAAW,CAAA,CAAE,CAAA;AAAA,MACxD;AACA,MAAA,KAAA,CAAM,IAAA,CAAK,EAAE,WAAA,EAAa,KAAA,EAAwB,MAAM,CAAA;AAAA,IAC1D,CAAA;AAAA,IAEA,mBAAA,CACE,WAAA,EACA,KAAA,EACA,IAAA,EACM;AACN,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,MACpD;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,MACpD;AACA,MAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,MAAA,IAAI,CAAC,aAAa,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,WAAA,KAAgB,WAAW,CAAA,EAAG;AAC5D,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,WAAW,CAAA,CAAE,CAAA;AAAA,MACxD;AACA,MAAA,KAAA,CAAM,IAAA,CAAK,EAAE,WAAA,EAAa,KAAA,EAAO,MAAM,CAAA;AAAA,IACzC,CAAA;AAAA;AAAA,IAGA,SAAS,OAAA,EAA6B;AACpC,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,MAC7D;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,MAC7D;AACA,MAAA,UAAA,CAAW,IAAA,CAAK,EAAE,KAAA,EAAO,iBAAA,EAAmB,MAAM,EAAE,OAAA,IAAW,CAAA;AAAA,IACjE,CAAA;AAAA,IAEA,WAAA,GAAoB;AAClB,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAAA,MAChE;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAAA,MAChE;AAAA,IAEF,CAAA;AAAA;AAAA,IAGA,EAAA,CACE,OACA,OAAA,EACY;AACZ,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,KAAA;AACjB,MAAA,IAAI,CAAC,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG;AAC5B,QAAA,SAAA,CAAU,GAAA,CAAI,QAAA,kBAAU,IAAI,GAAA,EAAK,CAAA;AAAA,MACnC;AACA,MAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,CAAG,GAAA,CAAI,OAA6B,CAAA;AAE1D,MAAA,OAAO,MAAM;AACX,QAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAA,CAAO,OAA6B,CAAA;AAAA,MAC/D,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,IAAA,CACE,OACA,OAAA,EACY;AACZ,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,OAAA,GAAqD,CAAC,WAAA,EAAa,IAAA,KAAS;AAChF,QAAA,OAAA,CAAQ,aAAa,IAAI,CAAA;AACzB,QAAA,MAAA,CAAO,GAAA,CAAI,OAAO,OAAO,CAAA;AAAA,MAC3B,CAAA;AACA,MAAA,OAAO,MAAA,CAAO,EAAA,CAAG,KAAA,EAAO,OAAO,CAAA;AAAA,IACjC,CAAA;AAAA,IAEA,GAAA,CACE,OACA,OAAA,EACM;AACN,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,KAAA;AACjB,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA,MAAO;AACL,QAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAA,CAAO,OAA6B,CAAA;AAAA,MAC/D;AAAA,IACF,CAAA;AAAA;AAAA,IAGA,cAAc,WAAA,EAAsD;AAClE,MAAA,OAAO,aAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAgB,WAAW,CAAA;AAAA,IAC/D,CAAA;AAAA,IAEA,kBAAA,GAA6B;AAC3B,MAAA,OAAO,YAAA,CAAa,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,CAAA,CAAE,MAAA;AAAA,IAC/C,CAAA;AAAA,IAEA,0BAAA,GAAsC;AACpC,MAAA,OAAO,YAAA,CAAa,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,CAAA;AAAA,IAC3C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,OAAA,GAAgB;AACd,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA,UAAA,CAAW,MAAA,GAAS,CAAA;AACpB,MAAA,KAAA,CAAM,MAAA,GAAS,CAAA;AAAA,IACjB,CAAA;AAAA;AAAA,IAGA,aAAA,CACE,WAAA,EACA,KAAA,EACA,IAAA,EACM;AACN,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,KAAA;AACjB,MAAA,MAAM,QAAA,GAAW,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA;AACvC,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC5B,UAAA,OAAA,CAAQ,aAAa,IAAI,CAAA;AAAA,QAC3B,CAAC,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IAEA,uBAAuB,IAAA,EAA4B;AACjD,MAAA,YAAA,CAAa,KAAK,IAAI,CAAA;AACtB,MAAA,0BAAA,CAA2B,QAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,IAAA,CAAK,WAAA,EAAa,IAAI,CAAC,CAAA;AAAA,IACrE,CAAA;AAAA,IAEA,wBAAwB,WAAA,EAAgC;AACtD,MAAA,YAAA,GAAe,aAAa,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAgB,WAAW,CAAA;AACvE,MAAA,2BAAA,CAA4B,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAW,CAAC,CAAA;AAAA,IAC3D,CAAA;AAAA;AAAA;AAAA;AAAA,IAKA,6BAA6B,WAAA,EAAgC;AAC3D,MAAA,MAAM,aAAa,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAgB,WAAW,CAAA;AACzE,MAAA,IAAI,CAAC,UAAA,EAAY;AACf,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,WAAA,EAAc,WAAW,CAAA,UAAA,CAAY,CAAA;AAAA,MACvD;AAEA,MAAA,YAAA,GAAe,YAAA,CAAa,GAAA;AAAA,QAAI,CAAC,CAAA,KAC/B,CAAA,CAAE,WAAA,KAAgB,WAAA,GACd,EAAE,GAAG,CAAA,EAAG,SAAA,EAAW,KAAA,EAAM,GACzB;AAAA,OACN;AACA,MAAA,gCAAA,CAAiC,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAW,CAAC,CAAA;AAAA,IAChE,CAAA;AAAA;AAAA;AAAA;AAAA,IAKA,4BAA4B,WAAA,EAAgC;AAC1D,MAAA,MAAM,aAAa,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAgB,WAAW,CAAA;AACzE,MAAA,IAAI,CAAC,UAAA,EAAY;AACf,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,WAAA,EAAc,WAAW,CAAA,UAAA,CAAY,CAAA;AAAA,MACvD;AAEA,MAAA,MAAM,qBAAA,GAAwB,EAAE,GAAG,UAAA,EAAY,WAAW,IAAA,EAAK;AAC/D,MAAA,YAAA,GAAe,YAAA,CAAa,GAAA;AAAA,QAAI,CAAC,CAAA,KAC/B,CAAA,CAAE,WAAA,KAAgB,cAAc,qBAAA,GAAwB;AAAA,OAC1D;AACA,MAAA,+BAAA,CAAgC,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAA,EAAa,qBAAqB,CAAC,CAAA;AAAA,IACtF,CAAA;AAAA,IAEA,uBAAA,CAAwB,aAA0B,UAAA,EAA8C;AAC9F,MAAA,MAAM,aAAa,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAgB,WAAW,CAAA;AACzE,MAAA,IAAI,CAAC,UAAA,EAAY;AACf,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,WAAA,EAAc,WAAW,CAAA,UAAA,CAAY,CAAA;AAAA,MACvD;AACA,MAAA,YAAA,GAAe,YAAA,CAAa,GAAA;AAAA,QAAI,CAAC,MAC/B,CAAA,CAAE,WAAA,KAAgB,cAAc,EAAE,GAAG,CAAA,EAAG,UAAA,EAAW,GAAI;AAAA,OACzD;AACA,MAAA,4BAAA,CAA6B,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAA,EAAa,UAAU,CAAC,CAAA;AAAA,IACxE,CAAA;AAAA,IAEA,oBAAoB,KAAA,EAAqB;AACvC,MAAA,uBAAA,CAAwB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,KAAK,CAAC,CAAA;AAAA,IACjD,CAAA;AAAA,IAEA,gBAAA,GAAyB;AACvB,MAAA,cAAA,GAAiB,IAAA;AACjB,MAAA,oBAAA,CAAqB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,EAAI,CAAA;AAAA,IACzC,CAAA;AAAA,IAEA,cAAc,KAAA,EAAkB;AAC9B,MAAA,iBAAA,CAAkB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,KAAK,CAAC,CAAA;AAAA,IAC3C,CAAA;AAAA,IAEA,aAAA,GAAyD;AACvD,MAAA,OAAO,CAAC,GAAG,UAAU,CAAA;AAAA,IACvB,CAAA;AAAA,IAEA,oBACE,WAAA,EACyC;AACzC,MAAA,OAAO,MACJ,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,WAAA,KAAgB,WAAW,CAAA,CAC3C,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,KAAA,EAAO,CAAA,CAAE,OAAO,IAAA,EAAM,CAAA,CAAE,MAAK,CAAE,CAAA;AAAA,IAClD,CAAA;AAAA,IAEA,mBAAA,GAA4B;AAC1B,MAAA,UAAA,CAAW,MAAA,GAAS,CAAA;AACpB,MAAA,KAAA,CAAM,MAAA,GAAS,CAAA;AAAA,IACjB,CAAA;AAAA,IAEA,YAAA,GAAqB;AACnB,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,aAAA,EAAc;AAAA,IAChB;AAAA,GACF;AAQA,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,UAAA,CAAW,MAAM,MAAA,CAAO,YAAA,EAAa,EAAG,CAAC,CAAA;AAAA,EAC3C;AAEA,EAAA,OAAO,MAAA;AACT;AAuCO,SAAS,oBAAA,CACd,OAAA,GAAuB,EAAC,EACC;AACzB,EAAA,MAAM;AAAA,IACJ,QAAA,GAAW,MAAA;AAAA,IACX,OAAA,GAAU,CAAA;AAAA,IACV,SAAA,GAAY;AAAA,GACd,GAAI,OAAA;AAIJ,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,IAAI,YAAA,GAAe,KAAA;AACnB,EAAA,IAAI,YAAA,GAAiC,OAAA,CAAQ,WAAA,IAAe,EAAC;AAG7D,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAyC;AAG/D,EAAA,MAAM,oBAAA,uBAA2B,GAAA,EAAgB;AACjD,EAAA,MAAM,0BAAA,uBAAiC,GAAA,EAAwD;AAC/F,EAAA,MAAM,2BAAA,uBAAkC,GAAA,EAAkC;AAC1E,EAAA,MAAM,gCAAA,uBAAuC,GAAA,EAAkC;AAC/E,EAAA,MAAM,+BAAA,uBAAsC,GAAA,EAAwD;AACpG,EAAA,MAAM,4BAAA,uBAAmC,GAAA,EAA0E;AACnH,EAAA,MAAM,uBAAA,uBAA8B,GAAA,EAA6B;AACjE,EAAA,MAAM,iBAAA,uBAAwB,GAAA,EAAiC;AAG/D,EAAA,IAAI,cAAA,GAAiB,KAAA;AAGrB,EAAA,IAAI,aAAA;AACJ,EAAA,MAAM,aAAA,GAAgB,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACnD,IAAA,aAAA,GAAgB,OAAA;AAAA,EAClB,CAAC,CAAA;AAGD,EAAA,MAAM,aAA8B,EAAC;AAGrC,EAAA,MAAM,UAAA,GAAsC;AAAA;AAAA,IAE1C,IAAI,OAAA,GAAU;AACZ,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,QAAA,GAAW;AACb,MAAA,OAAO,QAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,OAAA,GAAU;AACZ,MAAA,OAAO,QAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,WAAA,GAAc;AAChB,MAAA,OAAO,YAAA;AAAA,IACT,CAAA;AAAA,IAEA,IAAI,WAAA,GAAyC;AAC3C,MAAA,OAAO,CAAC,GAAG,YAAY,CAAA;AAAA,IACzB,CAAA;AAAA,IAEA,IAAI,KAAA,GAAQ;AACV,MAAA,OAAO,aAAA;AAAA,IACT,CAAA;AAAA;AAAA,IAGA,WAAW,QAAA,EAAkC;AAC3C,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,QAAA,EAAS;AAAA,MACX;AACA,MAAA,oBAAA,CAAqB,IAAI,QAAQ,CAAA;AACjC,MAAA,OAAO,MAAM;AAAE,QAAA,oBAAA,CAAqB,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACxD,CAAA;AAAA,IAEA,iBAAiB,QAAA,EAAgF;AAC/F,MAAA,0BAAA,CAA2B,IAAI,QAAQ,CAAA;AACvC,MAAA,OAAO,MAAM;AAAE,QAAA,0BAAA,CAA2B,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAC9D,CAAA;AAAA,IAEA,kBAAkB,QAAA,EAA0D;AAC1E,MAAA,2BAAA,CAA4B,IAAI,QAAQ,CAAA;AACxC,MAAA,OAAO,MAAM;AAAE,QAAA,2BAAA,CAA4B,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAC/D,CAAA;AAAA,IAEA,uBAAuB,QAAA,EAA0D;AAC/E,MAAA,gCAAA,CAAiC,IAAI,QAAQ,CAAA;AAC7C,MAAA,OAAO,MAAM;AAAE,QAAA,gCAAA,CAAiC,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACpE,CAAA;AAAA,IAEA,sBAAsB,QAAA,EAAgF;AACpG,MAAA,+BAAA,CAAgC,IAAI,QAAQ,CAAA;AAC5C,MAAA,OAAO,MAAM;AAAE,QAAA,+BAAA,CAAgC,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACnE,CAAA;AAAA,IAEA,mBAAmB,QAAA,EAAkG;AACnH,MAAA,4BAAA,CAA6B,IAAI,QAAQ,CAAA;AACzC,MAAA,OAAO,MAAM;AAAE,QAAA,4BAAA,CAA6B,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAChE,CAAA;AAAA,IAEA,cAAc,QAAA,EAA+C;AAC3D,MAAA,uBAAA,CAAwB,IAAI,QAAQ,CAAA;AACpC,MAAA,OAAO,MAAM;AAAE,QAAA,uBAAA,CAAwB,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IAC3D,CAAA;AAAA,IAEA,QAAQ,QAAA,EAAmD;AACzD,MAAA,iBAAA,CAAkB,IAAI,QAAQ,CAAA;AAC9B,MAAA,OAAO,MAAM;AAAE,QAAA,iBAAA,CAAkB,OAAO,QAAQ,CAAA;AAAA,MAAG,CAAA;AAAA,IACrD,CAAA;AAAA,IAEA,kBAAA,GAA6B;AAC3B,MAAA,OAAO,YAAA,CAAa,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,CAAA,CAAE,MAAA;AAAA,IAC/C,CAAA;AAAA;AAAA,IAGA,IAAA,CACE,OACA,IAAA,EACM;AACN,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AACA,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,UAAA,CAAW,IAAA,CAAK,EAAE,KAAA,EAAwB,IAAA,EAAM,CAAA;AAAA,IAClD,CAAA;AAAA,IAEA,OAAA,CAAQ,OAAe,IAAA,EAAsB;AAC3C,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AACA,MAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,MAAA,UAAA,CAAW,IAAA,CAAK,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,IACjC,CAAA;AAAA,IAEA,WAAA,GAAoB;AAClB,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,MACpE;AACA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,MACpE;AAAA,IAEF,CAAA;AAAA;AAAA,IAGA,EAAA,CACE,OACA,OAAA,EACY;AACZ,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,KAAA;AACjB,MAAA,IAAI,CAAC,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG;AAC5B,QAAA,SAAA,CAAU,GAAA,CAAI,QAAA,kBAAU,IAAI,GAAA,EAAK,CAAA;AAAA,MACnC;AACA,MAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,CAAG,GAAA,CAAI,OAAiC,CAAA;AAE9D,MAAA,OAAO,MAAM;AACX,QAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAA,CAAO,OAAiC,CAAA;AAAA,MACnE,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,IAAA,CACE,OACA,OAAA,EACY;AACZ,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,OAAA,GAAyD,CAAC,IAAA,KAAS;AACvE,QAAA,OAAA,CAAQ,IAAI,CAAA;AACZ,QAAA,UAAA,CAAW,GAAA,CAAI,OAAO,OAAO,CAAA;AAAA,MAC/B,CAAA;AACA,MAAA,OAAO,UAAA,CAAW,EAAA,CAAG,KAAA,EAAO,OAAO,CAAA;AAAA,IACrC,CAAA;AAAA,IAEA,GAAA,CACE,OACA,OAAA,EACM;AACN,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,KAAA;AACjB,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA,MAAO;AACL,QAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAA,CAAO,OAAiC,CAAA;AAAA,MACnE;AAAA,IACF,CAAA;AAAA;AAAA,IAGA,OAAA,GAAgB;AACd,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA,UAAA,CAAW,MAAA,GAAS,CAAA;AAAA,IACtB,CAAA;AAAA;AAAA,IAGA,aAAA,CACE,OACA,IAAA,EACM;AACN,MAAA,iBAAA,CAAkB,KAAe,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,KAAA;AACjB,MAAA,MAAM,QAAA,GAAW,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA;AACvC,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC5B,UAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,QACd,CAAC,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IAEA,aAAA,GAAyD;AACvD,MAAA,OAAO,CAAC,GAAG,UAAU,CAAA;AAAA,IACvB,CAAA;AAAA,IAEA,mBAAA,GAA4B;AAC1B,MAAA,UAAA,CAAW,MAAA,GAAS,CAAA;AAAA,IACtB,CAAA;AAAA,IAEA,YAAA,GAAqB;AACnB,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,kBAAA,CAAmB,aAA0B,IAAA,EAA4B;AACvE,MAAA,IAAI,CAAC,YAAA,CAAa,IAAA,CAAK,OAAK,CAAA,CAAE,WAAA,KAAgB,WAAW,CAAA,EAAG;AAC1D,QAAA,YAAA,GAAe,CAAC,GAAG,YAAA,EAAc,EAAE,GAAG,MAAM,SAAA,EAAW,IAAA,CAAK,SAAA,IAAa,IAAA,EAAM,CAAA;AAAA,MACjF;AACA,MAAA,0BAAA,CAA2B,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAA,EAAa,IAAI,CAAC,CAAA;AAAA,IAChE,CAAA;AAAA;AAAA;AAAA;AAAA,IAKA,oBAAoB,WAAA,EAAgC;AAClD,MAAA,YAAA,GAAe,YAAA,CAAa,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,gBAAgB,WAAW,CAAA;AACrE,MAAA,2BAAA,CAA4B,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAW,CAAC,CAAA;AAAA,IAC3D,CAAA;AAAA;AAAA;AAAA;AAAA,IAKA,yBAAyB,WAAA,EAAgC;AACvD,MAAA,YAAA,GAAe,YAAA,CAAa,GAAA;AAAA,QAAI,CAAA,CAAA,KAC9B,EAAE,WAAA,KAAgB,WAAA,GAAc,EAAE,GAAG,CAAA,EAAG,SAAA,EAAW,KAAA,EAAM,GAAI;AAAA,OAC/D;AACA,MAAA,gCAAA,CAAiC,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAW,CAAC,CAAA;AAAA,IAChE,CAAA;AAAA;AAAA;AAAA;AAAA,IAKA,uBAAA,CAAwB,aAA0B,IAAA,EAA4B;AAC5E,MAAA,YAAA,GAAe,YAAA,CAAa,GAAA;AAAA,QAAI,CAAA,CAAA,KAC9B,EAAE,WAAA,KAAgB,WAAA,GAAc,EAAE,GAAG,IAAA,EAAM,SAAA,EAAW,IAAA,EAAK,GAAI;AAAA,OACjE;AACA,MAAA,+BAAA,CAAgC,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAA,EAAa,IAAI,CAAC,CAAA;AAAA,IACrE,CAAA;AAAA,IAEA,uBAAA,CAAwB,aAA0B,UAAA,EAA8C;AAC9F,MAAA,MAAM,OAAO,YAAA,CAAa,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAgB,WAAW,CAAA;AACnE,MAAA,IAAI,CAAC,IAAA,EAAM;AACT,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,WAAA,EAAc,WAAW,CAAA,UAAA,CAAY,CAAA;AAAA,MACvD;AACA,MAAA,YAAA,GAAe,YAAA,CAAa,GAAA;AAAA,QAAI,CAAC,MAC/B,CAAA,CAAE,WAAA,KAAgB,cAAc,EAAE,GAAG,CAAA,EAAG,UAAA,EAAW,GAAI;AAAA,OACzD;AACA,MAAA,4BAAA,CAA6B,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,WAAA,EAAa,UAAU,CAAC,CAAA;AAAA,IACxE,CAAA;AAAA,IAEA,oBAAoB,KAAA,EAAqB;AACvC,MAAA,uBAAA,CAAwB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,KAAK,CAAC,CAAA;AAAA,IACjD,CAAA;AAAA,IAEA,gBAAA,GAAyB;AACvB,MAAA,cAAA,GAAiB,IAAA;AACjB,MAAA,oBAAA,CAAqB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,EAAI,CAAA;AAAA,IACzC,CAAA;AAAA,IAEA,cAAc,KAAA,EAAkB;AAC9B,MAAA,iBAAA,CAAkB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,CAAG,KAAK,CAAC,CAAA;AAAA,IAC3C;AAAA,GACF;AAQA,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,UAAA,CAAW,MAAM,UAAA,CAAW,YAAA,EAAa,EAAG,CAAC,CAAA;AAAA,EAC/C;AAEA,EAAA,OAAO,UAAA;AACT;;;;"}
|
|
1
|
+
{"version":3,"file":"testing.js","sources":["../../src/testing.ts"],"sourcesContent":["/**\n * @smoregg/sdk - Testing Utilities\n *\n * Mock implementations of Screen and Controller for unit testing.\n * All methods work synchronously for predictable test execution.\n *\n * Mocks use the same event emitter pattern as the real implementations:\n * lifecycle callbacks are registered via methods (onAllReady, onControllerJoin, etc.)\n * on the returned instance rather than via config options.\n *\n * @packageDocumentation\n */\n\nimport type {\n EventMap,\n EventNames,\n EventData,\n CharacterAppearance,\n ControllerInfo,\n PlayerIndex,\n GameResults,\n ScreenEventHandler,\n ControllerEventHandler,\n SmoreError,\n Screen,\n Controller,\n RoomCode,\n} from './types';\nimport { validateEventName } from './events';\nimport { SmoreSDKError } from './errors';\n\n// =============================================================================\n// MOCK TYPES (internal — not part of public API)\n// =============================================================================\n\n/**\n * Options for creating mock instances.\n */\nexport interface MockOptions {\n /** Initial room code */\n roomCode?: RoomCode;\n /** Initial controllers for Screen mock */\n controllers?: ControllerInfo[];\n /** My index for Controller mock */\n myPlayerIndex?: PlayerIndex;\n /** Auto-trigger ready state */\n autoReady?: boolean;\n}\n\n/**\n * Mock Screen for testing.\n * Extends Screen with additional test utilities.\n */\nexport interface MockScreen<TEvents extends EventMap = EventMap>\n extends Screen<TEvents> {\n simulateEvent<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void;\n simulateControllerJoin(info: ControllerInfo): void;\n simulateControllerLeave(playerIndex: PlayerIndex): void;\n simulateControllerDisconnect(playerIndex: PlayerIndex): void;\n simulateControllerReconnect(playerIndex: PlayerIndex): void;\n getBroadcasts(): Array<{ event: string; data: unknown }>;\n getSentToController(playerIndex: PlayerIndex): Array<{ event: string; data: unknown }>;\n clearRecordedEvents(): void;\n triggerReady(): void;\n simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void;\n simulateAllReady(): void;\n simulateError(error: any): void;\n simulateConnectionChange(connected: boolean): void;\n simulateStateChange(playerIndex: number, state: Record<string, any>): void;\n}\n\n/**\n * Mock Controller for testing.\n * Extends Controller with additional test utilities.\n */\nexport interface MockController<TEvents extends EventMap = EventMap>\n extends Controller<TEvents> {\n simulateEvent<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void;\n getSentEvents(): Array<{ event: string; data: unknown }>;\n clearRecordedEvents(): void;\n triggerReady(): void;\n simulatePlayerJoin(playerIndex: PlayerIndex, info: ControllerInfo): void;\n simulatePlayerLeave(playerIndex: PlayerIndex): void;\n simulatePlayerDisconnect(playerIndex: PlayerIndex): void;\n simulatePlayerReconnect(playerIndex: PlayerIndex, info: ControllerInfo): void;\n simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void;\n simulateAllReady(): void;\n simulateError(error: any): void;\n simulateGameOver(results?: GameResults): void;\n simulateConnectionChange(connected: boolean): void;\n simulateStateChange(playerIndex: number, state: Record<string, any>): void;\n}\n\n// =============================================================================\n// MOCK SCREEN IMPLEMENTATION\n// =============================================================================\n\ninterface RecordedBroadcast {\n event: string;\n data: unknown;\n}\n\ninterface RecordedSend {\n playerIndex: PlayerIndex;\n event: string;\n data: unknown;\n}\n\n/**\n * Create a mock Screen for testing game logic.\n *\n * All methods work synchronously. Events can be simulated and recorded\n * for assertions in unit tests.\n *\n * Uses the same event emitter pattern as real Screen: register lifecycle\n * callbacks via methods on the returned instance.\n *\n * @example\n * ```ts\n * const screen = createMockScreen<MyEvents>({\n * controllers: [\n * { playerIndex: 0, nickname: 'Player 1', connected: true },\n * { playerIndex: 1, nickname: 'Player 2', connected: true },\n * ],\n * });\n *\n * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));\n * screen.onAllReady(() => startGame());\n *\n * // Simulate player input\n * screen.simulateEvent(0, 'tap', { x: 100, y: 200 });\n *\n * // Check what was broadcast\n * expect(screen.getBroadcasts()).toContainEqual({\n * event: 'score-update',\n * data: { scores: { 0: 10 } },\n * });\n * ```\n */\nexport function createMockScreen<TEvents extends EventMap = EventMap>(\n options: MockOptions = {},\n): MockScreen<TEvents> {\n const {\n roomCode = 'TEST',\n controllers: initialControllers = [],\n autoReady = true,\n } = options;\n\n // Internal state\n let _controllers: ControllerInfo[] = [...initialControllers];\n let _isReady = false;\n let _isConnected = false;\n let _isDestroyed = false;\n\n // Event listeners\n const listeners = new Map<string, Set<ScreenEventHandler>>();\n\n // Lifecycle callback sets\n const _onAllReadyCallbacks = new Set<() => void>();\n const _onControllerJoinCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onControllerLeaveCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerDisconnectCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerReconnectCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onCharacterUpdatedCallbacks = new Set<(index: PlayerIndex, appearance: CharacterAppearance | null) => void>();\n const _onErrorCallbacks = new Set<(error: SmoreError) => void>();\n const _onConnectionChangeCallbacks = new Set<(connected: boolean) => void>();\n\n // Whether all-ready has fired\n let _allReadyFired = false;\n\n // Ready promise\n let _readyResolve: () => void;\n const _readyPromise = new Promise<void>((resolve) => {\n _readyResolve = resolve;\n });\n\n // Custom state storage\n const _customStates = new Map<number, Record<string, any>>();\n const _stateChangeListeners = new Set<(playerIndex: number, state: Record<string, any>) => void>();\n\n // Recorded events for testing\n const broadcasts: RecordedBroadcast[] = [];\n const sends: RecordedSend[] = [];\n\n // Screen implementation\n const screen: MockScreen<TEvents> = {\n // === Properties ===\n get controllers() {\n return [..._controllers];\n },\n get roomCode() {\n return roomCode;\n },\n get isReady() {\n return _isReady;\n },\n get isDestroyed() {\n return _isDestroyed;\n },\n get isConnected() {\n return _isConnected;\n },\n get protocolVersion() {\n return 1;\n },\n get ready() {\n return _readyPromise;\n },\n\n // === Lifecycle Methods ===\n onAllReady(callback: () => void): () => void {\n if (_allReadyFired) {\n callback();\n }\n _onAllReadyCallbacks.add(callback);\n return () => { _onAllReadyCallbacks.delete(callback); };\n },\n\n onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerJoinCallbacks.add(callback);\n return () => { _onControllerJoinCallbacks.delete(callback); };\n },\n\n onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerLeaveCallbacks.add(callback);\n return () => { _onControllerLeaveCallbacks.delete(callback); };\n },\n\n onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerDisconnectCallbacks.add(callback);\n return () => { _onControllerDisconnectCallbacks.delete(callback); };\n },\n\n onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerReconnectCallbacks.add(callback);\n return () => { _onControllerReconnectCallbacks.delete(callback); };\n },\n\n onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void {\n _onCharacterUpdatedCallbacks.add(callback);\n return () => { _onCharacterUpdatedCallbacks.delete(callback); };\n },\n\n onError(callback: (error: SmoreError) => void): () => void {\n _onErrorCallbacks.add(callback);\n return () => { _onErrorCallbacks.delete(callback); };\n },\n\n onConnectionChange(callback: (connected: boolean) => void): () => void {\n _onConnectionChangeCallbacks.add(callback);\n return () => { _onConnectionChangeCallbacks.delete(callback); };\n },\n\n // === Communication Methods ===\n broadcast<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new SmoreSDKError('DESTROYED', 'Cannot call broadcast() after destroy()');\n }\n if (!_isReady) {\n throw new SmoreSDKError('NOT_READY', 'Cannot call broadcast() before screen is ready');\n }\n validateEventName(event as string);\n broadcasts.push({ event: event as string, data });\n },\n\n sendToController<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new SmoreSDKError('DESTROYED', 'Cannot call sendToController() after destroy()');\n }\n if (!_isReady) {\n throw new SmoreSDKError('NOT_READY', 'Cannot call sendToController() before screen is ready');\n }\n validateEventName(event as string);\n if (!_controllers.some((c) => c.playerIndex === playerIndex)) {\n throw new SmoreSDKError('INVALID_PLAYER', `No controller found with player index ${playerIndex}`);\n }\n sends.push({ playerIndex, event: event as string, data });\n },\n\n // === Custom State Methods ===\n getControllerState(playerIndex: number): Record<string, any> | undefined {\n return _customStates.get(playerIndex);\n },\n\n getAllControllerStates(): Record<number, Record<string, any>> {\n const result: Record<number, Record<string, any>> = {};\n _customStates.forEach((state, playerIndex) => {\n result[playerIndex] = state;\n });\n return result;\n },\n\n onCustomStateChange(listener: (playerIndex: number, state: Record<string, any>) => void): () => void {\n _stateChangeListeners.add(listener);\n return () => { _stateChangeListeners.delete(listener); };\n },\n\n simulateStateChange(playerIndex: number, state: Record<string, any>): void {\n const existing = _customStates.get(playerIndex) ?? {};\n const next = { ...existing, ...state };\n _customStates.set(playerIndex, next);\n _stateChangeListeners.forEach(cb => cb(playerIndex, next));\n },\n\n // === Game Lifecycle ===\n gameOver(results?: GameResults): void {\n if (_isDestroyed) {\n throw new SmoreSDKError('DESTROYED', 'Cannot call gameOver() after destroy()');\n }\n if (!_isReady) {\n throw new SmoreSDKError('NOT_READY', 'Cannot call gameOver() before screen is ready');\n }\n broadcasts.push({ event: 'smore:game-over', data: { results } });\n },\n\n signalReady(): void {\n if (_isDestroyed) {\n throw new SmoreSDKError('DESTROYED', 'Cannot call signalReady() after destroy()');\n }\n if (!_isReady) {\n throw new SmoreSDKError('NOT_READY', 'Cannot call signalReady() before screen is ready');\n }\n // No-op in mock (real implementation emits to server)\n },\n\n // === Event Subscription ===\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void {\n const eventStr = event as string;\n\n // Handle $-prefixed lifecycle events by delegating to appropriate lifecycle method\n if (eventStr.startsWith('$')) {\n switch (eventStr) {\n case '$controller-join':\n return screen.onControllerJoin(handler as any);\n case '$controller-leave':\n return screen.onControllerLeave(handler as any);\n case '$controller-disconnect':\n return screen.onControllerDisconnect(handler as any);\n case '$controller-reconnect':\n return screen.onControllerReconnect(handler as any);\n case '$character-updated':\n return screen.onCharacterUpdated(handler as any);\n case '$all-ready':\n return screen.onAllReady(handler as any);\n case '$error':\n return screen.onError(handler as any);\n case '$connection-change':\n return screen.onConnectionChange(handler as any);\n default:\n // Unknown lifecycle event, just treat as regular event\n break;\n }\n }\n\n validateEventName(event as string);\n\n if (!listeners.has(eventStr)) {\n listeners.set(eventStr, new Set());\n }\n listeners.get(eventStr)!.add(handler as ScreenEventHandler);\n\n return () => {\n listeners.get(eventStr)?.delete(handler as ScreenEventHandler);\n };\n },\n\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void {\n validateEventName(event as string);\n const wrapper: ScreenEventHandler<EventData<TEvents, K>> = (playerIndex, data) => {\n handler(playerIndex, data);\n screen.off(event, wrapper);\n };\n return screen.on(event, wrapper);\n },\n\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ScreenEventHandler<EventData<TEvents, K>>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n if (!handler) {\n listeners.delete(eventStr);\n } else {\n listeners.get(eventStr)?.delete(handler as ScreenEventHandler);\n }\n },\n\n removeAllListeners(event?: string): void {\n if (event) {\n listeners.delete(event);\n } else {\n listeners.clear();\n }\n },\n\n // === Utilities ===\n getController(playerIndex: PlayerIndex): ControllerInfo | undefined {\n return _controllers.find((c) => c.playerIndex === playerIndex);\n },\n\n getControllerCount(): number {\n return _controllers.filter(c => c.connected).length;\n },\n\n // === Cleanup ===\n /**\n * Note: destroy() clears recorded broadcast/event arrays. Call getBroadcasts() before destroy() if assertions are needed.\n */\n destroy(): void {\n _isDestroyed = true;\n listeners.clear();\n broadcasts.length = 0;\n sends.length = 0;\n },\n\n // === Mock-specific methods ===\n simulateEvent<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n const handlers = listeners.get(eventStr);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(playerIndex, data);\n });\n }\n },\n\n simulateControllerJoin(info: ControllerInfo): void {\n _controllers.push(info);\n _onControllerJoinCallbacks.forEach(cb => cb(info.playerIndex, info));\n },\n\n simulateControllerLeave(playerIndex: PlayerIndex): void {\n _controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);\n _onControllerLeaveCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a controller network disconnect (player still in room but unreachable).\n */\n simulateControllerDisconnect(playerIndex: PlayerIndex): void {\n const controller = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!controller) {\n throw new SmoreSDKError('INVALID_PLAYER', `Controller ${playerIndex} not found`);\n }\n // Mark as disconnected (need to create new object since ControllerInfo is readonly)\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex\n ? { ...c, connected: false }\n : c\n );\n _onControllerDisconnectCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a controller network reconnect after disconnect.\n */\n simulateControllerReconnect(playerIndex: PlayerIndex): void {\n const controller = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!controller) {\n throw new SmoreSDKError('INVALID_PLAYER', `Controller ${playerIndex} not found`);\n }\n // Mark as connected (need to create new object since ControllerInfo is readonly)\n const reconnectedController = { ...controller, connected: true };\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex ? reconnectedController : c\n );\n _onControllerReconnectCallbacks.forEach(cb => cb(playerIndex, reconnectedController));\n },\n\n simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void {\n const controller = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!controller) {\n throw new SmoreSDKError('INVALID_PLAYER', `Controller ${playerIndex} not found`);\n }\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex ? { ...c, appearance } : c\n );\n _onCharacterUpdatedCallbacks.forEach(cb => cb(playerIndex, appearance));\n },\n\n simulateAllReady(): void {\n _allReadyFired = true;\n _onAllReadyCallbacks.forEach(cb => cb());\n },\n\n simulateError(error: any): void {\n _onErrorCallbacks.forEach(cb => cb(error));\n },\n\n simulateConnectionChange(connected: boolean): void {\n _isConnected = connected;\n _onConnectionChangeCallbacks.forEach(cb => cb(connected));\n },\n\n getBroadcasts(): Array<{ event: string; data: unknown }> {\n return [...broadcasts];\n },\n\n getSentToController(\n playerIndex: PlayerIndex,\n ): Array<{ event: string; data: unknown }> {\n return sends\n .filter((s) => s.playerIndex === playerIndex)\n .map((s) => ({ event: s.event, data: s.data }));\n },\n\n clearRecordedEvents(): void {\n broadcasts.length = 0;\n sends.length = 0;\n },\n\n triggerReady(): void {\n _isReady = true;\n _isConnected = true;\n _readyResolve();\n },\n };\n\n /**\n * Auto-trigger ready if enabled.\n *\n * **Note:** `autoReady` uses `setTimeout(0)` which fires asynchronously (next tick).\n * For synchronous test control, use `autoReady: false` and call `triggerReady()` manually.\n */\n if (autoReady) {\n setTimeout(() => screen.triggerReady(), 0);\n }\n\n return screen;\n}\n\n// =============================================================================\n// MOCK CONTROLLER IMPLEMENTATION\n// =============================================================================\n\ninterface RecordedEvent {\n event: string;\n data: unknown;\n}\n\n/**\n * Create a mock Controller for testing player input logic.\n *\n * All methods work synchronously. Events can be simulated and recorded\n * for assertions in unit tests.\n *\n * Uses the same event emitter pattern as real Controller: register lifecycle\n * callbacks via methods on the returned instance.\n *\n * @example\n * ```ts\n * const controller = createMockController<MyEvents>({\n * myPlayerIndex: 0,\n * });\n *\n * controller.on('your-turn', (data) => handleTurn(data));\n * controller.onAllReady(() => console.log('Ready!'));\n *\n * // Simulate receiving from screen\n * controller.simulateEvent('your-turn', { timeLimit: 30 });\n *\n * // Check what was sent\n * expect(controller.getSentEvents()).toContainEqual({\n * event: 'answer',\n * data: { choice: 2 },\n * });\n * ```\n */\nexport function createMockController<TEvents extends EventMap = EventMap>(\n options: MockOptions = {},\n): MockController<TEvents> {\n const {\n roomCode = 'TEST',\n myPlayerIndex = 0,\n autoReady = true,\n } = options;\n\n // Internal state -- uses a full ControllerInfo[] array (matching MockScreen pattern)\n // to preserve nickname/appearance data from simulatePlayerJoin()\n let _isReady = false;\n let _isConnected = false;\n let _isDestroyed = false;\n let _controllers: ControllerInfo[] = options.controllers ?? [];\n\n // Event listeners\n const listeners = new Map<string, Set<ControllerEventHandler>>();\n\n // Lifecycle callback sets\n const _onAllReadyCallbacks = new Set<() => void>();\n const _onControllerJoinCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onControllerLeaveCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerDisconnectCallbacks = new Set<(index: PlayerIndex) => void>();\n const _onControllerReconnectCallbacks = new Set<(index: PlayerIndex, info: ControllerInfo) => void>();\n const _onCharacterUpdatedCallbacks = new Set<(index: PlayerIndex, appearance: CharacterAppearance | null) => void>();\n const _onErrorCallbacks = new Set<(error: SmoreError) => void>();\n const _onGameOverCallbacks = new Set<(results?: GameResults) => void>();\n const _onConnectionChangeCallbacks = new Set<(connected: boolean) => void>();\n\n // Whether all-ready has fired\n let _allReadyFired = false;\n\n // Ready promise\n let _readyResolve: () => void;\n const _readyPromise = new Promise<void>((resolve) => {\n _readyResolve = resolve;\n });\n\n // Custom state storage\n const _customStates = new Map<number, Record<string, any>>();\n const _stateChangeListeners = new Set<(playerIndex: number, state: Record<string, any>) => void>();\n\n // Recorded events for testing\n const sentEvents: RecordedEvent[] = [];\n\n // Controller implementation\n const controller: MockController<TEvents> = {\n // === Properties ===\n get myPlayerIndex() {\n return myPlayerIndex;\n },\n get roomCode() {\n return roomCode;\n },\n get isReady() {\n return _isReady;\n },\n get isDestroyed() {\n return _isDestroyed;\n },\n\n get isConnected() {\n return _isConnected;\n },\n\n get protocolVersion() {\n return 1;\n },\n\n get controllers(): readonly ControllerInfo[] {\n return [..._controllers];\n },\n\n get ready() {\n return _readyPromise;\n },\n\n // === Lifecycle Methods ===\n onAllReady(callback: () => void): () => void {\n if (_allReadyFired) {\n callback();\n }\n _onAllReadyCallbacks.add(callback);\n return () => { _onAllReadyCallbacks.delete(callback); };\n },\n\n onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerJoinCallbacks.add(callback);\n return () => { _onControllerJoinCallbacks.delete(callback); };\n },\n\n onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerLeaveCallbacks.add(callback);\n return () => { _onControllerLeaveCallbacks.delete(callback); };\n },\n\n onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void {\n _onControllerDisconnectCallbacks.add(callback);\n return () => { _onControllerDisconnectCallbacks.delete(callback); };\n },\n\n onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void {\n _onControllerReconnectCallbacks.add(callback);\n return () => { _onControllerReconnectCallbacks.delete(callback); };\n },\n\n onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void {\n _onCharacterUpdatedCallbacks.add(callback);\n return () => { _onCharacterUpdatedCallbacks.delete(callback); };\n },\n\n onError(callback: (error: SmoreError) => void): () => void {\n _onErrorCallbacks.add(callback);\n return () => { _onErrorCallbacks.delete(callback); };\n },\n\n onConnectionChange(callback: (connected: boolean) => void): () => void {\n _onConnectionChangeCallbacks.add(callback);\n return () => { _onConnectionChangeCallbacks.delete(callback); };\n },\n\n onGameOver(callback: (results?: GameResults) => void): () => void {\n _onGameOverCallbacks.add(callback);\n return () => { _onGameOverCallbacks.delete(callback); };\n },\n\n getControllerCount(): number {\n return _controllers.filter(c => c.connected).length;\n },\n\n getController(playerIndex: PlayerIndex): ControllerInfo | undefined {\n return _controllers.find((c) => c.playerIndex === playerIndex);\n },\n\n get me(): ControllerInfo | undefined {\n return _controllers.find(c => c.playerIndex === myPlayerIndex);\n },\n\n setState(state: Record<string, any>): void {\n const existing = _customStates.get(myPlayerIndex) ?? {};\n const next = { ...existing, ...state };\n _customStates.set(myPlayerIndex, next);\n _stateChangeListeners.forEach(cb => cb(myPlayerIndex, next));\n },\n\n getMyState(): Record<string, any> | undefined {\n return _customStates.get(myPlayerIndex);\n },\n\n onCustomStateChange(listener: (playerIndex: number, state: Record<string, any>) => void): () => void {\n _stateChangeListeners.add(listener);\n return () => { _stateChangeListeners.delete(listener); };\n },\n\n // === Communication Methods ===\n send<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new SmoreSDKError('DESTROYED', 'Cannot call send() after destroy()');\n }\n validateEventName(event as string);\n sentEvents.push({ event: event as string, data });\n },\n\n signalReady(): void {\n if (_isDestroyed) {\n throw new SmoreSDKError('DESTROYED', 'Cannot call signalReady() after destroy()');\n }\n if (!_isReady) {\n throw new SmoreSDKError('NOT_READY', 'Cannot call signalReady() before controller is ready');\n }\n // No-op in mock (real implementation emits to server)\n },\n\n // === Event Subscription ===\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void {\n const eventStr = event as string;\n\n // Handle $-prefixed lifecycle events by delegating to appropriate lifecycle method\n if (eventStr.startsWith('$')) {\n switch (eventStr) {\n case '$controller-join':\n return controller.onControllerJoin(handler as any);\n case '$controller-leave':\n return controller.onControllerLeave(handler as any);\n case '$controller-disconnect':\n return controller.onControllerDisconnect(handler as any);\n case '$controller-reconnect':\n return controller.onControllerReconnect(handler as any);\n case '$character-updated':\n return controller.onCharacterUpdated(handler as any);\n case '$all-ready':\n return controller.onAllReady(handler as any);\n case '$error':\n return controller.onError(handler as any);\n case '$game-over':\n return controller.onGameOver(handler as any);\n case '$state-recovery':\n return controller.onCustomStateChange(handler as any);\n case '$connection-change':\n return controller.onConnectionChange(handler as any);\n default:\n // Unknown lifecycle event, just treat as regular event\n break;\n }\n }\n\n validateEventName(event as string);\n\n if (!listeners.has(eventStr)) {\n listeners.set(eventStr, new Set());\n }\n listeners.get(eventStr)!.add(handler as ControllerEventHandler);\n\n return () => {\n listeners.get(eventStr)?.delete(handler as ControllerEventHandler);\n };\n },\n\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void {\n validateEventName(event as string);\n const wrapper: ControllerEventHandler<EventData<TEvents, K>> = (data) => {\n handler(data);\n controller.off(event, wrapper);\n };\n return controller.on(event, wrapper);\n },\n\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ControllerEventHandler<EventData<TEvents, K>>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n if (!handler) {\n listeners.delete(eventStr);\n } else {\n listeners.get(eventStr)?.delete(handler as ControllerEventHandler);\n }\n },\n\n removeAllListeners(event?: string): void {\n if (event) {\n listeners.delete(event);\n } else {\n listeners.clear();\n }\n },\n\n // === Cleanup ===\n destroy(): void {\n _isDestroyed = true;\n listeners.clear();\n sentEvents.length = 0;\n },\n\n // === Mock-specific methods ===\n simulateEvent<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n validateEventName(event as string);\n const eventStr = event as string;\n const handlers = listeners.get(eventStr);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(data);\n });\n }\n },\n\n getSentEvents(): Array<{ event: string; data: unknown }> {\n return [...sentEvents];\n },\n\n clearRecordedEvents(): void {\n sentEvents.length = 0;\n },\n\n triggerReady(): void {\n _isReady = true;\n _isConnected = true;\n _readyResolve();\n },\n\n /**\n * Simulate a new player joining the room.\n * Stores full ControllerInfo (nickname, appearance, etc.) for later retrieval.\n */\n simulatePlayerJoin(playerIndex: PlayerIndex, info: ControllerInfo): void {\n if (!_controllers.some(c => c.playerIndex === playerIndex)) {\n _controllers = [..._controllers, { ...info, connected: info.connected ?? true }];\n }\n _onControllerJoinCallbacks.forEach(cb => cb(playerIndex, info));\n },\n\n /**\n * Simulate a player leaving the room (fully removed).\n */\n simulatePlayerLeave(playerIndex: PlayerIndex): void {\n _controllers = _controllers.filter(c => c.playerIndex !== playerIndex);\n _onControllerLeaveCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a player network disconnect (player still in room but unreachable).\n */\n simulatePlayerDisconnect(playerIndex: PlayerIndex): void {\n _controllers = _controllers.map(c =>\n c.playerIndex === playerIndex ? { ...c, connected: false } : c\n );\n _onControllerDisconnectCallbacks.forEach(cb => cb(playerIndex));\n },\n\n /**\n * Simulate a player network reconnect after disconnect.\n */\n simulatePlayerReconnect(playerIndex: PlayerIndex, info: ControllerInfo): void {\n _controllers = _controllers.map(c =>\n c.playerIndex === playerIndex ? { ...info, connected: true } : c\n );\n _onControllerReconnectCallbacks.forEach(cb => cb(playerIndex, info));\n },\n\n simulateCharacterUpdate(playerIndex: PlayerIndex, appearance: CharacterAppearance | null): void {\n const ctrl = _controllers.find((c) => c.playerIndex === playerIndex);\n if (!ctrl) {\n throw new SmoreSDKError('INVALID_PLAYER', `Controller ${playerIndex} not found`);\n }\n _controllers = _controllers.map((c) =>\n c.playerIndex === playerIndex ? { ...c, appearance } : c\n );\n _onCharacterUpdatedCallbacks.forEach(cb => cb(playerIndex, appearance));\n },\n\n simulateAllReady(): void {\n _allReadyFired = true;\n _onAllReadyCallbacks.forEach(cb => cb());\n },\n\n simulateError(error: any): void {\n _onErrorCallbacks.forEach(cb => cb(error));\n },\n\n simulateGameOver(results?: GameResults): void {\n _onGameOverCallbacks.forEach(cb => cb(results));\n },\n\n simulateConnectionChange(connected: boolean): void {\n _isConnected = connected;\n _onConnectionChangeCallbacks.forEach(cb => cb(connected));\n },\n\n simulateStateChange(playerIndex: number, state: Record<string, any>): void {\n const existing = _customStates.get(playerIndex) ?? {};\n const next = { ...existing, ...state };\n _customStates.set(playerIndex, next);\n _stateChangeListeners.forEach(cb => cb(playerIndex, next));\n },\n };\n\n /**\n * Auto-trigger ready if enabled.\n *\n * **Note:** `autoReady` uses `setTimeout(0)` which fires asynchronously (next tick).\n * For synchronous test control, use `autoReady: false` and call `triggerReady()` manually.\n */\n if (autoReady) {\n setTimeout(() => controller.triggerReady(), 0);\n }\n\n return controller;\n}\n\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isBridgeMessage } from './protocol.js';
|
|
2
2
|
|
|
3
3
|
class PostMessageTransport {
|
|
4
|
+
static ACK_TIMEOUT = 3e4;
|
|
5
|
+
// 30 seconds
|
|
4
6
|
handlers = /* @__PURE__ */ new Map();
|
|
5
7
|
ackCallbacks = /* @__PURE__ */ new Map();
|
|
6
8
|
ackCounter = 0;
|
|
@@ -19,11 +21,21 @@ class PostMessageTransport {
|
|
|
19
21
|
const callback = args[0];
|
|
20
22
|
ackId = `ack_${++this.ackCounter}`;
|
|
21
23
|
this.ackCallbacks.set(ackId, callback);
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
if (this.ackCallbacks.has(ackId)) {
|
|
26
|
+
this.ackCallbacks.delete(ackId);
|
|
27
|
+
}
|
|
28
|
+
}, PostMessageTransport.ACK_TIMEOUT);
|
|
22
29
|
} else if (args.length >= 2 && typeof args[args.length - 1] === "function") {
|
|
23
30
|
data = args[0];
|
|
24
31
|
const callback = args[args.length - 1];
|
|
25
32
|
ackId = `ack_${++this.ackCounter}`;
|
|
26
33
|
this.ackCallbacks.set(ackId, callback);
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
if (this.ackCallbacks.has(ackId)) {
|
|
36
|
+
this.ackCallbacks.delete(ackId);
|
|
37
|
+
}
|
|
38
|
+
}, PostMessageTransport.ACK_TIMEOUT);
|
|
27
39
|
}
|
|
28
40
|
window.parent.postMessage(
|
|
29
41
|
{ type: "_bridge:emit", payload: { event, data, ackId } },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostMessageTransport.js","sources":["../../../src/transport/PostMessageTransport.ts"],"sourcesContent":["/**\n * PostMessageTransport - Transport over window.postMessage for iframe-hosted games.\n *\n * Used inside an iframe. Sends `_bridge:emit` to parent and listens for `_bridge:event` from parent.\n */\n\nimport type { Transport, TransportEventHandler } from './types';\nimport type { BridgeEventMessage, BridgeAckMessage } from './protocol';\nimport { isBridgeMessage } from './protocol';\n\n/**\n * PostMessage-based transport for iframe-hosted games.\n *\n * Handles bi-directional communication between iframe game and parent platform using _bridge:* protocol.\n * - Outbound: `_bridge:emit` messages sent to parent\n * - Inbound: `_bridge:event` messages received from parent\n * - Acknowledgment: `_bridge:ack` pattern for request-response flows\n *\n * @example\n * ```ts\n * const transport = new PostMessageTransport('https://smore.gg');\n * transport.on('game-start', (data) => console.log('Game started', data));\n * transport.emit('player-ready', { playerIndex: 0 });\n * ```\n */\nexport class PostMessageTransport implements Transport {\n private handlers = new Map<string, Set<TransportEventHandler>>();\n private ackCallbacks = new Map<string, (...args: unknown[]) => void>();\n private ackCounter = 0;\n private parentOrigin: string;\n private boundMessageHandler: (e: MessageEvent) => void;\n\n constructor(parentOrigin: string = '*') {\n this.parentOrigin = parentOrigin;\n this.boundMessageHandler = this.handleMessage.bind(this);\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n emit(event: string, ...args: unknown[]): void {\n // Detect if last arg is a callback (ack pattern)\n let data: unknown = args[0];\n let ackId: string | undefined;\n\n // Branch 1: emit('event', callback) shorthand — no data, callback only\n // Callback will be invoked when parent sends _bridge:ack with matching ackId\n if (args.length === 1 && typeof args[0] === 'function') {\n data = undefined;\n const callback = args[0] as (...cbArgs: unknown[]) => void;\n ackId = `ack_${++this.ackCounter}`;\n this.ackCallbacks.set(ackId, callback);\n }\n // Branch 2: emit('event', data, callback) — data + callback (request-response pattern)\n // Parent receives event with data and can send _bridge:ack response\n else if (args.length >= 2 && typeof args[args.length - 1] === 'function') {\n data = args[0];\n const callback = args[args.length - 1] as (...cbArgs: unknown[]) => void;\n ackId = `ack_${++this.ackCounter}`;\n this.ackCallbacks.set(ackId, callback);\n }\n\n window.parent.postMessage(\n { type: '_bridge:emit', payload: { event, data, ackId } },\n this.parentOrigin,\n );\n }\n\n on(event: string, handler: TransportEventHandler): void {\n let set = this.handlers.get(event);\n if (!set) {\n set = new Set();\n this.handlers.set(event, set);\n }\n set.add(handler);\n }\n\n off(event: string, handler?: TransportEventHandler): void {\n if (!handler) {\n this.handlers.delete(event);\n return;\n }\n this.handlers.get(event)?.delete(handler);\n }\n\n destroy(): void {\n window.removeEventListener('message', this.boundMessageHandler);\n this.handlers.clear();\n this.ackCallbacks.clear();\n }\n\n private handleMessage(e: MessageEvent): void {\n // Origin validation: only accept messages from the expected parent\n if (this.parentOrigin !== '*' && e.origin !== this.parentOrigin) return;\n\n const msg = e.data;\n if (!isBridgeMessage(msg)) return;\n\n // Branch 1: _bridge:event — state sync from bridge (server → bridge → game)\n // Parent relays socket events to iframe via this type. Game subscribes with on().\n if (msg.type === '_bridge:event') {\n const { event, data } = (msg as BridgeEventMessage).payload;\n const set = this.handlers.get(event);\n if (set) {\n set.forEach((handler) => handler(data));\n }\n }\n // Branch 2: _bridge:ack — response to emit with callback (request-response flow)\n // Fires the callback that was passed to emit('event', data, callback).\n // Edge case: In postMessage environment, callbacks cannot be truly serialized,\n // so only data types that survive JSON serialization roundtrip will work.\n else if (msg.type === '_bridge:ack') {\n const { ackId, data } = (msg as BridgeAckMessage).payload;\n const cb = this.ackCallbacks.get(ackId);\n if (cb) {\n this.ackCallbacks.delete(ackId);\n cb(data);\n }\n }\n }\n}\n"],"names":[],"mappings":";;AAyBO,MAAM,oBAAA,CAA0C;AAAA,
|
|
1
|
+
{"version":3,"file":"PostMessageTransport.js","sources":["../../../src/transport/PostMessageTransport.ts"],"sourcesContent":["/**\n * PostMessageTransport - Transport over window.postMessage for iframe-hosted games.\n *\n * Used inside an iframe. Sends `_bridge:emit` to parent and listens for `_bridge:event` from parent.\n */\n\nimport type { Transport, TransportEventHandler } from './types';\nimport type { BridgeEventMessage, BridgeAckMessage } from './protocol';\nimport { isBridgeMessage } from './protocol';\n\n/**\n * PostMessage-based transport for iframe-hosted games.\n *\n * Handles bi-directional communication between iframe game and parent platform using _bridge:* protocol.\n * - Outbound: `_bridge:emit` messages sent to parent\n * - Inbound: `_bridge:event` messages received from parent\n * - Acknowledgment: `_bridge:ack` pattern for request-response flows\n *\n * @example\n * ```ts\n * const transport = new PostMessageTransport('https://smore.gg');\n * transport.on('game-start', (data) => console.log('Game started', data));\n * transport.emit('player-ready', { playerIndex: 0 });\n * ```\n */\nexport class PostMessageTransport implements Transport {\n private static readonly ACK_TIMEOUT = 30000; // 30 seconds\n private handlers = new Map<string, Set<TransportEventHandler>>();\n private ackCallbacks = new Map<string, (...args: unknown[]) => void>();\n private ackCounter = 0;\n private parentOrigin: string;\n private boundMessageHandler: (e: MessageEvent) => void;\n\n constructor(parentOrigin: string = '*') {\n this.parentOrigin = parentOrigin;\n this.boundMessageHandler = this.handleMessage.bind(this);\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n emit(event: string, ...args: unknown[]): void {\n // Detect if last arg is a callback (ack pattern)\n let data: unknown = args[0];\n let ackId: string | undefined;\n\n // Branch 1: emit('event', callback) shorthand — no data, callback only\n // Callback will be invoked when parent sends _bridge:ack with matching ackId\n if (args.length === 1 && typeof args[0] === 'function') {\n data = undefined;\n const callback = args[0] as (...cbArgs: unknown[]) => void;\n ackId = `ack_${++this.ackCounter}`;\n this.ackCallbacks.set(ackId, callback);\n setTimeout(() => {\n if (this.ackCallbacks.has(ackId!)) {\n this.ackCallbacks.delete(ackId!);\n }\n }, PostMessageTransport.ACK_TIMEOUT);\n }\n // Branch 2: emit('event', data, callback) — data + callback (request-response pattern)\n // Parent receives event with data and can send _bridge:ack response\n else if (args.length >= 2 && typeof args[args.length - 1] === 'function') {\n data = args[0];\n const callback = args[args.length - 1] as (...cbArgs: unknown[]) => void;\n ackId = `ack_${++this.ackCounter}`;\n this.ackCallbacks.set(ackId, callback);\n setTimeout(() => {\n if (this.ackCallbacks.has(ackId!)) {\n this.ackCallbacks.delete(ackId!);\n }\n }, PostMessageTransport.ACK_TIMEOUT);\n }\n\n window.parent.postMessage(\n { type: '_bridge:emit', payload: { event, data, ackId } },\n this.parentOrigin,\n );\n }\n\n on(event: string, handler: TransportEventHandler): void {\n let set = this.handlers.get(event);\n if (!set) {\n set = new Set();\n this.handlers.set(event, set);\n }\n set.add(handler);\n }\n\n off(event: string, handler?: TransportEventHandler): void {\n if (!handler) {\n this.handlers.delete(event);\n return;\n }\n this.handlers.get(event)?.delete(handler);\n }\n\n destroy(): void {\n window.removeEventListener('message', this.boundMessageHandler);\n this.handlers.clear();\n this.ackCallbacks.clear();\n }\n\n private handleMessage(e: MessageEvent): void {\n // Origin validation: only accept messages from the expected parent\n if (this.parentOrigin !== '*' && e.origin !== this.parentOrigin) return;\n\n const msg = e.data;\n if (!isBridgeMessage(msg)) return;\n\n // Branch 1: _bridge:event — state sync from bridge (server → bridge → game)\n // Parent relays socket events to iframe via this type. Game subscribes with on().\n if (msg.type === '_bridge:event') {\n const { event, data } = (msg as BridgeEventMessage).payload;\n const set = this.handlers.get(event);\n if (set) {\n set.forEach((handler) => handler(data));\n }\n }\n // Branch 2: _bridge:ack — response to emit with callback (request-response flow)\n // Fires the callback that was passed to emit('event', data, callback).\n // Edge case: In postMessage environment, callbacks cannot be truly serialized,\n // so only data types that survive JSON serialization roundtrip will work.\n else if (msg.type === '_bridge:ack') {\n const { ackId, data } = (msg as BridgeAckMessage).payload;\n const cb = this.ackCallbacks.get(ackId);\n if (cb) {\n this.ackCallbacks.delete(ackId);\n cb(data);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const BRIDGE_MSG_PREFIX = "_bridge:";
|
|
2
|
+
const PROTOCOL_VERSION = 1;
|
|
2
3
|
function isBridgeMessage(data) {
|
|
3
4
|
return data !== null && typeof data === "object" && "type" in data && typeof data.type === "string" && data.type.startsWith(BRIDGE_MSG_PREFIX);
|
|
4
5
|
}
|
|
@@ -22,5 +23,5 @@ function validateInitPayload(payload) {
|
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export { BRIDGE_MSG_PREFIX, isBridgeMessage, validateInitPayload };
|
|
26
|
+
export { BRIDGE_MSG_PREFIX, PROTOCOL_VERSION, isBridgeMessage, validateInitPayload };
|
|
26
27
|
//# sourceMappingURL=protocol.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sources":["../../../src/transport/protocol.ts"],"sourcesContent":["/**\n * postMessage protocol types for iframe ↔ parent communication.\n *\n * Uses `_bridge:` prefix to clearly distinguish from `smore:*` socket events.\n * - `_bridge:*` = internal iframe postMessage protocol (never on socket)\n * - `smore:*` = platform service events (socket-level, e.g. smore:player-joined)\n */\n\n/**\n * Cross-reference: `CharacterAppearance` (SDK type) has an identical shape to\n * `CharacterDTO` (server type in game-project/types/src/types.ts).\n * If either type changes, the other must be updated to stay in sync.\n */\nimport type { CharacterAppearance } from '../types';\n\nexport const BRIDGE_MSG_PREFIX = '_bridge:' as const;\n\nexport interface BridgeReadyMessage {\n type: '_bridge:ready';\n}\n\n/**\n * BridgeInitMessage contains player data sent from the platform to the game iframe.\n *\n * **Field naming convention:**\n * The server uses `name` and `character` fields (matching server/Player model naming).\n * SDK code may reference fallback fields like `nickname` and `appearance` for defensive\n * compatibility with potential future field name changes, but currently the server\n * always sends `name` and `character`.\n */\nexport interface BridgeInitMessage {\n type: '_bridge:init';\n payload: {\n // 'host' = screen side, 'player' = controller side (legacy naming)\n side: 'host' | 'player';\n roomCode: string;\n players: Array<{\n playerIndex: number;\n name: string;\n connected: boolean;\n character: CharacterAppearance | null;\n }>;\n myIndex?: number;\n };\n}\n\nexport interface BridgeEmitMessage {\n type: '_bridge:emit';\n payload: {\n event: string;\n data?: unknown;\n ackId?: string;\n };\n}\n\nexport interface BridgeEventMessage {\n type: '_bridge:event';\n payload: {\n event: string;\n data?: unknown;\n };\n}\n\nexport interface BridgeAckMessage {\n type: '_bridge:ack';\n payload: {\n ackId: string;\n data?: unknown;\n };\n}\n\nexport interface BridgeUpdateMessage {\n type: '_bridge:update';\n payload: {\n players?: Array<{\n playerIndex: number;\n name: string;\n connected: boolean;\n character: CharacterAppearance | null;\n }>;\n };\n}\n\nexport type BridgeMessage =\n | BridgeReadyMessage\n | BridgeInitMessage\n | BridgeEmitMessage\n | BridgeEventMessage\n | BridgeAckMessage\n | BridgeUpdateMessage;\n\nexport function isBridgeMessage(data: unknown): data is BridgeMessage {\n return (\n data !== null &&\n typeof data === 'object' &&\n 'type' in data &&\n typeof data.type === 'string' &&\n data.type.startsWith(BRIDGE_MSG_PREFIX)\n );\n}\n\n/**\n * Validates the structure of a _bridge:init payload.\n *\n * Performs runtime validation to ensure the payload contains all required fields\n * with correct types. This provides early error detection if the parent frame\n * sends malformed initialization data.\n *\n * @param payload - The payload to validate\n * @returns true if payload is valid\n * @throws {Error} if validation fails with a descriptive error message\n *\n * @example\n * ```ts\n * try {\n * validateInitPayload(msg.payload);\n * // proceed with initialization\n * } catch (err) {\n * console.error('Invalid init payload:', err.message);\n * }\n * ```\n */\nexport function validateInitPayload(payload: unknown): payload is BridgeInitMessage['payload'] {\n if (!payload || typeof payload !== 'object') {\n throw new Error('[SDK] _bridge:init payload must be an object');\n }\n\n const p = payload as Record<string, unknown>;\n\n // Required: side\n if (typeof p.side !== 'string' || !['host', 'player'].includes(p.side)) {\n throw new Error(`[SDK] _bridge:init payload.side must be \"host\" or \"player\", got: ${p.side}`);\n }\n\n // Required: roomCode\n if (typeof p.roomCode !== 'string' || p.roomCode.length === 0) {\n throw new Error('[SDK] _bridge:init payload.roomCode must be a non-empty string');\n }\n\n // Required: players (array)\n if (!Array.isArray(p.players)) {\n throw new Error('[SDK] _bridge:init payload.players must be an array');\n }\n\n // Optional but validated if present: myIndex (controller-side only)\n if (p.myIndex !== undefined && typeof p.myIndex !== 'number') {\n throw new Error('[SDK] _bridge:init payload.myIndex must be a number if provided');\n }\n\n return true;\n}\n"],"names":[],"mappings":"AAeO,MAAM,iBAAA,GAAoB;
|
|
1
|
+
{"version":3,"file":"protocol.js","sources":["../../../src/transport/protocol.ts"],"sourcesContent":["/**\n * postMessage protocol types for iframe ↔ parent communication.\n *\n * Uses `_bridge:` prefix to clearly distinguish from `smore:*` socket events.\n * - `_bridge:*` = internal iframe postMessage protocol (never on socket)\n * - `smore:*` = platform service events (socket-level, e.g. smore:player-joined)\n */\n\n/**\n * Cross-reference: `CharacterAppearance` (SDK type) has an identical shape to\n * `CharacterDTO` (server type in game-project/types/src/types.ts).\n * If either type changes, the other must be updated to stay in sync.\n */\nimport type { CharacterAppearance } from '../types';\n\nexport const BRIDGE_MSG_PREFIX = '_bridge:' as const;\n\n/** Current SDK protocol version. Incremented on breaking protocol changes. */\nexport const PROTOCOL_VERSION = 1;\n\nexport interface BridgeReadyMessage {\n type: '_bridge:ready';\n protocolVersion?: number;\n}\n\n/**\n * BridgeInitMessage contains player data sent from the platform to the game iframe.\n *\n * **Field naming convention:**\n * The server uses `name` and `character` fields (matching server/Player model naming).\n * SDK code may reference fallback fields like `nickname` and `appearance` for defensive\n * compatibility with potential future field name changes, but currently the server\n * always sends `name` and `character`.\n */\nexport interface BridgeInitMessage {\n type: '_bridge:init';\n payload: {\n // 'host' = screen side, 'player' = controller side (legacy naming)\n side: 'host' | 'player';\n roomCode: string;\n players: Array<{\n playerIndex: number;\n name: string;\n connected: boolean;\n character: CharacterAppearance | null;\n }>;\n myIndex?: number;\n protocolVersion?: number;\n gameInProgress?: boolean;\n };\n}\n\nexport interface BridgeEmitMessage {\n type: '_bridge:emit';\n payload: {\n event: string;\n data?: unknown;\n ackId?: string;\n };\n}\n\nexport interface BridgeEventMessage {\n type: '_bridge:event';\n payload: {\n event: string;\n data?: unknown;\n };\n}\n\nexport interface BridgeAckMessage {\n type: '_bridge:ack';\n payload: {\n ackId: string;\n data?: unknown;\n };\n}\n\nexport interface BridgeUpdateMessage {\n type: '_bridge:update';\n payload: {\n players?: Array<{\n playerIndex: number;\n name: string;\n connected: boolean;\n character: CharacterAppearance | null;\n }>;\n };\n}\n\nexport type BridgeMessage =\n | BridgeReadyMessage\n | BridgeInitMessage\n | BridgeEmitMessage\n | BridgeEventMessage\n | BridgeAckMessage\n | BridgeUpdateMessage;\n\nexport function isBridgeMessage(data: unknown): data is BridgeMessage {\n return (\n data !== null &&\n typeof data === 'object' &&\n 'type' in data &&\n typeof data.type === 'string' &&\n data.type.startsWith(BRIDGE_MSG_PREFIX)\n );\n}\n\n/**\n * Validates the structure of a _bridge:init payload.\n *\n * Performs runtime validation to ensure the payload contains all required fields\n * with correct types. This provides early error detection if the parent frame\n * sends malformed initialization data.\n *\n * @param payload - The payload to validate\n * @returns true if payload is valid\n * @throws {Error} if validation fails with a descriptive error message\n *\n * @example\n * ```ts\n * try {\n * validateInitPayload(msg.payload);\n * // proceed with initialization\n * } catch (err) {\n * console.error('Invalid init payload:', err.message);\n * }\n * ```\n */\nexport function validateInitPayload(payload: unknown): payload is BridgeInitMessage['payload'] {\n if (!payload || typeof payload !== 'object') {\n throw new Error('[SDK] _bridge:init payload must be an object');\n }\n\n const p = payload as Record<string, unknown>;\n\n // Required: side\n if (typeof p.side !== 'string' || !['host', 'player'].includes(p.side)) {\n throw new Error(`[SDK] _bridge:init payload.side must be \"host\" or \"player\", got: ${p.side}`);\n }\n\n // Required: roomCode\n if (typeof p.roomCode !== 'string' || p.roomCode.length === 0) {\n throw new Error('[SDK] _bridge:init payload.roomCode must be a non-empty string');\n }\n\n // Required: players (array)\n if (!Array.isArray(p.players)) {\n throw new Error('[SDK] _bridge:init payload.players must be an array');\n }\n\n // Optional but validated if present: myIndex (controller-side only)\n if (p.myIndex !== undefined && typeof p.myIndex !== 'number') {\n throw new Error('[SDK] _bridge:init payload.myIndex must be a number if provided');\n }\n\n return true;\n}\n"],"names":[],"mappings":"AAeO,MAAM,iBAAA,GAAoB;AAG1B,MAAM,gBAAA,GAAmB;AA+EzB,SAAS,gBAAgB,IAAA,EAAsC;AACpE,EAAA,OACE,IAAA,KAAS,IAAA,IACT,OAAO,IAAA,KAAS,YAChB,MAAA,IAAU,IAAA,IACV,OAAO,IAAA,CAAK,IAAA,KAAS,QAAA,IACrB,IAAA,CAAK,IAAA,CAAK,WAAW,iBAAiB,CAAA;AAE1C;AAuBO,SAAS,oBAAoB,OAAA,EAA2D;AAC7F,EAAA,IAAI,CAAC,OAAA,IAAW,OAAO,OAAA,KAAY,QAAA,EAAU;AAC3C,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAAA,EAChE;AAEA,EAAA,MAAM,CAAA,GAAI,OAAA;AAGV,EAAA,IAAI,OAAO,CAAA,CAAE,IAAA,KAAS,QAAA,IAAY,CAAC,CAAC,MAAA,EAAQ,QAAQ,CAAA,CAAE,QAAA,CAAS,CAAA,CAAE,IAAI,CAAA,EAAG;AACtE,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iEAAA,EAAoE,CAAA,CAAE,IAAI,CAAA,CAAE,CAAA;AAAA,EAC9F;AAGA,EAAA,IAAI,OAAO,CAAA,CAAE,QAAA,KAAa,YAAY,CAAA,CAAE,QAAA,CAAS,WAAW,CAAA,EAAG;AAC7D,IAAA,MAAM,IAAI,MAAM,gEAAgE,CAAA;AAAA,EAClF;AAGA,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,OAAO,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAI,MAAM,qDAAqD,CAAA;AAAA,EACvE;AAGA,EAAA,IAAI,EAAE,OAAA,KAAY,MAAA,IAAa,OAAO,CAAA,CAAE,YAAY,QAAA,EAAU;AAC5D,IAAA,MAAM,IAAI,MAAM,iEAAiE,CAAA;AAAA,EACnF;AAEA,EAAA,OAAO,IAAA;AACT;;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const LifecycleEvent = {
|
|
2
|
+
ALL_READY: "$all-ready",
|
|
3
|
+
CONTROLLER_JOIN: "$controller-join",
|
|
4
|
+
CONTROLLER_LEAVE: "$controller-leave",
|
|
5
|
+
CONTROLLER_DISCONNECT: "$controller-disconnect",
|
|
6
|
+
CONTROLLER_RECONNECT: "$controller-reconnect",
|
|
7
|
+
CHARACTER_UPDATED: "$character-updated",
|
|
8
|
+
ERROR: "$error",
|
|
9
|
+
GAME_OVER: "$game-over",
|
|
10
|
+
CONNECTION_CHANGE: "$connection-change"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { LifecycleEvent };
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["/**\n * @smoregg/sdk - World-Class Party Game SDK\n *\n * Type definitions for building multiplayer party games with type-safe events,\n * factory pattern initialization, and comprehensive error handling.\n *\n * @packageDocumentation\n */\n\n// =============================================================================\n// CORE PRIMITIVES\n// =============================================================================\n\n/**\n * Unique identifier for a player (0-indexed integer).\n * Used consistently across all SDK methods.\n */\nexport type PlayerIndex = number;\n\n/**\n * Room code for joining a game session (e.g., \"ABCD\").\n */\nexport type RoomCode = string;\n\n// =============================================================================\n// EVENT SYSTEM - TYPE-SAFE EVENTS\n// =============================================================================\n\n/**\n * Base event map type. Extend this to define your game's events.\n *\n * **Important:** Event data values must be plain objects, not primitives.\n * Primitive values (string, number, boolean) will be automatically wrapped\n * as `{ data: <value> }` by the relay server, which breaks type safety.\n *\n * **Payload Size Limit:** Maximum payload size per event is 64KB. This limit\n * is enforced by the server's genericRelay handler. Payloads exceeding this\n * limit will be silently dropped by the server without error notification.\n *\n * **Reserved Fields:** The field names `playerIndex` and `targetPlayerIndex` are reserved\n * by the SDK for internal routing. Using these as custom data field names will cause\n * a compile-time error. The SDK automatically extracts `playerIndex` to identify the sender\n * and uses `targetPlayerIndex` for targeted message delivery.\n *\n * **Type Safety Note:** Without providing an explicit generic type parameter,\n * `createScreen()` and `createController()` default to the empty `EventMap`,\n * which means `send()`, `broadcast()`, and `on()` accept any string as an event\n * name and `unknown` as data -- effectively losing compile-time type checking.\n * Always define and pass your game's event map for full type safety.\n *\n * @example Defining events for type safety\n * ```ts\n * interface MyGameEvents {\n * // Screen receives from Controller\n * 'tap': { x: number; y: number };\n * 'answer': { choice: number };\n *\n * // Controller receives from Screen\n * 'phase-update': { phase: 'lobby' | 'playing' | 'results' };\n * 'your-turn': { timeLimit: number };\n * }\n *\n * // With explicit generic -- full type safety\n * const screen = createScreen<MyGameEvents>({ debug: true });\n * screen.on('tap', (playerIndex, data) => { ... });\n * await screen.ready;\n *\n * // Without generic -- no type safety (not recommended)\n * const screen = createScreen();\n * ```\n *\n * @example Event naming conventions\n * ```ts\n * // Define your game's event map for type safety:\n * type MyEvents = {\n * 'player-move': { x: number; y: number };\n * 'game-action': { action: string; value: number };\n * };\n * // Event names: use kebab-case, no colons (:), no 'smore:' prefix\n * ```\n */\nexport interface EventMap {\n [key: string]: Record<string, unknown> & { playerIndex?: never; targetPlayerIndex?: never };\n}\n\n/**\n * Extract event names from an event map.\n */\nexport type EventNames<TEvents extends EventMap> = keyof TEvents & string;\n\n/**\n * Extract event data type for a specific event.\n */\nexport type EventData<\n TEvents extends EventMap,\n TEvent extends EventNames<TEvents>,\n> = TEvents[TEvent];\n\n// =============================================================================\n// CONTROLLER INFO - PLAYER INFORMATION\n// =============================================================================\n\n/**\n * Character appearance data for player avatars.\n *\n * This type matches the server's CharacterDTO structure to ensure\n * type consistency across the platform.\n *\n * @property id - Unique character identifier\n * @property seed - Random seed for generating the character\n * @property style - Character style preset identifier\n * @property options - Additional character customization options\n */\nexport interface CharacterAppearance {\n /** Unique character identifier */\n id: string;\n /** Random seed for generating the character */\n seed: string;\n /** Character style preset identifier */\n style: string;\n /** Additional character customization options */\n options: Record<string, unknown>;\n}\n\n/**\n * Information about a connected controller (player).\n * Used by Screen to identify and manage players.\n */\nexport interface ControllerInfo {\n /** Player's unique index (0, 1, 2, ...) */\n readonly playerIndex: PlayerIndex;\n /**\n * Player's chosen display name.\n *\n * Maps to `PlayerDTO.name` on the server side. The SDK exposes it as\n * \"nickname\" for semantic clarity in game code.\n * The mapping (`name` -> `nickname`) is handled automatically during\n * player data deserialization in the transport layer.\n *\n * @see PlayerDTO.name (server) -- same value, different field name\n */\n readonly nickname: string;\n /** Whether the player is currently connected */\n readonly connected: boolean;\n /**\n * Optional character appearance data.\n *\n * Note: The server sends this as \"character\" in PlayerDTO.\n * The SDK maps it to \"appearance\" for semantic clarity.\n * The server may send `null` when no character is set.\n */\n readonly appearance?: CharacterAppearance | null;\n}\n\n// =============================================================================\n// TRANSPORT\n// =============================================================================\n\n/**\n * Transport interface for custom communication implementations.\n * Implement this to use a custom transport instead of the default PostMessageTransport.\n */\nexport interface Transport {\n emit(event: string, ...args: unknown[]): void;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler?: (...args: unknown[]) => void): void;\n destroy(): void;\n}\n\n// =============================================================================\n// ERROR HANDLING\n// =============================================================================\n\n/**\n * Error codes for SDK errors.\n */\nexport type SmoreErrorCode =\n | 'TIMEOUT' // Connection or operation timeout\n | 'NOT_READY' // Operation called before ready\n | 'DESTROYED' // Operation called after destroy\n | 'INVALID_EVENT' // Invalid event name\n | 'INVALID_PLAYER' // Invalid player index\n | 'CONNECTION_LOST' // Lost connection to server\n | 'INIT_FAILED' // Failed to initialize\n | 'RATE_LIMITED' // Server rate-limited an event\n | 'PAYLOAD_TOO_LARGE' // Payload exceeds 64KB limit\n | 'UNKNOWN'; // Unknown error\n\n/**\n * Structured error type for SDK errors.\n */\nexport interface SmoreError {\n /** Error code for programmatic handling */\n code: SmoreErrorCode;\n /** Human-readable error message */\n message: string;\n /** Original error if available */\n cause?: Error;\n /** Additional context */\n details?: Record<string, unknown>;\n}\n\n// =============================================================================\n// GAME RESULTS\n// =============================================================================\n\n/**\n * Game results structure for gameOver().\n * Flexible to accommodate different game types.\n */\nexport interface GameResults {\n /** Player scores indexed by player index */\n scores?: Record<PlayerIndex, number>;\n /** Winner's player index (or -1 for no winner/tie) */\n winner?: PlayerIndex;\n /** Ranked player indices from first to last */\n rankings?: PlayerIndex[];\n /** Additional custom game-specific data */\n custom?: Record<string, unknown>;\n}\n\n// =============================================================================\n// LIFECYCLE EVENTS — UNIFIED EVENT SYSTEM\n// =============================================================================\n\n/**\n * Lifecycle event names for subscribing via `on()`.\n *\n * These `$`-prefixed event names allow lifecycle events to be registered\n * through the same `on()` method used for game events. The `$` prefix is\n * reserved by the SDK and cannot be used for user-defined events.\n *\n * @example\n * ```ts\n * // These are equivalent:\n * screen.onControllerJoin((pi, info) => { ... });\n * screen.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });\n * ```\n */\nexport const LifecycleEvent = {\n ALL_READY: '$all-ready',\n CONTROLLER_JOIN: '$controller-join',\n CONTROLLER_LEAVE: '$controller-leave',\n CONTROLLER_DISCONNECT: '$controller-disconnect',\n CONTROLLER_RECONNECT: '$controller-reconnect',\n CHARACTER_UPDATED: '$character-updated',\n ERROR: '$error',\n GAME_OVER: '$game-over',\n CONNECTION_CHANGE: '$connection-change',\n} as const;\n\n/** Union of all lifecycle event name strings. */\nexport type LifecycleEventName = typeof LifecycleEvent[keyof typeof LifecycleEvent];\n\n/**\n * Handler signatures for Screen lifecycle events.\n * Used by `on()` overloads to provide type-safe lifecycle event subscription.\n */\nexport interface ScreenLifecycleHandlers {\n '$all-ready': () => void;\n '$controller-join': (playerIndex: PlayerIndex, info: ControllerInfo) => void;\n '$controller-leave': (playerIndex: PlayerIndex) => void;\n '$controller-disconnect': (playerIndex: PlayerIndex) => void;\n '$controller-reconnect': (playerIndex: PlayerIndex, info: ControllerInfo) => void;\n '$character-updated': (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void;\n '$error': (error: SmoreError) => void;\n '$connection-change': (connected: boolean) => void;\n}\n\n/** Screen lifecycle event name union. */\nexport type ScreenLifecycleEvent = keyof ScreenLifecycleHandlers;\n\n/**\n * Handler signatures for Controller lifecycle events.\n * Extends Screen lifecycle events with Controller-specific events.\n */\nexport interface ControllerLifecycleHandlers extends ScreenLifecycleHandlers {\n '$game-over': (results?: GameResults) => void;\n '$state-recovery': (states: Record<number, Record<string, any>>) => void;\n}\n\n/** Controller lifecycle event name union. */\nexport type ControllerLifecycleEvent = keyof ControllerLifecycleHandlers;\n\n// =============================================================================\n// SCREEN TYPES - HOST/TV SIDE\n// =============================================================================\n\n/**\n * Handler for events received from controllers.\n * Receives the player index and event data.\n */\nexport type ScreenEventHandler<TData = unknown> = (\n playerIndex: PlayerIndex,\n data: TData,\n) => void;\n\n/**\n * Configuration options for creating a Screen instance.\n *\n * In the event emitter pattern, only static options are passed via config.\n * All lifecycle callbacks and event listeners are registered via methods\n * on the returned Screen instance.\n *\n * @template TEvents - Event map type for type-safe events\n *\n * @example\n * ```ts\n * const screen = createScreen<MyEvents>({ debug: true });\n *\n * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));\n * screen.onAllReady(() => startCountdown());\n * screen.onControllerJoin((playerIndex, info) => console.log('Joined:', playerIndex));\n *\n * await screen.ready;\n * ```\n */\nexport interface ScreenConfig {\n // === Debug Options ===\n\n /**\n * Enable debug mode for verbose logging.\n * @default false\n */\n debug?: boolean | DebugOptions;\n\n // === Advanced Options ===\n\n /**\n * Parent window origin for postMessage validation (iframe games).\n * Use '*' to accept messages from any origin (not recommended for production).\n * @default '*'\n */\n parentOrigin?: string;\n\n /**\n * Connection timeout in milliseconds.\n * @default 10000\n */\n timeout?: number;\n\n /**\n * Automatically signal ready after initialization.\n * When true (default), the SDK calls signalReady() automatically after init completes.\n * Set to false if your game needs to load resources before signaling ready.\n * @default true\n */\n autoReady?: boolean;\n\n /**\n * Custom transport implementation.\n * If provided, uses this instead of the default PostMessageTransport.\n * The bridge handshake (_bridge:init) still occurs via postMessage.\n */\n transport?: Transport;\n\n}\n\n/**\n * Screen instance - the main interface for the host/TV side of your game.\n *\n * Uses an event emitter pattern: lifecycle callbacks and event listeners\n * are registered via methods on the instance, not via config.\n *\n * @template TEvents - Event map type for type-safe events\n *\n * @example\n * ```ts\n * const screen = createScreen<MyEvents>({ debug: true });\n *\n * screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));\n * screen.onAllReady(() => startGame());\n * screen.onControllerJoin((playerIndex, info) => addPlayer(playerIndex, info));\n *\n * await screen.ready;\n * screen.broadcast('phase-update', { phase: 'playing' });\n * screen.gameOver({ scores: { 0: 100, 1: 75 } });\n * ```\n */\nexport interface Screen<TEvents extends EventMap = EventMap> {\n // === Properties (readonly) ===\n\n /**\n * All connected controllers. Returns a new shallow copy on every access.\n *\n * **Performance note:** Each access creates a new array via spread.\n * Avoid calling this in tight loops; cache the result in a local variable\n * if you need to access it repeatedly within the same frame/tick.\n */\n readonly controllers: readonly ControllerInfo[];\n\n /** The room code for this game session. */\n readonly roomCode: RoomCode;\n\n /** Whether the screen is initialized and ready. */\n readonly isReady: boolean;\n\n /** Whether the screen has been destroyed. */\n readonly isDestroyed: boolean;\n\n /** Whether the connection to the server is active. */\n readonly isConnected: boolean;\n\n /** Protocol version negotiated with the parent frame. */\n readonly protocolVersion: number;\n\n /**\n * A Promise that resolves when the screen is initialized and ready.\n * Use this to await readiness after creating a screen synchronously.\n *\n * @example\n * ```ts\n * const screen = createScreen<MyEvents>();\n * screen.on('tap', handler);\n * await screen.ready;\n * screen.broadcast('start', {});\n * ```\n */\n readonly ready: Promise<void>;\n\n // === Lifecycle Methods ===\n\n /**\n * Register a callback for when all participants are ready (all-ready event).\n * If the all-ready event has already fired when called, the callback fires immediately.\n *\n * @param callback - Called when all participants signal ready\n * @returns Unsubscribe function to remove the callback\n */\n onAllReady(callback: () => void): () => void;\n\n /**\n * Register a callback for when a controller (player) joins the room.\n *\n * @param callback - Called with player index and controller info\n * @returns Unsubscribe function to remove the callback\n */\n onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;\n\n /**\n * Register a callback for when a controller (player) leaves the room.\n *\n * @param callback - Called with the leaving player's index\n * @returns Unsubscribe function to remove the callback\n */\n onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void;\n\n /**\n * Register a callback for when a controller temporarily disconnects.\n *\n * @param callback - Called with the disconnected player's index\n * @returns Unsubscribe function to remove the callback\n */\n onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void;\n\n /**\n * Register a callback for when a controller reconnects after a disconnect.\n *\n * @param callback - Called with player index and updated controller info\n * @returns Unsubscribe function to remove the callback\n */\n onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;\n\n /**\n * Register a callback for when a player's character appearance is updated.\n *\n * @param callback - Called with player index and new appearance (or null if reset)\n * @returns Unsubscribe function to remove the callback\n */\n onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void;\n\n /**\n * Register a callback for when an error occurs.\n * If no error callback is registered, errors are logged to console in debug mode.\n *\n * @param callback - Called with the error object\n * @returns Unsubscribe function to remove the callback\n */\n onError(callback: (error: SmoreError) => void): () => void;\n\n /**\n * Register a callback for when the connection status changes.\n * Called when the connection to the server is lost or restored.\n *\n * @param callback - Called with true (connected) or false (disconnected)\n * @returns Unsubscribe function to remove the callback\n */\n onConnectionChange(callback: (connected: boolean) => void): () => void;\n\n // === Communication Methods ===\n\n /**\n * Broadcast an event to all connected controllers.\n *\n * @param event - Event name (must match TEvents keys)\n * @param data - Event data (type-safe based on event name)\n * @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.\n * @note Maximum payload size is 64KB. Payloads exceeding this limit will be silently dropped by the server.\n *\n * @example\n * ```ts\n * screen.broadcast('phase-update', { phase: 'playing' });\n * ```\n */\n broadcast<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void;\n\n /**\n * Send an event to a specific controller.\n *\n * Screen -> Controller direction only. For Controller -> Screen, see `send()`.\n *\n * @param playerIndex - Target controller's player index\n * @param event - Event name (must match TEvents keys)\n * @param data - Event data (type-safe based on event name)\n * @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.\n * @note Maximum payload size is 64KB. Payloads exceeding this limit will be silently dropped by the server.\n *\n * @example\n * ```ts\n * screen.sendToController(0, 'your-turn', { timeLimit: 30 });\n * ```\n */\n sendToController<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void;\n\n // === Custom State Methods ===\n\n /**\n * Get a specific controller's cached custom state.\n * Returns undefined if no state has been set for this controller.\n *\n * @param playerIndex - The controller's player index\n */\n getControllerState(playerIndex: number): Record<string, any> | undefined;\n\n /**\n * Get all controllers' cached custom states.\n * Returns a record mapping player index to state.\n */\n getAllControllerStates(): Record<number, Record<string, any>>;\n\n /**\n * Register a listener for custom state changes from any controller.\n *\n * @param listener - Called with the player index and the new state\n * @returns Unsubscribe function\n */\n onCustomStateChange(listener: (playerIndex: number, state: Record<string, any>) => void): () => void;\n\n // === Game Lifecycle ===\n\n /**\n * Signal that the game is over and send results.\n * This will broadcast a game-over event to all controllers.\n *\n * @param results - Game results (scores, rankings, etc.)\n *\n * @example\n * ```ts\n * screen.gameOver({\n * scores: { 0: 100, 1: 75, 2: 50 },\n * winner: 0,\n * rankings: [0, 1, 2],\n * });\n * ```\n */\n gameOver(results?: GameResults): void;\n\n /**\n * Signal that this screen has finished loading resources and is ready to start.\n *\n * Call this after all game resources (Phaser assets, images, sounds, etc.) are loaded.\n * The server will wait until all participants (screen + all connected controllers)\n * have signaled ready, then broadcast an all-ready event to everyone.\n *\n * @example\n * ```ts\n * // After Phaser scene loads all assets:\n * screen.signalReady();\n * ```\n */\n signalReady(): void;\n\n // === Event Subscription ===\n\n /**\n * Subscribe to a lifecycle event or a user-defined game event.\n *\n * Lifecycle events use `$`-prefixed names. Use `LifecycleEvent` constants\n * for type-safe subscription:\n * ```ts\n * screen.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });\n * screen.on('tap', (pi, data) => { ... }); // user event\n * ```\n */\n on<K extends ScreenLifecycleEvent>(event: K, handler: ScreenLifecycleHandlers[K]): () => void;\n /**\n * Add a listener for a specific event after construction.\n * Can be called before the screen is ready -- handlers are queued\n * and activated when the transport becomes available.\n *\n * @param event - Event name\n * @param handler - Handler function (playerIndex, data) => void\n * @returns Cleanup function to remove the listener\n *\n * @example\n * ```ts\n * const unsubscribe = screen.on('tap', (playerIndex, data) => {\n * console.log(`Player ${playerIndex} tapped at`, data.x, data.y);\n * });\n *\n * // Later: remove listener\n * unsubscribe();\n * ```\n */\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void;\n\n once<K extends ScreenLifecycleEvent>(event: K, handler: ScreenLifecycleHandlers[K]): () => void;\n /**\n * Add a one-time listener that auto-removes after first call.\n *\n * @note The handler is internally wrapped, so it cannot be removed via\n * `off(event, originalHandler)`. Use the returned unsubscribe function instead.\n */\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void;\n\n off<K extends ScreenLifecycleEvent>(event: K, handler?: ScreenLifecycleHandlers[K]): void;\n /**\n * Remove a specific listener or all listeners for an event.\n */\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ScreenEventHandler<EventData<TEvents, K>>,\n ): void;\n\n /**\n * Remove all event listeners, or all listeners for a specific event.\n * Only removes user event listeners registered via on()/once().\n * Lifecycle callbacks (onControllerJoin, onAllReady, etc.) are not affected.\n *\n * @param event - Optional event name. If omitted, removes ALL user event listeners.\n */\n removeAllListeners(event?: string): void;\n\n // === Utilities ===\n\n /**\n * Get a specific controller by player index.\n * Returns undefined if not found.\n */\n getController(playerIndex: PlayerIndex): ControllerInfo | undefined;\n\n /**\n * Get the number of connected controllers.\n */\n getControllerCount(): number;\n\n // === Cleanup ===\n\n /**\n * Clean up all resources and disconnect.\n * Call this when unmounting/destroying your game.\n */\n destroy(): void;\n}\n\n// =============================================================================\n// CONTROLLER TYPES - PLAYER/PHONE SIDE\n// =============================================================================\n\n/**\n * Handler for events received from the screen.\n * Receives only the event data (no player index needed).\n */\nexport type ControllerEventHandler<TData = unknown> = (data: TData) => void;\n\n/**\n * Configuration options for creating a Controller instance.\n *\n * In the event emitter pattern, only static options are passed via config.\n * All lifecycle callbacks and event listeners are registered via methods\n * on the returned Controller instance.\n *\n * @template TEvents - Event map type for type-safe events\n *\n * @example\n * ```ts\n * const controller = createController<MyEvents>({ debug: true });\n *\n * controller.on('phase-update', (data) => handlePhase(data.phase));\n * controller.onAllReady(() => console.log('Ready!'));\n *\n * await controller.ready;\n * controller.send('tap', { x: 100, y: 200 });\n * ```\n */\nexport interface ControllerConfig {\n // === Debug Options ===\n\n /**\n * Enable debug mode for verbose logging.\n * @default false\n */\n debug?: boolean | DebugOptions;\n\n // === Advanced Options ===\n\n /**\n * Parent window origin for postMessage validation (iframe games).\n * @default '*'\n */\n parentOrigin?: string;\n\n /**\n * Connection timeout in milliseconds.\n * @default 10000\n */\n timeout?: number;\n\n /**\n * Automatically signal ready after initialization.\n * When true (default), the SDK calls signalReady() automatically after init completes.\n * Set to false if your game needs to load resources before signaling ready.\n * @default true\n */\n autoReady?: boolean;\n\n /**\n * Custom transport implementation.\n * If provided, uses this instead of the default PostMessageTransport.\n * The bridge handshake (_bridge:init) still occurs via postMessage.\n */\n transport?: Transport;\n\n}\n\n/**\n * Controller instance - the main interface for the player/phone side.\n *\n * Uses an event emitter pattern: lifecycle callbacks and event listeners\n * are registered via methods on the instance, not via config.\n *\n * @template TEvents - Event map type for type-safe events\n *\n * @example\n * ```ts\n * const controller = createController<MyEvents>({ debug: true });\n *\n * controller.on('phase-update', (data) => setPhase(data.phase));\n * controller.onAllReady(() => console.log('Game starting!'));\n *\n * await controller.ready;\n * controller.send('tap', { x: 100, y: 200 });\n * ```\n *\n * ## Controller API Design Note:\n *\n * Controller only has `send()` (no `broadcast()` or `gameOver()`).\n * This is intentional: Controller-to-Controller (C2C) communication is not supported.\n * All coordination between controllers must go through the Screen.\n *\n * Flow: Controller.send() -> Screen receives -> Screen.broadcast() or Screen.sendToController()\n */\nexport interface Controller<TEvents extends EventMap = EventMap> {\n // === Properties (readonly) ===\n\n /** My player index (0, 1, 2, ...). */\n readonly myPlayerIndex: PlayerIndex;\n\n /** My own controller info. undefined before initialization. */\n readonly me: ControllerInfo | undefined;\n\n /** The room code for this game session. */\n readonly roomCode: RoomCode;\n\n /** Whether the controller is initialized and ready. */\n readonly isReady: boolean;\n\n /** Whether the controller has been destroyed. */\n readonly isDestroyed: boolean;\n\n /** Whether the connection to the server is active. */\n readonly isConnected: boolean;\n\n /** Protocol version negotiated with the parent frame. */\n readonly protocolVersion: number;\n\n /**\n * Read-only list of all known controllers (players) in the room.\n * Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.\n * Consistent with Screen's `controllers` property.\n *\n * **Performance note:** Each access creates a new shallow copy array.\n * Cache the result in a local variable if you need to access it repeatedly.\n */\n readonly controllers: readonly ControllerInfo[];\n\n /**\n * A Promise that resolves when the controller is initialized and ready.\n * Use this to await readiness after creating a controller synchronously.\n *\n * @example\n * ```ts\n * const controller = createController<MyEvents>();\n * controller.on('phase-update', handler);\n * await controller.ready;\n * controller.send('tap', { x: 0, y: 0 });\n * ```\n */\n readonly ready: Promise<void>;\n\n // === Lifecycle Methods ===\n\n /**\n * Register a callback for when all participants are ready (all-ready event).\n * If the all-ready event has already fired when called, the callback fires immediately.\n *\n * @param callback - Called when all participants signal ready\n * @returns Unsubscribe function to remove the callback\n */\n onAllReady(callback: () => void): () => void;\n\n /**\n * Register a callback for when another player joins the room.\n *\n * @param callback - Called with player index and controller info\n * @returns Unsubscribe function to remove the callback\n */\n onControllerJoin(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;\n\n /**\n * Register a callback for when another player leaves the room.\n *\n * @param callback - Called with the leaving player's index\n * @returns Unsubscribe function to remove the callback\n */\n onControllerLeave(callback: (playerIndex: PlayerIndex) => void): () => void;\n\n /**\n * Register a callback for when another player temporarily disconnects.\n *\n * @param callback - Called with the disconnected player's index\n * @returns Unsubscribe function to remove the callback\n */\n onControllerDisconnect(callback: (playerIndex: PlayerIndex) => void): () => void;\n\n /**\n * Register a callback for when another player reconnects after a disconnect.\n *\n * @param callback - Called with player index and updated controller info\n * @returns Unsubscribe function to remove the callback\n */\n onControllerReconnect(callback: (playerIndex: PlayerIndex, info: ControllerInfo) => void): () => void;\n\n /**\n * Register a callback for when a player's character appearance is updated.\n *\n * @param callback - Called with player index and new appearance (or null if reset)\n * @returns Unsubscribe function to remove the callback\n */\n onCharacterUpdated(callback: (playerIndex: PlayerIndex, appearance: CharacterAppearance | null) => void): () => void;\n\n /**\n * Register a callback for when an error occurs.\n * If no error callback is registered, errors are logged to console in debug mode.\n *\n * @param callback - Called with the error object\n * @returns Unsubscribe function to remove the callback\n */\n onError(callback: (error: SmoreError) => void): () => void;\n\n /**\n * Register a callback for when the game ends.\n * Called when the Screen calls gameOver().\n *\n * @param callback - Called with optional game results\n * @returns Unsubscribe function to remove the callback\n */\n onGameOver(callback: (results?: GameResults) => void): () => void;\n\n /**\n * Register a callback for when the connection status changes.\n * Called when the connection to the server is lost or restored.\n *\n * @param callback - Called with true (connected) or false (disconnected)\n * @returns Unsubscribe function to remove the callback\n */\n onConnectionChange(callback: (connected: boolean) => void): () => void;\n\n /**\n * Returns the number of currently connected players.\n */\n getControllerCount(): number;\n\n /**\n * Get a specific controller by player index.\n * Returns undefined if not found.\n */\n getController(playerIndex: PlayerIndex): ControllerInfo | undefined;\n\n // === Custom State Methods ===\n\n /**\n * Set custom state for this controller. State is merged with existing state on the server.\n * State persists only for the duration of the game session.\n *\n * @param state - Key-value state to merge\n */\n setState(state: Record<string, any>): void;\n\n /**\n * Get this controller's cached custom state.\n * Returns undefined if no state has been set.\n */\n getMyState(): Record<string, any> | undefined;\n\n /**\n * Register a listener for custom state changes from any controller.\n *\n * @param listener - Called with the player index and the new state\n * @returns Unsubscribe function\n */\n onCustomStateChange(listener: (playerIndex: number, state: Record<string, any>) => void): () => void;\n\n // === Communication Methods ===\n\n /**\n * Send an event to the screen.\n *\n * Controller -> Screen direction only. For Screen -> Controller, see `sendToController()`.\n *\n * @param event - Event name (must match TEvents keys)\n * @param data - Event data (type-safe based on event name)\n * @note Maximum payload size is 64KB. Payloads exceeding this limit will be silently dropped by the server.\n *\n * @example\n * ```ts\n * controller.send('tap', { x: 100, y: 200 });\n * controller.send('answer', { choice: 2 });\n * ```\n */\n send<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void;\n\n /**\n * Signal that this controller has finished loading resources and is ready to start.\n *\n * Call this after all game resources are loaded.\n * The server will wait until all participants have signaled ready,\n * then broadcast an all-ready event to everyone.\n *\n * @example\n * ```ts\n * // After loading completes:\n * controller.signalReady();\n * ```\n */\n signalReady(): void;\n\n // === Event Subscription ===\n\n /**\n * Subscribe to a lifecycle event or a user-defined game event.\n *\n * Lifecycle events use `$`-prefixed names. Use `LifecycleEvent` constants\n * for type-safe subscription:\n * ```ts\n * controller.on(LifecycleEvent.CONTROLLER_JOIN, (pi, info) => { ... });\n * controller.on('phase-update', (data) => { ... }); // user event\n * ```\n */\n on<K extends ControllerLifecycleEvent>(event: K, handler: ControllerLifecycleHandlers[K]): () => void;\n /**\n * Add a listener for a specific event after construction.\n * Can be called before the controller is ready -- handlers are queued\n * and activated when the transport becomes available.\n *\n * @param event - Event name\n * @param handler - Handler function (data) => void\n * @returns Cleanup function to remove the listener\n *\n * @note Events from screen.broadcast() do not include playerIndex.\n * The handler signature is `(data: D)` without playerIndex context.\n * If you need to know which player triggered the broadcast, include\n * that information in the data object from the Screen side.\n *\n * @example\n * ```ts\n * const unsubscribe = controller.on('phase-update', (data) => {\n * console.log('New phase:', data.phase);\n * });\n *\n * // Later: remove listener\n * unsubscribe();\n * ```\n */\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void;\n\n once<K extends ControllerLifecycleEvent>(event: K, handler: ControllerLifecycleHandlers[K]): () => void;\n /**\n * Add a one-time listener that auto-removes after first call.\n *\n * @note The handler is internally wrapped, so it cannot be removed via\n * `off(event, originalHandler)`. Use the returned unsubscribe function instead.\n */\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void;\n\n off<K extends ControllerLifecycleEvent>(event: K, handler?: ControllerLifecycleHandlers[K]): void;\n /**\n * Remove a specific listener or all listeners for an event.\n */\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ControllerEventHandler<EventData<TEvents, K>>,\n ): void;\n\n /**\n * Remove all event listeners, or all listeners for a specific event.\n * Only removes user event listeners registered via on()/once().\n * Lifecycle callbacks (onControllerJoin, onAllReady, etc.) are not affected.\n *\n * @param event - Optional event name. If omitted, removes ALL user event listeners.\n */\n removeAllListeners(event?: string): void;\n\n // === Cleanup ===\n\n /**\n * Clean up all resources and disconnect.\n * Call this when unmounting/destroying your game.\n */\n destroy(): void;\n}\n\n// =============================================================================\n// DEBUG OPTIONS\n// =============================================================================\n\n/**\n * Log levels for debug output.\n */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Debug configuration options.\n */\nexport interface DebugOptions {\n /** Enable debug logging */\n enabled?: boolean;\n\n /** Minimum log level to output */\n level?: LogLevel;\n\n /** Prefix for log messages */\n prefix?: string;\n\n /** Log events being sent */\n logSend?: boolean;\n\n /** Log events being received */\n logReceive?: boolean;\n\n /** Log lifecycle events (ready, destroy, etc.) */\n logLifecycle?: boolean;\n\n /** Custom logger function */\n logger?: (level: LogLevel, message: string, data?: unknown) => void;\n}\n"],"names":[],"mappings":"AA+OO,MAAM,cAAA,GAAiB;AAAA,EAC5B,SAAA,EAAW,YAAA;AAAA,EACX,eAAA,EAAiB,kBAAA;AAAA,EACjB,gBAAA,EAAkB,mBAAA;AAAA,EAClB,qBAAA,EAAuB,wBAAA;AAAA,EACvB,oBAAA,EAAsB,uBAAA;AAAA,EACtB,iBAAA,EAAmB,oBAAA;AAAA,EACnB,KAAA,EAAO,QAAA;AAAA,EACP,SAAA,EAAW,YAAA;AAAA,EACX,iBAAA,EAAmB;AACrB;;;;"}
|
|
@@ -40,5 +40,5 @@ import type { Controller, ControllerConfig, EventMap } from './types';
|
|
|
40
40
|
* controller.send('tap', { x: 100, y: 200 });
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
|
-
export declare function createController<TEvents extends EventMap = EventMap>(config?: ControllerConfig
|
|
43
|
+
export declare function createController<TEvents extends EventMap = EventMap>(config?: ControllerConfig): Controller<TEvents>;
|
|
44
44
|
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAIhB,QAAQ,
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAIhB,QAAQ,EAOT,MAAM,SAAS,CAAC;AAw9BjB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,QAAQ,GAAG,QAAQ,EAClE,MAAM,CAAC,EAAE,gBAAgB,GACxB,UAAU,CAAC,OAAO,CAAC,CAErB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAGzC,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAGzC,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE;IAgBhE,YAAY,IAAI,UAAU;CAQ3B"}
|
package/dist/types/events.d.ts
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export declare const SMORE_EVENTS: {
|
|
20
20
|
readonly GAME_OVER: "smore:game-over";
|
|
21
|
-
readonly RETURN_TO_LOBBY: "smore:return-to-lobby";
|
|
22
21
|
readonly PLAYER_JOINED: "smore:player-joined";
|
|
23
22
|
readonly PLAYER_LEFT: "smore:player-left";
|
|
24
23
|
readonly PLAYER_DISCONNECTED: "smore:player-disconnected";
|
|
@@ -27,7 +26,13 @@ export declare const SMORE_EVENTS: {
|
|
|
27
26
|
readonly RATE_LIMITED: "smore:rate-limited";
|
|
28
27
|
readonly GAME_READY: "smore:game-ready";
|
|
29
28
|
readonly ALL_READY: "smore:all-ready";
|
|
29
|
+
readonly SELF_DISCONNECTED: "smore:self-disconnected";
|
|
30
|
+
readonly SELF_RECONNECTED: "smore:self-reconnected";
|
|
30
31
|
readonly SEND_TO_PLAYER: "smore:send-to-player";
|
|
32
|
+
readonly STATE_SET: "smore:set-custom-state";
|
|
33
|
+
readonly STATE_CHANGED: "smore:custom-state-changed";
|
|
34
|
+
readonly STATE_GET_ALL: "smore:get-custom-states";
|
|
35
|
+
readonly STATE_ALL: "smore:custom-states";
|
|
31
36
|
};
|
|
32
37
|
export type SmoreEvent = typeof SMORE_EVENTS[keyof typeof SMORE_EVENTS];
|
|
33
38
|
export declare const SYSTEM_EVENTS: ReadonlySet<string>;
|
|
@@ -65,4 +70,12 @@ export declare function validateEventName(event: string): void;
|
|
|
65
70
|
* @returns true if the event is a system event
|
|
66
71
|
*/
|
|
67
72
|
export declare function isSystemEvent(event: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Set of valid Screen lifecycle event names (used for $-prefix routing).
|
|
75
|
+
*/
|
|
76
|
+
export declare const SCREEN_LIFECYCLE_EVENTS: ReadonlySet<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Set of valid Controller lifecycle event names (used for $-prefix routing).
|
|
79
|
+
*/
|
|
80
|
+
export declare const CONTROLLER_LIFECYCLE_EVENTS: ReadonlySet<string>;
|
|
68
81
|
//# sourceMappingURL=events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;CAgCf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,OAAO,YAAY,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAExE,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,CAE7C,CAAC;AAGF,eAAO,MAAM,gBAAgB,QAA2C,CAAC;AAEzE,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAmBrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAStD,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,2BAA2B,EAAE,WAAW,CAAC,MAAM,CAI1D,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* screen.on('tap', (playerIndex, data) => handleTap(playerIndex, data));
|
|
16
16
|
* screen.onAllReady(() => startGame());
|
|
17
|
-
* screen.onControllerJoin((
|
|
17
|
+
* screen.onControllerJoin((playerIndex, info) => console.log('Player joined:', playerIndex));
|
|
18
18
|
*
|
|
19
19
|
* await screen.ready;
|
|
20
20
|
* screen.broadcast('phase-update', { phase: 'playing' });
|
|
@@ -31,17 +31,13 @@
|
|
|
31
31
|
* controller.onAllReady(() => console.log('Ready!'));
|
|
32
32
|
*
|
|
33
33
|
* await controller.ready;
|
|
34
|
-
* console.log('My index:', controller.
|
|
34
|
+
* console.log('My index:', controller.myPlayerIndex);
|
|
35
35
|
* controller.send('tap', { timestamp: Date.now() });
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
export { createScreen } from './screen';
|
|
39
39
|
export { createController } from './controller';
|
|
40
40
|
export { SmoreSDKError } from './errors';
|
|
41
|
-
export {
|
|
42
|
-
export {
|
|
43
|
-
export type { SmoreGlobalConfig } from './config';
|
|
44
|
-
export type { PlayerIndex, RoomCode, EventMap, EventNames, EventData, Controller, ControllerConfig, ControllerEventHandler, ControllerInfo, CharacterAppearance, Screen, ScreenConfig, ScreenEventHandler, GameResults, SmoreError, SmoreErrorCode, LogLevel, DebugOptions, GameMetadata, } from './types';
|
|
45
|
-
export { createMockScreen, createMockController } from './testing';
|
|
46
|
-
export type { MockScreen, MockController, MockOptions } from './testing';
|
|
41
|
+
export { LifecycleEvent } from './types';
|
|
42
|
+
export type { PlayerIndex, RoomCode, EventMap, EventNames, EventData, LifecycleEventName, ScreenLifecycleEvent, ScreenLifecycleHandlers, ControllerLifecycleEvent, ControllerLifecycleHandlers, Controller, ControllerConfig, ControllerEventHandler, ControllerInfo, CharacterAppearance, Screen, ScreenConfig, ScreenEventHandler, Transport, GameResults, SmoreError, SmoreErrorCode, DebugOptions, LogLevel, } from './types';
|
|
47
43
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAMH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAMH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMzC,YAAY,EAEV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,UAAU,EACV,SAAS,EAET,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,2BAA2B,EAE3B,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,cAAc,EACd,mBAAmB,EAEnB,MAAM,EACN,YAAY,EACZ,kBAAkB,EAElB,SAAS,EAET,WAAW,EAEX,UAAU,EACV,cAAc,EAEd,YAAY,EACZ,QAAQ,GACT,MAAM,SAAS,CAAC"}
|
package/dist/types/screen.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* });
|
|
20
20
|
*
|
|
21
21
|
* screen.onAllReady(() => startGame());
|
|
22
|
-
* screen.onControllerJoin((
|
|
22
|
+
* screen.onControllerJoin((playerIndex, info) => addPlayer(playerIndex, info));
|
|
23
23
|
*
|
|
24
24
|
* await screen.ready;
|
|
25
25
|
* screen.broadcast('phase-update', { phase: 'playing' });
|
|
@@ -46,10 +46,10 @@ import type { EventMap, Screen, ScreenConfig } from './types';
|
|
|
46
46
|
* });
|
|
47
47
|
*
|
|
48
48
|
* screen.onAllReady(() => startGame());
|
|
49
|
-
* screen.onControllerJoin((
|
|
49
|
+
* screen.onControllerJoin((playerIndex, info) => addPlayer(playerIndex, info));
|
|
50
50
|
*
|
|
51
51
|
* await screen.ready;
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
|
-
export declare function createScreen<TEvents extends EventMap = EventMap>(config?: ScreenConfig
|
|
54
|
+
export declare function createScreen<TEvents extends EventMap = EventMap>(config?: ScreenConfig): Screen<TEvents>;
|
|
55
55
|
//# sourceMappingURL=screen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen.d.ts","sourceRoot":"","sources":["../../src/screen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EACV,QAAQ,EAGR,MAAM,EACN,YAAY,EAQb,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"screen.d.ts","sourceRoot":"","sources":["../../src/screen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EACV,QAAQ,EAGR,MAAM,EACN,YAAY,EAQb,MAAM,SAAS,CAAC;AA69BjB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,QAAQ,GAAG,QAAQ,EAC9D,MAAM,CAAC,EAAE,YAAY,GACpB,MAAM,CAAC,OAAO,CAAC,CAEjB"}
|