@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oyna360
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @oyna360/lobby-sdk
2
+
3
+ لابی سه‌بعدی **oyna360** برای بازی‌سازان — بر پایه Babylon.js.
4
+
5
+ پلتفرم هویت، آواتار و اتاق WebSocket را می‌دهد.
6
+ **ظاهر لابی، پورتال‌ها و شروع گیم‌پلی مال شماست.**
7
+
8
+ **نسخه پکیج:** مطابق GitHub tag · پروتکل داخل همین پکیج است — `@platform/lobby-protocol` را نصب نکنید.
9
+
10
+ > اتصال اولیه به پلتفرم (سشن / Authorize) کار [`@oyna360/game-sdk`](https://github.com/mamadjavadrasti/oyna360-game-sdk) است. این SDK لابی را اجرا می‌کند، لاگین نمی‌کند.
11
+
12
+ ---
13
+
14
+ ## نصب
15
+
16
+ ```bash
17
+ npm install github:mamadjavadrasti/oyna360-lobby-sdk#master @babylonjs/core
18
+ ```
19
+
20
+ `@babylonjs/core` peer dependency است (نسخه ۷ توصیه می‌شود).
21
+
22
+ ```json
23
+ {
24
+ "dependencies": {
25
+ "@oyna360/lobby-sdk": "github:mamadjavadrasti/oyna360-lobby-sdk#master",
26
+ "@babylonjs/core": "^7.44.0"
27
+ }
28
+ }
29
+ ```
30
+
31
+ تگ/`dist` تازه را پین کنید. اگر `dist/index.js` هنوز `@platform/lobby-protocol` import کند، بیلد کهنه است.
32
+
33
+ ---
34
+
35
+ ## سه حالت ورود به لابی
36
+
37
+ | حالت | کی استفاده کنید | API |
38
+ |------|------------------|-----|
39
+ | **Production** | بازیکن از `/play/{slug}` آمده | `createFromPlatform` یا `create({ platformInit })` بعد از game-sdk |
40
+ | **Direct Development** | بازی روی origin خودتان + game-sdk Authorize | `create({ platformInit })` با Context از `PlatformSDK.init()` |
41
+ | **Offline / Fake** | تست UI بدون سرور | `createDev()` — مالتی‌پلیر واقعی ندارد |
42
+
43
+ ---
44
+
45
+ ## شروع سریع — با پلتفرم واقعی
46
+
47
+ ```ts
48
+ import { PlatformSDK } from '@oyna360/game-sdk';
49
+ import { PlatformLobby } from '@oyna360/lobby-sdk';
50
+
51
+ const canvas = document.getElementById('lobby') as HTMLCanvasElement;
52
+
53
+ // 1) Context از پلتفرم (iframe یا Direct Dev)
54
+ const init = await PlatformSDK.init({
55
+ // Direct فقط:
56
+ // platformUrl, platformWebUrl, gameSlug
57
+ });
58
+
59
+ // 2) لابی واقعی
60
+ const lobby = await PlatformLobby.create({
61
+ canvas,
62
+ platformInit: init,
63
+ roomId: init.lobby!.roomId,
64
+ wsUrl: init.lobby!.wsUrl,
65
+ });
66
+
67
+ lobby.applyPlazaLayout({
68
+ onRoomStart: (room) => {
69
+ lobby.destroy();
70
+ startGameplay(room.id);
71
+ },
72
+ });
73
+ ```
74
+
75
+ `wsUrl` / `roomId` را هاردکد نکنید.
76
+
77
+ ---
78
+
79
+ ## شروع سریع — فقط UI آفلاین
80
+
81
+ ```ts
82
+ const lobby = await PlatformLobby.createDev({
83
+ canvas,
84
+ roomId: 'game:my-game',
85
+ config: { enableMultiplayer: false },
86
+ });
87
+ lobby.applyPlazaLayout();
88
+ ```
89
+
90
+ این مسیر برای توسعه ظاهر است، نه تست مالتی‌پلیر روی سرور.
91
+
92
+ ---
93
+
94
+ ## مستندات
95
+
96
+ | موضوع | فایل |
97
+ |--------|------|
98
+ | نقش SDK و مرز با game-sdk | [docs/00-overview.md](./docs/00-overview.md) |
99
+ | اتصال و پیش‌نیاز | [docs/01-connect.md](./docs/01-connect.md) |
100
+ | حالت‌های Production / Direct / Dev | [docs/03-modes.md](./docs/03-modes.md) |
101
+ | API `PlatformLobby` | [docs/04-api.md](./docs/04-api.md) |
102
+ | آواتار، چت، voice، data | [docs/05-features.md](./docs/05-features.md) |
103
+ | کنترل‌ها و موبایل | [docs/06-controls.md](./docs/06-controls.md) |
104
+ | عیب‌یابی | [docs/07-troubleshooting.md](./docs/07-troubleshooting.md) |
105
+ | انتشار از GitHub | [docs/02-github.md](./docs/02-github.md) |
106
+
107
+ ---
108
+
109
+ ## امکانات اصلی
110
+
111
+ - صحنه Babylon، دوربین سوم‌شخص، برخورد
112
+ - آواتار procedural / GLB + اکسسوری
113
+ - بازیکنان remote، حرکت، انیمیشن
114
+ - چت زنده، presence، voice (اختیاری)
115
+ - کانال `lobby:data` برای matchmaking / کنترل بازی
116
+ - Plaza / Starter layout، zone، portal، plugin
117
+ - کیفیت خودکار و UI فارسی/انگلیسی
118
+
119
+ ---
120
+
121
+ ## لایسنس
122
+
123
+ MIT
@@ -0,0 +1,16 @@
1
+ import { Scene, TransformNode } from '@babylonjs/core';
2
+ export type AccessoryPrimitive = 'cap' | 'pack' | 'glasses' | 'watch';
3
+ export interface EquippedAccessory {
4
+ slot: 'hat' | 'back' | 'face' | 'hand';
5
+ itemId: string;
6
+ glbUrl?: string;
7
+ primitive?: AccessoryPrimitive;
8
+ attachBone?: string;
9
+ }
10
+ /**
11
+ * Attach equipped accessories onto an avatar root produced by AvatarFactory.
12
+ * Safe to call repeatedly — clears previous `metadata.accessoryNodes` first.
13
+ */
14
+ export declare function attachAvatarAccessories(scene: Scene, root: TransformNode, accessories: EquippedAccessory[] | undefined | null): Promise<void>;
15
+ export declare function attachAccessoriesFromAvatarConfig(scene: Scene, root: TransformNode, customConfig: Record<string, unknown> | undefined): Promise<void>;
16
+ //# sourceMappingURL=avatar-accessories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar-accessories.d.ts","sourceRoot":"","sources":["../src/avatar-accessories.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,EAGL,aAAa,EAEd,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAmLD;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,iBAAiB,EAAE,GAAG,SAAS,GAAG,IAAI,iBA8CpD;AAED,wBAAsB,iCAAiC,CACrD,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,iBAGlD"}
@@ -0,0 +1,214 @@
1
+ import { Color3, Material, MeshBuilder, SceneLoader, StandardMaterial, TransformNode, Vector3 as BVector3, } from '@babylonjs/core';
2
+ function solid(scene, name, color) {
3
+ const mat = new StandardMaterial(name, scene);
4
+ mat.diffuseColor = Color3.FromHexString(color);
5
+ mat.specularColor = Color3.Black();
6
+ return mat;
7
+ }
8
+ function getRig(root) {
9
+ return root.metadata?.rig ?? null;
10
+ }
11
+ function resolveAttachParent(rig, slot, attachBone) {
12
+ if (attachBone && rig.humanoid?.joints) {
13
+ const joint = rig.humanoid.joints[attachBone];
14
+ if (joint?.node)
15
+ return joint.node;
16
+ }
17
+ const humanoid = rig.humanoid;
18
+ switch (slot) {
19
+ case 'hat':
20
+ case 'face':
21
+ return humanoid?.joints.head?.node ?? rig.head;
22
+ case 'back':
23
+ return humanoid?.joints.chest?.node ?? rig.torso;
24
+ case 'hand':
25
+ return humanoid?.joints.handR?.node ?? rig.armR;
26
+ default:
27
+ return rig.head;
28
+ }
29
+ }
30
+ function localOffset(slot) {
31
+ switch (slot) {
32
+ case 'hat':
33
+ return new BVector3(0, 0.18, 0);
34
+ case 'face':
35
+ return new BVector3(0, 0.02, 0.12);
36
+ case 'back':
37
+ return new BVector3(0, 0.05, -0.18);
38
+ case 'hand':
39
+ return new BVector3(0, -0.35, 0);
40
+ }
41
+ }
42
+ function buildPrimitive(scene, name, primitive, parent, slot) {
43
+ const root = new TransformNode(`${name}-acc`, scene);
44
+ root.parent = parent;
45
+ root.position = localOffset(slot);
46
+ if (primitive === 'cap') {
47
+ const mat = solid(scene, `${name}-cap-mat`, '#2563eb');
48
+ const brim = MeshBuilder.CreateCylinder(`${name}-brim`, { diameter: 0.42, height: 0.04, tessellation: 20 }, scene);
49
+ brim.parent = root;
50
+ brim.position.y = 0.02;
51
+ brim.material = mat;
52
+ const dome = MeshBuilder.CreateSphere(`${name}-dome`, { diameter: 0.34, segments: 12 }, scene);
53
+ dome.parent = root;
54
+ dome.position.y = 0.14;
55
+ dome.scaling.y = 0.7;
56
+ dome.material = mat;
57
+ }
58
+ else if (primitive === 'pack') {
59
+ const mat = solid(scene, `${name}-pack-mat`, '#78350f');
60
+ const pack = MeshBuilder.CreateBox(`${name}-pack`, { width: 0.36, height: 0.42, depth: 0.18 }, scene);
61
+ pack.parent = root;
62
+ pack.material = mat;
63
+ }
64
+ else if (primitive === 'glasses') {
65
+ const mat = solid(scene, `${name}-glasses-mat`, '#111827');
66
+ const left = MeshBuilder.CreateTorus(`${name}-gl`, { diameter: 0.12, thickness: 0.015, tessellation: 12 }, scene);
67
+ left.parent = root;
68
+ left.position.set(-0.07, 0, 0.02);
69
+ left.rotation.x = Math.PI / 2;
70
+ left.material = mat;
71
+ const right = left.clone(`${name}-gr`, root);
72
+ right.position.x = 0.07;
73
+ }
74
+ else if (primitive === 'watch') {
75
+ const mat = solid(scene, `${name}-watch-mat`, '#f59e0b');
76
+ const band = MeshBuilder.CreateTorus(`${name}-band`, { diameter: 0.1, thickness: 0.02, tessellation: 12 }, scene);
77
+ band.parent = root;
78
+ band.rotation.z = Math.PI / 2;
79
+ band.material = mat;
80
+ }
81
+ return root;
82
+ }
83
+ async function loadAccessoryGlb(scene, name, glbUrl, parent, slot) {
84
+ const slash = glbUrl.lastIndexOf('/');
85
+ const rootUrl = slash >= 0 ? glbUrl.slice(0, slash + 1) : '';
86
+ const fileName = slash >= 0 ? glbUrl.slice(slash + 1) : glbUrl;
87
+ const result = await SceneLoader.ImportMeshAsync('', rootUrl, fileName, scene);
88
+ const holder = new TransformNode(`${name}-acc`, scene);
89
+ holder.parent = parent;
90
+ holder.position = localOffset(slot);
91
+ const meshes = result.meshes;
92
+ for (const mesh of meshes) {
93
+ if (mesh.name === '__root__' || !mesh.parent || mesh.parent === scene) {
94
+ mesh.parent = holder;
95
+ }
96
+ mesh.receiveShadows = false;
97
+ if (mesh.material) {
98
+ const mat = mesh.material;
99
+ mat.backFaceCulling = false;
100
+ mat.alpha = 1;
101
+ mat.transparencyMode = Material.MATERIAL_OPAQUE;
102
+ mat.alphaMode = 0;
103
+ mat.disableDepthWrite = false;
104
+ mat.forceDepthWrite = false;
105
+ }
106
+ }
107
+ return holder;
108
+ }
109
+ function readAccessories(avatarConfig) {
110
+ const raw = avatarConfig?.accessories;
111
+ if (!Array.isArray(raw))
112
+ return [];
113
+ const out = [];
114
+ for (const entry of raw) {
115
+ if (!entry || typeof entry !== 'object')
116
+ continue;
117
+ const slot = entry.slot;
118
+ const itemId = entry.itemId;
119
+ if (slot !== 'hat' && slot !== 'back' && slot !== 'face' && slot !== 'hand')
120
+ continue;
121
+ if (typeof itemId !== 'string' || !itemId)
122
+ continue;
123
+ const acc = { slot, itemId };
124
+ const glbUrl = entry.glbUrl;
125
+ const primitive = entry.primitive;
126
+ const attachBone = entry.attachBone;
127
+ if (typeof glbUrl === 'string' && glbUrl)
128
+ acc.glbUrl = glbUrl;
129
+ if (primitive === 'cap' ||
130
+ primitive === 'pack' ||
131
+ primitive === 'glasses' ||
132
+ primitive === 'watch') {
133
+ acc.primitive = primitive;
134
+ }
135
+ if (typeof attachBone === 'string' && attachBone)
136
+ acc.attachBone = attachBone;
137
+ out.push(acc);
138
+ }
139
+ return out;
140
+ }
141
+ /**
142
+ * Colour-variant albedo is one GPU texture shared by every avatar wearing it,
143
+ * so a node being disposed must let go of it instead of freeing it.
144
+ */
145
+ function detachSharedTextures(node) {
146
+ for (const mesh of [node, ...node.getChildMeshes(false)]) {
147
+ const materials = [mesh.material, ...(mesh.material?.subMaterials ?? [])];
148
+ for (const material of materials) {
149
+ if (!material)
150
+ continue;
151
+ const mat = material;
152
+ for (const slot of ['albedoTexture', 'diffuseTexture']) {
153
+ const texture = mat[slot];
154
+ if (texture?.metadata?.sharedAvatarAlbedo)
155
+ mat[slot] = null;
156
+ }
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * Attach equipped accessories onto an avatar root produced by AvatarFactory.
162
+ * Safe to call repeatedly — clears previous `metadata.accessoryNodes` first.
163
+ */
164
+ export async function attachAvatarAccessories(scene, root, accessories) {
165
+ const previous = root.metadata?.accessoryNodes ?? [];
166
+ for (const node of previous) {
167
+ try {
168
+ detachSharedTextures(node);
169
+ node.dispose(false, true);
170
+ }
171
+ catch {
172
+ // ignore
173
+ }
174
+ }
175
+ const rig = getRig(root);
176
+ if (!rig || !accessories?.length) {
177
+ root.metadata = { ...(root.metadata ?? {}), accessoryNodes: [] };
178
+ return;
179
+ }
180
+ const nodes = [];
181
+ for (const acc of accessories) {
182
+ const parent = resolveAttachParent(rig, acc.slot, acc.attachBone);
183
+ const name = `${root.name}-${acc.slot}-${acc.itemId}`;
184
+ try {
185
+ if (acc.glbUrl) {
186
+ const absolute = typeof window !== 'undefined' && acc.glbUrl.startsWith('/')
187
+ ? (() => {
188
+ try {
189
+ const ref = document.referrer ? new URL(document.referrer).origin : '';
190
+ if (ref && ref !== window.location.origin)
191
+ return `${ref}${acc.glbUrl}`;
192
+ }
193
+ catch {
194
+ // ignore
195
+ }
196
+ return `${window.location.origin}${acc.glbUrl}`;
197
+ })()
198
+ : acc.glbUrl;
199
+ nodes.push(await loadAccessoryGlb(scene, name, absolute, parent, acc.slot));
200
+ }
201
+ else if (acc.primitive) {
202
+ nodes.push(buildPrimitive(scene, name, acc.primitive, parent, acc.slot));
203
+ }
204
+ }
205
+ catch (err) {
206
+ console.warn('[lobby-sdk] accessory attach failed', acc, err);
207
+ }
208
+ }
209
+ root.metadata = { ...(root.metadata ?? {}), accessoryNodes: nodes };
210
+ }
211
+ export async function attachAccessoriesFromAvatarConfig(scene, root, customConfig) {
212
+ await attachAvatarAccessories(scene, root, readAccessories(customConfig));
213
+ }
214
+ //# sourceMappingURL=avatar-accessories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar-accessories.js","sourceRoot":"","sources":["../src/avatar-accessories.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Avatar GLB asset loading + per-scene container cache + clone/instantiate.
3
+ * Moved verbatim from AvatarFactory (phase-2 isolation). No behavior changes.
4
+ */
5
+ import { Scene, Skeleton, TransformNode } from '@babylonjs/core';
6
+ import type { AbstractMesh, AnimationGroup, AssetContainer } from '@babylonjs/core';
7
+ import '@babylonjs/loaders/glTF';
8
+ import '@babylonjs/core/Meshes/Compression/dracoCompression';
9
+ /** Same file on :3000/uploads vs :3001/uploads must share one GPU/container copy. */
10
+ export declare function assetCacheKey(url: string): string;
11
+ export interface GlbInstance {
12
+ meshes: AbstractMesh[];
13
+ transformNodes: TransformNode[];
14
+ skeletons: Skeleton[];
15
+ animationGroups: AnimationGroup[];
16
+ }
17
+ /** Stable catalog entry for a shared base GLB (tint/accessories apply later on instances). */
18
+ export interface AvatarBaseDef {
19
+ id: string;
20
+ glbUrl: string;
21
+ }
22
+ export declare class AvatarAssetManager {
23
+ /** Load (or cache-hit) + clone one avatar GLB instance into the scene. */
24
+ static instantiateAvatarGlb(scene: Scene, glbUrl: string): Promise<GlbInstance>;
25
+ /** Expose container load for diagnostics/tests — same cache as instantiate. */
26
+ static loadContainer(scene: Scene, glbUrl: string): Promise<AssetContainer>;
27
+ /**
28
+ * Warm the per-scene container cache for an arbitrary GLB URL (no registry required).
29
+ * Non-throwing — used when a remote joins with a base that was not pre-registered.
30
+ */
31
+ static warmGlbUrl(scene: Scene, glbUrl: string): Promise<AssetContainer | null>;
32
+ /** Register / replace a base avatar catalog entry (id → glbUrl). */
33
+ static registerAvatarBase(def: AvatarBaseDef): void;
34
+ static registerAvatarBases(defs: readonly AvatarBaseDef[]): void;
35
+ static getAvatarBase(id: string): AvatarBaseDef | undefined;
36
+ static listAvatarBases(): AvatarBaseDef[];
37
+ /** True when this scene already has a (pending or resolved) container promise for the URL. */
38
+ static isContainerCached(scene: Scene, glbUrl: string): boolean;
39
+ static isAvatarBaseCached(scene: Scene, id: string): boolean;
40
+ /**
41
+ * Warm the shared glbContainerCache for a registered base.
42
+ * Reuses loadGlbContainer — no duplicate parse path. Failures resolve to null (non-throwing).
43
+ */
44
+ static preloadAvatarBase(scene: Scene, id: string): Promise<AssetContainer | null>;
45
+ /**
46
+ * Preload one or more bases. Omitting ids preloads the full registry.
47
+ * Uses Promise.allSettled semantics per id (one failure does not abort others).
48
+ */
49
+ static preloadAvatarBases(scene: Scene, ids?: readonly string[]): Promise<Array<{
50
+ id: string;
51
+ ok: boolean;
52
+ }>>;
53
+ }
54
+ //# sourceMappingURL=avatar-asset-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar-asset-manager.d.ts","sourceRoot":"","sources":["../src/avatar-asset-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,KAAK,EAEL,QAAQ,EACR,aAAa,EACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,yBAAyB,CAAC;AACjC,OAAO,qDAAqD,CAAC;AAK7D,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAED,8FAA8F;AAC9F,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAyHD,qBAAa,kBAAkB;IAC7B,0EAA0E;IAC1E,MAAM,CAAC,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAI/E,+EAA+E;IAC/E,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAI3E;;;OAGG;WACU,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAWrF,oEAAoE;IACpE,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAKnD,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI;IAIhE,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI3D,MAAM,CAAC,eAAe,IAAI,aAAa,EAAE;IAIzC,8FAA8F;IAC9F,MAAM,CAAC,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAM/D,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAM5D;;;OAGG;WACU,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAqBxF;;;OAGG;WACU,kBAAkB,CAC7B,KAAK,EAAE,KAAK,EACZ,GAAG,CAAC,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAU/C"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Avatar GLB asset loading + per-scene container cache + clone/instantiate.
3
+ * Moved verbatim from AvatarFactory (phase-2 isolation). No behavior changes.
4
+ */
5
+ import { SceneLoader, } from '@babylonjs/core';
6
+ import '@babylonjs/loaders/glTF';
7
+ import '@babylonjs/core/Meshes/Compression/dracoCompression';
8
+ import { ensureDracoDecoder } from './draco';
9
+ import { isLobbyPerfDiagEnabled, lobbyPerfMark, lobbyPerfTimeAsync } from './lobby-perf-diag';
10
+ import { yieldToRenderLoop } from './yield-to-render';
11
+ /** Same file on :3000/uploads vs :3001/uploads must share one GPU/container copy. */
12
+ export function assetCacheKey(url) {
13
+ if (!url || url.startsWith('data:'))
14
+ return url;
15
+ try {
16
+ const parsed = new URL(url, 'http://lobby.local');
17
+ return parsed.pathname || url;
18
+ }
19
+ catch {
20
+ return url;
21
+ }
22
+ }
23
+ /** App-level base registry (not per-scene). Container cache remains per-scene. */
24
+ const avatarBaseRegistry = new Map();
25
+ /** One download + parse per avatar GLB per scene; every player clones from it. */
26
+ const glbContainerCache = new WeakMap();
27
+ let glbCloneSerial = 0;
28
+ function getSceneContainerMap(scene) {
29
+ let byUrl = glbContainerCache.get(scene);
30
+ if (!byUrl) {
31
+ byUrl = new Map();
32
+ glbContainerCache.set(scene, byUrl);
33
+ scene.onDisposeObservable.addOnce(() => glbContainerCache.delete(scene));
34
+ }
35
+ return byUrl;
36
+ }
37
+ function loadGlbContainer(scene, glbUrl) {
38
+ const byUrl = getSceneContainerMap(scene);
39
+ const cacheKey = assetCacheKey(glbUrl);
40
+ const cached = byUrl.get(cacheKey);
41
+ if (cached) {
42
+ if (isLobbyPerfDiagEnabled()) {
43
+ return lobbyPerfTimeAsync('glb_container_load', () => cached, 'cache_hit');
44
+ }
45
+ return cached;
46
+ }
47
+ // Draco WASM must be ready before the first compressed avatar parse.
48
+ const pending = (async () => {
49
+ const tDraco = performance.now();
50
+ await ensureDracoDecoder();
51
+ if (isLobbyPerfDiagEnabled()) {
52
+ lobbyPerfMark('draco_ensure', performance.now() - tDraco, glbUrl.slice(-48));
53
+ }
54
+ const tLoad = performance.now();
55
+ const container = await SceneLoader.LoadAssetContainerAsync('', glbUrl, scene, null,
56
+ // A data URI carries no filename for the loader to sniff an extension from.
57
+ glbUrl.startsWith('data:') ? '.glb' : undefined);
58
+ if (isLobbyPerfDiagEnabled()) {
59
+ lobbyPerfMark('glb_container_load', performance.now() - tLoad, `cache_miss:${glbUrl.slice(-64)}`);
60
+ }
61
+ // Template stays in the scene for cloning. Keep it off so it cannot render
62
+ // or steal a skeleton from a named lookup of "Mesh0" / "Armature".
63
+ for (const mesh of container.meshes) {
64
+ mesh.setEnabled(false);
65
+ mesh.isVisible = false;
66
+ }
67
+ return container;
68
+ })();
69
+ // A failed load must not poison later attempts (fallback URLs are retried).
70
+ pending.catch(() => byUrl?.delete(cacheKey));
71
+ byUrl.set(cacheKey, pending);
72
+ return pending;
73
+ }
74
+ /**
75
+ * Clone an avatar from the cached container instead of re-importing the file.
76
+ * Geometry and textures are shared; materials and skeletons are per avatar, so
77
+ * each player keeps their own colour variant and pose.
78
+ *
79
+ * Names MUST be unique. Babylon's instantiate looks up nodes by name; two
80
+ * avatars named "Mesh0"/"Armature" make the second clone steal the first
81
+ * skeleton — local body vanishes when someone else joins.
82
+ */
83
+ async function instantiateGlb(scene, glbUrl) {
84
+ const container = await loadGlbContainer(scene, glbUrl);
85
+ // Container parse/Draco may resume on the main thread; yield so the lobby can paint
86
+ // (remote placeholder) before the deep clone/instantiate spike.
87
+ await yieldToRenderLoop();
88
+ const t0 = performance.now();
89
+ const serial = ++glbCloneSerial;
90
+ const entries = container.instantiateModelsToScene((name) => `${name}__a${serial}`, true, {
91
+ doNotInstantiate: true,
92
+ });
93
+ // Cloning a disabled template copies isEnabled=false. Turn the clone on,
94
+ // and keep the shared template off so it never renders under the plaza.
95
+ for (const mesh of container.meshes) {
96
+ mesh.setEnabled(false);
97
+ mesh.isVisible = false;
98
+ }
99
+ const meshes = [];
100
+ const transformNodes = [];
101
+ for (const rootNode of entries.rootNodes) {
102
+ rootNode.setEnabled(true);
103
+ for (const node of [rootNode, ...rootNode.getDescendants(false)]) {
104
+ node.setEnabled(true);
105
+ const mesh = node;
106
+ if (typeof mesh.getTotalVertices === 'function') {
107
+ mesh.isVisible = true;
108
+ mesh.visibility = 1;
109
+ meshes.push(mesh);
110
+ }
111
+ else {
112
+ transformNodes.push(node);
113
+ }
114
+ }
115
+ }
116
+ if (isLobbyPerfDiagEnabled()) {
117
+ lobbyPerfMark('glb_instantiate', performance.now() - t0, `serial=${serial}`);
118
+ }
119
+ console.info('[lobby-sdk] GLB cloned', { serial, names: meshes.map((m) => m.name) });
120
+ return {
121
+ meshes,
122
+ transformNodes,
123
+ skeletons: entries.skeletons,
124
+ animationGroups: entries.animationGroups,
125
+ };
126
+ }
127
+ export class AvatarAssetManager {
128
+ /** Load (or cache-hit) + clone one avatar GLB instance into the scene. */
129
+ static instantiateAvatarGlb(scene, glbUrl) {
130
+ return instantiateGlb(scene, glbUrl);
131
+ }
132
+ /** Expose container load for diagnostics/tests — same cache as instantiate. */
133
+ static loadContainer(scene, glbUrl) {
134
+ return loadGlbContainer(scene, glbUrl);
135
+ }
136
+ /**
137
+ * Warm the per-scene container cache for an arbitrary GLB URL (no registry required).
138
+ * Non-throwing — used when a remote joins with a base that was not pre-registered.
139
+ */
140
+ static async warmGlbUrl(scene, glbUrl) {
141
+ const url = typeof glbUrl === 'string' ? glbUrl.trim() : '';
142
+ if (!url)
143
+ return null;
144
+ try {
145
+ return await loadGlbContainer(scene, url);
146
+ }
147
+ catch (err) {
148
+ console.warn('[lobby-sdk] warmGlbUrl failed', url.slice(-64), err);
149
+ return null;
150
+ }
151
+ }
152
+ /** Register / replace a base avatar catalog entry (id → glbUrl). */
153
+ static registerAvatarBase(def) {
154
+ if (!def?.id || typeof def.glbUrl !== 'string' || !def.glbUrl.trim())
155
+ return;
156
+ avatarBaseRegistry.set(def.id, { id: def.id, glbUrl: def.glbUrl.trim() });
157
+ }
158
+ static registerAvatarBases(defs) {
159
+ for (const def of defs)
160
+ this.registerAvatarBase(def);
161
+ }
162
+ static getAvatarBase(id) {
163
+ return avatarBaseRegistry.get(id);
164
+ }
165
+ static listAvatarBases() {
166
+ return [...avatarBaseRegistry.values()];
167
+ }
168
+ /** True when this scene already has a (pending or resolved) container promise for the URL. */
169
+ static isContainerCached(scene, glbUrl) {
170
+ const byUrl = glbContainerCache.get(scene);
171
+ if (!byUrl)
172
+ return false;
173
+ return byUrl.has(assetCacheKey(glbUrl));
174
+ }
175
+ static isAvatarBaseCached(scene, id) {
176
+ const base = avatarBaseRegistry.get(id);
177
+ if (!base)
178
+ return false;
179
+ return this.isContainerCached(scene, base.glbUrl);
180
+ }
181
+ /**
182
+ * Warm the shared glbContainerCache for a registered base.
183
+ * Reuses loadGlbContainer — no duplicate parse path. Failures resolve to null (non-throwing).
184
+ */
185
+ static async preloadAvatarBase(scene, id) {
186
+ const base = avatarBaseRegistry.get(id);
187
+ if (!base) {
188
+ console.warn('[lobby-sdk] preloadAvatarBase: unknown base id', id);
189
+ return null;
190
+ }
191
+ try {
192
+ if (isLobbyPerfDiagEnabled()) {
193
+ return await lobbyPerfTimeAsync('glb_preload', () => loadGlbContainer(scene, base.glbUrl), id);
194
+ }
195
+ return await loadGlbContainer(scene, base.glbUrl);
196
+ }
197
+ catch (err) {
198
+ console.warn('[lobby-sdk] preloadAvatarBase failed', id, err);
199
+ return null;
200
+ }
201
+ }
202
+ /**
203
+ * Preload one or more bases. Omitting ids preloads the full registry.
204
+ * Uses Promise.allSettled semantics per id (one failure does not abort others).
205
+ */
206
+ static async preloadAvatarBases(scene, ids) {
207
+ const list = ids?.length ? [...ids] : [...avatarBaseRegistry.keys()];
208
+ const settled = await Promise.all(list.map(async (id) => {
209
+ const container = await this.preloadAvatarBase(scene, id);
210
+ return { id, ok: !!container };
211
+ }));
212
+ return settled;
213
+ }
214
+ }
215
+ //# sourceMappingURL=avatar-asset-manager.js.map