canvasengine 2.0.0-beta.37 → 2.0.0-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DebugRenderer-CTWPthRt.js → DebugRenderer-Rrw9FlTd.js} +2 -2
- package/dist/{DebugRenderer-CTWPthRt.js.map → DebugRenderer-Rrw9FlTd.js.map} +1 -1
- package/dist/components/Button.d.ts +50 -3
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Joystick.d.ts +36 -0
- package/dist/components/Joystick.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +2 -0
- package/dist/components/Sprite.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/types/Spritesheet.d.ts +0 -118
- package/dist/components/types/Spritesheet.d.ts.map +1 -1
- package/dist/directives/Controls.d.ts +16 -7
- package/dist/directives/Controls.d.ts.map +1 -1
- package/dist/directives/GamepadControls.d.ts +3 -1
- package/dist/directives/GamepadControls.d.ts.map +1 -1
- package/dist/directives/JoystickControls.d.ts +172 -0
- package/dist/directives/JoystickControls.d.ts.map +1 -0
- package/dist/directives/index.d.ts +1 -0
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/engine/signal.d.ts.map +1 -1
- package/dist/{index-BqwprEPH.js → index-BQ99FClW.js} +6057 -5433
- package/dist/index-BQ99FClW.js.map +1 -0
- package/dist/index.global.js +7 -7
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +59 -57
- package/dist/utils/GlobalAssetLoader.d.ts +141 -0
- package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/Button.ts +168 -41
- package/src/components/Canvas.ts +3 -0
- package/src/components/Joystick.ts +361 -0
- package/src/components/Sprite.ts +82 -16
- package/src/components/index.ts +2 -1
- package/src/components/types/Spritesheet.ts +0 -118
- package/src/directives/Controls.ts +42 -8
- package/src/directives/GamepadControls.ts +40 -18
- package/src/directives/JoystickControls.ts +396 -0
- package/src/directives/index.ts +1 -0
- package/src/engine/reactive.ts +362 -242
- package/src/engine/signal.ts +8 -2
- package/src/utils/GlobalAssetLoader.ts +257 -0
- package/dist/index-BqwprEPH.js.map +0 -1
|
@@ -3,14 +3,15 @@ import { Element } from "../engine/reactive";
|
|
|
3
3
|
import { ControlsBase, Controls } from "./ControlsBase";
|
|
4
4
|
import { KeyboardControls } from "./KeyboardControls";
|
|
5
5
|
import { GamepadControls, GamepadConfig } from "./GamepadControls";
|
|
6
|
+
import { JoystickControls, JoystickConfig } from "./JoystickControls";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Controls directive that coordinates keyboard and
|
|
9
|
+
* Controls directive that coordinates keyboard, gamepad, and joystick input systems
|
|
9
10
|
*
|
|
10
|
-
* This directive automatically activates
|
|
11
|
+
* This directive automatically activates keyboard, gamepad, and joystick controls when available.
|
|
11
12
|
* The gamepad is automatically enabled if joypad.js is detected in the environment.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
+
* All systems share the same control configuration and can work simultaneously.
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
17
|
* ```html
|
|
@@ -25,10 +26,11 @@ import { GamepadControls, GamepadConfig } from "./GamepadControls";
|
|
|
25
26
|
export class ControlsDirective extends Directive {
|
|
26
27
|
private keyboardControls: KeyboardControls | null = null;
|
|
27
28
|
private gamepadControls: GamepadControls | null = null;
|
|
29
|
+
private joystickControls: JoystickControls | null = null;
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Initialize the controls directive
|
|
31
|
-
* Sets up keyboard and
|
|
33
|
+
* Sets up keyboard, gamepad, and joystick controls if available
|
|
32
34
|
*/
|
|
33
35
|
onInit(element: Element) {
|
|
34
36
|
const value = element.props.controls?.value ?? element.props.controls;
|
|
@@ -47,6 +49,14 @@ export class ControlsDirective extends Directive {
|
|
|
47
49
|
this.gamepadControls.setInputs(value as Controls & { gamepad?: GamepadConfig });
|
|
48
50
|
this.gamepadControls.start();
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
// Initialize joystick controls if joystick config is present
|
|
54
|
+
const joystickConfig = (value as Controls & { joystick?: JoystickConfig }).joystick;
|
|
55
|
+
if (joystickConfig !== undefined && joystickConfig.enabled !== false) {
|
|
56
|
+
this.joystickControls = new JoystickControls();
|
|
57
|
+
this.joystickControls.setInputs(value as Controls & { joystick?: JoystickConfig });
|
|
58
|
+
this.joystickControls.start();
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
/**
|
|
@@ -84,6 +94,11 @@ export class ControlsDirective extends Directive {
|
|
|
84
94
|
this.gamepadControls.destroy();
|
|
85
95
|
this.gamepadControls = null;
|
|
86
96
|
}
|
|
97
|
+
|
|
98
|
+
if (this.joystickControls) {
|
|
99
|
+
this.joystickControls.destroy();
|
|
100
|
+
this.joystickControls = null;
|
|
101
|
+
}
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
/**
|
|
@@ -113,20 +128,24 @@ export class ControlsDirective extends Directive {
|
|
|
113
128
|
*
|
|
114
129
|
* @param controlName - Name of the control
|
|
115
130
|
* @param isDown - Whether the control is pressed (true) or released (false)
|
|
131
|
+
* @param payload - Optional payload to pass to keyDown/keyUp callbacks (e.g., { power: 0.8 })
|
|
116
132
|
* @returns Promise that resolves when the action is complete
|
|
117
133
|
*/
|
|
118
|
-
async applyControl(controlName: string | number, isDown?: boolean): Promise<void> {
|
|
134
|
+
async applyControl(controlName: string | number, isDown?: boolean, payload?: any): Promise<void> {
|
|
119
135
|
if (this.keyboardControls) {
|
|
120
136
|
await this.keyboardControls.applyControl(controlName, isDown);
|
|
121
137
|
}
|
|
122
138
|
if (this.gamepadControls) {
|
|
123
|
-
await this.gamepadControls.applyControl(controlName, isDown);
|
|
139
|
+
await this.gamepadControls.applyControl(controlName, isDown, payload);
|
|
140
|
+
}
|
|
141
|
+
if (this.joystickControls) {
|
|
142
|
+
await this.joystickControls.applyControl(controlName, isDown, payload);
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
/**
|
|
128
147
|
* Stop listening to inputs
|
|
129
|
-
* Stops
|
|
148
|
+
* Stops keyboard, gamepad, and joystick input processing
|
|
130
149
|
*/
|
|
131
150
|
stopInputs() {
|
|
132
151
|
if (this.keyboardControls) {
|
|
@@ -135,11 +154,14 @@ export class ControlsDirective extends Directive {
|
|
|
135
154
|
if (this.gamepadControls) {
|
|
136
155
|
this.gamepadControls.stopInputs();
|
|
137
156
|
}
|
|
157
|
+
if (this.joystickControls) {
|
|
158
|
+
this.joystickControls.stopInputs();
|
|
159
|
+
}
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
/**
|
|
141
163
|
* Resume listening to inputs
|
|
142
|
-
* Resumes
|
|
164
|
+
* Resumes keyboard, gamepad, and joystick input processing
|
|
143
165
|
*/
|
|
144
166
|
listenInputs() {
|
|
145
167
|
if (this.keyboardControls) {
|
|
@@ -148,6 +170,9 @@ export class ControlsDirective extends Directive {
|
|
|
148
170
|
if (this.gamepadControls) {
|
|
149
171
|
this.gamepadControls.listenInputs();
|
|
150
172
|
}
|
|
173
|
+
if (this.joystickControls) {
|
|
174
|
+
this.joystickControls.listenInputs();
|
|
175
|
+
}
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
/**
|
|
@@ -177,6 +202,15 @@ export class ControlsDirective extends Directive {
|
|
|
177
202
|
get gamepad(): GamepadControls | null {
|
|
178
203
|
return this.gamepadControls;
|
|
179
204
|
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the joystick controls instance
|
|
208
|
+
*
|
|
209
|
+
* @returns JoystickControls instance or null
|
|
210
|
+
*/
|
|
211
|
+
get joystick(): JoystickControls | null {
|
|
212
|
+
return this.joystickControls;
|
|
213
|
+
}
|
|
180
214
|
}
|
|
181
215
|
|
|
182
216
|
registerDirective('controls', ControlsDirective);
|
|
@@ -112,6 +112,7 @@ export class GamepadControls extends ControlsBase {
|
|
|
112
112
|
private joypad: any = null;
|
|
113
113
|
private connectCallbacks: Array<() => void> = [];
|
|
114
114
|
private disconnectCallbacks: Array<() => void> = [];
|
|
115
|
+
private currentPower: number = 0;
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
118
|
* Setup gamepad event listeners
|
|
@@ -129,13 +130,6 @@ export class GamepadControls extends ControlsBase {
|
|
|
129
130
|
clearInterval(this.gamepadMoveInterval);
|
|
130
131
|
this.gamepadMoveInterval = null;
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
-
if (this.joypad) {
|
|
134
|
-
this.joypad.off('connect');
|
|
135
|
-
this.joypad.off('disconnect');
|
|
136
|
-
this.joypad.off('button_press');
|
|
137
|
-
this.joypad.off('axis_move');
|
|
138
|
-
}
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
/**
|
|
@@ -198,6 +192,7 @@ export class GamepadControls extends ControlsBase {
|
|
|
198
192
|
this.gamepadMoving = false;
|
|
199
193
|
this.gamepadDirections = {};
|
|
200
194
|
this.gamepadAxisDate = 0;
|
|
195
|
+
this.currentPower = 0;
|
|
201
196
|
|
|
202
197
|
// Update gamepadConnected signal if provided
|
|
203
198
|
if (this.gamepadConfig.gamepadConnected) {
|
|
@@ -250,6 +245,21 @@ export class GamepadControls extends ControlsBase {
|
|
|
250
245
|
else if (direction === 'left') direction = axisMapping['left'] || 'left';
|
|
251
246
|
else if (direction === 'right') direction = axisMapping['right'] || 'right';
|
|
252
247
|
|
|
248
|
+
// Calculate power/intensity from axis values
|
|
249
|
+
// Get the first connected gamepad instance
|
|
250
|
+
if (this.joypad && this.joypad.instances) {
|
|
251
|
+
const gamepadInstances = Object.values(this.joypad.instances) as any[];
|
|
252
|
+
if (gamepadInstances.length > 0) {
|
|
253
|
+
const gamepad = gamepadInstances[0];
|
|
254
|
+
// Get axes values (axes 0-1 for left stick, 2-3 for right stick)
|
|
255
|
+
// We'll use the left stick by default (axes 0 and 1)
|
|
256
|
+
const axisX = gamepad.axes?.[0] || 0;
|
|
257
|
+
const axisY = gamepad.axes?.[1] || 0;
|
|
258
|
+
// Calculate power as magnitude of the vector
|
|
259
|
+
this.currentPower = Math.min(1, Math.sqrt(axisX * axisX + axisY * axisY));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
253
263
|
// Update active directions
|
|
254
264
|
this.gamepadDirections = {
|
|
255
265
|
[direction]: true
|
|
@@ -265,7 +275,7 @@ export class GamepadControls extends ControlsBase {
|
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
|
|
268
|
-
// Trigger movement
|
|
278
|
+
// Trigger movement with power
|
|
269
279
|
this.processGamepadMovement();
|
|
270
280
|
}
|
|
271
281
|
|
|
@@ -277,9 +287,20 @@ export class GamepadControls extends ControlsBase {
|
|
|
277
287
|
if (!this.gamepadMoving) return;
|
|
278
288
|
if (this.stop) return;
|
|
279
289
|
|
|
290
|
+
// Update current power from gamepad axes if available
|
|
291
|
+
if (this.joypad && this.joypad.instances) {
|
|
292
|
+
const gamepadInstances = Object.values(this.joypad.instances) as any[];
|
|
293
|
+
if (gamepadInstances.length > 0) {
|
|
294
|
+
const gamepad = gamepadInstances[0];
|
|
295
|
+
const axisX = gamepad.axes?.[0] || 0;
|
|
296
|
+
const axisY = gamepad.axes?.[1] || 0;
|
|
297
|
+
this.currentPower = Math.min(1, Math.sqrt(axisX * axisX + axisY * axisY));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
280
301
|
for (const direction in this.gamepadDirections) {
|
|
281
302
|
if (this.gamepadDirections[direction]) {
|
|
282
|
-
this.applyControl(direction, true).catch(() => {
|
|
303
|
+
this.applyControl(direction, true, { power: this.currentPower }).catch(() => {
|
|
283
304
|
// Ignore errors
|
|
284
305
|
});
|
|
285
306
|
}
|
|
@@ -370,9 +391,10 @@ export class GamepadControls extends ControlsBase {
|
|
|
370
391
|
*
|
|
371
392
|
* @param controlName - Name of the control
|
|
372
393
|
* @param isDown - Whether the control is pressed (true) or released (false)
|
|
394
|
+
* @param payload - Optional payload to pass to keyDown/keyUp callbacks (e.g., { power: 0.8 })
|
|
373
395
|
* @returns Promise that resolves when the action is complete
|
|
374
396
|
*/
|
|
375
|
-
async applyControl(controlName: string | number, isDown?: boolean): Promise<void> {
|
|
397
|
+
async applyControl(controlName: string | number, isDown?: boolean, payload?: any): Promise<void> {
|
|
376
398
|
const control = this._controlsOptions[controlName];
|
|
377
399
|
if (!control) return;
|
|
378
400
|
|
|
@@ -385,40 +407,40 @@ export class GamepadControls extends ControlsBase {
|
|
|
385
407
|
if (isDown === undefined) {
|
|
386
408
|
// Press and release (simulate button press)
|
|
387
409
|
if (boundKey.options.keyDown) {
|
|
388
|
-
let parameters = boundKey.parameters;
|
|
410
|
+
let parameters = payload ?? boundKey.parameters;
|
|
389
411
|
if (typeof parameters === "function") {
|
|
390
412
|
parameters = parameters();
|
|
391
413
|
}
|
|
392
|
-
boundKey.options.keyDown(boundKey);
|
|
414
|
+
boundKey.options.keyDown(boundKey, parameters);
|
|
393
415
|
}
|
|
394
416
|
// Release after a short delay (similar to keyboard)
|
|
395
417
|
return new Promise((resolve) => {
|
|
396
418
|
setTimeout(() => {
|
|
397
419
|
if (boundKey.options.keyUp) {
|
|
398
|
-
let parameters = boundKey.parameters;
|
|
420
|
+
let parameters = payload ?? boundKey.parameters;
|
|
399
421
|
if (typeof parameters === "function") {
|
|
400
422
|
parameters = parameters();
|
|
401
423
|
}
|
|
402
|
-
boundKey.options.keyUp(boundKey);
|
|
424
|
+
boundKey.options.keyUp(boundKey, parameters);
|
|
403
425
|
}
|
|
404
426
|
resolve();
|
|
405
427
|
}, 200);
|
|
406
428
|
});
|
|
407
429
|
} else if (isDown) {
|
|
408
430
|
if (boundKey.options.keyDown) {
|
|
409
|
-
let parameters = boundKey.parameters;
|
|
431
|
+
let parameters = payload ?? boundKey.parameters;
|
|
410
432
|
if (typeof parameters === "function") {
|
|
411
433
|
parameters = parameters();
|
|
412
434
|
}
|
|
413
|
-
boundKey.options.keyDown(boundKey);
|
|
435
|
+
boundKey.options.keyDown(boundKey, parameters);
|
|
414
436
|
}
|
|
415
437
|
} else {
|
|
416
438
|
if (boundKey.options.keyUp) {
|
|
417
|
-
let parameters = boundKey.parameters;
|
|
439
|
+
let parameters = payload ?? boundKey.parameters;
|
|
418
440
|
if (typeof parameters === "function") {
|
|
419
441
|
parameters = parameters();
|
|
420
442
|
}
|
|
421
|
-
boundKey.options.keyUp(boundKey);
|
|
443
|
+
boundKey.options.keyUp(boundKey, parameters);
|
|
422
444
|
}
|
|
423
445
|
}
|
|
424
446
|
break;
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { ControlsBase, Controls } from "./ControlsBase";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Joystick directions reported by the Joystick component
|
|
5
|
+
*/
|
|
6
|
+
export type JoystickDirection =
|
|
7
|
+
| 'left'
|
|
8
|
+
| 'right'
|
|
9
|
+
| 'top'
|
|
10
|
+
| 'bottom'
|
|
11
|
+
| 'top_left'
|
|
12
|
+
| 'top_right'
|
|
13
|
+
| 'bottom_left'
|
|
14
|
+
| 'bottom_right';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Joystick change event payload
|
|
18
|
+
*/
|
|
19
|
+
export interface JoystickChangeEvent {
|
|
20
|
+
angle: number;
|
|
21
|
+
direction: JoystickDirection;
|
|
22
|
+
power: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Joystick configuration interface
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const joystickConfig: JoystickConfig = {
|
|
31
|
+
* enabled: true,
|
|
32
|
+
* directionMapping: {
|
|
33
|
+
* 'top': 'up',
|
|
34
|
+
* 'bottom': 'down',
|
|
35
|
+
* 'left': 'left',
|
|
36
|
+
* 'right': 'right',
|
|
37
|
+
* 'top_left': ['up', 'left'],
|
|
38
|
+
* 'top_right': ['up', 'right'],
|
|
39
|
+
* 'bottom_left': ['down', 'left'],
|
|
40
|
+
* 'bottom_right': ['down', 'right']
|
|
41
|
+
* },
|
|
42
|
+
* moveInterval: 50,
|
|
43
|
+
* threshold: 0.1
|
|
44
|
+
* };
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export interface JoystickConfig {
|
|
48
|
+
/** Whether joystick is enabled (default: true) */
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
/** Mapping of joystick direction names to control names (can be single string or array for diagonals) */
|
|
51
|
+
directionMapping?: {
|
|
52
|
+
[joystickDirection: string]: string | string[]; // e.g., 'top' -> 'up', 'top_left' -> ['up', 'left']
|
|
53
|
+
};
|
|
54
|
+
/** Interval in milliseconds for repeating movement actions (default: 50) */
|
|
55
|
+
moveInterval?: number;
|
|
56
|
+
/** Threshold for power value to trigger movement (default: 0.1) */
|
|
57
|
+
threshold?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default direction mapping
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_DIRECTION_MAPPING: { [direction: string]: string | string[] } = {
|
|
64
|
+
'top': 'up',
|
|
65
|
+
'bottom': 'down',
|
|
66
|
+
'left': 'left',
|
|
67
|
+
'right': 'right',
|
|
68
|
+
'top_left': ['up', 'left'],
|
|
69
|
+
'top_right': ['up', 'right'],
|
|
70
|
+
'bottom_left': ['down', 'left'],
|
|
71
|
+
'bottom_right': ['down', 'right']
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Joystick input controls implementation
|
|
76
|
+
*
|
|
77
|
+
* Handles joystick input events from the Joystick component and maps them to control actions.
|
|
78
|
+
* Supports directional movement with configurable mappings, including diagonal directions.
|
|
79
|
+
*
|
|
80
|
+
* The joystick controls work by receiving change events from a Joystick component instance.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const joystickControls = new JoystickControls();
|
|
85
|
+
* joystickControls.setInputs({
|
|
86
|
+
* up: {
|
|
87
|
+
* repeat: true,
|
|
88
|
+
* bind: 'up',
|
|
89
|
+
* keyDown() {
|
|
90
|
+
* console.log('Up pressed');
|
|
91
|
+
* }
|
|
92
|
+
* }
|
|
93
|
+
* });
|
|
94
|
+
* joystickControls.updateJoystickConfig({
|
|
95
|
+
* enabled: true,
|
|
96
|
+
* directionMapping: {
|
|
97
|
+
* 'top': 'up'
|
|
98
|
+
* }
|
|
99
|
+
* });
|
|
100
|
+
* joystickControls.start();
|
|
101
|
+
*
|
|
102
|
+
* // Later, when joystick changes:
|
|
103
|
+
* joystickControls.handleJoystickChange({ angle: 90, direction: Direction.TOP, power: 0.8 });
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export class JoystickControls extends ControlsBase {
|
|
107
|
+
private joystickEnabled: boolean = true;
|
|
108
|
+
private joystickConfig: JoystickConfig = {
|
|
109
|
+
enabled: true,
|
|
110
|
+
directionMapping: DEFAULT_DIRECTION_MAPPING,
|
|
111
|
+
moveInterval: 50,
|
|
112
|
+
threshold: 0.1
|
|
113
|
+
};
|
|
114
|
+
private joystickMoving: boolean = false;
|
|
115
|
+
private joystickDirections: { [direction: string]: boolean } = {};
|
|
116
|
+
private joystickLastUpdate: number = 0;
|
|
117
|
+
private joystickMoveInterval: any = null;
|
|
118
|
+
private currentPower: number = 0;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Setup joystick event listeners
|
|
122
|
+
* Note: Joystick events are handled via handleJoystickChange() method
|
|
123
|
+
*/
|
|
124
|
+
protected setupListeners(): void {
|
|
125
|
+
// Joystick events are handled externally via handleJoystickChange()
|
|
126
|
+
// This method is kept for consistency with ControlsBase interface
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Cleanup joystick intervals
|
|
131
|
+
*/
|
|
132
|
+
protected cleanup(): void {
|
|
133
|
+
if (this.joystickMoveInterval) {
|
|
134
|
+
clearInterval(this.joystickMoveInterval);
|
|
135
|
+
this.joystickMoveInterval = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Process joystick inputs each step
|
|
141
|
+
* Continuous actions are handled by the interval; no inactivity timeout here.
|
|
142
|
+
*/
|
|
143
|
+
protected preStep(): void {
|
|
144
|
+
if (this.stop) return;
|
|
145
|
+
// No-op: continuous movement is driven by processJoystickMovement interval
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle joystick change event
|
|
150
|
+
* Called by the Joystick component when its position changes
|
|
151
|
+
*
|
|
152
|
+
* @param event - Joystick change event containing angle, direction, and power
|
|
153
|
+
*/
|
|
154
|
+
handleJoystickChange(event: JoystickChangeEvent): void {
|
|
155
|
+
if (!this.joystickEnabled || !this.joystickConfig.enabled) return;
|
|
156
|
+
if (this.stop) return;
|
|
157
|
+
|
|
158
|
+
this.joystickLastUpdate = Date.now();
|
|
159
|
+
this.currentPower = event.power;
|
|
160
|
+
|
|
161
|
+
// Check threshold
|
|
162
|
+
if (event.power < (this.joystickConfig.threshold || 0.1)) {
|
|
163
|
+
// Power too low, stop all movements
|
|
164
|
+
this.stopAllMovements();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const directionMapping = this.joystickConfig.directionMapping || DEFAULT_DIRECTION_MAPPING;
|
|
169
|
+
const directionKey = event.direction;
|
|
170
|
+
const mappedControls = directionMapping[directionKey];
|
|
171
|
+
|
|
172
|
+
if (!mappedControls) {
|
|
173
|
+
// No mapping for this direction, stop all movements
|
|
174
|
+
this.stopAllMovements();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Convert to array if single string
|
|
179
|
+
const controlNames = Array.isArray(mappedControls) ? mappedControls : [mappedControls];
|
|
180
|
+
|
|
181
|
+
// Determine which directions to activate and deactivate
|
|
182
|
+
const previousDirections = this.joystickDirections;
|
|
183
|
+
const newDirections: { [dir: string]: boolean } = {};
|
|
184
|
+
controlNames.forEach(controlName => {
|
|
185
|
+
newDirections[controlName] = true;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Deactivate directions that are no longer active
|
|
189
|
+
const allDirections = new Set([
|
|
190
|
+
...Object.keys(this.joystickDirections),
|
|
191
|
+
...Object.keys(newDirections)
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
for (const dir of allDirections) {
|
|
195
|
+
const wasActive = this.joystickDirections[dir];
|
|
196
|
+
const shouldBeActive = newDirections[dir] || false;
|
|
197
|
+
|
|
198
|
+
if (wasActive && !shouldBeActive) {
|
|
199
|
+
// Deactivate this direction
|
|
200
|
+
this.applyControl(dir, false).catch(() => {
|
|
201
|
+
// Ignore errors
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Update active directions
|
|
207
|
+
this.joystickDirections = { ...newDirections };
|
|
208
|
+
this.joystickMoving = true;
|
|
209
|
+
|
|
210
|
+
// Activate new directions
|
|
211
|
+
const directionsToActivate = controlNames.filter((name) => !previousDirections[name]);
|
|
212
|
+
for (const controlName of directionsToActivate) {
|
|
213
|
+
this.applyControl(controlName, true, { power: event.power }).catch(() => {
|
|
214
|
+
// Ignore errors
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Start movement interval if not already running
|
|
219
|
+
if (!this.joystickMoveInterval) {
|
|
220
|
+
this.joystickMoveInterval = setInterval(() => {
|
|
221
|
+
this.processJoystickMovement();
|
|
222
|
+
}, this.joystickConfig.moveInterval || 50);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle joystick start event
|
|
228
|
+
* Called when user starts interacting with the joystick
|
|
229
|
+
*/
|
|
230
|
+
handleJoystickStart(): void {
|
|
231
|
+
if (!this.joystickEnabled || !this.joystickConfig.enabled) return;
|
|
232
|
+
if (this.stop) return;
|
|
233
|
+
// Start event doesn't need special handling, change event will handle activation
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Handle joystick end event
|
|
238
|
+
* Called when user stops interacting with the joystick
|
|
239
|
+
*/
|
|
240
|
+
handleJoystickEnd(): void {
|
|
241
|
+
if (!this.joystickEnabled || !this.joystickConfig.enabled) return;
|
|
242
|
+
this.stopAllMovements();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Stop all active joystick movements
|
|
247
|
+
*/
|
|
248
|
+
private stopAllMovements(): void {
|
|
249
|
+
if (this.joystickMoveInterval) {
|
|
250
|
+
clearInterval(this.joystickMoveInterval);
|
|
251
|
+
this.joystickMoveInterval = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const allDirections = Object.keys(this.joystickDirections);
|
|
255
|
+
for (const dir of allDirections) {
|
|
256
|
+
this.applyControl(dir, false).catch(() => {
|
|
257
|
+
// Ignore errors
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.joystickDirections = {};
|
|
262
|
+
this.joystickMoving = false;
|
|
263
|
+
this.currentPower = 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Process continuous joystick movement
|
|
268
|
+
* Called at intervals to repeat movement actions while joystick is active
|
|
269
|
+
*/
|
|
270
|
+
private processJoystickMovement(): void {
|
|
271
|
+
if (!this.joystickMoving) return;
|
|
272
|
+
if (this.stop) return;
|
|
273
|
+
|
|
274
|
+
for (const direction in this.joystickDirections) {
|
|
275
|
+
if (this.joystickDirections[direction]) {
|
|
276
|
+
this.applyControl(direction, true, { power: this.currentPower }).catch(() => {
|
|
277
|
+
// Ignore errors
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Update joystick configuration
|
|
285
|
+
* Merges provided config with defaults
|
|
286
|
+
*
|
|
287
|
+
* @param config - Partial joystick configuration
|
|
288
|
+
*/
|
|
289
|
+
updateJoystickConfig(config: Partial<JoystickConfig>): void {
|
|
290
|
+
this.joystickConfig = {
|
|
291
|
+
enabled: config.enabled !== undefined ? config.enabled : true,
|
|
292
|
+
directionMapping: config.directionMapping || DEFAULT_DIRECTION_MAPPING,
|
|
293
|
+
moveInterval: config.moveInterval || 50,
|
|
294
|
+
threshold: config.threshold || 0.1
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Extract joystick config from controls configuration and update
|
|
300
|
+
*
|
|
301
|
+
* @param inputs - Controls configuration that may contain a 'joystick' property
|
|
302
|
+
*/
|
|
303
|
+
extractJoystickConfig(inputs: Controls & { joystick?: JoystickConfig }): void {
|
|
304
|
+
if (inputs.joystick) {
|
|
305
|
+
this.updateJoystickConfig(inputs.joystick);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the current joystick configuration
|
|
311
|
+
*
|
|
312
|
+
* @returns The joystick configuration object
|
|
313
|
+
*/
|
|
314
|
+
getJoystickConfig(): JoystickConfig {
|
|
315
|
+
return this.joystickConfig;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Apply a control action programmatically
|
|
320
|
+
* Uses the bound controls to trigger actions
|
|
321
|
+
*
|
|
322
|
+
* @param controlName - Name of the control
|
|
323
|
+
* @param isDown - Whether the control is pressed (true) or released (false)
|
|
324
|
+
* @returns Promise that resolves when the action is complete
|
|
325
|
+
*/
|
|
326
|
+
async applyControl(controlName: string | number, isDown?: boolean, payload?: any): Promise<void> {
|
|
327
|
+
const control = this._controlsOptions[controlName];
|
|
328
|
+
if (!control) return;
|
|
329
|
+
|
|
330
|
+
// Find the bound key for this control
|
|
331
|
+
const boundKeys = Object.keys(this.boundKeys);
|
|
332
|
+
for (const keyName of boundKeys) {
|
|
333
|
+
const boundKey = this.boundKeys[keyName];
|
|
334
|
+
if (boundKey.actionName === String(controlName)) {
|
|
335
|
+
// Execute the control callback
|
|
336
|
+
if (isDown === undefined) {
|
|
337
|
+
// Press and release (simulate button press)
|
|
338
|
+
if (boundKey.options.keyDown) {
|
|
339
|
+
let parameters = payload ?? boundKey.parameters;
|
|
340
|
+
if (typeof parameters === "function") {
|
|
341
|
+
parameters = parameters();
|
|
342
|
+
}
|
|
343
|
+
boundKey.options.keyDown(boundKey, parameters);
|
|
344
|
+
}
|
|
345
|
+
// Release after a short delay (similar to keyboard)
|
|
346
|
+
return new Promise((resolve) => {
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
if (boundKey.options.keyUp) {
|
|
349
|
+
let parameters = payload ?? boundKey.parameters;
|
|
350
|
+
if (typeof parameters === "function") {
|
|
351
|
+
parameters = parameters();
|
|
352
|
+
}
|
|
353
|
+
boundKey.options.keyUp(boundKey, parameters);
|
|
354
|
+
}
|
|
355
|
+
resolve();
|
|
356
|
+
}, 200);
|
|
357
|
+
});
|
|
358
|
+
} else if (isDown) {
|
|
359
|
+
if (boundKey.options.keyDown) {
|
|
360
|
+
let parameters = payload ?? boundKey.parameters;
|
|
361
|
+
if (typeof parameters === "function") {
|
|
362
|
+
parameters = parameters();
|
|
363
|
+
}
|
|
364
|
+
boundKey.options.keyDown(boundKey, parameters);
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
if (boundKey.options.keyUp) {
|
|
368
|
+
let parameters = payload ?? boundKey.parameters;
|
|
369
|
+
if (typeof parameters === "function") {
|
|
370
|
+
parameters = parameters();
|
|
371
|
+
}
|
|
372
|
+
boundKey.options.keyUp(boundKey, parameters);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Override setInputs to extract joystick config
|
|
382
|
+
*/
|
|
383
|
+
setInputs(inputs: Controls & { joystick?: JoystickConfig }): void {
|
|
384
|
+
super.setInputs(inputs);
|
|
385
|
+
this.extractJoystickConfig(inputs);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Check if joystick is currently active
|
|
390
|
+
*
|
|
391
|
+
* @returns true if joystick is moving, false otherwise
|
|
392
|
+
*/
|
|
393
|
+
isActive(): boolean {
|
|
394
|
+
return this.joystickMoving;
|
|
395
|
+
}
|
|
396
|
+
}
|
package/src/directives/index.ts
CHANGED