import {compose, EventEmitter} from '@avocado/core'; import {QuadTree, Rectangle, Vector} from '@avocado/math'; import { SynchronizedCreatePacket, SynchronizedDestroyPacket, } from '@avocado/net'; import {EntityListUpdateEntityPacket} from '../packets/entity-list-update-entity.packet'; import {Entity} from '../entity.synchronized'; const decorate = compose( EventEmitter, ); export class EntityList extends decorate(class {}) { constructor() { super(); this._afterDestructionTickers = []; this._entities = {}; this._entityTickers = [] this._flatEntities = []; this._informedEntities = new Map(); this._quadTree = new QuadTree(); } *[Symbol.iterator]() { for (const uuid in this._entities) { const entity = this._entities[uuid]; yield entity; } } acceptPacket(packet) { if (packet instanceof EntityListUpdateEntityPacket) { for (let i = 0; i < packet.data.length; i++) { const {uuid, packets} = packet.data[i]; for (let j = 0; j < packets.length; j++) { if (this._entities[uuid]) { this._entities[uuid].acceptPacket(packets[j]); } } } } if (packet instanceof SynchronizedCreatePacket) { Entity.loadOrInstance(packet.data.spec).then((entity) => { entity.hydrate().then(() => { this.addEntity(entity); }); }); } if (packet instanceof SynchronizedDestroyPacket) { const uuid = packet.data.synchronized.id; if (this._entities[uuid]) { this._entities[uuid].destroy(); } } } addEntity(entity) { const uuid = entity.instanceUuid; // Already exists? if (this._entities[uuid]) { return; } this._entities[uuid] = entity; this._flatEntities.push(entity); this._entityTickers.push(entity.tick); if (AVOCADO_SERVER) { this._informedEntities.set(entity, []); } entity.attachToList(this); entity.once('destroy', () => { this.removeEntity(entity); // In the process of destroying, allow entities to specify tickers that // must live on past destruction. const tickers = entity.invokeHookFlat('afterDestructionTickers'); for (let i = 0; i < tickers.length; i++) { const ticker = tickers[i]; this._afterDestructionTickers.push(ticker); } }); this.emit('entityAdded', entity); } cleanPackets() { for (let i = 0; i < this._flatEntities.length; i++) { this._flatEntities[i].cleanPackets(); } } destroy() { for (let i = 0; i < this._flatEntities.length; i++) { this._flatEntities[i].destroy(); } } findEntity(uuid) { if (uuid in this._entities) { return this._entities[uuid]; } } fromJSON(json) { for (let i = 0; i < json.length; i++) { const entityJSON = json[i]; if (entityJSON.uri) { Entity.read(entityJSON.uri).then((readJSON) => { this.addEntity(new Entity(readJSON, entityJSON)); }); } else { this.addEntity(new Entity(json[i])); } } } packets(informed) { const packets = []; // Visible entities. const {areaToInform} = informed; const previousVisibleEntities = this._informedEntities.get(informed); const visibleEntities = this.visibleEntities(areaToInform); const updates = []; for (let i = 0; i < visibleEntities.length; i++) { const entity = visibleEntities[i]; // Newly visible entity. const index = previousVisibleEntities.indexOf(entity); if (-1 === index) { packets.push(entity.createPacket(informed)); } // Still visible entity. else { const entityPackets = entity.packets(informed); if (entityPackets.length > 0) { updates.push({ uuid: entity.instanceUuid, packets: entityPackets, }); } previousVisibleEntities.splice(index, 1); } } // Send updates. this._informedEntities.set(informed, visibleEntities); if (updates.length > 0) { packets.push(new EntityListUpdateEntityPacket(updates)); } // Send destroys. for (let i = 0; i < previousVisibleEntities.length; i++) { const entity = previousVisibleEntities[i]; // Newly removed entity. if (-1 === visibleEntities.indexOf(entity)) { packets.push(entity.destroyPacket(informed)); } } return packets; } get quadTree() { return this._quadTree; } removeEntity(entity) { const uuid = entity.instanceUuid; if (!(uuid in this._entities)) { return; } if (AVOCADO_SERVER) { this._informedEntities.delete(entity); } entity.detachFromList(); delete this._entities[uuid]; this._flatEntities.splice(this._flatEntities.indexOf(entity), 1); this._entityTickers.splice(this._entityTickers.indexOf(entity.tick), 1); this.emit('entityRemoved', entity); } tick(elapsed) { // Run after destruction tickers. if (this._afterDestructionTickers.length > 0) { this.tickAfterDestructionTickers(elapsed); } // Run normal tickers. this.tickEntities(elapsed) } tickAfterDestructionTickers(elapsed) { const finishedTickers = []; for (let i = 0; i < this._afterDestructionTickers.length; ++i) { const ticker = this._afterDestructionTickers[i]; if (ticker(elapsed)) { finishedTickers.push(ticker); } } for (let i = 0; i < finishedTickers.length; ++i) { const ticker = finishedTickers[i]; const index = this._afterDestructionTickers.indexOf(ticker); this._afterDestructionTickers.splice(index, 1); } } tickEntities(elapsed) { for (let i = 0; i < this._entityTickers.length; i++) { this._entityTickers[i](elapsed); } } toNetwork(informed) { const {areaToInform} = informed; const visibleEntities = this.visibleEntities(areaToInform); // Mark as notified. this._informedEntities.set(informed, visibleEntities); return visibleEntities.map((entity) => { return entity.toNetwork(informed); }); } toJSON() { const json = []; for (let i = 0; i < this._flatEntities.length; i++) { const entity = this._flatEntities[i]; json.push(entity.mergeDiff(entity.toJSON())); } return json; } visibleEntities(query) { const entities = []; const entitiesChecked = []; const quadTree = this._quadTree; const nodes = quadTree.search(query); // Check all nodes. for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; const entity = node.data[2]; const aabb = node.data[3]; if (-1 === entitiesChecked.indexOf(entity)) { entitiesChecked.push(entity); // Make sure the AABB is actually in the query due to expansion. if (entity.isVisible && Rectangle.intersects(query, aabb)) { entities.push(entity); } } } return entities; } visibleEntitiesWithUri(query, uri) { return this.visibleEntities(query).filter((entity) => { return entity.uri === uri; }); } } export {EntityListView} from './view';