/* global platypus */
import {arrayCache, greenSlice, greenSplice, union} from '../utils/array.js';
import Data from '../Data.js';
import Entity from '../Entity.js';
import Messenger from '../Messenger.js';
import createComponentClass from '../factory.js';
const
childBroadcast = function (event) {
return function (value, debug) {
this.triggerOnChildren(event, value, debug);
};
},
EntityContainer = createComponentClass(/** @lends platypus.components.EntityContainer.prototype */{
id: 'EntityContainer',
properties: {
/**
* An Array listing messages that are triggered on the entity and should be triggered on the children as well.
*
* @property childEvents
* @type Array
* @default []
*/
childEvents: []
},
/**
* This component allows the entity to contain child entities. It will add several methods to the entity to manage adding and removing entities.
*
* @memberof platypus.components
* @extends platypus.Messenger
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#add-entity
* @listens platypus.Entity#child-entity-updated
* @listens platypus.Entity#handle-logic
* @listens platypus.Entity#remove-entity
*/
initialize: function (definition, callback) {
const
//combine component list and entity list into one if they both exist.
owner = this.owner,
entities = (definition.entities && owner.entities) ? definition.entities.concat(owner.entities) : (definition.entities ?? owner.entities ?? null),
events = this.childEvents;
Messenger.initialize(this);
this.newAdds = arrayCache.setUp();
this._childEvents = {};
owner.entities = this.entities = arrayCache.setUp();
this.childEvents = arrayCache.setUp();
for (let i = 0; i < events.length; i++) {
this.addNewPublicEvent(events[i]);
}
this.addNewPrivateEvent('peer-entity-added');
this.addNewPrivateEvent('peer-entity-removed');
if (entities) {
Promise.all(entities.map((entity) => new Promise((resolve) => this.addEntity(entity, resolve)))).then(callback);
return true; // notifies owner that this component is asynchronous.
} else {
return false;
}
},
events: {
/**
* This message will added the given entity to this component's list of entities.
*
* @event platypus.Entity#add-entity
* @param entity {platypus.Entity} This is the entity to be added as a child.
* @param [callback] {Function} A function to run once all of the components on the Entity have been loaded.
*/
"add-entity": function (entity, callback) {
this.addEntity(entity, callback);
},
/**
* On receiving this message, the provided entity will be removed from the list of child entities.
*
* @method platypus.Entity#remove-entity
* @param entity {platypus.Entity} The entity to remove.
*/
"remove-entity": function (entity) {
this.removeEntity(entity);
},
"child-entity-updated": function (entity) {
this.updateChildEventListeners(entity);
},
"handle-logic": function () {
const
adds = this.newAdds,
l = adds.length;
if (l) {
const
removals = arrayCache.setUp();
let j = 0;
//must go in order so entities are added in the expected order.
for (let i = 0; i < l; i++) {
const
adding = adds[i];
if (adding.destroyed || !adding.loadingComponents) {
removals.push(i);
}
}
j = removals.length;
while (j--) {
greenSplice(adds, removals[j]);
}
arrayCache.recycle(removals);
}
}
},
methods: {
addNewPublicEvent: function (event) {
this.addNewPrivateEvent(event);
for (let i = 0; i < this.childEvents.length; i++) {
if (this.childEvents[i] === event) {
return false;
}
}
this.childEvents.push(event);
// Listens for specified messages and on receiving them, re-triggers them on child entities.
this.addEventListener(event, childBroadcast(event));
return true;
},
addNewPrivateEvent (event) {
if (this._childEvents[event]) {
return false;
}
this._childEvents[event] = true;
// Listen for message on children
for (let x = 0; x < this.entities.length; x++) {
const
listenerList = this.entities[x]._listeners[event];
if (listenerList) {
const
{handlers} = listenerList;
for (let y = 0; y < handlers.length; y++) {
this.addChildEventListener(
this.entities[x],
event,
handlers[y]
);
}
}
}
return true;
},
updateChildEventListeners: function (entity) {
this.removeChildEventListeners(entity);
this.addChildEventListeners(entity);
},
addChildEventListeners (entity) {
const
{_listeners} = entity,
keys = Object.keys(_listeners),
{length} = keys;
for (let i = 0; i < length; i++) {
const
key = keys[i],
listenerList = _listeners[key];
if (listenerList) {
const
{handlers} = listenerList;
for (let j = 0; j < handlers.length; j++) {
this.addChildEventListener(
entity,
key,
handlers[j]
);
}
}
}
},
removeChildEventListeners: function (entity) {
if (entity.containerListener?.events) {
const
{events, handlers} = entity.containerListener,
{length} = events;
for (let i = 0; i < length; i++) {
this.off(
events[i],
handlers[i].callback,
handlers[i].context
);
}
arrayCache.recycle(events);
arrayCache.recycle(handlers);
entity.containerListener.recycle();
entity.containerListener = null;
}
},
addChildEventListener: function (entity, event, handler) {
if (!entity.containerListener) {
entity.containerListener = Data.setUp(
"events", arrayCache.setUp(),
"handlers", arrayCache.setUp()
);
}
const
newHandler = this.on(
event,
handler.callback,
handler.context,
handler.once,
handler.priority
);
if (newHandler) {
entity.containerListener.events.push(event);
entity.containerListener.handlers.push(newHandler);
}
},
removeChildEventListener: function (entity, event, callback) {
const
{events, handlers} = entity.containerListener;
let i = events.length;
while (i--) {
if (
events[i] === event &&
(
!callback ||
handlers[i].callback === callback
)
) {
this.off(
event,
handlers[i].callback,
handlers[i].context
);
greenSplice(events, i);
greenSplice(handlers, i);
}
}
},
destroy: function () {
const
entities = greenSlice(this.entities); // Make a copy to handle entities being destroyed while processing list.
let i = entities.length;
while (i--) {
const
entity = entities[i];
this.removeChildEventListeners(entity);
entity.destroy();
}
arrayCache.recycle(entities);
arrayCache.recycle(this.entities);
this.owner.entities = null;
arrayCache.recycle(this.childEvents);
this.childEvents = null;
arrayCache.recycle(this.newAdds);
this.newAdds = null;
}
},
publicMethods: {
/**
* Gets an entity in this layer by its Id. Returns `null` if not found.
*
* @method platypus.components.EntityContainer#getEntityById
* @param {String} id
* @return {Entity}
*/
getEntityById: function (id) {
const
entity = this.entities.filter((entity) => entity.id === id)[0] ?? this.newAdds.filter((entity) => entity.id === id)[0];
if (entity) {
return entity;
}
for (let i = 0; i < this.entities.length; i++) {
const
selection = this.entities[i].getEntityById?.(id);
if (selection) {
return selection;
}
}
return null;
},
/**
* Returns a list of entities of the requested type.
*
* @method platypus.components.EntityContainer#getEntitiesByType
* @param {String} type
* @return {Array}
*/
getEntitiesByType: function (type) {
const
entities = arrayCache.setUp();
for (let i = 0; i < this.entities.length; i++) {
if (this.entities[i].type === type) {
entities.push(this.entities[i]);
}
if (this.entities[i].getEntitiesByType) {
const
selection = this.entities[i].getEntitiesByType(type);
union(entities, selection);
arrayCache.recycle(selection);
}
}
return entities;
},
/**
* This method adds an entity to the owner's group. If an entity definition or a reference to an entity definition is provided, the entity is created and then added to the owner's group.
*
* @method platypus.components.EntityContainer#addEntity
* @param newEntity {platypus.Entity|Object|String} Specifies the entity to add. If an object with a "type" property is provided or a String is provided, this component looks up the entity definition to create the entity.
* @param [newEntity.type] {String} If an object with a "type" property is provided, this component looks up the entity definition to create the entity.
* @param [newEntity.properties] {Object} A list of key/value pairs that sets the initial properties on the new entity.
* @param [callback] {Function} A function to run once all of the components on the Entity have been loaded.
* @return {platypus.Entity} The entity that was just added.
* @fires platypus.Entity#child-entity-added
* @fires platypus.Entity#entity-created
* @fires platypus.Entity#peer-entity-added
*/
addEntity (newEntity, callback) {
const
{owner} = this,
whenReady = (entity) => {
const
{owner, entities} = this;
let i = entities.length;
entity.triggerEvent('adopted', entity);
/**
* This message is triggered when a new entity has been added to the parent's list of children entities.
*
* @event platypus.Entity#peer-entity-added
* @param {platypus.Entity} entity The entity that was just added.
*/
while (i--) {
if (!entity.triggerEvent('peer-entity-added', entities[i])) {
break;
}
}
this.triggerEventOnChildren('peer-entity-added', entity);
this.addChildEventListeners(entity);
entities.push(entity);
/**
* This message is triggered when a new entity has been added to the list of children entities.
*
* @event platypus.Entity#child-entity-added
* @param {platypus.Entity} entity The entity that was just added.
*/
owner.triggerEvent('child-entity-added', entity);
if (callback) {
callback(entity);
}
};
let entity = null;
if (newEntity instanceof Entity) {
entity = newEntity;
entity.parent = owner;
whenReady(entity);
} else {
if (typeof newEntity === 'string') {
const
entityDefinition = platypus.game.settings.entities[newEntity];
if (entityDefinition) {
entity = new Entity(entityDefinition, {}, whenReady, owner);
} else {
throw new Error(`EntityContainer: There is no entity defined for type "${newEntity}".`);
}
} else if (newEntity.id) {
entity = new Entity(newEntity, {}, whenReady, owner);
} else {
const
entityDefinition = platypus.game.settings.entities[newEntity.type];
if (entityDefinition) {
entity = new Entity(entityDefinition, newEntity, whenReady, owner);
} else {
throw new Error(`EntityContainer: There is no entity defined for type "${newEntity.type}".`);
}
}
/**
* Called when this entity spawns a new entity, this event links the newly created entity to this entity.
*
* @event platypus.Entity#entity-created
* @param entity {platypus.Entity} The entity to link.
*/
this.owner.triggerEvent('entity-created', entity);
}
this.newAdds.push(entity);
return entity;
},
/**
* Removes the provided entity from the layer and destroys it. Returns `false` if the entity is not found in the layer.
*
* @method platypus.components.EntityContainer#removeEntity
* @param {Entity} entity
* @return {Entity}
* @fires platypus.Entity#child-entity-removed
* @fires platypus.Entity#peer-entity-removed
*/
removeEntity: function (entity) {
const
i = this.entities.indexOf(entity);
if (i >= 0) {
this.removeChildEventListeners(entity);
greenSplice(this.entities, i);
/**
* This message is triggered when an entity has been removed from the parent's list of children entities.
*
* @event platypus.Entity#peer-entity-removed
* @param {platypus.Entity} entity The entity that was just removed.
*/
this.triggerEventOnChildren('peer-entity-removed', entity);
/**
* This message is triggered when an entity has been removed from the list of children entities.
*
* @event platypus.Entity#child-entity-removed
* @param {platypus.Entity} entity The entity that was just added.
*/
this.owner.triggerEvent('child-entity-removed', entity);
entity.destroy();
entity.parent = null;
return entity;
}
return false;
},
/**
* Triggers a single event on the child entities in the layer.
*
* @method platypus.components.EntityContainer#triggerEventOnChildren
* @param {*} event
* @param {*} message
* @param {*} debug
*/
triggerEventOnChildren: function (event, message, debug) {
if (this.destroyed) {
return 0;
}
if (!this._childEvents[event]) {
this.addNewPrivateEvent(event);
}
return this.triggerEvent(event, message, debug);
},
/**
* Triggers one or more events on the child entities in the layer. This is unique from `triggerEventOnChildren` in that it also accepts an `Array` to send multiple events.
*
* @method platypus.components.EntityContainer#triggerOnChildren
* @param {*} event
* @param {*} message
* @param {*} debug
*/
triggerOnChildren: function (event) {
if (this.destroyed) {
return 0;
}
if (!this._childEvents[event]) {
this.addNewPrivateEvent(event);
}
return this.trigger.apply(this, arguments);
}
},
getAssetList: function (def, props, defaultProps, data) {
const
assets = arrayCache.setUp(),
entities = arrayCache.setUp();
if (def.entities) {
union(entities, def.entities);
}
if (props && props.entities) {
union(entities, props.entities);
} else if (defaultProps && defaultProps.entities) {
union(entities, defaultProps.entities);
}
for (let i = 0; i < entities.length; i++) {
const
arr = Entity.getAssetList(entities[i], null, data);
union(assets, arr);
arrayCache.recycle(arr);
}
arrayCache.recycle(entities);
return assets;
}
});
Messenger.mixin(EntityContainer);
export default EntityContainer;