avocado-old/packages/entity/traits.js

285 lines
7.0 KiB
JavaScript
Raw Normal View History

2019-03-17 23:45:48 -05:00
const debug = require('debug')('@avocado/entity:traits');
import without from 'lodash.without';
import * as I from 'immutable';
import {Resource} from '@avocado/resource';
2019-04-07 11:43:50 -05:00
import {Synchronized} from '@avocado/state';
2019-03-17 23:45:48 -05:00
import {hasTrait, lookupTrait, registerTrait} from './trait-registry';
2019-03-17 23:45:48 -05:00
function enumerateProperties(prototype) {
const result = {};
do {
Object.getOwnPropertyNames(prototype).forEach((property) => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
if (typeof descriptor.get === 'function') {
result[property] = result[property] || {};
result[property].get = true;
}
if (typeof descriptor.set === 'function') {
result[property] = result[property] || {};
result[property].set = true;
}
});
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
return result;
}
2019-04-07 11:43:50 -05:00
export class Traits extends Synchronized {
2019-03-17 23:45:48 -05:00
constructor(entity) {
2019-04-07 11:43:50 -05:00
super();
2019-04-07 12:00:11 -05:00
this._methods = {};
this._entity = entity;
this._hooks = {};
this._properties = {};
this._traits = {};
2019-03-21 00:09:17 -05:00
entity.once('destroyed', () => {
2019-03-20 23:23:34 -05:00
this.removeAllTraits();
2019-03-21 00:09:17 -05:00
});
2019-03-17 23:45:48 -05:00
}
allInstances() {
2019-04-07 12:00:11 -05:00
return this._traits;
2019-03-17 23:45:48 -05:00
}
allTypes() {
2019-04-07 12:00:11 -05:00
return Object.keys(this._traits);
2019-03-17 23:45:48 -05:00
}
2019-03-20 23:00:42 -05:00
addTrait(type, json = {}) {
2019-03-17 23:45:48 -05:00
if (this.hasTrait(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.allTypes();
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;
2019-04-07 12:00:11 -05:00
const instance = new Trait(this._entity, params, state);
// Proxy properties.
const properties = enumerateProperties(Trait.prototype);
for (const key in properties) {
properties[key].instance = instance;
2019-04-07 12:00:11 -05:00
this._properties[key] = properties[key];
}
// Let the Trait do its initialization.
instance.initialize();
// Attach listeners.
const listeners = instance.listeners();
for (const eventName in listeners) {
2019-04-07 12:00:11 -05:00
this._entity.on(
`${eventName}.trait-${type}`,
listeners[eventName]
);
}
2019-03-20 18:32:54 -05:00
// Proxy methods.
const methods = instance.methods();
for (const key in methods) {
2019-04-07 12:00:11 -05:00
this._methods[key] = methods[key];
2019-03-17 23:45:48 -05:00
}
// Register hook listeners.
const hooks = instance.hooks();
for (const key in hooks) {
2019-04-07 12:00:11 -05:00
this._hooks[key] = this._hooks[key] || [];
this._hooks[key].push({
2019-03-17 23:45:48 -05:00
fn: hooks[key],
type: Trait.type(),
});
}
// Add state.
2019-03-18 21:22:54 -05:00
this._setInstanceState(type, instance);
2019-03-17 23:45:48 -05:00
// Track trait.
2019-04-07 12:00:11 -05:00
this._traits[type] = instance;
this._entity.emit('traitAdded', type, instance);
2019-03-17 23:45:48 -05:00
}
addTraits(traits) {
const allTypes = this.allTypes();
for (const type in traits) {
this.addTrait(type, traits[type]);
}
}
fromJSON(json) {
this.addTraits(json);
return this;
}
getProperty(property) {
2019-04-07 12:00:11 -05:00
if (property in this._methods) {
return this._methods[property];
2019-03-17 23:45:48 -05:00
}
2019-04-07 12:00:11 -05:00
if (property in this._properties) {
const instance = this._properties[property].instance;
if (!this._properties[property].get) {
2019-03-17 23:45:48 -05:00
const type = instance.constructor.type();
throw new ReferenceError(
`Property '${property}' from Trait '${type}' has no getter`
);
}
return instance[property];
}
}
hasProperty(property) {
2019-04-07 12:00:11 -05:00
if (property in this._methods) {
2019-03-17 23:45:48 -05:00
return true;
}
2019-04-07 12:00:11 -05:00
if (property in this._properties) {
2019-03-17 23:45:48 -05:00
return true;
}
return false;
}
hasTrait(type) {
2019-04-07 12:00:11 -05:00
return type in this._traits;
2019-03-17 23:45:48 -05:00
}
hydrate() {
const promises = [];
2019-04-07 12:00:11 -05:00
for (const type in this._traits) {
const instance = this._traits[type];
2019-03-17 23:45:48 -05:00
promises.push(instance.hydrate());
}
return Promise.all(promises);
}
instance(type) {
2019-04-07 12:00:11 -05:00
return this._traits[type];
2019-03-17 23:45:48 -05:00
}
invokeHook(hook, ...args) {
const results = {};
2019-04-07 12:00:11 -05:00
if (!(hook in this._hooks)) {
2019-03-17 23:45:48 -05:00
return results;
}
2019-04-07 12:00:11 -05:00
for (const {fn, type} of this._hooks[hook]) {
2019-03-17 23:45:48 -05:00
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;
}
2019-04-07 11:43:50 -05:00
patchStateStep(type, step) {
2019-04-07 12:00:11 -05:00
let instance = this._traits[type];
2019-04-07 11:43:50 -05:00
// New trait requested?
2019-04-07 12:00:11 -05:00
if (!this._traits[type]) {
2019-04-07 11:43:50 -05:00
// Doesn't exist?
if (!hasTrait(type)) {
return;
2019-04-06 23:19:32 -05:00
}
2019-04-07 11:43:50 -05:00
this.addTrait(type, step.value);
2019-04-07 12:00:11 -05:00
instance = this._traits[type];
2019-04-06 23:19:32 -05:00
}
2019-04-07 11:43:50 -05:00
else {
// Accept state.
instance.patchState([step]);
2019-04-05 15:16:55 -05:00
}
2019-04-07 11:43:50 -05:00
this._setInstanceState(type, instance);
2019-04-05 15:16:55 -05:00
}
2019-03-17 23:45:48 -05:00
removeAllTraits() {
const types = this.allTypes();
this.removeTraits(types);
}
removeTrait(type) {
if (!this.hasTrait(type)) {
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
return;
}
2019-04-07 12:00:11 -05:00
const instance = this._traits[type];
instance.destroy();
2019-03-17 23:45:48 -05:00
2019-03-20 18:32:54 -05:00
const methods = instance.methods();
for (const key in methods) {
2019-04-07 12:00:11 -05:00
delete this._methods[key];
2019-03-17 23:45:48 -05:00
}
const hooks = instance.hooks();
for (const key in hooks) {
2019-04-07 12:00:11 -05:00
delete this._hooks[key];
2019-03-17 23:45:48 -05:00
}
const Trait = lookupTrait(type);
const properties = enumerateProperties(Trait.prototype);
for (const key in properties) {
2019-04-07 12:00:11 -05:00
delete this._properties[key];
2019-03-17 23:45:48 -05:00
}
2019-04-07 12:00:11 -05:00
this._entity.off(`.trait-${type}`);
2019-04-07 11:43:50 -05:00
this.state = this.state.delete(type);
2019-04-07 12:00:11 -05:00
delete this._traits[type];
2019-03-17 23:45:48 -05:00
}
removeTraits(types) {
types.forEach((type) => this.removeTrait(type));
}
2019-03-18 21:22:54 -05:00
_setInstanceState(type, instance) {
2019-04-07 11:43:50 -05:00
const state = this.state;
2019-03-18 21:22:54 -05:00
let map = state.has(type) ? state.get(type) : I.Map();
map = map.set('state', instance.state);
map = map.set('params', instance.params);
2019-04-07 11:43:50 -05:00
this.state = this.state.set(type, map);
2019-03-18 21:22:54 -05:00
}
2019-03-17 23:45:48 -05:00
setProperty(property, value, receiver) {
2019-04-07 12:00:11 -05:00
if (property in this._properties) {
const instance = this._properties[property].instance;
2019-03-17 23:45:48 -05:00
const type = instance.constructor.type();
2019-04-07 12:00:11 -05:00
if (!this._properties[property].set) {
2019-03-17 23:45:48 -05:00
throw new ReferenceError(
`Property '${property}' from Trait '${type}' has no setter`
);
}
instance[property] = value;
2019-03-18 21:22:54 -05:00
this._setInstanceState(type, instance);
2019-03-17 23:45:48 -05:00
return true;
}
return false;
}
tick(elapsed) {
for (const type in this._traits) {
const instance = this._traits[type];
instance.tick(elapsed);
}
}
2019-03-17 23:45:48 -05:00
toJSON() {
const json = {};
2019-04-07 12:00:11 -05:00
for (const type in this._traits) {
json[type] = this._traits[type].toJSON();
2019-03-17 23:45:48 -05:00
}
return json;
}
}