Source: packages/ecs-js/entity.js

const BaseComponent = require('./component');
const UUID = require('uuid/v1');

/**ECS Entity
 * @class Entity
 */
class Entity {

  constructor(ecs, definition = {}) {

    Object.defineProperty(this, 'ecs', { enumerable: false, value: ecs });
    this.id = definition.id || UUID();
    Object.defineProperty(this, 'components', { enumerable: false, value: {} });
    Object.defineProperty(this, 'componentMap', { enumerable: false, value: {} });

    this.updatedComponents = this.ecs.ticks;
    this.updatedValues = this.ecs.ticks;

    for (const type of Object.keys(definition)) {
      if (type === 'id') continue;
      const cdefs = definition[type];
      if (!ecs.types.hasOwnProperty(type)) throw new Error(`No component type named "${type}". Hasn't been registered?`)
      const mapBy = ecs.types[type].definition.mapBy;
      if (Array.isArray(cdefs)) {
        for (const def of cdefs) {
          this.addComponent(type, def, true);
        }
      } else if (mapBy) {
        for (const key of Object.keys(cdefs)) {
          const def = cdefs[key];
          def[mapBy] = key;
          this.addComponent(type, def, true);
        }
      } else {
        this.addComponent(type, cdefs, true);
      }
    }
    this.ecs.entities.set(this.id, this);
    this.ecs._updateCache(this);
  }

  addComponent(type, definition, delayCache) {

    const ecs = this.ecs;
    const component = new ecs.types[type](ecs, this, definition);

    let addedType = false;
    if (ecs.types[type].definition.multiset) {
      const mapBy = ecs.types[type].definition.mapBy;
      if (mapBy) {
        if (!this.components.hasOwnProperty(component.type)) {
          this.components[component.type] = {};
          addedType = true;
        }
        this.components[component.type][component[mapBy]] = component;
      } else {
        if(!this.components.hasOwnProperty(component.type)) {
          this.components[component.type] = new Set([component]);
          addedType = true;
        } else {
          this.components[component.type].add(component);
        }
      }
    } else {
      if(this.components.hasOwnProperty(component.type)) {
        throw new Error(`Entity<${this.id}> already has component ${component.type}`)
      }
      this.components[component.type] = component;
      addedType = true;
    }
    if (addedType) {
      Object.defineProperty(this, component.type, {
        configurable: true,
        enumerable: true,
        get: () => {
          return Reflect.get(this.components, component.type);
        }
      });
    }

    ecs.entityComponents.get(component.type).add(this.id);
    ecs.components.get(component.type).add(component);


    this.updatedComponents = this.ecs.ticks;
    if (!delayCache) {
      this.ecs._updateCache(this);
    }

    this.componentMap[component.id] = component;
    return component;
  }

  removeComponentByType(cname) {

    if (!this.components.hasOwnProperty(cname)) {
      return;
    }

    if (this.ecs.types[cname].definition.multiset) {
      for (const component of this.components[cname]) {
        this.removeComponent(component, true);
      }
      this.ecs._updateCache(this);
    } else {
      this.removeComponent(this.components[cname]);
    }
  }

  removeComponent(component, delayCache) {

    if (!(component instanceof BaseComponent)) {
      component = this.componentMap[component];
    }
    const ecs = this.ecs;
    const name = component.type;
    let removedType = false;
    if (ecs.types[name].definition.multiset) {
      const mapBy = ecs.types[name].definition.mapBy;
      if (mapBy) {
        const mapValue = component[mapBy]
        if (this.components.hasOwnProperty(component.type)
          && this.components[component.type].hasOwnProperty(mapValue)
          && this.components[component.type][mapValue].id === component.id
        ) {
          delete this.components[component.type][mapValue];
          if (Object.entries(this.components[component.type]).length === 0) {
            removedType = true;
          }
        } else {
          return;
        }
      } else {
        if (this.components.hasOwnProperty(component.type)) {
          const cset = this.components[component.type];
          cset.delete(component);
          if (cset.size === 0) {
            removedType = true;
          }
        } else {
          return;
        }
      }
    } else {
      removedType = true;
    }
    if (removedType) {
      ecs.entityComponents.get(component.type).delete(this.id);
      delete this.components[component.type];
      delete this[component.type];
    }

    ecs.components.get(component.type).delete(component);
    if (!delayCache) {
      this.ecs._updateCache(this);
    }

    delete this.componentMap[component.id];
    this.updatedComponents = this.ecs.ticks;
  }

  getObject() {

    const result = {};
    for (const type of Object.keys(this.components)) {
      const definition = this.ecs.types[type].definition;
      if (definition.serialize && definition.serialize.skip) continue;
      let next;
      if (this.components[type] instanceof Set) {
        next = [];
        for (const component of this.components[type]) {
          next.push(component.getObject());
        }
      } else if (definition.mapBy) {
        next = {};
        for (const key of Object.keys(this.components[type])) {
          next[key] = this.components[type][key].getObject();
        }
      } else {
        next = this.components[type].getObject();
      }
      result[type] = next;
    }
    return Object.assign({ id: this.id}, result);
  }

  destroy() {

    this.ecs._clearEntityFromCache(this);
    if (this.ecs.refs[this.id]) {
      for (const ref of this.ecs.refs[this.id]) {
        const [entityId, componentId, prop, sub] = ref.split('...');
        const entity = this.ecs.getEntity(entityId);
        // remove coverage because I can't think of how this would go wrng
        /* $lab:coverage:off$ */
        if (!entity) continue;
        const component = entity.componentMap[componentId];
        if (!component) continue;
        /* $lab:coverage:on$ */
        if (!sub) {
          component[prop] = null;
        } else if (sub === '__set__') {
          component[prop].delete(this);
        } else {
          component[prop][sub] = null;
        }
      }
    }
    this.ecs.entities.delete(this.id);
  }

}

module.exports = Entity;