├── .eslintignore
├── .eslintrc.js
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── CHANGELOG.MD
├── LICENSE
├── README.md
├── build
├── core
│ ├── aspect.d.ts
│ ├── aspect.js
│ ├── collection.d.ts
│ ├── collection.js
│ ├── component.d.ts
│ ├── component.js
│ ├── dispatcher.d.ts
│ ├── dispatcher.js
│ ├── engine.d.ts
│ ├── engine.js
│ ├── entity.collection.d.ts
│ ├── entity.collection.js
│ ├── entity.d.ts
│ ├── entity.js
│ ├── system.d.ts
│ ├── system.js
│ ├── types.d.ts
│ └── types.js
├── index.d.ts
└── index.js
├── examples
└── rectangles
│ ├── .gitignore
│ ├── README.md
│ ├── build
│ └── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── position.ts
│ │ ├── size.ts
│ │ └── velocity.ts
│ ├── entity.ts
│ ├── index.ts
│ └── systems
│ │ ├── collision.ts
│ │ ├── gravity.ts
│ │ ├── movement.ts
│ │ ├── renderer.ts
│ │ └── resize.ts
│ └── tsconfig.json
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── core
│ ├── aspect.spec.ts
│ ├── aspect.ts
│ ├── collection.spec.ts
│ ├── collection.ts
│ ├── component.spec.ts
│ ├── component.ts
│ ├── dispatcher.spec.ts
│ ├── dispatcher.ts
│ ├── engine.spec.ts
│ ├── engine.ts
│ ├── entity.collection.spec.ts
│ ├── entity.collection.ts
│ ├── entity.spec.ts
│ ├── entity.ts
│ ├── system.spec.ts
│ ├── system.ts
│ └── types.ts
└── index.ts
├── tsconfig.json
└── tsconfig.test.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | build
2 | coverage
3 | node_modules
4 | webpack.config.js
5 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | plugins: ['@typescript-eslint'],
5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
6 | env: {
7 | browser: true,
8 | amd: true,
9 | node: true,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Trixt0r
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: trixt0r
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom:
13 | [
14 | 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WDV9MU2GU35NN',
15 | 'https://www.buymeacoffee.com/Trixt0r',
16 | ]
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | !./build
2 |
3 | .github
4 | .vscode
5 | examples
6 | coverage
7 | src
8 | .eslintignore
9 | .eslintrc.js
10 | .prettierignore
11 | .prettierrc
12 | CHANGELOG.MD
13 | jest.config.js
14 | tsconfig.*
15 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
4 | node_modules
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "bracketSpacing": true,
7 | "printWidth": 120,
8 | "endOfLine": "lf",
9 | "useTabs": false,
10 | "arrowParens": "avoid",
11 | "bracketSameLine": true
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "streetsidesoftware.code-spell-checker",
4 | "dbaeumer.vscode-eslint",
5 | "esbenp.prettier-vscode",
6 | "orta.vscode-jest",
7 | "eg2.vscode-npm-script"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "name": "vscode-jest-tests",
10 | "request": "launch",
11 | "args": ["--runInBand"],
12 | "cwd": "${workspaceFolder}",
13 | "console": "integratedTerminal",
14 | "internalConsoleOptions": "neverOpen",
15 | "disableOptimisticBPs": true,
16 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "jest.coverageFormatter": "GutterFormatter",
3 | "jest.showCoverageOnLoad": true
4 | }
5 |
--------------------------------------------------------------------------------
/CHANGELOG.MD:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### 0.5.0
4 |
5 | Added `EntityCollection` which enables you to access entities directly by their `id`.
6 |
7 | ```ts
8 | engine.entities.get(1);
9 | ```
10 |
11 | ### 0.4.6
12 |
13 | Type definition fix for `Class` and `ComponentClass`.
14 |
15 | ### 0.4.4
16 |
17 | 100% code coverage & code cleanup.
18 |
19 | ### 0.4.3
20 |
21 | Fixed component caching bug, not returning all components for a string type per component instance.
22 |
23 | ### 0.4.0
24 |
25 | Fixed aspect bugs.
26 |
27 | ### 0.3.0
28 |
29 | Components can now also have an optional `id`.
30 | Aspects support the id value for filtering certain entities.
31 |
32 | ### 0.2.2
33 |
34 | Target ES5.
35 |
36 | ### 0.2.1
37 |
38 | Fixed vulnerabilities in package.json.
39 |
40 | ## 0.2.0
41 |
42 | This version adds improvements to the `Aspect` and `AbstractEntitySystem` classes.
43 |
44 | ### Improvements
45 |
46 | - `Aspect` is now a dispatcher, which dispatches events like `onAddedEntities`, `onAddedComponents`, etc.
47 | - `AbstractEntitySystem` listens for the new dispatched events and adds stub methods
48 | for systems extending the abstraction. This way you can avoid boilerplate code.
49 | - Various minor fixes.
50 |
51 | ## 0.1.0
52 |
53 | This version includes breaking changes for filtering entities by components.
54 |
55 | ### Improvements
56 |
57 | - Upgrade to Typescript 3.6.3
58 | - Renamed `Filter` to `Aspect`
59 | - The API changed.
60 | - Instead of using `Filter.getFor(...)` use now `Aspect.for(engine|collection).one(...)` to get the old behavior.
61 | - `Aspect` provides 3 more methods for better filtering: `all`, `exclude` and `one`.
62 | - `Engine` has no `getFilter` method anymore. Use `Aspect.for(engine)` instead.
63 | - Options for `Engine.run` is now an optional argument.
64 | - New `AbstractEntitySystem` which can be used for systems which process entities.
65 | - Use this class to keep boilerplate code low.
66 | - Those systems can pass component type arguments to the super constructor, to auto filter entities within an engine.
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Heinrich Reich
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ECS-TS
2 |
3 | A simple to use entity component system library written in TypeScript.
4 |
5 | It is meant to be used for any use case.
6 | So you will not find any game specific logic in this library.
7 |
8 | ## Install
9 |
10 | ```
11 | npm install @trixt0r/ecs
12 | ```
13 |
14 | ## Examples
15 |
16 | Checkout the [examples](https://github.com/Trixt0r/ecsts/tree/master/examples).
17 |
18 | Check the [rectangles example](https://stackblitz.com/edit/ecs-example-rectangles) out, if you do not want to checkout the code.
19 |
20 | ## Usage
21 |
22 | The main parts of this library are
23 |
24 | - [`Component`](https://github.com/Trixt0r/ecsts/blob/master/src/core/component.ts)
25 | - [`Entity`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L52)
26 | - [`System`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L77)
27 | - [`Engine`](https://github.com/Trixt0r/ecsts/blob/master/src/core/engine.ts#L96)
28 |
29 | ### Component
30 |
31 | A [`Component`](https://github.com/Trixt0r/ecsts/blob/master/src/core/component.ts) is defined as an interface with no specific methods or properties.
32 | I highly suggest you to implement your components as classes, since the systems will rely on those.
33 |
34 | For example, you could define a position like this:
35 |
36 | ```ts
37 | import { Component } from '@trixt0r/ecs';
38 |
39 | class Position implements Component {
40 | constructor(public x = 0, public y = 0) {}
41 | }
42 | ```
43 |
44 | ### Entity
45 |
46 | Entities are the elements, your systems will work with and have an unique identifier.
47 |
48 | Since this library doesn't want you to tell, how to generate your ids, the base class [`AbstractEntity`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L52) is abstract.
49 | This means your entity implementation should extend [`AbstractEntity`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L52).
50 |
51 | For example, you could do something like this:
52 |
53 | ```ts
54 | import { AbstractEntity } from '@trixt0r/ecs';
55 |
56 | class MyEntity extends AbstractEntity {
57 | constructor() {
58 | super(makeId());
59 | }
60 | }
61 | ```
62 |
63 | Adding components is just as simple as:
64 |
65 | ```ts
66 | myComponent.components.add(new Position(10, 20));
67 | ```
68 |
69 | An entity, is a [`Dispatcher`](https://github.com/Trixt0r/ecsts/blob/master/src/core/dispatcher.ts), which means, you can register an [`EntityListener`](https://github.com/Trixt0r/ecsts/blob/master/src/core/entity.ts#L12) on it, to check whether a component has been added, removed, the components have been sorted or cleared.
70 |
71 | ### System
72 |
73 | Systems implement the actual behavior of your entities, based on which components they own.
74 |
75 | For programming your own systems, you should implement the abstract class [`System`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L77).
76 | This base class provides basic functionalities, such as
77 |
78 | - an `updating` flag, which indicates whether a system is still updating.
79 | - an `active` flag, which tells the engine to either run the system in the next update call or not.
80 | - an `engine` property, which will be set/unset, as soon as the system gets added/removed to/from an engine.
81 |
82 | A system is also a [`Dispatcher`](https://github.com/Trixt0r/ecsts/blob/master/src/core/dispatcher.ts), which means, you can react to any actions happening to a system, by registering a [`SystemListener`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L14).
83 |
84 | Here is a minimal example of a system, which obtains a list of entities with the component type `Position`.
85 |
86 | ```ts
87 | import { System, Aspect } from '@trixt0r/ecs';
88 |
89 | class MySystem extends System {
90 | private aspect: Aspect;
91 |
92 | constructor() {
93 | super(/* optional priority here */);
94 | }
95 |
96 | onAddedToEngine(engine: Engine): void {
97 | // get entities by component 'Position'
98 | this.aspect = Aspect.for(engine).all(Position);
99 | }
100 |
101 | async process(): void {
102 | const entities = this.aspect.entities;
103 | entities.forEach(entity => {
104 | const position = entity.components.get(Position);
105 | //... do your logic here
106 | });
107 | }
108 | }
109 | ```
110 |
111 | Note that `process` can be `async`.
112 | If your systems need to do asynchronous tasks, you can implement them as those.
113 | Your engine can then run them as such.
114 | This might be useful, if you do not have data which needs to be processed every frame.
115 |
116 | In order to keep your focus on the actual system and not the boilerplate code around,
117 | you can use the [`AbstractEntitySystem`](https://github.com/Trixt0r/ecsts/blob/master/src/core/system.ts#L269).
118 |
119 | The class will help you by providing component types for directly defining an aspect for your system.
120 | The above code would become:
121 |
122 | ```ts
123 | import { System, Aspect } from '@trixt0r/ecs';
124 |
125 | class MySystem extends AbstractEntitySystem {
126 |
127 | constructor() {
128 | super(/* optional priority here */, [Position]);
129 | }
130 |
131 | processEntity(entity: MyEntity): void {
132 | const position = entity.components.get(Position);
133 | //... do your logic here
134 | }
135 | }
136 | ```
137 |
138 | ### Engine
139 |
140 | An [`Engine`](https://github.com/Trixt0r/ecsts/blob/master/src/core/engine.ts#L96) ties systems and entities together.
141 | It holds collections of both types, to which you can register listeners. But you could also register an [`EngineListener`](https://github.com/Trixt0r/ecsts/blob/master/src/core/engine.ts#L12), to listen for actions happening inside an engine.
142 |
143 | Here is a minimal example on how to initialize an engine and add systems and/or entities to it:
144 |
145 | ```ts
146 | import { Engine, EngineMode } from '@trixt0r/ecs';
147 |
148 | // Init the engine
149 | const engine = new Engine();
150 |
151 | engine.systems.add(new MySystem());
152 | engine.entities.add(new MyEntity());
153 |
154 | // anywhere in your business logic or main loop
155 | engine.run(delta);
156 |
157 | // if you want to perform your tasks asynchronously
158 | engine.run(delta, EngineMode.SUCCESSIVE); // Wait for a task to finish
159 | // or...
160 | engine.run(delta, EngineMode.PARALLEL); // Run all systems asynchronously in parallel
161 | ```
162 |
163 | ## Support
164 |
165 | If you find any odd behavior or other improvements, feel free to create issues.
166 | Pull requests are also welcome!
167 |
168 | Otherwise you can help me out by buying me a coffee.
169 |
170 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WDV9MU2GU35NN)
171 |
172 |
173 |
--------------------------------------------------------------------------------
/build/core/aspect.d.ts:
--------------------------------------------------------------------------------
1 | import { Component } from './component';
2 | import { Collection, CollectionListener } from './collection';
3 | import { AbstractEntity, EntityListener } from './entity';
4 | import { Engine } from './engine';
5 | import { ComponentClass } from './types';
6 | import { Dispatcher } from './dispatcher';
7 | /**
8 | * Component or component class.
9 | */
10 | declare type CompType = ComponentClass | Component;
11 | /**
12 | * A collection of entities.
13 | */
14 | declare type EntityCollection = Collection;
15 | /**
16 | * Entity which is synced within an aspect.
17 | */
18 | declare type SyncedEntity = AbstractEntity & {
19 | /**
20 | * Entity listener mapping for aspect specific caching purposes.
21 | */
22 | __ecsEntityListener: Record;
23 | /**
24 | * The list of listeners for this entity.
25 | */
26 | _lockedListeners: EntityListener[];
27 | };
28 | /**
29 | * Describes the constraints of an aspect.
30 | */
31 | export interface AspectDescriptor {
32 | /**
33 | * Components which all have to be matched by an entity.
34 | */
35 | all: CompType[];
36 | /**
37 | * Components which are not allowed to be matched by an entity.
38 | */
39 | exclude: CompType[];
40 | /**
41 | * Components of which at least one has to be matched by an entity.
42 | */
43 | one: CompType[];
44 | }
45 | /**
46 | * Listener which listens to various aspect events.
47 | */
48 | export interface AspectListener {
49 | /**
50 | * Called if new entities got added to the aspect.
51 | *
52 | * @param entities
53 | *
54 | */
55 | onAddedEntities?(...entities: AbstractEntity[]): void;
56 | /**
57 | * Called if existing entities got removed from the aspect.
58 | *
59 | * @param entities
60 | *
61 | */
62 | onRemovedEntities?(...entities: AbstractEntity[]): void;
63 | /**
64 | * Called if the source entities got cleared.
65 | *
66 | *
67 | */
68 | onClearedEntities?(): void;
69 | /**
70 | * Called if the source entities got sorted.
71 | *
72 | *
73 | */
74 | onSortedEntities?(): void;
75 | /**
76 | * Gets called if new components got added to the given entity.
77 | *
78 | * @param entity
79 | * @param components
80 | *
81 | */
82 | onAddedComponents?(entity: AbstractEntity, ...components: Component[]): void;
83 | /**
84 | * Gets called if components got removed from the given entity.
85 | *
86 | * @param entity
87 | * @param components
88 | *
89 | */
90 | onRemovedComponents?(entity: AbstractEntity, ...components: Component[]): void;
91 | /**
92 | * Gets called if the components of the given entity got cleared.
93 | *
94 | * @param entity
95 | *
96 | */
97 | onClearedComponents?(entity: AbstractEntity): void;
98 | /**
99 | * Gets called if the components of the given entity got sorted.
100 | *
101 | * @param entity
102 | *
103 | */
104 | onSortedComponents?(entity: AbstractEntity): void;
105 | /**
106 | * Gets called if the aspect got attached.
107 | *
108 | *
109 | */
110 | onAttached?(): void;
111 | /**
112 | * Gets called if the aspect got detached.
113 | *
114 | *
115 | */
116 | onDetached?(): void;
117 | }
118 | /**
119 | * An aspect is used to filter a collection of entities by component types.
120 | *
121 | * Use @see {Aspect#get} to obtain an aspect for a list of components to observe on an engine or a collection of entities.
122 | * The obtained aspect instance will take care of synchronizing with the source collection in an efficient way.
123 | * The user will always have snapshot of entities which meet the aspect criteria no matter when an entity got
124 | * added or removed.
125 | *
126 | */
127 | export declare class Aspect extends Dispatcher {
128 | source: EntityCollection;
129 | /**
130 | * Internal index.
131 | */
132 | protected static ID: number;
133 | /**
134 | * Internal unique id.
135 | */
136 | protected readonly id: number;
137 | /**
138 | * Component types which all have to be matched by the entity source.
139 | */
140 | protected allComponents: CompType[];
141 | /**
142 | * Component types which all are not allowed to match.
143 | */
144 | protected excludeComponents: CompType[];
145 | /**
146 | * Component types of which at least one has to match.
147 | */
148 | protected oneComponents: CompType[];
149 | /**
150 | * The entities which meet the filter conditions.
151 | */
152 | protected filteredEntities: SyncedEntity[];
153 | /**
154 | * A frozen copy of the filtered entities for the public access.
155 | */
156 | protected frozenEntities: AbstractEntity[];
157 | /**
158 | * The collection listener for syncing data.
159 | */
160 | protected listener: CollectionListener;
161 | /**
162 | * Whether this filter is currently attached to its collection as a listener or not.
163 | */
164 | protected attached: boolean;
165 | /**
166 | * Creates an instance of an Aspect.
167 | *
168 | * @param source The collection of entities to filter.
169 | * @param all Optional component types which should all match.
170 | * @param exclude Optional component types which should not match.
171 | * @param one Optional component types of which at least one should match.
172 | */
173 | protected constructor(source: EntityCollection, all?: CompType[], exclude?: CompType[], one?: CompType[]);
174 | /**
175 | * Performs the match on each entity in the source collection.
176 | *
177 | *
178 | */
179 | protected matchAll(): void;
180 | /**
181 | * Checks whether the given entity matches the constraints on this aspect.
182 | *
183 | * @param entity The entity to check for.
184 | * @return Whether the given entity has at least one component which matches.
185 | */
186 | matches(entity: AbstractEntity): boolean;
187 | /**
188 | * Updates the frozen entities.
189 | *
190 | *
191 | */
192 | protected updateFrozen(): void;
193 | /**
194 | * Sets up the component sync logic.
195 | *
196 | * @param entities The entities to perform the setup for.
197 | * @return {void}
198 | */
199 | protected setupComponentSync(entities: SyncedEntity[]): void;
200 | /**
201 | * Removes the component sync logic.
202 | *
203 | * @param entities The entities to remove the setup from.
204 | * @return {void}
205 | */
206 | protected removeComponentSync(entities: Readonly): void;
207 | /**
208 | * Attaches this filter to its collection.
209 | *
210 | *
211 | */
212 | attach(): void;
213 | /**
214 | * Detaches this filter from its collection.
215 | *
216 | *
217 | */
218 | detach(): void;
219 | /**
220 | * Whether this filter is attached to its collection or not.
221 | */
222 | get isAttached(): boolean;
223 | /**
224 | * The entities which match the criteria of this filter.
225 | */
226 | get entities(): readonly AbstractEntity[];
227 | /**
228 | * Includes all the given component types.
229 | *
230 | * Entities have to match every type.
231 | *
232 | * @param classes
233 | */
234 | all(...classes: CompType[]): this;
235 | /**
236 | * @alias @see {Aspect#all}
237 | * @param classes
238 | */
239 | every(...classes: CompType[]): this;
240 | /**
241 | * Excludes all of the given component types.
242 | *
243 | * Entities have to exclude all types.
244 | *
245 | * @param classes
246 | */
247 | exclude(...classes: CompType[]): this;
248 | /**
249 | * @alias @see {Aspect#exclude}
250 | * @param classes
251 | */
252 | without(...classes: CompType[]): this;
253 | /**
254 | * Includes one of the given component types.
255 | *
256 | * Entities have to match only one type.
257 | *
258 | * @param classes
259 | */
260 | one(...classes: CompType[]): this;
261 | /**
262 | * @alias @see {Aspect#one}
263 | * @param classes
264 | */
265 | some(...classes: CompType[]): this;
266 | /**
267 | * Collects information about this aspect and returns it.
268 | *
269 | *
270 | */
271 | getDescriptor(): AspectDescriptor;
272 | /**
273 | * Returns an aspect for the given engine or collection of entities.
274 | *
275 | * @param collOrEngine
276 | * @param all Optional component types which should all match.
277 | * @param exclude Optional component types which should not match.
278 | * @param one Optional component types of which at least one should match.
279 | *
280 | */
281 | static for(collOrEngine: EntityCollection | Engine, all?: CompType[], exclude?: CompType[], one?: CompType[]): Aspect;
282 | }
283 | export {};
284 |
--------------------------------------------------------------------------------
/build/core/collection.d.ts:
--------------------------------------------------------------------------------
1 | import { Dispatcher } from './dispatcher';
2 | /**
3 | * The listener interface for a listener on an entity.
4 | */
5 | export interface CollectionListener {
6 | /**
7 | * Called as soon as new elements have been added to the collection.
8 | *
9 | * @param elements
10 | */
11 | onAdded?(...elements: T[]): void;
12 | /**
13 | * Called as soon as elements got removed from the collection.
14 | *
15 | * @param elements
16 | */
17 | onRemoved?(...elements: T[]): void;
18 | /**
19 | * Called as soon as all elements got removed from the collection.
20 | */
21 | onCleared?(): void;
22 | /**
23 | * Called as soon as the elements got sorted.
24 | */
25 | onSorted?(): void;
26 | }
27 | /**
28 | * A collection holds a list of elements of a certain type
29 | * and allows to add, remove, sort and clear the list.
30 | * On each operation the internal list of elements gets frozen,
31 | * so a user of the collection will not be able to operate on the real reference,
32 | * but read the data without the need of copying the data on each read access.
33 | */
34 | export declare class Collection extends Dispatcher> implements IterableIterator {
35 | /**
36 | * The internal list of elements.
37 | */
38 | protected _elements: T[];
39 | /**
40 | * The frozen list of elements which is used to expose the element list to the public.
41 | */
42 | protected _frozenElements: T[];
43 | /**
44 | * The internal iterator pointer.
45 | */
46 | protected pointer: number;
47 | /**
48 | * Creates an instance of Collection.
49 | *
50 | * @param initial An optional initial list of elements.
51 | */
52 | constructor(initial?: T[]);
53 | /**
54 | * @inheritdoc
55 | */
56 | next(): IteratorResult;
57 | /**
58 | * @inheritdoc
59 | */
60 | [Symbol.iterator](): IterableIterator;
61 | /**
62 | * A snapshot of all elements in this collection.
63 | */
64 | get elements(): readonly T[];
65 | /**
66 | * The length, of this collection, i.e. how many elements this collection contains.
67 | */
68 | get length(): number;
69 | /**
70 | * Updates the internal frozen element list.
71 | */
72 | protected updatedFrozenObjects(): void;
73 | /**
74 | * Adds the given element to this collection.
75 | *
76 | * @param element
77 | * @return Whether the element has been added or not.
78 | * It may not be added, if already present in the element list.
79 | */
80 | protected addSingle(element: T): boolean;
81 | /**
82 | * Adds the given element(s) to this collection.
83 | *
84 | * @param elements
85 | * @return Whether elements have been added or not.
86 | * They may not have been added, if they were already present in the element list.
87 | */
88 | add(...elements: T[]): boolean;
89 | /**
90 | * Removes the given element or the element at the given index.
91 | *
92 | * @param elOrIndex
93 | * @return Whether the element has been removed or not.
94 | * It may not have been removed, if it was not in the element list.
95 | */
96 | protected removeSingle(elOrIndex: T | number): boolean;
97 | /**
98 | * Removes the given element(s) or the elements at the given indices.
99 | *
100 | * @param elementsOrIndices
101 | * @return Whether elements have been removed or not.
102 | * They may not have been removed, if every element was not in the element list.
103 | */
104 | remove(...elementsOrIndices: (T | number)[]): boolean;
105 | /**
106 | * Clears this collection, i.e. removes all elements from the internal list.
107 | */
108 | clear(): void;
109 | /**
110 | * Returns the index of the given element.
111 | *
112 | * @param element The element.
113 | * @return The index of the given element or id.
114 | */
115 | indexOf(element: T): number;
116 | /**
117 | * Sorts this collection.
118 | *
119 | * @param [compareFn]
120 | *
121 | */
122 | sort(compareFn?: (a: T, b: T) => number): this;
123 | /**
124 | * Returns the elements of this collection that meet the condition specified in a callback function.
125 | *
126 | * @param callbackfn
127 | * A function that accepts up to three arguments.
128 | * The filter method calls the `callbackfn` function one time for each element in the collection.
129 | * @param thisArg An object to which the this keyword can refer in the callbackfn function.
130 | * If `thisArg` is omitted, undefined is used as the this value.
131 | * @return An array with elements which met the condition.
132 | */
133 | filter(callbackfn: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T[];
134 | /**
135 | * Performs the specified action for each element in the collection.
136 | *
137 | * @param callbackFn
138 | * A function that accepts up to three arguments.
139 | * forEach calls the callbackfn function one time for each element in the array.
140 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
141 | * If thisArg is omitted, undefined is used as the this value.
142 | */
143 | forEach(callbackFn: (element: T, index: number, array: readonly T[]) => void, thisArg?: U): void;
144 | /**
145 | * Returns the value of the first element in the collection where predicate is true, and undefined
146 | * otherwise.
147 | *
148 | * @param predicate
149 | * Find calls predicate once for each element of the array, in ascending order,
150 | * until it finds one where predicate returns true. If such an element is found, find
151 | * immediately returns that element value. Otherwise, find returns undefined.
152 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
153 | * If thisArg is omitted, undefined is used as the this value.
154 | *
155 | */
156 | find(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T | undefined;
157 | /**
158 | * Returns the index of the first element in the collection where predicate is true, and -1
159 | * otherwise.
160 | *
161 | * @param predicate
162 | * Find calls predicate once for each element of the array, in ascending order,
163 | * until it finds one where predicate returns true. If such an element is found,
164 | * findIndex immediately returns that element index. Otherwise, findIndex returns -1.
165 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
166 | * If thisArg is omitted, undefined is used as the this value.
167 | *
168 | */
169 | findIndex(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): number;
170 | /**
171 | * Calls a defined callback function on each element of an collection, and returns an array that contains the results.
172 | *
173 | * @param callbackFn
174 | * A function that accepts up to three arguments.
175 | * The map method calls the callbackfn function one time for each element in the array.
176 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
177 | * If thisArg is omitted, undefined is used as the this value.
178 | *
179 | */
180 | map(callbackFn: (element: T, index: number, array: readonly T[]) => V, thisArg?: U): V[];
181 | /**
182 | *
183 | * Determines whether all the members of the collection satisfy the specified test.
184 | *
185 | * @param callbackFn
186 | * A function that accepts up to three arguments.
187 | * The every method calls the callbackfn function for each element in array1 until the callbackfn
188 | * returns false, or until the end of the array.
189 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
190 | * If thisArg is omitted, undefined is used as the this value.
191 | *
192 | */
193 | every(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean;
194 | /**
195 | * Determines whether the specified callback function returns true for any element of the collection.
196 | *
197 | * @param callbackFn
198 | * A function that accepts up to three arguments.
199 | * The some method calls the callbackfn function for each element in the collection until the callbackfn
200 | * returns true, or until the end of the collection.
201 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
202 | * If thisArg is omitted, undefined is used as the this value.
203 | *
204 | */
205 | some(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean;
206 | /**
207 | * Calls the specified callback function for all the elements in the collection.
208 | * The return value of the callback function is the accumulated result,
209 | * and is provided as an argument in the next call to the callback function.
210 | *
211 | * @param callbackFn
212 | * A function that accepts up to four arguments.
213 | * The reduce method calls the callbackfn function one time for each element in the array.
214 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation.
215 | * The first call to the callbackfn function provides this value as an argument instead of
216 | * an collection value.
217 | *
218 | */
219 | reduce(callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;
220 | /**
221 | * Calls the specified callback function for all the elements in the collection, in descending order.
222 | * The return value of the callback function is the accumulated result,
223 | * and is provided as an argument in the next call to the callback function.
224 | *
225 | * @param callbackFn
226 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation.
227 | * The first call to the callbackfn function provides this value as an argument instead of
228 | * an collection value.
229 | *
230 | */
231 | reduceRight(callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;
232 | }
233 |
--------------------------------------------------------------------------------
/build/core/collection.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __read = (this && this.__read) || function (o, n) {
16 | var m = typeof Symbol === "function" && o[Symbol.iterator];
17 | if (!m) return o;
18 | var i = m.call(o), r, ar = [], e;
19 | try {
20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
21 | }
22 | catch (error) { e = { error: error }; }
23 | finally {
24 | try {
25 | if (r && !r.done && (m = i["return"])) m.call(i);
26 | }
27 | finally { if (e) throw e.error; }
28 | }
29 | return ar;
30 | };
31 | var __spread = (this && this.__spread) || function () {
32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
33 | return ar;
34 | };
35 | Object.defineProperty(exports, "__esModule", { value: true });
36 | exports.Collection = void 0;
37 | var dispatcher_1 = require("./dispatcher");
38 | /**
39 | * A collection holds a list of elements of a certain type
40 | * and allows to add, remove, sort and clear the list.
41 | * On each operation the internal list of elements gets frozen,
42 | * so a user of the collection will not be able to operate on the real reference,
43 | * but read the data without the need of copying the data on each read access.
44 | */
45 | var Collection = /** @class */ (function (_super) {
46 | __extends(Collection, _super);
47 | /**
48 | * Creates an instance of Collection.
49 | *
50 | * @param initial An optional initial list of elements.
51 | */
52 | function Collection(initial) {
53 | if (initial === void 0) { initial = []; }
54 | var _this = _super.call(this) || this;
55 | /**
56 | * The frozen list of elements which is used to expose the element list to the public.
57 | */
58 | _this._frozenElements = [];
59 | /**
60 | * The internal iterator pointer.
61 | */
62 | _this.pointer = 0;
63 | _this._elements = initial.slice();
64 | _this.updatedFrozenObjects();
65 | return _this;
66 | }
67 | /**
68 | * @inheritdoc
69 | */
70 | Collection.prototype.next = function () {
71 | if (this.pointer < this._elements.length) {
72 | return {
73 | done: false,
74 | value: this._elements[this.pointer++],
75 | };
76 | }
77 | else {
78 | return {
79 | done: true,
80 | value: null,
81 | };
82 | }
83 | };
84 | /**
85 | * @inheritdoc
86 | */
87 | Collection.prototype[Symbol.iterator] = function () {
88 | this.pointer = 0;
89 | return this;
90 | };
91 | Object.defineProperty(Collection.prototype, "elements", {
92 | /**
93 | * A snapshot of all elements in this collection.
94 | */
95 | get: function () {
96 | return this._frozenElements;
97 | },
98 | enumerable: false,
99 | configurable: true
100 | });
101 | Object.defineProperty(Collection.prototype, "length", {
102 | /**
103 | * The length, of this collection, i.e. how many elements this collection contains.
104 | */
105 | get: function () {
106 | return this._frozenElements.length;
107 | },
108 | enumerable: false,
109 | configurable: true
110 | });
111 | /**
112 | * Updates the internal frozen element list.
113 | */
114 | Collection.prototype.updatedFrozenObjects = function () {
115 | this._frozenElements = this._elements.slice();
116 | Object.freeze(this._frozenElements);
117 | };
118 | /**
119 | * Adds the given element to this collection.
120 | *
121 | * @param element
122 | * @return Whether the element has been added or not.
123 | * It may not be added, if already present in the element list.
124 | */
125 | Collection.prototype.addSingle = function (element) {
126 | if (this._elements.indexOf(element) >= 0)
127 | return false;
128 | this._elements.push(element);
129 | return true;
130 | };
131 | /**
132 | * Adds the given element(s) to this collection.
133 | *
134 | * @param elements
135 | * @return Whether elements have been added or not.
136 | * They may not have been added, if they were already present in the element list.
137 | */
138 | Collection.prototype.add = function () {
139 | var _this = this;
140 | var elements = [];
141 | for (var _i = 0; _i < arguments.length; _i++) {
142 | elements[_i] = arguments[_i];
143 | }
144 | var added = elements.filter(function (element) { return _this.addSingle(element); });
145 | if (added.length <= 0)
146 | return false;
147 | this.updatedFrozenObjects();
148 | this.dispatch.apply(this, __spread(['onAdded'], added));
149 | return true;
150 | };
151 | /**
152 | * Removes the given element or the element at the given index.
153 | *
154 | * @param elOrIndex
155 | * @return Whether the element has been removed or not.
156 | * It may not have been removed, if it was not in the element list.
157 | */
158 | Collection.prototype.removeSingle = function (elOrIndex) {
159 | if (typeof elOrIndex === 'number')
160 | elOrIndex = this.elements[elOrIndex];
161 | var idx = this._elements.findIndex(function (_) { return _ === elOrIndex; });
162 | if (idx < 0 || idx >= this._elements.length)
163 | return false;
164 | this._elements.splice(idx, 1);
165 | return true;
166 | };
167 | /**
168 | * Removes the given element(s) or the elements at the given indices.
169 | *
170 | * @param elementsOrIndices
171 | * @return Whether elements have been removed or not.
172 | * They may not have been removed, if every element was not in the element list.
173 | */
174 | Collection.prototype.remove = function () {
175 | var _this = this;
176 | var elementsOrIndices = [];
177 | for (var _i = 0; _i < arguments.length; _i++) {
178 | elementsOrIndices[_i] = arguments[_i];
179 | }
180 | var removed = elementsOrIndices.filter(function (element) { return _this.removeSingle(element); });
181 | if (removed.length <= 0)
182 | return false;
183 | var removedElements = removed.map(function (o) { return (typeof o === 'number' ? _this.elements[o] : o); });
184 | this.updatedFrozenObjects();
185 | this.dispatch.apply(this, __spread(['onRemoved'], removedElements));
186 | return true;
187 | };
188 | /**
189 | * Clears this collection, i.e. removes all elements from the internal list.
190 | */
191 | Collection.prototype.clear = function () {
192 | if (!this._elements.length)
193 | return;
194 | this._elements = [];
195 | this.updatedFrozenObjects();
196 | this.dispatch('onCleared');
197 | };
198 | /**
199 | * Returns the index of the given element.
200 | *
201 | * @param element The element.
202 | * @return The index of the given element or id.
203 | */
204 | Collection.prototype.indexOf = function (element) {
205 | return this._elements.indexOf(element);
206 | };
207 | /**
208 | * Sorts this collection.
209 | *
210 | * @param [compareFn]
211 | *
212 | */
213 | Collection.prototype.sort = function (compareFn) {
214 | if (!this._elements.length)
215 | return this;
216 | this._elements.sort(compareFn);
217 | this.updatedFrozenObjects();
218 | this.dispatch('onSorted');
219 | return this;
220 | };
221 | /**
222 | * Returns the elements of this collection that meet the condition specified in a callback function.
223 | *
224 | * @param callbackfn
225 | * A function that accepts up to three arguments.
226 | * The filter method calls the `callbackfn` function one time for each element in the collection.
227 | * @param thisArg An object to which the this keyword can refer in the callbackfn function.
228 | * If `thisArg` is omitted, undefined is used as the this value.
229 | * @return An array with elements which met the condition.
230 | */
231 | Collection.prototype.filter = function (callbackfn, thisArg) {
232 | return this._elements.filter(callbackfn, thisArg);
233 | };
234 | /**
235 | * Performs the specified action for each element in the collection.
236 | *
237 | * @param callbackFn
238 | * A function that accepts up to three arguments.
239 | * forEach calls the callbackfn function one time for each element in the array.
240 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
241 | * If thisArg is omitted, undefined is used as the this value.
242 | */
243 | Collection.prototype.forEach = function (callbackFn, thisArg) {
244 | this._elements.forEach(callbackFn, thisArg);
245 | };
246 | /**
247 | * Returns the value of the first element in the collection where predicate is true, and undefined
248 | * otherwise.
249 | *
250 | * @param predicate
251 | * Find calls predicate once for each element of the array, in ascending order,
252 | * until it finds one where predicate returns true. If such an element is found, find
253 | * immediately returns that element value. Otherwise, find returns undefined.
254 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
255 | * If thisArg is omitted, undefined is used as the this value.
256 | *
257 | */
258 | Collection.prototype.find = function (predicate, thisArg) {
259 | return this._elements.find(predicate, thisArg);
260 | };
261 | /**
262 | * Returns the index of the first element in the collection where predicate is true, and -1
263 | * otherwise.
264 | *
265 | * @param predicate
266 | * Find calls predicate once for each element of the array, in ascending order,
267 | * until it finds one where predicate returns true. If such an element is found,
268 | * findIndex immediately returns that element index. Otherwise, findIndex returns -1.
269 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
270 | * If thisArg is omitted, undefined is used as the this value.
271 | *
272 | */
273 | Collection.prototype.findIndex = function (predicate, thisArg) {
274 | return this._elements.findIndex(predicate, thisArg);
275 | };
276 | /**
277 | * Calls a defined callback function on each element of an collection, and returns an array that contains the results.
278 | *
279 | * @param callbackFn
280 | * A function that accepts up to three arguments.
281 | * The map method calls the callbackfn function one time for each element in the array.
282 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
283 | * If thisArg is omitted, undefined is used as the this value.
284 | *
285 | */
286 | Collection.prototype.map = function (callbackFn, thisArg) {
287 | return this._elements.map(callbackFn, thisArg);
288 | };
289 | /**
290 | *
291 | * Determines whether all the members of the collection satisfy the specified test.
292 | *
293 | * @param callbackFn
294 | * A function that accepts up to three arguments.
295 | * The every method calls the callbackfn function for each element in array1 until the callbackfn
296 | * returns false, or until the end of the array.
297 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
298 | * If thisArg is omitted, undefined is used as the this value.
299 | *
300 | */
301 | Collection.prototype.every = function (callbackFn, thisArg) {
302 | return this._elements.every(callbackFn, thisArg);
303 | };
304 | /**
305 | * Determines whether the specified callback function returns true for any element of the collection.
306 | *
307 | * @param callbackFn
308 | * A function that accepts up to three arguments.
309 | * The some method calls the callbackfn function for each element in the collection until the callbackfn
310 | * returns true, or until the end of the collection.
311 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
312 | * If thisArg is omitted, undefined is used as the this value.
313 | *
314 | */
315 | Collection.prototype.some = function (callbackFn, thisArg) {
316 | return this._elements.some(callbackFn, thisArg);
317 | };
318 | /**
319 | * Calls the specified callback function for all the elements in the collection.
320 | * The return value of the callback function is the accumulated result,
321 | * and is provided as an argument in the next call to the callback function.
322 | *
323 | * @param callbackFn
324 | * A function that accepts up to four arguments.
325 | * The reduce method calls the callbackfn function one time for each element in the array.
326 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation.
327 | * The first call to the callbackfn function provides this value as an argument instead of
328 | * an collection value.
329 | *
330 | */
331 | Collection.prototype.reduce = function (callbackFn, initialValue) {
332 | return this._elements.reduce(callbackFn, initialValue);
333 | };
334 | /**
335 | * Calls the specified callback function for all the elements in the collection, in descending order.
336 | * The return value of the callback function is the accumulated result,
337 | * and is provided as an argument in the next call to the callback function.
338 | *
339 | * @param callbackFn
340 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation.
341 | * The first call to the callbackfn function provides this value as an argument instead of
342 | * an collection value.
343 | *
344 | */
345 | Collection.prototype.reduceRight = function (callbackFn, initialValue) {
346 | return this._elements.reduceRight(callbackFn, initialValue);
347 | };
348 | return Collection;
349 | }(dispatcher_1.Dispatcher));
350 | exports.Collection = Collection;
351 |
--------------------------------------------------------------------------------
/build/core/component.d.ts:
--------------------------------------------------------------------------------
1 | import { Collection, CollectionListener } from './collection';
2 | import { ComponentClass } from './types';
3 | /**
4 | * The component interface, every component has to implement.
5 | *
6 | * If you want your system to treat different Components the same way,
7 | * you may define a static string variable named `type` in your components.
8 | */
9 | export interface Component extends Record {
10 | /**
11 | * An optional id for the component.
12 | */
13 | id?: string;
14 | /**
15 | * An optional type for the component.
16 | */
17 | type?: string;
18 | }
19 | /**
20 | * A collection for components.
21 | * Supports accessing components by their class.
22 | *
23 | */
24 | export declare class ComponentCollection extends Collection implements CollectionListener {
25 | /**
26 | * Internal map for faster component access, by class or type.
27 | */
28 | protected cache: Map, readonly C[]>;
29 | /**
30 | * Internal state for updating the components access memory.
31 | *
32 | */
33 | protected dirty: Map, boolean>;
34 | constructor(initial?: C[]);
35 | /**
36 | * @inheritdoc
37 | * Update the internal cache.
38 | */
39 | onAdded(...elements: C[]): void;
40 | /**
41 | * @inheritdoc
42 | * Update the internal cache.
43 | */
44 | onRemoved(...elements: C[]): void;
45 | /**
46 | * @inheritdoc
47 | * Update the internal cache.
48 | */
49 | onCleared(): void;
50 | /**
51 | * Searches for the first component matching the given class or type.
52 | *
53 | * @param classOrType The class or type a component has to match.
54 | * @return The found component or `null`.
55 | */
56 | get(classOrType: ComponentClass | string): T;
57 | /**
58 | * Searches for the all components matching the given class or type.
59 | *
60 | * @param classOrType The class or type components have to match.
61 | * @return A list of all components matching the given class.
62 | */
63 | getAll(classOrType: ComponentClass | string): readonly T[];
64 | /**
65 | * Updates the cache for the given class or type.
66 | *
67 | * @param classOrType The class or type to update the cache for.
68 | */
69 | protected updateCache(classOrType: ComponentClass | string): void;
70 | /**
71 | * Marks the classes and types of the given elements as dirty,
72 | * so their cache gets updated on the next request.
73 | *
74 | * @param elements
75 | *
76 | */
77 | protected markForCacheUpdate(...elements: C[]): void;
78 | }
79 |
--------------------------------------------------------------------------------
/build/core/component.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __read = (this && this.__read) || function (o, n) {
16 | var m = typeof Symbol === "function" && o[Symbol.iterator];
17 | if (!m) return o;
18 | var i = m.call(o), r, ar = [], e;
19 | try {
20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
21 | }
22 | catch (error) { e = { error: error }; }
23 | finally {
24 | try {
25 | if (r && !r.done && (m = i["return"])) m.call(i);
26 | }
27 | finally { if (e) throw e.error; }
28 | }
29 | return ar;
30 | };
31 | var __spread = (this && this.__spread) || function () {
32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
33 | return ar;
34 | };
35 | var __values = (this && this.__values) || function(o) {
36 | var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
37 | if (m) return m.call(o);
38 | if (o && typeof o.length === "number") return {
39 | next: function () {
40 | if (o && i >= o.length) o = void 0;
41 | return { value: o && o[i++], done: !o };
42 | }
43 | };
44 | throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
45 | };
46 | Object.defineProperty(exports, "__esModule", { value: true });
47 | exports.ComponentCollection = void 0;
48 | var collection_1 = require("./collection");
49 | /**
50 | * A collection for components.
51 | * Supports accessing components by their class.
52 | *
53 | */
54 | var ComponentCollection = /** @class */ (function (_super) {
55 | __extends(ComponentCollection, _super);
56 | function ComponentCollection(initial) {
57 | if (initial === void 0) { initial = []; }
58 | var _this = _super.call(this, initial) || this;
59 | /**
60 | * Internal map for faster component access, by class or type.
61 | */
62 | _this.cache = new Map();
63 | /**
64 | * Internal state for updating the components access memory.
65 | *
66 | */
67 | _this.dirty = new Map();
68 | _this.addListener(_this, true);
69 | return _this;
70 | }
71 | /**
72 | * @inheritdoc
73 | * Update the internal cache.
74 | */
75 | ComponentCollection.prototype.onAdded = function () {
76 | var elements = [];
77 | for (var _i = 0; _i < arguments.length; _i++) {
78 | elements[_i] = arguments[_i];
79 | }
80 | this.markForCacheUpdate.apply(this, __spread(elements));
81 | };
82 | /**
83 | * @inheritdoc
84 | * Update the internal cache.
85 | */
86 | ComponentCollection.prototype.onRemoved = function () {
87 | var elements = [];
88 | for (var _i = 0; _i < arguments.length; _i++) {
89 | elements[_i] = arguments[_i];
90 | }
91 | this.markForCacheUpdate.apply(this, __spread(elements));
92 | };
93 | /**
94 | * @inheritdoc
95 | * Update the internal cache.
96 | */
97 | ComponentCollection.prototype.onCleared = function () {
98 | this.dirty.clear();
99 | this.cache.clear();
100 | };
101 | /**
102 | * Searches for the first component matching the given class or type.
103 | *
104 | * @param classOrType The class or type a component has to match.
105 | * @return The found component or `null`.
106 | */
107 | ComponentCollection.prototype.get = function (classOrType) {
108 | return this.getAll(classOrType)[0];
109 | };
110 | /**
111 | * Searches for the all components matching the given class or type.
112 | *
113 | * @param classOrType The class or type components have to match.
114 | * @return A list of all components matching the given class.
115 | */
116 | ComponentCollection.prototype.getAll = function (classOrType) {
117 | if (this.dirty.get(classOrType))
118 | this.updateCache(classOrType);
119 | if (this.cache.has(classOrType))
120 | return this.cache.get(classOrType);
121 | this.updateCache(classOrType);
122 | return this.cache.get(classOrType);
123 | };
124 | /**
125 | * Updates the cache for the given class or type.
126 | *
127 | * @param classOrType The class or type to update the cache for.
128 | */
129 | ComponentCollection.prototype.updateCache = function (classOrType) {
130 | var e_1, _a;
131 | var keys = this.cache.keys();
132 | var type = typeof classOrType === 'string' ? classOrType : classOrType.type;
133 | var filtered = this.filter(function (element) {
134 | var _a;
135 | var clazz = element.constructor;
136 | var typeVal = (_a = element.type) !== null && _a !== void 0 ? _a : clazz.type;
137 | return type && typeVal ? type === typeVal : clazz === classOrType;
138 | });
139 | if (typeof classOrType !== 'string' && classOrType.type) {
140 | this.cache.set(classOrType.type, filtered);
141 | this.dirty.delete(classOrType.type);
142 | }
143 | else if (typeof classOrType === 'string') {
144 | try {
145 | for (var keys_1 = __values(keys), keys_1_1 = keys_1.next(); !keys_1_1.done; keys_1_1 = keys_1.next()) {
146 | var key = keys_1_1.value;
147 | if (typeof key !== 'string' && key.type === classOrType) {
148 | this.cache.set(key, filtered);
149 | this.dirty.delete(key);
150 | }
151 | }
152 | }
153 | catch (e_1_1) { e_1 = { error: e_1_1 }; }
154 | finally {
155 | try {
156 | if (keys_1_1 && !keys_1_1.done && (_a = keys_1.return)) _a.call(keys_1);
157 | }
158 | finally { if (e_1) throw e_1.error; }
159 | }
160 | }
161 | this.cache.set(classOrType, filtered);
162 | this.dirty.delete(classOrType);
163 | };
164 | /**
165 | * Marks the classes and types of the given elements as dirty,
166 | * so their cache gets updated on the next request.
167 | *
168 | * @param elements
169 | *
170 | */
171 | ComponentCollection.prototype.markForCacheUpdate = function () {
172 | var _this = this;
173 | var elements = [];
174 | for (var _i = 0; _i < arguments.length; _i++) {
175 | elements[_i] = arguments[_i];
176 | }
177 | var keys = this.cache.keys();
178 | elements.forEach(function (element) {
179 | var e_2, _a;
180 | var _b, _c;
181 | var clazz = element.constructor;
182 | var classOrType = (_c = (_b = element.type) !== null && _b !== void 0 ? _b : clazz.type) !== null && _c !== void 0 ? _c : clazz;
183 | if (_this.dirty.get(classOrType))
184 | return;
185 | if (typeof classOrType === 'string') {
186 | try {
187 | for (var keys_2 = __values(keys), keys_2_1 = keys_2.next(); !keys_2_1.done; keys_2_1 = keys_2.next()) {
188 | var key = keys_2_1.value;
189 | if (typeof key !== 'string' && key.type === classOrType)
190 | _this.dirty.set(key, true);
191 | }
192 | }
193 | catch (e_2_1) { e_2 = { error: e_2_1 }; }
194 | finally {
195 | try {
196 | if (keys_2_1 && !keys_2_1.done && (_a = keys_2.return)) _a.call(keys_2);
197 | }
198 | finally { if (e_2) throw e_2.error; }
199 | }
200 | }
201 | _this.dirty.set(classOrType, true);
202 | });
203 | };
204 | return ComponentCollection;
205 | }(collection_1.Collection));
206 | exports.ComponentCollection = ComponentCollection;
207 |
--------------------------------------------------------------------------------
/build/core/dispatcher.d.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentTypes } from './types';
2 | /**
3 | * A dispatcher is an abstract object which holds a list of listeners
4 | * to which data during certain events can be dispatched, by calling functions implemented by listeners.
5 | *
6 | */
7 | export declare abstract class Dispatcher {
8 | /**
9 | * The list of listeners for this dispatcher.
10 | */
11 | protected _listeners: Partial[];
12 | /**
13 | * Locked listeners.
14 | * Those listeners in this array can not be removed anymore.
15 | */
16 | protected _lockedListeners: Partial[];
17 | /**
18 | * Creates an instance of Dispatcher.
19 | */
20 | constructor();
21 | /**
22 | * The current listeners for this dispatcher.
23 | */
24 | get listeners(): readonly Partial[];
25 | /**
26 | * Adds the given listener to this entity.
27 | *
28 | * @param listener
29 | * @return Whether the listener has been added or not.
30 | * It may not be added, if already present in the listener list.
31 | */
32 | addListener(listener: Partial, lock?: boolean): boolean;
33 | /**
34 | * Removes the given listener or the listener at the given index.
35 | *
36 | * @param listenerOrIndex
37 | * @return Whether the listener has been removed or not.
38 | * It may not have been removed, if it was not in the listener list.
39 | */
40 | removeListener(listenerOrIndex: Partial | number): boolean;
41 | /**
42 | * Dispatches the given arguments by calling the given function name
43 | * on each listener, if implemented.
44 | * Note that the listener's scope will be used, when the listener's function gets called.
45 | *
46 | * @param name The function name to call.
47 | * @param args The arguments to pass to the function.
48 | */
49 | dispatch(name: K, ...args: ArgumentTypes): void;
50 | }
51 |
--------------------------------------------------------------------------------
/build/core/dispatcher.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.Dispatcher = void 0;
4 | /**
5 | * A dispatcher is an abstract object which holds a list of listeners
6 | * to which data during certain events can be dispatched, by calling functions implemented by listeners.
7 | *
8 | */
9 | var Dispatcher = /** @class */ (function () {
10 | /**
11 | * Creates an instance of Dispatcher.
12 | */
13 | function Dispatcher() {
14 | this._listeners = [];
15 | this._lockedListeners = [];
16 | }
17 | Object.defineProperty(Dispatcher.prototype, "listeners", {
18 | /**
19 | * The current listeners for this dispatcher.
20 | */
21 | get: function () {
22 | return this._listeners.slice();
23 | },
24 | enumerable: false,
25 | configurable: true
26 | });
27 | /**
28 | * Adds the given listener to this entity.
29 | *
30 | * @param listener
31 | * @return Whether the listener has been added or not.
32 | * It may not be added, if already present in the listener list.
33 | */
34 | Dispatcher.prototype.addListener = function (listener, lock) {
35 | if (lock === void 0) { lock = false; }
36 | if (this._listeners.indexOf(listener) >= 0)
37 | return false;
38 | this._listeners.push(listener);
39 | if (lock)
40 | this._lockedListeners.push(listener);
41 | return true;
42 | };
43 | /**
44 | * Removes the given listener or the listener at the given index.
45 | *
46 | * @param listenerOrIndex
47 | * @return Whether the listener has been removed or not.
48 | * It may not have been removed, if it was not in the listener list.
49 | */
50 | Dispatcher.prototype.removeListener = function (listenerOrIndex) {
51 | var idx = typeof listenerOrIndex === 'number' ? listenerOrIndex : this._listeners.indexOf(listenerOrIndex);
52 | if (idx >= 0 && idx < this._listeners.length) {
53 | var listener = this._listeners[idx];
54 | var isLocked = this._lockedListeners.indexOf(listener) >= 0;
55 | if (isLocked)
56 | throw new Error("Listener at index " + idx + " is locked.");
57 | this._listeners.splice(idx, 1);
58 | return true;
59 | }
60 | return false;
61 | };
62 | /**
63 | * Dispatches the given arguments by calling the given function name
64 | * on each listener, if implemented.
65 | * Note that the listener's scope will be used, when the listener's function gets called.
66 | *
67 | * @param name The function name to call.
68 | * @param args The arguments to pass to the function.
69 | */
70 | Dispatcher.prototype.dispatch = function (name) {
71 | var args = [];
72 | for (var _i = 1; _i < arguments.length; _i++) {
73 | args[_i - 1] = arguments[_i];
74 | }
75 | // TODO: optimize this; cache the listeners with the given function name
76 | this._listeners.forEach(function (listener) {
77 | var fn = listener[name];
78 | if (typeof fn !== 'function')
79 | return;
80 | fn.apply(listener, args);
81 | });
82 | };
83 | return Dispatcher;
84 | }());
85 | exports.Dispatcher = Dispatcher;
86 |
--------------------------------------------------------------------------------
/build/core/engine.d.ts:
--------------------------------------------------------------------------------
1 | import { System, SystemListener } from './system';
2 | import { AbstractEntity } from './entity';
3 | import { Dispatcher } from './dispatcher';
4 | import { Collection } from './collection';
5 | import { EntityCollection } from './entity.collection';
6 | /**
7 | * The listener interface for a listener on an engine.
8 | */
9 | export interface EngineListener {
10 | /**
11 | * Called as soon as the given system gets added to the engine.
12 | *
13 | * @param systems
14 | */
15 | onAddedSystems?(...systems: System[]): void;
16 | /**
17 | * Called as soon as the given system gets removed from the engine.
18 | *
19 | * @param systems
20 | */
21 | onRemovedSystems?(...systems: System[]): void;
22 | /**
23 | * Called as soon as all systems got cleared from the engine.
24 | */
25 | onClearedSystems?(): void;
26 | /**
27 | * Called as soon as an error occurred on in an active system during update.
28 | *
29 | * @param error The error that occurred.
30 | * @param system The system on which the error occurred.
31 | */
32 | onErrorBySystem?(error: Error, system: System): void;
33 | /**
34 | * Called as soon as the given entity gets added to the engine.
35 | *
36 | * @param entities
37 | */
38 | onAddedEntities?(...entities: T[]): void;
39 | /**
40 | * Called as soon as the given entity gets removed from the engine.
41 | *
42 | * @param entities
43 | */
44 | onRemovedEntities?(...entities: AbstractEntity[]): void;
45 | /**
46 | * Called as soon as all entities got cleared from the engine.
47 | */
48 | onClearedEntities?(): void;
49 | }
50 | /**
51 | * Defines how an engine executes its active systems.
52 | */
53 | export declare enum EngineMode {
54 | /**
55 | * Execute all systems by priority without waiting for them to resolve.
56 | */
57 | DEFAULT = "runDefault",
58 | /**
59 | * Execute all systems by priority. Successive systems
60 | * will wait until the current executing system resolves or rejects.
61 | */
62 | SUCCESSIVE = "runSuccessive",
63 | /**
64 | * Start all systems by priority, but run them all in parallel.
65 | */
66 | PARALLEL = "runParallel"
67 | }
68 | /**
69 | * An engine puts entities and systems together.
70 | * It holds for each type a collection, which can be queried by each system.
71 | *
72 | * The @see {Engine#update} method has to be called in order to perform updates on each system in a certain order.
73 | * The engine takes care of updating only active systems in any point of time.
74 | *
75 | */
76 | export declare class Engine extends Dispatcher {
77 | /**
78 | * The internal list of all systems in this engine.
79 | */
80 | protected _systems: Collection>;
81 | /**
82 | * The frozen list of active systems which is used to iterate during the update.
83 | */
84 | protected _activeSystems: System[];
85 | /**
86 | * The internal list of all entities in this engine.
87 | */
88 | protected _entities: EntityCollection;
89 | /**
90 | * Creates an instance of Engine.
91 | */
92 | constructor();
93 | /**
94 | * A snapshot of all entities in this engine.
95 | */
96 | get entities(): EntityCollection;
97 | /**
98 | * A snapshot of all systems in this engine.
99 | */
100 | get systems(): Collection;
101 | /**
102 | * A snapshot of all active systems in this engine.
103 | */
104 | get activeSystems(): readonly System[];
105 | /**
106 | * Updates the internal active system list.
107 | */
108 | protected updatedActiveSystems(): void;
109 | /**
110 | * Updates all systems in this engine by the given delta value.
111 | *
112 | * @param [options]
113 | * @param [mode = EngineMode.DEFAULT]
114 | */
115 | run(options?: T, mode?: EngineMode): void | Promise;
116 | /**
117 | * Updates all systems in this engine by the given delta value,
118 | * without waiting for a resolve or reject of each system.
119 | *
120 | * @param [options]
121 | */
122 | protected runDefault(options?: T): void;
123 | /**
124 | * Updates all systems in this engine by the given delta value,
125 | * by waiting for a system to resolve or reject before continuing with the next one.
126 | *
127 | * @param [options]
128 | */
129 | protected runSuccessive(options?: T): Promise;
130 | /**
131 | * Updates all systems in this engine by the given delta value,
132 | * by running all systems in parallel and waiting for all systems to resolve or reject.
133 | *
134 | * @param [options]
135 | */
136 | protected runParallel(options?: T): Promise;
137 | }
138 |
--------------------------------------------------------------------------------
/build/core/engine.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
17 | return new (P || (P = Promise))(function (resolve, reject) {
18 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
19 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
20 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
21 | step((generator = generator.apply(thisArg, _arguments || [])).next());
22 | });
23 | };
24 | var __generator = (this && this.__generator) || function (thisArg, body) {
25 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
26 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
27 | function verb(n) { return function (v) { return step([n, v]); }; }
28 | function step(op) {
29 | if (f) throw new TypeError("Generator is already executing.");
30 | while (_) try {
31 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
32 | if (y = 0, t) op = [op[0] & 2, t.value];
33 | switch (op[0]) {
34 | case 0: case 1: t = op; break;
35 | case 4: _.label++; return { value: op[1], done: false };
36 | case 5: _.label++; y = op[1]; op = [0]; continue;
37 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
38 | default:
39 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
40 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
41 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
42 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
43 | if (t[2]) _.ops.pop();
44 | _.trys.pop(); continue;
45 | }
46 | op = body.call(thisArg, _);
47 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
48 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
49 | }
50 | };
51 | var __read = (this && this.__read) || function (o, n) {
52 | var m = typeof Symbol === "function" && o[Symbol.iterator];
53 | if (!m) return o;
54 | var i = m.call(o), r, ar = [], e;
55 | try {
56 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
57 | }
58 | catch (error) { e = { error: error }; }
59 | finally {
60 | try {
61 | if (r && !r.done && (m = i["return"])) m.call(i);
62 | }
63 | finally { if (e) throw e.error; }
64 | }
65 | return ar;
66 | };
67 | var __spread = (this && this.__spread) || function () {
68 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
69 | return ar;
70 | };
71 | Object.defineProperty(exports, "__esModule", { value: true });
72 | exports.Engine = exports.EngineMode = void 0;
73 | var system_1 = require("./system");
74 | var dispatcher_1 = require("./dispatcher");
75 | var collection_1 = require("./collection");
76 | var entity_collection_1 = require("./entity.collection");
77 | /**
78 | * Defines how an engine executes its active systems.
79 | */
80 | var EngineMode;
81 | (function (EngineMode) {
82 | /**
83 | * Execute all systems by priority without waiting for them to resolve.
84 | */
85 | EngineMode["DEFAULT"] = "runDefault";
86 | /**
87 | * Execute all systems by priority. Successive systems
88 | * will wait until the current executing system resolves or rejects.
89 | */
90 | EngineMode["SUCCESSIVE"] = "runSuccessive";
91 | /**
92 | * Start all systems by priority, but run them all in parallel.
93 | */
94 | EngineMode["PARALLEL"] = "runParallel";
95 | })(EngineMode = exports.EngineMode || (exports.EngineMode = {}));
96 | /**
97 | * An engine puts entities and systems together.
98 | * It holds for each type a collection, which can be queried by each system.
99 | *
100 | * The @see {Engine#update} method has to be called in order to perform updates on each system in a certain order.
101 | * The engine takes care of updating only active systems in any point of time.
102 | *
103 | */
104 | var Engine = /** @class */ (function (_super) {
105 | __extends(Engine, _super);
106 | /**
107 | * Creates an instance of Engine.
108 | */
109 | function Engine() {
110 | var _this = _super.call(this) || this;
111 | /**
112 | * The internal list of all systems in this engine.
113 | */
114 | _this._systems = new collection_1.Collection();
115 | /**
116 | * The frozen list of active systems which is used to iterate during the update.
117 | */
118 | _this._activeSystems = [];
119 | /**
120 | * The internal list of all entities in this engine.
121 | */
122 | _this._entities = new entity_collection_1.EntityCollection();
123 | _this._systems.addListener({
124 | onAdded: function () {
125 | var systems = [];
126 | for (var _i = 0; _i < arguments.length; _i++) {
127 | systems[_i] = arguments[_i];
128 | }
129 | _this._systems.sort(function (a, b) { return a.priority - b.priority; });
130 | systems.forEach(function (system) {
131 | system.engine = _this;
132 | _this.updatedActiveSystems();
133 | var systemListener = {
134 | onActivated: function () { return _this.updatedActiveSystems(); },
135 | onDeactivated: function () { return _this.updatedActiveSystems(); },
136 | onError: function (error) { return _this.dispatch('onErrorBySystem', error, system); },
137 | };
138 | system.__ecsEngineListener = systemListener;
139 | system.addListener(systemListener, true);
140 | });
141 | _this.dispatch.apply(_this, __spread(['onAddedSystems'], systems));
142 | },
143 | onRemoved: function () {
144 | var systems = [];
145 | for (var _i = 0; _i < arguments.length; _i++) {
146 | systems[_i] = arguments[_i];
147 | }
148 | systems.forEach(function (system) {
149 | system.engine = null;
150 | _this.updatedActiveSystems();
151 | var systemListener = system.__ecsEngineListener;
152 | var locked = system._lockedListeners;
153 | locked.splice(locked.indexOf(systemListener), 1);
154 | system.removeListener(systemListener);
155 | });
156 | _this.dispatch.apply(_this, __spread(['onRemovedSystems'], systems));
157 | },
158 | onCleared: function () { return _this.dispatch('onClearedSystems'); },
159 | }, true);
160 | _this._entities.addListener({
161 | onAdded: function () {
162 | var entities = [];
163 | for (var _i = 0; _i < arguments.length; _i++) {
164 | entities[_i] = arguments[_i];
165 | }
166 | return _this.dispatch.apply(_this, __spread(['onAddedEntities'], entities));
167 | },
168 | onRemoved: function () {
169 | var entities = [];
170 | for (var _i = 0; _i < arguments.length; _i++) {
171 | entities[_i] = arguments[_i];
172 | }
173 | return _this.dispatch.apply(_this, __spread(['onRemovedEntities'], entities));
174 | },
175 | onCleared: function () { return _this.dispatch('onClearedEntities'); },
176 | }, true);
177 | _this.updatedActiveSystems();
178 | return _this;
179 | }
180 | Object.defineProperty(Engine.prototype, "entities", {
181 | /**
182 | * A snapshot of all entities in this engine.
183 | */
184 | get: function () {
185 | return this._entities;
186 | },
187 | enumerable: false,
188 | configurable: true
189 | });
190 | Object.defineProperty(Engine.prototype, "systems", {
191 | /**
192 | * A snapshot of all systems in this engine.
193 | */
194 | get: function () {
195 | return this._systems;
196 | },
197 | enumerable: false,
198 | configurable: true
199 | });
200 | Object.defineProperty(Engine.prototype, "activeSystems", {
201 | /**
202 | * A snapshot of all active systems in this engine.
203 | */
204 | get: function () {
205 | return this._activeSystems;
206 | },
207 | enumerable: false,
208 | configurable: true
209 | });
210 | /**
211 | * Updates the internal active system list.
212 | */
213 | Engine.prototype.updatedActiveSystems = function () {
214 | this._activeSystems = this.systems.filter(function (system) { return system.active; });
215 | Object.freeze(this._activeSystems);
216 | };
217 | /**
218 | * Updates all systems in this engine by the given delta value.
219 | *
220 | * @param [options]
221 | * @param [mode = EngineMode.DEFAULT]
222 | */
223 | Engine.prototype.run = function (options, mode) {
224 | if (mode === void 0) { mode = EngineMode.DEFAULT; }
225 | return this[mode].call(this, options);
226 | };
227 | /**
228 | * Updates all systems in this engine by the given delta value,
229 | * without waiting for a resolve or reject of each system.
230 | *
231 | * @param [options]
232 | */
233 | Engine.prototype.runDefault = function (options) {
234 | var length = this._activeSystems.length;
235 | for (var i = 0; i < length; i++)
236 | this._activeSystems[i].run(options, system_1.SystemMode.SYNC);
237 | };
238 | /**
239 | * Updates all systems in this engine by the given delta value,
240 | * by waiting for a system to resolve or reject before continuing with the next one.
241 | *
242 | * @param [options]
243 | */
244 | Engine.prototype.runSuccessive = function (options) {
245 | return __awaiter(this, void 0, void 0, function () {
246 | var length, i;
247 | return __generator(this, function (_a) {
248 | switch (_a.label) {
249 | case 0:
250 | length = this._activeSystems.length;
251 | i = 0;
252 | _a.label = 1;
253 | case 1:
254 | if (!(i < length)) return [3 /*break*/, 4];
255 | return [4 /*yield*/, this._activeSystems[i].run(options, system_1.SystemMode.SYNC)];
256 | case 2:
257 | _a.sent();
258 | _a.label = 3;
259 | case 3:
260 | i++;
261 | return [3 /*break*/, 1];
262 | case 4: return [2 /*return*/];
263 | }
264 | });
265 | });
266 | };
267 | /**
268 | * Updates all systems in this engine by the given delta value,
269 | * by running all systems in parallel and waiting for all systems to resolve or reject.
270 | *
271 | * @param [options]
272 | */
273 | Engine.prototype.runParallel = function (options) {
274 | return __awaiter(this, void 0, void 0, function () {
275 | var mapped;
276 | return __generator(this, function (_a) {
277 | switch (_a.label) {
278 | case 0:
279 | mapped = this._activeSystems.map(function (system) { return system.run(options, system_1.SystemMode.ASYNC); });
280 | return [4 /*yield*/, Promise.all(mapped)];
281 | case 1:
282 | _a.sent();
283 | return [2 /*return*/];
284 | }
285 | });
286 | });
287 | };
288 | return Engine;
289 | }(dispatcher_1.Dispatcher));
290 | exports.Engine = Engine;
291 |
--------------------------------------------------------------------------------
/build/core/entity.collection.d.ts:
--------------------------------------------------------------------------------
1 | import { Collection, CollectionListener } from './collection';
2 | import { AbstractEntity } from './entity';
3 | export declare class EntityCollection extends Collection implements CollectionListener {
4 | /**
5 | * Internal map for faster entity access, by id.
6 | */
7 | protected cache: Map;
8 | constructor(...args: any[]);
9 | /**
10 | * Returns the entity for the given id in this collection.
11 | *
12 | * @param id The id to search for.
13 | * @return The found entity or `undefined` if not found.
14 | */
15 | get(id: string | number): T | undefined;
16 | /**
17 | * @inheritdoc
18 | */
19 | onAdded(...entities: T[]): void;
20 | /**
21 | * @inheritdoc
22 | */
23 | onRemoved(...entities: T[]): void;
24 | /**
25 | * @inheritdoc
26 | */
27 | onCleared(): void;
28 | }
29 |
--------------------------------------------------------------------------------
/build/core/entity.collection.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __read = (this && this.__read) || function (o, n) {
16 | var m = typeof Symbol === "function" && o[Symbol.iterator];
17 | if (!m) return o;
18 | var i = m.call(o), r, ar = [], e;
19 | try {
20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
21 | }
22 | catch (error) { e = { error: error }; }
23 | finally {
24 | try {
25 | if (r && !r.done && (m = i["return"])) m.call(i);
26 | }
27 | finally { if (e) throw e.error; }
28 | }
29 | return ar;
30 | };
31 | var __spread = (this && this.__spread) || function () {
32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
33 | return ar;
34 | };
35 | Object.defineProperty(exports, "__esModule", { value: true });
36 | exports.EntityCollection = void 0;
37 | var collection_1 = require("./collection");
38 | var EntityCollection = /** @class */ (function (_super) {
39 | __extends(EntityCollection, _super);
40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
41 | function EntityCollection() {
42 | var args = [];
43 | for (var _i = 0; _i < arguments.length; _i++) {
44 | args[_i] = arguments[_i];
45 | }
46 | var _this = _super.apply(this, __spread(args)) || this;
47 | /**
48 | * Internal map for faster entity access, by id.
49 | */
50 | _this.cache = new Map();
51 | _this.addListener(_this, true);
52 | return _this;
53 | }
54 | /**
55 | * Returns the entity for the given id in this collection.
56 | *
57 | * @param id The id to search for.
58 | * @return The found entity or `undefined` if not found.
59 | */
60 | EntityCollection.prototype.get = function (id) {
61 | return this.cache.get(id);
62 | };
63 | /**
64 | * @inheritdoc
65 | */
66 | EntityCollection.prototype.onAdded = function () {
67 | var entities = [];
68 | for (var _i = 0; _i < arguments.length; _i++) {
69 | entities[_i] = arguments[_i];
70 | }
71 | for (var i = 0, l = entities.length; i < l; i++)
72 | this.cache.set(entities[i].id, entities[i]);
73 | };
74 | /**
75 | * @inheritdoc
76 | */
77 | EntityCollection.prototype.onRemoved = function () {
78 | var entities = [];
79 | for (var _i = 0; _i < arguments.length; _i++) {
80 | entities[_i] = arguments[_i];
81 | }
82 | for (var i = 0, l = entities.length; i < l; i++)
83 | this.cache.delete(entities[i].id);
84 | };
85 | /**
86 | * @inheritdoc
87 | */
88 | EntityCollection.prototype.onCleared = function () {
89 | this.cache.clear();
90 | };
91 | return EntityCollection;
92 | }(collection_1.Collection));
93 | exports.EntityCollection = EntityCollection;
94 |
--------------------------------------------------------------------------------
/build/core/entity.d.ts:
--------------------------------------------------------------------------------
1 | import { Component, ComponentCollection } from './component';
2 | import { Dispatcher } from './dispatcher';
3 | import { CollectionListener } from './collection';
4 | /**
5 | * The listener interface for a listener on an entity.
6 | */
7 | export interface EntityListener {
8 | /**
9 | * Called as soon as a new component as been added to the entity.
10 | *
11 | * @param components The new added components.
12 | */
13 | onAddedComponents?(...components: C[]): void;
14 | /**
15 | * Called as soon as a component got removed from the entity.
16 | *
17 | * @param components The removed components
18 | */
19 | onRemovedComponents?(...components: C[]): void;
20 | /**
21 | * Called as soon as all components got removed from the entity.
22 | */
23 | onClearedComponents?(): void;
24 | /**
25 | * Called as soon as the components got sorted.
26 | */
27 | onSortedComponents?(): void;
28 | }
29 | /**
30 | *
31 | * An entity holds an id and a list of components attached to it.
32 | * You can add or remove components from the entity.
33 | */
34 | export declare abstract class AbstractEntity> extends Dispatcher implements CollectionListener {
35 | readonly id: number | string;
36 | /**
37 | * The internal list of components.
38 | */
39 | protected _components: ComponentCollection;
40 | /**
41 | * Creates an instance of Entity.
42 | *
43 | * @param id The id, you should provide by yourself. Maybe an uuid or a simple number.
44 | */
45 | constructor(id: number | string);
46 | /**
47 | * A snapshot of all components of this entity.
48 | */
49 | get components(): ComponentCollection;
50 | /**
51 | * Dispatches the `onAdded` event to all listeners as `onAddedComponents`.
52 | *
53 | * @param components
54 | */
55 | onAdded(...components: C[]): void;
56 | /**
57 | * Dispatches the `onRemoved` event to all listeners as `onRemovedComponents`.
58 | *
59 | * @param components
60 | *
61 | */
62 | onRemoved(...components: C[]): void;
63 | /**
64 | * Dispatches the `onCleared` event to all listeners as `onClearedComponents`.
65 | *
66 | *
67 | */
68 | onCleared(): void;
69 | /**
70 | * Dispatches the `onSorted` event to all listeners as `onSortedComponents`.
71 | *
72 | *
73 | */
74 | onSorted(): void;
75 | }
76 |
--------------------------------------------------------------------------------
/build/core/entity.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __read = (this && this.__read) || function (o, n) {
16 | var m = typeof Symbol === "function" && o[Symbol.iterator];
17 | if (!m) return o;
18 | var i = m.call(o), r, ar = [], e;
19 | try {
20 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
21 | }
22 | catch (error) { e = { error: error }; }
23 | finally {
24 | try {
25 | if (r && !r.done && (m = i["return"])) m.call(i);
26 | }
27 | finally { if (e) throw e.error; }
28 | }
29 | return ar;
30 | };
31 | var __spread = (this && this.__spread) || function () {
32 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
33 | return ar;
34 | };
35 | Object.defineProperty(exports, "__esModule", { value: true });
36 | exports.AbstractEntity = void 0;
37 | var component_1 = require("./component");
38 | var dispatcher_1 = require("./dispatcher");
39 | /**
40 | *
41 | * An entity holds an id and a list of components attached to it.
42 | * You can add or remove components from the entity.
43 | */
44 | var AbstractEntity = /** @class */ (function (_super) {
45 | __extends(AbstractEntity, _super);
46 | /**
47 | * Creates an instance of Entity.
48 | *
49 | * @param id The id, you should provide by yourself. Maybe an uuid or a simple number.
50 | */
51 | function AbstractEntity(id) {
52 | var _this = _super.call(this) || this;
53 | _this.id = id;
54 | _this._components = new component_1.ComponentCollection();
55 | _this._components.addListener(_this, true);
56 | return _this;
57 | }
58 | Object.defineProperty(AbstractEntity.prototype, "components", {
59 | /**
60 | * A snapshot of all components of this entity.
61 | */
62 | get: function () {
63 | return this._components;
64 | },
65 | enumerable: false,
66 | configurable: true
67 | });
68 | /**
69 | * Dispatches the `onAdded` event to all listeners as `onAddedComponents`.
70 | *
71 | * @param components
72 | */
73 | AbstractEntity.prototype.onAdded = function () {
74 | var _a;
75 | var components = [];
76 | for (var _i = 0; _i < arguments.length; _i++) {
77 | components[_i] = arguments[_i];
78 | }
79 | return (_a = this).dispatch.apply(_a, __spread(['onAddedComponents'], components));
80 | };
81 | /**
82 | * Dispatches the `onRemoved` event to all listeners as `onRemovedComponents`.
83 | *
84 | * @param components
85 | *
86 | */
87 | AbstractEntity.prototype.onRemoved = function () {
88 | var _a;
89 | var components = [];
90 | for (var _i = 0; _i < arguments.length; _i++) {
91 | components[_i] = arguments[_i];
92 | }
93 | return (_a = this).dispatch.apply(_a, __spread(['onRemovedComponents'], components));
94 | };
95 | /**
96 | * Dispatches the `onCleared` event to all listeners as `onClearedComponents`.
97 | *
98 | *
99 | */
100 | AbstractEntity.prototype.onCleared = function () {
101 | return this.dispatch('onClearedComponents');
102 | };
103 | /**
104 | * Dispatches the `onSorted` event to all listeners as `onSortedComponents`.
105 | *
106 | *
107 | */
108 | AbstractEntity.prototype.onSorted = function () {
109 | return this.dispatch('onSortedComponents');
110 | };
111 | return AbstractEntity;
112 | }(dispatcher_1.Dispatcher));
113 | exports.AbstractEntity = AbstractEntity;
114 |
--------------------------------------------------------------------------------
/build/core/system.d.ts:
--------------------------------------------------------------------------------
1 | import { Engine } from './engine';
2 | import { Dispatcher } from './dispatcher';
3 | import { AbstractEntity } from './entity';
4 | import { ComponentClass } from './types';
5 | import { Component } from './component';
6 | import { Aspect, AspectListener } from './aspect';
7 | /**
8 | * The listener interface for a listener added to a system.
9 | */
10 | export interface SystemListener {
11 | /**
12 | * Called as soon as the `active` switched to `true`.
13 | */
14 | onActivated?(): void;
15 | /**
16 | * Called as soon as the `active` switched to `false`.
17 | */
18 | onDeactivated?(): void;
19 | /**
20 | * Called as soon as the system got removed from an engine.
21 | *
22 | * @param engine The engine this system got removed from.
23 | */
24 | onRemovedFromEngine?(engine: Engine): void;
25 | /**
26 | * Called as soon as the system got added to an engine.
27 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}.
28 | *
29 | * @param engine The engine this system got added to.
30 | */
31 | onAddedToEngine?(engine: Engine): void;
32 | /**
33 | * Called as soon an error occurred during update.
34 | *
35 | * @param error The error which occurred.
36 | */
37 | onError?(error: Error): void;
38 | }
39 | /**
40 | * Defines how a system executes its task.
41 | *
42 | * @enum {number}
43 | */
44 | export declare enum SystemMode {
45 | /**
46 | * Do work and resolve immediately.
47 | */
48 | SYNC = "runSync",
49 | /**
50 | * Do async work. E.g. do work in a worker, make requests to another server, etc.
51 | */
52 | ASYNC = "runAsync"
53 | }
54 | /**
55 | * A system processes a list of entities which belong to an engine.
56 | * Entities can only be accessed via the assigned engine. @see {Engine}.
57 | * The implementation of the specific system has to choose on which components of an entity to operate.
58 | *
59 | */
60 | export declare abstract class System extends Dispatcher {
61 | priority: number;
62 | /**
63 | * Determines whether this system is active or not.
64 | *
65 | */
66 | protected _active: boolean;
67 | /**
68 | * Determines whether this system is currently updating or not.
69 | *
70 | */
71 | protected _updating: boolean;
72 | /**
73 | * The reference to the current engine.
74 | *
75 | * @memberof System
76 | */
77 | protected _engine: Engine | null;
78 | /**
79 | * Creates an instance of System.
80 | *
81 | * @param [priority=0] The priority of this system. The lower the value the earlier it will process.
82 | */
83 | constructor(priority?: number);
84 | /**
85 | * The active state of this system.
86 | * If the flag is set to `false`, this system will not be able to process.
87 | *
88 | */
89 | get active(): boolean;
90 | set active(active: boolean);
91 | /**
92 | * The engine this system is assigned to.
93 | *
94 | */
95 | get engine(): Engine | null;
96 | set engine(engine: Engine | null);
97 | /**
98 | * Determines whether this system is currently updating or not.
99 | * The value will stay `true` until @see {System#process} resolves or rejects.
100 | *
101 | * @readonly
102 | */
103 | get updating(): boolean;
104 | /**
105 | * Runs the system process with the given delta time.
106 | *
107 | * @param options
108 | * @param mode The system mode to run in.
109 | *
110 | */
111 | run(options: T, mode?: SystemMode): void | Promise;
112 | /**
113 | * Processes data synchronously.
114 | *
115 | * @param options
116 | *
117 | */
118 | protected runSync(options: T): void;
119 | /**
120 | * Processes data asynchronously.
121 | *
122 | * @param options
123 | *
124 | */
125 | protected runAsync(options: T): Promise;
126 | /**
127 | * Processes the entities of the current engine.
128 | * To be implemented by any concrete system.
129 | *
130 | * @param options
131 | *
132 | */
133 | abstract process(options: T): void | Promise;
134 | /**
135 | * Called as soon as the `active` switched to `true`.
136 | *
137 | *
138 | */
139 | onActivated(): void;
140 | /**
141 | * Called as soon as the `active` switched to `false`.
142 | *
143 | *
144 | */
145 | onDeactivated(): void;
146 | /**
147 | * Called as soon as the system got removed from an engine.
148 | *
149 | * @param engine The engine this system got added to.
150 | *
151 | *
152 | */
153 | onRemovedFromEngine(engine: Engine): void;
154 | /**
155 | * Called as soon as the system got added to an engine.
156 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}.
157 | *
158 | * @param engine The engine this system got added to.
159 | *
160 | *
161 | */
162 | onAddedToEngine(engine: Engine): void;
163 | /**
164 | * Called as soon an error occurred during update.
165 | *
166 | * @param error The error which occurred.
167 | *
168 | *
169 | */
170 | onError(error: Error): void;
171 | }
172 | /**
173 | * An abstract entity system is a system which processes each entity.
174 | *
175 | * Optionally it accepts component types for auto filtering the entities before processing.
176 | * This class abstracts away the initialization of aspects and detaches them properly, if needed.
177 | *
178 | */
179 | export declare abstract class AbstractEntitySystem extends System implements AspectListener {
180 | priority: number;
181 | protected all?: (Component | ComponentClass)[] | undefined;
182 | protected exclude?: (Component | ComponentClass)[] | undefined;
183 | protected one?: (Component | ComponentClass)[] | undefined;
184 | /**
185 | * The optional aspect, if any.
186 | *
187 | */
188 | protected aspect: Aspect | null;
189 | /**
190 | * Creates an instance of AbstractEntitySystem.
191 | *
192 | * @param priority The priority of this system. The lower the value the earlier it will process.
193 | * @param all Optional component types which should all match.
194 | * @param exclude Optional component types which should not match.
195 | * @param one Optional component types of which at least one should match.
196 | */
197 | constructor(priority?: number, all?: (Component | ComponentClass)[] | undefined, exclude?: (Component | ComponentClass)[] | undefined, one?: (Component | ComponentClass)[] | undefined);
198 | /** @inheritdoc */
199 | onAddedToEngine(engine: Engine): void;
200 | /** @inheritdoc */
201 | onRemovedFromEngine(): void;
202 | /**
203 | * Called if new entities got added to the system.
204 | *
205 | * @param entities
206 | *
207 | */
208 | onAddedEntities(...entities: AbstractEntity[]): void;
209 | /**
210 | * Called if existing entities got removed from the system.
211 | *
212 | * @param entities
213 | *
214 | */
215 | onRemovedEntities?(...entities: AbstractEntity[]): void;
216 | /**
217 | * Called if the entities got cleared.
218 | *
219 | *
220 | */
221 | onClearedEntities?(): void;
222 | /**
223 | * Called if the entities got sorted.
224 | *
225 | *
226 | */
227 | onSortedEntities?(): void;
228 | /**
229 | * Gets called if new components got added to the given entity.
230 | *
231 | * @param entity
232 | * @param components
233 | *
234 | */
235 | onAddedComponents?(entity: AbstractEntity, ...components: Component[]): void;
236 | /**
237 | * Gets called if components got removed from the given entity.
238 | *
239 | * @param entity
240 | * @param components
241 | *
242 | */
243 | onRemovedComponents?(entity: AbstractEntity, ...components: Component[]): void;
244 | /**
245 | * Gets called if the components of the given entity got cleared.
246 | *
247 | * @param entity
248 | *
249 | */
250 | onClearedComponents?(entity: AbstractEntity): void;
251 | /**
252 | * Gets called if the components of the given entity got sorted.
253 | *
254 | * @param entity
255 | *
256 | */
257 | onSortedComponents?(entity: AbstractEntity): void;
258 | /** @inheritdoc */
259 | process(options?: U): void;
260 | /**
261 | * Processes the given entity.
262 | *
263 | * @param entity
264 | * @param index
265 | * @param entities
266 | * @param options
267 | *
268 | */
269 | abstract processEntity(entity: T, index?: number, entities?: T[], options?: U): void;
270 | }
271 |
--------------------------------------------------------------------------------
/build/core/system.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | };
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
17 | return new (P || (P = Promise))(function (resolve, reject) {
18 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
19 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
20 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
21 | step((generator = generator.apply(thisArg, _arguments || [])).next());
22 | });
23 | };
24 | var __generator = (this && this.__generator) || function (thisArg, body) {
25 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
26 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
27 | function verb(n) { return function (v) { return step([n, v]); }; }
28 | function step(op) {
29 | if (f) throw new TypeError("Generator is already executing.");
30 | while (_) try {
31 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
32 | if (y = 0, t) op = [op[0] & 2, t.value];
33 | switch (op[0]) {
34 | case 0: case 1: t = op; break;
35 | case 4: _.label++; return { value: op[1], done: false };
36 | case 5: _.label++; y = op[1]; op = [0]; continue;
37 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
38 | default:
39 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
40 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
41 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
42 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
43 | if (t[2]) _.ops.pop();
44 | _.trys.pop(); continue;
45 | }
46 | op = body.call(thisArg, _);
47 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
48 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
49 | }
50 | };
51 | Object.defineProperty(exports, "__esModule", { value: true });
52 | exports.AbstractEntitySystem = exports.System = exports.SystemMode = void 0;
53 | /* eslint-disable @typescript-eslint/no-empty-function */
54 | var engine_1 = require("./engine");
55 | var dispatcher_1 = require("./dispatcher");
56 | var aspect_1 = require("./aspect");
57 | /**
58 | * Defines how a system executes its task.
59 | *
60 | * @enum {number}
61 | */
62 | var SystemMode;
63 | (function (SystemMode) {
64 | /**
65 | * Do work and resolve immediately.
66 | */
67 | SystemMode["SYNC"] = "runSync";
68 | /**
69 | * Do async work. E.g. do work in a worker, make requests to another server, etc.
70 | */
71 | SystemMode["ASYNC"] = "runAsync";
72 | })(SystemMode = exports.SystemMode || (exports.SystemMode = {}));
73 | /**
74 | * A system processes a list of entities which belong to an engine.
75 | * Entities can only be accessed via the assigned engine. @see {Engine}.
76 | * The implementation of the specific system has to choose on which components of an entity to operate.
77 | *
78 | */
79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
80 | var System = /** @class */ (function (_super) {
81 | __extends(System, _super);
82 | /**
83 | * Creates an instance of System.
84 | *
85 | * @param [priority=0] The priority of this system. The lower the value the earlier it will process.
86 | */
87 | function System(priority) {
88 | if (priority === void 0) { priority = 0; }
89 | var _this = _super.call(this) || this;
90 | _this.priority = priority;
91 | _this._active = true;
92 | _this._updating = false;
93 | _this._engine = null;
94 | return _this;
95 | }
96 | Object.defineProperty(System.prototype, "active", {
97 | /**
98 | * The active state of this system.
99 | * If the flag is set to `false`, this system will not be able to process.
100 | *
101 | */
102 | get: function () {
103 | return this._active;
104 | },
105 | set: function (active) {
106 | if (active === this._active)
107 | return;
108 | this._active = active;
109 | if (active) {
110 | this.onActivated();
111 | }
112 | else {
113 | this.onDeactivated();
114 | }
115 | this.dispatch(active ? 'onActivated' : 'onDeactivated');
116 | },
117 | enumerable: false,
118 | configurable: true
119 | });
120 | Object.defineProperty(System.prototype, "engine", {
121 | /**
122 | * The engine this system is assigned to.
123 | *
124 | */
125 | get: function () {
126 | return this._engine;
127 | },
128 | set: function (engine) {
129 | if (engine === this._engine)
130 | return;
131 | var oldEngine = this._engine;
132 | this._engine = engine;
133 | if (oldEngine instanceof engine_1.Engine) {
134 | this.onRemovedFromEngine(oldEngine);
135 | this.dispatch('onRemovedFromEngine', oldEngine);
136 | }
137 | if (engine instanceof engine_1.Engine) {
138 | this.onAddedToEngine(engine);
139 | this.dispatch('onAddedToEngine', engine);
140 | }
141 | },
142 | enumerable: false,
143 | configurable: true
144 | });
145 | Object.defineProperty(System.prototype, "updating", {
146 | /**
147 | * Determines whether this system is currently updating or not.
148 | * The value will stay `true` until @see {System#process} resolves or rejects.
149 | *
150 | * @readonly
151 | */
152 | get: function () {
153 | return this._updating;
154 | },
155 | enumerable: false,
156 | configurable: true
157 | });
158 | /**
159 | * Runs the system process with the given delta time.
160 | *
161 | * @param options
162 | * @param mode The system mode to run in.
163 | *
164 | */
165 | System.prototype.run = function (options, mode) {
166 | if (mode === void 0) { mode = SystemMode.SYNC; }
167 | return this[mode].call(this, options);
168 | };
169 | /**
170 | * Processes data synchronously.
171 | *
172 | * @param options
173 | *
174 | */
175 | System.prototype.runSync = function (options) {
176 | try {
177 | this.process(options);
178 | }
179 | catch (e) {
180 | this.onError(e);
181 | this.dispatch('onError', e);
182 | }
183 | };
184 | /**
185 | * Processes data asynchronously.
186 | *
187 | * @param options
188 | *
189 | */
190 | System.prototype.runAsync = function (options) {
191 | return __awaiter(this, void 0, void 0, function () {
192 | var e_1;
193 | return __generator(this, function (_a) {
194 | switch (_a.label) {
195 | case 0:
196 | this._updating = true;
197 | _a.label = 1;
198 | case 1:
199 | _a.trys.push([1, 3, 4, 5]);
200 | return [4 /*yield*/, this.process(options)];
201 | case 2:
202 | _a.sent();
203 | return [3 /*break*/, 5];
204 | case 3:
205 | e_1 = _a.sent();
206 | this.onError(e_1);
207 | this.dispatch('onError', e_1);
208 | return [3 /*break*/, 5];
209 | case 4:
210 | this._updating = false;
211 | return [7 /*endfinally*/];
212 | case 5: return [2 /*return*/];
213 | }
214 | });
215 | });
216 | };
217 | /**
218 | * Called as soon as the `active` switched to `true`.
219 | *
220 | *
221 | */
222 | System.prototype.onActivated = function () {
223 | /* NOOP */
224 | };
225 | /**
226 | * Called as soon as the `active` switched to `false`.
227 | *
228 | *
229 | */
230 | System.prototype.onDeactivated = function () {
231 | /* NOOP */
232 | };
233 | /**
234 | * Called as soon as the system got removed from an engine.
235 | *
236 | * @param engine The engine this system got added to.
237 | *
238 | *
239 | */
240 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
241 | System.prototype.onRemovedFromEngine = function (engine) {
242 | /* NOOP */
243 | };
244 | /**
245 | * Called as soon as the system got added to an engine.
246 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}.
247 | *
248 | * @param engine The engine this system got added to.
249 | *
250 | *
251 | */
252 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
253 | System.prototype.onAddedToEngine = function (engine) {
254 | /* NOOP */
255 | };
256 | /**
257 | * Called as soon an error occurred during update.
258 | *
259 | * @param error The error which occurred.
260 | *
261 | *
262 | */
263 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
264 | System.prototype.onError = function (error) {
265 | /* NOOP */
266 | };
267 | return System;
268 | }(dispatcher_1.Dispatcher));
269 | exports.System = System;
270 | /**
271 | * An abstract entity system is a system which processes each entity.
272 | *
273 | * Optionally it accepts component types for auto filtering the entities before processing.
274 | * This class abstracts away the initialization of aspects and detaches them properly, if needed.
275 | *
276 | */
277 | var AbstractEntitySystem = /** @class */ (function (_super) {
278 | __extends(AbstractEntitySystem, _super);
279 | /**
280 | * Creates an instance of AbstractEntitySystem.
281 | *
282 | * @param priority The priority of this system. The lower the value the earlier it will process.
283 | * @param all Optional component types which should all match.
284 | * @param exclude Optional component types which should not match.
285 | * @param one Optional component types of which at least one should match.
286 | */
287 | function AbstractEntitySystem(priority, all, exclude, one) {
288 | if (priority === void 0) { priority = 0; }
289 | var _this = _super.call(this, priority) || this;
290 | _this.priority = priority;
291 | _this.all = all;
292 | _this.exclude = exclude;
293 | _this.one = one;
294 | /**
295 | * The optional aspect, if any.
296 | *
297 | */
298 | _this.aspect = null;
299 | return _this;
300 | }
301 | /** @inheritdoc */
302 | AbstractEntitySystem.prototype.onAddedToEngine = function (engine) {
303 | this.aspect = aspect_1.Aspect.for(engine, this.all, this.exclude, this.one);
304 | this.aspect.addListener(this);
305 | };
306 | /** @inheritdoc */
307 | AbstractEntitySystem.prototype.onRemovedFromEngine = function () {
308 | if (!this.aspect)
309 | return;
310 | this.aspect.removeListener(this);
311 | this.aspect.detach();
312 | };
313 | /**
314 | * Called if new entities got added to the system.
315 | *
316 | * @param entities
317 | *
318 | */
319 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
320 | AbstractEntitySystem.prototype.onAddedEntities = function () {
321 | var entities = [];
322 | for (var _i = 0; _i < arguments.length; _i++) {
323 | entities[_i] = arguments[_i];
324 | }
325 | };
326 | /**
327 | * Called if existing entities got removed from the system.
328 | *
329 | * @param entities
330 | *
331 | */
332 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
333 | AbstractEntitySystem.prototype.onRemovedEntities = function () {
334 | var entities = [];
335 | for (var _i = 0; _i < arguments.length; _i++) {
336 | entities[_i] = arguments[_i];
337 | }
338 | };
339 | /**
340 | * Called if the entities got cleared.
341 | *
342 | *
343 | */
344 | AbstractEntitySystem.prototype.onClearedEntities = function () { };
345 | /**
346 | * Called if the entities got sorted.
347 | *
348 | *
349 | */
350 | AbstractEntitySystem.prototype.onSortedEntities = function () { };
351 | /**
352 | * Gets called if new components got added to the given entity.
353 | *
354 | * @param entity
355 | * @param components
356 | *
357 | */
358 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
359 | AbstractEntitySystem.prototype.onAddedComponents = function (entity) {
360 | var components = [];
361 | for (var _i = 1; _i < arguments.length; _i++) {
362 | components[_i - 1] = arguments[_i];
363 | }
364 | };
365 | /**
366 | * Gets called if components got removed from the given entity.
367 | *
368 | * @param entity
369 | * @param components
370 | *
371 | */
372 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
373 | AbstractEntitySystem.prototype.onRemovedComponents = function (entity) {
374 | var components = [];
375 | for (var _i = 1; _i < arguments.length; _i++) {
376 | components[_i - 1] = arguments[_i];
377 | }
378 | };
379 | /**
380 | * Gets called if the components of the given entity got cleared.
381 | *
382 | * @param entity
383 | *
384 | */
385 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
386 | AbstractEntitySystem.prototype.onClearedComponents = function (entity) { };
387 | /**
388 | * Gets called if the components of the given entity got sorted.
389 | *
390 | * @param entity
391 | *
392 | */
393 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
394 | AbstractEntitySystem.prototype.onSortedComponents = function (entity) { };
395 | /** @inheritdoc */
396 | AbstractEntitySystem.prototype.process = function (options) {
397 | var _a;
398 | var entities = this.aspect ? this.aspect.entities : (_a = this._engine) === null || _a === void 0 ? void 0 : _a.entities.elements;
399 | if (!(entities === null || entities === void 0 ? void 0 : entities.length))
400 | return;
401 | for (var i = 0, l = entities.length; i < l; i++) {
402 | this.processEntity(entities[i], i, entities, options);
403 | }
404 | };
405 | return AbstractEntitySystem;
406 | }(System));
407 | exports.AbstractEntitySystem = AbstractEntitySystem;
408 |
--------------------------------------------------------------------------------
/build/core/types.d.ts:
--------------------------------------------------------------------------------
1 | import { Component } from './component';
2 | /**
3 | * A type for the arguments of `F`.
4 | */
5 | export declare type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never;
6 | /**
7 | * Class definition for type `T`.
8 | */
9 | export interface Class extends Function {
10 | new (...args: never[]): T;
11 | }
12 | /**
13 | * Class definition for a component type `T`.
14 | */
15 | export interface ComponentClass extends Class {
16 | /**
17 | * The static id of the component.
18 | *
19 | */
20 | readonly id?: string;
21 | /**
22 | * The static type of the component.
23 | *
24 | */
25 | readonly type?: string;
26 | }
27 |
--------------------------------------------------------------------------------
/build/core/types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 |
--------------------------------------------------------------------------------
/build/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './core/collection';
2 | export * from './core/component';
3 | export * from './core/dispatcher';
4 | export * from './core/engine';
5 | export * from './core/entity';
6 | export * from './core/entity.collection';
7 | export * from './core/aspect';
8 | export * from './core/system';
9 | export * from './core/types';
10 |
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5 | }) : (function(o, m, k, k2) {
6 | if (k2 === undefined) k2 = k;
7 | o[k2] = m[k];
8 | }));
9 | var __exportStar = (this && this.__exportStar) || function(m, exports) {
10 | for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p);
11 | };
12 | Object.defineProperty(exports, "__esModule", { value: true });
13 | __exportStar(require("./core/collection"), exports);
14 | __exportStar(require("./core/component"), exports);
15 | __exportStar(require("./core/dispatcher"), exports);
16 | __exportStar(require("./core/engine"), exports);
17 | __exportStar(require("./core/entity"), exports);
18 | __exportStar(require("./core/entity.collection"), exports);
19 | __exportStar(require("./core/aspect"), exports);
20 | __exportStar(require("./core/system"), exports);
21 | __exportStar(require("./core/types"), exports);
22 |
--------------------------------------------------------------------------------
/examples/rectangles/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build/main.js
3 |
--------------------------------------------------------------------------------
/examples/rectangles/README.md:
--------------------------------------------------------------------------------
1 | # ECTS - Bouncing Rectangles
2 |
3 | A minimal example, built with pixi.js.
4 |
5 | The example shows how to implement the following systems:
6 |
7 | - Rendering (with pixi.js)
8 | - Movement
9 | - Gravity
10 | - Collision
11 | - Window Resizing
12 |
13 | # Run
14 |
15 | Install all dependencies and run the dev script:
16 |
17 | ```
18 | npm install
19 | npm run dev
20 | ```
21 |
22 | This will start a server on [http://localhost:8000](http://localhost:8000/).
23 |
--------------------------------------------------------------------------------
/examples/rectangles/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
25 | ECS - PIXI
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/rectangles/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@trixt0r/ecs-pixi",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "esbuild ./src/index.ts --bundle --outfile=build/main.js --servedir=build"
7 | },
8 | "devDependencies": {
9 | "@trixt0r/ecs": "^0.5.1",
10 | "esbuild": "^0.14.8",
11 | "pixi.js": "^6.2.1"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/rectangles/src/components/position.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@trixt0r/ecs';
2 |
3 | export class Position implements Component {
4 | constructor(public x = 0, public y = 0) {}
5 | }
6 |
--------------------------------------------------------------------------------
/examples/rectangles/src/components/size.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@trixt0r/ecs';
2 |
3 | export class Size implements Component {
4 | constructor(public width = 10, public height = 10) {}
5 | }
6 |
--------------------------------------------------------------------------------
/examples/rectangles/src/components/velocity.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@trixt0r/ecs';
2 |
3 | export class Velocity implements Component {
4 | constructor(public x = 0, public y = 0) {}
5 | }
6 |
--------------------------------------------------------------------------------
/examples/rectangles/src/entity.ts:
--------------------------------------------------------------------------------
1 | import { AbstractEntity } from '@trixt0r/ecs';
2 |
3 | let id = 1;
4 |
5 | export class MyEntity extends AbstractEntity {
6 | constructor() {
7 | super(id++);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/rectangles/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Engine } from '@trixt0r/ecs';
2 | import { RenderingSystem } from './systems/renderer';
3 | import { MovementSystem } from './systems/movement';
4 | import { CollisionSystem } from './systems/collision';
5 | import { GravitySystem } from './systems/gravity';
6 | import { MyEntity } from './entity';
7 | import { Position } from './components/position';
8 | import { Velocity } from './components/velocity';
9 | import { Size } from './components/size';
10 | import { ResizeSystem } from './systems/resize';
11 |
12 | // Setup the engine
13 | const engine = new Engine();
14 |
15 | // Add all systems
16 | engine.systems.add(new MovementSystem(0));
17 | engine.systems.add(new ResizeSystem(0));
18 | engine.systems.add(new GravitySystem(0.25, 1));
19 | engine.systems.add(new CollisionSystem(2));
20 | engine.systems.add(new RenderingSystem(3));
21 |
22 | const canvas = document.getElementById('canvas');
23 | const canvasWidth = canvas.clientWidth;
24 | const canvasHeight = canvas.clientHeight;
25 |
26 | /**
27 | * Adds new entities to the engine, with a position, size and velocity.
28 | *
29 | * @param {number} times Indicates how many entities to add.
30 | */
31 | function addEntity(times: number) {
32 | const toAdd = [];
33 | for (let i = 0; i < times; i++) {
34 | const entity = new MyEntity();
35 | const size = new Size(5 + Math.random() * 10, 5 + Math.random() * 10);
36 | const position = new Position(Math.random() * canvasWidth, Math.random() * canvasHeight);
37 |
38 | entity.components.add(position);
39 | entity.components.add(new Velocity(-1 + Math.random() * 2, -1 + Math.random() * 2));
40 | entity.components.add(size);
41 | toAdd.push(entity);
42 | }
43 | engine.entities.add(...toAdd);
44 | }
45 |
46 | /**
47 | * Resolves after the given time.
48 | */
49 | async function wait(ms: number): Promise {
50 | return new Promise(resolve => setTimeout(resolve, ms));
51 | }
52 |
53 | let t = Date.now();
54 |
55 | function run() {
56 | requestAnimationFrame(() => {
57 | const now = Date.now();
58 | const delta = now - t;
59 | t = now;
60 | engine.run(delta);
61 | run();
62 | });
63 | }
64 |
65 | run();
66 |
67 | async function setUp() {
68 | for (let i = 0; i < 5; i++) {
69 | addEntity(200);
70 | await wait(10);
71 | }
72 | }
73 |
74 | setUp();
75 |
--------------------------------------------------------------------------------
/examples/rectangles/src/systems/collision.ts:
--------------------------------------------------------------------------------
1 | import { Engine, AbstractEntitySystem } from '@trixt0r/ecs';
2 | import { Position } from '../components/position';
3 | import { Velocity } from '../components/velocity';
4 | import { Size } from '../components/size';
5 | import { MyEntity } from 'entity';
6 |
7 | /**
8 | * A system which handles collisions with the bounds of the scene.
9 | *
10 | * @export
11 | * @class CollisionSystem
12 | * @extends {System}
13 | */
14 | export class CollisionSystem extends AbstractEntitySystem {
15 | canvas: HTMLCanvasElement;
16 |
17 | constructor(priority = 0) {
18 | super(priority, [Position, Velocity, Size]);
19 | }
20 |
21 | /**
22 | * Caches the filter and canvas.
23 | *
24 | * @inheritdoc
25 | * @param {Engine} engine
26 | */
27 | onAddedToEngine(engine: Engine) {
28 | super.onAddedToEngine(engine);
29 | this.canvas = document.getElementById('canvas');
30 | }
31 |
32 | /**
33 | * Performs the collision detection,
34 | * i.e. makes sure each entity stays in the scene.
35 | */
36 | processEntity(entity: MyEntity) {
37 | const position = entity.components.get(Position);
38 | const velocity = entity.components.get(Velocity);
39 | const size = entity.components.get(Size);
40 | if (position.x + size.width > this.canvas.width || position.x < 0) velocity.x *= -1;
41 | if (position.y < 0 && velocity.y < 0) velocity.y = 0;
42 | else if (position.y + size.height >= this.canvas.height && velocity.y > 0) velocity.y *= -1;
43 | position.x = Math.min(this.canvas.width - size.width, Math.max(0, position.x));
44 | position.y = Math.min(this.canvas.height - size.height, Math.max(0, position.y));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/rectangles/src/systems/gravity.ts:
--------------------------------------------------------------------------------
1 | import { Engine, AbstractEntitySystem } from '@trixt0r/ecs';
2 | import { Position } from '../components/position';
3 | import { Velocity } from '../components/velocity';
4 | import { Size } from '../components/size';
5 | import { MyEntity } from 'entity';
6 |
7 | /**
8 | * The gravity system is responsible for increasing the vertical velocity
9 | * of each entity if not grounded.
10 | *
11 | * @export
12 | * @class GravitySystem
13 | * @extends {System}
14 | */
15 | export class GravitySystem extends AbstractEntitySystem {
16 | canvas: HTMLCanvasElement;
17 |
18 | constructor(public speed: number, priority?: number) {
19 | super(priority, [Position, Velocity, Size]);
20 | }
21 |
22 | /**
23 | * Caches the filter and canvas.
24 | *
25 | * @inheritdoc
26 | * @param {Engine} engine
27 | */
28 | onAddedToEngine(engine: Engine) {
29 | super.onAddedToEngine(engine);
30 | this.canvas = document.getElementById('canvas');
31 | }
32 |
33 | /**
34 | * Makes each entity moves down, if not grounded.
35 | * The velocity will be increased based on current size component.
36 | */
37 | processEntity(entity: MyEntity) {
38 | const position = entity.components.get(Position);
39 | const velocity = entity.components.get(Velocity);
40 | const size = entity.components.get(Size);
41 | if (position.y + size.height < this.canvas.height) velocity.y += this.speed * (size.width * size.height) * 0.01;
42 | else if (velocity.y !== 0) {
43 | velocity.y *= 0.5;
44 | if (Math.floor(Math.abs(velocity.y)) === 0) {
45 | position.y = this.canvas.height - size.height;
46 | velocity.y = 0;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/rectangles/src/systems/movement.ts:
--------------------------------------------------------------------------------
1 | import { AbstractEntitySystem } from '@trixt0r/ecs';
2 | import { Position } from '../components/position';
3 | import { Velocity } from '../components/velocity';
4 | import { MyEntity } from 'entity';
5 |
6 | /**
7 | * The movement system is responsible to update the position of each entity
8 | * based on its current velocity component.
9 | *
10 | * @export
11 | * @class MovementSystem
12 | * @extends {System}
13 | */
14 | export class MovementSystem extends AbstractEntitySystem {
15 | constructor(priority = 0) {
16 | super(priority, [Position, Velocity]);
17 | }
18 |
19 | /**
20 | * Updates the position.
21 | */
22 | processEntity(entity: MyEntity) {
23 | const position = entity.components.get(Position);
24 | const velocity = entity.components.get(Velocity);
25 | position.x += velocity.x;
26 | position.y += velocity.y;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/rectangles/src/systems/renderer.ts:
--------------------------------------------------------------------------------
1 | import { Application, Graphics, Text } from 'pixi.js';
2 | import { System, Aspect, Engine } from '@trixt0r/ecs';
3 | import { Position } from '../components/position';
4 | import { Size } from '../components/size';
5 |
6 | /**
7 | * Renders each entity, based its position and size component.
8 | *
9 | * @export
10 | * @class RenderingSystem
11 | * @extends {System}
12 | */
13 | export class RenderingSystem extends System {
14 | aspect: Aspect;
15 | pixiApp: Application;
16 | graphics: Graphics;
17 | fps: Text;
18 | timePassed: number;
19 |
20 | constructor(priority = 0) {
21 | super(priority);
22 | this.pixiApp = new Application({
23 | view: document.getElementById('canvas'),
24 | backgroundColor: 0x1155aa,
25 | });
26 | this.timePassed = 350;
27 | this.fps = new Text('FPS: ', { fill: 0xffffff });
28 | this.graphics = new Graphics();
29 | this.pixiApp.stage.addChild(this.graphics);
30 | this.pixiApp.stage.addChild(this.fps);
31 | this.pixiApp.renderer.resize(window.innerWidth, window.innerHeight);
32 | this.pixiApp.stop();
33 | window.addEventListener('resize', () => {
34 | this.pixiApp.renderer.resize(window.innerWidth, window.innerHeight);
35 | });
36 | }
37 |
38 | /**
39 | * Caches the filter.
40 | *
41 | * @inheritdoc
42 | * @param {Engine} engine
43 | */
44 | onAddedToEngine(engine: Engine) {
45 | this.aspect = Aspect.for(engine).all(Position, Size);
46 | }
47 |
48 | /**
49 | * Renders each entity as a red rectangle.
50 | *
51 | * @param {number} delta
52 | */
53 | process(delta: number) {
54 | this.timePassed += delta;
55 | if (this.timePassed > 300) {
56 | this.fps.text =
57 | 'FPS: ' + Math.floor(1000 / delta) + '\n' + 'ms: ' + delta + '\n' + 'entities: ' + this.engine.entities.length;
58 | this.timePassed = 0;
59 | }
60 | const entities = this.aspect.entities;
61 | this.graphics.clear();
62 | this.graphics.beginFill(0xaa1100);
63 | this.graphics.lineStyle(1, 0xffffff);
64 | for (const entity of entities) {
65 | const position = entity.components.get(Position);
66 | const size = entity.components.get(Size);
67 | this.graphics.drawRect(position.x, position.y, size.width, size.height);
68 | }
69 | this.graphics.endFill();
70 | this.pixiApp.render();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/examples/rectangles/src/systems/resize.ts:
--------------------------------------------------------------------------------
1 | import { System, Aspect, Engine } from '@trixt0r/ecs';
2 | import { Velocity } from '../components/velocity';
3 |
4 | /**
5 | * The resize system is responsible for making the rectangles bounce up
6 | * if the user shrinks the window height.
7 | *
8 | * @export
9 | * @class ResizeSystem
10 | * @extends {System}
11 | */
12 | export class ResizeSystem extends System {
13 | aspect: Aspect;
14 | canvas: HTMLCanvasElement;
15 | dirty: boolean;
16 | oldHeight: number;
17 |
18 | constructor(priority?: number) {
19 | super(priority);
20 | this.dirty = false;
21 | this.oldHeight = window.innerHeight;
22 | window.addEventListener('resize', () => {
23 | if (window.innerHeight > this.oldHeight) this.oldHeight = window.innerHeight;
24 | else this.dirty = true;
25 | });
26 | }
27 |
28 | /**
29 | * Caches the filter and canvas.
30 | *
31 | * @inheritdoc
32 | * @param {Engine} engine
33 | */
34 | onAddedToEngine(engine: Engine) {
35 | this.aspect = Aspect.for(engine).one(Velocity);
36 | this.canvas = document.getElementById('canvas');
37 | }
38 |
39 | /**
40 | * Runs the actual bounce logic on each entity,
41 | * if previously resized.
42 | */
43 | process() {
44 | if (!this.dirty) return;
45 | const diff = Math.min(20, this.oldHeight - window.innerHeight);
46 | this.aspect.entities.forEach(entity => {
47 | const velocity = entity.components.get(Velocity);
48 | if (velocity.y === 0) {
49 | velocity.y -= diff;
50 | }
51 | });
52 | this.oldHeight = window.innerHeight;
53 | this.dirty = false;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/rectangles/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "outDir": "./build",
5 | "baseUrl": "./src",
6 | "rootDir": "./src",
7 | "moduleResolution": "node",
8 | "noImplicitAny": true,
9 | "module": "commonjs",
10 | "target": "es6",
11 | "lib": ["dom", "es2015"]
12 | },
13 | "exclude": ["**/*.spec.ts", "**/build/*"]
14 | }
15 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: 'tsconfig.test.json',
7 | },
8 | },
9 | transform: {
10 | '^.+\\.(t|j)sx?$': ['@swc/jest'],
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@trixt0r/ecs",
3 | "version": "0.5.1",
4 | "description": "Entity component system for any JS environment written in TypeScript.",
5 | "main": "build/index.js",
6 | "keywords": [
7 | "ecs",
8 | "entity",
9 | "component",
10 | "system",
11 | "entity component system",
12 | "typescript",
13 | "ts",
14 | "tsc"
15 | ],
16 | "types": "build/index.d.ts",
17 | "scripts": {
18 | "lint": "eslint ./src --ext .js,.ts",
19 | "lint.fix": "eslint ./src --ext .js,.ts --fix",
20 | "prettier": "prettier --write ./src",
21 | "test": "jest -i --coverage",
22 | "build": "tsc && tsc --declaration --emitDeclarationOnly"
23 | },
24 | "author": "",
25 | "license": "ISC",
26 | "devDependencies": {
27 | "@swc/jest": "^0.2.20",
28 | "@types/jest": "^27.0.3",
29 | "@typescript-eslint/eslint-plugin": "^5.7.0",
30 | "@typescript-eslint/parser": "^5.7.0",
31 | "eslint": "^8.5.0",
32 | "eslint-config-prettier": "^8.3.0",
33 | "jest": "^27.5.1",
34 | "prettier": "2.5.1",
35 | "ts-jest": "^27.1.2",
36 | "typescript": "^3.9.10"
37 | },
38 | "homepage": "https://github.com/Trixt0r/ecsts"
39 | }
40 |
--------------------------------------------------------------------------------
/src/core/collection.spec.ts:
--------------------------------------------------------------------------------
1 | import { Collection, CollectionListener } from './collection';
2 | import { Dispatcher } from './dispatcher';
3 |
4 | class MyType {}
5 | class MySortableType extends MyType {
6 | constructor(public position: number) {
7 | super();
8 | }
9 | }
10 |
11 | describe('Collection', () => {
12 | let collection: Collection;
13 | beforeEach(() => (collection = new Collection()));
14 |
15 | describe('initial', () => {
16 | it('should be a dispatcher', () => {
17 | expect(collection instanceof Dispatcher).toBe(true);
18 | });
19 |
20 | it('should have no elements', () => {
21 | expect(collection.length).toBe(0);
22 | });
23 |
24 | it('should accept an initial list of elements', () => {
25 | const source = [new MyType(), new MyType(), new MyType()];
26 | const c = new Collection(source);
27 | expect(c.length).toBe(source.length);
28 | c.forEach((el, i) => expect(el).toBe(source[i]));
29 | });
30 | });
31 |
32 | describe('elements', () => {
33 | it('should be frozen initially', () => {
34 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow();
35 | });
36 |
37 | it('should be frozen after an element got added', () => {
38 | collection.add(new MyType());
39 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow();
40 | });
41 |
42 | it('should be frozen after an element got removed', () => {
43 | const comp = new MyType();
44 | collection.add(comp);
45 | collection.remove(comp);
46 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow();
47 | });
48 |
49 | it('should be frozen after the collection got cleared', () => {
50 | const comp = new MyType();
51 | collection.add(comp);
52 | collection.clear();
53 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow();
54 | });
55 |
56 | it('should be frozen after the collection got sorted', () => {
57 | const comp = new MyType();
58 | collection.add(comp);
59 | collection.sort();
60 | expect(() => (collection.elements as unknown[]).push(new MyType())).toThrow();
61 | });
62 | });
63 |
64 | describe('add', () => {
65 | let element: MyType;
66 |
67 | beforeEach(() => (element = new MyType()));
68 |
69 | it('should add an element', () => {
70 | const re = collection.add(element);
71 | expect(re).toBe(true);
72 | expect(collection.length).toBe(1);
73 | expect(collection.elements).toContain(element);
74 | expect(collection.elements[0]).toBe(element);
75 | });
76 |
77 | it('should add multiple elements', () => {
78 | const o1 = new MyType();
79 | const o2 = new MyType();
80 | const re = collection.add(element, o1, o2);
81 | expect(re).toBe(true);
82 | expect(collection.length).toBe(3);
83 | expect(collection.elements).toContain(element);
84 | expect(collection.elements).toContain(o1);
85 | expect(collection.elements).toContain(o2);
86 | expect(collection.elements[0]).toBe(element);
87 | expect(collection.elements[1]).toBe(o1);
88 | expect(collection.elements[2]).toBe(o2);
89 | });
90 |
91 | it('should not add the same element twice (2 calls)', () => {
92 | collection.add(element);
93 | expect(collection.length).toBe(1);
94 | expect(collection.elements).toContain(element);
95 |
96 | const re = collection.add(element);
97 | expect(re).toBe(false);
98 | expect(collection.length).toBe(1);
99 | });
100 |
101 | it('should not add the same element multiple times (1 call)', () => {
102 | collection.add(element, element, element, element);
103 | expect(collection.length).toBe(1);
104 | expect(collection.elements).toContain(element);
105 | });
106 |
107 | it('should notify all listeners that an element got added', () => {
108 | let added: MyType = null;
109 | const listener: CollectionListener = {
110 | onAdded: (element: MyType) => (added = element),
111 | };
112 |
113 | collection.addListener(listener);
114 | collection.add(element);
115 | expect(added).toBe(element);
116 | });
117 |
118 | it('should notify all listeners that elements got added', () => {
119 | let added: MyType[] = [];
120 | const listener: CollectionListener = {
121 | onAdded: (...elements: MyType[]) => (added = elements),
122 | };
123 |
124 | collection.addListener(listener);
125 | const o1 = new MyType();
126 | const o2 = new MyType();
127 | collection.add(element, o1, o2);
128 | expect(added.length).toBe(3);
129 | const objs = [element, o1, o2];
130 | objs.forEach((obj, i) => expect(obj).toBe(added[i]));
131 | });
132 |
133 | it('should not notify any listener that an element has been removed', () => {
134 | let removed: MyType = null;
135 | const listener: CollectionListener = {
136 | onRemoved: (element: MyType) => (removed = element),
137 | };
138 |
139 | collection.addListener(listener);
140 | collection.add(element);
141 | expect(removed).toBe(null);
142 | });
143 | });
144 |
145 | describe('remove', () => {
146 | let element: MyType;
147 |
148 | beforeEach(() => {
149 | element = new MyType();
150 | collection.add(element);
151 | });
152 |
153 | it('should remove a previously added element', () => {
154 | const re = collection.remove(element);
155 | expect(re).toBe(true);
156 | expect(collection.length).toBe(0);
157 | expect(collection.elements).not.toContain(element);
158 | });
159 |
160 | it('should remove multiple previously added elements', () => {
161 | const o1 = new MyType();
162 | const o2 = new MyType();
163 | collection.add(o1, o2);
164 | const re = collection.remove(element, o1, o2);
165 | expect(re).toBe(true);
166 | expect(collection.length).toBe(0);
167 | expect(collection.elements).not.toContain(element);
168 | expect(collection.elements).not.toContain(o1);
169 | expect(collection.elements).not.toContain(o2);
170 | });
171 |
172 | it('should remove an element at the specified index (0)', () => {
173 | const re = collection.remove(0);
174 | expect(re).toBe(true);
175 | expect(collection.length).toBe(0);
176 | expect(collection.elements).not.toContain(element);
177 | });
178 |
179 | it('should remove elements at the specified indices (0, 1, 2)', () => {
180 | const o1 = new MyType();
181 | const o2 = new MyType();
182 | collection.add(o1, o2);
183 | const re = collection.remove(0, 1, 2);
184 | expect(re).toBe(true);
185 | expect(collection.length).toBe(0);
186 | expect(collection.elements).not.toContain(element);
187 | expect(collection.elements).not.toContain(o1);
188 | expect(collection.elements).not.toContain(o2);
189 | });
190 |
191 | it('should not remove an element which is not part of the collection', () => {
192 | const re = collection.remove(new MyType());
193 | expect(re).toBe(false);
194 | expect(collection.length).toBe(1);
195 | });
196 |
197 | it('should not remove elements which are not part of the collection', () => {
198 | const re = collection.remove(new MyType(), new MyType(), new MyType());
199 | expect(re).toBe(false);
200 | expect(collection.length).toBe(1);
201 | });
202 |
203 | it('should not remove an element at an out of bounds index', () => {
204 | const re = collection.remove(collection.length);
205 | expect(re).toBe(false);
206 | });
207 |
208 | it('should not remove multiple elements at out of bounds indices', () => {
209 | const re = collection.remove(-1, collection.length, collection.length + 1);
210 | expect(re).toBe(false);
211 | });
212 |
213 | it('should notify all listeners that an element got removed', () => {
214 | let removed: MyType = null;
215 | const listener: CollectionListener = {
216 | onRemoved: (element: MyType) => (removed = element),
217 | };
218 |
219 | collection.addListener(listener);
220 | collection.remove(element);
221 | expect(removed).toBe(element);
222 | });
223 |
224 | it('should notify all listeners that multiple elements got removed', () => {
225 | const o1 = new MyType();
226 | const o2 = new MyType();
227 | collection.add(o1, o2);
228 | let removed: MyType[] = [];
229 | const listener: CollectionListener = {
230 | onRemoved: (...elements: MyType[]) => (removed = elements),
231 | };
232 |
233 | collection.addListener(listener);
234 | collection.remove(element, o1, o2);
235 | expect(removed.length).toBe(3);
236 | const objs = [element, o1, o2];
237 | objs.forEach((obj, i) => expect(obj).toBe(removed[i]));
238 | });
239 |
240 | it('should not notify any listener that a element has been added', () => {
241 | let added: MyType = null;
242 | const listener: CollectionListener = {
243 | onAdded: (element: MyType) => (added = element),
244 | };
245 |
246 | collection.addListener(listener);
247 | collection.remove(element);
248 | expect(added).toBe(null);
249 | });
250 | });
251 |
252 | describe('clear', () => {
253 | let listener;
254 | beforeEach(() => {
255 | collection.add(new MyType(), new MyType(), new MyType());
256 | listener = { onCleared: jest.fn() };
257 | collection.addListener(listener);
258 | });
259 |
260 | it('should remove all elements from the collection', () => {
261 | collection.clear();
262 | expect(collection.length).toBe(0);
263 | });
264 |
265 | it('should notify all listeners that the collection has been cleared', () => {
266 | collection.add(new MyType());
267 | collection.clear();
268 | expect(listener.onCleared).toHaveBeenCalled();
269 | });
270 |
271 | it('should not notify any listener that the collection has been cleared if there were no elements', () => {
272 | collection.remove(0, 1, 2);
273 | collection.clear();
274 | expect(listener.onCleared).not.toHaveBeenCalled();
275 | });
276 | });
277 |
278 | describe('sort', () => {
279 | let listener;
280 | beforeEach(() => {
281 | collection.add(new MySortableType(3), new MySortableType(2), new MySortableType(1));
282 | listener = { onSorted: jest.fn() };
283 | collection.addListener(listener);
284 | });
285 |
286 | it('should sort the collection', () => {
287 | collection.sort((a, b) => (a as MySortableType).position - (b as MySortableType).position);
288 |
289 | const elements = collection.elements;
290 | expect(elements[0].position).toBe(1);
291 | expect(elements[1].position).toBe(2);
292 | expect(elements[2].position).toBe(3);
293 | });
294 |
295 | it('should notify all listeners that the collection has been sorted', () => {
296 | collection.add(new MySortableType(1));
297 | collection.sort();
298 | expect(listener.onSorted).toHaveBeenCalled();
299 | });
300 |
301 | it('should not notify any listener that the collection has been sorted if there were no elements', () => {
302 | collection.remove(0, 1, 2);
303 | collection.sort();
304 | expect(listener.onSorted).not.toHaveBeenCalled();
305 | });
306 | });
307 |
308 | describe('iterable', () => {
309 | const amount = 10;
310 | beforeEach(() => {
311 | for (let i = 0; i < amount; i++) collection.add(new MyType());
312 | });
313 |
314 | it('should be iterable', () => {
315 | expect(typeof collection[Symbol.iterator] === 'function').toBe(true);
316 | });
317 |
318 | it('should iterate over all elements in the collection', () => {
319 | let iterations = 0;
320 | for (const element of collection) {
321 | expect(element).toBe(collection.elements[iterations]);
322 | iterations++;
323 | }
324 | expect(iterations).toBe(collection.length);
325 | });
326 |
327 | it('should iterate over all elements in the collection multiple times', () => {
328 | let iterations = 0;
329 | const times = 5;
330 | for (let i = 0; i < 5; i++) {
331 | let iteration = 0;
332 | for (const element of collection) {
333 | expect(element).toBe(collection.elements[iteration++]);
334 | iterations++;
335 | }
336 | }
337 | expect(iterations).toBe(collection.length * times);
338 | });
339 |
340 | it('should iterate over all remaining elements in the collection', () => {
341 | let iterations = 0;
342 | collection.remove(amount - 1, amount - 2, amount - 3, amount - 4, amount - 5);
343 | for (const element of collection) {
344 | expect(element).toBe(collection.elements[iterations]);
345 | iterations++;
346 | }
347 | expect(iterations).toBe(collection.length);
348 | });
349 | });
350 |
351 | describe('array-like', () => {
352 | const amount = 10;
353 | beforeEach(() => {
354 | for (let i = 0; i < amount; i++) collection.add(new MyType());
355 | });
356 |
357 | it.each([
358 | { name: 'filter', args: [(_: MyType, index: number) => index === 1, {}] },
359 | { name: 'forEach', args: [(_: MyType, index: number) => index === 1, {}] },
360 | { name: 'find', args: [(_: MyType, index: number) => index === 1, {}] },
361 | { name: 'findIndex', args: [(_: MyType, index: number) => index === 1, {}] },
362 | { name: 'indexOf', args: [0] },
363 | { name: 'map', args: [(_: MyType, index: number) => index, {}] },
364 | { name: 'every', args: [(_: MyType, index: number) => index === 1, {}] },
365 | { name: 'some', args: [(_: MyType, index: number) => index === 1, {}] },
366 | { name: 'reduce', args: [(prev, _elmnt, idx) => prev + idx, 0] },
367 | { name: 'reduceRight', args: [(prev, _elmnt, idx) => prev + idx, 0] },
368 | ])('should execute $name with $args', ({ name, args }) => {
369 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
370 | const spy = jest.spyOn((collection as any)._elements, name);
371 |
372 | collection[name](...args);
373 |
374 | expect(spy).toHaveBeenCalledWith(...args);
375 | });
376 | });
377 | });
378 |
--------------------------------------------------------------------------------
/src/core/collection.ts:
--------------------------------------------------------------------------------
1 | import { Dispatcher } from './dispatcher';
2 |
3 | /**
4 | * The listener interface for a listener on an entity.
5 | */
6 | export interface CollectionListener {
7 | /**
8 | * Called as soon as new elements have been added to the collection.
9 | *
10 | * @param elements
11 | */
12 | onAdded?(...elements: T[]): void;
13 |
14 | /**
15 | * Called as soon as elements got removed from the collection.
16 | *
17 | * @param elements
18 | */
19 | onRemoved?(...elements: T[]): void;
20 |
21 | /**
22 | * Called as soon as all elements got removed from the collection.
23 | */
24 | onCleared?(): void;
25 |
26 | /**
27 | * Called as soon as the elements got sorted.
28 | */
29 | onSorted?(): void;
30 | }
31 |
32 | /**
33 | * A collection holds a list of elements of a certain type
34 | * and allows to add, remove, sort and clear the list.
35 | * On each operation the internal list of elements gets frozen,
36 | * so a user of the collection will not be able to operate on the real reference,
37 | * but read the data without the need of copying the data on each read access.
38 | */
39 | export class Collection extends Dispatcher> implements IterableIterator {
40 | /**
41 | * The internal list of elements.
42 | */
43 | protected _elements: T[];
44 |
45 | /**
46 | * The frozen list of elements which is used to expose the element list to the public.
47 | */
48 | protected _frozenElements: T[] = [];
49 |
50 | /**
51 | * The internal iterator pointer.
52 | */
53 | protected pointer = 0;
54 |
55 | /**
56 | * Creates an instance of Collection.
57 | *
58 | * @param initial An optional initial list of elements.
59 | */
60 | constructor(initial: T[] = []) {
61 | super();
62 | this._elements = initial.slice();
63 | this.updatedFrozenObjects();
64 | }
65 |
66 | /**
67 | * @inheritdoc
68 | */
69 | public next(): IteratorResult {
70 | if (this.pointer < this._elements.length) {
71 | return {
72 | done: false,
73 | value: this._elements[this.pointer++],
74 | };
75 | } else {
76 | return {
77 | done: true,
78 | value: null,
79 | };
80 | }
81 | }
82 |
83 | /**
84 | * @inheritdoc
85 | */
86 | [Symbol.iterator](): IterableIterator {
87 | this.pointer = 0;
88 | return this;
89 | }
90 |
91 | /**
92 | * A snapshot of all elements in this collection.
93 | */
94 | get elements(): readonly T[] {
95 | return this._frozenElements;
96 | }
97 |
98 | /**
99 | * The length, of this collection, i.e. how many elements this collection contains.
100 | */
101 | get length(): number {
102 | return this._frozenElements.length;
103 | }
104 |
105 | /**
106 | * Updates the internal frozen element list.
107 | */
108 | protected updatedFrozenObjects(): void {
109 | this._frozenElements = this._elements.slice();
110 | Object.freeze(this._frozenElements);
111 | }
112 |
113 | /**
114 | * Adds the given element to this collection.
115 | *
116 | * @param element
117 | * @return Whether the element has been added or not.
118 | * It may not be added, if already present in the element list.
119 | */
120 | protected addSingle(element: T): boolean {
121 | if (this._elements.indexOf(element) >= 0) return false;
122 | this._elements.push(element);
123 | return true;
124 | }
125 |
126 | /**
127 | * Adds the given element(s) to this collection.
128 | *
129 | * @param elements
130 | * @return Whether elements have been added or not.
131 | * They may not have been added, if they were already present in the element list.
132 | */
133 | add(...elements: T[]): boolean {
134 | const added: T[] = elements.filter(element => this.addSingle(element));
135 | if (added.length <= 0) return false;
136 | this.updatedFrozenObjects();
137 | this.dispatch('onAdded', ...added);
138 | return true;
139 | }
140 |
141 | /**
142 | * Removes the given element or the element at the given index.
143 | *
144 | * @param elOrIndex
145 | * @return Whether the element has been removed or not.
146 | * It may not have been removed, if it was not in the element list.
147 | */
148 | protected removeSingle(elOrIndex: T | number): boolean {
149 | if (typeof elOrIndex === 'number') elOrIndex = this.elements[elOrIndex];
150 | const idx = this._elements.findIndex(_ => _ === elOrIndex);
151 | if (idx < 0 || idx >= this._elements.length) return false;
152 | this._elements.splice(idx, 1);
153 | return true;
154 | }
155 |
156 | /**
157 | * Removes the given element(s) or the elements at the given indices.
158 | *
159 | * @param elementsOrIndices
160 | * @return Whether elements have been removed or not.
161 | * They may not have been removed, if every element was not in the element list.
162 | */
163 | remove(...elementsOrIndices: (T | number)[]): boolean {
164 | const removed = elementsOrIndices.filter(element => this.removeSingle(element));
165 | if (removed.length <= 0) return false;
166 | const removedElements = removed.map(o => (typeof o === 'number' ? this.elements[o] : o));
167 | this.updatedFrozenObjects();
168 | this.dispatch('onRemoved', ...removedElements);
169 | return true;
170 | }
171 |
172 | /**
173 | * Clears this collection, i.e. removes all elements from the internal list.
174 | */
175 | clear(): void {
176 | if (!this._elements.length) return;
177 | this._elements = [];
178 | this.updatedFrozenObjects();
179 | this.dispatch('onCleared');
180 | }
181 |
182 | /**
183 | * Returns the index of the given element.
184 | *
185 | * @param element The element.
186 | * @return The index of the given element or id.
187 | */
188 | indexOf(element: T): number {
189 | return this._elements.indexOf(element);
190 | }
191 |
192 | /**
193 | * Sorts this collection.
194 | *
195 | * @param [compareFn]
196 | *
197 | */
198 | sort(compareFn?: (a: T, b: T) => number): this {
199 | if (!this._elements.length) return this;
200 | this._elements.sort(compareFn);
201 | this.updatedFrozenObjects();
202 | this.dispatch('onSorted');
203 | return this;
204 | }
205 |
206 | /**
207 | * Returns the elements of this collection that meet the condition specified in a callback function.
208 | *
209 | * @param callbackfn
210 | * A function that accepts up to three arguments.
211 | * The filter method calls the `callbackfn` function one time for each element in the collection.
212 | * @param thisArg An object to which the this keyword can refer in the callbackfn function.
213 | * If `thisArg` is omitted, undefined is used as the this value.
214 | * @return An array with elements which met the condition.
215 | */
216 | filter(callbackfn: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T[] {
217 | return this._elements.filter(callbackfn, thisArg);
218 | }
219 |
220 | /**
221 | * Performs the specified action for each element in the collection.
222 | *
223 | * @param callbackFn
224 | * A function that accepts up to three arguments.
225 | * forEach calls the callbackfn function one time for each element in the array.
226 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
227 | * If thisArg is omitted, undefined is used as the this value.
228 | */
229 | forEach(callbackFn: (element: T, index: number, array: readonly T[]) => void, thisArg?: U): void {
230 | this._elements.forEach(callbackFn, thisArg);
231 | }
232 |
233 | /**
234 | * Returns the value of the first element in the collection where predicate is true, and undefined
235 | * otherwise.
236 | *
237 | * @param predicate
238 | * Find calls predicate once for each element of the array, in ascending order,
239 | * until it finds one where predicate returns true. If such an element is found, find
240 | * immediately returns that element value. Otherwise, find returns undefined.
241 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
242 | * If thisArg is omitted, undefined is used as the this value.
243 | *
244 | */
245 | find(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): T | undefined {
246 | return this._elements.find(predicate, thisArg);
247 | }
248 |
249 | /**
250 | * Returns the index of the first element in the collection where predicate is true, and -1
251 | * otherwise.
252 | *
253 | * @param predicate
254 | * Find calls predicate once for each element of the array, in ascending order,
255 | * until it finds one where predicate returns true. If such an element is found,
256 | * findIndex immediately returns that element index. Otherwise, findIndex returns -1.
257 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
258 | * If thisArg is omitted, undefined is used as the this value.
259 | *
260 | */
261 | findIndex(predicate: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): number {
262 | return this._elements.findIndex(predicate, thisArg);
263 | }
264 |
265 | /**
266 | * Calls a defined callback function on each element of an collection, and returns an array that contains the results.
267 | *
268 | * @param callbackFn
269 | * A function that accepts up to three arguments.
270 | * The map method calls the callbackfn function one time for each element in the array.
271 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
272 | * If thisArg is omitted, undefined is used as the this value.
273 | *
274 | */
275 | map(callbackFn: (element: T, index: number, array: readonly T[]) => V, thisArg?: U): V[] {
276 | return this._elements.map(callbackFn, thisArg);
277 | }
278 |
279 | /**
280 | *
281 | * Determines whether all the members of the collection satisfy the specified test.
282 | *
283 | * @param callbackFn
284 | * A function that accepts up to three arguments.
285 | * The every method calls the callbackfn function for each element in array1 until the callbackfn
286 | * returns false, or until the end of the array.
287 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
288 | * If thisArg is omitted, undefined is used as the this value.
289 | *
290 | */
291 | every(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean {
292 | return this._elements.every(callbackFn, thisArg);
293 | }
294 |
295 | /**
296 | * Determines whether the specified callback function returns true for any element of the collection.
297 | *
298 | * @param callbackFn
299 | * A function that accepts up to three arguments.
300 | * The some method calls the callbackfn function for each element in the collection until the callbackfn
301 | * returns true, or until the end of the collection.
302 | * @param [thisArg] An object to which the this keyword can refer in the callbackfn function.
303 | * If thisArg is omitted, undefined is used as the this value.
304 | *
305 | */
306 | some(callbackFn: (element: T, index: number, array: readonly T[]) => unknown, thisArg?: U): boolean {
307 | return this._elements.some(callbackFn, thisArg);
308 | }
309 |
310 | /**
311 | * Calls the specified callback function for all the elements in the collection.
312 | * The return value of the callback function is the accumulated result,
313 | * and is provided as an argument in the next call to the callback function.
314 | *
315 | * @param callbackFn
316 | * A function that accepts up to four arguments.
317 | * The reduce method calls the callbackfn function one time for each element in the array.
318 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation.
319 | * The first call to the callbackfn function provides this value as an argument instead of
320 | * an collection value.
321 | *
322 | */
323 | reduce(
324 | callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U,
325 | initialValue: U
326 | ): U {
327 | return this._elements.reduce(callbackFn, initialValue);
328 | }
329 |
330 | /**
331 | * Calls the specified callback function for all the elements in the collection, in descending order.
332 | * The return value of the callback function is the accumulated result,
333 | * and is provided as an argument in the next call to the callback function.
334 | *
335 | * @param callbackFn
336 | * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation.
337 | * The first call to the callbackfn function provides this value as an argument instead of
338 | * an collection value.
339 | *
340 | */
341 | reduceRight(
342 | callbackFn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U,
343 | initialValue: U
344 | ): U {
345 | return this._elements.reduceRight(callbackFn, initialValue);
346 | }
347 | }
348 |
--------------------------------------------------------------------------------
/src/core/component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentCollection, Component } from './component';
2 |
3 | class MyComponent1 implements Component {}
4 |
5 | class MyComponentWithType implements Component {
6 | static type = 'myType';
7 | }
8 |
9 | describe('ComponentCollection', () => {
10 | let collection: ComponentCollection;
11 |
12 | beforeEach(() => (collection = new ComponentCollection()));
13 |
14 | describe('initial', () => {
15 | it('should throw if someone tries to remove the first component listener', () => {
16 | expect(() => collection.removeListener(0)).toThrowError('Listener at index 0 is locked.');
17 | });
18 | });
19 |
20 | describe('get', () => {
21 | it.each([MyComponent1, MyComponentWithType, 'myType'])('should not return any component for %p if empty', _ =>
22 | expect(collection.get(_)).toBeUndefined()
23 | );
24 |
25 | it.each([
26 | { query: MyComponent1, comp: new MyComponent1() },
27 | { query: MyComponentWithType, comp: new MyComponentWithType() },
28 | { query: MyComponentWithType, comp: { type: 'myType' } },
29 | { query: MyComponentWithType.type, comp: new MyComponentWithType() },
30 | { query: 'testType', comp: { type: 'testType' } },
31 | ])('should return $comp for $query', ({ query, comp }) => {
32 | collection.add(comp);
33 | expect(collection.get(query)).toBe(comp);
34 | });
35 |
36 | it.each([
37 | { query: MyComponent1, comp: new MyComponent1() },
38 | { query: MyComponentWithType, comp: new MyComponentWithType() },
39 | { query: MyComponentWithType, comp: { type: 'myType' } },
40 | { query: MyComponentWithType.type, comp: new MyComponentWithType() },
41 | { query: 'testType', comp: { type: 'testType' } },
42 | ])('should return $comp for previously added class, after the first access', ({ query, comp }) => {
43 | collection.get(query);
44 | collection.add(comp);
45 | expect(collection.get(query)).toBe(comp);
46 | });
47 |
48 | it.each([
49 | { count: 10, query: MyComponent1, type: 'class' },
50 | { count: 10, query: MyComponentWithType, type: 'type' },
51 | ])('should return the first component of the previously added $query', ({ count, query }) => {
52 | const comps = [];
53 | for (let i = 0; i < count; i++) {
54 | comps.push(new query());
55 | collection.add(comps[comps.length - 1]);
56 | }
57 | expect(collection.get(query)).toBe(comps[0]);
58 | expect(collection.get(query)).toBe(collection.elements[0]);
59 | });
60 | });
61 |
62 | describe('getAll', () => {
63 | it.each([MyComponent1, MyComponentWithType, 'myType'])('should not return any component for %p if empty', _ => {
64 | expect(collection.getAll(_)).toEqual([]);
65 | });
66 |
67 | it.each([
68 | { query: MyComponent1, comps: [new MyComponent1(), new MyComponent1(), new MyComponent1()] },
69 | { query: MyComponentWithType, comps: [new MyComponentWithType(), { type: 'myType' }] },
70 | { query: MyComponentWithType.type, comps: [new MyComponentWithType(), { type: 'myType' }] },
71 | { query: 'testType', comps: [{ type: 'testType' }, { type: 'testType' }, { type: 'testType' }] },
72 | ])('should return components for $query', ({ query, comps }) => {
73 | collection.add(...comps);
74 | expect(collection.getAll(query)).toEqual(comps);
75 | });
76 |
77 | it.each([
78 | { query: MyComponent1, comps: [new MyComponent1(), new MyComponent1(), new MyComponent1()] },
79 | { query: MyComponentWithType, comps: [new MyComponentWithType(), { type: 'myType' }] },
80 | { query: MyComponentWithType.type, comps: [new MyComponentWithType(), { type: 'myType' }] },
81 | { query: 'testType', comps: [{ type: 'testType' }, { type: 'testType' }, { type: 'testType' }] },
82 | ])('should return components for $query, after the first access', ({ query, comps }) => {
83 | collection.getAll(query);
84 | collection.add(...comps);
85 | expect(collection.getAll(query)).toEqual(comps);
86 | });
87 |
88 | it('should return same type components with different prototypes', () => {
89 | collection.getAll(MyComponentWithType);
90 | collection.getAll(MyComponentWithType.type);
91 |
92 | const comp1 = { type: MyComponentWithType.type };
93 | const comp2 = new MyComponentWithType();
94 | collection.add(comp1, comp2);
95 |
96 | expect(collection.getAll(MyComponentWithType.type)).toEqual([comp1, comp2]);
97 | expect(collection.getAll(MyComponentWithType)).toEqual([comp1, comp2]);
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/core/component.ts:
--------------------------------------------------------------------------------
1 | import { Collection, CollectionListener } from './collection';
2 | import { ComponentClass } from './types';
3 |
4 | /**
5 | * The component interface, every component has to implement.
6 | *
7 | * If you want your system to treat different Components the same way,
8 | * you may define a static string variable named `type` in your components.
9 | */
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | export interface Component extends Record {
12 | /**
13 | * An optional id for the component.
14 | */
15 | id?: string;
16 |
17 | /**
18 | * An optional type for the component.
19 | */
20 | type?: string;
21 | }
22 |
23 | /**
24 | * A collection for components.
25 | * Supports accessing components by their class.
26 | *
27 | */
28 | export class ComponentCollection
29 | extends Collection
30 | implements CollectionListener
31 | {
32 | /**
33 | * Internal map for faster component access, by class or type.
34 | */
35 | protected cache = new Map | string, readonly C[]>();
36 |
37 | /**
38 | * Internal state for updating the components access memory.
39 | *
40 | */
41 | protected dirty = new Map | string, boolean>();
42 |
43 | constructor(initial: C[] = []) {
44 | super(initial);
45 | this.addListener(this, true);
46 | }
47 |
48 | /**
49 | * @inheritdoc
50 | * Update the internal cache.
51 | */
52 | onAdded(...elements: C[]): void {
53 | this.markForCacheUpdate(...elements);
54 | }
55 |
56 | /**
57 | * @inheritdoc
58 | * Update the internal cache.
59 | */
60 | onRemoved(...elements: C[]): void {
61 | this.markForCacheUpdate(...elements);
62 | }
63 |
64 | /**
65 | * @inheritdoc
66 | * Update the internal cache.
67 | */
68 | onCleared() {
69 | this.dirty.clear();
70 | this.cache.clear();
71 | }
72 |
73 | /**
74 | * Searches for the first component matching the given class or type.
75 | *
76 | * @param classOrType The class or type a component has to match.
77 | * @return The found component or `null`.
78 | */
79 | get(classOrType: ComponentClass | string): T {
80 | return this.getAll(classOrType)[0];
81 | }
82 |
83 | /**
84 | * Searches for the all components matching the given class or type.
85 | *
86 | * @param classOrType The class or type components have to match.
87 | * @return A list of all components matching the given class.
88 | */
89 | getAll(classOrType: ComponentClass | string): readonly T[] {
90 | if (this.dirty.get(classOrType)) this.updateCache(classOrType);
91 | if (this.cache.has(classOrType)) return this.cache.get(classOrType) as T[];
92 | this.updateCache(classOrType);
93 | return this.cache.get(classOrType) as T[];
94 | }
95 |
96 | /**
97 | * Updates the cache for the given class or type.
98 | *
99 | * @param classOrType The class or type to update the cache for.
100 | */
101 | protected updateCache(classOrType: ComponentClass | string): void {
102 | const keys = this.cache.keys();
103 | const type = typeof classOrType === 'string' ? classOrType : classOrType.type;
104 | const filtered = this.filter(element => {
105 | const clazz = element.constructor as ComponentClass;
106 | const typeVal = element.type ?? clazz.type;
107 | return type && typeVal ? type === typeVal : clazz === classOrType;
108 | });
109 | if (typeof classOrType !== 'string' && classOrType.type) {
110 | this.cache.set(classOrType.type, filtered);
111 | this.dirty.delete(classOrType.type);
112 | } else if (typeof classOrType === 'string') {
113 | for (const key of keys) {
114 | if (typeof key !== 'string' && key.type === classOrType) {
115 | this.cache.set(key, filtered);
116 | this.dirty.delete(key);
117 | }
118 | }
119 | }
120 | this.cache.set(classOrType, filtered);
121 | this.dirty.delete(classOrType);
122 | }
123 |
124 | /**
125 | * Marks the classes and types of the given elements as dirty,
126 | * so their cache gets updated on the next request.
127 | *
128 | * @param elements
129 | *
130 | */
131 | protected markForCacheUpdate(...elements: C[]): void {
132 | const keys = this.cache.keys();
133 | elements.forEach(element => {
134 | const clazz = element.constructor as ComponentClass;
135 | const classOrType = element.type ?? clazz.type ?? clazz;
136 | if (this.dirty.get(classOrType)) return;
137 | if (typeof classOrType === 'string') {
138 | for (const key of keys) {
139 | if (typeof key !== 'string' && key.type === classOrType) this.dirty.set(key, true);
140 | }
141 | }
142 | this.dirty.set(classOrType, true);
143 | });
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/core/dispatcher.spec.ts:
--------------------------------------------------------------------------------
1 | import { Dispatcher } from './dispatcher';
2 |
3 | type MyListener = Record & { process?(...args: T[]): void };
4 | class MyDispatcher extends Dispatcher {}
5 |
6 | describe('Dispatcher', () => {
7 | let dispatcher: MyDispatcher;
8 |
9 | beforeEach(() => {
10 | dispatcher = new MyDispatcher();
11 | });
12 |
13 | describe('initial', () => {
14 | it('should have no listeners', () => {
15 | expect(dispatcher.listeners.length).toBe(0);
16 | });
17 | });
18 |
19 | describe('listeners', () => {
20 | it('should not care about directly pushed listeners', () => {
21 | (dispatcher.listeners as unknown[]).push({});
22 | expect(dispatcher.listeners.length).toBe(0);
23 | });
24 | });
25 |
26 | describe('addListener', () => {
27 | it('should add a listener to the dispatcher', () => {
28 | const listener: MyListener = {};
29 | const re = dispatcher.addListener(listener);
30 | expect(dispatcher.listeners.length).toBe(1);
31 | expect(re).toBe(true);
32 | });
33 |
34 | it('should not add the same listener twice', () => {
35 | const listener: MyListener = {};
36 | dispatcher.addListener(listener);
37 | const re = dispatcher.addListener(listener);
38 | expect(dispatcher.listeners.length).toBe(1);
39 | expect(re).toBe(false);
40 | });
41 | });
42 |
43 | describe('removeListener', () => {
44 | const listener: MyListener = {};
45 |
46 | beforeEach(() => dispatcher.addListener(listener));
47 |
48 | it('should remove the previously added event listener', () => {
49 | const re = dispatcher.removeListener(listener);
50 | expect(dispatcher.listeners.length).toBe(0);
51 | expect(re).toBe(true);
52 | });
53 |
54 | it('should remove the listener at an index in bounds', () => {
55 | const re = dispatcher.removeListener(0);
56 | expect(dispatcher.listeners.length).toBe(0);
57 | expect(re).toBe(true);
58 | });
59 |
60 | it('should not remove a listener which was not added', () => {
61 | const re = dispatcher.removeListener({});
62 | expect(dispatcher.listeners.length).toBe(1);
63 | expect(re).toBe(false);
64 | });
65 |
66 | it('should not remove anything if the index is out of bounds', () => {
67 | const re = dispatcher.removeListener(1);
68 | expect(dispatcher.listeners.length).toBe(1);
69 | expect(re).toBe(false);
70 | });
71 |
72 | it('should not remove previoulsy removed listener', () => {
73 | dispatcher.removeListener(listener);
74 | const re = dispatcher.removeListener(listener);
75 | expect(dispatcher.listeners.length).toBe(0);
76 | expect(re).toBe(false);
77 | });
78 |
79 | it('should throw if a locked listener gets removed', () => {
80 | const listener = {};
81 | dispatcher.addListener(listener, true);
82 | expect(() => dispatcher.removeListener(listener)).toThrowError('Listener at index 1 is locked.');
83 | });
84 | });
85 |
86 | describe('dispatch', () => {
87 | it('should call the given function name on each listener', () => {
88 | let calls = 0;
89 | const listeners = 10;
90 | for (let i = 0; i < listeners; i++) dispatcher.addListener({ process: () => calls++ });
91 | dispatcher.dispatch('process');
92 | expect(calls).toBe(listeners);
93 | });
94 |
95 | it('should pass all arguments to the listeners', () => {
96 | let args = 0;
97 | const listeners = 10;
98 | for (let i = 0; i < listeners; i++)
99 | dispatcher.addListener({
100 | process: function (number, string, bool, nil, array) {
101 | args += arguments.length;
102 | expect(number).toBe(1);
103 | expect(string).toBe('abc');
104 | expect(bool).toBe(true);
105 | expect(nil).toBe(null);
106 | expect(array).toEqual([]);
107 | },
108 | });
109 | dispatcher.dispatch('process', 1, 'abc', true, null, []);
110 | expect(args).toBe(listeners * 5);
111 | });
112 |
113 | it('should not call the given function name on listeners which have been removed', () => {
114 | let calls = 0;
115 | const listeners = 10;
116 | const toRemove = 5;
117 | for (let i = 0; i < listeners; i++) dispatcher.addListener({ process: () => calls++ });
118 | for (let i = 0; i < toRemove; i++) dispatcher.removeListener(listeners - 1 - i);
119 | dispatcher.dispatch('process');
120 | expect(calls).toBe(listeners - toRemove);
121 | });
122 |
123 | it('should only call the given function name on listeners which implemented the function', () => {
124 | let calls = 0;
125 | const listeners = 5;
126 | const wrongListeners = 5;
127 | for (let i = 0; i < listeners; i++) dispatcher.addListener({ process: () => calls++ });
128 | for (let i = 0; i < wrongListeners; i++) dispatcher.addListener({ processOther: () => calls++ });
129 | dispatcher.dispatch('process');
130 | expect(calls).toBe(listeners);
131 | });
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/src/core/dispatcher.ts:
--------------------------------------------------------------------------------
1 | import { ArgumentTypes } from './types';
2 |
3 | /**
4 | * A dispatcher is an abstract object which holds a list of listeners
5 | * to which data during certain events can be dispatched, by calling functions implemented by listeners.
6 | *
7 | */
8 | export abstract class Dispatcher {
9 | /**
10 | * The list of listeners for this dispatcher.
11 | */
12 | protected _listeners: Partial[];
13 |
14 | /**
15 | * Locked listeners.
16 | * Those listeners in this array can not be removed anymore.
17 | */
18 | protected _lockedListeners: Partial[];
19 |
20 | /**
21 | * Creates an instance of Dispatcher.
22 | */
23 | constructor() {
24 | this._listeners = [];
25 | this._lockedListeners = [];
26 | }
27 |
28 | /**
29 | * The current listeners for this dispatcher.
30 | */
31 | get listeners(): readonly Partial[] {
32 | return this._listeners.slice();
33 | }
34 |
35 | /**
36 | * Adds the given listener to this entity.
37 | *
38 | * @param listener
39 | * @return Whether the listener has been added or not.
40 | * It may not be added, if already present in the listener list.
41 | */
42 | addListener(listener: Partial, lock = false): boolean {
43 | if (this._listeners.indexOf(listener) >= 0) return false;
44 | this._listeners.push(listener);
45 | if (lock) this._lockedListeners.push(listener);
46 | return true;
47 | }
48 |
49 | /**
50 | * Removes the given listener or the listener at the given index.
51 | *
52 | * @param listenerOrIndex
53 | * @return Whether the listener has been removed or not.
54 | * It may not have been removed, if it was not in the listener list.
55 | */
56 | removeListener(listenerOrIndex: Partial | number): boolean {
57 | const idx = typeof listenerOrIndex === 'number' ? listenerOrIndex : this._listeners.indexOf(listenerOrIndex);
58 | if (idx >= 0 && idx < this._listeners.length) {
59 | const listener = this._listeners[idx];
60 | const isLocked = this._lockedListeners.indexOf(listener) >= 0;
61 | if (isLocked) throw new Error(`Listener at index ${idx} is locked.`);
62 | this._listeners.splice(idx, 1);
63 | return true;
64 | }
65 | return false;
66 | }
67 |
68 | /**
69 | * Dispatches the given arguments by calling the given function name
70 | * on each listener, if implemented.
71 | * Note that the listener's scope will be used, when the listener's function gets called.
72 | *
73 | * @param name The function name to call.
74 | * @param args The arguments to pass to the function.
75 | */
76 | dispatch(name: K, ...args: ArgumentTypes): void {
77 | // TODO: optimize this; cache the listeners with the given function name
78 | this._listeners.forEach(listener => {
79 | const fn = listener[name];
80 | if (typeof fn !== 'function') return;
81 | fn.apply(listener, args);
82 | });
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/core/engine.ts:
--------------------------------------------------------------------------------
1 | import { System, SystemListener, SystemMode } from './system';
2 | import { AbstractEntity } from './entity';
3 | import { Dispatcher } from './dispatcher';
4 | import { Collection } from './collection';
5 | import { EntityCollection } from './entity.collection';
6 |
7 | /**
8 | * System which is synced within an engine.
9 | */
10 | type SyncedSystem = System & {
11 | /**
12 | * System listener mapping for engine specific caching purposes.
13 | */
14 | __ecsEngineListener: SystemListener;
15 |
16 | /**
17 | * The list of listeners for this system.
18 | */
19 | _lockedListeners: SystemListener[];
20 | };
21 |
22 | /**
23 | * The listener interface for a listener on an engine.
24 | */
25 | export interface EngineListener {
26 | /**
27 | * Called as soon as the given system gets added to the engine.
28 | *
29 | * @param systems
30 | */
31 | onAddedSystems?(...systems: System[]): void;
32 |
33 | /**
34 | * Called as soon as the given system gets removed from the engine.
35 | *
36 | * @param systems
37 | */
38 | onRemovedSystems?(...systems: System[]): void;
39 |
40 | /**
41 | * Called as soon as all systems got cleared from the engine.
42 | */
43 | onClearedSystems?(): void;
44 |
45 | /**
46 | * Called as soon as an error occurred on in an active system during update.
47 | *
48 | * @param error The error that occurred.
49 | * @param system The system on which the error occurred.
50 | */
51 | onErrorBySystem?(error: Error, system: System): void;
52 |
53 | /**
54 | * Called as soon as the given entity gets added to the engine.
55 | *
56 | * @param entities
57 | */
58 | onAddedEntities?(...entities: T[]): void;
59 |
60 | /**
61 | * Called as soon as the given entity gets removed from the engine.
62 | *
63 | * @param entities
64 | */
65 | onRemovedEntities?(...entities: AbstractEntity[]): void;
66 |
67 | /**
68 | * Called as soon as all entities got cleared from the engine.
69 | */
70 | onClearedEntities?(): void;
71 | }
72 |
73 | /**
74 | * Defines how an engine executes its active systems.
75 | */
76 | export enum EngineMode {
77 | /**
78 | * Execute all systems by priority without waiting for them to resolve.
79 | */
80 | DEFAULT = 'runDefault',
81 |
82 | /**
83 | * Execute all systems by priority. Successive systems
84 | * will wait until the current executing system resolves or rejects.
85 | */
86 | SUCCESSIVE = 'runSuccessive',
87 |
88 | /**
89 | * Start all systems by priority, but run them all in parallel.
90 | */
91 | PARALLEL = 'runParallel',
92 | }
93 |
94 | /**
95 | * An engine puts entities and systems together.
96 | * It holds for each type a collection, which can be queried by each system.
97 | *
98 | * The @see {Engine#update} method has to be called in order to perform updates on each system in a certain order.
99 | * The engine takes care of updating only active systems in any point of time.
100 | *
101 | */
102 | export class Engine extends Dispatcher {
103 | /**
104 | * The internal list of all systems in this engine.
105 | */
106 | protected _systems = new Collection();
107 |
108 | /**
109 | * The frozen list of active systems which is used to iterate during the update.
110 | */
111 | protected _activeSystems: System[] = [];
112 |
113 | /**
114 | * The internal list of all entities in this engine.
115 | */
116 | protected _entities = new EntityCollection();
117 |
118 | /**
119 | * Creates an instance of Engine.
120 | */
121 | constructor() {
122 | super();
123 | this._systems.addListener(
124 | {
125 | onAdded: (...systems: SyncedSystem[]) => {
126 | this._systems.sort((a, b) => a.priority - b.priority);
127 | systems.forEach(system => {
128 | system.engine = this;
129 | this.updatedActiveSystems();
130 |
131 | const systemListener: SystemListener = {
132 | onActivated: () => this.updatedActiveSystems(),
133 | onDeactivated: () => this.updatedActiveSystems(),
134 | onError: error => this.dispatch('onErrorBySystem', error, system),
135 | };
136 | system.__ecsEngineListener = systemListener;
137 | system.addListener(systemListener, true);
138 | });
139 | this.dispatch('onAddedSystems', ...systems);
140 | },
141 | onRemoved: (...systems: SyncedSystem[]) => {
142 | systems.forEach(system => {
143 | system.engine = null;
144 | this.updatedActiveSystems();
145 | const systemListener = system.__ecsEngineListener;
146 | const locked = system._lockedListeners;
147 | locked.splice(locked.indexOf(systemListener), 1);
148 | system.removeListener(systemListener);
149 | });
150 | this.dispatch('onRemovedSystems', ...systems);
151 | },
152 | onCleared: () => this.dispatch('onClearedSystems'),
153 | },
154 | true
155 | );
156 |
157 | this._entities.addListener(
158 | {
159 | onAdded: (...entities: T[]) => this.dispatch('onAddedEntities', ...entities),
160 | onRemoved: (...entities: T[]) => this.dispatch('onRemovedEntities', ...entities),
161 | onCleared: () => this.dispatch('onClearedEntities'),
162 | },
163 | true
164 | );
165 |
166 | this.updatedActiveSystems();
167 | }
168 |
169 | /**
170 | * A snapshot of all entities in this engine.
171 | */
172 | get entities(): EntityCollection {
173 | return this._entities;
174 | }
175 |
176 | /**
177 | * A snapshot of all systems in this engine.
178 | */
179 | get systems(): Collection {
180 | return this._systems;
181 | }
182 |
183 | /**
184 | * A snapshot of all active systems in this engine.
185 | */
186 | get activeSystems(): readonly System[] {
187 | return this._activeSystems;
188 | }
189 |
190 | /**
191 | * Updates the internal active system list.
192 | */
193 | protected updatedActiveSystems(): void {
194 | this._activeSystems = this.systems.filter(system => system.active);
195 | Object.freeze(this._activeSystems);
196 | }
197 |
198 | /**
199 | * Updates all systems in this engine by the given delta value.
200 | *
201 | * @param [options]
202 | * @param [mode = EngineMode.DEFAULT]
203 | */
204 | run(options?: T, mode: EngineMode = EngineMode.DEFAULT): void | Promise {
205 | return this[mode].call(this, options);
206 | }
207 |
208 | /**
209 | * Updates all systems in this engine by the given delta value,
210 | * without waiting for a resolve or reject of each system.
211 | *
212 | * @param [options]
213 | */
214 | protected runDefault(options?: T): void {
215 | const length = this._activeSystems.length;
216 | for (let i = 0; i < length; i++) this._activeSystems[i].run(options, SystemMode.SYNC);
217 | }
218 |
219 | /**
220 | * Updates all systems in this engine by the given delta value,
221 | * by waiting for a system to resolve or reject before continuing with the next one.
222 | *
223 | * @param [options]
224 | */
225 | protected async runSuccessive(options?: T): Promise {
226 | const length = this._activeSystems.length;
227 | for (let i = 0; i < length; i++) await this._activeSystems[i].run(options, SystemMode.SYNC);
228 | }
229 |
230 | /**
231 | * Updates all systems in this engine by the given delta value,
232 | * by running all systems in parallel and waiting for all systems to resolve or reject.
233 | *
234 | * @param [options]
235 | */
236 | protected async runParallel(options?: T): Promise {
237 | const mapped = this._activeSystems.map(system => system.run(options, SystemMode.ASYNC));
238 | await Promise.all(mapped);
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/core/entity.collection.spec.ts:
--------------------------------------------------------------------------------
1 | import { AbstractEntity } from './entity';
2 | import { EntityCollection } from './entity.collection';
3 |
4 | class Entity extends AbstractEntity {}
5 |
6 | describe('EntityCollection', () => {
7 | let collection: EntityCollection;
8 |
9 | beforeEach(() => (collection = new EntityCollection()));
10 |
11 | describe('get', () => {
12 | describe('onAdded', () => {
13 | beforeEach(() => {
14 | collection.add(new Entity(3), new Entity('1'), new Entity(null));
15 | });
16 | it('should return entities for previously added entities', () => {
17 | expect(collection.get(3)).toBeInstanceOf(Entity);
18 | expect(collection.get('1')).toBeInstanceOf(Entity);
19 | expect(collection.get(null)).toBeInstanceOf(Entity);
20 |
21 | expect(collection.get(3).id).toBe(3);
22 | expect(collection.get('1').id).toBe('1');
23 | expect(collection.get(null).id).toBe(null);
24 | });
25 | it('should not return entities which were not added', () => {
26 | expect(collection.get(1)).toBeUndefined();
27 | expect(collection.get('3')).toBeUndefined();
28 | expect(collection.get(undefined)).toBeUndefined();
29 | });
30 | });
31 | describe('onRemoved', () => {
32 | beforeEach(() => {
33 | collection.add(new Entity(3), new Entity('1'), new Entity(null));
34 | collection.remove(0, 2);
35 | });
36 | it('should not return entities for previously removed entities', () => {
37 | expect(collection.get(3)).toBeUndefined();
38 | expect(collection.get(null)).toBeUndefined();
39 | });
40 | it('should return entities which were not removed', () => {
41 | expect(collection.get('1')).toBeDefined();
42 | });
43 | });
44 | describe('onCleared', () => {
45 | beforeEach(() => {
46 | collection.add(new Entity(3), new Entity('1'), new Entity(null));
47 | collection.clear();
48 | });
49 | it('should not return any entities after clear', () => {
50 | expect(collection.get(3)).toBeUndefined();
51 | expect(collection.get('1')).toBeUndefined();
52 | expect(collection.get(null)).toBeUndefined();
53 | });
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/core/entity.collection.ts:
--------------------------------------------------------------------------------
1 | import { Collection, CollectionListener } from './collection';
2 | import { AbstractEntity } from './entity';
3 |
4 | export class EntityCollection
5 | extends Collection
6 | implements CollectionListener
7 | {
8 | /**
9 | * Internal map for faster entity access, by id.
10 | */
11 | protected cache = new Map();
12 |
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | constructor(...args: any[]) {
15 | super(...args);
16 | this.addListener(this, true);
17 | }
18 |
19 | /**
20 | * Returns the entity for the given id in this collection.
21 | *
22 | * @param id The id to search for.
23 | * @return The found entity or `undefined` if not found.
24 | */
25 | get(id: string | number): T | undefined {
26 | return this.cache.get(id);
27 | }
28 |
29 | /**
30 | * @inheritdoc
31 | */
32 | onAdded(...entities: T[]): void {
33 | for (let i = 0, l = entities.length; i < l; i++) this.cache.set(entities[i].id, entities[i]);
34 | }
35 |
36 | /**
37 | * @inheritdoc
38 | */
39 | onRemoved(...entities: T[]): void {
40 | for (let i = 0, l = entities.length; i < l; i++) this.cache.delete(entities[i].id);
41 | }
42 |
43 | /**
44 | * @inheritdoc
45 | */
46 | onCleared(): void {
47 | this.cache.clear();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/core/entity.spec.ts:
--------------------------------------------------------------------------------
1 | import { AbstractEntity } from './entity';
2 | import { Dispatcher } from './dispatcher';
3 | import { ComponentCollection } from './component';
4 |
5 | class MyEntity extends AbstractEntity {}
6 |
7 | describe('Entity', () => {
8 | let myEntity: AbstractEntity;
9 | beforeEach(() => (myEntity = new MyEntity('randomId')));
10 |
11 | describe('initial', () => {
12 | it('should have an id', () => {
13 | expect(myEntity.id).toBe('randomId');
14 | });
15 |
16 | it('should be a dispatcher', () => {
17 | expect(myEntity instanceof Dispatcher).toBe(true);
18 | });
19 |
20 | it('should have a component collection', () => {
21 | expect(myEntity.components instanceof ComponentCollection).toBe(true);
22 | });
23 |
24 | it('should have no components', () => {
25 | expect(myEntity.components.length).toBe(0);
26 | });
27 |
28 | it('should have the entity as the first listener on the components collection', () => {
29 | expect(myEntity.components.listeners.length).toBe(2);
30 | expect(myEntity.components.listeners[1]).toBe(myEntity);
31 | });
32 |
33 | it('should throw if someone tries to remove the entity from the components listeners', () => {
34 | expect(() => myEntity.components.removeListener(myEntity)).toThrowError('Listener at index 1 is locked.');
35 | });
36 | });
37 |
38 | describe('dispatch', () => {
39 | it('should dispatch onAddedComponents if a component got added', () => {
40 | let called = false;
41 | myEntity.addListener({ onAddedComponents: () => (called = true) });
42 | myEntity.components.add({});
43 | expect(called).toBe(true);
44 | });
45 |
46 | it('should dispatch onRemovedComponents if a component got removed', () => {
47 | let called = false;
48 | myEntity.addListener({ onRemovedComponents: () => (called = true) });
49 | myEntity.components.add({});
50 | myEntity.components.remove(0);
51 | expect(called).toBe(true);
52 | });
53 |
54 | it('should dispatch onClearedComponents if the components got cleared', () => {
55 | let called = false;
56 | myEntity.addListener({ onClearedComponents: () => (called = true) });
57 | myEntity.components.add({});
58 | myEntity.components.clear();
59 | expect(called).toBe(true);
60 | });
61 |
62 | it('should dispatch onSortedComponents if the components got sorted', () => {
63 | let called = false;
64 | myEntity.addListener({ onSortedComponents: () => (called = true) });
65 | myEntity.components.add({});
66 | myEntity.components.sort();
67 | expect(called).toBe(true);
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/src/core/entity.ts:
--------------------------------------------------------------------------------
1 | import { Component, ComponentCollection } from './component';
2 | import { Dispatcher } from './dispatcher';
3 | import { CollectionListener } from './collection';
4 |
5 | /**
6 | * The listener interface for a listener on an entity.
7 | */
8 | export interface EntityListener {
9 | /**
10 | * Called as soon as a new component as been added to the entity.
11 | *
12 | * @param components The new added components.
13 | */
14 | onAddedComponents?(...components: C[]): void;
15 |
16 | /**
17 | * Called as soon as a component got removed from the entity.
18 | *
19 | * @param components The removed components
20 | */
21 | onRemovedComponents?(...components: C[]): void;
22 |
23 | /**
24 | * Called as soon as all components got removed from the entity.
25 | */
26 | onClearedComponents?(): void;
27 |
28 | /**
29 | * Called as soon as the components got sorted.
30 | */
31 | onSortedComponents?(): void;
32 | }
33 |
34 | /**
35 | *
36 | * An entity holds an id and a list of components attached to it.
37 | * You can add or remove components from the entity.
38 | */
39 | export abstract class AbstractEntity>
40 | extends Dispatcher
41 | implements CollectionListener
42 | {
43 | /**
44 | * The internal list of components.
45 | */
46 | protected _components: ComponentCollection;
47 |
48 | /**
49 | * Creates an instance of Entity.
50 | *
51 | * @param id The id, you should provide by yourself. Maybe an uuid or a simple number.
52 | */
53 | constructor(public readonly id: number | string) {
54 | super();
55 | this._components = new ComponentCollection();
56 | this._components.addListener(this, true);
57 | }
58 |
59 | /**
60 | * A snapshot of all components of this entity.
61 | */
62 | get components(): ComponentCollection {
63 | return this._components;
64 | }
65 |
66 | /**
67 | * Dispatches the `onAdded` event to all listeners as `onAddedComponents`.
68 | *
69 | * @param components
70 | */
71 | onAdded(...components: C[]): void {
72 | return (>this).dispatch('onAddedComponents', ...components);
73 | }
74 |
75 | /**
76 | * Dispatches the `onRemoved` event to all listeners as `onRemovedComponents`.
77 | *
78 | * @param components
79 | *
80 | */
81 | onRemoved(...components: C[]): void {
82 | return (>this).dispatch('onRemovedComponents', ...components);
83 | }
84 |
85 | /**
86 | * Dispatches the `onCleared` event to all listeners as `onClearedComponents`.
87 | *
88 | *
89 | */
90 | onCleared(): void {
91 | return (>this).dispatch('onClearedComponents');
92 | }
93 |
94 | /**
95 | * Dispatches the `onSorted` event to all listeners as `onSortedComponents`.
96 | *
97 | *
98 | */
99 | onSorted(): void {
100 | return (>this).dispatch('onSortedComponents');
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/core/system.spec.ts:
--------------------------------------------------------------------------------
1 | import { System, SystemMode, AbstractEntitySystem, SystemListener } from './system';
2 | import { Engine } from './engine';
3 | import { Dispatcher } from './dispatcher';
4 | import { AbstractEntity } from './entity';
5 | import { Component } from './component';
6 | import { Aspect } from './aspect';
7 |
8 | class MySyncSystem extends System {
9 | public throw: string;
10 |
11 | process(): void {
12 | if (this.throw) throw new Error(this.throw);
13 | }
14 | }
15 |
16 | class MyAsyncSystem extends System {
17 | public throw: string;
18 | public timer = 10;
19 |
20 | async process(): Promise {
21 | if (this.throw) throw new Error(this.throw);
22 | else return new Promise(resolve => setTimeout(resolve, this.timer));
23 | }
24 | }
25 |
26 | describe('System', () => {
27 | let system: MySyncSystem;
28 | let listener: SystemListener;
29 |
30 | beforeEach(() => {
31 | system = new MySyncSystem();
32 | listener = {
33 | onActivated: jest.fn(),
34 | onDeactivated: jest.fn(),
35 | onAddedToEngine: jest.fn(),
36 | onRemovedFromEngine: jest.fn(),
37 | onError: jest.fn(),
38 | };
39 | system.addListener(listener);
40 | });
41 |
42 | describe('initial', () => {
43 | it('should be a dispatcher', () => {
44 | expect(system instanceof Dispatcher).toBe(true);
45 | });
46 |
47 | it('should be active', () => {
48 | expect(system.active).toBe(true);
49 | });
50 |
51 | it('should not be updating', () => {
52 | expect(system.updating).toBe(false);
53 | });
54 | });
55 |
56 | describe('active', () => {
57 | it('should not notify any listener if the value did not change', () => {
58 | system.active = true;
59 | expect(listener.onActivated).not.toHaveBeenCalled();
60 | expect(listener.onDeactivated).not.toHaveBeenCalled();
61 | });
62 |
63 | it('should call onActivated on each listener if the value changes to "true"', () => {
64 | system.active = false;
65 | system.active = true;
66 | expect(listener.onActivated).toHaveBeenCalled();
67 | expect(listener.onDeactivated).toHaveBeenCalledTimes(1);
68 | });
69 |
70 | it('should call onDeactivated on each listener if the value changes to "false"', () => {
71 | system.active = false;
72 | expect(listener.onActivated).not.toHaveBeenCalled();
73 | expect(listener.onDeactivated).toHaveBeenCalled();
74 | });
75 | });
76 |
77 | describe('engine', () => {
78 | it('should not notify any listener if the value did not change', () => {
79 | const engine = system.engine;
80 | system.engine = engine;
81 | expect(listener.onAddedToEngine).not.toHaveBeenCalled();
82 | expect(listener.onRemovedFromEngine).not.toHaveBeenCalled();
83 | });
84 |
85 | it('should call onAddedToEngine on each listener if the value changes', () => {
86 | system.engine = new Engine();
87 | expect(listener.onAddedToEngine).toHaveBeenCalledWith(system.engine);
88 | expect(listener.onRemovedFromEngine).not.toHaveBeenCalled();
89 | });
90 |
91 | it('should not call onRemovedFromEngine on any listener if there was no engine before assigned"', () => {
92 | system.engine = new Engine();
93 | expect(listener.onRemovedFromEngine).not.toHaveBeenCalled();
94 | });
95 |
96 | it('should not call onAddedToEngine on any listener if there is no new engine assigned"', () => {
97 | const before = new Engine();
98 | system.engine = before;
99 | system.engine = null;
100 | expect(listener.onAddedToEngine).toHaveBeenLastCalledWith(before);
101 | });
102 |
103 | it.each([new Engine(), null])(
104 | 'should call onRemovedFromEngine on each listener if the value changes to %p and an engine was assigned',
105 | newEngine => {
106 | const oldEngine = new Engine();
107 | system.engine = oldEngine;
108 | system.engine = newEngine;
109 | expect(listener.onRemovedFromEngine).toHaveBeenCalledWith(oldEngine);
110 | }
111 | );
112 | });
113 |
114 | describe('run (sync)', () => {
115 | it('should call the process method with the correct delta', () => {
116 | let called = false;
117 | const dlt = 5;
118 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
119 | (system as any).process = function (delta) {
120 | called = delta;
121 | };
122 | system.run(5);
123 | expect(called).toBe(dlt);
124 | });
125 |
126 | it('should notify all listeners if an exception occurred', () => {
127 | system.throw = 'Error system.spec';
128 | let called: Error = null;
129 | system.addListener({ onError: error => (called = error) });
130 | try {
131 | system.run(0);
132 | } finally {
133 | expect(called).not.toBe(null);
134 | expect(called.message).toBe(system.throw);
135 | }
136 | });
137 |
138 | it('should convert the result of the system into a promise, if forced', () => {
139 | expect(system.run(5, SystemMode.ASYNC) instanceof Promise).toBe(true);
140 | });
141 | });
142 |
143 | describe('run (async)', () => {
144 | beforeEach(() => (system = new MyAsyncSystem()));
145 |
146 | it('should turn the system into the updating state', async () => {
147 | const re = system.run(0, SystemMode.ASYNC);
148 | expect(system.updating).toBe(true);
149 | await re;
150 | });
151 |
152 | it('should call the process method with the correct delta', async () => {
153 | let called = false;
154 | const dlt = 5;
155 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
156 | (system).process = function (delta) {
157 | called = delta;
158 | };
159 | const re = system.run(5, SystemMode.ASYNC);
160 | await re;
161 | expect(called).toBe(dlt);
162 | });
163 |
164 | it('should return the system from the updating state', async () => {
165 | await system.run(0, SystemMode.ASYNC);
166 | expect(system.updating).toBe(false);
167 | });
168 |
169 | it('should return the system from the updating state if an exception occurred', async () => {
170 | system.throw = 'Error system.spec';
171 | try {
172 | await system.run(0, SystemMode.ASYNC);
173 | } finally {
174 | expect(system.updating).toBe(false);
175 | }
176 | });
177 |
178 | it('should notify all listeners if an exception occurred', async () => {
179 | system.throw = 'Error system.spec';
180 | let called: Error = null;
181 | system.addListener({ onError: error => (called = error) });
182 | try {
183 | await system.run(0, SystemMode.ASYNC);
184 | } finally {
185 | expect(called).not.toBe(null);
186 | expect(called.message).toBe(system.throw);
187 | }
188 | });
189 | });
190 | });
191 |
192 | class MyEntity extends AbstractEntity {}
193 | class MyComponent1 implements Component {}
194 | class MyComponent2 implements Component {}
195 | class MyComponent3 implements Component {}
196 | class MyComponent4 implements Component {}
197 |
198 | class MyEntitySystem extends AbstractEntitySystem {
199 | entities: MyEntity[] = [];
200 |
201 | processEntity(entity: MyEntity): void {
202 | this.entities.push(entity);
203 | }
204 |
205 | getAspect(): Aspect {
206 | return this.aspect;
207 | }
208 |
209 | setEngine(engine: Engine): void {
210 | this._engine = engine;
211 | }
212 | }
213 |
214 | describe('AbstractEntitySystem', () => {
215 | it('should process each entity in the engine', () => {
216 | const engine = new Engine();
217 | engine.entities.add(new MyEntity('1'), new MyEntity('2'), new MyEntity('3'));
218 | const system = new MyEntitySystem();
219 | engine.systems.add(system);
220 | engine.run();
221 | expect(system.entities.length).toBe(engine.entities.length);
222 | engine.entities.forEach((entity, i) => {
223 | expect(system.entities[i]).toBe(entity);
224 | });
225 | });
226 |
227 | it('should process each entity fitting the provided aspects', () => {
228 | const engine = new Engine();
229 | engine.entities.add(new MyEntity('1'), new MyEntity('2'), new MyEntity('3'));
230 | engine.entities.elements[0].components.add(new MyComponent1(), new MyComponent2(), new MyComponent4());
231 | engine.entities.elements[1].components.add(new MyComponent1(), new MyComponent2(), new MyComponent3());
232 | engine.entities.elements[2].components.add(new MyComponent1(), new MyComponent2());
233 |
234 | const system = new MyEntitySystem(0, [MyComponent1], [MyComponent3], [MyComponent4, MyComponent2]);
235 | engine.systems.add(system);
236 | engine.run();
237 | expect(system.entities.length).toBe(2);
238 | });
239 |
240 | it('should not do anything if onRemovedFromEngine is being called without have been added to an engine', () => {
241 | const system = new MyEntitySystem(0);
242 | expect(() => system.onRemovedFromEngine()).not.toThrow();
243 | });
244 |
245 | it('should detach the aspect after removing the system from the engine', () => {
246 | const engine = new Engine();
247 |
248 | const system = new MyEntitySystem(0, [MyComponent1], [MyComponent3], [MyComponent4, MyComponent2]);
249 | engine.systems.add(system);
250 | expect(system.getAspect().isAttached).toBe(true);
251 | engine.systems.remove(system);
252 | expect(system.getAspect().isAttached).toBe(false);
253 | });
254 |
255 | it.each([
256 | { name: 'onAddedEntities', args: [new MyEntity('1'), new MyEntity('2'), new MyEntity('3')] },
257 | { name: 'onRemovedEntities', args: [new MyEntity('1'), new MyEntity('2'), new MyEntity('3')] },
258 | { name: 'onClearedEntities', args: [] },
259 | { name: 'onSortedEntities', args: [] },
260 | { name: 'onAddedComponents', args: [new MyEntity('1'), [new MyComponent1(), new MyComponent2()]] },
261 | { name: 'onRemovedComponents', args: [new MyEntity('2'), [new MyComponent1(), new MyComponent2()]] },
262 | { name: 'onClearedComponents', args: [new MyEntity('3')] },
263 | { name: 'onSortedComponents', args: [new MyEntity('4')] },
264 | ])('should call $name with $args when dispatching via the aspect', ({ name, args }) => {
265 | const system = new MyEntitySystem();
266 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
267 | const spy = jest.spyOn(system, name as any);
268 | system.engine = new Engine();
269 | const aspect = system.getAspect();
270 |
271 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
272 | aspect.dispatch(name as any, ...args);
273 |
274 | expect(spy).toHaveBeenCalledWith(...args);
275 | });
276 |
277 | it.each(['engine', 'engine-no-aspect', 'aspect'])(
278 | 'should not process entities, if there are no %s entities',
279 | type => {
280 | const system = new MyEntitySystem(0);
281 | if (type === 'aspect') system.engine = new Engine();
282 | else if (type === 'engine-no-aspect') system.setEngine(new Engine());
283 | const spy = jest.spyOn(system, 'processEntity');
284 |
285 | system.process();
286 |
287 | expect(spy).not.toHaveBeenCalled();
288 | }
289 | );
290 | });
291 |
--------------------------------------------------------------------------------
/src/core/system.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import { Engine } from './engine';
3 | import { Dispatcher } from './dispatcher';
4 | import { AbstractEntity } from './entity';
5 | import { ComponentClass } from './types';
6 | import { Component } from './component';
7 | import { Aspect, AspectListener } from './aspect';
8 |
9 | /**
10 | * The listener interface for a listener added to a system.
11 | */
12 | export interface SystemListener {
13 | /**
14 | * Called as soon as the `active` switched to `true`.
15 | */
16 | onActivated?(): void;
17 |
18 | /**
19 | * Called as soon as the `active` switched to `false`.
20 | */
21 | onDeactivated?(): void;
22 |
23 | /**
24 | * Called as soon as the system got removed from an engine.
25 | *
26 | * @param engine The engine this system got removed from.
27 | */
28 | onRemovedFromEngine?(engine: Engine): void;
29 |
30 | /**
31 | * Called as soon as the system got added to an engine.
32 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}.
33 | *
34 | * @param engine The engine this system got added to.
35 | */
36 | onAddedToEngine?(engine: Engine): void;
37 |
38 | /**
39 | * Called as soon an error occurred during update.
40 | *
41 | * @param error The error which occurred.
42 | */
43 | onError?(error: Error): void;
44 | }
45 |
46 | /**
47 | * Defines how a system executes its task.
48 | *
49 | * @enum {number}
50 | */
51 | export enum SystemMode {
52 | /**
53 | * Do work and resolve immediately.
54 | */
55 | SYNC = 'runSync',
56 |
57 | /**
58 | * Do async work. E.g. do work in a worker, make requests to another server, etc.
59 | */
60 | ASYNC = 'runAsync',
61 | }
62 |
63 | /**
64 | * A system processes a list of entities which belong to an engine.
65 | * Entities can only be accessed via the assigned engine. @see {Engine}.
66 | * The implementation of the specific system has to choose on which components of an entity to operate.
67 | *
68 | */
69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
70 | export abstract class System extends Dispatcher {
71 | /**
72 | * Determines whether this system is active or not.
73 | *
74 | */
75 | protected _active: boolean;
76 |
77 | /**
78 | * Determines whether this system is currently updating or not.
79 | *
80 | */
81 | protected _updating: boolean;
82 |
83 | /**
84 | * The reference to the current engine.
85 | *
86 | * @memberof System
87 | */
88 | protected _engine: Engine | null;
89 |
90 | /**
91 | * Creates an instance of System.
92 | *
93 | * @param [priority=0] The priority of this system. The lower the value the earlier it will process.
94 | */
95 | constructor(public priority: number = 0) {
96 | super();
97 | this._active = true;
98 | this._updating = false;
99 | this._engine = null;
100 | }
101 |
102 | /**
103 | * The active state of this system.
104 | * If the flag is set to `false`, this system will not be able to process.
105 | *
106 | */
107 | get active(): boolean {
108 | return this._active;
109 | }
110 |
111 | set active(active: boolean) {
112 | if (active === this._active) return;
113 | this._active = active;
114 | if (active) {
115 | this.onActivated();
116 | } else {
117 | this.onDeactivated();
118 | }
119 | (>this).dispatch(active ? 'onActivated' : 'onDeactivated');
120 | }
121 |
122 | /**
123 | * The engine this system is assigned to.
124 | *
125 | */
126 | get engine(): Engine | null {
127 | return this._engine;
128 | }
129 |
130 | set engine(engine: Engine | null) {
131 | if (engine === this._engine) return;
132 | const oldEngine = this._engine;
133 | this._engine = engine;
134 | if (oldEngine instanceof Engine) {
135 | this.onRemovedFromEngine(oldEngine);
136 | (>this).dispatch('onRemovedFromEngine', oldEngine);
137 | }
138 | if (engine instanceof Engine) {
139 | this.onAddedToEngine(engine);
140 | (>this).dispatch('onAddedToEngine', engine);
141 | }
142 | }
143 |
144 | /**
145 | * Determines whether this system is currently updating or not.
146 | * The value will stay `true` until @see {System#process} resolves or rejects.
147 | *
148 | * @readonly
149 | */
150 | get updating(): boolean {
151 | return this._updating;
152 | }
153 |
154 | /**
155 | * Runs the system process with the given delta time.
156 | *
157 | * @param options
158 | * @param mode The system mode to run in.
159 | *
160 | */
161 | run(options: T, mode: SystemMode = SystemMode.SYNC): void | Promise {
162 | return this[mode].call(this, options);
163 | }
164 |
165 | /**
166 | * Processes data synchronously.
167 | *
168 | * @param options
169 | *
170 | */
171 | protected runSync(options: T): void {
172 | try {
173 | this.process(options);
174 | } catch (e) {
175 | this.onError(e as Error);
176 | (>this).dispatch('onError', e as Error);
177 | }
178 | }
179 |
180 | /**
181 | * Processes data asynchronously.
182 | *
183 | * @param options
184 | *
185 | */
186 | protected async runAsync(options: T): Promise {
187 | this._updating = true;
188 | try {
189 | await this.process(options);
190 | } catch (e) {
191 | this.onError(e as Error);
192 | (>this).dispatch('onError', e as Error);
193 | } finally {
194 | this._updating = false;
195 | }
196 | }
197 |
198 | /**
199 | * Processes the entities of the current engine.
200 | * To be implemented by any concrete system.
201 | *
202 | * @param options
203 | *
204 | */
205 | abstract process(options: T): void | Promise;
206 |
207 | /**
208 | * Called as soon as the `active` switched to `true`.
209 | *
210 | *
211 | */
212 | onActivated(): void {
213 | /* NOOP */
214 | }
215 |
216 | /**
217 | * Called as soon as the `active` switched to `false`.
218 | *
219 | *
220 | */
221 | onDeactivated(): void {
222 | /* NOOP */
223 | }
224 |
225 | /**
226 | * Called as soon as the system got removed from an engine.
227 | *
228 | * @param engine The engine this system got added to.
229 | *
230 | *
231 | */
232 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
233 | onRemovedFromEngine(engine: Engine): void {
234 | /* NOOP */
235 | }
236 |
237 | /**
238 | * Called as soon as the system got added to an engine.
239 | * Note that this will be called after @see {SystemListener#onRemovedFromEngine}.
240 | *
241 | * @param engine The engine this system got added to.
242 | *
243 | *
244 | */
245 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
246 | onAddedToEngine(engine: Engine): void {
247 | /* NOOP */
248 | }
249 |
250 | /**
251 | * Called as soon an error occurred during update.
252 | *
253 | * @param error The error which occurred.
254 | *
255 | *
256 | */
257 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
258 | onError(error: Error): void {
259 | /* NOOP */
260 | }
261 | }
262 |
263 | type CompType = ComponentClass | Component;
264 |
265 | /**
266 | * An abstract entity system is a system which processes each entity.
267 | *
268 | * Optionally it accepts component types for auto filtering the entities before processing.
269 | * This class abstracts away the initialization of aspects and detaches them properly, if needed.
270 | *
271 | */
272 | export abstract class AbstractEntitySystem
273 | extends System
274 | implements AspectListener
275 | {
276 | /**
277 | * The optional aspect, if any.
278 | *
279 | */
280 | protected aspect: Aspect | null = null;
281 |
282 | /**
283 | * Creates an instance of AbstractEntitySystem.
284 | *
285 | * @param priority The priority of this system. The lower the value the earlier it will process.
286 | * @param all Optional component types which should all match.
287 | * @param exclude Optional component types which should not match.
288 | * @param one Optional component types of which at least one should match.
289 | */
290 | constructor(
291 | public priority = 0,
292 | protected all?: CompType[],
293 | protected exclude?: CompType[],
294 | protected one?: CompType[]
295 | ) {
296 | super(priority);
297 | }
298 |
299 | /** @inheritdoc */
300 | onAddedToEngine(engine: Engine): void {
301 | this.aspect = Aspect.for(engine, this.all, this.exclude, this.one);
302 | this.aspect.addListener(this);
303 | }
304 |
305 | /** @inheritdoc */
306 | onRemovedFromEngine(): void {
307 | if (!this.aspect) return;
308 | this.aspect.removeListener(this);
309 | this.aspect.detach();
310 | }
311 |
312 | /**
313 | * Called if new entities got added to the system.
314 | *
315 | * @param entities
316 | *
317 | */
318 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
319 | onAddedEntities(...entities: AbstractEntity[]): void {}
320 |
321 | /**
322 | * Called if existing entities got removed from the system.
323 | *
324 | * @param entities
325 | *
326 | */
327 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
328 | onRemovedEntities?(...entities: AbstractEntity[]): void {}
329 |
330 | /**
331 | * Called if the entities got cleared.
332 | *
333 | *
334 | */
335 | onClearedEntities?(): void {}
336 |
337 | /**
338 | * Called if the entities got sorted.
339 | *
340 | *
341 | */
342 | onSortedEntities?(): void {}
343 |
344 | /**
345 | * Gets called if new components got added to the given entity.
346 | *
347 | * @param entity
348 | * @param components
349 | *
350 | */
351 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
352 | onAddedComponents?(entity: AbstractEntity, ...components: Component[]): void {}
353 |
354 | /**
355 | * Gets called if components got removed from the given entity.
356 | *
357 | * @param entity
358 | * @param components
359 | *
360 | */
361 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
362 | onRemovedComponents?(entity: AbstractEntity, ...components: Component[]): void {}
363 |
364 | /**
365 | * Gets called if the components of the given entity got cleared.
366 | *
367 | * @param entity
368 | *
369 | */
370 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
371 | onClearedComponents?(entity: AbstractEntity): void {}
372 |
373 | /**
374 | * Gets called if the components of the given entity got sorted.
375 | *
376 | * @param entity
377 | *
378 | */
379 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
380 | onSortedComponents?(entity: AbstractEntity): void {}
381 |
382 | /** @inheritdoc */
383 | process(options?: U): void {
384 | const entities = this.aspect ? this.aspect.entities : this._engine?.entities.elements;
385 | if (!entities?.length) return;
386 | for (let i = 0, l = entities.length; i < l; i++) {
387 | this.processEntity(entities[i], i, entities, options);
388 | }
389 | }
390 |
391 | /**
392 | * Processes the given entity.
393 | *
394 | * @param entity
395 | * @param index
396 | * @param entities
397 | * @param options
398 | *
399 | */
400 | abstract processEntity(entity: T, index?: number, entities?: T[], options?: U): void;
401 | }
402 |
--------------------------------------------------------------------------------
/src/core/types.ts:
--------------------------------------------------------------------------------
1 | import { Component } from './component';
2 |
3 | /**
4 | * A type for the arguments of `F`.
5 | */
6 | export type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never;
7 |
8 | /**
9 | * Class definition for type `T`.
10 | */
11 | export interface Class extends Function {
12 | new (...args: never[]): T;
13 | }
14 |
15 | /**
16 | * Class definition for a component type `T`.
17 | */
18 | export interface ComponentClass extends Class {
19 | /**
20 | * The static id of the component.
21 | *
22 | */
23 | readonly id?: string;
24 |
25 | /**
26 | * The static type of the component.
27 | *
28 | */
29 | readonly type?: string;
30 | }
31 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './core/collection';
2 | export * from './core/component';
3 | export * from './core/dispatcher';
4 | export * from './core/engine';
5 | export * from './core/entity';
6 | export * from './core/entity.collection';
7 | export * from './core/aspect';
8 | export * from './core/system';
9 | export * from './core/types';
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "target": "es5",
5 | "outDir": "./build",
6 | "baseUrl": "./src",
7 | "rootDir": "./src",
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "alwaysStrict": true,
11 | "lib": ["ES2015"],
12 | "downlevelIteration": true
13 | },
14 | "exclude": ["**/*.spec.ts", "**/build/*", "**/examples/*"]
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "esModuleInterop": true,
5 | "target": "es6",
6 | "outDir": "./build",
7 | "baseUrl": "./src",
8 | "rootDir": "./src",
9 | "skipLibCheck": true
10 | },
11 | "exclude": ["**/*.spec.ts", "**/build/*", "**/examples/*"]
12 | }
13 |
--------------------------------------------------------------------------------