@arcane-engine/runtime 0.1.0 → 0.2.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.
Files changed (47) hide show
  1. package/package.json +4 -2
  2. package/src/agent/protocol.ts +35 -1
  3. package/src/agent/types.ts +98 -13
  4. package/src/particles/emitter.test.ts +323 -0
  5. package/src/particles/emitter.ts +409 -0
  6. package/src/particles/index.ts +25 -0
  7. package/src/particles/types.ts +236 -0
  8. package/src/pathfinding/astar.ts +27 -0
  9. package/src/pathfinding/types.ts +39 -0
  10. package/src/physics/aabb.ts +55 -8
  11. package/src/rendering/animation.ts +73 -0
  12. package/src/rendering/audio.ts +29 -9
  13. package/src/rendering/camera.ts +28 -4
  14. package/src/rendering/input.ts +45 -9
  15. package/src/rendering/lighting.ts +29 -3
  16. package/src/rendering/loop.ts +16 -3
  17. package/src/rendering/sprites.ts +24 -1
  18. package/src/rendering/text.ts +52 -6
  19. package/src/rendering/texture.ts +22 -4
  20. package/src/rendering/tilemap.ts +36 -4
  21. package/src/rendering/types.ts +37 -19
  22. package/src/rendering/validate.ts +48 -3
  23. package/src/state/error.ts +21 -2
  24. package/src/state/observe.ts +40 -9
  25. package/src/state/prng.ts +88 -10
  26. package/src/state/query.ts +115 -15
  27. package/src/state/store.ts +42 -11
  28. package/src/state/transaction.ts +116 -12
  29. package/src/state/types.ts +31 -5
  30. package/src/systems/system.ts +77 -5
  31. package/src/systems/types.ts +52 -6
  32. package/src/testing/harness.ts +103 -5
  33. package/src/testing/mock-renderer.test.ts +16 -20
  34. package/src/tweening/chain.test.ts +191 -0
  35. package/src/tweening/chain.ts +103 -0
  36. package/src/tweening/easing.test.ts +134 -0
  37. package/src/tweening/easing.ts +288 -0
  38. package/src/tweening/helpers.test.ts +185 -0
  39. package/src/tweening/helpers.ts +166 -0
  40. package/src/tweening/index.ts +76 -0
  41. package/src/tweening/tween.test.ts +322 -0
  42. package/src/tweening/tween.ts +296 -0
  43. package/src/tweening/types.ts +134 -0
  44. package/src/ui/colors.ts +129 -0
  45. package/src/ui/index.ts +1 -0
  46. package/src/ui/primitives.ts +44 -5
  47. package/src/ui/types.ts +41 -2
@@ -4,25 +4,41 @@ import { getCamera } from "./camera.ts";
4
4
 
5
5
  // --- Types ---
6
6
 
7
+ /** Descriptor for a bitmap font backed by a texture atlas. */
7
8
  export type BitmapFont = {
9
+ /** Texture handle containing the font glyph atlas. */
8
10
  textureId: TextureId;
11
+ /** Width of each glyph cell in pixels. */
9
12
  glyphW: number;
13
+ /** Height of each glyph cell in pixels. */
10
14
  glyphH: number;
15
+ /** Number of glyph columns in the atlas. */
11
16
  columns: number;
17
+ /** Number of glyph rows in the atlas. */
12
18
  rows: number;
19
+ /** ASCII code of the first glyph in the atlas. Default: 32 (space). */
13
20
  firstChar: number;
14
21
  };
15
22
 
23
+ /** Options for {@link drawText} and {@link measureText}. */
16
24
  export type TextOptions = {
25
+ /** Font to use. Default: built-in 8x8 CP437 bitmap font via getDefaultFont(). */
17
26
  font?: BitmapFont;
27
+ /** Scale multiplier for glyph size. Default: 1. A value of 2 draws at 16x16. */
18
28
  scale?: number;
29
+ /** RGBA tint color for the text (0.0-1.0 per channel). Default: white. */
19
30
  tint?: { r: number; g: number; b: number; a: number };
31
+ /** Draw order layer. Default: 100 (above most game sprites). */
20
32
  layer?: number;
33
+ /** If true, position is in screen pixels (HUD). If false, position is in world units. Default: false. */
21
34
  screenSpace?: boolean;
22
35
  };
23
36
 
37
+ /** Result of {@link measureText}. Dimensions in pixels (before camera transform). */
24
38
  export type TextMeasurement = {
39
+ /** Total width in pixels (text.length * glyphW * scale). */
25
40
  width: number;
41
+ /** Total height in pixels (glyphH * scale). */
26
42
  height: number;
27
43
  };
28
44
 
@@ -54,6 +70,15 @@ let defaultFont: BitmapFont | null = null;
54
70
 
55
71
  /**
56
72
  * Create a bitmap font descriptor from a texture atlas.
73
+ * The atlas should contain a grid of equally-sized glyphs in ASCII order.
74
+ *
75
+ * @param textureId - Texture handle of the font atlas (from loadTexture()).
76
+ * @param glyphW - Width of each glyph cell in pixels.
77
+ * @param glyphH - Height of each glyph cell in pixels.
78
+ * @param columns - Number of glyph columns in the atlas.
79
+ * @param rows - Number of glyph rows in the atlas.
80
+ * @param firstChar - ASCII code of the first glyph in the atlas. Default: 32 (space).
81
+ * @returns BitmapFont descriptor for use with drawText().
57
82
  */
58
83
  export function loadFont(
59
84
  textureId: TextureId,
@@ -67,8 +92,10 @@ export function loadFont(
67
92
  }
68
93
 
69
94
  /**
70
- * Get the default 8x8 bitmap font, lazily initialized.
95
+ * Get the default built-in 8x8 CP437 bitmap font, lazily initialized.
71
96
  * In headless mode returns a dummy font (textureId 0).
97
+ *
98
+ * @returns The built-in BitmapFont (8x8 glyphs, 16 columns, 6 rows, ASCII 32-127).
72
99
  */
73
100
  export function getDefaultFont(): BitmapFont {
74
101
  if (defaultFont !== null) return defaultFont;
@@ -100,8 +127,12 @@ export function getDefaultFont(): BitmapFont {
100
127
  }
101
128
 
102
129
  /**
103
- * Measure the pixel dimensions of a text string.
104
- * Pure math works in headless mode.
130
+ * Measure the pixel dimensions of a text string without drawing it.
131
+ * Pure math -- works in headless mode.
132
+ *
133
+ * @param text - The string to measure.
134
+ * @param options - Font and scale options. Default font and scale 1 if omitted.
135
+ * @returns Width and height in pixels.
105
136
  */
106
137
  export function measureText(
107
138
  text: string,
@@ -116,9 +147,24 @@ export function measureText(
116
147
  }
117
148
 
118
149
  /**
119
- * Draw a text string using the sprite pipeline.
120
- * Each character becomes one drawSprite() call.
121
- * No-op in headless mode.
150
+ * Draw a text string using the sprite pipeline (one sprite per character).
151
+ * Must be called every frame. No-op in headless mode.
152
+ *
153
+ * @param text - The string to draw.
154
+ * @param x - X position (screen pixels if screenSpace, world units otherwise).
155
+ * @param y - Y position (screen pixels if screenSpace, world units otherwise).
156
+ * @param options - Font, scale, tint, layer, and screenSpace options.
157
+ *
158
+ * @example
159
+ * // Draw HUD text at the top-left of the screen
160
+ * drawText("HP: 100", 10, 10, { scale: 2, screenSpace: true });
161
+ *
162
+ * @example
163
+ * // Draw world-space text above an entity
164
+ * drawText("Enemy", enemy.x, enemy.y - 12, {
165
+ * tint: { r: 1, g: 0.3, b: 0.3, a: 1 },
166
+ * layer: 50,
167
+ * });
122
168
  */
123
169
  export function drawText(
124
170
  text: string,
@@ -5,9 +5,16 @@ const hasRenderOps =
5
5
  typeof (globalThis as any).Deno?.core?.ops?.op_load_texture === "function";
6
6
 
7
7
  /**
8
- * Load a texture from a PNG file path. Returns a texture handle.
9
- * Caches by path loading the same path twice returns the same handle.
8
+ * Load a texture from a PNG file path. Returns an opaque texture handle.
9
+ * Caches by path -- loading the same path twice returns the same handle.
10
10
  * Returns 0 (no texture) in headless mode.
11
+ *
12
+ * @param path - File path to a PNG image (relative to game entry file or absolute).
13
+ * @returns Texture handle for use with drawSprite(), createTilemap(), etc.
14
+ *
15
+ * @example
16
+ * const playerTex = loadTexture("assets/player.png");
17
+ * drawSprite({ textureId: playerTex, x: 0, y: 0, w: 32, h: 32 });
11
18
  */
12
19
  export function loadTexture(path: string): TextureId {
13
20
  if (!hasRenderOps) return 0;
@@ -15,9 +22,20 @@ export function loadTexture(path: string): TextureId {
15
22
  }
16
23
 
17
24
  /**
18
- * Create a solid-color 1x1 texture. Useful for placeholder sprites.
19
- * Colors are 0-255 RGBA.
25
+ * Create a 1x1 solid-color texture. Useful for rectangles, placeholder sprites,
26
+ * and UI elements. Cached by name -- creating the same name twice returns the same handle.
20
27
  * Returns 0 (no texture) in headless mode.
28
+ *
29
+ * @param name - Unique name for caching (e.g. "red", "health-green").
30
+ * @param r - Red channel, 0-255.
31
+ * @param g - Green channel, 0-255.
32
+ * @param b - Blue channel, 0-255.
33
+ * @param a - Alpha channel, 0-255. Default: 255 (fully opaque).
34
+ * @returns Texture handle for use with drawSprite().
35
+ *
36
+ * @example
37
+ * const redTex = createSolidTexture("red", 255, 0, 0);
38
+ * drawSprite({ textureId: redTex, x: 10, y: 10, w: 50, h: 50 });
21
39
  */
22
40
  export function createSolidTexture(
23
41
  name: string,
@@ -4,7 +4,14 @@ const hasRenderOps =
4
4
  typeof (globalThis as any).Deno !== "undefined" &&
5
5
  typeof (globalThis as any).Deno?.core?.ops?.op_create_tilemap === "function";
6
6
 
7
- /** Create a tilemap backed by a texture atlas. Returns a TilemapId handle. */
7
+ /**
8
+ * Create a tilemap backed by a texture atlas. Returns an opaque TilemapId handle.
9
+ * The tilemap stores a grid of tile IDs that map to sub-regions of the atlas texture.
10
+ * Returns 0 in headless mode.
11
+ *
12
+ * @param opts - Tilemap configuration (atlas texture, grid size, tile size, atlas layout).
13
+ * @returns Tilemap handle for use with setTile(), getTile(), drawTilemap().
14
+ */
8
15
  export function createTilemap(opts: TilemapOptions): TilemapId {
9
16
  if (!hasRenderOps) return 0;
10
17
  return (globalThis as any).Deno.core.ops.op_create_tilemap(
@@ -17,7 +24,16 @@ export function createTilemap(opts: TilemapOptions): TilemapId {
17
24
  );
18
25
  }
19
26
 
20
- /** Set a tile at grid position (gx, gy). Tile ID 0 = empty. */
27
+ /**
28
+ * Set a tile at grid position (gx, gy).
29
+ * Tile IDs correspond to positions in the texture atlas (left-to-right, top-to-bottom).
30
+ * Tile ID 0 = empty (not drawn). No-op in headless mode.
31
+ *
32
+ * @param id - Tilemap handle from createTilemap().
33
+ * @param gx - Grid X position (column). 0 = leftmost.
34
+ * @param gy - Grid Y position (row). 0 = topmost.
35
+ * @param tileId - Tile index in the atlas (1-based). 0 = empty/clear.
36
+ */
21
37
  export function setTile(
22
38
  id: TilemapId,
23
39
  gx: number,
@@ -28,13 +44,29 @@ export function setTile(
28
44
  (globalThis as any).Deno.core.ops.op_set_tile(id, gx, gy, tileId);
29
45
  }
30
46
 
31
- /** Get the tile ID at grid position (gx, gy). Returns 0 if out of bounds. */
47
+ /**
48
+ * Get the tile ID at grid position (gx, gy).
49
+ * Returns 0 if out of bounds or in headless mode.
50
+ *
51
+ * @param id - Tilemap handle from createTilemap().
52
+ * @param gx - Grid X position (column).
53
+ * @param gy - Grid Y position (row).
54
+ * @returns Tile ID at the given position, or 0 if empty/out of bounds.
55
+ */
32
56
  export function getTile(id: TilemapId, gx: number, gy: number): number {
33
57
  if (!hasRenderOps) return 0;
34
58
  return (globalThis as any).Deno.core.ops.op_get_tile(id, gx, gy);
35
59
  }
36
60
 
37
- /** Draw all visible tiles as sprites (camera-culled). */
61
+ /**
62
+ * Draw all visible tiles as sprites. Only draws tiles within the camera viewport (culled).
63
+ * Must be called every frame. No-op in headless mode.
64
+ *
65
+ * @param id - Tilemap handle from createTilemap().
66
+ * @param x - World X offset for the tilemap origin. Default: 0.
67
+ * @param y - World Y offset for the tilemap origin. Default: 0.
68
+ * @param layer - Draw order layer. Default: 0.
69
+ */
38
70
  export function drawTilemap(
39
71
  id: TilemapId,
40
72
  x: number = 0,
@@ -1,54 +1,72 @@
1
- /** Opaque handle to a loaded texture. */
1
+ /**
2
+ * Opaque handle to a loaded texture. Returned by {@link loadTexture} or {@link createSolidTexture}.
3
+ * A value of 0 means "no texture" (headless mode fallback).
4
+ */
2
5
  export type TextureId = number;
3
6
 
4
- /** Options for drawing a sprite. */
7
+ /** Options for drawing a sprite via {@link drawSprite}. */
5
8
  export type SpriteOptions = {
6
9
  /** Texture handle from loadTexture() or createSolidTexture(). */
7
10
  textureId: TextureId;
8
- /** World X position (top-left corner). */
11
+ /** World X position (top-left corner of sprite). */
9
12
  x: number;
10
- /** World Y position (top-left corner). */
13
+ /** World Y position (top-left corner of sprite). */
11
14
  y: number;
12
- /** Width in world units. */
15
+ /** Width in world units (pixels at zoom 1). */
13
16
  w: number;
14
- /** Height in world units. */
17
+ /** Height in world units (pixels at zoom 1). */
15
18
  h: number;
16
- /** Draw order layer (lower = drawn first / behind). Default: 0. */
19
+ /** Draw order layer. Lower values are drawn first (behind). Default: 0. Use 100+ for HUD elements. */
17
20
  layer?: number;
18
- /** UV sub-rect for atlas sprites. Default: full texture. */
21
+ /**
22
+ * UV sub-rectangle for atlas/sprite-sheet textures.
23
+ * All values are normalized 0.0-1.0 (fraction of full texture).
24
+ * Default: full texture `{ x: 0, y: 0, w: 1, h: 1 }`.
25
+ */
19
26
  uv?: { x: number; y: number; w: number; h: number };
20
- /** RGBA tint color (0-1 range). Default: white (1,1,1,1). */
27
+ /**
28
+ * RGBA tint color multiplied with the texture color.
29
+ * Each channel is 0.0-1.0. Default: white `{ r: 1, g: 1, b: 1, a: 1 }` (no tint).
30
+ */
21
31
  tint?: { r: number; g: number; b: number; a: number };
22
32
  };
23
33
 
24
- /** Camera state. */
34
+ /** Camera state returned by {@link getCamera}. */
25
35
  export type CameraState = {
36
+ /** Camera center X position in world units. */
26
37
  x: number;
38
+ /** Camera center Y position in world units. */
27
39
  y: number;
40
+ /** Zoom level. 1.0 = default, >1.0 = zoomed in, <1.0 = zoomed out. */
28
41
  zoom: number;
29
42
  };
30
43
 
31
- /** Mouse position. */
44
+ /** Mouse position in screen or world coordinates. */
32
45
  export type MousePosition = {
46
+ /** X position in pixels (screen) or world units (world). */
33
47
  x: number;
48
+ /** Y position in pixels (screen) or world units (world). */
34
49
  y: number;
35
50
  };
36
51
 
37
- /** Opaque handle to a tilemap. */
52
+ /**
53
+ * Opaque handle to a tilemap. Returned by {@link createTilemap}.
54
+ * A value of 0 means "no tilemap" (headless mode fallback).
55
+ */
38
56
  export type TilemapId = number;
39
57
 
40
- /** Options for creating a tilemap. */
58
+ /** Options for creating a tilemap via {@link createTilemap}. */
41
59
  export type TilemapOptions = {
42
- /** Texture atlas handle from loadTexture(). */
60
+ /** Texture atlas handle from loadTexture(). Must be a valid TextureId. */
43
61
  textureId: number;
44
- /** Grid width in tiles. */
62
+ /** Grid width in tiles. Must be a positive integer. */
45
63
  width: number;
46
- /** Grid height in tiles. */
64
+ /** Grid height in tiles. Must be a positive integer. */
47
65
  height: number;
48
- /** Size of each tile in world units. */
66
+ /** Size of each tile in world units (pixels at zoom 1). Must be positive. */
49
67
  tileSize: number;
50
- /** Number of columns in the texture atlas. */
68
+ /** Number of tile columns in the texture atlas. Must be a positive integer. */
51
69
  atlasColumns: number;
52
- /** Number of rows in the texture atlas. */
70
+ /** Number of tile rows in the texture atlas. Must be a positive integer. */
53
71
  atlasRows: number;
54
72
  };
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Runtime validation for rendering API calls
3
- * Add this to production code to catch errors early
2
+ * Runtime validation for rendering API calls.
3
+ * Add validation to catch type errors and invalid values early in development.
4
4
  */
5
5
 
6
+ /** Options controlling how validation errors are reported. */
6
7
  export interface ValidationOptions {
8
+ /** If true, throw on validation failure. Default: true. */
7
9
  throwOnError?: boolean;
10
+ /** If true, log errors to console.error. Default: false. */
8
11
  logErrors?: boolean;
9
12
  }
10
13
 
@@ -13,6 +16,15 @@ const defaultOptions: ValidationOptions = {
13
16
  logErrors: false,
14
17
  };
15
18
 
19
+ /**
20
+ * Validate that a value is a finite number (not NaN, not Infinity).
21
+ *
22
+ * @param api - Name of the calling API (for error messages).
23
+ * @param param - Name of the parameter being validated.
24
+ * @param value - The value to validate.
25
+ * @param options - Error reporting options.
26
+ * @returns true if valid, false or throws if invalid.
27
+ */
16
28
  export function validateNumber(
17
29
  api: string,
18
30
  param: string,
@@ -37,6 +49,14 @@ export function validateNumber(
37
49
  return true;
38
50
  }
39
51
 
52
+ /**
53
+ * Validate that a value is a valid Color object with r, g, b channels in 0.0-1.0.
54
+ *
55
+ * @param api - Name of the calling API (for error messages).
56
+ * @param color - The color object to validate.
57
+ * @param options - Error reporting options.
58
+ * @returns true if valid, false or throws if invalid.
59
+ */
40
60
  export function validateColor(
41
61
  api: string,
42
62
  color: any,
@@ -67,6 +87,13 @@ export function validateColor(
67
87
  return true;
68
88
  }
69
89
 
90
+ /**
91
+ * Validate drawText options (x, y, size must be valid numbers, color must be valid).
92
+ *
93
+ * @param opts - The text options object to validate.
94
+ * @param options - Error reporting options.
95
+ * @returns true if valid, false or throws if invalid.
96
+ */
70
97
  export function validateTextOptions(
71
98
  opts: any,
72
99
  options: ValidationOptions = defaultOptions
@@ -87,6 +114,17 @@ export function validateTextOptions(
87
114
  return true;
88
115
  }
89
116
 
117
+ /**
118
+ * Validate drawRect parameters (x, y, w, h must be finite numbers, color must be valid).
119
+ *
120
+ * @param x - X position to validate.
121
+ * @param y - Y position to validate.
122
+ * @param w - Width to validate.
123
+ * @param h - Height to validate.
124
+ * @param opts - Optional rect options with color.
125
+ * @param options - Error reporting options.
126
+ * @returns true if valid, false or throws if invalid.
127
+ */
90
128
  export function validateRectParams(
91
129
  x: any,
92
130
  y: any,
@@ -119,7 +157,14 @@ function handleError(message: string, options: ValidationOptions): boolean {
119
157
  return false;
120
158
  }
121
159
 
122
- // Helper: wrap a rendering function with validation
160
+ /**
161
+ * Wrap a rendering function with automatic parameter validation.
162
+ * If validation fails, the wrapped function is not called.
163
+ *
164
+ * @param fn - The rendering function to wrap.
165
+ * @param validator - Validation function that receives the same args. Returns true if valid.
166
+ * @returns Wrapped function that validates before calling the original.
167
+ */
123
168
  export function withValidation<T extends (...args: any[]) => any>(
124
169
  fn: T,
125
170
  validator: (...args: Parameters<T>) => boolean
@@ -1,4 +1,15 @@
1
- /** Structured error type — per docs/api-design.md */
1
+ /**
2
+ * Structured error type for the Arcane engine.
3
+ * Provides machine-readable codes, human-readable messages, and actionable context.
4
+ * Follows the error design in docs/api-design.md.
5
+ *
6
+ * - `code` - Machine-readable error code (e.g., "TRANSACTION_FAILED", "INVALID_PATH").
7
+ * - `message` - Human-readable error description.
8
+ * - `context.action` - What operation was being attempted.
9
+ * - `context.reason` - Why the operation failed.
10
+ * - `context.state` - Optional snapshot of relevant state at failure time.
11
+ * - `context.suggestion` - Optional suggestion for how to fix the error.
12
+ */
2
13
  export type ArcaneError = Readonly<{
3
14
  code: string;
4
15
  message: string;
@@ -10,7 +21,15 @@ export type ArcaneError = Readonly<{
10
21
  }>;
11
22
  }>;
12
23
 
13
- /** Create an ArcaneError */
24
+ /**
25
+ * Create an ArcaneError with structured context.
26
+ * Pure function — returns a new frozen error object.
27
+ *
28
+ * @param code - Machine-readable error code (e.g., "TRANSACTION_FAILED").
29
+ * @param message - Human-readable error description.
30
+ * @param context - Structured context with action, reason, and optional suggestion.
31
+ * @returns A new ArcaneError object.
32
+ */
14
33
  export function createError(
15
34
  code: string,
16
35
  message: string,
@@ -1,36 +1,62 @@
1
1
  import type { Diff } from "./transaction.ts";
2
2
 
3
- /** Observer callback — receives new value, old value, and context */
3
+ /**
4
+ * Callback invoked when an observed path changes.
5
+ * Receives the new value, old value, and context about the change.
6
+ *
7
+ * @param newValue - The value after the change.
8
+ * @param oldValue - The value before the change.
9
+ * @param context - Metadata about the change (path, full diff).
10
+ */
4
11
  export type ObserverCallback<T = unknown> = (
5
12
  newValue: T,
6
13
  oldValue: T,
7
14
  context: ObserverContext,
8
15
  ) => void;
9
16
 
10
- /** Context provided to observer callbacks */
17
+ /**
18
+ * Context provided to observer callbacks when a change is detected.
19
+ *
20
+ * - `path` - The specific path that changed (e.g., "player.hp").
21
+ * - `diff` - The full Diff from the transaction that triggered this notification.
22
+ */
11
23
  export type ObserverContext = Readonly<{
12
24
  path: string;
13
25
  diff: Diff;
14
26
  }>;
15
27
 
16
- /** Unsubscribe function */
28
+ /**
29
+ * Function returned by observe() to unsubscribe from further notifications.
30
+ * Call it to stop receiving callbacks for that subscription.
31
+ */
17
32
  export type Unsubscribe = () => void;
18
33
 
19
- /** Pattern for path matching (supports * wildcards) */
34
+ /**
35
+ * Pattern for matching state paths. Supports `*` wildcards at any segment position.
36
+ * Examples: "player.hp", "enemies.*.hp", "*.position".
37
+ * Each `*` matches exactly one path segment.
38
+ */
20
39
  export type PathPattern = string;
21
40
 
22
- /** Observer registry — manages subscriptions and dispatches notifications */
41
+ /**
42
+ * Observer registry that manages subscriptions and dispatches change notifications.
43
+ * Created via {@link createObserverRegistry}. Used internally by GameStore.
44
+ *
45
+ * - `observe` - Subscribe to changes matching a path pattern. Returns an Unsubscribe function.
46
+ * - `notify` - Dispatch notifications to all matching observers for a given diff.
47
+ * - `clear` - Remove all observers (useful for cleanup/reset).
48
+ */
23
49
  export type ObserverRegistry<S> = Readonly<{
24
- /** Subscribe to changes at a path pattern */
50
+ /** Subscribe to changes at a path pattern. Returns an unsubscribe function. */
25
51
  observe: <T = unknown>(
26
52
  pattern: PathPattern,
27
53
  callback: ObserverCallback<T>,
28
54
  ) => Unsubscribe;
29
55
 
30
- /** Notify all matching observers after a transaction commits */
56
+ /** Notify all matching observers after a transaction commits. */
31
57
  notify: (oldState: S, newState: S, diff: Diff) => void;
32
58
 
33
- /** Remove all observers */
59
+ /** Remove all observers. */
34
60
  clear: () => void;
35
61
  }>;
36
62
 
@@ -39,7 +65,12 @@ type Subscription = {
39
65
  callback: ObserverCallback;
40
66
  };
41
67
 
42
- /** Create a new observer registry */
68
+ /**
69
+ * Create a new observer registry for tracking state change subscriptions.
70
+ * Used internally by {@link createStore} to power the store's observe() method.
71
+ *
72
+ * @returns A new ObserverRegistry with observe, notify, and clear methods.
73
+ */
43
74
  export function createObserverRegistry<S>(): ObserverRegistry<S> {
44
75
  const subscriptions = new Set<Subscription>();
45
76
 
package/src/state/prng.ts CHANGED
@@ -1,4 +1,13 @@
1
- /** Opaque PRNG state — serializable, deterministic (xoshiro128**) */
1
+ /**
2
+ * Opaque PRNG state using the xoshiro128** algorithm.
3
+ * Serializable and deterministic — the same seed always produces the same sequence.
4
+ * All PRNG functions are pure: they take a PRNGState and return a new PRNGState.
5
+ * Create via {@link seed}.
6
+ *
7
+ * - `__brand` - Type brand, always "PRNGState".
8
+ * - `seed` - The original seed value used to initialize this state.
9
+ * - `s0`, `s1`, `s2`, `s3` - Internal 32-bit state words. Do not modify directly.
10
+ */
2
11
  export type PRNGState = Readonly<{
3
12
  readonly __brand: "PRNGState";
4
13
  seed: number;
@@ -8,7 +17,18 @@ export type PRNGState = Readonly<{
8
17
  s3: number;
9
18
  }>;
10
19
 
11
- /** Create a seeded PRNG */
20
+ /**
21
+ * Create a seeded PRNG state. The same seed always produces the same random sequence.
22
+ * Uses splitmix32 to initialize the xoshiro128** internal state.
23
+ *
24
+ * @param n - The seed value. Truncated to a 32-bit integer.
25
+ * @returns A new PRNGState ready for use with {@link rollDice}, {@link randomInt}, etc.
26
+ *
27
+ * @example
28
+ * const rng = seed(42);
29
+ * const [value, rng2] = randomInt(rng, 1, 6);
30
+ * // value is deterministic for seed 42
31
+ */
12
32
  export function seed(n: number): PRNGState {
13
33
  // Initialize state from seed using splitmix32
14
34
  let s = n | 0;
@@ -30,17 +50,33 @@ export function seed(n: number): PRNGState {
30
50
  };
31
51
  }
32
52
 
33
- /** Dice notation: "2d6+3" */
53
+ /**
54
+ * Branded string type for dice notation (e.g., "2d6+3", "1d20", "3d8-1").
55
+ * Format: `NdS` or `NdS+M` / `NdS-M` where N=count, S=sides, M=modifier.
56
+ */
34
57
  export type DiceNotation = string & { readonly __dice: true };
35
58
 
36
- /** Parsed dice specification */
59
+ /**
60
+ * Parsed dice specification. Created by {@link parseDice} or passed directly to {@link rollDice}.
61
+ *
62
+ * - `count` - Number of dice to roll (the N in NdS). Must be >= 1.
63
+ * - `sides` - Number of sides per die (the S in NdS). Must be >= 1.
64
+ * - `modifier` - Added to the total after all dice are summed. Can be negative.
65
+ */
37
66
  export type DiceSpec = Readonly<{
38
67
  count: number;
39
68
  sides: number;
40
69
  modifier: number;
41
70
  }>;
42
71
 
43
- /** Parse dice notation string into a spec */
72
+ /**
73
+ * Parse a dice notation string into a DiceSpec.
74
+ * Pure function. Throws if the notation is invalid.
75
+ *
76
+ * @param notation - Dice notation string (e.g., "2d6+3", "1d20", "3d8-1").
77
+ * @returns Parsed DiceSpec with count, sides, and modifier.
78
+ * @throws Error if notation doesn't match the `NdS` or `NdS+M` / `NdS-M` format.
79
+ */
44
80
  export function parseDice(notation: string): DiceSpec {
45
81
  const match = notation.match(/^(\d+)d(\d+)([+-]\d+)?$/);
46
82
  if (!match) {
@@ -55,7 +91,22 @@ export function parseDice(notation: string): DiceSpec {
55
91
  };
56
92
  }
57
93
 
58
- /** Roll dice. Returns [result, newRngState] */
94
+ /**
95
+ * Roll dice deterministically using the PRNG. Pure function — returns the result
96
+ * and a new PRNGState without modifying the original.
97
+ *
98
+ * Accepts either a DiceSpec object or a dice notation string (e.g., "2d6+3").
99
+ * Each die is rolled individually using {@link randomInt}, then summed with the modifier.
100
+ *
101
+ * @param rng - Current PRNG state.
102
+ * @param spec - A DiceSpec or dice notation string (e.g., "1d20", "2d6+3").
103
+ * @returns A tuple of [total roll result, new PRNGState].
104
+ *
105
+ * @example
106
+ * const rng = seed(42);
107
+ * const [damage, rng2] = rollDice(rng, "2d6+3");
108
+ * const [toHit, rng3] = rollDice(rng2, "1d20");
109
+ */
59
110
  export function rollDice(
60
111
  rng: PRNGState,
61
112
  spec: DiceSpec | string,
@@ -73,7 +124,15 @@ export function rollDice(
73
124
  return [total, current];
74
125
  }
75
126
 
76
- /** Random integer in [min, max] inclusive */
127
+ /**
128
+ * Generate a random integer in the range [min, max] (inclusive on both ends).
129
+ * Pure function — returns the value and a new PRNGState.
130
+ *
131
+ * @param rng - Current PRNG state.
132
+ * @param min - Minimum value (inclusive).
133
+ * @param max - Maximum value (inclusive). Must be >= min.
134
+ * @returns A tuple of [random integer, new PRNGState].
135
+ */
77
136
  export function randomInt(
78
137
  rng: PRNGState,
79
138
  min: number,
@@ -85,14 +144,26 @@ export function randomInt(
85
144
  return [value, next];
86
145
  }
87
146
 
88
- /** Random float in [0, 1) */
147
+ /**
148
+ * Generate a random float in the range [0, 1) (inclusive of 0, exclusive of 1).
149
+ * Pure function — returns the value and a new PRNGState.
150
+ *
151
+ * @param rng - Current PRNG state.
152
+ * @returns A tuple of [random float in [0,1), new PRNGState].
153
+ */
89
154
  export function randomFloat(rng: PRNGState): [number, PRNGState] {
90
155
  const next = advance(rng);
91
156
  const result = (xoshiro128ss(rng) >>> 0) / 4294967296;
92
157
  return [result, next];
93
158
  }
94
159
 
95
- /** Pick one random element */
160
+ /**
161
+ * Pick one random element from an array. Pure function — does not modify the array.
162
+ *
163
+ * @param rng - Current PRNG state.
164
+ * @param items - Non-empty array to pick from.
165
+ * @returns A tuple of [randomly selected item, new PRNGState].
166
+ */
96
167
  export function randomPick<T>(
97
168
  rng: PRNGState,
98
169
  items: readonly T[],
@@ -101,7 +172,14 @@ export function randomPick<T>(
101
172
  return [items[index], next];
102
173
  }
103
174
 
104
- /** Shuffle array (Fisher-Yates). Returns new array. */
175
+ /**
176
+ * Shuffle an array using Fisher-Yates algorithm. Pure function — returns a new
177
+ * shuffled array without modifying the original.
178
+ *
179
+ * @param rng - Current PRNG state.
180
+ * @param items - Array to shuffle.
181
+ * @returns A tuple of [new shuffled array, new PRNGState].
182
+ */
105
183
  export function shuffle<T>(
106
184
  rng: PRNGState,
107
185
  items: readonly T[],