Docs/@katforge/anvil

@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.

shell
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

TypeScript
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

TypeScript
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

TypeScript
// 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

TypeScript
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

TypeScript
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.