Full Code of mayakwd/tick-knock for AI

develop 48e16b69a643 cached
31 files
174.2 KB
42.7k tokens
249 symbols
1 requests
Download .txt
Repository: mayakwd/tick-knock
Branch: develop
Commit: 48e16b69a643
Files: 31
Total size: 174.2 KB

Directory structure:
gitextract_slnklggk/

├── .github/
│   └── workflows/
│       ├── build.yml
│       └── publish.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest-ci.json
├── package.json
├── src/
│   ├── ecs/
│   │   ├── ComponentId.ts
│   │   ├── Engine.ts
│   │   ├── Entity.ts
│   │   ├── IterativeSystem.ts
│   │   ├── LinkedComponent.ts
│   │   ├── LinkedComponentList.ts
│   │   ├── Query.ts
│   │   ├── ReactionSystem.ts
│   │   ├── Subscription.ts
│   │   ├── System.ts
│   │   └── Tag.ts
│   ├── index.ts
│   └── utils/
│       ├── Class.ts
│       └── Signal.ts
├── tests/
│   └── unit/
│       ├── engine.spec.ts
│       ├── entity.spec.ts
│       ├── linked.list.spec.ts
│       ├── query.spec.ts
│       ├── shared.config.spec.ts
│       ├── signal.spec.ts
│       └── system.spec.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/build.yml
================================================
name: build

on:
  workflow_dispatch:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [ 20 ]

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install

      - name: Build
        run: yarn build

      - name: Run tests
        run: yarn test-ci


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to NPM

on:
  push:
    branches:
      - main
      - master
    tags:
      - v*
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'yarn'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: yarn install

      - name: Run tests
        run: yarn test-ci

  publish-to-npm:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [ 20 ]
    needs: build
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'yarn'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: yarn install

      - name: Build
        run: yarn build

      - name: Run tests
        run: yarn test-ci

      - name: Pack
        run: yarn pack

      - name: Publish to NPM
        run: yarn publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

# Generated library
lib

================================================
FILE: .travis.yml
================================================
language: node_js
dist: focal
node_js:
  - "19"
before_install:
  - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.15.2
  - export PATH=$HOME/.yarn/bin:$PATH

install:
  - yarn

cache:
  yarn: true

before_script:
  - yarn global add codecov
  - yarn setup
  - yarn build

script:
  - yarn test-ci

after_success:
  - codecov
  - yarn pack

deploy:
  - provider: npm
    edge: true
    cleanup: false
    email: "contact@pastila.org"
    api_key: $NPM_TOKEN
    on:
      tags: true


================================================
FILE: CHANGELOG.md
================================================
# 4.3.0

Features:

- Introduced possibility to safely remove entities from the engine.
  Now `Engine.removeEntity` takes a boolean value as a second argument "safe",
  which indicates whether the entity should be removed safely or not.

  If safe argument value is `true` then the entity will be removed after the Engine update cycle is
  iteration is finished, meaning that the entity will be removed after all systems have been updated.

  Safely removed entities won't be discoverable by getEntityById method, but they will be still accessible
  in the queries (and remaining systems updates).

  This behavior will become default in the next major release.

# 4.2.0

Features:

- Now you can request for system removal when it's no longer needed. Check `requestRemoval`.

# 4.1.0

Fixes:

- \[Breaking Change\] Arguments order of `pick` by id API aligned with other APIs.
- `isLinkedComponent` now returns false for undefined values, instead of throwing an Error.

# 4.0.5

Features:

- The following APIs got an additional optional id parameter to make working with Linked Components easier: `has`
  , `hasComponent`, `get`.

# 4.0.4

Fixes:

- `id` of `LinkedComponent` is not readonly anymore.

# 4.0.3

Features:

- Linked components now have an optional id, and can be picked with `pick` by id.

Fixes:

- Fixed usage sample for LinkedComponents

# 4.0.2

Fixes:

- If IterativeSystem was removed from the engine and added again later, no iteration took place.

# 4.0.1

Fixes:

- ReactionSystem now exported through index.ts

# 4.0.0

Features:

- Added a new convenient API for working with linked components:
  - Method `withdraw` removes the first LinkedComponent component of the provided type or existing standard component
  - Method `pick` removes provided LinkedComponent component instance or existing standard component
  - Method `iterate` iterates over instances of LinkedComponent and performs the `action` over each. Works for standard
    components (action will be called for a single instance in this case).
  - Method `find` searches a component instance of the specified class. Works for standard components (predicate will be
    called for a single instance in this case).
  - Method `getAll` returns a generator that can be used for iteration over all instances of specific type components.
  - Method `lengthOf` returns the number of existing components of the specified class.

Breaking changes:

- Signals `onComponentAdded`, `onComponentRemoved` now will be triggered for every LinkedComponent.
- Adding a linked component with `add` or `addComponent` will remove all existing linked components of the same type.
  Linked components will be replaced even if the passed component already exists in the Entity.

# 3.0.1

Fixes:

- `EntitySnapshot.current` now is writable.
- Added inline documentation to `EntitySnapshot.previous`.

# 3.0.0

Features:

- Added shared config entity, that is accessible across all systems added to `Engine`
- Added possibility to retrieve `Entity` from `Engine` by id

Breaking changes:

- Parameter `engine` was removed from `onAddedToEngine` and `onRemovedFromEngine` methods in the systems. Use `this.engine` instead.
- `EntitySnapshot` was reimplemented. It has distinguished fields `EntitySnapshot.current and `EntitySnapshot.previous`,
  which reflects current and previous Entity states accordingly.
- `Entity.components` now represented as a `Record` instead of the `Map`

Improvements:

- Typed-signals was replaced with the built-in light-weight implementation.
- `EntitySnapshot` won't be created if there are no change listeners.

Fixes:

- `Entity.copyFrom` now copies tags.
- `EntitySnapshot` now works properly with the tags. Previously, the difference between the previous state and the
  current state did not show changes in the tags.
- `EntitySnapshot` now works properly with the resolveClass.

# 2.2.0

Features:

- Add linked components Fixed:
- Documentation readability

# 2.1.0

Features:

- Add possibility to set any type as the message type for subscription

# 2.0.2

- Fixed broken Class API

# 2.0.1

- Fixed broken API for QueryBuilder and Entity.remove

# 2.0.0
- Added tags support
- Added messaging channel for system->engine->user
- Fixed EntitySnapshot behavior
- Added `engine` getter in the System
- Added support of initialization ReactionSystem and IterativeSystem with QueryPredicate and QueryBuilder
- Query got possibility to check whether entity is in it, via `has` method
- Documentation completely rewritten

# 1.4.1

- Removed redundant `updateEntity` from `ReactionSystem`

# 1.4.0

- Added `ReactionSystem`
- Documentation updated

# 1.3.0

- Fixed critical issue with updating of a `Query`. Queries whose predicates were a set of conditions that went beyond the capabilities of QueryBuilder could incorrectly evaluate the presence state for Entity after removing or adding components.   

# 1.2.7

- Fixed wrong type inference for `Entity#hasAll` and `Entity#hasAny`
- Added several utility methods for `Query`

# 1.2.6

- Added `first`, `last` and `length` getter for queries

# 1.2.5

- Added feature of invalidation entity and queries
- Fixed disconnecting of entities from engine

# 1.2.4

- Switched to commonjs modules

# 1.2.3

- Reverted `IterativeSystem#entities` remove
- Added `IterativeSystem#prepare` protected method, which will be invoked after adding iterative system to engine

# 1.2.2

- Added Entity#hasAny, Entity#hasAll methods
- Fixed throwing an error with passing invalid value to param `component` of `Entity#add` method
- Removed redundant `entities` getter from `IterativeSystem`

# 1.2.1

- Fixed bug with disconnecting from Entity events after remove from Engine. 
- Added utility methods for clearing `Engine`. 
  - `Engine#clear()`
  - `Engine#removeAllSystems()`
  - `Engine#removeAllQueries()`
  - `Engine#removeAllEntities()`

# 1.2.0
- Changed logic of resolving of component identifier. Changes could affect resolving of inherited components. Now inherited components will not be resolved as its ancestors.
- Added parameter for Entity#add "resolveClass" - which specifies how could be resolved component.
- Updated documentation
- Added tests for Query#isEmpty 

# 1.1.2
- Added Query#isEmpty property

# 1.1.1
- Added documentation

# 1.1.0
- Fixed query onEntityAdded, onEntityRemoved handlers
- Added entity snapshot for properly handling of the entity changes

# 1.0.7
- Fixed false-positive query trigger

# 1.0.6
- Switched library target to ES5

# 1.0.5
- Updated documentation for every core type
- Added guard that stops updating process for IterativeSystem, if it was removed engine
- Fixed order of dispatching and removing of the component. Now dispatching happens before removing.
- Added "get accessor" to query entities from Iterative system 

# 1.0.0
- Initial release



================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019-2020 Ilya Malanin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Tick-Knock

> Small and powerful, type-safe and easy-to-use Entity-Component-System (ECS)
> library written in TypeScript

[![Build Status](https://github.com/mayakwd/tick-knock/actions/workflows/build.yml/badge.svg)](https://travis-ci.org/mayakwd/tick-knock)
[![Codecov Coverage](https://img.shields.io/codecov/c/github/mayakwd/tick-knock/develop.svg?style=flat-square)](https://codecov.io/gh/mayakwd/tick-knock/)

😊 [Buy me a coffee](https://www.buymeacoffee.com/rdolivaw)

# Table of contents

- [Installing]
- [How it works?]
- [Inside the Tick-Knock]
    - [Engine]
        - [Subscription]
    - [Component]
    - [Linked Component]
        - [Tag]
        - [Entity]
        - [System]
        - [Query]
            - [QueryBuilder]
            - [Queries and Systems]
            - [Built-in query-based systems]
                - [ReactionSystem]
                - [IterativeSystem]
        - [Snapshot]
        - [Shared Config]
        - [Linked Components How-To]
- [Restrictions]
    - [Shared and Local Queries]
    - [Queries with complex logic and Entity invalidation]
- [License]
- [Donation]

# Installing

- Yarn: `yarn add tick-knock`
- NPM: `npm i --save tick-knock`

# How it works?

Tick-Knock was inspired by several ECS libraries, mostly by [Ash ECS](https://www.richardlord.net/ash/).

The main approach was re-imagined to make it lightweight, easy-to-use, and less boiler-plate based.

# Inside the Tick-Knock

In this part, you will learn all basics of Tick-Knock step by step.

## Engine

Engine is a "world" where entities, systems, and queries interact with each other.

Since the Engine is the initial entry point for development with Tick-Knock, it is from this point that the creation of
your world starts. Usually, the Engine exists in just one instance, and it does nothing but orchestrating everything
added to it.

To begin with, you can add the most usual "inhabitants" to it.

```typescript
const engine = new Engine();
const entity = new Entity()
  .add(new Hero())
  .add(new health(10))
engine.addEntity(entity);
```

Or you can take it out:

```typescript
engine.removeEntity(entity);
```

The second main "inhabitant" is System. It is responsible for processing Entities and their components. We will learn
about them in detail later.

```typescript
engine.addSystem(new ViewSystem(), 1);
engine.addSystem(new PhysicsSystem(), 2);
```

As you may have noticed, we pass two parameters: system instance, and the second is update priority. The higher the
priority number is, the later the system will be processed.

The third type of resident is Query, which is responsible for mapping entities within the Engine and returns a list of
already filtered and ready-to-use entities.

```typescript
const heroesQuery = new Query((entity) => entity.has(Hero));
engine.addQuery(heroesQuery);
````

The main task of the engine is to start the world update process and to report on the ongoing changes to Queries.  
These changes can be: additions to and removal of entities from the Engine, and changes in the components of specific
Entities.

To perform the update step, we must call the `update` method and pass as a parameter the time elapsed since the previous
update.  
Every time we start an update, the systems take turns, in order of priority, executing their own update methods.

```typescript
// Half a second has passed from the previous step.
engine.update(0.5); 
```

### Subscription

An additional - one of the Engine's responsibilities - transferring the messages from systems to the user. This can be
very useful when, for example, you want to report that the round in your game is over.

```typescript
engine.subscribe(GameOver, (message: GameOver) => {
  if (game.win) {
    this.showWinMessage();
  } else {
    this.showLoseMessage();
  }
});
```

You can use not only class type as an argument but any value. For example, it could be a string or number.

```typescript
const GAME_OVER = 'gameOver';
engine.subscribe(GAME_OVER, () => {
  this.showGameOver();
});
```

> **Details of implementation**
>
> When the `dispatch` method is called in the system, then to get the right listeners, the compliance of
> the `messageType` for each subscription will be checked.
> - If `typeof subscription.messageType` is a `'function'`, then the matching will be performed using `instanceOf`.
> - Otherwise, the matching will be done through strict equality `message === subscription.messageType`.

## Component

It is a data object, its purpose - to represent a single aspect of your entity. For example, position, velocity,
acceleration.

- ❕ Any class could be considered as the component. There are no restrictions.
- ❗ For proper understanding, it needs to be noticed that the component should be a data class, without any logic.
  Otherwise, you'll lose the benefits of the ECS pattern.

**Let's write your first component:**

```typescript
class Position {
  public constructor(
    public x: number = 0,
    public y: number = 0
  ) {}
}
```

> Yes, this is a component! 🎉

## Linked component

It is still a data class, but it is made to solve the problem when you need to have multiple components of the same
type.

Let's assume that you have a Damage component in your game. Several enemies attack the Hero simultaneously by adding the
Damage component to it. What will happen? Only the last Damage component will be added to the Hero Entity because every
previous one will be removed.

To solve this problem - you need to implement ILinkedComponent interface in your Damage component and "append" instead
of "add" the Damage component to the entity. That will do the job. After that, in DamageSystem you can find all damage
sources:

```typescript
class Damage extends LinkedComponent {
  public constructor(
    public readonly value: number
  ) {
    super()
  }
}

hero.append(new Damage(100));
hero.append(new Damage(5));

class DamageSystem extends IterativeSystem {
  public constructor() {
    super((entity) => entity.hasAll(Damage, Health));
  }

  public updateEntity(entity: Entity) {
    const health = entity.get(Health)!;
    while (entity.has(Damage)) {
      const damage = entity.withdraw(Damage);
      health.value -= damage.value;
    }
  }
}
```

## Tag

It also can be called a "label". It's a simplistic way to help you not "inflate" your code with classes without data.
For instance, you want to mark your entity as Dead. There are two ways:

- To create a component class: `class Dead {}`
- Or to create a tag - that can be represented as a `string` or `number`.

Using tags is much easier and consumes less memory if you do not have additional component data.

**Example:**

```typescript
const ENEMY = 'enemy';
const HERO = 100500;
```

> Keep it simple! 😄

## Entity

It is a general-purpose object, which can be marked with tags and can contain different components.

- So it can be considered as a container that can represent any in-game entity, like an enemy, bomb, configuration, game
  state, etc.
- Entity can contain only one component or tag of each type. You can't add two `Position` components to the entity, the
  second one will replace the first one.

**This is how it works:**

```typescript
const entity = new Entity()
  .add(new Position(100, 100))
  .add(new Position(200, 200))
  .add(HERO);

console.log(entity.get(Position)); // Position(x = 200, y = 200)
```

> Looks easy? Yes, it is!

## System

Systems are logic bricks in your application. If you want to manipulate entities, their components, and tags - it is the
right place.

Please, keep in mind that the complexity of the system mustn't be too high. When you find that your system is doing too
much in the "update" method, you need to split it into several systems.

Responsibility of the system should cover no more than one logical aspect.

The system always has the following functionality:

- Priority, which can be set before adding a system to the engine.
- Reference to the `engine` will give you access to the engine itself and its entities. But be aware - you can't access
  an engine if the system is not connected to it. Otherwise, you'll get an error.
- Methods `onAddedToEngine` and `onRemovedFromEngine` will be called in the cases described by their naming.
- With the method `dispatch`, you can easily send a message outside of the system. It will be delivered through the
  engine [Subscription](#subscription) pipe. There are the same restrictions as for the engine. If the system is not
  attached to the engine, then an attempt to send a message will throw an error.
- And last but not least, the heart of your system - method `update`. It will be called whenever `Engine.update` is
  being invoked. Update method - the right place to put your logic.

**Example:**
It's time to write our first and straightforward system. It will iterate through all the entities that are in the
Engine, check if they have Position and Velocity components.  
And if they do, then move our object.

```typescript
class Velocity {
  public constructor(
    public x: number = 0,
    public y: number = 0
  ) {}
}

class PhysicsSystem extends System {
  public constructor() {
    super();
  }

  public update(dt: number): void {
    const {entities} = this.engine;
    for (const entity of entities) {
      if (entity.hasAll(Position, Velocity)) {
        const position = entity.get(Position)!;
        const velocity = entity.get(Velocity)!;
        position.x += velocity.x * dt;
        position.y += velocity.y * dt;
      }
    }
  }
}
```

> There you go!
> 🎁 In real life, you don't have to iterate through every entity in every system. It's completely uncomfortable and not
> optimal. In this library, there is a mechanism that can prepare a list of the entities that you need according to the
> criteria you set - it's called Query.

## Query

So what the "Query" is? It's a matching mechanism that can tell you which entities in the Engine are suitable for your
needs.

For example, you want to write a system that is responsible for displaying sprites on your screen. To do this, you
always need a current list of entities, each of which has three components - View, Position, Rotation, and you want to
exclude those marked with the HIDDEN tag.

**Let's write our first Query.**

```typescript
const displayListQuery = new Query((entity: Entity) => {
  return entity.hasAll(View, Position, Rotation) && !entity.has(HIDDEN);
});
```

> That's all!

Adding this Query to the Engine will always contain an up-to-date list of entities that meet the described requirements.
Besides, you can always find out when a new entity has appeared in the Query, or an old entity has left it.

```typescript
displayListQuery.onEntityAdded.connect(({current}: EntitySnapshot) => {
  console.log("We've got a rookie here!");
  container.addChild(current.get(View)!.view);
});
displayListQuery.onEntityRemoved.connect(({previous}: EntitySnapshot) => {
  container.removeChild(previous.get(View)!.view);
  console.log("Good bye, friend!");
});
```

### QueryBuilder

Query builder is super simple. It has not much power, but you can use it for creating queries that must contain specific
Components.

```typescript
const query: Query = new QueryBuilder()
  .contains(ComponentA, ComponentB)
  .contains(TAG)
  .build();
```

### Queries and Systems

Now let's see how we can use Query on systems?

Let's write `ViewSystem`, which will be responsible for displaying our Entity on the screen.  
When entities get to the list, the system will add them to the screen, and when they leave the list, the system will
remove them from the screen.

**Example:**

```typescript
const query = new Query((entity: Entity) => {
  return entity.hasAll(View, Position, Rotation) && !entity.has(HIDDEN);
});

class ViewSystem extends System {
  public constructor(
    private readonly container: Container
  ) { super(); }

  public onAddedToEngine(): void {
    // To make query work - we need to add it to the engine
    this.engine.addQuery(query);
    // And we need to add to the display list all entities that already 
    // exists in the Engine`s world and matches our Query 
    this.prepare();
    // We want to know if new entities were added or removed
    query.onEntityAdded.connect(this.onEntityAdded);
    query.onEntityRemoved.connect(this.onEntityRemoved);
  }

  public onRemovedFromEngine(): void {
    // There is no reason to update query after system was removed 
    // from the engine
    this.engine.removeQuery(query);
    // No reason for further listening of the updates
    query.onEntityAdded.disconnect(this.onEntityAdded);
    query.onEntityRemoved.disconnect(this.onEntityRemoved);
  }

  // We only want to update positions of the views on the screen,
  // so there is no need for "dt" parameter, it can be omitted
  public update(): void {
    const entities = this.query.entities;
    for (const entity of entities) {
      this.updatePosition(entity);
    }
  }

  private prepare(): void {
    for (const entity of this.query.entities) {
      this.onEntityAdded(entity);
    }
  }

  private updatePosition(entity: Entity): void {
    const {view} = entity.get(View)!;
    const {x, y} = entity.get(Position)!;
    const {rotation} = entity.get(Rotation)!;
    view.position.set(x, y);
    view.rotaion.set(rotation);
  }

  private onEntityAdded = ({current}: EntitySnapshot) => {
    // Let's add new view to the screen
    this.container.addChild(current.get(View)!.view);
    // Don't forget to update it's position on the screen
    this.updatePosition(current);
  };

  private onEntityRemoved = ({previous}: EntitySnapshot) => {
    // Let's remove the view from the screen, because Entity no longer 
    // meets the requirements (might be it lost the View component 
    // or it was hidden)
    this.container.removeChild(previous.get(View)!.view);
  };
}
```

> 😎 I'm sure you saw the reference to `EntitySnapshot` and wondering, "what the heck is that?". Please, be
> patient, [I'll tell you about](#Snapshot) it a bit later.
> I think it looks good and clear for understanding!

- 🤔 You can say: "we need to write too much boilerplate-code".
- And of course, Tick-Knock will help you to reduce boilerplate-code!

### Built-in query-based systems

In favor of reducing the time to write the boilerplate code - Tick-Knock provides two built-in systems. Each of them
already knows how to work with Query, process the information coming from it, and allow access to this Query's entities.

All of the following built-in systems have the following features:

You can initialize those systems via three different items, which will be converted to Query eventually:

- Query itself
- Query predicate - Query will be automatically created on top of it. This feature was introduced to reduce the size of
  the boilerplate code.
- QueryBuilder - it is also a valid option.
- They have a getter `entities`, which returns the current entities list of the Query.
- They have a built-in property entityAdded and entityRemoved, you need to define them if you want to track Query
  changes.

#### ReactionSystem

ReactionSystem can be considered as the system that has the ability to react to changes in Query. It is a basic built-in
system. Exactly it will be used in most cases when developing your application.

Let's try to rewrite our ViewSystem, taking ReactionSystem as a basis, and take advantage of all the conveniences it
provides.

**Example:**

```typescript
class ViewSystem extends ReactionSystem {
  public constructor(private readonly container: Container) {
    super((entity: Entity) => {
      return entity.hasAll(View, Position, Rotation) && !entity.has(HIDDEN);
    });
  }

  public update(): void {
    for (const entity of this.entities) {
      this.updatePosition(entity);
    }
  }

  protected prepare(): void {
    for (const entity of this.entities) {
      this.entityAdded(entity);
    }
  }

  private updatePosition(entity: Entity): void {
    const {view} = entity.get(View)!;
    const {x, y} = entity.get(Position)!;
    const {rotation} = entity.get(Rotation)!;
    view.position.set(x, y);
    view.rotaion.set(rotation);
  }

  protected entityAdded = ({current}: EntitySnapshot) => {
    this.updatePosition(current);
    this.container.addChild(current.get(View)!.view);
  };

  protected entityRemoved = ({previous}: EntitySnapshot) => {
    this.container.removeChild(previous.get(View)!.view);
  };
}
```

> Now it's pretty simpler! 🎉

#### IterativeSystem

This system has the same advantages as the ReactionSystem because it is inherited from the last one. 😅 All it brings is
a built-in iteration cycle for our Query inside the update method.

**So, let's upgrade our `ViewSystem` a bit.**

```typescript
class ViewSystem extends IterativeSystem {
  // almost everything remains the same, so I'll skip most of the code.
  // The only difference regarding example with ReactionSystem - that we 
  // don't need to override `update` method. 
  // Instead of it we need to override updateEntity method.
  // Also, we can safely omit the dt parameter because we do not use it.
  protected updateEntity(entity: Entity, dt: number) {
    this.updatePosition(entity);
  }
}
```

#### Remove the system as it's done

It's possible to request removal of the system when you don't need it anymore. For example, the system is only
needed to render the playing field, and trying to run it at every update cycle is wasteful.

Fortunately, you can request deletion right from the system:

```typescript
class RenderBoardSystem extends System {
  public update(dt: number): void {
    // Your render board code
    this.requestRemoval();
  }
}
```

That's it. Your system will be removed right after update cycle.

## Snapshot

As you may have noticed, when we are tracking changes in Query, we get in `entityAdded` and `entityRemoved` not `Entity`
but `EntitySnapshot`.
**So what is a snapshot?**
It is a container that displays the difference between the current state of Entity and its previous state. The `entity`
property always reflects the current state. Still, methods ` get` and `has` methods of the snapshot return the data from
the previous state of the Entity before it was changed. So you can understand which components have been added and which
have been removed.

> ❗ It is important to note that changes in the same entity components' data will not be reflected in the snapshot, even
> if a manual invalidation of the entity has been triggered.

Snapshots are very handy when you need to get a component or tag in Entity, but now it is missing. Let's take a closer
look at it with our `ViewSystem` example.
**Example:**

```typescript
class ViewSystem extends IterativeSystem {
  // ...
  protected entityAdded = ({current}: EntitySnapshot) => {
    // When entity added to the Query that means that it has `View` 
    // component - one hundred percent! So we just need its current 
    // state. 
    this.container.addChild(current.get(View)!.view);
    this.updatePosition(current);
  };

  protected entityRemoved = ({previous}: EntitySnapshot) => {
    // But when entity removed - we can't be sure that current state 
    // of the entity has `View` component. So we need to get it from
    // the previous state. Previous state has it one hundred percent.
    this.container.removeChild(previous.get(View)!.view);
  };
  // ...
}
```

## Shared Config

In real life, there is often a need to have a single Entity that acts as a configuration for the whole world.

For example, you have a set of complex systems that involve both game logic and visualization, and animations. But for
functional test purposes - you don't care about the visuals and animations. You face the situation of passing a specific
flag in each system during initialization, which will be responsible for disabling animation and visualization.

Now imagine that you have several configuration parameters, and each of them you need to pass to all systems of your
world.

To simplify handling such situations - you can use `Engine.sharedConfig`. Shared Config is an `Entity` available in all
systems after adding them to `Engine`.

**Example:**

```typescript
const NO_VISUALS = 'no-visuals';

class ViewSystem extends IterativeSystem {
  protected updateEntity(entity: Entity): void {
    if (this.sharedConfig.has(NO_VISUALS)) {
      return;
    }

    // Otherwise - update visuals
  }
}

const engine = new Engine();
engine.sharedConfig.add(NO_VISUALS);
engine.addSystem(new ViewSystem());
```

> ☝ Shared Config is the single instance connected to `Engine` since its initialization and can't be removed from it. It
> affects queries like any regular `Entity`.

## How to work with linked components?

Tick-knock provides an extended API for working with linked components since version 4.0.0.

- Method `withdraw` removes the first LinkedComponent component of the provided type or existing standard component
- Method `pick` removes provided LinkedComponent component instance or existing standard component.

  **Example**
  You have a system responsible for checking boons (buffs) expiration, and you wish to remove expired boons from the
  hero:
  ```ts
  enum BoonType {
    PROTECTION,
    AEGIS,
    REGENERATION
  }

  class Boon extends LinkedComponent {
    public constructor(
        public readonly type: BoonType,
        public value: number,
        public duration: number
    ) { super(); }
  }

  class BoonExpirationTestSystem extends IterativeSystem {
    public constructor() {
      super((entity) => entity.has(Boon));
    }
    
    public updateEntity(entity: Entity, dt: number) {
      // Let's update all boons
      entity.iterate(Boon, (boon) => {
          // Let's reduce boon remaining duration
          boon.duration -= dt;
          // If boon is expired
          if (boon.duration <= 0) {
             // Then we need to removed it from the Entity
             // But `entity.remove` will remove all boons, so we need to cherry-pick
             entity.pick(boon);
          } 
      });
    }
  }
  ```
- Method `iterate` iterates over instances of LinkedComponent and performs the `action` over each. Works for standard
  components (action will be called for a single instance in this case).
  > 🎈 It's safe to `pick` only current entity during iteration.
- Method `find` searches a component instance of the specified class. Works for standard components (predicate will be
  called for a single instance in this case).
- Method `getAll` returns a generator that can be used for iteration over all instances of specific type components.
- Method `lengthOf` returns the number of existing components of the specified class.

Now you know the basics. Now let's look at some examples to help you understand when linked components are helpful and
how to work with them.

### Real world example

We want to get a system that handles "Regeneration" buff on the hero. There can be more than one sources of
regeneration, so we must handle all of them at the same time.

Regeneration has two effects:

- Instantly healing heroes by constant amount of health points
- Regenerates some amount of health over the time.

Thus, our system should do the following:

- Heal the hero on the adding every new Regeneration buff.
- Heal the hero over the time.
- Manages regeneration expiration.

```ts
class Regeneration extends LinkedComponent {
  public constructor(
    public instantHealValue: number,
    public healPerSecond: number,
    public duration: number
  ) { super(); }
}

class RegenerationSystem extends IterativeSystem {
  public constructor() {
    super((entity) => entity.has(Hero, Regeneration));
  }

  public updateEntity(entity: Entity, dt: number) {
    const hero = entity.get(Hero)!
    // Let's update all regeneration components on our hero and apply their effects 
    entity.iterate(Regeneration, (it) => {
      // We need to heal hero
      const healthPointsToAdd = Math.ceil(it.healPerSecond * dt);
      hero.health += healthPointsToAdd;
      // And then reduce regeneration duration
      it.duration -= dt;
      // If it's expired
      if (it.duration <= 0) {
        // Then we need to removed it from the Entity
        // But `entity.remove` will remove all boons, so we need to cherry-pick
        entity.pick(it);
      }
    });
  }

  protected entityAdded = ({current}: EntitySnapshot) => {
    // When new entity appears in the queue, that means that it has Hero and Regeneration
    // so we want to instantly heal the hero by existing Regeneration buffs
    current.iterate(Regeneration, (regeneration) => {
      this.instantlyHealHero(entity, regeneration);
    })
    // Also, if any additional Regeneration buff will appear in the entity, we will handle 
    // them as well and instantly heal the hero
    current.onComponentAdded.connect(this.instantlyHealHero);
  }

  protected entityRemoved = ({current}: EntitySnapshot) => {
    // We don't want to know if any new components were added to the entity when it left 
    // the queue already.
    current.onComponentAdded.disconnect(this.instantlyHealHero);
  }

  private instantlyHealHero = (entity: Entity, regeneration: any) => {
    // We need to filter components, because this function will called on every added 
    // component (not only Regeneration)
    if (!(regeneration instanceof Regeneration)) return;

    const hero = entity.get(Hero)!;
    hero.health += regeneration.instantHealValue;
  }

}
```

# Restrictions

## Shared and Local Queries

In real development, you'll definitely face a situation when you want to reuse Query.

For example, when developing a game with heroes and enemies, you will surely always need two queries:

**Simplified version**

```typescript
const heroes = new Query(entity => entity.has(Hero));
const enemies = new Query(entity => entity.has(Enemy));
```

And you will want to use them in different systems. But the systems use local Queries. This means that after excluding a
system from Engine, the Query in it will no longer be updated.

To prevent this from happening, you need to use the shared queries approach. To do this, you only need to add the query
manually after initializing the Engine.

> shared-queries.ts

```typescript
export const heroes = new Query(entity => entity.has(Hero));
export const enemies = new Query(entity => entity.has(Enemy));
```

```typescript
import {heroes, enemies} from 'shared-queries';
// ...
engine.addQuery(heroes);
engine.addQuery(enemies)
```

Now you can use these Queries in any other system.

**Example:**

```typescript
import {heroes, enemies} from 'shared-queries';

class DamageSystem extends IterativeSystem {
  // ...
  protected updateEntity(entity: Entity) {
    const damage = entity.remove(Damage)!
    const isHero = heroes.has(entity);
    if (damage.type === DamageType.SPLASH) {
      const neighbours = getNeighbours(isHero ? heroes : enemies);
      // ...
    }
  }
}
```

## Queries with complex logic and Entity invalidation

There are limitations for Query that do not allow you to track changes made inside components automatically.

Suppose that you want Query to track entities with an X position of 10.

```typescript
const query = new Query((entity) => entity.has(Position) && entity.get(Position).x === 10);
```

And you have changed the Position parameters accordingly:

```typescript
entity.get(Position)!.x = 10;
```

The query will not know about these changes because the mechanism for tracking changes in component fields is redundant
and heavy, which will have a huge impact on performance. But to fix this, you can use an entity method
called `invalidate`, it will force Query to check this particular entity.

❗ Try not to use this approach too often. It may affect the performance of your application.

# License

This software released under [MIT](https://github.com/Leopotam/ecs/blob/master/LICENSE.md) license! Good luck, folks.

[Restrictions]: #restrictions

[Shared Config]: #shared-config

[Shared and Local Queries]: #shared-and-local-queries

[Queries with complex logic and Entity invalidation]: #queries-with-complex-logic-and-entity-invalidation

[Snapshot]: #snapshot

[IterativeSystem]: #iterativesystem

[ReactionSystem]: #reactionsystem

[Built-in query-based systems]: #built-in-query-based-systems

[Queries and Systems]: #queries-and-systems

[QueryBuilder]: #querybuilder

[Query]: #query

[System]: #system

[Entity]: #entity

[Tag]: #tag

[Component]: #component

[Linked Component]: #linked-component

[Linked Components How-To]: #how-to-work-with-linked-components

[Installing]: #installing

[How it works?]: #how-it-works

[Inside the Tick-Knock]: #inside-the-tick-knock

[Subscription]: #subscription

[Engine]: #engine

[License]: #license


================================================
FILE: jest-ci.json
================================================
{
  "transform": {
    "^.+\\.tsx?$": "ts-jest"
  },
  "collectCoverage": true,
  "moduleFileExtensions": [
    "ts",
    "js"
  ],
  "testMatch": [
    "**/tests/unit/**/*.spec.(js|ts)|**/__tests__/*.(js|ts)"
  ],
  "transformIgnorePatterns": [
    "/node_modules/"
  ],
  "moduleNameMapper": {
    "^@/(.*)$": "<rootDir>/src/$1"
  }
}

================================================
FILE: package.json
================================================
{
  "name": "tick-knock",
  "version": "4.3.0",
  "description": "TypeScript Entity-Component-System library",
  "author": "Ilya Malanin",
  "license": "MIT",
  "main": "lib/index.js",
  "typings": "lib/index.d.ts",
  "scripts": {
    "setup": "yarn install",
    "build": "tsc",
    "build-watch": "tsc --watch",
    "test": "jest",
    "test-ci": "jest --config jest-ci.json"
  },
  "devDependencies": {
    "@types/jest": "^29.2.4",
    "@types/node": "^18.11.17",
    "jest": "^29.3.1",
    "ts-jest": "^29.0.3",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.5"
  },
  "dependencies": {
    "tslib": "^2.4.0"
  },
  "files": [
    "lib"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mayakwd/tick-knock.git"
  },
  "bugs": {
    "url": "https://github.com/mayakwd/tick-knock/issues"
  },
  "homepage": "https://github.com/mayakwd/tick-knock#readme",
  "keywords": [
    "ecs",
    "entity",
    "typescript",
    "entity-component-system",
    "gamedev",
    "game"
  ],
  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "js"
    ],
    "testMatch": [
      "**/tests/unit/**/*.spec.(js|ts)|**/__tests__/*.(js|ts)"
    ],
    "transformIgnorePatterns": [
      "/node_modules/"
    ],
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}


================================================
FILE: src/ecs/ComponentId.ts
================================================
import {Class} from '../utils/Class';

/**
 * Gets an id for a component class.
 *
 * @param component Component class
 * @param createIfNotExists - If `true` then unique id for class component will be created,
 *  in case if it wasn't assigned earlier
 */
export function getComponentId<T>(
  component: Class<T>,
  createIfNotExists: boolean = false,
): number | undefined {
  if (component.hasOwnProperty(COMPONENT_CLASS_ID)) {
    return (component as ComponentId<T>)[COMPONENT_CLASS_ID];
  } else if (createIfNotExists) {
    return (component as ComponentId<T>)[COMPONENT_CLASS_ID] = componentClassId++;
  }
  return undefined;
}

/**
 * @internal
 */
export function getComponentClass<T extends K, K>(component: NonNullable<T>, resolveClass?: Class<K>) {
  let componentClass = Object.getPrototypeOf(component).constructor as Class<T>;
  if (resolveClass) {
    if (!(component instanceof resolveClass || componentClass === resolveClass)) {
      throw new Error('Resolve class should be an ancestor of component class');
    }
    componentClass = resolveClass as Class<T>;
  }
  return componentClass;
}

let COMPONENT_CLASS_ID = '__componentClassId__';
let componentClassId: number = 1;

type ComponentId<T> = Class<T> & {
  [key: string]: number;
};



================================================
FILE: src/ecs/Engine.ts
================================================
import {Entity} from './Entity';
import {System} from './System';
import {Class} from '../utils/Class';
import {Query} from './Query';
import {Subscription} from './Subscription';
import {Signal} from '../utils/Signal';

/**
 * Engine represents game state, and provides entities update loop on top of systems.
 */
export class Engine {
  /**
   * Signal dispatches when new entity were added to engine
   */
  public onEntityAdded: Signal<(entity: Entity) => void> = new Signal();
  /**
   * Signal dispatches when entity was removed from engine
   */
  public onEntityRemoved: Signal<(entity: Entity) => void> = new Signal();

  private _entityMap: Map<number, Entity> = new Map();
  private _entities: Entity[] = [];
  private _systems: System[] = [];
  private _queries: Query[] = [];
  private _subscriptions: Subscription<any>[] = [];
  private _sharedConfig: Entity = new Entity();
  private _removalRequested: Set<number> = new Set();

  /**
   * Gets a list of entities added to engine
   */
  public get entities(): ReadonlyArray<Entity> {
    return Array.from(this._entities);
  }

  /**
   * Gets a list of systems added to engine
   */
  public get systems(): ReadonlyArray<System> {
    return this._systems;
  }

  /**
   * Gets a list of queries added to engine
   */
  public get queries(): ReadonlyArray<Query> {
    return this._queries;
  }

  public constructor() {
    this.connectEntity(this._sharedConfig);
  }

  /**
   * @internal
   */
  public get subscriptions(): ReadonlyArray<Subscription<any>> {
    return this._subscriptions;
  }

  /**
   * Gets a shared config entity, that's accessible from every system added to engine
   *
   * @return {Entity}
   */
  public get sharedConfig(): Entity {
    return this._sharedConfig;
  }

  /**
   * Adds an entity to engine.
   * If entity is already added to engine - it does nothing.
   *
   * @param entity Entity to add to engine
   * @see onEntityAdded
   */
  public addEntity(entity: Entity): Engine {
    if (this._entityMap.has(entity.id)) {
      this._removalRequested.delete(entity.id);
      return this;
    }
    this._entities.push(entity);
    this._entityMap.set(entity.id, entity);
    this.onEntityAdded.emit(entity);
    this.connectEntity(entity);
    return this;
  }

  /**
   * Remove entity from engine
   * If engine not contains entity - it does nothing.
   *
   * @param entity Entity to remove from engine
   * @param safe If true - entity will be removed after update loop, if false - entity is removed immediately.
   * @since 4.3.0 - Added `safe` option.
   *  The "safe" flag will be removed in the next major version release, and the default behavior will be changed to "safe".
   * @see onEntityRemoved
   */
  public removeEntity(entity: Entity, safe: boolean = false): Engine {
    if (!this._entityMap.has(entity.id)) return this;
    if (!safe) {
      return this.removeEntityNow(entity);
    }
    this._removalRequested.add(entity.id)
    return this;
  }

  /**
   * Gets an entity by its id
   *
   * @param {number} id Entity identifier
   * @return {Entity | undefined} corresponding entity or undefined if it's not found.
   */
  public getEntityById(id: number): Entity | undefined {
    if (this._removalRequested.has(id)) return undefined;
    return this._entityMap.get(id);
  }

  /**
   * Removes a system from engine
   * Avoid remove the system during update cycle, do it only if your sure what you are doing.
   * Note: {@link IterativeSystem} has aware guard during update loop, if system removed - updating is being stopped.
   *
   * @param system System to remove
   */
  public removeSystem(system: System): Engine {
    const index = this._systems.indexOf(system);
    if (index === -1) return this;
    this._systems.splice(index, 1);
    system.onRemovedFromEngine();
    system.setEngine(undefined);
    return this;
  }

  /**
   * Updates the engine. This cause updating all the systems in the engine in the order of priority they've been added.
   *
   * @param dt Delta time in seconds
   */
  public update(dt: number): void {
    for (const system of this._systems) {
      system.update(dt);
      if (system.isRemovalRequested) {
        this.removeSystem(system);
      }
    }
    if (this._removalRequested.size > 0) {
      for (const id of this._removalRequested) {
        const entity = this._entityMap.get(id);
        if (entity) {
          this.removeEntityNow(entity);
        }
      }
      this._removalRequested.clear();
    }
  }

  /**
   * Gets a system of the specific class
   *
   * @param systemClass Class of the system that should be found
   */
  public getSystem<T extends System>(systemClass: Class<T>): T | undefined {
    return this._systems.find(value => value instanceof systemClass) as T;
  }

  /**
   * Remove all systems
   */
  public removeAllSystems(): void {
    const systems = this._systems;
    this._systems = [];
    for (const system of systems) {
      system.onRemovedFromEngine();
    }
  }

  /**
   * Remove all queries.
   * After remove all queries will be cleared.
   */
  public removeAllQueries(): void {
    const queries = this._queries;
    this._queries = [];
    for (const query of queries) {
      this.disconnectQuery(query);
      query.clear();
    }
  }

  /**
   * Remove all entities.
   * onEntityRemoved will be fired for every entity.
   */
  public removeAllEntities(): void {
    this.removeAllEntitiesInternal(false);
  }

  /**
   * Removes all entities, queries and systems.
   * All entities will be removed silently, {@link onEntityRemoved} event will not be fired.
   * Queries will be cleared.
   */
  public clear(): void {
    this.removeAllEntitiesInternal(true);
    this.removeAllSystems();
    this.removeAllQueries();
  }

  private removeEntityNow(entity: Entity): Engine {
    const index = this._entities.indexOf(entity);
    this._entities.splice(index, 1);
    this._entityMap.delete(entity.id);
    this.onEntityRemoved.emit(entity);
    this.disconnectEntity(entity);

    return this;
  }

  /**
   * Adds a query to engine. It matches all available in engine entities with query.
   *
   * When any entity will be added, removed, their components will be modified - this query will be updated,
   * until not being removed from engine.
   *
   * @param query Entity match query
   */
  public addQuery(query: Query): Engine {
    this.connectQuery(query);
    query.matchEntities(this.entities);
    this._queries[this._queries.length] = query;
    return this;
  }

  /**
   * Adds a system to engine, and set its priority inside of engine update loop.
   *
   * @param system System to add to the engine
   * @param priority Value indicating the priority of updating system in update loop. Lower priority
   *  means sooner update.
   */
  public addSystem(system: System, priority: number = 0): Engine {
    system.setPriority(priority);
    if (this._systems.length === 0) {
      this._systems[0] = system;
    } else {
      const index = this._systems.findIndex(value => value.priority > priority);
      if (index === -1) {
        this._systems[this._systems.length] = system;
      } else {
        this._systems.splice(index, 0, system);
      }
    }
    system.setEngine(this);
    system.onAddedToEngine();

    return this;
  }

  /**
   * Removes a query and clear it.
   *
   * @param query Entity match query
   */
  public removeQuery(query: Query) {
    const index = this._queries.indexOf(query);
    if (index == -1) return undefined;
    this._queries.splice(index, 1);
    this.disconnectQuery(query);
    query.clear();
    return this;
  }

  /**
   * Subscribe to any message of the {@link messageType}.
   * Those messages can be dispatched from any system attached to the engine
   *
   * @param {Class<T> | T} messageType - Message type (can be class or any instance, for example string or number)
   * @param {(value: T) => void} handler - Handler for the message
   */
  public subscribe<T>(messageType: Class<T> | T, handler: (value: T) => void): Subscription<T> {
    return this.addSubscription(messageType, handler);
  }

  /**
   * Unsubscribe from messages of specific type
   *
   * @param {Class<T>} messageType - Message type
   * @param {(value: T) => void} handler - Specific handler that must be unsubscribed, if not defined then all handlers
   *  related to this message type will be unsubscribed.
   */
  public unsubscribe<T>(messageType: Class<T> | T, handler?: (value: T) => void): void {
    this.removeSubscription(messageType, handler);
  }

  /**
   * Unsubscribe from all type of messages
   */
  public unsubscribeAll(): void {
    this._subscriptions.length = 0;
  }

  /**
   * @internal
   */
  public addSubscription<T>(messageType: Class<T> | T, handler: (value: T) => void): Subscription<T> {
    for (const subscription of this._subscriptions) {
      if (subscription.equals(messageType, handler)) return subscription;
    }
    const subscription = new Subscription<T>(messageType, handler);
    this._subscriptions.push(subscription);
    return subscription;
  }

  /**
   * @internal
   */
  public removeSubscription<T>(messageType: Class<T> | T, handler: ((value: T) => void) | undefined): void {
    let i = this._subscriptions.length;
    while (--i >= 0) {
      const subscription = this._subscriptions[i];
      if (subscription.equals(messageType, handler)) {
        this._subscriptions.splice(i, 1);
        if (handler !== undefined) return;
      }
    }
  }

  /**
   * @internal
   */
  public dispatch<T>(message: T) {
    for (const subscription of this._subscriptions) {
      if ((typeof subscription.messageType === 'function' && message instanceof subscription.messageType) || message === subscription.messageType) {
        subscription.handler(message);
      }
    }
  }

  private connectEntity(entity: Entity) {
    entity.onComponentAdded.connect(this.onComponentAdded, Number.POSITIVE_INFINITY);
    entity.onComponentRemoved.connect(this.onComponentRemoved, Number.POSITIVE_INFINITY);
    entity.onInvalidationRequested.connect(this.onInvalidationRequested, Number.NEGATIVE_INFINITY);
  }

  private disconnectEntity(entity: Entity) {
    entity.onComponentAdded.disconnect(this.onComponentAdded);
    entity.onComponentRemoved.disconnect(this.onComponentRemoved);
    entity.onInvalidationRequested.disconnect(this.onInvalidationRequested);
  }

  private connectQuery(query: Query) {
    this.onEntityAdded.connect(query.entityAdded);
    this.onEntityRemoved.connect(query.entityRemoved);
  }

  private disconnectQuery(query: Query) {
    this.onEntityAdded.disconnect(query.entityAdded);
    this.onEntityRemoved.disconnect(query.entityRemoved);
  }

  private removeAllEntitiesInternal(silently: boolean): void {
    const entities = this._entities;
    this._entities = [];
    this._entityMap.clear();
    for (const entity of entities) {
      if (!silently) {
        this.onEntityRemoved.emit(entity);
      }
      this.disconnectEntity(entity);
    }
  }

  private onComponentAdded = <T>(entity: Entity, component: NonNullable<T>, componentClass?: Class<NonNullable<T>>) => {
    this._queries.forEach(value => value.entityComponentAdded(entity, component, componentClass));
  };

  private onInvalidationRequested = (entity: Entity) => {
    this._queries.forEach(value => value.validateEntity(entity));
  };

  private onComponentRemoved = <T>(entity: Entity, component: NonNullable<T>, componentClass?: Class<NonNullable<T>>) => {
    this._queries.forEach(value => value.entityComponentRemoved(entity, component, componentClass));
  };
}


================================================
FILE: src/ecs/Entity.ts
================================================
import {getComponentClass, getComponentId} from './ComponentId';
import {Class} from '../utils/Class';
import {Signal} from '../utils/Signal';
import {isTag, Tag} from './Tag';
import {ILinkedComponent, isLinkedComponent} from './LinkedComponent';
import {LinkedComponentList} from './LinkedComponentList';

/**
 * Entity readonly interface
 */
export interface ReadonlyEntity {
  /**
   * The signal dispatches if new component or tag was added to the entity
   */
  readonly onComponentAdded: Signal<ComponentUpdateHandler>;
  /**
   * The signal dispatches if component was removed from the entity
   */
  readonly onComponentRemoved: Signal<ComponentUpdateHandler>;
  /**
   * Returns components map, where key is component identifier, and value is a component itself
   * @see {@link getComponentId}, {@link Entity.getComponents}
   */
  readonly components: Readonly<Record<number, unknown>>;
  /**
   * Returns set of tags applied to the entity
   * @see getComponentId
   */
  readonly tags: ReadonlySet<Tag>;

  /**
   * Returns value indicating whether entity has a specific component or tag
   *
   * @param {Class | Tag} componentClassOrTag
   * @param id Identifier of the LinkedComponent
   * @example
   * ```ts
   * const BERSERK = 10091;
   * if (!entity.has(Immobile) || entity.has(BERSERK)) {
   *   const position = entity.get(Position)!;
   *   position.x += 1;
   * }
   * ```
   */
  has<T>(componentClassOrTag: Class<T> | Tag, id?: string): boolean;

  /**
   * Returns value indicating whether entity contains a component instance.
   * If the component is an instance of ILinkedComponent then all components of its type will be checked for equality.
   *
   * @param {T} component
   * @param {Class<K>} resolveClass
   * @example
   * ```ts
   * const boon = new Boon(BoonType.HEAL);
   * entity
   *   .append(new Boon(BoonType.PROTECTION));
   *   .append(boon);
   *
   * if (entity.contains(boon)) {
   *   logger.info('Ah, sweet. We have not only protection but heal as well!');
   * }
   * ```
   */
  contains<T extends K, K>(component: T, resolveClass?: Class<K>): boolean;

  /**
   * Returns value indicating whether entity has a specific component
   *
   * @param component
   * @param id Identifier of the LinkedComponent
   * @example
   * ```
   * if (!entity.hasComponent(Immobile)) {
   *   const position = entity.get(Position)!;
   *   position.x += 1;
   * }
   * ```
   */
  hasComponent<T>(component: Class<T>, id?: string): boolean;

  /**
   * Returns value indicating whether entity has a specific tag
   *
   * @param tag
   * @example
   * ```ts
   * const BERSERK = "berserk";
   * let damage = initialDamage;
   * if (entity.hasTag(BERSERK)) {
   *   damage *= 1.2;
   * }
   * ```
   */
  hasTag(tag: Tag): boolean;

  /**
   * Returns value indicating whether entity have any of specified components/tags
   *
   * @param {Class<unknown> | Tag} componentClassOrTag
   * @returns {boolean}
   * @example
   * ```ts
   * const IMMORTAL = "immortal";
   * if (!entity.hasAny(Destroy, Destroying, IMMORTAL)) {
   *   entity.add(new Destroy());
   * }
   * ```
   */
  hasAny(...componentClassOrTag: Array<Class<unknown> | Tag>): boolean;

  /**
   * Returns value indicating whether entity have all of specified components/tags
   *
   * @param {Class<unknown> | Tag} componentClassOrTag
   * @returns {boolean}
   * @example
   * ```ts
   * const I_LOVE_GRAVITY = "no-i-don't";
   * if (entity.hasAll(Position, Acceleration, I_LOVE_GRAVITY)) {
   *   entity.get(Position)!.y += entity.get(Acceleration)!.y * dt;
   * }
   * ```
   */
  hasAll(...componentClassOrTag: Array<Class<unknown> | Tag>): boolean;

  /**
   * Returns an array of entity components
   *
   * @returns {unknown[]}
   */
  getComponents(): unknown[];

  /**
   * Returns an array of tags applied to the entity
   */
  getTags(): Tag[];

  /**
   * Gets a component instance if it's exists in the entity, otherwise returns `undefined`
   * - If you want to check presence of the tag then use {@link has} instead.
   *
   * @param componentClass Specific component class
   * @param id Identifier of the LinkedComponent
   */
  get<T>(componentClass: Class<T>, id?: string): T | undefined;

  /**
   * Iterates over instances of linked component appended to the Entity and performs the action over each.<br>
   * Works and for standard components (action will be called for a single instance in this case).
   *
   * @param {Class<T>} componentClass Component`s class
   * @param {(component: T) => void} action Action to perform over every component instance.
   * @example
   * ```ts
   * class Boon extends LinkedComponent {
   *   public constructor(
   *     public type: BoonType,
   *     public duration: number
   *   ) { super(); }
   * }
   * const entity = new Entity()
   *   .append(new Boon(BoonType.HEAL, 2))
   *   .append(new Boon(BoonType.PROTECTION, 3);
   *
   * // Let's decrease every boon duration and remove them if they are expired.
   * entity.iterate(Boon, (boon) => {
   *   if (--boon.duration <= 0) {
   *      entity.pick(boon);
   *   }
   * });
   * ```
   */
  iterate<T>(componentClass: Class<T>, action: (component: T) => void): void;

  /**
   * Returns generator with all instances of specified linked component class
   *
   * @param {Class<T>} componentClass Component`s class
   * @example
   * ```ts
   * for (const damage of entity.linkedComponents(Damage)) {
   *   if (damage.value < 0) {
   *   throw new Error('Damage value can't be less than zero');
   * }
   * ```
   */
  getAll<T>(componentClass: Class<T>): Generator<T, void, T>;

  /**
   * Searches a component instance of specified linked component class.
   * Works and for standard components (predicate will be called for a single instance in this case).
   *
   * @param {Class<T>} componentClass
   * @param {(component: T) => boolean} predicate
   * @return {T | undefined}
   */
  find<T>(componentClass: Class<T>, predicate: (component: T) => boolean): T | undefined;

  /**
   * Returns number of components of specified class.
   *
   * @param {Class<T>} componentClass
   * @return {number}
   */
  lengthOf<T>(componentClass: Class<T>): number;
}

/**
 * Entity is a general purpose object, which can be marked with tags and can contain different components.
 * So it is just a container, that can represent any in-game entity, like enemy, bomb, configuration, game state, etc.
 *
 * @example
 * ```ts
 * // Here we can see structure of the component "Position", it's just a data that can be attached to the Entity
 * // There is no limits for component`s structure.
 * // Components mustn't hold the reference to the entity that it attached to.
 *
 * class Position {
 *   public x:number;
 *   public y:number;
 *
 *   public constructor(x:number = 0, y:number = 0) {
 *     this.x = x;
 *     this.y = y;
 *   }
 * }
 *
 * // We can mark an entity with the tag OBSTACLE. Tag can be represented as a number or string.
 * const OBSTACLE = 10100;
 *
 * const entity = new Entity()
 *  .add(OBSTACLE)
 *  .add(new Position(10, 5));
 * ```
 */
export class Entity implements ReadonlyEntity {
  /**
   * The signal dispatches if new component or tag was added to the entity. Works for every linked component as well.
   */
  public readonly onComponentAdded: Signal<ComponentUpdateHandler> = new Signal();
  /**
   * The signal dispatches if component was removed from the entity. Works for every linked component as well.
   */
  public readonly onComponentRemoved: Signal<ComponentUpdateHandler> = new Signal();
  /**
   * The signal dispatches that invalidation requested for this entity.
   * Which means that if the entity attached to the engine — its queries will be updated.
   *
   * Use {@link Entity.invalidate} method in case if in query test function is using component properties or complex
   * logic.
   *
   * Only adding/removing components and tags are tracked by Engine. So you need to request queries invalidation
   * manually, if some of your queries depends on logic or component`s properties.
   */
  public readonly onInvalidationRequested: Signal<(entity: Entity) => void> = new Signal();

  /**
   * Unique id identifier
   */
  public readonly id = entityId++;

  private _components: Record<number, unknown> = {};
  private _linkedComponents: Record<number, LinkedComponentList<ILinkedComponent>> = {};
  private _tags: Set<Tag> = new Set();

  /**
   * Returns components map, where key is component identifier, and value is a component itself
   * @see {@link getComponentId}, {@link Entity.getComponents}
   */
  public get components(): Readonly<Record<number, unknown>> {
    return this._components;
  }

  /**
   * Returns set of tags applied to the entity
   * @see getComponentId
   */
  public get tags(): ReadonlySet<Tag> {
    return this._tags;
  }

  /**
   * Adds a component or tag to the entity.
   * It's a unified shorthand for {@link addComponent} and {@link addTag}.
   *
   * - If a component of the same type already exists in entity, it will be replaced by the passed one (only if
   *  component itself is not the same, in this case - no actions will be done).
   * - If the tag is already present in the entity - no actions will be done.
   * - During components replacement {@link onComponentRemoved} and {@link onComponentAdded} are will be triggered
   *  sequentially.
   * - If there is no component of the same type, or the tag is not present in the entity - then only
   * - If the passed component is an instance of ILinkedComponent then all existing instances will be removed, and the
   *  passed instance will be added to the Entity. {@link onComponentRemoved} will be triggered for every removed
   *  instance and {@link onComponentAdded} will be triggered for the passed component.
   * - Linked component always replaces all existing instances. Even if the passed instance already exists in the
   *  Entity - all existing linked components will be removed anyway, and replaced with the passed one.
   *
   * @throws Throws error if component is null or undefined, or if component is not an instance of the class as well
   * @param {T | Tag} componentOrTag Component instance or Tag
   * @param {K} resolveClass Class that should be used as resolving class.
   *  Passed class always should be an ancestor of Component's class.
   *  It has sense only if component instance is passed, but not the Tag.
   * @returns {Entity} Reference to the entity itself. It helps to build chain of calls.
   * @see {@link addComponent, appendComponent}, {@link addTag}
   * @example
   * ```ts
   * const BULLET = 1;
   * const EXPLOSIVE = "explosive";
   * const entity = new Entity()
   *  .add(new Position())
   *  .add(new View())
   *  .add(new Velocity())
   *  .add(BULLET)
   *  .add(EXPLOSIVE);
   * ```
   */
  public add<T extends K, K extends unknown>(componentOrTag: NonNullable<T> | Tag, resolveClass?: Class<K>): Entity {
    if (isTag(componentOrTag)) {
      this.addTag(componentOrTag);
    } else {
      this.addComponent(componentOrTag, resolveClass);
    }
    return this;
  }

  /**
   * Appends a linked component to the entity.
   *
   * - If linked component is not exists, then it will be added to the Entity and {@link onComponentAdded}
   * will be triggered.
   * - If component already exists in the entity, then passed one will be appended to the tail. {@link onComponentAdded}
   *  will be triggered as well.
   *
   * It's a shorthand to {@link appendComponent}
   *
   * @throws Throws error if component is null or undefined, or if component is not an instance of the class as well
   * @param {T | Tag} component ILinkedComponent instance
   * @param {K} resolveClass Class that should be used as resolving class.
   *  Passed class always should be an ancestor of Component's class.
   *
   * @returns {Entity} Reference to the entity itself. It helps to build chain of calls.
   * @see {@link addComponent}
   * @see {@link appendComponent}
   * @example
   * ```ts
   * const damage = new Damage();
   * const entity = new Entity()
   *  .append(new Damage(1))
   *  .append(new Damage(2))
   *
   *  const damage = entity.get(Damage);
   *  while (entity.has(Damage)) {
   *    const entity = entity.withdraw(Damage);
   *    print(damage.value);
   *  }
   * ```
   */
  public append<T extends K, K extends ILinkedComponent>(component: NonNullable<T>, resolveClass?: Class<K>): Entity {
    return this.appendComponent(component, resolveClass);
  }

  /**
   * Removes first appended linked component instance of the specified type.
   * Unlike {@link remove} and {@link removeComponent} remaining linked components stays in the Entity.
   *
   * - If linked component exists in the Entity, then it will be removed from Entity and {@link onComponentRemoved}
   * will be triggered.
   *
   * @param {Class<T>} componentClass
   * @return {T | undefined} Component instance if any of the specified type exists in the entity, otherwise undefined
   * @example
   * ```ts
   * const entity = new Entity()
   *   .append(new Damage(1))
   *   .append(new Damage(2))
   *   .append(new Damage(3));
   *
   * entity.withdraw(Damage);
   * entity.iterate(Damage, (damage) => {
   *   print('Remaining damage: ' + damage.value);
   * });
   *
   * // Remaining damage: 2
   * // Remaining damage: 3
   * ```
   */
  public withdraw<T>(componentClass: Class<T>): T | undefined {
    const component = this.get(componentClass);
    if (component === undefined) return;
    if (isLinkedComponent(component)) {
      return this.withdrawComponent(component, componentClass as Class<ILinkedComponent>);
    } else {
      return this.remove(componentClass);
    }
  }

  /**
   * Removes particular linked component instance from the Entity by its id.
   *
   * - If linked component instance exists in the Entity, then it will be removed from Entity and
   * {@link onComponentRemoved} will be triggered.
   *
   * @param {Class<K>} resolveClass Resolve class
   * @param {string} id Linked component id
   * @return {T | undefined} Component instance if it exists in the entity, otherwise undefined
   */
  public pick<T extends ILinkedComponent>(resolveClass: Class<T>, id: string): T | undefined;
  /**
   * Removes particular linked component instance from the Entity.
   *
   * - If linked component instance exists in the Entity, then it will be removed from Entity and
   * {@link onComponentRemoved} will be triggered.
   *
   * @param {NonNullable<T>} component Linked component instance
   * @param {Class<K> | undefined} resolveClass Resolve class
   * @return {T | undefined} Component instance if it exists in the entity, otherwise undefined
   */
  public pick<T>(component: NonNullable<T>, resolveClass?: Class<T>): T | undefined;
  public pick<T>(componentOrResolveClass: NonNullable<T> | Class<T>, resolveClassOrId?: Class<T> | string): T | undefined {
    if (typeof resolveClassOrId === 'string') {
      const component = this.find<T>(componentOrResolveClass as Class<T>, (component) => isLinkedComponent(component) && component.id === resolveClassOrId);
      if (isLinkedComponent(component)) {
        return this.withdrawComponent(component, componentOrResolveClass as Class<ILinkedComponent>);
      }
      return undefined;
    }
    if (isLinkedComponent(componentOrResolveClass)) {
      return this.withdrawComponent(componentOrResolveClass, resolveClassOrId as Class<ILinkedComponent>);
    }
    return this.remove(resolveClassOrId ?? getComponentClass(componentOrResolveClass as NonNullable<T>));
  }

  /**
   * Adds a component to the entity.
   *
   * - If a component of the same type already exists in entity, it will be replaced by the passed one (only if
   *  component itself is not the same, in this case - no actions will be done).
   * - During components replacement {@link onComponentRemoved} and {@link onComponentAdded} are will be triggered
   *  sequentially.
   * - If there is no component of the same type - then only {@link onComponentAdded} will be triggered.
   *
   * @throws Throws error if component is null or undefined, or if component is not an instance of the class as well
   * @param {T} component Component instance
   * @param {K} resolveClass Class that should be used as resolving class.
   *  Passed class always should be an ancestor of Component's class.
   * @returns {Entity} Reference to the entity itself. It helps to build chain of calls.
   * @see {@link add}, {@link addTag}
   * @example
   * ```ts
   * const BULLET = 1;
   * const entity = new Entity()
   *  .addComponent(new Position())
   *  .addComponent(new View())
   *  .add(BULLET);
   * ```
   */
  public addComponent<T extends K, K extends unknown>(component: NonNullable<T>, resolveClass?: Class<K>): Entity {
    const componentClass = getComponentClass(component, resolveClass);
    const id = getComponentId(componentClass, true)!;
    const linkedComponent = isLinkedComponent(component);
    if (this._components[id] !== undefined) {
      if (!linkedComponent && component === this._components[id]) {
        return this;
      }
      this.remove(componentClass);
    }
    if (linkedComponent) {
      this.append(component as ILinkedComponent, resolveClass as Class<ILinkedComponent>);
    } else {
      this._components[id] = component;
      this.dispatchOnComponentAdded(component);
    }
    return this;
  }

  /**
   * Appends a linked component to the entity.
   *
   * - If linked component is not exists, then it will be added via `addComponent` method and {@link onComponentAdded}
   * will be triggered.
   * - If component already exists in the entity, then passed one will be appended to the tail. {@link
    * onComponentAdded} won't be triggered.
   *
   * @throws Throws error if component is null or undefined, or if component is not an instance of the class as well
   * @param {T | Tag} component ILinkedComponent instance
   * @param {K} resolveClass Class that should be used as resolving class.
   *  Passed class always should be an ancestor of Component's class.
   *
   * @returns {Entity} Reference to the entity itself. It helps to build chain of calls.
   * @see {@link append}
   * @see {@link addComponent}
   * @example
   * ```ts
   * const damage = new Damage();
   * const entity = new Entity()
   *  .append(new Damage())
   *  .append(new Damage())
   *
   *  const damage = entity.get(Damage);
   *  while (damage !== undefined) {
   *    print(damage.value);
   *    damage = damage.next;
   *  }
   * ```
   */
  public appendComponent<T extends K, K extends ILinkedComponent>(component: NonNullable<T>, resolveClass?: Class<K>): Entity {
    const componentClass = getComponentClass(component, resolveClass);
    const componentId = getComponentId(componentClass, true)!;
    const componentList = this.getLinkedComponentList(componentId)!;
    componentList.add(component);
    if (this._components[componentId] === undefined) {
      this._components[componentId] = componentList.head;
    }
    this.dispatchOnComponentAdded(component);
    return this;
  }

  /**
   * Adds a tag to the entity.
   *
   * - If the tag is already present in the entity - no actions will be done.
   * - If there is such tag in the entity then {@link onComponentAdded} will be triggered.
   *
   * @param {Tag} tag Tag
   * @returns {Entity} Reference to the entity itself. It helps to build chain of calls.
   * @see {@link add}, {@link addComponent}
   * @example
   * ```ts
   * const DEVELOPER = "developer;
   * const EXHAUSTED = 2;
   * const  = "game-over";
   * const entity = new Entity()
   *  .addTag(DEVELOPER)
   *  .add(EXHAUSTED)
   * ```
   */
  public addTag(tag: Tag): Entity {
    if (!this._tags.has(tag)) {
      this._tags.add(tag);
      this.dispatchOnComponentAdded(tag);
    }
    return this;
  }

  /**
   * Returns componentClassOrTag indicating whether entity has a specific component or tag
   *
   * @param componentClassOrTag
   * @param id Identifier of the LinkedComponent
   * @example
   * ```ts
   * const BERSERK = 10091;
   * if (!entity.has(Immobile) || entity.has(BERSERK)) {
   *   const position = entity.get(Position)!;
   *   position.x += 1;
   * }
   * ```
   */
  public has<T>(componentClassOrTag: Class<T> | Tag, id?: string): boolean {
    if (isTag(componentClassOrTag)) {
      return this.hasTag(componentClassOrTag);
    }
    return this.hasComponent(componentClassOrTag, id);
  }

  /**
   * Returns value indicating whether entity contains a component instance.
   * If the component is an instance of ILinkedComponent then all components of its type will be checked for equality.
   *
   * @param {NonNullable<T>} component
   * @param {Class<K>} resolveClass
   * @example
   * ```ts
   * const boon = new Boon(BoonType.HEAL);
   * entity
   *   .append(new Boon(BoonType.PROTECTION));
   *   .append(boon);
   *
   * if (entity.contains(boon)) {
   *   logger.info('Ah, sweet. We have not only protection but heal as well!');
   * }
   * ```
   */
  public contains<T extends K, K>(component: NonNullable<T>, resolveClass?: Class<K>): boolean {
    const componentClass = getComponentClass(component, resolveClass);
    if (isLinkedComponent(component)) {
      return this.find(componentClass, (value) => value === component) !== undefined;
    }
    return this.get(componentClass) === component;
  }

  /**
   * Returns value indicating whether entity has a specific component
   *
   * @param component Component class
   * @param id Identifier of the LinkedComponent
   *
   * @example
   * ```
   * if (!entity.hasComponent(Immobile)) {
   *   const position = entity.get(Position)!;
   *   position.x += 1;
   * }
   * ```
   */
  public hasComponent<T>(component: Class<T>, id?: string): boolean {
    return this.get(component, id) !== undefined;
  }

  /**
   * Returns value indicating whether entity has a specific tag
   *
   * @param tag
   * @example
   * ```ts
   * const BERSERK = "berserk";
   * let damage = initialDamage;
   * if (entity.hasTag(BERSERK)) {
   *   damage *= 1.2;
   * }
   * ```
   */
  public hasTag(tag: Tag): boolean {
    return this._tags.has(tag);
  }

  /**
   * Returns value indicating whether entity have any of specified components/tags
   *
   * @param {Class<unknown> | Tag} componentClassOrTag
   * @returns {boolean}
   * @example
   * ```ts
   * const IMMORTAL = "immortal";
   * if (!entity.hasAny(Destroy, Destroying, IMMORTAL)) {
   *   entity.add(new Destroy());
   * }
   * ```
   */
  public hasAny(...componentClassOrTag: Array<Class<unknown> | Tag>): boolean {
    return componentClassOrTag.some(value => this.has(value));
  }

  /**
   * Returns value indicating whether entity have all of specified components/tags
   *
   * @param {Class<unknown> | Tag} componentClassOrTag
   * @returns {boolean}
   * @example
   * ```ts
   * const I_LOVE_GRAVITY = "no-i-don't";
   * if (entity.hasAll(Position, Acceleration, I_LOVE_GRAVITY)) {
   *   entity.get(Position)!.y += entity.get(Acceleration)!.y * dt;
   * }
   * ```
   */
  public hasAll(...componentClassOrTag: Array<Class<unknown> | Tag>): boolean {
    return componentClassOrTag.every(value => this.has(value));
  }

  /**
   * Gets a component instance if it's exists in the entity, otherwise returns `undefined`
   * - If you want to check presence of the tag then use {@link has} instead.
   *
   * @param componentClass Specific component class
   * @param id Identifier of the LinkedComponent
   */
  public get<T>(componentClass: Class<T>, id?: string): T | undefined {
    const cid = getComponentId(componentClass);
    if (cid === undefined) return undefined;
    let component = this._components[cid];
    if (id !== undefined) {
      if (isLinkedComponent(component)) {
        while (component !== undefined) {
          if ((component as ILinkedComponent).id === id) return component as T;
          component = (component as ILinkedComponent).next;
        }
      }
      return undefined;
    }
    return this._components[cid] as T;
  }

  /**
   * Returns an array of entity components
   *
   * @returns {unknown[]}
   */
  public getComponents(): unknown[] {
    return Array.from(Object.values(this._components));
  }

  /**
   * Returns an array of tags applied to the entity
   */
  public getTags(): Tag[] {
    return Array.from(this._tags);
  }

  /**
   * Removes a component or tag from the entity.
   *  In case if the component or tag is present - then {@link onComponentRemoved} will be
   *  dispatched after removing it from the entity.
   *
   * If linked component type provided:
   * - For each instance of linked component {@link onComponentRemoved} will be called
   * - Only head of the linked list will be returned.
   *
   * If you need to get all instances use {@link withdraw} or {@link pick} instead, or check {@link iterate},
   * {@link getAll}
   *
   * It's a shorthand for {@link removeComponent}
   *
   * @param componentClassOrTag Specific component class or tag
   * @returns Component instance or `undefined` if it doesn't exists in the entity, or tag was removed
   * @see {@link withdraw}
   * @see {@link pick}
   */
  public remove<T>(componentClassOrTag: Class<T> | Tag): T | undefined {
    if (isTag(componentClassOrTag)) {
      this.removeTag(componentClassOrTag);
      return undefined;
    }
    return this.removeComponent(componentClassOrTag);
  }

  /**
   * Removes a component from the entity.
   *  In case if the component or tag is present - then {@link onComponentRemoved} will be
   *  dispatched after removing it from the entity.
   *
   * If linked component type provided:
   * - For each instance of linked component {@link onComponentRemoved} will be called
   * - Only head of the linked list will be returned.
   *
   * If you need to get all instances use {@link withdraw} or {@link pick} instead, or check {@link iterate},
   * {@link getAll}
   *
   * @param componentClassOrTag Specific component class
   * @returns Component instance or `undefined` if it doesn't exists in the entity
   */
  public removeComponent<T>(componentClassOrTag: Class<T>): T | undefined {
    const id = getComponentId(componentClassOrTag);
    if (id === undefined || this._components[id] === undefined) {
      return undefined;
    }

    let value = this._components[id]!;
    if (isLinkedComponent(value)) {
      const list = this.getLinkedComponentList(componentClassOrTag)!;
      while (!list.isEmpty) {
        this.withdraw(componentClassOrTag);
      }
    } else {
      delete this._components[id];
      this.dispatchOnComponentRemoved(value);
    }

    return value as T;
  }

  /**
   * Removes a tag from the entity.
   *  In case if the component tag is present - then {@link onComponentRemoved} will be
   *  dispatched after removing it from the entity
   *
   * @param {Tag} tag Specific tag
   * @returns {void}
   */
  public removeTag(tag: Tag): void {
    if (this._tags.has(tag)) {
      this._tags.delete(tag);
      this.dispatchOnComponentRemoved(tag);
    }
  }

  /**
   * Removes all components and tags from entity
   */
  public clear(): void {
    this._components = {};
    this._linkedComponents = {};
    this._tags.clear();
  }

  /**
   * Copies content from entity to itself.
   * Linked components structure will be copied by the link, because we can't duplicate linked list order without
   * cloning components itself. So modifying linked components in the copy will affect linked components in copy
   * source.
   *
   * @param {Entity} entity
   * @return {this}
   */
  public copyFrom(entity: Entity): this {
    this._components = Object.assign({}, entity._components);
    this._linkedComponents = Object.assign({}, entity._linkedComponents);
    this._tags = new Set(entity._tags);
    return this;
  }

  /**
   * Iterates over instances of linked component appended to the Entity and performs the action over each.<br>
   * Works and for standard components (action will be called for a single instance in this case).
   *
   * @param {Class<T>} componentClass Component`s class
   * @param {(component: T) => void} action Action to perform over every component instance.
   * @example
   * ```ts
   * class Boon extends LinkedComponent {
   *   public constructor(
   *     public type: BoonType,
   *     public duration: number
   *   ) { super(); }
   * }
   * const entity = new Entity()
   *   .append(new Boon(BoonType.HEAL, 2))
   *   .append(new Boon(BoonType.PROTECTION, 3);
   *
   * // Let's decrease every boon duration and remove them if they are expired.
   * entity.iterate(Boon, (boon) => {
   *   if (--boon.duration <= 0) {
   *      entity.pick(boon);
   *   }
   * });
   * ```
   */
  public iterate<T>(componentClass: Class<T>, action: (component: T) => void): void {
    if (!this.hasComponent(componentClass)) return;
    this.getLinkedComponentList(componentClass)?.iterate(action);
  }

  /**
   * Returns generator with all instances of specified linked component class
   *
   * @param {Class<T>} componentClass Component`s class
   * @example
   * ```ts
   * for (const damage of entity.linkedComponents(Damage)) {
   *   if (damage.value < 0) {
   *   throw new Error('Damage value can't be less than zero');
   * }
   * ```
   */
  public* getAll<T>(componentClass: Class<T>): Generator<T, void, T | undefined> {
    if (!this.hasComponent(componentClass)) return;
    const list = this.getLinkedComponentList(componentClass, false);
    if (list === undefined) return undefined;
    yield* list.nodes();
  }

  /**
   * Searches a component instance of specified linked component class.
   * Works and for standard components (predicate will be called for a single instance in this case).
   *
   * @param {Class<T>} componentClass
   * @param {(component: T) => boolean} predicate
   * @return {T | undefined}
   */
  public find<T>(componentClass: Class<T>, predicate: (component: T) => boolean): T | undefined {
    const componentIdToFind = getComponentId(componentClass, false);
    if (componentIdToFind === undefined) return undefined;
    const component = this._components[componentIdToFind];
    if (component === undefined) return undefined;
    if (isLinkedComponent(component)) {
      let linkedComponent: ILinkedComponent | undefined = component;
      while (linkedComponent !== undefined) {
        if (predicate(linkedComponent as T)) return linkedComponent as T;
        linkedComponent = linkedComponent.next;
      }
    } else return predicate(component as T) ? component as T : undefined;
  }

  /**
   * Returns number of components of specified class.
   *
   * @param {Class<T>} componentClass
   * @return {number}
   */
  public lengthOf<T>(componentClass: Class<T>): number {
    let result = 0;
    this.iterate(componentClass, () => {
      result++;
    });
    return result;
  }

  /**
   * Use this method to dispatch that entity component properties were changed, in case if
   * queries predicates are depends on them.
   * Components properties are not tracking by Engine itself, because it's too expensive.
   */
  public invalidate(): void {
    this.onInvalidationRequested.emit(this);
  }

  /**
   * @internal
   * @param {EntitySnapshot} result
   * @param {T} changedComponentOrTag
   * @param {Class<T>} resolveClass
   */
  public takeSnapshot<T>(result: EntitySnapshot, changedComponentOrTag?: T, resolveClass?: Class<T>): void {
    const previousState = result.previous as Entity;
    if (result.current !== this) {
      result.current = this;
      previousState.copyFrom(this);
    }

    if (changedComponentOrTag === undefined) {
      return;
    }

    if (isTag(changedComponentOrTag)) {
      const previousTags = previousState._tags;
      if (this.has(changedComponentOrTag)) {
        previousTags.delete(changedComponentOrTag);
      } else {
        previousTags.add(changedComponentOrTag);
      }
    } else {
      const componentClass = resolveClass ?? Object.getPrototypeOf(changedComponentOrTag).constructor;
      const componentId = getComponentId(componentClass!, true)!;
      const previousComponents = previousState._components;
      if (this.has(componentClass)) {
        delete previousComponents[componentId];
      } else {
        previousComponents[componentId] = changedComponentOrTag;
      }
    }
  }

  /**
   * @internal
   */
  public getLinkedComponentList(componentClassOrId: number | Class<any>, createIfNotExists = true): LinkedComponentList<any> | undefined {
    if (typeof componentClassOrId !== 'number') {
      componentClassOrId = getComponentId(componentClassOrId)!;
    }
    if (this._linkedComponents[componentClassOrId] !== undefined || !createIfNotExists) {
      return this._linkedComponents[componentClassOrId];
    } else {
      return this._linkedComponents[componentClassOrId] = new LinkedComponentList<ILinkedComponent>();
    }
  }

  private withdrawComponent<T extends K, K extends ILinkedComponent>(component: NonNullable<T>, resolveClass?: Class<K>): T | undefined {
    const componentClass = getComponentClass(component, resolveClass);
    const componentList = this.getLinkedComponentList(componentClass, false);
    if (!this.hasComponent(componentClass) || componentList === undefined) return undefined;
    const result = componentList.remove(component) ? component : undefined;
    const componentId = getComponentId(componentClass, true)!;
    if (componentList.isEmpty) {
      delete this._components[componentId];
      delete this._linkedComponents[componentId];
    } else {
      this._components[componentId] = componentList.head;
    }
    if (result !== undefined) {
      this.dispatchOnComponentRemoved(result);
    }
    return result;
  }

  private dispatchOnComponentAdded<T>(component: NonNullable<T>): void {
    if (this.onComponentAdded.hasHandlers) {
      this.onComponentAdded.emit(this, component);
    }
  }

  private dispatchOnComponentRemoved<T>(value: NonNullable<T>): void {
    if (this.onComponentRemoved.hasHandlers) {
      this.onComponentRemoved.emit(this, value);
    }
  }
}

/**
 * EntitySnapshot is a content container that displays the difference between the current state of Entity and its
 * previous state.
 *
 * The {@link EntitySnapshot.current} property always reflects the current state, and {@link EntitySnapshot.previous} -
 * previous one. So you can understand which components have been added and which have been removed.
 *
 * <p>It is important to note that changes in the data of the same entity components will not be reflected in the
 * snapshot, even if a manual invalidation of the entity has been triggered.</p>
 */
export class EntitySnapshot {
  private _current?: Entity;
  private _previous: ReadonlyEntity = new Entity();

  /**
   * Gets an instance of the actual entity
   * @returns {Entity}
   */
  public get current(): Entity {
    return this._current!;
  }

  /**
   * @internal
   */
  public set current(value: Entity) {
    this._current = value;
  }

  /**
   * Gets an instance of the previous state of entity
   */
  public get previous(): ReadonlyEntity {
    return this._previous;
  }
}

/**
 * Component update handler type.
 * @see {@link Entity.onComponentAdded}
 * @see {@link Entity.onComponentRemoved}
 */
export type ComponentUpdateHandler = <T>(entity: Entity, component: NonNullable<T>, componentClass?: Class<NonNullable<T>>) => void;

/**
 * Entity ids enumerator
 */
let entityId: number = 1;

================================================
FILE: src/ecs/IterativeSystem.ts
================================================
import {Query, QueryBuilder, QueryPredicate} from './Query';
import {Entity} from './Entity';
import {ReactionSystem} from './ReactionSystem';

/**
 * Iterative system made for iterating over entities that matches its query.
 *
 * @example
 * You have a View component, that is responsible for entity displaying and contains an image.
 * So every step you want to update image positions, that can depends on Position component.
 *
 * ```ts
 * class ViewSystem extends IterativeSystem {
 *   constructor(container:Container) {
 *      super(new Query((entity:Entity) => entity.hasAll(View, Position));
 *      this.container = container;
 *   }
 *
 *   // Update entity view position on screen, via position component data
 *   updateEntity(entity:Entity) {
 *     const {view} = entity.get(View)!;
 *     const {x, y) = entity.get(Position)!;
 *     view.x = x;
 *     view.y = y;
 *   }
 *
 *   // Add entity view from screen
 *   entityAdded = ({entity}:EntitySnapshot) => {
 *    this.container.add(entity.get(View)!.view);
 *   }
 *
 *   // Remove entity view from screen
 *   entityRemoved = (snapshot:EntitySnapshot) => {
 *    this.container.remove(snapshot.get(View)!.view);
 *   }
 * }
 * ```
 */
export abstract class IterativeSystem extends ReactionSystem {
  private _removed: boolean = false;

  protected constructor(query: Query | QueryBuilder | QueryPredicate) {
    super(query);
  }

  public update(dt: number) {
    this.updateEntities(dt);
  }

  public onAddedToEngine() {
    this._removed = false;
    super.onAddedToEngine();
  }

  public onRemovedFromEngine() {
    this._removed = true;
    super.onRemovedFromEngine();
  }

  protected updateEntities(dt: number) {
    for (let entity of this.query.entities) {
      if (this._removed) return;
      this.updateEntity(entity, dt);
    }
  }

  /**
   * Update entity
   *
   * @param entity Entity to update
   * @param dt Delta time in seconds
   */
  protected abstract updateEntity(entity: Entity, dt: number): void;
}


================================================
FILE: src/ecs/LinkedComponent.ts
================================================
/**
 * Linked list interface for linked components
 * @see {@link Entity.append}
 */

export interface ILinkedComponent {
  id?: string;
  next?: ILinkedComponent;
}

/**
 * Simple ILinkedComponent implementation
 * @see {@link Entity.append}
 */
export class LinkedComponent implements ILinkedComponent {
  public next?: this = undefined;

  public constructor(public id?: string) {
  }
}

/**
 * @internal
 */
export function isLinkedComponent(component: any): component is ILinkedComponent {
  return component !== undefined && component.hasOwnProperty('next');
}


================================================
FILE: src/ecs/LinkedComponentList.ts
================================================
import {ILinkedComponent} from './LinkedComponent';

export class LinkedComponentList<T extends ILinkedComponent> {
  private _head?: T;

  public get head(): T | undefined {
    return this._head;
  }

  public get isEmpty(): boolean {
    return this._head === undefined;
  }

  public add(linkedComponent: T): void {
    let prev: T | undefined = undefined;
    let current: T | undefined = this._head;
    while (current !== undefined) {
      if (current === linkedComponent) {
        throw new Error('Component is already appended, appending it once again will break linked items order');
      }
      prev = current;
      current = current.next as (T | undefined);
    }
    if (this._head === undefined) {
      this._head = linkedComponent;
    } else {
      prev!.next = linkedComponent;
    }
  }

  public remove(linkedComponent: T): boolean {
    const [prev, current] = this.find(linkedComponent);
    if (current === undefined) {
      return false;
    }
    if (prev === undefined) {
      this._head = current.next as (T | undefined);
    } else {
      prev.next = current.next;
    }
    return true;
  }

  public* nodes() {
    let node = this.head;
    while (node !== undefined) {
      yield node;
      node = node.next as (T | undefined);
    }
  }

  public iterate(action: (value: T) => void): void {
    for (const node of this.nodes()) {
      action(node);
    }
  }

  public clear(): void {
    this._head = undefined;
  }

  private find(linkedComponent: T): [prev: T | undefined, current: T | undefined] {
    let prev: T | undefined;
    let current: T | undefined = this._head;

    while (current !== undefined) {
      if (current === linkedComponent) {
        return [prev, current];
      }
      prev = current;
      current = current.next as (T | undefined);
    }
    return [undefined, undefined];
  }
}


================================================
FILE: src/ecs/Query.ts
================================================
import {getComponentId} from './ComponentId';
import {Entity, EntitySnapshot} from './Entity';
import {isTag, Tag} from './Tag';
import {Signal} from '../utils/Signal';
import {Class} from '../utils/Class';

/**
 * Query Predicate is the type that describes a function that compares Entities with the conditions it sets.
 * In other words, it's a function that determines whether Entities meets the right conditions to get into a
 * given Query or not.
 */
export type QueryPredicate = (entity: Entity) => boolean;

/**
 * Query represents list of entities that matches query request.
 * @see QueryBuilder
 */
export class Query {
  /**
   * Signal dispatches if new matched entity were added
   */
  public onEntityAdded: Signal<(snapshot: EntitySnapshot) => void> = new Signal();
  /**
   * Signal dispatches if entity stops matching query
   */
  public onEntityRemoved: Signal<(snapshot: EntitySnapshot) => void> = new Signal();

  private readonly _snapshot: EntitySnapshot = new EntitySnapshot();
  private readonly _predicate: QueryPredicate;
  private _entities: Entity[] = [];

  /**
   * Initializes Query instance
   * @param predicate Matching predicate
   */
  public constructor(predicate: QueryPredicate) {
    this._predicate = predicate;
  }

  /**
   * Entities list which matches the query
   */
  public get entities(): ReadonlyArray<Entity> {
    return this._entities;
  }

  /**
   * Returns the first entity in the query or `undefined` if query is empty.
   * @returns {Entity | undefined}
   */
  public get first(): Entity | undefined {
    if (this._entities.length === 0) return undefined;
    return this._entities[0];
  }

  /**
   * Returns the last entity in the query or `undefined` if query is empty.
   * @returns {Entity | undefined}
   */
  public get last(): Entity | undefined {
    if (this._entities.length === 0) return undefined;
    return this._entities[this._entities.length - 1];
  }

  /**
   * Returns the number of the entities in the query
   * @returns {Entity | undefined}
   */
  public get length(): number {
    return this._entities.length;
  }

  /**
   * Returns the number of entities that have been tested by the predicate.
   * @param {(entity: Entity) => boolean} predicate
   * @returns {number}
   */
  public countBy(predicate: QueryPredicate): number {
    let result = 0;
    for (const entity of this._entities) {
      if (predicate(entity)) result++;
    }
    return result;
  }

  /**
   * Returns the first entity from the query, that was accepted by predicate
   * @param {(entity: Entity) => boolean} predicate - function that will be called for every entity in the query until
   *  the result of the function become true.
   * @returns {Entity | undefined}
   */
  public find(predicate: QueryPredicate): Entity | undefined {
    return this._entities.find(predicate);
  }

  /**
   * Returns new array of entities, which passed testing via predicate
   * @param {(entity: Entity) => boolean} predicate - function that will be called for every entity in the query.
   *  If function returns `true` - entity will stay in the array, if `false` than it will be removed.
   * @returns {Entity[]}
   */
  public filter(predicate: QueryPredicate): Entity[] {
    return this._entities.filter(predicate);
  }

  /**
   * Returns a value that indicates whether the entity is in the Query.
   * @param {Entity} entity
   * @returns {boolean}
   */
  public has(entity: Entity): boolean {
    return this._entities.indexOf(entity) !== -1;
  }

  /**
   * This method is matching passed list of entities with predicate of the query to determine
   * if entities are the part of query or not.
   *
   * Entities that will pass testing will become a part of the query
   */
  public matchEntities(entities: ReadonlyArray<Entity>) {
    entities.forEach((entity) => this.entityAdded(entity));
  }

  /**
   * Gets a value indicating that query is empty
   */
  public get isEmpty(): boolean {
    return this.entities.length == 0;
  }

  /**
   * Clears the list of entities of the query
   */
  public clear(): void {
    this._entities = [];
  }

  /**
   * @internal
   */
  public validateEntity(entity: Entity): void {
    const index = this._entities.indexOf(entity);
    const isMatch = this._predicate(entity);
    if (index !== -1 && !isMatch) {
      this.entityRemoved(entity);
    } else {
      this.entityAdded(entity);
    }
  }

  /**
   * @internal
   */
  public entityAdded = (entity: Entity) => {
    const index = this._entities.indexOf(entity);
    if (index === -1 && this._predicate(entity)) {
      this._entities.push(entity);
      if (this.onEntityAdded.hasHandlers) {
        entity.takeSnapshot(this._snapshot);
        this.onEntityAdded.emit(this._snapshot);
      }
    }
  };

  /**
   * @internal
   */
  public entityRemoved = (entity: Entity) => {
    const index = this._entities.indexOf(entity);
    if (index !== -1) {
      this._entities.splice(index, 1);
      if (this.onEntityRemoved.hasHandlers) {
        entity.takeSnapshot(this._snapshot);
        this.onEntityRemoved.emit(this._snapshot);
      }
    }
  };

  /**
   * @internal
   */
  public entityComponentAdded = <T>(entity: Entity, componentOrTag: NonNullable<T>, componentClass?: Class<NonNullable<T>>) => {
    const hasAddedHandlers = this.onEntityAdded.hasHandlers;
    const hasRemovedHandlers = this.onEntityRemoved.hasHandlers;

    const index = this._entities.indexOf(entity);
    const isMatch = this._predicate(entity);
    if (index === -1 && isMatch) {
      this._entities.push(entity);
      if (hasAddedHandlers) {
        entity.takeSnapshot(this._snapshot, componentOrTag, componentClass);
        this.onEntityAdded.emit(this._snapshot);
      }
    } else if (index !== -1 && !isMatch) {
      this._entities.splice(index, 1);
      if (hasRemovedHandlers) {
        entity.takeSnapshot(this._snapshot, componentOrTag, componentClass);
        this.onEntityRemoved.emit(this._snapshot);
      }
    }
  };

  /**
   * @internal
   */
  public entityComponentRemoved = <T>(entity: Entity, component: NonNullable<T>, componentClass?: Class<NonNullable<T>>) => {
    const hasAddedHandlers = this.onEntityAdded.hasHandlers;
    const hasRemovedHandlers = this.onEntityRemoved.hasHandlers;

    const index = this._entities.indexOf(entity);
    const isMatch = this._predicate(entity);
    if (index !== -1 && !isMatch) {
      this._entities.splice(index, 1);
      if (hasRemovedHandlers) {
        entity.takeSnapshot(this._snapshot, component, componentClass);
        this.onEntityRemoved.emit(this._snapshot);
      }
    } else if (index === -1 && isMatch) {
      this._entities.push(entity);
      if (hasAddedHandlers) {
        entity.takeSnapshot(this._snapshot, component, componentClass);
        this.onEntityAdded.emit(this._snapshot);
      }
    }
  };
}

function hasAll(entity: Entity, components: Set<number>, tags: Set<Tag>): boolean {
  if (components.size > 0) {
    for (const componentId of components) {
      if (entity.components[componentId] === undefined) {
        return false;
      }
    }
  }
  if (tags.size > 0) {
    for (const tag of tags) {
      if (!entity.tags.has(tag)) {
        return false;
      }
    }
  }
  return true;
}

/**
 * Query builder, helps to create queries
 * @example
 * const query = new QueryBuilder()
 *  .contains(Position)
 *  .contains(Acceleration)
 *  .contains(TorqueForce)
 *  .build();
 */
export class QueryBuilder {
  private readonly _components: Set<number> = new Set();
  private readonly _tags: Set<Tag> = new Set();

  /**
   * Specifies components that must be added to entity to be matched
   * @param componentsOrTags
   */
  public contains(...componentsOrTags: Array<any>): QueryBuilder {
    for (const componentOrTag of componentsOrTags) {
      if (isTag(componentOrTag)) {
        if (!this._tags.has(componentOrTag)) {
          this._tags.add(componentOrTag);
        }
      } else {
        const componentId = getComponentId(componentOrTag, true)!;
        if (!this._components.has(componentId)) {
          this._components.add(componentId);
        }
      }
    }
    return this;
  }

  /**
   * Build query
   */
  public build(): Query {
    return new Query((entity: Entity) => hasAll(entity, this._components, this._tags));
  }

  /**
   * @internal
   */
  public getComponents(): ReadonlySet<number> {
    return this._components;
  }

  /**
   * @internal
   */
  public getTags(): ReadonlySet<Tag> {
    return this._tags;
  }
}

/**
 * @internal
 */
export function isQueryPredicate(item: unknown): item is QueryPredicate {
  return typeof item === 'function';
}

/**
 * @internal
 */
export function isQueryBuilder(item: unknown): item is QueryBuilder {
  return item instanceof QueryBuilder;
}


================================================
FILE: src/ecs/ReactionSystem.ts
================================================
import {isQueryBuilder, isQueryPredicate, Query, QueryBuilder, QueryPredicate} from './Query';
import {Engine} from './Engine';
import {Entity, EntitySnapshot} from './Entity';
import {System} from './System';

/**
 * Represents a system that reacts when entities are added to or removed from its query.
 * `entityAdded` and `entityRemoved` will be called accordingly.
 *
 * @example
 * ```ts
 * class ViewSystem extends ReactionSystem {
 *   constructor(
 *      private readonly container:Container
 *   ) {
 *      super(new Query((entity:Entity) => entity.has(View));
 *   }
 *
 *   // Add entity view to the screen
 *   entityAdded = ({entity}:EntitySnapshot) => {
 *    this.container.add(entity.get(View)!.view);
 *   }
 *
 *   // Remove entity view from screen
 *   entityRemoved = (snapshot:EntitySnapshot) => {
 *    this.container.remove(snapshot.get(View)!.view);
 *   }
 * }
 * ```
 */
export abstract class ReactionSystem extends System {
  protected readonly query: Query;

  protected constructor(query: Query | QueryBuilder | QueryPredicate) {
    super();
    if (isQueryBuilder(query)) {
      this.query = query.build();
    } else if (isQueryPredicate(query)) {
      this.query = new Query(query);
    } else {
      this.query = query;
    }
  }

  protected get entities(): ReadonlyArray<Entity> {
    return this.query.entities;
  }

  public onAddedToEngine() {
    this.engine.addQuery(this.query);
    this.prepare();
    this.query.onEntityAdded.connect(this.entityAdded);
    this.query.onEntityRemoved.connect(this.entityRemoved);
  }

  public onRemovedFromEngine() {
    this.engine.removeQuery(this.query);
    this.query.onEntityAdded.disconnect(this.entityAdded);
    this.query.onEntityRemoved.disconnect(this.entityRemoved);
    this.query.clear();
  }

  protected prepare() {}

  /**
   * Method will be called for every new entity that matches system query.
   * You could easily override it with your own logic.
   *
   * Note: Method will not be called for already existing in query entities (at the adding system to engine phase),
   * only new entities will be handled
   *
   * @param entity EntitySnapshot that contains entity that was removed from query or engine, and components that it has
   *   before adding, and component that will be added
   */
  protected entityAdded = (entity: EntitySnapshot) => {
  };

  /**
   * Method will be called for every entity matches system query, that is going to be removed from engine, or it stops
   * matching to the query.
   * You could easily override it with your own logic.
   *
   * @param entity EntitySnapshot that contains entity that was removed from query or engine, and components that it has
   *   before removing
   */
  protected entityRemoved = (entity: EntitySnapshot) => {
  };
}


================================================
FILE: src/ecs/Subscription.ts
================================================
import {Class} from '../utils/Class';

/**
 * @internal
 */
export class Subscription<T> {
  public constructor(
    public readonly messageType: Class<T> | T,
    public readonly handler: (message: T) => void,
  ) {}

  public equals(messageType: Class<T> | T, handler?: (message: T) => void) {
    return this.messageType === messageType && (handler === undefined || this.handler === handler);
  }
}


================================================
FILE: src/ecs/System.ts
================================================
import {Engine} from './Engine';
import {Entity} from './Entity';

/**
 * Systems are logic bricks in your application.
 * If you want to manipulate entities and their components - it is the right place for that.
 */
export abstract class System {
  private _priority: number = 0;
  private _engine?: Engine;
  private _isRemovalRequested: boolean = false;

  /**
   * Gets an {@link Engine} instance that system attached to
   * @returns {Engine}
   * @throws An error if system is not attached to the engine
   */
  public get engine(): Engine {
    if (this._engine === undefined) throw new Error(`Property "engine" can't be accessed when system is not added to the engine`);
    return this._engine;
  }

  /**
   * Indicates that system should be removed from engine at the end of the current update cycle
   * @internal
   * @returns {boolean}
   */
  public get isRemovalRequested(): boolean {
    return this._isRemovalRequested;
  }

  /**
   * Gets an {@link Entity} instance that is shared across all systems and can be used as a config.
   * @return {Entity}
   */
  protected get sharedConfig(): Entity {
    if (this._engine === undefined) throw new Error(`Property "sharedConfig" can't be accessed when system is not added to the engine`);
    return this._engine.sharedConfig;
  }

  /**
   * Gets a priority of the system
   */
  public get priority(): number {
    return this._priority;
  }

  /**
   * All logic aimed at making changes in entities and their components must be placed in this method.
   * @param dt - The time in seconds it took from previous update call.
   */
  public update(dt: number) {}

  /**
   * This method will be called after the system will be added to the Engine.
   */
  public onAddedToEngine() {}

  /**
   * Callback that will be invoked after removing system from engine
   */
  public onRemovedFromEngine() {}

  /**
   * Dispatches a message, that can be caught via {@link Engine#subscribe}.
   * It's the best way to send a message outside. This mechanism allows you not to invent the signals/dispatchers
   * mechanism for your systems, to report an event. For example, you can dispatch that the game round has been
   * completed.
   *
   * @param {T} message
   * @throws An error if system is not attached to the engine
   * @example
   * ```ts
   * class RoundCompleted {
   *   public constructor(
   *      public readonly win:boolean
   *   ) {}
   * }
   *
   * const engine = new Engine();
   * engine.subscribe(RoundCompleted, (message:RoundCompleted) => {
   *   if (message.win) {
   *     this.showWinDialog();
   *   } else {
   *     this.showLoseDialog();
   *   }
   * })
   *
   * class RoundCompletionSystem extends System {
   *   public update(dt:number) {
   *     if (heroesQuery.isEmpty) {
   *       this.dispatch(new RoundCompleted(false));
   *     } else if (enemiesQuery.isEmpty) {
   *       this.dispatch(new RoundCompleted(true));
   *     }
   *   }
   * }
   * ```
   */
  public dispatch<T>(message: T): void {
    if (this._engine === undefined) {
      throw new Error('Dispatching a message can\'t be done while system is not attached to the engine');
    }
    this.engine.dispatch(message);
  }

  /**
   * @internal
   */
  public setEngine(engine: Engine | undefined): void {
    this._engine = engine;
  }

  /**
   * @internal
   */
  public setPriority(priority: number): void {
    this._priority = priority;
  }

  protected requestRemoval(): void {
    this._isRemovalRequested = true;
  }
}


================================================
FILE: src/ecs/Tag.ts
================================================
/**
 * A tag is a simple marker that can be considered as a component without data.
 * It can be used instead of creating a new component class, when you don't need an additional data.
 */
export type Tag = number | string;

/**
 * This predicate can help you to understand whether item is a component or tag
 * @param item
 * @returns {item is Tag}
 */
export function isTag(item: unknown): item is Tag {
  const type = typeof item;
  return type === 'string' || type === 'number';
}


================================================
FILE: src/index.ts
================================================
export * from './utils/Class';
export * from './utils/Signal';
export * from './ecs/ComponentId';
export * from './ecs/Tag';
export * from './ecs/LinkedComponent';
export * from './ecs/Engine';
export * from './ecs/Entity';
export * from './ecs/System';
export * from './ecs/Query';
export * from './ecs/IterativeSystem';
export * from './ecs/ReactionSystem';

================================================
FILE: src/utils/Class.ts
================================================
export type Class<T> = {
  new(...args: any[]): T;
};



================================================
FILE: src/utils/Signal.ts
================================================
/**
 * Lightweight implementation of Signal
 */
export class Signal<Handler extends (...args: any[]) => any> {
  private readonly handlers: SignalHandler<Handler>[] = [];

  /**
   * Gets a value that indicates whether signal has handlers
   * @return {boolean}
   */
  public get hasHandlers(): boolean {
    return this.handlers.length > 0;
  }

  /**
   * Gets an amount of connected handlers
   * @return {number}
   */
  public get handlersAmount(): number {
    return this.handlers.length;
  }

  /**
   * Connects signal handler, that will be invoked on signal emit.
   * @param {Handler} handler
   * @param priority Handler invocation priority (handler with higher priority will be called later than with lower one)
   */
  public connect(handler: Handler, priority: number = 0): void {
    const existingHandler = this.handlers.find((it) => it.equals(handler));
    let needResort: boolean;
    if (existingHandler !== undefined) {
      needResort = existingHandler.priority !== priority;
      existingHandler.priority = priority;
    } else {
      const lastHandler = this.handlers[this.handlers.length - 1];
      this.handlers.push(new SignalHandler(handler, priority));
      needResort = (lastHandler !== undefined && lastHandler.priority > priority);
    }
    if (needResort) {
      this.handlers.sort((a, b) => a.priority - b.priority);
    }
  }

  /**
   * Disconnects signal handler
   * @param {Handler} handler
   */
  public disconnect(handler: Handler): void {
    const existingHandlerIndex = this.handlers.findIndex((it) => it.equals(handler));
    if (existingHandlerIndex >= 0) {
      this.handlers.splice(existingHandlerIndex, 1);
    }
  }

  /**
   * Disconnects all signal handlers
   * @param {Handler} handler
   */
  public disconnectAll(): void {
    this.handlers.length = 0;
  }

  /**
   * Invokes connected handlers with passed parameters.
   * @param {any} args
   */
  public emit(...args: Parameters<Handler>): void {
    for (const handler of this.handlers) {
      handler.handle(...args);
    }
  }
}

class SignalHandler<Handler extends (...args: any[]) => any> {
  public constructor(public readonly handler: Handler, public priority: number) {}

  public equals(handler: Handler): boolean {
    return this.handler === handler;
  }

  public handle(...args: any[]) {
    this.handler(...args);
  }
}


================================================
FILE: tests/unit/engine.spec.ts
================================================
import {Engine, Entity, IterativeSystem, LinkedComponent, Query, QueryBuilder, QueryPredicate, System, ReactionSystem } from '../../src';

class Component {}

class Message {}

const handler1 = (message: Message) => {};
const handler2 = (message: Message) => {};
const handler3 = (message: Message) => {};

abstract class TestSystem extends IterativeSystem {
  protected constructor(
    query: Query | QueryBuilder | QueryPredicate,
    private readonly arr?: number[],
  ) {
    super(query);
    this.arr = arr;
  }

  public update(dt: number) {
    super.update(dt);
    if (this.arr !== undefined) {
      this.arr.push(this.priority);
    }
  }

  protected updateEntity(entity: Entity, dt: number): void {
  }
}

class TestSystem1 extends TestSystem {
  public constructor(arr?: number[]) {
    super(new Query((entity: Entity) => entity.has(Component)), arr);
  }
}

class TestSystem2 extends TestSystem {
  public constructor(arr?: number[]) {
    super((entity: Entity) => entity.has(Component), arr);
  }
}

class TestSystem3 extends TestSystem {
  public constructor(arr?: number[]) {
    super(new QueryBuilder().contains(Component), arr);
  }
}

describe('System manipulation', () => {
  it('Engine system creating', () => {
    const engine = new Engine();
    expect(engine.systems).toBeDefined();
    expect(engine.systems.length).toBe(0);
    expect(engine.entities).toBeDefined();
    expect(engine.entities.length).toBe(0);
    expect(engine.queries).toBeDefined();
    expect(engine.queries.length).toBe(0);
  });

  it('Adding system', () => {
    const engine = new Engine();
    const system = new TestSystem1();

    engine.addSystem(system);

    expect(engine.systems.length).toBe(1);
    expect(engine.getSystem(TestSystem1)).toBe(system);
  });

  it('Adding and removing multiple system with priority', () => {
    const engine = new Engine();
    const system1 = new TestSystem1();
    const system2 = new TestSystem2();
    const system3 = new TestSystem3();

    engine.addSystem(system1, 200);
    engine.addSystem(system2, 300);
    engine.addSystem(system3, 100);

    expect(engine.systems.length).toBe(3);
    expect(engine.getSystem(TestSystem1)).toBe(system1);
    expect(engine.getSystem(TestSystem2)).toBe(system2);
    expect(engine.getSystem(TestSystem3)).toBe(system3);
    expect(engine.systems).toEqual([system3, system1, system2]);

    engine.removeAllSystems();

    expect(engine.systems.length).toBe(0);
  });

  it('Adding multiple systems with same priority must added in same order', () => {
    const engine = new Engine();
    const system1 = new TestSystem1();
    const system2 = new TestSystem2();
    const system3 = new TestSystem3();

    engine.addSystem(system1);
    engine.addSystem(system2);
    engine.addSystem(system3);

    expect(engine.systems.length).toBe(3);
    expect(engine.systems).toEqual([system1, system2, system3]);
  });

  it('Remove system', () => {
    const engine = new Engine();
    const system = new TestSystem1();

    engine.addSystem(system);

    expect(engine.systems.length).toBe(1);
    expect(engine.getSystem(TestSystem1)).toBe(system);

    engine.removeSystem(system);

    expect(engine.systems.length).toBe(0);
    expect(engine.getSystem(TestSystem1)).toBeUndefined();
  });

  it(`Expected that removing not attached system will not throw an error`, () => {
    const engine = new Engine();
    const system = new TestSystem1();
    expect(() => { engine.removeSystem(system);}).not.toThrowError();
  });

  it('Engine updating', () => {
    const engine = new Engine();
    const arr: number[] = [];
    const system1 = new TestSystem1(arr);
    const system2 = new TestSystem2(arr);
    const system3 = new TestSystem3(arr);

    engine.addSystem(system1, 1);
    engine.addSystem(system2, 2);
    engine.addSystem(system3, 3);

    engine.update(1);
    expect(arr).toEqual([1, 2, 3]);
  });

  it('Engine#clear should remove entities, systems, remove and clear queries', () => {
    class TestSystem extends IterativeSystem {
      public constructor() {
        super(new Query(entity => true));
      }

      protected updateEntity(entity: Entity, dt: number): void {
      }
    }

    const engine = new Engine();
    const query = new Query(entity => entity.has(Component));
    const system = new TestSystem();

    engine.addQuery(query);
    engine.addSystem(system);
    engine.addEntity(new Entity().add(new Component()));

    expect(query.isEmpty).toBeFalsy();

    engine.clear();
    expect(engine.systems.length).toBe(0);
    expect(engine.queries.length).toBe(0);
    expect(engine.entities.length).toBe(0);
    expect(query.isEmpty).toBeTruthy();

    engine.addEntity(new Entity().add(new Component()));
    expect(query.isEmpty).toBeTruthy();
  });

  it('Expected that removing all entities will fire onEntityRemoved', () => {
    const engine = new Engine();
    const entitiesCount = 2;
    let removedCount = 0;
    for (let i = 0; i < entitiesCount; i++) {
      engine.addEntity(new Entity());
    }
    engine.onEntityRemoved.connect(() => removedCount++);
    engine.removeAllEntities();
    expect(engine.entities.length).toBe(0);
    expect(removedCount).toBe(entitiesCount);
  });

  it(`Expected that engine will not add same handler twice for the same message`, () => {
    const engine = new Engine();
    const handler = (message: Message) => {};
    const subscription1 = engine.subscribe(Message, handler);
    const subscription2 = engine.subscribe(Message, handler);
    expect(subscription1).toBe(subscription2);
  });

  it(`Expected that unsubscribe removes specific subscription`, () => {
    const engine = new Engine();
    engine.subscribe(Message, handler1);
    engine.subscribe(Message, handler2);
    engine.subscribe(Message, handler3);
    expect(engine.subscriptions.length).toBe(3);
    engine.unsubscribe(Message, handler1);
    expect(engine.subscriptions.length).toBe(2);
  });

  it(`Expected that unsubscribe removes all relevant subscriptions`, () => {
    const engine = new Engine();
    engine.subscribe(Message, handler1);
    engine.subscribe(Message, handler2);
    engine.subscribe(Message, handler3);
    expect(engine.subscriptions.length).toBe(3);
    engine.unsubscribe(Message);
    expect(engine.subscriptions.length).toBe(0);
  });

  it(`Expected that unsubscribeAll removes all subscriptions`, () => {
    const engine = new Engine();
    engine.subscribe(Message, handler1);
    engine.subscribe(Message, handler2);
    engine.subscribe(Message, handler3);
    expect(engine.subscriptions.length).toBe(3);
    engine.unsubscribeAll();
    expect(engine.subscriptions.length).toBe(0);
  });

  it(`Expected that system\`s message will be delivered through the engine to the handler`, () => {
    const HERO = 'hero';
    const GAME_OVER = 'gameOver';

    class GameOverSystem extends ReactionSystem {
      private dispatched: boolean = false;

      public constructor() {
        super((entity: Entity) => entity.has(HERO));
      }

      public update(dt: number) {
        if (this.dispatched) return;

        if (!this.query.isEmpty && !this.dispatched) {
          this.dispatch(GAME_OVER);
          this.dispatched = true;
        }
      }

      protected prepare() {
        this.dispatched = false;
      }
    }

    let gameOverReceived = false;
    const engine = new Engine();
    const system = new GameOverSystem();
    engine.subscribe(GAME_OVER, () => { gameOverReceived = true; });
    engine.addSystem(system);
    engine.addEntity(new Entity().add(HERO));
    engine.addEntity(new Entity().add(HERO));
    engine.update(1);
    engine.removeAllEntities();
    engine.update(1);
    expect(gameOverReceived).toBeTruthy();
  });

  it(`Expected that system\`s message will be delivered through the engine to the handler`, () => {
    const HERO = 'hero';

    class GameOver {}

    class OtherMessage {}

    class GameOverSystem extends ReactionSystem {
      private dispatched: boolean = false;

      public constructor() {
        super((entity: Entity) => entity.has(HERO));
      }

      public update(dt: number) {
        if (this.dispatched) return;

        if (!this.query.isEmpty && !this.dispatched) {
          this.dispatch(new GameOver());
          this.dispatched = true;
        }
      }

      protected prepare() {
        this.dispatched = false;
      }
    }

    let gameOverReceived = false;
    let otherMessageReceived = false;
    const engine = new Engine();
    const system = new GameOverSystem();
    engine.subscribe(GameOver, () => { gameOverReceived = true; });
    engine.subscribe(OtherMessage, () => { otherMessageReceived = true; });
    engine.addSystem(system);
    engine.addEntity(new Entity().add(HERO));
    engine.addEntity(new Entity().add(HERO));
    engine.update(1);
    engine.removeAllEntities();
    engine.update(1);
    expect(gameOverReceived).toBeTruthy();
    expect(otherMessageReceived).toBeFalsy();
  });

  it(`Expected that removing of not attached query will not throw an error`, () => {
    const TAG = 1;
    const query = new Query((entity: Entity) => entity.has(TAG));
    const engine = new Engine();
    expect(() => {engine.removeQuery(query);}).not.toThrowError();
  });

  it(`Expected that adding the same entity twice will add it only once`, () => {
    const entity = new Entity();
    const engine = new Engine();
    engine.addEntity(entity);
    engine.addEntity(entity);
    expect(engine.entities.length).toBe(1);
  });

  it(`Expected that removing an entity that wasn't added to engine will do nothing`, () => {
    const entity1 = new Entity();
    const entity2 = new Entity();
    const engine = new Engine();
    engine.addEntity(entity1);
    engine.removeEntity(entity2);
    let entityRemovedCount = 0;
    engine.onEntityRemoved.connect((entity) => {
      entityRemovedCount++;
    });
    expect(engine.entities.length).toBe(1);
    expect(engine.entities[0]).toBe(entity1);
    expect(entityRemovedCount).toBe(0);
  });

  it('Getting entity by id from engine should success if entity is in the engine', () => {
    const engine = new Engine();
    const entity = new Entity();
    const id = entity.id;
    engine.addEntity(entity);
    expect(engine.getEntityById(id)).toBe(entity);
  });

  it('Getting entity by id from engine should fail if entity is not in the engine', () => {
    const engine = new Engine();
    const entity = new Entity();
    const id = entity.id;
    engine.addEntity(entity);
    engine.removeEntity(entity);
    expect(engine.getEntityById(id)).toBeUndefined();
  });
});


================================================
FILE: tests/unit/entity.spec.ts
================================================
import {Entity, EntitySnapshot, getComponentId, LinkedComponent} from '../../src';

class Position {
  public x: number = 0;
  public y: number = 0;

  constructor(x: number = 0, y: number = 0) {
    this.x = x;
    this.y = y;
  }
}

class Damage extends LinkedComponent {
  public constructor(
    public value: number,
    id?: string,
  ) {
    super(id);
  }
}

class AnotherDamage extends LinkedComponent {
  public constructor() {
    super();
  }
}

class DamageChild extends Damage {
  public constructor() {
    super(1);
  }
}

describe('Components id', () => {
  it('Getting component id without forcing of id creation returns undefined', () => {
    expect(getComponentId(
      class Test {
      },
    )).toBeUndefined();
  });

  it('Getting component id return equal values for same component twice', () => {
    class Test1 {
    }

    class Test2 {
    }

    expect(getComponentId(Test1, true))
      .toBe(getComponentId(Test1, true));

    expect(getComponentId(Test2, true))
      .toBe(getComponentId(Test2));
  });

  it('Getting components id returns different values', () => {
    class Test1 {
    }

    class Test2 {
    }

    const positionId = getComponentId(Test1, true);
    const viewId = getComponentId(Test2, true);

    expect(positionId).toBeDefined();
    expect(viewId).toBeDefined();

    expect(positionId == viewId).toBeFalsy();
  });
});

describe('Components and Tags', () => {
  it('Adding single component, must to dispatch only onComponentAdded once', () => {
    const entity = new Entity();
    let addedCount = 0;
    let removedCount = 0;

    expect(entity.has(Position)).toBe(false);

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);

    entity.add(new Position());

    entity.onComponentAdded.disconnect(addedCallback);
    entity.onComponentRemoved.disconnect(removedCallback);

    expect(entity.has(Position)).toBe(true);
    expect(addedCount).toBe(1);
    expect(removedCount).toBe(0);
  });

  it('Adding component twice, must override previous component', () => {
    const entity = new Entity();
    let addedCount = 0;
    let removedCount = 0;

    const position1 = new Position(0, 0);
    const position2 = new Position(1, 1);

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);

    entity.add(position1);
    entity.add(position2);

    entity.onComponentAdded.disconnect(addedCallback);
    entity.onComponentRemoved.disconnect(removedCallback);

    expect(entity.get(Position)).toBe(position2);
    expect(entity.getComponents().length).toBe(1);
    expect(addedCount).toBe(2);
    expect(removedCount).toBe(1);
  });

  it(`Adding the same component, must not trigger onComponentAdded for the second call`, () => {
    const entity = new Entity();
    let addedCount = 0;
    let removedCount = 0;

    const position = new Position(0, 0);

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);

    entity.add(position);
    entity.add(position);

    entity.onComponentAdded.disconnect(addedCallback);
    entity.onComponentRemoved.disconnect(removedCallback);

    expect(entity.get(Position)).toBe(position);
    expect(entity.getComponents().length).toBe(1);
    expect(addedCount).toBe(1);
    expect(removedCount).toBe(0);
  });

  it(`Adding component with 'resolve class' ancestor`, () => {
    class Ancestor {}

    class Descendant extends Ancestor {}

    class Descendant2 extends Ancestor {}

    const entity = new Entity();
    entity.add(new Descendant(), Ancestor);
    const id1 = getComponentId(Ancestor);
    const id2 = getComponentId(Descendant);

    expect(id1).not.toEqual(id2);

    expect(entity.has(Ancestor)).toBeTruthy();
    expect(entity.get(Ancestor)).toBeDefined();

    expect(entity.has(Descendant)).toBeFalsy();
    expect(entity.get(Descendant)).toBeUndefined();

    expect(entity.has(Descendant2)).toBeFalsy();
    expect(entity.get(Descendant2)).toBeUndefined();
  });

  it(`Adding component with 'resolve class' not ancestor`, () => {
    class Ancestor {}

    class Descendant extends Ancestor {}

    class Other {}

    const entity = new Entity();
    expect(
      () => { entity.add(new Ancestor(), Descendant); },
    ).toThrow();

    expect(
      () => { entity.add(new Ancestor(), Other); },
    ).toThrow();

  });

  it(`Adding component of type Ancestor should override component with 'resolve class' Ancestor`, () => {
    class Ancestor {}

    class Descendant extends Ancestor {}

    const entity = new Entity();
    const ancestor = new Ancestor();
    const descendant = new Descendant();
    entity.add(descendant, Ancestor);
    expect(entity.get(Ancestor)).toBe(descendant);

    entity.add(ancestor);
    expect(entity.has(Ancestor)).toBeTruthy();
    expect(entity.get(Ancestor)).toBe(ancestor);
  });

  it('Expected that hasAny returns true from component', () => {
    class Other {}

    const entity = new Entity();
    entity.add(new Position());
    expect(entity.hasAny(Other, Position)).toBeTruthy();
  });

  it('Expected that hasAny returns false', () => {
    class Other {}

    class A {}

    const TAG = 'tag';

    const entity = new Entity();
    entity.add(new A());
    entity.add(TAG);
    expect(entity.hasAny(Other, Position)).toBeFalsy();
  });

  it('Expected that hasAll returns true', () => {
    const entity = new Entity();
    const TAG = 12345;
    entity.add(new Position());
    entity.add(TAG);
    expect(entity.hasAll(TAG, Position)).toBeTruthy();
  });

  it('Expected that hasAll returns false', () => {
    class Other {}

    const entity = new Entity();
    entity.add(new Position());
    expect(entity.hasAll(Other, Position)).toBeFalsy();
  });

  it(`Expected that adding a tag dispatches onComponentAdded once`, () => {
    const TAG = 0;
    let addedCount = 0;
    let removedCount = 0;

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;

    const entity = new Entity();
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);
    entity.add(TAG);

    const tags = entity.getTags();
    expect(addedCount).toBe(1);
    expect(entity.tags.size).toBe(1);
    expect(tags.length).toBe(1);
    expect(removedCount).toBe(0);
  });

  it(`Expected that adding a tag twice dispatches onComponentAdded only once`, () => {
    const TAG = 0;
    let addedCount = 0;
    let removedCount = 0;

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;

    const entity = new Entity();
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);
    entity.add(TAG);
    entity.add(TAG);

    expect(addedCount).toBe(1);
    expect(removedCount).toBe(0);
  });

  it(`Expected that entity has an added tag`, () => {
    const TAG = 0;
    const entity = new Entity();
    entity.add(TAG);

    expect(entity.has(TAG)).toBeTruthy();
  });

  it(`Expected that appending the same linked component twice will throw an error`, () => {
    const entity = new Entity();
    const damage = new Damage(10);
    expect(() => {
      entity.append(damage);
      entity.append(damage);
    }).toThrowError();
  });

  it(`Expected that specifying not ancestor as a resolve class for appended component throws an error`, () => {
    const entity = new Entity();
    expect(() => {
      entity.append(new Damage(10), AnotherDamage);
    }).toThrow();
    expect(() => {
      entity.append(new Damage(10), DamageChild);
    }).toThrow();
  });

  it(`Expected that specifying resolve class for appended component gives right resolving`, () => {
    const entity = new Entity();
    const secondChild = new DamageChild();
    const firstChild = new DamageChild();
    entity.append(firstChild, Damage);
    entity.append(secondChild, Damage);
    expect(entity.get(Damage)).toEqual(firstChild);
  });

  it(`Expected that appending the same linked component twice with gaps will throw an error`, () => {
    const entity = new Entity();
    const damage = new Damage(10);
    expect(() => {
      entity.append(damage);
      for (let i = 0; i < 5; i++) {
        entity.append(new Damage(i));
      }
      entity.append(damage);
    }).toThrowError();
  });

  it(`Expected that appending the two different instances of linked component will not throw an error`, () => {
    const entity = new Entity();
    expect(() => {
      entity.append(new Damage(10));
      entity.append(new Damage(10));
    }).not.toThrowError();
  });

  it(`Expected that appending the two different instances of linked component will trigger onComponentAdded only once`, () => {
    const entity = new Entity();
    let addedAmount = 0;
    entity.onComponentAdded.connect(() => { addedAmount++; });
    entity.append(new Damage(10));
    entity.append(new Damage(10));
    expect(addedAmount).toBe(2);
  });

  it(`Removing linked component with "remove" removes whole linked list`, () => {
    const entity = new Entity();
    entity.append(new Damage(10));
    entity.append(new Damage(10));
    entity.remove(Damage);

    expect(entity.get(Damage)).toBeUndefined();
  });

  it(`Removing linked component with "pick" removes only first component`, () => {
    const entity = new Entity();
    const damage1 = new Damage(1);
    const damage2 = new Damage(2);
    entity.append(damage1);
    entity.append(damage2);
    entity.pick(damage1);
    expect(entity.get(Damage)).toBe(damage2);
  });

  it(`get LinkedComponent by id returns specific linked component instance`, () => {
    const entity = new Entity();
    const damage1 = new Damage(1);
    const damage2 = new Damage(2, 'ka-boom');
    entity.append(damage1);
    entity.append(damage2);
    expect(entity.get(Damage, 'ka-boom')).toBe(damage2);
  });

  it(`get regular component by id returns always undefined`, () => {
    const entity = new Entity();
    entity.add(new Position());
    expect(entity.get(Position, 'ka-boom')).toBeUndefined();
  });

  it(`has LinkedComponent with id returns specific linked component instance`, () => {
    const entity = new Entity();
    const damage1 = new Damage(1);
    const damage2 = new Damage(2, 'ka-boom');
    entity.append(damage1);
    entity.append(damage2);
    expect(entity.has(Damage, 'ka-boom')).toBeTruthy();
  });

  it(`has regular Component with id always returns false`, () => {
    const entity = new Entity();
    const position = new Position();
    entity.add(position);
    expect(entity.has(Position, 'ka-boom')).toBeFalsy();
  });

  it(`"pick" by id removes component as expected`, () => {
    const entity = new Entity();
    const damage1 = new Damage(1);
    const damage2 = new Damage(2, 'ka-boom');
    entity.append(damage1);
    entity.append(damage2);
    const picked = entity.pick(Damage, 'ka-boom');
    expect(damage2).toBe(picked);
  });

  it(`"pick" by id won't remove anything if component is not entity`, () => {
    const entity = new Entity();
    const damage1 = new Damage(1);
    const damage2 = new Damage(2);
    entity.append(damage1);
    entity.append(damage2);
    const picked = entity.pick(Damage, 'ka-boom');
    expect(picked).toBeUndefined();
  });

  it(`Withdrawing all components clears linked list associated to component class`, () => {
    const entity = new Entity()
      .append(new Damage(1))
      .append(new Damage(2))
      .append(new Damage(3));

    while (entity.has(Damage)) {
      entity.withdraw(Damage);
    }

    expect(entity.has(Damage)).toBeFalsy();
    expect(entity.getLinkedComponentList(Damage, false)).toBeUndefined();
  });

  it(`"withdraw" returns undefined if there is no linked components appended`, () => {
    const entity = new Entity()
      .add(new Position())
      .append(new Damage(1))
      .append(new Damage(2));

    while (entity.has(Damage)) {
      entity.withdraw(Damage);
    }

    expect(entity.withdraw(Damage)).toBeUndefined();
  });

  it('"contains" returns the same instance if it exists in the linked components appended to the Entity', () => {
    const damage = new Damage(1);
    const entity = new Entity()
      .append(new Damage(1))
      .append(damage)
      .append(new Damage(2));

    expect(entity.contains(damage)).toBeTruthy();
  });

  it('"contains" returns undefined linked component is not appended to the Entity', () => {
    const damage = new Damage(1);
    const entity = new Entity()
      .append(new Damage(1))
      .append(new Damage(2));

    expect(entity.contains(damage)).toBeFalsy();
  });

  it('"contains" returns undefined for linked component registered under another resolveClass', () => {
    const damage = new DamageChild();
    const entity = new Entity()
      .append(damage, Damage);

    expect(entity.contains(damage, DamageChild)).toBeFalsy();
  });

  it('"contains" works for regular components', () => {
    const position = new Position(1, 1);
    const entity = new Entity()
      .append(new Damage(1))
      .append(new Damage(2))
      .add(position);
    expect(entity.contains(position)).toBeTruthy();
  });

  it(`Linked components must be cleared after remove`, () => {
    const entity = new Entity();
    entity.append(new Damage(1));
    entity.append(new Damage(2));
    entity.remove(Damage);
    entity.append(new Damage(3));
    expect(entity.lengthOf(Damage)).toBe(1);
  });

  it(`Find component returns linked component instance accepted by predicate`, () => {
    const entity = new Entity();
    const damage1 = new Damage(1);
    const damage2 = new Damage(2);
    entity
      .append(damage1)
      .append(damage2);
    expect(entity.find(Damage, (it) => it.value === 2)).toBe(damage2);
  });

  it(`Find component returns regular component instance accepted by predicate`, () => {
    const entity = new Entity();
    entity.append(new Damage(1))
          .add(new Position(100, 100));
    expect(entity.find(Position, (it) => it.x === 100 && it.y === 100)).toBe(entity.get(Position));
  });

  it('Entity.linkedComponents returns all linked components instances for specific component class', () => {
    const entity = new Entity();
    entity
      .append(new Damage(1))
      .append(new Damage(2))
      .append(new Damage(3));
    let amount = 0;
    for (const damage of entity.getAll(Damage)) {
      if (damage.value === amount + 1) {
        amount++;
      }
    }
    expect(amount).toBe(3);
  });
});

describe('Removing component', () => {
  it('Simple', () => {
    const entity = new Entity();
    const position = new Position(1, 1);

    let addedCount = 0;
    let removedCount = 0;

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);

    entity.add(position);
    const removedComponent = entity.remove(Position);

    entity.onComponentAdded.disconnect(addedCallback);
    entity.onComponentRemoved.disconnect(removedCallback);

    expect(entity.getComponents().length).toBe(0);
    expect(addedCount).toBe(1);
    expect(removedCount).toBe(1);
    expect(removedComponent).toBeDefined();
    expect(removedComponent).toBe(position);
  });

  it('Removing absent component', () => {
    const entity = new Entity();

    let addedCount = 0;
    let removedCount = 0;

    const addedCallback = () => addedCount++;
    const removedCallback = () => removedCount++;
    entity.onComponentAdded.connect(addedCallback);
    entity.onComponentRemoved.connect(removedCallback);

    const removedComponent = entity.remove(Position);

    entity.onComponentAdded.disconnect(addedCallback);
    entity.onComponentRemoved.disconnect(removedCallback);

    expect(entity.getComponents().length).toBe(0);
    expect(addedCount).toBe(0);
    expect(removedCount).toBe(0);
    expect(removedComponent).toBeUndefined();
  });

  it(`Expected that entity doesn't have removed tag`, () => {
    const TAG = 0;
    const entity = new Entity();
    entity.add(TAG);
    entity.remove(TAG);
    expect(entity.has(TAG)).toBeFalsy();
  });

  it(`Expected that removing absent tag returns undefined`, () => {
    const TAG = 1234;
    const entity = new Entity();
    expect(entity.remove(TAG)).toBeUndefined();
  });

  it(`"withdraw" can remove regular component as well`, () => {
    const entity = new Entity();
    const position = new Position(1, 1);
    const result = entity
      .add(position)
      .withdraw(Position);
    expect(result).toBe(position);
    expect(entity.has(Position)).toBeFalsy();
  });

  it(`"pick" can remove regular component as well`, () => {
    const entity = new Entity();
    const position = new Position(1, 1);
    const result = entity
      .add(position)
      .pick(position);
    expect(result).toBe(position);
    expect(entity.has(Position)).toBeFalsy();
  });
});

describe('Snapshot', () => {
  it(`Expected that checking tag in the blank snapshot gives false`, () => {
    const TAG = 1;
    const snapshot = new EntitySnapshot();
    expect(snapshot.previous.has(TAG)).toBeFalsy();
  });

  it('Expect undefined value (but not throwing an error) for getting component instance, if snapshot not initialized', () => {
    class Component {}

    const snapshot = new EntitySnapshot();
    expect(() => snapshot.previous.get(Component)).not.toThrowError();
    expect(snapshot.previous.get(Component)).toBeUndefined();
  });

  it('Expect undefined value for class that was not being initialized as component', () => {
    class Component {}

    class NotAComponent {}

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

    const snapshot = new EntitySnapshot();
    entity.takeSnapshot(snapshot, new Component());
    expect(() => snapshot.previous.get(NotAComponent)).not.toThrowError();
    expect(snapshot.previous.get(NotAComponent)).toBeUndefined();
  });

  it(`Expected that added component appears in current state, but not in the previous`, () => {
    class ComponentA {}

    class ComponentB {}

    const TAG_C = 'tag-c';

    const snapshot = new EntitySnapshot();
    const entity = new Entity().add(new ComponentA());
    entity.onComponentAdded.connect((entity, componentOrTag) => {
      entity.takeSnapshot(snapshot, componentOrTag);
    });

    {
      entity.add(new ComponentB());
      expect(snapshot.current.has(ComponentB)).toBeTruthy();
      expect(snapshot.current.get(ComponentB)).toBeDefined();
      expect(snapshot.previous.has(ComponentB)).toBeFalsy();
      expect(snapshot.previous.get(ComponentB)).toBeUndefined();
    }
    {
      entity.add(TAG_C);
      expect(snapshot.current.has(TAG_C)).toBeTruthy();
      expect(snapshot.previous.has(TAG_C)).toBeFalsy();
    }
  });

  it(`Expected that removed component appears in previous state, but not in the current`, () => {
    class ComponentA {}

    const TAG_C = 'tag-c';

    const snapshot = new EntitySnapshot();
    const entity = new Entity().add(new ComponentA()).add(TAG_C);
    entity.onComponentRemoved.connect((entity, componentOrTag) => {
      entity.takeSnapshot(snapshot, componentOrTag);
    });

    {
      entity.remove(ComponentA);
      const current = snapshot.current;
      const previous = snapshot.previous;
      expect(current.has(ComponentA)).toBeFalsy();
      expect(current.get(ComponentA)).toBeUndefined();
      expect(previous.has(ComponentA)).toBeTruthy();
      expect(previous.get(ComponentA)).toBeDefined();
    }
    {
      entity.remove(TAG_C);
      expect(snapshot.current.has(TAG_C)).toBeFalsy();
      expect(snapshot.previous.has(TAG_C)).toBeTruthy();
    }
  });

  it('Adding linked component must replace all existing linked component instances', () => {
    const entity = new Entity()
      .append(new Damage(1))
      .append(new Damage(2))
      .append(new Damage(3));

    entity.add(new Damage(100));
    expect(entity.lengthOf(Damage)).toBe(1);
  });

  it('Replacing linked component with "add" must trigger onComponentRemoved for every appended linked component', () => {
    const entity = new Entity()
      .append(new Damage(1))
      .append(new Damage(2))
      .append(new Damage(3));

    let removedNumber = 0;
    entity.onComponentRemoved.connect(() => {
      removedNumber++;
    });
    entity.add(new Damage(100));
    expect(removedNumber).toBe(3);
  });
});


================================================
FILE: tests/unit/linked.list.spec.ts
================================================
import {LinkedComponentList} from '../../src/ecs/LinkedComponentList';
import {LinkedComponent} from '../../src';

class Component extends LinkedComponent {}

describe('Linked list', () => {
  it(`Adding component to the empty list putting it to the head`, () => {
    const list = new LinkedComponentList();
    const component = new Component();
    list.add(component);
    expect(list.head).toBe(component);
  });

  it(`Adding component to the empty list makes it non-empty`, () => {
    const list = new LinkedComponentList();
    const component = new Component();
    expect(list.isEmpty).toBeTruthy();
    list.add(component);
    expect(list.isEmpty).toBeFalsy();
  });

  it(`Removing component from the list makes it empty`, () => {
    const list = new LinkedComponentList();
    const component = new Component();
    list.add(component);
    expect(list.remove(component)).toBeTruthy();
    expect(list.isEmpty).toBeTruthy();
  });

  it(`Removing component from the the empty list returns false`, () => {
    const list = new LinkedComponentList();
    const component = new Component();
    expect(list.remove(component)).toBeFalsy();
  });

  it(`Removing not head component from the the list not makes it empty`, () => {
    const list = new LinkedComponentList();
    const component1 = new Component();
    const component2 = new Component();
    list.add(component1);
    list.add(component2);
    list.remove(component2);
    expect(list.isEmpty).toBeFalsy();
  });

  it(`"iterate" iterates through all components in the list`, () => {
    const list = new LinkedComponentList();
    const components = [new Component(), new Component(), new Component()];
    components.forEach((component) => list.add(component));
    list.iterate((component) => {
      const index = components.indexOf(component);
      expect(index).not.toBe(-1);
      components.splice(index, 1);
    });
    expect(components.length).toBe(0);
  });

  it('removing current component during iteration won\'t breaks iteration', () => {
    const list = new LinkedComponentList();
    const components = [new Component(), new Component(), new Component()];
    components.forEach((component) => list.add(component));
    list.iterate((component) => {
      list.remove(component);
      const index = components.indexOf(component);
      expect(index).not.toBe(-1);
      components.splice(index, 1);
    });
    expect(components.length).toBe(0);
  });

  it(`"clear" removes all components from the list`, () => {
    const list = new LinkedComponentList();
    list.add(new Component());
    list.add(new Component());
    list.add(new Component());
    list.clear();
    expect(list.isEmpty).toBeTruthy();
  });
});


================================================
FILE: tests/unit/query.spec.ts
================================================
import {Engine, Entity, LinkedComponent, Query, QueryBuilder} from '../../src';

class Position {
  public x: number = 0;
  public y: number = 0;

  constructor(x: number = 0, y: number = 0) {
    this.x = x;
    this.y = y;
  }
}

class View {}

class Move {}

class Stay {}

class Damage extends LinkedComponent {}

describe('Query builder', () => {
  it('Building query', () => {
    const query = new QueryBuilder()
      .contains(Position)
      .contains(View)
      .build();
    expect(query).toBeDefined();
    expect(query.entities).toBeDefined();
    expect(query.isEmpty).toBeTruthy();
  });

  it('Expected that built query matches defined pattern', () => {
    const query = new QueryBuilder()
      .contains(Position)
      .contains(View)
      .build();
    const entities = [
      new Entity().add(new Position()).add(new View()),
      new Entity().add(new Position()).add(new View()),
    ];
    query.matchEntities(entities);
    expect(query.length).toBe(2);
  });

  it(`Expected that adding the same component to the builder twice will use only it only once for construction of predicate `, () => {
    const builder = new QueryBuilder()
      .contains(Position)
      .contains(Position)
      .contains(View);
    expect(builder.getComponents().size).toBe(2);
  });

  it(`Expected that adding the same tag to the builder twice will use only it only once for construction of predicate `, () => {
    const TAG = 1;
    const builder = new QueryBuilder()
      .contains(TAG)
      .contains(TAG);
    expect(builder.getTags().size).toBe(1);
  });

  it(`Expected that query built with QueryBuilder matches entities with provided conditions`, () => {
    const TAG = 1;
    const query = new QueryBuilder().contains(Position, TAG).build();
    query.matchEntities([
      new Entity().add(new Position()).add(TAG),
      new Entity(),
      new Entity().add(new Position()),
      new Entity().add(TAG),
    ]);
    expect(query.length).toBe(1);
  });

  it(`Expected that query built with QueryBuilder matches entities with provided conditions (no components)`, () => {
    const TAG = 1;
    const query = new QueryBuilder().contains(TAG).build();
    query.matchEntities([
      new Entity().add(new Position()).add(TAG),
      new Entity(),
      new Entity().add(new Position()),
      new Entity().add(TAG),
    ]);
    expect(query.length).toBe(2);
  });

  it(`Expected that query built with QueryBuilder matches entities with provided conditions (no tags)`, () => {
    const TAG = 1;
    const query = new QueryBuilder().contains(Position).build();
    query.matchEntities([
      new Entity().add(new Position()).add(TAG),
      new Entity(),
      new Entity().add(new Position()),
      new Entity().add(TAG),
    ]);
    expect(query.length).toBe(2);
  });
});

describe('Query matching', () => {
  const position = new Position();
  const view = new View();
  const move = new Move();
  const stay = new Stay();

  function getQuery() {
    return new QueryBuilder()
      .contains(Position, View)
      .build();
  }

  it('Query not matching entity with only position component', () => {
    const engine = new Engine();
    const entity = new Entity().add(position);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);
    expect(query.entities).toBeDefined();
    expect(query.isEmpty).toBeTruthy();
  });

  it('Query not matching entity with only view component', () => {
    const engine = new Engine();
    const entity = new Entity().add(view);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.isEmpty).toBeTruthy();
  });

  it('Query matching entity with view and position components', () => {
    const engine = new Engine();
    const entity = new Entity().add(position).add(view);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.isEmpty).toBeFalsy();
    expect(query.entities[0]).toBe(entity);
  });

  it(`Expected that 'has' returns true for entity that is in the query`, () => {
    const targetEntity = new Entity().add(view);
    const entities = [
      new Entity().add(position),
      targetEntity,
      new Entity().add(view).add(position),
    ];
    const query = new Query((entity) => entity.has(View));
    query.matchEntities(entities);
    expect(query.has(targetEntity)).toBeTruthy();
  });

  it('Adding component to entity adding it to query', () => {
    const engine = new Engine();
    const entity = new Entity().add(position);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.isEmpty).toBeTruthy();

    entity.add(view);

    expect(query.entities.length).toBe(1);
  });

  it('Removing component removes entity from query', () => {
    const engine = new Engine();
    const entity = new Entity().add(position).add(view);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.entities.length).toBe(1);
    expect(query.entities[0]).toBe(entity);

    entity.remove(View);

    expect(query.isEmpty).toBeTruthy();
  });

  it('Removing not matching with query components not removes entity from query', () => {
    const engine = new Engine();
    const entity = new Entity()
      .add(position)
      .add(view)
      .add(move);

    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.entities.length).toBe(1);
    expect(query.entities[0]).toBe(entity);

    entity.remove(Move);

    expect(query.entities.length).toBe(1);
    expect(query.entities[0]).toBe(entity);

    entity.add(stay);

    expect(query.entities.length).toBe(1);
    expect(query.entities[0]).toBe(entity);

    entity.remove(View);

    expect(query.isEmpty).toBeTruthy();
  });

  it('Removing entity from engine removes entity from query', () => {
    const engine = new Engine();
    const entity = new Entity().add(position).add(view);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.entities.length).toBe(1);
    expect(query.entities[0]).toBe(entity);

    engine.removeEntity(entity);

    expect(query.isEmpty).toBeTruthy();
  });

  it('Removing query from engine clears query and not updating it anymore', () => {
    const engine = new Engine();
    const entity = new Entity().add(position).add(view);
    const query = getQuery();
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities).toBeDefined();
    expect(query.entities.length).toBe(1);
    expect(query.entities[0]).toBe(entity);

    engine.removeQuery(query);
    expect(query.isEmpty).toBeTruthy();

    engine.removeEntity(entity);
    engine.addEntity(entity);

    expect(query.isEmpty).toBeTruthy();
  });

  it('Entity invalidation should add entity to query with custom predicate', () => {
    const engine = new Engine();
    const entity = new Entity().add(new Position(0, 0));
    const query = new Query((entity: Entity) => {
      return entity.has(Position) && entity.get(Position)!.y > 100;
    });
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities.length).toBe(0);
    entity.get(Position)!.y = 150;
    entity.invalidate();
    expect(query.entities.length).toBe(1);
  });

  it('Entity invalidation should remove entity from query with custom predicate', () => {
    const engine = new Engine();
    const entity = new Entity().add(new Position(0, 0));
    const query = new Query((entity: Entity) => {
      return entity.has(Position) && entity.get(Position)!.y === 0;
    });
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities.length).toBe(1);
    entity.get(Position)!.y = 150;
    entity.invalidate();
    expect(query.entities.length).toBe(0);
  });

  it('Entity invalidation should add entity to query with custom predicate', () => {
    const engine = new Engine();
    const entity = new Entity().add(new Position(0, 150));
    const query = new Query((entity: Entity) => {
      return entity.has(Position) && entity.get(Position)!.y === 0;
    });
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.entities.length).toBe(0);
    entity.get(Position)!.y = 0;
    entity.invalidate();
    expect(query.entities.length).toBe(1);
  });

  it('Removing and adding components to entity should properly update custom query', () => {
    const engine = new Engine();
    const entity = new Entity().add(new Position(0, 0));
    const query = new Query((entity: Entity) => {
      return entity.has(Position) && !entity.has(View);
    });
    engine.addQuery(query);
    engine.addEntity(entity);

    expect(query.length).toBe(1);
    entity.add(new View());
    expect(query.length).toBe(0);
    entity.remove(View);
    expect(query.length).toBe(1);
  });

  it('Adding and removing entity that not related to query, must not affect it', () => {
    const engine = new Engine();
    const entity1 = new Entity().add(new Position(0, 0));
    const entity2 = new Entity();
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    engine.addQuery(query);
    engine.addEntity(entity1);
    expect(query.length).toBe(1);
    engine.addEntity(entity2);
    expect(query.length).toBe(1);
    engine.removeEntity(entity2);
    expect(query.length).toBe(1);
  });

  it(`countBy returns the number of elements that tested by predicate successfully`, () => {
    const initialEntitiesAmount = 10;
    const entitiesWithViewAmount = 4;

    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });

    const entities = [];
    for (let i = 0; i < initialEntitiesAmount; i++) {
      const entity = new Entity().add(new Position());
      if (i < entitiesWithViewAmount) {
        entity.add(new View());
      }
      entities.push(entity);
    }
    query.matchEntities(entities);
    expect(query.countBy((entity: Entity) => entity.hasAll(View, Position))).toBe(entitiesWithViewAmount);
  });

  it(`countBy returns zero for empty query`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    expect(query.countBy((entity: Entity) => entity.hasAll(Position))).toBe(0);
  });

  it(`'first' getter returns first element from the query`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    const entities = [new Entity().add(new Position()), new Entity().add(new Position())];
    const firstElement = entities[0];
    query.matchEntities(entities);
    expect(query.first).toBe(firstElement);
  });

  it(`'first' getter returns undefined if the query is empty`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    expect(query.first).toBeUndefined();
  });

  it(`'last' getter returns last element from the query`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    const entities = [new Entity().add(new Position()), new Entity().add(new Position())];
    const lastElement = entities[1];
    query.matchEntities(entities);
    expect(query.last).toBe(lastElement);
  });

  it(`'last' getter returns undefined if the query is empty`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    expect(query.last).toBeUndefined();
  });

  it(`'find' returns first element that is accepted by predicate`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    const entities = [
      new Entity().add(new Position()),
      new Entity().add(new Position()).add(new View()),
      new Entity().add(new Position()).add(new View()),
    ];
    const targetEntity = entities[1];
    query.matchEntities(entities);
    expect(query.find((value) => value.has(View))).toBe(targetEntity);
  });

  it(`'find' returns undefined when no suitable elements found`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    query.matchEntities([
      new Entity().add(new Position()),
      new Entity().add(new Position()),
      new Entity().add(new Position()),
    ]);
    expect(query.find((value) => value.has(View))).toBeUndefined();
  });

  it(`'filter' returns all suitable elements`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    const TAG = 'tag';
    const entities = [
      new Entity().add(new Position()),
      new Entity().add(new Position()).add(TAG),
      new Entity().add(new Position()).add(TAG),
    ];
    query.matchEntities(entities);
    const filteredItems = query.filter((value) => value.has(TAG));
    expect(filteredItems.length).toBe(2);
    expect(filteredItems[0]).toBe(entities[1]);
    expect(filteredItems[1]).toBe(entities[2]);
  });

  it(`'filter' returns empty array when no suitable elements found`, () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Position);
    });
    const TAG = 'tag';
    const entities = [
      new Entity().add(TAG),
      new Entity().add(TAG),
      new Entity().add(TAG),
    ];
    query.matchEntities(entities);
    const filteredItems = query.filter((value) => value.has(Position));
    expect(filteredItems.length).toBe(0);
  });

  it(`appending LinkedComponent affects query`, () => {
    class LogComponent extends LinkedComponent {
      public constructor(public readonly log: string) {
        super();
      }
    }

    const query = new Query((entity: Entity) => {
      return entity.has(LogComponent);
    });
    const entity = new Entity()
      .append(new LogComponent('test'));

    query.matchEntities([entity]);
    expect(query.length).toBe(1);
  });
});

describe('Query signals', () => {
  it('Query`s onEntityAdded must be invoked when entity is added to query', () => {
    const query = new Query((entity: Entity) => {
      return entity.hasAll(View, Position);
    });

    let currentState = undefined;
    let previousState = undefined;

    query.onEntityAdded.connect(snapshot => {
      currentState = snapshot.current.hasAll(View, Position);
      previousState = snapshot.previous.has(View) && !snapshot.previous.has(Position);
    });

    const entity = new Entity()
      .add(new View());

    query.matchEntities([entity]);
    entity.add(new Position());
    query.entityComponentAdded(entity, entity.get(Position)!, Position);

    expect(currentState).toBeTruthy();
    expect(previousState).toBeTruthy();
  });

  it('Query`s onEntityRemoved must be invoked when entity is removed from query', () => {
    const query = new Query((entity: Entity) => {
      return entity.hasAll(View, Position);
    });

    let currentState = undefined;
    let previousState = undefined;

    query.onEntityRemoved.connect(snapshot => {
      currentState = snapshot.current.has(View) && !snapshot.current.has(Position);
      previousState = snapshot.previous.hasAll(View, Position);
    });

    const entity = new Entity()
      .add(new View())
      .add(new Position());

    query.matchEntities([entity]);
    query.entityComponentRemoved(entity, entity.remove(Position)!, Position);

    expect(currentState).toBeTruthy();
    expect(previousState).toBeTruthy();
  });

  it('Query`s onEntityAdded must be invoked when specific component is removed from entity', () => {
    const query = new Query((entity: Entity) => {
      return entity.has(View) && !entity.has(Position);
    });

    let currentState = undefined;
    let previousState = undefined;

    query.onEntityAdded.connect(snapshot => {
      currentState = snapshot.current.has(View) && !snapshot.current.has(Position);
      previousState = snapshot.previous.hasAll(View, Position);
    });

    const entity = new Entity()
      .add(new View())
      .add(new Position());

    query.matchEntities([entity]);
    query.entityComponentRemoved(entity, entity.remove(Position)!, Position);

    expect(currentState).toBeTruthy();
    expect(previousState).toBeTruthy();
  });

  it('Query`s onEntityRemoved must be invoked when specific component is added to entity', () => {
    const query = new Query((entity: Entity) => {
      return entity.has(View) && !entity.has(Position);
    });

    let currentState = undefined;
    let previousState = undefined;

    query.onEntityRemoved.connect(snapshot => {
      currentState = snapshot.current.hasAll(View, Position);
      previousState = snapshot.previous.has(View) && !snapshot.previous.has(Position);
    });

    const entity = new Entity()
      .add(new View());

    query.matchEntities([entity]);
    entity.add(new Position());
    query.entityComponentAdded(entity, entity.get(Position)!, Position);

    expect(currentState).toBeTruthy();
    expect(previousState).toBeTruthy();
  });

  it('Query onEntityAdded mustn\'t be triggered more than once if several linked components added to the entity', () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Damage);
    });

    let addedNumber = 0;
    query.onEntityAdded.connect(snapshot => {
      addedNumber++;
    });

    const entity = new Entity()
      .append(new Damage());

    query.matchEntities([entity]);
    for (let i = 0; i < 3; i++) {
      const damage = new Damage();
      entity.append(damage);
      query.entityComponentAdded(entity, damage, Damage);
    }

    expect(addedNumber).toBe(1);
  });

  it('Query onEntityRemoved must be triggered only when last linked component withdrawn', () => {
    const query = new Query((entity: Entity) => {
      return entity.has(Damage);
    });

    let removedNumber = 0;
    query.onEntityRemoved.connect(snapshot => {
      removedNumber++;
    });

    const entity = new Entity()
      .append(new Damage())
      .append(new Damage())
      .append(new Damage());

    query.matchEntities([entity]);
    while (entity.has(Damage)) {
      const damage = entity.withdraw(Damage)!;
      query.entityComponentRemoved(entity, damage, Damage);
    }

    expect(removedNumber).toBe(1);
  });

  it(`Query.entityComponentAdded will be call after all other connected handlers`, () => {
    const engine = new Engine();
    const query = new Query((entity) => entity.has(View));
    const entity = new Entity();
    engine.addQuery(query);
    engine.addEntity(entity);

    let callIndex = 0;
    let queryCallIndex;
    entity.onComponentAdded.connect(() => {
      callIndex++;
    });
    query.onEntityAdded.connect(() => {
      queryCallIndex = callIndex++;
    });
    entity.add(new View());
    expect(queryCallIndex).toBe(1);
  });

  it(`Query.entityComponentRemoved must be called only when all linked components are removed and after all other handlers`, () => {
    const engine = new Engine();
    const query = new Query((entity) => entity.has(Damage));
    const entity = new Entity();
    engine.addQuery(query);
    engine.addEntity(entity);
    entity
      .append(new Damage())
      .append(new Damage())
      .append(new Damage())
      .append(new Damage())
      .append(new Damage());
    let callIndex = 0;
    let queryCallIndex;
    entity.onComponentRemoved.connect(() => {
      callIndex++;
    }, 1000);
    query.onEntityRemoved.connect(() => {
      queryCallIndex = callIndex++;
    });
    entity.remove(Damage);
    expect(queryCallIndex).toBe(5);
  });
});


================================================
FILE: tests/unit/shared.config.spec.ts
================================================
import {Engine, Query, System} from '../../src';

describe('Shared config', () => {
  it('Shared config is accessible when system added to engine', () => {
    let sharedConfigAccessible = false;

    const engine = new Engine();
    const system = new class extends System {
      public onAddedToEngine() {
        sharedConfigAccessible = this.sharedConfig !== undefined;
      }
    }();

    expect(() => {
      engine.addSystem(system);
    }).not.toThrowError();
    expect(sharedConfigAccessible).toBeTruthy();
  });

  it('Accessing shared config throws an error, when system is not added to engine', () => {
    expect(() => {
      new class extends System {
        public constructor() {
          super();
          this.sharedConfig;
        }
      };
    }).toThrowError();
  });

  it(`Shared config can't be removed from engine`, () => {
    expect(() => {
      class Component {}

      const engine = new Engine();
      engine.sharedConfig.add(new Component());
      engine.removeEntity(engine.sharedConfig);

      let sharedConfigAccessibleAndStillTheSame = false;
      const system = new class extends System {
        public onAddedToEngine() {
          sharedConfigAccessibleAndStillTheSame = this.sharedConfig !== undefined && this.sharedConfig.has(Component);
        }
      };
      engine.addSystem(system);
      expect(sharedConfigAccessibleAndStillTheSame).toBeTruthy();
    });
  });

  it(`Shared config is presented in the queries`, () => {
    expect(() => {
      const TAG = 'tag';
      const engine = new Engine();
      const query = new Query((entity) => entity.has(TAG));
      engine.sharedConfig.add(TAG);
      engine.addQuery(query);

      expect(query.length).toBe(1);
      expect(query.first).toBe(engine.sharedConfig);
    });
  });
});


================================================
FILE: tests/unit/signal.spec.ts
================================================
import {Signal} from '../../src/utils/Signal';

describe('Signals', function () {
  it('Connecting increases amount of handlers', () => {
    const signal = new Signal<(value: number) => void>();
    signal.connect((value: number) => {});
    expect(signal.hasHandlers).toBeTruthy();
    expect(signal.handlersAmount).toEqual(1);
  });

  it('Connecting same handler twice add it only once', () => {
    const signal = new Signal<(value: number) => void>();
    const handler = (value: number) => {};
    signal.connect(handler);
    signal.connect(handler);
    expect(signal.handlersAmount).toBe(1);
  });

  it('Disconnecting decreases amount of handlers', () => {
    const signal = new Signal<(value: number) => void>();
    const handler = (value: number) => {};
    signal.connect(handler);
    signal.disconnect(handler);
    expect(signal.hasHandlers).toBeFalsy();
  });

  it('Disconnecting not connected handler don\'t remove any existing handler', () => {
    const signal = new Signal<(value: number) => void>();
    const addedHandler = (value: number) => {};
    const wrongHandler = (value: number) => {};
    signal.connect(addedHandler);
    signal.disconnect(wrongHandler);
    expect(signal.handlersAmount).toEqual(1);
  });

  it('Disconnecting all handlers clears them from signal', () => {
    const signal = new Signal<(value: number) => void>();
    signal.connect(() => {});
    signal.connect(() => {});
    signal.connect(() => {});
    signal.disconnectAll();
    expect(signal.hasHandlers).toBeFalsy();
  });
});


================================================
FILE: tests/unit/system.spec.ts
================================================
import {Engine, Entity, EntitySnapshot, IterativeSystem, Query, QueryBuilder, System} from '../../src';

class Position {
  public x: number = 0;
  public y: number = 0;

  public constructor(x: number = 0, y: number = 0) {
    this.x = x;
    this.y = y;
  }
}

class MovementSystem extends IterativeSystem {
  public constructor() {
    super(new QueryBuilder().contains(Position).build());
  }

  protected updateEntity(entity: Entity, dt: number): void {
    const position = entity.get(Position);
    if (position != null) {
      position.x += 10 * dt;
      position.y += 10 * dt;
    }
  }

  protected entityAdded = ({current}: EntitySnapshot) => {
    current.get(Position)!.x = 100;
  };
}

describe('Iterative system', () => {
  it('Updating entities', () => {
    const engine = new Engine();
    const entity = new Entity().add(new Position());

    engine.addSystem(new MovementSystem());
    engine.addEntity(entity);
    engine.update(1);

    const position = entity.get(Position);
    expect(position).toBeDefined();
    expect(position!.x).toBe(110);
    expect(position!.y).toBe(10);
  });

  it('Entities in prepare should be available', () => {
    let entities!: ReadonlyArray<Entity>;

    class TestSystem extends IterativeSystem {
      public constructor() {
        super(new QueryBuilder().contains(Position).build());
      }

      protected prepare() {
        entities = this.entities;
      }

      protected updateEntity(entity: Entity, dt: number): void {
      }
    }

    const engine = new Engine();
    const entitiesCount = 5;
    for (let i = 0; i < entitiesCount; i++) {
      engine.addEntity(new Entity().add(new Position()));
    }
    engine.addSystem(new TestSystem());

    expect(entities).toBeDefined();
    expect(entities.length).toBe(entitiesCount);

  });

  it('Adding and removing should properly construct EntitySnapshot ', () => {
    let onRemoved: { snapshot?: boolean, entity?: boolean } = {snapshot: undefined, entity: undefined};
    let onAdded: { snapshot?: boolean, entity?: boolean } = {snapshot: undefined, entity: undefined};

    class MovementSystem extends IterativeSystem {
      public constructor() {
        super(new QueryBuilder().contains(Position).build());
      }

      protected updateEntity(entity: Entity, dt: number): void {
      }

      protected entityAdded = ({current, previous}: EntitySnapshot) => {
        onAdded = {snapshot: previous.has(Position), entity: current.has(Position)};
      };

      protected entityRemoved = ({current, previous}: EntitySnapshot) => {
        onRemoved = {snapshot: previous.has(Position), entity: current.has(Position)};
      };
    }

    const engine = new Engine();
    const entity = new Entity();
    const system = new MovementSystem();

    engine.addSystem(system);
    engine.addEntity(entity);
    engine.update(1);

    entity.add(new Position());
    entity.remove(Position);

    expect(onAdded).toEqual({snapshot: false, entity: true});
    expect(onRemoved).toEqual({snapshot: true, entity: false});
  });

  it("Entities safe removal during iteration should not break the iteration ordering", () => {
    class Health {
      public constructor(public value: number) {
      }
    }

    class HealthTickSystem extends IterativeSystem {
      public constructor() {
        super(new QueryBuilder().contains(Health).build());
      }

      protected updateEntity(entity: Entity, dt: number): void {
        const health = entity.get(Health)!;
        health.value -= 1;
        if (health.value <= 0) {
          this.engine.removeEntity(entity, true);
        }
      }
    }

    const engine = new Engine();
    engine.addSystem(new HealthTickSystem());
    for (let i = 0; i < 5; i++) {
      engine.addEntity(new Entity().add(new Health(1)));
    }
    engine.update(1);
    expect(engine.entities.length).toBe(0);
  })

  it.each([true, false])(`Re-adding entities which were removed should work after the engine update cycle`, (safe) => {
    const engine = new Engine();
    const query = new QueryBuilder().contains(Position).build();
    engine.addQuery(query);

    for (let i = 0; i < 5; i++) {
      engine.addEntity(new Entity().add(new Position()));
    }

    const entities = query.entities.concat()
    for (let entity of entities) {
      engine.removeEntity(entity, safe);
    }
    for (let entity of entities) {
      engine.addEntity(entity);
    }
    engine.update(0);
    expect(engine.entities.length).toBe(5);
  })
});

describe('Failure on accessing engine if not attached to it', () => {
  it(`Expected that engine can't be accessed if system is not attached to it`, () => {
    class Message {
    }

    class TestSystem extends System {
      public update(dt: number) {
        this.engine.addEntity(new Entity());
      }
    }

    const system = new TestSystem();
    expect(() => system.update(0)).toThrowError();
  });

  it(`Expected that message can't be sent if system is not attached to the engine`, () => {
    class Message {
    }

    class TestSystem extends System {
      public update(dt: number) {
        this.dispatch(new Message());
      }
    }

    const system = new TestSystem();
    expect(() => system.update(0)).toThrowError();
  });

  it(`Expected that removing system from engine breaking the iteration`, () => {
    class Component {
    }

    let amountOfIterations = 0;

    class TestSystem extends IterativeSystem {
      public constructor() {
        super(new Query(entity => entity.has(Component)));
      }

      protected updateEntity(entity: Entity, dt: number) {
        // In case if iteration continues - after removing system from engine
        // then the line below should throw an exception
        this.engine.clear();
        amountOfIterations++;
      }
    }

    const engine = new Engine();
    engine.addSystem(new TestSystem());
    engine.addEntity(new Entity().add(new Component()));
    engine.addEntity(new Entity().add(new Component()));
    engine.addEntity(new Entity().add(new Component()));
    expect(() => {
      engine.update(0);
    }).not.toThrowError();
    expect(amountOfIterations).toBe(1);
  });

  it(`Iterative system should iterate over entities after removing and subsequent adding it to the engine`, () => {
    class Component {
    }

    const engine = new Engine();
    const entity = new Entity().add(new Component());
    let iterationsCount = 0;
    const system = new class extends IterativeSystem {
      public constructor() {
        super((entity) => entity.has(Component));
      }

      protected updateEntity(entity: Entity, dt: number) {
        iterationsCount++;
      }
    }();
    engine.addEntity(entity);

    engine.addSystem(system);
    engine.update(1);

    engine.removeSystem(system);
    engine.update(1);

    engine.addSystem(system);
    engine.update(1);

    expect(iterationsCount).toBe(2);
  });

  it(`After removal request system must be deleted`, () => {
    const engine = new Engine();
    let iterationsCount = 0;
    const system = new class extends System {
      public update(dt: number) {
        iterationsCount++;
        this.requestRemoval();
      }
    };
    engine.addSystem(system);
    for (let i = 0; i < 5; i++) {
      engine.update(0);
    }
    expect(iterationsCount).toBe(1);
  });
});

================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "moduleResolution": "node",
    "module": "commonjs",
    "target": "es2016",
    "lib": [
      "es2016"
    ],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "noEmitHelpers": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "importHelpers": true,
    "downlevelIteration": false,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "outDir": "lib",
    "typeRoots": ["node_modules/@types"],
    "stripInternal": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "tests/**/*"
  ]
}
Download .txt
gitextract_slnklggk/

├── .github/
│   └── workflows/
│       ├── build.yml
│       └── publish.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest-ci.json
├── package.json
├── src/
│   ├── ecs/
│   │   ├── ComponentId.ts
│   │   ├── Engine.ts
│   │   ├── Entity.ts
│   │   ├── IterativeSystem.ts
│   │   ├── LinkedComponent.ts
│   │   ├── LinkedComponentList.ts
│   │   ├── Query.ts
│   │   ├── ReactionSystem.ts
│   │   ├── Subscription.ts
│   │   ├── System.ts
│   │   └── Tag.ts
│   ├── index.ts
│   └── utils/
│       ├── Class.ts
│       └── Signal.ts
├── tests/
│   └── unit/
│       ├── engine.spec.ts
│       ├── entity.spec.ts
│       ├── linked.list.spec.ts
│       ├── query.spec.ts
│       ├── shared.config.spec.ts
│       ├── signal.spec.ts
│       └── system.spec.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (249 symbols across 19 files)

FILE: src/ecs/ComponentId.ts
  function getComponentId (line 10) | function getComponentId<T>(
  function getComponentClass (line 25) | function getComponentClass<T extends K, K>(component: NonNullable<T>, re...
  constant COMPONENT_CLASS_ID (line 36) | let COMPONENT_CLASS_ID = '__componentClassId__';
  type ComponentId (line 39) | type ComponentId<T> = Class<T> & {

FILE: src/ecs/Engine.ts
  class Engine (line 11) | class Engine {
    method entities (line 32) | public get entities(): ReadonlyArray<Entity> {
    method systems (line 39) | public get systems(): ReadonlyArray<System> {
    method queries (line 46) | public get queries(): ReadonlyArray<Query> {
    method constructor (line 50) | public constructor() {
    method subscriptions (line 57) | public get subscriptions(): ReadonlyArray<Subscription<any>> {
    method sharedConfig (line 66) | public get sharedConfig(): Entity {
    method addEntity (line 77) | public addEntity(entity: Entity): Engine {
    method removeEntity (line 99) | public removeEntity(entity: Entity, safe: boolean = false): Engine {
    method getEntityById (line 114) | public getEntityById(id: number): Entity | undefined {
    method removeSystem (line 126) | public removeSystem(system: System): Engine {
    method update (line 140) | public update(dt: number): void {
    method getSystem (line 163) | public getSystem<T extends System>(systemClass: Class<T>): T | undefin...
    method removeAllSystems (line 170) | public removeAllSystems(): void {
    method removeAllQueries (line 182) | public removeAllQueries(): void {
    method removeAllEntities (line 195) | public removeAllEntities(): void {
    method clear (line 204) | public clear(): void {
    method removeEntityNow (line 210) | private removeEntityNow(entity: Entity): Engine {
    method addQuery (line 228) | public addQuery(query: Query): Engine {
    method addSystem (line 242) | public addSystem(system: System, priority: number = 0): Engine {
    method removeQuery (line 265) | public removeQuery(query: Query) {
    method subscribe (line 281) | public subscribe<T>(messageType: Class<T> | T, handler: (value: T) => ...
    method unsubscribe (line 292) | public unsubscribe<T>(messageType: Class<T> | T, handler?: (value: T) ...
    method unsubscribeAll (line 299) | public unsubscribeAll(): void {
    method addSubscription (line 306) | public addSubscription<T>(messageType: Class<T> | T, handler: (value: ...
    method removeSubscription (line 318) | public removeSubscription<T>(messageType: Class<T> | T, handler: ((val...
    method dispatch (line 332) | public dispatch<T>(message: T) {
    method connectEntity (line 340) | private connectEntity(entity: Entity) {
    method disconnectEntity (line 346) | private disconnectEntity(entity: Entity) {
    method connectQuery (line 352) | private connectQuery(query: Query) {
    method disconnectQuery (line 357) | private disconnectQuery(query: Query) {
    method removeAllEntitiesInternal (line 362) | private removeAllEntitiesInternal(silently: boolean): void {

FILE: src/ecs/Entity.ts
  type ReadonlyEntity (line 11) | interface ReadonlyEntity {
  class Entity (line 237) | class Entity implements ReadonlyEntity {
    method components (line 271) | public get components(): Readonly<Record<number, unknown>> {
    method tags (line 279) | public get tags(): ReadonlySet<Tag> {
    method add (line 318) | public add<T extends K, K extends unknown>(componentOrTag: NonNullable...
    method append (line 359) | public append<T extends K, K extends ILinkedComponent>(component: NonN...
    method withdraw (line 388) | public withdraw<T>(componentClass: Class<T>): T | undefined {
    method pick (line 420) | public pick<T>(componentOrResolveClass: NonNullable<T> | Class<T>, res...
    method addComponent (line 458) | public addComponent<T extends K, K extends unknown>(component: NonNull...
    method appendComponent (line 507) | public appendComponent<T extends K, K extends ILinkedComponent>(compon...
    method addTag (line 538) | public addTag(tag: Tag): Entity {
    method has (line 560) | public has<T>(componentClassOrTag: Class<T> | Tag, id?: string): boole...
    method contains (line 585) | public contains<T extends K, K>(component: NonNullable<T>, resolveClas...
    method hasComponent (line 607) | public hasComponent<T>(component: Class<T>, id?: string): boolean {
    method hasTag (line 624) | public hasTag(tag: Tag): boolean {
    method hasAny (line 641) | public hasAny(...componentClassOrTag: Array<Class<unknown> | Tag>): bo...
    method hasAll (line 658) | public hasAll(...componentClassOrTag: Array<Class<unknown> | Tag>): bo...
    method get (line 669) | public get<T>(componentClass: Class<T>, id?: string): T | undefined {
    method getComponents (line 690) | public getComponents(): unknown[] {
    method getTags (line 697) | public getTags(): Tag[] {
    method remove (line 720) | public remove<T>(componentClassOrTag: Class<T> | Tag): T | undefined {
    method removeComponent (line 743) | public removeComponent<T>(componentClassOrTag: Class<T>): T | undefined {
    method removeTag (line 771) | public removeTag(tag: Tag): void {
    method clear (line 781) | public clear(): void {
    method copyFrom (line 796) | public copyFrom(entity: Entity): this {
    method iterate (line 829) | public iterate<T>(componentClass: Class<T>, action: (component: T) => ...
    method getAll (line 846) | public* getAll<T>(componentClass: Class<T>): Generator<T, void, T | un...
    method find (line 861) | public find<T>(componentClass: Class<T>, predicate: (component: T) => ...
    method lengthOf (line 881) | public lengthOf<T>(componentClass: Class<T>): number {
    method invalidate (line 894) | public invalidate(): void {
    method takeSnapshot (line 904) | public takeSnapshot<T>(result: EntitySnapshot, changedComponentOrTag?:...
    method getLinkedComponentList (line 937) | public getLinkedComponentList(componentClassOrId: number | Class<any>,...
    method withdrawComponent (line 948) | private withdrawComponent<T extends K, K extends ILinkedComponent>(com...
    method dispatchOnComponentAdded (line 966) | private dispatchOnComponentAdded<T>(component: NonNullable<T>): void {
    method dispatchOnComponentRemoved (line 972) | private dispatchOnComponentRemoved<T>(value: NonNullable<T>): void {
  class EntitySnapshot (line 989) | class EntitySnapshot {
    method current (line 997) | public get current(): Entity {
    method current (line 1004) | public set current(value: Entity) {
    method previous (line 1011) | public get previous(): ReadonlyEntity {
  type ComponentUpdateHandler (line 1021) | type ComponentUpdateHandler = <T>(entity: Entity, component: NonNullable...

FILE: src/ecs/IterativeSystem.ts
  method constructor (line 42) | protected constructor(query: Query | QueryBuilder | QueryPredicate) {
  method update (line 46) | public update(dt: number) {
  method onAddedToEngine (line 50) | public onAddedToEngine() {
  method onRemovedFromEngine (line 55) | public onRemovedFromEngine() {
  method updateEntities (line 60) | protected updateEntities(dt: number) {

FILE: src/ecs/LinkedComponent.ts
  type ILinkedComponent (line 6) | interface ILinkedComponent {
  class LinkedComponent (line 15) | class LinkedComponent implements ILinkedComponent {
    method constructor (line 18) | public constructor(public id?: string) {
  function isLinkedComponent (line 25) | function isLinkedComponent(component: any): component is ILinkedComponent {

FILE: src/ecs/LinkedComponentList.ts
  class LinkedComponentList (line 3) | class LinkedComponentList<T extends ILinkedComponent> {
    method head (line 6) | public get head(): T | undefined {
    method isEmpty (line 10) | public get isEmpty(): boolean {
    method add (line 14) | public add(linkedComponent: T): void {
    method remove (line 31) | public remove(linkedComponent: T): boolean {
    method nodes (line 44) | public* nodes() {
    method iterate (line 52) | public iterate(action: (value: T) => void): void {
    method clear (line 58) | public clear(): void {
    method find (line 62) | private find(linkedComponent: T): [prev: T | undefined, current: T | u...

FILE: src/ecs/Query.ts
  type QueryPredicate (line 12) | type QueryPredicate = (entity: Entity) => boolean;
  class Query (line 18) | class Query {
    method constructor (line 36) | public constructor(predicate: QueryPredicate) {
    method entities (line 43) | public get entities(): ReadonlyArray<Entity> {
    method first (line 51) | public get first(): Entity | undefined {
    method last (line 60) | public get last(): Entity | undefined {
    method length (line 69) | public get length(): number {
    method countBy (line 78) | public countBy(predicate: QueryPredicate): number {
    method find (line 92) | public find(predicate: QueryPredicate): Entity | undefined {
    method filter (line 102) | public filter(predicate: QueryPredicate): Entity[] {
    method has (line 111) | public has(entity: Entity): boolean {
    method matchEntities (line 121) | public matchEntities(entities: ReadonlyArray<Entity>) {
    method isEmpty (line 128) | public get isEmpty(): boolean {
    method clear (line 135) | public clear(): void {
    method validateEntity (line 142) | public validateEntity(entity: Entity): void {
  function hasAll (line 229) | function hasAll(entity: Entity, components: Set<number>, tags: Set<Tag>)...
  class QueryBuilder (line 256) | class QueryBuilder {
    method contains (line 264) | public contains(...componentsOrTags: Array<any>): QueryBuilder {
    method build (line 283) | public build(): Query {
    method getComponents (line 290) | public getComponents(): ReadonlySet<number> {
    method getTags (line 297) | public getTags(): ReadonlySet<Tag> {
  function isQueryPredicate (line 305) | function isQueryPredicate(item: unknown): item is QueryPredicate {
  function isQueryBuilder (line 312) | function isQueryBuilder(item: unknown): item is QueryBuilder {

FILE: src/ecs/ReactionSystem.ts
  method constructor (line 34) | protected constructor(query: Query | QueryBuilder | QueryPredicate) {
  method entities (line 45) | protected get entities(): ReadonlyArray<Entity> {
  method onAddedToEngine (line 49) | public onAddedToEngine() {
  method onRemovedFromEngine (line 56) | public onRemovedFromEngine() {
  method prepare (line 63) | protected prepare() {}

FILE: src/ecs/Subscription.ts
  class Subscription (line 6) | class Subscription<T> {
    method constructor (line 7) | public constructor(
    method equals (line 12) | public equals(messageType: Class<T> | T, handler?: (message: T) => voi...

FILE: src/ecs/System.ts
  method engine (line 18) | public get engine(): Engine {
  method isRemovalRequested (line 28) | public get isRemovalRequested(): boolean {
  method sharedConfig (line 36) | protected get sharedConfig(): Entity {
  method priority (line 44) | public get priority(): number {
  method update (line 52) | public update(dt: number) {}
  method onAddedToEngine (line 57) | public onAddedToEngine() {}
  method onRemovedFromEngine (line 62) | public onRemovedFromEngine() {}
  method dispatch (line 100) | public dispatch<T>(message: T): void {
  method setEngine (line 110) | public setEngine(engine: Engine | undefined): void {
  method setPriority (line 117) | public setPriority(priority: number): void {
  method requestRemoval (line 121) | protected requestRemoval(): void {

FILE: src/ecs/Tag.ts
  type Tag (line 5) | type Tag = number | string;
  function isTag (line 12) | function isTag(item: unknown): item is Tag {

FILE: src/utils/Class.ts
  type Class (line 1) | type Class<T> = {

FILE: src/utils/Signal.ts
  class Signal (line 4) | class Signal<Handler extends (...args: any[]) => any> {
    method hasHandlers (line 11) | public get hasHandlers(): boolean {
    method handlersAmount (line 19) | public get handlersAmount(): number {
    method connect (line 28) | public connect(handler: Handler, priority: number = 0): void {
    method disconnect (line 48) | public disconnect(handler: Handler): void {
    method disconnectAll (line 59) | public disconnectAll(): void {
    method emit (line 67) | public emit(...args: Parameters<Handler>): void {
  class SignalHandler (line 74) | class SignalHandler<Handler extends (...args: any[]) => any> {
    method constructor (line 75) | public constructor(public readonly handler: Handler, public priority: ...
    method equals (line 77) | public equals(handler: Handler): boolean {
    method handle (line 81) | public handle(...args: any[]) {

FILE: tests/unit/engine.spec.ts
  class Component (line 3) | class Component {}
  class Message (line 5) | class Message {}
  method constructor (line 12) | protected constructor(
  method update (line 20) | public update(dt: number) {
  method updateEntity (line 27) | protected updateEntity(entity: Entity, dt: number): void {
  class TestSystem1 (line 31) | class TestSystem1 extends TestSystem {
    method constructor (line 32) | public constructor(arr?: number[]) {
  class TestSystem2 (line 37) | class TestSystem2 extends TestSystem {
    method constructor (line 38) | public constructor(arr?: number[]) {
  class TestSystem3 (line 43) | class TestSystem3 extends TestSystem {
    method constructor (line 44) | public constructor(arr?: number[]) {
  class TestSystem (line 142) | class TestSystem extends IterativeSystem {
    method constructor (line 143) | public constructor() {
    method updateEntity (line 147) | protected updateEntity(entity: Entity, dt: number): void {
  class GameOverSystem (line 226) | class GameOverSystem extends ReactionSystem {
    method constructor (line 229) | public constructor() {
    method update (line 233) | public update(dt: number) {
    method prepare (line 242) | protected prepare() {
    method constructor (line 270) | public constructor() {
    method update (line 274) | public update(dt: number) {
    method prepare (line 283) | protected prepare() {
  class GameOver (line 263) | class GameOver {}
  class OtherMessage (line 265) | class OtherMessage {}
  class GameOverSystem (line 267) | class GameOverSystem extends ReactionSystem {
    method constructor (line 229) | public constructor() {
    method update (line 233) | public update(dt: number) {
    method prepare (line 242) | protected prepare() {
    method constructor (line 270) | public constructor() {
    method update (line 274) | public update(dt: number) {
    method prepare (line 283) | protected prepare() {

FILE: tests/unit/entity.spec.ts
  class Position (line 3) | class Position {
    method constructor (line 7) | constructor(x: number = 0, y: number = 0) {
  class Damage (line 13) | class Damage extends LinkedComponent {
    method constructor (line 14) | public constructor(
  class AnotherDamage (line 22) | class AnotherDamage extends LinkedComponent {
    method constructor (line 23) | public constructor() {
  class DamageChild (line 28) | class DamageChild extends Damage {
    method constructor (line 29) | public constructor() {
  class Test1 (line 43) | class Test1 {
  class Test2 (line 46) | class Test2 {
  class Test1 (line 57) | class Test1 {
  class Test2 (line 60) | class Test2 {
  class Ancestor (line 146) | class Ancestor {}
  class Descendant (line 148) | class Descendant extends Ancestor {}
  class Descendant2 (line 150) | class Descendant2 extends Ancestor {}
  class Ancestor (line 170) | class Ancestor {}
  class Descendant (line 172) | class Descendant extends Ancestor {}
  class Other (line 174) | class Other {}
  class Ancestor (line 188) | class Ancestor {}
  class Descendant (line 190) | class Descendant extends Ancestor {}
  class Other (line 204) | class Other {}
  class Other (line 212) | class Other {}
  class A (line 214) | class A {}
  class Other (line 233) | class Other {}
  class Component (line 609) | class Component {}
  class Component (line 617) | class Component {}
  class NotAComponent (line 619) | class NotAComponent {}
  class ComponentA (line 631) | class ComponentA {}
  class ComponentB (line 633) | class ComponentB {}
  class ComponentA (line 658) | class ComponentA {}

FILE: tests/unit/linked.list.spec.ts
  class Component (line 4) | class Component extends LinkedComponent {}

FILE: tests/unit/query.spec.ts
  class Position (line 3) | class Position {
    method constructor (line 7) | constructor(x: number = 0, y: number = 0) {
  class View (line 13) | class View {}
  class Move (line 15) | class Move {}
  class Stay (line 17) | class Stay {}
  class Damage (line 19) | class Damage extends LinkedComponent {}
  function getQuery (line 104) | function getQuery() {
  class LogComponent (line 449) | class LogComponent extends LinkedComponent {
    method constructor (line 450) | public constructor(public readonly log: string) {

FILE: tests/unit/shared.config.spec.ts
  method onAddedToEngine (line 9) | public onAddedToEngine() {
  method constructor (line 23) | public constructor() {
  class Component (line 33) | class Component {}
  method onAddedToEngine (line 41) | public onAddedToEngine() {

FILE: tests/unit/system.spec.ts
  class Position (line 3) | class Position {
    method constructor (line 7) | public constructor(x: number = 0, y: number = 0) {
  class MovementSystem (line 13) | class MovementSystem extends IterativeSystem {
    method constructor (line 14) | public constructor() {
    method updateEntity (line 18) | protected updateEntity(entity: Entity, dt: number): void {
    method constructor (line 79) | public constructor() {
    method updateEntity (line 83) | protected updateEntity(entity: Entity, dt: number): void {
  class TestSystem (line 49) | class TestSystem extends IterativeSystem {
    method constructor (line 50) | public constructor() {
    method prepare (line 54) | protected prepare() {
    method updateEntity (line 58) | protected updateEntity(entity: Entity, dt: number): void {
    method update (line 166) | public update(dt: number) {
    method update (line 180) | public update(dt: number) {
    method constructor (line 196) | public constructor() {
    method updateEntity (line 200) | protected updateEntity(entity: Entity, dt: number) {
  class MovementSystem (line 78) | class MovementSystem extends IterativeSystem {
    method constructor (line 14) | public constructor() {
    method updateEntity (line 18) | protected updateEntity(entity: Entity, dt: number): void {
    method constructor (line 79) | public constructor() {
    method updateEntity (line 83) | protected updateEntity(entity: Entity, dt: number): void {
  class Health (line 111) | class Health {
    method constructor (line 112) | public constructor(public value: number) {
  class HealthTickSystem (line 116) | class HealthTickSystem extends IterativeSystem {
    method constructor (line 117) | public constructor() {
    method updateEntity (line 121) | protected updateEntity(entity: Entity, dt: number): void {
  class Message (line 162) | class Message {
  class TestSystem (line 165) | class TestSystem extends System {
    method constructor (line 50) | public constructor() {
    method prepare (line 54) | protected prepare() {
    method updateEntity (line 58) | protected updateEntity(entity: Entity, dt: number): void {
    method update (line 166) | public update(dt: number) {
    method update (line 180) | public update(dt: number) {
    method constructor (line 196) | public constructor() {
    method updateEntity (line 200) | protected updateEntity(entity: Entity, dt: number) {
  class Message (line 176) | class Message {
  class TestSystem (line 179) | class TestSystem extends System {
    method constructor (line 50) | public constructor() {
    method prepare (line 54) | protected prepare() {
    method updateEntity (line 58) | protected updateEntity(entity: Entity, dt: number): void {
    method update (line 166) | public update(dt: number) {
    method update (line 180) | public update(dt: number) {
    method constructor (line 196) | public constructor() {
    method updateEntity (line 200) | protected updateEntity(entity: Entity, dt: number) {
  class Component (line 190) | class Component {
  class TestSystem (line 195) | class TestSystem extends IterativeSystem {
    method constructor (line 50) | public constructor() {
    method prepare (line 54) | protected prepare() {
    method updateEntity (line 58) | protected updateEntity(entity: Entity, dt: number): void {
    method update (line 166) | public update(dt: number) {
    method update (line 180) | public update(dt: number) {
    method constructor (line 196) | public constructor() {
    method updateEntity (line 200) | protected updateEntity(entity: Entity, dt: number) {
  class Component (line 220) | class Component {
  method constructor (line 227) | public constructor() {
  method updateEntity (line 231) | protected updateEntity(entity: Entity, dt: number) {
  method update (line 253) | public update(dt: number) {
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (186K chars).
[
  {
    "path": ".github/workflows/build.yml",
    "chars": 614,
    "preview": "name: build\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ $default-branch ]\n  pull_request:\n    branches: [ $default"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1165,
    "preview": "name: Publish to NPM\n\non:\n  push:\n    branches:\n      - main\n      - master\n    tags:\n      - v*\njobs:\n  build:\n    runs"
  },
  {
    "path": ".gitignore",
    "chars": 938,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directo"
  },
  {
    "path": ".travis.yml",
    "chars": 504,
    "preview": "language: node_js\ndist: focal\nnode_js:\n  - \"19\"\nbefore_install:\n  - curl -o- -L https://yarnpkg.com/install.sh | bash -s"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 6840,
    "preview": "# 4.3.0\n\nFeatures:\n\n- Introduced possibility to safely remove entities from the engine.\n  Now `Engine.removeEntity` take"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2019-2020 Ilya Malanin\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 28755,
    "preview": "# Tick-Knock\n\n> Small and powerful, type-safe and easy-to-use Entity-Component-System (ECS)\n> library written in TypeScr"
  },
  {
    "path": "jest-ci.json",
    "chars": 336,
    "preview": "{\n  \"transform\": {\n    \"^.+\\\\.tsx?$\": \"ts-jest\"\n  },\n  \"collectCoverage\": true,\n  \"moduleFileExtensions\": [\n    \"ts\",\n  "
  },
  {
    "path": "package.json",
    "chars": 1365,
    "preview": "{\n  \"name\": \"tick-knock\",\n  \"version\": \"4.3.0\",\n  \"description\": \"TypeScript Entity-Component-System library\",\n  \"author"
  },
  {
    "path": "src/ecs/ComponentId.ts",
    "chars": 1262,
    "preview": "import {Class} from '../utils/Class';\n\n/**\n * Gets an id for a component class.\n *\n * @param component Component class\n "
  },
  {
    "path": "src/ecs/Engine.ts",
    "chars": 11601,
    "preview": "import {Entity} from './Entity';\nimport {System} from './System';\nimport {Class} from '../utils/Class';\nimport {Query} f"
  },
  {
    "path": "src/ecs/Entity.ts",
    "chars": 35462,
    "preview": "import {getComponentClass, getComponentId} from './ComponentId';\nimport {Class} from '../utils/Class';\nimport {Signal} f"
  },
  {
    "path": "src/ecs/IterativeSystem.ts",
    "chars": 2001,
    "preview": "import {Query, QueryBuilder, QueryPredicate} from './Query';\nimport {Entity} from './Entity';\nimport {ReactionSystem} fr"
  },
  {
    "path": "src/ecs/LinkedComponent.ts",
    "chars": 567,
    "preview": "/**\n * Linked list interface for linked components\n * @see {@link Entity.append}\n */\n\nexport interface ILinkedComponent "
  },
  {
    "path": "src/ecs/LinkedComponentList.ts",
    "chars": 1856,
    "preview": "import {ILinkedComponent} from './LinkedComponent';\n\nexport class LinkedComponentList<T extends ILinkedComponent> {\n  pr"
  },
  {
    "path": "src/ecs/Query.ts",
    "chars": 8794,
    "preview": "import {getComponentId} from './ComponentId';\nimport {Entity, EntitySnapshot} from './Entity';\nimport {isTag, Tag} from "
  },
  {
    "path": "src/ecs/ReactionSystem.ts",
    "chars": 2791,
    "preview": "import {isQueryBuilder, isQueryPredicate, Query, QueryBuilder, QueryPredicate} from './Query';\nimport {Engine} from './E"
  },
  {
    "path": "src/ecs/Subscription.ts",
    "chars": 402,
    "preview": "import {Class} from '../utils/Class';\n\n/**\n * @internal\n */\nexport class Subscription<T> {\n  public constructor(\n    pub"
  },
  {
    "path": "src/ecs/System.ts",
    "chars": 3499,
    "preview": "import {Engine} from './Engine';\nimport {Entity} from './Entity';\n\n/**\n * Systems are logic bricks in your application.\n"
  },
  {
    "path": "src/ecs/Tag.ts",
    "chars": 485,
    "preview": "/**\n * A tag is a simple marker that can be considered as a component without data.\n * It can be used instead of creatin"
  },
  {
    "path": "src/index.ts",
    "chars": 359,
    "preview": "export * from './utils/Class';\nexport * from './utils/Signal';\nexport * from './ecs/ComponentId';\nexport * from './ecs/T"
  },
  {
    "path": "src/utils/Class.ts",
    "chars": 55,
    "preview": "export type Class<T> = {\n  new(...args: any[]): T;\n};\n\n"
  },
  {
    "path": "src/utils/Signal.ts",
    "chars": 2356,
    "preview": "/**\n * Lightweight implementation of Signal\n */\nexport class Signal<Handler extends (...args: any[]) => any> {\n  private"
  },
  {
    "path": "tests/unit/engine.spec.ts",
    "chars": 10615,
    "preview": "import {Engine, Entity, IterativeSystem, LinkedComponent, Query, QueryBuilder, QueryPredicate, System, ReactionSystem } "
  },
  {
    "path": "tests/unit/entity.spec.ts",
    "chars": 20768,
    "preview": "import {Entity, EntitySnapshot, getComponentId, LinkedComponent} from '../../src';\n\nclass Position {\n  public x: number "
  },
  {
    "path": "tests/unit/linked.list.spec.ts",
    "chars": 2715,
    "preview": "import {LinkedComponentList} from '../../src/ecs/LinkedComponentList';\nimport {LinkedComponent} from '../../src';\n\nclass"
  },
  {
    "path": "tests/unit/query.spec.ts",
    "chars": 19793,
    "preview": "import {Engine, Entity, LinkedComponent, Query, QueryBuilder} from '../../src';\n\nclass Position {\n  public x: number = 0"
  },
  {
    "path": "tests/unit/shared.config.spec.ts",
    "chars": 1797,
    "preview": "import {Engine, Query, System} from '../../src';\n\ndescribe('Shared config', () => {\n  it('Shared config is accessible wh"
  },
  {
    "path": "tests/unit/signal.spec.ts",
    "chars": 1543,
    "preview": "import {Signal} from '../../src/utils/Signal';\n\ndescribe('Signals', function () {\n  it('Connecting increases amount of h"
  },
  {
    "path": "tests/unit/system.spec.ts",
    "chars": 7326,
    "preview": "import {Engine, Entity, EntitySnapshot, IterativeSystem, Query, QueryBuilder, System} from '../../src';\n\nclass Position "
  },
  {
    "path": "tsconfig.json",
    "chars": 753,
    "preview": "{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"node\",\n    \"module\": \"commonjs\",\n    \"target\": \"es2016\",\n    \"lib\": [\n"
  }
]

About this extraction

This page contains the full source code of the mayakwd/tick-knock GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (174.2 KB), approximately 42.7k tokens, and a symbol index with 249 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!