@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.d.ts +1139 -138
- package/dist/index.js +2473 -809
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +3 -3
- package/package.json +11 -11
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
|
|
485
|
-
constructor(
|
|
486
|
-
|
|
487
|
-
this.
|
|
488
|
-
this.
|
|
489
|
-
this.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
*
|
|
517
|
+
* Appends a number or another Futurable to this Futurable's expression.
|
|
499
518
|
*
|
|
500
|
-
* @
|
|
501
|
-
*
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
*
|
|
541
|
+
* Assigns a value, either known or Futurable, to this Futurable.
|
|
513
542
|
*
|
|
514
|
-
* @
|
|
515
|
-
*
|
|
543
|
+
* @remarks This method clears the current expression.
|
|
544
|
+
*
|
|
545
|
+
* @param value - value to assign to this Futurable.
|
|
516
546
|
*/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
);
|
|
547
|
+
assign(value) {
|
|
548
|
+
while (this.expression.length > 0) {
|
|
549
|
+
this.expression.pop();
|
|
550
|
+
}
|
|
551
|
+
this.pushToExpression(value);
|
|
522
552
|
}
|
|
523
553
|
/**
|
|
524
|
-
*
|
|
554
|
+
* Performs addition on this Futurable.
|
|
525
555
|
*
|
|
526
|
-
* @
|
|
527
|
-
*
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
options.runDuringTransition ?? false
|
|
533
|
-
);
|
|
562
|
+
add(...terms) {
|
|
563
|
+
this.appendOperation(Operator.Add, ...terms);
|
|
564
|
+
return this;
|
|
534
565
|
}
|
|
535
566
|
/**
|
|
536
|
-
*
|
|
567
|
+
* Performs subtraction on this Futurable.
|
|
537
568
|
*
|
|
538
|
-
* @remarks
|
|
569
|
+
* @remarks This method modifies the Futurable by subtracting the term(s)
|
|
570
|
+
* from the Futurable's expression.
|
|
539
571
|
*
|
|
540
|
-
* @param
|
|
541
|
-
* @returns
|
|
572
|
+
* @param terms - terms to subtract from this Futurable.
|
|
573
|
+
* @returns the modified Futurable.
|
|
542
574
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
options.duration,
|
|
547
|
-
options.runDuringTransition
|
|
548
|
-
);
|
|
575
|
+
subtract(...terms) {
|
|
576
|
+
this.appendOperation(Operator.Subtract, ...terms);
|
|
577
|
+
return this;
|
|
549
578
|
}
|
|
550
579
|
/**
|
|
551
|
-
*
|
|
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
|
|
556
|
-
* @
|
|
583
|
+
* @param operator - Add or Subtract.
|
|
584
|
+
* @param terms - terms to add to the expression.
|
|
557
585
|
*/
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
);
|
|
586
|
+
appendOperation(operator, ...terms) {
|
|
587
|
+
terms.forEach((term) => {
|
|
588
|
+
this.pushToExpression(operator);
|
|
589
|
+
this.pushToExpression(term);
|
|
590
|
+
});
|
|
564
591
|
}
|
|
565
592
|
/**
|
|
566
|
-
*
|
|
593
|
+
* Gets the numeric value of this Futurable.
|
|
567
594
|
*
|
|
568
|
-
* @remarks
|
|
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
|
-
* @
|
|
571
|
-
* @returns The rotate action
|
|
600
|
+
* @returns the numeric value of this Futurable.
|
|
572
601
|
*/
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
*
|
|
649
|
+
* Prepares the Action for evaluation.
|
|
598
650
|
*
|
|
599
|
-
* @remarks
|
|
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
|
|
602
|
-
* @returns
|
|
655
|
+
* @param key - optional string to identify an action
|
|
656
|
+
* @returns action prepared for evaluation
|
|
603
657
|
*/
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
615
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
*
|
|
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
|
|
766
|
+
* @param action - the action to assign start offsets to
|
|
871
767
|
*/
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (action
|
|
875
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
907
|
-
*
|
|
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
|
-
|
|
915
|
-
if (
|
|
916
|
-
|
|
917
|
-
actions.push(action);
|
|
781
|
+
calculateStartOffset(action) {
|
|
782
|
+
if (action.parent === void 0) {
|
|
783
|
+
return new Futurable(0);
|
|
918
784
|
}
|
|
919
|
-
if (action.
|
|
920
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
929
|
-
*
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
2135
|
-
(action) => action
|
|
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(
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
2396
|
-
|
|
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
|
|
4598
|
+
* Returns the translation text for the given key in the current locale.
|
|
3850
4599
|
*
|
|
3851
|
-
* @remarks
|
|
3852
|
-
*
|
|
3853
|
-
*
|
|
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
|
-
* @
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
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
|
|
3870
|
-
if (
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
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
|
-
|
|
3876
|
-
return rendererString;
|
|
4642
|
+
return void 0;
|
|
3877
4643
|
}
|
|
3878
4644
|
/**
|
|
3879
|
-
*
|
|
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
|
-
|
|
3882
|
-
const
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
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
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
this.
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
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
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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
|
-
|
|
4799
|
+
missing_localization_color: {
|
|
3936
4800
|
type: ["array", "null"],
|
|
3937
4801
|
default: null,
|
|
3938
|
-
description: "Font color for strings that are missing translation and
|
|
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
|
-
|
|
4807
|
+
translation: {
|
|
3944
4808
|
type: ["object", "null"],
|
|
3945
4809
|
default: null,
|
|
3946
|
-
description: "Additional
|
|
4810
|
+
description: "Additional translation for localization."
|
|
3947
4811
|
}
|
|
3948
4812
|
})
|
|
3949
4813
|
);
|
|
3950
4814
|
return localizationParameters;
|
|
3951
4815
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
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
|
-
|
|
3962
|
-
|
|
4819
|
+
isStringOrTextWithFontCustomization(value) {
|
|
4820
|
+
return typeof value === "string" || this.isTextWithFontCustomization(value);
|
|
3963
4821
|
}
|
|
3964
|
-
|
|
3965
|
-
return
|
|
4822
|
+
isStringArray(value) {
|
|
4823
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
3966
4824
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
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
|
-
|
|
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
|
|
4449
|
-
this.i18n = new I18n(
|
|
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
|
|
4531
|
-
|
|
4532
|
-
|
|
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
|
|
4553
|
-
return this.dataStores[0].getItem(
|
|
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
|
|
4573
|
-
return this.dataStores[0].deleteItem(
|
|
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].
|
|
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].
|
|
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
|
|
4627
|
-
return this.dataStores[0].itemExists(
|
|
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("
|
|
4645
|
-
const
|
|
4646
|
-
const
|
|
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
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6744
|
+
missing_localization_color,
|
|
5497
6745
|
// eslint-disable-line @typescript-eslint/no-unused-vars
|
|
5498
|
-
|
|
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
|
-
|
|
6766
|
+
missing_localization_color,
|
|
5519
6767
|
// eslint-disable-line @typescript-eslint/no-unused-vars
|
|
5520
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
6881
|
-
if (i18n) {
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
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
|
-
|
|
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
|
|
7031
|
-
return this.
|
|
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
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
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
|
-
|
|
7933
|
-
this.
|
|
7934
|
-
|
|
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
|
-
|
|
7945
|
-
if (!this.
|
|
7946
|
-
|
|
9497
|
+
resume() {
|
|
9498
|
+
if (!this.isRecording) {
|
|
9499
|
+
throw new Error("cannot resume SoundRecorder because it is not started.");
|
|
7947
9500
|
}
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
this.
|
|
7952
|
-
|
|
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
|
-
|
|
7974
|
-
|
|
9508
|
+
/** Is the `SoundRecorder` currently recording? */
|
|
9509
|
+
get isRecording() {
|
|
9510
|
+
return this._isRecording;
|
|
7975
9511
|
}
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
this.
|
|
9512
|
+
/** Is the `SoundRecorder` currently paused? */
|
|
9513
|
+
get isPaused() {
|
|
9514
|
+
return this._isPaused;
|
|
7979
9515
|
}
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
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
|
-
|
|
7987
|
-
|
|
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
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
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
|
-
|
|
7996
|
-
|
|
9623
|
+
getMimeTypeWithoutCodecs(mimeType) {
|
|
9624
|
+
const match = mimeType.match(/^[^;]+/);
|
|
9625
|
+
return match ? match[0] : "";
|
|
7997
9626
|
}
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
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
|
|
8003
|
-
}
|
|
8004
|
-
set fillColorPaintNotAntialiased(value) {
|
|
8005
|
-
this._fillColorPaintNotAntialiased = value;
|
|
9634
|
+
return void 0;
|
|
8006
9635
|
}
|
|
8007
|
-
|
|
8008
|
-
if (!this._strokeColorPaintNotAntialiased) {
|
|
8009
|
-
throw new Error("strokeColorPaintNotAntiAliased is undefined");
|
|
8010
|
-
}
|
|
8011
|
-
return this._strokeColorPaintNotAntialiased;
|
|
9636
|
+
dispose() {
|
|
8012
9637
|
}
|
|
8013
|
-
|
|
8014
|
-
|
|
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.
|
|
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
|
-
|
|
8088
|
-
|
|
9828
|
+
get interpolation() {
|
|
9829
|
+
return this._interpolation;
|
|
8089
9830
|
}
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
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
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9890
|
+
this.paint.setAlphaf(this.absoluteAlpha);
|
|
8236
9891
|
}
|
|
8237
|
-
canvas.drawText(textForDraw, x, y,
|
|
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.
|
|
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
|