lecodes-cli 0.18.2 → 0.19.2
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/dist/index.js +1866 -301
- package/package.json +14 -12
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/core/Aspect.ts +0 -512
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -81
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -99
- package/runtime/sdk/gl/Lightmap.ts +0 -179
- package/runtime/sdk/gl/Material.ts +0 -245
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -64
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -479
- package/runtime/sdk/gl/Shape.ts +0 -377
- package/runtime/sdk/gl/Texture.ts +0 -46
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -87
- package/runtime/sdk/gl/animation/Layer.ts +0 -29
- package/runtime/sdk/gl/animation/Loop.ts +0 -25
- package/runtime/sdk/gl/animation/Playback.ts +0 -43
- package/runtime/sdk/gl/animation/core.ts +0 -294
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -186
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1227
- package/runtime/sdk/scene/editorPlugins.ts +0 -92
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -826
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
package/runtime/sdk/g2/Node2D.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,346 +0,0 @@
|
|
|
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
|
-
}
|