const ComponentRefs = require('./componentrefs');
const UUID = require('uuid/v1');
const CoreProperties = new Set([
'ecs', 'entity', 'type', '_values', '_ready', 'id',
'updated', 'constructor', 'stringify', 'clone', 'getObject'
]);
/**ECS base class of component
* @class BaseComponent
*/
class BaseComponent {
constructor(ecs, entity, initialValues) {
Object.defineProperty(this, 'ecs', { enumerable: false, value: ecs });
Object.defineProperty(this, 'entity', { enumerable: true, value: entity });
Object.defineProperty(this, 'type', { enumerable: false, value: this.constructor.name });
Object.defineProperty(this, '_values', { enumerable: false, value: {} });
Object.defineProperty(this, '_refs', { enumerable: false, value: {} });
Object.defineProperty(this, '_ready', { writable: true, enumerable: false, value: false });
Object.defineProperty(this, 'id', { enumerable: true, value: initialValues.id || UUID() });
Object.defineProperty(this, 'updated', { enumerable: false, writable: true, value: this.ecs.ticks });
//loop through inheritance by way of prototypes
//avoiding constructor->super() boilerplate for every component
//also avoiding proxies just for a simple setter on properties
const definitions = [];
for (var c = this.constructor; c !== null; c = Object.getPrototypeOf(c)) {
if (!c.definition) continue;
definitions.push(c.definition);
}
//we want to inherit deep prototype defintions first
definitions.reverse();
for (let idx = 0, l = definitions.length; idx < l; idx++) {
const definition = definitions[idx];
// set component properties from Component.properties
if (!definition.properties) {
continue;
}
const properties = definition.properties;
const keys = Object.keys(properties);
for (let idx = 0, l = keys.length; idx < l; idx++) {
const property = keys[idx];
if (CoreProperties.has(property)) {
throw new Error(`Cannot override property in Component definition: ${property}`);
}
const value = properties[property];
if (this._values.hasOwnProperty(property)) {
this[property] = value;
continue;
}
switch (value) {
case '<EntitySet>':
Object.defineProperty(this, property, {
//writable: true,
enumerable: true,
set: (value) => {
Reflect.set(this._values, property, ComponentRefs.EntitySet(value, this, property));
},
get: () => {
return Reflect.get(this._values, property);
}
});
//this._refs[property] = this[property];
this[property] = [];
break;
case '<EntityObject>':
Object.defineProperty(this, property, {
writable: false,
enumerable: true,
value: ComponentRefs.EntityObject({}, this, property)
});
this._refs[property] = this[property];
break;
case '<Entity>':
Object.defineProperty(this, property, {
enumerable: true,
writeable: true,
set: (value) => {
if (value && value.id) {
value = value.id;
}
const old = Reflect.get(this._values, property);
if (old && old !== value) {
this.ecs.deleteRef(old, this.entity.id, this.id, property);
}
if (value && value !== old) {
this.ecs.addRef(value, this.entity.id, this.id, property);
}
const result = Reflect.set(this._values, property, value);
this.ecs._sendChange(this, 'setEntity', property, old, value);
return result;
},
get: () => {
return this.ecs.getEntity(this._values[property]);
}
});
this._values[property] = null;
break;
case '<ComponentObject>':
Object.defineProperty(this, property, {
writable: false,
enumerable: true,
value: ComponentRefs.ComponentObject({}, this)
});
this._refs[property] = this[property];
break;
case '<ComponentSet>':
Object.defineProperty(this, property, {
//writable: true,
enumerable: true,
set: (value) => {
Reflect.set(this._values, property, ComponentRefs.ComponentSet(value, this, property));
},
get: () => {
return Reflect.get(this._values, property);
}
});
//this._refs[property] = this[property];
this[property] = [];
break;
case '<Component>':
Object.defineProperty(this, property, {
enumerable: true,
writeable: true,
set: (value) => {
if (typeof value === 'object') {
value = value.id;
}
const old = Reflect.get(this._values, property);
const result = Reflect.set(this._values, property, value);
this.ecs._sendChange(this, 'setComponent', property, old, value);
return result;
},
get: () => {
return this.entity.componentMap[this._values[property]];
}
});
this._values[property] = null;
break;
default:
let reflect = null;
if (typeof value === 'string' && value.startsWith('<Pointer ')) {
reflect = value.substring(9, value.length - 1).trim().split('.')
}
Object.defineProperty(this, property, {
enumerable: true,
writeable: true,
set: (value) => {
const old = Reflect.get(this._values, property, value);
const result = Reflect.set(this._values, property, value);
if (reflect) {
let node = this;
let fail = false;
for (let i = 0; i < reflect.length - 1; i++) {
const subprop = reflect[i];
/* $lab:coverage:off$ */
if (typeof node === 'object' && node !== null && node.hasOwnProperty(subprop)) {
/* $lab:coverage:on */
node = node[subprop];
} else {
fail = true;
}
}
if (!fail) {
Reflect.set(node, reflect[reflect.length - 1], value);
node = value;
}
}
this.ecs._sendChange(this, 'set', property, old, value);
return result;
},
get: () => {
if (!reflect) {
return Reflect.get(this._values, property);
}
let node = this;
let fail = false;
for (let i = 0; i < reflect.length - 1; i++) {
const subprop = reflect[i];
/* $lab:coverage:off$ */
if (typeof node === 'object' && node !== null && node.hasOwnProperty(subprop)) {
/* $lab:coverage:on */
node = node[subprop];
} else {
fail = true;
}
}
if (!fail) {
return Reflect.get(node, reflect[reflect.length - 1]);
} else {
return Reflect.get(this._values, property);
}
}
});
this._values[property] = value;
break;
}
}
}
// don't allow new properties
Object.seal(this);
Object.seal(this._values);
const values = { ...initialValues };
delete values.type;
delete values.entity;
delete values.id;
Object.assign(this, values);
this.ecs._sendChange(this, 'addComponent');
this._ready = true;
}
stringify() {
return JSON.stringify(this.getObject());
}
getObject() {
const serialize = this.constructor.definition.serialize;
let values = this._values;
if (serialize) {
/* $lab:coverage:off$ */
if (serialize.skip) return undefined;
/* $lab:coverage:on$ */
if (serialize.ignore.length > 0) {
values = {}
const props = new Set([...serialize.ignore]);
for (const prop of Object.keys(this._values).filter(prop => !props.has(prop))) {
values[prop] = this._values[prop];
}
}
}
return Object.assign({ id: this.id, type: this.type }, values, this._refs);
}
}
BaseComponent.definition = {
properties: {
},
multiset: false,
serialize: {
skip: false,
ignore: [],
}
};
module.exports = BaseComponent;