315 lines
7.7 KiB
JavaScript
315 lines
7.7 KiB
JavaScript
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;
|
|
}
|
|
|
|
get isDirty() {
|
|
return this._isDirty;
|
|
}
|
|
|
|
set isDirty(isDirty) {
|
|
this._isDirty = isDirty;
|
|
}
|
|
|
|
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';
|