@skewedaspect/sage-core 0.10.0-beta.1

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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @skewedaspect/sage-core
2
+
3
+ The entity system: `GameEntity`, `Behavior`, the event map and the event bus, `GameSimulationContext`, the simulation manager, snapshot and restore, names and tags, and the action declaration type.
4
+
5
+ It imports nothing from Babylon and nothing from the DOM.
@@ -0,0 +1,13 @@
1
+ export interface DigitalAction {
2
+ type: 'digital';
3
+ name: string;
4
+ label?: string;
5
+ }
6
+ export interface AnalogAction {
7
+ type: 'analog';
8
+ name: string;
9
+ label?: string;
10
+ minValue?: number;
11
+ maxValue?: number;
12
+ }
13
+ export type Action = DigitalAction | AnalogAction;
@@ -0,0 +1,16 @@
1
+ import { EventSubscription, GameEvent } from "./events.d.ts";
2
+ import { GameEntity } from "./gameEntity.d.ts";
3
+ export declare abstract class Behavior<State extends object, Requires extends object = object> {
4
+ readonly state: State;
5
+ readonly ops: Requires;
6
+ readonly entity: GameEntity;
7
+ abstract readonly defaults: State;
8
+ onCreate?(): void;
9
+ onUpdate?(dt: number): void;
10
+ onEvent?(event: GameEvent): boolean | undefined;
11
+ }
12
+ export interface BehaviorClass {
13
+ new (): Behavior<object, object>;
14
+ readonly ops?: readonly PropertyKey[];
15
+ readonly events?: readonly EventSubscription[];
16
+ }
@@ -0,0 +1,3 @@
1
+ export declare const operationCallDepthLimit = 32;
2
+ export declare const drainPassLimit = 64;
3
+ export declare const dilationLimit = 5;
@@ -0,0 +1,7 @@
1
+ import { EventSubscription, GameEvent, Subscriber, Unsubscribe } from "./events.d.ts";
2
+ export declare class EventBus {
3
+ #private;
4
+ register(subscriber: Subscriber, subscriptions: readonly EventSubscription[]): Unsubscribe;
5
+ emit(event: GameEvent): void;
6
+ drain(): void;
7
+ }
@@ -0,0 +1,24 @@
1
+ import { EventMap } from "./index.d.ts";
2
+ export type { EventMap };
3
+ export type GameEvent = {
4
+ [Type in keyof EventMap & string]: {
5
+ type: Type;
6
+ senderID: string;
7
+ targetID?: string;
8
+ payload: EventMap[Type];
9
+ };
10
+ }[keyof EventMap & string];
11
+ type EventPrefix<Type extends string> = Type extends `${infer Prefix}:${infer Rest}` ? Prefix | `${Prefix}:${EventPrefix<Rest>}` : never;
12
+ export type EventSubscription = (keyof EventMap & string) | `${EventPrefix<keyof EventMap & string>}:*`;
13
+ export type Unsubscribe = () => void;
14
+ export interface Subscriber {
15
+ readonly id: string;
16
+ receive(event: GameEvent): boolean;
17
+ }
18
+ export interface RawGameEvent {
19
+ type: string;
20
+ senderID: string;
21
+ targetID?: string;
22
+ payload: unknown;
23
+ }
24
+ export declare function compileEventPattern(subscription: string): RegExp;
@@ -0,0 +1,85 @@
1
+ import { EventMap, EventSubscription, GameEvent, Subscriber } from "./events.d.ts";
2
+ import { Behavior, BehaviorClass } from "./behavior.d.ts";
3
+ import { GameSimulationContext } from "./simulationContext.d.ts";
4
+ export type StateValue = null | boolean | number | string | StateValue[] | {
5
+ [key: string]: StateValue;
6
+ };
7
+ export type StateBag = Record<string, StateValue>;
8
+ export type DeepReadonly<Value> = Value extends (infer Item)[] ? readonly DeepReadonly<Item>[] : Value extends object ? {
9
+ readonly [Key in keyof Value]: DeepReadonly<Value[Key]>;
10
+ } : Value;
11
+ export interface GameEntityState {
12
+ tags: string[];
13
+ }
14
+ export declare function copyStateBag(bag: Readonly<StateBag>): StateBag;
15
+ type UnionToIntersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (value: infer Intersection) => void ? Intersection : never;
16
+ type StateOf<Class> = Class extends new () => {
17
+ state: infer State;
18
+ } ? State : never;
19
+ type OperationNames<Class> = Class extends {
20
+ ops: readonly (infer Name)[];
21
+ } ? Name & PropertyKey : never;
22
+ type OpsFrom<Class> = Class extends BehaviorClass ? Pick<InstanceType<Class>, OperationNames<Class> & keyof InstanceType<Class>> : never;
23
+ type RequiresOf<Class> = Class extends new () => {
24
+ ops: infer Requires;
25
+ } ? Requires : never;
26
+ export type StateFor<Behaviors extends readonly BehaviorClass[]> = GameEntityState & UnionToIntersection<StateOf<Behaviors[number]>> & object;
27
+ export type OpsFor<Behaviors extends readonly BehaviorClass[]> = UnionToIntersection<OpsFrom<Behaviors[number]>> & object;
28
+ export type OpsOf<Class extends BehaviorClass> = OpsFrom<Class>;
29
+ export type StateSliceOf<Class extends BehaviorClass> = StateOf<Class>;
30
+ interface Unsatisfied<Missing> {
31
+ 'ERROR: a behavior requires operations the entity does not provide': Missing;
32
+ }
33
+ interface DuplicateOperation<Name> {
34
+ 'ERROR: two behaviors expose the same operation name': Name;
35
+ }
36
+ interface IncompatibleState<Name> {
37
+ 'ERROR: two behaviors declare one state field with incompatible types': Name;
38
+ }
39
+ type ValidateRequires<Behaviors extends readonly BehaviorClass[]> = {
40
+ [Index in keyof Behaviors]: OpsFor<Behaviors> extends RequiresOf<Behaviors[Index]> ? Behaviors[Index] : Unsatisfied<Exclude<keyof RequiresOf<Behaviors[Index]>, keyof OpsFor<Behaviors>>>;
41
+ };
42
+ type DuplicateNames<Behaviors extends readonly unknown[], Seen extends PropertyKey = never> = Behaviors extends readonly [infer Head, ...infer Tail] ? [Extract<OperationNames<Head>, Seen>] extends [never] ? DuplicateNames<Tail, Seen | OperationNames<Head>> : Extract<OperationNames<Head>, Seen> | DuplicateNames<Tail, Seen | OperationNames<Head>> : never;
43
+ type IncompatibleKeys<First, Second> = {
44
+ [Key in keyof First & keyof Second]: [First[Key]] extends [Second[Key]] ? [Second[Key]] extends [First[Key]] ? never : Key : Key;
45
+ }[keyof First & keyof Second];
46
+ type IncompatibleStateKeys<Behaviors extends readonly unknown[], SeenState extends object = object> = Behaviors extends readonly [infer Head, ...infer Tail] ? IncompatibleKeys<SeenState, StateOf<Head>> | IncompatibleStateKeys<Tail, SeenState & StateOf<Head>> : never;
47
+ export type ValidTuple<Behaviors extends readonly BehaviorClass[]> = [DuplicateNames<Behaviors>] extends [never] ? [IncompatibleStateKeys<Behaviors>] extends [never] ? Behaviors extends ValidateRequires<Behaviors> ? Behaviors : ValidateRequires<Behaviors> : readonly IncompatibleState<IncompatibleStateKeys<Behaviors>>[] : readonly DuplicateOperation<DuplicateNames<Behaviors>>[];
48
+ export interface GameEntityType {
49
+ readonly behaviors: readonly BehaviorClass[];
50
+ readonly prototype: GameEntity;
51
+ }
52
+ export type ComposedOf<Type extends GameEntityType> = Type['prototype'] & {
53
+ readonly state: StateFor<Type['behaviors']>;
54
+ readonly ops: OpsFor<Type['behaviors']>;
55
+ };
56
+ export type NarrowedTo<Class extends BehaviorClass> = GameEntity & {
57
+ readonly state: DeepReadonly<StateSliceOf<Class>>;
58
+ readonly ops: OpsOf<Class>;
59
+ };
60
+ type BehaviorInstance = Behavior<object, object>;
61
+ export declare abstract class GameEntity implements Subscriber {
62
+ #private;
63
+ readonly id: string;
64
+ readonly name: string;
65
+ protected readonly context: GameSimulationContext;
66
+ readonly state: object;
67
+ readonly ops: object;
68
+ readonly eventSubscriptions: readonly EventSubscription[];
69
+ static readonly behaviors: readonly BehaviorClass[];
70
+ static create<Type extends GameEntityType>(this: Type & {
71
+ behaviors: ValidTuple<Type['behaviors']>;
72
+ }, id: string, context: GameSimulationContext, state?: Partial<StateFor<Type['behaviors']>>, name?: string): ComposedOf<Type>;
73
+ static restore<Type extends GameEntityType>(this: Type & {
74
+ behaviors: ValidTuple<Type['behaviors']>;
75
+ }, id: string, context: GameSimulationContext, state: StateFor<Type['behaviors']>, name?: string): ComposedOf<Type>;
76
+ protected constructor(id: string, context: GameSimulationContext, providedState: object, runCreateHooks: boolean, name?: string);
77
+ hasBehavior<Class extends BehaviorClass>(behaviorClass: Class): this is NarrowedTo<Class>;
78
+ getEntity(id: string): GameEntity | undefined;
79
+ emit<Type extends keyof EventMap & string>(type: Type, payload: EventMap[Type], targetID?: string): void;
80
+ update(dt: number): void;
81
+ receive(event: GameEvent): boolean;
82
+ destroy(): void;
83
+ protected attachedBehaviors(): readonly BehaviorInstance[];
84
+ }
85
+ export {};
@@ -0,0 +1,15 @@
1
+ export interface EventMap {
2
+ }
3
+ export type { Action, AnalogAction, DigitalAction } from "./action.d.ts";
4
+ export { Behavior } from "./behavior.d.ts";
5
+ export type { BehaviorClass } from "./behavior.d.ts";
6
+ export { NamedBehavior } from "./namedBehavior.d.ts";
7
+ export type { NamedState } from "./namedBehavior.d.ts";
8
+ export { dilationLimit, drainPassLimit, operationCallDepthLimit } from "./constants.d.ts";
9
+ export type { EventSubscription, GameEvent, Subscriber, Unsubscribe } from "./events.d.ts";
10
+ export { EventBus } from "./eventBus.d.ts";
11
+ export { GameEntity } from "./gameEntity.d.ts";
12
+ export type { ComposedOf, DeepReadonly, GameEntityState, GameEntityType, NarrowedTo, OpsFor, OpsOf, StateBag, StateFor, StateSliceOf, StateValue, ValidTuple, } from "./gameEntity.d.ts";
13
+ export { GameSimulationManager } from "./simulation.d.ts";
14
+ export type { GameEntitySnapshot, IDAllocator, GameSimulationOptions, GameSimulationSnapshot, } from "./simulation.d.ts";
15
+ export type { GameSimulationContext } from "./simulationContext.d.ts";
@@ -0,0 +1,7 @@
1
+ import { Behavior } from "./behavior.d.ts";
2
+ export interface NamedState {
3
+ displayName: string;
4
+ }
5
+ export declare class NamedBehavior extends Behavior<NamedState> {
6
+ readonly defaults: NamedState;
7
+ }
@@ -0,0 +1,378 @@
1
+ //#region src/behavior.ts
2
+ var e = class {}, t = class extends e {
3
+ defaults = { displayName: "" };
4
+ }, n = 32, r = 64, i = 5;
5
+ //#endregion
6
+ //#region src/events.ts
7
+ function a(e) {
8
+ let t = e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
9
+ return RegExp(`^${t}$`);
10
+ }
11
+ //#endregion
12
+ //#region src/eventBus.ts
13
+ var o = class e {
14
+ #e = [];
15
+ #t = [];
16
+ #n = /* @__PURE__ */ new Map();
17
+ #r = /* @__PURE__ */ new Map();
18
+ register(e, t) {
19
+ if (this.#n.has(e.id)) throw Error(`A subscriber is already registered under the ID "${e.id}".`);
20
+ this.#n.set(e.id, e);
21
+ let n = [], r = [];
22
+ for (let i of t) {
23
+ let t = i;
24
+ if (t.endsWith(":*")) {
25
+ let n = {
26
+ pattern: a(t),
27
+ subscriber: e
28
+ };
29
+ r.push(n), this.#t.push(n);
30
+ } else n.includes(t) || (n.push(t), this.#i(t, e));
31
+ }
32
+ return this.#o(e, n, r);
33
+ }
34
+ emit(e) {
35
+ this.#e.push(e);
36
+ }
37
+ drain() {
38
+ let e = [], t = 0;
39
+ for (; this.#e.length > 0;) {
40
+ if (t += 1, t > 64) {
41
+ this.#e = [], e.push(/* @__PURE__ */ Error("The drain exceeded 64 passes."));
42
+ break;
43
+ }
44
+ let n = this.#e;
45
+ this.#e = [];
46
+ for (let t of n) this.#s(t, e);
47
+ }
48
+ if (e.length > 0) throw AggregateError(e, "The drain raised.");
49
+ }
50
+ #i(e, t) {
51
+ let n = this.#r.get(e) ?? [];
52
+ n.push(t), this.#r.set(e, n);
53
+ }
54
+ #a(e, t) {
55
+ let n = this.#r.get(e);
56
+ if (n === void 0) return;
57
+ let r = n.indexOf(t);
58
+ r >= 0 && n.splice(r, 1), n.length === 0 && this.#r.delete(e);
59
+ }
60
+ #o(e, t, n) {
61
+ let r = !0;
62
+ return () => {
63
+ if (!r) return;
64
+ r = !1, this.#n.get(e.id) === e && this.#n.delete(e.id);
65
+ for (let n of t) this.#a(n, e);
66
+ let i = [];
67
+ for (let e of this.#t) n.includes(e) || i.push(e);
68
+ this.#t = i;
69
+ };
70
+ }
71
+ #s(t, n) {
72
+ if (t.targetID !== void 0) {
73
+ let r = this.#n.get(t.targetID);
74
+ r !== void 0 && e.#l(r, t, n);
75
+ return;
76
+ }
77
+ let r = /* @__PURE__ */ new Set();
78
+ for (let i of [...this.#r.get(t.type) ?? []]) e.#c(i, t, r, n);
79
+ for (let i of [...this.#t]) i.pattern.test(t.type) && e.#c(i.subscriber, t, r, n);
80
+ }
81
+ static #c(t, n, r, i) {
82
+ r.has(t) || (r.add(t), e.#l(t, n, i));
83
+ }
84
+ static #l(e, t, n) {
85
+ try {
86
+ e.receive(t);
87
+ } catch (e) {
88
+ n.push(e);
89
+ }
90
+ }
91
+ };
92
+ //#endregion
93
+ //#region src/gameEntity.ts
94
+ function s(e) {
95
+ if (Array.isArray(e)) {
96
+ let t = [];
97
+ for (let n of e) t.push(s(n));
98
+ return t;
99
+ }
100
+ if (typeof e == "object" && e) {
101
+ let t = Object.create(null);
102
+ for (let [n, r] of Object.entries(e)) t[n] = s(r);
103
+ return t;
104
+ }
105
+ return e;
106
+ }
107
+ function c(e) {
108
+ return s(e);
109
+ }
110
+ var l = class e {
111
+ id;
112
+ name;
113
+ context;
114
+ state;
115
+ ops;
116
+ eventSubscriptions;
117
+ static behaviors = [];
118
+ #e = [];
119
+ #t = [];
120
+ #n;
121
+ #r = 0;
122
+ #i;
123
+ static create(t, n, r, i) {
124
+ return e.#a(this, t, n, r ?? {}, !0, i);
125
+ }
126
+ static restore(t, n, r, i) {
127
+ return e.#a(this, t, n, r, !1, i);
128
+ }
129
+ static #a(e, t, n, r, i, a) {
130
+ return new e(t, n, r, i, a);
131
+ }
132
+ constructor(t, n, r, i, a) {
133
+ this.id = t, this.name = a ?? new.target.name, this.context = n;
134
+ let o = new.target.behaviors;
135
+ for (let e of o) this.#e.push(new e());
136
+ this.state = e.#u(this.#e, r, t);
137
+ let s = Object.create(null);
138
+ this.ops = s;
139
+ let c = [];
140
+ for (let [e, t] of o.entries()) {
141
+ let n = this.#e[e];
142
+ n !== void 0 && (Object.assign(n, {
143
+ state: this.state,
144
+ ops: s,
145
+ entity: this
146
+ }), this.#o(n, t, s), this.#s(n, t, c));
147
+ }
148
+ this.eventSubscriptions = c, this.#n = n.bus.register(this, c), i && this.#l();
149
+ }
150
+ hasBehavior(e) {
151
+ for (let t of this.#e) if (t instanceof e) return !0;
152
+ return !1;
153
+ }
154
+ getEntity(e) {
155
+ return this.context.getEntity(e);
156
+ }
157
+ emit(e, t, n) {
158
+ let r = {
159
+ type: e,
160
+ senderID: this.id,
161
+ targetID: n,
162
+ payload: t
163
+ };
164
+ if (this.#i !== void 0) {
165
+ this.#i.push(r);
166
+ return;
167
+ }
168
+ this.context.bus.emit(r);
169
+ }
170
+ update(e) {
171
+ for (let t of this.#e) t.onUpdate?.(e);
172
+ }
173
+ receive(t) {
174
+ let n = t;
175
+ for (let r of this.#t) if (e.#g(r, n.type) && r.instance.onEvent?.(t) === !0) return !0;
176
+ return !1;
177
+ }
178
+ destroy() {
179
+ this.#n();
180
+ }
181
+ attachedBehaviors() {
182
+ return [...this.#e];
183
+ }
184
+ #o(e, t, n) {
185
+ for (let r of t.ops ?? []) {
186
+ if (Object.hasOwn(n, r)) throw Error(`Two behaviors on entity "${this.id}" expose the operation "${String(r)}".`);
187
+ let i = e[r];
188
+ if (typeof i != "function") throw Error(`Behavior "${t.name}" declares the operation "${String(r)}" with no method behind it.`);
189
+ n[r] = this.#c(r, i, e);
190
+ }
191
+ }
192
+ #s(e, t, n) {
193
+ let r = [], i = [];
194
+ for (let e of t.events ?? []) {
195
+ let t = e;
196
+ t.endsWith(":*") ? i.push(a(t)) : r.push(t), n.includes(e) || n.push(e);
197
+ }
198
+ (r.length > 0 || i.length > 0) && this.#t.push({
199
+ instance: e,
200
+ exact: r,
201
+ patterns: i
202
+ });
203
+ }
204
+ #c(e, t, n) {
205
+ return (...r) => {
206
+ this.#r += 1;
207
+ try {
208
+ if (this.#r > 32) throw RangeError(`Operation "${String(e)}" on entity "${this.id}" ran deeper than the operation call depth limit of 32.`);
209
+ return t.apply(n, r);
210
+ } finally {
211
+ --this.#r;
212
+ }
213
+ };
214
+ }
215
+ #l() {
216
+ this.#i = [];
217
+ try {
218
+ for (let e of this.#e) e.onCreate?.();
219
+ } catch (e) {
220
+ throw this.#i = void 0, this.#n(), e;
221
+ }
222
+ let e = this.#i;
223
+ this.#i = void 0;
224
+ for (let t of e) this.context.bus.emit(t);
225
+ }
226
+ static #u(t, n, r) {
227
+ let i = Object.create(null);
228
+ i.tags = [];
229
+ for (let n of t) Object.assign(i, e.#d(n.defaults, r));
230
+ return Object.assign(i, e.#d(n, r)), e.#f(i, r), i;
231
+ }
232
+ static #d(t, n) {
233
+ if (!e.#h(t, /* @__PURE__ */ new Set())) throw TypeError(`Entity "${n}" state holds a value that is not finite JSON-compatible data.`);
234
+ return c(t);
235
+ }
236
+ static #f(e, t) {
237
+ let n = e.tags;
238
+ if (!Array.isArray(n)) throw TypeError(`Entity "${t}" tags must be a list of strings.`);
239
+ let r = [];
240
+ for (let e of n) {
241
+ if (typeof e != "string") throw TypeError(`Entity "${t}" tags must be a list of strings.`);
242
+ if (r.includes(e)) throw Error(`Entity "${t}" carries the tag "${e}" more than once.`);
243
+ r.push(e);
244
+ }
245
+ }
246
+ static #p(t, n) {
247
+ return t === null || typeof t == "string" || typeof t == "boolean" ? !0 : typeof t == "number" ? Number.isFinite(t) : Array.isArray(t) ? e.#m(t, n) : typeof t == "object" && e.#h(t, n);
248
+ }
249
+ static #m(t, n) {
250
+ if (n.has(t)) return !1;
251
+ n.add(t);
252
+ for (let r of t) if (!e.#p(r, n)) return n.delete(t), !1;
253
+ return n.delete(t), !0;
254
+ }
255
+ static #h(t, n) {
256
+ let r = Object.getPrototypeOf(t);
257
+ if (r !== Object.prototype && r !== null || n.has(t)) return !1;
258
+ n.add(t);
259
+ for (let r of Object.values(t)) if (!e.#p(r, n)) return n.delete(t), !1;
260
+ return n.delete(t), !0;
261
+ }
262
+ static #g(e, t) {
263
+ if (e.exact.includes(t)) return !0;
264
+ for (let n of e.patterns) if (n.test(t)) return !0;
265
+ return !1;
266
+ }
267
+ }, u = class e {
268
+ #e = new o();
269
+ #t;
270
+ #n;
271
+ #r;
272
+ #i = 0;
273
+ #a = 0;
274
+ #o = 0;
275
+ #s = [];
276
+ constructor(e) {
277
+ if (this.#t = e.fixedStepDuration, this.#n = e.entityClasses, this.#r = e.allocator, !Number.isFinite(this.#t) || this.#t <= 0) throw RangeError("A fixed step duration must be a finite number greater than zero.");
278
+ }
279
+ get bus() {
280
+ return this.#e;
281
+ }
282
+ get tick() {
283
+ return this.#i;
284
+ }
285
+ get entities() {
286
+ return [...this.#s];
287
+ }
288
+ spawn(e, t, n) {
289
+ let r = this.#f(e), i = t;
290
+ if (i === void 0 && (i = this.#d()), this.getEntity(i) !== void 0) throw Error(`The simulation already holds an entity under the ID "${i}".`);
291
+ let a = r.create(i, this, n ?? {}, e);
292
+ return this.#s.push(a), a;
293
+ }
294
+ despawn(e) {
295
+ let t = this.#s.findIndex((t) => t.id === e);
296
+ if (t < 0) return;
297
+ let n = this.#s[t];
298
+ n !== void 0 && (n.destroy(), this.#s.splice(t, 1));
299
+ }
300
+ getEntity(e) {
301
+ return this.#s.find((t) => t.id === e);
302
+ }
303
+ getEntitiesByTag(t) {
304
+ let n = [];
305
+ for (let r of this.#s) e.#p(r).tags.includes(t) && n.push(r);
306
+ return n;
307
+ }
308
+ runStepsForElapsedTime(e) {
309
+ if (!Number.isFinite(e) || e < 0) throw RangeError("Elapsed time must be a finite number of zero or more.");
310
+ this.#o += e;
311
+ let t = 0;
312
+ for (; this.#l() && t < 5;) this.#u(), t += 1;
313
+ t === 5 && this.#l() && (this.#o %= this.#t);
314
+ }
315
+ snapshot() {
316
+ let e = [];
317
+ for (let t of this.#s) e.push({
318
+ id: t.id,
319
+ name: t.name,
320
+ state: c(t.state)
321
+ });
322
+ return {
323
+ tick: this.#i,
324
+ idCounter: this.#a,
325
+ entities: e
326
+ };
327
+ }
328
+ fromSnapshot(e) {
329
+ let t = this.buildReplacementSimulation({
330
+ fixedStepDuration: this.#t,
331
+ entityClasses: this.#n,
332
+ allocator: this.#r
333
+ });
334
+ return t.#c(e), t;
335
+ }
336
+ buildReplacementSimulation(t) {
337
+ return new e(t);
338
+ }
339
+ #c(e) {
340
+ let t = [];
341
+ try {
342
+ for (let n of e.entities) {
343
+ let e = this.#f(n.name).restore(n.id, this, n.state, n.name);
344
+ t.push(e);
345
+ }
346
+ } catch (e) {
347
+ for (let e of t) e.destroy();
348
+ throw e;
349
+ }
350
+ this.#s = t, this.#i = e.tick, this.#a = e.idCounter;
351
+ }
352
+ #l() {
353
+ return this.#o + 2 ** -52 >= this.#t;
354
+ }
355
+ #u() {
356
+ for (let e of [...this.#s]) e.update(this.#t);
357
+ try {
358
+ this.#e.drain();
359
+ } finally {
360
+ this.#o -= this.#t, Math.abs(this.#o) <= 2 ** -52 && (this.#o = 0), this.#i += 1;
361
+ }
362
+ }
363
+ #d() {
364
+ return this.#r === void 0 ? (this.#a += 1, `entity-${this.#a}`) : this.#r();
365
+ }
366
+ #f(e) {
367
+ let t = this.#n[e];
368
+ if (t === void 0) throw Error(`The entity class table holds no class named "${e}".`);
369
+ return t;
370
+ }
371
+ static #p(e) {
372
+ return e.state;
373
+ }
374
+ };
375
+ //#endregion
376
+ export { e as Behavior, o as EventBus, l as GameEntity, u as GameSimulationManager, t as NamedBehavior, i as dilationLimit, r as drainPassLimit, n as operationCallDepthLimit };
377
+
378
+ //# sourceMappingURL=sage-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sage-core.js","names":[],"sources":["../src/behavior.ts","../src/namedBehavior.ts","../src/constants.ts","../src/events.ts","../src/eventBus.ts","../src/gameEntity.ts","../src/simulation.ts"],"sourcesContent":["//----------------------------------------------------------------------------------------------------------------------\n// Behavior\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { EventSubscription, GameEvent } from './events.ts';\nimport type { GameEntity } from './gameEntity.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport abstract class Behavior<State extends object, Requires extends object = object>\n{\n declare readonly state : State;\n declare readonly ops : Requires;\n declare readonly entity : GameEntity;\n\n abstract readonly defaults : State;\n\n onCreate?() : void;\n onUpdate?(dt : number) : void;\n onEvent?(event : GameEvent) : boolean | undefined;\n}\n\nexport interface BehaviorClass\n{\n new () : Behavior<object, object>;\n readonly ops ?: readonly PropertyKey[];\n readonly events ?: readonly EventSubscription[];\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// NamedBehavior\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { Behavior } from './behavior.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport interface NamedState\n{\n displayName : string;\n}\n\nexport class NamedBehavior extends Behavior<NamedState>\n{\n readonly defaults : NamedState = { displayName: '' };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Tuning Constants\n//----------------------------------------------------------------------------------------------------------------------\n\nexport const operationCallDepthLimit = 32;\n\nexport const drainPassLimit = 64;\n\nexport const dilationLimit = 5;\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Events\n//\n// `EventMap` is declared in the package root because a `declare module` augmentation only merges into the module that\n// declares the interface, and the root is the only module a consumer can address.\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { EventMap } from './index.ts';\n\nexport type { EventMap };\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport type GameEvent = {\n [Type in keyof EventMap & string] : {\n type : Type;\n senderID : string;\n targetID ?: string;\n payload : EventMap[Type];\n }\n}[keyof EventMap & string];\n\ntype EventPrefix<Type extends string> = Type extends `${ infer Prefix }:${ infer Rest }`\n ? Prefix | `${ Prefix }:${ EventPrefix<Rest> }`\n : never;\n\nexport type EventSubscription = (keyof EventMap & string) | `${ EventPrefix<keyof EventMap & string> }:*`;\n\nexport type Unsubscribe = () => void;\n\nexport interface Subscriber\n{\n readonly id : string;\n receive(event : GameEvent) : boolean;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n// The empty map boundary\n//----------------------------------------------------------------------------------------------------------------------\n\n// Until a game augments `EventMap`, it holds nothing, `GameEvent` is `never` everywhere inside this package, and no\n// event value can be built against it. `RawGameEvent` is the shape an event has whatever the map holds, and it is the\n// only place core steps outside the typed vocabulary: it stays internal, and every public signature keeps `GameEvent`.\nexport interface RawGameEvent\n{\n type : string;\n senderID : string;\n targetID ?: string;\n payload : unknown;\n}\n\n// A pattern spans every remaining segment, so `door:*` matches `door:opened` and `door:lock:opened` alike.\nexport function compileEventPattern(subscription : string) : RegExp\n{\n const escaped = subscription\n .replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\\\\\*/g, '.*');\n\n return new RegExp(`^${ escaped }$`);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Event Bus\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { drainPassLimit } from './constants.ts';\nimport {\n type EventSubscription, type GameEvent, type RawGameEvent, type Subscriber, type Unsubscribe,\n compileEventPattern,\n} from './events.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface PatternSubscription\n{\n pattern : RegExp;\n subscriber : Subscriber;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class EventBus\n{\n #queue : RawGameEvent[] = [];\n #patterns : PatternSubscription[] = [];\n\n readonly #subscribers = new Map<string, Subscriber>();\n readonly #exact = new Map<string, Subscriber[]>();\n\n register(subscriber : Subscriber, subscriptions : readonly EventSubscription[]) : Unsubscribe\n {\n if(this.#subscribers.has(subscriber.id))\n {\n throw new Error(`A subscriber is already registered under the ID \"${ subscriber.id }\".`);\n }\n\n this.#subscribers.set(subscriber.id, subscriber);\n\n const exactNames : string[] = [];\n const patternEntries : PatternSubscription[] = [];\n\n for(const subscription of subscriptions)\n {\n const name : string = subscription;\n\n if(name.endsWith(':*'))\n {\n const entry = { pattern: compileEventPattern(name), subscriber };\n patternEntries.push(entry);\n this.#patterns.push(entry);\n }\n else if(!exactNames.includes(name))\n {\n exactNames.push(name);\n this.#addExact(name, subscriber);\n }\n }\n\n return this.#teardown(subscriber, exactNames, patternEntries);\n }\n\n emit(event : GameEvent) : void\n {\n this.#queue.push(event as RawGameEvent);\n }\n\n drain() : void\n {\n const errors : unknown[] = [];\n let passes = 0;\n\n while(this.#queue.length > 0)\n {\n passes += 1;\n\n if(passes > drainPassLimit)\n {\n this.#queue = [];\n errors.push(new Error(`The drain exceeded ${ drainPassLimit } passes.`));\n break;\n }\n\n const pass = this.#queue;\n this.#queue = [];\n\n for(const event of pass)\n {\n this.#deliver(event, errors);\n }\n }\n\n if(errors.length > 0)\n {\n throw new AggregateError(errors, 'The drain raised.');\n }\n }\n\n #addExact(name : string, subscriber : Subscriber) : void\n {\n const subscribers = this.#exact.get(name) ?? [];\n subscribers.push(subscriber);\n this.#exact.set(name, subscribers);\n }\n\n #removeExact(name : string, subscriber : Subscriber) : void\n {\n const subscribers = this.#exact.get(name);\n\n if(subscribers === undefined)\n {\n return;\n }\n\n const index = subscribers.indexOf(subscriber);\n\n if(index >= 0)\n {\n subscribers.splice(index, 1);\n }\n\n if(subscribers.length === 0)\n {\n this.#exact.delete(name);\n }\n }\n\n // A teardown is idempotent, and it evicts the subscriber under its ID only while that is still the registration it\n // was handed out for; a later registration that took the same ID survives a stale teardown.\n #teardown(\n subscriber : Subscriber,\n exactNames : readonly string[],\n patternEntries : readonly PatternSubscription[]\n ) : Unsubscribe\n {\n let active = true;\n\n return () : void =>\n {\n if(!active)\n {\n return;\n }\n\n active = false;\n\n if(this.#subscribers.get(subscriber.id) === subscriber)\n {\n this.#subscribers.delete(subscriber.id);\n }\n\n for(const name of exactNames)\n {\n this.#removeExact(name, subscriber);\n }\n\n const remaining : PatternSubscription[] = [];\n\n for(const entry of this.#patterns)\n {\n if(!patternEntries.includes(entry))\n {\n remaining.push(entry);\n }\n }\n\n this.#patterns = remaining;\n };\n }\n\n #deliver(event : RawGameEvent, errors : unknown[]) : void\n {\n if(event.targetID !== undefined)\n {\n const addressed = this.#subscribers.get(event.targetID);\n\n if(addressed !== undefined)\n {\n EventBus.#handOff(addressed, event, errors);\n }\n\n return;\n }\n\n const delivered = new Set<Subscriber>();\n\n for(const subscriber of [ ...this.#exact.get(event.type) ?? [] ])\n {\n EventBus.#handOffOnce(subscriber, event, delivered, errors);\n }\n\n for(const entry of [ ...this.#patterns ])\n {\n if(entry.pattern.test(event.type))\n {\n EventBus.#handOffOnce(entry.subscriber, event, delivered, errors);\n }\n }\n }\n\n static #handOffOnce(\n subscriber : Subscriber,\n event : RawGameEvent,\n delivered : Set<Subscriber>,\n errors : unknown[]\n ) : void\n {\n if(delivered.has(subscriber))\n {\n return;\n }\n\n delivered.add(subscriber);\n EventBus.#handOff(subscriber, event, errors);\n }\n\n static #handOff(subscriber : Subscriber, event : RawGameEvent, errors : unknown[]) : void\n {\n try\n {\n subscriber.receive(event as GameEvent);\n }\n catch(error)\n {\n errors.push(error);\n }\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// GameEntity\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { operationCallDepthLimit } from './constants.ts';\nimport {\n type EventMap, type EventSubscription, type GameEvent, type RawGameEvent, type Subscriber, type Unsubscribe,\n compileEventPattern,\n} from './events.ts';\n\nimport type { Behavior, BehaviorClass } from './behavior.ts';\nimport type { GameSimulationContext } from './simulationContext.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n// State\n//----------------------------------------------------------------------------------------------------------------------\n\nexport type StateValue = null | boolean | number | string | StateValue[] | { [key : string] : StateValue };\n\nexport type StateBag = Record<string, StateValue>;\n\nexport type DeepReadonly<Value> = Value extends (infer Item)[]\n ? readonly DeepReadonly<Item>[]\n : Value extends object\n ? { readonly [Key in keyof Value] : DeepReadonly<Value[Key]> }\n : Value;\n\nexport interface GameEntityState\n{\n tags : string[];\n}\n\n// Records are copied with no prototype, so a state field named `__proto__` stays an ordinary field of the copy.\nfunction copyStateValue(value : StateValue) : StateValue\n{\n if(Array.isArray(value))\n {\n const items : StateValue[] = [];\n\n for(const item of value)\n {\n items.push(copyStateValue(item));\n }\n\n return items;\n }\n\n if(value !== null && typeof value === 'object')\n {\n const copy = Object.create(null) as StateBag;\n\n for(const [ key, field ] of Object.entries(value))\n {\n copy[key] = copyStateValue(field);\n }\n\n return copy;\n }\n\n return value;\n}\n\nexport function copyStateBag(bag : Readonly<StateBag>) : StateBag\n{\n return copyStateValue(bag) as StateBag;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n// Composition\n//----------------------------------------------------------------------------------------------------------------------\n\ntype UnionToIntersection<Union>\n = (Union extends unknown ? (value : Union) => void : never) extends (value : infer Intersection) => void\n ? Intersection\n : never;\n\ntype StateOf<Class> = Class extends new () => { state : infer State } ? State : never;\n\ntype OperationNames<Class> = Class extends { ops : readonly (infer Name)[] } ? Name & PropertyKey : never;\n\ntype OpsFrom<Class> = Class extends BehaviorClass\n ? Pick<InstanceType<Class>, OperationNames<Class> & keyof InstanceType<Class>>\n : never;\n\ntype RequiresOf<Class> = Class extends new () => { ops : infer Requires } ? Requires : never;\n\nexport type StateFor<Behaviors extends readonly BehaviorClass[]>\n = GameEntityState & UnionToIntersection<StateOf<Behaviors[number]>> & object;\n\nexport type OpsFor<Behaviors extends readonly BehaviorClass[]>\n = UnionToIntersection<OpsFrom<Behaviors[number]>> & object;\n\nexport type OpsOf<Class extends BehaviorClass> = OpsFrom<Class>;\n\nexport type StateSliceOf<Class extends BehaviorClass> = StateOf<Class>;\n\ninterface Unsatisfied<Missing> { 'ERROR: a behavior requires operations the entity does not provide' : Missing }\ninterface DuplicateOperation<Name> { 'ERROR: two behaviors expose the same operation name' : Name }\ninterface IncompatibleState<Name> { 'ERROR: two behaviors declare one state field with incompatible types' : Name }\n\ntype ValidateRequires<Behaviors extends readonly BehaviorClass[]> = {\n [Index in keyof Behaviors] : OpsFor<Behaviors> extends RequiresOf<Behaviors[Index]>\n ? Behaviors[Index]\n : Unsatisfied<Exclude<keyof RequiresOf<Behaviors[Index]>, keyof OpsFor<Behaviors>>>\n};\n\ntype DuplicateNames<Behaviors extends readonly unknown[], Seen extends PropertyKey = never>\n = Behaviors extends readonly [ infer Head, ...infer Tail ]\n ? [Extract<OperationNames<Head>, Seen>] extends [never]\n ? DuplicateNames<Tail, Seen | OperationNames<Head>>\n : Extract<OperationNames<Head>, Seen> | DuplicateNames<Tail, Seen | OperationNames<Head>>\n : never;\n\ntype IncompatibleKeys<First, Second> = {\n [Key in keyof First & keyof Second] : [First[Key]] extends [Second[Key]]\n ? [Second[Key]] extends [First[Key]]\n ? never\n : Key\n : Key;\n}[keyof First & keyof Second];\n\ntype IncompatibleStateKeys<\n Behaviors extends readonly unknown[],\n SeenState extends object = object,\n> = Behaviors extends readonly [ infer Head, ...infer Tail ]\n ? IncompatibleKeys<SeenState, StateOf<Head>> | IncompatibleStateKeys<Tail, SeenState & StateOf<Head>>\n : never;\n\nexport type ValidTuple<Behaviors extends readonly BehaviorClass[]>\n = [DuplicateNames<Behaviors>] extends [never]\n ? [IncompatibleStateKeys<Behaviors>] extends [never]\n ? Behaviors extends ValidateRequires<Behaviors>\n ? Behaviors\n : ValidateRequires<Behaviors>\n : readonly IncompatibleState<IncompatibleStateKeys<Behaviors>>[]\n : readonly DuplicateOperation<DuplicateNames<Behaviors>>[];\n\nexport interface GameEntityType\n{\n readonly behaviors : readonly BehaviorClass[];\n readonly prototype : GameEntity;\n}\n\nexport type ComposedOf<Type extends GameEntityType> = Type['prototype'] & {\n readonly state : StateFor<Type['behaviors']>;\n readonly ops : OpsFor<Type['behaviors']>;\n};\n\nexport type NarrowedTo<Class extends BehaviorClass> = GameEntity & {\n readonly state : DeepReadonly<StateSliceOf<Class>>;\n readonly ops : OpsOf<Class>;\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype BehaviorInstance = Behavior<object, object>;\n\ntype OperationTable = Record<PropertyKey, unknown>;\n\ntype EntityConstructor = new (\n id : string,\n context : GameSimulationContext,\n providedState : object,\n runCreateHooks : boolean,\n name ?: string\n) => GameEntity;\n\ninterface BehaviorListener\n{\n instance : BehaviorInstance;\n exact : string[];\n patterns : RegExp[];\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n// The entity\n//----------------------------------------------------------------------------------------------------------------------\n\nexport abstract class GameEntity implements Subscriber\n{\n readonly id : string;\n readonly name : string;\n protected readonly context : GameSimulationContext;\n readonly state : object;\n readonly ops : object;\n readonly eventSubscriptions : readonly EventSubscription[];\n\n static readonly behaviors : readonly BehaviorClass[] = [];\n\n readonly #instances : BehaviorInstance[] = [];\n readonly #listeners : BehaviorListener[] = [];\n readonly #unsubscribe : Unsubscribe;\n\n #operationDepth = 0;\n #creationEvents : RawGameEvent[] | undefined;\n\n static create<Type extends GameEntityType>(\n this : Type & { behaviors : ValidTuple<Type['behaviors']> },\n id : string,\n context : GameSimulationContext,\n state ?: Partial<StateFor<Type['behaviors']>>,\n name ?: string\n ) : ComposedOf<Type>\n {\n return GameEntity.#construct(this, id, context, state ?? {}, true, name);\n }\n\n static restore<Type extends GameEntityType>(\n this : Type & { behaviors : ValidTuple<Type['behaviors']> },\n id : string,\n context : GameSimulationContext,\n state : StateFor<Type['behaviors']>,\n name ?: string\n ) : ComposedOf<Type>\n {\n return GameEntity.#construct(this, id, context, state, false, name);\n }\n\n static #construct<Type extends GameEntityType>(\n type : Type,\n id : string,\n context : GameSimulationContext,\n state : object,\n runCreateHooks : boolean,\n name ?: string\n ) : ComposedOf<Type>\n {\n const constructable = type as unknown as EntityConstructor;\n\n return new constructable(id, context, state, runCreateHooks, name) as ComposedOf<Type>;\n }\n\n protected constructor(\n id : string,\n context : GameSimulationContext,\n providedState : object,\n runCreateHooks : boolean,\n name ?: string\n )\n {\n this.id = id;\n this.name = name ?? (new.target as typeof GameEntity).name;\n this.context = context;\n\n const behaviorClasses = (new.target as typeof GameEntity).behaviors;\n\n for(const behaviorClass of behaviorClasses)\n {\n this.#instances.push(new behaviorClass());\n }\n\n this.state = GameEntity.#composeState(this.#instances, providedState, id);\n\n const ops = Object.create(null) as OperationTable;\n this.ops = ops;\n\n const subscriptions : EventSubscription[] = [];\n\n for(const [ index, behaviorClass ] of behaviorClasses.entries())\n {\n const instance = this.#instances[index];\n\n if(instance !== undefined)\n {\n Object.assign(instance, { state: this.state, ops, entity: this });\n this.#bindOperations(instance, behaviorClass, ops);\n this.#bindEvents(instance, behaviorClass, subscriptions);\n }\n }\n\n this.eventSubscriptions = subscriptions;\n this.#unsubscribe = context.bus.register(this, subscriptions);\n\n if(runCreateHooks)\n {\n this.#runCreateHooks();\n }\n }\n\n hasBehavior<Class extends BehaviorClass>(behaviorClass : Class) : this is NarrowedTo<Class>\n {\n for(const instance of this.#instances)\n {\n if(instance instanceof behaviorClass)\n {\n return true;\n }\n }\n\n return false;\n }\n\n getEntity(id : string) : GameEntity | undefined\n {\n return this.context.getEntity(id);\n }\n\n emit<Type extends keyof EventMap & string>(type : Type, payload : EventMap[Type], targetID ?: string) : void\n {\n const event : RawGameEvent = { type, senderID: this.id, targetID, payload };\n\n if(this.#creationEvents !== undefined)\n {\n this.#creationEvents.push(event);\n return;\n }\n\n this.context.bus.emit(event as GameEvent);\n }\n\n update(dt : number) : void\n {\n for(const instance of this.#instances)\n {\n instance.onUpdate?.(dt);\n }\n }\n\n receive(event : GameEvent) : boolean\n {\n const raw = event as RawGameEvent;\n\n for(const listener of this.#listeners)\n {\n if(GameEntity.#matches(listener, raw.type) && listener.instance.onEvent?.(event) === true)\n {\n return true;\n }\n }\n\n return false;\n }\n\n destroy() : void\n {\n this.#unsubscribe();\n }\n\n // A copy, so an override reads the attachment order without reaching the list the entity dispatches from.\n protected attachedBehaviors() : readonly BehaviorInstance[]\n {\n return [ ...this.#instances ];\n }\n\n #bindOperations(instance : BehaviorInstance, behaviorClass : BehaviorClass, ops : OperationTable) : void\n {\n for(const name of behaviorClass.ops ?? [])\n {\n if(Object.hasOwn(ops, name))\n {\n throw new Error(`Two behaviors on entity \"${ this.id }\" expose the operation \"${ String(name) }\".`);\n }\n\n const method = (instance as unknown as OperationTable)[name];\n\n if(typeof method !== 'function')\n {\n throw new Error(\n `Behavior \"${ behaviorClass.name }\" declares the operation \"${ String(name) }\" `\n + 'with no method behind it.'\n );\n }\n\n ops[name] = this.#guardedOperation(name, method as (...args : unknown[]) => unknown, instance);\n }\n }\n\n #bindEvents(\n instance : BehaviorInstance,\n behaviorClass : BehaviorClass,\n subscriptions : EventSubscription[]\n ) : void\n {\n const exact : string[] = [];\n const patterns : RegExp[] = [];\n\n for(const subscription of behaviorClass.events ?? [])\n {\n const name : string = subscription;\n\n if(name.endsWith(':*'))\n {\n patterns.push(compileEventPattern(name));\n }\n else\n {\n exact.push(name);\n }\n\n if(!subscriptions.includes(subscription))\n {\n subscriptions.push(subscription);\n }\n }\n\n if(exact.length > 0 || patterns.length > 0)\n {\n this.#listeners.push({ instance, exact, patterns });\n }\n }\n\n #guardedOperation(\n name : PropertyKey,\n method : (...args : unknown[]) => unknown,\n instance : BehaviorInstance\n ) : (...args : unknown[]) => unknown\n {\n return (...args : unknown[]) : unknown =>\n {\n this.#operationDepth += 1;\n\n try\n {\n if(this.#operationDepth > operationCallDepthLimit)\n {\n throw new RangeError(\n `Operation \"${ String(name) }\" on entity \"${ this.id }\" ran deeper than the operation `\n + `call depth limit of ${ operationCallDepthLimit }.`\n );\n }\n\n return method.apply(instance, args);\n }\n finally\n {\n this.#operationDepth -= 1;\n }\n };\n }\n\n // The hooks emit into a held list rather than onto the queue, so a hook that throws leaves the bus as it found it.\n #runCreateHooks() : void\n {\n this.#creationEvents = [];\n\n try\n {\n for(const instance of this.#instances)\n {\n instance.onCreate?.();\n }\n }\n catch(error)\n {\n this.#creationEvents = undefined;\n this.#unsubscribe();\n throw error;\n }\n\n const held = this.#creationEvents;\n this.#creationEvents = undefined;\n\n for(const event of held)\n {\n this.context.bus.emit(event as GameEvent);\n }\n }\n\n static #composeState(instances : readonly BehaviorInstance[], providedState : object, id : string) : StateBag\n {\n const state = Object.create(null) as StateBag;\n state['tags'] = [];\n\n for(const instance of instances)\n {\n Object.assign(state, GameEntity.#copyValidState(instance.defaults, id));\n }\n\n Object.assign(state, GameEntity.#copyValidState(providedState, id));\n GameEntity.#checkTags(state, id);\n\n return state;\n }\n\n static #copyValidState(value : object, id : string) : StateBag\n {\n if(!GameEntity.#isStateBag(value, new Set<object>()))\n {\n throw new TypeError(`Entity \"${ id }\" state holds a value that is not finite JSON-compatible data.`);\n }\n\n return copyStateBag(value);\n }\n\n static #checkTags(state : StateBag, id : string) : void\n {\n const tags = state['tags'];\n\n if(!Array.isArray(tags))\n {\n throw new TypeError(`Entity \"${ id }\" tags must be a list of strings.`);\n }\n\n const seen : string[] = [];\n\n for(const tag of tags)\n {\n if(typeof tag !== 'string')\n {\n throw new TypeError(`Entity \"${ id }\" tags must be a list of strings.`);\n }\n\n if(seen.includes(tag))\n {\n throw new Error(`Entity \"${ id }\" carries the tag \"${ tag }\" more than once.`);\n }\n\n seen.push(tag);\n }\n }\n\n static #isStateValue(value : unknown, seen : Set<object>) : value is StateValue\n {\n if(value === null || typeof value === 'string' || typeof value === 'boolean')\n {\n return true;\n }\n\n if(typeof value === 'number')\n {\n return Number.isFinite(value);\n }\n\n if(Array.isArray(value))\n {\n return GameEntity.#everyItemIsState(value, seen);\n }\n\n if(typeof value === 'object')\n {\n return GameEntity.#isStateBag(value, seen);\n }\n\n return false;\n }\n\n static #everyItemIsState(items : readonly unknown[], seen : Set<object>) : boolean\n {\n if(seen.has(items))\n {\n return false;\n }\n\n seen.add(items);\n\n for(const item of items)\n {\n if(!GameEntity.#isStateValue(item, seen))\n {\n seen.delete(items);\n return false;\n }\n }\n\n seen.delete(items);\n return true;\n }\n\n static #isStateBag(value : object, seen : Set<object>) : value is StateBag\n {\n const prototype = Object.getPrototypeOf(value) as unknown;\n\n if(prototype !== Object.prototype && prototype !== null)\n {\n return false;\n }\n\n if(seen.has(value))\n {\n return false;\n }\n\n seen.add(value);\n\n for(const item of Object.values(value))\n {\n if(!GameEntity.#isStateValue(item, seen))\n {\n seen.delete(value);\n return false;\n }\n }\n\n seen.delete(value);\n return true;\n }\n\n static #matches(listener : BehaviorListener, type : string) : boolean\n {\n if(listener.exact.includes(type))\n {\n return true;\n }\n\n for(const pattern of listener.patterns)\n {\n if(pattern.test(type))\n {\n return true;\n }\n }\n\n return false;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// Simulation Manager\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { dilationLimit } from './constants.ts';\nimport { EventBus } from './eventBus.ts';\nimport {\n type GameEntity, type GameEntityState, type GameEntityType, type StateBag, copyStateBag,\n} from './gameEntity.ts';\n\nimport type { GameSimulationContext } from './simulationContext.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport interface GameEntitySnapshot\n{\n id : string;\n name : string;\n state : StateBag;\n}\n\nexport interface GameSimulationSnapshot\n{\n tick : number;\n idCounter : number;\n entities : GameEntitySnapshot[];\n}\n\nexport type IDAllocator = () => string;\n\nexport interface GameSimulationOptions\n{\n fixedStepDuration : number;\n entityClasses : Readonly<Record<string, GameEntityType>>;\n allocator ?: IDAllocator;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface ConstructableEntityClass extends GameEntityType\n{\n create(id : string, context : GameSimulationContext, state ?: StateBag, name ?: string) : GameEntity;\n restore(id : string, context : GameSimulationContext, state : StateBag, name ?: string) : GameEntity;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport class GameSimulationManager implements GameSimulationContext\n{\n readonly #bus = new EventBus();\n readonly #fixedStepDuration : number;\n readonly #entityClasses : Readonly<Record<string, GameEntityType>>;\n readonly #allocator : IDAllocator | undefined;\n\n #tick = 0;\n #idCounter = 0;\n #accumulator = 0;\n #entries : GameEntity[] = [];\n\n constructor(options : GameSimulationOptions)\n {\n this.#fixedStepDuration = options.fixedStepDuration;\n this.#entityClasses = options.entityClasses;\n this.#allocator = options.allocator;\n\n if(!Number.isFinite(this.#fixedStepDuration) || this.#fixedStepDuration <= 0)\n {\n throw new RangeError('A fixed step duration must be a finite number greater than zero.');\n }\n }\n\n get bus() : EventBus\n {\n return this.#bus;\n }\n\n get tick() : number\n {\n return this.#tick;\n }\n\n get entities() : readonly GameEntity[]\n {\n return [ ...this.#entries ];\n }\n\n spawn(name : string, id ?: string, state ?: StateBag) : GameEntity\n {\n const entityClass = this.#entityClass(name);\n\n let entityID = id;\n\n if(entityID === undefined)\n {\n entityID = this.#mintID();\n }\n\n if(this.getEntity(entityID) !== undefined)\n {\n throw new Error(`The simulation already holds an entity under the ID \"${ entityID }\".`);\n }\n\n const entity = entityClass.create(entityID, this, state ?? {}, name);\n this.#entries.push(entity);\n\n return entity;\n }\n\n despawn(id : string) : void\n {\n const index = this.#entries.findIndex((entity) => entity.id === id);\n\n if(index < 0)\n {\n return;\n }\n\n const entity = this.#entries[index];\n\n if(entity !== undefined)\n {\n entity.destroy();\n this.#entries.splice(index, 1);\n }\n }\n\n getEntity(id : string) : GameEntity | undefined\n {\n return this.#entries.find((entity) => entity.id === id);\n }\n\n getEntitiesByTag(tag : string) : readonly GameEntity[]\n {\n const found : GameEntity[] = [];\n\n for(const entity of this.#entries)\n {\n if(GameSimulationManager.#entityState(entity).tags.includes(tag))\n {\n found.push(entity);\n }\n }\n\n return found;\n }\n\n runStepsForElapsedTime(elapsed : number) : void\n {\n if(!Number.isFinite(elapsed) || elapsed < 0)\n {\n throw new RangeError('Elapsed time must be a finite number of zero or more.');\n }\n\n this.#accumulator += elapsed;\n\n let steps = 0;\n\n while(this.#owesAFixedStep() && steps < dilationLimit)\n {\n this.#runFixedStep();\n steps += 1;\n }\n\n // Chronic overrun degrades into simulation time running slower than wall clock: the whole intervals still owed\n // are dropped and only the fraction is carried.\n if(steps === dilationLimit && this.#owesAFixedStep())\n {\n this.#accumulator %= this.#fixedStepDuration;\n }\n }\n\n snapshot() : GameSimulationSnapshot\n {\n const entities : GameEntitySnapshot[] = [];\n\n for(const entity of this.#entries)\n {\n entities.push({\n id: entity.id,\n name: entity.name,\n state: copyStateBag(entity.state as StateBag),\n });\n }\n\n return { tick: this.#tick, idCounter: this.#idCounter, entities };\n }\n\n fromSnapshot(snapshot : GameSimulationSnapshot) : GameSimulationManager\n {\n const restored = this.buildReplacementSimulation({\n fixedStepDuration: this.#fixedStepDuration,\n entityClasses: this.#entityClasses,\n allocator: this.#allocator,\n });\n\n restored.#load(snapshot);\n\n return restored;\n }\n\n protected buildReplacementSimulation(options : GameSimulationOptions) : GameSimulationManager\n {\n return new GameSimulationManager(options);\n }\n\n #load(snapshot : GameSimulationSnapshot) : void\n {\n const entries : GameEntity[] = [];\n\n try\n {\n for(const row of snapshot.entities)\n {\n const entityClass = this.#entityClass(row.name);\n const entity = entityClass.restore(row.id, this, row.state, row.name);\n entries.push(entity);\n }\n }\n catch(error)\n {\n for(const entity of entries)\n {\n entity.destroy();\n }\n\n throw error;\n }\n\n this.#entries = entries;\n this.#tick = snapshot.tick;\n this.#idCounter = snapshot.idCounter;\n }\n\n // The accumulator holds a sum of binary floating point values, which lands a few units in the last place short of\n // the interval the caller meant; without the epsilon three calls of a third of a step run no step at all.\n #owesAFixedStep() : boolean\n {\n return this.#accumulator + Number.EPSILON >= this.#fixedStepDuration;\n }\n\n #runFixedStep() : void\n {\n for(const entity of [ ...this.#entries ])\n {\n entity.update(this.#fixedStepDuration);\n }\n\n // The step ran, so the tick and the accumulator settle whether or not the drain raised.\n try\n {\n this.#bus.drain();\n }\n finally\n {\n this.#accumulator -= this.#fixedStepDuration;\n\n if(Math.abs(this.#accumulator) <= Number.EPSILON)\n {\n this.#accumulator = 0;\n }\n\n this.#tick += 1;\n }\n }\n\n #mintID() : string\n {\n if(this.#allocator !== undefined)\n {\n return this.#allocator();\n }\n\n this.#idCounter += 1;\n\n return `entity-${ this.#idCounter }`;\n }\n\n #entityClass(name : string) : ConstructableEntityClass\n {\n const entityClass = this.#entityClasses[name];\n\n if(entityClass === undefined)\n {\n throw new Error(`The entity class table holds no class named \"${ name }\".`);\n }\n\n return entityClass as ConstructableEntityClass;\n }\n\n // Every entity carries the tags field whatever its behaviors declare, so the query reads it straight.\n static #entityState(entity : GameEntity) : GameEntityState\n {\n return entity.state as GameEntityState;\n }\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { EventBus } from "./eventBus.d.ts";
2
+ import { GameEntity, GameEntityType, StateBag } from "./gameEntity.d.ts";
3
+ import { GameSimulationContext } from "./simulationContext.d.ts";
4
+ export interface GameEntitySnapshot {
5
+ id: string;
6
+ name: string;
7
+ state: StateBag;
8
+ }
9
+ export interface GameSimulationSnapshot {
10
+ tick: number;
11
+ idCounter: number;
12
+ entities: GameEntitySnapshot[];
13
+ }
14
+ export type IDAllocator = () => string;
15
+ export interface GameSimulationOptions {
16
+ fixedStepDuration: number;
17
+ entityClasses: Readonly<Record<string, GameEntityType>>;
18
+ allocator?: IDAllocator;
19
+ }
20
+ export declare class GameSimulationManager implements GameSimulationContext {
21
+ #private;
22
+ constructor(options: GameSimulationOptions);
23
+ get bus(): EventBus;
24
+ get tick(): number;
25
+ get entities(): readonly GameEntity[];
26
+ spawn(name: string, id?: string, state?: StateBag): GameEntity;
27
+ despawn(id: string): void;
28
+ getEntity(id: string): GameEntity | undefined;
29
+ getEntitiesByTag(tag: string): readonly GameEntity[];
30
+ runStepsForElapsedTime(elapsed: number): void;
31
+ snapshot(): GameSimulationSnapshot;
32
+ fromSnapshot(snapshot: GameSimulationSnapshot): GameSimulationManager;
33
+ protected buildReplacementSimulation(options: GameSimulationOptions): GameSimulationManager;
34
+ }
@@ -0,0 +1,6 @@
1
+ import { EventBus } from "./eventBus.d.ts";
2
+ import { GameEntity } from "./gameEntity.d.ts";
3
+ export interface GameSimulationContext {
4
+ readonly bus: EventBus;
5
+ getEntity(id: string): GameEntity | undefined;
6
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@skewedaspect/sage-core",
3
+ "version": "0.10.0-beta.1",
4
+ "type": "module",
5
+ "main": "dist/sage-core.js",
6
+ "module": "dist/sage-core.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/sage-core.js",
12
+ "default": "./dist/sage-core.js"
13
+ }
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://gitlab.com/skewed-aspect/sage/sage",
18
+ "directory": "packages/sage-core"
19
+ },
20
+ "author": "Christopher S. Case<chris.case@g33xnexus.com>",
21
+ "license": "MIT",
22
+ "description": "The entity system, event bus, and simulation for the SAGE game engine.",
23
+ "scripts": {
24
+ "build": "vite build",
25
+ "test": "vitest run --config vitest.config.ts",
26
+ "lint:types": "tsc --noEmit && tsc -p tsconfig.tests.json --noEmit"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "devDependencies": {
32
+ "typescript": "^6.0.2",
33
+ "vite": "^8.0.3",
34
+ "vite-plugin-dts": "^4.5.4"
35
+ }
36
+ }