@jael-ecs/core 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,493 @@
1
+ <div align="center">
2
+
3
+ # Jael (Just Another ECS Library)
4
+
5
+ [![npm version](https://badge.fury.io/js/%40jael%2Fcore.svg)](https://badge.fury.io/js/%40jael%2Fcore)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ _A modern, performant, and user-friendly Entity Component System library written in TypeScript_
10
+
11
+ </div>
12
+
13
+ ## Table of contents
14
+
15
+ - [Installation](#installation)
16
+ - [Quick Start](#quick-start)
17
+ - [Architecture](#architecture)
18
+ - [Api Reference](#-api-reference)
19
+ - [World](#world)
20
+ - [Entity](#entity)
21
+ - [System](#system)
22
+ - [Query](#query)
23
+ - [SparseSet](#sparseset)
24
+ - [Time](#time)
25
+ - [EventRegistry](#event-registry)
26
+ - [Best Practices](#best-practices)
27
+ - [Advanced Usage](#advanced-usage)
28
+ - [Contributing](#contributing)
29
+ - [Acknowledgments](#acknowledgments)
30
+
31
+ ## Features
32
+
33
+ - **User Friendly API** - Clean, fluent api that's easy to learn
34
+ - **High Performance** - Optimized SparseSet implementation for fast entity lookups
35
+ - **Minimal Bundle size** - Compact bundle size without dependencies.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @jael/core
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```typescript
46
+ import { World, System } from "@jael/core";
47
+
48
+ // Create your world
49
+ const world = new World();
50
+
51
+ // Define components
52
+ interface Position {
53
+ x: number;
54
+ y: number;
55
+ }
56
+
57
+ interface Velocity {
58
+ dx: number;
59
+ dy: number;
60
+ }
61
+
62
+ // Create entities
63
+ const player = world.create();
64
+ world.addComponent(player, "position", { x: 0, y: 0 });
65
+ world.addComponent(player, "velocity", { dx: 1, dy: 1 });
66
+
67
+ const enemy = world.create();
68
+ world.addComponent(enemy, "position", { x: 10, y: 10 });
69
+ world.addComponent(enemy, "velocity", { dx: -1, dy: 0 });
70
+
71
+ // Create a system
72
+ const movementSystem: System = {
73
+ priority: 0,
74
+ update(dt) {
75
+ const query = world.include("position", "velocity");
76
+
77
+ for (const entity of query.entities) {
78
+ const position = entity.get<Position>("position");
79
+ const velocity = entity.get<Velocity>("velocity");
80
+
81
+ position.x += velocity.dx * (dt || 0.016);
82
+ position.y += velocity.dy * (dt || 0.016);
83
+ }
84
+ },
85
+ };
86
+
87
+ // Add system to world
88
+ world.addSystem(movementSystem);
89
+
90
+ // Game loop
91
+ function gameLoop(dt: number) {
92
+ world.update(dt);
93
+ }
94
+ ```
95
+
96
+ ## Architecture
97
+
98
+ Jael follows the classic Entity Component System pattern:
99
+
100
+ - **Entities**: Unique identifiers (just IDs) - no data attached
101
+ - **Components**: Pure data containers (no logic)
102
+ - **Systems**: Process entities with specific component combinations
103
+
104
+ ## API Reference
105
+
106
+ ### World
107
+
108
+ The central hub that manages entities, components, and systems.
109
+
110
+ #### Entity Management
111
+
112
+ ```typescript
113
+ // Create a new entity
114
+ const entity = world.create();
115
+
116
+ // Destroy an entity
117
+ world.destroy(entity);
118
+
119
+ // Check if entity exists
120
+ const exists = world.exist(entity);
121
+ ```
122
+
123
+ #### Component Management
124
+
125
+ ```typescript
126
+ // Add component
127
+ world.addComponent(entity, "position", { x: 0, y: 0 });
128
+
129
+ // Remove component
130
+ world.removeComponent(entity, "position");
131
+
132
+ // Get component
133
+ const position = entity.get<Position>("position");
134
+
135
+ // Check if entity has component
136
+ const hasPosition = entity.has("position");
137
+ ```
138
+
139
+ #### System Management
140
+
141
+ ```typescript
142
+ // Add system
143
+ world.addSystem(yourSystem);
144
+
145
+ // Remove system
146
+ world.removeSystem(yourSystem);
147
+ ```
148
+
149
+ #### Events
150
+
151
+ ```typescript
152
+ // Listen to world events
153
+ world.on("entityCreated", ({ entity }) => {
154
+ console.log("Entity created:", entity);
155
+ });
156
+
157
+ world.on("entityDestroyed", ({ entity }) => {
158
+ console.log("Entity destroyed:", entity);
159
+ });
160
+
161
+ world.on("componentAdded", ({ entity, component }) => {
162
+ console.log(`Component ${component} added to entity`);
163
+ });
164
+
165
+ world.on("componentRemoved", ({ entity, component }) => {
166
+ console.log(`Component ${component} removed from entity`);
167
+ });
168
+
169
+ world.on("updated", () => {
170
+ console.log("World updated");
171
+ });
172
+ ```
173
+
174
+ ### Entity
175
+
176
+ Base entity class for intuitive component management
177
+
178
+ ```typescript
179
+ // Create entity
180
+ const entity = world.create();
181
+
182
+ // Add component
183
+ entity.add("position", { x: 0, y: 0 });
184
+
185
+ // Remove component
186
+ entity.remove("position");
187
+
188
+ // Check if component exist
189
+ const posExist = entity.has("position");
190
+
191
+ // Get curren value of the component
192
+ const compSchema = entity.get("position");
193
+ ```
194
+
195
+ ### System
196
+
197
+ Systems contain the game logic that processes entities with specific components.
198
+
199
+ ```typescript
200
+ interface System {
201
+ priority: number; // Execution order (lower = earlier)
202
+ exit?(): void; // Cleanup when removed
203
+ update(dt?: number): void; // Main update logic
204
+ }
205
+ ```
206
+
207
+ #### Example System
208
+
209
+ ```typescript
210
+ const renderSystem: System = {
211
+ priority: 100, // Render after all other systems
212
+
213
+ update(dt) {
214
+ const renderableQuery = world.include("position", "sprite");
215
+
216
+ for (const entity of renderableQuery.entities) {
217
+ const position = entity.get<Position>("position");
218
+ const sprite = entity.get<Sprite>("sprite");
219
+
220
+ // Render entity
221
+ drawSprite(sprite, position.x, position.y);
222
+ }
223
+ },
224
+
225
+ exit() {
226
+ console.log("Render system cleanup");
227
+ },
228
+ };
229
+ ```
230
+
231
+ ### Query
232
+
233
+ Queries provide efficient, cached access to entities matching specific component patterns.
234
+
235
+ ```typescript
236
+ interface QueryConfig {
237
+ include: string[]; // Required components
238
+ exclude: string[]; // Components to exclude
239
+ }
240
+ ```
241
+
242
+ #### Creating Queries
243
+
244
+ ```typescript
245
+ // Simple include query
246
+ const movingEntities = world.include("position", "velocity");
247
+
248
+ // Simple exclude query
249
+ const nonStatic = world.exclude("static");
250
+
251
+ // Complex query
252
+ const complex = world.query({
253
+ include: ["position", "velocity", "health"],
254
+ exclude: ["dead", "paused"],
255
+ });
256
+
257
+ // Can be use with builder pattern creating a hash for every include/exclude
258
+ const complexQuery2 = world.include("position", "health").exclude("static");
259
+ ```
260
+
261
+ #### Accessing Results
262
+
263
+ ```typescript
264
+ // Iterate through entities
265
+ for (const entity of query.entities) {
266
+ // Process entity
267
+ }
268
+
269
+ // Get the first value of the query
270
+ const first = query.entities.first();
271
+
272
+ // Check query size
273
+ const count = query.entities.size();
274
+
275
+ // Check if query has any entities
276
+ const isEmpty = query.entities.size() === 0;
277
+ ```
278
+
279
+ ### SparseSet
280
+
281
+ High-performance data structure used internally for entity and component storage.
282
+
283
+ ```typescript
284
+ const sparseSet = new SparseSet<Entity>();
285
+
286
+ // Add items
287
+ sparseSet.add(entity1);
288
+ sparseSet.add(entity2);
289
+
290
+ // Remove items
291
+ sparseSet.remove(entity1);
292
+
293
+ // Check existence
294
+ const exists = sparseSet.has(entity2);
295
+
296
+ // Iterate
297
+ for (const entity of sparseSet) {
298
+ // Process entity
299
+ }
300
+
301
+ // Get size
302
+ const size = sparseSet.size;
303
+
304
+ // Clear all
305
+ sparseSet.clear();
306
+ ```
307
+
308
+ ### Time
309
+
310
+ Utility singleton class for managing time and delta time calculations.
311
+
312
+ ```typescript
313
+ import { Time } from "@jael/core";
314
+
315
+ Time.start();
316
+
317
+ // Access time properties
318
+ const dt = Time.delta; // Delta time
319
+ const elapsed = Time.elapsed; // Total elapsed time
320
+
321
+ // Events
322
+ time.on("update", () => {
323
+ console.log(`Frame: ${dt}ms, Total: ${total}ms`);
324
+ });
325
+ ```
326
+
327
+ ### Event Registry
328
+
329
+ Base class providing event emission and listening capabilities.
330
+
331
+ ```typescript
332
+ interface WorldEvents {
333
+ entityCreated: { entity: Entity };
334
+ entityDestroyed: { entity: Entity };
335
+ componentAdded: { entity: Entity; component: string };
336
+ componentRemoved: { entity: Entity; component: string };
337
+ updated: void;
338
+ }
339
+
340
+ // Listen to events
341
+ world.on("entityCreated", (data) => {
342
+ // Handle event
343
+ });
344
+
345
+ // Emit events (handled internally by World)
346
+ world.emit("entityCreated", { entity: someEntity });
347
+
348
+ // Remove listeners
349
+ world.off("entityCreated", handler);
350
+
351
+ // Romeve all listeners of a type
352
+ world.clearEvent('type')
353
+
354
+ // Remove all listeners
355
+ world.clearAllEvents();
356
+ ```
357
+
358
+ ## Best Practices
359
+
360
+ ### 1. Component Design
361
+
362
+ ```typescript
363
+ // ✅ Good: Simple data containers
364
+ interface Position {
365
+ x: number;
366
+ y: number;
367
+ }
368
+
369
+ interface Health {
370
+ current: number;
371
+ max: number;
372
+ }
373
+
374
+ // ❌ Avoid: Methods in components
375
+ interface BadComponent {
376
+ x: number;
377
+ move(): void; // Put this in a system!
378
+ }
379
+ ```
380
+
381
+ ### 2. System Organization
382
+
383
+ ```typescript
384
+ // Organize systems by functionality and priority
385
+ const INPUT_PRIORITY = 0;
386
+ const PHYSICS_PRIORITY = 50;
387
+ const LOGIC_PRIORITY = 100;
388
+ const RENDER_PRIORITY = 200;
389
+
390
+ const inputSystem = { priority: INPUT_PRIORITY /* ... */ };
391
+ const physicsSystem = { priority: PHYSICS_PRIORITY /* ... */ };
392
+ const renderSystem = { priority: RENDER_PRIORITY /* ... */ };
393
+ ```
394
+
395
+ ### 3. Query Optimization
396
+
397
+ ```typescript
398
+ // ✅ Good: Cache queries when possible
399
+ class MovementSystem implements System {
400
+ private movementQuery: Query;
401
+ priority: number = 1
402
+
403
+ constructor(private world: World) {
404
+ this.movementQuery = world.include('position', 'velocity');
405
+ }
406
+
407
+ update(dt?: number) {
408
+ for (const entity of this.movementQuery.entities) {
409
+ // Process movement
410
+ }
411
+ }
412
+ }
413
+
414
+ // ✅ Also good: Use world.include/exclude for simple cases
415
+ update(dt?: number) {
416
+ const entities = this.world.include('position', 'velocity');
417
+ // ...
418
+ }
419
+ ```
420
+
421
+ ### 4. Memory Management
422
+
423
+ ```typescript
424
+ // Remember to clean up when removing entities
425
+ world.destroy(entity); // Automatically removes all components
426
+
427
+ // Clean up systems if they have resources
428
+ system.exit?.(); // Called automatically when removed from world
429
+ ```
430
+
431
+ ## Advanced Usage
432
+
433
+ ### Custom Events
434
+
435
+ ```typescript
436
+ // Extend world with custom events
437
+ interface CustomWorldEvents extends WorldEvents {
438
+ playerScored: { points: number };
439
+ gameOver: void;
440
+ }
441
+
442
+ const world = new World() as any as EventRegistry<CustomWorldEvents>;
443
+
444
+ // Emit custom events
445
+ world.emit("playerScored", { points: 100 });
446
+
447
+ // Listen to custom events
448
+ world.on("playerScored", ({ points }) => {
449
+ updateScore(points);
450
+ });
451
+ ```
452
+
453
+ ## Contributing
454
+
455
+ Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
456
+
457
+ ### Development Setup
458
+
459
+ ```bash
460
+ # Clone the repository
461
+ git clone https://github.com/cammyb1/jael.git
462
+ cd jael
463
+
464
+ # Install dependencies
465
+ npm install
466
+
467
+ # Start development
468
+ npm run dev
469
+
470
+ # Build
471
+ npm run build
472
+ ```
473
+
474
+ ## License
475
+
476
+ [MIT](https://choosealicense.com/licenses/mit/) - see the [LICENSE](LICENSE) file for details.
477
+
478
+ ## Acknowledgments
479
+
480
+ - Inspiration from ECS frameworks like [ECSY](https://github.com/ecsyjs/ecsy) and [Bevy](https://github.com/bevyengine/bevy)
481
+ - TypeScript for providing excellent type safety and developer experience
482
+
483
+ ---
484
+
485
+ <div align="center">
486
+
487
+ [⭐ Star this repo if it helped you!](https://github.com/cammyb1/jael)
488
+
489
+ [☕ You can buy me a coffee :)](https://buymeacoffee.com/jonathanva5)
490
+
491
+ **Built with ❤️ by [cammyb1](https://github.com/cammyb1)**
492
+
493
+ </div>
@@ -0,0 +1,23 @@
1
+ import { Entity } from './EntityManager';
2
+ import { default as EventRegistry } from './EventRegistry';
3
+ import { default as World } from './World';
4
+ export type ComponentSchema = Record<string, any>;
5
+ export interface ComponentManagerEvents {
6
+ add: {
7
+ entity: Entity;
8
+ component: keyof ComponentSchema;
9
+ };
10
+ remove: {
11
+ entity: Entity;
12
+ component: keyof ComponentSchema;
13
+ };
14
+ }
15
+ export declare class ComponentManager extends EventRegistry<ComponentManagerEvents> {
16
+ componentSet: Map<number, ComponentSchema>;
17
+ world: World;
18
+ constructor(world: World);
19
+ addComponent<K extends keyof ComponentSchema>(entity: Entity, key: K, value: ComponentSchema[K]): void;
20
+ getComponent<K extends keyof ComponentSchema>(entity: Entity, key: K): ComponentSchema[K] | undefined;
21
+ hasComponent<K extends keyof ComponentSchema>(entity: Entity, key: K): boolean;
22
+ removeComponent<K extends keyof ComponentSchema>(entity: Entity, key: K): void;
23
+ }
@@ -0,0 +1,47 @@
1
+ import { default as EventRegistry } from './EventRegistry';
2
+ import { SparseSet } from './SparseSet';
3
+ import { default as World } from './World';
4
+ declare class Entity {
5
+ readonly id: number;
6
+ private _world;
7
+ constructor(world: World, id: number);
8
+ /**
9
+ * Add component to current entity.
10
+ * @param compType Component name
11
+ * @param compValue Component value
12
+ */
13
+ add(compType: string, compValue: any): void;
14
+ /**
15
+ * Remove component of current entity.
16
+ * @param compType Component name
17
+ */
18
+ remove(compType: string): void;
19
+ /**
20
+ * Check if current entity has a component.
21
+ * @param compType Component name
22
+ * @returns boolean
23
+ */
24
+ has(compKey: string): boolean;
25
+ /**
26
+ * Get passed component schema of current entity.
27
+ * @param compType Component name
28
+ * @returns Return component schema with T(any as default) as type
29
+ */
30
+ get<T = any>(compType: string): T;
31
+ }
32
+ export declare class EntityManager extends EventRegistry<EntityManagerEvents> {
33
+ entityMap: SparseSet<Entity>;
34
+ nextId: number;
35
+ _world: World;
36
+ constructor(world: World);
37
+ get entities(): SparseSet<Entity>;
38
+ create(): Entity;
39
+ exist(entity: Entity): boolean;
40
+ size(): number;
41
+ destroy(entity: Entity): Entity;
42
+ }
43
+ export interface EntityManagerEvents {
44
+ create: Entity;
45
+ destroy: Entity;
46
+ }
47
+ export { type Entity };
@@ -0,0 +1,11 @@
1
+ export type Event<E> = (e: E[keyof E]) => void;
2
+ export default class EventRegistry<E extends Record<string, any> = {}> {
3
+ private _listeners;
4
+ on(type: keyof E, callback: Event<E>): void;
5
+ off(type: keyof E, callback: Event<E>): void;
6
+ once(type: keyof E, callback: Event<E>): void;
7
+ clearEvent(type: keyof E): void;
8
+ clearAllEvents(): void;
9
+ contains(type: keyof E, callback: Event<E>): boolean;
10
+ emit(type: keyof E, data: E[keyof E]): void;
11
+ }
@@ -0,0 +1,20 @@
1
+ import { Entity } from './EntityManager';
2
+ import { SparseSet } from './SparseSet';
3
+ import { default as World } from './World';
4
+ export interface QueryConfig {
5
+ include: string[];
6
+ exclude: string[];
7
+ }
8
+ export declare class Query {
9
+ config: QueryConfig;
10
+ entityMap: SparseSet<Entity>;
11
+ world: World;
12
+ constructor(config: QueryConfig, world: World);
13
+ hasComponents(entity: Entity): boolean;
14
+ get entities(): SparseSet<Entity>;
15
+ include(...comps: string[]): Query;
16
+ exclude(...comps: string[]): Query;
17
+ private _checkExistingEntities;
18
+ checkEntities(): void;
19
+ static getHash(config: QueryConfig): number;
20
+ }
@@ -0,0 +1,19 @@
1
+ export declare class SparseSet<V> {
2
+ denseValues: V[];
3
+ sparse: Map<V, number>;
4
+ [Symbol.iterator](): {
5
+ next: () => {
6
+ value: V;
7
+ done: boolean;
8
+ };
9
+ };
10
+ get values(): V[];
11
+ first(): V;
12
+ add(item: V): void;
13
+ indexOf(item: V): number;
14
+ remove(item: V): void;
15
+ forEach(predicate: (item: V) => void): void;
16
+ size(): number;
17
+ clear(): void;
18
+ has(item: V): boolean;
19
+ }
@@ -0,0 +1,12 @@
1
+ export interface System {
2
+ priority: number;
3
+ exit?(): void;
4
+ update(dt?: number): void;
5
+ }
6
+ export declare class SystemManager {
7
+ systemList: System[];
8
+ addSystem(system: System): void;
9
+ reorder(): void;
10
+ has(system: System): boolean;
11
+ removeSystem(system: System): void;
12
+ }
package/dist/Time.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { default as EventRegistry } from './EventRegistry';
2
+ export interface TimeEvents {
3
+ update: void;
4
+ }
5
+ declare class TimeSingleton extends EventRegistry<TimeEvents> {
6
+ private _startTime;
7
+ private _oldTime;
8
+ private _requestId;
9
+ running: boolean;
10
+ delta: number;
11
+ elapsed: number;
12
+ constructor();
13
+ private _loop;
14
+ start(): void;
15
+ stop(): void;
16
+ }
17
+ export declare let Time: TimeSingleton;
18
+ export {};
@@ -0,0 +1,43 @@
1
+ import { ComponentManager, ComponentSchema } from './ComponentManager';
2
+ import { EntityManager, Entity } from './EntityManager';
3
+ import { default as EventRegistry } from './EventRegistry';
4
+ import { Query, QueryConfig } from './Query';
5
+ import { SparseSet } from './SparseSet';
6
+ import { SystemManager, System } from './SystemManager';
7
+ export interface WorldEvents {
8
+ entityCreated: {
9
+ entity: Entity;
10
+ };
11
+ entityDestroyed: {
12
+ entity: Entity;
13
+ };
14
+ componentAdded: {
15
+ entity: Entity;
16
+ component: keyof ComponentSchema;
17
+ };
18
+ componentRemoved: {
19
+ entity: Entity;
20
+ component: keyof ComponentSchema;
21
+ };
22
+ updated: void;
23
+ }
24
+ export default class World extends EventRegistry<WorldEvents> {
25
+ entityManager: EntityManager;
26
+ componentManager: ComponentManager;
27
+ systemManager: SystemManager;
28
+ queries: Map<number, Query>;
29
+ constructor();
30
+ get entities(): SparseSet<Entity>;
31
+ query(config: QueryConfig): Query;
32
+ private _updateQueries;
33
+ exist(entity: Entity): boolean;
34
+ include(...comps: string[]): Query;
35
+ exclude(...comps: string[]): Query;
36
+ create(): Entity;
37
+ destroy(entity: Entity): void;
38
+ addSystem(sys: System): void;
39
+ removeSystem(sys: System): void;
40
+ addComponent(entity: Entity, compKey: string, compValue: any): void;
41
+ removeComponent(entity: Entity, compKey: string): void;
42
+ update(): void;
43
+ }
@@ -0,0 +1,9 @@
1
+ import { default as EventRegistry } from './EventRegistry';
2
+ import { System, SystemManager } from './SystemManager';
3
+ import { Entity, EntityManager, EntityManagerEvents } from './EntityManager';
4
+ import { ComponentSchema, ComponentManager, ComponentManagerEvents } from './ComponentManager';
5
+ import { Query, QueryConfig } from './Query';
6
+ import { SparseSet } from './SparseSet';
7
+ import { Time, TimeEvents } from './Time';
8
+ import { default as World, WorldEvents } from './World';
9
+ export { type System, type Entity, type EntityManagerEvents, type ComponentManagerEvents, type ComponentSchema, type QueryConfig, type TimeEvents, type WorldEvents, Query, World, Time, SparseSet, EventRegistry, SystemManager, EntityManager, ComponentManager, };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class h{_listeners=new Map;on(e,t){if(this.contains(e,t))return;const s=this._listeners.get(e);s?s.add(t):this._listeners.set(e,new Set([t]))}off(e,t){if(!this.contains(e,t))return;const s=this._listeners.get(e);s&&s.delete(t)}once(e,t){const s=(i=>{t(i),this.off(e,s)});this.on(e,s)}clearEvent(e){this._listeners.get(e)&&this._listeners.get(e)?.clear()}clearAllEvents(){this._listeners.forEach(e=>e.clear()),this._listeners.clear()}contains(e,t){return this._listeners.get(e)?this._listeners.get(e).has(t):!1}emit(e,t){this._listeners.get(e)&&this._listeners.get(e)?.forEach(s=>{s(t)})}}class u{systemList=[];addSystem(e){this.systemList.push(e),this.reorder()}reorder(){this.systemList.sort((e,t)=>e.priority-t.priority)}has(e){return this.systemList.indexOf(e)>0}removeSystem(e){if(!this.has(e))return;const t=this.systemList.indexOf(e);t>=0&&(this.systemList.splice(t,1),e.exit?.(),this.reorder())}}class d{denseValues=[];sparse=new Map;[Symbol.iterator](){let e=this.values.length;const t={value:void 0,done:!1};return{next:()=>(t.value=this.values[--e],t.done=e<0,t)}}get values(){return this.denseValues}first(){return this.denseValues[0]}add(e){this.has(e)||(this.denseValues.push(e),this.sparse.set(e,this.denseValues.length-1))}indexOf(e){return this.sparse.get(e)?this.sparse.get(e):-1}remove(e){if(!this.has(e))return;const t=this.sparse.get(e);this.sparse.delete(e);const s=this.denseValues[this.denseValues.length-1];s!==e&&(this.denseValues[t]=s,this.sparse.set(s,t)),this.denseValues.pop()}forEach(e){for(let t of this)e(t)}size(){return this.denseValues.length}clear(){for(let e of this)this.remove(e)}has(e){return this.sparse.has(e)}}class m{id;_world;constructor(e,t){this.id=t,this._world=e}add(e,t){this._world.addComponent(this,e,t)}remove(e){this._world.removeComponent(this,e)}has(e){return this._world.componentManager.hasComponent(this,e)}get(e){return this._world.componentManager.getComponent(this,e)}}class c extends h{entityMap=new d;nextId=0;_world;constructor(e){super(),this._world=e}get entities(){return this.entityMap}create(){const e=this.nextId++,t=new m(this._world,e);return this.entities.add(t),this.emit("create",t),t}exist(e){return this.entities.has(e)}size(){return this.entities.size()}destroy(e){return this.entities.remove(e),this.emit("destroy",e),e}}class l extends h{componentSet=new Map;world;constructor(e){super(),this.world=e,this.world.on("entityDestroyed",t=>{t&&t.entity&&this.componentSet.get(t.entity.id)&&this.componentSet.delete(t.entity.id)})}addComponent(e,t,s){const i=this.componentSet.get(e.id);i?i[t]=s:this.componentSet.set(e.id,{[t]:s}),this.emit("add",{entity:e,component:t})}getComponent(e,t){return this.hasComponent(e,t)?this.componentSet.get(e.id)[t]:void 0}hasComponent(e,t){const s=this.componentSet.get(e.id);return s?t in s:!1}removeComponent(e,t){if(!this.componentSet.get(e.id))return;const s=this.componentSet.get(e.id);s&&s[t]!==void 0&&(delete s[t],Object.keys(s).length===0&&this.componentSet.delete(e.id),this.emit("remove",{entity:e,component:t}))}}class a{config;entityMap;world;constructor(e,t){this.config=e,this.world=t,this.entityMap=new d}hasComponents(e){return this.config.include?.every(t=>e.has(t))&&this.config.exclude?.every(t=>!e.has(t))}get entities(){return this.entityMap}include(...e){return this.world.include(...e)}exclude(...e){return this.world.exclude(...e)}_checkExistingEntities(){for(let e of this.entities)this.world.exist(e)||this.entityMap.remove(e)}checkEntities(){for(let e of this.world.entities)e&&this.hasComponents(e)&&this.entityMap.add(e);this._checkExistingEntities()}static getHash(e){const t=e.include?.map(n=>n.trim()).filter(n=>n).join("_"),s=e.exclude?.map(n=>n.trim()).filter(n=>n).join("_"),i="in_"+t+"_out_"+s;let o=0;for(const n of i)o=(o<<5)-o+n.charCodeAt(0),o|=0;return o}}class p extends h{_startTime=0;_oldTime=0;_requestId=0;running=!1;delta=0;elapsed=0;constructor(){super()}_loop(){let e=0;if(this.running){const t=performance.now();e=(t-this._oldTime)/1e3,this._oldTime=t,this.elapsed+=e}this.delta=e,this.emit("update"),this._requestId=requestAnimationFrame(this._loop.bind(this))}start(){this._startTime=performance.now(),this._oldTime=this._startTime,this.elapsed=0,this.delta=0,this.running=!0,this._loop()}stop(){this.running=!1,cancelAnimationFrame(this._requestId),this._requestId=0}}let g=new p;class f extends h{entityManager;componentManager;systemManager;queries;constructor(){super(),this.entityManager=new c(this),this.componentManager=new l(this),this.systemManager=new u,this.entityManager.on("create",e=>{e&&this.emit("entityCreated",{entity:e}),this._updateQueries()}),this.entityManager.on("destroy",e=>{e&&this.emit("entityDestroyed",{entity:e}),this._updateQueries()}),this.componentManager.on("add",e=>{this.emit("componentAdded",{entity:e.entity,component:e.component}),this._updateQueries()}),this.componentManager.on("remove",e=>{this.emit("componentRemoved",{entity:e.entity,component:e.component}),this._updateQueries()}),this.queries=new Map}get entities(){return this.entityManager.entities}query(e){const t=a.getHash(e);let i=this.queries.get(t);return i||(i=new a(e,this),this.queries.set(t,i),this._updateQueries()),i}_updateQueries(){this.queries.forEach(e=>e.checkEntities())}exist(e){return this.entityManager.exist(e)}include(...e){return this.query({include:e,exclude:[]})}exclude(...e){return this.query({include:[],exclude:e})}create(){return this.entityManager.create()}destroy(e){this.entityManager.destroy(e)}addSystem(e){this.systemManager.addSystem(e)}removeSystem(e){this.systemManager.removeSystem(e)}addComponent(e,t,s){this.componentManager.addComponent(e,t,s)}removeComponent(e,t){this.componentManager.removeComponent(e,t)}update(){this.systemManager.systemList.forEach(e=>{e.update()}),this.emit("updated")}}exports.ComponentManager=l;exports.EntityManager=c;exports.EventRegistry=h;exports.Query=a;exports.SparseSet=d;exports.SystemManager=u;exports.Time=g;exports.World=f;
@@ -0,0 +1,332 @@
1
+ class h {
2
+ _listeners = /* @__PURE__ */ new Map();
3
+ on(e, t) {
4
+ if (this.contains(e, t))
5
+ return;
6
+ const s = this._listeners.get(e);
7
+ s ? s.add(t) : this._listeners.set(e, /* @__PURE__ */ new Set([t]));
8
+ }
9
+ off(e, t) {
10
+ if (!this.contains(e, t))
11
+ return;
12
+ const s = this._listeners.get(e);
13
+ s && s.delete(t);
14
+ }
15
+ once(e, t) {
16
+ const s = ((i) => {
17
+ t(i), this.off(e, s);
18
+ });
19
+ this.on(e, s);
20
+ }
21
+ clearEvent(e) {
22
+ this._listeners.get(e) && this._listeners.get(e)?.clear();
23
+ }
24
+ clearAllEvents() {
25
+ this._listeners.forEach((e) => e.clear()), this._listeners.clear();
26
+ }
27
+ contains(e, t) {
28
+ return this._listeners.get(e) ? this._listeners.get(e).has(t) : !1;
29
+ }
30
+ emit(e, t) {
31
+ this._listeners.get(e) && this._listeners.get(e)?.forEach((s) => {
32
+ s(t);
33
+ });
34
+ }
35
+ }
36
+ class u {
37
+ systemList = [];
38
+ addSystem(e) {
39
+ this.systemList.push(e), this.reorder();
40
+ }
41
+ reorder() {
42
+ this.systemList.sort((e, t) => e.priority - t.priority);
43
+ }
44
+ has(e) {
45
+ return this.systemList.indexOf(e) > 0;
46
+ }
47
+ removeSystem(e) {
48
+ if (!this.has(e)) return;
49
+ const t = this.systemList.indexOf(e);
50
+ t >= 0 && (this.systemList.splice(t, 1), e.exit?.(), this.reorder());
51
+ }
52
+ }
53
+ class d {
54
+ denseValues = [];
55
+ sparse = /* @__PURE__ */ new Map();
56
+ [Symbol.iterator]() {
57
+ let e = this.values.length;
58
+ const t = {
59
+ value: void 0,
60
+ done: !1
61
+ };
62
+ return {
63
+ next: () => (t.value = this.values[--e], t.done = e < 0, t)
64
+ };
65
+ }
66
+ get values() {
67
+ return this.denseValues;
68
+ }
69
+ first() {
70
+ return this.denseValues[0];
71
+ }
72
+ add(e) {
73
+ this.has(e) || (this.denseValues.push(e), this.sparse.set(e, this.denseValues.length - 1));
74
+ }
75
+ indexOf(e) {
76
+ return this.sparse.get(e) ? this.sparse.get(e) : -1;
77
+ }
78
+ remove(e) {
79
+ if (!this.has(e)) return;
80
+ const t = this.sparse.get(e);
81
+ this.sparse.delete(e);
82
+ const s = this.denseValues[this.denseValues.length - 1];
83
+ s !== e && (this.denseValues[t] = s, this.sparse.set(s, t)), this.denseValues.pop();
84
+ }
85
+ forEach(e) {
86
+ for (let t of this)
87
+ e(t);
88
+ }
89
+ size() {
90
+ return this.denseValues.length;
91
+ }
92
+ clear() {
93
+ for (let e of this)
94
+ this.remove(e);
95
+ }
96
+ has(e) {
97
+ return this.sparse.has(e);
98
+ }
99
+ }
100
+ class c {
101
+ id;
102
+ _world;
103
+ constructor(e, t) {
104
+ this.id = t, this._world = e;
105
+ }
106
+ /**
107
+ * Add component to current entity.
108
+ * @param compType Component name
109
+ * @param compValue Component value
110
+ */
111
+ add(e, t) {
112
+ this._world.addComponent(this, e, t);
113
+ }
114
+ /**
115
+ * Remove component of current entity.
116
+ * @param compType Component name
117
+ */
118
+ remove(e) {
119
+ this._world.removeComponent(this, e);
120
+ }
121
+ /**
122
+ * Check if current entity has a component.
123
+ * @param compType Component name
124
+ * @returns boolean
125
+ */
126
+ has(e) {
127
+ return this._world.componentManager.hasComponent(this, e);
128
+ }
129
+ /**
130
+ * Get passed component schema of current entity.
131
+ * @param compType Component name
132
+ * @returns Return component schema with T(any as default) as type
133
+ */
134
+ get(e) {
135
+ return this._world.componentManager.getComponent(this, e);
136
+ }
137
+ }
138
+ class l extends h {
139
+ entityMap = new d();
140
+ nextId = 0;
141
+ _world;
142
+ constructor(e) {
143
+ super(), this._world = e;
144
+ }
145
+ get entities() {
146
+ return this.entityMap;
147
+ }
148
+ create() {
149
+ const e = this.nextId++, t = new c(this._world, e);
150
+ return this.entities.add(t), this.emit("create", t), t;
151
+ }
152
+ exist(e) {
153
+ return this.entities.has(e);
154
+ }
155
+ size() {
156
+ return this.entities.size();
157
+ }
158
+ destroy(e) {
159
+ return this.entities.remove(e), this.emit("destroy", e), e;
160
+ }
161
+ }
162
+ class m extends h {
163
+ componentSet = /* @__PURE__ */ new Map();
164
+ world;
165
+ constructor(e) {
166
+ super(), this.world = e, this.world.on("entityDestroyed", (t) => {
167
+ t && t.entity && this.componentSet.get(t.entity.id) && this.componentSet.delete(t.entity.id);
168
+ });
169
+ }
170
+ addComponent(e, t, s) {
171
+ const i = this.componentSet.get(
172
+ e.id
173
+ );
174
+ i ? i[t] = s : this.componentSet.set(e.id, { [t]: s }), this.emit("add", { entity: e, component: t });
175
+ }
176
+ getComponent(e, t) {
177
+ return this.hasComponent(e, t) ? this.componentSet.get(e.id)[t] : void 0;
178
+ }
179
+ hasComponent(e, t) {
180
+ const s = this.componentSet.get(e.id);
181
+ return s ? t in s : !1;
182
+ }
183
+ removeComponent(e, t) {
184
+ if (!this.componentSet.get(e.id)) return;
185
+ const s = this.componentSet.get(e.id);
186
+ s && s[t] !== void 0 && (delete s[t], Object.keys(s).length === 0 && this.componentSet.delete(e.id), this.emit("remove", { entity: e, component: t }));
187
+ }
188
+ }
189
+ class a {
190
+ config;
191
+ entityMap;
192
+ world;
193
+ constructor(e, t) {
194
+ this.config = e, this.world = t, this.entityMap = new d();
195
+ }
196
+ hasComponents(e) {
197
+ return this.config.include?.every((t) => e.has(t)) && this.config.exclude?.every((t) => !e.has(t));
198
+ }
199
+ get entities() {
200
+ return this.entityMap;
201
+ }
202
+ include(...e) {
203
+ return this.world.include(...e);
204
+ }
205
+ exclude(...e) {
206
+ return this.world.exclude(...e);
207
+ }
208
+ _checkExistingEntities() {
209
+ for (let e of this.entities)
210
+ this.world.exist(e) || this.entityMap.remove(e);
211
+ }
212
+ checkEntities() {
213
+ for (let e of this.world.entities)
214
+ e && this.hasComponents(e) && this.entityMap.add(e);
215
+ this._checkExistingEntities();
216
+ }
217
+ static getHash(e) {
218
+ const t = e.include?.map((n) => n.trim()).filter((n) => n).join("_"), s = e.exclude?.map((n) => n.trim()).filter((n) => n).join("_"), i = "in_" + t + "_out_" + s;
219
+ let o = 0;
220
+ for (const n of i)
221
+ o = (o << 5) - o + n.charCodeAt(0), o |= 0;
222
+ return o;
223
+ }
224
+ }
225
+ class p extends h {
226
+ _startTime = 0;
227
+ _oldTime = 0;
228
+ _requestId = 0;
229
+ running = !1;
230
+ delta = 0;
231
+ elapsed = 0;
232
+ constructor() {
233
+ super();
234
+ }
235
+ _loop() {
236
+ let e = 0;
237
+ if (this.running) {
238
+ const t = performance.now();
239
+ e = (t - this._oldTime) / 1e3, this._oldTime = t, this.elapsed += e;
240
+ }
241
+ this.delta = e, this.emit("update"), this._requestId = requestAnimationFrame(this._loop.bind(this));
242
+ }
243
+ start() {
244
+ this._startTime = performance.now(), this._oldTime = this._startTime, this.elapsed = 0, this.delta = 0, this.running = !0, this._loop();
245
+ }
246
+ stop() {
247
+ this.running = !1, cancelAnimationFrame(this._requestId), this._requestId = 0;
248
+ }
249
+ }
250
+ let g = new p();
251
+ class f extends h {
252
+ entityManager;
253
+ componentManager;
254
+ systemManager;
255
+ queries;
256
+ constructor() {
257
+ super(), this.entityManager = new l(this), this.componentManager = new m(this), this.systemManager = new u(), this.entityManager.on("create", (e) => {
258
+ e && this.emit("entityCreated", { entity: e }), this._updateQueries();
259
+ }), this.entityManager.on("destroy", (e) => {
260
+ e && this.emit("entityDestroyed", { entity: e }), this._updateQueries();
261
+ }), this.componentManager.on(
262
+ "add",
263
+ (e) => {
264
+ this.emit("componentAdded", {
265
+ entity: e.entity,
266
+ component: e.component
267
+ }), this._updateQueries();
268
+ }
269
+ ), this.componentManager.on(
270
+ "remove",
271
+ (e) => {
272
+ this.emit("componentRemoved", {
273
+ entity: e.entity,
274
+ component: e.component
275
+ }), this._updateQueries();
276
+ }
277
+ ), this.queries = /* @__PURE__ */ new Map();
278
+ }
279
+ get entities() {
280
+ return this.entityManager.entities;
281
+ }
282
+ query(e) {
283
+ const t = a.getHash(e);
284
+ let i = this.queries.get(t);
285
+ return i || (i = new a(e, this), this.queries.set(t, i), this._updateQueries()), i;
286
+ }
287
+ _updateQueries() {
288
+ this.queries.forEach((e) => e.checkEntities());
289
+ }
290
+ exist(e) {
291
+ return this.entityManager.exist(e);
292
+ }
293
+ include(...e) {
294
+ return this.query({ include: e, exclude: [] });
295
+ }
296
+ exclude(...e) {
297
+ return this.query({ include: [], exclude: e });
298
+ }
299
+ create() {
300
+ return this.entityManager.create();
301
+ }
302
+ destroy(e) {
303
+ this.entityManager.destroy(e);
304
+ }
305
+ addSystem(e) {
306
+ this.systemManager.addSystem(e);
307
+ }
308
+ removeSystem(e) {
309
+ this.systemManager.removeSystem(e);
310
+ }
311
+ addComponent(e, t, s) {
312
+ this.componentManager.addComponent(e, t, s);
313
+ }
314
+ removeComponent(e, t) {
315
+ this.componentManager.removeComponent(e, t);
316
+ }
317
+ update() {
318
+ this.systemManager.systemList.forEach((e) => {
319
+ e.update();
320
+ }), this.emit("updated");
321
+ }
322
+ }
323
+ export {
324
+ m as ComponentManager,
325
+ l as EntityManager,
326
+ h as EventRegistry,
327
+ a as Query,
328
+ d as SparseSet,
329
+ u as SystemManager,
330
+ g as Time,
331
+ f as World
332
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@jael-ecs/core",
3
+ "version": "1.0.0",
4
+ "description": "Entity Component System library with typescript",
5
+ "keywords": [
6
+ "ecs",
7
+ "ts"
8
+ ],
9
+ "homepage": "https://github.com/cammyb1/jael#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/cammyb1/jael/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/cammyb1/jael.git"
16
+ },
17
+ "main": "./dist/jael-build.cjs",
18
+ "module": "./dist/jael-build.js",
19
+ "types": "./dist/index.d.ts",
20
+ "license": "MIT",
21
+ "author": "cammyb1",
22
+ "type": "module",
23
+ "files": [
24
+ "dist/**",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "dev": "vite",
30
+ "build": "tsc && vite build",
31
+ "preview": "vite preview"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "~5.9.3",
35
+ "unplugin-dts": "^1.0.0-beta.6",
36
+ "vite": "^7.2.4"
37
+ }
38
+ }