@caperjs/core 0.2.4 → 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 +23 -10
- 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.map +1 -1
- 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 +12 -0
- package/src/ui/Button.ts +1 -0
- 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
|
@@ -9,6 +9,7 @@ vi.mock('../core/Application', () => ({
|
|
|
9
9
|
Application: { getInstance: () => ({ renderer: { canvas: null } }) },
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
+
import { Signal } from '../signals';
|
|
12
13
|
import { WebEventsPlugin } from './WebEventsPlugin';
|
|
13
14
|
|
|
14
15
|
type FakeVisualViewport = EventTarget & { scale: number };
|
|
@@ -73,3 +74,34 @@ describe('WebEventsPlugin visualViewport resize', () => {
|
|
|
73
74
|
expect(() => plugin.initialize()).not.toThrow();
|
|
74
75
|
});
|
|
75
76
|
});
|
|
77
|
+
|
|
78
|
+
describe('WebEventsPlugin destroy', () => {
|
|
79
|
+
let plugin: WebEventsPlugin;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
plugin = new WebEventsPlugin();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('stops handling orientationchange after destroy', () => {
|
|
86
|
+
const spy = vi.spyOn(plugin as any, '_onOrientationChanged');
|
|
87
|
+
plugin.initialize();
|
|
88
|
+
|
|
89
|
+
window.dispatchEvent(new Event('orientationchange'));
|
|
90
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
91
|
+
|
|
92
|
+
plugin.destroy();
|
|
93
|
+
window.dispatchEvent(new Event('orientationchange'));
|
|
94
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('disconnects tracked signal connections on destroy', () => {
|
|
98
|
+
const signal = new Signal<() => void>();
|
|
99
|
+
const handler = vi.fn();
|
|
100
|
+
plugin.addSignalConnection(signal.connect(handler));
|
|
101
|
+
|
|
102
|
+
plugin.destroy();
|
|
103
|
+
signal.emit();
|
|
104
|
+
|
|
105
|
+
expect(handler).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -61,7 +61,9 @@ export class WebEventsPlugin extends Plugin implements IWebEventsPlugin {
|
|
|
61
61
|
document.removeEventListener('fullscreenchange', this._onResize);
|
|
62
62
|
window.removeEventListener('pagehide', this._onPageHide, false);
|
|
63
63
|
window.removeEventListener('pageshow', this._onPageShow, false);
|
|
64
|
+
window.removeEventListener('orientationchange', this._onOrientationChanged);
|
|
64
65
|
window.visualViewport?.removeEventListener('resize', this._onVisualViewportResize);
|
|
66
|
+
super.destroy();
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
protected getCoreSignals(): string[] {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// ActionsPlugin -> Plugin.ts transitively imports Application → Pixi display graph. Stub it.
|
|
4
|
+
vi.mock('../../core', () => ({
|
|
5
|
+
coreFunctionRegistry: {},
|
|
6
|
+
coreSignalRegistry: {},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../../core/Application', () => ({
|
|
9
|
+
Application: { getInstance: () => ({}) },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import type { IApplication } from '../../core';
|
|
13
|
+
import { ActionsPlugin } from './ActionsPlugin';
|
|
14
|
+
|
|
15
|
+
function makePlugin(actions: Record<string, { context: string | string[] }>) {
|
|
16
|
+
const plugin = new ActionsPlugin();
|
|
17
|
+
plugin.initialize({}, { config: { actions } } as unknown as IApplication);
|
|
18
|
+
return plugin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('ActionsPlugin', () => {
|
|
22
|
+
it('does not dispatch an action whose context merely contains the current context', () => {
|
|
23
|
+
const plugin = makePlugin({ foo: { context: 'popup' } });
|
|
24
|
+
const handler = vi.fn();
|
|
25
|
+
plugin.getAction('foo').connect(handler);
|
|
26
|
+
|
|
27
|
+
plugin.setActionContext('pop');
|
|
28
|
+
plugin.sendAction('foo');
|
|
29
|
+
|
|
30
|
+
expect(handler).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('dispatches an action when the string context matches exactly', () => {
|
|
34
|
+
const plugin = makePlugin({ foo: { context: 'popup' } });
|
|
35
|
+
const handler = vi.fn();
|
|
36
|
+
plugin.getAction('foo').connect(handler);
|
|
37
|
+
|
|
38
|
+
plugin.setActionContext('popup');
|
|
39
|
+
plugin.sendAction('foo');
|
|
40
|
+
|
|
41
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('dispatches an action with a wildcard context in any context', () => {
|
|
45
|
+
const plugin = makePlugin({ foo: { context: '*' } });
|
|
46
|
+
const handler = vi.fn();
|
|
47
|
+
plugin.getAction('foo').connect(handler);
|
|
48
|
+
|
|
49
|
+
plugin.setActionContext('anything');
|
|
50
|
+
plugin.sendAction('foo');
|
|
51
|
+
|
|
52
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('dispatches an action when an array context includes the current context', () => {
|
|
56
|
+
const plugin = makePlugin({ foo: { context: ['menu', 'popup'] } });
|
|
57
|
+
const handler = vi.fn();
|
|
58
|
+
plugin.getAction('foo').connect(handler);
|
|
59
|
+
|
|
60
|
+
plugin.setActionContext('menu');
|
|
61
|
+
plugin.sendAction('foo');
|
|
62
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
63
|
+
|
|
64
|
+
plugin.setActionContext('men');
|
|
65
|
+
plugin.sendAction('foo');
|
|
66
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -80,11 +80,11 @@ export class ActionsPlugin extends Plugin<IActionsPluginOptions> implements IAct
|
|
|
80
80
|
|
|
81
81
|
// check if action is allowed for current context
|
|
82
82
|
// send action if allowed
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
) {
|
|
83
|
+
const actionContext = this._actions[actionId]!.context;
|
|
84
|
+
const allowed = Array.isArray(actionContext)
|
|
85
|
+
? actionContext.includes(this.context)
|
|
86
|
+
: actionContext === this.context || actionContext === '*';
|
|
87
|
+
if (allowed) {
|
|
88
88
|
const detail: ActionDetail<TActionData> = { id: actionId, context: this.context, data };
|
|
89
89
|
this.getAction<TActionData>(actionId).emit(detail);
|
|
90
90
|
// notify automation / observers of the dispatched (allowed) action
|
|
@@ -80,6 +80,25 @@ describe('AudioChannel', () => {
|
|
|
80
80
|
expect(media.volume).toBeCloseTo(0.8 * 0.5 * 1);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
it('destroy() stops/destroys every tracked instance and clears them', () => {
|
|
84
|
+
const manager = createManager();
|
|
85
|
+
const channel = new AudioChannel('sfx', manager);
|
|
86
|
+
|
|
87
|
+
const instance1 = channel.add('boom', new AudioInstance('boom', channel, manager));
|
|
88
|
+
const media1 = createMedia();
|
|
89
|
+
instance1.media = media1 as never;
|
|
90
|
+
|
|
91
|
+
const instance2 = channel.add('splat', new AudioInstance('splat', channel, manager));
|
|
92
|
+
const media2 = createMedia();
|
|
93
|
+
instance2.media = media2 as never;
|
|
94
|
+
|
|
95
|
+
channel.destroy();
|
|
96
|
+
|
|
97
|
+
expect(media1.stop).toHaveBeenCalled();
|
|
98
|
+
expect(media2.stop).toHaveBeenCalled();
|
|
99
|
+
expect(channel.instances).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
83
102
|
it('remove(id) stops all instances tracked under that alias', () => {
|
|
84
103
|
const manager = createManager();
|
|
85
104
|
const channel = new AudioChannel('sfx', manager);
|
|
@@ -160,5 +160,12 @@ export class AudioChannel<C extends ChannelName = ChannelName> {
|
|
|
160
160
|
this.volume = this._volume;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
destroy() {
|
|
163
|
+
destroy() {
|
|
164
|
+
this._sounds.forEach((bucket) => {
|
|
165
|
+
// destroy() -> stop() -> emits onEnd, whose handler may call
|
|
166
|
+
// removeInstance() and mutate `bucket` mid-iteration. Iterate a copy.
|
|
167
|
+
[...bucket].forEach((instance) => instance.destroy());
|
|
168
|
+
});
|
|
169
|
+
this._sounds.clear();
|
|
170
|
+
}
|
|
164
171
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { sound } from '@pixi/sound';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// AudioManagerPlugin -> Plugin.ts transitively imports Application → Pixi display graph. Stub it.
|
|
5
|
+
vi.mock('../../core', () => ({
|
|
6
|
+
coreFunctionRegistry: {},
|
|
7
|
+
coreSignalRegistry: {},
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../core/Application', () => ({
|
|
10
|
+
Application: { getInstance: () => ({}) },
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { AudioManagerPlugin } from './AudioManagerPlugin';
|
|
14
|
+
|
|
15
|
+
describe('AudioManagerPlugin', () => {
|
|
16
|
+
it('destroy() disconnects onChannelMuted along with the other signals', () => {
|
|
17
|
+
const manager = new AudioManagerPlugin();
|
|
18
|
+
const handler = vi.fn();
|
|
19
|
+
manager.onChannelMuted.connect(handler);
|
|
20
|
+
|
|
21
|
+
manager.destroy();
|
|
22
|
+
|
|
23
|
+
manager.onChannelMuted.emit({ channel: {} as never, muted: true });
|
|
24
|
+
expect(handler).not.toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('caches a resolved sound id under the original id so probing is not repeated', () => {
|
|
28
|
+
const exists = vi.spyOn(sound, 'exists').mockImplementation((id: string) => id === 'foo.mp3');
|
|
29
|
+
const manager = new AudioManagerPlugin();
|
|
30
|
+
|
|
31
|
+
const resolved = (manager as any)._verifySoundId('foo');
|
|
32
|
+
expect(resolved).toBe('foo.mp3');
|
|
33
|
+
expect(((manager as any)._idMap as Map<string, string>).get('foo')).toBe('foo.mp3');
|
|
34
|
+
|
|
35
|
+
exists.mockClear();
|
|
36
|
+
expect((manager as any)._verifySoundId('foo')).toBe('foo.mp3');
|
|
37
|
+
expect(exists).not.toHaveBeenCalled();
|
|
38
|
+
|
|
39
|
+
exists.mockRestore();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -272,6 +272,7 @@ export class AudioManagerPlugin<C extends ChannelName = ChannelName> extends Plu
|
|
|
272
272
|
this.onMuted.disconnectAll();
|
|
273
273
|
this.onMasterVolumeChanged.disconnectAll();
|
|
274
274
|
this.onChannelVolumeChanged.disconnectAll();
|
|
275
|
+
this.onChannelMuted.disconnectAll();
|
|
275
276
|
|
|
276
277
|
super.destroy();
|
|
277
278
|
}
|
|
@@ -701,7 +702,7 @@ export class AudioManagerPlugin<C extends ChannelName = ChannelName> extends Plu
|
|
|
701
702
|
}
|
|
702
703
|
for (let id of soundId) {
|
|
703
704
|
if (this._idMap.has(id)) {
|
|
704
|
-
|
|
705
|
+
id = this._idMap.get(id) as string;
|
|
705
706
|
}
|
|
706
707
|
const channel = this._channels.get(channelName);
|
|
707
708
|
if (channel) {
|
|
@@ -795,7 +796,7 @@ export class AudioManagerPlugin<C extends ChannelName = ChannelName> extends Plu
|
|
|
795
796
|
}
|
|
796
797
|
}
|
|
797
798
|
}
|
|
798
|
-
this._idMap.set(
|
|
799
|
+
this._idMap.set(originalSoundId, soundId);
|
|
799
800
|
return soundId;
|
|
800
801
|
}
|
|
801
802
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Assets } from 'pixi.js';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// CaptionsPlugin -> Plugin.ts transitively imports Application → Pixi display graph. Stub it.
|
|
5
|
+
vi.mock('../../core', () => ({
|
|
6
|
+
coreFunctionRegistry: {},
|
|
7
|
+
coreSignalRegistry: {},
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../core/Application', () => ({
|
|
10
|
+
Application: { getInstance: () => ({}) },
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { CaptionsPlugin } from './CaptionsPlugin';
|
|
14
|
+
|
|
15
|
+
describe('CaptionsPlugin', () => {
|
|
16
|
+
it('loadLocale returns early when the locale is already loaded', async () => {
|
|
17
|
+
const load = vi.spyOn(Assets, 'load').mockResolvedValue({});
|
|
18
|
+
const plugin = new CaptionsPlugin();
|
|
19
|
+
(plugin as any)._locale = 'en';
|
|
20
|
+
(plugin as any)._dicts = { en: { hello: 'hello' } };
|
|
21
|
+
(plugin as any)._options = { files: [{ id: 'en', json: 'en.json' }] };
|
|
22
|
+
|
|
23
|
+
await plugin.loadLocale('en');
|
|
24
|
+
|
|
25
|
+
expect(load).not.toHaveBeenCalled();
|
|
26
|
+
load.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('loadLocale still loads the current locale when its dictionary is missing', async () => {
|
|
30
|
+
const load = vi.spyOn(Assets, 'load').mockResolvedValue({ a: 1 });
|
|
31
|
+
const plugin = new CaptionsPlugin();
|
|
32
|
+
(plugin as any)._locale = 'en';
|
|
33
|
+
(plugin as any)._options = { files: [{ id: 'en', json: 'en.json' }] };
|
|
34
|
+
|
|
35
|
+
await plugin.loadLocale('en');
|
|
36
|
+
|
|
37
|
+
expect(load).toHaveBeenCalledWith('en.json');
|
|
38
|
+
load.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -327,7 +327,7 @@ export class CaptionsPlugin extends Plugin<CaptionsOptions> implements ICaptions
|
|
|
327
327
|
* @returns Promise<void>
|
|
328
328
|
*/
|
|
329
329
|
async loadLocale(localeId: string) {
|
|
330
|
-
if (this.
|
|
330
|
+
if (this._dicts[localeId]) {
|
|
331
331
|
return;
|
|
332
332
|
}
|
|
333
333
|
const file = this._options.files.find((file) => localeId === file.id);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockApp = vi.hoisted(() => ({
|
|
4
|
+
config: { focus: {} },
|
|
5
|
+
renderer: {
|
|
6
|
+
accessibility: {
|
|
7
|
+
isActive: false,
|
|
8
|
+
destroy: vi.fn(),
|
|
9
|
+
postrender: vi.fn(),
|
|
10
|
+
_deactivate: vi.fn(),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
scenes: { onSceneChangeStart: { connect: vi.fn() } },
|
|
14
|
+
ticker: { addOnce: vi.fn(), add: vi.fn(), remove: vi.fn() },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// FocusManagerPlugin transitively imports Application → Pixi display graph. Stub it.
|
|
18
|
+
vi.mock('../../core', () => ({
|
|
19
|
+
coreFunctionRegistry: {},
|
|
20
|
+
coreSignalRegistry: {},
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('../../core/Application', () => ({
|
|
23
|
+
Application: { getInstance: () => mockApp },
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { FocusManagerPlugin } from './FocusManagerPlugin';
|
|
27
|
+
|
|
28
|
+
describe('FocusManagerPlugin destroy', () => {
|
|
29
|
+
let plugin: FocusManagerPlugin;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
plugin = new FocusManagerPlugin();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('stops handling window keydown after destroy', () => {
|
|
36
|
+
const spy = vi.spyOn(plugin as any, '_onKeyDown');
|
|
37
|
+
plugin.initialize({}, mockApp as never);
|
|
38
|
+
|
|
39
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
|
40
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
41
|
+
|
|
42
|
+
plugin.destroy();
|
|
43
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
|
44
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('stops handling window keyup after destroy', () => {
|
|
48
|
+
const spy = vi.spyOn(plugin as any, '_onKeyUp');
|
|
49
|
+
plugin.initialize({}, mockApp as never);
|
|
50
|
+
|
|
51
|
+
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
|
52
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
53
|
+
|
|
54
|
+
plugin.destroy();
|
|
55
|
+
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
|
56
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('stops handling the capture-phase document mousemove after destroy', () => {
|
|
60
|
+
const spy = vi.spyOn(plugin as any, '_onMouseMove');
|
|
61
|
+
plugin.initialize({}, mockApp as never);
|
|
62
|
+
(plugin as any)._activate();
|
|
63
|
+
|
|
64
|
+
document.dispatchEvent(new MouseEvent('mousemove'));
|
|
65
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
66
|
+
|
|
67
|
+
plugin.destroy();
|
|
68
|
+
document.dispatchEvent(new MouseEvent('mousemove'));
|
|
69
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -734,6 +734,13 @@ export class FocusManagerPlugin extends Plugin<FocusManagerPluginOptions> implem
|
|
|
734
734
|
private _removeGlobalListeners() {
|
|
735
735
|
globalThis.document.removeEventListener('mousemove', this._handleGlobalMouseMove);
|
|
736
736
|
globalThis.document.removeEventListener('pointerdown', this._handleGlobalPointerDown);
|
|
737
|
+
// keydown is added by _setupKeyboardListeners (window) and again, when pixi
|
|
738
|
+
// accessibility is off, by _updatePixiAccessibility (globalThis)
|
|
739
|
+
window.removeEventListener('keydown', this._onKeyDown, false);
|
|
740
|
+
globalThis.removeEventListener('keydown', this._onKeyDown, false);
|
|
741
|
+
globalThis.removeEventListener('keyup', this._onKeyUp, false);
|
|
742
|
+
// capture-phase listener added by _activate()
|
|
743
|
+
globalThis.document.removeEventListener('mousemove', this._onMouseMove, true);
|
|
737
744
|
}
|
|
738
745
|
|
|
739
746
|
private _handleGlobalMouseMove() {
|
|
@@ -12,4 +12,10 @@ export class AbstractControls {
|
|
|
12
12
|
initialize(scheme: Partial<ControlsActionMap>) {
|
|
13
13
|
this.scheme = scheme;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Terminates the destroy chain — subclasses are built on WithSignals, whose destroy()
|
|
18
|
+
* calls super.destroy().
|
|
19
|
+
*/
|
|
20
|
+
destroy(): void {}
|
|
15
21
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Signal } from '../../signals';
|
|
4
|
+
|
|
5
|
+
// Controls → KeyboardControls / VirtualControls reach for the Application singleton.
|
|
6
|
+
const mocks = vi.hoisted(() => ({ app: null as any }));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../core/Application', () => ({
|
|
9
|
+
Application: { getInstance: () => mocks.app },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import type { IButton } from '../../ui';
|
|
13
|
+
import { Controls } from './Controls';
|
|
14
|
+
|
|
15
|
+
type FakeApp = {
|
|
16
|
+
ticker: { add: ReturnType<typeof vi.fn>; remove: ReturnType<typeof vi.fn> };
|
|
17
|
+
actionContext: string;
|
|
18
|
+
actionsPlugin: { getActions: () => Record<string, { context: string; input: string[] }> };
|
|
19
|
+
signal: { onActionContextChanged: Signal<() => void> };
|
|
20
|
+
keyboard: {
|
|
21
|
+
keysDown: Set<string>;
|
|
22
|
+
onKeyDown: () => Signal<(detail: any) => void>;
|
|
23
|
+
onKeyUp: () => Signal<(detail: any) => void>;
|
|
24
|
+
};
|
|
25
|
+
action: ReturnType<typeof vi.fn>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let app: FakeApp;
|
|
29
|
+
let keyDown: Signal<(detail: any) => void>;
|
|
30
|
+
let keyUp: Signal<(detail: any) => void>;
|
|
31
|
+
let actions: Record<string, { context: string; input: string[] }>;
|
|
32
|
+
|
|
33
|
+
function pressKey(key: string) {
|
|
34
|
+
app.keyboard.keysDown.add(key);
|
|
35
|
+
keyDown.emit({ event: { key }, key });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeButton(id: string): IButton {
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
onDown: new Signal<() => void>(),
|
|
42
|
+
onUp: new Signal<() => void>(),
|
|
43
|
+
onUpOutside: new Signal<() => void>(),
|
|
44
|
+
onDestroy: new Signal<() => void>(),
|
|
45
|
+
} as unknown as IButton;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
keyDown = new Signal();
|
|
50
|
+
keyUp = new Signal();
|
|
51
|
+
actions = {
|
|
52
|
+
move_left: { context: '*', input: [] },
|
|
53
|
+
jump: { context: '*', input: [] },
|
|
54
|
+
};
|
|
55
|
+
app = {
|
|
56
|
+
ticker: { add: vi.fn(), remove: vi.fn() },
|
|
57
|
+
actionContext: 'default',
|
|
58
|
+
actionsPlugin: { getActions: () => actions },
|
|
59
|
+
signal: { onActionContextChanged: new Signal<() => void>() },
|
|
60
|
+
keyboard: {
|
|
61
|
+
keysDown: new Set<string>(),
|
|
62
|
+
onKeyDown: () => keyDown,
|
|
63
|
+
onKeyUp: () => keyUp,
|
|
64
|
+
},
|
|
65
|
+
action: vi.fn(),
|
|
66
|
+
};
|
|
67
|
+
mocks.app = app;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('Controls teardown', () => {
|
|
71
|
+
it('destroys configured control schemes without throwing and removes ticker callbacks', () => {
|
|
72
|
+
const controls = new Controls();
|
|
73
|
+
controls.initialize({
|
|
74
|
+
keyboard: { down: { move_left: 'A' } },
|
|
75
|
+
touch: { down: { jump: 'btn-a' } },
|
|
76
|
+
} as any);
|
|
77
|
+
controls.connect();
|
|
78
|
+
|
|
79
|
+
expect(app.ticker.add).toHaveBeenCalledTimes(2);
|
|
80
|
+
expect(() => controls.destroy()).not.toThrow();
|
|
81
|
+
expect(app.ticker.remove).toHaveBeenCalledTimes(2);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Controls combinations', () => {
|
|
86
|
+
it('reports a keyboard combination active when both keys are down', () => {
|
|
87
|
+
const controls = new Controls();
|
|
88
|
+
controls.initialize({ keyboard: { down: { move_left: ['A+B'] } } } as any);
|
|
89
|
+
controls.connect();
|
|
90
|
+
|
|
91
|
+
expect(controls.isActionActive('move_left')).toBe(false);
|
|
92
|
+
|
|
93
|
+
pressKey('A');
|
|
94
|
+
expect(controls.isActionActive('move_left')).toBe(false);
|
|
95
|
+
|
|
96
|
+
pressKey('B');
|
|
97
|
+
expect(controls.isActionActive('move_left')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('reports a virtual button combination active when both buttons are down', () => {
|
|
101
|
+
const controls = new Controls();
|
|
102
|
+
controls.initialize({ touch: { down: { jump: ['btn-a+btn-b'] } } } as any);
|
|
103
|
+
controls.connect();
|
|
104
|
+
|
|
105
|
+
const a = makeButton('btn-a');
|
|
106
|
+
const b = makeButton('btn-b');
|
|
107
|
+
controls.virtual.addButton(a);
|
|
108
|
+
controls.virtual.addButton(b);
|
|
109
|
+
|
|
110
|
+
a.onDown.emit();
|
|
111
|
+
expect(controls.isActionActive('jump')).toBe(false);
|
|
112
|
+
|
|
113
|
+
b.onDown.emit();
|
|
114
|
+
expect(controls.isActionActive('jump')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Controls with an unmapped action in the scheme', () => {
|
|
119
|
+
it('does not throw on initialize or on an action context change', () => {
|
|
120
|
+
const controls = new Controls();
|
|
121
|
+
|
|
122
|
+
expect(() =>
|
|
123
|
+
controls.initialize({
|
|
124
|
+
keyboard: { down: { not_an_action: 'A' } },
|
|
125
|
+
touch: { down: { not_an_action: 'btn-a' }, up: { also_not_an_action: 'btn-b' } },
|
|
126
|
+
} as any),
|
|
127
|
+
).not.toThrow();
|
|
128
|
+
|
|
129
|
+
controls.connect();
|
|
130
|
+
|
|
131
|
+
expect(() => app.signal.onActionContextChanged.emit()).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('VirtualControls.removeButton', () => {
|
|
136
|
+
it('stops a removed button from driving action state', () => {
|
|
137
|
+
const controls = new Controls();
|
|
138
|
+
controls.initialize({ touch: { down: { jump: 'btn-a' } } } as any);
|
|
139
|
+
controls.connect();
|
|
140
|
+
|
|
141
|
+
const button = makeButton('btn-a');
|
|
142
|
+
controls.virtual.addButton(button);
|
|
143
|
+
controls.virtual.removeButton(button);
|
|
144
|
+
|
|
145
|
+
button.onDown.emit();
|
|
146
|
+
|
|
147
|
+
expect(controls.isActionActive('jump')).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -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
|
+
});
|