@arcane-engine/runtime 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,21 +1,60 @@
1
1
  import type { Vec2 } from "../state/types.ts";
2
2
 
3
+ /**
4
+ * A grid abstraction for pathfinding.
5
+ *
6
+ * Provides dimensions and callbacks for walkability and movement cost.
7
+ * Tiles are addressed by integer (x, y) coordinates where (0,0) is the top-left.
8
+ */
3
9
  export type PathGrid = {
10
+ /** Grid width in tiles. Must be > 0. */
4
11
  width: number;
12
+ /** Grid height in tiles. Must be > 0. */
5
13
  height: number;
14
+ /**
15
+ * Returns whether the tile at (x, y) can be traversed.
16
+ * @param x - Tile X coordinate, 0..width-1.
17
+ * @param y - Tile Y coordinate, 0..height-1.
18
+ * @returns True if the tile is walkable.
19
+ */
6
20
  isWalkable: (x: number, y: number) => boolean;
21
+ /**
22
+ * Optional movement cost for entering the tile at (x, y).
23
+ * If omitted, cardinal moves cost 1 and diagonal moves cost sqrt(2).
24
+ * @param x - Tile X coordinate.
25
+ * @param y - Tile Y coordinate.
26
+ * @returns Movement cost. Must be > 0. Higher values = harder to traverse.
27
+ */
7
28
  cost?: (x: number, y: number) => number;
8
29
  };
9
30
 
31
+ /**
32
+ * Options for {@link findPath}.
33
+ */
10
34
  export type PathOptions = {
35
+ /** Allow diagonal movement (8-directional). Default: false (4-directional). */
11
36
  diagonal?: boolean;
37
+ /** Maximum A* iterations before giving up. Default: 10000. Prevents runaway on large grids. */
12
38
  maxIterations?: number;
39
+ /**
40
+ * Heuristic function for distance estimation.
41
+ * - `"manhattan"` — sum of axis distances. Best for 4-directional movement. (Default)
42
+ * - `"euclidean"` — straight-line distance. Best for any-angle movement.
43
+ * - `"chebyshev"` — max of axis distances. Best for 8-directional movement.
44
+ */
13
45
  heuristic?: "manhattan" | "euclidean" | "chebyshev";
14
46
  };
15
47
 
48
+ /**
49
+ * Result returned by {@link findPath}.
50
+ */
16
51
  export type PathResult = {
52
+ /** Whether a path from start to goal was found. */
17
53
  found: boolean;
54
+ /** Ordered array of tile positions from start to goal (inclusive). Empty if not found. */
18
55
  path: Vec2[];
56
+ /** Total movement cost of the path. 0 if not found. */
19
57
  cost: number;
58
+ /** Number of tiles explored during the search. Useful for profiling. */
20
59
  explored: number;
21
60
  };
@@ -1,18 +1,51 @@
1
- /** Axis-Aligned Bounding Box */
1
+ /**
2
+ * Axis-Aligned Bounding Box for 2D collision detection.
3
+ * Defined by its top-left corner and dimensions.
4
+ *
5
+ * - `x` - Left edge position (world units).
6
+ * - `y` - Top edge position (world units).
7
+ * - `w` - Width. Must be >= 0.
8
+ * - `h` - Height. Must be >= 0.
9
+ */
2
10
  export type AABB = {
3
- x: number; // left edge
4
- y: number; // top edge
5
- w: number; // width
6
- h: number; // height
11
+ x: number;
12
+ y: number;
13
+ w: number;
14
+ h: number;
7
15
  };
8
16
 
9
- /** Check if two AABBs overlap */
17
+ /**
18
+ * Check if two AABBs overlap. Pure function.
19
+ * Uses the separating axis theorem — returns true if there is no gap between
20
+ * the boxes on either the X or Y axis.
21
+ *
22
+ * @param a - First bounding box.
23
+ * @param b - Second bounding box.
24
+ * @returns True if the boxes overlap (touching edges do not count as overlap).
25
+ *
26
+ * @example
27
+ * const player = { x: 10, y: 10, w: 16, h: 16 };
28
+ * const enemy = { x: 20, y: 10, w: 16, h: 16 };
29
+ * if (aabbOverlap(player, enemy)) {
30
+ * // Handle collision
31
+ * }
32
+ */
10
33
  export function aabbOverlap(a: AABB, b: AABB): boolean {
11
34
  return a.x < b.x + b.w && a.x + a.w > b.x &&
12
35
  a.y < b.y + b.h && a.y + a.h > b.y;
13
36
  }
14
37
 
15
- /** Check if a circle overlaps an AABB */
38
+ /**
39
+ * Check if a circle overlaps an AABB. Pure function.
40
+ * Finds the closest point on the AABB to the circle center
41
+ * and checks if it's within the radius.
42
+ *
43
+ * @param cx - Circle center X position.
44
+ * @param cy - Circle center Y position.
45
+ * @param radius - Circle radius. Must be >= 0.
46
+ * @param box - The AABB to test against.
47
+ * @returns True if the circle and AABB overlap (inclusive of touching).
48
+ */
16
49
  export function circleAABBOverlap(
17
50
  cx: number, cy: number, radius: number,
18
51
  box: AABB
@@ -24,7 +57,21 @@ export function circleAABBOverlap(
24
57
  return (dx * dx + dy * dy) <= (radius * radius);
25
58
  }
26
59
 
27
- /** Get collision normal for circle vs AABB. Returns null if no collision. */
60
+ /**
61
+ * Get the collision resolution normal for a circle vs AABB collision.
62
+ * Returns a unit normal vector pointing from the AABB toward the circle center,
63
+ * or null if there is no collision.
64
+ *
65
+ * When the circle center is inside the AABB, pushes out along the shortest axis
66
+ * relative to the box center.
67
+ *
68
+ * @param cx - Circle center X position.
69
+ * @param cy - Circle center Y position.
70
+ * @param radius - Circle radius. Must be >= 0.
71
+ * @param box - The AABB to resolve against.
72
+ * @returns Object with `nx` and `ny` (unit normal), or null if no collision.
73
+ * nx and ny are in the range [-1, 1] and form a unit vector.
74
+ */
28
75
  export function circleAABBResolve(
29
76
  cx: number, cy: number, radius: number,
30
77
  box: AABB
@@ -1,27 +1,55 @@
1
1
  import type { TextureId } from "./types.ts";
2
2
  import { drawSprite } from "./sprites.ts";
3
3
 
4
+ /**
5
+ * Opaque handle to a registered animation definition.
6
+ * Returned by {@link createAnimation}.
7
+ */
4
8
  export type AnimationId = number;
5
9
 
10
+ /** Internal definition of a sprite-sheet animation. */
6
11
  export type AnimationDef = {
12
+ /** Texture handle containing the sprite sheet. */
7
13
  textureId: TextureId;
14
+ /** Width of each animation frame in pixels. */
8
15
  frameW: number;
16
+ /** Height of each animation frame in pixels. */
9
17
  frameH: number;
18
+ /** Total number of frames in the animation. */
10
19
  frameCount: number;
20
+ /** Playback speed in frames per second. */
11
21
  fps: number;
22
+ /** If true, animation loops. If false, stops on last frame. */
12
23
  loop: boolean;
13
24
  };
14
25
 
26
+ /** State of a playing animation instance. Immutable -- update via {@link updateAnimation}. */
15
27
  export type AnimationState = {
28
+ /** Reference to the animation definition. */
16
29
  defId: AnimationId;
30
+ /** Total elapsed time in seconds since animation started. */
17
31
  elapsed: number;
32
+ /** Current frame index (0-based). */
18
33
  frame: number;
34
+ /** True if non-looping animation has reached its last frame. */
19
35
  finished: boolean;
20
36
  };
21
37
 
22
38
  const registry = new Map<number, AnimationDef>();
23
39
  let nextId = 1;
24
40
 
41
+ /**
42
+ * Register a sprite-sheet animation definition.
43
+ * Frames must be arranged in a single horizontal row in the texture.
44
+ *
45
+ * @param textureId - Texture handle of the sprite sheet (from loadTexture()).
46
+ * @param frameW - Width of each frame in pixels.
47
+ * @param frameH - Height of each frame in pixels.
48
+ * @param frameCount - Number of frames in the animation.
49
+ * @param fps - Playback speed in frames per second. Higher = faster.
50
+ * @param options - Optional settings. `loop`: whether to loop (default: true).
51
+ * @returns AnimationId handle for use with playAnimation().
52
+ */
25
53
  export function createAnimation(
26
54
  textureId: TextureId,
27
55
  frameW: number,
@@ -42,10 +70,24 @@ export function createAnimation(
42
70
  return id;
43
71
  }
44
72
 
73
+ /**
74
+ * Create a new animation playback state starting from frame 0.
75
+ *
76
+ * @param defId - AnimationId from createAnimation().
77
+ * @returns Fresh AnimationState at frame 0.
78
+ */
45
79
  export function playAnimation(defId: AnimationId): AnimationState {
46
80
  return { defId, elapsed: 0, frame: 0, finished: false };
47
81
  }
48
82
 
83
+ /**
84
+ * Advance an animation by a time delta. Returns a new immutable state.
85
+ * For looping animations, wraps around. For non-looping, stops at the last frame.
86
+ *
87
+ * @param anim - Current animation state.
88
+ * @param dt - Time delta in seconds (from getDeltaTime()).
89
+ * @returns Updated animation state.
90
+ */
49
91
  export function updateAnimation(
50
92
  anim: AnimationState,
51
93
  dt: number,
@@ -83,6 +125,13 @@ export function updateAnimation(
83
125
  };
84
126
  }
85
127
 
128
+ /**
129
+ * Get the UV sub-rectangle for the current animation frame.
130
+ * Used internally by drawAnimatedSprite; also useful for custom rendering.
131
+ *
132
+ * @param anim - Current animation state.
133
+ * @returns UV rect (0.0-1.0 normalized) for the current frame.
134
+ */
86
135
  export function getAnimationUV(
87
136
  anim: AnimationState,
88
137
  ): { x: number; y: number; w: number; h: number } {
@@ -97,6 +146,18 @@ export function getAnimationUV(
97
146
  };
98
147
  }
99
148
 
149
+ /**
150
+ * Draw an animated sprite at the given position using the current animation frame.
151
+ * Combines getAnimationUV() + drawSprite() for convenience.
152
+ * Must be called every frame. No-op if the animation definition is not found.
153
+ *
154
+ * @param anim - Current animation state (from playAnimation/updateAnimation).
155
+ * @param x - World X position (top-left corner).
156
+ * @param y - World Y position (top-left corner).
157
+ * @param w - Width in world units.
158
+ * @param h - Height in world units.
159
+ * @param options - Optional layer and tint overrides.
160
+ */
100
161
  export function drawAnimatedSprite(
101
162
  anim: AnimationState,
102
163
  x: number,
@@ -123,10 +184,22 @@ export function drawAnimatedSprite(
123
184
  });
124
185
  }
125
186
 
187
+ /**
188
+ * Reset an animation to frame 0 (restart from beginning).
189
+ *
190
+ * @param anim - Animation state to reset.
191
+ * @returns New animation state at frame 0, not finished.
192
+ */
126
193
  export function resetAnimation(anim: AnimationState): AnimationState {
127
194
  return { ...anim, elapsed: 0, frame: 0, finished: false };
128
195
  }
129
196
 
197
+ /**
198
+ * Stop an animation immediately by marking it as finished.
199
+ *
200
+ * @param anim - Animation state to stop.
201
+ * @returns New animation state with finished = true.
202
+ */
130
203
  export function stopAnimation(anim: AnimationState): AnimationState {
131
204
  return { ...anim, finished: true };
132
205
  }
@@ -1,8 +1,14 @@
1
- /** Opaque handle to a loaded sound. */
1
+ /**
2
+ * Opaque handle to a loaded sound. Returned by {@link loadSound}.
3
+ * A value of 0 means "no sound" (headless mode fallback).
4
+ */
2
5
  export type SoundId = number;
3
6
 
7
+ /** Options for {@link playSound}. */
4
8
  export type PlayOptions = {
9
+ /** Playback volume, 0.0 (silent) to 1.0 (full). Default: 1.0. */
5
10
  volume?: number;
11
+ /** If true, sound loops until stopped. Default: false. */
6
12
  loop?: boolean;
7
13
  };
8
14
 
@@ -11,9 +17,12 @@ const hasRenderOps =
11
17
  typeof (globalThis as any).Deno?.core?.ops?.op_load_sound === "function";
12
18
 
13
19
  /**
14
- * Load a sound file (WAV, OGG, MP3). Returns a sound handle.
15
- * Caches by path loading the same path twice returns the same handle.
20
+ * Load a sound file (WAV, OGG, MP3). Returns an opaque sound handle.
21
+ * Caches by path -- loading the same path twice returns the same handle.
16
22
  * Returns 0 in headless mode.
23
+ *
24
+ * @param path - File path to an audio file (relative to game entry file or absolute).
25
+ * @returns Sound handle for use with playSound(), stopSound().
17
26
  */
18
27
  export function loadSound(path: string): SoundId {
19
28
  if (!hasRenderOps) return 0;
@@ -21,8 +30,11 @@ export function loadSound(path: string): SoundId {
21
30
  }
22
31
 
23
32
  /**
24
- * Play a loaded sound.
33
+ * Play a loaded sound effect.
25
34
  * No-op in headless mode.
35
+ *
36
+ * @param id - Sound handle from loadSound().
37
+ * @param options - Volume and loop settings.
26
38
  */
27
39
  export function playSound(id: SoundId, options?: PlayOptions): void {
28
40
  if (!hasRenderOps) return;
@@ -32,9 +44,13 @@ export function playSound(id: SoundId, options?: PlayOptions): void {
32
44
  }
33
45
 
34
46
  /**
35
- * Convenience: load and play a sound file as looping music.
36
- * Returns the sound ID for later stopping.
47
+ * Load and play a sound file as looping background music.
48
+ * Convenience function combining loadSound() + playSound() with loop: true.
37
49
  * Returns 0 in headless mode.
50
+ *
51
+ * @param path - File path to an audio file.
52
+ * @param volume - Playback volume, 0.0-1.0. Default: 1.0.
53
+ * @returns Sound handle for later stopping with stopSound().
38
54
  */
39
55
  export function playMusic(path: string, volume: number = 1.0): SoundId {
40
56
  const id = loadSound(path);
@@ -43,8 +59,10 @@ export function playMusic(path: string, volume: number = 1.0): SoundId {
43
59
  }
44
60
 
45
61
  /**
46
- * Stop a specific sound.
62
+ * Stop a specific playing sound.
47
63
  * No-op in headless mode.
64
+ *
65
+ * @param id - Sound handle from loadSound() or playMusic().
48
66
  */
49
67
  export function stopSound(id: SoundId): void {
50
68
  if (!hasRenderOps) return;
@@ -52,7 +70,7 @@ export function stopSound(id: SoundId): void {
52
70
  }
53
71
 
54
72
  /**
55
- * Stop all playing sounds.
73
+ * Stop all currently playing sounds and music.
56
74
  * No-op in headless mode.
57
75
  */
58
76
  export function stopAll(): void {
@@ -61,8 +79,10 @@ export function stopAll(): void {
61
79
  }
62
80
 
63
81
  /**
64
- * Set the master volume (0.0 = mute, 1.0 = full).
82
+ * Set the master volume for all audio output.
65
83
  * No-op in headless mode.
84
+ *
85
+ * @param volume - Master volume level, 0.0 (mute) to 1.0 (full). Values outside this range are not clamped.
66
86
  */
67
87
  export function setVolume(volume: number): void {
68
88
  if (!hasRenderOps) return;
@@ -5,8 +5,21 @@ const hasRenderOps =
5
5
  typeof (globalThis as any).Deno?.core?.ops?.op_set_camera === "function";
6
6
 
7
7
  /**
8
- * Set the camera position and zoom.
8
+ * Set the camera position and zoom level.
9
+ * The camera determines which part of the world is visible on screen.
9
10
  * No-op in headless mode.
11
+ *
12
+ * @param x - Camera center X in world units.
13
+ * @param y - Camera center Y in world units.
14
+ * @param zoom - Zoom level. 1.0 = default, >1.0 = zoomed in, <1.0 = zoomed out. Default: 1.
15
+ *
16
+ * @example
17
+ * // Center camera on the player
18
+ * setCamera(player.x, player.y);
19
+ *
20
+ * @example
21
+ * // Zoomed-in camera
22
+ * setCamera(player.x, player.y, 2.0);
10
23
  */
11
24
  export function setCamera(x: number, y: number, zoom: number = 1): void {
12
25
  if (!hasRenderOps) return;
@@ -14,8 +27,10 @@ export function setCamera(x: number, y: number, zoom: number = 1): void {
14
27
  }
15
28
 
16
29
  /**
17
- * Get the current camera state.
18
- * Returns default values in headless mode.
30
+ * Get the current camera state (position and zoom).
31
+ * Returns `{ x: 0, y: 0, zoom: 1 }` in headless mode.
32
+ *
33
+ * @returns Current camera position and zoom level.
19
34
  */
20
35
  export function getCamera(): CameraState {
21
36
  if (!hasRenderOps) return { x: 0, y: 0, zoom: 1 };
@@ -24,7 +39,16 @@ export function getCamera(): CameraState {
24
39
  }
25
40
 
26
41
  /**
27
- * Center the camera on a target position.
42
+ * Center the camera on a target position. Convenience wrapper around {@link setCamera}.
43
+ * Call every frame to follow a moving target.
44
+ *
45
+ * @param targetX - Target X position in world units to center on.
46
+ * @param targetY - Target Y position in world units to center on.
47
+ * @param zoom - Zoom level. Default: 1.
48
+ *
49
+ * @example
50
+ * // Follow the player each frame
51
+ * followTarget(player.x, player.y);
28
52
  */
29
53
  export function followTarget(
30
54
  targetX: number,
@@ -10,9 +10,25 @@ const hasViewportOp =
10
10
  typeof (globalThis as any).Deno?.core?.ops?.op_get_viewport_size === "function";
11
11
 
12
12
  /**
13
- * Check if a key is currently held down.
14
- * Key names match web standards: "ArrowUp", "ArrowDown", "a", "Space", etc.
13
+ * Check if a key is currently held down (returns true every frame while held).
15
14
  * Returns false in headless mode.
15
+ *
16
+ * Key names follow web KeyboardEvent.key standards:
17
+ * - Arrow keys: `"ArrowUp"`, `"ArrowDown"`, `"ArrowLeft"`, `"ArrowRight"`
18
+ * - Letters: `"a"` - `"z"` (lowercase)
19
+ * - Digits: `"0"` - `"9"`
20
+ * - Function keys: `"F1"` - `"F12"`
21
+ * - Whitespace: `"Space"`, `"Tab"`, `"Enter"`
22
+ * - Modifiers: `"Shift"`, `"Control"`, `"Alt"`
23
+ * - Navigation: `"Escape"`, `"Backspace"`, `"Delete"`, `"Home"`, `"End"`, `"PageUp"`, `"PageDown"`
24
+ *
25
+ * @param key - Key name string (case-sensitive, web standard).
26
+ * @returns true if the key is currently held down, false otherwise.
27
+ *
28
+ * @example
29
+ * if (isKeyDown("ArrowRight")) {
30
+ * player.x += speed * dt;
31
+ * }
16
32
  */
17
33
  export function isKeyDown(key: string): boolean {
18
34
  if (!hasRenderOps) return false;
@@ -20,8 +36,17 @@ export function isKeyDown(key: string): boolean {
20
36
  }
21
37
 
22
38
  /**
23
- * Check if a key was pressed this frame (just went down).
39
+ * Check if a key was pressed this frame (transitioned from up to down).
40
+ * Unlike {@link isKeyDown}, this returns true only on the first frame the key is pressed.
24
41
  * Returns false in headless mode.
42
+ *
43
+ * Valid key names are the same as {@link isKeyDown}:
44
+ * `"ArrowUp"`, `"ArrowDown"`, `"ArrowLeft"`, `"ArrowRight"`, `"Space"`, `"Enter"`,
45
+ * `"Escape"`, `"Tab"`, `"Shift"`, `"Control"`, `"Alt"`, `"a"`-`"z"`, `"0"`-`"9"`, `"F1"`-`"F12"`,
46
+ * `"Backspace"`, `"Delete"`, `"Home"`, `"End"`, `"PageUp"`, `"PageDown"`.
47
+ *
48
+ * @param key - Key name string (case-sensitive, web standard).
49
+ * @returns true if the key was just pressed this frame, false otherwise.
25
50
  */
26
51
  export function isKeyPressed(key: string): boolean {
27
52
  if (!hasRenderOps) return false;
@@ -29,8 +54,12 @@ export function isKeyPressed(key: string): boolean {
29
54
  }
30
55
 
31
56
  /**
32
- * Get the current mouse position in window/screen coordinates.
33
- * Returns (0, 0) in headless mode.
57
+ * Get the current mouse position in screen/window coordinates (pixels).
58
+ * (0, 0) is the top-left corner of the window.
59
+ * Returns `{ x: 0, y: 0 }` in headless mode.
60
+ * Use {@link getMouseWorldPosition} for world-space coordinates.
61
+ *
62
+ * @returns Mouse position in screen pixels.
34
63
  */
35
64
  export function getMousePosition(): MousePosition {
36
65
  if (!hasRenderOps) return { x: 0, y: 0 };
@@ -40,7 +69,9 @@ export function getMousePosition(): MousePosition {
40
69
 
41
70
  /**
42
71
  * Get the current viewport size in pixels.
43
- * Returns [800, 600] in headless mode.
72
+ * Returns `{ width: 800, height: 600 }` in headless mode.
73
+ *
74
+ * @returns Viewport dimensions in pixels.
44
75
  */
45
76
  export function getViewportSize(): { width: number; height: number } {
46
77
  if (!hasViewportOp) return { width: 800, height: 600 };
@@ -50,8 +81,11 @@ export function getViewportSize(): { width: number; height: number } {
50
81
 
51
82
  /**
52
83
  * Convert screen/window coordinates to world coordinates using the current camera.
53
- * Screen coordinates: (0, 0) = top-left, (viewport_width, viewport_height) = bottom-right
54
- * World coordinates: transformed by camera position and zoom
84
+ * Accounts for camera position and zoom.
85
+ *
86
+ * @param screenX - X position in screen pixels (0 = left edge).
87
+ * @param screenY - Y position in screen pixels (0 = top edge).
88
+ * @returns Corresponding world-space position.
55
89
  */
56
90
  export function screenToWorld(screenX: number, screenY: number): MousePosition {
57
91
  const viewport = getViewportSize();
@@ -74,7 +108,9 @@ export function screenToWorld(screenX: number, screenY: number): MousePosition {
74
108
 
75
109
  /**
76
110
  * Get the mouse position in world coordinates (accounting for camera transform).
77
- * This is a convenience function that combines getMousePosition() and screenToWorld().
111
+ * Convenience function combining {@link getMousePosition} and {@link screenToWorld}.
112
+ *
113
+ * @returns Mouse position in world units.
78
114
  */
79
115
  export function getMouseWorldPosition(): MousePosition {
80
116
  const screenPos = getMousePosition();
@@ -3,13 +3,35 @@ const hasRenderOps =
3
3
  typeof (globalThis as any).Deno?.core?.ops?.op_set_ambient_light ===
4
4
  "function";
5
5
 
6
- /** Set the ambient light color (0-1 per channel). Default is (1,1,1) = full white. */
6
+ /**
7
+ * Set the ambient light color applied to all sprites.
8
+ * (1, 1, 1) = full white (no darkening, the default).
9
+ * (0, 0, 0) = complete darkness (only point lights visible).
10
+ * No-op in headless mode.
11
+ *
12
+ * @param r - Red channel, 0.0-1.0.
13
+ * @param g - Green channel, 0.0-1.0.
14
+ * @param b - Blue channel, 0.0-1.0.
15
+ */
7
16
  export function setAmbientLight(r: number, g: number, b: number): void {
8
17
  if (!hasRenderOps) return;
9
18
  (globalThis as any).Deno.core.ops.op_set_ambient_light(r, g, b);
10
19
  }
11
20
 
12
- /** Add a point light at world position (x,y) with the given radius, color, and intensity. */
21
+ /**
22
+ * Add a point light at a world position.
23
+ * Point lights illuminate sprites within their radius, blending with the ambient light.
24
+ * Must be called every frame (lights are cleared at frame start).
25
+ * No-op in headless mode.
26
+ *
27
+ * @param x - Light center X in world units.
28
+ * @param y - Light center Y in world units.
29
+ * @param radius - Light radius in world units. Falloff is smooth to the edge.
30
+ * @param r - Light color red channel, 0.0-1.0. Default: 1.
31
+ * @param g - Light color green channel, 0.0-1.0. Default: 1.
32
+ * @param b - Light color blue channel, 0.0-1.0. Default: 1.
33
+ * @param intensity - Light brightness multiplier, 0.0+. Default: 1.
34
+ */
13
35
  export function addPointLight(
14
36
  x: number,
15
37
  y: number,
@@ -31,7 +53,11 @@ export function addPointLight(
31
53
  );
32
54
  }
33
55
 
34
- /** Clear all point lights for this frame. Called automatically at frame start. */
56
+ /**
57
+ * Clear all point lights for this frame.
58
+ * Called automatically at frame start by the renderer; manual use is rarely needed.
59
+ * No-op in headless mode.
60
+ */
35
61
  export function clearLights(): void {
36
62
  if (!hasRenderOps) return;
37
63
  (globalThis as any).Deno.core.ops.op_clear_lights();
@@ -3,9 +3,19 @@ const hasRenderOps =
3
3
  typeof (globalThis as any).Deno?.core?.ops?.op_get_delta_time === "function";
4
4
 
5
5
  /**
6
- * Register a callback to be called each frame.
7
- * Only one callback can be active at a time (last one wins).
8
- * No-op in headless mode.
6
+ * Register a callback to be called every frame by the Arcane renderer.
7
+ * Only one callback can be active -- calling onFrame() again replaces the previous one.
8
+ * The callback is invoked by the Rust game loop (not by requestAnimationFrame).
9
+ * No-op in headless mode (the callback is stored but never invoked).
10
+ *
11
+ * @param callback - Function to call each frame. Use {@link getDeltaTime} inside for timing.
12
+ *
13
+ * @example
14
+ * onFrame(() => {
15
+ * const dt = getDeltaTime();
16
+ * player.x += speed * dt;
17
+ * drawSprite({ textureId: tex, x: player.x, y: player.y, w: 32, h: 32 });
18
+ * });
9
19
  */
10
20
  export function onFrame(callback: () => void): void {
11
21
  (globalThis as any).__frameCallback = callback;
@@ -13,7 +23,10 @@ export function onFrame(callback: () => void): void {
13
23
 
14
24
  /**
15
25
  * Get the time elapsed since the last frame, in seconds.
26
+ * Typical values: ~0.016 at 60fps, ~0.033 at 30fps.
16
27
  * Returns 0 in headless mode.
28
+ *
29
+ * @returns Delta time in seconds (fractional).
17
30
  */
18
31
  export function getDeltaTime(): number {
19
32
  if (!hasRenderOps) return 0;
@@ -7,7 +7,29 @@ const hasRenderOps =
7
7
 
8
8
  /**
9
9
  * Queue a sprite to be drawn this frame.
10
- * No-op in headless mode (Node or V8 test runner).
10
+ * Must be called every frame -- sprites are not persisted between frames.
11
+ * No-op in headless mode (safe to import in game logic).
12
+ *
13
+ * @param opts - Sprite rendering options (position, size, texture, layer, UV, tint).
14
+ *
15
+ * @example
16
+ * drawSprite({
17
+ * textureId: playerTex,
18
+ * x: player.x, y: player.y,
19
+ * w: 32, h: 32,
20
+ * layer: 1,
21
+ * });
22
+ *
23
+ * @example
24
+ * // Draw a tinted, atlas-based sprite
25
+ * drawSprite({
26
+ * textureId: atlas,
27
+ * x: 100, y: 200,
28
+ * w: 16, h: 16,
29
+ * uv: { x: 0.25, y: 0, w: 0.25, h: 0.5 },
30
+ * tint: { r: 1, g: 0.5, b: 0.5, a: 1 },
31
+ * layer: 5,
32
+ * });
11
33
  */
12
34
  export function drawSprite(opts: SpriteOptions): void {
13
35
  if (!hasRenderOps) return;
@@ -52,6 +74,7 @@ export function drawSprite(opts: SpriteOptions): void {
52
74
 
53
75
  /**
54
76
  * Clear all queued sprites for this frame.
77
+ * Normally not needed -- the renderer clears automatically at frame start.
55
78
  * No-op in headless mode.
56
79
  */
57
80
  export function clearSprites(): void {