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.
- package/README.md +114 -0
- package/dist/global.d.ts +503 -0
- package/dist/host.d.ts +61 -0
- package/dist/inject.js +4730 -0
- package/dist/types/animate/animate.d.ts +20 -0
- package/dist/types/animate/bezier.d.ts +2 -0
- package/dist/types/animate/easings.d.ts +21 -0
- package/dist/types/canvas/Canvas.d.ts +168 -0
- package/dist/types/core/Aspect.d.ts +171 -0
- package/dist/types/core/InspectorUI.d.ts +88 -0
- package/dist/types/core/StateMachine.d.ts +81 -0
- package/dist/types/core/color.d.ts +13 -0
- package/dist/types/core/compWrite.d.ts +11 -0
- package/dist/types/core/events.d.ts +11 -0
- package/dist/types/core/fields.d.ts +58 -0
- package/dist/types/core/registry.d.ts +7 -0
- package/dist/types/core/signals.d.ts +29 -0
- package/dist/types/core/time.d.ts +31 -0
- package/dist/types/g2/Camera2D.d.ts +19 -0
- package/dist/types/g2/CharacterController2D.d.ts +134 -0
- package/dist/types/g2/Node2D.d.ts +91 -0
- package/dist/types/g2/OneWay2D.d.ts +24 -0
- package/dist/types/g2/Physics2D.d.ts +147 -0
- package/dist/types/g2/Scene2D.d.ts +72 -0
- package/dist/types/g2/Shape2D.d.ts +58 -0
- package/dist/types/g2/Sprite.d.ts +51 -0
- package/dist/types/g2/SpriteAnimation.d.ts +50 -0
- package/dist/types/g2/SpriteSheet.d.ts +69 -0
- package/dist/types/g2/Texture2D.d.ts +16 -0
- package/dist/types/g2/Tilemap.d.ts +20 -0
- package/dist/types/g2/Tileset.d.ts +32 -0
- package/dist/types/g2/Trigger2D.d.ts +20 -0
- package/dist/types/g2/autotile.d.ts +133 -0
- package/dist/types/g2/cells.d.ts +14 -0
- package/dist/types/g2/defineScene2d.d.ts +141 -0
- package/dist/types/g2/groups2d.d.ts +16 -0
- package/dist/types/g2/loop.d.ts +4 -0
- package/dist/types/g2/scenarios2d.d.ts +30 -0
- package/dist/types/g2/touch.d.ts +2 -0
- package/dist/types/gl/Camera.d.ts +79 -0
- package/dist/types/gl/CameraPlace.d.ts +26 -0
- package/dist/types/gl/CharacterController.d.ts +98 -0
- package/dist/types/gl/Gearbox.d.ts +86 -0
- package/dist/types/gl/Geometry.d.ts +39 -0
- package/dist/types/gl/IK.d.ts +53 -0
- package/dist/types/gl/InstancedMesh.d.ts +46 -0
- package/dist/types/gl/Light.d.ts +73 -0
- package/dist/types/gl/Lightmap.d.ts +85 -0
- package/dist/types/gl/Locomotion.d.ts +279 -0
- package/dist/types/gl/Material.d.ts +100 -0
- package/dist/types/gl/Mesh.d.ts +33 -0
- package/dist/types/gl/Model.d.ts +53 -0
- package/dist/types/gl/NavAgent.d.ts +99 -0
- package/dist/types/gl/NavMesh.d.ts +169 -0
- package/dist/types/gl/Node.d.ts +99 -0
- package/dist/types/gl/Noise.d.ts +20 -0
- package/dist/types/gl/Particles.d.ts +277 -0
- package/dist/types/gl/Physics.d.ts +91 -0
- package/dist/types/gl/Plane.d.ts +19 -0
- package/dist/types/gl/Ragdoll.d.ts +86 -0
- package/dist/types/gl/Ray.d.ts +8 -0
- package/dist/types/gl/Scene.d.ts +221 -0
- package/dist/types/gl/Shape.d.ts +122 -0
- package/dist/types/gl/Terrain.d.ts +280 -0
- package/dist/types/gl/Texture.d.ts +27 -0
- package/dist/types/gl/Trigger.d.ts +10 -0
- package/dist/types/gl/Vehicle.d.ts +191 -0
- package/dist/types/gl/Wheel.d.ts +95 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
- package/dist/types/gl/animation/Animator.d.ts +219 -0
- package/dist/types/gl/animation/Layer.d.ts +31 -0
- package/dist/types/gl/animation/Loop.d.ts +17 -0
- package/dist/types/gl/animation/Playback.d.ts +36 -0
- package/dist/types/gl/animation/core.d.ts +367 -0
- package/dist/types/gl/controls.d.ts +19 -0
- package/dist/types/gl/physicsEvents.d.ts +1 -0
- package/dist/types/gl/scenarios.d.ts +98 -0
- package/dist/types/gl/state.d.ts +5 -0
- package/dist/types/gl/terrainMesh.d.ts +46 -0
- package/dist/types/gl/touch.d.ts +2 -0
- package/dist/types/inject.d.ts +123 -0
- package/dist/types/math/Mathf.d.ts +39 -0
- package/dist/types/math/mat4.d.ts +66 -0
- package/dist/types/math/quat.d.ts +53 -0
- package/dist/types/math/vec.d.ts +129 -0
- package/dist/types/net/codec.d.ts +25 -0
- package/dist/types/net/core.d.ts +106 -0
- package/dist/types/net/index.d.ts +80 -0
- package/dist/types/net/replication.d.ts +101 -0
- package/dist/types/plugins/camera.d.ts +25 -0
- package/dist/types/plugins/geolocation.d.ts +39 -0
- package/dist/types/plugins/oauth.d.ts +25 -0
- package/dist/types/plugins/permission.d.ts +1 -0
- package/dist/types/plugins/push.d.ts +46 -0
- package/dist/types/plugins/qr.d.ts +21 -0
- package/dist/types/plugins/service.d.ts +33 -0
- package/dist/types/runtime/app.d.ts +67 -0
- package/dist/types/runtime/appEvents.d.ts +7 -0
- package/dist/types/runtime/channel.d.ts +1 -0
- package/dist/types/runtime/clipboard.d.ts +7 -0
- package/dist/types/runtime/datetime.d.ts +80 -0
- package/dist/types/runtime/device.d.ts +126 -0
- package/dist/types/runtime/fetch.d.ts +31 -0
- package/dist/types/runtime/files.d.ts +33 -0
- package/dist/types/runtime/input.d.ts +121 -0
- package/dist/types/runtime/media.d.ts +46 -0
- package/dist/types/runtime/misc.d.ts +11 -0
- package/dist/types/runtime/net.d.ts +15 -0
- package/dist/types/runtime/rpc.d.ts +44 -0
- package/dist/types/runtime/service.d.ts +1 -0
- package/dist/types/runtime/share.d.ts +2 -0
- package/dist/types/runtime/storage.d.ts +5 -0
- package/dist/types/runtime/touch.d.ts +56 -0
- package/dist/types/scene/defineScene.d.ts +296 -0
- package/dist/types/scene/editorPlugins.d.ts +82 -0
- package/dist/types/scene/gizmos.d.ts +56 -0
- package/dist/types/scene/grammar.d.ts +56 -0
- package/dist/types/scene/level.d.ts +98 -0
- package/dist/types/scene/material.d.ts +76 -0
- package/dist/types/server/auth/appConfig.d.ts +16 -0
- package/dist/types/server/auth/global.d.ts +56 -0
- package/dist/types/server/auth/models.d.ts +164 -0
- package/dist/types/server/auth/types.d.ts +56 -0
- package/dist/types/server/channel.d.ts +28 -0
- package/dist/types/server/context.d.ts +27 -0
- package/dist/types/server/db/defineDb.d.ts +36 -0
- package/dist/types/server/db/fields.d.ts +146 -0
- package/dist/types/server/db/index.d.ts +6 -0
- package/dist/types/server/db/marci/query.d.ts +326 -0
- package/dist/types/server/db/types.d.ts +163 -0
- package/dist/types/server/errors.d.ts +8 -0
- package/dist/types/server/inject.d.ts +5 -0
- package/dist/types/ui/NativeView.d.ts +38 -0
- package/dist/types/ui/UI.d.ts +27 -0
- package/dist/types/ui/UIBottomSheet.d.ts +71 -0
- package/dist/types/ui/UIButton.d.ts +30 -0
- package/dist/types/ui/UIContainer.d.ts +33 -0
- package/dist/types/ui/UIImage.d.ts +35 -0
- package/dist/types/ui/UIInput.d.ts +110 -0
- package/dist/types/ui/UIModal.d.ts +70 -0
- package/dist/types/ui/UINode.d.ts +363 -0
- package/dist/types/ui/UIPager.d.ts +123 -0
- package/dist/types/ui/UIPopover.d.ts +39 -0
- package/dist/types/ui/UIScreen.d.ts +57 -0
- package/dist/types/ui/UIScrollable.d.ts +40 -0
- package/dist/types/ui/UISpacer.d.ts +9 -0
- package/dist/types/ui/UITabs.d.ts +96 -0
- package/dist/types/ui/UIText.d.ts +19 -0
- package/dist/types/ui/UIVideo.d.ts +28 -0
- package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
- package/dist/types/ui/UIWidget.d.ts +65 -0
- package/dist/types/ui/fonts.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +68 -0
- package/dist/types/ui/router.d.ts +40 -0
- package/dist/types/ui/theme.d.ts +26 -0
- package/dist/types.json +1 -0
- package/package.json +46 -0
- package/prompts/2d.md +395 -0
- package/prompts/3d-scene-files.md +113 -0
- package/prompts/3d-scene.md +119 -0
- package/prompts/3d.md +303 -0
- package/prompts/README.md +142 -0
- package/prompts/ar.md +109 -0
- package/prompts/canvas.md +27 -0
- package/prompts/compose.ts +46 -0
- package/prompts/concept.md +49 -0
- package/prompts/core-design.md +75 -0
- package/prompts/core.md +264 -0
- package/prompts/design.md +218 -0
- package/prompts/dist/2d-game.md +1530 -0
- package/prompts/dist/3d-app.md +1672 -0
- package/prompts/dist/ar-app.md +1548 -0
- package/prompts/dist/concept.md +49 -0
- package/prompts/dist/design.md +498 -0
- package/prompts/dist/ui-app.md +1134 -0
- package/prompts/index.md +37 -0
- package/prompts/intake-prompt.md +33 -0
- package/prompts/namer-prompt.md +12 -0
- package/prompts/router-prompt.md +50 -0
- package/prompts/select.ts +111 -0
- package/prompts/ui-design.md +203 -0
- package/prompts/ui.md +803 -0
- package/src/animate/animate.ts +238 -0
- package/src/animate/bezier.ts +138 -0
- package/src/animate/easings.ts +126 -0
- package/src/bridges.d.ts +1345 -0
- package/src/canvas/Canvas.ts +305 -0
- package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
- package/src/compile/__tests__/assetMacro.test.ts +74 -0
- package/src/compile/__tests__/assetName.test.ts +55 -0
- package/src/compile/__tests__/compile.test.ts +287 -0
- package/src/compile/__tests__/detectEntry.test.ts +132 -0
- package/src/compile/__tests__/fontMacro.test.ts +199 -0
- package/src/compile/aspectMacro.ts +86 -0
- package/src/compile/assetIconMacro.ts +384 -0
- package/src/compile/assetMacro.ts +146 -0
- package/src/compile/assetName.ts +50 -0
- package/src/compile/bundler.ts +287 -0
- package/src/compile/compileProject.ts +127 -0
- package/src/compile/detectEntry.ts +128 -0
- package/src/compile/fontMacro.ts +459 -0
- package/src/compile/fontRegistry.ts +78 -0
- package/src/compile/header.ts +67 -0
- package/src/compile/index.ts +100 -0
- package/src/compile/libraryImports.ts +52 -0
- package/src/compile/liteMaterial.ts +247 -0
- package/src/compile/sceneEditor.ts +88 -0
- package/src/compile/serverSplit.ts +233 -0
- package/src/compile/serverTypes.ts +227 -0
- package/src/compile/sfnt.ts +98 -0
- package/src/compile/shaderSchema.ts +202 -0
- package/src/compile/shaderTargets.ts +77 -0
- package/src/compile/sourcemap.ts +25 -0
- package/src/core/Aspect.ts +568 -0
- package/src/core/InspectorUI.ts +212 -0
- package/src/core/StateMachine.ts +308 -0
- package/src/core/__tests__/stateMachine.test.ts +132 -0
- package/src/core/color.ts +66 -0
- package/src/core/compWrite.ts +42 -0
- package/src/core/events.ts +38 -0
- package/src/core/fields.ts +120 -0
- package/src/core/registry.ts +23 -0
- package/src/core/signals.ts +277 -0
- package/src/core/time.ts +84 -0
- package/src/g2/Camera2D.ts +40 -0
- package/src/g2/CharacterController2D.ts +276 -0
- package/src/g2/Node2D.ts +267 -0
- package/src/g2/OneWay2D.ts +66 -0
- package/src/g2/Physics2D.ts +346 -0
- package/src/g2/Scene2D.ts +209 -0
- package/src/g2/Shape2D.ts +259 -0
- package/src/g2/Sprite.ts +89 -0
- package/src/g2/SpriteAnimation.ts +171 -0
- package/src/g2/SpriteSheet.ts +166 -0
- package/src/g2/Texture2D.ts +47 -0
- package/src/g2/Tilemap.ts +41 -0
- package/src/g2/Tileset.ts +71 -0
- package/src/g2/Trigger2D.ts +77 -0
- package/src/g2/autotile.ts +433 -0
- package/src/g2/cells.ts +91 -0
- package/src/g2/defineScene2d.ts +381 -0
- package/src/g2/groups2d.ts +106 -0
- package/src/g2/loop.ts +50 -0
- package/src/g2/scenarios2d.ts +69 -0
- package/src/g2/touch.ts +83 -0
- package/src/gl/Camera.ts +160 -0
- package/src/gl/CameraPlace.ts +52 -0
- package/src/gl/CharacterController.ts +238 -0
- package/src/gl/Gearbox.ts +212 -0
- package/src/gl/Geometry.ts +279 -0
- package/src/gl/IK.ts +193 -0
- package/src/gl/InstancedMesh.ts +132 -0
- package/src/gl/Light.ts +135 -0
- package/src/gl/Lightmap.ts +402 -0
- package/src/gl/Locomotion.ts +498 -0
- package/src/gl/Material.ts +279 -0
- package/src/gl/Mesh.ts +83 -0
- package/src/gl/Model.ts +124 -0
- package/src/gl/NavAgent.ts +337 -0
- package/src/gl/NavMesh.ts +397 -0
- package/src/gl/Node.ts +350 -0
- package/src/gl/Noise.ts +30 -0
- package/src/gl/Particles.ts +676 -0
- package/src/gl/Physics.ts +222 -0
- package/src/gl/Plane.ts +53 -0
- package/src/gl/Ragdoll.ts +270 -0
- package/src/gl/Ray.ts +16 -0
- package/src/gl/Scene.ts +505 -0
- package/src/gl/Shape.ts +409 -0
- package/src/gl/Terrain.ts +1092 -0
- package/src/gl/Texture.ts +63 -0
- package/src/gl/Trigger.ts +45 -0
- package/src/gl/Vehicle.ts +473 -0
- package/src/gl/Wheel.ts +240 -0
- package/src/gl/animation/AnimationClip.ts +204 -0
- package/src/gl/animation/Animator.ts +329 -0
- package/src/gl/animation/Layer.ts +39 -0
- package/src/gl/animation/Loop.ts +31 -0
- package/src/gl/animation/Playback.ts +52 -0
- package/src/gl/animation/core.ts +670 -0
- package/src/gl/controls.ts +95 -0
- package/src/gl/physicsEvents.ts +20 -0
- package/src/gl/scenarios.ts +291 -0
- package/src/gl/state.ts +6 -0
- package/src/gl/terrainMesh.ts +219 -0
- package/src/gl/touch.ts +68 -0
- package/src/host.d.ts +61 -0
- package/src/inject.ts +203 -0
- package/src/math/Mathf.ts +118 -0
- package/src/math/mat4.ts +278 -0
- package/src/math/quat.ts +232 -0
- package/src/math/vec.ts +255 -0
- package/src/net/codec.ts +119 -0
- package/src/net/core.ts +384 -0
- package/src/net/index.ts +181 -0
- package/src/net/replication.ts +622 -0
- package/src/plugins/camera.ts +81 -0
- package/src/plugins/geolocation.ts +123 -0
- package/src/plugins/oauth.ts +61 -0
- package/src/plugins/permission.ts +7 -0
- package/src/plugins/push.ts +132 -0
- package/src/plugins/qr.ts +73 -0
- package/src/plugins/service.ts +47 -0
- package/src/runtime/app.ts +101 -0
- package/src/runtime/appEvents.ts +54 -0
- package/src/runtime/channel.ts +61 -0
- package/src/runtime/clipboard.ts +20 -0
- package/src/runtime/datetime.ts +329 -0
- package/src/runtime/device.ts +293 -0
- package/src/runtime/fetch.ts +77 -0
- package/src/runtime/files.ts +108 -0
- package/src/runtime/input.ts +175 -0
- package/src/runtime/media.ts +111 -0
- package/src/runtime/misc.ts +16 -0
- package/src/runtime/net.ts +36 -0
- package/src/runtime/rpc.ts +218 -0
- package/src/runtime/service.ts +83 -0
- package/src/runtime/share.ts +9 -0
- package/src/runtime/storage.ts +13 -0
- package/src/runtime/touch.ts +76 -0
- package/src/scene/defineScene.ts +1396 -0
- package/src/scene/editorPlugins.ts +110 -0
- package/src/scene/gizmos.ts +148 -0
- package/src/scene/grammar.ts +120 -0
- package/src/scene/level.ts +296 -0
- package/src/scene/material.ts +188 -0
- package/src/server/auth/appConfig.ts +12 -0
- package/src/server/auth/global.ts +80 -0
- package/src/server/auth/host.ts +318 -0
- package/src/server/auth/models.ts +83 -0
- package/src/server/auth/types.ts +50 -0
- package/src/server/channel.ts +56 -0
- package/src/server/context.ts +36 -0
- package/src/server/db/defineDb.ts +237 -0
- package/src/server/db/fields.ts +132 -0
- package/src/server/db/httpTransport.ts +93 -0
- package/src/server/db/index.ts +7 -0
- package/src/server/db/marci/query.ts +412 -0
- package/src/server/db/types.ts +202 -0
- package/src/server/errors.ts +12 -0
- package/src/server/host.ts +74 -0
- package/src/server/inject.ts +13 -0
- package/src/server/runtime.ts +133 -0
- package/src/server/validate.ts +87 -0
- package/src/ui/NativeView.ts +142 -0
- package/src/ui/UI.ts +39 -0
- package/src/ui/UIBottomSheet.ts +139 -0
- package/src/ui/UIButton.ts +101 -0
- package/src/ui/UIContainer.ts +60 -0
- package/src/ui/UIImage.ts +83 -0
- package/src/ui/UIInput.ts +185 -0
- package/src/ui/UIModal.ts +139 -0
- package/src/ui/UINode.ts +830 -0
- package/src/ui/UIPager.ts +362 -0
- package/src/ui/UIPopover.ts +100 -0
- package/src/ui/UIScreen.ts +123 -0
- package/src/ui/UIScrollable.ts +87 -0
- package/src/ui/UISpacer.ts +14 -0
- package/src/ui/UITabs.ts +236 -0
- package/src/ui/UIText.ts +51 -0
- package/src/ui/UIVideo.ts +88 -0
- package/src/ui/UIVirtualizedList.ts +241 -0
- package/src/ui/UIWidget.ts +127 -0
- package/src/ui/fonts.ts +13 -0
- package/src/ui/presentable.ts +117 -0
- package/src/ui/router.ts +132 -0
- 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>
|