murow 0.0.31 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ import { Component } from "./component";
2
+ /**
3
+ * Stores component data for entities using typed arrays.
4
+ * Provides efficient packed storage with O(1) access by entity ID.
5
+ *
6
+ * Key optimizations:
7
+ * - ArrayBuffer storage (exact byte size, no waste)
8
+ * - Single reusable DataView (no allocations)
9
+ * - Single reusable object for get() (zero allocations)
10
+ * - Pre-computed field offsets (no runtime calculations)
11
+ */
12
+ export declare class ComponentStore<T extends object> {
13
+ private buffer;
14
+ private view;
15
+ private stride;
16
+ private component;
17
+ private maxEntities;
18
+ private reusableObject;
19
+ private fieldOffsets;
20
+ private fields;
21
+ private fieldKeys;
22
+ private fieldIndexMap;
23
+ constructor(component: Component<T>, maxEntities: number);
24
+ /**
25
+ * Get component data for an entity.
26
+ *
27
+ * ⚠️ IMPORTANT: Returns a REUSED object that is overwritten on the next get() call.
28
+ * Use immediately or copy the data. For safe access, use getMutable() or copyTo().
29
+ *
30
+ * @example
31
+ * // ✅ CORRECT: Use immediately
32
+ * const t = store.get(entity);
33
+ * console.log(t.x, t.y);
34
+ *
35
+ * // ❌ WRONG: Storing reference
36
+ * const t1 = store.get(entity1);
37
+ * const t2 = store.get(entity2); // t1 is now corrupted!
38
+ *
39
+ * // ✅ CORRECT: Copy if you need multiple
40
+ * const t1 = { ...store.get(entity1) };
41
+ * const t2 = { ...store.get(entity2) };
42
+ */
43
+ get(entityId: number): Readonly<T>;
44
+ /**
45
+ * Get a mutable copy of component data.
46
+ * Use this when you need to modify and keep the data.
47
+ *
48
+ * Note: This allocates a new object. Use sparingly in hot paths.
49
+ */
50
+ getMutable(entityId: number): T;
51
+ /**
52
+ * Copy component data into a provided object.
53
+ * Use this when you need to keep multiple components at once.
54
+ */
55
+ copyTo(entityId: number, target: T): void;
56
+ /**
57
+ * Set component data for an entity.
58
+ * Writes the data directly into the typed array.
59
+ */
60
+ set(entityId: number, data: T): void;
61
+ /**
62
+ * Update specific fields of a component without reading the whole component first.
63
+ * Optimized to only iterate over the fields being updated.
64
+ */
65
+ update(entityId: number, partial: Partial<T>): void;
66
+ /**
67
+ * Clear component data for an entity (set to default values)
68
+ */
69
+ clear(entityId: number): void;
70
+ /**
71
+ * Get direct access to the underlying buffer.
72
+ * Advanced use only - for SIMD operations, GPU uploads, zero-copy networking, etc.
73
+ */
74
+ getRawBuffer(): ArrayBuffer;
75
+ /**
76
+ * Get the stride in bytes.
77
+ */
78
+ getStride(): number;
79
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Stores component data for entities using typed arrays.
3
+ * Provides efficient packed storage with O(1) access by entity ID.
4
+ *
5
+ * Key optimizations:
6
+ * - ArrayBuffer storage (exact byte size, no waste)
7
+ * - Single reusable DataView (no allocations)
8
+ * - Single reusable object for get() (zero allocations)
9
+ * - Pre-computed field offsets (no runtime calculations)
10
+ */
11
+ export class ComponentStore {
12
+ constructor(component, maxEntities) {
13
+ this.component = component;
14
+ this.maxEntities = maxEntities;
15
+ // Use exact byte size (no waste like Float32Array)
16
+ this.stride = component.size;
17
+ // Allocate exact memory needed
18
+ this.buffer = new ArrayBuffer(maxEntities * this.stride);
19
+ this.view = new DataView(this.buffer);
20
+ // Pre-compute field metadata once
21
+ this.fieldKeys = component.fieldNames;
22
+ this.fieldOffsets = [];
23
+ this.fields = [];
24
+ this.fieldIndexMap = new Map();
25
+ let offset = 0;
26
+ for (let i = 0; i < this.fieldKeys.length; i++) {
27
+ const key = this.fieldKeys[i];
28
+ const field = component.schema[key];
29
+ this.fieldOffsets.push(offset);
30
+ this.fields.push(field);
31
+ this.fieldIndexMap.set(key, i);
32
+ offset += field.size;
33
+ }
34
+ // Create single reusable object
35
+ this.reusableObject = {};
36
+ for (let i = 0; i < this.fieldKeys.length; i++) {
37
+ this.reusableObject[this.fieldKeys[i]] = this.fields[i].toNil();
38
+ }
39
+ }
40
+ /**
41
+ * Get component data for an entity.
42
+ *
43
+ * ⚠️ IMPORTANT: Returns a REUSED object that is overwritten on the next get() call.
44
+ * Use immediately or copy the data. For safe access, use getMutable() or copyTo().
45
+ *
46
+ * @example
47
+ * // ✅ CORRECT: Use immediately
48
+ * const t = store.get(entity);
49
+ * console.log(t.x, t.y);
50
+ *
51
+ * // ❌ WRONG: Storing reference
52
+ * const t1 = store.get(entity1);
53
+ * const t2 = store.get(entity2); // t1 is now corrupted!
54
+ *
55
+ * // ✅ CORRECT: Copy if you need multiple
56
+ * const t1 = { ...store.get(entity1) };
57
+ * const t2 = { ...store.get(entity2) };
58
+ */
59
+ get(entityId) {
60
+ const baseOffset = entityId * this.stride;
61
+ const length = this.fields.length;
62
+ // Unrolled loop for common cases
63
+ if (length === 2) {
64
+ this.reusableObject[this.fieldKeys[0]] = this.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
65
+ this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
66
+ }
67
+ else if (length === 3) {
68
+ this.reusableObject[this.fieldKeys[0]] = this.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
69
+ this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
70
+ this.reusableObject[this.fieldKeys[2]] = this.fields[2].read(this.view, baseOffset + this.fieldOffsets[2]);
71
+ }
72
+ else if (length === 4) {
73
+ this.reusableObject[this.fieldKeys[0]] = this.fields[0].read(this.view, baseOffset + this.fieldOffsets[0]);
74
+ this.reusableObject[this.fieldKeys[1]] = this.fields[1].read(this.view, baseOffset + this.fieldOffsets[1]);
75
+ this.reusableObject[this.fieldKeys[2]] = this.fields[2].read(this.view, baseOffset + this.fieldOffsets[2]);
76
+ this.reusableObject[this.fieldKeys[3]] = this.fields[3].read(this.view, baseOffset + this.fieldOffsets[3]);
77
+ }
78
+ else {
79
+ // Generic loop for other sizes
80
+ for (let i = 0; i < length; i++) {
81
+ this.reusableObject[this.fieldKeys[i]] = this.fields[i].read(this.view, baseOffset + this.fieldOffsets[i]);
82
+ }
83
+ }
84
+ return this.reusableObject;
85
+ }
86
+ /**
87
+ * Get a mutable copy of component data.
88
+ * Use this when you need to modify and keep the data.
89
+ *
90
+ * Note: This allocates a new object. Use sparingly in hot paths.
91
+ */
92
+ getMutable(entityId) {
93
+ const copy = {};
94
+ this.copyTo(entityId, copy);
95
+ return copy;
96
+ }
97
+ /**
98
+ * Copy component data into a provided object.
99
+ * Use this when you need to keep multiple components at once.
100
+ */
101
+ copyTo(entityId, target) {
102
+ const baseOffset = entityId * this.stride;
103
+ for (let i = 0; i < this.fields.length; i++) {
104
+ target[this.fieldKeys[i]] = this.fields[i].read(this.view, baseOffset + this.fieldOffsets[i]);
105
+ }
106
+ }
107
+ /**
108
+ * Set component data for an entity.
109
+ * Writes the data directly into the typed array.
110
+ */
111
+ set(entityId, data) {
112
+ const baseOffset = entityId * this.stride;
113
+ const length = this.fields.length;
114
+ // Unrolled loop for common cases
115
+ if (length === 2) {
116
+ this.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
117
+ this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
118
+ }
119
+ else if (length === 3) {
120
+ this.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
121
+ this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
122
+ this.fields[2].write(this.view, baseOffset + this.fieldOffsets[2], data[this.fieldKeys[2]]);
123
+ }
124
+ else if (length === 4) {
125
+ this.fields[0].write(this.view, baseOffset + this.fieldOffsets[0], data[this.fieldKeys[0]]);
126
+ this.fields[1].write(this.view, baseOffset + this.fieldOffsets[1], data[this.fieldKeys[1]]);
127
+ this.fields[2].write(this.view, baseOffset + this.fieldOffsets[2], data[this.fieldKeys[2]]);
128
+ this.fields[3].write(this.view, baseOffset + this.fieldOffsets[3], data[this.fieldKeys[3]]);
129
+ }
130
+ else {
131
+ // Generic loop for other sizes
132
+ for (let i = 0; i < length; i++) {
133
+ this.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], data[this.fieldKeys[i]]);
134
+ }
135
+ }
136
+ }
137
+ /**
138
+ * Update specific fields of a component without reading the whole component first.
139
+ * Optimized to only iterate over the fields being updated.
140
+ */
141
+ update(entityId, partial) {
142
+ const baseOffset = entityId * this.stride;
143
+ // Fast path for single field update (90% of cases) - avoids Object.keys allocation
144
+ const keys = Object.keys(partial);
145
+ const keyCount = keys.length;
146
+ if (keyCount === 1) {
147
+ const key = keys[0];
148
+ const i = this.fieldIndexMap.get(key);
149
+ this.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], partial[key]);
150
+ return;
151
+ }
152
+ // Fast path for two field update (common for 2D positions)
153
+ if (keyCount === 2) {
154
+ const key0 = keys[0];
155
+ const key1 = keys[1];
156
+ const i0 = this.fieldIndexMap.get(key0);
157
+ const i1 = this.fieldIndexMap.get(key1);
158
+ this.fields[i0].write(this.view, baseOffset + this.fieldOffsets[i0], partial[key0]);
159
+ this.fields[i1].write(this.view, baseOffset + this.fieldOffsets[i1], partial[key1]);
160
+ return;
161
+ }
162
+ // Generic path for multiple fields
163
+ for (let j = 0; j < keyCount; j++) {
164
+ const key = keys[j];
165
+ const i = this.fieldIndexMap.get(key);
166
+ this.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], partial[key]);
167
+ }
168
+ }
169
+ /**
170
+ * Clear component data for an entity (set to default values)
171
+ */
172
+ clear(entityId) {
173
+ const baseOffset = entityId * this.stride;
174
+ for (let i = 0; i < this.fields.length; i++) {
175
+ this.fields[i].write(this.view, baseOffset + this.fieldOffsets[i], this.fields[i].toNil());
176
+ }
177
+ }
178
+ /**
179
+ * Get direct access to the underlying buffer.
180
+ * Advanced use only - for SIMD operations, GPU uploads, zero-copy networking, etc.
181
+ */
182
+ getRawBuffer() {
183
+ return this.buffer;
184
+ }
185
+ /**
186
+ * Get the stride in bytes.
187
+ */
188
+ getStride() {
189
+ return this.stride;
190
+ }
191
+ }
@@ -0,0 +1,48 @@
1
+ import { Schema } from "../core/binary-codec";
2
+ import { ArrayField } from "../core/pooled-codec";
3
+ /**
4
+ * Metadata for a component definition
5
+ */
6
+ export interface ComponentMeta<T extends object> {
7
+ /** Schema defining the component's binary layout */
8
+ schema: Schema<T>;
9
+ /** Unique name for this component type */
10
+ name: string;
11
+ /** Size of the component in bytes */
12
+ size: number;
13
+ /** Number of fields in the schema */
14
+ fieldCount: number;
15
+ /** Field names in order */
16
+ fieldNames: (keyof T)[];
17
+ /** Codec for array serialization */
18
+ arrayCodec: ArrayField<T>;
19
+ }
20
+ /**
21
+ * Component type returned by defineComponent
22
+ */
23
+ export type Component<T extends object = any> = ComponentMeta<T> & {
24
+ /** Type marker for TypeScript inference */
25
+ __type?: T;
26
+ };
27
+ /**
28
+ * Infer the data type from a Component
29
+ */
30
+ export type InferComponentType<C> = C extends Component<infer T> ? T : never;
31
+ /**
32
+ * Define a component type with its binary schema.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const Transform = defineComponent('Transform', {
37
+ * x: BinaryCodec.f32,
38
+ * y: BinaryCodec.f32,
39
+ * rotation: BinaryCodec.f32,
40
+ * });
41
+ *
42
+ * const Health = defineComponent('Health', {
43
+ * current: BinaryCodec.u16,
44
+ * max: BinaryCodec.u16,
45
+ * });
46
+ * ```
47
+ */
48
+ export declare function defineComponent<T extends object>(name: string, schema: Schema<T>): Component<T>;
@@ -0,0 +1,43 @@
1
+ import { PooledCodec } from "../core/pooled-codec";
2
+ /**
3
+ * Calculate the byte size of a schema
4
+ */
5
+ function calculateSchemaSize(schema) {
6
+ let size = 0;
7
+ for (const key of Object.keys(schema)) {
8
+ size += schema[key].size;
9
+ }
10
+ return size;
11
+ }
12
+ /**
13
+ * Define a component type with its binary schema.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const Transform = defineComponent('Transform', {
18
+ * x: BinaryCodec.f32,
19
+ * y: BinaryCodec.f32,
20
+ * rotation: BinaryCodec.f32,
21
+ * });
22
+ *
23
+ * const Health = defineComponent('Health', {
24
+ * current: BinaryCodec.u16,
25
+ * max: BinaryCodec.u16,
26
+ * });
27
+ * ```
28
+ */
29
+ export function defineComponent(name, schema) {
30
+ const size = calculateSchemaSize(schema);
31
+ const fieldNames = Object.keys(schema);
32
+ const fieldCount = fieldNames.length;
33
+ // Create PooledCodec for array serialization
34
+ const arrayCodec = PooledCodec.array(schema);
35
+ return {
36
+ name,
37
+ schema,
38
+ size,
39
+ fieldCount,
40
+ fieldNames,
41
+ arrayCodec,
42
+ };
43
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ECS Usage Example
3
+ *
4
+ * This example demonstrates how to use the ECS for a simple game.
5
+ */
6
+ export {};
@@ -0,0 +1,125 @@
1
+ /**
2
+ * ECS Usage Example
3
+ *
4
+ * This example demonstrates how to use the ECS for a simple game.
5
+ */
6
+ import { defineComponent, World } from "./index";
7
+ import { BinaryCodec } from "../core/binary-codec";
8
+ // 1. Define components using BinaryCodec schemas
9
+ const Transform = defineComponent("Transform", {
10
+ x: BinaryCodec.f32,
11
+ y: BinaryCodec.f32,
12
+ rotation: BinaryCodec.f32,
13
+ });
14
+ const Velocity = defineComponent("Velocity", {
15
+ vx: BinaryCodec.f32,
16
+ vy: BinaryCodec.f32,
17
+ });
18
+ const Health = defineComponent("Health", {
19
+ current: BinaryCodec.u16,
20
+ max: BinaryCodec.u16,
21
+ });
22
+ const Damage = defineComponent("Damage", {
23
+ amount: BinaryCodec.u16,
24
+ });
25
+ // 2. Create systems
26
+ class MovementSystem {
27
+ update(world, deltaTime) {
28
+ for (const entity of world.query(Transform, Velocity)) {
29
+ const transform = world.get(entity, Transform); // Readonly!
30
+ const velocity = world.get(entity, Velocity); // Readonly!
31
+ // Update position using update() (efficient partial update)
32
+ world.update(entity, Transform, {
33
+ x: transform.x + velocity.vx * deltaTime,
34
+ y: transform.y + velocity.vy * deltaTime,
35
+ });
36
+ }
37
+ }
38
+ }
39
+ class HealthSystem {
40
+ update(world) {
41
+ for (const entity of world.query(Health)) {
42
+ const health = world.get(entity, Health);
43
+ // Despawn dead entities
44
+ if (health.current <= 0) {
45
+ console.log(`Entity ${entity} died`);
46
+ world.despawn(entity);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ class CombatSystem {
52
+ update(world) {
53
+ // Simple collision-based damage (in real game, use spatial partitioning)
54
+ const combatants = Array.from(world.query(Transform, Health, Damage));
55
+ for (let i = 0; i < combatants.length; i++) {
56
+ for (let j = i + 1; j < combatants.length; j++) {
57
+ const e1 = combatants[i];
58
+ const e2 = combatants[j];
59
+ const t1 = world.get(e1, Transform);
60
+ const t2 = world.get(e2, Transform);
61
+ // Check collision (simple distance check)
62
+ const dx = t1.x - t2.x;
63
+ const dy = t1.y - t2.y;
64
+ const dist = Math.sqrt(dx * dx + dy * dy);
65
+ if (dist < 10) {
66
+ // Apply damage
67
+ const d1 = world.get(e1, Damage);
68
+ const d2 = world.get(e2, Damage);
69
+ world.update(e1, Health, { current: world.get(e1, Health).current - d2.amount });
70
+ world.update(e2, Health, { current: world.get(e2, Health).current - d1.amount });
71
+ console.log(`Entities ${e1} and ${e2} collided! Applying damage.`);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ // 3. Create world and spawn entities
78
+ function runExample() {
79
+ const world = new World({
80
+ maxEntities: 1000,
81
+ components: [Transform, Velocity, Health, Damage],
82
+ });
83
+ // Spawn player
84
+ const player = world.spawn();
85
+ world.add(player, Transform, { x: 0, y: 0, rotation: 0 });
86
+ world.add(player, Velocity, { vx: 100, vy: 0 });
87
+ world.add(player, Health, { current: 100, max: 100 });
88
+ world.add(player, Damage, { amount: 10 });
89
+ // Spawn enemies
90
+ for (let i = 0; i < 5; i++) {
91
+ const enemy = world.spawn();
92
+ world.add(enemy, Transform, { x: 200 + i * 50, y: 0, rotation: 0 });
93
+ world.add(enemy, Velocity, { vx: -50, vy: 0 });
94
+ world.add(enemy, Health, { current: 50, max: 50 });
95
+ world.add(enemy, Damage, { amount: 5 });
96
+ }
97
+ console.log(`Spawned ${world.getEntityCount()} entities`);
98
+ // 4. Create systems
99
+ const movementSystem = new MovementSystem();
100
+ const combatSystem = new CombatSystem();
101
+ const healthSystem = new HealthSystem();
102
+ // 5. Game loop
103
+ const deltaTime = 0.016; // 60 FPS
104
+ let tick = 0;
105
+ const interval = setInterval(() => {
106
+ tick++;
107
+ // Update systems
108
+ movementSystem.update(world, deltaTime);
109
+ combatSystem.update(world);
110
+ healthSystem.update(world);
111
+ // Log stats every 10 ticks
112
+ if (tick % 10 === 0) {
113
+ console.log(`Tick ${tick}: ${world.getEntityCount()} entities alive`);
114
+ }
115
+ // Stop after 100 ticks or when all entities are dead
116
+ if (tick >= 100 || world.getEntityCount() === 0) {
117
+ clearInterval(interval);
118
+ console.log("Simulation ended");
119
+ }
120
+ }, deltaTime * 1000);
121
+ }
122
+ // Run the example
123
+ if (import.meta.main) {
124
+ runExample();
125
+ }
@@ -0,0 +1,3 @@
1
+ export { defineComponent, type Component, type InferComponentType, type ComponentMeta } from "./component";
2
+ export { ComponentStore } from "./component-store";
3
+ export { World, type Entity, type WorldConfig } from "./world";
@@ -0,0 +1,3 @@
1
+ export { defineComponent } from "./component";
2
+ export { ComponentStore } from "./component-store";
3
+ export { World } from "./world";