├── .babelrc ├── .eslintrc ├── .gitignore ├── MIT-LICENSE.txt ├── README.md ├── bower.json ├── changelog.md ├── docs ├── api.md ├── index.md └── requirements.txt ├── entity-manager.js ├── entity-manager.js.map ├── examples └── concentration │ ├── img │ ├── images.json │ └── images.png │ ├── index.html │ └── js │ ├── lib │ └── pixi.js │ └── main.js ├── lib └── require.js ├── mkdocs.yml ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── entity-manager.js └── test ├── SpecRunner.js ├── assemblages └── soldier.js ├── bench ├── bench-entity-manager.js ├── bench.js └── index.html ├── components ├── position.js └── unit.js ├── index.html └── test_entity-manager.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | rules: 2 | indent: 3 | - "error" 4 | - 4 5 | quotes: 6 | - "warn" 7 | - single 8 | linebreak-style: 9 | - "error" 10 | - unix 11 | semi: 12 | - "error" 13 | - always 14 | comma-dangle: 15 | - "warn" 16 | - always-multiline 17 | no-extra-semi: 18 | - "error" 19 | env: 20 | browser: true 21 | es6: true 22 | parserOptions: 23 | sourceType: "module" 24 | extends: 'eslint:recommended' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Adrian Gaudebert, http://adrian.gaudebert.fr/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ensy: Entity System for JavaScript 2 | 3 | ``ensy`` is a JavaScript implementation of the Entity System model as described by Adam Martin 4 | in his blog post series [Entity Systems are the future of MMOs](http://t-machine.org/index.php/2009/10/26/entity-systems-are-the-future-of-mmos-part-5/). 5 | 6 | > Component/Entity Systems are an architectural pattern used mostly in game development. A CES follows the Composition over Inheritance principle to allow for greater flexibility when defining entities (anything that's part of a game's scene: enemies, doors, bullets) by building out of individual parts that can be mixed-and-matched. This eliminates the ambiguity problems of long inheritance chains and promotes clean design. However, CES systems do incur a small cost to performance. 7 | 8 | — From the [Entity Systems Wiki](http://entity-systems.wikidot.com/) 9 | 10 | ## Installation 11 | 12 | This module is available on [``npm``](http://npmjs.com/package/ensy) as ``ensy``. It has no dependencies. 13 | 14 | ``npm install --save ensy`` 15 | 16 | ## Usage 17 | 18 | ```javascript 19 | import EntityManager from 'ensy'; 20 | 21 | let manager = new EntityManager(); 22 | 23 | // Create a component and add it to the manager. 24 | const PlayerComponent = { 25 | name: 'Player', 26 | description: "The player's state", 27 | state: { 28 | life: 100, 29 | strength: 18, 30 | charisma: 3, 31 | } 32 | }; 33 | manager.addComponent(PlayerComponent.name, PlayerComponent); 34 | 35 | // Create a new entity. 36 | const playerId = manager.createEntity(['Player']); 37 | 38 | // Update the player's state: 39 | let playerData = manager.getComponentDataForEntity('Player', playerId); 40 | playerData.life = 80; 41 | 42 | // Which is equivalent to: 43 | manager.updateComponentDataForEntity('Player', playerId, { 44 | life: 80, 45 | }); 46 | 47 | // Which can also be done when creating the entity: 48 | const playerId = manager.createEntity(['Player'], null, { 49 | Player: { 50 | life: 80, 51 | }, 52 | }); 53 | 54 | console.log(playerData); 55 | // { life: 80, strength: 18, charisma: 3 } 56 | } 57 | ``` 58 | 59 | ## Documentation 60 | 61 | The documentation is available on [Read the docs](https://entity-system-js.readthedocs.io/). All methods are well documented and parameters are described. 62 | 63 | For an overall explanation of ``ensy``, you can read my blog post [ensy - Entity System Reloaded](http://adrian.gaudebert.fr/blog/post/2015/07/04/ensy-mdash-entity-system-reloaded). 64 | 65 | ## Examples 66 | 67 | There are examples in the [examples](https://github.com/adngdb/entity-system-js/tree/master/examples) directory: 68 | 69 | * [Concentration](https://github.com/adngdb/entity-system-js/tree/master/examples/concentration) 70 | * [Total Madness Arena](https://github.com/adngdb/nth) (Jam game, made in 3 days) 71 | * Made something with ``ensy``? Add it here! 72 | 73 | ## For developers 74 | 75 | Install the dependencies with ``npm install``. The source files are in ``src/``. 76 | The code uses es6 features, and is compiled to es5 using ``babel`` and ``rollup``. 77 | 78 | ### Building the code 79 | 80 | ```bash 81 | $ npm run build 82 | ``` 83 | 84 | We use [rollup](http://rollupjs.org/) and [babel](http://babeljs.io/) to 85 | compile the code from es6 to es5, and [uglify](http://lisperator.net/uglifyjs/) 86 | to minify the source code. 87 | 88 | ### Running tests 89 | 90 | ```bash 91 | $ npm test 92 | ``` 93 | 94 | To have tests watch your files and re-run when they change: 95 | 96 | ```bash 97 | $ npm run test-w 98 | ``` 99 | 100 | To run the tests in your browser: 101 | 102 | ```bash 103 | $ npm run test-browser 104 | ``` 105 | 106 | ### Building the API documentation 107 | 108 | ```bash 109 | $ npm run build_doc 110 | ``` 111 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ensy", 3 | "version": "1.5.0", 4 | "description": "Entity System for JavaScript", 5 | "homepage": "https://github.com/adngdb/entity-system-js", 6 | "authors": [ 7 | "Adrian Gaudebert " 8 | ], 9 | "license": "MIT", 10 | 11 | "main": "entity-manager.js", 12 | "moduleType": [ 13 | "amd", 14 | "node" 15 | ], 16 | "keywords": [ 17 | "game", 18 | "entity", 19 | "ecs", 20 | "gamedev", 21 | "component" 22 | ], 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components", 27 | "test", 28 | "tests" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Current 4 | 5 | ## 1.5.2 (June 16, 2020) 6 | 7 | - Send events to processors ([#37](https://github.com/adngdb/entity-system-js/issues/37)) 8 | - Add support for initial state when creating entities ([#38](https://github.com/adngdb/entity-system-js/issues/38)) 9 | 10 | ## 1.5.1 (May 16, 2020) 11 | 12 | - Add `addProcessors` and `addAssemblages` methods to add lists of elements ([#32](https://github.com/adngdb/entity-system-js/issues/32)). 13 | - Add an `addComponents` method to add a list of components at once ([#31](https://github.com/adngdb/entity-system-js/issues/31)). 14 | - Fix event emitting when removing an entity. 15 | - Upgrade some dependencies. 16 | 17 | ## 1.5.0 (June 21, 2019) 18 | 19 | - Revert `getComponentsData` to return an object (entity => component) ([#29](https://github.com/adngdb/entity-system-js/issues/29)). **[BREAKING]** 20 | 21 | ## 1.4.0 (August 1, 2018) 22 | 23 | - Made `getComponentsData` return an actual array (as documented) instead of an object. **[BREAKING]** 24 | 25 | ## 1.3.0 (May 22, 2017) 26 | 27 | - Fixed a race condition when signaling that a component has been added to an entity. 28 | - Rewrote some loops to use `forEach`, and switched `var` to `let` or `const`. 29 | - Fixed a scope issue when adding multiple components at once. 30 | - Made component properties enumerable (when the EntityManager is instantiated with a listener). 31 | - Emit more events: 'entityCreated', 'entityComponentAdded' and 'entityComponentRemoved'. 32 | - createEntity now accepts an ID, to make synchronization of distant systems possible. 33 | - Upgraded libraries rollup and amdefine to their latest versions. 34 | - Use rollup to compile code ([#7](https://github.com/adngdb/entity-system-js/issues/7)). 35 | 36 | ## 1.2.0 (Jun 26, 2016) 37 | 38 | - Emit an event: 'entityComponentUpdated'. 39 | - Allow a listener to be passed to the constructor of EntityManager. 40 | 41 | ## 1.1.1 (Apr 10, 2016) 42 | 43 | - Fixed version number (hence the version bump). 44 | 45 | ## 1.1.0 (Apr 10, 2016) 46 | 47 | - Do not add getters and setters when there's no emit method ([#16](https://github.com/adngdb/entity-system-js/issues/16)). 48 | - Added a benchmark script ([#20](https://github.com/adngdb/entity-system-js/pull/20)). 49 | 50 | ## 1.0.0 (Jul 3, 2015) 51 | 52 | - Initial public release. 53 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## EntityManager 6 | Implement the Entity System model and provide tools to easily 7 | create and manipulate Entities, Components and Processors. 8 | 9 | ## getUid() 10 | 11 | Return an identifier unique to this system. 12 | 13 | ### Return: 14 | 15 | * **int** - Unique identifier. 16 | 17 | ## createEntity(componentIds, entityId, initialState) 18 | 19 | Create a new entity in the system by creating a new instance of each of its components. 20 | 21 | ### Params: 22 | 23 | * **array** *componentIds* - List of identifiers of the components that compose the new entity. 24 | * **int** *entityId* - Optional. Unique identifier of the entity. If passed, no new id will be generated. 25 | * **object** *initialState* - Optional. Object containing the initial state to apply. 26 | 27 | ### Return: 28 | 29 | * **int** - Unique identifier of the new entity. 30 | 31 | ## removeEntity(id) 32 | 33 | Remove an entity and its instanciated components from the system. 34 | 35 | ### Params: 36 | 37 | * **int** *id* - Unique identifier of the entity. 38 | 39 | ### Return: 40 | 41 | * **object** - this 42 | 43 | ## addComponent(id, component) 44 | 45 | Add a component to the list of known components. 46 | 47 | ### Params: 48 | 49 | * **string** *id* - Unique identifier of the component. 50 | * **object** *component* - Object containing the metadata and data of the component. 51 | 52 | ### Return: 53 | 54 | * **object** - this 55 | 56 | ## addComponents(components) 57 | 58 | Add a list of components to known components. 59 | 60 | ### Params: 61 | 62 | * **Array** *components* - Array of objects containing the metadata and data of components. Requires that each object has `name` used to identify 63 | it, and `data` to describe it. 64 | 65 | ### Return: 66 | 67 | * **object** - this 68 | 69 | ## removeComponent(id) 70 | 71 | Remove a component from the list of known components. 72 | 73 | ### Params: 74 | 75 | * **string** *id* - Unique identifier of the component. 76 | 77 | ### Return: 78 | 79 | * **object** - this 80 | 81 | ## getComponentsList() 82 | 83 | Get the list of components this instance knows. 84 | 85 | ### Return: 86 | 87 | * **array** - List of names of components. 88 | 89 | ## addComponentsToEntity(componentIds, entityId, initialState) 90 | 91 | Create a new instance of each listed component and associate them with the entity. 92 | 93 | ### Params: 94 | 95 | * **array** *componentIds* - List of identifiers of the components to add to the entity. 96 | * **int** *entityId* - Unique identifier of the entity. 97 | * **object** *initialState* - Optional. Object containing the initial state to apply. 98 | 99 | ### Return: 100 | 101 | * **object** - this 102 | 103 | ## removeComponentsFromEntity(componentIds, entityId) 104 | 105 | De-associate a list of components from the entity. 106 | 107 | ### Params: 108 | 109 | * **array** *componentIds* - List of identifiers of the components to remove from the entity. 110 | * **int** *entityId* - Unique identifier of the entity. 111 | 112 | ### Return: 113 | 114 | * **object** - this 115 | 116 | ## getComponentDataForEntity(entityId, componentId) 117 | 118 | Return a reference to an object that contains the data of an 119 | instanciated component of an entity. 120 | 121 | ### Params: 122 | 123 | * **int** *entityId* - Unique identifier of the entity. 124 | * **string** *componentId* - Unique identifier of the component. 125 | 126 | ### Return: 127 | 128 | * **object** - Component data of one entity. 129 | 130 | ## updateComponentDataForEntity(entityId, componentId, newState, sendUpdateEvent) 131 | 132 | Update the state of a component, many keys at once. 133 | 134 | ### Params: 135 | 136 | * **int** *entityId* - Unique identifier of the entity. 137 | * **string** *componentId* - Unique identifier of the component. 138 | * **object** *newState* - Object containing the new state to apply. 139 | * **boolean** *sendUpdateEvent* - Optional. True if the method has to send the `COMPONENT_UPDATED` event. 140 | 141 | ### Return: 142 | 143 | * **object** - this 144 | 145 | ## getComponentsData(componentId) 146 | 147 | Return a dictionary of objects containing the data of all instances of a 148 | given component. 149 | 150 | ### Params: 151 | 152 | * **string** *componentId* - Unique identifier of the component. 153 | 154 | ### Return: 155 | 156 | * **Object** - Dictionary of entity id -> component data. 157 | 158 | ## entityHasComponent(entityId, componentId) 159 | 160 | Return true if the entity has the component. 161 | 162 | ### Params: 163 | 164 | * **int** *entityId* - Unique identifier of the entity. 165 | * **string** *componentId* - Unique identifier of the component. 166 | 167 | ### Return: 168 | 169 | * **boolean** - True if the entity has the component. 170 | 171 | ## addAssemblage(id, assemblage) 172 | 173 | Add an assemblage to the list of known assemblages. 174 | 175 | ### Params: 176 | 177 | * **string** *id* - Unique identifier of the assemblage. 178 | * **object** *assemblage* - An instance of an assemblage to add. 179 | 180 | ### Return: 181 | 182 | * **object** - this 183 | 184 | ## addAssemblages(assemblages) 185 | 186 | Add a list of assemblages to known assemblages. 187 | 188 | ### Params: 189 | 190 | * **Array** *assemblages* - An array of assemblages to add. Require that each object has a `name` property to use as identifier. 191 | 192 | ### Return: 193 | 194 | * **object** - this 195 | 196 | ## removeAssemblage(id) 197 | 198 | Remove an assemblage from the list of known assemblages. 199 | 200 | ### Params: 201 | 202 | * **string** *id* - Unique identifier of the assemblage. 203 | 204 | ### Return: 205 | 206 | * **object** - this 207 | 208 | ## createEntityFromAssemblage(assemblageId) 209 | 210 | Create a new entity in the system by creating a new instance of each of 211 | its components and setting their initial state, using an assemblage. 212 | 213 | ### Params: 214 | 215 | * **string** *assemblageId* - Id of the assemblage to create the entity from. 216 | 217 | ### Return: 218 | 219 | * **int** - Unique identifier of the new entity. 220 | 221 | ## addProcessor(processor) 222 | 223 | Add a processor to the list of known processors. 224 | 225 | ### Params: 226 | 227 | * **object** *processor* - An instance of a processor to manage. 228 | 229 | ### Return: 230 | 231 | * **object** - this 232 | 233 | ## addProcessors(processors) 234 | 235 | Add a list of processors to known processors. 236 | 237 | ### Params: 238 | 239 | * **Array** *processors* - An array of processors to manage. 240 | 241 | ### Return: 242 | 243 | * **object** - this 244 | 245 | ## removeProcessor(processor) 246 | 247 | Remove a processor from the list of known processors. 248 | 249 | ### Params: 250 | 251 | * **object** *processor* - An instance of a processor to remove. 252 | 253 | ### Return: 254 | 255 | * **object** - this 256 | 257 | ## sendEventToProcessors(eventName, entityId, componentId) 258 | 259 | Send an event to the list of known processors. 260 | 261 | ### Params: 262 | 263 | * **string** *eventName* - Id of the event to send. 264 | * **number** *entityId* - Unique identifier of the entity on which the event occured. 265 | * **string** *componentId* - Unique identifier of the component on which the event occured. 266 | 267 | ### Return: 268 | 269 | * **object** - this 270 | 271 | ## update(dt) 272 | 273 | Update all the known processors. 274 | 275 | ### Params: 276 | 277 | * **int** *dt* - The time delta since the last call to update. Will be passed as an argument to all processor's `update` method. 278 | 279 | ### Return: 280 | 281 | * **object** - this 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Entity System for JavaScript 2 | 3 | This is a JavaScript implementation of the Entity System model as described by Adam Martin in his blog post series [Entity Systems are the future of MMOs](http://t-machine.org/index.php/2009/10/26/entity-systems-are-the-future-of-mmos-part-5/). 4 | 5 | You can find the [API documentation](api/) here. A more comprehensive documentation is coming, along with a tutorial. [Soon](http://i2.kym-cdn.com/photos/images/facebook/000/117/021/enhanced-buzz-28895-1301694293-0.jpg). 6 | 7 | For examples of how to use this library, you can take a look at the [examples folder](https://github.com/adngdb/entity-system-js/tree/master/examples) in the code repository. We also have a bigger scale example called [Total Madness Arena](https://github.com/adngdb/nth), a game made during a [Game Dev Party Jam](http://gamedevparty.fr). 8 | 9 | # General concepts of an Entity System 10 | 11 | There are 3 main concepts in the Entity System for JavaScript library, and others that gravitate around them in the Entity System model. 12 | 13 | ## Entities 14 | 15 | Concretely, an Entity is a simple identifier, in our case an integer (though it might become a UUID in the future). Entities do nothing by themselves, but they are associated with a list of components. There is no "Entity object", no Entity data, no Entity state: just a collection of instances of components, and those components are the objects, they contain the data and they have the state. 16 | 17 | ```javascript 18 | let entity = manager.createEntity(['MyComponent']); 19 | console.log(entity); 20 | // > 1 21 | ``` 22 | 23 | 24 | ## Components 25 | 26 | A Component is a collection of states about an aspect of the game. It has no logic, just data. In this library, Components are objects containing a sub-object called ``state``, and that ``state`` has a list of attributes that all need to be serializable. Strings, numbers and arrays are the most common types there. Functions should never be in a component's ``state``, and objects should not be there either, because if you feel the need to add an object that probably means you actually want to create a different component. 27 | 28 | Every time a component is added to an entity, a copy of the ``state`` is created and attached to that entity. You can then freely change all the values of that component's attributes without impacting other entities. 29 | 30 | ```javascript 31 | const positionComp = { 32 | name: 'Position', 33 | state: { 34 | x: 0, 35 | y: 0 36 | } 37 | }; 38 | manager.addComponent(positionComp.name, positionComp); 39 | 40 | let aPosition = manager.createEntity(['Position']); 41 | let aPositionData = manager.getComponentDataForEntity(aPosition, 'Position'); 42 | console.log(aPositionData.x); 43 | // > 0 44 | ``` 45 | 46 | Note: the ``state`` key is the only mandatory key in the root of a component. All others keys are metadata that won't be used by the Entity System. They are here for your convenience. For example, I try to always have a ``name`` in my components, and use that when declaring them, as shown in the example above. A ``description`` field could also be a good idea, especially when you start having a lot of components. 47 | 48 | 49 | ## Processors 50 | 51 | Components have no logic, so it has to live somewhere else. That's processors. Processors are at the core a simple ``update`` function that is called every frame of your game. 52 | 53 | ```javascript 54 | const MyProcessor = function (manager) { 55 | this.manager = manager; 56 | }; 57 | 58 | MyProcessor.prototype.update = function (dt) { 59 | let entities = this.manager.getComponentsData('MyComponent'); 60 | 61 | // Do something on these entities… 62 | }; 63 | 64 | manager.addProcessor(new MyProcessor(manager)); 65 | 66 | while (true) { 67 | manager.update(); 68 | } 69 | ``` 70 | 71 | 72 | ## Assemblages 73 | 74 | In a game, you often find yourself creating entities with the same components over and over. Assemblages are here to make that easier. They consist in a list of components, and some initial data to apply to those components. In your game, you can then easily create an entity from an assemblage, and automatically get the right components and the right default data. 75 | 76 | ```javascript 77 | const MyAssemblage = { 78 | name: 'SomeUnit', 79 | components: ['Position', 'Sprite', 'Attack'], 80 | initialState: { 81 | Positon: { 82 | x: 1, 83 | y: 2 84 | }, 85 | Sprite: { 86 | source: 'assets/some-unit.png' 87 | } 88 | } 89 | }; 90 | 91 | manager.addAssemblage(MyAssemblage.name, MyAssemblage); 92 | let entity = manager.createEntityFromAssemblage('SomeUnit'); 93 | ``` 94 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.0.4 \ 2 | --hash=sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939 \ 3 | --hash=sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a 4 | tornado==5.1.1 \ 5 | --hash=sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d \ 6 | --hash=sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409 \ 7 | --hash=sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f \ 8 | --hash=sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f \ 9 | --hash=sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5 \ 10 | --hash=sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb \ 11 | --hash=sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444 12 | Markdown==3.1.1 \ 13 | --hash=sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a \ 14 | --hash=sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c 15 | livereload==2.6.1 \ 16 | --hash=sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b \ 17 | --hash=sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66 18 | backports_abc==0.5 \ 19 | --hash=sha256:033be54514a03e255df75c5aee8f9e672f663f93abb723444caec8fe43437bde \ 20 | --hash=sha256:52089f97fe7a9aa0d3277b220c1d730a85aefd64e1b2664696fe35317c5470a7 21 | MarkupSafe==1.1.1 \ 22 | --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ 23 | --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ 24 | --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ 25 | --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ 26 | --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ 27 | --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \ 28 | --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ 29 | --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ 30 | --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ 31 | --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ 32 | --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ 33 | --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ 34 | --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ 35 | --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ 36 | --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ 37 | --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ 38 | --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ 39 | --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ 40 | --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ 41 | --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ 42 | --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ 43 | --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ 44 | --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ 45 | --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ 46 | --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ 47 | --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ 48 | --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ 49 | --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 50 | Jinja2==2.11.3 \ 51 | --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ 52 | --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 53 | PyYAML==5.4 \ 54 | --hash=sha256:f7a21e3d99aa3095ef0553e7ceba36fb693998fbb1226f1392ce33681047465f \ 55 | --hash=sha256:5e7ac4e0e79a53451dc2814f6876c2fa6f71452de1498bbe29c0b54b69a986f4 \ 56 | --hash=sha256:52bf0930903818e600ae6c2901f748bc4869c0c406056f679ab9614e5d21a166 \ 57 | --hash=sha256:a36a48a51e5471513a5aea920cdad84cbd56d70a5057cca3499a637496ea379c \ 58 | --hash=sha256:cc552b6434b90d9dbed6a4f13339625dc466fd82597119897e9489c953acbc22 \ 59 | --hash=sha256:0dc9f2eb2e3c97640928dec63fd8dc1dd91e6b6ed236bd5ac00332b99b5c2ff9 \ 60 | --hash=sha256:5a3f345acff76cad4aa9cb171ee76c590f37394186325d53d1aa25318b0d4a09 \ 61 | --hash=sha256:f3790156c606299ff499ec44db422f66f05a7363b39eb9d5b064f17bd7d7c47b \ 62 | --hash=sha256:124fd7c7bc1e95b1eafc60825f2daf67c73ce7b33f1194731240d24b0d1bf628 \ 63 | --hash=sha256:8b818b6c5a920cbe4203b5a6b14256f0e5244338244560da89b7b0f1313ea4b6 \ 64 | --hash=sha256:737bd70e454a284d456aa1fa71a0b429dd527bcbf52c5c33f7c8eee81ac16b89 \ 65 | --hash=sha256:7242790ab6c20316b8e7bb545be48d7ed36e26bbe279fd56f2c4a12510e60b4b \ 66 | --hash=sha256:cc547d3ead3754712223abb7b403f0a184e4c3eae18c9bb7fd15adef1597cc4b \ 67 | --hash=sha256:8635d53223b1f561b081ff4adecb828fd484b8efffe542edcfdff471997f7c39 \ 68 | --hash=sha256:26fcb33776857f4072601502d93e1a619f166c9c00befb52826e7b774efaa9db \ 69 | --hash=sha256:b2243dd033fd02c01212ad5c601dafb44fbb293065f430b0d3dbf03f3254d615 \ 70 | --hash=sha256:31ba07c54ef4a897758563e3a0fcc60077698df10180abe4b8165d9895c00ebf \ 71 | --hash=sha256:02c78d77281d8f8d07a255e57abdbf43b02257f59f50cc6b636937d68efa5dd0 \ 72 | --hash=sha256:fdc6b2cb4b19e431994f25a9160695cc59a4e861710cc6fc97161c5e845fc579 \ 73 | --hash=sha256:8bf38641b4713d77da19e91f8b5296b832e4db87338d6aeffe422d42f1ca896d \ 74 | --hash=sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a 75 | click==7.0 \ 76 | --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ 77 | --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 78 | six==1.12.0 \ 79 | --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ 80 | --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 81 | -------------------------------------------------------------------------------- /entity-manager.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).ensy=n()}(this,(function(){"use strict";function t(n){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(n)}function n(t,n){for(var e=0;ethis.uid&&(this.uid=n),this.addComponentsToEntity(t,n,e),this.entities.includes(n)||this.entities.push(n),this.listener&&this.listener.emit("entityCreated",n),n}},{key:"removeEntity",value:function(t){for(var n in this.entityComponentData)this.entityComponentData.hasOwnProperty(n)&&this.entityComponentData[n][t]&&delete this.entityComponentData[n][t];return this.entities.splice(this.entities.indexOf(t),1),this.listener&&this.listener.emit("entityRemoved",t),this}},{key:"addComponent",value:function(t,n){return this.components[t]=n,this}},{key:"addComponents",value:function(t){var n=this;return t.forEach((function(t){return n.addComponent(t.name,t)})),this}},{key:"removeComponent",value:function(t){return delete this.components[t],delete this.entityComponentData[t],this}},{key:"getComponentsList",value:function(){return Object.keys(this.components)}},{key:"addComponentsToEntity",value:function(t,n,o){var i=this,r=this;if(t.forEach((function(t){if(!i.components[t])throw new Error("Trying to use unknown component: "+t)})),t.forEach((function(t){i.entityComponentData[t]||(i.entityComponentData[t]={});var o=null;i.listener?function(t,o){var i=e(r.components[o].state);for(var s in i)i.hasOwnProperty(s)&&function(e){Object.defineProperty(t,e,{enumerable:!0,get:function(){return i[e]},set:function(t){i[e]=t,r.listener.emit("entityComponentUpdated",n,o)}})}(s)}(o={},t):o=e(r.components[t].state),o.__id=n,i.entityComponentData[t][n]=o,i.listener&&i.listener.emit("entityComponentAdded",n,t)})),o)for(var s in o)if(o.hasOwnProperty(s)){var a=o[s];this.updateComponentDataForEntity(s,n,a,!1)}return t.forEach((function(t){i.sendEventToProcessors("COMPONENT_CREATED",n,t)})),this}},{key:"removeComponentsFromEntity",value:function(t,n){var e=this;return t.forEach((function(t){if(!e.components[t])throw new Error("Trying to use unknown component: "+t)})),t.forEach((function(t){e.entityComponentData[t]&&e.entityComponentData[t][n]&&(delete e.entityComponentData[t][n],e.listener&&e.listener.emit("entityComponentRemoved",n,t))})),this}},{key:"getComponentDataForEntity",value:function(t,n){if(!(t in this.components))throw new Error("Trying to use unknown component: "+t);if(!this.entityComponentData.hasOwnProperty(t)||!this.entityComponentData[t].hasOwnProperty(n))throw new Error("No data for component "+t+" and entity "+n);return this.entityComponentData[t][n]}},{key:"updateComponentDataForEntity",value:function(t,n,e){var o=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],i=this.getComponentDataForEntity(t,n);for(var r in e)e.hasOwnProperty(r)&&i.hasOwnProperty(r)&&(i[r]=e[r]);return o&&this.sendEventToProcessors("COMPONENT_UPDATED",n,t),this}},{key:"getComponentsData",value:function(t){if(!(t in this.components))throw new Error("Trying to use unknown component: "+t);return this.entityComponentData.hasOwnProperty(t)?this.entityComponentData[t]:{}}},{key:"entityHasComponent",value:function(t,n){return n in this.components&&this.entityComponentData.hasOwnProperty(n)&&this.entityComponentData[n].hasOwnProperty(t)}},{key:"addAssemblage",value:function(t,n){return this.assemblages[t]=n,this}},{key:"addAssemblages",value:function(t){var n=this;return t.forEach((function(t){return n.assemblages[t.name]=t})),this}},{key:"removeAssemblage",value:function(t){return delete this.assemblages[t],this}},{key:"createEntityFromAssemblage",value:function(t){if(!(t in this.assemblages))throw new Error("Trying to use unknown assemblage: "+t);var n=this.assemblages[t];return this.createEntity(n.components,void 0,n.initialState)}},{key:"addProcessor",value:function(t){return this.processors.push(t),this}},{key:"addProcessors",value:function(t){var n=this;return t.forEach((function(t){return n.processors.push(t)})),this}},{key:"removeProcessor",value:function(t){return this.processors.splice(this.processors.indexOf(t),1),this}},{key:"sendEventToProcessors",value:function(t,n,e){var o=this;this.processors.forEach((function(i){"on"in i&&"function"==typeof i.on&&i.on(t,{entity:n,component:e,state:o.entityComponentData[e][n]})}))}},{key:"update",value:function(t){return this.processors.forEach((function(n){return n.update(t)})),this}}])&&n(o.prototype,i),r&&n(o,r),t}()})); 44 | //# sourceMappingURL=entity-manager.js.map 45 | -------------------------------------------------------------------------------- /entity-manager.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"entity-manager.js","sources":["src/entity-manager.js"],"sourcesContent":["/*!\n * ensy - Entity System JavaScript Library v1.5.2\n *\n * A JavaScript implementation of the Entity System model as described by\n * Adam Martin in http://t-machine.org/index.php/2009/10/26/entity-systems-are-the-future-of-mmos-part-5/\n *\n * @author Adrian Gaudebert - adrian@gaudebert.fr\n * @license MIT license.\n * @documentation https://entity-system-js.readthedocs.io/\n *\n */\n\n/*!\n * Return a clone of an object.\n * From https://stackoverflow.com/questions/728360\n */\nfunction clone(obj) {\n // Handle the 3 simple types, and null or undefined\n if (null == obj || 'object' != typeof obj) return obj;\n\n let copy;\n\n // Handle Date\n if (obj instanceof Date) {\n copy = new Date();\n copy.setTime(obj.getTime());\n return copy;\n }\n\n // Handle Array\n if (obj instanceof Array) {\n copy = [];\n for (let i = 0, len = obj.length; i < len; i++) {\n copy[i] = clone(obj[i]);\n }\n return copy;\n }\n\n // Handle Object\n if (obj instanceof Object) {\n copy = {};\n for (let attr in obj) {\n if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);\n }\n return copy;\n }\n}\n\n/*!\n * Return true if the parameter is a function.\n * From https://stackoverflow.com/questions/5999998\n */\nfunction isFunction(thingToCheck) {\n return thingToCheck && ({}).toString.call(thingToCheck) === '[object Function]';\n}\n\n/**\n * @class EntityManager\n *\n * Implement the Entity System model and provide tools to easily\n * create and manipulate Entities, Components and Processors.\n */\nclass EntityManager {\n constructor(listener) {\n this.listener = null;\n if (listener && isFunction(listener.emit)) {\n this.listener = listener;\n }\n\n // A list of entity IDs, each being a simple integer.\n this.entities = [];\n\n // A dictionary of components, where keys are the name of each\n // component. Components are objects containing:\n // * metadata (name, description)\n // * the initial set of data that defines the default state of a\n // newly instanciated component\n this.components = {};\n\n // A dictionary of assemblages, where keys are the name of each\n // assemblage. Assemblages are objects containing:\n // * metadata (name, description)\n // * a list of components to add to the entity\n // * an initial state for some components, to override the defaults\n this.assemblages = {};\n\n /*!\n * A relational-like list of entity states. There is one line for\n * each entity - component association.\n *\n * To optimize the access time to this data, it is stored in a\n * dictionary of dictionaries of this form:\n * {\n * \"componentId\": {\n * \"entityId\": {\n * ...\n * here comes the state of this entity for this component\n * ...\n * }\n * }\n * }\n *\n * This way, getting the data of one entity for one component is:\n * this.entityComponentData[componentId][entityId]\n * and getting all entities for one component is:\n * this.entityComponentData[componentId]\n */\n this.entityComponentData = {};\n\n // The ordered list of processors known by this manager.\n this.processors = [];\n\n // The next unique identifier.\n this.uid = 0;\n }\n\n /**\n * Return an identifier unique to this system.\n *\n * @return {int} - Unique identifier.\n */\n getUid() {\n return this.uid++;\n }\n\n //=========================================================================\n // ENTITIES\n\n /**\n * Create a new entity in the system by creating a new instance of each of its components.\n *\n * @param {array} componentIds - List of identifiers of the components that compose the new entity.\n * @param {int} entityId - Optional. Unique identifier of the entity. If passed, no new id will be generated.\n * @param {object} initialState - Optional. Object containing the initial state to apply.\n * @return {int} - Unique identifier of the new entity.\n */\n createEntity(componentIds, entityId, initialState) {\n if (typeof entityId === 'undefined' || entityId === null) {\n entityId = this.getUid();\n }\n else if (entityId > this.uid) {\n // Make sure another entity with the same ID won't be created in the future.\n this.uid = entityId;\n }\n\n this.addComponentsToEntity(componentIds, entityId, initialState);\n if (!this.entities.includes(entityId)) {\n this.entities.push(entityId);\n }\n if (this.listener) {\n // Signal the creation of a new entity.\n this.listener.emit('entityCreated', entityId);\n }\n return entityId;\n }\n\n /**\n * Remove an entity and its instanciated components from the system.\n *\n * @param {int} id - Unique identifier of the entity.\n * @return {object} - this\n */\n removeEntity(id) {\n // Remove all data for this entity.\n for (let comp in this.entityComponentData) {\n if (this.entityComponentData.hasOwnProperty(comp)) {\n if (this.entityComponentData[comp][id]) {\n delete this.entityComponentData[comp][id];\n }\n }\n }\n\n // Remove the entity from the list of known entities.\n this.entities.splice(this.entities.indexOf(id), 1);\n\n if (this.listener) {\n // Signal the removal of an entity.\n this.listener.emit('entityRemoved', id);\n }\n\n return this;\n }\n\n //=========================================================================\n // COMPONENTS\n\n /**\n * Add a component to the list of known components.\n *\n * @param {string} id - Unique identifier of the component.\n * @param {object} component - Object containing the metadata and data of the component.\n * @return {object} - this\n */\n addComponent(id, component) {\n this.components[id] = component;\n return this;\n }\n\n /**\n * Add a list of components to known components.\n *\n * @param {Array} components - Array of objects containing the metadata and\n * data of components. Requires that each object has `name` used to identify\n * it, and `data` to describe it.\n * @return {object} - this\n */\n addComponents(components) {\n components.forEach(comp => this.addComponent(comp.name, comp));\n return this;\n }\n\n /**\n * Remove a component from the list of known components.\n *\n * @param {string} id - Unique identifier of the component.\n * @return {object} - this\n */\n removeComponent(id) {\n delete this.components[id];\n delete this.entityComponentData[id];\n return this;\n }\n\n /**\n * Get the list of components this instance knows.\n *\n * @return {array} - List of names of components.\n */\n getComponentsList() {\n return Object.keys(this.components);\n }\n\n /**\n * Create a new instance of each listed component and associate them with the entity.\n *\n * @param {array} componentIds - List of identifiers of the components to add to the entity.\n * @param {int} entityId - Unique identifier of the entity.\n * @param {object} initialState - Optional. Object containing the initial state to apply.\n * @return {object} - this\n */\n addComponentsToEntity(componentIds, entityId, initialState) {\n const self = this;\n\n // First verify that all the components exist, and throw an error\n // if any is unknown.\n componentIds.forEach(comp => {\n if (!this.components[comp]) {\n throw new Error('Trying to use unknown component: ' + comp);\n }\n });\n\n // Now we know that this request is correct, let's create the new\n // entity and instanciate the component's states.\n componentIds.forEach(comp => {\n if (!this.entityComponentData[comp]) {\n this.entityComponentData[comp] = {};\n }\n\n let newCompState = null;\n\n // If the manager has a listener, we want to create getters\n // and setters so that we can emit state changes. But if it does\n // not have one, there is no need to add the overhead.\n if (this.listener) {\n newCompState = {};\n (function (newCompState, comp) {\n let state = clone(self.components[comp].state);\n\n // Create a setter for each state attribute, so we can emit an\n // event whenever the state of this component changes.\n for (let property in state) {\n if (state.hasOwnProperty(property)) {\n (function (property) {\n Object.defineProperty(newCompState, property, {\n enumerable: true,\n get: function () {\n return state[property];\n },\n set: function (val) {\n state[property] = val;\n self.listener.emit('entityComponentUpdated', entityId, comp);\n },\n });\n })(property);\n }\n }\n })(newCompState, comp);\n }\n else {\n newCompState = clone(self.components[comp].state);\n }\n\n // Store the entity's ID so it's easier to find other components for that entity.\n newCompState.__id = entityId;\n\n this.entityComponentData[comp][entityId] = newCompState;\n\n if (this.listener) {\n // Signal the addition of a new component to the entity.\n this.listener.emit('entityComponentAdded', entityId, comp);\n }\n });\n\n if (initialState) {\n for (let componentId in initialState) {\n if (initialState.hasOwnProperty(componentId)) {\n const newState = initialState[componentId];\n this.updateComponentDataForEntity(componentId, entityId, newState, false);\n }\n }\n }\n\n componentIds.forEach(componentId => {\n this.sendEventToProcessors('COMPONENT_CREATED', entityId, componentId);\n });\n\n return this;\n }\n\n /**\n * De-associate a list of components from the entity.\n *\n * @param {array} componentIds - List of identifiers of the components to remove from the entity.\n * @param {int} entityId - Unique identifier of the entity.\n * @return {object} - this\n */\n removeComponentsFromEntity(componentIds, entityId) {\n // First verify that all the components exist, and throw an error\n // if any is unknown.\n componentIds.forEach(comp => {\n if (!this.components[comp]) {\n throw new Error('Trying to use unknown component: ' + comp);\n }\n });\n\n // Now we know that this request is correct, let's remove all the\n // components' states for that entity.\n componentIds.forEach(comp => {\n if (this.entityComponentData[comp]) {\n if (this.entityComponentData[comp][entityId]) {\n delete this.entityComponentData[comp][entityId];\n if (this.listener) {\n // Signal the creation of a new entity.\n this.listener.emit('entityComponentRemoved', entityId, comp);\n }\n }\n }\n });\n\n\n return this;\n }\n\n /**\n * Return a reference to an object that contains the data of an\n * instanciated component of an entity.\n *\n * @param {int} entityId - Unique identifier of the entity.\n * @param {string} componentId - Unique identifier of the component.\n * @return {object} - Component data of one entity.\n */\n getComponentDataForEntity(componentId, entityId) {\n if (!(componentId in this.components)) {\n throw new Error('Trying to use unknown component: ' + componentId);\n }\n\n if (\n !this.entityComponentData.hasOwnProperty(componentId) ||\n !this.entityComponentData[componentId].hasOwnProperty(entityId)\n ) {\n throw new Error('No data for component ' + componentId + ' and entity ' + entityId);\n }\n\n return this.entityComponentData[componentId][entityId];\n }\n\n /**\n * Update the state of a component, many keys at once.\n *\n * @param {int} entityId - Unique identifier of the entity.\n * @param {string} componentId - Unique identifier of the component.\n * @param {object} newState - Object containing the new state to apply.\n * @param {boolean} sendUpdateEvent - Optional. True if the method has to send the `COMPONENT_UPDATED` event.\n * @return {object} - this\n */\n updateComponentDataForEntity(componentId, entityId, newState, sendUpdateEvent = true) {\n const compState = this.getComponentDataForEntity(componentId, entityId);\n\n for (let key in newState) {\n if (newState.hasOwnProperty(key) && compState.hasOwnProperty(key)) {\n compState[key] = newState[key];\n }\n }\n\n if (sendUpdateEvent) {\n this.sendEventToProcessors('COMPONENT_UPDATED', entityId, componentId);\n }\n\n return this;\n }\n\n /**\n * Return a dictionary of objects containing the data of all instances of a\n * given component.\n *\n * @param {string} componentId - Unique identifier of the component.\n * @return {Object} - Dictionary of entity id -> component data.\n */\n getComponentsData(componentId) {\n if (!(componentId in this.components)) {\n throw new Error('Trying to use unknown component: ' + componentId);\n }\n\n if (!this.entityComponentData.hasOwnProperty(componentId)) {\n return {};\n }\n\n return this.entityComponentData[componentId];\n }\n\n /**\n * Return true if the entity has the component.\n *\n * @param {int} entityId - Unique identifier of the entity.\n * @param {string} componentId - Unique identifier of the component.\n * @return {boolean} - True if the entity has the component.\n */\n entityHasComponent(entityId, componentId) {\n if (!(componentId in this.components)) {\n return false;\n }\n\n return (\n this.entityComponentData.hasOwnProperty(componentId) &&\n this.entityComponentData[componentId].hasOwnProperty(entityId)\n );\n }\n\n //=========================================================================\n // ASSEMBLAGES\n\n /**\n * Add an assemblage to the list of known assemblages.\n *\n * @param {string} id - Unique identifier of the assemblage.\n * @param {object} assemblage - An instance of an assemblage to add.\n * @return {object} - this\n */\n addAssemblage(id, assemblage) {\n this.assemblages[id] = assemblage;\n return this;\n }\n\n /**\n * Add a list of assemblages to known assemblages.\n *\n * @param {Array} assemblages - An array of assemblages to add. Require that\n * each object has a `name` property to use as identifier.\n * @return {object} - this\n */\n addAssemblages(assemblages) {\n assemblages.forEach(a => this.assemblages[a.name] = a);\n return this;\n }\n\n /**\n * Remove an assemblage from the list of known assemblages.\n *\n * @param {string} id - Unique identifier of the assemblage.\n * @return {object} - this\n */\n removeAssemblage(id) {\n delete this.assemblages[id];\n return this;\n }\n\n /**\n * Create a new entity in the system by creating a new instance of each of\n * its components and setting their initial state, using an assemblage.\n *\n * @param {string} assemblageId - Id of the assemblage to create the entity from.\n * @return {int} - Unique identifier of the new entity.\n */\n createEntityFromAssemblage(assemblageId) {\n if (!(assemblageId in this.assemblages)) {\n throw new Error('Trying to use unknown assemblage: ' + assemblageId);\n }\n\n const assemblage = this.assemblages[assemblageId];\n const entity = this.createEntity(assemblage.components, undefined, assemblage.initialState);\n\n return entity;\n }\n\n //=========================================================================\n // PROCESSORS\n\n /**\n * Add a processor to the list of known processors.\n *\n * @param {object} processor - An instance of a processor to manage.\n * @return {object} - this\n */\n addProcessor(processor) {\n this.processors.push(processor);\n return this;\n }\n\n /**\n * Add a list of processors to known processors.\n *\n * @param {Array} processors - An array of processors to manage.\n * @return {object} - this\n */\n addProcessors(processors) {\n processors.forEach(processor => this.processors.push(processor));\n return this;\n }\n\n /**\n * Remove a processor from the list of known processors.\n *\n * @param {object} processor - An instance of a processor to remove.\n * @return {object} - this\n */\n removeProcessor(processor) {\n this.processors.splice(this.processors.indexOf(processor), 1);\n return this;\n }\n\n /**\n * Send an event to the list of known processors.\n *\n * @param {string} eventName - Id of the event to send.\n * @param {number} entityId - Unique identifier of the entity on which the event occured.\n * @param {string} componentId - Unique identifier of the component on which the event occured.\n * @return {object} - this\n */\n sendEventToProcessors(eventName, entityId, componentId) {\n this.processors.forEach(processor => {\n if ('on' in processor && typeof processor.on === 'function') {\n processor.on(eventName, {\n entity: entityId,\n component: componentId,\n state: this.entityComponentData[componentId][entityId]\n });\n }\n });\n }\n\n /**\n * Update all the known processors.\n *\n * @param {int} dt - The time delta since the last call to update. Will be passed as an argument to all processor's `update` method.\n * @return {object} - this\n */\n update(dt) {\n this.processors.forEach(processor => processor.update(dt));\n return this;\n }\n}\n\nexport default EntityManager;\n"],"names":["clone","obj","copy","Date","setTime","getTime","Array","i","len","length","Object","attr","hasOwnProperty","listener","thingToCheck","emit","toString","call","entities","components","assemblages","entityComponentData","processors","uid","this","componentIds","entityId","initialState","getUid","addComponentsToEntity","includes","push","id","comp","splice","indexOf","component","forEach","_this","addComponent","name","keys","self","_this2","Error","newCompState","state","property","defineProperty","enumerable","get","set","val","__id","componentId","newState","updateComponentDataForEntity","sendEventToProcessors","_this3","sendUpdateEvent","compState","getComponentDataForEntity","key","assemblage","a","_this4","assemblageId","createEntity","undefined","processor","_this5","eventName","on","entity","_this6","dt","update"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAASA,EAAMC,MAEP,MAAQA,GAAO,YAAmBA,GAAK,OAAOA,MAE9CC,KAGAD,aAAeE,YACfD,EAAO,IAAIC,MACNC,QAAQH,EAAII,WACVH,KAIPD,aAAeK,MAAO,CACtBJ,EAAO,OACF,IAAIK,EAAI,EAAGC,EAAMP,EAAIQ,OAAQF,EAAIC,EAAKD,IACvCL,EAAKK,GAAKP,EAAMC,EAAIM,WAEjBL,KAIPD,aAAeS,OAAQ,KAElB,IAAIC,KADTT,EAAO,GACUD,EACTA,EAAIW,eAAeD,KAAOT,EAAKS,GAAQX,EAAMC,EAAIU,YAElDT;;;;kCAmBCW,GAXhB,IAAoBC,6GAYPD,SAAW,KACZA,KAbQC,EAaeD,EAASE,OAZoB,sBAApC,GAAIC,SAASC,KAAKH,WAa7BD,SAAWA,QAIfK,SAAW,QAOXC,WAAa,QAObC,YAAc;;;;;;;;;;;;;;;;;;;;;;KAuBdC,oBAAsB,QAGtBC,WAAa,QAGbC,IAAM,iEASJC,KAAKD,2CAcHE,EAAcC,EAAUC,UAC7B,MAAOD,EACPA,EAAWF,KAAKI,SAEXF,EAAWF,KAAKD,WAEhBA,IAAMG,QAGVG,sBAAsBJ,EAAcC,EAAUC,GAC9CH,KAAKN,SAASY,SAASJ,SACnBR,SAASa,KAAKL,GAEnBF,KAAKX,eAEAA,SAASE,KAAK,gBAAiBW,GAEjCA,uCASEM,OAEJ,IAAIC,KAAQT,KAAKH,oBACdG,KAAKH,oBAAoBT,eAAeqB,IACpCT,KAAKH,oBAAoBY,GAAMD,WACxBR,KAAKH,oBAAoBY,GAAMD,eAM7Cd,SAASgB,OAAOV,KAAKN,SAASiB,QAAQH,GAAK,GAE5CR,KAAKX,eAEAA,SAASE,KAAK,gBAAiBiB,GAGjCR,0CAaEQ,EAAII,eACRjB,WAAWa,GAAMI,EACfZ,2CAWGL,qBACVA,EAAWkB,SAAQ,SAAAJ,UAAQK,EAAKC,aAAaN,EAAKO,KAAMP,MACjDT,6CASKQ,iBACLR,KAAKL,WAAWa,UAChBR,KAAKH,oBAAoBW,GACzBR,wDASAd,OAAO+B,KAAKjB,KAAKL,0DAWNM,EAAcC,EAAUC,cACpCe,EAAOlB,QAIbC,EAAaY,SAAQ,SAAAJ,OACZU,EAAKxB,WAAWc,SACX,IAAIW,MAAM,oCAAsCX,MAM9DR,EAAaY,SAAQ,SAAAJ,GACZU,EAAKtB,oBAAoBY,KAC1BU,EAAKtB,oBAAoBY,GAAQ,QAGjCY,EAAe,KAKfF,EAAK9B,kBAEMgC,EAAcZ,OACjBa,EAAQ9C,EAAM0C,EAAKvB,WAAWc,GAAMa,WAInC,IAAIC,KAAYD,EACbA,EAAMlC,eAAemC,aACVA,GACPrC,OAAOsC,eAAeH,EAAcE,EAAU,CAC1CE,YAAY,EACZC,IAAK,kBACMJ,EAAMC,IAEjBI,IAAK,SAAUC,GACXN,EAAMC,GAAYK,EAClBV,EAAK7B,SAASE,KAAK,yBAA0BW,EAAUO,OAGhEc,IAnBfF,EAAe,GAsBEZ,GAGjBY,EAAe7C,EAAM0C,EAAKvB,WAAWc,GAAMa,OAI/CD,EAAaQ,KAAO3B,EAEpBiB,EAAKtB,oBAAoBY,GAAMP,GAAYmB,EAEvCF,EAAK9B,UAEL8B,EAAK9B,SAASE,KAAK,uBAAwBW,EAAUO,MAIzDN,MACK,IAAI2B,KAAe3B,KAChBA,EAAaf,eAAe0C,GAAc,KACpCC,EAAW5B,EAAa2B,QACzBE,6BAA6BF,EAAa5B,EAAU6B,GAAU,UAK/E9B,EAAaY,SAAQ,SAAAiB,GACjBX,EAAKc,sBAAsB,oBAAqB/B,EAAU4B,MAGvD9B,wDAUgBC,EAAcC,qBAGrCD,EAAaY,SAAQ,SAAAJ,OACZyB,EAAKvC,WAAWc,SACX,IAAIW,MAAM,oCAAsCX,MAM9DR,EAAaY,SAAQ,SAAAJ,GACbyB,EAAKrC,oBAAoBY,IACrByB,EAAKrC,oBAAoBY,GAAMP,YACxBgC,EAAKrC,oBAAoBY,GAAMP,GAClCgC,EAAK7C,UAEL6C,EAAK7C,SAASE,KAAK,yBAA0BW,EAAUO,OAOhET,uDAWe8B,EAAa5B,QAC7B4B,KAAe9B,KAAKL,kBAChB,IAAIyB,MAAM,oCAAsCU,OAIrD9B,KAAKH,oBAAoBT,eAAe0C,KACxC9B,KAAKH,oBAAoBiC,GAAa1C,eAAec,SAEhD,IAAIkB,MAAM,yBAA2BU,EAAc,eAAiB5B,UAGvEF,KAAKH,oBAAoBiC,GAAa5B,wDAYpB4B,EAAa5B,EAAU6B,OAAUI,6DACpDC,EAAYpC,KAAKqC,0BAA0BP,EAAa5B,OAEzD,IAAIoC,KAAOP,EACRA,EAAS3C,eAAekD,IAAQF,EAAUhD,eAAekD,KACzDF,EAAUE,GAAOP,EAASO,WAI9BH,QACKF,sBAAsB,oBAAqB/B,EAAU4B,GAGvD9B,+CAUO8B,QACRA,KAAe9B,KAAKL,kBAChB,IAAIyB,MAAM,oCAAsCU,UAGrD9B,KAAKH,oBAAoBT,eAAe0C,GAItC9B,KAAKH,oBAAoBiC,GAHrB,8CAaI5B,EAAU4B,UACnBA,KAAe9B,KAAKL,YAKtBK,KAAKH,oBAAoBT,eAAe0C,IACxC9B,KAAKH,oBAAoBiC,GAAa1C,eAAec,yCAc/CM,EAAI+B,eACT3C,YAAYY,GAAM+B,EAChBvC,4CAUIJ,qBACXA,EAAYiB,SAAQ,SAAA2B,UAAKC,EAAK7C,YAAY4C,EAAExB,MAAQwB,KAC7CxC,8CASMQ,iBACNR,KAAKJ,YAAYY,GACjBR,wDAUgB0C,QACjBA,KAAgB1C,KAAKJ,mBACjB,IAAIwB,MAAM,qCAAuCsB,OAGrDH,EAAavC,KAAKJ,YAAY8C,UACrB1C,KAAK2C,aAAaJ,EAAW5C,gBAAYiD,EAAWL,EAAWpC,mDAcrE0C,eACJ/C,WAAWS,KAAKsC,GACd7C,2CASGF,qBACVA,EAAWe,SAAQ,SAAAgC,UAAaC,EAAKhD,WAAWS,KAAKsC,MAC9C7C,6CASK6C,eACP/C,WAAWY,OAAOV,KAAKF,WAAWa,QAAQkC,GAAY,GACpD7C,mDAWW+C,EAAW7C,EAAU4B,mBAClChC,WAAWe,SAAQ,SAAAgC,GAChB,OAAQA,GAAqC,mBAAjBA,EAAUG,IACtCH,EAAUG,GAAGD,EAAW,CACpBE,OAAQ/C,EACRU,UAAWkB,EACXR,MAAO4B,EAAKrD,oBAAoBiC,GAAa5B,uCAYtDiD,eACErD,WAAWe,SAAQ,SAAAgC,UAAaA,EAAUO,OAAOD,MAC/CnD"} -------------------------------------------------------------------------------- /examples/concentration/img/images.json: -------------------------------------------------------------------------------- 1 | {"frames": [ 2 | 3 | { 4 | "filename": "bag_64px.png", 5 | "frame": {"x":266,"y":398,"w":64,"h":64}, 6 | "rotated": false, 7 | "trimmed": false, 8 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 9 | "sourceSize": {"w":64,"h":64} 10 | }, 11 | { 12 | "filename": "book_64px.png", 13 | "frame": {"x":266,"y":332,"w":64,"h":64}, 14 | "rotated": false, 15 | "trimmed": false, 16 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 17 | "sourceSize": {"w":64,"h":64} 18 | }, 19 | { 20 | "filename": "bookshelf_64px.png", 21 | "frame": {"x":266,"y":266,"w":64,"h":64}, 22 | "rotated": false, 23 | "trimmed": false, 24 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 25 | "sourceSize": {"w":64,"h":64} 26 | }, 27 | { 28 | "filename": "browser_64px.png", 29 | "frame": {"x":398,"y":200,"w":64,"h":64}, 30 | "rotated": false, 31 | "trimmed": false, 32 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 33 | "sourceSize": {"w":64,"h":64} 34 | }, 35 | { 36 | "filename": "calendar_64px.png", 37 | "frame": {"x":332,"y":200,"w":64,"h":64}, 38 | "rotated": false, 39 | "trimmed": false, 40 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 41 | "sourceSize": {"w":64,"h":64} 42 | }, 43 | { 44 | "filename": "chat_64px.png", 45 | "frame": {"x":266,"y":200,"w":64,"h":64}, 46 | "rotated": false, 47 | "trimmed": false, 48 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 49 | "sourceSize": {"w":64,"h":64} 50 | }, 51 | { 52 | "filename": "clipboard_64px.png", 53 | "frame": {"x":200,"y":398,"w":64,"h":64}, 54 | "rotated": false, 55 | "trimmed": false, 56 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 57 | "sourceSize": {"w":64,"h":64} 58 | }, 59 | { 60 | "filename": "clock_64px.png", 61 | "frame": {"x":200,"y":332,"w":64,"h":64}, 62 | "rotated": false, 63 | "trimmed": false, 64 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 65 | "sourceSize": {"w":64,"h":64} 66 | }, 67 | { 68 | "filename": "cloud_64px.png", 69 | "frame": {"x":200,"y":266,"w":64,"h":64}, 70 | "rotated": false, 71 | "trimmed": false, 72 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 73 | "sourceSize": {"w":64,"h":64} 74 | }, 75 | { 76 | "filename": "compass_64px.png", 77 | "frame": {"x":200,"y":200,"w":64,"h":64}, 78 | "rotated": false, 79 | "trimmed": false, 80 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 81 | "sourceSize": {"w":64,"h":64} 82 | }, 83 | { 84 | "filename": "compose_64px.png", 85 | "frame": {"x":398,"y":134,"w":64,"h":64}, 86 | "rotated": false, 87 | "trimmed": false, 88 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 89 | "sourceSize": {"w":64,"h":64} 90 | }, 91 | { 92 | "filename": "creditcard_64px.png", 93 | "frame": {"x":332,"y":134,"w":64,"h":64}, 94 | "rotated": false, 95 | "trimmed": false, 96 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 97 | "sourceSize": {"w":64,"h":64} 98 | }, 99 | { 100 | "filename": "denied_64px.png", 101 | "frame": {"x":266,"y":134,"w":64,"h":64}, 102 | "rotated": false, 103 | "trimmed": false, 104 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 105 | "sourceSize": {"w":64,"h":64} 106 | }, 107 | { 108 | "filename": "document_64px.png", 109 | "frame": {"x":200,"y":134,"w":64,"h":64}, 110 | "rotated": false, 111 | "trimmed": false, 112 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 113 | "sourceSize": {"w":64,"h":64} 114 | }, 115 | { 116 | "filename": "download_64px.png", 117 | "frame": {"x":134,"y":398,"w":64,"h":64}, 118 | "rotated": false, 119 | "trimmed": false, 120 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 121 | "sourceSize": {"w":64,"h":64} 122 | }, 123 | { 124 | "filename": "folder_64px.png", 125 | "frame": {"x":134,"y":332,"w":64,"h":64}, 126 | "rotated": false, 127 | "trimmed": false, 128 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 129 | "sourceSize": {"w":64,"h":64} 130 | }, 131 | { 132 | "filename": "gear_64px.png", 133 | "frame": {"x":134,"y":266,"w":64,"h":64}, 134 | "rotated": false, 135 | "trimmed": false, 136 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 137 | "sourceSize": {"w":64,"h":64} 138 | }, 139 | { 140 | "filename": "global_64px.png", 141 | "frame": {"x":134,"y":200,"w":64,"h":64}, 142 | "rotated": false, 143 | "trimmed": false, 144 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 145 | "sourceSize": {"w":64,"h":64} 146 | }, 147 | { 148 | "filename": "hazard_64px.png", 149 | "frame": {"x":134,"y":134,"w":64,"h":64}, 150 | "rotated": false, 151 | "trimmed": false, 152 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 153 | "sourceSize": {"w":64,"h":64} 154 | }, 155 | { 156 | "filename": "layers_64px.png", 157 | "frame": {"x":332,"y":266,"w":63,"h":63}, 158 | "rotated": false, 159 | "trimmed": false, 160 | "spriteSourceSize": {"x":0,"y":0,"w":63,"h":63}, 161 | "sourceSize": {"w":63,"h":63} 162 | }, 163 | { 164 | "filename": "lightbulb_64px.png", 165 | "frame": {"x":398,"y":68,"w":64,"h":64}, 166 | "rotated": false, 167 | "trimmed": false, 168 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 169 | "sourceSize": {"w":64,"h":64} 170 | }, 171 | { 172 | "filename": "lock_64px.png", 173 | "frame": {"x":332,"y":68,"w":64,"h":64}, 174 | "rotated": false, 175 | "trimmed": false, 176 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 177 | "sourceSize": {"w":64,"h":64} 178 | }, 179 | { 180 | "filename": "mail_64px.png", 181 | "frame": {"x":266,"y":68,"w":64,"h":64}, 182 | "rotated": false, 183 | "trimmed": false, 184 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 185 | "sourceSize": {"w":64,"h":64} 186 | }, 187 | { 188 | "filename": "meter_64px.png", 189 | "frame": {"x":200,"y":68,"w":64,"h":64}, 190 | "rotated": false, 191 | "trimmed": false, 192 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 193 | "sourceSize": {"w":64,"h":64} 194 | }, 195 | { 196 | "filename": "paint_64px.png", 197 | "frame": {"x":134,"y":68,"w":64,"h":64}, 198 | "rotated": false, 199 | "trimmed": false, 200 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 201 | "sourceSize": {"w":64,"h":64} 202 | }, 203 | { 204 | "filename": "phone_64px.png", 205 | "frame": {"x":68,"y":398,"w":64,"h":64}, 206 | "rotated": false, 207 | "trimmed": false, 208 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 209 | "sourceSize": {"w":64,"h":64} 210 | }, 211 | { 212 | "filename": "piechart_64px.png", 213 | "frame": {"x":68,"y":332,"w":64,"h":64}, 214 | "rotated": false, 215 | "trimmed": false, 216 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 217 | "sourceSize": {"w":64,"h":64} 218 | }, 219 | { 220 | "filename": "pin_64px.png", 221 | "frame": {"x":68,"y":266,"w":64,"h":64}, 222 | "rotated": false, 223 | "trimmed": false, 224 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 225 | "sourceSize": {"w":64,"h":64} 226 | }, 227 | { 228 | "filename": "record-sleeve_64px.png", 229 | "frame": {"x":68,"y":200,"w":64,"h":64}, 230 | "rotated": false, 231 | "trimmed": false, 232 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 233 | "sourceSize": {"w":64,"h":64} 234 | }, 235 | { 236 | "filename": "ribbon_64px.png", 237 | "frame": {"x":68,"y":134,"w":64,"h":64}, 238 | "rotated": false, 239 | "trimmed": false, 240 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 241 | "sourceSize": {"w":64,"h":64} 242 | }, 243 | { 244 | "filename": "scissors_64px.png", 245 | "frame": {"x":68,"y":68,"w":64,"h":64}, 246 | "rotated": false, 247 | "trimmed": false, 248 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 249 | "sourceSize": {"w":64,"h":64} 250 | }, 251 | { 252 | "filename": "security_64px.png", 253 | "frame": {"x":398,"y":2,"w":64,"h":64}, 254 | "rotated": false, 255 | "trimmed": false, 256 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 257 | "sourceSize": {"w":64,"h":64} 258 | }, 259 | { 260 | "filename": "settings_64px.png", 261 | "frame": {"x":332,"y":2,"w":64,"h":64}, 262 | "rotated": false, 263 | "trimmed": false, 264 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 265 | "sourceSize": {"w":64,"h":64} 266 | }, 267 | { 268 | "filename": "star_64px.png", 269 | "frame": {"x":266,"y":2,"w":64,"h":64}, 270 | "rotated": false, 271 | "trimmed": false, 272 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 273 | "sourceSize": {"w":64,"h":64} 274 | }, 275 | { 276 | "filename": "support_64px.png", 277 | "frame": {"x":200,"y":2,"w":64,"h":64}, 278 | "rotated": false, 279 | "trimmed": false, 280 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 281 | "sourceSize": {"w":64,"h":64} 282 | }, 283 | { 284 | "filename": "tablet_64px.png", 285 | "frame": {"x":134,"y":2,"w":64,"h":64}, 286 | "rotated": false, 287 | "trimmed": false, 288 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 289 | "sourceSize": {"w":64,"h":64} 290 | }, 291 | { 292 | "filename": "tag_64px.png", 293 | "frame": {"x":68,"y":2,"w":64,"h":64}, 294 | "rotated": false, 295 | "trimmed": false, 296 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 297 | "sourceSize": {"w":64,"h":64} 298 | }, 299 | { 300 | "filename": "target_64px.png", 301 | "frame": {"x":2,"y":398,"w":64,"h":64}, 302 | "rotated": false, 303 | "trimmed": false, 304 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 305 | "sourceSize": {"w":64,"h":64} 306 | }, 307 | { 308 | "filename": "tools_64px.png", 309 | "frame": {"x":2,"y":332,"w":64,"h":64}, 310 | "rotated": false, 311 | "trimmed": false, 312 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 313 | "sourceSize": {"w":64,"h":64} 314 | }, 315 | { 316 | "filename": "trends_64px.png", 317 | "frame": {"x":2,"y":266,"w":64,"h":64}, 318 | "rotated": false, 319 | "trimmed": false, 320 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 321 | "sourceSize": {"w":64,"h":64} 322 | }, 323 | { 324 | "filename": "turntable_64px.png", 325 | "frame": {"x":2,"y":200,"w":64,"h":64}, 326 | "rotated": false, 327 | "trimmed": false, 328 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 329 | "sourceSize": {"w":64,"h":64} 330 | }, 331 | { 332 | "filename": "upload_64px.png", 333 | "frame": {"x":2,"y":134,"w":64,"h":64}, 334 | "rotated": false, 335 | "trimmed": false, 336 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 337 | "sourceSize": {"w":64,"h":64} 338 | }, 339 | { 340 | "filename": "video_64px.png", 341 | "frame": {"x":2,"y":68,"w":64,"h":64}, 342 | "rotated": false, 343 | "trimmed": false, 344 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 345 | "sourceSize": {"w":64,"h":64} 346 | }, 347 | { 348 | "filename": "wallet_64px.png", 349 | "frame": {"x":2,"y":2,"w":64,"h":64}, 350 | "rotated": false, 351 | "trimmed": false, 352 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, 353 | "sourceSize": {"w":64,"h":64} 354 | }], 355 | "meta": { 356 | "app": "http://www.codeandweb.com/texturepacker ", 357 | "version": "1.0", 358 | "image": "images.png", 359 | "format": "RGBA8888", 360 | "size": {"w":512,"h":512}, 361 | "scale": "1", 362 | "smartupdate": "$TexturePacker:SmartUpdate:e5b4b12fba470b8ec789ca1bc7d13108:e4f4a916ca759434f4f40b8eb634b7fc:9be8c185d95a5be4a06ec0067a3d8380$" 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /examples/concentration/img/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adngdb/entity-system-js/4e4c38a9ba0f38960a505527c44014dec7e68c12/examples/concentration/img/images.png -------------------------------------------------------------------------------- /examples/concentration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Concentration - Example game with EntityManager.js 7 | 8 | 9 | 10 | 11 | 25 | 26 | 27 |

Concentration

28 | 29 |
30 | 31 |

32 | Image and code credits: 33 | 34 | Complete HTML5 Concentration game made with PIXI.js 35 | 36 |

37 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/concentration/js/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A game of Concentration. Each tile exists in double. Click on tiles to turn 3 | * them up, and find all the couples. 4 | * 5 | * Code initially taken from 6 | * http://www.emanueleferonato.com/2014/02/26/complete-html5-concentration-game-made-with-pixi-js/ 7 | * but then largely rewritten as an Entity System game. 8 | * 9 | * Using: 10 | * - ensy - Entity System for JavaScript - https://github.com/adngdb/entity-system-js 11 | * - Pixi.js - http://www.pixijs.com/ 12 | * - require.js - http://requirejs.org/ 13 | */ 14 | 15 | require.config({ 16 | paths: { 17 | 'entity-manager': ['../../../entity-manager'], 18 | 'lib': './lib' 19 | } 20 | }); 21 | 22 | require(['entity-manager', 'lib/pixi'], 23 | function (EntityManager, PIXI) { 24 | 25 | var stage = new PIXI.Stage(0x888888); 26 | var renderer = PIXI.autoDetectRenderer(640, 480); 27 | document.getElementById('stage').appendChild(renderer.view); 28 | 29 | /*--- COMPONENTS ---*/ 30 | 31 | /** 32 | * The basic Card component, that each tile on the board has. Controls the 33 | * position of the card and its tile number. 34 | */ 35 | var Card = { 36 | name: 'Card', 37 | state: { 38 | x: 0, 39 | y: 0, 40 | tile: 0, 41 | } 42 | }; 43 | 44 | /** 45 | * A marker for cards that are face down (tile hidden) in the game. 46 | */ 47 | var CardFaceDown = { 48 | name: 'CardFaceDown', 49 | state: { 50 | } 51 | }; 52 | 53 | /** 54 | * A marker for cards that are face up (tile visible) in the game. 55 | */ 56 | var CardFaceUp = { 57 | name: 'CardFaceUp', 58 | state: { 59 | } 60 | }; 61 | 62 | /** 63 | * A marker for cards that have been clicked by the player. 64 | */ 65 | var CardClicked = { 66 | name: 'CardClicked', 67 | state: { 68 | } 69 | }; 70 | 71 | /** 72 | * A marker for cards that have been found by the player and should be 73 | * removed from the game. 74 | */ 75 | var CardFound = { 76 | name: 'CardFound', 77 | state: { 78 | } 79 | }; 80 | 81 | /*--- PROCESSORS ---*/ 82 | 83 | /** 84 | * A processor that takes care of all things related to displaying the game. 85 | */ 86 | var RenderingProcessor = function (manager) { 87 | this.manager = manager; 88 | 89 | this.container = new PIXI.DisplayObjectContainer(); 90 | stage.addChild(this.container); 91 | 92 | // An associative array for entities' sprites. 93 | // entity id -> sprite 94 | this.sprites = {}; 95 | 96 | this.initTiles(); 97 | }; 98 | 99 | RenderingProcessor.prototype.initTiles = function () { 100 | var cards = this.manager.getComponentsData('Card'); 101 | for (var entityId in cards) { 102 | this.createCardTile(entityId, cards[entityId]); 103 | } 104 | }; 105 | 106 | RenderingProcessor.prototype.createCardTile = function (cardId, cardData) { 107 | var sprite = PIXI.Sprite.fromFrame(cardData.tile); 108 | 109 | sprite.buttonMode = true; 110 | sprite.interactive = true; 111 | sprite.position.x = 7 + cardData.x * 80; 112 | sprite.position.y = 7 + cardData.y * 80; 113 | 114 | (function (cardId, sprite, self) { 115 | sprite.click = function () { 116 | self.manager.addComponentsToEntity(['CardClicked'], cardId); 117 | }; 118 | })(cardId, sprite, this); 119 | 120 | this.container.addChild(sprite); 121 | 122 | this.sprites[cardId] = sprite; 123 | }; 124 | 125 | RenderingProcessor.prototype.update = function () { 126 | var found = this.manager.getComponentsData('CardFound'); 127 | for (var entityId in found) { 128 | this.container.removeChild(this.sprites[entityId]); 129 | this.manager.removeEntity(entityId); 130 | } 131 | 132 | var faceDown = this.manager.getComponentsData('CardFaceDown'); 133 | for (var entityId in faceDown) { 134 | this.sprites[entityId].tint = 0x000000; 135 | this.sprites[entityId].alpha = 0.5; 136 | } 137 | 138 | var faceUp = this.manager.getComponentsData('CardFaceUp'); 139 | for (var entityId in faceUp) { 140 | this.sprites[entityId].tint = 0xffffff; 141 | this.sprites[entityId].alpha = 1.0; 142 | } 143 | 144 | renderer.render(stage); 145 | }; 146 | 147 | /** 148 | * A processor that manages the cards and their change of state. 149 | */ 150 | var CardProcessor = function (manager) { 151 | this.manager = manager; 152 | 153 | this.numberOfTiles = 48; // Number of tiles that will get displayed. 154 | this.numberOfPossibleTiles = 44; // Number of tiles in the image file. 155 | this.width = 8; // in number of tiles. 156 | 157 | this.timerStart = null; 158 | 159 | this.init(); 160 | }; 161 | 162 | CardProcessor.prototype.init = function () { 163 | // Choose a random set of tiles. 164 | var chosenTiles = []; 165 | while (chosenTiles.length < this.numberOfTiles) { 166 | var candidate = Math.floor(Math.random() * this.numberOfPossibleTiles); 167 | if (chosenTiles.indexOf(candidate) === -1) { 168 | chosenTiles.push(candidate, candidate); 169 | } 170 | } 171 | 172 | // Randomize those tiles. 173 | for (var i = 0; i < 96; i++) { 174 | var from = Math.floor(Math.random() * this.numberOfTiles); 175 | var to = Math.floor(Math.random() * this.numberOfTiles); 176 | var tmp = chosenTiles[from]; 177 | chosenTiles[from] = chosenTiles[to]; 178 | chosenTiles[to] = tmp; 179 | } 180 | 181 | for (var i = chosenTiles.length - 1; i >= 0; i--) { 182 | var cardId = this.manager.createEntity(['Card', 'CardFaceDown']); 183 | var card = this.manager.getComponentDataForEntity('Card', cardId); 184 | card.tile = chosenTiles[i]; 185 | card.x = i % this.width; 186 | card.y = Math.floor(i / this.width); 187 | } 188 | }; 189 | 190 | CardProcessor.prototype.update = function () { 191 | var manager = this.manager; 192 | var card; 193 | 194 | // Get all the cards currently face up, used in tests because we never 195 | // want to have more than 2 cards face up. 196 | var faceUp = manager.getComponentsData('CardFaceUp'); 197 | var faceUpIds = Object.keys(faceUp); 198 | 199 | // Go through clicked cards and turn them face up if possible. 200 | var clicked = manager.getComponentsData('CardClicked'); 201 | for (card in clicked) { 202 | if (Object.keys(faceUp).length < 2 && manager.entityHasComponent(card, 'CardFaceDown')) { 203 | manager.removeComponentsFromEntity(['CardFaceDown'], card); 204 | manager.addComponentsToEntity(['CardFaceUp'], card); 205 | 206 | if (Object.keys(faceUp).length === 2) { 207 | this.timerStart = +new Date(); 208 | } 209 | } 210 | 211 | manager.removeComponentsFromEntity(['CardClicked'], card); 212 | } 213 | 214 | if (faceUpIds.length > 2) { 215 | throw 'You did your job poorly, developer. Now go and fix your code.'; 216 | } 217 | 218 | // When 2 cards are face up and we have waited long enough, if those 219 | // cards have the same tile, mark them as found, otherwise turn them 220 | // face down. 221 | if (faceUpIds.length == 2 && this.timerStart) { 222 | var now = +new Date(); 223 | if (now - this.timerStart >= 1000) { 224 | var card1 = manager.getComponentDataForEntity('Card', faceUpIds[0]); 225 | var card2 = manager.getComponentDataForEntity('Card', faceUpIds[1]); 226 | 227 | if (card1.tile === card2.tile) { 228 | // Found a pair. 229 | manager.addComponentsToEntity(['CardFound'], faceUpIds[0]); 230 | manager.addComponentsToEntity(['CardFound'], faceUpIds[1]); 231 | } 232 | else { 233 | // Not a pair. 234 | manager.removeComponentsFromEntity(['CardFaceUp'], faceUpIds[0]); 235 | manager.removeComponentsFromEntity(['CardFaceUp'], faceUpIds[1]); 236 | manager.addComponentsToEntity(['CardFaceDown'], faceUpIds[0]); 237 | manager.addComponentsToEntity(['CardFaceDown'], faceUpIds[1]); 238 | } 239 | 240 | this.timerStart = null; 241 | } 242 | } 243 | }; 244 | 245 | function start() { 246 | // Create an Entity System manager object. 247 | var manager = new EntityManager(); 248 | 249 | // Add all components to the system. 250 | var components = [ 251 | Card, 252 | CardFaceUp, 253 | CardFaceDown, 254 | CardClicked, 255 | CardFound, 256 | ]; 257 | for (var i = components.length - 1; i >= 0; i--) { 258 | manager.addComponent(components[i].name, components[i]); 259 | } 260 | 261 | // Add all processors in the system. Note that because of the logic 262 | // in our processors' constructors, order here matters. 263 | // CardProcessor creates all the card entities, and RenderingProcessor 264 | // then creates all the sprites to go with them. 265 | manager.addProcessor(new CardProcessor(manager)); 266 | manager.addProcessor(new RenderingProcessor(manager)); 267 | 268 | // Start the main loop of the game. 269 | requestAnimFrame(animate); 270 | function animate() { 271 | requestAnimFrame(animate); 272 | 273 | manager.update(); 274 | } 275 | } 276 | 277 | var tileAtlas = ["img/images.json"]; 278 | var loader = new PIXI.AssetLoader(tileAtlas); 279 | loader.onComplete = start; 280 | loader.load(); 281 | }); 282 | -------------------------------------------------------------------------------- /lib/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&& 19 | (f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a= 20 | this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f); 21 | if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval", 22 | "fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b, 23 | a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p, 24 | nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b, 25 | a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild= 26 | !0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!== 27 | e&&(!("."===k||".."===k)||1e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)): 34 | (e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl= 35 | O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b|| 36 | (b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this); 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Entity System Docs 2 | theme: readthedocs 3 | docs_dir: docs 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ensy", 3 | "version": "1.5.2", 4 | "description": "Entity System for JavaScript", 5 | "homepage": "https://github.com/adngdb/entity-system-js", 6 | "author": "Adrian Gaudebert (http://adrian.gaudebert.fr)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/adngdb/entity-system-js" 11 | }, 12 | "files": [ 13 | "entity-manager.js" 14 | ], 15 | "main": "entity-manager.js", 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "@babel/core": "7.9.6", 19 | "@babel/preset-env": "7.9.6", 20 | "amdefine": "1.0.1", 21 | "benchmark": "2.1.4", 22 | "chai": "4.2.0", 23 | "markdox": "0.1.10", 24 | "mocha": "7.1.2", 25 | "rollup": "1.16.2", 26 | "rollup-plugin-babel": "4.3.3", 27 | "rollup-plugin-terser": "^5.3.0" 28 | }, 29 | "scripts": { 30 | "test": "mocha `find test -name 'test_*.js'` --reporter spec", 31 | "test-w": "mocha `find test -name 'test_*.js'` --reporter spec --growl --watch", 32 | "test-browser": "xdg-open test/index.html", 33 | "build": "rollup -c", 34 | "build_doc": "markdox src/*.js --output=docs/api.md" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | 5 | export default { 6 | input: 'src/entity-manager.js', 7 | output: { 8 | file: 'entity-manager.js', 9 | format: 'umd', 10 | name: 'ensy', 11 | sourcemap: true, 12 | }, 13 | plugins: [ 14 | babel({ 15 | exclude: 'node_modules/**', 16 | }), 17 | terser(), 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /src/entity-manager.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ensy - Entity System JavaScript Library v1.5.2 3 | * 4 | * A JavaScript implementation of the Entity System model as described by 5 | * Adam Martin in http://t-machine.org/index.php/2009/10/26/entity-systems-are-the-future-of-mmos-part-5/ 6 | * 7 | * @author Adrian Gaudebert - adrian@gaudebert.fr 8 | * @license MIT license. 9 | * @documentation https://entity-system-js.readthedocs.io/ 10 | * 11 | */ 12 | 13 | /*! 14 | * Return a clone of an object. 15 | * From https://stackoverflow.com/questions/728360 16 | */ 17 | function clone(obj) { 18 | // Handle the 3 simple types, and null or undefined 19 | if (null == obj || 'object' != typeof obj) return obj; 20 | 21 | let copy; 22 | 23 | // Handle Date 24 | if (obj instanceof Date) { 25 | copy = new Date(); 26 | copy.setTime(obj.getTime()); 27 | return copy; 28 | } 29 | 30 | // Handle Array 31 | if (obj instanceof Array) { 32 | copy = []; 33 | for (let i = 0, len = obj.length; i < len; i++) { 34 | copy[i] = clone(obj[i]); 35 | } 36 | return copy; 37 | } 38 | 39 | // Handle Object 40 | if (obj instanceof Object) { 41 | copy = {}; 42 | for (let attr in obj) { 43 | if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); 44 | } 45 | return copy; 46 | } 47 | } 48 | 49 | /*! 50 | * Return true if the parameter is a function. 51 | * From https://stackoverflow.com/questions/5999998 52 | */ 53 | function isFunction(thingToCheck) { 54 | return thingToCheck && ({}).toString.call(thingToCheck) === '[object Function]'; 55 | } 56 | 57 | /** 58 | * @class EntityManager 59 | * 60 | * Implement the Entity System model and provide tools to easily 61 | * create and manipulate Entities, Components and Processors. 62 | */ 63 | class EntityManager { 64 | constructor(listener) { 65 | this.listener = null; 66 | if (listener && isFunction(listener.emit)) { 67 | this.listener = listener; 68 | } 69 | 70 | // A list of entity IDs, each being a simple integer. 71 | this.entities = []; 72 | 73 | // A dictionary of components, where keys are the name of each 74 | // component. Components are objects containing: 75 | // * metadata (name, description) 76 | // * the initial set of data that defines the default state of a 77 | // newly instanciated component 78 | this.components = {}; 79 | 80 | // A dictionary of assemblages, where keys are the name of each 81 | // assemblage. Assemblages are objects containing: 82 | // * metadata (name, description) 83 | // * a list of components to add to the entity 84 | // * an initial state for some components, to override the defaults 85 | this.assemblages = {}; 86 | 87 | /*! 88 | * A relational-like list of entity states. There is one line for 89 | * each entity - component association. 90 | * 91 | * To optimize the access time to this data, it is stored in a 92 | * dictionary of dictionaries of this form: 93 | * { 94 | * "componentId": { 95 | * "entityId": { 96 | * ... 97 | * here comes the state of this entity for this component 98 | * ... 99 | * } 100 | * } 101 | * } 102 | * 103 | * This way, getting the data of one entity for one component is: 104 | * this.entityComponentData[componentId][entityId] 105 | * and getting all entities for one component is: 106 | * this.entityComponentData[componentId] 107 | */ 108 | this.entityComponentData = {}; 109 | 110 | // The ordered list of processors known by this manager. 111 | this.processors = []; 112 | 113 | // The next unique identifier. 114 | this.uid = 0; 115 | } 116 | 117 | /** 118 | * Return an identifier unique to this system. 119 | * 120 | * @return {int} - Unique identifier. 121 | */ 122 | getUid() { 123 | return this.uid++; 124 | } 125 | 126 | //========================================================================= 127 | // ENTITIES 128 | 129 | /** 130 | * Create a new entity in the system by creating a new instance of each of its components. 131 | * 132 | * @param {array} componentIds - List of identifiers of the components that compose the new entity. 133 | * @param {int} entityId - Optional. Unique identifier of the entity. If passed, no new id will be generated. 134 | * @param {object} initialState - Optional. Object containing the initial state to apply. 135 | * @return {int} - Unique identifier of the new entity. 136 | */ 137 | createEntity(componentIds, entityId, initialState) { 138 | if (typeof entityId === 'undefined' || entityId === null) { 139 | entityId = this.getUid(); 140 | } 141 | else if (entityId > this.uid) { 142 | // Make sure another entity with the same ID won't be created in the future. 143 | this.uid = entityId; 144 | } 145 | 146 | this.addComponentsToEntity(componentIds, entityId, initialState); 147 | if (!this.entities.includes(entityId)) { 148 | this.entities.push(entityId); 149 | } 150 | if (this.listener) { 151 | // Signal the creation of a new entity. 152 | this.listener.emit('entityCreated', entityId); 153 | } 154 | return entityId; 155 | } 156 | 157 | /** 158 | * Remove an entity and its instanciated components from the system. 159 | * 160 | * @param {int} id - Unique identifier of the entity. 161 | * @return {object} - this 162 | */ 163 | removeEntity(id) { 164 | // Remove all data for this entity. 165 | for (let comp in this.entityComponentData) { 166 | if (this.entityComponentData.hasOwnProperty(comp)) { 167 | if (this.entityComponentData[comp][id]) { 168 | delete this.entityComponentData[comp][id]; 169 | } 170 | } 171 | } 172 | 173 | // Remove the entity from the list of known entities. 174 | this.entities.splice(this.entities.indexOf(id), 1); 175 | 176 | if (this.listener) { 177 | // Signal the removal of an entity. 178 | this.listener.emit('entityRemoved', id); 179 | } 180 | 181 | return this; 182 | } 183 | 184 | //========================================================================= 185 | // COMPONENTS 186 | 187 | /** 188 | * Add a component to the list of known components. 189 | * 190 | * @param {string} id - Unique identifier of the component. 191 | * @param {object} component - Object containing the metadata and data of the component. 192 | * @return {object} - this 193 | */ 194 | addComponent(id, component) { 195 | this.components[id] = component; 196 | return this; 197 | } 198 | 199 | /** 200 | * Add a list of components to known components. 201 | * 202 | * @param {Array} components - Array of objects containing the metadata and 203 | * data of components. Requires that each object has `name` used to identify 204 | * it, and `data` to describe it. 205 | * @return {object} - this 206 | */ 207 | addComponents(components) { 208 | components.forEach(comp => this.addComponent(comp.name, comp)); 209 | return this; 210 | } 211 | 212 | /** 213 | * Remove a component from the list of known components. 214 | * 215 | * @param {string} id - Unique identifier of the component. 216 | * @return {object} - this 217 | */ 218 | removeComponent(id) { 219 | delete this.components[id]; 220 | delete this.entityComponentData[id]; 221 | return this; 222 | } 223 | 224 | /** 225 | * Get the list of components this instance knows. 226 | * 227 | * @return {array} - List of names of components. 228 | */ 229 | getComponentsList() { 230 | return Object.keys(this.components); 231 | } 232 | 233 | /** 234 | * Create a new instance of each listed component and associate them with the entity. 235 | * 236 | * @param {array} componentIds - List of identifiers of the components to add to the entity. 237 | * @param {int} entityId - Unique identifier of the entity. 238 | * @param {object} initialState - Optional. Object containing the initial state to apply. 239 | * @return {object} - this 240 | */ 241 | addComponentsToEntity(componentIds, entityId, initialState) { 242 | const self = this; 243 | 244 | // First verify that all the components exist, and throw an error 245 | // if any is unknown. 246 | componentIds.forEach(comp => { 247 | if (!this.components[comp]) { 248 | throw new Error('Trying to use unknown component: ' + comp); 249 | } 250 | }); 251 | 252 | // Now we know that this request is correct, let's create the new 253 | // entity and instanciate the component's states. 254 | componentIds.forEach(comp => { 255 | if (!this.entityComponentData[comp]) { 256 | this.entityComponentData[comp] = {}; 257 | } 258 | 259 | let newCompState = null; 260 | 261 | // If the manager has a listener, we want to create getters 262 | // and setters so that we can emit state changes. But if it does 263 | // not have one, there is no need to add the overhead. 264 | if (this.listener) { 265 | newCompState = {}; 266 | (function (newCompState, comp) { 267 | let state = clone(self.components[comp].state); 268 | 269 | // Create a setter for each state attribute, so we can emit an 270 | // event whenever the state of this component changes. 271 | for (let property in state) { 272 | if (state.hasOwnProperty(property)) { 273 | (function (property) { 274 | Object.defineProperty(newCompState, property, { 275 | enumerable: true, 276 | get: function () { 277 | return state[property]; 278 | }, 279 | set: function (val) { 280 | state[property] = val; 281 | self.listener.emit('entityComponentUpdated', entityId, comp); 282 | }, 283 | }); 284 | })(property); 285 | } 286 | } 287 | })(newCompState, comp); 288 | } 289 | else { 290 | newCompState = clone(self.components[comp].state); 291 | } 292 | 293 | // Store the entity's ID so it's easier to find other components for that entity. 294 | newCompState.__id = entityId; 295 | 296 | this.entityComponentData[comp][entityId] = newCompState; 297 | 298 | if (this.listener) { 299 | // Signal the addition of a new component to the entity. 300 | this.listener.emit('entityComponentAdded', entityId, comp); 301 | } 302 | }); 303 | 304 | if (initialState) { 305 | for (let componentId in initialState) { 306 | if (initialState.hasOwnProperty(componentId)) { 307 | const newState = initialState[componentId]; 308 | this.updateComponentDataForEntity(componentId, entityId, newState, false); 309 | } 310 | } 311 | } 312 | 313 | componentIds.forEach(componentId => { 314 | this.sendEventToProcessors('COMPONENT_CREATED', entityId, componentId); 315 | }); 316 | 317 | return this; 318 | } 319 | 320 | /** 321 | * De-associate a list of components from the entity. 322 | * 323 | * @param {array} componentIds - List of identifiers of the components to remove from the entity. 324 | * @param {int} entityId - Unique identifier of the entity. 325 | * @return {object} - this 326 | */ 327 | removeComponentsFromEntity(componentIds, entityId) { 328 | // First verify that all the components exist, and throw an error 329 | // if any is unknown. 330 | componentIds.forEach(comp => { 331 | if (!this.components[comp]) { 332 | throw new Error('Trying to use unknown component: ' + comp); 333 | } 334 | }); 335 | 336 | // Now we know that this request is correct, let's remove all the 337 | // components' states for that entity. 338 | componentIds.forEach(comp => { 339 | if (this.entityComponentData[comp]) { 340 | if (this.entityComponentData[comp][entityId]) { 341 | delete this.entityComponentData[comp][entityId]; 342 | if (this.listener) { 343 | // Signal the creation of a new entity. 344 | this.listener.emit('entityComponentRemoved', entityId, comp); 345 | } 346 | } 347 | } 348 | }); 349 | 350 | 351 | return this; 352 | } 353 | 354 | /** 355 | * Return a reference to an object that contains the data of an 356 | * instanciated component of an entity. 357 | * 358 | * @param {int} entityId - Unique identifier of the entity. 359 | * @param {string} componentId - Unique identifier of the component. 360 | * @return {object} - Component data of one entity. 361 | */ 362 | getComponentDataForEntity(componentId, entityId) { 363 | if (!(componentId in this.components)) { 364 | throw new Error('Trying to use unknown component: ' + componentId); 365 | } 366 | 367 | if ( 368 | !this.entityComponentData.hasOwnProperty(componentId) || 369 | !this.entityComponentData[componentId].hasOwnProperty(entityId) 370 | ) { 371 | throw new Error('No data for component ' + componentId + ' and entity ' + entityId); 372 | } 373 | 374 | return this.entityComponentData[componentId][entityId]; 375 | } 376 | 377 | /** 378 | * Update the state of a component, many keys at once. 379 | * 380 | * @param {int} entityId - Unique identifier of the entity. 381 | * @param {string} componentId - Unique identifier of the component. 382 | * @param {object} newState - Object containing the new state to apply. 383 | * @param {boolean} sendUpdateEvent - Optional. True if the method has to send the `COMPONENT_UPDATED` event. 384 | * @return {object} - this 385 | */ 386 | updateComponentDataForEntity(componentId, entityId, newState, sendUpdateEvent = true) { 387 | const compState = this.getComponentDataForEntity(componentId, entityId); 388 | 389 | for (let key in newState) { 390 | if (newState.hasOwnProperty(key) && compState.hasOwnProperty(key)) { 391 | compState[key] = newState[key]; 392 | } 393 | } 394 | 395 | if (sendUpdateEvent) { 396 | this.sendEventToProcessors('COMPONENT_UPDATED', entityId, componentId); 397 | } 398 | 399 | return this; 400 | } 401 | 402 | /** 403 | * Return a dictionary of objects containing the data of all instances of a 404 | * given component. 405 | * 406 | * @param {string} componentId - Unique identifier of the component. 407 | * @return {Object} - Dictionary of entity id -> component data. 408 | */ 409 | getComponentsData(componentId) { 410 | if (!(componentId in this.components)) { 411 | throw new Error('Trying to use unknown component: ' + componentId); 412 | } 413 | 414 | if (!this.entityComponentData.hasOwnProperty(componentId)) { 415 | return {}; 416 | } 417 | 418 | return this.entityComponentData[componentId]; 419 | } 420 | 421 | /** 422 | * Return true if the entity has the component. 423 | * 424 | * @param {int} entityId - Unique identifier of the entity. 425 | * @param {string} componentId - Unique identifier of the component. 426 | * @return {boolean} - True if the entity has the component. 427 | */ 428 | entityHasComponent(entityId, componentId) { 429 | if (!(componentId in this.components)) { 430 | return false; 431 | } 432 | 433 | return ( 434 | this.entityComponentData.hasOwnProperty(componentId) && 435 | this.entityComponentData[componentId].hasOwnProperty(entityId) 436 | ); 437 | } 438 | 439 | //========================================================================= 440 | // ASSEMBLAGES 441 | 442 | /** 443 | * Add an assemblage to the list of known assemblages. 444 | * 445 | * @param {string} id - Unique identifier of the assemblage. 446 | * @param {object} assemblage - An instance of an assemblage to add. 447 | * @return {object} - this 448 | */ 449 | addAssemblage(id, assemblage) { 450 | this.assemblages[id] = assemblage; 451 | return this; 452 | } 453 | 454 | /** 455 | * Add a list of assemblages to known assemblages. 456 | * 457 | * @param {Array} assemblages - An array of assemblages to add. Require that 458 | * each object has a `name` property to use as identifier. 459 | * @return {object} - this 460 | */ 461 | addAssemblages(assemblages) { 462 | assemblages.forEach(a => this.assemblages[a.name] = a); 463 | return this; 464 | } 465 | 466 | /** 467 | * Remove an assemblage from the list of known assemblages. 468 | * 469 | * @param {string} id - Unique identifier of the assemblage. 470 | * @return {object} - this 471 | */ 472 | removeAssemblage(id) { 473 | delete this.assemblages[id]; 474 | return this; 475 | } 476 | 477 | /** 478 | * Create a new entity in the system by creating a new instance of each of 479 | * its components and setting their initial state, using an assemblage. 480 | * 481 | * @param {string} assemblageId - Id of the assemblage to create the entity from. 482 | * @return {int} - Unique identifier of the new entity. 483 | */ 484 | createEntityFromAssemblage(assemblageId) { 485 | if (!(assemblageId in this.assemblages)) { 486 | throw new Error('Trying to use unknown assemblage: ' + assemblageId); 487 | } 488 | 489 | const assemblage = this.assemblages[assemblageId]; 490 | const entity = this.createEntity(assemblage.components, undefined, assemblage.initialState); 491 | 492 | return entity; 493 | } 494 | 495 | //========================================================================= 496 | // PROCESSORS 497 | 498 | /** 499 | * Add a processor to the list of known processors. 500 | * 501 | * @param {object} processor - An instance of a processor to manage. 502 | * @return {object} - this 503 | */ 504 | addProcessor(processor) { 505 | this.processors.push(processor); 506 | return this; 507 | } 508 | 509 | /** 510 | * Add a list of processors to known processors. 511 | * 512 | * @param {Array} processors - An array of processors to manage. 513 | * @return {object} - this 514 | */ 515 | addProcessors(processors) { 516 | processors.forEach(processor => this.processors.push(processor)); 517 | return this; 518 | } 519 | 520 | /** 521 | * Remove a processor from the list of known processors. 522 | * 523 | * @param {object} processor - An instance of a processor to remove. 524 | * @return {object} - this 525 | */ 526 | removeProcessor(processor) { 527 | this.processors.splice(this.processors.indexOf(processor), 1); 528 | return this; 529 | } 530 | 531 | /** 532 | * Send an event to the list of known processors. 533 | * 534 | * @param {string} eventName - Id of the event to send. 535 | * @param {number} entityId - Unique identifier of the entity on which the event occured. 536 | * @param {string} componentId - Unique identifier of the component on which the event occured. 537 | * @return {object} - this 538 | */ 539 | sendEventToProcessors(eventName, entityId, componentId) { 540 | this.processors.forEach(processor => { 541 | if ('on' in processor && typeof processor.on === 'function') { 542 | processor.on(eventName, { 543 | entity: entityId, 544 | component: componentId, 545 | state: this.entityComponentData[componentId][entityId] 546 | }); 547 | } 548 | }); 549 | } 550 | 551 | /** 552 | * Update all the known processors. 553 | * 554 | * @param {int} dt - The time delta since the last call to update. Will be passed as an argument to all processor's `update` method. 555 | * @return {object} - this 556 | */ 557 | update(dt) { 558 | this.processors.forEach(processor => processor.update(dt)); 559 | return this; 560 | } 561 | } 562 | 563 | export default EntityManager; 564 | -------------------------------------------------------------------------------- /test/SpecRunner.js: -------------------------------------------------------------------------------- 1 | // Specs to run unit tests in a browser. 2 | 3 | requirejs.config({ 4 | baseUrl: '..', 5 | paths: { 6 | 'lib': '../lib', 7 | 'mocha': 'node_modules/mocha/mocha', 8 | 'chai': 'node_modules/chai/chai', 9 | }, 10 | }); 11 | 12 | require(['require', 'chai', 'mocha'], function(require, chai) { 13 | /*globals mocha */ 14 | mocha.setup('bdd'); 15 | 16 | require([ 17 | 'test/test_entity-manager', 18 | ], function(require) { 19 | mocha.run(); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/assemblages/soldier.js: -------------------------------------------------------------------------------- 1 | // for compatibility with node.js and require.js 2 | if (typeof define !== 'function') { 3 | var define = require('amdefine')(module) 4 | } 5 | 6 | define({ 7 | name: 'Soldier', 8 | description: 'A special unit that has a position', 9 | components: ['Position', 'Unit'], 10 | initialState: { 11 | 'Position': { 12 | x: 15, 13 | z: -1 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /test/bench/bench-entity-manager.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var EntityManager = require('../../entity-manager'); 3 | 4 | var PositionComponent = require('./../components/position'); 5 | var UnitComponent = require('./../components/unit'); 6 | 7 | 8 | var suite = new Benchmark.Suite('General perf tracking'); 9 | 10 | // add tests 11 | suite.add('EntityManager#general', function() { 12 | var manager = new EntityManager(); 13 | 14 | // Add some components. 15 | manager.addComponent(PositionComponent.name, PositionComponent); 16 | manager.addComponent(UnitComponent.name, UnitComponent); 17 | 18 | // Create a bunch of entities. 19 | for (var i = 0; i < 500; i++) { 20 | var entity = manager.createEntity(['Position', 'Unit']); 21 | manager.updateComponentDataForEntity('Position', entity, { 22 | x: 123, 23 | y: 42, 24 | }); 25 | manager.updateComponentDataForEntity('Unit', entity, { 26 | attack: 23, 27 | }); 28 | }; 29 | 30 | // Create a processor that does dummy things. 31 | var DummyProcessor = function (manager) { 32 | this.manager = manager; 33 | }; 34 | 35 | DummyProcessor.prototype.update = function (dt) { 36 | var units = this.manager.getComponentsData('Unit'); 37 | 38 | for (var i = 0; i < units.length; i++) { 39 | var unit = units[i]; 40 | unit.attack = i; 41 | }; 42 | }; 43 | 44 | manager.addProcessor(new DummyProcessor(manager)); 45 | 46 | manager.update(1); 47 | }) 48 | // add listeners 49 | .on('cycle', function(event) { 50 | console.log(String(event.target)); 51 | }) 52 | .on('complete', function() { 53 | console.log('done'); 54 | }) 55 | .run({ 'async': true }); 56 | -------------------------------------------------------------------------------- /test/bench/bench.js: -------------------------------------------------------------------------------- 1 | 2 | // settings 3 | var numCs = 200 4 | var numEs = 500 5 | var numCsPerE = 50 6 | 7 | 8 | requirejs.config({ 9 | baseUrl: '../..', 10 | }); 11 | 12 | require(['entity-manager'], function (EntityManager) { 13 | var manager = new EntityManager(); 14 | 15 | // randomly create/remove a few components to seed data structures 16 | var cs = [], es = [] 17 | for (var i=0; i<100; ++i) { 18 | var obj = {state: {}} 19 | obj.name = String(Math.random()) 20 | obj.state[String(Math.random())] = 1 21 | cs.push(obj.name) 22 | manager.addComponent( obj.name, obj) 23 | var ent = manager.createEntity([obj.name]) 24 | es.push(ent) 25 | } 26 | for (var i=0; i<100; ++i) { 27 | manager.removeComponent(cs[i]) 28 | manager.removeEntity(es[i]) 29 | } 30 | 31 | // UI 32 | cachedMgr = manager 33 | document.getElementById('testbut').addEventListener('click', onTestButton) 34 | 35 | }) 36 | 37 | 38 | 39 | // benchmark thingy 40 | var cachedMgr 41 | var testing = false 42 | 43 | function onTestButton() { 44 | if (!testing) { 45 | readInputs() 46 | startTest() 47 | out('Running...') 48 | } else { 49 | endTest() 50 | out([ runCt, ' iterations over ', numCs, ' components, ', 51 | numEs, ' entities w/ ', numCsPerE, ' components each', 52 | '
init time ' + initTime.toFixed(2) + ' ms', 53 | '
average time ' + (runTime/runCt).toFixed(2) + ' ms/iteration', 54 | '
checksum: ' + (runningSum/runCt).toFixed(0) + ' sum/iteration' 55 | ].join('')) 56 | } 57 | testing = !testing 58 | document.getElementById('testbut').innerHTML = (testing) ? "Stop" : "Start" 59 | } 60 | function out(s) { 61 | document.getElementById('output').innerHTML = s 62 | } 63 | 64 | 65 | 66 | var Es, compNames, initTime, runTime, runCt, runningSum, iterating 67 | 68 | function startTest() { 69 | var t = performance.now() 70 | var mgr = cachedMgr 71 | compNames = [] 72 | Es = [] 73 | for (var i=0; i0) numCs = nc 132 | if (ne>0) numEs = ne 133 | if (ce>0) numCsPerE = ce 134 | } 135 | 136 | -------------------------------------------------------------------------------- /test/bench/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Benchmark - EntityManager.js 5 | 19 | 20 | 21 | 22 |
23 | Total components 24 |
25 |
26 | Total entities 27 |
28 |
29 | Components per entity 30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/components/position.js: -------------------------------------------------------------------------------- 1 | // for compatibility with node.js and require.js 2 | if (typeof define !== 'function') { 3 | var define = require('amdefine')(module) 4 | } 5 | 6 | define(function () { 7 | /** 8 | * Class PositionComponent 9 | * 10 | * @author Adrian Gaudebert - adrian@gaudebert.fr 11 | * @constructor 12 | */ 13 | var PositionComponent = { 14 | name: 'Position', 15 | description: '3 dimensional coordinates', 16 | 17 | state: { 18 | x: 0, 19 | y: 0, 20 | z: 0, 21 | }, 22 | }; 23 | 24 | return PositionComponent; 25 | }); 26 | -------------------------------------------------------------------------------- /test/components/unit.js: -------------------------------------------------------------------------------- 1 | // for compatibility with node.js and require.js 2 | if (typeof define !== 'function') { 3 | var define = require('amdefine')(module) 4 | } 5 | 6 | define(function () { 7 | /** 8 | * Class UnitComponent 9 | * 10 | * @author Adrian Gaudebert - adrian@gaudebert.fr 11 | * @constructor 12 | */ 13 | var UnitComponent = { 14 | name: 'Unit', 15 | description: 'A basic unit that can attack, defend and move', 16 | 17 | state: { 18 | attack: 10, 19 | defense: 5, 20 | speed: 1, 21 | range: 2, 22 | }, 23 | }; 24 | 25 | return UnitComponent; 26 | }); 27 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests - EntityManager.js 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/test_entity-manager.js: -------------------------------------------------------------------------------- 1 | /* globals describe require module it */ 2 | 3 | /** 4 | * Test cases for EntityManager class. 5 | * 6 | * Requires node.js and its mocha module. 7 | * To run those tests: nodeunit tests/test_entity-manager.js 8 | * 9 | * @author Adrian Gaudebert - adrian@gaudebert.fr 10 | */ 11 | 12 | // for compatibility with node.js and require.js 13 | if (typeof define !== 'function') { 14 | var define = require('amdefine')(module); 15 | } 16 | 17 | define(function (require) { 18 | var chai = require('chai'); 19 | var expect = chai.expect; 20 | 21 | var EntityManager = require('../entity-manager'); 22 | var PositionComponent = require('./components/position'); 23 | var UnitComponent = require('./components/unit'); 24 | var SoldierAssemblage = require('./assemblages/soldier'); 25 | 26 | function prepareManager(listener) { 27 | var manager = new EntityManager(listener); 28 | manager.addComponent('Position', PositionComponent); 29 | manager.addComponent('Unit', UnitComponent); 30 | return manager; 31 | } 32 | 33 | describe('EntityManager', function () { 34 | 35 | //===================================================================== 36 | // ENTITIES 37 | 38 | describe('#createEntity()', function () { 39 | it('can create a new single-component entity', function () { 40 | var manager = prepareManager(); 41 | 42 | var entity = manager.createEntity(['Position']); 43 | var data = manager.getComponentDataForEntity('Position', entity); 44 | 45 | // Testing default values 46 | expect(data.x).to.equal(0); 47 | expect(data.y).to.equal(0); 48 | expect(data.z).to.equal(0); 49 | expect(data.attack).to.be.undefined; 50 | }); 51 | 52 | it('can create a new single-component entity with an initial state', function () { 53 | var manager = prepareManager(); 54 | 55 | var entity = manager.createEntity(['Position'], undefined, {Position: { 56 | x: 1, 57 | z: 3 58 | }}); 59 | var data = manager.getComponentDataForEntity('Position', entity); 60 | 61 | // Testing default values 62 | expect(data.x).to.equal(1); 63 | expect(data.y).to.equal(0); 64 | expect(data.z).to.equal(3); 65 | expect(data.attack).to.be.undefined; 66 | }); 67 | 68 | it('can create a new entity with several components', function () { 69 | var manager = prepareManager(); 70 | 71 | var entity = manager.createEntity(['Position', 'Unit']); 72 | var dataPos = manager.getComponentDataForEntity('Position', entity); 73 | var dataUnit = manager.getComponentDataForEntity('Unit', entity); 74 | 75 | expect(dataPos.x).to.equal(0); 76 | expect(dataPos.y).to.equal(0); 77 | expect(dataPos.z).to.equal(0); 78 | expect(dataPos.attack).to.be.undefined; 79 | expect(dataPos.unknownattribute).to.be.undefined; 80 | 81 | expect(dataUnit.attack).to.equal(10); 82 | expect(dataUnit.speed).to.equal(1); 83 | expect(dataUnit.x).to.be.undefined; 84 | }); 85 | 86 | it('throws an exception when creating an entity with unknown components', function () { 87 | var manager = prepareManager(); 88 | 89 | var fn = function() { 90 | manager.createEntity(['UnknownComponent']); 91 | }; 92 | 93 | expect(fn).to.throw('Trying to use unknown component: UnknownComponent'); 94 | }); 95 | 96 | it('can create an entity with a specific UID', function () { 97 | var manager = prepareManager(); 98 | var newEntityId = 42; 99 | 100 | var entity = manager.createEntity(['Position'], newEntityId); 101 | expect(entity).to.equal(newEntityId); 102 | 103 | // Testing default values 104 | var data = manager.getComponentDataForEntity('Position', entity); 105 | expect(data.x).to.equal(0); 106 | 107 | // Verify the internal UID number has changed to avoid conflicts. 108 | expect(manager.uid).to.equal(newEntityId); 109 | }); 110 | 111 | it('can override an existing entity', function () { 112 | var manager = prepareManager(); 113 | var newEntityId = 42; 114 | 115 | var entity = manager.createEntity(['Position'], newEntityId); 116 | expect(entity).to.equal(newEntityId); 117 | expect(manager.entityHasComponent(entity, 'Position')).to.be.true; 118 | expect(manager.entityHasComponent(entity, 'Unit')).to.be.false; 119 | 120 | entity = manager.createEntity(['Position', 'Unit'], newEntityId); 121 | expect(entity).to.equal(newEntityId); 122 | expect(manager.entityHasComponent(entity, 'Position')).to.be.true; 123 | expect(manager.entityHasComponent(entity, 'Unit')).to.be.true; 124 | }); 125 | 126 | it('sends the create event from manager to processors', function () { 127 | var manager = prepareManager(); 128 | var newEntityId = 42; 129 | 130 | var processor = { 131 | entities: [], 132 | on: function (type, data) { 133 | switch (type) { 134 | case 'COMPONENT_CREATED': 135 | this.entities.push(data.entity); 136 | break; 137 | default: 138 | break; 139 | } 140 | }, 141 | update: function () {}, 142 | }; 143 | manager.addProcessor(processor); 144 | 145 | var entity = manager.createEntity(['Position'], newEntityId); 146 | 147 | expect(manager.processors).to.have.length(1); 148 | expect(manager.processors[0]).to.equal(processor); 149 | expect(manager.processors[0].entities).to.have.length(1); 150 | expect(manager.processors[0].entities[0]).to.equal(newEntityId); 151 | }); 152 | 153 | it('sends the update event from manager to processors', function () { 154 | var manager = prepareManager(); 155 | var newEntityId = 42; 156 | 157 | var processor = { 158 | entities: [], 159 | on: function (type, data) { 160 | switch (type) { 161 | case 'COMPONENT_UPDATED': 162 | this.entities.push(data.entity); 163 | break; 164 | default: 165 | break; 166 | } 167 | }, 168 | update: function () {}, 169 | }; 170 | manager.addProcessor(processor); 171 | 172 | var entity = manager.createEntity(['Position'], newEntityId); 173 | 174 | expect(manager.processors).to.have.length(1); 175 | expect(manager.processors[0]).to.equal(processor); 176 | expect(manager.processors[0].entities).to.have.length(0); 177 | 178 | manager.updateComponentDataForEntity('Position', entity, { 179 | x: 12, 180 | }); 181 | 182 | expect(manager.processors[0].entities).to.have.length(1); 183 | expect(manager.processors[0].entities[0]).to.equal(newEntityId); 184 | }); 185 | 186 | it('does not sends the update event from manager to processors', function () { 187 | var manager = prepareManager(); 188 | var newEntityId = 42; 189 | 190 | var processor = { 191 | entities: [], 192 | on: function (type, data) { 193 | switch (type) { 194 | case 'COMPONENT_UPDATED': 195 | this.entities.push(data.entity); 196 | break; 197 | default: 198 | break; 199 | } 200 | }, 201 | update: function () {}, 202 | }; 203 | manager.addProcessor(processor); 204 | 205 | var entity = manager.createEntity(['Position'], newEntityId); 206 | 207 | expect(manager.processors).to.have.length(1); 208 | expect(manager.processors[0]).to.equal(processor); 209 | expect(manager.processors[0].entities).to.have.length(0); 210 | 211 | manager.updateComponentDataForEntity('Position', entity, {x: 12}, false); 212 | 213 | expect(manager.processors[0].entities).to.have.length(0); 214 | }); 215 | 216 | it('does not sends the update event from manager to processors when creating an entity with inital state', function () { 217 | var manager = prepareManager(); 218 | var newEntityId = 42; 219 | 220 | var processor = { 221 | entities: [], 222 | on: function (type, data) { 223 | switch (type) { 224 | case 'COMPONENT_UPDATED': 225 | this.entities.push(data.entity); 226 | break; 227 | default: 228 | break; 229 | } 230 | }, 231 | update: function () {}, 232 | }; 233 | manager.addProcessor(processor); 234 | 235 | var entity = manager.createEntity(['Position'], newEntityId, {Position: { 236 | x: 1, 237 | z: 3 238 | }}); 239 | 240 | var data = manager.getComponentDataForEntity('Position', entity); 241 | 242 | // Testing default values 243 | expect(data.x).to.equal(1); 244 | expect(data.z).to.equal(3); 245 | expect(manager.processors).to.have.length(1); 246 | expect(manager.processors[0]).to.equal(processor); 247 | expect(manager.processors[0].entities).to.have.length(0); 248 | }); 249 | }); 250 | 251 | describe('#removeEntity()', function () { 252 | it('can remove an existing entity', function () { 253 | var manager = prepareManager(); 254 | var entity = manager.createEntity(['Position', 'Unit']); 255 | 256 | // Verify all data was correctly created. 257 | var data = manager.getComponentDataForEntity('Position', entity); 258 | expect(data).to.be.instanceOf(Object); 259 | 260 | data = manager.getComponentDataForEntity('Unit', entity); 261 | expect(data).to.be.instanceOf(Object); 262 | 263 | manager.removeEntity(entity); 264 | 265 | // Verify all data does not exist anymore. 266 | expect(function () { 267 | manager.getComponentDataForEntity('Position', entity); 268 | }).to.throw('No data for component Position and entity ' + entity); 269 | 270 | expect(function () { 271 | manager.getComponentDataForEntity('Unit', entity); 272 | }).to.throw('No data for component Unit and entity ' + entity); 273 | 274 | expect(manager.entities).to.not.include(entity); 275 | }); 276 | 277 | it('can remove several existing entities', function () { 278 | var manager = prepareManager(); 279 | var entity1 = manager.createEntity(['Position']); 280 | var entity2 = manager.createEntity(['Position']); 281 | var entity3 = manager.createEntity(['Position']); 282 | 283 | manager.removeEntity(entity1); 284 | manager.removeEntity(entity3); 285 | 286 | var fn1 = function () { 287 | manager.getComponentDataForEntity('Position', entity1); 288 | }; 289 | 290 | var fn3 = function () { 291 | manager.getComponentDataForEntity('Position', entity3); 292 | }; 293 | 294 | expect(fn1).to.throw('No data for component Position and entity ' + entity1); 295 | expect(fn3).to.throw('No data for component Position and entity ' + entity3); 296 | 297 | expect(manager.getComponentDataForEntity('Position', entity2)).to.be.ok; 298 | 299 | expect(manager.entities).to.not.include(entity1); 300 | expect(manager.entities).to.not.include(entity3); 301 | expect(manager.entities).to.include(entity2); 302 | }); 303 | }); 304 | 305 | //===================================================================== 306 | // COMPONENTS 307 | 308 | describe('#addComponent()', function () { 309 | it('can add new components', function () { 310 | var manager = new EntityManager(); 311 | 312 | manager.addComponent('Position', PositionComponent); 313 | expect(manager.getComponentsList()).to.deep.equal(['Position']); 314 | 315 | manager.addComponent('Unit', UnitComponent); 316 | expect(manager.getComponentsList()).to.deep.equal(['Position', 'Unit']); 317 | }); 318 | }); 319 | 320 | describe('#addComponents()', function () { 321 | it('can add a list of components at once', function () { 322 | var manager = new EntityManager(); 323 | 324 | manager.addComponents([PositionComponent, UnitComponent]); 325 | expect(manager.getComponentsList()).to.deep.equal(['Position', 'Unit']); 326 | }); 327 | }); 328 | 329 | describe('#removeComponent()', function () { 330 | it('can remove an existing component', function () { 331 | var manager = prepareManager(); 332 | expect(manager.getComponentsList()).to.deep.equal(['Position', 'Unit']); 333 | 334 | manager.removeComponent('Position'); 335 | expect(manager.getComponentsList()).to.deep.equal(['Unit']); 336 | }); 337 | 338 | it('does not throw an error when trying to remove an unknown component', function () { 339 | var manager = prepareManager(); 340 | expect(manager.getComponentsList()).to.deep.equal(['Position', 'Unit']); 341 | 342 | manager.removeComponent('UnknownComponent'); 343 | expect(manager.getComponentsList()).to.deep.equal(['Position', 'Unit']); 344 | }); 345 | }); 346 | 347 | describe('#addComponentsToEntity()', function () { 348 | it('adds the entity\'s ID to the state', function () { 349 | var manager = prepareManager(); 350 | var entity = manager.createEntity(['Position']); 351 | 352 | manager.addComponentsToEntity(['Unit'], entity); 353 | 354 | var dataPos = manager.getComponentDataForEntity('Position', entity); 355 | expect(dataPos.__id).to.exist; 356 | expect(dataPos.__id).to.equal(entity); 357 | 358 | dataPos = manager.getComponentDataForEntity('Unit', entity); 359 | expect(dataPos.__id).to.exist; 360 | expect(dataPos.__id).to.equal(entity); 361 | }); 362 | 363 | it('can add components to an existing entity', function () { 364 | var manager = prepareManager(); 365 | var entity = manager.createEntity(['Position']); 366 | 367 | manager.addComponentsToEntity(['Unit'], entity); 368 | 369 | var dataPos = manager.getComponentDataForEntity('Position', entity); 370 | expect(dataPos.x).to.equal(0); 371 | expect(dataPos.attack).to.be.undefined; 372 | 373 | var dataUnit = manager.getComponentDataForEntity('Unit', entity); 374 | expect(dataUnit.attack).to.equal(10); 375 | expect(dataUnit.x).to.be.undefined; 376 | }); 377 | 378 | it('handles state changes correctly', function () { 379 | var manager = prepareManager(); 380 | var entity1 = manager.createEntity(['Position', 'Unit']); 381 | var entity2 = manager.createEntity(['Position', 'Unit']); 382 | var entity3 = manager.createEntity(['Position']); 383 | 384 | var dataPos = manager.getComponentDataForEntity('Position', entity1); 385 | expect(dataPos.x).to.equal(0); 386 | expect(dataPos.y).to.equal(0); 387 | expect(dataPos.z).to.equal(0); 388 | 389 | dataPos.x = 23; 390 | 391 | expect(dataPos.x).to.equal(23); 392 | expect(dataPos.y).to.equal(0); 393 | expect(dataPos.z).to.equal(0); 394 | 395 | dataPos.z = 32; 396 | 397 | expect(dataPos.x).to.equal(23); 398 | expect(dataPos.y).to.equal(0); 399 | expect(dataPos.z).to.equal(32); 400 | 401 | var dataUnit = manager.getComponentDataForEntity('Unit', entity1); 402 | expect(dataUnit.attack).to.equal(10); 403 | expect(dataUnit.defense).to.equal(5); 404 | expect(dataUnit.speed).to.equal(1); 405 | expect(dataUnit.range).to.equal(2); 406 | 407 | dataUnit.speed = 3; 408 | 409 | expect(dataUnit.attack).to.equal(10); 410 | expect(dataUnit.defense).to.equal(5); 411 | expect(dataUnit.speed).to.equal(3); 412 | expect(dataUnit.range).to.equal(2); 413 | 414 | var dataPos2 = manager.getComponentDataForEntity('Position', entity2); 415 | expect(dataPos2.x).to.equal(0); 416 | expect(dataPos2.y).to.equal(0); 417 | expect(dataPos2.z).to.equal(0); 418 | 419 | var dataPos3 = manager.getComponentDataForEntity('Position', entity3); 420 | expect(dataPos3.x).to.equal(0); 421 | expect(dataPos3.y).to.equal(0); 422 | expect(dataPos3.z).to.equal(0); 423 | }); 424 | 425 | it('emits the right component id', function () { 426 | // Mock a listener object. 427 | var listener = { 428 | events: [], 429 | emit: function(e, arg1, arg2) { 430 | this.events.push([e, arg1, arg2]); 431 | }, 432 | }; 433 | 434 | var manager = prepareManager(listener); 435 | var entity = manager.createEntity(['Position', 'Unit']); 436 | 437 | manager.updateComponentDataForEntity('Position', entity, { 438 | x: 12, 439 | }); 440 | expect(listener.events.pop()).to.deep.equal(['entityComponentUpdated', entity, 'Position']); 441 | 442 | manager.updateComponentDataForEntity('Unit', entity, { 443 | speed: 1, 444 | }); 445 | expect(listener.events.pop()).to.deep.equal(['entityComponentUpdated', entity, 'Unit']); 446 | }); 447 | 448 | it('emits an event when a state changes', function () { 449 | // Mock a listener object. 450 | var listener = { 451 | events: [], 452 | emit: function(e, arg1, arg2) { 453 | this.events.push([e, arg1, arg2]); 454 | }, 455 | }; 456 | 457 | var manager = prepareManager(listener); 458 | 459 | var entity = manager.createEntity(['Position']); 460 | manager.addComponentsToEntity(['Unit'], entity); 461 | 462 | var dataPos = manager.getComponentDataForEntity('Position', entity); 463 | 464 | // Verify we have 3 events beforehand. 465 | expect(listener.events.length).to.equal(3); 466 | dataPos.x = 43; 467 | 468 | expect(listener.events.length).to.equal(4); 469 | expect(listener.events[3]).to.deep.equal(['entityComponentUpdated', entity, 'Position']); 470 | 471 | // Reinitialize listener object. 472 | listener.events = []; 473 | 474 | var dataUnit = manager.getComponentDataForEntity('Unit', entity); 475 | dataUnit.speed = 44; 476 | dataUnit.attack = 42; 477 | dataUnit.attack = 4; 478 | 479 | expect(listener.events.length).to.equal(3); 480 | expect(listener.events[0]).to.deep.equal(['entityComponentUpdated', entity, 'Unit']); 481 | expect(listener.events[1]).to.deep.equal(['entityComponentUpdated', entity, 'Unit']); 482 | expect(listener.events[2]).to.deep.equal(['entityComponentUpdated', entity, 'Unit']); 483 | }); 484 | 485 | it('works with no emit function in the manager', function () { 486 | var manager = prepareManager(); 487 | 488 | // Remove the emit function. 489 | manager.emit = null; 490 | 491 | var entity = manager.createEntity(['Position']); 492 | manager.addComponentsToEntity(['Unit'], entity); 493 | 494 | var dataPos = manager.getComponentDataForEntity('Position', entity); 495 | expect(dataPos.x).to.equal(0); 496 | dataPos.x = 43; 497 | expect(dataPos.x).to.equal(43); 498 | }); 499 | }); 500 | 501 | describe('#removeComponentsFromEntity()', function () { 502 | it('removes one component from an existing entity', function () { 503 | var manager = prepareManager(); 504 | var entity = manager.createEntity(['Position', 'Unit']); 505 | 506 | manager.removeComponentsFromEntity(['Unit'], entity); 507 | 508 | // Verify the 'Position' component is still here. 509 | var dataPos = manager.getComponentDataForEntity('Position', entity); 510 | expect(dataPos).to.be.instanceOf(Object); 511 | 512 | // Verify the 'Unit' component has been removed. 513 | expect(function () { 514 | manager.getComponentDataForEntity('Unit', entity); 515 | }).to.throw('No data for component Unit and entity ' + entity); 516 | }); 517 | 518 | it('removes several components from an existing entity', function () { 519 | var manager = prepareManager(); 520 | var entity = manager.createEntity(['Position', 'Unit']); 521 | 522 | manager.removeComponentsFromEntity(['Position', 'Unit'], entity); 523 | 524 | // Verify the 'Position' component has been removed. 525 | expect(function () { 526 | manager.getComponentDataForEntity('Position', entity); 527 | }).to.throw('No data for component Position and entity ' + entity); 528 | 529 | // Verify the 'Unit' component has been removed. 530 | expect(function () { 531 | manager.getComponentDataForEntity('Unit', entity); 532 | }).to.throw('No data for component Unit and entity ' + entity); 533 | }); 534 | }); 535 | 536 | describe('#getComponentDataForEntity()', function () { 537 | it('returns the correct data object', function () { 538 | var manager = prepareManager(); 539 | 540 | var entity = manager.createEntity(['Position', 'Unit']); 541 | var dataPos = manager.getComponentDataForEntity('Position', entity); 542 | var dataUnit = manager.getComponentDataForEntity('Unit', entity); 543 | 544 | expect(dataPos.x).to.equal(0); 545 | expect(dataPos.y).to.equal(0); 546 | expect(dataPos.z).to.equal(0); 547 | expect(dataPos.attack).to.be.undefined; 548 | 549 | dataPos.x = 12; 550 | expect(dataPos.x).to.equal(12); 551 | 552 | dataPos.x++; 553 | expect(dataPos.x).to.equal(13); 554 | 555 | expect(dataUnit.attack).to.equal(10); 556 | expect(dataUnit.speed).to.equal(1); 557 | expect(dataUnit.x).to.be.undefined; 558 | 559 | dataUnit.speed = 2.5; 560 | expect(dataUnit.speed).to.equal(2.5); 561 | }); 562 | 563 | it('throws an error when the component does not exist', function () { 564 | var manager = prepareManager(); 565 | var entity = manager.createEntity(['Position', 'Unit']); 566 | 567 | var fn = function () { 568 | manager.getComponentDataForEntity('UnknownComponent', entity); 569 | }; 570 | 571 | expect(fn).to.throw('Trying to use unknown component: UnknownComponent'); 572 | }); 573 | 574 | it('throws an error when the component has no data', function () { 575 | var manager = prepareManager(); 576 | var entity = manager.createEntity(['Position']); 577 | 578 | var fn = function () { 579 | manager.getComponentDataForEntity('Unit', entity); 580 | }; 581 | 582 | expect(fn).to.throw('No data for component Unit and entity ' + entity); 583 | }); 584 | 585 | it('throws an error when the entity does not contain the component', function () { 586 | var manager = prepareManager(); 587 | var entity = manager.createEntity(['Position']); 588 | 589 | // Create an entity with the Unit component so it has data. 590 | manager.createEntity(['Unit']); 591 | 592 | var fn = function () { 593 | manager.getComponentDataForEntity('Unit', entity); 594 | }; 595 | 596 | expect(fn).to.throw('No data for component Unit and entity ' + entity); 597 | }); 598 | }); 599 | 600 | describe('#updateComponentDataForEntity()', function () { 601 | it('updates the data correctly', function () { 602 | var manager = prepareManager(); 603 | 604 | var entity = manager.createEntity(['Position', 'Unit']); 605 | var dataPos = manager.getComponentDataForEntity('Position', entity); 606 | var dataUnit = manager.getComponentDataForEntity('Unit', entity); 607 | 608 | expect(dataPos.x).to.equal(0); 609 | expect(dataPos.y).to.equal(0); 610 | expect(dataPos.z).to.equal(0); 611 | expect(dataPos.attack).to.be.undefined; 612 | 613 | manager.updateComponentDataForEntity('Position', entity, {x: 12}); 614 | 615 | expect(dataPos.x).to.equal(12); 616 | 617 | expect(dataUnit.attack).to.equal(10); 618 | expect(dataUnit.speed).to.equal(1); 619 | expect(dataUnit.x).to.be.undefined; 620 | 621 | manager.updateComponentDataForEntity('Unit', entity, {attack: 12, speed: 2.5, x: 1}); 622 | 623 | expect(dataUnit.attack).to.equal(12); 624 | expect(dataUnit.speed).to.equal(2.5); 625 | expect(dataUnit.x).to.be.undefined; 626 | }); 627 | 628 | it('throws an error when the component does not exist', function () { 629 | var manager = prepareManager(); 630 | var entity = manager.createEntity(['Position', 'Unit']); 631 | 632 | var fn = function () { 633 | manager.updateComponentDataForEntity('UnknownComponent', entity, {}); 634 | }; 635 | 636 | expect(fn).to.throw('Trying to use unknown component: UnknownComponent'); 637 | }); 638 | 639 | it('throws an error when the component has no data', function () { 640 | var manager = prepareManager(); 641 | var entity = manager.createEntity(['Position']); 642 | 643 | var fn = function () { 644 | manager.updateComponentDataForEntity('Unit', entity, {}); 645 | }; 646 | 647 | expect(fn).to.throw('No data for component Unit and entity ' + entity); 648 | }); 649 | 650 | it('throws an error when the entity does not contain the component', function () { 651 | var manager = prepareManager(); 652 | var entity = manager.createEntity(['Position']); 653 | 654 | // Create an entity with the Unit component so it has data. 655 | manager.createEntity(['Unit']); 656 | 657 | var fn = function () { 658 | manager.updateComponentDataForEntity('Unit', entity, {}); 659 | }; 660 | 661 | expect(fn).to.throw('No data for component Unit and entity ' + entity); 662 | }); 663 | }); 664 | 665 | describe('#getComponentsData()', function () { 666 | it('returns the correct array', function () { 667 | var manager = prepareManager(); 668 | 669 | manager.createEntity(['Position']); 670 | manager.createEntity(['Position']); 671 | manager.createEntity(['Position']); 672 | manager.createEntity(['Unit']); 673 | 674 | var allPositions = manager.getComponentsData('Position'); 675 | expect(Object.keys(allPositions)).to.have.length(3); 676 | 677 | var allUnits = manager.getComponentsData('Unit'); 678 | expect(Object.keys(allUnits)).to.have.length(1); 679 | }); 680 | 681 | it('throws an error when the component does not exist', function () { 682 | var manager = prepareManager(); 683 | manager.createEntity(['Position', 'Unit']); 684 | 685 | var fn = function () { 686 | manager.getComponentsData('UnknownComponent'); 687 | }; 688 | 689 | expect(fn).to.throw('Trying to use unknown component: UnknownComponent'); 690 | }); 691 | 692 | it('returns an empty object when the component has no data', function () { 693 | var manager = prepareManager(); 694 | manager.createEntity(['Position']); 695 | 696 | var components = manager.getComponentsData('Unit'); 697 | 698 | expect(components).to.be.an('object').that.is.empty; 699 | }); 700 | }); 701 | 702 | describe('#entityHasComponent()', function () { 703 | it('returns true when an entity has a component', function () { 704 | var manager = prepareManager(); 705 | var entity = manager.createEntity(['Position']); 706 | 707 | expect(manager.entityHasComponent(entity, 'Position')).to.be.true; 708 | expect(manager.entityHasComponent(entity, 'Unit')).to.be.false; 709 | }); 710 | }); 711 | 712 | //===================================================================== 713 | // ASSEMBLAGES 714 | 715 | describe('#addAssemblage()', function () { 716 | it('can add new assemblages', function () { 717 | var manager = new EntityManager(); 718 | 719 | manager.addAssemblage('Soldier', SoldierAssemblage); 720 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Soldier']); 721 | 722 | var someAssemblage = { 723 | components: ['Position'], 724 | initialState: {}, 725 | }; 726 | 727 | manager.addAssemblage('Something', someAssemblage); 728 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Soldier', 'Something']); 729 | }); 730 | }); 731 | 732 | describe('#addAssemblages()', function () { 733 | it('can add a list of assemblages', function () { 734 | var manager = new EntityManager(); 735 | 736 | var FakeAssemblage = { 737 | name: 'Fake', 738 | }; 739 | manager.addAssemblages([SoldierAssemblage, FakeAssemblage]); 740 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Soldier', 'Fake']); 741 | }); 742 | }); 743 | 744 | describe('#removeAssemblage()', function () { 745 | it('can remove an existing assemblage', function () { 746 | var manager = prepareManager(); 747 | var someAssemblage = { 748 | components: ['Position'], 749 | initialState: {}, 750 | }; 751 | 752 | manager.addAssemblage('Soldier', SoldierAssemblage); 753 | manager.addAssemblage('Something', someAssemblage); 754 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Soldier', 'Something']); 755 | 756 | manager.removeAssemblage('Soldier'); 757 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Something']); 758 | 759 | manager.removeAssemblage('Something'); 760 | expect(Object.keys(manager.assemblages)).to.have.length(0); 761 | }); 762 | 763 | it('does not throw an error when trying to remove an unknown assemblage', function () { 764 | var manager = prepareManager(); 765 | manager.addAssemblage('Soldier', SoldierAssemblage); 766 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Soldier']); 767 | 768 | manager.removeAssemblage('Unknown'); 769 | expect(Object.keys(manager.assemblages)).to.deep.equal(['Soldier']); 770 | }); 771 | }); 772 | 773 | describe('#createEntityFromAssemblage()', function () { 774 | it('correctly creates a new entity from an assemblage', function () { 775 | var manager = prepareManager(); 776 | manager.addAssemblage('Soldier', SoldierAssemblage); 777 | 778 | var entity = manager.createEntityFromAssemblage('Soldier'); 779 | 780 | var dataPos = manager.getComponentDataForEntity('Position', entity); 781 | var dataUnit = manager.getComponentDataForEntity('Unit', entity); 782 | 783 | expect(dataPos.x).to.equal(15); 784 | expect(dataPos.y).to.equal(0); 785 | expect(dataPos.z).to.equal(-1); 786 | expect(dataPos.attack).to.be.undefined; 787 | expect(dataPos.unknownattribute).to.be.undefined; 788 | 789 | expect(dataUnit.attack).to.equal(10); 790 | expect(dataUnit.speed).to.equal(1); 791 | expect(dataUnit.x).to.be.undefined; 792 | }); 793 | }); 794 | 795 | //===================================================================== 796 | // PROCESSORS 797 | 798 | describe('#addProcessor()', function () { 799 | it('adds the processor to the list of processors', function () { 800 | var manager = prepareManager(); 801 | expect(manager.processors).to.have.length(0); 802 | 803 | var processor = { 804 | update: function () {}, 805 | }; 806 | manager.addProcessor(processor); 807 | 808 | expect(manager.processors).to.have.length(1); 809 | }); 810 | }); 811 | 812 | describe('#addProcessors()', function () { 813 | it('adds a list of processors', function () { 814 | var manager = prepareManager(); 815 | expect(manager.processors).to.have.length(0); 816 | 817 | var processorA = { 818 | update: function () {}, 819 | }; 820 | var processorB = { 821 | update: function () {}, 822 | }; 823 | manager.addProcessors([processorA, processorB]); 824 | 825 | expect(manager.processors).to.have.length(2); 826 | }); 827 | }); 828 | 829 | describe('#removeProcessor()', function () { 830 | it('removes the processor from the list of processors', function () { 831 | var manager = prepareManager(); 832 | expect(manager.processors).to.have.length(0); 833 | 834 | var processor1 = { 835 | update: function () {}, 836 | }; 837 | manager.addProcessor(processor1); 838 | 839 | var processor2 = { 840 | update: function () {}, 841 | }; 842 | manager.addProcessor(processor2); 843 | 844 | expect(manager.processors).to.have.length(2); 845 | 846 | manager.removeProcessor(processor1); 847 | expect(manager.processors).to.have.length(1); 848 | expect(manager.processors).to.deep.equal([processor2]); 849 | }); 850 | }); 851 | 852 | describe('#update()', function () { 853 | it('updates all the processors in the list', function () { 854 | var manager = prepareManager(); 855 | 856 | var callCount1 = 0; 857 | var callCount2 = 0; 858 | 859 | var processor1 = { 860 | update: function () { 861 | callCount1++; 862 | }, 863 | }; 864 | manager.addProcessor(processor1); 865 | manager.update(); 866 | 867 | expect(callCount1).to.equal(1); 868 | expect(callCount2).to.equal(0); 869 | 870 | var processor2 = { 871 | update: function () { 872 | callCount2++; 873 | }, 874 | }; 875 | manager.addProcessor(processor2); 876 | manager.update(); 877 | 878 | expect(callCount1).to.equal(2); 879 | expect(callCount2).to.equal(1); 880 | 881 | manager.removeProcessor(processor1); 882 | manager.update(); 883 | 884 | expect(callCount1).to.equal(2); 885 | expect(callCount2).to.equal(2); 886 | }); 887 | }); 888 | }); 889 | }); 890 | --------------------------------------------------------------------------------