@vgai/editor 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +35 -0
  3. package/dist/a/b/c/test-nested.json +1 -0
  4. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +1 -0
  5. package/dist/assets/SelectionBox-BXsv_6aH.js +1 -0
  6. package/dist/assets/browser-project-scripts-DOru4L1x.js +2 -0
  7. package/dist/assets/browserAll-CEkWYlFk.js +1 -0
  8. package/dist/assets/collision-world-DniMB0qq.glb +0 -0
  9. package/dist/assets/create-runtime-DYIWr3va.js +1 -0
  10. package/dist/assets/data-asset-Bb9sIM7i.js +5 -0
  11. package/dist/assets/esbuild-BHljloGq.wasm +0 -0
  12. package/dist/assets/fake-three-BMTQ9rl_.js +1 -0
  13. package/dist/assets/game-8SDZPPgH.js +1 -0
  14. package/dist/assets/game-BI111o5f.js +10 -0
  15. package/dist/assets/game-CevorxyV.js +1 -0
  16. package/dist/assets/game-Di1H0gFj.js +1 -0
  17. package/dist/assets/game-Di7XlywD.js +1 -0
  18. package/dist/assets/game-DjnCaw8R.js +1 -0
  19. package/dist/assets/game-Q9A0aXSV.js +1 -0
  20. package/dist/assets/game-pt_1lQLv.js +1 -0
  21. package/dist/assets/game-source-CvidN7AB.js +1 -0
  22. package/dist/assets/game-xsOho_wg.js +83 -0
  23. package/dist/assets/index-Bp5cecD6.js +12 -0
  24. package/dist/assets/index-CPVV6pjw.js +1 -0
  25. package/dist/assets/index-CpOnYyeD.js +213 -0
  26. package/dist/assets/index-Cs8xwv3e.js +1 -0
  27. package/dist/assets/index-DL7RcQ0n.js +27 -0
  28. package/dist/assets/index-Dr8H_6Rl.js +1 -0
  29. package/dist/assets/index-_bCaipUk.js +13650 -0
  30. package/dist/assets/ingest-siblings-Dz8eEqrV.js +1 -0
  31. package/dist/assets/mount-manifest-Bhk0ohsl.js +1 -0
  32. package/dist/assets/recast-navigation.wasm-compat-CvP4UGK6.js +1 -0
  33. package/dist/assets/scene-query-BivAHhJu.js +1 -0
  34. package/dist/assets/stats.module--VATS4Kh.js +1 -0
  35. package/dist/assets/webworkerAll-BLrH0F6F.js +1 -0
  36. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +1 -0
  37. package/dist/assets/xstate.esm-BgUD33nl.js +3 -0
  38. package/dist/index.html +37 -0
  39. package/dist/ingest/racing-game/UPSTREAM.md +68 -0
  40. package/dist/ingest/racing-game/a/Inter-Black.woff +0 -0
  41. package/dist/ingest/racing-game/a/Inter-Black.woff2 +0 -0
  42. package/dist/ingest/racing-game/a/Inter-BlackItalic.woff +0 -0
  43. package/dist/ingest/racing-game/a/Inter-BlackItalic.woff2 +0 -0
  44. package/dist/ingest/racing-game/a/Inter-Bold.woff +0 -0
  45. package/dist/ingest/racing-game/a/Inter-Bold.woff2 +0 -0
  46. package/dist/ingest/racing-game/a/Inter-BoldItalic.woff +0 -0
  47. package/dist/ingest/racing-game/a/Inter-BoldItalic.woff2 +0 -0
  48. package/dist/ingest/racing-game/a/Inter-ExtraBold.woff +0 -0
  49. package/dist/ingest/racing-game/a/Inter-ExtraBold.woff2 +0 -0
  50. package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff +0 -0
  51. package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff2 +0 -0
  52. package/dist/ingest/racing-game/a/Inter-ExtraLight.woff +0 -0
  53. package/dist/ingest/racing-game/a/Inter-ExtraLight.woff2 +0 -0
  54. package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff +0 -0
  55. package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff2 +0 -0
  56. package/dist/ingest/racing-game/a/Inter-Italic.woff +0 -0
  57. package/dist/ingest/racing-game/a/Inter-Italic.woff2 +0 -0
  58. package/dist/ingest/racing-game/a/Inter-Light.woff +0 -0
  59. package/dist/ingest/racing-game/a/Inter-Light.woff2 +0 -0
  60. package/dist/ingest/racing-game/a/Inter-LightItalic.woff +0 -0
  61. package/dist/ingest/racing-game/a/Inter-LightItalic.woff2 +0 -0
  62. package/dist/ingest/racing-game/a/Inter-Medium.woff +0 -0
  63. package/dist/ingest/racing-game/a/Inter-Medium.woff2 +0 -0
  64. package/dist/ingest/racing-game/a/Inter-MediumItalic.woff +0 -0
  65. package/dist/ingest/racing-game/a/Inter-MediumItalic.woff2 +0 -0
  66. package/dist/ingest/racing-game/a/Inter-Regular.woff +0 -0
  67. package/dist/ingest/racing-game/a/Inter-Regular.woff2 +0 -0
  68. package/dist/ingest/racing-game/a/Inter-SemiBold.woff +0 -0
  69. package/dist/ingest/racing-game/a/Inter-SemiBold.woff2 +0 -0
  70. package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff +0 -0
  71. package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff2 +0 -0
  72. package/dist/ingest/racing-game/a/Inter-Thin.woff +0 -0
  73. package/dist/ingest/racing-game/a/Inter-Thin.woff2 +0 -0
  74. package/dist/ingest/racing-game/a/Inter-ThinItalic.woff +0 -0
  75. package/dist/ingest/racing-game/a/Inter-ThinItalic.woff2 +0 -0
  76. package/dist/ingest/racing-game/a/Inter-italic.var.woff2 +0 -0
  77. package/dist/ingest/racing-game/a/Inter-roman.var.woff2 +0 -0
  78. package/dist/ingest/racing-game/a/Inter.var.woff2 +0 -0
  79. package/dist/ingest/racing-game/a/index.css +1 -0
  80. package/dist/ingest/racing-game/images/bronze.png +0 -0
  81. package/dist/ingest/racing-game/images/gold.png +0 -0
  82. package/dist/ingest/racing-game/images/silver.png +0 -0
  83. package/dist/ingest/racing-game/index.html +15 -0
  84. package/dist/ingest/racing-game/models/chassis-draco.glb +0 -0
  85. package/dist/ingest/racing-game/models/track-draco.glb +0 -0
  86. package/dist/ingest/racing-game/models/wheel-draco.glb +0 -0
  87. package/dist/ingest/racing-game/rg-entry.js +1 -0
  88. package/dist/ingest/racing-game/sounds/accelerate.mp3 +0 -0
  89. package/dist/ingest/racing-game/sounds/boost.mp3 +0 -0
  90. package/dist/ingest/racing-game/sounds/crash.mp3 +0 -0
  91. package/dist/ingest/racing-game/sounds/engine.mp3 +0 -0
  92. package/dist/ingest/racing-game/sounds/honk.mp3 +0 -0
  93. package/dist/ingest/racing-game/sounds/tire-brake.mp3 +0 -0
  94. package/dist/ingest/racing-game/sounds/train.mp3 +0 -0
  95. package/dist/ingest/racing-game/sounds/water.mp3 +0 -0
  96. package/dist/ingest/racing-game/textures/cursor.svg +3 -0
  97. package/dist/ingest/racing-game/textures/dikhololo_night_1k.hdr +0 -0
  98. package/dist/ingest/racing-game/textures/heightmap_1024.png +0 -0
  99. package/dist/ingest/racing-game/textures/mask.svg +11 -0
  100. package/dist/ingest/racing-game/textures/no_sound.png +0 -0
  101. package/dist/ingest/racing-game/three-r139.module.js +50617 -0
  102. package/dist/ingest/racing-game/vendor.js +179 -0
  103. package/dist/ingest/racing-game/vgai.game.json +20 -0
  104. package/dist/ingest/simcity/UPSTREAM.md +65 -0
  105. package/dist/ingest/simcity/favicon.ico +0 -0
  106. package/dist/ingest/simcity/fonts/Bebas-Regular.otf +0 -0
  107. package/dist/ingest/simcity/fonts/Digitalt.otf +0 -0
  108. package/dist/ingest/simcity/fonts/Stanberry.ttf +0 -0
  109. package/dist/ingest/simcity/icons/bulldozer-color.png +0 -0
  110. package/dist/ingest/simcity/icons/bulldozer.png +0 -0
  111. package/dist/ingest/simcity/icons/calendar.png +0 -0
  112. package/dist/ingest/simcity/icons/factory-color.png +0 -0
  113. package/dist/ingest/simcity/icons/factory.png +0 -0
  114. package/dist/ingest/simcity/icons/house-color.png +0 -0
  115. package/dist/ingest/simcity/icons/house.png +0 -0
  116. package/dist/ingest/simcity/icons/job.png +0 -0
  117. package/dist/ingest/simcity/icons/office.png +0 -0
  118. package/dist/ingest/simcity/icons/pause-color.png +0 -0
  119. package/dist/ingest/simcity/icons/pause.png +0 -0
  120. package/dist/ingest/simcity/icons/person.png +0 -0
  121. package/dist/ingest/simcity/icons/play-color.png +0 -0
  122. package/dist/ingest/simcity/icons/play.png +0 -0
  123. package/dist/ingest/simcity/icons/power-color.png +0 -0
  124. package/dist/ingest/simcity/icons/power-line-color.png +0 -0
  125. package/dist/ingest/simcity/icons/power.png +0 -0
  126. package/dist/ingest/simcity/icons/road-color.png +0 -0
  127. package/dist/ingest/simcity/icons/road.png +0 -0
  128. package/dist/ingest/simcity/icons/select-color.png +0 -0
  129. package/dist/ingest/simcity/icons/select.png +0 -0
  130. package/dist/ingest/simcity/icons/store-color.png +0 -0
  131. package/dist/ingest/simcity/index.html +94 -0
  132. package/dist/ingest/simcity/index.js +58 -0
  133. package/dist/ingest/simcity/main.css +233 -0
  134. package/dist/ingest/simcity/models/armored-truck.glb +0 -0
  135. package/dist/ingest/simcity/models/building-block-4floor-corner.glb +0 -0
  136. package/dist/ingest/simcity/models/building-block-4floor-short.glb +0 -0
  137. package/dist/ingest/simcity/models/building-block-5floor.glb +0 -0
  138. package/dist/ingest/simcity/models/building-burger-joint.glb +0 -0
  139. package/dist/ingest/simcity/models/building-cafe.glb +0 -0
  140. package/dist/ingest/simcity/models/building-casino.glb +0 -0
  141. package/dist/ingest/simcity/models/building-cinema.glb +0 -0
  142. package/dist/ingest/simcity/models/building-house-block-big.glb +0 -0
  143. package/dist/ingest/simcity/models/building-house-family-large.glb +0 -0
  144. package/dist/ingest/simcity/models/building-house-family-small.glb +0 -0
  145. package/dist/ingest/simcity/models/building-office-balcony.glb +0 -0
  146. package/dist/ingest/simcity/models/building-office-big.glb +0 -0
  147. package/dist/ingest/simcity/models/building-office-pyramid.glb +0 -0
  148. package/dist/ingest/simcity/models/building-office-tall.glb +0 -0
  149. package/dist/ingest/simcity/models/building-office.glb +0 -0
  150. package/dist/ingest/simcity/models/building-restaurant.glb +0 -0
  151. package/dist/ingest/simcity/models/building-skyscraper.glb +0 -0
  152. package/dist/ingest/simcity/models/car-ambulance-pickup.glb +0 -0
  153. package/dist/ingest/simcity/models/car-baywatch.glb +0 -0
  154. package/dist/ingest/simcity/models/car-hippie-van.glb +0 -0
  155. package/dist/ingest/simcity/models/car-passenger-race.glb +0 -0
  156. package/dist/ingest/simcity/models/car-passenger.glb +0 -0
  157. package/dist/ingest/simcity/models/car-police.glb +0 -0
  158. package/dist/ingest/simcity/models/car-taxi.glb +0 -0
  159. package/dist/ingest/simcity/models/car-tow-truck.glb +0 -0
  160. package/dist/ingest/simcity/models/car-truck-dump.glb +0 -0
  161. package/dist/ingest/simcity/models/car-veteran.glb +0 -0
  162. package/dist/ingest/simcity/models/construction-small.glb +0 -0
  163. package/dist/ingest/simcity/models/data-center.glb +0 -0
  164. package/dist/ingest/simcity/models/industry-factory-old.glb +0 -0
  165. package/dist/ingest/simcity/models/industry-factory.glb +0 -0
  166. package/dist/ingest/simcity/models/industry-refinery.glb +0 -0
  167. package/dist/ingest/simcity/models/industry-warehouse.glb +0 -0
  168. package/dist/ingest/simcity/models/power_line_pole_modified.glb +0 -0
  169. package/dist/ingest/simcity/models/tile-plain_grass.glb +0 -0
  170. package/dist/ingest/simcity/models/tile-road-curve.glb +0 -0
  171. package/dist/ingest/simcity/models/tile-road-end.glb +0 -0
  172. package/dist/ingest/simcity/models/tile-road-intersection-t.glb +0 -0
  173. package/dist/ingest/simcity/models/tile-road-intersection.glb +0 -0
  174. package/dist/ingest/simcity/models/tile-road-straight.glb +0 -0
  175. package/dist/ingest/simcity/models/truck.glb +0 -0
  176. package/dist/ingest/simcity/statusIcons/no-power.png +0 -0
  177. package/dist/ingest/simcity/statusIcons/no-road-access.png +0 -0
  178. package/dist/ingest/simcity/textures/base.png +0 -0
  179. package/dist/ingest/simcity/textures/grid.png +0 -0
  180. package/dist/ingest/simcity/textures/specular.png +0 -0
  181. package/dist/ingest/simcity/three-r155.module.js +51466 -0
  182. package/dist/ingest/simcity/vgai.game.json +20 -0
  183. package/dist/ingest/tanks/UPSTREAM.md +69 -0
  184. package/dist/ingest/tanks/index.html +214 -0
  185. package/dist/ingest/tanks/models/pixelTank.fbx +0 -0
  186. package/dist/ingest/tanks/tanks-entry.js +92 -0
  187. package/dist/ingest/tanks/three-r170.module.js +54571 -0
  188. package/dist/ingest/tanks/vgai.game.json +20 -0
  189. package/dist/inputmaps/default.inputmap.json +41 -0
  190. package/dist/jsm/libs/draco/gltf/draco_decoder.js +33 -0
  191. package/dist/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
  192. package/dist/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
  193. package/dist/scenes/untitled.vscn.json +29 -0
  194. package/dist/textures/waternormals.jpg +0 -0
  195. package/dist-server/packaged.mjs +18155 -0
  196. package/package.json +36 -0
  197. package/server/asset-library-routes.ts +833 -0
  198. package/server/canonical-path.ts +50 -0
  199. package/server/cloud-asset-catalog.ts +101 -0
  200. package/server/dev.ts +350 -0
  201. package/server/editor-server.ts +1537 -0
  202. package/server/editor-sse.ts +73 -0
  203. package/server/game-globals-shadow.ts +58 -0
  204. package/server/local-asset-catalog.ts +163 -0
  205. package/server/packaged.ts +571 -0
  206. package/server/prod.ts +75 -0
  207. package/server/project-optimize-deps-entries.ts +149 -0
  208. package/server/project-view.ts +65 -0
  209. package/server/server-utils.ts +430 -0
  210. package/server/session-registry.ts +93 -0
  211. package/src/EditorContext.tsx +87 -0
  212. package/src/achieved-tier.ts +126 -0
  213. package/src/action-registry.ts +268 -0
  214. package/src/adapter-resolver.ts +1858 -0
  215. package/src/asset-events.ts +100 -0
  216. package/src/authoring/active-adapter.ts +98 -0
  217. package/src/authoring/active-systems.ts +50 -0
  218. package/src/authoring/boundary-authoring-adapter.ts +178 -0
  219. package/src/authoring/composite-authoring-adapter.ts +891 -0
  220. package/src/authoring/design-time-layers.ts +776 -0
  221. package/src/authoring/edit-mode-authoring.ts +643 -0
  222. package/src/authoring/embed-only-adapter.ts +57 -0
  223. package/src/authoring/ephemeral-persistence.ts +26 -0
  224. package/src/authoring/eyedropper-session.ts +69 -0
  225. package/src/authoring/first-party-hierarchy-menu.ts +74 -0
  226. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +118 -0
  227. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +550 -0
  228. package/src/authoring/first-party-inspector/HeaderSection.tsx +329 -0
  229. package/src/authoring/first-party-inspector/is-first-party-selection.ts +44 -0
  230. package/src/authoring/first-party-inspector/register.tsx +108 -0
  231. package/src/authoring/ingest-authoring-adapter.ts +475 -0
  232. package/src/authoring/ingest-capture.ts +94 -0
  233. package/src/authoring/ingest-game-adapter.ts +291 -0
  234. package/src/authoring/ingest-iframe-adapter.ts +38 -0
  235. package/src/authoring/ingest-iframe-reachable-adapter.ts +401 -0
  236. package/src/authoring/ingest-overlay.ts +193 -0
  237. package/src/authoring/layered-pick.ts +87 -0
  238. package/src/authoring/mount-failure-report.ts +112 -0
  239. package/src/authoring/overlay-report.ts +52 -0
  240. package/src/authoring/project-scenes.ts +111 -0
  241. package/src/authoring/react-dom-authoring-adapter.ts +795 -0
  242. package/src/authoring/react-inspector-section.tsx +1041 -0
  243. package/src/authoring/react-world-authoring-adapter.ts +2400 -0
  244. package/src/authoring/root-activation.ts +61 -0
  245. package/src/authoring/scene-ui-layer.ts +485 -0
  246. package/src/authoring/story-overlay.ts +48 -0
  247. package/src/authoring/ui-authoring-adapter.ts +325 -0
  248. package/src/authoring/ui-hierarchy-menu.ts +40 -0
  249. package/src/authoring/ui-inspector-sections.tsx +156 -0
  250. package/src/authoring/undo-timeline.ts +145 -0
  251. package/src/authoring/vgai-scene-authoring-adapter.ts +746 -0
  252. package/src/authoring/viewport-pick-context.ts +31 -0
  253. package/src/authoring/viewport-raycast.ts +80 -0
  254. package/src/authoring/world-overlay.ts +76 -0
  255. package/src/authoring/world-pan-state.ts +115 -0
  256. package/src/authoring/world-session-state.ts +102 -0
  257. package/src/authoring/world2d-authoring-adapter.ts +313 -0
  258. package/src/browser-external-watch.ts +98 -0
  259. package/src/browser-play-entry.ts +112 -0
  260. package/src/browser-project-scripts.ts +112 -0
  261. package/src/browser-transpile.ts +246 -0
  262. package/src/browser-watch-reload.ts +100 -0
  263. package/src/command-listener.ts +326 -0
  264. package/src/components/AlignToolbar.tsx +189 -0
  265. package/src/components/AppRoot.tsx +73 -0
  266. package/src/components/AssetBrowser.tsx +502 -0
  267. package/src/components/BottomPanel.tsx +326 -0
  268. package/src/components/BuildPanel.tsx +455 -0
  269. package/src/components/CameraInfo.tsx +56 -0
  270. package/src/components/CommandPalette.tsx +265 -0
  271. package/src/components/ConsolePanel.tsx +272 -0
  272. package/src/components/DataPanel.tsx +1268 -0
  273. package/src/components/DefaultEditorLayout.tsx +509 -0
  274. package/src/components/FolderTree.tsx +179 -0
  275. package/src/components/GameHierarchy.tsx +1349 -0
  276. package/src/components/Inspector.tsx +671 -0
  277. package/src/components/InspectorToolSection.tsx +74 -0
  278. package/src/components/MountFailureBanner.tsx +110 -0
  279. package/src/components/NetworkSection.tsx +109 -0
  280. package/src/components/OnlineAssetBrowser.tsx +448 -0
  281. package/src/components/OverlayReportBanner.tsx +122 -0
  282. package/src/components/PlayBar.tsx +356 -0
  283. package/src/components/PrefabSaveDialog.tsx +201 -0
  284. package/src/components/ProjectHeader.tsx +174 -0
  285. package/src/components/ProjectScreen.tsx +1004 -0
  286. package/src/components/RightPanel.tsx +166 -0
  287. package/src/components/RootActivateDialog.tsx +105 -0
  288. package/src/components/SaveDialog.tsx +197 -0
  289. package/src/components/SaveStatus.tsx +69 -0
  290. package/src/components/StatsOverlay.tsx +35 -0
  291. package/src/components/ToolHost.tsx +80 -0
  292. package/src/components/Toolbar.tsx +307 -0
  293. package/src/components/ViewportOverlay.tsx +234 -0
  294. package/src/components/ViewportPanel.tsx +369 -0
  295. package/src/components/WorldContextMenu.tsx +257 -0
  296. package/src/components/WorldSelectionOverlay.tsx +2003 -0
  297. package/src/components/WorldTextEditor.tsx +86 -0
  298. package/src/components/asset-viewers/AssetViewer.tsx +38 -0
  299. package/src/components/asset-viewers/AudioViewer.tsx +149 -0
  300. package/src/components/asset-viewers/GenericJsonViewer.tsx +77 -0
  301. package/src/components/asset-viewers/ImageViewer.tsx +65 -0
  302. package/src/components/asset-viewers/InputMapViewer.tsx +206 -0
  303. package/src/components/asset-viewers/ModelViewer.tsx +180 -0
  304. package/src/components/asset-viewers/OnlineAssetDetail.tsx +236 -0
  305. package/src/components/data-csv.ts +301 -0
  306. package/src/components/entity-name-commit.ts +18 -0
  307. package/src/components/inspector-property-grouping.ts +65 -0
  308. package/src/components/inspector-widgets/AlignmentGrid.tsx +188 -0
  309. package/src/components/inspector-widgets/BorderEditor.tsx +305 -0
  310. package/src/components/inspector-widgets/ColorPicker.tsx +548 -0
  311. package/src/components/inspector-widgets/FilterEditor.tsx +108 -0
  312. package/src/components/inspector-widgets/FontPicker.tsx +196 -0
  313. package/src/components/inspector-widgets/GradientEditor.tsx +277 -0
  314. package/src/components/inspector-widgets/ScrubbableInput.tsx +152 -0
  315. package/src/components/inspector-widgets/ShadowEditor.tsx +332 -0
  316. package/src/components/inspector-widgets/color-utils.ts +202 -0
  317. package/src/components/inspector-widgets/gallery-main.tsx +167 -0
  318. package/src/components/inspector-widgets/index.ts +22 -0
  319. package/src/components/inspector-widgets/shared.tsx +135 -0
  320. package/src/components/inspectors/AnimationSection.tsx +143 -0
  321. package/src/components/inspectors/AudioSection.tsx +215 -0
  322. package/src/components/inspectors/BoneAttachmentSection.tsx +82 -0
  323. package/src/components/inspectors/CameraSection.tsx +105 -0
  324. package/src/components/inspectors/ComponentsSection.tsx +358 -0
  325. package/src/components/inspectors/JointsSection.tsx +255 -0
  326. package/src/components/inspectors/LightSection.tsx +116 -0
  327. package/src/components/inspectors/MaterialSection.tsx +1189 -0
  328. package/src/components/inspectors/MeshSection.tsx +99 -0
  329. package/src/components/inspectors/NavigationSection.tsx +58 -0
  330. package/src/components/inspectors/ParticleSection.tsx +681 -0
  331. package/src/components/inspectors/PhysicsSection.tsx +326 -0
  332. package/src/components/inspectors/PostProcessingSection.tsx +1290 -0
  333. package/src/components/inspectors/RenderingSection.tsx +109 -0
  334. package/src/components/inspectors/ShadowSection.tsx +84 -0
  335. package/src/components/inspectors/SplineSection.tsx +191 -0
  336. package/src/components/inspectors/TransformSection.tsx +191 -0
  337. package/src/components/inspectors/json-schema-fields.ts +281 -0
  338. package/src/components/inspectors/multi-edit-utils.ts +49 -0
  339. package/src/components/inspectors/zod-fields.ts +184 -0
  340. package/src/components/primitives/ColorInput.tsx +109 -0
  341. package/src/components/primitives/JsonInput.tsx +98 -0
  342. package/src/components/primitives/NumberInput.tsx +164 -0
  343. package/src/components/primitives/Panel.tsx +68 -0
  344. package/src/components/primitives/SectionHeader.tsx +45 -0
  345. package/src/components/primitives/Tooltip.tsx +82 -0
  346. package/src/components/primitives/Vec3Input.tsx +62 -0
  347. package/src/components/world-overlay-gestures.ts +1438 -0
  348. package/src/doctor/data-checks.ts +285 -0
  349. package/src/doctor/report.ts +386 -0
  350. package/src/editor-api.ts +700 -0
  351. package/src/editor-console.ts +194 -0
  352. package/src/editor-hotkeys.ts +441 -0
  353. package/src/editor-layers.ts +2 -0
  354. package/src/editor-mode.ts +28 -0
  355. package/src/editor-store.ts +2225 -0
  356. package/src/editor-viewport.ts +1924 -0
  357. package/src/entity-factory.ts +221 -0
  358. package/src/game-globals-prelude.ts +27 -0
  359. package/src/gated-globals.ts +131 -0
  360. package/src/generated/setup-by-example.generated.ts +53 -0
  361. package/src/gizmo-registry.ts +608 -0
  362. package/src/handle-system.ts +221 -0
  363. package/src/hierarchy-menu-registry.ts +70 -0
  364. package/src/hierarchy-rows.ts +73 -0
  365. package/src/hosted-example.ts +160 -0
  366. package/src/hotkeys.ts +142 -0
  367. package/src/ingest/discovery-react.ts +132 -0
  368. package/src/ingest/discovery.ts +205 -0
  369. package/src/ingest/discovery2d.ts +204 -0
  370. package/src/ingest/esm-entry-resolution.ts +102 -0
  371. package/src/ingest/games/bloom-composer/game.js +99 -0
  372. package/src/ingest/games/bloom-composer/vgai.game.json +21 -0
  373. package/src/ingest/games/embed-bundle/embed.html +10 -0
  374. package/src/ingest/games/embed-bundle/vgai.game.json +21 -0
  375. package/src/ingest/games/games-fps/assets/collision-world.glb +0 -0
  376. package/src/ingest/games/games-fps/game.js +479 -0
  377. package/src/ingest/games/games-fps/vgai.game.json +24 -0
  378. package/src/ingest/games/iframe-reachable/game-source.js +58 -0
  379. package/src/ingest/games/iframe-reachable/vgai.game.json +21 -0
  380. package/src/ingest/games/interactive-cubes/game.js +139 -0
  381. package/src/ingest/games/interactive-cubes/vgai.game.json +21 -0
  382. package/src/ingest/games/mismatched-three/fake-three.js +38 -0
  383. package/src/ingest/games/mismatched-three/game.js +10 -0
  384. package/src/ingest/games/mismatched-three/vgai.game.json +22 -0
  385. package/src/ingest/games/own-three-bundled/game.js +43 -0
  386. package/src/ingest/games/own-three-bundled/vgai.game.json +21 -0
  387. package/src/ingest/games/pointerlock-maze/game.js +288 -0
  388. package/src/ingest/games/pointerlock-maze/vgai.game.json +22 -0
  389. package/src/ingest/games-2d/_fixtures/first-party-scene.scn2d.json +124 -0
  390. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +25 -0
  391. package/src/ingest/games-2d/bunnymark/game.ts +88 -0
  392. package/src/ingest/games-2d/bunnymark/vgai.game.json +21 -0
  393. package/src/ingest/games-2d/flappy/game.ts +148 -0
  394. package/src/ingest/games-2d/flappy/vgai.game.json +21 -0
  395. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +25 -0
  396. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +25 -0
  397. package/src/ingest/games-2d/tilemap-camera/game.ts +115 -0
  398. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +21 -0
  399. package/src/ingest/games-react/react-rpg/entry.tsx +185 -0
  400. package/src/ingest/games-react/react-rpg/shims.d.ts +26 -0
  401. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +129 -0
  402. package/src/ingest/games-react/react-rpg/vgai.game.json +21 -0
  403. package/src/ingest/host-namespace-registry.ts +196 -0
  404. package/src/ingest/types.ts +127 -0
  405. package/src/ingest-hierarchy-rows.ts +207 -0
  406. package/src/ingest-mode.ts +2092 -0
  407. package/src/ingest-siblings.ts +466 -0
  408. package/src/initial-scene.ts +194 -0
  409. package/src/inspector-section-registry.ts +117 -0
  410. package/src/interactive-edit-scope.ts +34 -0
  411. package/src/main.tsx +23 -0
  412. package/src/manifest-project.ts +43 -0
  413. package/src/model-thumbnail.ts +156 -0
  414. package/src/module-mode.ts +513 -0
  415. package/src/navmesh-handler.ts +94 -0
  416. package/src/packaged-runtime.ts +49 -0
  417. package/src/play-mode.ts +1148 -0
  418. package/src/playground/main.ts +206 -0
  419. package/src/project-manager.ts +173 -0
  420. package/src/scene-diff.ts +112 -0
  421. package/src/scene-serializer.ts +144 -0
  422. package/src/scene-sync.ts +587 -0
  423. package/src/scene-ui-mode/active-scene-ui.ts +58 -0
  424. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +152 -0
  425. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +29 -0
  426. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +74 -0
  427. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +49 -0
  428. package/src/scene-ui-mode/ui-prefab-io.ts +76 -0
  429. package/src/script-loader.ts +143 -0
  430. package/src/storage/fsa-storage.ts +55 -0
  431. package/src/storage/handle-storage.ts +150 -0
  432. package/src/storage/handle-store.ts +116 -0
  433. package/src/storage/http-storage.ts +137 -0
  434. package/src/storage/index.ts +147 -0
  435. package/src/storage/mem-storage.ts +135 -0
  436. package/src/storage/opfs-storage.ts +25 -0
  437. package/src/storage/paths.ts +26 -0
  438. package/src/storage/seed.ts +106 -0
  439. package/src/storage/types.ts +110 -0
  440. package/src/tool-loader.ts +291 -0
  441. package/src/ui-editor/inspector.tsx +563 -0
  442. package/src/ui-editor/node-ops.ts +266 -0
  443. package/src/ui-editor/overlay.tsx +380 -0
  444. package/src/ui-editor/palette.tsx +114 -0
  445. package/src/ui-editor/react-store.ts +78 -0
  446. package/src/ui-editor/timeline-panel.tsx +443 -0
  447. package/src/ui-editor/ui-edit-store.ts +552 -0
  448. package/src/ui-editor/ui-prefab.ts +95 -0
  449. package/src/ui-source/inspect.ts +544 -0
  450. package/src/ui-source/oid-transform.ts +133 -0
  451. package/src/ui-source/source-write-backend.ts +203 -0
  452. package/src/ui-source/tailwind-map.ts +174 -0
  453. package/src/ui-source/writer.ts +1175 -0
  454. package/src/widgets/index.ts +73 -0
  455. package/template/.claude/skills/editor/SKILL.md +252 -0
  456. package/template/CLAUDE.md +512 -0
  457. package/template/LICENSE +30 -0
  458. package/template/emit-schemas.ts +42 -0
  459. package/template/index.html +17 -0
  460. package/template/package.json +53 -0
  461. package/template/project.json +9 -0
  462. package/template/public/audio/ambient-arena.wav +0 -0
  463. package/template/public/audio/ambient-music.wav +0 -0
  464. package/template/public/audio/ambient-nature.wav +0 -0
  465. package/template/public/audio/ambient-wind.wav +0 -0
  466. package/template/public/inputmaps/default.inputmap.json +41 -0
  467. package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
  468. package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
  469. package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
  470. package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
  471. package/template/public/scenes/default.vscn.json +43 -0
  472. package/template/public/scenes/untitled.vscn.json +29 -0
  473. package/template/public/textures/waternormals.jpg +0 -0
  474. package/template/server/colyseus-setup.ts +85 -0
  475. package/template/server/main.ts +34 -0
  476. package/template/server/rooms/arena-room.ts +420 -0
  477. package/template/server/rooms/game-room.ts +162 -0
  478. package/template/server/rooms.ts +15 -0
  479. package/template/server/tsconfig.json +19 -0
  480. package/template/src/data/README.md +40 -0
  481. package/template/src/data/assets.ts +31 -0
  482. package/template/src/data/tuning.data.json +5 -0
  483. package/template/src/data/tuning.schema.json +21 -0
  484. package/template/src/data/tuning.schema.ts +34 -0
  485. package/template/src/data/tuning.ts +24 -0
  486. package/template/src/main.ts +157 -0
  487. package/template/src/scripts/components/data-spinner.ts +45 -0
  488. package/template/src/scripts/components/scene-camera.ts +40 -0
  489. package/template/src/scripts/main.ts +94 -0
  490. package/template/src/scripts/registry.ts +46 -0
  491. package/template/src/tools/README.md +14 -0
  492. package/template/src/tools/example.tool.tsx +91 -0
  493. package/template/src/tools/spin.tool.tsx +101 -0
  494. package/template/src/ui/Button.tsx +27 -0
  495. package/template/src/ui/DefaultHud.tsx +40 -0
  496. package/template/src/ui/KeyHint.tsx +25 -0
  497. package/template/src/ui/Panel.tsx +52 -0
  498. package/template/src/ui/RebindPanel.tsx +129 -0
  499. package/template/src/ui/Slider.tsx +28 -0
  500. package/template/src/ui/StatusDot.tsx +22 -0
  501. package/template/src/ui/Toggle.tsx +19 -0
  502. package/template/src/ui/game-state.tsx +37 -0
  503. package/template/src/ui/index.ts +7 -0
  504. package/template/src/ui/mount-default-hud.tsx +19 -0
  505. package/template/src/ui/mount-ui.ts +15 -0
  506. package/template/tsconfig.json +36 -0
  507. package/template/validate-asset-content.ts +78 -0
  508. package/template/validate-manifest.ts +55 -0
  509. package/template/validate-scenes.ts +211 -0
  510. package/template/vgai.game.json +21 -0
  511. package/template/vite.config.ts +128 -0
  512. package/vite-plugin-example-assets.ts +60 -0
  513. package/vite-plugin-game-globals.ts +46 -0
  514. package/vite-plugin-game-static.ts +71 -0
  515. package/vite-plugin-project-game-static.ts +84 -0
  516. package/vite-plugin-react-game-static.ts +98 -0
  517. package/vite-plugin-react-world-provider.ts +81 -0
  518. package/vite-plugin-react-world-runtime.ts +76 -0
  519. package/vite-plugin-ui-oid.ts +561 -0
@@ -0,0 +1,1148 @@
1
+ /**
2
+ * Play-mode orchestrator.
3
+ *
4
+ * Editor and game are fully isolated — separate canvas, renderer, and scene.
5
+ * Play mode creates a new game canvas, launches a standalone game session,
6
+ * and tears it all down on stop. The editor scene is never touched.
7
+ *
8
+ * Flow:
9
+ * enterPlayMode → create canvas → createGameRuntime() → game loop
10
+ * exitPlayMode → game stop → remove canvas → re-enable editor
11
+ */
12
+
13
+ import type { AuthoringAdapter } from '@engine/adapter';
14
+ import type { VgaiMountedGame } from '@engine/adapter/vgai-scene-game-adapter';
15
+ import { assertNever } from '@engine/adapter/world-kind';
16
+ import { _engineLogActive } from '@engine/dev/logger';
17
+ import type { GameComponentClass } from '@engine/ecs/game-component';
18
+ import * as gameComponentModule from '@engine/ecs/game-component';
19
+ import { logHmrSwapMiss } from '@engine/ecs/hmr-swap-report';
20
+ import type { GameSession } from '@engine/runtime/create-runtime';
21
+ import { isFirstPartyMounted, type WorldInstance } from '@engine/runtime/game';
22
+ import { parseSceneFile, SceneParseError } from '@engine/scene/parse';
23
+ import type { Overlay2D } from '@engine/world2d/authoring-2d';
24
+ import { createPhysics2DRegistry } from '@engine/world2d/physics2d-registry';
25
+ import { createPhysicsAdapter2D } from '@engine/world2d/system-adapters-2d';
26
+ import gsap from 'gsap';
27
+ import * as THREE from 'three';
28
+ import { resolveAllWorldEntries, resolveWorldAdapter } from './adapter-resolver';
29
+ import {
30
+ getActiveAuthoring,
31
+ getAuthoringOverride,
32
+ setActiveAuthoring,
33
+ } from './authoring/active-adapter';
34
+ import { setActiveSystems } from './authoring/active-systems';
35
+ import {
36
+ CompositeAuthoringAdapter,
37
+ type CompositeChild,
38
+ } from './authoring/composite-authoring-adapter';
39
+ import { createEphemeralPersistence } from './authoring/ephemeral-persistence';
40
+ import { IngestAuthoringAdapter } from './authoring/ingest-authoring-adapter';
41
+ import type { IngestOverlay } from './authoring/ingest-overlay';
42
+ import { ReactWorldAuthoringAdapter } from './authoring/react-world-authoring-adapter';
43
+ import { type UndoAdapter, unregisterUndoAdapter } from './authoring/undo-timeline';
44
+ import { loadWorldOverlay, saveWorldOverlay, worldOverlayPath } from './authoring/world-overlay';
45
+ import { World2DAuthoringAdapter } from './authoring/world2d-authoring-adapter';
46
+ import { importFromSource, registerRuntimeModules } from './browser-transpile';
47
+ import { watchScriptChanges } from './browser-watch-reload';
48
+ import type { LogEntry } from './editor-api';
49
+ import { endLogSession, flushLogEntries, startLogSession } from './editor-api';
50
+ import type { ConsoleEntry } from './editor-console';
51
+ import { editorConsole } from './editor-console';
52
+ import { getEditorMode } from './editor-mode';
53
+ import type { EditorStore } from './editor-store';
54
+ import { installGatedGameGlobals, setGameInputGate } from './gated-globals';
55
+ import { exampleDispatchFor } from './hosted-example';
56
+ import { fetchGameManifest } from './manifest-project';
57
+ import { getCurrentProject } from './project-manager';
58
+ import { loadProjectScripts, type ProjectScripts, reloadProjectScripts } from './script-loader';
59
+ import { createHttpSourceWriteBackend } from './ui-source/source-write-backend';
60
+
61
+ /** Context needed by the orchestrator (passed from ViewportPanel). */
62
+ export interface PlayModeContext {
63
+ store: EditorStore;
64
+ /** Container for the game canvas (the Game tab panel). */
65
+ gameContainer: HTMLElement;
66
+ /** Returns the editor viewport camera (for copying viewpoint into game). */
67
+ getViewportCamera: () => THREE.PerspectiveCamera;
68
+ }
69
+
70
+ let _gameContainerEl: HTMLElement | null = null;
71
+ let _ctx: PlayModeContext | null = null;
72
+ let _session: GameSession | null = null;
73
+
74
+ /**
75
+ * Play-mode generation counter. Bumped on every enterPlayMode and every
76
+ * exitPlayMode. enterPlayMode captures the value before its async boot and
77
+ * re-checks it around `createGameRuntime` — if exitPlayMode (Stop/Escape) ran
78
+ * while the runtime was still booting, the freshly-created session belongs to
79
+ * an already-exited play and must be stopped and abandoned, NOT adopted.
80
+ * Without this, a stop-during-boot left the store swapped onto the orphaned
81
+ * live game scene (empty hierarchy in edit mode) with the session leaked
82
+ * (RAF loop, Rapier world, WebGL context never released).
83
+ */
84
+ let _playEpoch = 0;
85
+
86
+ // T6.3: install the gated window/document proxies once so the dev server's/
87
+ // browser-transpile's GAME_GLOBALS_PRELUDE (prepended to project modules) has
88
+ // something to resolve to. No-op outside a browser (headless unit tests).
89
+ installGatedGameGlobals();
90
+
91
+ /**
92
+ * The first-party game runtime behind the current session. HMR (component
93
+ * hot-swap, warm restart) and physics edit-sync are inherently first-party, so
94
+ * play-mode reaches the `GameContext` + `hotReload` via the `VgaiMountedGame`
95
+ * extras on `session.mounted`. The generic host (`createGameRuntime`) no longer
96
+ * exposes these — that confinement is the adapter inversion.
97
+ */
98
+ /** e2e counter: proofs read `window.__vgaiHmrCount` to assert HMR fired. */
99
+ function bumpHmrCount(): void {
100
+ const w = window as unknown as { __vgaiHmrCount?: number };
101
+ w.__vgaiHmrCount = (w.__vgaiHmrCount ?? 0) + 1;
102
+ }
103
+
104
+ function firstParty(session: GameSession): VgaiMountedGame {
105
+ return session.mounted as VgaiMountedGame;
106
+ }
107
+
108
+ /**
109
+ * Fan HMR's `hotSwap` out across EVERY first-party world in the session
110
+ * (§7.1-4, probes 2/2b) — both call sites below used to reach only
111
+ * `firstParty(_session).ctx.components.hotSwap(...)`, i.e. the DEFAULT
112
+ * world's `ComponentManager` alone. In a multi-world session (T6.1) a
113
+ * component living in a second/third world never saw a hot update at all
114
+ * (probe2), and — worse — a component living ONLY in a non-default world
115
+ * produced a misleading "no live instance" swap-miss warning (probe2b) even
116
+ * though the instance was alive the whole time, just in a manager this call
117
+ * never consulted; structurally identical to "you renamed the class."
118
+ *
119
+ * Iterates `session.game.worlds` (declaration order), skipping any
120
+ * non-first-party mount (`isFirstPartyMounted` — an opaque/foreign or react
121
+ * mount has no `ComponentManager` to hotSwap against), and calls each
122
+ * first-party world's OWN `hotSwap` with its own swap-miss warning
123
+ * suppressed (`{ warnOnMiss: false }`) — a per-world zero-match is
124
+ * meaningless on its own (the name may simply live in a DIFFERENT world).
125
+ * The ONE real "nothing anywhere matched this name" warning is emitted HERE,
126
+ * exactly once, with the SAME message convention every other swap-miss uses
127
+ * (`logHmrSwapMiss`), only when the total across every first-party world is
128
+ * zero.
129
+ *
130
+ * Returns the total number of live instances swapped, summed across worlds.
131
+ */
132
+ export function hotSwapAcrossWorlds(
133
+ session: GameSession,
134
+ name: string,
135
+ NewClass: GameComponentClass,
136
+ ): number {
137
+ let total = 0;
138
+ for (const world of session.game.worlds) {
139
+ if (!isFirstPartyMounted(world.mounted)) continue;
140
+ total += world.mounted.ctx.components.hotSwap(name, NewClass, { warnOnMiss: false });
141
+ }
142
+ if (total === 0) {
143
+ logHmrSwapMiss({ key: name, reason: 'no-live-instance' });
144
+ }
145
+ return total;
146
+ }
147
+
148
+ /**
149
+ * Warm-restart's fan-out investigation (§7.1-4's second half): the
150
+ * non-component-edit HMR branch below re-runs ONLY the default world's
151
+ * `hotReload` — it does NOT fan out across `session.game.worlds` the way
152
+ * {@link hotSwapAcrossWorlds} does for true-HMR component swaps. This is a
153
+ * STRUCTURAL limitation, not an oversight left for later:
154
+ *
155
+ * 1. `hotReload` is a `VgaiMountedGame` (first-party THREEJS mount) extra —
156
+ * `world2d/pixi-game-adapter.ts`'s pixi mount and `create-runtime.ts`'s
157
+ * react mount expose no such method at all, so there is no capability to
158
+ * fan out to on a non-threejs world even in principle.
159
+ * 2. Even restricted to first-party THREEJS worlds, `reloadProjectScripts`
160
+ * (`script-loader.ts`) resolves exactly ONE project entry
161
+ * (`src/scripts/main.ts` by default) and returns ONE `setup` function —
162
+ * it has no per-world mapping, so there is no way to know which world a
163
+ * second/third `hotReload` call should even run against, let alone which
164
+ * source file to re-import for it.
165
+ *
166
+ * So this reports the limitation loudly instead of silently reloading only
167
+ * part of a multi-world session — called right after a successful warm
168
+ * restart, from the `vgai:script-update` handler below.
169
+ */
170
+ export function warnIfOtherFirstPartyWorldsUnrestarted(session: GameSession): void {
171
+ const worlds = session.game?.worlds;
172
+ if (!worlds || worlds.length <= 1) return;
173
+ const defaultWorldId = session.game?.defaultWorld.id;
174
+ const otherFirstParty = worlds.some(
175
+ (w) => w.id !== defaultWorldId && isFirstPartyMounted(w.mounted),
176
+ );
177
+ if (otherFirstParty) {
178
+ editorConsole.warn(
179
+ 'Warm restart applied to the default world only — components in other worlds keep ' +
180
+ 'running old code until play restarts (structural limitation: hotReload is a first-party ' +
181
+ 'threejs-mount-only capability, and reloadProjectScripts resolves a single ' +
182
+ 'src/scripts/main.ts entry with no per-world mapping).',
183
+ 'play-mode',
184
+ );
185
+ }
186
+ }
187
+
188
+ /**
189
+ * The warm-restart HMR bracket's pause/resume wrapper (§7.1 item 1 /
190
+ * probe3-warm-restart-unpauses: the bracket used to call
191
+ * `_session.pause(); await hotReload(...); _session.resume();`
192
+ * unconditionally, so it silently un-paused a user-paused game — the UI kept
193
+ * saying "paused" while the simulation resumed running). Fix: capture
194
+ * whether the game was ALREADY paused from the SAME source of truth
195
+ * `pause()`/`resume()` drive (`session.game.play.paused`, `game.ts:713`)
196
+ * BEFORE unconditionally pausing for the reload, and only resume afterward
197
+ * if it was not already paused — `pause()`/`resume()` are idempotent, so an
198
+ * unconditional `pause()` up front is always safe. try/finally so a throw
199
+ * mid-`reload()` still restores the correct pre-bracket state (a throw must
200
+ * never leave a previously-RUNNING game stuck paused). Exported for direct
201
+ * unit testing without any `import.meta.hot`/Vite HMR event machinery — see
202
+ * `packages/editor/test/play-mode-warm-restart-pause.test.ts`.
203
+ */
204
+ export async function runWarmRestartPauseBracket(
205
+ session: GameSession,
206
+ reload: () => Promise<void>,
207
+ ): Promise<void> {
208
+ const wasPaused = session.game.play.paused;
209
+ session.pause();
210
+ try {
211
+ await reload();
212
+ } finally {
213
+ if (!wasPaused) session.resume();
214
+ }
215
+ }
216
+ let _gameCanvas: HTMLCanvasElement | null = null;
217
+ let _resizeObserver: ResizeObserver | null = null;
218
+ let _unsubStore: (() => void) | null = null;
219
+ /** Browser-mode component-source watch (Phase A2); null in server mode / stopped. */
220
+ let _unsubBrowserScriptWatch: (() => void) | null = null;
221
+ /** True for the CURRENT session iff it mounted through the worlds path with
222
+ * more than one world — decides which of `installPlayAuthoring` (ephemeral,
223
+ * single `.vscn` preview) vs `installMultiWorldAuthoring` (real per-world
224
+ * overlay persistence) ran, so `exitPlayMode` tears down the right one. */
225
+ let _isMultiWorldSession = false;
226
+ /**
227
+ * The authoring override that was active before THIS play session installed its
228
+ * own (normally `null` — nothing else authors while playing today, but this
229
+ * restores whatever was there rather than assuming null; see
230
+ * `installPlayAuthoring`/`restorePriorAuthoring`). `undefined` ⇒ this play
231
+ * session never installed one (e.g. it bailed before adopting the scene) — exit
232
+ * must then leave the authoring override untouched.
233
+ */
234
+ let _priorAuthoringOverride: AuthoringAdapter | null | undefined;
235
+
236
+ /**
237
+ * Play adoption drives the SAME authoring adapter the editor already uses for
238
+ * `store` (first-party today — `getActiveAuthoring` falls back to it absent an
239
+ * override), just with its `persistence` swapped for the ephemeral provider
240
+ * (design `docs/PERSISTENCE-PROVIDER-DESIGN.md` §2/§7 slice 1): play-time edits
241
+ * apply to the live game scene exactly as before, but the store's autosave/
242
+ * saveNow guards now see `isDirty() → false` / no real destination instead of
243
+ * relying on the `_playState` flag alone. `undo`/`redo`/`subscribe` are rebound
244
+ * explicitly (a plain spread would keep them pointing at `base`'s `this`, which
245
+ * is fine for arrow-function fields but not for prototype methods).
246
+ */
247
+ function installPlayAuthoring(store: EditorStore): void {
248
+ _priorAuthoringOverride = getAuthoringOverride();
249
+ const base = getActiveAuthoring(store);
250
+ const wrapped: AuthoringAdapter = {
251
+ ...base,
252
+ persistence: createEphemeralPersistence(() => store.getDocument()),
253
+ };
254
+ // Only rebind when `base` actually implements these (exactOptionalPropertyTypes
255
+ // rejects assigning `undefined` to an already-optional property explicitly).
256
+ if (base.subscribe) wrapped.subscribe = base.subscribe.bind(base);
257
+ if (base.undo) wrapped.undo = base.undo.bind(base);
258
+ if (base.redo) wrapped.redo = base.redo.bind(base);
259
+ setActiveAuthoring(wrapped);
260
+ }
261
+
262
+ /** Restore whatever authoring override (if any) was active before play started. */
263
+ function restorePriorAuthoring(): void {
264
+ if (_priorAuthoringOverride === undefined) return; // installPlayAuthoring never ran
265
+ setActiveAuthoring(_priorAuthoringOverride);
266
+ _priorAuthoringOverride = undefined;
267
+ }
268
+
269
+ /**
270
+ * Per-world adapters built for the CURRENT multi-world session, tracked ONLY so
271
+ * `exitMultiWorldAuthoring` can unregister any that self-registered with the
272
+ * global undo timeline. As of v4 §7.1-13, `IngestAuthoringAdapter` AND
273
+ * `World2DAuthoringAdapter` both self-register (the `ReactWorldAuthoringAdapter`
274
+ * branch does too) — mirrors `ingest-mode.ts`'s teardown of its own single
275
+ * adapter.
276
+ */
277
+ let _multiWorldChildren: AuthoringAdapter[] = [];
278
+
279
+ /**
280
+ * Build ONE {@link AuthoringAdapter} per mounted world (T6.1 slice 3,
281
+ * docs/MULTI-WORLD-DESIGN.md §1.F/§3 slice 3) and install the N-ary
282
+ * {@link CompositeAuthoringAdapter} over all of them as the active authoring
283
+ * override.
284
+ *
285
+ * Study finding (this slice): `resolveAllWorlds`'s own doc comment records that
286
+ * NO world in a multi-world manifest gets the live editor document/editorPreview
287
+ * — every world mounts from its own on-disk `scene`/`entry`, single-world OR
288
+ * not. So there is no "the store's document IS this world's live scene" world
289
+ * the way `VgaiSceneAuthoringAdapter` assumes for the single-world path — EVERY
290
+ * world here (including a `kind:'threejs'` world) gets a live-tree REFLECTION
291
+ * adapter, the same shape `ingest-mode.ts` already uses for an unmodified game's
292
+ * `Object3D` tree: `IngestAuthoringAdapter` for a threejs world's `threeRoot()`,
293
+ * `World2DAuthoringAdapter` for a pixijs world's `pixiRoot()`. Persistence is
294
+ * REAL (not the ephemeral discard-on-stop wrapper `installPlayAuthoring` uses
295
+ * for the single-world `.vscn` preview path) — each per-world adapter's overlay
296
+ * (§1.G: `.vgai/overlays/<world-id>.json`, project-root-relative, `authoring/
297
+ * world-overlay.ts`) is the honest floor a manifest-mounted world has today,
298
+ * exactly like `ingest-mode.ts`'s own overlay persistence (both its three-side
299
+ * and — T6.1 slice 4 — pixi-side ingest sessions) —
300
+ * this is not a "preview of an editable document" session the way single-world
301
+ * play is, so ephemeral-wrapping it would silently discard the ONLY persistence
302
+ * mechanism these worlds have.
303
+ *
304
+ * `store` is used ONLY as the editor-global selection bag + change-notification
305
+ * bus (`selectedEntityIds`/`selectMultiple`/`subscribe`/`notifyIngestEdit`) —
306
+ * neither per-world adapter reads/writes `store`'s document, matching how
307
+ * `ingest-mode.ts` already uses the SAME store for the same narrow purpose
308
+ * over a live foreign tree.
309
+ *
310
+ * A `kind: 'react'` world (T6.2 slice 2, `docs/REACT-WORLD-DESIGN.md` §1.D/§3
311
+ * slice 2) gets a {@link ReactWorldAuthoringAdapter} over its `reactRoot()` DOM —
312
+ * NOT the overlay-file persistence the threejs/pixijs branches use: a react
313
+ * world's truth is JSX SOURCE (D4), so its persistence is the SAME immediate
314
+ * source-write seam `UIAuthoringAdapter` already uses
315
+ * (`../ui-source/source-write-backend.ts`), wired only under the dev server
316
+ * (`import.meta.env.DEV`) exactly like `edit-mode-authoring.ts`'s scene-UI
317
+ * child does — a hosted/no-dev-server session degrades to selection/
318
+ * inspection only, writes reporting unavailable (never silently faked).
319
+ */
320
+ async function installMultiWorldAuthoring(
321
+ store: EditorStore,
322
+ worlds: readonly WorldInstance[],
323
+ ): Promise<void> {
324
+ _priorAuthoringOverride = getAuthoringOverride();
325
+ const children: CompositeChild[] = [];
326
+ for (const world of worlds) {
327
+ if (world.kind === 'threejs') {
328
+ const overlay = await loadWorldOverlay<IngestOverlay>(world.id);
329
+ const adapter = new IngestAuthoringAdapter(store, world.threeRoot(), {
330
+ overlay,
331
+ persist: (o) => saveWorldOverlay(world.id, o),
332
+ destination: worldOverlayPath(world.id),
333
+ });
334
+ children.push({ worldId: world.id, kind: world.kind, adapter });
335
+ } else if (world.kind === 'pixijs') {
336
+ const overlay = await loadWorldOverlay<Overlay2D>(world.id);
337
+ const physics = createPhysicsAdapter2D(world.physics2d ?? createPhysics2DRegistry());
338
+ const adapter = new World2DAuthoringAdapter(world.pixiRoot(), physics, store, {
339
+ overlay,
340
+ persist: (o) => saveWorldOverlay(world.id, o),
341
+ destination: worldOverlayPath(world.id),
342
+ });
343
+ children.push({ worldId: world.id, kind: world.kind, adapter });
344
+ } else if (world.kind === 'react') {
345
+ const adapter = new ReactWorldAuthoringAdapter(world.reactRoot(), store, {
346
+ // Same dev-server-only gating `edit-mode-authoring.ts`'s scene-UI
347
+ // child uses for the identical seam: a hosted/production build has
348
+ // no `/__ui-source` middleware.
349
+ ...(import.meta.env.DEV ? { writeBackend: createHttpSourceWriteBackend() } : {}),
350
+ });
351
+ children.push({ worldId: world.id, kind: world.kind, adapter });
352
+ } else {
353
+ // Exhaustiveness guard (§7.4-2): the pre-existing if/else-if chain over
354
+ // `WorldInstance.kind` had no trailing else — a hypothetical 4th kind
355
+ // would silently get NO authoring child (no error, just missing
356
+ // authoring for that world) rather than failing loudly/at compile time.
357
+ assertNever(world.kind, 'installMultiWorldAuthoring');
358
+ }
359
+ }
360
+ _multiWorldChildren = children.map((c) => c.adapter);
361
+ const composite = new CompositeAuthoringAdapter(children);
362
+ setActiveAuthoring(composite);
363
+ // `setActiveAuthoring` is a plain module-level variable, not React state —
364
+ // the left hierarchy panel only re-checks `hasAuthoringOverride()` when the
365
+ // store notifies (same reason `ingest-mode.ts` calls this right after
366
+ // installing its own override).
367
+ store.notifyIngestEdit();
368
+
369
+ // Dev/e2e diagnostic handle — the SAME pattern `ingest-mode.ts`'s
370
+ // `window.__vgaiIngest`/`window.__vgaiIngest2D` use:
371
+ // `store.saveNow()` (Ctrl/Cmd+S) and `_autoSave()` are both structurally
372
+ // guarded OFF while ANY authoring override is active (`hasAuthoringOverride()`
373
+ // / the play-state check) — the adapter's OWN `persistence.save()` is the
374
+ // only way an override session's edits reach disk, so tests/tooling need a
375
+ // handle to call it directly, exactly as the ingest sessions expose.
376
+ if (import.meta.env.DEV) {
377
+ (window as unknown as Record<string, unknown>)['__vgaiMultiWorld'] = {
378
+ adapter: composite,
379
+ worldIds: children.map((c) => c.worldId),
380
+ store,
381
+ };
382
+ }
383
+ }
384
+
385
+ /** Teardown for `installMultiWorldAuthoring` — unregisters any per-world child
386
+ * that self-registered with the global undo timeline (mirrors
387
+ * `ingest-mode.ts`'s `unregisterUndoAdapter` call on its own adapter). Must
388
+ * pass the ORIGINAL adapter instance (not a rebuilt wrapper) — the timeline
389
+ * keys registration by object identity (see undo-timeline.ts's doc comment),
390
+ * and `registerUndoAdapter(this)` inside each of `IngestAuthoringAdapter`'s,
391
+ * `World2DAuthoringAdapter`'s (v4 §7.1-13), and `ReactWorldAuthoringAdapter`'s
392
+ * constructors registered exactly that instance. The type guard below is now
393
+ * just a defensive narrowing (every current child kind DOES register), kept
394
+ * in case a future child kind legitimately has no undo/redo of its own. */
395
+ function exitMultiWorldAuthoring(): void {
396
+ for (const adapter of _multiWorldChildren) {
397
+ if (adapter.undo && adapter.redo)
398
+ unregisterUndoAdapter(adapter as AuthoringAdapter & UndoAdapter);
399
+ }
400
+ _multiWorldChildren = [];
401
+ if (import.meta.env.DEV) {
402
+ delete (window as unknown as Record<string, unknown>)['__vgaiMultiWorld'];
403
+ }
404
+ }
405
+ let _escapeListener: ((e: KeyboardEvent) => void) | null = null;
406
+ let _originalConsoleLog: typeof console.log | null = null;
407
+ let _originalConsoleInfo: typeof console.info | null = null;
408
+ let _originalConsoleWarn: typeof console.warn | null = null;
409
+ let _originalConsoleError: typeof console.error | null = null;
410
+ let _logFlushChain: Promise<unknown> = Promise.resolve();
411
+ let _flushInterval: ReturnType<typeof setInterval> | null = null;
412
+ let _pendingEntries: LogEntry[] = [];
413
+
414
+ // --- Script HMR ---
415
+ if (import.meta.hot) {
416
+ import.meta.hot.on('vgai:script-update', async (data: { file: string }) => {
417
+ if (!_session || !_ctx) return;
418
+ const project = getCurrentProject();
419
+ if (!project) return;
420
+ try {
421
+ const { store } = _ctx;
422
+
423
+ // Strategy based on which file changed:
424
+ // Component source files → true HMR (import only the file, swap prototypes)
425
+ // Entry point or registry → warm restart (re-import main, re-run setup)
426
+ const isComponentFile =
427
+ !data.file.endsWith('/main.ts') && !data.file.endsWith('/registry.ts');
428
+
429
+ if (isComponentFile) {
430
+ // True HMR: import only the changed file to get fresh class definitions.
431
+ // This avoids re-importing the full module chain which can trigger page reloads.
432
+ const modulePath = `/@fs/${data.file}?t=${Date.now()}`;
433
+ const mod = await import(/* @vite-ignore */ modulePath);
434
+ for (const [key, value] of Object.entries(mod)) {
435
+ // §7.1-5/probe7: filter to GameComponent subclasses BEFORE ever
436
+ // calling hotSwap — a plain helper function export sharing a live
437
+ // component's registry key/name must never reach
438
+ // `Object.setPrototypeOf`. `gameComponentModule.GameComponent` is
439
+ // the SAME singleton class the running session's components were
440
+ // built from — Vite's dev-server module graph resolves
441
+ // `@engine/ecs/game-component` to one cached instance for both this
442
+ // file and the `/@fs/`-imported component file (the cache-busting
443
+ // `?t=` query targets ONLY the edited file's own module id, not its
444
+ // import specifiers), so `instanceof` here holds across the two
445
+ // import paths. See `applyBrowserComponentEdit` below for the
446
+ // in-browser (esbuild-wasm) path's identity guarantee, which is
447
+ // explicit-by-construction via `registerRuntimeModules`.
448
+ if (
449
+ typeof value === 'function' &&
450
+ value.prototype instanceof gameComponentModule.GameComponent
451
+ ) {
452
+ hotSwapAcrossWorlds(_session, key, value as unknown as GameComponentClass);
453
+ }
454
+ }
455
+ bumpHmrCount();
456
+ editorConsole.log('Hot-swapped component classes (true HMR)', 'play-mode');
457
+ } else {
458
+ // Warm restart: re-run setup() with fresh code
459
+ const scripts = await reloadProjectScripts(project.rootPath);
460
+ // W1 (docs/R3F-FIRST-PARTY-DESIGN.md §1.A) widened `ProjectScripts` so
461
+ // `setup` is no longer guaranteed — an entry that exports only
462
+ // `adapter` has no re-runnable setup() for `hotReload` to call (the
463
+ // adapter owns its whole mount lifecycle itself; O3 in the design doc
464
+ // already flags entry-module HMR as unverified for v1). Fail loudly
465
+ // here instead of handing `hotReload` an `undefined` — the outer
466
+ // try/catch (below) reports it exactly like any other HMR failure.
467
+ const reloadedSetup = scripts.setup;
468
+ if (!reloadedSetup) {
469
+ throw new Error(
470
+ 'Warm restart failed: the project entry now exports only `adapter`, not `setup` — ' +
471
+ 'hot-reloading a pre-built GameAdapter has no setup() to re-run in v1 ' +
472
+ '(docs/R3F-FIRST-PARTY-DESIGN.md O3). Restart play instead.',
473
+ );
474
+ }
475
+ const hotViewCam = _ctx.getViewportCamera();
476
+ // Pause the host loop around the warm restart (the generic host no longer
477
+ // owns hotReload; gate the loop so it doesn't tick mid-rebuild) — but never
478
+ // un-pause a game the USER had already paused (§7.1-1, probe3).
479
+ await runWarmRestartPauseBracket(_session, () =>
480
+ firstParty(_session as GameSession).hotReload(reloadedSetup, {
481
+ scenePath: store.savePath,
482
+ sceneData: store.getDocument(),
483
+ viewportCamera: {
484
+ position: hotViewCam.position.toArray() as [number, number, number],
485
+ quaternion: hotViewCam.quaternion.toArray() as [number, number, number, number],
486
+ },
487
+ }),
488
+ );
489
+ bumpHmrCount();
490
+ editorConsole.log('Hot-reloaded game scripts (warm restart)', 'play-mode');
491
+ warnIfOtherFirstPartyWorldsUnrestarted(_session);
492
+ }
493
+ } catch (err) {
494
+ editorConsole.error(`Script hot-reload failed: ${err}`, 'play-mode');
495
+ }
496
+ });
497
+ }
498
+
499
+ // --- In-browser HMR (no server) ---
500
+ //
501
+ // The `vgai:script-update` handler above relies on the Vite dev server to
502
+ // transpile the changed file and on a filesystem watcher to fire the event.
503
+ // The path below does the same live prototype-swap, but the source comes from
504
+ // the browser and is transpiled in-browser via esbuild-wasm — no Node, no Vite
505
+ // transpile, no file write. This is what makes hot reload possible entirely
506
+ // client-side. The transpiled component shares the running session's THREE and
507
+ // GameComponent singletons (registered below), so prototype chains stay valid.
508
+
509
+ let _runtimeModulesRegistered = false;
510
+ function ensureRuntimeModulesRegistered(): void {
511
+ if (_runtimeModulesRegistered) return;
512
+ registerRuntimeModules({
513
+ three: THREE as unknown as Record<string, unknown>,
514
+ gsap: { default: gsap } as Record<string, unknown>,
515
+ '@engine/ecs/game-component': gameComponentModule as unknown as Record<string, unknown>,
516
+ });
517
+ _runtimeModulesRegistered = true;
518
+ }
519
+
520
+ /**
521
+ * Apply an in-browser source edit to the running game: transpile the given
522
+ * component module source with esbuild-wasm and hot-swap every exported class
523
+ * onto its live instances. Returns the names of the classes that were swapped.
524
+ *
525
+ * This is the in-browser analogue of the `vgai:script-update` HMR handler —
526
+ * same engine `hotSwap`, but driven entirely client-side.
527
+ */
528
+ export async function applyBrowserComponentEdit(source: string): Promise<string[]> {
529
+ if (!_session) throw new Error('Not in play mode — enter play mode before editing components.');
530
+ ensureRuntimeModulesRegistered();
531
+ const mod = await importFromSource(source);
532
+ const swapped: string[] = [];
533
+ for (const [name, value] of Object.entries(mod)) {
534
+ // §7.1-5/probe7: same GameComponent-subclass filter as the vgai:script-update
535
+ // handler above — identity here is explicit-by-construction (not merely
536
+ // module-graph coincidence): `ensureRuntimeModulesRegistered` registers
537
+ // THIS session's live `gameComponentModule` singleton via
538
+ // `registerRuntimeModules`, and `importFromSource`'s esbuild-wasm output
539
+ // has its bare `@engine/ecs/game-component` import rewritten to a blob-URL
540
+ // shim that re-exports exactly that registered namespace — so a
541
+ // transpiled component's `class Foo extends GameComponent` extends the
542
+ // SAME class object checked here.
543
+ if (
544
+ typeof value === 'function' &&
545
+ value.prototype instanceof gameComponentModule.GameComponent
546
+ ) {
547
+ hotSwapAcrossWorlds(_session, name, value as GameComponentClass);
548
+ swapped.push(name);
549
+ }
550
+ }
551
+ if (swapped.length > 0) {
552
+ const w = window as unknown as Record<string, unknown>;
553
+ w['__vgaiHmrCount'] = ((w['__vgaiHmrCount'] as number) ?? 0) + 1;
554
+ editorConsole.log(`In-browser hot-swap: ${swapped.join(', ')}`, 'play-mode');
555
+ }
556
+ return swapped;
557
+ }
558
+
559
+ // Dev-only handle so the editor UI / e2e can drive in-browser component edits.
560
+ if (import.meta.env.DEV) {
561
+ (window as unknown as Record<string, unknown>)['__vgaiBrowserHmr'] = {
562
+ applyBrowserComponentEdit,
563
+ };
564
+ }
565
+
566
+ /** Set the Game panel container element (called by GamePanel on mount). */
567
+ export function setGameContainer(el: HTMLElement | null): void {
568
+ _gameContainerEl = el;
569
+ }
570
+
571
+ /** Get the Game panel container element (shared with ingest-mode). */
572
+ export function getGameContainer(): HTMLElement | null {
573
+ return _gameContainerEl;
574
+ }
575
+
576
+ /** Bind the play-mode orchestrator to editor context. Call once at init. */
577
+ export function bindPlayMode(ctx: Omit<PlayModeContext, 'gameContainer'>): void {
578
+ _ctx = { ...ctx, gameContainer: null! }; // gameContainer set dynamically via setGameContainer
579
+ }
580
+
581
+ /** True if play mode is currently active (playing or paused). */
582
+ export function isPlayModeActive(): boolean {
583
+ return _session !== null;
584
+ }
585
+
586
+ /** Get the running game's audio context (for volume/mute control). */
587
+ export function getGameAudio() {
588
+ return _session ? firstParty(_session).ctx.audio : null;
589
+ }
590
+
591
+ /** Get the running game's scene (for editor Scene tab rendering). */
592
+ export function getGameScene(): THREE.Scene | null {
593
+ return _session?.scene ?? null;
594
+ }
595
+
596
+ /** Resize the running game to a specific resolution. */
597
+ export function resizeGame(width: number, height: number): void {
598
+ _session?.resize(width, height);
599
+ }
600
+
601
+ /**
602
+ * Enter play mode: create a game canvas, start game, disable editor controls.
603
+ */
604
+ export async function enterPlayMode(): Promise<void> {
605
+ if (!_ctx) return;
606
+ // The play button is only shown while stopped, so a non-null _session here is
607
+ // stale — a previous play that didn't fully tear down (e.g. a re-entry race).
608
+ // Clean it up so re-entering play mode always works.
609
+ if (_session) exitPlayMode();
610
+ // Capture this play's generation AFTER the stale-session cleanup (which
611
+ // bumps the epoch via exitPlayMode). Any exitPlayMode — or a newer
612
+ // enterPlayMode — during the async boot below invalidates this generation.
613
+ const epoch = ++_playEpoch;
614
+ const { store } = _ctx;
615
+
616
+ // Validate scene before entering play mode
617
+ try {
618
+ parseSceneFile(store.getDocument());
619
+ } catch (err) {
620
+ const msg = err instanceof SceneParseError ? err.message : `Scene validation failed: ${err}`;
621
+ editorConsole.error(msg, 'scene');
622
+ return;
623
+ }
624
+
625
+ // Patch console → editorConsole tagged [game]
626
+ patchConsole();
627
+
628
+ // Start log session and register sink for JSONL persistence
629
+ await startLogSession();
630
+ _pendingEntries = [];
631
+ _logFlushChain = Promise.resolve();
632
+ editorConsole.setSink((entry: ConsoleEntry) => {
633
+ _pendingEntries.push({
634
+ t: entry.timestamp,
635
+ level: entry.level,
636
+ msg: entry.message,
637
+ ...(entry.source ? { source: entry.source } : {}),
638
+ ...(entry.subsystem ? { sub: entry.subsystem } : {}),
639
+ ...(entry.metadata ? { meta: entry.metadata } : {}),
640
+ });
641
+ });
642
+ _flushInterval = setInterval(() => {
643
+ if (_pendingEntries.length > 0) {
644
+ const batch = _pendingEntries.splice(0);
645
+ _logFlushChain = _logFlushChain.then(() => flushLogEntries(batch));
646
+ }
647
+ }, 200);
648
+
649
+ // Register Escape → stop play mode
650
+ _escapeListener = (e: KeyboardEvent) => {
651
+ if (e.key === 'Escape') exitPlayMode();
652
+ };
653
+ window.addEventListener('keydown', _escapeListener);
654
+
655
+ // Create game canvas inside the dedicated game container
656
+ if (!_gameContainerEl) {
657
+ editorConsole.error('Game container not mounted', 'play-mode');
658
+ // Tear down the console patch + flush interval + escape listener set up
659
+ // above, same as the other failure paths (ED8).
660
+ exitPlayMode();
661
+ return;
662
+ }
663
+ const gameContainer = _gameContainerEl;
664
+ const w = gameContainer.clientWidth;
665
+ const h = gameContainer.clientHeight;
666
+
667
+ const gameCanvas = document.createElement('canvas');
668
+ gameCanvas.style.cssText = 'width:100%;height:100%;display:block;';
669
+ gameContainer.appendChild(gameCanvas);
670
+ _gameCanvas = gameCanvas;
671
+
672
+ // setPlayState auto-switches to Game tab
673
+ store.setPlayState('playing');
674
+ editorConsole.log('Play mode started', 'play-mode');
675
+
676
+ try {
677
+ const sceneData = store.getDocument();
678
+ const project = getCurrentProject();
679
+
680
+ if (!project) {
681
+ editorConsole.error('No project open — open or create a project first', 'play-mode');
682
+ exitPlayMode();
683
+ return;
684
+ }
685
+
686
+ const { createGameRuntime } = await import('@engine/runtime/create-runtime');
687
+
688
+ // T3.3 slice 2 part C: a manifest-backed project (vgai.game.json present)
689
+ // plays through the resolver, against its first world — the manifest
690
+ // wins for game identity/mounting per docs/CLI-ON-FOLDER-DESIGN.md §1B.
691
+ // A legacy (project.json-only) project has no `/vgai.game.json` to fetch
692
+ // (404 -> null below) and falls through to today's scripts-based path,
693
+ // UNCHANGED. Browser mode (the hosted, no-dev-server build) never has
694
+ // this route served at all, so it always takes the legacy branch too.
695
+ const manifest =
696
+ getEditorMode() === 'browser' ? null : await fetchGameManifest().catch(() => null);
697
+ const firstWorld = manifest?.worlds[0];
698
+
699
+ // E4.R2 (reopen fix) — honest scoping of the three branches below,
700
+ // replacing the old blanket "editor-side because Vite-coupled" framing
701
+ // (`mount-manifest.ts`'s header comment, which is true of ADAPTER
702
+ // CONSTRUCTION generally but was being read as though it explained every
703
+ // branch here equally). Each branch has its OWN, different reason to
704
+ // live where it does:
705
+ // - The multi-world branch (below) is the one design/26 S-D D2 actually
706
+ // names ("editor play mode calls it, adapter-resolver keeps only
707
+ // editor-specific concerns: dev-server URLs, ingest session wiring")
708
+ // — it migrated onto the engine's `mountManifestWorlds` composer.
709
+ // - The single-manifest-world `else if (firstWorld)` branch stays HERE,
710
+ // permanently, NOT because of dev-server coupling but because it
711
+ // threads `editorPreview` (the live in-memory document + viewport
712
+ // camera, so unsaved edits play and the editor's own camera seeds the
713
+ // session) — a channel the engine composer must never grow, since it
714
+ // is inherently "there is exactly one editable document" (the
715
+ // editor's), which stops making sense the moment a manifest declares
716
+ // more than one world.
717
+ // - The no-manifest `else` branch (project.json-only legacy projects)
718
+ // stays HERE because there is no manifest world to resolve AT ALL —
719
+ // not a scoping choice, a structural one.
720
+ // - `ingest-mode.ts`/`module-mode.ts` (separate files, not this
721
+ // dispatch) still call `resolveAllWorlds` un-migrated — genuinely OUT
722
+ // of E4's scope (not named by it), left for a follow-on unit; their
723
+ // OWN dev-server `/@fs/` coupling for ingest session wiring and
724
+ // custom-module loading is real and correctly stays editor-side.
725
+ let session: GameSession;
726
+ if (manifest && (manifest.worlds.length > 1 || firstWorld?.kind === 'pixijs')) {
727
+ // T6.1 slice 2 (docs/MULTI-WORLD-DESIGN.md §3 slice 2): a manifest with
728
+ // MORE THAN ONE world mounts through the slice-1 worlds path — the
729
+ // single-canvas legacy path below is three-only (its `createGameRuntime`
730
+ // call constructs exactly one THREE renderer against one canvas) and
731
+ // cannot represent N stacked worlds. A LONE pixijs world routes here
732
+ // too: the legacy path's HostContext is three-shaped (≠ World2DHost),
733
+ // so it could resolve but never mount a pixi adapter — and lone pixi
734
+ // had no working legacy behavior to preserve (it THREW pre-slice), so
735
+ // this is new capability, not a stability break. A single-THREE-world
736
+ // manifest NEVER reaches this branch (the `else if (firstWorld)`
737
+ // branch below is BYTE-STABLE — same call shape as before this slice). The
738
+ // already-created single `gameCanvas` above is unused on this path —
739
+ // the composer's own host builds its OWN canvas stack directly inside
740
+ // `gameContainer` (`mountManifestWorlds`'s `MountManifestOptions
741
+ // .container`), so discard it rather than leave an orphaned extra
742
+ // canvas in the DOM.
743
+ gameCanvas.remove();
744
+ _gameCanvas = null;
745
+
746
+ // E4.R2 (reopen fix, design/26 S-D D2 — "editor play mode calls it,
747
+ // adapter-resolver keeps only editor-specific concerns: dev-server
748
+ // URLs, ingest session wiring"): per-world adapter construction stays
749
+ // HERE, editor-side, via `resolveAllWorldEntries` (which is itself just
750
+ // the scope-checked `resolveWorldAdapter` fan-out — the SAME dev-server
751
+ // `/@fs/` mechanics `resolveWorldAdapter` always used). What moved OFF
752
+ // this file is the WorldMountSpec ASSEMBLY (id/zOrder/pausable/loop +
753
+ // per-kind dispatch) that `resolveAllWorlds` used to hand-build for
754
+ // this call site — `mountManifestWorlds` below now does that from the
755
+ // manifest itself (`mount-manifest.ts`'s `buildWorldMountSpec`, the
756
+ // engine's own single authoritative implementation of that
757
+ // translation), so it is never duplicated here. `entries` (not the
758
+ // process-global `registerWorldKind` registry) is the right channel
759
+ // for an editor session specifically because play mode mounts a FRESH
760
+ // session on every play — registering into the global registry would
761
+ // double-register (and throw) on the second play; `entries` carries no
762
+ // such hazard since it is rebuilt from scratch every `enterPlayMode`.
763
+ const entries = await resolveAllWorldEntries(manifest, project.rootPath);
764
+ editorConsole.log(
765
+ `Resolved ${manifest.worlds.length} manifest worlds via the composer path: ` +
766
+ manifest.worlds.map((w) => `${w.id} (${w.kind})`).join(', '),
767
+ 'play-mode',
768
+ );
769
+ // Play may have been exited while resolving — don't boot a runtime for
770
+ // a play that is already over.
771
+ if (epoch !== _playEpoch) return;
772
+ // Re-read the container's real size instead of the `w`/`h` captured
773
+ // BEFORE `store.setPlayState('playing')` above (that capture can race
774
+ // the Game-tab layout switch and read 0x0). This still matters for the
775
+ // INITIAL render-buffer resolution (`mountManifestWorlds`'s own
776
+ // `width`/`height` default otherwise falls back to the manifest's
777
+ // `resolution`) even though it is no longer the ONLY thing standing
778
+ // between a 0x0 mount and a correctly-laid-out canvas: the worlds-path
779
+ // canvas's on-screen SIZE is now fully CSS-container-driven
780
+ // (`create-runtime.ts`'s `mountOneThreeWorld`, E4.R1 reopen fix), not
781
+ // pinned to whatever it mounted at.
782
+ const rw = gameContainer.clientWidth || w;
783
+ const rh = gameContainer.clientHeight || h;
784
+ const { mountManifestWorlds } = await import('@engine/runtime/mount-manifest');
785
+ session = await mountManifestWorlds({
786
+ manifest,
787
+ container: gameContainer,
788
+ entries,
789
+ width: rw,
790
+ height: rh,
791
+ });
792
+ } else if (firstWorld) {
793
+ // The SAME editorPreview the legacy branch builds below — live in-memory
794
+ // document (so unsaved edits play), current save path, viewport-camera
795
+ // spawn. The resolver threads it into whichever default-three branch it
796
+ // takes (entry -> setup(ctx, editorPreview), byte-equivalent to the
797
+ // legacy path; scene-only -> mounts sceneData, camera honored).
798
+ const manifestViewCam = _ctx.getViewportCamera();
799
+ const adapter = await resolveWorldAdapter(firstWorld, project.rootPath, {
800
+ editorPreview: {
801
+ scenePath: store.savePath,
802
+ sceneData,
803
+ viewportCamera: {
804
+ position: manifestViewCam.position.toArray() as [number, number, number],
805
+ quaternion: manifestViewCam.quaternion.toArray() as [number, number, number, number],
806
+ },
807
+ },
808
+ });
809
+ editorConsole.log(
810
+ `Resolved manifest world "${firstWorld.id}" (${firstWorld.adapter.identity})`,
811
+ 'play-mode',
812
+ );
813
+ // Play may have been exited while resolving — don't boot a runtime for
814
+ // a play that is already over.
815
+ if (epoch !== _playEpoch) return;
816
+ session = await createGameRuntime({ canvas: gameCanvas, width: w, height: h, adapter });
817
+ } else {
818
+ // Load project scripts and run with createGameRuntime.
819
+ // Server build: dev-server import of the on-disk project entry.
820
+ // Browser build: no dev server — load the pre-bundled scripts and transpile
821
+ // them in-browser (same esbuild-wasm path the Playground uses).
822
+ let scripts: ProjectScripts;
823
+ if (getEditorMode() === 'browser') {
824
+ const { loadBrowserProjectScripts } = await import('./browser-project-scripts');
825
+ scripts = await loadBrowserProjectScripts();
826
+ } else {
827
+ scripts = await loadProjectScripts(project.rootPath);
828
+ }
829
+ editorConsole.log('Loaded project scripts', 'play-mode');
830
+
831
+ const viewCam = _ctx.getViewportCamera();
832
+ // Play may have been exited while the scripts were loading — don't boot a
833
+ // runtime for a play that is already over.
834
+ if (epoch !== _playEpoch) return;
835
+ // Hosted example project (Track H): route play through the bundled
836
+ // example's own setup — or, for a `?scene=` override inside the project,
837
+ // play the open scene generically but keep its asset prefix. Pure
838
+ // decision in hosted-example.ts; empty object everywhere else.
839
+ const exampleDispatch =
840
+ getEditorMode() === 'browser'
841
+ ? exampleDispatchFor(
842
+ project.exampleId,
843
+ store.savePath,
844
+ new URLSearchParams(window.location.search).has('scene'),
845
+ )
846
+ : {};
847
+ session = await createGameRuntime({
848
+ canvas: gameCanvas,
849
+ width: w,
850
+ height: h,
851
+ setup: scripts.setup,
852
+ editorPreview: {
853
+ scenePath: store.savePath,
854
+ sceneData,
855
+ viewportCamera: {
856
+ position: viewCam.position.toArray() as [number, number, number],
857
+ quaternion: viewCam.quaternion.toArray() as [number, number, number, number],
858
+ },
859
+ ...exampleDispatch,
860
+ },
861
+ });
862
+ }
863
+
864
+ // Play was exited (Stop/Escape) — or re-entered — while the runtime was
865
+ // booting. This session belongs to a play that is already over: stop it
866
+ // and bail WITHOUT adopting its scene into the store. Adopting here left
867
+ // the editor showing an empty hierarchy in edit mode and leaked the
868
+ // session (RAF loop, Rapier world, WebGL context).
869
+ if (epoch !== _playEpoch) {
870
+ session.stop();
871
+ return;
872
+ }
873
+ _session = session;
874
+ // A2: browser mode has no Vite dev server, so no `vgai:script-update` HMR
875
+ // event ever fires (the `import.meta.hot` handler below is dead in the
876
+ // hosted build). Drive the SAME in-browser hot-swap from the storage
877
+ // backend's self-derived write events instead: any component source written
878
+ // through the backend is transpiled + prototype-swapped onto its live
879
+ // instances. Subscribed here (not at module load) so it binds the backend
880
+ // that is actually active by play time — after any File System Access pick
881
+ // has replaced the boot-default OPFS backend. Torn down in exitPlayMode.
882
+ if (getEditorMode() === 'browser') {
883
+ _unsubBrowserScriptWatch = watchScriptChanges(({ path, source }) => {
884
+ void applyBrowserComponentEdit(source).catch((err) =>
885
+ editorConsole.error(`In-browser hot-swap failed for ${path}: ${err}`, 'play-mode'),
886
+ );
887
+ });
888
+ }
889
+ // `session.game.worlds` is always present on a REAL `GameSession` (T6.1
890
+ // slice 1 — `create-runtime.ts` unconditionally sets `game`); the
891
+ // optional-chaining guard here is for the LEGACY/single-world play-mode
892
+ // unit tests (`adapter-resolver-play-path.test.ts`,
893
+ // `play-mode-multi-world.test.ts`), whose `fakeSession()` fixtures predate
894
+ // this slice and stub only the fields THEY need — never touch those
895
+ // pinned files, so this reads defensively instead.
896
+ _isMultiWorldSession = (session.game?.worlds.length ?? 1) > 1;
897
+ // Wave 20 (docs/MULTI-WORLD-DESIGN.md §1.F candidate improvement /
898
+ // MASTER-ARCHITECTURE-REVIEW §7.4 gap 4): surface WHICH persistence regime
899
+ // this session's edits are under. Derived from the SAME
900
+ // `_isMultiWorldSession` boolean the branch below uses to pick
901
+ // `installMultiWorldAuthoring` (overlay, real per-world persistence) vs
902
+ // `installPlayAuthoring` (ephemeral, discarded on stop) — never a second,
903
+ // independently-computed condition, so this can't drift from the actual
904
+ // provider that gets installed.
905
+ store.setPlayEditRegime(_isMultiWorldSession ? 'overlay' : 'ephemeral');
906
+
907
+ // Swap store to game scene so hierarchy/inspector show game entities.
908
+ // liveHierarchy: first-party play mode is the ONLY adoption path that may
909
+ // synthesize descriptors for untagged runtime objects (anti-shim rule —
910
+ // ingest/module mounts adopt foreign scenes and must never fabricate).
911
+ store.enterPlayScene(session.scene, { liveHierarchy: true });
912
+ if (_isMultiWorldSession) {
913
+ // T6.1 slice 3 (docs/MULTI-WORLD-DESIGN.md §1.F/§3 slice 3): N-ary
914
+ // Composite over one per-world adapter each, REAL (overlay) persistence —
915
+ // see installMultiWorldAuthoring's doc comment for why this differs from
916
+ // the ephemeral-wrapped single-world path below.
917
+ await installMultiWorldAuthoring(store, session.game!.worlds);
918
+ } else {
919
+ // Carry an ephemeral persistence provider for the duration of play (design
920
+ // docs/PERSISTENCE-PROVIDER-DESIGN.md §2/§7 slice 1) — see installPlayAuthoring.
921
+ installPlayAuthoring(store);
922
+ }
923
+
924
+ // Wire physics sync THROUGH the first-party `RapierPhysicsAdapter` (Phase E):
925
+ // the gizmo path freezes a Rapier-owned body on beginEdit, commits the edited
926
+ // pose each frame (the callback below), and unfreezes on release — so the
927
+ // postPhysics writer doesn't snap the gizmo edit back. The editor speaks only
928
+ // the `PhysicsAdapter` interface, never Rapier directly.
929
+ // Install the GAME-scoped System-adapter aggregate (§7.1-3, probe1) — every
930
+ // world's `mounted.systems` merged (first registration wins per key), NOT
931
+ // just the default world's own copy (`firstParty(session).systems`, the
932
+ // pre-fix read) — so a networking/etc. adapter registered from ANY world's
933
+ // setup is visible to the editor's gizmo path + inspector panels.
934
+ // `session.game` is absent on the legacy fake-session fixtures some
935
+ // play-mode unit tests use (predate T6.1's `game` field) — optional-chain
936
+ // through so those keep passing unmodified, same fallback as before.
937
+ const systems = session.game?.systemAdapters ?? {};
938
+ setActiveSystems(systems);
939
+
940
+ // T6.3: gate game input (raw `window`/`document` listeners in game code via
941
+ // gated-globals, AND the default world's first-party InputManager) to only
942
+ // fire while play is actually running AND the Game tab is the focused
943
+ // viewport — so keystrokes typed into the editor (Scene tab, inspector
944
+ // fields) don't leak into the running game. `session.game` is absent on the
945
+ // legacy fake-session fixtures some play-mode unit tests use (predate T6.1's
946
+ // `game` field) — optional-chain through so those keep passing unmodified.
947
+ const gameInputActive = () =>
948
+ store.playState === 'playing' && store.activeViewportTab === 'game';
949
+ setGameInputGate(gameInputActive);
950
+ // `Game.input` is a THROWING getter, not an optional field — it delegates
951
+ // to the default world's first-party ctx and throws when that mount has
952
+ // no `firstParty` brand (`game.ts`'s `requireFirstPartyCtx`). Every
953
+ // scene/setup world mounts through `VgaiSceneGameAdapter` (branded), so
954
+ // this never fired before W1 — but an `adapter`-export default-three
955
+ // world (docs/R3F-FIRST-PARTY-DESIGN.md §1.A, e.g. the R3F bridge) owns
956
+ // its whole mount and correctly has no first-party `InputManager`. There
957
+ // is nothing to wire in that case: the gated-globals half of T6.3
958
+ // (`setGameInputGate` above) still fences raw window/document listeners;
959
+ // only the InputManager half is absent.
960
+ let gameInput: NonNullable<typeof session.game>['input'] | undefined;
961
+ try {
962
+ gameInput = session.game?.input;
963
+ } catch {
964
+ gameInput = undefined;
965
+ }
966
+ const syncInputEnabled = () => gameInput?.setEnabled(gameInputActive());
967
+ syncInputEnabled();
968
+ _unsubStore = store.subscribe(syncInputEnabled);
969
+
970
+ const physics = systems.physics;
971
+ store.setEcsSyncTransform((obj) => {
972
+ physics?.commit(obj, {
973
+ position: obj.position.toArray() as [number, number, number],
974
+ rotation: obj.quaternion.toArray() as [number, number, number, number],
975
+ scale: obj.scale.toArray() as [number, number, number],
976
+ });
977
+ });
978
+
979
+ // Container may have been display:none when w/h were read above.
980
+ // Now that the session exists, do one resize with the actual dimensions.
981
+ const actualW = gameContainer.clientWidth;
982
+ const actualH = gameContainer.clientHeight;
983
+ if (actualW > 0 && actualH > 0 && (actualW !== w || actualH !== h)) {
984
+ session.resize(actualW, actualH);
985
+ }
986
+ } catch (err) {
987
+ const msg = `Play mode failed to start: ${err}`;
988
+ editorConsole.error(msg, 'play-mode');
989
+ // Roll back only if THIS play is still the live generation — if it was
990
+ // already exited during boot (epoch moved on), a second exitPlayMode here
991
+ // could tear down a newer play that started in the meantime.
992
+ if (epoch === _playEpoch) exitPlayMode();
993
+ throw new Error(msg);
994
+ }
995
+ }
996
+
997
+ /**
998
+ * Exit play mode: stop game, remove canvas, re-enable editor.
999
+ */
1000
+ export function exitPlayMode(): void {
1001
+ if (!_ctx) return;
1002
+ // Invalidate any enterPlayMode still awaiting its runtime boot (see
1003
+ // _playEpoch): its session must be abandoned, not adopted.
1004
+ _playEpoch++;
1005
+ const { store } = _ctx;
1006
+
1007
+ // Restore editor scene before stopping (stop disposes game scene children)
1008
+ store.setEcsSyncTransform(null);
1009
+ setActiveSystems(null);
1010
+ store.exitPlayScene();
1011
+ if (_isMultiWorldSession) exitMultiWorldAuthoring();
1012
+ restorePriorAuthoring();
1013
+ _isMultiWorldSession = false;
1014
+ // Readout-only — clear the regime the moment play is no longer active
1015
+ // (mirrors _isMultiWorldSession's own reset just above). Optional-chained:
1016
+ // some pre-existing play-mode unit tests (e.g. play-mode-teardown.test.ts's
1017
+ // ED8 no-container path) pass a minimal hand-built fake store that predates
1018
+ // this signal and stubs only the methods THEY need — never touch those
1019
+ // pinned fixtures, so this degrades gracefully instead (same convention as
1020
+ // this file's other `session.game?.` guards for legacy fixtures).
1021
+ store.setPlayEditRegime?.(null);
1022
+
1023
+ // Always null _session even if stop() throws — otherwise enterPlayMode's
1024
+ // `if (_session) return` guard would permanently block re-entering play mode.
1025
+ if (_session) {
1026
+ try {
1027
+ _session.stop();
1028
+ } catch (err) {
1029
+ editorConsole.error(`Error stopping play session: ${err}`, 'play-mode');
1030
+ } finally {
1031
+ _session = null;
1032
+ }
1033
+ }
1034
+
1035
+ // Remove game canvas
1036
+ _gameCanvas?.remove();
1037
+ _gameCanvas = null;
1038
+
1039
+ // Cleanup subscriptions
1040
+ _unsubStore?.();
1041
+ _unsubStore = null;
1042
+ _unsubBrowserScriptWatch?.();
1043
+ _unsubBrowserScriptWatch = null;
1044
+
1045
+ // T6.3: no game running — game input (raw window/document listeners AND the
1046
+ // InputManager sync above) should never be suppressed again until the next
1047
+ // play session re-gates it.
1048
+ setGameInputGate(() => true);
1049
+
1050
+ _resizeObserver?.disconnect();
1051
+ _resizeObserver = null;
1052
+
1053
+ if (_escapeListener) {
1054
+ window.removeEventListener('keydown', _escapeListener);
1055
+ _escapeListener = null;
1056
+ }
1057
+
1058
+ // Log before clearing sink so this message gets persisted
1059
+ editorConsole.log('Play mode stopped', 'play-mode');
1060
+
1061
+ // Flush remaining log entries and end session
1062
+ if (_flushInterval) {
1063
+ clearInterval(_flushInterval);
1064
+ _flushInterval = null;
1065
+ }
1066
+ editorConsole.setSink(null);
1067
+ if (_pendingEntries.length > 0) {
1068
+ const batch = _pendingEntries.splice(0);
1069
+ _logFlushChain = _logFlushChain.then(() => flushLogEntries(batch));
1070
+ }
1071
+ // The server has one active log file. End it only after every queued batch
1072
+ // has reached the append endpoint, otherwise Stop can race the final fetch
1073
+ // and silently discard the most useful end-of-run evidence.
1074
+ _logFlushChain = _logFlushChain.then(() => endLogSession());
1075
+
1076
+ unpatchConsole();
1077
+
1078
+ // setPlayState('stopped') auto-switches back to Scene tab
1079
+ store.setPlayState('stopped');
1080
+ }
1081
+
1082
+ /**
1083
+ * Pause play mode: freeze game, allow editor inspection.
1084
+ */
1085
+ export function pausePlayMode(): void {
1086
+ if (!_session || !_ctx) return;
1087
+ _session.pause();
1088
+ _ctx.store.setPlayState('paused');
1089
+
1090
+ editorConsole.log('Play mode paused', 'play-mode');
1091
+ }
1092
+
1093
+ /**
1094
+ * Resume play mode from pause.
1095
+ */
1096
+ export function resumePlayMode(): void {
1097
+ if (!_session || !_ctx) return;
1098
+
1099
+ _session.resume();
1100
+ _ctx.store.setPlayState('playing');
1101
+
1102
+ editorConsole.log('Play mode resumed', 'play-mode');
1103
+ }
1104
+
1105
+ /**
1106
+ * Step one fixed-timestep frame while paused.
1107
+ */
1108
+ export function stepPlayMode(): void {
1109
+ if (!_session) return;
1110
+ _session.step();
1111
+ }
1112
+
1113
+ // --- Console patching ---
1114
+
1115
+ function patchConsole(): void {
1116
+ _originalConsoleLog = console.log;
1117
+ _originalConsoleInfo = console.info;
1118
+ _originalConsoleWarn = console.warn;
1119
+ _originalConsoleError = console.error;
1120
+
1121
+ console.log = (...args: unknown[]) => {
1122
+ _originalConsoleLog?.apply(console, args);
1123
+ if (!_engineLogActive) editorConsole.log(args.map(String).join(' '), 'game');
1124
+ };
1125
+ console.info = (...args: unknown[]) => {
1126
+ _originalConsoleInfo?.apply(console, args);
1127
+ if (!_engineLogActive) editorConsole.log(args.map(String).join(' '), 'game');
1128
+ };
1129
+ console.warn = (...args: unknown[]) => {
1130
+ _originalConsoleWarn?.apply(console, args);
1131
+ if (!_engineLogActive) editorConsole.warn(args.map(String).join(' '), 'game');
1132
+ };
1133
+ console.error = (...args: unknown[]) => {
1134
+ _originalConsoleError?.apply(console, args);
1135
+ if (!_engineLogActive) editorConsole.error(args.map(String).join(' '), 'game');
1136
+ };
1137
+ }
1138
+
1139
+ function unpatchConsole(): void {
1140
+ if (_originalConsoleLog) console.log = _originalConsoleLog;
1141
+ if (_originalConsoleInfo) console.info = _originalConsoleInfo;
1142
+ if (_originalConsoleWarn) console.warn = _originalConsoleWarn;
1143
+ if (_originalConsoleError) console.error = _originalConsoleError;
1144
+ _originalConsoleLog = null;
1145
+ _originalConsoleInfo = null;
1146
+ _originalConsoleWarn = null;
1147
+ _originalConsoleError = null;
1148
+ }