• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 13619825401

02 Mar 2025 10:16PM UTC coverage: 89.29% (-0.03%) from 89.318%
13619825401

Pull #3380

github

web-flow
Merge 8f7089f10 into 6c9fca699
Pull Request #3380: feat: support any, all and not component/tag filters on Query

6445 of 8361 branches covered (77.08%)

59 of 63 new or added lines in 3 files covered. (93.65%)

5 existing lines in 2 files now uncovered.

13873 of 15537 relevant lines covered (89.29%)

25264.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.54
/src/engine/EntityComponentSystem/Query.ts
1
import { Entity } from './Entity';
2
import { Observable } from '../Util/Observable';
3
import { Component, ComponentCtor } from '../EntityComponentSystem/Component';
4

5
export type ComponentInstance<T> = T extends ComponentCtor<infer R> ? R : never;
6

7
/**
8
 * Turns `Entity<A | B>` into `Entity<A> | Entity<B>`
9
 */
10
export type DistributeEntity<T> = T extends infer U extends Component ? Entity<U> : never;
11

12
export interface QueryParams<
13
  TKnownComponentCtors extends ComponentCtor<Component> = never,
14
  TAnyComponentCtors extends ComponentCtor<Component> = never
15
> {
16
  components?: {
17
    all?: TKnownComponentCtors[];
18
    any?: TAnyComponentCtors[];
19
    not?: ComponentCtor<Component>[];
20
  };
21
  tags?: {
22
    all?: string[];
23
    any?: string[];
24
    not?: string[];
25
  };
26
}
27

28
export type QueryEntity<
29
  TAllComponentCtors extends ComponentCtor<Component> = never,
30
  TAnyComponentCtors extends ComponentCtor<Component> = never
31
> = [TAnyComponentCtors] extends [never] // (trick to exclude `never` explicitly)
32
  ? Entity<ComponentInstance<TAllComponentCtors>>
33
  : Entity<ComponentInstance<TAllComponentCtors>> | DistributeEntity<ComponentInstance<TAnyComponentCtors>>;
34

35
/**
36
 * Represents query for entities that match a list of types that is cached and observable
37
 *
38
 * Queries can be strongly typed by supplying a type union in the optional type parameter
39
 * ```typescript
40
 * const queryAB = new ex.Query<ComponentTypeA | ComponentTypeB>(['A', 'B']);
41
 * ```
42
 */
43
export class Query<
44
  TAllComponentCtors extends ComponentCtor<Component> = never,
45
  TAnyComponentCtors extends ComponentCtor<Component> = never
46
> {
47
  public readonly id: string;
48

49
  public entities: QueryEntity<TAllComponentCtors, TAnyComponentCtors>[] = [];
8,934✔
50

51
  /**
52
   * This fires right after the component is added
53
   */
54
  public entityAdded$ = new Observable<QueryEntity<TAllComponentCtors, TAnyComponentCtors>>();
8,934✔
55
  /**
56
   * This fires right before the component is actually removed from the entity, it will still be available for cleanup purposes
57
   */
58
  public entityRemoved$ = new Observable<QueryEntity<TAllComponentCtors, TAnyComponentCtors>>();
8,934✔
59

60
  public readonly filter = {
8,934✔
61
    components: {
62
      all: new Set<TAllComponentCtors>(),
63
      any: new Set<TAnyComponentCtors>(),
64
      not: new Set<ComponentCtor<Component>>()
65
    },
66
    tags: {
67
      all: new Set<string>(),
68
      any: new Set<string>(),
69
      not: new Set<string>()
70
    }
71
  };
72

73
  constructor(params: TAllComponentCtors[] | QueryParams<TAllComponentCtors, TAnyComponentCtors>) {
74
    if (Array.isArray(params)) {
8,934✔
75
      params = { components: { all: params } } as QueryParams<TAllComponentCtors, TAnyComponentCtors>;
8,930✔
76
    }
77

78
    this.filter.components.all = new Set(params.components?.all ?? []);
8,934✔
79
    this.filter.components.any = new Set(params.components?.any ?? []);
8,934✔
80
    this.filter.components.not = new Set(params.components?.not ?? []);
8,934✔
81
    this.filter.tags.all = new Set(params.tags?.all ?? []);
8,934!
82
    this.filter.tags.any = new Set(params.tags?.any ?? []);
8,934✔
83
    this.filter.tags.not = new Set(params.tags?.not ?? []);
8,934✔
84

85
    this.id = Query.createId(params);
8,934✔
86
  }
87

88
  static createId(params: Function[] | QueryParams<any, any>) {
89
    // TODO what happens if a user defines the same type name as a built in type
90
    // ! TODO this could be dangerous depending on the bundler's settings for names
91
    // Maybe some kind of hash function is better here?
92
    if (Array.isArray(params)) {
19,385✔
93
      params = { components: { all: params } } as QueryParams<any, any>;
10,451✔
94
    }
95

96
    const anyComponents = params.components?.any ? `any_${Query.hashComponents(new Set(params.components?.any))}` : '';
19,385!
97
    const allComponents = params.components?.all ? `all_${Query.hashComponents(new Set(params.components?.all))}` : '';
19,385!
98
    const notComponents = params.components?.not ? `not_${Query.hashComponents(new Set(params.components?.not))}` : '';
19,385!
99

100
    const anyTags = params.tags?.any ? `any_${Query.hashTags(new Set(params.tags?.any))}` : '';
19,385!
101
    const allTags = params.tags?.all ? `all_${Query.hashTags(new Set(params.tags?.all))}` : '';
19,385!
102
    const notTags = params.tags?.not ? `not_${Query.hashTags(new Set(params.tags?.not))}` : '';
19,385!
103

104
    return [anyComponents, allComponents, notComponents, anyTags, allTags, notTags].filter(Boolean).join('-');
19,385✔
105
  }
106

107
  static hashTags(set: Set<string>) {
108
    return Array.from(set)
2✔
109
      .map((t) => `t_${t}`)
2✔
110
      .sort()
111
      .join('-');
112
  }
113

114
  static hashComponents(set: Set<ComponentCtor<Component>>) {
115
    return Array.from(set)
19,385✔
116
      .map((c) => `c_${c.name}`)
36,195✔
117
      .sort()
118
      .join('-');
119
  }
120

121
  matches(entity: Entity): boolean {
122
    // Components
123
    // check if entity has all components
124
    for (const component of this.filter.components.all) {
13,555✔
125
      if (!entity.has(component)) {
23,800✔
126
        return false;
6,243✔
127
      }
128
    }
129

130
    // check if entity has any components
131
    if (this.filter.components.any.size > 0) {
7,312✔
132
      let found = false;
2✔
133
      for (const component of this.filter.components.any) {
2✔
134
        if (entity.has(component)) {
3✔
135
          found = true;
2✔
136
          break;
2✔
137
        }
138
      }
139

140
      if (!found) {
2!
NEW
141
        return false;
×
142
      }
143
    }
144

145
    // check if entity has none of the components
146
    for (const component of this.filter.components.not) {
7,312✔
147
      if (entity.has(component)) {
2✔
148
        return false;
1✔
149
      }
150
    }
151

152
    // Tags
153
    // check if entity has all tags
154
    for (const tag of this.filter.tags.all) {
7,311✔
NEW
155
      if (!entity.hasTag(tag)) {
×
NEW
156
        return false;
×
157
      }
158
    }
159

160
    // check if entity has any tags
161
    if (this.filter.tags.any.size > 0) {
7,311✔
162
      let found = false;
2✔
163
      for (const tag of this.filter.tags.any) {
2✔
164
        if (entity.hasTag(tag)) {
2!
165
          found = true;
2✔
166
          break;
2✔
167
        }
168
      }
169

170
      if (!found) {
2!
NEW
171
        return false;
×
172
      }
173
    }
174

175
    // check if entity has none of the tags
176
    for (const tag of this.filter.tags.not) {
7,311✔
177
      if (entity.hasTag(tag)) {
2✔
178
        return false;
1✔
179
      }
180
    }
181

182
    return true;
7,310✔
183
  }
184

185
  /**
186
   * Potentially adds an entity to a query index, returns true if added, false if not
187
   * @param entity
188
   */
189
  checkAndAdd(entity: Entity) {
190
    if (this.matches(entity) && !this.entities.includes(entity)) {
13,553✔
191
      this.entities.push(entity);
7,296✔
192
      this.entityAdded$.notifyAll(entity);
7,296✔
193
      return true;
7,296✔
194
    }
195
    return false;
6,257✔
196
  }
197

198
  removeEntity(entity: Entity) {
199
    const index = this.entities.indexOf(entity);
287✔
200
    if (index > -1) {
287✔
201
      this.entities.splice(index, 1);
182✔
202
      this.entityRemoved$.notifyAll(entity);
182✔
203
    }
204
  }
205

206
  /**
207
   * Returns a list of entities that match the query
208
   * @param sort Optional sorting function to sort entities returned from the query
209
   */
210
  public getEntities(sort?: (a: Entity, b: Entity) => number): QueryEntity<TAllComponentCtors, TAnyComponentCtors>[] {
211
    if (sort) {
30!
212
      this.entities.sort(sort);
×
213
    }
214
    return this.entities as QueryEntity<TAllComponentCtors, TAnyComponentCtors>[];
30✔
215
  }
216
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc