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; +}