@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,261 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // Toggle Binding
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import type { GameEventBus } from '../eventBus.ts';
6
+
7
+ // Interfaces
8
+ import type { Action } from '../../interfaces/action.ts';
9
+ import type { Binding, ToggleBindingDefinition } from '../../interfaces/binding.ts';
10
+ import type { DeviceValueReader, InputState } from '../../interfaces/input.ts';
11
+
12
+ //----------------------------------------------------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Options for configuring a toggle binding
16
+ */
17
+ export interface ToggleBindingOptions
18
+ {
19
+ /**
20
+ * If true, toggle on falling edge (true -> false) instead of rising edge (false -> true)
21
+ */
22
+ invert ?: boolean;
23
+
24
+ /**
25
+ * Initial toggle state (defaults to false - off)
26
+ */
27
+ initialState ?: boolean;
28
+
29
+ /**
30
+ * Threshold for analog inputs (0.0 to 1.0)
31
+ * When input value is >= threshold, it's considered "active" (true)
32
+ * When input value is < threshold, it's considered "inactive" (false)
33
+ * Defaults to 0.5 for analog inputs
34
+ */
35
+ threshold ?: number;
36
+
37
+ /**
38
+ * Value to emit when the toggle is in the "on" state for digital actions
39
+ * If undefined, will emit boolean true
40
+ * Ignored for analog actions
41
+ */
42
+ onValue ?: boolean | number;
43
+
44
+ /**
45
+ * Value to emit when the toggle is in the "off" state for digital actions
46
+ * If undefined, will emit boolean false
47
+ * Ignored for analog actions
48
+ */
49
+ offValue ?: boolean | number;
50
+
51
+ /**
52
+ * Optional context name this binding belongs to
53
+ */
54
+ context ?: string;
55
+ }
56
+
57
+ /**
58
+ * Implementation of a toggle binding, which maintains and toggles state between true/false
59
+ * when a button/key is pressed or released, remaining in that state until toggled again.
60
+ * Can handle both digital and analog inputs with configurable threshold.
61
+ * Can output either digital or analog values based on the action type.
62
+ */
63
+ export class ToggleBinding implements Binding
64
+ {
65
+ public readonly type = 'toggle' as const;
66
+ public readonly action : Action;
67
+ public readonly context ?: string;
68
+ public readonly deviceID : string;
69
+ public readonly reader : DeviceValueReader;
70
+
71
+ // Toggle-specific options
72
+ private readonly _invert : boolean;
73
+ private readonly _threshold : number;
74
+ private readonly _initialState : boolean;
75
+ private readonly _onValue : boolean | number;
76
+ private readonly _offValue : boolean | number;
77
+
78
+ // State tracking
79
+ private _lastDigitalState = false;
80
+ private _toggleState : boolean;
81
+
82
+ //------------------------------------------------------------------------------------------------------------------
83
+ // Public Getters
84
+ //------------------------------------------------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Get the current toggle state
88
+ *
89
+ * @returns Current toggle state (true = on, false = off)
90
+ */
91
+ public get state() : boolean
92
+ {
93
+ return this._toggleState;
94
+ }
95
+
96
+ /**
97
+ * Set the toggle state directly (useful for programmatic control)
98
+ *
99
+ * @param value - New toggle state
100
+ */
101
+ public set state(value : boolean)
102
+ {
103
+ this._toggleState = value;
104
+ }
105
+
106
+ /**
107
+ * Get the value emitted when toggle is in the "on" state
108
+ *
109
+ * @returns The value for the "on" state
110
+ */
111
+ public get onValue() : boolean | number
112
+ {
113
+ return this._onValue;
114
+ }
115
+
116
+ /**
117
+ * Get the value emitted when toggle is in the "off" state
118
+ *
119
+ * @returns The value for the "off" state
120
+ */
121
+ public get offValue() : boolean | number
122
+ {
123
+ return this._offValue;
124
+ }
125
+
126
+ /**
127
+ * Get the current value based on toggle state
128
+ *
129
+ * @returns The current value (boolean or number)
130
+ */
131
+ public get value() : boolean | number
132
+ {
133
+ if(this.action.type === 'analog')
134
+ {
135
+ return this._toggleState
136
+ ? (this.action.maxValue ?? 1)
137
+ : (this.action.minValue ?? 0);
138
+ }
139
+ else
140
+ {
141
+ return this._toggleState ? this._onValue : this._offValue;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get the current options for this toggle binding.
147
+ *
148
+ * @returns The current options for this toggle binding
149
+ */
150
+ public get options() : ToggleBindingOptions
151
+ {
152
+ return {
153
+ invert: this._invert,
154
+ threshold: this._threshold,
155
+ initialState: this._initialState,
156
+ onValue: this._onValue,
157
+ offValue: this._offValue,
158
+ };
159
+ }
160
+
161
+ //------------------------------------------------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Create a new toggle binding
165
+ *
166
+ * @param action - Action to toggle
167
+ * @param deviceID - Device ID this binding is for
168
+ * @param reader - Input reader to read values from
169
+ * @param options - Binding configuration options
170
+ */
171
+ constructor(
172
+ action : Action,
173
+ deviceID : string,
174
+ reader : DeviceValueReader,
175
+ options : ToggleBindingOptions = {}
176
+ )
177
+ {
178
+ this.action = action;
179
+ this.deviceID = deviceID;
180
+ this.reader = reader;
181
+ this.context = options.context;
182
+
183
+ // Set options with defaults
184
+ this._invert = options.invert ?? false;
185
+ this._threshold = options.threshold ?? 0.5;
186
+ this._initialState = options.initialState ?? false;
187
+ this._toggleState = this._initialState;
188
+
189
+ // For digital actions, these control the output value
190
+ // For analog actions, these are not used (we use action properties instead)
191
+ this._onValue = options.onValue ?? true;
192
+ this._offValue = options.offValue ?? false;
193
+ }
194
+
195
+ //------------------------------------------------------------------------------------------------------------------
196
+ // Public Methods
197
+ //------------------------------------------------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Process input state and emit a digital action if toggled
201
+ *
202
+ * @param state - Current input state
203
+ * @param eventBus - Event bus to emit action events to
204
+ */
205
+ public process(state : InputState, eventBus : GameEventBus) : void
206
+ {
207
+ const rawValue = this.reader.getValue(state) ?? false;
208
+ const digitalState = typeof rawValue === 'boolean'
209
+ ? rawValue
210
+ : rawValue >= this._threshold;
211
+
212
+ const shouldToggle = this._invert
213
+ ? !digitalState && this._lastDigitalState
214
+ : digitalState && !this._lastDigitalState;
215
+
216
+ this._lastDigitalState = digitalState;
217
+
218
+ if(!shouldToggle) { return; }
219
+
220
+ this._toggleState = !this._toggleState;
221
+
222
+ eventBus.publish({
223
+ type: `action:${ this.action.name }`,
224
+ payload: {
225
+ value: this.value,
226
+ deviceId: this.deviceID,
227
+ context: this.context,
228
+ },
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Reset toggle to its initial state
234
+ */
235
+ public reset() : void
236
+ {
237
+ this._toggleState = this._initialState;
238
+ }
239
+
240
+ /**
241
+ * Returns a JSON-serializable representation of this toggle binding
242
+ *
243
+ * @returns A simple object representation that can be converted to JSON
244
+ */
245
+ public toJSON() : ToggleBindingDefinition
246
+ {
247
+ return {
248
+ type: this.type,
249
+ action: this.action.name,
250
+ input: {
251
+ deviceID: this.deviceID,
252
+ ...this.reader.toJSON(),
253
+ },
254
+ state: this._toggleState,
255
+ context: this.context,
256
+ options: this.options,
257
+ };
258
+ }
259
+ }
260
+
261
+ //----------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,211 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // Trigger Binding
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import type { GameEventBus } from '../eventBus.ts';
6
+
7
+ // Interfaces
8
+ import type { Action } from '../../interfaces/action.ts';
9
+ import type { Binding, TriggerBindingDefinition } from '../../interfaces/binding.ts';
10
+ import type { DeviceValueReader, InputState } from '../../interfaces/input.ts';
11
+
12
+ //----------------------------------------------------------------------------------------------------------------------
13
+
14
+ /**
15
+ * All supported edge modes for validation
16
+ */
17
+ export const edgeModes = [ 'rising', 'falling', 'both' ] as const;
18
+
19
+ /**
20
+ * Defines when a trigger binding will fire based on input state changes
21
+ */
22
+ export type EdgeMode = typeof edgeModes[number];
23
+
24
+ /**
25
+ * Options for configuring a trigger binding
26
+ */
27
+ export interface TriggerBindingOptions
28
+ {
29
+ /**
30
+ * Which edge(s) should cause the trigger to fire
31
+ * - 'rising': Only trigger on false -> true transitions
32
+ * - 'falling': Only trigger on true -> false transitions
33
+ * - 'both': Trigger on any state change
34
+ */
35
+ edgeMode ?: EdgeMode;
36
+
37
+ /**
38
+ * Threshold for analog inputs (0.0 to 1.0)
39
+ * When input value is >= threshold, it's considered "active" (true)
40
+ * When input value is < threshold, it's considered "inactive" (false)
41
+ * Defaults to 0.5 for analog inputs
42
+ */
43
+ threshold ?: number;
44
+
45
+ /**
46
+ * Optional context name this binding belongs to
47
+ */
48
+ context ?: string;
49
+ }
50
+
51
+ //----------------------------------------------------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Implementation of a trigger binding, which emits an action when a button/key is pressed or released.
55
+ * Can handle both digital and analog inputs with configurable threshold.
56
+ * Can output either digital or analog values based on the action type.
57
+ */
58
+ export class TriggerBinding implements Binding
59
+ {
60
+ public readonly type = 'trigger' as const;
61
+ public readonly action : Action;
62
+ public readonly context ?: string;
63
+ public readonly deviceID : string;
64
+ public readonly reader : DeviceValueReader;
65
+
66
+ // Trigger-specific options
67
+ private readonly _edgeMode : EdgeMode;
68
+ private readonly _threshold : number;
69
+
70
+ // State tracking
71
+ private _lastDigitalState = false;
72
+
73
+ //------------------------------------------------------------------------------------------------------------------
74
+ // Public Getters
75
+ //------------------------------------------------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Get the current options for this trigger binding.
79
+ *
80
+ * @returns The current options for this trigger binding
81
+ */
82
+ get options() : TriggerBindingOptions
83
+ {
84
+ return {
85
+ edgeMode: this._edgeMode,
86
+ threshold: this._threshold,
87
+ };
88
+ }
89
+
90
+ //------------------------------------------------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Create a new trigger binding
94
+ *
95
+ * @param action - Action to trigger
96
+ * @param deviceID - Device ID this binding is for
97
+ * @param reader - Input reader to read values from
98
+ * @param options - Binding configuration options
99
+ */
100
+ constructor(
101
+ action : Action,
102
+ deviceID : string,
103
+ reader : DeviceValueReader,
104
+ options : TriggerBindingOptions = {}
105
+ )
106
+ {
107
+ this.action = action;
108
+ this.deviceID = deviceID;
109
+ this.reader = reader;
110
+ this.context = options.context;
111
+
112
+ // Set options with defaults
113
+ this._edgeMode = options.edgeMode ?? 'rising';
114
+ this._threshold = options.threshold ?? 0.5;
115
+ }
116
+
117
+ //------------------------------------------------------------------------------------------------------------------
118
+ // Public Methods
119
+ //------------------------------------------------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Process input state and emit an action if triggered
123
+ *
124
+ * @param state - Current input state
125
+ * @param eventBus - Event bus to emit action events to
126
+ * @returns True if an action was triggered, false otherwise
127
+ */
128
+ public process(state : InputState, eventBus : GameEventBus) : void
129
+ {
130
+ // Extract the raw value using our source
131
+ const rawValue = this.reader.getValue(state) ?? false;
132
+
133
+ // Determine the digital state based on the threshold
134
+ const digitalState = typeof rawValue === 'boolean'
135
+ ? rawValue
136
+ : rawValue >= this._threshold;
137
+
138
+ // Determine if we should trigger based on the configured edge mode
139
+ let shouldTrigger = false;
140
+
141
+ switch (this._edgeMode)
142
+ {
143
+ case 'rising':
144
+ shouldTrigger = digitalState && !this._lastDigitalState;
145
+ break;
146
+ case 'falling':
147
+ shouldTrigger = !digitalState && this._lastDigitalState;
148
+ break;
149
+ case 'both':
150
+ shouldTrigger = digitalState !== this._lastDigitalState;
151
+ break;
152
+ }
153
+
154
+ // Update last known state for next time
155
+ this._lastDigitalState = digitalState;
156
+
157
+ // If triggered, emit the action event
158
+ if(shouldTrigger)
159
+ {
160
+ let outputValue : boolean | number;
161
+ if(this.action.type === 'analog')
162
+ {
163
+ // Use raw value and convert to a number if needed
164
+ let finalValue = typeof rawValue === 'number' ? rawValue : (rawValue ? 1.0 : 0.0);
165
+
166
+ // Apply scaling if action parameters exist
167
+ if(this.action.minValue !== undefined || this.action.maxValue !== undefined)
168
+ {
169
+ const min = this.action.minValue ?? 0;
170
+ const max = this.action.maxValue ?? 1;
171
+ finalValue = min + (finalValue * (max - min));
172
+ }
173
+ outputValue = finalValue;
174
+ }
175
+ else
176
+ {
177
+ outputValue = true;
178
+ }
179
+ const actionEvent = {
180
+ type: `action:${ this.action.name }`,
181
+ payload: {
182
+ value: outputValue,
183
+ deviceId: this.deviceID,
184
+ context: this.context,
185
+ },
186
+ };
187
+ eventBus.publish(actionEvent);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Returns a JSON-serializable representation of this trigger binding
193
+ *
194
+ * @returns A simple object representation that can be converted to JSON
195
+ */
196
+ public toJSON() : TriggerBindingDefinition
197
+ {
198
+ return {
199
+ type: this.type,
200
+ action: this.action.name,
201
+ input: {
202
+ deviceID: this.deviceID,
203
+ ...this.reader.toJSON(),
204
+ },
205
+ context: this.context,
206
+ options: this.options,
207
+ };
208
+ }
209
+ }
210
+
211
+ //----------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,227 @@
1
+ //----------------------------------------------------------------------------------------------------------------------
2
+ // Value Binding
3
+ //----------------------------------------------------------------------------------------------------------------------
4
+
5
+ import type { GameEventBus } from '../eventBus.ts';
6
+
7
+ // Interfaces
8
+ import type { Action } from '../../interfaces/action.ts';
9
+ import type { Binding, ValueBindingDefinition } from '../../interfaces/binding.ts';
10
+ import type { DeviceValueReader, InputState } from '../../interfaces/input.ts';
11
+
12
+ //----------------------------------------------------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Options for configuring a value binding
16
+ */
17
+ export interface ValueBindingOptions
18
+ {
19
+ /**
20
+ * Factor to scale the input value by (1.0 = no scaling)
21
+ */
22
+ scale ?: number;
23
+
24
+ /**
25
+ * Value to add to the input value after scaling
26
+ */
27
+ offset ?: number;
28
+
29
+ /**
30
+ * Whether to invert the input value (multiply by -1)
31
+ */
32
+ invert ?: boolean;
33
+
34
+ /**
35
+ * Optional context name this binding belongs to
36
+ */
37
+ context ?: string;
38
+
39
+ /**
40
+ * If true, only emit values when they change
41
+ * If false, always emit values on each process call
42
+ */
43
+ emitOnChange ?: boolean;
44
+
45
+ /**
46
+ * Deadzone threshold (0.0 to 1.0)
47
+ * Input values below this threshold will be treated as 0
48
+ */
49
+ deadzone ?: number;
50
+
51
+ /**
52
+ * Value to emit when the input is digital and true
53
+ * Only used when action is digital
54
+ */
55
+ onValue ?: boolean | number;
56
+
57
+ /**
58
+ * Value to emit when the input is digital and false
59
+ * Only used when action is digital
60
+ */
61
+ offValue ?: boolean | number;
62
+
63
+ /**
64
+ * Minimum value to clamp output to
65
+ */
66
+ min ?: number;
67
+
68
+ /**
69
+ * Maximum value to clamp output to
70
+ */
71
+ max ?: number;
72
+ }
73
+
74
+ /**
75
+ * Implementation of a value binding, which directly converts an analog input
76
+ * to an analog output with optional scaling, offset, and clamping.
77
+ * Can handle both digital and analog inputs/outputs based on action type.
78
+ */
79
+ export class ValueBinding implements Binding
80
+ {
81
+ public readonly type = 'value' as const;
82
+ public readonly action : Action;
83
+ public readonly context ?: string;
84
+ public readonly deviceID : string;
85
+ public readonly reader : DeviceValueReader;
86
+
87
+ // Value-specific options
88
+ private readonly _scale : number;
89
+ private readonly _offset : number;
90
+ private readonly _invert : boolean;
91
+ private readonly _emitOnChange : boolean;
92
+ private readonly _deadzone : number;
93
+ private readonly _onValue : boolean | number;
94
+ private readonly _offValue : boolean | number;
95
+ private readonly _min : number;
96
+ private readonly _max : number;
97
+
98
+ // State tracking
99
+ private _lastValue : number | boolean | undefined;
100
+
101
+ //------------------------------------------------------------------------------------------------------------------
102
+
103
+ /**
104
+ * Get the current options for this value binding.
105
+ *
106
+ * @returns The current options for this value binding
107
+ */
108
+ get options() : ValueBindingOptions
109
+ {
110
+ return {
111
+ scale: this._scale,
112
+ offset: this._offset,
113
+ invert: this._invert,
114
+ emitOnChange: this._emitOnChange,
115
+ deadzone: this._deadzone,
116
+ onValue: this._onValue,
117
+ offValue: this._offValue,
118
+ min: this._min,
119
+ max: this._max,
120
+ };
121
+ }
122
+
123
+ //------------------------------------------------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Create a new value binding
127
+ *
128
+ * @param action - Action to emit values for
129
+ * @param deviceID - Device ID this binding is for
130
+ * @param reader - Input reader to read values from
131
+ * @param options - Binding configuration options
132
+ */
133
+ constructor(
134
+ action : Action,
135
+ deviceID : string,
136
+ reader : DeviceValueReader,
137
+ options : ValueBindingOptions = {}
138
+ )
139
+ {
140
+ this.action = action;
141
+ this.deviceID = deviceID;
142
+ this.reader = reader;
143
+ this.context = options.context;
144
+
145
+ // Common options that apply to both analog and digital
146
+ this._scale = options.scale ?? 1.0;
147
+ this._offset = options.offset ?? 0.0;
148
+ this._invert = options.invert ?? false;
149
+ this._emitOnChange = options.emitOnChange ?? true;
150
+ this._deadzone = options.deadzone ?? 0.0;
151
+
152
+ // For digital actions, these control the output value
153
+ // For analog actions, these are not used
154
+ this._onValue = options.onValue ?? true;
155
+ this._offValue = options.offValue ?? false;
156
+
157
+ // Store min/max values explicitly
158
+ const defaultMin = this.action.type === 'analog'
159
+ ? this.action.minValue ?? Number.NEGATIVE_INFINITY
160
+ : Number.NEGATIVE_INFINITY;
161
+ this._min = options.min ?? defaultMin;
162
+
163
+ const defaultMax = this.action.type === 'analog'
164
+ ? this.action.maxValue ?? Number.POSITIVE_INFINITY
165
+ : Number.POSITIVE_INFINITY;
166
+ this._max = options.max ?? defaultMax;
167
+ }
168
+
169
+ //------------------------------------------------------------------------------------------------------------------
170
+ // Public Methods
171
+ //------------------------------------------------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Process input state and emit an action value
175
+ *
176
+ * @param state - Current input state
177
+ * @param eventBus - Event bus to emit action events to
178
+ * @returns True if a value was emitted, false otherwise
179
+ */
180
+ public process(state : InputState, eventBus : GameEventBus) : void
181
+ {
182
+ const rawValue = this.reader.getValue(state);
183
+ if(rawValue === undefined) { return; }
184
+
185
+ const numericValue = typeof rawValue === 'boolean' ? (rawValue ? 1.0 : 0.0) : rawValue;
186
+ if(numericValue === undefined) { return; }
187
+
188
+ let value = this._deadzone > 0 && Math.abs(numericValue) < this._deadzone ? 0 : numericValue;
189
+ value = ((this._invert ? -value : value) * this._scale) + this._offset;
190
+
191
+ // Apply min/max clamping
192
+ value = Math.max(this._min, Math.min(this._max, value));
193
+
194
+ if(this._emitOnChange && this._lastValue === value) { return; }
195
+ this._lastValue = value;
196
+
197
+ eventBus.publish({
198
+ type: `action:${ this.action.name }`,
199
+ payload: {
200
+ value,
201
+ deviceId: this.deviceID,
202
+ context: this.context,
203
+ },
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Returns a JSON-serializable representation of this value binding
209
+ *
210
+ * @returns A simple object representation that can be converted to JSON
211
+ */
212
+ public toJSON() : ValueBindingDefinition
213
+ {
214
+ return {
215
+ type: this.type,
216
+ action: this.action.name,
217
+ input: {
218
+ deviceID: this.deviceID,
219
+ ...this.reader.toJSON(),
220
+ },
221
+ context: this.context,
222
+ options: this.options,
223
+ };
224
+ }
225
+ }
226
+
227
+ //----------------------------------------------------------------------------------------------------------------------