/* 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; } }