@vgai/editor 0.2.0 → 0.4.0-canary.20260715.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 (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. package/vite-plugin-react-world-provider.ts +0 -81
@@ -0,0 +1,1110 @@
1
+ # AGENTS.md — VGAI Game Project
2
+
3
+ ## The acceptance bar: feature-complete, bot-proven, great-looking
4
+
5
+ This project is not done at a vertical slice. It is done when the **whole
6
+ game** — every feature in the game's maximal spec/ROADMAP, enumerated up
7
+ front — ships, and every one of those features has a **bot-played
8
+ acceptance criterion** proving it works on the real client (see "Autoplay
9
+ bots" below). Two bars, non-negotiable:
10
+
11
+ - **Completeness.** Write the maximal spec/ROADMAP before or alongside the
12
+ first line of code — every feature the game needs, not just the ones
13
+ that are easy or exciting. **No descopes, no "for now," no
14
+ vertical-slice-and-ship.** A core-only build with the rest deferred to
15
+ "later" is not done; it's a partial delivery wearing a done label. If
16
+ scope turns out to be wrong, revise the spec explicitly and say so — do
17
+ not quietly narrow it by shipping less.
18
+ - **Visual quality.** "Great-looking" is a claim you DEMONSTRATE, never one
19
+ you assert. Every visual milestone ships a real, inspectable artifact —
20
+ screenshots and a gameplay video captured from the actual running client
21
+ (`npm run game`, not a mockup) — judged against this game's own
22
+ reference/style bar. "It renders" or "looks good" in prose is not proof;
23
+ if you can't point at the recording, it isn't shown.
24
+
25
+ Both bars are proven the same way the autoplay-bot doctrine below proves
26
+ everything else: **the bot plays the real game and the run is the
27
+ evidence.** A skill/feature isn't complete until a bot has played it
28
+ end-to-end and the logs/recording show it working — that's what turns
29
+ "I built X" and "X looks great" from a claim into a demonstrated fact on
30
+ the real product surface.
31
+
32
+ ## Before Writing Any Code
33
+
34
+ 0. **Use this project's matching CLI, then launch the editor and keep it running.**
35
+ In a source-linked scaffold, invoke CLI commands as `npm run vgai -- <command>`
36
+ (for example, `npm run vgai -- edit .` or `npm run vgai -- screenshot`).
37
+ That npm script runs the CLI from the same checkout as `@vgai/engine`;
38
+ a plain global `vgai` may belong to another worktree and must not be trusted.
39
+ The other npm scripts already delegate through it, so `npm run dev` / `npm run
40
+ test:acceptance` are safe. `npm run vgai -- edit .` (run it in the
41
+ background) — the editor is the authoring surface AND the verification
42
+ loop: `npm run vgai -- status` / `npm run vgai -- play` /
43
+ `npm run vgai -- select` / `npm run vgai -- apply-diff` and
44
+ the `/editor` skill all drive it. Let it auto-open the browser — do NOT
45
+ pass `--no-open` unless you're certain no human may be watching (that flag
46
+ is for CI/no-human contexts). On WSL, auto-open already reaches the
47
+ WINDOWS browser (it hands the URL to `powershell.exe Start-Process`), so
48
+ running headless in WSL is not by itself a reason to suppress it. ALSO
49
+ tell the user the URL `vgai edit` prints (the scaffold-generated preferred
50
+ port is stored in `vgai.game.json`) so they can watch the game
51
+ take shape — but "telling the user" only counts through a channel they
52
+ actually see (the user-facing chat/terminal, or the opened browser
53
+ itself); a note left in a subagent transcript or internal log the user
54
+ never reads does NOT satisfy this. The editor is ALSO the surface
55
+ you SHOW the user: when they ask to run/see/open the game, that means
56
+ the editor's Play mode — present `npm run game` (the standalone
57
+ player-view page) only when they explicitly ask for the game by
58
+ itself, or for bot/acceptance runs. Recent conversation topic never
59
+ changes this default. This step applies to agents
60
+ too, headless or not: the editor still runs and auto-open still fires (or
61
+ you explicitly report — through a user-visible channel — that the URL was
62
+ only printed, not opened, if it genuinely can't be); if part of the
63
+ editor loop genuinely doesn't fit your game (e.g. fully procedural world,
64
+ no scene to author), say so explicitly instead of silently skipping the
65
+ editor. Hand-editing project files (scenes, manifest) is equally
66
+ legitimate authoring now that the dev server validates every save
67
+ immediately and surfaces failures loudly — terminal, editor console, and
68
+ `vgai status` (#103) — so treat the editor's essential agent role here as
69
+ observation/verification (play, status, screenshot), not the only
70
+ legitimate way to write files. If a `.vscn.json` you hand-edit is the
71
+ editor's currently-open scene, it live-reloads automatically PROVIDED the
72
+ editor has no unsaved changes; if it does, the edit is held back (never
73
+ silently clobbered or silently dropped) and a loud notice appears in the
74
+ editor console telling you to save or discard first — watch for that
75
+ notice rather than assuming a hand edit always shows up immediately.
76
+
77
+ **Port identity:** every scaffold gets a random, persisted
78
+ `vgai.game.json.editorPort`, so projects normally reopen at distinct, stable
79
+ URLs. `vgai edit` will not silently retarget another project's running
80
+ editor; if the preferred port is occupied, it falls forward to a free port
81
+ and prints THAT URL. If you (or the user) already have
82
+ another vgai project's editor tab open in the browser, don't assume it's
83
+ this project — read the printed URL/port, or pass `vgai edit --port <n>`
84
+ to pin a specific port for this project, so an old tab left open from a
85
+ different project doesn't get mistaken for this session (see the
86
+ `/editor` skill's CLI Reference for `--port` and `vgai sessions`).
87
+ 1. **Choose explicit adapter roots before authoring.** Every v2 manifest has
88
+ a non-empty `roots` array. Each root declares its adapter directly:
89
+ `"adapter": "react"` for DOM/layout UI and board/card/menu games,
90
+ `"adapter": "pixijs"` for sprite/canvas gameplay, and
91
+ `"adapter": "threejs"` for spatial 3D. There is no game-level or root-level
92
+ `kind` field. The default template already declares Three.js + React because
93
+ every game needs UI; use `vgai create --template 2d` for PixiJS + React or
94
+ `--template react` for React-only. JSX is always a declared React root—never
95
+ mount a second React tree from Three.js setup code. The chosen roots must
96
+ appear honestly in the editor hierarchy. Root discovery is manifest-driven;
97
+ source location is only supporting evidence for validation. Do not call
98
+ `createRoot`, `hydrateRoot`, or legacy `ReactDOM.render` in project source—the
99
+ generated host owns React mounting for every declared React root. A React-
100
+ only game also does **not** add a Three.js/PixiJS root merely to obtain an
101
+ `InputManager`: its real input path is the DOM, driven in playtests through
102
+ `game.page(async (page) => page.keyboard.press(...))` or locator clicks.
103
+ 2. **Read an example.** A plain scaffold ships only a minimal default scene — no example content. The worked implementations (FPS, vehicle, third-person, RTS, multiplayer arena, IK, one-concept tutorials — see the Examples Catalog below) are standalone projects in the vgai monorepo's `examples/<id>/`. Scaffold one directly to read/copy its code: `npx create-vgai-project <name> --example <id>` (e.g. `--example third-person`) makes a correctly-rewritten standalone copy of that example, `vgai.game.json` and all. You can also open any of them live in the hosted editor at `https://vgai-editor.pages.dev/?project=<id>` (read-only — scaffold to keep edits). Otherwise, build gameplay from the Components section below.
104
+ 3. **Read the schema.** Scene format is defined by the engine's Zod schemas; the generated JSON Schema is `schemas/vscn.schema.json` inside the engine package (your tsconfig `paths` / vite alias points at the engine — the schema sits next to its `src/`). Read it before writing or editing any `.vscn.json` file. Wrong field names (e.g. `type` instead of `bodyType` in physics) silently break things. Do **not** author fields the runtime doesn't read: `gamepad_axis_pair`/`mouse_move` input bindings — these (like dead `.vscn` fields) **throw at parse** — the engine ships Zod schemas for every fetched asset format (`inputmap.schema.json` sits beside `vscn.schema.json` in the engine package). Discrete character animation (idle/walk/run/jump) is an XState machine + `bindXStateAnimation` in your own component code, not a `.vscn` field — see the engine's `packages/engine/src/animation/xstate-animation-binding.ts` and a scaffolded `--example third-person`'s `src/components.ts`.
105
+ 4. **Read `src/scripts/registry.ts`.** Every component used in a scene must be registered here. Unregistered components fail with "Component not found in registry".
106
+
107
+ ### Procedural humanoids and character animation
108
+
109
+ The source-linked engine includes a procedural, skinned humanoid capability at
110
+ `@engine/humanoid`: `generateHumanoid`, `createHumanoidClipLibrary`,
111
+ `skinToHumanoid`, `attachToBone`, and `restJointPosition`. It generates the
112
+ visible body; animation inputs remain ordinary `THREE.AnimationClip`s. This is
113
+ a capability boundary, not a character recipe: body shape, visual identity,
114
+ movement model, camera, controls, state names, and clip set all belong to the
115
+ game.
116
+
117
+ **The default humanoid is not a styled game character.** Its multicolored
118
+ capsule/part material is an engineering reference visualization for checking
119
+ the skeleton, proportions, skinning, and animation. Do not ship that default
120
+ look unchanged for a prominent player character unless the brief deliberately
121
+ calls for it. Changing proportion parameters creates a different body type but
122
+ does not, by itself, create visual identity. Make an explicit project-owned
123
+ styling choice before treating the character as finished: at minimum a
124
+ coherent material treatment, or ordinary Three.js clothing/accessories built
125
+ with `skinToHumanoid` and `attachToBone`. The engine intentionally does not
126
+ prescribe what that style should be.
127
+
128
+ Before choosing runtime generation for a persistent player/NPC, inspect the
129
+ project command catalog with `npm run vgai -- run --list --json`. Installed
130
+ libraries may advertise ordinary author-time commands through their own
131
+ `package.json`; the engine's humanoid package contributes a bake command that
132
+ can produce a normal GLB + `.prefab.json` + provenance, with optional
133
+ retargeted clips. Prefer that authored prefab when the character should exist
134
+ in the scene before Play. Runtime `generateHumanoid()` remains correct for
135
+ genuinely dynamic/spawned characters and previews. Commands are capabilities,
136
+ not game-design prescriptions—the baked asset still uses the reference body's
137
+ unstyled default material until the game deliberately restyles/composes it,
138
+ and the body, controls, camera, and behavior remain the game's own decisions.
139
+
140
+ For gameplay animation, preserve these contracts:
141
+
142
+ - A `prePhysics` controller owns movement and exposes corrected speed,
143
+ grounded state, and latched actions. A separate `animation`-phase
144
+ `GameComponent` reads that state, drives an ordinary XState actor, and ticks
145
+ `bindXStateAnimation`. Never advance `rig.update`, `AnimationMixer.update`,
146
+ or a binding's `tick` from the controller.
147
+ - Rapier's kinematic character-controller query treats sensors as movement
148
+ hits by default. Pass `ctx.rapier.QueryFilterFlags.EXCLUDE_SENSORS` as the
149
+ third argument to `computeColliderMovement`; the character then crosses the
150
+ sensor and the normal physics step delivers `onTriggerEnter/Exit`. Do not
151
+ replace that event contract with per-frame distance polling.
152
+ - Resolve every required gameplay clip during an awaited `async init()` and
153
+ let a missing clip reject initialization. `GameComponent.init()` supports
154
+ promises; do not bypass that readiness guarantee with `void this.loadClips()`.
155
+ Do not catch a missing locomotion/action clip and silently substitute or
156
+ downgrade it.
157
+ - Direct `rig.playClip` + `rig.update` is the small path for an isolated
158
+ preview, cinematic, or simple one-shot—not a second gameplay transition
159
+ system.
160
+ - `rig.dispose()` cleans the generated hierarchy, clothing, and attached props.
161
+ A material passed into `generateHumanoid(..., { material })` remains
162
+ caller-owned and must also be disposed by the caller.
163
+ - Acceptance evidence samples the animation state while honest input is held
164
+ and captures representative moving poses; a screenshot taken after release
165
+ proves only the return-to-idle frame.
166
+ - Keep provenance beside any copied clip asset: source, author/license where
167
+ applicable, exact upstream path or catalog ID, and a content hash.
168
+
169
+ The full API and retargeting policy are in the engine checkout's
170
+ `docs/PROCEDURAL-HUMANOID.md`. The rules above intentionally do not prescribe
171
+ a particular humanoid design or game concept.
172
+
173
+ ## Commands
174
+
175
+ ```bash
176
+ npm run vgai -- edit . # Launch the editor for this project (opens browser)
177
+ npm run vgai -- play # Enter play mode
178
+ # Play-mode (and ingest-mode) edit policy:
179
+ # - LIVE-BUT-DISCARDED: gizmo transforms, rename,
180
+ # visibility, environment tuning — applied to the
181
+ # running scene, thrown away on stop.
182
+ # - BLOCKED (console warning): saving (Cmd/Ctrl+S),
183
+ # structural ops (add/delete/cut/copy/paste/reparent),
184
+ # inspector edits that rebuild the object (mesh,
185
+ # material, physics, ...), undo/redo, opening files.
186
+ # Nothing you change during play can leak into your
187
+ # .vscn. Stop play mode to persist edits.
188
+ npm run vgai -- restart # Remount every game root without reloading the editor page.
189
+ # Component HMR swaps prototypes and preserves live instances;
190
+ # it does NOT rerun init(). Restart after changing init-time
191
+ # registrations, listeners, generated resources, or ownership.
192
+ npm run vgai -- screenshot # LOOK AT THE GAME — PNG of the RUNNING game (canvas + HUD,
193
+ [--out <path>] # composited), written to .vgai/screenshots/ and its path printed.
194
+ # Needs play mode running; fails loudly rather than writing a
195
+ # blank/editor-only image. Game STATE can report a healthy
196
+ # "playing" phase while the canvas is blank or double-rendered —
197
+ # only the pixels catch that. See "Autoplay bots" below.
198
+ npm run vgai -- status # Check editor state
199
+ npm run vgai -- apply-diff <scene.vscn.json> <patch.json> [--report]
200
+ # The canonical programmatic edit op: apply a
201
+ # JSON SceneDiff patch (add/remove/update/move ops +
202
+ # environment/name) to a scene file. Validates the
203
+ # scene, the patch, AND the result through the engine's
204
+ # Zod schemas, and dereferences every asset reference
205
+ # the diff introduces (existence for all; schema parse
206
+ # for .mat/.prefab refs) — nothing is
207
+ # written on any failure.
208
+ # --report: validate + summarize only, never write.
209
+ # Prefer this over hand-editing .vscn JSON in scripts.
210
+ npm run dev # Open the EDITOR on this project (delegates to `vgai edit .`) —
211
+ # the authoring surface; press Play (or `vgai play`) to run the game
212
+ npm run game # Vite dev server for the STANDALONE game page (no editor) — the
213
+ # ship-shaped form, for player-view testing/bots/CI, not authoring
214
+ # (show it to the user only on their explicit request — "see the
215
+ # game" means the EDITOR's Play mode)
216
+ npm test # Logic tests (vitest, tests/logic/) — headless, seconds per run
217
+ npm run playtest # Inner-loop autoplay: a plain @vgai/live script (npx tsx) driving
218
+ # the live `vgai edit` session/tab you already have open — no
219
+ # second browser, no committed spec. See "Autoplay bots" below.
220
+ npm run vgai -- probe # Committed acceptance suite (tests/acceptance/*.spec.ts) — the ONE
221
+ # way to bot-PROVE a feature for the record; see "Autoplay bots"
222
+ # below. (`npm run test:acceptance` is the same.) Plays inside a
223
+ # live `vgai edit` session's tab by default when one exists, else
224
+ # against the standalone game page (`--standalone` forces the
225
+ # latter, `--in-editor` requires the former).
226
+ # doctor vs probe: `vgai doctor` asks "does this project mount in
227
+ # the editor?"; `vgai probe` asks "do this game's acceptance specs
228
+ # pass?"
229
+ npm run build # tsc + vite build
230
+ npm run typecheck # Type-check (includes tests/tsconfig.json)
231
+ ```
232
+
233
+ ## Project Structure
234
+
235
+ ```
236
+ public/
237
+ scenes/ .vscn.json scene files (entities, transforms, components)
238
+ audio/ Audio assets
239
+ src/
240
+ main.ts Entry point — creates game runtime
241
+ runtime/ Scaffold-owned standalone host wiring; game roots do not edit this
242
+ data/ Data assets: <name>.schema.ts + emitted .schema.json + .data.json
243
+ (tuning.* ships as the worked example; see src/data/README.md)
244
+ operations/ Project automation: <name>.operation.ts thin wrappers around ordinary
245
+ functions — Node-discovered, schema-validated, never auto-bundled
246
+ tools/ Project tools: <name>.tool.tsx editor panels (center document or
247
+ inspector section) — editor-only, never bundled
248
+ (example.tool.tsx + spin.tool.tsx ship; see src/tools/README.md)
249
+ scripts/
250
+ registry.ts Component registry (GameComponent classes)
251
+ components/ Custom GameComponent classes
252
+ ui/ React UI kit, pages, live connectors, and colocated CSF stories
253
+ server/
254
+ rooms/ Colyseus multiplayer rooms
255
+ vgai.game.json Sole project manifest: identity, roots, editor port, build + authoring config
256
+ ```
257
+
258
+ ## Key Concepts
259
+
260
+ The project has a small set of fundamental concepts, each with one home:
261
+
262
+ - **Roots** — explicit runtime adapter mounts in
263
+ `vgai.game.json.roots[]`. Every root has an `id` and an `adapter`
264
+ (`threejs`, `react`, `pixijs`, or a module/ingest adapter object carrying
265
+ its `surface`). Roots are required and are never inferred or synthesized.
266
+ The editor projects them as `Game → Three.js / React / PixiJS`, introducing
267
+ named instances when one surface has multiple roots.
268
+ - **Hierarchy projection** — optional editor-only organization in
269
+ `vgai.game.json.authoring.hierarchy`; it labels roots and can group references
270
+ across surfaces (for example an `Overworld` group containing a Three.js scene
271
+ root and a React menu root). It never loads, owns, copies, pauses, or disposes
272
+ runtime roots. Example:
273
+ ```json
274
+ {
275
+ "authoring": {
276
+ "hierarchy": {
277
+ "rootLabels": { "main": "World Scene", "ui": "Menu Page" },
278
+ "groups": [{ "id": "overworld", "label": "Overworld", "roots": ["main", "ui"] }]
279
+ }
280
+ }
281
+ }
282
+ ```
283
+ The visible tree is semantic and adapter-declared: a root owns its active
284
+ document, a React CSF document owns its stories, and the active story owns
285
+ the live component/DOM subtree. Keep primary names source-owned (scene file,
286
+ CSF title/story, component boundary, entity); do not encode state or infer
287
+ behavior from magic words in ids/labels.
288
+ - **Scenes** — optional content assets owned by a scene-capable world adapter,
289
+ not peer game roots. `.vscn.json` files define Three.js entity hierarchies;
290
+ the default Three adapter loads the selected asset. **Prefabs**
291
+ (`.prefab.json`) are reusable entity templates.
292
+ - **GameComponents** — *how things behave*: per-entity classes with
293
+ `init`/`update`/`dispose` and a `static schema` (Zod) for per-entity config
294
+ (see Components below).
295
+ - **Loop hooks** — *when things run*: per-frame system phases `input` →
296
+ `prePhysics` → `physics` → `postPhysics` → `gameLogic` → `animation` →
297
+ `preRender` → `render`, via `GameComponent.phase` or `ctx.systems.add`.
298
+ **Every `setup()` must return a `dispose()`** that cleans up all objects,
299
+ bodies, listeners, and UI.
300
+ - **Data** — *what things are*: schema'd JSON in `src/data/`, the layer the
301
+ inspector (and designers, and VS Code) edit — shared/global definitions and
302
+ tuning, as opposed to per-entity component config. See "Tunable values → a
303
+ data asset" under One Canonical Way.
304
+ - **Tools** — *how you tune it*: project-authored React panels
305
+ (`src/tools/*.tool.tsx`) the editor discovers automatically. Placements:
306
+ `placement: 'document'` (a center workspace document, opened from the
307
+ command palette — `'dock'` is a deprecated alias — project-global
308
+ dashboards), `placement: 'utility'` (a bottom utility tab for transient
309
+ output), and `placement: 'inspector'` with a
310
+ `match: (node, adapter) => boolean` predicate (a ⚙ section in the Inspector
311
+ for matching selections). Each tool file exports `tool = defineTool({...})`
312
+ (from `@engine/tools/define-tool`) plus a default React component; build
313
+ the panel from `@editor/widgets` (the editor's own widget kit + THEME
314
+ tokens) so it looks native. `src/tools/example.tool.tsx` (document) and
315
+ `spin.tool.tsx` (inspector) ship as worked examples — both live-tune
316
+ `src/data/tuning.data.json` through `useData`/`writeData`
317
+ (`@engine/react/use-data`) and the running game follows via HMR. An
318
+ inspector tool can also read the selected entity's OWN component config —
319
+ `useSelection()` (`@engine/react/use-selection`) returns the selected node,
320
+ its attached components, and each component's authored config, live.
321
+ Tools are editor-only dev surfaces, stripped from shipped games by
322
+ construction: `npm run build` bundles only what `index.html` →
323
+ `src/main.ts` imports, and nothing in game code imports `src/tools/`.
324
+ - **Operations** — *how reusable project automation becomes discoverable*:
325
+ ordinary JS/TS functions remain the implementation and may be imported
326
+ directly by game code. A thin `src/operations/*.operation.ts` module exports
327
+ `operation = defineOperation({...})` from `@vgai/sdk/registry`, declaring
328
+ Zod input/result schemas, host, mutation, and permission metadata. No manual
329
+ registry or manifest entry exists: the editor scans the folder and exposes a
330
+ Project Commands catalog plus a default JSON/schema runner. Agents use `npm run vgai -- run --list --json`
331
+ to discover capabilities and `npm run vgai -- run <name>` to invoke one
332
+ (add `--input <file>` and `--yes` when needed). Keep the wrapper free of
333
+ React/editor imports; a custom interface is an optional
334
+ `src/tools/*.tool.tsx` that calls it through `@vgai/editor-sdk`. Files it
335
+ persists under `public/` are ordinary project assets and refresh the Assets
336
+ navigator after a successful mutation; the tool itself is not an asset. A
337
+ generator is simply an operation whose native function generates content,
338
+ not a new engine kind. See `src/operations/README.md`.
339
+
340
+ ## Components
341
+
342
+ All per-entity behavior is a **GameComponent class** — an OOP class with an `init`/`update`/`dispose` lifecycle. The Object3D *is* the entity (there is no ECS / no `eid`): inside a component, `this.object3D`, `this.rigidBody`, and `this.collider` are wired up for you. **Every component referenced by a scene must be registered in `src/scripts/registry.ts`** — unregistered components cause "Component not found in registry" at play time. `update` is mandatory (not optional like `init`/`dispose`) by deliberate design — components are for *behavior*: if you find yourself writing an empty `update(_dt, _ctx) {}` body, that's a signal the class may not need to be a GameComponent at all (a pure state-holder with only `init`/event methods usually belongs as a plain object instead).
343
+
344
+ | | GameComponent Class |
345
+ |---|---|
346
+ | **What** | OOP class extending `GameComponent` with lifecycle (`init`/`update`/`dispose`, optional `onTriggerEnter`/`onTriggerExit`) |
347
+ | **Use for** | All gameplay behavior — controllers, cameras, AI, pickups, tags |
348
+ | **Access** | `this.object3D` / `this.rigidBody` / `this.collider`; config fields from `static schema` (Zod) |
349
+ | **Editable fields** | Declare a `static schema = z.object({...})` with `.describe()` + `.default()` — these become typed, documented widgets in the editor inspector and are validated on load |
350
+ | **HMR** | Yes (prototype swap preserves instance state on live edit) |
351
+ | **Reference** | `src/scripts/components/scene-camera.ts` (`SceneCamera` — declares `static phase` + `static schema`) in this project; for richer patterns, scaffold a worked example (`npx create-vgai-project <name> --example fps`, `--example vehicle`, `--example third-person`, ...) and read its `src/components.ts`; base class at `packages/engine/src/ecs/game-component.ts` |
352
+
353
+ **Before creating a component:** read the reference above (or a scaffolded example's, per the "Read an example" step) and copy the pattern (`static phase`, `static schema`, `init`/`update`). Don't author ad-hoc `ctx.systems.add` closures for gameplay — they're neither editable nor hot-reloadable; reserve `systems.add` for non-gameplay infra (audio mixers, network send, etc.).
354
+
355
+ **Procedural games (no scene file):** components also attach at runtime to
356
+ code-created objects — construct the instance yourself and call
357
+ `ctx.components.attach(object3D, instance)`; the component manager runs `init`
358
+ and the per-frame lifecycle exactly as for scene-authored components. A fully
359
+ procedural world (voxel terrain, roguelike floors, …) legitimately has a
360
+ manifest world with an `entry` and **no `scene`**, and skips `.vscn` authoring
361
+ entirely — registry registration is only *required* for components referenced
362
+ by scene files. The editor still works on such games: play mode adopts the
363
+ live scene graph into the hierarchy/inspector.
364
+
365
+ Declaring an `entry` module on a manifest world needs **no edit to
366
+ `src/main.ts`**: the scaffold registers the engine's stock threejs adapter-surface
367
+ factory, so an entry module exporting `setup` (or `adapter`) just mounts. One
368
+ rule to know: an `entry` world's `setup()` **owns scene loading** — if the
369
+ world also declares `scene`, call `loadScene()` on it yourself inside
370
+ `setup()` (the automatic scene mount applies only to roots with no `entry`).
371
+
372
+ **Registration:** `src/scripts/registry.ts` exports two maps:
373
+ - `componentRegistry` — **all** GameComponent classes referenced by scene files (used by the loader + scene validator)
374
+ - `gameComponents` — the same classes, for HMR
375
+
376
+ **In scene JSON**, components are an object keyed by name: `"components": { "Checkpoint": {}, "MarbleController": { "forceStrength": 12 } }`. Names must match `componentRegistry` keys; field names must match the component's `static schema`. Empty `{}` for tag components (no schema).
377
+
378
+ **System phases** (for `ctx.systems.add` and `GameComponent.phase`): `input` → `prePhysics` → `physics` → `postPhysics` → `gameLogic` (default) → `animation` → `preRender` → `render`
379
+
380
+ ## One Canonical Way Per Task
381
+
382
+ For each common job there is exactly ONE recommended approach. Use it — competing paths cause ambiguity and non-determinism.
383
+
384
+ - **Camera → a camera GameComponent.** Attach a component that positions/aims `ctx.camera` (the shipped patterns to copy: `FPSCamera` in a `--example fps` scaffold's `src/components.ts`, `ChaseCamera` in `--example vehicle`, `CameraTarget` in `--example third-person` — all schema-declared and driven in `preRender`). This project also registers a minimal `SceneCamera` (`src/scripts/components/scene-camera.ts` — schema-declared position + look-at target). Precedence is fixed and intentional:
385
+
386
+ `editor viewport camera < scene-defined camera < camera GameComponent`
387
+
388
+ The editor's viewport transform is only the *initial* viewpoint, so a scene-defined or component-driven camera always wins (this is why play mode uses your scene's camera, not the editor's orbit camera). Do **not** also write an ad-hoc `ctx.systems.add('preRender', ...)` camera closure — put the camera logic in the component.
389
+
390
+ - **Gameplay behavior → a GameComponent.** Never an ad-hoc `ctx.systems.add` closure (those are neither editable nor hot-reloadable). Reserve `systems.add` for non-gameplay infra only (audio mixers, network send, debug overlays). See the Components section above.
391
+
392
+ - **Tunable values → a data asset, never a constant.** If a designer (or you,
393
+ while tuning) should be able to change a value without editing game code, it
394
+ lives in `src/data/` as JSON governed by a Zod schema — not as a constant in
395
+ a component or `setup()`. Per-entity config stays in the component's
396
+ `static schema` (see Components); data assets are the shared/global layer:
397
+ feel tuning, economy numbers, tables of game definitions.
398
+
399
+ Two blessed shapes, and only these (they're what the editor's DATA tab can
400
+ render as real widgets): a **singleton** — one `z.object` config whose
401
+ fields are scalars (number/string/boolean/enum), optionally grouped one or
402
+ two levels deep in nested `z.object`s (the tab renders groups as sub-forms)
403
+ — or a **table** — `z.record(z.string(), rowSchema)` with a flat row schema,
404
+ rendered as a grid (this Zod version's `z.record` takes a key schema AND a
405
+ value schema — the single-arg `z.record(rowSchema)` shorthand from Zod 3
406
+ throws here). Anything the tab can't type (arrays, deep nesting, unions)
407
+ degrades to a raw-JSON cell that refuses invalid commits — prefer
408
+ flattening, or a table, when a designer should tune it. Tables also get a
409
+ spreadsheet-friendly bulk-edit path: the DATA tab's **Export CSV** /
410
+ **Import CSV** buttons round-trip the whole table (header `key,<row
411
+ fields>`). Import is a WHOLE-TABLE REPLACE (a row the sheet doesn't mention
412
+ is deleted) and ATOMIC (any cell that fails to coerce to its field's kind —
413
+ e.g. non-numeric text in a `number` column — writes NOTHING; fix the sheet
414
+ and re-import). `"$schema"` is never a CSV column; it's carried over from
415
+ the file automatically.
416
+
417
+ ```ts
418
+ // src/data/tuning.schema.ts — engineer-owned: the contract
419
+ import { z } from 'zod'
420
+ export const tuningSchema = z.object({
421
+ gravity: z.number().min(0).max(50).default(9.8).describe('m/s², downward'),
422
+ })
423
+ ```
424
+ ```jsonc
425
+ // src/data/tuning.data.json — designer-owned: the values
426
+ { "$schema": "./tuning.schema.json", "gravity": 9.8 }
427
+ ```
428
+ ```ts
429
+ // src/data/tuning.ts — the live handle (engine defineData, W1)
430
+ import { defineData } from '@engine/data/data-asset'
431
+ import raw from './tuning.data.json'
432
+ import { tuningSchema } from './tuning.schema'
433
+ export const tuning = defineData(tuningSchema, raw, 'src/data/tuning.data.json')
434
+ // ['default'] not .default: the template tsconfig's
435
+ // noPropertyAccessFromIndexSignature applies to Vite's ModuleNamespace
436
+ import.meta.hot?.accept('./tuning.data.json', (m) => tuning.hotSwap(m?.['default']))
437
+ ```
438
+ Read `tuning.get()` live where the value is used (e.g. each frame) — don't
439
+ copy fields out at init, or HMR edits won't reach them. `subscribe(fn)` fires
440
+ on every hot edit. **This project ships the pattern working**: the trio in
441
+ `src/data/` (`tuning.schema.ts` / emitted `tuning.schema.json` /
442
+ `tuning.data.json`), the handle (`src/data/tuning.ts`), and a consumer
443
+ (`DataSpinner` on the default scene's Box — press ▶, edit the JSON, watch it
444
+ change live). Add a data asset by copying that trio AND registering it in
445
+ `src/data/assets.ts` (the one shared list); after every schema change run
446
+ `npm run emit-schemas` (commits the emitted `.schema.json` so VS Code
447
+ validates the data file with no dev server). Rows in table-shaped assets
448
+ are referenced as `"file#key"` strings — declare a cross-asset field with
449
+ `dataRef('items')` (`import { dataRef } from './data-ref'` — see
450
+ `src/data/data-ref.ts`, never `@engine/data/data-ref` or
451
+ `@vgai/engine/data/data-ref` directly: a schema file loads through
452
+ `vite.config.ts` at Vite config-load time, which only bundles relative
453
+ imports) instead of a bare `z.string()`: the emitted schema gets a real
454
+ `pattern` AND the editor's Data tab renders it as a `stem#key` PICKER (a
455
+ plain string still works, but has no picker and doesn't survive a
456
+ target-file rename in `vgai doctor`'s checks). Read the referenced row
457
+ with `getRef(itemsHandle, enemy.drop)` (also from `./data-ref`) — pass the
458
+ TARGET's own handle, not a stem string; it resolves live off `.get()`, same
459
+ HMR doctrine as everything else in this file. Data assets are immutable at
460
+ runtime — runtime state lives in your game's own state, never written back
461
+ into data files. Enforcement is automatic: `npm run build` fails loud on a
462
+ data file that no longer matches its schema, a dangling `"file#key"` ref (or,
463
+ for a declared `dataRef` field, a target file that no longer exists at all),
464
+ or tool code reaching the shipped bundle, and `vgai doctor <folder>` runs
465
+ the same data checks terminal-side.
466
+ (The editor edits these files in place — each data asset opens as its own
467
+ center document — and project
468
+ tools (`src/tools/`, see Key Concepts) tune them live through the same
469
+ file-write path. WSL note: if this project lives on `/mnt/c` (drvfs — no
470
+ inotify), polling is turned on AUTOMATICALLY — both the editor dev server
471
+ and this project's standalone `npm run game` detect the drvfs mount and
472
+ poll for changes with no setup needed, so external edits to `.data.json`
473
+ hot-reload into the running game either way. `VGAI_WATCH_POLL` is only an
474
+ explicit override, for the cases auto-detection doesn't cover: `=1`/`=<ms>`
475
+ forces polling on (e.g. a non-drvfs path that still needs it), `=0` forces
476
+ it off. Both servers honor the same variable the same way.)
477
+
478
+ - **Material → inline `material` OR `materialRef`, never both.** An entity uses one or the other: inline `material` for a one-off look, or `materialRef` pointing at a shared `.mat.json` when several entities share a material. Setting both is a hard load-time validation error (enforced by a Zod refine in the engine schema) — there is no silent precedence to memorize.
479
+
480
+ ## Rules
481
+
482
+ - **The engine is source-linked, not copied.** `@vgai/engine` resolves as a
483
+ symlink back into the vgai checkout you scaffolded from (a `file:`
484
+ dependency plus `@engine/*` aliases into `<checkout>/packages/engine/src`).
485
+ The engine source is yours to read — but **editing it edits the shared
486
+ checkout**, instantly affecting every project scaffolded from it. A game
487
+ that truly needs a divergent engine forks the checkout. Your manifest's
488
+ `engine.version` records which engine version this project was scaffolded
489
+ or last upgraded against; after updating the checkout, run `vgai upgrade`
490
+ (report first; `--report` for report-only) to re-sync untouched template
491
+ files and re-pin — your own edits are never overwritten.
492
+ **drvfs caveat (WSL, project on /mnt/c):** npm materializes the `file:`
493
+ dependency as a COPY there, not a symlink — after engine changes, code that
494
+ resolves through `node_modules/@vgai/engine` (root scripts like
495
+ `emit-schemas.ts`/`validate-scenes.ts`) sees a STALE engine until you
496
+ refresh it: `rm -rf node_modules/@vgai/engine && npm install`. The
497
+ `@engine/*` tsc/vite aliases always see the live checkout and are
498
+ unaffected.
499
+ - **Use engine/library APIs directly.** No wrappers around Three.js or Rapier.
500
+ - **GameComponents are for gameplay entities only.** Static scenery, lights, and UI are plain Three.js objects.
501
+ - **React UI is always a manifest adapter root with `"adapter": "react"` + an
502
+ `entry` that default-exports a component.** JSX is truth; React roots have no
503
+ scene file, and GameComponents cannot attach to React nodes. Position UI
504
+ against its contained root layer and do not portal to `document.body`, which
505
+ escapes host composition. The default template already includes a React root. Never
506
+ call `createRoot`, `hydrateRoot`, or `ReactDOM.render` in project source—the
507
+ host owns the one React lifecycle for every declared root.
508
+
509
+ **A first-party React root always uses the ecosystem-native three-file
510
+ split:**
511
+ ```text
512
+ game-page.tsx pure page: props + callbacks, no Game dependency
513
+ game-page-world.tsx thin live Game/useGameState connector; default export
514
+ game-page-world.stories.tsx portable CSF3 fixtures for stopped-mode design states
515
+ ```
516
+ Add the connector path as the React root's `entry` in `vgai.game.json`.
517
+ That is the complete host wiring: the scaffold's Vite config generates
518
+ static imports directly from the manifest's declared `entry` paths, and
519
+ the engine's optional React adapter entry point owns the standard
520
+ `createRoot` + `GameProvider` lifecycle. Do not add an import table or
521
+ duplicate a React adapter in game code.
522
+ Associate the CSF module with the manifest root through Storybook's standard
523
+ parameter extension point:
524
+ ```tsx
525
+ const meta = {
526
+ component: GamePage,
527
+ parameters: { vgai: { root: 'ui', defaultStory: 'Playing' } },
528
+ } satisfies Meta<typeof GamePage>;
529
+ ```
530
+ Use ordinary CSF `args`, decorators, loaders, and `play()` functions; the
531
+ editor composes the story with Storybook itself. Edit mode mounts the
532
+ selected/default story in the main viewport. Play replaces it with the live
533
+ connector; Stop restores the story. Do not create `src/ui/registry.ts`, a
534
+ fake `Game`, or a VGAI-specific story object. `vgai validate`/`vgai doctor`
535
+ warns when a first-party React root has no associated CSF module.
536
+
537
+ React is ordinary React—normal props/hooks/context/`useState`, normal inline
538
+ styling—with no second UI data language.
539
+
540
+ **Input/pointer:** each React root's DOM layer is `pointer-events: none` by default so
541
+ clicks fall through to the canvas underneath; an interactive element opts
542
+ in with `pointer-events: auto` (only that element — and its subtree —
543
+ becomes clickable, everything else stays click-through):
544
+ ```tsx
545
+ <button style={{ pointerEvents: 'auto' }} onClick={onFire}>Fire</button>
546
+ ```
547
+ **No emoji glyphs in HUD text** (meteor-dodge dry-run): the headless
548
+ Chromium `vgai probe` runs under has NO color-emoji font, so `❤️`-style
549
+ glyphs render as empty tofu boxes in every probe screenshot (and on any
550
+ machine without an emoji font). Use inline SVG icons for hearts/stars/etc.
551
+ — a vector shape renders identically in probe, the editor, and any
552
+ browser.
553
+
554
+ The host calls `root.unmount()` when the adapter root is disposed; project
555
+ code does not implement or duplicate this cleanup.
556
+
557
+ To bridge live game state (a singleton gameplay component's score/lives/wave)
558
+ into the UI, don't poll—the host already wraps the root in `GameProvider`.
559
+ Import `useGameState` directly from `@engine/react/game-state` and select,
560
+ for example, `g.queryByComponent(MyComponent)[0]?.field`. The selector
561
+ re-runs at most once per frame. Worked reference: `hp-hud/src/hud.tsx`. (This project's
562
+ `vite.config.ts` ships `resolve.dedupe: ['react', 'react-dom']` so the
563
+ source-linked engine's react bridge resolves the SAME physical `react` as
564
+ this project — without it, `<GameProvider>` crashes with `Invalid hook
565
+ call` from two react copies; don't remove that `dedupe` entry.)
566
+ - **Multiplayer → Colyseus directly.** vgai deliberately has no engine-level networking wrapper. Typed `joinOrCreate<T>` + the v0.17 Callbacks API (`onAdd`/`onChange`/`onRemove`; per-field `listen` — but see the P2P caveat below before using it); dynamic-import `@colyseus/sdk` so single-player bundles stay clean. Read live schema properties inside `onChange`; never destructure schema fields into local copies. The worked reference is `network.ts` in a `--example third-person-arena` scaffold (raw Callbacks + an ambient type mirror of the server schema; the engine's old `net/` wrapper is deleted).
567
+ - **P2P mode (browser-hosted rooms) → write dual-target code.** Your room can
568
+ also run browser-hosted over WebRTC via `@vgai/p2p-colyseus`: build/serve
569
+ with `VGAI_P2P=1` (this project's `vite.config.ts` aliases the Colyseus
570
+ packages onto browser-safe compat shims only then) and launch with
571
+ `?net=p2p-host` / `?net=p2p-join&roomId=…`. The same room class still runs
572
+ on a real server (`npm run server`), so keep room + client code inside the
573
+ two runtimes' intersection — full rules with rationale in the engine
574
+ checkout's `docs/P2P-COLYSEUS-MIGRATION.md` ("Dual-Target Portability
575
+ Rules"); the short list:
576
+ - No `$.listen` in the compat layer — read root-state fields inside
577
+ `room.onStateChange` (use `.once(...)`, not its return value: the real
578
+ SDK returns an EventEmitter, the compat an unsubscribe function).
579
+ - No `this.clock` on the compat `Room` — drive all timing from
580
+ `setSimulationInterval` (tick counters for delays).
581
+ - Never send messages from `onJoin` (dropped under P2P) — catch-up is
582
+ client-initiated: register handlers, then send a `sync_req`; reply in
583
+ bounded batches.
584
+ - Keep bulky world data OUT of room state (the P2P runtime diffs the whole
585
+ state per client per tick) — state carries players/actors + small
586
+ scalars; the world is a seed + an event log sent as messages.
587
+ - Relay budget ~30 messages/peer/sec, 16 KB max envelope — throttle moves
588
+ (~10 Hz) and batch bulk sync under the cap.
589
+
590
+ ## Editor Control
591
+
592
+ Use the `/editor` skill to control the visual editor (select entities, focus camera, play/stop, toggle display modes). The skill documents the `vgai` CLI and `@vgai/editor-sdk`.
593
+
594
+ ### Quick examples:
595
+ ```bash
596
+ # Select and focus an entity after editing its scene file
597
+ npm run vgai -- select <entity-id> && npm run vgai -- focus <entity-id>
598
+
599
+ # Test gameplay
600
+ npm run vgai -- play
601
+
602
+ # Re-run init() and remount all roots after init-time code changes
603
+ npm run vgai -- restart
604
+
605
+ # LOOK at what you just changed (state can lie; pixels can't)
606
+ npm run vgai -- screenshot
607
+
608
+ # Check editor state
609
+ npm run vgai -- status
610
+ ```
611
+
612
+ ## Autoplay bots — testing your game in cycles
613
+
614
+ The highest-leverage test harness for a vgai game is a **synthetic player**:
615
+ a bot that plays the game the way a human does. The full doctrine and the
616
+ evidence behind it live in the engine checkout's
617
+ `docs/SYNTHETIC-PLAYER-SPEC.md` and `docs/ACCEPTANCE-DRIVER-SPEC.md`; this
618
+ section is the practical playbook.
619
+
620
+ **The principle: synthesized input in, observable state out, no backdoors.**
621
+ For canvas roots the bot presses the same virtual actions; for React-only DOM
622
+ games it drives the same keyboard and interactive elements through
623
+ `game.page(...)`. It reads the same game state a player sees. It never
624
+ teleports the player, calls gameplay methods directly,
625
+ or writes inventory outside a declared test fixture. To the extent the bot
626
+ cannot cheat, every *gameplay* failure it hits is a real player-facing
627
+ defect — a physics gap, a missing line-of-sight check, a UI-sequencing bug.
628
+
629
+ **Three levels, one vocabulary.** `game.state`/`game.input.*`/`game.page`/`game.waitFor`/
630
+ `game.events.expect`/`game.command`/`game.screenshot` (the `GameClient`
631
+ surface, `@vgai/probe`) mean the same thing everywhere — what changes across
632
+ the three levels below is HOST (where the bot plays) and PERMANENCE (does
633
+ this run live in a file you keep). Be honest with yourself about which one
634
+ you actually need:
635
+
636
+ 1. **Logic** — pure functions, `tests/logic/`, run with `npm test` (vitest,
637
+ no browser). Seconds per run. Most bugs belong here — see "Test in
638
+ layers" below.
639
+ 2. **Inner-loop autoplay** — a plain script using `@vgai/live`, run with
640
+ `npx tsx`. No test runner, no Playwright config, no fixture, no second
641
+ browser: it drives the SAME live `vgai edit` session/tab you (or the
642
+ human watching) already have open. This is what you reach for to answer
643
+ "does my game work right now" — which is most of the time you're
644
+ iterating. Nothing here is committed or re-run later; it's scratch.
645
+ 3. **Committed acceptance** ("Acceptance tests", below) — `tests/acceptance/*.spec.ts`,
646
+ run by `vgai probe`. Same vocabulary as level 2, but a FILE you keep, that runs again
647
+ on every future change and reports a verdict. This is what proves a
648
+ feature is actually done.
649
+
650
+ The tradeoff is real, not sales copy: level 2 is disposable and fast to
651
+ write but proves nothing to anyone who isn't watching; level 3 is what you
652
+ commit, and what CI (or a future you) re-runs. Use level 2 constantly while
653
+ building a feature, then graduate the interaction that proves it works into
654
+ a level-3 spec before calling the feature done.
655
+
656
+ ### Level 2 — inner-loop autoplay with `@vgai/live`
657
+
658
+ `@vgai/live` is `{ editor, game, page }` over the SAME session wire `vgai
659
+ probe --in-editor` uses (and, since probe now defaults to that transport
660
+ whenever a session is open, the same wire `vgai probe` uses by default too)
661
+ — a plain node/tsx script, not a test file, not a fixture, no config. It
662
+ never starts a session; it only ATTACHES to one already running (`vgai edit`
663
+ — you likely have this open already), and every method rejects with a
664
+ "run `vgai edit` first" error if none exists.
665
+
666
+ ```ts
667
+ // scripts/playtest.ts (see the worked copy already in this project)
668
+ import { editor, game } from '@vgai/live';
669
+
670
+ await editor.play(); // enters play mode; waits for the game's full async setup
671
+ await game.command('spinner.reset'); // fixture (declared cheat) — jump to a known state
672
+ await game.input.whileHeld('attack', async () => { // proof + evidence while input is active
673
+ await game.waitFor((s) => (s('spinner') as { speed: number }).speed > 2,
674
+ { simSeconds: 10 });
675
+ await game.screenshot('during-boosted-spin');
676
+ });
677
+ ```
678
+
679
+ Run it with `npx tsx scripts/playtest.ts`. No browser of its own boots, no
680
+ Vite cold start, no watchdog racing a fresh game boot — it rides the tab
681
+ that's already open, so whoever is watching that tab sees the bot play live.
682
+ This is the loop: edit source, `npx tsx scripts/playtest.ts`, read the
683
+ output, repeat — seconds per cycle, not the minutes a fresh standalone boot
684
+ costs.
685
+
686
+ Everything the "Acceptance tests" (level 3) section below says about `game.command` never being a substitute
687
+ for honest `game.input.*` or React-native `game.page(...)` interaction (D16), sim-time-only budgets, and the
688
+ one-registry/three-doors seam applies here unchanged — `@vgai/live`'s `game`
689
+ is a `GameClient`, the identical type `vgai probe` specs use. What it is
690
+ NOT: a substitute for a committed spec. Nothing written against `game` here
691
+ is re-run later, checked into `tests/acceptance/`, or seen by CI — it is the
692
+ scratch space you iterate in before writing the level-3 spec that actually
693
+ proves the feature.
694
+
695
+ **React-only DOM games:** do not invent a canvas root for testing. Use the
696
+ cross-host page door; it is real Playwright on the standalone host and a
697
+ relay-safe DOM shim in the already-open editor tab:
698
+
699
+ ```ts
700
+ await game.page(async (page) => {
701
+ await page.keyboard.press('ArrowLeft');
702
+ await page.getByRole('button', { name: 'Restart' }).click();
703
+ });
704
+ await game.waitFor(
705
+ (s) => (s('board') as { moves: number }).moves >= 1,
706
+ { simSeconds: 5 },
707
+ );
708
+ ```
709
+
710
+ The `game.page` callback is a serialization boundary in the live-editor
711
+ relay. Do not close over imported `expect`, helpers, or outer variables inside
712
+ it. Inline interaction values, return observations from the callback, and
713
+ assert outside it:
714
+
715
+ ```ts
716
+ const boardText = await game.page((page) => page.getByTestId('board').textContent());
717
+ expect(boardText).toContain('2048');
718
+ ```
719
+
720
+ The relay-safe common subset is `keyboard`, `locator`, `getByRole`,
721
+ `getByText`, `getByTestId`, `click`, `fill`, `evaluate`, and locator
722
+ `click`/`fill`/`textContent`/`count`/`waitFor`/`first`/`nth`/`ariaSnapshot`.
723
+ Unsupported Playwright members fail immediately with the supported list;
724
+ they never silently no-op.
725
+
726
+ Pair this with `useDebugProvider` in the React root. `game.input.*` remains
727
+ the right door for action-map input owned by Three.js/PixiJS adapters;
728
+ `game.page(...)` is the honest path when the game itself is DOM-native.
729
+
730
+ ### After you change something, LOOK at it — `vgai screenshot`
731
+
732
+ ```bash
733
+ npm run vgai -- play # the game must be RUNNING to have pixels to capture
734
+ npm run vgai -- screenshot # writes .vgai/screenshots/<timestamp>.png, prints the path
735
+ npm run vgai -- screenshot --out shots/after-hazard-fix.png
736
+ ```
737
+
738
+ **Game state can lie. Pixels can't.** This is not a style preference — it is
739
+ a defect class that has already bitten, twice, in one session:
740
+
741
+ - A `vgai play` on an ALREADY-playing session left a **ghost runtime** alive:
742
+ the old game's components kept ticking and rendering alongside the new
743
+ one's. Three hazards rendered as six. Debug commands reached only one of
744
+ the two instances while the ghost kept killing the player.
745
+ - A re-entered play mode once came up **stalled at tick 0 with a completely
746
+ blank canvas** — while `game.state` cheerfully reported `phase: "playing",
747
+ lives: 3`. Every state read said healthy. The game was dead.
748
+
749
+ In both cases the state seam reported a healthy, plausible game, and only a
750
+ screenshot showed the truth. So: after any change you believe is visible —
751
+ LOOK. It's one command.
752
+
753
+ `vgai screenshot` captures the RUNNING GAME through the same
754
+ `bridge-screenshot` seam `game.screenshot()` uses: the full stack — canvas(es)
755
+ PLUS the HUD/react DOM layers, composited — not the editor viewport (a
756
+ viewport grab would happily hand you a HUD-less, possibly not-even-playing
757
+ image). If play mode isn't running it fails loudly and writes **nothing**,
758
+ rather than saving a blank frame you might later mistake for evidence. From a
759
+ `@vgai/live` script, `await game.screenshot('label')` is the same capture,
760
+ and returns the path it wrote.
761
+
762
+ ### Acceptance tests — level 3, the one canonical way to bot-PROVE a feature
763
+
764
+ <!-- Keep the literal "### Acceptance tests" in this heading. It is not
765
+ decorative: @vgai/probe's GAME_SEAM_NEVER_APPEARED error tells a stranded
766
+ user "see 'Acceptance tests' in AGENTS.md" (packages/vgai-probe/src/errors.ts,
767
+ pinned by errors.test.ts), run-probe.ts cites it, and so do
768
+ docs/ACCEPTANCE-DRIVER-SPEC.md and -BUILD-PLAN.md. Renaming it orphans a
769
+ real error message. packages/editor/test/template-agent-doctrine.test.ts
770
+ guards this. -->
771
+
772
+
773
+ **This is the ONE way to bot-prove a feature for the record: acceptance
774
+ specs in `tests/acceptance/*.spec.ts`, driven through `@vgai/probe`'s `game`
775
+ fixture, run by `vgai probe`. Do not hand-roll window globals, `?bot=` query
776
+ params, or your own Playwright boot scripts.** The runner owns Playwright,
777
+ the browser binary, and the config — never run `npx playwright test`
778
+ directly (the fixture refuses to run outside `vgai probe`, and this project
779
+ never needs `playwright install`). Your project contains spec files, plus
780
+ two devDependencies you should never need to touch: `@vgai/probe` (ships
781
+ compiled output — a real dependency, not raw TypeScript) and
782
+ `@playwright/test` (a real, versioned devDependency of YOUR project — `vgai
783
+ probe` resolves and spawns it directly, never `npx`'s registry fallback).
784
+ The project's own `.npmrc` pins `install-links=true`: this keeps exactly one
785
+ physical `@playwright/test` install reachable no matter how the project was
786
+ installed (docs/PROBE-EXTERNAL-INSTALL-DESIGN.md) — don't remove it, and
787
+ don't hand-edit either dependency's version without re-running `npm
788
+ install`.
789
+
790
+ **`vgai probe` picks its HOST automatically, and always narrates which one it
791
+ picked.** If a live `vgai edit` session already covers this project, `vgai
792
+ probe` plays the spec INSIDE that session's own tab — the same relay
793
+ transport `@vgai/live` (level 2, above) uses, so there's no second browser,
794
+ no fresh Vite/game boot, and it's visible in the tab you're already
795
+ watching; the first line printed always names the host and, for the
796
+ in-editor case, the URL to watch. Only when no session exists does it fall
797
+ back to the standalone host: its own Chromium against `npm run game` (the
798
+ ship-shaped artifact) with the debug bridge enabled (`?vgai-debug=1`
799
+ installs `window.__vgai` in dev builds; production builds keep it off unless
800
+ `vgai.game.json` sets `debug.allowInProduction`). `--standalone` forces the
801
+ standalone host even with a session open; `--in-editor` forces the relay and
802
+ fails loudly if there's no session to use — see `vgai probe --help`.
803
+ Disambiguation: `vgai doctor` asks "does this project mount in the editor?";
804
+ `vgai probe` asks "do this game's acceptance specs pass?"
805
+
806
+ **The loop (4 steps):**
807
+
808
+ 1. **Register the seam in the owning component** —
809
+ `ctx.debug.registerStateProvider(name, fn)` / `ctx.debug.registerCommand(name, spec, fn)`
810
+ / `ctx.debug.emit(event, detail)`: the component that owns the state
811
+ exposes it, right where it lives (`ctx.debug` is optional-chained because
812
+ a bare test `Game` may not carry a registry; a real runtime always does):
813
+ ```ts
814
+ init(ctx: GameContext) {
815
+ ctx.debug?.registerStateProvider('spinner', () => ({ laps: this.laps }));
816
+ // Fixture, never a proof (D16) — a declared cheat for test setup:
817
+ ctx.debug?.registerCommand('spinner.reset', { description: 'Reset laps.', locus: 'client' },
818
+ () => { this.laps = 0; });
819
+ }
820
+ update(dt: number, ctx: GameContext) {
821
+ if (lapJustCompleted) ctx.debug?.emit('spin-lap', { laps: this.laps });
822
+ }
823
+ ```
824
+ Worked example: `src/scripts/components/data-spinner.ts`. Two built-in
825
+ providers exist with zero game code: `time` (`{ simSeconds, tick }`) and
826
+ `input.actions`. Once the project declares a Colyseus room, every command
827
+ must declare `locus: 'client' | 'server'` — server-locus commands run on
828
+ the authoritative copy, not client prediction. In a react world or HUD
829
+ component, use the hooks instead: `useDebugProvider(name, fn)` /
830
+ `useDebugCommand(name, spec, fn)` / `const emit = useDebugEmit()` directly
831
+ from `@engine/react/game-state` — the same game-scoped registry, with
832
+ providers/commands auto-unregistered on unmount. Emit events with
833
+ `emit('spin-lap', { laps })`; never import or reach through the internal
834
+ debug registry from game code.
835
+ 2. **Copy `tests/acceptance/example.spec.ts`** and swap in your own
836
+ provider/command/action names. (If it's missing, `vgai probe --init`
837
+ scaffolds it plus `tests/tsconfig.json` — idempotent, never overwrites.)
838
+ 3. **Run `vgai probe`** — or, to run just one spec while iterating,
839
+ `vgai probe tests/acceptance/foo.spec.ts` / `vgai probe -g "<title>"`
840
+ (both pass through to Playwright). The game server is kept warm between
841
+ runs (`--fresh` boots a new one; `vgai close` tears it down; probe runs
842
+ show up in `vgai sessions`). `--junit` emits a JUnit XML report for CI.
843
+ Host selection (in-editor vs. standalone) is covered above; the
844
+ `--headed`/`--headless` flags below are a SEPARATE, standalone-only axis
845
+ — they're meaningless (and warned as ignored) once probe is playing
846
+ inside a live editor tab, since that mode has no browser of its own to
847
+ launch headed or headless. `--headed` runs the standalone host's browser
848
+ visibly so a human can watch the synthetic player live (needs a real
849
+ display — an X server or desktop session on WSL). **Mode selection is
850
+ automatic and loud, not silent**: on the standalone host, with no
851
+ `--headed`/`--headless` flag, `vgai probe` runs HEADED by default
852
+ whenever a live `vgai edit` session is already open for this project
853
+ (there's a human plausibly watching) and `CI` is unset — otherwise it
854
+ runs headless (CI-identical). Either way the very first line printed
855
+ names the mode and why (e.g. `probe: headed (live editor session on
856
+ :5173) — watch the synthetic player; --headless to opt out`, or `probe:
857
+ headless (no live editor session for this project) — run \`vgai edit\`
858
+ and re-run with --headed to watch`) — never guess which mode a run used,
859
+ read that line. An explicit `--headed` or `--headless` always wins over
860
+ the default, verbatim.
861
+ 4. **Read the failure block.** Every driver-originated failure carries:
862
+ sim-elapsed vs wall-elapsed with the ratio, the tick at failure, the
863
+ predicate source text, the last state snapshot the predicate saw, the
864
+ last tick-stamped events, an absolute screenshot path, and captured
865
+ console/page errors. A probe failure with NO state evidence attached is a
866
+ harness bug, not your game's. `vgai probe --last-failure` re-prints the
867
+ last run's failure blocks with zero boots; artifacts (screenshots,
868
+ videos, `last-run.json`) land in `.vgai/probe/last-run/` — which the NEXT
869
+ run wipes (Playwright's own `outputDir` behavior). Before that wipe, the
870
+ PREVIOUS run's `last-run/` is archived to `.vgai/probe/prev-run/` (one
871
+ generation back, plain rename, no accumulation) — so a green run's
872
+ screenshots survive at least one more `vgai probe` before disappearing;
873
+ check `prev-run/` if you needed evidence from two runs ago.
874
+
875
+ **Budget wall-clock for long sim waits:** under headless probe (SwiftShader
876
+ software rendering, no GPU) the sim often runs at ~0.3× real time — a spec
877
+ that waits `simSeconds: 30` can take ~100s of wall clock. That is not a hang:
878
+ give outer timeouts headroom (a full suite with one long survival spec can
879
+ legitimately run 3+ minutes), and where possible prove long-duration rules
880
+ with a declared fixture command (jump the clock/state) plus ONE honest
881
+ end-to-end spec, rather than many real-time waits.
882
+
883
+ Exit codes: 0 all pass · 1 spec failure (including a `tests/` type error) ·
884
+ 2 usage (including ZERO specs — an empty suite never reports green) · 3 the
885
+ page loaded but no debug bridge appeared · 5 the harness itself wedged (the
886
+ wedge watchdog treats a throttled bridge-call heartbeat — printed while
887
+ calls keep flowing, e.g. across many back-to-back `hold()`s in a long
888
+ honest-navigation spec — as liveness, so real progress with sparse stdout
889
+ never false-positives as a wedge).
890
+
891
+ A spec is plain Playwright with one import:
892
+
893
+ ```ts
894
+ import { test, expect } from '@vgai/probe';
895
+
896
+ test('boosting the spinner completes a lap', async ({ game }) => {
897
+ await game.command('spinner.reset'); // fixture (declared cheat)
898
+ await game.input.hold('attack', { simSeconds: 4 }); // proof: the honest input path
899
+ await game.waitFor((s) => (s('spinner') as { laps: number }).laps >= 1,
900
+ { simSeconds: 10 });
901
+ await game.events.expect(['spin-lap']); // ordered subsequence since test start
902
+ await game.screenshot('after-boosted-spin');
903
+ });
904
+ ```
905
+
906
+ For a helper function taking the `game` fixture as a parameter, type it with
907
+ the `GameClient` type, exported from `@vgai/probe` for exactly that purpose.
908
+
909
+ **Fixtures are never proofs (D16).** `game.command(...)` invokes a declared
910
+ cheat — use it to jump to a known starting state, never to demonstrate that
911
+ a feature works. The proof is always the honest path: `game.input.*` drives
912
+ adapter action bindings, while a React-only game uses `game.page(...)` for its
913
+ native DOM keyboard/controls. Assert either through `game.waitFor`
914
+ (durable state) and `game.events.expect` (transient facts). `vgai probe`'s
915
+ pre-boot lint FAILS a spec whose only interactions are commands.
916
+
917
+ In a long honest-input spec (many `hold()` calls wandering toward several
918
+ targets), assert final/aggregate counters with at-least bounds (`>=`)
919
+ rather than exact equality — honest wandering can legitimately brush a
920
+ background pickup or trigger an incidental event on the way, and an exact
921
+ `toBe` turns that legitimate variance into a flaky failure.
922
+
923
+ **Budgets are sim-time, never wall-clock.** `waitFor`/`hold` budgets are
924
+ `{ simSeconds }` (or `{ simTicks }`) only; passing `timeout` is a TypeScript
925
+ error AND a runtime throw. Why: probe runs headless Chromium under
926
+ SwiftShader (software rasterization, no GPU), where a frame costs several
927
+ times its real-hardware wall-clock and varies machine to machine — a
928
+ wall-clock budget flakes on slow CI while sim-time measures the game's OWN
929
+ clock, so a slow renderer never fails a correct game. The failure block
930
+ prints sim vs wall elapsed with the ratio so you can tell game-bug /
931
+ budget-too-small-under-SwiftShader / stalled-loop apart at a glance.
932
+ `game.input.hold(action, { simSeconds })` — extra hold time is extra
933
+ distance traveled, not slack; steer with closed-loop `waitFor` checks
934
+ rather than padded holds. Spec files must not call `test.setTimeout(...)` (or
935
+ `test.slow()`) either — `vgai probe`'s pre-boot lint rejects it: the runner
936
+ already disables Playwright's own wall-clock timeout (`timeout: 0`), and a
937
+ per-test wall budget fights the sim clock the same way a raw `timeout` would.
938
+
939
+ `hold()` resolves after it releases the action. When the evidence must observe
940
+ a transient active-input state or moving pose, use
941
+ `game.input.whileHeld(action, async () => { ... })`; it presses through the
942
+ real input map, runs the callback while still held, and releases in `finally`
943
+ even if an assertion or screenshot throws.
944
+
945
+ **One registry, three doors.** Whatever a component registers through
946
+ `ctx.debug` (or the react hooks) is visible — same names, same JSON — on
947
+ three surfaces:
948
+
949
+ | Door | Surface | Talks to |
950
+ | --- | --- | --- |
951
+ | Acceptance specs / `@vgai/live` scripts | `@vgai/probe`'s `game` fixture, or the identical `GameClient` bound by `@vgai/live` (`state`/`command`/`input`/`page`/`waitFor`/`events`) | the EDITOR session's live play mode when one exists (the default — same tab a human is watching), else (specs only, no session) its own probe-booted standalone game page (`window.__vgai`) |
952
+ | CLI / agents | the `vgai play …` op family | the EDITOR session's live play mode |
953
+ | Editor panels | play-mode data/debug panels | the in-process registry directly |
954
+
955
+ The one remaining asymmetry: only when NO live session exists do committed
956
+ specs fall back to their own probe-booted standalone page — with a session
957
+ open (the common case), specs, `@vgai/live` scripts, and the CLI all talk to
958
+ the SAME editor session's live play mode. (This table enumerates USER
959
+ surfaces; at the transport level the seam's doors are the in-page
960
+ `window.__vgai` bridge and the editor relay — the panels ride the in-process
961
+ path, not a transport.)
962
+
963
+ **Test in layers — never debug a pure-logic bug through the browser:**
964
+ 1. **L1 — logic.** Extract pure models (movement/physics stepping, lap math,
965
+ world rules) into plain functions and test them headlessly in
966
+ `tests/logic/` (`npm test`, vitest — no browser, no renderer). Seconds
967
+ per run; this is where most bugs should be found and fixed. Worked
968
+ example: `tests/logic/example.test.ts`. Footnote: a clamp/lerp/damping
969
+ helper can legitimately return `-0` (e.g. `clamp(0, ...)` after a
970
+ negative overshoot); vitest's `toBe` asserts with `Object.is`, which
971
+ treats `-0` and `0` as UNEQUAL — assert numeric results with
972
+ `toBeCloseTo`, or add `+0` to the actual value, rather than chasing a
973
+ "flaky" failing assertion that is actually a `-0`/`Object.is` mismatch.
974
+ 2. **L2 — acceptance.** One focused browser spec per feature, from a known
975
+ fixture, through `vgai probe`. A feature isn't done until its spec is
976
+ green — this is the acceptance bar's "bot-proven" evidence.
977
+ 3. **L3 — mission.** Long full-run specs (start-to-goal). Only run these
978
+ when the isolated layers underneath are green — otherwise every failure
979
+ is ambiguous.
980
+
981
+ **Events are the flight recorder.** `ctx.debug.emit(name, detail)` is
982
+ tick-stamped and ring-buffered; `game.events.expect([...])` asserts an
983
+ ordered subsequence of events since test start, and the failure block
984
+ replays the actual event window with the first unmatched expectation marked.
985
+ Emit gameplay milestones (`wave-cleared`, `tree-felled`, `goal-passed`) from
986
+ day one — the iteration cycle is: red run → read the failure block →
987
+ reproduce at the LOWEST layer that exhibits it → fix → re-run the one spec
988
+ (`vgai probe -g …`) → re-run the suite. Without the evidence, step two is
989
+ guesswork.
990
+
991
+ **Two hard-won invariants** (each cost a debugging session to learn):
992
+ - If your bot is a generator/coroutine stepped once per frame, every loop
993
+ iteration must `yield`. A loop that can re-enter without yielding (e.g.
994
+ "plan failed → retry") is a synchronous freeze that pins the renderer.
995
+ - Never edit source files watched by a running dev server mid-probe — the
996
+ hot reload kills the run and the result is garbage either way.
997
+ - If you drive either dev server (`vgai edit`/`npm run dev`, or `npm run
998
+ game`) with Playwright, use `page.goto(url, { waitUntil: 'load' })`, never
999
+ `'networkidle'` — the dev server keeps a live SSE/websocket/HMR
1000
+ connection open, so the network never goes idle and `'networkidle'`
1001
+ hangs forever.
1002
+
1003
+ **Multiplayer without multiplayer**: N synthetic players in N headless pages
1004
+ against your Colyseus room is a real multiplayer session — same input paths,
1005
+ same netcode, no humans needed. This is the cheapest way to soak-test rooms.
1006
+ For the headless L1 equivalent, `tests/logic/room.test.ts` drives your room
1007
+ in-process through `server/loopback.ts` — no ports, no browser, no editor.
1008
+
1009
+ ### Idioms checklist
1010
+
1011
+ The engine-usage doctrine above (GameComponents, HUD/state-bridge, `ctx.debug`
1012
+ observability, the `vgai probe`-is-the-one-harness rule, dynamic-import
1013
+ Colyseus, cleanup) is collected as a one-page checklist in `IDIOMS.md`, each
1014
+ item marked machine-checked or review-only. Run `npm run check-idioms`
1015
+ (`--strict` to also fail on the warn-level heuristics) to catch drift —
1016
+ e.g. a hand-rolled Playwright boot, a `window.__vgai` reach-in, or a
1017
+ `setInterval` polling pump next to `useGameState` — without waiting for a
1018
+ human review pass. It's a static scanner, not a substitute for `vgai probe`
1019
+ itself.
1020
+
1021
+ ## Examples Catalog
1022
+
1023
+ Nothing example-shaped ships inside this project — every example is a
1024
+ standalone project in the vgai monorepo's `examples/<id>/`, with its own
1025
+ `src/`, scenes, `vgai.game.json`, and (where multiplayer) `server/`. Get one's
1026
+ code by scaffolding it: `npx create-vgai-project <name> --example <id>` (e.g.
1027
+ `--example third-person`) — a correctly-rewritten standalone copy. Or open it
1028
+ live in the hosted editor at `https://vgai-editor.pages.dev/?project=<id>`
1029
+ (read-only; scaffold to keep edits).
1030
+
1031
+ **Gameplay examples:**
1032
+ - `third-person` — orbit camera, WASD movement, XState animation machine, AI chase
1033
+ - `fps` — pointer lock, throwable projectiles, dynamic physics
1034
+ - `vehicle` — Rapier raycast-vehicle suspension, steering, chase camera
1035
+ - `rts` — top-down camera, box selection, navmesh crowd pathfinding
1036
+ - `third-person-arena` — multiplayer tag with AI bots: client-authoritative movement, server-authoritative game state (tag/phases); ships its own `server/`
1037
+ - `starfighter` — flight controller with schema-declared tuning fields
1038
+ - `ik` — skeletal IK (CCDIKSolver) on a code-built rig
1039
+
1040
+ **Scene & technique showcases:**
1041
+ - `editor-tutorial` — feature-showcase scene: meshes, materials, lights, physics, particles, audio, bloom
1042
+ - `feature-scenes` — six technique scenes in one project: instancing, terrain, ocean, particles, custom water shader, HDR skybox
1043
+ - `littlest-tokyo` — GLTF model scene (Draco-compressed; ships its decoder assets)
1044
+ - `learn-joints`, `learn-matfiles`, `learn-triggers`, `learn-splines` — one-concept tutorials (physics joints, shared `.mat.json` materials, sensor triggers, spline motion)
1045
+ - `theatre-clock-demo` — Theatre.js `getProject`/`.sheet()`/`.object()` driven by the canonical `AnimationClock` (continuous camera/object animation; no renamed Theatre vocabulary)
1046
+ - `audio-showcase` — spatial audio (`THREE.PositionalAudio`) and procedural Tone.js music sharing one audio clock/bus
1047
+ - `input-showcase` — scalar/Vector2 input actions plus a live rebind panel
1048
+
1049
+ Cinematic authoring (Theatre + GSAP + XState + Tone + React HUD/letterbox,
1050
+ composed and rendered deterministically) has no standalone `--example` yet —
1051
+ see `docs/RECIPES.md` recipe 9 and `packages/engine/e2e/reference-cinematic/`
1052
+ for the reference composition, and `vgai render-cinematic --help` for the
1053
+ render CLI.
1054
+
1055
+ **UI / multi-world examples:**
1056
+ - `hp-hud` — a declared React root reading live state with `useGameState`
1057
+ - `tri-world` — one manifest, three adapter roots side by side (threejs + pixijs + react)
1058
+
1059
+ **Multiplayer note:** this project already ships a generic `server/` (a
1060
+ `GameRoom` in `server/rooms/`, registered in `server/rooms.ts`), but the
1061
+ Colyseus server is **not** started by `npm run dev`/`npm run game`. Run `npm run server` to
1062
+ boot it standalone — it starts `server/main.ts` on `ws://localhost:2567` and
1063
+ shuts down cleanly on Ctrl-C. Start it before connecting, or the client will
1064
+ fail to connect. (The script runs tsx against `server/tsconfig.json`, not the
1065
+ app tsconfig — `@colyseus/schema`'s legacy `@type` decorators need
1066
+ `experimentalDecorators`, which only the server tsconfig sets.) The
1067
+ `third-person-arena` example (`--example third-person-arena`) ships its own
1068
+ richer `server/` (an `ArenaRoom` with tag/phase state) in place of this
1069
+ generic one.
1070
+
1071
+ ## Scene File Format
1072
+
1073
+ Entities in `.vscn.json` have these optional fields:
1074
+ - `id` — UUID. The **editor** generates one when it creates an entity, and the
1075
+ runtime loader auto-generates a random one for id-less entities at load (so
1076
+ every loaded object carries an entity descriptor and appears in the play-mode
1077
+ hierarchy). But a random id can't be *referenced*: anything targeted from
1078
+ elsewhere (joints, god-rays, etc.) still needs an AUTHORED id.
1079
+ - `name` — display name
1080
+ - `transform` — `{ position: [x,y,z], rotation: [x,y,z,w], scale: [x,y,z] }`
1081
+ - `mesh` — `{ type: 'box'|'sphere'|'plane'|'cylinder'|... }`
1082
+ - `material` — `{ type: 'standard'|'physical', color, metalness, roughness, ... }` (inline material)
1083
+ - `materialRef` — path to a shared `.mat.json` asset. **Mutually exclusive with `material`** — set one or the other, never both (load-time error).
1084
+ - `light` — `{ type: 'directional'|'point'|'spot'|'hemisphere'|'area', ... }`.
1085
+ Light values are native Three.js values, not engine-normalized multipliers:
1086
+ point/spot `intensity` is candela with inverse-square `decay: 2` by default
1087
+ (a value near `1` is candle-scale), while area-light intensity is nit.
1088
+ `distance` limits point/spot range but does not make a low-candela light
1089
+ brighter. An emissive material only makes that mesh appear bright; add a
1090
+ real light when it must illuminate nearby objects. Inspect Play pixels under
1091
+ the authored tone mapping/exposure—visible bulbs are not lighting proof.
1092
+ - `camera` — `{ type: 'perspective'|'orthographic', ... }`
1093
+ - `physics` — `{ bodyType: 'dynamic'|'fixed'|'kinematic', collider: { type: 'cuboid'|'ball'|'capsule', halfExtents?, radius?, friction?, restitution?, density? }, mass? }`
1094
+
1095
+ **Physics contracts (current):**
1096
+ - Physics entities may be nested under transformed parents: the loader
1097
+ composes the full parent chain and spawns the body at the WORLD
1098
+ transform. One exception, enforced loudly: a `dynamic` body under an
1099
+ ancestor that itself has a `dynamic`/`kinematic` body is a load error
1100
+ (Rapier has no parent/child body relationship).
1101
+ - Collider dimensions (`halfExtents`, `radius`) are authored in local units
1102
+ and **scale with the entity's composed world scale** (per-axis for
1103
+ cuboids; round colliders require uniform scale on their round axes —
1104
+ non-uniform is a loud load error). The editor's collider gizmo renders
1105
+ the same computed dimensions the runtime uses.
1106
+ - Collision events fire for all scene-loaded colliders: solid-solid
1107
+ contacts reach `ctx.collisions.onCollision`; **sensor** colliders
1108
+ additionally dispatch `onTriggerEnter/Exit` on components.
1109
+ - `components` — `{ "ComponentName": { prop: value, ... }, ... }` (see Components section above)
1110
+ - `children` — nested entity array