lecodes-sdk 0.20.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/global.d.ts +35 -9
  2. package/dist/types/animate/tween/Animation.d.ts +69 -0
  3. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  4. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  5. package/dist/types/animate/tween/easing.d.ts +29 -0
  6. package/dist/types/animate/tween/spec.d.ts +178 -0
  7. package/dist/types/canvas/Canvas.d.ts +2 -0
  8. package/dist/types/g2/Node2D.d.ts +16 -0
  9. package/dist/types/g2/Sprite.d.ts +11 -1
  10. package/dist/types/gl/Camera.d.ts +15 -1
  11. package/dist/types/gl/DecalSet.d.ts +48 -3
  12. package/dist/types/gl/Foliage.d.ts +47 -0
  13. package/dist/types/gl/Geometry.d.ts +36 -0
  14. package/dist/types/gl/Light.d.ts +25 -7
  15. package/dist/types/gl/Lightmap.d.ts +90 -51
  16. package/dist/types/gl/Material.d.ts +32 -20
  17. package/dist/types/gl/Mesh.d.ts +7 -1
  18. package/dist/types/gl/Model.d.ts +41 -5
  19. package/dist/types/gl/Node.d.ts +18 -0
  20. package/dist/types/gl/Particles.d.ts +53 -1
  21. package/dist/types/gl/Scene.d.ts +21 -1
  22. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  23. package/dist/types/gl/animation/Animator.d.ts +27 -0
  24. package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
  25. package/dist/types/gl/animation/IK.d.ts +109 -0
  26. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  27. package/dist/types/gl/animation/Warp.d.ts +2 -1
  28. package/dist/types/gl/animation/core.d.ts +35 -4
  29. package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
  30. package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
  31. package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
  32. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  33. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  34. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  35. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  36. package/dist/types/gl/physics/Ragdoll.d.ts +161 -0
  37. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  38. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  39. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +10 -8
  40. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  41. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  42. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  43. package/dist/types/inject.d.ts +35 -27
  44. package/dist/types/runtime/files.d.ts +24 -1
  45. package/dist/types/scene/defineScene.d.ts +50 -31
  46. package/dist/types/ui/UIButton.d.ts +3 -1
  47. package/dist/types/ui/UIInput.d.ts +5 -1
  48. package/dist/types/ui/UINode.d.ts +24 -24
  49. package/dist/types.json +1 -1
  50. package/package.json +1 -1
  51. package/prompts/README.md +142 -142
  52. package/prompts/core-design.md +27 -4
  53. package/prompts/core.md +35 -6
  54. package/prompts/select.ts +19 -4
  55. package/src/animate/tween/Animation.ts +378 -0
  56. package/src/animate/tween/Timeline.ts +175 -0
  57. package/src/animate/tween/animateValue.ts +100 -0
  58. package/src/animate/tween/easing.ts +172 -0
  59. package/src/animate/tween/spec.ts +479 -0
  60. package/src/bridges.d.ts +1760 -1481
  61. package/src/canvas/Canvas.ts +21 -0
  62. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  63. package/src/compile/__tests__/compile.test.ts +11 -0
  64. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  65. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  66. package/src/compile/bundler.ts +34 -4
  67. package/src/compile/compileProject.ts +31 -1
  68. package/src/compile/detectEntry.ts +8 -3
  69. package/src/compile/header.ts +6 -3
  70. package/src/compile/index.ts +3 -0
  71. package/src/compile/sceneEditor.ts +42 -1
  72. package/src/compile/serverSplit.ts +9 -3
  73. package/src/g2/Node2D.ts +38 -0
  74. package/src/g2/Sprite.ts +20 -1
  75. package/src/gl/Camera.ts +34 -1
  76. package/src/gl/DecalSet.ts +132 -5
  77. package/src/gl/Foliage.ts +102 -0
  78. package/src/gl/Geometry.ts +109 -0
  79. package/src/gl/Light.ts +46 -16
  80. package/src/gl/Lightmap.ts +440 -249
  81. package/src/gl/Material.ts +69 -36
  82. package/src/gl/Mesh.ts +120 -102
  83. package/src/gl/Model.ts +167 -124
  84. package/src/gl/Node.ts +40 -1
  85. package/src/gl/Particles.ts +82 -5
  86. package/src/gl/Scene.ts +35 -2
  87. package/src/gl/animation/AnimationClip.ts +52 -0
  88. package/src/gl/animation/Animator.ts +42 -2
  89. package/src/gl/animation/DynamicBone.ts +482 -0
  90. package/src/gl/animation/IK.ts +214 -0
  91. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  92. package/src/gl/animation/Playback.ts +5 -4
  93. package/src/gl/animation/Warp.ts +5 -2
  94. package/src/gl/animation/core.ts +65 -4
  95. package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
  96. package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
  97. package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
  98. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  99. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  100. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  101. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  102. package/src/gl/physics/Ragdoll.ts +451 -0
  103. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  104. package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
  105. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  106. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +14 -12
  107. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  108. package/src/gl/vehicle/Vehicle.ts +666 -0
  109. package/src/gl/vehicle/Wheel.ts +290 -0
  110. package/src/inject.ts +236 -224
  111. package/src/runtime/files.ts +32 -2
  112. package/src/scene/defineScene.ts +92 -66
  113. package/src/scene/level.ts +2 -2
  114. package/src/ui/UIButton.ts +2 -2
  115. package/src/ui/UIInput.ts +3 -3
  116. package/src/ui/UINode.ts +61 -36
  117. package/dist/types/animate/animate.d.ts +0 -20
  118. package/dist/types/gl/Gearbox.d.ts +0 -86
  119. package/dist/types/gl/IK.d.ts +0 -53
  120. package/dist/types/gl/Ragdoll.d.ts +0 -86
  121. package/dist/types/gl/Vehicle.d.ts +0 -191
  122. package/dist/types/gl/Wheel.d.ts +0 -95
  123. package/src/animate/animate.ts +0 -238
  124. package/src/gl/Gearbox.ts +0 -212
  125. package/src/gl/IK.ts +0 -193
  126. package/src/gl/Ragdoll.ts +0 -270
  127. package/src/gl/Vehicle.ts +0 -473
  128. package/src/gl/Wheel.ts +0 -240
  129. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
@@ -11,11 +11,11 @@
11
11
  // velocity / applyImpulse. Reading node.position is free (native already wrote it). A `position` write
12
12
  // is a TELEPORT that reaches the body (Shape routes it), which is how a kinematic body is placed too.
13
13
 
14
- import { Aspect } from "../core/Aspect"
15
- import type { FieldMeta } from "../core/fields"
16
- import type { CompAxis, CompWriter } from "../core/compWrite"
17
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
18
- import { nodeRegistry, type Node } from "./Node"
14
+ import { Aspect } from "../../core/Aspect"
15
+ import type { FieldMeta } from "../../core/fields"
16
+ import type { CompAxis, CompWriter } from "../../core/compWrite"
17
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
18
+ import { nodeRegistry, type Node } from "../Node"
19
19
  import { Shape } from "./Shape"
20
20
 
21
21
  export type MotionType = "static" | "dynamic" | "kinematic"
@@ -95,6 +95,7 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
95
95
  }
96
96
 
97
97
  private _bodyId = 0
98
+ private static _refusedWarned = false
98
99
 
99
100
  onAttach(): void {
100
101
  if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
@@ -107,6 +108,12 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
107
108
  }
108
109
  const shapeId = shape._claim() // drop the pick-only body; reuse its shape
109
110
  this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
111
+ // The engine refuses a body past its limit (4 096 unless configured) — silently, and a level that
112
+ // crosses it loses whatever loads LAST (the terrain, streamed in after the props): say so once.
113
+ if (!this._bodyId && shapeId && !Physics._refusedWarned) {
114
+ Physics._refusedWarned = true
115
+ console.warn(`Physics: the engine refused a body for "${this.node.name}" — the body limit is reached? Raise it before the level loads: Physics.configure({ maxBodies })`)
116
+ }
110
117
  // A fresh body already carries DEFAULT_FRICTION host-side, so only a NAMED surface has to
111
118
  // travel — the common case costs no bridge call at all.
112
119
  if (this._friction !== DEFAULT_FRICTION) _creator.physicsSetFriction?.(this._bodyId, this._friction)
@@ -0,0 +1,451 @@
1
+ // A ragdoll: the model's skeleton handed to physics. One rigid body per listed bone — a capsule from
2
+ // the bone's origin to the next joint down the chain — joined to its parent part by a swing-twist
3
+ // constraint (a cone + a twist range, or a one-way hinge for knees and elbows) that carries a motor.
4
+ // Built once at attach from the pose the model is in (that pose is the joints' neutral for the
5
+ // limits); the limits themselves are LEARNED from the animation by default (`limits: 'clips'`: how
6
+ // far each joint swings and twists over every clip bound to the model's animator, measured at the
7
+ // first activation) — and switched on and off at will:
8
+ //
9
+ // const rag = hero.aspect(Ragdoll).get(Ragdoll)! // 'humanoid': the parts by bone name
10
+ // rag.activate() // drops from its CURRENT pose, every bone
11
+ // // keeps the motion it had (a swinging arm
12
+ // // keeps swinging, a runner keeps travelling)
13
+ // rag.goLimp(0.4) // a death: the joints give way over 0.4 s
14
+ // …
15
+ // rag.deactivate({ blend: 0.5 }) // the animator takes the bones back and
16
+ // hero.anim.play('GetUpBack', { fade: 0.5 }) // blends OUT of the fallen pose into this
17
+ //
18
+ // POWERED: every joint has a motor that pulls it toward the pose the animator shows — `strength`
19
+ // (0..1) is how hard. With the root `anchored` the hips follow the animation kinematically, so a
20
+ // standing character stays in its clip while physics jolts a limb: `hit(bone, impulse, at)` does
21
+ // exactly that for a flinch — powers the body up, pushes the struck part, and hands the bones back
22
+ // with a blend once the reaction is over. `strength` 0 with no anchor is a limp body.
23
+ //
24
+ // While active the engine writes the bodies' poses onto the bones every frame AFTER the animator, so
25
+ // the listed bones hide whatever plays underneath; the bones NOT listed (fingers, toes, the rest of a
26
+ // spine chain between two parts, the clavicles) follow the animation while the body is driven and
27
+ // hold their pose as it goes limp. Anything mounted on a bone (a weapon in the hand) rides along.
28
+ // Raycasts hit the parts and report the bone node, so a shot knows which limb it struck —
29
+ // `impulse(bone, …)` pushes that part.
30
+ //
31
+ // The character CONTROLLER is yours to switch off for a fall (`node.removeAspect(CharacterController)`):
32
+ // a capsule left standing would keep the fallen body from settling. `root` (the hips), `facing`
33
+ // (on the back / prone / on a side) and `heading` (where the head points, degrees) tell you where and
34
+ // how the body ended up when it is time to get back up.
35
+ //
36
+ // A fallen body is scenery, not an obstacle: by default (`collide: 'static'`) only the static world
37
+ // and other ragdolls stop it — dynamic bodies, character controllers and vehicles pass through, so it
38
+ // never shoves or blocks anything and the solver never pairs it with them — and once every part has
39
+ // come to rest (`freeze`, or `freezeAfter` seconds at the latest) the parts turn static where they
40
+ // lie: the pose holds, nothing can wake them, the solver skips them. Raycasts hit it either way, and
41
+ // a frozen `'all'` body is an obstacle you can stand on.
42
+
43
+ import { Aspect } from "../../core/Aspect"
44
+ import type { FieldMeta } from "../../core/fields"
45
+ import { Mathf } from "../../math/Mathf"
46
+ import { cx, cy, cz, Vec3, type Vec3Like } from "../../math/vec"
47
+ import { Node } from "../Node"
48
+ import { DEFAULT_FRICTION } from "./Physics"
49
+
50
+ export interface RagdollHinge {
51
+ /** The hinge axis in the MODEL's space (its rest pose: +Z forward, +Y up, +X the model's left in
52
+ * glTF). Perpendicular to the bone; the engine drops any component along it. */
53
+ axis: Vec3Like
54
+ /** The bend range about `axis` in degrees, relative to the pose the ragdoll was built in — a knee
55
+ * is `{ axis: [1, 0, 0], min: 0, max: 140 }`: it folds one way only. */
56
+ min: number
57
+ max: number
58
+ }
59
+
60
+ export interface RagdollPart {
61
+ /** The bone the body sits on — its origin is the joint. */
62
+ bone: string
63
+ /** The bone the capsule points at (the next joint down). Omitted = a leaf: `length` along the line
64
+ * from the parent part's origin (a head, a hand, a foot without toes). */
65
+ to?: string
66
+ /** The bone of the part this one hangs from (default: the previous part in the list). The root
67
+ * has none. */
68
+ parent?: string
69
+ /** Capsule radius in metres (default 0.05). */
70
+ radius?: number
71
+ /** Length in metres: a leaf's, or an override of the distance to `to`. */
72
+ length?: number
73
+ /** This part's share of the ragdoll's `mass` (relative weight, default 1). */
74
+ mass?: number
75
+ /** Cone half-angle in degrees the part may swing off its parent (default 30). Ignored with `hinge`. */
76
+ swing?: number
77
+ /** Twist half-angle in degrees about its own axis (default 20). */
78
+ twist?: number
79
+ /** A one-way joint instead of the cone. */
80
+ hinge?: RagdollHinge
81
+ }
82
+
83
+ /** How the body lies: on its back (the chest points up), prone (the chest points down), or on a side. */
84
+ export type RagdollFacing = "up" | "down" | "side"
85
+
86
+ export interface RagdollActivateOptions {
87
+ /** A LAUNCH added to every part's own motion (world m/s) — an explosion, a throw. The bones' own
88
+ * motion (the run, the swinging arms) is measured by the engine and needs no help. */
89
+ velocity?: Vec3Like
90
+ /** The motors' strength from the first step (default: the current `strength`). */
91
+ strength?: number
92
+ /** The root anchored to the animation from the first step (default: the current `anchored`). */
93
+ anchored?: boolean
94
+ }
95
+
96
+ export interface RagdollDeactivateOptions {
97
+ /** Seconds the animator takes to transition OUT of the fallen pose into whatever plays next (a
98
+ * get-up take started right after, or the loop). 0 (default) = a cut. Move the model node under
99
+ * the hips before this call: the bones are handed back against its new transform. */
100
+ blend?: number
101
+ }
102
+
103
+ export interface RagdollHitOptions {
104
+ /** How long the body stays powered before the bones go back to the animator (s, default 0.6). */
105
+ duration?: number
106
+ /** The transition out of the reaction's last physical pose (s, default 0.25). */
107
+ blend?: number
108
+ /** The motors' strength during the reaction (default 1). */
109
+ strength?: number
110
+ }
111
+
112
+ const VERSION = 3
113
+ const HEADER = 16
114
+ const STRIDE = 12
115
+
116
+ // Bone names per humanoid role, tried in order: Mixamo / MotusMan / Unity-style first, then UE-style.
117
+ // `node.bone()` already matches the part after a `:` / `|` prefix (`mixamorig:Hips`).
118
+ /** @internal shared with DynamicBone's `colliders: 'humanoid'` */
119
+ export const HUMANOID: Record<string, string[]> = {
120
+ hips: [ "Hips", "pelvis", "Pelvis", "hip" ],
121
+ spine: [ "Spine", "spine_01", "spine", "Spine1" ],
122
+ neck: [ "Neck", "neck_01", "neck" ],
123
+ head: [ "Head", "head" ],
124
+ upperArmL: [ "LeftArm", "upperarm_l", "LeftUpperArm", "UpperArm_L", "L_UpperArm" ],
125
+ foreArmL: [ "LeftForeArm", "lowerarm_l", "LeftLowerArm", "LowerArm_L", "L_Forearm" ],
126
+ handL: [ "LeftHand", "hand_l", "Hand_L", "L_Hand" ],
127
+ upLegL: [ "LeftUpLeg", "thigh_l", "LeftUpperLeg", "UpperLeg_L", "L_Thigh" ],
128
+ legL: [ "LeftLeg", "calf_l", "LeftLowerLeg", "LowerLeg_L", "L_Calf" ],
129
+ footL: [ "LeftFoot", "foot_l", "Foot_L", "L_Foot" ],
130
+ toeL: [ "LeftToeBase", "ball_l", "LeftToe", "Toe_L", "L_Toe" ],
131
+ upperArmR: [ "RightArm", "upperarm_r", "RightUpperArm", "UpperArm_R", "R_UpperArm" ],
132
+ foreArmR: [ "RightForeArm", "lowerarm_r", "RightLowerArm", "LowerArm_R", "R_Forearm" ],
133
+ handR: [ "RightHand", "hand_r", "Hand_R", "R_Hand" ],
134
+ upLegR: [ "RightUpLeg", "thigh_r", "RightUpperLeg", "UpperLeg_R", "R_Thigh" ],
135
+ legR: [ "RightLeg", "calf_r", "RightLowerLeg", "LowerLeg_R", "R_Calf" ],
136
+ footR: [ "RightFoot", "foot_r", "Foot_R", "R_Foot" ],
137
+ toeR: [ "RightToeBase", "ball_r", "RightToe", "Toe_R", "R_Toe" ],
138
+ }
139
+
140
+ /** The standard humanoid layout (hips, one spine capsule up to the neck, head, two-segment arms and
141
+ * legs, hands and feet where the rig has them): sizes for a ~1.8 m human, mass shares from body
142
+ * segment tables, knees and elbows as hinges that fold the natural way for a model standing in a
143
+ * T- or A-pose facing +Z. */
144
+ /** @internal shared with DynamicBone's `colliders: 'humanoid'` */
145
+ export const humanoidParts = (find: (role: string) => string | null): RagdollPart[] => {
146
+ const hips = find("hips"), spine = find("spine"), neck = find("neck"), head = find("head")
147
+ if (!hips) throw new Error("Ragdoll 'humanoid': no hips bone (Hips / pelvis) on this model — list the parts by name")
148
+ const parts: RagdollPart[] = []
149
+ parts.push({ bone: hips, to: spine ?? undefined, length: spine ? 0 : 0.2, radius: 0.12, mass: 15 })
150
+ const torso = spine ?? hips
151
+ if (spine) parts.push({ bone: spine, to: neck ?? head ?? undefined, length: neck || head ? 0 : 0.4, parent: hips, radius: 0.11, mass: 25, swing: 25, twist: 25 })
152
+ if (head) parts.push({ bone: head, parent: torso, radius: 0.1, length: 0.2, mass: 6, swing: 35, twist: 45 })
153
+ for (const side of [ "L", "R" ] as const) {
154
+ const upper = find(`upperArm${side}`), fore = find(`foreArm${side}`), hand = find(`hand${side}`)
155
+ if (upper) parts.push({ bone: upper, to: fore ?? undefined, length: fore ? 0 : 0.3, parent: torso, radius: 0.05, mass: 3, swing: 80, twist: 45 })
156
+ // the elbow: a T-pose arm along ±X folds its hand forward (+Z) — a negative turn about +Y on the
157
+ // left, a positive one on the right
158
+ if (upper && fore) parts.push({ bone: fore, to: hand ?? undefined, length: hand ? 0 : 0.28, parent: upper, radius: 0.04, mass: 2, twist: 30, hinge: { axis: [ 0, 1, 0 ], min: side === "L" ? -140 : 0, max: side === "L" ? 0 : 140 } })
159
+ if (upper && fore && hand) parts.push({ bone: hand, parent: fore, radius: 0.035, length: 0.16, mass: 1, swing: 30, twist: 20 })
160
+ const up = find(`upLeg${side}`), leg = find(`leg${side}`), foot = find(`foot${side}`), toe = find(`toe${side}`)
161
+ if (up) parts.push({ bone: up, to: leg ?? undefined, length: leg ? 0 : 0.45, parent: hips, radius: 0.08, mass: 10, swing: 60, twist: 30 })
162
+ // the knee: the shin (pointing down) swings its foot backward (−Z) for a positive turn about +X
163
+ if (up && leg) parts.push({ bone: leg, to: foot ?? undefined, length: foot ? 0 : 0.42, parent: up, radius: 0.06, mass: 5, twist: 10, hinge: { axis: [ 1, 0, 0 ], min: 0, max: 140 } })
164
+ if (up && leg && foot) parts.push({ bone: foot, to: toe ?? undefined, length: toe ? 0 : 0.2, parent: leg, radius: 0.04, mass: 2, swing: 30, twist: 15 })
165
+ }
166
+ return parts
167
+ }
168
+
169
+ export class Ragdoll extends Aspect<"ragdoll", Node> {
170
+ static readonly aspect = "ragdoll"
171
+
172
+ /** The parts: `'humanoid'` (default) finds the standard bones by name; a list places bodies on any
173
+ * bones — parents before children. */
174
+ parts: RagdollPart[] | "humanoid" = "humanoid"
175
+ /** Total mass in kg (default 70), split between the parts by their `mass` shares. */
176
+ mass = 70
177
+ /** Friction of every part (default 0.6 — the physics default). */
178
+ friction = DEFAULT_FRICTION
179
+ /** Linear / angular damping of every part; 0 = Jolt's own 0.05. More angular damping settles a
180
+ * flailing body sooner. */
181
+ linearDamping = 0
182
+ angularDamping = 0
183
+ /** What the parts collide with. `'static'` (default): the static geometry and other such ragdolls
184
+ * only — dynamic bodies, character controllers and vehicles pass through a fallen body, which then
185
+ * never shoves or blocks anything and costs nothing when they meet. `'all'`: a regular dynamic
186
+ * body that bumps into everything (and gets kicked awake by everything). */
187
+ collide: "static" | "all" = "static"
188
+ /** Once every part of a LIMP body has come to rest the parts turn static where they lie (default
189
+ * true): the pose holds, nothing can wake a settled body and it costs the solver nothing —
190
+ * `deactivate()` / `activate()` still work (activate makes it dynamic again). A driven or anchored
191
+ * body never freezes. */
192
+ freeze = true
193
+ /** Seconds limp after which the body freezes whatever it is doing — a twitch on a slope or a pile
194
+ * never sleeps on its own. 0 (default) = no cap. */
195
+ freezeAfter = 0
196
+ /** The joint motors: the position spring's frequency in Hz (default 6 — higher = a stiffer, quicker
197
+ * return to the animated pose) and damping ratio (default 1 = critical, no overshoot). */
198
+ driveFrequency = 6
199
+ driveDamping = 1
200
+ /** The motors' torque limit at strength 1, in N·m per kg of the part (default 12: ~180 N·m at the
201
+ * hips of an 80 kg body, ~25 at a forearm). Lower = a hit displaces a limb more before the
202
+ * animation wins it back. */
203
+ driveTorque = 12
204
+ /** Joint friction in N·m per kg of the part (default 0.3): a torque that resists any joint motion,
205
+ * motor or not — a limp body folds instead of flopping like jelly. */
206
+ jointFriction = 0.3
207
+ /** Where the joint limits come from. `'clips'` (default): measured from the animation — at the first
208
+ * activation that finds clips on the model's animator, every joint's swing and twist range over
209
+ * every bound clip becomes its limit (plus `limitsMargin` on each side), so the motors never
210
+ * target a pose the limits forbid and a limp body settles into poses the clips use; until clips
211
+ * are bound the part table's cones apply. `'table'`: the part table's cones and hinges only. */
212
+ limits: "clips" | "table" = "clips"
213
+ /** Degrees added on each side of a learned range (default 10). */
214
+ limitsMargin = 10
215
+
216
+ static fields: FieldMeta<Ragdoll> = {
217
+ mass: { min: 1, step: 5 },
218
+ friction: { min: 0, max: 2, step: 0.05 },
219
+ linearDamping: { label: "Linear damping", min: 0, max: 5, step: 0.05 },
220
+ angularDamping: { label: "Angular damping", min: 0, max: 5, step: 0.05 },
221
+ collide: { options: [ "static", "all" ] },
222
+ freezeAfter: { label: "Freeze after (s)", min: 0, max: 60, step: 0.5 },
223
+ driveFrequency: { label: "Drive frequency (Hz)", min: 0.5, max: 30, step: 0.5 },
224
+ driveDamping: { label: "Drive damping", min: 0, max: 3, step: 0.1 },
225
+ driveTorque: { label: "Drive torque (N·m/kg)", min: 0, max: 100, step: 1 },
226
+ jointFriction: { label: "Joint friction (N·m/kg)", min: 0, max: 5, step: 0.05 },
227
+ limits: { options: [ "clips", "table" ] },
228
+ limitsMargin: { label: "Limits margin (deg)", min: 0, max: 45, step: 1 },
229
+ parts: { hidden: true },
230
+ }
231
+
232
+ private _id = 0
233
+ private _active = false
234
+ private _bones: { name: string; node: Node }[] = []
235
+ private _strength = 0
236
+ private _anchored = false
237
+ // the hips' frame: where the model's forward and up point, in the hips bone's own space (at attach)
238
+ private _hipsFwd = new Vec3(0, 0, 1)
239
+ private _hipsUp = new Vec3(0, 1, 0)
240
+ // goLimp: the strength ramp
241
+ private _ramp: { from: number; duration: number; t: number } | null = null
242
+ // hit: the pending hand-back
243
+ private _release: { left: number; blend: number } | null = null
244
+
245
+ onAttach(): void {
246
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
247
+ if (!_creator.ragdollCreate) {
248
+ console.warn("Ragdoll needs a newer host (no ragdollCreate) — the body will not fall")
249
+ return
250
+ }
251
+ const find = (role: string): string | null => {
252
+ for (const name of HUMANOID[role] ?? []) if (this.node.bone(name)) return name
253
+ return null
254
+ }
255
+ const parts = this.parts === "humanoid" ? humanoidParts(find) : this.parts
256
+ if (parts.length === 0) throw new Error("Ragdoll: no parts")
257
+
258
+ const n = parts.length
259
+ const bones = new Uint32Array(n * 2)
260
+ const s = new Float32Array(HEADER + n * STRIDE)
261
+ s[0] = VERSION; s[1] = n; s[2] = STRIDE; s[3] = this.friction; s[4] = this.linearDamping; s[5] = this.angularDamping
262
+ s[6] = this.collide === "all" ? 0 : 1; s[7] = this.freeze ? 1 : 0; s[8] = Math.max(0, this.freezeAfter)
263
+ s[9] = Math.max(0, this.driveFrequency); s[10] = Math.max(0, this.driveDamping); s[11] = Math.max(0, this.driveTorque); s[12] = Math.max(0, this.jointFriction)
264
+ s[13] = this.limits === "clips" ? 1 : 0; s[14] = Math.max(0, this.limitsMargin)
265
+ let shares = 0
266
+ for (const p of parts) shares += p.mass ?? 1
267
+ this._bones = []
268
+ for (let i = 0; i < n; i++) {
269
+ const p = parts[i]!
270
+ const bone = this.node.bone(p.bone)
271
+ if (!bone) throw new Error(`Ragdoll: no bone '${p.bone}' on '${this.node.name}'`)
272
+ const to = p.to ? this.node.bone(p.to) : null
273
+ if (p.to && !to) throw new Error(`Ragdoll: no bone '${p.to}' (the 'to' of '${p.bone}') on '${this.node.name}'`)
274
+ let parent = -1
275
+ if (i > 0) {
276
+ parent = p.parent ? parts.findIndex((q, j) => j < i && q.bone === p.parent) : i - 1
277
+ if (parent < 0) throw new Error(`Ragdoll: the parent '${p.parent}' of '${p.bone}' must be listed before it`)
278
+ }
279
+ bones[i * 2] = bone.id
280
+ bones[i * 2 + 1] = to ? to.id : 0
281
+ const o = HEADER + i * STRIDE
282
+ s[o] = parent
283
+ s[o + 1] = p.radius ?? 0.05
284
+ s[o + 2] = p.length ?? 0
285
+ s[o + 3] = this.mass * (p.mass ?? 1) / shares
286
+ s[o + 4] = p.swing ?? 30
287
+ s[o + 5] = p.twist ?? 20
288
+ if (p.hinge) {
289
+ s[o + 6] = 1
290
+ s[o + 7] = cx(p.hinge.axis); s[o + 8] = cy(p.hinge.axis); s[o + 9] = cz(p.hinge.axis)
291
+ s[o + 10] = p.hinge.min; s[o + 11] = p.hinge.max
292
+ }
293
+ this._bones.push({ name: p.bone, node: bone })
294
+ }
295
+ // the model's forward / up in the hips' frame, for `facing` / `heading` later
296
+ const hips = this._bones[0]!.node
297
+ const hipsInv = hips.worldMatrix.invert()
298
+ const model = this.node.worldMatrix
299
+ this._hipsFwd = hipsInv.transformDirection(model.transformDirection([ 0, 0, 1 ])).normalize()
300
+ this._hipsUp = hipsInv.transformDirection(model.transformDirection([ 0, 1, 0 ])).normalize()
301
+ this._id = _creator.ragdollCreate(this.node.id, bones, s)
302
+ if (!this._id) console.warn(`Ragdoll: the host refused the parts on '${this.node.name}'`)
303
+ else if (this._strength > 0 || this._anchored) _creator.ragdollSetDrive?.(this._id, this._strength, this._anchored)
304
+ }
305
+
306
+ onDetach(): void {
307
+ if (this._id) {
308
+ if (this._active) _creator.ragdollDeactivate!(this._id, 0)
309
+ _creator.ragdollDestroy!(this._id)
310
+ this._id = 0
311
+ }
312
+ this._active = false
313
+ this._bones = []
314
+ this._ramp = null
315
+ this._release = null
316
+ }
317
+
318
+ /** Hand the bones to physics from the pose they are in right now, each moving as it did over the
319
+ * last two frames (the engine measures: a runner keeps travelling, a swinging arm keeps swinging). */
320
+ activate(opts: RagdollActivateOptions = {}): boolean {
321
+ if (!this._id) return false
322
+ if (opts.strength !== undefined || opts.anchored !== undefined) {
323
+ this._strength = Mathf.clamp(opts.strength ?? this._strength, 0, 1)
324
+ this._anchored = opts.anchored ?? this._anchored
325
+ _creator.ragdollSetDrive?.(this._id, this._strength, this._anchored)
326
+ }
327
+ const v = opts.velocity ?? [ 0, 0, 0 ]
328
+ this._active = _creator.ragdollActivate!(this._id, cx(v), cy(v), cz(v))
329
+ this._ramp = null
330
+ this._release = null
331
+ return this._active
332
+ }
333
+
334
+ /** Take the bones back: the animator's pose shows again from the next frame — through a `blend`
335
+ * out of the fallen pose when asked (play the get-up take right after, with its own fade: it
336
+ * starts from where the body lies). */
337
+ deactivate(opts: RagdollDeactivateOptions = {}): void {
338
+ this._ramp = null
339
+ this._release = null
340
+ if (!this._id || !this._active) return
341
+ _creator.ragdollDeactivate!(this._id, Math.max(0, opts.blend ?? 0))
342
+ this._active = false
343
+ }
344
+
345
+ /** Physics owns the bones right now. */
346
+ get active(): boolean { return this._active }
347
+
348
+ /** Active and every part asleep (or frozen) — the body has come to rest. */
349
+ get settled(): boolean {
350
+ return this._active && !!this._id && !_creator.ragdollActive!(this._id)
351
+ }
352
+
353
+ /** The joint motors' strength, 0..1: how hard every joint is pulled toward the pose the animator
354
+ * shows (0 = off — a limp body). Kept across activations. */
355
+ get strength(): number { return this._strength }
356
+ set strength(v: number) {
357
+ this._strength = Mathf.clamp(v, 0, 1)
358
+ this._ramp = null
359
+ if (this._id) _creator.ragdollSetDrive?.(this._id, this._strength, this._anchored)
360
+ }
361
+
362
+ /** The hips follow the animation kinematically (the body stands in its clip while physics moves the
363
+ * limbs — hit reactions, a stagger). Off = the hips are a free body (a fall). */
364
+ get anchored(): boolean { return this._anchored }
365
+ set anchored(v: boolean) {
366
+ this._anchored = v
367
+ if (this._id) _creator.ragdollSetDrive?.(this._id, this._strength, this._anchored)
368
+ }
369
+
370
+ /** The joints give way: the strength ramps to 0 over `seconds` and the anchor comes off — a body
371
+ * shot mid-stride keeps its pose for a moment and then collapses, instead of switching off. */
372
+ goLimp(seconds = 0.4): void {
373
+ this._anchored = false
374
+ if (seconds <= 0 || this._strength <= 0) { this.strength = 0; return }
375
+ this._ramp = { from: this._strength, duration: seconds, t: 0 }
376
+ if (this._id) _creator.ragdollSetDrive?.(this._id, this._strength, false)
377
+ }
378
+
379
+ /** A flinch: the body powers up anchored to its animation (if it is not already active), the struck
380
+ * part gets the impulse (N·s, at a world point), and after `duration` the bones go back to the
381
+ * animator through `blend`. On a limp body (a corpse) it is just the impulse. */
382
+ hit(bone: string, v: Vec3Like, at?: Vec3Like, opts: RagdollHitOptions = {}): boolean {
383
+ if (!this._id) return false
384
+ const limp = this._active && this._strength <= 0 && !this._anchored
385
+ if (!limp) {
386
+ if (!this._active) {
387
+ if (!this.activate({ strength: opts.strength ?? 1, anchored: true })) return false
388
+ }
389
+ this._release = { left: opts.duration ?? 0.6, blend: opts.blend ?? 0.25 }
390
+ }
391
+ return this.impulse(bone, v, at)
392
+ }
393
+
394
+ /** The root part's bone (the hips): where the body is. */
395
+ get root(): Node | null { return this._bones[0]?.node ?? null }
396
+
397
+ /** How the body lies: `'up'` on its back (the chest points up), `'down'` prone, `'side'` otherwise.
398
+ * Read while it is down, to pick the get-up take. */
399
+ get facing(): RagdollFacing {
400
+ const hips = this._bones[0]?.node
401
+ if (!hips) return "up"
402
+ const y = hips.worldMatrix.transformDirection(this._hipsFwd).normalize().y
403
+ return y > 0.5 ? "up" : y < -0.5 ? "down" : "side"
404
+ }
405
+
406
+ /** Where the head points along the ground, as a yaw in degrees (the model node's `eulerAngles` y
407
+ * that faces that way): where a get-up take that rises head-first ends up facing. A body still
408
+ * upright answers with the way its chest faces. */
409
+ get heading(): number {
410
+ const hips = this._bones[0]?.node
411
+ if (!hips) return 0
412
+ const m = hips.worldMatrix
413
+ let d = m.transformDirection(this._hipsUp)
414
+ if (Math.hypot(d.x, d.z) < 0.3) d = m.transformDirection(this._hipsFwd)
415
+ return Mathf.RAD2DEG * Math.atan2(d.x, d.z)
416
+ }
417
+
418
+ /** The bones that carry a part, in order. */
419
+ get bones(): string[] { return this._bones.map((b) => b.name) }
420
+
421
+ /** The rigid-body id of a part (0 if none) — for the plain body calls. */
422
+ bodyOf(bone: string): number {
423
+ const i = this._bones.findIndex((b) => b.name === bone)
424
+ return i < 0 || !this._id ? 0 : _creator.ragdollBodyId!(this._id, i)
425
+ }
426
+
427
+ /** Push one part: an impulse in N·s, at a world point (a hit) or through its centre. Wakes the body. */
428
+ impulse(bone: string, v: Vec3Like, at?: Vec3Like): boolean {
429
+ const id = this.bodyOf(bone)
430
+ if (!id) return false
431
+ if (at && _creator.physicsApplyImpulseAt) _creator.physicsApplyImpulseAt(id, cx(v), cy(v), cz(v), cx(at), cy(at), cz(at))
432
+ else _creator.physicsApplyImpulse(id, cx(v), cy(v), cz(v))
433
+ return true
434
+ }
435
+
436
+ /** The strength ramp (goLimp) and the hit reaction's hand-back. */
437
+ update(dt: number): void {
438
+ if (this._ramp) {
439
+ const r = this._ramp
440
+ r.t += dt
441
+ const k = Mathf.clamp(r.t / r.duration, 0, 1)
442
+ this._strength = r.from * (1 - k) * (1 - k) // eases out: the joints hold, then give
443
+ if (k >= 1) { this._strength = 0; this._ramp = null }
444
+ if (this._id) _creator.ragdollSetDrive?.(this._id, this._strength, this._anchored)
445
+ }
446
+ if (this._release && this._active) {
447
+ this._release.left -= dt
448
+ if (this._release.left <= 0) this.deactivate({ blend: this._release.blend })
449
+ }
450
+ }
451
+ }
@@ -17,9 +17,9 @@
17
17
  // pick body is a STATIC snapshot at attach time — for a moving pickable object add a Physics body
18
18
  // (which follows the sim), rather than relying on the pick body.
19
19
 
20
- import { Aspect } from "../core/Aspect"
21
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
22
- import type { Node } from "./Node"
20
+ import { Aspect } from "../../core/Aspect"
21
+ import { cx, cy, cz, type Vec3Like } from "../../math/vec"
22
+ import type { Node } from "../Node"
23
23
 
24
24
  /** Which geometry a Shape uses — the one-of the fields below encode (see `_build`'s order). */
25
25
  export type ShapeKind = "auto" | "box" | "sphere" | "cylinder" | "capsule" | "mesh" | "convex" | "heightfield"
@@ -1,45 +1,45 @@
1
- // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
- // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
- // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
- //
5
- // const goal = new Mesh(box(), material)
6
- // .aspect(Shape, { box: [1, 2, 1] })
7
- // .aspect(Trigger)
8
- // goal.addEventListener('enter', other => win(other))
9
- // goal.addEventListener('exit', other => …)
10
-
11
- import { Aspect } from "../core/Aspect"
12
- import { Shape } from "./Shape"
13
- import { ensurePhysicsEvents } from "./physicsEvents"
14
- import type { Node } from "./Node"
15
-
16
- export class Trigger extends Aspect<"trigger", Node> {
17
- static readonly aspect = "trigger"
18
-
19
- private _bodyId = 0
20
- /** Native body id (0 if no physics support). */
21
- get id(): number { return this._bodyId }
22
-
23
- onAttach(): void {
24
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
25
- const shape = this.node.get(Shape)
26
- if (!shape) {
27
- throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
28
- }
29
- const shapeId = shape._claim()
30
- this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
31
- shape._ownBody(this._bodyId)
32
- ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
33
- }
34
-
35
- onDetach(): void {
36
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
37
- const shape = this.node.get(Shape)
38
- if (shape) {
39
- shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
40
- shape._recreatePickBody()
41
- }
42
- }
43
-
44
- // No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
45
- }
1
+ // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
+ // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
+ // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
+ //
5
+ // const goal = new Mesh(box(), material)
6
+ // .aspect(Shape, { box: [1, 2, 1] })
7
+ // .aspect(Trigger)
8
+ // goal.addEventListener('enter', other => win(other))
9
+ // goal.addEventListener('exit', other => …)
10
+
11
+ import { Aspect } from "../../core/Aspect"
12
+ import { Shape } from "./Shape"
13
+ import { ensurePhysicsEvents } from "./physicsEvents"
14
+ import type { Node } from "../Node"
15
+
16
+ export class Trigger extends Aspect<"trigger", Node> {
17
+ static readonly aspect = "trigger"
18
+
19
+ private _bodyId = 0
20
+ /** Native body id (0 if no physics support). */
21
+ get id(): number { return this._bodyId }
22
+
23
+ onAttach(): void {
24
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
25
+ const shape = this.node.get(Shape)
26
+ if (!shape) {
27
+ throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
28
+ }
29
+ const shapeId = shape._claim()
30
+ this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
31
+ shape._ownBody(this._bodyId)
32
+ ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
33
+ }
34
+
35
+ onDetach(): void {
36
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
37
+ const shape = this.node.get(Shape)
38
+ if (shape) {
39
+ shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
40
+ shape._recreatePickBody()
41
+ }
42
+ }
43
+
44
+ // No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
45
+ }
@@ -2,7 +2,7 @@
2
2
  // is delivered to BOTH nodes with the other as the argument. type: 0 = enter (contact/overlap began),
3
3
  // 1 = exit (ended). Registered lazily on the first trigger/physics body or 'enter'/'exit' listener.
4
4
 
5
- import { nodeRegistry } from "./Node"
5
+ import { nodeRegistry } from "../Node"
6
6
 
7
7
  let registered = false
8
8
  export const ensurePhysicsEvents = (): void => {
@@ -31,26 +31,28 @@
31
31
  // headless, an older build) gets the same chunks as child Mesh nodes from gl/terrainMesh.ts — the
32
32
  // project runs everywhere, normal maps just read flat there.
33
33
 
34
- import { Node } from "./Node"
35
- import { Mesh } from "./Mesh"
36
- import { Model } from "./Model"
37
- import { Material } from "./Material"
38
- import { Texture } from "./Texture"
39
- import { Geometry } from "./Geometry"
40
- import { Shape } from "./Shape"
41
- import { Color, type ColorInput } from "../core/color"
42
- import { fetch } from "../runtime/fetch"
43
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
34
+ import { Node } from "../Node"
35
+ import { Mesh } from "../Mesh"
36
+ import { Model } from "../Model"
37
+ import { Material } from "../Material"
38
+ import { Texture } from "../Texture"
39
+ import { Geometry } from "../Geometry"
40
+ import { Shape } from "../physics/Shape"
41
+ import { Color, type ColorInput } from "../../core/color"
42
+ import { fetch } from "../../runtime/fetch"
43
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
44
44
  import {
45
45
  buildTerrainChunk, chunkCounts, faceNormalAt, gridRaycast, heightAt as gridHeightAt, normalAt as gridNormalAt, type TerrainGrid,
46
46
  } from "./terrainMesh"
47
47
 
48
48
  export type TerrainLayer = {
49
- /** Albedo texture (a URL / `asset()` handle, or a loaded Texture). Unset = white. */
49
+ /** Albedo texture (a URL / `asset()` handle, or a loaded Texture). Unset = white. Its ALPHA is the layer's roughness
50
+ * map (`lecodes assets terrain-pack --roughness` puts it there): an opaque albedo reads 1. */
50
51
  albedo?: string | Texture
51
52
  /** Metres per texture repeat. Default 8. */
52
53
  tiling?: number
53
- /** Perceptual roughness of the layer. Default 1. */
54
+ /** Perceptual roughness of the layer — a factor on the albedo's alpha (its roughness map), the whole value when the
55
+ * albedo is opaque. A terrain has no metallic: ground is a dielectric. Default 1. */
54
56
  roughness?: number
55
57
  /** Normal-map strength (0 = the layer's slot of the pack is not read). Default 0 — set 1 when the
56
58
  * layer has a normal map in `normals`. */
@@ -9,7 +9,7 @@
9
9
  // read wrong on those hosts — the accepted fallback cost.
10
10
  // Keep the hole / split rule in step with terrain.cpp buildChunk and Jolt's HeightFieldShape.
11
11
 
12
- import { Geometry } from "./Geometry"
12
+ import { Geometry } from "../Geometry"
13
13
 
14
14
  export type TerrainGrid = {
15
15
  sizeX: number