@vgai/engine 0.2.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/LICENSE +202 -0
- package/README.md +35 -0
- package/package.json +55 -0
- package/src/adapter/authoring.ts +402 -0
- package/src/adapter/colyseus-networking-adapter.ts +72 -0
- package/src/adapter/first-party-systems.ts +103 -0
- package/src/adapter/game-adapter.ts +151 -0
- package/src/adapter/host-context.ts +77 -0
- package/src/adapter/index.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +59 -0
- package/src/adapter/ingest/overlay-applier.ts +207 -0
- package/src/adapter/ingest/overlay-apply.ts +124 -0
- package/src/adapter/ingest/overlay-file.ts +126 -0
- package/src/adapter/ingest/overlay-report.ts +176 -0
- package/src/adapter/ingest/scene-capture.ts +307 -0
- package/src/adapter/ingest/upstream-pin.ts +52 -0
- package/src/adapter/loop-gate-report.ts +54 -0
- package/src/adapter/rapier-physics-adapter.ts +56 -0
- package/src/adapter/system-adapter.ts +154 -0
- package/src/adapter/transform.ts +18 -0
- package/src/adapter/vgai-scene-game-adapter.ts +886 -0
- package/src/adapter/world-kind.ts +34 -0
- package/src/ai/navigation.ts +164 -0
- package/src/animation/anim-graph-types.ts +56 -0
- package/src/animation/anim-graph.ts +406 -0
- package/src/animation/anim-system.ts +28 -0
- package/src/animation/blend-node.ts +119 -0
- package/src/animation/property-track.ts +178 -0
- package/src/animation/schema.ts +204 -0
- package/src/assets.ts +80 -0
- package/src/audio/ambient.ts +300 -0
- package/src/audio/impacts.ts +212 -0
- package/src/audio/index.ts +7 -0
- package/src/audio/movement.ts +140 -0
- package/src/audio/musical.ts +200 -0
- package/src/audio/ui-sounds.ts +171 -0
- package/src/audio/vehicle.ts +235 -0
- package/src/audio/weapons.ts +152 -0
- package/src/core/game-loop.ts +127 -0
- package/src/core/system-runner.ts +298 -0
- package/src/core/types.ts +58 -0
- package/src/dev/console-bridge.ts +83 -0
- package/src/dev/debug-draw.ts +80 -0
- package/src/dev/logger.ts +119 -0
- package/src/ecs/component-manager.ts +748 -0
- package/src/ecs/game-component.ts +147 -0
- package/src/ecs/hmr-swap-report.ts +65 -0
- package/src/input/input-manager.ts +439 -0
- package/src/input/input-types.ts +19 -0
- package/src/input/schema.ts +129 -0
- package/src/loader.ts +70 -0
- package/src/manifest/index.ts +24 -0
- package/src/manifest/load-file.ts +16 -0
- package/src/manifest/load.ts +378 -0
- package/src/manifest/schema.ts +375 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/physics/trigger-dispatch.ts +97 -0
- package/src/react/game-state.tsx +172 -0
- package/src/render/auto-batcher.ts +169 -0
- package/src/render/render-batch-system.ts +268 -0
- package/src/render/render-features.ts +146 -0
- package/src/render/render-settings.ts +72 -0
- package/src/runtime/create-runtime.ts +1152 -0
- package/src/runtime/frame-selector-cache.ts +81 -0
- package/src/runtime/game.ts +1003 -0
- package/src/runtime/input-router.ts +213 -0
- package/src/runtime/mount-game.ts +269 -0
- package/src/runtime/mount-manifest.ts +361 -0
- package/src/runtime/scene-ui-bridge.ts +86 -0
- package/src/runtime/scene-ui-data.ts +119 -0
- package/src/runtime/state-bridge.ts +79 -0
- package/src/runtime/types.ts +196 -0
- package/src/scene/asset-loaders.ts +195 -0
- package/src/scene/asset-paths.ts +123 -0
- package/src/scene/asset-registry.ts +67 -0
- package/src/scene/collider-dimensions.ts +125 -0
- package/src/scene/component-registry.ts +40 -0
- package/src/scene/defaults.ts +164 -0
- package/src/scene/geometries/index.ts +7 -0
- package/src/scene/geometries/terrain.ts +42 -0
- package/src/scene/geometry-registry.ts +42 -0
- package/src/scene/instance-registry.ts +84 -0
- package/src/scene/instancers/grid.ts +38 -0
- package/src/scene/instancers/index.ts +7 -0
- package/src/scene/light-camera-factory.ts +97 -0
- package/src/scene/material-factory.ts +211 -0
- package/src/scene/material-registry.ts +73 -0
- package/src/scene/materials/index.ts +7 -0
- package/src/scene/materials/water.ts +56 -0
- package/src/scene/parse.ts +71 -0
- package/src/scene/particles-factory.ts +383 -0
- package/src/scene/scene-apply.ts +356 -0
- package/src/scene/scene-diff-schema.ts +115 -0
- package/src/scene/scene-diff-types.ts +29 -0
- package/src/scene/scene-loader.ts +1533 -0
- package/src/scene/scene-query.ts +63 -0
- package/src/scene/scene-types.ts +34 -0
- package/src/scene/scene-version.ts +40 -0
- package/src/scene/schema/animation.ts +95 -0
- package/src/scene/schema/audio.ts +25 -0
- package/src/scene/schema/camera.ts +21 -0
- package/src/scene/schema/collider.ts +69 -0
- package/src/scene/schema/entity-ref.ts +78 -0
- package/src/scene/schema/entity.ts +169 -0
- package/src/scene/schema/environment.ts +384 -0
- package/src/scene/schema/index.ts +95 -0
- package/src/scene/schema/instances.ts +35 -0
- package/src/scene/schema/joint.ts +26 -0
- package/src/scene/schema/light.ts +38 -0
- package/src/scene/schema/material.ts +113 -0
- package/src/scene/schema/mesh.ts +108 -0
- package/src/scene/schema/particles.ts +398 -0
- package/src/scene/schema/physics.ts +49 -0
- package/src/scene/schema/scene-file.ts +299 -0
- package/src/scene/schema/shadow.ts +24 -0
- package/src/scene/schema/spline.ts +21 -0
- package/src/scene/schema/tuples.ts +21 -0
- package/src/scene/schema/ui.ts +602 -0
- package/src/scene/user-data.ts +203 -0
- package/src/setup/setup-audio.ts +60 -0
- package/src/setup/setup-particles.ts +23 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +529 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/src/world2d/authoring-2d.ts +208 -0
- package/src/world2d/capture-to-scene2d.ts +52 -0
- package/src/world2d/collision-2d.ts +106 -0
- package/src/world2d/components-2d.ts +86 -0
- package/src/world2d/index.ts +66 -0
- package/src/world2d/ingest-iframe-2d.ts +255 -0
- package/src/world2d/ingest2d.ts +131 -0
- package/src/world2d/physics2d-registry.ts +49 -0
- package/src/world2d/pixi-game-adapter.ts +325 -0
- package/src/world2d/pixi-surface.ts +78 -0
- package/src/world2d/scene-capture-2d.ts +117 -0
- package/src/world2d/scene2d-loader.ts +308 -0
- package/src/world2d/schema/entity2d.ts +145 -0
- package/src/world2d/schema/physics2d.ts +53 -0
- package/src/world2d/schema/sprite.ts +71 -0
- package/src/world2d/schema/tilemap.ts +22 -0
- package/src/world2d/schema/tuples2d.ts +25 -0
- package/src/world2d/system-adapters-2d.ts +49 -0
- package/src/world2d/transform-writer-2d.ts +24 -0
- package/src/world2d/types.ts +55 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import { getUserData } from './user-data';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime queries over a loaded THREE scene graph (P1.6d).
|
|
6
|
+
*
|
|
7
|
+
* The scene's `tags` schema field is documented as "for runtime queries" but,
|
|
8
|
+
* until now, no query existed. These helpers walk a THREE.Object3D graph and
|
|
9
|
+
* read entity metadata off `userData`, so gameplay code can find objects by tag,
|
|
10
|
+
* name, or attached component without bookkeeping its own indexes.
|
|
11
|
+
*
|
|
12
|
+
* Canonical metadata locations (in priority order):
|
|
13
|
+
* - tags: `userData['tags']` (string[]) — falls back to `userData['entity'].tags`
|
|
14
|
+
* - name: `Object3D.name`
|
|
15
|
+
* - components: `userData['components']` (record or string[]) — falls back to
|
|
16
|
+
* `userData['entity'].components` (record keyed by component name)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function getTags(obj: THREE.Object3D): string[] {
|
|
20
|
+
const direct = getUserData(obj, 'tags');
|
|
21
|
+
if (Array.isArray(direct)) return direct;
|
|
22
|
+
const fromDef = getUserData(obj, 'entity')?.tags;
|
|
23
|
+
return Array.isArray(fromDef) ? fromDef : [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getComponentNames(obj: THREE.Object3D): string[] {
|
|
27
|
+
const direct = getUserData(obj, 'components');
|
|
28
|
+
if (Array.isArray(direct)) return direct;
|
|
29
|
+
if (direct && typeof direct === 'object') return Object.keys(direct);
|
|
30
|
+
const fromDef = getUserData(obj, 'entity')?.components;
|
|
31
|
+
if (fromDef && typeof fromDef === 'object') return Object.keys(fromDef);
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** All objects in the graph carrying the given tag (depth-first, document order). */
|
|
36
|
+
export function queryByTag(root: THREE.Object3D, tag: string): THREE.Object3D[] {
|
|
37
|
+
const matches: THREE.Object3D[] = [];
|
|
38
|
+
root.traverse((obj) => {
|
|
39
|
+
if (getTags(obj).includes(tag)) matches.push(obj);
|
|
40
|
+
});
|
|
41
|
+
return matches;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** All objects in the graph with the given name (depth-first, document order). */
|
|
45
|
+
export function queryByName(root: THREE.Object3D, name: string): THREE.Object3D[] {
|
|
46
|
+
const matches: THREE.Object3D[] = [];
|
|
47
|
+
root.traverse((obj) => {
|
|
48
|
+
if (obj.name === name) matches.push(obj);
|
|
49
|
+
});
|
|
50
|
+
return matches;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* All objects in the graph that have the named component attached
|
|
55
|
+
* (depth-first, document order).
|
|
56
|
+
*/
|
|
57
|
+
export function queryByComponent(root: THREE.Object3D, componentName: string): THREE.Object3D[] {
|
|
58
|
+
const matches: THREE.Object3D[] = [];
|
|
59
|
+
root.traverse((obj) => {
|
|
60
|
+
if (getComponentNames(obj).includes(componentName)) matches.push(obj);
|
|
61
|
+
});
|
|
62
|
+
return matches;
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene file types — derived from Zod schemas (single source of truth).
|
|
3
|
+
*
|
|
4
|
+
* Raw types for the .vscn.json file format. Both the editor and the runtime
|
|
5
|
+
* loader work with these directly, applying DEFAULTS (./defaults.ts) for omitted
|
|
6
|
+
* fields — DEFAULTS is the single source of truth for default values.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
InstancesFile,
|
|
11
|
+
MaterialFile,
|
|
12
|
+
PrefabFile,
|
|
13
|
+
SceneAnimation,
|
|
14
|
+
SceneAudio,
|
|
15
|
+
SceneCamera,
|
|
16
|
+
SceneCollider,
|
|
17
|
+
SceneEntity,
|
|
18
|
+
SceneEnvironment,
|
|
19
|
+
SceneFile,
|
|
20
|
+
SceneJoint,
|
|
21
|
+
SceneLight,
|
|
22
|
+
SceneMaterial,
|
|
23
|
+
SceneMesh,
|
|
24
|
+
SceneNavigation,
|
|
25
|
+
SceneParticles,
|
|
26
|
+
ScenePhysics,
|
|
27
|
+
ScenePhysicsSettings,
|
|
28
|
+
ScenePostProcessing,
|
|
29
|
+
SceneSpline,
|
|
30
|
+
SceneToneMapping,
|
|
31
|
+
UIRoot,
|
|
32
|
+
} from './schema';
|
|
33
|
+
|
|
34
|
+
export { mergePrefabInstance } from './schema';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene/prefab file format `version` range this engine checkout can
|
|
3
|
+
* load (T2.3/D3 §1.E, docs/ENGINE-UPGRADE-DESIGN.md). `[min, max]` — today
|
|
4
|
+
* `[1, 1]`: every shipped `.vscn.json`/`.prefab.json` is version 1, and no
|
|
5
|
+
* migration machinery exists yet (a future format bump widens `max`, and a
|
|
6
|
+
* future engine that drops support for `1` would raise `min`).
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NOT in `./schema/` (that directory's byte contents are
|
|
9
|
+
* pinned by `packages/engine/schemas/engine-capabilities.json`'s
|
|
10
|
+
* `schemaDirHash`, refreshed only by `scripts/capabilities-hash.ts` — this
|
|
11
|
+
* slice does not run that script). This is also why the check below is a
|
|
12
|
+
* POST-Zod-parse function here in `scene/`, not a Zod refinement inside
|
|
13
|
+
* `SceneFileSchema`/`PrefabFileSchema`: the generated `vscn.schema.json`/
|
|
14
|
+
* `prefab.schema.json` stay byte-identical (the `version` field itself is
|
|
15
|
+
* still just `z.number()`).
|
|
16
|
+
*/
|
|
17
|
+
export const SUPPORTED_SCENE_VERSION_RANGE: readonly [number, number] = [1, 1];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Throw if `version` falls outside `SUPPORTED_SCENE_VERSION_RANGE`; silently
|
|
21
|
+
* accept in-range versions. `fileKind` only changes the error message's noun
|
|
22
|
+
* (`.vscn.json` and `.prefab.json` share this single version axis — verified
|
|
23
|
+
* both schemas declare their own `version: z.number()` field, not a shared
|
|
24
|
+
* ref, but the same counter/semantics per docs/ENGINE-UPGRADE-DESIGN.md).
|
|
25
|
+
*/
|
|
26
|
+
export function checkSceneVersionInRange(
|
|
27
|
+
version: number,
|
|
28
|
+
fileKind: 'scene' | 'prefab' = 'scene',
|
|
29
|
+
): void {
|
|
30
|
+
const [min, max] = SUPPORTED_SCENE_VERSION_RANGE;
|
|
31
|
+
if (version >= min && version <= max) return;
|
|
32
|
+
const cause =
|
|
33
|
+
version > max
|
|
34
|
+
? `the engine checkout is older than this ${fileKind}`
|
|
35
|
+
: `this ${fileKind} predates the oldest supported format version`;
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${fileKind} version ${version} is outside the supported range [${min}, ${max}] — ` +
|
|
38
|
+
`${cause}; see docs/ENGINE-UPGRADE-DESIGN.md`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Reuse the UI track easing vocabulary (S1) so there is ONE easing enum across
|
|
3
|
+
// the codebase — see F5 (docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Do NOT reuse
|
|
4
|
+
// UIKeyframeSchema itself: its `value` is `z.union([z.number(), z.string()])`,
|
|
5
|
+
// and property-track values MUST be numbers only (no expression/string-logic
|
|
6
|
+
// values — in-scene scripting is banned, G1).
|
|
7
|
+
import { UIEasingSchema } from './ui';
|
|
8
|
+
|
|
9
|
+
/** Validated at runtime against AnimGraph parameter definitions. */
|
|
10
|
+
const AnimParameterOverridesSchema = z.record(z.string(), z.union([z.number(), z.boolean()]));
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// F5 — Declarative property tracks (deterministic timeline).
|
|
14
|
+
//
|
|
15
|
+
// A track is DATA: a pure function of time (keyframes + named easing), never
|
|
16
|
+
// behavior. `target` is a FIXED allowlist, not an arbitrary property path —
|
|
17
|
+
// this is the guardrail that keeps the format from becoming an in-scene
|
|
18
|
+
// scripting surface (G1, docs/VSCN-STRUCTURAL-GAPS-DESIGN.md). Anything
|
|
19
|
+
// state-dependent ("move WHEN the player steps on it") stays a GameComponent,
|
|
20
|
+
// which may start/stop/seek a track — it never grows conditionals here.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export const PropertyTrackTargetSchema = z
|
|
24
|
+
.enum([
|
|
25
|
+
'position.x',
|
|
26
|
+
'position.y',
|
|
27
|
+
'position.z',
|
|
28
|
+
'rotation.x',
|
|
29
|
+
'rotation.y',
|
|
30
|
+
'rotation.z',
|
|
31
|
+
'scale.x',
|
|
32
|
+
'scale.y',
|
|
33
|
+
'scale.z',
|
|
34
|
+
'material.opacity',
|
|
35
|
+
'light.intensity',
|
|
36
|
+
])
|
|
37
|
+
.describe(
|
|
38
|
+
'The Object3D property this track drives. A FIXED allowlist, not an arbitrary property ' +
|
|
39
|
+
'path — an open path would let scene data address/mutate arbitrary runtime state, which ' +
|
|
40
|
+
'is in-scene scripting (banned, G1). Add new targets here deliberately, one at a time.',
|
|
41
|
+
);
|
|
42
|
+
export type PropertyTrackTarget = z.infer<typeof PropertyTrackTargetSchema>;
|
|
43
|
+
|
|
44
|
+
export const PropertyTrackKeyframeSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
time: z.number().describe('Seconds from track start'),
|
|
47
|
+
value: z
|
|
48
|
+
.number()
|
|
49
|
+
.describe(
|
|
50
|
+
'Numeric value at this time — numbers only; no expressions/strings (in-scene scripting is banned)',
|
|
51
|
+
),
|
|
52
|
+
easing: UIEasingSchema.optional().describe(
|
|
53
|
+
'Easing of the segment after this keyframe (reuses the UI track easing vocabulary)',
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
.describe('A single keyframe (time + numeric value + optional easing) on a property track');
|
|
57
|
+
export type PropertyTrackKeyframe = z.infer<typeof PropertyTrackKeyframeSchema>;
|
|
58
|
+
|
|
59
|
+
export const PropertyTrackSchema = z
|
|
60
|
+
.object({
|
|
61
|
+
target: PropertyTrackTargetSchema,
|
|
62
|
+
keyframes: z.array(PropertyTrackKeyframeSchema).describe('Ordered by time'),
|
|
63
|
+
loop: z.boolean().optional().describe('Whether the track repeats after reaching `duration`'),
|
|
64
|
+
duration: z.number().describe('Track length in seconds'),
|
|
65
|
+
})
|
|
66
|
+
.describe('A deterministic keyframed timeline driving one allowlisted property');
|
|
67
|
+
export type PropertyTrack = z.infer<typeof PropertyTrackSchema>;
|
|
68
|
+
|
|
69
|
+
export const SceneAnimationSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
animGraph: z.string().optional().describe('Path to .animgraph.json state machine file'),
|
|
72
|
+
parameters: AnimParameterOverridesSchema.optional().describe(
|
|
73
|
+
'Initial parameter values for the animation graph',
|
|
74
|
+
),
|
|
75
|
+
autoplay: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Clip name to play automatically on load (when no animGraph is set)'),
|
|
79
|
+
loop: z.boolean().optional().describe('Whether the autoplay clip should loop'),
|
|
80
|
+
clipAliases: z
|
|
81
|
+
.record(z.string(), z.string())
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Map raw GLTF clip names to clean names used by the animation graph'),
|
|
84
|
+
tracks: z
|
|
85
|
+
.array(PropertyTrackSchema)
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(
|
|
88
|
+
'Deterministic keyframed property timelines (data, not behavior — pure function of ' +
|
|
89
|
+
'time; state-dependent motion stays a GameComponent). Runs in the animation phase, ' +
|
|
90
|
+
'honors pause/timeScale.',
|
|
91
|
+
),
|
|
92
|
+
})
|
|
93
|
+
.describe('Animation playback configuration');
|
|
94
|
+
|
|
95
|
+
export type SceneAnimation = z.infer<typeof SceneAnimationSchema>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const SceneAudioSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
src: z.string().optional().describe('Path to audio file (WAV, MP3, OGG)'),
|
|
6
|
+
spatial: z.boolean().optional().describe('Enable 3D spatial audio (positional sound)'),
|
|
7
|
+
volume: z.number().optional().describe('Playback volume 0–1'),
|
|
8
|
+
refDistance: z
|
|
9
|
+
.number()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Distance at which volume starts to attenuate (spatial only)'),
|
|
12
|
+
rolloffFactor: z
|
|
13
|
+
.number()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('How quickly volume attenuates with distance (spatial only)'),
|
|
16
|
+
maxDistance: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Maximum distance at which sound is audible (spatial only)'),
|
|
20
|
+
loop: z.boolean().optional().describe('Whether the audio should loop'),
|
|
21
|
+
autoplay: z.boolean().optional().describe('Start playing automatically when the scene loads'),
|
|
22
|
+
})
|
|
23
|
+
.describe('Audio source configuration');
|
|
24
|
+
|
|
25
|
+
export type SceneAudio = z.infer<typeof SceneAudioSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const SceneCameraSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
type: z.enum(['perspective', 'orthographic']).describe('Camera projection type'),
|
|
6
|
+
fov: z.number().optional().describe('Vertical field of view in degrees (perspective only)'),
|
|
7
|
+
near: z.number().optional().describe('Near clipping plane distance'),
|
|
8
|
+
far: z.number().optional().describe('Far clipping plane distance'),
|
|
9
|
+
orthoSize: z
|
|
10
|
+
.number()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Half-height of the orthographic frustum (orthographic only)'),
|
|
13
|
+
width: z.number().optional().describe('Render width in pixels. Defaults to viewport width'),
|
|
14
|
+
height: z.number().optional().describe('Render height in pixels. Defaults to viewport height'),
|
|
15
|
+
})
|
|
16
|
+
.describe('Camera configuration');
|
|
17
|
+
|
|
18
|
+
/** Partial schema for prefab instance overrides (type not required). */
|
|
19
|
+
export const SceneCameraOverrideSchema = SceneCameraSchema.partial();
|
|
20
|
+
|
|
21
|
+
export type SceneCamera = z.infer<typeof SceneCameraSchema>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Vec3Schema } from './tuples';
|
|
3
|
+
|
|
4
|
+
export const SceneColliderSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
type: z
|
|
7
|
+
.enum(['cuboid', 'ball', 'capsule', 'trimesh'])
|
|
8
|
+
.describe(
|
|
9
|
+
"Collider shape type. T1.2: every shape scales with the entity's composed world scale " +
|
|
10
|
+
"(self × ancestors) — for trimesh this means the entity's own mesh geometry vertices " +
|
|
11
|
+
'are pre-scaled per-axis before building the Rapier trimesh, so it matches whatever ' +
|
|
12
|
+
"size ends up on screen; it does not require the collider's own dimension fields.",
|
|
13
|
+
),
|
|
14
|
+
halfExtents: Vec3Schema.optional().describe(
|
|
15
|
+
"Half-size [x, y, z] for cuboid colliders, in the ENTITY'S OWN LOCAL units. T1.2: " +
|
|
16
|
+
"scales with the entity — the actual Rapier collider is this value times the entity's " +
|
|
17
|
+
'composed world scale (self × every transformed ancestor), per axis, baked in once at ' +
|
|
18
|
+
"load time (Rapier has no live notion of an Object3D's scale).",
|
|
19
|
+
),
|
|
20
|
+
radius: z
|
|
21
|
+
.number()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(
|
|
24
|
+
"Radius for ball and capsule colliders, in the ENTITY'S OWN LOCAL units. T1.2: scales " +
|
|
25
|
+
"with the entity's composed world scale, which MUST be uniform on the collider's round " +
|
|
26
|
+
'axes (ball: all of x/y/z; capsule: x and z) — a non-uniform scale there throws a loud ' +
|
|
27
|
+
'load error (Rapier balls/capsules cannot represent an ellipse).',
|
|
28
|
+
),
|
|
29
|
+
halfHeight: z
|
|
30
|
+
.number()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"Half-height for capsule colliders, in the entity's own local units, along the capsule's " +
|
|
34
|
+
"axis (Y). T1.2: scales by the entity's composed world scale on that axis.",
|
|
35
|
+
),
|
|
36
|
+
friction: z.number().optional().describe('Surface friction coefficient'),
|
|
37
|
+
restitution: z
|
|
38
|
+
.number()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Bounciness coefficient (0 = no bounce, 1 = perfect bounce)'),
|
|
41
|
+
density: z
|
|
42
|
+
.number()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Mass density. Used to compute mass from collider volume'),
|
|
45
|
+
offset: Vec3Schema.optional().describe(
|
|
46
|
+
"Translation offset relative to body origin, in the ENTITY'S OWN LOCAL units. T1.2/R1c: " +
|
|
47
|
+
"scales with the entity's composed world scale (self × every transformed ancestor), " +
|
|
48
|
+
'per axis, the same way halfExtents/radius/halfHeight do — an unscaled offset would ' +
|
|
49
|
+
"silently drift away from the entity's visual anchor point under any non-1 scale.",
|
|
50
|
+
),
|
|
51
|
+
isSensor: z
|
|
52
|
+
.boolean()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('When true, detects overlaps without physical blocking (trigger volume)'),
|
|
55
|
+
collisionGroups: z
|
|
56
|
+
.number()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Bitmask: collision layers this collider belongs to (max 16 bits)'),
|
|
59
|
+
collisionFilter: z
|
|
60
|
+
.number()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Bitmask: collision layers this collider interacts with (max 16 bits)'),
|
|
63
|
+
})
|
|
64
|
+
.describe('Physics collider shape and properties');
|
|
65
|
+
|
|
66
|
+
/** Partial schema for prefab instance overrides (type not required). */
|
|
67
|
+
export const SceneColliderOverrideSchema = SceneColliderSchema.partial();
|
|
68
|
+
|
|
69
|
+
export type SceneCollider = z.infer<typeof SceneColliderSchema>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* F6 (`docs/VSCN-STRUCTURAL-GAPS-DESIGN.md`) — a first-class, reusable
|
|
5
|
+
* reference from one entity to another, for a `GameComponent`'s own static
|
|
6
|
+
* Zod schema (e.g. `target: EntityRefSchema` — "this button opens THAT
|
|
7
|
+
* door"). A branded string: on disk (and through generic component-data
|
|
8
|
+
* validation) it is a plain string id, but a component field typed
|
|
9
|
+
* `EntityRef` is compile-time distinct from an arbitrary `string` field, so a
|
|
10
|
+
* bare label can't be assigned where a ref is expected.
|
|
11
|
+
*
|
|
12
|
+
* Resolution is a runtime concern, not a schema one: `scene-loader.ts`'s
|
|
13
|
+
* `resolveEntityRefs` (the post-spawn pass, modeled on `joint.target`'s
|
|
14
|
+
* existing resolution) rewrites the field from the authored string id to the
|
|
15
|
+
* live `THREE.Object3D` — through the SAME `idMap` `createSceneJoints`
|
|
16
|
+
* already uses, no new registry (G3) — before the referencing component's
|
|
17
|
+
* `init()` runs.
|
|
18
|
+
*/
|
|
19
|
+
export const EntityRefSchema = z
|
|
20
|
+
.string()
|
|
21
|
+
.brand<'EntityRef'>()
|
|
22
|
+
.describe(
|
|
23
|
+
'A reference to another entity by its authored id. On disk it is a plain string id; the ' +
|
|
24
|
+
'scene loader resolves it to the live THREE.Object3D (WorldNode-as-truth) before the ' +
|
|
25
|
+
"referencing component's init() runs. A ref to a missing id is a loud load error.",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export type EntityRef = z.infer<typeof EntityRefSchema>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Identity registry of the `EntityRefSchema` object (there is exactly one —
|
|
32
|
+
* this module's export), used by {@link isEntityRefSchema}.
|
|
33
|
+
*
|
|
34
|
+
* Why identity rather than a Zod "type name" check: zod 4's `.brand()` is a
|
|
35
|
+
* type-only cast (see zod's own source — `inst.brand = () => inst`), not a
|
|
36
|
+
* distinct wrapper class runtime code could switch on. A branded schema is,
|
|
37
|
+
* at runtime, just the underlying `ZodString` (here, after `.describe()`
|
|
38
|
+
* clones it once more — see zod's `check()`/`clone()`). So "is this an
|
|
39
|
+
* EntityRef field" can only be answered by reference identity against the
|
|
40
|
+
* one schema object every ref field is declared with — which is exactly
|
|
41
|
+
* what we want anyway: a look-alike `z.string()` field must NOT count.
|
|
42
|
+
*/
|
|
43
|
+
const ENTITY_REF_SCHEMAS = new WeakSet<object>();
|
|
44
|
+
ENTITY_REF_SCHEMAS.add(EntityRefSchema);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Just enough of zod 4's internal `_def` shape to unwrap wrapper schemas.
|
|
48
|
+
* Typed against `z.core.$ZodType` (the base/"core" schema type, NOT the
|
|
49
|
+
* fuller "classic" `z.ZodType`) because that's what a `z.ZodObject`'s
|
|
50
|
+
* `.shape` values are typed as (`ZodRawShape = core.$ZodShape`) — the type
|
|
51
|
+
* every field schema `resolveEntityRefs` (`scene-loader.ts`) actually walks
|
|
52
|
+
* arrives as.
|
|
53
|
+
*/
|
|
54
|
+
interface UnwrappableDef {
|
|
55
|
+
innerType?: z.core.$ZodType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Is `schema` an {@link EntityRefSchema} field — including when wrapped by
|
|
60
|
+
* `.optional()`, `.default(...)`, or `.nullable()` (the common ways a
|
|
61
|
+
* component author would declare an optional ref)? Unwraps those via zod 4's
|
|
62
|
+
* internal `_def.innerType` (there is no public reflection API for this —
|
|
63
|
+
* `packages/editor/src/components/inspectors/zod-fields.ts` uses the same
|
|
64
|
+
* mechanism to introspect component schemas for the inspector) down to the
|
|
65
|
+
* core schema, then checks it against the registered `EntityRefSchema`
|
|
66
|
+
* instance by reference.
|
|
67
|
+
*/
|
|
68
|
+
export function isEntityRefSchema(schema: z.core.$ZodType): boolean {
|
|
69
|
+
let cur: z.core.$ZodType | undefined = schema;
|
|
70
|
+
while (cur) {
|
|
71
|
+
if (ENTITY_REF_SCHEMAS.has(cur)) return true;
|
|
72
|
+
const inner: z.core.$ZodType | undefined = (cur as unknown as { _def?: UnwrappableDef })._def
|
|
73
|
+
?.innerType;
|
|
74
|
+
if (!inner) return false;
|
|
75
|
+
cur = inner;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildRenderingZod } from '../../render/render-features';
|
|
3
|
+
import { SceneAnimationSchema } from './animation';
|
|
4
|
+
import { SceneAudioSchema } from './audio';
|
|
5
|
+
import { SceneCameraOverrideSchema, SceneCameraSchema } from './camera';
|
|
6
|
+
import { SceneJointSchema } from './joint';
|
|
7
|
+
import { SceneLightOverrideSchema, SceneLightSchema } from './light';
|
|
8
|
+
import { SceneMaterialOverrideSchema, SceneMaterialSchema } from './material';
|
|
9
|
+
import { SceneMeshOverrideSchema, SceneMeshSchema } from './mesh';
|
|
10
|
+
import { SceneParticlesOverrideSchema, SceneParticlesSchema } from './particles';
|
|
11
|
+
import { ScenePhysicsOverrideSchema, ScenePhysicsSchema } from './physics';
|
|
12
|
+
import { SceneShadowOverrideSchema, SceneShadowSchema } from './shadow';
|
|
13
|
+
import { SceneSplineOverrideSchema, SceneSplineSchema } from './spline';
|
|
14
|
+
import { TransformSchema, Vec3Schema } from './tuples';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* User-defined ECS component data. Validated at runtime against ComponentRegistry.
|
|
18
|
+
*
|
|
19
|
+
* Scalars (number/string/boolean) plus homogeneous arrays of those scalars so
|
|
20
|
+
* that vec3 (number[3]) and other array fields produced by the schema-driven
|
|
21
|
+
* inspector round-trip through parse/validate. The component's own static Zod
|
|
22
|
+
* schema (GameComponent.schema) does the precise per-field validation at load.
|
|
23
|
+
*
|
|
24
|
+
* F6 (`docs/VSCN-STRUCTURAL-GAPS-DESIGN.md`) entity refs (`EntityRefSchema`,
|
|
25
|
+
* `./entity-ref.ts`) are branded strings — structurally still a `string` on
|
|
26
|
+
* disk and through this coarse, per-component-agnostic schema, so they
|
|
27
|
+
* already parse via the `z.string()` branch below without a dedicated union
|
|
28
|
+
* member. (Deliberately not added as an explicit branch: this record's VALUE
|
|
29
|
+
* schema is the one place in the file with no `.describe()` calls anywhere in
|
|
30
|
+
* it — see `schema-consumption-map.ts`'s `entity.components` entry — and
|
|
31
|
+
* `EntityRefSchema` carries one, which would flip `entity.components` from a
|
|
32
|
+
* plain leaf into a `subtree: true` entry for no behavioral gain: the
|
|
33
|
+
* precise, per-field validation of a ref — and the brand — comes from the
|
|
34
|
+
* referencing component's OWN static schema in `applyComponents`, not from
|
|
35
|
+
* this generic envelope.)
|
|
36
|
+
*/
|
|
37
|
+
const ComponentScalarSchema = z.union([z.number(), z.string(), z.boolean()]);
|
|
38
|
+
const ComponentValueSchema = z.union([
|
|
39
|
+
ComponentScalarSchema,
|
|
40
|
+
z.array(z.number()),
|
|
41
|
+
z.array(z.string()),
|
|
42
|
+
z.array(z.boolean()),
|
|
43
|
+
]);
|
|
44
|
+
const ComponentDataSchema = z.record(z.string(), ComponentValueSchema);
|
|
45
|
+
const ComponentsSchema = z.record(z.string(), ComponentDataSchema);
|
|
46
|
+
|
|
47
|
+
const NavigationRoleSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
role: z
|
|
50
|
+
.enum(['walkable', 'obstacle'])
|
|
51
|
+
.describe('Whether this entity is walkable surface or an obstacle'),
|
|
52
|
+
})
|
|
53
|
+
.describe('Navigation mesh role');
|
|
54
|
+
|
|
55
|
+
// Fields shared by both standalone and prefab instance entities
|
|
56
|
+
const SharedEntityFields = {
|
|
57
|
+
id: z.string().optional().describe('Unique entity ID. Auto-generated if omitted'),
|
|
58
|
+
name: z.string().describe('Human-readable entity name (shown in hierarchy panel)'),
|
|
59
|
+
visible: z.boolean().optional().describe('Whether the entity and its children are rendered'),
|
|
60
|
+
locked: z.boolean().optional().describe('Prevent selection and editing in the scene editor'),
|
|
61
|
+
tags: z.array(z.string()).optional().describe('Arbitrary string tags for runtime queries'),
|
|
62
|
+
prefab: z.string().optional().describe('Path to .prefab.json file to instantiate'),
|
|
63
|
+
materialRef: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Path to shared .mat.json material asset. Mutually exclusive with inline material'),
|
|
67
|
+
transform: TransformSchema.optional().describe('Local transform relative to parent'),
|
|
68
|
+
pivot: Vec3Schema.optional().describe(
|
|
69
|
+
'Local-space pivot point for rotation/scale. [0,0,0] = geometry center',
|
|
70
|
+
),
|
|
71
|
+
navigation: NavigationRoleSchema.optional().describe('Navigation mesh role for this entity'),
|
|
72
|
+
render: buildRenderingZod({ perEntityOnly: true })
|
|
73
|
+
.optional()
|
|
74
|
+
.describe(
|
|
75
|
+
'Per-entity render overrides (e.g. autoBatch:false to keep a custom-shader entity out of batching). Omit to inherit from the scene/engine default.',
|
|
76
|
+
),
|
|
77
|
+
components: ComponentsSchema.optional().describe(
|
|
78
|
+
'User-defined ECS component data (validated against component registry)',
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Standalone entities: full section validation (type fields required)
|
|
83
|
+
const StandaloneEntityBase = z.object({
|
|
84
|
+
...SharedEntityFields,
|
|
85
|
+
mesh: SceneMeshSchema.optional().describe('Mesh geometry'),
|
|
86
|
+
material: SceneMaterialSchema.optional().describe('Material appearance'),
|
|
87
|
+
light: SceneLightSchema.optional().describe('Light source'),
|
|
88
|
+
shadow: SceneShadowSchema.optional().describe('Shadow configuration'),
|
|
89
|
+
physics: ScenePhysicsSchema.optional().describe('Physics rigid body'),
|
|
90
|
+
camera: SceneCameraSchema.optional().describe('Camera'),
|
|
91
|
+
audio: SceneAudioSchema.optional().describe('Audio source'),
|
|
92
|
+
animation: SceneAnimationSchema.optional().describe('Animation configuration'),
|
|
93
|
+
particles: SceneParticlesSchema.optional().describe('Particle system'),
|
|
94
|
+
spline: SceneSplineSchema.optional().describe('Spline path'),
|
|
95
|
+
joints: z
|
|
96
|
+
.array(SceneJointSchema)
|
|
97
|
+
.optional()
|
|
98
|
+
.describe('Physics joints connecting to other bodies'),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Prefab instance entities: section overrides are partial (merged with prefab root at load time)
|
|
102
|
+
const PrefabInstanceBase = z.object({
|
|
103
|
+
...SharedEntityFields,
|
|
104
|
+
prefab: z.string().describe('Path to .prefab.json file to instantiate'),
|
|
105
|
+
mesh: SceneMeshOverrideSchema.optional().describe('Mesh overrides merged with prefab'),
|
|
106
|
+
material: SceneMaterialOverrideSchema.optional().describe(
|
|
107
|
+
'Material overrides merged with prefab',
|
|
108
|
+
),
|
|
109
|
+
light: SceneLightOverrideSchema.optional().describe('Light overrides merged with prefab'),
|
|
110
|
+
shadow: SceneShadowOverrideSchema.optional().describe('Shadow overrides merged with prefab'),
|
|
111
|
+
physics: ScenePhysicsOverrideSchema.optional().describe('Physics overrides merged with prefab'),
|
|
112
|
+
camera: SceneCameraOverrideSchema.optional().describe('Camera overrides merged with prefab'),
|
|
113
|
+
audio: SceneAudioSchema.optional().describe('Audio source'),
|
|
114
|
+
animation: SceneAnimationSchema.optional().describe('Animation configuration'),
|
|
115
|
+
particles: SceneParticlesOverrideSchema.optional().describe(
|
|
116
|
+
'Particle overrides merged with prefab',
|
|
117
|
+
),
|
|
118
|
+
spline: SceneSplineOverrideSchema.optional().describe('Spline overrides merged with prefab'),
|
|
119
|
+
joints: z
|
|
120
|
+
.array(SceneJointSchema)
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('Physics joints connecting to other bodies'),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* SceneEntity type — the strict form used by all consumer code.
|
|
127
|
+
* Section types have required discriminant fields (type, bodyType, etc.).
|
|
128
|
+
*
|
|
129
|
+
* Prefab instances in scene files may have partial sections, but they are
|
|
130
|
+
* always completed by mergePrefabInstance() before being consumed.
|
|
131
|
+
*/
|
|
132
|
+
export type SceneEntity = z.infer<typeof StandaloneEntityBase> & {
|
|
133
|
+
children?: SceneEntity[] | undefined;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validation schema for scene entity JSON.
|
|
138
|
+
*
|
|
139
|
+
* Standalone entities are validated strictly (section discriminants required).
|
|
140
|
+
* Prefab instances accept partial section overrides — these are merged with
|
|
141
|
+
* the prefab root at load time via mergePrefabInstance().
|
|
142
|
+
*
|
|
143
|
+
* Typed as SceneEntity (strict) because prefab instances always go through
|
|
144
|
+
* mergePrefabInstance() before any code reads their section data.
|
|
145
|
+
*/
|
|
146
|
+
export const SceneEntitySchema: z.ZodType<SceneEntity> = z.lazy(() =>
|
|
147
|
+
z
|
|
148
|
+
.union([
|
|
149
|
+
StandaloneEntityBase.extend({
|
|
150
|
+
children: z.array(SceneEntitySchema).optional().describe('Child entities'),
|
|
151
|
+
}),
|
|
152
|
+
PrefabInstanceBase.extend({
|
|
153
|
+
children: z.array(SceneEntitySchema).optional().describe('Child entities'),
|
|
154
|
+
}),
|
|
155
|
+
])
|
|
156
|
+
// P1.6a: `material` (inline) and `materialRef` (shared .mat.json asset) are
|
|
157
|
+
// mutually exclusive — allowing both lets one silently win at load time.
|
|
158
|
+
// Reject up front instead of trapping the author.
|
|
159
|
+
.superRefine((entity, ctx) => {
|
|
160
|
+
if (entity.material !== undefined && entity.materialRef !== undefined) {
|
|
161
|
+
ctx.addIssue({
|
|
162
|
+
code: z.ZodIssueCode.custom,
|
|
163
|
+
message:
|
|
164
|
+
'An entity cannot set both `material` (inline) and `materialRef` (shared .mat.json asset). Choose one.',
|
|
165
|
+
path: ['material'],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
) as z.ZodType<SceneEntity>;
|