ianpaschal/aurora

View on GitHub
src/core/Entity.ts

Summary

Maintainability
A
2 hrs
Test Coverage
// Aurora is distributed under the MIT license.

import uuid from "uuid";
import getItem from "../utils/getItem";
import hasItem from "../utils/hasItem";
import Component from "./Component";
import System from "./System"; // Typing
import { EntityConfig } from "../utils/interfaces"; // Typing

/**
 * @module core
 * @classdesc Class representing an entity.
 */
export default class Entity {

    private _components: any[];
    private _destroy:    boolean; // TODO: Move to flags object
    private _dirty:      boolean; // TODO: Move to flags object
    private _name:       string;
    private _type:       string;
    private _uuid:       string;

    /**
     * @description Create an entity. An object can be used when loading a previously created entity
     * from disk, or creating an entity to be used as an assembly to clone into new entity instances.
     * @param {Object} [config] - Configuration object
     * @param {string} [config.uuid] - Entity UUID
     * @param {string} [config.type] - Entity type
     * @param {string} [config.name] - Entity name (typically also called "unit type" in-game)
     * @param {Array} [config.components] - Array of component data objects to generate component
     * instances from
     */
    constructor( config?: EntityConfig ) {

        // Define defaults
        this._uuid = uuid();
        this._type = "no-type";
        this._name = "No Name";
        this._components = [];
        this._dirty = true;
        this._destroy = false;

        // Apply config values
        if ( config ) {
            Object.keys( config ).forEach( ( key ) => {

                // Handle components slightly differently, otherwise simply overwite props with config value
                if ( key === "components" ) {
                    config.components.forEach( ( data ) => {
                        this.addComponent( new Component( data ) );
                    });
                } else {
                    this[ "_" + key ] = config[ key ];
                }
            });
        }
    }

    /**
     * @description Get all of the entity's component instances.
     * @readonly
     * @returns {Component[]} - Array of component instances
     */
    get components(): Component[] {
        return this._components;
    }

    /**
     * @description Get all of the entity's component types.
     * @readonly
     * @returns {string[]} - Array of component types
     */
    get componentTypes(): string[] {
        const componentTypes = [];
        this._components.forEach( ( component ) => {
            componentTypes.push( component.type );
        });
        return componentTypes;
    }

    get dirty(): boolean {
        return this._dirty;
    }

    set dirty( boolean ) {
        this._dirty = boolean;
    }

    get destroy(): boolean {
        return this._destroy;
    }

    set destroy( boolean ) {
        this._destroy = boolean;
    }

    /**
     * @description Get the entity's data as a pure object (as compared to a class instance).
     * @readonly
     * @returns {Object} - Entity data as an object
     */
    get flattened(): any {
        const data = {
            components: [],
            destroy: this._destroy,
            dirty: this._dirty,
            name: this._name,
            type: this._type,
            uuid: this._uuid,
        };
        this._components.forEach( ( component ) => {
            data.components.push({
                data: component.data,
                type: component.type,
                uuid: component.uuid
            });
        });
        return data;
    }

    /**
     * @description Get the entity's data as a JSON string.
     * @readonly
     * @returns {string} - JSON string
     */
    get json(): string {
        return JSON.stringify( this.flattened, null, 4 );
    }

    /**
     * @description Get the entity's name.
     * @readonly
     * @returns {string} - Name string
     */
    get name(): string {
        return this._name;
    }

    /**
     * @description Get the entity's type.
     * @readonly
     * @returns {string} - Type string
     */
    get type(): string {
        return this._type;
    }

    /**
     * @description Get the entity's UUID.
     * @readonly
     * @returns {string} - UUID string
     */
    get uuid(): string {
        return this._uuid;
    }

    /**
     * @description Add a component instance to the entity. This method should only be called
     * internally, and never after the entity has been registered.
     * @private
     * @param {Component} component - The component to add
     * @returns {Component[]} - Updated array of components, or null if the component already existed
     */
    addComponent( component: Component ): Component[] {
        if ( this.hasComponent( component.type ) ) {
            throw Error( `Component with type ${ component.type } was already added!` );
        }
        this._components.push( component );
        return this._components;
    }

    /**
     * @description Clone the entity.
     * @returns {Entity} - New Entity instance
     */
    clone(): Entity {
        const clone = new Entity();
        clone.copy( this );
        return clone;
    }

    /**
     * @description Copy another entity (such as an assembly) into the entity, replacing all
     * components.
     * @param {Entity} source - Entity to copy
     */
    copy( source ): void {
        this._type = source.type;
        this._name = source.name;
        this._components = [];
        source.components.forEach( ( component ) => {
            this._components.push( component.clone() );
        });
    }

    /**
     * @description Get a component instance by type from the entity.
     * @readonly
     * @param {string} type - Component type
     * @returns {Component} - Requested component instance
     */
    getComponent( type: string ): Component {
        return getItem( type, this._components, "_type" );
    }

    /**
     * @description Get data by component type from the entity. This is basically a shorthand for
     * .getComponent.getData();
     * @readonly
     * @param {string} type - Component type
     * @returns {any} - Requested component data
     */
    getComponentData( type: string ): any {
        const component = this.getComponent( type );
        if ( !component ) {
            throw Error( `Component with type ${ type } doesn't exist!` );
        }
        return component.data;
    }

    /**
     * @description Check if a component is present within the entity.
     * @readonly
     * @param {string} type - Component type
     * @returns {boolean} - True if the component is present
     */
    hasComponent( type: string ): boolean {
        return hasItem( type, this._components, "type" );
    }

    /**
     * @description Check if the entity is watchable by a given system.
     * @readonly
     * @param {System} system - System instance
     * @returns {boolean} - True if the entity is watchable
     */
    isWatchableBy( system: System ) {

        // Faster to loop through search criteria vs. all components on entity
        for ( const type of system.watchedComponentTypes ) {

            // Return early if any required component is missing on entity
            if ( !this.hasComponent( type ) ) {
                return false;
            }
        }
        return true;
    }

    /**
     * @description Remove a component instance from the entity. This method should only be called
     * internally, and never after the entity has been registered.
     * @private
     * @param {string} type - Component type
     * @returns {Component[]} - Array of component instances
     */
    removeComponent( type: string ): Component[] {
        const index = this._components.indexOf( this.getComponent( type ) );
        if ( index === -1 ) {
            throw Error( `Component with type ${ type } doesn't exist!` );
        }
        this._components.splice( index, 1 );
        return this._components;
    }

    /**
     * @description Overwrite the data for a component of the given type within the entity.
     * @param {string} type - Component type
     * @param {Object} data - Data object
     */
    setComponentData( type: string, data: {}): void {
        const index = this._components.indexOf( this.getComponent( type ) );
        if ( index === -1 ) {
            throw Error( `Component with type ${ type } doesn't exist!` );
        }
        const component = this.getComponent( type );
        component.mergeData( data );
        this.dirty = true;
    }

}