5.6 KiB
OpenSpec Change: enterprise-gaming-ecs
- Change ID: enterprise-gaming-ecs
- Package:
@suaveplan/ecs - Category: gaming/engine
- Target path:
gaming/engine/ecs - Status: PROPOSED
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/ecs.md
Summary
Deliver @suaveplan/ecs on the genesis branch from scratch at enterprise quality. The package provides an archetype-based Entity Component System with bitset queries — the foundational runtime for all gameplay systems. Entities are integer IDs; components are plain typed structs stored in contiguous typed arrays per archetype; queries filter archetypes by bitset AND/NOT masks for O(1) iteration. Genesis adds structured errors via @suaveplan/error, Zod schemas for all component descriptors, OTel span instrumentation on world tick and query execution, injectable clock and RNG, AbortSignal for async tick cancellation, and property-based tests with fast-check.
Current-state audit (genesis)
Package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/error (workspace:^) |
Base EcsError; stable ECS.* error codes |
@suaveplan/types (workspace:^) |
Clock, IdGenerator, LoggerLike |
@suaveplan/events (workspace:^) |
Entity lifecycle events (spawn, despawn, component added/removed) |
zod (catalog:forms) |
Component descriptor schemas, query option schemas |
@opentelemetry/api (catalog:observability) |
Spans on world.tick, query.run, world.spawn |
Public API surface
Core
// World factory
function createWorld(options?: WorldOptions): World
interface WorldOptions {
maxEntities?: number // default 65536
clock?: Clock
logger?: LoggerLike
}
interface World {
spawn(components?: ComponentInit[]): Entity
despawn(entity: Entity): void
addComponent<T>(entity: Entity, descriptor: ComponentDescriptor<T>, value: T): void
removeComponent(entity: Entity, type: ComponentType): void
hasComponent(entity: Entity, type: ComponentType): boolean
getComponent<T>(entity: Entity, descriptor: ComponentDescriptor<T>): T | undefined
query(mask: QueryMask): QueryResult
tick(dt: number, signal?: AbortSignal): void
registerSystem(system: System): void
destroy(): void
}
type Entity = number & { readonly __brand: 'Entity' }
// Component descriptor registry
function defineComponent<T>(name: string, schema: ZodType<T>): ComponentDescriptor<T>
interface ComponentDescriptor<T> {
readonly type: ComponentType
readonly name: string
readonly schema: ZodType<T>
}
type ComponentType = number & { readonly __brand: 'ComponentType' }
// Query API
interface QueryMask {
all?: ComponentType[]
none?: ComponentType[]
any?: ComponentType[]
}
interface QueryResult {
entities: readonly Entity[]
size: number
forEach(cb: (entity: Entity) => void): void
}
// System API
interface System {
readonly name: string
update(world: World, dt: number): void
}
// Archetype internals (exposed for advanced consumers)
interface Archetype {
readonly mask: bigint
readonly entities: readonly Entity[]
readonly size: number
}
Error codes
type EcsErrorCode =
| 'ECS_ENTITY_NOT_FOUND'
| 'ECS_COMPONENT_NOT_REGISTERED'
| 'ECS_MAX_ENTITIES_EXCEEDED'
| 'ECS_INVALID_QUERY'
| 'ECS_WORLD_DESTROYED'
New additions for genesis
- Bitset queries via BigInt masks — archetype mask stored as
bigint; AND/NOT/ANY evaluated with bitwise ops for O(1) archetype filtering. - Typed array component storage — Float32Array/Int32Array backing per numeric component type; fallback to generic Map for complex structs.
- OTel instrumentation —
ecs.tickspan with attributeecs.system_count;ecs.queryhistogram for query execution time;ecs.entities_alivegauge. - Reactive entity events —
@suaveplan/eventsbus firesecs:spawn,ecs:despawn,ecs:component:add,ecs:component:remove. - Property-based tests — fast-check: entity count monotonicity, query result consistency with exhaustive scan, archetype mask algebra.
- Benchmark suite —
__benchmarks__/ecs.bench.ts: 10k entity spawn, 100k query iteration, 60fps tick with 10 systems.
File layout
gaming/engine/ecs/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
common.ts
common.md
core/
world/
world.ts
world.md
world.types.ts
world.test.ts
index.ts
archetype/
archetype.ts
archetype.md
archetype.types.ts
archetype.test.ts
index.ts
component/
component.ts
component.md
component.types.ts
component.test.ts
index.ts
query/
query.ts
query.md
query.types.ts
query.test.ts
index.ts
system/
system.ts
system.md
system.types.ts
system.test.ts
index.ts
utils/
bitset.ts
bitset.md
bitset.test.ts
__benchmarks__/
ecs.bench.ts
tests/
integration.test.ts
property.test.ts
Impact
Downstream: @suaveplan/game-loop orchestrates world ticks; @suaveplan/r3f-* packages read ECS component state for rendering. No existing genesis package currently depends on it.
Breaking changes from main: none — fresh build.
Prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events published to Verdaccio.