├── packages
├── publish
│ ├── src
│ │ ├── index.ts
│ │ └── react.ts
│ ├── tsconfig.node.json
│ ├── tsconfig.json
│ ├── scripts
│ │ ├── copy-readme.ts
│ │ └── copy-react-files.ts
│ ├── LICENSE
│ ├── tsup.config.ts
│ ├── tests
│ │ ├── react
│ │ │ ├── actions.test.tsx
│ │ │ └── world.test.tsx
│ │ └── core
│ │ │ └── actions.test.ts
│ └── package.json
├── core
│ ├── .oxlintrc.json
│ ├── src
│ │ ├── utils
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ ├── shallow-equal.ts
│ │ │ ├── deque.ts
│ │ │ └── sparse-set.ts
│ │ ├── query
│ │ │ ├── symbols.ts
│ │ │ ├── modifiers
│ │ │ │ ├── not.ts
│ │ │ │ ├── or.ts
│ │ │ │ ├── added.ts
│ │ │ │ └── removed.ts
│ │ │ ├── utils
│ │ │ │ ├── create-binary-string.ts
│ │ │ │ ├── is-query.ts
│ │ │ │ ├── tracking-cursor.ts
│ │ │ │ ├── check-query-with-relations.ts
│ │ │ │ ├── check-query-tracking-with-relations.ts
│ │ │ │ ├── check-query.ts
│ │ │ │ ├── create-query-hash.ts
│ │ │ │ └── check-query-tracking.ts
│ │ │ └── modifier.ts
│ │ ├── world
│ │ │ ├── index.ts
│ │ │ └── utils
│ │ │ │ ├── increment-world-bit-flag.ts
│ │ │ │ └── world-index.ts
│ │ ├── relation
│ │ │ ├── symbols.ts
│ │ │ ├── utils
│ │ │ │ └── is-relation.ts
│ │ │ └── types.ts
│ │ ├── common.ts
│ │ ├── storage
│ │ │ ├── index.ts
│ │ │ ├── stores.ts
│ │ │ ├── schema.ts
│ │ │ └── types.ts
│ │ ├── universe
│ │ │ └── universe.ts
│ │ ├── actions
│ │ │ ├── types.ts
│ │ │ └── create-actions.ts
│ │ ├── entity
│ │ │ ├── types.ts
│ │ │ └── utils
│ │ │ │ └── pack-entity.ts
│ │ ├── trait
│ │ │ └── trait-instance.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── package.json
│ ├── tests
│ │ ├── actions.test.ts
│ │ └── utils
│ │ │ └── sparse-set.test.ts
│ └── architecture.md
└── react
│ ├── .oxlintrc.json
│ ├── tsconfig.json
│ ├── src
│ ├── world
│ │ ├── world-context.ts
│ │ ├── world-provider.tsx
│ │ └── use-world.ts
│ ├── utils
│ │ └── is-world.ts
│ ├── hooks
│ │ ├── use-query-first.ts
│ │ ├── use-actions.ts
│ │ ├── use-trait-effect.ts
│ │ ├── use-has.ts
│ │ ├── use-tag.ts
│ │ ├── use-target.ts
│ │ ├── use-targets.ts
│ │ ├── use-query.ts
│ │ └── use-trait.ts
│ └── index.ts
│ ├── package.json
│ └── tests
│ ├── actions.test.tsx
│ └── world.test.tsx
├── benches
├── apps
│ ├── boids
│ │ ├── src
│ │ │ ├── vite-env.d.ts
│ │ │ ├── styles.css
│ │ │ ├── scene.ts
│ │ │ ├── traits
│ │ │ │ └── InstancedMesh.ts
│ │ │ ├── systems
│ │ │ │ ├── render.ts
│ │ │ │ ├── syncThreeObjects.ts
│ │ │ │ └── init.ts
│ │ │ └── main.ts
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.js
│ │ ├── .gitignore
│ │ ├── index.html
│ │ └── package.json
│ ├── add-remove
│ │ ├── src
│ │ │ ├── vite-env.d.ts
│ │ │ ├── styles.css
│ │ │ ├── scene.ts
│ │ │ ├── trait
│ │ │ │ └── Points.ts
│ │ │ ├── systems
│ │ │ │ ├── syncThreeObjects.ts
│ │ │ │ └── init.ts
│ │ │ └── main.ts
│ │ ├── tsconfig.json
│ │ ├── .gitignore
│ │ ├── index.html
│ │ └── package.json
│ ├── n-body
│ │ ├── src
│ │ │ ├── vite-env.d.ts
│ │ │ ├── styles.css
│ │ │ ├── scene.ts
│ │ │ ├── traits
│ │ │ │ └── InstancedMesh.ts
│ │ │ └── systems
│ │ │ │ ├── render.ts
│ │ │ │ ├── cleanupRepulsors.ts
│ │ │ │ ├── spawnRepulsor.ts
│ │ │ │ ├── init.ts
│ │ │ │ └── syncThreeObjects.ts
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.js
│ │ ├── .gitignore
│ │ ├── index.html
│ │ └── package.json
│ ├── revade
│ │ ├── src
│ │ │ ├── vite-env.d.ts
│ │ │ ├── traits
│ │ │ │ ├── is-enemy.ts
│ │ │ │ ├── is-player.ts
│ │ │ │ ├── time.ts
│ │ │ │ ├── auto-rotate.ts
│ │ │ │ ├── is-shield-visible.ts
│ │ │ │ ├── shield-visibility.ts
│ │ │ │ ├── input.ts
│ │ │ │ ├── avoidance.ts
│ │ │ │ ├── relations.ts
│ │ │ │ ├── spatial-hash-map.ts
│ │ │ │ ├── bullet.ts
│ │ │ │ ├── explosion.ts
│ │ │ │ ├── transform.ts
│ │ │ │ ├── movement.ts
│ │ │ │ └── index.ts
│ │ │ ├── utils
│ │ │ │ ├── between.ts
│ │ │ │ └── use-stats.ts
│ │ │ ├── index.css
│ │ │ ├── world.ts
│ │ │ ├── systems
│ │ │ │ ├── damp-player-movement.ts
│ │ │ │ ├── update-auto-rotate.ts
│ │ │ │ ├── update-spatial-hashing.ts
│ │ │ │ ├── tick-explosion.ts
│ │ │ │ ├── cleanup-spatial-hash-map.ts
│ │ │ │ ├── update-time.ts
│ │ │ │ ├── spawn-enemies.ts
│ │ │ │ ├── apply-input.ts
│ │ │ │ ├── update-bullet.ts
│ │ │ │ ├── update-movement.ts
│ │ │ │ ├── follow-player.ts
│ │ │ │ ├── tick-shield-visibility.ts
│ │ │ │ ├── update-bullet-collisions.ts
│ │ │ │ ├── handle-shooting.ts
│ │ │ │ ├── update-avoidance.ts
│ │ │ │ ├── push-enemies.ts
│ │ │ │ ├── schedule.ts
│ │ │ │ └── poll-input.ts
│ │ │ ├── main.tsx
│ │ │ └── actions.ts
│ │ ├── .triplex
│ │ │ ├── config.json
│ │ │ └── provider.tsx
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ ├── index.html
│ │ └── package.json
│ ├── n-body-react
│ │ ├── src
│ │ │ ├── vite-env.d.ts
│ │ │ ├── traits
│ │ │ │ └── InstancedMesh.ts
│ │ │ ├── index.css
│ │ │ ├── use-stats.ts
│ │ │ ├── main.tsx
│ │ │ ├── use-raf.ts
│ │ │ ├── systems
│ │ │ │ ├── cleanupRepulsors.ts
│ │ │ │ └── syncThreeObjects.ts
│ │ │ └── actions.ts
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── .gitignore
│ │ ├── index.html
│ │ └── package.json
│ └── app-tools
│ │ ├── tsconfig.json
│ │ ├── src
│ │ ├── index.ts
│ │ └── stats
│ │ │ ├── stats.css
│ │ │ └── stats.ts
│ │ └── package.json
└── sims
│ ├── graph-traversal
│ ├── src
│ │ ├── traits
│ │ │ ├── index.ts
│ │ │ └── child-of.ts
│ │ ├── config.ts
│ │ ├── world.ts
│ │ ├── index.ts
│ │ ├── systems
│ │ │ ├── schedule.ts
│ │ │ ├── traverse.ts
│ │ │ └── init.ts
│ │ └── main.ts
│ ├── tsconfig.json
│ └── package.json
│ ├── boids
│ ├── .oxlintrc.json
│ ├── tsconfig.json
│ ├── src
│ │ ├── traits
│ │ │ ├── neighbor-of.ts
│ │ │ ├── time.ts
│ │ │ ├── position.ts
│ │ │ ├── velocity.ts
│ │ │ ├── index.ts
│ │ │ └── forces.ts
│ │ ├── utils
│ │ │ ├── between.ts
│ │ │ └── random-direction.ts
│ │ ├── world.ts
│ │ ├── config.ts
│ │ ├── index.ts
│ │ ├── systems
│ │ │ ├── update-time.ts
│ │ │ ├── apply-forces.ts
│ │ │ ├── avoid-edges.ts
│ │ │ ├── move-boids.ts
│ │ │ ├── schedule.ts
│ │ │ ├── update-alignment.ts
│ │ │ ├── update-coherence.ts
│ │ │ ├── update-neighbors.ts
│ │ │ └── update-separation.ts
│ │ ├── main.ts
│ │ └── actions.ts
│ └── package.json
│ ├── add-remove
│ ├── .oxlintrc.json
│ ├── tsconfig.json
│ ├── src
│ │ ├── trait
│ │ │ ├── Mass.ts
│ │ │ ├── Circle.ts
│ │ │ ├── Time.ts
│ │ │ ├── Velocity.ts
│ │ │ ├── Color.ts
│ │ │ ├── Position.ts
│ │ │ ├── Dummy.ts
│ │ │ └── index.ts
│ │ ├── utils
│ │ │ └── randInRange.ts
│ │ ├── world.ts
│ │ ├── constants.ts
│ │ ├── systems
│ │ │ ├── moveBodies.ts
│ │ │ ├── updateGravity.ts
│ │ │ ├── updateTime.ts
│ │ │ ├── init.ts
│ │ │ ├── schedule.ts
│ │ │ ├── recycleBodies.ts
│ │ │ └── setInitial.ts
│ │ ├── main.ts
│ │ └── index.ts
│ └── package.json
│ ├── n-body
│ ├── .oxlintrc.json
│ ├── tsconfig.json
│ ├── src
│ │ ├── traits
│ │ │ ├── Mass.ts
│ │ │ ├── Circle.ts
│ │ │ ├── IsCentralMass.ts
│ │ │ ├── Position.ts
│ │ │ ├── Time.ts
│ │ │ ├── Velocity.ts
│ │ │ ├── Acceleration.ts
│ │ │ ├── Color.ts
│ │ │ ├── Repulse.ts
│ │ │ └── index.ts
│ │ ├── utils
│ │ │ ├── randInRange.ts
│ │ │ └── colorFromSpeed.ts
│ │ ├── world.ts
│ │ ├── systems
│ │ │ ├── updateTime.ts
│ │ │ ├── moveBodies.ts
│ │ │ ├── updateColor.ts
│ │ │ ├── init.ts
│ │ │ ├── schedule.ts
│ │ │ ├── updateGravity.ts
│ │ │ └── setInitial.ts
│ │ ├── main.ts
│ │ ├── index.ts
│ │ └── constants.ts
│ └── package.json
│ └── bench-tools
│ ├── .oxlintrc.json
│ ├── tsconfig.json
│ ├── src
│ ├── index.ts
│ ├── Worker.ts
│ ├── getGlobal.ts
│ ├── getFPS.ts
│ ├── getThreadCount.ts
│ ├── raf.ts
│ └── measure.ts
│ └── package.json
├── .vscode
├── extensions.json
└── settings.json
├── scripts
├── tsconfig.json
├── utils
│ └── parseArguments.ts
├── constants
│ └── colors.ts
├── app.ts
└── sim.ts
├── .config
├── oxlint
│ ├── README.md
│ ├── react.json
│ ├── package.json
│ └── base.json
├── prettier
│ ├── README.md
│ ├── package.json
│ └── base.json
└── typescript
│ ├── README.md
│ ├── react.json
│ ├── package.json
│ ├── node.json
│ └── base.json
├── pnpm-workspace.yaml
├── .gitignore
├── LICENSE
├── package.json
└── .github
└── workflows
└── pr-checks.yml
/packages/publish/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../../core/src/index';
2 |
--------------------------------------------------------------------------------
/packages/publish/src/react.ts:
--------------------------------------------------------------------------------
1 | export * from '../../react/src/index';
2 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/.triplex/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "provider": "./provider.tsx"
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/traits/index.ts:
--------------------------------------------------------------------------------
1 | export * from './child-of';
2 |
3 |
--------------------------------------------------------------------------------
/packages/core/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["node_modules/@config/oxlint/base.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["node_modules/@config/oxlint/base.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/react/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["node_modules/@config/oxlint/react.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode", "oxc.oxc-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["node_modules/@config/oxlint/base.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["node_modules/@config/oxlint/base.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/.oxlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["node_modules/@config/oxlint/base.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.config/typescript/node.json",
3 | "include": ["**/*.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/is-enemy.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const IsEnemy = trait();
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/config.ts:
--------------------------------------------------------------------------------
1 | export const CONFIG = {
2 | depth: 7,
3 | childrenPerNode: 3,
4 | };
5 |
--------------------------------------------------------------------------------
/benches/sims/n-body/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/core/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { Deque } from './deque';
2 | export { SparseSet } from './sparse-set';
3 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/app-tools/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/base.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/is-player.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const IsPlayer = trait();
4 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Mass.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Mass = trait({ value: 0 });
4 |
--------------------------------------------------------------------------------
/packages/publish/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["scripts"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Mass.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Mass = trait({ value: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/world.ts:
--------------------------------------------------------------------------------
1 | import { createWorld } from 'koota';
2 |
3 | export const world = createWorld();
4 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Circle.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Circle = trait({ radius: 0 });
4 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/base.json",
3 | "include": ["src/**/*", "tests"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/react.json",
3 | "include": ["src/**/*", "tests"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/boids/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["*.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/n-body/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["*.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/time.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Time = trait({ last: 0, delta: 0 });
4 |
--------------------------------------------------------------------------------
/benches/apps/revade/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["*.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Circle.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Circle = trait({ radius: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/traits/neighbor-of.ts:
--------------------------------------------------------------------------------
1 | import { relation } from 'koota';
2 |
3 | export const NeighborOf = relation();
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/traits/time.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Time = trait({ last: 0, delta: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/IsCentralMass.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const IsCentralMass = trait();
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Position.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Position = trait({ x: 0, y: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Time.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Time = trait({ last: 0, delta: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Velocity.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Velocity = trait({ x: 0, y: 0 });
4 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "include": ["*.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/auto-rotate.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const AutoRotate = trait({ speed: 1 });
4 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/is-shield-visible.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const IsShieldVisible = trait();
4 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Time.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Time = trait({ last: 0, delta: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Velocity.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Velocity = trait({ x: 0, y: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/traits/position.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Position = trait({ x: 0, y: 0, z: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/traits/velocity.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Velocity = trait({ x: 0, y: 0, z: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/traits/child-of.ts:
--------------------------------------------------------------------------------
1 | import { relation } from 'koota';
2 |
3 | export const ChildOf = relation();
4 |
5 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Color.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Color = trait({ r: 0, g: 0, b: 0, a: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Position.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Position = trait({ x: 0, y: 0, z: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Acceleration.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Acceleration = trait({ x: 0, y: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Color.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Color = trait({ r: 0, g: 0, b: 0, a: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/utils/randInRange.ts:
--------------------------------------------------------------------------------
1 | export const randInRange = (min: number, max: number) => Math.random() * (max - min) + min;
2 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/utils/randInRange.ts:
--------------------------------------------------------------------------------
1 | export const randInRange = (min: number, max: number) => Math.random() * (max - min) + min;
2 |
--------------------------------------------------------------------------------
/packages/core/src/query/symbols.ts:
--------------------------------------------------------------------------------
1 | export const $parameters = Symbol.for('parameters');
2 | export const $queryRef = Symbol.for('queryRef');
3 |
--------------------------------------------------------------------------------
/packages/core/src/world/index.ts:
--------------------------------------------------------------------------------
1 | export { createWorld } from './world';
2 | export type { World, WorldOptions, WorldInternal } from './types';
3 |
--------------------------------------------------------------------------------
/.config/oxlint/README.md:
--------------------------------------------------------------------------------
1 | # `@config/oxlint`
2 |
3 | These are base shared `.oxlintrc.json`s from which all other `.oxlintrc.json`'s inherit from.
4 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/Repulse.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Repulse = trait({ force: 0, decay: 0, delay: 0 });
4 |
--------------------------------------------------------------------------------
/packages/core/src/relation/symbols.ts:
--------------------------------------------------------------------------------
1 | export const $relationPair = Symbol.for('relationPair');
2 | export const $relation = Symbol.for('relation');
3 |
--------------------------------------------------------------------------------
/.config/prettier/README.md:
--------------------------------------------------------------------------------
1 | # `@config/prettier`
2 |
3 | These are base shared `.prettierrc.json`s from which all other `.prettierrc.json`'s inherit from.
4 |
--------------------------------------------------------------------------------
/.config/typescript/README.md:
--------------------------------------------------------------------------------
1 | # `@config/typescript`
2 |
3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from.
4 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/utils/between.ts:
--------------------------------------------------------------------------------
1 | export function between(min: number, max: number): number {
2 | return Math.random() * (max - min) + min;
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/utils/between.ts:
--------------------------------------------------------------------------------
1 | export function between(min: number, max: number): number {
2 | return Math.random() * (max - min) + min;
3 | }
4 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/world.ts:
--------------------------------------------------------------------------------
1 | import { createWorld } from 'koota';
2 | import { Time } from './traits';
3 |
4 | export const world = createWorld(Time);
5 |
--------------------------------------------------------------------------------
/.config/prettier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@config/prettier",
3 | "version": "0.1.0",
4 | "private": true,
5 | "files": [
6 | "base.json"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.config/oxlint/react.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/oxlint/configuration_schema.json",
3 | "extends": ["base.json"],
4 | "plugins": ["react"]
5 | }
6 |
--------------------------------------------------------------------------------
/benches/apps/app-tools/src/index.ts:
--------------------------------------------------------------------------------
1 | export { getFPS, measure, requestAnimationFrame } from '@sim/bench-tools';
2 |
3 | export { initStats } from './stats/stats';
4 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/scene.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | export const scene = new THREE.Scene();
4 | scene.background = new THREE.Color(0x000000);
5 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/scene.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | export const scene = new THREE.Scene();
4 | scene.background = new THREE.Color(0x000000);
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/shield-visibility.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const ShieldVisibility = trait({ duration: 1400, current: 0 });
4 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/world.ts:
--------------------------------------------------------------------------------
1 | import { createWorld } from 'koota';
2 | import { Time } from './trait/Time';
3 |
4 | export const world = createWorld(Time);
5 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/world.ts:
--------------------------------------------------------------------------------
1 | import { createWorld } from 'koota';
2 | import { Time } from './traits/Time';
3 |
4 | export const world = createWorld(Time);
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "oxc.enable": true,
4 | "biome.enabled": false,
5 | "eslint.enable": false
6 | }
7 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/scene.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | export const scene = new THREE.Scene();
4 | scene.background = new THREE.Color(0x000000);
5 |
--------------------------------------------------------------------------------
/benches/apps/boids/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/base.json",
3 | "include": ["src"],
4 | "references": [{ "path": "./tsconfig.node.json" }]
5 | }
6 |
--------------------------------------------------------------------------------
/benches/apps/n-body/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/base.json",
3 | "include": ["src"],
4 | "references": [{ "path": "./tsconfig.node.json" }]
5 | }
6 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/input.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import * as THREE from 'three';
3 |
4 | export const Input = trait(() => new THREE.Vector2());
5 |
--------------------------------------------------------------------------------
/packages/core/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type Constructor = new (...args: any[]) => T;
2 | export type IsEmpty = T extends Record ? true : false;
3 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/react.json",
3 | "include": ["src"],
4 | "references": [{ "path": "./tsconfig.node.json" }]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/publish/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/react.json",
3 | "include": ["src", "tests"],
4 | "references": [{ "path": "tsconfig.node.json" }]
5 | }
6 |
--------------------------------------------------------------------------------
/.config/oxlint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@config/oxlint",
3 | "version": "0.1.0",
4 | "private": true,
5 | "files": [
6 | "base.json",
7 | "react.json"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.config/typescript/react.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "jsx": "react-jsx"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/trait/Points.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import type * as THREE from 'three';
3 |
4 | export const Points = trait({ object: null! as THREE.Points });
5 |
--------------------------------------------------------------------------------
/benches/apps/app-tools/src/stats/stats.css:
--------------------------------------------------------------------------------
1 | .stats {
2 | position: absolute;
3 | top: 20px;
4 | left: 20px;
5 | color: white;
6 | font-family: system-ui;
7 | font-size: 18px;
8 | }
9 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/avoidance.ts:
--------------------------------------------------------------------------------
1 | import { type Entity, trait } from 'koota';
2 |
3 | export const Avoidance = trait({
4 | neighbors: () => [] as Entity[],
5 | range: 1.5,
6 | });
7 |
--------------------------------------------------------------------------------
/packages/react/src/world/world-context.ts:
--------------------------------------------------------------------------------
1 | import type { World } from '@koota/core';
2 | import { createContext } from 'react';
3 |
4 | export const WorldContext = createContext(null!);
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/react.json",
3 | "include": ["src", ".triplex/provider.tsx"],
4 | "references": [{ "path": "./tsconfig.node.json" }]
5 | }
6 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@config/typescript/node.json",
3 | "compilerOptions": {
4 | "lib": ["DOM", "WebWorker", "ESNext"]
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/traits/InstancedMesh.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import type * as THREE from 'three';
3 |
4 | export const InstancedMesh = trait(() => null! as THREE.InstancedMesh);
5 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/traits/index.ts:
--------------------------------------------------------------------------------
1 | export * from './position.ts';
2 | export * from './velocity.ts';
3 | export * from './time.ts';
4 | export * from './forces.ts';
5 | export * from './neighbor-of.ts';
6 |
--------------------------------------------------------------------------------
/.config/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@config/typescript",
3 | "version": "0.1.0",
4 | "private": true,
5 | "files": [
6 | "base.json",
7 | "react.json",
8 | "node.json"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/traits/InstancedMesh.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import type * as THREE from 'three';
3 |
4 | export const InstancedMesh = trait({ object: () => null! as THREE.InstancedMesh });
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/relations.ts:
--------------------------------------------------------------------------------
1 | import { relation } from 'koota';
2 |
3 | export const FiredBy = relation({ autoRemoveTarget: true });
4 |
5 | export const Targeting = relation({ exclusive: true });
6 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const CONSTANTS = {
2 | BODIES: 40000,
3 | GRAVITY: -9.81 * 2,
4 | FLOOR: -1000,
5 | COMPONENTS: 100,
6 | MAX_COMPS_PER_ENTITY: 20,
7 | DRAIN: false,
8 | };
9 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/traits/InstancedMesh.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import type * as THREE from 'three';
3 |
4 | export const InstancedMesh = trait({ object: () => null! as THREE.InstancedMesh });
5 |
--------------------------------------------------------------------------------
/packages/react/src/utils/is-world.ts:
--------------------------------------------------------------------------------
1 | import type { Entity, World } from '@koota/core';
2 |
3 | export function isWorld(target: Entity | World): target is World {
4 | return typeof (target as World)?.spawn === 'function';
5 | }
6 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/spatial-hash-map.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import { SpatialHashMap as SpatialHashMapImpl } from '../utils/spatial-hash';
3 |
4 | export const SpatialHashMap = trait(() => new SpatialHashMapImpl(5));
5 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body,
7 | #root {
8 | width: 100%;
9 | height: 100%;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | body {
15 | background-color: #000;
16 | }
17 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body,
7 | #root {
8 | width: 100%;
9 | height: 100%;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | body {
15 | background-color: #000;
16 | }
17 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/bullet.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import * as THREE from 'three';
3 |
4 | export const Bullet = trait({
5 | speed: 60,
6 | direction: () => new THREE.Vector3(),
7 | lifetime: 2,
8 | timeAlive: 0,
9 | });
10 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/index.ts:
--------------------------------------------------------------------------------
1 | export { getFPS } from './getFPS.js';
2 | export { getThreadCount } from './getThreadCount.js';
3 | export * from './measure.js';
4 | export { requestAnimationFrame } from './raf.js';
5 | export { Worker } from './Worker.js';
6 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/explosion.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import type * as THREE from 'three';
3 |
4 | export const Explosion = trait({
5 | duration: 500,
6 | current: 0,
7 | count: 12,
8 | velocities: () => [] as THREE.Vector3[],
9 | });
10 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/systems/render.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Three } from '../main';
3 |
4 | export const render = ({ world }: { world: World }) => {
5 | const { renderer, scene, camera } = world.get(Three)!;
6 | renderer.render(scene, camera);
7 | };
8 |
--------------------------------------------------------------------------------
/benches/apps/boids/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | server: {
5 | headers: {
6 | 'Cross-Origin-Embedder-Policy': 'require-corp',
7 | 'Cross-Origin-Opener-Policy': 'same-origin',
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/systems/render.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Three } from '../main';
3 |
4 | export const render = ({ world }: { world: World }) => {
5 | const { renderer, scene, camera } = world.get(Three)!;
6 | renderer.render(scene, camera);
7 | };
8 |
--------------------------------------------------------------------------------
/benches/apps/n-body/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | server: {
5 | headers: {
6 | 'Cross-Origin-Embedder-Policy': 'require-corp',
7 | 'Cross-Origin-Opener-Policy': 'same-origin',
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/transform.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import * as THREE from 'three';
3 |
4 | export const Transform = trait({
5 | position: () => new THREE.Vector3(),
6 | rotation: () => new THREE.Euler(),
7 | quaternion: () => new THREE.Quaternion(),
8 | });
9 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/Dummy.ts:
--------------------------------------------------------------------------------
1 | import { type Trait, trait } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 |
4 | export const DummyComponents = [] as Trait[];
5 |
6 | for (let i = 0; i < CONSTANTS.COMPONENTS; i++) {
7 | DummyComponents.push(trait());
8 | }
9 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/config.ts:
--------------------------------------------------------------------------------
1 | export const CONFIG = {
2 | initialCount: 1000,
3 | maxVelocity: 20,
4 | minVelocity: 12,
5 | avoidEdgesFactor: 20,
6 | avoidEdgesMaxDistance: 200,
7 | coherenceFactor: 0.05,
8 | separationFactor: 80,
9 | alignmentFactor: 2.0,
10 | };
11 |
--------------------------------------------------------------------------------
/benches/apps/revade/.triplex/provider.tsx:
--------------------------------------------------------------------------------
1 | import { WorldProvider } from 'koota/react';
2 | import { world } from '../src/world';
3 |
4 | export default function Provider({ children }: { children?: React.ReactNode }) {
5 | return {children};
6 | }
7 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/index.ts:
--------------------------------------------------------------------------------
1 | export { CONFIG } from './config';
2 | export { init } from './systems/init';
3 | export { traverse } from './systems/traverse';
4 | export { schedule } from './systems/schedule';
5 | export * from './traits';
6 | export { world } from './world';
7 |
8 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/movement.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 | import * as THREE from 'three';
3 |
4 | export const Movement = trait({
5 | velocity: () => new THREE.Vector3(),
6 | force: () => new THREE.Vector3(),
7 | thrust: 1,
8 | maxSpeed: 10,
9 | damping: 0.9,
10 | });
11 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/traits/forces.ts:
--------------------------------------------------------------------------------
1 | import { trait } from 'koota';
2 |
3 | export const Forces = trait({
4 | coherence: () => ({ x: 0, y: 0, z: 0 }),
5 | separation: () => ({ x: 0, y: 0, z: 0 }),
6 | alignment: () => ({ x: 0, y: 0, z: 0 }),
7 | avoidEdges: () => ({ x: 0, y: 0, z: 0 }),
8 | });
9 |
--------------------------------------------------------------------------------
/packages/core/src/common.ts:
--------------------------------------------------------------------------------
1 | export const $internal = Symbol.for('koota.internal');
2 |
3 | /**
4 | * Type utility for symbol-branded runtime type checks.
5 | * Allows accessing a symbol property while maintaining type safety.
6 | */
7 | export type Brand = { readonly [K in S]?: true };
8 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/trait/index.ts:
--------------------------------------------------------------------------------
1 | export { Circle } from './Circle';
2 | export { Color } from './Color';
3 | export { DummyComponents } from './Dummy';
4 | export { Mass } from './Mass';
5 | export { Position } from './Position';
6 | export { Time } from './Time';
7 | export { Velocity } from './Velocity';
8 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/systems/schedule.ts:
--------------------------------------------------------------------------------
1 | import { Schedule } from 'directed';
2 | import { world } from '../world';
3 | import { traverse } from './traverse';
4 |
5 | export const schedule = new Schedule<{ world: typeof world }>();
6 |
7 | schedule.add(traverse);
8 |
9 | schedule.build();
10 |
11 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-query-first.ts:
--------------------------------------------------------------------------------
1 | import type { Entity, QueryParameter } from '@koota/core';
2 | import { useQuery } from './use-query';
3 |
4 | export function useQueryFirst(...parameters: T): Entity | undefined {
5 | const query = useQuery(...parameters);
6 | return query[0];
7 | }
8 |
--------------------------------------------------------------------------------
/packages/core/src/query/modifiers/not.ts:
--------------------------------------------------------------------------------
1 | import type { Trait } from '../../trait/types';
2 | import type { Modifier } from '../types';
3 | import { createModifier } from '../modifier';
4 |
5 | export const Not = (...traits: T): Modifier => {
6 | return createModifier('not', 1, traits);
7 | };
8 |
--------------------------------------------------------------------------------
/packages/core/src/query/modifiers/or.ts:
--------------------------------------------------------------------------------
1 | import type { Trait } from '../../trait/types';
2 | import type { Modifier } from '../types';
3 | import { createModifier } from '../modifier';
4 |
5 | export const Or = (...traits: T): Modifier => {
6 | return createModifier('or', 2, traits);
7 | };
8 |
--------------------------------------------------------------------------------
/scripts/utils/parseArguments.ts:
--------------------------------------------------------------------------------
1 | export const parseArguments = (args: string[]): Record => {
2 | const argsObj: Record = {};
3 | args.forEach((val, index) => {
4 | if (val.startsWith('--')) {
5 | argsObj[val.substring(2)] = args[index + 1];
6 | }
7 | });
8 | return argsObj;
9 | };
10 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-actions.ts:
--------------------------------------------------------------------------------
1 | import type { World } from '@koota/core';
2 | import { useWorld } from '../world/use-world';
3 |
4 | export function useActions any>>(
5 | actions: (world: World) => T
6 | ) {
7 | const world = useWorld();
8 | return actions(world);
9 | }
10 |
--------------------------------------------------------------------------------
/packages/react/src/world/world-provider.tsx:
--------------------------------------------------------------------------------
1 | import type { World } from '@koota/core';
2 | import { WorldContext } from './world-context';
3 |
4 | export function WorldProvider({ children, world }: { children: React.ReactNode; world: World }) {
5 | return {children};
6 | }
7 |
--------------------------------------------------------------------------------
/.config/typescript/node.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "lib": ["ESNext"],
6 | "composite": true,
7 | "allowSyntheticDefaultImports": true,
8 | "rewriteRelativeImportExtensions": true,
9 | "erasableSyntaxOnly": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/world.ts:
--------------------------------------------------------------------------------
1 | import { createWorld } from 'koota';
2 | import { Time } from './traits';
3 | import { SpatialHashMap } from './traits/spatial-hash-map';
4 | import { SpatialHashMap as SpatialHashMapImpl } from './utils/spatial-hash';
5 |
6 | export const world = createWorld(Time, SpatialHashMap(new SpatialHashMapImpl(50)));
7 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './traits';
2 | export { world } from './world';
3 | export { CONFIG } from './config';
4 | export { schedule } from './systems/schedule';
5 | export { actions } from './actions';
6 | export { randomSphericalDirection } from './utils/random-direction';
7 | export { between } from './utils/between';
8 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sim/bench-tools",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "Benchmark tools",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "dependencies": {
9 | "web-worker": "^1.5.0"
10 | },
11 | "devDependencies": {
12 | "@config/typescript": "workspace:*"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/benches/apps/app-tools/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@app/bench-tools",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "Benchmark tools",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "dependencies": {
9 | "@sim/bench-tools": "workspace:*"
10 | },
11 | "devDependencies": {
12 | "@config/typescript": "workspace:*"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/utils/random-direction.ts:
--------------------------------------------------------------------------------
1 | export function randomSphericalDirection(magnitude = 1) {
2 | const theta = Math.random() * Math.PI * 2;
3 | const u = Math.random() * 2 - 1;
4 | const c = Math.sqrt(1 - u * u);
5 |
6 | return {
7 | x: c * Math.cos(theta) * magnitude,
8 | y: u * magnitude,
9 | z: c * Math.sin(theta) * magnitude,
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/benches/apps/revade/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react';
2 | import { defineConfig } from 'vite';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | headers: {
9 | 'Cross-Origin-Embedder-Policy': 'require-corp',
10 | 'Cross-Origin-Opener-Policy': 'same-origin',
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/create-binary-string.ts:
--------------------------------------------------------------------------------
1 | export function createBinaryString(nMask: number) {
2 | // nMask must be between -2147483648 and 2147483647
3 | let nFlag = 0;
4 | let nShifted = nMask;
5 | let sMask = '';
6 |
7 | while (nFlag < 32) {
8 | sMask += String(nShifted >>> 31);
9 | nShifted <<= 1;
10 | nFlag++;
11 | }
12 |
13 | return sMask;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/core/src/storage/index.ts:
--------------------------------------------------------------------------------
1 | export { createStore } from './stores';
2 | export {
3 | createSetFunction,
4 | createFastSetFunction,
5 | createFastSetChangeFunction,
6 | createGetFunction,
7 | } from './accessors';
8 |
9 | export { validateSchema, getSchemaDefaults } from './schema';
10 |
11 | export type { Store, StoreType, Schema, AoSFactory, Norm } from './types';
12 |
--------------------------------------------------------------------------------
/benches/apps/boids/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react';
2 | import { defineConfig } from 'vite';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | headers: {
9 | 'Cross-Origin-Embedder-Policy': 'require-corp',
10 | 'Cross-Origin-Opener-Policy': 'same-origin',
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/Worker.ts:
--------------------------------------------------------------------------------
1 | import WorkerConstruct from 'web-worker';
2 | import { getGlobal } from './getGlobal.js';
3 |
4 | const global = getGlobal();
5 |
6 | const isNode =
7 | typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
8 |
9 | export const Worker: typeof globalThis.Worker = isNode ? WorkerConstruct : global.Worker;
10 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/benches/apps/n-body/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/benches/apps/revade/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/use-stats.ts:
--------------------------------------------------------------------------------
1 | import { initStats } from '@app/bench-tools';
2 | import { useEffect, useMemo } from 'react';
3 |
4 | export function useStats(extras: Parameters[0]) {
5 | const api = useMemo(() => initStats(extras), [extras]);
6 |
7 | useEffect(() => {
8 | api.create();
9 | return () => api.destroy();
10 | });
11 |
12 | return api;
13 | }
14 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/utils/use-stats.ts:
--------------------------------------------------------------------------------
1 | import { initStats } from '@app/bench-tools';
2 | import { useEffect, useMemo } from 'react';
3 |
4 | export function useStats(extras: Parameters[0]) {
5 | const api = useMemo(() => initStats(extras), [extras]);
6 |
7 | useEffect(() => {
8 | api.create();
9 | return () => api.destroy();
10 | });
11 |
12 | return api;
13 | }
14 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/damp-player-movement.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Input, Movement } from '../traits';
3 |
4 | export const dampPlayerMovement = ({ world }: { world: World }) => {
5 | world.query(Movement, Input).updateEach(([{ velocity, damping }, input]) => {
6 | if (input.lengthSq() === 0) {
7 | velocity.multiplyScalar(damping);
8 | }
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/packages/react/src/world/use-world.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import type { World } from '@koota/core';
3 | import { WorldContext } from './world-context';
4 |
5 | export function useWorld(): World {
6 | const world = useContext(WorldContext);
7 |
8 | if (!world) {
9 | throw new Error('Koota: useWorld must be used within a WorldProvider');
10 | }
11 |
12 | return world;
13 | }
14 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/traits/index.ts:
--------------------------------------------------------------------------------
1 | export { Acceleration } from './Acceleration';
2 | export { Circle } from './Circle';
3 | export { Color } from './Color';
4 | export { IsCentralMass } from './IsCentralMass';
5 | export { Mass } from './Mass';
6 | export { Position } from './Position';
7 | export { Repulse } from './Repulse';
8 | export { Time } from './Time';
9 | export { Velocity } from './Velocity';
10 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/getGlobal.ts:
--------------------------------------------------------------------------------
1 | export const getGlobal = () => {
2 | if (typeof self !== 'undefined') {
3 | return self;
4 | } else if (typeof window !== 'undefined') {
5 | return window;
6 | } else if (typeof global !== 'undefined') {
7 | return global;
8 | } else {
9 | // Undefined context, could throw an error or handle accordingly
10 | throw new Error('Unknown context');
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/is-query.ts:
--------------------------------------------------------------------------------
1 | import type { Brand } from '../../common';
2 | import type { Query } from '../types';
3 | import { $queryRef } from '../symbols';
4 |
5 | /**
6 | * Check if a value is a Query
7 | */
8 | export /* @inline @pure */ function isQuery(value: unknown): value is Query {
9 | return (value as Brand | null | undefined)?.[$queryRef] as unknown as boolean;
10 | }
11 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-auto-rotate.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { AutoRotate, Time, Transform } from '../traits';
3 |
4 | export const updateAutoRotate = ({ world }: { world: World }) => {
5 | const { delta } = world.get(Time)!;
6 | world.query(Transform, AutoRotate).updateEach(([{ rotation }, autoRotate]) => {
7 | rotation.x = rotation.y += delta * autoRotate.speed;
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/moveBodies.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Position, Time, Velocity } from '../trait';
3 |
4 | export const moveBodies = ({ world }: { world: World }) => {
5 | const { delta } = world.get(Time)!;
6 |
7 | world.query(Position, Velocity).updateEach(([position, velocity]) => {
8 | position.x += velocity.x * delta;
9 | position.y += velocity.y * delta;
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/benches/apps/boids/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/benches/apps/n-body/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/benches/apps/revade/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { App } from './app';
4 | import './index.css';
5 | import { WorldProvider } from 'koota/react';
6 | import { world } from './world';
7 |
8 | ReactDOM.createRoot(document.getElementById('root')!).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-spatial-hashing.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { SpatialHashMap, Transform } from '../traits';
3 |
4 | export const updateSpatialHashing = ({ world }: { world: World }) => {
5 | const spatialHashMap = world.get(SpatialHashMap)!;
6 |
7 | world.query(Transform).updateEach(([{ position }], entity) => {
8 | spatialHashMap.setEntity(entity, position.x, position.y, position.z);
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/tick-explosion.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Explosion, Time } from '../traits';
3 |
4 | export const tickExplosion = ({ world }: { world: World }) => {
5 | const { delta } = world.get(Time)!;
6 | world.query(Explosion).updateEach(([explosion], entity) => {
7 | explosion.current += delta * 1000;
8 | if (explosion.current >= explosion.duration) {
9 | entity.destroy();
10 | }
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { App } from './App.tsx';
4 | import './index.css';
5 | import { world } from '@sim/n-body';
6 | import { WorldProvider } from 'koota/react';
7 |
8 | ReactDOM.createRoot(document.getElementById('root')!).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/cleanup-spatial-hash-map.ts:
--------------------------------------------------------------------------------
1 | import { createRemoved, type World } from 'koota';
2 | import { SpatialHashMap, Transform } from '../traits';
3 |
4 | const Removed = createRemoved();
5 |
6 | export const cleanupSpatialHashMap = ({ world }: { world: World }) => {
7 | const spatialHashMap = world.get(SpatialHashMap)!;
8 | world.query(Removed(Transform)).forEach((entity) => {
9 | spatialHashMap.removeEntity(entity);
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/updateGravity.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Time, Velocity } from '../trait';
4 |
5 | export const updateGravity = ({ world }: { world: World }) => {
6 | const { delta } = world.get(Time)!;
7 |
8 | world.query(Velocity).updateEach(([velocity]) => {
9 | // Apply gravity directly to the velocity
10 | velocity.y += CONSTANTS.GRAVITY * delta;
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/getFPS.ts:
--------------------------------------------------------------------------------
1 | let lastTime = 0;
2 | let frameTime = 0;
3 | let frameCount = 0;
4 | let fps = 0;
5 |
6 | export function getFPS(ref?: { current: number }) {
7 | const now = performance.now();
8 | frameTime += now - lastTime;
9 | lastTime = now;
10 | frameCount++;
11 |
12 | if (frameCount % 10 === 0) {
13 | fps = 1000 / (frameTime / 10);
14 | frameTime = 0;
15 | }
16 |
17 | if (ref) ref.current = fps;
18 |
19 | return fps;
20 | }
21 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/getThreadCount.ts:
--------------------------------------------------------------------------------
1 | import { getGlobal } from './getGlobal.js';
2 |
3 | const global = getGlobal();
4 |
5 | const isNode =
6 | typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
7 | const os = isNode ? await import('node:os') : undefined;
8 |
9 | const threadCount = isNode && os ? os.cpus().length : global.navigator.hardwareConcurrency;
10 |
11 | export const getThreadCount = () => threadCount;
12 |
--------------------------------------------------------------------------------
/benches/sims/boids/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sim/boids",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "Boids simulation using Koota",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "dependencies": {
9 | "@sim/bench-tools": "workspace:*",
10 | "koota": "workspace:*",
11 | "directed": "catalog:"
12 | },
13 | "devDependencies": {
14 | "@config/oxlint": "workspace:*",
15 | "@config/typescript": "workspace:*"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/benches/sims/n-body/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sim/n-body",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "N-Body simulation using Koota",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "dependencies": {
9 | "@sim/bench-tools": "workspace:*",
10 | "koota": "workspace:*",
11 | "directed": "catalog:"
12 | },
13 | "devDependencies": {
14 | "@config/oxlint": "workspace:*",
15 | "@config/typescript": "workspace:*"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/main.ts:
--------------------------------------------------------------------------------
1 | import { measure, requestAnimationFrame } from '@sim/bench-tools';
2 | import { init } from './systems/init';
3 | import { schedule } from './systems/schedule';
4 | import { world } from './world';
5 |
6 | // Build the graph once.
7 | init({ world });
8 |
9 | // Start the simulation.
10 | const main = () => {
11 | measure(() => schedule.run({ world }));
12 | requestAnimationFrame(main);
13 | };
14 |
15 | requestAnimationFrame(main);
16 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/utils/colorFromSpeed.ts:
--------------------------------------------------------------------------------
1 | import { CONSTANTS } from '../constants';
2 |
3 | export function colorFromSpeed(speed: number) {
4 | let f = (speed / 8) * Math.sqrt(CONSTANTS.SPEED);
5 | f = Math.min(f, 1.0);
6 |
7 | const fRed = Math.max(0, f - 0.2) / 0.8;
8 | const fGreen = Math.max(0, f - 0.7) / 0.3;
9 |
10 | return {
11 | r: Math.floor(fRed * 255),
12 | g: Math.floor(fGreen * 255),
13 | b: Math.floor(f * 155 + 100),
14 | a: 255,
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/scripts/constants/colors.ts:
--------------------------------------------------------------------------------
1 | export const COLORS = {
2 | reset: '\x1b[0m',
3 | bright: '\x1b[1m',
4 | dim: '\x1b[2m',
5 | underscore: '\x1b[4m',
6 | blink: '\x1b[5m',
7 | reverse: '\x1b[7m',
8 | hidden: '\x1b[8m',
9 |
10 | fg: {
11 | black: '\x1b[30m',
12 | red: '\x1b[31m',
13 | green: '\x1b[32m',
14 | yellow: '\x1b[33m',
15 | blue: '\x1b[34m',
16 | magenta: '\x1b[35m',
17 | cyan: '\x1b[36m',
18 | white: '\x1b[37m',
19 | crimson: '\x1b[38m',
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-time.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Time } from '../traits/time';
3 |
4 | export const updateTime = ({ world }: { world: World }) => {
5 | const time = world.get(Time)!;
6 |
7 | if (time.last === 0) time.last = performance.now();
8 |
9 | const now = performance.now();
10 | const delta = now - time.last;
11 |
12 | time.delta = Math.min(delta / 1000, 1 / 30);
13 | time.last = now;
14 |
15 | world.set(Time, time);
16 | };
17 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/update-time.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Time } from '../traits/time';
3 |
4 | export const updateTime = ({ world }: { world: World }) => {
5 | const time = world.get(Time)!;
6 |
7 | if (time.last === 0) time.last = performance.now();
8 |
9 | const now = performance.now();
10 | const delta = now - time.last;
11 |
12 | time.delta = Math.min(delta / 1000, 1 / 30);
13 | time.last = now;
14 |
15 | world.set(Time, time);
16 | };
17 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/systems/traverse.ts:
--------------------------------------------------------------------------------
1 | import type { Entity, World } from 'koota';
2 | import { ChildOf } from '../traits';
3 | import { root } from './init';
4 |
5 | function traverseFromNode(world: World, node: Entity) {
6 | const children = world.query(ChildOf(node));
7 |
8 | for (const child of children) {
9 | traverseFromNode(world, child);
10 | }
11 | }
12 |
13 | export function traverse({ world }: { world: World }) {
14 | traverseFromNode(world, root);
15 | }
16 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/updateTime.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Time } from '../traits/Time';
3 |
4 | export const updateTime = ({ world }: { world: World }) => {
5 | const time = world.get(Time)!;
6 |
7 | if (time.last === 0) time.last = performance.now();
8 |
9 | const now = performance.now();
10 | const delta = now - time.last;
11 |
12 | time.delta = Math.min(delta / 1000, 1 / 30);
13 | time.last = now;
14 |
15 | world.set(Time, time);
16 | };
17 |
--------------------------------------------------------------------------------
/packages/core/src/storage/stores.ts:
--------------------------------------------------------------------------------
1 | import type { Schema } from './types';
2 | import type { Store } from './types';
3 |
4 | export function createStore(schema: T): Store;
5 | export function createStore(schema: Schema): unknown {
6 | if (typeof schema === 'function') {
7 | return [];
8 | } else {
9 | const store: Record = {};
10 |
11 | for (const key in schema) {
12 | store[key] = [];
13 | }
14 |
15 | return store;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/updateTime.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Time } from '../trait/Time';
3 |
4 | export const updateTime = ({ world }: { world: World }) => {
5 | const time = world.get(Time)!;
6 |
7 | if (time.last === 0) time.last = performance.now();
8 |
9 | const now = performance.now();
10 | const delta = now - time.last;
11 |
12 | time.delta = Math.min(delta / 1000, 1 / 30);
13 | time.last = now;
14 |
15 | world.set(Time, time);
16 | };
17 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@koota/core",
3 | "version": "0.0.1",
4 | "description": "Core vanilla library for Koota",
5 | "private": true,
6 | "license": "ISC",
7 | "type": "module",
8 | "main": "./src/index.ts",
9 | "types": "./src/index.ts",
10 | "scripts": {
11 | "test": "vitest",
12 | "lint": "oxlint"
13 | },
14 | "devDependencies": {
15 | "@config/oxlint": "workspace:*",
16 | "@config/typescript": "workspace:*",
17 | "vitest": "catalog:"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.config/oxlint/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/oxlint/configuration_schema.json",
3 | "plugins": ["unicorn", "typescript", "oxc"],
4 | "rules": {
5 | "no-unused-vars": [
6 | "warn",
7 | {
8 | "argsIgnorePattern": "^_",
9 | "varsIgnorePattern": "^_",
10 | "caughtErrorsIgnorePattern": "^_"
11 | }
12 | ],
13 | "no-useless-escape": "off",
14 | "no-unused-expressions": ["warn", { "allowShortCircuit": true }]
15 | },
16 | "ignorePatterns": ["dist"]
17 | }
18 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/main.ts:
--------------------------------------------------------------------------------
1 | // Based on Sander Mertens' ECS N-Body simulation for Flecs
2 | // https://github.com/SanderMertens/ecs_nbody
3 |
4 | import { measure, requestAnimationFrame } from '@sim/bench-tools';
5 | import { schedule } from './systems/schedule';
6 | import { world } from './world';
7 |
8 | // Start the simulation.
9 | const main = async () => {
10 | await measure(() => schedule.run({ world }));
11 | requestAnimationFrame(main);
12 | };
13 |
14 | requestAnimationFrame(main);
15 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/use-raf.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export function useRaf(callback: () => void | Promise, deps: readonly unknown[] = []) {
4 | const rafRef = useRef(0);
5 |
6 | useEffect(() => {
7 | const loop = async () => {
8 | await callback();
9 | rafRef.current = requestAnimationFrame(loop);
10 | };
11 | loop();
12 |
13 | return () => {
14 | cancelAnimationFrame(rafRef.current);
15 | };
16 | }, [callback, ...deps]);
17 | }
18 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/traits/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auto-rotate';
2 | export * from './avoidance';
3 | export * from './bullet';
4 | export * from './explosion';
5 | export * from './input';
6 | export * from './is-enemy';
7 | export * from './is-player';
8 | export * from './is-shield-visible';
9 | export * from './movement';
10 | export * from './shield-visibility';
11 | export * from './spatial-hash-map';
12 | export * from './time';
13 | export * from './transform';
14 | export * from './relations';
15 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sim/add-remove",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "Particle simulator to stress test adding and removing entities",
6 | "type": "module",
7 | "main": "src/index.ts",
8 | "dependencies": {
9 | "@sim/bench-tools": "workspace:*",
10 | "koota": "workspace:*",
11 | "directed": "catalog:"
12 | },
13 | "devDependencies": {
14 | "@config/oxlint": "workspace:*",
15 | "@config/typescript": "workspace:*"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sim/graph-traversal",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "Graph traversal simulation using Koota relations",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "dependencies": {
9 | "@sim/bench-tools": "workspace:*",
10 | "koota": "workspace:*",
11 | "directed": "catalog:"
12 | },
13 | "devDependencies": {
14 | "@config/oxlint": "workspace:*",
15 | "@config/typescript": "workspace:*"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/moveBodies.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Position, Time, Velocity } from '../traits';
4 |
5 | export const moveBodies = ({ world }: { world: World }) => {
6 | const { delta } = world.get(Time)!;
7 |
8 | world.query(Position, Velocity).updateEach(([position, velocity]) => {
9 | position.x += CONSTANTS.SPEED * velocity.x * delta;
10 | position.y += CONSTANTS.SPEED * velocity.y * delta;
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/packages/core/src/universe/universe.ts:
--------------------------------------------------------------------------------
1 | import type { Query } from '../query/types';
2 | import type { World } from '../world';
3 | import { createWorldIndex } from '../world/utils/world-index';
4 |
5 | export const universe = {
6 | worlds: [] as (World | null)[],
7 | cachedQueries: new Map>(),
8 | worldIndex: createWorldIndex(),
9 | reset: () => {
10 | universe.worlds = [];
11 | universe.cachedQueries = new Map();
12 | universe.worldIndex = createWorldIndex();
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/packages/core/src/actions/types.ts:
--------------------------------------------------------------------------------
1 | import type { World } from '../world';
2 |
3 | export type ActionRecord = Record void>;
4 | export type ActionsInitializer = (world: World) => T;
5 | export type Actions = {
6 | /** Public read-only ID for fast array lookups */
7 | readonly id: number;
8 | /** Initializer function */
9 | readonly initializer: ActionsInitializer;
10 | } & ((world: World) => T);
11 |
12 | export type ActionInstance = ActionRecord;
13 |
--------------------------------------------------------------------------------
/packages/core/src/world/utils/increment-world-bit-flag.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import type { World } from '../types';
3 |
4 | // These should be Float32Arrays since we are using bitwise operations.
5 | // They are native Arrays to avoid overlow issues due to recycling.
6 | export /* @inline */ function incrementWorldBitflag(world: World) {
7 | const ctx = world[$internal];
8 |
9 | ctx.bitflag *= 2;
10 |
11 | if (ctx.bitflag >= 2 ** 31) {
12 | ctx.bitflag = 1;
13 | ctx.entityMasks.push([]);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.config/typescript/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
5 | "module": "ESNext",
6 | "target": "ESNext",
7 | "esModuleInterop": true,
8 | "allowJs": true,
9 | "moduleResolution": "bundler",
10 | "declaration": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "isolatedModules": true,
13 | "strict": true,
14 | "resolveJsonModule": true,
15 | "useDefineForClassFields": true
16 | },
17 | "exclude": ["dist"]
18 | }
19 |
--------------------------------------------------------------------------------
/benches/apps/n-body/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@app/n-body",
3 | "private": true,
4 | "version": "0.0.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@app/bench-tools": "workspace:*",
13 | "@sim/n-body": "workspace:*",
14 | "koota": "workspace:*",
15 | "three": "catalog:"
16 | },
17 | "devDependencies": {
18 | "@config/typescript": "workspace:*",
19 | "@types/three": "catalog:",
20 | "vite": "catalog:"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/index.ts:
--------------------------------------------------------------------------------
1 | export { CONSTANTS } from './constants';
2 | export { init } from './systems/init';
3 | export { moveBodies } from './systems/moveBodies';
4 | export { schedule } from './systems/schedule';
5 | export { setInitial } from './systems/setInitial';
6 | export { updateColor } from './systems/updateColor';
7 | export { updateGravity } from './systems/updateGravity';
8 | export { updateTime } from './systems/updateTime';
9 | export * from './traits';
10 | export { randInRange } from './utils/randInRange';
11 | export { world } from './world';
12 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/updateColor.ts:
--------------------------------------------------------------------------------
1 | import { Not, type World } from 'koota';
2 | import { Color, Repulse, Velocity } from '../traits';
3 | import { colorFromSpeed } from '../utils/colorFromSpeed';
4 |
5 | export const updateColor = ({ world }: { world: World }) => {
6 | world.query(Velocity, Color, Not(Repulse)).updateEach(([velocity, color]) => {
7 | const speed = Math.hypot(velocity.x, velocity.y);
8 | const { r, g, b, a } = colorFromSpeed(speed);
9 |
10 | color.r = r;
11 | color.g = g;
12 | color.b = b;
13 | color.a = a;
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/packages/publish/scripts/copy-readme.ts:
--------------------------------------------------------------------------------
1 | import { copyFile } from 'node:fs/promises';
2 | import { join } from 'node:path';
3 |
4 | const sourceFile = join('..', '..', 'README.md');
5 | const destinationFile = 'README.md';
6 |
7 | async function copyReadme() {
8 | try {
9 | console.log('\n> Copying README.md...');
10 | await copyFile(sourceFile, destinationFile);
11 | console.log('✓ README.md copied successfully\n');
12 | } catch (error) {
13 | console.error('\n> Error copying README.md:', error);
14 | process.exit(1);
15 | }
16 | }
17 |
18 | copyReadme();
19 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 | - benches/sims/*
4 | - benches/apps/*
5 | - .config/*
6 |
7 | catalog:
8 | '@react-three/drei': ^10.7.7
9 | '@react-three/fiber': ^9.4.0
10 | '@types/react': ^19.2.6
11 | '@types/react-dom': ^19.2.3
12 | '@types/three': ^0.181.0
13 | directed: ^0.1.6
14 | react: ^19.2.0
15 | react-dom: ^19.2.0
16 | three: ^0.181.2
17 | unplugin-inline-functions: ^0.3.10
18 | vite: ^7.2.4
19 | vitest: ^4.0.13
20 |
21 | manage-package-manager-versions: true
22 |
23 | onlyBuiltDependencies:
24 | - '@swc/core'
25 | - esbuild
26 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/main.ts:
--------------------------------------------------------------------------------
1 | // Based on Sander Mertens' ECS N-Body simulation for Flecs
2 | // https://github.com/SanderMertens/ecs_nbody
3 |
4 | import { measure, requestAnimationFrame } from '@sim/bench-tools';
5 | import { init } from './systems/init';
6 | import { schedule } from './systems/schedule';
7 | import { world } from './world';
8 |
9 | // Start the simulation.
10 | const main = () => {
11 | measure(() => schedule.run({ world }));
12 | requestAnimationFrame(main);
13 | };
14 |
15 | // Initialize all entities.
16 | init({ world });
17 |
18 | requestAnimationFrame(main);
19 |
--------------------------------------------------------------------------------
/packages/react/src/index.ts:
--------------------------------------------------------------------------------
1 | export { useActions } from './hooks/use-actions';
2 | export { useQuery } from './hooks/use-query';
3 | export { useQueryFirst } from './hooks/use-query-first';
4 | export { useTag } from './hooks/use-tag';
5 | export { useHas } from './hooks/use-has';
6 | export { useTarget } from './hooks/use-target';
7 | export { useTargets } from './hooks/use-targets';
8 | export { useTrait } from './hooks/use-trait';
9 | export { useTraitEffect } from './hooks/use-trait-effect';
10 | export { useWorld } from './world/use-world';
11 | export { WorldProvider } from './world/world-provider';
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # turbo
33 | .turbo
34 |
35 | # dist
36 | dist
37 |
38 | # react
39 | packages/publish/react/
40 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/init.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Acceleration, Circle, Color, IsCentralMass, Mass, Position, Velocity } from '../traits';
4 |
5 | let inited = false;
6 |
7 | export const init = ({ world }: { world: World }) => {
8 | if (inited) return;
9 |
10 | for (let i = 0; i < CONSTANTS.NBODIES; i++) {
11 | const entity = world.spawn(Position, Velocity, Mass, Circle, Color, Acceleration);
12 |
13 | // Make the first entity the central mass.
14 | if (i === 0) entity.add(IsCentralMass);
15 | }
16 |
17 | inited = true;
18 | };
19 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@app/add-remove",
3 | "private": true,
4 | "version": "0.0.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "koota": "workspace:*",
13 | "@app/bench-tools": "workspace:*",
14 | "@sim/add-remove": "workspace:*",
15 | "three": "catalog:",
16 | "directed": "catalog:"
17 | },
18 | "devDependencies": {
19 | "@config/oxlint": "workspace:*",
20 | "@config/typescript": "workspace:*",
21 | "@types/three": "catalog:",
22 | "vite": "catalog:"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/benches/apps/boids/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@app/boids",
3 | "private": true,
4 | "version": "0.0.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@app/bench-tools": "workspace:*",
13 | "@sim/boids": "workspace:*",
14 | "koota": "workspace:*",
15 | "three": "catalog:"
16 | },
17 | "devDependencies": {
18 | "@config/typescript": "workspace:*",
19 | "@types/three": "catalog:",
20 | "@vitejs/plugin-react": "^5.1.1",
21 | "@vitejs/plugin-react-swc": "^4.2.2",
22 | "vite": "catalog:"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/main.ts:
--------------------------------------------------------------------------------
1 | import { measure, requestAnimationFrame } from '@sim/bench-tools';
2 | import { actions } from './actions';
3 | import { CONFIG } from './config';
4 | import { schedule } from './systems/schedule';
5 | import { world } from './world';
6 |
7 | // Start the simulation.
8 | const main = async () => {
9 | const { initialCount } = CONFIG;
10 | const { spawnBoid } = actions(world);
11 |
12 | // Spawn the initial boids.
13 | for (let i = 0; i < initialCount; i++) {
14 | spawnBoid();
15 | }
16 |
17 | await measure(() => schedule.run({ world }));
18 | requestAnimationFrame(main);
19 | };
20 |
21 | requestAnimationFrame(main);
22 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/spawn-enemies.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { actions } from '../actions';
3 | import { IsPlayer } from '../traits';
4 | import { Time } from '../traits/time';
5 |
6 | const SPAWN_INTERVAL = 1;
7 | let accumulatedTime = 0;
8 |
9 | export const spawnEnemies = ({ world }: { world: World }) => {
10 | const { delta } = world.get(Time)!;
11 | const { spawnEnemy } = actions(world);
12 | const player = world.queryFirst(IsPlayer);
13 |
14 | accumulatedTime += delta;
15 |
16 | if (accumulatedTime >= SPAWN_INTERVAL) {
17 | accumulatedTime -= SPAWN_INTERVAL;
18 | spawnEnemy({ target: player });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/constants.ts:
--------------------------------------------------------------------------------
1 | const NBODIES = 2000; // Number of entities
2 | const BASE_MASS = 0.1; // Base mass
3 | const VAR_MASS = 0.8; // Amount of randomness added to mass
4 | const INITIAL_C = 12000; // Mass used in calculation of orbital speed
5 | const MAX_RADIUS = 70; // Circle radius. Will be multiplied by ZOOM
6 | const SPEED = 10000; // Speed of simulation
7 | const STICKY = 10000; // Reduce acceleration in close encounters
8 | const CENTRAL_MASS = 12000; // Mass of the central mass
9 |
10 | export const CONSTANTS = {
11 | NBODIES,
12 | BASE_MASS,
13 | VAR_MASS,
14 | INITIAL_C,
15 | MAX_RADIUS,
16 | SPEED,
17 | STICKY,
18 | CENTRAL_MASS,
19 | };
20 |
--------------------------------------------------------------------------------
/.config/prettier/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "printWidth": 102,
4 | "tabWidth": 4,
5 | "useTabs": true,
6 | "semi": true,
7 | "singleQuote": true,
8 | "trailingComma": "es5",
9 | "bracketSpacing": true,
10 | "jsxBracketSameLine": false,
11 | "arrowParens": "always",
12 | "proseWrap": "preserve",
13 | "overrides": [
14 | {
15 | "files": "**/*.md",
16 | "options": {
17 | "useTabs": false,
18 | "tabWidth": 2,
19 | "semi": false
20 | }
21 | },
22 | {
23 | "files": ["**/*.yml", "**/*.yaml"],
24 | "options": {
25 | "useTabs": false,
26 | "tabWidth": 2,
27 | "semi": false
28 | }
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/packages/core/src/utils/shallow-equal.ts:
--------------------------------------------------------------------------------
1 | // This shallow equal looks insane because it is optimized to use short circuiting
2 | // and the least amount of evaluations possible.
3 |
4 | export function /* @inline @pure */ shallowEqual(obj1: any, obj2: any): boolean {
5 | return (
6 | obj1 === obj2 ||
7 | (typeof obj1 === 'object' &&
8 | obj1 !== null &&
9 | typeof obj2 === 'object' &&
10 | obj2 !== null &&
11 | (() => {
12 | const keys1 = Object.keys(obj1);
13 | const keys2 = Object.keys(obj2);
14 | return (
15 | keys1.length === keys2.length &&
16 | keys1.every((key) => Object.hasOwn(obj2, key) && obj1[key] === obj2[key])
17 | );
18 | })())
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/apply-input.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import * as THREE from 'three';
3 | import { Input, IsPlayer, Movement, Time, Transform } from '../traits';
4 |
5 | const UP = new THREE.Vector3(0, 1, 0);
6 | const tmpvec3 = new THREE.Vector3();
7 |
8 | export const applyInput = ({ world }: { world: World }) => {
9 | const { delta } = world.get(Time)!;
10 | world
11 | .query(IsPlayer, Input, Transform, Movement)
12 | .updateEach(([input, transform, { velocity, thrust }]) => {
13 | velocity.add(tmpvec3.set(input.x, input.y, 0).multiplyScalar(thrust * delta * 100));
14 | transform.quaternion.setFromUnitVectors(UP, velocity.clone().normalize());
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/init.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Circle } from '../trait/Circle';
4 | import { Color } from '../trait/Color';
5 | import { Mass } from '../trait/Mass';
6 | import { Position } from '../trait/Position';
7 | import { Velocity } from '../trait/Velocity';
8 |
9 | let first = false;
10 |
11 | export const init = ({ world }: { world: World }) => {
12 | if (first) return;
13 |
14 | for (let i = 0; i < CONSTANTS.BODIES; i++) {
15 | addBody(world);
16 | }
17 |
18 | first = true;
19 | };
20 |
21 | export const addBody = (world: World) => {
22 | world.spawn(Position, Velocity, Mass, Circle, Color);
23 | };
24 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-bullet.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import * as THREE from 'three';
3 | import { Bullet, Time, Transform } from '../traits';
4 |
5 | const tmpVec3 = new THREE.Vector3();
6 |
7 | export const updateBullets = ({ world }: { world: World }) => {
8 | const { delta } = world.get(Time)!;
9 |
10 | world.query(Bullet, Transform).updateEach(([bullet, transform], entity) => {
11 | // Update bullet position
12 | transform.position.add(tmpVec3.copy(bullet.direction).multiplyScalar(bullet.speed * delta));
13 |
14 | // Update lifetime
15 | bullet.timeAlive += delta;
16 | if (bullet.timeAlive >= bullet.lifetime) {
17 | entity.destroy();
18 | return;
19 | }
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/packages/core/src/utils/deque.ts:
--------------------------------------------------------------------------------
1 | export class Deque {
2 | #removed: T[] = [];
3 | #removedOut: T[] = [];
4 |
5 | dequeue(): T {
6 | if (this.#removedOut.length === 0) {
7 | while (this.#removed.length > 0) {
8 | this.#removedOut.push(this.#removed.pop()!);
9 | }
10 | }
11 |
12 | if (this.#removedOut.length === 0) {
13 | throw new Error('Queue is empty');
14 | }
15 |
16 | return this.#removedOut.pop()!;
17 | }
18 |
19 | enqueue(...items: T[]): void {
20 | this.#removed.push(...items);
21 | }
22 |
23 | get length(): number {
24 | return this.#removed.length + this.#removedOut.length;
25 | }
26 |
27 | clear(): void {
28 | this.#removed.length = 0;
29 | this.#removedOut.length = 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/core/src/query/modifier.ts:
--------------------------------------------------------------------------------
1 | import { Brand } from '../common';
2 | import { Trait } from '../trait/types';
3 | import { Modifier, QueryParameter } from './types';
4 |
5 | export const $modifier = Symbol('modifier');
6 |
7 | export function createModifier(
8 | type: TType,
9 | id: number,
10 | traits: TTrait
11 | ): Modifier {
12 | return {
13 | [$modifier]: true,
14 | type,
15 | id,
16 | traits,
17 | traitIds: traits.map((trait) => trait.id),
18 | } as const;
19 | }
20 |
21 | export /* @inline @pure */ function isModifier(param: QueryParameter): param is Modifier {
22 | return (param as Brand | null | undefined)?.[$modifier] as unknown as boolean;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/core/src/relation/utils/is-relation.ts:
--------------------------------------------------------------------------------
1 | import { Brand } from '../../common';
2 | import { Trait } from '../../trait/types';
3 | import { Relation, RelationPair } from '../types';
4 | import { $relation, $relationPair } from '../symbols';
5 |
6 | /**
7 | * Check if a value is a Relation
8 | */
9 | export /* @inline @pure */ function isRelation(value: unknown): value is Relation {
10 | return (value as Brand | null | undefined)?.[$relation] as unknown as boolean;
11 | }
12 |
13 | /**
14 | * Check if a value is a RelationPair
15 | */
16 | export /* @inline @pure */ function isRelationPair(value: unknown): value is RelationPair {
17 | return (value as Brand | null | undefined)?.[
18 | $relationPair
19 | ] as unknown as boolean;
20 | }
21 |
--------------------------------------------------------------------------------
/benches/sims/graph-traversal/src/systems/init.ts:
--------------------------------------------------------------------------------
1 | import type { Entity, World } from 'koota';
2 | import { CONFIG } from '../config';
3 | import { ChildOf } from '../traits';
4 |
5 | export let root: Entity;
6 |
7 | function buildGraph(world: World, parent: Entity, currentDepth: number) {
8 | if (currentDepth >= CONFIG.depth) return;
9 |
10 | for (let i = 0; i < CONFIG.childrenPerNode; i++) {
11 | const child = world.spawn(ChildOf(parent));
12 | buildGraph(world, child, currentDepth + 1);
13 | }
14 | }
15 |
16 | export function init({ world }: { world: World }) {
17 | root = world.spawn();
18 | buildGraph(world, root, 0);
19 |
20 | console.log(
21 | `Graph constructed: ${world.entities.length} nodes (depth: ${CONFIG.depth}, children per node: ${CONFIG.childrenPerNode})`
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2024-2025 Poimandres
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/systems/cleanupRepulsors.ts:
--------------------------------------------------------------------------------
1 | import { Position, Repulse } from '@sim/n-body';
2 | import { createRemoved, type World } from 'koota';
3 | import * as THREE from 'three';
4 | import { InstancedMesh } from '../traits/InstancedMesh';
5 |
6 | const Removed = createRemoved();
7 | const zeroScaleMatrix = new THREE.Matrix4().makeScale(0, 0, 0);
8 |
9 | export function cleanupBodies({ world }: { world: World }) {
10 | const instanceEntity = world.queryFirst(InstancedMesh);
11 | if (instanceEntity === undefined) return;
12 |
13 | const instancedMesh = instanceEntity.get(InstancedMesh)!;
14 |
15 | world.query(Removed(Repulse, Position)).forEach((e) => {
16 | instancedMesh.setMatrixAt(e.id(), zeroScaleMatrix);
17 | });
18 |
19 | instancedMesh.instanceMatrix.needsUpdate = true;
20 | }
21 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-movement.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import * as THREE from 'three';
3 | import { Movement, Time, Transform } from '../traits';
4 |
5 | const tmpvec3 = new THREE.Vector3();
6 |
7 | export const updateMovement = ({ world }: { world: World }) => {
8 | const { delta } = world.get(Time)!;
9 | world.query(Transform, Movement).updateEach(([transform, { velocity, maxSpeed, force }]) => {
10 | // Apply max speed
11 | velocity.clampLength(0, maxSpeed);
12 | velocity.add(force);
13 |
14 | // Damp force
15 | if (force.length() > 0.01) {
16 | force.multiplyScalar(1 - 0.05);
17 | } else {
18 | force.setScalar(0);
19 | }
20 |
21 | // Apply velocity
22 | transform.position.add(tmpvec3.copy(velocity).multiplyScalar(delta));
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/packages/publish/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/systems/cleanupRepulsors.ts:
--------------------------------------------------------------------------------
1 | import { Position, Repulse } from '@sim/n-body';
2 | import { createRemoved, type World } from 'koota';
3 | import * as THREE from 'three';
4 | import { InstancedMesh } from '../traits/InstancedMesh';
5 |
6 | const Removed = createRemoved();
7 | const zeroScaleMatrix = new THREE.Matrix4().makeScale(0, 0, 0);
8 |
9 | export function cleanupBodies({ world }: { world: World }) {
10 | const instanceEntity = world.queryFirst(InstancedMesh);
11 | if (instanceEntity === undefined) return;
12 |
13 | const instancedMesh = instanceEntity.get(InstancedMesh)!.object;
14 |
15 | world.query(Removed(Repulse, Position)).forEach((e) => {
16 | instancedMesh.setMatrixAt(e.id(), zeroScaleMatrix);
17 | });
18 |
19 | instancedMesh.instanceMatrix.needsUpdate = true;
20 | }
21 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/index.ts:
--------------------------------------------------------------------------------
1 | export { CONSTANTS } from './constants';
2 | export { init } from './systems/init';
3 | export { moveBodies } from './systems/moveBodies';
4 | export { recycleBodiesSim as recycleBodies } from './systems/recycleBodies';
5 | export { schedule } from './systems/schedule';
6 | export { setInitial } from './systems/setInitial';
7 |
8 | export { updateGravity } from './systems/updateGravity';
9 | export { updateTime } from './systems/updateTime';
10 | export { Circle } from './trait/Circle';
11 | export { Color } from './trait/Color';
12 | export { DummyComponents } from './trait/Dummy';
13 | export { Mass } from './trait/Mass';
14 | export { Position } from './trait/Position';
15 | export { Velocity } from './trait/Velocity';
16 | export { randInRange } from './utils/randInRange';
17 | export { world } from './world';
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "ISC",
6 | "scripts": {
7 | "app": "tsx ./scripts/app.ts",
8 | "sim": "tsx ./scripts/sim.ts",
9 | "ship": "pnpm -F koota build && pnpm -F koota test run && pnpm -F koota publish",
10 | "prepublish": "pnpm -F koota build && pnpm -F koota generate-tests",
11 | "test": "pnpm -F core test run && pnpm -F react test run",
12 | "test:build": "pnpm prepublish && pnpm -F koota test run",
13 | "lint": "pnpm -r lint"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^24.10.1",
17 | "oxlint": "^1.33.0",
18 | "tsx": "latest",
19 | "typescript": "latest"
20 | },
21 | "engines": {
22 | "node": ">=24.2.0",
23 | "pnpm": ">=10.12.1"
24 | },
25 | "prettier": "./.config/prettier/base.json",
26 | "packageManager": "pnpm@10.24.0"
27 | }
28 |
--------------------------------------------------------------------------------
/packages/core/src/relation/types.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../common';
2 | import type { Entity } from '../entity/types';
3 | import type { Trait } from '../trait/types';
4 | import { $relation, $relationPair } from './symbols';
5 |
6 | export type RelationTarget = Entity | '*';
7 |
8 | /** A pair represents a relation + target combination */
9 | export interface RelationPair {
10 | readonly [$relationPair]: true;
11 | [$internal]: {
12 | relation: Relation;
13 | target: RelationTarget;
14 | params?: Record;
15 | };
16 | }
17 |
18 | export type Relation = {
19 | readonly [$relation]: true;
20 | [$internal]: {
21 | trait: T;
22 | exclusive: boolean;
23 | autoRemoveTarget: boolean;
24 | };
25 | } & ((target: RelationTarget, params?: Record) => RelationPair);
26 |
--------------------------------------------------------------------------------
/benches/apps/revade/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@app/revade",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "oxlint",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@app/bench-tools": "workspace:*",
14 | "@react-three/drei": "catalog:",
15 | "@react-three/fiber": "catalog:",
16 | "directed": "catalog:",
17 | "koota": "workspace:*",
18 | "react": "catalog:",
19 | "react-dom": "catalog:",
20 | "three": "catalog:"
21 | },
22 | "devDependencies": {
23 | "@config/typescript": "workspace:*",
24 | "@types/react": "catalog:",
25 | "@types/react-dom": "catalog:",
26 | "@types/three": "catalog:",
27 | "@vitejs/plugin-react": "^5.1.1",
28 | "@vitejs/plugin-react-swc": "^4.2.2",
29 | "vite": "catalog:"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@app/n-body-react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "oxlint",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@app/bench-tools": "workspace:*",
14 | "@react-three/fiber": "catalog:",
15 | "@sim/n-body": "workspace:*",
16 | "directed": "catalog:",
17 | "koota": "workspace:*",
18 | "react": "catalog:",
19 | "react-dom": "catalog:",
20 | "three": "catalog:"
21 | },
22 | "devDependencies": {
23 | "@config/typescript": "workspace:*",
24 | "@types/react": "catalog:",
25 | "@types/react-dom": "catalog:",
26 | "@types/three": "catalog:",
27 | "@vitejs/plugin-react": "^5.1.1",
28 | "@vitejs/plugin-react-swc": "^4.2.2",
29 | "vite": "catalog:"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/tracking-cursor.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import type { World } from '../../world';
3 |
4 | // Some values are reserved.
5 | // 0 - has
6 | // 1 - not
7 | // 2 - or
8 | let cursor = 3;
9 |
10 | export function createTrackingId() {
11 | return cursor++;
12 | }
13 |
14 | export function getTrackingCursor() {
15 | return cursor;
16 | }
17 |
18 | export function setTrackingMasks(world: World, id: number) {
19 | const ctx = world[$internal];
20 | const snapshot = structuredClone(ctx.entityMasks);
21 | ctx.trackingSnapshots.set(id, snapshot);
22 |
23 | // For dirty and changed masks, make clone of entity masks and set all bits to 0.
24 | ctx.dirtyMasks.set(
25 | id,
26 | snapshot.map((mask) => mask.map(() => 0))
27 | );
28 |
29 | ctx.changedMasks.set(
30 | id,
31 | snapshot.map((mask) => mask.map(() => 0))
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/actions.ts:
--------------------------------------------------------------------------------
1 | import { createActions, TraitRecord } from 'koota';
2 | import { Forces, Position, Velocity } from './traits';
3 | import { between } from './utils/between';
4 | import { randomSphericalDirection } from './utils/random-direction';
5 |
6 | export const actions = createActions((world) => ({
7 | spawnBoid: (
8 | position: TraitRecord = randomSphericalDirection(between(0, 100)),
9 | velocity: TraitRecord = randomSphericalDirection(5)
10 | ) => {
11 | world.spawn(Position(position), Velocity(velocity), Forces);
12 | },
13 | destroyRandomBoid: () => {
14 | const entities = world.query(Position, Velocity);
15 | if (entities.length) entities[Math.floor(Math.random() * entities.length)].destroy();
16 | },
17 | destroyAllBoids: () => {
18 | world.query(Position, Velocity).forEach((entity) => {
19 | entity.destroy();
20 | });
21 | },
22 | }));
23 |
--------------------------------------------------------------------------------
/.github/workflows/pr-checks.yml:
--------------------------------------------------------------------------------
1 | name: PR Checks
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 | push:
7 | branches: [main]
8 | workflow_dispatch:
9 | inputs:
10 | test:
11 | description: 'Run tests'
12 | required: true
13 | default: 'false'
14 |
15 | jobs:
16 | test:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 |
21 | - name: Setup pnpm
22 | uses: pnpm/action-setup@v4
23 |
24 | - name: Setup Node.js
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: '24'
28 | cache: 'pnpm'
29 |
30 | - name: Install dependencies
31 | run: pnpm install --frozen-lockfile
32 |
33 | - name: Run tests
34 | run: pnpm test
35 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/apply-forces.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Forces, Time, Velocity } from '../traits';
3 |
4 | export const applyForces = ({ world }: { world: World }) => {
5 | const { delta } = world.get(Time)!;
6 |
7 | world.query(Forces, Velocity).updateEach(([forces, velocity]) => {
8 | velocity.x += forces.coherence.x * delta;
9 | velocity.y += forces.coherence.y * delta;
10 | velocity.z += forces.coherence.z * delta;
11 |
12 | velocity.x += forces.separation.x * delta;
13 | velocity.y += forces.separation.y * delta;
14 | velocity.z += forces.separation.z * delta;
15 |
16 | velocity.x += forces.alignment.x * delta;
17 | velocity.y += forces.alignment.y * delta;
18 | velocity.z += forces.alignment.z * delta;
19 |
20 | velocity.x += forces.avoidEdges.x * delta;
21 | velocity.y += forces.avoidEdges.y * delta;
22 | velocity.z += forces.avoidEdges.z * delta;
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/packages/core/src/query/modifiers/added.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import { isRelation } from '../../relation/utils/is-relation';
3 | import type { Trait, TraitOrRelation } from '../../trait/types';
4 | import { universe } from '../../universe/universe';
5 | import { createModifier } from '../modifier';
6 | import type { Modifier } from '../types';
7 | import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
8 |
9 | export function createAdded() {
10 | const id = createTrackingId();
11 |
12 | for (const world of universe.worlds) {
13 | if (!world) continue;
14 | setTrackingMasks(world, id);
15 | }
16 |
17 | return (
18 | ...inputs: T
19 | ): Modifier => {
20 | const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
21 | return createModifier(`added-${id}`, id, traits);
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/packages/core/src/query/modifiers/removed.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import { isRelation } from '../../relation/utils/is-relation';
3 | import type { Trait, TraitOrRelation } from '../../trait/types';
4 | import { universe } from '../../universe/universe';
5 | import { createModifier } from '../modifier';
6 | import type { Modifier } from '../types';
7 | import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
8 |
9 | export function createRemoved() {
10 | const id = createTrackingId();
11 |
12 | for (const world of universe.worlds) {
13 | if (!world) continue;
14 | setTrackingMasks(world, id);
15 | }
16 |
17 | return (
18 | ...inputs: T
19 | ): Modifier => {
20 | const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
21 | return createModifier(`removed-${id}`, id, traits);
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/check-query-with-relations.ts:
--------------------------------------------------------------------------------
1 | import type { Entity } from '../../entity/types';
2 | import { hasRelationPair } from '../../relation/relation';
3 | import type { World } from '../../world';
4 | import type { QueryInstance } from '../types';
5 | import { checkQuery } from './check-query';
6 |
7 | /**
8 | * Check if an entity matches a query with relation filters.
9 | * Uses hybrid bitmask strategy: trait bitmasks first (fast), then relation checks.
10 | */
11 | export function checkQueryWithRelations(world: World, query: QueryInstance, entity: Entity): boolean {
12 | // First check trait bitmasks (fast)
13 | if (!checkQuery(world, query, entity)) return false;
14 |
15 | // Then check relation pairs if any
16 | if (query.relationFilters && query.relationFilters.length > 0) {
17 | for (const pair of query.relationFilters) {
18 | if (!hasRelationPair(world, entity, pair)) {
19 | return false;
20 | }
21 | }
22 | }
23 |
24 | return true;
25 | }
26 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/schedule.ts:
--------------------------------------------------------------------------------
1 | import { Schedule } from 'directed';
2 | import type { World } from 'koota';
3 | import { init } from './init';
4 | import { moveBodies } from './moveBodies';
5 | import { recycleBodiesSim } from './recycleBodies';
6 | import { setInitial } from './setInitial';
7 | import { updateGravity } from './updateGravity';
8 | import { updateTime } from './updateTime';
9 |
10 | export const schedule = new Schedule<{ world: World }>();
11 |
12 | schedule.createTag('init');
13 | schedule.createTag('update', { after: 'init' });
14 | schedule.createTag('end', { after: 'update' });
15 |
16 | schedule.add(init, { tag: 'init' });
17 | schedule.add(setInitial, { tag: 'init', after: init });
18 |
19 | schedule.add(updateTime, { tag: 'update' });
20 | schedule.add(updateGravity, { after: updateTime, tag: 'update' });
21 | schedule.add(moveBodies, { after: updateGravity, tag: 'update' });
22 |
23 | schedule.add(recycleBodiesSim, { tag: 'end', after: 'update' });
24 |
25 | schedule.build();
26 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/avoid-edges.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Forces, Position } from '../traits';
3 | import { CONFIG } from '../config';
4 |
5 | export const avoidEdges = ({ world }: { world: World }) => {
6 | const { avoidEdgesFactor, avoidEdgesMaxDistance } = CONFIG;
7 |
8 | world.query(Forces, Position).updateEach(([{ avoidEdges }, position]) => {
9 | const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2);
10 |
11 | if (distance > avoidEdgesMaxDistance) {
12 | // Scale force by how far past the boundary (stronger the further out)
13 | const overshoot = distance - avoidEdgesMaxDistance;
14 | const strength = avoidEdgesFactor * (1 + overshoot * 0.5);
15 |
16 | avoidEdges.x = (-position.x / distance) * strength;
17 | avoidEdges.y = (-position.y / distance) * strength;
18 | avoidEdges.z = (-position.z / distance) * strength;
19 | } else {
20 | avoidEdges.x = 0;
21 | avoidEdges.y = 0;
22 | avoidEdges.z = 0;
23 | }
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/systems/spawnRepulsor.ts:
--------------------------------------------------------------------------------
1 | import { Acceleration, Circle, Color, Mass, Position, Repulse, Velocity, world } from '@sim/n-body';
2 |
3 | let lastSpawnTime = 0;
4 | const spawnInterval = 100; // milliseconds
5 |
6 | export function spawnRepulsor(e: PointerEvent, frustumSize: number) {
7 | const now = performance.now();
8 | if (now - lastSpawnTime < spawnInterval) return;
9 |
10 | lastSpawnTime = now;
11 |
12 | const aspect = window.innerWidth / window.innerHeight;
13 | const viewWidth = frustumSize * aspect;
14 | const viewHeight = frustumSize;
15 |
16 | const ndcX = (e.clientX / window.innerWidth) * 2 - 1;
17 | const ndcY = -(e.clientY / window.innerHeight) * 2 + 1;
18 |
19 | const x = (ndcX * viewWidth) / 2;
20 | const y = (ndcY * viewHeight) / 2;
21 |
22 | world.spawn(
23 | Position({ x, y }),
24 | Circle({ radius: 160 }),
25 | Color({ r: 255, g: 0, b: 0 }),
26 | Repulse({ force: 5, decay: 0.96, delay: 1 }),
27 | Velocity,
28 | Acceleration,
29 | Mass({ value: 200 })
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/core/src/entity/types.ts:
--------------------------------------------------------------------------------
1 | import type { Relation, RelationPair } from '../relation/types';
2 | import type {
3 | ConfigurableTrait,
4 | ExtractSchema,
5 | SetTraitCallback,
6 | Trait,
7 | TraitRecord,
8 | TraitValue,
9 | } from '../trait/types';
10 |
11 | export type Entity = number & {
12 | add: (...traits: ConfigurableTrait[]) => void;
13 | remove: (...traits: (Trait | RelationPair)[]) => void;
14 | has: (trait: Trait | RelationPair) => boolean;
15 | destroy: () => void;
16 | changed: (trait: Trait) => void;
17 | set: (
18 | trait: T,
19 | value: TraitValue> | SetTraitCallback,
20 | flagChanged?: boolean
21 | ) => void;
22 | get: (trait: T) => TraitRecord> | undefined;
23 | targetFor: (relation: Relation) => Entity | undefined;
24 | targetsFor: (relation: Relation) => Entity[];
25 | id: () => number;
26 | generation: () => number;
27 | isAlive: () => boolean;
28 | };
29 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/systems/syncThreeObjects.ts:
--------------------------------------------------------------------------------
1 | import { Position } from '@sim/boids';
2 | import type { World } from 'koota';
3 | import * as THREE from 'three';
4 | import { InstancedMesh } from '../traits/InstancedMesh';
5 |
6 | const dummy = new THREE.Object3D();
7 | const dummyColor = new THREE.Color();
8 |
9 | export const syncThreeObjects = ({ world }: { world: World }) => {
10 | const instanceEnt = world.queryFirst(InstancedMesh);
11 | if (instanceEnt === undefined) return;
12 |
13 | const instancedMesh = instanceEnt.get(InstancedMesh)!.object;
14 |
15 | world.query(Position).updateEach(([position], entity) => {
16 | dummy.position.set(position.x, position.y, 0);
17 | dummy.scale.set(1, 1, 1);
18 |
19 | dummy.updateMatrix();
20 | instancedMesh.setMatrixAt(entity.id(), dummy.matrix);
21 |
22 | dummyColor.setRGB(0.5, 0.5, 0.5);
23 | instancedMesh.setColorAt(entity.id(), dummyColor);
24 | });
25 |
26 | instancedMesh.instanceMatrix.needsUpdate = true;
27 | instancedMesh.instanceColor!.needsUpdate = true;
28 | };
29 |
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@koota/react",
3 | "version": "0.0.1",
4 | "description": "React bindings for Koota",
5 | "private": true,
6 | "license": "ISC",
7 | "type": "module",
8 | "main": "./src/index.ts",
9 | "types": "./src/index.ts",
10 | "exports": {
11 | ".": {
12 | "types": "./src/index.ts",
13 | "default": "./src/index.ts"
14 | }
15 | },
16 | "scripts": {
17 | "test": "vitest --environment=jsdom",
18 | "lint": "oxlint"
19 | },
20 | "dependencies": {
21 | "@koota/core": "workspace:*"
22 | },
23 | "devDependencies": {
24 | "@config/oxlint": "workspace:*",
25 | "@config/typescript": "workspace:*",
26 | "@testing-library/react": "^16.3.0",
27 | "@types/react": "catalog:",
28 | "@types/react-dom": "catalog:",
29 | "jsdom": "^27.2.0",
30 | "react": "catalog:",
31 | "react-dom": "catalog:",
32 | "vitest": "catalog:"
33 | },
34 | "peerDependencies": {
35 | "@types/react": "catalog:",
36 | "@types/react-dom": "catalog:",
37 | "react": "catalog:",
38 | "react-dom": "catalog:"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/publish/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 | import inlineFunctions from 'unplugin-inline-functions/esbuild';
3 |
4 | export default defineConfig({
5 | entry: ['src/index.ts', 'src/react.ts'],
6 | format: ['esm', 'cjs'],
7 | // Force emitting "use strict" for ESM output
8 | // Not all bundlers and frameworks are capable of correctly transforming esm
9 | // to cjs output and koota requires strict mode to be enabled for the code to
10 | // be sound. The "use strict" directive has no ill effect when running in an
11 | // esm environment, while bringing the extra guarantee of ensuring strict mode
12 | // is used in non-conformant environments.
13 | // See https://262.ecma-international.org/5.1/#sec-C for more details.
14 | esbuildOptions: (options, { format }) => {
15 | options.banner =
16 | format === 'esm'
17 | ? {
18 | js: '"use strict";',
19 | }
20 | : undefined;
21 | },
22 | dts: {
23 | resolve: true,
24 | },
25 | clean: true,
26 | esbuildPlugins: [inlineFunctions({ include: ['src/**/*.{js,ts,jsx,tsx}'] })],
27 | });
28 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/recycleBodies.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Circle, Color, Mass, Position, Velocity } from '../trait';
4 | import { addBody } from './init';
5 |
6 | let draining = true;
7 |
8 | export const recycleBodiesSim = ({ world }: { world: World }) => {
9 | const entities = world.query(Position, Circle, Mass, Velocity, Color);
10 |
11 | if (entities.length === 0) draining = false;
12 | if (entities.length > CONSTANTS.BODIES * 0.95) draining = true;
13 |
14 | entities.select(Position).updateEach(([position], entity) => {
15 | if (position.y < CONSTANTS.FLOOR) {
16 | // Remove the entity
17 | entity.destroy();
18 |
19 | if (!CONSTANTS.DRAIN) addBody(world);
20 | }
21 | });
22 |
23 | if (!CONSTANTS.DRAIN) return;
24 |
25 | const target = Math.min(
26 | Math.max(CONSTANTS.BODIES * 0.01, entities.length * 0.5),
27 | CONSTANTS.BODIES - entities.length
28 | );
29 |
30 | if (!draining) {
31 | for (let i = 0; i < target; i++) {
32 | addBody(world);
33 | }
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/move-boids.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Position, Time, Velocity } from '../traits';
3 | import { CONFIG } from '../config';
4 |
5 | export const moveBoids = ({ world }: { world: World }) => {
6 | const { delta } = world.get(Time)!;
7 | const { maxVelocity, minVelocity } = CONFIG;
8 |
9 | world.query(Position, Velocity).updateEach(([position, velocity]) => {
10 | // Clamp velocity magnitude
11 | const speedSq = velocity.x ** 2 + velocity.y ** 2 + velocity.z ** 2;
12 | const speed = Math.sqrt(speedSq);
13 |
14 | if (speed > maxVelocity) {
15 | const scale = maxVelocity / speed;
16 | velocity.x *= scale;
17 | velocity.y *= scale;
18 | velocity.z *= scale;
19 | } else if (speed < minVelocity && speed > 0) {
20 | const scale = minVelocity / speed;
21 | velocity.x *= scale;
22 | velocity.y *= scale;
23 | velocity.z *= scale;
24 | }
25 |
26 | // Add velocity to position
27 | position.x += velocity.x * delta;
28 | position.y += velocity.y * delta;
29 | position.z += velocity.z * delta;
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/follow-player.ts:
--------------------------------------------------------------------------------
1 | import { type World } from 'koota';
2 | import * as THREE from 'three';
3 | import { IsEnemy, Movement, Targeting, Transform } from '../traits';
4 |
5 | const acceleration = new THREE.Vector3();
6 |
7 | export const followPlayer = ({ world }: { world: World }) => {
8 | world.query(IsEnemy, Transform, Movement, Targeting('*')).updateEach(
9 | ([transform, { velocity, thrust, damping }], entity) => {
10 | // Get the target from the Targeting relation
11 | const targets = entity.targetsFor(Targeting);
12 | const target = targets[0];
13 | if (!target || typeof target === 'string' || !target.has(Transform)) return;
14 |
15 | const targetTransform = target.get(Transform)!;
16 |
17 | // Apply damping to current velocity
18 | velocity.multiplyScalar(damping);
19 |
20 | // Calculate and apply acceleration towards target
21 | acceleration
22 | .copy(targetTransform.position)
23 | .sub(transform.position)
24 | .normalize()
25 | .multiplyScalar(thrust);
26 |
27 | velocity.add(acceleration);
28 | }
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/schedule.ts:
--------------------------------------------------------------------------------
1 | import { Schedule } from 'directed';
2 | import { world } from '../world';
3 | import { updateTime } from './update-time';
4 | import { moveBoids } from './move-boids';
5 | import { applyForces } from './apply-forces';
6 | import { avoidEdges } from './avoid-edges';
7 | import { updateNeighbors } from './update-neighbors';
8 | import { updateCoherence } from './update-coherence';
9 | import { updateSeparation } from './update-separation';
10 | import { updateAlignment } from './update-alignment';
11 |
12 | export const schedule = new Schedule<{ world: typeof world }>();
13 |
14 | schedule.createTag('update');
15 |
16 | schedule.add(updateTime, { before: 'update' });
17 |
18 | schedule.add(updateNeighbors, { tag: 'update' });
19 | schedule.add(updateCoherence, { tag: 'update' });
20 | schedule.add(updateSeparation, { tag: 'update' });
21 | schedule.add(updateAlignment, { tag: 'update' });
22 | schedule.add(avoidEdges, { tag: 'update' });
23 | schedule.add(applyForces, { tag: 'update' });
24 | schedule.add(moveBoids, { tag: 'update' });
25 |
26 | schedule.build();
27 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/tick-shield-visibility.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { IsShieldVisible, ShieldVisibility, Time } from '../traits';
3 |
4 | export const tickShieldVisibility = ({ world }: { world: World }) => {
5 | const { delta } = world.get(Time)!;
6 | world.query(ShieldVisibility).updateEach(([shield], entity) => {
7 | shield.current += delta * 1000;
8 |
9 | if (shield.current >= shield.duration) {
10 | entity.remove(ShieldVisibility, IsShieldVisible);
11 | } else {
12 | // // Calculate remaining time percentage
13 | // const remainingPercent = 1 - shield.current / shield.duration;
14 | // // Increase blink frequency as time runs out
15 | // const blinkFrequency = 250 + remainingPercent * 400;
16 |
17 | const blinkFrequency = 250;
18 | // Use sine wave for smooth blinking, faster at end
19 | const shouldBeVisible = Math.sin((shield.current / blinkFrequency) * Math.PI * 2) > 0;
20 |
21 | if (shouldBeVisible) {
22 | entity.add(IsShieldVisible);
23 | } else {
24 | entity.remove(IsShieldVisible);
25 | }
26 | }
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/schedule.ts:
--------------------------------------------------------------------------------
1 | import { Schedule } from 'directed';
2 | import type { World } from 'koota';
3 | import { handleRepulse } from './handleRepulse';
4 | import { init } from './init';
5 | import { moveBodies } from './moveBodies';
6 | import { setInitial } from './setInitial';
7 | import { updateColor } from './updateColor';
8 | import { updateGravity } from './updateGravity';
9 | import { updateTime } from './updateTime';
10 |
11 | export const schedule = new Schedule<{ world: World }>();
12 |
13 | schedule.createTag('init');
14 | schedule.createTag('update', { after: 'init' });
15 | schedule.createTag('end', { after: 'update' });
16 |
17 | schedule.add(init, { tag: 'init', before: 'update' });
18 | schedule.add(setInitial, { tag: 'init', after: init, before: 'update' });
19 |
20 | schedule.add(updateTime, { tag: 'update' });
21 | schedule.add(updateGravity, { after: setInitial, tag: 'update' });
22 | schedule.add(handleRepulse, { after: updateGravity, tag: 'update' });
23 | schedule.add(moveBodies, { after: handleRepulse, tag: 'update' });
24 | schedule.add(updateColor, { after: moveBodies, tag: 'update' });
25 |
26 | schedule.build();
27 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/update-alignment.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Forces, NeighborOf, Velocity } from '../traits';
3 | import { CONFIG } from '../config';
4 |
5 | export const updateAlignment = ({ world }: { world: World }) => {
6 | const { alignmentFactor } = CONFIG;
7 |
8 | world.query(Forces, Velocity, NeighborOf('*')).updateEach(([{ alignment }, velocity], entity) => {
9 | const neighbors = entity.targetsFor(NeighborOf);
10 |
11 | alignment.x = 0;
12 | alignment.y = 0;
13 | alignment.z = 0;
14 |
15 | if (neighbors.length === 0) return;
16 |
17 | for (const neighbor of neighbors) {
18 | const neighborVelocity = neighbor.get(Velocity)!;
19 | alignment.x += neighborVelocity.x;
20 | alignment.y += neighborVelocity.y;
21 | alignment.z += neighborVelocity.z;
22 | }
23 |
24 | // Average neighbor velocity minus own velocity = steering force
25 | alignment.x = (alignment.x / neighbors.length - velocity.x) * alignmentFactor;
26 | alignment.y = (alignment.y / neighbors.length - velocity.y) * alignmentFactor;
27 | alignment.z = (alignment.z / neighbors.length - velocity.z) * alignmentFactor;
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/systems/init.ts:
--------------------------------------------------------------------------------
1 | import { CONFIG } from '@sim/boids';
2 | import type { World } from 'koota';
3 | import * as THREE from 'three';
4 | import { camera, renderer } from '../main';
5 | import { scene } from '../scene';
6 | import { InstancedMesh } from '../traits/InstancedMesh';
7 |
8 | let inited = false;
9 |
10 | const zeroScaleMatrix = new THREE.Matrix4().makeScale(0, 0, 0);
11 |
12 | export function init({ world }: { world: World }) {
13 | if (inited) return;
14 |
15 | // I'm not sure why it matters, but you can't set iniitial radius to 1 or everything is invisible.
16 | const geometry = new THREE.SphereGeometry(1, 12, 12);
17 | const material = new THREE.MeshBasicMaterial({ color: new THREE.Color().setRGB(1, 1, 1) });
18 | const instancedMesh = new THREE.InstancedMesh(geometry, material, CONFIG.initialCount + 200);
19 |
20 | // Set initial scale to zero
21 | for (let i = 0; i < instancedMesh.count; i++) instancedMesh.setMatrixAt(i, zeroScaleMatrix);
22 |
23 | scene.add(instancedMesh);
24 | world.spawn(InstancedMesh({ object: instancedMesh }));
25 |
26 | // Compile Three shaders.
27 | renderer.compile(scene, camera);
28 |
29 | inited = true;
30 | }
31 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-bullet-collisions.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Bullet, Explosion, IsEnemy, SpatialHashMap, Transform } from '../traits';
3 | import { between } from '../utils/between';
4 |
5 | export const updateBulletCollisions = ({ world }: { world: World }) => {
6 | const spatialHashMap = world.get(SpatialHashMap)!;
7 |
8 | world
9 | .query(Bullet, Transform)
10 | .select(Transform)
11 | .updateEach(([{ position }], entity) => {
12 | const nearbyEntities = spatialHashMap.getNearbyEntities(
13 | position.x,
14 | position.y,
15 | position.z,
16 | 2
17 | );
18 |
19 | const hitEnemy = nearbyEntities.find(
20 | (entity) =>
21 | entity.has(IsEnemy) && entity.get(Transform)!.position.distanceTo(position) < 1
22 | );
23 |
24 | if (hitEnemy !== undefined) {
25 | // Spawn explosion in enemy's position.
26 | world.spawn(
27 | Explosion({ count: Math.floor(between(12, 20)) }),
28 | Transform({ position: hitEnemy.get(Transform)!.position.clone() })
29 | );
30 |
31 | // Destroy bullet and enemy.
32 | hitEnemy.destroy();
33 | entity.destroy();
34 | }
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/update-coherence.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Forces, NeighborOf, Position } from '../traits';
3 | import { CONFIG } from '../config';
4 |
5 | export const updateCoherence = ({ world }: { world: World }) => {
6 | const { coherenceFactor } = CONFIG;
7 |
8 | world.query(Forces, Position, NeighborOf('*')).updateEach(([{ coherence }, position], entity) => {
9 | const neighbors = entity.targetsFor(NeighborOf);
10 |
11 | coherence.x = 0;
12 | coherence.y = 0;
13 | coherence.z = 0;
14 |
15 | if (neighbors.length === 0) return;
16 |
17 | for (const neighbor of neighbors) {
18 | const neighborPosition = neighbor.get(Position)!;
19 | coherence.x += neighborPosition.x;
20 | coherence.y += neighborPosition.y;
21 | coherence.z += neighborPosition.z;
22 | }
23 |
24 | coherence.x /= neighbors.length;
25 | coherence.y /= neighbors.length;
26 | coherence.z /= neighbors.length;
27 |
28 | coherence.x -= position.x;
29 | coherence.y -= position.y;
30 | coherence.z -= position.z;
31 |
32 | coherence.x *= coherenceFactor;
33 | coherence.y *= coherenceFactor;
34 | coherence.z *= coherenceFactor;
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/systems/init.ts:
--------------------------------------------------------------------------------
1 | import { CONSTANTS } from '@sim/n-body';
2 | import type { World } from 'koota';
3 | import * as THREE from 'three';
4 | import { camera, renderer } from '../main';
5 | import { scene } from '../scene';
6 | import { InstancedMesh } from '../traits/InstancedMesh';
7 |
8 | let inited = false;
9 |
10 | const zeroScaleMatrix = new THREE.Matrix4().makeScale(0, 0, 0);
11 |
12 | export function init({ world }: { world: World }) {
13 | if (inited) return;
14 |
15 | // I'm not sure why it matters, but you can't set iniitial radius to 1 or everything is invisible.
16 | const geometry = new THREE.CircleGeometry(CONSTANTS.MAX_RADIUS / 1.5, 12);
17 | const material = new THREE.MeshBasicMaterial({ color: new THREE.Color().setRGB(1, 1, 1) });
18 | const instancedMesh = new THREE.InstancedMesh(geometry, material, CONSTANTS.NBODIES + 200);
19 |
20 | // Set initial scale to zero
21 | for (let i = 0; i < instancedMesh.count; i++) instancedMesh.setMatrixAt(i, zeroScaleMatrix);
22 |
23 | scene.add(instancedMesh);
24 | world.spawn(InstancedMesh(instancedMesh));
25 |
26 | // Compile Three shaders.
27 | renderer.compile(scene, camera);
28 |
29 | inited = true;
30 | }
31 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/raf.ts:
--------------------------------------------------------------------------------
1 | const callbacks: ((...args: any[]) => any)[] = [];
2 | const fpsInterval = 1000 / 60;
3 | let time = performance.now();
4 |
5 | function requestAnimationFrameLoop() {
6 | const now = performance.now();
7 | const delta = now - time;
8 | if (delta >= fpsInterval) {
9 | // Adjust next execution time in case this loop took longer to execute
10 | time = now - (delta % fpsInterval);
11 | // Clone array in case callbacks pushes more functions to it
12 | const funcs = callbacks.slice();
13 | callbacks.length = 0;
14 | for (let i = 0; i < funcs.length; i++) {
15 | funcs[i]?.(now, delta);
16 | }
17 | } else {
18 | setImmediate(requestAnimationFrameLoop);
19 | }
20 | }
21 |
22 | export function requestAnimationFrame(func: (...args: any[]) => any) {
23 | if (typeof window !== 'undefined' && window.requestAnimationFrame) {
24 | return window.requestAnimationFrame(func);
25 | } else {
26 | callbacks.push(func);
27 | if (callbacks.length === 1) {
28 | setImmediate(requestAnimationFrameLoop);
29 | }
30 | return callbacks.length - 1;
31 | }
32 | }
33 |
34 | export function cancelAnimationFrame(id: number) {
35 | callbacks[id] = undefined as any;
36 | }
37 |
--------------------------------------------------------------------------------
/packages/core/src/actions/create-actions.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../common';
2 | import type { World } from '../world';
3 | import type { Actions, ActionsInitializer, ActionRecord } from './types';
4 |
5 | let actionsId = 0;
6 |
7 | export function createActions(
8 | initializer: ActionsInitializer
9 | ): Actions {
10 | const id = actionsId++;
11 |
12 | const actions = Object.assign(
13 | (world: World): T => {
14 | const ctx = world[$internal];
15 |
16 | // Try array lookup first (faster)
17 | let instance = ctx.actionInstances[id];
18 |
19 | if (!instance) {
20 | // Create and cache actions instance
21 | instance = initializer(world);
22 |
23 | // Ensure array is large enough
24 | if (id >= ctx.actionInstances.length) {
25 | ctx.actionInstances.length = id + 1;
26 | }
27 | ctx.actionInstances[id] = instance;
28 | }
29 |
30 | return instance as T;
31 | },
32 | {
33 | initializer,
34 | }
35 | ) as Actions;
36 |
37 | // Add public read-only id property
38 | Object.defineProperty(actions, 'id', {
39 | value: id,
40 | writable: false,
41 | enumerable: true,
42 | configurable: false,
43 | });
44 |
45 | return actions;
46 | }
47 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/check-query-tracking-with-relations.ts:
--------------------------------------------------------------------------------
1 | import type { Entity } from '../../entity/types';
2 | import { hasRelationPair } from '../../relation/relation';
3 | import type { World } from '../../world';
4 | import type { EventType, QueryInstance } from '../types';
5 | import { checkQueryTracking } from './check-query-tracking';
6 |
7 | /**
8 | * Check if an entity matches a tracking query with relation filters.
9 | * Combines checkQueryTracking (trait bitmasks + tracking state) with relation checks.
10 | */
11 | export function checkQueryTrackingWithRelations(
12 | world: World,
13 | query: QueryInstance,
14 | entity: Entity,
15 | eventType: EventType,
16 | eventGenerationId: number,
17 | eventBitflag: number
18 | ): boolean {
19 | // First check trait bitmasks and tracking state (fast)
20 | if (!checkQueryTracking(world, query, entity, eventType, eventGenerationId, eventBitflag)) {
21 | return false;
22 | }
23 |
24 | // Then check relation pairs if any
25 | if (query.relationFilters && query.relationFilters.length > 0) {
26 | for (const pair of query.relationFilters) {
27 | if (!hasRelationPair(world, entity, pair)) {
28 | return false;
29 | }
30 | }
31 | }
32 |
33 | return true;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/check-query.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import type { Entity } from '../../entity/types';
3 | import { getEntityId } from '../../entity/utils/pack-entity';
4 | import type { World } from '../../world';
5 | import type { QueryInstance } from '../types';
6 |
7 | /**
8 | * Check if an entity matches a non-tracking query.
9 | * For tracking queries, use checkQueryTracking instead.
10 | */
11 | export function checkQuery(world: World, query: QueryInstance, entity: Entity): boolean {
12 | const { bitmasks, generations } = query;
13 | const ctx = world[$internal];
14 | const eid = getEntityId(entity);
15 |
16 | if (query.traitInstances.all.length === 0) return false;
17 |
18 | for (let i = 0; i < generations.length; i++) {
19 | const generationId = generations[i];
20 | const bitmask = bitmasks[i];
21 | const { required, forbidden, or } = bitmask;
22 | const entityMask = ctx.entityMasks[generationId][eid];
23 |
24 | if (!forbidden && !required && !or) return false;
25 | if ((entityMask & forbidden) !== 0) return false;
26 | if ((entityMask & required) !== required) return false;
27 | if (or !== 0 && (entityMask & or) === 0) return false;
28 | }
29 |
30 | return true;
31 | }
32 |
--------------------------------------------------------------------------------
/scripts/app.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'node:child_process';
2 | import { existsSync, readdirSync } from 'node:fs';
3 | import { join } from 'node:path';
4 |
5 | // Parse the command-line arguments
6 | const args = process.argv.slice(2);
7 |
8 | // Retrieve the suite name and engine type from parsed arguments
9 | const suiteName = args[0];
10 |
11 | // Function to execute the main.ts file within a directory
12 | const runDev = (directoryPath: string) => {
13 | process.chdir(directoryPath);
14 | execSync(`pnpm run dev`, { stdio: 'inherit' });
15 | };
16 |
17 | // Function to find and run main.ts files for the specified suite
18 | const runSuites = async (app: string) => {
19 | const rootPath = process.cwd();
20 | const baseDir = join(rootPath, 'benches', 'apps');
21 | const appDir = join(baseDir, app);
22 |
23 | // Check if the specified suite directory exists
24 | if (existsSync(appDir) && readdirSync(baseDir).includes(app)) {
25 | runDev(appDir);
26 | } else {
27 | console.error(`Suite not found: ${app}`);
28 | }
29 | };
30 |
31 | // Check if a suite name was provided
32 | if (!suiteName) {
33 | console.error('Please provide a suite name as an argument.');
34 | process.exit(1);
35 | }
36 |
37 | // Run the suites
38 | runSuites(suiteName);
39 |
--------------------------------------------------------------------------------
/packages/core/src/storage/schema.ts:
--------------------------------------------------------------------------------
1 | import type { Schema, StoreType } from './types';
2 |
3 | /**
4 | * Get default values from a schema.
5 | * Returns null for tags (empty schemas) or if no defaults exist.
6 | */
7 | /* @inline @pure */ export function getSchemaDefaults(
8 | schema: Record | (() => unknown),
9 | type: StoreType
10 | ): Record | null {
11 | if (type === 'aos') {
12 | return typeof schema === 'function' ? (schema() as Record) : null;
13 | }
14 |
15 | if (!schema || typeof schema === 'function' || Object.keys(schema).length === 0) return null;
16 |
17 | const defaults: Record = {};
18 | for (const key in schema) {
19 | if (typeof schema[key] === 'function') {
20 | defaults[key] = schema[key]();
21 | } else {
22 | defaults[key] = schema[key];
23 | }
24 | }
25 | return defaults;
26 | }
27 |
28 | export /* @inline @pure */ function validateSchema(schema: Schema) {
29 | for (const key in schema) {
30 | const value = schema[key as keyof Schema];
31 | if (value !== null && typeof value === 'object') {
32 | const kind = Array.isArray(value) ? 'array' : 'object';
33 | throw new Error(`Koota: ${key} is an ${kind}, which is not supported in traits.`);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/benches/sims/bench-tools/src/measure.ts:
--------------------------------------------------------------------------------
1 | export type Measurement = {
2 | delta: number;
3 | average: number;
4 | };
5 |
6 | let totalExecutionTime = 0;
7 | let executionCount = 0;
8 |
9 | export async function measure(
10 | fn: (...args: any[]) => any,
11 | measurementRef?: { current: Measurement }
12 | ) {
13 | const startTime = performance.now();
14 |
15 | const result = await fn();
16 |
17 | const endTime = performance.now();
18 | const delta = endTime - startTime;
19 |
20 | totalExecutionTime += delta;
21 | executionCount += 1;
22 | const average = totalExecutionTime / executionCount;
23 |
24 | if (measurementRef) {
25 | measurementRef.current = {
26 | delta,
27 | average,
28 | };
29 | } else {
30 | if (typeof window !== 'undefined') {
31 | // Browser environment: use console.log
32 | console.log(
33 | `Execution time: ${delta.toFixed(3)} ms, Average time: ${average.toFixed(3)} ms`
34 | );
35 | } else if (typeof process !== 'undefined' && process.stdout && process.stdout.write) {
36 | // Node.js environment: use process.stdout.write to update the same line
37 | process.stdout.write(
38 | `\rExecution time: ${delta.toFixed(3)} ms, Average time: ${average.toFixed(3)} ms`
39 | );
40 | }
41 | }
42 |
43 | return result;
44 | }
45 |
--------------------------------------------------------------------------------
/packages/core/src/trait/trait-instance.ts:
--------------------------------------------------------------------------------
1 | import type { Trait, TraitInstance } from './types';
2 |
3 | export type TraitInstanceArray = (TraitInstance | undefined)[];
4 |
5 | /**
6 | * Get TraitInstance by trait ID
7 | */
8 | export /* @inline @pure */ function getTraitInstance(
9 | traitData: TraitInstanceArray,
10 | trait: Trait
11 | ): TraitInstance | undefined {
12 | return traitData[trait.id];
13 | }
14 |
15 | /**
16 | * Set TraitInstance by trait ID
17 | */
18 | export /* @inline */ function setTraitInstance(
19 | traitData: TraitInstanceArray,
20 | trait: Trait,
21 | data: TraitInstance
22 | ): void {
23 | const traitId = trait.id;
24 | // Ensure array is large enough
25 | if (traitId >= traitData.length) {
26 | traitData.length = traitId + 1;
27 | }
28 | traitData[traitId] = data;
29 | }
30 |
31 | /**
32 | * Check if trait is registered
33 | */
34 | export /* @inline @pure */ function hasTraitInstance(
35 | traitData: TraitInstanceArray,
36 | trait: Trait
37 | ): boolean {
38 | const traitId = trait.id;
39 | return traitId < traitData.length && traitData[traitId] !== undefined;
40 | }
41 |
42 | /**
43 | * Clear all trait data
44 | */
45 | export /* @inline */ function clearTraitInstance(traitData: TraitInstanceArray): void {
46 | traitData.length = 0;
47 | }
48 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/update-neighbors.ts:
--------------------------------------------------------------------------------
1 | import { World } from 'koota';
2 | import { NeighborOf, Position } from '../traits';
3 |
4 | const radius = 40;
5 | const maxNeighbors = 20;
6 |
7 | // This is very inefficient and you shouldn't create relations like
8 | // a machine gun but it is good for testing relation performance.
9 | export function updateNeighbors({ world }: { world: World }) {
10 | const entities = world.query(Position);
11 |
12 | // Remove all neighbors
13 | for (const entity of entities) {
14 | entity.remove(NeighborOf('*'));
15 | }
16 |
17 | for (const entityA of entities) {
18 | let neighbors = 0;
19 |
20 | // For each entity, find all entities within a radius
21 | for (const entityB of entities) {
22 | if (entityA.id() === entityB.id()) continue;
23 |
24 | const positionA = entityA.get(Position)!;
25 | const positionB = entityB.get(Position)!;
26 |
27 | const distance = Math.sqrt(
28 | (positionA.x - positionB.x) ** 2 +
29 | (positionA.y - positionB.y) ** 2 +
30 | (positionA.z - positionB.z) ** 2
31 | );
32 |
33 | if (distance < radius) {
34 | entityA.add(NeighborOf(entityB));
35 | // entityB.add(NeighborOf(entityA));
36 |
37 | neighbors++;
38 | if (neighbors >= maxNeighbors) break;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/handle-shooting.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { actions } from '../actions';
3 | import { IsPlayer, Time, Transform } from '../traits';
4 |
5 | let canShoot = true;
6 | const SHOOT_COOLDOWN = 0.15; // seconds
7 | let cooldownTimer = 0;
8 |
9 | // Track spacebar state
10 | const keys = {
11 | space: false,
12 | };
13 |
14 | window.addEventListener('keydown', (e) => {
15 | if (e.code === 'Space') {
16 | keys.space = true;
17 | }
18 | });
19 |
20 | window.addEventListener('keyup', (e) => {
21 | if (e.code === 'Space') {
22 | keys.space = false;
23 | }
24 | });
25 |
26 | export const handleShooting = ({ world }: { world: World }) => {
27 | const { delta } = world.get(Time)!;
28 | const { spawnBullet } = actions(world);
29 |
30 | // Update cooldown
31 | if (!canShoot) {
32 | cooldownTimer += delta;
33 | if (cooldownTimer >= SHOOT_COOLDOWN) {
34 | canShoot = true;
35 | cooldownTimer = 0;
36 | }
37 | }
38 |
39 | // Check for shooting input
40 | if (keys.space && canShoot) {
41 | const player = world.queryFirst(IsPlayer, Transform);
42 | if (player) {
43 | const playerTransform = player.get(Transform)!;
44 | spawnBullet(playerTransform.position, playerTransform.quaternion, player);
45 | canShoot = false;
46 | }
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/update-avoidance.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import * as THREE from 'three';
3 | import { Avoidance, Movement, SpatialHashMap, Transform } from '../traits';
4 |
5 | const acceleration = new THREE.Vector3();
6 |
7 | export const updateAvoidance = ({ world }: { world: World }) => {
8 | const spatialHashMap = world.get(SpatialHashMap)!;
9 |
10 | world
11 | .query(Avoidance, Transform, Movement)
12 | .updateEach(([avoidance, { position }, { velocity }]) => {
13 | let neighbors = spatialHashMap.getNearbyEntities(
14 | position.x,
15 | position.y,
16 | position.z,
17 | avoidance.range,
18 | avoidance.neighbors
19 | );
20 |
21 | // Only avoid other avoidance entities
22 | neighbors = neighbors.filter((neighbor) => {
23 | return (
24 | neighbor.has(Avoidance) &&
25 | neighbor.get(Transform)!.position.distanceTo(position) <= avoidance.range
26 | );
27 | });
28 |
29 | if (neighbors.length) {
30 | acceleration.setScalar(0);
31 |
32 | for (const neighbor of neighbors) {
33 | acceleration.add(neighbor.get(Transform)!.position).sub(position);
34 | }
35 |
36 | acceleration.divideScalar(-neighbors.length).normalize().multiplyScalar(2);
37 | velocity.add(acceleration);
38 | }
39 | });
40 | };
41 |
--------------------------------------------------------------------------------
/benches/apps/n-body/src/systems/syncThreeObjects.ts:
--------------------------------------------------------------------------------
1 | import { Circle, Color, Position } from '@sim/n-body';
2 | import type { World } from 'koota';
3 | import * as THREE from 'three';
4 | import { InstancedMesh } from '../traits/InstancedMesh';
5 |
6 | const normalize = (x: number, min: number, max: number) => (x - min) / (max - min);
7 |
8 | const dummy = new THREE.Object3D();
9 | const dummyColor = new THREE.Color();
10 |
11 | export const syncThreeObjects = ({ world }: { world: World }) => {
12 | const instanceEnt = world.queryFirst(InstancedMesh);
13 | if (instanceEnt === undefined) return;
14 |
15 | const instancedMesh = instanceEnt.get(InstancedMesh)!;
16 |
17 | world.query(Position, Circle, Color).updateEach(([position, circle, color], entity) => {
18 | dummy.position.set(position.x, position.y, 0);
19 |
20 | const radius = normalize(circle.radius, 0, 60);
21 | dummy.scale.set(radius, radius, radius);
22 |
23 | dummy.updateMatrix();
24 | instancedMesh.setMatrixAt(entity.id(), dummy.matrix);
25 |
26 | dummyColor.setRGB(
27 | normalize(color.r, 0, 255),
28 | normalize(color.g, 0, 255),
29 | normalize(color.b, 0, 255)
30 | );
31 | instancedMesh.setColorAt(entity.id(), dummyColor);
32 | });
33 |
34 | instancedMesh.instanceMatrix.needsUpdate = true;
35 | instancedMesh.instanceColor!.needsUpdate = true;
36 | };
37 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/systems/syncThreeObjects.ts:
--------------------------------------------------------------------------------
1 | import { Circle, Color, Position } from '@sim/n-body';
2 | import type { World } from 'koota';
3 | import * as THREE from 'three';
4 | import { InstancedMesh } from '../traits/InstancedMesh';
5 |
6 | const normalize = (x: number, min: number, max: number) => (x - min) / (max - min);
7 |
8 | const dummy = new THREE.Object3D();
9 | const dummyColor = new THREE.Color();
10 |
11 | export const syncThreeObjects = ({ world }: { world: World }) => {
12 | const instanceEnt = world.queryFirst(InstancedMesh);
13 | if (instanceEnt === undefined) return;
14 |
15 | const instancedMesh = instanceEnt.get(InstancedMesh)!.object;
16 |
17 | world.query(Position, Circle, Color).updateEach(([position, circle, color], entity) => {
18 | dummy.position.set(position.x, position.y, 0);
19 |
20 | const radius = normalize(circle.radius, 0, 60);
21 | dummy.scale.set(radius, radius, radius);
22 |
23 | dummy.updateMatrix();
24 | instancedMesh.setMatrixAt(entity.id(), dummy.matrix);
25 |
26 | dummyColor.setRGB(
27 | normalize(color.r, 0, 255),
28 | normalize(color.g, 0, 255),
29 | normalize(color.b, 0, 255)
30 | );
31 | instancedMesh.setColorAt(entity.id(), dummyColor);
32 | });
33 |
34 | instancedMesh.instanceMatrix.needsUpdate = true;
35 | instancedMesh.instanceColor!.needsUpdate = true;
36 | };
37 |
--------------------------------------------------------------------------------
/packages/react/tests/actions.test.tsx:
--------------------------------------------------------------------------------
1 | import { createActions, createWorld, type Entity, trait, universe, type World } from '@koota/core';
2 | import { render } from '@testing-library/react';
3 | import { act, StrictMode } from 'react';
4 | import { beforeEach, describe, expect, it } from 'vitest';
5 | import { useActions, WorldProvider } from '../src';
6 |
7 | declare global {
8 | var IS_REACT_ACT_ENVIRONMENT: boolean;
9 | }
10 |
11 | // Let React know that we'll be testing effectful components
12 | global.IS_REACT_ACT_ENVIRONMENT = true;
13 |
14 | let world: World;
15 | const Position = trait({ x: 0, y: 0 });
16 |
17 | describe('useActions', () => {
18 | beforeEach(() => {
19 | universe.reset();
20 | world = createWorld();
21 | });
22 |
23 | it('returns actions bound to the world in context', async () => {
24 | const actions = createActions((world) => ({
25 | spawnBody: () => world.spawn(Position),
26 | }));
27 |
28 | let spawnedEntity: Entity | undefined;
29 |
30 | function Test() {
31 | const { spawnBody } = useActions(actions);
32 | spawnedEntity = spawnBody();
33 | return null;
34 | }
35 |
36 | await act(async () => {
37 | render(
38 |
39 |
40 |
41 |
42 |
43 | );
44 | });
45 |
46 | expect(spawnedEntity).toBeDefined();
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/actions.ts:
--------------------------------------------------------------------------------
1 | import { createActions, type Entity, type TraitValue } from 'koota';
2 | import * as THREE from 'three';
3 | import {
4 | AutoRotate,
5 | Avoidance,
6 | Bullet,
7 | FiredBy,
8 | Input,
9 | IsEnemy,
10 | IsPlayer,
11 | Movement,
12 | Targeting,
13 | Transform,
14 | } from './traits';
15 |
16 | type TransformValue = TraitValue<(typeof Transform)['schema']>;
17 |
18 | export const actions = createActions((world) => ({
19 | spawnPlayer: (transform?: TransformValue) => {
20 | return world.spawn(IsPlayer, Movement, Input, Transform(transform));
21 | },
22 | spawnEnemy: (options: { transform?: TransformValue; target?: Entity }) => {
23 | const enemy = world.spawn(
24 | IsEnemy,
25 | Movement({ thrust: 0.5, damping: 0.98 }),
26 | Transform(options.transform),
27 | AutoRotate,
28 | Avoidance
29 | );
30 | if (options.target) enemy.add(Targeting(options.target));
31 |
32 | return enemy;
33 | },
34 | spawnBullet: (position: THREE.Vector3, rotation: THREE.Quaternion, firedBy?: Entity) => {
35 | const direction = new THREE.Vector3(0, 1, 0);
36 | direction.applyQuaternion(rotation);
37 |
38 | const bullet = world.spawn(
39 | Transform({ position: position.clone(), quaternion: rotation.clone() }),
40 | Bullet({ direction })
41 | );
42 | if (firedBy) bullet.add(FiredBy(firedBy));
43 |
44 | return bullet;
45 | },
46 | }));
47 |
--------------------------------------------------------------------------------
/packages/core/src/utils/sparse-set.ts:
--------------------------------------------------------------------------------
1 | export class SparseSet {
2 | #dense: number[] = [];
3 | #sparse: number[] = [];
4 | #cursor: number = 0;
5 |
6 | has(val: number): boolean {
7 | const index = this.#sparse[val];
8 | return index < this.#cursor && this.#dense[index] === val;
9 | }
10 |
11 | add(val: number): void {
12 | if (this.has(val)) return;
13 | this.#sparse[val] = this.#cursor;
14 | this.#dense[this.#cursor++] = val;
15 | }
16 |
17 | remove(val: number): void {
18 | if (!this.has(val)) return;
19 | const index = this.#sparse[val];
20 | this.#cursor--;
21 | const swapped = this.#dense[this.#cursor];
22 | if (swapped !== val) {
23 | this.#dense[index] = swapped;
24 | this.#sparse[swapped] = index;
25 | }
26 | }
27 |
28 | clear(): void {
29 | // Clear the sparse array entries for all active values
30 | for (let i = 0; i < this.#cursor; i++) {
31 | this.#sparse[this.#dense[i]] = 0;
32 | }
33 | this.#cursor = 0;
34 | }
35 |
36 | sort(): void {
37 | this.#dense.sort((a, b) => a - b);
38 | for (let i = 0; i < this.#dense.length; i++) {
39 | this.#sparse[this.#dense[i]] = i;
40 | }
41 | }
42 |
43 | getIndex(val: number): number {
44 | return this.#sparse[val];
45 | }
46 |
47 | get dense(): number[] {
48 | return this.#dense.slice(0, this.#cursor);
49 | }
50 |
51 | get sparse(): number[] {
52 | return this.#sparse;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/publish/tests/react/actions.test.tsx:
--------------------------------------------------------------------------------
1 | import { createActions, createWorld, type Entity, trait, universe, type World } from '../../dist';
2 | import { render } from '@testing-library/react';
3 | import { act, StrictMode } from 'react';
4 | import { beforeEach, describe, expect, it } from 'vitest';
5 | import { useActions, WorldProvider } from '../../react';
6 |
7 | declare global {
8 | var IS_REACT_ACT_ENVIRONMENT: boolean;
9 | }
10 |
11 | // Let React know that we'll be testing effectful components
12 | global.IS_REACT_ACT_ENVIRONMENT = true;
13 |
14 | let world: World;
15 | const Position = trait({ x: 0, y: 0 });
16 |
17 | describe('useActions', () => {
18 | beforeEach(() => {
19 | universe.reset();
20 | world = createWorld();
21 | });
22 |
23 | it('returns actions bound to the world in context', async () => {
24 | const actions = createActions((world) => ({
25 | spawnBody: () => world.spawn(Position),
26 | }));
27 |
28 | let spawnedEntity: Entity | undefined;
29 |
30 | function Test() {
31 | const { spawnBody } = useActions(actions);
32 | spawnedEntity = spawnBody();
33 | return null;
34 | }
35 |
36 | await act(async () => {
37 | render(
38 |
39 |
40 |
41 |
42 |
43 | );
44 | });
45 |
46 | expect(spawnedEntity).toBeDefined();
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/benches/sims/boids/src/systems/update-separation.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Forces, NeighborOf, Position } from '../traits';
3 | import { CONFIG } from '../config';
4 |
5 | export const updateSeparation = ({ world }: { world: World }) => {
6 | const { separationFactor } = CONFIG;
7 |
8 | world
9 | .query(Forces, Position, NeighborOf('*'))
10 | .updateEach(([{ separation }, position], entity) => {
11 | const neighbors = entity.targetsFor(NeighborOf);
12 |
13 | separation.x = 0;
14 | separation.y = 0;
15 | separation.z = 0;
16 |
17 | if (neighbors.length === 0) return;
18 |
19 | for (const neighbor of neighbors) {
20 | const neighborPosition = neighbor.get(Position)!;
21 | const dx = position.x - neighborPosition.x;
22 | const dy = position.y - neighborPosition.y;
23 | const dz = position.z - neighborPosition.z;
24 | const distanceSq = dx * dx + dy * dy + dz * dz;
25 |
26 | // Inverse linear law for separation (softer but longer range than inverse square)
27 | separation.x += dx / distanceSq;
28 | separation.y += dy / distanceSq;
29 | separation.z += dz / distanceSq;
30 | }
31 |
32 | separation.x = (separation.x / neighbors.length) * separationFactor;
33 | separation.y = (separation.y / neighbors.length) * separationFactor;
34 | separation.z = (separation.z / neighbors.length) * separationFactor;
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-trait-effect.ts:
--------------------------------------------------------------------------------
1 | import { $internal, type Entity, type Trait, type TraitRecord, type World } from '@koota/core';
2 | import { useEffect, useMemo, useRef } from 'react';
3 | import { isWorld } from '../utils/is-world';
4 | import { useWorld } from '../world/use-world';
5 |
6 | export function useTraitEffect(
7 | target: Entity | World,
8 | trait: T,
9 | callback: (value: TraitRecord | undefined) => void
10 | ) {
11 | const contextWorld = useWorld();
12 | const world = useMemo(() => (isWorld(target) ? target : contextWorld), [target, contextWorld]);
13 | const entity = useMemo(
14 | () => (isWorld(target) ? target[$internal].worldEntity : target),
15 | [target]
16 | );
17 |
18 | // Memoize the callback so it doesn't cause rerenders if an arrow function is used.
19 | const callbackRef = useRef(callback);
20 | callbackRef.current = callback;
21 |
22 | useEffect(() => {
23 | const onChangeUnsub = world.onChange(trait, (e) => {
24 | if (e === entity) callbackRef.current(e.get(trait));
25 | });
26 |
27 | const onAddUnsub = world.onAdd(trait, (e) => {
28 | if (e === entity) callbackRef.current(e.get(trait));
29 | });
30 |
31 | const onRemoveUnsub = world.onRemove(trait, (e) => {
32 | if (e === entity) callbackRef.current(undefined);
33 | });
34 |
35 | callbackRef.current(entity.has(trait) ? entity.get(trait) : undefined);
36 |
37 | return () => {
38 | onChangeUnsub();
39 | onAddUnsub();
40 | onRemoveUnsub();
41 | };
42 | }, [trait, world, entity]);
43 | }
44 |
--------------------------------------------------------------------------------
/benches/sims/add-remove/src/systems/setInitial.ts:
--------------------------------------------------------------------------------
1 | import { createAdded, type World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Circle, Color, DummyComponents, Mass, Position, Velocity } from '../trait';
4 | import { randInRange } from '../utils/randInRange';
5 |
6 | const body = [Position, Velocity, Mass, Circle, Color] as const;
7 | const Added = createAdded();
8 |
9 | export const setInitial = ({ world }: { world: World }) => {
10 | world.query(Added(...body)).updateEach(([position, velocity, mass, circle, color], entity) => {
11 | // Random positions
12 | position.x = randInRange(-400, 400);
13 | position.y = 100;
14 | // Jitter the z position to avoid z-fighting
15 | position.z = randInRange(-50, 50);
16 |
17 | // Shoot the bodies up at random angles and velocities
18 | const angle = randInRange(0, Math.PI * 2);
19 | const speed = randInRange(0, 50);
20 | velocity.x = Math.cos(angle) * speed;
21 | velocity.y = Math.sin(angle) * speed;
22 |
23 | // Add a random number of components to the body
24 | const numComponents = Math.floor(Math.random() * CONSTANTS.MAX_COMPS_PER_ENTITY);
25 | for (let j = 0; j < numComponents; j++) {
26 | entity.add(DummyComponents[Math.floor(Math.random() * DummyComponents.length)]);
27 | }
28 |
29 | // Set mass and radius based on the number of components
30 | mass.value = 1 + numComponents;
31 | circle.radius = mass.value;
32 |
33 | // Random colors
34 | color.r = randInRange(0, 255);
35 | color.g = randInRange(0, 255);
36 | color.b = randInRange(0, 255);
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/packages/core/tests/actions.test.ts:
--------------------------------------------------------------------------------
1 | import { beforeEach, describe, expect, it } from 'vitest';
2 | import { createActions, createWorld, trait } from '../src';
3 |
4 | const IsPlayer = trait();
5 |
6 | describe('Actions', () => {
7 | const world = createWorld();
8 |
9 | beforeEach(() => {
10 | world.reset();
11 | });
12 |
13 | it('should create memoized actions', () => {
14 | const actions = createActions((world) => ({
15 | spawnPlayer: () => world.spawn(IsPlayer),
16 | destroyPlayers: () => {
17 | world.query(IsPlayer).forEach((e) => e.destroy());
18 | },
19 | }));
20 |
21 | const { spawnPlayer } = actions(world);
22 |
23 | const player = spawnPlayer();
24 | expect(player.has(IsPlayer)).toBe(true);
25 |
26 | const { spawnPlayer: spawnPlayer2 } = actions(world);
27 |
28 | // Should be the same function
29 | expect(spawnPlayer2).toBe(spawnPlayer);
30 | });
31 |
32 | it('should create multiple memoized actions per world', () => {
33 | const actions1 = createActions((world) => ({
34 | spawnPlayer: () => world.spawn(IsPlayer),
35 | destroyPlayers: () => {
36 | world.query(IsPlayer).forEach((e) => e.destroy());
37 | },
38 | }));
39 |
40 | const actions2 = createActions((world) => ({
41 | spawnPlayer: () => world.spawn(IsPlayer),
42 | destroyPlayers: () => {
43 | world.query(IsPlayer).forEach((e) => e.destroy());
44 | },
45 | }));
46 |
47 | const { spawnPlayer: spawnPlayer1 } = actions1(world);
48 | const { spawnPlayer: spawnPlayer2 } = actions2(world);
49 |
50 | // Should be different functions
51 | expect(spawnPlayer1).not.toBe(spawnPlayer2);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/packages/core/tests/utils/sparse-set.test.ts:
--------------------------------------------------------------------------------
1 | import { beforeEach, describe, expect, it } from 'vitest';
2 | import { SparseSet } from '../../src/utils/sparse-set';
3 |
4 | describe('SparseSet', () => {
5 | let set: SparseSet;
6 |
7 | beforeEach(() => {
8 | set = new SparseSet();
9 | });
10 |
11 | it('should add values correctly', () => {
12 | // Should add 0 just fine.
13 | set.add(0);
14 | expect(set.dense).toEqual([0]);
15 | expect(set.has(0)).toBe(true);
16 |
17 | set.add(1);
18 | set.add(2);
19 | expect(set.dense).toEqual([0, 1, 2]);
20 | expect(set.sparse[1]).toBe(1);
21 | expect(set.sparse[2]).toBe(2);
22 | });
23 |
24 | it('should not add duplicate values', () => {
25 | set.add(1);
26 | set.add(1);
27 | expect(set.dense).toEqual([1]);
28 | });
29 |
30 | it('should check if a value exists', () => {
31 | set.add(1);
32 | expect(set.has(1)).toBe(true);
33 | expect(set.has(2)).toBe(false);
34 | });
35 |
36 | it('should remove values correctly', () => {
37 | set.add(1);
38 | set.add(2);
39 | set.remove(1);
40 | expect(set.dense).toEqual([2]);
41 | expect(set.has(1)).toBe(false);
42 | expect(set.has(2)).toBe(true);
43 | });
44 |
45 | it('should clear the set correctly', () => {
46 | set.add(1);
47 | set.add(2);
48 | set.clear();
49 | expect(set.dense).toEqual([]);
50 | });
51 |
52 | it('should sort the set correctly', () => {
53 | set.add(3);
54 | set.add(1);
55 | set.add(2);
56 | set.sort();
57 | expect(set.dense).toEqual([1, 2, 3]);
58 | expect(set.sparse[1]).toBe(0);
59 | expect(set.sparse[2]).toBe(1);
60 | expect(set.sparse[3]).toBe(2);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/packages/publish/tests/core/actions.test.ts:
--------------------------------------------------------------------------------
1 | import { beforeEach, describe, expect, it } from 'vitest';
2 | import { createActions, createWorld, trait } from '../../dist';
3 |
4 | const IsPlayer = trait();
5 |
6 | describe('Actions', () => {
7 | const world = createWorld();
8 |
9 | beforeEach(() => {
10 | world.reset();
11 | });
12 |
13 | it('should create memoized actions', () => {
14 | const actions = createActions((world) => ({
15 | spawnPlayer: () => world.spawn(IsPlayer),
16 | destroyPlayers: () => {
17 | world.query(IsPlayer).forEach((e) => e.destroy());
18 | },
19 | }));
20 |
21 | const { spawnPlayer } = actions(world);
22 |
23 | const player = spawnPlayer();
24 | expect(player.has(IsPlayer)).toBe(true);
25 |
26 | const { spawnPlayer: spawnPlayer2 } = actions(world);
27 |
28 | // Should be the same function
29 | expect(spawnPlayer2).toBe(spawnPlayer);
30 | });
31 |
32 | it('should create multiple memoized actions per world', () => {
33 | const actions1 = createActions((world) => ({
34 | spawnPlayer: () => world.spawn(IsPlayer),
35 | destroyPlayers: () => {
36 | world.query(IsPlayer).forEach((e) => e.destroy());
37 | },
38 | }));
39 |
40 | const actions2 = createActions((world) => ({
41 | spawnPlayer: () => world.spawn(IsPlayer),
42 | destroyPlayers: () => {
43 | world.query(IsPlayer).forEach((e) => e.destroy());
44 | },
45 | }));
46 |
47 | const { spawnPlayer: spawnPlayer1 } = actions1(world);
48 | const { spawnPlayer: spawnPlayer2 } = actions2(world);
49 |
50 | // Should be different functions
51 | expect(spawnPlayer1).not.toBe(spawnPlayer2);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/updateGravity.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Time } from '../traits/Time';
4 | import { bodyTraits } from './setInitial';
5 |
6 | export const updateGravity = ({ world }: { world: World }) => {
7 | const bodies = world.query(...bodyTraits);
8 | const { delta } = world.get(Time)!;
9 |
10 | bodies.useStores(([position, velocity, mass, _, acceleration], bodies) => {
11 | for (let j = 0; j < bodies.length; j++) {
12 | const currentId = bodies[j].id();
13 |
14 | acceleration.x[currentId] = 0;
15 | acceleration.y[currentId] = 0;
16 |
17 | for (let i = 0; i < bodies.length; i++) {
18 | const targetId = bodies[i].id();
19 | if (currentId === targetId) continue; // Skip self
20 |
21 | const dx = position.x[targetId] - position.x[currentId];
22 | const dy = position.y[targetId] - position.y[currentId];
23 | let distanceSquared = dx * dx + dy * dy;
24 |
25 | if (distanceSquared < CONSTANTS.STICKY) distanceSquared = CONSTANTS.STICKY; // Apply stickiness
26 |
27 | const distance = Math.sqrt(distanceSquared);
28 | const forceMagnitude =
29 | (+mass.value[currentId] * +mass.value[targetId]) / distanceSquared;
30 |
31 | acceleration.x[currentId] += (dx / distance) * forceMagnitude;
32 | acceleration.y[currentId] += (dy / distance) * forceMagnitude;
33 | }
34 |
35 | // Apply computed force to entity's velocity, adjusting for its mass
36 | velocity.x[currentId] += (acceleration.x[currentId] * delta) / mass.value[currentId];
37 | velocity.y[currentId] += (acceleration.y[currentId] * delta) / mass.value[currentId];
38 | }
39 | });
40 | };
41 |
--------------------------------------------------------------------------------
/benches/apps/n-body-react/src/actions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Acceleration,
3 | Circle,
4 | Color,
5 | IsCentralMass,
6 | Mass,
7 | Position,
8 | Repulse,
9 | Velocity,
10 | } from '@sim/n-body';
11 | import { createActions } from 'koota';
12 |
13 | let lastSpawnTime = 0;
14 | const spawnInterval = 100; // milliseconds
15 |
16 | export const actions = createActions((world) => ({
17 | spawnRepulsor: (e: React.PointerEvent, frustumSize: number) => {
18 | const now = performance.now();
19 | if (now - lastSpawnTime < spawnInterval) return;
20 |
21 | lastSpawnTime = now;
22 |
23 | const aspect = window.innerWidth / window.innerHeight;
24 | const viewWidth = frustumSize * aspect;
25 | const viewHeight = frustumSize;
26 |
27 | const ndcX = (e.clientX / window.innerWidth) * 2 - 1;
28 | const ndcY = -(e.clientY / window.innerHeight) * 2 + 1;
29 |
30 | const x = (ndcX * viewWidth) / 2;
31 | const y = (ndcY * viewHeight) / 2;
32 |
33 | world.spawn(
34 | Position({ x, y }),
35 | Circle({ radius: 160 }),
36 | Color({ r: 255, g: 0, b: 0 }),
37 | Repulse({ force: 5, decay: 0.96, delay: 1 }),
38 | Velocity,
39 | Acceleration,
40 | Mass({ value: 200 })
41 | );
42 | },
43 | spawnBodies: (count: number) => {
44 | for (let i = 0; i < count; i++) {
45 | world.spawn(Position, Velocity, Mass, Circle, Color, Acceleration);
46 | }
47 | },
48 | spawnCentralMasses: (count: number) => {
49 | for (let i = 0; i < count; i++) {
50 | world.spawn(Position, Velocity, Mass, Circle, Color, Acceleration, IsCentralMass);
51 | }
52 | },
53 | destroyAllBodies: () => {
54 | world.query(Position, Velocity, Mass, Circle, Color, Acceleration).forEach((entity) => {
55 | entity.destroy();
56 | });
57 | },
58 | }));
59 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/push-enemies.ts:
--------------------------------------------------------------------------------
1 | import type { Entity, World } from 'koota';
2 | import * as THREE from 'three';
3 | import { IsEnemy, IsPlayer, Movement, ShieldVisibility, SpatialHashMap, Transform } from '../traits';
4 |
5 | const collisionRadius = 2;
6 | const pushStrength = 0.1;
7 | const pushForce = new THREE.Vector3();
8 |
9 | export const pushEnemies = ({ world }: { world: World }) => {
10 | const spatialHashMap = world.get(SpatialHashMap)!;
11 |
12 | world.query(IsPlayer, Transform, Movement).updateEach(([{ position }, { velocity }], player) => {
13 | // Get nearby entities
14 | const nearbyEntities = spatialHashMap.getNearbyEntities(
15 | position.x,
16 | position.y,
17 | position.z,
18 | collisionRadius
19 | );
20 |
21 | // Filter for enemies within collision range
22 | const collidingEnemies: Entity[] = nearbyEntities.filter((entity) => {
23 | return (
24 | entity.has(IsEnemy) &&
25 | entity.get(Transform)!.position.distanceTo(position) <= collisionRadius
26 | );
27 | });
28 |
29 | // Apply push force to colliding enemies
30 | for (const enemy of collidingEnemies) {
31 | const enemyTransform = enemy.get(Transform)!;
32 | const enemyMovement = enemy.get(Movement)!;
33 |
34 | // Calculate push direction (away from player)
35 | pushForce
36 | .copy(enemyTransform.position)
37 | .sub(position)
38 | .normalize()
39 | .multiplyScalar(velocity.length() * pushStrength);
40 |
41 | // Apply push force to enemy
42 | enemyMovement.force.add(pushForce);
43 | }
44 |
45 | if (collidingEnemies.length > 0) {
46 | if (!player.has(ShieldVisibility)) player.add(ShieldVisibility);
47 | else player.set(ShieldVisibility, { current: 0 });
48 | }
49 | });
50 | };
51 |
--------------------------------------------------------------------------------
/packages/react/tests/world.test.tsx:
--------------------------------------------------------------------------------
1 | import { createWorld, universe, type World } from '@koota/core';
2 | import { render } from '@testing-library/react';
3 | import { act, StrictMode, useEffect, useMemo } from 'react';
4 | import { beforeEach, describe, expect, it } from 'vitest';
5 | import { useWorld, WorldProvider } from '../src';
6 |
7 | declare global {
8 | var IS_REACT_ACT_ENVIRONMENT: boolean;
9 | }
10 |
11 | // Let React know that we'll be testing effectful components
12 | global.IS_REACT_ACT_ENVIRONMENT = true;
13 |
14 | let world: World;
15 |
16 | describe('World', () => {
17 | beforeEach(() => {
18 | universe.reset();
19 | world = createWorld();
20 | });
21 |
22 | it('provides a world to its children', async () => {
23 | let worldTest: World | null = null;
24 |
25 | function Test() {
26 | worldTest = useWorld();
27 | return null;
28 | }
29 |
30 | await act(async () => {
31 | render(
32 |
33 |
34 |
35 |
36 |
37 | );
38 | });
39 |
40 | expect(worldTest).toBe(world);
41 | });
42 |
43 | it('can lazy init to create a world in useMemo', () => {
44 | universe.reset();
45 |
46 | let worldTest: World = null!;
47 |
48 | function Test() {
49 | worldTest = useMemo(() => createWorld({ lazy: true }), []);
50 |
51 | useEffect(() => {
52 | worldTest.init();
53 | return () => worldTest.destroy();
54 | }, [worldTest]);
55 |
56 | return null;
57 | }
58 |
59 | render(
60 |
61 |
62 |
63 | );
64 |
65 | expect(worldTest).toBeDefined();
66 | expect(worldTest!.isInitialized).toBe(true);
67 | expect(universe.worlds.length).toBe(1);
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/packages/publish/tests/react/world.test.tsx:
--------------------------------------------------------------------------------
1 | import { createWorld, universe, type World } from '../../dist';
2 | import { render } from '@testing-library/react';
3 | import { act, StrictMode, useEffect, useMemo } from 'react';
4 | import { beforeEach, describe, expect, it } from 'vitest';
5 | import { useWorld, WorldProvider } from '../../react';
6 |
7 | declare global {
8 | var IS_REACT_ACT_ENVIRONMENT: boolean;
9 | }
10 |
11 | // Let React know that we'll be testing effectful components
12 | global.IS_REACT_ACT_ENVIRONMENT = true;
13 |
14 | let world: World;
15 |
16 | describe('World', () => {
17 | beforeEach(() => {
18 | universe.reset();
19 | world = createWorld();
20 | });
21 |
22 | it('provides a world to its children', async () => {
23 | let worldTest: World | null = null;
24 |
25 | function Test() {
26 | worldTest = useWorld();
27 | return null;
28 | }
29 |
30 | await act(async () => {
31 | render(
32 |
33 |
34 |
35 |
36 |
37 | );
38 | });
39 |
40 | expect(worldTest).toBe(world);
41 | });
42 |
43 | it('can lazy init to create a world in useMemo', () => {
44 | universe.reset();
45 |
46 | let worldTest: World = null!;
47 |
48 | function Test() {
49 | worldTest = useMemo(() => createWorld({ lazy: true }), []);
50 |
51 | useEffect(() => {
52 | worldTest.init();
53 | return () => worldTest.destroy();
54 | }, [worldTest]);
55 |
56 | return null;
57 | }
58 |
59 | render(
60 |
61 |
62 |
63 | );
64 |
65 | expect(worldTest).toBeDefined();
66 | expect(worldTest!.isInitialized).toBe(true);
67 | expect(universe.worlds.length).toBe(1);
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/schedule.ts:
--------------------------------------------------------------------------------
1 | import { Schedule } from 'directed';
2 | import type { World } from 'koota';
3 | import { applyInput } from './apply-input';
4 | import { cleanupSpatialHashMap } from './cleanup-spatial-hash-map';
5 | import { dampPlayerMovement } from './damp-player-movement';
6 | import { followPlayer } from './follow-player';
7 | import { handleShooting } from './handle-shooting';
8 | import { pollInput } from './poll-input';
9 | import { pushEnemies } from './push-enemies';
10 | import { spawnEnemies } from './spawn-enemies';
11 | import { tickExplosion } from './tick-explosion';
12 | import { tickShieldVisibility } from './tick-shield-visibility';
13 | import { updateAutoRotate } from './update-auto-rotate';
14 | import { updateAvoidance } from './update-avoidance';
15 | import { updateBullets } from './update-bullet';
16 | import { updateBulletCollisions } from './update-bullet-collisions';
17 | import { updateMovement } from './update-movement';
18 | import { updateSpatialHashing } from './update-spatial-hashing';
19 | import { updateTime } from './update-time';
20 |
21 | export const schedule = new Schedule<{ world: World }>();
22 |
23 | schedule.add(updateTime);
24 | schedule.add(pollInput);
25 | schedule.add(spawnEnemies);
26 | schedule.add(followPlayer);
27 | schedule.add(updateAvoidance);
28 | schedule.add(applyInput);
29 | schedule.add(dampPlayerMovement);
30 | schedule.add(pushEnemies);
31 | schedule.add(handleShooting);
32 | schedule.add(updateMovement);
33 | schedule.add(updateBullets);
34 | schedule.add(updateBulletCollisions);
35 | schedule.add(updateAutoRotate);
36 | schedule.add(updateSpatialHashing);
37 | schedule.add(cleanupSpatialHashMap);
38 | schedule.add(tickShieldVisibility);
39 | schedule.add(tickExplosion);
40 |
41 | schedule.build();
42 |
--------------------------------------------------------------------------------
/packages/core/src/world/utils/world-index.ts:
--------------------------------------------------------------------------------
1 | import { WORLD_ID_BITS } from '../../entity/utils/pack-entity';
2 |
3 | export type WorldIndex = {
4 | worldCursor: number;
5 | releasedWorldIds: number[];
6 | maxWorlds: number;
7 | };
8 |
9 | /**
10 | * Creates and initializes a new WorldIndex.
11 | * @param worldIdBits - The number of bits used for world IDs.
12 | * @returns A new WorldIndex object.
13 | */
14 | export function createWorldIndex(): WorldIndex {
15 | return {
16 | worldCursor: 0,
17 | releasedWorldIds: [],
18 | maxWorlds: 2 ** WORLD_ID_BITS,
19 | };
20 | }
21 |
22 | /**
23 | * Allocates a new world ID or recycles an existing one.
24 | * @param index - The WorldIndex to allocate from.
25 | * @returns The new or recycled world ID.
26 | */
27 | export function allocateWorldId(index: WorldIndex): number {
28 | if (index.releasedWorldIds.length > 0) {
29 | return index.releasedWorldIds.pop()!;
30 | }
31 |
32 | if (index.worldCursor >= index.maxWorlds) {
33 | throw new Error(`Koota: Too many worlds created. The maximum is ${index.maxWorlds}.`);
34 | }
35 | return index.worldCursor++;
36 | }
37 |
38 | /**
39 | * Releases a world ID back to the index.
40 | * @param index - The WorldIndex to release to.
41 | * @param worldId - The world ID to release.
42 | */
43 | export function releaseWorldId(index: WorldIndex, worldId: number): void {
44 | if (worldId < 0 || worldId >= index.maxWorlds) {
45 | throw new Error(`Invalid world ID: ${worldId}`);
46 | }
47 |
48 | if (worldId === index.worldCursor - 1) {
49 | // If it's the last allocated ID, just decrement the cursor
50 | index.worldCursor--;
51 | } else if (worldId < index.worldCursor && !index.releasedWorldIds.includes(worldId)) {
52 | // Otherwise, add it to the released IDs list for recycling
53 | index.releasedWorldIds.push(worldId);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/publish/scripts/copy-react-files.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import path from 'node:path';
3 |
4 | const sourceDir = 'dist';
5 | const targetDir = 'react';
6 | const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
7 |
8 | async function copyAndRename() {
9 | try {
10 | if (verbose) console.log('\n> Preparing to copy React files...');
11 | // Ensure the target directory exists
12 | await fs.mkdir(targetDir, { recursive: true });
13 |
14 | const files = [
15 | { src: 'react.cjs', dest: 'index.cjs' },
16 | { src: 'react.js', dest: 'index.js' },
17 | { src: 'react.d.ts', dest: 'index.d.ts' },
18 | { src: 'react.d.cts', dest: 'index.d.cts' },
19 | ];
20 |
21 | for (const file of files) {
22 | await fs.copyFile(path.join(sourceDir, file.src), path.join(targetDir, file.dest));
23 | if (verbose) console.log(` ✓ ${file.src} → ${targetDir}/${file.dest}`);
24 | }
25 |
26 | if (verbose) console.log('\n> Updating imports...');
27 | // Update imports in all files
28 | for (const file of ['index.js', 'index.cjs', 'index.d.ts', 'index.d.cts']) {
29 | const filePath = path.join(targetDir, file);
30 | const content = await fs.readFile(filePath, 'utf-8');
31 |
32 | // Replace relative imports with paths pointing to dist folder
33 | const updatedContent = content.replace(
34 | /(from\s+['"])\.\.?\/(.*?)(['"])/g,
35 | `$1../${sourceDir}/$2$3`
36 | );
37 |
38 | await fs.writeFile(filePath, updatedContent);
39 | if (verbose) console.log(` ✓ ${file}`);
40 | }
41 |
42 | if (verbose) {
43 | console.log('\n> React files copied and updated successfully\n');
44 | } else {
45 | console.log(`✓ Copied ${files.length} React files`);
46 | }
47 | } catch (error) {
48 | console.error('\n> Error copying React files:', error);
49 | }
50 | }
51 |
52 | copyAndRename();
53 |
--------------------------------------------------------------------------------
/scripts/sim.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'node:child_process';
2 | import { existsSync, readdirSync } from 'node:fs';
3 | import { join } from 'node:path';
4 | import { COLORS } from './constants/colors';
5 |
6 | // Parse the command-line arguments
7 | const args = process.argv.slice(2);
8 |
9 | // Retrieve the suite name and engine type from parsed arguments
10 | const suiteName = args[0];
11 | const bunArg = args.includes('--bun');
12 |
13 | // Function to execute the main.ts file within a directory
14 | const executeMainTs = (directoryPath: string, engine: string = 'bun') => {
15 | const mainTsPath = join(directoryPath, 'src/main.ts');
16 |
17 | // Check if main.ts exists in the directory
18 | if (existsSync(mainTsPath)) {
19 | console.log(
20 | `Executing ${COLORS.fg.blue}${suiteName}${COLORS.reset} using ${COLORS.fg.yellow}${engine}${COLORS.reset}`
21 | );
22 | // Execute the main.ts file
23 | execSync(`pnpm dlx tsx ${mainTsPath}`, { stdio: 'inherit' });
24 | // if (engine === 'bun') execSync(`bun run ${mainTsPath}`, { stdio: 'inherit' });
25 | } else {
26 | console.error(`main.ts not found in ${directoryPath}`);
27 | }
28 | };
29 |
30 | // Function to find and run main.ts files for the specified suite
31 | const runSuites = async (sim: string) => {
32 | const rootPath = process.cwd();
33 | const baseDir = join(rootPath, 'benches', 'sims');
34 | const simDir = join(baseDir, sim);
35 |
36 | // Check if the specified suite directory exists
37 | if (existsSync(simDir) && readdirSync(baseDir).includes(sim)) {
38 | executeMainTs(simDir, bunArg ? 'bun' : 'node');
39 | } else {
40 | console.error(`Suite not found: ${sim}`);
41 | }
42 | };
43 |
44 | // Check if a suite name was provided
45 | if (!suiteName) {
46 | console.error('Please provide a suite name as an argument.');
47 | process.exit(1);
48 | }
49 |
50 | // Run the suites
51 | runSuites(suiteName);
52 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/systems/syncThreeObjects.ts:
--------------------------------------------------------------------------------
1 | import { Circle, Color, Position } from '@sim/add-remove';
2 | import { createRemoved, type World } from 'koota';
3 | import { Points } from '../trait/Points';
4 |
5 | const normalize = (x: number, min: number, max: number) => (x - min) / (max - min);
6 | const Removed = createRemoved();
7 |
8 | export const syncThreeObjects = ({ world }: { world: World }) => {
9 | const entities = world.query(Position, Circle, Color);
10 | const removedEntities = world.query(Removed(Position, Circle, Color));
11 |
12 | const particlesEntity = world.queryFirst(Points);
13 | if (!particlesEntity) return;
14 |
15 | const particles = particlesEntity.get(Points)!.object;
16 |
17 | const positions = particles.geometry.attributes.position.array;
18 | const colors = particles.geometry.attributes.color.array;
19 | const sizes = particles.geometry.attributes.size.array;
20 |
21 | entities.useStores(([position, circle, color]) => {
22 | for (let i = 0; i < entities.length; i++) {
23 | const eid = entities[i].id();
24 |
25 | // Update positions
26 | positions[eid * 3] = position.x[eid];
27 | positions[eid * 3 + 1] = position.y[eid];
28 | positions[eid * 3 + 2] = position.z[eid];
29 |
30 | // Update sizes
31 | sizes[eid] = circle.radius[eid] * 0.3;
32 |
33 | // Update colors
34 | const r = normalize(color.r[eid], 0, 255);
35 | const g = normalize(color.g[eid], 0, 255);
36 | const b = normalize(color.b[eid], 0, 255);
37 | colors[eid * 3] = r;
38 | colors[eid * 3 + 1] = g;
39 | colors[eid * 3 + 2] = b;
40 | }
41 |
42 | for (let i = 0; i < removedEntities.length; i++) {
43 | const eid = removedEntities[i];
44 | sizes[eid] = 0;
45 | }
46 | });
47 |
48 | particles.geometry.attributes.position.needsUpdate = true;
49 | particles.geometry.attributes.color.needsUpdate = true;
50 | particles.geometry.attributes.size.needsUpdate = true;
51 | };
52 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/create-query-hash.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import { isRelationPair } from '../../relation/utils/is-relation';
3 | import type { Relation } from '../../relation/types';
4 | import type { Trait } from '../../trait/types';
5 | import { isModifier } from '../modifier';
6 | import type { QueryHash, QueryParameter } from '../types';
7 |
8 | const sortedIDs = new Float64Array(1024); // Use Float64 for larger IDs with relation encoding
9 |
10 | export const createQueryHash = (parameters: QueryParameter[]): QueryHash => {
11 | sortedIDs.fill(0);
12 | let cursor = 0;
13 |
14 | for (let i = 0; i < parameters.length; i++) {
15 | const param = parameters[i];
16 |
17 | if (isRelationPair(param)) {
18 | // Encode relation pair as: (relationTraitId * 1000000) + targetId
19 | // This ensures unique hashes for different relation/target combinations
20 | const pairCtx = param[$internal];
21 | const relation = pairCtx.relation;
22 | const target = pairCtx.target;
23 |
24 | const relationId = (relation as Relation)[$internal].trait.id;
25 | const targetId = typeof target === 'number' ? target : -1;
26 |
27 | // Combine into a unique hash number
28 | sortedIDs[cursor++] = relationId * 10000000 + targetId + 5000000;
29 | } else if (isModifier(param)) {
30 | const modifierId = param.id;
31 | const traitIds = param.traitIds;
32 |
33 | for (let i = 0; i < traitIds.length; i++) {
34 | const traitId = traitIds[i];
35 | sortedIDs[cursor++] = modifierId * 100000 + traitId;
36 | }
37 | } else {
38 | const traitId = (param as Trait).id;
39 | sortedIDs[cursor++] = traitId;
40 | }
41 | }
42 |
43 | // Sort only the portion of the array that has been filled.
44 | const filledArray = sortedIDs.subarray(0, cursor);
45 | filledArray.sort();
46 |
47 | // Create string key.
48 | const hash = filledArray.join(',');
49 |
50 | return hash;
51 | };
52 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-has.ts:
--------------------------------------------------------------------------------
1 | import { $internal, Trait, type Entity, type World } from '@koota/core';
2 | import { useEffect, useMemo, useState } from 'react';
3 | import { isWorld } from '../utils/is-world';
4 | import { useWorld } from '../world/use-world';
5 |
6 | export function useHas(target: Entity | World | undefined | null, trait: Trait): boolean {
7 | // Get the world from context.
8 | const contextWorld = useWorld();
9 |
10 | // Memoize the target entity and a subscriber function.
11 | const memo = useMemo(
12 | () => (target ? createSubscriptions(target, trait, contextWorld) : undefined),
13 | [target, trait, contextWorld]
14 | );
15 |
16 | // Initialize the state with whether the entity has the tag.
17 | const [value, setValue] = useState(() => {
18 | return memo?.entity.has(trait) ?? false;
19 | });
20 |
21 | // Subscribe to add/remove events for the tag.
22 | useEffect(() => {
23 | if (!memo) {
24 | setValue(false);
25 | return;
26 | }
27 |
28 | const unsubscribe = memo.subscribe((value) => {
29 | setValue(value ?? false);
30 | });
31 |
32 | return () => {
33 | unsubscribe();
34 | setValue(false);
35 | };
36 | }, [memo]);
37 |
38 | return value;
39 | }
40 |
41 | function createSubscriptions(target: Entity | World, trait: Trait, contextWorld: World) {
42 | const world = isWorld(target) ? target : contextWorld;
43 | const entity = isWorld(target) ? target[$internal].worldEntity : target;
44 |
45 | return {
46 | entity,
47 | subscribe: (setValue: (value: boolean | undefined) => void) => {
48 | const onAddUnsub = world.onAdd(trait, (e) => {
49 | if (e === entity) setValue(true);
50 | });
51 |
52 | const onRemoveUnsub = world.onRemove(trait, (e) => {
53 | if (e === entity) setValue(false);
54 | });
55 |
56 | setValue(entity.has(trait));
57 |
58 | return () => {
59 | onAddUnsub();
60 | onRemoveUnsub();
61 | };
62 | },
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-tag.ts:
--------------------------------------------------------------------------------
1 | import { $internal, type Entity, type TagTrait, type World } from '@koota/core';
2 | import { useEffect, useMemo, useState } from 'react';
3 | import { isWorld } from '../utils/is-world';
4 | import { useWorld } from '../world/use-world';
5 |
6 | export function useTag(target: Entity | World | undefined | null, tag: TagTrait): boolean {
7 | // Get the world from context.
8 | const contextWorld = useWorld();
9 |
10 | // Memoize the target entity and a subscriber function.
11 | const memo = useMemo(
12 | () => (target ? createSubscriptions(target, tag, contextWorld) : undefined),
13 | [target, tag, contextWorld]
14 | );
15 |
16 | // Initialize the state with whether the entity has the tag.
17 | const [value, setValue] = useState(() => {
18 | return memo?.entity.has(tag) ?? false;
19 | });
20 |
21 | // Subscribe to add/remove events for the tag.
22 | useEffect(() => {
23 | if (!memo) {
24 | setValue(false);
25 | return;
26 | }
27 |
28 | const unsubscribe = memo.subscribe((value) => {
29 | setValue(value ?? false);
30 | });
31 |
32 | return () => {
33 | unsubscribe();
34 | setValue(false);
35 | };
36 | }, [memo]);
37 |
38 | return value;
39 | }
40 |
41 | function createSubscriptions(target: Entity | World, tag: TagTrait, contextWorld: World) {
42 | const world = isWorld(target) ? target : contextWorld;
43 | const entity = isWorld(target) ? target[$internal].worldEntity : target;
44 |
45 | return {
46 | entity,
47 | subscribe: (setValue: (value: boolean | undefined) => void) => {
48 | const onAddUnsub = world.onAdd(tag, (e) => {
49 | if (e === entity) setValue(true);
50 | });
51 |
52 | const onRemoveUnsub = world.onRemove(tag, (e) => {
53 | if (e === entity) setValue(false);
54 | });
55 |
56 | setValue(entity.has(tag));
57 |
58 | return () => {
59 | onAddUnsub();
60 | onRemoveUnsub();
61 | };
62 | },
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/benches/sims/n-body/src/systems/setInitial.ts:
--------------------------------------------------------------------------------
1 | import { createAdded, type World } from 'koota';
2 | import { CONSTANTS } from '../constants';
3 | import { Acceleration, Circle, IsCentralMass, Mass, Position, Velocity } from '../traits';
4 | import { randInRange } from '../utils/randInRange';
5 |
6 | export const bodyTraits = [Position, Velocity, Mass, Circle, Acceleration] as const;
7 | const Added = createAdded();
8 |
9 | export const setInitial = ({ world }: { world: World }) => {
10 | const bodies = world.query(Added(...bodyTraits));
11 | const centralMasses = world.query(Added(...bodyTraits, IsCentralMass));
12 |
13 | bodies.updateEach(([position, velocity, mass, circle]) => {
14 | if (mass.value === 0) {
15 | // Random positions
16 | position.x = randInRange(-4000, 4000);
17 | position.y = randInRange(-100, 100);
18 | mass.value = CONSTANTS.BASE_MASS + randInRange(0, CONSTANTS.VAR_MASS);
19 |
20 | // Calculate velocity for a stable orbit, assuming a circular orbit logic
21 | if (position.x !== 0 || position.y !== 0) {
22 | const radius = Math.sqrt(position.x ** 2 + position.y ** 2);
23 | const normX = position.x / radius;
24 | const normY = position.y / radius;
25 |
26 | // Perpendicular vector for circular orbit
27 | const vecRotX = -normY;
28 | const vecRotY = normX;
29 |
30 | const v = Math.sqrt(CONSTANTS.INITIAL_C / radius / mass.value / CONSTANTS.SPEED);
31 | velocity.x = vecRotX * v;
32 | velocity.y = vecRotY * v;
33 | }
34 |
35 | // Set circle radius based on mass
36 | circle.radius =
37 | CONSTANTS.MAX_RADIUS * (mass.value / (CONSTANTS.BASE_MASS + CONSTANTS.VAR_MASS)) + 1;
38 | }
39 | });
40 |
41 | // Set the central mass properties.
42 | centralMasses.updateEach(([position, velocity, mass, circle]) => {
43 | position.x = 0;
44 | position.y = 0;
45 |
46 | velocity.x = 0;
47 | velocity.y = 0;
48 |
49 | mass.value = CONSTANTS.CENTRAL_MASS;
50 |
51 | circle.radius = CONSTANTS.MAX_RADIUS / 1.5;
52 | });
53 | };
54 |
--------------------------------------------------------------------------------
/benches/apps/app-tools/src/stats/stats.ts:
--------------------------------------------------------------------------------
1 | import { getFPS, type Measurement, measure } from '@sim/bench-tools';
2 | import './stats.css';
3 |
4 | type Stats = Record any>;
5 |
6 | export function initStats(extras?: Stats) {
7 | const measurementRef = { current: { delta: 0, average: 0 } as Measurement };
8 | const fpsRef = { current: 0 };
9 |
10 | const updates: (() => void)[] = [];
11 |
12 | const stats = {
13 | FPS: () => fpsRef.current.toFixed(3),
14 | 'Frame time': () => `${measurementRef.current.average.toFixed(3)}ms`,
15 | ...extras,
16 | };
17 |
18 | let ele: HTMLElement | null = null;
19 |
20 | const reset = () => {
21 | measurementRef.current = { delta: 0, average: 0 };
22 | fpsRef.current = 0;
23 | updateStats();
24 | };
25 |
26 | const create = () => {
27 | ele = document.createElement('div');
28 | document.body.appendChild(ele);
29 | ele.classList.add('stats');
30 |
31 | for (const [label, value] of Object.entries(stats)) {
32 | const { div, update } = createStat(label, value);
33 | ele.appendChild(div);
34 | updates.push(update);
35 | }
36 |
37 | // Add reset button
38 | const resetButton = document.createElement('button');
39 | resetButton.textContent = 'Reset';
40 | resetButton.onclick = (e) => {
41 | e.preventDefault();
42 | reset();
43 | };
44 | ele.appendChild(resetButton);
45 | };
46 |
47 | const updateStats = () => {
48 | getFPS(fpsRef);
49 | for (const update of updates) {
50 | update();
51 | }
52 | };
53 |
54 | return {
55 | updateStats,
56 | measure: async (fn: (...args: any[]) => any) => await measure(fn, measurementRef),
57 | destroy: () => ele?.remove(),
58 | create,
59 | };
60 | }
61 |
62 | function createStat(label: string, getValue: () => any) {
63 | const div = document.createElement('div');
64 | div.classList.add('stat');
65 | div.innerHTML = `${label}: 0`;
66 |
67 | function update() {
68 | div.innerHTML = `${label}: ${getValue()}`;
69 | }
70 |
71 | return { div, update };
72 | }
73 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/systems/init.ts:
--------------------------------------------------------------------------------
1 | import { CONSTANTS } from '@sim/add-remove';
2 | import type { World } from 'koota';
3 | import * as THREE from 'three';
4 | import { scene } from '../scene';
5 | import { Points } from '../trait/Points';
6 |
7 | let first = false;
8 |
9 | export function init({ world }: { world: World }) {
10 | if (first) return;
11 |
12 | const particleCount = CONSTANTS.BODIES;
13 |
14 | // Create BufferGeometry for particles
15 | const geometry = new THREE.BufferGeometry();
16 | const positions = new Float32Array(particleCount * 3); // x, y, z for each particle
17 | const colors = new Float32Array(particleCount * 4); // r, g, b, a for each particle
18 | const sizes = new Float32Array(particleCount); // size for each particle
19 |
20 | geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
21 | geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
22 | geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
23 |
24 | const material = new THREE.ShaderMaterial({
25 | vertexShader: vertexShader(),
26 | fragmentShader: fragmentShader(),
27 | transparent: true,
28 | });
29 |
30 | const particles = new THREE.Points(geometry, material);
31 |
32 | scene.add(particles);
33 | world.spawn(Points({ object: particles }));
34 |
35 | first = true;
36 | }
37 |
38 | function vertexShader() {
39 | return `
40 | attribute float size;
41 | attribute vec4 color;
42 | varying vec4 vColor;
43 | void main() {
44 | vColor = color;
45 | vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
46 | gl_PointSize = size * ( 250.0 / -mvPosition.z );
47 | gl_Position = projectionMatrix * mvPosition;
48 | }
49 | `;
50 | }
51 |
52 | function fragmentShader() {
53 | return `
54 | varying vec4 vColor;
55 | void main() {
56 | float distanceFromCenter = length(gl_PointCoord - vec2(0.5, 0.5));
57 | if (distanceFromCenter > 0.5) {
58 | discard;
59 | }
60 | gl_FragColor = vec4( vColor );
61 | }
62 | `;
63 | }
64 |
--------------------------------------------------------------------------------
/packages/publish/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "koota",
3 | "version": "0.6.1",
4 | "description": "🌎 Performant real-time state management for React and TypeScript",
5 | "license": "ISC",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "types": "./src/index.ts",
9 | "exports": {
10 | ".": {
11 | "types": "./src/index.ts",
12 | "import": "./src/index.ts"
13 | },
14 | "./react": {
15 | "types": "./src/react.ts",
16 | "import": "./src/react.ts"
17 | }
18 | },
19 | "files": [
20 | "dist",
21 | "react",
22 | "README.md",
23 | "LICENSE"
24 | ],
25 | "publishConfig": {
26 | "main": "./dist/index.cjs",
27 | "module": "./dist/index.js",
28 | "types": "./dist/index.d.ts",
29 | "exports": {
30 | ".": {
31 | "types": {
32 | "import": "./dist/index.d.ts",
33 | "require": "./dist/index.d.cts"
34 | },
35 | "import": "./dist/index.js",
36 | "require": "./dist/index.cjs"
37 | },
38 | "./react": {
39 | "types": {
40 | "import": "./dist/react.d.ts",
41 | "require": "./dist/react.d.cts"
42 | },
43 | "import": "./dist/react.js",
44 | "require": "./dist/react.cjs"
45 | }
46 | }
47 | },
48 | "scripts": {
49 | "build": "tsup && node --no-warnings scripts/copy-readme.ts && node --no-warnings scripts/copy-react-files.ts",
50 | "test": "vitest --environment=jsdom",
51 | "generate-tests": "node scripts/generate-tests.ts"
52 | },
53 | "devDependencies": {
54 | "@config/typescript": "workspace:*",
55 | "@koota/core": "workspace:*",
56 | "@koota/react": "workspace:*",
57 | "@testing-library/react": "^16.3.0",
58 | "react": "catalog:",
59 | "react-dom": "catalog:",
60 | "tsup": "^8.5.1",
61 | "unplugin-inline-functions": "catalog:",
62 | "vitest": "catalog:"
63 | },
64 | "peerDependencies": {
65 | "@types/react": ">=18.0.0",
66 | "react": ">=18.0.0"
67 | },
68 | "peerDependenciesMeta": {
69 | "@types/react": {
70 | "optional": true
71 | },
72 | "react": {
73 | "optional": true
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-target.ts:
--------------------------------------------------------------------------------
1 | import { $internal, type Entity, type Relation, type Trait, type World } from '@koota/core';
2 | import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
3 | import { isWorld } from '../utils/is-world';
4 | import { useWorld } from '../world/use-world';
5 |
6 | export function useTarget(
7 | target: Entity | World | undefined | null,
8 | relation: Relation
9 | ): Entity | undefined {
10 | const contextWorld = useWorld();
11 |
12 | const memo = useMemo(
13 | () => (target ? createSubscriptions(target, relation, contextWorld) : undefined),
14 | [target, relation, contextWorld]
15 | );
16 |
17 | const [value, setValue] = useState(() => {
18 | return memo?.entity.targetFor(relation);
19 | });
20 |
21 | useEffect(() => {
22 | if (!memo) {
23 | setValue(undefined);
24 | return;
25 | }
26 | const unsubscribe = memo.subscribe(setValue);
27 | return () => unsubscribe();
28 | }, [memo]);
29 |
30 | return value;
31 | }
32 |
33 | function createSubscriptions(
34 | target: Entity | World,
35 | relation: Relation,
36 | contextWorld: World
37 | ) {
38 | const world = isWorld(target) ? target : contextWorld;
39 | const entity = isWorld(target) ? target[$internal].worldEntity : target;
40 |
41 | return {
42 | entity,
43 | subscribe: (setValue: Dispatch>) => {
44 | const onAddUnsub = world.onAdd(relation, (e, t) => {
45 | if (e === entity) setValue(entity.targetFor(relation));
46 | });
47 |
48 | const onRemoveUnsub = world.onRemove(relation, (e, t) => {
49 | if (e === entity) setValue(entity.targetFor(relation));
50 | });
51 |
52 | const onChangeUnsub = world.onChange(relation, (e, t) => {
53 | if (e === entity) setValue(entity.targetFor(relation));
54 | });
55 |
56 | setValue(entity.targetFor(relation));
57 |
58 | return () => {
59 | onAddUnsub();
60 | onRemoveUnsub();
61 | onChangeUnsub();
62 | };
63 | },
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/packages/core/src/storage/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Storage type for trait data.
3 | * - AoS: Array of instances, one per entity
4 | * - SoA: Object with arrays, one array per property
5 | */
6 | export type Store = T extends AoSFactory
7 | ? ReturnType[]
8 | : {
9 | [P in keyof T]: T[P] extends (...args: never[]) => unknown ? ReturnType[] : T[P][];
10 | };
11 |
12 | /**
13 | * Storage layout type.
14 | * - 'soa': Struct of Arrays - properties stored in separate arrays
15 | * - 'aos': Array of Structs - instances stored directly
16 | * - 'tag': No data storage - empty schema marker
17 | */
18 | export type StoreType = 'aos' | 'soa' | 'tag';
19 |
20 | /**
21 | * Schema definition for traits.
22 | * Can be a SoA object schema, an AoS factory function, or an empty object (tag).
23 | */
24 | export type Schema =
25 | | {
26 | [key: string]: number | bigint | string | boolean | null | undefined | (() => unknown);
27 | }
28 | | AoSFactory
29 | | Record;
30 |
31 | /**
32 | * Factory function for AoS (Array of Structs) storage.
33 | * Returns a single instance that will be stored per entity.
34 | */
35 | export type AoSFactory = () => unknown;
36 |
37 | /**
38 | * Normalizes schema types to their primitive forms.
39 | * Ensures that explicit values like true, false or "string literal" are
40 | * normalized to their primitive types (boolean, string, etc).
41 | */
42 | export type Norm = T extends Record
43 | ? T
44 | : T extends AoSFactory
45 | ? () => ReturnType extends number
46 | ? number
47 | : ReturnType extends boolean
48 | ? boolean
49 | : ReturnType extends string
50 | ? string
51 | : ReturnType
52 | : {
53 | [K in keyof T]: T[K] extends object
54 | ? T[K] extends (...args: never[]) => unknown
55 | ? T[K]
56 | : never
57 | : T[K] extends boolean
58 | ? boolean
59 | : T[K];
60 | }[keyof T] extends never
61 | ? never
62 | : {
63 | [K in keyof T]: T[K] extends boolean ? boolean : T[K];
64 | };
65 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-targets.ts:
--------------------------------------------------------------------------------
1 | import { $internal, type Entity, type Relation, type Trait, type World } from '@koota/core';
2 | import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
3 | import { isWorld } from '../utils/is-world';
4 | import { useWorld } from '../world/use-world';
5 |
6 | export function useTargets(
7 | target: Entity | World | undefined | null,
8 | relation: Relation
9 | ): Entity[] {
10 | const contextWorld = useWorld();
11 |
12 | const memo = useMemo(
13 | () => (target ? createSubscriptions(target, relation, contextWorld) : undefined),
14 | [target, relation, contextWorld]
15 | );
16 |
17 | const [value, setValue] = useState(() => {
18 | return memo?.entity.targetsFor(relation) ?? [];
19 | });
20 |
21 | useEffect(() => {
22 | if (!memo) {
23 | setValue([]);
24 | return;
25 | }
26 | const unsubscribe = memo.subscribe(setValue);
27 | return () => unsubscribe();
28 | }, [memo]);
29 |
30 | return value;
31 | }
32 |
33 | function createSubscriptions(
34 | target: Entity | World,
35 | relation: Relation,
36 | contextWorld: World
37 | ) {
38 | const world = isWorld(target) ? target : contextWorld;
39 | const entity = isWorld(target) ? target[$internal].worldEntity : target;
40 |
41 | return {
42 | entity,
43 | subscribe: (setValue: Dispatch>) => {
44 | const onAddUnsub = world.onAdd(relation, (e, t) => {
45 | if (e === entity) setValue(entity.targetsFor(relation));
46 | });
47 |
48 | // onRemove fires before data is removed, so filter out the target
49 | const onRemoveUnsub = world.onRemove(relation, (e, t) => {
50 | if (e === entity) setValue((prev) => prev.filter((p) => p !== t));
51 | });
52 |
53 | const onChangeUnsub = world.onChange(relation, (e, t) => {
54 | if (e === entity) setValue(entity.targetsFor(relation));
55 | });
56 |
57 | setValue(entity.targetsFor(relation));
58 |
59 | return () => {
60 | onAddUnsub();
61 | onRemoveUnsub();
62 | onChangeUnsub();
63 | };
64 | },
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/packages/core/src/entity/utils/pack-entity.ts:
--------------------------------------------------------------------------------
1 | import type { Entity } from '../types';
2 |
3 | // Constants for bit sizes
4 | export const WORLD_ID_BITS = 4; // 4 bits can represent 0-15
5 | export const GENERATION_BITS = 8; // 8 bits can represent 0-255
6 | export const ENTITY_ID_BITS = 20; // 20 bits can represent 0-1,048,575
7 | // Total bits used: 4 + 8 + 20 = 32 bits
8 |
9 | // Masks for extracting values
10 | export const WORLD_ID_MASK = (1 << WORLD_ID_BITS) - 1;
11 | export const GENERATION_MASK = (1 << GENERATION_BITS) - 1;
12 | export const ENTITY_ID_MASK = (1 << ENTITY_ID_BITS) - 1;
13 |
14 | // Bit shifts for positioning each component
15 | export const GENERATION_SHIFT = ENTITY_ID_BITS;
16 | export const WORLD_ID_SHIFT = GENERATION_SHIFT + GENERATION_BITS;
17 |
18 | export function packEntity(worldId: number, generation: number, entityId: number): Entity {
19 | return (((worldId & WORLD_ID_MASK) << WORLD_ID_SHIFT) |
20 | ((generation & GENERATION_MASK) << GENERATION_SHIFT) |
21 | (entityId & ENTITY_ID_MASK)) as Entity;
22 | }
23 |
24 | export function unpackEntity(entity: Entity) {
25 | return {
26 | worldId: entity >>> WORLD_ID_SHIFT,
27 | generation: (entity >>> GENERATION_SHIFT) & GENERATION_MASK,
28 | entityId: entity & ENTITY_ID_MASK,
29 | };
30 | }
31 |
32 | export const getEntityId = /* @inline @pure */ (entity: Entity) => entity & ENTITY_ID_MASK;
33 | export const getEntityWorldId = /* @inline @pure */ (entity: Entity) => entity >>> WORLD_ID_SHIFT;
34 | export const getEntityAndWorldId = /* @pure */ (entity: Entity): [number, number] => [
35 | entity & ENTITY_ID_MASK,
36 | entity >>> WORLD_ID_SHIFT,
37 | ];
38 |
39 | export const getEntityGeneration = /* @inline @pure */ (entity: Entity) =>
40 | (entity >>> GENERATION_SHIFT) & GENERATION_MASK;
41 |
42 | export const incrementGeneration = (entity: Entity): Entity =>
43 | ((entity & ~(GENERATION_MASK << GENERATION_SHIFT)) | // Clear current generation bits
44 | (((((entity >>> GENERATION_SHIFT) & GENERATION_MASK) + 1) & GENERATION_MASK) <<
45 | GENERATION_SHIFT)) as unknown as Entity; // Extract generation, increment, wrap around, shift back, and combine
46 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-query.ts:
--------------------------------------------------------------------------------
1 | import { $internal, createQuery, type QueryParameter, type QueryResult } from '@koota/core';
2 | import { useEffect, useMemo, useRef, useState } from 'react';
3 | import { useWorld } from '../world/use-world';
4 |
5 | export function useQuery(...parameters: T): QueryResult {
6 | const world = useWorld();
7 | const initialQueryVersionRef = useRef(0);
8 | // Used to track if we need to rerun effects internally.
9 | const [version, setVersion] = useState(0);
10 |
11 | // This will rerun every render since parameters will always be a fresh
12 | // array, but the return value will be stable.
13 | const queryRef = useMemo(() => createQuery(...parameters), [parameters]);
14 |
15 | useMemo(() => {
16 | // Using internals to get the query data.
17 | const query = world[$internal].queriesHashMap.get(queryRef.hash)!;
18 | initialQueryVersionRef.current = query.version;
19 | }, [world, queryRef]);
20 |
21 | const [entities, setEntities] = useState>(() => world.query(queryRef).sort());
22 |
23 | // Subscribe to changes.
24 | useEffect(() => {
25 | const unsubAdd = world.onQueryAdd(queryRef, () => {
26 | setEntities(world.query(queryRef).sort());
27 | });
28 |
29 | const unsubRemove = world.onQueryRemove(queryRef, () => {
30 | setEntities(world.query(queryRef).sort());
31 | });
32 |
33 | // Compare the initial version to the current version to
34 | // see it the query has changed.
35 | const query = world[$internal].queriesHashMap.get(queryRef.hash)!;
36 | if (query.version !== initialQueryVersionRef.current) {
37 | setEntities(world.query(queryRef).sort());
38 | }
39 |
40 | return () => {
41 | unsubAdd();
42 | unsubRemove();
43 | };
44 | }, [world, queryRef, version]);
45 |
46 | // Force reattaching event listeners when the world is reset.
47 | useEffect(() => {
48 | const handler = () => setVersion((v) => v + 1);
49 | world[$internal].resetSubscriptions.add(handler);
50 |
51 | return () => {
52 | world[$internal].resetSubscriptions.delete(handler);
53 | };
54 | }, [world]);
55 |
56 | return entities;
57 | }
58 |
--------------------------------------------------------------------------------
/benches/apps/revade/src/systems/poll-input.ts:
--------------------------------------------------------------------------------
1 | import type { World } from 'koota';
2 | import { Input, IsPlayer } from '../traits';
3 |
4 | // Track key states.
5 |
6 | const keys = {
7 | arrowUp: false,
8 | arrowDown: false,
9 | arrowLeft: false,
10 | arrowRight: false,
11 | w: false,
12 | a: false,
13 | s: false,
14 | d: false,
15 | };
16 |
17 | window.addEventListener('keydown', (e) => {
18 | switch (e.key.toLowerCase()) {
19 | case 'arrowup':
20 | case 'w':
21 | keys.arrowUp = true;
22 | keys.w = true;
23 | break;
24 | case 'arrowdown':
25 | case 's':
26 | keys.arrowDown = true;
27 | keys.s = true;
28 | break;
29 | case 'arrowleft':
30 | case 'a':
31 | keys.arrowLeft = true;
32 | keys.a = true;
33 | break;
34 | case 'arrowright':
35 | case 'd':
36 | keys.arrowRight = true;
37 | keys.d = true;
38 | break;
39 | }
40 | });
41 |
42 | window.addEventListener('keyup', (e) => {
43 | switch (e.key.toLowerCase()) {
44 | case 'arrowup':
45 | case 'w':
46 | keys.arrowUp = false;
47 | keys.w = false;
48 | break;
49 | case 'arrowdown':
50 | case 's':
51 | keys.arrowDown = false;
52 | keys.s = false;
53 | break;
54 | case 'arrowleft':
55 | case 'a':
56 | keys.arrowLeft = false;
57 | keys.a = false;
58 | break;
59 | case 'arrowright':
60 | case 'd':
61 | keys.arrowRight = false;
62 | keys.d = false;
63 | break;
64 | }
65 | });
66 |
67 | export const pollInput = ({ world }: { world: World }) => {
68 | world.query(IsPlayer, Input).updateEach(
69 | ([input]) => {
70 | // Get horizontal and vertical input.
71 | const horizontal =
72 | (keys.arrowRight || keys.d ? 1 : 0) - (keys.arrowLeft || keys.a ? 1 : 0);
73 | const vertical = (keys.arrowUp || keys.w ? 1 : 0) - (keys.arrowDown || keys.s ? 1 : 0);
74 |
75 | // Normalize the vector if moving diagonally.
76 | const length = Math.sqrt(horizontal * horizontal + vertical * vertical);
77 | if (length > 0) {
78 | input.x = horizontal / (length || 1);
79 | input.y = vertical / (length || 1);
80 | } else {
81 | input.x = 0;
82 | input.y = 0;
83 | }
84 | },
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/benches/apps/add-remove/src/main.ts:
--------------------------------------------------------------------------------
1 | import { initStats } from '@app/bench-tools';
2 | import { CONSTANTS, schedule, world } from '@sim/add-remove';
3 | import * as THREE from 'three';
4 | import { scene } from './scene';
5 | import './styles.css';
6 | import { init } from './systems/init';
7 | import { syncThreeObjects } from './systems/syncThreeObjects';
8 |
9 | // Renderer
10 | const renderer = new THREE.WebGLRenderer({
11 | antialias: true,
12 | powerPreference: 'high-performance',
13 | });
14 | renderer.setSize(window.innerWidth, window.innerHeight);
15 | document.body.appendChild(renderer.domElement);
16 |
17 | // Camera
18 | const frustumSize = 500;
19 | const aspect = window.innerWidth / window.innerHeight;
20 | const camera = new THREE.OrthographicCamera(
21 | (-frustumSize * aspect) / 2,
22 | (frustumSize * aspect) / 2,
23 | frustumSize / 2,
24 | -frustumSize / 2,
25 | 0.1,
26 | 500
27 | );
28 |
29 | // Set the floor to the bottom of the screen
30 | CONSTANTS.FLOOR = -frustumSize / 2;
31 |
32 | function onWindowResize() {
33 | const aspect = window.innerWidth / window.innerHeight;
34 |
35 | camera.left = (-frustumSize * aspect) / 2;
36 | camera.right = (frustumSize * aspect) / 2;
37 | camera.top = frustumSize / 2;
38 | camera.bottom = -frustumSize / 2;
39 | camera.updateProjectionMatrix();
40 |
41 | renderer.setSize(window.innerWidth, window.innerHeight);
42 | }
43 |
44 | window.addEventListener('resize', onWindowResize);
45 |
46 | // Camera position
47 | camera.position.set(0, 0, 100);
48 | camera.lookAt(0, 0, 0);
49 |
50 | schedule.add(init, { tag: 'init' });
51 | schedule.add(syncThreeObjects, { after: 'update' });
52 | schedule.build();
53 |
54 | // Init stats
55 | const { updateStats, measure, create } = initStats({
56 | Bodies: () => CONSTANTS.BODIES,
57 | 'Max comps per entity': () => CONSTANTS.MAX_COMPS_PER_ENTITY,
58 | Drain: () => CONSTANTS.DRAIN,
59 | });
60 | create();
61 |
62 | // Run the simulation
63 | const main = () => {
64 | measure(() => {
65 | schedule.run({ world });
66 | renderer.render(scene, camera);
67 | updateStats();
68 | });
69 | requestAnimationFrame(main);
70 | };
71 |
72 | requestAnimationFrame(main);
73 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/use-trait.ts:
--------------------------------------------------------------------------------
1 | import { $internal, type Entity, type Trait, type TraitRecord, type World } from '@koota/core';
2 | import { useEffect, useMemo, useState } from 'react';
3 | import { isWorld } from '../utils/is-world';
4 | import { useWorld } from '../world/use-world';
5 |
6 | export function useTrait(
7 | target: Entity | World | undefined | null,
8 | trait: T
9 | ): TraitRecord | undefined {
10 | // Get the world from context -- it may be used.
11 | // Note: With React 19 we can get it with use conditionally.
12 | const contextWorld = useWorld();
13 |
14 | // Memoize the target entity and a subscriber function.
15 | // If the target is undefined or null, undefined is returned here so the hook can exit early.
16 | const memo = useMemo(
17 | () => (target ? createSubscriptions(target, trait, contextWorld) : undefined),
18 | [target, trait, contextWorld]
19 | );
20 |
21 | // Initialize the state with the current value of the trait.
22 | const [value, setValue] = useState | undefined>(() => {
23 | return memo?.entity.has(trait) ? memo?.entity.get(trait) : undefined;
24 | });
25 |
26 | // Subscribe to changes in the trait.
27 | useEffect(() => {
28 | if (!memo) {
29 | setValue(undefined);
30 | return;
31 | }
32 | const unsubscribe = memo.subscribe(setValue);
33 | return () => unsubscribe();
34 | }, [memo]);
35 |
36 | return value;
37 | }
38 |
39 | function createSubscriptions(target: Entity | World, trait: T, contextWorld: World) {
40 | const world = isWorld(target) ? target : contextWorld;
41 | const entity = isWorld(target) ? target[$internal].worldEntity : target;
42 |
43 | return {
44 | entity,
45 | subscribe: (setValue: (value: TraitRecord | undefined) => void) => {
46 | const onChangeUnsub = world.onChange(trait, (e) => {
47 | if (e === entity) setValue(e.get(trait));
48 | });
49 |
50 | const onAddUnsub = world.onAdd(trait, (e) => {
51 | if (e === entity) setValue(e.get(trait));
52 | });
53 |
54 | const onRemoveUnsub = world.onRemove(trait, (e) => {
55 | if (e === entity) setValue(undefined);
56 | });
57 |
58 | setValue(entity.has(trait) ? entity.get(trait) : undefined);
59 |
60 | return () => {
61 | onChangeUnsub();
62 | onAddUnsub();
63 | onRemoveUnsub();
64 | };
65 | },
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export { createActions } from './actions/create-actions';
2 | export type { Actions, ActionsInitializer, ActionRecord } from './actions/types';
3 | export { $internal } from './common';
4 | export type { Entity } from './entity/types';
5 | export { unpackEntity } from './entity/utils/pack-entity';
6 | export { createAdded } from './query/modifiers/added';
7 | export { createChanged } from './query/modifiers/changed';
8 | export { Not } from './query/modifiers/not';
9 | export { Or } from './query/modifiers/or';
10 | export { createRemoved } from './query/modifiers/removed';
11 | export { $modifier } from './query/modifier';
12 | export { createQuery, IsExcluded } from './query/query';
13 | export type {
14 | EventType,
15 | InstancesFromParameters,
16 | IsNotModifier,
17 | Modifier,
18 | Query,
19 | QueryModifier,
20 | QueryParameter,
21 | QueryResult,
22 | QueryResultOptions,
23 | QuerySubscriber,
24 | QueryUnsubscriber,
25 | QueryHash,
26 | StoresFromParameters,
27 | } from './query/types';
28 | export { $queryRef } from './query/symbols';
29 | export { relation } from './relation/relation';
30 | export { $relationPair, $relation } from './relation/symbols';
31 | export type { Relation, RelationPair, RelationTarget } from './relation/types';
32 | export { getStore, trait } from './trait/trait';
33 | export type {
34 | ConfigurableTrait,
35 | ExtractIsTag,
36 | ExtractSchema,
37 | ExtractStore,
38 | IsTag,
39 | SetTraitCallback,
40 | TagTrait,
41 | Trait,
42 | TraitRecord,
43 | TraitTuple,
44 | TraitValue,
45 | } from './trait/types';
46 | export type { AoSFactory, Norm, Schema, Store, StoreType } from './storage/types';
47 | export type { TraitType } from './trait/types';
48 | export { universe } from './universe/universe';
49 | export type { World, WorldOptions } from './world';
50 | export { createWorld } from './world';
51 |
52 | /**
53 | * Deprecations. To be removed in v0.7.0.
54 | */
55 |
56 | import { createQuery } from './query/query';
57 | /** @deprecated Use createQuery instead */
58 | export const cacheQuery = createQuery;
59 |
60 | import type { TraitInstance } from './trait/types';
61 | /** @deprecated Use TraitInstance instead */
62 | export type TraitData = TraitInstance;
63 |
64 | /** @deprecated Will remove this internal type entirely */
65 | export type { TraitInstance } from './trait/types';
66 |
67 | /** @deprecated Will remove this internal type entirely */
68 | export type { QueryInstance } from './query/types';
69 |
--------------------------------------------------------------------------------
/benches/apps/boids/src/main.ts:
--------------------------------------------------------------------------------
1 | import { initStats } from '@app/bench-tools';
2 | import { actions, CONFIG, schedule, world } from '@sim/boids';
3 | import { trait } from 'koota';
4 | import * as THREE from 'three';
5 | import { scene } from './scene';
6 | import './styles.css';
7 | import { init } from './systems/init';
8 | import { render } from './systems/render';
9 | import { syncThreeObjects } from './systems/syncThreeObjects';
10 |
11 | // Renderer
12 | export const renderer = new THREE.WebGLRenderer({
13 | antialias: true,
14 | powerPreference: 'high-performance',
15 | });
16 | renderer.setSize(window.innerWidth, window.innerHeight);
17 | document.body.appendChild(renderer.domElement);
18 |
19 | // Camera
20 | const aspect = window.innerWidth / window.innerHeight;
21 | export const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
22 |
23 | // Camera position
24 | camera.position.set(0, 0, 100);
25 | camera.lookAt(0, 0, 0);
26 |
27 | function updateAvoidEdgesDistance() {
28 | const distance = camera.position.z;
29 | const vFov = (camera.fov * Math.PI) / 180;
30 | const visibleHeight = 2 * distance * Math.tan(vFov / 2);
31 | const visibleWidth = visibleHeight * camera.aspect;
32 | // Use the smaller dimension with some padding
33 | CONFIG.avoidEdgesMaxDistance = (Math.min(visibleWidth, visibleHeight) / 2) * 0.9;
34 | }
35 |
36 | function onWindowResize() {
37 | camera.aspect = window.innerWidth / window.innerHeight;
38 | camera.updateProjectionMatrix();
39 | renderer.setSize(window.innerWidth, window.innerHeight);
40 | updateAvoidEdgesDistance();
41 | }
42 |
43 | window.addEventListener('resize', onWindowResize);
44 | updateAvoidEdgesDistance();
45 |
46 | // Add view systems to the schedule
47 | schedule.add(syncThreeObjects, { after: 'update' });
48 | schedule.build();
49 |
50 | // Add Three resources to the world
51 | export const Three = trait(() => ({ renderer, camera, scene }));
52 | world.add(Three);
53 |
54 | // Init stats
55 | const { updateStats, measure, create } = initStats({ Boids: () => CONFIG.initialCount });
56 | create();
57 |
58 | // Init the scene
59 | init({ world });
60 |
61 | // Spawn the initial boids
62 | const { spawnBoid } = actions(world);
63 | for (let i = 0; i < CONFIG.initialCount; i++) {
64 | spawnBoid();
65 | }
66 |
67 | // Run the simulation
68 | const main = async () => {
69 | measure(async () => {
70 | schedule.run({ world });
71 | render({ world });
72 | updateStats();
73 | });
74 | requestAnimationFrame(main);
75 | };
76 |
77 | requestAnimationFrame(main);
78 |
--------------------------------------------------------------------------------
/packages/core/architecture.md:
--------------------------------------------------------------------------------
1 | A work in progress document for the architecture of Koota.
2 |
3 | Koota allows for many worlds. To make this experience simple there are global, stateless **refs** that get lazily **instantiated** on a world whenever it is used. Examples of this are:
4 |
5 | - Traits
6 | - Relations
7 | - Queries
8 | - Actions
9 | - Tracking modifiers
10 |
11 | A world is the context and holds the underlying storage, manages entities and the general lifecycle for data changes. Refs get instantiated on a world and use the id as a key for its instance.
12 |
13 | Traits are a user-facing handle for storage. The user never interacts with stores directly and isntead deals with the mental model of traits -- composable pieces of semantic data.
14 |
15 | ## Glossary
16 |
17 | **Ref.** A stateless, global definition returned by factory functions (`trait()`, `relation()`, `createQuery()`, `createActions()`). Refs are world-agnostic and contain only definition data (schema, configuration) plus a unique ID for fast lookups. Users interact primarily with refs. Since the user is not aware of internals like instances and only see the ref, the ref type is usually named as the target concept, such as `Trait` or `Query`.
18 |
19 | **Instance.** Per-world state created from a ref. Contains world-specific data like stores, subscriptions, query results, and bitmasks. Examples: `TraitInstance`, `QueryInstance`. Instances are internal — users don't interact with them directly.
20 |
21 | **Register.** The process of creating an instance for a ref on a world. Happens lazily on first use. Allocates storage, sets up bitmasks, and integrates with the world's query system.
22 |
23 | **Create.** The verb used for all factory functions. `create*` functions return refs (`createQuery`, `createActions`, `createAdded`) or instances (`createWorld`). The primitives `trait()` and `relation()` omit the verb for brevity. We used to use `define*` to differentiate creating a ref and creating an instance, but we now juse use `create*` in all cases and try to make this process hidden from the user.
24 |
25 | **World.** The context that holds all per-world state. Contains storage, trait instances, query instances, action instances, and manages the lifecycle of data changes.
26 |
27 | **Schema.** The shape definition for trait data. Can be SoA (struct of arrays), AoS (array of structs via factory function), or empty (tag trait).
28 |
29 | **Store.** The actual per-world storage for trait data, created from a schema. SoA stores have one array per property; AoS stores have one array of objects.
30 |
--------------------------------------------------------------------------------
/packages/core/src/query/utils/check-query-tracking.ts:
--------------------------------------------------------------------------------
1 | import { $internal } from '../../common';
2 | import { Entity } from '../../entity/types';
3 | import { getEntityId } from '../../entity/utils/pack-entity';
4 | import { World } from '../../world';
5 | import { EventType, QueryInstance } from '../types';
6 |
7 | /**
8 | * Check if an entity matches a tracking query with event handling.
9 | */
10 | export function checkQueryTracking(
11 | world: World,
12 | query: QueryInstance,
13 | entity: Entity,
14 | eventType: EventType,
15 | eventGenerationId: number,
16 | eventBitflag: number
17 | ): boolean {
18 | const { bitmasks, generations } = query;
19 | const ctx = world[$internal];
20 | const eid = getEntityId(entity);
21 |
22 | if (query.traitInstances.all.length === 0) return false;
23 |
24 | for (let i = 0; i < generations.length; i++) {
25 | const generationId = generations[i];
26 | const bitmask = bitmasks[i];
27 | const { required, forbidden, or, added, removed, changed } = bitmask;
28 | const entityMask = ctx.entityMasks[generationId][eid];
29 |
30 | if (!forbidden && !required && !or && !removed && !added && !changed) {
31 | return false;
32 | }
33 |
34 | // Handle events only for matching generation
35 | if (eventGenerationId === generationId) {
36 | if (eventType === 'add') {
37 | if (removed & eventBitflag) return false;
38 | if (added & eventBitflag) {
39 | bitmask.addedTracker[eid] |= eventBitflag;
40 | }
41 | } else if (eventType === 'remove') {
42 | if (added & eventBitflag) return false;
43 | if (removed & eventBitflag) {
44 | bitmask.removedTracker[eid] |= eventBitflag;
45 | }
46 | if (changed & eventBitflag) return false;
47 | } else if (eventType === 'change') {
48 | if (!(entityMask & eventBitflag)) return false;
49 | if (changed & eventBitflag) {
50 | bitmask.changedTracker[eid] |= eventBitflag;
51 | }
52 | }
53 | }
54 |
55 | // Check forbidden traits
56 | if ((entityMask & forbidden) !== 0) return false;
57 |
58 | // Check required traits
59 | if ((entityMask & required) !== required) return false;
60 |
61 | // Check Or traits
62 | if (or !== 0 && (entityMask & or) === 0) return false;
63 |
64 | // Check tracking masks only for matching generation
65 | if (eventGenerationId === generationId) {
66 | if (added) {
67 | const entityAddedTracker = bitmask.addedTracker[eid] || 0;
68 | if ((entityAddedTracker & added) !== added) return false;
69 | }
70 | if (removed) {
71 | const entityRemovedTracker = bitmask.removedTracker[eid] || 0;
72 | if ((entityRemovedTracker & removed) !== removed) return false;
73 | }
74 | if (changed) {
75 | const entityChangedTracker = bitmask.changedTracker[eid] || 0;
76 | if ((entityChangedTracker & changed) !== changed) return false;
77 | }
78 | }
79 | }
80 |
81 | return true;
82 | }
83 |
--------------------------------------------------------------------------------