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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Orthographic camera for a Scene2D. Position / zoom / rotation live natively. To make the camera
|
|
2
|
+
// track a node, attach a small update(dt) aspect that eases camera.position toward the node — there's
|
|
3
|
+
// no built-in follow() (and so nothing to dispose).
|
|
4
|
+
|
|
5
|
+
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
6
|
+
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
7
|
+
|
|
8
|
+
export class Camera2D implements CompWriter {
|
|
9
|
+
private _x = 0
|
|
10
|
+
private _y = 0
|
|
11
|
+
private _zoom = 1
|
|
12
|
+
private _rotation = 0
|
|
13
|
+
|
|
14
|
+
private readonly sceneId: number
|
|
15
|
+
constructor(sceneId: number) { this.sceneId = sceneId }
|
|
16
|
+
|
|
17
|
+
get position(): Vec2 { return new Vec2(this._x, this._y) }
|
|
18
|
+
set position(v: Vec2Like) { this._x = cx(v); this._y = cy(v); _creator2d.cameraSetPosition(this.sceneId, this._x, this._y) }
|
|
19
|
+
/** @internal chisel `comp_write` (see core/compWrite.ts): `camera.position.<axis> = v` compiles to a call here. */
|
|
20
|
+
static _comps = [ "position" ]
|
|
21
|
+
_writeComp(_prop: string, axis: CompAxis, v: number): void {
|
|
22
|
+
if (axis === "x") this.position = [ v, this._y ]
|
|
23
|
+
else if (axis === "y") this.position = [ this._x, v ]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get zoom(): number { return this._zoom }
|
|
27
|
+
set zoom(z: number) { this._zoom = z; _creator2d.cameraSetZoom(this.sceneId, z) }
|
|
28
|
+
|
|
29
|
+
get rotation(): number { return this._rotation }
|
|
30
|
+
set rotation(deg: number) { this._rotation = deg; _creator2d.cameraSetRotation(this.sceneId, deg) }
|
|
31
|
+
|
|
32
|
+
screenToWorld(screenX: number, screenY: number): Vec2 {
|
|
33
|
+
const o = _creator2d.cameraScreenToWorld(this.sceneId, screenX, screenY)
|
|
34
|
+
return new Vec2(o[0], o[1])
|
|
35
|
+
}
|
|
36
|
+
worldToScreen(worldX: number, worldY: number): Vec2 {
|
|
37
|
+
const o = _creator2d.cameraWorldToScreen(this.sceneId, worldX, worldY)
|
|
38
|
+
return new Vec2(o[0], o[1])
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// A character controller, as an aspect on a Node2D, backed by Box2D's kinematic MOVER — collide and
|
|
2
|
+
// slide, not a rigid body. Requires a capsule Shape2D (one is derived from the sprite if absent) and
|
|
3
|
+
// does NOT use Physics2D.
|
|
4
|
+
//
|
|
5
|
+
// const hero = new Sprite({ texture, anchor: [0.5, 1] })
|
|
6
|
+
// .aspect(Shape2D, { capsule: { from: [0, 8], to: [0, 26], radius: 8 } })
|
|
7
|
+
// .aspect(CharacterController2D)
|
|
8
|
+
//
|
|
9
|
+
// setLoop(() => hero.controller.move(((Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0)) * SPEED))
|
|
10
|
+
// Input.on('keydown', e => { if (e.code === 'Space' && hero.controller.grounded) hero.controller.velocityY = 700 })
|
|
11
|
+
//
|
|
12
|
+
// The two halves of the velocity have different NATURES, so they have different channels:
|
|
13
|
+
//
|
|
14
|
+
// • HORIZONTAL — `move(x)`, world units/s, a PER-FRAME COMMAND. A walking character's horizontal
|
|
15
|
+
// velocity is muscle-driven and not conserved: stop pushing, stop moving. So the command expires
|
|
16
|
+
// each frame — no call this frame means standing still, and releasing the keys needs no explicit
|
|
17
|
+
// zero. It is latched for the whole frame, so every physics sub-step of that frame sees it.
|
|
18
|
+
// • VERTICAL — `velocityY`, LATCHED ballistic state. Gravity integrates into it in the engine; you
|
|
19
|
+
// seed it for a jump / bounce pad / explosion. There is deliberately no ground check: coyote
|
|
20
|
+
// time, double jumps and wall jumps all need the caller's own condition.
|
|
21
|
+
//
|
|
22
|
+
// FREE MODE — `gravityScale = 0` turns the character into a swimmer / flyer / ladder climber / the
|
|
23
|
+
// hero of a TOP-DOWN game: no gravity, no ground, no stick-to-floor. There the vertical is
|
|
24
|
+
// muscle-driven too, so `move()` takes BOTH components and the whole velocity becomes a per-frame
|
|
25
|
+
// command. Top-down vs platformer is this one number, not two controllers.
|
|
26
|
+
//
|
|
27
|
+
// `velocity` reads back what the solver ENDED UP with after the last step (post-collision) — walk into
|
|
28
|
+
// a wall and it reads ~0 even though you commanded 300. Everything runs on the engine's fixed clock,
|
|
29
|
+
// so the aspect has no per-frame update: a character costs zero JS work per frame.
|
|
30
|
+
//
|
|
31
|
+
// ROTATION AND SCALE stay the node's: the engine writes only the position (a capsule is symmetric
|
|
32
|
+
// about its axis, so its angle is physically meaningless).
|
|
33
|
+
//
|
|
34
|
+
// The character IS visible to raycasts, overlaps and triggers — it carries a hidden kinematic body for
|
|
35
|
+
// exactly that, so pickups and hazard zones need no special code. Solid contacts do not arrive as
|
|
36
|
+
// 'enter' events (the mover is not a body); read `collisions` for those.
|
|
37
|
+
|
|
38
|
+
import { Aspect } from "../core/Aspect"
|
|
39
|
+
import type { FieldMeta } from "../core/fields"
|
|
40
|
+
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
41
|
+
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
42
|
+
import { PhysicsGroup2D, _watchGroupChanges, _unwatchGroupChanges, categoryOf, solidMask } from "./groups2d"
|
|
43
|
+
import { Shape2D } from "./Shape2D"
|
|
44
|
+
import { node2dRegistry, type Node2D } from "./Node2D"
|
|
45
|
+
|
|
46
|
+
/** Where the character's feet are, as reported by the solver after the last step. */
|
|
47
|
+
export type GroundState2D = "ground" | "slope" | "air"
|
|
48
|
+
const GROUND_STATES: readonly GroundState2D[] = [ "ground", "slope", "air" ]
|
|
49
|
+
|
|
50
|
+
/** One surface the mover pushed out of this step. `normal` points back at the character. */
|
|
51
|
+
export type Collision2D = { node: Node2D | null, normal: Vec2 }
|
|
52
|
+
|
|
53
|
+
const MAX_COLLISIONS = 8
|
|
54
|
+
const state = new Float32Array(8)
|
|
55
|
+
let warnedFreeMove = false
|
|
56
|
+
|
|
57
|
+
export class CharacterController2D extends Aspect<"controller", Node2D> implements CompWriter {
|
|
58
|
+
static readonly aspect = "controller"
|
|
59
|
+
|
|
60
|
+
// Accessor-backed tunables are invisible to describeFields (it enumerates own enumerable fields),
|
|
61
|
+
// so the inspector is told about them explicitly. Defaults still come from the getters.
|
|
62
|
+
static fields: FieldMeta<CharacterController2D> = {
|
|
63
|
+
gravityScale: { editor: "number", min: 0, step: 0.1, label: "Gravity scale" },
|
|
64
|
+
maxSlope: { editor: "number", min: 0, max: 89, step: 1, label: "Max slope°" },
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private _id = 0
|
|
68
|
+
private _gravityScale = 1
|
|
69
|
+
private _maxSlope = 45
|
|
70
|
+
private _group: PhysicsGroup2D | undefined
|
|
71
|
+
private _dropThrough: Node2D | null = null
|
|
72
|
+
private _repush = (): void => {
|
|
73
|
+
if (this._id) _creator2d.characterSetFilter(this._id, categoryOf(this._group), solidMask(this._group))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onAttach(): void {
|
|
77
|
+
if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
|
|
78
|
+
if (!this.node.has(Shape2D)) this.node.aspect(Shape2D, {})
|
|
79
|
+
const shape = this.node.get(Shape2D)!
|
|
80
|
+
const cap = shape._capsule()
|
|
81
|
+
// Read the options through their PUBLIC accessors — chisel drops an accessor pair whose name is
|
|
82
|
+
// only ever an object-literal key, and the option would silently do nothing (see Physics2D).
|
|
83
|
+
this._id = _creator2d.characterCreate(this.node.id, cap.x1, cap.y1, cap.x2, cap.y2, cap.radius,
|
|
84
|
+
this.maxSlope, categoryOf(this.group), solidMask(this.group))
|
|
85
|
+
if (!this._id) return
|
|
86
|
+
shape._claimedByCharacter()
|
|
87
|
+
_creator2d.characterSetGravityScale(this._id, this.gravityScale)
|
|
88
|
+
this.node._xf = this._id // routes node.position writes to the character
|
|
89
|
+
this.node._xfKind = 1
|
|
90
|
+
// The engine moves this node every step, so its JS transform cache must re-sync on READ — without
|
|
91
|
+
// this the character simulates correctly and node.position never changes.
|
|
92
|
+
this.node._physicsDriven = true
|
|
93
|
+
if (this._group) _watchGroupChanges(this._repush)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onDetach(): void {
|
|
97
|
+
_unwatchGroupChanges(this._repush)
|
|
98
|
+
if (this._id) { _creator2d.characterDestroy(this._id); this._id = 0 }
|
|
99
|
+
this.node._xf = 0
|
|
100
|
+
this.node._xfKind = 0
|
|
101
|
+
this.node._physicsDriven = false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Native character id (0 until attached / no physics support). */
|
|
105
|
+
get id(): number { return this._id }
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This frame's movement command, in world units/s — NOT normalized, NOT a per-frame displacement
|
|
109
|
+
* (that is what Unity's `Move` takes; passing `v * dt` here gives a character 60× too slow).
|
|
110
|
+
*
|
|
111
|
+
* ONE component = horizontal, the everyday platformer call. TWO = the whole velocity, for free mode
|
|
112
|
+
* (`gravityScale = 0`); with gravity on, a two-component call fights the ballistic vertical and the
|
|
113
|
+
* character hangs in the air, so that combination warns once.
|
|
114
|
+
*
|
|
115
|
+
* Sticky only within the frame: the command expires once the engine consumes it. It also **takes the
|
|
116
|
+
* axis back from a latched `velocity`** — commanding is claiming ownership, which is what keeps the
|
|
117
|
+
* two horizontal sources from ever fighting.
|
|
118
|
+
*/
|
|
119
|
+
move(x: number): void
|
|
120
|
+
move(x: number, y: number): void
|
|
121
|
+
move(v: Vec2Like): void
|
|
122
|
+
move(x: number | Vec2Like, y?: number): void {
|
|
123
|
+
if (!this._id) return
|
|
124
|
+
if (typeof x === "number") {
|
|
125
|
+
if (y === undefined) { _creator2d.characterMove(this._id, x); return }
|
|
126
|
+
this._warnFree()
|
|
127
|
+
_creator2d.characterMoveFree(this._id, x, y)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
this._warnFree()
|
|
131
|
+
_creator2d.characterMoveFree(this._id, cx(x), cy(x))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private _warnFree(): void {
|
|
135
|
+
if (this._gravityScale === 0 || warnedFreeMove) return
|
|
136
|
+
warnedFreeMove = true
|
|
137
|
+
console.warn("CharacterController2D.move(): the two-component form is the FREE-mode command (top-down / swimming / flying) and expects gravityScale = 0. With gravity on it overwrites the falling speed every frame, so the character will hang in the air. Use move(x) and velocityY for a platformer.")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Vertical velocity (world units/s) — LATCHED: gravity works on it, you seed it. `= 700` to jump,
|
|
141
|
+
* `+= 300` to stack an explosion on top of the current motion. No ground check: guard it yourself
|
|
142
|
+
* with `grounded` (or don't, for a double jump). Does nothing in free mode. `velocity.y = 700` is
|
|
143
|
+
* the same channel — pick whichever reads better. */
|
|
144
|
+
get velocityY(): number { return this._state()[1] }
|
|
145
|
+
set velocityY(v: number) { if (this._id) _creator2d.characterSetVerticalVelocity(this._id, v) }
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* READ — the velocity the solver ended up with after the most recent step (world units/s, fresh
|
|
149
|
+
* Vec2): what HAPPENED, not what you asked for. Walking into a wall reads ~0, sliding along one
|
|
150
|
+
* reads the tangent.
|
|
151
|
+
*
|
|
152
|
+
* WRITE — LATCH the whole velocity: a knockback, a wall jump, a launch pad. Unlike `move()` it does
|
|
153
|
+
* not expire, so the character keeps flying, and gravity still pulls the vertical down into a real
|
|
154
|
+
* ballistic arc. It stays until `move()` takes the axis back — so a game simply doesn't call
|
|
155
|
+
* `move()` while the throw lasts, and ends it on its own terms:
|
|
156
|
+
*
|
|
157
|
+
* hero.controller.velocity = [dir.x * 600, 400] // hit by the blast
|
|
158
|
+
* if (thrown) { if (hero.controller.grounded) thrown = false } // landing ends it
|
|
159
|
+
* else hero.controller.move(ix * SPEED) // …and this reclaims the axis
|
|
160
|
+
*
|
|
161
|
+
* Nothing clears the latch by itself, landing included — a kinematic controller has no friction.
|
|
162
|
+
* Reading is not the inverse of writing: against a wall the read is ~0 and would cancel the throw.
|
|
163
|
+
*
|
|
164
|
+
* `c.velocity.y = 700` (the direct spelling) is a jump — the compiler routes it to the exact
|
|
165
|
+
* `velocityY` channel via `_writeComp` below — and `c.velocity.x = 300` latches the whole vector
|
|
166
|
+
* with the measured vertical filling in. A STORED copy is still a copy (`const v = c.velocity`).
|
|
167
|
+
*/
|
|
168
|
+
get velocity(): Vec2 { const s = this._state(); return new Vec2(s[0], s[1]) }
|
|
169
|
+
set velocity(v: Vec2Like) { if (this._id) _creator2d.characterSetVelocity(this._id, cx(v), cy(v)) }
|
|
170
|
+
|
|
171
|
+
/** Compile-time list (chisel reads it, then strips it) — see CharacterController / core/compWrite.ts. */
|
|
172
|
+
static _comps = [ "velocity" ]
|
|
173
|
+
|
|
174
|
+
/** @internal `c.velocity.<axis> = v` compiles to this; see CharacterController. */
|
|
175
|
+
_writeComp(_prop: string, axis: CompAxis, v: number): void {
|
|
176
|
+
if (!this._id) return
|
|
177
|
+
if (axis === "y") { _creator2d.characterSetVerticalVelocity(this._id, v); return }
|
|
178
|
+
const s = this._state()
|
|
179
|
+
_creator2d.characterSetVelocity(this._id, axis === "x" ? v : s[0], s[1])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** True while standing on walkable ground. */
|
|
183
|
+
get grounded(): boolean { return this._state()[2] === 0 }
|
|
184
|
+
|
|
185
|
+
/** Where the feet are after the last step: walkable ground, too-steep ground, or the air. */
|
|
186
|
+
get groundState(): GroundState2D { return GROUND_STATES[this._state()[2]] ?? "air" }
|
|
187
|
+
|
|
188
|
+
/** The surface normal under the feet — for orienting a sprite to a slope, or deciding a slide. */
|
|
189
|
+
get groundNormal(): Vec2 { const s = this._state(); return new Vec2(s[3], s[4]) }
|
|
190
|
+
|
|
191
|
+
/** What the character is standing on: a moving platform, a hazard, an ice patch whose `friction`
|
|
192
|
+
* the game can read. The engine already carries the character along a kinematic platform. */
|
|
193
|
+
get groundNode(): Node2D | null {
|
|
194
|
+
const id = this._state()[5]
|
|
195
|
+
return id ? node2dRegistry.get(id) ?? null : null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Every surface the mover pushed out of during the last step, with the normal pointing back at the
|
|
200
|
+
* character. This is what wall jumps and box pushing are written against — a kinematic mover is not
|
|
201
|
+
* a body, so those contacts do not arrive as 'enter' events.
|
|
202
|
+
*
|
|
203
|
+
* const wall = c.collisions.find(h => Math.abs(h.normal.x) > 0.7)
|
|
204
|
+
* if (wall && jumpPressed) c.velocity = [-wall.normal.x * KICK, JUMP]
|
|
205
|
+
*
|
|
206
|
+
* for (const h of c.collisions) h.node?.physics?.applyImpulse([-h.normal.x * PUSH, 0])
|
|
207
|
+
*/
|
|
208
|
+
get collisions(): Collision2D[] {
|
|
209
|
+
if (!this._id) return []
|
|
210
|
+
const o = _creator2d.characterGetCollisions(this._id, MAX_COLLISIONS)
|
|
211
|
+
const out: Collision2D[] = []
|
|
212
|
+
for (let i = 0; i + 2 < o.length; i += 3) {
|
|
213
|
+
out.push({ node: o[i] ? node2dRegistry.get(o[i]) ?? null : null, normal: new Vec2(o[i + 1], o[i + 2]) })
|
|
214
|
+
}
|
|
215
|
+
return out
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Multiplier over the world gravity; default 1. **0 = free mode**: no gravity, no ground, no
|
|
219
|
+
* stick-to-floor — a swimmer, a drone, a ladder climber, or a top-down hero, driven by the
|
|
220
|
+
* two-component `move()`. */
|
|
221
|
+
get gravityScale(): number { return this._gravityScale }
|
|
222
|
+
set gravityScale(v: number) {
|
|
223
|
+
this._gravityScale = v
|
|
224
|
+
if (this._id) _creator2d.characterSetGravityScale(this._id, v)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Max ground slope (degrees) the character treats as walkable; default 45. Live. */
|
|
228
|
+
get maxSlope(): number { return this._maxSlope }
|
|
229
|
+
set maxSlope(v: number) {
|
|
230
|
+
this._maxSlope = v
|
|
231
|
+
if (this._id) _creator2d.characterSetMaxSlope(this._id, v)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Which collision group the character belongs to. Live. */
|
|
235
|
+
get group(): PhysicsGroup2D | undefined { return this._group }
|
|
236
|
+
set group(g: PhysicsGroup2D | undefined) { this._group = g; this._repush() }
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* While this names a node, that node's `OneWay2D` surfaces are not solid for this character — how
|
|
240
|
+
* you drop off a semisolid platform. A plain latch: whoever sets it clears it.
|
|
241
|
+
*
|
|
242
|
+
* if (downPressed && jumpPressed && c.grounded) {
|
|
243
|
+
* const platform = c.groundNode
|
|
244
|
+
* c.dropThrough = platform
|
|
245
|
+
* setTimeout(() => { if (c.dropThrough === platform) c.dropThrough = null }, 200)
|
|
246
|
+
* }
|
|
247
|
+
*
|
|
248
|
+
* Because it names ONE node, the timeout is not delicate: too long merely means you could have
|
|
249
|
+
* re-landed on that platform for a moment, and the platform below stays solid either way.
|
|
250
|
+
*/
|
|
251
|
+
get dropThrough(): Node2D | null { return this._dropThrough }
|
|
252
|
+
set dropThrough(n: Node2D | null) {
|
|
253
|
+
this._dropThrough = n
|
|
254
|
+
if (this._id) _creator2d.characterSetDropThrough(this._id, n ? n.id : 0)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* True while a requested collider resize hasn't taken — you asked to stand up and there is something
|
|
259
|
+
* overhead. Resizing goes through the `Shape2D` aspect itself:
|
|
260
|
+
*
|
|
261
|
+
* hero.aspect(Shape2D, { capsule: CROUCHED }) // always fits — you are shrinking
|
|
262
|
+
* hero.aspect(Shape2D, { capsule: STANDING }) // may be refused under a low ceiling
|
|
263
|
+
* if (hero.controller.resizing) … // still crouched; call it again next frame
|
|
264
|
+
*
|
|
265
|
+
* A refusal changes nothing, so the retry is just the same call again — and it is an exact headroom
|
|
266
|
+
* test against the real capsule, unlike a hand-rolled raycast (a ray is a line; a capsule has
|
|
267
|
+
* girth). The FEET stay planted across a resize, so the character neither hovers nor sinks.
|
|
268
|
+
*/
|
|
269
|
+
get resizing(): boolean { return this.node.get(Shape2D)?._resizeRefused ?? false }
|
|
270
|
+
|
|
271
|
+
private _state(): Float32Array {
|
|
272
|
+
if (!this._id) { state.fill(0); state[2] = 2; state[4] = 1; return state }
|
|
273
|
+
_creator2d.characterGetState(this._id, state)
|
|
274
|
+
return state
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/g2/Node2D.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Base 2D scene node — an empty transform. Sprite and Tilemap extend it, so every node operation
|
|
2
|
+
// (position, layer, visibility, events) works on them. Native (creator-2d) owns the authoritative
|
|
3
|
+
// transform. JS caches it for free reads (JS is normally the sole writer); the exception is a node
|
|
4
|
+
// driven by a dynamic/kinematic Physics2D body — there the native physics step also writes the
|
|
5
|
+
// transform, so such nodes are flagged `_physicsDriven` and their transform getters sync the cache
|
|
6
|
+
// from native first (lazy: only on read, only for moving bodies). So node.position stays correct
|
|
7
|
+
// under physics without any per-frame bookkeeping.
|
|
8
|
+
//
|
|
9
|
+
// Empty Node2Ds are useful as: grouping parents, world-space markers/spawn points, and physics /
|
|
10
|
+
// trigger objects — attach a Shape2D (geometry) plus a Physics2D (rigid body) or Trigger2D (sensor).
|
|
11
|
+
|
|
12
|
+
import { AspectHost } from "../core/Aspect"
|
|
13
|
+
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
14
|
+
import { Registry } from "../core/registry"
|
|
15
|
+
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
16
|
+
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
17
|
+
import { ensurePointerEvents } from "./touch"
|
|
18
|
+
import { ensurePhysicsEvents } from "./loop"
|
|
19
|
+
|
|
20
|
+
// re-exported so existing `import { Vec2 } from "./Node2D"` sites keep working; the canonical home is math/.
|
|
21
|
+
export { Vec2, type Vec2Like }
|
|
22
|
+
|
|
23
|
+
export type Node2DEvents = {
|
|
24
|
+
/** A looping animation clip wrapped around (fires each loop). */
|
|
25
|
+
loopReached: (clip: string) => void
|
|
26
|
+
/** A non-looping animation clip finished. */
|
|
27
|
+
completed: (clip: string) => void
|
|
28
|
+
/** A physics contact / sensor overlap began (the other node, plus where and how hard). The
|
|
29
|
+
* contact's `normal` points AWAY from the other node, so `normal.y > 0.7` reads as "I landed on
|
|
30
|
+
* top of it". Sensor overlaps and `exit` carry zeros. */
|
|
31
|
+
enter: (other: Node2D, contact: Contact2D) => void
|
|
32
|
+
exit: (other: Node2D, contact: Contact2D) => void
|
|
33
|
+
/** Pointer up over this node's Physics2D shape (a tap/click). */
|
|
34
|
+
click: (ev: ClickEvent<Node2D | null>) => void
|
|
35
|
+
/** Pointer down on this node's Physics2D shape. Call ev.track(...) to capture the drag. */
|
|
36
|
+
touchstart: (ev: TouchStartEvent<Node2D | null>) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Where a contact happened, delivered as the second argument of 'enter'. `speed` is the approach
|
|
40
|
+
* speed at impact (world units/s) — scale an impact sound with it. Zeros on sensor and 'exit'
|
|
41
|
+
* events, which have no manifold. */
|
|
42
|
+
export type Contact2D = { point: Vec2, normal: Vec2, speed: number }
|
|
43
|
+
|
|
44
|
+
/** id → Node2D, so host callbacks (animation events) route back to the owning JS object. */
|
|
45
|
+
export const node2dRegistry = new Registry<Node2D>()
|
|
46
|
+
|
|
47
|
+
export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
|
|
48
|
+
/** Native entity handle. */
|
|
49
|
+
readonly id: number
|
|
50
|
+
|
|
51
|
+
/** Optional debug/editor name (plain JS data — scene2d files stamp their node names here). */
|
|
52
|
+
name?: string
|
|
53
|
+
|
|
54
|
+
protected _x = 0
|
|
55
|
+
protected _y = 0
|
|
56
|
+
protected _rotation = 0
|
|
57
|
+
protected _sx = 1
|
|
58
|
+
protected _sy = 1
|
|
59
|
+
protected _layer = 0
|
|
60
|
+
protected _z = 0
|
|
61
|
+
protected _visible = true
|
|
62
|
+
|
|
63
|
+
/** @internal — set by Physics2D for a dynamic/kinematic body: the native step owns this node's
|
|
64
|
+
* transform, so the transform getters re-sync the cache from native before returning. */
|
|
65
|
+
_physicsDriven = false
|
|
66
|
+
|
|
67
|
+
/** @internal The engine object that mirrors this node's transform, if any — a physics body or a
|
|
68
|
+
* character (0 = none). WRITES to position / x / y / rotation are routed to it as well, because
|
|
69
|
+
* the engine is the authority: on a moving body a plain setPosition is overwritten by the next
|
|
70
|
+
* sync, and on a static / sensor body it would leave the COLLIDER behind while the node moved.
|
|
71
|
+
* Set by Physics2D, Trigger2D and CharacterController2D. This is what replaced `physics.moveTo`
|
|
72
|
+
* and `trigger.moveTo`. */
|
|
73
|
+
_xf = 0
|
|
74
|
+
/** @internal What `_xf` is: 1 = character, 2 = physics body. */
|
|
75
|
+
_xfKind = 0
|
|
76
|
+
|
|
77
|
+
// Push this node's (already written) transform into the engine object that owns it. The world math
|
|
78
|
+
// lives NATIVE — physicsSetFromEntity reads the entity's world matrix — so a parented body converts
|
|
79
|
+
// through its parent chain without this file re-deriving it.
|
|
80
|
+
//
|
|
81
|
+
// NOTE: this fires on a DIRECT write only. A body parented to a node that moves does not follow;
|
|
82
|
+
// keep bodies and sensors at the root and write their position (see docs/2d-physics-plan.md §6).
|
|
83
|
+
private _pushXf(): void {
|
|
84
|
+
if (this._xfKind === 2) _creator2d.physicsSetFromEntity(this._xf)
|
|
85
|
+
else if (this._xfKind === 1) _creator2d.characterSetFromEntity(this._xf)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private _parent: Node2D | null = null
|
|
89
|
+
private _children: Node2D[] = []
|
|
90
|
+
|
|
91
|
+
constructor(internalId?: number) {
|
|
92
|
+
super()
|
|
93
|
+
this.id = internalId ?? _creator2d.createEntity()
|
|
94
|
+
node2dRegistry.set(this.id, this)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- transform (Y-up, LOCAL to the parent; at zoom 1, units == logical pixels) ---
|
|
98
|
+
// Pull the native transform into the cache iff physics owns it. No-op for the common case (plain
|
|
99
|
+
// nodes keep free reads); one native read for a moving body, and only when actually read.
|
|
100
|
+
private _pull(): void { if (this._physicsDriven) this._syncLocalFromNative() }
|
|
101
|
+
|
|
102
|
+
get x(): number { this._pull(); return this._x }
|
|
103
|
+
set x(v: number) { this._pull(); this._x = v; _creator2d.setPosition(this.id, v, this._y); if (this._xf) this._pushXf() }
|
|
104
|
+
get y(): number { this._pull(); return this._y }
|
|
105
|
+
set y(v: number) { this._pull(); this._y = v; _creator2d.setPosition(this.id, this._x, v); if (this._xf) this._pushXf() }
|
|
106
|
+
|
|
107
|
+
/** @internal chisel `comp_write` (see core/compWrite.ts): `node.position.<axis> = v` compiles to a call
|
|
108
|
+
* here; the scalar setters carry the physics routing. The list is compile-time data. */
|
|
109
|
+
static _comps = [ "position" ]
|
|
110
|
+
_writeComp(_prop: string, axis: CompAxis, v: number): void {
|
|
111
|
+
if (axis === "x") this.x = v
|
|
112
|
+
else if (axis === "y") this.y = v
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get position(): Vec2 { this._pull(); return new Vec2(this._x, this._y) }
|
|
116
|
+
set position(v: Vec2Like) {
|
|
117
|
+
this._x = cx(v); this._y = cy(v)
|
|
118
|
+
_creator2d.setPosition(this.id, this._x, this._y)
|
|
119
|
+
if (this._xf) this._pushXf()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Rotation in degrees (CCW). */
|
|
123
|
+
get rotation(): number { this._pull(); return this._rotation }
|
|
124
|
+
set rotation(deg: number) {
|
|
125
|
+
this._rotation = deg
|
|
126
|
+
_creator2d.setRotation(this.id, deg)
|
|
127
|
+
if (this._xf) this._pushXf()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get scale(): Vec2 { return new Vec2(this._sx, this._sy) }
|
|
131
|
+
set scale(v: Vec2Like | number) {
|
|
132
|
+
const sx = typeof v === "number" ? v : cx(v)
|
|
133
|
+
const sy = typeof v === "number" ? v : cy(v)
|
|
134
|
+
this._sx = sx; this._sy = sy
|
|
135
|
+
_creator2d.setScale(this.id, sx, sy)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Draw layer (int). Higher layers render on top. */
|
|
139
|
+
get layer(): number { return this._layer }
|
|
140
|
+
set layer(v: number) { this._layer = v; _creator2d.setLayer(this.id, v) }
|
|
141
|
+
|
|
142
|
+
/** Intra-layer depth for non-Y-sorted layers (higher z = on top). */
|
|
143
|
+
get z(): number { return this._z }
|
|
144
|
+
set z(v: number) { this._z = v; _creator2d.setZ(this.id, v) }
|
|
145
|
+
|
|
146
|
+
get visible(): boolean { return this._visible }
|
|
147
|
+
set visible(v: boolean) { this._visible = v; _creator2d.setVisible(this.id, v) }
|
|
148
|
+
|
|
149
|
+
// --- hierarchy (parent/child; transforms above are LOCAL to the parent) ---
|
|
150
|
+
get parent(): Node2D | null { return this._parent }
|
|
151
|
+
set parent(p: Node2D | null) { this.setParent(p) }
|
|
152
|
+
|
|
153
|
+
get children(): readonly Node2D[] { return this._children }
|
|
154
|
+
|
|
155
|
+
/** Attach children to this node (each keeps its current world transform). Returns this. */
|
|
156
|
+
add(...children: Node2D[]): this {
|
|
157
|
+
for (const c of children) c.setParent(this)
|
|
158
|
+
return this
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Detach a direct child (reparents it to root, keeping its world transform). Returns this. */
|
|
162
|
+
remove(child: Node2D): this {
|
|
163
|
+
if (child._parent === this) child.setParent(null)
|
|
164
|
+
return this
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Reparent this node. `keepWorld` (default true) recomputes the local transform so the on-screen
|
|
169
|
+
* position/rotation/scale is unchanged; pass false to keep the local transform and move with the
|
|
170
|
+
* new parent. Self/descendant parents are rejected natively (no cycles).
|
|
171
|
+
*/
|
|
172
|
+
setParent(parent: Node2D | null, keepWorld = true): this {
|
|
173
|
+
if (parent === this) return this
|
|
174
|
+
if (this._parent) {
|
|
175
|
+
const arr = this._parent._children
|
|
176
|
+
const i = arr.indexOf(this)
|
|
177
|
+
if (i >= 0) arr.splice(i, 1)
|
|
178
|
+
}
|
|
179
|
+
this._parent = parent
|
|
180
|
+
if (parent) parent._children.push(this)
|
|
181
|
+
_creator2d.setParent(this.id, parent?.id ?? 0, keepWorld)
|
|
182
|
+
if (keepWorld) this._syncLocalFromNative() // native recomputed our local transform
|
|
183
|
+
return this
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** World-space position (composes all ancestors). */
|
|
187
|
+
get worldPosition(): Vec2 {
|
|
188
|
+
const o = _creator2d.getWorldPosition(this.id)
|
|
189
|
+
return new Vec2(o[0], o[1])
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Convert a world-space point into this node's local space (and back). */
|
|
193
|
+
worldToLocal(p: Vec2Like): Vec2 {
|
|
194
|
+
const o = _creator2d.worldToLocal(this.id, cx(p), cy(p))
|
|
195
|
+
return new Vec2(o[0], o[1])
|
|
196
|
+
}
|
|
197
|
+
localToWorld(p: Vec2Like): Vec2 {
|
|
198
|
+
const o = _creator2d.localToWorld(this.id, cx(p), cy(p))
|
|
199
|
+
return new Vec2(o[0], o[1])
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private _syncLocalFromNative(): void {
|
|
203
|
+
const t = _creator2d.getLocalTransform(this.id) // [x, y, rotDeg, sx, sy]
|
|
204
|
+
this._x = t[0]; this._y = t[1]; this._rotation = t[2]; this._sx = t[3]; this._sy = t[4]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @internal — host animation events route here by id; the SpriteAnimation aspect installs an
|
|
208
|
+
* instance override that translates clip ids to names and emits 'loopReached' / 'completed'. */
|
|
209
|
+
_dispatchAnimEvent(_clipId: number, _type: number): void {}
|
|
210
|
+
|
|
211
|
+
/** @internal — emit a translated animation event (the SpriteAnimation aspect can't reach the
|
|
212
|
+
* protected dispatch directly). */
|
|
213
|
+
_emitAnim(channel: "loopReached" | "completed", clip: string): void { this.dispatch(channel, clip) }
|
|
214
|
+
|
|
215
|
+
/** @internal — physics contact/sensor events route here (loop.ts) → the node's 'enter'/'exit'. */
|
|
216
|
+
_emitCollision(channel: "enter" | "exit", other: Node2D, contact: Contact2D): void {
|
|
217
|
+
this.dispatch(channel, other, contact)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @internal — this node wants contact events. Set by the first 'enter'/'exit' listener; read by
|
|
221
|
+
* Physics2D.onAttach for the case where the listener was added BEFORE the body existed. */
|
|
222
|
+
_wantsContacts = false
|
|
223
|
+
|
|
224
|
+
// Lazily wire the host callbacks the listener needs. Contact/sensor events used to be registered
|
|
225
|
+
// only by Trigger2D.onAttach, so solid-vs-solid 'enter' never fired in a project with no triggers —
|
|
226
|
+
// and, separately, every solid shape had contact events forced on. Both are fixed here: the channel
|
|
227
|
+
// is registered by the listener, and the body opts in.
|
|
228
|
+
override addEventListener<K extends keyof Node2DEvents>(channel: K, callback: Node2DEvents[K]): void {
|
|
229
|
+
if (channel === "click" || channel === "touchstart") ensurePointerEvents()
|
|
230
|
+
else if (channel === "enter" || channel === "exit") {
|
|
231
|
+
ensurePhysicsEvents()
|
|
232
|
+
this._wantsContacts = true
|
|
233
|
+
if (this._xfKind === 2) _creator2d.physicsSetContactEvents(this._xf, true)
|
|
234
|
+
}
|
|
235
|
+
super.addEventListener(channel, callback)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @internal — the pointer system (g2/touch.ts) fires click/touchstart here. */
|
|
239
|
+
_dispatchEvent<K extends keyof Node2DEvents>(channel: K, ...args: Parameters<Node2DEvents[K]>): void {
|
|
240
|
+
this.dispatch(channel, ...args)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
destroy(): void {
|
|
244
|
+
// Aspects go first: onDetach runs (a body-owning aspect releases its handle), updaters are
|
|
245
|
+
// unregistered so a destroyed node never ticks again, listeners are dropped. Children survive
|
|
246
|
+
// (native reparents them to the root), so only this node's aspects detach.
|
|
247
|
+
this._detachAll()
|
|
248
|
+
// Native frees this entity's physics bodies inside destroyEntity (they used to leak — an invisible
|
|
249
|
+
// collider stayed in the world, and recycled entity ids then delivered its contacts to whoever
|
|
250
|
+
// inherited the id). Clear the routing so a stale aspect can't push a dead handle.
|
|
251
|
+
this._xf = 0
|
|
252
|
+
this._xfKind = 0
|
|
253
|
+
this._physicsDriven = false
|
|
254
|
+
// Detach from parent; native reparents our children to root (keepWorld), so resync their caches.
|
|
255
|
+
if (this._parent) {
|
|
256
|
+
const arr = this._parent._children
|
|
257
|
+
const i = arr.indexOf(this)
|
|
258
|
+
if (i >= 0) arr.splice(i, 1)
|
|
259
|
+
this._parent = null
|
|
260
|
+
}
|
|
261
|
+
const orphans = this._children.slice()
|
|
262
|
+
this._children.length = 0
|
|
263
|
+
node2dRegistry.delete(this.id)
|
|
264
|
+
_creator2d.destroyEntity(this.id)
|
|
265
|
+
for (const c of orphans) { c._parent = null; c._syncLocalFromNative() }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// A one-way surface, as an aspect on a Node2D that already carries a Shape2D and a Physics2D body:
|
|
2
|
+
// solid from one side, passable from the other. The semisolid ledge of every platformer.
|
|
3
|
+
//
|
|
4
|
+
// const ledge = new Sprite({ texture: plank, size: [128, 8] })
|
|
5
|
+
// .aspect(Shape2D, {})
|
|
6
|
+
// .aspect(Physics2D, { motion: 'static' })
|
|
7
|
+
// .aspect(OneWay2D) // jump up through it, stand on top
|
|
8
|
+
//
|
|
9
|
+
// It is an ASPECT and not a flag on Shape2D on purpose: Shape2D is geometry, and which side of a
|
|
10
|
+
// surface is solid is behaviour. Keeping them apart is also what lets `normal` generalise the thing —
|
|
11
|
+
// a one-way WALL you can only walk through from the left is the same aspect turned sideways.
|
|
12
|
+
//
|
|
13
|
+
// Honoured by the character's mover AND, through a native pre-solve callback, by ordinary rigid
|
|
14
|
+
// bodies (a crate falls onto the ledge from above and passes up through it from below). Neither path
|
|
15
|
+
// costs any JS per frame.
|
|
16
|
+
//
|
|
17
|
+
// Dropping DOWN through one is the character's business, not the surface's — see
|
|
18
|
+
// `CharacterController2D.dropThrough`.
|
|
19
|
+
|
|
20
|
+
import { Aspect } from "../core/Aspect"
|
|
21
|
+
import { cx, cy, type Vec2Like } from "../math/vec"
|
|
22
|
+
import { Physics2D } from "./Physics2D"
|
|
23
|
+
import type { Node2D } from "./Node2D"
|
|
24
|
+
|
|
25
|
+
export class OneWay2D extends Aspect<"oneWay", Node2D> {
|
|
26
|
+
static readonly aspect = "oneWay"
|
|
27
|
+
|
|
28
|
+
private _normal: Vec2Like = [ 0, 1 ]
|
|
29
|
+
private _arc = 90
|
|
30
|
+
private _enabled = true
|
|
31
|
+
|
|
32
|
+
/** Which side is solid. Default `[0, 1]` — solid from above, i.e. a floor you pass through from
|
|
33
|
+
* below. `[1, 0]` is a wall you can only enter from the left. */
|
|
34
|
+
get normal(): Vec2Like { return this._normal }
|
|
35
|
+
set normal(v: Vec2Like) { this._normal = v; this._push() }
|
|
36
|
+
|
|
37
|
+
/** How far off `normal` an approach still counts as coming from the solid side, in degrees.
|
|
38
|
+
* Default 90 — the whole half-plane, which is what a platform wants. */
|
|
39
|
+
get arc(): number { return this._arc }
|
|
40
|
+
set arc(v: number) { this._arc = v; this._push() }
|
|
41
|
+
|
|
42
|
+
/** Turn the one-way behaviour off, making the surface solid from both sides again. */
|
|
43
|
+
get enabled(): boolean { return this._enabled }
|
|
44
|
+
set enabled(v: boolean) { this._enabled = v; this._push() }
|
|
45
|
+
|
|
46
|
+
onAttach(): void {
|
|
47
|
+
if (!this.node.has(Physics2D)) {
|
|
48
|
+
throw new Error("OneWay2D needs a Physics2D body on the same node — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, { motion: 'static' }).aspect(OneWay2D)")
|
|
49
|
+
}
|
|
50
|
+
this._push()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onDetach(): void {
|
|
54
|
+
const id = this.node.get(Physics2D)?.id
|
|
55
|
+
if (id) _creator2d.physicsSetOneWay(id, 0, 1, 90, false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onReconfigure(): void { this._push() }
|
|
59
|
+
|
|
60
|
+
private _push(): void {
|
|
61
|
+
const id = this.node.get(Physics2D)?.id
|
|
62
|
+
// via the public accessors on purpose: an accessor pair that is only ever an object-literal key
|
|
63
|
+
// gets tree-shaken away, and the option would silently do nothing (see Physics2D.onAttach).
|
|
64
|
+
if (id) _creator2d.physicsSetOneWay(id, cx(this.normal), cy(this.normal), this.arc, this.enabled)
|
|
65
|
+
}
|
|
66
|
+
}
|