@katforge/anvil
An ECS (Entity-Component-System) library. Entities are plain integers. Components are typed data attached to entities via decorators. Systems query and mutate component data. Used by Gear Goblins for all game state.
npm install @katforge/anvil
Core concepts
flowchart LR
World["World"]
E["Entity (int)"]
C["Component (typed data)"]
Q["Query (filter)"]
S["System (logic)"]
World -->|spawn| E
E -->|attach| C
Q -->|match| E
S -->|read/write| Q
World -->|tick| S
Define components
import { int, string, float, boolean, object } from '@katforge/anvil';
class Position {
@float x = 0;
@float y = 0;
}
class Health {
@int current = 100;
@int max = 100;
}
class Name {
@string value = '';
}
class Inventory {
@object items = [];
}
Component decorators (@int, @float, @string, @boolean, @object) define the storage layout. Typed columns enable batch iteration without boxing.
Create a world
import { World } from '@katforge/anvil';
const world = new World ();
// Spawn an entity with components
const player = world.spawn (
new Position (),
new Health ({ current: 80, max: 100 }),
new Name ({ value: 'Grimlock' })
);
Query
// All entities with Position AND Health
const q = world.query (Position, Health);
for (const [ pos, hp ] of q) {
pos.x += 1;
if (hp.current <= 0) { /* ... */ }
}
Queries are reactive: adding or removing a component from an entity automatically updates every matching query.
Lifecycle observers
import { Added, Removed, Changed } from '@katforge/anvil';
// React to new entities entering a query
world.observe (Added, Position, (entity, pos) => {
console.log ('new positioned entity', entity);
});
Relations
const ChildOf = world.relation ();
const parent = world.spawn ();
const child = world.spawn ();
world.relate (child, ChildOf, parent);
// Query all children of a specific parent
const children = world.query (ChildOf, parent);
Why ECS
Traditional OOP hierarchies (Player extends Character extends Entity) break down when cross-cutting concerns pile up. ECS composes behavior via data: an entity is just an integer; its capabilities are determined by which components are attached. Adding "poisoned" to a character is world.attach (entity, new Poisoned ()), not a class inheritance change.