@m2c2kit/core 0.3.17 → 0.3.18

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/index.js CHANGED
@@ -1,3 +1,33 @@
1
+ var M2NodeType = /* @__PURE__ */ ((M2NodeType2) => {
2
+ M2NodeType2["Node"] = "Node";
3
+ M2NodeType2["Scene"] = "Scene";
4
+ M2NodeType2["Sprite"] = "Sprite";
5
+ M2NodeType2["Label"] = "Label";
6
+ M2NodeType2["TextLine"] = "TextLine";
7
+ M2NodeType2["Shape"] = "Shape";
8
+ M2NodeType2["Composite"] = "Composite";
9
+ M2NodeType2["SoundPlayer"] = "SoundPlayer";
10
+ M2NodeType2["SoundRecorder"] = "SoundRecorder";
11
+ return M2NodeType2;
12
+ })(M2NodeType || {});
13
+
14
+ const M2SoundStatus = {
15
+ /** Sound was set for lazy loading, and loading has not yet been requested. */
16
+ Deferred: "Deferred",
17
+ /** Sound is indicated for fetching, but fetching has not begun. */
18
+ WillFetch: "WillFetch",
19
+ /** Sound is being fetched. */
20
+ Fetching: "Fetching",
21
+ /** Sound has been fetched. */
22
+ Fetched: "Fetched",
23
+ /** Sound is being decoded. */
24
+ Decoding: "Decoding",
25
+ /** Sound has fully finished loading and is ready to use. */
26
+ Ready: "Ready",
27
+ /** Error occurred in loading. */
28
+ Error: "Error"
29
+ };
30
+
1
31
  var ActionType = /* @__PURE__ */ ((ActionType2) => {
2
32
  ActionType2["Sequence"] = "Sequence";
3
33
  ActionType2["Group"] = "Group";
@@ -7,6 +37,9 @@ var ActionType = /* @__PURE__ */ ((ActionType2) => {
7
37
  ActionType2["Scale"] = "Scale";
8
38
  ActionType2["FadeAlpha"] = "FadeAlpha";
9
39
  ActionType2["Rotate"] = "Rotate";
40
+ ActionType2["Play"] = "Play";
41
+ ActionType2["Repeat"] = "Repeat";
42
+ ActionType2["RepeatForever"] = "RepeatForever";
10
43
  return ActionType2;
11
44
  })(ActionType || {});
12
45
 
@@ -44,8 +77,7 @@ Easings.quadraticOut = (t, b, c, d) => {
44
77
  };
45
78
  Easings.quadraticInOut = (t, b, c, d) => {
46
79
  t /= d / 2;
47
- if (t < 1)
48
- return c / 2 * t * t + b;
80
+ if (t < 1) return c / 2 * t * t + b;
49
81
  t--;
50
82
  return -c / 2 * (t * (t - 2) - 1) + b;
51
83
  };
@@ -60,8 +92,7 @@ Easings.cubicOut = (t, b, c, d) => {
60
92
  };
61
93
  Easings.cubicInOut = (t, b, c, d) => {
62
94
  t /= d / 2;
63
- if (t < 1)
64
- return c / 2 * t * t * t + b;
95
+ if (t < 1) return c / 2 * t * t * t + b;
65
96
  t -= 2;
66
97
  return c / 2 * (t * t * t + 2) + b;
67
98
  };
@@ -76,8 +107,7 @@ Easings.quarticOut = (t, b, c, d) => {
76
107
  };
77
108
  Easings.quarticInOut = (t, b, c, d) => {
78
109
  t /= d / 2;
79
- if (t < 1)
80
- return c / 2 * t * t * t * t + b;
110
+ if (t < 1) return c / 2 * t * t * t * t + b;
81
111
  t -= 2;
82
112
  return -c / 2 * (t * t * t * t - 2) + b;
83
113
  };
@@ -92,8 +122,7 @@ Easings.quinticOut = (t, b, c, d) => {
92
122
  };
93
123
  Easings.quinticInOut = (t, b, c, d) => {
94
124
  t /= d / 2;
95
- if (t < 1)
96
- return c / 2 * t * t * t * t * t + b;
125
+ if (t < 1) return c / 2 * t * t * t * t * t + b;
97
126
  t -= 2;
98
127
  return c / 2 * (t * t * t * t * t + 2) + b;
99
128
  };
@@ -114,8 +143,7 @@ Easings.exponentialOut = (t, b, c, d) => {
114
143
  };
115
144
  Easings.exponentialInOut = (t, b, c, d) => {
116
145
  t /= d / 2;
117
- if (t < 1)
118
- return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
146
+ if (t < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
119
147
  t--;
120
148
  return c / 2 * (-Math.pow(2, -10 * t) + 2) + b;
121
149
  };
@@ -130,23 +158,11 @@ Easings.circularOut = (t, b, c, d) => {
130
158
  };
131
159
  Easings.circularInOut = (t, b, c, d) => {
132
160
  t /= d / 2;
133
- if (t < 1)
134
- return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
161
+ if (t < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
135
162
  t -= 2;
136
163
  return c / 2 * (Math.sqrt(1 - t * t) + 1) + b;
137
164
  };
138
165
 
139
- var M2NodeType = /* @__PURE__ */ ((M2NodeType2) => {
140
- M2NodeType2["Node"] = "Node";
141
- M2NodeType2["Scene"] = "Scene";
142
- M2NodeType2["Sprite"] = "Sprite";
143
- M2NodeType2["Label"] = "Label";
144
- M2NodeType2["TextLine"] = "TextLine";
145
- M2NodeType2["Shape"] = "Shape";
146
- M2NodeType2["Composite"] = "Composite";
147
- return M2NodeType2;
148
- })(M2NodeType || {});
149
-
150
166
  var ShapeType = /* @__PURE__ */ ((ShapeType2) => {
151
167
  ShapeType2["Undefined"] = "Undefined";
152
168
  ShapeType2["Rectangle"] = "Rectangle";
@@ -481,357 +497,234 @@ function rotateRectangle(rect, radians, center) {
481
497
  return rotated;
482
498
  }
483
499
 
484
- class Action {
485
- constructor(runDuringTransition = false) {
486
- this.startOffset = -1;
487
- this.endOffset = -1;
488
- this.started = false;
489
- this.running = false;
490
- this.completed = false;
491
- this.runStartTime = -1;
492
- this.duration = 0;
493
- this.isParent = false;
494
- this.isChild = false;
495
- this.runDuringTransition = runDuringTransition;
500
+ class Futurable {
501
+ constructor(value) {
502
+ /** The numbers, operators, and other Futurables that represent a value. */
503
+ this.expression = new Array();
504
+ /** Log a warning to console if a expression is this length. */
505
+ this.WARNING_EXPRESSION_LENGTH = 32;
506
+ if (typeof value === "number") {
507
+ this.pushToExpression(value);
508
+ return;
509
+ }
510
+ if (value === void 0) {
511
+ this.pushToExpression(Infinity);
512
+ return;
513
+ }
514
+ this.pushToExpression(value);
496
515
  }
497
516
  /**
498
- * Creates an action that will move a node to a point on the screen.
517
+ * Appends a number or another Futurable to this Futurable's expression.
499
518
  *
500
- * @param options - {@link MoveActionOptions}
501
- * @returns The move action
519
+ * @remarks This method does a simple array push, but checks the length of
520
+ * the expression array and warns if it gets "too long." This may indicate
521
+ * a logic error, unintended recursion, etc. because our use cases will
522
+ * likely not have expressions that are longer than
523
+ * `Futural.WARNING_EXPRESSION_LENGTH` elements.
524
+ *
525
+ * @param value - value to add to the expression.
502
526
  */
503
- static move(options) {
504
- return new MoveAction(
505
- options.point,
506
- options.duration,
507
- options.easing ?? Easings.linear,
508
- options.runDuringTransition ?? false
509
- );
527
+ pushToExpression(value) {
528
+ if (value === this) {
529
+ throw new Error(
530
+ "Cannot add, subtract, or assign a Futurable with itself."
531
+ );
532
+ }
533
+ this.expression.push(value);
534
+ if (this.expression.length === this.WARNING_EXPRESSION_LENGTH) {
535
+ console.warn(
536
+ `Expression length is ${this.WARNING_EXPRESSION_LENGTH} elements. Something may be wrong.`
537
+ );
538
+ }
510
539
  }
511
540
  /**
512
- * Creates an action that will wait a given duration before it is considered complete.
541
+ * Assigns a value, either known or Futurable, to this Futurable.
513
542
  *
514
- * @param options - {@link WaitActionOptions}
515
- * @returns The wait action
543
+ * @remarks This method clears the current expression.
544
+ *
545
+ * @param value - value to assign to this Futurable.
516
546
  */
517
- static wait(options) {
518
- return new WaitAction(
519
- options.duration,
520
- options.runDuringTransition ?? false
521
- );
547
+ assign(value) {
548
+ while (this.expression.length > 0) {
549
+ this.expression.pop();
550
+ }
551
+ this.pushToExpression(value);
522
552
  }
523
553
  /**
524
- * Creates an action that will execute a callback function.
554
+ * Performs addition on this Futurable.
525
555
  *
526
- * @param options - {@link CustomActionOptions}
527
- * @returns The custom action
556
+ * @remarks This method modifies the Futurable by adding the term(s) to the
557
+ * Futurable's expression.
558
+ *
559
+ * @param terms - terms to add to this Futurable.
560
+ * @returns the modified Futurable.
528
561
  */
529
- static custom(options) {
530
- return new CustomAction(
531
- options.callback,
532
- options.runDuringTransition ?? false
533
- );
562
+ add(...terms) {
563
+ this.appendOperation(Operator.Add, ...terms);
564
+ return this;
534
565
  }
535
566
  /**
536
- * Creates an action that will scale the node's size.
567
+ * Performs subtraction on this Futurable.
537
568
  *
538
- * @remarks Scaling is relative to any inherited scaling, which is multiplicative. For example, if the node's parent is scaled to 2.0 and this node's action scales to 3.0, then the node will appear 6 times as large as original.
569
+ * @remarks This method modifies the Futurable by subtracting the term(s)
570
+ * from the Futurable's expression.
539
571
  *
540
- * @param options - {@link ScaleActionOptions}
541
- * @returns The scale action
572
+ * @param terms - terms to subtract from this Futurable.
573
+ * @returns the modified Futurable.
542
574
  */
543
- static scale(options) {
544
- return new ScaleAction(
545
- options.scale,
546
- options.duration,
547
- options.runDuringTransition
548
- );
575
+ subtract(...terms) {
576
+ this.appendOperation(Operator.Subtract, ...terms);
577
+ return this;
549
578
  }
550
579
  /**
551
- * Creates an action that will change the node's alpha (opacity).
552
- *
553
- * @remarks Alpha has multiplicative inheritance. For example, if the node's parent is alpha .5 and this node's action fades alpha to .4, then the node will appear with alpha .2.
580
+ * Adds an operation (an operator and term(s)) to the Futurable's
581
+ * expression.
554
582
  *
555
- * @param options - {@link FadeAlphaActionOptions}
556
- * @returns The fadeAlpha action
583
+ * @param operator - Add or Subtract.
584
+ * @param terms - terms to add to the expression.
557
585
  */
558
- static fadeAlpha(options) {
559
- return new FadeAlphaAction(
560
- options.alpha,
561
- options.duration,
562
- options.runDuringTransition
563
- );
586
+ appendOperation(operator, ...terms) {
587
+ terms.forEach((term) => {
588
+ this.pushToExpression(operator);
589
+ this.pushToExpression(term);
590
+ });
564
591
  }
565
592
  /**
566
- * Creates an action that will rotate the node.
593
+ * Gets the numeric value of this Futurable.
567
594
  *
568
- * @remarks Rotate actions are applied to their children. In addition to this node's rotate action, all ancestors' rotate actions will also be applied.
595
+ * @remarks This method evaluates the expression of the Futurable and
596
+ * returns the numeric value. If any of the terms in the expression are
597
+ * Futurables, it will recursively evaluate them. If any of the terms are
598
+ * unknown (represented by Infinity), it will return Infinity.
569
599
  *
570
- * @param options - {@link RotateActionOptions}
571
- * @returns The rotate action
600
+ * @returns the numeric value of this Futurable.
572
601
  */
573
- static rotate(options) {
574
- if (options.byAngle !== void 0 && options.toAngle !== void 0) {
575
- throw new Error("rotate Action: cannot specify both byAngle and toAngle");
576
- }
577
- if (options.byAngle === void 0 && options.toAngle === void 0) {
578
- throw new Error("rotate Action: must specify either byAngle or toAngle");
579
- }
580
- if (options.toAngle === void 0 && options.shortestUnitArc !== void 0) {
581
- throw new Error(
582
- "rotate Action: shortestUnitArc can only be specified when toAngle is provided"
583
- );
584
- }
585
- if (options.toAngle !== void 0 && options.shortestUnitArc === void 0) {
586
- options.shortestUnitArc = true;
602
+ get value() {
603
+ let result = 0;
604
+ const terms = this.expression.flat(Infinity);
605
+ let sign = 1;
606
+ for (let i = 0; i < terms.length; i++) {
607
+ if (typeof terms[i] === "number") {
608
+ result = result + sign * terms[i];
609
+ continue;
610
+ }
611
+ if (terms[i] instanceof Futurable) {
612
+ result = result + sign * terms[i].value;
613
+ continue;
614
+ }
615
+ if (terms[i] === Operator.Add) {
616
+ sign = 1;
617
+ continue;
618
+ }
619
+ if (terms[i] === Operator.Subtract) {
620
+ sign = -1;
621
+ continue;
622
+ }
587
623
  }
588
- return new RotateAction(
589
- options.byAngle,
590
- options.toAngle,
591
- options.shortestUnitArc,
592
- options.duration,
593
- options.runDuringTransition
594
- );
624
+ return result;
625
+ }
626
+ }
627
+ const Operator = {
628
+ /** Futurable addition operator */
629
+ Add: "Add",
630
+ /** Futurable subtraction operator */
631
+ Subtract: "Subtract"
632
+ };
633
+
634
+ class Action {
635
+ constructor(runDuringTransition = false) {
636
+ this.startOffset = new Futurable(0);
637
+ this.started = false;
638
+ this.running = false;
639
+ this._completed = false;
640
+ /**
641
+ * Start time of a running action is always known; it is not a `Futurable`.
642
+ * -1 indicates that the root action has not yet started running.
643
+ */
644
+ this.runStartTime = -1;
645
+ this.duration = new Futurable();
646
+ this.runDuringTransition = runDuringTransition;
595
647
  }
596
648
  /**
597
- * Creates an array of actions that will be run in order.
649
+ * Prepares the Action for evaluation.
598
650
  *
599
- * @remarks The next action will not begin until the current one has finished. The sequence will be considered completed when the last action has completed.
651
+ * @remarks Calculates start times for all actions in the hierarchy
652
+ * and returns a copy of the action that is prepared for evaluation during
653
+ * the update loop.
600
654
  *
601
- * @param actions - One or more actions that form the sequence
602
- * @returns
655
+ * @param key - optional string to identify an action
656
+ * @returns action prepared for evaluation
603
657
  */
604
- static sequence(actions) {
605
- const sequence = new SequenceAction(actions);
606
- sequence.children = actions;
607
- return sequence;
658
+ initialize(key) {
659
+ const action = this.clone();
660
+ this.assignParents(action, action, key);
661
+ this.propagateRunDuringTransition(action);
662
+ this.assignDurations(action);
663
+ this.assignStartOffsets(action);
664
+ return action;
608
665
  }
609
666
  /**
610
- * Create an array of actions that will be run simultaneously.
611
- *
612
- * @remarks All actions within the group will begin to run at the same time. The group will be considered completed when the longest-running action has completed.
667
+ * Parses the action hierarchy and assigns each action its parent and
668
+ * root action.
613
669
  *
614
- * @param actions - One or more actions that form the group
615
- * @returns
670
+ * @remarks Uses recursion to handle arbitrary level of nesting parent
671
+ * actions within parent actions. When this method is called from the
672
+ * `initialize()` method, the root action is both the `action` and the
673
+ * `rootAction`. This is because the action is the top-level action in the
674
+ * hierarchy. When the method calls itself recursively, the `rootAction`
675
+ * remains the same, but the `action` is a child action or the action of a
676
+ * repeating action.
677
+ *
678
+ * @param action - the action to assign parents to
679
+ * @param rootAction - top-level action passed to the run method
680
+ * @param key - optional string to identify an action. The key is assigned
681
+ * to every action in the hierarchy.
616
682
  */
617
- static group(actions) {
618
- const group = new GroupAction(actions);
619
- group.children = actions;
620
- return group;
621
- }
622
- initialize(node, key) {
623
- this.assignParents(this, this, key);
624
- const actions = this.flattenActions(this);
625
- actions.forEach(
626
- (action) => action.duration = this.calculateDuration(action)
627
- );
628
- this.calculateStartEndOffsets(this);
629
- const clonedActions = actions.filter(
630
- (action) => action.type !== ActionType.Group && action.type !== ActionType.Sequence
631
- ).map((action) => {
632
- return Action.cloneAction(action, key);
633
- });
634
- return clonedActions;
635
- }
636
- static cloneAction(action, key) {
637
- let cloned;
638
- switch (action.type) {
639
- case ActionType.Sequence: {
640
- const sequence = action;
641
- const sequenceChildren = sequence.children.map(
642
- (child) => Action.cloneAction(child, key)
643
- );
644
- cloned = Action.sequence(sequenceChildren);
645
- break;
646
- }
647
- case ActionType.Group: {
648
- const group = action;
649
- const groupChildren = group.children.map(
650
- (child) => Action.cloneAction(child, key)
651
- );
652
- cloned = Action.sequence(groupChildren);
653
- break;
654
- }
655
- case ActionType.Move: {
656
- const move = action;
657
- cloned = Action.move({
658
- point: move.point,
659
- duration: move.duration,
660
- easing: move.easing,
661
- runDuringTransition: move.runDuringTransition
662
- });
663
- break;
664
- }
665
- case ActionType.Custom: {
666
- const code = action;
667
- cloned = Action.custom({
668
- callback: code.callback,
669
- runDuringTransition: code.runDuringTransition
670
- });
671
- break;
672
- }
673
- case ActionType.Scale: {
674
- const scale = action;
675
- cloned = Action.scale({
676
- scale: scale.scale,
677
- duration: scale.duration,
678
- runDuringTransition: scale.runDuringTransition
679
- });
680
- break;
681
- }
682
- case ActionType.FadeAlpha: {
683
- const fadeAlpha = action;
684
- cloned = Action.fadeAlpha({
685
- alpha: fadeAlpha.alpha,
686
- duration: fadeAlpha.duration,
687
- runDuringTransition: fadeAlpha.runDuringTransition
688
- });
689
- break;
690
- }
691
- case ActionType.Rotate: {
692
- const rotate = action;
693
- cloned = Action.rotate({
694
- byAngle: rotate.byAngle,
695
- toAngle: rotate.toAngle,
696
- shortestUnitArc: rotate.shortestUnitArc,
697
- duration: rotate.duration,
698
- runDuringTransition: rotate.runDuringTransition
699
- });
700
- break;
701
- }
702
- case ActionType.Wait: {
703
- const wait = action;
704
- cloned = Action.wait({
705
- duration: wait.duration,
706
- runDuringTransition: wait.runDuringTransition
707
- });
708
- break;
709
- }
710
- default:
711
- throw new Error("unknown action");
712
- }
683
+ assignParents(action, rootAction, key) {
713
684
  if (key !== void 0) {
714
- cloned.key = key;
715
- }
716
- cloned.startOffset = action.startOffset;
717
- cloned.endOffset = action.endOffset;
718
- return cloned;
719
- }
720
- static evaluateAction(action, node, now, dt) {
721
- if (now < action.runStartTime + action.startOffset) {
722
- return;
723
- }
724
- if (now >= action.runStartTime + action.startOffset && now <= action.runStartTime + action.startOffset + action.duration) {
725
- action.running = true;
726
- } else {
727
- action.running = false;
728
- }
729
- if (action.running === false && action.completed === true) {
730
- return;
731
- }
732
- const elapsed = now - (action.runStartTime + action.startOffset);
733
- if (action.type === ActionType.Custom) {
734
- const customAction = action;
735
- customAction.callback();
736
- customAction.running = false;
737
- customAction.completed = true;
738
- }
739
- if (action.type === ActionType.Wait) {
740
- const waitAction = action;
741
- if (now > action.runStartTime + action.startOffset + action.duration) {
742
- waitAction.running = false;
743
- waitAction.completed = true;
744
- }
745
- }
746
- if (action.type === ActionType.Move) {
747
- const moveAction = action;
748
- if (!moveAction.started) {
749
- moveAction.dx = moveAction.point.x - node.position.x;
750
- moveAction.dy = moveAction.point.y - node.position.y;
751
- moveAction.startPoint.x = node.position.x;
752
- moveAction.startPoint.y = node.position.y;
753
- moveAction.started = true;
754
- }
755
- if (elapsed < moveAction.duration) {
756
- node.position.x = moveAction.easing(
757
- elapsed,
758
- moveAction.startPoint.x,
759
- moveAction.dx,
760
- moveAction.duration
761
- );
762
- node.position.y = moveAction.easing(
763
- elapsed,
764
- moveAction.startPoint.y,
765
- moveAction.dy,
766
- moveAction.duration
767
- );
768
- } else {
769
- node.position.x = moveAction.point.x;
770
- node.position.y = moveAction.point.y;
771
- moveAction.running = false;
772
- moveAction.completed = true;
773
- }
685
+ action.key = key;
774
686
  }
775
- if (action.type === ActionType.Scale) {
776
- const scaleAction = action;
777
- if (!scaleAction.started) {
778
- scaleAction.delta = scaleAction.scale - node.scale;
779
- scaleAction.started = true;
780
- }
781
- if (elapsed < scaleAction.duration) {
782
- node.scale = node.scale + scaleAction.delta * (dt / scaleAction.duration);
783
- } else {
784
- node.scale = scaleAction.scale;
785
- scaleAction.running = false;
786
- scaleAction.completed = true;
787
- }
687
+ if (this.isParent(action)) {
688
+ const children = action.children;
689
+ children.forEach((child) => {
690
+ child.parent = action;
691
+ });
692
+ children.filter((child) => this.isParent(child)).forEach((child) => this.assignParents(child, rootAction, key));
788
693
  }
789
- if (action.type === ActionType.FadeAlpha) {
790
- const fadeAlphaAction = action;
791
- if (!fadeAlphaAction.started) {
792
- fadeAlphaAction.delta = fadeAlphaAction.alpha - node.alpha;
793
- fadeAlphaAction.started = true;
794
- }
795
- if (elapsed < fadeAlphaAction.duration) {
796
- node.alpha = node.alpha + fadeAlphaAction.delta * (dt / fadeAlphaAction.duration);
797
- } else {
798
- node.alpha = fadeAlphaAction.alpha;
799
- fadeAlphaAction.running = false;
800
- fadeAlphaAction.completed = true;
694
+ }
695
+ /**
696
+ * Sets the runDuringTransition property based on descendants.
697
+ *
698
+ * @remarks This ensures that a parent action has its `runDuringTransition`
699
+ * property set to true if any of its descendants have their
700
+ * `runDuringTransition` property set to true. Parent actions do not have a
701
+ * way for the user to set this property directly; it is inferred (propagated
702
+ * up) from the descendants.
703
+ *
704
+ * @param action to propagate runDuringTransition property to
705
+ */
706
+ propagateRunDuringTransition(action) {
707
+ if (this.isParent(action)) {
708
+ if (action.descendants.some((child) => child.runDuringTransition)) {
709
+ action.runDuringTransition = true;
801
710
  }
711
+ action.children.forEach(
712
+ (child) => this.propagateRunDuringTransition(child)
713
+ );
802
714
  }
803
- if (action.type === ActionType.Rotate) {
804
- const rotateAction = action;
805
- if (!rotateAction.started) {
806
- if (rotateAction.byAngle !== void 0) {
807
- rotateAction.delta = rotateAction.byAngle;
808
- }
809
- if (rotateAction.toAngle !== void 0) {
810
- rotateAction.toAngle = M2c2KitHelpers.normalizeAngleRadians(
811
- rotateAction.toAngle
812
- );
813
- node.zRotation = M2c2KitHelpers.normalizeAngleRadians(node.zRotation);
814
- rotateAction.delta = rotateAction.toAngle - node.zRotation;
815
- if (rotateAction.shortestUnitArc === true && Math.abs(rotateAction.delta) > Math.PI) {
816
- rotateAction.delta = 2 * Math.PI - Math.abs(rotateAction.delta);
817
- }
818
- }
819
- rotateAction.started = true;
820
- rotateAction.finalValue = node.zRotation + rotateAction.delta;
821
- }
822
- if (elapsed < rotateAction.duration) {
823
- node.zRotation = node.zRotation + rotateAction.delta * (dt / rotateAction.duration);
824
- if (rotateAction.delta <= 0 && node.zRotation < rotateAction.finalValue) {
825
- node.zRotation = rotateAction.finalValue;
826
- }
827
- if (rotateAction.delta > 0 && node.zRotation > rotateAction.finalValue) {
828
- node.zRotation = rotateAction.finalValue;
829
- }
830
- } else {
831
- node.zRotation = rotateAction.finalValue;
832
- rotateAction.running = false;
833
- rotateAction.completed = true;
834
- }
715
+ }
716
+ /**
717
+ * Assigns durations to all actions in the hierarchy.
718
+ *
719
+ * @remarks Uses recursion to handle arbitrary level of nesting parent
720
+ * actions within parent actions.
721
+ *
722
+ * @param action - the action to assign durations to
723
+ */
724
+ assignDurations(action) {
725
+ action.duration = this.calculateDuration(action);
726
+ if (this.isParent(action)) {
727
+ action.children.forEach((child) => this.assignDurations(child));
835
728
  }
836
729
  }
837
730
  /**
@@ -848,106 +741,553 @@ class Action {
848
741
  if (action.type === ActionType.Group) {
849
742
  const groupAction = action;
850
743
  const duration = groupAction.children.map((child) => this.calculateDuration(child)).reduce((max, dur) => {
851
- return Math.max(max, dur);
744
+ return Math.max(max, dur.value);
852
745
  }, 0);
853
- return duration;
746
+ return new Futurable(duration);
854
747
  }
855
748
  if (action.type === ActionType.Sequence) {
856
749
  const sequenceAction = action;
857
750
  const duration = sequenceAction.children.map((child) => this.calculateDuration(child)).reduce((sum, dur) => {
858
- return sum + dur;
751
+ return sum + dur.value;
859
752
  }, 0);
860
- return duration;
753
+ return new Futurable(duration);
754
+ }
755
+ if (this.isRepeating(action)) {
756
+ return new Futurable();
861
757
  }
862
758
  return action.duration;
863
759
  }
864
760
  /**
865
- * Update each action's start and end offsets.
761
+ * Assigns start offsets to all actions in the hierarchy.
866
762
  *
867
763
  * @remarks Uses recursion to handle arbitrary level of nesting parent
868
764
  * actions within parent actions.
869
765
  *
870
- * @param action that needs assigning start and end offsets
766
+ * @param action - the action to assign start offsets to
871
767
  */
872
- calculateStartEndOffsets(action) {
873
- let parentStartOffset;
874
- if (action.parent === void 0) {
875
- parentStartOffset = 0;
876
- } else {
877
- parentStartOffset = action.parent.startOffset;
878
- }
879
- if (action.parent?.type === ActionType.Group) {
880
- action.startOffset = parentStartOffset;
881
- action.endOffset = action.startOffset + action.duration;
882
- } else if (action.parent?.type === ActionType.Sequence) {
883
- const parent = action.parent;
884
- let dur = 0;
885
- for (const a of parent.children) {
886
- if (a === action) {
887
- break;
888
- }
889
- dur = dur + a.duration;
890
- }
891
- action.startOffset = parentStartOffset + dur;
892
- action.endOffset = action.startOffset + action.duration;
893
- } else {
894
- action.startOffset = 0;
895
- action.endOffset = action.startOffset + action.duration;
896
- }
897
- if (action.isParent) {
898
- action.children?.forEach(
899
- (child) => this.calculateStartEndOffsets(child)
900
- );
768
+ assignStartOffsets(action) {
769
+ action.startOffset = this.calculateStartOffset(action);
770
+ if (this.isParent(action)) {
771
+ action.children.forEach((child) => this.assignStartOffsets(child));
901
772
  }
902
773
  }
903
774
  /**
904
- * Takes an action hierarchy and flattens to an array of non-nested actions
775
+ * Calculates the start offset. This is when an action should start,
776
+ * relative to the start time of its parent (if it has a parent).
905
777
  *
906
- * @remarks Uses recursion to handle arbitrary level of nesting parent
907
- * actions within parent actions
908
- *
909
- * @param action - the action to flatten
910
- * @param actions - the accumulator array of flattened actions. This will be
911
- * undefined on the first call, and an array on recursive calls
912
- * @returns flattened array of actions
778
+ * @param action - the action to calculate the start offset for
779
+ * @returns the start offset as a Futurable
913
780
  */
914
- flattenActions(action, actions) {
915
- if (!actions) {
916
- actions = new Array();
917
- actions.push(action);
781
+ calculateStartOffset(action) {
782
+ if (action.parent === void 0) {
783
+ return new Futurable(0);
918
784
  }
919
- if (action.isParent) {
920
- const parent = action;
921
- const children = parent.children;
922
- actions.push(...children);
923
- parent.children.filter((child) => child.isParent).forEach((child) => this.flattenActions(child, actions));
785
+ if (action.parent.type !== ActionType.Sequence) {
786
+ return action.parent.startOffset;
924
787
  }
925
- return actions;
788
+ const startOffset = new Futurable(0);
789
+ startOffset.add(action.parent.startOffset);
790
+ for (const siblingAction of action.parent.children) {
791
+ if (siblingAction === action) {
792
+ break;
793
+ }
794
+ startOffset.add(siblingAction.duration);
795
+ }
796
+ return startOffset;
926
797
  }
927
798
  /**
928
- * Parses the action hierarchy and assigns each action its parent and
929
- * root action.
799
+ * Evaluates an action, updating the node's properties as needed.
800
+ *
801
+ * @remarks This method is called every frame by the M2Node's `update()`
802
+ * method.
803
+ *
804
+ * @param action - the Action to be evaluated and possibly run
805
+ * @param node - the `M2Node` that the action will be run on
806
+ * @param now - the current elapsed time, from `performance.now()`
807
+ * @param dt - the time since the last frame (delta time)
808
+ */
809
+ static evaluateAction(action, node, now, dt) {
810
+ if (node.involvedInSceneTransition() && !action.runDuringTransition) {
811
+ return;
812
+ }
813
+ if (action.runStartTime === -1) {
814
+ action.assignRunStartTimes(action, now);
815
+ }
816
+ if (now < action.runStartTime + action.startOffset.value) {
817
+ return;
818
+ }
819
+ if (action.shouldBeRunning(now)) {
820
+ action.running = true;
821
+ }
822
+ if (action.isParent(action)) {
823
+ action.children.forEach((child) => {
824
+ Action.evaluateAction(child, node, now, dt);
825
+ });
826
+ if (!action.isRepeating(action)) {
827
+ if (!action.started) {
828
+ action.started = true;
829
+ }
830
+ if (action.running && action.completed) {
831
+ action.running = false;
832
+ }
833
+ return;
834
+ }
835
+ Action.evaluateRepeatingActions(action, now);
836
+ return;
837
+ }
838
+ if (!action.shouldBeRunning(now)) {
839
+ action.running = false;
840
+ }
841
+ if (action.running === false && action.completed === true) {
842
+ return;
843
+ }
844
+ const elapsed = now - (action.runStartTime + action.startOffset.value);
845
+ switch (action.type) {
846
+ case ActionType.Custom:
847
+ Action.evaluateCustomAction(action);
848
+ break;
849
+ case ActionType.Play:
850
+ Action.evaluatePlayAction(node, action);
851
+ break;
852
+ case ActionType.Wait:
853
+ Action.evaluateWaitAction(action, now);
854
+ break;
855
+ case ActionType.Move:
856
+ Action.evaluateMoveAction(action, node, elapsed);
857
+ break;
858
+ case ActionType.Scale:
859
+ Action.evaluateScaleAction(action, node, elapsed, dt);
860
+ break;
861
+ case ActionType.FadeAlpha:
862
+ Action.evaluateFadeAlphaAction(action, node, elapsed, dt);
863
+ break;
864
+ case ActionType.Rotate:
865
+ Action.evaluateRotateAction(action, node, elapsed, dt);
866
+ break;
867
+ default:
868
+ throw new Error(`Action type not recognized: ${action.type}`);
869
+ }
870
+ }
871
+ static evaluateRepeatingActions(action, now) {
872
+ if (!action.started) {
873
+ action.started = true;
874
+ }
875
+ if (action.repetitionHasCompleted) {
876
+ action.completedRepetitions++;
877
+ const repetitionDuration = action.children[0].duration.value;
878
+ action.cumulativeDuration = action.cumulativeDuration + repetitionDuration;
879
+ if (!isFinite(repetitionDuration)) {
880
+ throw "repetitionDuration is not finite";
881
+ }
882
+ if (!action.completed) {
883
+ action.restartAction(action, now);
884
+ } else {
885
+ if (action.type === ActionType.RepeatForever) {
886
+ throw new Error("RepeatForever action should never complete");
887
+ }
888
+ action.duration.assign(action.cumulativeDuration);
889
+ action.running = false;
890
+ }
891
+ }
892
+ }
893
+ static evaluateRotateAction(action, node, elapsed, dt) {
894
+ const rotateAction = action;
895
+ if (!rotateAction.started) {
896
+ if (rotateAction.byAngle !== void 0) {
897
+ rotateAction.delta = rotateAction.byAngle;
898
+ }
899
+ if (rotateAction.toAngle !== void 0) {
900
+ rotateAction.toAngle = M2c2KitHelpers.normalizeAngleRadians(
901
+ rotateAction.toAngle
902
+ );
903
+ node.zRotation = M2c2KitHelpers.normalizeAngleRadians(node.zRotation);
904
+ rotateAction.delta = rotateAction.toAngle - node.zRotation;
905
+ if (rotateAction.shortestUnitArc === true && Math.abs(rotateAction.delta) > Math.PI) {
906
+ rotateAction.delta = 2 * Math.PI - Math.abs(rotateAction.delta);
907
+ }
908
+ }
909
+ rotateAction.started = true;
910
+ rotateAction.finalValue = node.zRotation + rotateAction.delta;
911
+ }
912
+ if (elapsed < rotateAction.duration.value) {
913
+ node.zRotation = node.zRotation + rotateAction.delta * (dt / rotateAction.duration.value);
914
+ if (rotateAction.delta <= 0 && node.zRotation < rotateAction.finalValue) {
915
+ node.zRotation = rotateAction.finalValue;
916
+ }
917
+ if (rotateAction.delta > 0 && node.zRotation > rotateAction.finalValue) {
918
+ node.zRotation = rotateAction.finalValue;
919
+ }
920
+ } else {
921
+ node.zRotation = rotateAction.finalValue;
922
+ rotateAction.running = false;
923
+ rotateAction.completed = true;
924
+ }
925
+ }
926
+ static evaluateFadeAlphaAction(action, node, elapsed, dt) {
927
+ const fadeAlphaAction = action;
928
+ if (!fadeAlphaAction.started) {
929
+ fadeAlphaAction.delta = fadeAlphaAction.alpha - node.alpha;
930
+ fadeAlphaAction.started = true;
931
+ }
932
+ if (elapsed < fadeAlphaAction.duration.value) {
933
+ node.alpha = node.alpha + fadeAlphaAction.delta * (dt / fadeAlphaAction.duration.value);
934
+ } else {
935
+ node.alpha = fadeAlphaAction.alpha;
936
+ fadeAlphaAction.running = false;
937
+ fadeAlphaAction.completed = true;
938
+ }
939
+ }
940
+ static evaluateScaleAction(action, node, elapsed, dt) {
941
+ const scaleAction = action;
942
+ if (!scaleAction.started) {
943
+ scaleAction.delta = scaleAction.scale - node.scale;
944
+ scaleAction.started = true;
945
+ }
946
+ if (elapsed < scaleAction.duration.value) {
947
+ node.scale = node.scale + scaleAction.delta * (dt / scaleAction.duration.value);
948
+ } else {
949
+ node.scale = scaleAction.scale;
950
+ scaleAction.running = false;
951
+ scaleAction.completed = true;
952
+ }
953
+ }
954
+ static evaluateMoveAction(action, node, elapsed) {
955
+ const moveAction = action;
956
+ if (!moveAction.started) {
957
+ moveAction.dx = moveAction.point.x - node.position.x;
958
+ moveAction.dy = moveAction.point.y - node.position.y;
959
+ moveAction.startPoint.x = node.position.x;
960
+ moveAction.startPoint.y = node.position.y;
961
+ moveAction.started = true;
962
+ }
963
+ if (elapsed < moveAction.duration.value) {
964
+ node.position.x = moveAction.easing(
965
+ elapsed,
966
+ moveAction.startPoint.x,
967
+ moveAction.dx,
968
+ moveAction.duration.value
969
+ );
970
+ node.position.y = moveAction.easing(
971
+ elapsed,
972
+ moveAction.startPoint.y,
973
+ moveAction.dy,
974
+ moveAction.duration.value
975
+ );
976
+ } else {
977
+ node.position.x = moveAction.point.x;
978
+ node.position.y = moveAction.point.y;
979
+ moveAction.running = false;
980
+ moveAction.completed = true;
981
+ }
982
+ }
983
+ static evaluateWaitAction(action, now) {
984
+ const waitAction = action;
985
+ if (now > action.runStartTime + action.startOffset.value + action.duration.value) {
986
+ waitAction.running = false;
987
+ waitAction.completed = true;
988
+ }
989
+ }
990
+ static evaluatePlayAction(node, action) {
991
+ if (node.type !== M2NodeType.SoundPlayer) {
992
+ throw new Error("Play action can only be used with a SoundPlayer");
993
+ }
994
+ const playAction = action;
995
+ const soundPlayer = node;
996
+ const soundManager = soundPlayer.game.soundManager;
997
+ if (!playAction.started) {
998
+ const m2Sound = soundManager.getSound(soundPlayer.soundName);
999
+ if (m2Sound.audioBuffer) {
1000
+ const source = soundManager.audioContext.createBufferSource();
1001
+ source.buffer = m2Sound.audioBuffer;
1002
+ source.onended = () => {
1003
+ playAction.running = false;
1004
+ playAction.completed = true;
1005
+ const knownDuration = performance.now() - (action.runStartTime + action.startOffset.value);
1006
+ action.duration.assign(knownDuration);
1007
+ };
1008
+ source.connect(soundManager.audioContext.destination);
1009
+ source.start();
1010
+ playAction.started = true;
1011
+ } else {
1012
+ if (m2Sound.status === M2SoundStatus.Error) {
1013
+ throw new Error(
1014
+ `error loading sound ${m2Sound.soundName} (url ${m2Sound.url})`
1015
+ );
1016
+ }
1017
+ console.warn(
1018
+ `Play action: audio buffer not ready for sound ${soundPlayer.soundName} (url: ${m2Sound.url}); will try next frame`
1019
+ );
1020
+ if (m2Sound.status === M2SoundStatus.Deferred) {
1021
+ soundManager.fetchDeferredSound(m2Sound);
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ static evaluateCustomAction(action) {
1027
+ const customAction = action;
1028
+ customAction.callback();
1029
+ customAction.running = false;
1030
+ customAction.completed = true;
1031
+ }
1032
+ /**
1033
+ * Assigns RunStartTime to all actions in the hierarchy.
930
1034
  *
931
1035
  * @remarks Uses recursion to handle arbitrary level of nesting parent
932
- * actions within parent actions
1036
+ * actions within parent actions.
933
1037
  *
934
- * @param action
935
- * @param rootAction - top-level action passed to the run method
936
- * @param key - optional string to identify an action
1038
+ * @param action - the action to assign RunStartTime to
937
1039
  */
938
- assignParents(action, rootAction, key) {
939
- if (key !== void 0) {
940
- action.key = key;
1040
+ assignRunStartTimes(action, runStartTime) {
1041
+ action.runStartTime = runStartTime;
1042
+ if (action.isParent(action)) {
1043
+ action.children.forEach((child) => {
1044
+ action.assignRunStartTimes(child, runStartTime);
1045
+ });
941
1046
  }
942
- if (action.isParent) {
943
- const parent = action;
944
- const children = parent.children;
945
- children.forEach((child) => {
946
- child.parent = action;
947
- child.isChild = true;
1047
+ }
1048
+ /**
1049
+ * Configures action to be run again.
1050
+ *
1051
+ * @remarks This method is called on a repeating action's children when they
1052
+ * need to be run again.
1053
+ *
1054
+ * @param action - action to restart
1055
+ * @param now - current time
1056
+ */
1057
+ restartAction(action, now) {
1058
+ action.runStartTime = now;
1059
+ action.running = true;
1060
+ action.started = true;
1061
+ if (action.type === ActionType.Play) {
1062
+ action.duration = new Futurable();
1063
+ }
1064
+ if (action.isParent(action)) {
1065
+ action.children.forEach((child) => {
1066
+ action.restartAction(child, now);
948
1067
  });
949
- parent.children.filter((child) => child.isParent).forEach((child) => this.assignParents(child, rootAction, key));
1068
+ return;
950
1069
  }
1070
+ action.completed = false;
1071
+ }
1072
+ /**
1073
+ * Determines if the action should be running.
1074
+ *
1075
+ * @remarks An action should be running if current time is in the interval
1076
+ * [ start time + start offset, start time + start offset + duration ]
1077
+ *
1078
+ * @param now - current time
1079
+ * @returns true if the action should be running
1080
+ */
1081
+ shouldBeRunning(now) {
1082
+ return now >= this.runStartTime + this.startOffset.value && now <= this.runStartTime + this.startOffset.value + this.duration.value;
1083
+ }
1084
+ /**
1085
+ * Creates an action that will move a node to a point on the screen.
1086
+ *
1087
+ * @param options - {@link MoveActionOptions}
1088
+ * @returns The move action
1089
+ */
1090
+ static move(options) {
1091
+ return new MoveAction(
1092
+ options.point,
1093
+ new Futurable(options.duration),
1094
+ options.easing ?? Easings.linear,
1095
+ options.runDuringTransition ?? false
1096
+ );
1097
+ }
1098
+ /**
1099
+ * Creates an action that will wait a given duration before it is considered
1100
+ * complete.
1101
+ *
1102
+ * @param options - {@link WaitActionOptions}
1103
+ * @returns The wait action
1104
+ */
1105
+ static wait(options) {
1106
+ return new WaitAction(
1107
+ new Futurable(options.duration),
1108
+ options.runDuringTransition ?? false
1109
+ );
1110
+ }
1111
+ /**
1112
+ * Creates an action that will execute a callback function.
1113
+ *
1114
+ * @param options - {@link CustomActionOptions}
1115
+ * @returns The custom action
1116
+ */
1117
+ static custom(options) {
1118
+ return new CustomAction(
1119
+ options.callback,
1120
+ options.runDuringTransition ?? false
1121
+ );
1122
+ }
1123
+ /**
1124
+ * Creates an action that will play a sound.
1125
+ *
1126
+ * @remarks This action can only be used with a SoundPlayer node.
1127
+ * It will throw an error if used with any other node type.
1128
+ *
1129
+ * @param options - {@link PlayActionOptions}
1130
+ * @returns The play action
1131
+ */
1132
+ static play(options) {
1133
+ return new PlayAction(options?.runDuringTransition ?? false);
1134
+ }
1135
+ /**
1136
+ * Creates an action that will scale the node's size.
1137
+ *
1138
+ * @remarks Scaling is relative to any inherited scaling, which is
1139
+ * multiplicative. For example, if the node's parent is scaled to 2.0 and
1140
+ * this node's action scales to 3.0, then the node will appear 6 times as
1141
+ * large as original.
1142
+ *
1143
+ * @param options - {@link ScaleActionOptions}
1144
+ * @returns The scale action
1145
+ */
1146
+ static scale(options) {
1147
+ return new ScaleAction(
1148
+ options.scale,
1149
+ new Futurable(options.duration),
1150
+ options.runDuringTransition
1151
+ );
1152
+ }
1153
+ /**
1154
+ * Creates an action that will change the node's alpha (opacity).
1155
+ *
1156
+ * @remarks Alpha has multiplicative inheritance. For example, if the node's
1157
+ * parent is alpha .5 and this node's action fades alpha to .4, then the
1158
+ * node will appear with alpha .2.
1159
+ *
1160
+ * @param options - {@link FadeAlphaActionOptions}
1161
+ * @returns The fadeAlpha action
1162
+ */
1163
+ static fadeAlpha(options) {
1164
+ return new FadeAlphaAction(
1165
+ options.alpha,
1166
+ new Futurable(options.duration),
1167
+ options.runDuringTransition
1168
+ );
1169
+ }
1170
+ /**
1171
+ * Creates an action that will rotate the node.
1172
+ *
1173
+ * @remarks Rotate actions are applied to their children. In addition to this
1174
+ * node's rotate action, all ancestors' rotate actions will also be applied.
1175
+ *
1176
+ * @param options - {@link RotateActionOptions}
1177
+ * @returns The rotate action
1178
+ */
1179
+ static rotate(options) {
1180
+ if (options.byAngle !== void 0 && options.toAngle !== void 0) {
1181
+ throw new Error("rotate Action: cannot specify both byAngle and toAngle");
1182
+ }
1183
+ if (options.byAngle === void 0 && options.toAngle === void 0) {
1184
+ throw new Error("rotate Action: must specify either byAngle or toAngle");
1185
+ }
1186
+ if (options.toAngle === void 0 && options.shortestUnitArc !== void 0) {
1187
+ throw new Error(
1188
+ "rotate Action: shortestUnitArc can only be specified when toAngle is provided"
1189
+ );
1190
+ }
1191
+ if (options.toAngle !== void 0 && options.shortestUnitArc === void 0) {
1192
+ options.shortestUnitArc = true;
1193
+ }
1194
+ return new RotateAction(
1195
+ options.byAngle,
1196
+ options.toAngle,
1197
+ options.shortestUnitArc,
1198
+ new Futurable(options.duration),
1199
+ options.runDuringTransition
1200
+ );
1201
+ }
1202
+ /**
1203
+ * Creates an array of actions that will be run in order.
1204
+ *
1205
+ * @remarks The next action will not begin until the current one has
1206
+ * finished. The sequence will be considered completed when the last action
1207
+ * has completed.
1208
+ *
1209
+ * @param actions - One or more actions that form the sequence
1210
+ * @returns
1211
+ */
1212
+ static sequence(actions) {
1213
+ const sequence = new SequenceAction(actions);
1214
+ sequence.children = actions;
1215
+ return sequence;
1216
+ }
1217
+ /**
1218
+ * Create an array of actions that will be run simultaneously.
1219
+ *
1220
+ * @remarks All actions within the group will begin to run at the same time.
1221
+ * The group will be considered completed when the longest-running action
1222
+ * has completed.
1223
+ *
1224
+ * @param actions - One or more actions that form the group
1225
+ * @returns
1226
+ */
1227
+ static group(actions) {
1228
+ const group = new GroupAction(actions);
1229
+ group.children = actions;
1230
+ return group;
1231
+ }
1232
+ /**
1233
+ * Creates an action that will repeat another action a given number of times.
1234
+ *
1235
+ * @param options - {@link RepeatActionOptions}
1236
+ * @returns The repeat action
1237
+ */
1238
+ static repeat(options) {
1239
+ return new RepeatAction(
1240
+ options.action,
1241
+ options.count,
1242
+ options.runDuringTransition
1243
+ );
1244
+ }
1245
+ /**
1246
+ * Creates an action that will repeat another action forever.
1247
+ *
1248
+ * @remarks A repeat forever action is a special case of a repeat action
1249
+ * where the count is set to infinity.
1250
+ *
1251
+ * @param options - {@link RepeatForeverActionOptions}
1252
+ * @returns The repeat forever action
1253
+ */
1254
+ static repeatForever(options) {
1255
+ return new RepeatForeverAction(options.action, options.runDuringTransition);
1256
+ }
1257
+ /**
1258
+ * Type guard that returns true if the action is a parent action.
1259
+ *
1260
+ * @remarks Parent actions are Group, Sequence, Repeat, and RepeatForever
1261
+ * actions.
1262
+ *
1263
+ * @param action - action to check
1264
+ * @returns true if the action is a parent action
1265
+ */
1266
+ isParent(action) {
1267
+ return action.type === ActionType.Group || action.type === ActionType.Sequence || action.type === ActionType.Repeat || action.type === ActionType.RepeatForever;
1268
+ }
1269
+ /**
1270
+ * Type guard that returns true if the action can repeat.
1271
+ *
1272
+ * @remarks Repeating actions are Repeat and RepeatForever actions.
1273
+ *
1274
+ * @param action - action to check
1275
+ * @returns true if the action is a RepeatAction or RepeatForeverAction
1276
+ */
1277
+ isRepeating(action) {
1278
+ return action.type === ActionType.Repeat || action.type === ActionType.RepeatForever;
1279
+ }
1280
+ // Note: use getter and setter for completed property because we override
1281
+ // them in SequenceAction, GroupAction, RepeatAction, and
1282
+ // RepeatForeverAction.
1283
+ /**
1284
+ * Indicates whether the action has completed.
1285
+ */
1286
+ get completed() {
1287
+ return this._completed;
1288
+ }
1289
+ set completed(value) {
1290
+ this._completed = value;
951
1291
  }
952
1292
  }
953
1293
  class SequenceAction extends Action {
@@ -955,7 +1295,25 @@ class SequenceAction extends Action {
955
1295
  super();
956
1296
  this.type = ActionType.Sequence;
957
1297
  this.children = actions;
958
- this.isParent = true;
1298
+ }
1299
+ clone() {
1300
+ const clonedChildren = this.children.map((child) => child.clone());
1301
+ const clonedAction = Action.sequence(clonedChildren);
1302
+ clonedAction.children.forEach((child) => child.key = this.key);
1303
+ clonedAction.key = this.key;
1304
+ return clonedAction;
1305
+ }
1306
+ /**
1307
+ * Indicates whether the action has completed, taking into account all its
1308
+ * child actions.
1309
+ *
1310
+ * @remarks Is read-only for parent actions.
1311
+ */
1312
+ get completed() {
1313
+ return this.children.every((child) => child.completed);
1314
+ }
1315
+ get descendants() {
1316
+ return getParentActionDescendants(this);
959
1317
  }
960
1318
  }
961
1319
  class GroupAction extends Action {
@@ -964,16 +1322,137 @@ class GroupAction extends Action {
964
1322
  this.type = ActionType.Group;
965
1323
  this.children = new Array();
966
1324
  this.children = actions;
967
- this.isParent = true;
1325
+ }
1326
+ clone() {
1327
+ const clonedChildren = this.children.map((child) => child.clone());
1328
+ const clonedAction = Action.group(clonedChildren);
1329
+ clonedAction.children.forEach((child) => child.key = this.key);
1330
+ clonedAction.key = this.key;
1331
+ return clonedAction;
1332
+ }
1333
+ /**
1334
+ * Indicates whether the action has completed, taking into account all its
1335
+ * child actions.
1336
+ *
1337
+ * @remarks Is read-only for parent actions.
1338
+ */
1339
+ get completed() {
1340
+ return this.children.every((child) => child.completed);
1341
+ }
1342
+ get descendants() {
1343
+ return getParentActionDescendants(this);
1344
+ }
1345
+ }
1346
+ class RepeatAction extends Action {
1347
+ constructor(action, count, runDuringTransition = false) {
1348
+ super(runDuringTransition);
1349
+ this.type = ActionType.Repeat;
1350
+ this.completedRepetitions = 0;
1351
+ this.cumulativeDuration = 0;
1352
+ this.children = [action];
1353
+ this.count = count;
1354
+ this.duration = new Futurable();
1355
+ }
1356
+ clone() {
1357
+ if (this.children.length !== 1) {
1358
+ throw new Error("Repeat action must have exactly one child");
1359
+ }
1360
+ const clonedAction = Action.repeat({
1361
+ // RepeatAction always has exactly one child
1362
+ action: this.children[0].clone(),
1363
+ count: this.count,
1364
+ runDuringTransition: this.runDuringTransition
1365
+ });
1366
+ clonedAction.children[0].key = this.key;
1367
+ clonedAction.key = this.key;
1368
+ return clonedAction;
1369
+ }
1370
+ /**
1371
+ * Indicates whether the action has completed, taking into account all its
1372
+ * child actions and the number of repetitions.
1373
+ *
1374
+ * @remarks Is read-only for parent actions.
1375
+ */
1376
+ get completed() {
1377
+ return this.children.every((child) => child.completed) && this.completedRepetitions === this.count;
1378
+ }
1379
+ get descendantsAreCompleted() {
1380
+ return this.children.every((child) => child.completed);
1381
+ }
1382
+ /**
1383
+ * Indicates whether a single repetition of a repeating action has just
1384
+ * completed.
1385
+ *
1386
+ * @returns returns true if a repetition has completed
1387
+ */
1388
+ get repetitionHasCompleted() {
1389
+ return this.running && this.descendantsAreCompleted && !this.completed;
1390
+ }
1391
+ get descendants() {
1392
+ return getParentActionDescendants(this);
1393
+ }
1394
+ }
1395
+ class RepeatForeverAction extends RepeatAction {
1396
+ constructor(action, runDuringTransition = false) {
1397
+ super(action, Infinity, runDuringTransition);
1398
+ this.type = ActionType.RepeatForever;
1399
+ this.count = Infinity;
1400
+ }
1401
+ clone() {
1402
+ if (this.children.length !== 1) {
1403
+ throw new Error("RepeatForever action must have exactly one child");
1404
+ }
1405
+ const clonedAction = Action.repeatForever({
1406
+ // RepeatForeverAction always has exactly one child
1407
+ action: this.children[0].clone(),
1408
+ runDuringTransition: this.runDuringTransition
1409
+ });
1410
+ clonedAction.children[0].key = this.key;
1411
+ clonedAction.key = this.key;
1412
+ return clonedAction;
968
1413
  }
969
1414
  }
1415
+ function getParentActionDescendants(parentAction) {
1416
+ const descendants = [];
1417
+ function traverse(action) {
1418
+ if (action.isParent(action)) {
1419
+ for (const child of action.children) {
1420
+ descendants.push(child);
1421
+ traverse(child);
1422
+ }
1423
+ }
1424
+ }
1425
+ traverse(parentAction);
1426
+ return descendants;
1427
+ }
970
1428
  class CustomAction extends Action {
971
1429
  constructor(callback, runDuringTransition = false) {
972
1430
  super(runDuringTransition);
973
1431
  this.type = ActionType.Custom;
974
1432
  this.callback = callback;
975
- this.isParent = false;
976
- this.duration = 0;
1433
+ this.duration = new Futurable(0);
1434
+ }
1435
+ clone() {
1436
+ const cloned = Action.custom({
1437
+ callback: this.callback,
1438
+ runDuringTransition: this.runDuringTransition
1439
+ });
1440
+ cloned.key = this.key;
1441
+ return cloned;
1442
+ }
1443
+ }
1444
+ class PlayAction extends Action {
1445
+ constructor(runDuringTransition = false) {
1446
+ super(runDuringTransition);
1447
+ this.type = ActionType.Play;
1448
+ this.duration = new Futurable();
1449
+ }
1450
+ clone() {
1451
+ const cloned = Action.play({
1452
+ runDuringTransition: this.runDuringTransition
1453
+ });
1454
+ cloned.key = this.key;
1455
+ return cloned;
977
1456
  }
978
1457
  }
979
1458
  class WaitAction extends Action {
@@ -981,21 +1460,37 @@ class WaitAction extends Action {
981
1460
  super(runDuringTransition);
982
1461
  this.type = ActionType.Wait;
983
1462
  this.duration = duration;
984
- this.isParent = false;
1463
+ }
1464
+ clone() {
1465
+ const cloned = Action.wait({
1466
+ duration: this.duration.value,
1467
+ runDuringTransition: this.runDuringTransition
1468
+ });
1469
+ cloned.key = this.key;
1470
+ return cloned;
985
1471
  }
986
1472
  }
987
1473
  class MoveAction extends Action {
988
1474
  constructor(point, duration, easing, runDuringTransition) {
989
1475
  super(runDuringTransition);
990
1476
  this.type = ActionType.Move;
1477
+ this.startPoint = { x: NaN, y: NaN };
991
1478
  this.dx = 0;
992
1479
  this.dy = 0;
993
1480
  this.duration = duration;
994
1481
  this.point = point;
995
- this.isParent = false;
996
- this.startPoint = { x: NaN, y: NaN };
997
1482
  this.easing = easing;
998
1483
  }
1484
+ clone() {
1485
+ const cloned = Action.move({
1486
+ point: this.point,
1487
+ duration: this.duration.value,
1488
+ easing: this.easing,
1489
+ runDuringTransition: this.runDuringTransition
1490
+ });
1491
+ cloned.key = this.key;
1492
+ return cloned;
1493
+ }
999
1494
  }
1000
1495
  class ScaleAction extends Action {
1001
1496
  constructor(scale, duration, runDuringTransition = false) {
@@ -1004,7 +1499,15 @@ class ScaleAction extends Action {
1004
1499
  this.delta = 0;
1005
1500
  this.duration = duration;
1006
1501
  this.scale = scale;
1007
- this.isParent = false;
1502
+ }
1503
+ clone() {
1504
+ const cloned = Action.scale({
1505
+ scale: this.scale,
1506
+ duration: this.duration.value,
1507
+ runDuringTransition: this.runDuringTransition
1508
+ });
1509
+ cloned.key = this.key;
1510
+ return cloned;
1008
1511
  }
1009
1512
  }
1010
1513
  class FadeAlphaAction extends Action {
@@ -1014,7 +1517,15 @@ class FadeAlphaAction extends Action {
1014
1517
  this.delta = 0;
1015
1518
  this.duration = duration;
1016
1519
  this.alpha = alpha;
1017
- this.isParent = false;
1520
+ }
1521
+ clone() {
1522
+ const cloned = Action.fadeAlpha({
1523
+ alpha: this.alpha,
1524
+ duration: this.duration.value,
1525
+ runDuringTransition: this.runDuringTransition
1526
+ });
1527
+ cloned.key = this.key;
1528
+ return cloned;
1018
1529
  }
1019
1530
  }
1020
1531
  class RotateAction extends Action {
@@ -1027,7 +1538,17 @@ class RotateAction extends Action {
1027
1538
  this.byAngle = byAngle;
1028
1539
  this.toAngle = toAngle;
1029
1540
  this.shortestUnitArc = shortestUnitArc;
1030
- this.isParent = false;
1541
+ }
1542
+ clone() {
1543
+ const cloned = Action.rotate({
1544
+ byAngle: this.byAngle,
1545
+ toAngle: this.toAngle,
1546
+ shortestUnitArc: this.shortestUnitArc,
1547
+ duration: this.duration.value,
1548
+ runDuringTransition: this.runDuringTransition
1549
+ });
1550
+ cloned.key = this.key;
1551
+ return cloned;
1031
1552
  }
1032
1553
  }
1033
1554
 
@@ -1482,6 +2003,25 @@ class Uuid {
1482
2003
  );
1483
2004
  }
1484
2005
  }
2006
+ /**
2007
+ * Tests if a string is a valid UUID.
2008
+ *
2009
+ * @remarks Will match UUID versions 1 through 8, plus the nil UUID.
2010
+ *
2011
+ * @param uuid - the string to test
2012
+ * @returns true if the string is a valid UUID
2013
+ */
2014
+ static isValid(uuid) {
2015
+ if (!uuid) {
2016
+ return false;
2017
+ }
2018
+ if (uuid === "00000000-0000-0000-0000-000000000000") {
2019
+ return true;
2020
+ }
2021
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
2022
+ uuid
2023
+ );
2024
+ }
1485
2025
  }
1486
2026
 
1487
2027
  const M2EventType = {
@@ -1529,6 +2069,12 @@ function handleTextOptions(text, options) {
1529
2069
  if (options.fontSize !== void 0) {
1530
2070
  text.fontSize = options.fontSize;
1531
2071
  }
2072
+ if (options.interpolation) {
2073
+ text.interpolation = options.interpolation;
2074
+ }
2075
+ if (options.localize !== void 0) {
2076
+ text.localize = options.localize;
2077
+ }
1532
2078
  }
1533
2079
  function handleInterfaceOptions(node, options) {
1534
2080
  if (node.isDrawable) {
@@ -1552,7 +2098,7 @@ class M2Node {
1552
2098
  this._scale = 1;
1553
2099
  this.alpha = 1;
1554
2100
  this._zRotation = 0;
1555
- this.isUserInteractionEnabled = false;
2101
+ this._isUserInteractionEnabled = false;
1556
2102
  this.draggable = false;
1557
2103
  this.hidden = false;
1558
2104
  this.layout = {};
@@ -1564,7 +2110,6 @@ class M2Node {
1564
2110
  this.absoluteAlpha = 1;
1565
2111
  this.absoluteAlphaChange = 0;
1566
2112
  this.actions = new Array();
1567
- this.originalActions = new Array();
1568
2113
  this.eventListeners = new Array();
1569
2114
  this.uuid = Uuid.generate();
1570
2115
  this.needsInitialization = true;
@@ -2131,32 +2676,9 @@ class M2Node {
2131
2676
  } else ;
2132
2677
  }
2133
2678
  }
2134
- const uncompletedTransitionActions = this.actions.filter(
2135
- (action) => action.runDuringTransition && !action.completed
2679
+ this.actions.forEach(
2680
+ (action) => Action.evaluateAction(action, this, Globals.now, Globals.deltaTime)
2136
2681
  );
2137
- const uncompletedRegularActions = this.actions.filter(
2138
- (action) => !action.runDuringTransition && !action.completed
2139
- );
2140
- if (uncompletedTransitionActions.length > 0) {
2141
- uncompletedTransitionActions.forEach((action) => {
2142
- if (action.runStartTime === -1) {
2143
- action.runStartTime = Globals.now;
2144
- }
2145
- });
2146
- uncompletedTransitionActions.forEach(
2147
- (action) => Action.evaluateAction(action, this, Globals.now, Globals.deltaTime)
2148
- );
2149
- }
2150
- if (!this.involvedInSceneTransition() && uncompletedRegularActions.length > 0) {
2151
- uncompletedRegularActions.forEach((action) => {
2152
- if (action.runStartTime === -1) {
2153
- action.runStartTime = Globals.now;
2154
- }
2155
- });
2156
- uncompletedRegularActions.forEach(
2157
- (action) => Action.evaluateAction(action, this, Globals.now, Globals.deltaTime)
2158
- );
2159
- }
2160
2682
  function getSiblingConstraintUuids(parent, constraints) {
2161
2683
  const uuids = new Array();
2162
2684
  if (constraints === void 0) {
@@ -2239,8 +2761,7 @@ class M2Node {
2239
2761
  * Only needed if the action will be referred to later
2240
2762
  */
2241
2763
  run(action, key) {
2242
- this.actions.push(...action.initialize(this, key));
2243
- this.originalActions = this.actions.filter((action2) => action2.runDuringTransition === false).map((action2) => Action.cloneAction(action2, key));
2764
+ this.actions.push(action.initialize(key));
2244
2765
  }
2245
2766
  /**
2246
2767
  * Remove an action from this node. If the action is running, it will be
@@ -2358,6 +2879,12 @@ class M2Node {
2358
2879
  set scale(scale) {
2359
2880
  this._scale = scale;
2360
2881
  }
2882
+ get isUserInteractionEnabled() {
2883
+ return this._isUserInteractionEnabled;
2884
+ }
2885
+ set isUserInteractionEnabled(isUserInteractionEnabled) {
2886
+ this._isUserInteractionEnabled = isUserInteractionEnabled;
2887
+ }
2361
2888
  // from https://medium.com/@konduruharish/topological-sort-in-typescript-and-c-6d5ecc4bad95
2362
2889
  /**
2363
2890
  * For a given directed acyclic graph, topological ordering of the vertices will be identified using BFS
@@ -2372,7 +2899,11 @@ class M2Node {
2372
2899
  }
2373
2900
  edges.forEach((edge) => {
2374
2901
  if (inDegree.has(edge)) {
2375
- inDegree.set(edge, inDegree.get(edge) + 1);
2902
+ const inDegreeCount = inDegree.get(edge);
2903
+ if (inDegreeCount === void 0) {
2904
+ throw new Error(`Could not find inDegree for edge ${edge}`);
2905
+ }
2906
+ inDegree.set(edge, inDegreeCount + 1);
2376
2907
  } else {
2377
2908
  inDegree.set(edge, 1);
2378
2909
  }
@@ -2387,13 +2918,17 @@ class M2Node {
2387
2918
  while (queue.length > 0) {
2388
2919
  const current = queue.shift();
2389
2920
  if (current === void 0) {
2390
- throw "bad";
2921
+ throw "current vertex is undefined";
2391
2922
  }
2392
2923
  tSort.push(current);
2393
2924
  if (adjList.has(current)) {
2394
2925
  adjList.get(current)?.forEach((edge) => {
2395
- if (inDegree.has(edge) && inDegree.get(edge) > 0) {
2396
- const newDegree = inDegree.get(edge) - 1;
2926
+ const inDegreeCount = inDegree.get(edge);
2927
+ if (inDegreeCount === void 0) {
2928
+ throw new Error(`Could not find inDegree for edge ${edge}`);
2929
+ }
2930
+ if (inDegree.has(edge) && inDegreeCount > 0) {
2931
+ const newDegree = inDegreeCount - 1;
2397
2932
  inDegree.set(edge, newDegree);
2398
2933
  if (newDegree == 0) {
2399
2934
  queue.push(edge);
@@ -3340,6 +3875,9 @@ class Sprite extends M2Node {
3340
3875
  this.paint.setAlphaf(this.absoluteAlpha);
3341
3876
  }
3342
3877
  if (this.m2Image.status === M2ImageStatus.Ready && this.m2Image.canvaskitImage) {
3878
+ if (this.m2Image.isFallback) {
3879
+ this.drawFallbackImageBorder(canvas);
3880
+ }
3343
3881
  canvas.drawImage(this.m2Image.canvaskitImage, x, y, this.paint);
3344
3882
  } else {
3345
3883
  if (this.m2Image.status === M2ImageStatus.Deferred) {
@@ -3380,6 +3918,35 @@ class Sprite extends M2Node {
3380
3918
  }
3381
3919
  });
3382
3920
  }
3921
+ /**
3922
+ * Draws a rectangle border around the image to indicate that a fallback
3923
+ * image is being used.
3924
+ *
3925
+ * @remarks The size of the rectangle is the same as the image, but because
3926
+ * the stroke width of the paint is wider than 1 pixel (see method
3927
+ * `configureImageLocalization()` in `ImageManager.ts`), the rectangle will
3928
+ * be larger than the image and thus be visible.
3929
+ *
3930
+ * @param canvas - CanvasKit canvas to draw on
3931
+ */
3932
+ drawFallbackImageBorder(canvas) {
3933
+ const paint = this.game.imageManager.missingLocalizationImagePaint;
3934
+ if (!paint) {
3935
+ return;
3936
+ }
3937
+ const drawScale = Globals.canvasScale / this.absoluteScale;
3938
+ const rect = this.canvasKit.RRectXY(
3939
+ this.canvasKit.LTRBRect(
3940
+ (this.absolutePosition.x - this.anchorPoint.x * this.size.width * this.absoluteScale) * drawScale,
3941
+ (this.absolutePosition.y - this.anchorPoint.y * this.size.height * this.absoluteScale) * drawScale,
3942
+ (this.absolutePosition.x + this.size.width * this.absoluteScale - this.anchorPoint.x * this.size.width * this.absoluteScale) * drawScale,
3943
+ (this.absolutePosition.y + this.size.height * this.absoluteScale - this.anchorPoint.y * this.size.height * this.absoluteScale) * drawScale
3944
+ ),
3945
+ 0,
3946
+ 0
3947
+ );
3948
+ canvas.drawRRect(rect, paint);
3949
+ }
3383
3950
  }
3384
3951
 
3385
3952
  class Scene extends M2Node {
@@ -3842,82 +4409,379 @@ const deviceMetadataSchema = {
3842
4409
  description: "WebGL driver vendor and renderer. Taken from WEBGL_debug_renderer_info."
3843
4410
  }
3844
4411
  }
3845
- };
3846
-
3847
- class WebGlInfo {
4412
+ };
4413
+
4414
+ class WebGlInfo {
4415
+ /**
4416
+ * Returns graphics driver vendor and renderer information.
4417
+ *
4418
+ * @remarks Information is from parameters UNMASKED_VENDOR_WEBGL and
4419
+ * UNMASKED_RENDERER_WEBGL when asking for WEBGL_debug_renderer_info
4420
+ * from the WebGLRenderingContext.
4421
+ *
4422
+ * @returns string
4423
+ */
4424
+ static getRendererString() {
4425
+ const rendererInfoCanvas = document.createElement("canvas");
4426
+ rendererInfoCanvas.id = "webgl-renderer-info-canvas";
4427
+ rendererInfoCanvas.height = 0;
4428
+ rendererInfoCanvas.width = 0;
4429
+ rendererInfoCanvas.hidden = true;
4430
+ document.body.appendChild(rendererInfoCanvas);
4431
+ const gl = rendererInfoCanvas.getContext("webgl");
4432
+ let rendererString = "no webgl context";
4433
+ if (!gl) {
4434
+ return rendererString;
4435
+ }
4436
+ const debugRendererInfo = gl.getExtension("WEBGL_debug_renderer_info");
4437
+ if (debugRendererInfo != null) {
4438
+ rendererString = String(gl.getParameter(debugRendererInfo.UNMASKED_VENDOR_WEBGL)) + ", " + String(gl.getParameter(debugRendererInfo.UNMASKED_RENDERER_WEBGL));
4439
+ } else {
4440
+ rendererString = "no debug renderer info";
4441
+ }
4442
+ rendererInfoCanvas.remove();
4443
+ return rendererString;
4444
+ }
4445
+ /**
4446
+ * Removes the temporary canvas that was created to get WebGL information.
4447
+ */
4448
+ static dispose() {
4449
+ const rendererInfoCanvas = document.getElementById(
4450
+ "webgl-renderer-info-canvas"
4451
+ );
4452
+ if (rendererInfoCanvas) {
4453
+ rendererInfoCanvas.remove();
4454
+ }
4455
+ }
4456
+ }
4457
+
4458
+ class I18n {
4459
+ /**
4460
+ * The I18n class localizes text and images.
4461
+ *
4462
+ * @param game - game instance
4463
+ * @param options - {@link LocalizationOptions}
4464
+ */
4465
+ constructor(game, options) {
4466
+ this.locale = "";
4467
+ this.fallbackLocale = "en-US";
4468
+ this.baseLocale = "en-US";
4469
+ this.game = game;
4470
+ this._translation = this.mergeAdditionalTranslation(
4471
+ options.translation,
4472
+ options.additionalTranslation
4473
+ ) ?? {};
4474
+ if (this.translation.configuration?.baseLocale) {
4475
+ this.baseLocale = this.translation.configuration.baseLocale;
4476
+ }
4477
+ if (options.missingLocalizationColor) {
4478
+ this.missingLocalizationColor = options.missingLocalizationColor;
4479
+ }
4480
+ if (options.locale) {
4481
+ this.locale = options.locale;
4482
+ }
4483
+ if (options.fallbackLocale) {
4484
+ this.fallbackLocale = options.fallbackLocale;
4485
+ }
4486
+ }
4487
+ /**
4488
+ * Initializes the I18n instance and sets the initial locale.
4489
+ *
4490
+ * @remarks If the game instance has been configured to use a data store,
4491
+ * the previously used locale and fallback locale will be retrieved from the
4492
+ * data store if they have been previously set.
4493
+ */
4494
+ async initialize() {
4495
+ await this.configureInitialLocale();
4496
+ }
4497
+ async configureInitialLocale() {
4498
+ if (this.game.hasDataStores()) {
4499
+ const locale = await this.game.storeGetItem("locale");
4500
+ const fallbackLocale = await this.game.storeGetItem("fallbackLocale");
4501
+ if (typeof locale === "string" && typeof fallbackLocale === "string") {
4502
+ this.locale = locale;
4503
+ this.fallbackLocale = fallbackLocale;
4504
+ return;
4505
+ }
4506
+ }
4507
+ if (this.locale?.toLowerCase() === "auto") {
4508
+ const attemptedLocale = this.getEnvironmentLocale();
4509
+ if (attemptedLocale) {
4510
+ if (this.localeTranslationAvailable(attemptedLocale)) {
4511
+ this.locale = attemptedLocale;
4512
+ if (!this.localeTranslationAvailable(this.fallbackLocale)) {
4513
+ this.fallbackLocale = this.baseLocale;
4514
+ }
4515
+ } else {
4516
+ if (this.fallbackLocale && this.localeTranslationAvailable(this.fallbackLocale)) {
4517
+ console.warn(
4518
+ `auto locale requested, but detected locale ${attemptedLocale} does not have translation. Setting locale to fallback locale ${this.fallbackLocale}`
4519
+ );
4520
+ this.locale = this.fallbackLocale;
4521
+ this.fallbackLocale = this.baseLocale;
4522
+ } else {
4523
+ console.warn(
4524
+ `auto locale requested, but detected locale ${attemptedLocale} does not have translation, and fallback locale does not have translation or was not specified (fallback locale is ${this.fallbackLocale}). Setting locale to base locale ${this.baseLocale}.`
4525
+ );
4526
+ this.locale = this.baseLocale;
4527
+ this.fallbackLocale = this.baseLocale;
4528
+ }
4529
+ }
4530
+ } else {
4531
+ if (this.fallbackLocale && this.localeTranslationAvailable(this.fallbackLocale)) {
4532
+ console.warn(
4533
+ `auto locale requested, but environment cannot detect locale. Setting locale to fallback locale ${this.fallbackLocale}`
4534
+ );
4535
+ this.locale = this.fallbackLocale;
4536
+ this.fallbackLocale = this.baseLocale;
4537
+ } else {
4538
+ console.warn(
4539
+ `auto locale requested, but environment cannot detect locale, and fallback locale does not have translation or was not specified (fallback locale is ${this.fallbackLocale}). Setting locale to base locale ${this.baseLocale}.`
4540
+ );
4541
+ this.locale = this.baseLocale;
4542
+ this.fallbackLocale = this.baseLocale;
4543
+ }
4544
+ }
4545
+ } else {
4546
+ this.locale = this.locale ?? "";
4547
+ if (!this.fallbackLocale) {
4548
+ this.fallbackLocale = this.baseLocale;
4549
+ }
4550
+ }
4551
+ }
4552
+ localeTranslationAvailable(locale) {
4553
+ return this.translation[locale] !== void 0 || locale === this.baseLocale;
4554
+ }
4555
+ switchToLocale(locale) {
4556
+ this.locale = locale;
4557
+ this.game.nodes.filter((node) => node.isText).forEach((node) => node.needsInitialization = true);
4558
+ this.game.imageManager.reinitializeLocalizedImages();
4559
+ if (this.game && this.game.hasDataStores()) {
4560
+ this.game.storeSetItem("locale", this.locale);
4561
+ this.game.storeSetItem("fallbackLocale", this.fallbackLocale);
4562
+ }
4563
+ }
4564
+ /**
4565
+ *
4566
+ * @param key - Translation key
4567
+ * @param interpolation - Interpolation keys and values to replace
4568
+ * placeholders in the translated text
4569
+ * @returns a `TextLocalizationResult` object with the localized text, font
4570
+ * information, and whether the translation is a fallback.
4571
+ */
4572
+ getTextLocalization(key, interpolation) {
4573
+ let localizedText = "";
4574
+ let isFallbackOrMissingTranslation = false;
4575
+ let tf = this.tf(key, interpolation);
4576
+ if (tf?.text !== void 0) {
4577
+ localizedText = tf.text;
4578
+ } else {
4579
+ tf = this.tf(key, {
4580
+ useFallbackLocale: true,
4581
+ ...interpolation
4582
+ });
4583
+ if (tf === void 0 || tf.text === void 0) {
4584
+ localizedText = key;
4585
+ } else {
4586
+ localizedText = tf.text;
4587
+ }
4588
+ isFallbackOrMissingTranslation = true;
4589
+ }
4590
+ return {
4591
+ text: localizedText,
4592
+ fontName: tf?.fontName,
4593
+ fontNames: tf?.fontNames,
4594
+ isFallbackOrMissingTranslation
4595
+ };
4596
+ }
3848
4597
  /**
3849
- * Returns graphics driver vendor and renderer information.
4598
+ * Returns the translation text for the given key in the current locale.
3850
4599
  *
3851
- * @remarks Information is from parameters UNMASKED_VENDOR_WEBGL and
3852
- * UNMASKED_RENDERER_WEBGL when asking for WEBGL_debug_renderer_info
3853
- * from the WebGLRenderingContext.
4600
+ * @remarks Optional interpolation keys and values can be provided to replace
4601
+ * placeholders in the translated text. Placeholders are denoted by double
4602
+ * curly braces.
3854
4603
  *
3855
- * @returns string
3856
- */
3857
- static getRendererString() {
3858
- const rendererInfoCanvas = document.createElement("canvas");
3859
- rendererInfoCanvas.id = "webgl-renderer-info-canvas";
3860
- rendererInfoCanvas.height = 0;
3861
- rendererInfoCanvas.width = 0;
3862
- rendererInfoCanvas.hidden = true;
3863
- document.body.appendChild(rendererInfoCanvas);
3864
- const gl = rendererInfoCanvas.getContext("webgl");
3865
- let rendererString = "no webgl context";
3866
- if (!gl) {
3867
- return rendererString;
4604
+ * @param key - key to look up in the translation
4605
+ * @param options - `TranslationOptions`, such as interpolation keys/values
4606
+ * and whether to translate using the fallback locale
4607
+ * @returns the translation text for the key in the current locale, or
4608
+ * undefined if the key is not found
4609
+ *
4610
+ * @example
4611
+ *
4612
+ * ```
4613
+ * const translation: Translation = {
4614
+ * "en-US": {
4615
+ * "GREETING": "Hello, {{name}}."
4616
+ * }
4617
+ * }
4618
+ * ...
4619
+ * i18n.t("GREETING", { name: "World" }); // returns "Hello, World."
4620
+ *
4621
+ * ```
4622
+ */
4623
+ t(key, options) {
4624
+ const { useFallbackLocale, ...interpolationMap } = options ?? {};
4625
+ if (useFallbackLocale !== true) {
4626
+ const t = this.translation[this.locale]?.[key];
4627
+ if (this.isStringOrTextWithFontCustomization(t)) {
4628
+ return this.insertInterpolations(
4629
+ this.getKeyText(t),
4630
+ interpolationMap
4631
+ );
4632
+ }
4633
+ return void 0;
3868
4634
  }
3869
- const debugRendererInfo = gl.getExtension("WEBGL_debug_renderer_info");
3870
- if (debugRendererInfo != null) {
3871
- rendererString = String(gl.getParameter(debugRendererInfo.UNMASKED_VENDOR_WEBGL)) + ", " + String(gl.getParameter(debugRendererInfo.UNMASKED_RENDERER_WEBGL));
3872
- } else {
3873
- rendererString = "no debug renderer info";
4635
+ const fallbackT = this.translation[this.fallbackLocale]?.[key];
4636
+ if (this.isStringOrTextWithFontCustomization(fallbackT)) {
4637
+ return this.insertInterpolations(
4638
+ this.getKeyText(fallbackT),
4639
+ interpolationMap
4640
+ );
3874
4641
  }
3875
- rendererInfoCanvas.remove();
3876
- return rendererString;
4642
+ return void 0;
3877
4643
  }
3878
4644
  /**
3879
- * Removes the temporary canvas that was created to get WebGL information.
4645
+ * Returns the translation text and font information for the given key in the
4646
+ * current locale.
4647
+ *
4648
+ * @remarks Optional interpolation keys and values can be provided to replace
4649
+ * placeholders in the translated text. Placeholders are denoted by double
4650
+ * curly braces. See method {@link I18n.t()} for interpolation example.
4651
+ *
4652
+ * @param key - key to look up in the translation
4653
+ * @param options - `TranslationOptions`, such as interpolation keys/values
4654
+ * and whether to translate using the fallback locale
4655
+ * @returns the translation text and font information for the key in the
4656
+ * current locale, or undefined if the key is not found
3880
4657
  */
3881
- static dispose() {
3882
- const rendererInfoCanvas = document.getElementById(
3883
- "webgl-renderer-info-canvas"
3884
- );
3885
- if (rendererInfoCanvas) {
3886
- rendererInfoCanvas.remove();
4658
+ tf(key, options) {
4659
+ const { useFallbackLocale, ...interpolationMap } = options ?? {};
4660
+ if (useFallbackLocale !== true) {
4661
+ const t = this.translation[this.locale]?.[key];
4662
+ if (this.isStringOrTextWithFontCustomization(t)) {
4663
+ const tf = this.getKeyTextAndFont(t, this.locale);
4664
+ if (tf.text) {
4665
+ tf.text = this.insertInterpolations(
4666
+ tf.text,
4667
+ interpolationMap
4668
+ );
4669
+ }
4670
+ return tf;
4671
+ }
4672
+ return void 0;
4673
+ }
4674
+ const fallbackTranslation = this.translation[this.fallbackLocale]?.[key];
4675
+ if (this.isStringOrTextWithFontCustomization(fallbackTranslation)) {
4676
+ const tf = this.getKeyTextAndFont(
4677
+ fallbackTranslation,
4678
+ this.fallbackLocale
4679
+ );
4680
+ if (tf.text) {
4681
+ tf.text = this.insertInterpolations(
4682
+ tf.text,
4683
+ interpolationMap
4684
+ );
4685
+ }
4686
+ return tf;
3887
4687
  }
4688
+ return void 0;
3888
4689
  }
3889
- }
3890
-
3891
- class I18n {
3892
- constructor(options) {
3893
- this.locale = "";
3894
- this.fallbackLocale = "en";
3895
- this.environmentLocale = this.getEnvironmentLocale();
3896
- this.options = options;
3897
- this._translations = this.mergeAdditionalTranslations(
3898
- options.translations,
3899
- options.additionalTranslations
3900
- ) ?? {};
3901
- if (options.locale.toLowerCase() === "auto") {
3902
- this.locale = this.environmentLocale;
3903
- if (!this.locale) {
3904
- if (options.fallbackLocale) {
3905
- this.fallbackLocale = options.fallbackLocale;
3906
- console.warn(
3907
- `auto locale requested, but environment cannot provide locale. Using fallback locale ${options.fallbackLocale}`
3908
- );
3909
- } else {
3910
- console.warn(
3911
- `auto locale requested, but environment cannot provide locale. Defaulting to "en".`
3912
- );
4690
+ getKeyText(t) {
4691
+ if (this.isTextWithFontCustomization(t)) {
4692
+ return t.text;
4693
+ }
4694
+ return t;
4695
+ }
4696
+ getKeyTextAndFont(t, locale) {
4697
+ let fontNames = new Array();
4698
+ if (this.isString(this.translation[locale]?.fontName)) {
4699
+ fontNames.push(this.translation[locale].fontName);
4700
+ } else if (this.isStringArray(this.translation[locale]?.fontName)) {
4701
+ fontNames.push(...this.translation[locale].fontName);
4702
+ } else {
4703
+ fontNames.push("default");
4704
+ }
4705
+ let text;
4706
+ if (this.isTextWithFontCustomization(t)) {
4707
+ text = t.text;
4708
+ if (this.isString(t.additionalFontName)) {
4709
+ fontNames.push(t.additionalFontName);
4710
+ }
4711
+ if (this.isStringArray(t.additionalFontName)) {
4712
+ fontNames.push(...t.additionalFontName);
4713
+ }
4714
+ if (t.overrideFontName) {
4715
+ fontNames.length = 0;
4716
+ if (this.isString(t.overrideFontName)) {
4717
+ fontNames.push(t.overrideFontName);
4718
+ }
4719
+ if (this.isStringArray(t.overrideFontName)) {
4720
+ fontNames.push(...t.overrideFontName);
3913
4721
  }
3914
4722
  }
3915
4723
  } else {
3916
- this.locale = options.locale;
3917
- if (options.fallbackLocale) {
3918
- this.fallbackLocale = options.fallbackLocale;
4724
+ text = t;
4725
+ }
4726
+ fontNames = fontNames.filter((f) => f !== "default");
4727
+ switch (fontNames.length) {
4728
+ case 0:
4729
+ return { text };
4730
+ case 1:
4731
+ return { text, fontName: fontNames[0] };
4732
+ default:
4733
+ return { text, fontNames };
4734
+ }
4735
+ }
4736
+ insertInterpolations(text, options) {
4737
+ if (!options) {
4738
+ return text;
4739
+ }
4740
+ return text.replace(/\{\{(.*?)\}\}/g, (_match, key) => {
4741
+ if (Object.prototype.hasOwnProperty.call(options, key)) {
4742
+ return options[key];
4743
+ } else {
4744
+ throw new Error(
4745
+ `insertInterpolations(): placeholder "${key}" not found. Text was ${text}, provided interpolation was ${JSON.stringify(options)}`
4746
+ );
4747
+ }
4748
+ });
4749
+ }
4750
+ get translation() {
4751
+ return this._translation;
4752
+ }
4753
+ set translation(value) {
4754
+ this._translation = value;
4755
+ }
4756
+ getEnvironmentLocale() {
4757
+ return (navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.language) ?? "";
4758
+ }
4759
+ mergeAdditionalTranslation(baseTranslation, additionalTranslation) {
4760
+ if (!baseTranslation && !additionalTranslation) {
4761
+ return void 0;
4762
+ }
4763
+ if (!additionalTranslation) {
4764
+ return baseTranslation;
4765
+ }
4766
+ if (!baseTranslation) {
4767
+ return additionalTranslation;
4768
+ }
4769
+ const result = {};
4770
+ const processedLocales = new Array();
4771
+ for (const locale in baseTranslation) {
4772
+ processedLocales.push(locale);
4773
+ result[locale] = {
4774
+ ...baseTranslation[locale],
4775
+ ...additionalTranslation[locale]
4776
+ };
4777
+ }
4778
+ for (const locale in additionalTranslation) {
4779
+ if (processedLocales.includes(locale)) {
4780
+ continue;
3919
4781
  }
4782
+ result[locale] = additionalTranslation[locale];
3920
4783
  }
4784
+ return result;
3921
4785
  }
3922
4786
  static makeLocalizationParameters() {
3923
4787
  const localizationParameters = JSON.parse(
@@ -3932,64 +4796,34 @@ class I18n {
3932
4796
  default: null,
3933
4797
  description: `Locale to use if requested locale translation is not available, or if "auto" locale was requested and environment cannot provide a locale.`
3934
4798
  },
3935
- missing_translation_font_color: {
4799
+ missing_localization_color: {
3936
4800
  type: ["array", "null"],
3937
4801
  default: null,
3938
- description: "Font color for strings that are missing translation and use the fallback locale or untranslated string, [r,g,b,a].",
4802
+ description: "Font color for strings that are missing translation and outline color for images that are missing localization, [r,g,b,a].",
3939
4803
  items: {
3940
4804
  type: "number"
3941
4805
  }
3942
4806
  },
3943
- translations: {
4807
+ translation: {
3944
4808
  type: ["object", "null"],
3945
4809
  default: null,
3946
- description: "Additional translations for localization."
4810
+ description: "Additional translation for localization."
3947
4811
  }
3948
4812
  })
3949
4813
  );
3950
4814
  return localizationParameters;
3951
4815
  }
3952
- t(key, useFallback = false) {
3953
- if (useFallback) {
3954
- return this._translations[this.fallbackLocale]?.[key];
3955
- }
3956
- return this._translations[this.locale]?.[key];
3957
- }
3958
- get translations() {
3959
- return this._translations;
4816
+ isTextWithFontCustomization(value) {
4817
+ return value?.text !== void 0;
3960
4818
  }
3961
- set translations(value) {
3962
- this._translations = value;
4819
+ isStringOrTextWithFontCustomization(value) {
4820
+ return typeof value === "string" || this.isTextWithFontCustomization(value);
3963
4821
  }
3964
- getEnvironmentLocale() {
3965
- return navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.language;
4822
+ isStringArray(value) {
4823
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
3966
4824
  }
3967
- mergeAdditionalTranslations(baseTranslations, additionalTranslations) {
3968
- if (!baseTranslations && !additionalTranslations) {
3969
- return void 0;
3970
- }
3971
- if (!additionalTranslations) {
3972
- return baseTranslations;
3973
- }
3974
- if (!baseTranslations) {
3975
- return additionalTranslations;
3976
- }
3977
- const result = {};
3978
- const processedLocales = new Array();
3979
- for (const locale in baseTranslations) {
3980
- processedLocales.push(locale);
3981
- result[locale] = {
3982
- ...baseTranslations[locale],
3983
- ...additionalTranslations[locale]
3984
- };
3985
- }
3986
- for (const locale in additionalTranslations) {
3987
- if (processedLocales.includes(locale)) {
3988
- continue;
3989
- }
3990
- result[locale] = additionalTranslations[locale];
3991
- }
3992
- return result;
4825
+ isString(value) {
4826
+ return typeof value === "string";
3993
4827
  }
3994
4828
  }
3995
4829
 
@@ -4041,12 +4875,18 @@ class ImageManager {
4041
4875
  const m2Image = {
4042
4876
  imageName: browserImage.imageName,
4043
4877
  url,
4878
+ originalUrl: url,
4879
+ isFallback: false,
4880
+ localize: browserImage.localize ?? false,
4044
4881
  svgString: browserImage.svgString,
4045
4882
  canvaskitImage: void 0,
4046
4883
  width: browserImage.width,
4047
4884
  height: browserImage.height,
4048
4885
  status: browserImage.lazy ? M2ImageStatus.Deferred : M2ImageStatus.Loading
4049
4886
  };
4887
+ if (m2Image.localize) {
4888
+ this.configureImageLocalization(m2Image);
4889
+ }
4050
4890
  this.images[browserImage.imageName] = m2Image;
4051
4891
  if (m2Image.status === M2ImageStatus.Loading) {
4052
4892
  return this.renderM2Image(m2Image);
@@ -4055,6 +4895,80 @@ class ImageManager {
4055
4895
  });
4056
4896
  await Promise.all(renderImagesPromises);
4057
4897
  }
4898
+ configureImageLocalization(m2Image) {
4899
+ m2Image.fallbackLocalizationUrls = new Array();
4900
+ if (m2Image.originalUrl && this.game.i18n?.locale) {
4901
+ m2Image.status = "Deferred";
4902
+ if (this.game.i18n?.fallbackLocale) {
4903
+ if (this.game.i18n?.fallbackLocale !== this.game.i18n?.baseLocale) {
4904
+ m2Image.fallbackLocalizationUrls.push(
4905
+ this.localizeImageUrl(
4906
+ m2Image.originalUrl,
4907
+ this.game.i18n.fallbackLocale
4908
+ )
4909
+ );
4910
+ }
4911
+ }
4912
+ if (this.game.i18n?.locale === this.game.i18n?.baseLocale) {
4913
+ m2Image.url = m2Image.originalUrl;
4914
+ } else {
4915
+ m2Image.url = this.localizeImageUrl(
4916
+ m2Image.originalUrl,
4917
+ this.game.i18n.locale
4918
+ );
4919
+ }
4920
+ if (m2Image.url !== m2Image.originalUrl) {
4921
+ m2Image.fallbackLocalizationUrls.push(m2Image.originalUrl);
4922
+ }
4923
+ if (this.game.i18n.missingLocalizationColor && !this.missingLocalizationImagePaint) {
4924
+ this.missingLocalizationImagePaint = CanvasKitHelpers.makePaint(
4925
+ this.canvasKit,
4926
+ this.game.i18n.missingLocalizationColor,
4927
+ this.canvasKit.PaintStyle.Stroke,
4928
+ true
4929
+ );
4930
+ this.missingLocalizationImagePaint.setStrokeWidth(4);
4931
+ }
4932
+ }
4933
+ }
4934
+ /**
4935
+ * Localizes the image URL by appending the locale to the image URL,
4936
+ * immediately before the file extension.
4937
+ *
4938
+ * @remarks For example, `https://url.com/file.png` in en-US locale
4939
+ * becomes `https://url.com/file.en-US.png`. A URL without an extension
4940
+ * will throw an error.
4941
+ *
4942
+ * @param url - url of the image
4943
+ * @param locale - locale in format of xx-YY, where xx is the language code
4944
+ * and YY is the country code
4945
+ * @returns localized url
4946
+ */
4947
+ localizeImageUrl(url, locale) {
4948
+ const extensionIndex = url.lastIndexOf(".");
4949
+ if (extensionIndex === -1) {
4950
+ throw new Error("URL does not have an extension");
4951
+ }
4952
+ const localizedUrl = url.slice(0, extensionIndex) + `.${locale}` + url.slice(extensionIndex);
4953
+ return localizedUrl;
4954
+ }
4955
+ /**
4956
+ * Sets an image to be re-rendered within the current locale.
4957
+ */
4958
+ reinitializeLocalizedImages() {
4959
+ Object.keys(this.game.imageManager.images).forEach((imageName) => {
4960
+ const m2Image = this.game.imageManager.images[imageName];
4961
+ if (m2Image.localize) {
4962
+ this.game.imageManager.configureImageLocalization(m2Image);
4963
+ }
4964
+ });
4965
+ const sprites = this.game.nodes.filter(
4966
+ (node) => node.type === M2NodeType.Sprite
4967
+ );
4968
+ sprites.forEach((sprite) => {
4969
+ sprite.needsInitialization = true;
4970
+ });
4971
+ }
4058
4972
  checkImageNamesForDuplicates(browserImages) {
4059
4973
  const findDuplicates = (arr) => arr.filter((item, index) => arr.indexOf(item) != index);
4060
4974
  const duplicateImageNames = findDuplicates(
@@ -4077,7 +4991,26 @@ class ImageManager {
4077
4991
  */
4078
4992
  prepareDeferredImage(image) {
4079
4993
  image.status = M2ImageStatus.Loading;
4080
- return this.renderM2Image(image);
4994
+ image.isFallback = false;
4995
+ return this.renderM2Image(image).catch(async () => {
4996
+ image.isFallback = true;
4997
+ while (image.fallbackLocalizationUrls?.length) {
4998
+ image.url = image.fallbackLocalizationUrls.shift();
4999
+ try {
5000
+ await this.renderM2Image(image);
5001
+ } catch (error) {
5002
+ if (image.fallbackLocalizationUrls.length === 0) {
5003
+ if (error instanceof Error) {
5004
+ throw error;
5005
+ } else {
5006
+ throw new Error(
5007
+ `prepareDeferredImage(): unable to render image named ${image.imageName}. image source was ${image.svgString ? "svgString" : `url: ${image.url}`}`
5008
+ );
5009
+ }
5010
+ }
5011
+ }
5012
+ }
5013
+ });
4081
5014
  }
4082
5015
  /**
4083
5016
  * Uses the browser to render an image to a CanvasKit Image and make it
@@ -4248,6 +5181,217 @@ class ImageManager {
4248
5181
  }
4249
5182
  }
4250
5183
 
5184
+ class SoundManager {
5185
+ constructor(game, baseUrls) {
5186
+ this.sounds = {};
5187
+ this.game = game;
5188
+ this.baseUrls = baseUrls;
5189
+ }
5190
+ get audioContext() {
5191
+ if (!this._audioContext) {
5192
+ if (!navigator.userActivation.hasBeenActive) {
5193
+ throw new Error(
5194
+ "AudioContext cannot be created until user has interacted with the page"
5195
+ );
5196
+ }
5197
+ this._audioContext = new AudioContext();
5198
+ }
5199
+ return this._audioContext;
5200
+ }
5201
+ /**
5202
+ * Loads sound assets during the game initialization.
5203
+ *
5204
+ * @internal For m2c2kit library use only
5205
+ *
5206
+ * @remarks Typically, a user won't call this because the m2c2kit
5207
+ * framework will call this automatically. At initialization, sounds can
5208
+ * only be fetched, not decoded because the AudioContext can not yet
5209
+ * be created (it requires a user interaction).
5210
+ *
5211
+ * @param soundAssets - array of SoundAsset objects
5212
+ */
5213
+ initializeSounds(soundAssets) {
5214
+ if (!soundAssets) {
5215
+ return Promise.resolve();
5216
+ }
5217
+ return this.loadSounds(soundAssets);
5218
+ }
5219
+ /**
5220
+ * Loads an array of sound assets and makes them ready for the game.
5221
+ *
5222
+ * @remarks Loading a sound consists of 1) fetching the sound file and 2)
5223
+ * decoding the sound data. The sound is then ready to be played. Step 1
5224
+ * can be done at any time, but step 2 requires an `AudioContext`, which
5225
+ * can only be created after a user interaction. If a play `Action` is
5226
+ * attempted before the sound is ready (either it has not been fetched or
5227
+ * decoded), the play `Action` will log a warning to the console and the
5228
+ * loading process will continue in the background, and the sound will play
5229
+ * when ready. This `loadSounds()` method **does not** have to be awaited.
5230
+ *
5231
+ * @param soundAssets - an array of {@link SoundAsset}
5232
+ * @returns A promise that completes when all sounds have loaded
5233
+ */
5234
+ loadSounds(soundAssets) {
5235
+ if (soundAssets.length === 0) {
5236
+ return Promise.resolve();
5237
+ }
5238
+ soundAssets.forEach((sound) => {
5239
+ let url = sound.url;
5240
+ if (!M2c2KitHelpers.urlHasScheme(sound.url)) {
5241
+ url = M2c2KitHelpers.getUrlFromManifest(
5242
+ this.game,
5243
+ `${this.baseUrls.assets}/${sound.url}`
5244
+ );
5245
+ }
5246
+ const m2Sound = {
5247
+ soundName: sound.soundName,
5248
+ data: void 0,
5249
+ audioBuffer: void 0,
5250
+ url,
5251
+ status: sound.lazy ? M2SoundStatus.Deferred : M2SoundStatus.WillFetch
5252
+ };
5253
+ if (this.sounds[sound.soundName]) {
5254
+ console.warn(
5255
+ `A sound named ${sound.soundName} has already been loaded. It will be replaced.`
5256
+ );
5257
+ }
5258
+ this.sounds[sound.soundName] = m2Sound;
5259
+ });
5260
+ return this.fetchSounds();
5261
+ }
5262
+ async fetchSounds() {
5263
+ const fetchSoundsPromises = Object.values(this.sounds).map((m2Sound) => {
5264
+ if (m2Sound.status === M2SoundStatus.WillFetch) {
5265
+ m2Sound.status = M2SoundStatus.Fetching;
5266
+ return fetch(m2Sound.url).then((response) => {
5267
+ if (!response.ok) {
5268
+ m2Sound.status = M2SoundStatus.Error;
5269
+ throw new Error(
5270
+ `cannot fetch sound ${m2Sound.soundName} at url ${m2Sound.url}: ${response.statusText}`
5271
+ );
5272
+ }
5273
+ return response.arrayBuffer().then((arrayBuffer) => {
5274
+ m2Sound.data = arrayBuffer;
5275
+ m2Sound.status = M2SoundStatus.Fetched;
5276
+ console.log(
5277
+ `\u26AA sound fetched. name: ${m2Sound.soundName}, bytes: ${arrayBuffer.byteLength}`
5278
+ );
5279
+ });
5280
+ });
5281
+ }
5282
+ return Promise.resolve();
5283
+ });
5284
+ await Promise.all(fetchSoundsPromises);
5285
+ }
5286
+ /**
5287
+ * Fetches a m2c2kit sound ({@link M2Sound}) that was previously
5288
+ * initialized with lazy loading.
5289
+ *
5290
+ * @internal For m2c2kit library use only
5291
+ *
5292
+ * @param m2Sound - M2Sound to fetch
5293
+ * @returns A promise that completes when sounds have been fetched
5294
+ */
5295
+ fetchDeferredSound(m2Sound) {
5296
+ m2Sound.status = M2SoundStatus.WillFetch;
5297
+ return this.fetchSounds();
5298
+ }
5299
+ /**
5300
+ * Checks if the SoundManager has sounds needing decoding.
5301
+ *
5302
+ * @internal For m2c2kit library use only
5303
+ *
5304
+ * @returns true if there are sounds that have been fetched and are waiting
5305
+ * to be decoded (status is `M2SoundStatus.Fetched`)
5306
+ */
5307
+ hasSoundsToDecode() {
5308
+ return Object.values(this.sounds).filter(
5309
+ (sound) => sound.status === M2SoundStatus.Fetched
5310
+ ).length > 0;
5311
+ }
5312
+ /**
5313
+ * Decodes all fetched sounds from bytes to an `AudioBuffer`.
5314
+ *
5315
+ * @internal For m2c2kit library use only
5316
+ *
5317
+ * @remarks This method will be called after the `AudioContext` has been
5318
+ * created and if there are fetched sounds waiting to be decoded.
5319
+ *
5320
+ * @returns A promise that completes when all fetched sounds have been decoded
5321
+ */
5322
+ decodeFetchedSounds() {
5323
+ const sounds = Object.values(this.sounds);
5324
+ const decodeSoundsPromises = sounds.filter((sound) => sound.status === M2SoundStatus.Fetched).map((sound) => this.decodeSound(sound));
5325
+ return Promise.all(decodeSoundsPromises);
5326
+ }
5327
+ /**
5328
+ * Decodes a sound from bytes to an `AudioBuffer`.
5329
+ *
5330
+ * @param sound - sound to decode
5331
+ */
5332
+ async decodeSound(sound) {
5333
+ if (!sound.data) {
5334
+ throw new Error(
5335
+ `data is undefined for sound ${sound.soundName} (url ${sound.url})`
5336
+ );
5337
+ }
5338
+ try {
5339
+ sound.status = M2SoundStatus.Decoding;
5340
+ const buffer = await this.audioContext.decodeAudioData(sound.data);
5341
+ sound.audioBuffer = buffer;
5342
+ sound.status = M2SoundStatus.Ready;
5343
+ console.log(
5344
+ `\u26AA sound decoded. name: ${sound.soundName}, duration (seconds): ${buffer.duration}`
5345
+ );
5346
+ } catch {
5347
+ sound.status = M2SoundStatus.Error;
5348
+ throw new Error(
5349
+ `error decoding sound ${sound.soundName} (url: ${sound.url})`
5350
+ );
5351
+ }
5352
+ }
5353
+ /**
5354
+ * Returns a m2c2kit sound ({@link M2Sound}) that has been entered into the
5355
+ * SoundManager.
5356
+ *
5357
+ * @internal For m2c2kit library use only
5358
+ *
5359
+ * @remarks Typically, a user won't need to call this because sound
5360
+ * initialization and processing is handled by the framework.
5361
+ *
5362
+ * @param soundName - sound's name as defined in the game's sound assets
5363
+ * @returns a m2c2kit sound
5364
+ */
5365
+ getSound(soundName) {
5366
+ const sound = this.sounds[soundName];
5367
+ if (!sound) {
5368
+ throw new Error(`getSound(): sound ${soundName} not found`);
5369
+ }
5370
+ return sound;
5371
+ }
5372
+ /**
5373
+ * Frees up resources allocated by the SoundManager.
5374
+ *
5375
+ * @internal For m2c2kit library use only
5376
+ *
5377
+ * @remarks This will be done automatically by the m2c2kit library; the
5378
+ * end-user must not call this.
5379
+ */
5380
+ dispose() {
5381
+ }
5382
+ /**
5383
+ * Gets names of sounds entered in the `SoundManager`.
5384
+ *
5385
+ * @remarks These are sounds that the `SoundManager` is aware of. The sounds
5386
+ * may not be ready to play (may not have been fetched or decoded yet).
5387
+ *
5388
+ * @returns array of sound names
5389
+ */
5390
+ getSoundNames() {
5391
+ return Object.keys(this.sounds);
5392
+ }
5393
+ }
5394
+
4251
5395
  class Game {
4252
5396
  /**
4253
5397
  * The base class for all games. New games should extend this class.
@@ -4258,6 +5402,7 @@ class Game {
4258
5402
  this.type = ActivityType.Game;
4259
5403
  this.sessionUuid = "";
4260
5404
  this.uuid = Uuid.generate();
5405
+ this.publishUuid = "";
4261
5406
  this.canvasKitWasmVersion = "0.39.1";
4262
5407
  this.beginTimestamp = NaN;
4263
5408
  this.beginIso8601Timestamp = "";
@@ -4295,6 +5440,15 @@ class Game {
4295
5440
  * values in the trial data.
4296
5441
  */
4297
5442
  this.automaticTrialSchema = {
5443
+ study_id: {
5444
+ type: ["string", "null"],
5445
+ description: "The short human-readable text ID of the study (protocol, experiment, or other aggregate) that contains the administration of this activity."
5446
+ },
5447
+ study_uuid: {
5448
+ type: ["string", "null"],
5449
+ format: "uuid",
5450
+ description: "Unique identifier of the study (protocol, experiment, or other aggregate) that contains the administration of this activity."
5451
+ },
4298
5452
  document_uuid: {
4299
5453
  type: "string",
4300
5454
  format: "uuid",
@@ -4303,17 +5457,22 @@ class Game {
4303
5457
  session_uuid: {
4304
5458
  type: "string",
4305
5459
  format: "uuid",
4306
- description: "Unique identifier for all activities in this administration of the session."
5460
+ description: "Unique identifier for all activities in this administration of the session. This identifier changes each time a new session starts."
4307
5461
  },
4308
5462
  activity_uuid: {
4309
5463
  type: "string",
4310
5464
  format: "uuid",
4311
- description: "Unique identifier for all trials in this administration of the activity."
5465
+ description: "Unique identifier for all trials in this administration of the activity. This identifier changes each time the activity starts."
4312
5466
  },
4313
5467
  activity_id: {
4314
5468
  type: "string",
4315
5469
  description: "Human-readable identifier of the activity."
4316
5470
  },
5471
+ activity_publish_uuid: {
5472
+ type: "string",
5473
+ format: "uuid",
5474
+ description: "Persistent unique identifier of the activity. This identifier never changes. It can be used to identify the activity across different studies and sessions."
5475
+ },
4317
5476
  activity_version: {
4318
5477
  type: "string",
4319
5478
  description: "Version of the activity."
@@ -4325,20 +5484,48 @@ class Game {
4325
5484
  device_timezone_offset_minutes: {
4326
5485
  type: "integer",
4327
5486
  description: "Difference in minutes between UTC and device timezone. Calculated from Date.getTimezoneOffset()."
5487
+ },
5488
+ locale: {
5489
+ type: ["string", "null"],
5490
+ description: "Locale of the trial. null if the activity does not support localization."
4328
5491
  }
4329
5492
  };
4330
5493
  this.snapshots = new Array();
4331
5494
  if (!options.id || options.id.trim() === "") {
4332
5495
  throw new Error("id is required in GameOptions");
4333
5496
  }
5497
+ if (!Uuid.isValid(options.publishUuid)) {
5498
+ const providedPublishUuid = options.publishUuid ? `Provided publishUuid was ${options.publishUuid}. ` : "";
5499
+ console.warn(
5500
+ `Missing or invalid publishUuid in GameOptions. ${providedPublishUuid}To generate a valid UUID, visit a site such as https://www.uuidgenerator.net/version4`
5501
+ );
5502
+ }
4334
5503
  this.options = options;
4335
5504
  this.name = options.name;
4336
5505
  this.id = options.id;
5506
+ this.publishUuid = options.publishUuid;
4337
5507
  this.freeNodesScene.game = this;
4338
5508
  this.freeNodesScene.needsInitialization = true;
4339
5509
  this.fpsMetricReportThreshold = options.fpsMetricReportThreshold ?? Constants.FPS_METRIC_REPORT_THRESHOLD;
4340
5510
  this.maximumRecordedActivityMetrics = options.maximumRecordedActivityMetrics ?? Constants.MAXIMUM_RECORDED_ACTIVITY_METRICS;
4341
5511
  this.addLocalizationParametersToGameParameters();
5512
+ if (this.options.locale !== void 0) {
5513
+ this.setParameters({ locale: this.options.locale });
5514
+ }
5515
+ if (this.options.fallbackLocale !== void 0) {
5516
+ this.setParameters({ fallback_locale: this.options.fallbackLocale });
5517
+ }
5518
+ if (this.options.missingLocalizationColor) {
5519
+ this.setParameters({
5520
+ missing_localization_color: this.options.missingLocalizationColor
5521
+ });
5522
+ }
5523
+ if (this.options.translation) {
5524
+ this.setParameters({ translation: this.options.translation });
5525
+ }
5526
+ if (this.options.additionalTranslation) {
5527
+ this.setParameters({ translation: this.options.additionalTranslation });
5528
+ }
4342
5529
  if (!this.options.trialSchema) {
4343
5530
  this.options.trialSchema = {};
4344
5531
  }
@@ -4445,14 +5632,17 @@ class Game {
4445
5632
  }
4446
5633
  }
4447
5634
  if (this.isLocalizationRequested()) {
4448
- const options = this.getLocalizationOptionsFromGameParameters();
4449
- this.i18n = new I18n(options);
5635
+ const localizationOptions = this.getLocalizationOptionsFromGameParameters();
5636
+ this.i18n = new I18n(this, localizationOptions);
5637
+ await this.i18n.initialize();
4450
5638
  }
4451
5639
  this.fontManager = new FontManager(this, baseUrls);
4452
5640
  this.imageManager = new ImageManager(this, baseUrls);
5641
+ this.soundManager = new SoundManager(this, baseUrls);
4453
5642
  return Promise.all([
4454
5643
  this.fontManager.initializeFonts(this.options.fonts),
4455
- this.imageManager.initializeImages(this.options.images)
5644
+ this.imageManager.initializeImages(this.options.images),
5645
+ this.soundManager.initializeSounds(this.options.sounds)
4456
5646
  ]);
4457
5647
  }
4458
5648
  /**
@@ -4508,6 +5698,39 @@ class Game {
4508
5698
  set imageManager(imageManager) {
4509
5699
  this._imageManager = imageManager;
4510
5700
  }
5701
+ get soundManager() {
5702
+ if (!this._soundManager) {
5703
+ throw new Error("soundManager is undefined");
5704
+ }
5705
+ return this._soundManager;
5706
+ }
5707
+ set soundManager(soundManager) {
5708
+ this._soundManager = soundManager;
5709
+ }
5710
+ /**
5711
+ * Adds prefixes to a key to ensure that keys are unique across activities
5712
+ * and studies.
5713
+ *
5714
+ * @remarks When a value is saved to the key-value data store, the key must
5715
+ * be prefixed with additional information to ensure that keys are unique.
5716
+ * The prefixes will include the activity id and publish UUID, and possibly
5717
+ * the study id and study UUID, if they are set (this is so that keys are
5718
+ * unique across different studies that might use the same activity).
5719
+ *
5720
+ * @param key - item key to add prefixes to
5721
+ * @returns the item key with prefixes added
5722
+ */
5723
+ addPrefixesToKey(key) {
5724
+ let k = "";
5725
+ if (this.studyId && this.studyUuid) {
5726
+ k = this.studyId.concat(":", this.studyUuid, ":");
5727
+ } else if (this.studyId || this.studyUuid) {
5728
+ throw new Error(
5729
+ `study_id and study_uuid must both be set or unset. Values are study_id: ${this.studyId}, study_uuid: ${this.studyUuid}`
5730
+ );
5731
+ }
5732
+ return k.concat(this.id.concat(this.id, ":", this.publishUuid, ":", key));
5733
+ }
4511
5734
  /**
4512
5735
  * Saves an item to the activity's key-value store.
4513
5736
  *
@@ -4527,9 +5750,12 @@ class Game {
4527
5750
  * @returns key
4528
5751
  */
4529
5752
  storeSetItem(key, value, globalStore = false) {
4530
- const k = globalStore ? key : this.id.concat(":", key);
4531
- const activityId = globalStore ? "" : this.id;
4532
- return this.dataStores[0].setItem(k, value, activityId);
5753
+ const prefixedKey = globalStore ? key : this.addPrefixesToKey(key);
5754
+ return this.dataStores[0].setItem(
5755
+ prefixedKey,
5756
+ value,
5757
+ globalStore ? "" : this.publishUuid
5758
+ );
4533
5759
  }
4534
5760
  /**
4535
5761
  * Gets an item value from the activity's key-value store.
@@ -4549,8 +5775,8 @@ class Game {
4549
5775
  * @returns value of the item
4550
5776
  */
4551
5777
  storeGetItem(key, globalStore = false) {
4552
- const k = globalStore ? key : this.id.concat(":", key);
4553
- return this.dataStores[0].getItem(k);
5778
+ const prefixedKey = globalStore ? key : this.addPrefixesToKey(key);
5779
+ return this.dataStores[0].getItem(prefixedKey);
4554
5780
  }
4555
5781
  /**
4556
5782
  * Deletes an item value from the activity's key-value store.
@@ -4569,8 +5795,8 @@ class Game {
4569
5795
  * by any activity. Default is false.
4570
5796
  */
4571
5797
  storeDeleteItem(key, globalStore = false) {
4572
- const k = globalStore ? key : this.id.concat(":", key);
4573
- return this.dataStores[0].deleteItem(k);
5798
+ const prefixedKey = globalStore ? key : this.addPrefixesToKey(key);
5799
+ return this.dataStores[0].deleteItem(prefixedKey);
4574
5800
  }
4575
5801
  /**
4576
5802
  * Deletes all items from the activity's key-value store.
@@ -4585,7 +5811,7 @@ class Game {
4585
5811
  * });
4586
5812
  */
4587
5813
  storeClearItems() {
4588
- return this.dataStores[0].clearItemsByActivityId(this.id);
5814
+ return this.dataStores[0].clearItemsByActivityPublishUuid(this.publishUuid);
4589
5815
  }
4590
5816
  /**
4591
5817
  * Returns keys of all items in the activity's key-value store.
@@ -4603,7 +5829,9 @@ class Game {
4603
5829
  * by any activity. Default is false.
4604
5830
  */
4605
5831
  storeItemsKeys(globalStore = false) {
4606
- return this.dataStores[0].itemsKeysByActivityId(globalStore ? "" : this.id);
5832
+ return this.dataStores[0].itemsKeysByActivityPublishUuid(
5833
+ globalStore ? "" : this.publishUuid
5834
+ );
4607
5835
  }
4608
5836
  /**
4609
5837
  * Determines if a key exists in the activity's key-value store.
@@ -4623,8 +5851,8 @@ class Game {
4623
5851
  * @returns true if the key exists, false otherwise
4624
5852
  */
4625
5853
  storeItemExists(key, globalStore = false) {
4626
- const k = globalStore ? key : this.id.concat(":", key);
4627
- return this.dataStores[0].itemExists(k);
5854
+ const prefixedKey = globalStore ? key : this.addPrefixesToKey(key);
5855
+ return this.dataStores[0].itemExists(prefixedKey);
4628
5856
  }
4629
5857
  get dataStores() {
4630
5858
  if (!this._dataStores) {
@@ -4635,21 +5863,24 @@ class Game {
4635
5863
  set dataStores(dataStores) {
4636
5864
  this._dataStores = dataStores;
4637
5865
  }
5866
+ hasDataStores() {
5867
+ return this._dataStores && this._dataStores.length > 0 || false;
5868
+ }
4638
5869
  getLocalizationOptionsFromGameParameters() {
4639
5870
  const locale = this.getParameter("locale");
4640
5871
  const fallbackLocale = this.getParameterOrFallback(
4641
5872
  "fallback_locale",
4642
5873
  void 0
4643
5874
  );
4644
- const missingTranslationColor = this.getParameterOrFallback("missing_translation_font_color", void 0);
4645
- const additionalTranslations = this.getParameterOrFallback("translations", void 0);
4646
- const translations = this.options.translations;
5875
+ const missingTranslationColor = this.getParameterOrFallback("missing_localization_color", void 0);
5876
+ const additionalTranslation = this.getParameterOrFallback("translation", void 0);
5877
+ const translation = this.options.translation;
4647
5878
  return {
4648
5879
  locale,
4649
5880
  fallbackLocale,
4650
- missingTranslationFontColor: missingTranslationColor,
4651
- additionalTranslations,
4652
- translations
5881
+ missingLocalizationColor: missingTranslationColor,
5882
+ additionalTranslation,
5883
+ translation
4653
5884
  };
4654
5885
  }
4655
5886
  isLocalizationRequested() {
@@ -4662,7 +5893,14 @@ class Game {
4662
5893
  "Empty string in locale. Leave locale undefined or null to prevent localization."
4663
5894
  );
4664
5895
  }
4665
- return locale !== void 0 && locale !== null;
5896
+ if ((locale === null || locale === void 0) && this.options.translation) {
5897
+ this.setParameters({ locale: this.options.translation.baseLocale });
5898
+ return true;
5899
+ }
5900
+ if ((locale === null || locale === void 0) && this.options.translation === void 0) {
5901
+ return false;
5902
+ }
5903
+ return true;
4666
5904
  }
4667
5905
  setParameters(additionalParameters) {
4668
5906
  const { parameters } = this.options;
@@ -5310,12 +6548,16 @@ class Game {
5310
6548
  }
5311
6549
  this.data.trials.push({
5312
6550
  document_uuid: Uuid.generate(),
6551
+ study_id: this.studyId ?? null,
6552
+ study_uuid: this.studyUuid ?? null,
5313
6553
  session_uuid: this.sessionUuid,
5314
6554
  activity_uuid: this.uuid,
5315
6555
  activity_id: this.options.id,
6556
+ activity_publish_uuid: this.options.publishUuid,
5316
6557
  activity_version: this.options.version,
5317
6558
  device_timezone: Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone ?? "",
5318
6559
  device_timezone_offset_minutes: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
6560
+ locale: this.i18n?.locale ?? null,
5319
6561
  ...emptyTrial,
5320
6562
  device_metadata: this.getDeviceMetadata()
5321
6563
  });
@@ -5411,6 +6653,12 @@ class Game {
5411
6653
  * the appropriate time. It is not triggered automatically.
5412
6654
  */
5413
6655
  trialComplete() {
6656
+ if (this.data.trials[this.trialIndex]?.["locale"]) {
6657
+ this.data.trials[this.trialIndex]["locale"] = this.i18n?.locale ?? null;
6658
+ }
6659
+ if (this.data.trials[this.trialIndex]?.["device_metadata"]) {
6660
+ this.data.trials[this.trialIndex]["device_metadata"] = this.getDeviceMetadata();
6661
+ }
5414
6662
  if (Object.keys(this.staticTrialSchema).length > 0) {
5415
6663
  this.data.trials[this.trialIndex] = {
5416
6664
  ...this.data.trials[this.trialIndex],
@@ -5493,9 +6741,9 @@ class Game {
5493
6741
  // eslint-disable-line @typescript-eslint/no-unused-vars
5494
6742
  fallback_locale,
5495
6743
  // eslint-disable-line @typescript-eslint/no-unused-vars
5496
- missing_translation_font_color,
6744
+ missing_localization_color,
5497
6745
  // eslint-disable-line @typescript-eslint/no-unused-vars
5498
- translations,
6746
+ translation,
5499
6747
  // eslint-disable-line @typescript-eslint/no-unused-vars
5500
6748
  ...result
5501
6749
  } = gameParams;
@@ -5515,9 +6763,9 @@ class Game {
5515
6763
  // eslint-disable-line @typescript-eslint/no-unused-vars
5516
6764
  fallback_locale,
5517
6765
  // eslint-disable-line @typescript-eslint/no-unused-vars
5518
- missing_translation_font_color,
6766
+ missing_localization_color,
5519
6767
  // eslint-disable-line @typescript-eslint/no-unused-vars
5520
- translations,
6768
+ translation,
5521
6769
  // eslint-disable-line @typescript-eslint/no-unused-vars
5522
6770
  ...result
5523
6771
  } = gameParams;
@@ -5766,6 +7014,9 @@ class Game {
5766
7014
  this.surface.requestAnimationFrame(this.loop.bind(this));
5767
7015
  return;
5768
7016
  }
7017
+ if (this.soundManager.hasSoundsToDecode() && navigator.userActivation.hasBeenActive) {
7018
+ this.soundManager.decodeFetchedSounds();
7019
+ }
5769
7020
  if (this.gameStopRequested) {
5770
7021
  this.surface.deleteLater();
5771
7022
  return;
@@ -5856,7 +7107,9 @@ class Game {
5856
7107
  canvaskitImage: outgoingSceneImage,
5857
7108
  width: this.canvasCssWidth,
5858
7109
  height: this.canvasCssHeight,
5859
- status: M2ImageStatus.Ready
7110
+ status: M2ImageStatus.Ready,
7111
+ localize: false,
7112
+ isFallback: false
5860
7113
  };
5861
7114
  this.imageManager.addImage(image);
5862
7115
  const spr = new Sprite({
@@ -5895,11 +7148,43 @@ class Game {
5895
7148
  await plugin.initialize(this);
5896
7149
  }
5897
7150
  }
7151
+ /**
7152
+ * Updates active scenes and executes plugins.
7153
+ *
7154
+ */
5898
7155
  update() {
5899
- this.plugins.filter((p) => p.beforeUpdate !== void 0 && p.disabled !== true).forEach((p) => p.beforeUpdate(this, Globals.deltaTime));
7156
+ this.executeBeforeUpdatePlugins();
7157
+ this.updateScenes();
7158
+ this.executeAfterUpdatePlugins();
7159
+ }
7160
+ /**
7161
+ * Updates all active scenes and their children.
7162
+ */
7163
+ updateScenes() {
5900
7164
  this.scenes.filter((scene) => scene._active).forEach((scene) => scene.update());
5901
7165
  this.freeNodesScene.update();
5902
- this.plugins.filter((p) => p.afterUpdate !== void 0 && p.disabled !== true).forEach((p) => p.afterUpdate(this, Globals.deltaTime));
7166
+ }
7167
+ /**
7168
+ * Executes all active plugins before scenes are updated.
7169
+ */
7170
+ executeBeforeUpdatePlugins() {
7171
+ this.plugins.filter(
7172
+ (p) => typeof p.beforeUpdate === "function" && p.disabled !== true
7173
+ ).forEach((p) => {
7174
+ if (p.beforeUpdate) {
7175
+ p.beforeUpdate(this, Globals.deltaTime);
7176
+ }
7177
+ });
7178
+ }
7179
+ /**
7180
+ * Executes all active plugins after scenes have been updated.
7181
+ */
7182
+ executeAfterUpdatePlugins() {
7183
+ this.plugins.filter((p) => typeof p.afterUpdate === "function" && p.disabled !== true).forEach((p) => {
7184
+ if (p.afterUpdate) {
7185
+ p.afterUpdate(this, Globals.deltaTime);
7186
+ }
7187
+ });
5903
7188
  }
5904
7189
  draw(canvas) {
5905
7190
  this.scenes.filter((scene) => scene._active).forEach((scene) => scene.draw(canvas));
@@ -6821,9 +8106,12 @@ class Label extends M2Node {
6821
8106
  // public getter/setter is below
6822
8107
  this._fontSize = Constants.DEFAULT_FONT_SIZE;
6823
8108
  // public getter/setter is below
8109
+ this._interpolation = {};
6824
8110
  // Label options
6825
8111
  this._horizontalAlignmentMode = LabelHorizontalAlignmentMode.Center;
6826
- this._translatedText = "";
8112
+ // public getter/setter is below
8113
+ this._localize = true;
8114
+ this.localizedFontNames = [];
6827
8115
  handleInterfaceOptions(this, options);
6828
8116
  if (options.horizontalAlignmentMode) {
6829
8117
  this.horizontalAlignmentMode = options.horizontalAlignmentMode;
@@ -6839,20 +8127,6 @@ class Label extends M2Node {
6839
8127
  }
6840
8128
  }
6841
8129
  initialize() {
6842
- const fontManager = this.game.fontManager;
6843
- if (this.fontName && this.fontNames) {
6844
- throw new Error("cannot specify both fontName and fontNames");
6845
- }
6846
- const requiredFonts = this.getRequiredLabelFonts(fontManager);
6847
- requiredFonts.forEach((font) => {
6848
- if (font.status === M2FontStatus.Deferred) {
6849
- fontManager.prepareDeferredFont(font);
6850
- return;
6851
- }
6852
- });
6853
- if (!requiredFonts.every((font) => font.status === M2FontStatus.Ready)) {
6854
- return;
6855
- }
6856
8130
  let ckTextAlign = this.canvasKit.TextAlign.Center;
6857
8131
  switch (this.horizontalAlignmentMode) {
6858
8132
  case LabelHorizontalAlignmentMode.Center:
@@ -6877,33 +8151,39 @@ class Label extends M2Node {
6877
8151
  this.fontColor[3]
6878
8152
  );
6879
8153
  let textForParagraph;
6880
- const i18n = this.parentSceneAsNode.game.i18n;
6881
- if (i18n) {
6882
- let translated = i18n.t(this.text);
6883
- if (translated === void 0) {
6884
- const fallbackTranslated = i18n.t(this.text, true);
6885
- if (fallbackTranslated === void 0) {
6886
- translated = this.text;
6887
- } else {
6888
- translated = fallbackTranslated;
6889
- }
6890
- if (i18n.options.missingTranslationFontColor) {
6891
- textColor = this.canvasKit.Color(
6892
- i18n.options.missingTranslationFontColor[0],
6893
- i18n.options.missingTranslationFontColor[1],
6894
- i18n.options.missingTranslationFontColor[2],
6895
- i18n.options.missingTranslationFontColor[3]
6896
- );
6897
- }
6898
- }
6899
- this._translatedText = translated;
6900
- textForParagraph = this._translatedText;
6901
- if (this._translatedText === "") {
6902
- console.warn(`warning: empty translated text in label "${this.name}"`);
8154
+ const i18n = this.game.i18n;
8155
+ if (i18n && this.localize !== false) {
8156
+ const textLocalization = i18n.getTextLocalization(
8157
+ this.text,
8158
+ this.interpolation
8159
+ );
8160
+ textForParagraph = textLocalization.text;
8161
+ this.localizedFontName = textLocalization.fontName;
8162
+ this.localizedFontNames = textLocalization.fontNames ?? [];
8163
+ if (textLocalization.isFallbackOrMissingTranslation && i18n.missingLocalizationColor) {
8164
+ textColor = this.canvasKit.Color(
8165
+ i18n.missingLocalizationColor[0],
8166
+ i18n.missingLocalizationColor[1],
8167
+ i18n.missingLocalizationColor[2],
8168
+ i18n.missingLocalizationColor[3]
8169
+ );
6903
8170
  }
6904
8171
  } else {
6905
8172
  textForParagraph = this.text;
6906
- this._translatedText = "";
8173
+ }
8174
+ if (this.fontName && this.fontNames) {
8175
+ throw new Error("cannot specify both fontName and fontNames");
8176
+ }
8177
+ const fontManager = this.game.fontManager;
8178
+ const requiredFonts = this.getRequiredLabelFonts(fontManager);
8179
+ requiredFonts.forEach((font) => {
8180
+ if (font.status === M2FontStatus.Deferred) {
8181
+ fontManager.prepareDeferredFont(font);
8182
+ return;
8183
+ }
8184
+ });
8185
+ if (!requiredFonts.every((font) => font.status === M2FontStatus.Ready)) {
8186
+ return;
6907
8187
  }
6908
8188
  this.paraStyle = new this.canvasKit.ParagraphStyle({
6909
8189
  textStyle: {},
@@ -6999,6 +8279,17 @@ class Label extends M2Node {
6999
8279
  */
7000
8280
  getRequiredLabelFonts(fontManager) {
7001
8281
  let requiredFonts;
8282
+ if (this.game.i18n && this.localize !== false) {
8283
+ if (this.localizedFontName) {
8284
+ requiredFonts = [fontManager.fonts[this.localizedFontName]];
8285
+ return requiredFonts;
8286
+ } else if (this.localizedFontNames.length > 0) {
8287
+ requiredFonts = this.localizedFontNames.map(
8288
+ (font) => fontManager.fonts[font]
8289
+ );
8290
+ return requiredFonts;
8291
+ }
8292
+ }
7002
8293
  if (this.fontName === void 0 && this.fontNames === void 0) {
7003
8294
  requiredFonts = [fontManager.getDefaultFont()];
7004
8295
  } else if (this.fontName !== void 0) {
@@ -7027,8 +8318,13 @@ class Label extends M2Node {
7027
8318
  this._text = text;
7028
8319
  this.needsInitialization = true;
7029
8320
  }
7030
- get translatedText() {
7031
- return this._translatedText;
8321
+ get interpolation() {
8322
+ return this._interpolation;
8323
+ }
8324
+ set interpolation(interpolation) {
8325
+ this._interpolation = interpolation;
8326
+ Object.freeze(this._interpolation);
8327
+ this.needsInitialization = true;
7032
8328
  }
7033
8329
  get fontName() {
7034
8330
  return this._fontName;
@@ -7079,6 +8375,13 @@ class Label extends M2Node {
7079
8375
  this._backgroundColor = backgroundColor;
7080
8376
  this.needsInitialization = true;
7081
8377
  }
8378
+ get localize() {
8379
+ return this._localize;
8380
+ }
8381
+ set localize(localize) {
8382
+ this._localize = localize;
8383
+ this.needsInitialization = true;
8384
+ }
7082
8385
  get backgroundPaint() {
7083
8386
  if (!this._backgroundPaint) {
7084
8387
  throw new Error("backgroundPaint cannot be undefined");
@@ -7149,6 +8452,15 @@ class Label extends M2Node {
7149
8452
  super.drawChildren(canvas);
7150
8453
  }
7151
8454
  warmup(canvas) {
8455
+ const i18n = this.game.i18n;
8456
+ if (i18n && this.localize !== false) {
8457
+ const textLocalization = i18n.getTextLocalization(
8458
+ this.text,
8459
+ this.interpolation
8460
+ );
8461
+ this.localizedFontName = textLocalization.fontName;
8462
+ this.localizedFontNames = textLocalization.fontNames ?? [];
8463
+ }
7152
8464
  const requiredFonts = this.getRequiredLabelFonts(this.game.fontManager);
7153
8465
  if (requiredFonts.some((font) => font.status === M2FontStatus.Deferred)) {
7154
8466
  return;
@@ -7918,100 +9230,423 @@ class Shape extends M2Node {
7918
9230
  }
7919
9231
  });
7920
9232
  }
7921
- warmupFilledCircle(canvas) {
7922
- if (!this.circleOfRadius) {
7923
- return;
9233
+ warmupFilledCircle(canvas) {
9234
+ if (!this.circleOfRadius) {
9235
+ return;
9236
+ }
9237
+ this.drawCircleWithCanvasKit(canvas, this.fillColorPaintAntialiased);
9238
+ this.drawCircleWithCanvasKit(canvas, this.fillColorPaintNotAntialiased);
9239
+ }
9240
+ warmupStrokedCircle(canvas) {
9241
+ if (!this.lineWidth || !this.circleOfRadius) {
9242
+ return;
9243
+ }
9244
+ const drawScale = Globals.canvasScale / this.absoluteScale;
9245
+ this.strokeColorPaintAntialiased.setStrokeWidth(this.lineWidth * drawScale);
9246
+ this.drawCircleWithCanvasKit(canvas, this.strokeColorPaintAntialiased);
9247
+ this.strokeColorPaintNotAntialiased.setStrokeWidth(
9248
+ this.lineWidth * drawScale
9249
+ );
9250
+ this.drawCircleWithCanvasKit(canvas, this.strokeColorPaintNotAntialiased);
9251
+ }
9252
+ warmupFilledRectangle(canvas) {
9253
+ this.drawRectangleWithCanvasKit(canvas, this.fillColorPaintAntialiased);
9254
+ this.drawRectangleWithCanvasKit(canvas, this.fillColorPaintNotAntialiased);
9255
+ }
9256
+ warmupStrokedRectangle(canvas) {
9257
+ if (!this.lineWidth || !this.circleOfRadius) {
9258
+ return;
9259
+ }
9260
+ const drawScale = Globals.canvasScale / this.absoluteScale;
9261
+ this.strokeColorPaintAntialiased.setStrokeWidth(this.lineWidth * drawScale);
9262
+ this.drawRectangleWithCanvasKit(canvas, this.strokeColorPaintAntialiased);
9263
+ this.strokeColorPaintNotAntialiased.setStrokeWidth(
9264
+ this.lineWidth * drawScale
9265
+ );
9266
+ this.drawRectangleWithCanvasKit(
9267
+ canvas,
9268
+ this.strokeColorPaintNotAntialiased
9269
+ );
9270
+ }
9271
+ get fillColor() {
9272
+ return this._fillColor;
9273
+ }
9274
+ set fillColor(fillColor) {
9275
+ this._fillColor = fillColor;
9276
+ this.needsInitialization = true;
9277
+ }
9278
+ get strokeColor() {
9279
+ return this._strokeColor;
9280
+ }
9281
+ set strokeColor(strokeColor) {
9282
+ this._strokeColor = strokeColor;
9283
+ this.needsInitialization = true;
9284
+ }
9285
+ get isAntialiased() {
9286
+ return this._isAntialiased;
9287
+ }
9288
+ set isAntialiased(isAntialiased) {
9289
+ this._isAntialiased = isAntialiased;
9290
+ this.needsInitialization = true;
9291
+ }
9292
+ get fillColorPaintAntialiased() {
9293
+ if (!this._fillColorPaintAntialiased) {
9294
+ throw new Error("fillColorPaintAntiAliased is undefined");
9295
+ }
9296
+ return this._fillColorPaintAntialiased;
9297
+ }
9298
+ set fillColorPaintAntialiased(value) {
9299
+ this._fillColorPaintAntialiased = value;
9300
+ }
9301
+ get strokeColorPaintAntialiased() {
9302
+ if (!this._strokeColorPaintAntialiased) {
9303
+ throw new Error("strokeColorPaintAntiAliased is undefined");
9304
+ }
9305
+ return this._strokeColorPaintAntialiased;
9306
+ }
9307
+ set strokeColorPaintAntialiased(value) {
9308
+ this._strokeColorPaintAntialiased = value;
9309
+ }
9310
+ get fillColorPaintNotAntialiased() {
9311
+ if (!this._fillColorPaintNotAntialiased) {
9312
+ throw new Error("fillColorPaintNotAntiAliased is undefined");
9313
+ }
9314
+ return this._fillColorPaintNotAntialiased;
9315
+ }
9316
+ set fillColorPaintNotAntialiased(value) {
9317
+ this._fillColorPaintNotAntialiased = value;
9318
+ }
9319
+ get strokeColorPaintNotAntialiased() {
9320
+ if (!this._strokeColorPaintNotAntialiased) {
9321
+ throw new Error("strokeColorPaintNotAntiAliased is undefined");
9322
+ }
9323
+ return this._strokeColorPaintNotAntialiased;
9324
+ }
9325
+ set strokeColorPaintNotAntialiased(value) {
9326
+ this._strokeColorPaintNotAntialiased = value;
9327
+ }
9328
+ }
9329
+
9330
+ class SoundPlayer extends M2Node {
9331
+ /**
9332
+ * Node for playing sounds.
9333
+ *
9334
+ * @param options - {@link SoundPlayerOptions}
9335
+ */
9336
+ constructor(options) {
9337
+ super(options);
9338
+ this.type = M2NodeType.SoundPlayer;
9339
+ this.isDrawable = false;
9340
+ this.soundName = options.soundName;
9341
+ }
9342
+ initialize() {
9343
+ }
9344
+ dispose() {
9345
+ }
9346
+ /**
9347
+ * Duplicates a node using deep copy.
9348
+ *
9349
+ * @remarks This is a deep recursive clone (node and children).
9350
+ * The uuid property of all duplicated nodes will be newly created,
9351
+ * because uuid must be unique.
9352
+ *
9353
+ * @param newName - optional name of the new, duplicated node. If not
9354
+ * provided, name will be the new uuid
9355
+ */
9356
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9357
+ duplicate(newName) {
9358
+ throw new Error("Method not implemented.");
9359
+ }
9360
+ }
9361
+
9362
+ class SoundRecorder extends M2Node {
9363
+ /**
9364
+ * Node for recording sounds.
9365
+ *
9366
+ * @param options - {@link SoundRecorderOptions}
9367
+ */
9368
+ constructor(options) {
9369
+ super(options);
9370
+ this.type = M2NodeType.SoundRecorder;
9371
+ this.isDrawable = false;
9372
+ this._isRecording = false;
9373
+ this._isPaused = false;
9374
+ this.audioChunks = [];
9375
+ this.timerUuid = "";
9376
+ if (options?.mimeType) {
9377
+ const supportedMimeTypes = this.getMediaRecorderSupportedAudioMimeTypes();
9378
+ if (supportedMimeTypes.includes(options.mimeType)) {
9379
+ this.mimeType = options.mimeType;
9380
+ } else {
9381
+ console.warn(
9382
+ `Unsupported MIME type in SoundRecorderOptions: ${options.mimeType}. Supported types are: ${supportedMimeTypes}.`
9383
+ );
9384
+ if (options.backupMimeTypes) {
9385
+ const backupMimeType = this.getSupportedBackupMimeType(
9386
+ options.backupMimeTypes
9387
+ );
9388
+ if (backupMimeType) {
9389
+ this.mimeType = backupMimeType;
9390
+ console.log(`Using backup MIME type: ${backupMimeType}.`);
9391
+ }
9392
+ }
9393
+ }
9394
+ }
9395
+ if (options?.audioTrackConstraints) {
9396
+ this.audioTrackConstraints = options.audioTrackConstraints;
9397
+ }
9398
+ if (options?.maximumDuration) {
9399
+ this.maximumDuration = options.maximumDuration;
9400
+ }
9401
+ }
9402
+ initialize() {
9403
+ }
9404
+ async start() {
9405
+ if (this.isRecording) {
9406
+ throw new Error(
9407
+ "cannot start SoundRecorder because it is already started."
9408
+ );
9409
+ }
9410
+ const supportedMimeTypes = this.getMediaRecorderSupportedAudioMimeTypes();
9411
+ if (supportedMimeTypes.length === 0) {
9412
+ throw new Error(
9413
+ "SoundRecorder found no supported MIME types for MediaRecorder."
9414
+ );
9415
+ }
9416
+ if (!this.mimeType) {
9417
+ this.mimeType = supportedMimeTypes[0];
9418
+ console.log(`Using MIME type: ${this.mimeType}.`);
9419
+ }
9420
+ let stream;
9421
+ try {
9422
+ stream = await navigator.mediaDevices.getUserMedia({
9423
+ audio: this.audioTrackConstraints ? this.audioTrackConstraints : true
9424
+ });
9425
+ } catch (error) {
9426
+ throw new Error("Error getting user media.");
9427
+ }
9428
+ if (!stream) {
9429
+ throw new Error("no stream.");
9430
+ }
9431
+ const audioTracks = stream.getAudioTracks();
9432
+ this.mediaTrackSettings = audioTracks?.map((track) => track.getSettings());
9433
+ this.mediaRecorder = new MediaRecorder(stream, { mimeType: this.mimeType });
9434
+ this.mediaRecorder.ondataavailable = (event) => {
9435
+ this.audioChunks.push(event.data);
9436
+ };
9437
+ this.mediaRecorder.onerror = (event) => {
9438
+ throw new Error(
9439
+ `MediaRecorder error: ${event?.error?.message} ${event?.message}`
9440
+ );
9441
+ };
9442
+ this.mediaRecorder.start();
9443
+ this.beginIso8601Timestamp = (/* @__PURE__ */ new Date()).toISOString();
9444
+ this.timerUuid = Uuid.generate();
9445
+ Timer.startNew(this.timerUuid);
9446
+ this._isRecording = true;
9447
+ this._isPaused = false;
9448
+ }
9449
+ async stop() {
9450
+ if (!this.isRecording) {
9451
+ throw new Error("cannot stop SoundRecorder because it has not started.");
9452
+ }
9453
+ return new Promise((resolve) => {
9454
+ if (!this.mediaRecorder) {
9455
+ throw new Error("no media recorder");
9456
+ }
9457
+ this.mediaRecorder.onstop = async () => {
9458
+ if (!this.mimeType) {
9459
+ throw new Error("no mimeType");
9460
+ }
9461
+ this._isRecording = false;
9462
+ this._isPaused = false;
9463
+ const audioBlob = new Blob(this.audioChunks, {
9464
+ type: this.getMimeTypeWithoutCodecs(this.mimeType)
9465
+ });
9466
+ const audioBase64 = await this.blobToBase64(audioBlob);
9467
+ resolve({
9468
+ mimeType: this.mimeType,
9469
+ beginIso8601Timestamp: this.beginIso8601Timestamp ?? "",
9470
+ endIso8601Timestamp: this.endIso8601Timestamp ?? "",
9471
+ duration: Timer.elapsed(this.timerUuid),
9472
+ audioTrackSettings: this.mediaTrackSettings,
9473
+ audioBase64,
9474
+ audioBlob
9475
+ });
9476
+ };
9477
+ this.mediaRecorder.stop();
9478
+ this.endIso8601Timestamp = (/* @__PURE__ */ new Date()).toISOString();
9479
+ if (!this.isPaused) {
9480
+ Timer.stop(this.timerUuid);
9481
+ }
9482
+ });
9483
+ }
9484
+ pause() {
9485
+ if (!this.isRecording) {
9486
+ throw new Error("cannot pause SoundRecorder because it is not started.");
7924
9487
  }
7925
- this.drawCircleWithCanvasKit(canvas, this.fillColorPaintAntialiased);
7926
- this.drawCircleWithCanvasKit(canvas, this.fillColorPaintNotAntialiased);
7927
- }
7928
- warmupStrokedCircle(canvas) {
7929
- if (!this.lineWidth || !this.circleOfRadius) {
7930
- return;
9488
+ if (this.isPaused) {
9489
+ throw new Error(
9490
+ "cannot pause SoundRecorder because it is already paused."
9491
+ );
7931
9492
  }
7932
- const drawScale = Globals.canvasScale / this.absoluteScale;
7933
- this.strokeColorPaintAntialiased.setStrokeWidth(this.lineWidth * drawScale);
7934
- this.drawCircleWithCanvasKit(canvas, this.strokeColorPaintAntialiased);
7935
- this.strokeColorPaintNotAntialiased.setStrokeWidth(
7936
- this.lineWidth * drawScale
7937
- );
7938
- this.drawCircleWithCanvasKit(canvas, this.strokeColorPaintNotAntialiased);
7939
- }
7940
- warmupFilledRectangle(canvas) {
7941
- this.drawRectangleWithCanvasKit(canvas, this.fillColorPaintAntialiased);
7942
- this.drawRectangleWithCanvasKit(canvas, this.fillColorPaintNotAntialiased);
9493
+ this.mediaRecorder?.pause();
9494
+ this._isPaused = true;
9495
+ Timer.stop(this.timerUuid);
7943
9496
  }
7944
- warmupStrokedRectangle(canvas) {
7945
- if (!this.lineWidth || !this.circleOfRadius) {
7946
- return;
9497
+ resume() {
9498
+ if (!this.isRecording) {
9499
+ throw new Error("cannot resume SoundRecorder because it is not started.");
7947
9500
  }
7948
- const drawScale = Globals.canvasScale / this.absoluteScale;
7949
- this.strokeColorPaintAntialiased.setStrokeWidth(this.lineWidth * drawScale);
7950
- this.drawRectangleWithCanvasKit(canvas, this.strokeColorPaintAntialiased);
7951
- this.strokeColorPaintNotAntialiased.setStrokeWidth(
7952
- this.lineWidth * drawScale
7953
- );
7954
- this.drawRectangleWithCanvasKit(
7955
- canvas,
7956
- this.strokeColorPaintNotAntialiased
7957
- );
7958
- }
7959
- get fillColor() {
7960
- return this._fillColor;
7961
- }
7962
- set fillColor(fillColor) {
7963
- this._fillColor = fillColor;
7964
- this.needsInitialization = true;
7965
- }
7966
- get strokeColor() {
7967
- return this._strokeColor;
7968
- }
7969
- set strokeColor(strokeColor) {
7970
- this._strokeColor = strokeColor;
7971
- this.needsInitialization = true;
9501
+ if (!this.isPaused) {
9502
+ throw new Error("cannot resume SoundRecorder because it is not paused.");
9503
+ }
9504
+ this.mediaRecorder?.resume();
9505
+ this._isPaused = false;
9506
+ Timer.start(this.timerUuid);
7972
9507
  }
7973
- get isAntialiased() {
7974
- return this._isAntialiased;
9508
+ /** Is the `SoundRecorder` currently recording? */
9509
+ get isRecording() {
9510
+ return this._isRecording;
7975
9511
  }
7976
- set isAntialiased(isAntialiased) {
7977
- this._isAntialiased = isAntialiased;
7978
- this.needsInitialization = true;
9512
+ /** Is the `SoundRecorder` currently paused? */
9513
+ get isPaused() {
9514
+ return this._isPaused;
7979
9515
  }
7980
- get fillColorPaintAntialiased() {
7981
- if (!this._fillColorPaintAntialiased) {
7982
- throw new Error("fillColorPaintAntiAliased is undefined");
9516
+ update() {
9517
+ super.update();
9518
+ if (this.isRecording && !this.isPaused && this.maximumDuration !== void 0 && Timer.elapsed(this.timerUuid) > this.maximumDuration) {
9519
+ this.pause();
9520
+ return;
7983
9521
  }
7984
- return this._fillColorPaintAntialiased;
7985
9522
  }
7986
- set fillColorPaintAntialiased(value) {
7987
- this._fillColorPaintAntialiased = value;
9523
+ /**
9524
+ * Returns an array of supported audio MIME types for MediaRecorder.
9525
+ *
9526
+ * @remarks Adapted from https://stackoverflow.com/a/68236494
9527
+ * License: https://creativecommons.org/licenses/by-sa/4.0/
9528
+ *
9529
+ * @returns
9530
+ */
9531
+ getMediaRecorderSupportedAudioMimeTypes() {
9532
+ const mediaTypes = ["audio"];
9533
+ const containers = [
9534
+ "webm",
9535
+ "ogg",
9536
+ "mp3",
9537
+ "mp4",
9538
+ "x-matroska",
9539
+ "3gpp",
9540
+ "3gpp2",
9541
+ "3gp2",
9542
+ "quicktime",
9543
+ "mpeg",
9544
+ "aac",
9545
+ "flac",
9546
+ "x-flac",
9547
+ "wave",
9548
+ "wav",
9549
+ "x-wav",
9550
+ "x-pn-wav",
9551
+ "not-supported"
9552
+ ];
9553
+ const codecs = [
9554
+ "vp9",
9555
+ "vp9.0",
9556
+ "vp8",
9557
+ "vp8.0",
9558
+ "avc1",
9559
+ "av1",
9560
+ "h265",
9561
+ "h.265",
9562
+ "h264",
9563
+ "h.264",
9564
+ "opus",
9565
+ "vorbis",
9566
+ "pcm",
9567
+ "aac",
9568
+ "mpeg",
9569
+ "mp4a",
9570
+ "rtx",
9571
+ "red",
9572
+ "ulpfec",
9573
+ "g722",
9574
+ "pcmu",
9575
+ "pcma",
9576
+ "cn",
9577
+ "telephone-event",
9578
+ "not-supported"
9579
+ ];
9580
+ return [
9581
+ ...new Set(
9582
+ containers.flatMap(
9583
+ (ext) => mediaTypes.flatMap((mediaType) => [`${mediaType}/${ext}`])
9584
+ )
9585
+ ),
9586
+ ...new Set(
9587
+ containers.flatMap(
9588
+ (ext) => codecs.flatMap(
9589
+ (codec) => mediaTypes.flatMap((mediaType) => [
9590
+ // NOTE: 'codecs:' will always be true (false positive)
9591
+ `${mediaType}/${ext};codecs=${codec}`
9592
+ ])
9593
+ )
9594
+ )
9595
+ ),
9596
+ ...new Set(
9597
+ containers.flatMap(
9598
+ (ext) => codecs.flatMap(
9599
+ (codec1) => codecs.flatMap(
9600
+ (codec2) => mediaTypes.flatMap((mediaType) => [
9601
+ `${mediaType}/${ext};codecs="${codec1}, ${codec2}"`
9602
+ ])
9603
+ )
9604
+ )
9605
+ )
9606
+ )
9607
+ ].filter((variation) => MediaRecorder.isTypeSupported(variation));
7988
9608
  }
7989
- get strokeColorPaintAntialiased() {
7990
- if (!this._strokeColorPaintAntialiased) {
7991
- throw new Error("strokeColorPaintAntiAliased is undefined");
7992
- }
7993
- return this._strokeColorPaintAntialiased;
9609
+ blobToBase64(blob) {
9610
+ return new Promise((resolve, reject) => {
9611
+ const reader = new FileReader();
9612
+ reader.onloadend = () => {
9613
+ const base64WithoutPrefix = reader.result?.toString().split(",").pop();
9614
+ if (base64WithoutPrefix === void 0) {
9615
+ throw new Error("base64WithoutPrefix is undefined.");
9616
+ }
9617
+ resolve(base64WithoutPrefix);
9618
+ };
9619
+ reader.onerror = reject;
9620
+ reader.readAsDataURL(blob);
9621
+ });
7994
9622
  }
7995
- set strokeColorPaintAntialiased(value) {
7996
- this._strokeColorPaintAntialiased = value;
9623
+ getMimeTypeWithoutCodecs(mimeType) {
9624
+ const match = mimeType.match(/^[^;]+/);
9625
+ return match ? match[0] : "";
7997
9626
  }
7998
- get fillColorPaintNotAntialiased() {
7999
- if (!this._fillColorPaintNotAntialiased) {
8000
- throw new Error("fillColorPaintNotAntiAliased is undefined");
9627
+ getSupportedBackupMimeType(backupMimeTypes) {
9628
+ const supportedMimeTypes = this.getMediaRecorderSupportedAudioMimeTypes();
9629
+ for (const mimeType of backupMimeTypes) {
9630
+ if (supportedMimeTypes.includes(mimeType)) {
9631
+ return mimeType;
9632
+ }
8001
9633
  }
8002
- return this._fillColorPaintNotAntialiased;
8003
- }
8004
- set fillColorPaintNotAntialiased(value) {
8005
- this._fillColorPaintNotAntialiased = value;
9634
+ return void 0;
8006
9635
  }
8007
- get strokeColorPaintNotAntialiased() {
8008
- if (!this._strokeColorPaintNotAntialiased) {
8009
- throw new Error("strokeColorPaintNotAntiAliased is undefined");
8010
- }
8011
- return this._strokeColorPaintNotAntialiased;
9636
+ dispose() {
8012
9637
  }
8013
- set strokeColorPaintNotAntialiased(value) {
8014
- this._strokeColorPaintNotAntialiased = value;
9638
+ /**
9639
+ * Duplicates a node using deep copy.
9640
+ *
9641
+ * @remarks This is a deep recursive clone (node and children).
9642
+ * The uuid property of all duplicated nodes will be newly created,
9643
+ * because uuid must be unique.
9644
+ *
9645
+ * @param newName - optional name of the new, duplicated node. If not
9646
+ * provided, name will be the new uuid
9647
+ */
9648
+ duplicate(newName) {
9649
+ throw new Error(`Method not implemented. ${newName}`);
8015
9650
  }
8016
9651
  }
8017
9652
 
@@ -8047,12 +9682,121 @@ class TextLine extends M2Node {
8047
9682
  this._fontColor = Constants.DEFAULT_FONT_COLOR;
8048
9683
  // public getter/setter is below
8049
9684
  this._fontSize = Constants.DEFAULT_FONT_SIZE;
9685
+ // public getter/setter is below
9686
+ this._interpolation = {};
9687
+ this._localize = true;
8050
9688
  this.typeface = null;
8051
- this._translatedText = "";
9689
+ this.tryMissingTranslationPaint = false;
9690
+ this.textForDraw = "";
9691
+ this.localizedFontNames = [];
8052
9692
  handleInterfaceOptions(this, options);
8053
9693
  this.size.height = this.fontSize;
8054
9694
  this.size.width = options.width ?? NaN;
8055
9695
  }
9696
+ initialize() {
9697
+ const i18n = this.game.i18n;
9698
+ this.tryMissingTranslationPaint = false;
9699
+ if (i18n && this.localize !== false) {
9700
+ const textLocalization = i18n.getTextLocalization(
9701
+ this.text,
9702
+ this.interpolation
9703
+ );
9704
+ this.textForDraw = textLocalization.text;
9705
+ this.localizedFontName = textLocalization.fontName;
9706
+ this.localizedFontNames = textLocalization.fontNames ?? [];
9707
+ if (textLocalization.isFallbackOrMissingTranslation) {
9708
+ this.tryMissingTranslationPaint = true;
9709
+ }
9710
+ } else {
9711
+ this.textForDraw = this.text;
9712
+ }
9713
+ const fontManager = this.game.fontManager;
9714
+ this.fontForDraw = this.getRequiredTextLineFont(fontManager);
9715
+ if (this.fontForDraw.status === M2FontStatus.Deferred) {
9716
+ fontManager.prepareDeferredFont(this.fontForDraw);
9717
+ return;
9718
+ }
9719
+ if (this.fontForDraw.status === M2FontStatus.Loading) {
9720
+ return;
9721
+ }
9722
+ this.createFontPaint(i18n);
9723
+ this.createFont(fontManager);
9724
+ this.needsInitialization = false;
9725
+ }
9726
+ /**
9727
+ * Determines the M2Font object that needs to be ready in order to draw
9728
+ * the TextLine.
9729
+ *
9730
+ * @remarks It needs a FontManager because it may need to look up the
9731
+ * default font.
9732
+ *
9733
+ * @param fontManager - {@link FontManager}
9734
+ * @returns a M2Font object that is required for the TextLine
9735
+ */
9736
+ getRequiredTextLineFont(fontManager) {
9737
+ if (this.game.i18n) {
9738
+ if (this.localizedFontName !== void 0 && this.localizedFontNames.length !== 0 || this.localizedFontNames.length > 1) {
9739
+ throw new Error(
9740
+ `TextLine supports only one font, but multiple fonts are specified in translation.`
9741
+ );
9742
+ }
9743
+ if (this.localizedFontName !== void 0) {
9744
+ return fontManager.fonts[this.localizedFontName];
9745
+ } else if (this.localizedFontNames.length == 1) {
9746
+ return fontManager.fonts[this.localizedFontNames[0]];
9747
+ }
9748
+ }
9749
+ if (this.fontName === void 0) {
9750
+ return fontManager.getDefaultFont();
9751
+ }
9752
+ return fontManager.getFont(this.fontName);
9753
+ }
9754
+ createFontPaint(i18n) {
9755
+ if (this.paint) {
9756
+ this.paint.delete();
9757
+ }
9758
+ this.paint = new this.canvasKit.Paint();
9759
+ if (this.tryMissingTranslationPaint && this.localize !== false) {
9760
+ if (i18n?.missingLocalizationColor) {
9761
+ this.paint.setColor(
9762
+ this.canvasKit.Color(
9763
+ i18n.missingLocalizationColor[0],
9764
+ i18n.missingLocalizationColor[1],
9765
+ i18n.missingLocalizationColor[2],
9766
+ i18n.missingLocalizationColor[3]
9767
+ )
9768
+ );
9769
+ }
9770
+ } else {
9771
+ this.paint.setColor(
9772
+ this.canvasKit.Color(
9773
+ this.fontColor[0],
9774
+ this.fontColor[1],
9775
+ this.fontColor[2],
9776
+ this.fontColor[3]
9777
+ )
9778
+ );
9779
+ }
9780
+ this.paint.setStyle(this.canvasKit.PaintStyle.Fill);
9781
+ this.paint.setAntiAlias(true);
9782
+ }
9783
+ createFont(fontManager) {
9784
+ if (this.fontForDraw) {
9785
+ this.typeface = fontManager.getTypeface(this.fontForDraw.fontName);
9786
+ } else {
9787
+ const fontNames = fontManager.getFontNames();
9788
+ if (fontNames.length > 0) {
9789
+ this.typeface = fontManager.getTypeface(fontNames[0]);
9790
+ }
9791
+ }
9792
+ if (this.font) {
9793
+ this.font.delete();
9794
+ }
9795
+ this.font = new this.canvasKit.Font(
9796
+ this.typeface,
9797
+ this.fontSize * Globals.canvasScale
9798
+ );
9799
+ }
8056
9800
  get text() {
8057
9801
  return this._text;
8058
9802
  }
@@ -8060,9 +9804,6 @@ class TextLine extends M2Node {
8060
9804
  this._text = text;
8061
9805
  this.needsInitialization = true;
8062
9806
  }
8063
- get translatedText() {
8064
- return this._translatedText;
8065
- }
8066
9807
  get fontName() {
8067
9808
  return this._fontName;
8068
9809
  }
@@ -8084,79 +9825,20 @@ class TextLine extends M2Node {
8084
9825
  this._fontSize = fontSize;
8085
9826
  this.needsInitialization = true;
8086
9827
  }
8087
- update() {
8088
- super.update();
9828
+ get interpolation() {
9829
+ return this._interpolation;
8089
9830
  }
8090
- initialize() {
8091
- const fontManager = this.game.fontManager;
8092
- const requiredFont = this.getRequiredTextLineFont(fontManager);
8093
- if (requiredFont.status === M2FontStatus.Deferred) {
8094
- fontManager.prepareDeferredFont(requiredFont);
8095
- return;
8096
- }
8097
- if (requiredFont.status === M2FontStatus.Loading) {
8098
- return;
8099
- }
8100
- if (this.paint) {
8101
- this.paint.delete();
8102
- }
8103
- this.paint = new this.canvasKit.Paint();
8104
- this.paint.setColor(
8105
- this.canvasKit.Color(
8106
- this.fontColor[0],
8107
- this.fontColor[1],
8108
- this.fontColor[2],
8109
- this.fontColor[3]
8110
- )
8111
- );
8112
- this.paint.setStyle(this.canvasKit.PaintStyle.Fill);
8113
- this.paint.setAntiAlias(true);
8114
- const i18n = this.parentSceneAsNode.game.i18n;
8115
- if (i18n && i18n.options.missingTranslationFontColor) {
8116
- this.missingTranslationPaint = new this.canvasKit.Paint();
8117
- this.missingTranslationPaint.setColor(
8118
- this.canvasKit.Color(
8119
- i18n.options.missingTranslationFontColor[0],
8120
- i18n.options.missingTranslationFontColor[1],
8121
- i18n.options.missingTranslationFontColor[2],
8122
- i18n.options.missingTranslationFontColor[3]
8123
- )
8124
- );
8125
- this.paint.setStyle(this.canvasKit.PaintStyle.Fill);
8126
- this.paint.setAntiAlias(true);
8127
- }
8128
- if (this.fontName) {
8129
- this.typeface = fontManager.getTypeface(this.fontName);
8130
- } else {
8131
- const fontNames = fontManager.getFontNames();
8132
- if (fontNames.length > 0) {
8133
- this.typeface = fontManager.getTypeface(fontNames[0]);
8134
- }
8135
- }
8136
- if (this.font) {
8137
- this.font.delete();
8138
- }
8139
- this.font = new this.canvasKit.Font(
8140
- this.typeface,
8141
- this.fontSize * Globals.canvasScale
8142
- );
8143
- this.needsInitialization = false;
9831
+ set interpolation(interpolation) {
9832
+ this._interpolation = interpolation;
9833
+ Object.freeze(this._interpolation);
9834
+ this.needsInitialization = true;
8144
9835
  }
8145
- /**
8146
- * Determines the M2Font object that needs to be ready in order to draw
8147
- * the TextLine.
8148
- *
8149
- * @remarks It needs a FontManager because it may need to look up the
8150
- * default font.
8151
- *
8152
- * @param fontManager - {@link FontManager}
8153
- * @returns a M2Font object that is required for the TextLine
8154
- */
8155
- getRequiredTextLineFont(fontManager) {
8156
- if (this.fontName === void 0) {
8157
- return fontManager.getDefaultFont();
8158
- }
8159
- return fontManager.getFont(this.fontName);
9836
+ get localize() {
9837
+ return this._localize;
9838
+ }
9839
+ set localize(localize) {
9840
+ this._localize = localize;
9841
+ this.needsInitialization = true;
8160
9842
  }
8161
9843
  dispose() {
8162
9844
  CanvasKitHelpers.Dispose([this.font, this.typeface, this.paint]);
@@ -8188,6 +9870,9 @@ class TextLine extends M2Node {
8188
9870
  }
8189
9871
  return dest;
8190
9872
  }
9873
+ update() {
9874
+ super.update();
9875
+ }
8191
9876
  draw(canvas) {
8192
9877
  if (this.parent && this.text && !this.needsInitialization) {
8193
9878
  canvas.save();
@@ -8196,50 +9881,29 @@ class TextLine extends M2Node {
8196
9881
  M2c2KitHelpers.rotateCanvasForDrawableNode(canvas, this);
8197
9882
  const x = this.absolutePosition.x * drawScale;
8198
9883
  const y = (this.absolutePosition.y + this.size.height * this.anchorPoint.y * this.absoluteScale) * drawScale;
8199
- let textForDraw;
8200
- let paintForDraw = this.paint;
8201
- const i18n = this.parentSceneAsNode.game.i18n;
8202
- if (i18n) {
8203
- let translated = i18n.t(this.text);
8204
- if (translated === void 0) {
8205
- const fallbackTranslated = i18n.t(this.text, true);
8206
- if (fallbackTranslated === void 0) {
8207
- translated = this.text;
8208
- } else {
8209
- translated = fallbackTranslated;
8210
- }
8211
- if (this.missingTranslationPaint) {
8212
- paintForDraw = this.missingTranslationPaint;
8213
- }
8214
- }
8215
- this._translatedText = translated;
8216
- textForDraw = this._translatedText;
8217
- if (this._translatedText === "") {
8218
- console.warn(
8219
- `warning: empty translated text in TextLine "${this.name}"`
8220
- );
8221
- }
8222
- } else {
8223
- textForDraw = this.text;
8224
- this._translatedText = "";
8225
- if (this.text === "") {
8226
- console.warn(`warning: empty text in TextLine "${this.name}"`);
8227
- }
8228
- }
8229
- if (paintForDraw === void 0 || this.font === void 0) {
9884
+ if (this.paint === void 0 || this.font === void 0) {
8230
9885
  throw new Error(
8231
9886
  `in TextLine node ${this}, Paint or Font is undefined.`
8232
9887
  );
8233
9888
  }
8234
9889
  if (this.absoluteAlphaChange !== 0) {
8235
- paintForDraw.setAlphaf(this.absoluteAlpha);
9890
+ this.paint.setAlphaf(this.absoluteAlpha);
8236
9891
  }
8237
- canvas.drawText(textForDraw, x, y, paintForDraw, this.font);
9892
+ canvas.drawText(this.textForDraw, x, y, this.paint, this.font);
8238
9893
  canvas.restore();
8239
9894
  }
8240
9895
  super.drawChildren(canvas);
8241
9896
  }
8242
9897
  warmup(canvas) {
9898
+ const i18n = this.game.i18n;
9899
+ if (i18n && this.localize !== false) {
9900
+ const textLocalization = i18n.getTextLocalization(
9901
+ this.text,
9902
+ this.interpolation
9903
+ );
9904
+ this.localizedFontName = textLocalization.fontName;
9905
+ this.localizedFontNames = textLocalization.fontNames ?? [];
9906
+ }
8243
9907
  const requiredFont = this.getRequiredTextLineFont(this.game.fontManager);
8244
9908
  if (requiredFont.status === M2FontStatus.Deferred) {
8245
9909
  return;
@@ -8254,7 +9918,7 @@ class TextLine extends M2Node {
8254
9918
  }
8255
9919
  }
8256
9920
 
8257
- console.log("\u26AA @m2c2kit/core version 0.3.17 (ebbdc605)");
9921
+ console.log("\u26AA @m2c2kit/core version 0.3.18 (b3a70752)");
8258
9922
 
8259
- export { Action, ActivityType, CanvasKitHelpers, ColorfulMutablePath, Composite, Constants, ConstraintType, CustomAction, Dimensions, Easings, Equals, FadeAlphaAction, FontManager, Game, GlobalVariables, GroupAction, I18n, ImageManager, Label, LabelHorizontalAlignmentMode, LayoutConstraint, LegacyTimer, M2EventType, M2ImageStatus, M2Node, M2NodeType, M2c2KitHelpers, MoveAction, MutablePath, NoneTransition, RandomDraws, RotateAction, ScaleAction, Scene, SceneTransition, SequenceAction, Shape, ShapeType, SlideTransition, Sprite, Story, TextLine, Timer, Transition, TransitionDirection, TransitionType, Uuid, WaitAction, WebColors, WebGlInfo, handleInterfaceOptions };
9923
+ export { Action, ActivityType, CanvasKitHelpers, ColorfulMutablePath, Composite, Constants, ConstraintType, CustomAction, Dimensions, Easings, Equals, FadeAlphaAction, FontManager, Game, GlobalVariables, GroupAction, I18n, ImageManager, Label, LabelHorizontalAlignmentMode, LayoutConstraint, LegacyTimer, M2EventType, M2ImageStatus, M2Node, M2NodeType, M2SoundStatus, M2c2KitHelpers, MoveAction, MutablePath, NoneTransition, PlayAction, RandomDraws, RepeatAction, RepeatForeverAction, RotateAction, ScaleAction, Scene, SceneTransition, SequenceAction, Shape, ShapeType, SlideTransition, SoundManager, SoundPlayer, SoundRecorder, Sprite, Story, TextLine, Timer, Transition, TransitionDirection, TransitionType, Uuid, WaitAction, WebColors, WebGlInfo, handleInterfaceOptions };
8260
9924
  //# sourceMappingURL=index.js.map