lecodes-sdk 1.0.0 → 1.2.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 (94) hide show
  1. package/dist/global.d.ts +18 -4
  2. package/dist/inject.js +260 -361
  3. package/dist/types/animate/tween/Animation.d.ts +69 -0
  4. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  5. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  6. package/dist/types/animate/tween/easing.d.ts +29 -0
  7. package/dist/types/animate/tween/spec.d.ts +178 -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/Foliage.d.ts +47 -0
  12. package/dist/types/gl/Geometry.d.ts +24 -0
  13. package/dist/types/gl/Light.d.ts +28 -7
  14. package/dist/types/gl/Lightmap.d.ts +90 -60
  15. package/dist/types/gl/Material.d.ts +28 -20
  16. package/dist/types/gl/Model.d.ts +7 -5
  17. package/dist/types/gl/Node.d.ts +18 -0
  18. package/dist/types/gl/Particles.d.ts +55 -1
  19. package/dist/types/gl/Scene.d.ts +20 -0
  20. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  21. package/dist/types/gl/animation/Animator.d.ts +27 -0
  22. package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
  23. package/dist/types/gl/animation/IK.d.ts +86 -30
  24. package/dist/types/gl/animation/Locomotion.d.ts +52 -3
  25. package/dist/types/gl/animation/Warp.d.ts +2 -1
  26. package/dist/types/gl/animation/core.d.ts +35 -4
  27. package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
  28. package/dist/types/gl/terrain/Terrain.d.ts +12 -2
  29. package/dist/types/inject.d.ts +8 -2
  30. package/dist/types/runtime/input.d.ts +7 -0
  31. package/dist/types/scene/defineScene.d.ts +44 -32
  32. package/dist/types/ui/UIButton.d.ts +3 -1
  33. package/dist/types/ui/UIInput.d.ts +5 -1
  34. package/dist/types/ui/UINode.d.ts +24 -24
  35. package/dist/types.json +1 -1
  36. package/package.json +1 -1
  37. package/prompts/README.md +142 -142
  38. package/prompts/core-design.md +27 -4
  39. package/prompts/core.md +35 -6
  40. package/prompts/dist/2d-game.md +197 -408
  41. package/prompts/dist/3d-app.md +166 -491
  42. package/prompts/dist/ar-app.md +163 -373
  43. package/prompts/dist/design.md +87 -83
  44. package/prompts/dist/ui-app.md +136 -325
  45. package/prompts/select.ts +19 -4
  46. package/src/animate/tween/Animation.ts +378 -0
  47. package/src/animate/tween/Timeline.ts +175 -0
  48. package/src/animate/tween/animateValue.ts +100 -0
  49. package/src/animate/tween/easing.ts +172 -0
  50. package/src/animate/tween/spec.ts +479 -0
  51. package/src/audio/audio.ts +161 -161
  52. package/src/bridges.d.ts +235 -65
  53. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  54. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  55. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  56. package/src/compile/bundler.ts +34 -4
  57. package/src/compile/compileProject.ts +31 -1
  58. package/src/compile/detectEntry.ts +8 -3
  59. package/src/compile/index.ts +2 -0
  60. package/src/compile/serverSplit.ts +9 -3
  61. package/src/g2/Node2D.ts +38 -0
  62. package/src/g2/Sprite.ts +20 -1
  63. package/src/gl/Camera.ts +34 -1
  64. package/src/gl/CameraPlace.ts +52 -52
  65. package/src/gl/Foliage.ts +102 -0
  66. package/src/gl/Geometry.ts +393 -348
  67. package/src/gl/Light.ts +49 -16
  68. package/src/gl/Lightmap.ts +439 -275
  69. package/src/gl/Material.ts +59 -47
  70. package/src/gl/Mesh.ts +120 -120
  71. package/src/gl/Model.ts +23 -12
  72. package/src/gl/Node.ts +39 -0
  73. package/src/gl/Particles.ts +80 -2
  74. package/src/gl/Scene.ts +34 -1
  75. package/src/gl/animation/AnimationClip.ts +52 -0
  76. package/src/gl/animation/Animator.ts +42 -2
  77. package/src/gl/animation/DynamicBone.ts +35 -12
  78. package/src/gl/animation/IK.ts +173 -152
  79. package/src/gl/animation/Locomotion.ts +72 -8
  80. package/src/gl/animation/Playback.ts +5 -4
  81. package/src/gl/animation/Warp.ts +5 -2
  82. package/src/gl/animation/core.ts +65 -4
  83. package/src/gl/physics/Ragdoll.ts +451 -272
  84. package/src/gl/scenarios.ts +291 -291
  85. package/src/gl/terrain/Terrain.ts +33 -2
  86. package/src/inject.ts +236 -226
  87. package/src/runtime/input.ts +11 -0
  88. package/src/scene/defineScene.ts +72 -62
  89. package/src/scene/gizmos.ts +148 -148
  90. package/src/ui/UIButton.ts +2 -2
  91. package/src/ui/UIInput.ts +3 -3
  92. package/src/ui/UINode.ts +61 -36
  93. package/dist/types/animate/animate.d.ts +0 -20
  94. package/src/animate/animate.ts +0 -238
@@ -2,46 +2,102 @@ import { Aspect } from "../../core/Aspect";
2
2
  import { type Vec3Like } from "../../math/vec";
3
3
  import { Quat } from "../../math/quat";
4
4
  import type { Node } from "../Node";
5
+ import type { Core } from "./core";
5
6
  type Target = Node | Vec3Like;
6
- /** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })`
7
- * solves upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend
8
- * (knee/elbow) — a Node or world point. */
9
- declare class TwoBone extends Aspect<"ik", Node> {
10
- static readonly aspect = "ik";
11
- /** Where the end bone should be (Node or world position). */
12
- target?: Target;
13
- /** Bend hint — the mid joint is pulled toward it (Node or world position). */
14
- pole?: Target;
15
- /** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat.
16
- * Unset = the end bone keeps its animated local rotation (a hand on a grip, a foot on a slope
17
- * want it set). Blended by `weight · rotationWeight`. */
18
- rotation?: Node | Quat;
19
- /** 0–1 contribution of `rotation` (on top of `weight`). */
20
- rotationWeight: number;
7
+ /** What both chains share: the native handle, the parameter push, the readback. */
8
+ declare abstract class Chain<Name extends string> extends Aspect<Name, Node> {
9
+ protected _rig?: Core;
10
+ protected _ik: number;
11
+ protected _weight: number;
12
+ protected _enabled: boolean;
13
+ protected abstract readonly kind: 0 | 1;
21
14
  /** 0–1 contribution (blend in/out, e.g. foot planting only while grounded). */
22
- weight: number;
23
- /** Solve every frame (default). Set false to drive `solve()` yourself. */
24
- enabled: boolean;
25
- update(): void;
26
- /** One solve at the current pose. Safe to call manually (e.g. from a later-ordered aspect). */
27
- solve(): void;
15
+ get weight(): number;
16
+ set weight(v: number);
17
+ /** Solve every frame (default). */
18
+ get enabled(): boolean;
19
+ set enabled(v: boolean);
20
+ /** After the last solve: how far the end bone still is from its target, metres (0 = reached). */
21
+ get error(): number;
22
+ onAttach(): void;
23
+ onDetach(): void;
24
+ onReconfigure(): void;
25
+ protected params(): Float32Array;
26
+ /** Push the description to the engine (a no-op until attached). */
27
+ protected abstract push(): void;
28
+ }
29
+ /** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })` solves
30
+ * upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend (knee /
31
+ * elbow) — a Node or world point; `anchor` makes it ride a socket of the rig instead (see the file
32
+ * header). */
33
+ declare class TwoBone extends Chain<"ik"> {
34
+ static readonly aspect = "ik";
35
+ protected readonly kind: 0;
36
+ private _target?;
37
+ private _pole?;
38
+ private _rotation?;
39
+ private _rotationWeight;
40
+ private _anchor;
41
+ private _anchorTime;
42
+ private _anchorPin;
43
+ /** Where the end bone should be (a Node — read by the engine every frame — or a world position). */
44
+ get target(): Target | undefined;
45
+ set target(v: Target | undefined);
46
+ /** Bend hint — the mid joint is pulled toward it (a Node or a world point). */
47
+ get pole(): Target | undefined;
48
+ set pole(v: Target | undefined);
49
+ /** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat. Unset =
50
+ * the end bone keeps its animated rotation (a hand on a grip, a foot on a slope want it set). Blended
51
+ * by `weight · rotationWeight`. An anchored chain takes its rotation from the socket instead. */
52
+ get rotation(): Node | Quat | undefined;
53
+ set rotation(v: Node | Quat | undefined);
54
+ /** 0–1 contribution of `rotation` (on top of `weight`); for an anchored chain, of the socket's turn. */
55
+ get rotationWeight(): number;
56
+ set rotationWeight(v: number);
57
+ /** ANCHORED: the name of the LIVE socket the end bone rides (`anim.sockets(...)`) — its target and
58
+ * rotation come from the socket and the playing clips' anchor spans; `target` / `pole` / `rotation`
59
+ * are ignored. '' = a plain chain. */
60
+ get anchor(): string;
61
+ set anchor(v: string);
62
+ /** Anchored: how fast the hand moves between sockets — the halflife (s) of the spring the applied
63
+ * delta follows the wanted one with (default 0.05). */
64
+ get anchorTime(): number;
65
+ set anchorTime(v: number);
66
+ /** Anchored: 0..1 how hard the end bone is PINNED on its socket while the playing clips leave it there
67
+ * (a GAP in their spans — the hand is holding the gun). 1 = it sits exactly on the socket, so nothing
68
+ * in the pose below can slide it along the gun; 0 (default) = it keeps the take's own relation to the
69
+ * socket, moved by the delta. A clip's SPANS carry their own pin and default to 0, because a hand on
70
+ * its way to a magazine must keep the take's motion — so this knob is about holding, not reaching. */
71
+ get anchorPin(): number;
72
+ set anchorPin(v: number);
73
+ /** After the last solve: the anchor delta applied — how far (m) and how much (rad) the live socket
74
+ * moved the hand off the take's own pose. 0 for a plain chain. */
75
+ get anchorDelta(): {
76
+ distance: number;
77
+ angle: number;
78
+ };
79
+ protected push(): void;
28
80
  }
29
81
  /** Aim a bone at a target (head / eyes / turret). Attach to the bone itself:
30
82
  * `head.aspect(IK.LookAt, { target: camera, limit: 70 })`. `axis` is the bone's LOCAL forward
31
83
  * (the direction that should point at the target) — rigs differ; default −Z, Mixamo heads look
32
84
  * along +Z of the head bone in most exports, so pass `axis: [0, 0, 1]` there if it faces backwards. */
33
- declare class LookAt extends Aspect<"lookAt", Node> {
85
+ declare class LookAt extends Chain<"lookAt"> {
34
86
  static readonly aspect = "lookAt";
35
- target?: Target;
87
+ protected readonly kind: 1;
88
+ private _target?;
89
+ private _axis;
90
+ private _limit;
91
+ /** What to look at (a Node — read by the engine every frame — or a world position). */
92
+ get target(): Target | undefined;
93
+ set target(v: Target | undefined);
36
94
  /** The bone's local axis that should point at the target. */
37
- axis: Vec3Like;
95
+ get axis(): Vec3Like;
96
+ set axis(v: Vec3Like);
38
97
  /** Max deflection from the animated direction, in degrees (default 80). */
39
- limit: number;
40
- /** 0–1 contribution. */
41
- weight: number;
42
- enabled: boolean;
43
- update(): void;
44
- solve(): void;
98
+ get limit(): number;
99
+ set limit(v: number);
100
+ protected push(): void;
45
101
  }
46
102
  /** Inverse kinematics aspects — attach to bones (see file header). */
47
103
  export declare const IK: {
@@ -6,8 +6,9 @@ import type { WarpOptions } from "./Warp";
6
6
  import type { Node } from "../Node";
7
7
  /** How fast the character wants to go: the gait picks its clips and its speed. */
8
8
  export type Gait = "walk" | "run" | "sprint";
9
- /** What the engine shows: the idle, a start, the gait loop, a turn while moving, a stop, or a turn on the spot. */
10
- export type LocomotionState = "idle" | "start" | "move" | "turn" | "stop" | "spin";
9
+ /** What the engine shows: the idle, a start, the gait loop, a turn while moving, a stop, a turn on the spot —
10
+ * an ACTION (`act()`): a one-shot that owns the body until it hands over — or the STRAFE blend (`move({ strafe })`). */
11
+ export type LocomotionState = "idle" | "start" | "move" | "turn" | "stop" | "spin" | "action" | "strafe";
11
12
  /** Who moves the body:
12
13
  * `'hybrid'` (the default) — the clip's own recorded motion moves it; while the gait loop shows it
13
14
  * is ADJUSTED toward what the simulation wants by at most `tuning.adjust` metres a second, and a
@@ -42,6 +43,10 @@ export type LocomotionSet = {
42
43
  turns?: LocomotionClip[];
43
44
  /** turns on the spot — what a facing still owed by a standing body plays */
44
45
  spins?: LocomotionClip[];
46
+ /** the STRAFE set: the same gait recorded forward, backward, to either side and on the diagonals — the members of
47
+ * the directional blend `move(dir, { face, strafe: true })` shows. `angle` = the way a take travels in the body's
48
+ * frame (degrees, + = left); left out, the engine reads it off the take's root. */
49
+ strafes?: LocomotionClip[];
45
50
  };
46
51
  /** The tuning. Times are seconds, speeds m/s, angles degrees. */
47
52
  export type LocomotionTuning = {
@@ -199,6 +204,8 @@ export declare class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
199
204
  private _mag;
200
205
  private _gait;
201
206
  private _seq;
207
+ private _act;
208
+ private _strafe;
202
209
  private readonly _in;
203
210
  private readonly _out;
204
211
  private readonly _names;
@@ -225,11 +232,53 @@ export declare class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
225
232
  /** State the movement intent for this frame: a world direction (its length is the stick's pull,
226
233
  * clamped to 1) and the gait. `null` = no movement — the body stops, but it keeps facing where it
227
234
  * was last asked to, and turns there on the spot if it still owes the turn.
228
- * `face` states a facing of its own (aiming, strafing, a camera-relative shooter). */
235
+ * `face` states a facing of its own (aiming, a camera-relative shooter) — and with `strafe: true` the body MOVES
236
+ * where the direction points while it faces there: the loop becomes the directional blend of `set.strafes` (a
237
+ * guard, a lock-on), the facing is steered standing too, state `'strafe'`. Stated every frame like the rest. */
229
238
  move(direction: Vec3Like | null | undefined, options?: {
230
239
  gait?: Gait;
231
240
  face?: Vec3Like;
241
+ strafe?: boolean;
232
242
  }): void;
243
+ /** Play an ACTION: a one-shot that OWNS the body the way a start or a stop does — a roll, a backstep, a lunging
244
+ * attack. Its recording moves the body alone (in every displacement mode but `'data'`, where the host does),
245
+ * the steering and the selector stand by, and when it hands over the usual rules carry on: a direction held
246
+ * starts (or, still carrying speed, runs on), nothing held stands.
247
+ *
248
+ * `direction` is where its TRAVEL must go, world (`Vec3`, or a heading in degrees, 0 = +Z, + = toward +X). A
249
+ * clip travels some way relative to the body — forward, left, back; measured off its own root, or `angle`
250
+ * (degrees, + = left) — and the body is turned by what is left between that and `direction`, over `turn`
251
+ * seconds (default 0.12), so the travel lands exactly where it was pointed. Which clip to play is yours to
252
+ * pick: the one whose travel is nearest the direction IN THE BODY'S FRAME (`direction − facing`) leaves the
253
+ * least to turn. Left out = it plays where the body faces.
254
+ *
255
+ * `at` enters the clip that many seconds in (a wind-up skipped), `rate` plays it faster or slower (its metres
256
+ * stay, the time changes), `travel` takes only that share of its recorded travel (an attack aimed at a target:
257
+ * 0.3 to stop at the blade's length from one that is close, 1.3 to reach one a step too far — the pose is
258
+ * untouched, so keep it to a fast step; `{ share, at, after }` takes another share from clip time `at` on: a
259
+ * swing's take travels on after its cut, into a body that stands at the blade's length), `fade` is the transition into it and `out` the one out of it (default: the tuning's
260
+ * `blend`; an action ends in ITS stance, not the idle's, and a longer way out is what hides that), and `exit` is the
261
+ * clip time from which a MOVE intent may take over — the cancel window: along the way the body faces at once, a
262
+ * heading that needs a turn as soon as the body has slowed to where a start can answer; with nothing asked for it plays out, to
263
+ * its end (an action ends in its own recovery, at rest), and the body then STANDS THE WAY THE ACTION LEFT IT:
264
+ * the facing intent that outlives a released key is not a demand after an action — no turn on the spot.
265
+ * WHEN an action may be asked for (out of another one, out of a start) is the caller's rule: this cuts into
266
+ * whatever shows. `state` reads `'action'` while it does. False = no such clip. */
267
+ act(clip: string, options?: {
268
+ direction?: Vec3Like | number;
269
+ angle?: number;
270
+ turn?: number;
271
+ at?: number;
272
+ exit?: number;
273
+ fade?: number;
274
+ out?: number;
275
+ rate?: number;
276
+ travel?: number | {
277
+ share: number;
278
+ at: number;
279
+ after: number;
280
+ };
281
+ }): boolean;
233
282
  /** Face this way without moving — the facing intent on its own (`Vec3`, or a heading in degrees,
234
283
  * 0 = +Z, + = toward +X). A standing body turns to it on the spot. */
235
284
  face(direction: Vec3Like | number): void;
@@ -2,7 +2,8 @@
2
2
  export type WarpOptions = {
3
3
  /** Fit the stride to the speed the body actually travels at. `[min, max]` clamps the scale (default
4
4
  * 0.85…1.2). A CORRECTION: a pack whose takes already read right at the speeds it is played at wants
5
- * none of this; open the range for a pack that must cover speeds it was never recorded at. */
5
+ * none of this; open the range for a pack that must cover speeds it was never recorded at. The clip's
6
+ * own speed is its root motion — an in-place loop takes part only with a declared pace (see above). */
6
7
  stride?: boolean | [number, number];
7
8
  /** Turn the lower body toward where the body really travels; a number caps the turn in degrees
8
9
  * (default 20). The whole twist lives in one joint: a few degrees read as a lean, a lot as a broken
@@ -13,15 +13,24 @@ export type PlayOptions = {
13
13
  fade?: number;
14
14
  /** Transition seconds for the way in only (overrides `fade`). */
15
15
  fadeIn?: number;
16
- /** One-shots: the transition BACK to the loop at the end (overrides `fade`; `0` = cut at the end).
17
- * It starts this long before the clip ends — while it still plays — so the hand-over lands on the
18
- * last pose, not after it. On a layer with no loop the clip HOLDS its last frame (a rock that broke
19
- * stays broken) — `anim.stop({ fade })` is the way back to rest. */
16
+ /** One-shots: the transition BACK to the loop after the end (overrides `fade`; `0` = cut at the end).
17
+ * The clip plays to its last frame; the loop then takes over and the difference between the two poses
18
+ * (and their velocities) decays over this time, so no part of the clip is cut — to play less of it,
19
+ * give it an `end`. On a layer with no loop the clip HOLDS its last frame (a rock that broke stays
20
+ * broken) — `anim.stop({ fade })` is the way back to rest. */
20
21
  fadeOut?: number;
21
22
  /** Playback rate for this clip (default 1). */
22
23
  speed?: number;
23
24
  /** Rewind even if the clip is already playing (default: rewind only if it isn't). */
24
25
  restart?: boolean;
26
+ /** Play a WINDOW of the clip, seconds of the clip: enter at `start` (default 0; only when the play
27
+ * rewinds — see `restart`) and treat `end` (default the clip's end) as the end — the hand-over fires
28
+ * there, the last frame held there, events beyond it never fire. Only the swing of a longer take, the
29
+ * wind-up without the recovery. A window that lives in the clip table belongs in
30
+ * `AnimationClip.from(clip, { from, to })` instead. Not combined with `phase`. Hosts without the
31
+ * window play the whole clip. */
32
+ start?: number;
33
+ end?: number;
25
34
  /** Enter the clip at a point in the GAIT CYCLE instead of at its start: `'match'` = the phase the
26
35
  * layer shows right now (the walk's left foot is down → the turn clip starts where its left foot
27
36
  * is down too, so nothing slides), or a number 0–1. Clips without a gait cycle ignore it. */
@@ -302,6 +311,9 @@ export declare class Core {
302
311
  private clipNames;
303
312
  /** Create the native animator on demand. False = nothing to animate (warned). */
304
313
  ensure(): boolean;
314
+ /** The native animator for the RIG alone — an IK chain or a socket on a model that has no clips
315
+ * still needs the joint tree described to the engine. False = no transform hierarchy (warned). */
316
+ ensureRig(): boolean;
305
317
  destroy(): void;
306
318
  newLayer(options?: LayerOptions): LayerRec;
307
319
  addLayer(options: LayerOptions): Layer;
@@ -392,12 +404,31 @@ export declare class Core {
392
404
  /** Stride / orientation warping, as the engine's fixed-order parameter array. */
393
405
  private _warp?;
394
406
  setWarp(params: Float32Array): void;
407
+ /** The body's world velocity this frame (m/s) — the game side of the stride / orientation fit. Nothing
408
+ * to feed before the animator exists; hosts without the warp stage have no method. */
409
+ setMotion(vx: number, vy: number, vz: number): void;
395
410
  /** The feet stage (lock / ground IK), as the engine's fixed-order parameter array. */
396
411
  private _feetParams?;
397
412
  setFeetParams(params: Float32Array): void;
398
413
  /** One foot's lock state after this frame's evaluation into `out` (8 floats); false = no such foot
399
414
  * or no feet stage on this host. */
400
415
  footState(side: 0 | 1, out: Float32Array): boolean;
416
+ private static _warnedIk;
417
+ private hasIk;
418
+ /** A chain ending in `endEntity` (kind 0 two-bone: the bone, its parent, its grandparent; 1 look-at). 0 = none. */
419
+ ikCreate(kind: 0 | 1, endEntity: number): number;
420
+ ikDestroy(ik: number): void;
421
+ ikSet(ik: number, params: Float32Array): void;
422
+ ikNodes(ik: number, target: number, pole: number, rotation: number): void;
423
+ ikAnchor(ik: number, socket: string): void;
424
+ ikState(ik: number, out: Float32Array): boolean;
425
+ /** Sockets by "set/name", replayed when the animator is (re)created. */
426
+ private readonly _sockets;
427
+ /** Define a socket on joint `joint` (a bone's entity): a NODE socket (read by the engine every frame) or a
428
+ * fixed TRS in the joint's space. set "" = the live set, any other name = a donor's. */
429
+ setSocket(set: string, name: string, joint: number, node: number, trs: Float32Array | null): void;
430
+ removeSocket(set: string, name: string): void;
431
+ private pushSocket;
401
432
  private readonly _steps;
402
433
  onStepHandler(cb: StepHandler): void;
403
434
  offStepHandler(cb: StepHandler): void;
@@ -33,6 +33,31 @@ export interface RagdollPart {
33
33
  /** A one-way joint instead of the cone. */
34
34
  hinge?: RagdollHinge;
35
35
  }
36
+ /** How the body lies: on its back (the chest points up), prone (the chest points down), or on a side. */
37
+ export type RagdollFacing = "up" | "down" | "side";
38
+ export interface RagdollActivateOptions {
39
+ /** A LAUNCH added to every part's own motion (world m/s) — an explosion, a throw. The bones' own
40
+ * motion (the run, the swinging arms) is measured by the engine and needs no help. */
41
+ velocity?: Vec3Like;
42
+ /** The motors' strength from the first step (default: the current `strength`). */
43
+ strength?: number;
44
+ /** The root anchored to the animation from the first step (default: the current `anchored`). */
45
+ anchored?: boolean;
46
+ }
47
+ export interface RagdollDeactivateOptions {
48
+ /** Seconds the animator takes to transition OUT of the fallen pose into whatever plays next (a
49
+ * get-up take started right after, or the loop). 0 (default) = a cut. Move the model node under
50
+ * the hips before this call: the bones are handed back against its new transform. */
51
+ blend?: number;
52
+ }
53
+ export interface RagdollHitOptions {
54
+ /** How long the body stays powered before the bones go back to the animator (s, default 0.6). */
55
+ duration?: number;
56
+ /** The transition out of the reaction's last physical pose (s, default 0.25). */
57
+ blend?: number;
58
+ /** The motors' strength during the reaction (default 1). */
59
+ strength?: number;
60
+ }
36
61
  export declare class Ragdoll extends Aspect<"ragdoll", Node> {
37
62
  static readonly aspect = "ragdoll";
38
63
  /** The parts: `'humanoid'` (default) finds the standard bones by name; a list places bodies on any
@@ -51,36 +76,86 @@ export declare class Ragdoll extends Aspect<"ragdoll", Node> {
51
76
  * never shoves or blocks anything and costs nothing when they meet. `'all'`: a regular dynamic
52
77
  * body that bumps into everything (and gets kicked awake by everything). */
53
78
  collide: "static" | "all";
54
- /** Once every part has come to rest the parts turn static where they lie (default true): the pose
55
- * holds, nothing can wake a settled body and it costs the solver nothing — `deactivate()` /
56
- * `activate()` still work (activate makes it dynamic again). */
79
+ /** Once every part of a LIMP body has come to rest the parts turn static where they lie (default
80
+ * true): the pose holds, nothing can wake a settled body and it costs the solver nothing —
81
+ * `deactivate()` / `activate()` still work (activate makes it dynamic again). A driven or anchored
82
+ * body never freezes. */
57
83
  freeze: boolean;
58
- /** Seconds after `activate()` at which the body freezes whatever it is doing — a twitch on a slope
59
- * or a pile never sleeps on its own. 0 (default) = no cap. */
84
+ /** Seconds limp after which the body freezes whatever it is doing — a twitch on a slope or a pile
85
+ * never sleeps on its own. 0 (default) = no cap. */
60
86
  freezeAfter: number;
87
+ /** The joint motors: the position spring's frequency in Hz (default 6 — higher = a stiffer, quicker
88
+ * return to the animated pose) and damping ratio (default 1 = critical, no overshoot). */
89
+ driveFrequency: number;
90
+ driveDamping: number;
91
+ /** The motors' torque limit at strength 1, in N·m per kg of the part (default 12: ~180 N·m at the
92
+ * hips of an 80 kg body, ~25 at a forearm). Lower = a hit displaces a limb more before the
93
+ * animation wins it back. */
94
+ driveTorque: number;
95
+ /** Joint friction in N·m per kg of the part (default 0.3): a torque that resists any joint motion,
96
+ * motor or not — a limp body folds instead of flopping like jelly. */
97
+ jointFriction: number;
98
+ /** Where the joint limits come from. `'clips'` (default): measured from the animation — at the first
99
+ * activation that finds clips on the model's animator, every joint's swing and twist range over
100
+ * every bound clip becomes its limit (plus `limitsMargin` on each side), so the motors never
101
+ * target a pose the limits forbid and a limp body settles into poses the clips use; until clips
102
+ * are bound the part table's cones apply. `'table'`: the part table's cones and hinges only. */
103
+ limits: "clips" | "table";
104
+ /** Degrees added on each side of a learned range (default 10). */
105
+ limitsMargin: number;
61
106
  static fields: FieldMeta<Ragdoll>;
62
107
  private _id;
63
108
  private _active;
64
109
  private _bones;
110
+ private _strength;
111
+ private _anchored;
112
+ private _hipsFwd;
113
+ private _hipsUp;
114
+ private _ramp;
115
+ private _release;
65
116
  onAttach(): void;
66
117
  onDetach(): void;
67
- /** Hand the bones to physics from the pose they are in right now. `velocity` (world m/s) is given
68
- * to every part — pass the character's, so a running body keeps travelling. */
69
- activate(opts?: {
70
- velocity?: Vec3Like;
71
- }): boolean;
72
- /** Take the bones back: the animator's pose shows again from the next frame. */
73
- deactivate(): void;
118
+ /** Hand the bones to physics from the pose they are in right now, each moving as it did over the
119
+ * last two frames (the engine measures: a runner keeps travelling, a swinging arm keeps swinging). */
120
+ activate(opts?: RagdollActivateOptions): boolean;
121
+ /** Take the bones back: the animator's pose shows again from the next frame — through a `blend`
122
+ * out of the fallen pose when asked (play the get-up take right after, with its own fade: it
123
+ * starts from where the body lies). */
124
+ deactivate(opts?: RagdollDeactivateOptions): void;
74
125
  /** Physics owns the bones right now. */
75
126
  get active(): boolean;
76
127
  /** Active and every part asleep (or frozen) — the body has come to rest. */
77
128
  get settled(): boolean;
129
+ /** The joint motors' strength, 0..1: how hard every joint is pulled toward the pose the animator
130
+ * shows (0 = off — a limp body). Kept across activations. */
131
+ get strength(): number;
132
+ set strength(v: number);
133
+ /** The hips follow the animation kinematically (the body stands in its clip while physics moves the
134
+ * limbs — hit reactions, a stagger). Off = the hips are a free body (a fall). */
135
+ get anchored(): boolean;
136
+ set anchored(v: boolean);
137
+ /** The joints give way: the strength ramps to 0 over `seconds` and the anchor comes off — a body
138
+ * shot mid-stride keeps its pose for a moment and then collapses, instead of switching off. */
139
+ goLimp(seconds?: number): void;
140
+ /** A flinch: the body powers up anchored to its animation (if it is not already active), the struck
141
+ * part gets the impulse (N·s, at a world point), and after `duration` the bones go back to the
142
+ * animator through `blend`. On a limp body (a corpse) it is just the impulse. */
143
+ hit(bone: string, v: Vec3Like, at?: Vec3Like, opts?: RagdollHitOptions): boolean;
78
144
  /** The root part's bone (the hips): where the body is. */
79
145
  get root(): Node | null;
146
+ /** How the body lies: `'up'` on its back (the chest points up), `'down'` prone, `'side'` otherwise.
147
+ * Read while it is down, to pick the get-up take. */
148
+ get facing(): RagdollFacing;
149
+ /** Where the head points along the ground, as a yaw in degrees (the model node's `eulerAngles` y
150
+ * that faces that way): where a get-up take that rises head-first ends up facing. A body still
151
+ * upright answers with the way its chest faces. */
152
+ get heading(): number;
80
153
  /** The bones that carry a part, in order. */
81
154
  get bones(): string[];
82
155
  /** The rigid-body id of a part (0 if none) — for the plain body calls. */
83
156
  bodyOf(bone: string): number;
84
157
  /** Push one part: an impulse in N·s, at a world point (a hit) or through its centre. Wakes the body. */
85
158
  impulse(bone: string, v: Vec3Like, at?: Vec3Like): boolean;
159
+ /** The strength ramp (goLimp) and the hit reaction's hand-back. */
160
+ update(dt: number): void;
86
161
  }
@@ -5,11 +5,13 @@ import { Geometry } from "../Geometry";
5
5
  import { type ColorInput } from "../../core/color";
6
6
  import { Vec3, type Vec3Like } from "../../math/vec";
7
7
  export type TerrainLayer = {
8
- /** Albedo texture (a URL / `asset()` handle, or a loaded Texture). Unset = white. */
8
+ /** Albedo texture (a URL / `asset()` handle, or a loaded Texture). Unset = white. Its ALPHA is the layer's roughness
9
+ * map (`lecodes assets terrain-pack --roughness` puts it there): an opaque albedo reads 1. */
9
10
  albedo?: string | Texture;
10
11
  /** Metres per texture repeat. Default 8. */
11
12
  tiling?: number;
12
- /** Perceptual roughness of the layer. Default 1. */
13
+ /** Perceptual roughness of the layer — a factor on the albedo's alpha (its roughness map), the whole value when the
14
+ * albedo is opaque. A terrain has no metallic: ground is a dielectric. Default 1. */
13
15
  roughness?: number;
14
16
  /** Normal-map strength (0 = the layer's slot of the pack is not read). Default 0 — set 1 when the
15
17
  * layer has a normal map in `normals`. */
@@ -174,6 +176,14 @@ export declare class Terrain {
174
176
  heightAt(x: number, z: number): number;
175
177
  /** The drawn triangle's normal at a local (x, z) (unit Vec3, +Y up). */
176
178
  normalAt(x: number, z: number): Vec3;
179
+ /** The LAYER WEIGHTS at a local (x, z) — what the splat shader blends there: the control map read bilinearly
180
+ * between its samples and normalised to sum 1 (`[1, 0, 0, 0]` where the map is empty, as the shader has
181
+ * it). Index = the layer's in `layers`. Outside the grid the edge answers. This is how a game asks WHAT the
182
+ * ground is under a point — footprints in the sand and none on the cobble, a footstep sound per layer, dust
183
+ * by surface — and the blend is already in the numbers, so a transition is a fade and not a line. */
184
+ weightsAt(x: number, z: number): [number, number, number, number];
185
+ /** The heaviest layer's index at a local (x, z) — `weightsAt` when only "which one" is asked. */
186
+ layerAt(x: number, z: number): number;
177
187
  /** The smooth (vertex) normal at integer sample (ix, iz). */
178
188
  sampleNormal(ix: number, iz: number): Vec3;
179
189
  /** Extent in local metres: `[width, depth]`. */
@@ -58,8 +58,12 @@ export { Presentable } from "./ui/UI";
58
58
  export type { PresentOptions, Transition, TransitionName, TransitionSpec, TransitionTransform } from "./ui/UI";
59
59
  export type { UINode, UINodeChild } from "./ui/UINode";
60
60
  export { theme, type ThemeValues, type ThemeAccessors } from "./ui/theme";
61
- export { animate, animateMat4, stopAnimation, pauseAnimation, resumeAnimation } from "./animate/animate";
61
+ export { animate, type AnimateOptions, type AnimateValue, type AnimateOut } from "./animate/tween/animateValue";
62
62
  export { cubicBezier } from "./animate/bezier";
63
+ export { Timeline, type TimelineOptions, type TimelineAddOptions, type TimelinePosition } from "./animate/tween/Timeline";
64
+ export { type Animation } from "./animate/tween/Animation";
65
+ export { type TweenMeta } from "./animate/tween/spec";
66
+ export { type EasingInput } from "./animate/tween/easing";
63
67
  export { easeIn, easeOut, easeInOut } from "./animate/easings";
64
68
  export { QRScanner } from "./plugins/qr";
65
69
  export { CameraView, type CameraFacing } from "./plugins/camera";
@@ -96,7 +100,7 @@ export { Vehicle } from "./gl/vehicle/Vehicle";
96
100
  export type { DriveLayout, DifferentialMode, EngineConfig, SteeringConfig, SteeringFeedback, SteeringDriver, AeroConfig } from "./gl/vehicle/Vehicle";
97
101
  export { Wheel, Tire, resolveTire } from "./gl/vehicle/Wheel";
98
102
  export { Ragdoll } from "./gl/physics/Ragdoll";
99
- export type { RagdollPart, RagdollHinge } from "./gl/physics/Ragdoll";
103
+ export type { RagdollPart, RagdollHinge, RagdollFacing, RagdollActivateOptions, RagdollDeactivateOptions, RagdollHitOptions } from "./gl/physics/Ragdoll";
100
104
  export type { Point, WheelSuspension } from "./gl/vehicle/Wheel";
101
105
  export { AnimationClip } from "./gl/animation/AnimationClip";
102
106
  export type { ClipDef, ClipTrackDef, ClipKey } from "./gl/animation/AnimationClip";
@@ -117,6 +121,8 @@ export type { DynamicBoneCurve, DynamicBoneFloor, DynamicBoneColliders } from ".
117
121
  export type { IKTwoBone, IKLookAt } from "./gl/animation/IK";
118
122
  export { Light } from "./gl/Light";
119
123
  export { Lightmap } from "./gl/Lightmap";
124
+ export { Foliage } from "./gl/Foliage";
125
+ export type { FoliageOptions, FoliageWind } from "./gl/Foliage";
120
126
  export { Terrain } from "./gl/terrain/Terrain";
121
127
  export type { TerrainOptions, TerrainLayer, TerrainRegion, ConformOptions, RibbonOptions, AutoPaintRules, SculptOptions, TerrainHit, TerrainSnapshot } from "./gl/terrain/Terrain";
122
128
  export { NavMesh, NavCrowd } from "./gl/nav/NavMesh";
@@ -82,6 +82,13 @@ export interface GamepadState {
82
82
  axis(name: GamepadAxisName, deadzone?: number): number;
83
83
  /** Held? Same as `Input.key(code, index)`. */
84
84
  button(code: string): boolean;
85
+ /** Rumble: `strong` = the heavy low-frequency motor, `weak` = the light high-frequency one, both
86
+ * 0..1, for `durationMs` (default 200, hosts cap at 5000). A new call replaces the running
87
+ * effect. The host stops the motors by itself — after the duration, on focus loss and when the
88
+ * project is swapped — so there is nothing to clean up. False when the pad or host has no motors. */
89
+ rumble(strong: number, weak?: number, durationMs?: number): boolean;
90
+ /** Stop the running rumble now. */
91
+ stopRumble(): void;
85
92
  }
86
93
  export declare const Input: {
87
94
  /**
@@ -5,6 +5,8 @@ import { Scene, type SceneOptions } from "../gl/Scene";
5
5
  import { Node } from "../gl/Node";
6
6
  import { Mesh } from "../gl/Mesh";
7
7
  import { Model } from "../gl/Model";
8
+ import { type LightmapTransmit } from "../gl/Lightmap";
9
+ import { type FoliageOptions } from "../gl/Foliage";
8
10
  import { type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain";
9
11
  import { Light, type SunOptions } from "../gl/Light";
10
12
  import { type MaterialDef } from "./material";
@@ -130,6 +132,12 @@ export type SceneNodeDef = {
130
132
  * Default: static unless a `Physics` aspect moves the node (`dynamic` — Physics' default — or
131
133
  * `kinematic`). Set it only to override that rule; prefab subtrees inherit the verdict. */
132
134
  lightmap?: boolean;
135
+ /** `model` nodes: vegetation — load through the FOLIAGE tier (wind, touch bending, per-copy tint; see
136
+ * `Foliage`). `{ fade: [start, end] }` also thins the cards out over that distance range (m) and drops the
137
+ * copy past `end` — for ground cover. The fade is per ASSET: every node of the same GLB shares it. */
138
+ foliage?: boolean | {
139
+ fade?: [number, number];
140
+ };
133
141
  /** Navigation (a scene with `env.navmesh`, see navmesh.md): every STATIC body is walkable by
134
142
  * default. `false` leaves this one out of the bake; `'unwalkable'` cuts its footprint out
135
143
  * (nobody stands on or crosses it). */
@@ -144,41 +152,44 @@ export type SceneCameraDef = CameraProjectionDef & {
144
152
  /** Point the camera looks at. */
145
153
  target?: Vec3Like;
146
154
  };
147
- /** `env.lightmap` — the level's baked lighting (see lightmap.md): the two files
148
- * `lecodes lightmap bake` writes, plus how the bake is applied. Absent = real-time only. */
155
+ /** `env.lightmap` — the level's baked lighting (packages/creator-bake): the files `lecodes lightmap bake`
156
+ * writes. Absent = real-time only. */
149
157
  export type SceneLightmapDef = {
150
- /** `asset('./assets/lightmap/lightmap.bake')` */
158
+ /** `asset('./assets/lightmap/level.bake')` */
151
159
  data: string;
152
- /** `asset('./assets/lightmap/lightmap.ktx2')` — or, for a bake that took more than one atlas PAGE
153
- * (`lecodes lightmap bake --pages`), every page in order: `[asset('x.ktx2'), asset('x_1.ktx2')]`. */
154
- texture: string | string[];
155
- /** `asset('./assets/lightmap/lightmap-light.ktx2')` (pages like `texture`) — the point lights' baked
156
- * irradiance the bake writes when the level holds point lights; the real-time lights it carries are
157
- * switched off once it applies. Absent = the lamps stay real-time. */
158
- light?: string | string[];
159
- /** `asset('./assets/lightmap/lightmap.volume')` — the light VOLUME for everything that moves (the
160
- * same bake writes it): dynamic nodes, and every model loaded in code while the level runs, read
161
- * the baked sun/ambient at their own position. Absent = movers stay real-time only. */
160
+ /** `asset('./assets/lightmap/level-light.ktx2')` — or, for a bake that took more than one atlas PAGE
161
+ * (`lecodes lightmap bake --pages`), every page in order: `[asset('x-light.ktx2'), asset('x-light_1.ktx2')]`. */
162
+ light: string | string[];
163
+ /** `asset('./assets/lightmap/level-aux.ktx2')` (pages like `light`) — sun / sky visibility + light direction. */
164
+ aux: string | string[];
165
+ /** DEBUG: the page sets a `lecodes lightmap bake --split` wrote (`<stem>-direct[_n].ktx2`, `<stem>-indirect[_n].ktx2`),
166
+ * pages like `light` — the `Lightmap.debug("direct" | "indirect")` views bind them in place of `light`. */
167
+ direct?: string | string[];
168
+ indirect?: string | string[];
169
+ /** `asset('./assets/lightmap/level-probes.ktx2')` — the bake's reflection probes (one prefiltered cubemap of the level's
170
+ * radiance per probe; every static reflects the nearest). Absent = statics reflect the sky's SH alone. */
171
+ probes?: string;
172
+ /** FOR THE BAKE: the radiance in cd / m² of emission 1.0 on this level (emissive factor x strength x map) - every lit
173
+ * region of an emissive surface bakes as a rectangle lamp. An imported pack's emission is a LOOK (the school's panels
174
+ * say 10); this is where the level says what they give: a 1.3 m ceiling panel of ~8000 lm is ~1600 nits. Default: what
175
+ * the camera makes of 1.0 (`1.2 x 2^EV100`). 0 = emissive surfaces light nothing. `--emissive-nits` overrides. */
176
+ emissiveNits?: number;
177
+ /** `false` = the emissive surfaces light nothing in the bake - the level is lit by its lamps (`Light.point` with a
178
+ * `bakeArea` next to every panel, the Unity packs' way) and the panels are decoration; `emissiveNits` then only says how
179
+ * bright their glow is DRAWN (scene-referred: nits through the camera's exposure). Default true. */
180
+ emissiveBake?: boolean;
181
+ /** FOR THE BAKE: materials that let light THROUGH them, by glTF material name (`"name*"` = every name with that start) →
182
+ * the share of a shadow ray that passes, tinted by the material's base colour x map: `{ mat_awning: 0.3 }` puts the
183
+ * awning's warm, patterned light on the sand. The surface stays opaque - baked, drawn as before, a shadow caster.
184
+ * `{ through, diffuse }` splits it: `through` goes straight (the picture of the map), `diffuse` is scattered by the
185
+ * fibres (the underside glows, a soft fill with no picture) - dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
186
+ transmit?: Record<string, LightmapTransmit>;
187
+ /** `asset('./assets/lightmap/level.lgrid')` — THE LIGHT GRID for movers: every node a Physics aspect or a
188
+ * CharacterController moves (and what code marks with `Lightmap.track`) takes its ambient light from the bake at the
189
+ * place it is at, whatever its shader. Absent = movers keep the scene's IBL. */
162
190
  volume?: string;
163
- /** 1 = baked sun shadows at full strength, 0 = ambient occlusion only. Default 1. */
164
- sunStrength?: number;
165
- /** Multiplier on the ambient share in the shadow math (1 = filament's own darkness). Default 1. */
166
- ambientScale?: number;
167
- /** How much of the baked ambient occlusion applies: 1 = all of it, 0 = none. An interior lit by its
168
- * ambient probe alone can want less than the geometric truth. Default 1. */
169
- aoStrength?: number;
170
- /** Multiplier on the baked point lights' irradiance (default 1) - dial the lamps up or down
171
- * without a re-bake, since their irradiance lives in the atlas. Scales the light volume with the
172
- * atlas, so movers match the statics. See `LightmapLoadOptions.lightBoost`. */
173
- lightBoost?: number;
174
- /** Multiplier on the light VOLUME's irradiance alone (default = `lightBoost`). Since the volume is an
175
- * ambient cube (2026-08-31) movers shade like the statics and the default is right; it stays as a
176
- * trim. See `LightmapLoadOptions.volumeBoost`. */
177
- volumeBoost?: number;
178
- /** Occluder-only statics (props without lightmap UVs): `"baked"` (default) = their shadow is in the
179
- * atlas, they stop casting in real time and the shadow pass draws only the movers; `"realtime"` =
180
- * they keep casting (their shadow on other non-receiver props). See `LightmapLoadOptions.occluderShadows`. */
181
- occluderShadows?: "baked" | "realtime";
191
+ /** A DEBUG multiplier on the atlas' lux (default 1). */
192
+ lightScale?: number;
182
193
  };
183
194
  /** `env.navmesh` — the level's navigation mesh (see navmesh.md): the file `lecodes navmesh bake`
184
195
  * writes, the agent size it is built for, and the named areas. Absent = no navigation. */
@@ -199,6 +210,7 @@ export type SceneDef = {
199
210
  env?: SceneOptions & {
200
211
  lightmap?: SceneLightmapDef;
201
212
  navmesh?: SceneNavmeshDef;
213
+ foliage?: FoliageOptions;
202
214
  };
203
215
  camera?: SceneCameraDef;
204
216
  nodes?: Record<string, SceneNodeDef>;