Files
suaveplan/openspec/archive/2026-05-19-enterprise-gaming-ecs/proposal.md
2026-05-27 23:29:53 +01:00

194 lines
5.6 KiB
Markdown

# 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
```ts
// 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
```ts
type EcsErrorCode =
| 'ECS_ENTITY_NOT_FOUND'
| 'ECS_COMPONENT_NOT_REGISTERED'
| 'ECS_MAX_ENTITIES_EXCEEDED'
| 'ECS_INVALID_QUERY'
| 'ECS_WORLD_DESTROYED'
```
---
## New additions for genesis
1. **Bitset queries via BigInt masks** — archetype mask stored as `bigint`; AND/NOT/ANY evaluated with bitwise ops for O(1) archetype filtering.
2. **Typed array component storage** — Float32Array/Int32Array backing per numeric component type; fallback to generic Map for complex structs.
3. **OTel instrumentation** — `ecs.tick` span with attribute `ecs.system_count`; `ecs.query` histogram for query execution time; `ecs.entities_alive` gauge.
4. **Reactive entity events** — `@suaveplan/events` bus fires `ecs:spawn`, `ecs:despawn`, `ecs:component:add`, `ecs:component:remove`.
5. **Property-based tests** — fast-check: entity count monotonicity, query result consistency with exhaustive scan, archetype mask algebra.
6. **Benchmark suite** — `__benchmarks__/ecs.bench.ts`: 10k entity spawn, 100k query iteration, 60fps tick with 10 systems.
---
## File layout
```text
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.