lecodes-sdk 1.1.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.
@@ -37,8 +37,9 @@ import type { Node } from "../Node"
37
37
 
38
38
  /** How fast the character wants to go: the gait picks its clips and its speed. */
39
39
  export type Gait = "walk" | "run" | "sprint"
40
- /** What the engine shows: the idle, a start, the gait loop, a turn while moving, a stop, or a turn on the spot. */
41
- export type LocomotionState = "idle" | "start" | "move" | "turn" | "stop" | "spin"
40
+ /** What the engine shows: the idle, a start, the gait loop, a turn while moving, a stop, a turn on the spot —
41
+ * an ACTION (`act()`): a one-shot that owns the body until it hands over — or the STRAFE blend (`move({ strafe })`). */
42
+ export type LocomotionState = "idle" | "start" | "move" | "turn" | "stop" | "spin" | "action" | "strafe"
42
43
  /** Who moves the body:
43
44
  * `'hybrid'` (the default) — the clip's own recorded motion moves it; while the gait loop shows it
44
45
  * is ADJUSTED toward what the simulation wants by at most `tuning.adjust` metres a second, and a
@@ -70,6 +71,10 @@ export type LocomotionSet = {
70
71
  turns?: LocomotionClip[]
71
72
  /** turns on the spot — what a facing still owed by a standing body plays */
72
73
  spins?: LocomotionClip[]
74
+ /** the STRAFE set: the same gait recorded forward, backward, to either side and on the diagonals — the members of
75
+ * the directional blend `move(dir, { face, strafe: true })` shows. `angle` = the way a take travels in the body's
76
+ * frame (degrees, + = left); left out, the engine reads it off the take's root. */
77
+ strafes?: LocomotionClip[]
73
78
  }
74
79
 
75
80
  /** The tuning. Times are seconds, speeds m/s, angles degrees. */
@@ -201,7 +206,7 @@ export type StopCandidate = {
201
206
 
202
207
  const STOP_ROW = 9
203
208
  const STOP_ROWS = 8
204
- const IN = { DIR_X: 0, DIR_Z: 1, MAG: 2, FACE_X: 3, FACE_Z: 4, GAIT: 5, COUNT: 6 }
209
+ const IN = { DIR_X: 0, DIR_Z: 1, MAG: 2, FACE_X: 3, FACE_Z: 4, GAIT: 5, ACT_SEQ: 6, ACT_SLOT: 7, ACT_DIR_X: 8, ACT_DIR_Z: 9, ACT_ANGLE: 10, ACT_TURN: 11, ACT_AT: 12, ACT_EXIT: 13, ACT_FADE: 14, ACT_RATE: 15, ACT_OUT: 16, ACT_TRAVEL: 17, ACT_TRAVEL_AT: 18, ACT_TRAVEL_AFTER: 19, STRAFE: 20, COUNT: 21 }
205
210
  const OUT = { STATE: 0, SPEED: 1, VEL_X: 2, VEL_Y: 3, VEL_Z: 4, YAW: 5, YAW_RATE: 6, PHASE: 7, SEQ: 8, PRED: 9, YAW_LEFT: 21, CLIP_TIME: 22, EXIT_AT: 23, STOP_OFF: 24, STOP_STEPS: 25, STOP_WAIT: 26, COUNT: 27 }
206
211
  const P = {
207
212
  HALFLIFE_WALK: 0, HALFLIFE_RUN: 1, HALFLIFE_FACING: 2, SPEED_WALK: 3, SPEED_RUN: 4, SPEED_SPRINT: 5,
@@ -210,8 +215,8 @@ const P = {
210
215
  HALFLIFE_STOP: 20, ADJUST: 21, YAW_WARP: 22, EXIT_TOL: 23, RESUME_SPEED: 24, EXIT_STEPS: 25, STOP_EARLY: 26, STOP_STEPS: 27, STOP_FIT: 28, STOP_POSE: 29,
211
216
  STRIDE_SHARE: 30, STRIDE_MIN: 31, STRIDE_MAX: 32, WARP_STRIDE: 33, WARP_RATE: 34, COUNT: 35,
212
217
  }
213
- const KIND = { IDLE: 0, GAIT: 1, START: 2, STOP: 3, TURN: 4, MATCH: 5, SPIN: 6 }
214
- const STATES: LocomotionState[] = [ "idle", "start", "move", "turn", "stop", "spin" ]
218
+ const KIND = { IDLE: 0, GAIT: 1, START: 2, STOP: 3, TURN: 4, MATCH: 5, SPIN: 6, STRAFE: 8 }
219
+ const STATES: LocomotionState[] = [ "idle", "start", "move", "turn", "stop", "spin", "action", "strafe" ]
215
220
  const GAITS: Gait[] = [ "walk", "run", "sprint" ]
216
221
  const RAD2DEG = 180 / Math.PI
217
222
 
@@ -249,6 +254,8 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
249
254
  private _mag = 0
250
255
  private _gait: Gait = "walk"
251
256
  private _seq = -1
257
+ private _act = 0
258
+ private _strafe = false
252
259
  private readonly _in = new Float32Array(IN.COUNT)
253
260
  private readonly _out = new Float32Array(OUT.COUNT)
254
261
  private readonly _names = new Map<number, string>()
@@ -290,6 +297,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
290
297
  for (const c of this.set.stops ?? []) this.enter(c, KIND.STOP)
291
298
  for (const c of this.set.turns ?? []) this.enter(c, KIND.TURN)
292
299
  for (const c of this.set.spins ?? []) this.enter(c, KIND.SPIN)
300
+ for (const c of this.set.strafes ?? []) this.enter(c, KIND.STRAFE)
293
301
  if (this.warp !== undefined) anim.warp.set(this.warp)
294
302
  anim.feet.set(this.feet === undefined ? { lock: true } : this.feet === true ? { lock: true, ik: true } : this.feet === false ? { lock: false, ik: false } : this.feet)
295
303
  this.pushTuning()
@@ -306,7 +314,8 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
306
314
  this._names.set(slot, n)
307
315
  const o = typeof clip === "string" ? undefined : clip
308
316
  const measured = anim.clipInfo(n)
309
- const angle = o?.angle ?? (measured ? measured.turn * RAD2DEG : 0)
317
+ // (a strafe member's angle is the way it TRAVELS, which the engine reads off its root: 1000 = measure)
318
+ const angle = o?.angle ?? (kind === KIND.STRAFE ? 1000 : measured ? measured.turn * RAD2DEG : 0)
310
319
  const gait = kind === KIND.GAIT ? -1 : o?.gait ? GAITS.indexOf(o.gait) : -1
311
320
  _creator.locoSetEntry(this._loco, slot, kind, angle, o?.speed ?? 0, gait)
312
321
  }
@@ -379,8 +388,11 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
379
388
  /** State the movement intent for this frame: a world direction (its length is the stick's pull,
380
389
  * clamped to 1) and the gait. `null` = no movement — the body stops, but it keeps facing where it
381
390
  * was last asked to, and turns there on the spot if it still owes the turn.
382
- * `face` states a facing of its own (aiming, strafing, a camera-relative shooter). */
383
- move(direction: Vec3Like | null | undefined, options: { gait?: Gait, face?: Vec3Like } = {}): void {
391
+ * `face` states a facing of its own (aiming, a camera-relative shooter) — and with `strafe: true` the body MOVES
392
+ * where the direction points while it faces there: the loop becomes the directional blend of `set.strafes` (a
393
+ * guard, a lock-on), the facing is steered standing too, state `'strafe'`. Stated every frame like the rest. */
394
+ move(direction: Vec3Like | null | undefined, options: { gait?: Gait, face?: Vec3Like, strafe?: boolean } = {}): void {
395
+ this._strafe = !!options.strafe
384
396
  const dx = direction ? cx(direction) : 0, dz = direction ? cz(direction) : 0
385
397
  const len = Math.hypot(dx, dz)
386
398
  if (len > 1e-4) {
@@ -395,6 +407,57 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
395
407
  if (options.gait) this._gait = options.gait
396
408
  }
397
409
 
410
+ /** Play an ACTION: a one-shot that OWNS the body the way a start or a stop does — a roll, a backstep, a lunging
411
+ * attack. Its recording moves the body alone (in every displacement mode but `'data'`, where the host does),
412
+ * the steering and the selector stand by, and when it hands over the usual rules carry on: a direction held
413
+ * starts (or, still carrying speed, runs on), nothing held stands.
414
+ *
415
+ * `direction` is where its TRAVEL must go, world (`Vec3`, or a heading in degrees, 0 = +Z, + = toward +X). A
416
+ * clip travels some way relative to the body — forward, left, back; measured off its own root, or `angle`
417
+ * (degrees, + = left) — and the body is turned by what is left between that and `direction`, over `turn`
418
+ * seconds (default 0.12), so the travel lands exactly where it was pointed. Which clip to play is yours to
419
+ * pick: the one whose travel is nearest the direction IN THE BODY'S FRAME (`direction − facing`) leaves the
420
+ * least to turn. Left out = it plays where the body faces.
421
+ *
422
+ * `at` enters the clip that many seconds in (a wind-up skipped), `rate` plays it faster or slower (its metres
423
+ * stay, the time changes), `travel` takes only that share of its recorded travel (an attack aimed at a target:
424
+ * 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
425
+ * untouched, so keep it to a fast step; `{ share, at, after }` takes another share from clip time `at` on: a
426
+ * 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
427
+ * `blend`; an action ends in ITS stance, not the idle's, and a longer way out is what hides that), and `exit` is the
428
+ * clip time from which a MOVE intent may take over — the cancel window: along the way the body faces at once, a
429
+ * 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
430
+ * its end (an action ends in its own recovery, at rest), and the body then STANDS THE WAY THE ACTION LEFT IT:
431
+ * the facing intent that outlives a released key is not a demand after an action — no turn on the spot.
432
+ * WHEN an action may be asked for (out of another one, out of a start) is the caller's rule: this cuts into
433
+ * whatever shows. `state` reads `'action'` while it does. False = no such clip. */
434
+ act(clip: string, options: { direction?: Vec3Like | number, angle?: number, turn?: number, at?: number, exit?: number, fade?: number, out?: number, rate?: number, travel?: number | { share: number, at: number, after: number } } = {}): boolean {
435
+ const anim = this.anim
436
+ if (!anim || !this._loco) return false
437
+ const slot = anim._slot(clip)
438
+ if (slot < 0) { console.warn(`Locomotion: no clip '${clip}' on ${this.node.name || "the character"}`); return false }
439
+ const i = this._in
440
+ let dx = 0, dz = 0
441
+ const d = options.direction
442
+ if (typeof d === "number") { dx = Math.sin(d / RAD2DEG); dz = Math.cos(d / RAD2DEG) }
443
+ else if (d) { dx = cx(d); dz = cz(d) }
444
+ i[IN.ACT_SEQ] = ++this._act
445
+ i[IN.ACT_SLOT] = slot
446
+ i[IN.ACT_DIR_X] = dx; i[IN.ACT_DIR_Z] = dz
447
+ i[IN.ACT_ANGLE] = options.angle ?? 1000 // 1000 = measure the clip's own travel
448
+ i[IN.ACT_TURN] = options.turn ?? 0.12
449
+ i[IN.ACT_AT] = options.at ?? 0
450
+ i[IN.ACT_EXIT] = options.exit ?? 0
451
+ i[IN.ACT_FADE] = options.fade ?? 0
452
+ i[IN.ACT_RATE] = options.rate ?? 0
453
+ i[IN.ACT_OUT] = options.out ?? 0
454
+ const tr = options.travel
455
+ i[IN.ACT_TRAVEL] = typeof tr === "number" ? tr : tr?.share ?? 0
456
+ i[IN.ACT_TRAVEL_AT] = typeof tr === "object" ? tr.at : 0
457
+ i[IN.ACT_TRAVEL_AFTER] = typeof tr === "object" ? tr.after : 0
458
+ return true
459
+ }
460
+
398
461
  /** Face this way without moving — the facing intent on its own (`Vec3`, or a heading in degrees,
399
462
  * 0 = +Z, + = toward +X). A standing body turns to it on the spot. */
400
463
  face(direction: Vec3Like | number): void {
@@ -477,6 +540,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
477
540
  i[IN.DIR_X] = this._dir.x; i[IN.DIR_Z] = this._dir.z; i[IN.MAG] = this._mag
478
541
  i[IN.FACE_X] = this._face.x; i[IN.FACE_Z] = this._face.z
479
542
  i[IN.GAIT] = Math.max(0, GAITS.indexOf(this._gait))
543
+ i[IN.STRAFE] = this._strafe ? 1 : 0
480
544
  _creator.locoSetInput(this._loco, i)
481
545
  _creator.locoUpdate(this._loco, dt)
482
546
  _creator.locoRead(this._loco, this._out)