import * as I from 'immutable'; import D from 'debug'; import without from 'lodash.without'; import {compose, EventEmitter} from '@avocado/core'; import {Resource} from '@avocado/resource'; import {Synchronized} from '@avocado/state'; import {hasTrait, lookupTrait} from './trait/registry'; const debug = D('@avocado:entity:traits'); function copyTraitProperties(from, to, instance) { do { Object.getOwnPropertyNames(from).forEach((property) => { // Nooope. switch (property) { case 'state': return; } // Make sure it's actually a property. let descriptor = Object.getOwnPropertyDescriptor(from, property); if (!descriptor.get && !descriptor.set) { return; } // Bind properties to trait instance. if (descriptor.get) { descriptor.get = descriptor.get.bind(instance); } if (descriptor.set) { descriptor.set = descriptor.set.bind(instance); } Object.defineProperty(to, property, descriptor); }); } while (Object.prototype !== (from = Object.getPrototypeOf(from))); } function enumerateTraitProperties(prototype) { const keys = []; do { Object.getOwnPropertyNames(prototype).forEach((property) => { let descriptor = Object.getOwnPropertyDescriptor(prototype, property); if (!descriptor.get && !descriptor.set) { return; } keys.push(property); }); } while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype))); return keys; } const decorate = compose( EventEmitter, Synchronized, ); export class Entity extends decorate(Resource) { constructor() { super(); this._hooks = {}; this.isDirty = true; this._traits = {}; this.once('destroyed', () => { this.removeAllTraits(); }); } acceptPacket(packet) { for (const type in this._traits) { const instance = this._traits[type]; instance.acceptPacket(packet); } } addTrait(type, json = {}) { if (this.is(type)) { debug(`Tried to add trait "${type}" when it already exists!`); return; } if (!hasTrait(type)) { debug(`Tried to add trait "${type}" which isn't registered!`); return; } const Trait = lookupTrait(type); // Ensure dependencies. const dependencies = Trait.dependencies(); const allTypes = this.allTraitTypes(); const lacking = without(dependencies, ...allTypes); if (lacking.length > 0) { debug( `Tried to add trait "${type}" but lack one or more dependents: "${ lacking.join('", "') }"!` ); return; } // Instantiate. const {params, state} = json; const instance = new Trait(this, params, state); // Proxy properties. copyTraitProperties(Trait.prototype, this, instance); // Let the Trait do its initialization. instance.initialize(); // Attach listeners. const listeners = instance.memoizedListeners(); for (const eventName in listeners) { this.on(eventName, listeners[eventName]); } // Proxy methods. const methods = instance.methods(); for (const key in methods) { this[key] = methods[key]; } // Register hook listeners. const hooks = instance.hooks(); for (const key in hooks) { this._hooks[key] = this._hooks[key] || []; this._hooks[key].push({ fn: hooks[key], type: Trait.type(), }); } // Add state. this.state = this._setInstanceState(this.state, type, instance); // Track trait. this._traits[type] = instance; this.emit('traitAdded', type, instance); } addTraits(traits) { for (const type in traits) { this.addTrait(type, traits[type]); } } allTraitInstances() { return this._traits; } allTraitTypes() { return Object.keys(this._traits); } fromJSON(json) { super.fromJSON(json); this.addTraits(json.traits); return this; } hydrate() { const promises = []; for (const type in this._traits) { const instance = this._traits[type]; promises.push(instance.hydrate()); } return Promise.all(promises); } invokeHook(hook, ...args) { const results = {}; if (!(hook in this._hooks)) { return results; } for (const {fn, type} of this._hooks[hook]) { results[type] = fn(...args); } return results; } invokeHookFlat(hook, ...args) { const invokeResults = this.invokeHook(hook, ...args); const results = []; for (const type in invokeResults) { results.push(invokeResults[type]); } return results; } is(type) { return type in this._traits; } patchStateStep(type, step) { let instance = this._traits[type]; // New trait requested? if (!this._traits[type]) { // Doesn't exist? if (!hasTrait(type)) { return; } this.addTrait(type, step.value); instance = this._traits[type]; } else { // Accept state. instance.patchState([step]); } this.state = this._setInstanceState(this.state, type, instance); } renderTick(elapsed) { for (const type in this._traits) { const instance = this._traits[type]; instance.renderTick(elapsed); } } removeAllTraits() { const types = this.allTraitTypes(); this.removeTraits(types); } removeTrait(type) { if (!this.is(type)) { debug(`Tried to remove trait "${type}" when it doesn't exist!`); return; } // Destroy instance. const instance = this._traits[type]; instance.destroy(); // Remove methods, hooks, and properties. const methods = instance.methods(); for (const key in methods) { delete this[key]; } const hooks = instance.hooks(); for (const key in hooks) { delete this._hooks[key]; } const Trait = lookupTrait(type); const properties = enumerateTraitProperties(Trait.prototype); for (let i = 0; i < properties.length; ++i) { const property = properties[i]; delete this[property]; } // Remove all event listeners. const listeners = instance.memoizedListeners(); for (const eventName in listeners) { this.off(eventName, listeners[eventName]); } // Remove state. this.state = this.state.delete(type); // Remove instance. delete this._traits[type]; // Unloop. // instance.entity = undefined; } removeTraits(types) { types.forEach((type) => this.removeTrait(type)); } _setInstanceState(state, type, instance) { let map = state.has(type) ? state.get(type) : I.Map(); map = map.set('state', instance.state); map = map.set('params', instance.params); return state.set(type, map); } tick(elapsed) { for (const type in this._traits) { const instance = this._traits[type]; instance.tick(elapsed); } if (AVOCADO_SERVER) { this.state = this.state.withMutations((state) => { for (const type in this._traits) { const instance = this._traits[type]; if (!instance.isDirty) { continue; } instance.isDirty = false; this.isDirty = true; this._setInstanceState(state, type, instance); } }); } } toJSON() { const json = {}; for (const type in this._traits) { json[type] = this._traits[type].toJSON(); } return { ...super.toJSON(), traits: json, }; } } export {EntityPacket} from './entity.packet'; export {EntityList, EntityListView} from './list'; export {EntityPacketSynchronizer} from './packet-synchronizer'; export { hasTrait, lookupTrait, registerTrait, } from './trait/registry'; export {StateProperty, Trait} from './trait';