@draug/engine 1.0.4 → 1.0.6

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.
Files changed (2) hide show
  1. package/README.md +273 -1
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1 +1,273 @@
1
- # engine
1
+ # @draug/engine
2
+
3
+ Small ECS-first game skeleton for TypeScript: a `World` holds entities, components (plain data), systems (per-frame logic), double-buffered events, typed resources, and optional plugins. A thin `Runtime` plus `GameLoop` / `Clock` help you step simulation on a steady timer instead of growing everything inside one giant class.
4
+
5
+ ## Features
6
+
7
+ - **ECS core** — register component storages, attach data to entities, query by `include` / `exclude` / `anyOf`, run systems in dependency order (DAG over `@System` metadata).
8
+ - **Deferred commands** — queue structural changes (e.g. `commands.createEntity`) and apply them after systems run, so you do not mutate archetypes mid-iteration.
9
+ - **Events** — `EventBus` + `EventBuffer` with a per-frame `swap` (done for you inside `world.update` before systems).
10
+ - **Resources** — singleton-style services keyed by constructor (`insert` / `get` / `getOrInsert`).
11
+ - **Plugins** — install decorated `PluginBase` classes, resolve dependency order, then `world.build()` instantiates them.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @draug/engine
17
+ ```
18
+
19
+ The package is ESM-first (`"type": "module"`) and ships a CommonJS build as well (`exports` in `package.json`).
20
+
21
+ ## Quick start
22
+
23
+ Enable legacy decorators in `tsconfig.json` (the library uses `experimentalDecorators` today):
24
+
25
+ ```json
26
+ {
27
+ "compilerOptions": {
28
+ "experimentalDecorators": true,
29
+ "target": "ES2022",
30
+ "module": "ESNext",
31
+ "moduleResolution": "Bundler"
32
+ }
33
+ }
34
+ ```
35
+
36
+ Minimal world: register every component type once, register systems, call `systems.build()`, spawn entities, then step with `world.update(dt)`.
37
+
38
+ ```typescript
39
+ import {
40
+ World,
41
+ Component,
42
+ System,
43
+ SystemBase,
44
+ entry,
45
+ type SystemComputeContext,
46
+ } from '@draug/engine';
47
+
48
+ @Component()
49
+ class Position {
50
+ x = 0;
51
+ y = 0;
52
+ }
53
+
54
+ @System({ query: { include: [Position] } })
55
+ class GravitySystem extends SystemBase {
56
+ compute({ entities, world, dt }: SystemComputeContext): void {
57
+ const positions = world.components.getStorage(Position);
58
+ for (const id of entities) {
59
+ const p = positions.get(id);
60
+ if (p) p.y += 20 * dt;
61
+ }
62
+ }
63
+ }
64
+
65
+ const world = new World();
66
+
67
+ world.components.register(Position);
68
+ world.systems.register(new GravitySystem());
69
+ world.systems.build();
70
+
71
+ // Same-frame visibility: create entity, then attach components immediately.
72
+ const player = world.entities.create();
73
+ world.addComponent(player, Position, (p) => {
74
+ p.x = 0;
75
+ p.y = 0;
76
+ });
77
+
78
+ // Or defer to end-of-frame (handy when spawning from inside a system):
79
+ world.commands.createEntity(
80
+ entry(Position, (p) => {
81
+ p.x = 100;
82
+ p.y = 0;
83
+ }),
84
+ );
85
+
86
+ world.update(1 / 60);
87
+ world.update(1 / 60);
88
+ ```
89
+
90
+ **Note:** `world.update` runs systems first, then flushes the command queue. Anything created with `commands.createEntity` only gets components after that flush, so it will not appear in queries until the *next* `update` unless you attach components synchronously.
91
+
92
+ ## Usage / API
93
+
94
+ ### World
95
+
96
+ `World` is the façade: `entities`, `components`, `systems`, `events`, `resources`, `commands`, `queries`, `plugins`.
97
+
98
+ - **`world.query(params)`** — bitmask-backed query; supports `include`, `exclude`, `anyOf`, `excludeEntitiesIds`, and `filter`.
99
+ - **`world.addComponent(id, ComponentClass, init?)` / `removeComponent`** — structural changes; queries are invalidated for you.
100
+ - **`world.update(dt)`** — `events.swapAll()`, run systems in order, then `commands.flush`.
101
+
102
+ ### Components
103
+
104
+ Mark data classes with `@Component()` so the ECS can assign stable type ids:
105
+
106
+ ```typescript
107
+ import { Component, ComponentStorageType } from '@draug/engine';
108
+
109
+ // `world` is your World instance from the quick start.
110
+
111
+ @Component()
112
+ class Health {
113
+ current = 100;
114
+ max = 100;
115
+ }
116
+
117
+ world.components.register(Health);
118
+ ```
119
+
120
+ Optional **singleton** storage (one instance, not per-entity):
121
+
122
+ ```typescript
123
+ import { Component, ComponentStorageType } from '@draug/engine';
124
+
125
+ @Component()
126
+ class GlobalRNG {
127
+ next() {
128
+ return Math.random();
129
+ }
130
+ }
131
+
132
+ world.components.register(GlobalRNG, {
133
+ storageType: ComponentStorageType.SINGLETON_STORAGE,
134
+ factory: () => new GlobalRNG(),
135
+ });
136
+ ```
137
+
138
+ ### Systems
139
+
140
+ Extend `SystemBase`, implement `compute`, and decorate with `@System`:
141
+
142
+ ```typescript
143
+ import { System, SystemBase, type SystemComputeContext } from '@draug/engine';
144
+
145
+ // `Position`, `Health`, `SomeTag`, `PhysicsSystem` are your own component/system types.
146
+
147
+ @System({
148
+ query: { include: [Position, Health] },
149
+ requiredComponents: [SomeTag],
150
+ computeAfter: [PhysicsSystem],
151
+ })
152
+ class ApplyDamageSystem extends SystemBase {
153
+ compute(ctx: SystemComputeContext): void {
154
+ const { entities, world, dt } = ctx;
155
+ // ...
156
+ }
157
+ }
158
+ ```
159
+
160
+ - **`query`** — drives which entity ids are passed into `compute`.
161
+ - **`requiredComponents`** — ensures storages exist and documents extra reads that are not part of the query mask.
162
+ - **`computeAfter`** — ordering edges between system classes (both must be registered).
163
+
164
+ Call **`world.systems.build()`** after you finish registering systems so `onInit` hooks run and execution order is computed.
165
+
166
+ ### Commands
167
+
168
+ ```typescript
169
+ import { entry } from '@draug/engine';
170
+
171
+ world.commands.add((w) => {
172
+ const id = w.entities.create();
173
+ w.addComponent(id, Health, (h) => {
174
+ h.current = 50;
175
+ });
176
+ });
177
+
178
+ const id = world.commands.createEntity(
179
+ entry(Position, (p) => {
180
+ p.x = 0;
181
+ }),
182
+ );
183
+ ```
184
+
185
+ ### Events
186
+
187
+ ```typescript
188
+ import { createEventKey } from '@draug/engine';
189
+
190
+ const DamageDealt = createEventKey<{ target: number; amount: number }>();
191
+
192
+ const incoming = world.events.getBuffer(DamageDealt);
193
+ incoming.write({ target: 1, amount: 7 });
194
+
195
+ // Normally you only read after the bus swaps at the start of `world.update`.
196
+ world.events.swapAll();
197
+ for (const e of incoming.read()) {
198
+ console.log(e.amount);
199
+ }
200
+ ```
201
+
202
+ ### Plugins
203
+
204
+ ```typescript
205
+ import { Plugin, PluginBase } from '@draug/engine';
206
+
207
+ @Plugin({
208
+ id: 'audio',
209
+ version: '1.0.0',
210
+ name: 'Audio bootstrap',
211
+ })
212
+ class AudioPlugin extends PluginBase {}
213
+
214
+ world.plugins.install(AudioPlugin /*, ...ctor args if any */);
215
+ world.build(); // builds plugins, then call your own game bootstrap as needed
216
+ ```
217
+
218
+ After `world.build()`, resolve instances with `world.plugins.getPluginInstance(AudioPlugin)` (or by string id). Lifecycle hooks (`onPluginLoad`, etc.) live on `PluginBase` for you to call from your game code if you want explicit phases—the engine focuses on construction order and DAG validation.
219
+
220
+ ### Game loop
221
+
222
+ `Clock` measures delta time from a `TimeSource`; `GameLoop` invokes your step function and asks the host for the next frame (`requestAnimationFrame`, `queueMicrotask` in tests, etc.).
223
+
224
+ ```typescript
225
+ import { Clock, GameLoop, World } from '@draug/engine';
226
+
227
+ const world = new World();
228
+ const clock = new Clock({ now: () => performance.now() });
229
+
230
+ const loop = new GameLoop(clock, (dt) => {
231
+ world.update(dt);
232
+ });
233
+
234
+ loop.start((cb) => {
235
+ requestAnimationFrame(cb);
236
+ });
237
+
238
+ // loop.stop() when shutting down
239
+ ```
240
+
241
+ In Node, `performance.now()` is available on modern versions; otherwise pass `{ now: () => Date.now() }` (millisecond resolution).
242
+
243
+ ### Runtime (optional)
244
+
245
+ `Runtime` is a tiny wrapper: `update(dt)` forwards to `world.update(dt)`. In the Amber workspace it is usually constructed together with `@draug/assets`’s `AssetsManager` for loading textures and similar; for a headless server or a toy sim you can ignore `Runtime` and call `world.update` directly from your own driver.
246
+
247
+ ## Configuration
248
+
249
+ | Area | What you can tune |
250
+ |------|---------------------|
251
+ | **World** | `new World(maxEntityCount?)` — caps sparse sets / bitmaps (default is large; lower for fixed small games if you care about memory). |
252
+ | **Components** | `components.register(Type, { factory })` for pooled defaults; `ComponentStorageType.SINGLETON_STORAGE` + `factory` for global state. |
253
+ | **Systems** | `@System({ query, requiredComponents, computeAfter })` for selection, extra storages, and ordering. |
254
+ | **Clock / loop** | Inject any `TimeSource`; drive the loop with the scheduling primitive your platform gives you. |
255
+
256
+ ## Best practices
257
+
258
+ 1. **Register components before first `getStorage`** — registration is explicit; the world will throw if a system touches an unknown component type.
259
+ 2. **Treat commands as “end of frame”** — if something must exist in the same system pass, use `addComponent` / `entities.create` directly (or split into a later system).
260
+
261
+ ## Contributing
262
+
263
+ Issues and PRs are welcome in the [GitHub repository](https://github.com/yazmeyaa). If you change public API or query behaviour, add or update a small reproduction so we can turn it into a regression test later (the package is still light on automated tests).
264
+
265
+ ## Author & support
266
+
267
+ - **Author:** future_undefined — [GitHub @yazmeyaa](https://github.com/yazmeyaa) · [evgenijantonenkov456@gmail.com](mailto:evgenijantonenkov456@gmail.com)
268
+
269
+ Related workspace packages: `@draug/core` (DAG sort, object pools, etc.) and `@draug/types` (shared `ClassType` helpers). Everything this library exposes to apps is listed in `src/index.ts` in the repository.
270
+
271
+ ## License
272
+
273
+ GPL-3.0-only — see [LICENSE](https://www.gnu.org/licenses/gpl-3.0.html) for the full text.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draug/engine",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "future_undefined",
@@ -11,7 +11,7 @@
11
11
  "license": "GPL-3.0-only",
12
12
  "readme": "README.md",
13
13
  "dependencies": {
14
- "@draug/core": "^1.0.1",
14
+ "@draug/core": "^1.0.2",
15
15
  "bitmap-index": "^1.0.9",
16
16
  "ts-sparse-set": "^1.0.5"
17
17
  },