From 3d724b61c9aca137b3d68bc3eb1964d0804e6e71 Mon Sep 17 00:00:00 2001
From: CsUtil <45512166+cs-util@users.noreply.github.com>
Date: Fri, 10 Oct 2025 04:37:30 +0200
Subject: [PATCH] feat: implement math marauders simulation
---
README.md | 169 +-
babel.config.js => babel.config.cjs | 0
eslint.config.js | 22 +
index.html | 248 +-
jest.config.cjs | 8 +
package-lock.json | 1985 ++++++++++++++---
package.json | 38 +-
src/core/BattleSystem.js | 89 +
src/core/FlockSystem.js | 59 +
src/core/GameController.js | 249 +++
src/core/GateSystem.js | 38 +
src/core/PersistenceManager.js | 59 +
src/core/Telemetry.js | 30 +
src/core/WaveGenerator.js | 243 ++
src/core/__tests__/BattleSystem.test.js | 50 +
src/core/__tests__/PersistenceManager.test.js | 32 +
src/core/__tests__/Telemetry.test.js | 11 +
src/core/__tests__/WaveGenerator.test.js | 33 +
src/core/constants.js | 28 +
src/index.js | 30 +-
src/index.test.js | 12 -
src/ui/UIManager.js | 295 +++
src/utils/random.js | 34 +
23 files changed, 3239 insertions(+), 523 deletions(-)
rename babel.config.js => babel.config.cjs (100%)
create mode 100644 eslint.config.js
create mode 100644 jest.config.cjs
create mode 100644 src/core/BattleSystem.js
create mode 100644 src/core/FlockSystem.js
create mode 100644 src/core/GameController.js
create mode 100644 src/core/GateSystem.js
create mode 100644 src/core/PersistenceManager.js
create mode 100644 src/core/Telemetry.js
create mode 100644 src/core/WaveGenerator.js
create mode 100644 src/core/__tests__/BattleSystem.test.js
create mode 100644 src/core/__tests__/PersistenceManager.test.js
create mode 100644 src/core/__tests__/Telemetry.test.js
create mode 100644 src/core/__tests__/WaveGenerator.test.js
create mode 100644 src/core/constants.js
delete mode 100644 src/index.test.js
create mode 100644 src/ui/UIManager.js
create mode 100644 src/utils/random.js
diff --git a/README.md b/README.md
index ec569a3..67c67fc 100644
--- a/README.md
+++ b/README.md
@@ -1,137 +1,70 @@
-## 1. Overview
+# Math Marauders
-A wave-based runner where the player leads a flock of soldiers through a series of forward math-choice gates and skirmishes, then reverses course in a “showdown chase” back through retreat-phase gates while being pursued by a massive enemy army. All locomotion and obstacle avoidance is handled by a GPU boids system (Three.js GPGPU birds demo).
+Math Marauders is a wave-based strategy runner inspired by the project brief in this repository. Instead of relying on WebGL or
+GPGPU simulations, this version provides a lightweight HTML interface that lets you steer an abstract flock of soldiers through
+the core systems described in the specification:
----
+- **Forward run** gates that apply deterministic math operations.
+- **Skirmishes** sized off the optimal path after each forward gate.
+- A **showdown → retreat chase** where automatic arrow volleys whittle down the pursuing enemy.
+- **Star ratings**, persistence, and telemetry hooks ready for future integrations.
-## 2. Game Flow
+The app focuses on surfacing the math and progression logic so the systems can be verified through unit tests and easily evolved
+into a richer visual experience later.
-1. **Forward Run**
+## Getting started
- * **Gate Count**: Wave 1 = 5 gates; each new wave adds +1 gate.
- * **Math Gates**: Full mix of +N, –N, ×M, ÷M in waves 1–5; waves 6–10 introduce two-step (e.g. x\*4–2); waves 11+ allow parentheses and exponents.
- * **Optimal-Path**: At each gate, pick the larger outcome; chain through all gates to compute “optimal army” size.
- * **Skirmishes**: Immediately after each gate, spawn an enemy group sized at **80 % of optimal-army** at that point. Resolve by straight subtraction.
- * **Obstacles**: Static rocks with a 0.5 m soft-tolerance buffer around the divider; boids slide along rocks naturally; any boid outside the buffer for >2 s is removed. Use e.g. const rockGeo = new THREE.DodecahedronGeometry(1.2);
+```bash
+npm install
+npm start # optional if you add your own static server
+```
-2. **Final Showdown → Retreat Chase**
+The project is entirely static. Opening `index.html` in a browser is enough to play the simulation.
- * Upon clearing the last forward gate, a single large enemy gate opens and a huge enemy flock pours out.
- * **Camera**: Same angled top-down tilt, but target and dolly direction reverse so the player retreats back to the start.
- * **Retreat Gates**: Mirror the forward-run count (e.g. 5 gates in wave 1). Same math-choice logic applies to your shrinking army.
- * **Chase Pacing**: Both sides run at 6 m/s; **each time you clear a retreat-phase gate**, the enemy surges to 8 m/s for 1 s.
- * **Arrow volleys**:
+## Available scripts
- * Automatic burst every 0.8 s, equal to 10 % of current player-army size.
- * Each arrow removes one enemy on contact (straight subtraction).
- * Implement via instanced `ArrowHelper` or pooled custom arrow mesh.
- * **Failure**: If the player returns to the start with zero soldiers, the wave ends as a defeat and the game **immediately restarts at wave 1**.
+| Script | Description |
+| --- | --- |
+| `npm run lint` | ESLint using the flat config in `eslint.config.js`. |
+| `npm run check:all` | Alias for the lint script. |
+| `npm test` | Jest unit tests configured for ESM via Babel. |
+| `npm run validate:all` | Lint + test, matching the project instructions. |
-3. **Progression & Scoring**
+## Architecture overview
- * **Stars**: 1–5 stars per wave based on final survivors ÷ optimal-path:
+The implementation leans on small modules inside `src/core` and `src/ui`:
- * ★1: 0–40 %
- * ★2: 41–60 %
- * ★3: 61–75 %
- * ★4: 76–90 %
- * ★5: 91–100 %
- * **Persistence**: Store best star rating per wave in `localStorage`.
- * **Navigation**: Single “Play” button always advances to next uncompleted wave; after each wave show a **minimalist popup**:
+| Module | Responsibility |
+| --- | --- |
+| `WaveGenerator` | Deterministically generates gate operations, enemy sizing, and optimal-path reference data for each wave. |
+| `GateSystem` | Resolves player choices and reports telemetry. |
+| `BattleSystem` | Handles skirmish subtraction, retreat arrow volleys, and star rating calculations. |
+| `FlockSystem` | Tracks player/enemy counts and notifies listeners (the UI). |
+| `GameController` | Orchestrates wave flow, progression, and persistence. |
+| `UIManager` | Renders the HUD, slider controls, and end-of-wave popups. |
+| `PersistenceManager` | Wraps `localStorage` and exposes helper methods for storing star ratings. |
+| `Telemetry` | Provides an abstract interface plus the default console-backed implementation. |
- * “Wave X Complete”
- * ★★☆☆☆ stars
- * “Next” or “Retry”
+All public classes include JSDoc annotations to make the data flow explicit.
----
+## Testing strategy
-## 3. Architecture & Module Breakdown
+Unit tests cover the deterministic core systems:
-* **`GameController`**
+- Wave generation rules (gate count, operation tiers, skirmish sizing).
+- Star rating thresholds and failure cases.
+- Persistence logic around star upgrades.
+- Telemetry behavior.
- * Orchestrates wave start/end, transitions between forward and retreat phases, win/lose handling.
-* **`WaveGenerator`** (procedural)
+Run the full suite with:
- * Calculates gate count, selects operations (tiered unlock), computes skirmish sizes, retreat-gate count.
-* **`FlockSystem`**
+```bash
+npm test
+```
- * GPU boids simulation (based on three.js GPGPU birds).
- * Obstacle avoidance (rocks), cohesion radius, buffer timing for stragglers.
-* **`GateSystem`**
+## Future work
- * Spawns math gates, displays floating labels, evaluates player vs optimal choices.
-* **`BattleSystem`**
-
- * Resolves skirmishes and final showdown subtraction, arrow volley logic, enemy spawn/pursuit.
-* **`UIManager`**
-
- * Renders HTML/CSS slider overlay (` ` or custom div), floating soldier counts above flocks, gate-pop labels.
- * Summary popup, Play button, responsive layout for mouse/touch.
-* **`PersistenceManager`**
-
- * Wraps `localStorage` for saving/loading star ratings.
-* **`Telemetry`** (abstract interface)
-
- * Default → console logging only; can plug in other analytics later.
-
----
-
-## 4. Data & Configuration
-
-* **Wave configuration** is **fully procedural**—no external JSON. All parameters derive from formulas and rules in code.
-* **Math-expression tiers** and **skirmish percentage (80 %)** are constants defined in `WaveGenerator`.
-* **Star thresholds** and **movement speeds** are constants configurable at the top of each relevant module.
-
----
-
-## 5. Visuals & Performance
-
-* **Models**: Low-poly soldier mesh instanced via `InstancedMesh`. Use e.g. new THREE.BoxGeometry(0.6, 1.2, 0.6); for soldiers.
-* **Boids**: Thousands of agents at 60 fps desktop, 30 fps mobile.
-* **HUD**: Minimal floating labels (CSS2DRenderer or sprite text).
-* **Slider**: HTML/CSS overlay over the canvas; pointer events for drag/steer.
-* **Camera**: Single `PerspectiveCamera` at a fixed angle; follow-rig that lerps to the flock’s center, reversing direction on showdown.
-* **Performance Targets**:
-
- * Desktop → 60 fps, Mobile → 30 fps (select quality preset at startup).
- * No dynamic LOD or quality scaling beyond that.
-
----
-
-## 6. Build & Deployment
-
-* **Pure ES6 modules**: drop all `.js` files in a `src/` folder; no build step if browser supports modules.
-* **Fallback**: If older-browser support is needed, use **Vite + native ESM** with zero-config for fast HMR and bundling.
-
----
-
-## 7. Error Handling
-
-* **Resource load failures** (models, shaders, textures): catch, log to console, and **retry** up to N attempts before giving up silently.
-* **Runtime errors**: surface in console; game continues where possible.
-
----
-
-## 8. Testing & QA
-
-* **Unit tests only** (Jest or equivalent) covering:
-
- * Math-gate evaluation and optimal-path logic.
- * `WaveGenerator` formulas (gate count, skirmish sizing, tiered unlock).
- * Star-rating thresholds.
- * Persistence read/write.
- * Telemetry stub behavior.
-
----
-
-## 9. Documentation
-
-* **Inline JSDoc** on all public classes, methods, and data structures.
-* A small `README.md` describing architecture, module responsibilities, and how to run tests.
-
----
-
-## 10. Analytics & Telemetry
-
-* **Abstract `Telemetry` interface** with methods like `trackEvent(name, payload)`.
-* Default implementation logs to console; ready for remote analytics plug-in in future.
\ No newline at end of file
+- Swap the abstract UI for an actual WebGL flock simulation (Three.js + GPU boids).
+- Model obstacle avoidance and flock splitting per the original brief.
+- Persist additional analytics via the telemetry interface.
+- Expand tests to cover retreat pacing, timer behavior, and UI flows.
diff --git a/babel.config.js b/babel.config.cjs
similarity index 100%
rename from babel.config.js
rename to babel.config.cjs
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..b160503
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js';
+import globals from 'globals';
+
+export default [
+ js.configs.recommended,
+ {
+ files: ['**/*.js'],
+ languageOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2022,
+ globals: {
+ ...globals.browser,
+ ...globals.jest,
+ },
+ },
+ rules: {
+ 'no-console': 'off',
+ 'class-methods-use-this': 'off',
+ 'no-unused-vars': ['warn', {args: 'none', vars: 'all', varsIgnorePattern: '^_'}],
+ },
+ },
+];
diff --git a/index.html b/index.html
index 30404ce..c76e878 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,247 @@
-TODO
\ No newline at end of file
+
+
+
+
+
+ Math Marauders
+
+
+
+
+
+
+
+
+
+
diff --git a/jest.config.cjs b/jest.config.cjs
new file mode 100644
index 0000000..f9b846d
--- /dev/null
+++ b/jest.config.cjs
@@ -0,0 +1,8 @@
+module.exports = {
+ testEnvironment: 'jsdom',
+ transform: {
+ '^.+\\.js$': 'babel-jest',
+ },
+ moduleFileExtensions: ['js'],
+ collectCoverageFrom: ['src/**/*.js', '!src/**/index.js'],
+};
diff --git a/package-lock.json b/package-lock.json
index 8abce5a..4d113c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,22 @@
{
- "name": "templatejs",
+ "name": "math-marauders",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "templatejs",
+ "name": "math-marauders",
"version": "1.0.0",
- "license": "ISC",
"devDependencies": {
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
- "babel-jest": "^30.0.0-beta.3",
- "jest": "^29.7.0"
+ "@eslint/js": "^9.33.0",
+ "babel-jest": "^29.7.0",
+ "eslint": "^9.33.0",
+ "globals": "^16.3.0",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "prettier": "^3.3.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -891,6 +895,16 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-classes/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/plugin-transform-computed-properties": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
@@ -1722,6 +1736,16 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/types": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
@@ -1743,6 +1767,235 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
+ "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
+ "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.37.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
+ "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
+ "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.16.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1913,30 +2166,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
- "node_modules/@jest/pattern": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.0-beta.3.tgz",
- "integrity": "sha512-IuB9mweyJI5ToVBRdptKb2w97LGnNHFI+V9/cGaYeFareL7BYD6KiUH022OC51K1841c6YzgYjyQmJHFxELZSQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "jest-regex-util": "30.0.0-beta.3"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
- "node_modules/@jest/pattern/node_modules/jest-regex-util": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz",
- "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
"node_modules/@jest/reporters": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
@@ -2166,6 +2395,16 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2211,6 +2450,13 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -2248,6 +2494,25 @@
"@types/istanbul-lib-report": "*"
}
},
+ "node_modules/@types/jsdom": {
+ "version": "20.0.1",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
+ "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "22.15.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
@@ -2265,6 +2530,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -2282,12 +2554,90 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@ungap/structured-clone": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
- "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "deprecated": "Use your platform's native atob() and btoa() methods instead",
"dev": true,
- "license": "ISC"
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+ "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
@@ -2355,276 +2705,58 @@
"sprintf-js": "~1.0.2"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/babel-jest": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.0-beta.3.tgz",
- "integrity": "sha512-h7VooBet0MbW4KuDxLY38sD3VrX6ugyePeA6vKnAx0ncYDRJwGfa/ZFZtl0e4JQ7jyby4qPV9c8BMfsKOR1Big==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
+ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/transform": "30.0.0-beta.3",
+ "@jest/transform": "^29.7.0",
"@types/babel__core": "^7.1.14",
- "babel-plugin-istanbul": "^7.0.0",
- "babel-preset-jest": "30.0.0-beta.3",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"slash": "^3.0.0"
},
"engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
- "@babel/core": "^7.11.0"
+ "@babel/core": "^7.8.0"
}
},
- "node_modules/babel-jest/node_modules/@jest/schemas": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.0-beta.3.tgz",
- "integrity": "sha512-tiT79EKOlJGT5v8fYr9UKLSyjlA3Ek+nk0cVZwJGnRqVp26EQSOTYXBCzj0dGMegkgnPTt3f7wP1kGGI8q/e0g==",
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-3-Clause",
"dependencies": {
- "@sinclair/typebox": "^0.34.0"
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
},
"engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
+ "node": ">=8"
}
},
- "node_modules/babel-jest/node_modules/@jest/transform": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.0-beta.3.tgz",
- "integrity": "sha512-2gixxaYdRh3MQaRsEenHejw0qBIW72DfwG1q9HPLXpnLkm5TKZlTOvOS33S00PGEoa4UG1Iq9tNHh7fxOJAGwQ==",
+ "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.11.6",
- "@jest/types": "30.0.0-beta.3",
- "@jridgewell/trace-mapping": "^0.3.18",
- "babel-plugin-istanbul": "^7.0.0",
- "chalk": "^4.0.0",
- "convert-source-map": "^2.0.0",
- "fast-json-stable-stringify": "^2.1.0",
- "graceful-fs": "^4.2.9",
- "jest-haste-map": "30.0.0-beta.3",
- "jest-regex-util": "30.0.0-beta.3",
- "jest-util": "30.0.0-beta.3",
- "micromatch": "^4.0.8",
- "pirates": "^4.0.4",
- "slash": "^3.0.0",
- "write-file-atomic": "^5.0.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
- "node_modules/babel-jest/node_modules/@jest/types": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.0-beta.3.tgz",
- "integrity": "sha512-x7GyHD8rxZ4Ygmp4rea3uPDIPZ6Jglcglaav8wQNqXsVUAByapDwLF52Cp3wEYMPMnvH4BicEj56j8fqZx5jng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/pattern": "30.0.0-beta.3",
- "@jest/schemas": "30.0.0-beta.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
- "node_modules/babel-jest/node_modules/@sinclair/typebox": {
- "version": "0.34.33",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz",
- "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/babel-jest/node_modules/babel-plugin-istanbul": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz",
- "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.0.0",
- "@istanbuljs/load-nyc-config": "^1.0.0",
- "@istanbuljs/schema": "^0.1.3",
- "istanbul-lib-instrument": "^6.0.2",
- "test-exclude": "^6.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/babel-jest/node_modules/ci-info": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
- "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/sibiraj-s"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/babel-jest/node_modules/jest-haste-map": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.0-beta.3.tgz",
- "integrity": "sha512-MafsVPIca9E4HR3Fp9gYX+AET4YZmU/VtyLcnRJ9QHdVqHSCzOaElxX30BlyNf5Nw6ZcCafkbB0RGXqSwwsjxw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/types": "30.0.0-beta.3",
- "@types/node": "*",
- "anymatch": "^3.0.3",
- "fb-watchman": "^2.0.0",
- "graceful-fs": "^4.2.9",
- "jest-regex-util": "30.0.0-beta.3",
- "jest-util": "30.0.0-beta.3",
- "jest-worker": "30.0.0-beta.3",
- "micromatch": "^4.0.8",
- "walker": "^1.0.8"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- },
- "optionalDependencies": {
- "fsevents": "^2.3.2"
- }
- },
- "node_modules/babel-jest/node_modules/jest-regex-util": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz",
- "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
- "node_modules/babel-jest/node_modules/jest-util": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.0-beta.3.tgz",
- "integrity": "sha512-kob8YNaO1UPrG0TgGdH5l0ciNGuXDX93Yn2b2VCkALuqOXbqzT2xCr6O7dBuwhM7tmzBbpM6CkcK7Qyf/JmLZQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/types": "30.0.0-beta.3",
- "@types/node": "*",
- "chalk": "^4.0.0",
- "ci-info": "^4.0.0",
- "graceful-fs": "^4.2.9",
- "picomatch": "^4.0.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
- "node_modules/babel-jest/node_modules/jest-worker": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.0-beta.3.tgz",
- "integrity": "sha512-v17y4Jg9geh3tDm8aU2snuwr8oCJtFefuuPrMRqmC6Ew8K+sLfOcuB3moJ15PHoe4MjTGgsC1oO2PK/GaF1vTg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "@ungap/structured-clone": "^1.2.0",
- "jest-util": "30.0.0-beta.3",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
- }
- },
- "node_modules/babel-jest/node_modules/picomatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/babel-jest/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/babel-jest/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/babel-jest/node_modules/write-file-atomic": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
- "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "imurmurhash": "^0.1.4",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
- "node_modules/babel-plugin-istanbul": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
- "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.0.0",
- "@istanbuljs/load-nyc-config": "^1.0.0",
- "@istanbuljs/schema": "^0.1.2",
- "istanbul-lib-instrument": "^5.0.4",
- "test-exclude": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
- "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
- "dev": true,
- "license": "BSD-3-Clause",
+ "license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.12.3",
"@babel/parser": "^7.14.7",
@@ -2637,18 +2769,19 @@
}
},
"node_modules/babel-plugin-jest-hoist": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.0-beta.3.tgz",
- "integrity": "sha512-phSBX46tzCw+6KB9lUuYzyjq16nCRYntYvsDNOx5ZXSGPBcEGbe1mQI+CgdmKUKDD4+o/NDYlvDaQSB3UaSVSw==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.3.3",
"@babel/types": "^7.3.3",
- "@types/babel__core": "^7.1.14"
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
},
"engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
@@ -2721,20 +2854,20 @@
}
},
"node_modules/babel-preset-jest": {
- "version": "30.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.0-beta.3.tgz",
- "integrity": "sha512-2/Oy4J/MxFwNszlwYPO4L7Z+XI7CNCbiz5HZwrsfWnEEDBxJZBJzblfc8TP9lzeiQ4v+Vvem7BMS6B2dVCfzOg==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "babel-plugin-jest-hoist": "30.0.0-beta.3",
+ "babel-plugin-jest-hoist": "^29.6.3",
"babel-preset-current-node-syntax": "^1.0.0"
},
"engines": {
- "node": "^18.14.0 || ^20.0.0 || >=22.0.0"
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
- "@babel/core": "^7.11.0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/balanced-match": {
@@ -2818,6 +2951,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2962,6 +3109,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3027,6 +3187,48 @@
"node": ">= 8"
}
},
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -3045,6 +3247,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/dedent": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
@@ -3060,6 +3269,13 @@
}
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -3070,6 +3286,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -3090,6 +3316,35 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.165",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
@@ -3117,6 +3372,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -3127,6 +3395,55 @@
"is-arrayish": "^0.2.1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -3144,21 +3461,250 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=8"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.37.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
+ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.4.0",
+ "@eslint/core": "^0.16.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.37.0",
+ "@eslint/plugin-kit": "^0.4.0",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
}
},
- "node_modules/esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
- "bin": {
- "esparse": "bin/esparse.js",
- "esvalidate": "bin/esvalidate.js"
- },
"engines": {
- "node": ">=4"
+ "node": ">=4.0"
}
},
"node_modules/esutils": {
@@ -3221,6 +3767,13 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3228,6 +3781,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fb-watchman": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -3238,6 +3798,19 @@
"bser": "2.1.1"
}
},
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3265,6 +3838,44 @@
"node": ">=8"
}
},
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3317,6 +3928,31 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -3327,6 +3963,20 @@
"node": ">=8.0.0"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -3362,14 +4012,43 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "version": "16.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
+ "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=4"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
@@ -3389,6 +4068,35 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -3402,6 +4110,19 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -3409,6 +4130,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -3419,6 +4169,56 @@
"node": ">=10.17.0"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/import-local": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
@@ -3491,6 +4291,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -3511,6 +4321,19 @@
"node": ">=6"
}
},
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3521,6 +4344,13 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -3779,61 +4609,6 @@
}
}
},
- "node_modules/jest-config/node_modules/babel-jest": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
- "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/transform": "^29.7.0",
- "@types/babel__core": "^7.1.14",
- "babel-plugin-istanbul": "^6.1.1",
- "babel-preset-jest": "^29.6.3",
- "chalk": "^4.0.0",
- "graceful-fs": "^4.2.9",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.8.0"
- }
- },
- "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
- "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.3.3",
- "@babel/types": "^7.3.3",
- "@types/babel__core": "^7.1.14",
- "@types/babel__traverse": "^7.0.6"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-config/node_modules/babel-preset-jest": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
- "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "babel-plugin-jest-hoist": "^29.6.3",
- "babel-preset-current-node-syntax": "^1.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
"node_modules/jest-diff": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
@@ -3874,10 +4649,38 @@
"chalk": "^4.0.0",
"jest-get-type": "^29.6.3",
"jest-util": "^29.7.0",
- "pretty-format": "^29.7.0"
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
+ "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "@types/jsdom": "^20.0.0",
+ "@types/node": "*",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "jsdom": "^20.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
}
},
"node_modules/jest-environment-node": {
@@ -4297,6 +5100,52 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "20.0.3",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
+ "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "acorn": "^8.8.1",
+ "acorn-globals": "^7.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.2",
+ "decimal.js": "^10.4.2",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0",
+ "ws": "^8.11.0",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -4310,6 +5159,13 @@
"node": ">=6"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -4317,6 +5173,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -4330,6 +5200,16 @@
"node": ">=6"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -4350,6 +5230,20 @@
"node": ">=6"
}
},
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -4377,6 +5271,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4426,6 +5327,16 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -4447,6 +5358,29 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -4521,6 +5455,13 @@
"node": ">=8"
}
},
+ "node_modules/nwsapi": {
+ "version": "2.2.22",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz",
+ "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -4547,6 +5488,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -4602,6 +5561,19 @@
"node": ">=6"
}
},
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -4621,6 +5593,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4701,6 +5686,32 @@
"node": ">=8"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -4743,6 +5754,29 @@
"node": ">= 6"
}
},
+ "node_modules/psl": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lupomontero"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@@ -4760,6 +5794,13 @@
],
"license": "MIT"
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -4848,6 +5889,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -4902,6 +5950,26 @@
"node": ">=10"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -5101,6 +6169,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -5136,6 +6211,48 @@
"node": ">=8.0"
}
},
+ "node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@@ -5210,6 +6327,16 @@
"node": ">=4"
}
},
+ "node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -5241,6 +6368,27 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -5256,6 +6404,19 @@
"node": ">=10.12.0"
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+ "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -5266,6 +6427,53 @@
"makeerror": "1.0.12"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5282,6 +6490,16 @@
"node": ">= 8"
}
},
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -5321,6 +6539,45 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 8d43e8a..4c06299 100644
--- a/package.json
+++ b/package.json
@@ -1,40 +1,24 @@
{
- "name": "templatejs",
+ "name": "math-marauders",
"version": "1.0.0",
- "description": "TemplateJs is a minimal single-page web app template designed for quick deployment. It follows an AI-assisted iterative development process:",
- "main": "index.js",
+ "description": "Math Marauders is a strategy runner where you steer a flock through math gates and tactical battles.",
+ "type": "module",
"scripts": {
- "test": "npm run format && npm run lint && npm run check:all && npm run test:unit",
- "format": "prettier --write --ignore-unknown --no-error-on-unmatched-pattern \"src\" \"pages\" \"config\" \"docs\" index.html README.md",
- "test:unit": "jest --coverage --config config/jest.config.js",
- "lint": "eslint . --config config/eslint.config.js",
- "test:watch": "jest --watch --config config/jest.config.js",
- "mutation": "stryker run config/stryker.conf.json; node mutation-testing/mutation-report-to-md.js",
- "check:dup": "jscpd --config config/.jscpd.json src",
- "check:cycles": "madge --circular --extensions js src",
- "check:boundaries": "depcruise -c config/.dependency-cruiser.js src",
- "check:all": "npm run check:dup && npm run check:cycles && npm run check:boundaries",
- "validate:all": "npm run test && npm run mutation"
+ "lint": "eslint src",
+ "format": "prettier --check \"src/**/*.js\" \"index.html\" \"README.md\"",
+ "check:all": "npm run lint",
+ "test": "jest",
+ "validate:all": "npm run check:all && npm test"
},
- "keywords": [],
- "author": "",
- "license": "ISC",
"devDependencies": {
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@eslint/js": "^9.33.0",
- "@stryker-mutator/core": "^9.0.1",
- "@stryker-mutator/jest-runner": "^9.0.1",
- "babel-jest": "^30.0.0-beta.3",
- "dependency-cruiser": "^17.0.1",
+ "babel-jest": "^29.7.0",
"eslint": "^9.33.0",
- "eslint-plugin-jest": "^29.0.1",
- "fast-check": "^4.2.0",
"globals": "^16.3.0",
"jest": "^29.7.0",
- "jest-environment-jsdom": "^30.0.5",
- "jscpd": "^4.0.5",
- "prettier": "^3.3.3",
- "madge": "^8.0.0"
+ "jest-environment-jsdom": "^29.7.0",
+ "prettier": "^3.3.3"
}
}
diff --git a/src/core/BattleSystem.js b/src/core/BattleSystem.js
new file mode 100644
index 0000000..3080cd1
--- /dev/null
+++ b/src/core/BattleSystem.js
@@ -0,0 +1,89 @@
+import {
+ RETREAT_ARROW_RATIO,
+ TELEMETRY_EVENTS,
+ SHOWDOWN_ENEMY_RATIO,
+} from './constants.js';
+
+/**
+ * Manages arithmetic combat resolution.
+ */
+export class BattleSystem {
+ /**
+ * @param {object} deps
+ * @param {import('./FlockSystem.js').FlockSystem} deps.flocks
+ * @param {import('./Telemetry.js').Telemetry} deps.telemetry
+ */
+ constructor({flocks, telemetry}) {
+ this.flocks = flocks;
+ this.telemetry = telemetry;
+ }
+
+ /**
+ * Resolves a skirmish by subtracting the enemy from the player's army.
+ * @param {number} playerArmy
+ * @param {number} enemyArmy
+ * @returns {number} Player survivors
+ */
+ resolveSkirmish(playerArmy, enemyArmy) {
+ const survivors = Math.max(0, playerArmy - enemyArmy);
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.SKIRMISH, {
+ playerArmy,
+ enemyArmy,
+ survivors,
+ });
+ this.flocks.setPlayer(survivors);
+ return survivors;
+ }
+
+ /**
+ * Calculates the volley size when arrows are fired.
+ * @param {number} playerArmy
+ * @returns {number}
+ */
+ calculateArrowVolley(playerArmy) {
+ return Math.max(0, Math.floor(playerArmy * RETREAT_ARROW_RATIO));
+ }
+
+ /**
+ * Applies a volley to the pursuing enemy.
+ * @param {number} volleySize
+ * @returns {number} Remaining enemy count
+ */
+ applyArrowVolley(volleySize) {
+ this.flocks.removeEnemy(volleySize);
+ return this.flocks.enemy;
+ }
+
+ /**
+ * Calculates the initial showdown enemy size.
+ * @param {number} playerArmy
+ * @returns {number}
+ */
+ calculateShowdownEnemy(playerArmy) {
+ return Math.max(0, Math.ceil(playerArmy * SHOWDOWN_ENEMY_RATIO));
+ }
+}
+
+/**
+ * Determines the star rating from the final survivors and optimal path.
+ * @param {number} survivors - Player survivors.
+ * @param {number} optimal - Optimal survivors according to generator.
+ * @param {number[]} thresholds - Fractional thresholds ascending.
+ * @returns {number}
+ */
+export function calculateStarRating(survivors, optimal, thresholds) {
+ if (survivors <= 0) {
+ return 0;
+ }
+ if (optimal <= 0) {
+ return thresholds.length + 1;
+ }
+ const ratio = survivors / optimal;
+ let stars = 1;
+ thresholds.forEach((threshold, index) => {
+ if (ratio > threshold) {
+ stars = index + 2;
+ }
+ });
+ return Math.min(thresholds.length + 1, stars);
+}
diff --git a/src/core/FlockSystem.js b/src/core/FlockSystem.js
new file mode 100644
index 0000000..3bbcbec
--- /dev/null
+++ b/src/core/FlockSystem.js
@@ -0,0 +1,59 @@
+/**
+ * Maintains the abstract state of the player and enemy flocks.
+ */
+export class FlockSystem {
+ constructor() {
+ this.player = 0;
+ this.enemy = 0;
+ this.observers = new Set();
+ }
+
+ /**
+ * Subscribes to updates.
+ * @param {(state: {player: number, enemy: number}) => void} observer
+ */
+ subscribe(observer) {
+ this.observers.add(observer);
+ observer({player: this.player, enemy: this.enemy});
+ }
+
+ /**
+ * Removes a subscription.
+ * @param {(state: {player: number, enemy: number}) => void} observer
+ */
+ unsubscribe(observer) {
+ this.observers.delete(observer);
+ }
+
+ notify() {
+ const snapshot = {player: this.player, enemy: this.enemy};
+ this.observers.forEach((observer) => observer(snapshot));
+ }
+
+ /**
+ * Sets the player army size.
+ * @param {number} count
+ */
+ setPlayer(count) {
+ this.player = Math.max(0, Math.floor(count));
+ this.notify();
+ }
+
+ /**
+ * Sets the enemy army size.
+ * @param {number} count
+ */
+ setEnemy(count) {
+ this.enemy = Math.max(0, Math.floor(count));
+ this.notify();
+ }
+
+ /**
+ * Adjusts the enemy by subtracting defeated units.
+ * @param {number} defeated
+ */
+ removeEnemy(defeated) {
+ this.enemy = Math.max(0, this.enemy - Math.floor(defeated));
+ this.notify();
+ }
+}
diff --git a/src/core/GameController.js b/src/core/GameController.js
new file mode 100644
index 0000000..80086c0
--- /dev/null
+++ b/src/core/GameController.js
@@ -0,0 +1,249 @@
+import {
+ BASE_PLAYER_ARMY,
+ RETREAT_ARROW_INTERVAL_MS,
+ STAR_THRESHOLDS,
+ TELEMETRY_EVENTS,
+} from './constants.js';
+import {calculateStarRating} from './BattleSystem.js';
+
+/**
+ * Coordinates the main game loop.
+ */
+export class GameController {
+ /**
+ * @param {object} deps
+ * @param {import('./WaveGenerator.js').WaveGenerator} deps.generator
+ * @param {import('./GateSystem.js').GateSystem} deps.gates
+ * @param {import('./BattleSystem.js').BattleSystem} deps.battles
+ * @param {import('./FlockSystem.js').FlockSystem} deps.flocks
+ * @param {import('../ui/UIManager.js').UIManager} deps.ui
+ * @param {import('./PersistenceManager.js').PersistenceManager} deps.persistence
+ * @param {import('./Telemetry.js').Telemetry} deps.telemetry
+ */
+ constructor({generator, gates, battles, flocks, ui, persistence, telemetry}) {
+ this.generator = generator;
+ this.gates = gates;
+ this.battles = battles;
+ this.flocks = flocks;
+ this.ui = ui;
+ this.persistence = persistence;
+ this.telemetry = telemetry;
+
+ this.phase = 'idle';
+ this.currentWave = 1;
+ this.waveData = null;
+ this.forwardIndex = 0;
+ this.retreatIndex = 0;
+ this.currentArmy = BASE_PLAYER_ARMY;
+ this.arrowTimer = null;
+ this.stars = {};
+ }
+
+ /**
+ * Sets up listeners and initial UI.
+ */
+ initialize() {
+ this.stars = this.persistence.loadStars();
+ this.ui.initialize({
+ onPlay: () => this.startWave(this.currentWave),
+ onGateCommit: (choice) => this.handleGate(choice),
+ });
+ this.flocks.subscribe((state) => this.ui.updateFlocks(state));
+ this.ui.showThresholds();
+ this.#refreshStarPreview();
+ }
+
+ startWave(waveNumber) {
+ this.#clearArrowLoop();
+ this.currentWave = waveNumber;
+ this.waveData = this.generator.generate(waveNumber, BASE_PLAYER_ARMY);
+ this.phase = 'forward';
+ this.forwardIndex = 0;
+ this.retreatIndex = 0;
+ this.currentArmy = this.waveData.startingArmy;
+ this.flocks.setPlayer(this.currentArmy);
+ this.flocks.setEnemy(0);
+ this.ui.setWave(waveNumber);
+ this.ui.setPhase('Forward Run');
+ this.ui.setStatus('Lead your soldiers through the math gates.');
+ this.#refreshStarPreview();
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.WAVE_START, {wave: waveNumber});
+ this.#presentForwardGate();
+ }
+
+ handleGate(choice) {
+ if (this.phase === 'forward') {
+ this.#resolveForwardGate(choice);
+ } else if (this.phase === 'retreat') {
+ this.#resolveRetreatGate(choice);
+ }
+ }
+
+ #presentForwardGate() {
+ if (!this.waveData) {
+ return;
+ }
+ if (this.forwardIndex >= this.waveData.forwardGates.length) {
+ this.#startShowdown();
+ return;
+ }
+ const gate = this.waveData.forwardGates[this.forwardIndex];
+ this.ui.showGate(this.forwardIndex, this.waveData.forwardGates.length, gate);
+ }
+
+ #resolveForwardGate(choice) {
+ const gate = this.waveData.forwardGates[this.forwardIndex];
+ const optimalInfo = this.waveData.optimal.forward.checkpoints[this.forwardIndex];
+ const result = this.gates.resolve({gate, choice, army: this.currentArmy});
+ this.currentArmy = result.value;
+ this.flocks.setPlayer(this.currentArmy);
+ const enemy = optimalInfo.enemy;
+ this.flocks.setEnemy(enemy);
+ const survivors = this.battles.resolveSkirmish(this.currentArmy, enemy);
+ this.currentArmy = survivors;
+ this.flocks.setEnemy(0);
+ const choiceSummary = result.isOptimal ? 'Optimal choice!' : 'Suboptimal choice.';
+ this.ui.setStatus(
+ `${choiceSummary} Gate yielded ${result.value} (best ${result.best}). Skirmish enemy ${enemy}, survivors ${survivors}.`
+ );
+ if (this.currentArmy <= 0) {
+ this.#handleDefeat('Your army fell during the forward run.');
+ return;
+ }
+ this.forwardIndex += 1;
+ this.#presentForwardGate();
+ }
+
+ #startShowdown() {
+ this.phase = 'showdown';
+ this.ui.setPhase('Showdown');
+ const enemy = this.battles.calculateShowdownEnemy(this.currentArmy);
+ this.flocks.setEnemy(enemy);
+ this.ui.setStatus('The enemy charges! Begin the retreat.');
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.SHOWDOWN_START, {wave: this.currentWave, enemy});
+ this.#startRetreat();
+ }
+
+ #startRetreat() {
+ this.phase = 'retreat';
+ this.retreatIndex = 0;
+ this.ui.setPhase('Retreat Chase');
+ this.ui.setStatus('Slide toward the safest exits while arrows fire automatically.');
+ this.#startArrowLoop();
+ this.#presentRetreatGate();
+ }
+
+ #presentRetreatGate() {
+ if (this.retreatIndex >= this.waveData.retreatGates.length) {
+ this.#completeWave();
+ return;
+ }
+ const gate = this.waveData.retreatGates[this.retreatIndex];
+ this.ui.showGate(this.retreatIndex, this.waveData.retreatGates.length, gate);
+ }
+
+ #resolveRetreatGate(choice) {
+ const gate = this.waveData.retreatGates[this.retreatIndex];
+ const result = this.gates.resolve({gate, choice, army: this.currentArmy});
+ this.currentArmy = result.value;
+ this.flocks.setPlayer(this.currentArmy);
+ const volley = this.battles.calculateArrowVolley(this.currentArmy);
+ let message = `Retreat gate cleared. ${this.currentArmy} soldiers remain.`;
+ if (volley > 0) {
+ const remaining = this.battles.applyArrowVolley(volley);
+ message += ` Volley hits for ${volley}. Enemy remaining ${remaining}.`;
+ }
+ this.ui.setStatus(message);
+ if (this.currentArmy <= 0) {
+ this.#handleDefeat('Your forces disbanded during the retreat.');
+ return;
+ }
+ this.retreatIndex += 1;
+ if (this.retreatIndex >= this.waveData.retreatGates.length) {
+ this.#completeWave();
+ } else {
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.RETREAT_GATE, {
+ wave: this.currentWave,
+ gate: this.retreatIndex,
+ });
+ this.#presentRetreatGate();
+ }
+ }
+
+ #completeWave() {
+ this.#clearArrowLoop();
+ this.phase = 'completed';
+ this.flocks.setEnemy(0);
+ const optimal = this.waveData.optimal.retreat.finalArmy;
+ const stars = calculateStarRating(this.currentArmy, optimal, STAR_THRESHOLDS);
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.WAVE_COMPLETE, {
+ wave: this.currentWave,
+ survivors: this.currentArmy,
+ optimal,
+ stars,
+ });
+ this.stars = this.persistence.updateStars(this.currentWave, stars);
+ this.ui.setStarPreview(stars);
+ const completedWave = this.currentWave;
+ const nextWave = completedWave + 1;
+ this.ui.showPopup({
+ title: `Wave ${completedWave} Complete`,
+ stars,
+ message: `Survivors: ${this.currentArmy}. Optimal survivors: ${optimal}.`,
+ onNext: () => this.startWave(nextWave),
+ onRetry: () => this.startWave(completedWave),
+ });
+ this.currentWave = nextWave;
+ this.#refreshStarPreview();
+ }
+
+ #handleDefeat(reason) {
+ this.#clearArrowLoop();
+ this.phase = 'failed';
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.WAVE_FAILED, {
+ wave: this.currentWave,
+ reason,
+ });
+ this.ui.showPopup({
+ title: 'Wave Failed',
+ stars: 0,
+ message: `${reason} Restarting from wave 1 will restore your momentum.`,
+ onNext: () => this.startWave(1),
+ onRetry: () => this.startWave(1),
+ nextLabel: 'Restart',
+ retryLabel: 'Retry Wave 1',
+ });
+ this.currentWave = 1;
+ this.#refreshStarPreview();
+ }
+
+ #startArrowLoop() {
+ this.#clearArrowLoop();
+ this.arrowTimer = setInterval(() => {
+ if (this.phase !== 'retreat') {
+ return;
+ }
+ const volley = this.battles.calculateArrowVolley(this.currentArmy);
+ if (volley <= 0) {
+ return;
+ }
+ const remaining = this.battles.applyArrowVolley(volley);
+ this.ui.setStatus(`Arrow volley strikes ${volley} enemies. ${remaining} remain in pursuit.`);
+ if (remaining <= 0) {
+ this.#clearArrowLoop();
+ }
+ }, RETREAT_ARROW_INTERVAL_MS);
+ }
+
+ #clearArrowLoop() {
+ if (this.arrowTimer) {
+ clearInterval(this.arrowTimer);
+ this.arrowTimer = null;
+ }
+ }
+
+ #refreshStarPreview() {
+ const best = this.stars[this.currentWave] ?? 0;
+ this.ui.setStarPreview(best);
+ }
+}
diff --git a/src/core/GateSystem.js b/src/core/GateSystem.js
new file mode 100644
index 0000000..273dae5
--- /dev/null
+++ b/src/core/GateSystem.js
@@ -0,0 +1,38 @@
+import {TELEMETRY_EVENTS} from './constants.js';
+
+/**
+ * Handles resolving player choices at math gates.
+ */
+export class GateSystem {
+ /**
+ * @param {import('./Telemetry.js').Telemetry} telemetry - Telemetry sink.
+ */
+ constructor(telemetry) {
+ this.telemetry = telemetry;
+ }
+
+ /**
+ * Evaluates the provided gate for the supplied army size.
+ * @param {object} params
+ * @param {object} params.gate - Gate descriptor from the generator.
+ * @param {'left'|'right'} params.choice - Player choice.
+ * @param {number} params.army - Current player army.
+ * @returns {{value: number, left: number, right: number, isOptimal: boolean}}
+ */
+ resolve({gate, choice, army}) {
+ const left = gate.options[0].apply(army);
+ const right = gate.options[1].apply(army);
+ const value = choice === 'left' ? left : right;
+ const best = Math.max(left, right);
+ const isOptimal = value === best;
+ this.telemetry.trackEvent(TELEMETRY_EVENTS.GATE_RESOLVED, {
+ gateId: gate.id,
+ choice,
+ armyBefore: army,
+ result: value,
+ optimal: best,
+ isOptimal,
+ });
+ return {value, left, right, isOptimal, best};
+ }
+}
diff --git a/src/core/PersistenceManager.js b/src/core/PersistenceManager.js
new file mode 100644
index 0000000..363be9f
--- /dev/null
+++ b/src/core/PersistenceManager.js
@@ -0,0 +1,59 @@
+import {STORAGE_KEY} from './constants.js';
+
+/**
+ * Wraps localStorage access for star persistence.
+ */
+export class PersistenceManager {
+ /**
+ * @param {Storage} storage - Storage backend (defaults to browser localStorage).
+ */
+ constructor(storage = window.localStorage) {
+ this.storage = storage;
+ }
+
+ /**
+ * Loads the best star ratings per wave.
+ * @returns {Record}
+ */
+ loadStars() {
+ try {
+ const raw = this.storage.getItem(STORAGE_KEY);
+ if (!raw) {
+ return {};
+ }
+ const parsed = JSON.parse(raw);
+ if (typeof parsed !== 'object' || parsed === null) {
+ return {};
+ }
+ return parsed;
+ } catch (error) {
+ console.warn('Failed to load stars', error);
+ return {};
+ }
+ }
+
+ /**
+ * Persists the provided star ratings map.
+ * @param {Record} stars - Map of wave number to best star rating.
+ */
+ saveStars(stars) {
+ const serialized = JSON.stringify(stars);
+ this.storage.setItem(STORAGE_KEY, serialized);
+ }
+
+ /**
+ * Updates the stored rating for a wave, keeping the maximum.
+ * @param {number} wave - Wave index.
+ * @param {number} stars - Star rating.
+ * @returns {Record} Updated map.
+ */
+ updateStars(wave, stars) {
+ const current = this.loadStars();
+ const previous = current[wave] ?? 0;
+ if (stars > previous) {
+ current[wave] = stars;
+ this.saveStars(current);
+ }
+ return current;
+ }
+}
diff --git a/src/core/Telemetry.js b/src/core/Telemetry.js
new file mode 100644
index 0000000..66eb1e4
--- /dev/null
+++ b/src/core/Telemetry.js
@@ -0,0 +1,30 @@
+/**
+ * Abstract telemetry interface. Implementations can forward
+ * game analytics to any backend or simply log to the console.
+ */
+export class Telemetry {
+ /**
+ * Tracks an event.
+ * @param {string} _name - Event identifier.
+ * @param {object} _payload - Event payload.
+ */
+ trackEvent(_name, _payload = {}) {}
+}
+
+/**
+ * Default telemetry implementation which logs to the console.
+ */
+export class ConsoleTelemetry extends Telemetry {
+ /**
+ * @param {Console} logger - Console-like interface (defaults to global console).
+ */
+ constructor(logger = console) {
+ super();
+ this.logger = logger;
+ }
+
+ /** @inheritdoc */
+ trackEvent(name, payload = {}) {
+ this.logger.info(`[telemetry] ${name}`, payload);
+ }
+}
diff --git a/src/core/WaveGenerator.js b/src/core/WaveGenerator.js
new file mode 100644
index 0000000..2b12968
--- /dev/null
+++ b/src/core/WaveGenerator.js
@@ -0,0 +1,243 @@
+import {
+ BASE_FORWARD_GATE_COUNT,
+ SKIRMISH_RATIO,
+ BASE_PLAYER_ARMY,
+ MAX_OPERATION_FACTOR,
+ MAX_OPERATION_OFFSET,
+} from './constants.js';
+import {createRng, randomInt} from '../utils/random.js';
+
+const TIERS = {
+ BASIC: 'basic',
+ COMPOUND: 'compound',
+ ADVANCED: 'advanced',
+};
+
+function sanitize(value) {
+ if (!Number.isFinite(value)) {
+ return 0;
+ }
+ return Math.max(0, Math.floor(value));
+}
+
+function addOperation(offset) {
+ return {
+ label: `+${offset}`,
+ apply(value) {
+ return sanitize(value + offset);
+ },
+ };
+}
+
+function subtractOperation(offset) {
+ return {
+ label: `−${offset}`,
+ apply(value) {
+ return sanitize(value - offset);
+ },
+ };
+}
+
+function multiplyOperation(factor) {
+ return {
+ label: `×${factor}`,
+ apply(value) {
+ return sanitize(value * factor);
+ },
+ };
+}
+
+function divideOperation(divisor) {
+ return {
+ label: `÷${divisor}`,
+ apply(value) {
+ if (divisor === 0) {
+ return 0;
+ }
+ return sanitize(value / divisor);
+ },
+ };
+}
+
+function exponentOperation(offset, exponent) {
+ return {
+ label: `(${offset >= 0 ? `+${offset}` : offset})^${exponent}`,
+ apply(value) {
+ const adjusted = value + offset;
+ return sanitize(Math.pow(Math.max(adjusted, 0), exponent));
+ },
+ };
+}
+
+function compoundOperation(first, second) {
+ return {
+ label: `${first.label} ${second.label}`,
+ apply(value) {
+ return second.apply(first.apply(value));
+ },
+ };
+}
+
+function getTierForWave(waveNumber) {
+ if (waveNumber <= 5) {
+ return TIERS.BASIC;
+ }
+ if (waveNumber <= 10) {
+ return TIERS.COMPOUND;
+ }
+ return TIERS.ADVANCED;
+}
+
+function buildBasicOperation(rng) {
+ const choice = randomInt(rng, 0, 3);
+ switch (choice) {
+ case 0:
+ return addOperation(randomInt(rng, 2, MAX_OPERATION_OFFSET));
+ case 1:
+ return subtractOperation(randomInt(rng, 1, MAX_OPERATION_OFFSET - 2));
+ case 2:
+ return multiplyOperation(randomInt(rng, 2, MAX_OPERATION_FACTOR));
+ default:
+ return divideOperation(randomInt(rng, 2, MAX_OPERATION_FACTOR + 1));
+ }
+}
+
+function buildCompoundOperation(rng) {
+ const first = buildBasicOperation(rng);
+ const second = buildBasicOperation(rng);
+ return compoundOperation(first, second);
+}
+
+function buildAdvancedOperation(rng) {
+ const mode = randomInt(rng, 0, 2);
+ if (mode === 0) {
+ return compoundOperation(buildBasicOperation(rng), exponentOperation(randomInt(rng, -3, 3), 2));
+ }
+ if (mode === 1) {
+ return compoundOperation(addOperation(randomInt(rng, -3, 6)), multiplyOperation(randomInt(rng, 2, 4)));
+ }
+ const exponent = randomInt(rng, 2, 3);
+ return exponentOperation(randomInt(rng, -2, 4), exponent);
+}
+
+function buildOperationForTier(tier, rng) {
+ if (tier === TIERS.BASIC) {
+ return buildBasicOperation(rng);
+ }
+ if (tier === TIERS.COMPOUND) {
+ return buildCompoundOperation(rng);
+ }
+ return buildAdvancedOperation(rng);
+}
+
+function createGate({waveNumber, index, rng}) {
+ const tier = getTierForWave(waveNumber);
+ const left = buildOperationForTier(tier, rng);
+ const right = buildOperationForTier(tier, rng);
+ return {
+ id: `wave-${waveNumber}-gate-${index}`,
+ tier,
+ options: [
+ {id: 'left', ...left},
+ {id: 'right', ...right},
+ ],
+ };
+}
+
+function simulateForward(gates, initialArmy) {
+ let army = initialArmy;
+ const checkpoints = [];
+ gates.forEach((gate) => {
+ const left = gate.options[0].apply(army);
+ const right = gate.options[1].apply(army);
+ const best = left >= right ? {choice: 'left', value: left} : {choice: 'right', value: right};
+ const enemy = sanitize(best.value * SKIRMISH_RATIO);
+ const postBattle = sanitize(best.value - enemy);
+ checkpoints.push({
+ gateId: gate.id,
+ before: army,
+ left,
+ right,
+ bestChoice: best.choice,
+ bestValue: best.value,
+ enemy,
+ postBattle,
+ });
+ army = postBattle;
+ });
+ return {
+ checkpoints,
+ finalArmy: army,
+ };
+}
+
+function simulateRetreat(gates, initialArmy) {
+ let army = initialArmy;
+ const checkpoints = [];
+ gates.forEach((gate) => {
+ const left = gate.options[0].apply(army);
+ const right = gate.options[1].apply(army);
+ const best = left >= right ? {choice: 'left', value: left} : {choice: 'right', value: right};
+ checkpoints.push({
+ gateId: gate.id,
+ before: army,
+ left,
+ right,
+ bestChoice: best.choice,
+ bestValue: best.value,
+ });
+ army = best.value;
+ });
+ return {
+ checkpoints,
+ finalArmy: army,
+ };
+}
+
+/**
+ * Generates procedural wave layouts including gate operations and
+ * reference optimal paths used for scoring and encounter sizing.
+ */
+export class WaveGenerator {
+ /**
+ * @param {number} seed - Base seed for deterministic generation.
+ */
+ constructor(seed = 1337) {
+ this.seed = seed;
+ }
+
+ /**
+ * Creates the forward and retreat data for a wave.
+ * @param {number} waveNumber - Wave index starting at 1.
+ * @param {number} [startingArmy=BASE_PLAYER_ARMY] - Army size to feed into the first gate.
+ * @returns {object}
+ */
+ generate(waveNumber, startingArmy = BASE_PLAYER_ARMY) {
+ if (waveNumber < 1) {
+ throw new Error('waveNumber must be >= 1');
+ }
+ const gateCount = BASE_FORWARD_GATE_COUNT + (waveNumber - 1);
+ const rng = createRng(waveNumber * 97 + this.seed);
+ const forwardGates = Array.from({length: gateCount}, (_, index) =>
+ createGate({waveNumber, index, rng})
+ );
+ const retreatGates = Array.from({length: gateCount}, (_, index) =>
+ createGate({waveNumber, index: gateCount + index, rng})
+ );
+
+ const forward = simulateForward(forwardGates, startingArmy);
+ const retreat = simulateRetreat(retreatGates, forward.finalArmy);
+
+ return {
+ waveNumber,
+ gateCount,
+ startingArmy,
+ forwardGates,
+ retreatGates,
+ optimal: {
+ forward,
+ retreat,
+ },
+ };
+ }
+}
diff --git a/src/core/__tests__/BattleSystem.test.js b/src/core/__tests__/BattleSystem.test.js
new file mode 100644
index 0000000..f7e1f94
--- /dev/null
+++ b/src/core/__tests__/BattleSystem.test.js
@@ -0,0 +1,50 @@
+import {BattleSystem, calculateStarRating} from '../BattleSystem.js';
+import {FlockSystem} from '../FlockSystem.js';
+import {RETREAT_ARROW_RATIO, STAR_THRESHOLDS} from '../constants.js';
+
+describe('BattleSystem', () => {
+ let telemetry;
+ let flocks;
+ let battles;
+
+ beforeEach(() => {
+ telemetry = {trackEvent: jest.fn()};
+ flocks = new FlockSystem();
+ battles = new BattleSystem({flocks, telemetry});
+ });
+
+ it('reduces the player army during skirmishes', () => {
+ flocks.setPlayer(50);
+ const survivors = battles.resolveSkirmish(50, 20);
+ expect(survivors).toBe(30);
+ expect(flocks.player).toBe(30);
+ expect(telemetry.trackEvent).toHaveBeenCalled();
+ });
+
+ it('never returns negative survivors', () => {
+ const survivors = battles.resolveSkirmish(10, 25);
+ expect(survivors).toBe(0);
+ });
+
+ it('derives volley size from player army ratio', () => {
+ const volley = battles.calculateArrowVolley(120);
+ expect(volley).toBe(Math.floor(120 * RETREAT_ARROW_RATIO));
+ });
+});
+
+describe('calculateStarRating', () => {
+ it('returns zero stars when everyone is lost', () => {
+ expect(calculateStarRating(0, 100, STAR_THRESHOLDS)).toBe(0);
+ });
+
+ it('assigns the correct star bands', () => {
+ expect(calculateStarRating(30, 100, STAR_THRESHOLDS)).toBe(1);
+ expect(calculateStarRating(55, 100, STAR_THRESHOLDS)).toBe(2);
+ expect(calculateStarRating(80, 100, STAR_THRESHOLDS)).toBe(4);
+ expect(calculateStarRating(95, 100, STAR_THRESHOLDS)).toBe(5);
+ });
+
+ it('handles degenerate optimal totals', () => {
+ expect(calculateStarRating(10, 0, STAR_THRESHOLDS)).toBe(5);
+ });
+});
diff --git a/src/core/__tests__/PersistenceManager.test.js b/src/core/__tests__/PersistenceManager.test.js
new file mode 100644
index 0000000..032bc9b
--- /dev/null
+++ b/src/core/__tests__/PersistenceManager.test.js
@@ -0,0 +1,32 @@
+import {PersistenceManager} from '../PersistenceManager.js';
+import {STORAGE_KEY} from '../constants.js';
+
+describe('PersistenceManager', () => {
+ let manager;
+
+ beforeEach(() => {
+ window.localStorage.clear();
+ manager = new PersistenceManager(window.localStorage);
+ });
+
+ it('returns an empty map when nothing is stored', () => {
+ expect(manager.loadStars()).toEqual({});
+ });
+
+ it('saves and retrieves star ratings', () => {
+ const updated = manager.updateStars(2, 4);
+ expect(updated[2]).toBe(4);
+ const raw = window.localStorage.getItem(STORAGE_KEY);
+ expect(raw).toBe(JSON.stringify({2: 4}));
+ const loaded = manager.loadStars();
+ expect(loaded[2]).toBe(4);
+ });
+
+ it('preserves the higher star rating', () => {
+ manager.updateStars(3, 2);
+ manager.updateStars(3, 1);
+ expect(manager.loadStars()[3]).toBe(2);
+ manager.updateStars(3, 5);
+ expect(manager.loadStars()[3]).toBe(5);
+ });
+});
diff --git a/src/core/__tests__/Telemetry.test.js b/src/core/__tests__/Telemetry.test.js
new file mode 100644
index 0000000..e887681
--- /dev/null
+++ b/src/core/__tests__/Telemetry.test.js
@@ -0,0 +1,11 @@
+import {ConsoleTelemetry} from '../Telemetry.js';
+import {TELEMETRY_EVENTS} from '../constants.js';
+
+describe('ConsoleTelemetry', () => {
+ it('forwards events to the provided logger', () => {
+ const info = jest.fn();
+ const telemetry = new ConsoleTelemetry({info});
+ telemetry.trackEvent(TELEMETRY_EVENTS.WAVE_START, {wave: 2});
+ expect(info).toHaveBeenCalledWith('[telemetry] wave_start', {wave: 2});
+ });
+});
diff --git a/src/core/__tests__/WaveGenerator.test.js b/src/core/__tests__/WaveGenerator.test.js
new file mode 100644
index 0000000..a061e7d
--- /dev/null
+++ b/src/core/__tests__/WaveGenerator.test.js
@@ -0,0 +1,33 @@
+import {WaveGenerator} from '../WaveGenerator.js';
+import {BASE_FORWARD_GATE_COUNT, SKIRMISH_RATIO} from '../constants.js';
+
+const generator = new WaveGenerator(42);
+
+describe('WaveGenerator', () => {
+ it('increments gate count per wave', () => {
+ const wave1 = generator.generate(1, 50);
+ const wave3 = generator.generate(3, 50);
+ expect(wave1.gateCount).toBe(BASE_FORWARD_GATE_COUNT);
+ expect(wave3.gateCount).toBe(BASE_FORWARD_GATE_COUNT + 2);
+ });
+
+ it('selects the expected operation tiers for early waves', () => {
+ const wave1 = generator.generate(1, 60);
+ expect(wave1.forwardGates.every((gate) => gate.tier === 'basic')).toBe(true);
+ });
+
+ it('uses compound tiers for mid waves and advanced for late waves', () => {
+ const wave6 = generator.generate(6, 60);
+ const wave11 = generator.generate(11, 60);
+ expect(wave6.forwardGates.some((gate) => gate.tier === 'compound')).toBe(true);
+ expect(wave11.forwardGates.some((gate) => gate.tier === 'advanced')).toBe(true);
+ });
+
+ it('tracks optimal checkpoints and skirmish sizing', () => {
+ const wave = generator.generate(2, 60);
+ const checkpoint = wave.optimal.forward.checkpoints[0];
+ expect(checkpoint.enemy).toBe(Math.floor(checkpoint.bestValue * SKIRMISH_RATIO));
+ expect(checkpoint.postBattle).toBeLessThanOrEqual(checkpoint.bestValue);
+ expect(wave.optimal.forward.finalArmy).toBeGreaterThanOrEqual(0);
+ });
+});
diff --git a/src/core/constants.js b/src/core/constants.js
new file mode 100644
index 0000000..d3e8da7
--- /dev/null
+++ b/src/core/constants.js
@@ -0,0 +1,28 @@
+/**
+ * Shared game constants used across systems.
+ * @module core/constants
+ */
+
+export const BASE_FORWARD_GATE_COUNT = 5;
+export const SKIRMISH_RATIO = 0.8;
+export const BASE_PLAYER_ARMY = 60;
+export const RETREAT_ARROW_INTERVAL_MS = 800;
+export const RETREAT_ARROW_RATIO = 0.1;
+export const BASE_SPEED_MPS = 6;
+export const SURGE_SPEED_MPS = 8;
+export const STAR_THRESHOLDS = [0.4, 0.6, 0.75, 0.9];
+export const SHOWDOWN_ENEMY_RATIO = 1.4;
+export const MAX_OPERATION_FACTOR = 5;
+export const MAX_OPERATION_OFFSET = 12;
+
+export const STORAGE_KEY = 'math-marauders-stars';
+
+export const TELEMETRY_EVENTS = {
+ WAVE_START: 'wave_start',
+ GATE_RESOLVED: 'gate_resolved',
+ SKIRMISH: 'skirmish',
+ SHOWDOWN_START: 'showdown_start',
+ RETREAT_GATE: 'retreat_gate',
+ WAVE_COMPLETE: 'wave_complete',
+ WAVE_FAILED: 'wave_failed',
+};
diff --git a/src/index.js b/src/index.js
index ac0ebea..48691c3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1 +1,29 @@
-console.log("Hello from index.js!");
+import {WaveGenerator} from './core/WaveGenerator.js';
+import {GateSystem} from './core/GateSystem.js';
+import {BattleSystem} from './core/BattleSystem.js';
+import {FlockSystem} from './core/FlockSystem.js';
+import {UIManager} from './ui/UIManager.js';
+import {PersistenceManager} from './core/PersistenceManager.js';
+import {ConsoleTelemetry} from './core/Telemetry.js';
+import {GameController} from './core/GameController.js';
+
+const root = document.getElementById('app');
+const telemetry = new ConsoleTelemetry();
+const flocks = new FlockSystem();
+const generator = new WaveGenerator();
+const gates = new GateSystem(telemetry);
+const battles = new BattleSystem({flocks, telemetry});
+const persistence = new PersistenceManager(window.localStorage);
+const ui = new UIManager(root);
+
+const controller = new GameController({
+ generator,
+ gates,
+ battles,
+ flocks,
+ ui,
+ persistence,
+ telemetry,
+});
+
+controller.initialize();
diff --git a/src/index.test.js b/src/index.test.js
deleted file mode 100644
index e27abde..0000000
--- a/src/index.test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// This is a placeholder test file for index.js.
-// Since index.js currently only has a console.log,
-// a meaningful test isn't really possible without
-// more complex setup (e.g., mocking console.log or
-// integrating with an HTML environment if it manipulates the DOM).
-
-describe('Index', () => {
- test('placeholder test', () => {
- // Replace with actual tests once index.js has testable logic
- expect(true).toBe(true);
- });
-});
diff --git a/src/ui/UIManager.js b/src/ui/UIManager.js
new file mode 100644
index 0000000..5d7a64f
--- /dev/null
+++ b/src/ui/UIManager.js
@@ -0,0 +1,295 @@
+import {STAR_THRESHOLDS} from '../core/constants.js';
+
+const STAR_SYMBOL = '★';
+const EMPTY_STAR_SYMBOL = '☆';
+
+function formatStars(count) {
+ return `${STAR_SYMBOL.repeat(count)}${EMPTY_STAR_SYMBOL.repeat(5 - count)}`;
+}
+
+/**
+ * Lightweight HTML UI used to orchestrate the simulation without WebGL.
+ */
+export class UIManager {
+ /**
+ * @param {HTMLElement} root
+ */
+ constructor(root = document.body) {
+ this.root = root;
+ this.onPlay = () => {};
+ this.onGateCommit = () => {};
+ this.stateView = null;
+ this.popup = null;
+ this.slider = null;
+ this.leftLabel = null;
+ this.rightLabel = null;
+ this.choiceLabel = null;
+ this.gateButton = null;
+ this.phaseLabel = null;
+ this.waveLabel = null;
+ this.gateLabel = null;
+ this.enemyLabel = null;
+ this.playerLabel = null;
+ this.statusLabel = null;
+ this.starsLabel = null;
+ }
+
+ /**
+ * Builds the DOM structure and wires events.
+ * @param {object} params
+ * @param {() => void} params.onPlay
+ * @param {(choice: 'left'|'right') => void} params.onGateCommit
+ */
+ initialize({onPlay, onGateCommit}) {
+ this.onPlay = onPlay;
+ this.onGateCommit = onGateCommit;
+ this.root.innerHTML = '';
+ this.root.classList.add('math-marauders');
+ this.root.append(this.#buildLayout());
+ }
+
+ #buildLayout() {
+ const container = document.createElement('div');
+ container.className = 'hud';
+
+ const header = document.createElement('header');
+ header.className = 'hud__header';
+
+ this.waveLabel = document.createElement('span');
+ this.waveLabel.className = 'hud__wave';
+ this.waveLabel.textContent = 'Wave —';
+
+ this.phaseLabel = document.createElement('span');
+ this.phaseLabel.className = 'hud__phase';
+ this.phaseLabel.textContent = 'Phase: —';
+
+ header.append(this.waveLabel, this.phaseLabel);
+
+ const status = document.createElement('div');
+ status.className = 'hud__status';
+
+ this.playerLabel = document.createElement('div');
+ this.playerLabel.className = 'hud__army hud__army--player';
+ this.playerLabel.textContent = 'Player: 0';
+
+ this.enemyLabel = document.createElement('div');
+ this.enemyLabel.className = 'hud__army hud__army--enemy';
+ this.enemyLabel.textContent = 'Enemy: 0';
+
+ this.starsLabel = document.createElement('div');
+ this.starsLabel.className = 'hud__stars';
+ this.starsLabel.textContent = `${STAR_SYMBOL.repeat(5)}`;
+
+ status.append(this.playerLabel, this.enemyLabel, this.starsLabel);
+
+ const gatePanel = document.createElement('section');
+ gatePanel.className = 'hud__gate-panel';
+
+ this.gateLabel = document.createElement('h2');
+ this.gateLabel.className = 'hud__gate-title';
+ this.gateLabel.textContent = 'Gate';
+
+ const optionRow = document.createElement('div');
+ optionRow.className = 'hud__gate-options';
+
+ this.leftLabel = document.createElement('div');
+ this.leftLabel.className = 'hud__gate-option hud__gate-option--left';
+ this.leftLabel.textContent = 'Left';
+
+ this.rightLabel = document.createElement('div');
+ this.rightLabel.className = 'hud__gate-option hud__gate-option--right';
+ this.rightLabel.textContent = 'Right';
+
+ optionRow.append(this.leftLabel, this.rightLabel);
+
+ this.slider = document.createElement('input');
+ this.slider.type = 'range';
+ this.slider.min = '0';
+ this.slider.max = '100';
+ this.slider.value = '50';
+ this.slider.className = 'hud__gate-slider';
+
+ this.choiceLabel = document.createElement('div');
+ this.choiceLabel.className = 'hud__choice';
+ this.choiceLabel.textContent = 'Slide to choose';
+
+ this.gateButton = document.createElement('button');
+ this.gateButton.className = 'hud__button';
+ this.gateButton.type = 'button';
+ this.gateButton.textContent = 'Engage Gate';
+
+ this.statusLabel = document.createElement('p');
+ this.statusLabel.className = 'hud__log';
+
+ const playButton = document.createElement('button');
+ playButton.className = 'hud__button hud__button--primary';
+ playButton.type = 'button';
+ playButton.textContent = 'Play Wave';
+
+ gatePanel.append(
+ this.gateLabel,
+ optionRow,
+ this.slider,
+ this.choiceLabel,
+ this.gateButton,
+ playButton,
+ this.statusLabel
+ );
+
+ this.slider.addEventListener('input', () => this.#updateChoicePreview());
+ this.gateButton.addEventListener('click', () => this.#commitChoice());
+ playButton.addEventListener('click', () => this.onPlay());
+
+ container.append(header, status, gatePanel);
+
+ this.popup = document.createElement('div');
+ this.popup.className = 'hud__popup hidden';
+ container.append(this.popup);
+
+ this.#updateChoicePreview();
+ return container;
+ }
+
+ #commitChoice() {
+ const choice = this.#currentChoice();
+ this.onGateCommit(choice);
+ }
+
+ #updateChoicePreview() {
+ const choice = this.#currentChoice();
+ this.choiceLabel.textContent = `Current choice: ${choice.toUpperCase()}`;
+ this.leftLabel.classList.toggle('hud__gate-option--active', choice === 'left');
+ this.rightLabel.classList.toggle('hud__gate-option--active', choice === 'right');
+ }
+
+ #currentChoice() {
+ const value = Number(this.slider.value);
+ return value >= 50 ? 'right' : 'left';
+ }
+
+ /**
+ * Updates the current wave display.
+ * @param {number} wave
+ */
+ setWave(wave) {
+ this.waveLabel.textContent = `Wave ${wave}`;
+ }
+
+ /**
+ * Updates the phase label.
+ * @param {string} phase
+ */
+ setPhase(phase) {
+ this.phaseLabel.textContent = `Phase: ${phase}`;
+ }
+
+ /**
+ * Updates the gate counter and labels.
+ * @param {number} index
+ * @param {number} total
+ * @param {object} gate
+ */
+ showGate(index, total, gate) {
+ this.gateLabel.textContent = `Gate ${index + 1} / ${total}`;
+ this.leftLabel.textContent = gate.options[0].label;
+ this.rightLabel.textContent = gate.options[1].label;
+ this.slider.disabled = false;
+ this.gateButton.disabled = false;
+ }
+
+ /**
+ * Shows player/enemy counts.
+ * @param {{player: number, enemy: number}} state
+ */
+ updateFlocks(state) {
+ this.playerLabel.textContent = `Player: ${state.player}`;
+ this.enemyLabel.textContent = `Enemy: ${state.enemy}`;
+ }
+
+ /**
+ * Displays a status message.
+ * @param {string} message
+ */
+ setStatus(message) {
+ this.statusLabel.textContent = message;
+ }
+
+ /**
+ * Updates the star hint text.
+ * @param {number} stars
+ */
+ setStarPreview(stars) {
+ this.starsLabel.textContent = formatStars(stars);
+ }
+
+ /**
+ * Locks gate controls while animations resolve.
+ * @param {boolean} locked
+ */
+ lockGateControls(locked) {
+ this.slider.disabled = locked;
+ this.gateButton.disabled = locked;
+ }
+
+ /**
+ * Presents the summary popup.
+ * @param {object} params
+ * @param {string} params.title
+ * @param {number} params.stars
+ * @param {string} params.message
+ * @param {() => void} params.onNext
+ * @param {() => void} params.onRetry
+ */
+ showPopup({title, stars, message, onNext, onRetry, nextLabel = "Next", retryLabel = "Retry"}) {
+ this.popup.innerHTML = '';
+ this.popup.classList.remove('hidden');
+
+ const heading = document.createElement('h3');
+ heading.textContent = title;
+
+ const summary = document.createElement('p');
+ summary.textContent = message;
+
+ const starsEl = document.createElement('div');
+ starsEl.className = 'hud__popup-stars';
+ starsEl.textContent = formatStars(stars);
+
+ const buttons = document.createElement('div');
+ buttons.className = 'hud__popup-actions';
+
+ const nextButton = document.createElement('button');
+ nextButton.className = 'hud__button hud__button--primary';
+ nextButton.textContent = nextLabel;
+
+ const retryButton = document.createElement('button');
+ retryButton.className = 'hud__button';
+ retryButton.textContent = retryLabel;
+
+ nextButton.addEventListener('click', () => {
+ this.hidePopup();
+ onNext();
+ });
+ retryButton.addEventListener('click', () => {
+ this.hidePopup();
+ onRetry();
+ });
+
+ buttons.append(nextButton, retryButton);
+ this.popup.append(heading, starsEl, summary, buttons);
+ }
+
+ /**
+ * Hides the popup if visible.
+ */
+ hidePopup() {
+ this.popup.classList.add('hidden');
+ }
+
+ /**
+ * Displays the current star thresholds.
+ */
+ showThresholds() {
+ const thresholds = STAR_THRESHOLDS.map((t) => `${Math.round(t * 100)}%`).join(' / ');
+ this.setStatus(`Star thresholds: ${thresholds}`);
+ }
+}
diff --git a/src/utils/random.js b/src/utils/random.js
new file mode 100644
index 0000000..3cc3084
--- /dev/null
+++ b/src/utils/random.js
@@ -0,0 +1,34 @@
+/**
+ * Deterministic pseudo-random number generation helpers.
+ * The game uses seeded randomness so that a wave can be replayed
+ * consistently for testing and persistence.
+ *
+ * @module utils/random
+ */
+
+/**
+ * Mulberry32 pseudo random generator.
+ * @param {number} seed - Seed value.
+ * @returns {() => number} Generator that yields values in [0, 1).
+ */
+export function createRng(seed) {
+ let state = seed >>> 0;
+ return () => {
+ state += 0x6d2b79f5;
+ let t = state;
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ };
+}
+
+/**
+ * Selects a random integer in the provided range (inclusive).
+ * @param {() => number} rng - Random number generator.
+ * @param {number} min - Minimum value inclusive.
+ * @param {number} max - Maximum value inclusive.
+ * @returns {number}
+ */
+export function randomInt(rng, min, max) {
+ return Math.floor(rng() * (max - min + 1)) + min;
+}