@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.
- package/LICENSE +21 -0
- package/Readme.md +53 -0
- package/dist/classes/bindings/toggle.d.ts +122 -0
- package/dist/classes/bindings/trigger.d.ts +79 -0
- package/dist/classes/bindings/value.d.ts +104 -0
- package/dist/classes/entity.d.ts +83 -0
- package/dist/classes/eventBus.d.ts +94 -0
- package/dist/classes/gameEngine.d.ts +57 -0
- package/dist/classes/input/gamepad.d.ts +94 -0
- package/dist/classes/input/keyboard.d.ts +66 -0
- package/dist/classes/input/mouse.d.ts +80 -0
- package/dist/classes/input/readers/gamepad.d.ts +77 -0
- package/dist/classes/input/readers/keyboard.d.ts +60 -0
- package/dist/classes/input/readers/mouse.d.ts +45 -0
- package/dist/classes/loggers/consoleBackend.d.ts +29 -0
- package/dist/classes/loggers/nullBackend.d.ts +14 -0
- package/dist/engines/scene.d.ts +11 -0
- package/dist/interfaces/action.d.ts +20 -0
- package/dist/interfaces/binding.d.ts +144 -0
- package/dist/interfaces/entity.d.ts +9 -0
- package/dist/interfaces/game.d.ts +26 -0
- package/dist/interfaces/input.d.ts +181 -0
- package/dist/interfaces/logger.d.ts +88 -0
- package/dist/managers/binding.d.ts +185 -0
- package/dist/managers/entity.d.ts +70 -0
- package/dist/managers/game.d.ts +20 -0
- package/dist/managers/input.d.ts +56 -0
- package/dist/managers/level.d.ts +55 -0
- package/dist/sage.d.ts +20 -0
- package/dist/sage.es.js +2208 -0
- package/dist/sage.es.js.map +1 -0
- package/dist/sage.umd.js +2 -0
- package/dist/sage.umd.js.map +1 -0
- package/dist/utils/capabilities.d.ts +2 -0
- package/dist/utils/graphics.d.ts +10 -0
- package/dist/utils/logger.d.ts +66 -0
- package/dist/utils/physics.d.ts +2 -0
- package/dist/utils/version.d.ts +5 -0
- package/docs/architecture.md +129 -0
- package/docs/behaviors.md +706 -0
- package/docs/binding_system.md +820 -0
- package/docs/design/input.md +86 -0
- package/docs/entity_system.md +538 -0
- package/docs/eventbus.md +225 -0
- package/docs/getting_started.md +264 -0
- package/docs/images/sage_logo.png +0 -0
- package/docs/images/sage_logo_shape.png +0 -0
- package/docs/overview.md +38 -0
- package/docs/physics_system.md +686 -0
- package/docs/scene_system.md +513 -0
- package/package.json +69 -0
- package/src/classes/bindings/toggle.ts +261 -0
- package/src/classes/bindings/trigger.ts +211 -0
- package/src/classes/bindings/value.ts +227 -0
- package/src/classes/entity.ts +256 -0
- package/src/classes/eventBus.ts +259 -0
- package/src/classes/gameEngine.ts +125 -0
- package/src/classes/input/gamepad.ts +388 -0
- package/src/classes/input/keyboard.ts +189 -0
- package/src/classes/input/mouse.ts +276 -0
- package/src/classes/input/readers/gamepad.ts +179 -0
- package/src/classes/input/readers/keyboard.ts +123 -0
- package/src/classes/input/readers/mouse.ts +133 -0
- package/src/classes/loggers/consoleBackend.ts +135 -0
- package/src/classes/loggers/nullBackend.ts +51 -0
- package/src/engines/scene.ts +112 -0
- package/src/images/sage_logo.svg +172 -0
- package/src/images/sage_logo_shape.svg +146 -0
- package/src/interfaces/action.ts +30 -0
- package/src/interfaces/binding.ts +191 -0
- package/src/interfaces/entity.ts +21 -0
- package/src/interfaces/game.ts +44 -0
- package/src/interfaces/input.ts +221 -0
- package/src/interfaces/logger.ts +118 -0
- package/src/managers/binding.ts +729 -0
- package/src/managers/entity.ts +252 -0
- package/src/managers/game.ts +111 -0
- package/src/managers/input.ts +233 -0
- package/src/managers/level.ts +261 -0
- package/src/sage.ts +119 -0
- package/src/types/global.d.ts +11 -0
- package/src/utils/capabilities.ts +16 -0
- package/src/utils/graphics.ts +148 -0
- package/src/utils/logger.ts +225 -0
- package/src/utils/physics.ts +16 -0
- 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
|
+
//----------------------------------------------------------------------------------------------------------------------
|