@codehz/ecs 0.7.2 → 0.7.4
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/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +60 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- /package/{builder.d.mts → dist/builder.d.mts} +0 -0
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
- /package/{world.mjs → dist/world.mjs} +0 -0
- /package/{world.mjs.map → dist/world.mjs.map} +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { pipeline } from "@codehz/pipeline";
|
|
2
|
+
import { World, component, type Query } from "../src";
|
|
3
|
+
|
|
4
|
+
// Define component types
|
|
5
|
+
type Position = { x: number; y: number };
|
|
6
|
+
type Velocity = { x: number; y: number };
|
|
7
|
+
type Health = { value: number };
|
|
8
|
+
|
|
9
|
+
// Define component IDs
|
|
10
|
+
const Position = component<Position>();
|
|
11
|
+
const Velocity = component<Velocity>();
|
|
12
|
+
const Health = component<Health>();
|
|
13
|
+
|
|
14
|
+
// Create the world
|
|
15
|
+
const world = new World();
|
|
16
|
+
|
|
17
|
+
// Cache queries
|
|
18
|
+
const movementQuery: Query = world.createQuery([Position, Velocity]);
|
|
19
|
+
const damageQuery: Query = world.createQuery([Position, Health]);
|
|
20
|
+
const renderQuery: Query = world.createQuery([Position]);
|
|
21
|
+
|
|
22
|
+
// Build game loop using pipeline
|
|
23
|
+
// Pass execution order is determined by addition order; no need to manually manage dependencies
|
|
24
|
+
const gameLoop = pipeline<{ deltaTime: number }>()
|
|
25
|
+
// Input pass - handle user input
|
|
26
|
+
.addPass(() => {
|
|
27
|
+
console.log(`[InputPass] Processing input at ${Date.now()}`);
|
|
28
|
+
// Keyboard/mouse input handling etc. goes here
|
|
29
|
+
})
|
|
30
|
+
// Movement pass - update positions
|
|
31
|
+
.addPass((env) => {
|
|
32
|
+
console.log(`[MovementPass] Updating positions`);
|
|
33
|
+
movementQuery.forEach([Position, Velocity], (entity, position, velocity) => {
|
|
34
|
+
position.x += velocity.x * env.deltaTime;
|
|
35
|
+
position.y += velocity.y * env.deltaTime;
|
|
36
|
+
console.log(` Entity ${entity}: Position (${position.x.toFixed(2)}, ${position.y.toFixed(2)})`);
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
// Damage pass - calculate damage based on position
|
|
40
|
+
.addPass(() => {
|
|
41
|
+
console.log(`[DamagePass] Applying damage based on position`);
|
|
42
|
+
damageQuery.forEach([Position, Health], (entity, position, health) => {
|
|
43
|
+
// Calculate damage based on position (example logic)
|
|
44
|
+
const damage = Math.abs(position.x) * 0.1;
|
|
45
|
+
health.value -= damage;
|
|
46
|
+
console.log(` Entity ${entity}: Health reduced by ${damage.toFixed(2)}, now ${health.value.toFixed(2)}`);
|
|
47
|
+
});
|
|
48
|
+
})
|
|
49
|
+
// Render pass - render entities
|
|
50
|
+
.addPass(() => {
|
|
51
|
+
console.log(`[RenderPass] Rendering entities`);
|
|
52
|
+
renderQuery.forEach([Position], (entity, position) => {
|
|
53
|
+
console.log(` Rendering Entity ${entity} at (${position.x.toFixed(2)}, ${position.y.toFixed(2)})`);
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
// Sync pass - must be called as the last pass to execute all deferred commands
|
|
57
|
+
.addPass(() => {
|
|
58
|
+
world.sync();
|
|
59
|
+
})
|
|
60
|
+
.build();
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
console.log("ECS Advanced Scheduling Demo - Pipeline-based Execution");
|
|
64
|
+
console.log("========================================================");
|
|
65
|
+
|
|
66
|
+
// Create some entities
|
|
67
|
+
const entity1 = world
|
|
68
|
+
.spawn()
|
|
69
|
+
.with(Position, { x: 0, y: 0 })
|
|
70
|
+
.with(Velocity, { x: 2, y: 1 })
|
|
71
|
+
.with(Health, { value: 100 })
|
|
72
|
+
.build();
|
|
73
|
+
void entity1;
|
|
74
|
+
|
|
75
|
+
const entity2 = world
|
|
76
|
+
.spawn()
|
|
77
|
+
.with(Position, { x: 5, y: 3 })
|
|
78
|
+
.with(Velocity, { x: -1, y: 0.5 })
|
|
79
|
+
.with(Health, { value: 80 })
|
|
80
|
+
.build();
|
|
81
|
+
void entity2;
|
|
82
|
+
|
|
83
|
+
// Execute initial sync
|
|
84
|
+
world.sync();
|
|
85
|
+
|
|
86
|
+
// Run a few frames
|
|
87
|
+
console.log("\n--- Frame 1 ---");
|
|
88
|
+
gameLoop({ deltaTime: 1.0 });
|
|
89
|
+
|
|
90
|
+
console.log("\n--- Frame 2 ---");
|
|
91
|
+
gameLoop({ deltaTime: 1.0 });
|
|
92
|
+
|
|
93
|
+
console.log("\nDemo completed!");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main();
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { pipeline } from "@codehz/pipeline";
|
|
2
|
+
import { World, component, type EntityId, type Query } from "../src";
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Component Types
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
type Position = { x: number; y: number };
|
|
9
|
+
type Velocity = { x: number; y: number };
|
|
10
|
+
type Radius = number;
|
|
11
|
+
type Health = { value: number };
|
|
12
|
+
type CollisionEvent = { other: EntityId; overlap: number };
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Component IDs
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
const Position = component<Position>();
|
|
19
|
+
const Velocity = component<Velocity>();
|
|
20
|
+
const Radius = component<Radius>();
|
|
21
|
+
const Health = component<Health>();
|
|
22
|
+
const CollisionEvent = component<CollisionEvent>();
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// World & Pre-cached Queries
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
const world = new World();
|
|
29
|
+
|
|
30
|
+
const movementQuery: Query = world.createQuery([Position, Velocity]);
|
|
31
|
+
const collidableQuery: Query = world.createQuery([Position, Radius]);
|
|
32
|
+
const damagedQuery: Query = world.createQuery([Health]);
|
|
33
|
+
const collisionEventQuery: Query = world.createQuery([CollisionEvent]);
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Game Loop (Pipeline Passes)
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
const gameLoop = pipeline<{ deltaTime: number }>()
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// MovementPass: Move entities by velocity * deltaTime
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
.addPass((env) => {
|
|
44
|
+
console.log(`[MovementPass] deltaTime=${env.deltaTime}`);
|
|
45
|
+
movementQuery.forEach([Position, Velocity], (_entity, pos, vel) => {
|
|
46
|
+
pos.x += vel.x * env.deltaTime;
|
|
47
|
+
pos.y += vel.y * env.deltaTime;
|
|
48
|
+
});
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// CollisionDetectionPass: O(n^2) pair check
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
.addPass(() => {
|
|
55
|
+
console.log(`[CollisionDetectionPass]`);
|
|
56
|
+
const collidables = collidableQuery.getEntitiesWithComponents([Position, Radius]);
|
|
57
|
+
let collisionCount = 0;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < collidables.length; i++) {
|
|
60
|
+
const a = collidables[i]!;
|
|
61
|
+
for (let j = i + 1; j < collidables.length; j++) {
|
|
62
|
+
const b = collidables[j]!;
|
|
63
|
+
const dx = a.components[0].x - b.components[0].x;
|
|
64
|
+
const dy = a.components[0].y - b.components[0].y;
|
|
65
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
66
|
+
const overlap = a.components[1] + b.components[1] - dist;
|
|
67
|
+
|
|
68
|
+
if (overlap > 0) {
|
|
69
|
+
console.log(` Collision: Entity ${a.entity} <-> Entity ${b.entity} ` + `(overlap: ${overlap.toFixed(2)})`);
|
|
70
|
+
world.set(a.entity, CollisionEvent, { other: b.entity, overlap });
|
|
71
|
+
world.set(b.entity, CollisionEvent, { other: a.entity, overlap });
|
|
72
|
+
collisionCount++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (collisionCount === 0) {
|
|
78
|
+
console.log(` No collisions detected`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Sync so CollisionEvents are visible to the response pass
|
|
82
|
+
world.sync();
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// CollisionResponsePass: Apply damage & remove transient CollisionEvent
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
.addPass(() => {
|
|
89
|
+
console.log(`[CollisionResponsePass]`);
|
|
90
|
+
collisionEventQuery.forEach([CollisionEvent], (entity, event) => {
|
|
91
|
+
if (world.has(entity, Health)) {
|
|
92
|
+
const health = world.get(entity, Health);
|
|
93
|
+
health.value -= event.overlap;
|
|
94
|
+
console.log(
|
|
95
|
+
` Entity ${entity}: took ${event.overlap.toFixed(2)} damage ` +
|
|
96
|
+
`from Entity ${event.other}, health now ${health.value.toFixed(2)}`,
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(
|
|
100
|
+
` Entity ${entity}: collision with Entity ${event.other} ` + `(no Health component, skipping damage)`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
world.remove(entity, CollisionEvent);
|
|
104
|
+
});
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// CleanupPass: Delete dead entities (health <= 0)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
.addPass(() => {
|
|
111
|
+
console.log(`[CleanupPass]`);
|
|
112
|
+
const toDelete: EntityId[] = [];
|
|
113
|
+
|
|
114
|
+
damagedQuery.forEach([Health], (entity, health) => {
|
|
115
|
+
if (health.value <= 0) {
|
|
116
|
+
console.log(` Entity ${entity}: destroyed (health: ${health.value.toFixed(2)})`);
|
|
117
|
+
toDelete.push(entity);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const entity of toDelete) {
|
|
122
|
+
world.delete(entity);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (toDelete.length === 0) {
|
|
126
|
+
console.log(` No entities to clean up`);
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// RenderPass: Log positions and health of all living entities
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
.addPass(() => {
|
|
134
|
+
console.log(`[RenderPass]`);
|
|
135
|
+
movementQuery.forEach([Position, Velocity], (entity, pos) => {
|
|
136
|
+
const healthOpt = world.getOptional(entity, Health);
|
|
137
|
+
const healthStr = healthOpt ? healthOpt.value.value.toFixed(2) : "N/A";
|
|
138
|
+
console.log(` Entity ${entity}: pos=(${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}) ` + `health=${healthStr}`);
|
|
139
|
+
});
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// SyncPass: Apply all remaining buffered commands
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
.addPass(() => {
|
|
146
|
+
world.sync();
|
|
147
|
+
})
|
|
148
|
+
.build();
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Setup & Main
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
function main() {
|
|
155
|
+
console.log("ECS Collision Detection Demo");
|
|
156
|
+
console.log("============================\n");
|
|
157
|
+
|
|
158
|
+
// Spawn ~6 entities with diverse properties
|
|
159
|
+
console.log("Spawning entities...\n");
|
|
160
|
+
|
|
161
|
+
// Entity 1: Fast mover, small radius, moderate health
|
|
162
|
+
world
|
|
163
|
+
.spawn()
|
|
164
|
+
.with(Position, { x: 0, y: 0 })
|
|
165
|
+
.with(Velocity, { x: 3, y: 1 })
|
|
166
|
+
.with(Radius, 10)
|
|
167
|
+
.with(Health, { value: 50 })
|
|
168
|
+
.build();
|
|
169
|
+
|
|
170
|
+
// Entity 2: Slow mover, large radius, high health
|
|
171
|
+
world
|
|
172
|
+
.spawn()
|
|
173
|
+
.with(Position, { x: 30, y: 10 })
|
|
174
|
+
.with(Velocity, { x: -1.5, y: 2 })
|
|
175
|
+
.with(Radius, 18)
|
|
176
|
+
.with(Health, { value: 120 })
|
|
177
|
+
.build();
|
|
178
|
+
|
|
179
|
+
// Entity 3: Stationary, medium radius, low health
|
|
180
|
+
world
|
|
181
|
+
.spawn()
|
|
182
|
+
.with(Position, { x: 15, y: 20 })
|
|
183
|
+
.with(Velocity, { x: 0, y: 0 })
|
|
184
|
+
.with(Radius, 14)
|
|
185
|
+
.with(Health, { value: 25 })
|
|
186
|
+
.build();
|
|
187
|
+
|
|
188
|
+
// Entity 4: Diagonal mover, small radius, moderate health
|
|
189
|
+
world
|
|
190
|
+
.spawn()
|
|
191
|
+
.with(Position, { x: 40, y: 5 })
|
|
192
|
+
.with(Velocity, { x: -3, y: -2 })
|
|
193
|
+
.with(Radius, 8)
|
|
194
|
+
.with(Health, { value: 60 })
|
|
195
|
+
.build();
|
|
196
|
+
|
|
197
|
+
// Entity 5: Opposite diagonal, medium radius, high health
|
|
198
|
+
world
|
|
199
|
+
.spawn()
|
|
200
|
+
.with(Position, { x: 10, y: 35 })
|
|
201
|
+
.with(Velocity, { x: 2.5, y: -1.5 })
|
|
202
|
+
.with(Radius, 16)
|
|
203
|
+
.with(Health, { value: 100 })
|
|
204
|
+
.build();
|
|
205
|
+
|
|
206
|
+
// Entity 6: Slow vertical mover, large radius, moderate health
|
|
207
|
+
world
|
|
208
|
+
.spawn()
|
|
209
|
+
.with(Position, { x: 50, y: 25 })
|
|
210
|
+
.with(Velocity, { x: -0.5, y: 3 })
|
|
211
|
+
.with(Radius, 20)
|
|
212
|
+
.with(Health, { value: 80 })
|
|
213
|
+
.build();
|
|
214
|
+
|
|
215
|
+
// Apply initial spawns
|
|
216
|
+
world.sync();
|
|
217
|
+
console.log("All entities spawned and synced.\n");
|
|
218
|
+
|
|
219
|
+
// Run 5 frames
|
|
220
|
+
for (let frame = 1; frame <= 5; frame++) {
|
|
221
|
+
console.log(`--- Frame ${frame} ---`);
|
|
222
|
+
gameLoop({ deltaTime: 1.0 });
|
|
223
|
+
console.log();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log("Demo completed!");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { World, component, relation, type EntityId, type Query } from "../src";
|
|
2
|
+
|
|
3
|
+
type ItemName = { name: string };
|
|
4
|
+
type Stackable = { count: number; maxCount: number };
|
|
5
|
+
type Gold = { amount: number };
|
|
6
|
+
type EquipmentSlot = { slot: string };
|
|
7
|
+
|
|
8
|
+
const ItemName = component<ItemName>({ name: "ItemName" });
|
|
9
|
+
const Stackable = component<Stackable>({ name: "Stackable" });
|
|
10
|
+
const Gold = component<Gold>({ name: "Gold" });
|
|
11
|
+
const EquipmentSlot = component<EquipmentSlot>({ name: "EquipmentSlot" });
|
|
12
|
+
const InInventory = component<void>({ name: "InInventory", dontFragment: true });
|
|
13
|
+
|
|
14
|
+
const world = new World();
|
|
15
|
+
|
|
16
|
+
const inventoryOwners: Query = world.createQuery([relation(InInventory, "*")]);
|
|
17
|
+
|
|
18
|
+
function formatItem(item: EntityId): string {
|
|
19
|
+
const itemName = world.get(item, ItemName);
|
|
20
|
+
const stack = world.getOptional(item, Stackable)?.value;
|
|
21
|
+
const slot = world.getOptional(item, EquipmentSlot)?.value;
|
|
22
|
+
|
|
23
|
+
const details: string[] = [];
|
|
24
|
+
if (stack) {
|
|
25
|
+
details.push(`stack ${stack.count}/${stack.maxCount}`);
|
|
26
|
+
}
|
|
27
|
+
if (slot) {
|
|
28
|
+
details.push(`slot=${slot.slot}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return details.length > 0 ? `${itemName.name} (${details.join(", ")})` : itemName.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printInventory(owner: EntityId, label: string): void {
|
|
35
|
+
if (!world.has(owner, relation(InInventory, "*"))) {
|
|
36
|
+
console.log(`${label}: empty inventory`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entries = world.get(owner, relation(InInventory, "*"));
|
|
41
|
+
const itemLines = entries.map(([item]) => ` - Item ${item}: ${formatItem(item)}`);
|
|
42
|
+
console.log(`${label}:\n${itemLines.join("\n")}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function countInventoryItems(owner: EntityId): number {
|
|
46
|
+
return world.getOptional(owner, relation(InInventory, "*"))?.value.length ?? 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printInventorySummary(): void {
|
|
50
|
+
console.log("\n[InventorySummary]");
|
|
51
|
+
inventoryOwners.forEach([relation(InInventory, "*")], (owner, items) => {
|
|
52
|
+
const gold = world.getOptional(owner, Gold)?.value;
|
|
53
|
+
console.log(`Owner ${owner}: ${items.length} item(s), gold=${gold?.amount ?? 0}`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function main() {
|
|
58
|
+
console.log("ECS Inventory System Demo - Non-exclusive Relations");
|
|
59
|
+
console.log("===================================================");
|
|
60
|
+
|
|
61
|
+
const sword = world.spawn().with(ItemName, { name: "Iron Sword" }).with(EquipmentSlot, { slot: "weapon" }).build();
|
|
62
|
+
const armor = world.spawn().with(ItemName, { name: "Leather Armor" }).with(EquipmentSlot, { slot: "armor" }).build();
|
|
63
|
+
const potion = world
|
|
64
|
+
.spawn()
|
|
65
|
+
.with(ItemName, { name: "Health Potion" })
|
|
66
|
+
.with(Stackable, { count: 3, maxCount: 20 })
|
|
67
|
+
.build();
|
|
68
|
+
const arrows = world
|
|
69
|
+
.spawn()
|
|
70
|
+
.with(ItemName, { name: "Arrow Bundle" })
|
|
71
|
+
.with(Stackable, { count: 48, maxCount: 99 })
|
|
72
|
+
.build();
|
|
73
|
+
|
|
74
|
+
const player = world
|
|
75
|
+
.spawn()
|
|
76
|
+
.with(Gold, { amount: 125 })
|
|
77
|
+
.with(relation(InInventory, sword))
|
|
78
|
+
.with(relation(InInventory, armor))
|
|
79
|
+
.with(relation(InInventory, potion))
|
|
80
|
+
.with(relation(InInventory, arrows))
|
|
81
|
+
.build();
|
|
82
|
+
world.sync();
|
|
83
|
+
|
|
84
|
+
console.log(`\nPlayer ${player} starts with ${world.get(player, Gold).amount} gold.`);
|
|
85
|
+
printInventory(player, "Initial inventory");
|
|
86
|
+
|
|
87
|
+
const potionStack = world.get(potion, Stackable);
|
|
88
|
+
potionStack.count += 2;
|
|
89
|
+
console.log(`\nPicked up more potions. Potion stack is now ${potionStack.count}/${potionStack.maxCount}.`);
|
|
90
|
+
|
|
91
|
+
const swordSlot = world.get(sword, EquipmentSlot);
|
|
92
|
+
console.log(`Equipped ${world.get(sword, ItemName).name} to ${swordSlot.slot}.`);
|
|
93
|
+
|
|
94
|
+
world.remove(player, relation(InInventory, armor));
|
|
95
|
+
world.set(player, Gold, { amount: world.get(player, Gold).amount + 35 });
|
|
96
|
+
world.sync();
|
|
97
|
+
|
|
98
|
+
console.log(`\nSold ${world.get(armor, ItemName).name} for 35 gold.`);
|
|
99
|
+
console.log(`Player ${player} now has ${world.get(player, Gold).amount} gold.`);
|
|
100
|
+
printInventory(player, "Inventory after selling armor");
|
|
101
|
+
|
|
102
|
+
console.log(`\nInventory count via wildcard relation: ${countInventoryItems(player)}`);
|
|
103
|
+
printInventorySummary();
|
|
104
|
+
|
|
105
|
+
console.log("\nDemo completed!");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { pipeline } from "@codehz/pipeline";
|
|
2
|
+
import { World, component, relation, type EntityId, type Query } from "../src";
|
|
3
|
+
|
|
4
|
+
// Define component types
|
|
5
|
+
type Transform = { x: number; y: number; rotation: number; scale: number };
|
|
6
|
+
type LinearVelocity = { x: number; y: number };
|
|
7
|
+
type AngularVelocity = { degreesPerSecond: number };
|
|
8
|
+
type Name = { value: string };
|
|
9
|
+
|
|
10
|
+
// Define component IDs
|
|
11
|
+
const Name = component<Name>({ name: "Name" });
|
|
12
|
+
const LocalTransform = component<Transform>({ name: "LocalTransform" });
|
|
13
|
+
const WorldTransform = component<Transform>({ name: "WorldTransform" });
|
|
14
|
+
const LinearVelocity = component<LinearVelocity>({ name: "LinearVelocity" });
|
|
15
|
+
const AngularVelocity = component<AngularVelocity>({ name: "AngularVelocity" });
|
|
16
|
+
const ChildOf = component<void>({ exclusive: true, dontFragment: true, name: "ChildOf" });
|
|
17
|
+
|
|
18
|
+
// Create the world
|
|
19
|
+
const world = new World();
|
|
20
|
+
|
|
21
|
+
// Cache queries
|
|
22
|
+
const movementQuery: Query = world.createQuery([LocalTransform, LinearVelocity]);
|
|
23
|
+
const rotationQuery: Query = world.createQuery([LocalTransform, AngularVelocity]);
|
|
24
|
+
const transformQuery: Query = world.createQuery([Name, LocalTransform, WorldTransform]);
|
|
25
|
+
const childQuery: Query = world.createQuery([relation(ChildOf, "*")]);
|
|
26
|
+
const renderQuery: Query = world.createQuery([Name, WorldTransform]);
|
|
27
|
+
|
|
28
|
+
function toRadians(degrees: number): number {
|
|
29
|
+
return (degrees * Math.PI) / 180;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyTransform(target: Transform, source: Transform): void {
|
|
33
|
+
target.x = source.x;
|
|
34
|
+
target.y = source.y;
|
|
35
|
+
target.rotation = source.rotation;
|
|
36
|
+
target.scale = source.scale;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function composeTransform(local: Transform, parent?: Transform): Transform {
|
|
40
|
+
if (!parent) {
|
|
41
|
+
return { ...local };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const angle = toRadians(parent.rotation);
|
|
45
|
+
const scaledX = local.x * parent.scale;
|
|
46
|
+
const scaledY = local.y * parent.scale;
|
|
47
|
+
const cos = Math.cos(angle);
|
|
48
|
+
const sin = Math.sin(angle);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
x: parent.x + scaledX * cos - scaledY * sin,
|
|
52
|
+
y: parent.y + scaledX * sin + scaledY * cos,
|
|
53
|
+
rotation: parent.rotation + local.rotation,
|
|
54
|
+
scale: parent.scale * local.scale,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTransform(transform: Transform): string {
|
|
59
|
+
return `pos=(${transform.x.toFixed(2)}, ${transform.y.toFixed(2)}) rot=${transform.rotation.toFixed(1)}deg scale=${transform.scale.toFixed(2)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildChildrenByParent(): Map<EntityId, EntityId[]> {
|
|
63
|
+
const childrenByParent = new Map<EntityId, EntityId[]>();
|
|
64
|
+
|
|
65
|
+
childQuery.forEach([relation(ChildOf, "*")], (child, parents) => {
|
|
66
|
+
const parent = parents[0]?.[0];
|
|
67
|
+
if (parent === undefined) return;
|
|
68
|
+
|
|
69
|
+
const children = childrenByParent.get(parent) ?? [];
|
|
70
|
+
children.push(child);
|
|
71
|
+
childrenByParent.set(parent, children);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return childrenByParent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function propagateChildren(
|
|
78
|
+
parent: EntityId,
|
|
79
|
+
parentWorld: Transform,
|
|
80
|
+
childrenByParent: Map<EntityId, EntityId[]>,
|
|
81
|
+
): void {
|
|
82
|
+
const children = childrenByParent.get(parent);
|
|
83
|
+
if (!children) return;
|
|
84
|
+
|
|
85
|
+
for (const child of children) {
|
|
86
|
+
const name = world.get(child, Name);
|
|
87
|
+
const local = world.get(child, LocalTransform);
|
|
88
|
+
const worldTransform = world.get(child, WorldTransform);
|
|
89
|
+
copyTransform(worldTransform, composeTransform(local, parentWorld));
|
|
90
|
+
console.log(` Child ${name.value}: ${formatTransform(worldTransform)}`);
|
|
91
|
+
propagateChildren(child, worldTransform, childrenByParent);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build game loop using pipeline
|
|
96
|
+
// Pass execution order is determined by addition order; no need to manually manage dependencies
|
|
97
|
+
const gameLoop = pipeline<{ deltaTime: number }>()
|
|
98
|
+
// Local movement pass - update local positions
|
|
99
|
+
.addPass((env) => {
|
|
100
|
+
console.log(`[LocalMovementPass] Updating local positions`);
|
|
101
|
+
movementQuery.forEach([LocalTransform, LinearVelocity], (entity, localTransform, velocity) => {
|
|
102
|
+
localTransform.x += velocity.x * env.deltaTime;
|
|
103
|
+
localTransform.y += velocity.y * env.deltaTime;
|
|
104
|
+
const name = world.get(entity, Name);
|
|
105
|
+
console.log(` ${name.value}: local pos=(${localTransform.x.toFixed(2)}, ${localTransform.y.toFixed(2)})`);
|
|
106
|
+
});
|
|
107
|
+
})
|
|
108
|
+
// Local rotation pass - update local rotation
|
|
109
|
+
.addPass((env) => {
|
|
110
|
+
console.log(`[LocalRotationPass] Updating local rotations`);
|
|
111
|
+
rotationQuery.forEach([LocalTransform, AngularVelocity], (entity, localTransform, angularVelocity) => {
|
|
112
|
+
localTransform.rotation += angularVelocity.degreesPerSecond * env.deltaTime;
|
|
113
|
+
const name = world.get(entity, Name);
|
|
114
|
+
console.log(` ${name.value}: local rot=${localTransform.rotation.toFixed(1)}deg`);
|
|
115
|
+
});
|
|
116
|
+
})
|
|
117
|
+
// Hierarchy pass - propagate parent transforms into world transforms
|
|
118
|
+
.addPass(() => {
|
|
119
|
+
console.log(`[HierarchyPass] Propagating world transforms`);
|
|
120
|
+
const childrenByParent = buildChildrenByParent();
|
|
121
|
+
|
|
122
|
+
transformQuery.forEach([Name, LocalTransform, WorldTransform], (entity, name, localTransform, worldTransform) => {
|
|
123
|
+
if (world.has(entity, relation(ChildOf, "*"))) return;
|
|
124
|
+
|
|
125
|
+
copyTransform(worldTransform, composeTransform(localTransform));
|
|
126
|
+
console.log(` Root ${name.value}: ${formatTransform(worldTransform)}`);
|
|
127
|
+
propagateChildren(entity, worldTransform, childrenByParent);
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
// Render pass - render propagated world transforms
|
|
131
|
+
.addPass(() => {
|
|
132
|
+
console.log(`[RenderPass] Rendering world transforms`);
|
|
133
|
+
renderQuery.forEach([Name, WorldTransform], (_entity, name, worldTransform) => {
|
|
134
|
+
console.log(` ${name.value}: ${formatTransform(worldTransform)}`);
|
|
135
|
+
});
|
|
136
|
+
})
|
|
137
|
+
// Sync pass - must be called as the last pass to execute all deferred commands
|
|
138
|
+
.addPass(() => {
|
|
139
|
+
world.sync();
|
|
140
|
+
})
|
|
141
|
+
.build();
|
|
142
|
+
|
|
143
|
+
function main() {
|
|
144
|
+
console.log("ECS Parent-Child Hierarchy Demo - Transform Propagation");
|
|
145
|
+
console.log("=======================================================");
|
|
146
|
+
|
|
147
|
+
// Create a moving root entity
|
|
148
|
+
const ship = world
|
|
149
|
+
.spawn()
|
|
150
|
+
.with(Name, { value: "Ship" })
|
|
151
|
+
.with(LocalTransform, { x: 0, y: 0, rotation: 0, scale: 1 })
|
|
152
|
+
.with(WorldTransform, { x: 0, y: 0, rotation: 0, scale: 1 })
|
|
153
|
+
.with(LinearVelocity, { x: 1, y: 0.25 })
|
|
154
|
+
.build();
|
|
155
|
+
|
|
156
|
+
// Create a rotating child attached to the ship
|
|
157
|
+
const turret = world
|
|
158
|
+
.spawn()
|
|
159
|
+
.with(Name, { value: "Turret" })
|
|
160
|
+
.with(LocalTransform, { x: 2, y: 0, rotation: 0, scale: 1 })
|
|
161
|
+
.with(WorldTransform, { x: 0, y: 0, rotation: 0, scale: 1 })
|
|
162
|
+
.with(AngularVelocity, { degreesPerSecond: 45 })
|
|
163
|
+
.with(relation(ChildOf, ship))
|
|
164
|
+
.build();
|
|
165
|
+
|
|
166
|
+
// Create a grandchild so propagation traverses multiple levels
|
|
167
|
+
const muzzle = world
|
|
168
|
+
.spawn()
|
|
169
|
+
.with(Name, { value: "Muzzle" })
|
|
170
|
+
.with(LocalTransform, { x: 1.5, y: 0.5, rotation: 0, scale: 1 })
|
|
171
|
+
.with(WorldTransform, { x: 0, y: 0, rotation: 0, scale: 1 })
|
|
172
|
+
.with(relation(ChildOf, turret))
|
|
173
|
+
.build();
|
|
174
|
+
void muzzle;
|
|
175
|
+
|
|
176
|
+
// Create a second root entity to show independent hierarchies
|
|
177
|
+
const drone = world
|
|
178
|
+
.spawn()
|
|
179
|
+
.with(Name, { value: "Drone" })
|
|
180
|
+
.with(LocalTransform, { x: -4, y: 3, rotation: 15, scale: 0.75 })
|
|
181
|
+
.with(WorldTransform, { x: -4, y: 3, rotation: 15, scale: 0.75 })
|
|
182
|
+
.with(LinearVelocity, { x: 0.5, y: -0.25 })
|
|
183
|
+
.build();
|
|
184
|
+
void drone;
|
|
185
|
+
|
|
186
|
+
// Execute initial sync
|
|
187
|
+
world.sync();
|
|
188
|
+
|
|
189
|
+
// Run an initial propagation frame so children receive their world transforms
|
|
190
|
+
console.log("\n--- Initial Hierarchy ---");
|
|
191
|
+
gameLoop({ deltaTime: 0.0 });
|
|
192
|
+
|
|
193
|
+
// Run a few frames
|
|
194
|
+
console.log("\n--- Frame 1 ---");
|
|
195
|
+
gameLoop({ deltaTime: 1.0 });
|
|
196
|
+
|
|
197
|
+
console.log("\n--- Frame 2 ---");
|
|
198
|
+
gameLoop({ deltaTime: 1.0 });
|
|
199
|
+
|
|
200
|
+
console.log("\n--- Frame 3 ---");
|
|
201
|
+
gameLoop({ deltaTime: 1.0 });
|
|
202
|
+
|
|
203
|
+
console.log("\nDemo completed!");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main();
|