@vworlds/vecs 1.0.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 (58) hide show
  1. package/.claude/settings.json +12 -0
  2. package/.devcontainer/devcontainer.json +22 -0
  3. package/.github/workflows/publish.yml +32 -0
  4. package/README.md +464 -0
  5. package/dist/component.d.ts +135 -0
  6. package/dist/component.js +101 -0
  7. package/dist/component.js.map +1 -0
  8. package/dist/entity.d.ts +157 -0
  9. package/dist/entity.js +199 -0
  10. package/dist/entity.js.map +1 -0
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/package.json +25 -0
  15. package/dist/phase.d.ts +47 -0
  16. package/dist/phase.js +23 -0
  17. package/dist/phase.js.map +1 -0
  18. package/dist/system.d.ts +361 -0
  19. package/dist/system.js +396 -0
  20. package/dist/system.js.map +1 -0
  21. package/dist/util/array_map.d.ts +58 -0
  22. package/dist/util/array_map.js +84 -0
  23. package/dist/util/array_map.js.map +1 -0
  24. package/dist/util/bitset.d.ts +117 -0
  25. package/dist/util/bitset.js +177 -0
  26. package/dist/util/bitset.js.map +1 -0
  27. package/dist/util/events.d.ts +27 -0
  28. package/dist/util/events.js +43 -0
  29. package/dist/util/events.js.map +1 -0
  30. package/dist/util/ordered_set.d.ts +17 -0
  31. package/dist/util/ordered_set.js +69 -0
  32. package/dist/util/ordered_set.js.map +1 -0
  33. package/dist/world.d.ts +279 -0
  34. package/dist/world.js +453 -0
  35. package/dist/world.js.map +1 -0
  36. package/package.json +25 -0
  37. package/src/component.ts +180 -0
  38. package/src/entity.ts +276 -0
  39. package/src/index.ts +6 -0
  40. package/src/phase.ts +49 -0
  41. package/src/system.ts +693 -0
  42. package/src/util/array_map.ts +93 -0
  43. package/src/util/bitset.ts +199 -0
  44. package/src/util/events.ts +95 -0
  45. package/src/util/ordered_set.ts +82 -0
  46. package/src/world.ts +534 -0
  47. package/tests/_helpers.ts +30 -0
  48. package/tests/array_map.test.ts +68 -0
  49. package/tests/bitset.test.ts +127 -0
  50. package/tests/component.test.ts +104 -0
  51. package/tests/entity.test.ts +179 -0
  52. package/tests/events.test.ts +48 -0
  53. package/tests/ordered_set.test.ts +153 -0
  54. package/tests/setup.ts +6 -0
  55. package/tests/system.test.ts +800 -0
  56. package/tests/world.test.ts +174 -0
  57. package/tsconfig.json +21 -0
  58. package/vitest.config.ts +9 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * A `Map<number, T>` backed by a sparse JavaScript array.
3
+ *
4
+ * For small, dense integer key spaces this is significantly faster than
5
+ * `Map` because array index access avoids the hash-table overhead. Used
6
+ * internally to store per-entity component instances and per-type component
7
+ * metadata.
8
+ *
9
+ * Keys must be non-negative integers. Gaps in the key space are represented
10
+ * as `undefined` slots and do not count toward `size`.
11
+ *
12
+ * @typeParam T - The value type stored in the map.
13
+ */
14
+ export class ArrayMap<T> {
15
+ private backend: (T | undefined)[];
16
+ private _size: number;
17
+
18
+ constructor() {
19
+ this.backend = [];
20
+ this._size = 0;
21
+ }
22
+
23
+ /**
24
+ * Store `value` at `key`, replacing any existing value.
25
+ *
26
+ * @param key - Non-negative integer key.
27
+ * @param value - Value to store.
28
+ */
29
+ set(key: number, value: T): void {
30
+ if (this.backend[key] === undefined) {
31
+ this._size++;
32
+ }
33
+ this.backend[key] = value;
34
+ }
35
+
36
+ /**
37
+ * Return the value stored at `key`, or `undefined` if not present.
38
+ *
39
+ * @param key - Non-negative integer key.
40
+ */
41
+ get(key: number): T | undefined {
42
+ return this.backend[key];
43
+ }
44
+
45
+ /**
46
+ * Remove the entry at `key`. Does nothing if `key` is not present.
47
+ *
48
+ * @param key - Non-negative integer key.
49
+ */
50
+ delete(key: number): void {
51
+ if (this.backend[key] !== undefined) {
52
+ this.backend[key] = undefined;
53
+ this._size--;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Return `true` if an entry exists at `key`.
59
+ *
60
+ * @param key - Non-negative integer key.
61
+ */
62
+ has(key: number): boolean {
63
+ return this.backend[key] !== undefined;
64
+ }
65
+
66
+ /**
67
+ * Iterate over all present entries.
68
+ *
69
+ * Undefined slots are skipped; the callback is only called for keys that
70
+ * have an associated value.
71
+ *
72
+ * @param callback - Called with `(value, key, map)` for each entry.
73
+ */
74
+ forEach(callback: (value: T, key: number, map: ArrayMap<T>) => void): void {
75
+ this.backend.forEach((value, index) => {
76
+ if (value !== undefined) {
77
+ callback(value, index, this);
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Remove all entries and reset the size to zero.
84
+ */
85
+ clear() {
86
+ this.backend.length = 0;
87
+ }
88
+
89
+ /** The number of entries currently in the map. */
90
+ get size(): number {
91
+ return this._size;
92
+ }
93
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * A compact, growable set of non-negative integers backed by an array of
3
+ * 32-bit words.
4
+ *
5
+ * Used internally to represent entity archetypes (the set of component type
6
+ * ids attached to an entity) and system watchlists. Exposed in the public API
7
+ * so that component data can use it for bit-flag fields:
8
+ *
9
+ * ```ts
10
+ * class Tags extends Component {
11
+ * tags = new Bitset();
12
+ * oldTags = new Bitset();
13
+ * }
14
+ *
15
+ * // Check a specific tag bit:
16
+ * if (tags.tags.has(TAG_VISIBLE)) { ... }
17
+ * ```
18
+ */
19
+ export class Bitset {
20
+ private bits: number[];
21
+
22
+ constructor() {
23
+ this.bits = [];
24
+ }
25
+
26
+ /**
27
+ * Return `true` if this bitset and `other` have exactly the same bits set.
28
+ */
29
+ equal(other: Bitset) {
30
+ return (
31
+ this.bits.length === other.bits.length &&
32
+ this.bits.every((v, i) => other.bits[i] === v)
33
+ );
34
+ }
35
+
36
+ /**
37
+ * OR the given `bitmask` word into the word at position `arrayIndex`.
38
+ *
39
+ * @internal Low-level bulk operation; prefer {@link add} for single bits.
40
+ */
41
+ addIndexBitmask(arrayIndex: number, bitmask: number) {
42
+ this.bits[arrayIndex] |= bitmask;
43
+ }
44
+
45
+ /**
46
+ * Replace the word at position `arrayIndex` with `bitmask`.
47
+ *
48
+ * @internal Used by network deserialization to set a whole word at once.
49
+ */
50
+ setIndexBitmask(arrayIndex: number, bitmask: number) {
51
+ this.bits[arrayIndex] = bitmask;
52
+ }
53
+
54
+ /**
55
+ * Set the bit described by `bptr` (fast path using a pre-computed
56
+ * {@link BitPtr}).
57
+ */
58
+ addBit(bptr: BitPtr) {
59
+ this.addIndexBitmask(bptr.arrayIndex, bptr.bitmask);
60
+ }
61
+
62
+ /**
63
+ * Set bit `n`.
64
+ *
65
+ * @param n - Non-negative integer bit index.
66
+ */
67
+ add(n: number): void {
68
+ const arrayIndex = Math.floor(n / 32);
69
+ const bitmask = 1 << n % 32;
70
+ this.addIndexBitmask(arrayIndex, bitmask);
71
+ }
72
+
73
+ /**
74
+ * Clear bit `n`.
75
+ *
76
+ * Trailing zero words are trimmed so that the internal array stays compact.
77
+ *
78
+ * @param n - Non-negative integer bit index.
79
+ */
80
+ delete(n: number): void {
81
+ const arrayIndex = Math.floor(n / 32);
82
+ const current = this.bits[arrayIndex];
83
+ if (current === undefined) {
84
+ return;
85
+ } else {
86
+ this.bits[arrayIndex] = current & ~(1 << n % 32);
87
+ }
88
+ while (this.bits.length && this.bits[this.bits.length - 1] === 0)
89
+ this.bits.pop();
90
+ }
91
+
92
+ /**
93
+ * Return `true` if the bit described by `bptr` is set (fast path).
94
+ */
95
+ hasBit(bptr: BitPtr) {
96
+ return this.hasIndexBitmask(bptr.arrayIndex, bptr.bitmask);
97
+ }
98
+
99
+ /**
100
+ * Return `true` if bit `n` is set.
101
+ *
102
+ * @param n - Non-negative integer bit index.
103
+ */
104
+ has(n: number): boolean {
105
+ const arrayIndex = Math.floor(n / 32);
106
+ if (arrayIndex >= this.bits.length) return false;
107
+ const bitIndex = n % 32;
108
+ const bitmask = 1 << bitIndex;
109
+ return this.hasIndexBitmask(arrayIndex, bitmask);
110
+ }
111
+
112
+ /**
113
+ * Return `true` if the given word-level bitmask is fully set at `arrayIndex`.
114
+ *
115
+ * @internal
116
+ */
117
+ hasIndexBitmask(arrayIndex: number, bitmask: number) {
118
+ return (this.bits[arrayIndex] & bitmask) !== 0;
119
+ }
120
+
121
+ /**
122
+ * Return `true` if every bit set in `other` is also set in `this` (i.e.
123
+ * `other` is a subset of `this`).
124
+ *
125
+ * Used by the world to test whether an entity's archetype satisfies a
126
+ * system's `HAS` query.
127
+ */
128
+ hasBitset(other: Bitset): boolean {
129
+ if (this.bits.length < other.bits.length) {
130
+ return false;
131
+ }
132
+
133
+ for (let i = 0; i < other.bits.length; i++) {
134
+ if ((this.bits[i] & other.bits[i]) !== (other.bits[i] || 0)) {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ return true;
140
+ }
141
+
142
+ /**
143
+ * Iterate over every set bit index in ascending order.
144
+ *
145
+ * @param callback - Called with each set bit index.
146
+ */
147
+ forEach(callback: (n: number) => void): void {
148
+ this.bits.forEach((b, j) => {
149
+ for (let i = 0; i < 32; i++) {
150
+ if ((b & 1) !== 0) {
151
+ callback(i + j * 32);
152
+ }
153
+ b >>= 1;
154
+ }
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Return an array of all set bit indices in ascending order.
160
+ *
161
+ * @returns `number[]` of set bit positions.
162
+ */
163
+ indices(): number[] {
164
+ const idx: number[] = [];
165
+ this.forEach((i) => idx.push(i));
166
+ return idx;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * A pre-computed pointer into a {@link Bitset}'s internal word array.
172
+ *
173
+ * Computing `arrayIndex` and `bitmask` from a raw bit index requires a floor
174
+ * division and a bitshift. `BitPtr` caches those values so that hot-path
175
+ * archetype checks ({@link Bitset.hasBit}, {@link Bitset.addBit}) avoid
176
+ * repeating the arithmetic on every entity update.
177
+ *
178
+ * A `BitPtr` is created once per component type and stored on
179
+ * {@link ComponentMeta.bitPtr}.
180
+ */
181
+ export class BitPtr {
182
+ /** Index of the 32-bit word that contains this bit. */
183
+ public readonly arrayIndex: number;
184
+ /** Single-bit mask within that word. */
185
+ public readonly bitmask: number;
186
+
187
+ constructor(
188
+ /** The raw bit index this pointer refers to. */
189
+ public readonly value: number
190
+ ) {
191
+ this.arrayIndex = Math.floor(value / 32);
192
+ this.bitmask = 1 << value % 32;
193
+ }
194
+
195
+ /** Return `true` if both pointers refer to the same bit position. */
196
+ public equals(other: BitPtr) {
197
+ return this.arrayIndex == other.arrayIndex && this.bitmask == other.bitmask;
198
+ }
199
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The following type declarations define an alternative typings interface for eventemitter3
3
+ * Inspired by typings in @yandeu/events https://github.com/yandeu/events
4
+ * Turns out those typings pretty much can be used directly on eventemitter3
5
+ */
6
+
7
+ import eventEmitter3 from "eventemitter3";
8
+ const { EventEmitter } = eventEmitter3;
9
+
10
+ declare type ValidEventMap<T = any> = T extends {
11
+ [P in keyof T]: (...args: any[]) => void;
12
+ }
13
+ ? T
14
+ : never;
15
+ declare type Handler<T extends any | ((...args: any[]) => R), R = any> = T;
16
+ export declare type EventListener<
17
+ T extends ValidEventMap,
18
+ K extends EventNames<T>
19
+ > = T extends string | symbol
20
+ ? (...args: any[]) => void
21
+ : K extends keyof T
22
+ ? Handler<T[K], void>
23
+ : never;
24
+ declare type EventArgs<
25
+ T extends ValidEventMap,
26
+ K extends EventNames<T>
27
+ > = Parameters<EventListener<T, K>>;
28
+ export declare type EventNames<T extends ValidEventMap> = T extends
29
+ | string
30
+ | symbol
31
+ ? T
32
+ : keyof T;
33
+ class events<EventMap extends ValidEventMap = any> {
34
+ public on<T extends EventNames<EventMap>>(
35
+ event: T,
36
+ fn: EventListener<EventMap, T>,
37
+ context?: any
38
+ ): events<EventMap> {
39
+ return 0 as any;
40
+ }
41
+ public emit<T extends EventNames<EventMap>>(
42
+ event: T,
43
+ ...args: EventArgs<EventMap, T>
44
+ ): boolean {
45
+ return 0 as any;
46
+ }
47
+ public once<T extends EventNames<EventMap>>(
48
+ event: T,
49
+ fn: EventListener<EventMap, T>,
50
+ context?: any
51
+ ): events<EventMap> {
52
+ return 0 as any;
53
+ }
54
+ public eventNames(): EventNames<EventMap>[] {
55
+ return 0 as any;
56
+ }
57
+ public listeners(event: EventNames<EventMap>): any[] {
58
+ return 0 as any;
59
+ }
60
+ public listenerCount(event: EventNames<EventMap>): any {
61
+ return 0 as any;
62
+ }
63
+ public removeListener<T extends EventNames<EventMap>>(
64
+ event: T,
65
+ fn?: EventListener<EventMap, T>,
66
+ context?: any,
67
+ once?: boolean
68
+ ): this {
69
+ return 0 as any;
70
+ }
71
+ removeAllListeners(event?: EventNames<EventMap>): this {
72
+ return 0 as any;
73
+ }
74
+ public off<T extends EventNames<EventMap>>(
75
+ event: T,
76
+ fn?: EventListener<EventMap, T> | undefined,
77
+ context?: any,
78
+ once?: boolean | undefined
79
+ ): events<EventMap> {
80
+ return 0 as any;
81
+ }
82
+ public addListener<T extends EventNames<EventMap>>(
83
+ event: T,
84
+ fn: EventListener<EventMap, T>,
85
+ context?: any
86
+ ): events<EventMap> {
87
+ return 0 as any;
88
+ }
89
+ }
90
+
91
+ (events as any) = EventEmitter;
92
+
93
+ export class Events<
94
+ EventMap extends ValidEventMap = any
95
+ > extends events<EventMap> {}
@@ -0,0 +1,82 @@
1
+ export class OrderedSet<T> implements Set<T> {
2
+ private items: T[];
3
+ private readonly compare: (a: T, b: T) => number;
4
+
5
+ constructor(compare: (a: T, b: T) => number) {
6
+ this.items = [];
7
+ this.compare = compare;
8
+ }
9
+
10
+ private bisect(value: T): number {
11
+ let lo = 0;
12
+ let hi = this.items.length;
13
+ while (lo < hi) {
14
+ const mid = (lo + hi) >>> 1;
15
+ if (this.compare(this.items[mid], value) < 0) {
16
+ lo = mid + 1;
17
+ } else {
18
+ hi = mid;
19
+ }
20
+ }
21
+ return lo;
22
+ }
23
+
24
+ add(value: T): this {
25
+ const i = this.bisect(value);
26
+ if (i < this.items.length && this.compare(this.items[i], value) === 0) {
27
+ return this;
28
+ }
29
+ this.items.splice(i, 0, value);
30
+ return this;
31
+ }
32
+
33
+ has(value: T): boolean {
34
+ const i = this.bisect(value);
35
+ return i < this.items.length && this.compare(this.items[i], value) === 0;
36
+ }
37
+
38
+ delete(value: T): boolean {
39
+ const i = this.bisect(value);
40
+ if (i < this.items.length && this.compare(this.items[i], value) === 0) {
41
+ this.items.splice(i, 1);
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ clear(): void {
48
+ this.items.length = 0;
49
+ }
50
+
51
+ get size(): number {
52
+ return this.items.length;
53
+ }
54
+
55
+ forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: unknown): void {
56
+ for (const item of this.items) {
57
+ callbackfn.call(thisArg, item, item, this);
58
+ }
59
+ }
60
+
61
+ [Symbol.iterator](): IterableIterator<T> {
62
+ return this.items[Symbol.iterator]();
63
+ }
64
+
65
+ *entries(): IterableIterator<[T, T]> {
66
+ for (const item of this.items) {
67
+ yield [item, item];
68
+ }
69
+ }
70
+
71
+ keys(): IterableIterator<T> {
72
+ return this.items[Symbol.iterator]();
73
+ }
74
+
75
+ values(): IterableIterator<T> {
76
+ return this.items[Symbol.iterator]();
77
+ }
78
+
79
+ get [Symbol.toStringTag](): string {
80
+ return "OrderedSet";
81
+ }
82
+ }