@perplexdotgg/mecs 0.1.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 +300 -0
- package/build/index.d.ts +131 -0
- package/build/mecs.js +851 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 9547-3732 Québec inc. / perplex.gg
|
|
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,300 @@
|
|
|
1
|
+
<div style="display: flex;">
|
|
2
|
+
<img src="./mecs.svg" width="152" height="64" style="margin-right: 16px;" />
|
|
3
|
+
<h1>MECS - Monomorph Entity Component System</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
MECS is an [ECS](https://en.wikipedia.org/wiki/Entity_component_system) library for Typescript and Javascript games and simulations, built around [Monomorph](http://codeberg.org/perplexdotgg/monomorph), a library for performant objects. The API is inspired by [Miniplex](https://github.com/hmans/miniplex), though the implementation is quite different.
|
|
7
|
+
|
|
8
|
+
MECS focuses on the developer's experience in their IDE, especially around auto-complete and compile-type type-checking, while maintaining very high performance, about as high as you can achieve with an [AoS design](https://en.wikipedia.org/wiki/AoS_and_SoA).
|
|
9
|
+
|
|
10
|
+
<details>
|
|
11
|
+
<summary>Why not use SoA design?</summary>
|
|
12
|
+
While SoA can achieve better performance in the best cases, it tends to be unwieldy, especially for nested fields, and generally ECS implementations lack auto-complete for components and properties.
|
|
13
|
+
</details>
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
- Entities and monomorph-based components automatically gain **object-pooling** features, eliminating most garbage collection through automatic object re-use. Just create and destroy entities and components, and the pooling and re-use is taken care of.
|
|
17
|
+
- Simple and minimal: There are only entities, components and queries
|
|
18
|
+
- Queries, aka archetypes, automatically match and track entities based on which components they have, or don't have
|
|
19
|
+
- Strong compile-time type safety if you use Typescript (recommended)
|
|
20
|
+
- Coming soon: automatic serialization/deserialization (to/from number arrays) of entities (fully or a subset of components) to create snapshots
|
|
21
|
+
|
|
22
|
+
MECS creates a single entity pool, or world, when it creates your entity class from the schema you provide.
|
|
23
|
+
|
|
24
|
+
MECS has no built-in concept of "systems" in the ECS sense, instead it provides queries to help you quickly write efficient code that processes specific archetypes of entities. Your systems can just be functions that iterate over entities matching a query, and your systems can also subscribe to entities newly matching, or no longer matching, a query
|
|
25
|
+
|
|
26
|
+
MECS has been created primarily around having a single entity schema/pool where all entities _can_ have all possible components types, but most entities only have a subset of them. If all your entities have all the same components, it is likely simpler to use [Monomorph](http://codeberg.org/perplexdotgg/monomorph) on its own.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Install into your project
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @perplexdotgg/mecs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Defining an Entity schema to create your Entity class
|
|
38
|
+
|
|
39
|
+
The following is Typescript code. The Javascript version is below this.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createClass } from 'monomorph';
|
|
43
|
+
import { createEntityClass, type QueryMap, type VirtualComponent } from '@perplexdotgg/mecs';
|
|
44
|
+
|
|
45
|
+
// imagine some monomorph classes in your project
|
|
46
|
+
// see the monomorph docs for more on those:
|
|
47
|
+
// https://codeberg.org/perplexdotgg/monomorph
|
|
48
|
+
class Projectile extends createClass<Projectile, typeof projectileProps>(projectileProps) {}
|
|
49
|
+
class Transform extends createClass<Transform, typeof transformProps>(transformProps) {}
|
|
50
|
+
|
|
51
|
+
// and some non-monomorph class
|
|
52
|
+
class MyClass {}
|
|
53
|
+
|
|
54
|
+
// define all the components you want entities to potentially have
|
|
55
|
+
const components = {
|
|
56
|
+
// defining components when they are monomorph classes:
|
|
57
|
+
localTransform: Transform,
|
|
58
|
+
globalTransform: Transform,
|
|
59
|
+
Projectile, // this is shorthand for: projectile: Projectile
|
|
60
|
+
|
|
61
|
+
// for non-monomorph classes, pass a key with value null.
|
|
62
|
+
// this syntax allows for the correct types to be expected and inferred.
|
|
63
|
+
// you are responsible for creating and managing
|
|
64
|
+
// this field, including any needed clean-up before it is removed
|
|
65
|
+
myClass: null! as VirtualComponent<MyClass>,
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
const queries = {
|
|
69
|
+
projectiles: {
|
|
70
|
+
// an entity must have all of these components to match this query
|
|
71
|
+
with: ['globalTransform', 'projectile'],
|
|
72
|
+
// AND it can have none of these components to match this query
|
|
73
|
+
without: ['myClass'],
|
|
74
|
+
|
|
75
|
+
// listeners can added now, or later (see the Queries docs below)
|
|
76
|
+
afterEntityAdded: (entity) => {
|
|
77
|
+
// this is called when an entity newly matches this query
|
|
78
|
+
},
|
|
79
|
+
beforeEntityRemoved: (entity) => {
|
|
80
|
+
// this is called when an entity no longer matches this query,
|
|
81
|
+
// just before a relevant component is removed from the entity
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
query2: {
|
|
85
|
+
with: ['localTransform', 'globalTransform'],
|
|
86
|
+
without: [],
|
|
87
|
+
},
|
|
88
|
+
} as const satisfies QueryMap<Entity, typeof components>;
|
|
89
|
+
|
|
90
|
+
let debugLogging: boolean | undefined = false;
|
|
91
|
+
|
|
92
|
+
// automatically create our entity class from the schemas above
|
|
93
|
+
// note that there are two function calls after createEntityClass
|
|
94
|
+
// to circumvent a typescript limitation, see https://github.com/microsoft/TypeScript/issues/10571
|
|
95
|
+
class Entity extends createEntityClass<Entity>(debugLogging)(components, queries) {
|
|
96
|
+
|
|
97
|
+
// most methods will likely exist on your components directly,
|
|
98
|
+
// but you can also extend the generated entity class
|
|
99
|
+
myEntityMethod() {
|
|
100
|
+
// your code here! use 'this' like you would expect
|
|
101
|
+
if (this.hasComponent('example')) {
|
|
102
|
+
this.example.whatever = 5;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary>Javascript version</summary>
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
import { createClass } from 'monomorph';
|
|
113
|
+
import { createEntityClass } from '@perplexdotgg/mecs';
|
|
114
|
+
|
|
115
|
+
// imagine some monomorph classes in your project
|
|
116
|
+
// see the monomorph docs for more on those:
|
|
117
|
+
// https://codeberg.org/perplexdotgg/monomorph
|
|
118
|
+
class Projectile extends createClass(projectileProps) {}
|
|
119
|
+
class Transform extends createClass(transformProps) {}
|
|
120
|
+
|
|
121
|
+
// and some non-monomorph class
|
|
122
|
+
class MyClass {}
|
|
123
|
+
|
|
124
|
+
// define all the components you want entities to potentially have
|
|
125
|
+
const components = {
|
|
126
|
+
// defining components when they are monomorph classes:
|
|
127
|
+
localTransform: Transform,
|
|
128
|
+
globalTransform: Transform,
|
|
129
|
+
Projectile, // this is shorthand for: projectile: Projectile
|
|
130
|
+
|
|
131
|
+
// for non-monomorph classes, pass a key with value null.
|
|
132
|
+
// you are responsible for creating and managing
|
|
133
|
+
// this field, including any needed clean-up before it is removed
|
|
134
|
+
myClass: null,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const queries = {
|
|
138
|
+
projectiles: {
|
|
139
|
+
// an entity must have all of these components to match this query
|
|
140
|
+
with: ['globalTransform', 'projectile'],
|
|
141
|
+
// AND it can have none of these components to match this query
|
|
142
|
+
without: ['myClass'],
|
|
143
|
+
|
|
144
|
+
// listeners can added now, or later (see the Queries docs below)
|
|
145
|
+
afterEntityAdded: (entity) => {
|
|
146
|
+
// this is called when an entity newly matches this query
|
|
147
|
+
},
|
|
148
|
+
beforeEntityRemoved: (entity) => {
|
|
149
|
+
// this is called when an entity no longer matches this query,
|
|
150
|
+
// just before a relevant component is removed from the entity
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
query2: {
|
|
154
|
+
with: ['localTransform', 'globalTransform'],
|
|
155
|
+
without: [],
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let debugLogging = false;
|
|
160
|
+
|
|
161
|
+
// automatically create our entity class from the schemas above
|
|
162
|
+
// note that there are two function calls after createEntityClass
|
|
163
|
+
// to circumvent a typescript limitation (sorry, vanilla JS folks!)
|
|
164
|
+
class Entity extends createEntityClass(debugLogging)(components, queries) {
|
|
165
|
+
|
|
166
|
+
// most methods will likely exist on your components directly,
|
|
167
|
+
// but you can also extend the generated entity class
|
|
168
|
+
myEntityMethod() {
|
|
169
|
+
// your code here! use 'this' like you would expect
|
|
170
|
+
if (this.hasComponent('example')) {
|
|
171
|
+
this.example.whatever = 5;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
</details>
|
|
177
|
+
|
|
178
|
+
### Creating and destroying entities
|
|
179
|
+
Like monomorph's create method, entities accept the same object representations as inputs for each component. All components instances will be created automatically as needed
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const entity1 = Entity.create({
|
|
183
|
+
localTransform: {
|
|
184
|
+
position: { x: 0, y: 0, z: 0, },
|
|
185
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
186
|
+
scale: 1,
|
|
187
|
+
},
|
|
188
|
+
projectile: {
|
|
189
|
+
speed: 4,
|
|
190
|
+
direction: { x: 1, y: 0, z: 0 },
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// now entity1 is of the Entity class, and entity1.localTransform is of the Transform class
|
|
195
|
+
// so you can call methods on it like so:
|
|
196
|
+
entity1.localTransform.composeMatrix();
|
|
197
|
+
|
|
198
|
+
const entity2 = Entity.create({
|
|
199
|
+
projectile: {
|
|
200
|
+
speed: 4,
|
|
201
|
+
direction: { x: 1, y: 0, z: 0 },
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// you are responsible for instantiating your custom classes
|
|
205
|
+
myClass: new MyClass(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// destroy() to return the entity to the pool, it will automatically be re-used later
|
|
209
|
+
// this iterates on all components to remove them first, and triggers queries just
|
|
210
|
+
// before the component(s) needed for the query are deleted
|
|
211
|
+
// each destroyed component also gets recycled in its own component pool
|
|
212
|
+
entity2.destroy();
|
|
213
|
+
|
|
214
|
+
// note: if you need to do a cleanup on your custom class (like MyClass above),
|
|
215
|
+
// use a query's beforeEntityRemoved method/listeners for that purpose
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Accessing components and methods
|
|
219
|
+
```js
|
|
220
|
+
// calling component methods
|
|
221
|
+
entity1.projectile.checkCollisions();
|
|
222
|
+
|
|
223
|
+
// calling entity methods
|
|
224
|
+
entity1.myEntityMethod();
|
|
225
|
+
|
|
226
|
+
entity1.addComponent('globalTransform', {
|
|
227
|
+
position: { x: 1, y: 2, z: 3, },
|
|
228
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
229
|
+
scale: 1,
|
|
230
|
+
});
|
|
231
|
+
entity1.globalTransform.position.x += 5;
|
|
232
|
+
|
|
233
|
+
// add a component, using another component as input data
|
|
234
|
+
entity2.addComponent('localTransform', entity1.localTransform);
|
|
235
|
+
// ^^ they will not be the same objects, they will just have the same data
|
|
236
|
+
// this is also true for nested objects
|
|
237
|
+
|
|
238
|
+
// remove a component
|
|
239
|
+
entity1.removeComponent('projectile');
|
|
240
|
+
|
|
241
|
+
// checking if a component exists on an entity, two ways to do so:
|
|
242
|
+
if (entity1.globalTransform) { /* */ }
|
|
243
|
+
if (entity1.hasCompoonent('globalTransform')) { /* */ }
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Iterating on entities and components
|
|
248
|
+
```ts
|
|
249
|
+
// all entities
|
|
250
|
+
for (const entity of Entity.pool) {
|
|
251
|
+
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// only for monomorph-class components, iterating all instances of a component
|
|
255
|
+
for (const transform of Entity.components.localTransform) {
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// only for monomorph-class components, iterating all instances of a component,
|
|
260
|
+
// along with the entity for each
|
|
261
|
+
for (const [transform, entity] of Entity.componentsWithEntities.localTransform) {
|
|
262
|
+
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Iterating on queries, and adding/removing listeners
|
|
267
|
+
For defining queries, see the [entity schema definition section](#defining-an-entity-schema-to-create-your-entity-class) above
|
|
268
|
+
```ts
|
|
269
|
+
// all entities matching a query
|
|
270
|
+
for (const entity of Entity.queries.projectiles) {
|
|
271
|
+
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// in addition to being able to define `afterEntityAdded` and `beforeEntityRemoved` functions in
|
|
275
|
+
// the schema, you can dynamically add a new listener for entities matching a query,
|
|
276
|
+
// useful if you want to put this logic in your system code for example
|
|
277
|
+
function afterProjectileExists(entity) {
|
|
278
|
+
// do whatever
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Entity.queries.projectiles.afterEntityAdded.addListener(afterProjectileExists);
|
|
282
|
+
// you can also remove listeners
|
|
283
|
+
Entity.queries.projectiles.afterEntityAdded.removeListener(afterProjectileExists);
|
|
284
|
+
|
|
285
|
+
Entity.queries.projectiles.beforeEntityRemoved.addListener(someListener);
|
|
286
|
+
Entity.queries.projectiles.beforeEntityRemoved.removeListener(someListener);
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Roadmap
|
|
291
|
+
|
|
292
|
+
- Integrating the upcoming version of monomorph where non-monomorth classes are better supported (e.g. to allow a component to contain a reference to an html element)
|
|
293
|
+
- Example project/template coming soon
|
|
294
|
+
- Basic benchmarks
|
|
295
|
+
- Entity/component data debugging tool(s)
|
|
296
|
+
- Want to see something here? Please create an [issue on codeberg](https://codeberg.org/perplexdotgg/mecs/issues)
|
|
297
|
+
|
|
298
|
+
## How to contribute
|
|
299
|
+
|
|
300
|
+
If you like this project and would like to support our work, please consider contributing code via pull requests, or donating via [open collective](https://opencollective.com/perplexgg). Contributions are greatly appreciated!
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { InputType } from 'monomorph';
|
|
2
|
+
import { MonomorphClass } from 'monomorph';
|
|
3
|
+
import { MonomorphInstance } from 'monomorph';
|
|
4
|
+
import { PartialRecursive } from 'monomorph';
|
|
5
|
+
import { WithPool } from 'monomorph';
|
|
6
|
+
|
|
7
|
+
export declare type ComponentInput<X> = X extends MonomorphClass<infer P, infer I, infer M> ? M extends MonomorphInstance<infer iP, infer iI> ? InputType<M> : never : X extends VirtualComponent<infer T> ? T : X;
|
|
8
|
+
|
|
9
|
+
export declare type ComponentMap = Record<string, any>;
|
|
10
|
+
|
|
11
|
+
declare type ComponentMapClassProperties<CM> = {
|
|
12
|
+
[K in keyof CM as LowercaseFirstLetter<K>]?: CM[K] extends MonomorphClass<infer P, infer I, infer M> ? M : CM[K];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export declare function createEntityClass<C>(debug?: boolean): <CM extends ComponentMap, QM extends QueryMap<C, CM>, I extends EntityInput<CM> = EntityInput<CM>>(componentMap: CM, queries?: QM) => EntityClassWithStatics<C, CM, QM, I>;
|
|
16
|
+
|
|
17
|
+
export declare type CreateEntityFunction<C, CM extends ComponentMap, I extends EntityInput<CM>> = (data?: I, pool?: EntityPoolClass<C>) => EntityInstanceWithPool<CM, I, C>;
|
|
18
|
+
|
|
19
|
+
declare type ElementOfArray<T> = T extends (infer E)[] ? E : never;
|
|
20
|
+
|
|
21
|
+
declare interface EntityBaseProperties<CM extends ComponentMap, I extends EntityInput<CM>, QM extends QueryMap<any, CM>> {
|
|
22
|
+
componentMap: CM;
|
|
23
|
+
addComponent: <CCK extends LowercaseFirstLetter<keyof CM>, CC extends CM[OriginalComponentKey<CCK, CM>] = CM[OriginalComponentKey<CCK, CM>]>(key: CCK, data: ComponentInput<CC>) => void;
|
|
24
|
+
removeComponent: <CCK extends LowercaseFirstLetter<keyof CM>>(key: CCK) => void;
|
|
25
|
+
hasComponent: <CCK extends LowercaseFirstLetter<keyof CM>>(key: CCK) => boolean;
|
|
26
|
+
clone(): this;
|
|
27
|
+
copy(other: this): this;
|
|
28
|
+
set(data: PartialRecursive<I>): this;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
isDestroyed(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export declare type EntityClass<CM extends ComponentMap, I extends EntityInput<CM> = EntityInput<CM>, QM extends QueryMap<any, CM> = QueryMap<any, CM>, E extends EntityInstance<CM, I, QM> = EntityInstance<CM, I, QM>> = EntityConstructor<QM, E, I, CM>;
|
|
34
|
+
|
|
35
|
+
export declare type EntityClassWithStatics<C, CM extends ComponentMap, QM extends QueryMap<C, CM>, I extends EntityInput<CM> = EntityInput<CM>> = EntityClass<CM, I, QM> & {
|
|
36
|
+
create: CreateEntityFunction<C, CM, I>;
|
|
37
|
+
Pool: NoInfer<EntityPoolClass<C>>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export declare interface EntityConstructor<QM extends QueryMap<any, CM>, E extends EntityInstance<CM, I, QM>, I extends EntityInput<CM>, CM extends ComponentMap> {
|
|
41
|
+
new (data: I): E;
|
|
42
|
+
componentMap: CM;
|
|
43
|
+
pool: EntityPoolClass<E>;
|
|
44
|
+
componentsWithEntities: {
|
|
45
|
+
[K in keyof CM as CM[K] extends MonomorphClass<infer P, infer I, infer M> ? LowercaseFirstLetter<K> : never]: {
|
|
46
|
+
[Symbol.iterator](): IterableIterator<[WithPool<InstanceType<CM[K]>>, E]>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
components: {
|
|
50
|
+
[K in keyof CM as CM[K] extends MonomorphClass<infer P, infer I, infer M> ? LowercaseFirstLetter<K> : never]: {
|
|
51
|
+
[Symbol.iterator](): IterableIterator<WithPool<InstanceType<CM[K]>>>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
queries: {
|
|
55
|
+
[K in keyof QM]: {
|
|
56
|
+
afterEntityAdded: {
|
|
57
|
+
addListener(listener: (entity: E) => void): void;
|
|
58
|
+
removeListener(listener: (entity: E) => void): void;
|
|
59
|
+
};
|
|
60
|
+
beforeEntityRemoved: {
|
|
61
|
+
addListener(listener: (entity: E) => void): void;
|
|
62
|
+
removeListener(listener: (entity: E) => void): void;
|
|
63
|
+
};
|
|
64
|
+
[Symbol.iterator](): IterableIterator<WithComponent<E, ElementOfArray<QM[K]['with']> extends LowercaseFirstLetter<keyof E["componentMap"]> ? ElementOfArray<QM[K]['with']> : never>>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export declare type EntityInput<CM extends ComponentMap> = Partial<{
|
|
70
|
+
[K in keyof CM as LowercaseFirstLetter<K>]: ComponentInput<CM[K]>;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
export declare type EntityInstance<CM extends ComponentMap, I extends EntityInput<CM>, QM extends QueryMap<any, CM>> = EntityBaseProperties<CM, I, QM> & ComponentMapClassProperties<CM>;
|
|
74
|
+
|
|
75
|
+
export declare type EntityInstanceWithPool<CM extends ComponentMap, I extends EntityInput<CM>, E> = NoInfer<E & {
|
|
76
|
+
pool: EntityPoolClass<E> | null;
|
|
77
|
+
createInPool(data?: I): EntityInstanceWithPool<CM, I, E>;
|
|
78
|
+
}>;
|
|
79
|
+
|
|
80
|
+
export declare interface EntityPoolClass<M> {
|
|
81
|
+
/**
|
|
82
|
+
* create a new instance of this monomorph's pool
|
|
83
|
+
* @param maxLength the maximum length of this pool, if not specified, it will be Infinity
|
|
84
|
+
*/
|
|
85
|
+
new (maxLength?: number): EntityPoolClass<M>;
|
|
86
|
+
array: M[];
|
|
87
|
+
/**
|
|
88
|
+
* indices that are free to use, these are ones that have specifically been instantiated, initially this is empty
|
|
89
|
+
*/
|
|
90
|
+
freeIndices: number[];
|
|
91
|
+
/**
|
|
92
|
+
* a limit for how big this pool is allowed to get, an error
|
|
93
|
+
* is thrown when creating an instance when this limit is reached
|
|
94
|
+
*/
|
|
95
|
+
maxLength: number;
|
|
96
|
+
/** how many non-destroyed objects are in this pool, i.e. how many would be iterated on */
|
|
97
|
+
length: number;
|
|
98
|
+
[Symbol.iterator](): IterableIterator<WithPool<M>>;
|
|
99
|
+
create: (data?: M extends EntityInstance<infer CM, infer I, any> ? EntityInput<CM> : undefined) => M extends EntityInstanceWithPool<infer CM, infer I, any> ? EntityInstanceWithPool<CM, I, M> : never;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export declare function getEntityClassCode<C, CM extends ComponentMap>(componentMap: CM, queries?: QueryMap<C, CM>, debug?: boolean): string;
|
|
103
|
+
|
|
104
|
+
declare type LowercaseFirstLetter<S> = S extends `${infer FirstLetter}${infer Rest}` ? `${Lowercase<FirstLetter>}${Rest}` : S;
|
|
105
|
+
|
|
106
|
+
declare type OriginalComponentKey<CCK extends LowercaseFirstLetter<keyof CM>, CM extends ComponentMap> = CCK extends keyof CM ? CCK : UppercaseFirstLetter<CCK>;
|
|
107
|
+
|
|
108
|
+
export declare type QueryConfig<C, CM extends ComponentMap> = {
|
|
109
|
+
with?: LowercaseFirstLetter<keyof CM>[];
|
|
110
|
+
without?: LowercaseFirstLetter<keyof CM>[];
|
|
111
|
+
afterEntityAdded?: (entity: C) => void;
|
|
112
|
+
beforeEntityRemoved?: (entity: C) => void;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export declare type QueryMap<C, CM extends ComponentMap> = Record<string, QueryConfig<C, CM>>;
|
|
116
|
+
|
|
117
|
+
declare type UppercaseFirstLetter<S> = S extends `${infer FirstLetter}${infer Rest}` ? `${Uppercase<FirstLetter>}${Rest}` : S;
|
|
118
|
+
|
|
119
|
+
export declare type VirtualComponent<T> = T;
|
|
120
|
+
|
|
121
|
+
export declare type WithComponent<C extends {
|
|
122
|
+
componentMap: any;
|
|
123
|
+
}, KCM extends LowercaseFirstLetter<keyof C['componentMap']>> = Writeable<Omit<C, KCM> & {
|
|
124
|
+
[K in KCM as LowercaseFirstLetter<K>]-?: Exclude<C[K extends keyof C ? K : never], undefined | null>;
|
|
125
|
+
}>;
|
|
126
|
+
|
|
127
|
+
declare type Writeable<T> = {
|
|
128
|
+
-readonly [P in keyof T]: T[P];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export { }
|
package/build/mecs.js
ADDED
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
function lowercaseFirstLetter(str) {
|
|
2
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
3
|
+
}
|
|
4
|
+
function isMonomorphClass(x) {
|
|
5
|
+
return typeof x === "function" && "serializedSize" in x;
|
|
6
|
+
}
|
|
7
|
+
function getEntityClassCode(componentMap, queries, debug) {
|
|
8
|
+
let monomorphReferenceCode = "";
|
|
9
|
+
let poolCreationCode = "";
|
|
10
|
+
let poolMapCreationCode = "const componentPools = {\n";
|
|
11
|
+
let componentToEntityCode = "";
|
|
12
|
+
let componentIndicesCode = "";
|
|
13
|
+
let addComponentFunctionsCode = "";
|
|
14
|
+
let removeComponentFunctionsCode = "";
|
|
15
|
+
let componentFlagsCode = "";
|
|
16
|
+
let createFromDataCode = "";
|
|
17
|
+
let createFromDataCodeAllNulls = "";
|
|
18
|
+
let destroyCode = "";
|
|
19
|
+
let componentIteratorFunctionsCode = "";
|
|
20
|
+
const componentFlags = [];
|
|
21
|
+
const componentIndices = {};
|
|
22
|
+
const monomorphsAlreadyAdded = /* @__PURE__ */ new Set();
|
|
23
|
+
let componentIndex = 0;
|
|
24
|
+
let componentFlagValue = 1n;
|
|
25
|
+
for (const [key, component] of Object.entries(componentMap)) {
|
|
26
|
+
const componentKey = lowercaseFirstLetter(key);
|
|
27
|
+
const isMonomorph = isMonomorphClass(component);
|
|
28
|
+
componentIndices[componentKey] = componentIndex;
|
|
29
|
+
componentIndicesCode += `
|
|
30
|
+
${componentKey}: ${componentIndex},
|
|
31
|
+
`;
|
|
32
|
+
componentFlags.push(componentFlagValue);
|
|
33
|
+
componentFlagsCode += `${componentFlagValue}n, `;
|
|
34
|
+
createFromDataCode += `
|
|
35
|
+
if ('${componentKey}' in data) {
|
|
36
|
+
this.addComponent('${componentKey}', data.${componentKey}, false);
|
|
37
|
+
} else {
|
|
38
|
+
this.${componentKey} = null;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
createFromDataCodeAllNulls += `
|
|
42
|
+
this.${componentKey} = null;
|
|
43
|
+
`;
|
|
44
|
+
if (isMonomorph) {
|
|
45
|
+
if (!monomorphsAlreadyAdded.has(component.name)) {
|
|
46
|
+
monomorphReferenceCode += `
|
|
47
|
+
const ${component.name} = componentMap.${key};
|
|
48
|
+
`;
|
|
49
|
+
monomorphsAlreadyAdded.add(component.name);
|
|
50
|
+
}
|
|
51
|
+
poolCreationCode += `
|
|
52
|
+
const ${componentKey}Pool = new ${component.name}.Pool();
|
|
53
|
+
const ${componentKey}PoolArray = ${componentKey}Pool.array;
|
|
54
|
+
`;
|
|
55
|
+
poolMapCreationCode += `
|
|
56
|
+
${componentKey}: ${componentKey}Pool,
|
|
57
|
+
`;
|
|
58
|
+
addComponentFunctionsCode += `
|
|
59
|
+
(data, entity) => {
|
|
60
|
+
const instance = ${component.name}.create(data, ${componentKey}Pool);
|
|
61
|
+
${componentKey}ComponentToEntity[instance.index] = entity;
|
|
62
|
+
return instance;
|
|
63
|
+
},
|
|
64
|
+
`;
|
|
65
|
+
removeComponentFunctionsCode += `
|
|
66
|
+
instance => {
|
|
67
|
+
${componentKey}ComponentToEntity[instance.index] = null;
|
|
68
|
+
instance.destroy();
|
|
69
|
+
},
|
|
70
|
+
`;
|
|
71
|
+
destroyCode += `
|
|
72
|
+
if ((this.componentFlags & ${componentFlagValue}n) !== 0n) {
|
|
73
|
+
this.removeComponent('${componentKey}', updateQueryMemberships);
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
componentToEntityCode += `
|
|
77
|
+
const ${componentKey}ComponentToEntity = [null];
|
|
78
|
+
${componentKey}ComponentToEntity.pop();
|
|
79
|
+
`;
|
|
80
|
+
componentIteratorFunctionsCode += `
|
|
81
|
+
${componentKey}: {
|
|
82
|
+
[Symbol.iterator]() {
|
|
83
|
+
const resultArray = [null, null];
|
|
84
|
+
let index = 0;
|
|
85
|
+
let nonEmpty = 0;
|
|
86
|
+
const iteratorResult = {
|
|
87
|
+
done: false,
|
|
88
|
+
value: null,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
next() {
|
|
93
|
+
${""}
|
|
94
|
+
while (index < (${componentKey}Pool.length + ${componentKey}Pool.freeIndices.length) && (!${componentKey}PoolArray[index] || (${componentKey}PoolArray[index].version & 1) === 1)) {
|
|
95
|
+
index++;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (nonEmpty >= ${componentKey}Pool.length) {
|
|
99
|
+
iteratorResult.done = true;
|
|
100
|
+
iteratorResult.value = null;
|
|
101
|
+
return iteratorResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
resultArray[0] = ${componentKey}PoolArray[index];
|
|
105
|
+
resultArray[1] = ${componentKey}ComponentToEntity[index];
|
|
106
|
+
index++;
|
|
107
|
+
nonEmpty++;
|
|
108
|
+
iteratorResult.done = false;
|
|
109
|
+
iteratorResult.value = resultArray;
|
|
110
|
+
return iteratorResult;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
`;
|
|
116
|
+
} else {
|
|
117
|
+
addComponentFunctionsCode += `
|
|
118
|
+
null,
|
|
119
|
+
`;
|
|
120
|
+
removeComponentFunctionsCode += `
|
|
121
|
+
null,
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
componentFlagValue <<= 1n;
|
|
125
|
+
componentIndex++;
|
|
126
|
+
}
|
|
127
|
+
poolMapCreationCode += "\n};\n";
|
|
128
|
+
let queryWithFlag;
|
|
129
|
+
let queryWithoutFlag;
|
|
130
|
+
let queryIndex = 0;
|
|
131
|
+
let queryListenerCode = "";
|
|
132
|
+
let updateQueryMembershipsCode = "";
|
|
133
|
+
let createQueryEntityArraysCode = "";
|
|
134
|
+
let queriesObjectCode = "";
|
|
135
|
+
let queryCallbackReferencesCode = "";
|
|
136
|
+
for (const [queryName, queryConfig] of Object.entries(queries ?? {})) {
|
|
137
|
+
queryWithFlag = 0n;
|
|
138
|
+
for (const withComponent of queryConfig.with ?? []) {
|
|
139
|
+
if (withComponent in componentIndices) {
|
|
140
|
+
queryWithFlag |= componentFlags[componentIndices[withComponent]];
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(`Query "${queryName}" references non-existent component "${String(withComponent)}" in "with" array`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
queryWithoutFlag = 0n;
|
|
146
|
+
for (const withoutComponent of queryConfig.without ?? []) {
|
|
147
|
+
if (withoutComponent in componentIndices) {
|
|
148
|
+
queryWithoutFlag |= componentFlags[componentIndices[withoutComponent]];
|
|
149
|
+
} else {
|
|
150
|
+
throw new Error(`Query "${queryName}" references non-existent component "${String(withoutComponent)}" in "without" array`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
queryListenerCode += `
|
|
154
|
+
const queryEntityAddedListeners${queryIndex} = [
|
|
155
|
+
${queryConfig.afterEntityAdded ? "queryMap." + queryName + ".afterEntityAdded" : ""}
|
|
156
|
+
];
|
|
157
|
+
const queryEntityRemovedListeners${queryIndex} = [
|
|
158
|
+
${queryConfig.beforeEntityRemoved ? "queryMap." + queryName + ".beforeEntityRemoved" : ""}
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
function afterEntityAdded${queryIndex} (entity) {
|
|
162
|
+
for (const listener of queryEntityAddedListeners${queryIndex}) {
|
|
163
|
+
listener(entity);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function beforeEntityRemoved${queryIndex} (entity) {
|
|
168
|
+
for (const listener of queryEntityRemovedListeners${queryIndex}) {
|
|
169
|
+
listener(entity);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
updateQueryMembershipsCode += `
|
|
174
|
+
if (
|
|
175
|
+
this.queryIndices[${queryIndex}] === -1
|
|
176
|
+
&& (this.componentFlags & ${queryWithFlag}n) === ${queryWithFlag}n
|
|
177
|
+
&& (this.componentFlags & ~${queryWithoutFlag}n) === this.componentFlags
|
|
178
|
+
) {
|
|
179
|
+
${!debug ? "" : `
|
|
180
|
+
console.log('adding entity to query "${queryName}"', this);
|
|
181
|
+
`}
|
|
182
|
+
this.componentFlags |= ${queryWithFlag}n;
|
|
183
|
+
if (queryEntityFreeIndices${queryIndex}.length > 0) {
|
|
184
|
+
const index = queryEntityFreeIndices${queryIndex}.pop();
|
|
185
|
+
this.queryIndices[${queryIndex}] = index;
|
|
186
|
+
queryEntities${queryIndex}[index] = this;
|
|
187
|
+
} else {
|
|
188
|
+
this.queryIndices[${queryIndex}] = queryEntities${queryIndex}.length;
|
|
189
|
+
queryEntities${queryIndex}.push(this);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
afterEntityAdded${queryIndex}(this);
|
|
193
|
+
|
|
194
|
+
${!debug ? "" : `
|
|
195
|
+
console.log('added entity to query "${queryName}"', this);
|
|
196
|
+
`}
|
|
197
|
+
} else if (
|
|
198
|
+
this.queryIndices[${queryIndex}] !== -1
|
|
199
|
+
&& (
|
|
200
|
+
(this.componentFlags & ${queryWithFlag}n) !== ${queryWithFlag}n
|
|
201
|
+
|| (this.componentFlags & ~${queryWithoutFlag}n) !== this.componentFlags
|
|
202
|
+
)
|
|
203
|
+
) {
|
|
204
|
+
|
|
205
|
+
if (queryEntityRemovedListeners${queryIndex}.length > 0) {
|
|
206
|
+
if (componentFlagThatWasRemoved !== undefined) {
|
|
207
|
+
// before calling the listeners, toggle the components that were removed, if any
|
|
208
|
+
// this is so that in the listener, the entity's hasComponent() calls correctly
|
|
209
|
+
// show that the component is still there, since the removal hasn't happened yet
|
|
210
|
+
this.componentFlags &= ~componentFlagThatWasRemoved;
|
|
211
|
+
beforeEntityRemoved${queryIndex}(this);
|
|
212
|
+
this.componentFlags &= ~componentFlagThatWasRemoved;
|
|
213
|
+
} else {
|
|
214
|
+
beforeEntityRemoved${queryIndex}(this);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const index = this.queryIndices[${queryIndex}];
|
|
219
|
+
this.queryIndices[${queryIndex}] = -1;
|
|
220
|
+
queryEntities${queryIndex}[index] = null;
|
|
221
|
+
queryEntityFreeIndices${queryIndex}.push(index);
|
|
222
|
+
|
|
223
|
+
${!debug ? "" : `
|
|
224
|
+
console.log('removed entity from query "${queryName}"', this);
|
|
225
|
+
`}
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
createQueryEntityArraysCode += `
|
|
229
|
+
const queryEntities${queryIndex} = [null];
|
|
230
|
+
queryEntities${queryIndex}.pop();
|
|
231
|
+
const queryEntityFreeIndices${queryIndex} = [0];
|
|
232
|
+
queryEntityFreeIndices${queryIndex}.pop();
|
|
233
|
+
`;
|
|
234
|
+
queriesObjectCode += `
|
|
235
|
+
${queryName}: {
|
|
236
|
+
array: queryEntities${queryIndex},
|
|
237
|
+
afterEntityAdded: {
|
|
238
|
+
fn: afterEntityAdded${queryIndex},
|
|
239
|
+
addListener(listener) {
|
|
240
|
+
queryEntityAddedListeners${queryIndex}.push(listener);
|
|
241
|
+
},
|
|
242
|
+
removeListener(listener) {
|
|
243
|
+
const index = queryEntityAddedListeners${queryIndex}.indexOf(listener);
|
|
244
|
+
if (index !== -1) {
|
|
245
|
+
queryEntityAddedListeners${queryIndex}.splice(index, 1);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
listeners: queryEntityAddedListeners${queryIndex},
|
|
249
|
+
},
|
|
250
|
+
beforeEntityRemoved: {
|
|
251
|
+
fn: beforeEntityRemoved${queryIndex},
|
|
252
|
+
addListener(listener) {
|
|
253
|
+
queryEntityRemovedListeners${queryIndex}.push(listener);
|
|
254
|
+
},
|
|
255
|
+
removeListener(listener) {
|
|
256
|
+
const index = queryEntityRemovedListeners${queryIndex}.indexOf(listener);
|
|
257
|
+
if (index !== -1) {
|
|
258
|
+
queryEntityRemovedListeners${queryIndex}.splice(index, 1);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
listeners: queryEntityRemovedListeners${queryIndex},
|
|
262
|
+
},
|
|
263
|
+
[Symbol.iterator]() {
|
|
264
|
+
let index = 0;
|
|
265
|
+
let entity = null;
|
|
266
|
+
const iteratorResult = {
|
|
267
|
+
done: false,
|
|
268
|
+
value: null,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
next() {
|
|
273
|
+
// we need to skip nulls, nulls are used to maintain the order of the array during the frame, otherwise if we
|
|
274
|
+
// use swap/replace we may miss an entity that got swapped to an index before the one we're iterating over
|
|
275
|
+
|
|
276
|
+
while (index < queryEntities${queryIndex}.length) {
|
|
277
|
+
entity = queryEntities${queryIndex}[index];
|
|
278
|
+
index++;
|
|
279
|
+
if (entity !== null) {
|
|
280
|
+
iteratorResult.done = false;
|
|
281
|
+
iteratorResult.value = entity;
|
|
282
|
+
return iteratorResult;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
iteratorResult.done = true;
|
|
287
|
+
iteratorResult.value = null;
|
|
288
|
+
return iteratorResult;
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
`;
|
|
294
|
+
queryIndex++;
|
|
295
|
+
}
|
|
296
|
+
const code = `
|
|
297
|
+
${monomorphReferenceCode}
|
|
298
|
+
${poolCreationCode}
|
|
299
|
+
${poolMapCreationCode}
|
|
300
|
+
${componentToEntityCode}
|
|
301
|
+
const componentIndices = {
|
|
302
|
+
${componentIndicesCode}
|
|
303
|
+
};
|
|
304
|
+
const addComponentFunctions = [
|
|
305
|
+
${addComponentFunctionsCode}
|
|
306
|
+
];
|
|
307
|
+
const removeComponentFunctions = [
|
|
308
|
+
${removeComponentFunctionsCode}
|
|
309
|
+
];
|
|
310
|
+
const componentFlags = [ ${componentFlagsCode} ];
|
|
311
|
+
|
|
312
|
+
${createQueryEntityArraysCode}
|
|
313
|
+
|
|
314
|
+
${queryListenerCode}
|
|
315
|
+
|
|
316
|
+
const queries = {
|
|
317
|
+
${queriesObjectCode}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const componentToEntity = new Array(${componentIndex});
|
|
321
|
+
for (let i = 0; i < ${componentIndex}; i++) {
|
|
322
|
+
componentToEntity[i] = [null];
|
|
323
|
+
componentToEntity[i].pop();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
${queryCallbackReferencesCode}
|
|
327
|
+
|
|
328
|
+
function iterateOverPool(pool) {
|
|
329
|
+
let index = 0;
|
|
330
|
+
let nonEmpty = 0;
|
|
331
|
+
const array = pool.array;
|
|
332
|
+
const maxLength = pool.maxLength;
|
|
333
|
+
const iteratorResult = {
|
|
334
|
+
done: false,
|
|
335
|
+
value: null,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
next() {
|
|
340
|
+
${""}
|
|
341
|
+
while (index < (pool.length + pool.freeIndices.length) && (!array[index] || (array[index].version & 1) === 1)) {
|
|
342
|
+
index++;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
${""}
|
|
346
|
+
if (index >= maxLength || nonEmpty >= pool.length) {
|
|
347
|
+
iteratorResult.done = true;
|
|
348
|
+
iteratorResult.value = null;
|
|
349
|
+
return iteratorResult;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const value = array[index];
|
|
353
|
+
index++;
|
|
354
|
+
nonEmpty++;
|
|
355
|
+
iteratorResult.done = false;
|
|
356
|
+
iteratorResult.value = value;
|
|
357
|
+
return iteratorResult;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function iterateOverReferenceList(referenceList, fixDestroyed = true) {
|
|
363
|
+
let index = 0;
|
|
364
|
+
const poolArray = referenceList.pool.array;
|
|
365
|
+
const arrayOfIndices = referenceList.arrayOfIndices;
|
|
366
|
+
const arrayOfVersions = referenceList.arrayOfVersions;
|
|
367
|
+
const array = referenceList.array;
|
|
368
|
+
const maxLength = referenceList.maxLength;
|
|
369
|
+
const iteratorResult = {
|
|
370
|
+
done: false,
|
|
371
|
+
value: null,
|
|
372
|
+
};
|
|
373
|
+
let poolItem;
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
next() {
|
|
377
|
+
if (!arrayOfIndices.length || index >= arrayOfIndices.length) {
|
|
378
|
+
iteratorResult.done = true;
|
|
379
|
+
iteratorResult.value = null;
|
|
380
|
+
return iteratorResult;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
poolItem = poolArray[arrayOfIndices[index]];
|
|
384
|
+
|
|
385
|
+
${""}
|
|
386
|
+
while (index < arrayOfIndices.length && (((poolItem.version & 1) === 1) || poolItem.version !== arrayOfVersions[index])) {
|
|
387
|
+
if (fixDestroyed) {
|
|
388
|
+
if (index < arrayOfIndices.length - 1) {
|
|
389
|
+
arrayOfIndices[index] = arrayOfIndices[arrayOfIndices.length - 1];
|
|
390
|
+
arrayOfVersions[index] = arrayOfVersions[arrayOfVersions.length - 1];
|
|
391
|
+
poolItem = poolArray[arrayOfIndices[index]];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
referenceList.length -= 1;
|
|
395
|
+
} else {
|
|
396
|
+
index++;
|
|
397
|
+
if (index < arrayOfIndices.length) {
|
|
398
|
+
poolItem = poolArray[arrayOfIndices[index]];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (index >= arrayOfIndices.length) {
|
|
404
|
+
iteratorResult.done = true;
|
|
405
|
+
iteratorResult.value = null;
|
|
406
|
+
return iteratorResult;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
index++;
|
|
410
|
+
iteratorResult.done = false;
|
|
411
|
+
iteratorResult.value = poolItem;
|
|
412
|
+
return iteratorResult;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const theClass = class {
|
|
418
|
+
constructor(data, index = 0, updateQueryMemberships = true) {
|
|
419
|
+
this.index = index;
|
|
420
|
+
this.version = 0; ${""}
|
|
421
|
+
this.pool = this.constructor.pool;
|
|
422
|
+
this.queryIndices = new Array(${queryIndex}).fill(-1);
|
|
423
|
+
this.componentFlags = 0n; ${""}
|
|
424
|
+
if (data) {
|
|
425
|
+
${createFromDataCode}
|
|
426
|
+
} else {
|
|
427
|
+
${createFromDataCodeAllNulls}
|
|
428
|
+
}
|
|
429
|
+
if (updateQueryMemberships) {
|
|
430
|
+
this.updateQueryMemberships();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fullSet(data, updateQueryMemberships = true) {
|
|
435
|
+
this.queryIndices.fill(-1);
|
|
436
|
+
this.componentFlags = 0n;
|
|
437
|
+
if (data) {
|
|
438
|
+
${createFromDataCode}
|
|
439
|
+
} else {
|
|
440
|
+
${createFromDataCodeAllNulls}
|
|
441
|
+
}
|
|
442
|
+
if (updateQueryMemberships) {
|
|
443
|
+
this.updateQueryMemberships();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
addComponent(key, data, updateQueryMemberships = true) {
|
|
448
|
+
const componentIndex = componentIndices[key];
|
|
449
|
+
const fn = addComponentFunctions[componentIndex];
|
|
450
|
+
this[key] = fn ? fn(data, this) : data;
|
|
451
|
+
|
|
452
|
+
this.componentFlags |= componentFlags[componentIndex];
|
|
453
|
+
|
|
454
|
+
if (updateQueryMemberships) {
|
|
455
|
+
this.updateQueryMemberships();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
removeComponent(key, updateQueryMemberships = true) {
|
|
460
|
+
const componentIndex = componentIndices[key];
|
|
461
|
+
const componentFlag = componentFlags[componentIndex];
|
|
462
|
+
${!debug ? "" : `
|
|
463
|
+
if ((this.componentFlags & componentFlags[componentIndex]) === 0n) {
|
|
464
|
+
console.error('this entity:', this);
|
|
465
|
+
throw new Error('DEV-only check: Tried to remove component "'+key+'" that this entity does not have');
|
|
466
|
+
}
|
|
467
|
+
`}
|
|
468
|
+
this.componentFlags &= ~componentFlag;
|
|
469
|
+
|
|
470
|
+
if (updateQueryMemberships) {
|
|
471
|
+
this.updateQueryMemberships(componentFlag);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const fn = removeComponentFunctions[componentIndex];
|
|
475
|
+
if (fn) {
|
|
476
|
+
fn(this[key], this, key);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this[key] = null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
destroy(updateQueryMemberships = true) {
|
|
483
|
+
${destroyCode}
|
|
484
|
+
if (this.version & 1 === 1) {
|
|
485
|
+
${""}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.version++; // becomes odd, meaning deleted
|
|
489
|
+
if (this.pool) {
|
|
490
|
+
this.pool.length--;
|
|
491
|
+
this.pool.freeIndices.push(this.index);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
hasComponent(key) {
|
|
496
|
+
return (this.componentFlags & componentFlags[componentIndices[key]]) !== 0n;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
updateQueryMemberships(componentFlagThatWasRemoved) {
|
|
500
|
+
${updateQueryMembershipsCode}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
static get ReferenceList() {
|
|
504
|
+
if (!this.DynamicReferenceListClass) {
|
|
505
|
+
const currentClass = this;
|
|
506
|
+
this.DynamicReferenceListClass = class {
|
|
507
|
+
constructor(pool, maxLength = Infinity, data = undefined, classConstructor = currentClass) {
|
|
508
|
+
this.pool = pool;
|
|
509
|
+
this.maxLength = maxLength;
|
|
510
|
+
this.arrayOfIndices = [];
|
|
511
|
+
this.arrayOfVersions = [];
|
|
512
|
+
this.classConstructor = classConstructor;
|
|
513
|
+
|
|
514
|
+
if (data?.length) {
|
|
515
|
+
for (const item of data) {
|
|
516
|
+
this.create(item);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
get length() {
|
|
522
|
+
return this.arrayOfIndices.length;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
set length(v) {
|
|
526
|
+
this.arrayOfIndices.length = v;
|
|
527
|
+
this.arrayOfVersions.length = v;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
push(instance) {
|
|
531
|
+
if (this.length >= this.maxLength) {
|
|
532
|
+
throw new Error("Could not add instance to reference list, reference list maxLength of "+this.maxLength+" has been reached");
|
|
533
|
+
}
|
|
534
|
+
if (instance.pool !== this.pool) {
|
|
535
|
+
throw new Error("Could not add instance to reference list, instance does not belong to the same pool as the reference list");
|
|
536
|
+
}
|
|
537
|
+
const length = this.length;
|
|
538
|
+
this.arrayOfIndices[length] = instance.index;
|
|
539
|
+
this.arrayOfVersions[length] = instance.version;
|
|
540
|
+
return length;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
getAtIndex(index, fixDestroyed = true) {
|
|
544
|
+
if (index < 0 || index >= this.arrayOfIndices.length) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
let poolItem = this.pool.array[this.arrayOfIndices[index]];
|
|
548
|
+
|
|
549
|
+
if (fixDestroyed) {
|
|
550
|
+
while (index < this.arrayOfIndices.length && ((poolItem.version & 1) === 1 || poolItem.version !== this.arrayOfVersions[index])) {
|
|
551
|
+
if (index < this.arrayOfIndices.length - 1) {
|
|
552
|
+
this.arrayOfIndices[index] = this.arrayOfIndices[this.arrayOfIndices.length - 1];
|
|
553
|
+
this.arrayOfVersions[index] = this.arrayOfVersions[this.arrayOfVersions.length - 1];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
this.length -= 1;
|
|
557
|
+
|
|
558
|
+
if (index < this.arrayOfIndices.length) {
|
|
559
|
+
poolItem = this.pool.array[this.arrayOfIndices[index]];
|
|
560
|
+
} else {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} else if ((poolItem.version & 1) === 1 || poolItem.version !== this.arrayOfVersions[index]) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return poolItem;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
remove(instance) {
|
|
572
|
+
for (let i = 0; i < this.arrayOfIndices.length; i++) {
|
|
573
|
+
if (this.arrayOfIndices[i] === instance.index && this.arrayOfVersions[i] === instance.version) {
|
|
574
|
+
if (i < this.arrayOfIndices.length - 1) {
|
|
575
|
+
this.arrayOfIndices[i] = this.arrayOfIndices[this.arrayOfIndices.length - 1];
|
|
576
|
+
this.arrayOfVersions[i] = this.arrayOfVersions[this.arrayOfVersions.length - 1];
|
|
577
|
+
}
|
|
578
|
+
this.length -= 1;
|
|
579
|
+
return i;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return -1;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
removeIndex(index) {
|
|
586
|
+
if (index < 0 || index >= this.arrayOfIndices.length) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
if (index < this.arrayOfIndices.length - 1) {
|
|
590
|
+
this.arrayOfIndices[index] = this.arrayOfIndices[this.arrayOfIndices.length - 1];
|
|
591
|
+
this.arrayOfVersions[index] = this.arrayOfVersions[this.arrayOfVersions.length - 1];
|
|
592
|
+
}
|
|
593
|
+
this.length -= 1;
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
clear() {
|
|
598
|
+
this.length = 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
create(data) {
|
|
602
|
+
const instance = this.classConstructor.create(data, this.pool);
|
|
603
|
+
const length = this.length;
|
|
604
|
+
this.arrayOfIndices[length] = instance.index;
|
|
605
|
+
this.arrayOfVersions[length] = instance.version;
|
|
606
|
+
return instance;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
[Symbol.iterator]() {
|
|
610
|
+
return iterateOverReferenceList(this, true);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
get iterateIgnoreEmpty() {
|
|
614
|
+
const self = this;
|
|
615
|
+
return {
|
|
616
|
+
[Symbol.iterator]() {
|
|
617
|
+
return iterateOverReferenceList(self, false);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return this.DynamicReferenceListClass;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
static get pool() {
|
|
628
|
+
if (!this.dynamicPoolInstance) {
|
|
629
|
+
this.dynamicPoolInstance = new this.Pool();
|
|
630
|
+
}
|
|
631
|
+
return this.dynamicPoolInstance;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
static get Pool() {
|
|
635
|
+
if (!this.DynamicPoolClass) {
|
|
636
|
+
const currentClass = this;
|
|
637
|
+
this.DynamicPoolClass = class {
|
|
638
|
+
constructor(length = Infinity, classConstructor = currentClass) {
|
|
639
|
+
this.length = 0;
|
|
640
|
+
this.maxLength = length;
|
|
641
|
+
this.array = new Array(isFinite(length) ? length : undefined);
|
|
642
|
+
this.freeIndices = [1];
|
|
643
|
+
this.freeIndices.pop();
|
|
644
|
+
this.classConstructor = classConstructor;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
[Symbol.iterator]() {
|
|
648
|
+
return iterateOverPool(this);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
create(data, updateQueryMemberships = true) {
|
|
652
|
+
return this.classConstructor.create(data, this, updateQueryMemberships);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
toArray(array, startOffset = 0, skipPoolReferences = false) {
|
|
656
|
+
let offset = startOffset;
|
|
657
|
+
|
|
658
|
+
if (this.maxLength > 0 && isFinite(this.length)) {
|
|
659
|
+
array[offset++] = isFinite(this.maxLength) ? this.maxLength : -1;
|
|
660
|
+
array[offset++] = this.length + this.freeIndices.length;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
array[offset++] = this.freeIndices.length;
|
|
664
|
+
for (let i = 0; i < this.freeIndices.length; i++) {
|
|
665
|
+
array[offset++] = this.freeIndices[i];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
${""}
|
|
669
|
+
|
|
670
|
+
for (let i = 0; i < this.length + this.freeIndices.length; i++) {
|
|
671
|
+
const instance = this.array[i];
|
|
672
|
+
if (instance === undefined) {
|
|
673
|
+
array[offset++] = -1;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
array[offset++] = instance.version;
|
|
677
|
+
if (instance.version & 1) {${""}
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
offset = instance.toArray(array, offset, skipPoolReferences);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return offset;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
fromArray(arrayOfData, references, startOffset = 0, classConstructor = this.classConstructor) {
|
|
687
|
+
let offset = startOffset;
|
|
688
|
+
|
|
689
|
+
this.maxLength = arrayOfData[offset++];
|
|
690
|
+
if (this.maxLength === -1) {
|
|
691
|
+
this.maxLength = Infinity;
|
|
692
|
+
}
|
|
693
|
+
const usedLength = arrayOfData[offset++];
|
|
694
|
+
this.length = 0;
|
|
695
|
+
|
|
696
|
+
const freeIndicesLength = arrayOfData[offset++];
|
|
697
|
+
this.freeIndices.length = freeIndicesLength;
|
|
698
|
+
for (let i = 0; i < freeIndicesLength; i++) {
|
|
699
|
+
this.freeIndices[i] = arrayOfData[offset++];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
for (let i = 0; i < usedLength; i++) {
|
|
704
|
+
const version = arrayOfData[offset++];
|
|
705
|
+
|
|
706
|
+
if (version & 1) {
|
|
707
|
+
if (this.array[i]) {
|
|
708
|
+
this.array[i].version = version;
|
|
709
|
+
}
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
this.length++;
|
|
714
|
+
|
|
715
|
+
if (this.array[i] === undefined) {
|
|
716
|
+
this.array[i] = new classConstructor(undefined, i, this);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
this.array[i].version = version;
|
|
720
|
+
|
|
721
|
+
offset = this.array[i].fromArray(arrayOfData, references, offset);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return offset;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
fromArrayNoReferences(arrayOfData, startOffset = 0, classConstructor = this.classConstructor) {
|
|
728
|
+
let offset = startOffset;
|
|
729
|
+
|
|
730
|
+
this.maxLength = arrayOfData[offset++];
|
|
731
|
+
if (this.maxLength === -1) {
|
|
732
|
+
this.maxLength = Infinity;
|
|
733
|
+
}
|
|
734
|
+
const usedLength = arrayOfData[offset++];
|
|
735
|
+
this.length = 0;
|
|
736
|
+
|
|
737
|
+
const freeIndicesLength = arrayOfData[offset++];
|
|
738
|
+
this.freeIndices.length = freeIndicesLength;
|
|
739
|
+
for (let i = 0; i < freeIndicesLength; i++) {
|
|
740
|
+
this.freeIndices[i] = arrayOfData[offset++];
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
for (let i = 0; i < usedLength; i++) {
|
|
744
|
+
const version = arrayOfData[offset++];
|
|
745
|
+
|
|
746
|
+
if (version & 1) {
|
|
747
|
+
if (this.array[i]) {
|
|
748
|
+
this.array[i].version = version;
|
|
749
|
+
}
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
this.length++;
|
|
754
|
+
|
|
755
|
+
if (this.array[i] === undefined) {
|
|
756
|
+
this.array[i] = new classConstructor(undefined, i, this);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
this.array[i].version = version;
|
|
760
|
+
|
|
761
|
+
offset = this.array[i].fromArrayNoReferences(arrayOfData, offset);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return offset;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
fromArrayOnlyReferences(arrayOfData, references, startOffset = 0, classConstructor = this.classConstructor) {
|
|
768
|
+
let offset = startOffset;
|
|
769
|
+
|
|
770
|
+
this.maxLength = arrayOfData[offset++];
|
|
771
|
+
if (this.maxLength === -1) {
|
|
772
|
+
this.maxLength = Infinity;
|
|
773
|
+
}
|
|
774
|
+
const usedLength = arrayOfData[offset++];
|
|
775
|
+
this.length = 0;
|
|
776
|
+
|
|
777
|
+
const freeIndicesLength = arrayOfData[offset++];
|
|
778
|
+
this.freeIndices.length = freeIndicesLength;
|
|
779
|
+
for (let i = 0; i < freeIndicesLength; i++) {
|
|
780
|
+
this.freeIndices[i] = arrayOfData[offset++];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (let i = 0; i < usedLength; i++) {
|
|
784
|
+
const version = arrayOfData[offset++];
|
|
785
|
+
|
|
786
|
+
if (version & 1) {
|
|
787
|
+
if (this.array[i]) {
|
|
788
|
+
this.array[i].version = version;
|
|
789
|
+
}
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.length++;
|
|
794
|
+
|
|
795
|
+
if (this.array[i] === undefined) {
|
|
796
|
+
this.array[i] = new classConstructor(undefined, i, this);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
this.array[i].version = version;
|
|
800
|
+
|
|
801
|
+
offset = this.array[i].fromArrayOnlyReferences(arrayOfData, references, offset);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return offset;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return this.DynamicPoolClass;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
theClass.queries = queries;
|
|
815
|
+
theClass.componentToEntity = componentToEntity;
|
|
816
|
+
theClass.components = componentPools;
|
|
817
|
+
theClass.componentsWithEntities = {
|
|
818
|
+
${componentIteratorFunctionsCode}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
theClass.create = function(data, updateQueryMemberships = true) {
|
|
822
|
+
const pool = this.pool;
|
|
823
|
+
if (pool.freeIndices.length) {
|
|
824
|
+
const index = pool.freeIndices.pop();
|
|
825
|
+
pool.length++;
|
|
826
|
+
const instance = pool.array[index];
|
|
827
|
+
instance.version++; ${""}
|
|
828
|
+
instance.fullSet(data, updateQueryMemberships);
|
|
829
|
+
return instance;
|
|
830
|
+
} else {
|
|
831
|
+
const instance = new this(data, pool.length, updateQueryMemberships);
|
|
832
|
+
pool.array[pool.length] = instance;
|
|
833
|
+
pool.length++;
|
|
834
|
+
return instance;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return theClass;
|
|
839
|
+
`;
|
|
840
|
+
if (debug) {
|
|
841
|
+
console.log("code", code);
|
|
842
|
+
}
|
|
843
|
+
return code;
|
|
844
|
+
}
|
|
845
|
+
function createEntityClass(debug) {
|
|
846
|
+
return (componentMap, queries) => new Function("componentMap", "queryMap", getEntityClassCode(componentMap, queries, debug))(componentMap, queries);
|
|
847
|
+
}
|
|
848
|
+
export {
|
|
849
|
+
createEntityClass,
|
|
850
|
+
getEntityClassCode
|
|
851
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@perplexdotgg/mecs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MECS - Monomorph ECS - A high-performance Entity Component System for TypeScript and JavaScript, designed for games and simulations.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://codeberg.org/perplexdotgg/mecs"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "perplex.gg",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "build/mecs.js",
|
|
13
|
+
"types": "build/index.d.ts",
|
|
14
|
+
"directories": {
|
|
15
|
+
"test": "tests"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"build",
|
|
19
|
+
"package.json",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && vite build",
|
|
25
|
+
"watch": "tsc --watch",
|
|
26
|
+
"test": "vitest",
|
|
27
|
+
"bench": "vitest bench"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"monomorph": "^1.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.0.1",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"tslib": "^2.8.1",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vite": "^7.2.7",
|
|
38
|
+
"vite-plugin-conditional-compiler": "^0.4.0",
|
|
39
|
+
"vite-plugin-dts": "^4.5.4",
|
|
40
|
+
"vitest": "^4.0.15"
|
|
41
|
+
}
|
|
42
|
+
}
|