@cavegiant/cave-world-r3f 1.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 (339) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +189 -0
  4. package/dist/core/connector-label/readiness.d.ts +11 -0
  5. package/dist/core/connector-label/readiness.d.ts.map +1 -0
  6. package/dist/core/connector-label/readiness.js +31 -0
  7. package/dist/core/connector-label/readiness.js.map +1 -0
  8. package/dist/core/connector-label/types.d.ts +64 -0
  9. package/dist/core/connector-label/types.d.ts.map +1 -0
  10. package/dist/core/connector-label/types.js +47 -0
  11. package/dist/core/connector-label/types.js.map +1 -0
  12. package/dist/core/context/cave-world-context.d.ts +96 -0
  13. package/dist/core/context/cave-world-context.d.ts.map +1 -0
  14. package/dist/core/context/cave-world-context.js +100 -0
  15. package/dist/core/context/cave-world-context.js.map +1 -0
  16. package/dist/core/errors.d.ts +34 -0
  17. package/dist/core/errors.d.ts.map +1 -0
  18. package/dist/core/errors.js +17 -0
  19. package/dist/core/errors.js.map +1 -0
  20. package/dist/core/events/event-bus.d.ts +122 -0
  21. package/dist/core/events/event-bus.d.ts.map +1 -0
  22. package/dist/core/events/event-bus.js +62 -0
  23. package/dist/core/events/event-bus.js.map +1 -0
  24. package/dist/core/hooks/use-cave-event.d.ts +13 -0
  25. package/dist/core/hooks/use-cave-event.d.ts.map +1 -0
  26. package/dist/core/hooks/use-cave-event.js +12 -0
  27. package/dist/core/hooks/use-cave-event.js.map +1 -0
  28. package/dist/core/i18n/messages.d.ts +82 -0
  29. package/dist/core/i18n/messages.d.ts.map +1 -0
  30. package/dist/core/i18n/messages.js +131 -0
  31. package/dist/core/i18n/messages.js.map +1 -0
  32. package/dist/core/index.d.ts +11 -0
  33. package/dist/core/index.d.ts.map +1 -0
  34. package/dist/core/index.js +47 -0
  35. package/dist/core/index.js.map +1 -0
  36. package/dist/core/logging/logger.d.ts +29 -0
  37. package/dist/core/logging/logger.d.ts.map +1 -0
  38. package/dist/core/logging/logger.js +47 -0
  39. package/dist/core/logging/logger.js.map +1 -0
  40. package/dist/core/registry/component-registry.d.ts +41 -0
  41. package/dist/core/registry/component-registry.d.ts.map +1 -0
  42. package/dist/core/registry/component-registry.js +34 -0
  43. package/dist/core/registry/component-registry.js.map +1 -0
  44. package/dist/core/registry/object-registry.d.ts +38 -0
  45. package/dist/core/registry/object-registry.d.ts.map +1 -0
  46. package/dist/core/registry/object-registry.js +32 -0
  47. package/dist/core/registry/object-registry.js.map +1 -0
  48. package/dist/explore/cave-explore.d.ts +85 -0
  49. package/dist/explore/cave-explore.d.ts.map +1 -0
  50. package/dist/explore/cave-explore.js +204 -0
  51. package/dist/explore/cave-explore.js.map +1 -0
  52. package/dist/explore/cave-orbit-viewer.d.ts +76 -0
  53. package/dist/explore/cave-orbit-viewer.d.ts.map +1 -0
  54. package/dist/explore/cave-orbit-viewer.js +123 -0
  55. package/dist/explore/cave-orbit-viewer.js.map +1 -0
  56. package/dist/explore/characters/character-configs.d.ts +38 -0
  57. package/dist/explore/characters/character-configs.d.ts.map +1 -0
  58. package/dist/explore/characters/character-configs.js +45 -0
  59. package/dist/explore/characters/character-configs.js.map +1 -0
  60. package/dist/explore/characters/character-controls.d.ts +44 -0
  61. package/dist/explore/characters/character-controls.d.ts.map +1 -0
  62. package/dist/explore/characters/character-pose.d.ts +19 -0
  63. package/dist/explore/characters/character-pose.d.ts.map +1 -0
  64. package/dist/explore/characters/character-pose.js +13 -0
  65. package/dist/explore/characters/character-pose.js.map +1 -0
  66. package/dist/explore/characters/local-character-model.d.ts +53 -0
  67. package/dist/explore/characters/local-character-model.d.ts.map +1 -0
  68. package/dist/explore/characters/local-character-model.js +36 -0
  69. package/dist/explore/characters/local-character-model.js.map +1 -0
  70. package/dist/explore/characters/mixamo-style-bone-map.d.ts +9 -0
  71. package/dist/explore/characters/mixamo-style-bone-map.d.ts.map +1 -0
  72. package/dist/explore/characters/mixamo-style-bone-map.js +28 -0
  73. package/dist/explore/characters/mixamo-style-bone-map.js.map +1 -0
  74. package/dist/explore/characters/remote-character-controller.d.ts +38 -0
  75. package/dist/explore/characters/remote-character-controller.d.ts.map +1 -0
  76. package/dist/explore/characters/remote-character-controller.js +135 -0
  77. package/dist/explore/characters/remote-character-controller.js.map +1 -0
  78. package/dist/explore/characters/remote-character.d.ts +19 -0
  79. package/dist/explore/characters/remote-character.d.ts.map +1 -0
  80. package/dist/explore/characters/remote-character.js +63 -0
  81. package/dist/explore/characters/remote-character.js.map +1 -0
  82. package/dist/explore/connected-spaces-renderer.d.ts +16 -0
  83. package/dist/explore/connected-spaces-renderer.d.ts.map +1 -0
  84. package/dist/explore/connected-spaces-renderer.js +103 -0
  85. package/dist/explore/connected-spaces-renderer.js.map +1 -0
  86. package/dist/explore/hooks/use-action-shortcuts.d.ts +16 -0
  87. package/dist/explore/hooks/use-action-shortcuts.d.ts.map +1 -0
  88. package/dist/explore/hooks/use-action-shortcuts.js +23 -0
  89. package/dist/explore/hooks/use-action-shortcuts.js.map +1 -0
  90. package/dist/explore/hooks/use-active-space-notify.d.ts +11 -0
  91. package/dist/explore/hooks/use-active-space-notify.d.ts.map +1 -0
  92. package/dist/explore/hooks/use-active-space-notify.js +17 -0
  93. package/dist/explore/hooks/use-active-space-notify.js.map +1 -0
  94. package/dist/explore/hooks/use-character-sync.d.ts +46 -0
  95. package/dist/explore/hooks/use-character-sync.d.ts.map +1 -0
  96. package/dist/explore/hooks/use-character-sync.js +72 -0
  97. package/dist/explore/hooks/use-character-sync.js.map +1 -0
  98. package/dist/explore/hooks/use-connector-preloader.d.ts +20 -0
  99. package/dist/explore/hooks/use-connector-preloader.d.ts.map +1 -0
  100. package/dist/explore/hooks/use-connector-preloader.js +78 -0
  101. package/dist/explore/hooks/use-connector-preloader.js.map +1 -0
  102. package/dist/explore/hooks/use-connector-transitions.d.ts +31 -0
  103. package/dist/explore/hooks/use-connector-transitions.d.ts.map +1 -0
  104. package/dist/explore/hooks/use-connector-transitions.js +63 -0
  105. package/dist/explore/hooks/use-connector-transitions.js.map +1 -0
  106. package/dist/explore/hooks/use-explore-actions.d.ts +17 -0
  107. package/dist/explore/hooks/use-explore-actions.d.ts.map +1 -0
  108. package/dist/explore/hooks/use-explore-actions.js +134 -0
  109. package/dist/explore/hooks/use-explore-actions.js.map +1 -0
  110. package/dist/explore/hooks/use-load-error-notify.d.ts +11 -0
  111. package/dist/explore/hooks/use-load-error-notify.d.ts.map +1 -0
  112. package/dist/explore/hooks/use-load-error-notify.js +18 -0
  113. package/dist/explore/hooks/use-load-error-notify.js.map +1 -0
  114. package/dist/explore/hooks/use-primary-space-loader.d.ts +24 -0
  115. package/dist/explore/hooks/use-primary-space-loader.d.ts.map +1 -0
  116. package/dist/explore/hooks/use-primary-space-loader.js +29 -0
  117. package/dist/explore/hooks/use-primary-space-loader.js.map +1 -0
  118. package/dist/explore/index.d.ts +37 -0
  119. package/dist/explore/index.d.ts.map +1 -0
  120. package/dist/explore/index.js +107 -0
  121. package/dist/explore/index.js.map +1 -0
  122. package/dist/explore/loaders/cave-loader.d.ts +58 -0
  123. package/dist/explore/loaders/cave-loader.d.ts.map +1 -0
  124. package/dist/explore/loaders/cave-loader.js +79 -0
  125. package/dist/explore/loaders/cave-loader.js.map +1 -0
  126. package/dist/explore/math/space-transforms.d.ts +49 -0
  127. package/dist/explore/math/space-transforms.d.ts.map +1 -0
  128. package/dist/explore/math/space-transforms.js +54 -0
  129. package/dist/explore/math/space-transforms.js.map +1 -0
  130. package/dist/explore/overlays/action-bubble.d.ts +8 -0
  131. package/dist/explore/overlays/action-bubble.d.ts.map +1 -0
  132. package/dist/explore/overlays/action-bubble.js +18 -0
  133. package/dist/explore/overlays/action-bubble.js.map +1 -0
  134. package/dist/explore/overlays/action-panel.d.ts +12 -0
  135. package/dist/explore/overlays/action-panel.d.ts.map +1 -0
  136. package/dist/explore/overlays/action-panel.js +69 -0
  137. package/dist/explore/overlays/action-panel.js.map +1 -0
  138. package/dist/explore/overlays/connector-label-beacon.d.ts +11 -0
  139. package/dist/explore/overlays/connector-label-beacon.d.ts.map +1 -0
  140. package/dist/explore/overlays/connector-label-beacon.js +38 -0
  141. package/dist/explore/overlays/connector-label-beacon.js.map +1 -0
  142. package/dist/explore/overlays/connector-label-tag-shell.d.ts +16 -0
  143. package/dist/explore/overlays/connector-label-tag-shell.d.ts.map +1 -0
  144. package/dist/explore/overlays/connector-label-tag-shell.js +18 -0
  145. package/dist/explore/overlays/connector-label-tag-shell.js.map +1 -0
  146. package/dist/explore/overlays/detail-window.d.ts +9 -0
  147. package/dist/explore/overlays/detail-window.d.ts.map +1 -0
  148. package/dist/explore/overlays/detail-window.js +92 -0
  149. package/dist/explore/overlays/detail-window.js.map +1 -0
  150. package/dist/explore/overlays/explore-helper.d.ts +44 -0
  151. package/dist/explore/overlays/explore-helper.d.ts.map +1 -0
  152. package/dist/explore/overlays/explore-helper.js +57 -0
  153. package/dist/explore/overlays/explore-helper.js.map +1 -0
  154. package/dist/explore/overlays/icons.d.ts +16 -0
  155. package/dist/explore/overlays/icons.d.ts.map +1 -0
  156. package/dist/explore/overlays/icons.js +34 -0
  157. package/dist/explore/overlays/icons.js.map +1 -0
  158. package/dist/explore/overlays/space-load-error-overlay.d.ts +27 -0
  159. package/dist/explore/overlays/space-load-error-overlay.d.ts.map +1 -0
  160. package/dist/explore/overlays/space-load-error-overlay.js +24 -0
  161. package/dist/explore/overlays/space-load-error-overlay.js.map +1 -0
  162. package/dist/explore/services/websocket.service.d.ts +180 -0
  163. package/dist/explore/services/websocket.service.d.ts.map +1 -0
  164. package/dist/explore/services/websocket.service.js +187 -0
  165. package/dist/explore/services/websocket.service.js.map +1 -0
  166. package/dist/explore/simple-character.d.ts +46 -0
  167. package/dist/explore/simple-character.d.ts.map +1 -0
  168. package/dist/explore/simple-character.js +119 -0
  169. package/dist/explore/simple-character.js.map +1 -0
  170. package/dist/explore/stores/action-prompt.store.d.ts +27 -0
  171. package/dist/explore/stores/action-prompt.store.d.ts.map +1 -0
  172. package/dist/explore/stores/action-prompt.store.js +14 -0
  173. package/dist/explore/stores/action-prompt.store.js.map +1 -0
  174. package/dist/explore/stores/cave-explore.store.d.ts +77 -0
  175. package/dist/explore/stores/cave-explore.store.d.ts.map +1 -0
  176. package/dist/explore/stores/cave-explore.store.js +66 -0
  177. package/dist/explore/stores/cave-explore.store.js.map +1 -0
  178. package/dist/explore/stores/explore-context.d.ts +35 -0
  179. package/dist/explore/stores/explore-context.d.ts.map +1 -0
  180. package/dist/explore/stores/explore-context.js +43 -0
  181. package/dist/explore/stores/explore-context.js.map +1 -0
  182. package/dist/explore/styles/cave-world-css.d.ts +10 -0
  183. package/dist/explore/styles/cave-world-css.d.ts.map +1 -0
  184. package/dist/explore/styles/cave-world-css.js +935 -0
  185. package/dist/explore/styles/cave-world-css.js.map +1 -0
  186. package/dist/explore/styles/inject.d.ts +32 -0
  187. package/dist/explore/styles/inject.d.ts.map +1 -0
  188. package/dist/explore/styles/inject.js +40 -0
  189. package/dist/explore/styles/inject.js.map +1 -0
  190. package/dist/explore/utils/schedule-idle.d.ts +12 -0
  191. package/dist/explore/utils/schedule-idle.d.ts.map +1 -0
  192. package/dist/explore/utils/schedule-idle.js +16 -0
  193. package/dist/explore/utils/schedule-idle.js.map +1 -0
  194. package/dist/explore/viewer-shared.d.ts +56 -0
  195. package/dist/explore/viewer-shared.d.ts.map +1 -0
  196. package/dist/index.d.ts +16 -0
  197. package/dist/index.d.ts.map +1 -0
  198. package/dist/index.js +78 -0
  199. package/dist/index.js.map +1 -0
  200. package/dist/space/cave-base-layer.d.ts +42 -0
  201. package/dist/space/cave-base-layer.d.ts.map +1 -0
  202. package/dist/space/cave-base-layer.js +221 -0
  203. package/dist/space/cave-base-layer.js.map +1 -0
  204. package/dist/space/cave-custom-layer.d.ts +12 -0
  205. package/dist/space/cave-custom-layer.d.ts.map +1 -0
  206. package/dist/space/cave-custom-layer.js +17 -0
  207. package/dist/space/cave-custom-layer.js.map +1 -0
  208. package/dist/space/cave-env.d.ts +6 -0
  209. package/dist/space/cave-env.d.ts.map +1 -0
  210. package/dist/space/cave-env.js +23 -0
  211. package/dist/space/cave-env.js.map +1 -0
  212. package/dist/space/cave-space.d.ts +46 -0
  213. package/dist/space/cave-space.d.ts.map +1 -0
  214. package/dist/space/cave-space.js +37 -0
  215. package/dist/space/cave-space.js.map +1 -0
  216. package/dist/space/connector-label-host.d.ts +21 -0
  217. package/dist/space/connector-label-host.d.ts.map +1 -0
  218. package/dist/space/connector-label-host.js +65 -0
  219. package/dist/space/connector-label-host.js.map +1 -0
  220. package/dist/space/index.d.ts +18 -0
  221. package/dist/space/index.d.ts.map +1 -0
  222. package/dist/space/index.js +47 -0
  223. package/dist/space/index.js.map +1 -0
  224. package/dist/space/objects/builtin-registrar.d.ts +17 -0
  225. package/dist/space/objects/builtin-registrar.d.ts.map +1 -0
  226. package/dist/space/objects/builtin-registrar.js +19 -0
  227. package/dist/space/objects/builtin-registrar.js.map +1 -0
  228. package/dist/space/objects/custom-object.d.ts +9 -0
  229. package/dist/space/objects/custom-object.d.ts.map +1 -0
  230. package/dist/space/objects/custom-object.js +56 -0
  231. package/dist/space/objects/custom-object.js.map +1 -0
  232. package/dist/space/objects/image-object.d.ts +7 -0
  233. package/dist/space/objects/image-object.d.ts.map +1 -0
  234. package/dist/space/objects/image-object.js +14 -0
  235. package/dist/space/objects/image-object.js.map +1 -0
  236. package/dist/space/objects/model-object/fbx-object.d.ts +8 -0
  237. package/dist/space/objects/model-object/fbx-object.d.ts.map +1 -0
  238. package/dist/space/objects/model-object/fbx-object.js +26 -0
  239. package/dist/space/objects/model-object/fbx-object.js.map +1 -0
  240. package/dist/space/objects/model-object/gltf-object.d.ts +9 -0
  241. package/dist/space/objects/model-object/gltf-object.d.ts.map +1 -0
  242. package/dist/space/objects/model-object/gltf-object.js +66 -0
  243. package/dist/space/objects/model-object/gltf-object.js.map +1 -0
  244. package/dist/space/objects/model-object/index.d.ts +9 -0
  245. package/dist/space/objects/model-object/index.d.ts.map +1 -0
  246. package/dist/space/objects/model-object/index.js +12 -0
  247. package/dist/space/objects/model-object/index.js.map +1 -0
  248. package/dist/space/objects/model-object/splat-object.d.ts +9 -0
  249. package/dist/space/objects/model-object/splat-object.d.ts.map +1 -0
  250. package/dist/space/objects/model-object/splat-object.js +19 -0
  251. package/dist/space/objects/model-object/splat-object.js.map +1 -0
  252. package/dist/space/objects/object-components/windows/index.d.ts +5 -0
  253. package/dist/space/objects/object-components/windows/index.d.ts.map +1 -0
  254. package/dist/space/objects/object-components/windows/index.js +20 -0
  255. package/dist/space/objects/object-components/windows/index.js.map +1 -0
  256. package/dist/space/objects/placeholder-object.d.ts +7 -0
  257. package/dist/space/objects/placeholder-object.d.ts.map +1 -0
  258. package/dist/space/objects/placeholder-object.js +12 -0
  259. package/dist/space/objects/placeholder-object.js.map +1 -0
  260. package/dist/space/objects/poi-object.d.ts +7 -0
  261. package/dist/space/objects/poi-object.d.ts.map +1 -0
  262. package/dist/space/objects/poi-object.js +38 -0
  263. package/dist/space/objects/poi-object.js.map +1 -0
  264. package/dist/space/objects/portal-object.d.ts +7 -0
  265. package/dist/space/objects/portal-object.d.ts.map +1 -0
  266. package/dist/space/objects/portal-object.js +84 -0
  267. package/dist/space/objects/portal-object.js.map +1 -0
  268. package/dist/space/objects/video-object.d.ts +7 -0
  269. package/dist/space/objects/video-object.d.ts.map +1 -0
  270. package/dist/space/objects/video-object.js +32 -0
  271. package/dist/space/objects/video-object.js.map +1 -0
  272. package/dist/space/polygon-boundary.d.ts +10 -0
  273. package/dist/space/polygon-boundary.d.ts.map +1 -0
  274. package/dist/space/polygon-boundary.js +205 -0
  275. package/dist/space/polygon-boundary.js.map +1 -0
  276. package/dist/space/splats/spark/robust-splat-mesh.d.ts +59 -0
  277. package/dist/space/splats/spark/robust-splat-mesh.d.ts.map +1 -0
  278. package/dist/space/splats/spark/robust-splat-mesh.js +105 -0
  279. package/dist/space/splats/spark/robust-splat-mesh.js.map +1 -0
  280. package/dist/space/splats/spark/splat-mesh.d.ts +5 -0
  281. package/dist/space/splats/spark/splat-mesh.d.ts.map +1 -0
  282. package/dist/space/splats/spark/splat-mesh.js +7 -0
  283. package/dist/space/splats/spark/splat-mesh.js.map +1 -0
  284. package/dist/space/splats/spark/splat-spread-reveal.d.ts +14 -0
  285. package/dist/space/splats/spark/splat-spread-reveal.d.ts.map +1 -0
  286. package/dist/space/splats/spark/splat-spread-reveal.js +45 -0
  287. package/dist/space/splats/spark/splat-spread-reveal.js.map +1 -0
  288. package/dist/space/utils/cave-world-player.d.ts +3 -0
  289. package/dist/space/utils/cave-world-player.d.ts.map +1 -0
  290. package/dist/space/utils/cave-world-player.js +5 -0
  291. package/dist/space/utils/cave-world-player.js.map +1 -0
  292. package/dist/space/utils/configure-canvas.d.ts +20 -0
  293. package/dist/space/utils/configure-canvas.d.ts.map +1 -0
  294. package/dist/space/utils/configure-canvas.js +12 -0
  295. package/dist/space/utils/configure-canvas.js.map +1 -0
  296. package/dist/space/utils/polygon-boundary-geometry.d.ts +8 -0
  297. package/dist/space/utils/polygon-boundary-geometry.d.ts.map +1 -0
  298. package/dist/space/utils/polygon-boundary-geometry.js +20 -0
  299. package/dist/space/utils/polygon-boundary-geometry.js.map +1 -0
  300. package/dist/space/utils/polygon-boundary-segments.d.ts +50 -0
  301. package/dist/space/utils/polygon-boundary-segments.d.ts.map +1 -0
  302. package/dist/space/utils/polygon-boundary-segments.js +64 -0
  303. package/dist/space/utils/polygon-boundary-segments.js.map +1 -0
  304. package/dist/space/utils/rotation-compat.d.ts +4 -0
  305. package/dist/space/utils/rotation-compat.d.ts.map +1 -0
  306. package/dist/space/utils/rotation-compat.js +9 -0
  307. package/dist/space/utils/rotation-compat.js.map +1 -0
  308. package/dist/space/utils/select-splat-level.d.ts +52 -0
  309. package/dist/space/utils/select-splat-level.d.ts.map +1 -0
  310. package/dist/space/utils/select-splat-level.js +43 -0
  311. package/dist/space/utils/select-splat-level.js.map +1 -0
  312. package/dist/space/utils/spark-performance.d.ts +10 -0
  313. package/dist/space/utils/spark-performance.d.ts.map +1 -0
  314. package/dist/space/utils/spark-performance.js +10 -0
  315. package/dist/space/utils/spark-performance.js.map +1 -0
  316. package/dist/space/utils/splat-collider-helper.d.ts +5 -0
  317. package/dist/space/utils/splat-collider-helper.d.ts.map +1 -0
  318. package/dist/space/utils/splat-collider-helper.js +162 -0
  319. package/dist/space/utils/splat-collider-helper.js.map +1 -0
  320. package/dist/space/utils/touch-primary-device.d.ts +16 -0
  321. package/dist/space/utils/touch-primary-device.d.ts.map +1 -0
  322. package/dist/space/utils/touch-primary-device.js +52 -0
  323. package/dist/space/utils/touch-primary-device.js.map +1 -0
  324. package/dist/space/utils/use-visibility-frameloop.d.ts +20 -0
  325. package/dist/space/utils/use-visibility-frameloop.d.ts.map +1 -0
  326. package/dist/space/utils/use-visibility-frameloop.js +18 -0
  327. package/dist/space/utils/use-visibility-frameloop.js.map +1 -0
  328. package/dist/styles.css +930 -0
  329. package/dist/types/index.d.ts +2 -0
  330. package/dist/types/index.d.ts.map +1 -0
  331. package/dist/types/index.js +2 -0
  332. package/dist/types/index.js.map +1 -0
  333. package/dist/types/space-types.d.ts +404 -0
  334. package/dist/types/space-types.d.ts.map +1 -0
  335. package/doc/API.md +617 -0
  336. package/doc/ARCHITECTURE.md +264 -0
  337. package/doc/multiplayer-protocol.md +757 -0
  338. package/doc/scene-json-structure.md +567 -0
  339. package/package.json +131 -0
@@ -0,0 +1,46 @@
1
+ import type { Quaternion as QuaternionType, Vector3 as Vector3Type } from '../../types/index.js';
2
+ import type { AnimationName } from '../characters/character-configs.js';
3
+ import { type PlayerData, type WebSocketStatus } from '../services/websocket.service.js';
4
+ export interface CharacterSyncConfig {
5
+ websocketUrl: string;
6
+ appId: string;
7
+ localAvatarId: string;
8
+ onPlayerUpdate?: (data: PlayerData) => void;
9
+ onPlayerJoin?: (data: PlayerData) => void;
10
+ onPlayerLeave?: (playerId: string) => void;
11
+ /** `localPlayerId` is `undefined` until the server confirms the join. */
12
+ onRoomSync?: (players: PlayerData[], localPlayerId?: string) => void;
13
+ onJoinSuccess?: (data: {
14
+ roomId: string;
15
+ playerId: string;
16
+ players: PlayerData[];
17
+ isRejoin: boolean;
18
+ }) => void;
19
+ onConnectionStatusChange?: (status: WebSocketStatus) => void;
20
+ }
21
+ export interface CharacterSyncResult {
22
+ status: WebSocketStatus;
23
+ isConnected: boolean;
24
+ /** Player id assigned by the server; `null` until `JOIN_SUCCESS`. */
25
+ localUserId: string | null;
26
+ connect: () => Promise<void>;
27
+ disconnect: () => void;
28
+ joinGame: (avatarId: string) => boolean;
29
+ updateState: (data: {
30
+ position?: Vector3Type;
31
+ rotation?: QuaternionType;
32
+ animation?: AnimationName;
33
+ avatarId?: string;
34
+ }) => boolean;
35
+ syncState: () => boolean;
36
+ }
37
+ /**
38
+ * Own a realtime connection for the lifetime of the mounting component.
39
+ *
40
+ * The connection is per-instance (it used to be a module-level singleton, which
41
+ * leaked across viewers and got torn down by whichever instance unmounted
42
+ * first). Callbacks are read through a ref so changing them never reconnects.
43
+ */
44
+ export declare function useCharacterSync(config: CharacterSyncConfig): CharacterSyncResult;
45
+ export type { PlayerData, WebSocketStatus };
46
+ //# sourceMappingURL=use-character-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-character-sync.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-character-sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AACpE,OAAO,EAIL,KAAK,UAAU,EAEf,KAAK,eAAe,EACrB,MAAM,+BAA+B,CAAA;AAItC,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IAC3C,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IACzC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,yEAAyE;IACzE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,aAAa,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACpE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QACrB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,UAAU,EAAE,CAAA;QACrB,QAAQ,EAAE,OAAO,CAAA;KAClB,KAAK,IAAI,CAAA;IACV,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;CAC7D;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,eAAe,CAAA;IACvB,WAAW,EAAE,OAAO,CAAA;IACpB,qEAAqE;IACrE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAA;IACvC,WAAW,EAAE,CAAC,IAAI,EAAE;QAClB,QAAQ,CAAC,EAAE,WAAW,CAAA;QACtB,QAAQ,CAAC,EAAE,cAAc,CAAA;QACzB,SAAS,CAAC,EAAE,aAAa,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,KAAK,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,OAAO,CAAA;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,mBAAmB,CAiHjF;AAED,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA"}
@@ -0,0 +1,72 @@
1
+ import { useRef as C, useEffect as s, useMemo as L, useState as w, useCallback as c } from "react";
2
+ import { createLogger as R } from "../../core/logging/logger.js";
3
+ import { WebSocketService as h, MessageActionEnum as o, WebSocketStatusEnum as m } from "../services/websocket.service.js";
4
+ const v = R("multiplayer");
5
+ function M(d) {
6
+ const { websocketUrl: i, appId: S } = d, u = C(d);
7
+ s(() => {
8
+ u.current = d;
9
+ });
10
+ const e = L(() => new h({ url: i }), [i]), [l, T] = w(() => e.getStatus()), [b, U] = w(), p = b?.service === e ? b.playerId : null, r = C(null);
11
+ s(() => (e.connect().catch((n) => {
12
+ v.warn("Connection failed:", n);
13
+ }), () => {
14
+ e.destroy();
15
+ }), [e]), s(() => {
16
+ r.current = null;
17
+ const n = e.addStatusListener((t) => {
18
+ T(t), u.current.onConnectionStatusChange?.(t);
19
+ }), A = e.addMessageListener((t) => {
20
+ const a = u.current;
21
+ switch (t.action) {
22
+ case o.STATE_UPDATE:
23
+ t.data.playerId !== r.current && a.onPlayerUpdate?.(t.data);
24
+ break;
25
+ case o.PLAYER_JOINED:
26
+ t.data.playerId !== r.current && a.onPlayerJoin?.(t.data);
27
+ break;
28
+ case o.PLAYER_LEFT:
29
+ a.onPlayerLeave?.(t.data.playerId);
30
+ break;
31
+ case o.STATE_SYNC:
32
+ a.onRoomSync?.(t.data.players, r.current ?? void 0);
33
+ break;
34
+ case o.JOIN_SUCCESS:
35
+ r.current = t.data.playerId, U({ service: e, playerId: t.data.playerId }), a.onJoinSuccess?.(t.data);
36
+ break;
37
+ }
38
+ }), P = e.addErrorListener((t) => {
39
+ v.warn("Socket error:", t);
40
+ });
41
+ return () => {
42
+ n(), A(), P();
43
+ };
44
+ }, [e]), s(() => {
45
+ const n = () => e.disconnect();
46
+ return window.addEventListener("beforeunload", n), () => window.removeEventListener("beforeunload", n);
47
+ }, [e]);
48
+ const y = c(() => e.connect(), [e]), E = c(() => e.disconnect(), [e]), f = c(
49
+ (n) => e.joinGame(S, n),
50
+ [e, S]
51
+ ), k = c(
52
+ (n) => e.updateState(n),
53
+ [e]
54
+ ), I = c(() => e.syncState(), [e]);
55
+ return L(
56
+ () => ({
57
+ status: l,
58
+ isConnected: l === m.CONNECTED,
59
+ localUserId: p,
60
+ connect: y,
61
+ disconnect: E,
62
+ joinGame: f,
63
+ updateState: k,
64
+ syncState: I
65
+ }),
66
+ [l, p, y, E, f, k, I]
67
+ );
68
+ }
69
+ export {
70
+ M as useCharacterSync
71
+ };
72
+ //# sourceMappingURL=use-character-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-character-sync.js","sources":["../../../explore/hooks/use-character-sync.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createLogger } from '../../core'\nimport type { Quaternion as QuaternionType, Vector3 as Vector3Type } from '../../types'\nimport type { AnimationName } from '../characters/character-configs'\nimport {\n MessageActionEnum,\n WebSocketService,\n WebSocketStatusEnum,\n type PlayerData,\n type WebSocketMessage,\n type WebSocketStatus\n} from '../services/websocket.service'\n\nconst logger = createLogger('multiplayer')\n\nexport interface CharacterSyncConfig {\n websocketUrl: string\n appId: string\n localAvatarId: string\n onPlayerUpdate?: (data: PlayerData) => void\n onPlayerJoin?: (data: PlayerData) => void\n onPlayerLeave?: (playerId: string) => void\n /** `localPlayerId` is `undefined` until the server confirms the join. */\n onRoomSync?: (players: PlayerData[], localPlayerId?: string) => void\n onJoinSuccess?: (data: {\n roomId: string\n playerId: string\n players: PlayerData[]\n isRejoin: boolean\n }) => void\n onConnectionStatusChange?: (status: WebSocketStatus) => void\n}\n\nexport interface CharacterSyncResult {\n status: WebSocketStatus\n isConnected: boolean\n /** Player id assigned by the server; `null` until `JOIN_SUCCESS`. */\n localUserId: string | null\n connect: () => Promise<void>\n disconnect: () => void\n joinGame: (avatarId: string) => boolean\n updateState: (data: {\n position?: Vector3Type\n rotation?: QuaternionType\n animation?: AnimationName\n avatarId?: string\n }) => boolean\n syncState: () => boolean\n}\n\n/**\n * Own a realtime connection for the lifetime of the mounting component.\n *\n * The connection is per-instance (it used to be a module-level singleton, which\n * leaked across viewers and got torn down by whichever instance unmounted\n * first). Callbacks are read through a ref so changing them never reconnects.\n */\nexport function useCharacterSync(config: CharacterSyncConfig): CharacterSyncResult {\n const { websocketUrl, appId } = config\n\n const callbacksRef = useRef(config)\n useEffect(() => {\n callbacksRef.current = config\n })\n\n const service = useMemo(() => new WebSocketService({ url: websocketUrl }), [websocketUrl])\n\n const [status, setStatus] = useState<WebSocketStatus>(() => service.getStatus())\n\n // Tag the assigned player id with the service it came from, so a reconnect to\n // a different URL reports \"not joined\" immediately — without a reset effect.\n const [joined, setJoined] = useState<{ service: WebSocketService; playerId: string }>()\n const localUserId = joined?.service === service ? joined.playerId : null\n\n // Mirror of the id for use inside socket callbacks, which must not close over\n // a render-time value. Written only from the JOIN_SUCCESS handler below.\n const localUserIdRef = useRef<string | null>(null)\n\n // Connect on mount / URL change, and always tear the socket down with it.\n useEffect(() => {\n service.connect().catch((error: unknown) => {\n logger.warn('Connection failed:', error)\n })\n\n return () => {\n service.destroy()\n }\n }, [service])\n\n useEffect(() => {\n localUserIdRef.current = null\n\n const unsubscribeStatus = service.addStatusListener((next) => {\n setStatus(next)\n callbacksRef.current.onConnectionStatusChange?.(next)\n })\n\n const unsubscribeMessage = service.addMessageListener((message: WebSocketMessage) => {\n const callbacks = callbacksRef.current\n switch (message.action) {\n case MessageActionEnum.STATE_UPDATE:\n if (message.data.playerId !== localUserIdRef.current) {\n callbacks.onPlayerUpdate?.(message.data)\n }\n break\n case MessageActionEnum.PLAYER_JOINED:\n if (message.data.playerId !== localUserIdRef.current) {\n callbacks.onPlayerJoin?.(message.data)\n }\n break\n case MessageActionEnum.PLAYER_LEFT:\n callbacks.onPlayerLeave?.(message.data.playerId)\n break\n case MessageActionEnum.STATE_SYNC:\n callbacks.onRoomSync?.(message.data.players, localUserIdRef.current ?? undefined)\n break\n case MessageActionEnum.JOIN_SUCCESS:\n localUserIdRef.current = message.data.playerId\n setJoined({ service, playerId: message.data.playerId })\n callbacks.onJoinSuccess?.(message.data)\n break\n default:\n break\n }\n })\n\n const unsubscribeError = service.addErrorListener((error) => {\n logger.warn('Socket error:', error)\n })\n\n return () => {\n unsubscribeStatus()\n unsubscribeMessage()\n unsubscribeError()\n }\n }, [service])\n\n // Leave cleanly when the tab closes so the server drops us immediately\n // instead of waiting for a heartbeat timeout.\n useEffect(() => {\n const handleBeforeUnload = () => service.disconnect()\n window.addEventListener('beforeunload', handleBeforeUnload)\n return () => window.removeEventListener('beforeunload', handleBeforeUnload)\n }, [service])\n\n const connect = useCallback(() => service.connect(), [service])\n const disconnect = useCallback(() => service.disconnect(), [service])\n const joinGame = useCallback(\n (avatarId: string) => service.joinGame(appId, avatarId),\n [service, appId]\n )\n const updateState = useCallback<CharacterSyncResult['updateState']>(\n (data) => service.updateState(data),\n [service]\n )\n const syncState = useCallback(() => service.syncState(), [service])\n\n return useMemo(\n () => ({\n status,\n isConnected: status === WebSocketStatusEnum.CONNECTED,\n localUserId,\n connect,\n disconnect,\n joinGame,\n updateState,\n syncState\n }),\n [status, localUserId, connect, disconnect, joinGame, updateState, syncState]\n )\n}\n\nexport type { PlayerData, WebSocketStatus }\n"],"names":["logger","createLogger","useCharacterSync","config","websocketUrl","appId","callbacksRef","useRef","useEffect","service","useMemo","WebSocketService","status","setStatus","useState","joined","setJoined","localUserId","localUserIdRef","error","unsubscribeStatus","next","unsubscribeMessage","message","callbacks","MessageActionEnum","unsubscribeError","handleBeforeUnload","connect","useCallback","disconnect","joinGame","avatarId","updateState","data","syncState","WebSocketStatusEnum"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { CaveSpace } from '../../types/index.js';
2
+ import type { CaveSpaceLoader } from '../loaders/cave-loader.js';
3
+ /** Warm the loader cache for a space's heavy collider assets. */
4
+ export declare function preloadSpaceAssets(space: CaveSpace): void;
5
+ /**
6
+ * Preload every space referenced by the active space's connectors, and keep
7
+ * seamless neighbors' world-space group matrices up to date.
8
+ *
9
+ * Neighbor downloads are deferred until `primaryInitialized` (main splat +
10
+ * colliders ready) so they do not compete with the primary scene's assets on
11
+ * the same origin, and each neighbor's mount is scheduled in idle time so the
12
+ * burst doesn't stutter player input.
13
+ *
14
+ * The primary space is treated uniformly — it is registered in
15
+ * `connectedSpaces` with its own (initially identity) transform entry, which is
16
+ * what makes seamless back-traversal (A → B → A) work through the same path as
17
+ * forward traversal.
18
+ */
19
+ export declare function useConnectorPreloader(loader: CaveSpaceLoader, primarySpaceId: string | undefined, primaryInitialized: boolean): void;
20
+ //# sourceMappingURL=use-connector-preloader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-connector-preloader.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-connector-preloader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,aAAa,CAAA;AAQtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAa7D,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAMzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,kBAAkB,EAAE,OAAO,GAC1B,IAAI,CAoGN"}
@@ -0,0 +1,78 @@
1
+ import { useEffect as I, startTransition as C } from "react";
2
+ import { useLoader as L } from "@react-three/fiber";
3
+ import { Matrix4 as M } from "three";
4
+ import { OBJLoader as E } from "three/examples/jsm/loaders/OBJLoader.js";
5
+ import { createLogger as A } from "../../core/logging/logger.js";
6
+ import { getConnectorTargetIds as v, matrixFromM16 as b, computeSeamlessGroupMatrix as h } from "../math/space-transforms.js";
7
+ import { useCaveExploreStore as w, useCaveExplore as g } from "../stores/explore-context.js";
8
+ import { scheduleIdle as y } from "../utils/schedule-idle.js";
9
+ const N = A("explore:preload"), R = 1, O = /* @__PURE__ */ new M();
10
+ function _(i) {
11
+ for (const c of i.baseLayer?.colliders ?? [])
12
+ c.type === "model" && c.url && L.preload(E, c.url);
13
+ }
14
+ function J(i, c, T) {
15
+ const r = w(), m = g((t) => t.connectedSpaces), x = g((t) => t.spaceTransforms), n = g((t) => t.activeSpaceId), d = n ? m[n] : void 0;
16
+ I(() => {
17
+ if (!n || !T) return;
18
+ const t = r.getState().connectedSpaces[n];
19
+ if (!t) return;
20
+ const p = v(t);
21
+ let s = !1;
22
+ const o = new Set(p);
23
+ if (o.add(n), c) {
24
+ o.add(c);
25
+ const e = r.getState().connectedSpaces[c];
26
+ if (e)
27
+ for (const f of v(e)) o.add(f);
28
+ }
29
+ r.getState().pruneConnectedSpaces(o);
30
+ const l = p.filter((e) => !r.getState().connectedSpaces[e]), a = /* @__PURE__ */ new Set(), u = () => {
31
+ if (s) return;
32
+ const e = l.shift();
33
+ if (e) {
34
+ if (r.getState().connectedSpaces[e]) {
35
+ u();
36
+ return;
37
+ }
38
+ i(e).then((f) => {
39
+ if (s) return;
40
+ let S = () => {
41
+ };
42
+ S = y(() => {
43
+ a.delete(S), !s && (_(f), C(() => r.getState().setConnectedSpace(e, f)), u());
44
+ }), a.add(S);
45
+ }).catch((f) => {
46
+ N.warn(`Failed to preload connected space ${e}:`, f), s || u();
47
+ });
48
+ }
49
+ };
50
+ for (let e = 0; e < R; e++) u();
51
+ return () => {
52
+ s = !0;
53
+ for (const e of a) e();
54
+ a.clear();
55
+ };
56
+ }, [n, c, T, i, r]), I(() => {
57
+ if (!d || !n) return;
58
+ const t = b(x[n]) ?? O, p = d.baseLayer.transform;
59
+ for (const s of d.baseLayer?.connectors ?? [])
60
+ for (const o of s.connectorInfo.to) {
61
+ if (o.transitionType !== "seamless" || !o.transform) continue;
62
+ const l = m[o.caveWorldId];
63
+ if (!l) continue;
64
+ const a = h(
65
+ t,
66
+ p,
67
+ o.transform,
68
+ l.baseLayer.transform
69
+ );
70
+ r.getState().setSpaceTransform(o.caveWorldId, a.toArray());
71
+ }
72
+ }, [n, d, m, x, r]);
73
+ }
74
+ export {
75
+ _ as preloadSpaceAssets,
76
+ J as useConnectorPreloader
77
+ };
78
+ //# sourceMappingURL=use-connector-preloader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-connector-preloader.js","sources":["../../../explore/hooks/use-connector-preloader.ts"],"sourcesContent":["import { startTransition, useEffect } from 'react'\nimport { useLoader } from '@react-three/fiber'\nimport { Matrix4 } from 'three'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { createLogger } from '../../core'\nimport type { CaveSpace, Matrix16 } from '../../types'\nimport {\n computeSeamlessGroupMatrix,\n getConnectorTargetIds,\n matrixFromM16\n} from '../math/space-transforms'\nimport { useCaveExplore, useCaveExploreStore } from '../stores/explore-context'\nimport { scheduleIdle } from '../utils/schedule-idle'\nimport type { CaveSpaceLoader } from '../loaders/cave-loader'\n\nconst logger = createLogger('explore:preload')\n\n/**\n * Max neighbor scenes whose heavy mount runs concurrently. Loading 2-4 at once\n * bursts JSON + splat decode/upload + OBJ parse onto the main thread; a small\n * limit serializes them so each lands in its own idle slot.\n */\nconst MAX_CONCURRENT_PRELOADS = 1\n\nconst IDENTITY_MATRIX4 = /* @__PURE__ */ new Matrix4()\n\n/** Warm the loader cache for a space's heavy collider assets. */\nexport function preloadSpaceAssets(space: CaveSpace): void {\n for (const collider of space.baseLayer?.colliders ?? []) {\n if (collider.type === 'model' && collider.url) {\n useLoader.preload(OBJLoader, collider.url)\n }\n }\n}\n\n/**\n * Preload every space referenced by the active space's connectors, and keep\n * seamless neighbors' world-space group matrices up to date.\n *\n * Neighbor downloads are deferred until `primaryInitialized` (main splat +\n * colliders ready) so they do not compete with the primary scene's assets on\n * the same origin, and each neighbor's mount is scheduled in idle time so the\n * burst doesn't stutter player input.\n *\n * The primary space is treated uniformly — it is registered in\n * `connectedSpaces` with its own (initially identity) transform entry, which is\n * what makes seamless back-traversal (A → B → A) work through the same path as\n * forward traversal.\n */\nexport function useConnectorPreloader(\n loader: CaveSpaceLoader,\n primarySpaceId: string | undefined,\n primaryInitialized: boolean\n): void {\n const store = useCaveExploreStore()\n const connectedSpaces = useCaveExplore((s) => s.connectedSpaces)\n const spaceTransforms = useCaveExplore((s) => s.spaceTransforms)\n const activeSpaceId = useCaveExplore((s) => s.activeSpaceId)\n\n const activeSpace = activeSpaceId ? connectedSpaces[activeSpaceId] : undefined\n\n // Effect 1: prune unreachable spaces, then queue throttled idle-scheduled\n // preloads for the active space's neighbors.\n useEffect(() => {\n if (!activeSpaceId || !primaryInitialized) return\n const active = store.getState().connectedSpaces[activeSpaceId]\n if (!active) return\n\n const targetIds = getConnectorTargetIds(active)\n let cancelled = false\n\n // Keep: active + active's direct neighbors + primary + primary's direct\n // neighbors. The primary stays loaded so seamless back-traversal always\n // works, and its other targets stay warm for the common A → B → A → C path.\n const keepIds = new Set<string>(targetIds)\n keepIds.add(activeSpaceId)\n if (primarySpaceId) {\n keepIds.add(primarySpaceId)\n const primary = store.getState().connectedSpaces[primarySpaceId]\n if (primary) {\n for (const id of getConnectorTargetIds(primary)) keepIds.add(id)\n }\n }\n store.getState().pruneConnectedSpaces(keepIds)\n\n const queue = targetIds.filter((id) => !store.getState().connectedSpaces[id])\n const pendingIdles = new Set<() => void>()\n\n const processNext = () => {\n if (cancelled) return\n const id = queue.shift()\n if (!id) return\n if (store.getState().connectedSpaces[id]) {\n processNext()\n return\n }\n loader(id)\n .then((space) => {\n if (cancelled) return\n let cancelIdle: () => void = () => {}\n const run = () => {\n pendingIdles.delete(cancelIdle)\n if (cancelled) return\n preloadSpaceAssets(space)\n startTransition(() => store.getState().setConnectedSpace(id, space))\n processNext()\n }\n cancelIdle = scheduleIdle(run)\n pendingIdles.add(cancelIdle)\n })\n .catch((error: unknown) => {\n logger.warn(`Failed to preload connected space ${id}:`, error)\n if (!cancelled) processNext()\n })\n }\n\n for (let i = 0; i < MAX_CONCURRENT_PRELOADS; i++) processNext()\n\n return () => {\n cancelled = true\n for (const cancel of pendingIdles) cancel()\n pendingIdles.clear()\n }\n // Intentionally keyed on identity only: `connectedSpaces` is read through\n // `store.getState()` so a neighbor landing doesn't restart the queue.\n }, [activeSpaceId, primarySpaceId, primaryInitialized, loader, store])\n\n // Effect 2: recompute seamless group matrices as neighbors become available.\n useEffect(() => {\n if (!activeSpace || !activeSpaceId) return\n\n // G_source: whatever was previously computed for the active space (identity\n // for the primary on first entry; for back-traversal we honour the current\n // value so round-trips stay self-consistent).\n const sourceGroupMatrix = matrixFromM16(spaceTransforms[activeSpaceId]) ?? IDENTITY_MATRIX4\n const sourceTransform = activeSpace.baseLayer.transform\n\n for (const connector of activeSpace.baseLayer?.connectors ?? []) {\n for (const target of connector.connectorInfo.to) {\n if (target.transitionType !== 'seamless' || !target.transform) continue\n const targetSpace = connectedSpaces[target.caveWorldId]\n if (!targetSpace) continue\n\n const groupMatrix = computeSeamlessGroupMatrix(\n sourceGroupMatrix,\n sourceTransform,\n target.transform,\n targetSpace.baseLayer.transform\n )\n store.getState().setSpaceTransform(target.caveWorldId, groupMatrix.toArray() as Matrix16)\n }\n }\n }, [activeSpaceId, activeSpace, connectedSpaces, spaceTransforms, store])\n}\n"],"names":["logger","createLogger","MAX_CONCURRENT_PRELOADS","IDENTITY_MATRIX4","Matrix4","preloadSpaceAssets","space","collider","useLoader","OBJLoader","useConnectorPreloader","loader","primarySpaceId","primaryInitialized","store","useCaveExploreStore","connectedSpaces","useCaveExplore","s","spaceTransforms","activeSpaceId","activeSpace","useEffect","active","targetIds","getConnectorTargetIds","cancelled","keepIds","primary","id","queue","pendingIdles","processNext","cancelIdle","scheduleIdle","startTransition","error","i","cancel","sourceGroupMatrix","matrixFromM16","sourceTransform","connector","target","targetSpace","groupMatrix","computeSeamlessGroupMatrix"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type RefObject } from 'react';
2
+ import type { ExploreSimpleCharacterRoot } from '../simple-character.js';
3
+ /**
4
+ * Window (ms) during which `connector:enter` events are dropped after a
5
+ * seamless transition, to swallow the "ghost" enter fired by the target scene's
6
+ * matching connector.
7
+ *
8
+ * Rationale: after a seamless A → B transition the character stays at the same
9
+ * world position, which (by construction of the alignment matrix) is also
10
+ * inside B's matching back-link connector. Because connector sensors only mount
11
+ * for the active scene, B's sensor is added while the character is already
12
+ * inside it; the very next physics tick flips its state false → true and fires
13
+ * `onIntersectedChanged(true)`, which would otherwise ping-pong the user
14
+ * straight back to A or pop a spurious portal panel.
15
+ *
16
+ * The window is deliberately short: the ghost fires within ~16–32 ms (one
17
+ * physics tick), and a human walk-out-walk-in cycle is far slower than 200 ms,
18
+ * so no legitimate event is dropped. Portal transitions don't need this — the
19
+ * character lands 0.5 m outside the target box.
20
+ */
21
+ export declare const SEAMLESS_GHOST_ENTER_WINDOW_MS = 200;
22
+ /**
23
+ * Wire connector events to scene transitions: seamless auto-switch, and portal
24
+ * destination selection with character teleport.
25
+ *
26
+ * All handlers subscribe once and read mutable inputs (active space, connected
27
+ * spaces, primary id) through store `getState()` / refs, so subscriptions don't
28
+ * churn on every preload-driven `connectedSpaces` update.
29
+ */
30
+ export declare function useConnectorTransitions(primarySpaceId: string | undefined, characterRef: RefObject<ExploreSimpleCharacterRoot | null>): void;
31
+ //# sourceMappingURL=use-connector-transitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-connector-transitions.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-connector-transitions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAKvF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAA;AAQrE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,8BAA8B,MAAM,CAAA;AAEjD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,YAAY,EAAE,SAAS,CAAC,0BAA0B,GAAG,IAAI,CAAC,GACzD,IAAI,CAqGN"}
@@ -0,0 +1,63 @@
1
+ import { useRef as A, useEffect as l, useCallback as g, startTransition as E } from "react";
2
+ import { useCaveEventBus as T, useCaveWorldStoreApi as W } from "../../core/context/cave-world-context.js";
3
+ import { useCaveEvent as i } from "../../core/hooks/use-cave-event.js";
4
+ import { applyCharacterPose as x } from "../characters/character-pose.js";
5
+ import { findConnectorById as w, computePortalSpawnPose as y, matrixFromM16 as P } from "../math/space-transforms.js";
6
+ import { isConnectorInActiveSpace as u } from "../stores/cave-explore.store.js";
7
+ import { useCaveExploreStore as _, useActionPromptStore as B, useCaveExplore as M } from "../stores/explore-context.js";
8
+ const b = 200;
9
+ function q(a, p) {
10
+ const S = T(), f = W(), c = _(), s = B(), m = A(a);
11
+ l(() => {
12
+ m.current = a;
13
+ }, [a]);
14
+ const v = M((t) => t.activeSpaceId);
15
+ l(() => {
16
+ c.getState().setActiveConnector(void 0), s.getState().clearActions();
17
+ }, [v, c, s]);
18
+ const d = g(
19
+ (t, e, r) => {
20
+ const o = c.getState(), n = o.activeSpaceId ?? m.current ?? "";
21
+ r === "seamless" && o.setConnectorEnterSuppressUntil(performance.now() + b), E(() => {
22
+ o.setActiveSpaceId(t), f.getState().setSpaceConfig(e);
23
+ }), s.getState().clearActions(), S.emit("connector:transition", { from: n, to: t, type: r });
24
+ },
25
+ [c, f, s, S]
26
+ );
27
+ i("connector:enter", ({ connector: t }) => {
28
+ const e = c.getState();
29
+ if (performance.now() < e.connectorEnterSuppressUntil || !u(e, t)) return;
30
+ const r = t.connectorInfo.to, o = r.find((n) => n.transitionType === "seamless");
31
+ if (o) {
32
+ const n = e.connectedSpaces[o.caveWorldId];
33
+ if (!n || !e.readySpaceIds[o.caveWorldId]) return;
34
+ d(o.caveWorldId, n, "seamless");
35
+ return;
36
+ }
37
+ r.some((n) => n.transitionType === "portal") && e.setActiveConnector(t);
38
+ }), i("connector:leave", ({ connector: t }) => {
39
+ const e = c.getState();
40
+ u(e, t) && e.setActiveConnector(void 0);
41
+ }), i("connector:portal:select", ({ target: t }) => {
42
+ const e = t.caveWorldId, r = c.getState(), o = r.connectedSpaces[e];
43
+ if (!(!o || !r.readySpaceIds[e])) {
44
+ if (t.connectorId) {
45
+ const n = w(o, t.connectorId);
46
+ if (n && p.current) {
47
+ const { position: C, quaternion: I } = y(
48
+ n,
49
+ o.baseLayer.transform,
50
+ P(r.spaceTransforms[e])
51
+ );
52
+ x(p.current, C, I);
53
+ }
54
+ }
55
+ r.setActiveConnector(void 0), d(e, o, "portal");
56
+ }
57
+ });
58
+ }
59
+ export {
60
+ b as SEAMLESS_GHOST_ENTER_WINDOW_MS,
61
+ q as useConnectorTransitions
62
+ };
63
+ //# sourceMappingURL=use-connector-transitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-connector-transitions.js","sources":["../../../explore/hooks/use-connector-transitions.ts"],"sourcesContent":["import { startTransition, useCallback, useEffect, useRef, type RefObject } from 'react'\nimport { useCaveEvent, useCaveEventBus, useCaveWorldStoreApi } from '../../core'\nimport type { CaveSpace } from '../../types'\nimport { applyCharacterPose } from '../characters/character-pose'\nimport { computePortalSpawnPose, findConnectorById, matrixFromM16 } from '../math/space-transforms'\nimport type { ExploreSimpleCharacterRoot } from '../simple-character'\nimport { isConnectorInActiveSpace } from '../stores/cave-explore.store'\nimport {\n useActionPromptStore,\n useCaveExplore,\n useCaveExploreStore\n} from '../stores/explore-context'\n\n/**\n * Window (ms) during which `connector:enter` events are dropped after a\n * seamless transition, to swallow the \"ghost\" enter fired by the target scene's\n * matching connector.\n *\n * Rationale: after a seamless A → B transition the character stays at the same\n * world position, which (by construction of the alignment matrix) is also\n * inside B's matching back-link connector. Because connector sensors only mount\n * for the active scene, B's sensor is added while the character is already\n * inside it; the very next physics tick flips its state false → true and fires\n * `onIntersectedChanged(true)`, which would otherwise ping-pong the user\n * straight back to A or pop a spurious portal panel.\n *\n * The window is deliberately short: the ghost fires within ~16–32 ms (one\n * physics tick), and a human walk-out-walk-in cycle is far slower than 200 ms,\n * so no legitimate event is dropped. Portal transitions don't need this — the\n * character lands 0.5 m outside the target box.\n */\nexport const SEAMLESS_GHOST_ENTER_WINDOW_MS = 200\n\n/**\n * Wire connector events to scene transitions: seamless auto-switch, and portal\n * destination selection with character teleport.\n *\n * All handlers subscribe once and read mutable inputs (active space, connected\n * spaces, primary id) through store `getState()` / refs, so subscriptions don't\n * churn on every preload-driven `connectedSpaces` update.\n */\nexport function useConnectorTransitions(\n primarySpaceId: string | undefined,\n characterRef: RefObject<ExploreSimpleCharacterRoot | null>\n): void {\n const eventBus = useCaveEventBus()\n const worldStore = useCaveWorldStoreApi()\n const exploreStore = useCaveExploreStore()\n const actionPromptStore = useActionPromptStore()\n\n // Hold the latest primary id in a ref so the event handlers stay stable.\n const primaryIdRef = useRef(primarySpaceId)\n useEffect(() => {\n primaryIdRef.current = primarySpaceId\n }, [primarySpaceId])\n\n // When the active scene changes (seamless / portal), drop any connector prompt\n // and `activeConnector` left over from the previous scene. A portal teleport\n // moves the character while the previous scene is still active for a frame, so\n // the old scene's sensors can leave a stale prompt (showing a previous-scene\n // neighbor's name) that never clears. The new scene's sensors re-fire\n // `connector:enter` for connectors the character actually stands in, so a\n // legitimate prompt re-appears with the correct target-scene name.\n const activeSpaceId = useCaveExplore((s) => s.activeSpaceId)\n useEffect(() => {\n exploreStore.getState().setActiveConnector(undefined)\n actionPromptStore.getState().clearActions()\n }, [activeSpaceId, exploreStore, actionPromptStore])\n\n const switchActiveSpace = useCallback(\n (targetId: string, targetSpace: CaveSpace, type: 'seamless' | 'portal') => {\n const explore = exploreStore.getState()\n const fromId = explore.activeSpaceId ?? primaryIdRef.current ?? ''\n\n if (type === 'seamless') {\n explore.setConnectorEnterSuppressUntil(performance.now() + SEAMLESS_GHOST_ENTER_WINDOW_MS)\n }\n\n // Mark the heavy downstream work (active scene's custom objects mounting,\n // physics body re-wiring) as non-urgent so input handling stays responsive.\n startTransition(() => {\n explore.setActiveSpaceId(targetId)\n worldStore.getState().setSpaceConfig(targetSpace)\n })\n\n actionPromptStore.getState().clearActions()\n eventBus.emit('connector:transition', { from: fromId, to: targetId, type })\n },\n [exploreStore, worldStore, actionPromptStore, eventBus]\n )\n\n useCaveEvent('connector:enter', ({ connector }) => {\n const state = exploreStore.getState()\n\n // Ghost-enter guard. Note: we deliberately don't consume the timestamp here\n // — `useExploreActions` registers its own `connector:enter` handler on the\n // same bus, and consume-on-first-read would let one handler see the ghost\n // while the other sees a clean state, causing a spurious portal panel for\n // connectors with mixed seamless + portal targets.\n if (performance.now() < state.connectorEnterSuppressUntil) return\n // Ignore enters from a previous scene's still-mounted sensors mid-transition.\n if (!isConnectorInActiveSpace(state, connector)) return\n\n const targets = connector.connectorInfo.to\n const seamless = targets.find((t) => t.transitionType === 'seamless')\n\n if (seamless) {\n const targetSpace = state.connectedSpaces[seamless.caveWorldId]\n if (!targetSpace || !state.readySpaceIds[seamless.caveWorldId]) return\n switchActiveSpace(seamless.caveWorldId, targetSpace, 'seamless')\n return\n }\n\n if (targets.some((t) => t.transitionType === 'portal')) {\n state.setActiveConnector(connector)\n }\n })\n\n useCaveEvent('connector:leave', ({ connector }) => {\n const state = exploreStore.getState()\n if (!isConnectorInActiveSpace(state, connector)) return\n state.setActiveConnector(undefined)\n })\n\n useCaveEvent('connector:portal:select', ({ target }) => {\n const targetId = target.caveWorldId\n const state = exploreStore.getState()\n const targetSpace = state.connectedSpaces[targetId]\n if (!targetSpace || !state.readySpaceIds[targetId]) return\n\n if (target.connectorId) {\n const targetConnector = findConnectorById(targetSpace, target.connectorId)\n if (targetConnector && characterRef.current) {\n const { position, quaternion } = computePortalSpawnPose(\n targetConnector,\n targetSpace.baseLayer.transform,\n matrixFromM16(state.spaceTransforms[targetId])\n )\n applyCharacterPose(characterRef.current, position, quaternion)\n }\n }\n\n state.setActiveConnector(undefined)\n switchActiveSpace(targetId, targetSpace, 'portal')\n })\n}\n"],"names":["SEAMLESS_GHOST_ENTER_WINDOW_MS","useConnectorTransitions","primarySpaceId","characterRef","eventBus","useCaveEventBus","worldStore","useCaveWorldStoreApi","exploreStore","useCaveExploreStore","actionPromptStore","useActionPromptStore","primaryIdRef","useRef","useEffect","activeSpaceId","useCaveExplore","s","switchActiveSpace","useCallback","targetId","targetSpace","type","explore","fromId","startTransition","useCaveEvent","connector","state","isConnectorInActiveSpace","targets","seamless","t","target","targetConnector","findConnectorById","position","quaternion","computePortalSpawnPose","matrixFromM16","applyCharacterPose"],"mappings":";;;;;;;AA+BO,MAAMA,IAAiC;AAUvC,SAASC,EACdC,GACAC,GACM;AACN,QAAMC,IAAWC,EAAA,GACXC,IAAaC,EAAA,GACbC,IAAeC,EAAA,GACfC,IAAoBC,EAAA,GAGpBC,IAAeC,EAAOX,CAAc;AAC1C,EAAAY,EAAU,MAAM;AACd,IAAAF,EAAa,UAAUV;AAAA,EACzB,GAAG,CAACA,CAAc,CAAC;AASnB,QAAMa,IAAgBC,EAAe,CAACC,MAAMA,EAAE,aAAa;AAC3D,EAAAH,EAAU,MAAM;AACd,IAAAN,EAAa,SAAA,EAAW,mBAAmB,MAAS,GACpDE,EAAkB,SAAA,EAAW,aAAA;AAAA,EAC/B,GAAG,CAACK,GAAeP,GAAcE,CAAiB,CAAC;AAEnD,QAAMQ,IAAoBC;AAAA,IACxB,CAACC,GAAkBC,GAAwBC,MAAgC;AACzE,YAAMC,IAAUf,EAAa,SAAA,GACvBgB,IAASD,EAAQ,iBAAiBX,EAAa,WAAW;AAEhE,MAAIU,MAAS,cACXC,EAAQ,+BAA+B,YAAY,IAAA,IAAQvB,CAA8B,GAK3FyB,EAAgB,MAAM;AACpB,QAAAF,EAAQ,iBAAiBH,CAAQ,GACjCd,EAAW,SAAA,EAAW,eAAee,CAAW;AAAA,MAClD,CAAC,GAEDX,EAAkB,SAAA,EAAW,aAAA,GAC7BN,EAAS,KAAK,wBAAwB,EAAE,MAAMoB,GAAQ,IAAIJ,GAAU,MAAAE,GAAM;AAAA,IAC5E;AAAA,IACA,CAACd,GAAcF,GAAYI,GAAmBN,CAAQ;AAAA,EAAA;AAGxD,EAAAsB,EAAa,mBAAmB,CAAC,EAAE,WAAAC,QAAgB;AACjD,UAAMC,IAAQpB,EAAa,SAAA;AAS3B,QAFI,YAAY,QAAQoB,EAAM,+BAE1B,CAACC,EAAyBD,GAAOD,CAAS,EAAG;AAEjD,UAAMG,IAAUH,EAAU,cAAc,IAClCI,IAAWD,EAAQ,KAAK,CAACE,MAAMA,EAAE,mBAAmB,UAAU;AAEpE,QAAID,GAAU;AACZ,YAAMV,IAAcO,EAAM,gBAAgBG,EAAS,WAAW;AAC9D,UAAI,CAACV,KAAe,CAACO,EAAM,cAAcG,EAAS,WAAW,EAAG;AAChE,MAAAb,EAAkBa,EAAS,aAAaV,GAAa,UAAU;AAC/D;AAAA,IACF;AAEA,IAAIS,EAAQ,KAAK,CAACE,MAAMA,EAAE,mBAAmB,QAAQ,KACnDJ,EAAM,mBAAmBD,CAAS;AAAA,EAEtC,CAAC,GAEDD,EAAa,mBAAmB,CAAC,EAAE,WAAAC,QAAgB;AACjD,UAAMC,IAAQpB,EAAa,SAAA;AAC3B,IAAKqB,EAAyBD,GAAOD,CAAS,KAC9CC,EAAM,mBAAmB,MAAS;AAAA,EACpC,CAAC,GAEDF,EAAa,2BAA2B,CAAC,EAAE,QAAAO,QAAa;AACtD,UAAMb,IAAWa,EAAO,aAClBL,IAAQpB,EAAa,SAAA,GACrBa,IAAcO,EAAM,gBAAgBR,CAAQ;AAClD,QAAI,GAACC,KAAe,CAACO,EAAM,cAAcR,CAAQ,IAEjD;AAAA,UAAIa,EAAO,aAAa;AACtB,cAAMC,IAAkBC,EAAkBd,GAAaY,EAAO,WAAW;AACzE,YAAIC,KAAmB/B,EAAa,SAAS;AAC3C,gBAAM,EAAE,UAAAiC,GAAU,YAAAC,EAAA,IAAeC;AAAA,YAC/BJ;AAAA,YACAb,EAAY,UAAU;AAAA,YACtBkB,EAAcX,EAAM,gBAAgBR,CAAQ,CAAC;AAAA,UAAA;AAE/C,UAAAoB,EAAmBrC,EAAa,SAASiC,GAAUC,CAAU;AAAA,QAC/D;AAAA,MACF;AAEA,MAAAT,EAAM,mBAAmB,MAAS,GAClCV,EAAkBE,GAAUC,GAAa,QAAQ;AAAA;AAAA,EACnD,CAAC;AACH;"}
@@ -0,0 +1,17 @@
1
+ import type { CustomObject, PortalProps } from '../../types/index.js';
2
+ /**
3
+ * Decides what happens when the player activates a `portal` object.
4
+ *
5
+ * Return `true` if you handled the navigation. The default behavior only opens
6
+ * absolute `http(s)` URLs in a new tab; anything else (in-app routes, deep
7
+ * links) is left to the host, since the SDK cannot know your router.
8
+ */
9
+ export type PortalNavigateHandler = (portal: PortalProps, object: CustomObject) => boolean | void;
10
+ /**
11
+ * Turn object / connector proximity events into action prompts.
12
+ *
13
+ * Everything flows through the event bus and the action-prompt store, so no
14
+ * overlay needs a direct reference to the scene or the character.
15
+ */
16
+ export declare function useExploreActions(onPortalNavigate?: PortalNavigateHandler): void;
17
+ //# sourceMappingURL=use-explore-actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-explore-actions.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-explore-actions.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAIV,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAA;AAWpB;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,GAAG,IAAI,CAAA;AAmDjG;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,gBAAgB,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAsJhF"}
@@ -0,0 +1,134 @@
1
+ import { useCallback as b } from "react";
2
+ import { useCaveEventBus as h, useCaveWorldMessages as y, useCaveWorldStoreApi as P } from "../../core/context/cave-world-context.js";
3
+ import { useCaveEvent as c } from "../../core/hooks/use-cave-event.js";
4
+ import { formatMessage as w } from "../../core/i18n/messages.js";
5
+ import { getConnectorPortalTargets as g, isConnectorPortalNavigable as v, isPortalTargetReady as A } from "../../core/connector-label/readiness.js";
6
+ import { isConnectorInActiveSpace as m } from "../stores/cave-explore.store.js";
7
+ import { useCaveExploreStore as I, useActionPromptStore as E } from "../stores/explore-context.js";
8
+ const T = 1.8, x = 9;
9
+ function B(u) {
10
+ try {
11
+ const n = new URL(u);
12
+ return n.protocol === "http:" || n.protocol === "https:";
13
+ } catch {
14
+ return !1;
15
+ }
16
+ }
17
+ function U(u, n, l, f, s) {
18
+ const r = g(u), p = r.map(
19
+ (o) => A(o, n, l)
20
+ ), t = p.filter(Boolean).length;
21
+ let e = 0;
22
+ return r.map((o, i) => {
23
+ const a = p[i], d = n[o.caveWorldId]?.metadata?.name ?? f.actionPanel.unknownDestination, S = a ? e++ : -1, C = a ? t === 1 ? "F" : S < x ? String(S + 1) : void 0 : void 0;
24
+ return {
25
+ id: `connector-portal-${o.caveWorldId}-${o.connectorId ?? ""}`,
26
+ label: a ? d : w(f.connector.targetLoading, { name: d }),
27
+ shortcut: C,
28
+ disabled: !a,
29
+ onAction: () => {
30
+ a && s(o);
31
+ }
32
+ };
33
+ });
34
+ }
35
+ function H(u) {
36
+ const n = h(), l = y(), f = P(), s = I(), r = E(), p = b(
37
+ (t) => {
38
+ const { connectedSpaces: e, readySpaceIds: o } = s.getState(), { showActions: i } = r.getState();
39
+ if (g(t).length === 0) return;
40
+ if (!v(t, e, o)) {
41
+ i(
42
+ [
43
+ {
44
+ id: "connector-loading",
45
+ label: l.connector.loadingPanelLabel,
46
+ disabled: !0,
47
+ onAction: () => {
48
+ }
49
+ }
50
+ ],
51
+ null,
52
+ "panel"
53
+ );
54
+ return;
55
+ }
56
+ const a = U(
57
+ t,
58
+ e,
59
+ o,
60
+ l,
61
+ (d) => n.emit("connector:portal:select", { connector: t, target: d })
62
+ );
63
+ i(a, null, "panel");
64
+ },
65
+ [s, r, l, n]
66
+ );
67
+ c("object:enter", ({ object: t }) => {
68
+ if (f.getState().detailVisible) return;
69
+ const { showActions: e } = r.getState();
70
+ if (t.type === "portal" && t.portal) {
71
+ const o = t.portal, [i, a, d] = t.transform.position;
72
+ e(
73
+ [
74
+ {
75
+ id: "portal",
76
+ label: l.actions.enterPortal,
77
+ shortcut: "F",
78
+ onAction: () => {
79
+ n.emit("object:interact", { object: t, action: "portal" }), u?.(o, t) !== !0 && B(o.url) && window.open(o.url, "_blank", "noopener,noreferrer");
80
+ }
81
+ }
82
+ ],
83
+ [i, a + T, d],
84
+ "bubble"
85
+ );
86
+ return;
87
+ }
88
+ t.components?.Window && e(
89
+ [
90
+ {
91
+ id: "detail",
92
+ label: l.actions.viewDetail,
93
+ shortcut: "F",
94
+ onAction: () => {
95
+ n.emit("object:interact", { object: t, action: "detail" }), n.emit("detail:open", { object: t });
96
+ }
97
+ }
98
+ ],
99
+ [...t.transform.position],
100
+ "bubble"
101
+ );
102
+ }), c("object:leave", () => {
103
+ r.getState().clearActions();
104
+ }), c("detail:open", () => {
105
+ r.getState().clearActions();
106
+ }), c("connector:transition", () => {
107
+ r.getState().clearActions();
108
+ }), c("connector:enter", ({ connector: t }) => {
109
+ const e = s.getState();
110
+ performance.now() < e.connectorEnterSuppressUntil || m(e, t) && p(t);
111
+ }), c("connector:label:click", ({ connector: t }) => {
112
+ const { connectedSpaces: e, readySpaceIds: o } = s.getState();
113
+ if (!v(t, e, o)) return;
114
+ const i = g(t).filter(
115
+ (a) => A(a, e, o)
116
+ );
117
+ if (i.length === 1) {
118
+ n.emit("connector:portal:select", { connector: t, target: i[0] });
119
+ return;
120
+ }
121
+ p(t);
122
+ }), c("space:transfer:ready", () => {
123
+ const t = s.getState(), { activeConnector: e } = t;
124
+ e && m(t, e) && p(e);
125
+ }), c("connector:leave", ({ connector: t }) => {
126
+ m(s.getState(), t) && r.getState().clearActions();
127
+ }), c("connector:portal:select", () => {
128
+ r.getState().clearActions();
129
+ });
130
+ }
131
+ export {
132
+ H as useExploreActions
133
+ };
134
+ //# sourceMappingURL=use-explore-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-explore-actions.js","sources":["../../../explore/hooks/use-explore-actions.ts"],"sourcesContent":["import { useCallback } from 'react'\nimport {\n formatMessage,\n getConnectorPortalTargets,\n isConnectorPortalNavigable,\n isPortalTargetReady,\n useCaveEvent,\n useCaveEventBus,\n useCaveWorldMessages,\n useCaveWorldStoreApi,\n type CaveWorldMessages\n} from '../../core'\nimport type {\n CaveSpace,\n ConnectorDef,\n ConnectorTarget,\n CustomObject,\n PortalProps\n} from '../../types'\nimport type { ActionDef } from '../stores/action-prompt.store'\nimport { isConnectorInActiveSpace } from '../stores/cave-explore.store'\nimport { useActionPromptStore, useCaveExploreStore } from '../stores/explore-context'\n\n/** Height above an object's origin where the \"press F\" bubble is anchored. */\nconst PORTAL_BUBBLE_HEIGHT_M = 1.8\n\n/** Max destinations that get a number-key shortcut (1–9). */\nconst MAX_NUMERIC_SHORTCUTS = 9\n\n/**\n * Decides what happens when the player activates a `portal` object.\n *\n * Return `true` if you handled the navigation. The default behavior only opens\n * absolute `http(s)` URLs in a new tab; anything else (in-app routes, deep\n * links) is left to the host, since the SDK cannot know your router.\n */\nexport type PortalNavigateHandler = (portal: PortalProps, object: CustomObject) => boolean | void\n\nfunction isHttpUrl(url: string): boolean {\n try {\n const parsed = new URL(url)\n return parsed.protocol === 'http:' || parsed.protocol === 'https:'\n } catch {\n return false\n }\n}\n\nfunction buildConnectorPortalActions(\n connector: ConnectorDef,\n connectedSpaces: Record<string, CaveSpace>,\n readySpaceIds: Record<string, true>,\n messages: CaveWorldMessages,\n onSelect: (target: ConnectorTarget) => void\n): ActionDef[] {\n const portalTargets = getConnectorPortalTargets(connector)\n const readyFlags = portalTargets.map((target) =>\n isPortalTargetReady(target, connectedSpaces, readySpaceIds)\n )\n const readyCount = readyFlags.filter(Boolean).length\n\n let readyIndex = 0\n return portalTargets.map((target, index) => {\n const ready = readyFlags[index]\n const name =\n connectedSpaces[target.caveWorldId]?.metadata?.name ?? messages.actionPanel.unknownDestination\n const ordinal = ready ? readyIndex++ : -1\n\n const shortcut = !ready\n ? undefined\n : readyCount === 1\n ? 'F'\n : ordinal < MAX_NUMERIC_SHORTCUTS\n ? String(ordinal + 1)\n : undefined\n\n return {\n id: `connector-portal-${target.caveWorldId}-${target.connectorId ?? ''}`,\n label: ready ? name : formatMessage(messages.connector.targetLoading, { name }),\n shortcut,\n disabled: !ready,\n onAction: () => {\n if (ready) onSelect(target)\n }\n }\n })\n}\n\n/**\n * Turn object / connector proximity events into action prompts.\n *\n * Everything flows through the event bus and the action-prompt store, so no\n * overlay needs a direct reference to the scene or the character.\n */\nexport function useExploreActions(onPortalNavigate?: PortalNavigateHandler): void {\n const eventBus = useCaveEventBus()\n const messages = useCaveWorldMessages()\n const worldStore = useCaveWorldStoreApi()\n const exploreStore = useCaveExploreStore()\n const actionPromptStore = useActionPromptStore()\n\n const showConnectorPortalPanel = useCallback(\n (connector: ConnectorDef) => {\n const { connectedSpaces, readySpaceIds } = exploreStore.getState()\n const { showActions } = actionPromptStore.getState()\n if (getConnectorPortalTargets(connector).length === 0) return\n\n if (!isConnectorPortalNavigable(connector, connectedSpaces, readySpaceIds)) {\n showActions(\n [\n {\n id: 'connector-loading',\n label: messages.connector.loadingPanelLabel,\n disabled: true,\n onAction: () => {}\n }\n ],\n null,\n 'panel'\n )\n return\n }\n\n const actions = buildConnectorPortalActions(\n connector,\n connectedSpaces,\n readySpaceIds,\n messages,\n (target) => eventBus.emit('connector:portal:select', { connector, target })\n )\n showActions(actions, null, 'panel')\n },\n [exploreStore, actionPromptStore, messages, eventBus]\n )\n\n // ── Object interactions (detail window + portal objects) ──\n\n useCaveEvent('object:enter', ({ object }) => {\n if (worldStore.getState().detailVisible) return\n const { showActions } = actionPromptStore.getState()\n\n if (object.type === 'portal' && object.portal) {\n const portal = object.portal\n const [x, y, z] = object.transform.position\n showActions(\n [\n {\n id: 'portal',\n label: messages.actions.enterPortal,\n shortcut: 'F',\n onAction: () => {\n eventBus.emit('object:interact', { object, action: 'portal' })\n if (onPortalNavigate?.(portal, object) === true) return\n // Default: only absolute web URLs are safe to act on without\n // knowing the host's routing. Everything else stays an event.\n if (isHttpUrl(portal.url)) {\n window.open(portal.url, '_blank', 'noopener,noreferrer')\n }\n }\n }\n ],\n [x, y + PORTAL_BUBBLE_HEIGHT_M, z],\n 'bubble'\n )\n return\n }\n\n if (object.components?.Window) {\n showActions(\n [\n {\n id: 'detail',\n label: messages.actions.viewDetail,\n shortcut: 'F',\n onAction: () => {\n eventBus.emit('object:interact', { object, action: 'detail' })\n eventBus.emit('detail:open', { object })\n }\n }\n ],\n [...object.transform.position],\n 'bubble'\n )\n }\n })\n\n useCaveEvent('object:leave', () => {\n actionPromptStore.getState().clearActions()\n })\n\n useCaveEvent('detail:open', () => {\n actionPromptStore.getState().clearActions()\n })\n\n useCaveEvent('connector:transition', () => {\n actionPromptStore.getState().clearActions()\n })\n\n // ── Connector portal interactions ──\n\n useCaveEvent('connector:enter', ({ connector }) => {\n const state = exploreStore.getState()\n // Same ghost-enter guard as `useConnectorTransitions`: drop events fired\n // immediately after a seamless transition while the character is still\n // inside the new active scene's matching connector box.\n if (performance.now() < state.connectorEnterSuppressUntil) return\n if (!isConnectorInActiveSpace(state, connector)) return\n showConnectorPortalPanel(connector)\n })\n\n useCaveEvent('connector:label:click', ({ connector }) => {\n const { connectedSpaces, readySpaceIds } = exploreStore.getState()\n if (!isConnectorPortalNavigable(connector, connectedSpaces, readySpaceIds)) return\n\n const readyTargets = getConnectorPortalTargets(connector).filter((target) =>\n isPortalTargetReady(target, connectedSpaces, readySpaceIds)\n )\n\n // A single ready destination needs no picker — behave like pressing F.\n if (readyTargets.length === 1) {\n eventBus.emit('connector:portal:select', { connector, target: readyTargets[0] })\n return\n }\n showConnectorPortalPanel(connector)\n })\n\n // A destination finishing its preload should refresh an open picker so the\n // greyed-out row becomes selectable without the player walking out and back in.\n useCaveEvent('space:transfer:ready', () => {\n const state = exploreStore.getState()\n const { activeConnector } = state\n if (!activeConnector) return\n if (!isConnectorInActiveSpace(state, activeConnector)) return\n showConnectorPortalPanel(activeConnector)\n })\n\n useCaveEvent('connector:leave', ({ connector }) => {\n if (!isConnectorInActiveSpace(exploreStore.getState(), connector)) return\n actionPromptStore.getState().clearActions()\n })\n\n useCaveEvent('connector:portal:select', () => {\n actionPromptStore.getState().clearActions()\n })\n}\n"],"names":["PORTAL_BUBBLE_HEIGHT_M","MAX_NUMERIC_SHORTCUTS","isHttpUrl","url","parsed","buildConnectorPortalActions","connector","connectedSpaces","readySpaceIds","messages","onSelect","portalTargets","getConnectorPortalTargets","readyFlags","target","isPortalTargetReady","readyCount","readyIndex","index","ready","name","ordinal","shortcut","formatMessage","useExploreActions","onPortalNavigate","eventBus","useCaveEventBus","useCaveWorldMessages","worldStore","useCaveWorldStoreApi","exploreStore","useCaveExploreStore","actionPromptStore","useActionPromptStore","showConnectorPortalPanel","useCallback","showActions","isConnectorPortalNavigable","actions","useCaveEvent","object","portal","x","y","z","state","isConnectorInActiveSpace","readyTargets","activeConnector"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { type SpaceLoadError } from '../../core/index.js';
2
+ /**
3
+ * Invoke `onLoadError` once each time a terminal space/splat load failure is
4
+ * recorded on the per-instance store (`spaceLoadError` transitions to a value).
5
+ *
6
+ * Uses an imperative store subscription (with a manual diff) so it never
7
+ * re-renders the host component, and reads the latest callback from a ref.
8
+ * Shared by `CaveExplore` and `CaveOrbitViewer`.
9
+ */
10
+ export declare function useLoadErrorNotify(onLoadError?: (error: SpaceLoadError) => void): void;
11
+ //# sourceMappingURL=use-load-error-notify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-load-error-notify.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-load-error-notify.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAE9D;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,QAe/E"}
@@ -0,0 +1,18 @@
1
+ import { useRef as u, useEffect as c } from "react";
2
+ import { useCaveWorld as n } from "../../core/context/cave-world-context.js";
3
+ function p(e) {
4
+ const { store: t } = n(), o = u(e);
5
+ c(() => {
6
+ o.current = e;
7
+ }, [e]), c(() => {
8
+ let s = t.getState().spaceLoadError;
9
+ return t.subscribe((f) => {
10
+ const r = f.spaceLoadError;
11
+ r !== s && (s = r, r && o.current?.(r));
12
+ });
13
+ }, [t]);
14
+ }
15
+ export {
16
+ p as useLoadErrorNotify
17
+ };
18
+ //# sourceMappingURL=use-load-error-notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-load-error-notify.js","sources":["../../../explore/hooks/use-load-error-notify.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { useCaveWorld, type SpaceLoadError } from '../../core'\n\n/**\n * Invoke `onLoadError` once each time a terminal space/splat load failure is\n * recorded on the per-instance store (`spaceLoadError` transitions to a value).\n *\n * Uses an imperative store subscription (with a manual diff) so it never\n * re-renders the host component, and reads the latest callback from a ref.\n * Shared by `CaveExplore` and `CaveOrbitViewer`.\n */\nexport function useLoadErrorNotify(onLoadError?: (error: SpaceLoadError) => void) {\n const { store } = useCaveWorld()\n const onLoadErrorRef = useRef(onLoadError)\n useEffect(() => {\n onLoadErrorRef.current = onLoadError\n }, [onLoadError])\n useEffect(() => {\n let prev = store.getState().spaceLoadError\n return store.subscribe((state) => {\n const error = state.spaceLoadError\n if (error === prev) return\n prev = error\n if (error) onLoadErrorRef.current?.(error)\n })\n }, [store])\n}\n"],"names":["useLoadErrorNotify","onLoadError","store","useCaveWorld","onLoadErrorRef","useRef","useEffect","prev","state","error"],"mappings":";;AAWO,SAASA,EAAmBC,GAA+C;AAChF,QAAM,EAAE,OAAAC,EAAA,IAAUC,EAAA,GACZC,IAAiBC,EAAOJ,CAAW;AACzC,EAAAK,EAAU,MAAM;AACd,IAAAF,EAAe,UAAUH;AAAA,EAC3B,GAAG,CAACA,CAAW,CAAC,GAChBK,EAAU,MAAM;AACd,QAAIC,IAAOL,EAAM,SAAA,EAAW;AAC5B,WAAOA,EAAM,UAAU,CAACM,MAAU;AAChC,YAAMC,IAAQD,EAAM;AACpB,MAAIC,MAAUF,MACdA,IAAOE,GACHA,KAAOL,EAAe,UAAUK,CAAK;AAAA,IAC3C,CAAC;AAAA,EACH,GAAG,CAACP,CAAK,CAAC;AACZ;"}