first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
# Query
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Query` compiles a declarative `QueryDescriptor` (`include` / optional `exclude` schemas) into bitset masks and evaluates it against the archetype graph. Because archetypes group entities by their exact component set, a query only needs to test each archetype's mask once — every entity inside a matching archetype is automatically a match. The result is O(archetypes) filtering plus O(entities-in-matching-archetypes) collection.
`defineQuery` is the canonical factory; raw `Query` instances are exposed for advanced consumers building custom system runners.
## Features
- Bitset-only match predicate (`containsAll` + `intersects`).
- Cached `matchedArchetypes` list, invalidated by `World` whenever archetypes change.
- Handles include-only, include + exclude, and empty-exclude descriptors cleanly.
- Result is a flat `ReadonlyArray<Entity>` in archetype-discovery order.
## Usage: Basic Example
```ts
import { defineComponent, defineQuery, createWorld } from "@suaveplan/ecs";
const Position = defineComponent("Position", { x: 0, y: 0 });
const world = createWorld();
const entity = world.spawn();
world.addComponent(entity, Position);
const result = world.query(defineQuery([Position]));
console.log([...result]); // [1]
```
## Usage: Advanced Example
```ts
import {
defineComponent,
defineQuery,
Query,
createWorld,
} from "@suaveplan/ecs";
const Position = defineComponent("Position", {});
const Disabled = defineComponent("Disabled", {});
const world = createWorld();
world.registerComponent(Position);
world.registerComponent(Disabled);
// Build a long-lived Query and re-evaluate on every tick.
const activeMovers = new Query(
defineQuery([Position], [Disabled]),
world.registry,
);
function tick() {
const entities = activeMovers.evaluate(
// archetype graph is exposed indirectly via world.query; advanced
// consumers run their own archetype scans through reflection.
[],
);
// ... iterate entities ...
}
```
## API Reference
| Member | Description |
|---|---|
| `new Query(descriptor, registry)` | Compile masks from a descriptor. Throws if descriptor references unregistered schemas. |
| `defineQuery(include, exclude?)` | Build a descriptor with optional `exclude`. |
| `matches(archetype)` | Predicate: does this archetype's mask satisfy the query? |
| `evaluate(archetypes)` | Collect every entity from every matching archetype. Result is cached until `invalidate()`. |
| `invalidate()` | Drop the cached match list; the next `evaluate` rebuilds it. |
| `includeMask` / `excludeMask` | Read-only masks for debug / inspection. |
## Implementation Notes
- `excludeMask` is `null` (not an empty `BitSet`) when no exclude is declared, so the predicate skips the `intersects` test entirely in the common case.
- Empty `exclude: []` is treated as "no exclude" — the descriptor normalises the difference.
- The world owns its `Query` objects; one-off `world.query(descriptor)` calls build an ad-hoc `Query` per call and discard it. Long-lived systems should pin a `Query` instance and call `evaluate` repeatedly.
- The cached `matchedArchetypes` list is invalidated whenever the archetype graph mutates (component add/remove that creates a new archetype, or `clear`). Mutations within an existing archetype do not invalidate — entity lists are read fresh on every evaluate.
- Iteration order is "every entity in archetype 0, then every entity in archetype 1, …". Within an archetype the order matches insertion order (subject to swap-remove side-effects).