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