/* 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 type { ComponentMap } from './component-map.js'; import type { EntityMap } from './entity-map.js'; import { ComponentIterator } from './iterator.js'; import type { ComponentClass, ComponentClasses, Iterator, SingleOrArray } from './types.js'; /** * A reusable query for retrieving entities and their components * @category Queries * @example * const query = ecs.query(Player, Position) * * // get the first entry * const [playerId, player, position] = query.firstEntry() ?? [] * * // get the first key * const [playerId, position] = query.firstKey() ?? [] * * // get the first value * const [player, position] = query.firstValue() ?? [] * * // iterate * for (const [playerId, player, position] of query) { * * } */ export class ComponentQuery { private keyMap: ComponentMap private componentMaps: ComponentMap[] = [] /** * Creates a new component query * @param ecs - The {@link EntityMap} instance to query * @param components - The first component class is the primary component. Optional additional component classes to retrieve for each entity. * @throws {@link ComponentNotRegistered} when any of specified component(s) are not registered */ constructor( public ecs: EntityMap, ...components: [ComponentClass, ...ComponentClasses] ) { this.keyMap = this.ecs.getMap(components[0])!; for (let i = 1; i < components.length; i++) { this.componentMaps.push(this.ecs.getMap(components[i])!) } } /** * Gets the first entry matching the query. * * Includes the entity id, the key component data, and any related component data. * @returns A tuple of `[entityId, keyComponentData, ...relatedComponentData]`, or `undefined` if no entities match. */ firstEntry(): [number, TKey, ...TRelated] | undefined; firstEntry() { if (this.componentMaps.length === 0) return this.keyMap.firstEntry() const [entityId, entryValue] = this.keyMap.firstEntry() ?? [] if (entityId === undefined) return undefined; const results: any[] = [entityId, entryValue] for (let i = 0; i < this.componentMaps.length; i++) { results.push(this.componentMaps[i].get(entityId)); } return results } /** * Gets the first key matching the query. * * @returns The entity id if no related components were specified in the constructor. * * Otherwise, a tuple of `[entityId, ...relatedComponentData]`. * * Returns `undefined` if no entities match. */ firstKey(): SingleOrArray<[number, ...TRelated]> | undefined; firstKey() { const entityId = this.keyMap.firstKey() if (this.componentMaps.length === 0) return entityId; if (entityId === undefined) return undefined; const results: any[] = [entityId] for (let i = 0; i < this.componentMaps.length; i++) { results.push(this.componentMaps[i].get(entityId)); } return results } /** * Get the first component data matching the query. * * @returns The key component data if no related components were specified in the constructor. * * Otherwise, a tuple of `[keyComponentData, ...relatedComponentData]`. * * Returns `undefined` if no entities match. */ firstValue(): SingleOrArray<[TKey, ...TRelated]> | undefined; firstValue() { if (this.componentMaps.length === 0) return this.keyMap.firstValue() const [entityId, keyValue] = this.keyMap.firstEntry() ?? [] if (entityId === undefined) return undefined; const results = [keyValue] for (let i = 0; i < this.componentMaps.length; i++) { results.push(this.componentMaps[i].get(entityId)); } return results } /** * Returns the count of entities matching the query * @returns The number of entities that have the key component */ get entityCount() { return this.keyMap.size } /** * Destroys all entities matching the query * @returns The total number of components destroyed across all affected entities * @example * const destroyedCount = query.destroyEntities() */ destroyEntities(): number { return this.ecs.destroyEntity(...this.keyMap.keys()) } /** * Gets all entity ids matching the query. * * @returns An {@link ArrayIterator} of entity ids. */ keys(): ArrayIterator { return this.keyMap.keys() } /** * Gets all component data matching the query. * * @returns An {@link Iterator} of `[keyComponentData, ...relatedComponentData]` tuples containing component data * @example * const query = ecs.query(Player, Position) * for (const [player, position] of query.values()) { } */ values(): Iterator<[TKey, ...TRelated]> { const keyMap = this.keyMap; const componentMaps = this.componentMaps; let keysIterator = keyMap.keys(); return { next(): IteratorResult<[TKey, ...TRelated]> { const key = keysIterator.next(); if (key.done) return { value: undefined, done: true }; const entityId = key.value; const keyComponent = keyMap.get(entityId); // append the related components const value = [keyComponent]; for (let i = 0; i < componentMaps.length; i++) { const map = componentMaps[i]; value.push(map.get(entityId)); } return { value: value as any, done: false }; }, reset() { keysIterator = keyMap.keys(); }, [Symbol.iterator]() { return this; }, }; } /** * Gets all component data entries matching the query * * @returns A {@link ComponentIterator} instance that yields `[entityId, keyComponentData, ...relatedComponentData]` * @example * const query = ecs.query(Player, Position) * for (const [entityId, player, position] of query.entries()) { } */ entries() { return this[Symbol.iterator](); } /** * @returns A {@link ComponentIterator} instance that yields `[entityId, keyComponentData, ...relatedComponentData]` */ [Symbol.iterator]() { return new ComponentIterator(this); } }