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.
- package/dist/ecs/component-store.d.ts +79 -0
- package/dist/ecs/component-store.js +191 -0
- package/dist/ecs/component.d.ts +48 -0
- package/dist/ecs/component.js +43 -0
- package/dist/ecs/example.d.ts +6 -0
- package/dist/ecs/example.js +125 -0
- package/dist/ecs/index.d.ts +3 -0
- package/dist/ecs/index.js +3 -0
- package/dist/ecs/world.d.ts +208 -0
- package/dist/ecs/world.js +454 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/package.json +1 -1
- package/src/ecs/README.md +354 -0
- package/src/ecs/benchmark.test.ts +836 -0
- package/src/ecs/component-store.ts +246 -0
- package/src/ecs/component.ts +87 -0
- package/src/ecs/example.ts +152 -0
- package/src/ecs/index.ts +3 -0
- package/src/ecs/world.test.ts +309 -0
- package/src/ecs/world.ts +552 -0
- package/src/index.ts +4 -0
|
@@ -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,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
|
+
}
|