/* global platypus */
import {arrayCache, greenSplice, union} from './utils/array.js';
import Async from './Async.js';
import Data from './Data.js';
import Messenger from './Messenger.js';
import StateMap from './StateMap.js';
import createComponentClass from './factory.js';
const
getComponentClass = function (componentDefinition) {
if (componentDefinition.type) {
if (typeof componentDefinition.type === 'function') {
return componentDefinition.type;
} else if (platypus.components[componentDefinition.type]) {
return platypus.components[componentDefinition.type];
}
} else if (componentDefinition.id) { // "type" not specified, so we create the component directly.
return createComponentClass(componentDefinition);
} else if (typeof componentDefinition === 'function') {
return componentDefinition;
} else {
return null;
}
};
export default (function () {
var componentInit = function (Component, componentDefinition, callback) {
this.addComponent(new Component(this, componentDefinition, callback));
},
entityIds = {};
/**
* The Entity object acts as a container for components, facilitates communication between components and other game objects, and includes properties set by components to maintain a current state. The entity object serves as the foundation for most of the game objects in the platypus engine.
*
* ## JSON Definition Example
{
"id": "entity-id",
// "entity-id" becomes `entity.type` once the entity is created.
"components": [
// This array lists one or more component definition objects
{"type": "example-component"}
// The component objects must include a "type" property corresponding to a component to load, but may also include additional properties to customize the component in a particular way for this entity.
],
"properties": {
// This object lists properties that will be attached directly to this entity.
"x": 240
// For example, `x` becomes `entity.x` on the new entity.
},
"preload": ['image.png', 'sound.mp3']
// assets that need to be loaded before this entity loads
}
*
* @memberof platypus
* @extends platypus.Messenger
**/
class Entity extends Messenger {
/**
* @param {Object} [definition] Base definition for the entity.
* @param {Object} [definition.id] This declares the type of entity and will be stored on the Entity as `entity.type` after instantiation.
* @param {Object} [definition.components] This lists the components that should be attached to this entity.
* @param {Object} [definition.properties] [definition.properties] This is a list of key/value pairs that are added directly to the Entity as `entity.key = value`.
* @param {Object} [instanceDefinition] Specific instance definition including properties that override the base definition properties.
* @param {Object} [instanceDefinition.properties] This is a list of key/value pairs that are added directly to the Entity as `entity.key = value`.
* @param {Function} [callback] A function to run once all of the components on the Entity have been loaded. The first parameter is the entity itself.
* @param {Entity} [parent] Presets the parent of the entity so that the parent entity is available during component instantiation. Overrides `parent` in properties definitions.
* @return {Entity} Returns the new entity made up of the provided components.
* @fires platypus.Entity#load
*/
constructor (definition, instanceDefinition, callback, parent) {
var i = 0,
componentDefinition = null,
componentInits = arrayCache.setUp(),
def = Data.setUp(definition),
componentDefinitions = def.components,
defaultProperties = Data.setUp(def.properties),
instance = Data.setUp(instanceDefinition),
instanceProperties = Data.setUp(instance.properties),
savedEvents = arrayCache.setUp();
// Set properties of messenger on this entity.
super();
this.components = arrayCache.setUp();
this.type = def.id || 'none';
this.id = instance.id || instanceProperties.id;
if (this.id) { // check to make sure auto-ids don't overlap.
if (this.id.search(this.type + '-') === 0) {
i = parseInt(this.id.substring(this.id.search('-') + 1), 10);
if (!isNaN(i) && (!entityIds[this.type] || (entityIds[this.type] <= i))) {
entityIds[this.type] = i + 1;
}
}
} else {
if (!entityIds[this.type]) {
entityIds[this.type] = 0;
}
this.id = this.type + '-' + entityIds[this.type];
entityIds[this.type] += 1;
}
this.setProperty(defaultProperties); // This takes the list of properties in the JSON definition and appends them directly to the object.
this.setProperty(instanceProperties); // This takes the list of options for this particular instance and appends them directly to the object.
this.on('set-property', function (keyValuePairs) {
this.setProperty(keyValuePairs);
}.bind(this));
this.state = StateMap.setUp(this.state); //starts with no state information. This expands with boolean value properties entered by various logic components.
this.lastState = StateMap.setUp(); //This is used to determine if the state of the entity has changed.
if (parent) {
this.parent = parent;
}
this.trigger = this.triggerEvent = function (trigger, ...args) {
savedEvents.push(trigger.bind(this, ...args));
return -1; // Message has not been delivered yet.
}.bind(this, this.trigger);
if (componentDefinitions) {
for (i = 0; i < componentDefinitions.length; i++) {
componentDefinition = componentDefinitions[i];
if (componentDefinition) {
if (componentDefinition.type) {
const
componentClass = getComponentClass(componentDefinition);
if (componentClass) {
componentInits.push(componentInit.bind(this, componentClass, componentDefinition));
} else {
platypus.debug.warn('Entity "' + this.type + '": Component "' + componentDefinition.type + '" is not defined.', componentDefinition);
}
} else if (componentDefinition.id) { // "type" not specified, so we create the component directly.
componentInits.push(componentInit.bind(this, createComponentClass(componentDefinition), null));
} else if (typeof componentDefinition === 'function') {
componentInits.push(componentInit.bind(this, componentDefinition, null));
} else {
platypus.debug.warn('Entity "' + this.type + '": Component must have an `id` or `type` value.', componentDefinition);
}
}
}
}
this.loadingComponents = Async.setUp(componentInits, function () {
this.loadingComponents = null;
// Trigger saved events that were being fired during component addition.
delete this.trigger;
delete this.triggerEvent;
for (let i = 0; i < savedEvents.length; i++) {
savedEvents[i]();
}
arrayCache.recycle(savedEvents);
/**
* The entity triggers `load` on itself once all the properties and components have been attached, notifying the components that all their peer components are ready for messages.
*
* @event platypus.Entity#load
*/
this.triggerEvent('load');
if (callback) {
callback(this);
}
}.bind(this));
arrayCache.recycle(componentInits);
def.recycle();
defaultProperties.recycle();
instance.recycle();
instanceProperties.recycle();
}
/**
* Returns a string describing the entity.
*
* @return {String} Returns the entity type as a string of the form "[Entity entity-type]".
**/
toString () {
return "[Entity " + this.type + "]";
}
/**
* Returns a JSON object describing the entity.
*
* @param includeComponents {Boolean} Whether the returned JSON should list components. Defaults to `false` to condense output since components are generally defined in `platypus.game.settings.entities`, but may be needed for custom-constructed entities not so defined.
* @return {Object} Returns a JSON definition that can be used to recreate the entity.
**/
toJSON (includeComponents) {
var components = this.components,
definition = {
properties: {
id: this.id,
state: this.state.toJSON()
}
},
i = 0,
json = null,
properties = definition.properties;
if (includeComponents) {
definition.id = this.type;
definition.components = [];
} else {
definition.type = this.type;
}
for (i = 0; i < components.length; i++) {
json = components[i].toJSON(properties);
if (includeComponents && json) {
definition.components.push(json);
}
}
return definition;
}
/**
* Attaches the provided component to the entity.
*
* @param {platypus.Component} component Must be an object that functions as a Component.
* @return {platypus.Component} Returns the same object that was submitted.
* @fires platypus.Entity#component-added
**/
addComponent (component) {
this.components.push(component);
/**
* The entity triggers `component-added` on itself once a component has been attached, notifying other components of their peer component.
*
* @event platypus.Entity#component-added
* @param {platypus.Component} component The added component.
* @param {String} component.type The type of component.
**/
this.triggerEvent('component-added', component);
return component;
}
/**
* Removes the mentioned component from the entity.
*
* @param {Component} component Must be a [[Component]] attached to the entity.
* @return {Component} Returns the same object that was submitted if removal was successful; otherwise returns false (the component was not found attached to the entity).
* @fires platypus.Entity#component-removed
**/
removeComponent (component) {
var i = 0;
/**
* The entity triggers `component-removed` on itself once a component has been removed, notifying other components of their peer component's removal.
*
* @event platypus.Entity#component-removed
* @param {Component} component The removed component.
* @param {String} component.type The type of component.
**/
if (typeof component === 'string') {
for (i = 0; i < this.components.length; i++) {
if (this.components[i].type === component) {
component = this.components[i];
greenSplice(this.components, i);
this.triggerEvent('component-removed', component);
component.destroy();
return component;
}
}
} else {
for (i = 0; i < this.components.length; i++) {
if (this.components[i] === component) {
greenSplice(this.components, i);
this.triggerEvent('component-removed', component);
component.destroy();
return component;
}
}
}
return false;
}
/**
* This method sets one or more properties on the entity.
*
* @param {Object} properties A list of key/value pairs to set as properties on the entity.
**/
setProperty (properties) {
var index = '';
for (index in properties) { // This takes a list of properties and appends them directly to the object.
if (properties.hasOwnProperty(index)) {
this[index] = properties[index];
}
}
}
/**
* This method removes all components from the entity.
*
**/
destroy () {
var components = this.components;
if (!this._destroyed) {
while (components.length) {
components[0].destroy();
components.shift();
}
arrayCache.recycle(components);
this.components = null;
this.state.recycle();
this.state = null;
this.lastState.recycle();
this.lastState = null;
super.destroy();
}
}
/**
* Returns all of the assets required for this Entity. This method calls the corresponding method on all components to determine the list of assets.
*
* @param definition {Object} The definition for the Entity.
* @param properties {Object} Properties for this instance of the Entity.
* @param data {Object} Layer data that affects asset list.
* @return {Array} A list of the necessary assets to load.
*/
static getAssetList (def, props, data) {
var i = 0,
component = null,
arr = null,
assets = null,
definition = null;
if (def.type) {
definition = platypus.game.settings.entities[def.type];
if (!definition) {
platypus.debug.warn('Entity "' + def.type + '": This entity is not defined.', def);
return arrayCache.setUp();
}
return Entity.getAssetList(definition, def.properties, data);
}
assets = union(arrayCache.setUp(), def.preload);
for (i = 0; i < def.components.length; i++) {
component = getComponentClass(def.components[i]);
if (component) {
arr = component.getAssetList(def.components[i], def.properties, props, data);
union(assets, arr);
arrayCache.recycle(arr);
}
}
return assets;
}
}
return Entity;
}());