@smoregg/sdk 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/cjs/config.cjs.map +1 -1
  2. package/dist/cjs/controller.cjs +215 -145
  3. package/dist/cjs/controller.cjs.map +1 -1
  4. package/dist/cjs/screen.cjs +220 -178
  5. package/dist/cjs/screen.cjs.map +1 -1
  6. package/dist/cjs/testing.cjs +160 -151
  7. package/dist/cjs/testing.cjs.map +1 -1
  8. package/dist/esm/config.js.map +1 -1
  9. package/dist/esm/controller.js +216 -146
  10. package/dist/esm/controller.js.map +1 -1
  11. package/dist/esm/screen.js +221 -179
  12. package/dist/esm/screen.js.map +1 -1
  13. package/dist/esm/testing.js +160 -151
  14. package/dist/esm/testing.js.map +1 -1
  15. package/dist/types/config.d.ts +1 -2
  16. package/dist/types/config.d.ts.map +1 -1
  17. package/dist/types/controller.d.ts +22 -43
  18. package/dist/types/controller.d.ts.map +1 -1
  19. package/dist/types/index.d.ts +14 -14
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/screen.d.ts +26 -37
  22. package/dist/types/screen.d.ts.map +1 -1
  23. package/dist/types/testing.d.ts +16 -0
  24. package/dist/types/testing.d.ts.map +1 -1
  25. package/dist/types/types.d.ts +244 -338
  26. package/dist/types/types.d.ts.map +1 -1
  27. package/dist/umd/smore-sdk.umd.js +595 -474
  28. package/dist/umd/smore-sdk.umd.js.map +1 -1
  29. package/dist/umd/smore-sdk.umd.min.js +1 -1
  30. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  31. package/package.json +1 -1
  32. package/dist/cjs/SmoreHost.cjs +0 -306
  33. package/dist/cjs/SmoreHost.cjs.map +0 -1
  34. package/dist/cjs/SmorePlayer.cjs +0 -229
  35. package/dist/cjs/SmorePlayer.cjs.map +0 -1
  36. package/dist/cjs/components/DirectionPad.cjs +0 -68
  37. package/dist/cjs/components/DirectionPad.cjs.map +0 -1
  38. package/dist/cjs/components/DirectionPad.module.css.cjs +0 -12
  39. package/dist/cjs/components/DirectionPad.module.css.cjs.map +0 -1
  40. package/dist/cjs/components/HoldButton.cjs +0 -57
  41. package/dist/cjs/components/HoldButton.cjs.map +0 -1
  42. package/dist/cjs/components/HoldButton.module.css.cjs +0 -12
  43. package/dist/cjs/components/HoldButton.module.css.cjs.map +0 -1
  44. package/dist/cjs/components/IframeGameBridge.cjs +0 -115
  45. package/dist/cjs/components/IframeGameBridge.cjs.map +0 -1
  46. package/dist/cjs/components/SwipeArea.cjs +0 -58
  47. package/dist/cjs/components/SwipeArea.cjs.map +0 -1
  48. package/dist/cjs/components/SwipeArea.module.css.cjs +0 -12
  49. package/dist/cjs/components/SwipeArea.module.css.cjs.map +0 -1
  50. package/dist/cjs/components/TapButton.cjs +0 -58
  51. package/dist/cjs/components/TapButton.cjs.map +0 -1
  52. package/dist/cjs/components/TapButton.module.css.cjs +0 -12
  53. package/dist/cjs/components/TapButton.module.css.cjs.map +0 -1
  54. package/dist/cjs/context/RoomProvider.cjs +0 -118
  55. package/dist/cjs/context/RoomProvider.cjs.map +0 -1
  56. package/dist/cjs/hooks/useExternalGames.cjs +0 -49
  57. package/dist/cjs/hooks/useExternalGames.cjs.map +0 -1
  58. package/dist/cjs/hooks/useGameHost.cjs +0 -206
  59. package/dist/cjs/hooks/useGameHost.cjs.map +0 -1
  60. package/dist/cjs/hooks/useGamePlayer.cjs +0 -134
  61. package/dist/cjs/hooks/useGamePlayer.cjs.map +0 -1
  62. package/dist/cjs/iframe/index.cjs +0 -260
  63. package/dist/cjs/iframe/index.cjs.map +0 -1
  64. package/dist/cjs/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.cjs +0 -33
  65. package/dist/cjs/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.cjs.map +0 -1
  66. package/dist/cjs/server/index.cjs +0 -45
  67. package/dist/cjs/server/index.cjs.map +0 -1
  68. package/dist/cjs/transport/DirectTransport.cjs +0 -23
  69. package/dist/cjs/transport/DirectTransport.cjs.map +0 -1
  70. package/dist/cjs/utils/connectionMonitor.cjs +0 -77
  71. package/dist/cjs/utils/connectionMonitor.cjs.map +0 -1
  72. package/dist/cjs/utils/preloadAssets.cjs +0 -66
  73. package/dist/cjs/utils/preloadAssets.cjs.map +0 -1
  74. package/dist/cjs/utils/serverTime.cjs +0 -43
  75. package/dist/cjs/utils/serverTime.cjs.map +0 -1
  76. package/dist/esm/SmoreHost.js +0 -304
  77. package/dist/esm/SmoreHost.js.map +0 -1
  78. package/dist/esm/SmorePlayer.js +0 -227
  79. package/dist/esm/SmorePlayer.js.map +0 -1
  80. package/dist/esm/components/DirectionPad.js +0 -66
  81. package/dist/esm/components/DirectionPad.js.map +0 -1
  82. package/dist/esm/components/DirectionPad.module.css.js +0 -8
  83. package/dist/esm/components/DirectionPad.module.css.js.map +0 -1
  84. package/dist/esm/components/HoldButton.js +0 -55
  85. package/dist/esm/components/HoldButton.js.map +0 -1
  86. package/dist/esm/components/HoldButton.module.css.js +0 -8
  87. package/dist/esm/components/HoldButton.module.css.js.map +0 -1
  88. package/dist/esm/components/IframeGameBridge.js +0 -113
  89. package/dist/esm/components/IframeGameBridge.js.map +0 -1
  90. package/dist/esm/components/SwipeArea.js +0 -56
  91. package/dist/esm/components/SwipeArea.js.map +0 -1
  92. package/dist/esm/components/SwipeArea.module.css.js +0 -8
  93. package/dist/esm/components/SwipeArea.module.css.js.map +0 -1
  94. package/dist/esm/components/TapButton.js +0 -56
  95. package/dist/esm/components/TapButton.js.map +0 -1
  96. package/dist/esm/components/TapButton.module.css.js +0 -8
  97. package/dist/esm/components/TapButton.module.css.js.map +0 -1
  98. package/dist/esm/context/RoomProvider.js +0 -109
  99. package/dist/esm/context/RoomProvider.js.map +0 -1
  100. package/dist/esm/hooks/useExternalGames.js +0 -47
  101. package/dist/esm/hooks/useExternalGames.js.map +0 -1
  102. package/dist/esm/hooks/useGameHost.js +0 -204
  103. package/dist/esm/hooks/useGameHost.js.map +0 -1
  104. package/dist/esm/hooks/useGamePlayer.js +0 -132
  105. package/dist/esm/hooks/useGamePlayer.js.map +0 -1
  106. package/dist/esm/iframe/index.js +0 -257
  107. package/dist/esm/iframe/index.js.map +0 -1
  108. package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js +0 -29
  109. package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js.map +0 -1
  110. package/dist/esm/server/index.js +0 -43
  111. package/dist/esm/server/index.js.map +0 -1
  112. package/dist/esm/transport/DirectTransport.js +0 -21
  113. package/dist/esm/transport/DirectTransport.js.map +0 -1
  114. package/dist/esm/utils/connectionMonitor.js +0 -75
  115. package/dist/esm/utils/connectionMonitor.js.map +0 -1
  116. package/dist/esm/utils/preloadAssets.js +0 -63
  117. package/dist/esm/utils/preloadAssets.js.map +0 -1
  118. package/dist/esm/utils/serverTime.js +0 -41
  119. package/dist/esm/utils/serverTime.js.map +0 -1
  120. package/dist/types/SmoreHost.d.ts +0 -187
  121. package/dist/types/SmoreHost.d.ts.map +0 -1
  122. package/dist/types/SmorePlayer.d.ts +0 -146
  123. package/dist/types/SmorePlayer.d.ts.map +0 -1
  124. package/dist/types/components/DirectionPad.d.ts +0 -21
  125. package/dist/types/components/DirectionPad.d.ts.map +0 -1
  126. package/dist/types/components/HoldButton.d.ts +0 -22
  127. package/dist/types/components/HoldButton.d.ts.map +0 -1
  128. package/dist/types/components/IframeGameBridge.d.ts +0 -38
  129. package/dist/types/components/IframeGameBridge.d.ts.map +0 -1
  130. package/dist/types/components/SwipeArea.d.ts +0 -19
  131. package/dist/types/components/SwipeArea.d.ts.map +0 -1
  132. package/dist/types/components/TapButton.d.ts +0 -19
  133. package/dist/types/components/TapButton.d.ts.map +0 -1
  134. package/dist/types/components/index.d.ts +0 -6
  135. package/dist/types/components/index.d.ts.map +0 -1
  136. package/dist/types/context/RoomProvider.d.ts +0 -69
  137. package/dist/types/context/RoomProvider.d.ts.map +0 -1
  138. package/dist/types/context/index.d.ts +0 -3
  139. package/dist/types/context/index.d.ts.map +0 -1
  140. package/dist/types/dev/DevSimulator.d.ts +0 -31
  141. package/dist/types/dev/DevSimulator.d.ts.map +0 -1
  142. package/dist/types/dev/index.d.ts +0 -2
  143. package/dist/types/dev/index.d.ts.map +0 -1
  144. package/dist/types/hooks/index.d.ts +0 -7
  145. package/dist/types/hooks/index.d.ts.map +0 -1
  146. package/dist/types/hooks/useExternalGames.d.ts +0 -32
  147. package/dist/types/hooks/useExternalGames.d.ts.map +0 -1
  148. package/dist/types/hooks/useGameHost.d.ts +0 -67
  149. package/dist/types/hooks/useGameHost.d.ts.map +0 -1
  150. package/dist/types/hooks/useGamePlayer.d.ts +0 -55
  151. package/dist/types/hooks/useGamePlayer.d.ts.map +0 -1
  152. package/dist/types/iframe/IframeRoomProvider.d.ts +0 -31
  153. package/dist/types/iframe/IframeRoomProvider.d.ts.map +0 -1
  154. package/dist/types/iframe/index.d.ts +0 -18
  155. package/dist/types/iframe/index.d.ts.map +0 -1
  156. package/dist/types/iframe/vanilla-entry.d.ts +0 -7
  157. package/dist/types/iframe/vanilla-entry.d.ts.map +0 -1
  158. package/dist/types/iframe/vanilla.d.ts +0 -49
  159. package/dist/types/iframe/vanilla.d.ts.map +0 -1
  160. package/dist/types/server/createGameRelay.d.ts +0 -26
  161. package/dist/types/server/createGameRelay.d.ts.map +0 -1
  162. package/dist/types/server/index.d.ts +0 -3
  163. package/dist/types/server/index.d.ts.map +0 -1
  164. package/dist/types/utils/connectionMonitor.d.ts +0 -57
  165. package/dist/types/utils/connectionMonitor.d.ts.map +0 -1
  166. package/dist/types/utils/index.d.ts +0 -7
  167. package/dist/types/utils/index.d.ts.map +0 -1
  168. package/dist/types/utils/preloadAssets.d.ts +0 -29
  169. package/dist/types/utils/preloadAssets.d.ts.map +0 -1
  170. package/dist/types/utils/serverTime.d.ts +0 -28
  171. package/dist/types/utils/serverTime.d.ts.map +0 -1
  172. package/dist/umd/smore-sdk-iframe.umd.js +0 -266
  173. package/dist/umd/smore-sdk-iframe.umd.js.map +0 -1
  174. package/dist/umd/smore-sdk-iframe.umd.min.js +0 -2
  175. package/dist/umd/smore-sdk-iframe.umd.min.js.map +0 -1
  176. package/dist/umd/smore-sdk-vanilla.umd.js +0 -1275
  177. package/dist/umd/smore-sdk-vanilla.umd.js.map +0 -1
  178. package/dist/umd/smore-sdk-vanilla.umd.min.js +0 -2
  179. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"serverTime.cjs","sources":["../../../src/utils/serverTime.ts"],"sourcesContent":["/**\n * Server time synchronization utility.\n * AirConsole pattern: getServerTime() for synchronized clocks.\n *\n * Used for quiz games (who pressed first), action games (latency compensation).\n *\n * @example\n * ```typescript\n * const timeSync = createTimeSync(socket);\n * await timeSync.sync(); // Initial sync\n *\n * const serverNow = timeSync.getServerTime();\n * socket.emit('answer', { timestamp: serverNow });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\n\nexport interface TimeSync {\n /** Synchronize with server (call once at start) */\n sync: () => Promise<void>;\n /** Get current server time (after sync) */\n getServerTime: () => number;\n /** Get latency to server in ms */\n getLatency: () => number;\n /** Check if synchronized */\n isSynced: () => boolean;\n}\n\nexport function createTimeSync(socket: Socket): TimeSync {\n let offset = 0; // serverTime - clientTime\n let latency = 0;\n let synced = false;\n\n return {\n sync: () => {\n return new Promise((resolve, reject) => {\n const requestTime = Date.now();\n\n socket.emit('smore:time-sync', { requestTime }, (response: any) => {\n if (!response?.success) {\n reject(new Error('Time sync failed'));\n return;\n }\n\n const responseTime = Date.now();\n const roundTrip = responseTime - requestTime;\n latency = roundTrip / 2;\n\n // Server time when we received response (estimated)\n const serverTimeAtResponse = response.serverTime + latency;\n offset = serverTimeAtResponse - responseTime;\n\n synced = true;\n console.log(`[TimeSync] Synced. Offset: ${offset}ms, Latency: ${latency}ms`);\n resolve();\n });\n\n // Timeout\n setTimeout(() => {\n if (!synced) reject(new Error('Time sync timeout'));\n }, 5000);\n });\n },\n\n getServerTime: () => {\n if (!synced) {\n console.warn('[TimeSync] Not synced yet, returning local time');\n return Date.now();\n }\n return Date.now() + offset;\n },\n\n getLatency: () => latency,\n\n isSynced: () => synced,\n };\n}\n"],"names":[],"mappings":";;AA6BO,SAAS,eAAe,MAAA,EAA0B;AACvD,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,IAAI,MAAA,GAAS,KAAA;AAEb,EAAA,OAAO;AAAA,IACL,MAAM,MAAM;AACV,MAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,QAAA,MAAM,WAAA,GAAc,KAAK,GAAA,EAAI;AAE7B,QAAA,MAAA,CAAO,KAAK,iBAAA,EAAmB,EAAE,WAAA,EAAY,EAAG,CAAC,QAAA,KAAkB;AACjE,UAAA,IAAI,CAAC,UAAU,OAAA,EAAS;AACtB,YAAA,MAAA,CAAO,IAAI,KAAA,CAAM,kBAAkB,CAAC,CAAA;AACpC,YAAA;AAAA,UACF;AAEA,UAAA,MAAM,YAAA,GAAe,KAAK,GAAA,EAAI;AAC9B,UAAA,MAAM,YAAY,YAAA,GAAe,WAAA;AACjC,UAAA,OAAA,GAAU,SAAA,GAAY,CAAA;AAGtB,UAAA,MAAM,oBAAA,GAAuB,SAAS,UAAA,GAAa,OAAA;AACnD,UAAA,MAAA,GAAS,oBAAA,GAAuB,YAAA;AAEhC,UAAA,MAAA,GAAS,IAAA;AACT,UAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,2BAAA,EAA8B,MAAM,CAAA,aAAA,EAAgB,OAAO,CAAA,EAAA,CAAI,CAAA;AAC3E,UAAA,OAAA,EAAQ;AAAA,QACV,CAAC,CAAA;AAGD,QAAA,UAAA,CAAW,MAAM;AACf,UAAA,IAAI,CAAC,MAAA,EAAQ,MAAA,CAAO,IAAI,KAAA,CAAM,mBAAmB,CAAC,CAAA;AAAA,QACpD,GAAG,GAAI,CAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,eAAe,MAAM;AACnB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,OAAA,CAAQ,KAAK,iDAAiD,CAAA;AAC9D,QAAA,OAAO,KAAK,GAAA,EAAI;AAAA,MAClB;AACA,MAAA,OAAO,IAAA,CAAK,KAAI,GAAI,MAAA;AAAA,IACtB,CAAA;AAAA,IAEA,YAAY,MAAM,OAAA;AAAA,IAElB,UAAU,MAAM;AAAA,GAClB;AACF;;;;"}
@@ -1,304 +0,0 @@
1
- import { DirectTransport } from './transport/DirectTransport.js';
2
- import { PostMessageTransport } from './transport/PostMessageTransport.js';
3
- import { isSmoreMessage } from './transport/protocol.js';
4
-
5
- const SYSTEM_PREFIX = "smore:";
6
- const SYSTEM_EVENTS = {
7
- PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
8
- PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,
9
- GAME_OVER: `${SYSTEM_PREFIX}game-over`
10
- };
11
- const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
12
- function validateEventName(event) {
13
- if (!EVENT_NAME_REGEX.test(event)) {
14
- throw new Error(
15
- `[SmoreHost] Invalid event name "${event}". Event names must:
16
- - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
17
- - Start and end with a letter (no leading/trailing - or _)`
18
- );
19
- }
20
- }
21
- class SmoreHost {
22
- transport = null;
23
- config;
24
- _players = [];
25
- _roomCode = "";
26
- _leaderIndex = -1;
27
- _isReady = false;
28
- _isDestroyed = false;
29
- boundMessageHandler = null;
30
- registeredHandlers = [];
31
- constructor(config = {}) {
32
- this.config = config;
33
- if (config.listeners) {
34
- for (const event of Object.keys(config.listeners)) {
35
- validateEventName(event);
36
- }
37
- }
38
- if (config.socket) {
39
- this.initBundled(config);
40
- } else {
41
- this.initIframe(config);
42
- }
43
- }
44
- // ---------------------------------------------------------------------------
45
- // Initialization
46
- // ---------------------------------------------------------------------------
47
- initBundled(config) {
48
- if (!config.socket) {
49
- throw new Error("[SmoreHost] socket is required for bundled games");
50
- }
51
- this.transport = new DirectTransport(config.socket);
52
- this._roomCode = config.roomCode || "";
53
- this._players = config.players || [];
54
- this._leaderIndex = config.leaderIndex ?? -1;
55
- this.setupEventHandlers();
56
- this._isReady = true;
57
- this.config.onReady?.();
58
- }
59
- initIframe(config) {
60
- const parentOrigin = config.parentOrigin || "*";
61
- window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
62
- this.boundMessageHandler = (e) => {
63
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
64
- const msg = e.data;
65
- if (!isSmoreMessage(msg)) return;
66
- if (msg.type === "smore:init") {
67
- const initData = msg.payload;
68
- if (initData.side !== "host") {
69
- console.error("[SmoreHost] Received init for wrong side:", initData.side);
70
- return;
71
- }
72
- this.transport = new PostMessageTransport(parentOrigin);
73
- this._roomCode = initData.roomCode;
74
- this._players = this.mapPlayersFromInit(initData.players);
75
- this._leaderIndex = this.findLeaderIndex(initData.players, initData.leaderId);
76
- this.setupEventHandlers();
77
- this._isReady = true;
78
- this.config.onReady?.();
79
- } else if (msg.type === "smore:update") {
80
- const updateData = msg.payload;
81
- if (updateData.players) {
82
- this._players = this.mapPlayersFromInit(updateData.players);
83
- }
84
- if (updateData.leaderId !== void 0) {
85
- this._leaderIndex = this.findLeaderIndex(this._players, updateData.leaderId);
86
- }
87
- }
88
- };
89
- window.addEventListener("message", this.boundMessageHandler);
90
- }
91
- mapPlayersFromInit(players) {
92
- return players.map((p, index) => ({
93
- playerIndex: p.playerIndex ?? index,
94
- nickname: p.nickname || `Player ${index + 1}`,
95
- connected: p.connected !== false,
96
- appearance: p.appearance
97
- }));
98
- }
99
- findLeaderIndex(players, leaderId) {
100
- if (!leaderId) return -1;
101
- const idx = players.findIndex((p) => p.sessionId === leaderId);
102
- return idx >= 0 ? idx : -1;
103
- }
104
- setupEventHandlers() {
105
- if (!this.transport) return;
106
- this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data) => {
107
- const playerIndex = data.player?.playerIndex;
108
- if (playerIndex !== void 0) {
109
- this.config.onPlayerJoin?.(playerIndex);
110
- }
111
- });
112
- this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data) => {
113
- if (data.playerIndex !== void 0) {
114
- this.config.onPlayerLeave?.(data.playerIndex);
115
- }
116
- });
117
- this.registerHandler("room:player-joined", (data) => {
118
- const playerIndex = data?.player?.playerIndex ?? data?.playerIndex;
119
- if (playerIndex !== void 0) {
120
- this.config.onPlayerJoin?.(playerIndex);
121
- }
122
- });
123
- this.registerHandler("room:player-left", (data) => {
124
- const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;
125
- if (playerIndex !== void 0) {
126
- this.config.onPlayerLeave?.(playerIndex);
127
- }
128
- });
129
- if (this.config.listeners) {
130
- for (const [event, handler] of Object.entries(this.config.listeners)) {
131
- if (!handler) continue;
132
- this.registerHandler(event, (data) => {
133
- const { playerIndex, ...rest } = data;
134
- if (playerIndex !== void 0) {
135
- handler(playerIndex, rest);
136
- }
137
- });
138
- }
139
- }
140
- }
141
- registerHandler(event, handler) {
142
- if (!this.transport) return;
143
- this.transport.on(event, handler);
144
- this.registeredHandlers.push({ event, handler });
145
- }
146
- // ---------------------------------------------------------------------------
147
- // Public Properties
148
- // ---------------------------------------------------------------------------
149
- /**
150
- * Get all players in the room.
151
- * Returns a copy to prevent external mutation.
152
- */
153
- get players() {
154
- return [...this._players];
155
- }
156
- /**
157
- * Get the room code.
158
- */
159
- get roomCode() {
160
- return this._roomCode;
161
- }
162
- /**
163
- * Get the leader's player index (-1 if no leader).
164
- */
165
- get leaderIndex() {
166
- return this._leaderIndex;
167
- }
168
- /**
169
- * Check if the host is initialized and ready.
170
- */
171
- get isReady() {
172
- return this._isReady;
173
- }
174
- // ---------------------------------------------------------------------------
175
- // Public Methods
176
- // ---------------------------------------------------------------------------
177
- /**
178
- * Broadcast an event to all players.
179
- *
180
- * @param event - Event name (no colons allowed)
181
- * @param data - Optional data payload
182
- *
183
- * @example
184
- * ```ts
185
- * host.broadcast('phase-update', { phase: 'playing' });
186
- * host.broadcast('timer-tick', { remaining: 30 });
187
- * ```
188
- */
189
- broadcast(event, data) {
190
- this.ensureReady("broadcast");
191
- validateEventName(event);
192
- this.transport.emit(event, data);
193
- }
194
- /**
195
- * Send an event to a specific player.
196
- *
197
- * @param playerIndex - Target player index (0, 1, 2, ...)
198
- * @param event - Event name (no colons allowed)
199
- * @param data - Optional data payload
200
- *
201
- * @example
202
- * ```ts
203
- * host.sendToPlayer(0, 'your-turn', { timeLimit: 30 });
204
- * host.sendToPlayer(1, 'wait', { message: 'Not your turn' });
205
- * ```
206
- */
207
- sendToPlayer(playerIndex, event, data) {
208
- this.ensureReady("sendToPlayer");
209
- validateEventName(event);
210
- this.transport.emit(event, {
211
- targetPlayerIndex: playerIndex,
212
- ...data && typeof data === "object" ? data : { data }
213
- });
214
- }
215
- /**
216
- * Signal game over with results.
217
- * This will broadcast the game over event to all players.
218
- *
219
- * @param results - Game results (scores, winner, etc.)
220
- *
221
- * @example
222
- * ```ts
223
- * host.gameOver({
224
- * scores: { 0: 100, 1: 75, 2: 50 },
225
- * winner: 0,
226
- * });
227
- * ```
228
- */
229
- gameOver(results) {
230
- this.ensureReady("gameOver");
231
- this.transport.emit(SYSTEM_EVENTS.GAME_OVER, { results });
232
- }
233
- /**
234
- * Add a listener for a specific event after construction.
235
- *
236
- * @param event - Event name (no colons allowed)
237
- * @param handler - Handler function (playerIndex, data) => void
238
- * @returns Cleanup function to remove the listener
239
- *
240
- * @example
241
- * ```ts
242
- * const cleanup = host.on('tap', (playerIndex, data) => {
243
- * console.log(`Player ${playerIndex} tapped`);
244
- * });
245
- *
246
- * // Later
247
- * cleanup();
248
- * ```
249
- */
250
- on(event, handler) {
251
- validateEventName(event);
252
- const wrappedHandler = (data) => {
253
- const { playerIndex, ...rest } = data;
254
- if (playerIndex !== void 0) {
255
- handler(playerIndex, rest);
256
- }
257
- };
258
- if (this.transport) {
259
- this.transport.on(event, wrappedHandler);
260
- this.registeredHandlers.push({ event, handler: wrappedHandler });
261
- }
262
- return () => {
263
- this.transport?.off(event, wrappedHandler);
264
- this.registeredHandlers = this.registeredHandlers.filter(
265
- (h) => h.event !== event || h.handler !== wrappedHandler
266
- );
267
- };
268
- }
269
- /**
270
- * Clean up all resources.
271
- * Call this when unmounting/destroying the game.
272
- */
273
- destroy() {
274
- if (this._isDestroyed) return;
275
- this._isDestroyed = true;
276
- this._isReady = false;
277
- for (const { event, handler } of this.registeredHandlers) {
278
- this.transport?.off(event, handler);
279
- }
280
- this.registeredHandlers = [];
281
- if (this.transport instanceof PostMessageTransport) {
282
- this.transport.destroy();
283
- }
284
- this.transport = null;
285
- if (this.boundMessageHandler) {
286
- window.removeEventListener("message", this.boundMessageHandler);
287
- this.boundMessageHandler = null;
288
- }
289
- }
290
- // ---------------------------------------------------------------------------
291
- // Private Helpers
292
- // ---------------------------------------------------------------------------
293
- ensureReady(method) {
294
- if (!this._isReady || !this.transport) {
295
- throw new Error(`[SmoreHost] Cannot call ${method}() before host is ready. Wait for onReady callback.`);
296
- }
297
- if (this._isDestroyed) {
298
- throw new Error(`[SmoreHost] Cannot call ${method}() after destroy()`);
299
- }
300
- }
301
- }
302
-
303
- export { SmoreHost };
304
- //# sourceMappingURL=SmoreHost.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SmoreHost.js","sources":["../../src/SmoreHost.ts"],"sourcesContent":["/**\n * SmoreHost - Unified Host-side class for the S'MORE SDK (AirConsole style)\n *\n * Works in any environment: React, Phaser, Vanilla JS.\n * Automatically detects iframe vs bundled environment.\n *\n * @example Iframe game (auto-detection)\n * ```ts\n * const host = new SmoreHost({\n * onPlayerJoin: (playerIndex) => console.log('Player joined:', playerIndex),\n * listeners: {\n * tap: (playerIndex, data) => handleTap(playerIndex, data),\n * },\n * });\n *\n * // Later\n * host.broadcast('phase-update', { phase: 'playing' });\n * host.gameOver({ scores: { 0: 100, 1: 50 } });\n * ```\n *\n * @example Bundled game (direct socket)\n * ```ts\n * const host = new SmoreHost({\n * socket,\n * roomCode: 'ABCD',\n * players: [...],\n * leaderIndex: 0,\n * listeners: { ... },\n * });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\nimport type { Transport, TransportEventHandler } from './transport/types';\nimport { DirectTransport } from './transport/DirectTransport';\nimport { PostMessageTransport } from './transport/PostMessageTransport';\nimport { isSmoreMessage, type SmoreInitMessage, type SmoreUpdateMessage } from './transport/protocol';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n GAME_OVER: `${SYSTEM_PREFIX}game-over`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SmoreHost] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Player information exposed to game developers.\n * Uses playerIndex (0, 1, 2, ...) instead of internal sessionId.\n */\nexport interface SmorePlayer {\n /** Player index (0, 1, 2, ...) */\n playerIndex: number;\n /** Player's chosen nickname */\n nickname: string;\n /** Whether player is currently connected */\n connected: boolean;\n /** Player's character appearance (optional) */\n appearance?: {\n skinColor?: string;\n hairColor?: string;\n shirtColor?: string;\n pantsColor?: string;\n };\n}\n\n/**\n * Configuration for SmoreHost constructor.\n */\nexport interface SmoreHostConfig {\n // === Callbacks ===\n\n /** Called when the host is initialized and ready (iframe games only) */\n onReady?: () => void;\n\n /** Called when a player joins the room */\n onPlayerJoin?: (playerIndex: number) => void;\n\n /** Called when a player leaves the room */\n onPlayerLeave?: (playerIndex: number) => void;\n\n /**\n * Event listeners for specific events.\n * Keys are event names (no colons), values are handler functions.\n * Handler receives (playerIndex, data).\n */\n listeners?: Record<string, (playerIndex: number, data: any) => void>;\n\n // === Bundled game options (skip iframe detection) ===\n\n /** Socket.IO socket instance (bundled games only) */\n socket?: Socket;\n\n /** Room code (bundled games only) */\n roomCode?: string;\n\n /** Initial players array (bundled games only) */\n players?: SmorePlayer[];\n\n /** Leader player index (bundled games only) */\n leaderIndex?: number;\n\n // === Iframe game options ===\n\n /** Parent window origin for postMessage validation (iframe games) */\n parentOrigin?: string;\n}\n\n// ---------------------------------------------------------------------------\n// SmoreHost Class\n// ---------------------------------------------------------------------------\n\n/**\n * SmoreHost - Main host-side class for game development.\n *\n * Automatically detects iframe vs bundled environment:\n * - Iframe: Uses PostMessageTransport, waits for smore:init from parent\n * - Bundled: Uses DirectTransport with provided socket\n */\nexport class SmoreHost {\n private transport: Transport | null = null;\n private config: SmoreHostConfig;\n private _players: SmorePlayer[] = [];\n private _roomCode: string = '';\n private _leaderIndex: number = -1;\n private _isReady: boolean = false;\n private _isDestroyed: boolean = false;\n private boundMessageHandler: ((e: MessageEvent) => void) | null = null;\n private registeredHandlers: Array<{ event: string; handler: TransportEventHandler }> = [];\n\n constructor(config: SmoreHostConfig = {}) {\n this.config = config;\n\n // Validate event names in listeners\n if (config.listeners) {\n for (const event of Object.keys(config.listeners)) {\n validateEventName(event);\n }\n }\n\n // Detect environment and initialize\n if (config.socket) {\n // Bundled game mode: use DirectTransport\n this.initBundled(config);\n } else {\n // Iframe game mode: use PostMessageTransport\n this.initIframe(config);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n private initBundled(config: SmoreHostConfig): void {\n if (!config.socket) {\n throw new Error('[SmoreHost] socket is required for bundled games');\n }\n\n this.transport = new DirectTransport(config.socket);\n this._roomCode = config.roomCode || '';\n this._players = config.players || [];\n this._leaderIndex = config.leaderIndex ?? -1;\n\n this.setupEventHandlers();\n\n // Mark as ready immediately for bundled games\n this._isReady = true;\n this.config.onReady?.();\n }\n\n private initIframe(config: SmoreHostConfig): void {\n const parentOrigin = config.parentOrigin || '*';\n\n // Signal ready to parent\n window.parent.postMessage({ type: 'smore:ready' }, parentOrigin);\n\n // Listen for init message from parent\n this.boundMessageHandler = (e: MessageEvent) => {\n if (parentOrigin !== '*' && e.origin !== parentOrigin) return;\n\n const msg = e.data;\n if (!isSmoreMessage(msg)) return;\n\n if (msg.type === 'smore:init') {\n const initData = (msg as SmoreInitMessage).payload;\n\n if (initData.side !== 'host') {\n console.error('[SmoreHost] Received init for wrong side:', initData.side);\n return;\n }\n\n // Initialize transport\n this.transport = new PostMessageTransport(parentOrigin);\n this._roomCode = initData.roomCode;\n this._players = this.mapPlayersFromInit(initData.players);\n this._leaderIndex = this.findLeaderIndex(initData.players, initData.leaderId);\n\n this.setupEventHandlers();\n\n this._isReady = true;\n this.config.onReady?.();\n } else if (msg.type === 'smore:update') {\n const updateData = (msg as SmoreUpdateMessage).payload;\n\n if (updateData.players) {\n this._players = this.mapPlayersFromInit(updateData.players);\n }\n if (updateData.leaderId !== undefined) {\n this._leaderIndex = this.findLeaderIndex(this._players as any[], updateData.leaderId);\n }\n }\n };\n\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n private mapPlayersFromInit(players: any[]): SmorePlayer[] {\n return players.map((p, index) => ({\n playerIndex: p.playerIndex ?? index,\n nickname: p.nickname || `Player ${index + 1}`,\n connected: p.connected !== false,\n appearance: p.appearance,\n }));\n }\n\n private findLeaderIndex(players: any[], leaderId: string | null): number {\n if (!leaderId) return -1;\n const idx = players.findIndex((p) => p.sessionId === leaderId);\n return idx >= 0 ? idx : -1;\n }\n\n private setupEventHandlers(): void {\n if (!this.transport) return;\n\n // System events: player join/leave\n this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data: { player: SmorePlayer }) => {\n const playerIndex = data.player?.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data: { playerIndex: number }) => {\n if (data.playerIndex !== undefined) {\n this.config.onPlayerLeave?.(data.playerIndex);\n }\n });\n\n // Legacy room events (backward compatibility)\n this.registerHandler('room:player-joined', (data: any) => {\n const playerIndex = data?.player?.playerIndex ?? data?.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler('room:player-left', (data: any) => {\n const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerLeave?.(playerIndex);\n }\n });\n\n // User event listeners\n if (this.config.listeners) {\n for (const [event, handler] of Object.entries(this.config.listeners)) {\n if (!handler) continue;\n\n this.registerHandler(event, (data: { playerIndex: number; [key: string]: any }) => {\n const { playerIndex, ...rest } = data;\n if (playerIndex !== undefined) {\n handler(playerIndex, rest);\n }\n });\n }\n }\n }\n\n private registerHandler(event: string, handler: TransportEventHandler): void {\n if (!this.transport) return;\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n // ---------------------------------------------------------------------------\n // Public Properties\n // ---------------------------------------------------------------------------\n\n /**\n * Get all players in the room.\n * Returns a copy to prevent external mutation.\n */\n get players(): SmorePlayer[] {\n return [...this._players];\n }\n\n /**\n * Get the room code.\n */\n get roomCode(): string {\n return this._roomCode;\n }\n\n /**\n * Get the leader's player index (-1 if no leader).\n */\n get leaderIndex(): number {\n return this._leaderIndex;\n }\n\n /**\n * Check if the host is initialized and ready.\n */\n get isReady(): boolean {\n return this._isReady;\n }\n\n // ---------------------------------------------------------------------------\n // Public Methods\n // ---------------------------------------------------------------------------\n\n /**\n * Broadcast an event to all players.\n *\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * host.broadcast('phase-update', { phase: 'playing' });\n * host.broadcast('timer-tick', { remaining: 30 });\n * ```\n */\n broadcast(event: string, data?: any): void {\n this.ensureReady('broadcast');\n validateEventName(event);\n // Emit user event directly - genericRelay handles Host → Room broadcast\n this.transport!.emit(event, data);\n }\n\n /**\n * Send an event to a specific player.\n *\n * @param playerIndex - Target player index (0, 1, 2, ...)\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * host.sendToPlayer(0, 'your-turn', { timeLimit: 30 });\n * host.sendToPlayer(1, 'wait', { message: 'Not your turn' });\n * ```\n */\n sendToPlayer(playerIndex: number, event: string, data?: any): void {\n this.ensureReady('sendToPlayer');\n validateEventName(event);\n // Emit user event with targetPlayerIndex - genericRelay handles Host → Player\n this.transport!.emit(event, {\n targetPlayerIndex: playerIndex,\n ...(data && typeof data === 'object' ? data : { data }),\n });\n }\n\n /**\n * Signal game over with results.\n * This will broadcast the game over event to all players.\n *\n * @param results - Game results (scores, winner, etc.)\n *\n * @example\n * ```ts\n * host.gameOver({\n * scores: { 0: 100, 1: 75, 2: 50 },\n * winner: 0,\n * });\n * ```\n */\n gameOver(results?: any): void {\n this.ensureReady('gameOver');\n this.transport!.emit(SYSTEM_EVENTS.GAME_OVER, { results });\n }\n\n /**\n * Add a listener for a specific event after construction.\n *\n * @param event - Event name (no colons allowed)\n * @param handler - Handler function (playerIndex, data) => void\n * @returns Cleanup function to remove the listener\n *\n * @example\n * ```ts\n * const cleanup = host.on('tap', (playerIndex, data) => {\n * console.log(`Player ${playerIndex} tapped`);\n * });\n *\n * // Later\n * cleanup();\n * ```\n */\n on(event: string, handler: (playerIndex: number, data: any) => void): () => void {\n validateEventName(event);\n\n const wrappedHandler = (data: { playerIndex: number; [key: string]: any }) => {\n const { playerIndex, ...rest } = data;\n if (playerIndex !== undefined) {\n handler(playerIndex, rest);\n }\n };\n\n if (this.transport) {\n this.transport.on(event, wrappedHandler);\n this.registeredHandlers.push({ event, handler: wrappedHandler });\n }\n\n return () => {\n this.transport?.off(event, wrappedHandler);\n this.registeredHandlers = this.registeredHandlers.filter(\n (h) => h.event !== event || h.handler !== wrappedHandler\n );\n };\n }\n\n /**\n * Clean up all resources.\n * Call this when unmounting/destroying the game.\n */\n destroy(): void {\n if (this._isDestroyed) return;\n\n this._isDestroyed = true;\n this._isReady = false;\n\n // Remove all registered handlers\n for (const { event, handler } of this.registeredHandlers) {\n this.transport?.off(event, handler);\n }\n this.registeredHandlers = [];\n\n // Destroy transport\n if (this.transport instanceof PostMessageTransport) {\n (this.transport as PostMessageTransport).destroy();\n }\n this.transport = null;\n\n // Remove message listener\n if (this.boundMessageHandler) {\n window.removeEventListener('message', this.boundMessageHandler);\n this.boundMessageHandler = null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private Helpers\n // ---------------------------------------------------------------------------\n\n private ensureReady(method: string): void {\n if (!this._isReady || !this.transport) {\n throw new Error(`[SmoreHost] Cannot call ${method}() before host is ready. Wait for onReady callback.`);\n }\n if (this._isDestroyed) {\n throw new Error(`[SmoreHost] Cannot call ${method}() after destroy()`);\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,227 +0,0 @@
1
- import { DirectTransport } from './transport/DirectTransport.js';
2
- import { PostMessageTransport } from './transport/PostMessageTransport.js';
3
- import { isSmoreMessage } from './transport/protocol.js';
4
-
5
- const SYSTEM_PREFIX = "smore:";
6
- const SYSTEM_EVENTS = {
7
- PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
8
- PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`
9
- };
10
- const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
11
- function validateEventName(event) {
12
- if (!EVENT_NAME_REGEX.test(event)) {
13
- throw new Error(
14
- `[SmorePlayer] Invalid event name "${event}". Event names must:
15
- - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
16
- - Start and end with a letter (no leading/trailing - or _)`
17
- );
18
- }
19
- }
20
- class SmorePlayer {
21
- transport = null;
22
- config;
23
- _roomCode = "";
24
- _myIndex = -1;
25
- _isLeader = false;
26
- _isReady = false;
27
- _isDestroyed = false;
28
- boundMessageHandler = null;
29
- registeredHandlers = [];
30
- constructor(config = {}) {
31
- this.config = config;
32
- if (config.listeners) {
33
- for (const event of Object.keys(config.listeners)) {
34
- validateEventName(event);
35
- }
36
- }
37
- if (config.socket) {
38
- this.initBundled(config);
39
- } else {
40
- this.initIframe(config);
41
- }
42
- }
43
- // ---------------------------------------------------------------------------
44
- // Initialization
45
- // ---------------------------------------------------------------------------
46
- initBundled(config) {
47
- if (!config.socket) {
48
- throw new Error("[SmorePlayer] socket is required for bundled games");
49
- }
50
- this.transport = new DirectTransport(config.socket);
51
- this._roomCode = config.roomCode || "";
52
- this._myIndex = config.myIndex ?? -1;
53
- this._isLeader = config.isLeader ?? false;
54
- this.setupEventHandlers();
55
- this._isReady = true;
56
- this.config.onReady?.();
57
- }
58
- initIframe(config) {
59
- const parentOrigin = config.parentOrigin || "*";
60
- window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
61
- this.boundMessageHandler = (e) => {
62
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
63
- const msg = e.data;
64
- if (!isSmoreMessage(msg)) return;
65
- if (msg.type === "smore:init") {
66
- const initData = msg.payload;
67
- if (initData.side !== "player") {
68
- console.error("[SmorePlayer] Received init for wrong side:", initData.side);
69
- return;
70
- }
71
- if (initData.myIndex === void 0) {
72
- console.error("[SmorePlayer] Missing myIndex in init payload");
73
- return;
74
- }
75
- this.transport = new PostMessageTransport(parentOrigin);
76
- this._roomCode = initData.roomCode;
77
- this._myIndex = initData.myIndex;
78
- this._isLeader = initData.isLeader ?? false;
79
- this.setupEventHandlers();
80
- this._isReady = true;
81
- this.config.onReady?.();
82
- } else if (msg.type === "smore:update") {
83
- const updateData = msg.payload;
84
- if (updateData.leaderId !== void 0) ;
85
- }
86
- };
87
- window.addEventListener("message", this.boundMessageHandler);
88
- }
89
- setupEventHandlers() {
90
- if (!this.transport) return;
91
- this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data) => {
92
- const playerIndex = data.player?.playerIndex ?? data.playerIndex;
93
- if (playerIndex !== void 0) {
94
- this.config.onPlayerJoin?.(playerIndex);
95
- }
96
- });
97
- this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data) => {
98
- const playerIndex = data.player?.playerIndex ?? data.playerIndex;
99
- if (playerIndex !== void 0) {
100
- this.config.onPlayerLeave?.(playerIndex);
101
- }
102
- });
103
- if (this.config.listeners) {
104
- for (const [event, handler] of Object.entries(this.config.listeners)) {
105
- if (!handler) continue;
106
- this.registerHandler(event, handler);
107
- }
108
- }
109
- }
110
- registerHandler(event, handler) {
111
- if (!this.transport) return;
112
- this.transport.on(event, handler);
113
- this.registeredHandlers.push({ event, handler });
114
- }
115
- // ---------------------------------------------------------------------------
116
- // Public Properties
117
- // ---------------------------------------------------------------------------
118
- /**
119
- * Get my player index (0, 1, 2, ...).
120
- */
121
- get myIndex() {
122
- return this._myIndex;
123
- }
124
- /**
125
- * Check if I am the room leader.
126
- */
127
- get isLeader() {
128
- return this._isLeader;
129
- }
130
- /**
131
- * Get the room code.
132
- */
133
- get roomCode() {
134
- return this._roomCode;
135
- }
136
- /**
137
- * Check if the player is initialized and ready.
138
- */
139
- get isReady() {
140
- return this._isReady;
141
- }
142
- // ---------------------------------------------------------------------------
143
- // Public Methods
144
- // ---------------------------------------------------------------------------
145
- /**
146
- * Send an event to the host.
147
- *
148
- * @param event - Event name (no colons allowed)
149
- * @param data - Optional data payload
150
- *
151
- * @example
152
- * ```ts
153
- * player.send('tap', { timestamp: Date.now() });
154
- * player.send('answer', { choice: 2 });
155
- * ```
156
- */
157
- send(event, data) {
158
- this.ensureReady("send");
159
- validateEventName(event);
160
- this.transport.emit(event, data);
161
- }
162
- /**
163
- * Add a listener for a specific event after construction.
164
- *
165
- * @param event - Event name (no colons allowed)
166
- * @param handler - Handler function (data) => void
167
- * @returns Cleanup function to remove the listener
168
- *
169
- * @example
170
- * ```ts
171
- * const cleanup = player.on('phase-update', (data) => {
172
- * console.log('New phase:', data.phase);
173
- * });
174
- *
175
- * // Later
176
- * cleanup();
177
- * ```
178
- */
179
- on(event, handler) {
180
- validateEventName(event);
181
- if (this.transport) {
182
- this.transport.on(event, handler);
183
- this.registeredHandlers.push({ event, handler });
184
- }
185
- return () => {
186
- this.transport?.off(event, handler);
187
- this.registeredHandlers = this.registeredHandlers.filter(
188
- (h) => h.event !== event || h.handler !== handler
189
- );
190
- };
191
- }
192
- /**
193
- * Clean up all resources.
194
- * Call this when unmounting/destroying the game.
195
- */
196
- destroy() {
197
- if (this._isDestroyed) return;
198
- this._isDestroyed = true;
199
- this._isReady = false;
200
- for (const { event, handler } of this.registeredHandlers) {
201
- this.transport?.off(event, handler);
202
- }
203
- this.registeredHandlers = [];
204
- if (this.transport instanceof PostMessageTransport) {
205
- this.transport.destroy();
206
- }
207
- this.transport = null;
208
- if (this.boundMessageHandler) {
209
- window.removeEventListener("message", this.boundMessageHandler);
210
- this.boundMessageHandler = null;
211
- }
212
- }
213
- // ---------------------------------------------------------------------------
214
- // Private Helpers
215
- // ---------------------------------------------------------------------------
216
- ensureReady(method) {
217
- if (!this._isReady || !this.transport) {
218
- throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);
219
- }
220
- if (this._isDestroyed) {
221
- throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);
222
- }
223
- }
224
- }
225
-
226
- export { SmorePlayer };
227
- //# sourceMappingURL=SmorePlayer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SmorePlayer.js","sources":["../../src/SmorePlayer.ts"],"sourcesContent":["/**\n * SmorePlayer - Unified Player-side class for the S'MORE SDK (AirConsole style)\n *\n * Works in any environment: React, Phaser, Vanilla JS.\n * Automatically detects iframe vs bundled environment.\n *\n * @example Iframe game (auto-detection)\n * ```ts\n * const player = new SmorePlayer({\n * onReady: () => console.log('Ready! My index:', player.myIndex),\n * listeners: {\n * 'phase-update': (data) => handlePhaseUpdate(data),\n * },\n * });\n *\n * // Later\n * player.send('tap', { timestamp: Date.now() });\n * ```\n *\n * @example Bundled game (direct socket)\n * ```ts\n * const player = new SmorePlayer({\n * socket,\n * roomCode: 'ABCD',\n * myIndex: 0,\n * isLeader: true,\n * listeners: { ... },\n * });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\nimport type { Transport, TransportEventHandler } from './transport/types';\nimport { DirectTransport } from './transport/DirectTransport';\nimport { PostMessageTransport } from './transport/PostMessageTransport';\nimport { isSmoreMessage, type SmoreInitMessage, type SmoreUpdateMessage } from './transport/protocol';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SmorePlayer] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Player information.\n */\nexport interface SmorePlayerInfo {\n /** Player index (0, 1, 2, ...) */\n playerIndex: number;\n /** Player's chosen nickname */\n nickname: string;\n /** Whether player is currently connected */\n connected: boolean;\n}\n\n/**\n * Configuration for SmorePlayer constructor.\n */\nexport interface SmorePlayerConfig {\n // === Callbacks ===\n\n /** Called when the player is ready and initialized (iframe games only) */\n onReady?: () => void;\n\n /** Called when another player joins the room */\n onPlayerJoin?: (playerIndex: number) => void;\n\n /** Called when another player leaves the room */\n onPlayerLeave?: (playerIndex: number) => void;\n\n /**\n * Event listeners for specific events.\n * Keys are event names (no colons), values are handler functions.\n * Handler receives (data) only - player side doesn't need playerIndex.\n */\n listeners?: Record<string, (data: any) => void>;\n\n // === Bundled game options (skip iframe detection) ===\n\n /** Socket.IO socket instance (bundled games only) */\n socket?: Socket;\n\n /** Room code (bundled games only) */\n roomCode?: string;\n\n /** My player index (bundled games only) */\n myIndex?: number;\n\n /** Am I the leader? (bundled games only) */\n isLeader?: boolean;\n\n // === Iframe game options ===\n\n /** Parent window origin for postMessage validation (iframe games) */\n parentOrigin?: string;\n}\n\n// ---------------------------------------------------------------------------\n// SmorePlayer Class\n// ---------------------------------------------------------------------------\n\n/**\n * SmorePlayer - Main player-side class for game development.\n *\n * Automatically detects iframe vs bundled environment:\n * - Iframe: Uses PostMessageTransport, waits for smore:init from parent\n * - Bundled: Uses DirectTransport with provided socket\n */\nexport class SmorePlayer {\n private transport: Transport | null = null;\n private config: SmorePlayerConfig;\n private _roomCode: string = '';\n private _myIndex: number = -1;\n private _isLeader: boolean = false;\n private _isReady: boolean = false;\n private _isDestroyed: boolean = false;\n private boundMessageHandler: ((e: MessageEvent) => void) | null = null;\n private registeredHandlers: Array<{ event: string; handler: TransportEventHandler }> = [];\n\n constructor(config: SmorePlayerConfig = {}) {\n this.config = config;\n\n // Validate event names in listeners\n if (config.listeners) {\n for (const event of Object.keys(config.listeners)) {\n validateEventName(event);\n }\n }\n\n // Detect environment and initialize\n if (config.socket) {\n // Bundled game mode: use DirectTransport\n this.initBundled(config);\n } else {\n // Iframe game mode: use PostMessageTransport\n this.initIframe(config);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n private initBundled(config: SmorePlayerConfig): void {\n if (!config.socket) {\n throw new Error('[SmorePlayer] socket is required for bundled games');\n }\n\n this.transport = new DirectTransport(config.socket);\n this._roomCode = config.roomCode || '';\n this._myIndex = config.myIndex ?? -1;\n this._isLeader = config.isLeader ?? false;\n\n this.setupEventHandlers();\n\n // Mark as ready immediately for bundled games\n this._isReady = true;\n this.config.onReady?.();\n }\n\n private initIframe(config: SmorePlayerConfig): void {\n const parentOrigin = config.parentOrigin || '*';\n\n // Signal ready to parent\n window.parent.postMessage({ type: 'smore:ready' }, parentOrigin);\n\n // Listen for init message from parent\n this.boundMessageHandler = (e: MessageEvent) => {\n if (parentOrigin !== '*' && e.origin !== parentOrigin) return;\n\n const msg = e.data;\n if (!isSmoreMessage(msg)) return;\n\n if (msg.type === 'smore:init') {\n const initData = (msg as SmoreInitMessage).payload;\n\n if (initData.side !== 'player') {\n console.error('[SmorePlayer] Received init for wrong side:', initData.side);\n return;\n }\n\n if (initData.myIndex === undefined) {\n console.error('[SmorePlayer] Missing myIndex in init payload');\n return;\n }\n\n // Initialize transport\n this.transport = new PostMessageTransport(parentOrigin);\n this._roomCode = initData.roomCode;\n this._myIndex = initData.myIndex;\n this._isLeader = initData.isLeader ?? false;\n\n this.setupEventHandlers();\n\n this._isReady = true;\n this.config.onReady?.();\n } else if (msg.type === 'smore:update') {\n const updateData = (msg as SmoreUpdateMessage).payload;\n\n // Update leader status if changed\n if (updateData.leaderId !== undefined) {\n // Note: Without players array, we can't determine isLeader change\n // This would require the parent to send myIndex in update\n }\n }\n };\n\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n private setupEventHandlers(): void {\n if (!this.transport) return;\n\n // System events: player join/leave\n this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data: { player?: SmorePlayerInfo; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data: { player?: { playerIndex?: number }; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerLeave?.(playerIndex);\n }\n });\n\n // User event listeners\n if (this.config.listeners) {\n for (const [event, handler] of Object.entries(this.config.listeners)) {\n if (!handler) continue;\n\n // Player side receives data directly (no playerIndex unwrapping)\n this.registerHandler(event, handler);\n }\n }\n }\n\n private registerHandler(event: string, handler: TransportEventHandler): void {\n if (!this.transport) return;\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n // ---------------------------------------------------------------------------\n // Public Properties\n // ---------------------------------------------------------------------------\n\n /**\n * Get my player index (0, 1, 2, ...).\n */\n get myIndex(): number {\n return this._myIndex;\n }\n\n /**\n * Check if I am the room leader.\n */\n get isLeader(): boolean {\n return this._isLeader;\n }\n\n /**\n * Get the room code.\n */\n get roomCode(): string {\n return this._roomCode;\n }\n\n /**\n * Check if the player is initialized and ready.\n */\n get isReady(): boolean {\n return this._isReady;\n }\n\n // ---------------------------------------------------------------------------\n // Public Methods\n // ---------------------------------------------------------------------------\n\n /**\n * Send an event to the host.\n *\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * player.send('tap', { timestamp: Date.now() });\n * player.send('answer', { choice: 2 });\n * ```\n */\n send(event: string, data?: any): void {\n this.ensureReady('send');\n validateEventName(event);\n this.transport!.emit(event, data);\n }\n\n /**\n * Add a listener for a specific event after construction.\n *\n * @param event - Event name (no colons allowed)\n * @param handler - Handler function (data) => void\n * @returns Cleanup function to remove the listener\n *\n * @example\n * ```ts\n * const cleanup = player.on('phase-update', (data) => {\n * console.log('New phase:', data.phase);\n * });\n *\n * // Later\n * cleanup();\n * ```\n */\n on(event: string, handler: (data: any) => void): () => void {\n validateEventName(event);\n\n if (this.transport) {\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n return () => {\n this.transport?.off(event, handler);\n this.registeredHandlers = this.registeredHandlers.filter(\n (h) => h.event !== event || h.handler !== handler\n );\n };\n }\n\n /**\n * Clean up all resources.\n * Call this when unmounting/destroying the game.\n */\n destroy(): void {\n if (this._isDestroyed) return;\n\n this._isDestroyed = true;\n this._isReady = false;\n\n // Remove all registered handlers\n for (const { event, handler } of this.registeredHandlers) {\n this.transport?.off(event, handler);\n }\n this.registeredHandlers = [];\n\n // Destroy transport\n if (this.transport instanceof PostMessageTransport) {\n (this.transport as PostMessageTransport).destroy();\n }\n this.transport = null;\n\n // Remove message listener\n if (this.boundMessageHandler) {\n window.removeEventListener('message', this.boundMessageHandler);\n this.boundMessageHandler = null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private Helpers\n // ---------------------------------------------------------------------------\n\n private ensureReady(method: string): void {\n if (!this._isReady || !this.transport) {\n throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);\n }\n if (this._isDestroyed) {\n throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);\n }\n }\n}\n"],"names":[],"mappings":";;;;AAyCA,MAAM,aAAA,GAAgB,QAAA;AAEtB,MAAM,aAAA,GAAgB;AAAA,EAEpB,WAAA,EAAa,GAAG,aAAa,CAAA,WAAA,CAAA;AAAA,EAC7B,YAAA,EAAc,GAAG,aAAa,CAAA,YAAA;AAChC,CAAA;AAMA,MAAM,gBAAA,GAAmB,kCAAA;AAEzB,SAAS,kBAAkB,KAAA,EAAqB;AAC9C,EAAA,IAAI,CAAC,gBAAA,CAAiB,IAAA,CAAK,KAAK,CAAA,EAAG;AACjC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,qCAAqC,KAAK,CAAA;AAAA;AAAA,4DAAA;AAAA,KAG5C;AAAA,EACF;AACF;AAuEO,MAAM,WAAA,CAAY;AAAA,EACf,SAAA,GAA8B,IAAA;AAAA,EAC9B,MAAA;AAAA,EACA,SAAA,GAAoB,EAAA;AAAA,EACpB,QAAA,GAAmB,EAAA;AAAA,EACnB,SAAA,GAAqB,KAAA;AAAA,EACrB,QAAA,GAAoB,KAAA;AAAA,EACpB,YAAA,GAAwB,KAAA;AAAA,EACxB,mBAAA,GAA0D,IAAA;AAAA,EAC1D,qBAA+E,EAAC;AAAA,EAExF,WAAA,CAAY,MAAA,GAA4B,EAAC,EAAG;AAC1C,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAGd,IAAA,IAAI,OAAO,SAAA,EAAW;AACpB,MAAA,KAAA,MAAW,KAAA,IAAS,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,SAAS,CAAA,EAAG;AACjD,QAAA,iBAAA,CAAkB,KAAK,CAAA;AAAA,MACzB;AAAA,IACF;AAGA,IAAA,IAAI,OAAO,MAAA,EAAQ;AAEjB,MAAA,IAAA,CAAK,YAAY,MAAM,CAAA;AAAA,IACzB,CAAA,MAAO;AAEL,MAAA,IAAA,CAAK,WAAW,MAAM,CAAA;AAAA,IACxB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAY,MAAA,EAAiC;AACnD,IAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,MAAA,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAAA,IACtE;AAEA,IAAA,IAAA,CAAK,SAAA,GAAY,IAAI,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA;AAClD,IAAA,IAAA,CAAK,SAAA,GAAY,OAAO,QAAA,IAAY,EAAA;AACpC,IAAA,IAAA,CAAK,QAAA,GAAW,OAAO,OAAA,IAAW,EAAA;AAClC,IAAA,IAAA,CAAK,SAAA,GAAY,OAAO,QAAA,IAAY,KAAA;AAEpC,IAAA,IAAA,CAAK,kBAAA,EAAmB;AAGxB,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,OAAO,OAAA,IAAU;AAAA,EACxB;AAAA,EAEQ,WAAW,MAAA,EAAiC;AAClD,IAAA,MAAM,YAAA,GAAe,OAAO,YAAA,IAAgB,GAAA;AAG5C,IAAA,MAAA,CAAO,OAAO,WAAA,CAAY,EAAE,IAAA,EAAM,aAAA,IAAiB,YAAY,CAAA;AAG/D,IAAA,IAAA,CAAK,mBAAA,GAAsB,CAAC,CAAA,KAAoB;AAC9C,MAAA,IAAI,YAAA,KAAiB,GAAA,IAAO,CAAA,CAAE,MAAA,KAAW,YAAA,EAAc;AAEvD,MAAA,MAAM,MAAM,CAAA,CAAE,IAAA;AACd,MAAA,IAAI,CAAC,cAAA,CAAe,GAAG,CAAA,EAAG;AAE1B,MAAA,IAAI,GAAA,CAAI,SAAS,YAAA,EAAc;AAC7B,QAAA,MAAM,WAAY,GAAA,CAAyB,OAAA;AAE3C,QAAA,IAAI,QAAA,CAAS,SAAS,QAAA,EAAU;AAC9B,UAAA,OAAA,CAAQ,KAAA,CAAM,6CAAA,EAA+C,QAAA,CAAS,IAAI,CAAA;AAC1E,UAAA;AAAA,QACF;AAEA,QAAA,IAAI,QAAA,CAAS,YAAY,MAAA,EAAW;AAClC,UAAA,OAAA,CAAQ,MAAM,+CAA+C,CAAA;AAC7D,UAAA;AAAA,QACF;AAGA,QAAA,IAAA,CAAK,SAAA,GAAY,IAAI,oBAAA,CAAqB,YAAY,CAAA;AACtD,QAAA,IAAA,CAAK,YAAY,QAAA,CAAS,QAAA;AAC1B,QAAA,IAAA,CAAK,WAAW,QAAA,CAAS,OAAA;AACzB,QAAA,IAAA,CAAK,SAAA,GAAY,SAAS,QAAA,IAAY,KAAA;AAEtC,QAAA,IAAA,CAAK,kBAAA,EAAmB;AAExB,QAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,QAAA,IAAA,CAAK,OAAO,OAAA,IAAU;AAAA,MACxB,CAAA,MAAA,IAAW,GAAA,CAAI,IAAA,KAAS,cAAA,EAAgB;AACtC,QAAA,MAAM,aAAc,GAAA,CAA2B,OAAA;AAG/C,QAAA,IAAI,UAAA,CAAW,aAAa,MAAA,EAAW;AAGvC,MACF;AAAA,IACF,CAAA;AAEA,IAAA,MAAA,CAAO,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,mBAAmB,CAAA;AAAA,EAC7D;AAAA,EAEQ,kBAAA,GAA2B;AACjC,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AAGrB,IAAA,IAAA,CAAK,eAAA,CAAgB,aAAA,CAAc,WAAA,EAAa,CAAC,IAAA,KAA6D;AAC5G,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,MAAA,EAAQ,WAAA,IAAe,IAAA,CAAK,WAAA;AACrD,MAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,QAAA,IAAA,CAAK,MAAA,CAAO,eAAe,WAAW,CAAA;AAAA,MACxC;AAAA,IACF,CAAC,CAAA;AAED,IAAA,IAAA,CAAK,eAAA,CAAgB,aAAA,CAAc,YAAA,EAAc,CAAC,IAAA,KAAsE;AACtH,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,MAAA,EAAQ,WAAA,IAAe,IAAA,CAAK,WAAA;AACrD,MAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,QAAA,IAAA,CAAK,MAAA,CAAO,gBAAgB,WAAW,CAAA;AAAA,MACzC;AAAA,IACF,CAAC,CAAA;AAGD,IAAA,IAAI,IAAA,CAAK,OAAO,SAAA,EAAW;AACzB,MAAA,KAAA,MAAW,CAAC,OAAO,OAAO,CAAA,IAAK,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,SAAS,CAAA,EAAG;AACpE,QAAA,IAAI,CAAC,OAAA,EAAS;AAGd,QAAA,IAAA,CAAK,eAAA,CAAgB,OAAO,OAAO,CAAA;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,eAAA,CAAgB,OAAe,OAAA,EAAsC;AAC3E,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACrB,IAAA,IAAA,CAAK,SAAA,CAAU,EAAA,CAAG,KAAA,EAAO,OAAO,CAAA;AAChC,IAAA,IAAA,CAAK,kBAAA,CAAmB,IAAA,CAAK,EAAE,KAAA,EAAO,SAAS,CAAA;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,IAAI,OAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,QAAA,GAAoB;AACtB,IAAA,OAAO,IAAA,CAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,QAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,IAAA,CAAK,OAAe,IAAA,EAAkB;AACpC,IAAA,IAAA,CAAK,YAAY,MAAM,CAAA;AACvB,IAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,IAAA,IAAA,CAAK,SAAA,CAAW,IAAA,CAAK,KAAA,EAAO,IAAI,CAAA;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,EAAA,CAAG,OAAe,OAAA,EAA0C;AAC1D,IAAA,iBAAA,CAAkB,KAAK,CAAA;AAEvB,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,IAAA,CAAK,SAAA,CAAU,EAAA,CAAG,KAAA,EAAO,OAAO,CAAA;AAChC,MAAA,IAAA,CAAK,kBAAA,CAAmB,IAAA,CAAK,EAAE,KAAA,EAAO,SAAS,CAAA;AAAA,IACjD;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,CAAK,SAAA,EAAW,GAAA,CAAI,KAAA,EAAO,OAAO,CAAA;AAClC,MAAA,IAAA,CAAK,kBAAA,GAAqB,KAAK,kBAAA,CAAmB,MAAA;AAAA,QAChD,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,KAAU,KAAA,IAAS,EAAE,OAAA,KAAY;AAAA,OAC5C;AAAA,IACF,CAAA;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAA,GAAgB;AACd,IAAA,IAAI,KAAK,YAAA,EAAc;AAEvB,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AACpB,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAGhB,IAAA,KAAA,MAAW,EAAE,KAAA,EAAO,OAAA,EAAQ,IAAK,KAAK,kBAAA,EAAoB;AACxD,MAAA,IAAA,CAAK,SAAA,EAAW,GAAA,CAAI,KAAA,EAAO,OAAO,CAAA;AAAA,IACpC;AACA,IAAA,IAAA,CAAK,qBAAqB,EAAC;AAG3B,IAAA,IAAI,IAAA,CAAK,qBAAqB,oBAAA,EAAsB;AAClD,MAAC,IAAA,CAAK,UAAmC,OAAA,EAAQ;AAAA,IACnD;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAGjB,IAAA,IAAI,KAAK,mBAAA,EAAqB;AAC5B,MAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,mBAAmB,CAAA;AAC9D,MAAA,IAAA,CAAK,mBAAA,GAAsB,IAAA;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAY,MAAA,EAAsB;AACxC,IAAA,IAAI,CAAC,IAAA,CAAK,QAAA,IAAY,CAAC,KAAK,SAAA,EAAW;AACrC,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,MAAM,CAAA,qDAAA,CAAuD,CAAA;AAAA,IAC5G;AACA,IAAA,IAAI,KAAK,YAAA,EAAc;AACrB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,MAAM,CAAA,kBAAA,CAAoB,CAAA;AAAA,IACzE;AAAA,EACF;AACF;;;;"}