import Schema from './schema.js'; export default class Component { ecs; Instance; instances = {}; pool = []; static properties = {}; static $$schema; constructor(ecs) { this.ecs = ecs; this.Instance = this.instanceFromSchema(); } allocateMany(count) { const results = []; while (count > 0) { results.push( this.pool.length > 0 ? this.pool.pop() : new this.Instance(), ) count -= 1; } return results; } async create(entityId, values) { const [created] = await this.createMany([[entityId, values]]); return created; } async createMany(entries) { if (0 === entries.length) { return []; } const allocated = this.allocateMany(entries.length); const { constructor: Schema, specification: {concrete: {properties}}, } = this.constructor.schema; const defaults = {}; for (const key in properties) { defaults[key] = ((key) => () => Schema.defaultValue(properties[key]))(key); switch (properties[key].type) { case 'float32': case 'float64': case 'int8': case 'int16': case 'int32': case 'int64': case 'string': case 'uint8': case 'uint16': case 'uint32': case 'uint64': { defaults[key] = defaults[key](); break; } } } const promises = []; for (let i = 0; i < entries.length; ++i) { const [entityId, values] = entries[i]; const instance = allocated[i]; instance.entity = entityId; this.instances[entityId] = instance; const keys = new Set(Object.keys(defaults)); for (const key in values) { keys.delete(key); } const defaultValues = {}; for (const key of keys) { defaultValues[key] = 'function' === typeof defaults[key] ? defaults[key]() : defaults[key]; } instance.initialize(values, defaultValues); promises.push(this.load(instance)); } await Promise.all(promises); return allocated; } deserialize(entityId, view, offset) { const {properties} = this.constructor.schema.specification.concrete; const instance = this.get(entityId); const deserialized = this.constructor.schema.deserialize(view, offset); for (const key in properties) { instance[key] = deserialized[key]; } } destroy(entityId) { this.destroyMany(new Set([entityId])); } destroyMany(entityIds) { for (const entityId of entityIds) { const instance = this.instances[entityId]; if ('undefined' === typeof instance) { throw new Error(`can't free for non-existent id ${entityId}`); } instance.destroy(); this.pool.push(instance); delete this.instances[entityId]; } } static filterDefaults(instance) { const {properties} = this.schema.specification.concrete; const Schema = this.schema.constructor; const json = {}; for (const key in properties) { if (key in instance && instance[key] !== Schema.defaultValue(properties[key])) { json[key] = instance[key]; } } return json; } get(entityId) { return this.instances[entityId]; } async insertMany(entities) { await this.createMany(entities); } instanceFromSchema() { const Component = this; const {concrete} = Component.constructor.schema.specification; const Instance = class { $$entity = 0; destroy() {} initialize(values, defaults) { for (const key in values) { this[`$$${key}`] = values[key]; } for (const key in defaults) { this[`$$${key}`] = defaults[key]; } Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values}) } toFullJSON() { const {properties} = concrete; const json = {}; for (const key in properties) { json[key] = this[key]; } return json; } toNet(recipient, data) { if (data) { return data; } return this.toFullJSON(); } toJSON() { return this.toFullJSON(); } async update(values) { for (const key in values) { if (concrete.properties[key]) { this[`$$${key}`] = values[key]; } else { this[key] = values[key]; } } } }; const properties = {}; properties.entity = { get: function get() { return this.$$entity; }, set: function set(v) { this.$$entity = v; }, }; for (const key in concrete.properties) { properties[key] = { get: function get() { return this[`$$${key}`]; }, set: function set(value) { if (this[`$$${key}`] !== value) { this[`$$${key}`] = value; Component.markChange(this.entity, key, value); } }, }; } Object.defineProperties(Instance.prototype, properties); return Instance; } async load(instance) { return instance; } markChange(entityId, key, value) { this.ecs.markChange(entityId, {[this.constructor.componentName]: {[key]: value}}) } mergeDiff(original, update) { return {...original, ...update}; } static get schema() { if (!this.$$schema) { this.$$schema = new Schema({ type: 'object', properties: this.properties, }); } return this.$$schema; } serialize(entityId, view, offset) { this.constructor.schema.serialize(this.get(entityId), view, offset); } sizeOf(entityId) { return this.constructor.schema.sizeOf(this.get(entityId)); } async updateMany(entities) { const promises = []; for (let i = 0; i < entities.length; i++) { const [entityId, values] = entities[i]; promises.push(this.get(entityId).update(values)); } return Promise.all(promises); } }