@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,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const SceneMaterialSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
type: z
|
|
6
|
+
.enum(['standard', 'physical', 'basic', 'toon', 'custom'])
|
|
7
|
+
.describe('Material shading model. "custom" resolves `def` against the material registry'),
|
|
8
|
+
def: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Registered custom-material name (required when type is "custom")'),
|
|
12
|
+
uniforms: z
|
|
13
|
+
.record(z.string(), z.union([z.number(), z.string(), z.boolean(), z.array(z.number())]))
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Authored uniform values for a custom material, keyed by uniform name'),
|
|
16
|
+
color: z.string().optional().describe('Base color as CSS hex string (e.g. "#ff0000")'),
|
|
17
|
+
metalness: z.number().optional().describe('Metalness factor 0–1 (standard/physical only)'),
|
|
18
|
+
roughness: z.number().optional().describe('Roughness factor 0–1 (standard/physical only)'),
|
|
19
|
+
map: z.string().optional().describe('Path to albedo/diffuse texture image'),
|
|
20
|
+
normalMap: z.string().optional().describe('Path to normal map texture image'),
|
|
21
|
+
emissive: z.string().optional().describe('Emissive color as CSS hex string'),
|
|
22
|
+
emissiveIntensity: z.number().optional().describe('Emissive light intensity multiplier'),
|
|
23
|
+
emissiveMap: z.string().optional().describe('Path to emissive map texture image'),
|
|
24
|
+
aoMap: z.string().optional().describe('Path to ambient occlusion map texture image'),
|
|
25
|
+
lightMap: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"Path to a baked lightmap texture (uses the mesh's second UV set). Applies the pre-baked GI/shadow result on top of the material.",
|
|
30
|
+
),
|
|
31
|
+
lightMapIntensity: z.number().optional().describe('Lightmap intensity multiplier (default 1).'),
|
|
32
|
+
roughnessMap: z.string().optional().describe('Path to roughness map texture image'),
|
|
33
|
+
metalnessMap: z.string().optional().describe('Path to metalness map texture image'),
|
|
34
|
+
opacity: z.number().optional().describe('Opacity 0–1. Set transparent: true to enable'),
|
|
35
|
+
transparent: z.boolean().optional().describe('Enable alpha blending for this material'),
|
|
36
|
+
side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
|
|
37
|
+
flatShading: z.boolean().optional().describe('Use flat (faceted) shading instead of smooth'),
|
|
38
|
+
|
|
39
|
+
// Physical material feature groups (physical only, all optional)
|
|
40
|
+
clearcoat: z
|
|
41
|
+
.object({
|
|
42
|
+
clearcoat: z.number().describe('Clearcoat layer intensity 0–1'),
|
|
43
|
+
clearcoatRoughness: z.number().optional().describe('Clearcoat roughness 0–1'),
|
|
44
|
+
clearcoatMap: z.string().optional().describe('Path to clearcoat intensity map'),
|
|
45
|
+
clearcoatRoughnessMap: z.string().optional().describe('Path to clearcoat roughness map'),
|
|
46
|
+
})
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Clearcoat layer (physical only)'),
|
|
49
|
+
|
|
50
|
+
transmission: z
|
|
51
|
+
.object({
|
|
52
|
+
transmission: z.number().describe('Transmission intensity 0–1 (glass, liquid)'),
|
|
53
|
+
ior: z.number().optional().describe('Index of refraction (default 1.5)'),
|
|
54
|
+
thickness: z.number().optional().describe('Volume thickness for refraction'),
|
|
55
|
+
attenuationColor: z.string().optional().describe('Color absorbed over distance'),
|
|
56
|
+
attenuationDistance: z
|
|
57
|
+
.number()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Distance at which attenuation color takes full effect'),
|
|
60
|
+
transmissionMap: z.string().optional().describe('Path to transmission map'),
|
|
61
|
+
})
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Light transmission (physical only)'),
|
|
64
|
+
|
|
65
|
+
sheen: z
|
|
66
|
+
.object({
|
|
67
|
+
sheen: z.number().describe('Sheen intensity 0–1 (fabric, velvet)'),
|
|
68
|
+
sheenColor: z.string().optional().describe('Sheen tint color'),
|
|
69
|
+
sheenRoughness: z.number().optional().describe('Sheen roughness 0–1'),
|
|
70
|
+
sheenColorMap: z.string().optional().describe('Path to sheen color map'),
|
|
71
|
+
sheenRoughnessMap: z.string().optional().describe('Path to sheen roughness map'),
|
|
72
|
+
})
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Sheen layer (physical only)'),
|
|
75
|
+
|
|
76
|
+
iridescence: z
|
|
77
|
+
.object({
|
|
78
|
+
iridescence: z.number().describe('Iridescence intensity 0–1 (soap bubbles, oil slick)'),
|
|
79
|
+
iridescenceIOR: z.number().optional().describe('Thin-film IOR (default 1.3)'),
|
|
80
|
+
iridescenceThicknessRange: z
|
|
81
|
+
.tuple([z.number(), z.number()])
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Min/max thin-film thickness in nm'),
|
|
84
|
+
iridescenceMap: z.string().optional().describe('Path to iridescence intensity map'),
|
|
85
|
+
iridescenceThicknessMap: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Path to iridescence thickness map'),
|
|
89
|
+
})
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Iridescence thin-film (physical only)'),
|
|
92
|
+
|
|
93
|
+
displacementMap: z.string().optional().describe('Path to displacement/height map'),
|
|
94
|
+
displacementScale: z.number().optional().describe('Displacement height multiplier'),
|
|
95
|
+
displacementBias: z.number().optional().describe('Displacement offset'),
|
|
96
|
+
})
|
|
97
|
+
.describe('Material appearance properties');
|
|
98
|
+
|
|
99
|
+
/** Partial schema for prefab instance overrides (type not required). */
|
|
100
|
+
export const SceneMaterialOverrideSchema = SceneMaterialSchema.partial();
|
|
101
|
+
|
|
102
|
+
export type SceneMaterial = z.infer<typeof SceneMaterialSchema>;
|
|
103
|
+
|
|
104
|
+
/** .mat.json file format for shared/reusable material assets. */
|
|
105
|
+
export const MaterialFileSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
version: z.number().describe('File format version'),
|
|
108
|
+
name: z.string().describe('Human-readable material name'),
|
|
109
|
+
material: SceneMaterialSchema.describe('Material definition'),
|
|
110
|
+
})
|
|
111
|
+
.describe('Shared material asset file (.mat.json)');
|
|
112
|
+
|
|
113
|
+
export type MaterialFile = z.infer<typeof MaterialFileSchema>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const SceneMeshBase = z.object({
|
|
4
|
+
type: z
|
|
5
|
+
.enum(['box', 'sphere', 'plane', 'cylinder', 'capsule', 'gltf'])
|
|
6
|
+
.describe('Mesh primitive type, or "gltf" for an external 3D model'),
|
|
7
|
+
args: z
|
|
8
|
+
.array(z.number())
|
|
9
|
+
.optional()
|
|
10
|
+
.describe(
|
|
11
|
+
'Geometry constructor args — box: [w,h,d], sphere: [radius], cylinder: [rTop,rBot,h], capsule: [radius,length], plane: [w,h]',
|
|
12
|
+
),
|
|
13
|
+
src: z.string().optional().describe('Path to GLTF/GLB model file. Required when type is "gltf"'),
|
|
14
|
+
node: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe(
|
|
18
|
+
'Name (or path) of a single node inside the referenced glTF (resolved via ' +
|
|
19
|
+
"getObjectByName) to instantiate as this entity's geometry, instead of the whole " +
|
|
20
|
+
'file. The geometry stays SHARED from the cached glTF — nothing is copied out. ' +
|
|
21
|
+
'Only meaningful with type: "gltf". CONSTRAINT: a skinned/animated node cannot be ' +
|
|
22
|
+
'lifted out of its armature — if the named node is a SkinnedMesh (or contains one), ' +
|
|
23
|
+
'resolving it throws (degrade loudly, not silently).',
|
|
24
|
+
),
|
|
25
|
+
instances: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
'Path to an instance-transform asset (.instances.json: an array of ' +
|
|
30
|
+
'[posX,posY,posZ, quatX,quatY,quatZ,quatW, scaleX,scaleY,scaleZ] tuples). Renders one ' +
|
|
31
|
+
'THREE.InstancedMesh (one draw call). Bulk transform data is referenced as an asset, ' +
|
|
32
|
+
'never inlined — keeps vscn thin.',
|
|
33
|
+
),
|
|
34
|
+
instancer: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe(
|
|
38
|
+
'Registered instancer name. When set, the mesh renders as a single THREE.InstancedMesh ' +
|
|
39
|
+
'(one draw call) with code-generated per-instance transforms instead of one THREE.Mesh',
|
|
40
|
+
),
|
|
41
|
+
instancerParams: z
|
|
42
|
+
.record(z.string(), z.union([z.number(), z.string(), z.boolean()]))
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Params for the instancer (e.g. count, spacing), keyed by name'),
|
|
45
|
+
generator: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
'Registered geometry-generator name (e.g. "Terrain"). When set, the mesh geometry is ' +
|
|
50
|
+
'produced procedurally in code instead of from `type`/`args`. Pairs with a "trimesh" ' +
|
|
51
|
+
'collider for matching physics.',
|
|
52
|
+
),
|
|
53
|
+
generatorParams: z
|
|
54
|
+
.record(z.string(), z.union([z.number(), z.string(), z.boolean()]))
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Params for the geometry generator (e.g. width, segments, amplitude)'),
|
|
57
|
+
lod: z
|
|
58
|
+
.array(
|
|
59
|
+
z.object({
|
|
60
|
+
distance: z
|
|
61
|
+
.number()
|
|
62
|
+
.describe(
|
|
63
|
+
'Camera distance in world units at/beyond which this level shows (THREE.LOD ' +
|
|
64
|
+
'threshold). Use 0 for the closest/highest-detail level.',
|
|
65
|
+
),
|
|
66
|
+
node: z
|
|
67
|
+
.string()
|
|
68
|
+
.describe(
|
|
69
|
+
'Name of a node inside the referenced glTF (mesh.src) to show at this distance — ' +
|
|
70
|
+
'resolved via getObjectByName, geometry shared (F4-style). A skinned node throws ' +
|
|
71
|
+
'(atomic-subtree rule).',
|
|
72
|
+
),
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
.optional()
|
|
76
|
+
.describe(
|
|
77
|
+
'Per-entity Level-of-Detail: distance-keyed glTF sub-nodes swapped by camera distance ' +
|
|
78
|
+
'(THREE.LOD). gltf-only. Gated by the environment.rendering.lod master toggle — when ' +
|
|
79
|
+
'off, only the closest level renders. Cannot combine with node/instances/instancer.',
|
|
80
|
+
),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const SceneMeshSchema = SceneMeshBase.refine((m) => m.type !== 'gltf' || m.src != null, {
|
|
84
|
+
message: "GLTF mesh requires 'src' field",
|
|
85
|
+
})
|
|
86
|
+
.refine((m) => m.node == null || m.type === 'gltf', {
|
|
87
|
+
message:
|
|
88
|
+
"Mesh 'node' is only meaningful with type: 'gltf' (names a sub-node of the referenced glTF)",
|
|
89
|
+
})
|
|
90
|
+
.refine((m) => !(m.instances && m.instancer), {
|
|
91
|
+
message:
|
|
92
|
+
"Mesh cannot set both 'instances' (authored) and 'instancer' (code-registered) — " +
|
|
93
|
+
'pick one canonical instancing source per mesh',
|
|
94
|
+
})
|
|
95
|
+
.refine((m) => m.lod == null || m.type === 'gltf', {
|
|
96
|
+
message: "Mesh 'lod' is only meaningful with type: 'gltf'",
|
|
97
|
+
})
|
|
98
|
+
.refine((m) => !(m.lod && (m.node || m.instances || m.instancer)), {
|
|
99
|
+
message:
|
|
100
|
+
"Mesh 'lod' cannot combine with 'node'/'instances'/'instancer' — LOD is the multi-level " +
|
|
101
|
+
'form of a node ref',
|
|
102
|
+
})
|
|
103
|
+
.describe('Mesh geometry definition');
|
|
104
|
+
|
|
105
|
+
/** Partial schema for prefab instance overrides (type not required). */
|
|
106
|
+
export const SceneMeshOverrideSchema = SceneMeshBase.partial();
|
|
107
|
+
|
|
108
|
+
export type SceneMesh = z.infer<typeof SceneMeshSchema>;
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Vec3Schema } from './tuples';
|
|
3
|
+
|
|
4
|
+
// --- Value Generators (mirrors quarks.core FunctionJSON) ---
|
|
5
|
+
|
|
6
|
+
const ConstantValueSchema = z.object({
|
|
7
|
+
type: z.literal('ConstantValue'),
|
|
8
|
+
value: z.number(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const IntervalValueSchema = z.object({
|
|
12
|
+
type: z.literal('IntervalValue'),
|
|
13
|
+
a: z.number(),
|
|
14
|
+
b: z.number(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const BezierSchema = z.object({
|
|
18
|
+
p0: z.number(),
|
|
19
|
+
p1: z.number(),
|
|
20
|
+
p2: z.number(),
|
|
21
|
+
p3: z.number(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const PiecewiseBezierSchema = z.object({
|
|
25
|
+
type: z.literal('PiecewiseBezier'),
|
|
26
|
+
functions: z.array(
|
|
27
|
+
z.object({
|
|
28
|
+
function: BezierSchema,
|
|
29
|
+
start: z.number(),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const ValueGeneratorSchema = z
|
|
35
|
+
.discriminatedUnion('type', [ConstantValueSchema, IntervalValueSchema, PiecewiseBezierSchema])
|
|
36
|
+
.describe('Numeric value: constant, random interval, or bezier curve');
|
|
37
|
+
|
|
38
|
+
export type ValueGeneratorJSON = z.infer<typeof ValueGeneratorSchema>;
|
|
39
|
+
|
|
40
|
+
// --- Color Generators (mirrors quarks.core ColorGenerator JSON) ---
|
|
41
|
+
|
|
42
|
+
const ColorRGBA = z.object({
|
|
43
|
+
r: z.number(),
|
|
44
|
+
g: z.number(),
|
|
45
|
+
b: z.number(),
|
|
46
|
+
a: z.number(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ColorRGB = z.object({
|
|
50
|
+
r: z.number(),
|
|
51
|
+
g: z.number(),
|
|
52
|
+
b: z.number(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** ContinuousLinearFunction JSON format used by Gradient internally */
|
|
56
|
+
const CLinearColorSchema = z.object({
|
|
57
|
+
type: z.literal('CLinearFunction'),
|
|
58
|
+
subType: z.literal('Color'),
|
|
59
|
+
keys: z.array(z.object({ value: ColorRGB, pos: z.number() })),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const CLinearNumberSchema = z.object({
|
|
63
|
+
type: z.literal('CLinearFunction'),
|
|
64
|
+
subType: z.literal('Number'),
|
|
65
|
+
keys: z.array(z.object({ value: z.number(), pos: z.number() })),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ConstantColorSchema = z.object({
|
|
69
|
+
type: z.literal('ConstantColor'),
|
|
70
|
+
color: ColorRGBA,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const ColorRangeSchema = z.object({
|
|
74
|
+
type: z.literal('ColorRange'),
|
|
75
|
+
a: ColorRGBA,
|
|
76
|
+
b: ColorRGBA,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const GradientSchema = z.object({
|
|
80
|
+
type: z.literal('Gradient'),
|
|
81
|
+
color: CLinearColorSchema,
|
|
82
|
+
alpha: CLinearNumberSchema,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const RandomColorBetweenGradientSchema = z.object({
|
|
86
|
+
type: z.literal('RandomColorBetweenGradient'),
|
|
87
|
+
gradient1: GradientSchema,
|
|
88
|
+
gradient2: GradientSchema,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const ColorGeneratorSchema = z
|
|
92
|
+
.discriminatedUnion('type', [
|
|
93
|
+
ConstantColorSchema,
|
|
94
|
+
ColorRangeSchema,
|
|
95
|
+
GradientSchema,
|
|
96
|
+
RandomColorBetweenGradientSchema,
|
|
97
|
+
])
|
|
98
|
+
.describe('Color generator: constant, range, gradient, or random between gradients');
|
|
99
|
+
|
|
100
|
+
export type ColorGeneratorJSON = z.infer<typeof ColorGeneratorSchema>;
|
|
101
|
+
|
|
102
|
+
// --- Emitter Shapes (mirrors quarks.core shape constructor params) ---
|
|
103
|
+
|
|
104
|
+
const PointEmitterSchema = z.object({ type: z.literal('point') });
|
|
105
|
+
|
|
106
|
+
const SphereEmitterSchema = z.object({
|
|
107
|
+
type: z.literal('sphere'),
|
|
108
|
+
radius: z.number().optional().describe('Sphere radius'),
|
|
109
|
+
arc: z.number().optional().describe('Arc angle in radians (2π = full sphere)'),
|
|
110
|
+
thickness: z.number().optional().describe('Shell thickness 0–1 (1 = solid, 0 = surface only)'),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const HemisphereEmitterSchema = z.object({
|
|
114
|
+
type: z.literal('hemisphere'),
|
|
115
|
+
radius: z.number().optional().describe('Hemisphere radius'),
|
|
116
|
+
arc: z.number().optional().describe('Arc angle in radians'),
|
|
117
|
+
thickness: z.number().optional().describe('Shell thickness 0–1'),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const ConeEmitterSchema = z.object({
|
|
121
|
+
type: z.literal('cone'),
|
|
122
|
+
radius: z.number().optional().describe('Cone base radius'),
|
|
123
|
+
arc: z.number().optional().describe('Arc angle in radians'),
|
|
124
|
+
thickness: z.number().optional().describe('Shell thickness 0–1'),
|
|
125
|
+
angle: z.number().optional().describe('Cone opening angle in radians'),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const CircleEmitterSchema = z.object({
|
|
129
|
+
type: z.literal('circle'),
|
|
130
|
+
radius: z.number().optional().describe('Circle radius'),
|
|
131
|
+
arc: z.number().optional().describe('Arc angle in radians'),
|
|
132
|
+
thickness: z.number().optional().describe('Ring thickness 0–1'),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const DonutEmitterSchema = z.object({
|
|
136
|
+
type: z.literal('donut'),
|
|
137
|
+
radius: z.number().optional().describe('Donut major radius'),
|
|
138
|
+
arc: z.number().optional().describe('Arc angle in radians'),
|
|
139
|
+
thickness: z.number().optional().describe('Ring thickness 0–1'),
|
|
140
|
+
donutRadius: z.number().optional().describe('Donut tube (minor) radius'),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const RectangleEmitterSchema = z.object({
|
|
144
|
+
type: z.literal('rectangle'),
|
|
145
|
+
width: z.number().optional().describe('Rectangle width'),
|
|
146
|
+
height: z.number().optional().describe('Rectangle height'),
|
|
147
|
+
thickness: z.number().optional().describe('Extrusion depth 0–1'),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const GridEmitterSchema = z.object({
|
|
151
|
+
type: z.literal('grid'),
|
|
152
|
+
width: z.number().optional().describe('Grid total width'),
|
|
153
|
+
height: z.number().optional().describe('Grid total height'),
|
|
154
|
+
column: z.number().int().optional().describe('Number of columns'),
|
|
155
|
+
row: z.number().int().optional().describe('Number of rows'),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const EmitterShapeSchema = z
|
|
159
|
+
.discriminatedUnion('type', [
|
|
160
|
+
PointEmitterSchema,
|
|
161
|
+
SphereEmitterSchema,
|
|
162
|
+
HemisphereEmitterSchema,
|
|
163
|
+
ConeEmitterSchema,
|
|
164
|
+
CircleEmitterSchema,
|
|
165
|
+
DonutEmitterSchema,
|
|
166
|
+
RectangleEmitterSchema,
|
|
167
|
+
GridEmitterSchema,
|
|
168
|
+
])
|
|
169
|
+
.describe('Particle emitter shape');
|
|
170
|
+
|
|
171
|
+
export type EmitterShapeJSON = z.infer<typeof EmitterShapeSchema>;
|
|
172
|
+
|
|
173
|
+
// --- Behaviors (mirrors quarks.core behavior toJSON) ---
|
|
174
|
+
|
|
175
|
+
const ApplyForceBehaviorSchema = z.object({
|
|
176
|
+
type: z.literal('ApplyForce'),
|
|
177
|
+
direction: Vec3Schema.describe('Force direction vector'),
|
|
178
|
+
magnitude: ValueGeneratorSchema.describe('Force magnitude'),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const ColorOverLifeBehaviorSchema = z.object({
|
|
182
|
+
type: z.literal('ColorOverLife'),
|
|
183
|
+
color: ColorGeneratorSchema.describe('Color curve over particle lifetime'),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const SizeOverLifeBehaviorSchema = z.object({
|
|
187
|
+
type: z.literal('SizeOverLife'),
|
|
188
|
+
size: ValueGeneratorSchema.describe('Size curve over particle lifetime'),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const SpeedOverLifeBehaviorSchema = z.object({
|
|
192
|
+
type: z.literal('SpeedOverLife'),
|
|
193
|
+
speed: ValueGeneratorSchema.describe('Speed multiplier curve over lifetime'),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const RotationOverLifeBehaviorSchema = z.object({
|
|
197
|
+
type: z.literal('RotationOverLife'),
|
|
198
|
+
angularVelocity: ValueGeneratorSchema.describe('Angular velocity in radians/sec'),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const ForceOverLifeBehaviorSchema = z.object({
|
|
202
|
+
type: z.literal('ForceOverLife'),
|
|
203
|
+
x: ValueGeneratorSchema.describe('Force along X axis over lifetime'),
|
|
204
|
+
y: ValueGeneratorSchema.describe('Force along Y axis over lifetime'),
|
|
205
|
+
z: ValueGeneratorSchema.describe('Force along Z axis over lifetime'),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const OrbitOverLifeBehaviorSchema = z.object({
|
|
209
|
+
type: z.literal('OrbitOverLife'),
|
|
210
|
+
orbitSpeed: ValueGeneratorSchema.describe('Orbit speed over lifetime'),
|
|
211
|
+
axis: Vec3Schema.optional().describe('Orbit axis. Defaults to [0, 1, 0]'),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const NoiseBehaviorSchema = z.object({
|
|
215
|
+
type: z.literal('Noise'),
|
|
216
|
+
frequency: ValueGeneratorSchema.describe('Noise frequency'),
|
|
217
|
+
power: ValueGeneratorSchema.describe('Noise amplitude'),
|
|
218
|
+
positionAmount: ValueGeneratorSchema.optional().describe('Position displacement amount'),
|
|
219
|
+
rotationAmount: ValueGeneratorSchema.optional().describe('Rotation displacement amount'),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const TurbulenceFieldBehaviorSchema = z.object({
|
|
223
|
+
type: z.literal('TurbulenceField'),
|
|
224
|
+
scale: Vec3Schema.describe('Turbulence field scale'),
|
|
225
|
+
octaves: z.number().int().describe('Noise octave count'),
|
|
226
|
+
velocityMultiplier: Vec3Schema.describe('Velocity multiplier per axis'),
|
|
227
|
+
timeScale: Vec3Schema.describe('Time scale per axis'),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const FrameOverLifeBehaviorSchema = z.object({
|
|
231
|
+
type: z.literal('FrameOverLife'),
|
|
232
|
+
frame: ValueGeneratorSchema.describe('Sprite sheet frame index over lifetime'),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const LimitSpeedOverLifeBehaviorSchema = z.object({
|
|
236
|
+
type: z.literal('LimitSpeedOverLife'),
|
|
237
|
+
speed: ValueGeneratorSchema.describe('Maximum speed over lifetime'),
|
|
238
|
+
dampen: z.number().describe('Damping factor when speed exceeds limit (0–1)'),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const ChangeEmitDirectionBehaviorSchema = z.object({
|
|
242
|
+
type: z.literal('ChangeEmitDirection'),
|
|
243
|
+
angle: ValueGeneratorSchema.describe('Angle deviation from original emit direction'),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const GravityForceBehaviorSchema = z.object({
|
|
247
|
+
type: z.literal('GravityForce'),
|
|
248
|
+
center: Vec3Schema.describe('Gravity center point'),
|
|
249
|
+
magnitude: z.number().describe('Gravity force strength'),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const WidthOverLengthBehaviorSchema = z.object({
|
|
253
|
+
type: z.literal('WidthOverLength'),
|
|
254
|
+
width: ValueGeneratorSchema.describe('Trail width over trail length'),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export const BehaviorSchema = z
|
|
258
|
+
.discriminatedUnion('type', [
|
|
259
|
+
ApplyForceBehaviorSchema,
|
|
260
|
+
ColorOverLifeBehaviorSchema,
|
|
261
|
+
SizeOverLifeBehaviorSchema,
|
|
262
|
+
SpeedOverLifeBehaviorSchema,
|
|
263
|
+
RotationOverLifeBehaviorSchema,
|
|
264
|
+
ForceOverLifeBehaviorSchema,
|
|
265
|
+
OrbitOverLifeBehaviorSchema,
|
|
266
|
+
NoiseBehaviorSchema,
|
|
267
|
+
TurbulenceFieldBehaviorSchema,
|
|
268
|
+
FrameOverLifeBehaviorSchema,
|
|
269
|
+
LimitSpeedOverLifeBehaviorSchema,
|
|
270
|
+
ChangeEmitDirectionBehaviorSchema,
|
|
271
|
+
GravityForceBehaviorSchema,
|
|
272
|
+
WidthOverLengthBehaviorSchema,
|
|
273
|
+
])
|
|
274
|
+
.describe('Particle behavior applied over lifetime');
|
|
275
|
+
|
|
276
|
+
export type BehaviorJSON = z.infer<typeof BehaviorSchema>;
|
|
277
|
+
|
|
278
|
+
// --- Render Mode ---
|
|
279
|
+
|
|
280
|
+
export const RenderModeSchema = z
|
|
281
|
+
.enum([
|
|
282
|
+
'billboard',
|
|
283
|
+
'stretchedBillboard',
|
|
284
|
+
'mesh',
|
|
285
|
+
'trail',
|
|
286
|
+
'horizontalBillboard',
|
|
287
|
+
'verticalBillboard',
|
|
288
|
+
])
|
|
289
|
+
.describe('How particles are rendered');
|
|
290
|
+
|
|
291
|
+
// --- Particle Material (our format — asset paths, not Three.js UUIDs) ---
|
|
292
|
+
|
|
293
|
+
const ParticleMaterialSchema = z
|
|
294
|
+
.object({
|
|
295
|
+
type: z.enum(['basic', 'standard']).optional().describe('Particle material shading model'),
|
|
296
|
+
color: z.string().optional().describe('Particle color as CSS hex string'),
|
|
297
|
+
map: z.string().optional().describe('Path to particle sprite texture'),
|
|
298
|
+
blending: z
|
|
299
|
+
.enum(['normal', 'additive'])
|
|
300
|
+
.optional()
|
|
301
|
+
.describe('Blending mode (additive for glowing/fire effects)'),
|
|
302
|
+
transparent: z.boolean().optional().describe('Enable transparency'),
|
|
303
|
+
depthWrite: z
|
|
304
|
+
.boolean()
|
|
305
|
+
.optional()
|
|
306
|
+
.describe('Write to depth buffer (disable for additive effects)'),
|
|
307
|
+
side: z.enum(['front', 'back', 'double']).optional().describe('Which face sides to render'),
|
|
308
|
+
})
|
|
309
|
+
.describe('Particle material settings');
|
|
310
|
+
|
|
311
|
+
// --- Burst ---
|
|
312
|
+
|
|
313
|
+
const BurstSchema = z
|
|
314
|
+
.object({
|
|
315
|
+
time: z.number().describe('Time offset in seconds within the emission cycle'),
|
|
316
|
+
count: ValueGeneratorSchema.describe('Number of particles to emit'),
|
|
317
|
+
cycle: z.number().optional().describe('Burst repeat interval in seconds'),
|
|
318
|
+
interval: z.number().optional().describe('Delay between burst repetitions'),
|
|
319
|
+
probability: z.number().optional().describe('Chance of burst firing (0–1)'),
|
|
320
|
+
})
|
|
321
|
+
.describe('Timed particle burst');
|
|
322
|
+
|
|
323
|
+
// --- Top-level SceneParticles ---
|
|
324
|
+
|
|
325
|
+
export const SceneParticlesSchema = z
|
|
326
|
+
.object({
|
|
327
|
+
// Lifecycle
|
|
328
|
+
autoDestroy: z
|
|
329
|
+
.boolean()
|
|
330
|
+
.optional()
|
|
331
|
+
.describe('Destroy the particle system when all particles have died'),
|
|
332
|
+
looping: z.boolean().optional().describe('Continuously emit particles'),
|
|
333
|
+
prewarm: z
|
|
334
|
+
.boolean()
|
|
335
|
+
.optional()
|
|
336
|
+
.describe('Simulate one full cycle on start so particles appear immediately'),
|
|
337
|
+
duration: z.number().optional().describe('Total emission duration in seconds'),
|
|
338
|
+
|
|
339
|
+
// Shape
|
|
340
|
+
shape: EmitterShapeSchema.optional().describe(
|
|
341
|
+
'Emitter shape that defines where particles spawn',
|
|
342
|
+
),
|
|
343
|
+
|
|
344
|
+
// Per-particle initial values
|
|
345
|
+
startLife: ValueGeneratorSchema.optional().describe('Initial particle lifetime in seconds'),
|
|
346
|
+
startSpeed: ValueGeneratorSchema.optional().describe('Initial particle speed'),
|
|
347
|
+
startSize: ValueGeneratorSchema.optional().describe('Initial particle size'),
|
|
348
|
+
startRotation: ValueGeneratorSchema.optional().describe('Initial particle rotation in radians'),
|
|
349
|
+
startColor: ColorGeneratorSchema.optional().describe('Initial particle color'),
|
|
350
|
+
startLength: ValueGeneratorSchema.optional().describe(
|
|
351
|
+
'Initial trail length (trail render mode only)',
|
|
352
|
+
),
|
|
353
|
+
startTileIndex: ValueGeneratorSchema.optional().describe('Initial sprite sheet tile index'),
|
|
354
|
+
|
|
355
|
+
// Emission
|
|
356
|
+
emissionOverTime: ValueGeneratorSchema.optional().describe('Particles emitted per second'),
|
|
357
|
+
emissionOverDistance: ValueGeneratorSchema.optional().describe(
|
|
358
|
+
'Particles emitted per unit of emitter movement',
|
|
359
|
+
),
|
|
360
|
+
emissionBursts: z.array(BurstSchema).optional().describe('Timed burst emissions'),
|
|
361
|
+
|
|
362
|
+
// Behaviors
|
|
363
|
+
behaviors: z
|
|
364
|
+
.array(BehaviorSchema)
|
|
365
|
+
.optional()
|
|
366
|
+
.describe('Behaviors applied to particles over their lifetime'),
|
|
367
|
+
|
|
368
|
+
// Rendering
|
|
369
|
+
renderMode: RenderModeSchema.optional().describe(
|
|
370
|
+
'How particles are rendered (billboard, trail, mesh, etc.)',
|
|
371
|
+
),
|
|
372
|
+
worldSpace: z
|
|
373
|
+
.boolean()
|
|
374
|
+
.optional()
|
|
375
|
+
.describe('Simulate particles in world space instead of local space'),
|
|
376
|
+
renderOrder: z
|
|
377
|
+
.number()
|
|
378
|
+
.optional()
|
|
379
|
+
.describe('Render order for sorting (higher = rendered later)'),
|
|
380
|
+
uTileCount: z.number().int().optional().describe('Horizontal tile count in sprite sheet'),
|
|
381
|
+
vTileCount: z.number().int().optional().describe('Vertical tile count in sprite sheet'),
|
|
382
|
+
blendTiles: z
|
|
383
|
+
.boolean()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe('Blend between sprite sheet tiles for smooth animation'),
|
|
386
|
+
softParticles: z.boolean().optional().describe('Enable soft particle depth fading'),
|
|
387
|
+
softFarFade: z.number().optional().describe('Far fade distance for soft particles'),
|
|
388
|
+
softNearFade: z.number().optional().describe('Near fade distance for soft particles'),
|
|
389
|
+
|
|
390
|
+
// Material
|
|
391
|
+
material: ParticleMaterialSchema.describe('Particle material settings'),
|
|
392
|
+
})
|
|
393
|
+
.describe('Particle system configuration');
|
|
394
|
+
|
|
395
|
+
/** Partial schema for prefab instance overrides. */
|
|
396
|
+
export const SceneParticlesOverrideSchema = SceneParticlesSchema.partial();
|
|
397
|
+
|
|
398
|
+
export type SceneParticles = z.infer<typeof SceneParticlesSchema>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SceneColliderOverrideSchema, SceneColliderSchema } from './collider';
|
|
3
|
+
|
|
4
|
+
export const ScenePhysicsSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
bodyType: z
|
|
7
|
+
.enum(['dynamic', 'fixed', 'kinematic'])
|
|
8
|
+
.describe(
|
|
9
|
+
'Physics body type: dynamic (simulated), fixed (immovable), kinematic (script-driven). ' +
|
|
10
|
+
'T1.1/R1c: ANY physics body (dynamic, fixed, OR kinematic) nested under an ancestor ' +
|
|
11
|
+
'entity that itself has a "dynamic" or "kinematic" body is unsupported (loud load ' +
|
|
12
|
+
'error) — Rapier has no parent/child body relationship, so a moving ancestor cannot ' +
|
|
13
|
+
"carry a nested body of ANY type with it; the child's Object3D would stay pinned to " +
|
|
14
|
+
"its own (non-moving) body's pose while the ancestor moves out from under it. " +
|
|
15
|
+
'Nesting under a "fixed"/static or physics-less transformed ancestor is fine.',
|
|
16
|
+
),
|
|
17
|
+
mass: z
|
|
18
|
+
.number()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Explicit mass override. If omitted, computed from collider density and volume'),
|
|
21
|
+
collider: SceneColliderSchema.describe(
|
|
22
|
+
'Collider shape attached to this body. T1.1/T1.2: the Rapier body is created at this ' +
|
|
23
|
+
"entity's COMPOSED WORLD transform (parent chain × own local transform) — Rapier has " +
|
|
24
|
+
"no concept of a parent Object3D, so nested entities' transforms are baked in at spawn " +
|
|
25
|
+
'time, not read live thereafter.',
|
|
26
|
+
),
|
|
27
|
+
})
|
|
28
|
+
.describe('Physics rigid body configuration');
|
|
29
|
+
|
|
30
|
+
/** Partial override — uses SceneColliderOverrideSchema for deep partial on collider. */
|
|
31
|
+
export const ScenePhysicsOverrideSchema = z.object({
|
|
32
|
+
bodyType: z
|
|
33
|
+
.enum(['dynamic', 'fixed', 'kinematic'])
|
|
34
|
+
.optional()
|
|
35
|
+
.describe(
|
|
36
|
+
'Physics body type: dynamic (simulated), fixed (immovable), kinematic (script-driven). ' +
|
|
37
|
+
'A body of ANY type nested under a "dynamic"/"kinematic"-bodied ancestor throws a loud ' +
|
|
38
|
+
'load error (T1.1/R1c) — see ScenePhysicsSchema.bodyType.',
|
|
39
|
+
),
|
|
40
|
+
mass: z
|
|
41
|
+
.number()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Explicit mass override. If omitted, computed from collider density and volume'),
|
|
44
|
+
collider: SceneColliderOverrideSchema.optional().describe(
|
|
45
|
+
'Partial collider overrides merged with prefab',
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type ScenePhysics = z.infer<typeof ScenePhysicsSchema>;
|