import * as I from 'immutable'; import mapValues from 'lodash.mapvalues'; import {compose, EventEmitter} from '@avocado/core'; import {QuadTree, Rectangle, Vector} from '@avocado/math'; import {Synchronized} from '@avocado/state'; import {Entity} from '../index'; const decorate = compose( EventEmitter, Synchronized, ); export class EntityList extends decorate(class {}) { constructor() { super(); this._afterDestructionTickers = []; this._entities = {}; this._quadTree = new QuadTree(); } *[Symbol.iterator]() { for (const uuid in this._entities) { const entity = this._entities[uuid]; yield entity; } } addEntity(entity) { const uuid = entity.instanceUuid; this._entities[uuid] = entity; this.state = this.state.set(uuid, entity.state); entity.addTrait('listed'); entity.list = 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'); this._afterDestructionTickers.push(...tickers); }); this.emit('entityAdded', entity); } destroy() { for (const entity of this) { entity.destroy(); } } findEntity(uuid) { if (this._entities[uuid]) { return this._entities[uuid]; } } patchStateStep(uuid, step) { const entity = this._entities[uuid]; if ('/' === step.path) { switch (step.op) { case 'add': // New entity. Create with patch as traits. const newEntity = (new Entity()).fromJSON({ traits: step.value, }); newEntity.instanceUuid = uuid; this.addEntity(newEntity); break; case 'remove': // Maybe already destroyed on the client? if (entity && entity.is('existent')) { entity.destroy(); } break; } return; } if ('replace' === step.op && entity) { // Exists; patch. entity.patchState([step]); } } get quadTree() { return this._quadTree; } removeEntity(entity) { const uuid = entity.instanceUuid; delete this._entities[uuid]; this.state = this.state.delete(uuid); this.emit('entityRemoved', entity); if (entity.is('listed')) { entity.removeTrait('listed'); } } tick(elapsed) { // Run after destruction tickers. 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); } // Run normal tickers. for (const uuid in this._entities) { const entity = this._entities[uuid]; entity.tick(elapsed); } // Update state. if (AVOCADO_SERVER) { this.state = this.state.withMutations((state) => { for (const uuid in this._entities) { const entity = this._entities[uuid]; if (!entity.isDirty) { continue; } entity.isDirty = false; state.set(uuid, entity.state); } }); } } visibleEntities(query) { const entities = []; const entitiesTrack = []; const quadTree = this._quadTree; const nodes = quadTree.search(query); // First, uniqueify. for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; const entity = node.data[2]; if (-1 === entitiesTrack.indexOf(entity)) { entitiesTrack.push(entity); } } for (let i = 0; i < entitiesTrack.length; i++) { const entity = entitiesTrack[i]; // Make sure they're actually in the query due to expanded AABB. const visibleAabb = entity.visibleAabb; if (!Rectangle.intersects(query, visibleAabb)) { continue; } entities.push(entity); } // Hitting multiple points for each entity can return duplicates. return entities; } } export {EntityListView} from './view';