lecodes-sdk 0.19.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.
Files changed (367) hide show
  1. package/README.md +114 -0
  2. package/dist/global.d.ts +503 -0
  3. package/dist/host.d.ts +61 -0
  4. package/dist/inject.js +4730 -0
  5. package/dist/types/animate/animate.d.ts +20 -0
  6. package/dist/types/animate/bezier.d.ts +2 -0
  7. package/dist/types/animate/easings.d.ts +21 -0
  8. package/dist/types/canvas/Canvas.d.ts +168 -0
  9. package/dist/types/core/Aspect.d.ts +171 -0
  10. package/dist/types/core/InspectorUI.d.ts +88 -0
  11. package/dist/types/core/StateMachine.d.ts +81 -0
  12. package/dist/types/core/color.d.ts +13 -0
  13. package/dist/types/core/compWrite.d.ts +11 -0
  14. package/dist/types/core/events.d.ts +11 -0
  15. package/dist/types/core/fields.d.ts +58 -0
  16. package/dist/types/core/registry.d.ts +7 -0
  17. package/dist/types/core/signals.d.ts +29 -0
  18. package/dist/types/core/time.d.ts +31 -0
  19. package/dist/types/g2/Camera2D.d.ts +19 -0
  20. package/dist/types/g2/CharacterController2D.d.ts +134 -0
  21. package/dist/types/g2/Node2D.d.ts +91 -0
  22. package/dist/types/g2/OneWay2D.d.ts +24 -0
  23. package/dist/types/g2/Physics2D.d.ts +147 -0
  24. package/dist/types/g2/Scene2D.d.ts +72 -0
  25. package/dist/types/g2/Shape2D.d.ts +58 -0
  26. package/dist/types/g2/Sprite.d.ts +51 -0
  27. package/dist/types/g2/SpriteAnimation.d.ts +50 -0
  28. package/dist/types/g2/SpriteSheet.d.ts +69 -0
  29. package/dist/types/g2/Texture2D.d.ts +16 -0
  30. package/dist/types/g2/Tilemap.d.ts +20 -0
  31. package/dist/types/g2/Tileset.d.ts +32 -0
  32. package/dist/types/g2/Trigger2D.d.ts +20 -0
  33. package/dist/types/g2/autotile.d.ts +133 -0
  34. package/dist/types/g2/cells.d.ts +14 -0
  35. package/dist/types/g2/defineScene2d.d.ts +141 -0
  36. package/dist/types/g2/groups2d.d.ts +16 -0
  37. package/dist/types/g2/loop.d.ts +4 -0
  38. package/dist/types/g2/scenarios2d.d.ts +30 -0
  39. package/dist/types/g2/touch.d.ts +2 -0
  40. package/dist/types/gl/Camera.d.ts +79 -0
  41. package/dist/types/gl/CameraPlace.d.ts +26 -0
  42. package/dist/types/gl/CharacterController.d.ts +98 -0
  43. package/dist/types/gl/Gearbox.d.ts +86 -0
  44. package/dist/types/gl/Geometry.d.ts +39 -0
  45. package/dist/types/gl/IK.d.ts +53 -0
  46. package/dist/types/gl/InstancedMesh.d.ts +46 -0
  47. package/dist/types/gl/Light.d.ts +73 -0
  48. package/dist/types/gl/Lightmap.d.ts +85 -0
  49. package/dist/types/gl/Locomotion.d.ts +279 -0
  50. package/dist/types/gl/Material.d.ts +100 -0
  51. package/dist/types/gl/Mesh.d.ts +33 -0
  52. package/dist/types/gl/Model.d.ts +53 -0
  53. package/dist/types/gl/NavAgent.d.ts +99 -0
  54. package/dist/types/gl/NavMesh.d.ts +169 -0
  55. package/dist/types/gl/Node.d.ts +99 -0
  56. package/dist/types/gl/Noise.d.ts +20 -0
  57. package/dist/types/gl/Particles.d.ts +277 -0
  58. package/dist/types/gl/Physics.d.ts +91 -0
  59. package/dist/types/gl/Plane.d.ts +19 -0
  60. package/dist/types/gl/Ragdoll.d.ts +86 -0
  61. package/dist/types/gl/Ray.d.ts +8 -0
  62. package/dist/types/gl/Scene.d.ts +221 -0
  63. package/dist/types/gl/Shape.d.ts +122 -0
  64. package/dist/types/gl/Terrain.d.ts +280 -0
  65. package/dist/types/gl/Texture.d.ts +27 -0
  66. package/dist/types/gl/Trigger.d.ts +10 -0
  67. package/dist/types/gl/Vehicle.d.ts +191 -0
  68. package/dist/types/gl/Wheel.d.ts +95 -0
  69. package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
  70. package/dist/types/gl/animation/Animator.d.ts +219 -0
  71. package/dist/types/gl/animation/Layer.d.ts +31 -0
  72. package/dist/types/gl/animation/Loop.d.ts +17 -0
  73. package/dist/types/gl/animation/Playback.d.ts +36 -0
  74. package/dist/types/gl/animation/core.d.ts +367 -0
  75. package/dist/types/gl/controls.d.ts +19 -0
  76. package/dist/types/gl/physicsEvents.d.ts +1 -0
  77. package/dist/types/gl/scenarios.d.ts +98 -0
  78. package/dist/types/gl/state.d.ts +5 -0
  79. package/dist/types/gl/terrainMesh.d.ts +46 -0
  80. package/dist/types/gl/touch.d.ts +2 -0
  81. package/dist/types/inject.d.ts +123 -0
  82. package/dist/types/math/Mathf.d.ts +39 -0
  83. package/dist/types/math/mat4.d.ts +66 -0
  84. package/dist/types/math/quat.d.ts +53 -0
  85. package/dist/types/math/vec.d.ts +129 -0
  86. package/dist/types/net/codec.d.ts +25 -0
  87. package/dist/types/net/core.d.ts +106 -0
  88. package/dist/types/net/index.d.ts +80 -0
  89. package/dist/types/net/replication.d.ts +101 -0
  90. package/dist/types/plugins/camera.d.ts +25 -0
  91. package/dist/types/plugins/geolocation.d.ts +39 -0
  92. package/dist/types/plugins/oauth.d.ts +25 -0
  93. package/dist/types/plugins/permission.d.ts +1 -0
  94. package/dist/types/plugins/push.d.ts +46 -0
  95. package/dist/types/plugins/qr.d.ts +21 -0
  96. package/dist/types/plugins/service.d.ts +33 -0
  97. package/dist/types/runtime/app.d.ts +67 -0
  98. package/dist/types/runtime/appEvents.d.ts +7 -0
  99. package/dist/types/runtime/channel.d.ts +1 -0
  100. package/dist/types/runtime/clipboard.d.ts +7 -0
  101. package/dist/types/runtime/datetime.d.ts +80 -0
  102. package/dist/types/runtime/device.d.ts +126 -0
  103. package/dist/types/runtime/fetch.d.ts +31 -0
  104. package/dist/types/runtime/files.d.ts +33 -0
  105. package/dist/types/runtime/input.d.ts +121 -0
  106. package/dist/types/runtime/media.d.ts +46 -0
  107. package/dist/types/runtime/misc.d.ts +11 -0
  108. package/dist/types/runtime/net.d.ts +15 -0
  109. package/dist/types/runtime/rpc.d.ts +44 -0
  110. package/dist/types/runtime/service.d.ts +1 -0
  111. package/dist/types/runtime/share.d.ts +2 -0
  112. package/dist/types/runtime/storage.d.ts +5 -0
  113. package/dist/types/runtime/touch.d.ts +56 -0
  114. package/dist/types/scene/defineScene.d.ts +296 -0
  115. package/dist/types/scene/editorPlugins.d.ts +82 -0
  116. package/dist/types/scene/gizmos.d.ts +56 -0
  117. package/dist/types/scene/grammar.d.ts +56 -0
  118. package/dist/types/scene/level.d.ts +98 -0
  119. package/dist/types/scene/material.d.ts +76 -0
  120. package/dist/types/server/auth/appConfig.d.ts +16 -0
  121. package/dist/types/server/auth/global.d.ts +56 -0
  122. package/dist/types/server/auth/models.d.ts +164 -0
  123. package/dist/types/server/auth/types.d.ts +56 -0
  124. package/dist/types/server/channel.d.ts +28 -0
  125. package/dist/types/server/context.d.ts +27 -0
  126. package/dist/types/server/db/defineDb.d.ts +36 -0
  127. package/dist/types/server/db/fields.d.ts +146 -0
  128. package/dist/types/server/db/index.d.ts +6 -0
  129. package/dist/types/server/db/marci/query.d.ts +326 -0
  130. package/dist/types/server/db/types.d.ts +163 -0
  131. package/dist/types/server/errors.d.ts +8 -0
  132. package/dist/types/server/inject.d.ts +5 -0
  133. package/dist/types/ui/NativeView.d.ts +38 -0
  134. package/dist/types/ui/UI.d.ts +27 -0
  135. package/dist/types/ui/UIBottomSheet.d.ts +71 -0
  136. package/dist/types/ui/UIButton.d.ts +30 -0
  137. package/dist/types/ui/UIContainer.d.ts +33 -0
  138. package/dist/types/ui/UIImage.d.ts +35 -0
  139. package/dist/types/ui/UIInput.d.ts +110 -0
  140. package/dist/types/ui/UIModal.d.ts +70 -0
  141. package/dist/types/ui/UINode.d.ts +363 -0
  142. package/dist/types/ui/UIPager.d.ts +123 -0
  143. package/dist/types/ui/UIPopover.d.ts +39 -0
  144. package/dist/types/ui/UIScreen.d.ts +57 -0
  145. package/dist/types/ui/UIScrollable.d.ts +40 -0
  146. package/dist/types/ui/UISpacer.d.ts +9 -0
  147. package/dist/types/ui/UITabs.d.ts +96 -0
  148. package/dist/types/ui/UIText.d.ts +19 -0
  149. package/dist/types/ui/UIVideo.d.ts +28 -0
  150. package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
  151. package/dist/types/ui/UIWidget.d.ts +65 -0
  152. package/dist/types/ui/fonts.d.ts +9 -0
  153. package/dist/types/ui/presentable.d.ts +68 -0
  154. package/dist/types/ui/router.d.ts +40 -0
  155. package/dist/types/ui/theme.d.ts +26 -0
  156. package/dist/types.json +1 -0
  157. package/package.json +46 -0
  158. package/prompts/2d.md +395 -0
  159. package/prompts/3d-scene-files.md +113 -0
  160. package/prompts/3d-scene.md +119 -0
  161. package/prompts/3d.md +303 -0
  162. package/prompts/README.md +142 -0
  163. package/prompts/ar.md +109 -0
  164. package/prompts/canvas.md +27 -0
  165. package/prompts/compose.ts +46 -0
  166. package/prompts/concept.md +49 -0
  167. package/prompts/core-design.md +75 -0
  168. package/prompts/core.md +264 -0
  169. package/prompts/design.md +218 -0
  170. package/prompts/dist/2d-game.md +1530 -0
  171. package/prompts/dist/3d-app.md +1672 -0
  172. package/prompts/dist/ar-app.md +1548 -0
  173. package/prompts/dist/concept.md +49 -0
  174. package/prompts/dist/design.md +498 -0
  175. package/prompts/dist/ui-app.md +1134 -0
  176. package/prompts/index.md +37 -0
  177. package/prompts/intake-prompt.md +33 -0
  178. package/prompts/namer-prompt.md +12 -0
  179. package/prompts/router-prompt.md +50 -0
  180. package/prompts/select.ts +111 -0
  181. package/prompts/ui-design.md +203 -0
  182. package/prompts/ui.md +803 -0
  183. package/src/animate/animate.ts +238 -0
  184. package/src/animate/bezier.ts +138 -0
  185. package/src/animate/easings.ts +126 -0
  186. package/src/bridges.d.ts +1345 -0
  187. package/src/canvas/Canvas.ts +305 -0
  188. package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
  189. package/src/compile/__tests__/assetMacro.test.ts +74 -0
  190. package/src/compile/__tests__/assetName.test.ts +55 -0
  191. package/src/compile/__tests__/compile.test.ts +287 -0
  192. package/src/compile/__tests__/detectEntry.test.ts +132 -0
  193. package/src/compile/__tests__/fontMacro.test.ts +199 -0
  194. package/src/compile/aspectMacro.ts +86 -0
  195. package/src/compile/assetIconMacro.ts +384 -0
  196. package/src/compile/assetMacro.ts +146 -0
  197. package/src/compile/assetName.ts +50 -0
  198. package/src/compile/bundler.ts +287 -0
  199. package/src/compile/compileProject.ts +127 -0
  200. package/src/compile/detectEntry.ts +128 -0
  201. package/src/compile/fontMacro.ts +459 -0
  202. package/src/compile/fontRegistry.ts +78 -0
  203. package/src/compile/header.ts +67 -0
  204. package/src/compile/index.ts +100 -0
  205. package/src/compile/libraryImports.ts +52 -0
  206. package/src/compile/liteMaterial.ts +247 -0
  207. package/src/compile/sceneEditor.ts +88 -0
  208. package/src/compile/serverSplit.ts +233 -0
  209. package/src/compile/serverTypes.ts +227 -0
  210. package/src/compile/sfnt.ts +98 -0
  211. package/src/compile/shaderSchema.ts +202 -0
  212. package/src/compile/shaderTargets.ts +77 -0
  213. package/src/compile/sourcemap.ts +25 -0
  214. package/src/core/Aspect.ts +568 -0
  215. package/src/core/InspectorUI.ts +212 -0
  216. package/src/core/StateMachine.ts +308 -0
  217. package/src/core/__tests__/stateMachine.test.ts +132 -0
  218. package/src/core/color.ts +66 -0
  219. package/src/core/compWrite.ts +42 -0
  220. package/src/core/events.ts +38 -0
  221. package/src/core/fields.ts +120 -0
  222. package/src/core/registry.ts +23 -0
  223. package/src/core/signals.ts +277 -0
  224. package/src/core/time.ts +84 -0
  225. package/src/g2/Camera2D.ts +40 -0
  226. package/src/g2/CharacterController2D.ts +276 -0
  227. package/src/g2/Node2D.ts +267 -0
  228. package/src/g2/OneWay2D.ts +66 -0
  229. package/src/g2/Physics2D.ts +346 -0
  230. package/src/g2/Scene2D.ts +209 -0
  231. package/src/g2/Shape2D.ts +259 -0
  232. package/src/g2/Sprite.ts +89 -0
  233. package/src/g2/SpriteAnimation.ts +171 -0
  234. package/src/g2/SpriteSheet.ts +166 -0
  235. package/src/g2/Texture2D.ts +47 -0
  236. package/src/g2/Tilemap.ts +41 -0
  237. package/src/g2/Tileset.ts +71 -0
  238. package/src/g2/Trigger2D.ts +77 -0
  239. package/src/g2/autotile.ts +433 -0
  240. package/src/g2/cells.ts +91 -0
  241. package/src/g2/defineScene2d.ts +381 -0
  242. package/src/g2/groups2d.ts +106 -0
  243. package/src/g2/loop.ts +50 -0
  244. package/src/g2/scenarios2d.ts +69 -0
  245. package/src/g2/touch.ts +83 -0
  246. package/src/gl/Camera.ts +160 -0
  247. package/src/gl/CameraPlace.ts +52 -0
  248. package/src/gl/CharacterController.ts +238 -0
  249. package/src/gl/Gearbox.ts +212 -0
  250. package/src/gl/Geometry.ts +279 -0
  251. package/src/gl/IK.ts +193 -0
  252. package/src/gl/InstancedMesh.ts +132 -0
  253. package/src/gl/Light.ts +135 -0
  254. package/src/gl/Lightmap.ts +402 -0
  255. package/src/gl/Locomotion.ts +498 -0
  256. package/src/gl/Material.ts +279 -0
  257. package/src/gl/Mesh.ts +83 -0
  258. package/src/gl/Model.ts +124 -0
  259. package/src/gl/NavAgent.ts +337 -0
  260. package/src/gl/NavMesh.ts +397 -0
  261. package/src/gl/Node.ts +350 -0
  262. package/src/gl/Noise.ts +30 -0
  263. package/src/gl/Particles.ts +676 -0
  264. package/src/gl/Physics.ts +222 -0
  265. package/src/gl/Plane.ts +53 -0
  266. package/src/gl/Ragdoll.ts +270 -0
  267. package/src/gl/Ray.ts +16 -0
  268. package/src/gl/Scene.ts +505 -0
  269. package/src/gl/Shape.ts +409 -0
  270. package/src/gl/Terrain.ts +1092 -0
  271. package/src/gl/Texture.ts +63 -0
  272. package/src/gl/Trigger.ts +45 -0
  273. package/src/gl/Vehicle.ts +473 -0
  274. package/src/gl/Wheel.ts +240 -0
  275. package/src/gl/animation/AnimationClip.ts +204 -0
  276. package/src/gl/animation/Animator.ts +329 -0
  277. package/src/gl/animation/Layer.ts +39 -0
  278. package/src/gl/animation/Loop.ts +31 -0
  279. package/src/gl/animation/Playback.ts +52 -0
  280. package/src/gl/animation/core.ts +670 -0
  281. package/src/gl/controls.ts +95 -0
  282. package/src/gl/physicsEvents.ts +20 -0
  283. package/src/gl/scenarios.ts +291 -0
  284. package/src/gl/state.ts +6 -0
  285. package/src/gl/terrainMesh.ts +219 -0
  286. package/src/gl/touch.ts +68 -0
  287. package/src/host.d.ts +61 -0
  288. package/src/inject.ts +203 -0
  289. package/src/math/Mathf.ts +118 -0
  290. package/src/math/mat4.ts +278 -0
  291. package/src/math/quat.ts +232 -0
  292. package/src/math/vec.ts +255 -0
  293. package/src/net/codec.ts +119 -0
  294. package/src/net/core.ts +384 -0
  295. package/src/net/index.ts +181 -0
  296. package/src/net/replication.ts +622 -0
  297. package/src/plugins/camera.ts +81 -0
  298. package/src/plugins/geolocation.ts +123 -0
  299. package/src/plugins/oauth.ts +61 -0
  300. package/src/plugins/permission.ts +7 -0
  301. package/src/plugins/push.ts +132 -0
  302. package/src/plugins/qr.ts +73 -0
  303. package/src/plugins/service.ts +47 -0
  304. package/src/runtime/app.ts +101 -0
  305. package/src/runtime/appEvents.ts +54 -0
  306. package/src/runtime/channel.ts +61 -0
  307. package/src/runtime/clipboard.ts +20 -0
  308. package/src/runtime/datetime.ts +329 -0
  309. package/src/runtime/device.ts +293 -0
  310. package/src/runtime/fetch.ts +77 -0
  311. package/src/runtime/files.ts +108 -0
  312. package/src/runtime/input.ts +175 -0
  313. package/src/runtime/media.ts +111 -0
  314. package/src/runtime/misc.ts +16 -0
  315. package/src/runtime/net.ts +36 -0
  316. package/src/runtime/rpc.ts +218 -0
  317. package/src/runtime/service.ts +83 -0
  318. package/src/runtime/share.ts +9 -0
  319. package/src/runtime/storage.ts +13 -0
  320. package/src/runtime/touch.ts +76 -0
  321. package/src/scene/defineScene.ts +1396 -0
  322. package/src/scene/editorPlugins.ts +110 -0
  323. package/src/scene/gizmos.ts +148 -0
  324. package/src/scene/grammar.ts +120 -0
  325. package/src/scene/level.ts +296 -0
  326. package/src/scene/material.ts +188 -0
  327. package/src/server/auth/appConfig.ts +12 -0
  328. package/src/server/auth/global.ts +80 -0
  329. package/src/server/auth/host.ts +318 -0
  330. package/src/server/auth/models.ts +83 -0
  331. package/src/server/auth/types.ts +50 -0
  332. package/src/server/channel.ts +56 -0
  333. package/src/server/context.ts +36 -0
  334. package/src/server/db/defineDb.ts +237 -0
  335. package/src/server/db/fields.ts +132 -0
  336. package/src/server/db/httpTransport.ts +93 -0
  337. package/src/server/db/index.ts +7 -0
  338. package/src/server/db/marci/query.ts +412 -0
  339. package/src/server/db/types.ts +202 -0
  340. package/src/server/errors.ts +12 -0
  341. package/src/server/host.ts +74 -0
  342. package/src/server/inject.ts +13 -0
  343. package/src/server/runtime.ts +133 -0
  344. package/src/server/validate.ts +87 -0
  345. package/src/ui/NativeView.ts +142 -0
  346. package/src/ui/UI.ts +39 -0
  347. package/src/ui/UIBottomSheet.ts +139 -0
  348. package/src/ui/UIButton.ts +101 -0
  349. package/src/ui/UIContainer.ts +60 -0
  350. package/src/ui/UIImage.ts +83 -0
  351. package/src/ui/UIInput.ts +185 -0
  352. package/src/ui/UIModal.ts +139 -0
  353. package/src/ui/UINode.ts +830 -0
  354. package/src/ui/UIPager.ts +362 -0
  355. package/src/ui/UIPopover.ts +100 -0
  356. package/src/ui/UIScreen.ts +123 -0
  357. package/src/ui/UIScrollable.ts +87 -0
  358. package/src/ui/UISpacer.ts +14 -0
  359. package/src/ui/UITabs.ts +236 -0
  360. package/src/ui/UIText.ts +51 -0
  361. package/src/ui/UIVideo.ts +88 -0
  362. package/src/ui/UIVirtualizedList.ts +241 -0
  363. package/src/ui/UIWidget.ts +127 -0
  364. package/src/ui/fonts.ts +13 -0
  365. package/src/ui/presentable.ts +117 -0
  366. package/src/ui/router.ts +132 -0
  367. package/src/ui/theme.ts +84 -0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "lecodes-sdk",
3
+ "type": "module",
4
+ "sideEffects": false,
5
+ "exports": {
6
+ "./compile": "./src/compile/index.ts",
7
+ "./compile/assetName": "./src/compile/assetName.ts",
8
+ "./compile/detectEntry": "./src/compile/detectEntry.ts",
9
+ "./compile/shaderSchema": "./src/compile/shaderSchema.ts",
10
+ "./compile/liteMaterial": "./src/compile/liteMaterial.ts",
11
+ "./inject": "./src/inject.ts",
12
+ "./server": "./src/server/host.ts",
13
+ "./prompts/select": "./prompts/select.ts",
14
+ "./global.d.ts": "./dist/global.d.ts",
15
+ "./host.d.ts": "./dist/host.d.ts",
16
+ "./types.json": "./dist/types.json"
17
+ },
18
+ "scripts": {
19
+ "build": "bun run build-types.ts",
20
+ "test": "bun test",
21
+ "typecheck": "tsc --noEmit",
22
+ "docs:check": "node docs/check.mjs",
23
+ "prompts:build": "bun prompts/compose.ts",
24
+ "sync:marcidb": "bun scripts/sync-marcidb.ts",
25
+ "prepublishOnly": "bun run build"
26
+ },
27
+ "dependencies": {
28
+ "@letary/chisel": "^0.10.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "^1.3.14",
32
+ "@types/node": "^25.9.1",
33
+ "typescript": "~5.8.3",
34
+ "gl-matrix": "^3.4.4"
35
+ },
36
+ "version": "0.19.0",
37
+ "files": [
38
+ "src",
39
+ "dist",
40
+ "prompts",
41
+ "README.md"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
package/prompts/2d.md ADDED
@@ -0,0 +1,395 @@
1
+ # 2D engine
2
+
3
+ Scene2D + nodes (Sprite, Tilemap) + aspects (SpriteAnimation, Shape2D/Physics2D/Trigger2D/OneWay2D/CharacterController2D) + Canvas for baked text/vector graphics. All globals. `Scene2D` ≠ the 3D `Scene`; one scene active at a time. World units: 1 unit = 1 logical px at camera zoom 1. **Y-UP** (up = +y, gravity = negative y). Rotations in **degrees**, CCW. Game loop = setLoop(dt) from core. The UI kit renders ON TOP of the 2D canvas — build HUDs/menus/on-screen controls with UIScreen/UIWidget as usual (open the scene, then the HUD screen).
4
+
5
+ // ===== SCENE & NODES =====
6
+
7
+ new Scene2D(options?: {
8
+ background?: Color // clear color (hex string or 0xRRGGBB); settable later via scene.background
9
+ filter?: 'nearest' | 'linear' // texture sampling; default nearest (crisp pixel art), 'linear' for hi-res
10
+ }) // art. filter is GLOBAL (one sampler for all textures), re-applied on open()
11
+
12
+ scene.open(): this // make active: starts the 2D engine, closes any other scene
13
+ scene.close(): this / scene.destroy(): void; Scene2D.active // static: open Scene2D | null
14
+ scene.camera // the scene's Camera2D (readonly — never construct Camera2D yourself)
15
+ scene.add(...nodes) / scene.remove(...nodes): this // only ROOT nodes need add — children come with their parent
16
+
17
+ // Layers & Y-sort: node.layer picks the layer (higher draws on top); within a layer node.z orders.
18
+ scene.layer(1).ySort() // top-down depth sorting: sprites lower on screen draw in front ("walk behind
19
+ // trees"). Compares ANCHOR points → give nodes on that layer anchor [0.5, 1] (feet)
20
+
21
+ scene.pick(worldPoint: Vec2Like): Node2D | null // body-free hit-test: topmost visible sprite at the point
22
+
23
+ // Scene-level pointer events — fire for EVERY tap; ev: { target: Node2D|null, worldX, worldY, clientX, clientY }
24
+ scene.addEventListener('click', ev => {}) // pointer-up; target = hit physics-body node or null
25
+ scene.addEventListener('touchstart', ev => {}) // pointer-down; ev.track({...}) for drags (see core) —
26
+ // onMove gives SCREEN px, camera.screenToWorld for world drags
27
+
28
+ // --- Node2D — base node: an empty transform. Sprite/Tilemap extend it. ---
29
+ // Bare Node2D: grouping parent, spawn marker, invisible collider/trigger. removeEventListener to detach.
30
+ const node = new Node2D()
31
+ node.x, node.y // single-axis setters — use for per-axis writes
32
+ node.position = [120, 64] // Vec2Like; getter returns a copy — node.position.x = 3 written directly still works (compiled to node.x)
33
+ node.rotation = 45 // degrees CCW
34
+ node.scale = 2 // number or [sx, sy] — DRAW scale only (physics shapes ignore it)
35
+ node.layer = 1; node.z = 5; node.visible = false
36
+
37
+ node.add(...children): this // attach (keeps world transform); node.remove(child) detaches → root
38
+ node.setParent(parent | null, keepWorld = true): this; node.parent / node.children
39
+ node.worldPosition // Vec2 — composes all ancestors (fresh copy)
40
+ node.worldToLocal(p) / node.localToWorld(p): Vec2
41
+ node.destroy(): void // frees the native entity. CHILDREN ARE NOT DESTROYED — they reparent to root.
42
+
43
+ // Node events (node.addEventListener(channel, cb)):
44
+ // 'click' / 'touchstart' — pointer on this node's PHYSICS shape (needs Shape2D + Physics2D or Trigger2D)
45
+ // 'enter' / 'exit' — physics contact / sensor overlap began/ended; arg = the other Node2D
46
+ // 'loopReached' / 'completed' — SpriteAnimation clip looped / non-looping clip finished; arg = clip NAME
47
+
48
+ // --- Camera2D (scene.camera) ---
49
+ camera.position = [x, y] // world point at the CENTER of the screen (getter returns a copy)
50
+ camera.zoom = 2 // magnification; default 1. camera.rotation = 15 (degrees CCW)
51
+ camera.screenToWorld(sx, sy): Vec2 / camera.worldToScreen(wx, wy): Vec2 // logical px ↔ world (handles the Y flip)
52
+ // There is NO camera.follow() — lerp toward the target from setLoop, or better, use a LATE-phase aspect
53
+ // that reads the final post-physics position (see ASPECTS: CameraFollow, and Example 1).
54
+
55
+ // ===== SPRITES & TEXTURES =====
56
+
57
+ const tex = await Texture2D.load(asset('./hero.png')) // string url (asset() or https://) | FetchResponse | File
58
+ tex.width, tex.height // pixels (readonly); tex.destroy() frees it (textures are shared: many sprites, one texture)
59
+
60
+ new Sprite(options?: {
61
+ texture?: Texture2D | Canvas // a Canvas bakes to a texture on assign (sprite then defaults to its LOGICAL size)
62
+ anchor?: Vec2Like // pivot, Y-DOWN: [0,0] top-left … [1,1] bottom-right; default [0.5,0.5].
63
+ // [0.5, 1] = bottom-center = "feet" — use on Y-sorted layers
64
+ size?: Vec2Like // world size; default = texture pixel dims (1 texel = 1 world unit)
65
+ frame?: [u0, v0, u1, v1] // normalized UV sub-rect
66
+ color?: Color // tint MULTIPLIED with the texture; hex/packed only (default white = untinted)
67
+ opacity?: number // 0..1
68
+ layer?: number
69
+ position?: Vec2Like
70
+ })
71
+
72
+ sprite.texture = tex; sprite.size = [w, h]
73
+ sprite.setFramePx(x, y, w, h): this // atlas sub-rect in TEXTURE PIXELS (image-editor coords) — chainable.
74
+ // Does NOT change size — set size to the frame's pixel dims yourself
75
+ sprite.color = '#ff8800'; sprite.opacity = 0.5
76
+ sprite.flipX = true; sprite.flipY = true // mirror to face left/right — don't use negative scale
77
+
78
+ // ===== ASPECTS — the composition system =====
79
+ // Everything a node CAN DO attaches as an aspect — built-ins and your own game logic alike.
80
+ node.aspect(Class, opts?) // attach + configure; returns the node → chains. Re-calling merges opts
81
+ node.get(Class) // instance | undefined; node.has(Class) — boolean + type guard
82
+ node.removeAspect(Class) // detach (runs onDetach)
83
+ // Each aspect adds a NAMED ACCESSOR: hero.anim, hero.physics, hero.shape, hero.trigger, hero.controller —
84
+ // for your own, the first generic param: Aspect<'camFollow', …> → node.camFollow.
85
+
86
+ class CameraFollow extends Aspect<'camFollow', Node2D> { // <accessor name, host node kind>
87
+ camera!: Camera2D // class fields = configurable defaults, set via aspect() opts
88
+ smooth = 8
89
+ onAttach() {} // node + opts are set here; onDetach() for cleanup. NEVER write a constructor —
90
+ // the engine instantiates via node.aspect()
91
+ update(dt: number) { // ticked every frame while attached; dt in SECONDS
92
+ this.camera.position = this.camera.position.lerp(this.node.worldPosition, this.smooth * dt)
93
+ }
94
+ }
95
+ hero.aspect(CameraFollow, { camera: scene.camera })
96
+ // update() default phase = LATE (after the physics step — worldPosition is the final drawn position; right
97
+ // for cameras/followers/readers). Aspects that WRITE velocity/forces/kinematic moves set the class field
98
+ // `updateBeforePhysics = true` (same frame's step consumes them). `order = -10` sorts ticks within a phase.
99
+
100
+ // ===== SPRITE ANIMATION =====
101
+ // Sheet animation as an aspect on a Sprite — accessor node.anim. Playback advances natively.
102
+
103
+ sprite.aspect(SpriteAnimation, {
104
+ size?: [w, h] // ONE frame cell in texture px; default = full texture. Also sets sprite.size to the cell
105
+ fps?: number // clip default; default 12
106
+ loop?: boolean // clip default; default true
107
+ clips?: Record<string, number[] | { frames: number[], fps?, loop? }>
108
+ })
109
+ // Frame indices are ROW-MAJOR through the sheet grid: 0 = top-left cell, left-to-right then down.
110
+ // The sprite MUST already have its texture when the aspect attaches (grid slices from it) — throws otherwise.
111
+
112
+ sprite.anim.play('walk'): this // throws on unknown clip. Re-playing the active clip is a NO-OP — safe every frame
113
+ sprite.anim.stop(): this; sprite.anim.speed = 2 // rate multiplier (write-only)
114
+ sprite.anim.current // playing clip name | null; sprite.anim.frame — current index (readonly)
115
+
116
+ // Events land on the NODE, arg = clip NAME ('loopReached' = looping clip wrapped):
117
+ hero.addEventListener('completed', clip => { if (clip === 'die') hero.destroy() }) // non-looping clip finished
118
+
119
+ // ===== TILEMAP =====
120
+ // One node draws a whole level: atlas texture on a uniform grid, uploaded once, view-culled natively.
121
+
122
+ new Tilemap(options: {
123
+ texture: Texture2D
124
+ cols: number, rows: number // map size in tiles
125
+ tile: number | [w, h] // cell size in world units (number = square)
126
+ atlas: [atlasCols, atlasRows] // how the texture slices into tiles
127
+ data: number[] | Int32Array // row-major atlas indices, length cols*rows; -1 = empty
128
+ })
129
+ // data row 0 = the TOP row of the map (reads like level text), even though world Y is up. Atlas indices
130
+ // are row-major too: 0 = the atlas's top-left tile. node.position = the map's BOTTOM-LEFT corner.
131
+ map.setTile(x, y, index): this // grid coords as in data (row 0 = top); -1 clears. Cheap.
132
+ // LIMITS — a static uniform grid. NO animated tiles (swap cells with setTile yourself), NO per-tile flip/
133
+ // rotate (bake variants into the atlas), and NO collision: physics NEVER reads a tilemap, by design. Solid
134
+ // geometry is scene objects — Node2Ds with Shape2D box/polygon/chain + a static Physics2D.
135
+
136
+ // ===== 2D PHYSICS (Box2D) =====
137
+
138
+ Physics2D.configure(config?: {
139
+ gravity?: Vec2Like // world units/s², Y-up (down = negative Y); default [0, -980]. Top-down → [0, 0]
140
+ pixelsPerMeter?: number // Box2D tolerance tuning (you still author in world units); default 64
141
+ subSteps?: number // default 4
142
+ })
143
+ // OPTIONAL: the world builds itself on the first body. Calling it later changes gravity LIVE and leaves
144
+ // every existing body alone. Fixed 60 Hz step (max 4 sub-steps/frame); rendered transforms interpolated.
145
+ Physics2D.supported // some builds ship without physics — the family then silently no-ops. Guard with it.
146
+
147
+ // --- Shape2D — pure geometry (node.shape). Does nothing alone. Attach BEFORE the body. ---
148
+ node.aspect(Shape2D, {
149
+ box?: [hw, hh] // HALF-extents: box:[12,20] = a 24×40 box
150
+ circle?: number // radius
151
+ capsule?: { from: Vec2Like, to: Vec2Like, radius: number } // between two node-local points
152
+ segment?: { from: Vec2Like, to: Vec2Like } // one thin edge — STATIC only
153
+ polygon?: Vec2Like[] // CONVEX, ≤ 8 local points (a concave outline is silently replaced by its hull)
154
+ chain?: Vec2Like[] // polyline: long CONCAVE seam-free surfaces (terrain, cave walls). STATIC only.
155
+ loop?: boolean // chain: close the contour. flip?: put the solid side on the other side
156
+ origin?: Vec2Like // the collider's centre relative to the node — applies to EVERY kind
157
+ })
158
+ // PREFER `{}`: it measures the rect the node actually DRAWS (sprite size × scale, positioned by anchor),
159
+ // so a sprite anchored at its feet gets a collider around the art with no hand-computed numbers.
160
+ // Re-configuring rebuilds the fixture IN PLACE (same body, same id, same velocity) — that is how a crouch
161
+ // works. A chain is ONE-SIDED: solid on the RIGHT of the point order, so left-to-right ground needs flip.
162
+
163
+ // --- Physics2D — rigid body (node.physics). Requires a Shape2D on the node (throws otherwise). ---
164
+ node.aspect(Physics2D, {
165
+ motion?: 'static' | 'kinematic' | 'dynamic' // default 'dynamic'
166
+ mass?: number // omitted = from the collider's AREA (a big crate really is heavier)
167
+ friction?: number // default 0.6; two bodies combine as sqrt(a*b) — the LOWER value wins
168
+ bounce?: number // restitution 0..1, default 0; two bodies combine as MAX — the bouncier wins
169
+ fixedRotation?: boolean // lock rotation; default false. bullet?: continuous collision for fast movers
170
+ gravityScale?: number // per-body multiplier, default 1. linearDamping? / angularDamping?
171
+ group?: PhysicsGroup2D // collision filtering, see below
172
+ })
173
+ // Everything above is LIVE: node.physics.motion = 'static' freezes a crate, .friction / .bounce / .mass /
174
+ // .group / .enabled / .awake all apply immediately.
175
+ node.physics.velocity = [vx, vy] // world units/s (getter returns a fresh Vec2)
176
+ node.physics.angularVelocity = 90 // degrees/s, rw
177
+ node.physics.applyImpulse([x, y]) // at the centre of mass — NEVER spins the body
178
+ node.physics.applyImpulseAt(v, worldPoint) // …the lever arm becomes spin (a bullet's hit point)
179
+ node.physics.applyForce([x, y])
180
+ // PHYSICS OWNS THE TRANSFORM of dynamic/kinematic bodies. Never set node.position per frame on one —
181
+ // drive dynamic with velocity/applyImpulse. A position/rotation WRITE is a teleport that reaches the
182
+ // body (there is no moveTo any more); READING node.position is always correct. Keep bodies and sensors
183
+ // on ROOT nodes — one parented to a moving node does not follow it.
184
+
185
+ // --- Trigger2D — sensor zone (node.trigger). Fires enter/exit, blocks nothing. ---
186
+ goal.aspect(Shape2D, { circle: 48 }).aspect(Trigger2D) // { group? }
187
+ goal.position = [x, y] // the zone FOLLOWS the node (it is kinematic)
188
+ goal.trigger.enabled = false // switch it off instead of parking it off-screen
189
+ goal.addEventListener('enter', (other, contact) => {}) // contact: { point, normal, speed }
190
+ // enter/exit also fire for SOLID contacts, delivered to BOTH nodes; the normal points back at YOU, so
191
+ // normal.y > 0.7 reads as "I landed on top of it". Adding the listener is what ENABLES contact events on
192
+ // that body — without one nothing is reported and nothing is paid. Characters ARE detected by triggers.
193
+
194
+ // --- OneWay2D — one-way surface (node.oneWay): the semisolid ledge of a platformer ---
195
+ ledge.aspect(Shape2D, {}).aspect(Physics2D, { motion: 'static' }).aspect(OneWay2D)
196
+ // { normal?: Vec2Like (which side is solid, default [0,1]), arc?: degrees (default 90), enabled? }
197
+ // Jump up through it, stand on top. Works for characters AND ordinary bodies, no per-frame cost.
198
+
199
+ // --- Collision groups: SUBTRACTIVE and SYMMETRIC ---
200
+ const player = Physics2D.addGroup()
201
+ const bullets = Physics2D.addGroup().ignoreSelf().ignore(player) // 30 groups max
202
+ // A group collides with everything EXCEPT what it ignores, so adding a new group later can't silently
203
+ // stop existing pairs colliding. ignore() is symmetric (name it once) and works after bodies exist.
204
+
205
+ // --- CharacterController2D — KINEMATIC collide-and-slide mover (node.controller) ---
206
+ node.aspect(CharacterController2D, { gravityScale?: 1, maxSlope?: 45, group?: PhysicsGroup2D })
207
+ // Needs a CAPSULE Shape2D (derived from the sprite if absent). Does NOT use Physics2D. Zero JS per frame.
208
+ c.move(x) // HORIZONTAL command, world units/s — a VELOCITY, not a displacement (never * dt).
209
+ // It EXPIRES each frame: no call = standing still, releasing keys needs no zero.
210
+ c.move(x, y) // FREE MODE (gravityScale 0): both axes. Top-down / swimming / a ladder.
211
+ c.velocityY = 700 // LATCHED ballistic vertical. No ground check — do coyote time yourself.
212
+ c.velocity // read = what the SOLVER did (into a wall reads ~0); write = latch the whole vector
213
+ c.grounded / c.groundState ('ground'|'slope'|'air') / c.groundNormal / c.groundNode
214
+ c.collisions // [{ node, normal }] from the last step — wall jumps, pushing crates
215
+ c.dropThrough = platform // ignore that node's OneWay2D surfaces; a plain latch, you clear it
216
+ c.resizing // a requested Shape2D resize didn't fit (standing up under a beam)
217
+ // PLATFORMER vs TOP-DOWN is gravityScale, not two controllers. A kinematic platform carries the
218
+ // character automatically. Crouch by re-configuring Shape2D with a shorter capsule; a refusal changes
219
+ // nothing, so just call it again next frame. Solid contacts arrive in c.collisions, not as 'enter'.
220
+
221
+ // Queries (static). q = { groups?: PhysicsGroup2D[], ignore?: Node2D | Node2D[] }
222
+ Physics2D.raycast(from, to, q?) // closest → { node, point, normal, fraction } | null
223
+ Physics2D.raycastAll(from, to, q?) // every hit, NEAREST FIRST (max 32)
224
+ Physics2D.overlapPoint(p, q?) // topmost by DRAW order (layer, then z)
225
+ Physics2D.overlapCircle(centre, r, q?) / overlapBox(centre, half, q?) / overlapCapsule(from, to, r, q?)
226
+ // `ignore` is the don't-hit-myself case. overlapCapsule is the DIRECTED shape (a beam with width, a sword
227
+ // arc) and doubles as the SWEPT test for a moving circle — it can't tunnel through a thin wall.
228
+
229
+ // Pointer picking: a node with Shape2D + (Physics2D | Trigger2D | CharacterController2D) is tappable —
230
+ // it receives 'click'/'touchstart'. The scene ALSO gets every event (ev.target, ev.worldX/worldY).
231
+ // Without physics: scene.pick(camera.screenToWorld(ev.clientX, ev.clientY)).
232
+
233
+ // ===== CANVAS IN 2D =====
234
+ // The Canvas drawing surface (see the CANVAS section above) is how TEXT and vector graphics get
235
+ // into sprites — there is no Text2D node. Assign it wherever a texture goes; it bakes automatically:
236
+ const label = new Sprite({ texture: c, layer: 2 }) // sprite world size = the canvas's LOGICAL size
237
+ // Redraw + c.update() to refresh the sprite's pixels (see the drawing-pad example below).
238
+
239
+ // ===== COMMON MISTAKES — DO NOT DO THESE =====
240
+
241
+ // ❌ writing the transform of a dynamic body per frame — physics owns it and overwrites you
242
+ setLoop(() => { hero.y += 2 }) // WRONG
243
+ setLoop(() => { hero.physics.velocity = [0, 120] }) // ✅ drive the body (kinematic: moveTo)
244
+
245
+ // ❌ expecting scale to resize the collider
246
+ sprite.scale = 2 // draws 2×, physics box UNCHANGED — Shape2D never reads scale
247
+ sprite.aspect(Shape2D, { box: [12, 20] }) // ✅ declare the extents you want (HALF-extents: this is 24×40)
248
+
249
+ // ❌ Physics2D.configure() after bodies exist — resets the world, orphans every body
250
+ // ✅ configure once at startup, before the first Shape2D/Physics2D/Trigger2D attach
251
+
252
+ // node.position.x = 3 compiles to node.x = 3 (direct spelling only). ❌ const p = node.position; p.x = 3 — a stored copy, no-op
253
+
254
+ // ❌ default [0.5,0.5] anchor on a Y-sorted layer — depth sorts wrong. ✅ anchor: [0.5, 1] (feet)
255
+
256
+ // ❌ scene.camera.follow(hero) — does not exist. ✅ lerp in setLoop, or a LATE-phase aspect (Example 1)
257
+
258
+ // ❌ per-frame setFramePx to animate — poorly re-implements SpriteAnimation
259
+ setLoop(() => sprite.setFramePx(...)) // WRONG
260
+ sprite.aspect(SpriteAnimation, { size: [32, 48], clips: { walk: [0,1,2,3] } }) // ✅ then anim.play('walk')
261
+
262
+ // ❌ CSS color strings on sprites — engine colors are hex/packed only (Canvas fillStyle is the exception)
263
+ sprite.color = 'rgba(255,0,0,0.5)' // WRONG
264
+ sprite.color = '#ff0000'; sprite.opacity = 0.5 // ✅
265
+
266
+ // ❌ attaching SpriteAnimation before the sprite has a texture — throws (the grid slices from it)
267
+ // ❌ attaching Physics2D before Shape2D on the same node — throws (attach the shape first)
268
+ // ❌ setting spawn position AFTER attaching Physics2D — the body already seeded from [0,0]; position first
269
+
270
+ ## Code examples
271
+
272
+ // === EXAMPLE 1: top-down collector — tilemap, physics-driven hero, trigger pickups, camera follow, HUD ===
273
+ <file name="main.ts">
274
+ const TILE = 32, COLS = 24, ROWS = 18
275
+ const W = COLS * TILE, H = ROWS * TILE
276
+ const SPEED = 170
277
+
278
+ // LATE-phase follow aspect: runs after the physics step, reads the hero's final drawn position — no lag.
279
+ class CameraFollow extends Aspect<'camFollow', Node2D> {
280
+ camera!: Camera2D
281
+ smooth = 8
282
+ update(dt: number) {
283
+ this.camera.position = this.camera.position.lerp(this.node.worldPosition, this.smooth * dt)
284
+ }
285
+ }
286
+
287
+ async function main() {
288
+ const scene = new Scene2D({ background: '#2a3b24' })
289
+ scene.layer(1).ySort() // actors/props sort by their feet
290
+ Physics2D.configure({ gravity: [0, 0] }) // top-down: no gravity. BEFORE any body is created.
291
+
292
+ const texTiles = await Texture2D.load(asset('./tiles.png')) // 4x4 atlas of 32px tiles
293
+ const texHero = await Texture2D.load(asset('./hero.png')) // sheet of 32x48 cells: idle + walk
294
+ const texCoin = await Texture2D.load(asset('./coin.png')) // 6x1 sheet of 16x16 frames
295
+
296
+ // Ground: grass (atlas 0) with scattered detail (atlas 1). data row 0 = TOP row of the map.
297
+ const data = new Int32Array(COLS * ROWS)
298
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() < 0.12 ? 1 : 0
299
+ scene.add(new Tilemap({ texture: texTiles, cols: COLS, rows: ROWS, tile: TILE, atlas: [4, 4], data }))
300
+
301
+ // Border walls: static segments (world coords — the nodes sit at [0,0]) keep the hero inside
302
+ const wall = (ax: number, ay: number, bx: number, by: number) => new Node2D()
303
+ .aspect(Shape2D, { segment: { from: [ax, ay], to: [bx, by] } })
304
+ .aspect(Physics2D, { motion: 'static' })
305
+ wall(0, 0, W, 0); wall(0, H, W, H); wall(0, 0, 0, H); wall(W, 0, W, H)
306
+
307
+ // Hero: feet anchor for Y-sort; feet-box collider offset up from the anchor; dynamic body driven
308
+ // by velocity (physics owns the transform). Position set BEFORE the body attaches.
309
+ const hero = new Sprite({ texture: texHero, anchor: [0.5, 1], layer: 1, position: [W / 2, H / 2] })
310
+ .aspect(SpriteAnimation, { size: [32, 48], fps: 10, clips: { idle: [0], walk: [1, 2, 3, 4] } })
311
+ .aspect(Shape2D, { box: [10, 6], offset: [0, 6] })
312
+ .aspect(Physics2D, { motion: 'dynamic', fixedRotation: true, friction: 0 })
313
+ .aspect(CameraFollow, { camera: scene.camera })
314
+ hero.anim.play('idle')
315
+ scene.add(hero)
316
+
317
+ // HUD on top of the 2D canvas (UI kit)
318
+ let scoreText: UIText
319
+ const hud = UIScreen(
320
+ scoreText = UIText("Coins: 0").style({ color: "white", fontSize: 18, fontWeight: 700 }),
321
+ ).style({ p: 16, pt: "max(safe-top, 16px)" })
322
+ hud.open()
323
+
324
+ // Coin pickups: spinning sprite + a Trigger2D sensor — 'enter' fires on overlap, blocks nothing
325
+ let score = 0
326
+ const spawnCoin = (x: number, y: number) => {
327
+ const coin = new Sprite({ texture: texCoin, anchor: [0.5, 0.5], layer: 1, position: [x, y] })
328
+ .aspect(SpriteAnimation, { size: [16, 16], fps: 8, clips: { spin: [0, 1, 2, 3, 4, 5] } })
329
+ .aspect(Shape2D, { circle: 12 })
330
+ .aspect(Trigger2D)
331
+ coin.anim.play('spin')
332
+ coin.addEventListener('enter', other => {
333
+ if (other !== hero) return
334
+ scoreText.text = `Coins: ${++score}`
335
+ coin.destroy()
336
+ })
337
+ scene.add(coin)
338
+ }
339
+ for (let i = 0; i < 10; i++)
340
+ spawnCoin(48 + Math.random() * (W - 96), 48 + Math.random() * (H - 96))
341
+
342
+ scene.open()
343
+
344
+ // Input → velocity, every frame. (For touch, add an on-screen joystick UIWidget and merge its
345
+ // vector here — same velocity write.) Dynamic bodies keep momentum, so zero velocity when idle.
346
+ const body = hero.get(Physics2D)!
347
+ const held = (a: string, b: string) => Input.key(a) || Input.key(b) ? 1 : 0
348
+ setLoop(() => {
349
+ const dx = held('KeyD', 'ArrowRight') - held('KeyA', 'ArrowLeft')
350
+ const dy = held('KeyW', 'ArrowUp') - held('KeyS', 'ArrowDown')
351
+ const len = Math.hypot(dx, dy)
352
+ if (len > 0) {
353
+ body.velocity = [dx / len * SPEED, dy / len * SPEED] // Y-up: W/up = +y
354
+ if (dx !== 0) hero.flipX = dx < 0
355
+ hero.anim.play('walk') // re-playing the active clip is a no-op — safe every frame
356
+ } else {
357
+ body.velocity = [0, 0]
358
+ hero.anim.play('idle')
359
+ }
360
+ })
361
+ }
362
+
363
+ main()
364
+ </file>
365
+
366
+ // === EXAMPLE 2: drawing pad — Canvas sprite + scene gesture ===
367
+ <file name="main.ts">
368
+ const scene = new Scene2D({ background: '#ffffff', filter: 'linear' })
369
+ const W = device.width, H = device.height
370
+ const paper = new Canvas(W, H, { pixelRatio: device.pixelRatio }) // logical size; pixelRatio = crispness
371
+ paper.strokeStyle = '#1a3aa8'; paper.lineWidth = 4; paper.lineCap = 'round'
372
+
373
+ // Canvas-backed sprite takes the LOGICAL size; center the camera on it so world == screen area
374
+ scene.add(new Sprite({ texture: paper, position: [W / 2, H / 2] }))
375
+ scene.camera.position = [W / 2, H / 2]
376
+ scene.open()
377
+
378
+ // screen px → canvas coords (canvas is Y-down from its top-left; world is Y-up)
379
+ const toCanvas = (sx: number, sy: number): [number, number] => {
380
+ const w = scene.camera.screenToWorld(sx, sy)
381
+ return [w.x, H - w.y]
382
+ }
383
+
384
+ scene.addEventListener('touchstart', ev => {
385
+ let [lx, ly] = toCanvas(ev.clientX, ev.clientY)
386
+ ev.track({
387
+ onMove({ clientX, clientY }) { // logical SCREEN px
388
+ const [x, y] = toCanvas(clientX, clientY)
389
+ paper.beginPath().moveTo(lx, ly).lineTo(x, y).stroke()
390
+ paper.update() // re-bake: push new pixels to the sprite
391
+ lx = x; ly = y
392
+ },
393
+ })
394
+ })
395
+ </file>
@@ -0,0 +1,113 @@
1
+ ## Scene files (.scene.ts)
2
+
3
+ Projects may contain `*.scene.ts` files: declarative scenes that the platform's VISUAL scene
4
+ editor reads and writes. The user may have built them by dragging models around — treat the file
5
+ as their artwork. You may edit values and add nodes/aspects, but keep the `defineScene({...})`
6
+ literal-object shape; an imperative rewrite (`new Scene()` + `scene.add(...)`) destroys their
7
+ ability to keep editing it visually. `defineScene`, `use`, `ref` and `make` are globals.
8
+
9
+ // ===== DEFINE SCENE =====
10
+
11
+ // Default-export exactly one defineScene call per .scene.ts file.
12
+ export default defineScene({
13
+ env?: SceneOptions, // same options object as `new Scene(...)` above (skybox, ibl, bloom…)
14
+ nodes?: {
15
+ name: { // names are sibling-unique, no '/' or ':' in them
16
+ // -- source: AT MOST ONE of these per node; none = empty group node --
17
+ mesh?: { kind: 'box', size?: n | [x,y,z] } | { kind: 'sphere' | 'cylinder' | 'plane', ... },
18
+ model?: string, // GLB via the asset macro: asset('./hero.glb')
19
+ light?: { kind: 'sun', direction?, intensity?, shadowsQuality? }, // 'sun' is the ONLY kind
20
+ camera?: { fov?, near?, far? }, // this node IS the scene camera (defaults 60 / 0.01 / 1000)
21
+ make?: make(factoryFn, { ...literalArgs }), // code-built subtree from a user function
22
+ prefab?: SceneHandle, // another .scene.ts's default export (import it)
23
+ // -- for a mesh source --
24
+ material?: { lit: { color?, metallic?, roughness? } } | { unlit: { color? } },
25
+ // -- transform / render --
26
+ position?: [x,y,z], eulerAngles?: [x,y,z], scale?: [x,y,z] | n,
27
+ visible?, castShadows?, receiveShadows?,
28
+ locked?: boolean, // editor-only flag, zero runtime effect — leave it alone
29
+ // -- behavior / hierarchy --
30
+ aspects?: [ use(AspectClass, { ...props }) ], // props may use ref('otherNode') for node refs
31
+ children?: { name: { ...same shape } },
32
+ // pose parts INSIDE a model/prefab, and attach a child node to such a part:
33
+ overrides?: { 'Bone/Path': { position?, eulerAngles?, scale?, visible? } },
34
+ mount?: 'Bone/Path', // parents this node to that part of the PARENT node's asset
35
+ },
36
+ },
37
+ })
38
+
39
+ // ===== USING A SCENE FROM APP CODE =====
40
+
41
+ import city from './city.scene' // extension-less specifier; default export: SceneHandle
42
+
43
+ const { scene, nodes, get } = await city.open() // load (fetches GLBs) + become the active view
44
+ // city.load() — instantiate without showing; both are idempotent (same instance every call)
45
+ nodes.crate // typed by source: model → Model, mesh → Mesh, light → Light
46
+ nodes['props/lamp'] // keys are absolute '/'-joined paths; get(path) for dynamic
47
+ nodes.crate.position = [1, 0.5, 2] // plain SDK nodes — assign vectors, or scalar axes: node.y = 2
48
+ nodes.hero.anim.play('walk') // a model node is a Model: animations, parts — all there
49
+ scene.close() // closing goes through the scene (there is NO handle.close())
50
+
51
+ // ===== BEHAVIOR & INPUT =====
52
+
53
+ // Put per-frame behavior in ASPECTS attached in the file — not in code that mutates the doc:
54
+ // aspects: [use(Spin, { speed: 40 })]
55
+ // Built-in no-code aspects: MoveTo, FollowPath ({ path: ref('waypoints'), duration }), Spin,
56
+ // LookAt, PlayAnimation. Custom ones are ~5 lines (see Aspects; update(dt) — dt in SECONDS):
57
+ export class Spinner extends Aspect<'spinner'> {
58
+ speed = 30 // class fields = props settable from use(...)
59
+ update(dt: number) { this.node.eulerAngles = [0, this.node.eulerAngles.y + this.speed * dt, 0] }
60
+ }
61
+ // Clicks/taps: a node is pickable only with a Shape aspect — aspects: [use(Shape, {})] — then
62
+ // nodes.crate.addEventListener('click', ev => ...). Catch-all with target (null on miss):
63
+ // scene.addEventListener('click', ev => ev.target). Shape alone = static pick body; moving
64
+ // clickables also need Physics.
65
+
66
+ // ===== UI OVER AN OPEN SCENE =====
67
+
68
+ // Exactly one Presentable is visible at a time: UIScreen(...).open() REPLACES the scene. A HUD is
69
+ // a UIWidget attached to the scene — it shows/hides and transitions together with it:
70
+ const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
71
+ .style({ top: 'max(safe-top, 16px)', left: 16 })
72
+ hud.attachTo(scene).show()
73
+
74
+ // ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
75
+
76
+ // - Rebuilding a .scene.ts imperatively, or moving its content into main.ts. Edit the def in
77
+ // place; logic goes into aspect classes or app code around handle.open().
78
+ // - scene.ref(...), handle.close(), onTap — none exist. It's nodes[path] / get(path),
79
+ // loaded.scene.close(), addEventListener('click').
80
+ // - Two source keys on one node (e.g. mesh + model) — throws at load.
81
+ // - Expecting clicks without use(Shape, {}) on the node.
82
+ // - const p = node.position; p.x = 3 — NO-OP (a stored copy). node.position.x = 3 itself compiles to node.x = 3.
83
+ // - env HDRI paths or light kinds other than 'sun' — not supported; ibl is just a boolean.
84
+
85
+ ## Scene file wiring example
86
+
87
+ // A scene built in the visual editor, wired up with a click counter and a HUD.
88
+
89
+ <file name="main.scene.ts">
90
+ export default defineScene({
91
+ env: { skybox: '#a9b6c8' },
92
+ nodes: {
93
+ camera: { camera: {}, position: [0, 4, 9], eulerAngles: [-22, 0, 0] },
94
+ sun: { light: { kind: 'sun', shadowsQuality: 1 } },
95
+ ground: { mesh: { kind: 'box', size: [12, 0.4, 12] }, material: { lit: { color: '#3d4351' } },
96
+ position: [0, -0.2, 0], locked: true },
97
+ crate: { mesh: { kind: 'box' }, material: { lit: { color: '#8a8f98' } }, position: [0, 0.5, 0],
98
+ aspects: [use(Shape, {}), use(Spin, { speed: 25 })] },
99
+ },
100
+ })
101
+ </file>
102
+
103
+ <file name="main.ts">
104
+ import mainScene from './main.scene'
105
+
106
+ const { scene, nodes } = await mainScene.open()
107
+
108
+ let taps = 0
109
+ const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
110
+ UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 }).attachTo(scene).show()
111
+
112
+ nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
113
+ </file>