@oyna360/lobby-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/avatar-accessories.d.ts +16 -0
  4. package/dist/avatar-accessories.d.ts.map +1 -0
  5. package/dist/avatar-accessories.js +214 -0
  6. package/dist/avatar-accessories.js.map +1 -0
  7. package/dist/avatar-asset-manager.d.ts +54 -0
  8. package/dist/avatar-asset-manager.d.ts.map +1 -0
  9. package/dist/avatar-asset-manager.js +215 -0
  10. package/dist/avatar-asset-manager.js.map +1 -0
  11. package/dist/avatar-clips.d.ts +17 -0
  12. package/dist/avatar-clips.d.ts.map +1 -0
  13. package/dist/avatar-clips.js +47 -0
  14. package/dist/avatar-clips.js.map +1 -0
  15. package/dist/avatar-config.d.ts +4 -0
  16. package/dist/avatar-config.d.ts.map +1 -0
  17. package/dist/avatar-config.js +9 -0
  18. package/dist/avatar-config.js.map +1 -0
  19. package/dist/avatar-factory.d.ts +64 -0
  20. package/dist/avatar-factory.d.ts.map +1 -0
  21. package/dist/avatar-factory.js +464 -0
  22. package/dist/avatar-factory.js.map +1 -0
  23. package/dist/avatar-instance.d.ts +31 -0
  24. package/dist/avatar-instance.d.ts.map +1 -0
  25. package/dist/avatar-instance.js +56 -0
  26. package/dist/avatar-instance.js.map +1 -0
  27. package/dist/avatar-material-pipeline.d.ts +34 -0
  28. package/dist/avatar-material-pipeline.d.ts.map +1 -0
  29. package/dist/avatar-material-pipeline.js +580 -0
  30. package/dist/avatar-material-pipeline.js.map +1 -0
  31. package/dist/avatar-rig-builder.d.ts +74 -0
  32. package/dist/avatar-rig-builder.d.ts.map +1 -0
  33. package/dist/avatar-rig-builder.js +347 -0
  34. package/dist/avatar-rig-builder.js.map +1 -0
  35. package/dist/draco.d.ts +7 -0
  36. package/dist/draco.d.ts.map +1 -0
  37. package/dist/draco.js +82 -0
  38. package/dist/draco.js.map +1 -0
  39. package/dist/humanoid-animator.d.ts +35 -0
  40. package/dist/humanoid-animator.d.ts.map +1 -0
  41. package/dist/humanoid-animator.js +191 -0
  42. package/dist/humanoid-animator.js.map +1 -0
  43. package/dist/humanoid-bone-names.d.ts +35 -0
  44. package/dist/humanoid-bone-names.d.ts.map +1 -0
  45. package/dist/humanoid-bone-names.js +126 -0
  46. package/dist/humanoid-bone-names.js.map +1 -0
  47. package/dist/humanoid-locomotion.d.ts +101 -0
  48. package/dist/humanoid-locomotion.d.ts.map +1 -0
  49. package/dist/humanoid-locomotion.js +300 -0
  50. package/dist/humanoid-locomotion.js.map +1 -0
  51. package/dist/humanoid-rig.d.ts +51 -0
  52. package/dist/humanoid-rig.d.ts.map +1 -0
  53. package/dist/humanoid-rig.js +304 -0
  54. package/dist/humanoid-rig.js.map +1 -0
  55. package/dist/index.d.ts +60 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +42 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/lobby-chat-ui.d.ts +3 -0
  60. package/dist/lobby-chat-ui.d.ts.map +1 -0
  61. package/dist/lobby-chat-ui.js +258 -0
  62. package/dist/lobby-chat-ui.js.map +1 -0
  63. package/dist/lobby-colliders.d.ts +10 -0
  64. package/dist/lobby-colliders.d.ts.map +1 -0
  65. package/dist/lobby-colliders.js +225 -0
  66. package/dist/lobby-colliders.js.map +1 -0
  67. package/dist/lobby-connection-ui.d.ts +8 -0
  68. package/dist/lobby-connection-ui.d.ts.map +1 -0
  69. package/dist/lobby-connection-ui.js +68 -0
  70. package/dist/lobby-connection-ui.js.map +1 -0
  71. package/dist/lobby-debug.d.ts +50 -0
  72. package/dist/lobby-debug.d.ts.map +1 -0
  73. package/dist/lobby-debug.js +132 -0
  74. package/dist/lobby-debug.js.map +1 -0
  75. package/dist/lobby-font.d.ts +5 -0
  76. package/dist/lobby-font.d.ts.map +1 -0
  77. package/dist/lobby-font.js +35 -0
  78. package/dist/lobby-font.js.map +1 -0
  79. package/dist/lobby-music.d.ts +40 -0
  80. package/dist/lobby-music.d.ts.map +1 -0
  81. package/dist/lobby-music.js +250 -0
  82. package/dist/lobby-music.js.map +1 -0
  83. package/dist/lobby-orientation-ui.d.ts +10 -0
  84. package/dist/lobby-orientation-ui.d.ts.map +1 -0
  85. package/dist/lobby-orientation-ui.js +64 -0
  86. package/dist/lobby-orientation-ui.js.map +1 -0
  87. package/dist/lobby-perf-diag.d.ts +254 -0
  88. package/dist/lobby-perf-diag.d.ts.map +1 -0
  89. package/dist/lobby-perf-diag.js +775 -0
  90. package/dist/lobby-perf-diag.js.map +1 -0
  91. package/dist/lobby-plaza-diag.d.ts +103 -0
  92. package/dist/lobby-plaza-diag.d.ts.map +1 -0
  93. package/dist/lobby-plaza-diag.js +534 -0
  94. package/dist/lobby-plaza-diag.js.map +1 -0
  95. package/dist/lobby-presence-ui.d.ts +4 -0
  96. package/dist/lobby-presence-ui.d.ts.map +1 -0
  97. package/dist/lobby-presence-ui.js +89 -0
  98. package/dist/lobby-presence-ui.js.map +1 -0
  99. package/dist/lobby-real-device-diag.d.ts +119 -0
  100. package/dist/lobby-real-device-diag.d.ts.map +1 -0
  101. package/dist/lobby-real-device-diag.js +468 -0
  102. package/dist/lobby-real-device-diag.js.map +1 -0
  103. package/dist/lobby-spawn.d.ts +15 -0
  104. package/dist/lobby-spawn.d.ts.map +1 -0
  105. package/dist/lobby-spawn.js +18 -0
  106. package/dist/lobby-spawn.js.map +1 -0
  107. package/dist/lobby-ui-i18n.d.ts +11 -0
  108. package/dist/lobby-ui-i18n.d.ts.map +1 -0
  109. package/dist/lobby-ui-i18n.js +21 -0
  110. package/dist/lobby-ui-i18n.js.map +1 -0
  111. package/dist/lobby-voice-ui.d.ts +3 -0
  112. package/dist/lobby-voice-ui.d.ts.map +1 -0
  113. package/dist/lobby-voice-ui.js +105 -0
  114. package/dist/lobby-voice-ui.js.map +1 -0
  115. package/dist/local-player-controller.d.ts +80 -0
  116. package/dist/local-player-controller.d.ts.map +1 -0
  117. package/dist/local-player-controller.js +422 -0
  118. package/dist/local-player-controller.js.map +1 -0
  119. package/dist/name-tag.d.ts +3 -0
  120. package/dist/name-tag.d.ts.map +1 -0
  121. package/dist/name-tag.js +59 -0
  122. package/dist/name-tag.js.map +1 -0
  123. package/dist/network-client.d.ts +87 -0
  124. package/dist/network-client.d.ts.map +1 -0
  125. package/dist/network-client.js +182 -0
  126. package/dist/network-client.js.map +1 -0
  127. package/dist/platform-bridge.d.ts +17 -0
  128. package/dist/platform-bridge.d.ts.map +1 -0
  129. package/dist/platform-bridge.js +90 -0
  130. package/dist/platform-bridge.js.map +1 -0
  131. package/dist/platform-lobby.d.ts +176 -0
  132. package/dist/platform-lobby.d.ts.map +1 -0
  133. package/dist/platform-lobby.js +679 -0
  134. package/dist/platform-lobby.js.map +1 -0
  135. package/dist/platform-types.d.ts +56 -0
  136. package/dist/platform-types.d.ts.map +1 -0
  137. package/dist/platform-types.js +3 -0
  138. package/dist/platform-types.js.map +1 -0
  139. package/dist/player-label.d.ts +9 -0
  140. package/dist/player-label.d.ts.map +1 -0
  141. package/dist/player-label.js +14 -0
  142. package/dist/player-label.js.map +1 -0
  143. package/dist/playground/playground-layout.d.ts +14 -0
  144. package/dist/playground/playground-layout.d.ts.map +1 -0
  145. package/dist/playground/playground-layout.js +320 -0
  146. package/dist/playground/playground-layout.js.map +1 -0
  147. package/dist/playground/playground-system.d.ts +30 -0
  148. package/dist/playground/playground-system.d.ts.map +1 -0
  149. package/dist/playground/playground-system.js +202 -0
  150. package/dist/playground/playground-system.js.map +1 -0
  151. package/dist/plaza-layout.d.ts +30 -0
  152. package/dist/plaza-layout.d.ts.map +1 -0
  153. package/dist/plaza-layout.js +527 -0
  154. package/dist/plaza-layout.js.map +1 -0
  155. package/dist/plugins.d.ts +5 -0
  156. package/dist/plugins.d.ts.map +1 -0
  157. package/dist/plugins.js +26 -0
  158. package/dist/plugins.js.map +1 -0
  159. package/dist/protocol.d.ts +276 -0
  160. package/dist/protocol.d.ts.map +1 -0
  161. package/dist/protocol.js +107 -0
  162. package/dist/protocol.js.map +1 -0
  163. package/dist/quality.d.ts +39 -0
  164. package/dist/quality.d.ts.map +1 -0
  165. package/dist/quality.js +95 -0
  166. package/dist/quality.js.map +1 -0
  167. package/dist/remote-player-manager.d.ts +48 -0
  168. package/dist/remote-player-manager.d.ts.map +1 -0
  169. package/dist/remote-player-manager.js +339 -0
  170. package/dist/remote-player-manager.js.map +1 -0
  171. package/dist/scene-manager.d.ts +26 -0
  172. package/dist/scene-manager.d.ts.map +1 -0
  173. package/dist/scene-manager.js +169 -0
  174. package/dist/scene-manager.js.map +1 -0
  175. package/dist/spawn-utils.d.ts +8 -0
  176. package/dist/spawn-utils.d.ts.map +1 -0
  177. package/dist/spawn-utils.js +29 -0
  178. package/dist/spawn-utils.js.map +1 -0
  179. package/dist/starter-layout.d.ts +17 -0
  180. package/dist/starter-layout.d.ts.map +1 -0
  181. package/dist/starter-layout.js +102 -0
  182. package/dist/starter-layout.js.map +1 -0
  183. package/dist/third-person-camera.d.ts +35 -0
  184. package/dist/third-person-camera.d.ts.map +1 -0
  185. package/dist/third-person-camera.js +181 -0
  186. package/dist/third-person-camera.js.map +1 -0
  187. package/dist/types.d.ts +164 -0
  188. package/dist/types.d.ts.map +1 -0
  189. package/dist/types.js +2 -0
  190. package/dist/types.js.map +1 -0
  191. package/dist/voice-chat.d.ts +78 -0
  192. package/dist/voice-chat.d.ts.map +1 -0
  193. package/dist/voice-chat.js +321 -0
  194. package/dist/voice-chat.js.map +1 -0
  195. package/dist/yield-to-render.d.ts +3 -0
  196. package/dist/yield-to-render.d.ts.map +1 -0
  197. package/dist/yield-to-render.js +11 -0
  198. package/dist/yield-to-render.js.map +1 -0
  199. package/dist/zones.d.ts +36 -0
  200. package/dist/zones.d.ts.map +1 -0
  201. package/dist/zones.js +68 -0
  202. package/dist/zones.js.map +1 -0
  203. package/docs/00-overview.md +47 -0
  204. package/docs/01-connect.md +85 -0
  205. package/docs/02-github.md +43 -0
  206. package/docs/03-modes.md +70 -0
  207. package/docs/04-api.md +106 -0
  208. package/docs/05-features.md +55 -0
  209. package/docs/06-controls.md +34 -0
  210. package/docs/07-troubleshooting.md +39 -0
  211. package/package.json +65 -0
@@ -0,0 +1,679 @@
1
+ import { MeshBuilder, SceneLoader, Vector3 as BVector3 } from '@babylonjs/core';
2
+ import '@babylonjs/loaders/glTF';
3
+ import { sanitizeLobbyChat, sanitizeLobbyDataChannel, sanitizeLobbyDataPayload, } from './protocol';
4
+ import { AvatarFactory } from './avatar-factory';
5
+ import { AvatarAssetManager } from './avatar-asset-manager';
6
+ import { ensureDracoDecoder } from './draco';
7
+ import { LocalPlayerController } from './local-player-controller';
8
+ import { NetworkClient } from './network-client';
9
+ import { PlatformBridge } from './platform-bridge';
10
+ import { RemotePlayerManager } from './remote-player-manager';
11
+ import { SceneManager } from './scene-manager';
12
+ import { PortalManager, ZoneManager } from './zones';
13
+ import { applyStarterLayout as applyStarterLayoutFn } from './starter-layout';
14
+ import { applyPlazaLayout as applyPlazaLayoutFn } from './plaza-layout';
15
+ import { applyLobbyCollisions } from './lobby-colliders';
16
+ import { attachLobbyDebug, buildLobbyDebugReport } from './lobby-debug';
17
+ import { attachLobbyPerfDiag, lobbyPerfNoteLocalUpdate, isLobbyPerfDiagEnabled } from './lobby-perf-diag';
18
+ import { attachRealDeviceAvatarDiag } from './lobby-real-device-diag';
19
+ import { attachPlazaDiag } from './lobby-plaza-diag';
20
+ import { attachPlayground } from './playground/playground-system';
21
+ import { LobbyMusic } from './lobby-music';
22
+ import { attachLobbyChatUi } from './lobby-chat-ui';
23
+ import { attachLobbyPresenceUi } from './lobby-presence-ui';
24
+ import { attachLobbyVoiceUi } from './lobby-voice-ui';
25
+ import { attachLobbyConnectionUi } from './lobby-connection-ui';
26
+ import { attachLobbyOrientationUi } from './lobby-orientation-ui';
27
+ import { ensureLobbyPersianFont } from './lobby-font';
28
+ import { provisionalSpawnSlot, resolveSpawnPose } from './spawn-utils';
29
+ import { LobbyVoiceChat } from './voice-chat';
30
+ const DEFAULT_LOBBY_FEATURES = {
31
+ chatEnabled: true,
32
+ voiceEnabled: true,
33
+ chatAllowed: true,
34
+ voiceAllowed: true,
35
+ };
36
+ export class PlatformLobby {
37
+ canvas;
38
+ strictRoom;
39
+ init;
40
+ config;
41
+ roomId;
42
+ wsUrl;
43
+ sceneManager;
44
+ localAvatar;
45
+ localController;
46
+ remotePlayers;
47
+ network = null;
48
+ zoneManager = new ZoneManager();
49
+ portalManager = new PortalManager();
50
+ plugins = [];
51
+ overlayHandlers = new Map();
52
+ listeners = new Map();
53
+ lastNetworkSend = 0;
54
+ destroyed = false;
55
+ ready = false;
56
+ playground = null;
57
+ music = new LobbyMusic();
58
+ chatDispose = null;
59
+ presenceDispose = null;
60
+ voiceDispose = null;
61
+ connectionDispose = null;
62
+ orientationDispose = null;
63
+ voiceChat = null;
64
+ lobbyFeatures = { ...DEFAULT_LOBBY_FEATURES };
65
+ resizeHandler = () => this.sceneManager?.resize();
66
+ constructor(init, canvas, roomId, wsUrl, config = {}, strictRoom = false) {
67
+ this.canvas = canvas;
68
+ this.strictRoom = strictRoom;
69
+ this.init = init;
70
+ this.roomId = roomId;
71
+ this.wsUrl = wsUrl;
72
+ this.config = {
73
+ enableMultiplayer: true,
74
+ ...config,
75
+ };
76
+ }
77
+ static async create(options) {
78
+ const init = PlatformBridge.fromInit(options.platformInit);
79
+ const roomId = options.roomId ?? init.lobby?.roomId ?? `game:${init.game.slug}`;
80
+ const wsUrl = options.wsUrl ?? init.lobby?.wsUrl ?? 'http://localhost:3001/lobby';
81
+ const strictRoom = options.strictRoom ?? init.lobby?.strictRoom ?? false;
82
+ const lobby = new PlatformLobby(init, options.canvas, roomId, wsUrl, options.config ?? {}, strictRoom);
83
+ await lobby.bootstrap();
84
+ return lobby;
85
+ }
86
+ static async createFromPlatform(canvas, config) {
87
+ const init = await PlatformBridge.waitForInit();
88
+ return PlatformLobby.create({
89
+ canvas,
90
+ roomId: init.lobby?.roomId ?? `game:${init.game.slug}`,
91
+ strictRoom: init.lobby?.strictRoom,
92
+ platformInit: init,
93
+ config,
94
+ wsUrl: init.lobby?.wsUrl,
95
+ });
96
+ }
97
+ static async createDev(options) {
98
+ const init = PlatformBridge.createDevInit({
99
+ mockUser: options.mockUser,
100
+ mockAvatar: options.mockAvatar,
101
+ gameSlug: options.roomId?.replace(/^game:/, '') ?? 'dev-game',
102
+ });
103
+ return PlatformLobby.create({
104
+ canvas: options.canvas,
105
+ roomId: options.roomId ?? init.lobby.roomId,
106
+ platformInit: init,
107
+ config: { enableMultiplayer: false, ...options.config },
108
+ wsUrl: options.wsUrl ?? init.lobby?.wsUrl,
109
+ });
110
+ }
111
+ async bootstrap() {
112
+ // Font CDN must never block first paint (can hang >60s on filtered networks).
113
+ void ensureLobbyPersianFont();
114
+ // Warm Draco WASM in parallel with scene setup (HTTP-cached on repeat visits).
115
+ void ensureDracoDecoder();
116
+ this.sceneManager = new SceneManager(this.canvas, this.config);
117
+ const layout = this.config;
118
+ const provisionalSlot = provisionalSpawnSlot(this.init.user.id, layout.spawnSlotCount ?? layout.spawnPoints?.length ?? 16);
119
+ const spawnPose = resolveSpawnPose(layout, provisionalSlot);
120
+ this.remotePlayers = new RemotePlayerManager(this.sceneManager.scene, this.init.user.id);
121
+ window.addEventListener('resize', this.resizeHandler);
122
+ // Register + fire-and-forget preload so first paint / render loop stay unblocked.
123
+ this.applyConfiguredAvatarBases();
124
+ // Show sky/ground immediately — do not freeze the canvas while the local GLB parses.
125
+ let lastTime = performance.now();
126
+ this.sceneManager.startRenderLoop(() => {
127
+ if (this.destroyed)
128
+ return;
129
+ const now = performance.now();
130
+ const dt = Math.min(0.05, (now - lastTime) / 1000);
131
+ lastTime = now;
132
+ if (!this.localController)
133
+ return;
134
+ const tLocal = isLobbyPerfDiagEnabled() ? performance.now() : 0;
135
+ const state = this.localController.update(dt);
136
+ if (isLobbyPerfDiagEnabled()) {
137
+ lobbyPerfNoteLocalUpdate(performance.now() - tLocal);
138
+ }
139
+ this.playground?.update(dt, this.localController, state.position);
140
+ this.sceneManager.followPlayer(this.localAvatar.position, dt, this.localController.getIgnoreMeshes());
141
+ this.zoneManager.updatePlayer(this.init.user.id, state.position);
142
+ const portalHits = this.portalManager.updatePlayer(this.init.user.id, state.position);
143
+ for (const hit of portalHits) {
144
+ hit.onTrigger?.();
145
+ this.emit('portalTrigger', {
146
+ portalId: hit.portalId,
147
+ toGameSlug: hit.toGameSlug,
148
+ toScene: hit.toScene,
149
+ });
150
+ }
151
+ if (this.network?.isConnected() && now - this.lastNetworkSend > 50) {
152
+ this.lastNetworkSend = now;
153
+ this.network.sendMove({
154
+ position: state.position,
155
+ rotationY: state.rotationY,
156
+ animation: state.animation,
157
+ });
158
+ this.voiceChat?.refreshMesh();
159
+ }
160
+ this.remotePlayers.update(dt);
161
+ });
162
+ this.localAvatar = await AvatarFactory.createAsync(this.sceneManager.scene, this.init.avatar, 'local-player', this.init.user.displayName, this.init.user.username, { collider: true });
163
+ if (this.destroyed)
164
+ return;
165
+ this.localController = new LocalPlayerController(this.localAvatar, spawnPose.position, this.sceneManager.scene, () => this.sceneManager.camera, this.config);
166
+ this.localController.teleportTo(spawnPose.position, spawnPose.rotationY);
167
+ this.localController.setSounds(this.music);
168
+ this.canvas.addEventListener('pointerdown', () => {
169
+ void this.music.unlock(true);
170
+ });
171
+ if (this.config.enableMultiplayer !== false && this.init.session.token !== 'dev-token') {
172
+ this.connectNetwork();
173
+ }
174
+ this.emit('ready', undefined);
175
+ this.ready = true;
176
+ const uiMessages = this.config.uiMessages;
177
+ if (this.config.enableMultiplayer !== false && this.config.enablePresenceUi !== false) {
178
+ this.presenceDispose = attachLobbyPresenceUi(this, uiMessages);
179
+ }
180
+ if (this.config.enableChat !== false)
181
+ this.attachChat();
182
+ if (this.config.enableVoice !== false && this.config.enableMultiplayer !== false) {
183
+ this.attachVoice();
184
+ }
185
+ if (this.config.enableMultiplayer !== false && this.config.enableConnectionUi !== false) {
186
+ this.connectionDispose = attachLobbyConnectionUi(this, uiMessages);
187
+ }
188
+ if (this.config.enableOrientationUi !== false) {
189
+ this.orientationDispose = attachLobbyOrientationUi({
190
+ locale: this.config.locale,
191
+ messages: uiMessages,
192
+ });
193
+ }
194
+ for (const plugin of this.plugins) {
195
+ void plugin.setup(this);
196
+ }
197
+ if (typeof window !== 'undefined') {
198
+ this.attachDebug(window);
199
+ const perf = attachLobbyPerfDiag(() => this.sceneManager.scene, () => this.sceneManager.engine, window);
200
+ attachRealDeviceAvatarDiag({
201
+ upsertRemote: (player) => this.diagUpsertRemote(player),
202
+ removeRemote: (userId) => this.diagRemoveRemote(userId),
203
+ waitRemote: (userId, timeoutMs) => this.diagWaitRemote(userId, timeoutMs),
204
+ waitRemoteReady: (userId, timeoutMs) => this.diagWaitRemoteReady(userId, timeoutMs),
205
+ applyRemoteMove: (payload) => this.diagApplyRemoteMove(payload),
206
+ isAvatarBaseCached: (id) => this.isAvatarBaseCached(id),
207
+ preloadAvatarBases: (ids) => this.preloadAvatarBases(ids),
208
+ registerAvatarBases: (defs) => this.registerAvatarBases(defs),
209
+ getScene: () => this.sceneManager.scene,
210
+ getEngine: () => this.sceneManager.engine,
211
+ }, perf, window);
212
+ attachPlazaDiag(() => this.sceneManager.scene, () => this.sceneManager.engine, perf, window);
213
+ window.__OYNA360_SDK_BUILD__ = 'avatar-join-v13';
214
+ console.info('[lobby-sdk] build avatar-join-v13');
215
+ }
216
+ }
217
+ /**
218
+ * DIAG ONLY: upsert a synthetic remote player for performance measurement.
219
+ * Does not change multiplayer protocol behavior.
220
+ */
221
+ diagUpsertRemote(player) {
222
+ this.remotePlayers.upsert(player);
223
+ }
224
+ /** DIAG ONLY: remove a synthetic/remote player used for measurement. */
225
+ diagRemoveRemote(userId) {
226
+ this.remotePlayers.remove(userId);
227
+ }
228
+ /** DIAG ONLY: wait until a remote userId appears in the remote list (or timeout). */
229
+ async diagWaitRemote(userId, timeoutMs = 60_000) {
230
+ const start = performance.now();
231
+ while (performance.now() - start < timeoutMs) {
232
+ if (this.remotePlayers.list().some((p) => p.userId === userId))
233
+ return true;
234
+ await new Promise((r) => setTimeout(r, 50));
235
+ }
236
+ return false;
237
+ }
238
+ /** DIAG ONLY: wait until remote GLB/placeholder swap finished. */
239
+ async diagWaitRemoteReady(userId, timeoutMs = 60_000) {
240
+ const start = performance.now();
241
+ while (performance.now() - start < timeoutMs) {
242
+ if (this.remotePlayers.isRemoteReady(userId))
243
+ return true;
244
+ await new Promise((r) => setTimeout(r, 50));
245
+ }
246
+ return false;
247
+ }
248
+ /** Register shared base avatar GLBs (catalog). Does not load until preload/spawn. */
249
+ registerAvatarBases(defs) {
250
+ AvatarAssetManager.registerAvatarBases(defs);
251
+ }
252
+ registerAvatarBase(def) {
253
+ AvatarAssetManager.registerAvatarBase(def);
254
+ }
255
+ listAvatarBases() {
256
+ return AvatarAssetManager.listAvatarBases();
257
+ }
258
+ isAvatarBaseCached(id) {
259
+ if (!this.sceneManager?.scene)
260
+ return false;
261
+ return AvatarAssetManager.isAvatarBaseCached(this.sceneManager.scene, id);
262
+ }
263
+ /**
264
+ * Warm container cache for one base. Non-throwing; returns null on failure.
265
+ * Safe to call after bootstrap — does not block the render loop by itself.
266
+ */
267
+ preloadAvatarBase(id) {
268
+ return AvatarAssetManager.preloadAvatarBase(this.sceneManager.scene, id);
269
+ }
270
+ /**
271
+ * Warm container cache for bases (all registered if ids omitted).
272
+ * Failures are per-id; lobby continues on the normal async spawn path.
273
+ */
274
+ preloadAvatarBases(ids) {
275
+ return AvatarAssetManager.preloadAvatarBases(this.sceneManager.scene, ids);
276
+ }
277
+ /** Apply config.avatarBases + init.avatarBases + optional fire-and-forget preload (non-blocking). */
278
+ applyConfiguredAvatarBases() {
279
+ const fromConfig = this.config.avatarBases ?? [];
280
+ const fromInit = this.init.avatarBases ?? [];
281
+ const bases = [...fromConfig, ...fromInit];
282
+ if (bases.length) {
283
+ AvatarAssetManager.registerAvatarBases(bases);
284
+ }
285
+ const flag = this.config.preloadAvatarBases;
286
+ if (flag === false)
287
+ return;
288
+ // Always warm registered bases (config and/or launch catalog) unless explicitly disabled.
289
+ if (!bases.length && flag !== true && !Array.isArray(flag))
290
+ return;
291
+ const ids = Array.isArray(flag) ? flag : undefined;
292
+ void AvatarAssetManager.preloadAvatarBases(this.sceneManager.scene, ids).then((results) => {
293
+ const failed = results.filter((r) => !r.ok);
294
+ if (failed.length) {
295
+ console.warn('[lobby-sdk] avatar base preload incomplete', failed.map((f) => f.id));
296
+ }
297
+ });
298
+ }
299
+ /** DIAG ONLY: update synthetic remote move/animation targets (no network). */
300
+ diagApplyRemoteMove(payload) {
301
+ this.remotePlayers.applyMove(payload);
302
+ }
303
+ connectNetwork() {
304
+ this.voiceChat = new LobbyVoiceChat({
305
+ sendJoin: (mode) => this.network?.sendVoiceJoin(mode),
306
+ sendLeave: () => this.network?.sendVoiceLeave(),
307
+ sendMute: (muted) => this.network?.sendVoiceMute(muted),
308
+ sendOffer: (toUserId, sdp) => this.network?.sendVoiceOffer(toUserId, sdp),
309
+ sendAnswer: (toUserId, sdp) => this.network?.sendVoiceAnswer(toUserId, sdp),
310
+ sendIce: (toUserId, candidate) => this.network?.sendVoiceIce(toUserId, candidate),
311
+ });
312
+ this.voiceChat.setContext(this.init.user.id, []);
313
+ this.voiceChat.setPositionProvider((userId) => {
314
+ if (userId === this.init.user.id) {
315
+ return this.localController.getState().position;
316
+ }
317
+ return this.remotePlayers.getPosition(userId);
318
+ });
319
+ this.network = new NetworkClient(this.wsUrl, this.roomId, this.init.session.token, {
320
+ onWelcome: (self, players, meta) => {
321
+ this.localController.teleportTo(self.position, self.rotationY);
322
+ if (meta?.lobbyFeatures)
323
+ this.lobbyFeatures = meta.lobbyFeatures;
324
+ this.voiceChat?.setContext(this.init.user.id, meta?.friendUserIds ?? []);
325
+ if (meta?.voicePeers) {
326
+ this.voiceChat?.handleVoiceState(meta.voicePeers, meta?.friendUserIds ?? []);
327
+ }
328
+ for (const p of players)
329
+ this.remotePlayers.upsert(p);
330
+ this.emit('connected', undefined);
331
+ },
332
+ onPlayerJoined: (player) => {
333
+ this.remotePlayers.upsert(player);
334
+ this.emit('playerJoined', {
335
+ userId: player.userId,
336
+ displayName: player.displayName,
337
+ username: player.username,
338
+ });
339
+ },
340
+ onPlayerLeft: (payload) => {
341
+ const info = this.remotePlayers.getIdentity(payload.userId);
342
+ this.remotePlayers.remove(payload.userId);
343
+ this.emit('playerLeft', {
344
+ userId: payload.userId,
345
+ displayName: payload.displayName ?? info?.displayName,
346
+ username: payload.username ?? info?.username,
347
+ });
348
+ },
349
+ onPlayerMoved: (payload) => {
350
+ this.remotePlayers.applyMove(payload);
351
+ },
352
+ onPlayersMoved: (moves) => {
353
+ for (const payload of moves) {
354
+ if (payload.userId === this.init.user.id)
355
+ continue;
356
+ this.remotePlayers.applyMove(payload);
357
+ }
358
+ },
359
+ onPlayerEmote: (userId, emote) => {
360
+ this.remotePlayers.applyEmote(userId, emote);
361
+ },
362
+ onChat: (payload) => {
363
+ if (payload.userId === this.init.user.id)
364
+ return;
365
+ this.emit('chat', payload);
366
+ },
367
+ onData: (payload) => {
368
+ if (payload.userId === this.init.user.id)
369
+ return;
370
+ this.emit('data', payload);
371
+ },
372
+ onVoiceState: (peers, friendUserIds) => {
373
+ this.voiceChat?.handleVoiceState(peers, friendUserIds);
374
+ },
375
+ onVoiceJoined: (peer) => {
376
+ this.voiceChat?.handleVoiceJoined(peer);
377
+ },
378
+ onVoiceLeft: (userId) => {
379
+ this.voiceChat?.handleVoiceLeft(userId);
380
+ },
381
+ onVoiceMute: (userId, muted) => {
382
+ this.voiceChat?.handleVoiceMute(userId, muted);
383
+ },
384
+ onVoiceOffer: (fromUserId, sdp) => {
385
+ void this.voiceChat?.handleOffer(fromUserId, sdp);
386
+ },
387
+ onVoiceAnswer: (fromUserId, sdp) => {
388
+ void this.voiceChat?.handleAnswer(fromUserId, sdp);
389
+ },
390
+ onVoiceIce: (fromUserId, candidate) => {
391
+ void this.voiceChat?.handleIce(fromUserId, candidate);
392
+ },
393
+ onError: (code, message) => {
394
+ this.emit('error', { code, message });
395
+ },
396
+ onDisconnected: () => {
397
+ this.emit('disconnected', undefined);
398
+ },
399
+ onReconnecting: (attempt) => {
400
+ this.emit('reconnecting', { attempt });
401
+ },
402
+ onReconnectFailed: () => {
403
+ this.emit('reconnectFailed', undefined);
404
+ },
405
+ }, { strictRoom: this.strictRoom });
406
+ this.network.connect();
407
+ }
408
+ on(event, handler) {
409
+ if (!this.listeners.has(event))
410
+ this.listeners.set(event, new Set());
411
+ this.listeners.get(event).add(handler);
412
+ if (event === 'ready' && this.ready) {
413
+ handler(undefined);
414
+ }
415
+ return () => this.off(event, handler);
416
+ }
417
+ off(event, handler) {
418
+ this.listeners.get(event)?.delete(handler);
419
+ }
420
+ emit(event, payload) {
421
+ for (const handler of this.listeners.get(event) ?? []) {
422
+ handler(payload);
423
+ }
424
+ }
425
+ use(plugin) {
426
+ this.plugins.push(plugin);
427
+ if (this.ready)
428
+ void plugin.setup(this);
429
+ return this;
430
+ }
431
+ /** Optional starter-plaza template. Appearance and start routing stay in the game. */
432
+ applyPlazaLayout(config) {
433
+ applyPlazaLayoutFn(this, config);
434
+ applyLobbyCollisions(this.sceneManager.scene);
435
+ if (!this.playground)
436
+ this.playground = attachPlayground(this);
437
+ return this;
438
+ }
439
+ applyStarterLayout(config) {
440
+ applyStarterLayoutFn(this, config);
441
+ applyLobbyCollisions(this.sceneManager.scene);
442
+ return this;
443
+ }
444
+ attachDebug(win) {
445
+ return attachLobbyDebug(this, win ?? window);
446
+ }
447
+ getDebugReport() {
448
+ return buildLobbyDebugReport(this);
449
+ }
450
+ addZone(options) {
451
+ this.zoneManager.addZone({
452
+ id: options.id,
453
+ bounds: options.bounds,
454
+ onEnter: (playerId) => {
455
+ options.onEnter?.(playerId);
456
+ this.emit('zoneEnter', { zoneId: options.id, playerId });
457
+ },
458
+ onExit: (playerId) => {
459
+ options.onExit?.(playerId);
460
+ this.emit('zoneExit', { zoneId: options.id, playerId });
461
+ },
462
+ });
463
+ }
464
+ /**
465
+ * Game-owned portal. The SDK only detects the player and fires `onTrigger` / `portalTrigger`.
466
+ * It never navigates the browser — the game starts its own gameplay.
467
+ */
468
+ addPortal(options) {
469
+ this.portalManager.addPortal(options);
470
+ }
471
+ /** Platform identity: user, avatar, session, room. */
472
+ getSession() {
473
+ return this.init.session;
474
+ }
475
+ getUser() {
476
+ return this.init.user;
477
+ }
478
+ getAvatar() {
479
+ return this.init.avatar;
480
+ }
481
+ /** Other players currently synced in this lobby room. */
482
+ getPlayers() {
483
+ return this.remotePlayers.list();
484
+ }
485
+ async loadGLB(url, name) {
486
+ const result = await SceneLoader.ImportMeshAsync('', url, undefined, this.sceneManager.scene);
487
+ const root = result.meshes[0];
488
+ if (name)
489
+ root.name = name;
490
+ return root;
491
+ }
492
+ onOverlay(id, handler) {
493
+ this.overlayHandlers.set(id, handler);
494
+ }
495
+ emitOverlay(id, payload) {
496
+ this.overlayHandlers.get(id)?.(payload);
497
+ }
498
+ playEmote(emote) {
499
+ this.network?.sendEmote(emote);
500
+ }
501
+ /** Live lobby chat. Not saved. Always echoes locally so the sender sees the line. */
502
+ sendChat(text) {
503
+ if (!this.canUseChat())
504
+ return false;
505
+ const clean = sanitizeLobbyChat(text);
506
+ if (!clean)
507
+ return false;
508
+ this.emit('chat', {
509
+ userId: this.init.user.id,
510
+ username: this.init.user.username,
511
+ displayName: this.init.user.displayName,
512
+ text: clean,
513
+ at: Date.now(),
514
+ });
515
+ this.network?.sendChat(clean);
516
+ return true;
517
+ }
518
+ /**
519
+ * Ephemeral game data for this lobby room (matchmaking, pad sync, …).
520
+ * Independent of chat — works when chat is disabled/banned.
521
+ * Local-echoes so the sender sees the same `data` event as peers.
522
+ * Prefer namespaced channels: `{gameSlug}.{feature}` (e.g. `fc.pad-room`).
523
+ */
524
+ sendData(channel, payload) {
525
+ if (!this.canUseData())
526
+ return false;
527
+ const ch = sanitizeLobbyDataChannel(channel);
528
+ const body = sanitizeLobbyDataPayload(payload);
529
+ if (!ch || !body)
530
+ return false;
531
+ if (!this.network?.isConnected())
532
+ return false;
533
+ this.emit('data', {
534
+ userId: this.init.user.id,
535
+ username: this.init.user.username,
536
+ displayName: this.init.user.displayName,
537
+ channel: ch,
538
+ payload: body,
539
+ at: Date.now(),
540
+ });
541
+ this.network.sendData(ch, body);
542
+ return true;
543
+ }
544
+ attachChat() {
545
+ if (this.chatDispose)
546
+ return this;
547
+ this.chatDispose = attachLobbyChatUi(this, this.canvas);
548
+ return this;
549
+ }
550
+ attachVoice() {
551
+ if (this.voiceDispose || !this.voiceChat)
552
+ return this;
553
+ this.voiceDispose = attachLobbyVoiceUi(this);
554
+ return this;
555
+ }
556
+ getVoiceChat() {
557
+ return this.voiceChat;
558
+ }
559
+ getLobbyFeatures() {
560
+ return this.lobbyFeatures;
561
+ }
562
+ canUseChat() {
563
+ return this.lobbyFeatures.chatEnabled && this.lobbyFeatures.chatAllowed;
564
+ }
565
+ /** Game data channel — defaults on when flags are omitted. */
566
+ canUseData() {
567
+ return this.lobbyFeatures.dataEnabled !== false && this.lobbyFeatures.dataAllowed !== false;
568
+ }
569
+ canUseVoice() {
570
+ return this.lobbyFeatures.voiceEnabled && this.lobbyFeatures.voiceAllowed;
571
+ }
572
+ /** Virtual joystick / on-screen pad. x = strafe, z = forward (-1..1). */
573
+ setMoveStick(x, z) {
574
+ this.localController.setStick(x, z);
575
+ }
576
+ setLookStick(x, y) {
577
+ this.sceneManager.thirdPerson.setLookStick(x, y);
578
+ }
579
+ addLookDelta(dx, dy) {
580
+ this.sceneManager.thirdPerson.addLookDelta(dx, dy);
581
+ }
582
+ /** Game-side camera distance (third-person orbit radius). */
583
+ setCameraOrbit(radius, limits) {
584
+ const tp = this.sceneManager.thirdPerson;
585
+ const cam = tp.camera;
586
+ if (typeof limits?.lower === 'number')
587
+ cam.lowerRadiusLimit = limits.lower;
588
+ if (typeof limits?.upper === 'number')
589
+ cam.upperRadiusLimit = limits.upper;
590
+ const clamped = Math.min(cam.upperRadiusLimit ?? 16, Math.max(cam.lowerRadiusLimit ?? 2.2, radius));
591
+ tp.wantedRadius = clamped;
592
+ cam.radius = clamped;
593
+ }
594
+ tryPlaygroundInteract() {
595
+ return this.playground?.trySlide() ?? false;
596
+ }
597
+ getLocalController() {
598
+ return this.localController;
599
+ }
600
+ noteMusicToggle() {
601
+ this.music.noteUserToggled();
602
+ }
603
+ toggleMusic() {
604
+ return this.music.toggleMuted();
605
+ }
606
+ setMusicMuted(muted) {
607
+ this.music.setMuted(muted);
608
+ }
609
+ isMusicMuted() {
610
+ return this.music.isMuted();
611
+ }
612
+ jump() {
613
+ this.localController.jump();
614
+ }
615
+ setJumpHeld(on) {
616
+ this.localController.setJumpHeld(on);
617
+ }
618
+ setSprint(on) {
619
+ this.localController.setSprint(on);
620
+ }
621
+ /** Ask the hub to close this lobby iframe and return home. Never navigates itself. */
622
+ requestExit() {
623
+ if (typeof window === 'undefined' || window.parent === window)
624
+ return;
625
+ const token = this.init.session?.token;
626
+ if (token) {
627
+ window.parent.postMessage({ type: 'platform:session:end', sessionToken: token }, '*');
628
+ }
629
+ window.parent.postMessage({ type: 'platform:lobby:exit' }, '*');
630
+ }
631
+ getInitPayload() {
632
+ return this.init;
633
+ }
634
+ getLocalPose() {
635
+ return this.localController.getState();
636
+ }
637
+ getScene() {
638
+ return this.sceneManager.scene;
639
+ }
640
+ getUiLocale() {
641
+ return this.config.locale === 'en' ? 'en' : 'fa';
642
+ }
643
+ getEngine() {
644
+ return this.sceneManager.engine;
645
+ }
646
+ addMeshAt(position, size = 1) {
647
+ const box = MeshBuilder.CreateBox('custom-mesh', { size }, this.sceneManager.scene);
648
+ box.position = new BVector3(position.x, position.y + size / 2, position.z);
649
+ return box;
650
+ }
651
+ destroy() {
652
+ if (this.destroyed)
653
+ return;
654
+ this.destroyed = true;
655
+ window.removeEventListener('resize', this.resizeHandler);
656
+ this.chatDispose?.();
657
+ this.chatDispose = null;
658
+ this.presenceDispose?.();
659
+ this.presenceDispose = null;
660
+ this.voiceDispose?.();
661
+ this.voiceDispose = null;
662
+ this.connectionDispose?.();
663
+ this.connectionDispose = null;
664
+ this.orientationDispose?.();
665
+ this.orientationDispose = null;
666
+ this.voiceChat?.dispose();
667
+ this.voiceChat = null;
668
+ this.network?.disconnect();
669
+ this.localController.dispose();
670
+ this.music.dispose();
671
+ this.playground?.dispose();
672
+ this.remotePlayers.dispose();
673
+ this.sceneManager.dispose();
674
+ this.zoneManager.clear();
675
+ this.portalManager.clear();
676
+ this.emit('destroyed', undefined);
677
+ }
678
+ }
679
+ //# sourceMappingURL=platform-lobby.js.map