{"id":34665163,"url":"https://github.com/mayakwd/tick-knock","last_synced_at":"2025-12-24T19:01:33.991Z","repository":{"id":34681476,"uuid":"181303337","full_name":"mayakwd/tick-knock","owner":"mayakwd","description":"Small and powerful, type-safe and easy-to-use Entity-Component-System (ECS) library written in TypeScript","archived":false,"fork":false,"pushed_at":"2024-11-18T22:42:19.000Z","size":1471,"stargazers_count":148,"open_issues_count":5,"forks_count":13,"subscribers_count":9,"default_branch":"develop","last_synced_at":"2025-08-27T05:31:39.321Z","etag":null,"topics":["ecs","entity-component-system","game-development","gamedev","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mayakwd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-04-14T12:09:01.000Z","updated_at":"2025-08-10T21:22:55.000Z","dependencies_parsed_at":"2024-01-23T15:29:11.908Z","dependency_job_id":"308147e3-463a-4244-ab85-d70e3a3042fa","html_url":"https://github.com/mayakwd/tick-knock","commit_stats":{"total_commits":209,"total_committers":6,"mean_commits":"34.833333333333336","dds":"0.13875598086124397","last_synced_commit":"10fda308b6bbcba7b4b04858107b997465bdd905"},"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"purl":"pkg:github/mayakwd/tick-knock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mayakwd%2Ftick-knock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mayakwd%2Ftick-knock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mayakwd%2Ftick-knock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mayakwd%2Ftick-knock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mayakwd","download_url":"https://codeload.github.com/mayakwd/tick-knock/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mayakwd%2Ftick-knock/sbom","scorecard":{"id":630248,"data":{"date":"2025-08-11","repo":{"name":"github.com/mayakwd/tick-knock","commit":"48e16b69a64394eadb60a8f7cc58dda55cee9f57"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.9,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Code-Review","score":1,"reason":"Found 2/20 approved changesets -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/mayakwd/tick-knock/build.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/mayakwd/tick-knock/build.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/mayakwd/tick-knock/publish.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/mayakwd/tick-knock/publish.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/mayakwd/tick-knock/publish.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/mayakwd/tick-knock/publish.yml/develop?enable=pin","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/build.yml:1","Warn: no topLevel permission defined: .github/workflows/publish.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'develop'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":0,"reason":"Project has not signed or included provenance with any releases.","details":["Warn: release artifact 4.1.0 not signed: https://api.github.com/repos/mayakwd/tick-knock/releases/68052831","Warn: release artifact 4.0.5 not signed: https://api.github.com/repos/mayakwd/tick-knock/releases/68029300","Warn: release artifact 4.0.4 not signed: https://api.github.com/repos/mayakwd/tick-knock/releases/68021420","Warn: release artifact 4.0.3 not signed: https://api.github.com/repos/mayakwd/tick-knock/releases/68009577","Warn: release artifact 4.0.2 not signed: https://api.github.com/repos/mayakwd/tick-knock/releases/45173269","Warn: release artifact 4.1.0 does not have provenance: https://api.github.com/repos/mayakwd/tick-knock/releases/68052831","Warn: release artifact 4.0.5 does not have provenance: https://api.github.com/repos/mayakwd/tick-knock/releases/68029300","Warn: release artifact 4.0.4 does not have provenance: https://api.github.com/repos/mayakwd/tick-knock/releases/68021420","Warn: release artifact 4.0.3 does not have provenance: https://api.github.com/repos/mayakwd/tick-knock/releases/68009577","Warn: release artifact 4.0.2 does not have provenance: https://api.github.com/repos/mayakwd/tick-knock/releases/45173269"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 13 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":6,"reason":"4 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T07:42:09.062Z","repository_id":34681476,"created_at":"2025-08-21T07:42:09.062Z","updated_at":"2025-08-21T07:42:09.062Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28006375,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-24T02:00:07.193Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecs","entity-component-system","game-development","gamedev","typescript"],"created_at":"2025-12-24T19:00:33.021Z","updated_at":"2025-12-24T19:01:33.978Z","avatar_url":"https://github.com/mayakwd.png","language":"TypeScript","funding_links":["https://www.buymeacoffee.com/rdolivaw"],"categories":[],"sub_categories":[],"readme":"# Tick-Knock\n\n\u003e Small and powerful, type-safe and easy-to-use Entity-Component-System (ECS)\n\u003e library written in TypeScript\n\n[![Build Status](https://github.com/mayakwd/tick-knock/actions/workflows/build.yml/badge.svg)](https://travis-ci.org/mayakwd/tick-knock)\n[![Codecov Coverage](https://img.shields.io/codecov/c/github/mayakwd/tick-knock/develop.svg?style=flat-square)](https://codecov.io/gh/mayakwd/tick-knock/)\n\n😊 [Buy me a coffee](https://www.buymeacoffee.com/rdolivaw)\n\n# Table of contents\n\n- [Installing]\n- [How it works?]\n- [Inside the Tick-Knock]\n    - [Engine]\n        - [Subscription]\n    - [Component]\n    - [Linked Component]\n        - [Tag]\n        - [Entity]\n        - [System]\n        - [Query]\n            - [QueryBuilder]\n            - [Queries and Systems]\n            - [Built-in query-based systems]\n                - [ReactionSystem]\n                - [IterativeSystem]\n        - [Snapshot]\n        - [Shared Config]\n        - [Linked Components How-To]\n- [Restrictions]\n    - [Shared and Local Queries]\n    - [Queries with complex logic and Entity invalidation]\n- [License]\n- [Donation]\n\n# Installing\n\n- Yarn: `yarn add tick-knock`\n- NPM: `npm i --save tick-knock`\n\n# How it works?\n\nTick-Knock was inspired by several ECS libraries, mostly by [Ash ECS](https://www.richardlord.net/ash/).\n\nThe main approach was re-imagined to make it lightweight, easy-to-use, and less boiler-plate based.\n\n# Inside the Tick-Knock\n\nIn this part, you will learn all basics of Tick-Knock step by step.\n\n## Engine\n\nEngine is a \"world\" where entities, systems, and queries interact with each other.\n\nSince the Engine is the initial entry point for development with Tick-Knock, it is from this point that the creation of\nyour world starts. Usually, the Engine exists in just one instance, and it does nothing but orchestrating everything\nadded to it.\n\nTo begin with, you can add the most usual \"inhabitants\" to it.\n\n```typescript\nconst engine = new Engine();\nconst entity = new Entity()\n  .add(new Hero())\n  .add(new health(10))\nengine.addEntity(entity);\n```\n\nOr you can take it out:\n\n```typescript\nengine.removeEntity(entity);\n```\n\nThe second main \"inhabitant\" is System. It is responsible for processing Entities and their components. We will learn\nabout them in detail later.\n\n```typescript\nengine.addSystem(new ViewSystem(), 1);\nengine.addSystem(new PhysicsSystem(), 2);\n```\n\nAs you may have noticed, we pass two parameters: system instance, and the second is update priority. The higher the\npriority number is, the later the system will be processed.\n\nThe third type of resident is Query, which is responsible for mapping entities within the Engine and returns a list of\nalready filtered and ready-to-use entities.\n\n```typescript\nconst heroesQuery = new Query((entity) =\u003e entity.has(Hero));\nengine.addQuery(heroesQuery);\n````\n\nThe main task of the engine is to start the world update process and to report on the ongoing changes to Queries.  \nThese changes can be: additions to and removal of entities from the Engine, and changes in the components of specific\nEntities.\n\nTo perform the update step, we must call the `update` method and pass as a parameter the time elapsed since the previous\nupdate.  \nEvery time we start an update, the systems take turns, in order of priority, executing their own update methods.\n\n```typescript\n// Half a second has passed from the previous step.\nengine.update(0.5); \n```\n\n### Subscription\n\nAn additional - one of the Engine's responsibilities - transferring the messages from systems to the user. This can be\nvery useful when, for example, you want to report that the round in your game is over.\n\n```typescript\nengine.subscribe(GameOver, (message: GameOver) =\u003e {\n  if (game.win) {\n    this.showWinMessage();\n  } else {\n    this.showLoseMessage();\n  }\n});\n```\n\nYou can use not only class type as an argument but any value. For example, it could be a string or number.\n\n```typescript\nconst GAME_OVER = 'gameOver';\nengine.subscribe(GAME_OVER, () =\u003e {\n  this.showGameOver();\n});\n```\n\n\u003e **Details of implementation**\n\u003e\n\u003e When the `dispatch` method is called in the system, then to get the right listeners, the compliance of\n\u003e the `messageType` for each subscription will be checked.\n\u003e - If `typeof subscription.messageType` is a `'function'`, then the matching will be performed using `instanceOf`.\n\u003e - Otherwise, the matching will be done through strict equality `message === subscription.messageType`.\n\n## Component\n\nIt is a data object, its purpose - to represent a single aspect of your entity. For example, position, velocity,\nacceleration.\n\n- ❕ Any class could be considered as the component. There are no restrictions.\n- ❗ For proper understanding, it needs to be noticed that the component should be a data class, without any logic.\n  Otherwise, you'll lose the benefits of the ECS pattern.\n\n**Let's write your first component:**\n\n```typescript\nclass Position {\n  public constructor(\n    public x: number = 0,\n    public y: number = 0\n  ) {}\n}\n```\n\n\u003e Yes, this is a component! 🎉\n\n## Linked component\n\nIt is still a data class, but it is made to solve the problem when you need to have multiple components of the same\ntype.\n\nLet's assume that you have a Damage component in your game. Several enemies attack the Hero simultaneously by adding the\nDamage component to it. What will happen? Only the last Damage component will be added to the Hero Entity because every\nprevious one will be removed.\n\nTo solve this problem - you need to implement ILinkedComponent interface in your Damage component and \"append\" instead\nof \"add\" the Damage component to the entity. That will do the job. After that, in DamageSystem you can find all damage\nsources:\n\n```typescript\nclass Damage extends LinkedComponent {\n  public constructor(\n    public readonly value: number\n  ) {\n    super()\n  }\n}\n\nhero.append(new Damage(100));\nhero.append(new Damage(5));\n\nclass DamageSystem extends IterativeSystem {\n  public constructor() {\n    super((entity) =\u003e entity.hasAll(Damage, Health));\n  }\n\n  public updateEntity(entity: Entity) {\n    const health = entity.get(Health)!;\n    while (entity.has(Damage)) {\n      const damage = entity.withdraw(Damage);\n      health.value -= damage.value;\n    }\n  }\n}\n```\n\n## Tag\n\nIt also can be called a \"label\". It's a simplistic way to help you not \"inflate\" your code with classes without data.\nFor instance, you want to mark your entity as Dead. There are two ways:\n\n- To create a component class: `class Dead {}`\n- Or to create a tag - that can be represented as a `string` or `number`.\n\nUsing tags is much easier and consumes less memory if you do not have additional component data.\n\n**Example:**\n\n```typescript\nconst ENEMY = 'enemy';\nconst HERO = 100500;\n```\n\n\u003e Keep it simple! 😄\n\n## Entity\n\nIt is a general-purpose object, which can be marked with tags and can contain different components.\n\n- So it can be considered as a container that can represent any in-game entity, like an enemy, bomb, configuration, game\n  state, etc.\n- Entity can contain only one component or tag of each type. You can't add two `Position` components to the entity, the\n  second one will replace the first one.\n\n**This is how it works:**\n\n```typescript\nconst entity = new Entity()\n  .add(new Position(100, 100))\n  .add(new Position(200, 200))\n  .add(HERO);\n\nconsole.log(entity.get(Position)); // Position(x = 200, y = 200)\n```\n\n\u003e Looks easy? Yes, it is!\n\n## System\n\nSystems are logic bricks in your application. If you want to manipulate entities, their components, and tags - it is the\nright place.\n\nPlease, keep in mind that the complexity of the system mustn't be too high. When you find that your system is doing too\nmuch in the \"update\" method, you need to split it into several systems.\n\nResponsibility of the system should cover no more than one logical aspect.\n\nThe system always has the following functionality:\n\n- Priority, which can be set before adding a system to the engine.\n- Reference to the `engine` will give you access to the engine itself and its entities. But be aware - you can't access\n  an engine if the system is not connected to it. Otherwise, you'll get an error.\n- Methods `onAddedToEngine` and `onRemovedFromEngine` will be called in the cases described by their naming.\n- With the method `dispatch`, you can easily send a message outside of the system. It will be delivered through the\n  engine [Subscription](#subscription) pipe. There are the same restrictions as for the engine. If the system is not\n  attached to the engine, then an attempt to send a message will throw an error.\n- And last but not least, the heart of your system - method `update`. It will be called whenever `Engine.update` is\n  being invoked. Update method - the right place to put your logic.\n\n**Example:**\nIt's time to write our first and straightforward system. It will iterate through all the entities that are in the\nEngine, check if they have Position and Velocity components.  \nAnd if they do, then move our object.\n\n```typescript\nclass Velocity {\n  public constructor(\n    public x: number = 0,\n    public y: number = 0\n  ) {}\n}\n\nclass PhysicsSystem extends System {\n  public constructor() {\n    super();\n  }\n\n  public update(dt: number): void {\n    const {entities} = this.engine;\n    for (const entity of entities) {\n      if (entity.hasAll(Position, Velocity)) {\n        const position = entity.get(Position)!;\n        const velocity = entity.get(Velocity)!;\n        position.x += velocity.x * dt;\n        position.y += velocity.y * dt;\n      }\n    }\n  }\n}\n```\n\n\u003e There you go!\n\u003e 🎁 In real life, you don't have to iterate through every entity in every system. It's completely uncomfortable and not\n\u003e optimal. In this library, there is a mechanism that can prepare a list of the entities that you need according to the\n\u003e criteria you set - it's called Query.\n\n## Query\n\nSo what the \"Query\" is? It's a matching mechanism that can tell you which entities in the Engine are suitable for your\nneeds.\n\nFor example, you want to write a system that is responsible for displaying sprites on your screen. To do this, you\nalways need a current list of entities, each of which has three components - View, Position, Rotation, and you want to\nexclude those marked with the HIDDEN tag.\n\n**Let's write our first Query.**\n\n```typescript\nconst displayListQuery = new Query((entity: Entity) =\u003e {\n  return entity.hasAll(View, Position, Rotation) \u0026\u0026 !entity.has(HIDDEN);\n});\n```\n\n\u003e That's all!\n\nAdding this Query to the Engine will always contain an up-to-date list of entities that meet the described requirements.\nBesides, you can always find out when a new entity has appeared in the Query, or an old entity has left it.\n\n```typescript\ndisplayListQuery.onEntityAdded.connect(({current}: EntitySnapshot) =\u003e {\n  console.log(\"We've got a rookie here!\");\n  container.addChild(current.get(View)!.view);\n});\ndisplayListQuery.onEntityRemoved.connect(({previous}: EntitySnapshot) =\u003e {\n  container.removeChild(previous.get(View)!.view);\n  console.log(\"Good bye, friend!\");\n});\n```\n\n### QueryBuilder\n\nQuery builder is super simple. It has not much power, but you can use it for creating queries that must contain specific\nComponents.\n\n```typescript\nconst query: Query = new QueryBuilder()\n  .contains(ComponentA, ComponentB)\n  .contains(TAG)\n  .build();\n```\n\n### Queries and Systems\n\nNow let's see how we can use Query on systems?\n\nLet's write `ViewSystem`, which will be responsible for displaying our Entity on the screen.  \nWhen entities get to the list, the system will add them to the screen, and when they leave the list, the system will\nremove them from the screen.\n\n**Example:**\n\n```typescript\nconst query = new Query((entity: Entity) =\u003e {\n  return entity.hasAll(View, Position, Rotation) \u0026\u0026 !entity.has(HIDDEN);\n});\n\nclass ViewSystem extends System {\n  public constructor(\n    private readonly container: Container\n  ) { super(); }\n\n  public onAddedToEngine(): void {\n    // To make query work - we need to add it to the engine\n    this.engine.addQuery(query);\n    // And we need to add to the display list all entities that already \n    // exists in the Engine`s world and matches our Query \n    this.prepare();\n    // We want to know if new entities were added or removed\n    query.onEntityAdded.connect(this.onEntityAdded);\n    query.onEntityRemoved.connect(this.onEntityRemoved);\n  }\n\n  public onRemovedFromEngine(): void {\n    // There is no reason to update query after system was removed \n    // from the engine\n    this.engine.removeQuery(query);\n    // No reason for further listening of the updates\n    query.onEntityAdded.disconnect(this.onEntityAdded);\n    query.onEntityRemoved.disconnect(this.onEntityRemoved);\n  }\n\n  // We only want to update positions of the views on the screen,\n  // so there is no need for \"dt\" parameter, it can be omitted\n  public update(): void {\n    const entities = this.query.entities;\n    for (const entity of entities) {\n      this.updatePosition(entity);\n    }\n  }\n\n  private prepare(): void {\n    for (const entity of this.query.entities) {\n      this.onEntityAdded(entity);\n    }\n  }\n\n  private updatePosition(entity: Entity): void {\n    const {view} = entity.get(View)!;\n    const {x, y} = entity.get(Position)!;\n    const {rotation} = entity.get(Rotation)!;\n    view.position.set(x, y);\n    view.rotaion.set(rotation);\n  }\n\n  private onEntityAdded = ({current}: EntitySnapshot) =\u003e {\n    // Let's add new view to the screen\n    this.container.addChild(current.get(View)!.view);\n    // Don't forget to update it's position on the screen\n    this.updatePosition(current);\n  };\n\n  private onEntityRemoved = ({previous}: EntitySnapshot) =\u003e {\n    // Let's remove the view from the screen, because Entity no longer \n    // meets the requirements (might be it lost the View component \n    // or it was hidden)\n    this.container.removeChild(previous.get(View)!.view);\n  };\n}\n```\n\n\u003e 😎 I'm sure you saw the reference to `EntitySnapshot` and wondering, \"what the heck is that?\". Please, be\n\u003e patient, [I'll tell you about](#Snapshot) it a bit later.\n\u003e I think it looks good and clear for understanding!\n\n- 🤔 You can say: \"we need to write too much boilerplate-code\".\n- And of course, Tick-Knock will help you to reduce boilerplate-code!\n\n### Built-in query-based systems\n\nIn favor of reducing the time to write the boilerplate code - Tick-Knock provides two built-in systems. Each of them\nalready knows how to work with Query, process the information coming from it, and allow access to this Query's entities.\n\nAll of the following built-in systems have the following features:\n\nYou can initialize those systems via three different items, which will be converted to Query eventually:\n\n- Query itself\n- Query predicate - Query will be automatically created on top of it. This feature was introduced to reduce the size of\n  the boilerplate code.\n- QueryBuilder - it is also a valid option.\n- They have a getter `entities`, which returns the current entities list of the Query.\n- They have a built-in property entityAdded and entityRemoved, you need to define them if you want to track Query\n  changes.\n\n#### ReactionSystem\n\nReactionSystem can be considered as the system that has the ability to react to changes in Query. It is a basic built-in\nsystem. Exactly it will be used in most cases when developing your application.\n\nLet's try to rewrite our ViewSystem, taking ReactionSystem as a basis, and take advantage of all the conveniences it\nprovides.\n\n**Example:**\n\n```typescript\nclass ViewSystem extends ReactionSystem {\n  public constructor(private readonly container: Container) {\n    super((entity: Entity) =\u003e {\n      return entity.hasAll(View, Position, Rotation) \u0026\u0026 !entity.has(HIDDEN);\n    });\n  }\n\n  public update(): void {\n    for (const entity of this.entities) {\n      this.updatePosition(entity);\n    }\n  }\n\n  protected prepare(): void {\n    for (const entity of this.entities) {\n      this.entityAdded(entity);\n    }\n  }\n\n  private updatePosition(entity: Entity): void {\n    const {view} = entity.get(View)!;\n    const {x, y} = entity.get(Position)!;\n    const {rotation} = entity.get(Rotation)!;\n    view.position.set(x, y);\n    view.rotaion.set(rotation);\n  }\n\n  protected entityAdded = ({current}: EntitySnapshot) =\u003e {\n    this.updatePosition(current);\n    this.container.addChild(current.get(View)!.view);\n  };\n\n  protected entityRemoved = ({previous}: EntitySnapshot) =\u003e {\n    this.container.removeChild(previous.get(View)!.view);\n  };\n}\n```\n\n\u003e Now it's pretty simpler! 🎉\n\n#### IterativeSystem\n\nThis system has the same advantages as the ReactionSystem because it is inherited from the last one. 😅 All it brings is\na built-in iteration cycle for our Query inside the update method.\n\n**So, let's upgrade our `ViewSystem` a bit.**\n\n```typescript\nclass ViewSystem extends IterativeSystem {\n  // almost everything remains the same, so I'll skip most of the code.\n  // The only difference regarding example with ReactionSystem - that we \n  // don't need to override `update` method. \n  // Instead of it we need to override updateEntity method.\n  // Also, we can safely omit the dt parameter because we do not use it.\n  protected updateEntity(entity: Entity, dt: number) {\n    this.updatePosition(entity);\n  }\n}\n```\n\n#### Remove the system as it's done\n\nIt's possible to request removal of the system when you don't need it anymore. For example, the system is only\nneeded to render the playing field, and trying to run it at every update cycle is wasteful.\n\nFortunately, you can request deletion right from the system:\n\n```typescript\nclass RenderBoardSystem extends System {\n  public update(dt: number): void {\n    // Your render board code\n    this.requestRemoval();\n  }\n}\n```\n\nThat's it. Your system will be removed right after update cycle.\n\n## Snapshot\n\nAs you may have noticed, when we are tracking changes in Query, we get in `entityAdded` and `entityRemoved` not `Entity`\nbut `EntitySnapshot`.\n**So what is a snapshot?**\nIt is a container that displays the difference between the current state of Entity and its previous state. The `entity`\nproperty always reflects the current state. Still, methods ` get` and `has` methods of the snapshot return the data from\nthe previous state of the Entity before it was changed. So you can understand which components have been added and which\nhave been removed.\n\n\u003e ❗ It is important to note that changes in the same entity components' data will not be reflected in the snapshot, even\n\u003e if a manual invalidation of the entity has been triggered.\n\nSnapshots are very handy when you need to get a component or tag in Entity, but now it is missing. Let's take a closer\nlook at it with our `ViewSystem` example.\n**Example:**\n\n```typescript\nclass ViewSystem extends IterativeSystem {\n  // ...\n  protected entityAdded = ({current}: EntitySnapshot) =\u003e {\n    // When entity added to the Query that means that it has `View` \n    // component - one hundred percent! So we just need its current \n    // state. \n    this.container.addChild(current.get(View)!.view);\n    this.updatePosition(current);\n  };\n\n  protected entityRemoved = ({previous}: EntitySnapshot) =\u003e {\n    // But when entity removed - we can't be sure that current state \n    // of the entity has `View` component. So we need to get it from\n    // the previous state. Previous state has it one hundred percent.\n    this.container.removeChild(previous.get(View)!.view);\n  };\n  // ...\n}\n```\n\n## Shared Config\n\nIn real life, there is often a need to have a single Entity that acts as a configuration for the whole world.\n\nFor example, you have a set of complex systems that involve both game logic and visualization, and animations. But for\nfunctional test purposes - you don't care about the visuals and animations. You face the situation of passing a specific\nflag in each system during initialization, which will be responsible for disabling animation and visualization.\n\nNow imagine that you have several configuration parameters, and each of them you need to pass to all systems of your\nworld.\n\nTo simplify handling such situations - you can use `Engine.sharedConfig`. Shared Config is an `Entity` available in all\nsystems after adding them to `Engine`.\n\n**Example:**\n\n```typescript\nconst NO_VISUALS = 'no-visuals';\n\nclass ViewSystem extends IterativeSystem {\n  protected updateEntity(entity: Entity): void {\n    if (this.sharedConfig.has(NO_VISUALS)) {\n      return;\n    }\n\n    // Otherwise - update visuals\n  }\n}\n\nconst engine = new Engine();\nengine.sharedConfig.add(NO_VISUALS);\nengine.addSystem(new ViewSystem());\n```\n\n\u003e ☝ Shared Config is the single instance connected to `Engine` since its initialization and can't be removed from it. It\n\u003e affects queries like any regular `Entity`.\n\n## How to work with linked components?\n\nTick-knock provides an extended API for working with linked components since version 4.0.0.\n\n- Method `withdraw` removes the first LinkedComponent component of the provided type or existing standard component\n- Method `pick` removes provided LinkedComponent component instance or existing standard component.\n\n  **Example**\n  You have a system responsible for checking boons (buffs) expiration, and you wish to remove expired boons from the\n  hero:\n  ```ts\n  enum BoonType {\n    PROTECTION,\n    AEGIS,\n    REGENERATION\n  }\n\n  class Boon extends LinkedComponent {\n    public constructor(\n        public readonly type: BoonType,\n        public value: number,\n        public duration: number\n    ) { super(); }\n  }\n\n  class BoonExpirationTestSystem extends IterativeSystem {\n    public constructor() {\n      super((entity) =\u003e entity.has(Boon));\n    }\n    \n    public updateEntity(entity: Entity, dt: number) {\n      // Let's update all boons\n      entity.iterate(Boon, (boon) =\u003e {\n          // Let's reduce boon remaining duration\n          boon.duration -= dt;\n          // If boon is expired\n          if (boon.duration \u003c= 0) {\n             // Then we need to removed it from the Entity\n             // But `entity.remove` will remove all boons, so we need to cherry-pick\n             entity.pick(boon);\n          } \n      });\n    }\n  }\n  ```\n- Method `iterate` iterates over instances of LinkedComponent and performs the `action` over each. Works for standard\n  components (action will be called for a single instance in this case).\n  \u003e 🎈 It's safe to `pick` only current entity during iteration.\n- Method `find` searches a component instance of the specified class. Works for standard components (predicate will be\n  called for a single instance in this case).\n- Method `getAll` returns a generator that can be used for iteration over all instances of specific type components.\n- Method `lengthOf` returns the number of existing components of the specified class.\n\nNow you know the basics. Now let's look at some examples to help you understand when linked components are helpful and\nhow to work with them.\n\n### Real world example\n\nWe want to get a system that handles \"Regeneration\" buff on the hero. There can be more than one sources of\nregeneration, so we must handle all of them at the same time.\n\nRegeneration has two effects:\n\n- Instantly healing heroes by constant amount of health points\n- Regenerates some amount of health over the time.\n\nThus, our system should do the following:\n\n- Heal the hero on the adding every new Regeneration buff.\n- Heal the hero over the time.\n- Manages regeneration expiration.\n\n```ts\nclass Regeneration extends LinkedComponent {\n  public constructor(\n    public instantHealValue: number,\n    public healPerSecond: number,\n    public duration: number\n  ) { super(); }\n}\n\nclass RegenerationSystem extends IterativeSystem {\n  public constructor() {\n    super((entity) =\u003e entity.has(Hero, Regeneration));\n  }\n\n  public updateEntity(entity: Entity, dt: number) {\n    const hero = entity.get(Hero)!\n    // Let's update all regeneration components on our hero and apply their effects \n    entity.iterate(Regeneration, (it) =\u003e {\n      // We need to heal hero\n      const healthPointsToAdd = Math.ceil(it.healPerSecond * dt);\n      hero.health += healthPointsToAdd;\n      // And then reduce regeneration duration\n      it.duration -= dt;\n      // If it's expired\n      if (it.duration \u003c= 0) {\n        // Then we need to removed it from the Entity\n        // But `entity.remove` will remove all boons, so we need to cherry-pick\n        entity.pick(it);\n      }\n    });\n  }\n\n  protected entityAdded = ({current}: EntitySnapshot) =\u003e {\n    // When new entity appears in the queue, that means that it has Hero and Regeneration\n    // so we want to instantly heal the hero by existing Regeneration buffs\n    current.iterate(Regeneration, (regeneration) =\u003e {\n      this.instantlyHealHero(entity, regeneration);\n    })\n    // Also, if any additional Regeneration buff will appear in the entity, we will handle \n    // them as well and instantly heal the hero\n    current.onComponentAdded.connect(this.instantlyHealHero);\n  }\n\n  protected entityRemoved = ({current}: EntitySnapshot) =\u003e {\n    // We don't want to know if any new components were added to the entity when it left \n    // the queue already.\n    current.onComponentAdded.disconnect(this.instantlyHealHero);\n  }\n\n  private instantlyHealHero = (entity: Entity, regeneration: any) =\u003e {\n    // We need to filter components, because this function will called on every added \n    // component (not only Regeneration)\n    if (!(regeneration instanceof Regeneration)) return;\n\n    const hero = entity.get(Hero)!;\n    hero.health += regeneration.instantHealValue;\n  }\n\n}\n```\n\n# Restrictions\n\n## Shared and Local Queries\n\nIn real development, you'll definitely face a situation when you want to reuse Query.\n\nFor example, when developing a game with heroes and enemies, you will surely always need two queries:\n\n**Simplified version**\n\n```typescript\nconst heroes = new Query(entity =\u003e entity.has(Hero));\nconst enemies = new Query(entity =\u003e entity.has(Enemy));\n```\n\nAnd you will want to use them in different systems. But the systems use local Queries. This means that after excluding a\nsystem from Engine, the Query in it will no longer be updated.\n\nTo prevent this from happening, you need to use the shared queries approach. To do this, you only need to add the query\nmanually after initializing the Engine.\n\n\u003e shared-queries.ts\n\n```typescript\nexport const heroes = new Query(entity =\u003e entity.has(Hero));\nexport const enemies = new Query(entity =\u003e entity.has(Enemy));\n```\n\n```typescript\nimport {heroes, enemies} from 'shared-queries';\n// ...\nengine.addQuery(heroes);\nengine.addQuery(enemies)\n```\n\nNow you can use these Queries in any other system.\n\n**Example:**\n\n```typescript\nimport {heroes, enemies} from 'shared-queries';\n\nclass DamageSystem extends IterativeSystem {\n  // ...\n  protected updateEntity(entity: Entity) {\n    const damage = entity.remove(Damage)!\n    const isHero = heroes.has(entity);\n    if (damage.type === DamageType.SPLASH) {\n      const neighbours = getNeighbours(isHero ? heroes : enemies);\n      // ...\n    }\n  }\n}\n```\n\n## Queries with complex logic and Entity invalidation\n\nThere are limitations for Query that do not allow you to track changes made inside components automatically.\n\nSuppose that you want Query to track entities with an X position of 10.\n\n```typescript\nconst query = new Query((entity) =\u003e entity.has(Position) \u0026\u0026 entity.get(Position).x === 10);\n```\n\nAnd you have changed the Position parameters accordingly:\n\n```typescript\nentity.get(Position)!.x = 10;\n```\n\nThe query will not know about these changes because the mechanism for tracking changes in component fields is redundant\nand heavy, which will have a huge impact on performance. But to fix this, you can use an entity method\ncalled `invalidate`, it will force Query to check this particular entity.\n\n❗ Try not to use this approach too often. It may affect the performance of your application.\n\n# License\n\nThis software released under [MIT](https://github.com/Leopotam/ecs/blob/master/LICENSE.md) license! Good luck, folks.\n\n[Restrictions]: #restrictions\n\n[Shared Config]: #shared-config\n\n[Shared and Local Queries]: #shared-and-local-queries\n\n[Queries with complex logic and Entity invalidation]: #queries-with-complex-logic-and-entity-invalidation\n\n[Snapshot]: #snapshot\n\n[IterativeSystem]: #iterativesystem\n\n[ReactionSystem]: #reactionsystem\n\n[Built-in query-based systems]: #built-in-query-based-systems\n\n[Queries and Systems]: #queries-and-systems\n\n[QueryBuilder]: #querybuilder\n\n[Query]: #query\n\n[System]: #system\n\n[Entity]: #entity\n\n[Tag]: #tag\n\n[Component]: #component\n\n[Linked Component]: #linked-component\n\n[Linked Components How-To]: #how-to-work-with-linked-components\n\n[Installing]: #installing\n\n[How it works?]: #how-it-works\n\n[Inside the Tick-Knock]: #inside-the-tick-knock\n\n[Subscription]: #subscription\n\n[Engine]: #engine\n\n[License]: #license\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmayakwd%2Ftick-knock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmayakwd%2Ftick-knock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmayakwd%2Ftick-knock/lists"}