murow 0.1.1 → 0.1.3

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/core/clock/clock.js +1 -0
  3. package/dist/cjs/core/clock/index.js +1 -0
  4. package/dist/cjs/core/hitbox/hitbox-library.js +1 -0
  5. package/dist/cjs/core/hitbox/hitbox.js +1 -0
  6. package/dist/cjs/core/hitbox/index.js +1 -0
  7. package/dist/cjs/core/hitbox/test.js +1 -0
  8. package/dist/cjs/core/index.js +1 -1
  9. package/dist/cjs/core/prediction/prediction.js +1 -1
  10. package/dist/cjs/core/ray/ray-3d.js +1 -1
  11. package/dist/cjs/core/raycast/hit-buffer.js +1 -0
  12. package/dist/cjs/core/raycast/index.js +1 -0
  13. package/dist/cjs/core/raycast/raycaster.js +1 -0
  14. package/dist/cjs/core/slot-map/index.js +1 -0
  15. package/dist/cjs/core/slot-map/slot-map.js +1 -0
  16. package/dist/cjs/core/state-machine/index.js +1 -0
  17. package/dist/cjs/core/state-machine/state-machine.js +1 -0
  18. package/dist/cjs/core/timeline/index.js +1 -0
  19. package/dist/cjs/core/timeline/timeline.js +1 -0
  20. package/dist/cjs/game/loop/loop.js +1 -1
  21. package/dist/cjs/game/loop/ticker-schedule.js +1 -0
  22. package/dist/cjs/renderer/index.js +1 -1
  23. package/dist/cjs/renderer/prefab-bucket/concrete.js +1 -1
  24. package/dist/cjs/renderer/prefab-bucket/index.js +1 -1
  25. package/dist/cjs/renderer/prefab-bucket/parsers.js +1 -1
  26. package/dist/cjs/renderer/prefab-bucket/specs.js +1 -1
  27. package/dist/cjs/renderer/raycast/index.js +1 -0
  28. package/dist/cjs/renderer/raycast/raycast.js +1 -0
  29. package/dist/esm/core/clock/clock.js +1 -0
  30. package/dist/esm/core/clock/index.js +1 -0
  31. package/dist/esm/core/hitbox/hitbox-library.js +1 -0
  32. package/dist/esm/core/hitbox/hitbox.js +1 -0
  33. package/dist/esm/core/hitbox/index.js +1 -0
  34. package/dist/esm/core/hitbox/test.js +1 -0
  35. package/dist/esm/core/index.js +1 -1
  36. package/dist/esm/core/prediction/prediction.js +1 -1
  37. package/dist/esm/core/ray/ray-3d.js +1 -1
  38. package/dist/esm/core/raycast/hit-buffer.js +1 -0
  39. package/dist/esm/core/raycast/index.js +1 -0
  40. package/dist/esm/core/raycast/raycaster.js +1 -0
  41. package/dist/esm/core/slot-map/index.js +1 -0
  42. package/dist/esm/core/slot-map/slot-map.js +1 -0
  43. package/dist/esm/core/state-machine/index.js +1 -0
  44. package/dist/esm/core/state-machine/state-machine.js +1 -0
  45. package/dist/esm/core/timeline/index.js +1 -0
  46. package/dist/esm/core/timeline/timeline.js +1 -0
  47. package/dist/esm/game/loop/loop.js +1 -1
  48. package/dist/esm/game/loop/ticker-schedule.js +1 -0
  49. package/dist/esm/renderer/index.js +1 -1
  50. package/dist/esm/renderer/prefab-bucket/concrete.js +1 -1
  51. package/dist/esm/renderer/prefab-bucket/index.js +1 -1
  52. package/dist/esm/renderer/prefab-bucket/parsers.js +1 -1
  53. package/dist/esm/renderer/raycast/index.js +1 -0
  54. package/dist/esm/renderer/raycast/raycast.js +1 -0
  55. package/dist/netcode/cjs/index.js +144 -140
  56. package/dist/netcode/esm/index.js +144 -140
  57. package/dist/netcode/types/client/game-client.d.ts +17 -3
  58. package/dist/netcode/types/client/strategies/snapshot-interpolation.d.ts +33 -0
  59. package/dist/netcode/types/codec/delta-codec.d.ts +1 -1
  60. package/dist/netcode/types/components/sync-spec.d.ts +6 -0
  61. package/dist/types/core/clock/clock.d.ts +37 -0
  62. package/dist/types/core/clock/index.d.ts +1 -0
  63. package/dist/types/core/hitbox/hitbox-library.d.ts +29 -0
  64. package/dist/types/core/hitbox/hitbox.d.ts +50 -0
  65. package/dist/types/core/hitbox/index.d.ts +3 -0
  66. package/dist/types/core/hitbox/test.d.ts +44 -0
  67. package/dist/types/core/index.d.ts +6 -0
  68. package/dist/types/core/prediction/prediction.d.ts +35 -58
  69. package/dist/types/core/ray/ray-3d.d.ts +21 -1
  70. package/dist/types/core/raycast/hit-buffer.d.ts +43 -0
  71. package/dist/types/core/raycast/index.d.ts +2 -0
  72. package/dist/types/core/raycast/raycaster.d.ts +54 -0
  73. package/dist/types/core/slot-map/index.d.ts +1 -0
  74. package/dist/types/core/slot-map/slot-map.d.ts +109 -0
  75. package/dist/types/core/state-machine/index.d.ts +1 -0
  76. package/dist/types/core/state-machine/state-machine.d.ts +114 -0
  77. package/dist/types/core/timeline/index.d.ts +1 -0
  78. package/dist/types/core/timeline/timeline.d.ts +34 -0
  79. package/dist/types/game/loop/loop.d.ts +30 -0
  80. package/dist/types/game/loop/ticker-schedule.d.ts +52 -0
  81. package/dist/types/renderer/index.d.ts +1 -0
  82. package/dist/types/renderer/prefab-bucket/concrete.d.ts +16 -6
  83. package/dist/types/renderer/prefab-bucket/index.d.ts +11 -7
  84. package/dist/types/renderer/prefab-bucket/specs.d.ts +10 -0
  85. package/dist/types/renderer/raycast/index.d.ts +1 -0
  86. package/dist/types/renderer/raycast/raycast.d.ts +24 -0
  87. package/dist/types/renderer/types.d.ts +1 -0
  88. package/dist/webgpu/cjs/index.js +1777 -587
  89. package/dist/webgpu/esm/index.js +1769 -573
  90. package/dist/webgpu/types/2d/raycast.d.ts +45 -0
  91. package/dist/webgpu/types/2d/renderer.d.ts +11 -0
  92. package/dist/webgpu/types/2d/sprite-accessor.d.ts +3 -1
  93. package/dist/webgpu/types/3d/hitbox.d.ts +32 -0
  94. package/dist/webgpu/types/3d/lights.d.ts +113 -0
  95. package/dist/webgpu/types/3d/lights.test.d.ts +1 -0
  96. package/dist/webgpu/types/3d/raycast.d.ts +44 -0
  97. package/dist/webgpu/types/3d/renderer.d.ts +50 -1
  98. package/dist/webgpu/types/3d/shader.d.ts +88 -5
  99. package/dist/webgpu/types/core/types.d.ts +55 -0
  100. package/dist/webgpu/types/geometry/geometry-builder.d.ts +1 -4
  101. package/dist/webgpu/types/index.d.ts +1 -0
  102. package/dist/webgpu/types/shaders/utils.d.ts +24 -0
  103. package/package.json +1 -1
  104. package/dist/netcode/types/client/interpolation-buffer.d.ts +0 -37
  105. /package/dist/netcode/types/client/{interpolation-buffer.test.d.ts → strategies/snapshot-interpolation.test.d.ts} +0 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Hitbox — a named set of collision shapes in model-local space.
3
+ *
4
+ * Declared once and shared across every prefab/instance that uses it. Pure
5
+ * data: no GPU, no per-instance state. The renderer picks against it; game
6
+ * logic and a headless server run authoritative hit tests against the same
7
+ * object. Shapes are scaled by instance scale at test time.
8
+ *
9
+ * The mode parameter (`'2d'` | `'3d'`) gates which shapes `add` accepts and
10
+ * accumulates the part-name union so consumers can narrow `hit.part`.
11
+ */
12
+ export type Shape3D = {
13
+ readonly shape: 'sphere';
14
+ readonly radius: number;
15
+ readonly offset?: readonly [number, number, number];
16
+ } | {
17
+ readonly shape: 'box';
18
+ readonly size: readonly [number, number, number];
19
+ readonly offset?: readonly [number, number, number];
20
+ } | {
21
+ readonly shape: 'cylinder';
22
+ readonly radius: number;
23
+ readonly height: number;
24
+ readonly offset?: readonly [number, number, number];
25
+ };
26
+ export type Shape2D = {
27
+ readonly shape: 'circle';
28
+ readonly radius: number;
29
+ readonly offset?: readonly [number, number];
30
+ } | {
31
+ readonly shape: 'rect';
32
+ readonly size: readonly [number, number];
33
+ readonly offset?: readonly [number, number];
34
+ } | {
35
+ readonly shape: 'capsule';
36
+ readonly radius: number;
37
+ readonly length: number;
38
+ readonly offset?: readonly [number, number];
39
+ };
40
+ export type ShapeForMode<M extends '2d' | '3d'> = M extends '3d' ? Shape3D : Shape2D;
41
+ export type HitboxPart<M extends '2d' | '3d'> = {
42
+ readonly name: string;
43
+ } & ShapeForMode<M>;
44
+ export declare class Hitbox<M extends '2d' | '3d' = '3d', Names extends string = never> {
45
+ readonly mode: M;
46
+ readonly parts: readonly HitboxPart<M>[];
47
+ constructor(mode: M, parts?: readonly HitboxPart<M>[]);
48
+ /** Add a named shape. Returns a new Hitbox whose type carries the added name. */
49
+ add<const N extends string>(name: N, shape: ShapeForMode<M>): Hitbox<M, Names | N>;
50
+ }
@@ -0,0 +1,3 @@
1
+ export * from './hitbox';
2
+ export * from './hitbox-library';
3
+ export * from './test';
@@ -0,0 +1,44 @@
1
+ import type { Ray3D } from '../ray/ray-3d';
2
+ import type { Hitbox, HitboxPart } from './hitbox';
3
+ /** Nearest part struck plus where. The `point` is filled in 3D only. */
4
+ export interface PartHit {
5
+ part: string;
6
+ distance: number;
7
+ point: [number, number, number];
8
+ }
9
+ /**
10
+ * A 3D part resolved to world space: its center after offset, and the
11
+ * half-extents after per-axis scale. Single-radius shapes (sphere,
12
+ * cylinder) inflate by the largest relevant axis so they enclose a
13
+ * non-uniformly scaled visual rather than clipping inside it. The single
14
+ * source of placement truth shared by picking and debug rendering.
15
+ */
16
+ export interface PlacedPart {
17
+ cx: number;
18
+ cy: number;
19
+ cz: number;
20
+ hx: number;
21
+ hy: number;
22
+ hz: number;
23
+ }
24
+ /** Place a 3D part at world center `(cx,cy,cz)` grown by scale `(sx,sy,sz)`. Reused output. */
25
+ export declare function placePart3D(part: HitboxPart<'3d'>, cx: number, cy: number, cz: number, sx: number, sy: number, sz: number): PlacedPart;
26
+ /**
27
+ * Entry-test a ray against a 3D hitbox placed at world center `(cx,cy,cz)`
28
+ * and grown by per-axis scale `(sx,sy,sz)`. Returns the nearest part the
29
+ * ray enters (a reused object, valid until the next call) or `null`.
30
+ */
31
+ export declare function testHitbox3D(ray: Ray3D, hitbox: Hitbox<'3d'>, cx: number, cy: number, cz: number, sx: number, sy: number, sz: number): PartHit | null;
32
+ /**
33
+ * Point-test against a 2D hitbox at center `(cx,cy)`, scaled `(sx,sy)`,
34
+ * rotated by `rot` radians. The world point is rotated into the hitbox's
35
+ * local frame, then tested against each part. Returns the first part
36
+ * containing the point (a reused object) or `null`.
37
+ */
38
+ export declare function testHitbox2D(hitbox: Hitbox<'2d'>, cx: number, cy: number, sx: number, sy: number, rot: number, wx: number, wy: number): PartHit | null;
39
+ /**
40
+ * Point inside a sprite's rendered quad: an `sx` by `sy` rect centered at
41
+ * `(cx,cy)`, rotated by `rot`. The default pick bound for a sprite with no
42
+ * declared hitbox.
43
+ */
44
+ export declare function pointInQuad2D(cx: number, cy: number, sx: number, sy: number, rot: number, wx: number, wy: number): boolean;
@@ -7,9 +7,15 @@ export * from './driver';
7
7
  export * from './navmesh';
8
8
  export * from './pooled-codec';
9
9
  export * from './prediction';
10
+ export * from './clock';
11
+ export * from './timeline';
10
12
  export * from './input';
11
13
  export * from './free-list';
12
14
  export * from './sparse-batcher';
15
+ export * from './slot-map';
16
+ export * from './state-machine';
13
17
  export * from './simple-rng';
14
18
  export * from './ray';
19
+ export * from './raycast';
20
+ export * from './hitbox';
15
21
  export * from '../renderer';
@@ -1,65 +1,42 @@
1
1
  /**
2
- * @template T
3
- * @description
4
- * Tracks client-side intents that have been sent to the server but not yet confirmed.
5
- * Used for prediction and reconciliation in a server-authoritative architecture.
2
+ * Bounded, sequence-ordered log of locally-applied commands awaiting
3
+ * confirmation. Records are pushed in ascending sequence; `dropThrough`
4
+ * removes everything the authority has confirmed.
6
5
  */
7
- export declare class IntentTracker<T> {
8
- tracker: Map<number, T[]>;
6
+ export declare class PredictionLog<Cmd> {
7
+ private capacity;
8
+ private entries;
9
+ constructor(capacity?: number);
9
10
  get size(): number;
10
- /**
11
- * Adds a new intent for a specific tick.
12
- * @param {number} tick - The tick number associated with the intent.
13
- * @param {T} intent - The intent data.
14
- */
15
- track(tick: number, intent: T): T;
16
- /**
17
- * Removes all intents up to and including a given tick.
18
- * Returns the remaining intents in ascending tick order.
19
- * @param {number} tick - The tick up to which intents should be dropped.
20
- * @returns {T[]} Array of remaining intents.
21
- */
22
- dropUpTo(tick: number): T[];
23
- /**
24
- * Returns all currently tracked intents in ascending tick order.
25
- * @returns {T[]}
26
- */
27
- values(): T[];
11
+ /** Record a command with its sequence. Commands must be recorded in ascending sequence. */
12
+ record(sequence: number, cmd: Cmd): void;
13
+ /** Drop every entry with sequence <= the confirmed sequence. */
14
+ dropThrough(sequence: number): void;
15
+ /** The commands still awaiting confirmation, in order. */
16
+ pending(): Cmd[];
17
+ clear(): void;
18
+ }
19
+ export interface ReconcilerOptions<Cmd, Ctx> {
20
+ /** Max unconfirmed commands kept; older ones are dropped. Default 64. */
21
+ bufferSize?: number;
22
+ /** Load authoritative state. Runs before confirmed commands are dropped. */
23
+ restore: (ctx: Ctx) => void;
24
+ /** Re-apply the still-unconfirmed commands on top of the restored state. */
25
+ replay: (cmds: Cmd[], ctx: Ctx) => void;
28
26
  }
29
27
  /**
30
- * @template T,U
31
- * @description
32
- * Handles client-side reconciliation of authoritative snapshots with unconfirmed intents.
33
- * Used for prediction correction in server-authoritative multiplayer games.
28
+ * Client-side rollback-replay: record locally-applied commands, and when the
29
+ * authority confirms up to a sequence, restore its state, drop the confirmed
30
+ * commands, and replay the rest. Domain-agnostic; the caller supplies what a
31
+ * command is, how to restore, and how to replay via callbacks.
34
32
  */
35
- export declare class Reconciliator<T, U> {
36
- private options;
37
- tracker: IntentTracker<T>;
38
- /**
39
- * @param {Object} options - Callbacks for applying snapshot state and replaying intents.
40
- * @param {(snapshotState: U) => void} options.onLoadState - Called to load authoritative snapshot state.
41
- * @param {(remainingIntents: T[]) => void} options.onReplay - Called to reapply remaining intents for prediction.
42
- */
43
- constructor(options: {
44
- onLoadState: (snapshotState: U) => void;
45
- onReplay: (remainingIntents: T[]) => void;
46
- });
47
- /**
48
- * Adds a new intent to the tracker.
49
- * @param {number} tick - Tick number associated with the intent.
50
- * @param {T} intent - The intent data.
51
- */
52
- trackIntent(tick: number, intent: T): void;
53
- /**
54
- * Called when an authoritative snapshot is received from the server.
55
- * Resets client state and replays unconfirmed intents.
56
- * @param {Object} snapshot - The snapshot from the server.
57
- * @param {number} snapshot.tick - Tick number of the snapshot.
58
- * @param {U} snapshot.state - The authoritative state.
59
- */
60
- onSnapshot(snapshot: {
61
- tick: number;
62
- state: U;
63
- }): void;
64
- replay(intents: T[]): void;
33
+ export declare class Reconciler<Cmd, Ctx = void> {
34
+ private log;
35
+ private restore;
36
+ private replay;
37
+ constructor(opts: ReconcilerOptions<Cmd, Ctx>);
38
+ record(sequence: number, cmd: Cmd): void;
39
+ get pending(): number;
40
+ clear(): void;
41
+ reconcile(ackSequence: number, ctx: Ctx): void;
65
42
  }
@@ -35,7 +35,27 @@ export declare class Ray3D {
35
35
  */
36
36
  intersectsAABB(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number | null;
37
37
  /**
38
- * Intersection with a triangle using the Möller–Trumbore algorithm.
38
+ * Sphere entry for picking: `t` to the nearest front-facing surface,
39
+ * or `null`. Unlike `intersectsSphere`, an origin inside the sphere or
40
+ * a sphere entirely behind the origin return `null` -- you only pick
41
+ * surfaces facing you.
42
+ */
43
+ entrySphere(cx: number, cy: number, cz: number, r: number): number | null;
44
+ /**
45
+ * Axis-aligned box entry for picking, given center and half-extents.
46
+ * `t` to the front face, or `null`. Origin-inside and fully-behind
47
+ * both return `null` (see `entrySphere`).
48
+ */
49
+ entryBox(cx: number, cy: number, cz: number, hx: number, hy: number, hz: number): number | null;
50
+ /**
51
+ * Y-axis-aligned cylinder entry for picking: center `(cx,cy,cz)`,
52
+ * radius `r`, total height `h` (spans `cy +- h/2`). Side = 2D
53
+ * ray-vs-circle in XZ keeping in-range Y; caps = the two disk planes.
54
+ * Nearest candidate, or `null`; origin-inside is rejected.
55
+ */
56
+ entryCylinder(cx: number, cy: number, cz: number, r: number, h: number): number | null;
57
+ /**
58
+ * Intersection with a triangle using the Möller-Trumbore algorithm.
39
59
  * Returns `t` at the hit point, `null` if no hit.
40
60
  */
41
61
  intersectsTriangle(ax: number, ay: number, az: number, bx: number, by: number, bz: number, cx: number, cy: number, cz: number): number | null;
@@ -0,0 +1,43 @@
1
+ export interface BufferedHit<H, Point extends number[]> {
2
+ handle: H;
3
+ distance: number;
4
+ point: Point;
5
+ /** Name of the hitbox part struck, or `null` for a default-bound (sphere/box/quad) hit. */
6
+ part: string | null;
7
+ }
8
+ type Filter<H> = (handle: H) => boolean;
9
+ export declare class HitBuffer<H, Point extends number[]> {
10
+ private handles;
11
+ private parts;
12
+ private keys;
13
+ private distances;
14
+ private px;
15
+ private py;
16
+ private pz;
17
+ private order;
18
+ count: number;
19
+ private capacity;
20
+ private sorted;
21
+ private readonly dims;
22
+ private nearestHit;
23
+ constructor(dims: 2 | 3);
24
+ private makeHit;
25
+ reset(): void;
26
+ /**
27
+ * `key` orders the hit (ascending = nearer); `distance` is the value
28
+ * the public hit reports. They differ only when ordering isn't by
29
+ * distance (e.g. 2D layer). `distance` defaults to `key`.
30
+ */
31
+ push(handle: H, key: number, x: number, y: number, z?: number, distance?: number, part?: string | null): void;
32
+ nearest(filter: Filter<H> | undefined, cap: number): BufferedHit<H, Point> | null;
33
+ collectInto(out: BufferedHit<H, Point>[], filter: Filter<H> | undefined, cap: number): void;
34
+ /**
35
+ * True if any live hit's handle matches `id` — comparing the handle
36
+ * directly when it is a number, or its `.id` when it is an object.
37
+ */
38
+ containsId(id: number): boolean;
39
+ private fill;
40
+ private grow;
41
+ private ensureSorted;
42
+ }
43
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './hit-buffer';
2
+ export * from './raycaster';
@@ -0,0 +1,54 @@
1
+ import type { Ray3D } from '../ray/ray-3d';
2
+ import type { Hitbox } from '../hitbox/hitbox';
3
+ import { type BufferedHit } from './hit-buffer';
4
+ /** Per-axis column accessors. Each returns the field arrays for a frame's worth of entities. */
5
+ interface Transform3D {
6
+ position: () => {
7
+ x: ArrayLike<number>;
8
+ y: ArrayLike<number>;
9
+ z: ArrayLike<number>;
10
+ };
11
+ scale: () => {
12
+ x: ArrayLike<number>;
13
+ y: ArrayLike<number>;
14
+ z: ArrayLike<number>;
15
+ };
16
+ }
17
+ interface Lookup {
18
+ /** Candidate ids to test this cast (e.g. an ECS query result). */
19
+ query: () => readonly number[];
20
+ /** Resolve an id to its hitbox, or `null` to skip it. */
21
+ hitbox: (id: number) => Hitbox<'3d'> | null;
22
+ }
23
+ type Hit = BufferedHit<number, [number, number, number]>;
24
+ interface QueryOptions {
25
+ filter?: (id: number) => boolean;
26
+ maxDistance?: number;
27
+ }
28
+ /**
29
+ * Raycaster — casts a ray against a set of entities and ranks the hits.
30
+ *
31
+ * Source-agnostic: `lookup` supplies the candidate ids and their hitboxes,
32
+ * `configure` supplies how to read each id's transform. Both carry all
33
+ * world knowledge as closures, so the raycaster depends on neither an ECS
34
+ * nor a renderer. Owns a reused hit buffer; `cast` is allocation-free per
35
+ * entity. The same instance can be exported from shared code and wired to
36
+ * a sim world on the server and a render world on the client.
37
+ */
38
+ export declare class Raycaster {
39
+ private buf;
40
+ private resultBuffer;
41
+ private _lookup;
42
+ private _transform;
43
+ /** Wire the candidate source: which ids to test and how to resolve each id's hitbox. */
44
+ lookup(lookup: Lookup): this;
45
+ /** Wire the transform source: how to read each id's position and scale. */
46
+ configure(transform: Transform3D): this;
47
+ /** Cast `ray` against the configured source, populating the hit buffer. Chains to `hit`/`hitAll`. */
48
+ cast(ray: Ray3D): this;
49
+ /** Nearest hit, or null. Pool-backed; valid until the next cast. */
50
+ hit(opts?: QueryOptions): Hit | null;
51
+ /** All hits, nearest first. Reused array; do not retain across casts. */
52
+ hitAll(opts?: QueryOptions): readonly Hit[];
53
+ }
54
+ export {};
@@ -0,0 +1 @@
1
+ export * from "./slot-map";
@@ -0,0 +1,109 @@
1
+ /**
2
+ * A branded integer id. Use `as SlotId<'light'>` (or your own brand) to keep
3
+ * a light id from being passed where an instance id is expected. Purely a
4
+ * compile-time tag — at runtime it is a plain number.
5
+ */
6
+ export type SlotId<Brand extends string = string> = number & {
7
+ readonly __slot?: Brand;
8
+ };
9
+ /**
10
+ * Dense slot set. Slots are allocated from a fixed-capacity FreeList; the id
11
+ * returned by `add` IS the slot and stays stable until freed. Live slots are
12
+ * kept in a packed array (`activeSlots`) for cache-friendly iteration with no
13
+ * per-call allocation, and a sparse `Int32Array` maps slot -> dense position
14
+ * (`-1` when absent) for O(1) membership and removal.
15
+ */
16
+ export declare class SlotMap {
17
+ private readonly freeList;
18
+ /** Packed live slots, valid for `[0, size)`. Iterate this. */
19
+ private readonly dense;
20
+ /** slot -> index into `dense`, or `-1` if the slot is not live. */
21
+ private readonly sparse;
22
+ private _size;
23
+ private readonly _capacity;
24
+ constructor(capacity: number);
25
+ /**
26
+ * Allocate a slot and add it to the live set.
27
+ * @returns the slot, or `-1` if the pool is exhausted.
28
+ */
29
+ add(): number;
30
+ /**
31
+ * Remove a slot from the live set and return it to the pool. Keeps `dense`
32
+ * packed by swapping the last live slot into the freed position and fixing
33
+ * its sparse entry. No-op if the slot is not live.
34
+ */
35
+ remove(slot: number): void;
36
+ /** Whether `slot` is currently live. O(1). */
37
+ has(slot: number): boolean;
38
+ /**
39
+ * The packed live-slot array. Valid for indices `[0, size)`; entries past
40
+ * `size` are stale. Reused across calls — do not retain.
41
+ */
42
+ get activeSlots(): Uint32Array;
43
+ /** Number of live slots. Iterate `activeSlots` over `[0, size)`. */
44
+ get size(): number;
45
+ /** Configured capacity (max simultaneously live slots). */
46
+ get capacity(): number;
47
+ /** Whether another slot can be allocated. */
48
+ hasAvailable(): boolean;
49
+ /**
50
+ * Iterate live slots in packed order. Zero allocations. Removing the
51
+ * current slot inside the callback is safe (swap-and-pop), but the
52
+ * swapped-in slot then occupies the current index — guard accordingly or
53
+ * iterate `activeSlots` manually if you need full control.
54
+ */
55
+ forEach(fn: (slot: number, index: number) => void): void;
56
+ /** Empty the set, returning every slot to the pool. */
57
+ clear(): void;
58
+ }
59
+ /**
60
+ * Dense object set keyed by an external id. Wraps a SlotMap (slot lifecycle +
61
+ * dense iteration) and a slot-indexed object array, plus a sparse id -> slot
62
+ * map so the external id need not equal the slot. The typed, tested
63
+ * replacement for `Map<id, T>` collections you iterate every frame.
64
+ *
65
+ * `capacity` caps how many items can be live at once. `maxId` sizes the id
66
+ * lookup table and defaults to `capacity`; pass it when ids come from a larger
67
+ * space than the store holds (e.g. a 256-slot archetype store keyed by ECS
68
+ * entity ids drawn from `maxEntities`). Ids must be non-negative integers
69
+ * below `maxId`. If your identity is a string or otherwise non-integer, keep a
70
+ * `Map<string, number>` at the boundary that translates it to a dense id once.
71
+ */
72
+ export declare class SlotStore<TId extends number, T> {
73
+ private readonly slots;
74
+ /** slot -> stored item. */
75
+ private readonly items;
76
+ /** external id -> slot, or `-1` if the id is not present. Sized by maxId. */
77
+ private readonly idToSlot;
78
+ /** slot -> external id (so dense iteration can report the id). */
79
+ private readonly slotToId;
80
+ private readonly _maxId;
81
+ constructor(capacity: number, maxId?: number);
82
+ /**
83
+ * Store `item` under `id`. Allocates a slot. Throws if `id` is already
84
+ * present or the pool is exhausted.
85
+ * @returns the allocated slot.
86
+ */
87
+ add(id: TId, item: T): number;
88
+ /** Remove the item stored under `id`. No-op if absent or out of range. */
89
+ remove(id: TId): void;
90
+ /** The item stored under `id`, or `null` if absent or out of range. O(1). */
91
+ get(id: TId): T | null;
92
+ /** Whether `id` is present. O(1). */
93
+ has(id: TId): boolean;
94
+ /** The stable slot for `id`, or `-1` if absent or out of range. */
95
+ slotOf(id: TId): number;
96
+ /** Number of stored items. */
97
+ get size(): number;
98
+ /** Configured capacity. */
99
+ get capacity(): number;
100
+ /** Whether another item can be added. */
101
+ hasAvailable(): boolean;
102
+ /**
103
+ * Iterate stored items in packed order. Zero allocations. The callback
104
+ * receives the item, its external id, and its slot.
105
+ */
106
+ forEach(fn: (item: T, id: TId, slot: number) => void): void;
107
+ /** Remove every item. */
108
+ clear(): void;
109
+ }
@@ -0,0 +1 @@
1
+ export * from "./state-machine";
@@ -0,0 +1,114 @@
1
+ import { EventSystem } from "../events";
2
+ import { SimpleRNG } from "../simple-rng";
3
+ import type { Field } from "../binary-codec";
4
+ type Schema = Record<string, Field<any>>;
5
+ type StatesSpec = Record<string, Schema>;
6
+ type FieldValue<F> = F extends Field<infer T, any> ? T : never;
7
+ type SchemaValues<Sc extends Schema> = {
8
+ -readonly [K in keyof Sc]: FieldValue<Sc[K]>;
9
+ };
10
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
11
+ type StateFields<S extends StatesSpec> = UnionToIntersection<{
12
+ [K in keyof S]: SchemaValues<S[K]>;
13
+ }[keyof S]>;
14
+ /**
15
+ * Per-entity cursor over the machine's columns. Reused for the entity's
16
+ * lifetime; reading a field returns that entity's value, writing sets it.
17
+ */
18
+ export type Handle<S extends StatesSpec> = HandleBase<S> & StateFields<S>;
19
+ interface HandleBase<S extends StatesSpec> {
20
+ readonly id: number;
21
+ readonly state: keyof S & string;
22
+ readonly stateId: number;
23
+ readonly ticksInState: number;
24
+ is(state: keyof S & string): boolean;
25
+ change(to: (keyof S & string) | number, payload?: number): void;
26
+ }
27
+ interface Handlers<H> {
28
+ enter?(handle: H, payload: number): void;
29
+ update?(handle: H): void;
30
+ exit?(handle: H): void;
31
+ }
32
+ interface ChangeEvent {
33
+ id: number;
34
+ from: number;
35
+ to: number;
36
+ }
37
+ type ChangeEvents = [["change", ChangeEvent]];
38
+ interface StateMachineOptions<S extends StatesSpec> {
39
+ initial: keyof S & string;
40
+ states: S;
41
+ capacity?: number;
42
+ maxId?: number;
43
+ rng?: SimpleRNG;
44
+ }
45
+ /**
46
+ * Fixed-capacity, zero-GC state machine over many entities. State and
47
+ * per-state data live in binary columns indexed by a stable slot; one machine
48
+ * definition drives every entity. Behavior is registered per state with `add`
49
+ * and runs during `tick`. Depends only on `SlotStore`, `EventSystem`,
50
+ * `SimpleRNG`, and `BinaryCodec` fields.
51
+ */
52
+ export declare class StateMachine<S extends StatesSpec> {
53
+ /** State name to numeric id. */
54
+ readonly id: {
55
+ readonly [K in keyof S & string]: number;
56
+ };
57
+ readonly rng: SimpleRNG;
58
+ /** Transition channel, emitted whenever any entity changes state. */
59
+ readonly events: EventSystem<ChangeEvents>;
60
+ private readonly names;
61
+ private readonly initialId;
62
+ private readonly store;
63
+ private readonly stateCol;
64
+ private readonly prevCol;
65
+ private readonly enteredCol;
66
+ private readonly cols;
67
+ private readonly fieldsByState;
68
+ private readonly enterFns;
69
+ private readonly updateFns;
70
+ private readonly exitFns;
71
+ private readonly Handle;
72
+ private now;
73
+ private ticking;
74
+ private readonly dead;
75
+ private readonly ev;
76
+ constructor(opts: StateMachineOptions<S>);
77
+ /** Number of live entities. */
78
+ get size(): number;
79
+ /**
80
+ * Register behavior for a state. Cumulative: calling `add` again for the
81
+ * same state appends another set of handlers. Returns the machine for
82
+ * chaining.
83
+ */
84
+ add(state: keyof S & string, handlers: Handlers<Handle<S>>): this;
85
+ /** Register an entity by id, in the initial state. Returns its handle. */
86
+ spawn(id: number): Handle<S>;
87
+ /** The handle for `id`, or `null` if absent. */
88
+ of(id: number): Handle<S> | null;
89
+ has(id: number): boolean;
90
+ /** Remove an entity. Deferred to the end of the pass if called during `tick`. */
91
+ remove(id: number): void;
92
+ /**
93
+ * Advance every entity by one step. Runs the current state's `update`
94
+ * handlers in registration order; the first that transitions ends that
95
+ * entity's chain for this step.
96
+ */
97
+ tick(): void;
98
+ /**
99
+ * Write an entity's state id and current-state fields into `dv` at
100
+ * `offset`. Returns the offset past the written bytes.
101
+ */
102
+ serialize(id: number, dv: DataView, offset: number): number;
103
+ /**
104
+ * Load an entity's state id and fields from `dv` at `offset`, re-anchoring
105
+ * its `ticksInState`. Returns the offset past the read bytes.
106
+ */
107
+ restore(id: number, dv: DataView, offset: number): number;
108
+ /** Serialized byte size of an entity in its current state. */
109
+ byteSize(id: number): number;
110
+ private readonly step;
111
+ private transition;
112
+ private buildHandle;
113
+ }
114
+ export {};
@@ -0,0 +1 @@
1
+ export { Timeline, type TimelineEntry } from './timeline';
@@ -0,0 +1,34 @@
1
+ export interface TimelineEntry<S> {
2
+ tick: number;
3
+ receivedAt: number;
4
+ sample: S;
5
+ }
6
+ /**
7
+ * A bounded, tick-ordered ring of timestamped samples. Inserts in tick order
8
+ * (dedup by tick), prunes to capacity, and drops history when a wall-clock gap
9
+ * exceeds the stale window. Knows nothing about what a sample contains.
10
+ */
11
+ export declare class Timeline<S> {
12
+ private entries;
13
+ private _latestReceivedAt;
14
+ capacity: number;
15
+ staleWindow: number;
16
+ constructor(capacity: number, staleWindowMs: number);
17
+ get length(): number;
18
+ get latestReceivedAt(): number;
19
+ at(index: number): TimelineEntry<S>;
20
+ newest(): TimelineEntry<S> | undefined;
21
+ oldest(): TimelineEntry<S> | undefined;
22
+ setStaleWindow(staleWindowMs: number): void;
23
+ clear(): void;
24
+ /**
25
+ * Insert a sample in tick order. Returns true if a wall-clock gap beyond
26
+ * the stale window dropped the existing history first.
27
+ */
28
+ record(tick: number, receivedAt: number, sample: S): boolean;
29
+ /**
30
+ * Indices `[a, b]` of the consecutive entries straddling `tick`
31
+ * (`entries[a].tick <= tick <= entries[b].tick`), or null if none.
32
+ */
33
+ straddle(tick: number): [number, number] | null;
34
+ }
@@ -29,6 +29,7 @@ export declare class GameLoop<T extends GameLoopType = DriverType> {
29
29
  private _tickData;
30
30
  private _skipData;
31
31
  private _renderData;
32
+ private _scheduler;
32
33
  constructor(options: GameLoopOptions<T>);
33
34
  step(deltaTime: number): void;
34
35
  /**
@@ -47,10 +48,39 @@ export declare class GameLoop<T extends GameLoopType = DriverType> {
47
48
  * Stops the game ticker and emits a 'stop' event.
48
49
  */
49
50
  stop(): void;
51
+ /**
52
+ * Registers a callback to run on a fixed tick interval.
53
+ *
54
+ * The unit methods resolve to a tick count from the loop's `tickRate`, so
55
+ * every schedule fires in lockstep with the simulation regardless of unit.
56
+ *
57
+ * @example
58
+ * const id = loop.every(2).seconds(spawnWave);
59
+ * loop.clearSchedule(id);
60
+ */
61
+ every(count: number): ScheduleBuilder;
62
+ /**
63
+ * Cancels a single schedule by the id returned from `every`.
64
+ */
65
+ clearSchedule(id: number): boolean;
66
+ /**
67
+ * Cancels every registered schedule.
68
+ */
69
+ clearSchedules(): void;
70
+ }
71
+ interface ScheduleBuilder {
72
+ ticks(cb: () => void): number;
73
+ seconds(cb: () => void): number;
74
+ milliseconds(cb: () => void): number;
50
75
  }
51
76
  interface GameLoopOptions<T extends GameLoopType> {
52
77
  tickRate: number;
53
78
  type: T;
79
+ /**
80
+ * Maximum number of simultaneously live schedules registered via `every`.
81
+ * Defaults to 32.
82
+ */
83
+ maxSchedules?: number;
54
84
  onTick?: (deltaTime: number, tick: number, input: ReturnType<InputManager["snapshot"]>) => void;
55
85
  onRender?: (deltaTime: number, alpha: number, input: ReturnType<InputManager["peek"]>) => void;
56
86
  }