Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/entity/entity-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {IEntity} from "./entity.spec.ts";
import type {IEntity, TTag} from "./entity.spec.ts";
import type {TObjectProto} from "../_.spec.ts";

export interface IEntityBuilder {
Expand All @@ -19,6 +19,18 @@ export interface IEntityBuilder {
* @param component
*/
withAll(...component: ReadonlyArray<object | TObjectProto>): IEntityBuilder

/**
* Add tag to target entity
* @param tag
*/
withTag(tag: TTag): IEntityBuilder

/**
* Add all tags to target entity
* @param tags
*/
withTags(...tags: ReadonlyArray<TTag>): IEntityBuilder
}

export type TEntityBuilderProto = { new(): TEntityBuilderProto };
75 changes: 75 additions & 0 deletions src/entity/entity-builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {assert} from 'chai';
import {EntityBuilder} from "./entity-builder.ts";
import {buildWorld} from "../ecs/ecs-world.ts";
import {createSystem} from "../system/system.ts";
import {queryComponents} from "../ecs/ecs-query.ts";
import {Read, WithTag, WithoutTag} from "../query/query.ts";

class Component {
health = 100
}

const Tag = 'a-tag';

describe('Test EntityBuilder', () => {
it('withTag', () => {
const entity = new EntityBuilder().withTag(Tag).build();

assert.isTrue(entity.hasTag(Tag));
assert.equal(entity.getTagCount(), 1);
});

it('withTags', () => {
const entity = new EntityBuilder().withTags(Tag, 'another').build();

assert.equal(entity.getTagCount(), 2);
});

it('deduplicates repeated tags', () => {
const entity = new EntityBuilder().withTag(Tag).withTag(Tag).build();

assert.equal(entity.getTagCount(), 1);
});

it('applies tags before the entity is handed to the world', () => {
let tagged = false;
new EntityBuilder(undefined, entity => tagged = entity.hasTag(Tag))
.withTag(Tag)
.build();

assert.isTrue(tagged);
});
});

describe('Tags set at build time are visible to queries', () => {
async function count(descriptor: Parameters<typeof queryComponents>[0], tagged: boolean) {
const query = queryComponents(descriptor);
const System = createSystem({query}).withName('Probe').withRunFunction(() => {}).build();
const prepWorld = buildWorld()
.withDefaultScheduling(root => root.addNewStage(stage => stage.addSystem(System)))
.withComponents(Component)
.build();
const runWorld = await prepWorld.prepareRun();
const builder = runWorld.buildEntity().with(new Component());

if (tagged) {
builder.withTag(Tag);
}

builder.build();

return query.resultLength;
}

it('WithTag matches an entity tagged by the builder', async () => {
assert.equal(await count({c: Read(Component), _e: WithTag(Tag)}, true), 1);
});

it('WithoutTag rejects an entity tagged by the builder', async () => {
assert.equal(await count({c: Read(Component), _e: WithoutTag(Tag)}, true), 0);
});

it('WithoutTag matches an entity the builder left untagged', async () => {
assert.equal(await count({c: Read(Component), _e: WithoutTag(Tag)}, false), 1);
});
});
21 changes: 21 additions & 0 deletions src/entity/entity-builder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {Entity, type TEntityId} from "./entity.ts";
import type {TTag} from "./entity.spec.ts";
import type {IEntityBuilder} from "./entity-builder.spec.ts";
import type {TObjectProto} from "../_.spec.ts";

export * from './entity-builder.spec.ts';

export class EntityBuilder implements IEntityBuilder {
protected components = new Map<Readonly<object | TObjectProto>, ReadonlyArray<unknown>>();
protected tags = new Set<TTag>();

constructor(
protected uuid?: TEntityId,
Expand All @@ -15,11 +17,16 @@ export class EntityBuilder implements IEntityBuilder {
build(): Entity {
const entity = new Entity(this.uuid);
let component;
let tag;

for (component of this.components) {
entity.addComponent(component[0], ...component[1]);
}

for (tag of this.tags) {
entity.addTag(tag);
}

this.callback?.(entity);
return entity;
}
Expand All @@ -37,4 +44,18 @@ export class EntityBuilder implements IEntityBuilder {

return this;
}

withTag(tag: TTag): EntityBuilder {
this.tags.add(tag);
return this;
}

withTags(...tags: ReadonlyArray<TTag>): EntityBuilder {
let tag;
for (tag of tags) {
this.withTag(tag);
}

return this;
}
}
26 changes: 23 additions & 3 deletions src/query/components-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {
IAccessDescriptor,
IAccessQuery,
IComponentsQuery,
IExistenceDescriptor,
TAccessQueryData,
TAccessQueryParameter,
} from "./query.spec.ts";
import {EQueryType, ETargetType} from "./query.spec.ts";
import {EExistence, EQueryType, ETargetType} from "./query.spec.ts";
import {Query} from "./query.ts";
import type {TObjectProto} from "../_.spec.ts";
import type {IEntity, TTag} from "../entity/entity.spec.ts";
import {accessDescSym, addEntitySym, entitySym} from "./_.ts";
import {accessDescSym, addEntitySym, entitySym, existenceDescSym} from "./_.ts";

export class ComponentsQuery<DESC extends IAccessQuery<TObjectProto>> extends Query<DESC, TAccessQueryData<DESC>> implements IComponentsQuery<DESC> {
constructor(
Expand All @@ -28,7 +29,7 @@ export class ComponentsQuery<DESC extends IAccessQuery<TObjectProto>> extends Qu
}
}

protected getComponentDataFromEntity(entity: Readonly<IEntity>, descriptor: Readonly<DESC>): Readonly<TAccessQueryData<DESC>> {
protected getComponentDataFromEntity(entity: Readonly<IEntity>, descriptor: Readonly<DESC>): TAccessQueryData<DESC> {
const components: Record<string, Readonly<object>> = {};
let accessDesc;
let componentDesc: Readonly<TObjectProto | TAccessQueryParameter<TObjectProto>>;
Expand All @@ -37,6 +38,10 @@ export class ComponentsQuery<DESC extends IAccessQuery<TObjectProto>> extends Qu
for ([componentName, componentDesc] of Object.entries(descriptor)) {
accessDesc = (componentDesc as IAccessDescriptor<object>)[accessDescSym];

if (accessDesc === undefined) {
continue;
}

components[componentName] = accessDesc.targetType == ETargetType.component
? (entity.getComponent(accessDesc.target as TObjectProto) ?? entity)
: entity;
Expand All @@ -47,9 +52,24 @@ export class ComponentsQuery<DESC extends IAccessQuery<TObjectProto>> extends Qu

matchesEntity(entity: Readonly<IEntity>): boolean {
let componentDesc: Readonly<IAccessDescriptor<TObjectProto | undefined>>;
let existenceDesc;

// @ts-ignore todo: figure out typing. Something is still wrong somewhere
for (componentDesc of Object.values(this.queryDescriptor)) {
existenceDesc = (componentDesc as Partial<IExistenceDescriptor<TObjectProto>>)[existenceDescSym];

if (existenceDesc !== undefined) {
const exists = existenceDesc.targetType == ETargetType.tag
? entity.hasTag(existenceDesc.target as TTag)
: entity.hasComponent(existenceDesc.target as TObjectProto);

if (exists != (existenceDesc.type == EExistence.set)) {
return false;
}

continue;
}

if (
componentDesc[accessDescSym].targetType == ETargetType.tag
&& !entity.hasTag(componentDesc[accessDescSym].target as TTag)
Expand Down
13 changes: 8 additions & 5 deletions src/query/query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {IEntity, TTag} from "../entity/entity.spec.ts";
import {accessDescSym, addEntitySym, clearEntitiesSym, existenceDescSym, removeEntitySym, setEntitiesSym} from "./_.ts";

export type TAccessQueryParameter<C extends TObjectProto> = C & IAccessDescriptor<InstanceType<C>>;
export type TOptionalAccessQueryParameter<C extends TObjectProto | undefined> = IAccessDescriptor<C extends TObjectProto ? InstanceType<C> : undefined> & C extends TObjectProto ? C : undefined;
export interface IAccessQuery<C extends TObjectProto> { [componentName: string]: TAccessQueryParameter<C> | TOptionalAccessQueryParameter<C> }
export type TOptionalAccessQueryParameter<C extends TObjectProto | undefined> = IAccessDescriptor<C extends TObjectProto ? InstanceType<C> : undefined> & (C extends TObjectProto ? C : undefined);
export interface IAccessQuery<C extends TObjectProto> { [componentName: string]: TAccessQueryParameter<C> | TOptionalAccessQueryParameter<C> | TExistenceQueryParameter<C> }

export type TExistenceQueryParameter<C extends TObjectProto> = IExistenceDescriptor<C>;
export type TExistenceQuery<C extends TObjectProto> = Array<TExistenceQueryParameter<C>>;
Expand All @@ -31,10 +31,13 @@ export enum EQueryType {
Entities,
}

type TQueryParameterData<PARAM> = Required<Omit<InstanceType<PARAM & TObjectProto>, keyof IAccessDescriptor<object>>>;

export type TAccessQueryData<DESC extends IAccessQuery<TObjectProto>> = {
[P in keyof DESC]: DESC[P] extends TAccessQueryParameter<TObjectProto>
? Required<Omit<InstanceType<DESC[P]>, keyof IAccessDescriptor<object>>>
: (Required<Omit<InstanceType<DESC[P]>, keyof IAccessDescriptor<object>>> | undefined)
[P in keyof DESC as DESC[P] extends IAccessDescriptor<object | undefined> ? P : never]:
DESC[P] extends TAccessQueryParameter<TObjectProto>
? TQueryParameterData<DESC[P]>
: (TQueryParameterData<DESC[P]> | undefined)
}

export type TComparator<DATA> = (a: DATA, b: DATA) => number;
Expand Down
97 changes: 96 additions & 1 deletion src/query/query.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import {Read, ReadEntity, Write} from "./query";
import {Read, ReadEntity, ReadOptional, With, WithTag, Without, WithoutTag, Write} from "./query";
import {queryComponents} from "../ecs/ecs-query";
import {buildWorld} from "../ecs/ecs-world";
import {createSystem} from "../system/system";
import {Entity} from "../entity/entity";
import {assert} from "chai";

class Component {
health = 100
}

class Marker {
flag = true
}

const Tag = 'a-tag';

describe('Test Query', () => {
it('pop', () => {
const query = queryComponents({
Expand All @@ -18,3 +28,88 @@ describe('Test Query', () => {
}
});
});

describe('Existence parameters in component queries', () => {
async function count(descriptor: Parameters<typeof queryComponents>[0], tagged: boolean) {
const query = queryComponents(descriptor);
const System = createSystem({query}).withName('Probe').withRunFunction(() => {}).build();
const prepWorld = buildWorld()
.withDefaultScheduling(root => root.addNewStage(stage => stage.addSystem(System)))
.withComponents(Component, Marker)
.build();
const runWorld = await prepWorld.prepareRun();

const entity = new Entity();
entity.addComponent(new Component());

if (tagged) {
entity.addTag(Tag);
}

runWorld.addEntity(entity);

return query.resultLength;
}

it('With matches an entity which has the component', async () => {
assert.equal(await count({c: Read(Component), _e: With(Component)}, false), 1);
});

it('With rejects an entity which lacks the component', async () => {
assert.equal(await count({c: Read(Component), _e: With(Marker)}, false), 0);
});

it('Without rejects an entity which has the component', async () => {
assert.equal(await count({c: Read(Component), _e: Without(Component)}, false), 0);
});

it('Without matches an entity which lacks the component', async () => {
assert.equal(await count({c: Read(Component), _e: Without(Marker)}, false), 1);
});

it('WithTag matches a tagged entity', async () => {
assert.equal(await count({c: Read(Component), _e: WithTag(Tag)}, true), 1);
});

it('WithTag rejects an untagged entity', async () => {
assert.equal(await count({c: Read(Component), _e: WithTag(Tag)}, false), 0);
});

it('WithoutTag rejects a tagged entity', async () => {
assert.equal(await count({c: Read(Component), _e: WithoutTag(Tag)}, true), 0);
});

it('WithoutTag matches an untagged entity', async () => {
assert.equal(await count({c: Read(Component), _e: WithoutTag(Tag)}, false), 1);
});

it('still passes optional parameters to the system as data', async () => {
const query = queryComponents({c: Read(Component), o: ReadOptional(Marker)});
const System = createSystem({query}).withName('Probe').withRunFunction(() => {}).build();
const prepWorld = buildWorld()
.withDefaultScheduling(root => root.addNewStage(stage => stage.addSystem(System)))
.withComponents(Component, Marker)
.build();
const runWorld = await prepWorld.prepareRun();
runWorld.buildEntity().with(new Component()).build();

const first = query.getFirst()!;
assert.property(first, 'c');
assert.property(first, 'o');
});

it('does not pass existence parameters to the system as data', async () => {
const query = queryComponents({c: Read(Component), _e: Without(Marker)});
const System = createSystem({query}).withName('Probe').withRunFunction(() => {}).build();
const prepWorld = buildWorld()
.withDefaultScheduling(root => root.addNewStage(stage => stage.addSystem(System)))
.withComponents(Component, Marker)
.build();
const runWorld = await prepWorld.prepareRun();
runWorld.buildEntity().with(new Component()).build();

const first = query.getFirst()!;
assert.property(first, 'c');
assert.notProperty(first, '_e');
});
});