diff --git a/.esdoc.json b/.esdoc.json index 8a45c25..b9e48d8 100644 --- a/.esdoc.json +++ b/.esdoc.json @@ -1,41 +1,37 @@ { - "source": "./src", - "destination": "./docs", - "plugins": [ - { - "name": "esdoc-ecmascript-proposal-plugin", - "option": { - "all": true - } - }, - { - "name": "esdoc-undocumented-identifier-plugin", - "option": { - "enable": true - } - }, - { - "name": "esdoc-unexported-identifier-plugin", - "option": { - "enable": true - } - }, - { - "name": "esdoc-standard-plugin", - "option": { - "undocumentIdentifier": { - "enable": false - }, - "unexportedIdentifier": { - "enable": true - } - } - } - ], - "includes": [ - "./*.js" - ], - "excludes": [ - "(test|benchmarks)" - ] -} \ No newline at end of file + "source": "./src", + "destination": "./docs", + "plugins": [ + { + "name": "esdoc-ecmascript-proposal-plugin", + "option": { + "all": true + } + }, + { + "name": "esdoc-undocumented-identifier-plugin", + "option": { + "enable": true + } + }, + { + "name": "esdoc-unexported-identifier-plugin", + "option": { + "enable": true + } + }, + { + "name": "esdoc-standard-plugin", + "option": { + "undocumentIdentifier": { + "enable": false + }, + "unexportedIdentifier": { + "enable": true + } + } + } + ], + "includes": ["./*.js"], + "excludes": ["(test|benchmarks)"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2f0dc55 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +docs/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f37100d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "es5", + "semi": false, + "useTabs": false, + "arrowParens": "avoid" +} diff --git a/.travis.yml b/.travis.yml index f8b9b7d..6c32c1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "10" + - '10' branches: only: - - master \ No newline at end of file + - master diff --git a/README.md b/README.md index 22057fd..c7e9adc 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ ### Table Of Contents -- [About](#about) - - [Features](#features) - - [Terminology](#terminology) - - [License](#license) - - [Author](#author) -- [Instructions](#instructions) - - [Setup](#setup) - - [Documentation](#documentation) - - [Examples](#examples) +- [About](#about) + - [Features](#features) + - [Terminology](#terminology) + - [License](#license) + - [Author](#author) +- [Instructions](#instructions) + - [Setup](#setup) + - [Documentation](#documentation) + - [Examples](#examples) ## About @@ -24,37 +24,29 @@ This entity system is designed to be as simple as possible, while still having u ### Features -- **Simple query syntax** - - `world.each('a', 'b', ({a, b}) => { a.foo = b.bar })` - - See the examples below for more advanced usage, or the [reference docs](https://ayebear.com/picoes/class/src/world.js~World.html#instance-method-each) -- **No formal declarations required** - - Can create components and entities in a world and query on them, without needing to define structured systems and components -- **Strings as component keys** - - No need to manually define component keys, or manually include component classes to use them -- **Automatic dependency injection for systems** - - No need to pass state to each system, can have a single context that gets injected into all systems automatically -- **High performance indexing options** - - SimpleIndex (Default): O(1) component add/remove, O(m) query time - - Where `m` is the smallest size component index - - MemoizedQueryIndex: O(q) component add/remove, O(1) average query time (memoized), O(n) worst query time (initial) - - Where `q` is the total number of memoized queries - - And `n` is the total number of entities - - _Note: Above time complexities are amortized assuming the number of components used is a known constant_ - - Can also write your own and pass it to the World constructor! Needs clear, add, remove, and query. -- **Prototypes** - - Allows entity definitions to be data-driven, outside of code +- **Simple query syntax** + - `world.each('a', 'b', ({a, b}) => { a.foo = b.bar })` + - See the examples below for more advanced usage, or the [reference docs](https://ayebear.com/picoes/class/src/world.js~World.html#instance-method-each) +- **No formal declarations required** + - Can create **unlimited** (within memory limits) components and entities in a world and query on them, without needing to define structured systems and components +- **Strings as component keys** + - No need to manually define component keys, or manually include component classes to use them +- **Automatic dependency injection for systems** + - No need to pass state to each system, can have a single context that gets injected into all systems automatically +- **Balanced performance** + - See [ECS benchmark comparison](https://github.com/noctjs/ecs-benchmark) + - Entity/Component adding/removing performance is decent with PicoES, which is important for many games. + - Active research and work is being done to significantly improve PicoES performance as much as possible without making it harder to use. ### Terminology -- **Component:** Holds some related data - - Example: Position, Velocity, Health -- **Entity:** Refers to a collection of components - - Example: Position + Health could represent a player -- **Prototype:** A template of components used for creating entities - - Example: Player could contain Position, Velocity, and Health -- **System:** Logic loop that processes entities - - Example: Movement system which handles positions and velocities -- **World:** Lets you register components, systems, and prototypes in a self-contained object - which avoids the use of singletons. This is also where you can create entities from. +- **Component:** Holds some related data + - Example: Position, Velocity, Health +- **Entity:** Refers to a collection of components + - Example: Position + Health could represent a player +- **System:** Logic loop that processes entities + - Example: Movement system which handles positions and velocities +- **World:** The entry point of all PicoES features. Can register components/systems and create/query entities in a self-contained object - which avoids the use of singletons. ### License @@ -84,6 +76,8 @@ npm i -D picoes ### Documentation +The full reference documentation can be found here: + [PicoES Documentation](https://ayebear.com/picoes) ### Examples @@ -91,129 +85,25 @@ npm i -D picoes #### Shorthand anonymous components and systems ```javascript -// import { World } from 'picoes' -const { World } = require('picoes') +import { World } from 'picoes' // Create a world to store entities in const world = new World() -// Create player with anonymous health component +// Create a player entity with health component const player = world.entity().set('health', { value: 100 }) // Create enemies world.entity().set('damages', 10) world.entity().set('damages', 30) -// Apply damage +// Apply damage to player from enemies world.each('damages', ({ damages }) => { - player.get('health').value -= damages + player.get('health').value -= damages }) // Player now has reduced health console.assert(player.get('health').value === 60) ``` -#### Full component and system definitions - -```javascript -// const { World } = require('picoes') -import { World } from 'picoes' - -// Create a world to store entities in -const world = new World() - -// Define and register components -class Vec2 { - constructor(x = 0, y = 0) { - this.x = x - this.y = y - } -} -world.component('position', Vec2) -world.component('velocity', Vec2) -world.component('health', class { - constructor(start = 100) { - this.value = start - } -}) - -// Example of using onCreate and onRemove -world.component('sprite', class { - onCreate(texture) { - // this.entity is auto-injected into registered components - // It is not available in the constructor, but is available in onCreate - this.container = this.entity.get('gameContainer') - this.sprite = new Sprite(texture) - this.container.add(this.sprite) - } - - onRemove() { - this.container.remove(this.sprite) - } -}) - -// Define systems -// Log statements are to show flow order below -class MovementSystem { - init(...args) { - // Context is available here as well - console.log('init() called with args:', ...args) - } - - run(dt) { - console.log(`run(${dt}) called`) - world.each('position', 'velocity', ({ position, velocity }, entity) => { - console.log(`each() called for entity ${entity.id}`) - position.x += velocity.x * dt - position.y += velocity.y * dt - }) - } -} - -// Register systems -world.system(MovementSystem, 'extra', 'args') - -// Create entity without prototype -const entityA = world.entity().set('position').set('velocity') -console.assert(entityA.has('position')) -console.assert(entityA.has('velocity')) - -// Create entity with prototype (results are the same as above) -world.prototype({ - Movable: { - position: {}, - velocity: {}, - }, -}) -const entityB = world.entity('Movable') -console.assert(entityB.has('position')) -console.assert(entityB.has('velocity')) - -// This will re-create the component using the constructor -entityB.set('position', 100, 100) - -// This set a property in the existing component -entityA.get('position').x = 100 - -// Set velocities by using update() -entityA.update('velocity', { x: 10, y: 10 }) -entityB.update('velocity', { x: -10, y: -10 }) - -// Run systems (pass one second for dt) -world.run(1.0) - -// Since the movement system ran once, the positions changed by the amount of their velocity -console.assert(entityA.get('position').x === 110) -console.assert(entityA.get('position').y === 10) -console.assert(entityB.get('position').x === 90) -console.assert(entityB.get('position').y === 90) -``` - -Expected output: - -``` -init() called with args: extra args -run(1) called -each() called for entity 1 -each() called for entity 2 -``` +More complete examples coming with final 1.0.0 release! For now, refer to the [full documentation](https://ayebear.com/picoes). diff --git a/index.js b/index.js index 2b4a107..b23638a 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,2 @@ -exports.World = require('./src/world.js').World -exports.SimpleIndex = require('./src/simple_index.js').SimpleIndex -exports.MemoizedQueryIndex = require('./src/memoized_query_index.js').MemoizedQueryIndex \ No newline at end of file +import { World } from './src/world.js' +export { World } diff --git a/package.json b/package.json index ebb030b..f22a793 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "picoes", - "version": "1.0.0-alpha4", - "description": "Pico Entity System for JavaScript (ES6).", - "main": "./index.js", + "version": "1.0.0-alpha7", + "description": "Pico Entity System for JavaScript", + "main": "index.js", + "files": [ + "src", + "index.js" + ], "scripts": { - "test": "jest --coverage", - "doc": "node ./node_modules/.bin/esdoc", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage", + "doc": "node --experimental-vm-modules ./node_modules/.bin/esdoc", "deploy": "gh-pages -d docs" }, "repository": { @@ -17,8 +21,7 @@ "component", "system", "ecs", - "picoes", - "es6" + "picoes" ], "author": "Eric Hebert", "license": "MIT", @@ -34,7 +37,16 @@ "esdoc-undocumented-identifier-plugin": "^1.0.0", "esdoc-unexported-identifier-plugin": "^1.0.0", "gh-pages": "^3.1.0", - "jest": "^26.6.3" + "husky": "^6.0.0", + "jest": "^26.6.3", + "prettier": "2.2.1", + "pretty-quick": "^3.1.0" }, - "dependencies": {} + "dependencies": {}, + "type": "module", + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } + } } diff --git a/src/benchmarks.js b/src/benchmarks.js deleted file mode 100644 index b8d6bf4..0000000 --- a/src/benchmarks.js +++ /dev/null @@ -1,80 +0,0 @@ -const { World } = require('../index.js') -const { SimpleIndex } = require('../src/simple_index.js') -const { MemoizedQueryIndex } = require('../src/memoized_query_index.js') - -function runBenchmarks() { - - for (let indexer of [SimpleIndex, MemoizedQueryIndex]) { - console.log(`========== Testing ${indexer.name} ===========`) - - let world = new World(indexer) - world.component('compA', function(val) { - this.val = val - }) - - let start = new Date() - for (let fullTrial = 1; fullTrial <= 5; ++fullTrial) { - console.log('Full trial #' + fullTrial) - - // Create some entities, and add components to them - let count = 10000 - - for (let i = 0; i < count / 4; ++i) { - let ent = world.entity() - ent.set('compA', 1) - ent.set('compB', {val: 7}) - ent.set('compC', {val: 42}) - for (let i = 0; i < 40; ++i) { - ent.set('comp' + i, i) - } - } - for (let i = 0; i < count / 4; ++i) { - let ent = world.entity() - ent.set('compA', 2) - for (let i = 0; i < 20; ++i) { - ent.set('comp' + i, i) - } - } - for (let i = 0; i < count / 4; ++i) { - let ent = world.entity() - ent.set('compB', {val: 3}) - for (let i = 15; i < 50; ++i) { - ent.set('comp' + i, i) - } - } - for (let i = 0; i < count / 4; ++i) { - let ent = world.entity() - ent.set('compC', {val: 4}) - } - - // Query for entities - let systems = 50 - let results = null - for (let i = 0; i < systems; ++i) { - results = world.each(['compA', 'compB']) - results = world.each(['compA', 'compB', 'compC', 'comp' + i]) - results = world.each(['compA', 'compC', 'comp5', 'comp6', 'comp7']) - results = world.each(['compB', 'compC', 'comp10']) - results = world.each(['compB', 'compC', 'comp30']) - results = world.each(['compC', 'comp30']) - results = world.each(['compC']) - - // Simulate a real system - world.each(['comp45', 'compB'], ({comp45: f, compB: b}) => b.val += f) - } - - // Destroy all numbered components - for (let i = 0; i < systems; ++i) { - world.each('comp' + i, (_, e) => e.destroy()) - } - } - - // Print elapsed time - let end = new Date() - let elapsed = (end - start) / 1000 - console.log(`Elapsed time: ${elapsed} sec`) - } -} - -// Only run benchmarks outside of tests -// runBenchmarks() diff --git a/src/entity.js b/src/entity.js index e02c1e0..84fb341 100644 --- a/src/entity.js +++ b/src/entity.js @@ -1,502 +1,465 @@ /** @ignore */ -const { invoke, shallowClone } = require('./utilities.js') +import { invoke, shallowClone } from './utilities.js' /** * Entity class used for storing components. * * @class Entity (name) */ -class Entity { - /** - * Do not construct an Entity yourself - use the entity() method in World instead. - * Also, do not shallow/deep copy entity objects, only pass around references. - * - * @private - * - * @param {World} world - The world - * @param {number} id - The identifier - */ - constructor(world, id) { - /** @ignore */ - this.world = world - - /** @ignore */ - this._id = id - - /** @ignore */ - this.data = {} - } - - /** - * Return the entity ID. - * - * @return {number} Integer entity ID - */ - get id() { - return this._id - } - - /** - * ID is read-only, attempting to set it will throw an error. - * - * @private - * - * @throws {Error} Cannot set entity id - */ - set id(id) { - throw new Error('Cannot set entity id') - } - - /** - * Returns true if the entity has ALL of the specified component names. - * Additional components that the entity has, which are not specified in has(), will be ignored. - * If no component names are specified, this method returns true. - * - * @example - * if (entity.has('position', 'velocity')) {...} - * - * @param {...string} [components] - The component names to check for - * - * @return {boolean} true or false - */ - has(...components) { - return components.every(name => name in this.data) - } - - /** - * Returns true if the entity has ANY of the specified component names. - * If no component names are specified, this method returns false. - * - * @example - * if (entity.hasAny('position', 'velocity')) {...} - * - * @param {...string} [components] - The component names to check for - * - * @return {boolean} true or false - */ - hasAny(...components) { - return components.some(name => name in this.data) - } - - /** - * Returns a component by name, or undefined if it doesn't exist - * - * @example - * let position = entity.get('position') - * - * @param {string} component - The component name to get - * - * @return {Object} The component if defined, otherwise undefined - */ - get(component) { - return this.data[component] - } - - /** - * Returns a component by name (automatically created if it doesn't exist) - * - * @example - * let position = entity.access('position', 3, 4) - * - * @param {string} component - The component name to create/get - * @param {Object} fallback - The value to set the component as for unregistered components - * @param {...Object} [args] - The arguments to forward to create the new component, only if it doesn't exist. - * - * @return {Object} Always returns either the existing component, or the newly created one. - */ - access(component, fallback, ...args) { - if (!this.has(component)) { - this.setWithFallback(component, fallback, ...args) - } - return this.data[component] - } - - /** - * Adds a new component, or re-creates and overwrites an existing component - * - * @example - * entity.set('position', 1, 2) - * - * @example - * entity.set('anonymousComponent', { keys: 'values' }) - * - * @example - * entity.set('anotherAnonymousComponent', 'Any type of any value') - * - * @param {string} component - The component name to create. If there is a registered component for this name, - * then its constructor will be called with (...args) and an object of that type will be created. The parent - * entity reference gets injected into registered components after they are constructed. The onCreate method - * gets called after the component is added to the entity, and after the entity is injected. This method - * also gets passed the same parameters. - * @param {...Object} [args] - The arguments to forward to the registered component type. If the component type is - * registered, then only the first additional argument will be used as the value of the entire component. - * - * @return {Object} The original entity that set() was called on, so that operations can be chained. - */ - set(component, ...args) { - // Use first argument as fallback - return this.setWithFallback(component, args[0], ...args) - } - - /** - * Adds a new component, or re-creates and overwrites an existing component. Has separate fallback argument. - * - * @example - * entity.setWithFallback('position', { x: 1, y: 2 }, 1, 2) - * - * @param {string} component - See entity.set() for details. - * @param {Object} fallback - The object or value to use as the component when there is no registered type. - * @param {...Object} [args] - See entity.set() for details. - * - * @return {Object} The original entity that setWithFallback() was called on, so that operations can be chained. - */ - setWithFallback(component, fallback, ...args) { - if (this.valid() && component in this.world.components) { - // Create component and store in entity - this.data[component] = new this.world.components[component](...args) - - // Inject parent entity into component - this.data[component].entity = this - } else { - // Use fallback argument as component value - this.data[component] = fallback - } - - // Update the index with this new component - if (this.valid()) { - this.world.index.add(this, component) - } - - // Call custom onCreate to initialize component, and any additional arguments passed into set() - invoke(this.data[component], 'onCreate', ...args) - - return this - } - - /** - * Sets a component value directly. The onCreate method is not called, and it is expected that you - * pass an already initialized component. - * - * @example - * entity.set('position', position) - * - * @param {string} component - The component name to set. - * @param {Object} value - Should be a previous component instance, or whatever is expected for - * the component name. - * - * @return {Object} The original entity that setRaw() was called on, so that operations can be chained. - */ - setRaw(component, value) { - // Directly set value - this.data[component] = value - - // Update the index with this new component - if (this.valid()) { - this.world.index.add(this, component) - } - - return this - } - - /** - * Updates component data from an object or other component. Similar to access() with a shallow merge applied after. - * - * @example - * entity.update('position', { x: 1, y: 2 }) - * - * @param {string} component - The component name to update - * @param {Object} data - The object or other component to merge into the specified component. - * @param {...Object} [args] - See entity.set() for details. - * - * @return {Object} The original entity that update() was called on, so that operations can be chained. - */ - update(component, data, ...args) { - const comp = this.access(component, {}, ...args) - - // Shallow set keys of the component - for (const key in data) { - comp[key] = data[key] - } - - return this - } - - /** - * Removes a component from the entity - has no effect when it doesn't exist. - * Can specify an onRemove() method in your component which gets called before it is removed. - * If nothing is specified, then nothing will be removed. Use removeAll() to remove all components. - * - * @example - * entity.remove('position') - * - * @param {...string} [components] - The component names to remove from the entity. - * - * @return {Object} The original entity that remove() was called on, so that operations can be chained. - */ - remove(...components) { - for (let component of components) { - if (component in this.data) { - - // Call custom onRemove - invoke(this.data[component], 'onRemove') - - // Remove from index - if (this.valid()) { - this.world.index.remove(this, component) - } - - // Remove from entity - delete this.data[component] - } - } - return this - } - - // Remove all components - - /** - * Removes all components from the entity. - * - * @example - * entity.removeAll() - * - * @return {Object} The original entity that removeAll() was called on, so that operations can be chained. - */ - removeAll() { - this.remove(...this.components) - - if (this.components.length > 0) { - throw new Error('Failed to remove all components. Components must have been added during the removeAll().') - } - - return this - } - - /** - * Remove this entity and all of its components from the world. After an entity is destroyed, the object should be discarded, - * and it is recommended to avoid re-using it. - * - * @example - * entity.destroy() - */ - destroy() { - this.removeAll() - - if (this.valid()) { - // Remove from world - this.world.entities.delete(this._id) - this._id = undefined - } - } - - /** - * Returns an array of component names this entity currently has. - * - * @return {Array} Array of component names. - */ - get components() { - return Object.keys(this.data) - } - - /** @ignore */ - set components(c) { - throw new Error('Cannot set components in this way. See entity.set().') - } - - /** - * Returns true if this is a valid, existing, and usable entity, which is attached to a world. - * - * @example - * if (entity.valid()) {...} - * - * @return {boolean} true or false - */ - valid() { - // Note: No need to actually look in the world for the ID, if entities are only ever copied by reference. - // If entities are ever deep/shallow copied, this function will need to check this to be more robust. - return this.world && this._id !== undefined - } - - /** - * Returns unique entity ID as a string. - * - * @example - * let entityId = entity.toString() - * - * @return {string} String representation of the entity ID. - */ - toString() { - return String(this._id) - } - - /** - * Serializes entire entity and components to JSON. - * Note: Defining toJSON methods in your components will override the built-in behavior. - * - * @example - * let serializedEntity = entity.toJSON() - * - * @return {string} JSON encoded string - */ - toJSON() { - return JSON.stringify(this.data) - } - - /** - * Deserializes data from JSON, creating new components and overwriting existing components. - * Note: Defining fromJSON methods in your components will override the built-in behavior. - * - * @example - * entity.fromJSON(serializedEntity) - * - * @param {string} data - A JSON string containing component data to parse, and store in this entity. - * - * @return {Object} The original entity that fromJSON() was called on, so that operations can be chained. - */ - fromJSON(data) { - const parsed = JSON.parse(data) - for (const name in parsed) { - const comp = this.access(name, {}) - - // Either call custom method or copy all properties - if (typeof comp.fromJSON === 'function') { - comp.fromJSON(parsed[name]) - } else { - this.update(name, parsed[name]) - } - } - return this - } - - /** - * Attaches a currently detached entity back to a world. - * Note: Do not use detached entities, get() may be safe, but avoid calling other methods - * Note: The ID will be reassigned, so do not rely on this - * - * @example - * entity.attach(world) - * - * @param {World} world - The world to attach this entity to - */ - attach(world) { - if (world && !this.valid()) { - // Assign new id, and reattach to world - this.world = world - this._id = this.world.idCounter++ - this.world.entities.set(this._id, this) - this.world.index.add(this, ...this.components) - } - } - - /** - * Removes this entity from the current world, without removing any components or data. - * It can be re-attached to another world (or the same world), using the attach() method. - * Note: Do not use detached entities, get() may be safe, but avoid calling other methods - * Note: The ID will be reassigned, so do not rely on this - * - * @example - * entity.detach() - */ - detach() { - if (this.valid()) { - // Remove from current world - this.world.index.remove(this, ...this.components) - this.world.entities.delete(this._id) - this._id = undefined - this.world = undefined - } - } - - /** - * Creates a copy of this entity with all of the components cloned and returns it. - * Individual components are either shallow or deep copied, depending on component - * registration status and if a clone() method is defined. See entity.cloneComponentTo(). - * - * @example - * entity.clone() - */ - clone() { - if (!this.valid()) { - throw new Error('Cannot clone detached or invalid entity.') - } - - // Clone each component in this entity, to a new entity - const newEntity = this.world.entity() - for (const name in this.data) { - this.cloneComponentTo(newEntity, name) - } - - // Return the cloned entity - return newEntity - } - - /** - * Clones a component from this entity to the target entity. - * - * @example - * const source = world.entity().set('foo', 'bar') - * const target = world.entity() - * source.cloneComponentTo(target, 'foo') - * assert(target.get('foo') === 'bar') - * - * @example - * world.component('foo', class { - * onCreate(bar, baz) { - * this.bar = bar - * this.baz = baz - * this.qux = false - * } - * setQux(qux = true) { - * this.qux = qux - * } - * cloneArgs() { - * return [this.bar, this.baz] - * } - * clone(target) { - * target.qux = this.qux - * } - * }) - * const source = world.entity() - * .set('foo', 'bar', 'baz') - * .set('qux', true) - * const target = world.entity() - * source.cloneComponentTo(target, 'foo') - * assert(source.get('foo').bar === target.get('foo').bar) - * assert(source.get('foo').baz === target.get('foo').baz) - * assert(source.get('foo').qux === target.get('foo').qux) - * - * @param {Entity} targetEntity - Must be a valid entity. Could be part of another world, but it - * is undefined behavior if the registered components are different types. - * @param {string} name - Component name of both source and target components. - * - * @return {Object} The original entity that cloneComponentTo() was called on, - * so that operations can be chained. - */ - cloneComponentTo(targetEntity, name) { - // Get component and optional arguments for cloning - const component = this.get(name) - const args = invoke(component, 'cloneArgs') || [] - - if (name in targetEntity.world.components) { - // Registered component, so create new using constructor, inject - // entity, and call optional clone - const newComponent = new targetEntity.world.components[name](...args) - newComponent.entity = targetEntity - targetEntity.data[name] = newComponent - invoke(component, 'clone', newComponent) - } else { - // Unregistered component, so just shallow clone it - targetEntity.data[name] = shallowClone(component) - } - - // Update the index with this new component - targetEntity.world.index.add(targetEntity, name) - - // Call custom onCreate to initialize component, and any additional arguments passed into set() - invoke(targetEntity.data[name], 'onCreate', ...args) - - return this - } +export class Entity { + /** + * Do not construct an Entity yourself - use the entity() method in World instead. + * Also, do not shallow/deep copy entity objects, only pass around references. + * + * @private + * + * @param {World} world - The world + * @param {number} id - The identifier + */ + constructor(world, id) { + /** @ignore */ + this.world = world + + /** @ignore */ + this._id = id + + /** @ignore */ + this.data = {} + } + + /** + * Return the entity ID. + * + * @return {number} Integer entity ID + */ + get id() { + return this._id + } + + /** + * ID is read-only, attempting to set it will throw an error. + * + * @private + * + * @throws {Error} Cannot set entity id + */ + set id(id) { + throw new Error('Cannot set entity id') + } + + /** + * Returns true if the entity has ALL of the specified component names. + * Additional components that the entity has, which are not specified in has(), will be ignored. + * If no component names are specified, this method returns true. + * + * @example + * if (entity.has('position', 'velocity')) {...} + * + * @param {...string} [components] - The component names to check for + * + * @return {boolean} true or false + */ + has(...components) { + return components.every(name => name in this.data) + } + + /** + * Returns true if the entity has ANY of the specified component names. + * If no component names are specified, this method returns false. + * + * @example + * if (entity.hasAny('position', 'velocity')) {...} + * + * @param {...string} [components] - The component names to check for + * + * @return {boolean} true or false + */ + hasAny(...components) { + return components.some(name => name in this.data) + } + + /** + * Returns a component by name, or undefined if it doesn't exist + * + * @example + * let position = entity.get('position') + * + * @param {string} component - The component name to get + * + * @return {Object} The component if defined, otherwise undefined + */ + get(component) { + return this.data[component] + } + + /** + * Returns a component by name (automatically created if it doesn't exist) + * + * @example + * let position = entity.access('position', 3, 4) + * + * @param {string} component - The component name to create/get + * @param {Object} fallback - The value to set the component as for unregistered components + * @param {...Object} [args] - The arguments to forward to create the new component, only if it doesn't exist. + * + * @return {Object} Always returns either the existing component, or the newly created one. + */ + access(component, fallback, ...args) { + if (!this.has(component)) { + this.setWithFallback(component, fallback, ...args) + } + return this.data[component] + } + + /** + * Adds a new component, or re-creates and overwrites an existing component + * + * @example + * entity.set('position', 1, 2) + * + * @example + * entity.set('anonymousComponent', { keys: 'values' }) + * + * @example + * entity.set('anotherAnonymousComponent', 'Any type of any value') + * + * @param {string} component - The component name to create. If there is a registered component for this name, + * then its constructor will be called with (...args) and an object of that type will be created. The parent + * entity reference gets injected into registered components after they are constructed. The onCreate method + * gets called after the component is added to the entity, and after the entity is injected. This method + * also gets passed the same parameters. + * @param {...Object} [args] - The arguments to forward to the registered component type. If the component type is + * registered, then only the first additional argument will be used as the value of the entire component. + * + * @return {Object} The original entity that set() was called on, so that operations can be chained. + */ + set(component, ...args) { + // Use first argument as fallback + return this.setWithFallback(component, args[0], ...args) + } + + /** + * Adds a new component, or re-creates and overwrites an existing component. Has separate fallback argument. + * + * @example + * entity.setWithFallback('position', { x: 1, y: 2 }, 1, 2) + * + * @param {string} component - See entity.set() for details. + * @param {Object} fallback - The object or value to use as the component when there is no registered type. + * @param {...Object} [args] - See entity.set() for details. + * + * @return {Object} The original entity that setWithFallback() was called on, so that operations can be chained. + */ + setWithFallback(component, fallback, ...args) { + if (this.valid() && component in this.world.entities.componentClasses) { + // Create component and store in entity + this.data[component] = new this.world.entities.componentClasses[ + component + ](...args) + + // Inject parent entity into component + this.data[component].entity = this + } else { + // Use fallback argument as component value + this.data[component] = fallback + } + + // Update the index with this new component + if (this.valid()) { + this.world.entities.addToIndex(this, component) + } + + // Call custom onCreate to initialize component, and any additional arguments passed into set() + invoke(this.data[component], 'onCreate', ...args) + + return this + } + + /** + * Sets a component value directly. The onCreate method is not called, and it is expected that you + * pass an already initialized component. + * + * @example + * entity.set('position', position) + * + * @param {string} component - The component name to set. + * @param {Object} value - Should be a previous component instance, or whatever is expected for + * the component name. + * + * @return {Object} The original entity that setRaw() was called on, so that operations can be chained. + */ + setRaw(component, value) { + // Directly set value + this.data[component] = value + + // Update the index with this new component + if (this.valid()) { + this.world.entities.addToIndex(this, component) + } + + return this + } + + /** + * Removes a component from the entity - has no effect when it doesn't exist. + * Can specify an onRemove() method in your component which gets called before it is removed. + * If nothing is specified, then nothing will be removed. Use removeAll() to remove all components. + * + * @example + * entity.remove('position') + * + * @param {...string} [components] - The component names to remove from the entity. + * + * @return {Object} The original entity that remove() was called on, so that operations can be chained. + */ + remove(...components) { + for (let component of components) { + if (component in this.data) { + // Call custom onRemove + invoke(this.data[component], 'onRemove') + + // Remove from index + if (this.valid()) { + this.world.entities.removeFromIndex(this, component) + } + + // Remove from entity + delete this.data[component] + } + } + return this + } + + // Remove all components + + /** + * Removes all components from the entity. + * + * @example + * entity.removeAll() + * + * @return {Object} The original entity that removeAll() was called on, so that operations can be chained. + */ + removeAll() { + this.remove(...this.components) + + if (this.components.length > 0) { + throw new Error( + 'Failed to remove all components. Components must have been added during the removeAll().' + ) + } + + return this + } + + /** + * Remove this entity and all of its components from the world. After an entity is destroyed, the object should be discarded, + * and it is recommended to avoid re-using it. + * + * @example + * entity.destroy() + */ + destroy() { + this.removeAll() + + if (this.valid()) { + // Remove from world + this.world.entities.entities.delete(this._id) + this._id = undefined + } + } + + /** + * Returns an array of component names this entity currently has. + * + * @return {Array} Array of component names. + */ + get components() { + return Object.keys(this.data) + } + + /** + * Returns true if this is a valid, existing, and usable entity, which is attached to a world. + * + * @example + * if (entity.valid()) {...} + * + * @return {boolean} true or false + */ + valid() { + // Note: No need to actually look in the world for the ID, if entities are only ever copied by reference. + // If entities are ever deep/shallow copied, this function will need to check this to be more robust. + return this.world && this._id !== undefined + } + + /** + * Serializes entire entity and components to JSON. + * Note: Defining toJSON methods in your components will override the built-in behavior. + * + * @example + * let serializedEntity = entity.toJSON() + * + * @return {string} JSON encoded string + */ + toJSON() { + return JSON.stringify(this.data) + } + + /** + * Deserializes data from JSON, creating new components and overwriting existing components. + * Note: Defining fromJSON methods in your components will override the built-in behavior. + * + * @example + * entity.fromJSON(serializedEntity) + * + * @param {string} data - A JSON string containing component data to parse, and store in this entity. + * + * @return {Object} The original entity that fromJSON() was called on, so that operations can be chained. + */ + fromJSON(data) { + const parsed = JSON.parse(data) + for (const name in parsed) { + const comp = this.access(name, {}) + + // Either call custom method or copy all properties + if (typeof comp.fromJSON === 'function') { + comp.fromJSON(parsed[name]) + } else { + Object.assign(this.access(name, {}), parsed[name]) + } + } + return this + } + + /** + * Attaches a currently detached entity back to a world. + * Note: Do not use detached entities, get() may be safe, but avoid calling other methods + * Note: The ID will be reassigned, so do not rely on this + * + * @example + * entity.attach(world) + * + * @param {World} world - The world to attach this entity to + */ + attach(world) { + if (world && !this.valid()) { + // Assign new id, and reattach to world + this.world = world + this._id = this.world.entities.nextEntityId++ + this.world.entities.entities.set(this._id, this) + this.world.entities.addToIndex(this, ...this.components) + } + } + + /** + * Removes this entity from the current world, without removing any components or data. + * It can be re-attached to another world (or the same world), using the attach() method. + * Note: Do not use detached entities, get() may be safe, but avoid calling other methods + * Note: The ID will be reassigned, so do not rely on this + * + * @example + * entity.detach() + */ + detach() { + if (this.valid()) { + // Remove from current world + this.world.entities.removeFromIndex(this, ...this.components) + this.world.entities.entities.delete(this._id) + this._id = undefined + this.world = undefined + } + } + + /** + * Creates a copy of this entity with all of the components cloned and returns it. + * Individual components are either shallow or deep copied, depending on component + * registration status and if a clone() method is defined. See entity.cloneComponentTo(). + * + * @example + * entity.clone() + */ + clone() { + if (!this.valid()) { + throw new Error('Cannot clone detached or invalid entity.') + } + + // Clone each component in this entity, to a new entity + const newEntity = this.world.entity() + for (const name in this.data) { + this.cloneComponentTo(newEntity, name) + } + + // Return the cloned entity + return newEntity + } + + /** + * Clones a component from this entity to the target entity. + * + * @example + * const source = world.entity().set('foo', 'bar') + * const target = world.entity() + * source.cloneComponentTo(target, 'foo') + * assert(target.get('foo') === 'bar') + * + * @example + * world.component('foo', class { + * onCreate(bar, baz) { + * this.bar = bar + * this.baz = baz + * this.qux = false + * } + * setQux(qux = true) { + * this.qux = qux + * } + * cloneArgs() { + * return [this.bar, this.baz] + * } + * clone(target) { + * target.qux = this.qux + * } + * }) + * const source = world.entity() + * .set('foo', 'bar', 'baz') + * .set('qux', true) + * const target = world.entity() + * source.cloneComponentTo(target, 'foo') + * assert(source.get('foo').bar === target.get('foo').bar) + * assert(source.get('foo').baz === target.get('foo').baz) + * assert(source.get('foo').qux === target.get('foo').qux) + * + * @param {Entity} targetEntity - Must be a valid entity. Could be part of another world, but it + * is undefined behavior if the registered components are different types. + * @param {string} name - Component name of both source and target components. + * + * @return {Object} The original entity that cloneComponentTo() was called on, + * so that operations can be chained. + */ + cloneComponentTo(targetEntity, name) { + // Get component and optional arguments for cloning + const component = this.get(name) + const args = invoke(component, 'cloneArgs') || [] + + if (name in targetEntity.world.entities.componentClasses) { + // Registered component, so create new using constructor, inject + // entity, and call optional clone + const newComponent = new targetEntity.world.entities.componentClasses[ + name + ](...args) + newComponent.entity = targetEntity + targetEntity.data[name] = newComponent + invoke(component, 'clone', newComponent) + } else { + // Unregistered component, so just shallow clone it + targetEntity.data[name] = shallowClone(component) + } + + // Update the index with this new component + targetEntity.world.entities.addToIndex(targetEntity, name) + + // Call custom onCreate to initialize component, and any additional arguments passed into set() + invoke(targetEntity.data[name], 'onCreate', ...args) + + return this + } } - -exports.Entity = Entity diff --git a/src/entity.test.js b/src/entity.test.js deleted file mode 100644 index c964dcc..0000000 --- a/src/entity.test.js +++ /dev/null @@ -1,519 +0,0 @@ -const { - testIndexers, - assert -} = require('./test_utils.js') - -test('entity: create an entity', testIndexers(world => { - world.component('position') - let ent = world.entity() - assert(world.entities.size == 1) - assert(ent.toString() == String(ent.id)) -})) - -test('entity: test if ID is read-only', testIndexers(world => { - let ent = world.entity() - expect(() => ent.id = 5).toThrow() - assert(typeof ent.id === 'number' && ent.id === 1) -})) - -test('entity: valid entities', testIndexers(world => { - let entityA = world.entity().set('test') - let entityB = world.get('test')[0] - assert(entityA.valid()) - assert(entityB.valid()) - assert(entityA.id === entityB.id) - assert(entityA === entityB) - - entityA.destroy() - assert(!entityA.valid()) - assert(!entityB.valid()) -})) - -test('entity: get entity by id', testIndexers(world => { - const entityA = world.entity().set('test') - const id = entityA.id - assert(world.getEntityById(id) === entityA) - assert(world.getEntityById(id).id === id) - entityA.destroy() - assert(world.getEntityById(id) === undefined) - assert(!entityA.valid()) -})) - -test('entity: remove an entity', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - let ent = world.entity() - ent.set('position') - ent.get('position').val = 100 - - assert(world.entities.size == 1) - assert(Object.keys(world.components).length == 1) - assert(ent.has('position')) - assert(ent.get('position').val === 100) - assert(ent.valid()) - - ent.destroy() - - assert(world.entities.size == 0) - assert(Object.keys(world.components).length == 1) - assert(!ent.valid()) - assert(!ent.has('position')) - - // Just for safe measure - ent.destroy() - - assert(world.entities.size == 0) - assert(Object.keys(world.components).length == 1) - assert(!ent.valid()) - assert(!ent.has('position')) -})) - -test('entity: get and set components', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - world.component('empty') - let ent = world.entity() - ent.set('position', 5) - assert(ent.has('position')) - assert(ent.hasAny('position')) - assert(ent.components.length == 1) - assert(ent.get('position').x === 5) - assert(ent.get('position').y === 0) - - ent.update('position', {y: 3}) - assert(ent.has('position')) - assert(ent.hasAny('position')) - assert(ent.components.length == 1) - assert(ent.get('position').x === 5) - assert(ent.get('position').y === 3) - - ent.update('object', {val: 50}) - assert(ent.has('object')) - assert(ent.hasAny('object')) - assert(ent.components.length == 2) - assert(ent.get('object').val === 50) - - ent.update('empty', {testing: 100}) - assert(ent.has('empty')) - assert(ent.hasAny('empty')) - assert(ent.components.length == 3) - assert(ent.get('empty').testing === 100) - - ent.set('anonymous') - assert(ent.components.length == 4) - assert(ent.has('anonymous')) - assert(ent.hasAny('anonymous')) - - // Access test - ent.removeAll() - assert(!ent.has('position')) - assert(!ent.hasAny('position')) - ent.access('position').x = 300 - assert(ent.has('position')) - assert(ent.hasAny('position')) - assert(ent.get('position').x === 300) - - // Get test - ent.removeAll() - assert(!ent.has('position')) - assert(ent.get('position') === undefined) - assert(!ent.has('position')) - ent.set('position', 333) - assert(ent.get('position').x === 333) - assert(ent.get('position').y === 0) - - // Undefined component tests - ent.removeAll() - ent.set('invalid', {a: 'test'}) - assert(ent.get('invalid').a === 'test') - - ent.set('invalid', {b: 'test2'}) - assert(ent.get('invalid').a === undefined) - assert(ent.get('invalid').b === 'test2') - - ent.set('invalid2', 5) - assert(ent.get('invalid2') === 5) - - ent.set('invalid2', 'test') - assert(ent.get('invalid2') === 'test') - - ent.set('invalid2', ['test']) - assert(ent.get('invalid2')[0] === 'test') -})) - -test('entity: check existence of components', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - world.component('empty') - const ent = world.entity() - assert(!ent.hasAny()) - assert(ent.has()) - ent.set('a') - ent.set('b') - assert(ent.has()) - assert(ent.has('a')) - assert(ent.has('a','b')) - assert(!ent.has('a','b','c','d')) - assert(!ent.hasAny()) - assert(ent.hasAny('a')) - assert(ent.hasAny('a','b')) - assert(ent.hasAny('','a','c')) - assert(ent.hasAny('a','b','c','d')) - - ent.removeAll() - assert(ent.has()) - assert(!ent.has('a')) - assert(!ent.has('a','b')) - assert(!ent.has('a','b','c','d')) - assert(!ent.hasAny()) - assert(!ent.hasAny('a')) - assert(!ent.hasAny('a','b')) - assert(!ent.hasAny('','a','c')) - assert(!ent.hasAny('a','b','c','d')) -})) - -test('entity: setRaw', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - let ent = world.entity() - ent.set('position', 10, 20) - assert(ent.has('position')) - assert(ent.get('position').x === 10) - assert(ent.get('position').y === 20) - - // Set raw tests - const previous = ent.get('position') - ent.remove('position') - assert(!ent.has('position')) - - ent.setRaw('position', previous) - - assert(ent.has('position')) - assert(ent.get('position').x === 10) - assert(ent.get('position').y === 20) - - // Invalid entity - ent.remove('position') - ent.detach() - ent.setRaw('position', previous) - assert(ent.has('position')) - assert(ent.get('position').x === 10) - assert(ent.get('position').y === 20) -})) - -test('entity: remove components', testIndexers(world => { - world.component('position') - world.component('velocity') - let ent = world.entity().set('position').set('velocity') - assert(ent.components.length == 2) - assert(ent.has('position')) - assert(ent.has('velocity')) - - ent.remove('invalid') - ent.remove() - - ent.remove('position') - assert(ent.components.length == 1) - assert(!ent.has('position')) - assert(ent.has('velocity')) - - ent.remove('velocity') - assert(ent.components.length == 0) - assert(!ent.has('position')) - assert(!ent.has('velocity')) - - ent.set('position').set('velocity') - assert(ent.components.length == 2) - assert(ent.has('position')) - assert(ent.has('velocity')) - ent.removeAll() - assert(ent.components.length == 0) - assert(!ent.has('position')) - assert(!ent.has('velocity')) - - // Remove many components - ent.set('position').set('velocity').set('testA').set('testB') - assert(ent.components.length == 4) - assert(ent.has('position')) - assert(ent.has('velocity')) - assert(ent.has('testA')) - assert(ent.has('testB')) - ent.remove('invalidA', 'position', 'testA', 'invalidB') - assert(!ent.has('position')) - assert(ent.has('velocity')) - assert(!ent.has('testA')) - assert(ent.has('testB')) - ent.remove('velocity', 'testB') - assert(!ent.has('position')) - assert(!ent.has('velocity')) - assert(!ent.has('testA')) - assert(!ent.has('testB')) -})) - -test('entity: remove components - onRemove', testIndexers(world => { - world.component('test', function(obj) { - this.obj = obj - this.obj.created = true - - this.onRemove = testIndexers(world => { - this.obj.removed = true - }) - - }) - let obj = { - created: false, - removed: false - } - let ent = world.entity().set('test', obj) - assert(ent.has('test')) - assert(obj.created) - assert(!obj.removed) - - ent.remove('test') - assert(ent.components.length == 0) - assert(!ent.has('test')) - assert(obj.created) - assert(obj.removed) - - - let obj2 = { - created: false, - removed: false - } - let ent2 = world.entity().set('test', obj2) - assert(ent2.has('test')) - assert(obj2.created) - assert(!obj2.removed) - - ent2.destroy() - assert(obj2.created) - assert(obj2.removed) -})) - -test('entity: serialize components', testIndexers(world => { - world.component('position') - let ent = world.entity().update('position', {x: 4, y: 6}) - - let data = JSON.parse(ent.toJSON()) - assert(data) - assert(data.position) - assert(data.position.x === 4) - assert(data.position.y === 6) -})) - -test('entity: serialize custom components', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - - this.toJSON = () => ({ result: this.x * this.y }) - - }) - let ent = world.entity().set('position', 4, 6) - - let data = JSON.parse(ent.toJSON()) - assert(data) - assert(data.position) - assert(data.position.result === 24) -})) - -test('entity: deserialize components', testIndexers(world => { - world.component('position') - let ent = world.entity() - assert(ent.components.length == 0) - - ent.fromJSON('{"position": {"x": 4, "y": 6}}') - assert(ent.has('position')) - assert(ent.components.length == 1) - assert(ent.get('position')) - assert(ent.get('position').x === 4) - assert(ent.get('position').y === 6) -})) - -test('entity: deserialize custom components', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - - this.toJSON = () => ({ result: this.x * this.y }) - - this.fromJSON = (data) => { - this.x = data.result / 2 - this.y = 2 - } - }) - - // Old deserialization test - let ent = world.entity() - assert(ent.components.length == 0) - ent.fromJSON('{"position": {"result": 24}}') - assert(ent.has('position')) - assert(ent.components.length == 1) - assert(ent.get('position')) - assert(ent.get('position').x === 12) - assert(ent.get('position').y === 2) - - // Full entity-based serialization/deserialization test - let ent2 = world.entity().set('position', 7, 4) - let jsonData = ent2.toJSON() - let ent3 = world.entity().fromJSON(jsonData) - assert(ent3.has('position')) - assert(ent3.get('position').x === 14) - assert(ent3.get('position').y === 2) - ent2.fromJSON(jsonData) - assert(ent2.has('position')) - assert(ent2.get('position').x === 14) - assert(ent2.get('position').y === 2) -})) - -test('entity: check for existence of components', testIndexers(world => { - // Test all component types - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - world.component('velocity') - world.component('player') - - let ent = world.entity() - .set('position', 1, 2) - .set('velocity', {x: 3, y: 4}) - .set('player') - .set('anonymous') - - // Check for existence - assert(ent.has('position') && ent.has('velocity') && ent.has('player') && ent.has('anonymous')) - assert(ent.has('position', 'velocity', 'player', 'anonymous')) - assert(!ent.has('position', 'invalid')) - assert(!ent.has('velocity', 'invalid')) - assert(!ent.has('player', 'invalid')) - assert(!ent.has('anonymous', 'invalid')) - assert(!ent.has('invalid')) - - // This behavior is important for world.each to work properly when no components are specified - // Basically, it should return all entities when nothing is specified - assert(ent.has()) - let ent2 = world.entity() - assert(ent2.has()) - assert(!ent2.has('invalid')) -})) - -test('entity: register and use prototypes', testIndexers(world => { - // Test all three component types - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - - let result = world.prototype() - assert(result == 0) - - // Register prototypes in all ways - result = world.prototype({Player: { - position: { - x: 5, - y: 10 - }, - velocity: { - x: 15, - y: 20 - }, - player: {} - }, Enemy: { - position: {}, - velocity: {} - }}) - assert(result == 2) - - let stringTest = JSON.stringify({Test: { - position: { - x: 3.14159, - y: 5000 - } - }}) - result = world.prototype(stringTest) - assert(result == 1) - - // Create entities with the prototype - let p = world.entity('Player') - let e = world.entity('Enemy') - let t = world.entity('Test') - - // Make sure all components exist and there are no extras - assert(p.has('position', 'velocity', 'player')) - assert(e.has('position', 'velocity') && !e.has('player')) - assert(t.has('position') && !t.has('velocity') && !t.has('player')) - - // Make sure all component values are correct - assert(p.get('position').x === 5 && p.get('position').y === 10) - assert(p.get('velocity').x === 15 && p.get('velocity').y === 20) - assert(p.get('player') !== undefined) - expect(e.get('position').x).toEqual(0) - expect(e.get('position').y).toEqual(0) - assert(e.get('velocity').x === undefined && e.get('velocity').y === undefined) - assert(t.get('position').x === 3.14159 && t.get('position').y === 5000) -})) - -test('entity: cloning basic', testIndexers(world => { - const source = world.entity().set('a', 'aaa') - const target = world.entity() - source.cloneComponentTo(target, 'a') - expect(target.get('a')).toEqual('aaa') -})) - -test('entity: cloning advanced', testIndexers(world => { - world.component('foo', class { - onCreate(bar, baz) { - this.bar = bar - this.baz = baz - this.qux = false - } - setQux(qux = true) { - this.qux = qux - } - cloneArgs() { - return [this.bar, this.baz] - } - clone(target) { - target.qux = this.qux - } - }) - const source = world.entity() - .set('foo', 'bar', 'baz') - .set('qux', true) - const target = world.entity() - source.cloneComponentTo(target, 'foo') - expect(source.get('foo').bar).toEqual(target.get('foo').bar) - expect(source.get('foo').baz).toEqual(target.get('foo').baz) - expect(source.get('foo').qux).toEqual(target.get('foo').qux) - - const target2 = source.clone() - expect(source.get('foo').bar).toEqual(target2.get('foo').bar) - expect(source.get('foo').baz).toEqual(target2.get('foo').baz) - expect(source.get('foo').qux).toEqual(target2.get('foo').qux) - - target.get('foo').bar = 'change1' - target2.get('foo').baz = 'change2' - expect(source.get('foo').bar).not.toEqual(target.get('foo').bar) - expect(source.get('foo').baz).toEqual(target.get('foo').baz) - expect(source.get('foo').qux).toEqual(target.get('foo').qux) - expect(source.get('foo').bar).toEqual(target2.get('foo').bar) - expect(source.get('foo').baz).not.toEqual(target2.get('foo').baz) - expect(source.get('foo').qux).toEqual(target2.get('foo').qux) - - const target3 = target2.clone() - expect(target3.get('foo').bar).toEqual(target2.get('foo').bar) - expect(target3.get('foo').baz).toEqual(target2.get('foo').baz) - expect(target3.get('foo').qux).toEqual(target2.get('foo').qux) - - target3.destroy() - expect(() => target3.clone()).toThrow() -})) diff --git a/src/entity_storage.js b/src/entity_storage.js new file mode 100644 index 0000000..1831bc0 --- /dev/null +++ b/src/entity_storage.js @@ -0,0 +1,162 @@ +import { Entity } from './entity.js' +import { invoke } from './utilities.js' + +/** + * @ignore + * Returns index of smallest element + */ +const minIndexReducer = (minIndex, value, index, values) => + value < values[minIndex] ? index : minIndex + +/** @ignore */ +export class EntityStorage { + constructor(world) { + /** @ignore */ + this.world = world + /** @ignore */ + this.componentClasses = {} + /** @ignore */ + this.nextEntityId = 1 + + /** + * Maps entity IDs to entities + * @ignore + */ + this.entities = new Map() + + /** + * Maps component keys to entities + * @ignore + */ + this.index = new Map() + } + + clear() { + // Call onRemove on all components of all entities + for (const [, entity] of this.entities) { + for (let componentName in entity.data) { + // Get component, and call onRemove if it exists as a function + let component = entity.data[componentName] + invoke(component, 'onRemove') + } + } + + // Clear entities + this.entities.clear() + this.index.clear() + } + + registerComponent(name, componentClass) { + // Only allow functions and classes to be components + if (typeof componentClass !== 'function') { + throw new Error('Component is not a valid function or class.') + } + this.componentClasses[name] = componentClass + } + + // Creates a new entity attached to the world + createEntity() { + const entityId = this.nextEntityId++ + const entity = new Entity(this.world, entityId) + this.entities.set(entityId, entity) + return entity + } + + // Forwards query args from world + each(...args) { + return this.queryIndex(this.queryArgs(...args)) + } + + // Returns an existing or new index + accessIndex(component) { + // TODO: Compare with object based approach for performance + return ( + this.index.get(component) || + this.index.set(component, new Map()).get(component) + ) + } + + // Add certain components with an entity to the index + addToIndex(entity, ...componentNames) { + for (let component of componentNames) { + this.accessIndex(component).set(entity.id, entity) + } + } + + // Remove certain components from the index for an entity + removeFromIndex(entity, ...componentNames) { + for (let component of componentNames) { + this.accessIndex(component).delete(entity.id) + } + } + + queryArgs(...args) { + // Gather component names and a callback (if any) from args + const result = { + componentNames: [], + callback: null, + } + for (const arg of args) { + if (typeof arg === 'string') { + result.componentNames.push(arg) + } else if (typeof arg === 'function') { + result.callback = arg + } else if (Array.isArray(arg)) { + // Add 1-level deep arrays of strings as separate component names + for (const name of arg) { + result.componentNames.push(name) + } + } else { + throw new Error( + `Unknown argument ${arg} with type ${typeof arg} passed to world.each().` + ) + } + } + return result + } + + // Uses an existing index or builds a new index, to get entities with the specified components + // If callback is defined, it will be called for each entity with component data, and returns undefined + // If callback is not defined, an array of entities will be returned + queryIndex({ componentNames, callback }) { + // Return all entities (array if no callback) + if (componentNames.length === 0) { + const iter = this.entities.values() + if (!callback) { + return [...iter] + } + for (const entity of iter) { + if (callback(entity.data, entity) === false) { + break + } + } + return + } + + // Get the index name with the least number of entities + const minCompIndex = componentNames + .map(name => this.accessIndex(name).size) + .reduce(minIndexReducer, 0) + const minComp = componentNames[minCompIndex] + + // Return matching entities (array if no callback) + const iter = this.index.get(minComp).values() + if (!callback) { + const results = [] + for (const entity of iter) { + if (entity.has(...componentNames)) { + results.push(entity) + } + } + return results + } + for (const entity of iter) { + if ( + entity.has(...componentNames) && + callback(entity.data, entity) === false + ) { + return + } + } + } +} diff --git a/src/memoized_query_index.js b/src/memoized_query_index.js deleted file mode 100644 index 0ad676e..0000000 --- a/src/memoized_query_index.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * An alternative indexer class to SimpleIndex. This has true O(1) queries (when memoized), for the cost - * of slower component add/remove operations. As more queries are made, the slower add/remove become. - * - * @class MemoizedQueryIndex (name) - */ -class MemoizedQueryIndex { - constructor(world) { - this.world = world - this.clear() - } - - // Removes everything from the index - clear() { - this.index = {} - } - - // Uses an existing index or builds a new index, to return entities with the specified components - *query(...componentNames) { - // Return all entities - if (componentNames.length === 0) { - yield* this.world.entities.values() - return - } - - // Hash the component list - let hash = this.hashComponents(componentNames) - - // Return already existing index - if (hash in this.index) { - yield* this.index[hash].entities.values() - return - } - - // Build new index for this component list - yield* this.build(hash, componentNames).entities.values() - } - - // Creates a hash from an array of component names - hashComponents(names) { - return JSON.stringify(names.sort()) - } - - // Builds an initial index for a set of components - // These indexes are expected to be updated when doing entity/component operations - build(hash, componentNames) { - let matchingEntities = new Map() - - for (const [entityId, entity] of this.world.entities) { - // Ensure entity contains all specified components - if (entity.has(...componentNames)) { - // Add entity to index - matchingEntities.set(entity.id, entity) - } - } - - return this.index[hash] = { - components: new Set(componentNames), - entities: matchingEntities - } - } - - // Must use all component names from entity - add(entity) { - for (let hash in this.index) { - const group = this.index[hash] - - // Check if the entity has all of the components of the index group - if (entity.has(...group.components)) { - // Add the entity - group.entities.set(entity.id, entity) - } - } - } - - // Remove certain components from the index for an entity - remove(entity, ...componentNames) { - for (let hash in this.index) { - const group = this.index[hash] - - // Check if index group has any of the components that the entity has - if (componentNames.some(name => group.components.has(name))) { - // Remove the entity - group.entities.delete(entity.id) - } - } - } -} - -exports.MemoizedQueryIndex = MemoizedQueryIndex diff --git a/src/simple_index.js b/src/simple_index.js deleted file mode 100644 index c93f16d..0000000 --- a/src/simple_index.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @ignore - * Returns index of smallest element - */ -const minIndexReducer = (minIndex, value, index, values) => (value < values[minIndex] ? index : minIndex) - -/** - * The default indexer for World. Extremely fast component adding/removing, for the cost of slightly - * slower entity querying performance. - * - * @class SimpleIndex (name) - */ -class SimpleIndex { - constructor(world) { - this.world = world - this.clear() - } - - // Removes everything from the index - clear() { - this.index = {} - } - - // Returns an existing or new index - access(component) { - return this.index[component] || (this.index[component] = new Map()) - } - - // Uses an existing index or builds a new index, to return entities with the specified components - *query(...componentNames) { - // Return all entities - if (componentNames.length === 0) { - yield* this.world.entities.values() - return - } - - // Get the index name with the least number of entities - const minCompIndex = componentNames - .map(name => this.access(name).size) - .reduce(minIndexReducer, 0) - const minComp = componentNames[minCompIndex] - - // Return matching entities - for (let entity of this.index[minComp].values()) { - if (entity.has(...componentNames)) { - yield entity - } - } - } - - // Add certain components with an entity to the index - add(entity, ...componentNames) { - for (let component of componentNames) { - this.access(component).set(entity.id, entity) - } - } - - // Remove certain components from the index for an entity - remove(entity, ...componentNames) { - for (let component of componentNames) { - this.access(component).delete(entity.id) - } - } -} - -exports.SimpleIndex = SimpleIndex diff --git a/src/system_storage.js b/src/system_storage.js new file mode 100644 index 0000000..7478dbd --- /dev/null +++ b/src/system_storage.js @@ -0,0 +1,54 @@ +import { invoke } from './utilities.js' + +/** @ignore */ +export class SystemStorage { + constructor() { + this.systems = [] + this.context = undefined + } + + register(systemClass, ...args) { + // Make sure the system is valid + if (typeof systemClass !== 'function') { + throw new Error('System is not a valid function or class.') + } + // Create and add the system with context + const newSystem = new systemClass(...args) + this._injectContext(newSystem) + invoke(newSystem, 'init', ...args) + this.systems.push(newSystem) + } + + run(...args) { + let status = true + // Continue rerunning while any systems return true + while (status) { + status = undefined + for (const system of this.systems) { + // Try to call the "run" method + const result = invoke(system, 'run', ...args) + status = status || result + } + // Clear args after first run, so re-runs can be identified + args.length = 0 + } + } + + // Update existing systems' context + setContext(data) { + this.context = data + for (const system of this.systems) { + this._injectContext(system) + } + } + + // Injects context into a system based on current context state + _injectContext(system) { + if (this.context) { + // Inject as keys of context + for (const key in this.context) { + system[key] = this.context[key] + } + } + } +} diff --git a/src/test_utils.js b/src/test_utils.js deleted file mode 100644 index 9d55c0d..0000000 --- a/src/test_utils.js +++ /dev/null @@ -1,41 +0,0 @@ -const { World } = require('../index.js') -const { SimpleIndex } = require('./simple_index.js') -const { MemoizedQueryIndex } = require('./memoized_query_index.js') -const indexers = [SimpleIndex, MemoizedQueryIndex] - -function getSize(it) { - let num = 0 - for (let elem of it) { - ++num - } - return num -} - -function has(it, target) { - for (let elem of it) { - if (elem.toString() == target.toString()) { - return true - } - } - return false -} - -function testIndexers(callback) { - return () => { - for (let indexer of indexers) { - callback(new World(indexer)) - } - } -} - -// TODO: Result of mocha/chai to jest upgrade, remove and use jest's "expect" -function assert(value) { - expect(Boolean(value)).toBe(true) -} - -module.exports = { - getSize, - has, - testIndexers, - assert -} \ No newline at end of file diff --git a/src/utilities.js b/src/utilities.js index a458e27..2ec674f 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -9,10 +9,10 @@ * * @return {Object} Returns what the called method returns */ -function invoke(object, method, ...args) { - if (object && typeof object[method] === 'function') { - return object[method].call(object, ...args) - } +export function invoke(object, method, ...args) { + if (object && typeof object[method] === 'function') { + return object[method].call(object, ...args) + } } /** @@ -20,14 +20,11 @@ function invoke(object, method, ...args) { * * @ignore */ -function shallowClone(val) { - if (Array.isArray(val)) { - return [...val] - } else if (typeof val === 'object') { - return {...val} - } - return val +export function shallowClone(val) { + if (Array.isArray(val)) { + return [...val] + } else if (typeof val === 'object') { + return { ...val } + } + return val } - -exports.invoke = invoke -exports.shallowClone = shallowClone diff --git a/src/utilities.test.js b/src/utilities.test.js deleted file mode 100644 index ac48385..0000000 --- a/src/utilities.test.js +++ /dev/null @@ -1,50 +0,0 @@ -const { invoke, shallowClone } = require('./utilities') - -test('utilities: invoke', () => { - const obj = { - foo1: () => 'bar', - foo2: 'bar', - foo3: (...args) => args.join(' ') - } - class C { - constructor() { - this.that = this - this.foo5 = () => { - return Boolean(this.that && this === this.that) - } - } - foo4() { - return Boolean(this.that && this === this.that) - } - } - const obj2 = new C() - expect(invoke(obj, 'foo1')).toBe('bar') - expect(invoke(obj, 'foo2')).toBe(undefined) - expect(invoke(obj, 'foo3', 'foo', 'bar')).toBe('foo bar') - expect(invoke(obj2, 'foo4')).toBe(true) - expect(obj2.foo4()).toBe(true) - expect(obj2.foo4.call(this)).toBe(false) - expect(invoke(obj2, 'foo5')).toBe(true) - expect(obj2.foo5()).toBe(true) - expect(obj2.foo5.call(this)).toBe(true) -}) - -test('utilities: shallowClone', () => { - const obj = { a: 1, b: 2 } - const arr = [ 1, 2, { c: 3 }, { d: 4 } ] - const obj2 = shallowClone(obj) - const arr2 = shallowClone(arr) - expect(obj).toEqual(obj2) - expect(arr).toEqual(arr2) - obj.a = 0 - obj2.a = -1 - arr[0] = 0 - arr[2] = 3 - expect(obj.a).toBe(0) - expect(obj2.a).toBe(-1) - expect(arr).toEqual([ 0, 2, 3, { d: 4 } ]) - expect(arr2).toEqual([ 1, 2, { c: 3 }, { d: 4 } ]) - arr[3].d = 10 - expect(arr).toEqual([ 0, 2, 3, { d: 10 } ]) - expect(arr2).toEqual([ 1, 2, { c: 3 }, { d: 10 } ]) -}) diff --git a/src/world.js b/src/world.js index dfb545c..1cae29b 100644 --- a/src/world.js +++ b/src/world.js @@ -1,439 +1,232 @@ -/** @ignore */ -const { invoke, isFunction } = require('./utilities.js') - -/** @ignore */ -const { Entity } = require('./entity.js') - -/** @ignore */ -const { SimpleIndex } = require('./simple_index.js') +import { Entity } from './entity.js' +import { SystemStorage } from './system_storage.js' +import { EntityStorage } from './entity_storage.js' /** * Class for world. * - * @class World (name) + * @class World (name) */ -class World { - /** - * Constructs an instance of the world. - * - * @param {Function} [indexer=SimpleIndex] The indexer to use. Default is SimpleIndex. Can use MemoizedQueryIndex if better querying performance is needed, for increased component creation/removal costs. - */ - constructor(indexer = SimpleIndex) { - /** @ignore */ - this.systems = [] - - /** - * Maps entity IDs to entities - * @ignore - */ - this.entities = new Map() - - /** @ignore */ - this.components = {} - - /** @ignore */ - this.entityTemplates = {} - - /** @ignore */ - this.idCounter = 1 - - /** - * Maps entire queries to arrays of entities - * @ignore - */ - this.index = new indexer(this) - - /** - * Context information - */ - this.contextData = undefined - this.contextKey = undefined - } - - /** - * Removes all entities from the world. - * Does not affect registered systems, components, or prototypes. - * - * @example - * world.clear() - */ - clear() { - // Call onRemove on all components of all entities - for (const [entityId, entity] of this.entities) { - for (let componentName in entity.data) { - // Get component, and call onRemove if it exists as a function - let component = entity.data[componentName] - invoke(component, 'onRemove') - } - } - - // Clear entities - this.entities = new Map() - this.index.clear() - } - - /** - * Registers a component type to the world. Components must be constructable. If the component has - * an onCreate(), it is passed all of the arguments from methods like entity.set(). Also, components - * can have an onRemove() method, which gets called when removing that component from an entity. - * - * @param {string} name - The name - * @param {function} componentClass - The component class, must be a constructable class or function - * - * @example - * world.component('myComponent', class { - * // It is highly recommended to use onCreate() over constructor(), because the component - * // will have already been added to the entity. In the constructor(), it is not safe to use - * // "entity" because it does not contain the current component while still in the constructor. - * onCreate(some, args) { - * this.some = some - * this.args = args - * this.entity.set('whatever') // this.entity is auto-injected, and this is safe to do here - * } - * }) - * // entity === the new entity object - * // some === 10 - * // args === 500 - * world.entity().set('myComponent', 10, 500) - * - * @return {string} Registered component name on success, undefined on failure - */ - component(name, componentClass) { - // Only allow functions and classes to be components - if (typeof componentClass === 'function') { - this.components[name] = componentClass - return name - } - } - - /** - * Creates a new entity in the world - * - * @param {string} [name] - The prototype name to use - * - * @example - * world.entity() - * - * @example - * world.entity('Player') - * - * @return {Entity} The new entity created - */ - entity(name) { - let entityId = this.idCounter++ - let entity = new Entity(this, entityId) - - // Use 'name' to get prototype data (if specified) - if (name && name in this.entityTemplates) { - // Add all components from prototype - let template = this.entityTemplates[name] - for (let componentName in template) { - // Update component with data from template - let newComponentData = JSON.parse(template[componentName]) - entity.update(componentName, newComponentData) - } - } - - this.entities.set(entityId, entity) - return entity - } - - /** - * Sets a context object that is automatically injected into all existing and new systems. - * Calling this multiple times will overwrite any previous contexts passed. One caveat is that - * you can only start to use the injected context in systems starting with init(). It is not - * available in the constructor. - * - * @param {Object} [data] - The object to use as context to pass to systems - * @param {string} [key] - The top-level key to inject into systems for the context object. - * If no key is specified, then all the keys inside the context object will be spread into the - * top-level of the system. - * - * @example - * const state = { app: new PIXI.Application() } - * const world = new World() - * world.context(state) // systems can directly use this.app - * world.system(...) - * - * @example - * world.context(state, 'state') // systems use this.state.app - * - * @return {Entity} The new entity created - */ - context(data, key) { - this.contextData = data - this.contextKey = key - - // Update existing systems' context - for (const system of this.systems) { - this._injectContext(system) - } - } - - /** - * Registers a system to the world. - * The order the systems get registered, is the order then run in. - * - * @example - * // Movement system (basic example) - * class MovementSystem { - * run(dt) { - * world.each('position', 'velocity', ({ position, velocity }) => { - * position.x += velocity.x * dt - * position.y += velocity.y * dt - * }) - * } - * } - * // Input system (advanced example) - * class InputSystem { - * init(key) { - * // Have access to this.keyboard here, but not in constructor - * this.key = key - * } - * run(dt) { - * if (this.keyboard.isPressed(this.key)) { - * world.each('controlled', 'velocity', ({ velocity }, entity) => { - * // Start moving all controlled entities to the right - * velocity.x = 1 - * velocity.y = 0 - * // Can also use the full entity here, in this case to add a new component - * entity.set('useFuel') - * }) - * } - * } - * } - * // Inject context (see world.context()) - * world.context({ keyboard: new Keyboard() }) - * // Register systems in order (this method) - * world.system(InputSystem, 'w') // pass arguments to init/constructor - * world.system(MovementSystem) - * // Run systems (can get dt or frame time) - * world.run(1000.0 / 60.0) - * - * @param {Function} systemClass - The system class to instantiate. Can contain a - * constructor(), init(), run(), or any other custom methods/properties. - * - * @param {...Object} args - The arguments to forward to the system's constructor and init. - * Note that it is recommended to use init if using context, see world.context(). - * - * @return {number} Unique ID of the system on success or undefined on failure - */ - system(systemClass, ...args) { - // Make sure the system is valid - if (typeof systemClass === 'function') { - // Create the system - const newSystem = new systemClass(...args) - - // Inject context - this._injectContext(newSystem) - - // Call init - invoke(newSystem, 'init', ...args) - - // Add the system, return its ID - return this.systems.push(newSystem) - 1 - } - } - - /** - * Calls run() on all systems. These methods can return true to cause an additional rerun of all systems. - * Reruns will not receive the args passed into run(), as a way to identify reruns. - * - * @example - * world.run(deltaTime) - * - * @example - * // Example flow of method call order: - * // Setup systems: - * world.system(systemA) - * world.system(systemB) - * // During world.run(): - * // systemA.run() - * // systemB.run() - * - * @param {...Object} [args] - The arguments to forward to the systems' methods - */ - run(...args) { - let status = true - // Continue rerunning while any systems return true - while (status) { - status = undefined - for (const system of this.systems) { - // Try to call the "run" method - const result = invoke(system, 'run', ...args) - status = status || result - } - - // Clear args after first run, so re-runs can be identified - args = [] - } - } - - /** - * Iterate through components and entities with all of the specified component names - * - * @example - * // Use a callback to process entities one-by-one - * world.each('comp', ({ comp }) => { comp.value = 0 }) - * - * @example - * // Get an iterator for the entities - * const it = world.each('comp') - * for (let entity of it) {...} - * - * @example - * // Pass multiple components, arrays, use extra entity parameter, - * // and destructure components outside the query - * world.each('compA', ['more', 'comps'], 'compB', ({ compA, compC }, entity) => { - * if (compC) compC.foo(compC.bar) - * compA.foo = 'bar' - * entity.remove('compB') - * }) - * - * @param {...Object} args - Can pass component names, arrays of component names, and a callback, - * in any order. - * - * **{...string}**: The component names to match entities with. This checks if the entity - * has ALL of the specified components, but does not check for additional components. - * - * **{Function}**: The callback to call for each matched entity. Takes (entity.data, entity). - * Entity data is an object of {[componentName]: [component]}, that can be destructured with syntax - * shown in the examples. - * - * @return {MapIterator} If no callback specified, then returns a one-time-use iterator to the entities. - * Otherwise, returns the last loop iteration status, returned by the callback. - */ - each(...args) { - // Gather component names and a callback (if any) from args - const compNames = [] - let callback - for (const arg of args) { - if (typeof arg === 'string') { - compNames.push(arg) - } else if (typeof arg === 'function') { - callback = arg - } else if (Array.isArray(arg)) { - // Add 1-level deep arrays of strings as separate component names - for (const name of arg) { - compNames.push(name) - } - } else { - throw new Error( - `Unknown argument ${arg} with type ${typeof arg} passed to world.each().` - ) - } - } - - // Get indexed map of entities - const entities = this.index.query(...compNames) - - if (callback) { - // Go through the map of entities - let status - for (const entity of entities) { - // Pass component data and the main entity - status = callback(entity.data, entity) - - // Stop the iteration when the callback returns false - if (status === false) { - break - } - } - return status - } - return entities - } - - /** - * Returns an array of entities with matching components - * Simplified version of each(), returns an array instead of an iterator. - * - * @example - * const entities = world.get('player', 'sprite') - * - * @param {Array} componentNames - The component names to match on. See each() for how this matches. - * - * @return {Array} Array of entities, instead of iterator like each(). - */ - get(...componentNames) { - return [...this.each(componentNames)] - } - - /** - * Returns an entity by ID - * Returns undefined if it doesn't exist - * - * @example - * world.getEntityById(123) - * - * @param {number} entityId - The entity ID to lookup for the entity - * - * @return {Entity} Entity if found, otherwise undefined - */ - getEntityById(entityId) { - return this.entities.get(entityId) - } - - /** - * Registers entity prototype(s). Any existing prototype names that are the same will be overwritten - * - * @example - * world.prototype({ - * Movable: { - * position: {}, - * velocity: {} - * } - * }) - * - * @param {Object} data - Object structure to register as a prototype. Should be a dictionary with the top level keys - * being the prototype names. Can also be a JSON formatted string. - * - * @return {number} Number of prototypes added. - */ - prototype(data) { - let count = 0 - - // Convert to an object when given a string - if (typeof data === 'string') { - data = JSON.parse(data) - } - - // Data must be an object at this point - if (typeof data === 'object') { - // Iterate through prototype names - for (let protoName in data) { - let inputObject = data[protoName] - let protoObject = {} - // Iterate through component names - for (let compName in inputObject) { - // Store strings of each component - protoObject[compName] = JSON.stringify( - inputObject[compName] - ) - } - this.entityTemplates[protoName] = protoObject - ++count - } - } - - return count - } - - /** - * Injects context into a system based on current context state - * @ignore - */ - _injectContext(system) { - if (this.contextData && this.contextKey) { - // Inject into specified key - system[this.contextKey] = this.contextData - } else if (this.contextData) { - // Inject as keys of context - for (const key in this.contextData) { - system[key] = this.contextData[key] - } - } - } +export class World { + /** + * Constructs an instance of the world. + * + * @param {object} options - The initial systems, components, and context to setup in the world. + * + * @example + * const world = new World({ + * + * }) + */ + constructor(options) { + /** @ignore */ + this.systems = new SystemStorage() + /** @ignore */ + this.entities = new EntityStorage(this) + + // Register components, context, and systems + if (options) { + if (options.components) { + for (const name in options.components) { + this.component(name, options.components[name]) + } + } + if (options.context) { + this.context(options.context) + } + if (options.systems) { + for (const systemClass of options.systems) { + this.system(systemClass) + } + } + } + } + + /** + * Removes all entities from the world. + * Does not affect any registered systems or components. + * + * @example + * world.clear() + */ + clear() { + this.entities.clear() + } + + /** + * Registers a component type to the world. Components must be constructable. If the component has + * an onCreate(), it is passed all of the arguments from methods like entity.set(). Also, components + * can have an onRemove() method, which gets called when removing that component from an entity. + * + * @param {string} name - The name + * @param {function} componentClass - The component class, must be a constructable class or function + * + * @example + * world.component('myComponent', class { + * // It is highly recommended to use onCreate() over constructor(), because the component + * // will have already been added to the entity. In the constructor(), it is not safe to use + * // "entity" because it does not contain the current component while still in the constructor. + * onCreate(some, args) { + * this.some = some + * this.args = args + * this.entity.set('whatever') // this.entity is auto-injected, and this is safe to do here + * } + * }) + * // entity === the new entity object + * // some === 10 + * // args === 500 + * world.entity().set('myComponent', 10, 500) + * + * @return {string} Registered component name on success, undefined on failure + */ + component(name, componentClass) { + this.entities.registerComponent(name, componentClass) + } + + /** + * Creates a new entity in the world + * + * @example + * world.entity() + * + * @return {Entity} The new entity created + */ + entity() { + return this.entities.createEntity() + } + + /** + * Sets a context object that is automatically injected into all existing and new systems. + * Calling this multiple times will overwrite any previous contexts passed. One caveat is that + * you can only start to use the injected context in systems starting with init(). It is not + * available in the constructor. + * + * @param {Object} [data] - The object to use as context to pass to systems. + * All the keys inside the context object will be spread into the top-level of the system. + * + * @example + * const state = { app: new PIXI.Application() } + * const world = new World() + * world.context(state) // systems can directly use this.app + * world.system(...) + * + * @example + * world.context(state, 'state') // systems use this.state.app + * + * @return {Entity} The new entity created + */ + context(data) { + this.systems.setContext(data) + } + + /** + * Registers a system to the world. + * The order the systems get registered, is the order then run in. + * + * @example + * // Movement system (basic example) + * class MovementSystem { + * run(dt) { + * world.each('position', 'velocity', ({ position, velocity }) => { + * position.x += velocity.x * dt + * position.y += velocity.y * dt + * }) + * } + * } + * // Input system (advanced example) + * class InputSystem { + * init(key) { + * // Have access to this.keyboard here, but not in constructor + * this.key = key + * } + * run(dt) { + * if (this.keyboard.isPressed(this.key)) { + * world.each('controlled', 'velocity', ({ velocity }, entity) => { + * // Start moving all controlled entities to the right + * velocity.x = 1 + * velocity.y = 0 + * // Can also use the full entity here, in this case to add a new component + * entity.set('useFuel') + * }) + * } + * } + * } + * // Inject context (see world.context()) + * world.context({ keyboard: new Keyboard() }) + * // Register systems in order (this method) + * world.system(InputSystem, 'w') // pass arguments to init/constructor + * world.system(MovementSystem) + * // Run systems (can get dt or frame time) + * world.run(1000.0 / 60.0) + * + * @param {Function} systemClass - The system class to instantiate. Can contain a + * constructor(), init(), run(), or any other custom methods/properties. + * + * @param {...Object} args - The arguments to forward to the system's constructor and init. + * Note that it is recommended to use init if using context, see world.context(). + */ + system(systemClass, ...args) { + // TODO: Get rid of args because of context + this.systems.register(systemClass, ...args) + } + + /** + * Calls run() on all systems. These methods can return true to cause an additional rerun of all systems. + * Reruns will not receive the args passed into run(), as a way to identify reruns. + * + * @example + * world.run(deltaTime) + * + * @example + * // Example flow of method call order: + * // Setup systems: + * world.system(systemA) + * world.system(systemB) + * // During world.run(): + * // systemA.run() + * // systemB.run() + * + * @param {...Object} [args] - The arguments to forward to the systems' methods + */ + run(...args) { + this.systems.run(...args) + } + + /** + * Iterate through components and entities with all of the specified component names + * + * @example + * // Use a callback to process entities one-by-one + * world.each('comp', ({ comp }) => { comp.value = 0 }) + * + * @example + * // Get an iterator for the entities + * const it = world.each('comp') + * for (let entity of it) {...} + * + * @example + * // Pass multiple components, arrays, use extra entity parameter, + * // and destructure components outside the query + * world.each('compA', ['more', 'comps'], 'compB', ({ compA, compC }, entity) => { + * if (compC) compC.foo(compC.bar) + * compA.foo = 'bar' + * entity.remove('compB') + * }) + * + * @param {...Object} args - Can pass component names, arrays of component names, and a callback, + * in any order. + * + * **{...string}**: The component names to match entities with. This checks if the entity + * has ALL of the specified components, but does not check for additional components. + * + * **{Function}**: The callback to call for each matched entity. Takes (entity.data, entity). + * Entity data is an object of {[componentName]: [component]}, that can be destructured with syntax + * shown in the examples. + * + * @return {MapIterator} If no callback specified, then returns a one-time-use iterator to the entities. + * Otherwise, returns the last loop iteration status, returned by the callback. + */ + each(...args) { + return this.entities.each(...args) + } } - -exports.World = World diff --git a/src/world.test.js b/src/world.test.js deleted file mode 100644 index 3b621d2..0000000 --- a/src/world.test.js +++ /dev/null @@ -1,819 +0,0 @@ -const { World } = require('../index.js') -const { Entity } = require('./entity.js') -const { - getSize, - has, - testIndexers, - assert -} = require('./test_utils.js') - -test('world: create a world', () => { - const world = new World() - assert(world instanceof World) - assert(typeof world.component === 'function') -}) - -test('component: define a component', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - - this.inc = (a) => { - return a + 1 - } - }) - let ent = world.entity().set('position', 1, 2) - assert('position' in world.components) - assert(Object.keys(world.components).length == 1) - assert(ent.has('position')) - assert(ent.get('position').x === 1) - assert(ent.get('position').y === 2) - assert(ent.get('position').inc(5) === 6) - - // Using class syntax - world.component('velocity', class { - constructor(x = 0, y = 0) { - this.x = x - this.y = y - } - - inc(a) { - return a + 1 - } - }) - let ent2 = world.entity().set('velocity', 1, 2) - assert('velocity' in world.components) - assert(Object.keys(world.components).length == 2) - assert(ent2.has('velocity')) - assert(ent2.get('velocity').x === 1) - assert(ent2.get('velocity').y === 2) - assert(ent2.get('velocity').inc(5) === 6) - - // Should throw when calling "components" setter - expect(() => (ent2.components = ['anything'])).toThrow() -})) - -test('component: README example', testIndexers(world => { - // Create player - let player = world.entity().set('health', { value: 100 }) - - // Create enemies - world.entity().set('damages', 10) - world.entity().set('damages', 30) - - // Apply damage - world.each('damages', ({damages: amount}) => { - player.get('health').value -= amount - }) - - // Player now has reduced health - assert(player.get('health').value === 60) -})) - -test('component: define an object component (should be invalid)', testIndexers(world => { - let result = world.component('position', { - x: 0, - y: 0 - }) - assert(result === undefined) - let result2 = world.component('invalid', 555) - assert(result2 === undefined) - assert(Object.keys(world.components).length === 0) -})) - -test('component: define an empty component', testIndexers(world => { - let result = world.component('position') - assert(result === undefined) -})) - -test('component: use an empty component', testIndexers(world => { - let ent = world.entity().update('position', { - x: 1 - }) - assert(ent.has('position')) - assert(ent.get('position').x === 1) - - let ent2 = world.entity().set('velocity', { - x: 2 - }) - assert(ent2.has('velocity')) - assert(ent2.get('velocity').x === 2) - ent2.set('velocity', { - y: 3 - }) - assert(ent2.get('velocity').x === undefined) - assert(ent2.get('velocity').y === 3) - ent2.update('velocity', { - x: 42 - }) - assert(ent2.get('velocity').x === 42) - assert(ent2.get('velocity').y === 3) - - let ent3 = world.entity().set('singleValue', 5) - assert(ent3.has('singleValue')) - assert(ent3.get('singleValue') === 5) - ent3.set('singleValue', 500) - assert(ent3.get('singleValue') === 500) - - let ent4 = world.entity().set('string', 'hello') - assert(ent4.has('string')) - assert(ent4.get('string') === 'hello') - ent4.set('string', 'goodbye') - assert(ent4.get('string') === 'goodbye') - - ent4.remove('string') - assert(!ent4.has('string')) -})) - -test('component: test clearing with indexes', testIndexers(world => { - world.component('position', function(x = 0, y = 0) { - this.x = x - this.y = y - }) - world.component('velocity') - world.component('sprite') - let results = world.each() - results = world.each('position') - results = world.each('position', 'velocity') - - world.entity().set('position', 1, 2).set('velocity') - world.entity().set('position', 3, 4).set('velocity') - world.entity().set('position', 5, 6) - world.entity().set('velocity') - - let count = 0 - world.each('position', ({position}) => { - assert(position.x >= 1) - assert(position.y >= 2) - ++count - }) - assert(count === 3) - - world.clear() - - count = 0 - world.each('position', ({position}) => { - ++count - }) - world.each((_, ent) => { - ++count - }) - assert(count === 0) -})) - -test('component: test entity creation with constructor parameters', testIndexers(world => { - let when = 0 - world.component('sprite', class { - onCreate(texture, size, invalid) { - this.texture = texture - this.size = size - this.constructorCalled = ++when - assert(texture && texture === this.texture) - assert(size && size === this.size) - assert(invalid === undefined) - - // Regression in 0.3.0, fixed in 0.3.1 - assert(this.entity.get('sprite') === this) - } - }) - - let ent = world.entity().set('sprite', 'test.png', 100) - assert(ent.get('sprite').constructorCalled === 1) - assert(ent.get('sprite').entity === ent) - assert(ent.get('sprite').texture === 'test.png') - assert(ent.get('sprite').size === 100) - -})) - -test('component: test clearing with onRemove', testIndexers(world => { - let spriteCount = 0 - world.component('sprite', class { - constructor() { - ++spriteCount - } - - onRemove() { - --spriteCount - } - }) - - let ent = world.entity().set('sprite') - assert(spriteCount === 1) - - let ent2 = world.entity().set('sprite') - assert(spriteCount === 2) - - world.clear() - assert(spriteCount === 0) -})) - -test('component: test detach and attach', testIndexers(world => { - let spriteCount = 0 - world.component('sprite', class { - constructor() { - ++spriteCount - } - - onRemove() { - --spriteCount - } - }) - - let ent = world.entity().set('sprite').set('position', {x: 1}) - assert(spriteCount === 1) - - let ent2 = world.entity().set('sprite').set('position', {x: 2}) - assert(spriteCount === 2) - - // Test detaching - assert(ent.valid()) - ent.detach() - assert(!ent.valid()) - assert(spriteCount === 2) - assert(world.entities.size === 1) - assert(getSize(world.each('position')) === 1) - assert(world.get('position').length === 1) - assert(world.get('position')[0].get('position').x === 2) - assert(ent.get('position').x === 1) - - // Test attaching - ent.attach(world) - assert(ent.valid()) - assert(spriteCount === 2) - assert(world.entities.size === 2) - assert(getSize(world.each('position')) === 2) - assert(world.get('position').length === 2) - assert(ent.get('position').x === 1) - - // Test edge cases - ent.detach() - assert(!ent.valid()) - ent.detach() - assert(!ent.valid()) - ent.attach() - assert(!ent.valid()) - ent.attach(world) - assert(ent.valid()) -})) - -test('component: test detached entities', testIndexers(world => { - let ent = world.entity() - .set('sprite', {texture: 'image.png'}) - .set('position', 5) - - assert(ent.valid()) - assert(ent.has('sprite')) - assert(ent.has('position')) - assert(ent.get('sprite').texture === 'image.png') - assert(ent.get('position') === 5) - - ent.detach() - - assert(!ent.valid()) - assert(ent.has('sprite')) - assert(ent.has('position')) - assert(ent.get('sprite').texture === 'image.png') - assert(ent.get('position') === 5) - - ent.set('velocity', {x: 10}) - assert(ent.has('velocity')) - assert(ent.get('velocity').x === 10) - - ent.set('position', 6) - assert(ent.has('position')) - assert(ent.get('position') === 6) - - ent.remove('position') - assert(!ent.has('position')) - - // Create entity outside of the world - let ent2 = new Entity() - assert(!ent2.valid()) - ent2.set('velocity', {x: 30}) - ent2.set('position', 7) - assert(ent2.has('velocity', 'position')) - assert(ent2.get('velocity').x === 30) - assert(ent2.get('position') === 7) - ent2.removeAll() - assert(!ent2.has('velocity')) - assert(!ent2.has('position')) -})) - -test('system: define a system', testIndexers(world => { - world.system(class {}) - assert(world.systems.length == 1) -})) - -test('system: define a system with arguments', testIndexers(world => { - let velocitySystem = class { - constructor(maxVelocity, canvas, textures) { - this.maxVelocity = maxVelocity - this.canvas = canvas - this.textures = textures - } - } - world.component('velocity') - world.system(velocitySystem, 3500, 'someCanvas', ['textures.png']) - assert(world.systems[0].maxVelocity === 3500) - assert(world.systems[0].canvas === 'someCanvas') - assert(world.systems[0].textures[0] === 'textures.png') -})) - -test('system: define a system with context (no key)', testIndexers(world => { - const state = { - maxVelocity: 3500, - canvas: 'someCanvas', - textures: ['textures.png'] - } - const ran = [] - const velocitySystem = class { - init(existing) { - this.existing = existing - if (!existing) { - expect(this.maxVelocity).toEqual(3500) - expect(this.canvas).toEqual('someCanvas') - expect(this.textures[0]).toEqual('textures.png') - ran.push(existing) - } - } - run() { - expect(this.maxVelocity).toEqual(3500) - expect(this.canvas).toEqual('someCanvas') - expect(this.textures[0]).toEqual('textures.png') - ran.push(this.existing) - } - } - world.component('velocity') - world.system(velocitySystem, true) // Existing system - world.context(state) // Set keyless context - world.system(velocitySystem, false) // New system - expect(world.systems.length).toEqual(2) - world.run() - expect(ran).toEqual([false, true, false]) -})) - -test('system: define a system with context (specific key)', testIndexers(world => { - const state = { - maxVelocity: 3500, - canvas: 'someCanvas', - textures: ['textures.png'] - } - const ran = [] - const velocitySystem = class { - init(existing) { - this.existing = existing - if (!existing) { - expect(this.state.maxVelocity).toEqual(3500) - expect(this.state.canvas).toEqual('someCanvas') - expect(this.state.textures[0]).toEqual('textures.png') - ran.push(existing) - } - } - run() { - expect(this.state.maxVelocity).toEqual(3500) - expect(this.state.canvas).toEqual('someCanvas') - expect(this.state.textures[0]).toEqual('textures.png') - ran.push(this.existing) - } - } - world.component('velocity') - world.system(velocitySystem, true) // Existing system - world.context(state, 'state') // Set keyed context - world.system(velocitySystem, false) // New system - expect(world.systems.length).toEqual(2) - world.run() - expect(ran).toEqual([false, true, false]) -})) - -test('system: system iteration', testIndexers(world => { - world.component('position') - world.component('velocity') - world.system(class { - run(dt, total) { - assert(dt > 0) - assert(total > 0) - world.each('position', 'velocity', ({ position, velocity }, ent) => { - assert(position) - assert(velocity) - position.x += velocity.x - position.y += velocity.y - assert(ent) - assert(ent.has('position')) - assert(ent.has('velocity')) - assert(dt > 0) - assert(total > 0) - }) - } - }) - - let dt = 0.1667 - let total = dt - - let entA = world.entity() - let entB = world.entity() - let entC = world.entity() - entA.update('position', {x: 1, y: 1}).update('velocity', {x: 1, y: 0}) - entB.update('position', {x: 30, y: 40}).update('velocity', {x: -1, y: 2}) - - assert(entA.get('position').x == 1 && entA.get('position').y == 1) - assert(entB.get('position').x == 30 && entB.get('position').y == 40) - - world.run(dt, total) - - assert(entA.get('position').x == 2 && entA.get('position').y == 1) - assert(entB.get('position').x == 29 && entB.get('position').y == 42) - - total += dt - world.run(dt, total) - - assert(entA.get('position').x == 3 && entA.get('position').y == 1) - assert(entB.get('position').x == 28 && entB.get('position').y == 44) -})) - -test('system: system methods', testIndexers(world => { - expect(world.component('position')).toBeUndefined() - - let methodsCalled = 0 - - world.system(class { - constructor() { - this.val = 10 - ++methodsCalled - } - init() { - ++methodsCalled - } - run() { - ++methodsCalled - assert(this.val === 10) - world.each('position', ({position}) => { - position.x = 1 - ++methodsCalled - assert(this.val === 10) - }) - } - }) - - world.system(class {}) - world.system() - - world.entity().set('position', {}) - assert(methodsCalled == 2) - world.run() - assert(methodsCalled == 4) -})) - -test('system: system edge cases', testIndexers(world => { - world.component('position', class {}) - world.component('velocity', class {}) - - let testEnt0 = world.entity().set('position').set('velocity') - let testEnt2 = null - for (let i = 0; i < 100; ++i) { - let tmpEnt = world.entity() - tmpEnt.set('position').set('velocity') - if (i == 80) { - testEnt2 = tmpEnt - } - } - - let testEnt1 = world.entity().set('position').set('velocity') - let count = 0 - - world.system(class { - run() { - world.each(['position', 'velocity'], ({position, velocity}, ent) => { - ++count - if (count == 1) { - testEnt1.removeAll() - testEnt2.remove('position') - testEnt0.remove('velocity') - return - } - assert(position) - assert(velocity) - position.x += velocity.x - position.y += velocity.y - assert(ent) - assert(ent.has('position')) - assert(ent.has('velocity')) - - // Make sure the test entities do not show up here - assert(ent.id !== testEnt0.id) - assert(ent.id !== testEnt1.id) - assert(ent.id !== testEnt2.id) - }) - } - }) - let entA = world.entity() - let entB = world.entity() - let entC = world.entity() - entA.update('position', {x: 1, y: 1}).update('velocity', {x: 1, y: 0}) - entB.update('position', {x: 30, y: 40}).update('velocity', {x: -1, y: 2}) - - assert(entA.get('position').x == 1 && entA.get('position').y == 1) - assert(entB.get('position').x == 30 && entB.get('position').y == 40) - - world.run() - - assert(entA.get('position').x == 2 && entA.get('position').y == 1) - assert(entB.get('position').x == 29 && entB.get('position').y == 42) - - world.run() - - assert(entA.get('position').x == 3 && entA.get('position').y == 1) - assert(entB.get('position').x == 28 && entB.get('position').y == 44) -})) - -test('system: adding entities to index', testIndexers(world => { - world.entity().set('a').set('b') - assert(world.get('a', 'b').length === 1) - world.get('a', 'b')[0].destroy() - assert(world.get('a', 'b').length === 0) - world.entity().set('a') - assert(world.get('a', 'b').length === 0) - world.entity().set('b') - assert(world.get('a', 'b').length === 0) - world.entity().set('b').set('a') - assert(world.get('a', 'b').length === 1) -})) - -test('system: onRemove edge cases', testIndexers(world => { - world.component('position', class { - onCreate(value) { - this.value = value - } - - onRemove() { - this.entity.set('somethingElse') - } - }) - - let entity = world.entity().set('position') - - expect(() => entity.destroy()).toThrow() -})) - -test('system: indexing edge cases', testIndexers(world => { - // This test was to discover the "adding entities to index" and "onRemove edge cases" tests above - // Keeping it in case there are other problems in the future - - let g = { count: 0 } - - // Define components - world.component('position', class { - onCreate(x = 0, y = 0) { - this.x = x - this.y = y - } - }) - world.component('velocity', class { - onCreate(x = 0, y = 0) { - this.x = x - this.y = y - } - }) - world.component('sprite', class { - onCreate(texture) { - this.texture = texture - } - - onRemove() { - g.entity.set('sideEffect', ++g.count) - } - }) - - const REPEAT = 3 - - for (let i = 0; i < REPEAT; ++i) { - - g.count = 0 - - // Create test entities - world.entity().set('noOtherComponents', 0) - world.entity().set('position', 1) - g.entity = world.entity().set('velocity', 2) - world.entity().set('sprite', 'three') - world.entity().set('position', 4).set('velocity', 4) - world.entity().set('position', 5).set('velocity', 5).set('sprite', 'five') - world.entity().set('position', 6).set('sprite', 'six') - world.entity().set('velocity', 7).set('sprite', 'seven') - - // Ensure initial indexes are good - for (let i = 0; i < REPEAT; ++i) { - assert(world.get('noOtherComponents').length === 1) - assert(world.get('sideEffect').length === 0) - assert(world.get('sprite').length === 4) - assert(world.get('velocity').length === 4) - assert(world.get().length === 8) - assert(world.get('position').length === 4) - assert(world.get('position', 'velocity').length === 2) - assert(world.get('position', 'sprite').length === 2) - assert(world.get('position', 'velocity', 'sprite').length === 1) - assert(world.get('velocity', 'sprite').length === 2) - } - - // Remove test entities, create more test entities - let count = 0 - world.each('sprite', ({sprite}, entity) => { ++count; entity.destroy() }) - assert(count === 4) - - count = 0 - world.each('sprite', ({sprite}, entity) => { ++count; entity.destroy() }) - assert(count === 0) - - - assert(g.count === 4) - - // Ensure indexes are still good - for (let i = 0; i < REPEAT; ++i) { - assert(world.get().length === 4) - assert(world.get('noOtherComponents').length === 1) - assert(world.get('sideEffect').length === 1) - assert(world.get('sprite').length === 0) - assert(world.get('velocity').length === 2) - assert(world.get('position').length === 2) - } - - count = 0 - world.each('velocity', ({velocity}, entity) => { ++count; entity.destroy() }) - assert(count === 2) - - count = 0 - world.each('velocity', ({velocity}, entity) => { ++count; entity.destroy() }) - assert(count === 0) - - // Ensure indexes are still good - for (let i = 0; i < REPEAT; ++i) { - assert(world.get().length === 2) - assert(world.get('noOtherComponents').length === 1) - assert(world.get('sideEffect').length === 0) - assert(world.get('sprite').length === 0) - assert(world.get('velocity').length === 0) - assert(world.get('position').length === 1) - } - - count = 0 - world.each('position', ({position}, entity) => { ++count; entity.destroy() }) - assert(count === 1) - - count = 0 - world.each('position', ({position}, entity) => { ++count; entity.destroy() }) - assert(count === 0) - - world.get('noOtherComponents')[0].destroy() - - // Ensure new indexes are good - for (let i = 0; i < REPEAT; ++i) { - assert(world.get().length === 0) - assert(world.get('noOtherComponents').length === 0) - assert(world.get('sideEffect').length === 0) - assert(world.get('sprite').length === 0) - assert(world.get('velocity').length === 0) - assert(world.get('position').length === 0) - } - } -})) - -test('system: system variadic arguments with optional components', testIndexers(world => { - let created = false - world.system(class { - constructor(first, second) { - assert(first === 1) - assert(second === 2) - created = true - } - }, 1, 2) - assert(created) -})) - -test('system: use the each() method', testIndexers(world => { - let ent1 = world.entity().set('position', {}).set('"velocity"', {}) - let ent2 = world.entity().set('position', {}) - let ent3 = world.entity().set('position:"velocity"', {}) - let externalVar = 5 - world.each('position', ({position: pos}, ent) => { - assert(pos) - assert(ent) - assert(ent.has('position')) - assert(externalVar === 5) - }) - world.each('position', function({position: pos}, ent) { - assert(pos) - assert(ent) - assert(ent.has('position')) - assert(externalVar === 5) - }) - - // Test hash collisions and escaping - world.each('position:"velocity"') - let count = 0 - world.each('position', '"velocity"', function({position: pos, ['"velocity"']: vel}, ent) { - assert(pos) - assert(vel) - assert(ent) - assert(ent.has('position', '"velocity"')) - ++count - }) - assert(count === 1) - - // Test iterator usage - count = 0 - let results = world.each('position', '"velocity"') - for (let ent of results) { - ++count - } - assert(count === 1) - - // Passing callbacks cause the return value to be undefined - results = world.each('position', () => {}) - assert(results === undefined) - results = world.each(() => {}) - assert(results === undefined) - - // Test breaking out of the loop - count = 0 - world.each('position', function({position}, ent) { - assert(position) - assert(ent) - assert(ent.has('position')) - ++count - return false - }) - assert(count === 1) - - // And just to be sure there are more than 1 - count = world.get('position').length - assert(count === 2) - - // Invalid args - expect(() => { - world.each('position', () => {}, 999) - }).toThrow() -})) - -test('system: test indexing with each()', testIndexers(world => { - world.component('position', function(val = 0) { - this.val = val - }) - world.component('velocity') - world.component('sprite') - let ent1 = world.entity().set('position', 1).set('velocity') - let ent2 = world.entity().set('position', 10) - let ent3 = world.entity().set('position', 100).set('velocity').set('sprite') - let count = 0 - world.each('position', 'velocity', ({position: pos, velocity: vel}, ent) => { - assert(ent.has('position', 'velocity')) - count += pos.val - }) - assert(count == 101) - count = 0 - - ent1.remove('position') - ent1.set('sprite') - ent2.set('velocity') - world.each('position', 'velocity', ({position: pos, velocity: vel}, ent) => { - assert(ent.has('position', 'velocity')) - count += pos.val - }) - assert(count == 110) - - ent1.remove('sprite') - ent2.remove('sprite') - ent3.remove('sprite') - - // Query for all entities - let test = world.each() - assert(getSize(test) == 3) - - let ent4 = world.entity() - assert(getSize(world.each()) == 4) - assert(has(world.each(), ent4)) - - ent4.set('velocity') - assert(getSize(world.each()) == 4) - assert(has(world.each(), ent4)) - - ent4.remove('velocity') - assert(getSize(world.each()) == 4) - assert(has(world.each(), ent4)) - - ent4.destroy() - assert(getSize(world.each()) == 3) - assert(!has(world.each(), ent4)) - - count = 0 - world.each(ent => { - ++count - }) - assert(count == 3) - - count = 0 - world.system(class { - run() { - world.each(() => { ++count }) - } - }) - world.run() - expect(count).toEqual(3) -})) diff --git a/test/entity.test.js b/test/entity.test.js new file mode 100644 index 0000000..f6e26e6 --- /dev/null +++ b/test/entity.test.js @@ -0,0 +1,465 @@ +import { World } from '../index.js' +import { assert } from './test_utils.js' + +test('entity: create an entity', () => { + const world = new World() + let ent = world.entity() + assert(world.entities.entities.size === 1) + assert(typeof ent.id === 'number' && ent.id === 1) +}) + +test('entity: test if ID is read-only', () => { + const world = new World() + let ent = world.entity() + expect(() => (ent.id = 5)).toThrow() + assert(typeof ent.id === 'number' && ent.id === 1) +}) + +test('entity: valid entities', () => { + const world = new World() + let entityA = world.entity().set('test') + let entityB = world.each('test')[0] + assert(entityA.valid()) + assert(entityB.valid()) + assert(entityA.id === entityB.id) + assert(entityA === entityB) + + entityA.destroy() + assert(!entityA.valid()) + assert(!entityB.valid()) +}) + +test('entity: remove an entity', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + }) + let ent = world.entity() + ent.set('position') + ent.get('position').val = 100 + + expect(world.entities.entities.size).toBe(1) + assert(Object.keys(world.entities.componentClasses).length == 1) + assert(ent.has('position')) + assert(ent.get('position').val === 100) + assert(ent.valid()) + + ent.destroy() + + assert(world.entities.entities.size == 0) + assert(Object.keys(world.entities.componentClasses).length == 1) + assert(!ent.valid()) + assert(!ent.has('position')) + + // Just for safe measure + ent.destroy() + + assert(world.entities.entities.size == 0) + assert(Object.keys(world.entities.componentClasses).length == 1) + assert(!ent.valid()) + assert(!ent.has('position')) +}) + +test('entity: get and set components', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + }) + expect(() => { + world.component('empty') + }).toThrow() + let ent = world.entity() + ent.set('position', 5) + assert(ent.has('position')) + assert(ent.hasAny('position')) + assert(ent.components.length == 1) + assert(ent.get('position').x === 5) + assert(ent.get('position').y === 0) + + Object.assign(ent.access('position', {}), { y: 3 }) + assert(ent.has('position')) + assert(ent.hasAny('position')) + assert(ent.components.length == 1) + assert(ent.get('position').x === 5) + assert(ent.get('position').y === 3) + + Object.assign(ent.access('object', {}), { val: 50 }) + assert(ent.has('object')) + assert(ent.hasAny('object')) + assert(ent.components.length == 2) + assert(ent.get('object').val === 50) + + Object.assign(ent.access('empty', {}), { testing: 100 }) + assert(ent.has('empty')) + assert(ent.hasAny('empty')) + assert(ent.components.length == 3) + assert(ent.get('empty').testing === 100) + + ent.set('anonymous') + assert(ent.components.length == 4) + assert(ent.has('anonymous')) + assert(ent.hasAny('anonymous')) + + // Access test + ent.removeAll() + assert(!ent.has('position')) + assert(!ent.hasAny('position')) + ent.access('position').x = 300 + assert(ent.has('position')) + assert(ent.hasAny('position')) + assert(ent.get('position').x === 300) + + // Get test + ent.removeAll() + assert(!ent.has('position')) + assert(ent.get('position') === undefined) + assert(!ent.has('position')) + ent.set('position', 333) + assert(ent.get('position').x === 333) + assert(ent.get('position').y === 0) + + // Undefined component tests + ent.removeAll() + ent.set('invalid', { a: 'test' }) + assert(ent.get('invalid').a === 'test') + + ent.set('invalid', { b: 'test2' }) + assert(ent.get('invalid').a === undefined) + assert(ent.get('invalid').b === 'test2') + + ent.set('invalid2', 5) + assert(ent.get('invalid2') === 5) + + ent.set('invalid2', 'test') + assert(ent.get('invalid2') === 'test') + + ent.set('invalid2', ['test']) + assert(ent.get('invalid2')[0] === 'test') +}) + +test('entity: check existence of components', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + }) + const ent = world.entity() + assert(!ent.hasAny()) + assert(ent.has()) + ent.set('a') + ent.set('b') + assert(ent.has()) + assert(ent.has('a')) + assert(ent.has('a', 'b')) + assert(!ent.has('a', 'b', 'c', 'd')) + assert(!ent.hasAny()) + assert(ent.hasAny('a')) + assert(ent.hasAny('a', 'b')) + assert(ent.hasAny('', 'a', 'c')) + assert(ent.hasAny('a', 'b', 'c', 'd')) + + ent.removeAll() + assert(ent.has()) + assert(!ent.has('a')) + assert(!ent.has('a', 'b')) + assert(!ent.has('a', 'b', 'c', 'd')) + assert(!ent.hasAny()) + assert(!ent.hasAny('a')) + assert(!ent.hasAny('a', 'b')) + assert(!ent.hasAny('', 'a', 'c')) + assert(!ent.hasAny('a', 'b', 'c', 'd')) +}) + +test('entity: setRaw', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + }) + let ent = world.entity() + ent.set('position', 10, 20) + assert(ent.has('position')) + assert(ent.get('position').x === 10) + assert(ent.get('position').y === 20) + + // Set raw tests + const previous = ent.get('position') + ent.remove('position') + assert(!ent.has('position')) + + ent.setRaw('position', previous) + + assert(ent.has('position')) + assert(ent.get('position').x === 10) + assert(ent.get('position').y === 20) + + // Invalid entity + ent.remove('position') + ent.detach() + ent.setRaw('position', previous) + assert(ent.has('position')) + assert(ent.get('position').x === 10) + assert(ent.get('position').y === 20) +}) + +test('entity: remove components', () => { + const world = new World() + let ent = world.entity().set('position').set('velocity') + assert(ent.components.length == 2) + assert(ent.has('position')) + assert(ent.has('velocity')) + + ent.remove('invalid') + ent.remove() + + ent.remove('position') + assert(ent.components.length == 1) + assert(!ent.has('position')) + assert(ent.has('velocity')) + + ent.remove('velocity') + assert(ent.components.length == 0) + assert(!ent.has('position')) + assert(!ent.has('velocity')) + + ent.set('position').set('velocity') + assert(ent.components.length == 2) + assert(ent.has('position')) + assert(ent.has('velocity')) + ent.removeAll() + assert(ent.components.length == 0) + assert(!ent.has('position')) + assert(!ent.has('velocity')) + + // Remove many components + ent.set('position').set('velocity').set('testA').set('testB') + assert(ent.components.length == 4) + assert(ent.has('position')) + assert(ent.has('velocity')) + assert(ent.has('testA')) + assert(ent.has('testB')) + ent.remove('invalidA', 'position', 'testA', 'invalidB') + assert(!ent.has('position')) + assert(ent.has('velocity')) + assert(!ent.has('testA')) + assert(ent.has('testB')) + ent.remove('velocity', 'testB') + assert(!ent.has('position')) + assert(!ent.has('velocity')) + assert(!ent.has('testA')) + assert(!ent.has('testB')) +}) + +test('entity: remove components - onRemove', () => { + const world = new World() + world.component('test', function (obj) { + this.obj = obj + this.obj.created = true + + this.onRemove = () => { + this.obj.removed = true + } + }) + let obj = { + created: false, + removed: false, + } + let ent = world.entity().set('test', obj) + assert(ent.has('test')) + assert(obj.created) + assert(!obj.removed) + + ent.remove('test') + assert(ent.components.length == 0) + assert(!ent.has('test')) + assert(obj.created) + assert(obj.removed) + + let obj2 = { + created: false, + removed: false, + } + let ent2 = world.entity().set('test', obj2) + assert(ent2.has('test')) + assert(obj2.created) + assert(!obj2.removed) + + ent2.destroy() + assert(obj2.created) + assert(obj2.removed) +}) + +test('entity: serialize components', () => { + const world = new World() + let ent = world.entity().set('position', { x: 4, y: 6 }) + + let data = JSON.parse(ent.toJSON()) + assert(data) + assert(data.position) + assert(data.position.x === 4) + assert(data.position.y === 6) +}) + +test('entity: serialize custom components', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + + this.toJSON = () => ({ result: this.x * this.y }) + }) + let ent = world.entity().set('position', 4, 6) + + let data = JSON.parse(ent.toJSON()) + assert(data) + assert(data.position) + assert(data.position.result === 24) +}) + +test('entity: deserialize components', () => { + const world = new World() + let ent = world.entity() + assert(ent.components.length == 0) + + ent.fromJSON('{"position": {"x": 4, "y": 6}}') + assert(ent.has('position')) + assert(ent.components.length == 1) + assert(ent.get('position')) + assert(ent.get('position').x === 4) + assert(ent.get('position').y === 6) +}) + +test('entity: deserialize custom components', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + + this.toJSON = () => ({ result: this.x * this.y }) + + this.fromJSON = data => { + this.x = data.result / 2 + this.y = 2 + } + }) + + // Old deserialization test + let ent = world.entity() + assert(ent.components.length == 0) + ent.fromJSON('{"position": {"result": 24}}') + assert(ent.has('position')) + assert(ent.components.length == 1) + assert(ent.get('position')) + assert(ent.get('position').x === 12) + assert(ent.get('position').y === 2) + + // Full entity-based serialization/deserialization test + let ent2 = world.entity().set('position', 7, 4) + let jsonData = ent2.toJSON() + let ent3 = world.entity().fromJSON(jsonData) + assert(ent3.has('position')) + assert(ent3.get('position').x === 14) + assert(ent3.get('position').y === 2) + ent2.fromJSON(jsonData) + assert(ent2.has('position')) + assert(ent2.get('position').x === 14) + assert(ent2.get('position').y === 2) +}) + +test('entity: check for existence of components', () => { + const world = new World() + // Test all component types + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + }) + + let ent = world + .entity() + .set('position', 1, 2) + .set('velocity', { x: 3, y: 4 }) + .set('player') + .set('anonymous') + + // Check for existence + assert( + ent.has('position') && + ent.has('velocity') && + ent.has('player') && + ent.has('anonymous') + ) + assert(ent.has('position', 'velocity', 'player', 'anonymous')) + assert(!ent.has('position', 'invalid')) + assert(!ent.has('velocity', 'invalid')) + assert(!ent.has('player', 'invalid')) + assert(!ent.has('anonymous', 'invalid')) + assert(!ent.has('invalid')) + + // This behavior is important for world.each to work properly when no components are specified + // Basically, it should return all entities when nothing is specified + assert(ent.has()) + let ent2 = world.entity() + assert(ent2.has()) + assert(!ent2.has('invalid')) +}) + +test('entity: cloning basic', () => { + const world = new World() + const source = world.entity().set('a', 'aaa') + const target = world.entity() + source.cloneComponentTo(target, 'a') + expect(target.get('a')).toEqual('aaa') +}) + +test('entity: cloning advanced', () => { + const world = new World() + world.component( + 'foo', + class { + onCreate(bar, baz) { + this.bar = bar + this.baz = baz + this.qux = false + } + setQux(qux = true) { + this.qux = qux + } + cloneArgs() { + return [this.bar, this.baz] + } + clone(target) { + target.qux = this.qux + } + } + ) + const source = world.entity().set('foo', 'bar', 'baz').set('qux', true) + const target = world.entity() + source.cloneComponentTo(target, 'foo') + expect(source.get('foo').bar).toEqual(target.get('foo').bar) + expect(source.get('foo').baz).toEqual(target.get('foo').baz) + expect(source.get('foo').qux).toEqual(target.get('foo').qux) + + const target2 = source.clone() + expect(source.get('foo').bar).toEqual(target2.get('foo').bar) + expect(source.get('foo').baz).toEqual(target2.get('foo').baz) + expect(source.get('foo').qux).toEqual(target2.get('foo').qux) + + target.get('foo').bar = 'change1' + target2.get('foo').baz = 'change2' + expect(source.get('foo').bar).not.toEqual(target.get('foo').bar) + expect(source.get('foo').baz).toEqual(target.get('foo').baz) + expect(source.get('foo').qux).toEqual(target.get('foo').qux) + expect(source.get('foo').bar).toEqual(target2.get('foo').bar) + expect(source.get('foo').baz).not.toEqual(target2.get('foo').baz) + expect(source.get('foo').qux).toEqual(target2.get('foo').qux) + + const target3 = target2.clone() + expect(target3.get('foo').bar).toEqual(target2.get('foo').bar) + expect(target3.get('foo').baz).toEqual(target2.get('foo').baz) + expect(target3.get('foo').qux).toEqual(target2.get('foo').qux) + + target3.destroy() + expect(() => target3.clone()).toThrow() +}) diff --git a/test/test_utils.js b/test/test_utils.js new file mode 100644 index 0000000..fc0bf46 --- /dev/null +++ b/test/test_utils.js @@ -0,0 +1,21 @@ +export function getSize(it) { + let num = 0 + for (let _elem in it) { + ++num + } + return num +} + +export function has(it, target) { + for (let elem of it) { + if (elem.id === target.id) { + return true + } + } + return false +} + +// TODO: Result of mocha/chai to jest upgrade, remove and use jest's "expect" +export function assert(value) { + expect(Boolean(value)).toBe(true) +} diff --git a/test/utilities.test.js b/test/utilities.test.js new file mode 100644 index 0000000..214c9ae --- /dev/null +++ b/test/utilities.test.js @@ -0,0 +1,50 @@ +import { invoke, shallowClone } from '../src/utilities.js' + +test('utilities: invoke', () => { + const obj = { + foo1: () => 'bar', + foo2: 'bar', + foo3: (...args) => args.join(' '), + } + class C { + constructor() { + this.that = this + this.foo5 = () => { + return Boolean(this && this.that && this === this.that) + } + } + foo4() { + return Boolean(this && this.that && this === this.that) + } + } + const obj2 = new C() + expect(invoke(obj, 'foo1')).toBe('bar') + expect(invoke(obj, 'foo2')).toBe(undefined) + expect(invoke(obj, 'foo3', 'foo', 'bar')).toBe('foo bar') + expect(invoke(obj2, 'foo4')).toBe(true) + expect(obj2.foo4()).toBe(true) + expect(obj2.foo4.call(this)).toBe(false) + expect(invoke(obj2, 'foo5')).toBe(true) + expect(obj2.foo5()).toBe(true) + expect(obj2.foo5.call(this)).toBe(true) +}) + +test('utilities: shallowClone', () => { + const obj = { a: 1, b: 2 } + const arr = [1, 2, { c: 3 }, { d: 4 }] + const obj2 = shallowClone(obj) + const arr2 = shallowClone(arr) + expect(obj).toEqual(obj2) + expect(arr).toEqual(arr2) + obj.a = 0 + obj2.a = -1 + arr[0] = 0 + arr[2] = 3 + expect(obj.a).toBe(0) + expect(obj2.a).toBe(-1) + expect(arr).toEqual([0, 2, 3, { d: 4 }]) + expect(arr2).toEqual([1, 2, { c: 3 }, { d: 4 }]) + arr[3].d = 10 + expect(arr).toEqual([0, 2, 3, { d: 10 }]) + expect(arr2).toEqual([1, 2, { c: 3 }, { d: 10 }]) +}) diff --git a/test/world.test.js b/test/world.test.js new file mode 100644 index 0000000..e1e132f --- /dev/null +++ b/test/world.test.js @@ -0,0 +1,941 @@ +import { World } from '../index.js' +import { Entity } from '../src/entity.js' +import { getSize, has, assert } from './test_utils.js' + +test('world: create a world', () => { + const world = new World() + assert(world instanceof World) + assert(typeof world.component === 'function') +}) + +test('world: create a world with options', () => { + // Define components + class position {} + class velocity {} + // Define systems + class input {} + class physics {} + class render {} + // Define state + const state = {} + // Make worlds + const world1 = new World({}) + expect(world1).toBeInstanceOf(World) + expect(world1.systems.systems).toHaveLength(0) + expect(getSize(world1.entities.componentClasses)).toBe(0) + expect(getSize(world1.systems.context)).toBe(0) + const world2 = new World({ + components: {}, + systems: [], + context: {}, + }) + expect(world2).toBeInstanceOf(World) + expect(world2.systems.systems).toHaveLength(0) + expect(getSize(world2.entities.componentClasses)).toBe(0) + expect(getSize(world2.systems.context)).toBe(0) + const world3 = new World({ + components: { position, velocity }, + systems: [input, physics, render], + context: { state }, + }) + expect(world3).toBeInstanceOf(World) + expect(world3.systems.systems).toHaveLength(3) + expect(getSize(world3.entities.componentClasses)).toBe(2) + expect(getSize(world3.systems.context)).toBe(1) +}) + +test('component: define a component', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + + this.inc = a => { + return a + 1 + } + }) + let ent = world.entity().set('position', 1, 2) + assert('position' in world.entities.componentClasses) + assert(Object.keys(world.entities.componentClasses).length == 1) + assert(ent.has('position')) + assert(ent.get('position').x === 1) + assert(ent.get('position').y === 2) + assert(ent.get('position').inc(5) === 6) + + // Using class syntax + world.component( + 'velocity', + class { + constructor(x = 0, y = 0) { + this.x = x + this.y = y + } + + inc(a) { + return a + 1 + } + } + ) + let ent2 = world.entity().set('velocity', 1, 2) + assert('velocity' in world.entities.componentClasses) + assert(Object.keys(world.entities.componentClasses).length == 2) + assert(ent2.has('velocity')) + assert(ent2.get('velocity').x === 1) + assert(ent2.get('velocity').y === 2) + assert(ent2.get('velocity').inc(5) === 6) + + // Should throw when calling "components" setter + expect(() => (ent2.components = ['anything'])).toThrow() +}) + +test('component: README example', () => { + const world = new World() + // Create player + let player = world.entity().set('health', { value: 100 }) + + // Create enemies + world.entity().set('damages', 10) + world.entity().set('damages', 30) + + // Apply damage + world.each('damages', ({ damages: amount }) => { + player.get('health').value -= amount + }) + + // Player now has reduced health + assert(player.get('health').value === 60) +}) + +test('component: define invalid components', () => { + const world = new World() + expect(() => { + world.component('empty') + }).toThrow() + expect(() => { + world.component('position', { + x: 0, + y: 0, + }) + }).toThrow() + expect(() => { + world.component('invalid', 555) + }).toThrow() + assert(Object.keys(world.entities.componentClasses).length === 0) +}) + +test('component: use an empty component', () => { + const world = new World() + let ent = world.entity().set('position', { + x: 1, + }) + assert(ent.has('position')) + assert(ent.get('position').x === 1) + + let ent2 = world.entity().set('velocity', { + x: 2, + }) + assert(ent2.has('velocity')) + assert(ent2.get('velocity').x === 2) + ent2.set('velocity', { + y: 3, + }) + assert(ent2.get('velocity').x === undefined) + assert(ent2.get('velocity').y === 3) + ent2.get('velocity').x = 42 + assert(ent2.get('velocity').x === 42) + assert(ent2.get('velocity').y === 3) + + let ent3 = world.entity().set('singleValue', 5) + assert(ent3.has('singleValue')) + assert(ent3.get('singleValue') === 5) + ent3.set('singleValue', 500) + assert(ent3.get('singleValue') === 500) + + let ent4 = world.entity().set('string', 'hello') + assert(ent4.has('string')) + assert(ent4.get('string') === 'hello') + ent4.set('string', 'goodbye') + assert(ent4.get('string') === 'goodbye') + + ent4.remove('string') + assert(!ent4.has('string')) +}) + +test('component: test clearing with indexes', () => { + const world = new World() + world.component('position', function (x = 0, y = 0) { + this.x = x + this.y = y + }) + let results = world.each() + results = world.each('position') + results = world.each('position', 'velocity') + + world.entity().set('position', 1, 2).set('velocity') + world.entity().set('position', 3, 4).set('velocity') + world.entity().set('position', 5, 6) + world.entity().set('velocity') + + let count = 0 + world.each('position', ({ position }) => { + assert(position.x >= 1) + assert(position.y >= 2) + ++count + }) + assert(count === 3) + + world.clear() + + count = 0 + world.each('position', ({ position }) => { + ++count + }) + world.each((_, ent) => { + ++count + }) + assert(count === 0) +}) + +test('component: test entity creation with constructor parameters', () => { + const world = new World() + let when = 0 + world.component( + 'sprite', + class { + onCreate(texture, size, invalid) { + this.texture = texture + this.size = size + this.constructorCalled = ++when + assert(texture && texture === this.texture) + assert(size && size === this.size) + assert(invalid === undefined) + + // Regression in 0.3.0, fixed in 0.3.1 + assert(this.entity.get('sprite') === this) + } + } + ) + + let ent = world.entity().set('sprite', 'test.png', 100) + assert(ent.get('sprite').constructorCalled === 1) + assert(ent.get('sprite').entity === ent) + assert(ent.get('sprite').texture === 'test.png') + assert(ent.get('sprite').size === 100) +}) + +test('component: test clearing with onRemove', () => { + const world = new World() + let spriteCount = 0 + world.component( + 'sprite', + class { + constructor() { + ++spriteCount + } + + onRemove() { + --spriteCount + } + } + ) + + let ent = world.entity().set('sprite') + assert(spriteCount === 1) + + let ent2 = world.entity().set('sprite') + assert(spriteCount === 2) + + world.clear() + assert(spriteCount === 0) +}) + +test('component: test detach and attach', () => { + const world = new World() + let spriteCount = 0 + world.component( + 'sprite', + class { + constructor() { + ++spriteCount + } + + onRemove() { + --spriteCount + } + } + ) + + let ent = world.entity().set('sprite').set('position', { x: 1 }) + assert(spriteCount === 1) + + let ent2 = world.entity().set('sprite').set('position', { x: 2 }) + assert(spriteCount === 2) + + // Test detaching + assert(ent.valid()) + ent.detach() + assert(!ent.valid()) + assert(spriteCount === 2) + assert(world.entities.entities.size === 1) + assert(getSize(world.each('position')) === 1) + assert(world.each('position').length === 1) + assert(world.each('position')[0].get('position').x === 2) + assert(ent.get('position').x === 1) + + // Test attaching + ent.attach(world) + assert(ent.valid()) + assert(spriteCount === 2) + assert(world.entities.entities.size === 2) + assert(getSize(world.each('position')) === 2) + assert(world.each('position').length === 2) + assert(ent.get('position').x === 1) + + // Test edge cases + ent.detach() + assert(!ent.valid()) + ent.detach() + assert(!ent.valid()) + ent.attach() + assert(!ent.valid()) + ent.attach(world) + assert(ent.valid()) +}) + +test('component: test detached entities', () => { + const world = new World() + let ent = world + .entity() + .set('sprite', { texture: 'image.png' }) + .set('position', 5) + + assert(ent.valid()) + assert(ent.has('sprite')) + assert(ent.has('position')) + assert(ent.get('sprite').texture === 'image.png') + assert(ent.get('position') === 5) + + ent.detach() + + assert(!ent.valid()) + assert(ent.has('sprite')) + assert(ent.has('position')) + assert(ent.get('sprite').texture === 'image.png') + assert(ent.get('position') === 5) + + ent.set('velocity', { x: 10 }) + assert(ent.has('velocity')) + assert(ent.get('velocity').x === 10) + + ent.set('position', 6) + assert(ent.has('position')) + assert(ent.get('position') === 6) + + ent.remove('position') + assert(!ent.has('position')) + + // Create entity outside of the world + let ent2 = new Entity() + assert(!ent2.valid()) + ent2.set('velocity', { x: 30 }) + ent2.set('position', 7) + assert(ent2.has('velocity', 'position')) + assert(ent2.get('velocity').x === 30) + assert(ent2.get('position') === 7) + ent2.removeAll() + assert(!ent2.has('velocity')) + assert(!ent2.has('position')) +}) + +test('system: define a system', () => { + const world = new World() + world.system(class {}) + assert(world.systems.systems.length == 1) +}) + +test('system: define a system with arguments', () => { + const world = new World() + let velocitySystem = class { + constructor(maxVelocity, canvas, textures) { + this.maxVelocity = maxVelocity + this.canvas = canvas + this.textures = textures + } + } + world.system(velocitySystem, 3500, 'someCanvas', ['textures.png']) + assert(world.systems.systems[0].maxVelocity === 3500) + assert(world.systems.systems[0].canvas === 'someCanvas') + assert(world.systems.systems[0].textures[0] === 'textures.png') +}) + +test('system: define a system with context (no key)', () => { + const world = new World() + const state = { + maxVelocity: 3500, + canvas: 'someCanvas', + textures: ['textures.png'], + } + const ran = [] + const velocitySystem = class { + init(existing) { + this.existing = existing + if (!existing) { + expect(this.maxVelocity).toEqual(3500) + expect(this.canvas).toEqual('someCanvas') + expect(this.textures[0]).toEqual('textures.png') + ran.push(existing) + } + } + run() { + expect(this.maxVelocity).toEqual(3500) + expect(this.canvas).toEqual('someCanvas') + expect(this.textures[0]).toEqual('textures.png') + ran.push(this.existing) + } + } + world.system(velocitySystem, true) // Existing system + world.context(state) // Set keyless context + world.system(velocitySystem, false) // New system + expect(world.systems.systems.length).toEqual(2) + world.run() + expect(ran).toEqual([false, true, false]) +}) + +test('system: define a system with context (specific key)', () => { + const world = new World() + const state = { + maxVelocity: 3500, + canvas: 'someCanvas', + textures: ['textures.png'], + } + const ran = [] + const velocitySystem = class { + init(existing) { + this.existing = existing + if (!existing) { + expect(this.state.maxVelocity).toEqual(3500) + expect(this.state.canvas).toEqual('someCanvas') + expect(this.state.textures[0]).toEqual('textures.png') + ran.push(existing) + } + } + run() { + expect(this.state.maxVelocity).toEqual(3500) + expect(this.state.canvas).toEqual('someCanvas') + expect(this.state.textures[0]).toEqual('textures.png') + ran.push(this.existing) + } + } + world.system(velocitySystem, true) // Existing system + world.context({ state }) // Set keyed context + world.system(velocitySystem, false) // New system + expect(world.systems.systems.length).toEqual(2) + world.run() + expect(ran).toEqual([false, true, false]) +}) + +test('system: system iteration', () => { + const world = new World() + world.system( + class { + run(dt, total) { + assert(dt > 0) + assert(total > 0) + world.each('position', 'velocity', ({ position, velocity }, ent) => { + assert(position) + assert(velocity) + position.x += velocity.x + position.y += velocity.y + assert(ent) + assert(ent.has('position')) + assert(ent.has('velocity')) + assert(dt > 0) + assert(total > 0) + }) + } + } + ) + + let dt = 0.1667 + let total = dt + + let entA = world.entity() + let entB = world.entity() + let entC = world.entity() + Object.assign(entA.access('position', {}), { x: 1, y: 1 }) + Object.assign(entA.access('velocity', {}), { x: 1, y: 0 }) + Object.assign(entB.access('position', {}), { x: 30, y: 40 }) + Object.assign(entB.access('velocity', {}), { x: -1, y: 2 }) + + assert(entA.get('position').x == 1 && entA.get('position').y == 1) + assert(entB.get('position').x == 30 && entB.get('position').y == 40) + + world.run(dt, total) + + assert(entA.get('position').x == 2 && entA.get('position').y == 1) + assert(entB.get('position').x == 29 && entB.get('position').y == 42) + + total += dt + world.run(dt, total) + + assert(entA.get('position').x == 3 && entA.get('position').y == 1) + assert(entB.get('position').x == 28 && entB.get('position').y == 44) +}) + +test('system: system methods', () => { + const world = new World() + let methodsCalled = 0 + + world.system( + class { + constructor() { + this.val = 10 + ++methodsCalled + } + init() { + ++methodsCalled + } + run() { + ++methodsCalled + assert(this.val === 10) + world.each('position', ({ position }) => { + position.x = 1 + ++methodsCalled + assert(this.val === 10) + }) + } + } + ) + + world.system(class {}) + expect(() => { + world.system() + }).toThrow() + + world.entity().set('position', {}) + assert(methodsCalled == 2) + world.run() + assert(methodsCalled == 4) +}) + +test('system: system edge cases', () => { + const world = new World() + world.component('position', class {}) + world.component('velocity', class {}) + + let testEnt0 = world.entity().set('position').set('velocity') + let testEnt2 = null + for (let i = 0; i < 100; ++i) { + let tmpEnt = world.entity() + tmpEnt.set('position').set('velocity') + if (i == 80) { + testEnt2 = tmpEnt + } + } + + let testEnt1 = world.entity().set('position').set('velocity') + let count = 0 + + world.system( + class { + run() { + world.each(['position', 'velocity'], ({ position, velocity }, ent) => { + ++count + if (count == 1) { + testEnt1.removeAll() + testEnt2.remove('position') + testEnt0.remove('velocity') + return + } + assert(position) + assert(velocity) + position.x += velocity.x + position.y += velocity.y + assert(ent) + assert(ent.has('position')) + assert(ent.has('velocity')) + + // Make sure the test entities do not show up here + assert(ent.id !== testEnt0.id) + assert(ent.id !== testEnt1.id) + assert(ent.id !== testEnt2.id) + }) + } + } + ) + let entA = world.entity() + let entB = world.entity() + let entC = world.entity() + Object.assign(entA.access('position', {}), { x: 1, y: 1 }) + Object.assign(entA.access('velocity', {}), { x: 1, y: 0 }) + Object.assign(entB.access('position', {}), { x: 30, y: 40 }) + Object.assign(entB.access('velocity', {}), { x: -1, y: 2 }) + + assert(entA.get('position').x == 1 && entA.get('position').y == 1) + assert(entB.get('position').x == 30 && entB.get('position').y == 40) + + world.run() + + assert(entA.get('position').x == 2 && entA.get('position').y == 1) + assert(entB.get('position').x == 29 && entB.get('position').y == 42) + + world.run() + + assert(entA.get('position').x == 3 && entA.get('position').y == 1) + assert(entB.get('position').x == 28 && entB.get('position').y == 44) +}) + +test('system: adding entities to index', () => { + const world = new World() + world.entity().set('a').set('b') + expect(world.each('a', 'b').length).toBe(1) + world.each('a', 'b')[0].destroy() + expect(world.each('a', 'b').length).toBe(0) + world.entity().set('a') + expect(world.each('a', 'b').length).toBe(0) + world.entity().set('b') + expect(world.each('a', 'b').length).toBe(0) + world.entity().set('b').set('a') + expect(world.each('a', 'b').length).toBe(1) +}) + +test('system: onRemove edge cases', () => { + const world = new World() + world.component( + 'position', + class { + onCreate(value) { + this.value = value + } + + onRemove() { + this.entity.set('somethingElse') + } + } + ) + + let entity = world.entity().set('position') + + expect(() => entity.destroy()).toThrow() +}) + +test('system: indexing edge cases', () => { + const world = new World() + // This test was to discover the "adding entities to index" and "onRemove edge cases" tests above + // Keeping it in case there are other problems in the future + + let g = { count: 0 } + + // Define components + world.component( + 'position', + class { + onCreate(x = 0, y = 0) { + this.x = x + this.y = y + } + } + ) + world.component( + 'velocity', + class { + onCreate(x = 0, y = 0) { + this.x = x + this.y = y + } + } + ) + world.component( + 'sprite', + class { + onCreate(texture) { + this.texture = texture + } + + onRemove() { + g.entity.set('sideEffect', ++g.count) + } + } + ) + + const REPEAT = 3 + + for (let i = 0; i < REPEAT; ++i) { + g.count = 0 + + // Create test entities + world.entity().set('noOtherComponents', 0) + world.entity().set('position', 1) + g.entity = world.entity().set('velocity', 2) + world.entity().set('sprite', 'three') + world.entity().set('position', 4).set('velocity', 4) + world.entity().set('position', 5).set('velocity', 5).set('sprite', 'five') + world.entity().set('position', 6).set('sprite', 'six') + world.entity().set('velocity', 7).set('sprite', 'seven') + + // Ensure initial indexes are good + for (let i = 0; i < REPEAT; ++i) { + assert(world.each('noOtherComponents').length === 1) + assert(world.each('sideEffect').length === 0) + assert(world.each('sprite').length === 4) + assert(world.each('velocity').length === 4) + assert(world.each().length === 8) + assert(world.each('position').length === 4) + assert(world.each('position', 'velocity').length === 2) + assert(world.each('position', 'sprite').length === 2) + assert(world.each('position', 'velocity', 'sprite').length === 1) + assert(world.each('velocity', 'sprite').length === 2) + } + + // Remove test entities, create more test entities + let count = 0 + world.each('sprite', ({ sprite }, entity) => { + ++count + entity.destroy() + }) + assert(count === 4) + + count = 0 + world.each('sprite', ({ sprite }, entity) => { + ++count + entity.destroy() + }) + assert(count === 0) + + assert(g.count === 4) + + // Ensure indexes are still good + for (let i = 0; i < REPEAT; ++i) { + assert(world.each().length === 4) + assert(world.each('noOtherComponents').length === 1) + assert(world.each('sideEffect').length === 1) + assert(world.each('sprite').length === 0) + assert(world.each('velocity').length === 2) + assert(world.each('position').length === 2) + } + + count = 0 + world.each('velocity', ({ velocity }, entity) => { + ++count + entity.destroy() + }) + expect(count).toBe(2) + + count = 0 + world.each('velocity', ({ velocity }, entity) => { + ++count + entity.destroy() + }) + assert(count === 0) + + // Ensure indexes are still good + for (let i = 0; i < REPEAT; ++i) { + assert(world.each().length === 2) + assert(world.each('noOtherComponents').length === 1) + assert(world.each('sideEffect').length === 0) + assert(world.each('sprite').length === 0) + assert(world.each('velocity').length === 0) + assert(world.each('position').length === 1) + } + + count = 0 + world.each('position', ({ position }, entity) => { + ++count + entity.destroy() + }) + expect(count).toBe(1) + + count = 0 + world.each('position', ({ position }, entity) => { + ++count + entity.destroy() + }) + assert(count === 0) + + world.each('noOtherComponents')[0].destroy() + + // Ensure new indexes are good + for (let i = 0; i < REPEAT; ++i) { + assert(world.each().length === 0) + assert(world.each('noOtherComponents').length === 0) + assert(world.each('sideEffect').length === 0) + assert(world.each('sprite').length === 0) + assert(world.each('velocity').length === 0) + assert(world.each('position').length === 0) + } + } +}) + +test('system: system variadic arguments with optional components', () => { + const world = new World() + let created = false + world.system( + class { + constructor(first, second) { + assert(first === 1) + assert(second === 2) + created = true + } + }, + 1, + 2 + ) + assert(created) +}) + +test('system: use the each() method', () => { + const world = new World() + let ent1 = world.entity().set('position', {}).set('"velocity"', {}) + let ent2 = world.entity().set('position', {}) + let ent3 = world.entity().set('position:"velocity"', {}) + let externalVar = 5 + world.each('position', ({ position: pos }, ent) => { + assert(pos) + assert(ent) + assert(ent.has('position')) + assert(externalVar === 5) + }) + world.each('position', function ({ position: pos }, ent) { + assert(pos) + assert(ent) + assert(ent.has('position')) + assert(externalVar === 5) + }) + + // Test hash collisions and escaping + world.each('position:"velocity"') + let count = 0 + world.each( + 'position', + '"velocity"', + function ({ position: pos, ['"velocity"']: vel }, ent) { + assert(pos) + assert(vel) + assert(ent) + assert(ent.has('position', '"velocity"')) + ++count + } + ) + expect(count).toBe(1) + + // Test iterator usage + count = 0 + let results = world.each('position', '"velocity"') + for (let ent of results) { + ++count + } + expect(count).toBe(1) + + // Passing callbacks cause the return value to be undefined + results = world.each('position', () => {}) + assert(results === undefined) + results = world.each(() => {}) + assert(results === undefined) + + // Test breaking out of the loop (with components) + count = 0 + world.each('position', function ({ position }, ent) { + assert(position) + assert(ent) + assert(ent.has('position')) + ++count + return false + }) + expect(count).toBe(1) + + // Test breaking out of the loop (without components) + count = 0 + world.each(function (_, ent) { + assert(ent) + assert(ent.valid()) + ++count + return false + }) + expect(count).toBe(1) + + // And just to be sure there are more than 1 + count = world.each('position').length + expect(count).toBe(2) + + // Invalid args + expect(() => { + world.each('position', () => {}, 999) + }).toThrow() +}) + +test('system: test indexing with each()', () => { + const world = new World() + world.component('position', function (val = 0) { + this.val = val + }) + let ent1 = world.entity().set('position', 1).set('velocity') + let ent2 = world.entity().set('position', 10) + let ent3 = world.entity().set('position', 100).set('velocity').set('sprite') + let count = 0 + world.each( + 'position', + 'velocity', + ({ position: pos, velocity: vel }, ent) => { + assert(ent.has('position', 'velocity')) + count += pos.val + } + ) + assert(count == 101) + count = 0 + + ent1.remove('position') + ent1.set('sprite') + ent2.set('velocity') + world.each( + 'position', + 'velocity', + ({ position: pos, velocity: vel }, ent) => { + assert(ent.has('position', 'velocity')) + count += pos.val + } + ) + assert(count == 110) + + ent1.remove('sprite') + ent2.remove('sprite') + ent3.remove('sprite') + + // Query for all entities + let test = world.each() + assert(getSize(test) == 3) + + let ent4 = world.entity() + assert(getSize(world.each()) == 4) + assert(has(world.each(), ent4)) + + ent4.set('velocity') + assert(getSize(world.each()) == 4) + assert(has(world.each(), ent4)) + + ent4.remove('velocity') + assert(getSize(world.each()) == 4) + assert(has(world.each(), ent4)) + + ent4.destroy() + assert(getSize(world.each()) == 3) + assert(!has(world.each(), ent4)) + + count = 0 + world.each(ent => { + ++count + }) + assert(count == 3) + + count = 0 + world.system( + class { + run() { + world.each(() => { + ++count + }) + } + } + ) + world.run() + expect(count).toEqual(3) +}) diff --git a/yarn.lock b/yarn.lock index f162493..cf1df4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,36 +9,51 @@ dependencies: "@babel/highlight" "^7.12.13" +"@babel/compat-data@^7.13.12": + version "7.13.15" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4" + integrity sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA== + "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.12.16" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.16.tgz#8c6ba456b23b680a6493ddcfcd9d3c3ad51cab7c" - integrity sha512-t/hHIB504wWceOeaOoONOhu+gX+hpjfeN6YRBT209X/4sibZQfSF1I0HFRRlBe97UZZosGx5XwUg1ZgNbelmNw== + version "7.13.15" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.15.tgz#a6d40917df027487b54312202a06812c4f7792d0" + integrity sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.12.15" - "@babel/helper-module-transforms" "^7.12.13" - "@babel/helpers" "^7.12.13" - "@babel/parser" "^7.12.16" + "@babel/generator" "^7.13.9" + "@babel/helper-compilation-targets" "^7.13.13" + "@babel/helper-module-transforms" "^7.13.14" + "@babel/helpers" "^7.13.10" + "@babel/parser" "^7.13.15" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/traverse" "^7.13.15" + "@babel/types" "^7.13.14" convert-source-map "^1.7.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" + gensync "^1.0.0-beta.2" json5 "^2.1.2" - lodash "^4.17.19" - semver "^5.4.1" + semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.12.13", "@babel/generator@^7.12.15": - version "7.12.15" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.15.tgz#4617b5d0b25cc572474cc1aafee1edeaf9b5368f" - integrity sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ== +"@babel/generator@^7.13.9": + version "7.13.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" + integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== dependencies: - "@babel/types" "^7.12.13" + "@babel/types" "^7.13.0" jsesc "^2.5.1" source-map "^0.5.0" +"@babel/helper-compilation-targets@^7.13.13": + version "7.13.13" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5" + integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ== + dependencies: + "@babel/compat-data" "^7.13.12" + "@babel/helper-validator-option" "^7.12.17" + browserslist "^4.14.5" + semver "^6.3.0" + "@babel/helper-function-name@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" @@ -55,34 +70,33 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-member-expression-to-functions@^7.12.13": - version "7.12.16" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.16.tgz#41e0916b99f8d5f43da4f05d85f4930fa3d62b22" - integrity sha512-zYoZC1uvebBFmj1wFAlXwt35JLEgecefATtKp20xalwEK8vHAixLBXTGxNrVGEmTT+gzOThUgr8UEdgtalc1BQ== +"@babel/helper-member-expression-to-functions@^7.13.12": + version "7.13.12" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72" + integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw== dependencies: - "@babel/types" "^7.12.13" + "@babel/types" "^7.13.12" -"@babel/helper-module-imports@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" - integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g== +"@babel/helper-module-imports@^7.13.12": + version "7.13.12" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" + integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== dependencies: - "@babel/types" "^7.12.13" + "@babel/types" "^7.13.12" -"@babel/helper-module-transforms@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz#01afb052dcad2044289b7b20beb3fa8bd0265bea" - integrity sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA== +"@babel/helper-module-transforms@^7.13.14": + version "7.13.14" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz#e600652ba48ccb1641775413cb32cfa4e8b495ef" + integrity sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g== dependencies: - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-replace-supers" "^7.12.13" - "@babel/helper-simple-access" "^7.12.13" + "@babel/helper-module-imports" "^7.13.12" + "@babel/helper-replace-supers" "^7.13.12" + "@babel/helper-simple-access" "^7.13.12" "@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-validator-identifier" "^7.12.11" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@babel/types" "^7.12.13" - lodash "^4.17.19" + "@babel/traverse" "^7.13.13" + "@babel/types" "^7.13.14" "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" @@ -92,26 +106,26 @@ "@babel/types" "^7.12.13" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.8.0": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz#174254d0f2424d8aefb4dd48057511247b0a9eeb" - integrity sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA== + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" + integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== -"@babel/helper-replace-supers@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz#00ec4fb6862546bd3d0aff9aac56074277173121" - integrity sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg== +"@babel/helper-replace-supers@^7.13.12": + version "7.13.12" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804" + integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.12.13" + "@babel/helper-member-expression-to-functions" "^7.13.12" "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.12" -"@babel/helper-simple-access@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4" - integrity sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA== +"@babel/helper-simple-access@^7.13.12": + version "7.13.12" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" + integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA== dependencies: - "@babel/types" "^7.12.13" + "@babel/types" "^7.13.12" "@babel/helper-split-export-declaration@^7.12.13": version "7.12.13" @@ -125,28 +139,33 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== -"@babel/helpers@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.13.tgz#3c75e993632e4dadc0274eae219c73eb7645ba47" - integrity sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ== +"@babel/helper-validator-option@^7.12.17": + version "7.12.17" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" + integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== + +"@babel/helpers@^7.13.10": + version "7.13.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8" + integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ== dependencies: "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" "@babel/highlight@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" - integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== + version "7.13.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" + integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== dependencies: "@babel/helper-validator-identifier" "^7.12.11" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.12.16": - version "7.12.16" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.16.tgz#cc31257419d2c3189d394081635703f549fc1ed4" - integrity sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.15": + version "7.13.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8" + integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -241,25 +260,24 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.13.tgz#689f0e4b4c08587ad26622832632735fb8c4e0c0" - integrity sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.13.15": + version "7.13.15" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.15.tgz#c38bf7679334ddd4028e8e1f7b3aa5019f0dada7" + integrity sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.12.13" + "@babel/generator" "^7.13.9" "@babel/helper-function-name" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/parser" "^7.13.15" + "@babel/types" "^7.13.14" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.13.tgz#8be1aa8f2c876da11a9cf650c0ecf656913ad611" - integrity sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ== +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.14", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.13.14" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d" + integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ== dependencies: "@babel/helper-validator-identifier" "^7.12.11" lodash "^4.17.19" @@ -466,9 +484,9 @@ chalk "^4.0.0" "@sinonjs/commons@^1.7.0": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b" - integrity sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw== + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== dependencies: type-detect "4.0.8" @@ -480,9 +498,9 @@ "@sinonjs/commons" "^1.7.0" "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": - version "7.1.12" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" - integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ== + version "7.1.14" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" + integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -506,9 +524,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0" - integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg== + version "7.11.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.1.tgz#654f6c4f67568e24c23b367e947098c6206fa639" + integrity sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw== dependencies: "@babel/types" "^7.3.0" @@ -538,6 +556,11 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/minimatch@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" + integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== + "@types/node@*": version "13.11.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" @@ -549,9 +572,9 @@ integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== "@types/prettier@^2.0.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.1.tgz#374e31645d58cb18a07b3ecd8e9dede4deb2cccd" - integrity sha512-DxZZbyMAM9GWEzXL+BMZROWz9oo6A9EilwwOMET2UVu2uZTqMWS5S69KVtuVKaRjCUpcrOXRalet86/OpG4kqw== + version "2.2.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0" + integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA== "@types/stack-utils@^2.0.0": version "2.0.0" @@ -575,7 +598,7 @@ abab@^1.0.0: resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" integrity sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4= -abab@^2.0.3: +abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== @@ -610,6 +633,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" + integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== + ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -621,11 +649,11 @@ ajv@^6.12.3: uri-js "^4.2.2" ansi-escapes@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: - type-fest "^0.11.0" + type-fest "^0.21.3" ansi-regex@^2.0.0: version "2.1.1" @@ -665,9 +693,9 @@ anymatch@^2.0.0: normalize-path "^2.1.1" anymatch@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -694,6 +722,11 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -701,6 +734,11 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -711,6 +749,11 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -962,6 +1005,17 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browserslist@^4.14.5: + version "4.16.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" + integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + dependencies: + caniuse-lite "^1.0.30001181" + colorette "^1.2.1" + electron-to-chromium "^1.3.649" + escalade "^3.1.1" + node-releases "^1.1.70" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -1004,6 +1058,11 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== +caniuse-lite@^1.0.30001181: + version "1.0.30001208" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" + integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -1036,6 +1095,14 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" @@ -1177,6 +1244,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1278,7 +1350,7 @@ cssom@^0.4.4: dependencies: cssom "0.3.x" -cssstyle@^2.2.0: +cssstyle@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== @@ -1320,7 +1392,7 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decimal.js@^10.2.0: +decimal.js@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== @@ -1464,6 +1536,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +electron-to-chromium@^1.3.649: + version "1.3.711" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.711.tgz#92c3caf7ffed5e18bf63f66b4b57b4db2409c450" + integrity sha512-XbklBVCDiUeho0PZQCjC25Ha6uBwqqJeyDhPLwLwfWRAo4x+FZFsmu1pPPkXT+B4MQMQoQULfyaMltDopfeiHQ== + email-addresses@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.1.0.tgz#cabf7e085cbdb63008a70319a74e6136188812fb" @@ -1508,6 +1585,11 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-html@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -1523,10 +1605,10 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.14.1: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== +escodegen@^1.6.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" + integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== dependencies: esprima "^4.0.1" estraverse "^4.2.0" @@ -1535,13 +1617,13 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" -escodegen@^1.6.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" - integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== dependencies: esprima "^4.0.1" - estraverse "^4.2.0" + estraverse "^5.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: @@ -1668,15 +1750,20 @@ estraverse@^4.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== exec-sh@^0.3.2: - version "0.3.4" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" - integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== + version "0.3.6" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" + integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== execa@^1.0.0: version "1.0.0" @@ -1926,7 +2013,7 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -gensync@^1.0.0-beta.1: +gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -2102,9 +2189,9 @@ has@^1.0.3: function-bind "^1.1.1" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== html-encoding-sniffer@^2.0.1: version "2.0.1" @@ -2163,6 +2250,11 @@ humanize-url@^1.0.0: normalize-url "^1.0.0" strip-url-auth "^1.0.0" +husky@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" + integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== + ice-cap@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/ice-cap/-/ice-cap-0.0.4.tgz#8a6d31ab4cac8d4b56de4fa946df3352561b6e18" @@ -2178,6 +2270,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + import-local@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" @@ -2211,11 +2308,6 @@ invariant@^2.2.2: dependencies: loose-envify "^1.0.0" -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -2287,9 +2379,9 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: kind-of "^6.0.2" is-docker@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" - integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -2343,9 +2435,9 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: isobject "^3.0.1" is-potential-custom-element-name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" - integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-stream@^1.1.0: version "1.1.0" @@ -2844,35 +2936,35 @@ jsbn@~0.1.0: integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= jsdom@^16.4.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" - integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== + version "16.5.2" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.2.tgz#583fac89a0aea31dbf6237e7e4bedccd9beab472" + integrity sha512-JxNtPt9C1ut85boCbJmffaQ06NBnzkQY/MWO3YxPW8IWS38A26z+B1oBvA9LwKrytewdfymnhi4UNH3/RAgZrg== dependencies: - abab "^2.0.3" - acorn "^7.1.1" + abab "^2.0.5" + acorn "^8.1.0" acorn-globals "^6.0.0" cssom "^0.4.4" - cssstyle "^2.2.0" + cssstyle "^2.3.0" data-urls "^2.0.0" - decimal.js "^10.2.0" + decimal.js "^10.2.1" domexception "^2.0.1" - escodegen "^1.14.1" + escodegen "^2.0.0" html-encoding-sniffer "^2.0.1" is-potential-custom-element-name "^1.0.0" nwsapi "^2.2.0" - parse5 "5.1.1" + parse5 "6.0.1" request "^2.88.2" - request-promise-native "^1.0.8" - saxes "^5.0.0" + request-promise-native "^1.0.9" + saxes "^5.0.1" symbol-tree "^3.2.4" - tough-cookie "^3.0.1" + tough-cookie "^4.0.0" w3c-hr-time "^1.0.2" w3c-xmlserializer "^2.0.0" webidl-conversions "^6.1.0" whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - ws "^7.2.3" + whatwg-url "^8.5.0" + ws "^7.4.4" xml-name-validator "^3.0.0" jsdom@^7.0.2: @@ -3078,20 +3170,15 @@ lodash.some@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - lodash@^4.1.0, lodash@^4.15.0, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.2.0: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== -lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.19, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== loose-envify@^1.0.0: version "1.4.0" @@ -3163,12 +3250,12 @@ micromatch@^3.1.4: to-regex "^3.0.2" micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + version "4.0.3" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.3.tgz#fdad8352bf0cbeb89b391b5d244bc22ff3dd4ec8" + integrity sha512-ueuSaP4i67F/FAUac9zzZ0Dz/5KeKDkITYIS/k4fps+9qeh1SkeH6gbljcqz97mNBOsaWZ+iv2UobMKK/yD+aw== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.1" mime-db@1.45.0: version "1.45.0" @@ -3212,6 +3299,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mri@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6" + integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3222,6 +3314,17 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multimatch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -3260,9 +3363,9 @@ node-modules-regexp@^1.0.0: integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-notifier@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" - integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== + version "8.0.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" + integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== dependencies: growly "^1.3.0" is-wsl "^2.2.0" @@ -3271,6 +3374,11 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" +node-releases@^1.1.70: + version "1.1.71" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" + integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -3432,10 +3540,10 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parse5@^1.5.1: version "1.5.1" @@ -3484,7 +3592,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.5: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -3535,6 +3643,11 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= +prettier@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" @@ -3545,15 +3658,27 @@ pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-quick@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-3.1.0.tgz#cb172e9086deb57455dea7c7e8f136cd0a4aef6c" + integrity sha512-DtxIxksaUWCgPFN7E1ZZk4+Aav3CCuRdhrDSFZENb404sYMtuo9Zka823F+Mgeyt8Zt3bUiCjFzzWYE9LYqkmQ== + dependencies: + chalk "^3.0.0" + execa "^4.0.0" + find-up "^4.1.0" + ignore "^5.1.4" + mri "^1.1.5" + multimatch "^4.0.0" + prompts@^2.0.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" - integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== + version "2.4.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" + integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ== dependencies: kleur "^3.0.3" sisteransi "^1.0.5" -psl@^1.1.28: +psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== @@ -3585,9 +3710,9 @@ query-string@^4.1.0: strict-uri-encode "^1.0.0" react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== read-pkg-up@^7.0.1: version "7.0.1" @@ -3646,9 +3771,9 @@ remove-trailing-separator@^1.0.1: integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== repeat-string@^1.6.1: version "1.6.1" @@ -3676,7 +3801,7 @@ request-promise-core@1.1.4: dependencies: lodash "^4.17.19" -request-promise-native@^1.0.8: +request-promise-native@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== @@ -3810,14 +3935,14 @@ sax@^1.1.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -saxes@^5.0.0: +saxes@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== dependencies: xmlchars "^2.2.0" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -3828,9 +3953,9 @@ semver@^6.0.0, semver@^6.3.0: integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== semver@^7.3.2: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== dependencies: lru-cache "^6.0.0" @@ -4048,17 +4173,17 @@ strict-uri-encode@^1.0.0: integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= string-length@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" - integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" strip-ansi "^6.0.0" string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" @@ -4137,9 +4262,9 @@ supports-color@^7.0.0, supports-color@^7.1.0: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" - integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -4236,14 +4361,14 @@ tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tough-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" + psl "^1.1.33" punycode "^2.1.1" + universalify "^0.1.2" tr46@^2.0.2: version "2.0.2" @@ -4293,10 +4418,10 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^0.6.0: version "0.6.0" @@ -4325,7 +4450,7 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" -universalify@^0.1.0: +universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== @@ -4371,9 +4496,9 @@ uuid@^8.3.0: integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-to-istanbul@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz#5b95cef45c0f83217ec79f8fc7ee1c8b486aee07" - integrity sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g== + version "7.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.1.tgz#04bfd1026ba4577de5472df4f5e89af49de5edda" + integrity sha512-p0BB09E5FRjx0ELN6RgusIPsSPhtgexSRcKETybEs6IGOTXJSZqfwxp7r//55nnu0f1AxltY5VvdVqy2vZf9AA== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -4451,12 +4576,12 @@ whatwg-url-compat@~0.6.5: dependencies: tr46 "~0.0.1" -whatwg-url@^8.0.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" - integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.5.0.tgz#7752b8464fc0903fec89aa9846fc9efe07351fd3" + integrity sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg== dependencies: - lodash.sortby "^4.7.0" + lodash "^4.7.0" tr46 "^2.0.2" webidl-conversions "^6.1.0" @@ -4508,10 +4633,10 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.2.3: - version "7.4.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" - integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== +ws@^7.4.4: + version "7.4.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" + integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== "xml-name-validator@>= 2.0.1 < 3.0.0": version "2.0.1" @@ -4529,9 +4654,9 @@ xmlchars@^2.2.0: integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== yallist@^4.0.0: version "4.0.0"