@dryanovski/gamefoo 0.2.3 → 0.2.5

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 (84) hide show
  1. package/dist/core/animate.js +147 -0
  2. package/dist/core/asset.js +74 -0
  3. package/dist/core/behaviour.js +88 -0
  4. package/dist/core/behaviours/collidable.js +186 -0
  5. package/dist/core/behaviours/control.js +75 -0
  6. package/dist/core/behaviours/healtkit.js +153 -0
  7. package/dist/core/behaviours/sprite_render.js +193 -0
  8. package/dist/core/camera.js +134 -0
  9. package/dist/core/engine.d.ts +1 -1
  10. package/dist/core/engine.d.ts.map +1 -1
  11. package/dist/core/engine.js +527 -0
  12. package/dist/core/fonts/font_bitmap.js +205 -0
  13. package/dist/core/fonts/font_bitmap_prebuild.js +137 -0
  14. package/dist/core/fonts/internal/font_3x5.js +169 -0
  15. package/dist/core/fonts/internal/font_4x6.js +171 -0
  16. package/dist/core/fonts/internal/font_5x5.js +129 -0
  17. package/dist/core/fonts/internal/font_6x8.js +171 -0
  18. package/dist/core/fonts/internal/font_8x13.js +171 -0
  19. package/dist/core/fonts/internal/font_8x8.js +171 -0
  20. package/dist/core/game_object_register.js +134 -0
  21. package/dist/core/input.js +170 -0
  22. package/dist/core/sprite.js +222 -0
  23. package/dist/core/utils/perlin_noise.js +183 -0
  24. package/dist/core/world.js +304 -0
  25. package/dist/debug/monitor.js +47 -0
  26. package/dist/decorators/index.js +1 -0
  27. package/dist/decorators/log.js +42 -0
  28. package/dist/entities/dynamic_entity.js +99 -0
  29. package/dist/entities/entity.js +283 -0
  30. package/dist/entities/player.js +93 -0
  31. package/dist/entities/text.js +62 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +47 -29
  35. package/dist/subsystems/camera_system.d.ts +2 -2
  36. package/dist/subsystems/camera_system.d.ts.map +1 -1
  37. package/dist/subsystems/camera_system.js +41 -0
  38. package/dist/subsystems/collision_system.js +17 -0
  39. package/dist/subsystems/monitor_system.js +20 -0
  40. package/dist/subsystems/object_system.d.ts +1 -1
  41. package/dist/subsystems/object_system.d.ts.map +1 -1
  42. package/dist/subsystems/object_system.js +29 -0
  43. package/dist/subsystems/types.d.ts +1 -1
  44. package/dist/subsystems/types.d.ts.map +1 -1
  45. package/dist/subsystems/types.js +0 -0
  46. package/dist/types.js +10 -0
  47. package/package.json +17 -6
  48. package/src/core/animate.ts +159 -0
  49. package/src/core/asset.ts +76 -0
  50. package/src/core/behaviour.ts +145 -0
  51. package/src/core/behaviours/collidable.ts +296 -0
  52. package/src/core/behaviours/control.ts +80 -0
  53. package/src/core/behaviours/healtkit.ts +166 -0
  54. package/src/core/behaviours/sprite_render.ts +216 -0
  55. package/src/core/camera.ts +145 -0
  56. package/src/core/engine.ts +607 -0
  57. package/src/core/fonts/font_bitmap.ts +232 -0
  58. package/src/core/fonts/font_bitmap_prebuild.ts +141 -0
  59. package/src/core/fonts/internal/font_3x5.ts +178 -0
  60. package/src/core/fonts/internal/font_4x6.ts +180 -0
  61. package/src/core/fonts/internal/font_5x5.ts +137 -0
  62. package/src/core/fonts/internal/font_6x8.ts +180 -0
  63. package/src/core/fonts/internal/font_8x13.ts +180 -0
  64. package/src/core/fonts/internal/font_8x8.ts +180 -0
  65. package/src/core/game_object_register.ts +146 -0
  66. package/src/core/input.ts +182 -0
  67. package/src/core/sprite.ts +339 -0
  68. package/src/core/utils/perlin_noise.ts +196 -0
  69. package/src/core/world.ts +331 -0
  70. package/src/debug/monitor.ts +60 -0
  71. package/src/decorators/index.ts +1 -0
  72. package/src/decorators/log.ts +45 -0
  73. package/src/entities/dynamic_entity.ts +106 -0
  74. package/src/entities/entity.ts +322 -0
  75. package/src/entities/player.ts +99 -0
  76. package/src/entities/text.ts +72 -0
  77. package/src/index.ts +51 -0
  78. package/src/subsystems/camera_system.ts +52 -0
  79. package/src/subsystems/collision_system.ts +21 -0
  80. package/src/subsystems/monitor_system.ts +26 -0
  81. package/src/subsystems/object_system.ts +37 -0
  82. package/src/subsystems/types.ts +46 -0
  83. package/src/types.ts +178 -0
  84. package/dist/index.js.map +0 -9
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Central registry that stores and manages all non-player
3
+ * {@link GameObject | game objects} within the engine.
4
+ *
5
+ * Objects are keyed by their `id` property, so each ID must be unique.
6
+ * The {@link Engine} delegates per-frame `update` and `render` calls to
7
+ * this register.
8
+ *
9
+ * @category Core
10
+ * @since 0.1.0
11
+ *
12
+ * @example Registering and retrieving objects
13
+ * ```ts
14
+ * const register = new GameObjectRegister();
15
+ *
16
+ * register.register(tree);
17
+ * register.register(rock);
18
+ *
19
+ * const found = register.get("tree"); // Entity | undefined
20
+ * console.log(register.has("rock")); // true
21
+ * ```
22
+ *
23
+ * @example Bulk update / render
24
+ * ```ts
25
+ * // Called internally by Engine each frame:
26
+ * register.updateAll(deltaTime);
27
+ * register.renderAll(ctx);
28
+ * ```
29
+ *
30
+ * @see {@link Engine.attachObjects} — convenience method that delegates here
31
+ */
32
+ export default class GameObjectRegister {
33
+ /**
34
+ * Internal map from entity ID to its {@link GameObject} instance.
35
+ */
36
+ objects = new Map();
37
+ _cache = null;
38
+ /**
39
+ * Adds a game object to the registry.
40
+ *
41
+ * If an object with the same `id` already exists it will be
42
+ * silently overwritten.
43
+ *
44
+ * @param object - The game object to register.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * register.register(new Crate("crate_1", 200, 150, 32, 32));
49
+ * ```
50
+ */
51
+ register(object) {
52
+ this.objects.set(object.id, object);
53
+ this._cache = null;
54
+ }
55
+ /**
56
+ * Retrieves a registered object by its unique ID.
57
+ *
58
+ * @param id - The ID of the object to find.
59
+ * @returns The matching {@link GameObject}, or `undefined` if not found.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const crate = register.get("crate_1");
64
+ * if (crate) crate.x += 10;
65
+ * ```
66
+ */
67
+ get(id) {
68
+ return this.objects.get(id);
69
+ }
70
+ /**
71
+ * Checks whether an object with the given ID is registered.
72
+ *
73
+ * @param id - The ID to look up.
74
+ * @returns `true` if the registry contains the object.
75
+ */
76
+ has(id) {
77
+ return this.objects.has(id);
78
+ }
79
+ /**
80
+ * Returns all registered objects as an array.
81
+ *
82
+ * Make sure to also cache the objects
83
+ *
84
+ * @since 0.2.0
85
+ *
86
+ * @returns An array of all {@link GameObject} instances in the registry.
87
+ */
88
+ toArray() {
89
+ if (!this._cache) {
90
+ this._cache = Array.from(this.objects.values());
91
+ }
92
+ return this._cache;
93
+ }
94
+ /**
95
+ * Returns all registered objects that pass the supplied filter.
96
+ *
97
+ * @param filter - (optional) A predicate function. Return `true` to include the
98
+ * object in the result.
99
+ * @returns An array of matching {@link GameObject} instances.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const enemies = register.getAll(() => true);
104
+ * ```
105
+ */
106
+ getAll(filter) {
107
+ if (typeof filter === "function") {
108
+ return this.toArray().filter(filter);
109
+ }
110
+ return this.toArray();
111
+ }
112
+ /**
113
+ * Calls {@link GameObject.update | update(deltaTime)} on every
114
+ * registered object.
115
+ *
116
+ * @param deltaTime - Seconds elapsed since the previous frame.
117
+ */
118
+ updateAll(deltaTime) {
119
+ for (const obj of this.getAll()) {
120
+ obj.update(deltaTime);
121
+ }
122
+ }
123
+ /**
124
+ * Calls {@link GameObject.render | render(ctx)} on every registered
125
+ * object.
126
+ *
127
+ * @param ctx - The canvas 2-D rendering context.
128
+ */
129
+ renderAll(ctx) {
130
+ for (const obj of this.getAll()) {
131
+ obj.render(ctx);
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Unified keyboard and mouse input manager.
3
+ *
4
+ * `Input` listens to `keydown`, `keyup`, `mousedown`, `mouseup`, and
5
+ * `mousemove` events on the `window` and exposes a polling API so game
6
+ * logic can query the current state at any point during a frame rather
7
+ * than relying on event callbacks.
8
+ *
9
+ * All keyboard keys are stored **lowercased** for case-insensitive
10
+ * look-ups.
11
+ *
12
+ * @category Core
13
+ * @since 0.1.0
14
+ *
15
+ * @example Polling keys
16
+ * ```ts
17
+ * const input = new Input();
18
+ *
19
+ * function update() {
20
+ * if (input.isKeyDown("w")) {
21
+ * player.y -= speed;
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ * @example Checking mouse state
27
+ * ```ts
28
+ * const input = new Input();
29
+ *
30
+ * if (input.isMouseButtonDown(0)) { // left-click
31
+ * const { x, y } = input.getMousePosition();
32
+ * shoot(x, y);
33
+ * }
34
+ * ```
35
+ *
36
+ * @see {@link Control} — behaviour that consumes `Input` for player movement
37
+ */
38
+ export default class Input {
39
+ /**
40
+ * Set of currently-pressed keyboard keys (lowercased).
41
+ *
42
+ * Populated on `keydown`, cleared on `keyup`.
43
+ */
44
+ keys = new Set();
45
+ /**
46
+ * Set of currently-pressed mouse button indices.
47
+ *
48
+ * Standard mapping: `0` = left, `1` = middle, `2` = right.
49
+ */
50
+ mouseButtons = new Set();
51
+ /**
52
+ * Last known mouse position in **client** (viewport) coordinates.
53
+ *
54
+ * @defaultValue `{ x: 0, y: 0 }`
55
+ */
56
+ mousePosition = { x: 0, y: 0 };
57
+ /**
58
+ * Creates a new `Input` instance and attaches global event listeners
59
+ * to the `window`.
60
+ *
61
+ * @remarks
62
+ * Only one `Input` instance should exist at a time to avoid
63
+ * duplicate listeners. If you need to tear down, call {@link Input.reset}
64
+ * to clear tracked state.
65
+ */
66
+ constructor() {
67
+ window.addEventListener("keydown", (e) => {
68
+ this.keys.add(e.key.toLowerCase());
69
+ });
70
+ window.addEventListener("keyup", (e) => {
71
+ this.keys.delete(e.key.toLowerCase());
72
+ });
73
+ window.addEventListener("mousedown", (e) => {
74
+ this.mouseButtons.add(e.button);
75
+ });
76
+ window.addEventListener("mouseup", (e) => {
77
+ this.mouseButtons.delete(e.button);
78
+ });
79
+ window.addEventListener("mousemove", (e) => {
80
+ this.mousePosition.x = e.clientX;
81
+ this.mousePosition.y = e.clientY;
82
+ });
83
+ }
84
+ /**
85
+ * Checks whether a specific key is currently held down.
86
+ *
87
+ * @param key - The key name to check (case-insensitive).
88
+ * Uses the standard {@link KeyboardEvent.key} values (e.g. `"a"`,
89
+ * `"ArrowLeft"`, `"Shift"`).
90
+ * @returns `true` if the key is currently pressed.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * if (input.isKeyDown("space")) {
95
+ * player.jump();
96
+ * }
97
+ * ```
98
+ */
99
+ isKeyDown(key) {
100
+ return this.keys.has(key.toLowerCase());
101
+ }
102
+ /**
103
+ * Returns a snapshot of all keys that are currently held down.
104
+ *
105
+ * The returned `Set` is a **copy** — mutating it does not affect
106
+ * the internal state.
107
+ *
108
+ * @returns A new `Set<string>` of pressed key names (lowercased).
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const pressed = input.getPressedKeys();
113
+ * console.log([...pressed]); // e.g. ["w", "shift"]
114
+ * ```
115
+ */
116
+ getPressedKeys() {
117
+ return new Set(this.keys);
118
+ }
119
+ /**
120
+ * Checks whether a specific mouse button is currently held down.
121
+ *
122
+ * @param button - The mouse button index (`0` = left, `1` = middle,
123
+ * `2` = right).
124
+ * @returns `true` if the button is currently pressed.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * if (input.isMouseButtonDown(2)) {
129
+ * openContextMenu();
130
+ * }
131
+ * ```
132
+ */
133
+ isMouseButtonDown(button) {
134
+ return this.mouseButtons.has(button);
135
+ }
136
+ /**
137
+ * Returns the last known mouse position in client (viewport)
138
+ * coordinates.
139
+ *
140
+ * The returned object is a **copy** — mutating it does not affect
141
+ * the internal state.
142
+ *
143
+ * @returns An `{ x, y }` object with the mouse coordinates.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const pos = input.getMousePosition();
148
+ * ctx.fillRect(pos.x, pos.y, 4, 4); // draw cursor dot
149
+ * ```
150
+ */
151
+ getMousePosition() {
152
+ return { ...this.mousePosition };
153
+ }
154
+ /**
155
+ * Clears all tracked key and mouse-button state.
156
+ *
157
+ * Useful when pausing the game or switching scenes to prevent stale
158
+ * input from carrying over.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * engine.pause();
163
+ * input.reset();
164
+ * ```
165
+ */
166
+ reset() {
167
+ this.keys.clear();
168
+ this.mouseButtons.clear();
169
+ }
170
+ }
@@ -0,0 +1,222 @@
1
+ import Asset from "./asset";
2
+ /**
3
+ * Metadata wrapper around an {@link HTMLImageElement} that describes how
4
+ * it is sliced into a uniform grid of frames and what named animations
5
+ * are available.
6
+ *
7
+ * `Sprite` does **not** handle rendering itself — use
8
+ * {@link SpriteRender} to play animations on an entity.
9
+ *
10
+ * @category Core
11
+ * @since 0.1.0
12
+ *
13
+ * @example Loading and creating a sprite
14
+ * ```ts
15
+ * import { Asset } from "gamefoo";
16
+ *
17
+ * const image = await Asset.load("hero.png");
18
+ * const sprite = new Sprite(image, 32, 32, {
19
+ * idle: { frames: [0, 1], duration: 0.25, loop: true },
20
+ * run: { frames: [2, 3, 4, 5], duration: 0.1, loop: true },
21
+ * });
22
+ * ```
23
+ *
24
+ * @example Querying frame coordinates
25
+ * ```ts
26
+ * const rect = sprite.getFrameRect(5);
27
+ * ctx.drawImage(
28
+ * sprite.image,
29
+ * rect.x, rect.y, rect.width, rect.height,
30
+ * destX, destY, rect.width, rect.height,
31
+ * );
32
+ * ```
33
+ *
34
+ * @see {@link SpriteRender} — behaviour that plays sprite animations
35
+ * @see {@link Asset} — image loading utility
36
+ */
37
+ export default class Sprite {
38
+ /** The underlying image element containing the full spritesheet. */
39
+ image;
40
+ /** Width of a single frame cell in pixels. */
41
+ width;
42
+ /** Height of a single frame cell in pixels. */
43
+ height;
44
+ /**
45
+ * Number of frame columns in the spritesheet, computed as
46
+ * `Math.floor(image.width / width)`.
47
+ */
48
+ columns;
49
+ /**
50
+ * Number of frame rows in the spritesheet, computed as
51
+ * `Math.floor(image.height / height)`.
52
+ */
53
+ rows;
54
+ /**
55
+ * Named animation definitions keyed by animation name.
56
+ *
57
+ * Populated from the optional `animations` parameter passed to the
58
+ * constructor.
59
+ */
60
+ animations;
61
+ /**
62
+ * @since 0.2.0
63
+ */
64
+ frames;
65
+ /**
66
+ * Creates a new spritesheet descriptor.
67
+ *
68
+ * @param image - A fully-loaded `HTMLImageElement` containing the
69
+ * spritesheet texture.
70
+ * @param width - Width of each individual frame in pixels.
71
+ * @param height - Height of each individual frame in pixels.
72
+ * @param animations - Optional map of named animation definitions.
73
+ * Keys are animation names (e.g. `"idle"`, `"run"`).
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const sprite = new Sprite(img, 64, 64, {
78
+ * idle: { frames: [0], duration: 1, loop: false },
79
+ * });
80
+ * ```
81
+ */
82
+ constructor(image, width, height, animations) {
83
+ this.image = image;
84
+ this.width = width;
85
+ this.height = height;
86
+ this.columns = Math.floor(image.width / width);
87
+ this.rows = Math.floor(image.height / height);
88
+ this.frames = Sprite.generateGridFrames(image, {
89
+ frameWidth: width,
90
+ frameHeight: height,
91
+ });
92
+ this.animations = new Map(Object.entries(animations || {}));
93
+ }
94
+ /**
95
+ * Alternative constructor for spritesheets that are already sliced into a
96
+ * uniform grid of frames.
97
+ *
98
+ * @since 0.2.0
99
+ *
100
+ * @param image - A fully-loaded `HTMLImageElement` containing the
101
+ * spritesheet texture.
102
+ * @param config - Configuration options for slicing the image into a
103
+ * grid of frames.
104
+ * @param animations - Optional map of named animation definitions.
105
+ * Keys are animation names (e.g. `"idle"`, `"run"`).
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const sprite = Sprite.fromGrid(img, {
110
+ * frameWidth: 64,
111
+ *
112
+ * frameHeight: 64,
113
+ * offsetX: 0,
114
+ * offsetY: 0,
115
+ * spacingX: 0,
116
+ * spacingY: 0,
117
+ * count: 16,
118
+ * }, {
119
+ * idle: { frames: [0, 1], duration: 0.25, loop: true },
120
+ * run: { frames: [2, 3, 4, 5], duration: 0.1, loop: true },
121
+ * });
122
+ * ```
123
+ */
124
+ static fromGrid(image, config, animations) {
125
+ const sprite = Object.create(Sprite.prototype);
126
+ sprite.image = image;
127
+ sprite.frames = Sprite.generateGridFrames(image, config);
128
+ sprite.animations = new Map(Object.entries(animations || {}));
129
+ return sprite;
130
+ }
131
+ /**
132
+ * Helper method to compute frame rectangles for a spritesheet sliced into a
133
+ * uniform grid.
134
+ *
135
+ * @since 0.2.0
136
+ *
137
+ * @param image - A fully-loaded `HTMLImageElement` containing the
138
+ * spritesheet texture.
139
+ * @param config - Configuration options for slicing the image into a grid of
140
+ * frames.
141
+ *
142
+ * @returns A map of frame indices to their corresponding source rectangles.
143
+ * Frame indices are zero-based and laid out left-to-right, top-to-bottom.
144
+ * The source rectangles are in pixel coordinates relative to the top-left corner
145
+ * of the source image.
146
+ */
147
+ static generateGridFrames(image, config) {
148
+ const { frameWidth, frameHeight, offsetX = 0, offsetY = 0, spacingX = 0, spacingY = 0 } = config;
149
+ const cols = Math.floor((image.width - offsetX + spacingX) / (frameWidth + spacingX));
150
+ const rows = Math.floor((image.height - offsetY + spacingY) / (frameHeight + spacingY));
151
+ const total = config.count ?? cols * rows;
152
+ const frames = new Map();
153
+ for (let i = 0; i < total; i++) {
154
+ const col = i % cols;
155
+ const row = Math.floor(i / cols);
156
+ frames.set(i, {
157
+ x: offsetX + col * (frameWidth + spacingX),
158
+ y: offsetY + row * (frameHeight + spacingY),
159
+ width: frameWidth,
160
+ height: frameHeight,
161
+ });
162
+ }
163
+ return frames;
164
+ }
165
+ static fromAtlas(image, regions, animations) {
166
+ const sprite = Object.create(Sprite.prototype);
167
+ sprite.image = image;
168
+ sprite.frames = new Map(Object.entries(regions));
169
+ sprite.animations = new Map(Object.entries(animations || {}));
170
+ return sprite;
171
+ }
172
+ static async fromAseprite(imagePath, jsonPath) {
173
+ const [image, response] = await Promise.all([Asset.load(imagePath), fetch(jsonPath)]);
174
+ const data = await response.json();
175
+ const regions = {};
176
+ for (const [name, entry] of Object.entries(data.frames)) {
177
+ const f = entry.frame;
178
+ regions[name] = { x: f.x, y: f.y, width: f.w, height: f.h };
179
+ }
180
+ const animations = {};
181
+ if (data.meta?.frameTags) {
182
+ for (const tag of data.meta.frameTags) {
183
+ const frameNames = [];
184
+ for (let i = tag.from; i <= tag.to; i++) {
185
+ const key = Object.keys(data.frames)[i];
186
+ if (key)
187
+ frameNames.push(key);
188
+ }
189
+ animations[tag.name] = {
190
+ frames: frameNames,
191
+ duration: (data.frames[frameNames[0]]?.duration ?? 100) / 1000,
192
+ loop: tag.direction !== "forward_once",
193
+ };
194
+ }
195
+ }
196
+ return Sprite.fromAtlas(image, regions, animations);
197
+ }
198
+ /**
199
+ * Computes the source rectangle for a given frame index within the
200
+ * spritesheet.
201
+ *
202
+ * Frame indices are zero-based and laid out left-to-right,
203
+ * top-to-bottom.
204
+ *
205
+ * @param frame - Zero-based frame index.
206
+ * @returns An `{ x, y, width, height }` rectangle in pixel coordinates
207
+ * relative to the top-left corner of the source image.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * // For a 4-column sheet, frame 5 → col 1, row 1
212
+ * const rect = sprite.getFrameRect(5);
213
+ * ```
214
+ */
215
+ getFrameRect(frame) {
216
+ const rect = this.frames.get(frame);
217
+ if (!rect) {
218
+ throw new Error(`Frame "${frame}" not found in sprite`);
219
+ }
220
+ return rect;
221
+ }
222
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Deterministic 2-D Perlin noise generator with fractal Brownian motion
3
+ * (fBm) support.
4
+ *
5
+ * Produces smooth, continuous noise suitable for terrain heightmaps,
6
+ * cloud textures, procedural vegetation placement, and other organic
7
+ * patterns. The output is fully reproducible for a given seed.
8
+ *
9
+ * The implementation uses a 256-entry permutation table shuffled by a
10
+ * seeded linear congruential generator (LCG) and Ken Perlin's improved
11
+ * 5th-order fade curve \( 6t^5 - 15t^4 + 10t^3 \).
12
+ *
13
+ * @category Utilities
14
+ * @since 0.1.0
15
+ *
16
+ * @example Basic noise sampling
17
+ * ```ts
18
+ * import { PerlinNoise } from "gamefoo";
19
+ *
20
+ * const noise = new PerlinNoise(42);
21
+ * const value = noise.noise2d(1.5, 2.3); // returns a value in [-1, 1]
22
+ * ```
23
+ *
24
+ * @example Generating a heightmap with fBm
25
+ * ```ts
26
+ * const noise = new PerlinNoise(12345);
27
+ * const map: number[][] = [];
28
+ *
29
+ * for (let y = 0; y < 128; y++) {
30
+ * map[y] = [];
31
+ * for (let x = 0; x < 128; x++) {
32
+ * map[y][x] = noise.fbm(x * 0.05, y * 0.05, 6, 2, 0.5);
33
+ * }
34
+ * }
35
+ * ```
36
+ *
37
+ * @example Seeded reproducibility
38
+ * ```ts
39
+ * const a = new PerlinNoise(7);
40
+ * const b = new PerlinNoise(7);
41
+ * console.log(a.noise2d(3, 4) === b.noise2d(3, 4)); // true
42
+ * ```
43
+ */
44
+ export class PerlinNoise {
45
+ /**
46
+ * Doubled permutation table (512 entries) used for gradient hashing.
47
+ * Built from a 256-entry shuffle of `[0..255]` seeded by the LCG.
48
+ */
49
+ perm;
50
+ /**
51
+ * Creates a new noise generator with the given seed.
52
+ *
53
+ * @param seed - An integer seed for the internal LCG. Identical seeds
54
+ * produce identical noise fields.
55
+ *
56
+ * @defaultValue `0`
57
+ */
58
+ constructor(seed = 0) {
59
+ this.perm = new Uint8Array(512);
60
+ const p = new Uint8Array(256);
61
+ for (let i = 0; i < 256; i++)
62
+ p[i] = i;
63
+ let s = seed >>> 0;
64
+ for (let i = 255; i > 0; i--) {
65
+ s = (Math.imul(1664525, s) + 1013904223) >>> 0;
66
+ const j = s % (i + 1);
67
+ [p[i], p[j]] = [p[j], p[i]];
68
+ }
69
+ for (let i = 0; i < 512; i++)
70
+ this.perm[i] = p[i & 255];
71
+ }
72
+ /**
73
+ * Ken Perlin's improved 5th-order fade (smoothstep) curve:
74
+ * \( 6t^5 - 15t^4 + 10t^3 \).
75
+ *
76
+ * @param t - Value in `[0, 1]`.
77
+ * @returns Smoothed value in `[0, 1]`.
78
+ *
79
+ * @internal
80
+ */
81
+ fade(t) {
82
+ return t * t * t * (t * (t * 6 - 15) + 10);
83
+ }
84
+ /**
85
+ * Standard linear interpolation.
86
+ *
87
+ * @param a - Start value.
88
+ * @param b - End value.
89
+ * @param t - Interpolant in `[0, 1]`.
90
+ * @returns Interpolated value.
91
+ *
92
+ * @internal
93
+ */
94
+ lerp(a, b, t) {
95
+ return a + t * (b - a);
96
+ }
97
+ /**
98
+ * Computes a pseudo-random gradient dot product from a hash and
99
+ * 2-D offset.
100
+ *
101
+ * Uses the bottom 2 bits of `hash` to select one of four gradient
102
+ * directions.
103
+ *
104
+ * @param hash - Permutation table entry.
105
+ * @param x - X offset from the grid point.
106
+ * @param y - Y offset from the grid point.
107
+ * @returns Dot product of the gradient and offset vectors.
108
+ *
109
+ * @internal
110
+ */
111
+ grad(hash, x, y) {
112
+ const h = hash & 3;
113
+ const u = h < 2 ? x : y;
114
+ const v = h < 2 ? y : x;
115
+ return (h & 1 ? -u : u) + (h & 2 ? -v : v);
116
+ }
117
+ /**
118
+ * Samples 2-D Perlin noise at the given coordinates.
119
+ *
120
+ * @param x - X coordinate (any real number).
121
+ * @param y - Y coordinate (any real number).
122
+ * @returns A noise value in the range `[-1, 1]`.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const n = noise.noise2d(0.5, 0.5);
127
+ * // n is a smooth, deterministic value in [-1, 1]
128
+ * ```
129
+ */
130
+ noise2d(x, y) {
131
+ const xi = Math.floor(x) & 255;
132
+ const yi = Math.floor(y) & 255;
133
+ const xf = x - Math.floor(x);
134
+ const yf = y - Math.floor(y);
135
+ const u = this.fade(xf);
136
+ const v = this.fade(yf);
137
+ const aa = this.perm[this.perm[xi] + yi];
138
+ const ab = this.perm[this.perm[xi] + yi + 1];
139
+ const ba = this.perm[this.perm[xi + 1] + yi];
140
+ const bb = this.perm[this.perm[xi + 1] + yi + 1];
141
+ const x1 = this.lerp(this.grad(aa, xf, yf), this.grad(ba, xf - 1, yf), u);
142
+ const x2 = this.lerp(this.grad(ab, xf, yf - 1), this.grad(bb, xf - 1, yf - 1), u);
143
+ return this.lerp(x1, x2, v);
144
+ }
145
+ /**
146
+ * Computes fractal Brownian motion (fBm) by layering multiple
147
+ * octaves of {@link PerlinNoise.noise2d} with increasing frequency
148
+ * and decreasing amplitude.
149
+ *
150
+ * The result is normalised to `[-1, 1]`.
151
+ *
152
+ * @param x - X coordinate.
153
+ * @param y - Y coordinate.
154
+ * @param octaves - Number of noise layers to sum.
155
+ * @param lacunarity - Frequency multiplier per octave.
156
+ * @param persistence - Amplitude multiplier per octave (controls
157
+ * roughness).
158
+ * @returns A noise value in `[-1, 1]`.
159
+ *
160
+ * @defaultValue octaves = `4`
161
+ * @defaultValue lacunarity = `2`
162
+ * @defaultValue persistence = `0.5`
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // 6 octaves for high detail:
167
+ * const height = noise.fbm(x * 0.01, y * 0.01, 6, 2.0, 0.5);
168
+ * ```
169
+ */
170
+ fbm(x, y, octaves = 4, lacunarity = 2, persistence = 0.5) {
171
+ let value = 0;
172
+ let amplitude = 1;
173
+ let frequency = 1;
174
+ let max = 0;
175
+ for (let i = 0; i < octaves; i++) {
176
+ value += this.noise2d(x * frequency, y * frequency) * amplitude;
177
+ max += amplitude;
178
+ amplitude *= persistence;
179
+ frequency *= lacunarity;
180
+ }
181
+ return value / max;
182
+ }
183
+ }