@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.
- package/build/assetpack.mjs +90 -0
- 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/build/plugins/pruneFallbacks.mjs +25 -2
- 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-cSJj-R9M.js} +5 -5
- package/lib/{CaptionsPlugin-WPAcfAkx.js.map → CaptionsPlugin-cSJj-R9M.js.map} +1 -1
- package/lib/{DataAdapter-DS-twowe.js → DataAdapter-D2tc6hx9.js} +15 -10
- package/lib/DataAdapter-D2tc6hx9.js.map +1 -0
- package/lib/{DebugRenderer-D1ryrRYx.js → DebugRenderer-CGUKy6Fy.js} +2 -2
- package/lib/{DebugRenderer-D1ryrRYx.js.map → DebugRenderer-CGUKy6Fy.js.map} +1 -1
- package/lib/{DevToolsPlugin-DH7zX7HU.js → DevToolsPlugin-B8GX3H9b.js} +2 -2
- package/lib/{DevToolsPlugin-DH7zX7HU.js.map → DevToolsPlugin-B8GX3H9b.js.map} +1 -1
- package/lib/{GSAPPlugin-BBtRjUqe.js → GSAPPlugin-Br9AjuTD.js} +6 -6
- package/lib/{GSAPPlugin-BBtRjUqe.js.map → GSAPPlugin-Br9AjuTD.js.map} +1 -1
- package/lib/{LayoutPlugin-Co6vcoV4.js → LayoutPlugin-BX3OP7pw.js} +4 -4
- package/lib/{LayoutPlugin-Co6vcoV4.js.map → LayoutPlugin-BX3OP7pw.js.map} +1 -1
- package/lib/{SpinePlugin-uN641Hmk.js → SpinePlugin-C-JDdXpf.js} +690 -793
- package/lib/SpinePlugin-C-JDdXpf.js.map +1 -0
- package/lib/{StatsPlugin-vZQRm6wh.js → StatsPlugin-yJlJtoQ2.js} +7 -7
- package/lib/StatsPlugin-yJlJtoQ2.js.map +1 -0
- package/lib/{VoiceOverPlugin-KlZdGj1G.js → VoiceOverPlugin-DjSZp4Ko.js} +7 -6
- package/lib/VoiceOverPlugin-DjSZp4Ko.js.map +1 -0
- package/lib/caper.mjs +159 -1917
- package/lib/caper.mjs.map +1 -1
- package/lib/const-C3sxeYhl.js +1746 -0
- package/lib/const-C3sxeYhl.js.map +1 -0
- 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/create.d.ts.map +1 -1
- 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/padding-1kHDpGlQ.js +41 -0
- package/lib/padding-1kHDpGlQ.js.map +1 -0
- 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 +0 -5
- 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/KeyboardPlugin.d.ts +0 -1
- package/lib/plugins/KeyboardPlugin.d.ts.map +1 -1
- 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/Plugin.d.ts +101 -2
- package/lib/plugins/Plugin.d.ts.map +1 -1
- 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 -1
- 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.map +1 -1
- package/lib/plugins/TimerPlugin.d.ts.map +1 -1
- package/lib/plugins/WebEventsPlugin.d.ts +0 -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/breakpoints/BreakpointPlugin.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/gesture/GesturePlugin.d.ts.map +1 -1
- package/lib/plugins/input/AbstractControls.d.ts +47 -3
- 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/InputPlugin.d.ts.map +1 -1
- package/lib/plugins/input/controlsCore.d.ts +58 -0
- package/lib/plugins/input/controlsCore.d.ts.map +1 -0
- package/lib/plugins/input/controlsCore.test.d.ts +2 -0
- package/lib/plugins/input/controlsCore.test.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/KeyboardControls.d.ts +15 -0
- package/lib/plugins/input/keyboard/KeyboardControls.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/index.d.ts +1 -1
- package/lib/plugins/input/keyboard/index.d.ts.map +1 -1
- package/lib/plugins/input/touch/VirtualControls.d.ts +8 -14
- 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-BfV-VF7a.js} +665 -597
- package/lib/registries-BfV-VF7a.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/create.ts +11 -0
- 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 +9 -18
- package/src/plugins/GSAPPlugin.test.ts +31 -0
- package/src/plugins/GSAPPlugin.ts +1 -4
- package/src/plugins/KeyboardPlugin.ts +3 -10
- package/src/plugins/LookupPlugin.test.ts +42 -0
- package/src/plugins/LookupPlugin.ts +15 -3
- package/src/plugins/Plugin.test.ts +130 -2
- package/src/plugins/Plugin.ts +159 -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 +18 -13
- package/src/plugins/StatsPlugin.ts +3 -2
- package/src/plugins/TimerPlugin.ts +1 -2
- package/src/plugins/WebEventsPlugin.test.ts +32 -0
- package/src/plugins/WebEventsPlugin.ts +9 -16
- 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/breakpoints/BreakpointPlugin.ts +1 -2
- package/src/plugins/captions/CaptionsPlugin.test.ts +40 -0
- package/src/plugins/captions/CaptionsPlugin.ts +2 -2
- package/src/plugins/focus/FocusManagerPlugin.test.ts +71 -0
- package/src/plugins/focus/FocusManagerPlugin.ts +7 -0
- package/src/plugins/gesture/GesturePlugin.ts +4 -8
- package/src/plugins/input/AbstractControls.ts +104 -3
- package/src/plugins/input/Controls.test.ts +205 -0
- package/src/plugins/input/InputPlugin.ts +6 -14
- package/src/plugins/input/controlsCore.test.ts +151 -0
- package/src/plugins/input/controlsCore.ts +162 -0
- package/src/plugins/input/keyboard/KeyboardControls.ts +84 -0
- package/src/plugins/input/keyboard/index.ts +1 -1
- package/src/plugins/input/touch/VirtualControls.test.ts +38 -0
- package/src/plugins/input/touch/VirtualControls.ts +53 -120
- 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/plugins/input/keyboard/KeyboardContols.d.ts +0 -28
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +0 -1
- package/lib/registries-BLrxALZ0.js.map +0 -1
- package/src/plugins/input/keyboard/KeyboardContols.ts +0 -182
|
@@ -0,0 +1,205 @@
|
|
|
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 { Logger } from '../../utils';
|
|
14
|
+
import { Controls } from './Controls';
|
|
15
|
+
|
|
16
|
+
type FakeApp = {
|
|
17
|
+
ticker: { add: ReturnType<typeof vi.fn>; remove: ReturnType<typeof vi.fn> };
|
|
18
|
+
actionContext: string;
|
|
19
|
+
actionsPlugin: { getActions: () => Record<string, { context: string; input: string[] }> };
|
|
20
|
+
signal: { onActionContextChanged: Signal<() => void> };
|
|
21
|
+
keyboard: {
|
|
22
|
+
keysDown: Set<string>;
|
|
23
|
+
onKeyDown: () => Signal<(detail: any) => void>;
|
|
24
|
+
onKeyUp: () => Signal<(detail: any) => void>;
|
|
25
|
+
};
|
|
26
|
+
action: ReturnType<typeof vi.fn>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let app: FakeApp;
|
|
30
|
+
let keyDown: Signal<(detail: any) => void>;
|
|
31
|
+
let keyUp: Signal<(detail: any) => void>;
|
|
32
|
+
let actions: Record<string, { context: string; input: string[] }>;
|
|
33
|
+
|
|
34
|
+
function pressKey(key: string) {
|
|
35
|
+
app.keyboard.keysDown.add(key);
|
|
36
|
+
keyDown.emit({ event: { key }, key });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function releaseKey(key: string) {
|
|
40
|
+
app.keyboard.keysDown.delete(key);
|
|
41
|
+
keyUp.emit({ event: { key }, key });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeButton(id: string): IButton {
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
onDown: new Signal<() => void>(),
|
|
48
|
+
onUp: new Signal<() => void>(),
|
|
49
|
+
onUpOutside: new Signal<() => void>(),
|
|
50
|
+
onDestroy: new Signal<() => void>(),
|
|
51
|
+
} as unknown as IButton;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
keyDown = new Signal();
|
|
56
|
+
keyUp = new Signal();
|
|
57
|
+
actions = {
|
|
58
|
+
move_left: { context: '*', input: [] },
|
|
59
|
+
jump: { context: '*', input: [] },
|
|
60
|
+
};
|
|
61
|
+
app = {
|
|
62
|
+
ticker: { add: vi.fn(), remove: vi.fn() },
|
|
63
|
+
actionContext: 'default',
|
|
64
|
+
actionsPlugin: { getActions: () => actions },
|
|
65
|
+
signal: { onActionContextChanged: new Signal<() => void>() },
|
|
66
|
+
keyboard: {
|
|
67
|
+
keysDown: new Set<string>(),
|
|
68
|
+
onKeyDown: () => keyDown,
|
|
69
|
+
onKeyUp: () => keyUp,
|
|
70
|
+
},
|
|
71
|
+
action: vi.fn(),
|
|
72
|
+
};
|
|
73
|
+
mocks.app = app;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Controls teardown', () => {
|
|
77
|
+
it('destroys configured control schemes without throwing and removes ticker callbacks', () => {
|
|
78
|
+
const controls = new Controls();
|
|
79
|
+
controls.initialize({
|
|
80
|
+
keyboard: { down: { move_left: 'A' } },
|
|
81
|
+
touch: { down: { jump: 'btn-a' } },
|
|
82
|
+
} as any);
|
|
83
|
+
controls.connect();
|
|
84
|
+
|
|
85
|
+
expect(app.ticker.add).toHaveBeenCalledTimes(2);
|
|
86
|
+
expect(() => controls.destroy()).not.toThrow();
|
|
87
|
+
expect(app.ticker.remove).toHaveBeenCalledTimes(2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Controls combinations', () => {
|
|
92
|
+
it('reports a keyboard combination active when both keys are down', () => {
|
|
93
|
+
const controls = new Controls();
|
|
94
|
+
controls.initialize({ keyboard: { down: { move_left: ['A+B'] } } } as any);
|
|
95
|
+
controls.connect();
|
|
96
|
+
|
|
97
|
+
expect(controls.isActionActive('move_left')).toBe(false);
|
|
98
|
+
|
|
99
|
+
pressKey('A');
|
|
100
|
+
expect(controls.isActionActive('move_left')).toBe(false);
|
|
101
|
+
|
|
102
|
+
pressKey('B');
|
|
103
|
+
expect(controls.isActionActive('move_left')).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('reports a virtual button combination active when both buttons are down', () => {
|
|
107
|
+
const controls = new Controls();
|
|
108
|
+
controls.initialize({ touch: { down: { jump: ['btn-a+btn-b'] } } } as any);
|
|
109
|
+
controls.connect();
|
|
110
|
+
|
|
111
|
+
const a = makeButton('btn-a');
|
|
112
|
+
const b = makeButton('btn-b');
|
|
113
|
+
controls.virtual.addButton(a);
|
|
114
|
+
controls.virtual.addButton(b);
|
|
115
|
+
|
|
116
|
+
a.onDown.emit();
|
|
117
|
+
expect(controls.isActionActive('jump')).toBe(false);
|
|
118
|
+
|
|
119
|
+
b.onDown.emit();
|
|
120
|
+
expect(controls.isActionActive('jump')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('Controls with an unmapped action in the scheme', () => {
|
|
125
|
+
it('does not throw on initialize or on an action context change', () => {
|
|
126
|
+
const controls = new Controls();
|
|
127
|
+
|
|
128
|
+
expect(() =>
|
|
129
|
+
controls.initialize({
|
|
130
|
+
keyboard: { down: { not_an_action: 'A' } },
|
|
131
|
+
touch: { down: { not_an_action: 'btn-a' }, up: { also_not_an_action: 'btn-b' } },
|
|
132
|
+
} as any),
|
|
133
|
+
).not.toThrow();
|
|
134
|
+
|
|
135
|
+
controls.connect();
|
|
136
|
+
|
|
137
|
+
expect(() => app.signal.onActionContextChanged.emit()).not.toThrow();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Controls up maps', () => {
|
|
142
|
+
it('dispatches a keyboard up action that is in context', () => {
|
|
143
|
+
const controls = new Controls();
|
|
144
|
+
controls.initialize({ keyboard: { up: { jump: 'A' } } } as any);
|
|
145
|
+
controls.connect();
|
|
146
|
+
|
|
147
|
+
pressKey('A');
|
|
148
|
+
releaseKey('A');
|
|
149
|
+
|
|
150
|
+
expect(app.action).toHaveBeenCalledWith('jump', { combination: false, inputState: 'up', key: 'A' });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not dispatch a keyboard up action that is out of context', () => {
|
|
154
|
+
actions.pause = { context: 'menu', input: [] };
|
|
155
|
+
|
|
156
|
+
const controls = new Controls();
|
|
157
|
+
controls.initialize({ keyboard: { up: { pause: 'A' } } } as any);
|
|
158
|
+
controls.connect();
|
|
159
|
+
|
|
160
|
+
pressKey('A');
|
|
161
|
+
releaseKey('A');
|
|
162
|
+
|
|
163
|
+
expect(app.action).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('warns once for an unknown action in the keyboard up map', () => {
|
|
167
|
+
const warn = vi.spyOn(Logger, 'warn').mockImplementation(() => undefined);
|
|
168
|
+
|
|
169
|
+
const controls = new Controls();
|
|
170
|
+
controls.initialize({ keyboard: { up: { not_an_action: 'A' } } } as any);
|
|
171
|
+
controls.connect();
|
|
172
|
+
|
|
173
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
174
|
+
warn.mockRestore();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('dispatches a virtual up action declared as an array', () => {
|
|
178
|
+
const controls = new Controls();
|
|
179
|
+
controls.initialize({ touch: { up: { jump: ['btn-a'] } } } as any);
|
|
180
|
+
controls.connect();
|
|
181
|
+
|
|
182
|
+
const button = makeButton('btn-a');
|
|
183
|
+
controls.virtual.addButton(button);
|
|
184
|
+
button.onDown.emit();
|
|
185
|
+
button.onUp.emit();
|
|
186
|
+
|
|
187
|
+
expect(app.action).toHaveBeenCalledWith('jump', { combination: false, inputState: 'up', button: 'btn-a' });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('VirtualControls.removeButton', () => {
|
|
192
|
+
it('stops a removed button from driving action state', () => {
|
|
193
|
+
const controls = new Controls();
|
|
194
|
+
controls.initialize({ touch: { down: { jump: 'btn-a' } } } as any);
|
|
195
|
+
controls.connect();
|
|
196
|
+
|
|
197
|
+
const button = makeButton('btn-a');
|
|
198
|
+
controls.virtual.addButton(button);
|
|
199
|
+
controls.virtual.removeButton(button);
|
|
200
|
+
|
|
201
|
+
button.onDown.emit();
|
|
202
|
+
|
|
203
|
+
expect(controls.isActionActive('jump')).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -70,11 +70,11 @@ export class InputPlugin extends Plugin<InputManagerOptions> implements IInputPl
|
|
|
70
70
|
|
|
71
71
|
app.stage.eventMode = 'static';
|
|
72
72
|
this._canvas = app.canvas as HTMLCanvasElement;
|
|
73
|
-
this._canvas
|
|
74
|
-
this._canvas
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
this.listen(this._canvas, 'pointerdown', this._onPointerDown as EventListener);
|
|
74
|
+
this.listen(this._canvas, 'pointermove', this._onPointerMove as EventListener);
|
|
75
|
+
this.listen(window, 'keydown', this._onKeyDown);
|
|
76
|
+
this.listen(window, 'gamepadconnected', this._onGamepadConnected as EventListener);
|
|
77
|
+
this.listen(window, 'gamepaddisconnected', this._onGamepadDisconnected as EventListener);
|
|
78
78
|
|
|
79
79
|
if (this._options.controls) {
|
|
80
80
|
this.controls.initialize(this._options.controls);
|
|
@@ -88,15 +88,7 @@ export class InputPlugin extends Plugin<InputManagerOptions> implements IInputPl
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
destroy(): void {
|
|
91
|
-
|
|
92
|
-
if (this._canvas) {
|
|
93
|
-
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
94
|
-
this._canvas.removeEventListener('pointermove', this._onPointerMove);
|
|
95
|
-
this._canvas = null;
|
|
96
|
-
}
|
|
97
|
-
window.removeEventListener('keydown', this._onKeyDown);
|
|
98
|
-
window.removeEventListener('gamepadconnected', this._onGamepadConnected);
|
|
99
|
-
window.removeEventListener('gamepaddisconnected', this._onGamepadDisconnected);
|
|
91
|
+
this._canvas = null;
|
|
100
92
|
|
|
101
93
|
this.controls.destroy();
|
|
102
94
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { buildDownMaps, buildUpMap, contextAllows, evaluateCombinations, isInputActive } from './controlsCore';
|
|
4
|
+
|
|
5
|
+
const anywhere = { context: '*' as const };
|
|
6
|
+
|
|
7
|
+
describe('contextAllows', () => {
|
|
8
|
+
it("allows every context for '*'", () => {
|
|
9
|
+
expect(contextAllows('*', 'default')).toBe(true);
|
|
10
|
+
expect(contextAllows('*', 'menu')).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('matches a string context exactly', () => {
|
|
14
|
+
expect(contextAllows('menu', 'menu')).toBe(true);
|
|
15
|
+
expect(contextAllows('menu', 'game')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does not treat a substring as a match', () => {
|
|
19
|
+
expect(contextAllows('game_over', 'game')).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('matches any member of an array context', () => {
|
|
23
|
+
expect(contextAllows(['menu', 'game'], 'game')).toBe(true);
|
|
24
|
+
expect(contextAllows(['menu', 'game'], 'pause')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('buildDownMaps', () => {
|
|
29
|
+
it('splits singles from combinations and sorts combinations longest first', () => {
|
|
30
|
+
const { combinations, combinationsMap, singles } = buildDownMaps(
|
|
31
|
+
{ jump: 'A', special: ['B+C', 'D+E+F'] },
|
|
32
|
+
{ jump: anywhere, special: anywhere },
|
|
33
|
+
'default',
|
|
34
|
+
vi.fn(),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect([...singles]).toEqual([['A', 'jump']]);
|
|
38
|
+
expect(combinations).toEqual([
|
|
39
|
+
['D', 'E', 'F'],
|
|
40
|
+
['B', 'C'],
|
|
41
|
+
]);
|
|
42
|
+
expect(combinationsMap.get(combinations[0])).toBe('special');
|
|
43
|
+
expect(combinationsMap.get(combinations[1])).toBe('special');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('skips actions that are out of context', () => {
|
|
47
|
+
const { singles } = buildDownMaps(
|
|
48
|
+
{ jump: 'A', pause: 'B' },
|
|
49
|
+
{ jump: anywhere, pause: { context: 'menu' } },
|
|
50
|
+
'default',
|
|
51
|
+
vi.fn(),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect([...singles]).toEqual([['A', 'jump']]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('warns once per unknown action and skips it', () => {
|
|
58
|
+
const warn = vi.fn();
|
|
59
|
+
const { singles } = buildDownMaps({ nope: ['A', 'B'] }, {}, 'default', warn);
|
|
60
|
+
|
|
61
|
+
expect(singles.size).toBe(0);
|
|
62
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(warn).toHaveBeenCalledWith('nope');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles a missing section', () => {
|
|
67
|
+
const { combinations, singles } = buildDownMaps(undefined, {}, 'default', vi.fn());
|
|
68
|
+
|
|
69
|
+
expect(combinations).toEqual([]);
|
|
70
|
+
expect(singles.size).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('buildUpMap', () => {
|
|
75
|
+
it('normalizes a single input and an array of inputs', () => {
|
|
76
|
+
const map = buildUpMap({ jump: 'A', pause: ['B', 'C'] }, { jump: anywhere, pause: anywhere }, 'default', vi.fn());
|
|
77
|
+
|
|
78
|
+
expect([...map]).toEqual([
|
|
79
|
+
['A', 'jump'],
|
|
80
|
+
['B', 'pause'],
|
|
81
|
+
['C', 'pause'],
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('skips out-of-context actions and warns once for unknown ones', () => {
|
|
86
|
+
const warn = vi.fn();
|
|
87
|
+
const map = buildUpMap(
|
|
88
|
+
{ jump: 'A', pause: 'B', nope: 'C' },
|
|
89
|
+
{ jump: anywhere, pause: { context: 'menu' } },
|
|
90
|
+
'default',
|
|
91
|
+
warn,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect([...map]).toEqual([['A', 'jump']]);
|
|
95
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(warn).toHaveBeenCalledWith('nope');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('isInputActive', () => {
|
|
101
|
+
it('uses the down predicate for a single input', () => {
|
|
102
|
+
const isDown = (id: string) => id === 'A';
|
|
103
|
+
|
|
104
|
+
expect(isInputActive('A', isDown)).toBe(true);
|
|
105
|
+
expect(isInputActive('B', isDown)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('requires every part of a combination', () => {
|
|
109
|
+
const down = new Set(['A', 'B']);
|
|
110
|
+
const isDown = (id: string) => down.has(id);
|
|
111
|
+
|
|
112
|
+
expect(isInputActive('A+B', isDown)).toBe(true);
|
|
113
|
+
expect(isInputActive('A+C', isDown)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('uses the combination predicate for combination parts only', () => {
|
|
117
|
+
const isDown = (id: string) => id === 'A';
|
|
118
|
+
const isComboPartDown = (id: string) => id === 'A' || id === 'joystick_up';
|
|
119
|
+
|
|
120
|
+
expect(isInputActive('A+joystick_up', isDown, isComboPartDown)).toBe(true);
|
|
121
|
+
expect(isInputActive('joystick_up', isDown, isComboPartDown)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('evaluateCombinations', () => {
|
|
126
|
+
it('fires a combination whose inputs are all down and eliminates its inputs', () => {
|
|
127
|
+
const combinations = [['A', 'B']];
|
|
128
|
+
const { fired, eliminated } = evaluateCombinations(combinations, (id) => id === 'A' || id === 'B');
|
|
129
|
+
|
|
130
|
+
expect(fired).toEqual([['A', 'B']]);
|
|
131
|
+
expect([...eliminated]).toEqual(['A', 'B']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not fire a combination that shares an input with an already fired one', () => {
|
|
135
|
+
const combinations = [
|
|
136
|
+
['A', 'B', 'C'],
|
|
137
|
+
['A', 'B'],
|
|
138
|
+
];
|
|
139
|
+
const down = new Set(['A', 'B', 'C']);
|
|
140
|
+
const { fired } = evaluateCombinations(combinations, (id) => down.has(id));
|
|
141
|
+
|
|
142
|
+
expect(fired).toEqual([['A', 'B', 'C']]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('fires nothing when no combination is fully down', () => {
|
|
146
|
+
const { fired, eliminated } = evaluateCombinations([['A', 'B']], (id) => id === 'A');
|
|
147
|
+
|
|
148
|
+
expect(fired).toEqual([]);
|
|
149
|
+
expect(eliminated.size).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scheme processing shared by every controls adapter — context gating,
|
|
3
|
+
* down/up map building and combination elimination. No caper/Pixi imports so
|
|
4
|
+
* it stays trivially unit-testable, and so keyboard and virtual input can not
|
|
5
|
+
* drift apart again.
|
|
6
|
+
*
|
|
7
|
+
* `A` is the adapter's action-id type; ids are plain object keys at runtime.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** One `down` / `up` / `joystick` block of a controls scheme. */
|
|
11
|
+
export interface SchemeSection {
|
|
12
|
+
[actionId: string]: string | string[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The part of a registered action this module cares about. */
|
|
16
|
+
export interface ActionLike {
|
|
17
|
+
context: string | string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The down-map trio: combinations (longest first), their action lookup, and single inputs. */
|
|
21
|
+
export interface DownMaps<A = string> {
|
|
22
|
+
combinations: string[][];
|
|
23
|
+
combinationsMap: Map<string[], A>;
|
|
24
|
+
singles: Map<string, A>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether an action registered for `actionContext` may fire in `current`.
|
|
29
|
+
* Matching is exact — a string context is compared with `===`, never with
|
|
30
|
+
* `includes`, so `'game_over'` does not swallow `'game'`.
|
|
31
|
+
*/
|
|
32
|
+
export function contextAllows(actionContext: string | string[], current: string): boolean {
|
|
33
|
+
if (actionContext === '*') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(actionContext)) {
|
|
37
|
+
return actionContext.includes(current);
|
|
38
|
+
}
|
|
39
|
+
return actionContext === current;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the down maps for a scheme section. Unknown actions are reported to
|
|
44
|
+
* `warn` (once per action) and skipped; out-of-context actions are skipped
|
|
45
|
+
* silently. Combinations are sorted largest first so the elimination pass in
|
|
46
|
+
* {@link evaluateCombinations} prefers the most specific match.
|
|
47
|
+
*/
|
|
48
|
+
export function buildDownMaps<A = string>(
|
|
49
|
+
section: SchemeSection | undefined,
|
|
50
|
+
actions: Record<string, ActionLike | undefined>,
|
|
51
|
+
currentContext: string,
|
|
52
|
+
warn: (actionId: string) => void,
|
|
53
|
+
): DownMaps<A> {
|
|
54
|
+
const combinations: string[][] = [];
|
|
55
|
+
const combinationsMap = new Map<string[], A>();
|
|
56
|
+
const singles = new Map<string, A>();
|
|
57
|
+
|
|
58
|
+
forEachInput<A>(section, actions, currentContext, warn, (key, input) => {
|
|
59
|
+
if (input.includes('+')) {
|
|
60
|
+
const combo = input.split('+');
|
|
61
|
+
combinations.push(combo);
|
|
62
|
+
combinationsMap.set(combo, key);
|
|
63
|
+
} else {
|
|
64
|
+
singles.set(input, key);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// sort them from the largest to smallest
|
|
69
|
+
combinations.sort((a, b) => b.length - a.length);
|
|
70
|
+
|
|
71
|
+
return { combinations, combinationsMap, singles };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the up map for a scheme section — same gating and warning rules as
|
|
76
|
+
* {@link buildDownMaps}, but every input is a single value.
|
|
77
|
+
*/
|
|
78
|
+
export function buildUpMap<A = string>(
|
|
79
|
+
section: SchemeSection | undefined,
|
|
80
|
+
actions: Record<string, ActionLike | undefined>,
|
|
81
|
+
currentContext: string,
|
|
82
|
+
warn: (actionId: string) => void,
|
|
83
|
+
): Map<string, A> {
|
|
84
|
+
const map = new Map<string, A>();
|
|
85
|
+
forEachInput<A>(section, actions, currentContext, warn, (key, input) => {
|
|
86
|
+
map.set(input, key);
|
|
87
|
+
});
|
|
88
|
+
return map;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether an input is active: a single input is tested with `isDown`, a `+`
|
|
93
|
+
* separated combination requires every part to satisfy `isComboPartDown`
|
|
94
|
+
* (which defaults to `isDown` — the virtual adapter uses the split to let a
|
|
95
|
+
* joystick direction take part in a combination but not stand alone).
|
|
96
|
+
*/
|
|
97
|
+
export function isInputActive(
|
|
98
|
+
input: string,
|
|
99
|
+
isDown: (id: string) => boolean,
|
|
100
|
+
isComboPartDown: (id: string) => boolean = isDown,
|
|
101
|
+
): boolean {
|
|
102
|
+
if (input.includes('+')) {
|
|
103
|
+
return input.split('+').every((id) => isComboPartDown(id));
|
|
104
|
+
}
|
|
105
|
+
return isDown(input);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Walk combinations (already sorted largest first) and fire each one whose
|
|
110
|
+
* inputs are all down and none of which a longer combination already claimed.
|
|
111
|
+
* Returns the fired combinations plus the claimed inputs, so the caller can
|
|
112
|
+
* skip them when dispatching single inputs.
|
|
113
|
+
*/
|
|
114
|
+
export function evaluateCombinations(
|
|
115
|
+
combinations: string[][],
|
|
116
|
+
isDown: (id: string) => boolean,
|
|
117
|
+
): { fired: string[][]; eliminated: Set<string> } {
|
|
118
|
+
const fired: string[][] = [];
|
|
119
|
+
const eliminated = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < combinations.length; i++) {
|
|
122
|
+
const combination = combinations[i];
|
|
123
|
+
if (combination.some((id) => eliminated.has(id))) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (combination.every((id) => isDown(id))) {
|
|
127
|
+
combination.forEach((id) => eliminated.add(id));
|
|
128
|
+
fired.push(combination);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { fired, eliminated };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Shared scheme walk: resolve the action, gate it, normalize its inputs. */
|
|
136
|
+
function forEachInput<A>(
|
|
137
|
+
section: SchemeSection | undefined,
|
|
138
|
+
actions: Record<string, ActionLike | undefined>,
|
|
139
|
+
currentContext: string,
|
|
140
|
+
warn: (actionId: string) => void,
|
|
141
|
+
visit: (actionId: A, input: string) => void,
|
|
142
|
+
): void {
|
|
143
|
+
if (!section) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
Object.keys(section).forEach((key) => {
|
|
147
|
+
const action = actions[key];
|
|
148
|
+
if (!action) {
|
|
149
|
+
warn(key);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!contextAllows(action.context, currentContext)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const input = section[key];
|
|
156
|
+
if (!input) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
160
|
+
inputs.forEach((inputString) => visit(key as unknown as A, inputString));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { bindAllMethods } from '../../../utils';
|
|
2
|
+
import { normalizeKey, type KeyboardEventDetail } from '../../KeyboardPlugin';
|
|
3
|
+
import { AbstractControls } from '../AbstractControls';
|
|
4
|
+
import { evaluateCombinations } from '../controlsCore';
|
|
5
|
+
import type { ControlsActionMap, KeyboardControlsMap } from '../types';
|
|
6
|
+
|
|
7
|
+
export class KeyboardControls extends AbstractControls {
|
|
8
|
+
protected scheme: Partial<KeyboardControlsMap>;
|
|
9
|
+
protected readonly logLabel = 'KeyboardControls';
|
|
10
|
+
private _singleDownKeys: Set<string> = new Set();
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
bindAllMethods(this);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public initialize(scheme: Partial<KeyboardControlsMap>): void {
|
|
18
|
+
super.initialize(scheme as Partial<ControlsActionMap>);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public connect() {
|
|
22
|
+
this.addSignalConnection(
|
|
23
|
+
this.app.keyboard.onKeyDown().connect(this._handleKeyDown),
|
|
24
|
+
this.app.keyboard.onKeyUp().connect(this._handleKeyUp),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
super.connect();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected isInputDown(id: string): boolean {
|
|
31
|
+
return this._singleDownKeys.has(id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private _handleKeyDown(detail: KeyboardEventDetail): void {
|
|
35
|
+
const key = normalizeKey(detail.event.key);
|
|
36
|
+
this._singleDownKeys.add(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private _handleKeyUp(detail: KeyboardEventDetail): void {
|
|
40
|
+
const key = normalizeKey(detail.event.key);
|
|
41
|
+
this._singleDownKeys.delete(key);
|
|
42
|
+
|
|
43
|
+
const action = this.activeUpInputs.get(key);
|
|
44
|
+
if (action) {
|
|
45
|
+
this.app.action(action, { combination: false, inputState: 'up', key });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected _update() {
|
|
50
|
+
if (!this.app.keyboard) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const keysDown = this.app.keyboard.keysDown;
|
|
54
|
+
if (keysDown.size === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// this.combinations is already sorted from largest to smallest
|
|
59
|
+
const { fired, eliminated } = evaluateCombinations(this.combinations, (key) => keysDown.has(key));
|
|
60
|
+
fired.forEach((combination) => {
|
|
61
|
+
const action = this.combinationsMap.get(combination);
|
|
62
|
+
if (action) {
|
|
63
|
+
this.app.action(action, {
|
|
64
|
+
key: combination,
|
|
65
|
+
combination: true,
|
|
66
|
+
inputState: 'down',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// order doesn't matter here
|
|
72
|
+
this._singleDownKeys.forEach((key) => {
|
|
73
|
+
if (eliminated.has(key)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (keysDown.has(key)) {
|
|
77
|
+
const action = this.activeDownInputs.get(key);
|
|
78
|
+
if (action) {
|
|
79
|
+
this.app.action(action, { key, combination: false, inputState: 'down' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './interfaces';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './KeyboardControls';
|
|
@@ -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
|
+
});
|