Skip to content

Commit c24159d

Browse files
authored
✨ ai: add koota skill
* πŸŽ‰ ai: add koota skill * πŸ“ ai: add skill/readme note
1 parent 6386f27 commit c24159d

File tree

8 files changed

+1954
-0
lines changed

8 files changed

+1954
-0
lines changed

β€ŽAGENTS.mdβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
This monorepo uses `pnpm`.
2+
3+
IMPORTANT: Always use kebab-case for file names, even if it is not the usual convention.
4+
5+
The `skills/koota` directory contains a skill with reference documentation for the koota project. When updating the README, the skill should be updated as well to keep documentation in sync. Follow best practices for agewt skill.

β€Žskills/koota/SKILL.mdβ€Ž

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
---
2+
name: koota
3+
description: Real-time ECS state management for TypeScript and React. Use when the user mentions koota, ECS, entities, traits, queries, or building data-oriented applications.
4+
---
5+
6+
# Koota ECS
7+
8+
Koota manages state using entities with composable traits.
9+
10+
## Glossary
11+
12+
- **Entity** - A unique identifier pointing to data defined by traits. Spawned from a world.
13+
- **Trait** - A reusable data definition. Can be schema-based (SoA), callback-based (AoS), or a tag.
14+
- **Relation** - A directional connection between entities to build graphs.
15+
- **World** - The context for all entities and their data (traits).
16+
- **Archetype** - A unique combination of traits that entities share.
17+
- **Query** - Fetches entities matching an archetype. The primary way to batch update state.
18+
19+
## Design Principles
20+
21+
### Data-oriented
22+
23+
Behavior is separated from data. Data is defined as traits, entities compose traits, and systems mutate data on traits via queries. See [Basic usage](#basic-usage) for a complete example.
24+
25+
### Composable systems
26+
27+
Design systems as small, single-purpose units rather than monolithic functions that do everything in sequence. Each system should handle one concern so that behaviors can be toggled on/off independently.
28+
29+
```typescript
30+
// Good: Composable systems - each can be enabled/disabled independently
31+
function applyVelocity(world: World) {}
32+
function applyGravity(world: World) {}
33+
function applyFriction(world: World) {}
34+
function syncToDOM(world: World) {}
35+
36+
// Bad: Monolithic system - can't disable gravity without disabling everything
37+
function updatePhysicsAndRender(world: World) {
38+
// velocity, gravity, friction, DOM sync all in one function
39+
}
40+
```
41+
42+
This enables feature flags, debugging (disable one system to isolate issues), and flexible runtime configurations.
43+
44+
### Decouple view from logic
45+
46+
Separate core state and logic (the "core") from the view ("app"):
47+
48+
- Run logic independent of rendering
49+
- Swap views while keeping state (2D ↔ 3D)
50+
- Run logic in a worker or on a server
51+
52+
### Prefer traits + actions over classes
53+
54+
Prefer not to use classes to encapsulate data and behavior. Use traits for data and actions for behavior. Only use classes when required by external libraries (e.g., THREE.js objects) or the user prefers it.
55+
56+
## Directory structure
57+
58+
If the user has a preferred structure, follow it. Otherwise, use this guidance: the directory structure should mirror how the app's data model is organized. Separate core state/logic from the view layer:
59+
60+
- **Core** - Pure TypeScript. Traits, systems, actions, world. No view imports.
61+
- **View** - Reads from world, mutates via actions. Organized by domain/feature.
62+
63+
```
64+
src/
65+
β”œβ”€β”€ core/ # Pure TypeScript, no view imports
66+
β”‚ β”œβ”€β”€ traits/
67+
β”‚ β”œβ”€β”€ systems/
68+
β”‚ β”œβ”€β”€ actions/
69+
β”‚ └── world.ts
70+
└── features/ # View layer, organized by domain
71+
```
72+
73+
Files are organized by role, not by feature slice. Traits and systems are composable and don't map cleanly to features.
74+
75+
For detailed patterns and monorepo structures, see [references/architecture.md](references/architecture.md).
76+
77+
## Trait types
78+
79+
| Type | Syntax | Use when | Examples |
80+
| ------------------ | -------------------------- | ------------------------- | -------------------------------- |
81+
| **SoA (Schema)** | `trait({ x: 0 })` | Simple primitive data | `Position`, `Velocity`, `Health` |
82+
| **AoS (Callback)** | `trait(() => new Thing())` | Complex objects/instances | `Ref` (DOM), `Keyboard` (Set) |
83+
| **Tag** | `trait()` | No data, just a flag | `IsPlayer`, `IsEnemy`, `IsDead` |
84+
85+
## Trait naming conventions
86+
87+
| Type | Pattern | Examples |
88+
| ------------- | --------------- | -------------------------------- |
89+
| **Tags** | Start with `Is` | `IsPlayer`, `IsEnemy`, `IsDead` |
90+
| **Relations** | Prepositional | `ChildOf`, `HeldBy`, `Contains` |
91+
| **Trait** | Noun | `Position`, `Velocity`, `Health` |
92+
93+
## Relations
94+
95+
Relations build graphs between entities such as hierarchies, inventories, targeting.
96+
97+
```typescript
98+
import { relation } from 'koota'
99+
100+
const ChildOf = relation({ autoDestroy: 'orphan' }) // Hierarchy
101+
const Contains = relation({ store: { amount: 0 } }) // With data
102+
const Targeting = relation({ exclusive: true }) // One target only
103+
104+
// Build graph
105+
const parent = world.spawn()
106+
const child = world.spawn(ChildOf(parent))
107+
108+
// Query children of parent
109+
const children = world.query(ChildOf(parent))
110+
111+
// Query all entities with any ChildOf relation
112+
const allChildren = world.query(ChildOf('*'))
113+
114+
// Get targets from entity
115+
const items = entity.targetsFor(Contains) // Entity[]
116+
const target = entity.targetFor(Targeting) // Entity | undefined
117+
```
118+
119+
For detailed patterns, traversal, ordered relations, and anti-patterns, see [references/relations.md](references/relations.md).
120+
121+
## Basic usage
122+
123+
```typescript
124+
import { trait, createWorld } from 'koota'
125+
126+
// 1. Define traits
127+
const Position = trait({ x: 0, y: 0 })
128+
const Velocity = trait({ x: 0, y: 0 })
129+
const IsPlayer = trait()
130+
131+
// 2. Create world and spawn entities
132+
const world = createWorld()
133+
const player = world.spawn(Position({ x: 100, y: 50 }), Velocity, IsPlayer)
134+
135+
// 3. Query and update
136+
world.query(Position, Velocity).updateEach(([pos, vel]) => {
137+
pos.x += vel.x
138+
pos.y += vel.y
139+
})
140+
```
141+
142+
## Entities
143+
144+
Entities are unique identifiers that compose traits. Spawned from a world.
145+
146+
```typescript
147+
// Spawn
148+
const entity = world.spawn(Position, Velocity)
149+
150+
// Read/write traits
151+
entity.get(Position) // Read trait data
152+
entity.set(Position, { x: 10 }) // Write (triggers change events)
153+
entity.add(IsPlayer) // Add trait
154+
entity.remove(Velocity) // Remove trait
155+
entity.has(Position) // Check if has trait
156+
157+
// Destroy
158+
entity.destroy()
159+
```
160+
161+
**Entity IDs**
162+
163+
An entity is internally a number packed with entity ID, generation ID (for recycling), and world ID. Safe to store directly for persistence or networking.
164+
165+
```typescript
166+
entity.id() // Just the entity ID (reused after destroy)
167+
entity // Full packed number (unique forever)
168+
```
169+
170+
**Typing**
171+
172+
Use `TraitRecord` to get the type that `entity.get()` returns
173+
174+
```typescript
175+
type PositionRecord = TraitRecord<typeof Position>
176+
```
177+
178+
## Queries
179+
180+
Queries fetch entities matching an archetype and are the primary way to batch update state.
181+
182+
```typescript
183+
// Query and update
184+
world.query(Position, Velocity).updateEach(([pos, vel]) => {
185+
pos.x += vel.x
186+
pos.y += vel.y
187+
})
188+
189+
// Read-only iteration (no write-back)
190+
const data: Array<{ x: number; y: number }> = []
191+
world.query(Position, Velocity).readEach(([pos, vel]) => {
192+
data.push({ x: pos.x, y: pos.y })
193+
})
194+
195+
// Get first match
196+
const player = world.queryFirst(IsPlayer, Position)
197+
198+
// Filter with modifiers
199+
world.query(Position, Not(Velocity)) // Has Position but not Velocity
200+
world.query(Or(IsPlayer, IsEnemy)) // Has either trait
201+
```
202+
203+
**Note:** `updateEach`/`readEach` only return data-bearing traits (SoA/AoS). Tags, `Not()`, and relation filters are **excluded**:
204+
205+
```typescript
206+
world.query(IsPlayer, Position, Velocity).updateEach(([pos, vel]) => {
207+
// Array has 2 elements - IsPlayer (tag) excluded
208+
})
209+
```
210+
211+
For tracking changes, caching queries, and advanced patterns, see [references/queries.md](references/queries.md).
212+
213+
## React integration
214+
215+
**Imports:** Core types (`World`, `Entity`) from `'koota'`. React hooks from `'koota/react'`.
216+
217+
**Change detection:** `entity.set()` and `world.set()` trigger change events that cause hooks like `useTrait` to rerender. For AoS traits where you mutate objects directly, manually signal with `entity.changed(Trait)`.
218+
219+
For React hooks and actions, see [references/react-hooks.md](references/react-hooks.md).
220+
221+
For component patterns (App, Startup, Renderer, view sync, input), see [references/react-patterns.md](references/react-patterns.md).
222+
223+
## Runtime
224+
225+
Systems query the world and update entities. Run them via frameloop (continuous) or event handlers (discrete).
226+
227+
For systems, frameloop, event-driven patterns, and time management, see [references/runtime.md](references/runtime.md).
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Architecture and File Structure
2+
3+
## Core Principle
4+
5+
Decompose classes into traits (data) and actions (behavior) unless there is a specific reason to use a class.
6+
7+
## Conventions Override
8+
9+
Examples below use common conventions. Always follow the user's stated preferences or existing codebase conventions (file naming, casing, structure) over these examples.
10+
11+
## Standard Structure
12+
13+
Separate core (pure TypeScript) from view (React/framework code):
14+
15+
```
16+
src/
17+
β”œβ”€β”€ core/ # Pure TypeScript, ECS with Koota
18+
β”‚ β”œβ”€β”€ traits/
19+
β”‚ β”œβ”€β”€ systems/
20+
β”‚ β”œβ”€β”€ actions/
21+
β”‚ └── world.ts
22+
β”‚
23+
β”œβ”€β”€ features/ # View layer, organized by domain
24+
β”‚ β”œβ”€β”€ enemies/
25+
β”‚ β”œβ”€β”€ terrain/
26+
β”‚ └── ui/
27+
β”‚
28+
β”œβ”€β”€ utils/ # Generic, reusable
29+
β”‚
30+
β”œβ”€β”€ App.tsx
31+
└── main.tsx # Entry point
32+
```
33+
34+
## Organize by Role, Not Feature
35+
36+
Organize `core/` by role (traits, systems, actions), not by feature slice. Traits and systems are composable across features.
37+
38+
## Data modeling
39+
40+
**Prefer multiple entities over array traits.** Instead of one entity with a flat array of objects, spawn many entities with shared traits.
41+
42+
```typescript
43+
// ❌ Singleton with array β€” harder to query, compose, and extend
44+
const Inventory = trait(() => ({ items: [] as { id: string; count: number }[] }))
45+
const inventory = world.spawn(Inventory)
46+
47+
// βœ… Multiple entities β€” queryable, composable, per-item traits
48+
const Item = trait({ id: '', count: 0 })
49+
const IsInInventory = trait()
50+
world.spawn(Item({ id: 'sword', count: 1 }), IsInInventory)
51+
world.spawn(Item({ id: 'potion', count: 5 }), IsInInventory)
52+
53+
// Query all items
54+
world.query(Item, IsInInventory)
55+
```
56+
57+
**Why multiple entities:**
58+
59+
- **Queryable** β€” filter, sort, iterate with `query()`
60+
- **Composable** β€” add traits per-item (e.g., `IsEquipped`, `IsDamaged`)
61+
- **Extensible** β€” new behaviors without changing existing traits
62+
- **Reactive** β€” React hooks work per-entity, not per-array-element
63+
- **Graphs** β€” use relations to connect entities (e.g., `ChildOf`, `Contains`, `DependsOn`)
64+
65+
## Detailed Example
66+
67+
```
68+
src/
69+
β”œβ”€β”€ core/
70+
β”‚ β”œβ”€β”€ traits/
71+
β”‚ β”‚ β”œβ”€β”€ position.ts
72+
β”‚ β”‚ β”œβ”€β”€ health.ts
73+
β”‚ β”‚ β”œβ”€β”€ velocity.ts
74+
β”‚ β”‚ β”œβ”€β”€ terrain.ts
75+
β”‚ β”‚ └── index.ts
76+
β”‚ β”‚
77+
β”‚ β”œβ”€β”€ systems/
78+
β”‚ β”‚ β”œβ”€β”€ updatePhysics.ts
79+
β”‚ β”‚ β”œβ”€β”€ updateDamage.ts
80+
β”‚ β”‚ └── index.ts
81+
β”‚ β”‚
82+
β”‚ β”œβ”€β”€ actions/
83+
β”‚ β”‚ β”œβ”€β”€ sceneActions.ts
84+
β”‚ β”‚ β”œβ”€β”€ combatActions.ts
85+
β”‚ β”‚ └── index.ts
86+
β”‚ β”‚
87+
β”‚ └── world.ts
88+
β”‚
89+
β”œβ”€β”€ features/
90+
β”‚ β”œβ”€β”€ enemies/
91+
β”‚ β”‚ β”œβ”€β”€ EnemyRenderer.tsx
92+
β”‚ β”‚ └── EnemyView.tsx
93+
β”‚ β”‚
94+
β”‚ β”œβ”€β”€ terrain/
95+
β”‚ β”‚ β”œβ”€β”€ TerrainRenderer.tsx
96+
β”‚ β”‚ └── TerrainTile.tsx
97+
β”‚ β”‚
98+
β”‚ └── player/
99+
β”‚ β”œβ”€β”€ PlayerRenderer.tsx
100+
β”‚ └── PlayerView.tsx
101+
β”‚
102+
β”œβ”€β”€ utils/
103+
β”‚
104+
β”œβ”€β”€ App.tsx
105+
└── main.tsx
106+
```
107+
108+
## Monorepo Structure
109+
110+
Use when core needs to run independently (workers, servers, CLI) or with multiple views:
111+
112+
```
113+
my-app/
114+
β”œβ”€β”€ packages/
115+
β”‚ β”œβ”€β”€ core/
116+
β”‚ β”‚ β”œβ”€β”€ src/
117+
β”‚ β”‚ β”‚ β”œβ”€β”€ traits/
118+
β”‚ β”‚ β”‚ β”œβ”€β”€ systems/
119+
β”‚ β”‚ β”‚ β”œβ”€β”€ actions/
120+
β”‚ β”‚ β”‚ └── world.ts
121+
β”‚ β”‚ └── package.json β†’ @my-app/core
122+
β”‚ β”‚
123+
β”‚ └── react/
124+
β”‚ β”œβ”€β”€ src/
125+
β”‚ β”‚ β”œβ”€β”€ hooks/
126+
β”‚ β”‚ └── index.ts
127+
β”‚ └── package.json β†’ @my-app/react
128+
β”‚
129+
β”œβ”€β”€ apps/
130+
β”‚ β”œβ”€β”€ editor/ β†’ imports @my-app/core, @my-app/react
131+
β”‚ β”œβ”€β”€ cli/ β†’ imports @my-app/core only
132+
β”‚ └── agent/ β†’ imports @my-app/core only
133+
β”‚
134+
└── pnpm-workspace.yaml
135+
```

0 commit comments

Comments
Β (0)