import * as I from 'immutable'; import immutablediff from 'immutablediff'; import isPlainObject from 'is-plain-object'; import {compose} from '@avocado/core'; import {Trait} from '@avocado/entity'; import {Packer, Synchronizer} from '@avocado/state'; import {KeysPacket, StatePacket} from '../common/packet'; const decorate = compose( ); export class Informed extends decorate(Trait) { initialize() { this._packer = new Packer(); this._rememberedEntities = {}; this._socket = undefined; this._state = I.Map(); } destroy() { if (this._socket) { delete this._socket.entity; delete this._socket; } } reduceState(state) { // Set client's self entity. state = state.set('selfEntity', this.entity.instanceUuid); // Reduce entity list to visible. const room = this.entity.room; const camera = this.entity.camera; for (const {index, layer} of room.layers) { const visibleEntities = layer.visibleEntities( camera.rectangle ); let reducedEntityList = I.Map(); for (const entity of visibleEntities) { reducedEntityList = reducedEntityList.set( entity.instanceUuid, entity.state, ); } const entityListPath = ['room', 'layers', index, 'entityList']; state = state.setIn(entityListPath, reducedEntityList); } return state; } get socket() { return this._socket; } set socket(socket) { socket.entity = this.entity; this._socket = socket; } methods() { return { inform: (state) => { // Reduce state. const reducedState = this.reduceState(state); // Take a pure JS diff. const steps = immutablediff(this._state, reducedState).toJS(); // Rewrite entity removals. const entityRemovals = steps.filter((step) => { return 'remove' === step.op && step.path.match( /\/room\/layers\/.*\/entityList\/[a-z0-9-]+/ ); }); for (const entityRemoval of entityRemovals) { // Remember the entity. const parts = entityRemoval.path.split('/'); parts.shift(); const uuid = parts[4]; this._rememberedEntities[uuid] = state.getIn(parts); // Swap out the removal op for replaces to hide the entity. const index = steps.indexOf(entityRemoval); steps.splice(index, 1); const pathOverrides = [ ['existent', 'state', 'isTicking'], ['physical', 'state', 'addedToPhysics'], ['graphical', 'state', 'isVisible'], ] for (const pathOverride of pathOverrides) { steps.push({ op: 'replace', path: entityRemoval.path + '/' + pathOverride.join('/'), value: false, }); } } // Rewrite entity adds. const entityAdds = steps.filter((step) => { return 'add' === step.op && step.path.match( /\/room\/layers\/.*\/entityList\/[a-z0-9-]+/ ); }); for (const entityAdd of entityAdds) { // Remember the entity. const parts = entityAdd.path.split('/'); parts.shift(); const uuid = parts[4]; if (!(uuid in this._rememberedEntities)) { continue; } const rememberedEntity = this._rememberedEntities[uuid]; const currentState = state.getIn(parts); const addSteps = immutablediff( rememberedEntity, currentState, ).toJS(); const fullAddSteps = addSteps.map((addStep) => { return { op: addStep.op, path: entityAdd.path + addStep.path, value: addStep.value, }; }); // Swap out the add op for replaces to show the entity. const index = steps.indexOf(entityAdd); steps.splice(index, 1); steps.push(...fullAddSteps); const pathOverrides = [ ['existent', 'state', 'isTicking'], ['physical', 'state', 'addedToPhysics'], ['graphical', 'state', 'isVisible'], ] for (const pathOverride of pathOverrides) { steps.push({ op: 'replace', path: entityAdd.path + '/' + pathOverride.join('/'), value: rememberedEntity.getIn(pathOverride), }); } delete this._rememberedEntities[uuid]; } // Remember state. this._state = reducedState; if (0 === steps.length) { return; } // Emit! const keys = this._packer.computeNewKeys(steps); if (0 !== keys[0].length) { this._socket.send(new KeysPacket(keys)); } const packed = this._packer.pack(steps); this._socket.send(new StatePacket(packed)); }, }; } }