@vgai/editor 0.4.0 → 0.4.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.
Files changed (137) hide show
  1. package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
  2. package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
  3. package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
  4. package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
  5. package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
  6. package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
  7. package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
  8. package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
  9. package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
  10. package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
  11. package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
  12. package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
  13. package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
  14. package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
  15. package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
  16. package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
  17. package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
  18. package/dist/assets/index-DiUacMXR.css +1 -0
  19. package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
  20. package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
  21. package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
  22. package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
  23. package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +27125 -1075
  26. package/package.json +2 -2
  27. package/server/asset-history-snapshots.ts +189 -0
  28. package/server/asset-library-routes.ts +404 -53
  29. package/server/catalog-import-recipe.ts +97 -0
  30. package/server/catalog-reimport.ts +141 -0
  31. package/server/cloud-asset-catalog.ts +71 -5
  32. package/server/dev.ts +1 -1
  33. package/server/editor-server.ts +106 -51
  34. package/server/editor-sse.ts +5 -0
  35. package/server/generative-execution-context.ts +45 -0
  36. package/server/model-import-conversion.ts +345 -0
  37. package/server/packaged.ts +1 -1
  38. package/server/project-hmr-files.ts +3 -5
  39. package/server/project-output-writer.ts +440 -36
  40. package/server/project-tools.ts +529 -0
  41. package/server/server-utils.ts +14 -11
  42. package/src/EditorContext.tsx +4 -4
  43. package/src/action-registry.ts +12 -0
  44. package/src/asset-editor-context.tsx +9 -0
  45. package/src/asset-events.ts +3 -15
  46. package/src/asset-selection.ts +3 -0
  47. package/src/asset-workflow/asset-capabilities.ts +40 -0
  48. package/src/asset-workflow/asset-import-jobs.ts +108 -0
  49. package/src/asset-workflow/asset-library-query.ts +8 -1
  50. package/src/asset-workflow/asset-workflow-quality.ts +2 -0
  51. package/src/asset-workflow/import-contract.ts +2 -0
  52. package/src/asset-workflow/model-inspection.ts +92 -1
  53. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  54. package/src/asset-workflow/project-asset-commands.ts +1 -0
  55. package/src/asset-workflow/project-asset-health.ts +3 -6
  56. package/src/asset-workflow/project-asset-operations.ts +119 -4
  57. package/src/command-listener.ts +37 -9
  58. package/src/components/ApplicationMenus.tsx +13 -15
  59. package/src/components/AssetBrowser.tsx +301 -26
  60. package/src/components/AssetImportDetails.tsx +207 -92
  61. package/src/components/AssetImportJobStatus.tsx +55 -0
  62. package/src/components/CenterDocuments.tsx +28 -1
  63. package/src/components/CommandPalette.tsx +2 -2
  64. package/src/components/DefaultEditorLayout.tsx +4 -0
  65. package/src/components/InspectorToolSection.tsx +12 -10
  66. package/src/components/OnlineAssetBrowser.tsx +197 -11
  67. package/src/components/ToolHost.tsx +8 -8
  68. package/src/components/ViewportOverlay.tsx +13 -26
  69. package/src/components/ViewportPanel.tsx +19 -14
  70. package/src/components/ViewportShadingMenu.tsx +103 -0
  71. package/src/components/WorkspaceDock.tsx +103 -17
  72. package/src/components/asset-documents.tsx +38 -11
  73. package/src/components/asset-editor-persistence.ts +300 -5
  74. package/src/components/asset-selection-section.tsx +83 -9
  75. package/src/components/asset-viewers/AudioViewer.tsx +4 -7
  76. package/src/components/asset-viewers/ModelViewer.tsx +168 -5
  77. package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
  78. package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
  79. package/src/components/asset-workflow.css +30 -3
  80. package/src/components/primitives/editor-icons.ts +1 -0
  81. package/src/components/project-tool-documents.tsx +298 -0
  82. package/src/components/status-contributions.tsx +38 -0
  83. package/src/components/tool-documents.tsx +20 -17
  84. package/src/components/tool-schema-form.tsx +258 -0
  85. package/src/components/workspace-dock.css +16 -0
  86. package/src/editor-api.ts +159 -22
  87. package/src/editor-store.ts +6 -5
  88. package/src/editor-viewport.ts +12 -7
  89. package/src/game-viewport-state.ts +28 -0
  90. package/src/model-thumbnail.ts +97 -9
  91. package/src/play-mode.ts +30 -3
  92. package/src/project-tool-discovery.ts +5 -5
  93. package/src/{project-operations.ts → project-tools.ts} +39 -26
  94. package/src/scene-sync.ts +0 -67
  95. package/src/storage/handle-storage.ts +5 -1
  96. package/src/storage/http-storage.ts +7 -2
  97. package/src/storage/mem-storage.ts +7 -1
  98. package/src/storage/types.ts +2 -0
  99. package/src/theme-css.ts +17 -7
  100. package/src/tool-loader.ts +152 -241
  101. package/src/viewport-shading-boundary.ts +15 -0
  102. package/src/widgets/index.ts +2 -1
  103. package/src/workspace-dock-controller.ts +8 -0
  104. package/src/workspace-document-registry.ts +4 -4
  105. package/src/workspace-state-persistence.ts +36 -10
  106. package/template/.claude/skills/editor/SKILL.md +13 -3
  107. package/template/AGENTS.md +100 -83
  108. package/template/package.json +16 -0
  109. package/template/public/scenes/default.vscn.json +1 -2
  110. package/template/scripts/playtest.ts +7 -37
  111. package/template/server/rooms/game-room.ts +2 -2
  112. package/template/src/data/README.md +3 -2
  113. package/template/src/data/tuning.data.json +1 -3
  114. package/template/src/data/tuning.schema.json +0 -12
  115. package/template/src/data/tuning.schema.ts +2 -9
  116. package/template/src/data/tuning.ts +1 -1
  117. package/template/src/scripts/registry.ts +0 -3
  118. package/template/src/tools/README.md +151 -16
  119. package/template/src/tools/autoplay.tool.ts +31 -0
  120. package/template/src/tools/autoplay.ts +15 -0
  121. package/template/src/tools/tuning.document.tsx +107 -0
  122. package/template/src/tools/tuning.tool.ts +24 -0
  123. package/template/src/tools/tuning.ts +9 -0
  124. package/template/src/ui/Button.stories.tsx +1 -1
  125. package/template/tests/acceptance/example.spec.ts +4 -59
  126. package/template/vgai.game.json +1 -1
  127. package/dist/assets/index-bAGhEnnO.css +0 -1
  128. package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
  129. package/dist/project-scripts.bundle.js +0 -3917
  130. package/server/project-operations.ts +0 -343
  131. package/src/components/project-operation-documents.tsx +0 -226
  132. package/template/src/operations/README.md +0 -88
  133. package/template/src/scripts/components/data-spinner.ts +0 -86
  134. package/template/src/scripts/components/spin-lap-math.ts +0 -27
  135. package/template/src/tools/example.tool.tsx +0 -97
  136. package/template/src/tools/spin.tool.tsx +0 -101
  137. package/template/tests/logic/example.test.ts +0 -31
@@ -8,7 +8,7 @@
8
8
  * own store (`hierarchy-expansion-state.ts`, §6.1), same key discipline.
9
9
  *
10
10
  * STORAGE DECISION (recorded): `localStorage`, key
11
- * `vgai:workspace-state:v8`, holding a map keyed PER PROJECT by
11
+ * `vgai:workspace-state:v10`, holding a map keyed PER PROJECT by
12
12
  * `getCurrentProject().rootPath` (the same project identity
13
13
  * `hierarchy-expansion-state.ts` established — it prevents one game's layout
14
14
  * leaking into another, and `rootPath` is the durable cross-session
@@ -32,7 +32,8 @@
32
32
  * path). Verified via `projectFileExists()` (storage-first stat, HTTP HEAD
33
33
  * fallback — an existence-only probe; unlike the prefab/scene loaders'
34
34
  * `readProjectTextFile`, this never downloads the asset body just to check
35
- * it is still there). Asset-Lab (`asset-lab:entity:*`) and unresolved
35
+ * it is still there). Live-entity Asset Editor (`asset-editor:entity:*`,
36
+ * with legacy `asset-lab:*` input aliases) and unresolved
36
37
  * online (`online:*`) documents are NOT persisted: a live-entity lab
37
38
  * session and a remote detail view are not truthfully re-resolvable from a
38
39
  * cold boot.
@@ -41,6 +42,8 @@
41
42
  * - tool documents: `openToolDocument` self-verifies against the discovered
42
43
  * tool store (awaited `refreshProjectTools()` first) — unknown ids open
43
44
  * nothing.
45
+ * - the Project Tools catalog always reopens; schema-driven fallback runners
46
+ * verify their registered callable after an awaited catalog refresh.
44
47
  * - story documents: verified against the story registry after an awaited
45
48
  * `refreshProjectStories()` — module must load ok AND still export the
46
49
  * story name.
@@ -79,6 +82,12 @@ import {
79
82
  refreshDataFiles,
80
83
  } from './components/data-documents';
81
84
  import { openDebuggerPanel } from './components/debugger-panel-state';
85
+ import {
86
+ openProjectToolDocument,
87
+ openProjectToolsDocument,
88
+ PROJECT_TOOL_DOCUMENT_PREFIX,
89
+ PROJECT_TOOLS_DOCUMENT_ID,
90
+ } from './components/project-tool-documents';
82
91
  import {
83
92
  openStoryDocsDocument,
84
93
  openStoryDocument,
@@ -94,8 +103,9 @@ import {
94
103
  subscribeHarnessChatLayout,
95
104
  } from './harness-chat-layout';
96
105
  import { getCurrentProject } from './project-manager';
106
+ import { refreshProjectTools } from './project-tools';
97
107
  import { getProjectStoryModules, refreshProjectStories } from './stories/story-registry';
98
- import { refreshProjectTools } from './tool-loader';
108
+ import { refreshProjectToolContributions } from './tool-loader';
99
109
  import {
100
110
  activateWorkspaceDocument,
101
111
  activeWorkspaceDocumentId,
@@ -113,7 +123,10 @@ import {
113
123
  // world-surface/resource tab-family seam. A v7 layout cannot name those
114
124
  // panels and can restore center documents into a navigator group, so reset
115
125
  // that physical layout once instead of guessing at user intent.
116
- const STORAGE_KEY = 'vgai:workspace-state:v8';
126
+ // v9 removes the old UI-only tool document shape in favor of distinct
127
+ // registered project-tool runners. v10 adds package-registered editor
128
+ // contributions as independently restorable documents.
129
+ const STORAGE_KEY = 'vgai:workspace-state:v10';
117
130
  const WRITE_DEBOUNCE_MS = 300;
118
131
  /** Bound on the document-restore verification before write-through installs
119
132
  * anyway (module header's "wait is BOUNDED" rule). */
@@ -124,7 +137,8 @@ const LEGACY_PROFILER_DOCUMENT_ID = 'workspace:profiler';
124
137
  export type PersistedDocument =
125
138
  | { readonly t: 'asset'; readonly id: string; readonly path: string; readonly assetKind: string }
126
139
  | { readonly t: 'data'; readonly id: string; readonly path: string }
127
- | { readonly t: 'tool'; readonly id: string; readonly toolId: string }
140
+ | { readonly t: 'tool-contribution'; readonly id: string; readonly contributionId: string }
141
+ | { readonly t: 'project-tool'; readonly id: string; readonly name: string }
128
142
  | {
129
143
  readonly t: 'story';
130
144
  readonly id: string;
@@ -210,8 +224,14 @@ export function persistableDocument(doc: {
210
224
  return id === DATA_ASSETS_DOCUMENT_ID
211
225
  ? { t: 'static', id }
212
226
  : { t: 'data', id, path: id.slice('data:'.length) };
213
- case 'tool':
214
- return { t: 'tool', id, toolId: id.slice('tool:'.length) };
227
+ case 'tool-contribution':
228
+ return { t: 'tool-contribution', id, contributionId: id.slice('tool:'.length) };
229
+ case 'project-tool':
230
+ if (id === PROJECT_TOOLS_DOCUMENT_ID) return { t: 'static', id };
231
+ if (id.startsWith(PROJECT_TOOL_DOCUMENT_PREFIX)) {
232
+ return { t: 'project-tool', id, name: id.slice(PROJECT_TOOL_DOCUMENT_PREFIX.length) };
233
+ }
234
+ return null;
215
235
  case 'story': {
216
236
  const state = storyDocumentState(id);
217
237
  if (!state) return null;
@@ -271,6 +291,7 @@ function restoreStoryDocument(
271
291
 
272
292
  function restoreStaticDocument(store: WorkspaceStateStore, id: string): void {
273
293
  if (id === LEGACY_PROFILER_DOCUMENT_ID) openDebuggerPanel('profiler');
294
+ else if (id === PROJECT_TOOLS_DOCUMENT_ID) openProjectToolsDocument(store);
274
295
  else if (id === DATA_ASSETS_DOCUMENT_ID) openDataAssetsDocument(store);
275
296
  else if (id === BUILD_PROFILES_DOCUMENT_ID && getEditorMode() === 'server')
276
297
  openBuildProfilesDocument(store);
@@ -286,8 +307,11 @@ async function restoreOneDocument(
286
307
  case 'data':
287
308
  if (loadedDataFiles().some((f) => f.path === doc.path)) openDataDocument(store, doc.path);
288
309
  return;
289
- case 'tool':
290
- openToolDocument(store, doc.toolId); // self-verifying (false = no-op)
310
+ case 'tool-contribution':
311
+ openToolDocument(store, doc.contributionId); // self-verifying (false = no-op)
312
+ return;
313
+ case 'project-tool':
314
+ openProjectToolDocument(store, doc.name); // self-verifying (false = no-op)
291
315
  return;
292
316
  case 'story':
293
317
  restoreStoryDocument(store, doc);
@@ -315,7 +339,9 @@ export async function restoreWorkspaceDocuments(
315
339
  return;
316
340
  }
317
341
  if (docs.open.some((d) => d.t === 'data')) await refreshDataFiles().catch(() => {});
318
- if (docs.open.some((d) => d.t === 'tool')) await refreshProjectTools().catch(() => {});
342
+ if (docs.open.some((d) => d.t === 'project-tool')) await refreshProjectTools().catch(() => {});
343
+ if (docs.open.some((d) => d.t === 'tool-contribution'))
344
+ await refreshProjectToolContributions().catch(() => {});
319
345
  if (docs.open.some((d) => d.t === 'story')) {
320
346
  const project = getCurrentProject();
321
347
  if (project) await refreshProjectStories(project).catch(() => {});
@@ -157,11 +157,21 @@ npm run vgai -- helpers on # Show all helpers (lights, cameras, colliders,
157
157
  npm run vgai -- helpers off # Hide all helpers
158
158
  npm run vgai -- stats on # Show performance stats overlay
159
159
  npm run vgai -- stats off # Hide stats
160
- npm run vgai -- shading solid # Standard shading
161
- npm run vgai -- shading wireframe # Wireframe mode
162
- npm run vgai -- shading unlit # Unlit/flat shading
160
+ npm run vgai -- shading solid # Native authored materials and lighting
161
+ npm run vgai -- shading unlit # Authored color/texture without lighting
162
+ npm run vgai -- shading wireframe # Topology view
163
+ npm run vgai -- shading normals # Surface-normal orientation view
164
+ npm run vgai -- shading overdraw # Additive overlap-density view
163
165
  ```
164
166
 
167
+ Shading is temporary, render-only editor state: it never rewrites scene or
168
+ game materials and is not saved into a scene. Scene and Game remember separate
169
+ session-local modes; `shading` targets whichever viewport is active. Game
170
+ shading applies to compatible first-party Three.js roots, while React/Pixi and
171
+ other adapter-owned layers continue rendering natively. The Scene helper menu
172
+ also exposes independent lights, cameras, colliders, skeletons, audio, and
173
+ bounds toggles; use `setHelperType()` through the SDK when you need one category.
174
+
165
175
  ### State
166
176
 
167
177
  ```bash
@@ -84,7 +84,21 @@ the real product surface.
84
84
  to pin a specific port for this project, so an old tab left open from a
85
85
  different project doesn't get mistaken for this session (see the
86
86
  `/editor` skill's CLI Reference for `--port` and `vgai sessions`).
87
- 1. **Choose explicit adapter roots before authoring.** Every v2 manifest has
87
+ 1. **Autoplay from first playability.** Do not build most of the game and save
88
+ autoplay for final QA. After the ROADMAP identifies the first playable
89
+ behavior, adapt `scripts/playtest.ts` as the first game-specific verification
90
+ edit. Implement only the minimum state provider plus honest player interaction
91
+ needed to make that behavior playable, then stop and run `npm run playtest`
92
+ before expanding the game. A first run may be red; that is useful evidence,
93
+ not a reason to postpone the loop. For every later playable behavior, repeat:
94
+ edit → autoplay in the live editor → inspect state, logs, and screenshot → fix.
95
+ The bot must drive the real input/DOM path described under "Autoplay bots";
96
+ fixture commands may arrange setup but never prove the behavior. Once the
97
+ interaction is green, graduate it into `tests/acceptance/` and run
98
+ `npm run vgai -- probe` before calling the feature done. "Autoplay first"
99
+ means from the first honest playable seam onward—not before a behavior exists,
100
+ and not after the whole game has already been assembled.
101
+ 2. **Choose explicit adapter roots before authoring.** Every v2 manifest has
88
102
  a non-empty `roots` array. Each root declares its adapter directly:
89
103
  `"adapter": "react"` for DOM/layout UI and board/card/menu games,
90
104
  `"adapter": "pixijs"` for sprite/canvas gameplay, and
@@ -100,9 +114,9 @@ the real product surface.
100
114
  only game also does **not** add a Three.js/PixiJS root merely to obtain an
101
115
  `InputManager`: its real input path is the DOM, driven in playtests through
102
116
  `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".
117
+ 3. **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.
118
+ 4. **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`.
119
+ 5. **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
120
 
107
121
  ### Procedural humanoids and character animation
108
122
 
@@ -126,7 +140,7 @@ with `skinToHumanoid` and `attachToBone`. The engine intentionally does not
126
140
  prescribe what that style should be.
127
141
 
128
142
  Before choosing runtime generation for a persistent player/NPC, inspect the
129
- project command catalog with `npm run vgai -- run --list --json`. Installed
143
+ project tools catalog with `npm run vgai -- run --list --json`. Installed
130
144
  libraries may advertise ordinary author-time commands through their own
131
145
  `package.json`; the engine's humanoid package contributes a bake command that
132
146
  can produce a normal GLB + `.prefab.json` + provenance, with optional
@@ -163,13 +177,45 @@ For gameplay animation, preserve these contracts:
163
177
  - Acceptance evidence samples the animation state while honest input is held
164
178
  and captures representative moving poses; a screenshot taken after release
165
179
  proves only the return-to-idle frame.
166
- - Keep provenance beside any copied clip asset: source, author/license where
180
+ - Commit copied clip assets through the catalog acquisition/project-output
181
+ boundary so `.vgai/provenance.json` records source, author/license where
167
182
  applicable, exact upstream path or catalog ID, and a content hash.
168
183
 
169
184
  The full API and retargeting policy are in the engine checkout's
170
185
  `docs/PROCEDURAL-HUMANOID.md`. The rules above intentionally do not prescribe
171
186
  a particular humanoid design or game concept.
172
187
 
188
+ ### Generative assets
189
+
190
+ Generation is author-time project automation, not an engine subsystem. Use the
191
+ provider API an ordinary developer already knows: `@fal-ai/client` for Fal and
192
+ documented HTTP `fetch` for Tripo. Do not add a generation provider interface,
193
+ generated-asset object model, runtime component, domain-specific editor protocol, or
194
+ `vgai.game.json` field.
195
+
196
+ - Select deterministic mock, direct/BYOK, or VGAI-managed execution in the
197
+ Node tool that makes the provider call. Mock first while developing;
198
+ never silently replace a real provider call.
199
+ - Install provider/compatibility packages only in projects that need them;
200
+ they are author-time development dependencies, not default game runtime
201
+ dependencies.
202
+ - Keep the useful implementation as ordinary TypeScript. Register it in
203
+ `package.json#vgai.tools` with a thin `src/tools/*.tool.ts` definition only
204
+ when agents or humans should discover and invoke it externally.
205
+ - Download and validate output with its production reader, then commit the
206
+ complete accepted batch once through `ctx.projectOutputs.write(...)`.
207
+ Persistent tools do not write directly into `public/`.
208
+ - Never create per-asset provenance sidecars or manually pass provider task
209
+ facts. The tool host records a single request or ordered task chain in
210
+ `.vgai/provenance.json` atomically with the ordinary project files.
211
+ - Author through native code and files. Use the editor afterward to inspect,
212
+ place, and play-verify the result; editor control is observation, not the
213
+ generation language.
214
+
215
+ See `src/tools/README.md` for the registered-tool contract and use the
216
+ `vgai-generative-assets` skill for provider selection, cost/security, Tripo
217
+ task chains, and media-specific validation.
218
+
173
219
  ## Commands
174
220
 
175
221
  ```bash
@@ -240,12 +286,8 @@ src/
240
286
  main.ts Entry point — creates game runtime
241
287
  runtime/ Scaffold-owned standalone host wiring; game roots do not edit this
242
288
  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)
289
+ tools/ Registered ordinary callables plus optional React surfaces
290
+ (`package.json#vgai.tools`; see src/tools/README.md)
249
291
  scripts/
250
292
  registry.ts Component registry (GameComponent classes)
251
293
  components/ Custom GameComponent classes
@@ -301,41 +343,28 @@ The project has a small set of fundamental concepts, each with one home:
301
343
  inspector (and designers, and VS Code) edit — shared/global definitions and
302
344
  tuning, as opposed to per-entity component config. See "Tunable values → a
303
345
  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`.
346
+ - **Tools** — ordinary JS/TS functions that need external discovery or
347
+ invocation. Register callable modules explicitly in
348
+ `package.json#vgai.tools`; each exports `tool = defineTool({...})` from
349
+ `@vgai/sdk/tools`, declaring Zod input/result schemas, host, mutation, and
350
+ permission metadata around an ordinary directly importable implementation.
351
+ Every registration appears in the editor's Project Tools catalog and
352
+ schema-driven fallback form, `vgai run`, and `@vgai/live`'s
353
+ `tools.list()`/`tools.call()`. A registration may list ordinary React modules
354
+ under `contributions`; each default component receives `{tool, client}`,
355
+ calls the same registered tool through `client.runProjectTool(tool.name,
356
+ ...)`, and never owns a second implementation. Contribution points are
357
+ `workspace.document`, `workspace.utility`, and selection-scoped
358
+ `selection.inspector` (which also exports `match(node, adapter)`). Prefer a
359
+ focused custom contribution for a substantial workflow. Build it with React
360
+ directly for layout and state, and use `@editor/widgets` for shared editor
361
+ controls so focus, disabled, density, and theme behavior stay native. Editor
362
+ SDK hooks remain optional helpers. The starter's
363
+ `project.tuning` and `project.autoplay` are deliberately unconfigured real
364
+ tools: replace their schemas and ordinary implementations for this game.
365
+ Files a tool persists under `public/` are ordinary project assets; the tool
366
+ itself is not an asset. Generators, autoplay, tuning, and validation all use
367
+ this same thin registered-callable boundary. See `src/tools/README.md`.
339
368
 
340
369
  ## Components
341
370
 
@@ -437,11 +466,10 @@ For each common job there is exactly ONE recommended approach. Use it — compet
437
466
  ```
438
467
  Read `tuning.get()` live where the value is used (e.g. each frame) — don't
439
468
  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
469
+ on every hot edit. The starter's tuning schema and registered tuning tool
470
+ are intentionally empty stubs: define game-specific fields, consume them
471
+ through the existing handle, and replace the tool's input/UI before claiming
472
+ tuning works. Add a data asset by copying that trio AND registering it in
445
473
  `src/data/assets.ts` (the one shared list); after every schema change run
446
474
  `npm run emit-schemas` (commits the emitted `.schema.json` so VS Code
447
475
  validates the data file with no dev server). Rows in table-shaped assets
@@ -655,7 +683,7 @@ a level-3 spec before calling the feature done.
655
683
 
656
684
  ### Level 2 — inner-loop autoplay with `@vgai/live`
657
685
 
658
- `@vgai/live` is `{ editor, game, page }` over the SAME session wire `vgai
686
+ `@vgai/live` is `{ editor, game, page, tools }` over the SAME session wire `vgai
659
687
  probe --in-editor` uses (and, since probe now defaults to that transport
660
688
  whenever a session is open, the same wire `vgai probe` uses by default too)
661
689
  — a plain node/tsx script, not a test file, not a fixture, no config. It
@@ -663,18 +691,12 @@ never starts a session; it only ATTACHES to one already running (`vgai edit`
663
691
  — you likely have this open already), and every method rejects with a
664
692
  "run `vgai edit` first" error if none exists.
665
693
 
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
- ```
694
+ `scripts/playtest.ts` invokes the same ordinary `runAutoplay()` implementation
695
+ as the registered `project.autoplay` tool. Implement that function with
696
+ `editor`, `game`, and `page` from `@vgai/live`; agents may then run the exact
697
+ same callable through `tools.call('project.autoplay')`. It deliberately throws
698
+ `TOOL_NOT_CONFIGURED` in a fresh scaffold so an untouched placeholder can
699
+ never masquerade as acceptance evidence.
678
700
 
679
701
  Run it with `npx tsx scripts/playtest.ts`. No browser of its own boots, no
680
702
  Vite cold start, no watchdog racing a fresh game boot — it rides the tab
@@ -812,25 +834,21 @@ Disambiguation: `vgai doctor` asks "does this project mount in the editor?";
812
834
  a bare test `Game` may not carry a registry; a real runtime always does):
813
835
  ```ts
814
836
  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 });
837
+ ctx.debug?.registerStateProvider('session', () => ({ phase: this.phase }));
838
+ // Fixtures are allowed for setup, never as proof of behavior:
839
+ ctx.debug?.registerCommand('session.reset',
840
+ { description: 'Reset the session.', locus: 'client' }, () => this.reset());
822
841
  }
823
842
  ```
824
- Worked example: `src/scripts/components/data-spinner.ts`. Two built-in
825
- providers exist with zero game code: `time` (`{ simSeconds, tick }`) and
843
+ Two built-in providers exist with zero game code: `time` (`{ simSeconds, tick }`) and
826
844
  `input.actions`. Once the project declares a Colyseus room, every command
827
845
  must declare `locus: 'client' | 'server'` — server-locus commands run on
828
846
  the authoritative copy, not client prediction. In a react world or HUD
829
847
  component, use the hooks instead: `useDebugProvider(name, fn)` /
830
848
  `useDebugCommand(name, spec, fn)` / `const emit = useDebugEmit()` directly
831
849
  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
850
+ providers/commands auto-unregistered on unmount. Emit game-owned events
851
+ through that hook; never import or reach through the internal
834
852
  debug registry from game code.
835
853
  2. **Copy `tests/acceptance/example.spec.ts`** and swap in your own
836
854
  provider/command/action names. (If it's missing, `vgai probe --init`
@@ -893,13 +911,12 @@ A spec is plain Playwright with one import:
893
911
  ```ts
894
912
  import { test, expect } from '@vgai/probe';
895
913
 
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');
914
+ test('the primary interaction changes observable game state', async ({ game }) => {
915
+ await game.command('session.reset'); // fixture only
916
+ await game.input.tap('primary'); // proof: honest input path
917
+ await game.waitFor((s) =>
918
+ (s('session') as { phase: string }).phase === 'active', { simSeconds: 10 });
919
+ await game.screenshot('primary-interaction');
903
920
  });
904
921
  ```
905
922
 
@@ -4,6 +4,22 @@
4
4
  "private": true,
5
5
  "version": "0.1.0",
6
6
  "type": "module",
7
+ "vgai": {
8
+ "tools": [
9
+ {
10
+ "entry": "./src/tools/tuning.tool.ts",
11
+ "contributions": [
12
+ {
13
+ "id": "tuning",
14
+ "point": "workspace.document",
15
+ "title": "Tuning",
16
+ "entry": "./src/tools/tuning.document.tsx"
17
+ }
18
+ ]
19
+ },
20
+ "./src/tools/autoplay.tool.ts"
21
+ ]
22
+ },
7
23
  "engines": {
8
24
  "node": ">=22.0.0"
9
25
  },
@@ -35,8 +35,7 @@
35
35
  "mesh": { "type": "box", "args": [1, 1, 1] },
36
36
  "material": { "type": "standard", "color": "#4488ff" },
37
37
  "transform": { "position": [0, 0.5, 0] },
38
- "shadow": { "enabled": true },
39
- "components": { "DataSpinner": { "spinScale": 1 } }
38
+ "shadow": { "enabled": true }
40
39
  },
41
40
  {
42
41
  "id": "af6855d6-9bf9-47f3-bcb1-d5e6843c97bd",
@@ -1,40 +1,10 @@
1
1
  /**
2
- * Inner-loop autoplay (see AGENTS.md's "Autoplay bots" Level 2). A plain
3
- * `@vgai/live` script: no test runner, no Playwright config, no fixture, no
4
- * second browser. It ATTACHES to the live `vgai edit` session/tab you
5
- * already have open (run `vgai edit .` first if you don't) and drives it
6
- * with the SAME `GameClient` vocabulary `tests/acceptance/example.spec.ts`
7
- * uses — compare the two side by side; the difference in ceremony is the
8
- * whole point.
9
- *
10
- * Run: `npm run playtest` (or `npx tsx scripts/playtest.ts`). Nothing here
11
- * is committed evidence — it's scratch space for "does my game work right
12
- * now". Once an interaction proves a feature, graduate it into a spec under
13
- * `tests/acceptance/` (`vgai probe --init` scaffolds the first one).
14
- *
15
- * Drives the template's shipped example component, `DataSpinner`
16
- * (`src/scripts/components/data-spinner.ts`) — replace this with your own
17
- * provider/command/action names as your first real edit.
2
+ * The npm shortcut invokes the same implementation as project.autoplay.
3
+ * Replacing `runAutoplay` is the first game-specific verification edit; run
4
+ * it at first playability and do not wait until the rest of the game exists.
18
5
  */
6
+ import { runAutoplay } from '../src/tools/autoplay';
19
7
 
20
- import { editor, game } from '@vgai/live';
21
-
22
- await editor.play(); // enters play mode; the ack waits for the game's full async setup
23
-
24
- // Fixture (declared cheat, D16) — jump to a known state. Never a substitute
25
- // for the honest-input proof below.
26
- await game.command('spinner.reset');
27
-
28
- // Proof: the same honest input path a human's keys hit (see
29
- // public/inputmaps/default.inputmap.json's 'attack' binding).
30
- await game.input.hold('attack', { simSeconds: 4 });
31
-
32
- await game.waitFor((s) => (s('spinner') as { lapsCompleted: number }).lapsCompleted >= 1, {
33
- simSeconds: 10,
34
- });
35
-
36
- const spinner = (await game.state('spinner')) as { lapsCompleted: number };
37
- console.log(`spinner lapsCompleted: ${spinner.lapsCompleted}`);
38
-
39
- const shot = await game.screenshot('playtest-after-boosted-spin');
40
- console.log(`screenshot: ${shot}`);
8
+ const result = await runAutoplay();
9
+ // biome-ignore lint/suspicious/noConsole: this executable npm script reports its structured result to the caller
10
+ console.log(JSON.stringify(result, null, 2));
@@ -60,8 +60,8 @@ let nextOrbId = 0;
60
60
 
61
61
  /**
62
62
  * The `locus: 'server'` debug-command table (Task 4.2) — one worked example,
63
- * `give-score`, mirroring the client-side `DataSpinner.spinner.reset` fixture
64
- * (`src/scripts/components/data-spinner.ts`): mutates the AUTHORITATIVE
63
+ * `give-score`, demonstrating the same declared-fixture rule as client-side
64
+ * debug commands: mutates the AUTHORITATIVE
65
65
  * (server) copy of state, never client prediction. Gated by
66
66
  * `allowDebugCommands` — see `onCreate` — so a production `npm run server`
67
67
  * boot never accepts these unless explicitly opted in.
@@ -3,8 +3,9 @@
3
3
  Designer-tunable values live here: `<name>.data.json` (the values, with a
4
4
  `"$schema"` line) governed by `<name>.schema.ts` (Zod, engineer-owned) and its
5
5
  emitted `<name>.schema.json` (`npm run emit-schemas`, committed). `tuning.*` +
6
- `tuning.ts` (the engine `defineData` handle) are the shipped worked example —
7
- copy that trio for each new asset and register it in `assets.ts` (one list,
6
+ `tuning.ts` (the engine `defineData` handle) are an intentionally empty starter
7
+ contract—define real fields and consumers for this game, then register each
8
+ new asset in `assets.ts` (one list,
8
9
  feeding both `npm run emit-schemas` and the build-time check that fails
9
10
  `npm run build` loud on invalid data or a dangling `"file#key"` ref; `vgai
10
11
  doctor <folder>` runs the same checks). Never hardcode a tunable as a constant in
@@ -1,5 +1,3 @@
1
1
  {
2
- "$schema": "./tuning.schema.json",
3
- "boxSpinSpeed": 0.8,
4
- "boxColor": "#4488ff"
2
+ "$schema": "./tuning.schema.json"
5
3
  }
@@ -2,18 +2,6 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
- "boxSpinSpeed": {
6
- "default": 0.8,
7
- "description": "Demo box spin speed, radians/sec — edit tuning.data.json while playing to see live tuning",
8
- "type": "number",
9
- "minimum": -5,
10
- "maximum": 5
11
- },
12
- "boxColor": {
13
- "default": "#4488ff",
14
- "description": "Demo box color (hex)",
15
- "type": "string"
16
- },
17
5
  "$schema": {
18
6
  "type": "string"
19
7
  }
@@ -21,14 +21,7 @@
21
21
  */
22
22
  import { z } from 'zod';
23
23
 
24
- export const TuningSchema = z.object({
25
- boxSpinSpeed: z
26
- .number()
27
- .min(-5)
28
- .max(5)
29
- .default(0.8)
30
- .describe('Demo box spin speed, radians/sec — edit tuning.data.json while playing to see live tuning'),
31
- boxColor: z.string().default('#4488ff').describe('Demo box color (hex)'),
32
- });
24
+ // Intentionally empty: define this game's actual tuning contract before use.
25
+ export const TuningSchema = z.object({});
33
26
 
34
27
  export type Tuning = z.infer<typeof TuningSchema>;
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * Consume with `tuning.get()` INSIDE your update loop every frame (never
15
15
  * cache fields across frames, or live tuning silently stops working for that
16
- * field) — see `src/scripts/components/data-spinner.ts` for the worked use.
16
+ * field).
17
17
  */
18
18
  import { defineData } from '@engine/data/data-asset';
19
19
  import raw from './tuning.data.json';
@@ -15,12 +15,10 @@
15
15
 
16
16
  import type { GameComponentClass } from '@engine/ecs/game-component';
17
17
  import type { ComponentRegistry } from '@engine/scene/component-registry';
18
- import { DataSpinner } from './components/data-spinner';
19
18
  import { SceneCamera } from './components/scene-camera';
20
19
 
21
20
  export const componentRegistry = {
22
21
  // GameComponent classes (OOP components with true HMR)
23
- DataSpinner,
24
22
  SceneCamera,
25
23
  } satisfies ComponentRegistry;
26
24
 
@@ -41,6 +39,5 @@ export function getComponent(name: ComponentName): GameComponentClass {
41
39
 
42
40
  /** GameComponent classes for true HMR. Keyed by class name. */
43
41
  export const gameComponents: Record<string, GameComponentClass> = {
44
- DataSpinner,
45
42
  SceneCamera,
46
43
  };