factory.js

/**
 * The component factory takes in component definitions and creates component classes that can be used to create components by entities.  It adds properties and methods that are common to all components so that component definitions can focus on unique properties and methods.
 *
 * To create an extended component class, use the following syntax:
 *
 *      platypus.createComponentClass(componentDefinition);
 *
 *  * `componentDefinition` is list of key/value pairs that describe the component's behavior.
 *
 * See ComponentExample.js for an example componentDefinition that can be sent into this component class factory.
 *
 */
/* global platypus */
import {arrayCache, greenSlice} from './utils/array.js';
import Component from './Component.js';

const
    getId = (type) => {
        if (typeof ids[type] === 'number') {
            ids[type] += 1;
        } else {
            ids[type] = 0;
        }
        return `${type}-${ids[type]}`;
    };
let ids = {};

export default function (componentDefinition = {}) {
    const
        {events, getAssetList, id, initialize, methods, properties, publicMethods, publicProperties} = componentDefinition,

        // Not sure if this is future-proof, but putting in object to create dynamic name.
        NewComponent = ({[id]: class extends Component {
            constructor (owner, definition = {}, callback) {
                const
                    {aliases, id: componentId = getId(id)} = definition;

                super(id, owner, componentId);
    
                // Set up properties, prioritizing component settings, entity settings, and finally defaults.
                if (properties) {
                    const
                        keys = Object.keys(properties),
                        {length} = keys;

                    for (let i = 0; i < length; i++) {
                        const
                            key = keys[i];

                        this[key] = definition[key] ?? owner[key] ?? properties[key];
                    }
                }
    
                // These component properties are equivalent with `entity.property`
                if (publicProperties) {
                    const
                        keys = Object.keys(publicProperties),
                        {length} = keys;

                    for (let i = 0; i < length; i++) {
                        const
                            key = keys[i];

                        Object.defineProperty(this, key, {
                            get: function () {
                                return owner[key];
                            },
                            set: function (value) {
                                owner[key] = value;
                            },
                            enumerable: true
                        });
                        this[key] = definition[key] ?? owner[key] ?? publicProperties[key];
                    }
                }
    
                if (events) {
                    const
                        keys = Object.keys(events),
                        {length} = keys;

                    for (let i = 0; i < length; i++) {
                        const
                            key = keys[i];

                        this.addEventListener(key, events[key]);
                        if (aliases) {
                            const
                                aliasKeys = Object.keys(aliases),
                                {length} = aliasKeys;
        
                            for (let j = 0; j < length; j++) {
                                const
                                    alias = aliasKeys[j];
        
                                if (aliases[alias] === key) {
                                    this.addEventListener(alias, events[key]);
                                }
                            }
                        }
                    }
                }
    
                if (publicMethods) {
                    const
                        keys = Object.keys(publicMethods),
                        {length} = keys;

                    for (let i = 0; i < length; i++) {
                        const
                            key = keys[i];
                        let name = key;

                        if (aliases) {
                            const
                                aliasKeys = Object.keys(aliases),
                                {length} = aliasKeys;
        
                            for (let j = 0; j < length; j++) {
                                const
                                    alias = aliasKeys[j];
        
                                if (aliases[alias] === key) {
                                    name = alias;
                                }
                            }
                        }
                        this.addMethod(name, publicMethods[key]);
                    }
                }
    
                if (!this.initialize(definition, callback) && callback) { // whether the callback will be used; if not, we run immediately.
                    callback();
                }
            }
        }})[id],
        proto = NewComponent.prototype;

    if (initialize) {
        proto.initialize = initialize;
    }
    
    /**
     * Returns a JSON structure describing this componet. This can be overridden by a "toJSON" method in the component definition. This is by design.
     * 
     * @method toJSON
     * @return {Object}
     */
    proto.toJSON = (function () {
        const
            valid = function (value, depthArray) {
                const
                    type = typeof value;
                let depth = null,
                    root = false,
                    invalid = false;
                
                if (!validating) { // prevents endless validation during recursion.
                    validating = true;
                    root = true;
                }

                if (type === 'function') {
                    invalid = true;
                } else if ((type === 'object') && (value !== null)) {
                    if (value.toJSON) { // We know it's valid but we run this for the depth check to make sure that there is no recursion.
                        depth = depthArray ? greenSlice(depthArray) : arrayCache.setUp();
                        depth.push(value);
                        if (!valid(value.toJSON(), depth)) {
                            invalid = true;
                        }
                    } else if (Array.isArray(value)) {
                        let i = value.length;

                        while (i--) {
                            const
                                propValue = value[i];

                            if (depthArray && depthArray.indexOf(propValue) >= 0) {
                                invalid = true;
                                break;
                            }
                            depth = depthArray ? greenSlice(depthArray) : arrayCache.setUp();
                            depth.push(propValue);
                            if (!valid(propValue, depth)) {
                                invalid = true;
                                break;
                            }
                        }
                    } else {
                        const
                            keys = Object.keys(value),
                            {length} = keys;

                        for (let i = 0; i < length; i++) {
                            const
                                propValue = value[keys[i]];

                            if (depthArray && depthArray.indexOf(propValue) >= 0) {
                                invalid = true;
                                break;
                            }
                            depth = depthArray ? greenSlice(depthArray) : arrayCache.setUp();
                            depth.push(propValue);
                            if (!valid(propValue, depth)) {
                                invalid = true;
                                break;
                            }
                        }
                    }
                }

                if (depthArray) {
                    arrayCache.recycle(depthArray);
                }

                if (root) {
                    validating = false;
                }

                return !invalid;
            };
        let validating = false;

        return function (propertiesDefinition, debug) {
            const
                component = {
                    type: this.type
                };
            
            if (properties) {
                const
                    keys = Object.keys(properties),
                    {length} = keys;

                for (let i = 0; i < length; i++) {
                    const
                        key = keys[i];

                    if (properties[key] !== this[key]) {
                        if (debug && !validating && !valid(this[key])) {
                            platypus.debug.warn('Component "' + this.type + '" includes a non-JSON property value for "' + key + '" (type "' + (typeof this[key]) + '"). You may want to create a custom `toJSON` method for this component.', this[key]);
                        }
                        component[key] = this[key];
                    }
                }
            }

            if (publicProperties) {
                const
                    keys = Object.keys(publicProperties),
                    {length} = keys;

                for (let i = 0; i < length; i++) {
                    const
                        key = keys[i];

                    if ((publicProperties[key] !== this.owner[key]) && (typeof propertiesDefinition[key] === 'undefined')) {
                        if (debug && !validating && !valid(this.owner[key])) {
                            platypus.debug.warn('Component "' + this.type + '" includes a non-JSON public property value for "' + key + '" (type "' + (typeof this.owner[key]) + '"). You may want to create a custom `toJSON` method for this component.', this.owner[key]);
                        }
                        propertiesDefinition[key] = this.owner[key];
                    }
                }
            }

            return component;
        };
    }());

    if (methods) {
        const
            keys = Object.keys(methods),
            {length} = keys;

        for (let i = 0; i < length; i++) {
            const
                key = keys[i];

            if (key === 'destroy') {
                proto._destroy = methods[key];
            } else {
                proto[key] = methods[key];
            }
        }
    }
    if (publicMethods) {
        const
            keys = Object.keys(publicMethods),
            {length} = keys;

        for (let i = 0; i < length; i++) {
            const
                key = keys[i];

            proto[key] = publicMethods[key];
        }
    }

    if (getAssetList) {
        NewComponent.getAssetList = getAssetList;
    }

    return NewComponent;
};