lecodes-sdk 1.0.0 → 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.
- package/dist/global.d.ts +18 -4
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +40 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +4 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +226 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Model.ts +167 -156
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +61 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -459
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/terrain/Terrain.ts +4 -2
- package/src/inject.ts +12 -2
- package/src/scene/defineScene.ts +72 -62
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
|
@@ -1,272 +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)
|
|
4
|
-
// attach from the pose the model is in (that pose is the joints' neutral for the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// rag.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/** The
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
parts:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|