@skewedaspect/sage 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/Readme.md +53 -0
  3. package/dist/classes/bindings/toggle.d.ts +122 -0
  4. package/dist/classes/bindings/trigger.d.ts +79 -0
  5. package/dist/classes/bindings/value.d.ts +104 -0
  6. package/dist/classes/entity.d.ts +83 -0
  7. package/dist/classes/eventBus.d.ts +94 -0
  8. package/dist/classes/gameEngine.d.ts +57 -0
  9. package/dist/classes/input/gamepad.d.ts +94 -0
  10. package/dist/classes/input/keyboard.d.ts +66 -0
  11. package/dist/classes/input/mouse.d.ts +80 -0
  12. package/dist/classes/input/readers/gamepad.d.ts +77 -0
  13. package/dist/classes/input/readers/keyboard.d.ts +60 -0
  14. package/dist/classes/input/readers/mouse.d.ts +45 -0
  15. package/dist/classes/loggers/consoleBackend.d.ts +29 -0
  16. package/dist/classes/loggers/nullBackend.d.ts +14 -0
  17. package/dist/engines/scene.d.ts +11 -0
  18. package/dist/interfaces/action.d.ts +20 -0
  19. package/dist/interfaces/binding.d.ts +144 -0
  20. package/dist/interfaces/entity.d.ts +9 -0
  21. package/dist/interfaces/game.d.ts +26 -0
  22. package/dist/interfaces/input.d.ts +181 -0
  23. package/dist/interfaces/logger.d.ts +88 -0
  24. package/dist/managers/binding.d.ts +185 -0
  25. package/dist/managers/entity.d.ts +70 -0
  26. package/dist/managers/game.d.ts +20 -0
  27. package/dist/managers/input.d.ts +56 -0
  28. package/dist/managers/level.d.ts +55 -0
  29. package/dist/sage.d.ts +20 -0
  30. package/dist/sage.es.js +2208 -0
  31. package/dist/sage.es.js.map +1 -0
  32. package/dist/sage.umd.js +2 -0
  33. package/dist/sage.umd.js.map +1 -0
  34. package/dist/utils/capabilities.d.ts +2 -0
  35. package/dist/utils/graphics.d.ts +10 -0
  36. package/dist/utils/logger.d.ts +66 -0
  37. package/dist/utils/physics.d.ts +2 -0
  38. package/dist/utils/version.d.ts +5 -0
  39. package/docs/architecture.md +129 -0
  40. package/docs/behaviors.md +706 -0
  41. package/docs/binding_system.md +820 -0
  42. package/docs/design/input.md +86 -0
  43. package/docs/entity_system.md +538 -0
  44. package/docs/eventbus.md +225 -0
  45. package/docs/getting_started.md +264 -0
  46. package/docs/images/sage_logo.png +0 -0
  47. package/docs/images/sage_logo_shape.png +0 -0
  48. package/docs/overview.md +38 -0
  49. package/docs/physics_system.md +686 -0
  50. package/docs/scene_system.md +513 -0
  51. package/package.json +69 -0
  52. package/src/classes/bindings/toggle.ts +261 -0
  53. package/src/classes/bindings/trigger.ts +211 -0
  54. package/src/classes/bindings/value.ts +227 -0
  55. package/src/classes/entity.ts +256 -0
  56. package/src/classes/eventBus.ts +259 -0
  57. package/src/classes/gameEngine.ts +125 -0
  58. package/src/classes/input/gamepad.ts +388 -0
  59. package/src/classes/input/keyboard.ts +189 -0
  60. package/src/classes/input/mouse.ts +276 -0
  61. package/src/classes/input/readers/gamepad.ts +179 -0
  62. package/src/classes/input/readers/keyboard.ts +123 -0
  63. package/src/classes/input/readers/mouse.ts +133 -0
  64. package/src/classes/loggers/consoleBackend.ts +135 -0
  65. package/src/classes/loggers/nullBackend.ts +51 -0
  66. package/src/engines/scene.ts +112 -0
  67. package/src/images/sage_logo.svg +172 -0
  68. package/src/images/sage_logo_shape.svg +146 -0
  69. package/src/interfaces/action.ts +30 -0
  70. package/src/interfaces/binding.ts +191 -0
  71. package/src/interfaces/entity.ts +21 -0
  72. package/src/interfaces/game.ts +44 -0
  73. package/src/interfaces/input.ts +221 -0
  74. package/src/interfaces/logger.ts +118 -0
  75. package/src/managers/binding.ts +729 -0
  76. package/src/managers/entity.ts +252 -0
  77. package/src/managers/game.ts +111 -0
  78. package/src/managers/input.ts +233 -0
  79. package/src/managers/level.ts +261 -0
  80. package/src/sage.ts +119 -0
  81. package/src/types/global.d.ts +11 -0
  82. package/src/utils/capabilities.ts +16 -0
  83. package/src/utils/graphics.ts +148 -0
  84. package/src/utils/logger.ts +225 -0
  85. package/src/utils/physics.ts +16 -0
  86. package/src/utils/version.ts +11 -0
@@ -0,0 +1,820 @@
1
+ # Binding System
2
+
3
+ ## Overview
4
+
5
+ The SAGE Binding System is a flexible and powerful approach to handling user input in your games. It creates an abstraction layer between physical input devices (keyboard, mouse, gamepad) and the game actions they trigger, making your code more modular and easier to maintain.
6
+
7
+ ## Core Concepts
8
+
9
+ ### Actions
10
+
11
+ Actions are the "what" in your game's input system - they represent game events that can be triggered by user input. Actions are divided into two types:
12
+
13
+ - **Digital Actions**: Boolean (on/off) values, like jumping or interacting
14
+ - **Analog Actions**: Numeric values (typically ranging from -1 to 1 or 0 to 1), like throttle control or camera rotation
15
+
16
+ Actions are defined by a name and type, and analog actions can optionally specify minimum and maximum values:
17
+
18
+ ```typescript
19
+ // Digital action example
20
+ {
21
+ name: 'jump',
22
+ type: 'digital'
23
+ }
24
+
25
+ // Analog action example
26
+ {
27
+ name: 'throttle',
28
+ type: 'analog',
29
+ minValue: 0, // No throttle
30
+ maxValue: 1 // Full throttle
31
+ }
32
+ ```
33
+
34
+ ### Bindings
35
+
36
+ Bindings are the "how" in your input system - they connect physical inputs to actions. SAGE supports multiple binding types that process input differently:
37
+
38
+ - **Trigger Bindings**: Fire once when a button is pressed or released
39
+ - **Toggle Bindings**: Toggle between on/off states each time a button is pressed (like crouch)
40
+ - **Value Bindings**: Continuously translate input values (like joystick position) to action values
41
+
42
+ Each binding connects a specific input from a device (like a keyboard key or gamepad button) to an action, with options to customize how the input translates to the action.
43
+
44
+ ### Contexts
45
+
46
+ Contexts allow you to organize bindings into groups that can be activated or deactivated together. This is particularly useful for handling different input modes, such as:
47
+
48
+ - Gameplay controls
49
+ - Menu navigation
50
+ - Vehicle controls
51
+ - Dialog interactions
52
+
53
+ Contexts can be exclusive (only one active at a time) or non-exclusive (multiple can be active simultaneously).
54
+
55
+ ## Basic Setup
56
+
57
+ ### Initializing the Binding System
58
+
59
+ The binding system relies on the SAGE event bus and works alongside the input manager:
60
+
61
+ ```typescript
62
+ import { GameEventBus } from './src/classes/eventBus.ts';
63
+ import { BindingManager } from './src/managers/binding.ts';
64
+
65
+ // Create event bus and binding manager
66
+ const eventBus = new GameEventBus();
67
+ const bindingManager = new BindingManager(eventBus);
68
+ ```
69
+
70
+ ### Registering Actions
71
+
72
+ Before you can create bindings, you need to register the actions they will trigger:
73
+
74
+ ```typescript
75
+ // Register a digital action
76
+ bindingManager.registerAction({
77
+ name: 'quit',
78
+ type: 'digital'
79
+ });
80
+
81
+ // Register an analog action
82
+ bindingManager.registerAction({
83
+ name: 'throttle',
84
+ type: 'analog',
85
+ minValue: 0, // No throttle
86
+ maxValue: 1 // Full throttle
87
+ });
88
+ ```
89
+
90
+ ### Creating and Registering Bindings
91
+
92
+ Here's how to create a binding that triggers the "quit" action when the Escape key is pressed:
93
+
94
+ ```typescript
95
+ import { KeyboardValueReader } from './src/classes/input/readers/keyboard.ts';
96
+ import { TriggerBinding } from './src/classes/bindings/trigger.ts';
97
+
98
+ // Create a trigger binding for the Escape key
99
+ const quitBinding = new TriggerBinding(
100
+ { name: 'quit', type: 'digital' }, // The action
101
+ 'keyboard-main', // Device ID
102
+ new KeyboardValueReader('Escape'), // Input source
103
+ { edgeMode: 'rising' } // Options (trigger on key press)
104
+ );
105
+
106
+ // Register the binding
107
+ bindingManager.registerBinding(quitBinding);
108
+ ```
109
+
110
+ ### Using Binding Definitions
111
+
112
+ You can also create bindings using definitions, which is useful for creating bindings programmatically or from configuration:
113
+
114
+ ```typescript
115
+ // Define a binding definition object
116
+ const jumpBindingDefinition = {
117
+ type: 'trigger',
118
+ action: 'jump', // Action name as a string
119
+ deviceID: 'keyboard-main',
120
+ reader: {
121
+ type: 'keyboard',
122
+ sourceType: 'key',
123
+ sourceKey: 'Space'
124
+ },
125
+ options: {
126
+ edgeMode: 'rising'
127
+ }
128
+ };
129
+
130
+ // Register the binding from the definition
131
+ bindingManager.registerBindingFromDefinition(jumpBindingDefinition);
132
+ ```
133
+
134
+ ### Handling Actions
135
+
136
+ Once actions and bindings are set up, you listen for action events on the event bus:
137
+
138
+ ```typescript
139
+ // Subscribe to the quit action
140
+ eventBus.subscribe('action:quit', (event) => {
141
+ console.log('Quit action triggered!');
142
+ game.quit();
143
+ });
144
+ ```
145
+
146
+ ## Binding Types in Detail
147
+
148
+ ### Trigger Binding
149
+
150
+ The trigger binding fires a one-time event when a button state changes:
151
+
152
+ ```typescript
153
+ // Create a trigger binding for the Space key to jump
154
+ const jumpBinding = new TriggerBinding(
155
+ { name: 'jump', type: 'digital' },
156
+ 'keyboard-main',
157
+ new KeyboardValueReader('Space'),
158
+ {
159
+ edgeMode: 'rising', // 'rising' = when pressed, 'falling' = when released
160
+ threshold: 0.5 // For analog inputs, the activation threshold
161
+ }
162
+ );
163
+ ```
164
+
165
+ ### Toggle Binding
166
+
167
+ The toggle binding switches between on and off states with each activation:
168
+
169
+ ```typescript
170
+ // Create a toggle binding for the C key to toggle crouch
171
+ const crouchBinding = new ToggleBinding(
172
+ { name: 'crouch', type: 'digital' },
173
+ 'keyboard-main',
174
+ new KeyboardValueReader('KeyC'),
175
+ {
176
+ initialState: false, // Start in "off" state
177
+ onValue: true, // Value when toggled on
178
+ offValue: false // Value when toggled off
179
+ }
180
+ );
181
+ ```
182
+
183
+ ### Value Binding
184
+
185
+ The value binding continuously passes input values to an action:
186
+
187
+ ```typescript
188
+ // Create a value binding for the right trigger for throttle control
189
+ const throttleBinding = new ValueBinding(
190
+ { name: 'throttle', type: 'analog', minValue: 0, maxValue: 1 },
191
+ 'gamepad-0',
192
+ new GamepadValueReader('button', '7'), // Right trigger
193
+ {
194
+ scale: 1.0, // Full value when pressed
195
+ deadzone: 0.05, // Ignore small values
196
+ emitOnChange: true // Only emit when value changes
197
+ }
198
+ );
199
+ ```
200
+
201
+ ## Working with Contexts
202
+
203
+ ### Registering Contexts
204
+
205
+ ```typescript
206
+ // Register contexts for different game states
207
+ bindingManager.registerContext('gameplay', true); // true = exclusive
208
+ bindingManager.registerContext('menu', true); // also exclusive
209
+ bindingManager.registerContext('always', false); // false = non-exclusive
210
+ ```
211
+
212
+ ### Assigning Bindings to Contexts
213
+
214
+ When creating bindings, you can specify their context:
215
+
216
+ ```typescript
217
+ // Create a binding that only works in the gameplay context
218
+ const jumpBinding = new TriggerBinding(
219
+ { name: 'jump', type: 'digital' },
220
+ 'keyboard-main',
221
+ new KeyboardValueReader('Space'),
222
+ {
223
+ context: 'gameplay',
224
+ edgeMode: 'rising'
225
+ }
226
+ );
227
+ ```
228
+
229
+ ### Activating and Deactivating Contexts
230
+
231
+ ```typescript
232
+ // Start in gameplay mode
233
+ bindingManager.activateContext('gameplay');
234
+
235
+ // When opening a menu, switch to menu mode
236
+ bindingManager.activateContext('menu'); // This automatically deactivates 'gameplay' since both are exclusive
237
+
238
+ // Return to gameplay
239
+ bindingManager.activateContext('gameplay');
240
+
241
+ // Deactivate a specific context
242
+ bindingManager.deactivateContext('menu');
243
+ ```
244
+
245
+ ## Implementing Character Movement with WASD Controls
246
+
247
+ Here's a complete example of implementing WASD movement controls using digital actions:
248
+
249
+ ```typescript
250
+ // 1. Register movement actions (using digital actions for directional movement)
251
+ bindingManager.registerAction({
252
+ name: 'move_forward',
253
+ type: 'digital'
254
+ });
255
+
256
+ bindingManager.registerAction({
257
+ name: 'move_backward',
258
+ type: 'digital'
259
+ });
260
+
261
+ bindingManager.registerAction({
262
+ name: 'move_left',
263
+ type: 'digital'
264
+ });
265
+
266
+ bindingManager.registerAction({
267
+ name: 'move_right',
268
+ type: 'digital'
269
+ });
270
+
271
+ // 2. Create WASD keyboard bindings using trigger bindings
272
+ // Trigger bindings fire once when pressed/released, which is appropriate for digital actions
273
+ const wBinding = new TriggerBinding(
274
+ bindingManager.getAction('move_forward'),
275
+ 'keyboard-main',
276
+ new KeyboardValueReader('KeyW'),
277
+ { edgeMode: 'both' } // Fire on both press and release
278
+ );
279
+
280
+ const sBinding = new TriggerBinding(
281
+ bindingManager.getAction('move_backward'),
282
+ 'keyboard-main',
283
+ new KeyboardValueReader('KeyS'),
284
+ { edgeMode: 'both' }
285
+ );
286
+
287
+ const aBinding = new TriggerBinding(
288
+ bindingManager.getAction('move_left'),
289
+ 'keyboard-main',
290
+ new KeyboardValueReader('KeyA'),
291
+ { edgeMode: 'both' }
292
+ );
293
+
294
+ const dBinding = new TriggerBinding(
295
+ bindingManager.getAction('move_right'),
296
+ 'keyboard-main',
297
+ new KeyboardValueReader('KeyD'),
298
+ { edgeMode: 'both' }
299
+ );
300
+
301
+ // 3. Register all bindings
302
+ bindingManager.registerBinding(wBinding);
303
+ bindingManager.registerBinding(sBinding);
304
+ bindingManager.registerBinding(aBinding);
305
+ bindingManager.registerBinding(dBinding);
306
+
307
+ // 4. Handle the movement actions
308
+ eventBus.subscribe('action:move_forward', (event) => {
309
+ // True when key is pressed, false when released
310
+ if (event.payload.value === true) {
311
+ player.velocity.z = -player.speed; // Move forward (negative Z in many 3D systems)
312
+ } else {
313
+ // Only zero out velocity if player isn't also moving backward
314
+ if (!player.isMovingBackward) {
315
+ player.velocity.z = 0;
316
+ }
317
+ }
318
+ });
319
+
320
+ // Similar handlers for the other actions
321
+ // ...existing code...
322
+ ```
323
+
324
+ ### Alternative: Using Binding Definitions for WASD Controls
325
+
326
+ You can also set up WASD controls using binding definitions, which is more concise:
327
+
328
+ ```typescript
329
+ // Define all actions at once
330
+ const movementActions = [
331
+ { name: 'move_forward', type: 'digital' },
332
+ { name: 'move_backward', type: 'digital' },
333
+ { name: 'move_left', type: 'digital' },
334
+ { name: 'move_right', type: 'digital' }
335
+ ];
336
+
337
+ // Register all actions
338
+ movementActions.forEach(action => bindingManager.registerAction(action));
339
+
340
+ // Define all bindings at once using binding definitions
341
+ const wasdBindings = [
342
+ {
343
+ type: 'trigger', // Use trigger binding for digital actions
344
+ action: 'move_forward',
345
+ deviceID: 'keyboard-main',
346
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyW' },
347
+ options: { edgeMode: 'both' } // Fire on both press and release
348
+ },
349
+ {
350
+ type: 'trigger',
351
+ action: 'move_backward',
352
+ deviceID: 'keyboard-main',
353
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyS' },
354
+ options: { edgeMode: 'both' }
355
+ },
356
+ {
357
+ type: 'trigger',
358
+ action: 'move_left',
359
+ deviceID: 'keyboard-main',
360
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyA' },
361
+ options: { edgeMode: 'both' }
362
+ },
363
+ {
364
+ type: 'trigger',
365
+ action: 'move_right',
366
+ deviceID: 'keyboard-main',
367
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyD' },
368
+ options: { edgeMode: 'both' }
369
+ }
370
+ ];
371
+
372
+ // Register all bindings in one go
373
+ wasdBindings.forEach(binding => bindingManager.registerBinding(binding));
374
+ ```
375
+
376
+ ## Integration with Entity System
377
+
378
+ The entity system in SAGE allows you to define required actions at the entity level, creating a clean way to associate input controls with specific game objects.
379
+
380
+ ### Defining Entity Actions
381
+
382
+ When defining an entity, you can specify the actions it requires:
383
+
384
+ ```typescript
385
+ import { GameEntityDefinition } from './src/interfaces/entity.ts';
386
+
387
+ // Define a player entity with movement behaviors
388
+ const playerEntityDefinition: GameEntityDefinition = {
389
+ type: 'player',
390
+ defaultState: {
391
+ position: { x: 0, y: 0, z: 0 },
392
+ velocity: { x: 0, y: 0, z: 0 },
393
+ speed: 5,
394
+ isMovingForward: false,
395
+ isMovingBackward: false,
396
+ isMovingLeft: false,
397
+ isMovingRight: false
398
+ },
399
+ behaviors: [
400
+ PlayerMovementBehavior,
401
+ PlayerAnimationBehavior
402
+ ],
403
+ // Define actions needed by this entity (digital actions for directional movement)
404
+ actions: [
405
+ { name: 'move_forward', type: 'digital' },
406
+ { name: 'move_backward', type: 'digital' },
407
+ { name: 'move_left', type: 'digital' },
408
+ { name: 'move_right', type: 'digital' },
409
+ { name: 'jump', type: 'digital' }
410
+ ]
411
+ };
412
+ ```
413
+
414
+ ### Creating Player Movement Behavior
415
+
416
+ The entity system works with behaviors that respond to action events:
417
+
418
+ ```typescript
419
+ import { GameEntityBehavior } from './src/classes/entity.ts';
420
+
421
+ class PlayerMovementBehavior extends GameEntityBehavior<{
422
+ position: { x: number, y: number, z: number },
423
+ velocity: { x: number, y: number, z: number },
424
+ speed: number,
425
+ isMovingForward: boolean,
426
+ isMovingBackward: boolean,
427
+ isMovingLeft: boolean,
428
+ isMovingRight: boolean
429
+ }> {
430
+ initialize(entity) {
431
+ // Subscribe to movement actions
432
+ this.eventBus.subscribe('action:move_forward', (event) => {
433
+ const isActive = event.payload.value === true;
434
+ entity.state.isMovingForward = isActive;
435
+
436
+ if (isActive) {
437
+ entity.state.velocity.z = -entity.state.speed;
438
+ } else if (!entity.state.isMovingBackward) {
439
+ entity.state.velocity.z = 0;
440
+ }
441
+ });
442
+
443
+ this.eventBus.subscribe('action:move_backward', (event) => {
444
+ const isActive = event.payload.value === true;
445
+ entity.state.isMovingBackward = isActive;
446
+
447
+ if (isActive) {
448
+ entity.state.velocity.z = entity.state.speed;
449
+ } else if (!entity.state.isMovingForward) {
450
+ entity.state.velocity.z = 0;
451
+ }
452
+ });
453
+
454
+ this.eventBus.subscribe('action:move_left', (event) => {
455
+ const isActive = event.payload.value === true;
456
+ entity.state.isMovingLeft = isActive;
457
+
458
+ if (isActive) {
459
+ entity.state.velocity.x = -entity.state.speed;
460
+ } else if (!entity.state.isMovingRight) {
461
+ entity.state.velocity.x = 0;
462
+ }
463
+ });
464
+
465
+ this.eventBus.subscribe('action:move_right', (event) => {
466
+ const isActive = event.payload.value === true;
467
+ entity.state.isMovingRight = isActive;
468
+
469
+ if (isActive) {
470
+ entity.state.velocity.x = entity.state.speed;
471
+ } else if (!entity.state.isMovingLeft) {
472
+ entity.state.velocity.x = 0;
473
+ }
474
+ });
475
+
476
+ this.eventBus.subscribe('action:jump', () => {
477
+ // Implement jump logic
478
+ if (entity.state.onGround) {
479
+ entity.state.velocity.y = entity.state.jumpForce;
480
+ }
481
+ });
482
+ }
483
+
484
+ update(entity, deltaTime) {
485
+ // Apply velocity to position
486
+ entity.state.position.x += entity.state.velocity.x * deltaTime;
487
+ entity.state.position.y += entity.state.velocity.y * deltaTime;
488
+ entity.state.position.z += entity.state.velocity.z * deltaTime;
489
+
490
+ // Apply physics like gravity, friction, etc.
491
+ }
492
+ }
493
+ ```
494
+
495
+ ## Saving and Loading Bindings
496
+
497
+ The binding system provides methods to export and import configurations, allowing for customizable controls that persist between game sessions.
498
+
499
+ ### Exporting Configuration
500
+
501
+ ```typescript
502
+ // Get current configuration as a serializable object
503
+ const savedConfig = bindingManager.exportConfiguration();
504
+
505
+ // You can now save this object however you prefer
506
+ localStorage.setItem('gameControls', JSON.stringify(savedConfig));
507
+ // or
508
+ saveToFile('controls.json', JSON.stringify(savedConfig));
509
+ // or
510
+ await api.saveUserPreferences(savedConfig);
511
+ ```
512
+
513
+ ### Importing Configuration
514
+
515
+ ```typescript
516
+ // Load the configuration from your storage
517
+ const loadedConfig = JSON.parse(localStorage.getItem('gameControls'));
518
+
519
+ // Apply the configuration to the binding manager
520
+ bindingManager.importConfiguration(loadedConfig);
521
+ ```
522
+
523
+ ### Complete Example: Saving and Loading Controls
524
+
525
+ Here's a complete example of how to implement customizable controls with saving/loading:
526
+
527
+ ```typescript
528
+ class GameControlsManager {
529
+ private bindingManager: BindingManager;
530
+
531
+ constructor(eventBus: GameEventBus) {
532
+ this.bindingManager = new BindingManager(eventBus);
533
+ this.setupDefaultControls();
534
+ }
535
+
536
+ // Set up default control scheme
537
+ setupDefaultControls() {
538
+ // Register default actions
539
+ const defaultActions = [
540
+ { name: 'move_forward', type: 'digital' },
541
+ { name: 'move_backward', type: 'digital' },
542
+ { name: 'move_left', type: 'digital' },
543
+ { name: 'move_right', type: 'digital' },
544
+ { name: 'jump', type: 'digital' },
545
+ { name: 'interact', type: 'digital' },
546
+ { name: 'pause', type: 'digital' }
547
+ ];
548
+
549
+ // Register all default actions
550
+ defaultActions.forEach(action => this.bindingManager.registerAction(action));
551
+
552
+ // Create contexts
553
+ this.bindingManager.registerContext('gameplay', true);
554
+ this.bindingManager.registerContext('menu', true);
555
+
556
+ // Define default bindings
557
+ const defaultBindings = [
558
+ // Movement - WASD
559
+ {
560
+ type: 'value',
561
+ action: 'move_forward',
562
+ deviceID: 'keyboard-main',
563
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyW' },
564
+ context: 'gameplay',
565
+ options: { onValue: true, offValue: false }
566
+ },
567
+ {
568
+ type: 'value',
569
+ action: 'move_backward',
570
+ deviceID: 'keyboard-main',
571
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyS' },
572
+ context: 'gameplay',
573
+ options: { onValue: true, offValue: false }
574
+ },
575
+ {
576
+ type: 'value',
577
+ action: 'move_left',
578
+ deviceID: 'keyboard-main',
579
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyA' },
580
+ context: 'gameplay',
581
+ options: { onValue: true, offValue: false }
582
+ },
583
+ {
584
+ type: 'value',
585
+ action: 'move_right',
586
+ deviceID: 'keyboard-main',
587
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyD' },
588
+ context: 'gameplay',
589
+ options: { onValue: true, offValue: false }
590
+ },
591
+ // Jump - Space
592
+ {
593
+ type: 'trigger',
594
+ action: 'jump',
595
+ deviceID: 'keyboard-main',
596
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'Space' },
597
+ context: 'gameplay',
598
+ options: { edgeMode: 'rising' }
599
+ },
600
+ // Interact - E
601
+ {
602
+ type: 'trigger',
603
+ action: 'interact',
604
+ deviceID: 'keyboard-main',
605
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'KeyE' },
606
+ context: 'gameplay',
607
+ options: { edgeMode: 'rising' }
608
+ },
609
+ // Pause - Escape (works in both gameplay and menu contexts)
610
+ {
611
+ type: 'trigger',
612
+ action: 'pause',
613
+ deviceID: 'keyboard-main',
614
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'Escape' },
615
+ options: { edgeMode: 'rising' }
616
+ }
617
+ ];
618
+
619
+ // Register all default bindings
620
+ defaultBindings.forEach(binding => this.bindingManager.registerBindingFromDefinition(binding));
621
+
622
+ // Start in gameplay context
623
+ this.bindingManager.activateContext('gameplay');
624
+ }
625
+
626
+ // Load control configuration from storage
627
+ loadControls() {
628
+ try {
629
+ const savedConfig = localStorage.getItem('gameControls');
630
+ if (savedConfig) {
631
+ const config = JSON.parse(savedConfig);
632
+ this.bindingManager.importConfiguration(config);
633
+ console.log('Custom controls loaded successfully');
634
+ return true;
635
+ }
636
+ } catch (err) {
637
+ console.error('Failed to load custom controls:', err);
638
+ }
639
+ return false;
640
+ }
641
+
642
+ // Save current control configuration to storage
643
+ saveControls() {
644
+ try {
645
+ const config = this.bindingManager.exportConfiguration();
646
+ localStorage.setItem('gameControls', JSON.stringify(config));
647
+ console.log('Controls saved successfully');
648
+ return true;
649
+ } catch (err) {
650
+ console.error('Failed to save controls:', err);
651
+ return false;
652
+ }
653
+ }
654
+
655
+ // Reset to default controls
656
+ resetToDefaults() {
657
+ // Clear existing bindings and re-setup defaults
658
+ localStorage.removeItem('gameControls');
659
+ // Re-initialize binding manager
660
+ this.bindingManager.importConfiguration({
661
+ actions: [],
662
+ bindings: [],
663
+ contexts: []
664
+ });
665
+ this.setupDefaultControls();
666
+ }
667
+
668
+ // Rebind a specific action to a new key
669
+ rebindAction(actionName: string, newKey: string, context?: string) {
670
+ // Remove existing bindings for this action in this context
671
+ this.bindingManager.unregisterBindings(actionName, context || null);
672
+
673
+ // Create new binding
674
+ const action = this.bindingManager.getAction(actionName);
675
+ if (!action) {
676
+ console.error(`Cannot rebind: action "${actionName}" not found`);
677
+ return false;
678
+ }
679
+
680
+ // Create and register the new binding
681
+ const bindingDef = {
682
+ type: action.type === 'digital' ? 'value' : 'value',
683
+ action,
684
+ deviceID: 'keyboard-main',
685
+ reader: {
686
+ type: 'keyboard',
687
+ sourceType: 'key',
688
+ sourceKey: newKey
689
+ },
690
+ context,
691
+ options: action.type === 'digital'
692
+ ? { onValue: true, offValue: false }
693
+ : {}
694
+ };
695
+
696
+ this.bindingManager.registerBindingFromDefinition(bindingDef);
697
+ return true;
698
+ }
699
+
700
+ // Get the binding manager for direct access if needed
701
+ getBindingManager() {
702
+ return this.bindingManager;
703
+ }
704
+ }
705
+
706
+ // Usage:
707
+ const controls = new GameControlsManager(eventBus);
708
+
709
+ // On game startup, try to load saved controls or fall back to defaults
710
+ if (!controls.loadControls()) {
711
+ console.log('No saved controls found, using defaults');
712
+ }
713
+
714
+ // When user changes a keybinding in settings menu
715
+ controls.rebindAction('jump', 'KeyF', 'gameplay');
716
+ controls.saveControls();
717
+
718
+ // When user wants to reset all controls
719
+ controls.resetToDefaults();
720
+ ```
721
+
722
+ ### Storage Considerations
723
+
724
+ The binding system is deliberately agnostic about how you store configurations. It simply provides methods to convert the binding state to and from plain JavaScript objects. This gives you the flexibility to:
725
+
726
+ - Save to browser storage like localStorage
727
+ - Save to files on disk
728
+ - Store in databases
729
+ - Sync with cloud services
730
+ - Embed in user profiles
731
+
732
+ ## Advanced Usage: Multiple Input Methods
733
+
734
+ One of the strengths of the binding system is support for multiple input devices. Here's how to set up the same action with different input methods:
735
+
736
+ ```typescript
737
+ // Jump action with keyboard
738
+ const keyboardJump = new TriggerBinding(
739
+ { name: 'jump', type: 'digital' },
740
+ 'keyboard-main',
741
+ new KeyboardValueReader('Space')
742
+ );
743
+
744
+ // Jump action with gamepad
745
+ const gamepadJump = new TriggerBinding(
746
+ { name: 'jump', type: 'digital' },
747
+ 'gamepad-0',
748
+ new GamepadValueReader('button', '0') // A/Cross button
749
+ );
750
+
751
+ // Movement with gamepad analog stick (using digital actions for clarity)
752
+ // Map left stick up to move_forward action
753
+ const gamepadMoveForward = new ValueBinding(
754
+ { name: 'move_forward', type: 'digital' },
755
+ 'gamepad-0',
756
+ new GamepadValueReader('axis', '1'), // Left stick Y-axis
757
+ {
758
+ deadzone: 0.2, // Prevent drift
759
+ threshold: -0.5, // Activate when stick pushed upward (negative Y)
760
+ invert: true // Invert because pushing up gives negative values
761
+ }
762
+ );
763
+
764
+ // Map left stick down to move_backward action
765
+ const gamepadMoveBackward = new ValueBinding(
766
+ { name: 'move_backward', type: 'digital' },
767
+ 'gamepad-0',
768
+ new GamepadValueReader('axis', '1'), // Left stick Y-axis
769
+ {
770
+ deadzone: 0.2, // Prevent drift
771
+ threshold: 0.5 // Activate when stick pushed downward (positive Y)
772
+ }
773
+ );
774
+
775
+ // Register all bindings
776
+ bindingManager.registerBinding(keyboardJump);
777
+ bindingManager.registerBinding(gamepadJump);
778
+ bindingManager.registerBinding(gamepadMoveForward);
779
+ bindingManager.registerBinding(gamepadMoveBackward);
780
+ ```
781
+
782
+ ### Supporting Multiple Input Devices with Binding Definitions
783
+
784
+ Using binding definitions makes it easier to support multiple input methods:
785
+
786
+ ```typescript
787
+ // Define action once
788
+ bindingManager.registerAction({ name: 'jump', type: 'digital' });
789
+
790
+ // Define bindings for multiple devices
791
+ const jumpBindings = [
792
+ {
793
+ // Keyboard binding
794
+ type: 'trigger',
795
+ action: 'jump', // Action name as a string
796
+ deviceID: 'keyboard-main',
797
+ reader: { type: 'keyboard', sourceType: 'key', sourceKey: 'Space' },
798
+ context: 'gameplay'
799
+ },
800
+ {
801
+ // Gamepad A button binding
802
+ type: 'trigger',
803
+ action: 'jump', // Action name as a string
804
+ deviceID: 'gamepad-0',
805
+ reader: { type: 'gamepad', sourceType: 'button', sourceKey: '0' },
806
+ context: 'gameplay'
807
+ },
808
+ {
809
+ // Mouse left button binding
810
+ type: 'trigger',
811
+ action: 'jump', // Action name as a string
812
+ deviceID: 'mouse-main',
813
+ reader: { type: 'mouse', sourceType: 'button', sourceKey: '0' },
814
+ context: 'gameplay'
815
+ }
816
+ ];
817
+
818
+ // Register all bindings
819
+ jumpBindings.forEach(binding => bindingManager.registerBindingFromDefinition(binding));
820
+ ```