1793 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			1793 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | /** | ||
|  |  * Return the name of a component | ||
|  |  * @param {Component} Component | ||
|  |  * @private | ||
|  |  */ | ||
|  | 
 | ||
|  | /** | ||
|  |  * Get a key from a list of components | ||
|  |  * @param {Array(Component)} Components Array of components to generate the key | ||
|  |  * @private | ||
|  |  */ | ||
|  | function queryKey(Components) { | ||
|  |   var ids = []; | ||
|  |   for (var n = 0; n < Components.length; n++) { | ||
|  |     var T = Components[n]; | ||
|  | 
 | ||
|  |     if (!componentRegistered(T)) { | ||
|  |       throw new Error(`Tried to create a query with an unregistered component`); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (typeof T === "object") { | ||
|  |       var operator = T.operator === "not" ? "!" : T.operator; | ||
|  |       ids.push(operator + T.Component._typeId); | ||
|  |     } else { | ||
|  |       ids.push(T._typeId); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return ids.sort().join("-"); | ||
|  | } | ||
|  | 
 | ||
|  | // Detector for browser's "window"
 | ||
|  | const hasWindow = typeof window !== "undefined"; | ||
|  | 
 | ||
|  | // performance.now() "polyfill"
 | ||
|  | const now = | ||
|  |   hasWindow && typeof window.performance !== "undefined" | ||
|  |     ? performance.now.bind(performance) | ||
|  |     : Date.now.bind(Date); | ||
|  | 
 | ||
|  | function componentRegistered(T) { | ||
|  |   return ( | ||
|  |     (typeof T === "object" && T.Component._typeId !== undefined) || | ||
|  |     (T.isComponent && T._typeId !== undefined) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | class SystemManager { | ||
|  |   constructor(world) { | ||
|  |     this._systems = []; | ||
|  |     this._executeSystems = []; // Systems that have `execute` method
 | ||
|  |     this.world = world; | ||
|  |     this.lastExecutedSystem = null; | ||
|  |   } | ||
|  | 
 | ||
|  |   registerSystem(SystemClass, attributes) { | ||
|  |     if (!SystemClass.isSystem) { | ||
|  |       throw new Error( | ||
|  |         `System '${SystemClass.name}' does not extend 'System' class` | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this.getSystem(SystemClass) !== undefined) { | ||
|  |       console.warn(`System '${SystemClass.getName()}' already registered.`); | ||
|  |       return this; | ||
|  |     } | ||
|  | 
 | ||
|  |     var system = new SystemClass(this.world, attributes); | ||
|  |     if (system.init) system.init(attributes); | ||
|  |     system.order = this._systems.length; | ||
|  |     this._systems.push(system); | ||
|  |     if (system.execute) { | ||
|  |       this._executeSystems.push(system); | ||
|  |       this.sortSystems(); | ||
|  |     } | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   unregisterSystem(SystemClass) { | ||
|  |     let system = this.getSystem(SystemClass); | ||
|  |     if (system === undefined) { | ||
|  |       console.warn( | ||
|  |         `Can unregister system '${SystemClass.getName()}'. It doesn't exist.` | ||
|  |       ); | ||
|  |       return this; | ||
|  |     } | ||
|  | 
 | ||
|  |     this._systems.splice(this._systems.indexOf(system), 1); | ||
|  | 
 | ||
|  |     if (system.execute) { | ||
|  |       this._executeSystems.splice(this._executeSystems.indexOf(system), 1); | ||
|  |     } | ||
|  | 
 | ||
|  |     // @todo Add system.unregister() call to free resources
 | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   sortSystems() { | ||
|  |     this._executeSystems.sort((a, b) => { | ||
|  |       return a.priority - b.priority || a.order - b.order; | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   getSystem(SystemClass) { | ||
|  |     return this._systems.find((s) => s instanceof SystemClass); | ||
|  |   } | ||
|  | 
 | ||
|  |   getSystems() { | ||
|  |     return this._systems; | ||
|  |   } | ||
|  | 
 | ||
|  |   removeSystem(SystemClass) { | ||
|  |     var index = this._systems.indexOf(SystemClass); | ||
|  |     if (!~index) return; | ||
|  | 
 | ||
|  |     this._systems.splice(index, 1); | ||
|  |   } | ||
|  | 
 | ||
|  |   executeSystem(system, delta, time) { | ||
|  |     if (system.initialized) { | ||
|  |       if (system.canExecute()) { | ||
|  |         let startTime = now(); | ||
|  |         system.execute(delta, time); | ||
|  |         system.executeTime = now() - startTime; | ||
|  |         this.lastExecutedSystem = system; | ||
|  |         system.clearEvents(); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   stop() { | ||
|  |     this._executeSystems.forEach((system) => system.stop()); | ||
|  |   } | ||
|  | 
 | ||
|  |   execute(delta, time, forcePlay) { | ||
|  |     this._executeSystems.forEach( | ||
|  |       (system) => | ||
|  |         (forcePlay || system.enabled) && this.executeSystem(system, delta, time) | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   stats() { | ||
|  |     var stats = { | ||
|  |       numSystems: this._systems.length, | ||
|  |       systems: {}, | ||
|  |     }; | ||
|  | 
 | ||
|  |     for (var i = 0; i < this._systems.length; i++) { | ||
|  |       var system = this._systems[i]; | ||
|  |       var systemStats = (stats.systems[system.getName()] = { | ||
|  |         queries: {}, | ||
|  |         executeTime: system.executeTime, | ||
|  |       }); | ||
|  |       for (var name in system.ctx) { | ||
|  |         systemStats.queries[name] = system.ctx[name].stats(); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return stats; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | class ObjectPool { | ||
|  |   // @todo Add initial size
 | ||
|  |   constructor(T, initialSize) { | ||
|  |     this.freeList = []; | ||
|  |     this.count = 0; | ||
|  |     this.T = T; | ||
|  |     this.isObjectPool = true; | ||
|  | 
 | ||
|  |     if (typeof initialSize !== "undefined") { | ||
|  |       this.expand(initialSize); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   acquire() { | ||
|  |     // Grow the list by 20%ish if we're out
 | ||
|  |     if (this.freeList.length <= 0) { | ||
|  |       this.expand(Math.round(this.count * 0.2) + 1); | ||
|  |     } | ||
|  | 
 | ||
|  |     var item = this.freeList.pop(); | ||
|  | 
 | ||
|  |     return item; | ||
|  |   } | ||
|  | 
 | ||
|  |   release(item) { | ||
|  |     item.reset(); | ||
|  |     this.freeList.push(item); | ||
|  |   } | ||
|  | 
 | ||
|  |   expand(count) { | ||
|  |     for (var n = 0; n < count; n++) { | ||
|  |       var clone = new this.T(); | ||
|  |       clone._pool = this; | ||
|  |       this.freeList.push(clone); | ||
|  |     } | ||
|  |     this.count += count; | ||
|  |   } | ||
|  | 
 | ||
|  |   totalSize() { | ||
|  |     return this.count; | ||
|  |   } | ||
|  | 
 | ||
|  |   totalFree() { | ||
|  |     return this.freeList.length; | ||
|  |   } | ||
|  | 
 | ||
|  |   totalUsed() { | ||
|  |     return this.count - this.freeList.length; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * @class EventDispatcher | ||
|  |  */ | ||
|  | class EventDispatcher { | ||
|  |   constructor() { | ||
|  |     this._listeners = {}; | ||
|  |     this.stats = { | ||
|  |       fired: 0, | ||
|  |       handled: 0, | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Add an event listener | ||
|  |    * @param {String} eventName Name of the event to listen | ||
|  |    * @param {Function} listener Callback to trigger when the event is fired | ||
|  |    */ | ||
|  |   addEventListener(eventName, listener) { | ||
|  |     let listeners = this._listeners; | ||
|  |     if (listeners[eventName] === undefined) { | ||
|  |       listeners[eventName] = []; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (listeners[eventName].indexOf(listener) === -1) { | ||
|  |       listeners[eventName].push(listener); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Check if an event listener is already added to the list of listeners | ||
|  |    * @param {String} eventName Name of the event to check | ||
|  |    * @param {Function} listener Callback for the specified event | ||
|  |    */ | ||
|  |   hasEventListener(eventName, listener) { | ||
|  |     return ( | ||
|  |       this._listeners[eventName] !== undefined && | ||
|  |       this._listeners[eventName].indexOf(listener) !== -1 | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove an event listener | ||
|  |    * @param {String} eventName Name of the event to remove | ||
|  |    * @param {Function} listener Callback for the specified event | ||
|  |    */ | ||
|  |   removeEventListener(eventName, listener) { | ||
|  |     var listenerArray = this._listeners[eventName]; | ||
|  |     if (listenerArray !== undefined) { | ||
|  |       var index = listenerArray.indexOf(listener); | ||
|  |       if (index !== -1) { | ||
|  |         listenerArray.splice(index, 1); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Dispatch an event | ||
|  |    * @param {String} eventName Name of the event to dispatch | ||
|  |    * @param {Entity} entity (Optional) Entity to emit | ||
|  |    * @param {Component} component | ||
|  |    */ | ||
|  |   dispatchEvent(eventName, entity, component) { | ||
|  |     this.stats.fired++; | ||
|  | 
 | ||
|  |     var listenerArray = this._listeners[eventName]; | ||
|  |     if (listenerArray !== undefined) { | ||
|  |       var array = listenerArray.slice(0); | ||
|  | 
 | ||
|  |       for (var i = 0; i < array.length; i++) { | ||
|  |         array[i].call(this, entity, component); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Reset stats counters | ||
|  |    */ | ||
|  |   resetCounters() { | ||
|  |     this.stats.fired = this.stats.handled = 0; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | class Query { | ||
|  |   /** | ||
|  |    * @param {Array(Component)} Components List of types of components to query | ||
|  |    */ | ||
|  |   constructor(Components, manager) { | ||
|  |     this.Components = []; | ||
|  |     this.NotComponents = []; | ||
|  | 
 | ||
|  |     Components.forEach((component) => { | ||
|  |       if (typeof component === "object") { | ||
|  |         this.NotComponents.push(component.Component); | ||
|  |       } else { | ||
|  |         this.Components.push(component); | ||
|  |       } | ||
|  |     }); | ||
|  | 
 | ||
|  |     if (this.Components.length === 0) { | ||
|  |       throw new Error("Can't create a query without components"); | ||
|  |     } | ||
|  | 
 | ||
|  |     this.entities = []; | ||
|  | 
 | ||
|  |     this.eventDispatcher = new EventDispatcher(); | ||
|  | 
 | ||
|  |     // This query is being used by a reactive system
 | ||
|  |     this.reactive = false; | ||
|  | 
 | ||
|  |     this.key = queryKey(Components); | ||
|  | 
 | ||
|  |     // Fill the query with the existing entities
 | ||
|  |     for (var i = 0; i < manager._entities.length; i++) { | ||
|  |       var entity = manager._entities[i]; | ||
|  |       if (this.match(entity)) { | ||
|  |         // @todo ??? this.addEntity(entity); => preventing the event to be generated
 | ||
|  |         entity.queries.push(this); | ||
|  |         this.entities.push(entity); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Add entity to this query | ||
|  |    * @param {Entity} entity | ||
|  |    */ | ||
|  |   addEntity(entity) { | ||
|  |     entity.queries.push(this); | ||
|  |     this.entities.push(entity); | ||
|  | 
 | ||
|  |     this.eventDispatcher.dispatchEvent(Query.prototype.ENTITY_ADDED, entity); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove entity from this query | ||
|  |    * @param {Entity} entity | ||
|  |    */ | ||
|  |   removeEntity(entity) { | ||
|  |     let index = this.entities.indexOf(entity); | ||
|  |     if (~index) { | ||
|  |       this.entities.splice(index, 1); | ||
|  | 
 | ||
|  |       index = entity.queries.indexOf(this); | ||
|  |       entity.queries.splice(index, 1); | ||
|  | 
 | ||
|  |       this.eventDispatcher.dispatchEvent( | ||
|  |         Query.prototype.ENTITY_REMOVED, | ||
|  |         entity | ||
|  |       ); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   match(entity) { | ||
|  |     return ( | ||
|  |       entity.hasAllComponents(this.Components) && | ||
|  |       !entity.hasAnyComponents(this.NotComponents) | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   toJSON() { | ||
|  |     return { | ||
|  |       key: this.key, | ||
|  |       reactive: this.reactive, | ||
|  |       components: { | ||
|  |         included: this.Components.map((C) => C.name), | ||
|  |         not: this.NotComponents.map((C) => C.name), | ||
|  |       }, | ||
|  |       numEntities: this.entities.length, | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Return stats for this query | ||
|  |    */ | ||
|  |   stats() { | ||
|  |     return { | ||
|  |       numComponents: this.Components.length, | ||
|  |       numEntities: this.entities.length, | ||
|  |     }; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Query.prototype.ENTITY_ADDED = "Query#ENTITY_ADDED"; | ||
|  | Query.prototype.ENTITY_REMOVED = "Query#ENTITY_REMOVED"; | ||
|  | Query.prototype.COMPONENT_CHANGED = "Query#COMPONENT_CHANGED"; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * @class QueryManager | ||
|  |  */ | ||
|  | class QueryManager { | ||
|  |   constructor(world) { | ||
|  |     this._world = world; | ||
|  | 
 | ||
|  |     // Queries indexed by a unique identifier for the components it has
 | ||
|  |     this._queries = {}; | ||
|  |   } | ||
|  | 
 | ||
|  |   onEntityRemoved(entity) { | ||
|  |     for (var queryName in this._queries) { | ||
|  |       var query = this._queries[queryName]; | ||
|  |       if (entity.queries.indexOf(query) !== -1) { | ||
|  |         query.removeEntity(entity); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Callback when a component is added to an entity | ||
|  |    * @param {Entity} entity Entity that just got the new component | ||
|  |    * @param {Component} Component Component added to the entity | ||
|  |    */ | ||
|  |   onEntityComponentAdded(entity, Component) { | ||
|  |     // @todo Use bitmask for checking components?
 | ||
|  | 
 | ||
|  |     // Check each indexed query to see if we need to add this entity to the list
 | ||
|  |     for (var queryName in this._queries) { | ||
|  |       var query = this._queries[queryName]; | ||
|  | 
 | ||
|  |       if ( | ||
|  |         !!~query.NotComponents.indexOf(Component) && | ||
|  |         ~query.entities.indexOf(entity) | ||
|  |       ) { | ||
|  |         query.removeEntity(entity); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       // Add the entity only if:
 | ||
|  |       // Component is in the query
 | ||
|  |       // and Entity has ALL the components of the query
 | ||
|  |       // and Entity is not already in the query
 | ||
|  |       if ( | ||
|  |         !~query.Components.indexOf(Component) || | ||
|  |         !query.match(entity) || | ||
|  |         ~query.entities.indexOf(entity) | ||
|  |       ) | ||
|  |         continue; | ||
|  | 
 | ||
|  |       query.addEntity(entity); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Callback when a component is removed from an entity | ||
|  |    * @param {Entity} entity Entity to remove the component from | ||
|  |    * @param {Component} Component Component to remove from the entity | ||
|  |    */ | ||
|  |   onEntityComponentRemoved(entity, Component) { | ||
|  |     for (var queryName in this._queries) { | ||
|  |       var query = this._queries[queryName]; | ||
|  | 
 | ||
|  |       if ( | ||
|  |         !!~query.NotComponents.indexOf(Component) && | ||
|  |         !~query.entities.indexOf(entity) && | ||
|  |         query.match(entity) | ||
|  |       ) { | ||
|  |         query.addEntity(entity); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       if ( | ||
|  |         !!~query.Components.indexOf(Component) && | ||
|  |         !!~query.entities.indexOf(entity) && | ||
|  |         !query.match(entity) | ||
|  |       ) { | ||
|  |         query.removeEntity(entity); | ||
|  |         continue; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Get a query for the specified components | ||
|  |    * @param {Component} Components Components that the query should have | ||
|  |    */ | ||
|  |   getQuery(Components) { | ||
|  |     var key = queryKey(Components); | ||
|  |     var query = this._queries[key]; | ||
|  |     if (!query) { | ||
|  |       this._queries[key] = query = new Query(Components, this._world); | ||
|  |     } | ||
|  |     return query; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Return some stats from this class | ||
|  |    */ | ||
|  |   stats() { | ||
|  |     var stats = {}; | ||
|  |     for (var queryName in this._queries) { | ||
|  |       stats[queryName] = this._queries[queryName].stats(); | ||
|  |     } | ||
|  |     return stats; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | class Component { | ||
|  |   constructor(props) { | ||
|  |     if (props !== false) { | ||
|  |       const schema = this.constructor.schema; | ||
|  | 
 | ||
|  |       for (const key in schema) { | ||
|  |         if (props && props.hasOwnProperty(key)) { | ||
|  |           this[key] = props[key]; | ||
|  |         } else { | ||
|  |           const schemaProp = schema[key]; | ||
|  |           if (schemaProp.hasOwnProperty("default")) { | ||
|  |             this[key] = schemaProp.type.clone(schemaProp.default); | ||
|  |           } else { | ||
|  |             const type = schemaProp.type; | ||
|  |             this[key] = type.clone(type.default); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       if ( props !== undefined) { | ||
|  |         this.checkUndefinedAttributes(props); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this._pool = null; | ||
|  |   } | ||
|  | 
 | ||
|  |   copy(source) { | ||
|  |     const schema = this.constructor.schema; | ||
|  | 
 | ||
|  |     for (const key in schema) { | ||
|  |       const prop = schema[key]; | ||
|  | 
 | ||
|  |       if (source.hasOwnProperty(key)) { | ||
|  |         this[key] = prop.type.copy(source[key], this[key]); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // @DEBUG
 | ||
|  |     { | ||
|  |       this.checkUndefinedAttributes(source); | ||
|  |     } | ||
|  | 
 | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   clone() { | ||
|  |     return new this.constructor().copy(this); | ||
|  |   } | ||
|  | 
 | ||
|  |   reset() { | ||
|  |     const schema = this.constructor.schema; | ||
|  | 
 | ||
|  |     for (const key in schema) { | ||
|  |       const schemaProp = schema[key]; | ||
|  | 
 | ||
|  |       if (schemaProp.hasOwnProperty("default")) { | ||
|  |         this[key] = schemaProp.type.copy(schemaProp.default, this[key]); | ||
|  |       } else { | ||
|  |         const type = schemaProp.type; | ||
|  |         this[key] = type.copy(type.default, this[key]); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   dispose() { | ||
|  |     if (this._pool) { | ||
|  |       this._pool.release(this); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   getName() { | ||
|  |     return this.constructor.getName(); | ||
|  |   } | ||
|  | 
 | ||
|  |   checkUndefinedAttributes(src) { | ||
|  |     const schema = this.constructor.schema; | ||
|  | 
 | ||
|  |     // Check that the attributes defined in source are also defined in the schema
 | ||
|  |     Object.keys(src).forEach((srcKey) => { | ||
|  |       if (!schema.hasOwnProperty(srcKey)) { | ||
|  |         console.warn( | ||
|  |           `Trying to set attribute '${srcKey}' not defined in the '${this.constructor.name}' schema. Please fix the schema, the attribute value won't be set` | ||
|  |         ); | ||
|  |       } | ||
|  |     }); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Component.schema = {}; | ||
|  | Component.isComponent = true; | ||
|  | Component.getName = function () { | ||
|  |   return this.displayName || this.name; | ||
|  | }; | ||
|  | 
 | ||
|  | class SystemStateComponent extends Component {} | ||
|  | 
 | ||
|  | SystemStateComponent.isSystemStateComponent = true; | ||
|  | 
 | ||
|  | class EntityPool extends ObjectPool { | ||
|  |   constructor(entityManager, entityClass, initialSize) { | ||
|  |     super(entityClass, undefined); | ||
|  |     this.entityManager = entityManager; | ||
|  | 
 | ||
|  |     if (typeof initialSize !== "undefined") { | ||
|  |       this.expand(initialSize); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   expand(count) { | ||
|  |     for (var n = 0; n < count; n++) { | ||
|  |       var clone = new this.T(this.entityManager); | ||
|  |       clone._pool = this; | ||
|  |       this.freeList.push(clone); | ||
|  |     } | ||
|  |     this.count += count; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  * @class EntityManager | ||
|  |  */ | ||
|  | class EntityManager { | ||
|  |   constructor(world) { | ||
|  |     this.world = world; | ||
|  |     this.componentsManager = world.componentsManager; | ||
|  | 
 | ||
|  |     // All the entities in this instance
 | ||
|  |     this._entities = []; | ||
|  |     this._nextEntityId = 0; | ||
|  | 
 | ||
|  |     this._entitiesByNames = {}; | ||
|  | 
 | ||
|  |     this._queryManager = new QueryManager(this); | ||
|  |     this.eventDispatcher = new EventDispatcher(); | ||
|  |     this._entityPool = new EntityPool( | ||
|  |       this, | ||
|  |       this.world.options.entityClass, | ||
|  |       this.world.options.entityPoolSize | ||
|  |     ); | ||
|  | 
 | ||
|  |     // Deferred deletion
 | ||
|  |     this.entitiesWithComponentsToRemove = []; | ||
|  |     this.entitiesToRemove = []; | ||
|  |     this.deferredRemovalEnabled = true; | ||
|  |   } | ||
|  | 
 | ||
|  |   getEntityByName(name) { | ||
|  |     return this._entitiesByNames[name]; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Create a new entity | ||
|  |    */ | ||
|  |   createEntity(name) { | ||
|  |     var entity = this._entityPool.acquire(); | ||
|  |     entity.alive = true; | ||
|  |     entity.name = name || ""; | ||
|  |     if (name) { | ||
|  |       if (this._entitiesByNames[name]) { | ||
|  |         console.warn(`Entity name '${name}' already exist`); | ||
|  |       } else { | ||
|  |         this._entitiesByNames[name] = entity; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this._entities.push(entity); | ||
|  |     this.eventDispatcher.dispatchEvent(ENTITY_CREATED, entity); | ||
|  |     return entity; | ||
|  |   } | ||
|  | 
 | ||
|  |   // COMPONENTS
 | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Add a component to an entity | ||
|  |    * @param {Entity} entity Entity where the component will be added | ||
|  |    * @param {Component} Component Component to be added to the entity | ||
|  |    * @param {Object} values Optional values to replace the default attributes | ||
|  |    */ | ||
|  |   entityAddComponent(entity, Component, values) { | ||
|  |     // @todo Probably define Component._typeId with a default value and avoid using typeof
 | ||
|  |     if ( | ||
|  |       typeof Component._typeId === "undefined" && | ||
|  |       !this.world.componentsManager._ComponentsMap[Component._typeId] | ||
|  |     ) { | ||
|  |       throw new Error( | ||
|  |         `Attempted to add unregistered component "${Component.getName()}"` | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (~entity._ComponentTypes.indexOf(Component)) { | ||
|  |       { | ||
|  |         console.warn( | ||
|  |           "Component type already exists on entity.", | ||
|  |           entity, | ||
|  |           Component.getName() | ||
|  |         ); | ||
|  |       } | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     entity._ComponentTypes.push(Component); | ||
|  | 
 | ||
|  |     if (Component.__proto__ === SystemStateComponent) { | ||
|  |       entity.numStateComponents++; | ||
|  |     } | ||
|  | 
 | ||
|  |     var componentPool = this.world.componentsManager.getComponentsPool( | ||
|  |       Component | ||
|  |     ); | ||
|  | 
 | ||
|  |     var component = componentPool | ||
|  |       ? componentPool.acquire() | ||
|  |       : new Component(values); | ||
|  | 
 | ||
|  |     if (componentPool && values) { | ||
|  |       component.copy(values); | ||
|  |     } | ||
|  | 
 | ||
|  |     entity._components[Component._typeId] = component; | ||
|  | 
 | ||
|  |     this._queryManager.onEntityComponentAdded(entity, Component); | ||
|  |     this.world.componentsManager.componentAddedToEntity(Component); | ||
|  | 
 | ||
|  |     this.eventDispatcher.dispatchEvent(COMPONENT_ADDED, entity, Component); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove a component from an entity | ||
|  |    * @param {Entity} entity Entity which will get removed the component | ||
|  |    * @param {*} Component Component to remove from the entity | ||
|  |    * @param {Bool} immediately If you want to remove the component immediately instead of deferred (Default is false) | ||
|  |    */ | ||
|  |   entityRemoveComponent(entity, Component, immediately) { | ||
|  |     var index = entity._ComponentTypes.indexOf(Component); | ||
|  |     if (!~index) return; | ||
|  | 
 | ||
|  |     this.eventDispatcher.dispatchEvent(COMPONENT_REMOVE, entity, Component); | ||
|  | 
 | ||
|  |     if (immediately) { | ||
|  |       this._entityRemoveComponentSync(entity, Component, index); | ||
|  |     } else { | ||
|  |       if (entity._ComponentTypesToRemove.length === 0) | ||
|  |         this.entitiesWithComponentsToRemove.push(entity); | ||
|  | 
 | ||
|  |       entity._ComponentTypes.splice(index, 1); | ||
|  |       entity._ComponentTypesToRemove.push(Component); | ||
|  | 
 | ||
|  |       entity._componentsToRemove[Component._typeId] = | ||
|  |         entity._components[Component._typeId]; | ||
|  |       delete entity._components[Component._typeId]; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Check each indexed query to see if we need to remove it
 | ||
|  |     this._queryManager.onEntityComponentRemoved(entity, Component); | ||
|  | 
 | ||
|  |     if (Component.__proto__ === SystemStateComponent) { | ||
|  |       entity.numStateComponents--; | ||
|  | 
 | ||
|  |       // Check if the entity was a ghost waiting for the last system state component to be removed
 | ||
|  |       if (entity.numStateComponents === 0 && !entity.alive) { | ||
|  |         entity.remove(); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   _entityRemoveComponentSync(entity, Component, index) { | ||
|  |     // Remove T listing on entity and property ref, then free the component.
 | ||
|  |     entity._ComponentTypes.splice(index, 1); | ||
|  |     var component = entity._components[Component._typeId]; | ||
|  |     delete entity._components[Component._typeId]; | ||
|  |     component.dispose(); | ||
|  |     this.world.componentsManager.componentRemovedFromEntity(Component); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove all the components from an entity | ||
|  |    * @param {Entity} entity Entity from which the components will be removed | ||
|  |    */ | ||
|  |   entityRemoveAllComponents(entity, immediately) { | ||
|  |     let Components = entity._ComponentTypes; | ||
|  | 
 | ||
|  |     for (let j = Components.length - 1; j >= 0; j--) { | ||
|  |       if (Components[j].__proto__ !== SystemStateComponent) | ||
|  |         this.entityRemoveComponent(entity, Components[j], immediately); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove the entity from this manager. It will clear also its components | ||
|  |    * @param {Entity} entity Entity to remove from the manager | ||
|  |    * @param {Bool} immediately If you want to remove the component immediately instead of deferred (Default is false) | ||
|  |    */ | ||
|  |   removeEntity(entity, immediately) { | ||
|  |     var index = this._entities.indexOf(entity); | ||
|  | 
 | ||
|  |     if (!~index) throw new Error("Tried to remove entity not in list"); | ||
|  | 
 | ||
|  |     entity.alive = false; | ||
|  |     this.entityRemoveAllComponents(entity, immediately); | ||
|  | 
 | ||
|  |     if (entity.numStateComponents === 0) { | ||
|  |       // Remove from entity list
 | ||
|  |       this.eventDispatcher.dispatchEvent(ENTITY_REMOVED, entity); | ||
|  |       this._queryManager.onEntityRemoved(entity); | ||
|  |       if (immediately === true) { | ||
|  |         this._releaseEntity(entity, index); | ||
|  |       } else { | ||
|  |         this.entitiesToRemove.push(entity); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   _releaseEntity(entity, index) { | ||
|  |     this._entities.splice(index, 1); | ||
|  | 
 | ||
|  |     if (this._entitiesByNames[entity.name]) { | ||
|  |       delete this._entitiesByNames[entity.name]; | ||
|  |     } | ||
|  |     entity._pool.release(entity); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Remove all entities from this manager | ||
|  |    */ | ||
|  |   removeAllEntities() { | ||
|  |     for (var i = this._entities.length - 1; i >= 0; i--) { | ||
|  |       this.removeEntity(this._entities[i]); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   processDeferredRemoval() { | ||
|  |     if (!this.deferredRemovalEnabled) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     for (let i = 0; i < this.entitiesToRemove.length; i++) { | ||
|  |       let entity = this.entitiesToRemove[i]; | ||
|  |       let index = this._entities.indexOf(entity); | ||
|  |       this._releaseEntity(entity, index); | ||
|  |     } | ||
|  |     this.entitiesToRemove.length = 0; | ||
|  | 
 | ||
|  |     for (let i = 0; i < this.entitiesWithComponentsToRemove.length; i++) { | ||
|  |       let entity = this.entitiesWithComponentsToRemove[i]; | ||
|  |       while (entity._ComponentTypesToRemove.length > 0) { | ||
|  |         let Component = entity._ComponentTypesToRemove.pop(); | ||
|  | 
 | ||
|  |         var component = entity._componentsToRemove[Component._typeId]; | ||
|  |         delete entity._componentsToRemove[Component._typeId]; | ||
|  |         component.dispose(); | ||
|  |         this.world.componentsManager.componentRemovedFromEntity(Component); | ||
|  | 
 | ||
|  |         //this._entityRemoveComponentSync(entity, Component, index);
 | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this.entitiesWithComponentsToRemove.length = 0; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Get a query based on a list of components | ||
|  |    * @param {Array(Component)} Components List of components that will form the query | ||
|  |    */ | ||
|  |   queryComponents(Components) { | ||
|  |     return this._queryManager.getQuery(Components); | ||
|  |   } | ||
|  | 
 | ||
|  |   // EXTRAS
 | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Return number of entities | ||
|  |    */ | ||
|  |   count() { | ||
|  |     return this._entities.length; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Return some stats | ||
|  |    */ | ||
|  |   stats() { | ||
|  |     var stats = { | ||
|  |       numEntities: this._entities.length, | ||
|  |       numQueries: Object.keys(this._queryManager._queries).length, | ||
|  |       queries: this._queryManager.stats(), | ||
|  |       numComponentPool: Object.keys(this.componentsManager._componentPool) | ||
|  |         .length, | ||
|  |       componentPool: {}, | ||
|  |       eventDispatcher: this.eventDispatcher.stats, | ||
|  |     }; | ||
|  | 
 | ||
|  |     for (var ecsyComponentId in this.componentsManager._componentPool) { | ||
|  |       var pool = this.componentsManager._componentPool[ecsyComponentId]; | ||
|  |       stats.componentPool[pool.T.getName()] = { | ||
|  |         used: pool.totalUsed(), | ||
|  |         size: pool.count, | ||
|  |       }; | ||
|  |     } | ||
|  | 
 | ||
|  |     return stats; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const ENTITY_CREATED = "EntityManager#ENTITY_CREATE"; | ||
|  | const ENTITY_REMOVED = "EntityManager#ENTITY_REMOVED"; | ||
|  | const COMPONENT_ADDED = "EntityManager#COMPONENT_ADDED"; | ||
|  | const COMPONENT_REMOVE = "EntityManager#COMPONENT_REMOVE"; | ||
|  | 
 | ||
|  | class ComponentManager { | ||
|  |   constructor() { | ||
|  |     this.Components = []; | ||
|  |     this._ComponentsMap = {}; | ||
|  | 
 | ||
|  |     this._componentPool = {}; | ||
|  |     this.numComponents = {}; | ||
|  |     this.nextComponentId = 0; | ||
|  |   } | ||
|  | 
 | ||
|  |   hasComponent(Component) { | ||
|  |     return this.Components.indexOf(Component) !== -1; | ||
|  |   } | ||
|  | 
 | ||
|  |   registerComponent(Component, objectPool) { | ||
|  |     if (this.Components.indexOf(Component) !== -1) { | ||
|  |       console.warn( | ||
|  |         `Component type: '${Component.getName()}' already registered.` | ||
|  |       ); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const schema = Component.schema; | ||
|  | 
 | ||
|  |     if (!schema) { | ||
|  |       throw new Error( | ||
|  |         `Component "${Component.getName()}" has no schema property.` | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     for (const propName in schema) { | ||
|  |       const prop = schema[propName]; | ||
|  | 
 | ||
|  |       if (!prop.type) { | ||
|  |         throw new Error( | ||
|  |           `Invalid schema for component "${Component.getName()}". Missing type for "${propName}" property.` | ||
|  |         ); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     Component._typeId = this.nextComponentId++; | ||
|  |     this.Components.push(Component); | ||
|  |     this._ComponentsMap[Component._typeId] = Component; | ||
|  |     this.numComponents[Component._typeId] = 0; | ||
|  | 
 | ||
|  |     if (objectPool === undefined) { | ||
|  |       objectPool = new ObjectPool(Component); | ||
|  |     } else if (objectPool === false) { | ||
|  |       objectPool = undefined; | ||
|  |     } | ||
|  | 
 | ||
|  |     this._componentPool[Component._typeId] = objectPool; | ||
|  |   } | ||
|  | 
 | ||
|  |   componentAddedToEntity(Component) { | ||
|  |     this.numComponents[Component._typeId]++; | ||
|  |   } | ||
|  | 
 | ||
|  |   componentRemovedFromEntity(Component) { | ||
|  |     this.numComponents[Component._typeId]--; | ||
|  |   } | ||
|  | 
 | ||
|  |   getComponentsPool(Component) { | ||
|  |     return this._componentPool[Component._typeId]; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const Version = "0.3.1"; | ||
|  | 
 | ||
|  | const proxyMap = new WeakMap(); | ||
|  | 
 | ||
|  | const proxyHandler = { | ||
|  |   set(target, prop) { | ||
|  |     throw new Error( | ||
|  |       `Tried to write to "${target.constructor.getName()}#${String( | ||
|  |         prop | ||
|  |       )}" on immutable component. Use .getMutableComponent() to modify a component.`
 | ||
|  |     ); | ||
|  |   }, | ||
|  | }; | ||
|  | 
 | ||
|  | function wrapImmutableComponent(T, component) { | ||
|  |   if (component === undefined) { | ||
|  |     return undefined; | ||
|  |   } | ||
|  | 
 | ||
|  |   let wrappedComponent = proxyMap.get(component); | ||
|  | 
 | ||
|  |   if (!wrappedComponent) { | ||
|  |     wrappedComponent = new Proxy(component, proxyHandler); | ||
|  |     proxyMap.set(component, wrappedComponent); | ||
|  |   } | ||
|  | 
 | ||
|  |   return wrappedComponent; | ||
|  | } | ||
|  | 
 | ||
|  | class Entity { | ||
|  |   constructor(entityManager) { | ||
|  |     this._entityManager = entityManager || null; | ||
|  | 
 | ||
|  |     // Unique ID for this entity
 | ||
|  |     this.id = entityManager._nextEntityId++; | ||
|  | 
 | ||
|  |     // List of components types the entity has
 | ||
|  |     this._ComponentTypes = []; | ||
|  | 
 | ||
|  |     // Instance of the components
 | ||
|  |     this._components = {}; | ||
|  | 
 | ||
|  |     this._componentsToRemove = {}; | ||
|  | 
 | ||
|  |     // Queries where the entity is added
 | ||
|  |     this.queries = []; | ||
|  | 
 | ||
|  |     // Used for deferred removal
 | ||
|  |     this._ComponentTypesToRemove = []; | ||
|  | 
 | ||
|  |     this.alive = false; | ||
|  | 
 | ||
|  |     //if there are state components on a entity, it can't be removed completely
 | ||
|  |     this.numStateComponents = 0; | ||
|  |   } | ||
|  | 
 | ||
|  |   // COMPONENTS
 | ||
|  | 
 | ||
|  |   getComponent(Component, includeRemoved) { | ||
|  |     var component = this._components[Component._typeId]; | ||
|  | 
 | ||
|  |     if (!component && includeRemoved === true) { | ||
|  |       component = this._componentsToRemove[Component._typeId]; | ||
|  |     } | ||
|  | 
 | ||
|  |     return  wrapImmutableComponent(Component, component) | ||
|  |       ; | ||
|  |   } | ||
|  | 
 | ||
|  |   getRemovedComponent(Component) { | ||
|  |     const component = this._componentsToRemove[Component._typeId]; | ||
|  | 
 | ||
|  |     return  wrapImmutableComponent(Component, component) | ||
|  |       ; | ||
|  |   } | ||
|  | 
 | ||
|  |   getComponents() { | ||
|  |     return this._components; | ||
|  |   } | ||
|  | 
 | ||
|  |   getComponentsToRemove() { | ||
|  |     return this._componentsToRemove; | ||
|  |   } | ||
|  | 
 | ||
|  |   getComponentTypes() { | ||
|  |     return this._ComponentTypes; | ||
|  |   } | ||
|  | 
 | ||
|  |   getMutableComponent(Component) { | ||
|  |     var component = this._components[Component._typeId]; | ||
|  | 
 | ||
|  |     if (!component) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     for (var i = 0; i < this.queries.length; i++) { | ||
|  |       var query = this.queries[i]; | ||
|  |       // @todo accelerate this check. Maybe having query._Components as an object
 | ||
|  |       // @todo add Not components
 | ||
|  |       if (query.reactive && query.Components.indexOf(Component) !== -1) { | ||
|  |         query.eventDispatcher.dispatchEvent( | ||
|  |           Query.prototype.COMPONENT_CHANGED, | ||
|  |           this, | ||
|  |           component | ||
|  |         ); | ||
|  |       } | ||
|  |     } | ||
|  |     return component; | ||
|  |   } | ||
|  | 
 | ||
|  |   addComponent(Component, values) { | ||
|  |     this._entityManager.entityAddComponent(this, Component, values); | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   removeComponent(Component, forceImmediate) { | ||
|  |     this._entityManager.entityRemoveComponent(this, Component, forceImmediate); | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   hasComponent(Component, includeRemoved) { | ||
|  |     return ( | ||
|  |       !!~this._ComponentTypes.indexOf(Component) || | ||
|  |       (includeRemoved === true && this.hasRemovedComponent(Component)) | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   hasRemovedComponent(Component) { | ||
|  |     return !!~this._ComponentTypesToRemove.indexOf(Component); | ||
|  |   } | ||
|  | 
 | ||
|  |   hasAllComponents(Components) { | ||
|  |     for (var i = 0; i < Components.length; i++) { | ||
|  |       if (!this.hasComponent(Components[i])) return false; | ||
|  |     } | ||
|  |     return true; | ||
|  |   } | ||
|  | 
 | ||
|  |   hasAnyComponents(Components) { | ||
|  |     for (var i = 0; i < Components.length; i++) { | ||
|  |       if (this.hasComponent(Components[i])) return true; | ||
|  |     } | ||
|  |     return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   removeAllComponents(forceImmediate) { | ||
|  |     return this._entityManager.entityRemoveAllComponents(this, forceImmediate); | ||
|  |   } | ||
|  | 
 | ||
|  |   copy(src) { | ||
|  |     // TODO: This can definitely be optimized
 | ||
|  |     for (var ecsyComponentId in src._components) { | ||
|  |       var srcComponent = src._components[ecsyComponentId]; | ||
|  |       this.addComponent(srcComponent.constructor); | ||
|  |       var component = this.getComponent(srcComponent.constructor); | ||
|  |       component.copy(srcComponent); | ||
|  |     } | ||
|  | 
 | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   clone() { | ||
|  |     return new Entity(this._entityManager).copy(this); | ||
|  |   } | ||
|  | 
 | ||
|  |   reset() { | ||
|  |     this.id = this._entityManager._nextEntityId++; | ||
|  |     this._ComponentTypes.length = 0; | ||
|  |     this.queries.length = 0; | ||
|  | 
 | ||
|  |     for (var ecsyComponentId in this._components) { | ||
|  |       delete this._components[ecsyComponentId]; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   remove(forceImmediate) { | ||
|  |     return this._entityManager.removeEntity(this, forceImmediate); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const DEFAULT_OPTIONS = { | ||
|  |   entityPoolSize: 0, | ||
|  |   entityClass: Entity, | ||
|  | }; | ||
|  | 
 | ||
|  | class World { | ||
|  |   constructor(options = {}) { | ||
|  |     this.options = Object.assign({}, DEFAULT_OPTIONS, options); | ||
|  | 
 | ||
|  |     this.componentsManager = new ComponentManager(this); | ||
|  |     this.entityManager = new EntityManager(this); | ||
|  |     this.systemManager = new SystemManager(this); | ||
|  | 
 | ||
|  |     this.enabled = true; | ||
|  | 
 | ||
|  |     this.eventQueues = {}; | ||
|  | 
 | ||
|  |     if (hasWindow && typeof CustomEvent !== "undefined") { | ||
|  |       var event = new CustomEvent("ecsy-world-created", { | ||
|  |         detail: { world: this, version: Version }, | ||
|  |       }); | ||
|  |       window.dispatchEvent(event); | ||
|  |     } | ||
|  | 
 | ||
|  |     this.lastTime = now() / 1000; | ||
|  |   } | ||
|  | 
 | ||
|  |   registerComponent(Component, objectPool) { | ||
|  |     this.componentsManager.registerComponent(Component, objectPool); | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   registerSystem(System, attributes) { | ||
|  |     this.systemManager.registerSystem(System, attributes); | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   hasRegisteredComponent(Component) { | ||
|  |     return this.componentsManager.hasComponent(Component); | ||
|  |   } | ||
|  | 
 | ||
|  |   unregisterSystem(System) { | ||
|  |     this.systemManager.unregisterSystem(System); | ||
|  |     return this; | ||
|  |   } | ||
|  | 
 | ||
|  |   getSystem(SystemClass) { | ||
|  |     return this.systemManager.getSystem(SystemClass); | ||
|  |   } | ||
|  | 
 | ||
|  |   getSystems() { | ||
|  |     return this.systemManager.getSystems(); | ||
|  |   } | ||
|  | 
 | ||
|  |   execute(delta, time) { | ||
|  |     if (!delta) { | ||
|  |       time = now() / 1000; | ||
|  |       delta = time - this.lastTime; | ||
|  |       this.lastTime = time; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this.enabled) { | ||
|  |       this.systemManager.execute(delta, time); | ||
|  |       this.entityManager.processDeferredRemoval(); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   stop() { | ||
|  |     this.enabled = false; | ||
|  |   } | ||
|  | 
 | ||
|  |   play() { | ||
|  |     this.enabled = true; | ||
|  |   } | ||
|  | 
 | ||
|  |   createEntity(name) { | ||
|  |     return this.entityManager.createEntity(name); | ||
|  |   } | ||
|  | 
 | ||
|  |   stats() { | ||
|  |     var stats = { | ||
|  |       entities: this.entityManager.stats(), | ||
|  |       system: this.systemManager.stats(), | ||
|  |     }; | ||
|  | 
 | ||
|  |     return stats; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | class System { | ||
|  |   canExecute() { | ||
|  |     if (this._mandatoryQueries.length === 0) return true; | ||
|  | 
 | ||
|  |     for (let i = 0; i < this._mandatoryQueries.length; i++) { | ||
|  |       var query = this._mandatoryQueries[i]; | ||
|  |       if (query.entities.length === 0) { | ||
|  |         return false; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return true; | ||
|  |   } | ||
|  | 
 | ||
|  |   getName() { | ||
|  |     return this.constructor.getName(); | ||
|  |   } | ||
|  | 
 | ||
|  |   constructor(world, attributes) { | ||
|  |     this.world = world; | ||
|  |     this.enabled = true; | ||
|  | 
 | ||
|  |     // @todo Better naming :)
 | ||
|  |     this._queries = {}; | ||
|  |     this.queries = {}; | ||
|  | 
 | ||
|  |     this.priority = 0; | ||
|  | 
 | ||
|  |     // Used for stats
 | ||
|  |     this.executeTime = 0; | ||
|  | 
 | ||
|  |     if (attributes && attributes.priority) { | ||
|  |       this.priority = attributes.priority; | ||
|  |     } | ||
|  | 
 | ||
|  |     this._mandatoryQueries = []; | ||
|  | 
 | ||
|  |     this.initialized = true; | ||
|  | 
 | ||
|  |     if (this.constructor.queries) { | ||
|  |       for (var queryName in this.constructor.queries) { | ||
|  |         var queryConfig = this.constructor.queries[queryName]; | ||
|  |         var Components = queryConfig.components; | ||
|  |         if (!Components || Components.length === 0) { | ||
|  |           throw new Error("'components' attribute can't be empty in a query"); | ||
|  |         } | ||
|  | 
 | ||
|  |         // Detect if the components have already been registered
 | ||
|  |         let unregisteredComponents = Components.filter( | ||
|  |           (Component) => !componentRegistered(Component) | ||
|  |         ); | ||
|  | 
 | ||
|  |         if (unregisteredComponents.length > 0) { | ||
|  |           throw new Error( | ||
|  |             `Tried to create a query '${ | ||
|  |               this.constructor.name | ||
|  |             }.${queryName}' with unregistered components: [${unregisteredComponents | ||
|  |               .map((c) => c.getName()) | ||
|  |               .join(", ")}]`
 | ||
|  |           ); | ||
|  |         } | ||
|  | 
 | ||
|  |         var query = this.world.entityManager.queryComponents(Components); | ||
|  | 
 | ||
|  |         this._queries[queryName] = query; | ||
|  |         if (queryConfig.mandatory === true) { | ||
|  |           this._mandatoryQueries.push(query); | ||
|  |         } | ||
|  |         this.queries[queryName] = { | ||
|  |           results: query.entities, | ||
|  |         }; | ||
|  | 
 | ||
|  |         // Reactive configuration added/removed/changed
 | ||
|  |         var validEvents = ["added", "removed", "changed"]; | ||
|  | 
 | ||
|  |         const eventMapping = { | ||
|  |           added: Query.prototype.ENTITY_ADDED, | ||
|  |           removed: Query.prototype.ENTITY_REMOVED, | ||
|  |           changed: Query.prototype.COMPONENT_CHANGED, // Query.prototype.ENTITY_CHANGED
 | ||
|  |         }; | ||
|  | 
 | ||
|  |         if (queryConfig.listen) { | ||
|  |           validEvents.forEach((eventName) => { | ||
|  |             if (!this.execute) { | ||
|  |               console.warn( | ||
|  |                 `System '${this.getName()}' has defined listen events (${validEvents.join( | ||
|  |                   ", " | ||
|  |                 )}) for query '${queryName}' but it does not implement the 'execute' method.`
 | ||
|  |               ); | ||
|  |             } | ||
|  | 
 | ||
|  |             // Is the event enabled on this system's query?
 | ||
|  |             if (queryConfig.listen[eventName]) { | ||
|  |               let event = queryConfig.listen[eventName]; | ||
|  | 
 | ||
|  |               if (eventName === "changed") { | ||
|  |                 query.reactive = true; | ||
|  |                 if (event === true) { | ||
|  |                   // Any change on the entity from the components in the query
 | ||
|  |                   let eventList = (this.queries[queryName][eventName] = []); | ||
|  |                   query.eventDispatcher.addEventListener( | ||
|  |                     Query.prototype.COMPONENT_CHANGED, | ||
|  |                     (entity) => { | ||
|  |                       // Avoid duplicates
 | ||
|  |                       if (eventList.indexOf(entity) === -1) { | ||
|  |                         eventList.push(entity); | ||
|  |                       } | ||
|  |                     } | ||
|  |                   ); | ||
|  |                 } else if (Array.isArray(event)) { | ||
|  |                   let eventList = (this.queries[queryName][eventName] = []); | ||
|  |                   query.eventDispatcher.addEventListener( | ||
|  |                     Query.prototype.COMPONENT_CHANGED, | ||
|  |                     (entity, changedComponent) => { | ||
|  |                       // Avoid duplicates
 | ||
|  |                       if ( | ||
|  |                         event.indexOf(changedComponent.constructor) !== -1 && | ||
|  |                         eventList.indexOf(entity) === -1 | ||
|  |                       ) { | ||
|  |                         eventList.push(entity); | ||
|  |                       } | ||
|  |                     } | ||
|  |                   ); | ||
|  |                 } | ||
|  |               } else { | ||
|  |                 let eventList = (this.queries[queryName][eventName] = []); | ||
|  | 
 | ||
|  |                 query.eventDispatcher.addEventListener( | ||
|  |                   eventMapping[eventName], | ||
|  |                   (entity) => { | ||
|  |                     // @fixme overhead?
 | ||
|  |                     if (eventList.indexOf(entity) === -1) | ||
|  |                       eventList.push(entity); | ||
|  |                   } | ||
|  |                 ); | ||
|  |               } | ||
|  |             } | ||
|  |           }); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   stop() { | ||
|  |     this.executeTime = 0; | ||
|  |     this.enabled = false; | ||
|  |   } | ||
|  | 
 | ||
|  |   play() { | ||
|  |     this.enabled = true; | ||
|  |   } | ||
|  | 
 | ||
|  |   // @question rename to clear queues?
 | ||
|  |   clearEvents() { | ||
|  |     for (let queryName in this.queries) { | ||
|  |       var query = this.queries[queryName]; | ||
|  |       if (query.added) { | ||
|  |         query.added.length = 0; | ||
|  |       } | ||
|  |       if (query.removed) { | ||
|  |         query.removed.length = 0; | ||
|  |       } | ||
|  |       if (query.changed) { | ||
|  |         if (Array.isArray(query.changed)) { | ||
|  |           query.changed.length = 0; | ||
|  |         } else { | ||
|  |           for (let name in query.changed) { | ||
|  |             query.changed[name].length = 0; | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   toJSON() { | ||
|  |     var json = { | ||
|  |       name: this.getName(), | ||
|  |       enabled: this.enabled, | ||
|  |       executeTime: this.executeTime, | ||
|  |       priority: this.priority, | ||
|  |       queries: {}, | ||
|  |     }; | ||
|  | 
 | ||
|  |     if (this.constructor.queries) { | ||
|  |       var queries = this.constructor.queries; | ||
|  |       for (let queryName in queries) { | ||
|  |         let query = this.queries[queryName]; | ||
|  |         let queryDefinition = queries[queryName]; | ||
|  |         let jsonQuery = (json.queries[queryName] = { | ||
|  |           key: this._queries[queryName].key, | ||
|  |         }); | ||
|  | 
 | ||
|  |         jsonQuery.mandatory = queryDefinition.mandatory === true; | ||
|  |         jsonQuery.reactive = | ||
|  |           queryDefinition.listen && | ||
|  |           (queryDefinition.listen.added === true || | ||
|  |             queryDefinition.listen.removed === true || | ||
|  |             queryDefinition.listen.changed === true || | ||
|  |             Array.isArray(queryDefinition.listen.changed)); | ||
|  | 
 | ||
|  |         if (jsonQuery.reactive) { | ||
|  |           jsonQuery.listen = {}; | ||
|  | 
 | ||
|  |           const methods = ["added", "removed", "changed"]; | ||
|  |           methods.forEach((method) => { | ||
|  |             if (query[method]) { | ||
|  |               jsonQuery.listen[method] = { | ||
|  |                 entities: query[method].length, | ||
|  |               }; | ||
|  |             } | ||
|  |           }); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return json; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | System.isSystem = true; | ||
|  | System.getName = function () { | ||
|  |   return this.displayName || this.name; | ||
|  | }; | ||
|  | 
 | ||
|  | function Not(Component) { | ||
|  |   return { | ||
|  |     operator: "not", | ||
|  |     Component: Component, | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | class TagComponent extends Component { | ||
|  |   constructor() { | ||
|  |     super(false); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | TagComponent.isTagComponent = true; | ||
|  | 
 | ||
|  | const copyValue = (src) => src; | ||
|  | 
 | ||
|  | const cloneValue = (src) => src; | ||
|  | 
 | ||
|  | const copyArray = (src, dest) => { | ||
|  |   if (!src) { | ||
|  |     return src; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!dest) { | ||
|  |     return src.slice(); | ||
|  |   } | ||
|  | 
 | ||
|  |   dest.length = 0; | ||
|  | 
 | ||
|  |   for (let i = 0; i < src.length; i++) { | ||
|  |     dest.push(src[i]); | ||
|  |   } | ||
|  | 
 | ||
|  |   return dest; | ||
|  | }; | ||
|  | 
 | ||
|  | const cloneArray = (src) => src && src.slice(); | ||
|  | 
 | ||
|  | const copyJSON = (src) => JSON.parse(JSON.stringify(src)); | ||
|  | 
 | ||
|  | const cloneJSON = (src) => JSON.parse(JSON.stringify(src)); | ||
|  | 
 | ||
|  | const copyCopyable = (src, dest) => { | ||
|  |   if (!src) { | ||
|  |     return src; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!dest) { | ||
|  |     return src.clone(); | ||
|  |   } | ||
|  | 
 | ||
|  |   return dest.copy(src); | ||
|  | }; | ||
|  | 
 | ||
|  | const cloneClonable = (src) => src && src.clone(); | ||
|  | 
 | ||
|  | function createType(typeDefinition) { | ||
|  |   var mandatoryProperties = ["name", "default", "copy", "clone"]; | ||
|  | 
 | ||
|  |   var undefinedProperties = mandatoryProperties.filter((p) => { | ||
|  |     return !typeDefinition.hasOwnProperty(p); | ||
|  |   }); | ||
|  | 
 | ||
|  |   if (undefinedProperties.length > 0) { | ||
|  |     throw new Error( | ||
|  |       `createType expects a type definition with the following properties: ${undefinedProperties.join( | ||
|  |         ", " | ||
|  |       )}`
 | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   typeDefinition.isType = true; | ||
|  | 
 | ||
|  |   return typeDefinition; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Standard types | ||
|  |  */ | ||
|  | const Types = { | ||
|  |   Number: createType({ | ||
|  |     name: "Number", | ||
|  |     default: 0, | ||
|  |     copy: copyValue, | ||
|  |     clone: cloneValue, | ||
|  |   }), | ||
|  | 
 | ||
|  |   Boolean: createType({ | ||
|  |     name: "Boolean", | ||
|  |     default: false, | ||
|  |     copy: copyValue, | ||
|  |     clone: cloneValue, | ||
|  |   }), | ||
|  | 
 | ||
|  |   String: createType({ | ||
|  |     name: "String", | ||
|  |     default: "", | ||
|  |     copy: copyValue, | ||
|  |     clone: cloneValue, | ||
|  |   }), | ||
|  | 
 | ||
|  |   Array: createType({ | ||
|  |     name: "Array", | ||
|  |     default: [], | ||
|  |     copy: copyArray, | ||
|  |     clone: cloneArray, | ||
|  |   }), | ||
|  | 
 | ||
|  |   Ref: createType({ | ||
|  |     name: "Ref", | ||
|  |     default: undefined, | ||
|  |     copy: copyValue, | ||
|  |     clone: cloneValue, | ||
|  |   }), | ||
|  | 
 | ||
|  |   JSON: createType({ | ||
|  |     name: "JSON", | ||
|  |     default: null, | ||
|  |     copy: copyJSON, | ||
|  |     clone: cloneJSON, | ||
|  |   }), | ||
|  | }; | ||
|  | 
 | ||
|  | function generateId(length) { | ||
|  |   var result = ""; | ||
|  |   var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; | ||
|  |   var charactersLength = characters.length; | ||
|  |   for (var i = 0; i < length; i++) { | ||
|  |     result += characters.charAt(Math.floor(Math.random() * charactersLength)); | ||
|  |   } | ||
|  |   return result; | ||
|  | } | ||
|  | 
 | ||
|  | function injectScript(src, onLoad) { | ||
|  |   var script = document.createElement("script"); | ||
|  |   // @todo Use link to the ecsy-devtools repo?
 | ||
|  |   script.src = src; | ||
|  |   script.onload = onLoad; | ||
|  |   (document.head || document.documentElement).appendChild(script); | ||
|  | } | ||
|  | 
 | ||
|  | /* global Peer */ | ||
|  | 
 | ||
|  | function hookConsoleAndErrors(connection) { | ||
|  |   var wrapFunctions = ["error", "warning", "log"]; | ||
|  |   wrapFunctions.forEach((key) => { | ||
|  |     if (typeof console[key] === "function") { | ||
|  |       var fn = console[key].bind(console); | ||
|  |       console[key] = (...args) => { | ||
|  |         connection.send({ | ||
|  |           method: "console", | ||
|  |           type: key, | ||
|  |           args: JSON.stringify(args), | ||
|  |         }); | ||
|  |         return fn.apply(null, args); | ||
|  |       }; | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   window.addEventListener("error", (error) => { | ||
|  |     connection.send({ | ||
|  |       method: "error", | ||
|  |       error: JSON.stringify({ | ||
|  |         message: error.error.message, | ||
|  |         stack: error.error.stack, | ||
|  |       }), | ||
|  |     }); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | function includeRemoteIdHTML(remoteId) { | ||
|  |   let infoDiv = document.createElement("div"); | ||
|  |   infoDiv.style.cssText = `
 | ||
|  |     align-items: center; | ||
|  |     background-color: #333; | ||
|  |     color: #aaa; | ||
|  |     display:flex; | ||
|  |     font-family: Arial; | ||
|  |     font-size: 1.1em; | ||
|  |     height: 40px; | ||
|  |     justify-content: center; | ||
|  |     left: 0; | ||
|  |     opacity: 0.9; | ||
|  |     position: absolute; | ||
|  |     right: 0; | ||
|  |     text-align: center; | ||
|  |     top: 0; | ||
|  |   `;
 | ||
|  | 
 | ||
|  |   infoDiv.innerHTML = `Open ECSY devtools to connect to this page using the code: <b style="color: #fff">${remoteId}</b> <button onClick="generateNewCode()">Generate new code</button>`; | ||
|  |   document.body.appendChild(infoDiv); | ||
|  | 
 | ||
|  |   return infoDiv; | ||
|  | } | ||
|  | 
 | ||
|  | function enableRemoteDevtools(remoteId) { | ||
|  |   if (!hasWindow) { | ||
|  |     console.warn("Remote devtools not available outside the browser"); | ||
|  |     return; | ||
|  |   } | ||
|  | 
 | ||
|  |   window.generateNewCode = () => { | ||
|  |     window.localStorage.clear(); | ||
|  |     remoteId = generateId(6); | ||
|  |     window.localStorage.setItem("ecsyRemoteId", remoteId); | ||
|  |     window.location.reload(false); | ||
|  |   }; | ||
|  | 
 | ||
|  |   remoteId = remoteId || window.localStorage.getItem("ecsyRemoteId"); | ||
|  |   if (!remoteId) { | ||
|  |     remoteId = generateId(6); | ||
|  |     window.localStorage.setItem("ecsyRemoteId", remoteId); | ||
|  |   } | ||
|  | 
 | ||
|  |   let infoDiv = includeRemoteIdHTML(remoteId); | ||
|  | 
 | ||
|  |   window.__ECSY_REMOTE_DEVTOOLS_INJECTED = true; | ||
|  |   window.__ECSY_REMOTE_DEVTOOLS = {}; | ||
|  | 
 | ||
|  |   let Version = ""; | ||
|  | 
 | ||
|  |   // This is used to collect the worlds created before the communication is being established
 | ||
|  |   let worldsBeforeLoading = []; | ||
|  |   let onWorldCreated = (e) => { | ||
|  |     var world = e.detail.world; | ||
|  |     Version = e.detail.version; | ||
|  |     worldsBeforeLoading.push(world); | ||
|  |   }; | ||
|  |   window.addEventListener("ecsy-world-created", onWorldCreated); | ||
|  | 
 | ||
|  |   let onLoaded = () => { | ||
|  |     // var peer = new Peer(remoteId);
 | ||
|  |     var peer = new Peer(remoteId, { | ||
|  |       host: "peerjs.ecsy.io", | ||
|  |       secure: true, | ||
|  |       port: 443, | ||
|  |       config: { | ||
|  |         iceServers: [ | ||
|  |           { url: "stun:stun.l.google.com:19302" }, | ||
|  |           { url: "stun:stun1.l.google.com:19302" }, | ||
|  |           { url: "stun:stun2.l.google.com:19302" }, | ||
|  |           { url: "stun:stun3.l.google.com:19302" }, | ||
|  |           { url: "stun:stun4.l.google.com:19302" }, | ||
|  |         ], | ||
|  |       }, | ||
|  |       debug: 3, | ||
|  |     }); | ||
|  | 
 | ||
|  |     peer.on("open", (/* id */) => { | ||
|  |       peer.on("connection", (connection) => { | ||
|  |         window.__ECSY_REMOTE_DEVTOOLS.connection = connection; | ||
|  |         connection.on("open", function () { | ||
|  |           // infoDiv.style.visibility = "hidden";
 | ||
|  |           infoDiv.innerHTML = "Connected"; | ||
|  | 
 | ||
|  |           // Receive messages
 | ||
|  |           connection.on("data", function (data) { | ||
|  |             if (data.type === "init") { | ||
|  |               var script = document.createElement("script"); | ||
|  |               script.setAttribute("type", "text/javascript"); | ||
|  |               script.onload = () => { | ||
|  |                 script.parentNode.removeChild(script); | ||
|  | 
 | ||
|  |                 // Once the script is injected we don't need to listen
 | ||
|  |                 window.removeEventListener( | ||
|  |                   "ecsy-world-created", | ||
|  |                   onWorldCreated | ||
|  |                 ); | ||
|  |                 worldsBeforeLoading.forEach((world) => { | ||
|  |                   var event = new CustomEvent("ecsy-world-created", { | ||
|  |                     detail: { world: world, version: Version }, | ||
|  |                   }); | ||
|  |                   window.dispatchEvent(event); | ||
|  |                 }); | ||
|  |               }; | ||
|  |               script.innerHTML = data.script; | ||
|  |               (document.head || document.documentElement).appendChild(script); | ||
|  |               script.onload(); | ||
|  | 
 | ||
|  |               hookConsoleAndErrors(connection); | ||
|  |             } else if (data.type === "executeScript") { | ||
|  |               let value = eval(data.script); | ||
|  |               if (data.returnEval) { | ||
|  |                 connection.send({ | ||
|  |                   method: "evalReturn", | ||
|  |                   value: value, | ||
|  |                 }); | ||
|  |               } | ||
|  |             } | ||
|  |           }); | ||
|  |         }); | ||
|  |       }); | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   // Inject PeerJS script
 | ||
|  |   injectScript( | ||
|  |     "https://cdn.jsdelivr.net/npm/peerjs@0.3.20/dist/peer.min.js", | ||
|  |     onLoaded | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | if (hasWindow) { | ||
|  |   const urlParams = new URLSearchParams(window.location.search); | ||
|  | 
 | ||
|  |   // @todo Provide a way to disable it if needed
 | ||
|  |   if (urlParams.has("enable-remote-devtools")) { | ||
|  |     enableRemoteDevtools(); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export { Component, Not, ObjectPool, System, SystemStateComponent, TagComponent, Types, Version, World, Entity as _Entity, cloneArray, cloneClonable, cloneJSON, cloneValue, copyArray, copyCopyable, copyJSON, copyValue, createType, enableRemoteDevtools }; |