catalog.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import { z } from "zod";
  2. import { defineCatalog } from "@json-render/core";
  3. import { schema } from "@json-render/react/schema";
  4. import { threeComponentDefinitions } from "@json-render/react-three-fiber/catalog";
  5. const vector3Schema = z.tuple([z.number(), z.number(), z.number()]);
  6. const materialSchema = z.object({
  7. color: z.string().nullable(),
  8. metalness: z.number().nullable(),
  9. roughness: z.number().nullable(),
  10. emissive: z.string().nullable(),
  11. emissiveIntensity: z.number().nullable(),
  12. opacity: z.number().nullable(),
  13. transparent: z.boolean().nullable(),
  14. wireframe: z.boolean().nullable(),
  15. });
  16. const transformProps = {
  17. position: vector3Schema.nullable(),
  18. rotation: vector3Schema.nullable(),
  19. scale: vector3Schema.nullable(),
  20. } as const;
  21. const shadowProps = {
  22. castShadow: z.boolean().nullable(),
  23. receiveShadow: z.boolean().nullable(),
  24. } as const;
  25. const physicsSchema = z.object({
  26. mass: z.number().nullable(),
  27. isStatic: z.boolean().nullable(),
  28. restitution: z.number().nullable(),
  29. friction: z.number().nullable(),
  30. colliderType: z.enum(["cuboid", "ball", "capsule", "none"]).nullable(),
  31. });
  32. const damageSchema = z.object({
  33. amount: z.number().nullable(),
  34. enabled: z.boolean().nullable(),
  35. });
  36. const gameTransformShadowPhysicsDamage = {
  37. ...transformProps,
  38. ...shadowProps,
  39. material: materialSchema.nullable(),
  40. physics: physicsSchema.nullable(),
  41. damage: damageSchema.nullable(),
  42. objectId: z.string().nullable(),
  43. };
  44. export const gameComponentDefinitions = {
  45. // Keep all non-primitive R3F components as-is
  46. AmbientLight: threeComponentDefinitions.AmbientLight,
  47. DirectionalLight: threeComponentDefinitions.DirectionalLight,
  48. PointLight: threeComponentDefinitions.PointLight,
  49. SpotLight: threeComponentDefinitions.SpotLight,
  50. Group: threeComponentDefinitions.Group,
  51. Model: threeComponentDefinitions.Model,
  52. Environment: threeComponentDefinitions.Environment,
  53. Fog: threeComponentDefinitions.Fog,
  54. GridHelper: threeComponentDefinitions.GridHelper,
  55. Text3D: threeComponentDefinitions.Text3D,
  56. Sparkles: threeComponentDefinitions.Sparkles,
  57. Stars: threeComponentDefinitions.Stars,
  58. Sky: threeComponentDefinitions.Sky,
  59. Cloud: threeComponentDefinitions.Cloud,
  60. ContactShadows: threeComponentDefinitions.ContactShadows,
  61. Float: threeComponentDefinitions.Float,
  62. EffectComposer: threeComponentDefinitions.EffectComposer,
  63. Bloom: threeComponentDefinitions.Bloom,
  64. Vignette: threeComponentDefinitions.Vignette,
  65. PerspectiveCamera: threeComponentDefinitions.PerspectiveCamera,
  66. OrbitControls: threeComponentDefinitions.OrbitControls,
  67. // Game-specific primitives with physics & damage
  68. GameBox: {
  69. props: z.object({
  70. ...gameTransformShadowPhysicsDamage,
  71. width: z.number().nullable(),
  72. height: z.number().nullable(),
  73. depth: z.number().nullable(),
  74. }),
  75. description:
  76. "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.",
  77. example: {
  78. position: [0, 1.5, 0],
  79. width: 2,
  80. height: 3,
  81. depth: 0.3,
  82. material: { color: "#4488ff" },
  83. physics: { mass: 1, isStatic: true, colliderType: "cuboid" },
  84. },
  85. },
  86. GameSphere: {
  87. props: z.object({
  88. ...gameTransformShadowPhysicsDamage,
  89. radius: z.number().nullable(),
  90. widthSegments: z.number().nullable(),
  91. heightSegments: z.number().nullable(),
  92. }),
  93. description:
  94. "Sphere with optional physics and damage. Set radius explicitly (0.2 for small details, 1-3 for tree canopies, 0.5 for default).",
  95. example: {
  96. position: [0, 1.5, 0],
  97. radius: 1.5,
  98. material: { color: "#228B22" },
  99. castShadow: true,
  100. },
  101. },
  102. GameCylinder: {
  103. props: z.object({
  104. ...gameTransformShadowPhysicsDamage,
  105. radiusTop: z.number().nullable(),
  106. radiusBottom: z.number().nullable(),
  107. height: z.number().nullable(),
  108. radialSegments: z.number().nullable(),
  109. }),
  110. description:
  111. "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).",
  112. example: {
  113. position: [0, 2, 0],
  114. radiusTop: 0.15,
  115. radiusBottom: 0.2,
  116. height: 4,
  117. material: { color: "#8B4513" },
  118. castShadow: true,
  119. },
  120. },
  121. GameCone: {
  122. props: z.object({
  123. ...gameTransformShadowPhysicsDamage,
  124. radius: z.number().nullable(),
  125. height: z.number().nullable(),
  126. radialSegments: z.number().nullable(),
  127. }),
  128. description:
  129. "Cone with optional physics and damage. Set radius and height for varied shapes (e.g. roof: radius 2.5, height 1.5).",
  130. example: {
  131. position: [0, 3.5, 0],
  132. radius: 2.5,
  133. height: 1.5,
  134. material: { color: "#8B0000" },
  135. castShadow: true,
  136. },
  137. },
  138. GameTorus: {
  139. props: z.object({
  140. ...gameTransformShadowPhysicsDamage,
  141. radius: z.number().nullable(),
  142. tube: z.number().nullable(),
  143. radialSegments: z.number().nullable(),
  144. tubularSegments: z.number().nullable(),
  145. }),
  146. description:
  147. "Torus (ring) with optional physics and damage. Set radius and tube to control ring size and thickness.",
  148. example: {
  149. position: [0, 1, 0],
  150. radius: 1,
  151. tube: 0.15,
  152. material: { color: "#ff44ff" },
  153. },
  154. },
  155. GamePlane: {
  156. props: z.object({
  157. ...gameTransformShadowPhysicsDamage,
  158. width: z.number().nullable(),
  159. height: z.number().nullable(),
  160. }),
  161. description:
  162. "Plane with optional physics. Use for floors and walls. Rotate [-PI/2, 0, 0] for ground.",
  163. example: {
  164. position: [0, 0, 0],
  165. rotation: [-1.5708, 0, 0],
  166. scale: [10, 10, 1],
  167. material: { color: "#888888" },
  168. physics: { isStatic: true, colliderType: "cuboid" },
  169. },
  170. },
  171. GameCapsule: {
  172. props: z.object({
  173. ...gameTransformShadowPhysicsDamage,
  174. radius: z.number().nullable(),
  175. length: z.number().nullable(),
  176. capSegments: z.number().nullable(),
  177. radialSegments: z.number().nullable(),
  178. }),
  179. description: "Capsule with optional physics and damage.",
  180. example: {
  181. position: [0, 1, 0],
  182. material: { color: "#00cccc" },
  183. physics: { mass: 1, colliderType: "capsule" },
  184. },
  185. },
  186. GameKnot: {
  187. props: z.object({
  188. ...gameTransformShadowPhysicsDamage,
  189. radius: z.number().nullable(),
  190. tube: z.number().nullable(),
  191. tubularSegments: z.number().nullable(),
  192. radialSegments: z.number().nullable(),
  193. p: z.number().nullable(),
  194. q: z.number().nullable(),
  195. }),
  196. description: "Torus knot with optional physics and damage.",
  197. example: {
  198. position: [0, 1, 0],
  199. radius: 1,
  200. tube: 0.3,
  201. material: { color: "#ff44ff" },
  202. },
  203. },
  204. GameTetrahedron: {
  205. props: z.object({
  206. ...gameTransformShadowPhysicsDamage,
  207. radius: z.number().nullable(),
  208. }),
  209. description: "Tetrahedron with optional physics and damage.",
  210. },
  211. GameOctahedron: {
  212. props: z.object({
  213. ...gameTransformShadowPhysicsDamage,
  214. radius: z.number().nullable(),
  215. }),
  216. description: "Octahedron with optional physics and damage.",
  217. },
  218. GameDodecahedron: {
  219. props: z.object({
  220. ...gameTransformShadowPhysicsDamage,
  221. radius: z.number().nullable(),
  222. }),
  223. description: "Dodecahedron with optional physics and damage.",
  224. },
  225. GameIcosahedron: {
  226. props: z.object({
  227. ...gameTransformShadowPhysicsDamage,
  228. radius: z.number().nullable(),
  229. }),
  230. description: "Icosahedron with optional physics and damage.",
  231. },
  232. GameExtrude: {
  233. props: z.object({
  234. ...gameTransformShadowPhysicsDamage,
  235. shapeData: z
  236. .object({
  237. points: z.array(z.tuple([z.number(), z.number()])),
  238. holes: z.array(z.array(z.tuple([z.number(), z.number()]))).nullable(),
  239. })
  240. .nullable(),
  241. depth: z.number().nullable(),
  242. }),
  243. description: "Extruded 2D shape into 3D with optional physics and damage.",
  244. example: {
  245. position: [0, 0.5, 0],
  246. shapeData: {
  247. points: [
  248. [-0.5, -0.5],
  249. [0.5, -0.5],
  250. [0.5, 0.5],
  251. [-0.5, 0.5],
  252. ],
  253. },
  254. depth: 1,
  255. material: { color: "#ff8800" },
  256. },
  257. },
  258. GameTube: {
  259. props: z.object({
  260. ...gameTransformShadowPhysicsDamage,
  261. radius: z.number().nullable(),
  262. tubularSegments: z.number().nullable(),
  263. radialSegments: z.number().nullable(),
  264. }),
  265. description: "Tube geometry following a curve with optional physics.",
  266. example: {
  267. position: [0, 0.5, 0],
  268. radius: 0.1,
  269. material: { color: "#00ccff" },
  270. },
  271. },
  272. GameShape: {
  273. props: z.object({
  274. ...gameTransformShadowPhysicsDamage,
  275. shapeData: z
  276. .object({
  277. points: z.array(z.tuple([z.number(), z.number()])),
  278. holes: z.array(z.array(z.tuple([z.number(), z.number()]))).nullable(),
  279. })
  280. .nullable(),
  281. }),
  282. description:
  283. "2D shape geometry (flat polygon) with optional physics and damage.",
  284. example: {
  285. position: [0, 0.5, 0],
  286. shapeData: {
  287. points: [
  288. [-0.5, -0.5],
  289. [0.5, -0.5],
  290. [0, 0.5],
  291. ],
  292. },
  293. material: { color: "#ff00ff" },
  294. },
  295. },
  296. GameMesh: {
  297. props: z.object({
  298. ...gameTransformShadowPhysicsDamage,
  299. meshData: z
  300. .object({
  301. vertices: z.array(z.number()),
  302. indices: z.array(z.number()),
  303. normals: z.array(z.number()).nullable(),
  304. uvs: z.array(z.number()).nullable(),
  305. })
  306. .nullable(),
  307. }),
  308. description:
  309. "Custom mesh with raw vertex data. Specify vertices, indices, normals, and UVs.",
  310. example: {
  311. position: [0, 0.5, 0],
  312. meshData: {
  313. vertices: [-0.5, -0.5, 0, 0.5, -0.5, 0, 0, 0.5, 0],
  314. indices: [0, 1, 2],
  315. },
  316. material: { color: "#88ff00" },
  317. },
  318. },
  319. // Unified light component
  320. GameLight: {
  321. props: z.object({
  322. ...transformProps,
  323. lightType: z.enum(["ambient", "directional", "point", "spot"]),
  324. color: z.string().nullable(),
  325. intensity: z.number().nullable(),
  326. distance: z.number().nullable(),
  327. decay: z.number().nullable(),
  328. angle: z.number().nullable(),
  329. penumbra: z.number().nullable(),
  330. castShadow: z.boolean().nullable(),
  331. objectId: z.string().nullable(),
  332. }),
  333. description:
  334. "Unified light component. Set lightType to ambient, directional, point, or spot.",
  335. example: {
  336. lightType: "directional",
  337. position: [5, 10, 5],
  338. intensity: 1,
  339. castShadow: true,
  340. },
  341. },
  342. // Player controller
  343. Player: {
  344. props: z.object({
  345. ...transformProps,
  346. objectId: z.string().nullable(),
  347. isPlayer: z.boolean().nullable(),
  348. }),
  349. description:
  350. "Player spawn point. Enables first/third-person controls when playing.",
  351. example: {
  352. position: [0, 0, 0],
  353. },
  354. },
  355. // Character NPC
  356. GameCharacter: {
  357. props: z.object({
  358. ...transformProps,
  359. ...shadowProps,
  360. modelUrl: z.string(),
  361. role: z.string().nullable(),
  362. physics: physicsSchema.nullable(),
  363. objectId: z.string().nullable(),
  364. }),
  365. description:
  366. "NPC character with GLTF model, proximity interaction, and AI dialogue.",
  367. example: {
  368. position: [3, 0, 0],
  369. modelUrl: "/models/character.glb",
  370. role: "village guard",
  371. },
  372. },
  373. // Game model with physics
  374. GameModel: {
  375. props: z.object({
  376. ...transformProps,
  377. ...shadowProps,
  378. url: z.string(),
  379. physics: physicsSchema.nullable(),
  380. damage: damageSchema.nullable(),
  381. objectId: z.string().nullable(),
  382. }),
  383. description: "GLTF/GLB 3D model with optional physics and damage.",
  384. example: {
  385. url: "/models/crate.glb",
  386. position: [0, 0, 0],
  387. physics: { isStatic: true, colliderType: "cuboid" },
  388. },
  389. },
  390. // Sound emitter
  391. SoundEmitter: {
  392. props: z.object({
  393. ...transformProps,
  394. url: z.string(),
  395. loop: z.boolean().nullable(),
  396. volume: z.number().nullable(),
  397. positional: z.boolean().nullable(),
  398. distance: z.number().nullable(),
  399. objectId: z.string().nullable(),
  400. }),
  401. description: "Positional or global audio emitter.",
  402. example: {
  403. position: [0, 1, 0],
  404. url: "/sounds/ambient.mp3",
  405. loop: true,
  406. positional: true,
  407. },
  408. },
  409. // Media plane
  410. MediaPlane: {
  411. props: z.object({
  412. ...transformProps,
  413. ...shadowProps,
  414. url: z.string(),
  415. mediaType: z.enum(["image", "video"]),
  416. loop: z.boolean().nullable(),
  417. autoplay: z.boolean().nullable(),
  418. muted: z.boolean().nullable(),
  419. width: z.number().nullable(),
  420. height: z.number().nullable(),
  421. objectId: z.string().nullable(),
  422. }),
  423. description: "Image or video displayed on a 3D plane.",
  424. example: {
  425. position: [0, 2, -3],
  426. url: "/images/poster.jpg",
  427. mediaType: "image",
  428. width: 3,
  429. height: 2,
  430. },
  431. },
  432. // Ground plane (physics-enabled floor)
  433. GroundPlane: {
  434. props: z.object({
  435. ...transformProps,
  436. material: materialSchema.nullable(),
  437. size: z.number().nullable(),
  438. }),
  439. description: "Physics-enabled ground plane.",
  440. example: {
  441. position: [0, -0.1, 0],
  442. size: 5000,
  443. material: { color: "#4CAF50" },
  444. },
  445. },
  446. };
  447. export type GameComponentDefinitions = typeof gameComponentDefinitions;
  448. export const catalog = defineCatalog(schema, {
  449. components: gameComponentDefinitions,
  450. actions: {},
  451. });