import * as I from 'immutable'; import mapValues from 'lodash.mapvalues'; import {arrayUnique, compose} from '@avocado/core'; import {QuadTree, Rectangle, Vector} from '@avocado/math'; import {EventEmitter} from '@avocado/mixins'; 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(); this._uuidMap = {}; } *[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._uuidMap[uuid]) { return this._entities[this._uuidMap[uuid]]; } if (this._entities[uuid]) { return this._entities[uuid]; } } patchStateStep(uuid, step) { const localUuid = this._uuidMap[uuid]; const entity = this._entities[localUuid]; if ('remove' === step.op) { // Maybe already destroyed on the client? if (entity && 'destroy' in entity) { entity.destroy(); } } if (entity) { // Exists; patch. entity.patchState([step]); } else { // We got an update for an entity we've already destroyed. Discard! if (localUuid) { return; } // New entity. Create with patch as traits. const newEntity = (new Entity()).fromJSON({ traits: step.value, }); this._uuidMap[uuid] = newEntity.instanceUuid; this.addEntity(newEntity); } } 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. 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 quadTree = this._quadTree; const nodes = quadTree.search(query); for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; const entity = node.data[2]; // Make sure they're actually in the query due to expanded AABB. if (!Rectangle.intersects(query, entity.visibleBoundingBox)) { continue; } entities.push(entity); } // Hitting multiple points for each entity can return duplicates. return arrayUnique(entities); } } export {EntityListView} from './view';