import Symbol from 'es6-symbol';
import EventEmitter from 'eventemitter3';
import values from 'lodash/values';
import assign from 'lodash/assign';
import forEach from 'lodash/forEach';
import isNil from 'lodash/isNil';
import isString from 'lodash/isString';
import {
assertComponent,
assertString,
assertDisposable,
} from './asserts';
import {
ComponentNotFoundError,
ComponentAlreadyOwnedError,
DuplicateComponentTypeError,
} from './errors';
import createClass from './util/create-class';
import decorateObject from './util/decorate-object';
import subscribableMixin from './mixins/subscribable';
import disposableMixin from './mixins/disposable';
import disposableDecorator from './mixins/decorator/disposable-decorator';
const ID = {
id: 0,
getNext: () => ID.id++,
};
const FIELDS = {
id: Symbol('id'),
components: Symbol('components'),
subscriptions: Symbol('subscriptions'),
emitter: Symbol('emitter'),
};
function onDispose() {
this[FIELDS.emitter].emit('entity:dispose', this);
this[FIELDS.emitter].removeAllListeners();
forEach(this[FIELDS.components], (component) => component.dispose());
}
function onComponentChange(component, current, previous) {
const type = component.getType();
this[FIELDS.emitter].emit('entity:change', this, component);
this[FIELDS.emitter].emit(`entity:change:${type}`, this, component, current, previous);
}
function onComponentDispose(component) {
this.removeComponents(component);
}
function onComponentError(error) {
this[FIELDS.emitter].emit('error', error);
}
function unsubscribe(component) {
forEach(this[FIELDS.subscriptions][component.getType()], (componentUnsubscribe) => componentUnsubscribe());
delete this[FIELDS.subscriptions][component.getType()];
}
/**
* @class
* An entity is simply by an id used to reference zero to many components.
* The types of components that an entity references must be unique.
* @examples
* #Class members are accessed through accessors.
* const entity = new Entity();
* const entityId = entity.getId();
* @access public
* @mixes Subscribable
* @mixes Disposable
*/
const Entity = createClass({
/**
* An entity:add event occurs when an entity adds a component.
* @event entity:add
* @param {Entity} entity - The entity that added the component.
* @param {Component} component - The component that was added.
*
* @instance
* @memberof Entity
*/
/**
* An entity:add:{type} event occurs when an entity adds a component.
* @event entity:add:{type}
* @param {Entity} entity - The entity that added the component.
* @param {Component} component - The component that was added.
*
* @instance
* @memberof Entity
*/
/**
* An entity:change event occurs when a component, that an entity owns, changes.
* @event entity:change
* @param {Entity} entity - The entity that owns the component.
* @param {Component} component - The component that caused the change.
* @param {*} newState - The new value of the component.
* @param {*} oldState - The old value of the component.
*
* @instance
* @memberof Entity
*/
/**
* A entity:change:{type} event occurs when a component, that an entity owns, changes.
* @event entity:change:{type}
* @param {Entity} entity - The entity that owns the component.
* @param {Component} component - The component that was changed.
* @param {*} newState - The new value of the component.
* @param {*} oldState - The old value of the component.
* @instance
* @memberof Entity
*/
/**
* An entity:remove event occurs when an entity disposes a component.
* @event entity:remove
* @param {Entity} entity - The entity that disposing the component.
* @param {Component} component - The component that is being disposed.
*
* @instance
* @memberof Entity
*/
/**
* An entity:remove:{type} event occurs when an entity disposes a component.
* @event entity:remove:{type}
* @param {Entity} entity - The entity that is disposing the component.
* @param {Component} component - The component that is being disposed.
*
* @instance
* @memberof Entity
*/
/**
* An entity:dispose event occurs when an entity is being disposed.
* @event entity:dispose
* @param {Entity} entity - The entity that is being disposed.
* @instance
* @memberof Entity
*/
mixins: [
decorateObject(subscribableMixin(FIELDS.emitter), disposableDecorator),
disposableMixin(values(FIELDS), onDispose),
],
constructor() {
/**
* A literal that identifies the entity.
* @member {number} id
* @instance
* @memberof Entity
* @private
*/
this[FIELDS.id] = ID.getNext();
/**
* The data contained in the component, it is immutable.
* @member {*} state
* @instance
* @memberof Entity
* @private
*/
this[FIELDS.components] = {};
/**
* The entity subscriptions.
* @member {Map<string, function[]>} subscriptions
* @instance
* @memberof Entity
* @ignore
* @private
*/
this[FIELDS.subscriptions] = {};
/**
* The event emitter.
* @member {EventEmitter} emitter
* @instance
* @memberof Entity
* @private
* @ignore
*/
this[FIELDS.emitter] = new EventEmitter();
},
/**
* Get the id.
* @returns {integer} The entity id.
* @throws {ReferenceError} Will throw this error if invoked after being disposed.
* @instance
* @memberof Entity
*/
getId() {
assertDisposable(this);
return this[FIELDS.id];
},
/**
* Gets all components contained in the entity for specified component types. Returns only a single component if
* only one type is specified, but returns a map of all components is returned if no types are specified.
* @param {...string} types - the desired types of components
* @returns {(Component|Map<string, Component>)} A single component or the map of components to component type
* @examples
* # Returns all components when no arguments are passed
* const entity = new Entity().addComponents(new Component('player'), new Component('health', { hp: 100 });
* const components = entity.getComponents();
* const playerComponent = components.player;
* const healthComponent = components.health;
* @examples
* # Returns a single component when only one component type is passed
* const entity = new Entity().addComponents(new Component('player'), new Component('health', { hp: 100 });
* const healthComponent = entity.getComponents('health');
* @examples
* # Returns a map of components when more than one component type is passed
* const entity = new Entity().addComponents(new Component('player'), new Component('health', { hp: 100 });
* const components = entity.getComponents('health', 'player');
* const playerComponent = components.player;
* const healthComponent = components.health;
* @throws {TypeError} when an argument other than a string is passed.
* @throws {ReferenceError} when invoked after the entity has been disposed.
* @emits module:transvolve#error
* @memberof Entity
* @instance
*/
getComponents(...types) {
assertDisposable(this);
const components = this[FIELDS.components];
const emitter = this[FIELDS.emitter];
function getComponent(type) {
assertString(type);
if (components[type]) {
return components[type];
}
emitter.emit('error', new ComponentNotFoundError());
return undefined;
}
if (types.length === 0) {
return assign({}, components);
} else if (types.length === 1) {
return getComponent(types[0]);
}
const map = {};
forEach(types, (type) => {
const component = getComponent(type);
if (!isNil(component)) {
map[type] = component;
}
});
return map;
},
/**
* Adds all the specified components to the entity.
* @param {...Component} components - the components to add
* @returns {Entity} Itself
* @throws {TypeError} Will throw this error when an argument other than a Component is passed.
* @throws {ReferenceError} Will throw this error When invoked after the entity has been disposed.
* @emits Entity#entity:add
* @emits Entity#entity:add:{type}
* @emits module:transvolve#error
* @memberof Entity
* @instance
*/
addComponents(...components) {
assertDisposable(this);
forEach(components, (component) => {
assertComponent(component);
const type = component.getType();
if (this[FIELDS.components][type]) {
const error = (component === this[FIELDS.components][type]) ?
new ComponentAlreadyOwnedError() :
new DuplicateComponentTypeError();
this[FIELDS.emitter].emit('error', error, this);
} else {
this[FIELDS.components][type] = component;
this[FIELDS.subscriptions][type] = [];
this[FIELDS.subscriptions][type].push(component.subscribe('component:change', onComponentChange.bind(this)));
this[FIELDS.subscriptions][type].push(component.subscribe('component:dispose', onComponentDispose.bind(this)));
this[FIELDS.subscriptions][type].push(component.subscribe('error', onComponentError.bind(this)));
this[FIELDS.emitter].emit('entity:add', this, component);
this[FIELDS.emitter].emit(`entity:add:${type}`, this, component);
}
});
return this;
},
/**
* Removes all the specified components or types of components from the entity.
* Each component that is removed is disposed.
* @param {...(Component|string)} componentsOrTypes - the components and/or component types to remove
* @returns {Entity} Itself
* @throws {TypeError} Will throw this error when an argument other than a Component is passed.
* @throws {ReferenceError} Will throw this error when invoked after the entity has been disposed.
* @emits Entity#entity:remove
* @emits Entity#entity:remove:{type}
* @emits module:transvolve#error
* @memberof Entity
* @instance
*/
removeComponents(...componentsOrTypes) {
assertDisposable(this);
forEach(componentsOrTypes, (componentOrType) => {
let component = componentOrType;
if (isString(componentOrType)) {
component = this[FIELDS.components][component];
if (isNil(component)) {
this[FIELDS.emitter].emit('error', new ComponentNotFoundError());
return;
}
} else {
assertComponent(component);
if (component !== this[FIELDS.components][component.getType()]) {
this[FIELDS.emitter].emit('error', new ComponentNotFoundError());
return;
}
}
delete this[FIELDS.components][component.getType()];
unsubscribe.call(this, component);
this[FIELDS.emitter].emit('entity:remove', this, component);
this[FIELDS.emitter].emit(`entity:remove:${component.getType()}`, this, component);
component.dispose();
});
return this;
},
/**
* Determines if the entity has components of all the specified types.
* @param {...string} types - The component types
* @returns {boolean} The result
* @throws {TypeError} Will throw this error when an argument other than a string is passed.
* @throws {ReferenceError} Will throw this error when invoked after the entity has been disposed.
* @memberof Entity
* @instance
*/
hasComponents(...types) {
assertDisposable(this);
const self = this;
let hasComponents = true;
forEach(types, (type) => {
assertString(type);
hasComponents = !isNil(self[FIELDS.components][type]);
return hasComponents;
});
return hasComponents;
},
});
export default Entity;