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,346 @@
|
|
|
1
|
+
// A rigid body, as an aspect on a Node2D, backed by Box2D v3 in creator-2d. Mirrors the 3D Physics
|
|
2
|
+
// aspect. Requires a Shape2D aspect on the same node (its geometry becomes the body's solid fixture).
|
|
3
|
+
//
|
|
4
|
+
// ground.aspect(Shape2D, { box: [500, 8] }).aspect(Physics2D, { motion: 'static' })
|
|
5
|
+
// crate.aspect(Shape2D, {}).aspect(Physics2D, { mass: 4, friction: 0.7, bounce: 0.1 })
|
|
6
|
+
// crate.physics.applyImpulse([0, 400])
|
|
7
|
+
// crate.addEventListener('enter', (other, c) => spark(c.point, c.normal))
|
|
8
|
+
//
|
|
9
|
+
// Physics2D.configure() is OPTIONAL — the world is created on the first body with sane defaults, and
|
|
10
|
+
// calling configure later just changes gravity (it used to destroy and rebuild the world, orphaning
|
|
11
|
+
// every handle that was already out).
|
|
12
|
+
//
|
|
13
|
+
// For a `dynamic` body, physics OWNS the transform — don't set node.position per frame; drive it via
|
|
14
|
+
// velocity / applyImpulse. A `position` write is a TELEPORT that reaches the body (Node2D routes it),
|
|
15
|
+
// which is also how a kinematic body is placed. Reading node.position always works.
|
|
16
|
+
//
|
|
17
|
+
// Keep bodies on ROOT nodes: a body parented to a moving node does not follow it (see
|
|
18
|
+
// docs/2d-physics-plan.md §6).
|
|
19
|
+
|
|
20
|
+
import { Aspect } from "../core/Aspect"
|
|
21
|
+
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
22
|
+
import { node2dRegistry, type Node2D } from "./Node2D"
|
|
23
|
+
import { Shape2D } from "./Shape2D"
|
|
24
|
+
import {
|
|
25
|
+
PhysicsGroup2D, _addGroup, _watchGroupChanges, _unwatchGroupChanges,
|
|
26
|
+
categoryOf, solidMask, defaultGroup,
|
|
27
|
+
} from "./groups2d"
|
|
28
|
+
|
|
29
|
+
export type MotionType = "static" | "kinematic" | "dynamic"
|
|
30
|
+
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
|
|
31
|
+
|
|
32
|
+
/** A neutral solid surface. Deliberately the same default as 3D `Physics.friction`. */
|
|
33
|
+
export const DEFAULT_FRICTION = 0.6
|
|
34
|
+
|
|
35
|
+
export interface PhysicsConfig {
|
|
36
|
+
/** Gravity in world units/s² (Y-up: down is negative). Default [0, -980]. */
|
|
37
|
+
gravity?: Vec2Like
|
|
38
|
+
/** Pixels per meter — tunes Box2D's internal tolerances; you still author in world units. Default 64.
|
|
39
|
+
* Only applies before the world exists (it is a global solver tolerance). */
|
|
40
|
+
pixelsPerMeter?: number
|
|
41
|
+
/** Solver sub-steps per fixed step. Default 4. */
|
|
42
|
+
subSteps?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Closest raycast hit. */
|
|
46
|
+
export interface RayHit {
|
|
47
|
+
node: Node2D | null
|
|
48
|
+
point: Vec2
|
|
49
|
+
normal: Vec2
|
|
50
|
+
fraction: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Resolve a native entity id to its Node2D (0 = none).
|
|
54
|
+
const node2dById = (id: number): Node2D | null => (id ? node2dRegistry.get(id) ?? null : null)
|
|
55
|
+
|
|
56
|
+
export class Physics2D extends Aspect<"physics", Node2D> {
|
|
57
|
+
static readonly aspect = "physics"
|
|
58
|
+
|
|
59
|
+
private _motion: MotionType = "dynamic"
|
|
60
|
+
/** Static (walls, floors), kinematic (script-driven platforms), or dynamic (fully simulated).
|
|
61
|
+
* Default "dynamic", and LIVE: freeze a crate into `'static'`, hand a ragdoll to `'dynamic'`. */
|
|
62
|
+
get motion(): MotionType { return this._motion }
|
|
63
|
+
set motion(m: MotionType) {
|
|
64
|
+
this._motion = m
|
|
65
|
+
this.node._physicsDriven = m !== "static"
|
|
66
|
+
if (this._h) _creator2d.physicsSetMotionType(this._h, MOTION[m])
|
|
67
|
+
}
|
|
68
|
+
/** Lock rotation (essential for platformer characters). */
|
|
69
|
+
fixedRotation = false
|
|
70
|
+
/** Continuous collision for fast-moving bodies (projectiles). */
|
|
71
|
+
bullet = false
|
|
72
|
+
|
|
73
|
+
private _mass = 0 // 0 = derive from the collider area
|
|
74
|
+
private _friction = DEFAULT_FRICTION
|
|
75
|
+
private _bounce = 0
|
|
76
|
+
private _gravityScale = 1
|
|
77
|
+
private _linearDamping = 0
|
|
78
|
+
private _angularDamping = 0
|
|
79
|
+
private _group: PhysicsGroup2D | undefined
|
|
80
|
+
private _repush = (): void => {
|
|
81
|
+
if (this._h) _creator2d.physicsSetFilter(this._h, categoryOf(this._group), solidMask(this._group))
|
|
82
|
+
}
|
|
83
|
+
private _h = 0 // native body id (0 = none / no physics support)
|
|
84
|
+
|
|
85
|
+
/** Which collision group this body belongs to (see Physics2D.addGroup). Live. */
|
|
86
|
+
get group(): PhysicsGroup2D | undefined { return this._group }
|
|
87
|
+
set group(g: PhysicsGroup2D | undefined) {
|
|
88
|
+
this._group = g
|
|
89
|
+
this.node.get(Shape2D)?._setFilter(categoryOf(g), solidMask(g))
|
|
90
|
+
this._repush()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Mass in world units. Omitted (or 0) = derived from the collider's AREA, so a big crate really is
|
|
94
|
+
* heavier than a small one. Setting it scales the shape-derived mass data, keeping the rotational
|
|
95
|
+
* inertia in the same ratio — a lopsided collider still tips the way its geometry says. */
|
|
96
|
+
get mass(): number { return this._h ? _creator2d.physicsGetMass(this._h) : this._mass }
|
|
97
|
+
set mass(v: number) { this._mass = v; if (this._h) _creator2d.physicsSetMass(this._h, v) }
|
|
98
|
+
|
|
99
|
+
/** Surface friction. 0 is frictionless, ~1 grippy; two bodies COMBINE as sqrt(a×b), so the lower
|
|
100
|
+
* value wins the argument. Default 0.6 — a neutral solid, matching 3D. */
|
|
101
|
+
get friction(): number { return this._friction }
|
|
102
|
+
set friction(v: number) { this._friction = v; this._pushMaterial() }
|
|
103
|
+
|
|
104
|
+
/** Bounciness, 0..1 — the fraction of the approach speed returned on impact. Two bodies combine as
|
|
105
|
+
* MAX (the bouncier one wins — the opposite of friction), and below ~64 units/s no bounce is
|
|
106
|
+
* applied at all, which is what stops a ball jittering on the floor forever. */
|
|
107
|
+
get bounce(): number { return this._bounce }
|
|
108
|
+
set bounce(v: number) { this._bounce = v; this._pushMaterial() }
|
|
109
|
+
|
|
110
|
+
// The live setters go to the BODY, but a later collider rebuild re-creates the fixture from the
|
|
111
|
+
// Shape2D — so the shape keeps its own copy of the material or a crouch would silently reset it.
|
|
112
|
+
private _pushMaterial(): void {
|
|
113
|
+
if (!this._h) return
|
|
114
|
+
_creator2d.physicsSetFriction(this._h, this._friction)
|
|
115
|
+
_creator2d.physicsSetBounce(this._h, this._bounce)
|
|
116
|
+
this.node.get(Shape2D)?._setMaterial(this._friction, this._bounce)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Native physics body id. 0 until attached, or if the build has no physics support. Mirrors 3D `Physics.id`. */
|
|
120
|
+
get id(): number { return this._h }
|
|
121
|
+
|
|
122
|
+
// NOTE — read every option through its PUBLIC accessor here, never the `_backing` field. chisel
|
|
123
|
+
// tree-shakes class members and keeps `_*` unconditionally, but it DROPS an accessor pair whose
|
|
124
|
+
// name no bundled code reads as a PROPERTY: an object-literal key (`aspect(Physics2D, { gravityScale: 0 })`)
|
|
125
|
+
// does NOT count. With the pair gone, Object.assign writes a plain own property, the backing field
|
|
126
|
+
// keeps its default, and the option silently does nothing — measured: a `gravityScale: 0` body fell.
|
|
127
|
+
// One `this.gravityScale` read in bundled SDK code keeps the whole pair alive.
|
|
128
|
+
onAttach(): void {
|
|
129
|
+
if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
|
|
130
|
+
const shape = this.node.get(Shape2D)
|
|
131
|
+
if (!shape) {
|
|
132
|
+
throw new Error("Physics2D requires a Shape2D aspect — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, {…})")
|
|
133
|
+
}
|
|
134
|
+
this._h = _creator2d.physicsCreateBody(this.node.id, MOTION[this.motion])
|
|
135
|
+
if (!this._h) return
|
|
136
|
+
shape._addTo(this._h, {
|
|
137
|
+
sensor: false, friction: this.friction, bounce: this.bounce,
|
|
138
|
+
category: categoryOf(this.group), mask: solidMask(this.group),
|
|
139
|
+
staticBody: this.motion === "static",
|
|
140
|
+
})
|
|
141
|
+
// Only a body that named a group cares when the topology changes.
|
|
142
|
+
if (this._group) _watchGroupChanges(this._repush)
|
|
143
|
+
// dynamic/kinematic bodies are moved by the native step → the node must re-sync its transform
|
|
144
|
+
// cache from native on read. Static bodies never move, so they keep the free cache.
|
|
145
|
+
if (this.motion !== "static") this.node._physicsDriven = true
|
|
146
|
+
// Route node.position / .rotation writes to the body (this is what replaced physics.moveTo).
|
|
147
|
+
this.node._xf = this._h
|
|
148
|
+
this.node._xfKind = 2
|
|
149
|
+
if (this._mass > 0) _creator2d.physicsSetMass(this._h, this.mass)
|
|
150
|
+
if (this.fixedRotation) _creator2d.physicsSetFixedRotation(this._h, true)
|
|
151
|
+
if (this.gravityScale !== 1) _creator2d.physicsSetGravityScale(this._h, this.gravityScale)
|
|
152
|
+
if (this.linearDamping !== 0) _creator2d.physicsSetLinearDamping(this._h, this.linearDamping)
|
|
153
|
+
if (this.angularDamping !== 0) _creator2d.physicsSetAngularDamping(this._h, this.angularDamping)
|
|
154
|
+
if (this.bullet) _creator2d.physicsSetBullet(this._h, true)
|
|
155
|
+
// The listener may have been added before the body existed; contact events are per-body opt-in.
|
|
156
|
+
if (this.node._wantsContacts) _creator2d.physicsSetContactEvents(this._h, true)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
onDetach(): void {
|
|
160
|
+
_unwatchGroupChanges(this._repush)
|
|
161
|
+
if (this._h) { _creator2d.physicsRemoveBody(this._h); this._h = 0 }
|
|
162
|
+
this.node._physicsDriven = false
|
|
163
|
+
this.node._xf = 0
|
|
164
|
+
this.node._xfKind = 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Re-applying options (`node.aspect(Physics2D, {…})`) pushes the live ones through. */
|
|
168
|
+
onReconfigure(): void {
|
|
169
|
+
if (!this._h) return
|
|
170
|
+
this._pushMaterial()
|
|
171
|
+
_creator2d.physicsSetFixedRotation(this._h, this.fixedRotation)
|
|
172
|
+
_creator2d.physicsSetBullet(this._h, this.bullet)
|
|
173
|
+
_creator2d.physicsSetGravityScale(this._h, this.gravityScale)
|
|
174
|
+
_creator2d.physicsSetLinearDamping(this._h, this.linearDamping)
|
|
175
|
+
_creator2d.physicsSetAngularDamping(this._h, this.angularDamping)
|
|
176
|
+
if (this._mass > 0) _creator2d.physicsSetMass(this._h, this.mass)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Linear velocity in world units/second (fresh Vec2 on read). */
|
|
180
|
+
get velocity(): Vec2 {
|
|
181
|
+
const o = this._h ? _creator2d.physicsGetLinearVelocity(this._h) : null
|
|
182
|
+
return o ? new Vec2(o[0], o[1]) : new Vec2(0, 0)
|
|
183
|
+
}
|
|
184
|
+
set velocity(v: Vec2Like) { if (this._h) _creator2d.physicsSetLinearVelocity(this._h, cx(v), cy(v)) }
|
|
185
|
+
|
|
186
|
+
/** Angular velocity in degrees/second. */
|
|
187
|
+
get angularVelocity(): number { return this._h ? _creator2d.physicsGetAngularVelocity(this._h) : 0 }
|
|
188
|
+
set angularVelocity(degPerSec: number) { if (this._h) _creator2d.physicsSetAngularVelocity(this._h, degPerSec) }
|
|
189
|
+
|
|
190
|
+
/** Per-body gravity multiplier: 1 = full world gravity, 0 = floats, <0 = repelled. */
|
|
191
|
+
get gravityScale(): number { return this._gravityScale }
|
|
192
|
+
set gravityScale(scale: number) { this._gravityScale = scale; if (this._h) _creator2d.physicsSetGravityScale(this._h, scale) }
|
|
193
|
+
|
|
194
|
+
get linearDamping(): number { return this._linearDamping }
|
|
195
|
+
set linearDamping(d: number) { this._linearDamping = d; if (this._h) _creator2d.physicsSetLinearDamping(this._h, d) }
|
|
196
|
+
|
|
197
|
+
get angularDamping(): number { return this._angularDamping }
|
|
198
|
+
set angularDamping(d: number) { this._angularDamping = d; if (this._h) _creator2d.physicsSetAngularDamping(this._h, d) }
|
|
199
|
+
|
|
200
|
+
/** Pause this body — it stops simulating and stops colliding until re-enabled. */
|
|
201
|
+
set enabled(v: boolean) { if (this._h) _creator2d.physicsSetEnabled(this._h, v) }
|
|
202
|
+
|
|
203
|
+
/** Box2D puts resting bodies to sleep; wake one to make it react to a change you made by hand. */
|
|
204
|
+
set awake(v: boolean) { if (this._h) _creator2d.physicsSetAwake(this._h, v) }
|
|
205
|
+
|
|
206
|
+
/** Apply an instantaneous impulse at the centre of mass (never spins the body) and wake it. */
|
|
207
|
+
applyImpulse(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyLinearImpulse(this._h, cx(v), cy(v)); return this }
|
|
208
|
+
/** Apply an impulse at a WORLD point — the lever arm becomes spin. Use this when the push has a real
|
|
209
|
+
* point of contact: a bullet's hit point, a blast pushing a prop at its base. */
|
|
210
|
+
applyImpulseAt(v: Vec2Like, worldPoint: Vec2Like): this {
|
|
211
|
+
if (this._h) _creator2d.physicsApplyImpulseAt(this._h, cx(v), cy(v), cx(worldPoint), cy(worldPoint))
|
|
212
|
+
return this
|
|
213
|
+
}
|
|
214
|
+
/** Apply a continuous force (world units) and wake the body. */
|
|
215
|
+
applyForce(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyForce(this._h, cx(v), cy(v)); return this }
|
|
216
|
+
|
|
217
|
+
// --- world (static) -------------------------------------------------------------------------
|
|
218
|
+
/** Set the world's gravity / solver settings. OPTIONAL: the world is created on the first body with
|
|
219
|
+
* gravity [0, -980], 64 px/m and 4 sub-steps. Calling this later changes gravity LIVE and leaves
|
|
220
|
+
* bodies alone. */
|
|
221
|
+
static configure(config: PhysicsConfig = {}): void {
|
|
222
|
+
const g = config.gravity ?? [0, -980]
|
|
223
|
+
_creator2d.physicsConfigure(cx(g), cy(g), config.pixelsPerMeter ?? 64, config.subSteps ?? 4)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** World gravity, live. */
|
|
227
|
+
static get gravity(): Vec2 { return new Vec2(Physics2D._gravity) }
|
|
228
|
+
static set gravity(g: Vec2Like) {
|
|
229
|
+
Physics2D._gravity = [ cx(g), cy(g) ]
|
|
230
|
+
_creator2d.physicsConfigure(cx(g), cy(g), 0, 0)
|
|
231
|
+
}
|
|
232
|
+
private static _gravity: [number, number] = [ 0, -980 ]
|
|
233
|
+
|
|
234
|
+
/** Whether this build has physics support (CREATOR_2D_PHYSICS). NOTE: this reports the BUILD, not
|
|
235
|
+
* the world — there is nothing else to guard, since the world creates itself. */
|
|
236
|
+
static get supported(): boolean { return !!_creator2d.physicsHasSupport && _creator2d.physicsHasSupport() }
|
|
237
|
+
|
|
238
|
+
/** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
|
|
239
|
+
* off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
|
|
240
|
+
* smoothness — bodies then advance in discrete steps. */
|
|
241
|
+
private static _interpolation = true
|
|
242
|
+
static get interpolation(): boolean { return Physics2D._interpolation }
|
|
243
|
+
static set interpolation(on: boolean) { Physics2D._interpolation = on; _creator2d.setInterpolation(on) }
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Register a collision group. Groups are SUBTRACTIVE and SYMMETRIC (see groups2d.ts):
|
|
247
|
+
*
|
|
248
|
+
* const player = Physics2D.addGroup()
|
|
249
|
+
* const bullets = Physics2D.addGroup().ignoreSelf().ignore(player)
|
|
250
|
+
*/
|
|
251
|
+
static addGroup(label?: string): PhysicsGroup2D { return _addGroup(label) }
|
|
252
|
+
|
|
253
|
+
/** The implicit group of every body that never named one - name it to exclude "everything else". */
|
|
254
|
+
static get defaultGroup(): PhysicsGroup2D { return defaultGroup }
|
|
255
|
+
|
|
256
|
+
// --- queries (static) -----------------------------------------------------------------------
|
|
257
|
+
/** Closest body hit by the segment, or null. `node` resolves the hit entity. */
|
|
258
|
+
static raycast(from: Vec2Like, to: Vec2Like, q?: Query2D): RayHit | null {
|
|
259
|
+
const o = _creator2d.physicsRaycastClosest(cx(from), cy(from), cx(to), cy(to), maskOf(q), ignoreOf(q), ignoreCount(q))
|
|
260
|
+
if (o[0] < 0.5) return null
|
|
261
|
+
return { node: node2dById(o[1]), point: new Vec2(o[2], o[3]), normal: new Vec2(o[4], o[5]), fraction: o[6] }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Every body along the segment, NEAREST FIRST - a piercing bullet, a laser, "how many walls are
|
|
265
|
+
* between us". Capped at 32 hits. */
|
|
266
|
+
static raycastAll(from: Vec2Like, to: Vec2Like, q?: Query2D): RayHit[] {
|
|
267
|
+
const o = _creator2d.physicsRaycastAll(cx(from), cy(from), cx(to), cy(to), maskOf(q), ignoreOf(q), ignoreCount(q), MAX_HITS)
|
|
268
|
+
const hits: RayHit[] = []
|
|
269
|
+
for (let i = 0; i + 5 < o.length; i += 6) {
|
|
270
|
+
if (o[i] === 0) break
|
|
271
|
+
hits.push({ node: node2dById(o[i]), point: new Vec2(o[i + 1], o[i + 2]), normal: new Vec2(o[i + 3], o[i + 4]), fraction: o[i + 5] })
|
|
272
|
+
}
|
|
273
|
+
return hits
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Topmost body (solid or sensor) whose shape contains the world point, or null. "Topmost" means
|
|
277
|
+
* by DRAW order - layer, then z - so picking agrees with what the player sees. */
|
|
278
|
+
static overlapPoint(p: Vec2Like, q?: Query2D): Node2D | null {
|
|
279
|
+
return node2dById(_creator2d.physicsOverlapPoint(cx(p), cy(p), maskOf(q), ignoreOf(q), ignoreCount(q)))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Everything within `radius` of a point - an explosion, an aura, an aggro range. */
|
|
283
|
+
static overlapCircle(centre: Vec2Like, radius: number, q?: Query2D): Node2D[] {
|
|
284
|
+
return overlapNodes([ cx(centre), cy(centre) ], radius, q)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Everything inside an axis-aligned box (centre + HALF-extents) - a room, a camera bound. */
|
|
288
|
+
static overlapBox(centre: Vec2Like, half: Vec2Like, q?: Query2D): Node2D[] {
|
|
289
|
+
const x = cx(centre), y = cy(centre), hw = cx(half), hh = cy(half)
|
|
290
|
+
return overlapNodes([ x - hw, y - hh, x + hw, y - hh, x + hw, y + hh, x - hw, y + hh ], 0, q)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Everything touching a capsule from-to of the given radius. A capsule is the shape a DIRECTED
|
|
295
|
+
* question has: a beam with width, a sword arc, "does my radius fit through this gap" - none of
|
|
296
|
+
* which an axis-aligned box can express.
|
|
297
|
+
*
|
|
298
|
+
* It is also the SWEPT test: a circle of radius r moving from A to B during a frame sweeps exactly
|
|
299
|
+
* this capsule, so a fast projectile checked this way cannot tunnel through a thin wall the way two
|
|
300
|
+
* separate position checks do.
|
|
301
|
+
*/
|
|
302
|
+
static overlapCapsule(from: Vec2Like, to: Vec2Like, radius: number, q?: Query2D): Node2D[] {
|
|
303
|
+
return overlapNodes([ cx(from), cy(from), cx(to), cy(to) ], radius, q)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Which groups a query may hit, and what to skip. */
|
|
308
|
+
export type Query2D = {
|
|
309
|
+
/** Only hit bodies in these groups. Omitted = everything. */
|
|
310
|
+
groups?: PhysicsGroup2D[]
|
|
311
|
+
/** Skip these nodes - the "don't hit myself" case a ray fired from inside its own body needs. */
|
|
312
|
+
ignore?: Node2D | Node2D[]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const MAX_HITS = 32
|
|
316
|
+
const emptyIgnore = new Float32Array(0)
|
|
317
|
+
let ignoreBuf = new Float32Array(8)
|
|
318
|
+
|
|
319
|
+
const maskOf = (q?: Query2D): number => {
|
|
320
|
+
if (!q?.groups || q.groups.length === 0) return 0 // 0 = everything, native-side
|
|
321
|
+
let m = 0
|
|
322
|
+
for (const g of q.groups) m |= g.category
|
|
323
|
+
return m >>> 0
|
|
324
|
+
}
|
|
325
|
+
const ignoreCount = (q?: Query2D): number =>
|
|
326
|
+
!q?.ignore ? 0 : Array.isArray(q.ignore) ? q.ignore.length : 1
|
|
327
|
+
const ignoreOf = (q?: Query2D): Float32Array => {
|
|
328
|
+
const n = ignoreCount(q)
|
|
329
|
+
if (n === 0) return emptyIgnore
|
|
330
|
+
if (ignoreBuf.length < n) ignoreBuf = new Float32Array(n)
|
|
331
|
+
const ig = q!.ignore!
|
|
332
|
+
if (Array.isArray(ig)) ig.forEach((node, i) => { ignoreBuf[i] = node.id })
|
|
333
|
+
else ignoreBuf[0] = (ig as Node2D).id
|
|
334
|
+
return ignoreBuf
|
|
335
|
+
}
|
|
336
|
+
const overlapNodes = (pts: number[], radius: number, q?: Query2D): Node2D[] => {
|
|
337
|
+
const o = _creator2d.physicsOverlap(new Float32Array(pts), pts.length / 2, radius,
|
|
338
|
+
maskOf(q), ignoreOf(q), ignoreCount(q), MAX_HITS)
|
|
339
|
+
const out: Node2D[] = []
|
|
340
|
+
for (let i = 0; i < o.length; i++) {
|
|
341
|
+
if (o[i] === 0) break
|
|
342
|
+
const n = node2dById(o[i])
|
|
343
|
+
if (n) out.push(n)
|
|
344
|
+
}
|
|
345
|
+
return out
|
|
346
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// A 2D scene: a draw set + an orthographic Camera2D. Opening a scene asks the host to bring up the
|
|
2
|
+
// 2D engine and starts its RAF loop; only the active scene renders. Grown from the Phase 0 seed.
|
|
3
|
+
|
|
4
|
+
import { Color, type ColorInput } from "../core/color"
|
|
5
|
+
import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem, _detachAllSystems, type Aspect, type AspectCtor, type FieldOf, type TargetOf } from "../core/Aspect"
|
|
6
|
+
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
|
|
7
|
+
import { cx, cy, type Vec2Like } from "../math/vec"
|
|
8
|
+
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
9
|
+
import { Camera2D } from "./Camera2D"
|
|
10
|
+
import { node2dRegistry, type Node2D } from "./Node2D"
|
|
11
|
+
import { ensurePointerEvents } from "./touch"
|
|
12
|
+
|
|
13
|
+
// Route aspect update(dt) through the render-synced native phases (c2dRender early = before physics,
|
|
14
|
+
// late = after animations / before draw), so followers/cameras are in lockstep with the draw. Done from
|
|
15
|
+
// the 2D layer (not core) so a 3D-only bundle never pulls in the 2D bridge. Installed once, lazily on
|
|
16
|
+
// the first Scene2D. On an older wasm without the hooks the installer returns false → setLoop fallback.
|
|
17
|
+
let framesInstalled = false
|
|
18
|
+
const installRenderSyncedFrames = (): void => {
|
|
19
|
+
if (framesInstalled) return
|
|
20
|
+
framesInstalled = true
|
|
21
|
+
_installAspectFrames((early, late) => {
|
|
22
|
+
if (typeof _creator2d.setEarlyUpdate !== "function") return false
|
|
23
|
+
_creator2d.setEarlyUpdate(early)
|
|
24
|
+
_creator2d.setLateUpdate(late)
|
|
25
|
+
return true
|
|
26
|
+
})
|
|
27
|
+
// Time.scale / Time.paused reach the engine's physics + animation clocks (feature-detected).
|
|
28
|
+
_installTimeScale((scale) => { _creator2d.setTimeScale?.(scale) })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Scene2DOptions = {
|
|
32
|
+
/** Background color — '#10131a', 0x10131a, or [r,g,b]/[r,g,b,a] in 0..1. */
|
|
33
|
+
background?: ColorInput
|
|
34
|
+
/** Texture sampling for the 2D renderer: `'nearest'` (default — crisp pixel art) or `'linear'`
|
|
35
|
+
* (smooth — for hi-res / non-pixel-art assets). Applied when the scene is opened, so switching
|
|
36
|
+
* scenes restores the active scene's choice. NOTE: this is a GLOBAL renderer setting (one sampler
|
|
37
|
+
* for ALL textures, not per-texture), so the currently-open scene's filter wins. Omit to leave the
|
|
38
|
+
* current setting (engine default is nearest). */
|
|
39
|
+
filter?: 'nearest' | 'linear'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returned by scene2d.layer(n); chain .ySort() to enable top-down depth sorting for that layer. */
|
|
43
|
+
class LayerHandle {
|
|
44
|
+
private readonly sceneId: number
|
|
45
|
+
private readonly layer: number
|
|
46
|
+
constructor(sceneId: number, layer: number) { this.sceneId = sceneId; this.layer = layer }
|
|
47
|
+
/** In this layer, lower-on-screen sprites overlap higher ones (top-down feet sorting). */
|
|
48
|
+
ySort(enabled = true): this {
|
|
49
|
+
_creator2d.setLayerYSort(this.sceneId, this.layer, enabled)
|
|
50
|
+
return this
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Scene2D implements Presentable {
|
|
55
|
+
/** Native scene handle. */
|
|
56
|
+
readonly id: number
|
|
57
|
+
readonly camera: Camera2D
|
|
58
|
+
|
|
59
|
+
private static _active: Scene2D | null = null
|
|
60
|
+
static get active(): Scene2D | null { return Scene2D._active }
|
|
61
|
+
|
|
62
|
+
/** @internal — scene-level pointer listeners (the catch-all; target = hit node or null). */
|
|
63
|
+
readonly _clickListeners: Array<(ev: ClickEvent<Node2D | null>) => void> = []
|
|
64
|
+
readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node2D | null>) => void> = []
|
|
65
|
+
|
|
66
|
+
/** Texture filter to (re)assert on open(); the sampler is global, so each scene reclaims it. */
|
|
67
|
+
private readonly _filter?: 'nearest' | 'linear'
|
|
68
|
+
|
|
69
|
+
constructor(options: Scene2DOptions = {}) {
|
|
70
|
+
installRenderSyncedFrames()
|
|
71
|
+
this.id = _creator2d.createScene()
|
|
72
|
+
this.camera = new Camera2D(this.id)
|
|
73
|
+
this._filter = options.filter
|
|
74
|
+
if (options.background !== undefined) this.background = options.background
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
set background(color: ColorInput) {
|
|
78
|
+
const [ r, g, b, a ] = Color.toRgba01(color)
|
|
79
|
+
_creator2d.sceneSetClearColor(this.id, r, g, b, a)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Address a draw layer to configure it, e.g. scene2d.layer(1).ySort(). */
|
|
83
|
+
layer(n: number): LayerHandle {
|
|
84
|
+
return new LayerHandle(this.id, n)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Topmost visible sprite whose world bounds contain the world point, or null. Body-free
|
|
88
|
+
* hit-test for tap/click picking — pair with camera.screenToWorld(). */
|
|
89
|
+
pick(worldPoint: Vec2Like): Node2D | null {
|
|
90
|
+
const id = _creator2d.pickSprite(this.id, cx(worldPoint), cy(worldPoint))
|
|
91
|
+
return id ? node2dRegistry.get(id) ?? null : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Scene-level pointer events. The handler fires for every tap/press in the scene, with
|
|
95
|
+
* `ev.target` = the hit Physics2D node (or null). On 'touchstart' you can ev.track(...) a drag. */
|
|
96
|
+
addEventListener(channel: "click", callback: (ev: ClickEvent<Node2D | null>) => void): void
|
|
97
|
+
addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<Node2D | null>) => void): void
|
|
98
|
+
addEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
|
|
99
|
+
const list = channel === "click" ? this._clickListeners : this._touchStartListeners
|
|
100
|
+
if (!list.includes(callback)) { list.push(callback); ensurePointerEvents() }
|
|
101
|
+
}
|
|
102
|
+
removeEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
|
|
103
|
+
const list = channel === "click" ? this._clickListeners : this._touchStartListeners
|
|
104
|
+
const i = list.indexOf(callback)
|
|
105
|
+
if (i >= 0) list.splice(i, 1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
add(...nodes: Node2D[]): this {
|
|
109
|
+
for (const n of nodes) _creator2d.addEntityToScene(this.id, n.id)
|
|
110
|
+
return this
|
|
111
|
+
}
|
|
112
|
+
remove(...nodes: Node2D[]): this {
|
|
113
|
+
for (const n of nodes) _creator2d.removeEntityFromScene(this.id, n.id)
|
|
114
|
+
return this
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- systems: aspects of the scene (core/Aspect.ts `System<'x', Scene2D>`) ----
|
|
118
|
+
/** @internal class → instance, the authoritative store (named accessors mirror this). */
|
|
119
|
+
readonly _systems = new Map<Function, Aspect<any, any, any>>()
|
|
120
|
+
|
|
121
|
+
/** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
|
|
122
|
+
* as now-having it (`scene.system(Hud).hud`). A 3D `System<'x'>` (target Scene) is rejected here. */
|
|
123
|
+
// `this: Self` (not `Self & Scene2D`): with the intersection TS infers Self = Scene2D and a chain
|
|
124
|
+
// `scene.system(A).system(B)` loses A's accessor from the result type.
|
|
125
|
+
system<Self extends TargetOf<A>, A extends Aspect<any, any, any>>(
|
|
126
|
+
this: Self,
|
|
127
|
+
ctor: AspectCtor<A>,
|
|
128
|
+
opts?: Partial<A>,
|
|
129
|
+
): Self & FieldOf<A> {
|
|
130
|
+
return _attachSystem(this as unknown as Scene2D, ctor, opts) as unknown as Self & FieldOf<A>
|
|
131
|
+
}
|
|
132
|
+
/** Safe access — undefined if the system isn't attached. */
|
|
133
|
+
get<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): A | undefined {
|
|
134
|
+
return this._systems.get(ctor) as A | undefined
|
|
135
|
+
}
|
|
136
|
+
/** Existence check AND type guard: inside `if (scene.has(Hud))`, `scene.hud` is present. */
|
|
137
|
+
has<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
|
|
138
|
+
return this._systems.has(ctor)
|
|
139
|
+
}
|
|
140
|
+
/** Detach a system (runs its onDetach). Chainable. */
|
|
141
|
+
removeSystem<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this {
|
|
142
|
+
_detachSystem(this, ctor)
|
|
143
|
+
return this
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
147
|
+
|
|
148
|
+
/** @internal presentation lifecycle — fired by the host on present/dismiss (new navigation),
|
|
149
|
+
* or synchronously by open()/close() on legacy hosts. */
|
|
150
|
+
readonly ol: (() => void)[] = []
|
|
151
|
+
readonly cl: (() => void)[] = []
|
|
152
|
+
_backButtonCallback?: () => void
|
|
153
|
+
private _vd?: object
|
|
154
|
+
|
|
155
|
+
/** Fires when the scene becomes the visible destination (incl. a pop revealing it). */
|
|
156
|
+
onOpen(callback: () => void): this {
|
|
157
|
+
this.ol.push(callback)
|
|
158
|
+
return this
|
|
159
|
+
}
|
|
160
|
+
/** Fires when the scene stops being visible (closed, replaced, or covered by a push). */
|
|
161
|
+
onClose(callback: () => void): this {
|
|
162
|
+
this.cl.push(callback)
|
|
163
|
+
return this
|
|
164
|
+
}
|
|
165
|
+
onBackPressed(callback: () => void): this {
|
|
166
|
+
this._backButtonCallback = callback
|
|
167
|
+
return this
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
|
|
171
|
+
_viewDesc(): object {
|
|
172
|
+
return this._vd ??= { type: "scene2d", sceneId: this.id, _p: this }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Make this the active scene — shows it as the current destination (replaces a screen /
|
|
176
|
+
* another scene; only the active scene renders). */
|
|
177
|
+
open(options?: PresentOptions): this {
|
|
178
|
+
_bumpNavEpoch()
|
|
179
|
+
// Reassert this scene's texture filter (the sampler is global — see Scene2DOptions.filter).
|
|
180
|
+
if (this._filter !== undefined) _creator2d.setDefaultFilter(this._filter === 'linear')
|
|
181
|
+
if (_navSupported()) {
|
|
182
|
+
// The host activates the engine (openScene) as part of presenting the destination.
|
|
183
|
+
_creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
|
|
184
|
+
} else {
|
|
185
|
+
_creator2d.openScene(this.id)
|
|
186
|
+
for (const cb of this.ol) cb()
|
|
187
|
+
}
|
|
188
|
+
Scene2D._active = this
|
|
189
|
+
_setCurrent(this)
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
close(): this {
|
|
193
|
+
_bumpNavEpoch()
|
|
194
|
+
if (_navSupported()) {
|
|
195
|
+
_creatorUI.closeView!()
|
|
196
|
+
} else {
|
|
197
|
+
_creator2d.closeScene()
|
|
198
|
+
for (const cb of this.cl) cb()
|
|
199
|
+
}
|
|
200
|
+
if (Scene2D._active === this) Scene2D._active = null
|
|
201
|
+
if (Presentable.current === this) _setCurrent(null)
|
|
202
|
+
return this
|
|
203
|
+
}
|
|
204
|
+
destroy(): void {
|
|
205
|
+
_detachAllSystems(this) // systems go first (last-attached first), while the scene still exists
|
|
206
|
+
this.close()
|
|
207
|
+
_creator2d.destroyScene(this.id)
|
|
208
|
+
}
|
|
209
|
+
}
|