silphius/app/ecs/component.js
2024-08-01 13:23:23 -05:00

202 lines
4.8 KiB
JavaScript

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();
}
async create(entityId, values) {
const [created] = await this.createMany([[entityId, values]]);
return created;
}
async createMany(entries) {
if (0 === entries.length) {
return [];
}
let {length} = entries;
const allocated = [];
while (length > 0) {
allocated.push(
this.pool.length > 0
? this.pool.pop()
: new this.Instance(),
)
length -= 1;
}
const promises = [];
for (const [entityId, values] of entries) {
const instance = allocated.pop();
this.instances[entityId] = instance;
instance.entity = entityId;
for (const key in values) {
instance[key] = values[key];
}
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([entityId]);
}
destroyMany(entityIds) {
this.freeMany(
entityIds
.map((entityId) => {
if ('undefined' !== typeof this.instances[entityId]) {
return this.instances[entityId];
}
throw new Error(`can't free for non-existent id ${entityId}`);
}),
);
for (const entityId of entityIds) {
this.instances[entityId].destroy();
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;
}
freeMany(instances) {
this.pool.push(...instances);
}
get(entityId) {
return this.instances[entityId];
}
async insertMany(entities) {
await this.createMany(entities);
}
instanceFromSchema() {
const Component = this;
const {concrete} = Component.constructor.schema.specification;
const Schema = Component.constructor.schema.constructor;
const Instance = class {
$$entity = 0;
constructor() {
this.$$reset();
}
$$reset() {
for (const key in concrete.properties) {
this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]);
}
}
destroy() {}
toNet(recipient, data) {
return data || Component.constructor.filterDefaults(this);
}
toJSON() {
return Component.constructor.filterDefaults(this);
}
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;
this.$$reset();
},
};
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);
}
}