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 {StateSynchronizer} from '@avocado/state'; import {create} from './index'; const decorate = compose( EventEmitter, ); export class EntityList extends decorate(class {}) { constructor() { super(); this.entities_PRIVATE = {}; this._quadTree = new QuadTree(); this.state_PRIVATE = I.Map(); this.uuidMap_PRIVATE = {}; } *[Symbol.iterator]() { for (const uuid in this.entities_PRIVATE) { const entity = this.entities_PRIVATE[uuid]; yield entity; } } addEntity(entity) { const uuid = entity.instanceUuid; this.entities_PRIVATE[uuid] = entity; this.state_PRIVATE = this.state_PRIVATE.set(uuid, entity.state); entity.addTrait('listed'); entity.list = this; entity.once('destroyed', () => { this.removeEntity(entity); }); this.emit('entityAdded', entity); } destroy() { for (const entity of this) { entity.destroy(); } } findEntity(uuid) { if (this.uuidMap_PRIVATE[uuid]) { return this.entities_PRIVATE[this.uuidMap_PRIVATE[uuid]]; } if (this.entities_PRIVATE[uuid]) { return this.entities_PRIVATE[uuid]; } } patchState(patch) { for (const step of patch) { const {op, path, value} = step; if ('/' === path) { for (const uuid in value) { const localUuid = this.uuidMap_PRIVATE[uuid]; const entity = this.entities_PRIVATE[localUuid]; if (entity) { if (false === value[uuid]) { // Entity removed. this.removeEntity(entity); } else { entity.patchState([ { op: 'replace', path: '/', value: value[uuid], } ]); this.state_PRIVATE = this.state_PRIVATE.set(localUuid, entity.state); } } else { // New entity. Create with patch as traits. const newEntity = create().fromJSON({ traits: value[uuid], }); this.uuidMap_PRIVATE[uuid] = newEntity.instanceUuid; this.addEntity(newEntity); } } } else { const [uuid, substep] = StateSynchronizer.forwardStep(step); const localUuid = this.uuidMap_PRIVATE[uuid]; const entity = this.entities_PRIVATE[localUuid]; if (entity) { if (false === substep.value[uuid]) { // Entity removed. this.removeEntity(entity); } else { entity.patchState([substep]); this.state_PRIVATE = this.state_PRIVATE.set(localUuid, entity.state); } } else { // New entity. Create with patch as traits. const newEntity = create().fromJSON({ traits: substep.value[uuid], }); this.uuidMap_PRIVATE[uuid] = newEntity.instanceUuid; this.addEntity(newEntity); } } } } get quadTree() { return this._quadTree; } recomputeState() { for (const uuid in this.entities_PRIVATE) { const entity = this.entities_PRIVATE[uuid]; this.state_PRIVATE = this.state_PRIVATE.set(uuid, entity.state); } } removeEntity(entity) { const uuid = entity.instanceUuid; delete this.entities_PRIVATE[uuid]; this.state_PRIVATE = this.state_PRIVATE.delete(uuid); this.emit('entityRemoved', entity); if (entity.is('listed')) { entity.removeTrait('listed'); } } get state() { return this.state_PRIVATE; } tick(elapsed) { for (const uuid in this.entities_PRIVATE) { const entity = this.entities_PRIVATE[uuid]; if ('tick' in entity) { entity.tick(elapsed); } } this.recomputeState(); } visibleEntities(query) { const quadTree = this._quadTree; const entities = quadTree.search(query).map((node) => { return node.data[2]; }); // Hitting multiple points for each entity can return duplicates. return arrayUnique(entities); } }