/* global platypus */
import {arrayCache, greenSplice} from './utils/array.js';
import Messenger from './Messenger.js';
import StateMap from './StateMap.js';
import createComponentClass from './factory.js';
const
entityIds = {},
getComponentClass = function (componentDefinition) {
const
{type} = componentDefinition;
if (type) {
if (typeof type === 'function') {
return type;
} else if (platypus.components[type]) {
return platypus.components[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;
}
};
/**
* 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
**/
export default class Entity extends Messenger {
/**
* @param {Object} [definition] Base definition for the entity.
* @param {Object} [definition.components] This lists the components that should be attached to this 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.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 ({
components: componentDefinitions,
properties: defaultProperties = {},
id: type
}, {
id,
properties: instanceProperties = {}
} = {}, callback, parent) {
// Set properties of messenger on this entity.
super();
const
componentInit = (Component, componentDefinition) => new Promise((resolve) => this.addComponent(new Component(this, componentDefinition, resolve))),
componentInits = arrayCache.setUp(),
savedEvents = arrayCache.setUp(),
trigger = this.trigger; // trigger reference for saved events
this.components = arrayCache.setUp();
this.type = type || 'none';
this.id = id ?? instanceProperties.id;
if (this.id) { // check to make sure auto-ids don't overlap.
if (this.id.search(this.type + '-') === 0) {
const
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', (keyValuePairs) => {
this.setProperty(keyValuePairs);
});
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 = (...args) => {
savedEvents.push(trigger.bind(this, ...args));
return -1; // Message has not been delivered yet.
};
if (componentDefinitions) {
for (let i = 0; i < componentDefinitions.length; i++) {
const
componentDefinition = componentDefinitions[i];
if (componentDefinition) {
if (componentDefinition.type) {
const
componentClass = getComponentClass(componentDefinition);
if (componentClass) {
componentInits.push(componentInit(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(createComponentClass(componentDefinition)));
} else if (typeof componentDefinition === 'function') {
componentInits.push(componentInit(componentDefinition));
} else {
platypus.debug.warn('Entity "' + this.type + '": Component must have an `id` or `type` value.', componentDefinition);
}
}
}
}
this.loadingComponents = Promise.all(componentInits).then(() => {
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);
}
});
arrayCache.recycle(componentInits);
}
/**
* 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) {
const
{components, type} = this,
properties = {
id: this.id,
state: this.state.toJSON()
},
definition = {
properties
};
if (includeComponents) {
definition.id = type;
definition.components = [];
} else {
definition.type = type;
}
for (let i = 0; i < components.length; i++) {
const
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) {
/**
* 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 (let 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 (let 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 gets a component by its id.
*
* @param {String} componentId
* @returns {Component} A component
*/
getComponentById (componentId) {
return this.components.filter(({id}) => id === componentId)[0] ?? null;
}
/**
* This method gets all components by their type.
*
* @param {String} componentType
* @returns {Array} An array of components
*/
getComponentsByType (componentType) {
return this.components.filter(({type}) => type === componentType);
}
/**
* Allows lookup for components or child entities and their properties.
*
* For example, "component-id.sprite" will return an entity component's sprite property value.
*
* @param {String} path
* @returns {*}
*/
get (path) {
const
tree = path.split('.');
let obj = this;
while (tree.length) {
let param = tree.shift();
obj = obj[param] ?? obj.getComponentById?.(param) ?? obj.getEntityById?.(param);
if (!obj) {
return null;
}
}
return obj;
}
/**
* 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) {
const
keys = Object.keys(properties);
// This takes a list of properties and appends them directly to the object.
for (let i = 0; i < keys.length; i++) {
const
key = keys[i];
this[key] = properties[key];
}
}
/**
* This method removes all components from the entity.
*
**/
destroy () {
const
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 ({components = [], preload = [], properties = {}, type} = {}, props, data) {
if (type) {
const
definition = platypus.game.settings.entities[type];
if (!definition) {
platypus.debug.warn(`Entity "${type}": This entity is not defined.`);
return arrayCache.setUp();
}
return Entity.getAssetList({
...definition,
preload: [
...preload,
...definition.preload ?? []
]
}, properties, data);
} else {
return [...new Set([
...preload,
...components.map((component) => getComponentClass(component)?.getAssetList(component, properties, props, data) ?? []).flat()
])];
}
}
}