Skip to content

Commit 87d89d8

Browse files
committed
feat: add top-down movement controller
1 parent 1e4a895 commit 87d89d8

9 files changed

Lines changed: 390 additions & 5 deletions

File tree

PROJECT_DESCRIPTION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ npm run build
3939
| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts` |
4040
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
4141
| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts` |
42-
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts` |
42+
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController`, `createTopDownController` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts`, `gameplay/topDownMovement.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts` |
4343
| Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
4444
| Data transforms & diffing | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
4545
| Graph traversal | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |
@@ -94,7 +94,7 @@ Consistency between runtime code, documentation, and TypeScript declarations kee
9494
- **Procedural:** 2D/3D Perlin, Worley noise, Wave Function Collapse tile synthesis.
9595
- **Spatial:** Quadtree, AABB helpers, SAT convex polygon collision.
9696
- **Performance utilities:** Debounce, throttle, LRU cache, memoize, request deduplication, virtual scrolling, weighted alias sampling, object pooling, Fisher–Yates shuffle.
97-
- **Gameplay systems:** Delta-time manager, fixed timestep loop, 2D camera with smoothing and shake, particle system with configurable emitters, sprite animation controller with frame events, tween system with easing and repeats, platformer physics helper with coyote time and jump buffering.
97+
- **Gameplay systems:** Delta-time manager, fixed timestep loop, 2D camera with smoothing and shake, particle system with configurable emitters, sprite animation controller with frame events, tween system with easing and repeats, platformer physics helper with coyote time and jump buffering, top-down movement controller with acceleration and drag.
9898
- **Search:** Fuzzy search + scoring, Trie-based autocomplete, binary search, Levenshtein distance.
9999
- **Data tools:** Diff operations (LCS), deep clone, groupBy, JSON diff/patch helpers.
100100
- **Graph:** BFS distance map, DFS traversal, topological sort.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ CDN usage:
2929
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
3030
| AI behaviours & crowds | `seek`, `flee`, `arrive`, `pursue`, `wander`, `updateBoids`, `BehaviorTree`, `rvoStep` | `ai/steering.ts`, `ai/boids.ts`, `ai/behaviorTree.ts`, `ai/rvo.ts` | `examples/steering.ts`, `examples/boids.ts`, `examples/rvo.ts` |
3131
| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts` |
32-
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts` |
32+
| Gameplay systems | `createDeltaTimeManager`, `createFixedTimestepLoop`, `createCamera2D`, `createParticleSystem`, `createSpriteAnimation`, `createTweenSystem`, `createPlatformerController`, `createTopDownController` | `util/deltaTime.ts`, `util/fixedTimestep.ts`, `gameplay/camera2D.ts`, `gameplay/particleSystem.ts`, `gameplay/spriteAnimation.ts`, `gameplay/tween.ts`, `gameplay/platformerPhysics.ts`, `gameplay/topDownMovement.ts` | `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts` |
3333
| Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
3434
| Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
3535
| Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |
@@ -53,7 +53,7 @@ npm run size # Enforce bundle size budget
5353
- Milestone 0.2 next targets crowd-flow integrations (RVO + flow fields) and behaviour-tree decorators for richer AI control.
5454
- Milestone 0.4 plans a procedural + gameplay systems toolkit (Wave Function Collapse, dungeon suite, L-systems, game loop, camera, particles, inventory, combat, save/load, and more).
5555

56-
Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.ts`, `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/search.ts`, `examples/graph.ts`, `examples/geometry.ts`, `examples/bresenham.ts`, `examples/visual.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts. The `examples` registry exported from `src/index.ts` provides a typed index you can traverse programmatically.
56+
Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.ts`, `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/camera2D.ts`, `examples/particleSystem.ts`, `examples/spriteAnimation.ts`, `examples/tween.ts`, `examples/platformerPhysics.ts`, `examples/topDownMovement.ts`, `examples/search.ts`, `examples/graph.ts`, `examples/geometry.ts`, `examples/bresenham.ts`, `examples/visual.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts. The `examples` registry exported from `src/index.ts` provides a typed index you can traverse programmatically.
5757

5858
## Contributing
5959
1. Fork the repository.

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
- [x] Sprite animation controller (frame timing, events)
5757
- [x] Tween/lerp utility for smooth interpolation
5858
- [x] Platformer physics helper (gravity, coyote time, jump buffering)
59-
- [ ] Top-down movement helper (8-direction)
59+
- [x] Top-down movement helper (8-direction)
6060
- [ ] Tile map renderer helpers (chunking, layering, collision tags)
6161
- [ ] Shadowcasting field-of-view utilities and minimap helpers
6262
- **Systems for gameplay loops**

docs/index.d.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,73 @@ export function createPlatformerController(
12961296
initialState?: PlatformerCharacterState
12971297
): PlatformerController;
12981298

1299+
/**
1300+
* Top-down movement options.
1301+
* Use for: configuring acceleration, deceleration, and max speed for 2D characters.
1302+
* Import: gameplay/topDownMovement.ts
1303+
*/
1304+
export interface TopDownMovementOptions {
1305+
acceleration: number;
1306+
deceleration: number;
1307+
maxSpeed: number;
1308+
drag?: number;
1309+
normalizeDiagonal?: boolean;
1310+
}
1311+
1312+
/**
1313+
* Top-down movement state snapshot.
1314+
* Use for: rendering and collision updates.
1315+
* Import: gameplay/topDownMovement.ts
1316+
*/
1317+
export interface TopDownState {
1318+
position: Vector2D;
1319+
velocity: Vector2D;
1320+
facing: Vector2D;
1321+
}
1322+
1323+
/**
1324+
* Top-down movement input axes.
1325+
* Use for: feeding directional input (-1..1).
1326+
* Import: gameplay/topDownMovement.ts
1327+
*/
1328+
export interface TopDownInput {
1329+
x: number;
1330+
y: number;
1331+
}
1332+
1333+
/**
1334+
* Top-down movement update payload.
1335+
* Use for: advancing the controller with delta time and current input.
1336+
* Import: gameplay/topDownMovement.ts
1337+
*/
1338+
export interface TopDownUpdateOptions {
1339+
delta: number;
1340+
input: TopDownInput;
1341+
}
1342+
1343+
/**
1344+
* Top-down movement controller API.
1345+
* Use for: updating state, resetting, and retuning movement parameters.
1346+
* Import: gameplay/topDownMovement.ts
1347+
*/
1348+
export interface TopDownController {
1349+
update(options: TopDownUpdateOptions): TopDownState;
1350+
getState(): TopDownState;
1351+
reset(state?: Partial<TopDownState>): void;
1352+
setOptions(options: Partial<TopDownMovementOptions>): void;
1353+
}
1354+
1355+
/**
1356+
* Creates a top-down movement controller with acceleration and damping.
1357+
* Use for: eight-direction movement in action or RPG games.
1358+
* Performance: O(1) per update.
1359+
* Import: gameplay/topDownMovement.ts
1360+
*/
1361+
export function createTopDownController(
1362+
options: TopDownMovementOptions,
1363+
initialState?: TopDownState
1364+
): TopDownController;
1365+
12991366
/**
13001367
* Least recently used cache.
13011368
* Use for: memoizing responses, data loaders, pagination caches.

examples/topDownMovement.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createTopDownController } from '../src/index.js';
2+
3+
const controller = createTopDownController({
4+
acceleration: 20,
5+
deceleration: 18,
6+
maxSpeed: 6,
7+
drag: 0.1,
8+
});
9+
10+
for (let frame = 0; frame < 10; frame += 1) {
11+
const angle = (frame / 10) * Math.PI * 2;
12+
const input = { x: Math.cos(angle), y: Math.sin(angle) };
13+
const state = controller.update({ delta: 1 / 30, input });
14+
console.log(
15+
`frame ${frame}: position=(${state.position.x.toFixed(2)}, ${state.position.y.toFixed(2)}) velocity=(${state.velocity.x.toFixed(2)}, ${state.velocity.y.toFixed(2)})`
16+
);
17+
}

src/gameplay/topDownMovement.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import type { Vector2D } from '../types.js';
2+
3+
export interface TopDownState {
4+
position: Vector2D;
5+
velocity: Vector2D;
6+
facing: Vector2D;
7+
}
8+
9+
export interface TopDownInput {
10+
x: number;
11+
y: number;
12+
}
13+
14+
export interface TopDownUpdateOptions {
15+
delta: number;
16+
input: TopDownInput;
17+
}
18+
19+
export interface TopDownMovementOptions {
20+
acceleration: number;
21+
deceleration: number;
22+
maxSpeed: number;
23+
drag?: number;
24+
normalizeDiagonal?: boolean;
25+
}
26+
27+
export interface TopDownController {
28+
update(options: TopDownUpdateOptions): TopDownState;
29+
getState(): TopDownState;
30+
reset(state?: Partial<TopDownState>): void;
31+
setOptions(options: Partial<TopDownMovementOptions>): void;
32+
}
33+
34+
interface InternalOptions {
35+
acceleration: number;
36+
deceleration: number;
37+
maxSpeed: number;
38+
drag: number;
39+
normalizeDiagonal: boolean;
40+
}
41+
42+
function assertFinite(value: number, label: string): void {
43+
if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) {
44+
throw new Error(`${label} must be a finite number.`);
45+
}
46+
}
47+
48+
function normalizeOptions(options: TopDownMovementOptions): InternalOptions {
49+
assertFinite(options.acceleration, 'acceleration');
50+
assertFinite(options.deceleration, 'deceleration');
51+
assertFinite(options.maxSpeed, 'maxSpeed');
52+
if (options.acceleration <= 0 || options.deceleration <= 0) {
53+
throw new Error('acceleration and deceleration must be greater than 0.');
54+
}
55+
if (options.maxSpeed <= 0) {
56+
throw new Error('maxSpeed must be greater than 0.');
57+
}
58+
const drag = options.drag ?? 0;
59+
assertFinite(drag, 'drag');
60+
if (drag < 0) {
61+
throw new Error('drag must be >= 0.');
62+
}
63+
const normalizeDiagonal = options.normalizeDiagonal ?? true;
64+
65+
return {
66+
acceleration: options.acceleration,
67+
deceleration: options.deceleration,
68+
maxSpeed: options.maxSpeed,
69+
drag,
70+
normalizeDiagonal,
71+
};
72+
}
73+
74+
function cloneState(state: TopDownState): TopDownState {
75+
return {
76+
position: { x: state.position.x, y: state.position.y },
77+
velocity: { x: state.velocity.x, y: state.velocity.y },
78+
facing: { x: state.facing.x, y: state.facing.y },
79+
};
80+
}
81+
82+
function magnitude(x: number, y: number): number {
83+
return Math.hypot(x, y);
84+
}
85+
86+
function normalize(x: number, y: number): Vector2D {
87+
const length = magnitude(x, y);
88+
if (length === 0) {
89+
return { x: 0, y: 0 };
90+
}
91+
return { x: x / length, y: y / length };
92+
}
93+
94+
/**
95+
* Creates a top-down movement controller with acceleration and directional damping.
96+
* Useful for: twin-stick or tile-based characters needing eight-direction movement.
97+
*/
98+
export function createTopDownController(
99+
options: TopDownMovementOptions,
100+
initialState: TopDownState = {
101+
position: { x: 0, y: 0 },
102+
velocity: { x: 0, y: 0 },
103+
facing: { x: 1, y: 0 },
104+
}
105+
): TopDownController {
106+
let config = normalizeOptions(options);
107+
const baseline = cloneState(initialState);
108+
const state: TopDownState = cloneState(initialState);
109+
110+
function setOptions(partial: Partial<TopDownMovementOptions>): void {
111+
config = normalizeOptions({
112+
acceleration: partial.acceleration ?? config.acceleration,
113+
deceleration: partial.deceleration ?? config.deceleration,
114+
maxSpeed: partial.maxSpeed ?? config.maxSpeed,
115+
drag: partial.drag ?? config.drag,
116+
normalizeDiagonal: partial.normalizeDiagonal ?? config.normalizeDiagonal,
117+
});
118+
}
119+
120+
function update({ delta, input }: TopDownUpdateOptions): TopDownState {
121+
assertFinite(delta, 'delta');
122+
if (delta < 0) {
123+
throw new Error('delta must be >= 0.');
124+
}
125+
if (!input) {
126+
throw new Error('input is required.');
127+
}
128+
129+
let moveX = input.x ?? 0;
130+
let moveY = input.y ?? 0;
131+
if (!Number.isFinite(moveX) || !Number.isFinite(moveY)) {
132+
throw new Error('input values must be finite numbers.');
133+
}
134+
135+
if (config.normalizeDiagonal) {
136+
const magnitudeInput = magnitude(moveX, moveY);
137+
if (magnitudeInput > 1) {
138+
moveX /= magnitudeInput;
139+
moveY /= magnitudeInput;
140+
}
141+
}
142+
143+
const hasInput = Math.abs(moveX) > 1e-3 || Math.abs(moveY) > 1e-3;
144+
145+
if (hasInput) {
146+
const direction = normalize(moveX, moveY);
147+
state.velocity.x += direction.x * config.acceleration * delta;
148+
state.velocity.y += direction.y * config.acceleration * delta;
149+
150+
const speed = magnitude(state.velocity.x, state.velocity.y);
151+
if (speed > config.maxSpeed) {
152+
const normalized = normalize(state.velocity.x, state.velocity.y);
153+
state.velocity.x = normalized.x * config.maxSpeed;
154+
state.velocity.y = normalized.y * config.maxSpeed;
155+
}
156+
157+
state.facing.x = direction.x;
158+
state.facing.y = direction.y;
159+
} else {
160+
const speed = magnitude(state.velocity.x, state.velocity.y);
161+
if (speed > 0) {
162+
const decelAmount = config.deceleration * delta;
163+
if (speed <= decelAmount) {
164+
state.velocity.x = 0;
165+
state.velocity.y = 0;
166+
} else {
167+
const normalized = normalize(state.velocity.x, state.velocity.y);
168+
const newSpeed = speed - decelAmount;
169+
state.velocity.x = normalized.x * newSpeed;
170+
state.velocity.y = normalized.y * newSpeed;
171+
}
172+
}
173+
}
174+
175+
if (config.drag > 0) {
176+
const dragFactor = Math.max(0, 1 - config.drag * delta);
177+
state.velocity.x *= dragFactor;
178+
state.velocity.y *= dragFactor;
179+
}
180+
181+
state.position.x += state.velocity.x * delta;
182+
state.position.y += state.velocity.y * delta;
183+
184+
return cloneState(state);
185+
}
186+
187+
function getState(): TopDownState {
188+
return cloneState(state);
189+
}
190+
191+
function reset(partial: Partial<TopDownState> = {}): void {
192+
const position = partial.position ?? baseline.position;
193+
const velocity = partial.velocity ?? baseline.velocity;
194+
const facing = partial.facing ?? baseline.facing;
195+
196+
state.position.x = position.x;
197+
state.position.y = position.y;
198+
state.velocity.x = velocity.x;
199+
state.velocity.y = velocity.y;
200+
state.facing.x = facing.x;
201+
state.facing.y = facing.y;
202+
}
203+
204+
return {
205+
update,
206+
getState,
207+
reset,
208+
setOptions,
209+
};
210+
}
211+
212+
/** @internal */
213+
export const __internals = {
214+
normalizeOptions,
215+
normalize,
216+
magnitude,
217+
};

src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const examples = {
9797
createSpriteAnimation: 'examples/spriteAnimation.ts',
9898
createTweenSystem: 'examples/tween.ts',
9999
createPlatformerController: 'examples/platformerPhysics.ts',
100+
createTopDownController: 'examples/topDownMovement.ts',
100101
},
101102
ai: {
102103
seek: 'examples/steering.ts',
@@ -570,6 +571,24 @@ export type {
570571
PlatformerUpdateOptions,
571572
} from './gameplay/platformerPhysics.js';
572573

574+
/**
575+
* Top-down movement controller for 8-direction navigation.
576+
*
577+
* Example file: examples/topDownMovement.ts
578+
*/
579+
export { createTopDownController } from './gameplay/topDownMovement.js';
580+
581+
/**
582+
* Top-down movement configuration, state, and input types.
583+
*/
584+
export type {
585+
TopDownMovementOptions,
586+
TopDownController,
587+
TopDownState,
588+
TopDownInput,
589+
TopDownUpdateOptions,
590+
} from './gameplay/topDownMovement.js';
591+
573592

574593
// ============================================================================
575594
// 🔍 SEARCH & STRING UTILITIES

0 commit comments

Comments
 (0)