├── 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 | --------------------------------------------------------------------------------