@aethervtt/shard-core 0.0.1 → 0.2.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.
package/dist/ecs/world.js CHANGED
@@ -3,12 +3,14 @@ import { Registry } from "../schema/registry.js";
3
3
  import { formatEntity, MAX_ENTITIES, makeEntity } from "./entity.js";
4
4
  import { EventQueue, EventReader } from "./events.js";
5
5
  import { ChildOf, Children } from "./hierarchy.js";
6
+ import { assertHostWrite } from "./host-write.js";
6
7
  import {
7
8
  ObserverTable,
8
9
  onAdd,
9
10
  onRemove,
10
11
  onSet
11
12
  } from "./observers.js";
13
+ import { Owners } from "./owners.js";
12
14
  import { Query, queryKey } from "./query.js";
13
15
  import { Table } from "./table.js";
14
16
  const FREE = 4294967295;
@@ -19,6 +21,14 @@ class World {
19
21
  /** Change-detection clock. The scheduler advances it once per system run. */
20
22
  tick = 1;
21
23
  registry = new Registry();
24
+ /**
25
+ * While true, the next write (`set`, `add`, `remove`, `spawn`, `despawn`, `insertResource`,
26
+ * `removeResource`, `send`, `patchResource`, `touchResource`) clears it and calls `onWake`. The
27
+ * on-demand runner sets it when a frame ends and nothing asks for another (0052).
28
+ */
29
+ asleep = false;
30
+ /** Called by the first write while `asleep`. Set by the on-demand runner. */
31
+ onWake = void 0;
22
32
  generations = new Uint32Array(1024);
23
33
  tableOf = new Uint32Array(1024).fill(FREE);
24
34
  rowOf = new Uint32Array(1024);
@@ -30,18 +40,36 @@ class World {
30
40
  emptyTable;
31
41
  queries = /* @__PURE__ */ new Map();
32
42
  resources = /* @__PURE__ */ new Map();
43
+ /** Change tick per resource id: set by insertResource, patchResource, and touchResource. */
44
+ resourceTicks = [];
33
45
  eventQueues = /* @__PURE__ */ new Map();
34
46
  observers = new ObserverTable();
35
47
  registered = [];
48
+ owners_;
36
49
  constructor() {
37
50
  this.emptyTable = this.getOrCreateTable([]);
38
51
  this.installHierarchy();
39
52
  }
53
+ /**
54
+ * Who made what (0061): owners hold entities and asset leases, and release them together.
55
+ * Only code holding an `Owner` can spawn into it, lease for it, or release it.
56
+ */
57
+ get owners() {
58
+ this.owners_ ??= new Owners(this);
59
+ return this.owners_;
60
+ }
40
61
  incrementTick() {
41
62
  return ++this.tick;
42
63
  }
64
+ /** Wakes the world as a write would: if it's asleep, clears `asleep` and calls `onWake`. */
65
+ wake() {
66
+ if (!this.asleep) return;
67
+ this.asleep = false;
68
+ this.onWake?.();
69
+ }
43
70
  // --- entities --------------------------------------------------------------
44
71
  spawn(...inits) {
72
+ if (this.asleep) this.wake();
45
73
  const entity = this.allocate();
46
74
  this.insert(entity, inits);
47
75
  return entity;
@@ -55,6 +83,7 @@ class World {
55
83
  if (index >= this.nextIndex || this.tableOf[index] !== PENDING || this.generations[index] !== Math.floor(entity / MAX_ENTITIES)) {
56
84
  throw new ShardError("ecs/dead-entity", `Entity ${formatEntity(entity)} is not reserved`);
57
85
  }
86
+ if (this.asleep) this.wake();
58
87
  this.insert(entity, inits);
59
88
  }
60
89
  isAlive(entity) {
@@ -69,6 +98,7 @@ class World {
69
98
  /** Despawns the entity and, if it has children, all of its descendants. */
70
99
  despawn(entity) {
71
100
  this.locate(entity);
101
+ if (this.asleep) this.wake();
72
102
  const children = this.tryGet(entity, Children);
73
103
  if (children) {
74
104
  for (const child of children.entities) {
@@ -80,6 +110,7 @@ class World {
80
110
  /** Despawns only this entity. Its children lose their `ChildOf` and become roots. */
81
111
  despawnSingle(entity) {
82
112
  this.locate(entity);
113
+ if (this.asleep) this.wake();
83
114
  const children = this.tryGet(entity, Children);
84
115
  if (children) {
85
116
  for (const child of children.entities) {
@@ -105,7 +136,9 @@ class World {
105
136
  /** Adds a component (defaults for missing fields). If present, replaces it and fires `onSet`. */
106
137
  add(entity, component, value) {
107
138
  const def = component;
139
+ if (def.hostOnly) assertHostWrite(def);
108
140
  const index = this.locate(entity);
141
+ if (this.asleep) this.wake();
109
142
  this.ensureRegistered(def);
110
143
  const table = this.tables[this.tableOf[index]];
111
144
  const row = this.rowOf[index];
@@ -134,8 +167,10 @@ class World {
134
167
  }
135
168
  /** Removes a component. Returns false if the entity didn't have it. */
136
169
  remove(entity, component) {
170
+ if (component.hostOnly) assertHostWrite(component);
137
171
  let index = this.locate(entity);
138
172
  if (!this.tables[this.tableOf[index]].has(component)) return false;
173
+ if (this.asleep) this.wake();
139
174
  this.fire("remove", entity, component, void 0);
140
175
  index = this.locate(entity);
141
176
  const table = this.tables[this.tableOf[index]];
@@ -162,9 +197,11 @@ class World {
162
197
  /** Writes the given fields and marks the component changed. */
163
198
  set(entity, component, values) {
164
199
  const def = component;
200
+ if (def.hostOnly) assertHostWrite(def);
165
201
  const index = this.locate(entity);
166
202
  const table = this.tables[this.tableOf[index]];
167
203
  if (!table.has(def)) throw missingComponent(entity, def.name);
204
+ if (this.asleep) this.wake();
168
205
  const row = this.rowOf[index];
169
206
  const previous = this.hasObservers("set", def) ? table.readComponent(def, row) : void 0;
170
207
  table.writeComponent(def, row, values);
@@ -207,7 +244,32 @@ class World {
207
244
  // --- resources -------------------------------------------------------------
208
245
  insertResource(def, value) {
209
246
  this.registry.register(def);
247
+ if (this.asleep) this.wake();
210
248
  this.resources.set(def.id, value);
249
+ this.resourceTicks[def.id] = this.tick;
250
+ }
251
+ /**
252
+ * Assigns `partial`'s fields onto the resource, marks it changed, and wakes the world. How code
253
+ * outside a frame writes a resource; a bare field assignment is invisible to both (0052).
254
+ */
255
+ patchResource(def, partial) {
256
+ const value = this.resource(def);
257
+ Object.assign(value, partial);
258
+ this.touchResource(def);
259
+ return value;
260
+ }
261
+ /** Marks the resource changed and wakes the world, after editing it in place. */
262
+ touchResource(def) {
263
+ if (!this.resources.has(def.id)) this.resource(def);
264
+ this.resourceTicks[def.id] = this.tick;
265
+ if (this.asleep) this.wake();
266
+ }
267
+ /**
268
+ * Whether the resource was inserted, patched, or touched after tick `since` (a system passes
269
+ * `ctx.lastRunTick`), like component change detection.
270
+ */
271
+ resourceChanged(def, since) {
272
+ return (this.resourceTicks[def.id] ?? 0) > since;
211
273
  }
212
274
  /** Inserts the resource from its `init` if it isn't present yet, and returns it. */
213
275
  initResource(def) {
@@ -240,10 +302,14 @@ class World {
240
302
  return this.resources.has(def.id);
241
303
  }
242
304
  removeResource(def) {
305
+ if (!this.resources.has(def.id)) return false;
306
+ if (this.asleep) this.wake();
307
+ this.resourceTicks[def.id] = this.tick;
243
308
  return this.resources.delete(def.id);
244
309
  }
245
310
  // --- events ----------------------------------------------------------------
246
311
  send(def, ...data) {
312
+ if (this.asleep) this.wake();
247
313
  this.eventQueue(def).send(data[0]);
248
314
  }
249
315
  reader(def) {
@@ -350,6 +416,7 @@ class World {
350
416
  for (let i = 0; i < inits.length; i++) {
351
417
  const init = inits[i];
352
418
  const def = Array.isArray(init) ? init[0] : init;
419
+ if (def.hostOnly) assertHostWrite(def);
353
420
  this.ensureRegistered(def);
354
421
  table = this.tableWith(table, def);
355
422
  }
@@ -5,6 +5,7 @@ export * as frustum from './frustum.js';
5
5
  export { hash32, hashSeed, LATTICE_PRIMES, mix32 } from './hash.js';
6
6
  export * as mat3 from './mat3.js';
7
7
  export * as mat4 from './mat4.js';
8
+ export * as polygon from './polygon.js';
8
9
  export * as quat from './quat.js';
9
10
  export * as ray from './ray.js';
10
11
  export { Rng } from './rng.js';
@@ -5,6 +5,7 @@ import * as frustum from "./frustum.js";
5
5
  import { hash32, hashSeed, LATTICE_PRIMES, mix32 } from "./hash.js";
6
6
  import * as mat3 from "./mat3.js";
7
7
  import * as mat4 from "./mat4.js";
8
+ import * as polygon from "./polygon.js";
8
9
  import * as quat from "./quat.js";
9
10
  import * as ray from "./ray.js";
10
11
  import { Rng } from "./rng.js";
@@ -25,6 +26,7 @@ export {
25
26
  mat3,
26
27
  mat4,
27
28
  mix32,
29
+ polygon,
28
30
  quat,
29
31
  ray,
30
32
  vec2,
@@ -0,0 +1,17 @@
1
+ /** The signed area of a ring. Positive when counter-clockwise (y up). */
2
+ export declare function signedArea(points: ArrayLike<number>, start?: number, end?: number): number;
3
+ /** The total area of triangles given by `indices` into flat xy `points`. */
4
+ export declare function trianglesArea(points: ArrayLike<number>, indices: ArrayLike<number>): number;
5
+ /**
6
+ * Triangulates a simple polygon, with optional holes, by ear clipping. `points` holds the outer
7
+ * ring first, then each hole; `holes` lists the vertex index where each hole starts. Either
8
+ * winding works for any ring. Returns vertex indices, three per triangle, counter-clockwise.
9
+ * Holes are bridged to the outer ring first, so the result covers the polygon minus its holes.
10
+ */
11
+ export declare function triangulate(points: ArrayLike<number>, holes?: ArrayLike<number>): number[];
12
+ /**
13
+ * Clips a convex polygon (flat xy, `n` points, in `src`) to the rectangle
14
+ * `[minX, maxX] × [minY, maxY]` (Sutherland–Hodgman). Writes the result to `out` and returns its
15
+ * point count (0 when nothing is inside). `out` and `scratch` need room for `n + 4` points.
16
+ */
17
+ export declare function clipToRect(src: ArrayLike<number>, n: number, minX: number, minY: number, maxX: number, maxY: number, out: Float64Array, scratch: Float64Array): number;
@@ -0,0 +1,307 @@
1
+ function signedArea(points, start = 0, end = points.length >> 1) {
2
+ let area = 0;
3
+ for (let i = start, j = end - 1; i < end; j = i++) {
4
+ area += points[j * 2] * points[i * 2 + 1] - points[i * 2] * points[j * 2 + 1];
5
+ }
6
+ return area / 2;
7
+ }
8
+ function trianglesArea(points, indices) {
9
+ let area = 0;
10
+ for (let i = 0; i + 2 < indices.length; i += 3) {
11
+ const a = indices[i] * 2;
12
+ const b = indices[i + 1] * 2;
13
+ const c = indices[i + 2] * 2;
14
+ area += Math.abs(
15
+ (points[b] - points[a]) * (points[c + 1] - points[a + 1]) - (points[c] - points[a]) * (points[b + 1] - points[a + 1])
16
+ );
17
+ }
18
+ return area / 2;
19
+ }
20
+ function triangulate(points, holes = []) {
21
+ const out = [];
22
+ const count = points.length >> 1;
23
+ const outerEnd = holes.length > 0 ? holes[0] : count;
24
+ let outer = ring(points, 0, outerEnd, true);
25
+ if (!outer || outer.next === outer.prev) return out;
26
+ if (holes.length > 0) outer = eliminateHoles(points, holes, outer, count);
27
+ earcut(outer, out, 0);
28
+ return out;
29
+ }
30
+ function ring(points, start, end, ccw) {
31
+ if (end - start < 3) return void 0;
32
+ const forward = signedArea(points, start, end) > 0 === ccw;
33
+ let last;
34
+ if (forward) for (let i = start; i < end; i++) last = insert(i, points, last);
35
+ else for (let i = end - 1; i >= start; i--) last = insert(i, points, last);
36
+ if (last && last.x === last.next.x && last.y === last.next.y) {
37
+ const next = last.next;
38
+ remove(last);
39
+ last = next;
40
+ }
41
+ return last;
42
+ }
43
+ function insert(i, points, last) {
44
+ const node = { i, x: points[i * 2], y: points[i * 2 + 1], gone: false };
45
+ if (!last) {
46
+ node.prev = node;
47
+ node.next = node;
48
+ } else {
49
+ node.next = last.next;
50
+ node.prev = last;
51
+ last.next.prev = node;
52
+ last.next = node;
53
+ }
54
+ return node;
55
+ }
56
+ function remove(node) {
57
+ node.next.prev = node.prev;
58
+ node.prev.next = node.next;
59
+ node.gone = true;
60
+ }
61
+ function area2(a, b, c) {
62
+ return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
63
+ }
64
+ function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
65
+ return (cx - px) * (ay - py) >= (ax - px) * (cy - py) && (ax - px) * (by - py) >= (bx - px) * (ay - py) && (bx - px) * (cy - py) >= (cx - px) * (by - py);
66
+ }
67
+ function isEar(ear) {
68
+ const a = ear.prev;
69
+ const b = ear;
70
+ const c = ear.next;
71
+ if (area2(a, b, c) <= 0) return false;
72
+ for (let p = c.next; p !== a; p = p.next) {
73
+ if (p.x === a.x && p.y === a.y || p.x === b.x && p.y === b.y || p.x === c.x && p.y === c.y)
74
+ continue;
75
+ if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area2(p.prev, p, p.next) <= 0)
76
+ return false;
77
+ }
78
+ return true;
79
+ }
80
+ function earcut(start, out, pass) {
81
+ let ear = start;
82
+ let stop = ear;
83
+ while (ear.prev !== ear.next) {
84
+ const prev = ear.prev;
85
+ const next = ear.next;
86
+ if (isEar(ear)) {
87
+ out.push(prev.i, ear.i, next.i);
88
+ remove(ear);
89
+ ear = next.next;
90
+ stop = next.next;
91
+ continue;
92
+ }
93
+ ear = next;
94
+ if (ear === stop) {
95
+ if (pass === 0) earcut(filterPoints(ear), out, 1);
96
+ else if (pass === 1) earcut(cureLocalIntersections(filterPoints(ear), out), out, 2);
97
+ else if (pass === 2) splitEarcut(ear, out);
98
+ return;
99
+ }
100
+ }
101
+ }
102
+ function filterPoints(start) {
103
+ let p = start;
104
+ let end = start;
105
+ let again;
106
+ do {
107
+ again = false;
108
+ if (p.x === p.next.x && p.y === p.next.y || area2(p.prev, p, p.next) === 0) {
109
+ remove(p);
110
+ p = end = p.prev;
111
+ if (p === p.next) break;
112
+ again = true;
113
+ } else p = p.next;
114
+ } while (again || p !== end);
115
+ return end;
116
+ }
117
+ function intersects(p1, q1, p2, q2) {
118
+ const o1 = Math.sign(area2(p1, q1, p2));
119
+ const o2 = Math.sign(area2(p1, q1, q2));
120
+ const o3 = Math.sign(area2(p2, q2, p1));
121
+ const o4 = Math.sign(area2(p2, q2, q1));
122
+ return o1 !== o2 && o3 !== o4;
123
+ }
124
+ function locallyInside(a, b) {
125
+ return area2(a.prev, a, a.next) < 0 ? area2(a, b, a.next) >= 0 && area2(a, a.prev, b) >= 0 : area2(a, b, a.prev) < 0 || area2(a, a.next, b) < 0;
126
+ }
127
+ function cureLocalIntersections(start, out) {
128
+ let p = start;
129
+ do {
130
+ const a = p.prev;
131
+ const b = p.next.next;
132
+ if (!(a.x === b.x && a.y === b.y) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
133
+ out.push(a.i, p.i, b.i);
134
+ remove(p);
135
+ remove(p.next);
136
+ p = start = b;
137
+ }
138
+ p = p.next;
139
+ } while (p !== start);
140
+ return filterPoints(p);
141
+ }
142
+ function middleInside(a, b) {
143
+ let p = a;
144
+ let inside = false;
145
+ const px = (a.x + b.x) / 2;
146
+ const py = (a.y + b.y) / 2;
147
+ do {
148
+ if (p.y > py !== p.next.y > py && p.next.y !== p.y && px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)
149
+ inside = !inside;
150
+ p = p.next;
151
+ } while (p !== a);
152
+ return inside;
153
+ }
154
+ function intersectsPolygon(a, b) {
155
+ let p = a;
156
+ do {
157
+ if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i && intersects(p, p.next, a, b))
158
+ return true;
159
+ p = p.next;
160
+ } while (p !== a);
161
+ return false;
162
+ }
163
+ function isValidDiagonal(a, b) {
164
+ return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b);
165
+ }
166
+ function splitPolygon(a, b) {
167
+ const a2 = { i: a.i, x: a.x, y: a.y, gone: false };
168
+ const b2 = { i: b.i, x: b.x, y: b.y, gone: false };
169
+ const an = a.next;
170
+ const bp = b.prev;
171
+ a.next = b;
172
+ b.prev = a;
173
+ a2.next = an;
174
+ an.prev = a2;
175
+ b2.next = a2;
176
+ a2.prev = b2;
177
+ bp.next = b2;
178
+ b2.prev = bp;
179
+ return b2;
180
+ }
181
+ function splitEarcut(start, out) {
182
+ let a = start;
183
+ do {
184
+ let b = a.next.next;
185
+ while (b !== a.prev) {
186
+ if (a.i !== b.i && isValidDiagonal(a, b)) {
187
+ let c = splitPolygon(a, b);
188
+ a = filterPoints(a);
189
+ c = filterPoints(c);
190
+ earcut(a, out, 0);
191
+ earcut(c, out, 0);
192
+ return;
193
+ }
194
+ b = b.next;
195
+ }
196
+ a = a.next;
197
+ } while (a !== start);
198
+ }
199
+ function eliminateHoles(points, holes, outer, count) {
200
+ const queue = [];
201
+ for (let h = 0; h < holes.length; h++) {
202
+ const start = holes[h];
203
+ const end = h + 1 < holes.length ? holes[h + 1] : count;
204
+ const list = ring(points, start, end, false);
205
+ if (!list) continue;
206
+ let left = list;
207
+ let p = list;
208
+ do {
209
+ if (p.x < left.x || p.x === left.x && p.y < left.y) left = p;
210
+ p = p.next;
211
+ } while (p !== list);
212
+ queue.push(left);
213
+ }
214
+ queue.sort((a, b) => a.x - b.x || a.y - b.y);
215
+ for (const hole of queue) outer = filterPoints(eliminateHole(hole, outer));
216
+ return outer;
217
+ }
218
+ function eliminateHole(hole, outer) {
219
+ const bridge = findHoleBridge(hole, outer);
220
+ if (!bridge) return outer;
221
+ const reverse = splitPolygon(bridge, hole);
222
+ filterPoints(reverse);
223
+ return filterPoints(bridge);
224
+ }
225
+ function findHoleBridge(hole, outer) {
226
+ let p = outer;
227
+ const hx = hole.x;
228
+ const hy = hole.y;
229
+ let qx = -Infinity;
230
+ let m;
231
+ do {
232
+ if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
233
+ const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
234
+ if (x <= hx && x > qx) {
235
+ qx = x;
236
+ m = p.x < p.next.x ? p : p.next;
237
+ if (x === hx) return m;
238
+ }
239
+ }
240
+ p = p.next;
241
+ } while (p !== outer);
242
+ if (!m) return void 0;
243
+ const stop = m;
244
+ const mx = m.x;
245
+ const my = m.y;
246
+ let tanMin = Infinity;
247
+ p = m;
248
+ do {
249
+ if (hx >= p.x && p.x >= mx && hx !== p.x && pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
250
+ const tan = Math.abs(hy - p.y) / (hx - p.x);
251
+ if (locallyInside(p, hole) && (tan < tanMin || tan === tanMin && (p.x > m.x || p.x === m.x && sectorContainsSector(m, p)))) {
252
+ m = p;
253
+ tanMin = tan;
254
+ }
255
+ }
256
+ p = p.next;
257
+ } while (p !== stop);
258
+ return m;
259
+ }
260
+ function sectorContainsSector(m, p) {
261
+ return area2(m.prev, m, p.prev) < 0 && area2(p.next, m, m.next) < 0;
262
+ }
263
+ function clipToRect(src, n, minX, minY, maxX, maxY, out, scratch) {
264
+ let count = clipEdge(src, n, scratch, 0, minX, 1);
265
+ count = clipEdge(scratch, count, out, 0, maxX, -1);
266
+ count = clipEdge(out, count, scratch, 1, minY, 1);
267
+ count = clipEdge(scratch, count, out, 1, maxY, -1);
268
+ return count;
269
+ }
270
+ function clipEdge(src, n, dst, axis, at, sign) {
271
+ let count = 0;
272
+ if (n === 0) return 0;
273
+ let px = src[(n - 1) * 2];
274
+ let py = src[(n - 1) * 2 + 1];
275
+ let pd = sign * ((axis === 0 ? px : py) - at);
276
+ for (let i = 0; i < n; i++) {
277
+ const x = src[i * 2];
278
+ const y = src[i * 2 + 1];
279
+ const d = sign * ((axis === 0 ? x : y) - at);
280
+ if (d >= 0) {
281
+ if (pd < 0) {
282
+ const t = pd / (pd - d);
283
+ dst[count * 2] = px + (x - px) * t;
284
+ dst[count * 2 + 1] = py + (y - py) * t;
285
+ count++;
286
+ }
287
+ dst[count * 2] = x;
288
+ dst[count * 2 + 1] = y;
289
+ count++;
290
+ } else if (pd >= 0) {
291
+ const t = pd / (pd - d);
292
+ dst[count * 2] = px + (x - px) * t;
293
+ dst[count * 2 + 1] = py + (y - py) * t;
294
+ count++;
295
+ }
296
+ px = x;
297
+ py = y;
298
+ pd = d;
299
+ }
300
+ return count;
301
+ }
302
+ export {
303
+ clipToRect,
304
+ signedArea,
305
+ trianglesArea,
306
+ triangulate
307
+ };