const UUID = require('uuid/v1');
const BaseComponent = require('./component');
const Entity = require('./entity');
const QueryCache = require('./querycache');
const componentMethods = new Set(['stringify', 'clone', 'getObject', Symbol.iterator]);
/**
* ECS main class.
* see <a href='https://github.com/fritzy/ecs-js/README.md'>frityz/ecs-js</a>
* @class ECS
*/
class ECS {
/**@constructor ECS */
constructor() {
this.ticks = 0;
this.entities = new Map();
this.types = {};
this.entityComponents = new Map();
this.components = new Map();
this.queryCache = new Map();
this.subscriptions = new Map();
this.systems = new Map();
this.refs = {};
}
/**@property tick */
tick() {
this.ticks++;
return this.ticks;
}
/**
* last tick
* @property lastick */
get lastick() {
return this.ticks;
}
addRef(target, entity, component, prop, sub) {
if (!this.refs[target]) {
this.refs[target] = new Set();
}
this.refs[target].add([entity, component, prop, sub].join('...'));
}
deleteRef(target, entity, component, prop, sub) {
/* $lab:coverage:off$ */
if (!this.refs[target]) return;
/* $lab:coverage:on$ */
this.refs[target].delete([entity, component, prop, sub].join('...'));
if (this.refs[target].size === 0) {
delete this.refs[target];
}
}
registerComponent(name, definition = {}) {
const klass = class Component extends BaseComponent {}
klass.definition = definition;
Object.defineProperty(klass, 'name', {value: name});
this.registerComponentClass(klass);
return klass;
}
registerComponentClass(klass) {
// ody July 28, 2020 registering cleared created entities
if (this.types[klass.name]) {
console.warn("ECS framework thinking the component is already registered: ",
klass.name,
"\nTo avoid created entities been dropped, this registering is ignored.",
"\nEnitity Set: ", this.components.set(klass.name) );
return;
}
else {
this.types[klass.name] = klass;
this.entityComponents.set(klass.name, new Set());
this.components.set(klass.name, new Set());
}
}
// Change Log:
/** Extending fouction:
*
* when a type of component is found, it's better to give the caller a chance
* to triggere something for the component.
*
* Call this to setup the event handler.
*
* Currrently only xworld use this to setup post effect's flags.
* @param {string | array} cnames component name
* @param {function} onFound event handler
* @return {ECS} this
* @function
*/
componentTriggered(cnames, onFound) {
if (Array.isArray(cnames)) {
for (var cn of cnames)
this.componentTriggered(cn, onFound);
}
else {
if (!this.compoTriggers)
this.compoTriggers = new Set();
this.compoTriggers[cnames] = typeof onFound === 'stirng' ?
eval(onFound) : onFound;
}
return this;
}
createEntity(definition) {
if (this.compoTriggers) {
for (const type of Object.keys(definition)) {
if (typeof this.compoTriggers[type] === 'function')
this.compoTriggers[type](definition);
}
}
return new Entity(this, definition);
}
removeEntity(id) {
let entity;
if (id instanceof Entity) {
entity = id;
id = entity.id;
} else {
entity = this.getEntity(id);
}
entity.destroy();
}
getEntity(entityId) {
return this.entities.get(`${entityId}`);
}
queryEntities(args) {
/* e.g. args = {persist: CamCtrl, has: Array(2)}
has = (2) ["UserCmd", "CmdFlag"],
hasnt = [],
persist = CamCtrl {ecs: ECS, changes: Array(0), lastTick: 0},
updatedValues = 0, updatedComponents = 0
*/
// branch ANY (and IFFALL)
const { hasnt, has, iffall, any, persist, updatedValues, updatedComponents } = Object.assign({
hasnt: [],
has: [],
iffall: [],
any: [],
persist: false,
updatedValues: 0,
updatedComponents: 0
}, args);
let query;
if (persist) {
query = this.queryCache.get(persist);
}
if (!query) {
// branch ANY
query = new QueryCache(this, has, hasnt, any, iffall);
}
if (persist) {
this.queryCache.set(persist, query);
}
return query.filter(updatedValues, updatedComponents);
}
getComponents(name) {
return this.components.get(name);
}
subscribe(system, type) {
if (!this.subscriptions.has(type)) {
this.subscriptions.set(type, new Set());
}
this.subscriptions.get(type).add(system);
}
addSystem(group, system) {
if (typeof system === 'function') {
system = new system(this);
}
if (!this.systems.has(group)) {
this.systems.set(group, new Set());
}
this.systems.get(group).add(system);
}
runSystemGroup(group) {
const systems = this.systems.get(group);
if (!systems) return;
for (const system of systems) {
let entities;
if (this.queryCache.has(system)) {
entities = this.queryCache.get(system).filter();
}
system.update(this.ticks, entities ? entities : []);
system.lastTick = this.ticks;
if (system.changes.length !== 0) {
system.changes = [];
}
}
}
_clearEntityFromCache(entity) {
for (const query of this.queryCache) {
query[1].clearEntity(entity);
}
}
_updateCache(entity) {
// e.g. this.queryCache = Map(2) {D3Pie => QueryCache, XSankey => QueryCache}
for (const query of this.queryCache) {
query[1].updateEntity(entity); // e.g. query = [D3Pie, QueryCache]
}
}
_sendChange(component, op, key, old, value) {
if (!component._ready) return;
component.updated = component.entity.updatedValues = this.ticks;
const systems = this.subscriptions.get(component.type);
if (systems) {
const change = { component, op, key, old, value };
for (const system of systems) {
system._sendChange(change);
}
}
}
}
module.exports = ECS;