@caperjs/core 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.mjs +3 -1
- package/build/internal/discovery.mjs +1 -1
- package/build/internal/manifest.mjs +8 -3
- package/build/internal/validate.mjs +3 -1
- package/build/plugins/assetTypes.mjs +4 -4
- package/build/plugins/caperConfig.mjs +12 -5
- package/build/plugins/lists.mjs +29 -9
- package/cli/create.mjs +57 -6
- package/cli/create.test.mjs +36 -0
- package/cli/update.mjs +1 -1
- package/lib/{CaptionsPlugin-WPAcfAkx.js → CaptionsPlugin-BLbLl3WB.js} +3 -3
- package/lib/{CaptionsPlugin-WPAcfAkx.js.map → CaptionsPlugin-BLbLl3WB.js.map} +1 -1
- package/lib/{DataAdapter-DS-twowe.js → DataAdapter-aHSNM_26.js} +12 -7
- package/lib/DataAdapter-aHSNM_26.js.map +1 -0
- package/lib/{DebugRenderer-D1ryrRYx.js → DebugRenderer-8LjHtBzA.js} +2 -2
- package/lib/{DebugRenderer-D1ryrRYx.js.map → DebugRenderer-8LjHtBzA.js.map} +1 -1
- package/lib/{DevToolsPlugin-DH7zX7HU.js → DevToolsPlugin-Vrk6aOtS.js} +2 -2
- package/lib/{DevToolsPlugin-DH7zX7HU.js.map → DevToolsPlugin-Vrk6aOtS.js.map} +1 -1
- package/lib/{GSAPPlugin-BBtRjUqe.js → GSAPPlugin-Hm3F4zdg.js} +3 -3
- package/lib/{GSAPPlugin-BBtRjUqe.js.map → GSAPPlugin-Hm3F4zdg.js.map} +1 -1
- package/lib/{LayoutPlugin-Co6vcoV4.js → LayoutPlugin-Dpgeawfw.js} +4 -4
- package/lib/{LayoutPlugin-Co6vcoV4.js.map → LayoutPlugin-Dpgeawfw.js.map} +1 -1
- package/lib/{SpinePlugin-uN641Hmk.js → SpinePlugin-B08yAw1S.js} +690 -793
- package/lib/SpinePlugin-B08yAw1S.js.map +1 -0
- package/lib/{StatsPlugin-vZQRm6wh.js → StatsPlugin-D2Xa5j97.js} +5 -2
- package/lib/StatsPlugin-D2Xa5j97.js.map +1 -0
- package/lib/{VoiceOverPlugin-KlZdGj1G.js → VoiceOverPlugin-qtFlqpRn.js} +6 -5
- package/lib/VoiceOverPlugin-qtFlqpRn.js.map +1 -0
- package/lib/caper.mjs +37 -19
- package/lib/caper.mjs.map +1 -1
- package/lib/core/Application.d.ts +8 -3
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/Application.test.d.ts +2 -0
- package/lib/core/Application.test.d.ts.map +1 -0
- package/lib/core/config.d.ts +6 -1
- package/lib/core/config.d.ts.map +1 -1
- package/lib/core/config.test.d.ts +2 -0
- package/lib/core/config.test.d.ts.map +1 -0
- package/lib/core/interfaces/IApplication.d.ts +6 -6
- package/lib/core/interfaces/IApplication.d.ts.map +1 -1
- package/lib/core/interfaces/ICoreSignals.d.ts +2 -1
- package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
- package/lib/display/Camera.test.d.ts +2 -0
- package/lib/display/Camera.test.d.ts.map +1 -0
- package/lib/display/Entity.d.ts +2 -2
- package/lib/display/SceneTransition.test.d.ts +1 -0
- package/lib/display/SceneTransition.test.d.ts.map +1 -0
- package/lib/display/SpineAnimation.d.ts +3 -3
- package/lib/mixins/factory/methods.d.ts.map +1 -1
- package/lib/plugins/AssetsPlugin.d.ts.map +1 -1
- package/lib/plugins/AssetsPlugin.test.d.ts +2 -0
- package/lib/plugins/AssetsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/DataAdapter.d.ts.map +1 -1
- package/lib/plugins/DataAdapter.test.d.ts +2 -0
- package/lib/plugins/DataAdapter.test.d.ts.map +1 -0
- package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
- package/lib/plugins/FullScreenPlugin.test.d.ts +2 -0
- package/lib/plugins/FullScreenPlugin.test.d.ts.map +1 -0
- package/lib/plugins/GSAPPlugin.d.ts +0 -2
- package/lib/plugins/GSAPPlugin.d.ts.map +1 -1
- package/lib/plugins/GSAPPlugin.test.d.ts +2 -0
- package/lib/plugins/GSAPPlugin.test.d.ts.map +1 -0
- package/lib/plugins/LookupPlugin.d.ts +6 -1
- package/lib/plugins/LookupPlugin.d.ts.map +1 -1
- package/lib/plugins/LookupPlugin.test.d.ts +2 -0
- package/lib/plugins/LookupPlugin.test.d.ts.map +1 -0
- package/lib/plugins/PopupManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/PopupManagerPlugin.test.d.ts +1 -0
- package/lib/plugins/PopupManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/SceneManagerPlugin.d.ts +1 -0
- package/lib/plugins/SceneManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/SceneManagerPlugin.test.d.ts +2 -0
- package/lib/plugins/SceneManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/StatsPlugin.d.ts +1 -0
- package/lib/plugins/StatsPlugin.d.ts.map +1 -1
- package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
- package/lib/plugins/actions/ActionsPlugin.test.d.ts +2 -0
- package/lib/plugins/actions/ActionsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/audio/AudioChannel.d.ts.map +1 -1
- package/lib/plugins/audio/AudioManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/audio/AudioManagerPlugin.test.d.ts +2 -0
- package/lib/plugins/audio/AudioManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/audio/VoiceOverPlugin.d.ts.map +1 -1
- package/lib/plugins/captions/CaptionsPlugin.test.d.ts +2 -0
- package/lib/plugins/captions/CaptionsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/focus/FocusManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/focus/FocusManagerPlugin.test.d.ts +2 -0
- package/lib/plugins/focus/FocusManagerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/input/AbstractControls.d.ts +5 -0
- package/lib/plugins/input/AbstractControls.d.ts.map +1 -1
- package/lib/plugins/input/Controls.test.d.ts +2 -0
- package/lib/plugins/input/Controls.test.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts +5 -0
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +1 -1
- package/lib/plugins/input/touch/VirtualControls.d.ts +6 -0
- package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
- package/lib/plugins/input/touch/VirtualControls.test.d.ts +2 -0
- package/lib/plugins/input/touch/VirtualControls.test.d.ts.map +1 -0
- package/lib/{registries-BLrxALZ0.js → registries-m2vKTIih.js} +156 -101
- package/lib/registries-m2vKTIih.js.map +1 -0
- package/lib/store/Store.d.ts.map +1 -1
- package/lib/ui/Button.d.ts +8 -1
- package/lib/ui/Button.d.ts.map +1 -1
- package/lib/ui/Button.test.d.ts +2 -0
- package/lib/ui/Button.test.d.ts.map +1 -0
- package/lib/ui/UICanvas.d.ts +10 -1
- package/lib/ui/UICanvas.d.ts.map +1 -1
- package/lib/ui/UICanvas.test.d.ts +0 -1
- package/lib/ui/UICanvas.test.d.ts.map +1 -1
- package/lib/utils/color.d.ts.map +1 -1
- package/lib/utils/color.test.d.ts +2 -0
- package/lib/utils/color.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/Application.test.ts +168 -0
- package/src/core/Application.ts +37 -27
- package/src/core/config.test.ts +79 -0
- package/src/core/config.ts +28 -6
- package/src/core/interfaces/IApplication.ts +6 -6
- package/src/core/interfaces/ICoreSignals.ts +2 -0
- package/src/display/Camera.test.ts +40 -0
- package/src/display/Camera.ts +3 -3
- package/src/display/Entity.ts +2 -2
- package/src/display/SceneTransition.test.ts +58 -0
- package/src/display/SceneTransition.ts +1 -1
- package/src/display/SpineAnimation.ts +4 -4
- package/src/mixins/factory/methods.ts +0 -1
- package/src/plugins/AssetsPlugin.test.ts +48 -0
- package/src/plugins/AssetsPlugin.ts +3 -1
- package/src/plugins/DataAdapter.test.ts +32 -0
- package/src/plugins/DataAdapter.ts +5 -1
- package/src/plugins/FullScreenPlugin.test.ts +48 -0
- package/src/plugins/FullScreenPlugin.ts +5 -1
- package/src/plugins/GSAPPlugin.test.ts +31 -0
- package/src/plugins/GSAPPlugin.ts +1 -4
- package/src/plugins/LookupPlugin.test.ts +42 -0
- package/src/plugins/LookupPlugin.ts +15 -3
- package/src/plugins/PopupManagerPlugin.test.ts +53 -0
- package/src/plugins/PopupManagerPlugin.ts +5 -3
- package/src/plugins/SceneManagerPlugin.test.ts +76 -0
- package/src/plugins/SceneManagerPlugin.ts +21 -11
- package/src/plugins/StatsPlugin.ts +8 -0
- package/src/plugins/WebEventsPlugin.test.ts +32 -0
- package/src/plugins/WebEventsPlugin.ts +2 -0
- package/src/plugins/actions/ActionsPlugin.test.ts +68 -0
- package/src/plugins/actions/ActionsPlugin.ts +5 -5
- package/src/plugins/audio/AudioChannel.test.ts +19 -0
- package/src/plugins/audio/AudioChannel.ts +8 -1
- package/src/plugins/audio/AudioManagerPlugin.test.ts +41 -0
- package/src/plugins/audio/AudioManagerPlugin.ts +3 -2
- package/src/plugins/audio/VoiceOverPlugin.ts +1 -0
- package/src/plugins/captions/CaptionsPlugin.test.ts +40 -0
- package/src/plugins/captions/CaptionsPlugin.ts +1 -1
- package/src/plugins/focus/FocusManagerPlugin.test.ts +71 -0
- package/src/plugins/focus/FocusManagerPlugin.ts +7 -0
- package/src/plugins/input/AbstractControls.ts +6 -0
- package/src/plugins/input/Controls.test.ts +149 -0
- package/src/plugins/input/keyboard/KeyboardContols.ts +29 -5
- package/src/plugins/input/touch/VirtualControls.test.ts +38 -0
- package/src/plugins/input/touch/VirtualControls.ts +44 -10
- package/src/plugins/spine/SpinePlugin.ts +2 -2
- package/src/store/Store.test.ts +15 -0
- package/src/store/Store.ts +4 -1
- package/src/ui/Button.test.ts +248 -0
- package/src/ui/Button.ts +42 -10
- package/src/ui/UICanvas.test.ts +109 -6
- package/src/ui/UICanvas.ts +41 -3
- package/src/utils/color.test.ts +16 -0
- package/src/utils/color.ts +2 -1
- package/src/utils/platform.ts +1 -1
- package/src/version.ts +1 -1
- package/lib/DataAdapter-DS-twowe.js.map +0 -1
- package/lib/SpinePlugin-uN641Hmk.js.map +0 -1
- package/lib/StatsPlugin-vZQRm6wh.js.map +0 -1
- package/lib/VoiceOverPlugin-KlZdGj1G.js.map +0 -1
- package/lib/registries-BLrxALZ0.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application } from '../../../core/Application';
|
|
2
2
|
import { WithSignals } from '../../../mixins/signals';
|
|
3
|
-
import { bindAllMethods } from '../../../utils';
|
|
3
|
+
import { bindAllMethods, Logger } from '../../../utils';
|
|
4
4
|
import type { Action } from '../../actions';
|
|
5
5
|
import { normalizeKey, type KeyboardEventDetail } from '../../KeyboardPlugin';
|
|
6
6
|
import { AbstractControls } from '../AbstractControls';
|
|
@@ -15,6 +15,7 @@ export class KeyboardControls extends WithSignals(AbstractControls) {
|
|
|
15
15
|
private _keyCombinationsMap: Map<string[], Action> = new Map();
|
|
16
16
|
private _activeDownKeys: Map<string, Action> = new Map();
|
|
17
17
|
private _activeUpKeys: Map<string, Action> = new Map();
|
|
18
|
+
private _warnedMissingActions: Set<string> = new Set();
|
|
18
19
|
|
|
19
20
|
constructor() {
|
|
20
21
|
super();
|
|
@@ -31,11 +32,9 @@ export class KeyboardControls extends WithSignals(AbstractControls) {
|
|
|
31
32
|
return false;
|
|
32
33
|
}
|
|
33
34
|
if (Array.isArray(controlsAction)) {
|
|
34
|
-
return (
|
|
35
|
-
this._keyCombinationsMap.has(controlsAction) || controlsAction.some((key) => this._singleDownKeys.has(key))
|
|
36
|
-
);
|
|
35
|
+
return controlsAction.some((key) => this._isInputActive(key));
|
|
37
36
|
} else {
|
|
38
|
-
return this.
|
|
37
|
+
return this._isInputActive(controlsAction);
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -57,6 +56,27 @@ export class KeyboardControls extends WithSignals(AbstractControls) {
|
|
|
57
56
|
this.app.ticker.add(this._update);
|
|
58
57
|
}
|
|
59
58
|
|
|
59
|
+
public destroy(): void {
|
|
60
|
+
this.app.ticker.remove(this._update);
|
|
61
|
+
super.destroy();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A single key, or a `+` separated combination requiring every key to be down. */
|
|
65
|
+
private _isInputActive(input: string): boolean {
|
|
66
|
+
if (input.includes('+')) {
|
|
67
|
+
return input.split('+').every((key) => this._singleDownKeys.has(key));
|
|
68
|
+
}
|
|
69
|
+
return this._singleDownKeys.has(input);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private _warnMissingAction(key: string) {
|
|
73
|
+
if (this._warnedMissingActions.has(key)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this._warnedMissingActions.add(key);
|
|
77
|
+
Logger.warn(`KeyboardControls: scheme references unknown action "${key}"`);
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
private _sortActions() {
|
|
61
81
|
const actions = this.app.actionsPlugin.getActions();
|
|
62
82
|
this._keyCombinations = [];
|
|
@@ -68,6 +88,10 @@ export class KeyboardControls extends WithSignals(AbstractControls) {
|
|
|
68
88
|
keys.forEach((key) => {
|
|
69
89
|
const item = this._keyDownMap[key];
|
|
70
90
|
const action = actions[key];
|
|
91
|
+
if (!action) {
|
|
92
|
+
this._warnMissingAction(key);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
71
95
|
if (
|
|
72
96
|
action.context !== '*' &&
|
|
73
97
|
action.context !== this.app.actionContext &&
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// VirtualControls reaches the app through Application.getInstance(). Stub it.
|
|
4
|
+
// The stub app is filled in after the imports (getInstance is only called lazily).
|
|
5
|
+
const holder = vi.hoisted(() => ({ app: null as any }));
|
|
6
|
+
vi.mock('../../../core/Application', () => ({
|
|
7
|
+
Application: { getInstance: () => holder.app },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { Signal } from '../../../signals';
|
|
11
|
+
import { VirtualControls } from './VirtualControls';
|
|
12
|
+
|
|
13
|
+
describe('VirtualControls', () => {
|
|
14
|
+
it('stops reacting to action context changes after destroy', () => {
|
|
15
|
+
const onActionContextChanged = new Signal<(context: string) => void>();
|
|
16
|
+
holder.app = {
|
|
17
|
+
signal: { onActionContextChanged },
|
|
18
|
+
ticker: { add: vi.fn(), remove: vi.fn() },
|
|
19
|
+
actionsPlugin: { getActions: () => ({}) },
|
|
20
|
+
actionContext: 'default',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const controls = new VirtualControls();
|
|
24
|
+
const sortActions = vi.fn();
|
|
25
|
+
(controls as any)._sortActions = sortActions;
|
|
26
|
+
|
|
27
|
+
controls.initialize({});
|
|
28
|
+
const callsAfterInit = sortActions.mock.calls.length;
|
|
29
|
+
|
|
30
|
+
onActionContextChanged.emit('menu');
|
|
31
|
+
expect(sortActions).toHaveBeenCalledTimes(callsAfterInit + 1);
|
|
32
|
+
|
|
33
|
+
controls.destroy();
|
|
34
|
+
|
|
35
|
+
onActionContextChanged.emit('game');
|
|
36
|
+
expect(sortActions).toHaveBeenCalledTimes(callsAfterInit + 1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { bindAllMethods } from '../../../utils';
|
|
1
|
+
import { bindAllMethods, Logger } from '../../../utils';
|
|
2
2
|
import type { ControlsActionMap, JoystickControlsScheme, TouchControlsMap, TouchControlsScheme } from '../types';
|
|
3
3
|
|
|
4
4
|
import { JoystickDirection } from '..';
|
|
5
5
|
import { Application } from '../../../core/Application';
|
|
6
6
|
import { WithSignals } from '../../../mixins/signals';
|
|
7
|
+
import type { SignalConnection } from '../../../signals';
|
|
7
8
|
import type { IButton, IJoystick } from '../../../ui';
|
|
8
9
|
import type { Action } from '../../actions';
|
|
9
10
|
import { AbstractControls } from '../AbstractControls';
|
|
@@ -20,6 +21,8 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
20
21
|
private _activeButtonDownIds: Map<string, Action> = new Map();
|
|
21
22
|
private _activeButtonUpIds: Map<string, Action> = new Map();
|
|
22
23
|
private _combinationsMap: Map<string[], Action> = new Map();
|
|
24
|
+
private _buttonConnections: Map<IButton, SignalConnection[]> = new Map();
|
|
25
|
+
private _warnedMissingActions: Set<string> = new Set();
|
|
23
26
|
|
|
24
27
|
constructor() {
|
|
25
28
|
super();
|
|
@@ -44,12 +47,14 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
44
47
|
if (!button || this._buttons.has(button)) {
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
const connections = [
|
|
48
51
|
button.onDown.connect(() => this._handleButtonDown(button)),
|
|
49
52
|
button.onUp.connect(() => this._handleButtonUp(button)),
|
|
50
53
|
button.onUpOutside.connect(() => this._handleButtonUp(button)),
|
|
51
54
|
button.onDestroy.connect(() => this.removeButton(button)),
|
|
52
|
-
|
|
55
|
+
];
|
|
56
|
+
this.addSignalConnection(...connections);
|
|
57
|
+
this._buttonConnections.set(button, connections);
|
|
53
58
|
this._buttons.add(button);
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -57,10 +62,10 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
57
62
|
if (!button || !this._buttons.has(button)) {
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
// signals disconnect by identity — disconnect the connections made in addButton
|
|
66
|
+
this._buttonConnections.get(button)?.forEach((connection) => connection.disconnect());
|
|
67
|
+
this._buttonConnections.delete(button);
|
|
68
|
+
this._singleDownButtons.delete(button.id!);
|
|
64
69
|
this._buttons.delete(button);
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -69,7 +74,7 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
69
74
|
this._buttonDownMap = scheme.down || {};
|
|
70
75
|
this._buttonUpMap = scheme.up || {};
|
|
71
76
|
this._joystickMap = scheme.joystick || {};
|
|
72
|
-
this.app.signal.onActionContextChanged.connect(this._sortActions);
|
|
77
|
+
this.addSignalConnection(this.app.signal.onActionContextChanged.connect(this._sortActions));
|
|
73
78
|
this._sortActions();
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -77,13 +82,18 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
77
82
|
this.app.ticker.add(this._update);
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
public destroy(): void {
|
|
86
|
+
this.app.ticker.remove(this._update);
|
|
87
|
+
super.destroy();
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
isActionActive(action: Action): boolean {
|
|
81
91
|
const buttonAction = this.scheme['down']?.[action] ?? null;
|
|
82
92
|
if (buttonAction) {
|
|
83
93
|
if (Array.isArray(buttonAction)) {
|
|
84
|
-
return this.
|
|
94
|
+
return buttonAction.some((input) => this._isInputActive(input));
|
|
85
95
|
} else {
|
|
86
|
-
return this.
|
|
96
|
+
return this._isInputActive(buttonAction);
|
|
87
97
|
}
|
|
88
98
|
} else {
|
|
89
99
|
const joystickAction = this.scheme['joystick']?.[action] ?? null;
|
|
@@ -98,6 +108,22 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
98
108
|
return false;
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
/** A single button id, or a `+` separated combination requiring every input to be down. */
|
|
112
|
+
private _isInputActive(input: string): boolean {
|
|
113
|
+
if (input.includes('+')) {
|
|
114
|
+
return input.split('+').every((id) => this._singleDownButtons.has(id) || this._joystick?.direction === id);
|
|
115
|
+
}
|
|
116
|
+
return this._singleDownButtons.has(input);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private _warnMissingAction(key: string) {
|
|
120
|
+
if (this._warnedMissingActions.has(key)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this._warnedMissingActions.add(key);
|
|
124
|
+
Logger.warn(`VirtualControls: scheme references unknown action "${key}"`);
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
private _sortActions() {
|
|
102
128
|
const actions = this.app.actionsPlugin.getActions();
|
|
103
129
|
this._combinations = [];
|
|
@@ -110,6 +136,10 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
110
136
|
buttons.forEach((key) => {
|
|
111
137
|
const item = this._buttonDownMap[key];
|
|
112
138
|
const action = actions[key];
|
|
139
|
+
if (!action) {
|
|
140
|
+
this._warnMissingAction(key);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
113
143
|
if (
|
|
114
144
|
action.context !== '*' &&
|
|
115
145
|
action.context !== this.app.actionContext &&
|
|
@@ -141,6 +171,10 @@ export class VirtualControls extends WithSignals(AbstractControls) {
|
|
|
141
171
|
buttons.forEach((key) => {
|
|
142
172
|
const item = this._buttonUpMap[key];
|
|
143
173
|
const action = actions[key];
|
|
174
|
+
if (!action) {
|
|
175
|
+
this._warnMissingAction(key);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
144
178
|
if (
|
|
145
179
|
action.context !== '*' &&
|
|
146
180
|
action.context !== this.app.actionContext &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extensions } from 'pixi.js';
|
|
2
2
|
import { Plugin } from '../Plugin';
|
|
3
|
-
import { Spine, spineLoaderExtension,
|
|
3
|
+
import { Spine, spineLoaderExtension, spineTextureAtlasLoader } from './pixi-spine';
|
|
4
4
|
|
|
5
5
|
export class SpinePlugin extends Plugin {
|
|
6
6
|
public readonly id = 'SpinePlugin';
|
|
@@ -8,7 +8,7 @@ export class SpinePlugin extends Plugin {
|
|
|
8
8
|
public async initialize() {
|
|
9
9
|
extensions.add(spineTextureAtlasLoader);
|
|
10
10
|
extensions.add(spineLoaderExtension);
|
|
11
|
-
|
|
11
|
+
// SpinePipe and DarkTintBatcher register themselves at module scope in ./pixi-spine
|
|
12
12
|
(window as any).Spine = Spine;
|
|
13
13
|
}
|
|
14
14
|
}
|
package/src/store/Store.test.ts
CHANGED
|
@@ -143,4 +143,19 @@ describe('Store', () => {
|
|
|
143
143
|
expect(saveA).toHaveBeenCalledWith('k', 1);
|
|
144
144
|
expect(saveB).toHaveBeenCalledWith('k', 1);
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
it('save with "*" anywhere in an adapter array fans out to every storage-capable plugin', async () => {
|
|
148
|
+
const saveA = vi.fn(async () => 'a');
|
|
149
|
+
const saveB = vi.fn(async () => 'b');
|
|
150
|
+
const store = new Store().initialize(
|
|
151
|
+
makeApp([
|
|
152
|
+
{ id: 'a', save: saveA, load: async () => undefined },
|
|
153
|
+
{ id: 'b', save: saveB, load: async () => undefined },
|
|
154
|
+
{ id: 'c' }, // not storage-capable → must be skipped
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
await store.save(['a', '*'] as never, 'k', 1);
|
|
158
|
+
expect(saveA).toHaveBeenCalledWith('k', 1);
|
|
159
|
+
expect(saveB).toHaveBeenCalledWith('k', 1);
|
|
160
|
+
});
|
|
146
161
|
});
|
package/src/store/Store.ts
CHANGED
|
@@ -126,7 +126,10 @@ export class Store implements IStore {
|
|
|
126
126
|
keys = adapterId as string[] | Partial<AdapterSaveConfig>[];
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
const hasStarKey = keys.some(
|
|
130
|
+
(k) => (k as string) === '*' || (k as Partial<AdapterSaveConfig>)?.adapterId === '*',
|
|
131
|
+
);
|
|
132
|
+
if (hasStarKey) {
|
|
130
133
|
keys = this.allAdapterIds();
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Button transitively imports Application → Pixi display graph. Stub it — these
|
|
4
|
+
// tests drive the press-state machine directly and never touch the app singleton.
|
|
5
|
+
vi.mock('../core/Application', () => ({
|
|
6
|
+
Application: { getInstance: () => ({}) },
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { Button } from './Button';
|
|
10
|
+
import { Signal } from '../signals';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds a Button instance without running the real constructor (which drags in
|
|
14
|
+
* Factory/Interactive/Focusable mixin construction and a real Pixi Sprite). Only
|
|
15
|
+
* the fields the handlers under test actually touch are hand-initialized.
|
|
16
|
+
*/
|
|
17
|
+
function makeButton() {
|
|
18
|
+
const button = Object.create(Button.prototype) as Button;
|
|
19
|
+
|
|
20
|
+
// Pixi's Container (via eventemitter3) normally initializes these in its own
|
|
21
|
+
// constructor, which this hand-rolled instance never runs; `on`/`off` throw
|
|
22
|
+
// without them.
|
|
23
|
+
(button as any)._events = Object.create(null);
|
|
24
|
+
(button as any)._eventsCount = 0;
|
|
25
|
+
|
|
26
|
+
(button as any).config = {
|
|
27
|
+
id: 'button',
|
|
28
|
+
textures: { default: '' },
|
|
29
|
+
sheet: undefined,
|
|
30
|
+
enabled: true,
|
|
31
|
+
cursor: 'default',
|
|
32
|
+
disabledCursor: 'not-allowed',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
(button as any)._enabled = true;
|
|
36
|
+
button.isDown = false;
|
|
37
|
+
button.isOver = false;
|
|
38
|
+
(button as any)._pointerId = undefined;
|
|
39
|
+
(button as any).isKeyDown = false;
|
|
40
|
+
(button as any).cursor = 'default';
|
|
41
|
+
(button as any).focusEnabled = true;
|
|
42
|
+
|
|
43
|
+
(button as any).view = { texture: undefined };
|
|
44
|
+
(button as any).make = { texture: vi.fn(() => 'texture') };
|
|
45
|
+
(button as any)._isDownCallbacks = new Map();
|
|
46
|
+
(button as any)._isDownListenerAdded = false;
|
|
47
|
+
|
|
48
|
+
button.onDown = new Signal();
|
|
49
|
+
button.onUp = new Signal();
|
|
50
|
+
button.onUpOutside = new Signal();
|
|
51
|
+
button.onOut = new Signal();
|
|
52
|
+
button.onOver = new Signal();
|
|
53
|
+
button.onClick = new Signal();
|
|
54
|
+
button.onEnabled = new Signal();
|
|
55
|
+
button.onDisabled = new Signal();
|
|
56
|
+
button.onKeyboardEvent = new Signal();
|
|
57
|
+
button.onDestroy = new Signal();
|
|
58
|
+
|
|
59
|
+
return button;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('Button press-state machine', () => {
|
|
63
|
+
it('recovers from a press that is disabled mid-press', () => {
|
|
64
|
+
const button = makeButton();
|
|
65
|
+
const onDown = vi.fn();
|
|
66
|
+
button.onDown.connect(onDown);
|
|
67
|
+
|
|
68
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
69
|
+
expect(button.isDown).toBe(true);
|
|
70
|
+
|
|
71
|
+
button.enabled = false;
|
|
72
|
+
expect(button.isDown).toBe(false);
|
|
73
|
+
expect((button as any)._pointerId).toBeUndefined();
|
|
74
|
+
|
|
75
|
+
button.enabled = true;
|
|
76
|
+
(button as any).handlePointerDown({ pointerId: 2 });
|
|
77
|
+
|
|
78
|
+
expect(onDown).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(button.isDown).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('clears press state on pointerupoutside while disabled and does not emit onUpOutside', () => {
|
|
83
|
+
const button = makeButton();
|
|
84
|
+
const onUpOutside = vi.fn();
|
|
85
|
+
button.onUpOutside.connect(onUpOutside);
|
|
86
|
+
|
|
87
|
+
// Arm a stale press, then disable without going through the enabled setter,
|
|
88
|
+
// to exercise the handler's own cleanup path directly.
|
|
89
|
+
button.isDown = true;
|
|
90
|
+
(button as any)._pointerId = 1;
|
|
91
|
+
(button as any)._enabled = false;
|
|
92
|
+
|
|
93
|
+
(button as any).handlePointerUpOutside({ pointerId: 1 });
|
|
94
|
+
|
|
95
|
+
expect(button.isDown).toBe(false);
|
|
96
|
+
expect((button as any)._pointerId).toBeUndefined();
|
|
97
|
+
expect(onUpOutside).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('removes the window pointerup listener on destroy', () => {
|
|
101
|
+
const button = makeButton();
|
|
102
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
103
|
+
|
|
104
|
+
// Stub out the real mixin chain's destroy() (Focusable → Interactive →
|
|
105
|
+
// WithSignals → Factory → real Pixi Container.destroy) so this test isolates
|
|
106
|
+
// Button.destroy()'s own added cleanup without needing a fully-constructed
|
|
107
|
+
// Container underneath.
|
|
108
|
+
const superDestroy = vi
|
|
109
|
+
.spyOn(Object.getPrototypeOf(Button.prototype), 'destroy')
|
|
110
|
+
.mockImplementation(() => {});
|
|
111
|
+
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
112
|
+
|
|
113
|
+
button.destroy();
|
|
114
|
+
|
|
115
|
+
expect(removeSpy).toHaveBeenCalledWith('pointerup', (button as any).handlePointerUpOutside);
|
|
116
|
+
expect(superDestroy).toHaveBeenCalled();
|
|
117
|
+
expect(button.isDown).toBe(false);
|
|
118
|
+
expect((button as any)._pointerId).toBeUndefined();
|
|
119
|
+
|
|
120
|
+
removeSpy.mockRestore();
|
|
121
|
+
superDestroy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not click after a cancelled press (disabled then re-enabled)', () => {
|
|
125
|
+
const button = makeButton();
|
|
126
|
+
const onClick = vi.fn();
|
|
127
|
+
button.onClick.connect(onClick);
|
|
128
|
+
|
|
129
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
130
|
+
button.enabled = false;
|
|
131
|
+
button.enabled = true;
|
|
132
|
+
|
|
133
|
+
// A real pointer-driven click event carries the same pointerId as the press.
|
|
134
|
+
(button as any).handleClick({ pointerId: 1 });
|
|
135
|
+
|
|
136
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('clicks normally after a press', () => {
|
|
140
|
+
const button = makeButton();
|
|
141
|
+
const onClick = vi.fn();
|
|
142
|
+
button.onClick.connect(onClick);
|
|
143
|
+
|
|
144
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
145
|
+
(button as any).handleClick({ pointerId: 1 });
|
|
146
|
+
|
|
147
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('dispatches a click action with no data without throwing, attaching the button to the payload', () => {
|
|
151
|
+
const button = makeButton();
|
|
152
|
+
(button as any).config.actions = { click: { id: 'foo' } };
|
|
153
|
+
const action = vi.fn();
|
|
154
|
+
Object.defineProperty(button, 'app', { value: { action }, configurable: true });
|
|
155
|
+
|
|
156
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
157
|
+
expect(() => (button as any).handleClick({ pointerId: 1 })).not.toThrow();
|
|
158
|
+
|
|
159
|
+
expect(action).toHaveBeenCalledWith('foo', { button });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('still clicks for a synthetic keyboard activation (no pointerId)', () => {
|
|
163
|
+
const button = makeButton();
|
|
164
|
+
const onClick = vi.fn();
|
|
165
|
+
button.onClick.connect(onClick);
|
|
166
|
+
|
|
167
|
+
// Mimics FocusManagerPlugin's synthetic pointerdown on Enter/Space keydown.
|
|
168
|
+
(button as any).handlePointerDown({ type: 'pointerdown' });
|
|
169
|
+
(button as any).handleClick();
|
|
170
|
+
|
|
171
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('fires onClick exactly once for a single accessibility activation delivered as click+tap', () => {
|
|
175
|
+
const button = makeButton();
|
|
176
|
+
const onClick = vi.fn();
|
|
177
|
+
button.onClick.connect(onClick);
|
|
178
|
+
|
|
179
|
+
// AccessibilitySystem dispatches ONE shared FederatedEvent instance as both
|
|
180
|
+
// 'click' and 'tap' — Button connects both to handleClick.
|
|
181
|
+
const a11yEvent = { type: 'click' };
|
|
182
|
+
(button as any).handleClick(a11yEvent);
|
|
183
|
+
(button as any).handleClick(a11yEvent);
|
|
184
|
+
|
|
185
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('suppresses a cancelled pointer-driven click even without a prior press in this test', () => {
|
|
189
|
+
const button = makeButton();
|
|
190
|
+
const onClick = vi.fn();
|
|
191
|
+
button.onClick.connect(onClick);
|
|
192
|
+
|
|
193
|
+
// isDown is false (no press) — a click event carrying a pointerId must be treated
|
|
194
|
+
// as a cancelled press and suppressed.
|
|
195
|
+
(button as any).handleClick({ pointerId: 5 });
|
|
196
|
+
|
|
197
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('fires onClick for each of two distinct accessibility activations', () => {
|
|
201
|
+
const button = makeButton();
|
|
202
|
+
const onClick = vi.fn();
|
|
203
|
+
button.onClick.connect(onClick);
|
|
204
|
+
|
|
205
|
+
(button as any).handleClick({ type: 'click' });
|
|
206
|
+
(button as any).handleClick({ type: 'click' });
|
|
207
|
+
|
|
208
|
+
expect(onClick).toHaveBeenCalledTimes(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('toggles the ticker listener on for the first isDown callback and off when the last is removed', () => {
|
|
212
|
+
const button = makeButton();
|
|
213
|
+
const add = vi.fn();
|
|
214
|
+
const remove = vi.fn();
|
|
215
|
+
Object.defineProperty(button, 'app', { value: { ticker: { add, remove } }, configurable: true });
|
|
216
|
+
|
|
217
|
+
button.addIsDownCallback('a', () => {});
|
|
218
|
+
button.addIsDownCallback('b', () => {});
|
|
219
|
+
|
|
220
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
221
|
+
expect(remove).not.toHaveBeenCalled();
|
|
222
|
+
|
|
223
|
+
button.removeIsDownCallback('a');
|
|
224
|
+
expect(remove).not.toHaveBeenCalled();
|
|
225
|
+
|
|
226
|
+
button.removeIsDownCallback('b');
|
|
227
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('removes the ticker listener on destroy when one is active', () => {
|
|
231
|
+
const button = makeButton();
|
|
232
|
+
const add = vi.fn();
|
|
233
|
+
const remove = vi.fn();
|
|
234
|
+
Object.defineProperty(button, 'app', { value: { ticker: { add, remove } }, configurable: true });
|
|
235
|
+
|
|
236
|
+
button.addIsDownCallback('a', () => {});
|
|
237
|
+
|
|
238
|
+
const superDestroy = vi
|
|
239
|
+
.spyOn(Object.getPrototypeOf(Button.prototype), 'destroy')
|
|
240
|
+
.mockImplementation(() => {});
|
|
241
|
+
|
|
242
|
+
button.destroy();
|
|
243
|
+
|
|
244
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
245
|
+
|
|
246
|
+
superDestroy.mockRestore();
|
|
247
|
+
});
|
|
248
|
+
});
|
package/src/ui/Button.ts
CHANGED
|
@@ -121,6 +121,7 @@ export class Button extends _Button implements IButton {
|
|
|
121
121
|
private _isDownListenerAdded: boolean = false;
|
|
122
122
|
private _pointerId?: number;
|
|
123
123
|
private _textLabel?: Text | HTMLText | BitmapText;
|
|
124
|
+
private _lastClickEvent?: FederatedEvent;
|
|
124
125
|
|
|
125
126
|
/**
|
|
126
127
|
* The text label of the button.
|
|
@@ -190,6 +191,8 @@ export class Button extends _Button implements IButton {
|
|
|
190
191
|
this.onInteraction('tap').connect(this.handleClick, -1),
|
|
191
192
|
this.onInteraction('pointerdown').connect(this.handlePointerDown, -1),
|
|
192
193
|
);
|
|
194
|
+
|
|
195
|
+
this.on('removed', this._resetPressState);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
// enabled state
|
|
@@ -213,6 +216,7 @@ export class Button extends _Button implements IButton {
|
|
|
213
216
|
});
|
|
214
217
|
this.onEnabled.emit();
|
|
215
218
|
} else {
|
|
219
|
+
this._resetPressState();
|
|
216
220
|
this.view.texture = this.make.texture({
|
|
217
221
|
asset: this.config.textures.disabled || this.config.textures.default,
|
|
218
222
|
sheet: this.config.sheet ?? undefined,
|
|
@@ -221,6 +225,18 @@ export class Button extends _Button implements IButton {
|
|
|
221
225
|
}
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Clears any in-progress press: removes the window-level release listener and
|
|
230
|
+
* resets press tracking. Called when the button is disabled, removed, or
|
|
231
|
+
* destroyed mid-press so a stale pointerId can't block future presses.
|
|
232
|
+
*/
|
|
233
|
+
private _resetPressState() {
|
|
234
|
+
window.removeEventListener('pointerup', this.handlePointerUpOutside);
|
|
235
|
+
this.off('pointerupoutside', this.handlePointerUpOutside);
|
|
236
|
+
this.isDown = false;
|
|
237
|
+
this._pointerId = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
224
240
|
public addLabel<T extends Text | HTMLText | BitmapText>(
|
|
225
241
|
config: (Partial<TextProps | HTMLTextProps> & { type?: 'text' | 'html' | 'bitmap' }) | T,
|
|
226
242
|
): T {
|
|
@@ -264,6 +280,11 @@ export class Button extends _Button implements IButton {
|
|
|
264
280
|
|
|
265
281
|
destroy(options?: DestroyOptions) {
|
|
266
282
|
this.onDestroy.emit();
|
|
283
|
+
this._resetPressState();
|
|
284
|
+
if (this._isDownListenerAdded) {
|
|
285
|
+
this.app.ticker.remove(this._handleIsDownCallbacks);
|
|
286
|
+
this._isDownListenerAdded = false;
|
|
287
|
+
}
|
|
267
288
|
super.destroy(options);
|
|
268
289
|
}
|
|
269
290
|
|
|
@@ -290,6 +311,7 @@ export class Button extends _Button implements IButton {
|
|
|
290
311
|
|
|
291
312
|
removeIsDownCallback(callbackId: string) {
|
|
292
313
|
this._isDownCallbacks.delete(callbackId);
|
|
314
|
+
this._checkIsDownCallbacks();
|
|
293
315
|
}
|
|
294
316
|
|
|
295
317
|
setTexture(textureId: ButtonTextureId, texture: TextureAsset) {
|
|
@@ -395,10 +417,20 @@ export class Button extends _Button implements IButton {
|
|
|
395
417
|
this._pointerId = undefined;
|
|
396
418
|
}
|
|
397
419
|
|
|
398
|
-
protected handleClick() {
|
|
420
|
+
protected handleClick(e?: FederatedEvent) {
|
|
399
421
|
if (!this._enabled) {
|
|
400
422
|
return;
|
|
401
423
|
}
|
|
424
|
+
const pointerId = (e as FederatedPointerEvent | undefined)?.pointerId;
|
|
425
|
+
if (pointerId !== undefined && !this.isDown) {
|
|
426
|
+
// a pointer press that was cancelled (e.g. disabled mid-press) must not click
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (e !== undefined && e === this._lastClickEvent) {
|
|
430
|
+
// accessibility dispatches one event instance as both 'click' and 'tap'
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this._lastClickEvent = e;
|
|
402
434
|
this.isDown = false;
|
|
403
435
|
this.onClick.emit();
|
|
404
436
|
if (this.config.sounds?.click) {
|
|
@@ -413,23 +445,23 @@ export class Button extends _Button implements IButton {
|
|
|
413
445
|
* Handles the pointer up event.
|
|
414
446
|
*/
|
|
415
447
|
protected handlePointerUpOutside(e: PointerEvent | FederatedPointerEvent) {
|
|
416
|
-
if (
|
|
448
|
+
if (e.pointerId !== this._pointerId) {
|
|
417
449
|
return;
|
|
418
450
|
}
|
|
419
|
-
|
|
420
|
-
this.
|
|
421
|
-
this.view.texture = this.make.texture({ asset: this.config.textures.default, sheet: this.config.sheet });
|
|
422
|
-
this.isDown = false;
|
|
451
|
+
const wasEnabled = this._enabled;
|
|
452
|
+
this._resetPressState();
|
|
423
453
|
this.isOver = false;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
454
|
+
if (wasEnabled) {
|
|
455
|
+
this.view.texture = this.make.texture({ asset: this.config.textures.default, sheet: this.config.sheet });
|
|
456
|
+
this.onUpOutside.emit();
|
|
457
|
+
}
|
|
427
458
|
}
|
|
428
459
|
|
|
429
460
|
private _doAction(action: ButtonActionOrCallback) {
|
|
430
461
|
if (typeof action === 'function') {
|
|
431
462
|
void action();
|
|
432
463
|
} else {
|
|
464
|
+
action.data ??= {};
|
|
433
465
|
if (!action.data.button) {
|
|
434
466
|
action.data.button = this;
|
|
435
467
|
}
|
|
@@ -442,7 +474,7 @@ export class Button extends _Button implements IButton {
|
|
|
442
474
|
if (!this._isDownListenerAdded && this._isDownCallbacks.size > 0) {
|
|
443
475
|
this._isDownListenerAdded = true;
|
|
444
476
|
this.app.ticker.add(this._handleIsDownCallbacks);
|
|
445
|
-
} else {
|
|
477
|
+
} else if (this._isDownListenerAdded && this._isDownCallbacks.size === 0) {
|
|
446
478
|
this.app.ticker.remove(this._handleIsDownCallbacks);
|
|
447
479
|
this._isDownListenerAdded = false;
|
|
448
480
|
}
|