@oasys/oecs 0.3.1 → 0.4.0

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 (204) hide show
  1. package/README.md +196 -267
  2. package/dist/array-CxbPyiHO.cjs +1 -0
  3. package/dist/array-uFR7Q8fU.js +132 -0
  4. package/dist/core/ecs/access_check.d.ts +77 -0
  5. package/dist/core/ecs/access_check.d.ts.map +1 -0
  6. package/dist/core/ecs/archetype.d.ts +474 -0
  7. package/dist/core/ecs/archetype.d.ts.map +1 -0
  8. package/dist/core/ecs/builtin_relations.d.ts +43 -0
  9. package/dist/core/ecs/builtin_relations.d.ts.map +1 -0
  10. package/dist/core/ecs/command_log.d.ts +113 -0
  11. package/dist/core/ecs/command_log.d.ts.map +1 -0
  12. package/dist/core/ecs/component.d.ts +106 -0
  13. package/dist/core/ecs/component.d.ts.map +1 -0
  14. package/dist/core/ecs/compute_backend.d.ts +37 -0
  15. package/dist/core/ecs/compute_backend.d.ts.map +1 -0
  16. package/dist/core/ecs/dispatch_trace.d.ts +93 -0
  17. package/dist/core/ecs/dispatch_trace.d.ts.map +1 -0
  18. package/dist/core/ecs/ecs.d.ts +533 -0
  19. package/dist/core/ecs/ecs.d.ts.map +1 -0
  20. package/dist/core/ecs/ecs_memory.d.ts +179 -0
  21. package/dist/core/ecs/ecs_memory.d.ts.map +1 -0
  22. package/dist/core/ecs/entity.d.ts +28 -0
  23. package/dist/core/ecs/entity.d.ts.map +1 -0
  24. package/dist/core/ecs/event.d.ts +54 -0
  25. package/dist/core/ecs/event.d.ts.map +1 -0
  26. package/dist/core/ecs/frame_trace.d.ts +133 -0
  27. package/dist/core/ecs/frame_trace.d.ts.map +1 -0
  28. package/dist/core/ecs/host_commands.d.ts +252 -0
  29. package/dist/core/ecs/host_commands.d.ts.map +1 -0
  30. package/dist/core/ecs/index.d.ts +41 -0
  31. package/dist/core/ecs/index.d.ts.map +1 -0
  32. package/dist/core/ecs/observer.d.ts +142 -0
  33. package/dist/core/ecs/observer.d.ts.map +1 -0
  34. package/dist/core/ecs/query.d.ts +557 -0
  35. package/dist/core/ecs/query.d.ts.map +1 -0
  36. package/dist/core/ecs/ref.d.ts +31 -0
  37. package/dist/core/ecs/ref.d.ts.map +1 -0
  38. package/dist/core/ecs/relation.d.ts +231 -0
  39. package/dist/core/ecs/relation.d.ts.map +1 -0
  40. package/dist/core/ecs/resource.d.ts +33 -0
  41. package/dist/core/ecs/resource.d.ts.map +1 -0
  42. package/dist/core/ecs/resume.d.ts +85 -0
  43. package/dist/core/ecs/resume.d.ts.map +1 -0
  44. package/dist/core/ecs/run_condition.d.ts +75 -0
  45. package/dist/core/ecs/run_condition.d.ts.map +1 -0
  46. package/dist/core/ecs/schedule.d.ts +133 -0
  47. package/dist/core/ecs/schedule.d.ts.map +1 -0
  48. package/dist/core/ecs/sparse_store.d.ts +107 -0
  49. package/dist/core/ecs/sparse_store.d.ts.map +1 -0
  50. package/dist/core/ecs/store.d.ts +1149 -0
  51. package/dist/core/ecs/store.d.ts.map +1 -0
  52. package/dist/core/ecs/store_layout_listener.d.ts +23 -0
  53. package/dist/core/ecs/store_layout_listener.d.ts.map +1 -0
  54. package/dist/core/ecs/system.d.ts +134 -0
  55. package/dist/core/ecs/system.d.ts.map +1 -0
  56. package/dist/core/ecs/utils/arrays.d.ts +7 -0
  57. package/dist/core/ecs/utils/arrays.d.ts.map +1 -0
  58. package/dist/core/ecs/utils/constants.d.ts +12 -0
  59. package/dist/core/ecs/utils/constants.d.ts.map +1 -0
  60. package/dist/core/ecs/utils/error.d.ts +51 -0
  61. package/dist/core/ecs/utils/error.d.ts.map +1 -0
  62. package/dist/core/reactive/array.d.ts +24 -0
  63. package/dist/core/reactive/array.d.ts.map +1 -0
  64. package/dist/core/reactive/index.cjs +1 -0
  65. package/dist/core/reactive/index.d.ts +10 -0
  66. package/dist/core/reactive/index.d.ts.map +1 -0
  67. package/dist/core/reactive/index.js +17 -0
  68. package/dist/core/reactive/interop.d.ts +19 -0
  69. package/dist/core/reactive/interop.d.ts.map +1 -0
  70. package/dist/core/reactive/kernel.d.ts +71 -0
  71. package/dist/core/reactive/kernel.d.ts.map +1 -0
  72. package/dist/core/reactive/map.d.ts +16 -0
  73. package/dist/core/reactive/map.d.ts.map +1 -0
  74. package/dist/core/reactive/struct.d.ts +10 -0
  75. package/dist/core/reactive/struct.d.ts.map +1 -0
  76. package/dist/core/store/__generated__/abi.d.ts +43 -0
  77. package/dist/core/store/__generated__/abi.d.ts.map +1 -0
  78. package/dist/core/store/action_ring.d.ts +136 -0
  79. package/dist/core/store/action_ring.d.ts.map +1 -0
  80. package/dist/core/store/allocator.d.ts +238 -0
  81. package/dist/core/store/allocator.d.ts.map +1 -0
  82. package/dist/core/store/buffer_backed_column.d.ts +69 -0
  83. package/dist/core/store/buffer_backed_column.d.ts.map +1 -0
  84. package/dist/core/store/column_store.d.ts +265 -0
  85. package/dist/core/store/column_store.d.ts.map +1 -0
  86. package/dist/core/store/command_dispatch.d.ts +52 -0
  87. package/dist/core/store/command_dispatch.d.ts.map +1 -0
  88. package/dist/core/store/command_ring.d.ts +107 -0
  89. package/dist/core/store/command_ring.d.ts.map +1 -0
  90. package/dist/core/store/descriptor.d.ts +80 -0
  91. package/dist/core/store/descriptor.d.ts.map +1 -0
  92. package/dist/core/store/entity_index.d.ts +108 -0
  93. package/dist/core/store/entity_index.d.ts.map +1 -0
  94. package/dist/core/store/event_ring.d.ts +95 -0
  95. package/dist/core/store/event_ring.d.ts.map +1 -0
  96. package/dist/core/store/extend.d.ts +109 -0
  97. package/dist/core/store/extend.d.ts.map +1 -0
  98. package/dist/core/store/grow.d.ts +39 -0
  99. package/dist/core/store/grow.d.ts.map +1 -0
  100. package/dist/core/store/header.d.ts +64 -0
  101. package/dist/core/store/header.d.ts.map +1 -0
  102. package/dist/core/store/index.d.ts +16 -0
  103. package/dist/core/store/index.d.ts.map +1 -0
  104. package/dist/core/store/region_table.d.ts +74 -0
  105. package/dist/core/store/region_table.d.ts.map +1 -0
  106. package/dist/core/store/snapshot.d.ts +43 -0
  107. package/dist/core/store/snapshot.d.ts.map +1 -0
  108. package/dist/core/store/state_hash.d.ts +38 -0
  109. package/dist/core/store/state_hash.d.ts.map +1 -0
  110. package/dist/core/store/store_regions.d.ts +38 -0
  111. package/dist/core/store/store_regions.d.ts.map +1 -0
  112. package/dist/extensions/editor/editor.d.ts +149 -0
  113. package/dist/extensions/editor/editor.d.ts.map +1 -0
  114. package/dist/extensions/editor/field_handle.d.ts +35 -0
  115. package/dist/extensions/editor/field_handle.d.ts.map +1 -0
  116. package/dist/extensions/editor/index.cjs +1 -0
  117. package/dist/extensions/editor/index.d.ts +21 -0
  118. package/dist/extensions/editor/index.d.ts.map +1 -0
  119. package/dist/extensions/editor/index.js +209 -0
  120. package/dist/extensions/reactive/ecs_sync.d.ts +210 -0
  121. package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -0
  122. package/dist/extensions/reactive/index.cjs +1 -0
  123. package/dist/extensions/reactive/index.d.ts +23 -0
  124. package/dist/extensions/reactive/index.d.ts.map +1 -0
  125. package/dist/extensions/reactive/index.js +225 -0
  126. package/dist/extensions/solid/index.cjs +1 -0
  127. package/dist/extensions/solid/index.d.ts +6 -0
  128. package/dist/extensions/solid/index.d.ts.map +1 -0
  129. package/dist/extensions/solid/index.js +32 -0
  130. package/dist/extensions/solid/kernel_solid.d.ts +42 -0
  131. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -0
  132. package/dist/index.cjs +2 -1
  133. package/dist/index.d.ts +16 -12
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +8565 -1310
  136. package/dist/interop-CT-REx0W.cjs +1 -0
  137. package/dist/interop-CcY6ASQc.js +18 -0
  138. package/dist/kernel-DgyrLFjW.js +227 -0
  139. package/dist/kernel-yWV3XnAb.cjs +1 -0
  140. package/dist/log/console_sink.d.ts +4 -0
  141. package/dist/log/console_sink.d.ts.map +1 -0
  142. package/dist/log/index.d.ts +3 -0
  143. package/dist/log/index.d.ts.map +1 -0
  144. package/dist/log/logger.d.ts +27 -0
  145. package/dist/log/logger.d.ts.map +1 -0
  146. package/dist/primitives.cjs +1 -0
  147. package/dist/primitives.d.ts +18 -0
  148. package/dist/primitives.d.ts.map +1 -0
  149. package/dist/primitives.js +44 -0
  150. package/dist/shared-BXSZnxx4.cjs +1 -0
  151. package/dist/shared-C678TAPY.js +99 -0
  152. package/dist/shared.cjs +1 -0
  153. package/dist/shared.d.ts +22 -0
  154. package/dist/shared.d.ts.map +1 -0
  155. package/dist/shared.js +7 -0
  156. package/dist/topological_sort-DlRpSrxu.js +391 -0
  157. package/dist/topological_sort-WAT-VHb-.cjs +1 -0
  158. package/dist/type_primitives/assertions.d.ts +12 -8
  159. package/dist/type_primitives/assertions.d.ts.map +1 -1
  160. package/dist/type_primitives/binary_heap/binary_heap.d.ts +6 -2
  161. package/dist/type_primitives/binary_heap/binary_heap.d.ts.map +1 -1
  162. package/dist/type_primitives/bitset/bitset.d.ts +16 -4
  163. package/dist/type_primitives/bitset/bitset.d.ts.map +1 -1
  164. package/dist/type_primitives/brand.d.ts +6 -1
  165. package/dist/type_primitives/brand.d.ts.map +1 -1
  166. package/dist/type_primitives/error.d.ts +4 -0
  167. package/dist/type_primitives/error.d.ts.map +1 -1
  168. package/dist/type_primitives/index.d.ts +3 -0
  169. package/dist/type_primitives/index.d.ts.map +1 -1
  170. package/dist/type_primitives/sparse_map/sparse_map.d.ts +7 -3
  171. package/dist/type_primitives/sparse_map/sparse_map.d.ts.map +1 -1
  172. package/dist/type_primitives/sparse_set/sparse_set.d.ts +4 -0
  173. package/dist/type_primitives/sparse_set/sparse_set.d.ts.map +1 -1
  174. package/dist/type_primitives/topological_sort/topological_sort.d.ts +7 -3
  175. package/dist/type_primitives/topological_sort/topological_sort.d.ts.map +1 -1
  176. package/dist/type_primitives/typed_arrays/typed_arrays.d.ts +53 -16
  177. package/dist/type_primitives/typed_arrays/typed_arrays.d.ts.map +1 -1
  178. package/dist/utils/arrays.d.ts +1 -1
  179. package/dist/utils/arrays.d.ts.map +1 -1
  180. package/dist/utils/error.d.ts +2 -20
  181. package/dist/utils/error.d.ts.map +1 -1
  182. package/package.json +36 -9
  183. package/dist/archetype.d.ts +0 -108
  184. package/dist/archetype.d.ts.map +0 -1
  185. package/dist/component.d.ts +0 -45
  186. package/dist/component.d.ts.map +0 -1
  187. package/dist/ecs.d.ts +0 -104
  188. package/dist/ecs.d.ts.map +0 -1
  189. package/dist/entity.d.ts +0 -11
  190. package/dist/entity.d.ts.map +0 -1
  191. package/dist/event.d.ts +0 -30
  192. package/dist/event.d.ts.map +0 -1
  193. package/dist/query.d.ts +0 -94
  194. package/dist/query.d.ts.map +0 -1
  195. package/dist/ref.d.ts +0 -23
  196. package/dist/ref.d.ts.map +0 -1
  197. package/dist/resource.d.ts +0 -23
  198. package/dist/resource.d.ts.map +0 -1
  199. package/dist/schedule.d.ts +0 -45
  200. package/dist/schedule.d.ts.map +0 -1
  201. package/dist/store.d.ts +0 -118
  202. package/dist/store.d.ts.map +0 -1
  203. package/dist/system.d.ts +0 -16
  204. package/dist/system.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,289 +1,188 @@
1
1
  # oecs
2
2
 
3
- A fast, minimal, archetype-based Entity Component System for TypeScript.
4
-
5
- ## Features
6
-
7
- - **Archetype-based SoA storage.** Entities sharing a component set share contiguous typed-array columns — cache-friendly loops, no per-entity object allocation.
8
- - **Phantom-typed components.** `ComponentDef<{ x: "f64", y: "f64" }>` is a branded integer at runtime and a fully-typed schema at compile time. Misspelled fields are compile errors.
9
- - **Callback iteration.** `query.for_each(arch => ...)` yields non-empty archetypes; you write the row loop over typed-array columns.
10
- - **Tick-based change detection.** Each `(archetype, component)` tracks a change tick. `query.changed(Pos).for_each(...)` visits only archetypes mutated since the system's last run.
11
- - **Key-based events and resources.** `event_key<F>` / `resource_key<T>` create module-scope symbol handles carrying their schema as a phantom — import the key anywhere.
12
- - **Cached single-entity refs.** `ctx.ref` / `ctx.ref_mut` give ergonomic `pos.x += vel.vx * dt` that compiles to a direct typed-array index op.
13
- - **Deferred structural changes.** `ctx.add_component` / `ctx.remove_component` / `ctx.destroy_entity` buffer until the schedule flushes between phases, so iterators stay valid.
14
- - **Topological scheduler.** Per-phase Kahn's-algorithm sort over a binary heap, with insertion order as a deterministic tiebreaker.
15
- - **Fixed timestep.** Accumulator loop with configurable `fixed_timestep` and spiral-of-death protection.
16
- - **Reusable primitives.** `BitSet`, `SparseSet`, `SparseMap`, `GrowableTypedArray`, `BinaryHeap`, `topological_sort` all exported.
3
+ **A full-featured, archetype-based Entity Component System for TypeScript.**
4
+
5
+ `@oasys/oecs` is a complete ECS — not just storage-and-queries, but the whole toolkit you expect from a
6
+ mature engine: observers, relations with wildcards, sparse storage, system sets and run conditions,
7
+ entity enable/disable, templates, deterministic hashing with snapshot/restore, a typed host→ECS write
8
+ seam, and an optional reactive UI bridge. It is **pure TypeScript and zero-dependency by default** it
9
+ runs over a plain resizable `ArrayBuffer`, so it needs no `SharedArrayBuffer` and no cross-origin
10
+ isolation (COOP/COEP). An opt-in shared-memory profile swaps in a `SharedArrayBuffer` for worker offload
11
+ or a WASM compute backend; both profiles share one core and agree, byte-for-byte, on `stateHash`.
12
+
13
+ - **Fast** struct-of-arrays column storage grouped by archetype; iteration is a tight loop over typed
14
+ arrays with no per-entity object allocation.
15
+ - **Type-safe** components are branded integers at runtime and fully-typed schemas at compile time;
16
+ misspelled fields are compile errors.
17
+ - **Deterministic** — an opt-in mode gives a backing-agnostic `stateHash` plus snapshot/restore and
18
+ command-log replay.
19
+ - **Complete** — the feature surface below is the whole engine, not a starting point.
17
20
 
18
21
  ## Installation
19
22
 
20
23
  ```bash
21
- pnpm add @oasys/oecs
24
+ pnpm add @oasys/oecs # npm / pnpm / yarn
25
+ # or
26
+ deno add jsr:@oasys/oecs # JSR (Deno)
27
+ # or
28
+ npx jsr add @oasys/oecs # JSR (npm-compatible)
22
29
  ```
23
30
 
24
31
  ## Quick start
25
32
 
26
33
  ```ts
27
- import { ECS, SCHEDULE, event_key, resource_key } from "@oasys/oecs";
28
-
29
- // Keysmodule scope, phantom-typed
30
- const Time = resource_key<{ delta: number; elapsed: number }>("Time");
31
- const DamageEvent = event_key<readonly ["target", "amount"]>("Damage");
32
-
33
- const world = new ECS();
34
-
35
- // Components
36
- const Pos = world.register_component({ x: "f64", y: "f64" });
37
- const Vel = world.register_component(["vx", "vy"] as const);
38
-
39
- // Resources & events
40
- world.register_resource(Time, { delta: 0, elapsed: 0 });
41
- world.register_event(DamageEvent, ["target", "amount"] as const);
42
-
43
- // Entities
44
- const e = world.create_entity();
45
- world.add_components(e, [
46
- { def: Pos, values: { x: 0, y: 0 } },
47
- { def: Vel, values: { vx: 100, vy: 50 } },
48
- ]);
49
-
50
- // System — query resolved once at registration
51
- const moveSys = world.register_system(
52
- (q, ctx, dt) => {
53
- q.for_each((arch) => {
54
- const px = arch.get_column_mut(Pos, "x", ctx.world_tick);
55
- const py = arch.get_column_mut(Pos, "y", ctx.world_tick);
56
- const vx = arch.get_column(Vel, "vx");
57
- const vy = arch.get_column(Vel, "vy");
58
- for (let i = 0; i < arch.entity_count; i++) {
59
- px[i] += vx[i] * dt;
60
- py[i] += vy[i] * dt;
34
+ import { ECS, SCHEDULE } from "@oasys/oecs";
35
+
36
+ const world = new ECS(); // pure-TS heap profile no SharedArrayBuffer needed
37
+
38
+ // Components record syntax (per-field type) or array shorthand (defaults to "f64")
39
+ const Pos = world.registerComponent({ x: "f64", y: "f64" });
40
+ const Vel = world.registerComponent(["vx", "vy"] as const);
41
+
42
+ // A query is a live, cached view over matching archetypes — build it once, reuse it.
43
+ const movers = world.query(Pos, Vel);
44
+
45
+ // Systems declare the components they read/write (checked in dev builds).
46
+ const move = world.registerSystem({
47
+ reads: [Vel],
48
+ writes: [Pos], // a declared write implies read of the same component
49
+ fn: (ctx, dt) => {
50
+ movers.eachChunk((cols, count) => {
51
+ const { x, y } = cols.mut(Pos); // whole group; stamps Pos's change tick once
52
+ const { vx, vy } = cols.read(Vel); // read-only group
53
+ for (let i = 0; i < count; i++) {
54
+ x[i] += vx[i] * dt;
55
+ y[i] += vy[i] * dt;
61
56
  }
62
57
  });
63
58
  },
64
- (qb) => qb.every(Pos, Vel),
65
- );
66
-
67
- world.add_systems(SCHEDULE.UPDATE, moveSys);
68
- world.startup();
69
-
70
- let last = performance.now();
71
- function frame() {
72
- const now = performance.now();
73
- const dt = (now - last) / 1000;
74
- last = now;
75
- const t = world.resource(Time);
76
- world.set_resource(Time, { delta: dt, elapsed: t.elapsed + dt });
77
- world.update(dt);
78
- requestAnimationFrame(frame);
79
- }
80
- requestAnimationFrame(frame);
81
- ```
82
-
83
- ## World options
84
-
85
- ```ts
86
- const world = new ECS({
87
- initial_capacity: 4096,
88
- fixed_timestep: 1 / 50,
89
- max_fixed_steps: 4,
90
59
  });
91
- ```
92
-
93
- | Option | Type | Default | Description |
94
- | ------------------ | -------- | ------- | ----------- |
95
- | `initial_capacity` | `number` | `1024` | Starting size of each archetype's entity-ID and column buffers. Buffers double on overflow; pick close to your expected per-archetype entity count to avoid early reallocations. |
96
- | `fixed_timestep` | `number` | `1/60` | Interval (seconds) at which `SCHEDULE.FIXED_UPDATE` systems run. |
97
- | `max_fixed_steps` | `number` | `5` | Hard cap on fixed-update iterations per frame. Protects against spiral of death. |
98
-
99
- ## Components
100
-
101
- Records give per-field type control; array shorthand defaults to `f64`. Tags have no fields.
102
-
103
- ```ts
104
- const Pos = world.register_component({ x: "f64", y: "f64" });
105
- const Health = world.register_component({ current: "i32", max: "i32" });
106
- const Vel = world.register_component(["vx", "vy"] as const);
107
- const IsEnemy = world.register_tag();
108
-
109
- world.add_components(e, [
110
- { def: Pos, values: { x: 0, y: 0 } },
111
- { def: Vel, values: { vx: 1, vy: 0 } },
112
- { def: IsEnemy },
113
- ]);
114
- ```
115
-
116
- Supported tags: `f32`, `f64`, `i8`, `i16`, `i32`, `u8`, `u16`, `u32`.
117
-
118
- See [docs/api/components.md](docs/api/components.md).
119
-
120
- ## Queries
121
-
122
- Live, cached views over matching archetypes. Iterate with `for_each`.
123
-
124
- ```ts
125
- const q = world.query(Pos, Vel);
126
-
127
- q.for_each((arch) => {
128
- const px = arch.get_column(Pos, "x");
129
- const py = arch.get_column(Pos, "y");
130
- for (let i = 0; i < arch.entity_count; i++) { /* ... */ }
131
- });
132
-
133
- // Chaining returns new cached queries
134
- const targets = world.query(Pos).and(Health).not(Shield).any_of(IsEnemy, IsBoss);
135
-
136
- // Change detection — only archetypes whose Pos column changed since last run
137
- q.changed(Pos).for_each((arch) => { /* ... */ });
138
- ```
139
-
140
- See [docs/api/queries.md](docs/api/queries.md) and [docs/api/change-detection.md](docs/api/change-detection.md).
141
-
142
- ## Systems
143
-
144
- Systems are plain functions. Three registration shapes all return a `SystemDescriptor`.
145
-
146
- ```ts
147
- // Bare function
148
- const logSys = world.register_system((ctx, dt) => { /* ... */ });
149
-
150
- // Function + query builder (query resolved once at registration)
151
- const moveSys = world.register_system(
152
- (q, ctx, dt) => { q.for_each((arch) => { /* ... */ }); },
153
- (qb) => qb.every(Pos, Vel),
154
- );
155
-
156
- // Full config — lifecycle hooks, name
157
- const spawnSys = world.register_system({
158
- name: "spawn",
159
- fn(ctx, dt) { /* every frame */ },
160
- on_added(ctx) { /* once during world.startup() */ },
161
- dispose() { /* during world.dispose() */ },
162
- });
163
- ```
164
-
165
- `SystemContext` exposes deferred structural ops, per-entity access, events, resources, and tick bookkeeping (`ctx.world_tick`, `ctx.last_run_tick`).
166
-
167
- See [docs/api/systems.md](docs/api/systems.md).
168
-
169
- ## Resources
170
-
171
- Global singletons keyed by `ResourceKey<T>`. Values can be any type — objects, typed arrays, class instances.
172
-
173
- ```ts
174
- import { resource_key } from "@oasys/oecs";
175
-
176
- const Time = resource_key<{ delta: number; elapsed: number }>("Time");
177
- const Assets = resource_key<Map<string, ImageBitmap>>("Assets");
178
-
179
- world.register_resource(Time, { delta: 0, elapsed: 0 });
180
- world.register_resource(Assets, new Map());
181
-
182
- const t = world.resource(Time); // typed as { delta, elapsed }
183
- world.set_resource(Time, { delta: 0.016, elapsed: 0 }); // swap in a new value
184
- ```
185
-
186
- See [docs/api/resources.md](docs/api/resources.md).
187
-
188
- ## Events
189
60
 
190
- Fire-and-forget SoA channels. Data events carry typed fields; signals carry only a count. Cleared at the end of each `world.update(dt)`.
191
-
192
- ```ts
193
- import { event_key, signal_key } from "@oasys/oecs";
194
-
195
- const DamageEvent = event_key<readonly ["target", "amount"]>("Damage");
196
- const GameOver = signal_key("GameOver");
197
-
198
- world.register_event(DamageEvent, ["target", "amount"] as const);
199
- world.register_signal(GameOver);
200
-
201
- ctx.emit(DamageEvent, { target: victimId, amount: 25 });
202
- ctx.emit(GameOver);
203
-
204
- const dmg = ctx.read(DamageEvent);
205
- for (let i = 0; i < dmg.length; i++) {
206
- dmg.target[i]; dmg.amount[i]; // number columns
207
- }
208
- if (ctx.read(GameOver).length > 0) { /* fired */ }
209
- ```
210
-
211
- See [docs/api/events.md](docs/api/events.md).
212
-
213
- ## Refs
214
-
215
- Cached single-entity handles — resolve archetype + row + column once, then read/write fields by name.
216
-
217
- ```ts
218
- const pos = ctx.ref_mut(Pos, entity); // writable; bumps Pos change tick
219
- const vel = ctx.ref(Vel, entity); // readonly
220
- pos.x += vel.vx * dt;
221
- pos.y += vel.vy * dt;
222
- ```
223
-
224
- Prefer `ctx.ref` by default; reach for `ctx.ref_mut` at the point of mutation. Do not hold refs across archetype transitions or phase flushes.
225
-
226
- See [docs/api/refs.md](docs/api/refs.md).
227
-
228
- ## Schedule
229
-
230
- Seven phases run in a fixed order:
231
-
232
- | Phase | When | Typical use |
233
- | -------------- | -------------------------------- | ----------------------- |
234
- | `PRE_STARTUP` | Once, before `STARTUP` | Resource loading |
235
- | `STARTUP` | Once | Initial entity spawning |
236
- | `POST_STARTUP` | Once, after `STARTUP` | Validation |
237
- | `FIXED_UPDATE` | Zero+ times per frame (fixed dt) | Physics, simulation |
238
- | `PRE_UPDATE` | Every frame, first | Input, time |
239
- | `UPDATE` | Every frame | Game logic, AI |
240
- | `POST_UPDATE` | Every frame, last | Rendering, cleanup |
241
-
242
- ```ts
243
- world.add_systems(SCHEDULE.UPDATE, moveSys, damageSys, {
244
- system: deathSys,
245
- ordering: { after: [damageSys] },
246
- });
247
- ```
248
-
249
- Within a phase, systems are topologically sorted by `before` / `after` constraints. `ctx.flush()` runs automatically between phases.
250
-
251
- See [docs/api/schedule.md](docs/api/schedule.md).
61
+ world.addSystems(SCHEDULE.UPDATE, move);
62
+ world.startup();
252
63
 
253
- ## Entity lifecycle
64
+ const e = world.createEntity();
65
+ world.addComponent(e, Pos, { x: 0, y: 0 });
66
+ world.addComponent(e, Vel, { vx: 100, vy: 50 });
254
67
 
255
- ```ts
256
- const e = world.create_entity();
257
- world.is_alive(e); // true
258
- world.destroy_entity_deferred(e);
259
- world.flush();
260
- world.is_alive(e); // false
68
+ world.update(1 / 60);
69
+ world.getField(e, Pos, "x"); // ≈ 1.667
261
70
  ```
262
71
 
263
- `EntityID` is a packed 31-bit integer (20-bit slot index, 11-bit generation). Destroying an entity bumps its slot's generation, so stale handles are detected as dead. Inside systems, use `ctx.create_entity()` (immediate) and `ctx.destroy_entity(e)` (deferred).
264
-
265
- See [docs/api/entities.md](docs/api/entities.md).
266
-
267
- ## Dev vs Prod modes
268
-
269
- A compile-time `__DEV__` flag gates runtime sanity checks: bounds checks, liveness checks, duplicate-system detection, and registration validation. These are tree-shaken out of production bundles by the Vite build. Scheduler cycle detection is always active and throws `ECS_ERROR.CIRCULAR_SYSTEM_DEPENDENCY` on the first offending run.
270
-
271
- ## Development
272
-
273
- ```bash
274
- pnpm install
275
- pnpm test # vitest
276
- pnpm bench # vitest bench
277
- pnpm build # vite library build
278
- pnpm tsc --noEmit # type check
279
- ```
280
-
281
- ## Guides
72
+ ## Features
282
73
 
283
- - [Getting Started](docs/GETTING_STARTED.md) — step-by-step tutorial.
284
- - [Best Practices](docs/BEST_PRACTICES.md) — component design, query patterns, pitfalls.
285
- - [Architecture](docs/ARCHITECTURE.md)data layout, flush model, cache invalidation.
286
- - API reference:
74
+ **Storage & data model**
75
+
76
+ - **Archetype SoA storage** over a backing-neutral `ColumnStore` entities with the same component set
77
+ share contiguous typed-array columns; cache-friendly loops, no per-entity object allocation.
78
+ - **Phantom-typed components** — `registerComponent({ x: "f64", y: "f64" })` is a branded integer at
79
+ runtime and a fully-typed schema at compile time. Record syntax for per-field types, array shorthand
80
+ for uniform `f64`, and `registerTag()` for data-free markers. Field types: `f32 f64 i8 i16 i32 u8 u16 u32`.
81
+ - **Two storage profiles, one core** — pure-TS heap (`ArrayBuffer`) by default; opt-in
82
+ `SharedArrayBuffer` for workers / WASM. Same code path, same `stateHash`, sized through a single
83
+ `memory` surface (entity budget, byte cap, or pinned capacity).
84
+
85
+ **Queries**
86
+
87
+ - **Live, cached queries** — `world.query(Pos, Vel)` refined with `.and()` / `.without()` / `.anyOf()`;
88
+ new matching archetypes are pushed in automatically.
89
+ - **Two iteration verbs** — `forEach(arch => …)` for read-only archetype iteration, `eachChunk((cols, count) => …)`
90
+ for the mutable hot path (`cols.mut` / `cols.read` resolve a whole component's columns at once).
91
+ - **Change detection** — per-`(archetype, component)` change ticks; `query.changed(Pos)` visits only
92
+ archetypes written since the system's threshold tick.
93
+ - **Relation & hierarchy queries** — `(R, *)` / `(*, T)` wildcards, `forEachRelatedTo`, and
94
+ `query.hierarchy(rel, depth)`. **Sparse queries** via `query.withSparse(...)`; disabled entities are
95
+ skipped unless you opt in with `query.includeDisabled()`.
96
+
97
+ **Systems & scheduling**
98
+
99
+ - **Declarative systems** — plain functions in a `SystemConfig` declaring `reads` / `writes`, enforced by
100
+ a dev-mode access checker (tree-shaken in production). Bare `(ctx, dt)` and `(q, ctx, dt)` +
101
+ query-builder overloads exist for access-free glue; lifecycle hooks `onAdded` / `onRemoved` / `dispose`;
102
+ `exclusive: true` for full-world setup/teardown.
103
+ - **Topological scheduler** — seven phases (`PRE_STARTUP` → `STARTUP` → `POST_STARTUP`, `FIXED_UPDATE`,
104
+ `PRE_UPDATE` → `UPDATE` → `POST_UPDATE`); per-phase Kahn sort by `before` / `after`, with insertion
105
+ order as a deterministic tiebreaker. Always-on cycle detection.
106
+ - **Fixed timestep** — accumulator loop with configurable `fixedTimestep` and spiral-of-death protection.
107
+ - **System sets & run conditions** — `systemSet(...)` + `configureSet(...)`; `runIfResourceEq`,
108
+ `runEveryNTicks`, `runIfAnyMatch`, and custom `RunCondition`s.
109
+
110
+ **Structural changes**
111
+
112
+ - **Deferred by default** — `ctx.commands` (a Bevy-`Commands`-style facade) buffers
113
+ spawn / add / remove / despawn / enable / disable until the phase flush, so iterators stay valid.
114
+ `world.addComponent` etc. are the immediate counterparts.
115
+ - **Entity enable/disable** — `disable` / `enable` / `isDisabled`; disabled rows sit in a partitioned
116
+ tail and are skipped by default queries.
117
+ - **Templates & bundles** — `world.template([...])` blueprints consumed by `createEntity` /
118
+ `createEntities` for zero-transition spawns; `bundle(...)` + `spawnBundle(...)`.
119
+
120
+ **Reactivity & relationships**
121
+
122
+ - **Observers** — `world.observe(...)` for `onAdd` / `onRemove` / `onSet` / `onEnable` / `onDisable`,
123
+ structural or per-entity.
124
+ - **Relations** — `(relation, target)` pairs with `ChildOf` / `IsA` presets, exclusive / multi arities,
125
+ bidirectional queries (`targetOf` / `sourcesOf` / `ancestorsOf` / `rootOf` / `cascadeOf`), and
126
+ configurable on-delete cleanup (`delete` / `clear` / `orphan`). Stored sparsely — no archetype
127
+ transition, no identity bit.
128
+ - **Sparse storage** — `registerSparseComponent` / `registerSparseTag`, `addSparse` / `removeSparse` for
129
+ churny or rare data that shouldn't cause archetype transitions.
130
+ - **Resources** — typed global singletons via `resourceKey<T>`. **Events** — fire-and-forget SoA channels
131
+ via `eventKey<F>` / `signalKey`, cleared at the end of each `update`.
132
+ - **Cached refs** — `ctx.ref(def, e)` (mutable, bumps the change tick) / `ctx.refRead(def, e)`
133
+ (read-only): resolve archetype + row + column once, then `pos.x += vel.vx * dt`.
134
+
135
+ **Determinism, persistence & integration**
136
+
137
+ - **Determinism** (opt-in) — `new ECS({ deterministic: true })`, then `world.stateHash()` (FNV-1a over
138
+ live column bytes), `snapshot()` / `restoreInto(...)`, plus sparse variants. Backing-agnostic: a heap
139
+ world and a shared world with identical history produce identical hashes.
140
+ - **Host → ECS write seam** — `installHostCommandSeam(world)` applies typed `HostCommand`s off-schedule
141
+ via a blessed `exclusive` system, with record/replay (`HostCommandRecorder`, `replayCommandLog`) and a
142
+ cross-thread ring transport.
143
+ - **Reactive UI seam** (optional) — a zero-dep signals kernel (`@oasys/oecs/reactive`), an ECS→reactive
144
+ bridge that publishes only dirty entities/columns (`@oasys/oecs/reactive-sync`), and a SolidJS adapter
145
+ (`@oasys/oecs/solid`).
146
+ - **Editor layer** — undo/redo + field handles over the write seam (`@oasys/oecs/editor`).
147
+ - **Frame tracing** — `world.setTrace(sink)` + `FrameTraceRecorder` for a structured per-frame event
148
+ stream (dev-gated).
149
+ - **Compute backend seam** — `world.attachBackend(...)` to run a system body on a compiled backend (WASM,
150
+ …) instead of its TS closure.
151
+
152
+ **Reference**
153
+
154
+ - **Typed errors** — an `ECSError` taxonomy with a `category` enum and an `isEcsError` guard, all exported.
155
+ - **Reusable primitives** (`@oasys/oecs/primitives`) — `BitSet`, `SparseSet`, `SparseMap`,
156
+ `GrowableTypedArray`, `BinaryHeap`, and `topologicalSort`, usable standalone.
157
+
158
+ ## Entry points
159
+
160
+ The core is `@oasys/oecs`; everything else is opt-in and costs nothing until imported.
161
+
162
+ | Import | What it is |
163
+ | --- | --- |
164
+ | `@oasys/oecs` | the ECS — pure-TS heap profile by default |
165
+ | `@oasys/oecs/shared` | opt-in `SharedArrayBuffer` allocators for worker offload / a WASM backend (needs COOP/COEP) |
166
+ | `@oasys/oecs/reactive` | zero-dependency reactive kernel (`signal`/`computed`/`effect`, reactive collections) |
167
+ | `@oasys/oecs/reactive-sync` | ECS→reactive bridge — publishes only dirty entities/columns |
168
+ | `@oasys/oecs/editor` | undo/redo + field-handle layer over the host-write seam |
169
+ | `@oasys/oecs/solid` | SolidJS adapter (`solid-js` is an **optional** peer dependency) |
170
+ | `@oasys/oecs/primitives` | the standalone data structures oecs is built on |
171
+
172
+ ## Dev vs prod
173
+
174
+ A compile-time `__DEV__` flag gates every runtime check — bounds and liveness checks, duplicate-system
175
+ detection, registration validation, and the system access checker (`reads`/`writes`). These are
176
+ **tree-shaken out of production builds**, so treat "throws in dev" as a development tripwire, not a
177
+ production guarantee. The scheduler's cycle detection is the one check that is always active.
178
+
179
+ ## Documentation
180
+
181
+ - **New to oecs?** Start with the [Getting Started](docs/GETTING_STARTED.md) tutorial, then
182
+ [Best Practices](docs/BEST_PRACTICES.md) and the [Architecture](docs/ARCHITECTURE.md) overview.
183
+ - **Upgrading from 0.3?** See the [Migration guide (0.3 → 0.4)](docs/MIGRATION-0.3-to-0.4.md) and the
184
+ [CHANGELOG](CHANGELOG.md).
185
+ - **Full API reference** — start at the [reference index](docs/api/index.md):
287
186
  [components](docs/api/components.md) ·
288
187
  [entities](docs/api/entities.md) ·
289
188
  [queries](docs/api/queries.md) ·
@@ -293,8 +192,38 @@ pnpm tsc --noEmit # type check
293
192
  [events](docs/api/events.md) ·
294
193
  [refs](docs/api/refs.md) ·
295
194
  [change detection](docs/api/change-detection.md) ·
296
- [type primitives](docs/api/type-primitives.md)
195
+ [observers](docs/api/observers.md) ·
196
+ [relations](docs/api/relations.md) ·
197
+ [sparse storage](docs/api/sparse-storage.md) ·
198
+ [determinism](docs/api/determinism.md) ·
199
+ [memory](docs/api/memory.md) ·
200
+ [host-write seam](docs/api/host-write-seam.md) ·
201
+ [reactive](docs/api/reactive.md) ·
202
+ [editor](docs/api/editor.md) ·
203
+ [tracing](docs/api/tracing.md) ·
204
+ [primitives](docs/api/primitives.md) ·
205
+ [errors](docs/api/errors.md)
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ pnpm install
211
+ pnpm test # vitest
212
+ pnpm bench # vitest bench
213
+ pnpm build # vite library build (multi-entry → dist/)
214
+ pnpm exec tsc --noEmit # type check
215
+ ```
216
+
217
+ ## Acknowledgements
218
+
219
+ oecs stands on the shoulders of the ECS community. Special thanks to:
220
+
221
+ - **[Bevy](https://bevyengine.org)**, **[Flecs](https://github.com/SanderMertens/flecs)**, and
222
+ **[bitECS](https://github.com/NateTheGreatt/bitECS)** — a constant source of inspiration; their
223
+ designs shaped how oecs approaches archetypes, relations, scheduling, and change detection.
224
+ - **[@clinuxrulz](https://github.com/clinuxrulz)** — for his amazing showcase and invaluable input on
225
+ the ECS.
297
226
 
298
227
  ## License
299
228
 
300
- MIT
229
+ [MIT](LICENSE)
@@ -0,0 +1 @@
1
+ "use strict";const g=require("./kernel-yWV3XnAb.cjs");function w(v=Object.is){const l=new Map,[t,f]=g.signal(0);let u=0;const h=(c,n)=>c!==void 0&&n!==void 0?v(c,n):c===n;return{get(c){const n=l.get(c);if(n!==void 0)return n[0]();t()},set(c,n){const s=l.get(c);s!==void 0?s[1](n):(l.set(c,g.signal(n,h)),f(++u))},delete(c){const n=l.get(c);return n===void 0?!1:(l.delete(c),g.batch(()=>{n[1](void 0),f(++u)}),!0)},has(c){return t(),l.has(c)},size(){return t(),l.size},keys(){return t(),[...l.keys()]}}}function M(v,l={}){const t={},f={},u=Object.keys(v),h=new Set(u);for(const n of u){const[s,y]=g.signal(v[n],l[n]);t[n]=s,f[n]=y}return[new Proxy({},{get:(n,s)=>h.has(s)?t[s]():Reflect.get(n,s),has:(n,s)=>h.has(s),ownKeys:()=>u,getOwnPropertyDescriptor:(n,s)=>h.has(s)?{get:()=>t[s](),enumerable:!0,configurable:!0}:void 0}),f]}const m=Symbol("reactiveArray.absent");function A(v=[],l=Object.is){const t=[],[f,u]=g.signal(0);let h=0;const c=(e,r)=>e!==m&&r!==m?l(e,r):e===r,n=e=>g.signal(e,c),s=e=>g.untrack(t[e][0]);for(const e of v)t.push(n(e));function y(e){g.batch(()=>{const r=t.length,a=e.length,p=a<r?a:r;for(let o=0;o<p;o++)t[o][1](e[o]);if(a>r){for(let o=r;o<a;o++)t.push(n(e[o]));u(++h)}else if(a<r){const o=t.splice(a);for(let d=0;d<o.length;d++)o[d][1](m);u(++h)}})}return{get(e){if(e>=0&&e<t.length){const r=t[e][0]();if(r!==m)return r}f()},length(){return f(),t.length},snapshot(){f();const e=new Array(t.length);for(let r=0;r<t.length;r++)e[r]=t[r][0]();return e},set(e,r){e>=0&&e<t.length&&t[e][1](r)},push(e){t.push(n(e)),u(++h)},pop(){const e=t.length;if(e===0)return;const r=s(e-1),[a]=t.splice(e-1);return g.batch(()=>{a[1](m),u(++h)}),r},splice(e,r,...a){const p=t.length,o=e<0?Math.max(p+e,0):Math.min(e,p),d=r===void 0?p-o:Math.max(0,Math.min(r,p-o)),S=[];for(let i=0;i<d;i++)S.push(s(o+i));const b=[];for(let i=0;i<o;i++)b.push(s(i));for(let i=0;i<a.length;i++)b.push(a[i]);for(let i=o+d;i<p;i++)b.push(s(i));return y(b),S},reconcile(e){y(e)}}}exports.reactiveArray=A;exports.reactiveMap=w;exports.reactiveStruct=M;
@@ -0,0 +1,132 @@
1
+ import { s as m, b as w, u as M } from "./kernel-DgyrLFjW.js";
2
+ function A(g = Object.is) {
3
+ const i = /* @__PURE__ */ new Map(), [t, f] = m(0);
4
+ let u = 0;
5
+ const a = (c, n) => c !== void 0 && n !== void 0 ? g(c, n) : c === n;
6
+ return {
7
+ get(c) {
8
+ const n = i.get(c);
9
+ if (n !== void 0) return n[0]();
10
+ t();
11
+ },
12
+ set(c, n) {
13
+ const s = i.get(c);
14
+ s !== void 0 ? s[1](n) : (i.set(c, m(n, a)), f(++u));
15
+ },
16
+ delete(c) {
17
+ const n = i.get(c);
18
+ return n === void 0 ? !1 : (i.delete(c), w(() => {
19
+ n[1](void 0), f(++u);
20
+ }), !0);
21
+ },
22
+ has(c) {
23
+ return t(), i.has(c);
24
+ },
25
+ size() {
26
+ return t(), i.size;
27
+ },
28
+ keys() {
29
+ return t(), [...i.keys()];
30
+ }
31
+ };
32
+ }
33
+ function O(g, i = {}) {
34
+ const t = {}, f = {}, u = Object.keys(g), a = new Set(u);
35
+ for (const n of u) {
36
+ const [s, y] = m(
37
+ g[n],
38
+ i[n]
39
+ );
40
+ t[n] = s, f[n] = y;
41
+ }
42
+ return [new Proxy({}, {
43
+ // A field read subscribes; a NON-field key must not throw. `JSON.stringify`
44
+ // (`toJSON`), `await proxy` (`then`), `String(proxy)` (`Symbol.toPrimitive`)
45
+ // and `for..of` (`Symbol.iterator`) all probe keys that aren't fields — fall
46
+ // through to the (empty) target so they see the ordinary undefined/inherited
47
+ // value instead of calling `undefined()`. `fieldSet` (not `k in reads`) so
48
+ // inherited `toString`/`constructor` aren't mistaken for fields.
49
+ get: (n, s) => a.has(s) ? t[s]() : Reflect.get(n, s),
50
+ has: (n, s) => a.has(s),
51
+ // Enumerable without subscribing: enumeration calls `ownKeys` +
52
+ // `getOwnPropertyDescriptor`, never `get`, so `Object.keys(proxy)` returns
53
+ // the field set and tracks nothing. The descriptor is an ACCESSOR whose `get`
54
+ // reads the live signal, so `{...proxy}` / `Object.values(proxy)` /
55
+ // `Object.getOwnPropertyDescriptor(proxy, f).value` see the current value
56
+ // (a value-less descriptor would normalize to `value: undefined`). Non-field
57
+ // keys report no own descriptor. The target is the empty (extensible) object,
58
+ // so these configurable own keys satisfy the Proxy invariants.
59
+ ownKeys: () => u,
60
+ getOwnPropertyDescriptor: (n, s) => a.has(s) ? { get: () => t[s](), enumerable: !0, configurable: !0 } : void 0
61
+ }), f];
62
+ }
63
+ const v = Symbol("reactiveArray.absent");
64
+ function j(g = [], i = Object.is) {
65
+ const t = [], [f, u] = m(0);
66
+ let a = 0;
67
+ const c = (e, r) => e !== v && r !== v ? i(e, r) : e === r, n = (e) => m(e, c), s = (e) => M(t[e][0]);
68
+ for (const e of g) t.push(n(e));
69
+ function y(e) {
70
+ w(() => {
71
+ const r = t.length, h = e.length, p = h < r ? h : r;
72
+ for (let o = 0; o < p; o++) t[o][1](e[o]);
73
+ if (h > r) {
74
+ for (let o = r; o < h; o++) t.push(n(e[o]));
75
+ u(++a);
76
+ } else if (h < r) {
77
+ const o = t.splice(h);
78
+ for (let d = 0; d < o.length; d++) o[d][1](v);
79
+ u(++a);
80
+ }
81
+ });
82
+ }
83
+ return {
84
+ get(e) {
85
+ if (e >= 0 && e < t.length) {
86
+ const r = t[e][0]();
87
+ if (r !== v) return r;
88
+ }
89
+ f();
90
+ },
91
+ length() {
92
+ return f(), t.length;
93
+ },
94
+ snapshot() {
95
+ f();
96
+ const e = new Array(t.length);
97
+ for (let r = 0; r < t.length; r++) e[r] = t[r][0]();
98
+ return e;
99
+ },
100
+ set(e, r) {
101
+ e >= 0 && e < t.length && t[e][1](r);
102
+ },
103
+ push(e) {
104
+ t.push(n(e)), u(++a);
105
+ },
106
+ pop() {
107
+ const e = t.length;
108
+ if (e === 0) return;
109
+ const r = s(e - 1), [h] = t.splice(e - 1);
110
+ return w(() => {
111
+ h[1](v), u(++a);
112
+ }), r;
113
+ },
114
+ splice(e, r, ...h) {
115
+ const p = t.length, o = e < 0 ? Math.max(p + e, 0) : Math.min(e, p), d = r === void 0 ? p - o : Math.max(0, Math.min(r, p - o)), S = [];
116
+ for (let l = 0; l < d; l++) S.push(s(o + l));
117
+ const b = [];
118
+ for (let l = 0; l < o; l++) b.push(s(l));
119
+ for (let l = 0; l < h.length; l++) b.push(h[l]);
120
+ for (let l = o + d; l < p; l++) b.push(s(l));
121
+ return y(b), S;
122
+ },
123
+ reconcile(e) {
124
+ y(e);
125
+ }
126
+ };
127
+ }
128
+ export {
129
+ O as a,
130
+ j as b,
131
+ A as r
132
+ };