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
@@ -0,0 +1,381 @@
1
+ // 2D scenes as data: the runtime behind `.scene2d.ts` files (docs/scene2d-editor-plan.md §4).
2
+ // The 2D sibling of scene/defineScene.ts — same literal grammar (one default-exported
3
+ // `defineScene2d({...})` call, nodes keyed by name, `use(...)` aspects, `make(...)` factories),
4
+ // with the 2D source vocabulary: `sprite` (a named entry of an imported `.sprite.ts` sheet),
5
+ // `tilemap` (texture + grid + a `cells('…')` payload), `camera`, `make`, or none = group.
6
+ //
7
+ // // forest.scene2d.ts
8
+ // import props from './props.sprite'
9
+ // import hero from './hero.sprite'
10
+ // export default defineScene2d({
11
+ // env: { background: '#5a6b3b', pixelPerfect: true, gravity: [0, 0] },
12
+ // layers: { 1: { ySort: true } },
13
+ // nodes: {
14
+ // ground: { tilemap: { texture: asset('./TileSet.png'), tile: 64, atlas: [64, 64],
15
+ // cells: cells('…') }, layer: 0 },
16
+ // tree1: { sprite: { sheet: props, name: 'tree_orange' }, position: [160, 288], layer: 1 },
17
+ // player: { sprite: { sheet: hero, clip: 'idle' }, position: [800, 200], layer: 1,
18
+ // aspects: [use(CameraFollow, { smooth: 0.12 })] },
19
+ // },
20
+ // })
21
+ //
22
+ // // main.ts
23
+ // import forest from './forest.scene2d'
24
+ // const { scene, nodes } = await forest.open()
25
+ //
26
+ // Colliders and bodies are SCENE semantics (sheets are art only): give a node `aspects:
27
+ // [use(Shape2D, {…}), use(Physics2D, {…})]`. Aspects attach normally in play mode; in edit mode
28
+ // (`globalThis.__lecodesSceneEdit`) they are held as data, so nothing falls or fires while
29
+ // editing — clip playback still runs (render-side).
30
+ // Prefabs (scene-in-scene) are a later phase — not in this grammar yet.
31
+
32
+ import type { ColorInput } from "../core/color"
33
+ import type { Vec2Like } from "../math/vec"
34
+ import {
35
+ EDIT_FLAG, isEditMode, resolveRefs, type AspectEntry, type MakeEntry as SharedMakeEntry,
36
+ } from "../scene/grammar"
37
+ import { Scene2D } from "./Scene2D"
38
+ import { Node2D } from "./Node2D"
39
+ import { Sprite } from "./Sprite"
40
+ import { Tilemap } from "./Tilemap"
41
+ import { Texture2D } from "./Texture2D"
42
+ import { Physics2D } from "./Physics2D"
43
+ import { SpriteSheet } from "./SpriteSheet"
44
+ import { Tileset, Autotile2D } from "./Tileset"
45
+ import type { SpriteAnimation } from "./SpriteAnimation"
46
+ import type { CellsData } from "./cells"
47
+ import type { With } from "../core/Aspect"
48
+
49
+ // ---- the literal grammar (what the visual editor reads and writes) -----------
50
+
51
+ export type Scene2dEnv = {
52
+ /** Background color — '#5a6b3b', 0x5a6b3b, or [r,g,b] in 0..1. */
53
+ background?: ColorInput
54
+ /** Texture sampling: 'nearest' (crisp pixel art, engine default) or 'linear'. */
55
+ filter?: "nearest" | "linear"
56
+ /** Pixel-art crispness: snap the camera to whole screen pixels (seeds CameraFollow's
57
+ * `pixelSnap`, and the initial camera position lands on whole world units). */
58
+ pixelPerfect?: boolean
59
+ /** Physics world gravity (Y-up: down is negative). Presence configures the world before any
60
+ * body is created — `[0, 0]` for top-down worlds. Omit to leave the world unconfigured. */
61
+ gravity?: Vec2Like
62
+ }
63
+
64
+ /** A sprite node: one named entry of an imported `.sprite.ts` sheet. The sheet is ART only —
65
+ * colliders/bodies are scene semantics: put Shape2D / Physics2D / Trigger2D in `aspects`. */
66
+ export type SpriteSourceDef = {
67
+ /** The imported sheet handle: `import props from './props.sprite'` … `sheet: props`. */
68
+ sheet: SpriteSheet
69
+ /** The entry name; omit when the sheet declares exactly one sprite. */
70
+ name?: string
71
+ /** Start this clip immediately (sliced entries with clips). */
72
+ clip?: string
73
+ /** Initial facing for `perDirection` clips. */
74
+ direction?: string
75
+ }
76
+
77
+ export type TilemapSourceDef = {
78
+ /** The tileset image — `asset('./TileSet.png')`. */
79
+ texture: string
80
+ /** On-screen cell size in world units — a number (square) or [w, h]. */
81
+ tile: number | [number, number]
82
+ /** Atlas grid: how the texture slices into tiles, [atlasCols, atlasRows]. */
83
+ atlas: [number, number]
84
+ /** The map: a `cells('…')` payload — or any `{ cols, rows, data }` of your own. */
85
+ cells: CellsData | { cols: number, rows: number, data: number[] | Int32Array }
86
+ }
87
+
88
+ /** A camera marker node: seeds the scene camera's starting position (+ zoom). To MOVE the camera
89
+ * in play, attach `CameraFollow` to the node being followed — not aspects on this marker. */
90
+ export type Camera2dNodeDef = { zoom?: number }
91
+
92
+ export type Scene2dNodeDef = {
93
+ // -- source (at most one; none = plain group node) --
94
+ sprite?: SpriteSourceDef
95
+ tilemap?: TilemapSourceDef
96
+ camera?: Camera2dNodeDef
97
+ /** A code-built subtree — `make(factoryFn, { ...literal args })`, factory returns a Node2D. */
98
+ make?: SharedMakeEntry<any, Node2D>
99
+ // -- transform / render --
100
+ position?: Vec2Like
101
+ /** Degrees, CCW. */
102
+ rotation?: number
103
+ scale?: Vec2Like | number
104
+ /** Draw layer (int) — higher renders on top. */
105
+ layer?: number
106
+ /** Intra-layer depth for non-Y-sorted layers. */
107
+ z?: number
108
+ /** Sprite tint (sprite nodes only). */
109
+ color?: ColorInput
110
+ /** Sprite opacity 0..1 (sprite nodes only). */
111
+ opacity?: number
112
+ flipX?: boolean
113
+ flipY?: boolean
114
+ visible?: boolean
115
+ /** Editor-only: the move gizmo won't target this node (fields still edit). No runtime effect. */
116
+ locked?: boolean
117
+ // -- capabilities / hierarchy --
118
+ aspects?: readonly AspectEntry<any>[]
119
+ children?: Record<string, Scene2dNodeDef>
120
+ }
121
+
122
+ export type Scene2dCameraDef = {
123
+ position?: Vec2Like
124
+ zoom?: number
125
+ }
126
+
127
+ export type Scene2dDef = {
128
+ env?: Scene2dEnv
129
+ /** Per-layer config, keyed by layer number: `layers: { 1: { ySort: true } }`. */
130
+ layers?: Record<number | string, { ySort?: boolean }>
131
+ /** Starting camera when there's no camera NODE (a camera node wins). */
132
+ camera?: Scene2dCameraDef
133
+ nodes?: Record<string, Scene2dNodeDef>
134
+ }
135
+
136
+ // ---- typed handle -------------------------------------------------------------
137
+
138
+ type SourceNodeOf<N extends Scene2dNodeDef> =
139
+ N extends { sprite: { clip: string } } ? Sprite & { anim: SpriteAnimation }
140
+ : N extends { sprite: SpriteSourceDef } ? Sprite
141
+ : N extends { tilemap: TilemapSourceDef } ? Tilemap
142
+ : Node2D
143
+
144
+ type AspectsOf<N extends Scene2dNodeDef> =
145
+ N extends { aspects: readonly AspectEntry<infer A>[] } ? A : never
146
+
147
+ type NodeOf<N extends Scene2dNodeDef> =
148
+ [AspectsOf<N>] extends [never] ? SourceNodeOf<N> : With<SourceNodeOf<N>, AspectsOf<N>>
149
+
150
+ type UnionToIntersection<U> =
151
+ (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
152
+
153
+ type ChildMapsOf<T extends Record<string, Scene2dNodeDef>> =
154
+ { [K in keyof T]: T[K] extends { children: infer C extends Record<string, Scene2dNodeDef> } ? NodesOf<C> : never }[keyof T]
155
+
156
+ type NodesOf<T extends Record<string, Scene2dNodeDef>> =
157
+ { [K in keyof T]: NodeOf<T[K]> } &
158
+ ([ChildMapsOf<T>] extends [never] ? unknown : UnionToIntersection<ChildMapsOf<T>>)
159
+
160
+ export type Scene2dNodes<D extends Scene2dDef> =
161
+ D["nodes"] extends Record<string, Scene2dNodeDef> ? NodesOf<D["nodes"]> : Record<string, Node2D>
162
+
163
+ export type LoadedScene2d<D extends Scene2dDef> = {
164
+ scene: Scene2D
165
+ nodes: Scene2dNodes<D>
166
+ }
167
+
168
+ // ---- runtime -------------------------------------------------------------------
169
+
170
+ const createSprite = (name: string, def: SpriteSourceDef): Sprite => {
171
+ const sheet = def.sheet
172
+ if (!(sheet instanceof SpriteSheet)) {
173
+ throw new Error(`Scene2d node "${name}": sprite.sheet is not a sheet handle (import the .sprite file's default export)`)
174
+ }
175
+ const entryName = def.name ?? (sheet.spriteNames.length === 1 ? sheet.spriteNames[0]! : undefined)
176
+ if (entryName === undefined) {
177
+ throw new Error(`Scene2d node "${name}": sprite.name is required — the sheet has ${sheet.spriteNames.length} sprites (${sheet.spriteNames.join(", ")})`)
178
+ }
179
+ return sheet.make(entryName, {
180
+ clip: def.clip,
181
+ direction: def.direction,
182
+ } as Parameters<SpriteSheet["make"]>[1])
183
+ }
184
+
185
+ const createTilemap = async (
186
+ name: string, def: TilemapSourceDef, aspects?: readonly AspectEntry<any>[],
187
+ ): Promise<Tilemap> => {
188
+ const grid = def.cells
189
+ if (!grid || typeof grid !== "object" || typeof (grid as CellsData).cols !== "number") {
190
+ throw new Error(`Scene2d node "${name}": tilemap.cells must be a cells('…') payload or a { cols, rows, data } value`)
191
+ }
192
+ // Autotile2D on the node flips `cells` from raw atlas indices to material ids — derive the
193
+ // display indices HERE, before the Tilemap exists (aspects are data on the def), so the native
194
+ // layer keeps receiving a dumb index grid and derived data is never persisted.
195
+ let data: number[] | Int32Array = grid.data
196
+ for (const entry of aspects ?? []) {
197
+ if ((entry.ctor as unknown) !== Autotile2D) continue
198
+ const props = entry.props as Partial<Autotile2D> | undefined
199
+ if (props?.tileset instanceof Tileset) {
200
+ data = props.tileset.derive(grid.cols, grid.rows, data, props.seed ?? 0)
201
+ } else {
202
+ console.error(`[scene2d] node "${name}": Autotile2D has no tileset handle — cells render as raw indices`)
203
+ }
204
+ }
205
+ const texture = await Texture2D.load(def.texture)
206
+ return new Tilemap({
207
+ texture, cols: grid.cols, rows: grid.rows,
208
+ tile: def.tile, atlas: def.atlas, data,
209
+ })
210
+ }
211
+
212
+ const createSource = (name: string, def: Scene2dNodeDef): Node2D | Promise<Node2D> => {
213
+ const sources = [ def.sprite, def.tilemap, def.camera, def.make ].filter((s) => s !== undefined).length
214
+ if (sources > 1) throw new Error(`Scene2d node "${name}" declares more than one source (sprite/tilemap/camera/make)`)
215
+ if (def.sprite !== undefined) return createSprite(name, def.sprite)
216
+ if (def.tilemap !== undefined) return createTilemap(name, def.tilemap, def.aspects)
217
+ // make/camera/group nodes are plain wrappers — make subtrees mount under them in phase 2
218
+ return new Node2D()
219
+ }
220
+
221
+ const applyNode = (node: Node2D, name: string, def: Scene2dNodeDef): void => {
222
+ node.name = name
223
+ if (def.position) node.position = def.position
224
+ if (def.rotation !== undefined) node.rotation = def.rotation
225
+ if (def.scale !== undefined) node.scale = def.scale
226
+ if (def.layer !== undefined) node.layer = def.layer
227
+ if (def.z !== undefined) node.z = def.z
228
+ if (def.visible !== undefined) node.visible = def.visible
229
+ if (def.locked !== undefined) (node as { _sceneLocked?: boolean })._sceneLocked = def.locked
230
+ if (def.camera !== undefined) (node as { _sceneCamera?: boolean })._sceneCamera = true
231
+ if (def.children) (node as { _sceneChildOrder?: string[] })._sceneChildOrder = Object.keys(def.children)
232
+ if (node instanceof Sprite) {
233
+ if (def.color !== undefined) node.color = def.color
234
+ if (def.opacity !== undefined) node.opacity = def.opacity
235
+ if (def.flipX !== undefined) node.flipX = def.flipX
236
+ if (def.flipY !== undefined) node.flipY = def.flipY
237
+ }
238
+ }
239
+
240
+ /** The first camera-source def in file order (depth-first), or null. */
241
+ const findCamera = (defs?: Record<string, Scene2dNodeDef>): { name: string, def: Camera2dNodeDef } | null => {
242
+ for (const [ name, nd ] of Object.entries(defs ?? {})) {
243
+ if (nd.camera !== undefined) return { name, def: nd.camera }
244
+ const inner = findCamera(nd.children)
245
+ if (inner) return inner
246
+ }
247
+ return null
248
+ }
249
+
250
+ const attachAspects = (
251
+ node: Node2D, def: Scene2dNodeDef, nodes: Record<string, Node2D>, scene: Scene2D, env: Scene2dEnv,
252
+ ): void => {
253
+ if (isEditMode()) {
254
+ // Data-only (the future harness reads [ctor, props] from here) — no onAttach side effects
255
+ // (bodies, loops) while editing.
256
+ ;(node as unknown as { _sceneAspects: readonly AspectEntry<any>[] })._sceneAspects = def.aspects ?? []
257
+ return
258
+ }
259
+ for (const entry of def.aspects ?? []) {
260
+ let props = resolveRefs(entry.props as Record<string, unknown> | undefined, nodes)
261
+ // Aspects that declare `static wantsScene` (CameraFollow) get the scene wired in, and their
262
+ // pixel snapping defaults to env.pixelPerfect — explicit props always win.
263
+ if ((entry.ctor as { wantsScene?: boolean }).wantsScene) {
264
+ props = {
265
+ scene,
266
+ ...(env.pixelPerfect !== undefined ? { pixelSnap: env.pixelPerfect } : {}),
267
+ ...props,
268
+ }
269
+ }
270
+ ;(node as Node2D & { aspect(c: unknown, p?: unknown): unknown }).aspect(entry.ctor, props)
271
+ }
272
+ }
273
+
274
+ /** Run a def's make() factory (phase 2 — every node exists, so ref() args resolve). The subtree
275
+ * mounts under the wrapper node; the wrapper's transform stays editor-owned. */
276
+ const attachMake = (
277
+ node: Node2D, name: string, def: Scene2dNodeDef, nodes: Record<string, Node2D>, scene: Scene2D,
278
+ ): Promise<void> | undefined => {
279
+ const entry = def.make
280
+ if (!entry) return undefined
281
+ const args = resolveRefs({ ...(entry.args ?? {}) } as Record<string, unknown>, nodes) ?? {}
282
+ return Promise.resolve(entry.fn(args as never)).then((made) => {
283
+ if (made instanceof Node2D) {
284
+ node.add(made)
285
+ // membership is per-entity in creator-2d — add the whole made subtree to the draw set
286
+ const addAll = (n: Node2D): void => { scene.add(n); n.children.forEach(addAll) }
287
+ addAll(made)
288
+ }
289
+ }).catch((e) => { console.error(`[scene2d] make() factory failed on "${name}":`, e) })
290
+ }
291
+
292
+ /** Build one defs record into `scene` under `parent`, two-phase (all nodes first, then
293
+ * aspects/make — so `ref()`s resolve regardless of declaration order). */
294
+ const buildNodes = async (
295
+ defs: Record<string, Scene2dNodeDef>, parent: Node2D | null, scene: Scene2D,
296
+ nodes: Record<string, Node2D>, env: Scene2dEnv,
297
+ ): Promise<void> => {
298
+ const pending: { name: string, node: Node2D, def: Scene2dNodeDef }[] = []
299
+
300
+ const build = async (name: string, nd: Scene2dNodeDef, parentNode: Node2D | null): Promise<void> => {
301
+ const node = await createSource(name, nd)
302
+ if (parentNode) parentNode.add(node)
303
+ scene.add(node)
304
+ applyNode(node, name, nd)
305
+ pending.push({ name, node, def: nd })
306
+ nodes[name] = node
307
+ await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node)))
308
+ }
309
+
310
+ await Promise.all(Object.entries(defs).map(([ name, nd ]) => build(name, nd, parent)))
311
+ const waits: Promise<void>[] = []
312
+ for (const p of pending) {
313
+ attachAspects(p.node, p.def, nodes, scene, env)
314
+ const wait = attachMake(p.node, p.name, p.def, nodes, scene)
315
+ if (wait) waits.push(wait)
316
+ }
317
+ if (waits.length > 0) await Promise.all(waits)
318
+ }
319
+
320
+ const instantiate = async (def: Scene2dDef): Promise<{ scene: Scene2D, nodes: Record<string, Node2D> }> => {
321
+ const env = def.env ?? {}
322
+ // configure() resets the physics world, so it must run before any body is created (aspects
323
+ // create bodies right after the node build). Edit mode holds physics as data — skip.
324
+ if (env.gravity !== undefined && !isEditMode()) Physics2D.configure({ gravity: env.gravity })
325
+
326
+ const scene = new Scene2D({ background: env.background, filter: env.filter })
327
+ for (const [ key, layer ] of Object.entries(def.layers ?? {})) {
328
+ if (layer?.ySort) scene.layer(Number(key)).ySort(true)
329
+ }
330
+
331
+ const nodes: Record<string, Node2D> = {}
332
+ await buildNodes(def.nodes ?? {}, null, scene, nodes, env)
333
+
334
+ // a camera NODE wins over the top-level `camera:` block; both only SEED the starting view —
335
+ // attach CameraFollow to the tracked node to move the camera in play
336
+ const cam = findCamera(def.nodes)
337
+ const camNode = cam ? nodes[cam.name] : undefined
338
+ if (cam && camNode) {
339
+ const p = camNode.worldPosition
340
+ scene.camera.position = env.pixelPerfect ? [ Math.round(p.x), Math.round(p.y) ] : p
341
+ if (cam.def.zoom !== undefined) scene.camera.zoom = cam.def.zoom
342
+ } else if (def.camera) {
343
+ if (def.camera.position) scene.camera.position = def.camera.position
344
+ if (def.camera.zoom !== undefined) scene.camera.zoom = def.camera.zoom
345
+ }
346
+
347
+ return { scene, nodes }
348
+ }
349
+
350
+ export class Scene2dHandle<D extends Scene2dDef = Scene2dDef> {
351
+ readonly def: D
352
+ private _loading?: Promise<LoadedScene2d<D>>
353
+
354
+ constructor(def: D) { this.def = def }
355
+
356
+ /** Instantiate the scene (idempotent — subsequent calls return the same instance). Does not open. */
357
+ load(): Promise<LoadedScene2d<D>> {
358
+ this._loading ??= instantiate(this.def).then((live) => live as LoadedScene2d<D>)
359
+ return this._loading
360
+ }
361
+
362
+ /** Load and make active (only the active 2D scene renders). */
363
+ async open(): Promise<LoadedScene2d<D>> {
364
+ const loaded = await this.load()
365
+ loaded.scene.open()
366
+ return loaded
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Define a 2D scene as data — the default export of a `.scene2d.ts` file. Returns a typed handle:
372
+ * `const { scene, nodes } = await handle.open()` gives `nodes.<name>` typed by its source block
373
+ * (Sprite / Tilemap / Node2D) with its `use(...)`d aspects attached.
374
+ */
375
+ export const defineScene2d = <const D extends Scene2dDef>(def: D): Scene2dHandle<D> => {
376
+ const handle = new Scene2dHandle(def)
377
+ const g = globalThis as unknown as { [EDIT_FLAG]?: boolean, __lecodesScenes2d?: Scene2dHandle[] }
378
+ // Editor hook: expose defined handles to a host that runs the bundle in edit mode.
379
+ if (g[EDIT_FLAG]) (g.__lecodesScenes2d ??= []).push(handle as Scene2dHandle)
380
+ return handle
381
+ }
@@ -0,0 +1,106 @@
1
+ // Collision groups — which bodies are allowed to touch which. Called groups, not layers, because
2
+ // `layer` already means DRAW order in 2D (`node.layer`, `scene2d.layer(n).ySort()`).
3
+ //
4
+ // // game/groups.ts
5
+ // export const player = Physics2D.addGroup()
6
+ // export const bullets = Physics2D.addGroup().ignoreSelf().ignore(player)
7
+ //
8
+ // bullet.aspect(Physics2D, { motion: 'dynamic', group: bullets })
9
+ //
10
+ // Two decisions worth knowing, both deliberate:
11
+ //
12
+ // SUBTRACTIVE, not enumerative. A group collides with everything EXCEPT what it ignores, so adding a
13
+ // new group later cannot silently stop existing pairs from colliding. The enumerative form —
14
+ // "collides only with [ground, enemy]" — decays: introduce `crate` a month later and the bullets
15
+ // start flying through it, with nothing to notice.
16
+ //
17
+ // SYMMETRIC. Box2D's filter is an AND of two masks, so declaring the relation on one side only is the
18
+ // classic trap ("A allowed B but B didn't allow A" → they silently pass through). Both sides are set
19
+ // from one undirected edge here, so naming it once is enough.
20
+ //
21
+ // Budget: 32 bits, because a category/mask must cross the bridge as a plain JS number and only 2^32 is
22
+ // exactly representable there. One bit is the implicit default group, one is reserved for the
23
+ // character's shadow body — 30 user groups.
24
+
25
+ /** Collides with everything. */
26
+ export const ALL_GROUPS = 0xffffffff
27
+
28
+ /** @internal Bit 0 — every body that never names a group. */
29
+ export const DEFAULT_BIT = 1 << 0
30
+ /** @internal Bit 1 — the character's shadow body: solid shapes never see it, sensors always do. */
31
+ export const CHARACTER_BIT = 1 << 1
32
+
33
+ const MAX_USER_GROUPS = 30
34
+ let nextBit = 2
35
+ const all: PhysicsGroup2D[] = []
36
+
37
+ // Bodies whose filter has to be re-pushed when the topology changes. Only bodies that actually named
38
+ // a group register, so a project that never touches groups keeps an empty set.
39
+ const listeners = new Set<() => void>()
40
+ /** @internal */
41
+ export const _watchGroupChanges = (fn: () => void): void => { listeners.add(fn) }
42
+ /** @internal */
43
+ export const _unwatchGroupChanges = (fn: () => void): void => { listeners.delete(fn) }
44
+
45
+ export class PhysicsGroup2D {
46
+ /** The single category bit this group owns. */
47
+ readonly bit: number
48
+ /** Optional debug label, so an inspector says "bullets" rather than "group #3". */
49
+ readonly label: string
50
+
51
+ private _ignored = 0
52
+
53
+ /** @internal — use `Physics2D.addGroup()`. */
54
+ constructor(bit: number, label: string) {
55
+ this.bit = bit
56
+ this.label = label
57
+ }
58
+
59
+ /** @internal The Box2D pair for a shape in this group. */
60
+ get category(): number { return this.bit }
61
+ /** @internal */
62
+ get mask(): number { return (ALL_GROUPS & ~this._ignored) >>> 0 }
63
+
64
+ /**
65
+ * Stop colliding with these groups — symmetric, so naming it on one side is enough. Safe to call
66
+ * after bodies exist: their filters are re-pushed.
67
+ */
68
+ ignore(...groups: PhysicsGroup2D[]): this {
69
+ for (const g of groups) {
70
+ this._ignored |= g.bit
71
+ g._ignored |= this.bit
72
+ }
73
+ for (const fn of listeners) fn()
74
+ return this
75
+ }
76
+
77
+ /** Members of this group don't collide with EACH OTHER — bullets, debris, a ragdoll's own bones. */
78
+ ignoreSelf(): this {
79
+ this._ignored |= this.bit
80
+ for (const fn of listeners) fn()
81
+ return this
82
+ }
83
+ }
84
+
85
+ /** @internal The implicit group of every body that never names one. */
86
+ export const defaultGroup = new PhysicsGroup2D(DEFAULT_BIT, "default")
87
+
88
+ /** @internal Register a new group (see `Physics2D.addGroup`). */
89
+ export const _addGroup = (label = ""): PhysicsGroup2D => {
90
+ if (nextBit >= 2 + MAX_USER_GROUPS) {
91
+ throw new Error(`Physics2D.addGroup: out of collision groups (${MAX_USER_GROUPS} max — a category has to cross the bridge as a 32-bit number, and two bits are reserved).`)
92
+ }
93
+ const g = new PhysicsGroup2D((1 << nextBit) >>> 0, label || `group${nextBit - 1}`)
94
+ nextBit++
95
+ all.push(g)
96
+ return g
97
+ }
98
+
99
+ /** @internal Sensors must always be visible to the character's shadow body, solids never. */
100
+ export const sensorMask = (g: PhysicsGroup2D | undefined): number =>
101
+ (((g ?? defaultGroup).mask | CHARACTER_BIT) & ALL_GROUPS) >>> 0
102
+ /** @internal */
103
+ export const solidMask = (g: PhysicsGroup2D | undefined): number =>
104
+ (((g ?? defaultGroup).mask & ~CHARACTER_BIT) & ALL_GROUPS) >>> 0
105
+ /** @internal */
106
+ export const categoryOf = (g: PhysicsGroup2D | undefined): number => (g ?? defaultGroup).category
package/src/g2/loop.ts ADDED
@@ -0,0 +1,50 @@
1
+ // Internal per-frame plumbing for the 2D engine. NOT user-facing — user game logic uses the host
2
+ // `setLoop` global. `onUpdate` taps the engine's render-synced pre-frame hook (_creator2d.onUpdate),
3
+ // for anything that must move in lockstep with the native render (no 1-frame lag).
4
+ //
5
+ // The animation-event registration is lazy (first use) so a 2D project that never animates a sprite
6
+ // pulls none of this in.
7
+
8
+ import { Vec2 } from "../math/vec"
9
+ import { node2dRegistry } from "./Node2D"
10
+
11
+ export const onUpdate = (callback: (dt: number) => void): void => _creator2d.onUpdate(callback)
12
+ export const offUpdate = (callback: (dt: number) => void): void => _creator2d.offUpdate(callback)
13
+
14
+ // Route native animation events (queued in C++, drained once per frame) to the owning node, which
15
+ // translates the native clip id to a clip name and emits 'loopReached' / 'completed'.
16
+ let animEventsRegistered = false
17
+ export const ensureAnimEvents = (): void => {
18
+ if (animEventsRegistered) return
19
+ animEventsRegistered = true
20
+ _creator2d.setOnAnimEvent(({ entityId, clipId, type }) => {
21
+ node2dRegistry.get(entityId)?._dispatchAnimEvent(clipId, type)
22
+ })
23
+ }
24
+
25
+ // Route native physics contact/sensor events to the owning nodes' 'enter'/'exit' events. Each event
26
+ // is delivered to BOTH nodes with the other as the argument. type: 0 contactBegin, 1 contactEnd,
27
+ // 2 sensorBegin, 3 sensorEnd. Registered lazily by the first 'enter'/'exit' listener (Node2D) or the
28
+ // first Trigger2D — it used to hang off Trigger2D alone, so solid-vs-solid contacts silently never
29
+ // fired in a project without a single trigger.
30
+ //
31
+ // A contact BEGIN also carries the manifold, which the listener gets as a second argument: the world
32
+ // point, the surface normal, and the approach speed. The normal points from A to B, so it is FLIPPED
33
+ // for the second delivery — each side gets the normal pointing away from the thing it hit, which is
34
+ // what "did I land on top or hit a wall" needs.
35
+ let physicsEventsRegistered = false
36
+ export const ensurePhysicsEvents = (): void => {
37
+ if (physicsEventsRegistered) return
38
+ physicsEventsRegistered = true
39
+ _creator2d.setOnPhysicsEvent((a, b, type, px, py, nx, ny, speed) => {
40
+ const na = node2dRegistry.get(a)
41
+ const nb = node2dRegistry.get(b)
42
+ if (!na || !nb) return
43
+ const channel = type === 1 || type === 3 ? "exit" : "enter"
44
+ // Box2D's manifold normal points from A TOWARD B. Each listener wants the normal of the surface
45
+ // it hit, pointing back at itself — so A gets it negated and B gets it as-is. Landing on a floor
46
+ // then reads normal.y ≈ +1 for the falling body, which is the test a game actually writes.
47
+ na._emitCollision(channel, nb, { point: new Vec2(px, py), normal: new Vec2(-nx, -ny), speed })
48
+ nb._emitCollision(channel, na, { point: new Vec2(px, py), normal: new Vec2(nx, ny), speed })
49
+ })
50
+ }
@@ -0,0 +1,69 @@
1
+ // No-code 2D scenario aspects (the gl/scenarios.ts precedent): behaviors a scene builder attaches
2
+ // in a `.scene2d.ts` file without writing aspect code. First resident: CameraFollow — the eased,
3
+ // pixel-snapped follow camera every top-down/side-scroller needs (it absorbs the reference app's
4
+ // ~50 lines of snap/follow plumbing).
5
+ //
6
+ // Conventions: update() runs in the LATE phase (after physics, render-synced when the engine
7
+ // provides the hooks), so followers read final, drawn-this-frame positions.
8
+
9
+ import { Aspect } from "../core/Aspect"
10
+ import type { Camera2D } from "./Camera2D"
11
+ import type { Node2D } from "./Node2D"
12
+ import type { Scene2D } from "./Scene2D"
13
+
14
+ // SCREEN pixels per world unit, MEASURED from the native projection (zoom + world→screen mapping
15
+ // are applied natively, so "1 world unit = 1 px" can't be assumed — and worldToScreen is affine,
16
+ // so the per-unit delta is position-independent). Re-measured only when zoom changes.
17
+ type PixelGrid = { sx: number, sy: number, zoomAt: number }
18
+
19
+ const snapToGrid = (cam: Camera2D, grid: PixelGrid, x: number, y: number): [number, number] => {
20
+ if (grid.sx === 0 || cam.zoom !== grid.zoomAt) {
21
+ const o = cam.worldToScreen(0, 0)
22
+ grid.sx = Math.abs(cam.worldToScreen(1, 0).x - o.x) || 1
23
+ grid.sy = Math.abs(cam.worldToScreen(0, 1).y - o.y) || 1
24
+ grid.zoomAt = cam.zoom
25
+ }
26
+ return [ Math.round(x * grid.sx) / grid.sx, Math.round(y * grid.sy) / grid.sy ]
27
+ }
28
+
29
+ /**
30
+ * Ease the scene camera toward this node every frame (attach it to the player). In a `.scene2d.ts`
31
+ * file the loader wires `scene` automatically; from hand-written code pass it yourself:
32
+ * `hero.aspect(CameraFollow, { scene })`.
33
+ *
34
+ * The eased position accumulates at full precision and only the value PUSHED to the camera is
35
+ * snapped — reading the snapped camera back as the ease-from value would let tiny sub-pixel steps
36
+ * round to zero near settle (the follow would stall a few px off-centre, then jump).
37
+ */
38
+ export class CameraFollow extends Aspect<"cameraFollow", Node2D> {
39
+ static readonly aspect = "cameraFollow"
40
+ /** The scene2d loader injects the scene into aspects that declare this. */
41
+ static readonly wantsScene = true
42
+
43
+ /** The scene whose camera follows (wired by the scene file loader). */
44
+ scene: Scene2D | null = null
45
+ /** Exponential time constant in seconds; smaller = snappier, 0 = pin the node to centre. */
46
+ smooth = 0.12
47
+ /** Camera offset from the node, in world units. */
48
+ offset: [number, number] = [ 0, 0 ]
49
+ /** Quantize the camera to whole screen pixels so eased motion never shimmers pixel art.
50
+ * Defaults to the scene's `env.pixelPerfect` (true when unset). */
51
+ pixelSnap = true
52
+
53
+ private _x = NaN
54
+ private _y = NaN
55
+ private readonly _grid: PixelGrid = { sx: 0, sy: 0, zoomAt: NaN }
56
+
57
+ update(dt: number): void {
58
+ const scene = this.scene
59
+ if (!scene) return
60
+ const cam = scene.camera
61
+ const p = this.node.worldPosition
62
+ const tx = p.x + this.offset[0], ty = p.y + this.offset[1]
63
+ if (Number.isNaN(this._x)) { const c = cam.position; this._x = c.x; this._y = c.y } // seed from the camera's pose
64
+ const k = this.smooth > 0 ? 1 - Math.exp(-dt / this.smooth) : 1
65
+ this._x += (tx - this._x) * k
66
+ this._y += (ty - this._y) * k
67
+ cam.position = this.pixelSnap ? snapToGrid(cam, this._grid, this._x, this._y) : [ this._x, this._y ]
68
+ }
69
+ }