@volter/editor-model-play 0.5.195 → 0.5.196

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/play-script.ts +55 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/editor-model-play",
3
- "version": "0.5.195",
3
+ "version": "0.5.196",
4
4
  "author": "Volter AI, Inc.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  ]
29
29
  },
30
30
  "dependencies": {
31
- "@volter/editor-sdk": "0.5.195"
31
+ "@volter/editor-sdk": "0.5.196"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": "^19.0.0",
@@ -86,6 +86,7 @@ import {
86
86
  takeModelPlayStep,
87
87
  } from './model-play';
88
88
  import { beginModelPlayLog } from './play-log';
89
+ import type { DocumentPlayAnimation } from '@volter/editor-sdk/kit/document-play-extension';
89
90
  import { materialOverrides } from './play-materials';
90
91
  interface PlayComposition {
91
92
  readonly entries: readonly string[];
@@ -157,6 +158,29 @@ export interface ModelPlayContext {
157
158
  * });
158
159
  */
159
160
  autoplay(bot: ModelPlayAutoplayController | Readonly<Record<string, ModelPlayAutoplayController>> | null): void;
161
+ /**
162
+ * Set the action an object's armature plays, as Blender's `animation_data.action` does: any
163
+ * action in the file that animates that armature's bones, by name. `object` is the armature, its
164
+ * skinned mesh, or an object above or below them, or its name. Each armature starts a run playing
165
+ * the action the file assigns it. Setting another crossfades over `fade` seconds (0.2), loops
166
+ * unless `loop: false` (which holds the last frame), and plays at `speed`; setting the action
167
+ * already playing does nothing, so a script may set it every update from the character's state.
168
+ * `null` fades it out. Actions run on the game's clock. Each change is an `action` entry in the
169
+ * play log; a name the armature lacks is `action-unknown` once and returns false.
170
+ *
171
+ * play.setAction('Hero', moving ? 'Run' : 'Idle');
172
+ */
173
+ setAction(object: THREE.Object3D | string, action: string | null, options?: ModelPlayActionOptions): boolean;
174
+ /** The actions an object's armature can play, by name (empty without an armature). */
175
+ actions(object: THREE.Object3D | string): readonly string[];
176
+ }
177
+
178
+ export interface ModelPlayActionOptions {
179
+ readonly loop?: boolean;
180
+ readonly fade?: number;
181
+ readonly speed?: number;
182
+ /** Start again from the first frame when this clip is already playing. */
183
+ readonly restart?: boolean;
160
184
  }
161
185
 
162
186
  /** What the bot is handed before each `update` it drives. */
@@ -290,8 +314,11 @@ export function runPlayScript(options: {
290
314
  readonly ready: () => void;
291
315
  readonly returning: () => void;
292
316
  readonly ownMaterial?: ((material: THREE.Material) => THREE.Material | null) | undefined;
317
+ /** The document's skins and clips bound to `root`; absent, characters stand in their exported pose. */
318
+ readonly animation?: DocumentPlayAnimation | undefined;
293
319
  }): () => void {
294
320
  const { blend, root, camera, onFrame } = options;
321
+ const options_ = options;
295
322
  const modulePath = playScriptPath(blend);
296
323
  // A fresh copy is a fresh run: its log starts empty, its clock at zero. Writes go through
297
324
  // this run's handle, which is inert once the run has ended.
@@ -385,6 +412,32 @@ export function runPlayScript(options: {
385
412
  const behaviors = botBehaviors(bot);
386
413
  if (alive.value) alive.bot = behaviors;
387
414
  },
415
+ setAction(object, action, options) {
416
+ if (!alive.value) return false;
417
+ const name = typeof object === 'string' ? object : object.name;
418
+ const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
419
+ const unknown = (why: string): false => {
420
+ const key = `${name}\u0000${action}`;
421
+ if (!alive.unknown.has(key)) { alive.unknown.add(key); run.append('play', 'action-unknown', { object: name, action, why }); }
422
+ return false;
423
+ };
424
+ if (!target) return unknown('the model has no such object');
425
+ if (!options_.animation) return unknown('this document lends no animation');
426
+ const before = options_.animation.playing(target);
427
+ if (action === null) {
428
+ options_.animation.stop(target, options?.fade);
429
+ if (before !== null) run.append('play', 'action', { object: target.name, action: null, from: before });
430
+ return true;
431
+ }
432
+ const answer = options_.animation.play(target, action, options);
433
+ if (!answer.ok) return unknown(answer.why);
434
+ if (before !== action || options?.restart) run.append('play', 'action', { armature: answer.armature, action, from: before });
435
+ return true;
436
+ },
437
+ actions(object) {
438
+ const target = typeof object === 'string' ? root.getObjectByName(object) ?? null : object;
439
+ return target && options_.animation ? options_.animation.clips(target) : [];
440
+ },
388
441
  });
389
442
  const scripts = new WeakMap<ModelPlayGame, Script>();
390
443
  let stopped = false;
@@ -566,6 +619,8 @@ export function runPlayScript(options: {
566
619
  // A paused frame that runs an update is a Step; its entry carries the step's own tick.
567
620
  const stepping = modelPlayClock(options.documentId).paused;
568
621
  const tick = (dt: number): void => {
622
+ // The characters' clips move on the game's clock, one update's `dt` at a time.
623
+ options.animation?.update(dt);
569
624
  run.advance(dt);
570
625
  ran += 1;
571
626
  simulated += dt;