@vgai/engine 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
package/src/manifest/schema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// T3.1 slice 1 — the game manifest schema (`vgai.game.json`).
|
|
2
2
|
//
|
|
3
|
-
// This is the serialized form of the Game's
|
|
3
|
+
// This is the serialized form of the Game's root list
|
|
4
4
|
// (docs/GAME-ROOT-DESIGN.md §1, D6) — see docs/GAME-MANIFEST-DESIGN.md §3 for
|
|
5
5
|
// the adjudicated field list this file implements exactly. `load.ts` is this
|
|
6
6
|
// schema's runtime reader (T4.1 policy): it parses via this schema, then
|
|
@@ -11,20 +11,24 @@
|
|
|
11
11
|
// Naming: this file follows the repo's `XxxSchema` (Zod schema) / `Xxx`
|
|
12
12
|
// (inferred type) convention used throughout `scene/schema/` — the design
|
|
13
13
|
// doc's §3 sketch names the bare consts (`Tier`, `IngestStrategy`,
|
|
14
|
-
// `
|
|
14
|
+
// `AdapterRoot`) without the `Schema` suffix; that sketch is explicitly a
|
|
15
15
|
// "Zod sketch" the doc tells implementers to adapt to real code style
|
|
16
16
|
// ("mirror the style in scene/schema/scene-file.ts" per CLAUDE.md), so the
|
|
17
17
|
// schema consts here are suffixed and the bare names are reserved for the
|
|
18
18
|
// inferred types, matching every other schema file in the repo.
|
|
19
19
|
//
|
|
20
|
-
//
|
|
20
|
+
// Adapter surface values mirror `AdapterSurface` in
|
|
21
21
|
// `packages/engine/src/runtime/game.ts` (`'threejs' | 'pixijs' | 'react'`)
|
|
22
|
-
// by value, not by import
|
|
23
|
-
// beyond `zod` (kept parseable/bundlable in any context), matching the
|
|
24
|
-
// `scene/schema/` precedent.
|
|
22
|
+
// by value, not by import. The adapter remains the sole root discriminator.
|
|
25
23
|
|
|
26
24
|
import { z } from 'zod';
|
|
27
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Current on-disk `vgai.game.json` format. Exported so editor/server
|
|
28
|
+
* compatibility checks do not duplicate the schema's literal value.
|
|
29
|
+
*/
|
|
30
|
+
export const GAME_MANIFEST_VERSION = 2 as const;
|
|
31
|
+
|
|
28
32
|
// ---------------------------------------------------------------------------
|
|
29
33
|
// Ingest capture strategy (world2d today; the general vocabulary D6/§4 names)
|
|
30
34
|
// ---------------------------------------------------------------------------
|
|
@@ -42,266 +46,274 @@ export type IngestStrategy = z.infer<typeof IngestStrategySchema>;
|
|
|
42
46
|
// ---------------------------------------------------------------------------
|
|
43
47
|
|
|
44
48
|
export const TierSchema = z.enum([
|
|
45
|
-
'first-party', // vgai-native
|
|
49
|
+
'first-party', // vgai-native root: full editor, full loop control
|
|
46
50
|
'shared', // = IngestStrategy 'shared' rung
|
|
47
51
|
'deduped', // = IngestStrategy 'deduped' rung
|
|
48
52
|
'iframe-reachable', // = IngestStrategy 'iframe-reachable' rung
|
|
49
53
|
'opaque-embed', // = IngestStrategy 'opaque-embed' rung
|
|
50
|
-
'unsupported', // this
|
|
54
|
+
'unsupported', // this root does not function in this delivery context
|
|
51
55
|
]);
|
|
52
56
|
export type Tier = z.infer<typeof TierSchema>;
|
|
53
57
|
|
|
54
58
|
// ---------------------------------------------------------------------------
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
+
// Root adapter identity. The adapter is the sole discriminator: a root never
|
|
60
|
+
// repeats its rendering substrate in a sibling `kind` field. Module and ingest
|
|
61
|
+
// adapters carry the surface their adapter contract exposes because the host
|
|
62
|
+
// must allocate a layer before the module has mounted.
|
|
59
63
|
// ---------------------------------------------------------------------------
|
|
60
64
|
|
|
61
|
-
export const
|
|
65
|
+
export const RootAdapterSchema = z
|
|
62
66
|
.union([
|
|
63
|
-
z.
|
|
64
|
-
z
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` ' +
|
|
84
|
-
'instead, the wired reachable route).',
|
|
85
|
-
),
|
|
86
|
-
assets: z
|
|
87
|
-
.record(z.string(), z.string())
|
|
88
|
-
.optional()
|
|
89
|
-
.describe(
|
|
90
|
-
'Path-substring -> served-URL rewrites for this ingested game (IngestGame.assets today)',
|
|
91
|
-
),
|
|
92
|
-
domStubs: z
|
|
93
|
-
.array(z.string())
|
|
94
|
-
.optional()
|
|
95
|
-
.describe('DOM API stub ids this ingested game requires to run headlessly/in-realm'),
|
|
96
|
-
captureTimeoutMs: z
|
|
97
|
-
.number()
|
|
98
|
-
.optional()
|
|
99
|
-
.describe('Max time to wait for in-realm/cross-realm capture before failing loudly'),
|
|
100
|
-
// Track P (docs/PIXI-INGEST-LANDING-DESIGN.md §2) — the six
|
|
101
|
-
// iframe-reachable-multi mount fields (`ingest-iframe-2d.ts`'s
|
|
102
|
-
// `IframeReachableMultiOpts`), legal ONLY when `strategy` is
|
|
103
|
-
// 'iframe-reachable' (enforced by the `.superRefine` below).
|
|
104
|
-
bundleUrl: z
|
|
105
|
-
.string()
|
|
106
|
-
.optional()
|
|
107
|
-
.describe(
|
|
108
|
-
'Absolute URL of the externalized built game bundle — the WIRED iframe-reachable ' +
|
|
109
|
-
'mount route (iframe-reachable only), for BOTH pixijs (Track P, ' +
|
|
110
|
-
"ingest-iframe-2d.ts's mountIngestGame2DIframeReachableMulti) and threejs (D-W1, " +
|
|
111
|
-
'docs/WAVE4-FTUE-HARDENING-DESIGN.md, ' +
|
|
112
|
-
"ingest-iframe-reachable-adapter.ts's mountIngestGameIframeReachableBundle). " +
|
|
113
|
-
'PRECONDITION: the bundle must externalize its world`s own runtime as a bare, ' +
|
|
114
|
-
'un-rewritten import (`pixi.js` for pixijs, `three` for threejs — `vgai bundle` ' +
|
|
115
|
-
'does this for you) so it resolves through the iframe importmap to the HOST ' +
|
|
116
|
-
'instance the capture trap is installed on; a bundle with an inlined runtime is ' +
|
|
117
|
-
'unreachable and degrades to the embed-only floor. Exactly one of bundleUrl/' +
|
|
118
|
-
'entryHtml is required when strategy is iframe-reachable.',
|
|
119
|
-
),
|
|
120
|
-
baseHref: z
|
|
121
|
-
.string()
|
|
122
|
-
.optional()
|
|
123
|
-
.describe(
|
|
124
|
-
"Iframe <base href> so the game's relative asset URLs resolve (iframe-reachable only).",
|
|
125
|
-
),
|
|
126
|
-
assetBaseUrl: z
|
|
127
|
-
.string()
|
|
128
|
-
.optional()
|
|
129
|
-
.describe(
|
|
130
|
-
"Host-side basePath forced onto the trapped runtime's own asset resolution — " +
|
|
131
|
-
"pixi's `Assets.init` for a pixijs world, three's " +
|
|
132
|
-
'`DefaultLoadingManager.setURLModifier` for a threejs world (D-W1) — absolute, or ' +
|
|
133
|
-
"root-relative (resolved against the editor page's own origin at mount time, " +
|
|
134
|
-
'ingest-mode.ts, so a static manifest never has to know the dev-server port) ' +
|
|
135
|
-
'(iframe-reachable only — a document.write iframe keeps the parent window.location).',
|
|
136
|
-
),
|
|
137
|
-
extraDeps: z
|
|
138
|
-
.array(z.string())
|
|
139
|
-
.optional()
|
|
140
|
-
.describe(
|
|
141
|
-
"Bare specifiers of this world's externalized deps beyond its own runtime " +
|
|
142
|
-
"(pixi.js/three — e.g. '@pixi/sound', 'gsap'). Resolved by the editor's " +
|
|
143
|
-
'host-namespace registry into module namespaces when known (D-P1 — the manifest ' +
|
|
144
|
-
'carries data, not code); an unregistered specifier falls back to the open ' +
|
|
145
|
-
"project's own node_modules (D-Z3, docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md), served " +
|
|
146
|
-
'as a verbatim iframe importmap URL. Throws a named error if neither resolves it ' +
|
|
147
|
-
'(iframe-reachable only).',
|
|
148
|
-
),
|
|
149
|
-
pixiModuleUrl: z
|
|
150
|
-
.string()
|
|
151
|
-
.optional()
|
|
152
|
-
.describe(
|
|
153
|
-
'PIXI-ONLY: standalone matching-major pixi ESM URL to trap instead of the host ' +
|
|
154
|
-
"pixi, for version-skewed games (e.g. a v6 game on a v8 host); this field's " +
|
|
155
|
-
'PRESENCE is how the skew is expressed (D-P2 — no separate strategy/tier for ' +
|
|
156
|
-
'version skew) (iframe-reachable only). Declaring this on a `kind: "threejs"` ' +
|
|
157
|
-
'world is schema-legal (this field is kind-agnostic here) but throws a NAMED ' +
|
|
158
|
-
'not-supported error at resolve — three has no version-skew mount this wave (D-W7, ' +
|
|
159
|
-
'docs/WAVE4-FTUE-HARDENING-DESIGN.md: a hypothetical `threeModuleUrl` is explicitly ' +
|
|
160
|
-
'parked, no consumer exists).',
|
|
161
|
-
),
|
|
162
|
-
bodyHtml: z
|
|
163
|
-
.string()
|
|
164
|
-
.optional()
|
|
165
|
-
.describe(
|
|
166
|
-
'HTML injected into the iframe body before the game script boots (e.g. a ' +
|
|
167
|
-
'`<div id="game-root">` the game expects to find) (iframe-reachable only).',
|
|
67
|
+
z.enum(['threejs', 'pixijs', 'react']),
|
|
68
|
+
z
|
|
69
|
+
.object({
|
|
70
|
+
module: z
|
|
71
|
+
.string()
|
|
72
|
+
.describe('Game-folder-relative path to a custom adapter module (trust boundary)'),
|
|
73
|
+
surface: z
|
|
74
|
+
.enum(['threejs', 'pixijs', 'react'])
|
|
75
|
+
.describe('Native surface exposed by this custom adapter contract'),
|
|
76
|
+
})
|
|
77
|
+
.strict(),
|
|
78
|
+
z
|
|
79
|
+
.object({
|
|
80
|
+
surface: z
|
|
81
|
+
.enum(['threejs', 'pixijs', 'react'])
|
|
82
|
+
.describe('Native surface captured from the unmodified game'),
|
|
83
|
+
ingest: z
|
|
84
|
+
.object({
|
|
85
|
+
strategy: IngestStrategySchema.describe(
|
|
86
|
+
'Capture mechanics rung this ingested root uses (§4) — also the root`s natural local capability tier',
|
|
168
87
|
),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
88
|
+
entryHtml: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe(
|
|
92
|
+
'Entry HTML path, project-relative. WIRED for opaque-embed (D-W2, ' +
|
|
93
|
+
'docs/WAVE4-FTUE-HARDENING-DESIGN.md): the file`s bytes are read verbatim through ' +
|
|
94
|
+
'the project-static route and hosted in a sandboxed iframe (embed-only, no scene ' +
|
|
95
|
+
'introspection), for both pixijs and threejs roots. Legal but NOT wired to a ' +
|
|
96
|
+
'mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` ' +
|
|
97
|
+
'instead, the wired reachable route).',
|
|
98
|
+
),
|
|
99
|
+
assets: z
|
|
100
|
+
.record(z.string(), z.string())
|
|
101
|
+
.optional()
|
|
102
|
+
.describe(
|
|
103
|
+
'Path-substring -> served-URL rewrites for this ingested game (IngestGame.assets today)',
|
|
104
|
+
),
|
|
105
|
+
domStubs: z
|
|
106
|
+
.array(z.string())
|
|
107
|
+
.optional()
|
|
108
|
+
.describe('DOM API stub ids this ingested game requires to run headlessly/in-realm'),
|
|
109
|
+
captureTimeoutMs: z
|
|
110
|
+
.number()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('Max time to wait for in-realm/cross-realm capture before failing loudly'),
|
|
113
|
+
// Track P (docs/PIXI-INGEST-LANDING-DESIGN.md §2) — the six
|
|
114
|
+
// iframe-reachable-multi mount fields (`ingest-iframe-2d.ts`'s
|
|
115
|
+
// `IframeReachableMultiOpts`), legal ONLY when `strategy` is
|
|
116
|
+
// 'iframe-reachable' (enforced by the `.superRefine` below).
|
|
117
|
+
bundleUrl: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe(
|
|
121
|
+
'Absolute URL of the externalized built game bundle — the WIRED iframe-reachable ' +
|
|
122
|
+
'mount route (iframe-reachable only), for BOTH pixijs (Track P, ' +
|
|
123
|
+
"ingest-iframe-2d.ts's mountIngestGame2DIframeReachableMulti) and threejs (D-W1, " +
|
|
124
|
+
'docs/WAVE4-FTUE-HARDENING-DESIGN.md, ' +
|
|
125
|
+
"ingest-iframe-reachable-adapter.ts's mountIngestGameIframeReachableBundle). " +
|
|
126
|
+
'PRECONDITION: the bundle must externalize its root`s own runtime as a bare, ' +
|
|
127
|
+
'un-rewritten import (`pixi.js` for pixijs, `three` for threejs — `vgai bundle` ' +
|
|
128
|
+
'does this for you) so it resolves through the iframe importmap to the HOST ' +
|
|
129
|
+
'instance the capture trap is installed on; a bundle with an inlined runtime is ' +
|
|
130
|
+
'unreachable and degrades to the embed-only floor. Exactly one of bundleUrl/' +
|
|
131
|
+
'entryHtml is required when strategy is iframe-reachable.',
|
|
132
|
+
),
|
|
133
|
+
baseHref: z
|
|
134
|
+
.string()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe(
|
|
137
|
+
"Iframe <base href> so the game's relative asset URLs resolve (iframe-reachable only).",
|
|
138
|
+
),
|
|
139
|
+
assetBaseUrl: z
|
|
140
|
+
.string()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe(
|
|
143
|
+
"Host-side basePath forced onto the trapped runtime's own asset resolution — " +
|
|
144
|
+
"pixi's `Assets.init` for a pixijs root, three's " +
|
|
145
|
+
'`DefaultLoadingManager.setURLModifier` for a threejs root (D-W1) — absolute, or ' +
|
|
146
|
+
"root-relative (resolved against the editor page's own origin at mount time, " +
|
|
147
|
+
'ingest-mode.ts, so a static manifest never has to know the dev-server port) ' +
|
|
148
|
+
'(iframe-reachable only — a document.write iframe keeps the parent window.location).',
|
|
149
|
+
),
|
|
150
|
+
extraDeps: z
|
|
151
|
+
.array(z.string())
|
|
152
|
+
.optional()
|
|
153
|
+
.describe(
|
|
154
|
+
"Bare specifiers of this root's externalized deps beyond its own runtime " +
|
|
155
|
+
"(pixi.js/three — e.g. '@pixi/sound', 'gsap'). Resolved by the editor's " +
|
|
156
|
+
'host-namespace registry into module namespaces when known (D-P1 — the manifest ' +
|
|
157
|
+
'carries data, not code); an unregistered specifier falls back to the open ' +
|
|
158
|
+
"project's own node_modules (D-Z3, docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md), served " +
|
|
159
|
+
'as a verbatim iframe importmap URL. Throws a named error if neither resolves it ' +
|
|
160
|
+
'(iframe-reachable only).',
|
|
161
|
+
),
|
|
162
|
+
pixiModuleUrl: z
|
|
163
|
+
.string()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe(
|
|
166
|
+
'PIXI-ONLY: standalone matching-major pixi ESM URL to trap instead of the host ' +
|
|
167
|
+
"pixi, for version-skewed games (e.g. a v6 game on a v8 host); this field's " +
|
|
168
|
+
'PRESENCE is how the skew is expressed (D-P2 — no separate strategy/tier for ' +
|
|
169
|
+
'version skew) (iframe-reachable only). Declaring this on a `kind: "threejs"` ' +
|
|
170
|
+
'root is schema-legal (this field is kind-agnostic here) but throws a NAMED ' +
|
|
171
|
+
'not-supported error at resolve — three has no version-skew mount this wave (D-W7, ' +
|
|
172
|
+
'docs/WAVE4-FTUE-HARDENING-DESIGN.md: a hypothetical `threeModuleUrl` is explicitly ' +
|
|
173
|
+
'parked, no consumer exists).',
|
|
174
|
+
),
|
|
175
|
+
bodyHtml: z
|
|
176
|
+
.string()
|
|
177
|
+
.optional()
|
|
178
|
+
.describe(
|
|
179
|
+
'HTML injected into the iframe body before the game script boots (e.g. a ' +
|
|
180
|
+
'`<div id="game-root">` the game expects to find) (iframe-reachable only).',
|
|
181
|
+
),
|
|
182
|
+
})
|
|
183
|
+
.describe(
|
|
184
|
+
'Ingest adapter configuration for an unmodified game (a repo-vendored game or your ' +
|
|
185
|
+
'own external folder)',
|
|
186
|
+
)
|
|
187
|
+
.superRefine((ingest, ctx) => {
|
|
188
|
+
// Track P (docs/PIXI-INGEST-LANDING-DESIGN.md §2/D-P4): the six
|
|
189
|
+
// iframe-reachable-multi fields only make sense for the
|
|
190
|
+
// iframe-reachable-multi mount — reject them under any other
|
|
191
|
+
// strategy (mirrors ui.ts's dead-field-under-wrong-variant pattern).
|
|
192
|
+
const iframeOnlyFields: ReadonlyArray<
|
|
193
|
+
readonly [
|
|
194
|
+
key:
|
|
195
|
+
| 'bundleUrl'
|
|
196
|
+
| 'baseHref'
|
|
197
|
+
| 'assetBaseUrl'
|
|
198
|
+
| 'extraDeps'
|
|
199
|
+
| 'pixiModuleUrl'
|
|
200
|
+
| 'bodyHtml',
|
|
201
|
+
present: boolean,
|
|
202
|
+
]
|
|
203
|
+
> = [
|
|
204
|
+
['bundleUrl', ingest.bundleUrl !== undefined],
|
|
205
|
+
['baseHref', ingest.baseHref !== undefined],
|
|
206
|
+
['assetBaseUrl', ingest.assetBaseUrl !== undefined],
|
|
207
|
+
['extraDeps', ingest.extraDeps !== undefined],
|
|
208
|
+
['pixiModuleUrl', ingest.pixiModuleUrl !== undefined],
|
|
209
|
+
['bodyHtml', ingest.bodyHtml !== undefined],
|
|
210
|
+
];
|
|
211
|
+
if (ingest.strategy !== 'iframe-reachable') {
|
|
212
|
+
for (const [key, present] of iframeOnlyFields) {
|
|
213
|
+
if (present) {
|
|
214
|
+
ctx.addIssue({
|
|
215
|
+
code: z.ZodIssueCode.custom,
|
|
216
|
+
message:
|
|
217
|
+
`\`${key}\` is only legal when strategy is 'iframe-reachable' (got ` +
|
|
218
|
+
`"${ingest.strategy}") — bundleUrl/baseHref/assetBaseUrl/extraDeps/` +
|
|
219
|
+
"pixiModuleUrl/bodyHtml are the iframe-reachable-multi mount's options " +
|
|
220
|
+
'(pixijs: ingest-iframe-2d.ts IframeReachableMultiOpts; threejs, D-W1: ' +
|
|
221
|
+
'ingest-iframe-reachable-adapter.ts IframeReachableBundleOpts) and have no ' +
|
|
222
|
+
'meaning under any other strategy.',
|
|
223
|
+
path: [key],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
212
226
|
}
|
|
227
|
+
return;
|
|
213
228
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}),
|
|
229
|
+
const hasBundleUrl = ingest.bundleUrl !== undefined;
|
|
230
|
+
const hasEntryHtml = ingest.entryHtml !== undefined;
|
|
231
|
+
if (hasBundleUrl === hasEntryHtml) {
|
|
232
|
+
ctx.addIssue({
|
|
233
|
+
code: z.ZodIssueCode.custom,
|
|
234
|
+
message:
|
|
235
|
+
"strategy 'iframe-reachable' requires EXACTLY ONE of `bundleUrl` (externalized " +
|
|
236
|
+
'multi-file bundle, the iframe-reachable-multi mount) or `entryHtml` (single-' +
|
|
237
|
+
`file/legacy iframe entry) — ${hasBundleUrl ? 'both are present' : 'neither is present'}.`,
|
|
238
|
+
path: hasBundleUrl ? ['bundleUrl'] : ['entryHtml'],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}),
|
|
242
|
+
})
|
|
243
|
+
.strict(),
|
|
230
244
|
])
|
|
231
245
|
.describe(
|
|
232
|
-
"
|
|
233
|
-
'
|
|
234
|
-
'game (a repo-vendored game or your own external folder)',
|
|
246
|
+
"Root adapter identity: built-in 'threejs'/'pixijs'/'react', a custom " +
|
|
247
|
+
'{ module, surface } adapter, or an { ingest, surface } adapter for an unmodified game',
|
|
235
248
|
);
|
|
236
|
-
export type
|
|
249
|
+
export type RootAdapter = z.infer<typeof RootAdapterSchema>;
|
|
237
250
|
|
|
238
251
|
// ---------------------------------------------------------------------------
|
|
239
|
-
//
|
|
252
|
+
// Root entry
|
|
240
253
|
// ---------------------------------------------------------------------------
|
|
241
254
|
|
|
242
|
-
export const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
kind: z
|
|
249
|
-
.enum(['threejs', 'pixijs', 'react'])
|
|
250
|
-
.describe('World render substrate kind (mirrors runtime/game.ts WorldKind)'),
|
|
251
|
-
description: z
|
|
252
|
-
.string()
|
|
253
|
-
.optional()
|
|
254
|
-
.describe(
|
|
255
|
-
"Optional one-line description of this world's game/content (e.g. an ingested game's " +
|
|
256
|
-
"blurb — the manifest-native replacement for a registry entry's description field)",
|
|
257
|
-
),
|
|
258
|
-
adapter: WorldAdapterSchema.default('default').describe(
|
|
259
|
-
"This world's adapter — see WorldAdapterSchema; defaults to 'default' (resolved by kind)",
|
|
260
|
-
),
|
|
261
|
-
scene: z
|
|
262
|
-
.string()
|
|
263
|
-
.optional()
|
|
264
|
-
.describe(
|
|
265
|
-
'.vscn.json / .scn2d.json scene path (default adapters only — ingest adapters keep their own formats)',
|
|
266
|
-
),
|
|
267
|
-
entry: z
|
|
268
|
-
.string()
|
|
269
|
-
.optional()
|
|
270
|
-
.describe(
|
|
271
|
-
"Module exporting setup()/adapter (default adapters' code-authored alternative to `scene`)",
|
|
272
|
-
),
|
|
273
|
-
zOrder: z
|
|
274
|
-
.number()
|
|
275
|
-
.int()
|
|
276
|
-
.default(0)
|
|
277
|
-
.describe('Canvas stacking order (COMPOSITION-DESIGN); ties broken by array order'),
|
|
278
|
-
pausable: z
|
|
279
|
-
.boolean()
|
|
280
|
-
.default(true)
|
|
281
|
-
.describe('Whether play-mode pause/step applies to this world'),
|
|
282
|
-
loop: z
|
|
283
|
-
.enum(['gated', 'self-driven'])
|
|
284
|
-
.default('gated')
|
|
285
|
-
.describe(
|
|
286
|
-
'gated: host-driven loop (default). self-driven: this world drives its own loop ' +
|
|
287
|
-
'(the "composited, unsynchronized" tier) — an axis independent of capability tier',
|
|
288
|
-
),
|
|
289
|
-
capabilities: z
|
|
290
|
-
.object({
|
|
291
|
-
local: TierSchema.optional().describe(
|
|
292
|
-
'Declared capability tier when served via the local CLI (Vite pipeline); omitted -> derived (§4)',
|
|
255
|
+
export const AdapterRootSchema = z
|
|
256
|
+
.object({
|
|
257
|
+
id: z
|
|
258
|
+
.string()
|
|
259
|
+
.describe(
|
|
260
|
+
'Unique id for this root within the manifest (enforced-unique, D-V6 — GameManifestSchema.roots rejects duplicates)',
|
|
293
261
|
),
|
|
294
|
-
|
|
295
|
-
|
|
262
|
+
description: z
|
|
263
|
+
.string()
|
|
264
|
+
.optional()
|
|
265
|
+
.describe(
|
|
266
|
+
"Optional one-line description of this root's game/content (e.g. an ingested game's " +
|
|
267
|
+
"blurb — the manifest-native replacement for a registry entry's description field)",
|
|
296
268
|
),
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
.describe(
|
|
300
|
-
"Per-delivery-context capability tier declarations (§4). Declaring above the adapter's " +
|
|
301
|
-
'ceiling for that context is an error; declaring at or below it (including `unsupported`) is legal.',
|
|
269
|
+
adapter: RootAdapterSchema.describe(
|
|
270
|
+
"This root's adapter and sole rendering/lifecycle discriminator",
|
|
302
271
|
),
|
|
303
|
-
|
|
304
|
-
|
|
272
|
+
scene: z
|
|
273
|
+
.string()
|
|
274
|
+
.optional()
|
|
275
|
+
.describe(
|
|
276
|
+
'.vscn.json / .scn2d.json scene path (default adapters only — ingest adapters keep their own formats)',
|
|
277
|
+
),
|
|
278
|
+
entry: z
|
|
279
|
+
.string()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe(
|
|
282
|
+
"Module exporting setup()/adapter (default adapters' code-authored alternative to `scene`)",
|
|
283
|
+
),
|
|
284
|
+
zOrder: z
|
|
285
|
+
.number()
|
|
286
|
+
.int()
|
|
287
|
+
.default(0)
|
|
288
|
+
.describe('Canvas stacking order (COMPOSITION-DESIGN); ties broken by array order'),
|
|
289
|
+
pausable: z
|
|
290
|
+
.boolean()
|
|
291
|
+
.default(true)
|
|
292
|
+
.describe('Whether play-mode pause/step applies to this root'),
|
|
293
|
+
loop: z
|
|
294
|
+
.enum(['gated', 'self-driven'])
|
|
295
|
+
.default('gated')
|
|
296
|
+
.describe(
|
|
297
|
+
'gated: host-driven loop (default). self-driven: this root drives its own loop ' +
|
|
298
|
+
'(the "composited, unsynchronized" tier) — an axis independent of capability tier',
|
|
299
|
+
),
|
|
300
|
+
capabilities: z
|
|
301
|
+
.object({
|
|
302
|
+
local: TierSchema.optional().describe(
|
|
303
|
+
'Declared capability tier when served via the local CLI (Vite pipeline); omitted -> derived (§4)',
|
|
304
|
+
),
|
|
305
|
+
hosted: TierSchema.optional().describe(
|
|
306
|
+
'Declared capability tier when served hosted (no bundler: esbuild-wasm + externals only); omitted -> derived (§4)',
|
|
307
|
+
),
|
|
308
|
+
})
|
|
309
|
+
.optional()
|
|
310
|
+
.describe(
|
|
311
|
+
"Per-delivery-context capability tier declarations (§4). Declaring above the adapter's " +
|
|
312
|
+
'ceiling for that context is an error; declaring at or below it (including `unsupported`) is legal.',
|
|
313
|
+
),
|
|
314
|
+
})
|
|
315
|
+
.strict();
|
|
316
|
+
export type AdapterRoot = z.infer<typeof AdapterRootSchema>;
|
|
305
317
|
|
|
306
318
|
// ---------------------------------------------------------------------------
|
|
307
319
|
// Game manifest (vgai.game.json)
|
|
@@ -309,8 +321,25 @@ export type WorldEntry = z.infer<typeof WorldEntrySchema>;
|
|
|
309
321
|
|
|
310
322
|
export const GameManifestSchema = z
|
|
311
323
|
.object({
|
|
312
|
-
|
|
324
|
+
$schema: z
|
|
325
|
+
.string()
|
|
326
|
+
.optional()
|
|
327
|
+
.describe('Optional JSON Schema URI for editor autocomplete and validation'),
|
|
328
|
+
manifestVersion: z
|
|
329
|
+
.literal(GAME_MANIFEST_VERSION)
|
|
330
|
+
.describe('Manifest file format version (v2 clean roots)'),
|
|
313
331
|
name: z.string().describe('Game display name'),
|
|
332
|
+
appId: z
|
|
333
|
+
.string()
|
|
334
|
+
.optional()
|
|
335
|
+
.describe('Stable reverse-domain application id used by packaged targets'),
|
|
336
|
+
editorPort: z
|
|
337
|
+
.number()
|
|
338
|
+
.int()
|
|
339
|
+
.min(1024)
|
|
340
|
+
.max(65535)
|
|
341
|
+
.optional()
|
|
342
|
+
.describe('Stable preferred local editor port; launchers may fall forward when occupied'),
|
|
314
343
|
version: z.string().describe("The game's own version (used for orphan detection)"),
|
|
315
344
|
engine: z
|
|
316
345
|
.object({
|
|
@@ -322,22 +351,22 @@ export const GameManifestSchema = z
|
|
|
322
351
|
),
|
|
323
352
|
})
|
|
324
353
|
.describe('Engine version pin'),
|
|
325
|
-
|
|
326
|
-
.array(
|
|
354
|
+
roots: z
|
|
355
|
+
.array(AdapterRootSchema)
|
|
327
356
|
.min(1)
|
|
328
|
-
.superRefine((
|
|
357
|
+
.superRefine((roots, ctx) => {
|
|
329
358
|
// D-V6 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md §1G debt,
|
|
330
|
-
// docs/MULTI-WORLD-DESIGN.md §1G):
|
|
359
|
+
// docs/MULTI-WORLD-DESIGN.md §1G): root ids are described as unique
|
|
331
360
|
// above but that was describe-only at the SCHEMA level — only
|
|
332
361
|
// load.ts's loader-level cross-field check caught a collision, so
|
|
333
362
|
// any OTHER consumer of this schema (the generated JSON Schema,
|
|
334
363
|
// editor tooling) saw duplicate ids as legal. Enforced HERE instead,
|
|
335
364
|
// naming every colliding id (not just the first found) — overlay
|
|
336
|
-
// files are keyed by
|
|
337
|
-
// silently-permitted duplicate would have two
|
|
365
|
+
// files are keyed by root id (`.vgai/overlays/<id>.json`), so a
|
|
366
|
+
// silently-permitted duplicate would have two roots silently share
|
|
338
367
|
// one overlay.
|
|
339
368
|
const counts = new Map<string, number>();
|
|
340
|
-
for (const
|
|
369
|
+
for (const root of roots) counts.set(root.id, (counts.get(root.id) ?? 0) + 1);
|
|
341
370
|
const duplicates = [...counts.entries()].filter(([, n]) => n > 1).map(([id]) => id);
|
|
342
371
|
if (duplicates.length === 0) return;
|
|
343
372
|
const idsText = duplicates.map((id) => `"${id}"`).join(', ');
|
|
@@ -345,11 +374,40 @@ export const GameManifestSchema = z
|
|
|
345
374
|
code: z.ZodIssueCode.custom,
|
|
346
375
|
message:
|
|
347
376
|
duplicates.length === 1
|
|
348
|
-
? `Game manifest: duplicate
|
|
349
|
-
: `Game manifest: duplicate
|
|
377
|
+
? `Game manifest: duplicate root id ${idsText} — root ids must be unique across the manifest (enforced-unique).`
|
|
378
|
+
: `Game manifest: duplicate root ids: ${idsText} — root ids must be unique across the manifest (enforced-unique).`,
|
|
350
379
|
});
|
|
351
380
|
})
|
|
352
|
-
.describe(
|
|
381
|
+
.describe(
|
|
382
|
+
"The Game's explicit, non-empty adapter-root composition. There is no implicit root.",
|
|
383
|
+
),
|
|
384
|
+
authoring: z
|
|
385
|
+
.object({
|
|
386
|
+
hierarchy: z
|
|
387
|
+
.object({
|
|
388
|
+
rootLabels: z
|
|
389
|
+
.record(z.string(), z.string())
|
|
390
|
+
.optional()
|
|
391
|
+
.describe('Optional editor labels keyed by adapter-root id'),
|
|
392
|
+
groups: z
|
|
393
|
+
.array(
|
|
394
|
+
z.object({
|
|
395
|
+
id: z.string().describe('Unique editor-only group id'),
|
|
396
|
+
label: z.string().describe('Editor-only group label'),
|
|
397
|
+
roots: z
|
|
398
|
+
.array(z.string())
|
|
399
|
+
.min(1)
|
|
400
|
+
.describe('Adapter-root ids shown under this editor-only group'),
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
.optional()
|
|
404
|
+
.describe('Optional editor-only hierarchy groups'),
|
|
405
|
+
})
|
|
406
|
+
.optional()
|
|
407
|
+
.describe('Editor projection for native adapter roots'),
|
|
408
|
+
})
|
|
409
|
+
.optional()
|
|
410
|
+
.describe('Editor-only authoring metadata; never runtime ownership'),
|
|
353
411
|
server: z
|
|
354
412
|
.object({
|
|
355
413
|
room: z.string().describe('Colyseus room name'),
|
|
@@ -368,8 +426,57 @@ export const GameManifestSchema = z
|
|
|
368
426
|
height: z.number().describe('Canvas height in pixels'),
|
|
369
427
|
})
|
|
370
428
|
.optional()
|
|
371
|
-
.describe(
|
|
429
|
+
.describe('Canvas resolution'),
|
|
430
|
+
debug: z
|
|
431
|
+
.object({
|
|
432
|
+
allowInProduction: z
|
|
433
|
+
.boolean()
|
|
434
|
+
.describe(
|
|
435
|
+
'Allow the ?vgai-debug=1 introspection bridge and debug-command invocation in ' +
|
|
436
|
+
'production builds. Default false: the bridge only installs in dev builds. The ' +
|
|
437
|
+
'runtime reader is the debug-bridge installer (D18).',
|
|
438
|
+
),
|
|
439
|
+
})
|
|
440
|
+
.optional()
|
|
441
|
+
.describe(
|
|
442
|
+
'Debug-bridge production gating (D18) — governs whether ?vgai-debug=1 installs ' +
|
|
443
|
+
'window.__vgai outside dev builds.',
|
|
444
|
+
),
|
|
445
|
+
determinism: z
|
|
446
|
+
.object({
|
|
447
|
+
seededRandom: z
|
|
448
|
+
.boolean()
|
|
449
|
+
.describe(
|
|
450
|
+
'Declares that ALL gameplay RNG in this project flows through ctx.random (the ' +
|
|
451
|
+
'named-stream seeded PRNG, docs/D15-DETERMINISM-DESIGN.md) rather than raw ' +
|
|
452
|
+
'Math.random/Date.now/performance.now. Three runtime enforcers key off this flag: ' +
|
|
453
|
+
'the boot-time seeding reader (mount-manifest.ts seeds ctx.random from ' +
|
|
454
|
+
'defaultSeed/?vgai-seed=/explicit config, in that precedence, only when true), the ' +
|
|
455
|
+
'gameplay-rng-ban burn-down scan (test/gameplay-rng-ban.test.ts — lints this ' +
|
|
456
|
+
"project's src/ for raw RNG/wall-clock calls), and the dev-mode Math.random phase " +
|
|
457
|
+
'trap (runtime/gameplay-rng-trap.ts — warns once per call site during a gameplay ' +
|
|
458
|
+
'frame, never throws). Default false: an undeclared project gets no determinism ' +
|
|
459
|
+
'contract at all (that IS the opt-out — no dead field, per the T4.1 policy).',
|
|
460
|
+
),
|
|
461
|
+
defaultSeed: z
|
|
462
|
+
.number()
|
|
463
|
+
.int()
|
|
464
|
+
.optional()
|
|
465
|
+
.describe(
|
|
466
|
+
'The seed ctx.random boots from when seededRandom is true, unless overridden by ' +
|
|
467
|
+
'?vgai-seed=<int> (the query param always wins over this manifest default) or an ' +
|
|
468
|
+
'even higher-precedence explicit config value a host passes directly. Optional — ' +
|
|
469
|
+
"omit to fall back to the runtime's own fixed default seed.",
|
|
470
|
+
),
|
|
471
|
+
})
|
|
472
|
+
.optional()
|
|
473
|
+
.describe(
|
|
474
|
+
'Seeded-RNG determinism contract (D15, docs/D15-DETERMINISM-DESIGN.md) — governs ' +
|
|
475
|
+
'whether ctx.random is boot-seeded from a reproducible seed and whether the ' +
|
|
476
|
+
'gameplay-rng-ban scan / dev-mode Math.random phase trap are active for this project.',
|
|
477
|
+
),
|
|
372
478
|
})
|
|
373
|
-
.
|
|
479
|
+
.strict()
|
|
480
|
+
.describe('Game manifest (vgai.game.json) — the complete v2 project configuration');
|
|
374
481
|
|
|
375
482
|
export type GameManifest = z.infer<typeof GameManifestSchema>;
|