@caperjs/core 0.2.4 → 0.4.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.
Files changed (208) hide show
  1. package/build/assetpack.mjs +90 -0
  2. package/build/index.mjs +3 -1
  3. package/build/internal/discovery.mjs +1 -1
  4. package/build/internal/manifest.mjs +8 -3
  5. package/build/internal/validate.mjs +3 -1
  6. package/build/plugins/assetTypes.mjs +4 -4
  7. package/build/plugins/caperConfig.mjs +12 -5
  8. package/build/plugins/lists.mjs +29 -9
  9. package/build/plugins/pruneFallbacks.mjs +25 -2
  10. package/cli/create.mjs +57 -6
  11. package/cli/create.test.mjs +36 -0
  12. package/cli/update.mjs +1 -1
  13. package/lib/{CaptionsPlugin-WPAcfAkx.js → CaptionsPlugin-cSJj-R9M.js} +5 -5
  14. package/lib/{CaptionsPlugin-WPAcfAkx.js.map → CaptionsPlugin-cSJj-R9M.js.map} +1 -1
  15. package/lib/{DataAdapter-DS-twowe.js → DataAdapter-D2tc6hx9.js} +15 -10
  16. package/lib/DataAdapter-D2tc6hx9.js.map +1 -0
  17. package/lib/{DebugRenderer-D1ryrRYx.js → DebugRenderer-CGUKy6Fy.js} +2 -2
  18. package/lib/{DebugRenderer-D1ryrRYx.js.map → DebugRenderer-CGUKy6Fy.js.map} +1 -1
  19. package/lib/{DevToolsPlugin-DH7zX7HU.js → DevToolsPlugin-B8GX3H9b.js} +2 -2
  20. package/lib/{DevToolsPlugin-DH7zX7HU.js.map → DevToolsPlugin-B8GX3H9b.js.map} +1 -1
  21. package/lib/{GSAPPlugin-BBtRjUqe.js → GSAPPlugin-Br9AjuTD.js} +6 -6
  22. package/lib/{GSAPPlugin-BBtRjUqe.js.map → GSAPPlugin-Br9AjuTD.js.map} +1 -1
  23. package/lib/{LayoutPlugin-Co6vcoV4.js → LayoutPlugin-BX3OP7pw.js} +4 -4
  24. package/lib/{LayoutPlugin-Co6vcoV4.js.map → LayoutPlugin-BX3OP7pw.js.map} +1 -1
  25. package/lib/{SpinePlugin-uN641Hmk.js → SpinePlugin-C-JDdXpf.js} +690 -793
  26. package/lib/SpinePlugin-C-JDdXpf.js.map +1 -0
  27. package/lib/{StatsPlugin-vZQRm6wh.js → StatsPlugin-yJlJtoQ2.js} +7 -7
  28. package/lib/StatsPlugin-yJlJtoQ2.js.map +1 -0
  29. package/lib/{VoiceOverPlugin-KlZdGj1G.js → VoiceOverPlugin-DjSZp4Ko.js} +7 -6
  30. package/lib/VoiceOverPlugin-DjSZp4Ko.js.map +1 -0
  31. package/lib/caper.mjs +159 -1917
  32. package/lib/caper.mjs.map +1 -1
  33. package/lib/const-C3sxeYhl.js +1746 -0
  34. package/lib/const-C3sxeYhl.js.map +1 -0
  35. package/lib/core/Application.d.ts +8 -3
  36. package/lib/core/Application.d.ts.map +1 -1
  37. package/lib/core/Application.test.d.ts +2 -0
  38. package/lib/core/Application.test.d.ts.map +1 -0
  39. package/lib/core/config.d.ts +6 -1
  40. package/lib/core/config.d.ts.map +1 -1
  41. package/lib/core/config.test.d.ts +2 -0
  42. package/lib/core/config.test.d.ts.map +1 -0
  43. package/lib/core/create.d.ts.map +1 -1
  44. package/lib/core/interfaces/IApplication.d.ts +6 -6
  45. package/lib/core/interfaces/IApplication.d.ts.map +1 -1
  46. package/lib/core/interfaces/ICoreSignals.d.ts +2 -1
  47. package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
  48. package/lib/display/Camera.test.d.ts +2 -0
  49. package/lib/display/Camera.test.d.ts.map +1 -0
  50. package/lib/display/Entity.d.ts +2 -2
  51. package/lib/display/SceneTransition.test.d.ts +1 -0
  52. package/lib/display/SceneTransition.test.d.ts.map +1 -0
  53. package/lib/display/SpineAnimation.d.ts +3 -3
  54. package/lib/mixins/factory/methods.d.ts.map +1 -1
  55. package/lib/padding-1kHDpGlQ.js +41 -0
  56. package/lib/padding-1kHDpGlQ.js.map +1 -0
  57. package/lib/plugins/AssetsPlugin.d.ts.map +1 -1
  58. package/lib/plugins/AssetsPlugin.test.d.ts +2 -0
  59. package/lib/plugins/AssetsPlugin.test.d.ts.map +1 -0
  60. package/lib/plugins/DataAdapter.d.ts.map +1 -1
  61. package/lib/plugins/DataAdapter.test.d.ts +2 -0
  62. package/lib/plugins/DataAdapter.test.d.ts.map +1 -0
  63. package/lib/plugins/FullScreenPlugin.d.ts +0 -5
  64. package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
  65. package/lib/plugins/FullScreenPlugin.test.d.ts +2 -0
  66. package/lib/plugins/FullScreenPlugin.test.d.ts.map +1 -0
  67. package/lib/plugins/GSAPPlugin.d.ts +0 -2
  68. package/lib/plugins/GSAPPlugin.d.ts.map +1 -1
  69. package/lib/plugins/GSAPPlugin.test.d.ts +2 -0
  70. package/lib/plugins/GSAPPlugin.test.d.ts.map +1 -0
  71. package/lib/plugins/KeyboardPlugin.d.ts +0 -1
  72. package/lib/plugins/KeyboardPlugin.d.ts.map +1 -1
  73. package/lib/plugins/LookupPlugin.d.ts +6 -1
  74. package/lib/plugins/LookupPlugin.d.ts.map +1 -1
  75. package/lib/plugins/LookupPlugin.test.d.ts +2 -0
  76. package/lib/plugins/LookupPlugin.test.d.ts.map +1 -0
  77. package/lib/plugins/Plugin.d.ts +101 -2
  78. package/lib/plugins/Plugin.d.ts.map +1 -1
  79. package/lib/plugins/PopupManagerPlugin.d.ts.map +1 -1
  80. package/lib/plugins/PopupManagerPlugin.test.d.ts +1 -0
  81. package/lib/plugins/PopupManagerPlugin.test.d.ts.map +1 -0
  82. package/lib/plugins/SceneManagerPlugin.d.ts +1 -1
  83. package/lib/plugins/SceneManagerPlugin.d.ts.map +1 -1
  84. package/lib/plugins/SceneManagerPlugin.test.d.ts +2 -0
  85. package/lib/plugins/SceneManagerPlugin.test.d.ts.map +1 -0
  86. package/lib/plugins/StatsPlugin.d.ts.map +1 -1
  87. package/lib/plugins/TimerPlugin.d.ts.map +1 -1
  88. package/lib/plugins/WebEventsPlugin.d.ts +0 -1
  89. package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
  90. package/lib/plugins/actions/ActionsPlugin.test.d.ts +2 -0
  91. package/lib/plugins/actions/ActionsPlugin.test.d.ts.map +1 -0
  92. package/lib/plugins/audio/AudioChannel.d.ts.map +1 -1
  93. package/lib/plugins/audio/AudioManagerPlugin.d.ts.map +1 -1
  94. package/lib/plugins/audio/AudioManagerPlugin.test.d.ts +2 -0
  95. package/lib/plugins/audio/AudioManagerPlugin.test.d.ts.map +1 -0
  96. package/lib/plugins/audio/VoiceOverPlugin.d.ts.map +1 -1
  97. package/lib/plugins/breakpoints/BreakpointPlugin.d.ts.map +1 -1
  98. package/lib/plugins/captions/CaptionsPlugin.test.d.ts +2 -0
  99. package/lib/plugins/captions/CaptionsPlugin.test.d.ts.map +1 -0
  100. package/lib/plugins/focus/FocusManagerPlugin.d.ts.map +1 -1
  101. package/lib/plugins/focus/FocusManagerPlugin.test.d.ts +2 -0
  102. package/lib/plugins/focus/FocusManagerPlugin.test.d.ts.map +1 -0
  103. package/lib/plugins/gesture/GesturePlugin.d.ts.map +1 -1
  104. package/lib/plugins/input/AbstractControls.d.ts +47 -3
  105. package/lib/plugins/input/AbstractControls.d.ts.map +1 -1
  106. package/lib/plugins/input/Controls.test.d.ts +2 -0
  107. package/lib/plugins/input/Controls.test.d.ts.map +1 -0
  108. package/lib/plugins/input/InputPlugin.d.ts.map +1 -1
  109. package/lib/plugins/input/controlsCore.d.ts +58 -0
  110. package/lib/plugins/input/controlsCore.d.ts.map +1 -0
  111. package/lib/plugins/input/controlsCore.test.d.ts +2 -0
  112. package/lib/plugins/input/controlsCore.test.d.ts.map +1 -0
  113. package/lib/plugins/input/keyboard/KeyboardControls.d.ts +15 -0
  114. package/lib/plugins/input/keyboard/KeyboardControls.d.ts.map +1 -0
  115. package/lib/plugins/input/keyboard/index.d.ts +1 -1
  116. package/lib/plugins/input/keyboard/index.d.ts.map +1 -1
  117. package/lib/plugins/input/touch/VirtualControls.d.ts +8 -14
  118. package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
  119. package/lib/plugins/input/touch/VirtualControls.test.d.ts +2 -0
  120. package/lib/plugins/input/touch/VirtualControls.test.d.ts.map +1 -0
  121. package/lib/{registries-BLrxALZ0.js → registries-BfV-VF7a.js} +665 -597
  122. package/lib/registries-BfV-VF7a.js.map +1 -0
  123. package/lib/store/Store.d.ts.map +1 -1
  124. package/lib/ui/Button.d.ts.map +1 -1
  125. package/lib/ui/UICanvas.d.ts +10 -1
  126. package/lib/ui/UICanvas.d.ts.map +1 -1
  127. package/lib/ui/UICanvas.test.d.ts +0 -1
  128. package/lib/ui/UICanvas.test.d.ts.map +1 -1
  129. package/lib/utils/color.d.ts.map +1 -1
  130. package/lib/utils/color.test.d.ts +2 -0
  131. package/lib/utils/color.test.d.ts.map +1 -0
  132. package/package.json +1 -1
  133. package/src/core/Application.test.ts +168 -0
  134. package/src/core/Application.ts +37 -27
  135. package/src/core/config.test.ts +79 -0
  136. package/src/core/config.ts +28 -6
  137. package/src/core/create.ts +11 -0
  138. package/src/core/interfaces/IApplication.ts +6 -6
  139. package/src/core/interfaces/ICoreSignals.ts +2 -0
  140. package/src/display/Camera.test.ts +40 -0
  141. package/src/display/Camera.ts +3 -3
  142. package/src/display/Entity.ts +2 -2
  143. package/src/display/SceneTransition.test.ts +58 -0
  144. package/src/display/SceneTransition.ts +1 -1
  145. package/src/display/SpineAnimation.ts +4 -4
  146. package/src/mixins/factory/methods.ts +0 -1
  147. package/src/plugins/AssetsPlugin.test.ts +48 -0
  148. package/src/plugins/AssetsPlugin.ts +3 -1
  149. package/src/plugins/DataAdapter.test.ts +32 -0
  150. package/src/plugins/DataAdapter.ts +5 -1
  151. package/src/plugins/FullScreenPlugin.test.ts +48 -0
  152. package/src/plugins/FullScreenPlugin.ts +9 -18
  153. package/src/plugins/GSAPPlugin.test.ts +31 -0
  154. package/src/plugins/GSAPPlugin.ts +1 -4
  155. package/src/plugins/KeyboardPlugin.ts +3 -10
  156. package/src/plugins/LookupPlugin.test.ts +42 -0
  157. package/src/plugins/LookupPlugin.ts +15 -3
  158. package/src/plugins/Plugin.test.ts +130 -2
  159. package/src/plugins/Plugin.ts +159 -3
  160. package/src/plugins/PopupManagerPlugin.test.ts +53 -0
  161. package/src/plugins/PopupManagerPlugin.ts +5 -3
  162. package/src/plugins/SceneManagerPlugin.test.ts +76 -0
  163. package/src/plugins/SceneManagerPlugin.ts +18 -13
  164. package/src/plugins/StatsPlugin.ts +3 -2
  165. package/src/plugins/TimerPlugin.ts +1 -2
  166. package/src/plugins/WebEventsPlugin.test.ts +32 -0
  167. package/src/plugins/WebEventsPlugin.ts +9 -16
  168. package/src/plugins/actions/ActionsPlugin.test.ts +68 -0
  169. package/src/plugins/actions/ActionsPlugin.ts +5 -5
  170. package/src/plugins/audio/AudioChannel.test.ts +19 -0
  171. package/src/plugins/audio/AudioChannel.ts +8 -1
  172. package/src/plugins/audio/AudioManagerPlugin.test.ts +41 -0
  173. package/src/plugins/audio/AudioManagerPlugin.ts +3 -2
  174. package/src/plugins/audio/VoiceOverPlugin.ts +1 -0
  175. package/src/plugins/breakpoints/BreakpointPlugin.ts +1 -2
  176. package/src/plugins/captions/CaptionsPlugin.test.ts +40 -0
  177. package/src/plugins/captions/CaptionsPlugin.ts +2 -2
  178. package/src/plugins/focus/FocusManagerPlugin.test.ts +71 -0
  179. package/src/plugins/focus/FocusManagerPlugin.ts +7 -0
  180. package/src/plugins/gesture/GesturePlugin.ts +4 -8
  181. package/src/plugins/input/AbstractControls.ts +104 -3
  182. package/src/plugins/input/Controls.test.ts +205 -0
  183. package/src/plugins/input/InputPlugin.ts +6 -14
  184. package/src/plugins/input/controlsCore.test.ts +151 -0
  185. package/src/plugins/input/controlsCore.ts +162 -0
  186. package/src/plugins/input/keyboard/KeyboardControls.ts +84 -0
  187. package/src/plugins/input/keyboard/index.ts +1 -1
  188. package/src/plugins/input/touch/VirtualControls.test.ts +38 -0
  189. package/src/plugins/input/touch/VirtualControls.ts +53 -120
  190. package/src/plugins/spine/SpinePlugin.ts +2 -2
  191. package/src/store/Store.test.ts +15 -0
  192. package/src/store/Store.ts +4 -1
  193. package/src/ui/Button.test.ts +12 -0
  194. package/src/ui/Button.ts +1 -0
  195. package/src/ui/UICanvas.test.ts +109 -6
  196. package/src/ui/UICanvas.ts +41 -3
  197. package/src/utils/color.test.ts +16 -0
  198. package/src/utils/color.ts +2 -1
  199. package/src/utils/platform.ts +1 -1
  200. package/src/version.ts +1 -1
  201. package/lib/DataAdapter-DS-twowe.js.map +0 -1
  202. package/lib/SpinePlugin-uN641Hmk.js.map +0 -1
  203. package/lib/StatsPlugin-vZQRm6wh.js.map +0 -1
  204. package/lib/VoiceOverPlugin-KlZdGj1G.js.map +0 -1
  205. package/lib/plugins/input/keyboard/KeyboardContols.d.ts +0 -28
  206. package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +0 -1
  207. package/lib/registries-BLrxALZ0.js.map +0 -1
  208. package/src/plugins/input/keyboard/KeyboardContols.ts +0 -182
@@ -44,24 +44,17 @@ export class WebEventsPlugin extends Plugin implements IWebEventsPlugin {
44
44
  }
45
45
 
46
46
  public initialize(): void {
47
- document.addEventListener('visibilitychange', this._onVisibilityChanged, false);
48
- window.addEventListener('pagehide', this._onPageHide, false);
49
- window.addEventListener('pageshow', this._onPageShow, false);
50
- window.addEventListener('resize', this._onResize);
51
- document.addEventListener('fullscreenchange', this._onResize);
52
- window.addEventListener('orientationchange', this._onOrientationChanged);
47
+ this.listen(document, 'visibilitychange', this._onVisibilityChanged, false);
48
+ this.listen(window, 'pagehide', this._onPageHide, false);
49
+ this.listen(window, 'pageshow', this._onPageShow, false);
50
+ this.listen(window, 'resize', this._onResize);
51
+ this.listen(document, 'fullscreenchange', this._onResize);
52
+ this.listen(window, 'orientationchange', this._onOrientationChanged);
53
53
  // installed PWAs (standalone/fullscreen display modes) can miss or mistime window.resize
54
54
  // during launch transitions; visualViewport reports the settled size
55
- window.visualViewport?.addEventListener('resize', this._onVisualViewportResize);
56
- }
57
-
58
- public destroy() {
59
- document.removeEventListener('visibilitychange', this._onVisibilityChanged, false);
60
- window.removeEventListener('resize', this._onResize);
61
- document.removeEventListener('fullscreenchange', this._onResize);
62
- window.removeEventListener('pagehide', this._onPageHide, false);
63
- window.removeEventListener('pageshow', this._onPageShow, false);
64
- window.visualViewport?.removeEventListener('resize', this._onVisualViewportResize);
55
+ if (window.visualViewport) {
56
+ this.listen(window.visualViewport, 'resize', this._onVisualViewportResize);
57
+ }
65
58
  }
66
59
 
67
60
  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
- if (
84
- this._actions[actionId]?.context === '*' ||
85
- this._actions[actionId]?.context === this.context ||
86
- this._actions[actionId]?.context?.includes(this.context)
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
- soundId = this._idMap.get(id) as string;
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(soundId, soundId);
799
+ this._idMap.set(originalSoundId, soundId);
799
800
  return soundId;
800
801
  }
801
802
 
@@ -1,3 +1,4 @@
1
+ import { gsap } from 'gsap';
1
2
  import { Signal } from '../../signals';
2
3
  import { Logger } from '../../utils';
3
4
  import type { IPlugin } from '../Plugin';
@@ -150,7 +150,6 @@ export class BreakpointPlugin extends Plugin<BreakpointPluginOptions> implements
150
150
  }
151
151
 
152
152
  public destroy(): void {
153
- this._pointerQuery?.removeEventListener('change', this._onPointerChange);
154
153
  this._pointerQuery = null;
155
154
  for (const signal of this._enter.values()) signal.disconnectAll();
156
155
  for (const signal of this._leave.values()) signal.disconnectAll();
@@ -259,7 +258,7 @@ export class BreakpointPlugin extends Plugin<BreakpointPluginOptions> implements
259
258
  }
260
259
  this._pointerQuery = window.matchMedia('(pointer: coarse)');
261
260
  this._pointer = this._pointerQuery.matches ? 'coarse' : 'fine';
262
- this._pointerQuery.addEventListener('change', this._onPointerChange);
261
+ this.listen(this._pointerQuery, 'change', this._onPointerChange as EventListener);
263
262
  }
264
263
 
265
264
  private _signal(store: Map<string, Signal<() => void>>, name: string): Signal<() => void> {
@@ -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
+ });
@@ -298,7 +298,7 @@ export class CaptionsPlugin extends Plugin<CaptionsOptions> implements ICaptions
298
298
  const RendererClass: CaptionRendererConstructor = this.options.renderer;
299
299
  this.renderer = this.view.addChild(new RendererClass(this)) as CaptionsRenderer;
300
300
 
301
- this.app.ticker.add(this.update);
301
+ this.addTickerCallback(this.update);
302
302
  this.app.scenes.onSceneChangeStart.connect(this.stopAllCaptions);
303
303
  }
304
304
 
@@ -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._locale === 'localeId') {
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() {
@@ -74,23 +74,19 @@ export class GesturePlugin extends Plugin<GesturePluginOptions> implements IGest
74
74
  canvas.style.touchAction = 'none';
75
75
  }
76
76
 
77
- canvas.addEventListener('pointerdown', this._onPointerDown);
78
- window.addEventListener('pointermove', this._onPointerMove);
79
- window.addEventListener('pointerup', this._onPointerUp);
80
- window.addEventListener('pointercancel', this._onPointerCancel);
77
+ this.listen(canvas, 'pointerdown', this._onPointerDown as EventListener);
78
+ this.listen(window, 'pointermove', this._onPointerMove as EventListener);
79
+ this.listen(window, 'pointerup', this._onPointerUp as EventListener);
80
+ this.listen(window, 'pointercancel', this._onPointerCancel as EventListener);
81
81
  }
82
82
 
83
83
  public destroy(): void {
84
84
  if (this._canvas) {
85
- this._canvas.removeEventListener('pointerdown', this._onPointerDown);
86
85
  if (this._options.preventDefault) {
87
86
  this._canvas.style.touchAction = this._previousTouchAction;
88
87
  }
89
88
  this._canvas = null;
90
89
  }
91
- window.removeEventListener('pointermove', this._onPointerMove);
92
- window.removeEventListener('pointerup', this._onPointerUp);
93
- window.removeEventListener('pointercancel', this._onPointerCancel);
94
90
 
95
91
  this._pointers.clear();
96
92
  this._state = 'idle';
@@ -1,15 +1,116 @@
1
1
  import type { ControlsActionMap } from '../..';
2
- import { IApplication } from '../../core';
3
2
  import { Application } from '../../core/Application';
3
+ import { WithSignals } from '../../mixins/signals';
4
+ import { Logger } from '../../utils';
5
+ import type { Action } from '../actions';
6
+ import { buildDownMaps, buildUpMap, isInputActive, type SchemeSection } from './controlsCore';
4
7
 
5
- export class AbstractControls {
8
+ /**
9
+ * Terminates the destroy chain — WithSignals' destroy() calls super.destroy().
10
+ * Also the binding root: `bindAllMethods` in an adapter binds everything down
11
+ * to here and stops, exactly like `Application` / `Plugin` / `Container`.
12
+ */
13
+ class ControlsRoot {
14
+ private static readonly __caper_method_binding_root = true;
15
+
16
+ destroy(): void {}
17
+ }
18
+
19
+ /**
20
+ * Shared machinery for every controls adapter: scheme storage, context-gated
21
+ * down/up map building (rebuilt whenever the action context changes), the
22
+ * `isActionActive` query and the ticker hookup. Adapters supply the scheme
23
+ * sections, the "is this input down" predicates and the dispatch loop.
24
+ */
25
+ export abstract class AbstractControls extends WithSignals(ControlsRoot) {
6
26
  protected scheme: any;
27
+ /** `+` combinations, longest first — the update loop relies on that order. */
28
+ protected combinations: string[][] = [];
29
+ protected combinationsMap: Map<string[], Action> = new Map();
30
+ protected activeDownInputs: Map<string, Action> = new Map();
31
+ protected activeUpInputs: Map<string, Action> = new Map();
32
+ private _warnedMissingActions: Set<string> = new Set();
7
33
 
8
- get app(): IApplication {
34
+ get app() {
9
35
  return Application.getInstance();
10
36
  }
11
37
 
12
38
  initialize(scheme: Partial<ControlsActionMap>) {
13
39
  this.scheme = scheme;
40
+ this.addSignalConnection(this.app.signal.onActionContextChanged.connect(this._sortActions));
41
+ this._sortActions();
42
+ }
43
+
44
+ public connect(): void {
45
+ this.app.ticker.add(this._update);
46
+ }
47
+
48
+ public destroy(): void {
49
+ this.app.ticker.remove(this._update);
50
+ super.destroy();
51
+ }
52
+
53
+ isActionActive(action: Action): boolean {
54
+ const input = this.scheme?.down?.[action] ?? null;
55
+ if (!input) {
56
+ return false;
57
+ }
58
+ if (Array.isArray(input)) {
59
+ return input.some((item: string) => this._isInputActive(item));
60
+ }
61
+ return this._isInputActive(input);
62
+ }
63
+
64
+ /** The `down` section of the adapter's scheme. */
65
+ protected get downSection(): SchemeSection | undefined {
66
+ return this.scheme?.down;
67
+ }
68
+
69
+ /** The `up` section of the adapter's scheme. */
70
+ protected get upSection(): SchemeSection | undefined {
71
+ return this.scheme?.up;
72
+ }
73
+
74
+ /** Prefix for scheme warnings — a field, so minification cannot rename it away. */
75
+ protected abstract readonly logLabel: string;
76
+
77
+ /** Whether a single input is currently down. */
78
+ protected abstract isInputDown(id: string): boolean;
79
+
80
+ /** Whether an input counts as down *as part of a combination*. */
81
+ protected isComboPartDown(id: string): boolean {
82
+ return this.isInputDown(id);
83
+ }
84
+
85
+ /** Dispatch actions for the current input state — added to the ticker by {@link connect}. */
86
+ protected abstract _update(): void;
87
+
88
+ protected _isInputActive(input: string): boolean {
89
+ return isInputActive(input, this.isInputDown, this.isComboPartDown);
90
+ }
91
+
92
+ /** Rebuild the down/up maps for the current action context. */
93
+ protected _sortActions(): void {
94
+ const actions = this.app.actionsPlugin.getActions();
95
+ const context = this.app.actionContext as string;
96
+
97
+ const { combinations, combinationsMap, singles } = buildDownMaps<Action>(
98
+ this.downSection,
99
+ actions,
100
+ context,
101
+ this._warnMissingAction,
102
+ );
103
+ this.combinations = combinations;
104
+ this.combinationsMap = combinationsMap;
105
+ this.activeDownInputs = singles;
106
+ this.activeUpInputs = buildUpMap<Action>(this.upSection, actions, context, this._warnMissingAction);
107
+ }
108
+
109
+ protected _warnMissingAction(key: string): void {
110
+ if (this._warnedMissingActions.has(key)) {
111
+ return;
112
+ }
113
+ this._warnedMissingActions.add(key);
114
+ Logger.warn(`${this.logLabel}: scheme references unknown action "${key}"`);
14
115
  }
15
116
  }