├── .gitignore ├── .mocharc.cjs ├── .npmignore ├── LICENSE ├── README.md ├── example └── all-modes.mjs ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── API │ └── apiv2.js ├── PerformancePoints │ ├── catch_ppv2.js │ ├── mania_ppv2.js │ ├── ppv2.js │ ├── std_ppv2.js │ └── taiko_ppv2.js ├── Shared │ ├── Helper.js │ └── Mods.js └── index.js └── test ├── apiv2 ├── auth.js └── mods.js ├── catch └── compare-api-tops.js ├── mania └── compare-api-tops.js ├── std └── compare-api-tops.js └── taiko └── compare-api-tops.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.mocharc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | timeout: 7500, 3 | recursive: true 4 | }; 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | .env 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LeaPhant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bubble tea 2 | # booba 3 | pure javascript osu! api wrapper and helper library for all modes (heavily W.I.P.) 4 | 5 | ### Roadmap 6 | - [ ] API wrapper 7 | - [ ] osu! api v1 8 | - [ ] osu! api v2 9 | - [x] pp calculation 10 | - [x] standard 11 | - [x] taiko 12 | - [x] catch 13 | - [x] mania (currently mismatches possible due to ongoing changes) 14 | - [ ] beatmap parsing 15 | - [ ] difficulty calculation 16 | - [ ] beatmap rendering 17 | - [ ] replay analyzation 18 | 19 | ### Installation 20 | ``` 21 | npm i booba 22 | ``` 23 | ### Usage Examples 24 | 25 | #### Calculating pp for a recent score from osu! api v1. 26 | ```JavaScript 27 | import fetch from 'node-fetch'; 28 | import { std_ppv2 } from 'booba'; 29 | 30 | const API_KEY = 'put api key here'; // osu! api v1 key 31 | const USER = '1023489'; 32 | 33 | (async () => { 34 | const response = await fetch(`https://osu.ppy.sh/api/get_user_recent?k=${API_KEY}&u=${USER}&limit=1`); 35 | const json = await response.json(); 36 | const [score] = json; 37 | 38 | const pp = new std_ppv2().setPerformance(score); 39 | 40 | console.log(await pp.compute()) 41 | /* => { 42 | aim: 108.36677305976224, 43 | speed: 121.39049498160061, 44 | fl: 514.2615576494688, 45 | acc: 48.88425340242263, 46 | total: 812.3689077733752 47 | } */ 48 | })(); 49 | ``` 50 | -------------------------------------------------------------------------------- /example/all-modes.mjs: -------------------------------------------------------------------------------- 1 | import { std_ppv2, taiko_ppv2, catch_ppv2, mania_ppv2 } from '../src/index.js'; 2 | import fetch from 'node-fetch'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | console.log('-- std --'); 7 | 8 | try { 9 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=124493&limit=1&m=0`); 10 | const json = await response.json(); 11 | const [score] = json; 12 | 13 | const pp = new std_ppv2().setPerformance(score); 14 | 15 | console.log(await pp.compute()); 16 | } catch(e) { 17 | console.error(e); 18 | } 19 | 20 | console.log('-- taiko --'); 21 | 22 | try { 23 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=165027&limit=1&m=1`); 24 | const json = await response.json(); 25 | const [score] = json; 26 | 27 | const pp = new taiko_ppv2().setPerformance(score); 28 | 29 | console.log(await pp.compute()); 30 | } catch(e) { 31 | console.error(e); 32 | } 33 | 34 | console.log('-- catch --'); 35 | 36 | try { 37 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=4158549&limit=1&m=2`); 38 | const json = await response.json(); 39 | const [score] = json; 40 | 41 | const pp = new catch_ppv2().setPerformance(score); 42 | 43 | console.log(await pp.compute()); 44 | } catch(e) { 45 | console.error(e); 46 | } 47 | 48 | console.log('-- mania --'); 49 | 50 | try { 51 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=259972&limit=1&m=3`); 52 | const json = await response.json(); 53 | const [score] = json; 54 | 55 | const pp = new mania_ppv2().setPerformance(score); 56 | 57 | console.log(await pp.compute()); 58 | } catch(e) { 59 | console.error(e); 60 | } 61 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "booba", 3 | "version": "0.0.16", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "booba", 9 | "version": "0.0.16", 10 | "license": "MIT", 11 | "dependencies": { 12 | "node-fetch": "^2.6.6" 13 | }, 14 | "devDependencies": { 15 | "chai": "^4.3.4", 16 | "chai-stats": "^0.3.0", 17 | "dotenv": "^10.0.0", 18 | "mocha": "^9.1.3", 19 | "rollup": "^2.59.0" 20 | } 21 | }, 22 | "node_modules/@ungap/promise-all-settled": { 23 | "version": "1.1.2", 24 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 25 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 26 | "dev": true 27 | }, 28 | "node_modules/ansi-colors": { 29 | "version": "4.1.1", 30 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 31 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 32 | "dev": true, 33 | "engines": { 34 | "node": ">=6" 35 | } 36 | }, 37 | "node_modules/ansi-regex": { 38 | "version": "5.0.1", 39 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 40 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 41 | "dev": true, 42 | "engines": { 43 | "node": ">=8" 44 | } 45 | }, 46 | "node_modules/ansi-styles": { 47 | "version": "4.3.0", 48 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 49 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 50 | "dev": true, 51 | "dependencies": { 52 | "color-convert": "^2.0.1" 53 | }, 54 | "engines": { 55 | "node": ">=8" 56 | }, 57 | "funding": { 58 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 59 | } 60 | }, 61 | "node_modules/anymatch": { 62 | "version": "3.1.2", 63 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 64 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 65 | "dev": true, 66 | "dependencies": { 67 | "normalize-path": "^3.0.0", 68 | "picomatch": "^2.0.4" 69 | }, 70 | "engines": { 71 | "node": ">= 8" 72 | } 73 | }, 74 | "node_modules/argparse": { 75 | "version": "2.0.1", 76 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 77 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 78 | "dev": true 79 | }, 80 | "node_modules/assertion-error": { 81 | "version": "1.1.0", 82 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 83 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 84 | "dev": true, 85 | "engines": { 86 | "node": "*" 87 | } 88 | }, 89 | "node_modules/balanced-match": { 90 | "version": "1.0.2", 91 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 92 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 93 | "dev": true 94 | }, 95 | "node_modules/binary-extensions": { 96 | "version": "2.2.0", 97 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 98 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 99 | "dev": true, 100 | "engines": { 101 | "node": ">=8" 102 | } 103 | }, 104 | "node_modules/brace-expansion": { 105 | "version": "1.1.11", 106 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 107 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 108 | "dev": true, 109 | "dependencies": { 110 | "balanced-match": "^1.0.0", 111 | "concat-map": "0.0.1" 112 | } 113 | }, 114 | "node_modules/braces": { 115 | "version": "3.0.2", 116 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 117 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 118 | "dev": true, 119 | "dependencies": { 120 | "fill-range": "^7.0.1" 121 | }, 122 | "engines": { 123 | "node": ">=8" 124 | } 125 | }, 126 | "node_modules/browser-stdout": { 127 | "version": "1.3.1", 128 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 129 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 130 | "dev": true 131 | }, 132 | "node_modules/camelcase": { 133 | "version": "6.2.0", 134 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", 135 | "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", 136 | "dev": true, 137 | "engines": { 138 | "node": ">=10" 139 | }, 140 | "funding": { 141 | "url": "https://github.com/sponsors/sindresorhus" 142 | } 143 | }, 144 | "node_modules/chai": { 145 | "version": "4.3.4", 146 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", 147 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", 148 | "dev": true, 149 | "dependencies": { 150 | "assertion-error": "^1.1.0", 151 | "check-error": "^1.0.2", 152 | "deep-eql": "^3.0.1", 153 | "get-func-name": "^2.0.0", 154 | "pathval": "^1.1.1", 155 | "type-detect": "^4.0.5" 156 | }, 157 | "engines": { 158 | "node": ">=4" 159 | } 160 | }, 161 | "node_modules/chai-stats": { 162 | "version": "0.3.0", 163 | "resolved": "https://registry.npmjs.org/chai-stats/-/chai-stats-0.3.0.tgz", 164 | "integrity": "sha1-pd38c2vX0Z7cr/+s/3s4VFRrIFY=", 165 | "dev": true, 166 | "engines": { 167 | "node": "*" 168 | } 169 | }, 170 | "node_modules/chalk": { 171 | "version": "4.1.2", 172 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 173 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 174 | "dev": true, 175 | "dependencies": { 176 | "ansi-styles": "^4.1.0", 177 | "supports-color": "^7.1.0" 178 | }, 179 | "engines": { 180 | "node": ">=10" 181 | }, 182 | "funding": { 183 | "url": "https://github.com/chalk/chalk?sponsor=1" 184 | } 185 | }, 186 | "node_modules/chalk/node_modules/supports-color": { 187 | "version": "7.2.0", 188 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 189 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 190 | "dev": true, 191 | "dependencies": { 192 | "has-flag": "^4.0.0" 193 | }, 194 | "engines": { 195 | "node": ">=8" 196 | } 197 | }, 198 | "node_modules/check-error": { 199 | "version": "1.0.2", 200 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 201 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 202 | "dev": true, 203 | "engines": { 204 | "node": "*" 205 | } 206 | }, 207 | "node_modules/chokidar": { 208 | "version": "3.5.2", 209 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 210 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 211 | "dev": true, 212 | "dependencies": { 213 | "anymatch": "~3.1.2", 214 | "braces": "~3.0.2", 215 | "glob-parent": "~5.1.2", 216 | "is-binary-path": "~2.1.0", 217 | "is-glob": "~4.0.1", 218 | "normalize-path": "~3.0.0", 219 | "readdirp": "~3.6.0" 220 | }, 221 | "engines": { 222 | "node": ">= 8.10.0" 223 | }, 224 | "optionalDependencies": { 225 | "fsevents": "~2.3.2" 226 | } 227 | }, 228 | "node_modules/cliui": { 229 | "version": "7.0.4", 230 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 231 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 232 | "dev": true, 233 | "dependencies": { 234 | "string-width": "^4.2.0", 235 | "strip-ansi": "^6.0.0", 236 | "wrap-ansi": "^7.0.0" 237 | } 238 | }, 239 | "node_modules/color-convert": { 240 | "version": "2.0.1", 241 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 242 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 243 | "dev": true, 244 | "dependencies": { 245 | "color-name": "~1.1.4" 246 | }, 247 | "engines": { 248 | "node": ">=7.0.0" 249 | } 250 | }, 251 | "node_modules/color-name": { 252 | "version": "1.1.4", 253 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 254 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 255 | "dev": true 256 | }, 257 | "node_modules/concat-map": { 258 | "version": "0.0.1", 259 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 260 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 261 | "dev": true 262 | }, 263 | "node_modules/debug": { 264 | "version": "4.3.2", 265 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", 266 | "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", 267 | "dev": true, 268 | "dependencies": { 269 | "ms": "2.1.2" 270 | }, 271 | "engines": { 272 | "node": ">=6.0" 273 | }, 274 | "peerDependenciesMeta": { 275 | "supports-color": { 276 | "optional": true 277 | } 278 | } 279 | }, 280 | "node_modules/debug/node_modules/ms": { 281 | "version": "2.1.2", 282 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 283 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 284 | "dev": true 285 | }, 286 | "node_modules/decamelize": { 287 | "version": "4.0.0", 288 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 289 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 290 | "dev": true, 291 | "engines": { 292 | "node": ">=10" 293 | }, 294 | "funding": { 295 | "url": "https://github.com/sponsors/sindresorhus" 296 | } 297 | }, 298 | "node_modules/deep-eql": { 299 | "version": "3.0.1", 300 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 301 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 302 | "dev": true, 303 | "dependencies": { 304 | "type-detect": "^4.0.0" 305 | }, 306 | "engines": { 307 | "node": ">=0.12" 308 | } 309 | }, 310 | "node_modules/diff": { 311 | "version": "5.0.0", 312 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 313 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 314 | "dev": true, 315 | "engines": { 316 | "node": ">=0.3.1" 317 | } 318 | }, 319 | "node_modules/dotenv": { 320 | "version": "10.0.0", 321 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 322 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", 323 | "dev": true, 324 | "engines": { 325 | "node": ">=10" 326 | } 327 | }, 328 | "node_modules/emoji-regex": { 329 | "version": "8.0.0", 330 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 331 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 332 | "dev": true 333 | }, 334 | "node_modules/escalade": { 335 | "version": "3.1.1", 336 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 337 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 338 | "dev": true, 339 | "engines": { 340 | "node": ">=6" 341 | } 342 | }, 343 | "node_modules/escape-string-regexp": { 344 | "version": "4.0.0", 345 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 346 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 347 | "dev": true, 348 | "engines": { 349 | "node": ">=10" 350 | }, 351 | "funding": { 352 | "url": "https://github.com/sponsors/sindresorhus" 353 | } 354 | }, 355 | "node_modules/fill-range": { 356 | "version": "7.0.1", 357 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 358 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 359 | "dev": true, 360 | "dependencies": { 361 | "to-regex-range": "^5.0.1" 362 | }, 363 | "engines": { 364 | "node": ">=8" 365 | } 366 | }, 367 | "node_modules/find-up": { 368 | "version": "5.0.0", 369 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 370 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 371 | "dev": true, 372 | "dependencies": { 373 | "locate-path": "^6.0.0", 374 | "path-exists": "^4.0.0" 375 | }, 376 | "engines": { 377 | "node": ">=10" 378 | }, 379 | "funding": { 380 | "url": "https://github.com/sponsors/sindresorhus" 381 | } 382 | }, 383 | "node_modules/flat": { 384 | "version": "5.0.2", 385 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 386 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 387 | "dev": true, 388 | "bin": { 389 | "flat": "cli.js" 390 | } 391 | }, 392 | "node_modules/fs.realpath": { 393 | "version": "1.0.0", 394 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 395 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 396 | "dev": true 397 | }, 398 | "node_modules/fsevents": { 399 | "version": "2.3.2", 400 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 401 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 402 | "dev": true, 403 | "hasInstallScript": true, 404 | "optional": true, 405 | "os": [ 406 | "darwin" 407 | ], 408 | "engines": { 409 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 410 | } 411 | }, 412 | "node_modules/get-caller-file": { 413 | "version": "2.0.5", 414 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 415 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 416 | "dev": true, 417 | "engines": { 418 | "node": "6.* || 8.* || >= 10.*" 419 | } 420 | }, 421 | "node_modules/get-func-name": { 422 | "version": "2.0.0", 423 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 424 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 425 | "dev": true, 426 | "engines": { 427 | "node": "*" 428 | } 429 | }, 430 | "node_modules/glob": { 431 | "version": "7.1.7", 432 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 433 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 434 | "dev": true, 435 | "dependencies": { 436 | "fs.realpath": "^1.0.0", 437 | "inflight": "^1.0.4", 438 | "inherits": "2", 439 | "minimatch": "^3.0.4", 440 | "once": "^1.3.0", 441 | "path-is-absolute": "^1.0.0" 442 | }, 443 | "engines": { 444 | "node": "*" 445 | }, 446 | "funding": { 447 | "url": "https://github.com/sponsors/isaacs" 448 | } 449 | }, 450 | "node_modules/glob-parent": { 451 | "version": "5.1.2", 452 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 453 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 454 | "dev": true, 455 | "dependencies": { 456 | "is-glob": "^4.0.1" 457 | }, 458 | "engines": { 459 | "node": ">= 6" 460 | } 461 | }, 462 | "node_modules/growl": { 463 | "version": "1.10.5", 464 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 465 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 466 | "dev": true, 467 | "engines": { 468 | "node": ">=4.x" 469 | } 470 | }, 471 | "node_modules/has-flag": { 472 | "version": "4.0.0", 473 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 474 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 475 | "dev": true, 476 | "engines": { 477 | "node": ">=8" 478 | } 479 | }, 480 | "node_modules/he": { 481 | "version": "1.2.0", 482 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 483 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 484 | "dev": true, 485 | "bin": { 486 | "he": "bin/he" 487 | } 488 | }, 489 | "node_modules/inflight": { 490 | "version": "1.0.6", 491 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 492 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 493 | "dev": true, 494 | "dependencies": { 495 | "once": "^1.3.0", 496 | "wrappy": "1" 497 | } 498 | }, 499 | "node_modules/inherits": { 500 | "version": "2.0.4", 501 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 502 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 503 | "dev": true 504 | }, 505 | "node_modules/is-binary-path": { 506 | "version": "2.1.0", 507 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 508 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 509 | "dev": true, 510 | "dependencies": { 511 | "binary-extensions": "^2.0.0" 512 | }, 513 | "engines": { 514 | "node": ">=8" 515 | } 516 | }, 517 | "node_modules/is-extglob": { 518 | "version": "2.1.1", 519 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 520 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 521 | "dev": true, 522 | "engines": { 523 | "node": ">=0.10.0" 524 | } 525 | }, 526 | "node_modules/is-fullwidth-code-point": { 527 | "version": "3.0.0", 528 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 529 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 530 | "dev": true, 531 | "engines": { 532 | "node": ">=8" 533 | } 534 | }, 535 | "node_modules/is-glob": { 536 | "version": "4.0.3", 537 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 538 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 539 | "dev": true, 540 | "dependencies": { 541 | "is-extglob": "^2.1.1" 542 | }, 543 | "engines": { 544 | "node": ">=0.10.0" 545 | } 546 | }, 547 | "node_modules/is-number": { 548 | "version": "7.0.0", 549 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 550 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 551 | "dev": true, 552 | "engines": { 553 | "node": ">=0.12.0" 554 | } 555 | }, 556 | "node_modules/is-plain-obj": { 557 | "version": "2.1.0", 558 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 559 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 560 | "dev": true, 561 | "engines": { 562 | "node": ">=8" 563 | } 564 | }, 565 | "node_modules/is-unicode-supported": { 566 | "version": "0.1.0", 567 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 568 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 569 | "dev": true, 570 | "engines": { 571 | "node": ">=10" 572 | }, 573 | "funding": { 574 | "url": "https://github.com/sponsors/sindresorhus" 575 | } 576 | }, 577 | "node_modules/isexe": { 578 | "version": "2.0.0", 579 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 580 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 581 | "dev": true 582 | }, 583 | "node_modules/js-yaml": { 584 | "version": "4.1.0", 585 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 586 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 587 | "dev": true, 588 | "dependencies": { 589 | "argparse": "^2.0.1" 590 | }, 591 | "bin": { 592 | "js-yaml": "bin/js-yaml.js" 593 | } 594 | }, 595 | "node_modules/locate-path": { 596 | "version": "6.0.0", 597 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 598 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 599 | "dev": true, 600 | "dependencies": { 601 | "p-locate": "^5.0.0" 602 | }, 603 | "engines": { 604 | "node": ">=10" 605 | }, 606 | "funding": { 607 | "url": "https://github.com/sponsors/sindresorhus" 608 | } 609 | }, 610 | "node_modules/log-symbols": { 611 | "version": "4.1.0", 612 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 613 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 614 | "dev": true, 615 | "dependencies": { 616 | "chalk": "^4.1.0", 617 | "is-unicode-supported": "^0.1.0" 618 | }, 619 | "engines": { 620 | "node": ">=10" 621 | }, 622 | "funding": { 623 | "url": "https://github.com/sponsors/sindresorhus" 624 | } 625 | }, 626 | "node_modules/minimatch": { 627 | "version": "3.0.4", 628 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 629 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 630 | "dev": true, 631 | "dependencies": { 632 | "brace-expansion": "^1.1.7" 633 | }, 634 | "engines": { 635 | "node": "*" 636 | } 637 | }, 638 | "node_modules/mocha": { 639 | "version": "9.1.3", 640 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", 641 | "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", 642 | "dev": true, 643 | "dependencies": { 644 | "@ungap/promise-all-settled": "1.1.2", 645 | "ansi-colors": "4.1.1", 646 | "browser-stdout": "1.3.1", 647 | "chokidar": "3.5.2", 648 | "debug": "4.3.2", 649 | "diff": "5.0.0", 650 | "escape-string-regexp": "4.0.0", 651 | "find-up": "5.0.0", 652 | "glob": "7.1.7", 653 | "growl": "1.10.5", 654 | "he": "1.2.0", 655 | "js-yaml": "4.1.0", 656 | "log-symbols": "4.1.0", 657 | "minimatch": "3.0.4", 658 | "ms": "2.1.3", 659 | "nanoid": "3.1.25", 660 | "serialize-javascript": "6.0.0", 661 | "strip-json-comments": "3.1.1", 662 | "supports-color": "8.1.1", 663 | "which": "2.0.2", 664 | "workerpool": "6.1.5", 665 | "yargs": "16.2.0", 666 | "yargs-parser": "20.2.4", 667 | "yargs-unparser": "2.0.0" 668 | }, 669 | "bin": { 670 | "_mocha": "bin/_mocha", 671 | "mocha": "bin/mocha" 672 | }, 673 | "engines": { 674 | "node": ">= 12.0.0" 675 | }, 676 | "funding": { 677 | "type": "opencollective", 678 | "url": "https://opencollective.com/mochajs" 679 | } 680 | }, 681 | "node_modules/ms": { 682 | "version": "2.1.3", 683 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 684 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 685 | "dev": true 686 | }, 687 | "node_modules/nanoid": { 688 | "version": "3.1.25", 689 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", 690 | "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", 691 | "dev": true, 692 | "bin": { 693 | "nanoid": "bin/nanoid.cjs" 694 | }, 695 | "engines": { 696 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 697 | } 698 | }, 699 | "node_modules/node-fetch": { 700 | "version": "2.6.6", 701 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", 702 | "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", 703 | "dependencies": { 704 | "whatwg-url": "^5.0.0" 705 | }, 706 | "engines": { 707 | "node": "4.x || >=6.0.0" 708 | } 709 | }, 710 | "node_modules/normalize-path": { 711 | "version": "3.0.0", 712 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 713 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 714 | "dev": true, 715 | "engines": { 716 | "node": ">=0.10.0" 717 | } 718 | }, 719 | "node_modules/once": { 720 | "version": "1.4.0", 721 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 722 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 723 | "dev": true, 724 | "dependencies": { 725 | "wrappy": "1" 726 | } 727 | }, 728 | "node_modules/p-limit": { 729 | "version": "3.1.0", 730 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 731 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 732 | "dev": true, 733 | "dependencies": { 734 | "yocto-queue": "^0.1.0" 735 | }, 736 | "engines": { 737 | "node": ">=10" 738 | }, 739 | "funding": { 740 | "url": "https://github.com/sponsors/sindresorhus" 741 | } 742 | }, 743 | "node_modules/p-locate": { 744 | "version": "5.0.0", 745 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 746 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 747 | "dev": true, 748 | "dependencies": { 749 | "p-limit": "^3.0.2" 750 | }, 751 | "engines": { 752 | "node": ">=10" 753 | }, 754 | "funding": { 755 | "url": "https://github.com/sponsors/sindresorhus" 756 | } 757 | }, 758 | "node_modules/path-exists": { 759 | "version": "4.0.0", 760 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 761 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 762 | "dev": true, 763 | "engines": { 764 | "node": ">=8" 765 | } 766 | }, 767 | "node_modules/path-is-absolute": { 768 | "version": "1.0.1", 769 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 770 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 771 | "dev": true, 772 | "engines": { 773 | "node": ">=0.10.0" 774 | } 775 | }, 776 | "node_modules/pathval": { 777 | "version": "1.1.1", 778 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 779 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 780 | "dev": true, 781 | "engines": { 782 | "node": "*" 783 | } 784 | }, 785 | "node_modules/picomatch": { 786 | "version": "2.3.0", 787 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 788 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 789 | "dev": true, 790 | "engines": { 791 | "node": ">=8.6" 792 | }, 793 | "funding": { 794 | "url": "https://github.com/sponsors/jonschlinkert" 795 | } 796 | }, 797 | "node_modules/randombytes": { 798 | "version": "2.1.0", 799 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 800 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 801 | "dev": true, 802 | "dependencies": { 803 | "safe-buffer": "^5.1.0" 804 | } 805 | }, 806 | "node_modules/readdirp": { 807 | "version": "3.6.0", 808 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 809 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 810 | "dev": true, 811 | "dependencies": { 812 | "picomatch": "^2.2.1" 813 | }, 814 | "engines": { 815 | "node": ">=8.10.0" 816 | } 817 | }, 818 | "node_modules/require-directory": { 819 | "version": "2.1.1", 820 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 821 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 822 | "dev": true, 823 | "engines": { 824 | "node": ">=0.10.0" 825 | } 826 | }, 827 | "node_modules/rollup": { 828 | "version": "2.59.0", 829 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.59.0.tgz", 830 | "integrity": "sha512-l7s90JQhCQ6JyZjKgo7Lq1dKh2RxatOM+Jr6a9F7WbS9WgKbocyUSeLmZl8evAse7y96Ae98L2k1cBOwWD8nHw==", 831 | "dev": true, 832 | "bin": { 833 | "rollup": "dist/bin/rollup" 834 | }, 835 | "engines": { 836 | "node": ">=10.0.0" 837 | }, 838 | "optionalDependencies": { 839 | "fsevents": "~2.3.2" 840 | } 841 | }, 842 | "node_modules/safe-buffer": { 843 | "version": "5.2.1", 844 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 845 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 846 | "dev": true, 847 | "funding": [ 848 | { 849 | "type": "github", 850 | "url": "https://github.com/sponsors/feross" 851 | }, 852 | { 853 | "type": "patreon", 854 | "url": "https://www.patreon.com/feross" 855 | }, 856 | { 857 | "type": "consulting", 858 | "url": "https://feross.org/support" 859 | } 860 | ] 861 | }, 862 | "node_modules/serialize-javascript": { 863 | "version": "6.0.0", 864 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 865 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 866 | "dev": true, 867 | "dependencies": { 868 | "randombytes": "^2.1.0" 869 | } 870 | }, 871 | "node_modules/string-width": { 872 | "version": "4.2.3", 873 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 874 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 875 | "dev": true, 876 | "dependencies": { 877 | "emoji-regex": "^8.0.0", 878 | "is-fullwidth-code-point": "^3.0.0", 879 | "strip-ansi": "^6.0.1" 880 | }, 881 | "engines": { 882 | "node": ">=8" 883 | } 884 | }, 885 | "node_modules/strip-ansi": { 886 | "version": "6.0.1", 887 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 888 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 889 | "dev": true, 890 | "dependencies": { 891 | "ansi-regex": "^5.0.1" 892 | }, 893 | "engines": { 894 | "node": ">=8" 895 | } 896 | }, 897 | "node_modules/strip-json-comments": { 898 | "version": "3.1.1", 899 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 900 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 901 | "dev": true, 902 | "engines": { 903 | "node": ">=8" 904 | }, 905 | "funding": { 906 | "url": "https://github.com/sponsors/sindresorhus" 907 | } 908 | }, 909 | "node_modules/supports-color": { 910 | "version": "8.1.1", 911 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 912 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 913 | "dev": true, 914 | "dependencies": { 915 | "has-flag": "^4.0.0" 916 | }, 917 | "engines": { 918 | "node": ">=10" 919 | }, 920 | "funding": { 921 | "url": "https://github.com/chalk/supports-color?sponsor=1" 922 | } 923 | }, 924 | "node_modules/to-regex-range": { 925 | "version": "5.0.1", 926 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 927 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 928 | "dev": true, 929 | "dependencies": { 930 | "is-number": "^7.0.0" 931 | }, 932 | "engines": { 933 | "node": ">=8.0" 934 | } 935 | }, 936 | "node_modules/tr46": { 937 | "version": "0.0.3", 938 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 939 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 940 | }, 941 | "node_modules/type-detect": { 942 | "version": "4.0.8", 943 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 944 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 945 | "dev": true, 946 | "engines": { 947 | "node": ">=4" 948 | } 949 | }, 950 | "node_modules/webidl-conversions": { 951 | "version": "3.0.1", 952 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 953 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 954 | }, 955 | "node_modules/whatwg-url": { 956 | "version": "5.0.0", 957 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 958 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 959 | "dependencies": { 960 | "tr46": "~0.0.3", 961 | "webidl-conversions": "^3.0.0" 962 | } 963 | }, 964 | "node_modules/which": { 965 | "version": "2.0.2", 966 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 967 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 968 | "dev": true, 969 | "dependencies": { 970 | "isexe": "^2.0.0" 971 | }, 972 | "bin": { 973 | "node-which": "bin/node-which" 974 | }, 975 | "engines": { 976 | "node": ">= 8" 977 | } 978 | }, 979 | "node_modules/workerpool": { 980 | "version": "6.1.5", 981 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", 982 | "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", 983 | "dev": true 984 | }, 985 | "node_modules/wrap-ansi": { 986 | "version": "7.0.0", 987 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 988 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 989 | "dev": true, 990 | "dependencies": { 991 | "ansi-styles": "^4.0.0", 992 | "string-width": "^4.1.0", 993 | "strip-ansi": "^6.0.0" 994 | }, 995 | "engines": { 996 | "node": ">=10" 997 | }, 998 | "funding": { 999 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1000 | } 1001 | }, 1002 | "node_modules/wrappy": { 1003 | "version": "1.0.2", 1004 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1005 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1006 | "dev": true 1007 | }, 1008 | "node_modules/y18n": { 1009 | "version": "5.0.8", 1010 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1011 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1012 | "dev": true, 1013 | "engines": { 1014 | "node": ">=10" 1015 | } 1016 | }, 1017 | "node_modules/yargs": { 1018 | "version": "16.2.0", 1019 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1020 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1021 | "dev": true, 1022 | "dependencies": { 1023 | "cliui": "^7.0.2", 1024 | "escalade": "^3.1.1", 1025 | "get-caller-file": "^2.0.5", 1026 | "require-directory": "^2.1.1", 1027 | "string-width": "^4.2.0", 1028 | "y18n": "^5.0.5", 1029 | "yargs-parser": "^20.2.2" 1030 | }, 1031 | "engines": { 1032 | "node": ">=10" 1033 | } 1034 | }, 1035 | "node_modules/yargs-parser": { 1036 | "version": "20.2.4", 1037 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1038 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1039 | "dev": true, 1040 | "engines": { 1041 | "node": ">=10" 1042 | } 1043 | }, 1044 | "node_modules/yargs-unparser": { 1045 | "version": "2.0.0", 1046 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1047 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1048 | "dev": true, 1049 | "dependencies": { 1050 | "camelcase": "^6.0.0", 1051 | "decamelize": "^4.0.0", 1052 | "flat": "^5.0.2", 1053 | "is-plain-obj": "^2.1.0" 1054 | }, 1055 | "engines": { 1056 | "node": ">=10" 1057 | } 1058 | }, 1059 | "node_modules/yocto-queue": { 1060 | "version": "0.1.0", 1061 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1062 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1063 | "dev": true, 1064 | "engines": { 1065 | "node": ">=10" 1066 | }, 1067 | "funding": { 1068 | "url": "https://github.com/sponsors/sindresorhus" 1069 | } 1070 | } 1071 | }, 1072 | "dependencies": { 1073 | "@ungap/promise-all-settled": { 1074 | "version": "1.1.2", 1075 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 1076 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 1077 | "dev": true 1078 | }, 1079 | "ansi-colors": { 1080 | "version": "4.1.1", 1081 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 1082 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 1083 | "dev": true 1084 | }, 1085 | "ansi-regex": { 1086 | "version": "5.0.1", 1087 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1088 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1089 | "dev": true 1090 | }, 1091 | "ansi-styles": { 1092 | "version": "4.3.0", 1093 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1094 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1095 | "dev": true, 1096 | "requires": { 1097 | "color-convert": "^2.0.1" 1098 | } 1099 | }, 1100 | "anymatch": { 1101 | "version": "3.1.2", 1102 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 1103 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 1104 | "dev": true, 1105 | "requires": { 1106 | "normalize-path": "^3.0.0", 1107 | "picomatch": "^2.0.4" 1108 | } 1109 | }, 1110 | "argparse": { 1111 | "version": "2.0.1", 1112 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1113 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1114 | "dev": true 1115 | }, 1116 | "assertion-error": { 1117 | "version": "1.1.0", 1118 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 1119 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 1120 | "dev": true 1121 | }, 1122 | "balanced-match": { 1123 | "version": "1.0.2", 1124 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1125 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1126 | "dev": true 1127 | }, 1128 | "binary-extensions": { 1129 | "version": "2.2.0", 1130 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 1131 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 1132 | "dev": true 1133 | }, 1134 | "brace-expansion": { 1135 | "version": "1.1.11", 1136 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1137 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1138 | "dev": true, 1139 | "requires": { 1140 | "balanced-match": "^1.0.0", 1141 | "concat-map": "0.0.1" 1142 | } 1143 | }, 1144 | "braces": { 1145 | "version": "3.0.2", 1146 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 1147 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 1148 | "dev": true, 1149 | "requires": { 1150 | "fill-range": "^7.0.1" 1151 | } 1152 | }, 1153 | "browser-stdout": { 1154 | "version": "1.3.1", 1155 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 1156 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 1157 | "dev": true 1158 | }, 1159 | "camelcase": { 1160 | "version": "6.2.0", 1161 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", 1162 | "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", 1163 | "dev": true 1164 | }, 1165 | "chai": { 1166 | "version": "4.3.4", 1167 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", 1168 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", 1169 | "dev": true, 1170 | "requires": { 1171 | "assertion-error": "^1.1.0", 1172 | "check-error": "^1.0.2", 1173 | "deep-eql": "^3.0.1", 1174 | "get-func-name": "^2.0.0", 1175 | "pathval": "^1.1.1", 1176 | "type-detect": "^4.0.5" 1177 | } 1178 | }, 1179 | "chai-stats": { 1180 | "version": "0.3.0", 1181 | "resolved": "https://registry.npmjs.org/chai-stats/-/chai-stats-0.3.0.tgz", 1182 | "integrity": "sha1-pd38c2vX0Z7cr/+s/3s4VFRrIFY=", 1183 | "dev": true 1184 | }, 1185 | "chalk": { 1186 | "version": "4.1.2", 1187 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1188 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1189 | "dev": true, 1190 | "requires": { 1191 | "ansi-styles": "^4.1.0", 1192 | "supports-color": "^7.1.0" 1193 | }, 1194 | "dependencies": { 1195 | "supports-color": { 1196 | "version": "7.2.0", 1197 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1198 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1199 | "dev": true, 1200 | "requires": { 1201 | "has-flag": "^4.0.0" 1202 | } 1203 | } 1204 | } 1205 | }, 1206 | "check-error": { 1207 | "version": "1.0.2", 1208 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 1209 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 1210 | "dev": true 1211 | }, 1212 | "chokidar": { 1213 | "version": "3.5.2", 1214 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 1215 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 1216 | "dev": true, 1217 | "requires": { 1218 | "anymatch": "~3.1.2", 1219 | "braces": "~3.0.2", 1220 | "fsevents": "~2.3.2", 1221 | "glob-parent": "~5.1.2", 1222 | "is-binary-path": "~2.1.0", 1223 | "is-glob": "~4.0.1", 1224 | "normalize-path": "~3.0.0", 1225 | "readdirp": "~3.6.0" 1226 | } 1227 | }, 1228 | "cliui": { 1229 | "version": "7.0.4", 1230 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 1231 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 1232 | "dev": true, 1233 | "requires": { 1234 | "string-width": "^4.2.0", 1235 | "strip-ansi": "^6.0.0", 1236 | "wrap-ansi": "^7.0.0" 1237 | } 1238 | }, 1239 | "color-convert": { 1240 | "version": "2.0.1", 1241 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1242 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1243 | "dev": true, 1244 | "requires": { 1245 | "color-name": "~1.1.4" 1246 | } 1247 | }, 1248 | "color-name": { 1249 | "version": "1.1.4", 1250 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1251 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1252 | "dev": true 1253 | }, 1254 | "concat-map": { 1255 | "version": "0.0.1", 1256 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1257 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 1258 | "dev": true 1259 | }, 1260 | "debug": { 1261 | "version": "4.3.2", 1262 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", 1263 | "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", 1264 | "dev": true, 1265 | "requires": { 1266 | "ms": "2.1.2" 1267 | }, 1268 | "dependencies": { 1269 | "ms": { 1270 | "version": "2.1.2", 1271 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1272 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1273 | "dev": true 1274 | } 1275 | } 1276 | }, 1277 | "decamelize": { 1278 | "version": "4.0.0", 1279 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 1280 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 1281 | "dev": true 1282 | }, 1283 | "deep-eql": { 1284 | "version": "3.0.1", 1285 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 1286 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 1287 | "dev": true, 1288 | "requires": { 1289 | "type-detect": "^4.0.0" 1290 | } 1291 | }, 1292 | "diff": { 1293 | "version": "5.0.0", 1294 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 1295 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 1296 | "dev": true 1297 | }, 1298 | "dotenv": { 1299 | "version": "10.0.0", 1300 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 1301 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", 1302 | "dev": true 1303 | }, 1304 | "emoji-regex": { 1305 | "version": "8.0.0", 1306 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1307 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1308 | "dev": true 1309 | }, 1310 | "escalade": { 1311 | "version": "3.1.1", 1312 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1313 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 1314 | "dev": true 1315 | }, 1316 | "escape-string-regexp": { 1317 | "version": "4.0.0", 1318 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1319 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1320 | "dev": true 1321 | }, 1322 | "fill-range": { 1323 | "version": "7.0.1", 1324 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 1325 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 1326 | "dev": true, 1327 | "requires": { 1328 | "to-regex-range": "^5.0.1" 1329 | } 1330 | }, 1331 | "find-up": { 1332 | "version": "5.0.0", 1333 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1334 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1335 | "dev": true, 1336 | "requires": { 1337 | "locate-path": "^6.0.0", 1338 | "path-exists": "^4.0.0" 1339 | } 1340 | }, 1341 | "flat": { 1342 | "version": "5.0.2", 1343 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 1344 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 1345 | "dev": true 1346 | }, 1347 | "fs.realpath": { 1348 | "version": "1.0.0", 1349 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1350 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 1351 | "dev": true 1352 | }, 1353 | "fsevents": { 1354 | "version": "2.3.2", 1355 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1356 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1357 | "dev": true, 1358 | "optional": true 1359 | }, 1360 | "get-caller-file": { 1361 | "version": "2.0.5", 1362 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1363 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 1364 | "dev": true 1365 | }, 1366 | "get-func-name": { 1367 | "version": "2.0.0", 1368 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 1369 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 1370 | "dev": true 1371 | }, 1372 | "glob": { 1373 | "version": "7.1.7", 1374 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 1375 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 1376 | "dev": true, 1377 | "requires": { 1378 | "fs.realpath": "^1.0.0", 1379 | "inflight": "^1.0.4", 1380 | "inherits": "2", 1381 | "minimatch": "^3.0.4", 1382 | "once": "^1.3.0", 1383 | "path-is-absolute": "^1.0.0" 1384 | } 1385 | }, 1386 | "glob-parent": { 1387 | "version": "5.1.2", 1388 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1389 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1390 | "dev": true, 1391 | "requires": { 1392 | "is-glob": "^4.0.1" 1393 | } 1394 | }, 1395 | "growl": { 1396 | "version": "1.10.5", 1397 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 1398 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 1399 | "dev": true 1400 | }, 1401 | "has-flag": { 1402 | "version": "4.0.0", 1403 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1404 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1405 | "dev": true 1406 | }, 1407 | "he": { 1408 | "version": "1.2.0", 1409 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1410 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1411 | "dev": true 1412 | }, 1413 | "inflight": { 1414 | "version": "1.0.6", 1415 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1416 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 1417 | "dev": true, 1418 | "requires": { 1419 | "once": "^1.3.0", 1420 | "wrappy": "1" 1421 | } 1422 | }, 1423 | "inherits": { 1424 | "version": "2.0.4", 1425 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1426 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1427 | "dev": true 1428 | }, 1429 | "is-binary-path": { 1430 | "version": "2.1.0", 1431 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1432 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1433 | "dev": true, 1434 | "requires": { 1435 | "binary-extensions": "^2.0.0" 1436 | } 1437 | }, 1438 | "is-extglob": { 1439 | "version": "2.1.1", 1440 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1441 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 1442 | "dev": true 1443 | }, 1444 | "is-fullwidth-code-point": { 1445 | "version": "3.0.0", 1446 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1447 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1448 | "dev": true 1449 | }, 1450 | "is-glob": { 1451 | "version": "4.0.3", 1452 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1453 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1454 | "dev": true, 1455 | "requires": { 1456 | "is-extglob": "^2.1.1" 1457 | } 1458 | }, 1459 | "is-number": { 1460 | "version": "7.0.0", 1461 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1462 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1463 | "dev": true 1464 | }, 1465 | "is-plain-obj": { 1466 | "version": "2.1.0", 1467 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 1468 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 1469 | "dev": true 1470 | }, 1471 | "is-unicode-supported": { 1472 | "version": "0.1.0", 1473 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1474 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1475 | "dev": true 1476 | }, 1477 | "isexe": { 1478 | "version": "2.0.0", 1479 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1480 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 1481 | "dev": true 1482 | }, 1483 | "js-yaml": { 1484 | "version": "4.1.0", 1485 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1486 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1487 | "dev": true, 1488 | "requires": { 1489 | "argparse": "^2.0.1" 1490 | } 1491 | }, 1492 | "locate-path": { 1493 | "version": "6.0.0", 1494 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1495 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1496 | "dev": true, 1497 | "requires": { 1498 | "p-locate": "^5.0.0" 1499 | } 1500 | }, 1501 | "log-symbols": { 1502 | "version": "4.1.0", 1503 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1504 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1505 | "dev": true, 1506 | "requires": { 1507 | "chalk": "^4.1.0", 1508 | "is-unicode-supported": "^0.1.0" 1509 | } 1510 | }, 1511 | "minimatch": { 1512 | "version": "3.0.4", 1513 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1514 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1515 | "dev": true, 1516 | "requires": { 1517 | "brace-expansion": "^1.1.7" 1518 | } 1519 | }, 1520 | "mocha": { 1521 | "version": "9.1.3", 1522 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", 1523 | "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", 1524 | "dev": true, 1525 | "requires": { 1526 | "@ungap/promise-all-settled": "1.1.2", 1527 | "ansi-colors": "4.1.1", 1528 | "browser-stdout": "1.3.1", 1529 | "chokidar": "3.5.2", 1530 | "debug": "4.3.2", 1531 | "diff": "5.0.0", 1532 | "escape-string-regexp": "4.0.0", 1533 | "find-up": "5.0.0", 1534 | "glob": "7.1.7", 1535 | "growl": "1.10.5", 1536 | "he": "1.2.0", 1537 | "js-yaml": "4.1.0", 1538 | "log-symbols": "4.1.0", 1539 | "minimatch": "3.0.4", 1540 | "ms": "2.1.3", 1541 | "nanoid": "3.1.25", 1542 | "serialize-javascript": "6.0.0", 1543 | "strip-json-comments": "3.1.1", 1544 | "supports-color": "8.1.1", 1545 | "which": "2.0.2", 1546 | "workerpool": "6.1.5", 1547 | "yargs": "16.2.0", 1548 | "yargs-parser": "20.2.4", 1549 | "yargs-unparser": "2.0.0" 1550 | } 1551 | }, 1552 | "ms": { 1553 | "version": "2.1.3", 1554 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1555 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1556 | "dev": true 1557 | }, 1558 | "nanoid": { 1559 | "version": "3.1.25", 1560 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", 1561 | "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", 1562 | "dev": true 1563 | }, 1564 | "node-fetch": { 1565 | "version": "2.6.6", 1566 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", 1567 | "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", 1568 | "requires": { 1569 | "whatwg-url": "^5.0.0" 1570 | } 1571 | }, 1572 | "normalize-path": { 1573 | "version": "3.0.0", 1574 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1575 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1576 | "dev": true 1577 | }, 1578 | "once": { 1579 | "version": "1.4.0", 1580 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1581 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1582 | "dev": true, 1583 | "requires": { 1584 | "wrappy": "1" 1585 | } 1586 | }, 1587 | "p-limit": { 1588 | "version": "3.1.0", 1589 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1590 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1591 | "dev": true, 1592 | "requires": { 1593 | "yocto-queue": "^0.1.0" 1594 | } 1595 | }, 1596 | "p-locate": { 1597 | "version": "5.0.0", 1598 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1599 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1600 | "dev": true, 1601 | "requires": { 1602 | "p-limit": "^3.0.2" 1603 | } 1604 | }, 1605 | "path-exists": { 1606 | "version": "4.0.0", 1607 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1608 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1609 | "dev": true 1610 | }, 1611 | "path-is-absolute": { 1612 | "version": "1.0.1", 1613 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1614 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1615 | "dev": true 1616 | }, 1617 | "pathval": { 1618 | "version": "1.1.1", 1619 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 1620 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 1621 | "dev": true 1622 | }, 1623 | "picomatch": { 1624 | "version": "2.3.0", 1625 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 1626 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 1627 | "dev": true 1628 | }, 1629 | "randombytes": { 1630 | "version": "2.1.0", 1631 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1632 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1633 | "dev": true, 1634 | "requires": { 1635 | "safe-buffer": "^5.1.0" 1636 | } 1637 | }, 1638 | "readdirp": { 1639 | "version": "3.6.0", 1640 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1641 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1642 | "dev": true, 1643 | "requires": { 1644 | "picomatch": "^2.2.1" 1645 | } 1646 | }, 1647 | "require-directory": { 1648 | "version": "2.1.1", 1649 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1650 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 1651 | "dev": true 1652 | }, 1653 | "rollup": { 1654 | "version": "2.59.0", 1655 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.59.0.tgz", 1656 | "integrity": "sha512-l7s90JQhCQ6JyZjKgo7Lq1dKh2RxatOM+Jr6a9F7WbS9WgKbocyUSeLmZl8evAse7y96Ae98L2k1cBOwWD8nHw==", 1657 | "dev": true, 1658 | "requires": { 1659 | "fsevents": "~2.3.2" 1660 | } 1661 | }, 1662 | "safe-buffer": { 1663 | "version": "5.2.1", 1664 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1665 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1666 | "dev": true 1667 | }, 1668 | "serialize-javascript": { 1669 | "version": "6.0.0", 1670 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 1671 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 1672 | "dev": true, 1673 | "requires": { 1674 | "randombytes": "^2.1.0" 1675 | } 1676 | }, 1677 | "string-width": { 1678 | "version": "4.2.3", 1679 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1680 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1681 | "dev": true, 1682 | "requires": { 1683 | "emoji-regex": "^8.0.0", 1684 | "is-fullwidth-code-point": "^3.0.0", 1685 | "strip-ansi": "^6.0.1" 1686 | } 1687 | }, 1688 | "strip-ansi": { 1689 | "version": "6.0.1", 1690 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1691 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1692 | "dev": true, 1693 | "requires": { 1694 | "ansi-regex": "^5.0.1" 1695 | } 1696 | }, 1697 | "strip-json-comments": { 1698 | "version": "3.1.1", 1699 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1700 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1701 | "dev": true 1702 | }, 1703 | "supports-color": { 1704 | "version": "8.1.1", 1705 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1706 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1707 | "dev": true, 1708 | "requires": { 1709 | "has-flag": "^4.0.0" 1710 | } 1711 | }, 1712 | "to-regex-range": { 1713 | "version": "5.0.1", 1714 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1715 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1716 | "dev": true, 1717 | "requires": { 1718 | "is-number": "^7.0.0" 1719 | } 1720 | }, 1721 | "tr46": { 1722 | "version": "0.0.3", 1723 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1724 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 1725 | }, 1726 | "type-detect": { 1727 | "version": "4.0.8", 1728 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1729 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1730 | "dev": true 1731 | }, 1732 | "webidl-conversions": { 1733 | "version": "3.0.1", 1734 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1735 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 1736 | }, 1737 | "whatwg-url": { 1738 | "version": "5.0.0", 1739 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1740 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 1741 | "requires": { 1742 | "tr46": "~0.0.3", 1743 | "webidl-conversions": "^3.0.0" 1744 | } 1745 | }, 1746 | "which": { 1747 | "version": "2.0.2", 1748 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1749 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1750 | "dev": true, 1751 | "requires": { 1752 | "isexe": "^2.0.0" 1753 | } 1754 | }, 1755 | "workerpool": { 1756 | "version": "6.1.5", 1757 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", 1758 | "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", 1759 | "dev": true 1760 | }, 1761 | "wrap-ansi": { 1762 | "version": "7.0.0", 1763 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1764 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1765 | "dev": true, 1766 | "requires": { 1767 | "ansi-styles": "^4.0.0", 1768 | "string-width": "^4.1.0", 1769 | "strip-ansi": "^6.0.0" 1770 | } 1771 | }, 1772 | "wrappy": { 1773 | "version": "1.0.2", 1774 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1775 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1776 | "dev": true 1777 | }, 1778 | "y18n": { 1779 | "version": "5.0.8", 1780 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1781 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1782 | "dev": true 1783 | }, 1784 | "yargs": { 1785 | "version": "16.2.0", 1786 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1787 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1788 | "dev": true, 1789 | "requires": { 1790 | "cliui": "^7.0.2", 1791 | "escalade": "^3.1.1", 1792 | "get-caller-file": "^2.0.5", 1793 | "require-directory": "^2.1.1", 1794 | "string-width": "^4.2.0", 1795 | "y18n": "^5.0.5", 1796 | "yargs-parser": "^20.2.2" 1797 | } 1798 | }, 1799 | "yargs-parser": { 1800 | "version": "20.2.4", 1801 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1802 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1803 | "dev": true 1804 | }, 1805 | "yargs-unparser": { 1806 | "version": "2.0.0", 1807 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1808 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1809 | "dev": true, 1810 | "requires": { 1811 | "camelcase": "^6.0.0", 1812 | "decamelize": "^4.0.0", 1813 | "flat": "^5.0.2", 1814 | "is-plain-obj": "^2.1.0" 1815 | } 1816 | }, 1817 | "yocto-queue": { 1818 | "version": "0.1.0", 1819 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1820 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1821 | "dev": true 1822 | } 1823 | } 1824 | } 1825 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "booba", 4 | "version": "0.0.19", 5 | "description": "osu! helper library and api wrapper", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.mjs", 8 | "exports": { 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.cjs" 11 | }, 12 | "scripts": { 13 | "build": "rm -rf ./dist && npx rollup -c", 14 | "test": "npx mocha" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/LeaPhant/booba.git" 19 | }, 20 | "keywords": [ 21 | "osu", 22 | "js", 23 | "performance points", 24 | "pp", 25 | "beatmaps", 26 | "all modes" 27 | ], 28 | "author": "LeaPhant", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/LeaPhant/booba/issues" 32 | }, 33 | "homepage": "https://github.com/LeaPhant/booba#readme", 34 | "devDependencies": { 35 | "chai": "^4.3.4", 36 | "chai-stats": "^0.3.0", 37 | "dotenv": "^10.0.0", 38 | "mocha": "^9.1.3", 39 | "rollup": "^2.59.0" 40 | }, 41 | "dependencies": { 42 | "node-fetch": "^2.6.6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/index.js', 3 | output: [{ 4 | file: './dist/index.cjs', 5 | format: 'cjs' 6 | }, { 7 | file: './dist/index.mjs', 8 | format: 'es' 9 | }], 10 | external: ['node-fetch'] 11 | }; 12 | -------------------------------------------------------------------------------- /src/API/apiv2.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const CLIENT_SECRET_FORMAT = new RegExp('^[a-zA-Z0-9]{40}$'); 4 | const DEFAULT_HEADERS = { 5 | 'Accept': 'application/json', 6 | 'Content-Type': 'application/x-www-form-urlencoded', 7 | 'x-api-version': '20240130' 8 | }; 9 | 10 | class apiv2 { 11 | #apiBase = 'https://osu.ppy.sh'; 12 | #clientId; 13 | #clientSecret; 14 | #token; 15 | /** 16 | * New apiv2 client 17 | * @param {Object} params 18 | * @param {any} params.clientId Client ID 19 | * @param {string} params.clientSecret Client Secret 20 | * @param {string} [params.apiBase="https://osu.ppy.sh"] Base URL for API requests 21 | */ 22 | constructor(params) { 23 | if (params.clientId == null) throw new Error('Client ID required.'); 24 | if (params.clientSecret == null) throw new Error('Client Secret required.'); 25 | 26 | if (isNaN(Number(params.clientId))) { 27 | throw new TypeError('Client ID has to be a number.'); 28 | } 29 | 30 | if (!CLIENT_SECRET_FORMAT.test(params.clientSecret)) { 31 | throw new TypeError('Client Secret not a valid format.'); 32 | } 33 | 34 | this.#clientId = params.clientId; 35 | this.#clientSecret = params.clientSecret; 36 | } 37 | 38 | /** 39 | * Check whether client has a valid bearer token 40 | * @returns {bool} 41 | */ 42 | isAuthorized() { 43 | if (this.#token == null) return false; 44 | if (Date.now() > this.#token?.expiry) return false; 45 | 46 | return true; 47 | } 48 | 49 | /** 50 | * Get authorization headers 51 | * @returns {Object} 52 | */ 53 | async getAuthorizationHeaders() { 54 | if (!this.isAuthorized()) { 55 | await this.obtainBearerToken(); 56 | } 57 | 58 | return { 59 | ...DEFAULT_HEADERS, 60 | 'Authorization': `Bearer ${this.#token.token}` 61 | }; 62 | } 63 | 64 | /** 65 | * Obtains and sets bearer token 66 | */ 67 | async obtainBearerToken() { 68 | const response = await fetch(new URL('/oauth/token', this.#apiBase), { 69 | method: 'POST', 70 | headers: DEFAULT_HEADERS, 71 | body: new URLSearchParams({ 72 | client_id: this.#clientId, 73 | client_secret: this.#clientSecret, 74 | grant_type: 'client_credentials', 75 | scope: 'public' 76 | }).toString() 77 | }); 78 | 79 | const body = await response.json(); 80 | 81 | this.#token = { 82 | token: body.access_token, 83 | expiry: Date.now() + body.expires_in * 1000 84 | }; 85 | } 86 | 87 | /** 88 | * Fetch response from an APIv2 endpoint 89 | * @param {String} path The API path to 90 | * @returns {Object} 91 | */ 92 | async fetch(path, options) { 93 | const headers = { 94 | ...await this.getAuthorizationHeaders(), 95 | 'Content-Type': 'application/json', 96 | ...options?.headers ?? {} 97 | }; 98 | 99 | let params = ''; 100 | 101 | if (options?.params) { 102 | const urlParams = new URLSearchParams(options.params); 103 | params = `?${urlParams}`; 104 | } 105 | 106 | const response = await fetch(new URL('/api/v2' + path + params, this.#apiBase), { 107 | method: 'GET', 108 | headers 109 | }); 110 | 111 | return await response.json(); 112 | } 113 | } 114 | 115 | export default apiv2; 116 | -------------------------------------------------------------------------------- /src/PerformancePoints/catch_ppv2.js: -------------------------------------------------------------------------------- 1 | import ppv2 from './ppv2.js'; 2 | 3 | class catch_ppv2 extends ppv2 { 4 | mode = 2; 5 | 6 | constructor() { 7 | super({ diff_mods: ['HardRock', 'Easy', 'DoubleTime', 'HalfTime'] }); 8 | } 9 | 10 | /** 11 | * Calculate accuracy 12 | * @returns {number} 13 | */ 14 | computeAccuracy() { 15 | return Math.max(Math.min((this.n50 + this.n100 + this.n300) 16 | / this.totalHits(), 1), 0); 17 | } 18 | 19 | /** 20 | * Calculate total hits 21 | * @returns {number} 22 | */ 23 | totalHits() { 24 | if (!this.total_hits) { 25 | this.total_hits = this.n300 + this.n100 + this.n50 + this.nmiss + this.nkatu; 26 | } 27 | 28 | return this.total_hits; 29 | } 30 | 31 | /** 32 | * Calculate total object count 33 | * @returns {number} 34 | */ 35 | totalComboHits() { 36 | if (!this.total_combo_hits) { 37 | this.total_combo_hits = this.n300 + this.n100 + this.nmiss; 38 | } 39 | 40 | return this.total_combo_hits; 41 | } 42 | 43 | /** 44 | * Set player performance. 45 | * @param {Object} params Information about the play. 46 | * @param {number} params.count300 47 | * @param {number} params.count100 48 | * @param {number} params.count50 49 | * @param {number} params.countmiss 50 | * @param {number} params.countkatu 51 | * @param {number} params.maxcombo 52 | */ 53 | setPerformance(params) { 54 | // osu! api v1 response 55 | if (params?.count300 != null) { 56 | if (params.beatmap_id != null) { 57 | this.beatmap_id = params.beatmap_id; 58 | } 59 | 60 | this.n300 = Number(params.count300); 61 | this.n100 = Number(params.count100); 62 | this.n50 = Number(params.count50); 63 | this.nmiss = Number(params.countmiss); 64 | this.nkatu = Number(params.countkatu); 65 | this.combo = Number(params.maxcombo); 66 | 67 | this.setMods(Number(params.enabled_mods)); 68 | } 69 | 70 | // osu! api v2 response 71 | else if (params?.statistics?.count_300 != null) { 72 | const { statistics } = params; 73 | 74 | this.beatmap_id = params?.beatmap?.id; 75 | this.n300 = statistics.count_300; 76 | this.n100 = statistics.count_100; 77 | this.n50 = statistics.count_50; 78 | this.nmiss = statistics.count_miss; 79 | this.nkatu = statistics.count_katu; 80 | this.combo = params.max_combo; 81 | 82 | this.setMods(params.mods); 83 | } 84 | 85 | this.total_hits = this.totalHits(); 86 | this.accuracy = this.computeAccuracy(); 87 | 88 | return this; 89 | } 90 | 91 | /** 92 | * Set the beatmap difficulty attributes. 93 | * @param {object} params Information about the beatmap 94 | * @param {number} params.total Total stars 95 | * @param {number} params.ar Approach rate 96 | * @param {number} params.max_combo Max combo 97 | */ 98 | setDifficulty(params) { 99 | // beatmap api response 100 | if (params.difficulty != null) { 101 | params = params.difficulty[this.mods_enabled_diff]; 102 | 103 | params.total = params.aim; 104 | } 105 | 106 | this.diff = { ...params }; 107 | 108 | return this; 109 | } 110 | 111 | /** 112 | * Compute total pp 113 | * @returns {number} 114 | */ 115 | computeTotal() { 116 | let value = Math.pow(5.0 * Math.max(1.0, this.diff.total / 0.0049) - 4.0, 2.0) / 100000.0; 117 | 118 | const lengthBonus = 119 | 0.95 + 0.3 * Math.min(1.0, this.totalComboHits() / 2500.0) + 120 | (this.totalComboHits() > 2500 ? Math.log10(this.totalComboHits() / 2500.0) * 0.475 : 0.0); 121 | 122 | value *= lengthBonus; 123 | 124 | value *= Math.pow(0.97, this.nmiss); 125 | 126 | if (this.diff.max_combo > 0) { 127 | value *= Math.min(Math.pow(this.combo, 0.8) / Math.pow(this.diff.max_combo, 0.8), 1.0); 128 | } 129 | 130 | let approachRateFactor = 1.0; 131 | 132 | if (this.diff.ar > 9) { 133 | approachRateFactor += 0.1 * (this.diff.ar - 9.0); 134 | } 135 | 136 | if (this.diff.ar > 10) { 137 | approachRateFactor += 0.1 * (this.diff.ar - 10.0); 138 | } 139 | 140 | if (this.diff.ar < 8) { 141 | approachRateFactor += 0.025 * (8.0 - this.diff.ar); 142 | } 143 | 144 | value *= approachRateFactor; 145 | 146 | if (this.mods.includes('Hidden')) { 147 | if (this.diff.ar <= 10) { 148 | value *= 1.05 + 0.075 * (10.0 - this.diff.ar); 149 | } else if (this.diff.ar > 10) { 150 | value *= 1.01 + 0.04 * (11.0 - Math.min(11.0, this.diff.ar)); 151 | } 152 | } 153 | 154 | if (this.mods.includes('Flashlight')) { 155 | value *= 1.35 * lengthBonus; 156 | } 157 | 158 | value *= Math.pow(this.accuracy, 5.5); 159 | 160 | if (this.mods.includes('NoFail')) { 161 | value *= 0.90; 162 | } 163 | 164 | if (this.mods.includes('SpunOut')) { 165 | value *= 0.95; 166 | } 167 | 168 | return value; 169 | } 170 | 171 | /** 172 | * 173 | * @param {bool} fc Whether to simulate a full combo 174 | */ 175 | async compute(fc = false) { 176 | if (this.diff?.total == null) { 177 | await this.fetchDifficulty(); 178 | } 179 | 180 | const n300 = this.n300, nmiss = this.nmiss, combo = this.combo, accuracy = this.accuracy; 181 | 182 | if (fc) { 183 | this.n300 += this.nmiss; 184 | this.nmiss = 0; 185 | this.combo = this.diff.max_combo; 186 | this.accuracy = this.computeAccuracy(); 187 | } 188 | 189 | const pp = { 190 | total: this.computeTotal(), 191 | computed_accuracy: this.accuracy * 100 192 | }; 193 | 194 | if (fc) { 195 | this.n300 = n300; 196 | this.nmiss = nmiss; 197 | this.combo = combo; 198 | this.accuracy = accuracy; 199 | 200 | this.pp_fc = pp; 201 | } else { 202 | this.pp = pp; 203 | } 204 | 205 | return pp; 206 | } 207 | } 208 | 209 | export default catch_ppv2; 210 | -------------------------------------------------------------------------------- /src/PerformancePoints/mania_ppv2.js: -------------------------------------------------------------------------------- 1 | import ppv2 from './ppv2.js'; 2 | 3 | class mania_ppv2 extends ppv2 { 4 | mode = 3; 5 | 6 | constructor() { 7 | const diff_mods = ['HardRock', 'Easy', 'DoubleTime', 'HalfTime']; 8 | 9 | for (let i = 1; i < 9; i++) { 10 | diff_mods.push(`Key${i}`); 11 | } 12 | 13 | super({ diff_mods }); 14 | } 15 | 16 | /** 17 | * Calculate accuracy 18 | * @returns {number} 19 | */ 20 | computeAccuracy() { 21 | return Math.max(Math.min((this.n300 + this.ngeki + this.nkatu * 2/3 + this.n100 * 1/3 + this.n50 * 1/6) 22 | / this.totalHits(), 1), 0); 23 | } 24 | 25 | computeCustomAccuracy() { 26 | if (this.totalHits() == 0) return 0; 27 | 28 | return (this.ngeki * 320 + this.n300 * 300 + this.nkatu * 200 + this.n100 * 100 + this.n50 * 50) 29 | / (this.totalHits() * 320); 30 | } 31 | 32 | /** 33 | * Calculate total hits 34 | * @returns {number} 35 | */ 36 | totalHits() { 37 | if (!this.total_hits) { 38 | this.total_hits = this.n300 + this.n100 + this.n50 + this.nmiss + this.ngeki + this.nkatu 39 | } 40 | 41 | return this.total_hits; 42 | } 43 | 44 | /** 45 | * Calculate score without mod multipliers 46 | * @returns {number} 47 | */ 48 | adjustedScore() { 49 | return this.score * (1.0 / this.diff.score_multiplier); 50 | } 51 | 52 | /** 53 | * Set player performance. 54 | * @param {Object} params Information about the play. 55 | * @param {number} params.count300 56 | * @param {number} params.count100 57 | * @param {number} params.count50 58 | * @param {number} params.countmiss 59 | * @param {number} params.countgeki 60 | * @param {number} params.countkatu 61 | * @param {number} params.score 62 | */ 63 | setPerformance(params) { 64 | // osu! api v1 response 65 | if (params?.count300 != null) { 66 | if (params.beatmap_id != null) { 67 | this.beatmap_id = params.beatmap_id; 68 | } 69 | 70 | this.n300 = Number(params.count300); 71 | this.n100 = Number(params.count100); 72 | this.n50 = Number(params.count50); 73 | this.nmiss = Number(params.countmiss); 74 | this.ngeki = Number(params.countgeki); 75 | this.nkatu = Number(params.countkatu); 76 | 77 | this.setMods(Number(params.enabled_mods)); 78 | } 79 | 80 | // osu! api v2 response 81 | else if (params?.statistics?.count_300 != null) { 82 | const { statistics } = params; 83 | 84 | this.beatmap_id = params?.beatmap?.id; 85 | this.n300 = statistics.count_300; 86 | this.n100 = statistics.count_100; 87 | this.n50 = statistics.count_50; 88 | this.nmiss = statistics.count_miss; 89 | this.nmiss = statistics.count_miss; 90 | this.ngeki = statistics.count_geki; 91 | this.nkatu = statistics.count_katu; 92 | 93 | this.setMods(params.mods); 94 | } 95 | 96 | this.score = Number(params.score); 97 | 98 | this.total_hits = this.totalHits(); 99 | this.accuracy = this.computeAccuracy(); 100 | 101 | return this; 102 | } 103 | 104 | /** 105 | * Set the beatmap difficulty attributes. 106 | * @param {object} params Information about the beatmap 107 | * @param {number} params.total Total stars 108 | * @param {number} params.hit_window_300 300 hit window 109 | * @param {number} params.score_multiplier Score multiplier 110 | */ 111 | setDifficulty(params) { 112 | if (params.difficulty != null) { 113 | params = params.difficulty[this.mods_enabled_diff]; 114 | } 115 | 116 | this.diff = { ...params }; 117 | 118 | return this; 119 | } 120 | 121 | /** 122 | * Compute strain skill pp 123 | * @returns {number} 124 | */ 125 | computeStrainValue() { 126 | const value = Math.pow(Math.max(this.diff.total -0.15, 0.05), 2.2) 127 | * Math.max(0.0, 5.0 * this.computeCustomAccuracy() - 4.0) 128 | * (1.0 + 0.1 * Math.min(1.0, this.totalHits() / 1500.0)); 129 | 130 | return value; 131 | } 132 | 133 | /** 134 | * Compute total pp from separate skills 135 | * @param {object} pp Object with pp values for all skills 136 | * @returns {number} 137 | */ 138 | computeTotal(pp) { 139 | let multiplier = 8.0; 140 | 141 | if (this.mods.includes('NoFail')) { 142 | multiplier *= 0.75; 143 | } 144 | 145 | if (this.mods.includes('SpunOut')) { 146 | multiplier *= 0.95; 147 | } 148 | 149 | if (this.mods.includes('Easy')) { 150 | multiplier *= 0.50; 151 | } 152 | 153 | return pp.strain * multiplier; 154 | } 155 | 156 | /** 157 | * Calculate pp and automatically fetch beatmap difficulty 158 | */ 159 | async compute() { 160 | if (this.diff?.total == null) { 161 | await this.fetchDifficulty(); 162 | } 163 | 164 | const pp = { 165 | strain: this.computeStrainValue(), 166 | computed_accuracy: this.accuracy * 100 167 | }; 168 | 169 | pp.total = this.computeTotal(pp); 170 | 171 | return pp; 172 | } 173 | } 174 | 175 | export default mania_ppv2; 176 | -------------------------------------------------------------------------------- /src/PerformancePoints/ppv2.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import Mods from '../Shared/Mods.js'; 3 | 4 | class ppv2 { 5 | accuracy = 1.00; 6 | mods = []; 7 | mods_enabled = 0; 8 | #DIFF_MODS = []; 9 | #beatmap_api = "https://osu.lea.moe"; 10 | 11 | constructor(params) { 12 | if (params.beatmap_api != null) { 13 | try { 14 | new URL(beatmap_api); 15 | 16 | this.beatmap_api = beatmap_api; 17 | } catch(e) { 18 | throw new Error("Not a valid URL"); 19 | } 20 | } 21 | 22 | if (Array.isArray(params.diff_mods)) { 23 | this.#DIFF_MODS = params.diff_mods; 24 | } 25 | } 26 | 27 | /** 28 | * Set beatmap 29 | * @param {string|number} beatmap_id 30 | * @returns {ppv2} 31 | */ 32 | setBeatmap(beatmap_id) { 33 | this.beatmap_id = beatmap_id; 34 | 35 | return this; 36 | } 37 | 38 | async fetchBeatmap() { 39 | if (this.beatmap_id == null) { 40 | throw new Error("No Beatmap ID given"); 41 | } 42 | 43 | try { 44 | const response = await fetch(`${this.#beatmap_api}/b/${this.beatmap_id}?mode=${this.mode}`); 45 | const { beatmap, difficulty } = await response.json(); 46 | 47 | return { beatmap, difficulty }; 48 | } catch(e) { 49 | console.error(e); 50 | throw new Error("Failed fetching beatmap"); 51 | } 52 | } 53 | 54 | async fetchDifficulty() { 55 | this.setDifficulty(await this.fetchBeatmap()); 56 | } 57 | 58 | /** 59 | * Set mods. 60 | * @param {any} mods_enabled Mods 61 | * @returns {ppv2} 62 | */ 63 | setMods(mods_enabled) { 64 | const mods = new Mods(mods_enabled); 65 | this.mods = mods.list; 66 | this.mods_enabled = mods.value ?? 0; 67 | 68 | // Only include Hidden in the diff_mods together with Flashlight 69 | if (mods.list.includes('Hidden') && !mods.list.includes('Flashlight')) { 70 | mods.list = mods.list.filter(m => m !== 'Hidden') 71 | } 72 | 73 | const diff_mods = new Mods(mods.list.filter(a => this.#DIFF_MODS.includes(a))); 74 | 75 | this.mods_enabled_diff = diff_mods.value ?? 0; 76 | 77 | return this; 78 | } 79 | } 80 | 81 | export default ppv2; 82 | -------------------------------------------------------------------------------- /src/PerformancePoints/std_ppv2.js: -------------------------------------------------------------------------------- 1 | import ppv2 from './ppv2.js'; 2 | import { clamp } from '../Shared/Helper.js'; 3 | 4 | class std_ppv2 extends ppv2 { 5 | mode = 0; 6 | 7 | constructor() { 8 | super({ diff_mods: ['TouchDevice', 'Hidden', 'HardRock', 'Easy', 'DoubleTime', 'HalfTime', 'Flashlight'] }); 9 | } 10 | 11 | /** 12 | * Calculate accuracy 13 | * @returns {number} 14 | */ 15 | computeAccuracy() { 16 | return Math.max(Math.min((this.n300 + this.n100 * 1/3 + this.n50 * 1/6) 17 | / this.totalHits(), 1), 0); 18 | } 19 | 20 | /** 21 | * Calculate total hits 22 | * @returns {number} 23 | */ 24 | totalHits() { 25 | return this.n300 + this.n100 + this.n50 + this.nmiss; 26 | } 27 | 28 | /** 29 | * Calculate effective miss count with sliderbreaks 30 | * @returns {number} 31 | */ 32 | effectiveMissCount() { 33 | let combo_based_miss_count = 0.0; 34 | 35 | if (this.map.nsliders > 0) { 36 | let full_combo_threshold = this.map.max_combo - 0.1 * this.map.nsliders; 37 | 38 | if (this.combo < full_combo_threshold) { 39 | combo_based_miss_count = full_combo_threshold / Math.max(1, this.combo); 40 | } 41 | } 42 | 43 | combo_based_miss_count = Math.min(combo_based_miss_count, this.n100 + this.n50 + this.nmiss); 44 | 45 | return Math.max(this.nmiss, combo_based_miss_count); 46 | } 47 | 48 | /** 49 | * Set player performance. 50 | * @param {Object} params Information about the play, can be osu! api response 51 | * @param {string} params.beatmap_id 52 | * @param {number} params.count300 53 | * @param {number} params.count100 54 | * @param {number} params.count50 55 | * @param {number} params.countmiss 56 | * @param {number} params.maxcombo 57 | * @returns {std_ppv2} 58 | */ 59 | setPerformance(params) { 60 | // osu! api v1 response 61 | if (params?.count300 != null) { 62 | if (params.beatmap_id != null) { 63 | this.beatmap_id = params.beatmap_id; 64 | } 65 | 66 | this.n300 = Number(params.count300); 67 | this.n100 = Number(params.count100); 68 | this.n50 = Number(params.count50); 69 | this.nmiss = Number(params.countmiss); 70 | this.combo = Number(params.maxcombo); 71 | 72 | this.setMods(Number(params.enabled_mods)); 73 | } 74 | 75 | // osu! api v2 response 76 | else if (params?.statistics?.count_300 != null) { 77 | const { statistics } = params; 78 | 79 | this.beatmap_id = params?.beatmap?.id; 80 | this.n300 = statistics.count_300; 81 | this.n100 = statistics.count_100; 82 | this.n50 = statistics.count_50; 83 | this.nmiss = statistics.count_miss; 84 | this.combo = params.max_combo; 85 | 86 | this.setMods(params.mods); 87 | } 88 | 89 | this.total_hits = this.totalHits(); 90 | this.accuracy = this.computeAccuracy(); 91 | 92 | return this; 93 | } 94 | 95 | /** 96 | * Set the beatmap difficulty attributes 97 | * @param {object} params Information about the beatmap 98 | * @param {number} params.max_combo Maximum achievable combo 99 | * @param {number} params.aim Aim stars 100 | * @param {number} params.speed Speed stars 101 | * @param {number} params.total Total stars 102 | * @param {number} params.ar Approach Rate 103 | * @param {number} params.od Overall Difficulty 104 | * @param {number} params.count_circles Amount of hit circles 105 | * @param {number} params.count_sliders Amount of hit circles 106 | * @param {number} params.count_spinners Amount of hit circles 107 | * @returns {std_ppv2} 108 | */ 109 | setDifficulty(params) { 110 | // beatmap api response 111 | if (params.beatmap != null && params.difficulty != null) { 112 | const { beatmap, difficulty } = params; 113 | 114 | const diff = difficulty[this.mods_enabled_diff]; 115 | 116 | this.diff = { 117 | aim: diff.aim ?? 0, 118 | speed: diff.speed ?? 0, 119 | fl: diff.flashlight_rating ?? 0, 120 | total: diff.total ?? 0, 121 | slider_factor: diff.slider_factor ?? 1, 122 | speed_note_count: diff.speed_note_count ?? 0 123 | }; 124 | 125 | this.map = { 126 | max_combo: diff.max_combo, 127 | ar: diff.ar, 128 | od: diff.od, 129 | ncircles: beatmap.num_circles ?? 0, 130 | nsliders: beatmap.num_sliders ?? 0, 131 | nspinners: beatmap.num_spinners ?? 0 132 | } 133 | } else { 134 | this.diff = { 135 | aim: params.aim, 136 | speed: params.speed, 137 | fl: params.fl ?? 0, 138 | total: params.total, 139 | slider_factor: params.slider_factor ?? 0, 140 | speed_note_count: params.speed_note_count ?? 0 141 | }; 142 | 143 | this.map = { 144 | max_combo: params.max_combo, 145 | ar: params.ar, 146 | od: params.od, 147 | ncircles: params.count_circles ?? 0, 148 | nsliders: params.count_sliders ?? 0, 149 | nspinners: params.count_spinners ?? 0, 150 | } 151 | } 152 | 153 | this.n300 = this.map.ncircles + this.map.nsliders + this.map.nspinners - this.n100 - this.n50 - this.nmiss; 154 | this.total_hits = this.totalHits(); 155 | 156 | return this; 157 | } 158 | 159 | /** 160 | * Compute aim skill pp 161 | * @returns {number} 162 | */ 163 | computeAimValue() { 164 | const nmiss_e = this.effectiveMissCount(); 165 | 166 | let value = Math.pow(5.0 * Math.max(1.0, this.diff.aim / 0.0675) - 4.0, 3.0) / 100000.0; 167 | 168 | let length_bonus = 0.95 + 0.4 * Math.min(1.0, this.total_hits / 2000.0) + 169 | (this.total_hits > 2000 ? Math.log10(this.total_hits / 2000.0) * 0.5 : 0.0); 170 | 171 | value *= length_bonus; 172 | 173 | if (nmiss_e > 0) { 174 | value *= 0.97 * Math.pow(1.0 - Math.pow(nmiss_e / this.total_hits, 0.775), nmiss_e); 175 | } 176 | 177 | if (this.map.max_combo > 0) { 178 | value *= Math.min(Math.pow(this.combo, 0.8) / Math.pow(this.map.max_combo, 0.8), 1.0); 179 | } 180 | 181 | let ar_factor = 0.0; 182 | 183 | if (this.map.ar > 10.33) { 184 | ar_factor += 0.3 * (this.map.ar - 10.33); 185 | } else if (this.map.ar < 8.0) { 186 | ar_factor += 0.05 * (8.0 - this.map.ar); 187 | } 188 | 189 | value *= 1.0 + ar_factor * length_bonus; 190 | 191 | if (this.mods.includes('Hidden')) { 192 | value *= 1.0 + 0.04 * (12.0 - this.map.ar); 193 | } 194 | 195 | const estimateDifficultSliders = this.map.nsliders * 0.15; 196 | 197 | if (this.map.nsliders > 0) 198 | { 199 | const estimateSliderEndsDropped = clamp(Math.min(this.n100 + this.n50 + this.nmiss, this.map.max_combo - this.combo), 0, estimateDifficultSliders); 200 | const sliderNerfFactor = (1 - this.diff.slider_factor) * Math.pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + this.diff.slider_factor; 201 | value *= sliderNerfFactor; 202 | } 203 | 204 | value *= this.accuracy; 205 | 206 | value *= 0.98 + Math.pow(this.map.od, 2) / 2500.0; 207 | 208 | return value; 209 | } 210 | 211 | /** 212 | * Compute speed skill pp 213 | * @returns {number} 214 | */ 215 | computeSpeedValue() { 216 | const nmiss_e = this.effectiveMissCount(); 217 | 218 | let value = Math.pow(5.0 * Math.max(1.0, this.diff.speed / 0.0675) - 4.0, 3.0) / 100000.0; 219 | 220 | let length_bonus = 0.95 + 0.4 * Math.min(1.0, this.total_hits / 2000.0) + 221 | (this.total_hits > 2000 ? Math.log10(this.total_hits / 2000.0) * 0.5 : 0.0); 222 | 223 | value *= length_bonus; 224 | 225 | if (nmiss_e > 0) { 226 | value *= 0.97 * Math.pow(1.0 - Math.pow(nmiss_e / this.total_hits, 0.775), Math.pow(nmiss_e, 0.875)); 227 | } 228 | 229 | if (this.map.max_combo > 0) { 230 | value *= Math.min(Math.pow(this.combo, 0.8) / Math.pow(this.map.max_combo, 0.8), 1.0); 231 | } 232 | 233 | let ar_factor = 0; 234 | 235 | if (this.map.ar > 10.33) { 236 | ar_factor += 0.3 * (this.map.ar - 10.33); 237 | } 238 | 239 | value *= 1.0 + ar_factor * length_bonus; 240 | 241 | if (this.mods.includes('Hidden')) { 242 | value *= 1.0 + 0.04 * (12.0 - this.map.ar); 243 | } 244 | 245 | let relevant_total_diff = this.total_hits - this.diff.speed_note_count; 246 | let relevant_count_great = Math.max(0, this.n300 - relevant_total_diff); 247 | let relevant_count_ok = Math.max(0, this.n100 - Math.max(0, relevant_total_diff - this.n300)); 248 | let relevant_count_meh = Math.max(0, this.n50 - Math.max(0, relevant_total_diff - this.n300 - this.n100)); 249 | let relevant_accuracy = this.diff.speed_note_count == 0 ? 0 : (relevant_count_great * 6.0 + relevant_count_ok * 2.0 + relevant_count_meh) / (this.diff.speed_note_count * 6.0); 250 | 251 | value *= (0.95 + Math.pow(this.map.od, 2) / 750) * Math.pow((this.accuracy + relevant_accuracy) / 2.0, (14.5 - Math.max(this.map.od, 8.0)) / 2); 252 | 253 | value *= Math.pow(0.99, this.n50 < this.total_hits / 500.0 ? 0.0 : this.n50 - this.total_hits / 500.0); 254 | 255 | return value; 256 | } 257 | 258 | /** 259 | * Compute acc skill pp 260 | * @returns {number} 261 | */ 262 | computeAccValue() { 263 | let better_acc_percentage; 264 | let n_objects_with_acc; 265 | 266 | if (this.mods.includes('ScoreV2')) { 267 | n_objects_with_acc = this.total_hits; 268 | better_acc_percentage = this.accuracy; 269 | } else { 270 | n_objects_with_acc = this.map.ncircles; 271 | 272 | if (n_objects_with_acc > 0) { 273 | better_acc_percentage = ((this.n300 - (this.total_hits - n_objects_with_acc)) * 6 + this.n100 * 2 + this.n50) / (n_objects_with_acc * 6); 274 | } else { 275 | better_acc_percentage = 0; 276 | } 277 | 278 | if (better_acc_percentage < 0) { 279 | better_acc_percentage = 0; 280 | } 281 | } 282 | 283 | let value = Math.pow(1.52163, this.map.od) * Math.pow(better_acc_percentage, 24) * 2.83; 284 | 285 | value *= Math.min(1.15, Math.pow(n_objects_with_acc / 1000.0, 0.3)); 286 | 287 | if (this.mods.includes('Hidden')) { 288 | value *= 1.08; 289 | } 290 | 291 | if (this.mods.includes('Flashlight')) { 292 | value *= 1.02; 293 | } 294 | 295 | return value; 296 | } 297 | 298 | /** 299 | * Compute flashlight skill pp 300 | * @returns {number} 301 | */ 302 | computeFlashlightValue() { 303 | let value = 0; 304 | 305 | if (!this.mods.includes('Flashlight')) { 306 | return value; 307 | } 308 | 309 | value = Math.pow(this.diff.fl, 2.0) * 25.0; 310 | 311 | if (this.nmiss > 0) { 312 | value *= 0.97 * Math.pow(1 - Math.pow(this.nmiss / this.total_hits, 0.775), Math.pow(this.nmiss, 0.875)); 313 | } 314 | 315 | if (this.map.max_combo > 0) { 316 | value *= Math.min(Math.pow(this.combo, 0.8) / Math.pow(this.map.max_combo, 0.8), 1); 317 | } 318 | 319 | value *= 0.7 + 0.1 * Math.min(1.0, this.total_hits / 200.0) + 320 | (this.total_hits > 200 ? 0.2 * Math.min(1.0, (this.total_hits - 200) / 200.0) : 0.0); 321 | 322 | value *= 0.5 + this.accuracy / 2.0; 323 | 324 | value *= 0.98 + Math.pow(this.map.od, 2.0) / 2500.0; 325 | 326 | return value; 327 | } 328 | 329 | /** 330 | * Compute total pp from separate skills 331 | * @param {object} pp Object with pp values for all skills 332 | * @returns {number} 333 | */ 334 | computeTotal(pp) { 335 | let multiplier = 1.14; 336 | 337 | if (this.mods.includes('NoFail')) { 338 | multiplier *= Math.max(0.9, 1.0 - 0.02 * this.effectiveMissCount()); 339 | } 340 | 341 | if (this.mods.includes('SpunOut')) { 342 | multiplier *= 1.0 - Math.pow(this.map.nspinners / this.total_hits, 0.85); 343 | } 344 | 345 | return Math.pow( 346 | Math.pow(pp.aim, 1.1) + 347 | Math.pow(pp.speed, 1.1) + 348 | Math.pow(pp.fl, 1.1) + 349 | Math.pow(pp.acc, 1.1), 1.0 / 1.1 350 | ) * multiplier; 351 | } 352 | 353 | /** 354 | * Calculate pp and automatically fetch beatmap difficulty 355 | * @param {bool} fc Whether to simulate a full combo 356 | */ 357 | async compute(fc = false) { 358 | if (this.diff?.total == null) { 359 | await this.fetchDifficulty(); 360 | } 361 | 362 | const n300 = this.n300, nmiss = this.nmiss, combo = this.combo, accuracy = this.accuracy; 363 | 364 | if (fc) { 365 | this.n300 += this.nmiss; 366 | this.nmiss = 0; 367 | this.combo = this.map.max_combo; 368 | this.accuracy = this.computeAccuracy(); 369 | } 370 | 371 | const pp = { 372 | aim: this.computeAimValue(), 373 | speed: this.computeSpeedValue(), 374 | fl: this.computeFlashlightValue(), 375 | acc: this.computeAccValue(), 376 | computed_accuracy: this.accuracy * 100 377 | }; 378 | 379 | pp.total = this.computeTotal(pp); 380 | 381 | if (fc) { 382 | this.n300 = n300; 383 | this.nmiss = nmiss; 384 | this.combo = combo; 385 | this.accuracy = accuracy; 386 | 387 | this.pp_fc = pp; 388 | } else { 389 | this.pp = pp; 390 | } 391 | 392 | return pp; 393 | } 394 | } 395 | 396 | export default std_ppv2; 397 | -------------------------------------------------------------------------------- /src/PerformancePoints/taiko_ppv2.js: -------------------------------------------------------------------------------- 1 | import ppv2 from './ppv2.js'; 2 | 3 | class taiko_ppv2 extends ppv2 { 4 | mode = 1; 5 | 6 | constructor() { 7 | super({ diff_mods: ['HardRock', 'Easy', 'DoubleTime', 'HalfTime'] }); 8 | } 9 | 10 | /** 11 | * Calculate accuracy 12 | * @returns {number} 13 | */ 14 | computeAccuracy() { 15 | return Math.max(Math.min((this.n100 * 1/2 + this.n300) 16 | / this.totalHits(), 1), 0); 17 | } 18 | 19 | /** 20 | * Calculate total hits 21 | * @returns {number} 22 | */ 23 | totalHits() { 24 | if (!this.total_hits) { 25 | this.total_hits = this.n300 + this.n100 + this.n50 + this.nmiss; 26 | } 27 | 28 | return this.total_hits; 29 | } 30 | 31 | /** 32 | * Calculate total successful hits 33 | * @returns {number} 34 | */ 35 | totalSuccessfulHits() { 36 | if (!this.total_successful_hits) { 37 | this.total_successful_hits = this.n300 + this.n100 + this.n50; 38 | } 39 | 40 | return this.total_successful_hits; 41 | } 42 | 43 | /** 44 | * Calculate effective miss count 45 | * @returns {number} 46 | */ 47 | effectiveMissCount() { 48 | return Math.max(1.0, 1000.0 / this.totalSuccessfulHits()) * this.nmiss; 49 | } 50 | 51 | /** 52 | * Set player performance. 53 | * @param {Object} params Information about the play. 54 | * @param {number} params.count300 55 | * @param {number} params.count100 56 | * @param {number} params.count50 57 | * @param {number} params.countmiss 58 | */ 59 | setPerformance(params) { 60 | // osu! api v1 response 61 | if (params?.count300 != null) { 62 | if (params.beatmap_id != null) { 63 | this.beatmap_id = params.beatmap_id; 64 | } 65 | 66 | this.n300 = Number(params.count300); 67 | this.n100 = Number(params.count100); 68 | this.n50 = Number(params.count50); 69 | this.nmiss = Number(params.countmiss); 70 | 71 | this.setMods(Number(params.enabled_mods)); 72 | } 73 | 74 | // osu! api v2 response 75 | else if (params?.statistics?.count_300 != null) { 76 | const { statistics } = params; 77 | 78 | this.beatmap_id = params?.beatmap?.id; 79 | this.n300 = statistics.count_300; 80 | this.n100 = statistics.count_100; 81 | this.n50 = statistics.count_50; 82 | this.nmiss = statistics.count_miss; 83 | 84 | this.setMods(params.mods); 85 | } 86 | 87 | this.total_hits = this.totalHits(); 88 | this.accuracy = this.computeAccuracy(); 89 | 90 | return this; 91 | } 92 | 93 | /** 94 | * Set the beatmap difficulty attributes. 95 | * @param {object} params Information about the beatmap 96 | * @param {number} params.total Total stars 97 | * @param {number} params.hit_window_300 300 hit window 98 | */ 99 | setDifficulty(params) { 100 | // beatmap api response 101 | if (params.difficulty != null) { 102 | params = params.difficulty[this.mods_enabled_diff]; 103 | } 104 | 105 | this.diff = { ...params }; 106 | 107 | return this; 108 | } 109 | 110 | /** 111 | * Compute strain skill pp 112 | * @returns {number} 113 | */ 114 | computeStrainValue() { 115 | let value = Math.pow(5 * Math.max(1.0, this.diff.total / 0.115) - 4.0, 2.25) / 1150.0; 116 | 117 | const lengthBonus = 1 + 0.1 * Math.min(1.0, this.totalHits() / 1500.0); 118 | 119 | value *= lengthBonus; 120 | value *= Math.pow(0.986, this.effectiveMissCount()); 121 | 122 | if (this.mods.includes('Easy')) { 123 | value *= 0.985; 124 | } 125 | 126 | if (this.mods.includes('Hidden')) { 127 | value *= 1.025; 128 | } 129 | 130 | if (this.mods.includes('HardRock')) { 131 | value *= 1.050; 132 | } 133 | 134 | if (this.mods.includes('Flashlight')) { 135 | value *= 1.050 * lengthBonus; 136 | } 137 | 138 | value *= Math.pow(this.accuracy, 2.0); 139 | 140 | return value; 141 | } 142 | 143 | /** 144 | * Compute acc skill pp 145 | * @returns {number} 146 | */ 147 | computeAccValue() { 148 | if (this.diff.hit_window_300 <= 0) { 149 | return 0; 150 | } 151 | 152 | let value 153 | = Math.pow(60.0 / this.diff.hit_window_300, 1.1) 154 | * Math.pow(this.accuracy, 8.0) 155 | * Math.pow(this.diff.total, 0.4) 156 | * 27.0; 157 | 158 | const lengthBonus = Math.min(1.15, Math.pow(this.totalHits() / 1500.0, 0.3)); 159 | value *= lengthBonus; 160 | 161 | if (this.mods.includes('Hidden') && this.mods.includes('Flashlight')) { 162 | value *= Math.max(1.050, 1.075 * lengthBonus); 163 | } 164 | 165 | return value; 166 | } 167 | 168 | /** 169 | * Compute total pp from separate skills 170 | * @param {object} pp Object with pp values for all skills 171 | * @returns {number} 172 | */ 173 | computeTotal(pp) { 174 | let multiplier = 1.13; 175 | 176 | if (this.mods.includes('Hidden')) { 177 | multiplier *= 1.075; 178 | } 179 | 180 | if (this.mods.includes('Easy')) { 181 | multiplier *= 0.975; 182 | } 183 | 184 | let value = Math.pow( 185 | Math.pow(pp.strain, 1.1) + 186 | Math.pow(pp.acc, 1.1), 1.0 / 1.1 187 | ) * multiplier; 188 | 189 | return value; 190 | } 191 | 192 | /** 193 | * Calculate pp and automatically fetch beatmap difficulty 194 | * @param {bool} fc Whether to simulate a full combo 195 | */ 196 | async compute(fc = false) { 197 | const n300 = this.n300, nmiss = this.nmiss, accuracy = this.accuracy; 198 | 199 | if (this.diff?.total == null) { 200 | await this.fetchDifficulty(); 201 | } 202 | 203 | if (fc) { 204 | this.n300 += this.nmiss; 205 | this.nmiss = 0; 206 | this.accuracy = this.computeAccuracy(); 207 | } 208 | 209 | const pp = { 210 | strain: this.computeStrainValue(), 211 | acc: this.computeAccValue(), 212 | computed_accuracy: this.accuracy * 100 213 | }; 214 | 215 | pp.total = this.computeTotal(pp); 216 | 217 | if (fc) { 218 | this.n300 = n300; 219 | this.nmiss = nmiss; 220 | this.accuracy = accuracy; 221 | 222 | this.pp_fc = pp; 223 | } else { 224 | this.pp = pp; 225 | } 226 | 227 | return pp; 228 | } 229 | } 230 | 231 | export default taiko_ppv2; 232 | -------------------------------------------------------------------------------- /src/Shared/Helper.js: -------------------------------------------------------------------------------- 1 | function clamp(num, min, max) { 2 | return Math.min(Math.max(num, min), max); 3 | } 4 | 5 | export { clamp }; 6 | -------------------------------------------------------------------------------- /src/Shared/Mods.js: -------------------------------------------------------------------------------- 1 | const MODS_ENUM = { 2 | NoFail: 1 << 0, 3 | Easy: 1 << 1, 4 | TouchDevice: 1 << 2, 5 | Hidden: 1 << 3, 6 | HardRock: 1 << 4, 7 | SuddenDeath: 1 << 5, 8 | DoubleTime: 1 << 6, 9 | Relax: 1 << 7, 10 | HalfTime: 1 << 8, 11 | Nightcore: 1 << 9, 12 | Flashlight: 1 << 10, 13 | Autoplay: 1 << 11, 14 | SpunOut: 1 << 12, 15 | Autopilot: 1 << 13, 16 | Perfect: 1 << 14, 17 | Key4: 1 << 15, 18 | Key5: 1 << 16, 19 | Key6: 1 << 17, 20 | Key7: 1 << 18, 21 | Key8: 1 << 19, 22 | FadeIn: 1 << 20, 23 | Random: 1 << 21, 24 | Cinema: 1 << 22, 25 | Target: 1 << 23, 26 | Key9: 1 << 24, 27 | KeyCoop: 1 << 25, 28 | Key1: 1 << 26, 29 | Key3: 1 << 27, 30 | Key2: 1 << 28, 31 | ScoreV2: 1 << 29, 32 | Mirror: 1 << 30 33 | } 34 | 35 | const MODS_SHORT_NAMES = { 36 | NoFail: 'NF', 37 | Easy: 'EZ', 38 | TouchDevice: 'TD', 39 | Hidden: 'HD', 40 | HardRock: 'HR', 41 | SuddenDeath: 'SD', 42 | DoubleTime: 'DT', 43 | Relax: 'RX', 44 | HalfTime: 'HT', 45 | Nightcore: 'NC', 46 | Flashlight: 'FL', 47 | Autoplay: 'AT', 48 | SpunOut: 'SO', 49 | Perfect: 'PF', 50 | Key4: '4K', 51 | Key5: '5K', 52 | Key6: '6K', 53 | Key7: '7K', 54 | Key8: '8K', 55 | FadeIn: 'FI', 56 | Random: 'RD', 57 | Cinema: 'CN', 58 | Target: 'TP', 59 | Key9: '9K', 60 | KeyCoop: 'KC', 61 | Key1: '1K', 62 | Key3: '3K', 63 | Key2: '2K', 64 | ScoreV2: 'V2', 65 | Mirror: 'MR' 66 | }; 67 | 68 | const MODS_INHERITS = { 69 | Nightcore: 'DoubleTime', 70 | Perfect: 'SuddenDeath' 71 | } 72 | 73 | class Mods { 74 | constructor(mods_enabled) { 75 | if (typeof mods_enabled == 'number') { 76 | if (isNaN(mods_enabled)) { 77 | throw new Error("Invalid mods provided (NaN)"); 78 | } 79 | 80 | this.value = mods_enabled; 81 | } else if (typeof mods_enabled == 'string') { 82 | this.value = this.fromString(mods_enabled); 83 | } else if (Array.isArray(mods_enabled)) { 84 | this.value = this.fromList(mods_enabled); 85 | } else { 86 | throw new Error("Invalid mods provided"); 87 | } 88 | 89 | this.list = this.toList(this.value); 90 | } 91 | 92 | fromString(mods_string) { 93 | let mods_enabled = 0; 94 | 95 | let parts; 96 | 97 | mods_string = mods_string.toUpperCase(); 98 | 99 | if (mods_string.startsWith('+')) { 100 | mods_string = mods_string.substring(1); 101 | } 102 | 103 | if (mods_string.includes(',')) { 104 | parts = mods_string.split(','); 105 | } else { 106 | parts = mods_string.match(/.{1,2}/g); 107 | } 108 | 109 | for (const [key, value] of Object.entries(MODS_SHORT_NAMES)) { 110 | if (parts.includes(value)) { 111 | mods_enabled |= MODS_ENUM[key]; 112 | } 113 | } 114 | 115 | return mods_enabled; 116 | } 117 | 118 | fromList(mods_list) { 119 | let mods_enabled = 0; 120 | 121 | for (const mod of mods_list) { 122 | if (mod.length == 2) { 123 | for (const [key, value] of Object.entries(MODS_SHORT_NAMES)) { 124 | if (value == mod.toUpperCase()) { 125 | mods_enabled |= MODS_ENUM[key]; 126 | } 127 | } 128 | } else { 129 | if (MODS_ENUM[mod] != null) { 130 | mods_enabled |= MODS_ENUM[mod]; 131 | } 132 | } 133 | } 134 | 135 | return mods_enabled; 136 | } 137 | 138 | toList(mods_enabled) { 139 | const mods = []; 140 | 141 | for (const [mod, value] of Object.entries(MODS_ENUM)) { 142 | if (value > mods_enabled) 143 | break; 144 | 145 | if ((mods_enabled & value) == value) { 146 | mods.push(mod); 147 | } 148 | } 149 | 150 | return mods; 151 | } 152 | 153 | /** 154 | * 155 | * @param {bool} with_plus Whether to output + at beginning of mods string 156 | */ 157 | toString(with_plus = true) { 158 | if (this.list.length == 0) { 159 | return ''; 160 | } 161 | 162 | let output_mods = []; 163 | 164 | for (const mod of this.list) { 165 | if (Object.keys(MODS_INHERITS).includes(mod)) { 166 | continue; 167 | } 168 | 169 | output_mods.push(MODS_SHORT_NAMES[mod]); 170 | } 171 | 172 | return (with_plus ? '+' : '') + output_mods.join(','); 173 | } 174 | } 175 | 176 | export default Mods; 177 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Mods } from './Shared/Mods.js'; 2 | export { default as std_ppv2 } from './PerformancePoints/std_ppv2.js'; 3 | export { default as taiko_ppv2 } from './PerformancePoints/taiko_ppv2.js'; 4 | export { default as catch_ppv2 } from './PerformancePoints/catch_ppv2.js'; 5 | export { default as mania_ppv2 } from './PerformancePoints/mania_ppv2.js'; 6 | export { default as apiv2 } from './API/apiv2.js'; 7 | -------------------------------------------------------------------------------- /test/apiv2/auth.js: -------------------------------------------------------------------------------- 1 | import { apiv2 } from '../../src/index.js'; 2 | import chai from 'chai'; 3 | import chaiStats from 'chai-stats'; 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | 7 | chai.use(chaiStats); 8 | 9 | const { assert } = chai; 10 | 11 | if (process.env.API_KEY == null) { 12 | throw new Error("No osu! api key provided"); 13 | } 14 | 15 | describe('[apiv2] check if authenticating works', () => { 16 | let client, headers; 17 | 18 | before(done => { 19 | client = new apiv2({ 20 | clientId: process.env.CLIENT_ID, 21 | clientSecret: process.env.CLIENT_SECRET, 22 | }); 23 | 24 | client.getAuthorizationHeaders().then(response => { 25 | headers = response; 26 | 27 | done(); 28 | }).catch(console.error); 29 | }); 30 | 31 | it('should return valid authorization headers', () => { 32 | assert.match(headers?.Authorization, /Bearer ./); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/apiv2/mods.js: -------------------------------------------------------------------------------- 1 | import { apiv2 } from '../../src/index.js'; 2 | import chai from 'chai'; 3 | import chaiStats from 'chai-stats'; 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | 7 | chai.use(chaiStats); 8 | 9 | const { assert } = chai; 10 | 11 | if (process.env.API_KEY == null) { 12 | throw new Error("No osu! api key provided"); 13 | } 14 | 15 | describe('[apiv2] check if returning the #1 DT score on a map works', () => { 16 | let client, scores; 17 | 18 | before(done => { 19 | client = new apiv2({ 20 | clientId: process.env.CLIENT_ID, 21 | clientSecret: process.env.CLIENT_SECRET, 22 | }); 23 | 24 | client.fetch('/beatmaps/999944/solo-scores', { params: { mods: ['DT']}}) 25 | .then(response => { 26 | scores = response?.scores; 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should return a DT score', () => { 32 | const top = scores[0]; 33 | assert(top?.mods?.find(x => x.acronym == 'DT') !== undefined); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/catch/compare-api-tops.js: -------------------------------------------------------------------------------- 1 | import { catch_ppv2 } from "../../src/index.js"; 2 | import fetch from 'node-fetch'; 3 | import chai from 'chai'; 4 | import chaiStats from 'chai-stats'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | chai.use(chaiStats); 9 | 10 | const { assert } = chai; 11 | 12 | if (process.env.API_KEY == null) { 13 | throw new Error("No osu! api key provided"); 14 | } 15 | 16 | async function fetchTopPlays() { 17 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=214187&limit=100&m=2`); 18 | const json = await response.json(); 19 | 20 | if (json.error != null) { 21 | throw new Error(json.error); 22 | } 23 | 24 | return json; 25 | } 26 | 27 | async function fetchBeatmaps(beatmapIds) { 28 | const response = await fetch(`https://osu.lea.moe/b/${beatmapIds.join(',')}?mode=2`); 29 | const json = await response.json(); 30 | 31 | return json; 32 | } 33 | 34 | describe('[catch] compare top 100 of ExGon to calculated values', () => { 35 | const scores = [], beatmaps = []; 36 | 37 | before(done => { 38 | fetchTopPlays().then(_scores => { 39 | scores.push(..._scores); 40 | 41 | const beatmapIds = scores.map(a => a.beatmap_id); 42 | 43 | fetchBeatmaps(beatmapIds).then(_beatmaps => { 44 | beatmaps.push(..._beatmaps); 45 | 46 | done(); 47 | }).catch(console.error); 48 | }).catch(console.error); 49 | }); 50 | 51 | it('should be 100 scores', () => { 52 | assert.equal(scores.length, 100); 53 | }); 54 | 55 | for (let i = 0; i < 100; i++) { 56 | it(`matches on #${i + 1} top play`, async () => { 57 | const score = scores[i]; 58 | const beatmap = beatmaps[i]; 59 | 60 | const play = new catch_ppv2().setPerformance(score).setDifficulty(beatmap); 61 | const pp = await play.compute(); 62 | 63 | assert.almostEqual(pp.total, score.pp, 0, `/b/${score.beatmap_id} ${beatmap.beatmap.title} (+${play.mods})`); 64 | }); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /test/mania/compare-api-tops.js: -------------------------------------------------------------------------------- 1 | import { mania_ppv2 } from "../../src/index.js"; 2 | import fetch from 'node-fetch'; 3 | import chai from 'chai'; 4 | import chaiStats from 'chai-stats'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | chai.use(chaiStats); 9 | 10 | const { assert } = chai; 11 | 12 | if (process.env.API_KEY == null) { 13 | throw new Error("No osu! api key provided"); 14 | } 15 | 16 | async function fetchTopPlays() { 17 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=758406&limit=100&m=3`); 18 | const json = await response.json(); 19 | 20 | if (json.error != null) { 21 | throw new Error(json.error); 22 | } 23 | 24 | return json; 25 | } 26 | 27 | async function fetchBeatmaps(beatmapIds) { 28 | const response = await fetch(`https://osu.lea.moe/b/${beatmapIds.join(',')}?mode=3`); 29 | const json = await response.json(); 30 | 31 | return json; 32 | } 33 | 34 | describe('[mania] compare top 100 of dressurf to calculated values', () => { 35 | const scores = [], beatmaps = []; 36 | 37 | before(done => { 38 | fetchTopPlays().then(_scores => { 39 | scores.push(..._scores); 40 | 41 | const beatmapIds = scores.map(a => a.beatmap_id); 42 | 43 | fetchBeatmaps(beatmapIds).then(_beatmaps => { 44 | beatmaps.push(..._beatmaps); 45 | 46 | done(); 47 | }).catch(console.error); 48 | }).catch(console.error); 49 | }); 50 | 51 | it('should be 100 scores', () => { 52 | assert.equal(scores.length, 100); 53 | }); 54 | 55 | for (let i = 0; i < 100; i++) { 56 | it(`matches on #${i + 1} top play`, async () => { 57 | const score = scores[i]; 58 | const beatmap = beatmaps[i]; 59 | 60 | const play = new mania_ppv2().setPerformance(score).setDifficulty(beatmap); 61 | const pp = await play.compute(); 62 | 63 | assert.almostEqual(pp.total, score.pp, 0, `/b/${score.beatmap_id} ${beatmap.beatmap.title} (${pp.strain} strain) (+${play.mods})`); 64 | }); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /test/std/compare-api-tops.js: -------------------------------------------------------------------------------- 1 | import { std_ppv2 } from "../../src/index.js"; 2 | import fetch from 'node-fetch'; 3 | import chai from 'chai'; 4 | import chaiStats from 'chai-stats'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | chai.use(chaiStats); 9 | 10 | const { assert } = chai; 11 | 12 | if (process.env.API_KEY == null) { 13 | throw new Error("No osu! api v1 key provided (Use API_KEY env variable)"); 14 | } 15 | 16 | async function fetchTopPlays() { 17 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=895581&limit=100`); 18 | const json = await response.json(); 19 | 20 | if (json.error != null) { 21 | throw new Error(json.error); 22 | } 23 | 24 | return json; 25 | } 26 | 27 | async function fetchBeatmaps(beatmapIds) { 28 | const response = await fetch(`https://osu.lea.moe/b/${beatmapIds.join(',')}`); 29 | const json = await response.json(); 30 | 31 | return json; 32 | } 33 | 34 | describe('[std] compare top 100 of -GN to calculated values', () => { 35 | const scores = [], beatmaps = []; 36 | 37 | before(done => { 38 | fetchTopPlays().then(_scores => { 39 | scores.push(..._scores); 40 | 41 | const beatmapIds = scores.map(a => a.beatmap_id); 42 | 43 | fetchBeatmaps(beatmapIds).then(_beatmaps => { 44 | beatmaps.push(..._beatmaps); 45 | 46 | done(); 47 | }).catch(console.error); 48 | }).catch(console.error); 49 | }); 50 | 51 | it('should be 100 scores', () => { 52 | assert.equal(scores.length, 100); 53 | }); 54 | 55 | for (let i = 0; i < 100; i++) { 56 | it(`matches on #${i + 1} top play`, async () => { 57 | const score = scores[i]; 58 | const beatmap = beatmaps[i]; 59 | 60 | const play = new std_ppv2().setPerformance(score).setDifficulty(beatmap); 61 | const pp = await play.compute(); 62 | 63 | assert.almostEqual(pp.total, score.pp, 0, `/b/${score.beatmap_id} ${beatmap.beatmap.title} (${pp.aim} aim, ${pp.speed} speed, ${pp.acc} acc, ${pp.fl} flashlight) (+${play.mods})`); 64 | }); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /test/taiko/compare-api-tops.js: -------------------------------------------------------------------------------- 1 | import { taiko_ppv2 } from "../../src/index.js"; 2 | import fetch from 'node-fetch'; 3 | import chai from 'chai'; 4 | import chaiStats from 'chai-stats'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | chai.use(chaiStats); 9 | 10 | const { assert } = chai; 11 | 12 | if (process.env.API_KEY == null) { 13 | throw new Error("No osu! api key provided"); 14 | } 15 | 16 | async function fetchTopPlays() { 17 | const response = await fetch(`https://osu.ppy.sh/api/get_user_best?k=${process.env.API_KEY}&u=8741695&limit=100&m=1`); 18 | const json = await response.json(); 19 | 20 | if (json.error != null) { 21 | throw new Error(json.error); 22 | } 23 | 24 | return json; 25 | } 26 | 27 | async function fetchBeatmaps(beatmapIds) { 28 | const response = await fetch(`https://osu.lea.moe/b/${beatmapIds.join(',')}?mode=1`); 29 | const json = await response.json(); 30 | 31 | return json; 32 | } 33 | 34 | describe('[taiko] compare top 100 of syaron105 to calculated values', () => { 35 | const scores = [], beatmaps = []; 36 | 37 | before(done => { 38 | fetchTopPlays().then(_scores => { 39 | scores.push(..._scores); 40 | 41 | const beatmapIds = scores.map(a => a.beatmap_id); 42 | 43 | fetchBeatmaps(beatmapIds).then(_beatmaps => { 44 | beatmaps.push(..._beatmaps); 45 | 46 | done(); 47 | }).catch(console.error); 48 | }).catch(console.error); 49 | }); 50 | 51 | it('should be 100 scores', () => { 52 | assert.equal(scores.length, 100); 53 | }); 54 | 55 | for (let i = 0; i < 100; i++) { 56 | it(`matches on #${i + 1} top play`, async () => { 57 | const score = scores[i]; 58 | const beatmap = beatmaps[i]; 59 | 60 | const play = new taiko_ppv2().setPerformance(score).setDifficulty(beatmap); 61 | const pp = await play.compute(); 62 | 63 | assert.almostEqual(pp.total, score.pp, 0, `/b/${score.beatmap_id} ${beatmap.beatmap.title} (${pp.strain} strain, ${pp.acc} acc) (+${play.mods})`); 64 | }); 65 | } 66 | }); 67 | --------------------------------------------------------------------------------