Files
suaveplan/packages/gaming/games/ecs/src/query/query.test.ts
2026-05-27 23:47:53 +01:00

104 lines
3.7 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { Archetype } from "../archetype/archetype.js";
import { BitSet } from "../bitset/bitset.js";
import { ComponentRegistry } from "../component-registry/component-registry.js";
import { defineComponent } from "../schema/schema.js";
import { defineQuery, Query } from "./query.js";
const Position = defineComponent("Position", { x: 0 });
const Velocity = defineComponent("Velocity", { dx: 0 });
const Disabled = defineComponent("Disabled", { d: true });
function makeArch(
registry: ComponentRegistry,
schemas: (typeof Position)[]
): Archetype {
const mask = new BitSet(registry.maxComponents);
const ids: number[] = [];
for (const s of schemas) {
const id = registry.getId(s);
mask.set(id);
ids.push(id);
}
ids.sort((a, b) => a - b);
return new Archetype(mask, ids);
}
describe("Query", () => {
it("include-only matches every superset archetype", {
timeout: 5000,
}, () => {
const reg = new ComponentRegistry();
reg.register(Position);
reg.register(Velocity);
const arch1 = makeArch(reg, [Position]);
const arch2 = makeArch(reg, [Position, Velocity]);
const arch3 = makeArch(reg, [Velocity]);
for (const id of [10, 11]) arch1.addEntity(id, new Map());
arch2.addEntity(20, new Map());
arch3.addEntity(30, new Map());
const q = new Query(defineQuery([Position]), reg);
const result = q.evaluate([arch1, arch2, arch3]);
expect([...result].sort((a, b) => a - b)).toEqual([10, 11, 20]);
});
it("exclude filters out archetypes whose mask intersects the exclude mask", {
timeout: 5000,
}, () => {
const reg = new ComponentRegistry();
reg.register(Position);
reg.register(Disabled);
const archEnabled = makeArch(reg, [Position]);
const archDisabled = makeArch(reg, [Position, Disabled]);
archEnabled.addEntity(1, new Map());
archDisabled.addEntity(2, new Map());
const q = new Query(defineQuery([Position], [Disabled]), reg);
const result = q.evaluate([archEnabled, archDisabled]);
expect([...result]).toEqual([1]);
});
it("invalidate clears the cached match list and forces a re-scan", {
timeout: 5000,
}, () => {
const reg = new ComponentRegistry();
reg.register(Position);
reg.register(Velocity);
const arch1 = makeArch(reg, [Position]);
arch1.addEntity(1, new Map());
const archetypes: Archetype[] = [arch1];
const q = new Query(defineQuery([Position]), reg);
expect(q.evaluate(archetypes)).toEqual([1]);
const arch2 = makeArch(reg, [Position, Velocity]);
arch2.addEntity(2, new Map());
archetypes.push(arch2);
// Without invalidate, the new archetype is not reconsidered.
expect(q.evaluate(archetypes)).toEqual([1]);
q.invalidate();
expect([...q.evaluate(archetypes)].sort()).toEqual([1, 2]);
});
it("defineQuery omits exclude when not supplied", { timeout: 5000 }, () => {
const descriptor = defineQuery([Position]);
expect(descriptor.exclude).toBeUndefined();
});
it("defineQuery includes exclude when provided", { timeout: 5000 }, () => {
const descriptor = defineQuery([Position], [Disabled]);
expect(descriptor.exclude).toEqual([Disabled]);
});
it("empty exclude array is treated as no exclude", {
timeout: 5000,
}, () => {
const reg = new ComponentRegistry();
reg.register(Position);
const q = new Query({ include: [Position], exclude: [] }, reg);
expect(q.excludeMask).toBeNull();
});
});