mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-09-14 13:41:32 +08:00
f4d13b6612
* game engine * fixes * fixes * fix controls * og * fixes * fixes
485 lines
13 KiB
TypeScript
485 lines
13 KiB
TypeScript
import { z } from "zod";
|
|
import { defineCatalog } from "@json-render/core";
|
|
import { schema } from "@json-render/react/schema";
|
|
import { threeComponentDefinitions } from "@json-render/react-three-fiber/catalog";
|
|
|
|
const vector3Schema = z.tuple([z.number(), z.number(), z.number()]);
|
|
|
|
const materialSchema = z.object({
|
|
color: z.string().nullable(),
|
|
metalness: z.number().nullable(),
|
|
roughness: z.number().nullable(),
|
|
emissive: z.string().nullable(),
|
|
emissiveIntensity: z.number().nullable(),
|
|
opacity: z.number().nullable(),
|
|
transparent: z.boolean().nullable(),
|
|
wireframe: z.boolean().nullable(),
|
|
});
|
|
|
|
const transformProps = {
|
|
position: vector3Schema.nullable(),
|
|
rotation: vector3Schema.nullable(),
|
|
scale: vector3Schema.nullable(),
|
|
} as const;
|
|
|
|
const shadowProps = {
|
|
castShadow: z.boolean().nullable(),
|
|
receiveShadow: z.boolean().nullable(),
|
|
} as const;
|
|
|
|
const physicsSchema = z.object({
|
|
mass: z.number().nullable(),
|
|
isStatic: z.boolean().nullable(),
|
|
restitution: z.number().nullable(),
|
|
friction: z.number().nullable(),
|
|
colliderType: z.enum(["cuboid", "ball", "capsule", "none"]).nullable(),
|
|
});
|
|
|
|
const damageSchema = z.object({
|
|
amount: z.number().nullable(),
|
|
enabled: z.boolean().nullable(),
|
|
});
|
|
|
|
const gameTransformShadowPhysicsDamage = {
|
|
...transformProps,
|
|
...shadowProps,
|
|
material: materialSchema.nullable(),
|
|
physics: physicsSchema.nullable(),
|
|
damage: damageSchema.nullable(),
|
|
objectId: z.string().nullable(),
|
|
};
|
|
|
|
export const gameComponentDefinitions = {
|
|
// Keep all non-primitive R3F components as-is
|
|
AmbientLight: threeComponentDefinitions.AmbientLight,
|
|
DirectionalLight: threeComponentDefinitions.DirectionalLight,
|
|
PointLight: threeComponentDefinitions.PointLight,
|
|
SpotLight: threeComponentDefinitions.SpotLight,
|
|
Group: threeComponentDefinitions.Group,
|
|
Model: threeComponentDefinitions.Model,
|
|
Environment: threeComponentDefinitions.Environment,
|
|
Fog: threeComponentDefinitions.Fog,
|
|
GridHelper: threeComponentDefinitions.GridHelper,
|
|
Text3D: threeComponentDefinitions.Text3D,
|
|
Sparkles: threeComponentDefinitions.Sparkles,
|
|
Stars: threeComponentDefinitions.Stars,
|
|
Sky: threeComponentDefinitions.Sky,
|
|
Cloud: threeComponentDefinitions.Cloud,
|
|
ContactShadows: threeComponentDefinitions.ContactShadows,
|
|
Float: threeComponentDefinitions.Float,
|
|
EffectComposer: threeComponentDefinitions.EffectComposer,
|
|
Bloom: threeComponentDefinitions.Bloom,
|
|
Vignette: threeComponentDefinitions.Vignette,
|
|
PerspectiveCamera: threeComponentDefinitions.PerspectiveCamera,
|
|
OrbitControls: threeComponentDefinitions.OrbitControls,
|
|
|
|
// Game-specific primitives with physics & damage
|
|
GameBox: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
width: z.number().nullable(),
|
|
height: z.number().nullable(),
|
|
depth: z.number().nullable(),
|
|
}),
|
|
description:
|
|
"Box with optional physics and damage. ALWAYS set width, height, depth to create varied shapes — wide flat platforms (4, 0.2, 4), tall walls (0.3, 3, 5), long beams (6, 0.3, 0.3). Avoid 1x1x1 cubes.",
|
|
example: {
|
|
position: [0, 1.5, 0],
|
|
width: 2,
|
|
height: 3,
|
|
depth: 0.3,
|
|
material: { color: "#4488ff" },
|
|
physics: { mass: 1, isStatic: true, colliderType: "cuboid" },
|
|
},
|
|
},
|
|
|
|
GameSphere: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
widthSegments: z.number().nullable(),
|
|
heightSegments: z.number().nullable(),
|
|
}),
|
|
description:
|
|
"Sphere with optional physics and damage. Set radius explicitly (0.2 for small details, 1-3 for tree canopies, 0.5 for default).",
|
|
example: {
|
|
position: [0, 1.5, 0],
|
|
radius: 1.5,
|
|
material: { color: "#228B22" },
|
|
castShadow: true,
|
|
},
|
|
},
|
|
|
|
GameCylinder: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radiusTop: z.number().nullable(),
|
|
radiusBottom: z.number().nullable(),
|
|
height: z.number().nullable(),
|
|
radialSegments: z.number().nullable(),
|
|
}),
|
|
description:
|
|
"Cylinder with optional physics and damage. Set radiusTop/radiusBottom to control thickness and height for tall/thin shapes (e.g. tree trunk: radiusTop 0.15, radiusBottom 0.2, height 4).",
|
|
example: {
|
|
position: [0, 2, 0],
|
|
radiusTop: 0.15,
|
|
radiusBottom: 0.2,
|
|
height: 4,
|
|
material: { color: "#8B4513" },
|
|
castShadow: true,
|
|
},
|
|
},
|
|
|
|
GameCone: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
height: z.number().nullable(),
|
|
radialSegments: z.number().nullable(),
|
|
}),
|
|
description:
|
|
"Cone with optional physics and damage. Set radius and height for varied shapes (e.g. roof: radius 2.5, height 1.5).",
|
|
example: {
|
|
position: [0, 3.5, 0],
|
|
radius: 2.5,
|
|
height: 1.5,
|
|
material: { color: "#8B0000" },
|
|
castShadow: true,
|
|
},
|
|
},
|
|
|
|
GameTorus: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
tube: z.number().nullable(),
|
|
radialSegments: z.number().nullable(),
|
|
tubularSegments: z.number().nullable(),
|
|
}),
|
|
description:
|
|
"Torus (ring) with optional physics and damage. Set radius and tube to control ring size and thickness.",
|
|
example: {
|
|
position: [0, 1, 0],
|
|
radius: 1,
|
|
tube: 0.15,
|
|
material: { color: "#ff44ff" },
|
|
},
|
|
},
|
|
|
|
GamePlane: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
width: z.number().nullable(),
|
|
height: z.number().nullable(),
|
|
}),
|
|
description:
|
|
"Plane with optional physics. Use for floors and walls. Rotate [-PI/2, 0, 0] for ground.",
|
|
example: {
|
|
position: [0, 0, 0],
|
|
rotation: [-1.5708, 0, 0],
|
|
scale: [10, 10, 1],
|
|
material: { color: "#888888" },
|
|
physics: { isStatic: true, colliderType: "cuboid" },
|
|
},
|
|
},
|
|
|
|
GameCapsule: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
length: z.number().nullable(),
|
|
capSegments: z.number().nullable(),
|
|
radialSegments: z.number().nullable(),
|
|
}),
|
|
description: "Capsule with optional physics and damage.",
|
|
example: {
|
|
position: [0, 1, 0],
|
|
material: { color: "#00cccc" },
|
|
physics: { mass: 1, colliderType: "capsule" },
|
|
},
|
|
},
|
|
|
|
GameKnot: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
tube: z.number().nullable(),
|
|
tubularSegments: z.number().nullable(),
|
|
radialSegments: z.number().nullable(),
|
|
p: z.number().nullable(),
|
|
q: z.number().nullable(),
|
|
}),
|
|
description: "Torus knot with optional physics and damage.",
|
|
example: {
|
|
position: [0, 1, 0],
|
|
radius: 1,
|
|
tube: 0.3,
|
|
material: { color: "#ff44ff" },
|
|
},
|
|
},
|
|
|
|
GameTetrahedron: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
}),
|
|
description: "Tetrahedron with optional physics and damage.",
|
|
},
|
|
|
|
GameOctahedron: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
}),
|
|
description: "Octahedron with optional physics and damage.",
|
|
},
|
|
|
|
GameDodecahedron: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
}),
|
|
description: "Dodecahedron with optional physics and damage.",
|
|
},
|
|
|
|
GameIcosahedron: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
}),
|
|
description: "Icosahedron with optional physics and damage.",
|
|
},
|
|
|
|
GameExtrude: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
shapeData: z
|
|
.object({
|
|
points: z.array(z.tuple([z.number(), z.number()])),
|
|
holes: z.array(z.array(z.tuple([z.number(), z.number()]))).nullable(),
|
|
})
|
|
.nullable(),
|
|
depth: z.number().nullable(),
|
|
}),
|
|
description: "Extruded 2D shape into 3D with optional physics and damage.",
|
|
example: {
|
|
position: [0, 0.5, 0],
|
|
shapeData: {
|
|
points: [
|
|
[-0.5, -0.5],
|
|
[0.5, -0.5],
|
|
[0.5, 0.5],
|
|
[-0.5, 0.5],
|
|
],
|
|
},
|
|
depth: 1,
|
|
material: { color: "#ff8800" },
|
|
},
|
|
},
|
|
|
|
GameTube: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
radius: z.number().nullable(),
|
|
tubularSegments: z.number().nullable(),
|
|
radialSegments: z.number().nullable(),
|
|
}),
|
|
description: "Tube geometry following a curve with optional physics.",
|
|
example: {
|
|
position: [0, 0.5, 0],
|
|
radius: 0.1,
|
|
material: { color: "#00ccff" },
|
|
},
|
|
},
|
|
|
|
GameShape: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
shapeData: z
|
|
.object({
|
|
points: z.array(z.tuple([z.number(), z.number()])),
|
|
holes: z.array(z.array(z.tuple([z.number(), z.number()]))).nullable(),
|
|
})
|
|
.nullable(),
|
|
}),
|
|
description:
|
|
"2D shape geometry (flat polygon) with optional physics and damage.",
|
|
example: {
|
|
position: [0, 0.5, 0],
|
|
shapeData: {
|
|
points: [
|
|
[-0.5, -0.5],
|
|
[0.5, -0.5],
|
|
[0, 0.5],
|
|
],
|
|
},
|
|
material: { color: "#ff00ff" },
|
|
},
|
|
},
|
|
|
|
GameMesh: {
|
|
props: z.object({
|
|
...gameTransformShadowPhysicsDamage,
|
|
meshData: z
|
|
.object({
|
|
vertices: z.array(z.number()),
|
|
indices: z.array(z.number()),
|
|
normals: z.array(z.number()).nullable(),
|
|
uvs: z.array(z.number()).nullable(),
|
|
})
|
|
.nullable(),
|
|
}),
|
|
description:
|
|
"Custom mesh with raw vertex data. Specify vertices, indices, normals, and UVs.",
|
|
example: {
|
|
position: [0, 0.5, 0],
|
|
meshData: {
|
|
vertices: [-0.5, -0.5, 0, 0.5, -0.5, 0, 0, 0.5, 0],
|
|
indices: [0, 1, 2],
|
|
},
|
|
material: { color: "#88ff00" },
|
|
},
|
|
},
|
|
|
|
// Unified light component
|
|
GameLight: {
|
|
props: z.object({
|
|
...transformProps,
|
|
lightType: z.enum(["ambient", "directional", "point", "spot"]),
|
|
color: z.string().nullable(),
|
|
intensity: z.number().nullable(),
|
|
distance: z.number().nullable(),
|
|
decay: z.number().nullable(),
|
|
angle: z.number().nullable(),
|
|
penumbra: z.number().nullable(),
|
|
castShadow: z.boolean().nullable(),
|
|
objectId: z.string().nullable(),
|
|
}),
|
|
description:
|
|
"Unified light component. Set lightType to ambient, directional, point, or spot.",
|
|
example: {
|
|
lightType: "directional",
|
|
position: [5, 10, 5],
|
|
intensity: 1,
|
|
castShadow: true,
|
|
},
|
|
},
|
|
|
|
// Player controller
|
|
Player: {
|
|
props: z.object({
|
|
...transformProps,
|
|
objectId: z.string().nullable(),
|
|
isPlayer: z.boolean().nullable(),
|
|
}),
|
|
description:
|
|
"Player spawn point. Enables first/third-person controls when playing.",
|
|
example: {
|
|
position: [0, 0, 0],
|
|
},
|
|
},
|
|
|
|
// Character NPC
|
|
GameCharacter: {
|
|
props: z.object({
|
|
...transformProps,
|
|
...shadowProps,
|
|
modelUrl: z.string(),
|
|
role: z.string().nullable(),
|
|
physics: physicsSchema.nullable(),
|
|
objectId: z.string().nullable(),
|
|
}),
|
|
description:
|
|
"NPC character with GLTF model, proximity interaction, and AI dialogue.",
|
|
example: {
|
|
position: [3, 0, 0],
|
|
modelUrl: "/models/character.glb",
|
|
role: "village guard",
|
|
},
|
|
},
|
|
|
|
// Game model with physics
|
|
GameModel: {
|
|
props: z.object({
|
|
...transformProps,
|
|
...shadowProps,
|
|
url: z.string(),
|
|
physics: physicsSchema.nullable(),
|
|
damage: damageSchema.nullable(),
|
|
objectId: z.string().nullable(),
|
|
}),
|
|
description: "GLTF/GLB 3D model with optional physics and damage.",
|
|
example: {
|
|
url: "/models/crate.glb",
|
|
position: [0, 0, 0],
|
|
physics: { isStatic: true, colliderType: "cuboid" },
|
|
},
|
|
},
|
|
|
|
// Sound emitter
|
|
SoundEmitter: {
|
|
props: z.object({
|
|
...transformProps,
|
|
url: z.string(),
|
|
loop: z.boolean().nullable(),
|
|
volume: z.number().nullable(),
|
|
positional: z.boolean().nullable(),
|
|
distance: z.number().nullable(),
|
|
objectId: z.string().nullable(),
|
|
}),
|
|
description: "Positional or global audio emitter.",
|
|
example: {
|
|
position: [0, 1, 0],
|
|
url: "/sounds/ambient.mp3",
|
|
loop: true,
|
|
positional: true,
|
|
},
|
|
},
|
|
|
|
// Media plane
|
|
MediaPlane: {
|
|
props: z.object({
|
|
...transformProps,
|
|
...shadowProps,
|
|
url: z.string(),
|
|
mediaType: z.enum(["image", "video"]),
|
|
loop: z.boolean().nullable(),
|
|
autoplay: z.boolean().nullable(),
|
|
muted: z.boolean().nullable(),
|
|
width: z.number().nullable(),
|
|
height: z.number().nullable(),
|
|
objectId: z.string().nullable(),
|
|
}),
|
|
description: "Image or video displayed on a 3D plane.",
|
|
example: {
|
|
position: [0, 2, -3],
|
|
url: "/images/poster.jpg",
|
|
mediaType: "image",
|
|
width: 3,
|
|
height: 2,
|
|
},
|
|
},
|
|
|
|
// Ground plane (physics-enabled floor)
|
|
GroundPlane: {
|
|
props: z.object({
|
|
...transformProps,
|
|
material: materialSchema.nullable(),
|
|
size: z.number().nullable(),
|
|
}),
|
|
description: "Physics-enabled ground plane.",
|
|
example: {
|
|
position: [0, -0.1, 0],
|
|
size: 5000,
|
|
material: { color: "#4CAF50" },
|
|
},
|
|
},
|
|
};
|
|
|
|
export type GameComponentDefinitions = typeof gameComponentDefinitions;
|
|
|
|
export const catalog = defineCatalog(schema, {
|
|
components: gameComponentDefinitions,
|
|
actions: {},
|
|
});
|