/*
ecsjs is an entity component system library for JavaScript
Copyright (C) 2014 Peter Flannery
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import {
ComponentMapKey,
type Iterator
} from './types.js';
/**
* Component map for storing entity ids and related component data
* @category Maps
*/
export class ComponentMap implements Iterable<[number, TComponentInstance]> {
private indices: number[] = [];
private entities: number[] = [];
private instances: TComponentInstance[] = [];
/**
* Creates a new component map
* @param entries - Optional initial data for the map
*/
constructor(entries?: readonly (readonly [number, TComponentInstance])[] | null) {
if (entries) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
this.set(entry[0], entry[1]);
}
}
}
/**
* Returns the number of entities in the map
*/
get size() {
return this.instances.length;
}
/**
* Clears all component data from the map
*/
clear() {
this.indices = [];
this.entities.length = 0;
this.instances.length = 0;
}
/**
* Adds or updates component data for an entity
* @param entityId - The entity id to set
* @param component - The component data to set
*/
set(entityId: number, component: TComponentInstance) {
if (this.has(entityId)) {
this.instances[this.indices[entityId]] = component;
return;
}
this.indices[entityId] = this.instances.length;
this.entities.push(entityId);
this.instances.push(component);
}
/**
* Gets the component data for an entity
* @param entityId - The entity id to get
* @returns The component data, or undefined if not found
*/
get(entityId: number) {
const index = this.indices[entityId];
return index !== undefined && this.entities[index] === entityId
? this.instances[index]
: undefined;
}
/**
* Removes component data for an entity
* @param entityId - The entity id to remove
* @returns True if the component was removed, otherwise false
*/
delete(entityId: number) {
const index = this.indices[entityId];
if (index === undefined || index === -1 || this.entities[index] !== entityId) return false;
// swap and pop with last element to keep dense
const lastIdx = this.instances.length - 1;
const lastEntity = this.entities[lastIdx];
this.instances[index] = this.instances[lastIdx];
this.entities[index] = lastEntity;
this.indices[lastEntity] = index;
this.instances.pop();
this.entities.pop();
// invalidate index
this.indices[entityId] = -1;
return true;
}
/**
* Checks if an entity has a component in this map
* @param entityId - The entity id to check
* @returns True if the entity has a component, otherwise false
*/
has(entityId: number) {
const index = this.indices[entityId];
return index !== undefined && index !== -1 && this.entities[index] === entityId;
}
/**
* Executes a callback for every map, providing the component data and its entity id
* @param callback - The callback to execute
*/
forEach(callback: (value: TComponentInstance, key: number) => void) {
for (let i = 0; i < this.instances.length; i++) {
callback(this.instances[i], this.entities[i]);
}
}
/**
* Returns the first entry
* @returns A tuple of [entityId, componentData], or undefined if empty
*/
firstEntry(): [entityId: number, value: TComponentInstance] | undefined {
return this.entities.length === 0
? undefined
: [this.entities[0], this.instances[0]];
}
/**
* Returns the first entity id
* @returns The first entity id, or undefined if empty
*/
firstKey(): number | undefined {
return this.entities.length === 0
? undefined
: this.entities[0];
}
/**
* Returns the first entity data
* @returns The first component data, or undefined if empty
*/
firstValue(): TComponentInstance | undefined {
return this.instances.length === 0
? undefined
: this.instances[0];
}
/**
* Returns an iterator of [entityId, componentData] pairs
* @returns An {@link Iterator} instance
*/
[Symbol.iterator](): Iterator<[number, TComponentInstance]> {
return this.entries();
}
/**
* Returns an iterator of all entity ids
* @returns An {@link ArrayIterator} of entity ids
*/
keys(): ArrayIterator {
return this.entities.values();
}
/**
* Returns an iterator of all component data
* @returns An {@link ArrayIterator} of component data
*/
values(): ArrayIterator {
return this.instances.values();
}
/**
* Returns an iterator of [entityId, componentData] pairs
* @returns An {@link Iterator} instance
*/
entries(): Iterator<[number, TComponentInstance]> {
let index = 0;
const entities = this.entities;
const instances = this.instances;
return {
next(): IteratorResult<[number, TComponentInstance]> {
if (index < instances.length) {
const result: IteratorResult<[number, TComponentInstance]> = {
value: [entities[index], instances[index]],
done: false
};
index++;
return result;
}
return { value: undefined, done: true };
},
reset() {
index = 0;
},
[Symbol.iterator]() {
return this;
},
};
}
/**
* Called when using JSON.stringify
* @returns An object representation for serialization
*/
toJSON() {
return { [ComponentMapKey]: 1, iterable: [...this.entries()] };
}
/**
* Returns an array of objects for tabular display
* @returns An array of objects formatted for {@link console.table}
*/
toTable(): any[] {
const table = []
for (const [key, value] of this.entries()) {
const entity = value as { new(): TComponentInstance };
const meta: Record = {};
meta["entity.key"] = key;
meta["entity.type"] = entity.constructor.name;
table.push({ ...meta, ...entity });
}
return table;
}
/**
* Prints entity data in a tabular format to the console
* @param properties - Optional filter. Specifies which property columns to display in the tables.
* @returns The {@link ComponentMap} instance for chaining
*/
printTable(properties: string[] = []): this {
console.table(this.toTable(), properties);
return this;
}
}