@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,256 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Game Entity
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { GameEntityBehaviorConstructor } from '../interfaces/entity.ts';
|
|
6
|
+
import { type GameEvent, GameEventBus } from './eventBus.ts';
|
|
7
|
+
|
|
8
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Interface representing a subscription to a game event.
|
|
12
|
+
*/
|
|
13
|
+
interface GameEventSubscription
|
|
14
|
+
{
|
|
15
|
+
count : number;
|
|
16
|
+
unsubscribe : () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Interface representing the context of a game entity.
|
|
21
|
+
* @template EntityState - The type of the state object.
|
|
22
|
+
*/
|
|
23
|
+
interface SimpleGameEntity<EntityState extends object = object>
|
|
24
|
+
{
|
|
25
|
+
id : string;
|
|
26
|
+
type : string;
|
|
27
|
+
state : EntityState;
|
|
28
|
+
eventBus : GameEventBus;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base class for game entity behaviors.
|
|
35
|
+
*/
|
|
36
|
+
export abstract class GameEntityBehavior<RequiredState extends object = object>
|
|
37
|
+
{
|
|
38
|
+
abstract name : string;
|
|
39
|
+
abstract eventSubscriptions : string[];
|
|
40
|
+
|
|
41
|
+
protected entity : SimpleGameEntity | null = null;
|
|
42
|
+
|
|
43
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
44
|
+
// Internal API
|
|
45
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
$emit(event : GameEvent) : void
|
|
48
|
+
{
|
|
49
|
+
if(!this.entity)
|
|
50
|
+
{
|
|
51
|
+
throw new Error('Entity is not set for this behavior.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
event.senderID = this.entity.id;
|
|
55
|
+
this.entity.eventBus.publish(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sets the entity for this behavior.
|
|
60
|
+
* @param entity - The entity to set.
|
|
61
|
+
*/
|
|
62
|
+
$setEntity(entity : SimpleGameEntity | null) : void
|
|
63
|
+
{
|
|
64
|
+
this.entity = entity;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
68
|
+
// Implementation API
|
|
69
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
abstract processEvent(event : GameEvent, state : RequiredState) : Promise<boolean> | boolean;
|
|
72
|
+
|
|
73
|
+
update?(dt : number, state : RequiredState) : void;
|
|
74
|
+
|
|
75
|
+
destroy?() : Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Base class for game entities.
|
|
82
|
+
* @template EntityState - The type of the state object.
|
|
83
|
+
*/
|
|
84
|
+
export class GameEntity<EntityState extends object = object> implements SimpleGameEntity<EntityState>
|
|
85
|
+
{
|
|
86
|
+
/** The unique identifier of the entity. */
|
|
87
|
+
public readonly id : string;
|
|
88
|
+
|
|
89
|
+
/** The type of the entity. */
|
|
90
|
+
public readonly type : string;
|
|
91
|
+
|
|
92
|
+
/** The state of the entity. */
|
|
93
|
+
public state : EntityState;
|
|
94
|
+
|
|
95
|
+
/** A map of behaviors attached to the entity. */
|
|
96
|
+
public behaviors : Map<string, GameEntityBehavior> = new Map<string, GameEntityBehavior>();
|
|
97
|
+
|
|
98
|
+
/** The event bus for the entity. */
|
|
99
|
+
public eventBus : GameEventBus;
|
|
100
|
+
|
|
101
|
+
/** The event subscriptions for the entity. */
|
|
102
|
+
private subscriptions : Map<string, GameEventSubscription> = new Map<string, GameEventSubscription>();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates an instance of GameEntityBase.
|
|
106
|
+
* @param type - The type of the entity.
|
|
107
|
+
* @param initialState - The initial state of the entity.
|
|
108
|
+
* @param behaviors - An array of behaviors to attach to the entity.
|
|
109
|
+
* @param eventBus
|
|
110
|
+
*/
|
|
111
|
+
constructor(
|
|
112
|
+
type : string,
|
|
113
|
+
eventBus : GameEventBus,
|
|
114
|
+
initialState : EntityState,
|
|
115
|
+
behaviors : GameEntityBehaviorConstructor[]
|
|
116
|
+
)
|
|
117
|
+
{
|
|
118
|
+
this.id = crypto.randomUUID();
|
|
119
|
+
this.type = type;
|
|
120
|
+
this.state = initialState;
|
|
121
|
+
this.eventBus = eventBus;
|
|
122
|
+
|
|
123
|
+
// Attach behaviors
|
|
124
|
+
for(const behaviorCtor of behaviors)
|
|
125
|
+
{
|
|
126
|
+
this.attachBehavior(new behaviorCtor());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
131
|
+
// Internal Methods
|
|
132
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Processes a game event by passing it to the entity's behaviors.
|
|
136
|
+
* @param event - The game event to process.
|
|
137
|
+
* @returns A promise that resolves when the event has been processed.
|
|
138
|
+
*/
|
|
139
|
+
async $processEvent(event : GameEvent) : Promise<void>
|
|
140
|
+
{
|
|
141
|
+
for(const behavior of this.behaviors.values())
|
|
142
|
+
{
|
|
143
|
+
// eslint-disable-next-line no-await-in-loop
|
|
144
|
+
const result = await behavior.processEvent(event, this.state);
|
|
145
|
+
if(result)
|
|
146
|
+
{
|
|
147
|
+
// If the behavior returns true, stop other behaviors from processing the event.
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Updates the entity by calling the update method of its behaviors.
|
|
155
|
+
* @param dt - The delta time since the last update.
|
|
156
|
+
*/
|
|
157
|
+
$update(dt : number) : void
|
|
158
|
+
{
|
|
159
|
+
for(const behavior of this.behaviors.values())
|
|
160
|
+
{
|
|
161
|
+
behavior.update?.(dt, this.state);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Destroys the entity by calling the destroy method of its behaviors.
|
|
167
|
+
* @returns A promise that resolves when the entity has been destroyed.
|
|
168
|
+
*/
|
|
169
|
+
async $destroy() : Promise<void>
|
|
170
|
+
{
|
|
171
|
+
for(const behavior of this.behaviors.values())
|
|
172
|
+
{
|
|
173
|
+
behavior.destroy?.();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Unsubscribe from all event subscriptions
|
|
177
|
+
for(const subscription of this.subscriptions.values())
|
|
178
|
+
{
|
|
179
|
+
subscription.unsubscribe();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.behaviors.clear();
|
|
183
|
+
this.subscriptions.clear();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
187
|
+
// Public Methods
|
|
188
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Attaches a behavior to the entity.
|
|
192
|
+
* @param behavior - The behavior to attach.
|
|
193
|
+
* @throws Will throw an error if the behavior is already attached.
|
|
194
|
+
*/
|
|
195
|
+
attachBehavior(behavior : GameEntityBehavior) : void
|
|
196
|
+
{
|
|
197
|
+
if(this.behaviors.has(behavior.name))
|
|
198
|
+
{
|
|
199
|
+
throw new Error(`Behavior ${ behavior.name } is already attached to this entity.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Subscribe to the behavior's event subscriptions
|
|
203
|
+
for(const event of behavior.eventSubscriptions)
|
|
204
|
+
{
|
|
205
|
+
// Check if the event is already subscribed
|
|
206
|
+
const existingSubscription = this.subscriptions.get(event);
|
|
207
|
+
if(existingSubscription)
|
|
208
|
+
{
|
|
209
|
+
existingSubscription.count++;
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
{
|
|
213
|
+
// Create a new subscription
|
|
214
|
+
const unsubscribe = this.eventBus.subscribe(event, this.$processEvent.bind(this));
|
|
215
|
+
this.subscriptions.set(event, { count: 1, unsubscribe });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Attach the behavior
|
|
220
|
+
this.behaviors.set(behavior.name, behavior);
|
|
221
|
+
behavior.$setEntity(this);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Detaches a behavior from the entity.
|
|
226
|
+
* @param behaviorName - The behavior to detach.
|
|
227
|
+
*/
|
|
228
|
+
detachBehavior(behaviorName : string) : void
|
|
229
|
+
{
|
|
230
|
+
const behavior = this.behaviors.get(behaviorName);
|
|
231
|
+
if(behavior)
|
|
232
|
+
{
|
|
233
|
+
// Remove the behavior's event subscriptions
|
|
234
|
+
for(const event of behavior.eventSubscriptions)
|
|
235
|
+
{
|
|
236
|
+
const subscription = this.subscriptions.get(event);
|
|
237
|
+
if(subscription)
|
|
238
|
+
{
|
|
239
|
+
subscription.count--;
|
|
240
|
+
if(subscription.count <= 0)
|
|
241
|
+
{
|
|
242
|
+
subscription.unsubscribe();
|
|
243
|
+
this.subscriptions.delete(event);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Remove the behavior
|
|
249
|
+
this.behaviors.delete(behavior.name);
|
|
250
|
+
behavior.$setEntity(null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
256
|
+
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Event Bus
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { LoggerInterface } from '../interfaces/logger.ts';
|
|
6
|
+
import { LoggingUtility, SAGELogger } from '../utils/logger.ts';
|
|
7
|
+
|
|
8
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A generic game event type.
|
|
12
|
+
*
|
|
13
|
+
* @template P - The shape of the payload. Default is a generic object if omitted.
|
|
14
|
+
*/
|
|
15
|
+
export interface GameEvent<P extends Record<string, any> = Record<string, any>>
|
|
16
|
+
{
|
|
17
|
+
/** The string identifier/category of the event (e.g. "input:keydown"). */
|
|
18
|
+
type : string;
|
|
19
|
+
/** The optional ID of whoever sent this event. */
|
|
20
|
+
senderID ?: string;
|
|
21
|
+
/** The optional ID of who should receive this event. */
|
|
22
|
+
targetID ?: string;
|
|
23
|
+
/** The payload, with user-defined shape. */
|
|
24
|
+
payload ?: P;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A callback function that receives a {@link GameEvent} with a given payload type.
|
|
29
|
+
*
|
|
30
|
+
* @template P - The shape of the payload in the event.
|
|
31
|
+
*/
|
|
32
|
+
export type GameEventCallback<P extends Record<string, any> = Record<string, any>> =
|
|
33
|
+
(event : GameEvent<P>) => void | Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents a subscription record internally within the EventBus.
|
|
37
|
+
*
|
|
38
|
+
* We store the callback as if it handles `GameEvent<any>`
|
|
39
|
+
* because we can’t unify multiple different payload types in the same structure.
|
|
40
|
+
*/
|
|
41
|
+
interface Subscription
|
|
42
|
+
{
|
|
43
|
+
/** Precompiled RegExp if this subscription is pattern-based. */
|
|
44
|
+
pattern ?: RegExp;
|
|
45
|
+
/** The callback, but stored to accept any payload type (due to the union nature). */
|
|
46
|
+
callback : (event : GameEvent<any>) => void | Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An unsubscribe function, returned by any subscribe method.
|
|
51
|
+
*/
|
|
52
|
+
export type Unsubscribe = () => void;
|
|
53
|
+
|
|
54
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A high-performance event bus that supports exact or pattern-based subscriptions,
|
|
58
|
+
* and allows subscribers to specify a more specific `payload` type for convenience.
|
|
59
|
+
*
|
|
60
|
+
* **Note**: This does NOT enforce that a given `type` always has a certain payload shape.
|
|
61
|
+
* It's a best-effort approach that lets you avoid `as` casts in callbacks.
|
|
62
|
+
*/
|
|
63
|
+
export class GameEventBus
|
|
64
|
+
{
|
|
65
|
+
/**
|
|
66
|
+
* For exact event-type matches:
|
|
67
|
+
* - Key = event type string (e.g. "input:keydown")
|
|
68
|
+
* - Value = set of subscriptions for that exact type
|
|
69
|
+
*/
|
|
70
|
+
private directMap = new Map<string, Set<Subscription>>();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* For pattern-based (wildcard or RegExp) subscriptions.
|
|
74
|
+
*/
|
|
75
|
+
private patternSubs = new Set<Subscription>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Logger instance
|
|
79
|
+
*/
|
|
80
|
+
private _log : LoggerInterface;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a new GameEventBus instance
|
|
84
|
+
*
|
|
85
|
+
* @param logger - The logging utility to use
|
|
86
|
+
*/
|
|
87
|
+
constructor(logger ?: LoggingUtility)
|
|
88
|
+
{
|
|
89
|
+
this._log = logger?.getLogger('EventBus') || new SAGELogger('EventBus');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe with automatic detection:
|
|
94
|
+
* - If `eventTypeOrPattern` is a `RegExp` or a string containing `*`, treat it as a pattern subscription.
|
|
95
|
+
* - Otherwise, treat it as an exact subscription.
|
|
96
|
+
*
|
|
97
|
+
* @template P - The payload shape you want to expect in the callback.
|
|
98
|
+
* @param {string | RegExp} eventTypeOrPattern - The exact event type or a wildcard/RegExp pattern.
|
|
99
|
+
* @param {GameEventCallback<P>} callback - A callback expecting a `GameEvent<P>`.
|
|
100
|
+
* @returns {Unsubscribe} function that, when called, removes this subscription.
|
|
101
|
+
*/
|
|
102
|
+
public subscribe<P extends Record<string, any> = Record<string, any>>(
|
|
103
|
+
eventTypeOrPattern : string | RegExp,
|
|
104
|
+
callback : GameEventCallback<P>
|
|
105
|
+
) : Unsubscribe
|
|
106
|
+
{
|
|
107
|
+
this._log.trace(`Subscribe request for: ${ eventTypeOrPattern.toString() }`);
|
|
108
|
+
|
|
109
|
+
// If it's explicitly a RegExp or includes '*', treat as pattern:
|
|
110
|
+
if(
|
|
111
|
+
eventTypeOrPattern instanceof RegExp
|
|
112
|
+
|| (typeof eventTypeOrPattern === 'string' && eventTypeOrPattern.includes('*'))
|
|
113
|
+
)
|
|
114
|
+
{
|
|
115
|
+
return this.subscribePattern(eventTypeOrPattern, callback);
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
{
|
|
119
|
+
return this.subscribeExact(eventTypeOrPattern, callback);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Subscribes to an exact event type (e.g. "input:keydown").
|
|
125
|
+
*
|
|
126
|
+
* @template P - The payload shape you want to expect in the callback.
|
|
127
|
+
* @param {string} eventType - The exact string to match.
|
|
128
|
+
* @param {GameEventCallback<P>} callback - A callback expecting a `GameEvent<P>`.
|
|
129
|
+
* @returns {Unsubscribe} function that unsubscribes this exact subscription.
|
|
130
|
+
*/
|
|
131
|
+
public subscribeExact<P extends Record<string, any> = Record<string, any>>(
|
|
132
|
+
eventType : string,
|
|
133
|
+
callback : GameEventCallback<P>
|
|
134
|
+
) : Unsubscribe
|
|
135
|
+
{
|
|
136
|
+
this._log.debug(`Adding exact subscription for: ${ eventType }`);
|
|
137
|
+
|
|
138
|
+
let subs = this.directMap.get(eventType);
|
|
139
|
+
if(!subs)
|
|
140
|
+
{
|
|
141
|
+
subs = new Set();
|
|
142
|
+
this.directMap.set(eventType, subs);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Store as a Subscription that accepts `GameEvent<any>`
|
|
146
|
+
const subscription : Subscription = {
|
|
147
|
+
callback: (event) => callback(event as GameEvent<P>), // runtime "type-lie" cast
|
|
148
|
+
};
|
|
149
|
+
subs.add(subscription);
|
|
150
|
+
|
|
151
|
+
return () =>
|
|
152
|
+
{
|
|
153
|
+
this._log.debug(`Removing exact subscription for: ${ eventType }`);
|
|
154
|
+
subs.delete(subscription);
|
|
155
|
+
if(subs.size === 0)
|
|
156
|
+
{
|
|
157
|
+
this.directMap.delete(eventType);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Subscribes with either:
|
|
164
|
+
* - a wildcard string (contains `*`) that gets converted to a RegExp
|
|
165
|
+
* - a RegExp directly
|
|
166
|
+
*
|
|
167
|
+
* @template P - The payload shape you want to expect in the callback.
|
|
168
|
+
* @param {string | RegExp} patternOrRegExp - The pattern to match (`"input:*"` or `/^input:/`).
|
|
169
|
+
* @param {GameEventCallback<P>} callback - A callback expecting a `GameEvent<P>`.
|
|
170
|
+
* @returns {Unsubscribe} function that unsubscribes this pattern-based subscription.
|
|
171
|
+
*/
|
|
172
|
+
public subscribePattern<P extends Record<string, any> = Record<string, any>>(
|
|
173
|
+
patternOrRegExp : string | RegExp,
|
|
174
|
+
callback : GameEventCallback<P>
|
|
175
|
+
) : Unsubscribe
|
|
176
|
+
{
|
|
177
|
+
let regex : RegExp;
|
|
178
|
+
|
|
179
|
+
if(typeof patternOrRegExp === 'string')
|
|
180
|
+
{
|
|
181
|
+
// MAIN BUG FIX:
|
|
182
|
+
// We replace '*' with '.*' (not '\*' or '\\*').
|
|
183
|
+
// The prior code was .replace('\\*', '.*'), which never matched 'input:*'
|
|
184
|
+
const escaped = patternOrRegExp
|
|
185
|
+
.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&') // escape special chars
|
|
186
|
+
.replace(/\*/g, '.*'); // replace '*' with '.*'
|
|
187
|
+
|
|
188
|
+
// e.g. "input:*" => "input:.*" => new RegExp("^input:.*$")
|
|
189
|
+
regex = new RegExp(`^${ escaped }$`);
|
|
190
|
+
this._log.debug(
|
|
191
|
+
`Adding pattern subscription for string: ${ patternOrRegExp }, regex: ${ regex.toString() }`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
{
|
|
196
|
+
// If already a RegExp, use as-is
|
|
197
|
+
regex = patternOrRegExp;
|
|
198
|
+
this._log.debug(`Adding pattern subscription for regex: ${ regex.toString() }`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const subscription : Subscription = {
|
|
202
|
+
pattern: regex,
|
|
203
|
+
callback: (event) => callback(event as GameEvent<P>),
|
|
204
|
+
};
|
|
205
|
+
this.patternSubs.add(subscription);
|
|
206
|
+
|
|
207
|
+
return () =>
|
|
208
|
+
{
|
|
209
|
+
this._log.debug(`Removing pattern subscription: ${ regex.toString() }`);
|
|
210
|
+
this.patternSubs.delete(subscription);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Publishes an event, invoking all matching callbacks.
|
|
216
|
+
*
|
|
217
|
+
* Callbacks are called asynchronously (microtask) via `Promise.resolve()`.
|
|
218
|
+
*
|
|
219
|
+
* @param {GameEvent<any>} event - The event object to publish.
|
|
220
|
+
*/
|
|
221
|
+
public publish(event : GameEvent<any>) : void
|
|
222
|
+
{
|
|
223
|
+
this._log.trace(`Publishing event: ${ event.type }`, event);
|
|
224
|
+
|
|
225
|
+
let callbackCount = 0;
|
|
226
|
+
|
|
227
|
+
// 1) Exact match
|
|
228
|
+
const directSubs = this.directMap.get(event.type);
|
|
229
|
+
if(directSubs)
|
|
230
|
+
{
|
|
231
|
+
callbackCount += directSubs.size;
|
|
232
|
+
for(const sub of directSubs)
|
|
233
|
+
{
|
|
234
|
+
Promise.resolve().then(() => sub.callback(event));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 2) Pattern subscriptions
|
|
239
|
+
for(const sub of this.patternSubs)
|
|
240
|
+
{
|
|
241
|
+
if(sub.pattern && sub.pattern.test(event.type))
|
|
242
|
+
{
|
|
243
|
+
callbackCount++;
|
|
244
|
+
Promise.resolve().then(() => sub.callback(event));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if(callbackCount === 0)
|
|
249
|
+
{
|
|
250
|
+
this._log.debug(`No subscribers found for event: ${ event.type }`);
|
|
251
|
+
}
|
|
252
|
+
else
|
|
253
|
+
{
|
|
254
|
+
this._log.trace(`Event ${ event.type } dispatched to ${ callbackCount } subscribers`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// SkewedAspect Game Engine
|
|
3
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { AbstractEngine, HavokPlugin } from '@babylonjs/core';
|
|
6
|
+
|
|
7
|
+
// Interfaces
|
|
8
|
+
import { GameCanvas } from '../interfaces/game.ts';
|
|
9
|
+
import { LoggerInterface } from '../interfaces/logger.ts';
|
|
10
|
+
|
|
11
|
+
// Engines
|
|
12
|
+
import { SceneEngine } from '../engines/scene.ts';
|
|
13
|
+
|
|
14
|
+
// Managers
|
|
15
|
+
import { BindingManager } from '../managers/binding.ts';
|
|
16
|
+
import { GameManager } from '../managers/game.ts';
|
|
17
|
+
import { GameEntityManager } from '../managers/entity.ts';
|
|
18
|
+
import { UserInputManager } from '../managers/input.ts';
|
|
19
|
+
|
|
20
|
+
// Utils
|
|
21
|
+
import { GameEventBus } from './eventBus.ts';
|
|
22
|
+
import { LoggingUtility } from '../utils/logger.ts';
|
|
23
|
+
import { VERSION } from '../utils/version.ts';
|
|
24
|
+
|
|
25
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interface representing the engines used in the game.
|
|
29
|
+
*/
|
|
30
|
+
interface Engines
|
|
31
|
+
{
|
|
32
|
+
sceneEngine : SceneEngine;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Interface representing the managers used in the game.
|
|
37
|
+
*/
|
|
38
|
+
interface Managers
|
|
39
|
+
{
|
|
40
|
+
bindingManager : BindingManager;
|
|
41
|
+
gameManager : GameManager;
|
|
42
|
+
entityManager : GameEntityManager;
|
|
43
|
+
inputManager : UserInputManager;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Class representing the SkewedAspect Game Engine.
|
|
50
|
+
*/
|
|
51
|
+
export class SkewedAspectGameEngine
|
|
52
|
+
{
|
|
53
|
+
public canvas : GameCanvas;
|
|
54
|
+
public renderEngine : AbstractEngine;
|
|
55
|
+
public physics : HavokPlugin;
|
|
56
|
+
public managers : Managers;
|
|
57
|
+
public engines : Engines;
|
|
58
|
+
public eventBus : GameEventBus;
|
|
59
|
+
public logger : LoggingUtility;
|
|
60
|
+
|
|
61
|
+
private _log : LoggerInterface;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates an instance of SkewedAspectGameEngine.
|
|
65
|
+
* @param canvas - The game canvas.
|
|
66
|
+
* @param renderEngine - The rendering engine.
|
|
67
|
+
* @param physics - The physics engine.
|
|
68
|
+
* @param eventBus - The event bus.
|
|
69
|
+
* @param logger - The logging utility.
|
|
70
|
+
* @param engines - The engines used in the game.
|
|
71
|
+
* @param managers - The managers used in the game.
|
|
72
|
+
*/
|
|
73
|
+
constructor(
|
|
74
|
+
canvas : GameCanvas,
|
|
75
|
+
renderEngine : AbstractEngine,
|
|
76
|
+
physics : HavokPlugin,
|
|
77
|
+
eventBus : GameEventBus,
|
|
78
|
+
logger : LoggingUtility,
|
|
79
|
+
engines : Engines,
|
|
80
|
+
managers : Managers
|
|
81
|
+
)
|
|
82
|
+
{
|
|
83
|
+
this.canvas = canvas;
|
|
84
|
+
this.renderEngine = renderEngine;
|
|
85
|
+
this.physics = physics;
|
|
86
|
+
this.eventBus = eventBus;
|
|
87
|
+
this.logger = logger;
|
|
88
|
+
this.engines = engines;
|
|
89
|
+
this.managers = managers;
|
|
90
|
+
|
|
91
|
+
this._log = logger.getLogger('GameEngine');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
95
|
+
// Public API
|
|
96
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Starts the game engine.
|
|
100
|
+
*/
|
|
101
|
+
async start() : Promise<void>
|
|
102
|
+
{
|
|
103
|
+
this._log.info(`Starting SkewedAspect Game Engine (Version: ${ VERSION })...`);
|
|
104
|
+
|
|
105
|
+
// Start the game manager
|
|
106
|
+
await this.managers.gameManager.start(this.canvas);
|
|
107
|
+
|
|
108
|
+
this._log.info('Game engine started successfully');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stops the game engine.
|
|
113
|
+
*/
|
|
114
|
+
async stop() : Promise<void>
|
|
115
|
+
{
|
|
116
|
+
this._log.info(`Stopping SkewedAspect Game Engine (Version: ${ VERSION })...`);
|
|
117
|
+
|
|
118
|
+
// Stop the game manager
|
|
119
|
+
await this.managers.gameManager.stop();
|
|
120
|
+
|
|
121
|
+
this._log.info('Game engine stopped successfully');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//------------------------------------------------------------------------------------------------------------------
|