@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,252 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Entity Manager
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { GameEntity } from '../classes/entity.ts';
|
|
6
|
+
import { GameEventBus } from '../classes/eventBus.ts';
|
|
7
|
+
|
|
8
|
+
// Interfaces
|
|
9
|
+
import type { Action } from '../interfaces/action.ts';
|
|
10
|
+
import type { GameEntityDefinition } from '../interfaces/entity.ts';
|
|
11
|
+
import type { LoggerInterface } from '../interfaces/logger.ts';
|
|
12
|
+
import type { BindingManager } from '../managers/binding.ts';
|
|
13
|
+
|
|
14
|
+
// Utils
|
|
15
|
+
import { LoggingUtility, SAGELogger } from '../utils/logger.ts';
|
|
16
|
+
|
|
17
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export class GameEntityManager
|
|
20
|
+
{
|
|
21
|
+
/** The event bus for the entity manager. */
|
|
22
|
+
private eventBus : GameEventBus;
|
|
23
|
+
|
|
24
|
+
/** A map of entities managed by the entity manager. */
|
|
25
|
+
private entities : Map<string, GameEntity> = new Map<string, GameEntity>();
|
|
26
|
+
|
|
27
|
+
/** A map of entity definitions registered with the entity manager. */
|
|
28
|
+
private entityDefinitions : Map<string, GameEntityDefinition> = new Map<string, GameEntityDefinition>();
|
|
29
|
+
|
|
30
|
+
/** Reference to the binding manager for registering actions */
|
|
31
|
+
private bindingManager : BindingManager;
|
|
32
|
+
|
|
33
|
+
/** Logger instance */
|
|
34
|
+
private _log : LoggerInterface;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates an instance of EntityManager.
|
|
38
|
+
* @param eventBus - The event bus for the entity manager.
|
|
39
|
+
* @param logger - The logging utility to use
|
|
40
|
+
* @param bindingManager - The binding manager for registering actions
|
|
41
|
+
*/
|
|
42
|
+
constructor(eventBus : GameEventBus, logger : LoggingUtility | undefined, bindingManager : BindingManager)
|
|
43
|
+
{
|
|
44
|
+
this.eventBus = eventBus;
|
|
45
|
+
this.bindingManager = bindingManager;
|
|
46
|
+
this._log = logger?.getLogger('EntityManager') || new SAGELogger('EntityManager');
|
|
47
|
+
this._log.info('EntityManager initialized');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
51
|
+
// Private Methods
|
|
52
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if two actions are compatible
|
|
56
|
+
* @param existingAction - The action already registered
|
|
57
|
+
* @param newAction - The action being registered
|
|
58
|
+
* @returns true if the actions are compatible, false if they have conflicting options
|
|
59
|
+
*/
|
|
60
|
+
private areActionsCompatible(existingAction : Action, newAction : Action) : boolean
|
|
61
|
+
{
|
|
62
|
+
// If types don't match, they're incompatible
|
|
63
|
+
if(existingAction.type !== newAction.type)
|
|
64
|
+
{
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For analog actions, check min and max values
|
|
69
|
+
if(existingAction.type === 'analog' && newAction.type === 'analog')
|
|
70
|
+
{
|
|
71
|
+
// If minValue or maxValue is specified and differs, they're incompatible
|
|
72
|
+
if((newAction.minValue !== undefined && existingAction.minValue !== newAction.minValue)
|
|
73
|
+
|| (newAction.maxValue !== undefined && existingAction.maxValue !== newAction.maxValue))
|
|
74
|
+
{
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Digital actions are always compatible if they have the same name
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Registers actions defined in the entity definition with the binding manager
|
|
85
|
+
* @param entityDef - The entity definition containing actions to register
|
|
86
|
+
*/
|
|
87
|
+
private registerEntityActions(entityDef : GameEntityDefinition) : void
|
|
88
|
+
{
|
|
89
|
+
if(!entityDef.actions)
|
|
90
|
+
{
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for(const action of entityDef.actions)
|
|
95
|
+
{
|
|
96
|
+
try
|
|
97
|
+
{
|
|
98
|
+
// Check if the action is already registered
|
|
99
|
+
const existingAction = this.bindingManager.getAction(action.name);
|
|
100
|
+
|
|
101
|
+
if(!existingAction)
|
|
102
|
+
{
|
|
103
|
+
// Action doesn't exist yet, register it
|
|
104
|
+
this._log.debug(`Registering action "${ action.name }" from entity type "${ entityDef.type }"`);
|
|
105
|
+
this.bindingManager.registerAction(action);
|
|
106
|
+
}
|
|
107
|
+
else if(!this.areActionsCompatible(existingAction, action))
|
|
108
|
+
{
|
|
109
|
+
this._log.warn(
|
|
110
|
+
`Action "${ action.name }" already registered with different options. `
|
|
111
|
+
+ `Entity "${ entityDef.type }" requires: ${ JSON.stringify(action) }, `
|
|
112
|
+
+ `but found: ${ JSON.stringify(existingAction) }`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
else
|
|
116
|
+
{
|
|
117
|
+
this._log.trace(
|
|
118
|
+
`Action "${ action.name }" already registered with compatible options, skipping registration`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (err)
|
|
123
|
+
{
|
|
124
|
+
this._log.debug(`Failed to register action "${ action.name }": `
|
|
125
|
+
+ `${ err instanceof Error ? err.message : String(err) }`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$frameUpdate(dt : number) : void
|
|
131
|
+
{
|
|
132
|
+
this._log.trace(`Updating ${ this.entities.size } entities with dt=${ dt }`);
|
|
133
|
+
for(const entity of this.entities.values())
|
|
134
|
+
{
|
|
135
|
+
entity.$update(dt);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
140
|
+
// Public API
|
|
141
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Registers a new entity definition.
|
|
145
|
+
* @param entityDef - The definition of the entity.
|
|
146
|
+
*/
|
|
147
|
+
registerEntityDefinition(entityDef : GameEntityDefinition) : void
|
|
148
|
+
{
|
|
149
|
+
this._log.debug(`Registering entity definition: ${ entityDef.type }`);
|
|
150
|
+
|
|
151
|
+
// Register any actions this entity requires
|
|
152
|
+
this.registerEntityActions(entityDef);
|
|
153
|
+
|
|
154
|
+
// Store the entity definition
|
|
155
|
+
this.entityDefinitions.set(entityDef.type, entityDef);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates a new entity of the given type.
|
|
160
|
+
* @param type - The type of the entity to create.
|
|
161
|
+
* @param initialState - The initial state of the entity.
|
|
162
|
+
* @returns The created entity.
|
|
163
|
+
*/
|
|
164
|
+
createEntity<State extends object = object>(type : string, initialState : Partial<State> = {}) : GameEntity<State>
|
|
165
|
+
{
|
|
166
|
+
this._log.debug(`Creating entity of type: ${ type }`);
|
|
167
|
+
const entityDef = this.entityDefinitions.get(type);
|
|
168
|
+
if(!entityDef)
|
|
169
|
+
{
|
|
170
|
+
const errorMsg = `Entity type ${ type } is not registered.`;
|
|
171
|
+
this._log.error(errorMsg);
|
|
172
|
+
throw new Error(errorMsg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this._log.trace(`Using entity definition with ${ entityDef.behaviors?.length || 0 } behaviors`);
|
|
176
|
+
const mergedState = { ...entityDef.defaultState, ...initialState } as State;
|
|
177
|
+
|
|
178
|
+
const entity = new GameEntity<State>(
|
|
179
|
+
entityDef.type,
|
|
180
|
+
this.eventBus,
|
|
181
|
+
mergedState,
|
|
182
|
+
entityDef.behaviors
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
this.entities.set(entity.id, entity);
|
|
186
|
+
this._log.debug(`Entity created with ID: ${ entity.id }`);
|
|
187
|
+
return entity;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Destroys the entity with the given ID.
|
|
192
|
+
* @param entityID - The ID of the entity to destroy.
|
|
193
|
+
*/
|
|
194
|
+
destroyEntity(entityID : string) : void
|
|
195
|
+
{
|
|
196
|
+
this._log.debug(`Destroying entity: ${ entityID }`);
|
|
197
|
+
const entity = this.entities.get(entityID);
|
|
198
|
+
if(entity)
|
|
199
|
+
{
|
|
200
|
+
entity.$destroy();
|
|
201
|
+
this.entities.delete(entityID);
|
|
202
|
+
this._log.debug(`Entity ${ entityID } destroyed`);
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
{
|
|
206
|
+
this._log.warn(`Attempted to destroy non-existent entity: ${ entityID }`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Gets the entity with the given ID.
|
|
212
|
+
* @param entityID - The ID of the entity to get.
|
|
213
|
+
* @returns The entity with the given ID, or null if it does not exist.
|
|
214
|
+
*/
|
|
215
|
+
getEntity(entityID : string) : GameEntity | null
|
|
216
|
+
{
|
|
217
|
+
this._log.trace(`Getting entity: ${ entityID }`);
|
|
218
|
+
return this.entities.get(entityID) ?? null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Adds an existing entity to the entity manager.
|
|
223
|
+
* @param entity - The entity to add.
|
|
224
|
+
*/
|
|
225
|
+
addEntity(entity : GameEntity) : void
|
|
226
|
+
{
|
|
227
|
+
this._log.debug(`Adding existing entity: ${ entity.id } (type: ${ entity.type })`);
|
|
228
|
+
this.entities.set(entity.id, entity);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Removes the entity with the given ID, without destroying it.
|
|
233
|
+
* @param entityID - The ID of the entity to remove.
|
|
234
|
+
*/
|
|
235
|
+
removeEntity(entityID : string) : void
|
|
236
|
+
{
|
|
237
|
+
this._log.debug(`Removing entity: ${ entityID }`);
|
|
238
|
+
const entity = this.entities.get(entityID);
|
|
239
|
+
if(entity)
|
|
240
|
+
{
|
|
241
|
+
this.entities.delete(entityID);
|
|
242
|
+
this._log.debug(`Entity ${ entityID } removed`);
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
{
|
|
246
|
+
this._log.warn(`Attempted to remove non-existent entity: ${ entityID }`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
252
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Game Manager
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { AbstractEngine, Scene } from '@babylonjs/core';
|
|
6
|
+
|
|
7
|
+
// Interfaces
|
|
8
|
+
import { GameCanvas } from '../interfaces/game.ts';
|
|
9
|
+
import { LoggerInterface } from '../interfaces/logger.ts';
|
|
10
|
+
|
|
11
|
+
// Managers
|
|
12
|
+
import { GameEntityManager } from './entity.ts';
|
|
13
|
+
import { UserInputManager } from './input.ts';
|
|
14
|
+
|
|
15
|
+
// Engines
|
|
16
|
+
import { SceneEngine } from '../engines/scene.ts';
|
|
17
|
+
|
|
18
|
+
// Utils
|
|
19
|
+
import { isBrowser } from '../utils/capabilities.ts';
|
|
20
|
+
import { LoggingUtility, SAGELogger } from '../utils/logger.ts';
|
|
21
|
+
|
|
22
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export class GameManager
|
|
25
|
+
{
|
|
26
|
+
private _engine : AbstractEngine;
|
|
27
|
+
private _entityManager : GameEntityManager;
|
|
28
|
+
private _inputManager : UserInputManager;
|
|
29
|
+
private _sceneEngine : SceneEngine;
|
|
30
|
+
private _currentScene : Scene | null = null;
|
|
31
|
+
private _log : LoggerInterface;
|
|
32
|
+
|
|
33
|
+
public started = false;
|
|
34
|
+
|
|
35
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
engine : AbstractEngine,
|
|
39
|
+
sceneEngine : SceneEngine,
|
|
40
|
+
entityManager : GameEntityManager,
|
|
41
|
+
inputManager : UserInputManager,
|
|
42
|
+
logger ?: LoggingUtility
|
|
43
|
+
)
|
|
44
|
+
{
|
|
45
|
+
this._engine = engine;
|
|
46
|
+
this._sceneEngine = sceneEngine;
|
|
47
|
+
this._entityManager = entityManager;
|
|
48
|
+
this._inputManager = inputManager;
|
|
49
|
+
this._log = logger?.getLogger('GameManager') || new SAGELogger('GameManager');
|
|
50
|
+
|
|
51
|
+
if(isBrowser())
|
|
52
|
+
{
|
|
53
|
+
// Resize the engine on window resize
|
|
54
|
+
window.addEventListener('resize', this._resizeHandler.bind(this));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private _renderLoop() : void
|
|
59
|
+
{
|
|
60
|
+
// Update entities and entity manager
|
|
61
|
+
const deltaTime = this._engine.getDeltaTime();
|
|
62
|
+
this._entityManager.$frameUpdate(deltaTime);
|
|
63
|
+
|
|
64
|
+
// Update input devices
|
|
65
|
+
if(this._inputManager)
|
|
66
|
+
{
|
|
67
|
+
this._inputManager.pollGamepads();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Render the current scene
|
|
71
|
+
if(this._currentScene)
|
|
72
|
+
{
|
|
73
|
+
this._currentScene.render();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private _resizeHandler() : void
|
|
78
|
+
{
|
|
79
|
+
if(this.started)
|
|
80
|
+
{
|
|
81
|
+
this._engine.resize();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
86
|
+
// Public API
|
|
87
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
async start(canvas : GameCanvas) : Promise<void>
|
|
90
|
+
{
|
|
91
|
+
// TODO: There needs to be some sort of initial scene we load
|
|
92
|
+
this._currentScene = await this._sceneEngine.loadScene(canvas);
|
|
93
|
+
|
|
94
|
+
// Start the render loop
|
|
95
|
+
this._engine.runRenderLoop(this._renderLoop.bind(this));
|
|
96
|
+
|
|
97
|
+
this.started = true;
|
|
98
|
+
|
|
99
|
+
this._log.info('SkewedAspect Game Engine started successfully');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async stop() : Promise<void>
|
|
103
|
+
{
|
|
104
|
+
this.started = false;
|
|
105
|
+
this._engine.stopRenderLoop();
|
|
106
|
+
|
|
107
|
+
this._log.info('SkewedAspect Game Engine stopped successfully');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// User Input Manager
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
// Interfaces
|
|
6
|
+
import type { GamepadInputState, InputDevice, KeyboardInputState, MouseInputState } from '../interfaces/input.ts';
|
|
7
|
+
import type { LoggerInterface } from '../interfaces/logger.ts';
|
|
8
|
+
|
|
9
|
+
// Classes
|
|
10
|
+
import { type GameEvent, GameEventBus } from '../classes/eventBus.ts';
|
|
11
|
+
|
|
12
|
+
// Utils
|
|
13
|
+
import { LoggingUtility, SAGELogger } from '../utils/logger.ts';
|
|
14
|
+
|
|
15
|
+
// Resource Access
|
|
16
|
+
import { KeyboardInputPlugin } from '../classes/input/keyboard.ts';
|
|
17
|
+
import { MouseInputPlugin } from '../classes/input/mouse.ts';
|
|
18
|
+
import { GamepadInputPlugin } from '../classes/input/gamepad.ts';
|
|
19
|
+
|
|
20
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manager for handling user input from various devices (keyboard, mouse, gamepad)
|
|
24
|
+
*/
|
|
25
|
+
export class UserInputManager
|
|
26
|
+
{
|
|
27
|
+
private _eventBus : GameEventBus;
|
|
28
|
+
|
|
29
|
+
private _keyboardRA : KeyboardInputPlugin;
|
|
30
|
+
private _mouseRA : MouseInputPlugin;
|
|
31
|
+
private _gamepadRA : GamepadInputPlugin;
|
|
32
|
+
|
|
33
|
+
/** Logger instance */
|
|
34
|
+
private _log : LoggerInterface;
|
|
35
|
+
|
|
36
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new UserInputManager
|
|
40
|
+
*
|
|
41
|
+
* @param eventBus - The game event bus to publish events to
|
|
42
|
+
* @param canvas - The DOM element to attach input listeners to
|
|
43
|
+
* @param logger - The logging utility to use
|
|
44
|
+
*/
|
|
45
|
+
constructor(
|
|
46
|
+
eventBus : GameEventBus,
|
|
47
|
+
canvas : HTMLElement,
|
|
48
|
+
logger ?: LoggingUtility
|
|
49
|
+
)
|
|
50
|
+
{
|
|
51
|
+
this._eventBus = eventBus;
|
|
52
|
+
|
|
53
|
+
// Initialize logger
|
|
54
|
+
this._log = logger?.getLogger('UserInputManager') || new SAGELogger('UserInputManager');
|
|
55
|
+
|
|
56
|
+
this._log.info('Initializing UserInputManager');
|
|
57
|
+
|
|
58
|
+
// Initialize resource access classes
|
|
59
|
+
this._log.debug('Initializing input resource access classes');
|
|
60
|
+
this._keyboardRA = new KeyboardInputPlugin();
|
|
61
|
+
this._mouseRA = new MouseInputPlugin(canvas);
|
|
62
|
+
this._gamepadRA = new GamepadInputPlugin();
|
|
63
|
+
|
|
64
|
+
// Register callbacks
|
|
65
|
+
this._log.debug('Registering input event callbacks');
|
|
66
|
+
this._keyboardRA.onDeviceConnected(this._publishDeviceConnected.bind(this));
|
|
67
|
+
this._keyboardRA.onInputChanged(this._publishInputChanged.bind(this));
|
|
68
|
+
this._mouseRA.onDeviceConnected(this._publishDeviceConnected.bind(this));
|
|
69
|
+
this._mouseRA.onInputChanged(this._publishInputChanged.bind(this));
|
|
70
|
+
this._gamepadRA.onDeviceConnected(this._publishDeviceConnected.bind(this));
|
|
71
|
+
this._gamepadRA.onDeviceDisconnected(this._publishDeviceDisconnected.bind(this));
|
|
72
|
+
this._gamepadRA.onInputChanged(this._publishInputChanged.bind(this));
|
|
73
|
+
|
|
74
|
+
this._log.info('UserInputManager initialized successfully');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
78
|
+
// Private methods
|
|
79
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Publish device connected event to the event bus
|
|
83
|
+
*/
|
|
84
|
+
private _publishDeviceConnected(device : InputDevice) : void
|
|
85
|
+
{
|
|
86
|
+
this._log.debug(`Device connected: ${ device.id } (${ device.name })`);
|
|
87
|
+
|
|
88
|
+
const gameEvent : GameEvent = {
|
|
89
|
+
type: 'input:device:connected',
|
|
90
|
+
payload: { device },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this._eventBus.publish(gameEvent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Publish device disconnected event to the event bus
|
|
98
|
+
*/
|
|
99
|
+
private _publishDeviceDisconnected(device : InputDevice) : void
|
|
100
|
+
{
|
|
101
|
+
this._log.debug(`Device disconnected: ${ device.id } (${ device.name })`);
|
|
102
|
+
|
|
103
|
+
const gameEvent : GameEvent = {
|
|
104
|
+
type: 'input:device:disconnected',
|
|
105
|
+
payload: { device },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
this._eventBus.publish(gameEvent);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Publish input changed event to the event bus, used by all device types
|
|
113
|
+
*/
|
|
114
|
+
private _publishInputChanged(
|
|
115
|
+
device : InputDevice,
|
|
116
|
+
state : KeyboardInputState | MouseInputState | GamepadInputState
|
|
117
|
+
) : void
|
|
118
|
+
{
|
|
119
|
+
this._log.trace(`Input changed: ${ device.id }`, state);
|
|
120
|
+
|
|
121
|
+
const gameEvent : GameEvent = {
|
|
122
|
+
type: 'input:changed',
|
|
123
|
+
payload: {
|
|
124
|
+
deviceId: device.id,
|
|
125
|
+
device,
|
|
126
|
+
state,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Publish the event to the event bus
|
|
131
|
+
this._eventBus.publish(gameEvent);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
135
|
+
// Internal methods
|
|
136
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Destroy the input manager and clean up event listeners
|
|
140
|
+
*/
|
|
141
|
+
public $destroy() : void
|
|
142
|
+
{
|
|
143
|
+
this._log.info('Destroying UserInputManager');
|
|
144
|
+
|
|
145
|
+
// Cleanup all resource access instances
|
|
146
|
+
this._log.debug('Cleaning up input resource access instances');
|
|
147
|
+
this._keyboardRA.destroy();
|
|
148
|
+
this._mouseRA.destroy();
|
|
149
|
+
this._gamepadRA.destroy();
|
|
150
|
+
|
|
151
|
+
this._log.info('UserInputManager destroyed');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
155
|
+
// Public methods
|
|
156
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get all input devices
|
|
160
|
+
*
|
|
161
|
+
* @return An array of input devices
|
|
162
|
+
*/
|
|
163
|
+
public listDevices() : InputDevice[]
|
|
164
|
+
{
|
|
165
|
+
this._log.debug('Getting all connected input devices');
|
|
166
|
+
|
|
167
|
+
const devices : InputDevice[] = [];
|
|
168
|
+
|
|
169
|
+
// Add keyboard and mouse
|
|
170
|
+
devices.push(this._keyboardRA.getDevice());
|
|
171
|
+
devices.push(this._mouseRA.getDevice());
|
|
172
|
+
|
|
173
|
+
// Add all gamepads
|
|
174
|
+
devices.push(...this._gamepadRA.getDevices());
|
|
175
|
+
|
|
176
|
+
this._log.debug(`Found ${ devices.length } connected devices`);
|
|
177
|
+
return devices;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get a specific input device by ID
|
|
182
|
+
*
|
|
183
|
+
* @param deviceId - The ID of the device to get
|
|
184
|
+
*
|
|
185
|
+
* @return The input device, or null if not found
|
|
186
|
+
*/
|
|
187
|
+
public getDevice(deviceId : string) : InputDevice | null
|
|
188
|
+
{
|
|
189
|
+
this._log.debug(`Getting device: ${ deviceId }`);
|
|
190
|
+
|
|
191
|
+
// Shortcut for keyboard and mouse
|
|
192
|
+
if(deviceId.startsWith('keyboard-'))
|
|
193
|
+
{
|
|
194
|
+
return this._keyboardRA.getDevice();
|
|
195
|
+
}
|
|
196
|
+
else if(deviceId.startsWith('mouse-'))
|
|
197
|
+
{
|
|
198
|
+
return this._mouseRA.getDevice();
|
|
199
|
+
}
|
|
200
|
+
else if(deviceId.startsWith('gamepad-'))
|
|
201
|
+
{
|
|
202
|
+
const index = parseInt(deviceId.split('-')[1], 10);
|
|
203
|
+
return this._gamepadRA.getDevice(index);
|
|
204
|
+
}
|
|
205
|
+
else
|
|
206
|
+
{
|
|
207
|
+
// This is the slow path, and should never be hit, but it is here for completeness
|
|
208
|
+
const devices = this.listDevices();
|
|
209
|
+
|
|
210
|
+
for(const device of devices)
|
|
211
|
+
{
|
|
212
|
+
if(device.id === deviceId)
|
|
213
|
+
{
|
|
214
|
+
return device;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this._log.warn(`Device not found: ${ deviceId }`);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Poll for gamepad state updates - call this in your game loop
|
|
225
|
+
*/
|
|
226
|
+
public pollGamepads() : void
|
|
227
|
+
{
|
|
228
|
+
this._gamepadRA.pollGamepads();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
233
|
+
|