@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.
- package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- 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:
|
|
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).
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
161
|
-
npm run vgai -- shading
|
|
162
|
-
npm run vgai -- 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
|
package/template/AGENTS.md
CHANGED
|
@@ -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. **
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
244
|
-
|
|
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** —
|
|
305
|
-
|
|
306
|
-
`
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
`
|
|
316
|
-
`
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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('
|
|
816
|
-
//
|
|
817
|
-
ctx.debug?.registerCommand('
|
|
818
|
-
|
|
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
|
-
|
|
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
|
|
833
|
-
|
|
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('
|
|
897
|
-
await game.command('
|
|
898
|
-
await game.input.
|
|
899
|
-
await game.waitFor((s) =>
|
|
900
|
-
{ simSeconds: 10 });
|
|
901
|
-
await game.
|
|
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
|
|
package/template/package.json
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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`,
|
|
64
|
-
*
|
|
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
|
|
7
|
-
|
|
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
|
|
@@ -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
|
-
|
|
25
|
-
|
|
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)
|
|
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
|
};
|