@javelin/ecs
Version:
188 lines • 7.33 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createQuery = void 0;
const core_1 = require("@javelin/core");
const component_1 = require("./component");
const internal_1 = require("./internal");
const type_1 = require("./type");
const ERROR_MSG_UNBOUND_QUERY = "a query must be executed within a system or bound to a world using Query.bind()";
/**
* Check if a query signature (type) matches a type signature, accounting for
* query filters (if any).
*/
function matchesTypeInner(typeA, typeB, filters) {
return type_1.typeIsSuperset(typeB, typeA) && typeB.every(c => !filters.not.has(c));
}
function createQueryInternal(options) {
var _a, _b;
const length = options.select.length;
const filters = (_a = options.filters) !== null && _a !== void 0 ? _a : { not: new Set() };
const type = type_1.normalizeType(options.select.map(schema => component_1.registerSchema(schema)));
const layout = ((_b = options.include) !== null && _b !== void 0 ? _b : options.select).map(schema => component_1.registerSchema(schema));
const recordsIndex = [];
const pool = core_1.createStackPool(() => [], components => {
core_1.mutableEmpty(components);
return components;
}, 1000);
let boundWorldId = options.boundWorldId;
/**
* Attempt to register an archetype with this query. If the archetype is
* matched, a live record of the archetype's entities, columns, and entity
* index is pushed into the query's registry.
*/
function maybeRegisterArchetype(archetype, records) {
if (matchesTypeInner(type, archetype.type, filters)) {
const columns = layout.map(schemaId => archetype.table[archetype.type.indexOf(schemaId)]);
records.push([
archetype.entities,
columns,
archetype.indices,
]);
}
}
/**
* Create a new index of archetype records for the provided world, attempt
* register existing archetypes, and subscribe to newly created ones.
*/
function registerWorld(worldId) {
const world = internal_1.UNSAFE_internals.worlds[worldId];
const records = [];
recordsIndex[worldId] = records;
world.storage.archetypes.forEach(archetype => maybeRegisterArchetype(archetype, records));
world.storage.archetypeCreated.subscribe(archetype => maybeRegisterArchetype(archetype, records));
return records;
}
function forEach(iteratee) {
const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : internal_1.UNSAFE_internals.currentWorldId;
core_1.assert(worldId !== null && worldId !== -1, ERROR_MSG_UNBOUND_QUERY, core_1.ErrorType.Query);
const records = recordsIndex[worldId] || registerWorld(worldId);
const components = pool.retain();
for (let i = 0; i < records.length; i++) {
const [entities, columns] = records[i];
for (let j = 0; j < entities.length; j++) {
for (let k = 0; k < length; k++) {
components[k] = columns[k][j];
}
iteratee(entities[j], components);
}
}
pool.release(components);
}
function iterator() {
const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : internal_1.UNSAFE_internals.currentWorldId;
core_1.assert(worldId !== null && worldId !== -1, ERROR_MSG_UNBOUND_QUERY, core_1.ErrorType.Query);
return (recordsIndex[worldId] || registerWorld(worldId))[Symbol.iterator]();
}
function not(...exclude) {
return createQueryInternal({
...options,
filters: {
not: new Set(exclude
.map(schema => internal_1.UNSAFE_internals.schemaIndex.get(schema))
.filter((x) => typeof x === "number")),
},
});
}
function select(...include) {
return createQueryInternal({ ...options, include });
}
function get(entity, out = []) {
const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : internal_1.UNSAFE_internals.currentWorldId;
const records = recordsIndex[worldId];
for (let i = 0; i < records.length; i++) {
const [, columns, indices] = records[i];
const index = indices[entity];
if (index !== undefined) {
for (let i = 0; i < columns.length; i++) {
out[i] = columns[i][index];
}
return out;
}
}
throw new Error("Failed to get components of query: entity does not match query");
}
function bind(world) {
return createQueryInternal({
...options,
boundWorldId: world.id,
});
}
function test(entity) {
const worldId = boundWorldId !== null && boundWorldId !== void 0 ? boundWorldId : internal_1.UNSAFE_internals.currentWorldId;
const records = recordsIndex[worldId];
for (let i = 0; i < records.length; i++) {
const record = records[i];
if (record[2][entity] !== undefined) {
return true;
}
}
return false;
}
let _type = type;
function matchesType(type) {
return matchesTypeInner(_type, type, filters);
}
function equals(query) {
if (query.type.length !== type.length) {
return false;
}
for (let i = 0; i < query.type.length; i++) {
if (query.type[i] !== type[i]) {
return false;
}
}
if (query.filters.not.size !== filters.not.size) {
return false;
}
let result = true;
query.filters.not.forEach(schemaId => (result = result && filters.not.has(schemaId)));
return result;
}
function match(components, out = []) {
for (let i = 0; i < layout.length; i++) {
out[i] = null;
}
for (let i = 0; i < components.length; i++) {
const component = components[i];
const index = layout.indexOf(component_1.getSchemaId(component));
if (index !== -1) {
out[index] = component;
}
}
return out;
}
const query = forEach;
query[Symbol.iterator] = iterator;
query.type = type;
query.filters = filters;
query.not = not;
query.select = select;
query.get = get;
query.bind = bind;
query.test = test;
query.matchesType = matchesType;
query.equals = equals;
query.match = match;
return query;
}
/**
* Create a query that can be used to iterate entities that match a selector.
* Maintains a live-updating cache of entities, and can be used across multiple
* worlds.
* @example
* const burning = createQuery(Player, Burn)
* burning((entity, [player, burn]) => {
* player.health -= burn.damage
* })
* @example
* for (const [entities, [p, b]] of burning) {
* for (let i = 0; i < entities.length; i++) {
* p[i].health -= b[i].damage
* }
* }
*/
function createQuery(...select) {
return createQueryInternal({ select });
}
exports.createQuery = createQuery;
//# sourceMappingURL=query.js.map