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.
Files changed (45) hide show
  1. package/dist/{DebugRenderer-CTWPthRt.js → DebugRenderer-Rrw9FlTd.js} +2 -2
  2. package/dist/{DebugRenderer-CTWPthRt.js.map → DebugRenderer-Rrw9FlTd.js.map} +1 -1
  3. package/dist/components/Button.d.ts +50 -3
  4. package/dist/components/Button.d.ts.map +1 -1
  5. package/dist/components/Canvas.d.ts.map +1 -1
  6. package/dist/components/Joystick.d.ts +36 -0
  7. package/dist/components/Joystick.d.ts.map +1 -0
  8. package/dist/components/Sprite.d.ts +2 -0
  9. package/dist/components/Sprite.d.ts.map +1 -1
  10. package/dist/components/index.d.ts +1 -0
  11. package/dist/components/index.d.ts.map +1 -1
  12. package/dist/components/types/Spritesheet.d.ts +0 -118
  13. package/dist/components/types/Spritesheet.d.ts.map +1 -1
  14. package/dist/directives/Controls.d.ts +16 -7
  15. package/dist/directives/Controls.d.ts.map +1 -1
  16. package/dist/directives/GamepadControls.d.ts +3 -1
  17. package/dist/directives/GamepadControls.d.ts.map +1 -1
  18. package/dist/directives/JoystickControls.d.ts +172 -0
  19. package/dist/directives/JoystickControls.d.ts.map +1 -0
  20. package/dist/directives/index.d.ts +1 -0
  21. package/dist/directives/index.d.ts.map +1 -1
  22. package/dist/engine/reactive.d.ts.map +1 -1
  23. package/dist/engine/signal.d.ts.map +1 -1
  24. package/dist/{index-BqwprEPH.js → index-BQ99FClW.js} +6057 -5433
  25. package/dist/index-BQ99FClW.js.map +1 -0
  26. package/dist/index.global.js +7 -7
  27. package/dist/index.global.js.map +1 -1
  28. package/dist/index.js +59 -57
  29. package/dist/utils/GlobalAssetLoader.d.ts +141 -0
  30. package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
  31. package/package.json +1 -1
  32. package/src/components/Button.ts +168 -41
  33. package/src/components/Canvas.ts +3 -0
  34. package/src/components/Joystick.ts +361 -0
  35. package/src/components/Sprite.ts +82 -16
  36. package/src/components/index.ts +2 -1
  37. package/src/components/types/Spritesheet.ts +0 -118
  38. package/src/directives/Controls.ts +42 -8
  39. package/src/directives/GamepadControls.ts +40 -18
  40. package/src/directives/JoystickControls.ts +396 -0
  41. package/src/directives/index.ts +1 -0
  42. package/src/engine/reactive.ts +362 -242
  43. package/src/engine/signal.ts +8 -2
  44. package/src/utils/GlobalAssetLoader.ts +257 -0
  45. 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 gamepad input systems
9
+ * Controls directive that coordinates keyboard, gamepad, and joystick input systems
9
10
  *
10
- * This directive automatically activates both keyboard and gamepad controls when available.
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
- * Both systems share the same control configuration and can work simultaneously.
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 gamepad controls if available
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 both keyboard and gamepad input processing
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 both keyboard and gamepad input processing
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
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './ControlsBase'
2
2
  export * from './KeyboardControls'
3
3
  export * from './GamepadControls'
4
+ export * from './JoystickControls'
4
5
  export * from './Controls'
5
6
  export * from './Scheduler'
6
7
  export * from './ViewportFollow'