@drawcall/acta 0.0.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.
@@ -0,0 +1,458 @@
1
+ import { AdditiveAnimationBlendMode, Euler, EventDispatcher, LoopOnce, Quaternion, Vector2, Vector3, } from 'three';
2
+ import { action, GraphTimeline, parallel, timePassed, animationFinished, runTimeline, scope, switch_, } from '@pmndrs/timeline';
3
+ import { applyMask, fixModelAnimationClip, lowerBody, startAnimation, upperBody, loadCharacterAnimation, WalkAnimationUrl, RunAnimationUrl, } from '@pmndrs/viverse';
4
+ import { makeClipAdditive } from 'three/src/animation/AnimationUtils.js';
5
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
6
+ import { VRM } from '@pixiv/three-vrm';
7
+ import { clamp } from 'three/src/math/MathUtils.js';
8
+ const ZeroVector = new Vector3();
9
+ const ForwardVector = new Vector3(0, 0, 1);
10
+ const UpVector = new Vector3(0, 1, 0);
11
+ const SpineAimYawOffset = -0.5;
12
+ const quaternionHelper = new Quaternion();
13
+ const parentQuaternionInvHelper = new Quaternion();
14
+ const localQuaternionHelper = new Quaternion();
15
+ const eulerHelper = new Euler();
16
+ const directionHelper = new Vector3();
17
+ const vectorHelper = new Vector3();
18
+ //TODO: support direction AND position targets for head and aim
19
+ //TODO: lerp everything (spine rotation etc)
20
+ //TODO: jumping support
21
+ export class CharacterBehavior extends EventDispatcher {
22
+ model;
23
+ applyMove;
24
+ properties = {};
25
+ computedProperties = {
26
+ isMoving: () => this.worldMoveVelocity.length() > 0,
27
+ };
28
+ // Direction and movement properties
29
+ worldHeadDirection;
30
+ worldAimDirection;
31
+ worldMoveVelocity = new Vector3(0, 0, 0);
32
+ abortController = new AbortController();
33
+ updateTimeline;
34
+ aimUpAnimation;
35
+ aimNeutralAnimation;
36
+ aimDownAnimation;
37
+ walkAnimation;
38
+ runAnimation;
39
+ jogRightAnimation;
40
+ jogLeftAnimation;
41
+ jogFwdRightAnimation;
42
+ jogFwdAnimation;
43
+ jogFwdLeftAnimation;
44
+ jogBwdRightAnimation;
45
+ jogBwdAnimation;
46
+ jogBwdLeftAnimation;
47
+ animations = {};
48
+ audios = {};
49
+ gltfLoader;
50
+ moveStatus = 'none';
51
+ isAiming = false;
52
+ constructor(behavior, animationUrls, audioUrls, model, applyMove, loadingManager) {
53
+ super();
54
+ this.model = model;
55
+ this.applyMove = applyMove;
56
+ if (model instanceof VRM) {
57
+ throw new Error(`not supported yet`);
58
+ }
59
+ this.gltfLoader = new GLTFLoader(loadingManager);
60
+ this.init(behavior, animationUrls, audioUrls).catch(console.error);
61
+ }
62
+ async init(behavior, animationUrls, audioUrls) {
63
+ await Promise.all([
64
+ this.loadMoveAnimations(),
65
+ this.loadBaseAimAnimation(),
66
+ ...Object.entries(animationUrls).map(([name, url]) => this.loadAnimation(name, url)),
67
+ ...Object.entries(audioUrls).map(([name, url]) => this.loadAudio(name, url)),
68
+ ]);
69
+ const timeline = this.convertToTimeline(behavior);
70
+ this.updateTimeline = runTimeline(timeline, this.abortController.signal);
71
+ }
72
+ async loadMoveAnimations() {
73
+ const [walkAnimation, runAnimation, jogRightAnimation, jogLeftAnimation, jogFwdRightAnimation, jogFwdAnimation, jogFwdLeftAnimation, jogBwdRightAnimation, jogBwdAnimation, jogBwdLeftAnimation,] = await Promise.all([
74
+ await loadCharacterAnimation(this.model, WalkAnimationUrl),
75
+ await loadCharacterAnimation(this.model, RunAnimationUrl),
76
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Right.js')).default, 'gltf'),
77
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Left.js')).default, 'gltf'),
78
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Fwd_R.js')).default, 'gltf'),
79
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Fwd.js')).default, 'gltf'),
80
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Fwd_L.js')).default, 'gltf'),
81
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Bwd_R.js')).default, 'gltf'),
82
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Bwd.js')).default, 'gltf'),
83
+ await loadCharacterAnimation(this.model, (await import('./animations/Jog_Bwd_L.js')).default, 'gltf'),
84
+ ]);
85
+ this.walkAnimation = walkAnimation;
86
+ this.runAnimation = runAnimation;
87
+ //apply masks because jogging is only used for the lower body
88
+ applyMask(jogRightAnimation, lowerBody);
89
+ applyMask(jogLeftAnimation, lowerBody);
90
+ applyMask(jogFwdRightAnimation, lowerBody);
91
+ applyMask(jogFwdAnimation, lowerBody);
92
+ applyMask(jogFwdLeftAnimation, lowerBody);
93
+ applyMask(jogBwdRightAnimation, lowerBody);
94
+ applyMask(jogBwdAnimation, lowerBody);
95
+ applyMask(jogBwdLeftAnimation, lowerBody);
96
+ this.jogRightAnimation = jogRightAnimation;
97
+ this.jogLeftAnimation = jogLeftAnimation;
98
+ this.jogFwdRightAnimation = jogFwdRightAnimation;
99
+ this.jogFwdAnimation = jogFwdAnimation;
100
+ this.jogFwdLeftAnimation = jogFwdLeftAnimation;
101
+ this.jogBwdRightAnimation = jogBwdRightAnimation;
102
+ this.jogBwdAnimation = jogBwdAnimation;
103
+ this.jogBwdLeftAnimation = jogBwdLeftAnimation;
104
+ }
105
+ async loadBaseAimAnimation() {
106
+ //TODO: load only if necassary
107
+ const [aimUpAnimation, aimNeutralAnimation, aimDownAnimation] = await Promise.all([
108
+ loadCharacterAnimation(this.model, (await import('./animations/Pistol_Aim_Up.js')).default, 'gltf'),
109
+ loadCharacterAnimation(this.model, (await import('./animations/Pistol_Aim_Neutral.js')).default, 'gltf'),
110
+ loadCharacterAnimation(this.model, (await import('./animations/Pistol_Aim_Down.js')).default, 'gltf'),
111
+ ]);
112
+ this.aimUpAnimation = aimUpAnimation;
113
+ this.aimNeutralAnimation = aimNeutralAnimation;
114
+ this.aimDownAnimation = aimDownAnimation;
115
+ }
116
+ async loadAnimation(name, url) {
117
+ const gltf = await this.gltfLoader.loadAsync(url);
118
+ if (gltf.animations.length != 1) {
119
+ throw new Error(`gltf contains has more than 1 animation`);
120
+ }
121
+ fixModelAnimationClip(this.model, gltf.animations[0], gltf.scene, false);
122
+ this.animations[name] = gltf.animations[0];
123
+ }
124
+ async loadAudio(name, url) { }
125
+ update(delta) {
126
+ if (this.moveStatus === 'fullbody' && this.worldMoveVelocity.lengthSq() > 0) {
127
+ // Signed angle from forward (0,0,1) to move direction on XZ plane
128
+ this.model.scene.rotation.y = Math.PI + Math.atan2(this.worldMoveVelocity.x, this.worldMoveVelocity.z);
129
+ }
130
+ else if (this.worldAimDirection != null && this.isAiming) {
131
+ this.model.scene.rotation.y = Math.PI + Math.atan2(this.worldAimDirection.x, this.worldAimDirection.z);
132
+ }
133
+ else if (this.worldHeadDirection != null) {
134
+ this.model.scene.rotation.y = Math.PI + Math.atan2(this.worldHeadDirection.x, this.worldHeadDirection.z);
135
+ }
136
+ this.updateTimeline?.(undefined, delta);
137
+ this.model.mixer.update(delta);
138
+ // Stabilize spine during aiming to face the aim direction
139
+ if (this.isAiming) {
140
+ const spineBone = this.model.scene.getObjectByName('spine');
141
+ // Extract yaw from aim direction and add PI offset for model facing
142
+ const aimYaw = this.model.scene.rotation.y + Math.PI + SpineAimYawOffset;
143
+ // Create quaternion with just the yaw (no pitch/roll)
144
+ eulerHelper.set(0, aimYaw, 0, 'YXZ');
145
+ quaternionHelper.setFromEuler(eulerHelper);
146
+ // Transform to spine's local space
147
+ spineBone.parent.getWorldQuaternion(parentQuaternionInvHelper).invert();
148
+ localQuaternionHelper.copy(parentQuaternionInvHelper).multiply(quaternionHelper);
149
+ spineBone.quaternion.copy(localQuaternionHelper);
150
+ spineBone.updateMatrixWorld();
151
+ }
152
+ if (this.worldHeadDirection != null) {
153
+ const neckBone = this.model.scene.getObjectByName('neck');
154
+ // Transform worldHeadDirection to neck's parent local space
155
+ neckBone.parent.getWorldQuaternion(quaternionHelper).invert();
156
+ directionHelper.copy(this.worldHeadDirection).applyQuaternion(quaternionHelper);
157
+ // Compute the local rotation that aligns (0,0,1) with the local target direction
158
+ quaternionHelper.setFromUnitVectors(ForwardVector, directionHelper);
159
+ neckBone.rotation.setFromQuaternion(quaternionHelper, 'YXZ');
160
+ neckBone.rotation.z = 0;
161
+ neckBone.rotation.y = clamp(neckBone.rotation.y, -Math.PI / 2, Math.PI / 2);
162
+ neckBone.rotation.x = clamp(neckBone.rotation.x, -Math.PI / 2, Math.PI / 2);
163
+ }
164
+ this.applyMove?.(this.moveStatus === 'none' ? ZeroVector : this.worldMoveVelocity, delta);
165
+ }
166
+ destroy() {
167
+ this.abortController.abort();
168
+ }
169
+ convertToTimeline(behavior, mask) {
170
+ if (behavior.type === 'animation' && behavior.name === 'Move') {
171
+ return this.createMoveTimeline(mask);
172
+ }
173
+ switch (behavior.type) {
174
+ case 'animation':
175
+ return this.createAnimationTimeline(behavior.name, mask);
176
+ case 'audio':
177
+ throw new Error('not implemented');
178
+ case 'graph':
179
+ return this.createGraphTimeline(behavior, mask);
180
+ case 'loop':
181
+ return this.createLoopTimeline(behavior.behavior, mask);
182
+ case 'parallel':
183
+ return () => parallel('race', ...behavior.behaviors.map((b) => this.convertToTimeline(b, mask)));
184
+ case 'sequential':
185
+ return this.createSequentialTimeline(behavior.behaviors, mask);
186
+ case 'split':
187
+ return () => parallel('race', this.convertToTimeline(behavior.lowerBodyBehavior, 'lowerbody'), this.convertToTimeline(behavior.upperBodyBehavior, 'upperbody'));
188
+ case 'wait':
189
+ return () => action({ until: timePassed(behavior.seconds, 'seconds') });
190
+ }
191
+ }
192
+ createMoveTimeline(mask) {
193
+ const layer = computeAnimationLayers(mask);
194
+ if (mask == null) {
195
+ //fullbody (just animate between walk and run)
196
+ const walkAction = this.model.mixer.clipAction(this.walkAnimation);
197
+ const runAction = this.model.mixer.clipAction(this.runAnimation);
198
+ const self = this;
199
+ const vectorHelper = new Vector2();
200
+ return () => parallel('all', action({
201
+ update: () => {
202
+ vectorHelper.set(this.worldMoveVelocity.x, -this.worldMoveVelocity.z);
203
+ const speed = vectorHelper.length();
204
+ walkAction.timeScale = 0.667 * speed;
205
+ runAction.timeScale = 0.208 * speed;
206
+ },
207
+ }), scope(async function* (abortSignal) {
208
+ self.moveStatus = 'fullbody';
209
+ self.dispatchEvent({ type: 'animationStarted', name: 'Move' });
210
+ abortSignal.addEventListener('abort', () => {
211
+ self.moveStatus = 'none';
212
+ self.dispatchEvent({ type: 'animationFinished', name: 'Move' });
213
+ }, { once: true });
214
+ yield* switch_([
215
+ {
216
+ timeline: () => action({ init: () => startAnimation(walkAction, self.model.currentAnimations, { layer }) }),
217
+ condition: () => vectorHelper.length() < 4.5,
218
+ },
219
+ {
220
+ timeline: () => action({ init: () => startAnimation(runAction, self.model.currentAnimations, { layer }) }),
221
+ },
222
+ ]);
223
+ }));
224
+ }
225
+ //only lower body (computing angle between world torso direction and world velocity)
226
+ const jogRightAction = this.model.mixer.clipAction(this.jogRightAnimation);
227
+ const jogLeftAction = this.model.mixer.clipAction(this.jogLeftAnimation);
228
+ const jogFwdRightAction = this.model.mixer.clipAction(this.jogFwdRightAnimation);
229
+ const jogFwdAction = this.model.mixer.clipAction(this.jogFwdAnimation);
230
+ const jogFwdLeftAction = this.model.mixer.clipAction(this.jogFwdLeftAnimation);
231
+ const jogBwdRightAction = this.model.mixer.clipAction(this.jogBwdRightAnimation);
232
+ const jogBwdAction = this.model.mixer.clipAction(this.jogBwdAnimation);
233
+ const jogBwdLeftAction = this.model.mixer.clipAction(this.jogBwdLeftAnimation);
234
+ const normalizedLocalMoveDirection = new Vector2();
235
+ const self = this;
236
+ return () => parallel('all', action({
237
+ update: () => {
238
+ // Transform world velocity to model's local space by rotating by inverse of model's Y rotation
239
+ vectorHelper.copy(this.worldMoveVelocity).applyAxisAngle(UpVector, -this.model.scene.rotation.y);
240
+ const speed = normalizedLocalMoveDirection.set(vectorHelper.x, -vectorHelper.z).length();
241
+ normalizedLocalMoveDirection.divideScalar(speed);
242
+ jogFwdAction.timeScale = 0.222 * speed;
243
+ jogFwdRightAction.timeScale = 0.222 * speed;
244
+ jogRightAction.timeScale = 0.37 * speed;
245
+ jogBwdRightAction.timeScale = 0.256 * speed;
246
+ jogBwdAction.timeScale = 0.238 * speed;
247
+ jogBwdLeftAction.timeScale = 0.256 * speed;
248
+ jogLeftAction.timeScale = 0.37 * speed;
249
+ jogFwdLeftAction.timeScale = 0.222 * speed;
250
+ },
251
+ }), scope(async function* (abortSignal) {
252
+ self.moveStatus = 'lowerbody';
253
+ self.dispatchEvent({ type: 'animationStarted', name: 'Move' });
254
+ abortSignal.addEventListener('abort', () => {
255
+ self.moveStatus = 'none';
256
+ self.dispatchEvent({ type: 'animationFinished', name: 'Move' });
257
+ }, { once: true });
258
+ yield* switch_([
259
+ {
260
+ timeline: () => action({
261
+ init: () => startAnimation(jogFwdAction, self.model.currentAnimations, { layer, sync: true }),
262
+ }),
263
+ condition: () => Math.abs(normalizedLocalMoveDirection.x) < 0.5 && normalizedLocalMoveDirection.y > 0.5,
264
+ },
265
+ {
266
+ timeline: () => action({
267
+ init: () => startAnimation(jogFwdRightAction, self.model.currentAnimations, { layer, sync: true }),
268
+ }),
269
+ condition: () => normalizedLocalMoveDirection.x > 0.5 && normalizedLocalMoveDirection.y > 0.5,
270
+ },
271
+ {
272
+ timeline: () => action({
273
+ init: () => startAnimation(jogRightAction, self.model.currentAnimations, { layer, sync: true }),
274
+ }),
275
+ condition: () => normalizedLocalMoveDirection.x > 0.5 && Math.abs(normalizedLocalMoveDirection.y) < 0.5,
276
+ },
277
+ {
278
+ timeline: () => action({
279
+ init: () => startAnimation(jogBwdRightAction, self.model.currentAnimations, { layer, sync: true }),
280
+ }),
281
+ condition: () => normalizedLocalMoveDirection.x > 0.5 && normalizedLocalMoveDirection.y < -0.5,
282
+ },
283
+ {
284
+ timeline: () => action({
285
+ init: () => startAnimation(jogBwdAction, self.model.currentAnimations, { layer, sync: true }),
286
+ }),
287
+ condition: () => Math.abs(normalizedLocalMoveDirection.x) < 0.5 && normalizedLocalMoveDirection.y < -0.5,
288
+ },
289
+ {
290
+ timeline: () => action({
291
+ init: () => startAnimation(jogBwdLeftAction, self.model.currentAnimations, { layer, sync: true }),
292
+ }),
293
+ condition: () => normalizedLocalMoveDirection.x < -0.5 && normalizedLocalMoveDirection.y < -0.5,
294
+ },
295
+ {
296
+ timeline: () => action({
297
+ init: () => startAnimation(jogLeftAction, self.model.currentAnimations, { layer, sync: true }),
298
+ }),
299
+ condition: () => normalizedLocalMoveDirection.x < -0.5 && Math.abs(normalizedLocalMoveDirection.y) < 0.5,
300
+ },
301
+ {
302
+ timeline: () => action({
303
+ init: () => startAnimation(jogFwdLeftAction, self.model.currentAnimations, { layer, sync: true }),
304
+ }),
305
+ },
306
+ ]);
307
+ }));
308
+ }
309
+ createAnimationTimeline(name, mask) {
310
+ let animationClip = this.animations[name];
311
+ if (mask != null) {
312
+ animationClip = animationClip.clone();
313
+ applyMask(animationClip, mask === 'lowerbody' ? lowerBody : upperBody);
314
+ }
315
+ if (name.includes('Pistol_')) {
316
+ return this.createAdditiveAimingTimeline(animationClip, mask, name);
317
+ }
318
+ return this.createStandardAnimationTimeline(animationClip, mask, name);
319
+ }
320
+ createStandardAnimationTimeline(clip, mask, name) {
321
+ const animationAction = this.model.mixer.clipAction(clip);
322
+ animationAction.loop = LoopOnce;
323
+ animationAction.clampWhenFinished = true;
324
+ const layer = computeAnimationLayers(mask);
325
+ const self = this;
326
+ return async function* () {
327
+ if (name)
328
+ self.dispatchEvent({ type: 'animationStarted', name });
329
+ try {
330
+ yield* action({
331
+ init: () => startAnimation(animationAction, self.model.currentAnimations, { layer }),
332
+ until: animationFinished(animationAction),
333
+ });
334
+ }
335
+ finally {
336
+ if (name)
337
+ self.dispatchEvent({ type: 'animationFinished', name });
338
+ }
339
+ };
340
+ }
341
+ createAdditiveAimingTimeline(clip, mask, name) {
342
+ let aimUpAnimation = this.aimUpAnimation;
343
+ let aimNeutralAnimation = this.aimNeutralAnimation;
344
+ let aimDownAnimation = this.aimDownAnimation;
345
+ if (mask != null) {
346
+ aimUpAnimation = aimUpAnimation.clone();
347
+ aimNeutralAnimation = aimNeutralAnimation.clone();
348
+ aimDownAnimation = aimDownAnimation.clone();
349
+ const maskFn = mask === 'lowerbody' ? lowerBody : upperBody;
350
+ applyMask(aimUpAnimation, maskFn);
351
+ applyMask(aimNeutralAnimation, maskFn);
352
+ applyMask(aimDownAnimation, maskFn);
353
+ }
354
+ clip = clip.clone();
355
+ makeClipAdditive(clip, undefined, aimNeutralAnimation);
356
+ const additiveAction = this.model.mixer.clipAction(clip);
357
+ additiveAction.loop = LoopOnce;
358
+ additiveAction.blendMode = AdditiveAnimationBlendMode;
359
+ additiveAction.clampWhenFinished = true;
360
+ const aimUpAcion = this.model.mixer.clipAction(aimUpAnimation);
361
+ const aimNeutralAction = this.model.mixer.clipAction(aimNeutralAnimation);
362
+ const aimDownAction = this.model.mixer.clipAction(aimDownAnimation);
363
+ const layer = computeAnimationLayers(mask, false);
364
+ const self = this;
365
+ return async function* () {
366
+ if (name)
367
+ self.dispatchEvent({ type: 'animationStarted', name });
368
+ self.isAiming = true;
369
+ try {
370
+ yield* action({
371
+ init: () => {
372
+ startAnimation(aimUpAcion, self.model.currentAnimations, { layer: 'aim-up' });
373
+ startAnimation(aimNeutralAction, self.model.currentAnimations, { layer });
374
+ startAnimation(aimDownAction, self.model.currentAnimations, { layer: 'adim-down' });
375
+ startAnimation(additiveAction, self.model.currentAnimations, { layer: 'aim' });
376
+ },
377
+ update: () => {
378
+ const pitch = self.worldAimDirection ? -Math.asin(self.worldAimDirection.y) : 0;
379
+ if (pitch <= 0) {
380
+ // looking up
381
+ aimUpAcion.weight = Math.min(1, Math.max(0, -pitch / (Math.PI / 2)));
382
+ aimNeutralAction.weight = 1 - aimUpAcion.weight;
383
+ aimDownAction.weight = 0;
384
+ }
385
+ else if (pitch > 0) {
386
+ // looking down
387
+ aimDownAction.weight = Math.min(1, Math.max(0, pitch / (Math.PI / 2)));
388
+ aimNeutralAction.weight = 1 - aimDownAction.weight;
389
+ aimUpAcion.weight = 0;
390
+ }
391
+ },
392
+ until: animationFinished(additiveAction),
393
+ });
394
+ }
395
+ finally {
396
+ self.isAiming = false;
397
+ if (name)
398
+ self.dispatchEvent({ type: 'animationFinished', name });
399
+ }
400
+ };
401
+ }
402
+ createGraphTimeline(behavior, mask) {
403
+ const graph = new GraphTimeline(behavior.enterState, undefined, behavior.exitState, undefined, true);
404
+ for (const name in behavior.states) {
405
+ const state = behavior.states[name];
406
+ const transitionsTo = {};
407
+ if (state.finally != null) {
408
+ transitionsTo.finally = state.finally;
409
+ }
410
+ for (const targetState in state.transitionsTo) {
411
+ const condition = state.transitionsTo[targetState];
412
+ transitionsTo[targetState] = {
413
+ whenUpdate: () => evaluateCondition(condition, this.properties, this.computedProperties),
414
+ };
415
+ }
416
+ graph.attach(name, this.convertToTimeline(state.behavior, mask), transitionsTo);
417
+ }
418
+ return () => graph.run();
419
+ }
420
+ createLoopTimeline(behavior, mask) {
421
+ const subTimeline = this.convertToTimeline(behavior, mask);
422
+ return async function* () {
423
+ while (true) {
424
+ yield* subTimeline();
425
+ }
426
+ };
427
+ }
428
+ createSequentialTimeline(behaviors, mask) {
429
+ const subTimelines = behaviors.map((b) => this.convertToTimeline(b, mask));
430
+ return async function* () {
431
+ for (const timeline of subTimelines) {
432
+ yield* timeline();
433
+ }
434
+ };
435
+ }
436
+ }
437
+ function computeAnimationLayers(mask, includeAim = true) {
438
+ const result = [];
439
+ if (mask !== 'lowerbody')
440
+ result.push('upperbody');
441
+ if (mask !== 'lowerbody' && includeAim)
442
+ result.push('aim', 'aim-up', 'aim-down');
443
+ if (mask !== 'upperbody')
444
+ result.push('lowerbody');
445
+ return result;
446
+ }
447
+ function evaluateCondition(condition, properties, computedProperties) {
448
+ switch (condition.type) {
449
+ case 'isTrue':
450
+ return computedProperties[condition.property]?.() ?? properties[condition.property] ?? false;
451
+ case 'and':
452
+ return condition.conditions.every((c) => evaluateCondition(c, properties, computedProperties));
453
+ case 'or':
454
+ return condition.conditions.some((c) => evaluateCondition(c, properties, computedProperties));
455
+ case 'not':
456
+ return !evaluateCondition(condition.condition, properties, computedProperties);
457
+ }
458
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types.js';
2
+ export * from './behavior.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './types.js';
2
+ export * from './behavior.js';
@@ -0,0 +1,52 @@
1
+ export type Condition = {
2
+ type: 'isTrue';
3
+ property: string;
4
+ } | {
5
+ type: 'not';
6
+ condition: Condition;
7
+ } | {
8
+ type: 'or';
9
+ conditions: Condition[];
10
+ } | {
11
+ type: 'and';
12
+ conditions: Condition[];
13
+ };
14
+ export type Behavior = {
15
+ type: 'animation';
16
+ name: string;
17
+ } | {
18
+ type: 'audio';
19
+ name: string;
20
+ } | {
21
+ type: 'wait';
22
+ seconds: number;
23
+ } | {
24
+ type: 'split';
25
+ upperBodyBehavior: Behavior;
26
+ lowerBodyBehavior: Behavior;
27
+ } | {
28
+ type: 'sequential';
29
+ behaviors: Behavior[];
30
+ } | {
31
+ type: 'parallel';
32
+ behaviors: Behavior[];
33
+ } | {
34
+ type: 'loop';
35
+ behavior: Behavior;
36
+ } | {
37
+ type: 'graph';
38
+ enterState: string;
39
+ exitState?: string;
40
+ states: Record<string, {
41
+ behavior: Behavior;
42
+ /**
43
+ * name of the state to transition to when the behavior finishes.
44
+ */
45
+ finally?: string;
46
+ /**
47
+ * Transitions are evaluated continuously during the state's behavior execution.
48
+ * If a condition is met, the current behavior is cancelled immediately and the transition occurs.
49
+ */
50
+ transitionsTo?: Record<string, Condition>;
51
+ }>;
52
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@drawcall/acta",
3
+ "type": "module",
4
+ "main": "dist/index.js",
5
+ "version": "0.0.0",
6
+ "author": "Bela Bohlender",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "homepage": "https://drawcall.ai",
9
+ "keywords": [
10
+ "drawcall.ai"
11
+ ],
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "@pixiv/three-vrm": "3.4.4",
17
+ "@pmndrs/timeline": "^0.3.7",
18
+ "@pmndrs/viverse": "^0.2.10"
19
+ },
20
+ "peerDependencies": {
21
+ "three": "*"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc"
25
+ }
26
+ }