@vgai/editor 0.2.0 → 0.4.0-canary.20260715.1
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.
- package/README.md +24 -0
- package/dist/assets/axe-CyiTVRi6.js +30 -0
- package/dist/assets/browserAll-fGZ_P1P8.js +1 -0
- package/dist/assets/create-runtime-BzSHBTYw.js +1 -0
- package/dist/assets/game-DQ7YHNS8.js +1 -0
- package/dist/assets/game-DX6Obomf.js +1 -0
- package/dist/assets/game-DoUJj8vY.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-HqybI_X7.js} +3 -3
- package/dist/assets/game-NUAnyPkS.js +1 -0
- package/dist/assets/game-ck_dNIE6.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-hIf5LD4E.js} +1 -1
- package/dist/assets/game-loop-o2CcBMFY.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-C8HM7gbP.js} +1 -1
- package/dist/assets/{game-BI111o5f.js → game-uR_lv3Mp.js} +3 -3
- package/dist/assets/index-B-HA1uu3.js +15412 -0
- package/dist/assets/{index-Bp5cecD6.js → index-CSBAZwMx.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-CoSRCKPS.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-D93bBuUl.js} +1 -1
- package/dist/assets/index-bu01fqch.css +1 -0
- package/dist/assets/ingest-siblings-CCkkE2h7.js +1 -0
- package/dist/assets/mount-manifest-B2ECssU2.js +1 -0
- package/dist/assets/react-18-Bmj05S8J.js +1 -0
- package/dist/assets/webworkerAll-BC6gMOLZ.js +1 -0
- package/dist/assets/world-documents-DXPcAEA0.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7718 -3436
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +71 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1236 -296
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +1116 -344
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +620 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- 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
|