diff --git a/.babelrc b/.babelrc index 62cb094bb3..5f71f31615 100644 --- a/.babelrc +++ b/.babelrc @@ -1,16 +1,20 @@ { "plugins": [ - ["@babel/plugin-transform-typescript", {"isTSX": true}], + // TypeScript + JSX + ["@babel/plugin-transform-typescript", {"isTSX": true, "allowDeclareFields": true}], ["@babel/plugin-transform-react-jsx", {"pragma": "preact.h", "pragmaFrag": "preact.Fragment", "useBuiltIns": true}], - ["@babel/plugin-proposal-class-properties", {"loose": true}], - ["@babel/plugin-proposal-optional-chaining", {"loose": true}], - ["@babel/plugin-proposal-object-rest-spread", {"loose": true, "useBuiltIns": true}], - "@babel/plugin-proposal-optional-catch-binding", - + // ESNext + "remove-import-export", + "@babel/plugin-transform-logical-assignment-operators", + ["@babel/plugin-transform-nullish-coalescing-operator", {"loose": true}], + ["@babel/plugin-transform-optional-chaining", {"loose": true}], + ["@babel/plugin-transform-object-rest-spread", {"loose": true, "useBuiltIns": true}], + "@babel/plugin-transform-optional-catch-binding", "@babel/plugin-transform-exponentiation-operator", + // ES6 "@babel/plugin-transform-arrow-functions", ["@babel/plugin-transform-block-scoping", {"throwIfClosureRequired": true}], ["@babel/plugin-transform-classes", {"loose": true}], @@ -23,6 +27,7 @@ ["@babel/plugin-transform-spread", {"loose": true}], ["@babel/plugin-transform-template-literals", {"loose": true}], + // ES3 "@babel/plugin-transform-member-expression-literals", "@babel/plugin-transform-property-literals" ], diff --git a/.eslintignore b/.eslintignore index ae1754409a..97369bc727 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,31 +1,16 @@ -data/* node_modules/ -/js/lib/ +/play.pokemonshowdown.com/data/* +/play.pokemonshowdown.com/js/* -/js/battle.js -/js/battledata.js -/js/battle-log.js -/js/battle-choices.js -/js/battle-text-parser.js -/js/battle-dex.js -/js/battle-sound.js -/js/battle-dex-data.js -/js/battle-animations-moves.js -/js/battle-animations.js -/js/battle-tooltips.js -/js/battle-scene-stub.js -/js/battle-dex-search.js -/js/battle-searchresults.js -/js/client-core.js -/js/client-main.js -/js/client-connection.js -/js/panels.js -/js/panel-topbar.js -/js/panel-example.js -/js/panel-mainmenu.js -/js/panel-rooms.js -/js/panel-chat.js -/js/panel-teambuilder.js -/js/panel-teambuilder-team.js -/js/panel-teamdropdown.js -/js/panel-battle.js +!/play.pokemonshowdown.com/js/client-battle.js +!/play.pokemonshowdown.com/js/client-chat-tournament.js +!/play.pokemonshowdown.com/js/client-chat.js +!/play.pokemonshowdown.com/js/client-ladder.js +!/play.pokemonshowdown.com/js/client-mainmenu.js +!/play.pokemonshowdown.com/js/client-rooms.js +!/play.pokemonshowdown.com/js/client-teambuilder.js +!/play.pokemonshowdown.com/js/client-topbar.js +!/play.pokemonshowdown.com/js/client.js +!/play.pokemonshowdown.com/js/replay-embed.template.js +!/play.pokemonshowdown.com/js/search.js +!/play.pokemonshowdown.com/js/storage.js diff --git a/.eslintrc.js b/.eslintrc.js index 9251d4fb1e..f3aa66b17e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,7 +23,7 @@ module.exports = { "BattleFormats": false, "BattleFormatsData": false, "BattleLearnsets": false, "BattleItems": false, "BattleMoveAnims": false, "BattleMovedex": false, "BattleNatures": false, "BattleOtherAnims": false, "BattlePokedex": false,"BattlePokemonSprites": false, "BattlePokemonSpritesBW": false, "BattleSearchCountIndex": false, "BattleSearchIndex": false, "BattleArticleTitles": false, "BattleSearchIndexOffset": false, "BattleSearchIndexType": false, "BattleStatIDs": false, "BattleStatNames": false, "BattleStatusAnims": false, "BattleStatuses": false, "BattleTeambuilderTable": false, - "ModifiableValue": false, "BattleStatGuesser": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false, + "ModifiableValue": false, "BattleStatGuesser": false, "BattleStatOptimizer": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false, "BattleTextParser": false, // Generic global variables diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74de8c123a..b1ea8233f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 961f550364..5a210bb73e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,54 +1,39 @@ -/sprites/ -/audio/ -/config/ -/index.php -/index.html -/preactalpha.html -/crossprotocol.html -/data/* node_modules/ -eslint-cache/ .DS_Store Thumbs.db npm-debug.log -package-lock.json +/config/ /vendor/ +/caches/ + +/play.pokemonshowdown.com/sprites/ +/play.pokemonshowdown.com/audio/ +/play.pokemonshowdown.com/index.php +/play.pokemonshowdown.com/index.html +/play.pokemonshowdown.com/index-test.html +/play.pokemonshowdown.com/preactalpha.html +/play.pokemonshowdown.com/crossprotocol.html +/play.pokemonshowdown.com/data/ -/js/battle.js -/js/battledata.js -/js/battle-log.js -/js/battle-choices.js -/js/battle-text-parser.js -/js/battle-dex.js -/js/battle-sound.js -/js/battle-dex-data.js -/js/battle-animations-moves.js -/js/battle-animations.js -/js/battle-tooltips.js -/js/battle-scene-stub.js -/js/battle-dex-search.js -/js/battle-searchresults.js -/js/client-core.js -/js/client-main.js -/js/client-connection.js -/js/panels.js -/js/panel-topbar.js -/js/panel-example.js -/js/panel-mainmenu.js -/js/panel-rooms.js -/js/panel-chat.js -/js/panel-teambuilder.js -/js/panel-teambuilder-team.js -/js/panel-teamdropdown.js -/js/panel-battle.js -/js/replay-embed.js -/js/*.js.map +/play.pokemonshowdown.com/js/server/ +/play.pokemonshowdown.com/js/*.js.map +/play.pokemonshowdown.com/js/battle*.js +/play.pokemonshowdown.com/js/panel*.js +/play.pokemonshowdown.com/js/replay-embed.js +/play.pokemonshowdown.com/js/client-main.js +/play.pokemonshowdown.com/js/client-core.js +/play.pokemonshowdown.com/js/client-connection.js +/play.pokemonshowdown.com/js/miniedit.js +/play.pokemonshowdown.com/ads.txt -/replays/caches/ -/replays/replay-config.inc.php -/replays/theme/wrapper.inc.php +/pokemonshowdown.com/.well-known/ +/pokemonshowdown.com/.pages-cached/ +/pokemonshowdown.com/files/ +/pokemonshowdown.com/images/ +/pokemonshowdown.com/ads.txt -/website/.well-known/ -/website/.pages-cached/ -/website/files/ -/website/images/ +/replay.pokemonshowdown.com/index.php +/replay.pokemonshowdown.com/js/ +/replay.pokemonshowdown.com/caches/ +/replay.pokemonshowdown.com/theme/wrapper.inc.php +/replay.pokemonshowdown.com/ads.txt diff --git a/.vscode/settings.json b/.vscode/settings.json index 675fc65ae3..3d7d233831 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.formatOnSave": false, "showdown.server": "", // e.g., "?~~localhost:8000" "showdown.clientUrl": "http://localhost:8080", - "tslint.configFile": "tslint.json" + "tslint.configFile": "tslint.json", + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..a881151496 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": [], + "label": "npm: build", + "detail": "node build" + } + ] +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..49b6cf3967 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +play.pokemonshowdown.com/src/battle-animations-moves.ts @KrisXV +play.pokemonshowdown.com/src/battle-animations.ts @KrisXV \ No newline at end of file diff --git a/README.md b/README.md index 497d10c48c..0f4dce16ce 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Testing ------------------------------------------------------------------------ Client testing now requires a build step! Install the latest Node.js (we -require v10 or later) and Git, and run `node build` (on Windows) or `./build` +require v14 or later) and Git, and run `node build` (on Windows) or `./build` (on other OSes) to build. You can make and test client changes simply by building after each change, @@ -56,7 +56,7 @@ they can't screw with your account, but it does make it harder to log in on the test client. The default hack makes you copy/paste the data instead, but if you're -refreshing a lot, just add a `config/testclient-key.js file`, with the +refreshing a lot, just add a `config/testclient-key.js` file, with the contents: const POKEMON_SHOWDOWN_TESTCLIENT_KEY = 'sid'; diff --git a/WEB-API.md b/WEB-API.md index 403c145358..64f9cd57ee 100644 --- a/WEB-API.md +++ b/WEB-API.md @@ -15,6 +15,14 @@ https://replay.pokemonshowdown.com/gen8doublesubers-1097585496.json https://replay.pokemonshowdown.com/gen8doublesubers-1097585496.log +Getting a replay inputlog directly (only for formats where the team is autogenerated): + +https://replay.pokemonshowdown.com/gen8randombattle-2005209836.inputlog + +Replay logs and inputlogs are also available in the JSON, so the `.log` and `.inputlog` forms are provided only for convenience. + +Also for convenience: scrolling down in the source code for the replay page. Obviously don't _scrape_ it, but `ctrl`+`u` is way faster than futzing with URLs if you just wanted to take a look at it. + Replay search ------------- @@ -41,11 +49,11 @@ https://replay.pokemonshowdown.com/search.json?user=zarel&user2=yuyuko&format=ge Paginate searches: -https://replay.pokemonshowdown.com/search.json?user=zarel&page=2 +https://replay.pokemonshowdown.com/search.json?user=zarel&before=1372221987 -Searches are limited to 51 results, and pages are offset by 50 each, so the existence of a 51st result means that there's at least one more page available. +Searches are limited to 51 results, and pages are offset by 50 each, so the existence of a 51st result means that there's at least one more page available. For the timestamp, use the uploadtime of the last replay in the list. -Pagination is not supported for the recent replays list, but is supported for everything else. +You can also use `page=[number]`, but this is an older API that is poorly supported and should not be relied upon. Users (including ladder information) @@ -66,3 +74,11 @@ News https://pokemonshowdown.com/news.json https://pokemonshowdown.com/news/270.json + + +Dex resources +------------- + +https://play.pokemonshowdown.com/data/pokedex.json + +https://play.pokemonshowdown.com/data/moves.json diff --git a/action.php b/action.php deleted file mode 100644 index fc8b68ea1d..0000000000 --- a/action.php +++ /dev/null @@ -1,39 +0,0 @@ - - -*/ - -error_reporting(E_ALL); - -include_once __DIR__ . '/config/config.inc.php'; - -if (@$_GET['act'] === 'dlteam') { - header("Content-Type: text/plain; charset=utf-8"); - if (substr(@$_SERVER['HTTP_REFERER'], 0, 32) !== 'https://' . $psconfig['routes']['client']) { - // since this is only to support Chrome on HTTPS, we can get away with a very specific referer check - die("access denied"); - } - echo base64_decode(@$_GET['team']); - die(); -} - -if (preg_match('/^http\\:\\/\\/[a-z0-9]+\\.psim\\.us\\//', $_SERVER['HTTP_REFERER'] ?? '')) { - header("Access-Control-Allow-Origin: *"); -} else if ($_POST['sid'] ?? null) { - header("Access-Control-Allow-Origin: *"); -} -// header("X-Debug: " . @$_SERVER['HTTP_REFERER']); - -require_once __DIR__ . '/lib/ntbb-session.lib.php'; -include_once __DIR__ . '/config/servers.inc.php'; -include_once __DIR__ . '/lib/dispatcher.lib.php'; - -$dispatcher = new ActionDispatcher(array( - new DefaultActionHandler(), - new LadderActionHandler() -)); -$dispatcher->executeActions(); diff --git a/ads.txt b/ads.txt deleted file mode 100644 index 0a7f85be06..0000000000 --- a/ads.txt +++ /dev/null @@ -1 +0,0 @@ -google.com, pub-6535472412829264, DIRECT, f08c47fec0942fa0 diff --git a/build b/build index 39bf326611..b4ee9a802f 100755 --- a/build +++ b/build @@ -49,9 +49,6 @@ case 'minidex': case 'sprites': execSync(`node ./build-tools/build-minidex`, options); break; -case 'sets': - execSync(`node ./build-tools/build-sets`, options); - break; case 'replays': execSync(`node ./replays/build`, options); process.exit(); diff --git a/build-tools/.eslintrc.js b/build-tools/.eslintrc.js index c76c719d0b..602ac6e65b 100644 --- a/build-tools/.eslintrc.js +++ b/build-tools/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { "no-confusing-arrow": 0, "no-const-assign": 2, "no-dupe-class-members": 2, + "no-restricted-syntax": "off", "no-this-before-super": 2, "no-var": 2, "require-yield": 2, diff --git a/build-tools/babel-cli/README.md b/build-tools/babel-cli/README.md deleted file mode 100644 index 28c91cbc5e..0000000000 --- a/build-tools/babel-cli/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This is just a fork of babel-cli to support incremental builds. - -See: https://github.com/babel/babel/pull/8877 diff --git a/build-tools/babel-cli/bin/babel.js b/build-tools/babel-cli/bin/babel.js deleted file mode 100755 index 7c227455f1..0000000000 --- a/build-tools/babel-cli/bin/babel.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require("../lib/babel"); diff --git a/build-tools/babel-cli/lib/babel/dir.js b/build-tools/babel-cli/lib/babel/dir.js deleted file mode 100644 index 9b857ac4a2..0000000000 --- a/build-tools/babel-cli/lib/babel/dir.js +++ /dev/null @@ -1,229 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = _default; - -function _defaults() { - const data = _interopRequireDefault(require("lodash/defaults")); - - _defaults = function () { - return data; - }; - - return data; -} - -function _outputFileSync() { - const data = _interopRequireDefault(require("output-file-sync")); - - _outputFileSync = function () { - return data; - }; - - return data; -} - -function _mkdirp() { - const data = require("mkdirp"); - - _mkdirp = function () { - return data; - }; - - return data; -} - -function _slash() { - const data = _interopRequireDefault(require("slash")); - - _slash = function () { - return data; - }; - - return data; -} - -function _path() { - const data = _interopRequireDefault(require("path")); - - _path = function () { - return data; - }; - - return data; -} - -function _fs() { - const data = _interopRequireDefault(require("fs")); - - _fs = function () { - return data; - }; - - return data; -} - -var util = _interopRequireWildcard(require("./util")); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const NOT_COMPILABLE = null; - -async function _default({ - cliOptions, - babelOptions -}) { - const filenames = cliOptions.filenames; - - async function write(src, base) { - let relative = _path().default.relative(base, src); - - if (!util.isCompilableExtension(relative, cliOptions.extensions)) { - return NOT_COMPILABLE; - } - - relative = util.adjustRelative(relative, cliOptions.keepFileExtension); - const dest = getDest(relative, base); - - if (cliOptions.incremental) { - try { - const srcStat = _fs().default.statSync(src); - - const destStat = _fs().default.statSync(dest); - - if (srcStat.ctimeMs < destStat.ctimeMs) return false; - } catch (e) {} - } - - try { - const res = await util.compile(src, (0, _defaults().default)({ - sourceFileName: (0, _slash().default)(_path().default.relative(dest + "/..", src)) - }, babelOptions)); - if (!res) return NOT_COMPILABLE; - - if (res.map && babelOptions.sourceMaps && babelOptions.sourceMaps !== "inline") { - const mapLoc = dest + ".map"; - res.code = util.addSourceMappingUrl(res.code, mapLoc); - res.map.file = _path().default.basename(relative); - (0, _outputFileSync().default)(mapLoc, JSON.stringify(res.map)); - } - - (0, _outputFileSync().default)(dest, res.code); - util.chmod(src, dest); - - if (cliOptions.verbose) { - console.log(src + " -> " + dest); - } - - return true; - } catch (err) { - if (cliOptions.watch) { - console.error(err); - return false; - } - - throw err; - } - } - - function getDest(filename, base) { - if (cliOptions.relative) { - return _path().default.join(base, cliOptions.outDir, filename); - } - - return _path().default.join(cliOptions.outDir, filename); - } - - async function handleFile(src, base) { - const written = await write(src, base); - - if (written === NOT_COMPILABLE) { - if (!cliOptions.copyFiles) return false; - - const filename = _path().default.relative(base, src); - - const dest = getDest(filename, base); - - if (cliOptions.incremental) { - try { - const srcStat = _fs().default.statSync(src); - - const destStat = _fs().default.statSync(dest); - - if (srcStat.ctimeMs < destStat.ctimeMs) return false; - } catch (e) {} - } - - (0, _outputFileSync().default)(dest, _fs().default.readFileSync(src)); - util.chmod(src, dest); - return false; - } - - return written; - } - - async function handle(filenameOrDir) { - if (!_fs().default.existsSync(filenameOrDir)) return 0; - - const stat = _fs().default.statSync(filenameOrDir); - - if (stat.isDirectory()) { - const dirname = filenameOrDir; - let count = 0; - const files = util.readdir(dirname, cliOptions.includeDotfiles); - - for (const filename of files) { - const src = _path().default.join(dirname, filename); - - const written = await handleFile(src, dirname); - if (written) count += 1; - } - - return count; - } else { - const filename = filenameOrDir; - const written = await handleFile(filename, _path().default.dirname(filename)); - return written ? 1 : 0; - } - } - - if (!cliOptions.skipInitialBuild) { - if (cliOptions.deleteDirOnStart) { - util.deleteDir(cliOptions.outDir); - } - - (0, _mkdirp().sync)(cliOptions.outDir); - let compiledFiles = 0; - - for (const filename of cliOptions.filenames) { - compiledFiles += await handle(filename); - } - - console.log(`Successfully compiled ${compiledFiles} ${compiledFiles !== 1 ? "files" : "file"} with Babel.`); - } - - if (cliOptions.watch) { - const chokidar = util.requireChokidar(); - filenames.forEach(function (filenameOrDir) { - const watcher = chokidar.watch(filenameOrDir, { - persistent: true, - ignoreInitial: true, - awaitWriteFinish: { - stabilityThreshold: 50, - pollInterval: 10 - } - }); - ["add", "change"].forEach(function (type) { - watcher.on(type, function (filename) { - handleFile(filename, filename === filenameOrDir ? _path().default.dirname(filenameOrDir) : filenameOrDir).catch(err => { - console.error(err); - }); - }); - }); - }); - } -} \ No newline at end of file diff --git a/build-tools/babel-cli/lib/babel/index.js b/build-tools/babel-cli/lib/babel/index.js deleted file mode 100755 index 66e4e11cdf..0000000000 --- a/build-tools/babel-cli/lib/babel/index.js +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -var _options = _interopRequireDefault(require("./options")); - -var _dir = _interopRequireDefault(require("./dir")); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -const opts = (0, _options.default)(process.argv); - -if (!opts.cliOptions.outDir) throw new Error("This fork of babel-cli is cut down to only what Pokemon-Showdown-Client uses, and only supports `--out-dir` mode"); - -const fn = _dir.default; -fn(opts).catch(err => { - console.error(err); - process.exit(1); -}); \ No newline at end of file diff --git a/build-tools/babel-cli/lib/babel/options.js b/build-tools/babel-cli/lib/babel/options.js deleted file mode 100644 index 94a8681d84..0000000000 --- a/build-tools/babel-cli/lib/babel/options.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = parseArgv; - -function _fs() { - const data = _interopRequireDefault(require("fs")); - - _fs = function () { - return data; - }; - - return data; -} - -function _commander() { - const data = _interopRequireDefault(require("commander")); - - _commander = function () { - return data; - }; - - return data; -} - -function _core() { - const data = require("@babel/core"); - - _core = function () { - return data; - }; - - return data; -} - -function _uniq() { - const data = _interopRequireDefault(require("lodash/uniq")); - - _uniq = function () { - return data; - }; - - return data; -} - -function _glob() { - const data = _interopRequireDefault(require("glob")); - - _glob = function () { - return data; - }; - - return data; -} - -var _package = _interopRequireDefault(require("../../package.json")); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -_commander().default.option("-f, --filename [filename]", "filename to use when reading from stdin - this will be used in source-maps, errors etc"); - -_commander().default.option("--presets [list]", "comma-separated list of preset names", collect); - -_commander().default.option("--plugins [list]", "comma-separated list of plugin names", collect); - -_commander().default.option("--config-file [path]", "Path a to .babelrc file to use"); - -_commander().default.option("--env-name [name]", "The name of the 'env' to use when loading configs and plugins. " + "Defaults to the value of BABEL_ENV, or else NODE_ENV, or else 'development'."); - -_commander().default.option("--root-mode [mode]", "The project-root resolution mode. " + "One of 'root' (the default), 'upward', or 'upward-optional'."); - -_commander().default.option("--source-type [script|module]", ""); - -_commander().default.option("--no-babelrc", "Whether or not to look up .babelrc and .babelignore files"); - -_commander().default.option("--ignore [list]", "list of glob paths to **not** compile", collect); - -_commander().default.option("--only [list]", "list of glob paths to **only** compile", collect); - -_commander().default.option("--no-highlight-code", "enable/disable ANSI syntax highlighting of code frames (on by default)"); - -_commander().default.option("--no-comments", "write comments to generated output (true by default)"); - -_commander().default.option("--retain-lines", "retain line numbers - will result in really ugly code"); - -_commander().default.option("--compact [true|false|auto]", "do not include superfluous whitespace characters and line terminators", booleanify); - -_commander().default.option("--minified", "save as much bytes when printing [true|false]"); - -_commander().default.option("--auxiliary-comment-before [string]", "print a comment before any injected non-user code"); - -_commander().default.option("--auxiliary-comment-after [string]", "print a comment after any injected non-user code"); - -_commander().default.option("-s, --source-maps [true|false|inline|both]", "", booleanify); - -_commander().default.option("--source-map-target [string]", "set `file` on returned source map"); - -_commander().default.option("--source-file-name [string]", "set `sources[0]` on returned source map"); - -_commander().default.option("--source-root [filename]", "the root from which all sources are relative"); - -_commander().default.option("--module-root [filename]", "optional prefix for the AMD module formatter that will be prepend to the filename on module definitions"); - -_commander().default.option("-M, --module-ids", "insert an explicit id for modules"); - -_commander().default.option("--module-id [string]", "specify a custom name for module ids"); - -_commander().default.option("-x, --extensions [extensions]", "List of extensions to compile when a directory has been input [.es6,.js,.es,.jsx,.mjs]", collect); - -_commander().default.option("--keep-file-extension", "Preserve the file extensions of the input files"); - -_commander().default.option("-w, --watch", "Recompile files on changes"); - -_commander().default.option("--skip-initial-build", "Do not compile files before watching"); - -_commander().default.option("--incremental", "Only compile files with modification time before corresponding output file"); - -_commander().default.option("-o, --out-file [out]", "Compile all input files into a single file"); - -_commander().default.option("-d, --out-dir [out]", "Compile an input directory of modules into an output directory"); - -_commander().default.option("--relative", "Compile into an output directory relative to input directory or file. Requires --out-dir [out]"); - -_commander().default.option("-D, --copy-files", "When compiling a directory copy over non-compilable files"); - -_commander().default.option("--include-dotfiles", "Include dotfiles when compiling and copying non-compilable files"); - -_commander().default.option("--verbose", "Log everything"); - -_commander().default.option("--delete-dir-on-start", "Delete the out directory before compilation"); - -_commander().default.version(_package.default.version + " (@babel/core " + _core().version + ")"); - -_commander().default.usage("[options] "); - -function parseArgv(args) { - _commander().default.parse(args); - - const errors = []; - - let filenames = _commander().default.args.reduce(function (globbed, input) { - let files = _glob().default.sync(input); - - if (!files.length) files = [input]; - return globbed.concat(files); - }, []); - - filenames = (0, _uniq().default)(filenames); - filenames.forEach(function (filename) { - if (!_fs().default.existsSync(filename)) { - errors.push(filename + " does not exist"); - } - }); - - if (_commander().default.outDir && !filenames.length) { - errors.push("--out-dir requires filenames"); - } - - if (_commander().default.outFile && _commander().default.outDir) { - errors.push("--out-file and --out-dir cannot be used together"); - } - - if (_commander().default.relative && !_commander().default.outDir) { - errors.push("--relative requires --out-dir usage"); - } - - if (_commander().default.watch) { - if (!_commander().default.outFile && !_commander().default.outDir) { - errors.push("--watch requires --out-file or --out-dir"); - } - - if (!filenames.length) { - errors.push("--watch requires filenames"); - } - } - - if (_commander().default.skipInitialBuild && !_commander().default.watch) { - errors.push("--skip-initial-build requires --watch"); - } - - if (_commander().default.incremental && !_commander().default.outDir) { - errors.push("--incremental requires --out-dir"); - } - - if (_commander().default.deleteDirOnStart && !_commander().default.outDir) { - errors.push("--delete-dir-on-start requires --out-dir"); - } - - if (!_commander().default.outDir && filenames.length === 0 && typeof _commander().default.filename !== "string" && _commander().default.babelrc !== false) { - errors.push("stdin compilation requires either -f/--filename [filename] or --no-babelrc"); - } - - if (errors.length) { - console.error("babel:"); - errors.forEach(function (e) { - console.error(" " + e); - }); - process.exit(2); - } - - const opts = _commander().default.opts(); - - const babelOptions = { - presets: opts.presets, - plugins: opts.plugins, - rootMode: opts.rootMode, - configFile: opts.configFile, - envName: opts.envName, - sourceType: opts.sourceType, - ignore: opts.ignore, - only: opts.only, - retainLines: opts.retainLines, - compact: opts.compact, - minified: opts.minified, - auxiliaryCommentBefore: opts.auxiliaryCommentBefore, - auxiliaryCommentAfter: opts.auxiliaryCommentAfter, - sourceMaps: opts.sourceMaps, - sourceFileName: opts.sourceFileName, - sourceRoot: opts.sourceRoot, - moduleRoot: opts.moduleRoot, - moduleIds: opts.moduleIds, - moduleId: opts.moduleId, - babelrc: opts.babelrc === true ? undefined : opts.babelrc, - highlightCode: opts.highlightCode === true ? undefined : opts.highlightCode, - comments: opts.comments === true ? undefined : opts.comments - }; - - for (const key of Object.keys(babelOptions)) { - if (babelOptions[key] === undefined) { - delete babelOptions[key]; - } - } - - return { - babelOptions, - cliOptions: { - filename: opts.filename, - filenames, - extensions: opts.extensions, - keepFileExtension: opts.keepFileExtension, - watch: opts.watch, - skipInitialBuild: opts.skipInitialBuild, - incremental: opts.incremental, - outFile: opts.outFile, - outDir: opts.outDir, - relative: opts.relative, - copyFiles: opts.copyFiles, - includeDotfiles: opts.includeDotfiles, - verbose: opts.verbose, - deleteDirOnStart: opts.deleteDirOnStart, - sourceMapTarget: opts.sourceMapTarget - } - }; -} - -function booleanify(val) { - if (val === "true" || val == 1) { - return true; - } - - if (val === "false" || val == 0 || !val) { - return false; - } - - return val; -} - -function collect(value, previousValue) { - if (typeof value !== "string") return previousValue; - const values = value.split(","); - return previousValue ? previousValue.concat(values) : values; -} \ No newline at end of file diff --git a/build-tools/babel-cli/lib/babel/util.js b/build-tools/babel-cli/lib/babel/util.js deleted file mode 100644 index cc50ffd3e5..0000000000 --- a/build-tools/babel-cli/lib/babel/util.js +++ /dev/null @@ -1,163 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.chmod = chmod; -exports.readdir = readdir; -exports.readdirForCompilable = readdirForCompilable; -exports.isCompilableExtension = isCompilableExtension; -exports.addSourceMappingUrl = addSourceMappingUrl; -exports.transform = transform; -exports.compile = compile; -exports.deleteDir = deleteDir; -exports.requireChokidar = requireChokidar; -exports.adjustRelative = adjustRelative; - -function _fsReaddirRecursive() { - const data = _interopRequireDefault(require("fs-readdir-recursive")); - - _fsReaddirRecursive = function () { - return data; - }; - - return data; -} - -function babel() { - const data = _interopRequireWildcard(require("@babel/core")); - - babel = function () { - return data; - }; - - return data; -} - -function _includes() { - const data = _interopRequireDefault(require("lodash/includes")); - - _includes = function () { - return data; - }; - - return data; -} - -function _path() { - const data = _interopRequireDefault(require("path")); - - _path = function () { - return data; - }; - - return data; -} - -function _fs() { - const data = _interopRequireDefault(require("fs")); - - _fs = function () { - return data; - }; - - return data; -} - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function chmod(src, dest) { - _fs().default.chmodSync(dest, _fs().default.statSync(src).mode); -} - -function readdir(dirname, includeDotfiles, filter) { - return (0, _fsReaddirRecursive().default)(dirname, (filename, _index, currentDirectory) => { - const stat = _fs().default.statSync(_path().default.join(currentDirectory, filename)); - - if (stat.isDirectory()) return true; - return (includeDotfiles || filename[0] !== ".") && (!filter || filter(filename)); - }); -} - -function readdirForCompilable(dirname, includeDotfiles) { - return readdir(dirname, includeDotfiles, isCompilableExtension); -} - -function isCompilableExtension(filename, altExts) { - const exts = altExts || babel().DEFAULT_EXTENSIONS; - - const ext = _path().default.extname(filename); - - return (0, _includes().default)(exts, ext); -} - -function addSourceMappingUrl(code, loc) { - return code + "\n//# sourceMappingURL=" + _path().default.basename(loc); -} - -const CALLER = { - name: "@babel/cli" -}; - -function transform(filename, code, opts) { - opts = Object.assign({}, opts, { - caller: CALLER, - filename - }); - return new Promise((resolve, reject) => { - babel().transform(code, opts, (err, result) => { - if (err) reject(err);else resolve(result); - }); - }); -} - -function compile(filename, opts) { - opts = Object.assign({}, opts, { - caller: CALLER - }); - return new Promise((resolve, reject) => { - babel().transformFile(filename, opts, (err, result) => { - if (err) reject(err);else resolve(result); - }); - }); -} - -function deleteDir(path) { - if (_fs().default.existsSync(path)) { - _fs().default.readdirSync(path).forEach(function (file) { - const curPath = path + "/" + file; - - if (_fs().default.lstatSync(curPath).isDirectory()) { - deleteDir(curPath); - } else { - _fs().default.unlinkSync(curPath); - } - }); - - _fs().default.rmdirSync(path); - } -} - -process.on("uncaughtException", function (err) { - console.error(err); - process.exit(1); -}); - -function requireChokidar() { - try { - return require("chokidar"); - } catch (err) { - console.error("The optional dependency chokidar failed to install and is required for " + "--watch. Chokidar is likely not supported on your platform."); - throw err; - } -} - -function adjustRelative(relative, keepFileExtension) { - if (keepFileExtension) { - return relative; - } - - return relative.replace(/\.(\w*?)$/, "") + ".js"; -} \ No newline at end of file diff --git a/build-tools/babel-cli/package.json b/build-tools/babel-cli/package.json deleted file mode 100644 index b520ad82f9..0000000000 --- a/build-tools/babel-cli/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@babel/cli", - "version": "7.1.2", - "description": "Babel command line.", - "author": "Sebastian McKenzie ", - "homepage": "https://babeljs.io/", - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "repository": "https://github.com/babel/babel/tree/master/packages/babel-cli", - "keywords": [ - "6to5", - "babel", - "es6", - "transpile", - "transpiler", - "babel-cli", - "compiler" - ], - "dependencies": { - "commander": "^2.8.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "lodash": "^4.17.10", - "mkdirp": "^0.5.1", - "output-file-sync": "^2.0.0", - "slash": "^2.0.0", - "source-map": "^0.5.0" - }, - "optionalDependencies": { - "chokidar": "^2.0.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "devDependencies": { - "@babel/core": "^7.0.0", - "@babel/helper-fixtures": "^7.0.0" - }, - "bin": { - "babel": "./bin/babel.js", - "babel-external-helpers": "./bin/babel-external-helpers.js" - } -} diff --git a/build-tools/build-indexes b/build-tools/build-indexes index 6af02d26ac..aa13c390fd 100755 --- a/build-tools/build-indexes +++ b/build-tools/build-indexes @@ -8,28 +8,28 @@ const child_process = require("child_process"); const rootDir = path.resolve(__dirname, '..'); process.chdir(rootDir); -if (!fs.existsSync('data/pokemon-showdown')) { +if (!fs.existsSync('caches/pokemon-showdown')) { child_process.execSync('git clone https://github.com/smogon/pokemon-showdown.git', { - cwd: 'data', + cwd: 'caches', }); } process.stdout.write("Syncing data from Git repository... "); -child_process.execSync('git pull', {cwd: 'data/pokemon-showdown'}); -child_process.execSync('npm run build', {cwd: 'data/pokemon-showdown'}); +child_process.execSync('git pull', {cwd: 'caches/pokemon-showdown'}); +child_process.execSync('npm run build', {cwd: 'caches/pokemon-showdown'}); console.log("DONE"); -const Dex = require('../data/pokemon-showdown/.sim-dist/dex').Dex; +const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex; const toID = Dex.toID; process.stdout.write("Loading gen 6 data... "); Dex.includeData(); console.log("DONE"); function es3stringify(obj) { - let buf = JSON.stringify(obj); - buf = buf.replace(/\"([A-Za-z][A-Za-z0-9]*)\"\:/g, '$1:'); - buf = buf.replace(/return\:/g, '"return":').replace(/new\:/g, '"new":').replace(/delete\:/g, '"delete":'); - return buf; + const buf = JSON.stringify(obj); + return buf.replace(/\"([A-Za-z][A-Za-z0-9]*)\"\:/g, (fullMatch, key) => ( + ['return', 'new', 'delete'].includes(key) ? fullMatch : `${key}:` + )); } function requireNoCache(pathSpec) { @@ -53,7 +53,7 @@ function requireNoCache(pathSpec) { index = index.concat(Object.keys(Dex.data.TypeChart).map(x => toID(x) + ' type')); index = index.concat(['physical', 'special', 'status'].map(x => toID(x) + ' category')); index = index.concat(['monster', 'water1', 'bug', 'flying', 'field', 'fairy', 'grass', 'humanlike', 'water3', 'mineral', 'amorphous', 'water2', 'ditto', 'dragon', 'undiscovered'].map(x => toID(x) + ' egggroup')); - index = index.concat(['ou', 'uu', 'ru', 'nu', 'pu', 'lc', 'nfe', 'uber', 'uubl', 'rubl', 'nubl', 'publ', 'cap', 'caplc', 'capnfe'].map(x => toID(x) + ' tier')); + index = index.concat(['ou', 'uu', 'ru', 'nu', 'pu', 'zu', 'lc', 'nfe', 'uber', 'uubl', 'rubl', 'nubl', 'publ', 'zubl', 'cap', 'caplc', 'capnfe'].map(x => toID(x) + ' tier')); let BattleArticleTitles = {}; @@ -97,10 +97,42 @@ function requireNoCache(pathSpec) { return; } let oldI = i; + if (name === 'Alakazam') i = 5; + if (name === 'Arctovish') i = 5; + if (name === 'Arctozolt') i = 5; + if (name === 'Articuno') i = 5; + if (name === 'Breloom') i = 3; if (name === 'Bronzong') i = 4; + if (name === 'Celebi') i = 4; if (name === 'Charizard') i = 5; + if (name === 'Donphan') i = 3; + if (name === 'Dracovish') i = 5; + if (name === 'Dracozolt') i = 5; + if (name === 'Dragapult') i = 5; + if (name === 'Dusclops') i = 3; + if (name === 'Electabuzz') i = 6; + if (name === 'Exeggutor') i = 2; if (name === 'Garchomp') i = 3; if (name === 'Hariyama') i = 4; + if (name === 'Magearna') i = 2; + if (name === 'Magnezone') i = 5; + if (name === 'Mamoswine') i = 4; + if (name === 'Moltres') i = 3; + if (name === 'Nidoking') i = 4; + if (name === 'Nidoqueen') i = 4; + if (name === 'Nidorina') i = 4; + if (name === 'Nidorino') i = 4; + if (name === 'Regice') i = 3; + if (name === 'Regidrago') i = 4; + if (name === 'Regieleki') i = 4; + if (name === 'Regigigas') i = 4; + if (name === 'Regirock') i = 4; + if (name === 'Registeel') i = 4; + if (name === 'Slowbro') i = 4; + if (name === 'Slowking') i = 4; + if (name === 'Starmie') i = 4; + if (name === 'Tyranitar') i = 6; + if (name === 'Zapdos') i = 3; if (name === 'Acupressure') i = 3; if (name === 'Aromatherapy') i = 5; @@ -164,6 +196,8 @@ function requireNoCache(pathSpec) { index.push('wupslap ' + type + ' ' + id + ' 0'); } else if (name === 'Zen Headbutt') { index.push('zhbutt ' + type + ' ' + id + ' 0'); + } else if (name === 'Articuno') { + index.push('cuno ' + type + ' ' + id + ' 4'); } let i2 = name.lastIndexOf(' ', i - 1); @@ -228,10 +262,10 @@ function requireNoCache(pathSpec) { const id = entry[0]; let name = ''; switch (entry[1]) { - case 'pokemon': name = Dex.getSpecies(id).name; break; - case 'move': name = Dex.getMove(id).name; break; - case 'item': name = Dex.getItem(id).name; break; - case 'ability': name = Dex.getAbility(id).name; break; + case 'pokemon': name = Dex.species.get(id).name; break; + case 'move': name = Dex.moves.get(id).name; break; + case 'item': name = Dex.items.get(id).name; break; + case 'ability': name = Dex.abilities.get(id).name; break; case 'article': name = BattleArticleTitles[id] || ''; break; } let res = ''; @@ -266,7 +300,7 @@ function requireNoCache(pathSpec) { buf += 'exports.BattleArticleTitles = ' + JSON.stringify(BattleArticleTitles) + ';\n\n'; - fs.writeFileSync('data/search-index.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/search-index.js', buf); } console.log("DONE"); @@ -275,82 +309,72 @@ console.log("DONE"); * Build teambuilder-tables.js *********************************************************/ -const restrictedLegends = ['Mewtwo', 'Lugia', 'Ho-Oh', 'Kyogre', 'Groudon', 'Rayquaza', 'Dialga', 'Palkia', 'Giratina', 'Reshiram', 'Zekrom', 'Kyurem', 'Xerneas', 'Yveltal', 'Zygarde', 'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', 'Necrozma']; -const mythicals = ['Mew', 'Celebi', 'Jirachi', 'Deoxys', 'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Arceus', 'Victini', 'Keldeo', 'Meloetta', 'Genesect', 'Diancie', 'Hoopa', 'Volcanion', 'Greninja-Ash', 'Magearna', 'Marshadow', 'Zeraora']; - process.stdout.write("Building `data/teambuilder-tables.js`... "); { const BattleTeambuilderTable = {}; let buf = '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n'; - const GENS = [8, 7, 6, 5, 4, 3, 2, 1]; + const GENS = [9, 8, 7, 6, 5, 4, 3, 2, 1]; const DOUBLES = GENS.filter(x => x > 2).map(num => -num); + const VGC = GENS.filter(x => x > 3).map(num => -num - 0.5); const NFE = GENS.map(num => num + 0.3); - const OTHER = [8.4, 8.2, 8.1, -8.4, 7.1, -7.5]; + const LC = GENS.map(num => num + 0.7); + const STADIUM = [2.04, 1.04]; + const NATDEX = [9.1, 8.1]; + const OTHER = [9.9, 9.6, 9.411, 9.41, 9.401, 9.4, 9.2, -9.4, -9.401, 8.6, 8.4, 8.2, 8.1, -8.4, -8.6, 7.1, 5.1]; // process.stdout.write("\n "); - for (const genIdent of [...GENS, ...DOUBLES, ...NFE, ...OTHER]) { + for (const genIdent of [...GENS, ...DOUBLES, ...VGC, ...NFE, ...STADIUM, ...OTHER, ...NATDEX, ...LC]) { const isLetsGo = (genIdent === 7.1); - const isMetBattle = (genIdent === 8.2); - const isNFE = (('' + genIdent).endsWith('.3')); - const isDLC1 = (genIdent === 8.4 || genIdent === -8.4); - const isNatDex = (genIdent === 8.1); + const isBDSP = (genIdent === 8.6 || genIdent === -8.6); + const isMetBattle = ('' + genIdent).endsWith('.2'); + const isNFE = ('' + genIdent).endsWith('.3'); + const isLC = ('' + genIdent).endsWith('.7'); + const isSSDLC1 = (genIdent === 8.4 || genIdent === -8.4); + const isPreDLC = (genIdent === 9.4 || genIdent === 9.41 || genIdent === -9.4); + const isSVDLC1 = (genIdent === 9.401 || genIdent === 9.411 || genIdent === -9.401); + const isNatDex = ('' + genIdent).endsWith('.1') && genIdent > 8; + const isStadium = ('' + genIdent).endsWith('.04'); const isDoubles = (genIdent < 0); - const isVGC = (genIdent === -7.5); + const isVGC = ('' + genIdent).endsWith('.5'); + const isGen9BH = genIdent === 9.9; + const isSSB = genIdent === 9.6; const genNum = Math.floor(isDoubles ? -genIdent : genIdent); - const gen = isLetsGo ? 'letsgo' : 'gen' + genNum + (isDLC1 ? 'dlc1' : ''); + const isBW1 = genIdent === 5.1; + const gen = (() => { + let genStr = 'gen' + genNum; + if (isSSDLC1) genStr += 'dlc1'; + if (isLetsGo) genStr += 'letsgo'; + if (isBDSP) genStr += 'bdsp'; + if (isPreDLC) genStr += 'predlc'; + if (isSVDLC1) genStr += 'dlc1'; + if (isStadium) genStr += 'stadium' + (genNum > 1 ? genNum : ''); + if (isSSB) genStr += 'ssb'; + if (isBW1) genStr += 'bw1'; + return genStr; + })(); // process.stdout.write("" + gen + (isDoubles ? " doubles" : "") + "... "); const pokemon = Object.keys(Dex.data.Pokedex); pokemon.sort(); const tierTable = {}; const overrideTier = {}; - const zuBans = {}; + const ubersUUBans = {}; + const ndDoublesBans = {}; + const thirtyfivePokes = {}; + const monotypeBans = {}; const nonstandardMoves = []; + const gen5zuBans = {}; for (const id of pokemon) { - const species = Dex.mod(gen).getSpecies(id); + const species = Dex.mod(gen).species.get(id); + const baseSpecies = Dex.mod(gen).species.get(species.baseSpecies); if (species.gen > genNum) continue; const tier = (() => { - if (isNatDex) { - const unobtainables = [ - 'Eevee-Starter', 'Floette-Eternal', 'Pichu-Spiky-eared', 'Pikachu-Belle', 'Pikachu-Cosplay', 'Pikachu-Libre', 'Pikachu-PhD', 'Pikachu-Pop-Star', 'Pikachu-Rock-Star', 'Pikachu-Starter', 'Eternatus-Eternamax', - ].map(toID); - if (species.isNonstandard && !['Past', 'Gigantamax'].includes(species.isNonstandard)) return 'Illegal'; - if (unobtainables.includes(species.id)) return 'Illegal'; - const uu = Dex.getFormat('gen8nationaldexuu'); - const ou = Dex.getFormat('gen8nationaldex'); - const uublIndex = uu.banlist.map(toID).indexOf('nduubl'); - if (Dex.getRuleTable(ou).isBannedSpecies(species)) return 'Uber'; - if (Dex.getRuleTable(ou).has('dynamaxclause') && species.name.endsWith('Gmax')) return '(Uber)'; - if (Dex.getRuleTable(uu).isBannedSpecies(species)) { - if ( - uu.banlist.map(toID).indexOf(species.id) >= uublIndex || - uu.banlist.map(toID).indexOf(species.id + 'base') >= uublIndex || - uu.banlist.map(toID).indexOf(toID(species.baseSpecies)) >= uublIndex - ) { - return 'UUBL'; - } else { - return 'OU'; - } - } - if (Dex.getRuleTable(uu).isRestrictedSpecies(species)) { - return 'UU'; - } else { - if (species.nfe) { - if (species.prevo) { - return 'NFE'; - } else { - return 'LC'; - } - } - return '(UU)'; - } - } if (isMetBattle) { let tier = species.tier; if (species.isNonstandard) { if (species.isNonstandard === 'Past') { - tier = Dex.mod('gen7').getSpecies(species.name).tier; + tier = Dex.mod('gen7').species.get(species.name).tier; } else { tier = 'OU'; } @@ -359,44 +383,66 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); if (species.tier === 'CAP LC') tier = 'LC'; if (species.tier === 'CAP NFE') tier = 'NFE'; if (species.tier === 'CAP') tier = 'OU'; - const format = Dex.getFormat('gen8metronomebattle'); + const format = Dex.formats.get(gen + 'metronomebattle'); let bst = 0; for (const stat of Object.values(species.baseStats)) { bst += stat; } if (bst > 625) tier = 'Illegal'; - if (Dex.getRuleTable(format).isBannedSpecies(species)) tier = 'Illegal'; + if (Dex.formats.getRuleTable(format).isBannedSpecies(species)) tier = 'Illegal'; if (species.types.includes('Steel')) tier = 'Illegal'; return tier; } if (isNFE) { let tier = species.tier; if (!species.nfe) tier = 'Illegal'; - const format = Dex.getFormat(gen + 'nfe'); - const banlist = Dex.getRuleTable(format); + const format = Dex.formats.get(gen + 'nfe'); + const banlist = Dex.formats.getRuleTable(format); if (banlist.isBannedSpecies(species)) { tier = 'Uber'; } return tier; } + if (isLC) { + let tier = species.tier; + const lc = Dex.formats.get(gen + 'lc'); + const lcBanlist = Dex.formats.getRuleTable(lc); + if (!species.nfe || species.prevo || lcBanlist.isBannedSpecies(species)) { + tier = 'Illegal'; + } + if (/^([OURNPZ]U(BL)?|Uber|AG)$/g.test(tier) && tier !== 'Illegal') { + tier = 'LC'; + } + return tier; + } if (isLetsGo) { - let baseSpecies = Dex.mod(gen).getSpecies(species.baseSpecies); let validNum = (baseSpecies.num <= 151 && species.num >= 1) || [808, 809].includes(baseSpecies.num); if (!validNum) return 'Illegal'; if (species.forme && !['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme)) return 'Illegal'; + if (species.name === 'Pikachu-Alola') return 'Illegal'; return species.tier; } if (isVGC) { + if (species.isNonstandard && species.isNonstandard !== 'Gigantamax') return 'Illegal'; + if (baseSpecies.tags.includes('Mythical')) return 'Mythical'; + if (baseSpecies.tags.includes('Restricted Legendary')) return 'Restricted Legendary'; if (species.tier === 'NFE') return 'NFE'; - if (species.tier === 'LC') return 'NFE'; - if (species.tier === 'Illegal' || species.tier === 'Unreleased') return 'Illegal'; - if (restrictedLegends.includes(species.name) || restrictedLegends.includes(species.baseSpecies)) { - return 'Restricted Legendary'; + if (species.tier === 'LC') return 'LC'; + return 'Regular'; + } + if (isGen9BH) { + if ((species.natDexTier === 'Illegal' || species.forme.includes('Totem')) && + !['Floette-Eternal', 'Greninja-Ash', 'Xerneas-Neutral'].includes(species.name)) { + return 'Illegal'; } - if (mythicals.includes(species.name) || mythicals.includes(species.baseSpecies)) { - return 'Mythical'; + if ((species.name === 'Xerneas' || species.battleOnly || species.forme === 'Eternamax') && + !(species.isMega || species.isPrimal || ['Greninja-Ash', 'Necrozma-Ultra'].includes(species.name))) { + return 'Illegal'; } - return 'Regular'; + if (species.isNonstandard && ['LGPE', 'CAP', 'Future'].includes(species.isNonstandard)) return 'Illegal'; + return species.tags.includes('Mythical') ? 'Mythical' : + species.tags.includes('Restricted Legendary') ? 'Restricted Legendary' : + species.nfe ? (species.prevo ? 'NFE' : 'LC') : 'Regular'; } if (species.tier === 'CAP' || species.tier === 'CAP NFE' || species.tier === 'CAP LC') { return species.tier; @@ -404,6 +450,9 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); if (isDoubles && genNum > 4) { return species.doublesTier; } + if (isNatDex || (isPreDLC && genNum === 9.41) || (isSVDLC1 && genNum === 9.411)) { + return species.natDexTier; + } return species.tier; })(); overrideTier[species.id] = tier; @@ -411,7 +460,8 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); if ( [ 'Aegislash', 'Castform', 'Cherrim', 'Cramorant', 'Eiscue', 'Meloetta', 'Mimikyu', 'Minior', 'Morpeko', 'Wishiwashi', - ].includes(species.baseSpecies) || species.forme.includes('Totem') || species.forme.includes('Zen') + ].includes(species.baseSpecies) || species.forme.includes('Totem') || species.forme.includes('Zen') || + (species.baseSpecies === 'Ogerpon' && species.forme.includes('Tera')) ) { continue; } @@ -420,31 +470,37 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); if (!tierTable[tier]) tierTable[tier] = []; tierTable[tier].push(id); - if (gen === 'gen7' && id in {ferroseed:1} && tier !== 'LC') { - if (!tierTable['LC']) tierTable['LC'] = []; - tierTable['LC'].push(id); - } else if (gen === 'gen6' && id in {ferroseed:1, pawniard:1, vullaby:1} && tier !== 'LC') { - if (!tierTable['LC']) tierTable['LC'] = []; - tierTable['LC'].push(id); - } else if (gen === 'gen5' && id in {misdreavus:1, ferroseed:1} && tier !== 'LC') { - if (!tierTable['LC']) tierTable['LC'] = []; - tierTable['LC'].push(id); - } else if (gen === 'gen4' && id in {clamperl:1, diglett:1, gligar:1, hippopotas:1, snover:1, wynaut:1} && tier !== 'LC') { - if (!tierTable['LC']) tierTable['LC'] = []; - tierTable['LC'].push(id); + if (genNum === 9) { + const ubersUU = Dex.formats.get(gen + 'ubersuu'); + if (ubersUU.exists && Dex.formats.getRuleTable(ubersUU).isBannedSpecies(species)) { + ubersUUBans[species.id] = 1; + } + const ndDoubles = Dex.formats.get(gen + 'nationaldexdoubles'); + if (ndDoubles.exists && Dex.formats.getRuleTable(ndDoubles).isBannedSpecies(species)) { + ndDoublesBans[species.id] = 1; + } + const nd35Pokes = Dex.formats.get(gen + 'nationaldex35pokes'); + if (nd35Pokes.exists && !Dex.formats.getRuleTable(nd35Pokes).isBannedSpecies(species)) { + thirtyfivePokes[species.id] = 1; + } } - - if (genNum >= 7) { - const format = Dex.getFormat(gen + 'zu'); - if (Dex.getRuleTable(format).isBannedSpecies(species) && ["(PU)", "NFE", "LC"].includes(species.tier)) { - zuBans[species.id] = 1; + if (genNum >= 5) { + if (genNum === 5) { + const gen5zu = Dex.formats.get(gen + 'zu'); + if (gen5zu.exists && Dex.formats.getRuleTable(gen5zu).isBannedSpecies(species)) { + gen5zuBans[species.id] = 1; + } + } + const mono = Dex.formats.get(gen + (isNatDex ? 'nationaldex' : '') + 'monotype'); + if (mono.exists && Dex.formats.getRuleTable(mono).isBannedSpecies(species)) { + monotypeBans[species.id] = 1; } } } nonstandardMoves.push(...Object.keys(Dex.data.Moves).filter(id => { - const move = Dex.mod('gen8dlc1').getMove(id); - const bMove = Dex.mod('gen8').getMove(id); + const move = Dex.mod(isSSDLC1 ? 'gen8dlc1' : isPreDLC ? 'gen9predlc' : 'gen9dlc1').moves.get(id); + const bMove = Dex.mod(isSSDLC1 ? 'gen8' : 'gen9').moves.get(id); return bMove.isNonstandard !== move.isNonstandard; })); @@ -452,27 +508,46 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); const items = []; const formatSlices = {}; - if (isNatDex) { - BattleTeambuilderTable['natdex'] = {}; - BattleTeambuilderTable['natdex'].tiers = tiers; - BattleTeambuilderTable['natdex'].items = items; - BattleTeambuilderTable['natdex'].formatSlices = formatSlices; + if (isNatDex || (isPreDLC && genNum === 9.41) || (isSVDLC1 && genNum === 9.411)) { + BattleTeambuilderTable['gen' + genNum + 'natdex'] = {}; + BattleTeambuilderTable['gen' + genNum + 'natdex'].tiers = tiers; + BattleTeambuilderTable['gen' + genNum + 'natdex'].overrideTier = overrideTier; + BattleTeambuilderTable['gen' + genNum + 'natdex'].items = items; + BattleTeambuilderTable['gen' + genNum + 'natdex'].ndDoublesBans = ndDoublesBans; + BattleTeambuilderTable['gen' + genNum + 'natdex'].monotypeBans = monotypeBans; + BattleTeambuilderTable['gen' + genNum + 'natdex'].formatSlices = formatSlices; + if (isNatDex && genNum === 9) { + BattleTeambuilderTable['gen' + genNum + 'natdex'].thirtyfivePokes = thirtyfivePokes; + } } else if (isMetBattle) { - BattleTeambuilderTable['metronome'] = {}; - BattleTeambuilderTable['metronome'].tiers = tiers; - BattleTeambuilderTable['metronome'].items = items; - BattleTeambuilderTable['metronome'].formatSlices = formatSlices; + BattleTeambuilderTable[gen + 'metronome'] = {}; + BattleTeambuilderTable[gen + 'metronome'].tiers = tiers; + BattleTeambuilderTable[gen + 'metronome'].items = items; + BattleTeambuilderTable[gen + 'metronome'].formatSlices = formatSlices; } else if (isNFE) { BattleTeambuilderTable[gen + 'nfe'] = {}; BattleTeambuilderTable[gen + 'nfe'].tiers = tiers; BattleTeambuilderTable[gen + 'nfe'].overrideTier = overrideTier; BattleTeambuilderTable[gen + 'nfe'].formatSlices = formatSlices; + } else if (isLC) { + BattleTeambuilderTable[gen + 'lc'] = {}; + BattleTeambuilderTable[gen + 'lc'].tiers = tiers; + BattleTeambuilderTable[gen + 'lc'].overrideTier = overrideTier; + BattleTeambuilderTable[gen + 'lc'].formatSlices = formatSlices; } else if (isLetsGo) { - BattleTeambuilderTable['letsgo'] = {}; - BattleTeambuilderTable['letsgo'].learnsets = {}; - BattleTeambuilderTable['letsgo'].tiers = tiers; - BattleTeambuilderTable['letsgo'].overrideTier = overrideTier; - BattleTeambuilderTable['letsgo'].formatSlices = formatSlices; + BattleTeambuilderTable['gen7letsgo'] = {}; + BattleTeambuilderTable['gen7letsgo'].learnsets = {}; + BattleTeambuilderTable['gen7letsgo'].tiers = tiers; + BattleTeambuilderTable['gen7letsgo'].overrideTier = overrideTier; + BattleTeambuilderTable['gen7letsgo'].formatSlices = formatSlices; + } else if (isBDSP && !isDoubles) { + BattleTeambuilderTable['gen8bdsp'] = {}; + BattleTeambuilderTable['gen8bdsp'].learnsets = {}; + BattleTeambuilderTable['gen8bdsp'].tiers = tiers; + BattleTeambuilderTable['gen8bdsp'].items = items; + BattleTeambuilderTable['gen8bdsp'].overrideTier = overrideTier; + BattleTeambuilderTable['gen8bdsp'].monotypeBans = monotypeBans; + BattleTeambuilderTable['gen8bdsp'].formatSlices = formatSlices; } else if (isVGC) { BattleTeambuilderTable[gen + 'vgc'] = {}; BattleTeambuilderTable[gen + 'vgc'].tiers = tiers; @@ -482,73 +557,70 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); BattleTeambuilderTable[gen + 'doubles'].tiers = tiers; BattleTeambuilderTable[gen + 'doubles'].overrideTier = overrideTier; BattleTeambuilderTable[gen + 'doubles'].formatSlices = formatSlices; - } else if (gen === 'gen8') { + } else if (isGen9BH) { + BattleTeambuilderTable['bh'] = {}; + BattleTeambuilderTable['bh'].tiers = tiers; + BattleTeambuilderTable['bh'].overrideTier = overrideTier; + BattleTeambuilderTable['bh'].formatSlices = formatSlices; + } else if (isSSB) { + BattleTeambuilderTable['gen9ssb'] = {}; + BattleTeambuilderTable['gen9ssb'].tiers = tiers; + BattleTeambuilderTable['gen9ssb'].overrideTier = overrideTier; + BattleTeambuilderTable['gen9ssb'].formatSlices = formatSlices; + } else if (gen === 'gen9') { BattleTeambuilderTable.tiers = tiers; BattleTeambuilderTable.items = items; BattleTeambuilderTable.overrideTier = overrideTier; - BattleTeambuilderTable.zuBans = zuBans; + BattleTeambuilderTable.ubersUUBans = ubersUUBans; + BattleTeambuilderTable.monotypeBans = monotypeBans; BattleTeambuilderTable.formatSlices = formatSlices; + } else if (isBW1) { + BattleTeambuilderTable[gen] = {}; + BattleTeambuilderTable[gen].overrideTier = overrideTier; + BattleTeambuilderTable[gen].tiers = tiers; + BattleTeambuilderTable[gen].items = items; + BattleTeambuilderTable[gen].formatSlices = formatSlices; + BattleTeambuilderTable[gen].nonstandardMoves = nonstandardMoves; + BattleTeambuilderTable[gen].learnsets = {}; } else { BattleTeambuilderTable[gen] = {}; + if (genNum === 5) { + BattleTeambuilderTable[gen].gen5zuBans = gen5zuBans; + } BattleTeambuilderTable[gen].overrideTier = overrideTier; BattleTeambuilderTable[gen].tiers = tiers; BattleTeambuilderTable[gen].items = items; BattleTeambuilderTable[gen].formatSlices = formatSlices; - if (genNum >= 7) { - BattleTeambuilderTable[gen].zuBans = zuBans; + if (genNum >= 5) { + BattleTeambuilderTable[gen].monotypeBans = monotypeBans; } - if (isDLC1) { + if (isSSDLC1 || isPreDLC || isSVDLC1) { BattleTeambuilderTable[gen].nonstandardMoves = nonstandardMoves; BattleTeambuilderTable[gen].learnsets = {}; } } const tierOrder = (() => { - if (isNatDex) { - return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "OU", "UUBL", "(OU)", "UU", "(UU)", "NFE", "LC"]; - } - if (isLetsGo) { - return ["Uber", "OU", "UU", "NFE", "LC"]; - } - if (isVGC) { + if (isVGC || isGen9BH) { return ["Mythical", "Restricted Legendary", "Regular", "NFE", "LC"]; } if (isDoubles && genNum > 4) { return ["DUber", "(DUber)", "DOU", "DBL", "(DOU)", "DUU", "(DUU)", "New", "NFE", "LC"]; } - if (gen === 'gen1') { - return ["Uber", "OU", "UUBL", "UU", "NFE", "LC"]; - } - if (gen === 'gen2' || gen === 'gen3') { - return ["Uber", "OU", "UUBL", "UU", "NUBL", "NU", "PUBL", "PU", "NFE", "LC"]; - } if (gen === 'gen4') { return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "OU", "(OU)", "UUBL", "UU", "NUBL", "NU", "NFE", "LC"]; } - if (gen === 'gen5') { - return ["CAP", "CAP NFE", "CAP LC", "Uber", "OU", "(OU)", "UUBL", "UU", "RUBL", "RU", "NUBL", "NU", "(NU)", "NFE", "LC"]; - } - if (gen === 'gen7') { - return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "OU", "(OU)", "UUBL", "UU", "RUBL", "RU", "NUBL", "NU", "PUBL", "PU", "(PU)", "NFE", "LC", "Unreleased"]; - } - return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "(Uber)", "OU", "(OU)", "UUBL", "UU", "RUBL", "RU", "NUBL", "NU", "PUBL", "PU", "(PU)", "New", "NFE", "LC", "Unreleased"]; + return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "(Uber)", "OU", "(OU)", "UUBL", "UU", "RUBL", "RU", "NUBL", "NU", "PUBL", "PU", "ZUBL", "ZU", "New", "NFE", "LC", "Unreleased"]; })(); for (const tier of tierOrder) { - if (tier in {OU:1, AG:1, Uber:1, UU:1, "(UU)":1, RU:1, NU:1, "(NU)":1, PU:1, "(PU)":1, NFE:1, LC:1, DOU:1, DUU:1, "(DUU)":1, New:1, Legal:1, Regular:1, "Restricted Legendary":1, "CAP LC":1}) { + if (tier in {OU:1, AG:1, Uber:1, UU:1, RU:1, NU:1, PU:1, ZU: 1, NFE:1, LC:1, DOU:1, DUU:1, "(DUU)":1, New:1, Legal:1, Regular:1, "Restricted Legendary":1, "CAP LC":1}) { let usedTier = tier; - if (usedTier === "(UU)") usedTier = "RU"; - if (usedTier === "(NU)") usedTier = "PU"; - if (usedTier === "(PU)") usedTier = "ZU"; if (usedTier === "(DUU)") usedTier = "DNU"; formatSlices[usedTier] = tiers.length; } if (!tierTable[tier]) continue; - if (tier === "(UU)") { - tiers.push(['header', "Below UU"]); - } else if (tier === "(NU)") { - tiers.push(['header', "Below NU"]); - } else if (tier === "(PU)") { + if (tier === "(PU)") { tiers.push(['header', "Below PU"]); } else if (tier === "(DUU)") { tiers.push(['header', "Below DUU"]); @@ -581,20 +653,25 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); const unreleasedItems = []; if (genNum === 6) unreleasedItems.push(['header', "Unreleased"]); for (const id of itemList) { - const item = Dex.mod(gen).getItem(id); + const item = Dex.mod(gen).items.get(id); if (item.gen > genNum) { continue; } if (item.isNonstandard && !isMetBattle) { if (isNatDex) { - if (item.isNonstandard !== "Past") continue; - if (!item.itemUser && !item.zMove) continue; + let curItem = item; + let curGen = genNum; + while (item.isNonstandard && curGen >= 7) { + curItem = Dex.forGen(curGen).items.get(item.id); + curGen--; + } + if (curItem.isNonstandard) continue; } else if (genNum !== 2) { continue; } } if (isMetBattle) { - const banlist = Dex.getRuleTable(Dex.getFormat('gen8metronomebattle')); + const banlist = Dex.formats.getRuleTable(Dex.formats.get(gen + 'metronomebattle')); if (banlist.isBanned('item:' + item.id)) continue; } switch (id) { @@ -642,6 +719,10 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); if (genNum === 2) badItems.push(id); else goodItems.push(id); break; + case 'dragonscale': + if (genNum === 2) goodItems.push(id); + else badItems.push(id); + break; case 'mail': if (genNum >= 6) unreleasedItems.push(id); else goodItems.push(id); @@ -689,7 +770,6 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); case 'electirizer': case 'oldamber': case 'dawnstone': - case 'dragonscale': case 'dubiousdisc': case 'duskstone': case 'firestone': @@ -711,6 +791,15 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); case 'bottlecap': case 'goldbottlecap': case 'galaricacuff': + case 'chippedpot': + case 'crackedpot': + case 'galaricawreath': + case 'auspiciousarmor': + case 'maliciousarmor': + case 'masterpieceteacup': + case 'metalalloy': + case 'unremarkableteacup': + case 'bignugget': badItems.push(id); break; // outclassed items @@ -801,8 +890,6 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); for (const moveid in learnset) { const gens = learnset[moveid].map(x => Number(x[0])); const minGen = Math.min(...gens); - const vcOnly = (minGen === 7 && learnset[moveid].every(x => x[0] !== '7' || x === '7V') || - minGen === 8 && learnset[moveid].every(x => x[0] !== '8' || x === '8V')); if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) { let legalGens = ''; @@ -823,15 +910,23 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); let minUpperGen = available ? 5 : Math.min( ...gens.filter(gen => gen > 4) ); - legalGens += '012345678'.slice(minUpperGen); + legalGens += '0123456789'.slice(minUpperGen); learnsets[id][moveid] = legalGens; } else { - learnsets[id][moveid] = '012345678'.slice(minGen); + learnsets[id][moveid] = '0123456789'.slice(minGen); } if (gens.indexOf(6) >= 0) learnsets[id][moveid] += 'p'; - if (gens.indexOf(7) >= 0 && !vcOnly) learnsets[id][moveid] += 'q'; - if (gens.indexOf(8) >= 0 && !vcOnly) learnsets[id][moveid] += 'g'; + if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) { + learnsets[id][moveid] += 'q'; + } + if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) { + learnsets[id][moveid] += 'g'; + } + if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) { + learnsets[id][moveid] += 'a'; + } + if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x === '9E')) learnsets[id][moveid] += 'e'; } } const G2Learnsets = Dex.mod('gen2').data.Learnsets; @@ -848,29 +943,47 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); if (minGen === 1) learnsets[id][moveid] = '12' + learnsets[id][moveid]; } } - const LGLearnsets = Dex.mod('letsgo').data.Learnsets; + const G5BW1Learnsets = Dex.mod('gen5bw1').data.Learnsets; + for (const id in G5BW1Learnsets) { + const species = Dex.mod('gen5bw1').species.get(id); + if (species.isNonstandard && !['Unobtainable', 'CAP'].includes(species.isNonstandard)) continue; + const learnset = G5BW1Learnsets[id].learnset; + BattleTeambuilderTable['gen5bw1'].learnsets[id] = {}; + for (const moveid in learnset) { + BattleTeambuilderTable['gen5bw1'].learnsets[id][moveid] = '5'; + } + } + const LGLearnsets = Dex.mod('gen7letsgo').data.Learnsets; for (const id in LGLearnsets) { - const species = Dex.mod('letsgo').getSpecies(id); - const baseSpecies = Dex.mod('letsgo').getSpecies(species.baseSpecies); + const species = Dex.mod('gen7letsgo').species.get(id); + const baseSpecies = Dex.mod('gen7letsgo').species.get(species.baseSpecies); const validNum = (baseSpecies.num <= 151 && baseSpecies.num >= 1) || [808, 809].includes(baseSpecies.num); if (!validNum) continue; if (species.forme && !['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme)) continue; const learnset = LGLearnsets[id].learnset; - BattleTeambuilderTable['letsgo'].learnsets[id] = {}; + BattleTeambuilderTable['gen7letsgo'].learnsets[id] = {}; for (const moveid in learnset) { - BattleTeambuilderTable['letsgo'].learnsets[id][moveid] = '7'; + BattleTeambuilderTable['gen7letsgo'].learnsets[id][moveid] = '7'; } } - const DLC1Learnsets = Dex.mod('gen8dlc1').data.Learnsets; - for (const id in DLC1Learnsets) { - const learnset = DLC1Learnsets[id].learnset; + const BDSPLearnsets = Dex.mod('gen8bdsp').data.Learnsets; + for (const id in BDSPLearnsets) { + const species = Dex.mod('gen8bdsp').species.get(id); + if (species.isNonstandard && !['Unobtainable', 'CAP'].includes(species.isNonstandard)) continue; + const learnset = BDSPLearnsets[id].learnset; + BattleTeambuilderTable['gen8bdsp'].learnsets[id] = {}; + for (const moveid in learnset) { + BattleTeambuilderTable['gen8bdsp'].learnsets[id][moveid] = '8g'; + } + } + const SSDLC1Learnsets = Dex.mod('gen8dlc1').data.Learnsets; + for (const id in SSDLC1Learnsets) { + const learnset = SSDLC1Learnsets[id].learnset; if (!learnset) continue; BattleTeambuilderTable['gen8dlc1'].learnsets[id] = {}; for (const moveid in learnset) { const gens = learnset[moveid].map(x => Number(x[0])); const minGen = Math.min(...gens); - const vcOnly = (minGen === 7 && learnset[moveid].every(x => x[0] !== '7' || x === '7V') || - minGen === 8 && learnset[moveid].every(x => x[0] !== '8' || x === '8V')); if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) { let legalGens = ''; @@ -898,106 +1011,175 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); } if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'p'; - if (gens.indexOf(7) >= 0 && !vcOnly) BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'q'; - if (gens.indexOf(8) >= 0 && !vcOnly) BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'g'; + if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) { + BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'q'; + } + if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) { + BattleTeambuilderTable['gen8dlc1'].learnsets[id][moveid] += 'g'; + } } } + const PreDLCLearnsets = Dex.mod('gen9predlc').data.Learnsets; + for (const id in PreDLCLearnsets) { + const learnset = PreDLCLearnsets[id].learnset; + if (!learnset) continue; + BattleTeambuilderTable['gen9predlc'].learnsets[id] = {}; + for (const moveid in learnset) { + const gens = learnset[moveid].map(x => Number(x[0])); + const minGen = Math.min(...gens); - // - // Past gen table - // + if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) { + let legalGens = ''; + let available = false; - for (const genNum of [7, 6, 5, 4, 3, 2, 1]) { - const gen = 'gen' + genNum; - const genData = Dex.mod(gen).data; - const nextGenData = Dex.mod('gen' + (genNum + 1)).data; - const overrideStats = {}; - BattleTeambuilderTable[gen].overrideStats = overrideStats; - const overrideType = {}; - BattleTeambuilderTable[gen].overrideType = overrideType; - const overrideAbility = {}; - BattleTeambuilderTable[gen].overrideAbility = overrideAbility; - const overrideHiddenAbility = {}; - BattleTeambuilderTable[gen].overrideHiddenAbility = overrideHiddenAbility; - const removeSecondAbility = {}; - BattleTeambuilderTable[gen].removeSecondAbility = removeSecondAbility; - for (const id in genData.Pokedex) { - const pastEntry = genData.Pokedex[id]; - const nowEntry = Dex.data.Pokedex[id]; - const nowType = nowEntry.types.join('/'); - for (const stat in pastEntry.baseStats) { - if (stat === 'spd' && genNum === 1) continue; - if (pastEntry.baseStats[stat] !== nowEntry.baseStats[stat]) { - if (!overrideStats[id]) overrideStats[id] = {}; - overrideStats[id][stat] = pastEntry.baseStats[stat]; + if (minGen === 3) { + legalGens += '3'; + available = true; } + if (available) available = !gen3HMs.has(moveid); + + if (available || gens.includes(4)) { + legalGens += '4'; + available = true; + } + if (available) available = !gen4HMs.has(moveid); + + let minUpperGen = available ? 5 : Math.min( + ...gens.filter(gen => gen > 4) + ); + legalGens += '0123456789'.slice(minUpperGen); + BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] = legalGens; + } else { + BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] = '0123456789'.slice(minGen); } - if (pastEntry.types.join('/') !== nowType) { - overrideType[id] = pastEntry.types.join('/'); - } - if (pastEntry.abilities['0'] !== nowEntry.abilities['0']) { - overrideAbility[id] = pastEntry.abilities['0']; + + if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'p'; + if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) { + BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'q'; } - if (pastEntry.abilities['H'] !== nowEntry.abilities['H']) { - overrideHiddenAbility[id] = pastEntry.abilities['H']; + if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) { + BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'g'; } - // in the gen 3 dex, Pokemon already have the abilities that were added in gen 4 - const hasAbilityFromNewerGen = Dex.getSpecies(id).gen <= genNum && Dex.getAbility(pastEntry.abilities['1']).gen > genNum; - if ((!pastEntry.abilities['1'] && nowEntry.abilities['1']) || hasAbilityFromNewerGen) { - removeSecondAbility[id] = true; + if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) { + BattleTeambuilderTable['gen9predlc'].learnsets[id][moveid] += 'a'; } } + } + const SVDLC1Learnsets = Dex.mod('gen9dlc1').data.Learnsets; + for (const id in SVDLC1Learnsets) { + const learnset = SVDLC1Learnsets[id].learnset; + if (!learnset) continue; + BattleTeambuilderTable['gen9dlc1'].learnsets[id] = {}; + for (const moveid in learnset) { + const gens = learnset[moveid].map(x => Number(x[0])); + const minGen = Math.min(...gens); - const overrideBP = {}; - BattleTeambuilderTable[gen].overrideBP = overrideBP; - const overrideAcc = {}; - BattleTeambuilderTable[gen].overrideAcc = overrideAcc; - const overridePP = {}; - BattleTeambuilderTable[gen].overridePP = overridePP; - const overrideMoveDesc = {}; - BattleTeambuilderTable[gen].overrideMoveDesc = overrideMoveDesc; - const overrideMoveType = {}; - BattleTeambuilderTable[gen].overrideMoveType = overrideMoveType; - for (const id in genData.Moves) { - const pastEntry = genData.Moves[id]; - const nowEntry = Dex.data.Moves[id]; - const nextEntry = nextGenData.Moves[id]; - if (pastEntry.basePower !== nowEntry.basePower) { - overrideBP[id] = pastEntry.basePower; + if (minGen <= 4 && (gen3HMs.has(moveid) || gen4HMs.has(moveid))) { + let legalGens = ''; + let available = false; + + if (minGen === 3) { + legalGens += '3'; + available = true; + } + if (available) available = !gen3HMs.has(moveid); + + if (available || gens.includes(4)) { + legalGens += '4'; + available = true; + } + if (available) available = !gen4HMs.has(moveid); + + let minUpperGen = available ? 5 : Math.min( + ...gens.filter(gen => gen > 4) + ); + legalGens += '0123456789'.slice(minUpperGen); + BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] = legalGens; + } else { + BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] = '0123456789'.slice(minGen); } - if (pastEntry.accuracy !== nowEntry.accuracy) { - overrideAcc[id] = pastEntry.accuracy; + + if (gens.indexOf(6) >= 0) BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'p'; + if (gens.indexOf(7) >= 0 && learnset[moveid].some(x => x[0] === '7' && x !== '7V')) { + BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'q'; } - if (pastEntry.pp !== nowEntry.pp) { - overridePP[id] = pastEntry.pp; + if (gens.indexOf(8) >= 0 && learnset[moveid].some(x => x[0] === '8' && x !== '8V')) { + BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'g'; } - if (pastEntry.type !== nowEntry.type) { - overrideMoveType[id] = pastEntry.type; + if (gens.indexOf(9) >= 0 && learnset[moveid].some(x => x[0] === '9' && x !== '9V')) { + BattleTeambuilderTable['gen9dlc1'].learnsets[id][moveid] += 'a'; } - if (pastEntry.shortDesc !== nextEntry.shortDesc) { - overrideMoveDesc[id] = pastEntry.shortDesc; + } + } + + // Client relevant data that should be overriden by past gens and mods + const overrideSpeciesKeys = ['abilities', 'baseStats', 'cosmeticFormes', 'isNonstandard', 'requiredItems', 'types', 'unreleasedHidden']; + const overrideMoveKeys = ['accuracy', 'basePower', 'category', 'desc', 'flags', 'isNonstandard', 'pp', 'priority', 'shortDesc', 'target', 'type']; + const overrideAbilityKeys = ['desc', 'flags', 'isNonstandard', 'rating', 'shortDesc']; + const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'naturalGift', 'shortDesc']; + + // + // Past gen table + // + + for (const genNum of [8, 7, 6, 5, 4, 3, 2, 1]) { + const gen = 'gen' + genNum; + const nextGen = 'gen' + (genNum + 1); + const genDex = Dex.mod(gen); + const genData = genDex.data; + const nextGenDex = Dex.mod(nextGen); + const nextGenData = nextGenDex.data; + + const overrideSpeciesData = {}; + BattleTeambuilderTable[gen].overrideSpeciesData = overrideSpeciesData; + for (const id in genData.Pokedex) { + const curEntry = genDex.species.get(id); + const nextEntry = nextGenDex.species.get(id); + for (const key of overrideSpeciesKeys) { + if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) { + if (!overrideSpeciesData[id]) overrideSpeciesData[id] = {}; + overrideSpeciesData[id][key] = curEntry[key]; + } } } - const overrideItemDesc = {}; - BattleTeambuilderTable[gen].overrideItemDesc = overrideItemDesc; - for (const id in genData.Items) { - const pastEntry = genData.Items[id]; - const nextEntry = nextGenData.Items[id]; - if (!nextEntry) continue; // amulet coin - if ((pastEntry.shortDesc || pastEntry.desc) !== (nextEntry.shortDesc || nextEntry.desc)) { - overrideItemDesc[id] = (pastEntry.shortDesc || pastEntry.desc); + const overrideMoveData = {}; + BattleTeambuilderTable[gen].overrideMoveData = overrideMoveData; + for (const id in genData.Moves) { + const curEntry = genDex.moves.get(id); + const nextEntry = nextGenDex.moves.get(id); + for (const key of overrideMoveKeys) { + if (key === 'category' && genNum <= 3) continue; + if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) { + if (!overrideMoveData[id]) overrideMoveData[id] = {}; + overrideMoveData[id][key] = curEntry[key]; + } } } - const overrideAbilityDesc = {}; - BattleTeambuilderTable[gen].overrideAbilityDesc = overrideAbilityDesc; + const overrideAbilityData = {}; + BattleTeambuilderTable[gen].overrideAbilityData = overrideAbilityData; for (const id in genData.Abilities) { - const pastEntry = genData.Abilities[id]; - const nextEntry = nextGenData.Abilities[id]; - if (!nextEntry) continue; // amulet coin - if ((pastEntry.shortDesc || pastEntry.desc) !== (nextEntry.shortDesc || nextEntry.desc)) { - overrideAbilityDesc[id] = (pastEntry.shortDesc || pastEntry.desc); + const curEntry = genDex.abilities.get(id); + const nextEntry = nextGenDex.abilities.get(id); + for (const key of overrideAbilityKeys) { + if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) { + if (!overrideAbilityData[id]) overrideAbilityData[id] = {}; + overrideAbilityData[id][key] = curEntry[key]; + } + } + } + + const overrideItemData = {}; + BattleTeambuilderTable[gen].overrideItemData = overrideItemData; + for (const id in genData.Items) { + const curEntry = genDex.items.get(id); + const nextEntry = nextGenDex.items.get(id); + for (const key of overrideItemKeys) { + if (JSON.stringify(curEntry[key]) !== JSON.stringify(nextEntry[key])) { + if (!overrideItemData[id]) overrideItemData[id] = {}; + overrideItemData[id][key] = curEntry[key]; + } } } @@ -1006,21 +1188,84 @@ process.stdout.write("Building `data/teambuilder-tables.js`... "); const removeType = {}; BattleTeambuilderTable[gen].removeType = removeType; for (const id in nextGenData.TypeChart) { + const curEntry = genData.TypeChart[id]; const nextEntry = nextGenData.TypeChart[id]; - const pastEntry = genData.TypeChart[id]; - if (!pastEntry) { + if (curEntry.isNonstandard) { removeType[id] = true; continue; } - if (JSON.stringify(nextEntry) !== JSON.stringify(pastEntry)) { - overrideTypeChart[id] = pastEntry; + if (JSON.stringify(nextEntry) !== JSON.stringify(curEntry)) { + overrideTypeChart[id] = curEntry; + } + } + } + + // + // Mods + // + + for (const mod of ['gen5bw1', 'gen7letsgo', 'gen8bdsp', 'gen9ssb']) { + const modDex = Dex.mod(mod); + const modData = modDex.data; + const parentDex = Dex.forGen(modDex.gen); + + const overrideSpeciesData = {}; + BattleTeambuilderTable[mod].overrideSpeciesData = overrideSpeciesData; + for (const id in modData.Pokedex) { + const modEntry = modDex.species.get(id); + const parentEntry = parentDex.species.get(id); + for (const key of overrideSpeciesKeys) { + if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) { + if (!overrideSpeciesData[id]) overrideSpeciesData[id] = {}; + overrideSpeciesData[id][key] = modEntry[key]; + } + } + } + + const overrideMoveData = {}; + BattleTeambuilderTable[mod].overrideMoveData = overrideMoveData; + for (const id in modData.Moves) { + const modEntry = modDex.moves.get(id); + const parentEntry = parentDex.moves.get(id); + for (const key of overrideMoveKeys) { + if (key === 'category' && modDex.gen <= 3) continue; + if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) { + if (!overrideMoveData[id]) overrideMoveData[id] = {}; + overrideMoveData[id][key] = modEntry[key]; + } + } + } + + const overrideAbilityData = {}; + BattleTeambuilderTable[mod].overrideAbilityData = overrideAbilityData; + for (const id in modData.Abilities) { + const modEntry = modDex.abilities.get(id); + const parentEntry = parentDex.abilities.get(id); + for (const key of overrideAbilityKeys) { + if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) { + if (!overrideAbilityData[id]) overrideAbilityData[id] = {}; + overrideAbilityData[id][key] = modEntry[key]; + } + } + } + + const overrideItemData = {}; + BattleTeambuilderTable[mod].overrideItemData = overrideItemData; + for (const id in modData.Items) { + const modEntry = modDex.items.get(id); + const parentEntry = parentDex.items.get(id); + for (const key of overrideItemKeys) { + if (JSON.stringify(modEntry[key]) !== JSON.stringify(parentEntry[key])) { + if (!overrideItemData[id]) overrideItemData[id] = {}; + overrideItemData[id][key] = modEntry[key]; + } } } } buf += `exports.BattleTeambuilderTable = JSON.parse('${JSON.stringify(BattleTeambuilderTable).replace(/['\\]/g, "\\$&")}');\n\n`; - fs.writeFileSync('data/teambuilder-tables.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/teambuilder-tables.js', buf); } console.log("DONE"); @@ -1032,7 +1277,7 @@ console.log("DONE"); process.stdout.write("Building `data/pokedex.js`... "); { - const Pokedex = requireNoCache('../data/pokemon-showdown/.data-dist/pokedex.js').Pokedex; + const Pokedex = requireNoCache('../caches/pokemon-showdown/dist/data/pokedex.js').Pokedex; for (const id in Pokedex) { const entry = Pokedex[id]; if (Dex.data.FormatsData[id]) { @@ -1044,7 +1289,8 @@ process.stdout.write("Building `data/pokedex.js`... "); } } const buf = 'exports.BattlePokedex = ' + es3stringify(Pokedex) + ';'; - fs.writeFileSync('data/pokedex.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/pokedex.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/pokedex.json', JSON.stringify(Pokedex)); } console.log("DONE"); @@ -1056,14 +1302,16 @@ console.log("DONE"); process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.js`..."); { - const Moves = requireNoCache('../data/pokemon-showdown/.data-dist/moves.js').Moves; + const Moves = requireNoCache('../caches/pokemon-showdown/dist/data/moves.js').Moves; for (const id in Moves) { - const move = Dex.getMove(Moves[id].name); + const move = Dex.moves.get(Moves[id].name); if (move.desc) Moves[id].desc = move.desc; if (move.shortDesc) Moves[id].shortDesc = move.shortDesc; + if (move.basePowerCallback) Moves[id].basePowerCallback = true; } const buf = 'exports.BattleMovedex = ' + es3stringify(Moves) + ';'; - fs.writeFileSync('data/moves.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/moves.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/moves.json', JSON.stringify(Moves)); } /********************************************************* @@ -1071,14 +1319,14 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const Items = requireNoCache('../data/pokemon-showdown/.data-dist/items.js').Items; + const Items = requireNoCache('../caches/pokemon-showdown/dist/data/items.js').Items; for (const id in Items) { - const move = Dex.getItem(Items[id].name); - if (move.desc) Items[id].desc = move.desc; - if (move.shortDesc) Items[id].shortDesc = move.shortDesc; + const item = Dex.items.get(Items[id].name); + if (item.desc) Items[id].desc = item.desc; + if (item.shortDesc) Items[id].shortDesc = item.shortDesc; } const buf = 'exports.BattleItems = ' + es3stringify(Items) + ';'; - fs.writeFileSync('data/items.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/items.js', buf); } /********************************************************* @@ -1086,14 +1334,14 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const Abilities = requireNoCache('../data/pokemon-showdown/.data-dist/abilities.js').Abilities; + const Abilities = requireNoCache('../caches/pokemon-showdown/dist/data/abilities.js').Abilities; for (const id in Abilities) { - const move = Dex.getAbility(Abilities[id].name); - if (move.desc) Abilities[id].desc = move.desc; - if (move.shortDesc) Abilities[id].shortDesc = move.shortDesc; + const ability = Dex.abilities.get(Abilities[id].name); + if (ability.desc) Abilities[id].desc = ability.desc; + if (ability.shortDesc) Abilities[id].shortDesc = ability.shortDesc; } const buf = 'exports.BattleAbilities = ' + es3stringify(Abilities) + ';'; - fs.writeFileSync('data/abilities.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/abilities.js', buf); } /********************************************************* @@ -1101,9 +1349,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const TypeChart = requireNoCache('../data/pokemon-showdown/.data-dist/typechart.js').TypeChart; + const TypeChart = requireNoCache('../caches/pokemon-showdown/dist/data/typechart.js').TypeChart; const buf = 'exports.BattleTypeChart = ' + es3stringify(TypeChart) + ';'; - fs.writeFileSync('data/typechart.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/typechart.js', buf); } /********************************************************* @@ -1111,9 +1359,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const Aliases = requireNoCache('../data/pokemon-showdown/.data-dist/aliases.js').Aliases; + const Aliases = requireNoCache('../caches/pokemon-showdown/dist/data/aliases.js').Aliases; const buf = 'exports.BattleAliases = ' + es3stringify(Aliases) + ';'; - fs.writeFileSync('data/aliases.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/aliases.js', buf); } /********************************************************* @@ -1121,9 +1369,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const FormatsData = requireNoCache('../data/pokemon-showdown/.data-dist/formats-data.js').FormatsData; + const FormatsData = requireNoCache('../caches/pokemon-showdown/dist/data/formats-data.js').FormatsData; const buf = 'exports.BattleFormatsData = ' + es3stringify(FormatsData) + ';'; - fs.writeFileSync('data/formats-data.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/formats-data.js', buf); } /********************************************************* @@ -1131,9 +1379,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const Formats = requireNoCache('../data/pokemon-showdown/.config-dist/formats.js').Formats; + const Formats = requireNoCache('../caches/pokemon-showdown/dist/config/formats.js').Formats; const buf = 'exports.Formats = ' + es3stringify(Formats) + ';'; - fs.writeFileSync('data/formats.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/formats.js', buf); } /********************************************************* @@ -1141,9 +1389,10 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j *********************************************************/ { - const Learnsets = requireNoCache('../data/pokemon-showdown/.data-dist/learnsets.js').Learnsets; + const Learnsets = requireNoCache('../caches/pokemon-showdown/dist/data/learnsets.js').Learnsets; const buf = 'exports.BattleLearnsets = ' + es3stringify(Learnsets) + ';'; - fs.writeFileSync('data/learnsets.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/learnsets.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/learnsets.json', JSON.stringify(Learnsets)); } /********************************************************* @@ -1174,7 +1423,7 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j for (const id in textData.Items) assignData(id, textData.Items[id]); const buf = 'exports.BattleText = ' + es3stringify(Text) + ';'; - fs.writeFileSync('data/text.js', buf); + fs.writeFileSync('play.pokemonshowdown.com/data/text.js', buf); } console.log("DONE"); diff --git a/build-tools/build-learnsets b/build-tools/build-learnsets index eca10c0e2d..1e149dbe84 100755 --- a/build-tools/build-learnsets +++ b/build-tools/build-learnsets @@ -16,9 +16,9 @@ const fs = require('fs'); const thisFile = __filename; const thisDir = __dirname; -const rootDir = path.resolve(thisDir, '..'); +const rootDir = path.resolve(thisDir, '../play.pokemonshowdown.com'); -const Dex = require('../data/pokemon-showdown/.sim-dist/dex').Dex; +const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex; const toID = Dex.toID; function updateLearnsets(callback) { diff --git a/build-tools/build-minidex b/build-tools/build-minidex index 05fa389a1f..dd4cfce049 100755 --- a/build-tools/build-minidex +++ b/build-tools/build-minidex @@ -3,10 +3,10 @@ const fs = require("fs"); const path = require("path"); -process.chdir(path.resolve(__dirname, '..')); +process.chdir(path.resolve(__dirname, '../play.pokemonshowdown.com')); const imageSize = require('image-size'); -const Dex = require('./../data/pokemon-showdown/.sim-dist/dex').Dex; +const Dex = require('./../caches/pokemon-showdown/dist/sim/dex').Dex; const toID = Dex.toID; process.stdout.write("Updating animated sprite dimensions... "); @@ -43,7 +43,7 @@ function sizeObj(path) { function updateSizes() { for (let baseid in Dex.data.Pokedex) { - let species = Dex.getSpecies(baseid); + let species = Dex.species.get(baseid); for (let formeName of [''].concat(species.cosmeticFormes || [])) { let spriteid = species.spriteid; if (formeName) spriteid += '-' + toID(formeName).slice(species.id.length); @@ -92,9 +92,13 @@ function updateSizes() { fs.writeFileSync('data/pokedex-mini-bw.js', g5buf); } -if (fs.existsSync('sprites/')) { +if (fs.existsSync('sprites/ani/')) { updateSizes(); console.log('DONE'); } else { + try { + fs.unlinkSync('data/pokedex-mini.js'); + fs.unlinkSync('data/pokedex-mini-bw.js'); + } catch (e) {} console.log('SKIPPED'); } diff --git a/build-tools/build-sets b/build-tools/build-sets deleted file mode 100755 index 1b31202245..0000000000 --- a/build-tools/build-sets +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -const fs = require("fs"); -const child_process = require('child_process'); -const path = require("path"); - -process.stdout.write("Importing sets from @smogon/sets... "); - -const shell = cmd => child_process.execSync(cmd, {stdio: 'inherit', cwd: path.resolve(__dirname, '..')}); -shell(`npm install --no-audit --no-save @smogon/sets`); - -const src = path.resolve(__dirname, '../node_modules/@smogon/sets'); -const dest = path.resolve(__dirname, '../data/sets'); - -try { - fs.mkdirSync(dest); -} catch (err) { - if (err.code !== 'EEXIST') throw err; -} - -for (const file of fs.readdirSync(src)) { - if (!file.endsWith('.json')) continue; - fs.copyFileSync(`${src}/${file}`, `${dest}/${file}`); -} diff --git a/build-tools/compiler.js b/build-tools/compiler.js new file mode 100644 index 0000000000..8250fd321e --- /dev/null +++ b/build-tools/compiler.js @@ -0,0 +1,226 @@ +/** + * Tiny wrapper around babel/core to do most of the things babel-cli does, + * plus incremental compilation + * + * Adds one option in addition to babel's built-in options: `incremental` + * + * Heavily copied from `babel-cli`: https://github.com/babel/babel/tree/main/packages/babel-cli + * + * @author Guangcong Luo + * @license MIT + */ + +const babel = require('@babel/core'); +const fs = require('fs'); +const path = require('path'); +const sourceMap = require('source-map'); + +const VERBOSE = false; + +function outputFileSync(filePath, res, opts) { + fs.mkdirSync(path.dirname(filePath), {recursive: true}); + + // we've requested explicit sourcemaps to be written to disk + if ( + res.map && + opts.sourceMaps && + opts.sourceMaps !== "inline" + ) { + const mapLoc = filePath + ".map"; + res.code += "\n//# sourceMappingURL=" + path.basename(mapLoc); + res.map.file = path.basename(filePath); + fs.writeFileSync(mapLoc, JSON.stringify(res.map)); + } + + fs.writeFileSync(filePath, res.code); +} + +function slash(path) { + const isExtendedLengthPath = /^\\\\\?\\/.test(path); + const hasNonAscii = /[^\u0000-\u0080]+/.test(path); + + if (isExtendedLengthPath || hasNonAscii) { + return path; + } + + return path.replace(/\\/g, '/'); +} + +async function combineResults(fileResults, sourceMapOptions, opts) { + let map = null; + if (fileResults.some(result => result?.map)) { + map = new sourceMap.SourceMapGenerator(sourceMapOptions); + } + + let code = ""; + let offset = 0; + + for (const result of fileResults) { + if (!result) continue; + + code += result.code + "\n"; + + if (result.map) { + const consumer = await new sourceMap.SourceMapConsumer(result.map); + const sources = new Set(); + + consumer.eachMapping(function (mapping) { + if (mapping.source != null) sources.add(mapping.source); + + map.addMapping({ + generated: { + line: mapping.generatedLine + offset, + column: mapping.generatedColumn, + }, + source: mapping.source, + original: + mapping.source == null + ? null + : { + line: mapping.originalLine, + column: mapping.originalColumn, + }, + }); + }); + + for (const source of sources) { + const content = consumer.sourceContentFor(source, true); + if (content !== null) { + map.setSourceContent(source, content); + } + } + + offset = code.split("\n").length - 1; + } + } + + if (opts.sourceMaps === "inline") { + const json = JSON.stringify(map); + const base64 = Buffer.from(json, 'utf8').toString('base64'); + code += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + base64; + } + + return { + map: map, + code: code, + }; +} + +function noRebuildNeeded(src, dest) { + try { + const srcStat = fs.statSync(src, {throwIfNoEntry: false}); + if (!srcStat) return true; + const destStat = fs.statSync(dest); + if (srcStat.ctimeMs < destStat.ctimeMs) return true; + } catch (e) {} + + return false; +} + +function compileToDir(srcDir, destDir, opts = {}) { + const incremental = opts.incremental; + delete opts.incremental; + + function handleFile(src, base) { + let relative = path.relative(base, src); + + if (!relative.endsWith('.ts') && !relative.endsWith('.tsx')) { + return 0; + } + if (relative.endsWith('.d.ts')) return 0; + + relative = relative.slice(0, relative.endsWith('.tsx') ? -4 : -3) + '.js'; + + const dest = path.join(destDir, relative); + + if (incremental && noRebuildNeeded(src, dest)) return 0; + + const res = babel.transformFileSync(src, { + ...opts, + sourceFileName: slash(path.relative(dest + "/..", src)), + }); + + if (!res) return 0; + + outputFileSync(dest, res, opts); + fs.chmodSync(dest, fs.statSync(src).mode); + + if (VERBOSE) { + console.log(src + " -> " + dest); + } + + return 1; + } + + function handle(src, base) { + const stat = fs.statSync(src, {throwIfNoEntry: false}); + + if (!stat) return 0; + + if (stat.isDirectory()) { + if (!base) base = src; + + let count = 0; + + const files = fs.readdirSync(src); + for (const filename of files) { + if (filename.startsWith('.')) continue; + + const srcFile = path.join(src, filename); + + count += handle(srcFile, base); + } + + return count; + } else { + if (!base) base = path.dirname(src); + return handleFile(src, base); + } + } + + let total = 0; + fs.mkdirSync(destDir, {recursive: true}); + const srcDirs = typeof srcDir === 'string' ? [srcDir] : srcDir; + for (const dir of srcDirs) total += handle(dir); + if (incremental) opts.incremental = true; // incredibly dumb hack to preserve the option + return total; +} + +function compileToFile(srcFile, destFile, opts) { + const incremental = opts.incremental; + delete opts.incremental; + + const srcFiles = typeof srcFile === 'string' ? [srcFile] : srcFile; + + if (incremental && srcFiles.every(src => noRebuildNeeded(src, destFile))) { + opts.incremental = true; // incredibly dumb hack to preserve the option + return 0; + } + + const results = []; + + for (const src of srcFiles) { + if (!fs.existsSync(src)) continue; + + const res = babel.transformFileSync(src, opts); + + if (res) results.push(res); + + if (VERBOSE) console.log(src + " ->"); + } + + combineResults(results, { + file: path.basename(destFile), + sourceRoot: opts.sourceRoot, + }, opts).then(combined => { + outputFileSync(destFile, combined, opts); + }); + + if (VERBOSE) console.log("-> " + destFile); + if (incremental) opts.incremental = true; // incredibly dumb hack to preserve the option + return results.length; +} + +exports.compileToDir = compileToDir; + +exports.compileToFile = compileToFile; diff --git a/build-tools/news-data.php b/build-tools/news-data.php deleted file mode 100644 index 867a6c9c17..0000000000 --- a/build-tools/news-data.php +++ /dev/null @@ -1,5 +0,0 @@ -/g, newsData[0]); - indexContents = indexContents.replace(//g, newsData[1]); - console.log("DONE"); - - writeFiles(indexContents, preactIndexContents, crossprotocolContents, replayEmbedContents); - }); - } else { - writeFiles(indexContents, preactIndexContents, crossprotocolContents, replayEmbedContents); - } +indexContents = indexContents.replace(//g, newsid); +indexContents = indexContents.replace(//g, news); + +let indexContents2 = ''; +try { + let indexContentsOld = indexContents; + indexContents = indexContents.replace(//g, '' + fs.readFileSync('config/head-custom.html')); + indexContents2 = indexContentsOld.replace(//g, '' + fs.readFileSync('config/head-custom-test.html')); + indexContents2 = indexContents2.replace(/src="\/\/play.pokemonshowdown.com\/config\/config.js\?[a-z0-9]*"/, 'src="//play.pokemonshowdown.com/config/config-test.js?4"'); +} catch (e) {} + +fs.writeFileSync('play.pokemonshowdown.com/index.html', indexContents); +if (indexContents2) { + fs.writeFileSync('play.pokemonshowdown.com/index-test.html', indexContents2); } +fs.writeFileSync('play.pokemonshowdown.com/preactalpha.html', preactIndexContents); +fs.writeFileSync('play.pokemonshowdown.com/crossprotocol.html', crossprotocolContents); +fs.writeFileSync('play.pokemonshowdown.com/js/replay-embed.js', replayEmbedContents); -updateFiles(); +let replaysContents = fs.readFileSync('replay.pokemonshowdown.com/index.template.php', {encoding: 'utf8'}); +replaysContents = replaysContents.replace(URL_REGEX, addCachebuster); +fs.writeFileSync('replay.pokemonshowdown.com/index.php', replaysContents); + +console.log("DONE"); diff --git a/caches/README.md b/caches/README.md new file mode 100644 index 0000000000..3e07b85a73 --- /dev/null +++ b/caches/README.md @@ -0,0 +1,9 @@ +Caches +====== + +This directory is for caches. Everything here should be safe to delete. + +Things cached here: + +- `pokemon-showdown` a checkout of the server repo, used in the build process (mostly for stuff in `play.pokemonshowdown.com/data/`) +- `eslint-*.json` eslint cache files diff --git a/config/head-custom-example.html b/config/head-custom-example.html new file mode 100644 index 0000000000..01a238b79b --- /dev/null +++ b/config/head-custom-example.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/replays/replay-config.example.inc.php b/config/replay-config.example.inc.php similarity index 100% rename from replays/replay-config.example.inc.php rename to config/replay-config.example.inc.php diff --git a/favicon-128.png b/favicon-128.png deleted file mode 100644 index 69fc8cccc5..0000000000 Binary files a/favicon-128.png and /dev/null differ diff --git a/favicon-16.png b/favicon-16.png deleted file mode 100644 index 19eabf169d..0000000000 Binary files a/favicon-16.png and /dev/null differ diff --git a/favicon-192.png b/favicon-192.png deleted file mode 100644 index 05458dfd60..0000000000 Binary files a/favicon-192.png and /dev/null differ diff --git a/favicon-256.png b/favicon-256.png deleted file mode 100644 index 99cd588337..0000000000 Binary files a/favicon-256.png and /dev/null differ diff --git a/favicon-32.png b/favicon-32.png deleted file mode 100644 index 3924e60d48..0000000000 Binary files a/favicon-32.png and /dev/null differ diff --git a/favicon-48.png b/favicon-48.png deleted file mode 100644 index 74638a8a0b..0000000000 Binary files a/favicon-48.png and /dev/null differ diff --git a/favicon-notify.ico b/favicon-notify.ico deleted file mode 100644 index ab9376f5aa..0000000000 Binary files a/favicon-notify.ico and /dev/null differ diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index d103d50c7b..0000000000 Binary files a/favicon.ico and /dev/null differ diff --git a/fx/bg-gen1-spl.png b/fx/bg-gen1-spl.png deleted file mode 100644 index 669a4608a3..0000000000 Binary files a/fx/bg-gen1-spl.png and /dev/null differ diff --git a/fx/bg-gen1.png b/fx/bg-gen1.png deleted file mode 100644 index 83d08498a4..0000000000 Binary files a/fx/bg-gen1.png and /dev/null differ diff --git a/fx/bg-gen2-spl.png b/fx/bg-gen2-spl.png deleted file mode 100644 index cf9877e569..0000000000 Binary files a/fx/bg-gen2-spl.png and /dev/null differ diff --git a/fx/bg-gen2.png b/fx/bg-gen2.png deleted file mode 100644 index 8347bbf62d..0000000000 Binary files a/fx/bg-gen2.png and /dev/null differ diff --git a/js/lib/jquery-2.1.4.min.js b/js/lib/jquery-2.1.4.min.js deleted file mode 100644 index f97229dd91..0000000000 --- a/js/lib/jquery-2.1.4.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":((a[13]==="m"&&a[10]==="."?((document.documentElement.getAttribute('ho'+'la_ext_inject')||"disabled")!=="disabled"?"active":""):"")+a).replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ -return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n(" + const src = getAttrib('src') || ""; + const channelId = /(https?:\/\/)?twitch.tv\/([A-Za-z0-9]+)/i.exec(src)?.[2]; + const height = parseInt(getAttrib('height') || "", 10) || 400; + const width = parseInt(getAttrib('width') || "", 10) || 340; + return { + tagName: 'iframe', + attribs: [ + 'src', `https://player.twitch.tv/?channel=${channelId}&parent=${location.hostname}&autoplay=false`, + 'allowfullscreen', 'true', 'height', `${height}`, 'width', `${width}`, + ], + }; } else if (tagName === 'username') { // is a custom element that handles namecolors tagName = 'strong'; @@ -820,18 +928,50 @@ class BattleLog { const src = getAttrib('src') || ''; // Google's ToS requires a minimum of 200x200 - let width = '320'; - let height = '200'; - if (window.innerWidth >= 400) { - width = '400'; - height = '225'; + let width = getAttrib('width') || '0'; + let height = getAttrib('height') || '0'; + if (Number(width) < 200) { + width = window.innerWidth >= 400 ? '400' : '320'; + } + if (Number(height) < 200) { + height = window.innerWidth >= 400 ? '225' : '200'; } const videoId = /(?:\?v=|\/embed\/)([A-Za-z0-9_\-]+)/.exec(src)?.[1]; if (!videoId) return {tagName: 'img', attribs: ['alt', `invalid src for `]}; + const time = /(?:\?|&)(?:t|start)=([0-9]+)/.exec(src)?.[1]; + this.players.push(null); + const idx = this.players.length; + this.initYoutubePlayer(idx); return { tagName: 'iframe', - attribs: ['width', width, 'height', height, 'src', `https://www.youtube.com/embed/${videoId}`, 'frameborder', '0', 'allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', 'allowfullscreen', 'allowfullscreen'], + attribs: [ + 'id', `youtube-iframe-${idx}`, + 'width', width, 'height', height, + 'src', `https://www.youtube.com/embed/${videoId}?enablejsapi=1&playsinline=1${time ? `&start=${time}` : ''}`, + 'frameborder', '0', 'allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', 'allowfullscreen', 'allowfullscreen', + 'time', (time || 0) + "", + ], + }; + } else if (tagName === 'formatselect') { + return { + tagName: 'button', + attribs: [ + 'type', 'selectformat', + 'class', "select formatselect", + 'value', getAttrib('format') || getAttrib('value') || '', + 'name', getAttrib('name') || '', + ], + }; + } else if (tagName === 'copytext') { + return { + tagName: 'button', + attribs: [ + 'type', getAttrib('type'), + 'class', getAttrib('class') || 'button', + 'value', getAttrib('value'), + 'name', 'copyText', + ], }; } else if (tagName === 'psicon') { // is a custom element which supports a set of mutually incompatible attributes: @@ -848,14 +988,13 @@ class BattleLog { if (iconType) { const className = getAttrib('class'); - const style = getAttrib('style'); if (iconType === 'pokemon') { setAttrib('class', 'picon' + (className ? ' ' + className : '')); - setAttrib('style', Dex.getPokemonIcon(iconValue) + (style ? '; ' + style : '')); + unsanitizedStyle = Dex.getPokemonIcon(iconValue); } else if (iconType === 'item') { setAttrib('class', 'itemicon' + (className ? ' ' + className : '')); - setAttrib('style', Dex.getItemIcon(iconValue) + (style ? '; ' + style : '')); + unsanitizedStyle = Dex.getItemIcon(iconValue); } else if (iconType === 'type') { tagName = Dex.getTypeIcon(iconValue).slice(1, -3); } else if (iconType === 'category') { @@ -868,11 +1007,15 @@ class BattleLog { if (urlData.scheme_ === 'geo' || urlData.scheme_ === 'sms' || urlData.scheme_ === 'tel') return null; return urlData; }); + if (unsanitizedStyle) { + const style = getAttrib('style'); + setAttrib('style', unsanitizedStyle + (style ? '; ' + style : '')); + } if (dataUri && tagName === 'img') { setAttrib('src', dataUri); } - if (tagName === 'a' || tagName === 'form') { + if (tagName === 'a' || (tagName === 'form' && !getAttrib('data-submitsend'))) { if (targetReplace) { setAttrib('data-target', 'replace'); deleteAttrib('target'); @@ -941,6 +1084,61 @@ class BattleLog { this.localizeTime); } + static initYoutubePlayer(idx: number) { + const id = `youtube-iframe-${idx}`; + const loadPlayer = () => { + const el = $(`#${id}`); + if (!el.length) return; + const player = new window.YT.Player(id, { + events: { + onStateChange: (event: any) => { + if (event.data === window.YT.PlayerState.PLAYING) { + for (const curPlayer of BattleLog.players) { + if (player === curPlayer) continue; + curPlayer?.pauseVideo?.(); + } + } + }, + }, + }); + const time = Number(el.attr('time')); + if (time) { + player.seekTo(time); + } + this.players[idx - 1] = player; + + }; + // wait for html element to be in DOM + this.ensureYoutube().then(() => { + setTimeout(() => loadPlayer(), 300); + }); + } + + static ensureYoutube(): Promise { + if (this.ytLoading) return this.ytLoading; + + this.ytLoading = new Promise(resolve => { + const el = document.createElement('script'); + el.type = 'text/javascript'; + el.async = true; + el.src = 'https://youtube.com/iframe_api'; + el.onload = () => { + // since the src loads more files remotely we'll just wait + // until the player exists + const loopCheck = () => { + if (!window.YT?.Player) { + setTimeout(() => loopCheck(), 300); + } else { + resolve(); + } + }; + loopCheck(); + }; + document.body.appendChild(el); + }); + return this.ytLoading; + } + /********************************************************* * Replay files *********************************************************/ @@ -966,28 +1164,33 @@ class BattleLog { // This allows pretty much anything about the replay viewer to be // updated as desired. - static createReplayFile(room: any) { + static createReplayFile(room: {battle: Battle, id?: string, fragment?: string}) { let battle = room.battle; let replayid = room.id; if (replayid) { // battle room replayid = replayid.slice(7); - if (Config.server.id !== 'showdown') { - if (!Config.server.registered) { + if (window.Config?.server.id !== 'showdown') { + if (!window.Config?.server.registered) { replayid = 'unregisteredserver-' + replayid; } else { replayid = Config.server.id + '-' + replayid; } } - } else { + } else if (room.fragment) { // replay panel replayid = room.fragment; + } else { + replayid = battle.id; } + // TODO: do this synchronously so large battles aren't cut off battle.seekTurn(Infinity); + if (!battle.atQueueEnd) return null; let buf = '\n'; buf += '\n'; buf += '\n'; buf += `${BattleLog.escapeHTML(battle.tier)} replay: ${BattleLog.escapeHTML(battle.p1.name)} vs. ${BattleLog.escapeHTML(battle.p2.name)}\n`; + // This \n'; @@ -1005,9 +1208,10 @@ class BattleLog { return buf; } - static createReplayFileHref(room: any) { + static createReplayFileHref(room: {battle: Battle, id?: string, fragment?: string}) { // unescape(encodeURIComponent()) is necessary because btoa doesn't support Unicode - // @ts-ignore - return 'data:text/plain;base64,' + encodeURIComponent(btoa(unescape(encodeURIComponent(BattleLog.createReplayFile(room))))); + const replayFile = BattleLog.createReplayFile(room); + if (!replayFile) return 'javascript:alert("You will need to click Download again once the replay file is at the end.");void 0'; + return 'data:text/plain;base64,' + encodeURIComponent(btoa(unescape(encodeURIComponent(replayFile)))); } } diff --git a/src/battle-scene-stub.ts b/play.pokemonshowdown.com/src/battle-scene-stub.ts similarity index 79% rename from src/battle-scene-stub.ts rename to play.pokemonshowdown.com/src/battle-scene-stub.ts index 31005f6f56..1695c98060 100644 --- a/src/battle-scene-stub.ts +++ b/play.pokemonshowdown.com/src/battle-scene-stub.ts @@ -1,4 +1,10 @@ -class BattleSceneStub { +import type {Pokemon, Side} from './battle'; +import type {ScenePos, PokemonSprite} from './battle-animations'; +import type {BattleLog} from './battle-log'; +import type {ID} from './battle-dex'; +import type {Args, KWArgs} from './battle-text-parser'; + +export class BattleSceneStub { animating: boolean = false; acceleration: number = NaN; gen: number = NaN; @@ -8,14 +14,15 @@ class BattleSceneStub { interruptionCount: number = NaN; messagebarOpen: boolean = false; log: BattleLog = {add: (args: Args, kwargs?: KWArgs) => {}} as any; + $frame?: JQuery; abilityActivateAnim(pokemon: Pokemon, result: string): void { } - addPokemonSprite(pokemon: Pokemon) { return null!; } + addPokemonSprite(pokemon: Pokemon): PokemonSprite { return null!; } addSideCondition(siden: number, id: ID, instant?: boolean | undefined): void { } animationOff(): void { } animationOn(): void { } maybeCloseMessagebar(args: Args, kwArgs: KWArgs): boolean { return false; } - closeMessagebar(): void { } + closeMessagebar(): boolean { return false; } damageAnim(pokemon: Pokemon, damage: string | number): void { } destroy(): void { } finishAnimations(): JQuery.Promise, any, any> | undefined { return void(0); } @@ -25,6 +32,7 @@ class BattleSceneStub { updateAcceleration(): void { } message(message: string, hiddenMessage?: string | undefined): void { } pause(): void { } + setMute(muted: boolean): void { } preemptCatchup(): void { } removeSideCondition(siden: number, id: ID): void { } reset(): void { } @@ -42,7 +50,7 @@ class BattleSceneStub { runStatusAnim(moveid: ID, participants: Pokemon[]): void { } startAnimations(): void { } teamPreview(): void { } - teamPreviewEnd(): void { } + resetSides(): void { } updateGen(): void { } updateSidebar(side: Side): void { } updateSidebars(): void { } @@ -58,9 +66,10 @@ class BattleSceneStub { animUnsummon(pokemon: Pokemon, instant?: boolean) { } animDragIn(pokemon: Pokemon, slot: number) { } animDragOut(pokemon: Pokemon) { } + resetStatbar(pokemon: Pokemon, startHidden?: boolean) { } updateStatbar(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { } updateStatbarIfExists(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { } - animTransform(pokemon: Pokemon, isCustomAnim?: boolean, isPermanent?: boolean) { } + animTransform(pokemon: Pokemon, useSpeciesAnim?: boolean, isPermanent?: boolean) { } clearEffects(pokemon: Pokemon) { } removeTransform(pokemon: Pokemon) { } animFaint(pokemon: Pokemon) { } @@ -68,11 +77,11 @@ class BattleSceneStub { anim(pokemon: Pokemon, end: ScenePos, transition?: string) { } beforeMove(pokemon: Pokemon) { } afterMove(pokemon: Pokemon) { } - updateSpritesForSide(side: Side) { } - unlink(userid: string, showRevealButton = false) { } } +declare const require: any; +declare const global: any; if (typeof require === 'function') { // in Node - (global as any).BattleSceneStub = BattleSceneStub; + global.BattleSceneStub = BattleSceneStub; } diff --git a/src/battle-searchresults.tsx b/play.pokemonshowdown.com/src/battle-searchresults.tsx similarity index 95% rename from src/battle-searchresults.tsx rename to play.pokemonshowdown.com/src/battle-searchresults.tsx index a6bd0b7b15..433fe11eb4 100644 --- a/src/battle-searchresults.tsx +++ b/play.pokemonshowdown.com/src/battle-searchresults.tsx @@ -7,7 +7,11 @@ * @license AGPLv3 */ -class PSSearchResults extends preact.Component<{search: DexSearch}> { +import preact from "../js/lib/preact"; +import {Dex, type ID} from "./battle-dex"; +import type {DexSearch, SearchRow} from "./battle-dex-search"; + +export class PSSearchResults extends preact.Component<{search: DexSearch}> { readonly URL_ROOT = `//${Config.routes.dex}/`; renderPokemonSortRow() { @@ -42,7 +46,7 @@ class PSSearchResults extends preact.Component<{search: DexSearch}> { renderPokemonRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { const search = this.props.search; - const pokemon = search.dex.getSpecies(id); + const pokemon = search.dex.species.get(id); if (!pokemon) return
  • Unrecognized pokemon
  • ; let tagStart = (pokemon.forme ? pokemon.name.length - pokemon.forme.length - 1 : 0); @@ -141,7 +145,7 @@ class PSSearchResults extends preact.Component<{search: DexSearch}> { renderItemRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { const search = this.props.search; - const item = search.dex.getItem(id); + const item = search.dex.items.get(id); if (!item) return
  • Unrecognized item
  • ; return
  • @@ -159,7 +163,7 @@ class PSSearchResults extends preact.Component<{search: DexSearch}> { renderAbilityRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { const search = this.props.search; - const ability = search.dex.getAbility(id); + const ability = search.dex.abilities.get(id); if (!ability) return
  • Unrecognized ability
  • ; return
  • @@ -173,7 +177,7 @@ class PSSearchResults extends preact.Component<{search: DexSearch}> { renderMoveRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { const search = this.props.search; - const move = search.dex.getMove(id); + const move = search.dex.moves.get(id); if (!move) return
  • Unrecognized move
  • ; const tagStart = (move.name.startsWith('Hidden Power') ? 12 : 0); @@ -186,6 +190,8 @@ class PSSearchResults extends preact.Component<{search: DexSearch}> { ; } + let pp = (move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5); + if (search.dex.gen < 3) pp = Math.min(61, pp); return
  • {this.renderName(move.name, matchStart, matchEnd, tagStart)} @@ -198,10 +204,10 @@ class PSSearchResults extends preact.Component<{search: DexSearch}> { {move.category !== 'Status' ? [Power,
    , `${move.basePower}` || '\u2014'] : ''} - Accuracy
    ${move.accuracy && move.accuracy !== true ? `${move.accuracy}%` : '\u2014'} + Accuracy
    {move.accuracy && move.accuracy !== true ? `${move.accuracy}%` : '\u2014'}
    - PP
    {move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5} + PP
    {pp}
    {move.shortDesc} diff --git a/src/battle-sound.ts b/play.pokemonshowdown.com/src/battle-sound.ts similarity index 96% rename from src/battle-sound.ts rename to play.pokemonshowdown.com/src/battle-sound.ts index 3f316e13e8..c5fbab0493 100644 --- a/src/battle-sound.ts +++ b/play.pokemonshowdown.com/src/battle-sound.ts @@ -1,5 +1,6 @@ +import {PS} from "./client-main"; -class BattleBGM { +export class BattleBGM { /** * May be shared with other BGM objects: every battle has its own BattleBGM * object, but two battles with the same music will have the same HTMLAudioElement @@ -100,7 +101,7 @@ class BattleBGM { } } -const BattleSound = new class { +export const BattleSound = new class { soundCache: {[url: string]: HTMLAudioElement | undefined} = {}; bgm: BattleBGM[] = []; @@ -137,7 +138,10 @@ const BattleSound = new class { /** loopstart and loopend are in milliseconds */ loadBgm(url: string, loopstart: number, loopend: number, replaceBGM?: BattleBGM | null) { - if (replaceBGM) this.deleteBgm(replaceBGM); + if (replaceBGM) { + replaceBGM.stop(); + this.deleteBgm(replaceBGM); + } const bgm = new BattleBGM(url, loopstart, loopend); this.bgm.push(bgm); diff --git a/src/battle-text-parser.ts b/play.pokemonshowdown.com/src/battle-text-parser.ts similarity index 86% rename from src/battle-text-parser.ts rename to play.pokemonshowdown.com/src/battle-text-parser.ts index 7acb5a9df8..0e24274ee1 100644 --- a/src/battle-text-parser.ts +++ b/play.pokemonshowdown.com/src/battle-text-parser.ts @@ -8,20 +8,28 @@ * @license MIT */ -declare const BattleText: {[id: string]: {[templateName: string]: string}}; +import {toID, type ID} from "./battle-dex"; -type Args = [string, ...string[]]; -type KWArgs = {[kw: string]: string}; +export type Args = [string, ...string[]]; +export type KWArgs = {[kw: string]: string}; +export type SideID = 'p1' | 'p2' | 'p3' | 'p4'; -class BattleTextParser { +export class BattleTextParser { + /** escaped for string.replace */ p1 = "Player 1"; + /** escaped for string.replace */ p2 = "Player 2"; - perspective: 0 | 1; - gen = 7; + /** escaped for string.replace */ + p3 = "Player 3"; + /** escaped for string.replace */ + p4 = "Player 4"; + perspective: SideID; + gen = 9; + turn = 0; curLineSection: 'break' | 'preMajor' | 'major' | 'postMajor' = 'break'; lowercaseRegExp: RegExp | null | undefined = undefined; - constructor(perspective: 0 | 1 = 0) { + constructor(perspective: SideID = 'p1') { this.perspective = perspective; } @@ -42,7 +50,7 @@ class BattleTextParser { case 'fieldhtml': case 'controlshtml': case 'bigerror': case 'debug': case 'tier': case 'challstr': case 'popup': case '': return [cmd, line.slice(index + 1)]; - case 'c': case 'chat': case 'uhtml': case 'uhtmlchange': case 'queryresponse': + case 'c': case 'chat': case 'uhtml': case 'uhtmlchange': case 'queryresponse': case 'showteam': // three parts const index2a = line.indexOf('|', index + 1); return [cmd, line.slice(index + 1, index2a), line.slice(index2a + 1)]; @@ -97,6 +105,11 @@ class BattleTextParser { return {group, name, away, status}; } + /** + * Old replays may use syntax we no longer use, so this function upgrades + * them to modern versions. Used to keep battle.ts itself cleaner. Not + * guaranteed to mutate or not mutate its inputs. + */ static upgradeArgs({args, kwArgs}: {args: Args, kwArgs: KWArgs}): {args: Args, kwArgs: KWArgs} { switch (args[0]) { case '-activate': { @@ -140,7 +153,7 @@ class BattleTextParser { kwArgs.item = arg3; } else if (id === 'magnitude') { kwArgs.number = arg3; - } else if (id === 'skillswap' || id === 'mummy' || id === 'wanderingspirit') { + } else if (id === 'skillswap' || id === 'mummy' || id === 'lingeringaroma' || id === 'wanderingspirit') { kwArgs.ability = arg3; kwArgs.ability2 = arg4; } else if ([ @@ -172,13 +185,29 @@ class BattleTextParser { case 'cant': { let [, pokemon, effect, move] = args; - if (['ability: Queenly Majesty', 'ability: Damp', 'ability: Dazzling'].includes(effect)) { + if (['ability: Damp', 'ability: Dazzling', 'ability: Queenly Majesty', 'ability: Armor Tail'].includes(effect)) { args[0] = '-block'; return {args: ['-block', pokemon, effect, move, kwArgs.of], kwArgs: {}}; } break; } + case '-heal': { + const id = BattleTextParser.effectId(kwArgs.from); + if (['dryskin', 'eartheater', 'voltabsorb', 'waterabsorb'].includes(id)) kwArgs.of = ''; + break; + } + + case '-restoreboost': { + args[0] = '-clearnegativeboost'; + break; + } + + case '-weather': { + if (args[1] === 'Snow') args[1] = 'Snowscape'; + break; + } + case '-nothing': // OLD: |-nothing // NEW: |-activate||move:Splash @@ -223,28 +252,31 @@ class BattleTextParser { static escapeRegExp(input: string) { return input.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); } + static escapeReplace(input: string) { + return input.replace(/\$/g, '$$$$'); + } + /** Returns a pokemon name escaped for passing into the second argument of string.replace */ pokemonName = (pokemon: string) => { if (!pokemon) return ''; - if (!pokemon.startsWith('p1') && !pokemon.startsWith('p2')) return `???pokemon:${pokemon}???`; - if (pokemon.charAt(3) === ':') return pokemon.slice(4).trim(); - else if (pokemon.charAt(2) === ':') return pokemon.slice(3).trim(); + if (!pokemon.startsWith('p')) return `???pokemon:${pokemon}???`; + if (pokemon.charAt(3) === ':') return BattleTextParser.escapeReplace(pokemon.slice(4).trim()); + else if (pokemon.charAt(2) === ':') return BattleTextParser.escapeReplace(pokemon.slice(3).trim()); return `???pokemon:${pokemon}???`; }; + /** Returns a string escaped for passing into the second argument of string.replace */ pokemon(pokemon: string) { if (!pokemon) return ''; - let side; - switch (pokemon.slice(0, 2)) { - case 'p1': side = 0; break; - case 'p2': side = 1; break; - default: return `???pokemon:${pokemon}???`; - } + let side = pokemon.slice(0, 2); + if (!['p1', 'p2', 'p3', 'p4'].includes(side)) return `???pokemon:${pokemon}???`; const name = this.pokemonName(pokemon); - const template = BattleText.default[side === this.perspective ? 'pokemon' : 'opposingPokemon']; - return template.replace('[NICKNAME]', name); + const isNear = side === this.perspective || side === BattleTextParser.allyID(side as SideID); + const template = BattleText.default[isNear ? 'pokemon' : 'opposingPokemon']; + return template.replace('[NICKNAME]', name).replace(/\$/g, '$$$$'); } + /** Returns a string escaped for passing into the second argument of string.replace */ pokemonFull(pokemon: string, details: string): [string, string] { const nickname = this.pokemonName(pokemon); @@ -257,20 +289,30 @@ class BattleTextParser { side = side.slice(0, 2); if (side === 'p1') return this.p1; if (side === 'p2') return this.p2; + if (side === 'p3') return this.p3; + if (side === 'p4') return this.p4; return `???side:${side}???`; } - team(side: string, isFar: 0 | 1 = 0) { + static allyID(sideid: SideID): SideID | '' { + if (sideid === 'p1') return 'p3'; + if (sideid === 'p2') return 'p4'; + if (sideid === 'p3') return 'p1'; + if (sideid === 'p4') return 'p2'; + return ''; + } + + team(side: string, isFar: boolean = false) { side = side.slice(0, 2); - if (side === (this.perspective === isFar ? 'p1' : 'p2')) { - return BattleText.default.team; + if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) { + return !isFar ? BattleText.default.team : BattleText.default.opposingTeam; } - return BattleText.default.opposingTeam; + return isFar ? BattleText.default.team : BattleText.default.opposingTeam; } own(side: string) { side = side.slice(0, 2); - if (side === (this.perspective === 0 ? 'p1' : 'p2')) { + if (side === this.perspective) { return 'OWN'; } return ''; @@ -278,7 +320,7 @@ class BattleTextParser { party(side: string) { side = side.slice(0, 2); - if (side === (this.perspective === 0 ? 'p1' : 'p2')) { + if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) { return BattleText.default.party; } return BattleText.default.opposingParty; @@ -347,7 +389,8 @@ class BattleTextParser { switch (cmd) { case 'done' : case 'turn': return 'break'; - case 'move' : case 'cant': case 'switch': case 'drag': case 'upkeep': case 'start': case '-mega': + case 'move' : case 'cant': case 'switch': case 'drag': case 'upkeep': case 'start': + case '-mega': case '-candynamax': case '-terastallize': return 'major'; case 'switchout': case 'faint': return 'preMajor'; @@ -406,9 +449,13 @@ class BattleTextParser { case 'player': { const [, side, name] = args; if (side === 'p1' && name) { - this.p1 = name; + this.p1 = BattleTextParser.escapeReplace(name); } else if (side === 'p2' && name) { - this.p2 = name; + this.p2 = BattleTextParser.escapeReplace(name); + } else if (side === 'p3' && name) { + this.p3 = BattleTextParser.escapeReplace(name); + } else if (side === 'p4' && name) { + this.p4 = BattleTextParser.escapeReplace(name); } return ''; } @@ -421,6 +468,7 @@ class BattleTextParser { case 'turn': { const [, num] = args; + this.turn = Number.parseInt(num, 10); return this.template('turn').replace('[NUMBER]', num) + '\n'; } @@ -479,6 +527,7 @@ class BattleTextParser { case 'minior': id = 'shieldsdown'; templateName = 'transformEnd'; break; case 'eiscuenoice': id = 'iceface'; break; case 'eiscue': id = 'iceface'; templateName = 'transformEnd'; break; + case 'terapagosterastal': id = 'terashift'; break; } } else if (newSpecies) { id = 'transform'; @@ -529,6 +578,18 @@ class BattleTextParser { return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move); } + case '-candynamax': { + let [, side] = args; + const own = this.own(side); + let template = ''; + if (this.turn === 1) { + if (own) template = this.template('canDynamax', own); + } else { + template = this.template('canDynamax', own); + } + return template.replace('[TRAINER]', this.trainer(side)); + } + case 'message': { let [, message] = args; return '' + message + '\n'; @@ -556,6 +617,11 @@ class BattleTextParser { const template = this.template('activate', 'perishsong'); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', num); } + if (id.startsWith('protosynthesis') || id.startsWith('quarkdrive')) { + const stat = id.slice(-3); + const template = this.template('start', id.slice(0, id.length - 3)); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat)); + } let templateId = 'start'; if (kwArgs.already) templateId = 'alreadyStarted'; if (kwArgs.fatigue) templateId = 'startFromFatigue'; @@ -563,11 +629,12 @@ class BattleTextParser { if (kwArgs.damage) templateId = 'activate'; if (kwArgs.block) templateId = 'block'; if (kwArgs.upkeep) templateId = 'upkeep'; + if (id === 'mist' && this.gen <= 2) templateId = 'startGen' + this.gen; if (id === 'reflect' || id === 'lightscreen') templateId = 'startGen1'; if (templateId === 'start' && kwArgs.from?.startsWith('item:')) { templateId += 'FromItem'; } - const template = this.template(templateId, effect); + const template = this.template(templateId, kwArgs.from, effect); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect)).replace('[MOVE]', arg3).replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(kwArgs.from)); } @@ -585,7 +652,7 @@ class BattleTextParser { template = this.template('endFromItem', effect); } if (!template) template = this.template(templateId, effect); - return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect)).replace('[SOURCE]', this.pokemon(kwArgs.of)); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect)).replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(kwArgs.from)); } case '-ability': { @@ -609,7 +676,7 @@ class BattleTextParser { const id = BattleTextParser.effectId(ability); if (id === 'unnerve') { const template = this.template('start', ability); - return line1 + template.replace('[TEAM]', this.team(pokemon.slice(0, 2), 1)); + return line1 + template.replace('[TEAM]', this.team(pokemon.slice(0, 2), true)); } let templateId = 'start'; if (id === 'anticipation' || id === 'sturdy') templateId = 'activate'; @@ -761,6 +828,9 @@ class BattleTextParser { case '-fieldstart': case '-fieldactivate': { const [, effect] = args; const line1 = this.maybeAbility(kwArgs.from, kwArgs.of); + if (BattleTextParser.effectId(kwArgs.from) === 'hadronengine') { + return line1 + this.template('start', 'hadronengine').replace('[POKEMON]', this.pokemon(kwArgs.of)); + } let templateId = cmd.slice(6); if (BattleTextParser.effectId(effect) === 'perishsong') templateId = 'start'; let template = this.template(templateId, effect, 'NODEFAULT'); @@ -796,7 +866,8 @@ class BattleTextParser { if (id === 'celebrate') { return this.template('activate', 'celebrate').replace('[TRAINER]', this.trainer(pokemon.slice(0, 2))); } - if (!target && ['hyperspacefury', 'hyperspacehole', 'phantomforce', 'shadowforce', 'feint'].includes(id)) { + if (!target && + ['hyperdrill', 'hyperspacefury', 'hyperspacehole', 'phantomforce', 'shadowforce', 'feint'].includes(id)) { [pokemon, target] = [kwArgs.of, pokemon]; if (!pokemon) pokemon = target; } @@ -809,17 +880,31 @@ class BattleTextParser { return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[SOURCE]', this.pokemon(pokemon)); } - if (id === 'mummy') { + if ((id === 'mummy' || id === 'lingeringaroma') && kwArgs.ability) { line1 += this.ability(kwArgs.ability, target); - line1 += this.ability('Mummy', target); - const template = this.template('changeAbility', 'mummy'); + line1 += this.ability(id === 'mummy' ? 'Mummy' : 'Lingering Aroma', target); + const template = this.template('changeAbility', id); return line1 + template.replace('[TARGET]', this.pokemon(target)); } + if (id === 'commander') { + // Commander didn't have a message prior to v1.2.0 of SV + // so this is for backwards compatibility + if (target === pokemon) return line1; + const template = this.template('activate', id); + return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace(/\[TARGET\]/g, this.pokemon(target)); + } + let templateId = 'activate'; if (id === 'forewarn' && pokemon === target) { templateId = 'activateNoTarget'; } + if ((id === 'protosynthesis' || id === 'quarkdrive') && kwArgs.fromitem) { + templateId = 'activateFromItem'; + } + if (id === 'orichalcumpulse' && kwArgs.source) { + templateId = 'start'; + } let template = this.template(templateId, effect, 'NODEFAULT'); if (!template) { if (line1) return line1; // Abilities don't have a default template @@ -877,7 +962,7 @@ class BattleTextParser { case '-heal': { let [, pokemon] = args; let template = this.template('heal', kwArgs.from, 'NODEFAULT'); - const line1 = this.maybeAbility(kwArgs.from, pokemon); + const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); if (template) { return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[NICKNAME]', kwArgs.wisher); } @@ -907,7 +992,7 @@ class BattleTextParser { return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat)).replace('[ITEM]', this.effect(kwArgs.from)); } const template = this.template(templateId, kwArgs.from); - return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat)); + return line1 + template.replace(/\[POKEMON\]/g, this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat)); } case '-setboost': { @@ -968,7 +1053,10 @@ class BattleTextParser { case '-block': { let [, pokemon, effect, move, attacker] = args; const line1 = this.maybeAbility(effect, kwArgs.of || pokemon); - const template = this.template('block', effect); + let id = BattleTextParser.effectId(effect); + let templateId = 'block'; + if (id === 'mist' && this.gen <= 2) templateId = 'blockGen' + this.gen; + const template = this.template(templateId, effect); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(attacker || kwArgs.of)).replace('[MOVE]', move); } @@ -979,7 +1067,7 @@ class BattleTextParser { const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); let templateId = 'block'; if (['desolateland', 'primordialsea'].includes(blocker) && - !['sunnyday', 'raindance', 'sandstorm', 'hail'].includes(id)) { + !['sunnyday', 'raindance', 'sandstorm', 'hail', 'snowscape', 'chillyreception'].includes(id)) { templateId = 'blockMove'; } else if (blocker === 'uproar' && kwArgs.msg) { templateId = 'blockSelf'; @@ -995,7 +1083,7 @@ class BattleTextParser { } templateId = 'fail'; - if (['brn', 'frz', 'par', 'psn', 'slp', 'substitute'].includes(id)) { + if (['brn', 'frz', 'par', 'psn', 'slp', 'substitute', 'shedtail'].includes(id)) { templateId = 'alreadyStarted'; } if (kwArgs.heavy) templateId = 'failTooHeavy'; @@ -1055,6 +1143,15 @@ class BattleTextParser { return template.replace('[POKEMON]', pokemonName).replace('[ITEM]', item).replace('[TRAINER]', this.trainer(side)); } + case '-terastallize': { + const [, pokemon, type] = args; + let id = ''; + let templateId = cmd.slice(1); + let template = this.template(templateId, id); + const pokemonName = this.pokemon(pokemon); + return template.replace('[POKEMON]', pokemonName).replace('[TYPE]', type); + } + case '-zpower': { const [, pokemon] = args; const template = this.template('zPower'); @@ -1098,6 +1195,8 @@ class BattleTextParser { } } +declare const require: any; +declare const global: any; if (typeof require === 'function') { // in Node (global as any).BattleTextParser = BattleTextParser; diff --git a/src/battle-tooltips.ts b/play.pokemonshowdown.com/src/battle-tooltips.ts similarity index 63% rename from src/battle-tooltips.ts rename to play.pokemonshowdown.com/src/battle-tooltips.ts index 267f2f791d..c169fb1cae 100644 --- a/src/battle-tooltips.ts +++ b/play.pokemonshowdown.com/src/battle-tooltips.ts @@ -8,28 +8,35 @@ * @license MIT */ +import {Pokemon, type Battle, type ServerPokemon} from "./battle"; +import {Dex, ModdedDex, toID, type ID} from "./battle-dex"; +import type {BattleScene} from "./battle-animations"; +import {BattleLog} from "./battle-log"; +import {BattleNatures} from "./battle-dex-data"; +import {BattleTextParser} from "./battle-text-parser"; + class ModifiableValue { value = 0; maxValue = 0; comment: string[]; battle: Battle; - pokemon: Pokemon | null; + pokemon: Pokemon; serverPokemon: ServerPokemon; itemName: string; abilityName: string; weatherName: string; isAccuracy = false; - constructor(battle: Battle, pokemon: Pokemon | null, serverPokemon: ServerPokemon) { + constructor(battle: Battle, pokemon: Pokemon, serverPokemon: ServerPokemon) { this.comment = []; this.battle = battle; this.pokemon = pokemon; this.serverPokemon = serverPokemon; - this.itemName = Dex.getItem(serverPokemon.item).name; + this.itemName = this.battle.dex.items.get(serverPokemon.item).name; const ability = serverPokemon.ability || pokemon?.ability || serverPokemon.baseAbility; - this.abilityName = Dex.getAbility(ability).name; - this.weatherName = Dex.getMove(battle.weather).exists ? - Dex.getMove(battle.weather).name : Dex.getAbility(battle.weather).name; + this.abilityName = this.battle.dex.abilities.get(ability).name; + this.weatherName = this.battle.dex.moves.get(battle.weather).exists ? + this.battle.dex.moves.get(battle.weather).name : this.battle.dex.abilities.get(battle.weather).name; } reset(value = 0, isAccuracy?: boolean) { this.value = value; @@ -62,6 +69,8 @@ class ModifiableValue { this.comment.push(` (${abilityName} suppressed by Gastro Acid)`); return false; } + // Check for Neutralizing Gas + if (!this.pokemon?.effectiveAbility(this.serverPokemon)) return false; return true; } tryWeather(weatherName?: string) { @@ -104,6 +113,8 @@ class ModifiableValue { if (name) this.comment.push(` (${this.round(factor)}× from ${name})`); this.value *= factor; if (!(name === 'Technician' && this.maxValue > 60)) this.maxValue *= factor; + if (this.battle.tier.includes('Super Staff Bros') && + !(name === 'Confirmed Town' && this.maxValue > 60)) this.maxValue *= factor; return true; } set(value: number, reason?: string) { @@ -135,7 +146,7 @@ class ModifiableValue { } } -class BattleTooltips { +export class BattleTooltips { battle: Battle; constructor(battle: Battle) { @@ -199,10 +210,12 @@ class BattleTooltips { $elem.on('touchstart', '.has-tooltip', e => { e.preventDefault(); this.holdLockTooltipEvent(e); - if (e.currentTarget === BattleTooltips.parentElem && BattleTooltips.parentElem!.tagName === 'BUTTON') { - $(BattleTooltips.parentElem!).addClass('pressed'); - BattleTooltips.isPressed = true; + if (!BattleTooltips.parentElem) { + // should never happen, but in case there's a bug in the tooltip handler + BattleTooltips.parentElem = e.currentTarget; } + $(BattleTooltips.parentElem!).addClass('pressed'); + BattleTooltips.isPressed = true; }); $elem.on('touchend', '.has-tooltip', e => { e.preventDefault(); @@ -270,12 +283,14 @@ class BattleTooltips { case 'move': case 'zmove': case 'maxmove': { // move|MOVE|ACTIVEPOKEMON|[GMAXMOVE] - let move = this.battle.dex.getMove(args[1]); - let index = parseInt(args[2], 10); - let pokemon = this.battle.nearSide.active[index]; - let serverPokemon = this.battle.myPokemon![index]; - let gmaxMove = args[3] ? this.battle.dex.getMove(args[3]) : undefined; + let move = this.battle.dex.moves.get(args[1]); + let teamIndex = parseInt(args[2], 10); + let pokemon = this.battle.nearSide.active[ + teamIndex + this.battle.pokemonControlled * Math.floor(this.battle.mySide.n / 2) + ]; + let gmaxMove = args[3] ? this.battle.dex.moves.get(args[3]) : undefined; if (!pokemon) return false; + let serverPokemon = this.battle.myPokemon![teamIndex]; buf = this.showMoveTooltip(move, type, pokemon, serverPokemon, gmaxMove); break; } @@ -305,12 +320,20 @@ class BattleTooltips { // mouse over active pokemon // pokemon definitely exists, serverPokemon maybe let sideIndex = parseInt(args[1], 10); - let side = this.battle.sides[+this.battle.sidesSwitched ^ sideIndex]; + let side = this.battle.sides[+this.battle.viewpointSwitched ^ sideIndex]; let activeIndex = parseInt(args[2], 10); + let pokemonIndex = activeIndex; + if (activeIndex >= 1 && this.battle.sides.length > 2) { + pokemonIndex -= 1; + side = this.battle.sides[side.n + 2]; + } let pokemon = side.active[activeIndex]; let serverPokemon = null; - if (sideIndex === 0 && this.battle.myPokemon) { - serverPokemon = this.battle.myPokemon[activeIndex]; + if (side === this.battle.mySide && this.battle.myPokemon) { + serverPokemon = this.battle.myPokemon[pokemonIndex]; + } + if (side === this.battle.mySide.ally && this.battle.myAllyPokemon) { + serverPokemon = this.battle.myAllyPokemon[pokemonIndex]; } if (!pokemon) return false; buf = this.showPokemonTooltip(pokemon, serverPokemon, true); @@ -319,16 +342,30 @@ class BattleTooltips { case 'switchpokemon': { // switchpokemon|POKEMON // mouse over switchable pokemon // serverPokemon definitely exists, sidePokemon maybe - let side = this.battle.mySide; + // let side = this.battle.mySide; let activeIndex = parseInt(args[1], 10); let pokemon = null; - if (activeIndex < side.active.length) { + /* if (activeIndex < side.active.length && activeIndex < this.battle.pokemonControlled) { pokemon = side.active[activeIndex]; - } + if (pokemon && pokemon.side === side.ally) pokemon = null; + } */ let serverPokemon = this.battle.myPokemon![activeIndex]; buf = this.showPokemonTooltip(pokemon, serverPokemon); break; } + case 'allypokemon': { // allypokemon|POKEMON + // mouse over ally's pokemon in multi battles + // serverPokemon definitely exists, sidePokemon maybe + // let side = this.battle.mySide.ally; + let activeIndex = parseInt(args[1], 10); + let pokemon = null; + /*if (activeIndex < side.pokemon.length) { + pokemon = side.pokemon[activeIndex] || side.ally ? side.ally.pokemon[activeIndex] : null; + }*/ + let serverPokemon = this.battle.myAllyPokemon ? this.battle.myAllyPokemon[activeIndex] : null; + buf = this.showPokemonTooltip(pokemon, serverPokemon); + break; + } case 'field': { buf = this.showFieldTooltip(); break; @@ -433,13 +470,13 @@ class BattleTooltips { 'healreplacement': "Restores replacement's HP 100%", }; - getStatusZMoveEffect(move: Move) { + getStatusZMoveEffect(move: Dex.Move) { if (move.zMove!.effect! in BattleTooltips.zMoveEffects) { return BattleTooltips.zMoveEffects[move.zMove!.effect!]; } let boostText = ''; if (move.zMove!.boost) { - let boosts = Object.keys(move.zMove!.boost) as StatName[]; + let boosts = Object.keys(move.zMove!.boost) as Dex.StatName[]; boostText = boosts.map(stat => BattleTextParser.stat(stat) + ' +' + move.zMove!.boost![stat] ).join(', '); @@ -447,7 +484,7 @@ class BattleTooltips { return boostText; } - static zMoveTable: {[type in TypeName]: string} = { + static zMoveTable: {[type in Dex.TypeName]: string} = { Poison: "Acid Downpour", Fighting: "All-Out Pummeling", Dark: "Black Hole Eclipse", @@ -466,10 +503,11 @@ class BattleTooltips { Flying: "Supersonic Skystrike", Ground: "Tectonic Rage", Fairy: "Twinkle Tackle", + Stellar: "", "???": "", }; - static maxMoveTable: {[type in TypeName]: string} = { + static maxMoveTable: {[type in Dex.TypeName]: string} = { Poison: "Max Ooze", Fighting: "Max Knuckle", Dark: "Max Darkness", @@ -488,86 +526,103 @@ class BattleTooltips { Flying: "Max Airstream", Ground: "Max Quake", Fairy: "Max Starfall", + Stellar: "", "???": "", }; - getMaxMoveFromType(type: TypeName, gmaxMove?: string | Move) { + getMaxMoveFromType(type: Dex.TypeName, gmaxMove?: string | Dex.Move) { if (gmaxMove) { - gmaxMove = Dex.getMove(gmaxMove); + if (typeof gmaxMove === 'string') gmaxMove = this.battle.dex.moves.get(gmaxMove); if (type === gmaxMove.type) return gmaxMove; } - return Dex.getMove(BattleTooltips.maxMoveTable[type]); + return this.battle.dex.moves.get(BattleTooltips.maxMoveTable[type]); } - showMoveTooltip(move: Move, isZOrMax: string, pokemon: Pokemon, serverPokemon: ServerPokemon, gmaxMove?: Move) { + showMoveTooltip( + move: Dex.Move, isZOrMax: string, pokemon: Pokemon, serverPokemon: ServerPokemon, gmaxMove?: Dex.Move + ) { let text = ''; let zEffect = ''; let foeActive = pokemon.side.foe.active; + if (this.battle.gameType === 'freeforall') { + foeActive = [...foeActive, ...pokemon.side.active].filter(active => active !== pokemon); + } // TODO: move this somewhere it makes more sense if (pokemon.ability === '(suppressed)') serverPokemon.ability = '(suppressed)'; let ability = toID(serverPokemon.ability || pokemon.ability || serverPokemon.baseAbility); - let item = this.battle.dex.getItem(serverPokemon.item); + let item = this.battle.dex.items.get(serverPokemon.item); let value = new ModifiableValue(this.battle, pokemon, serverPokemon); let [moveType, category] = this.getMoveType(move, value, gmaxMove || isZOrMax === 'maxmove'); + let categoryDiff = move.category !== category; if (isZOrMax === 'zmove') { if (item.zMoveFrom === move.name) { - move = this.battle.dex.getMove(item.zMove as string); + move = this.battle.dex.moves.get(item.zMove as string); } else if (move.category === 'Status') { - move = new Move(move.id, "", { + move = new Dex.Move(move.id, "", { ...move, name: 'Z-' + move.name, }); zEffect = this.getStatusZMoveEffect(move); } else { - let moveName = BattleTooltips.zMoveTable[item.zMoveType as TypeName]; - let zMove = this.battle.dex.getMove(moveName); + let moveName = BattleTooltips.zMoveTable[item.zMoveType as Dex.TypeName]; + let zMove = this.battle.dex.moves.get(moveName); let movePower = move.zMove!.basePower; // the different Hidden Power types don't have a Z power set, fall back on base move if (!movePower && move.id.startsWith('hiddenpower')) { - movePower = this.battle.dex.getMove('hiddenpower').zMove!.basePower; + movePower = this.battle.dex.moves.get('hiddenpower').zMove!.basePower; } if (move.id === 'weatherball') { switch (this.battle.weather) { case 'sunnyday': case 'desolateland': - zMove = this.battle.dex.getMove(BattleTooltips.zMoveTable['Fire']); + zMove = this.battle.dex.moves.get(BattleTooltips.zMoveTable['Fire']); break; case 'raindance': case 'primordialsea': - zMove = this.battle.dex.getMove(BattleTooltips.zMoveTable['Water']); + zMove = this.battle.dex.moves.get(BattleTooltips.zMoveTable['Water']); break; case 'sandstorm': - zMove = this.battle.dex.getMove(BattleTooltips.zMoveTable['Rock']); + zMove = this.battle.dex.moves.get(BattleTooltips.zMoveTable['Rock']); break; case 'hail': - zMove = this.battle.dex.getMove(BattleTooltips.zMoveTable['Ice']); + case 'snowscape': + zMove = this.battle.dex.moves.get(BattleTooltips.zMoveTable['Ice']); break; } } - move = new Move(zMove.id, zMove.name, { + move = new Dex.Move(zMove.id, zMove.name, { ...zMove, category: move.category, basePower: movePower, }); + categoryDiff = false; } } else if (isZOrMax === 'maxmove') { if (move.category === 'Status') { - move = this.battle.dex.getMove('Max Guard'); + move = this.battle.dex.moves.get('Max Guard'); } else { let maxMove = this.getMaxMoveFromType(moveType, gmaxMove); const basePower = ['gmaxdrumsolo', 'gmaxfireball', 'gmaxhydrosnipe'].includes(maxMove.id) ? maxMove.basePower : move.maxMove.basePower; - move = new Move(maxMove.id, maxMove.name, { + move = new Dex.Move(maxMove.id, maxMove.name, { ...maxMove, category: move.category, basePower, }); + categoryDiff = false; } } + if (categoryDiff) { + move = new Dex.Move(move.id, move.name, { + ...move, + category, + }); + } + text += '

    ' + move.name + '
    '; text += Dex.getTypeIcon(moveType); @@ -627,17 +682,17 @@ class BattleTooltips { // In gen 3 it calls Swift, so it retains its normal typing. calls = 'Swift'; } - let calledMove = this.battle.dex.getMove(calls); + let calledMove = this.battle.dex.moves.get(calls); text += 'Calls ' + Dex.getTypeIcon(this.getMoveType(calledMove, value)[0]) + ' ' + calledMove.name; } text += '

    Accuracy: ' + accuracy + '

    '; if (zEffect) text += '

    Z-Effect: ' + zEffect + '

    '; - if (this.battle.gen < 7 || this.battle.hardcoreMode) { - text += '

    ' + move.shortDesc + '

    '; + if (this.battle.hardcoreMode) { + text += '

    ' + move.shortDesc + '

    '; } else { - text += '

    '; + text += '

    '; if (move.priority > 1) { text += 'Nearly always moves first (priority +' + move.priority + ').

    '; } else if (move.priority <= -1) { @@ -650,9 +705,9 @@ class BattleTooltips { } } - text += '' + (move.desc || move.shortDesc) + '

    '; + text += '' + (move.desc || move.shortDesc || '') + '

    '; - if (this.battle.gameType === 'doubles') { + if (this.battle.gameType === 'doubles' || this.battle.gameType === 'multi') { if (move.target === 'allAdjacent') { text += '

    ◎ Hits both foes and ally.

    '; } else if (move.target === 'allAdjacentFoes') { @@ -666,6 +721,12 @@ class BattleTooltips { } else if (move.target === 'any') { text += '

    ◎ Can target distant Pokémon in Triples.

    '; } + } else if (this.battle.gameType === 'freeforall') { + if (move.target === 'allAdjacent' || move.target === 'allAdjacentFoes') { + text += '

    ◎ Hits all foes.

    '; + } else if (move.target === 'adjacentAlly') { + text += '

    ◎ Can target any foe in Free-For-All.

    '; + } } if (move.flags.defrost) { @@ -674,7 +735,7 @@ class BattleTooltips { if (!move.flags.protect && !['self', 'allySide'].includes(move.target)) { text += `

    Not blocked by Protect (and Detect, King's Shield, Spiky Shield)

    `; } - if (move.flags.authentic) { + if (move.flags.bypasssub) { text += `

    Bypasses Substitute (but does not break it)

    `; } if (!move.flags.reflectable && !['self', 'allySide'].includes(move.target) && move.category === 'Status') { @@ -687,7 +748,7 @@ class BattleTooltips { if (move.flags.sound) { text += `

    ✓ Sound (doesn't affect Soundproof pokemon)

    `; } - if (move.flags.powder) { + if (move.flags.powder && this.battle.gen > 5) { text += `

    ✓ Powder (doesn't affect Grass, Overcoat, Safety Goggles)

    `; } if (move.flags.punch && ability === 'ironfist') { @@ -705,6 +766,12 @@ class BattleTooltips { if (move.flags.bullet) { text += `

    ✓ Bullet-like (doesn't affect Bulletproof pokemon)

    `; } + if (move.flags.slicing) { + text += `

    ✓ Slicing (boosted by Sharpness)

    `; + } + if (move.flags.wind) { + text += `

    ✓ Wind (activates Wind Power and Wind Rider)

    `; + } } return text; } @@ -731,7 +798,7 @@ class BattleTooltips { let genderBuf = ''; const gender = pokemon.gender; if (gender === 'M' || gender === 'F') { - genderBuf = ` ${gender} `; + genderBuf = ` ${gender} `; } let name = BattleLog.escapeHTML(pokemon.name); @@ -751,17 +818,25 @@ class BattleTooltips { } } - let types = this.getPokemonTypes(pokemon); + let types = serverPokemon?.terastallized ? [serverPokemon.teraType] : this.getPokemonTypes(pokemon); + let knownPokemon = serverPokemon || clientPokemon!; - if (clientPokemon && (clientPokemon.volatiles.typechange || clientPokemon.volatiles.typeadd)) { + if (pokemon.terastallized) { + text += `(Terastallized)
    `; + } else if (clientPokemon?.volatiles.typechange || clientPokemon?.volatiles.typeadd) { text += `(Type changed)
    `; } - text += types.map(type => Dex.getTypeIcon(type)).join(' '); + text += `${types.map(type => Dex.getTypeIcon(type)).join(' ')}`; + if (pokemon.terastallized) { + text += `    (base: ${this.getPokemonTypes(pokemon, true).map(type => Dex.getTypeIcon(type)).join(' ')})`; + } else if (knownPokemon.teraType && !this.battle.rules['Terastal Clause']) { + text += `    (Tera Type: ${Dex.getTypeIcon(knownPokemon.teraType)})`; + } text += `

    `; } if (illusionIndex) { - text += `

    Possible Illusion #${illusionIndex}${levelBuf}

    `; + text += `

    Possible Illusion #${illusionIndex}${levelBuf}

    `; } if (pokemon.fainted) { @@ -777,7 +852,7 @@ class BattleTooltips { } else if (pokemon.maxhp === 48) { exacthp = ' (' + pokemon.hp + '/' + pokemon.maxhp + ' pixels)'; } - text += '

    HP: ' + Pokemon.getHPText(pokemon) + exacthp + (pokemon.status ? ' ' + pokemon.status.toUpperCase() + '' : ''); + text += '

    HP: ' + Pokemon.getHPText(pokemon, this.battle.reportExactHP) + exacthp + (pokemon.status ? ' ' + pokemon.status.toUpperCase() + '' : ''); if (clientPokemon) { if (pokemon.status === 'tox') { if (pokemon.ability === 'Poison Heal' || pokemon.ability === 'Magic Guard') { @@ -807,10 +882,10 @@ class BattleTooltips { let itemEffect = ''; if (clientPokemon?.prevItem) { item = 'None'; - let prevItem = Dex.getItem(clientPokemon.prevItem).name; + let prevItem = this.battle.dex.items.get(clientPokemon.prevItem).name; itemEffect += clientPokemon.prevItemEffect ? prevItem + ' was ' + clientPokemon.prevItemEffect : 'was ' + prevItem; } - if (serverPokemon.item) item = Dex.getItem(serverPokemon.item).name; + if (serverPokemon.item) item = this.battle.dex.items.get(serverPokemon.item).name; if (itemEffect) itemEffect = ' (' + itemEffect + ')'; if (item) itemText = 'Item: ' + item + itemEffect; } else if (clientPokemon) { @@ -819,31 +894,33 @@ class BattleTooltips { if (clientPokemon.prevItem) { item = 'None'; if (itemEffect) itemEffect += '; '; - let prevItem = Dex.getItem(clientPokemon.prevItem).name; + let prevItem = this.battle.dex.items.get(clientPokemon.prevItem).name; itemEffect += clientPokemon.prevItemEffect ? prevItem + ' was ' + clientPokemon.prevItemEffect : 'was ' + prevItem; } - if (pokemon.item) item = Dex.getItem(pokemon.item).name; + if (pokemon.item) item = this.battle.dex.items.get(pokemon.item).name; if (itemEffect) itemEffect = ' (' + itemEffect + ')'; if (item) itemText = 'Item: ' + item + itemEffect; } - text += '

    '; - text += abilityText; - if (itemText) { - // ability/item on one line for your own switch tooltips, two lines everywhere else - text += (!isActive && serverPokemon ? ' / ' : '

    '); + if (abilityText || itemText) { + text += '

    '; + text += abilityText; + if (abilityText && itemText) { + // ability/item on one line for your own switch tooltips, two lines everywhere else + text += (!isActive && serverPokemon ? ' / ' : '

    '); + } text += itemText; + text += '

    '; } - text += '

    '; text += this.renderStats(clientPokemon, serverPokemon, !isActive); if (serverPokemon && !isActive) { // move list - text += `

    `; + text += `

    `; const battlePokemon = clientPokemon || this.battle.findCorrespondingPokemon(pokemon); for (const moveid of serverPokemon.moves) { - const move = Dex.getMove(moveid); + const move = this.battle.dex.moves.get(moveid); let moveName = `• ${move.name}`; if (battlePokemon?.moveTrack) { for (const row of battlePokemon.moveTrack) { @@ -858,14 +935,14 @@ class BattleTooltips { text += '

    '; } else if (!this.battle.hardcoreMode && clientPokemon?.moveTrack.length) { // move list (guessed) - text += `

    `; + text += `

    `; for (const row of clientPokemon.moveTrack) { text += `${this.getPPUseText(row)}
    `; } if (clientPokemon.moveTrack.filter(([moveName]) => { if (moveName.charAt(0) === '*') return false; - const move = this.battle.dex.getMove(moveName); - return !move.isZ && !move.isMax; + const move = this.battle.dex.moves.get(moveName); + return !move.isZ && !move.isMax && move.name !== 'Mimic'; }).length > 4) { text += `(More than 4 moves is usually a sign of Illusion Zoroark/Zorua.) `; } @@ -888,7 +965,7 @@ class BattleTooltips { for (const side of this.battle.sides) { const sideConditions = scene.sideConditionsLeft(side, true); if (sideConditions) atLeastOne = true; - buf += `

    ${BattleLog.escapeHTML(side.name)}${sideConditions || "
    (no conditions)"}

    `; + buf += `

    ${BattleLog.escapeHTML(side.name)}${sideConditions || "
    (no conditions)"}

    `; } buf += ``; if (!atLeastOne) buf = ``; @@ -917,7 +994,7 @@ class BattleTooltips { return false; } - calculateModifiedStats(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon) { + calculateModifiedStats(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon, statStagesOnly?: boolean) { let stats = {...serverPokemon.stats}; let pokemon = clientPokemon || serverPokemon; const isPowerTrick = clientPokemon?.volatiles['powertrick']; @@ -943,9 +1020,11 @@ class BattleTooltips { stats[statName] = Math.floor(stats[statName]); } } + if (statStagesOnly) return stats; - let ability = toID(serverPokemon.ability || pokemon.ability || serverPokemon.baseAbility); - if (clientPokemon && 'gastroacid' in clientPokemon.volatiles) ability = '' as ID; + const ability = toID( + clientPokemon?.effectiveAbility(serverPokemon) ?? (serverPokemon.ability || serverPokemon.baseAbility) + ); // check for burn, paralysis, guts, quick feet if (pokemon.status) { @@ -955,14 +1034,9 @@ class BattleTooltips { stats.atk = Math.floor(stats.atk * 0.5); } - if (this.battle.gen > 2 && ability === 'quickfeet') { - stats.spe = Math.floor(stats.spe * 1.5); - } else if (pokemon.status === 'par') { - if (this.battle.gen > 6) { - stats.spe = Math.floor(stats.spe * 0.5); - } else { - stats.spe = Math.floor(stats.spe * 0.25); - } + // Paralysis is calculated later in newer generations, so we need to apply it early here + if (this.battle.gen <= 2 && pokemon.status === 'par') { + stats.spe = Math.floor(stats.spe * 0.25); } } @@ -975,26 +1049,38 @@ class BattleTooltips { } let item = toID(serverPokemon.item); - if (ability === 'klutz' && item !== 'machobrace') item = '' as ID; - const speciesForme = clientPokemon ? clientPokemon.getSpeciesForme() : serverPokemon.speciesForme; - let species = Dex.getSpecies(speciesForme).baseSpecies; + let speedHalvingEVItems = ['machobrace', 'poweranklet', 'powerband', 'powerbelt', 'powerbracer', 'powerlens', 'powerweight']; + if ( + (ability === 'klutz' && !speedHalvingEVItems.includes(item)) || + this.battle.hasPseudoWeather('Magic Room') || + clientPokemon?.volatiles['embargo'] + ) { + item = '' as ID; + } + + const species = this.battle.dex.species.get(serverPokemon.speciesForme).baseSpecies; + const isTransform = clientPokemon?.volatiles.transform; + const speciesName = isTransform && clientPokemon?.volatiles.formechange?.[1] && this.battle.gen <= 4 ? + this.battle.dex.species.get(clientPokemon.volatiles.formechange[1]).baseSpecies : species; + + let speedModifiers = []; // check for light ball, thick club, metal/quick powder // the only stat modifying items in gen 2 were light ball, thick club, metal powder - if (item === 'lightball' && species === 'Pikachu') { - if (this.battle.gen >= 4) stats.atk *= 2; + if (item === 'lightball' && speciesName === 'Pikachu' && this.battle.gen !== 4) { + if (this.battle.gen > 4) stats.atk *= 2; stats.spa *= 2; } if (item === 'thickclub') { - if (species === 'Marowak' || species === 'Cubone') { + if (speciesName === 'Marowak' || speciesName === 'Cubone') { stats.atk *= 2; } } - if (species === 'Ditto' && !(clientPokemon && 'transform' in clientPokemon.volatiles)) { + if (speciesName === 'Ditto' && !(clientPokemon && 'transform' in clientPokemon.volatiles)) { if (item === 'quickpowder') { - stats.spe *= 2; + speedModifiers.push(2); } if (item === 'metalpowder') { if (this.battle.gen === 2) { @@ -1013,16 +1099,8 @@ class BattleTooltips { } let weather = this.battle.weather; - if (weather) { - // Check if anyone has an anti-weather ability - outer: for (const side of this.battle.sides) { - for (const active of side.active) { - if (active && ['Air Lock', 'Cloud Nine'].includes(active.ability)) { - weather = '' as ID; - break outer; - } - } - } + if (this.battle.abilityActive(['Air Lock', 'Cloud Nine'])) { + weather = '' as ID; } if (item === 'choiceband' && !clientPokemon?.volatiles['dynamax']) { @@ -1035,20 +1113,29 @@ class BattleTooltips { stats.atk = Math.floor(stats.atk * 1.5); } if (weather) { - if (this.battle.gen >= 4 && this.pokemonHasType(serverPokemon, 'Rock') && weather === 'sandstorm') { + if (this.battle.gen >= 4 && this.pokemonHasType(pokemon, 'Rock') && weather === 'sandstorm') { stats.spd = Math.floor(stats.spd * 1.5); } + if (this.pokemonHasType(pokemon, 'Ice') && weather === 'snowscape') { + stats.def = Math.floor(stats.def * 1.5); + } if (ability === 'sandrush' && weather === 'sandstorm') { - stats.spe *= 2; + speedModifiers.push(2); } - if (ability === 'slushrush' && weather === 'hail') { - stats.spe *= 2; + if (ability === 'slushrush' && (weather === 'hail' || weather === 'snowscape')) { + speedModifiers.push(2); } if (item !== 'utilityumbrella') { if (weather === 'sunnyday' || weather === 'desolateland') { + if (ability === 'chlorophyll') { + speedModifiers.push(2); + } if (ability === 'solarpower') { stats.spa = Math.floor(stats.spa * 1.5); } + if (ability === 'orichalcumpulse') { + stats.atk = Math.floor(stats.atk * 1.3333); + } let allyActive = clientPokemon?.side.active; if (allyActive) { for (const ally of allyActive) { @@ -1061,11 +1148,10 @@ class BattleTooltips { } } } - if (ability === 'chlorophyll' && (weather === 'sunnyday' || weather === 'desolateland')) { - stats.spe *= 2; - } - if (ability === 'swiftswim' && (weather === 'raindance' || weather === 'primordialsea')) { - stats.spe *= 2; + if (weather === 'raindance' || weather === 'primordialsea') { + if (ability === 'swiftswim') { + speedModifiers.push(2); + } } } } @@ -1074,26 +1160,52 @@ class BattleTooltips { stats.spa = Math.floor(stats.spa * 0.5); } if (clientPokemon) { - if ('slowstart' in clientPokemon.volatiles) { + if (clientPokemon.volatiles['slowstart']) { stats.atk = Math.floor(stats.atk * 0.5); - stats.spe = Math.floor(stats.spe * 0.5); + speedModifiers.push(0.5); + } + if (ability === 'unburden' && clientPokemon.volatiles['itemremoved'] && !item) { + speedModifiers.push(2); } - if (ability === 'unburden' && 'itemremoved' in clientPokemon.volatiles && !item) { - stats.spe *= 2; + for (const statName of Dex.statNamesExceptHP) { + if (clientPokemon.volatiles['protosynthesis' + statName] || clientPokemon.volatiles['quarkdrive' + statName]) { + if (statName === 'spe') { + speedModifiers.push(1.5); + } else { + stats[statName] = Math.floor(stats[statName] * 1.3); + } + } } } - if (ability === 'marvelscale' && pokemon.status) { - stats.def = Math.floor(stats.def * 1.5); + if (pokemon.status) { + if (ability === 'marvelscale') { + stats.def = Math.floor(stats.def * 1.5); + } + if (ability === 'quickfeet') { + speedModifiers.push(1.5); + } } - if (item === 'eviolite' && Dex.getSpecies(pokemon.speciesForme).evos) { + const isNFE = this.battle.dex.species.get(serverPokemon.speciesForme).evos?.some(evo => { + const evoSpecies = this.battle.dex.species.get(evo); + return !evoSpecies.isNonstandard || + evoSpecies.isNonstandard === this.battle.dex.species.get(serverPokemon.speciesForme)?.isNonstandard || + // Pokemon with Hisui evolutions + evoSpecies.isNonstandard === "Unobtainable"; + }); + if (item === 'eviolite' && (isNFE || this.battle.dex.species.get(serverPokemon.speciesForme).id === 'dipplin')) { stats.def = Math.floor(stats.def * 1.5); stats.spd = Math.floor(stats.spd * 1.5); } if (ability === 'grasspelt' && this.battle.hasPseudoWeather('Grassy Terrain')) { stats.def = Math.floor(stats.def * 1.5); } - if (ability === 'surgesurfer' && this.battle.hasPseudoWeather('Electric Terrain')) { - stats.spe *= 2; + if (this.battle.hasPseudoWeather('Electric Terrain')) { + if (ability === 'surgesurfer') { + speedModifiers.push(2); + } + if (ability === 'hadronengine') { + stats.spa = Math.floor(stats.spa * 1.3333); + } } if (item === 'choicespecs' && !clientPokemon?.volatiles['dynamax']) { stats.spa = Math.floor(stats.spa * 1.5); @@ -1126,14 +1238,140 @@ class BattleTooltips { stats.spd *= 2; } if (item === 'choicescarf' && !clientPokemon?.volatiles['dynamax']) { - stats.spe = Math.floor(stats.spe * 1.5); + speedModifiers.push(1.5); } - if (item === 'ironball' || item === 'machobrace' || /power(?!herb)/.test(item)) { - stats.spe = Math.floor(stats.spe * 0.5); + if (item === 'ironball' || speedHalvingEVItems.includes(item)) { + speedModifiers.push(0.5); } if (ability === 'furcoat') { stats.def *= 2; } + if (this.battle.abilityActive('Vessel of Ruin')) { + if (ability !== 'vesselofruin') { + stats.spa = Math.floor(stats.spa * 0.75); + } + } + if (this.battle.abilityActive('Sword of Ruin')) { + if (ability !== 'swordofruin') { + stats.def = Math.floor(stats.def * 0.75); + } + } + if (this.battle.abilityActive('Tablets of Ruin')) { + if (ability !== 'tabletsofruin') { + stats.atk = Math.floor(stats.atk * 0.75); + } + } + if (this.battle.abilityActive('Beads of Ruin')) { + if (ability !== 'beadsofruin') { + stats.spd = Math.floor(stats.spd * 0.75); + } + } + + // SSB + if (this.battle.tier.includes('Super Staff Bros')) { + if (pokemon.name === 'Felucia') { + speedModifiers.push(1.5); + } + if (ability === 'misspelled') { + stats.spa = Math.floor(stats.spa * 1.5); + } + if (ability === 'fortifyingfrost' && weather === 'snowscape') { + stats.spa = Math.floor(stats.spa * 1.5); + stats.spd = Math.floor(stats.spd * 1.5); + } + if (weather === 'deserteddunes' && this.pokemonHasType(pokemon, 'Rock')) { + stats.spd = Math.floor(stats.spd * 1.5); + } + if (pokemon.status && ability === 'fortifiedmetal') { + stats.atk = Math.floor(stats.atk * 1.5); + } + if (ability === 'grassyemperor' && this.battle.hasPseudoWeather('Grassy Terrain')) { + stats.atk = Math.floor(stats.atk * 1.3333); + } + if (ability === 'magicalmysterycharge' && this.battle.hasPseudoWeather('Electric Terrain')) { + stats.spd = Math.floor(stats.spd * 1.5); + } + if (ability === 'youkaiofthedusk' || ability === 'galeguard') { + stats.def *= 2; + } + if (ability === 'climatechange') { + if (weather === 'snowscape') { + stats.def = Math.floor(stats.def * 1.5); + stats.spd = Math.floor(stats.spd * 1.5); + } + if (weather === 'sunnyday' || weather === 'desolateland') stats.spa = Math.floor(stats.spa * 1.5); + } + if (item !== 'utilityumbrella' && ability === 'ridethesun' && + (weather === 'sunnyday' || weather === 'desolateland')) { + speedModifiers.push(2); + } + if (ability === 'soulsurfer' && this.battle.hasPseudoWeather('Electric Terrain')) { + speedModifiers.push(2); + } + if (item === 'eviolite' && this.battle.dex.species.get(serverPokemon.speciesForme).id === 'pichuspikyeared') { + stats.def = Math.floor(stats.def * 1.5); + stats.spd = Math.floor(stats.spd * 1.5); + } + if (this.battle.abilityActive('quagofruin')) { + if (ability !== 'quagofruin') { + stats.def = Math.floor(stats.def * 0.85); + } + } + if (this.battle.abilityActive('clodofruin')) { + if (ability !== 'clodofruin') { + stats.atk = Math.floor(stats.atk * 0.85); + } + } + if (this.battle.abilityActive('blitzofruin')) { + if (ability !== 'blitzofruin') { + speedModifiers.push(0.75); + } + } + if (this.battle.hasPseudoWeather('Anfield Atmosphere') && ability === 'youllneverwalkalone') { + stats.atk = Math.floor(stats.atk * 1.25); + stats.def = Math.floor(stats.def * 1.25); + stats.spd = Math.floor(stats.spd * 1.25); + speedModifiers.push(1.25); + } + if (clientPokemon) { + if (clientPokemon.volatiles['boiled']) { + stats.spa = Math.floor(stats.spa * 1.5); + } + for (const statName of Dex.statNamesExceptHP) { + if (clientPokemon.volatiles['ultramystik']) { + if (statName === 'spe') { + speedModifiers.push(1.3); + } else { + stats[statName] = Math.floor(stats[statName] * 1.3); + } + } + } + } + } + + const sideConditions = this.battle.mySide.sideConditions; + if (sideConditions['tailwind']) { + speedModifiers.push(2); + } + if (sideConditions['grasspledge']) { + speedModifiers.push(0.25); + } + + let chainedSpeedModifier = 1; + for (const modifier of speedModifiers) { + chainedSpeedModifier *= modifier; + } + // Chained modifiers round down on 0.5 + stats.spe = stats.spe * chainedSpeedModifier; + stats.spe = stats.spe % 1 > 0.5 ? Math.ceil(stats.spe) : Math.floor(stats.spe); + + if (pokemon.status === 'par' && ability !== 'quickfeet') { + if (this.battle.gen > 6) { + stats.spe = Math.floor(stats.spe * 0.5); + } else { + stats.spe = Math.floor(stats.spe * 0.25); + } + } return stats; } @@ -1191,11 +1429,12 @@ class BattleTooltips { let maxpp; if (moveName.charAt(0) === '*') { // Transformed move - move = this.battle.dex.getMove(moveName.substr(1)); + move = this.battle.dex.moves.get(moveName.substr(1)); maxpp = 5; } else { - move = this.battle.dex.getMove(moveName); - maxpp = move.noPPBoosts ? move.pp : Math.floor(move.pp * 8 / 5); + move = this.battle.dex.moves.get(moveName); + maxpp = (move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5); + if (this.battle.gen < 3) maxpp = Math.min(61, maxpp); } const bullet = moveName.charAt(0) === '*' || move.isZ ? '' : '•'; if (ppUsed === Infinity) { @@ -1207,7 +1446,7 @@ class BattleTooltips { return `${bullet} ${move.name} ${showKnown ? ' (revealed)' : ''}`; } - ppUsed(move: Move, pokemon: Pokemon) { + ppUsed(move: Dex.Move, pokemon: Pokemon) { for (let [moveName, ppUsed] of pokemon.moveTrack) { if (moveName.charAt(0) === '*') moveName = moveName.substr(1); if (move.name === moveName) return ppUsed; @@ -1219,12 +1458,41 @@ class BattleTooltips { * Calculates possible Speed stat range of an opponent */ getSpeedRange(pokemon: Pokemon): [number, number] { - let level = pokemon.level; - let baseSpe = pokemon.getSpecies().baseStats['spe']; + const tr = Math.trunc || Math.floor; + const species = pokemon.getSpecies(); + let rules = this.battle.rules; + let baseSpe = species.baseStats.spe; + if (rules['Scalemons Mod']) { + const bstWithoutHp = species.bst - species.baseStats.hp; + const scale = 600 - species.baseStats.hp; + baseSpe = tr(baseSpe * scale / bstWithoutHp); + if (baseSpe < 1) baseSpe = 1; + if (baseSpe > 255) baseSpe = 255; + } + if (rules['Frantic Fusions Mod']) { + const fusionSpecies = this.battle.dex.species.get(pokemon.name); + if (fusionSpecies.exists && fusionSpecies.name !== species.name) { + baseSpe = baseSpe + tr(fusionSpecies.baseStats.spe / 4); + if (baseSpe < 1) baseSpe = 1; + if (baseSpe > 255) baseSpe = 255; + } + } + if (rules['Flipped Mod']) { + baseSpe = species.baseStats.hp; + if (baseSpe < 1) baseSpe = 1; + if (baseSpe > 255) baseSpe = 255; + } + if (rules['350 Cup Mod'] && species.bst <= 350) { + baseSpe *= 2; + if (baseSpe < 1) baseSpe = 1; + if (baseSpe > 255) baseSpe = 255; + } + let level = pokemon.volatiles.transform?.[4] || pokemon.level; let tier = this.battle.tier; let gen = this.battle.gen; + let isCGT = tier.includes('Computer-Generated Teams'); let isRandomBattle = tier.includes('Random Battle') || - (tier.includes('Random') && tier.includes('Battle') && gen >= 6); + (tier.includes('Random') && tier.includes('Battle') && gen >= 6) || isCGT; let minNature = (isRandomBattle || gen < 3) ? 1 : 0.9; let maxNature = (isRandomBattle || gen < 3) ? 1 : 1.1; @@ -1232,7 +1500,6 @@ class BattleTooltips { let min; let max; - const tr = Math.trunc || Math.floor; if (tier.includes("Let's Go")) { min = tr(tr(tr(2 * baseSpe * level / 100 + 5) * minNature) * tr((70 / 255 / 10 + 1) * 100) / 100); max = tr(tr(tr((2 * baseSpe + maxIv) * level / 100 + 5) * maxNature) * tr((70 / 255 / 10 + 1) * 100) / 100); @@ -1240,8 +1507,8 @@ class BattleTooltips { else if (tier.includes('Random')) max += 20; } else { let maxIvEvOffset = maxIv + ((isRandomBattle && gen >= 3) ? 21 : 63); - min = tr(tr(2 * baseSpe * level / 100 + 5) * minNature); max = tr(tr((2 * baseSpe + maxIvEvOffset) * level / 100 + 5) * maxNature); + min = isCGT ? max : tr(tr(2 * baseSpe * level / 100 + 5) * minNature); } return [min, max]; } @@ -1249,20 +1516,25 @@ class BattleTooltips { /** * Gets the proper current type for moves with a variable type. */ - getMoveType(move: Move, value: ModifiableValue, forMaxMove?: boolean | Move): [TypeName, 'Physical' | 'Special' | 'Status'] { + getMoveType( + move: Dex.Move, value: ModifiableValue, forMaxMove?: boolean | Dex.Move + ): [Dex.TypeName, 'Physical' | 'Special' | 'Status'] { + const pokemon = value.pokemon; + const serverPokemon = value.serverPokemon; + let moveType = move.type; let category = move.category; if (category === 'Status' && forMaxMove) return ['Normal', 'Status']; // Max Guard // can happen in obscure situations - if (!value.pokemon) return [moveType, category]; + if (!pokemon) return [moveType, category]; - let pokemonTypes = value.pokemon.getTypeList(value.serverPokemon); + let pokemonTypes = pokemon.getTypeList(serverPokemon); value.reset(); if (move.id === 'revelationdance') { moveType = pokemonTypes[0]; } // Moves that require an item to change their type. - let item = Dex.getItem(value.itemName); + let item = this.battle.dex.items.get(value.itemName); if (move.id === 'multiattack' && item.onMemory) { if (value.itemModify(0)) moveType = item.onMemory; } @@ -1292,11 +1564,12 @@ class BattleTooltips { moveType = 'Rock'; break; case 'hail': + case 'snowscape': moveType = 'Ice'; break; } } - if (move.id === 'terrainpulse') { + if (move.id === 'terrainpulse' && pokemon.isGrounded(serverPokemon)) { if (this.battle.hasPseudoWeather('Electric Terrain')) { moveType = 'Electric'; } else if (this.battle.hasPseudoWeather('Grassy Terrain')) { @@ -1307,65 +1580,167 @@ class BattleTooltips { moveType = 'Psychic'; } } + if (move.id === 'terablast' && pokemon.terastallized) { + moveType = pokemon.terastallized as Dex.TypeName; + } + if (move.id === 'terastarstorm' && pokemon.getSpeciesForme() === 'Terapagos-Stellar') { + moveType = 'Stellar'; + } // Aura Wheel as Morpeko-Hangry changes the type to Dark - if (move.id === 'aurawheel' && value.pokemon.getSpeciesForme() === 'Morpeko-Hangry') { + if (move.id === 'aurawheel' && pokemon.getSpeciesForme() === 'Morpeko-Hangry') { moveType = 'Dark'; } + // Raging Bull's type depends on the Tauros forme + if (move.id === 'ragingbull') { + switch (pokemon.getSpeciesForme()) { + case 'Tauros-Paldea-Combat': + moveType = 'Fighting'; + break; + case 'Tauros-Paldea-Blaze': + moveType = 'Fire'; + break; + case 'Tauros-Paldea-Aqua': + moveType = 'Water'; + break; + } + } + // Ivy Cudgel's type depends on the Ogerpon forme + if (move.id === 'ivycudgel') { + switch (pokemon.getSpeciesForme()) { + case 'Ogerpon-Wellspring': case 'Ogerpon-Wellspring-Tera': + moveType = 'Water'; + break; + case 'Ogerpon-Hearthflame': case 'Ogerpon-Hearthflame-Tera': + moveType = 'Fire'; + break; + case 'Ogerpon-Cornerstone': case 'Ogerpon-Cornerstone-Tera': + moveType = 'Rock'; + break; + } + } // Other abilities that change the move type. const noTypeOverride = [ 'judgment', 'multiattack', 'naturalgift', 'revelationdance', 'struggle', 'technoblast', 'terrainpulse', 'weatherball', ]; - const allowTypeOverride = !forMaxMove && !noTypeOverride.includes(move.id); + const allowTypeOverride = !noTypeOverride.includes(move.id) && (move.id !== 'terablast' || !pokemon.terastallized); + if (allowTypeOverride) { + if (this.battle.rules['Revelationmons Mod']) { + const [types] = pokemon.getTypes(serverPokemon); + for (let i = 0; i < types.length; i++) { + if (serverPokemon.moves[i] && move.id === toID(serverPokemon.moves[i])) { + moveType = types[i]; + } + } + } - if (allowTypeOverride && category !== 'Status' && !move.isZ) { - if (moveType === 'Normal') { - if (value.abilityModify(0, 'Aerilate')) moveType = 'Flying'; - if (value.abilityModify(0, 'Galvanize')) moveType = 'Electric'; - if (value.abilityModify(0, 'Pixilate')) moveType = 'Fairy'; - if (value.abilityModify(0, 'Refrigerate')) moveType = 'Ice'; + if (category !== 'Status' && !move.isZ && !move.id.startsWith('hiddenpower')) { + if (moveType === 'Normal') { + if (value.abilityModify(0, 'Aerilate')) moveType = 'Flying'; + if (value.abilityModify(0, 'Galvanize')) moveType = 'Electric'; + if (value.abilityModify(0, 'Pixilate')) moveType = 'Fairy'; + if (value.abilityModify(0, 'Refrigerate')) moveType = 'Ice'; + } + if (value.abilityModify(0, 'Normalize')) moveType = 'Normal'; + } + + // There aren't any max moves with the sound flag, but if there were, Liquid Voice would make them water type + const isSound = !!( + forMaxMove ? + this.getMaxMoveFromType(moveType, forMaxMove !== true && forMaxMove || undefined) : move + ).flags['sound']; + if (isSound && value.abilityModify(0, 'Liquid Voice')) { + moveType = 'Water'; } - if (value.abilityModify(0, 'Normalize')) moveType = 'Normal'; } - // There aren't any max moves with the sound flag, but if there were, Liquid Voice would make them water type - const isSound = !!(forMaxMove ? this.getMaxMoveFromType(moveType, forMaxMove !== true && forMaxMove || undefined) : move).flags['sound']; - if (allowTypeOverride && isSound && value.abilityModify(0, 'Liquid Voice')) { - moveType = 'Water'; + + if (move.id === 'photongeyser' || move.id === 'lightthatburnsthesky' || + (move.id === 'terablast' && pokemon.terastallized) || + (move.id === 'terastarstorm' && pokemon.getSpeciesForme() === 'Terapagos-Stellar')) { + const stats = this.calculateModifiedStats(pokemon, serverPokemon, true); + if (stats.atk > stats.spa) category = 'Physical'; } - if (this.battle.gen <= 3 && category !== 'Status') { - category = Dex.getGen3Category(moveType); + + // SSB + if (this.battle.tier.includes('Super Staff Bros')) { + if (allowTypeOverride && category !== "Status" && !move.isZ && !move.id.startsWith('hiddenpower')) { + if (value.abilityModify(0, 'Acetosa')) moveType = 'Grass'; + if (value.abilityModify(0, 'I Can Hear The Heart Beating As One') && moveType === 'Normal') moveType = 'Fairy'; + } + if (move.id === 'tsignore' || move.id === 'o') { + const stats = this.calculateModifiedStats(pokemon, serverPokemon, true); + if (stats.atk > stats.spa) category = 'Physical'; + } + if (move.id === 'tsignore' && pokemon.getSpeciesForme().startsWith('Meloetta') && + pokemon.terastallized) { + moveType = 'Stellar'; + } + if (move.id === 'weatherball' && value.weatherModify(0)) { + if (this.battle.weather === 'stormsurge') moveType = 'Water'; + if (this.battle.weather === 'deserteddunes') moveType = 'Rock'; + } + if (move.id === 'o' || move.id === 'worriednoises') { + moveType = pokemonTypes[0]; + } + if (move.id === 'dillydally') { + moveType = pokemonTypes[pokemonTypes.length - 1]; + } + if (move.id === 'magicalfocus') { + if (this.battle.turn % 3 === 1) { + moveType = 'Fire'; + } else if (this.battle.turn % 3 === 2) { + moveType = 'Electric'; + } else { + moveType = 'Ice'; + } + } + if (move.id === 'hydrostatics' && pokemon.terastallized) { + moveType = 'Water'; + } + if (move.id === 'asongoficeandfire' && pokemon.getSpeciesForme() === 'Volcarona') moveType = 'Ice'; + if (this.battle.abilityActive('dynamictyping')) { + moveType = '???'; + } + if (move.id === 'alting') { + moveType = '???'; + if (pokemon.shiny) { + category = 'Special'; + } + } } return [moveType, category]; } // Gets the current accuracy for a move. - getMoveAccuracy(move: Move, value: ModifiableValue, target?: Pokemon) { + getMoveAccuracy(move: Dex.Move, value: ModifiableValue, target?: Pokemon) { value.reset(move.accuracy === true ? 0 : move.accuracy, true); let pokemon = value.pokemon!; + // Sure-hit accuracy if (move.id === 'toxic' && this.battle.gen >= 6 && this.pokemonHasType(pokemon, 'Poison')) { value.set(0, "Poison type"); return value; } - if (move.id === 'blizzard') { + if (move.id === 'blizzard' && this.battle.gen >= 4) { value.weatherModify(0, 'Hail'); + value.weatherModify(0, 'Snow'); } - if (move.id === 'hurricane' || move.id === 'thunder') { + if (['hurricane', 'thunder', 'bleakwindstorm', 'wildboltstorm', 'sandsearstorm'].includes(move.id)) { value.weatherModify(0, 'Rain Dance'); value.weatherModify(0, 'Primordial Sea'); - if (value.tryWeather('Sunny Day')) value.set(50, 'Sunny Day'); - if (value.tryWeather('Desolate Land')) value.set(50, 'Desolate Land'); } value.abilityModify(0, 'No Guard'); if (!value.value) return value; + + // OHKO moves don't use standard accuracy / evasion modifiers if (move.ohko) { if (this.battle.gen === 1) { value.set(value.value, `fails if target's Speed is higher`); return value; } - if (move.id === 'sheercold' && this.battle.gen >= 7) { - if (!this.pokemonHasType(pokemon, 'Ice')) value.set(20, 'not Ice-type'); + if (move.id === 'sheercold' && this.battle.gen >= 7 && !this.pokemonHasType(pokemon, 'Ice')) { + value.set(20, 'not Ice-type'); } if (target) { if (pokemon.level < target.level) { @@ -1380,27 +1755,102 @@ class BattleTooltips { } return value; } - if (pokemon?.boosts.accuracy) { - if (pokemon.boosts.accuracy > 0) { - value.modify((pokemon.boosts.accuracy + 3) / 3); - } else { - value.modify(3 / (3 - pokemon.boosts.accuracy)); - } - } - if (move.category === 'Physical') { - value.abilityModify(0.8, "Hustle"); + + // Accuracy modifiers start + + let accuracyModifiers = []; + if (this.battle.hasPseudoWeather('Gravity')) { + accuracyModifiers.push(6840); + value.modify(5 / 3, "Gravity"); } - value.abilityModify(1.3, "Compound Eyes"); + for (const active of pokemon.side.active) { if (!active || active.fainted) continue; - let ability = this.getAllyAbility(active); + const ability = this.getAllyAbility(active); if (ability === 'Victory Star') { + accuracyModifiers.push(4506); value.modify(1.1, "Victory Star"); } } - value.itemModify(1.1, "Wide Lens"); - if (this.battle.hasPseudoWeather('Gravity')) { - value.modify(5 / 3, "Gravity"); + + if (value.tryAbility('Hustle') && move.category === 'Physical') { + accuracyModifiers.push(3277); + value.abilityModify(0.8, "Hustle"); + } else if (value.tryAbility('Compound Eyes')) { + accuracyModifiers.push(5325); + value.abilityModify(1.3, "Compound Eyes"); + } + + if (value.tryItem('Wide Lens')) { + accuracyModifiers.push(4505); + value.itemModify(1.1, "Wide Lens"); + } + + // SSB + if (this.battle.tier.includes('Super Staff Bros')) { + if (move.id === 'alting' && pokemon.shiny) { + value.set(100); + } + if (move.flags['wind'] && this.battle.weather === 'stormsurge') { + value.weatherModify(0, 'Storm Surge'); + } + if (value.tryAbility('Misspelled') && move.category === 'Special') { + accuracyModifiers.push(3277); + value.abilityModify(0.8, "Misspelled"); + } + if (value.tryAbility('Hydrostatic Positivity') && ['Electric', 'Water'].includes(move.type)) { + accuracyModifiers.push(5325); + value.abilityModify(1.3, "Hydrostatic Positivity"); + } + if (value.tryAbility('Hardcore Hustle')) { + for (let i = 1; i <= 5 && i <= pokemon.side.faintCounter; i++) { + if (pokemon.volatiles[`fallen${i}`]) { + value.abilityModify([1, 0.95, 0.90, 0.85, 0.80, 0.75][i], "Hardcore Hustle"); + } + } + } + if (value.tryAbility('See No Evil, Hear No Evil, Speak No Evil') && + pokemon.getSpeciesForme().includes('Wellspring')) { + value.abilityModify(0, 'See No Evil, Hear No Evil, Speak No Evil'); + } + value.abilityModify(0, 'Sure Hit Sorcery'); + value.abilityModify(0, 'Eyes of Eternity'); + if (!value.value) return value; + } + + // Chaining modifiers + let chain = 4096; + for (const mod of accuracyModifiers) { + if (mod !== 4096) { + chain = (chain * mod + 2048) >> 12; + } + } + + // Applying modifiers + value.set(move.accuracy as number); + + if (move.id === 'hurricane' || move.id === 'thunder') { + if (value.tryWeather('Sunny Day')) value.set(50, 'Sunny Day'); + if (value.tryWeather('Desolate Land')) value.set(50, 'Desolate Land'); + } + + // Chained modifiers round down on 0.5 + let accuracyAfterChain = (value.value * chain) / 4096; + accuracyAfterChain = accuracyAfterChain % 1 > 0.5 ? Math.ceil(accuracyAfterChain) : Math.floor(accuracyAfterChain); + value.set(accuracyAfterChain); + + // Unlike for Atk, Def, etc. accuracy and evasion boosts are applied after modifiers + if (pokemon?.boosts.accuracy) { + if (pokemon.boosts.accuracy > 0) { + value.set(Math.floor(value.value * (pokemon.boosts.accuracy + 3) / 3)); + } else { + value.set(Math.floor(value.value * 3 / (3 - pokemon.boosts.accuracy))); + } + } + + // 1/256 glitch + if (this.battle.gen === 1 && !toID(this.battle.tier).includes('stadium')) { + value.set((Math.floor(value.value * 255 / 100) / 256) * 100); } return value; } @@ -1408,7 +1858,7 @@ class BattleTooltips { // Gets the proper current base power for moves which have a variable base power. // Takes into account the target for some moves. // If it is unsure of the actual base power, it gives an estimate. - getMoveBasePower(move: Move, moveType: TypeName, value: ModifiableValue, target: Pokemon | null = null) { + getMoveBasePower(move: Dex.Move, moveType: Dex.TypeName, value: ModifiableValue, target: Pokemon | null = null) { const pokemon = value.pokemon!; const serverPokemon = value.serverPokemon; @@ -1422,12 +1872,18 @@ class BattleTooltips { value.modify(2, "Acrobatics + no item"); } } - if (['crushgrip', 'wringout'].includes(move.id) && target) { + let variableBPCap = ['crushgrip', 'wringout'].includes(move.id) ? 120 : move.id === 'hardpress' ? 100 : undefined; + if (variableBPCap && target) { value.set( - Math.floor(Math.floor((120 * (100 * Math.floor(target.hp * 4096 / target.maxhp)) + 2048 - 1) / 4096) / 100) || 1, + Math.floor( + Math.floor((variableBPCap * (100 * Math.floor(target.hp * 4096 / target.maxhp)) + 2048 - 1) / 4096) / 100 + ) || 1, 'approximate' ); } + if (move.id === 'terablast' && pokemon.terastallized === 'Stellar') { + value.set(100, 'Tera Stellar boost'); + } if (move.id === 'brine' && target && target.hp * 2 <= target.maxhp) { value.modify(2, 'Brine + target below half HP'); } @@ -1457,8 +1913,11 @@ class BattleTooltips { else basePower = 20; value.set(basePower); } - if (move.id === 'hex' && target?.status) { - value.modify(2, 'Hex + status'); + if (['hex', 'infernalparade'].includes(move.id) && target?.status) { + value.modify(2, move.name + ' + status'); + } + if (move.id === 'lastrespects') { + value.set(Math.min(50 + 50 * pokemon.side.faintCounter)); } if (move.id === 'punishment' && target) { let boostCount = 0; @@ -1488,9 +1947,12 @@ class BattleTooltips { else if (ppLeft === 4) basePower = 50; value.set(basePower); } - if (move.id === 'venoshock' && target) { + if (move.id === 'magnitude') { + value.setRange(10, 150); + } + if (['venoshock', 'barbbarrage'].includes(move.id) && target) { if (['psn', 'tox'].includes(target.status)) { - value.modify(2, 'Venoshock + Poison'); + value.modify(2, move.name + ' + Poison'); } } if (move.id === 'wakeupslap' && target) { @@ -1503,7 +1965,13 @@ class BattleTooltips { value.weatherModify(2); } } - if (move.id === 'terrainpulse') { + if (move.id === 'hydrosteam') { + value.weatherModify(1.5, 'Sunny Day'); + } + if (move.id === 'psyblade' && this.battle.hasPseudoWeather('Electric Terrain')) { + value.modify(1.5, 'Electric Terrain'); + } + if (move.id === 'terrainpulse' && pokemon.isGrounded(serverPokemon)) { if ( this.battle.hasPseudoWeather('Electric Terrain') || this.battle.hasPseudoWeather('Grassy Terrain') || @@ -1550,7 +2018,7 @@ class BattleTooltips { } // Moves which have base power changed due to items if (serverPokemon.item) { - let item = Dex.getItem(serverPokemon.item); + let item = this.battle.dex.items.get(serverPokemon.item); if (move.id === 'fling' && item.fling) { value.itemModify(item.fling.basePower); } @@ -1559,7 +2027,7 @@ class BattleTooltips { } } // Moves which have base power changed according to weight - if (['lowkick', 'grassknot', 'heavyslam', 'heatcrash'].includes(move.id)) { + if (['lowkick', 'grassknot', 'heavyslam', 'heatcrash'].includes(move.id) && this.battle.gen > 2) { let isGKLK = ['lowkick', 'grassknot'].includes(move.id); if (target) { let targetWeight = target.getWeightKg(); @@ -1574,10 +2042,10 @@ class BattleTooltips { else if (targetWeight >= 10) basePower = 40; } else { basePower = 40; - if (pokemonWeight > targetWeight * 5) basePower = 120; - else if (pokemonWeight > targetWeight * 4) basePower = 100; - else if (pokemonWeight > targetWeight * 3) basePower = 80; - else if (pokemonWeight > targetWeight * 2) basePower = 60; + if (pokemonWeight >= targetWeight * 5) basePower = 120; + else if (pokemonWeight >= targetWeight * 4) basePower = 100; + else if (pokemonWeight >= targetWeight * 3) basePower = 80; + else if (pokemonWeight >= targetWeight * 2) basePower = 60; } if (target.volatiles['dynamax']) { value.set(0, 'blocked by target\'s Dynamax'); @@ -1588,12 +2056,22 @@ class BattleTooltips { value.setRange(isGKLK ? 20 : 40, 120); } } + // Base power based on times hit + if (move.id === 'ragefist') { + value.set(Math.min(350, 50 + 50 * pokemon.timesAttacked), + pokemon.timesAttacked > 0 + ? `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}` + : undefined); + } if (!value.value) return value; // Other ability boosts if (pokemon.status === 'brn' && move.category === 'Special') { value.abilityModify(1.5, "Flare Boost"); } + if (move.flags['punch']) { + value.abilityModify(1.2, 'Iron Fist'); + } if (move.flags['pulse']) { value.abilityModify(1.5, "Mega Launcher"); } @@ -1606,9 +2084,6 @@ class BattleTooltips { if (['psn', 'tox'].includes(pokemon.status) && move.category === 'Physical') { value.abilityModify(1.5, "Toxic Boost"); } - if (this.battle.gen > 2 && serverPokemon.status === 'brn' && move.id !== 'facade' && move.category === 'Physical') { - if (!value.tryAbility("Guts")) value.modify(0.5, 'Burn'); - } if (['Rock', 'Ground', 'Steel'].includes(moveType) && this.battle.weather === 'sandstorm') { if (value.tryAbility("Sand Force")) value.weatherModify(1.3, "Sandstorm", "Sand Force"); } @@ -1618,12 +2093,17 @@ class BattleTooltips { if (move.flags['contact']) { value.abilityModify(1.3, "Tough Claws"); } - if (moveType === 'Steel') { - value.abilityModify(1.5, "Steely Spirit"); - } if (move.flags['sound']) { value.abilityModify(1.3, "Punk Rock"); } + if (move.flags['slicing']) { + value.abilityModify(1.5, "Sharpness"); + } + for (let i = 1; i <= 5 && i <= pokemon.side.faintCounter; i++) { + if (pokemon.volatiles[`fallen${i}`]) { + value.abilityModify(1 + 0.1 * i, "Supreme Overlord"); + } + } if (target) { if (["MF", "FM"].includes(pokemon.gender + target.gender)) { value.abilityModify(0.75, "Rivalry"); @@ -1634,7 +2114,11 @@ class BattleTooltips { const noTypeOverride = [ 'judgment', 'multiattack', 'naturalgift', 'revelationdance', 'struggle', 'technoblast', 'terrainpulse', 'weatherball', ]; - if (move.category !== 'Status' && !noTypeOverride.includes(move.id) && !move.isZ && !move.isMax) { + const allowTypeOverride = !noTypeOverride.includes(move.id) && (move.id !== 'terablast' || !pokemon.terastallized); + if ( + move.category !== 'Status' && allowTypeOverride && !move.isZ && !move.isMax && + !move.id.startsWith('hiddenpower') + ) { if (move.type === 'Normal') { value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Aerilate"); value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Galvanize"); @@ -1645,9 +2129,6 @@ class BattleTooltips { value.abilityModify(1.2, "Normalize"); } } - if (move.flags['punch']) { - value.abilityModify(1.2, 'Iron Fist'); - } if (move.recoil || move.hasCrashDamage) { value.abilityModify(1.2, 'Reckless'); } @@ -1664,22 +2145,20 @@ class BattleTooltips { auraBoosted = 'Dark Aura'; } else if (allyAbility === 'Aura Break') { auraBroken = true; - } else if (allyAbility === 'Battery') { - if (ally !== pokemon && move.category === 'Special') { - value.modify(1.3, 'Battery'); - } - } else if (allyAbility === 'Power Spot') { - if (ally !== pokemon) { - value.modify(1.3, 'Power Spot'); - } + } else if (allyAbility === 'Battery' && ally !== pokemon && move.category === 'Special') { + value.modify(1.3, 'Battery'); + } else if (allyAbility === 'Power Spot' && ally !== pokemon) { + value.modify(1.3, 'Power Spot'); + } else if (allyAbility === 'Steely Spirit' && moveType === 'Steel') { + value.modify(1.5, 'Steely Spirit'); } } for (const foe of pokemon.side.foe.active) { if (!foe || foe.fainted) continue; - if (foe.ability === 'Fairy Aura') { - if (moveType === 'Fairy') auraBoosted = 'Fairy Aura'; - } else if (foe.ability === 'Dark Aura') { - if (moveType === 'Dark') auraBoosted = 'Dark Aura'; + if (foe.ability === 'Fairy Aura' && moveType === 'Fairy') { + auraBoosted = 'Fairy Aura'; + } else if (foe.ability === 'Dark Aura' && moveType === 'Dark') { + auraBoosted = 'Dark Aura'; } else if (foe.ability === 'Aura Break') { auraBroken = true; } @@ -1704,6 +2183,12 @@ class BattleTooltips { if (target ? target.isGrounded() : true) { value.modify(0.5, 'Misty Terrain + grounded target'); } + } else if ( + this.battle.hasPseudoWeather('Grassy Terrain') && ['earthquake', 'bulldoze', 'magnitude'].includes(move.id) + ) { + if (target ? target.isGrounded() : true) { + value.modify(0.5, 'Grassy Terrain + grounded target'); + } } if ( move.id === 'expandingforce' && @@ -1718,6 +2203,25 @@ class BattleTooltips { if (move.id === 'risingvoltage' && this.battle.hasPseudoWeather('Electric Terrain') && target?.isGrounded()) { value.modify(2, 'Rising Voltage + Electric Terrain boost'); } + + // Item + value = this.getItemBoost(move, value, moveType); + + // Terastal base power floor + if ( + pokemon.terastallized && (pokemon.terastallized === move.type || pokemon.terastallized === 'Stellar') && + value.value < 60 && move.priority <= 0 && !move.multihit && !( + (move.basePower === 0 || move.basePower === 150) && move.basePowerCallback + ) + ) { + value.set(60, 'Tera type BP minimum'); + } + + // Burn isn't really a base power modifier, so it needs to be applied after the Tera BP floor + if (this.battle.gen > 2 && serverPokemon.status === 'brn' && move.id !== 'facade' && move.category === 'Physical') { + if (!value.tryAbility("Guts")) value.modify(0.5, 'Burn'); + } + if ( move.id === 'steelroller' && !this.battle.hasPseudoWeather('Electric Terrain') && @@ -1728,24 +2232,92 @@ class BattleTooltips { value.set(0, 'no Terrain'); } - // Item - value = this.getItemBoost(move, value, moveType); + // SSB + if (this.battle.tier.includes('Super Staff Bros')) { + if (move.id === 'bodycount') { + value.set(50 + 50 * pokemon.side.faintCounter, + pokemon.side.faintCounter > 0 + ? `${pokemon.side.faintCounter} teammate${pokemon.side.faintCounter > 1 ? 's' : ''} KOed` + : undefined); + } + // Base power based on times hit + if (move.id === 'vengefulmood') { + value.set(Math.min(140, 60 + 20 * pokemon.timesAttacked), + pokemon.timesAttacked > 0 + ? `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}` + : undefined); + } + if (move.id === 'alting' && pokemon.shiny) { + value.set(69, 'Shiny'); + } + if (move.id === 'darkmooncackle') { + let boostCount = 0; + for (const boost of Object.values(pokemon.boosts)) { + if (boost > 0) boostCount += boost; + } + value.set(30 + 20 * boostCount); + } + if (move.id === 'buildingcharacter' && target?.terastallized) { + value.modify(2, 'Terastallized target'); + } + if (move.id === 'mysticalbonfire' && target?.status) { + value.modify(1.5, 'Mystical Bonfire + status'); + } + if (move.id === 'adaptivebeam' && target && Object.values(target.boosts).some(x => x > 0)) { + value.set(0, "Target has more boosts"); + } + if (value.value <= 60) { + value.abilityModify(1.5, "Confirmed Town"); + } + if (move.category !== 'Status' && allowTypeOverride && !move.isZ && + !move.isMax && !move.id.startsWith('hiddenpower')) { + if (moveType === 'Normal') value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "I Can Hear The Heart Beating As One"); + value.abilityModify(this.battle.gen > 6 ? 1.2 : 1.3, "Acetosa"); + } + if (move.flags['punch']) { + value.abilityModify(1.5, "Harambe Hit"); + } + if (move.flags['slicing']) { + value.abilityModify(1.5, "I Can Hear The Heart Beating As One"); + } + if (move.priority > 0) { + value.abilityModify(2, "Full Bloom"); + } + if (move.recoil || move.hasCrashDamage) { + value.abilityModify(1.2, 'Hogwash'); + if (pokemon.name === "Billo") { + value.modify(1.2); + } + } + if (target?.gender === "M" && pokemon.getSpeciesForme().includes("Hearthflame")) { + value.abilityModify(1.3, 'See No Evil, Hear No Evil, Speak No Evil'); + } + for (let i = 1; i <= 5 && i <= pokemon.side.faintCounter; i++) { + if (pokemon.volatiles[`fallen${i}`]) { + value.abilityModify([1, 1.15, 1.3, 1.45, 1.6, 1.75][i], "Hardcore Hustle"); + } + } + let timeDilationBPMod = 1 + (0.1 * Math.floor(this.battle.turn / 10)); + if (timeDilationBPMod > 2) timeDilationBPMod = 2; + value.abilityModify(timeDilationBPMod, "Time Dilation"); + } return value; } - static incenseTypes: {[itemName: string]: TypeName} = { + static incenseTypes: {[itemName: string]: Dex.TypeName} = { 'Odd Incense': 'Psychic', 'Rock Incense': 'Rock', 'Rose Incense': 'Grass', 'Sea Incense': 'Water', 'Wave Incense': 'Water', }; - static itemTypes: {[itemName: string]: TypeName} = { + static itemTypes: {[itemName: string]: Dex.TypeName} = { 'Black Belt': 'Fighting', 'Black Glasses': 'Dark', 'Charcoal': 'Fire', 'Dragon Fang': 'Dragon', + 'Fairy Feather': 'Fairy', 'Hard Stone': 'Rock', 'Magnet': 'Electric', 'Metal Coat': 'Steel', @@ -1760,18 +2332,23 @@ class BattleTooltips { 'Spell Tag': 'Ghost', 'Twisted Spoon': 'Psychic', }; - static orbUsers: {[speciesForme: string]: string} = { - 'Latias': 'Soul Dew', - 'Latios': 'Soul Dew', - 'Dialga': 'Adamant Orb', - 'Palkia': 'Lustrous Orb', - 'Giratina': 'Griseous Orb', + static orbUsers: {[speciesForme: string]: string[]} = { + 'Latias': ['Soul Dew'], + 'Latios': ['Soul Dew'], + 'Dialga': ['Adamant Crystal', 'Adamant Orb'], + 'Palkia': ['Lustrous Globe', 'Lustrous Orb'], + 'Giratina': ['Griseous Core', 'Griseous Orb'], + 'Venomicon': ['Vile Vial'], }; - static orbTypes: {[itemName: string]: TypeName} = { - 'Soul Dew': 'Psychic', - 'Adamant Orb': 'Steel', - 'Lustrous Orb': 'Water', - 'Griseous Orb': 'Ghost', + static orbTypes: {[itemName: string]: Dex.TypeName[]} = { + 'Soul Dew': ['Psychic', 'Dragon'], + 'Adamant Crystal': ['Steel', 'Dragon'], + 'Adamant Orb': ['Steel', 'Dragon'], + 'Lustrous Globe': ['Water', 'Dragon'], + 'Lustrous Orb': ['Water', 'Dragon'], + 'Griseous Core': ['Ghost', 'Dragon'], + 'Griseous Orb': ['Ghost', 'Dragon'], + 'Vile Vial': ['Poison', 'Flying'], }; static noGemMoves = [ 'Fire Pledge', @@ -1780,10 +2357,14 @@ class BattleTooltips { 'Struggle', 'Water Pledge', ]; - getItemBoost(move: Move, value: ModifiableValue, moveType: TypeName) { - let item = this.battle.dex.getItem(value.serverPokemon.item); + getItemBoost(move: Dex.Move, value: ModifiableValue, moveType: Dex.TypeName) { + let item = this.battle.dex.items.get(value.serverPokemon.item); let itemName = item.name; let moveName = move.name; + let species = this.battle.dex.species.get(value.serverPokemon.speciesForme); + let isTransform = value.pokemon.volatiles.transform; + let speciesName = isTransform && value.pokemon.volatiles.formechange?.[1] && this.battle.gen <= 4 ? + this.battle.dex.species.get(value.pokemon.volatiles.formechange[1]).baseSpecies : species.baseSpecies; // Plates if (item.onPlate === moveType && !item.zMove) { @@ -1803,13 +2384,28 @@ class BattleTooltips { return value; } + // Light ball is a base power modifier in gen 4 only + if (item.name === 'Light Ball' && this.battle.gen === 4 && speciesName === 'Pikachu') { + value.itemModify(2); + return value; + } + // Pokemon-specific items if (item.name === 'Soul Dew' && this.battle.gen < 7) return value; - if (BattleTooltips.orbUsers[Dex.getSpecies(value.serverPokemon.speciesForme).baseSpecies] === item.name && - [BattleTooltips.orbTypes[item.name], 'Dragon'].includes(moveType)) { + if (BattleTooltips.orbUsers[speciesName]?.includes(item.name) && + BattleTooltips.orbTypes[item.name]?.includes(moveType)) { value.itemModify(1.2); return value; } + if (speciesName === 'Ogerpon') { + const speciesForme = value.pokemon.getSpeciesForme(); + if ((speciesForme.startsWith('Ogerpon-Wellspring') && itemName === 'Wellspring Mask') || + (speciesForme.startsWith('Ogerpon-Hearthflame') && itemName === 'Hearthflame Mask') || + (speciesForme.startsWith('Ogerpon-Cornerstone') && itemName === 'Cornerstone Mask')) { + value.itemModify(1.2); + return value; + } + } // Gems if (BattleTooltips.noGemMoves.includes(moveName)) return value; @@ -1818,16 +2414,22 @@ class BattleTooltips { return value; } + if (itemName === 'Muscle Band' && move.category === 'Physical' || + itemName === 'Wise Glasses' && move.category === 'Special' || + itemName === 'Punching Glove' && move.flags['punch']) { + value.itemModify(1.1); + } + return value; } - getPokemonTypes(pokemon: Pokemon | ServerPokemon): ReadonlyArray { + getPokemonTypes(pokemon: Pokemon | ServerPokemon, preterastallized = false): ReadonlyArray { if (!(pokemon as Pokemon).getTypes) { - return this.battle.dex.getSpecies(pokemon.speciesForme).types; + return this.battle.dex.species.get(pokemon.speciesForme).types; } - return (pokemon as Pokemon).getTypeList(); + return (pokemon as Pokemon).getTypeList(undefined, preterastallized); } - pokemonHasType(pokemon: Pokemon | ServerPokemon, type: TypeName, types?: ReadonlyArray) { + pokemonHasType(pokemon: Pokemon | ServerPokemon, type: Dex.TypeName, types?: ReadonlyArray) { if (!types) types = this.getPokemonTypes(pokemon); for (const curType of types) { if (curType === type) return true; @@ -1835,13 +2437,13 @@ class BattleTooltips { return false; } getAllyAbility(ally: Pokemon) { - // this will only be available if the ability announced itself in some way - let allyAbility = Dex.getAbility(ally.ability).name; - // otherwise fall back on the original set data sent from the server - if (!allyAbility && this.battle.myPokemon) { - allyAbility = Dex.getAbility(this.battle.myPokemon[ally.slot].ability).name; + let serverPokemon; + if (this.battle.myAllyPokemon) { + serverPokemon = this.battle.myAllyPokemon[ally.slot]; + } else if (this.battle.myPokemon) { + serverPokemon = this.battle.myPokemon[ally.slot]; } - return allyAbility; + return ally.effectiveAbility(serverPokemon); } getPokemonAbilityData(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon | null | undefined) { const abilityData: {ability: string, baseAbility: string, possibilities: string[]} = { @@ -1855,12 +2457,20 @@ class BattleTooltips { } } else { const speciesForme = clientPokemon.getSpeciesForme() || serverPokemon?.speciesForme || ''; - const species = this.battle.dex.getSpecies(speciesForme); + const species = this.battle.dex.species.get(speciesForme); if (species.exists && species.abilities) { abilityData.possibilities = [species.abilities['0']]; if (species.abilities['1']) abilityData.possibilities.push(species.abilities['1']); if (species.abilities['H']) abilityData.possibilities.push(species.abilities['H']); if (species.abilities['S']) abilityData.possibilities.push(species.abilities['S']); + if (this.battle.rules['Frantic Fusions Mod']) { + const fusionSpecies = this.battle.dex.species.get(clientPokemon.name); + if (fusionSpecies.exists && fusionSpecies.name !== species.name) { + abilityData.possibilities = Array.from( + new Set(abilityData.possibilities.concat(Object.values(fusionSpecies.abilities))) + ); + } + } } } } @@ -1883,60 +2493,25 @@ class BattleTooltips { if (!isActive) { // for switch tooltips, only show the original ability const ability = abilityData.baseAbility || abilityData.ability; - if (ability) text = 'Ability: ' + Dex.getAbility(ability).name; + if (ability) text = 'Ability: ' + this.battle.dex.abilities.get(ability).name; } else { if (abilityData.ability) { - const abilityName = Dex.getAbility(abilityData.ability).name; + const abilityName = this.battle.dex.abilities.get(abilityData.ability).name; text = 'Ability: ' + abilityName; - const baseAbilityName = Dex.getAbility(abilityData.baseAbility).name; + const baseAbilityName = this.battle.dex.abilities.get(abilityData.baseAbility).name; if (baseAbilityName && baseAbilityName !== abilityName) text += ' (base: ' + baseAbilityName + ')'; } } - if (!text && abilityData.possibilities.length && !hidePossible) { + const tier = this.battle.tier; + if (!text && abilityData.possibilities.length && !hidePossible && + !(tier.includes('Almost Any Ability') || tier.includes('Hackmons') || + tier.includes('Inheritance') || tier.includes('Metronome'))) { text = 'Possible abilities: ' + abilityData.possibilities.join(', '); } return text; } } -type StatsTable = {hp: number, atk: number, def: number, spa: number, spd: number, spe: number}; - -/** - * PokemonSet can be sparse, in which case that entry should be - * inferred from the rest of the set, according to sensible - * defaults. - */ -interface PokemonSet { - /** Defaults to species name (not including forme), like in games */ - name?: string; - species: string; - /** Defaults to no item */ - item?: string; - /** Defaults to no ability (error in Gen 3+) */ - ability?: string; - moves: string[]; - /** Defaults to no nature (error in Gen 3+) */ - nature?: NatureName; - /** Defaults to random legal gender, NOT subject to gender ratios */ - gender?: string; - /** Defaults to flat 252's (200's/0's in Let's Go) (error in gen 3+) */ - evs?: StatsTable; - /** Defaults to whatever makes sense - flat 31's unless you have Gyro Ball etc */ - ivs?: StatsTable; - /** Defaults as you'd expect (100 normally, 50 in VGC-likes, 5 in LC) */ - level?: number; - /** Defaults to no (error if shiny event) */ - shiny?: boolean; - /** Defaults to 255 unless you have Frustration, in which case 0 */ - happiness?: number; - /** Defaults to event required ball, otherwise Poké Ball */ - pokeball?: string; - /** Defaults to the type of your Hidden Power in Moves, otherwise Dark */ - hpType?: string; - /** Defaults to no (can only be yes for certain Pokemon) */ - gigantamax?: boolean; -} - class BattleStatGuesser { formatid: ID; dex: ModdedDex; @@ -1952,25 +2527,25 @@ class BattleStatGuesser { this.dex = formatid ? Dex.mod(formatid.slice(0, 4) as ID) : Dex; this.ignoreEVLimits = ( this.dex.gen < 3 || - this.formatid.endsWith('hackmons') || + ((this.formatid.endsWith('hackmons') || this.formatid.endsWith('bh')) && this.dex.gen !== 6) || this.formatid.includes('metronomebattle') || this.formatid.endsWith('norestrictions') ); this.supportsEVs = !this.formatid.includes('letsgo'); this.supportsAVs = !this.supportsEVs && this.formatid.endsWith('norestrictions'); } - guess(set: PokemonSet) { + guess(set: Dex.PokemonSet) { let role = this.guessRole(set); let comboEVs = this.guessEVs(set, role); let evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; for (let stat in evs) { - evs[stat as StatName] = comboEVs[stat as StatName] || 0; + evs[stat as Dex.StatName] = comboEVs[stat as Dex.StatName] || 0; } let plusStat = comboEVs.plusStat || ''; let minusStat = comboEVs.minusStat || ''; return {role, evs, plusStat, minusStat, moveCount: this.moveCount, hasMove: this.hasMove}; } - guessRole(set: PokemonSet) { + guessRole(set: Dex.PokemonSet) { if (!set) return '?'; if (!set.moves) return '?'; @@ -1996,29 +2571,30 @@ class BattleStatGuesser { }; let hasMove: {[moveid: string]: 1} = {}; let itemid = toID(set.item); - let item = this.dex.getItem(itemid); + let item = this.dex.items.get(itemid); let abilityid = toID(set.ability); - let species = this.dex.getSpecies(set.species || set.name!); - if (item.megaEvolves === species.name) species = this.dex.getSpecies(item.megaStone); + let species = this.dex.species.get(set.species || set.name!); + if (item.megaEvolves === species.name) species = this.dex.species.get(item.megaStone); if (!species.exists) return '?'; let stats = species.baseStats; if (set.moves.length < 1) return '?'; let needsFourMoves = !['unown', 'ditto'].includes(species.id); + let hasFourValidMoves = set.moves.length >= 4 && !set.moves.includes(''); let moveids = set.moves.map(toID); if (moveids.includes('lastresort' as ID)) needsFourMoves = false; - if (set.moves.length < 4 && needsFourMoves && this.formatid !== 'gen8metronomebattle') { + if (!hasFourValidMoves && needsFourMoves && !this.formatid.includes('metronomebattle')) { return '?'; } for (let i = 0, len = set.moves.length; i < len; i++) { - let move = Dex.getMove(set.moves[i]); + let move = this.dex.moves.get(set.moves[i]); hasMove[move.id] = 1; if (move.category === 'Status') { if (['batonpass', 'healingwish', 'lunardance'].includes(move.id)) { moveCount['Support']++; - } else if (['metronome', 'assist', 'copycat', 'mefirst'].includes(move.id)) { + } else if (['metronome', 'assist', 'copycat', 'mefirst', 'photongeyser', 'shellsidearm'].includes(move.id)) { moveCount['Physical'] += 0.5; moveCount['Special'] += 0.5; } else if (move.id === 'naturepower') { @@ -2052,7 +2628,7 @@ class BattleStatGuesser { } else if (['counter', 'endeavor', 'metalburst', 'mirrorcoat', 'rapidspin'].includes(move.id)) { moveCount['Support']++; } else if ([ - 'nightshade', 'seismictoss', 'psywave', 'superfang', 'naturesmadness', 'foulplay', 'endeavor', 'finalgambit', + 'nightshade', 'seismictoss', 'psywave', 'superfang', 'naturesmadness', 'foulplay', 'endeavor', 'finalgambit', 'bodypress', ].includes(move.id)) { moveCount['Offense']++; } else if (move.id === 'fellstinger') { @@ -2064,7 +2640,7 @@ class BattleStatGuesser { if (move.id === 'knockoff') { moveCount['Support']++; } - if (['scald', 'voltswitch', 'uturn'].includes(move.id)) { + if (['scald', 'voltswitch', 'uturn', 'flipturn'].includes(move.id)) { moveCount[move.category] -= 0.2; } } @@ -2248,7 +2824,7 @@ class BattleStatGuesser { if (specialBulk >= physicalBulk) return 'Specially Defensive'; return 'Physically Defensive'; } - ensureMinEVs(evs: StatsTable, stat: StatName, min: number, evTotal: number) { + ensureMinEVs(evs: Dex.StatsTable, stat: Dex.StatName, min: number, evTotal: number) { if (!evs[stat]) evs[stat] = 0; let diff = min - evs[stat]; if (diff <= 0) return evTotal; @@ -2260,7 +2836,7 @@ class BattleStatGuesser { } if (diff <= 0) return evTotal; let evPriority = {def: 1, spd: 1, hp: 1, atk: 1, spa: 1, spe: 1}; - let prioStat: StatName; + let prioStat: Dex.StatName; for (prioStat in evPriority) { if (prioStat === stat) continue; if (evs[prioStat] && evs[prioStat] > 128) { @@ -2271,7 +2847,7 @@ class BattleStatGuesser { } return evTotal; // can't do it :( } - ensureMaxEVs(evs: StatsTable, stat: StatName, min: number, evTotal: number) { + ensureMaxEVs(evs: Dex.StatsTable, stat: Dex.StatName, min: number, evTotal: number) { if (!evs[stat]) evs[stat] = 0; let diff = evs[stat] - min; if (diff <= 0) return evTotal; @@ -2279,22 +2855,24 @@ class BattleStatGuesser { evTotal -= diff; return evTotal; // can't do it :( } - guessEVs(set: PokemonSet, role: string): Partial & {plusStat?: StatName | '', minusStat?: StatName | ''} { + guessEVs( + set: Dex.PokemonSet, role: string + ): Partial & {plusStat?: Dex.StatName | '', minusStat?: Dex.StatName | ''} { if (!set) return {}; if (role === '?') return {}; - let species = this.dex.getSpecies(set.species || set.name!); + let species = this.dex.species.get(set.species || set.name!); let stats = species.baseStats; let hasMove = this.hasMove; let moveCount = this.moveCount; - let evs: StatsTable & {plusStat?: StatName | '', minusStat?: StatName | ''} = { + let evs: Dex.StatsTable & {plusStat?: Dex.StatName | '', minusStat?: Dex.StatName | ''} = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, }; - let plusStat: StatName | '' = ''; - let minusStat: StatName | '' = ''; + let plusStat: Dex.StatName | '' = ''; + let minusStat: Dex.StatName | '' = ''; - let statChart: {[role: string]: [StatName, StatName]} = { + let statChart: {[role: string]: [Dex.StatName, Dex.StatName]} = { 'Bulky Band': ['atk', 'hp'], 'Fast Band': ['spe', 'atk'], 'Bulky Specs': ['spa', 'hp'], @@ -2356,7 +2934,7 @@ class BattleStatGuesser { evs[primaryStat] = ev; evTotal += ev; - let secondaryStat: StatName | null = statChart[role][1]; + let secondaryStat: Dex.StatName | null = statChart[role][1]; if (secondaryStat === 'hp' && set.level && set.level < 20) secondaryStat = 'spd'; stat = this.getStat(secondaryStat, set, 252, plusStat === secondaryStat ? 1.1 : 1.0); ev = 252; @@ -2499,8 +3077,8 @@ class BattleStatGuesser { return evs; } - getStat(stat: StatName, set: PokemonSet, evOverride?: number, natureOverride?: number) { - let species = this.dex.getSpecies(set.species); + getStat(stat: Dex.StatName, set: Dex.PokemonSet, evOverride?: number, natureOverride?: number) { + let species = this.dex.species.get(set.species); if (!species.exists) return 0; let level = set.level || 100; @@ -2539,7 +3117,155 @@ class BattleStatGuesser { } } +function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) { + if (!set.evs) return null; + + const dex = Dex.mod(formatid.slice(0, 4) as ID); + const ignoreEVLimits = ( + dex.gen < 3 || + ((formatid.endsWith('hackmons') || formatid.endsWith('bh')) && dex.gen !== 6) || + formatid.includes('metronomebattle') || formatid.endsWith('norestrictions') + ); + const supportsEVs = !formatid.includes('letsgo'); + if (!supportsEVs || ignoreEVLimits) return false; + + const species = dex.species.get(set.species); + const level = set.level || 100; + const getStat = (stat: Dex.StatNameExceptHP, ev: number, nature: Dex.Nature) => { + const baseStat = species.baseStats[stat]; + const iv = set.ivs?.[stat] || 31; + let val = ~~(~~(2 * baseStat + iv + ~~(ev / 4)) * level / 100 + 5); + if (nature.plus === stat) { + val *= 1.1; + } else if (nature.minus === stat) { + val *= 0.9; + } + return ~~(val); + }; + + const origNature = BattleNatures[set.nature || 'Serious']; + const origStats = { + // no need to calculate hp + atk: getStat('atk', set.evs.atk || 0, origNature), + def: getStat('def', set.evs.def || 0, origNature), + spa: getStat('spa', set.evs.spa || 0, origNature), + spd: getStat('spd', set.evs.spd || 0, origNature), + spe: getStat('spe', set.evs.spe || 0, origNature), + }; + const getMinEVs = (stat: Dex.StatNameExceptHP, nature: Dex.Nature) => { + let ev = 0; + while (getStat(stat, ev, nature) < origStats[stat]) { + ev += 4; + } + return ev; + }; + + const origSpread = {evs: set.evs, ...origNature}; + let origLeftoverEVs = 508; + for (const stat of Dex.statNames) { + origLeftoverEVs -= origSpread.evs?.[stat] || 0; + } + // Only check for optimizations if EVs are completed + if (origLeftoverEVs > 4) return null; + + // Can't move the plus if it boosts its stat past normal EV limit + const plusTooHigh = origNature.plus && getStat(origNature.plus, 252, {}) < origStats[origNature.plus]; + // Can't move the minus if there's no investment in its stat to redistribute + const minusTooLow = origNature.minus && !origSpread.evs?.[origNature.minus]; + // If we can't move either of them, do nothing + if (plusTooHigh && minusTooLow) return null; + + let bestPlus = origNature.plus; + let bestPlusMinEVs = bestPlus && origSpread.evs[bestPlus]; + let bestMinus = origNature.minus || 'atk'; + let bestMinusMinEVs = origSpread.evs[bestMinus]; + let savedEVs = 0; + + // Try and move the minus first, as figuring out where the plus should go is harder if the minus hasn't been placed + if (!minusTooLow) { + for (const stat of Dex.statNamesExceptHP) { + if (origStats[stat] < origStats[bestMinus]) { + const minEVs = getMinEVs(stat, {minus: stat}); + if (minEVs > 252) continue; + // This number can go negative at this point, but we'll make up for it later (and check to make sure) + savedEVs = (origSpread.evs[stat] || 0) - minEVs; + if (origNature.minus) { + savedEVs += (origSpread.evs[origNature.minus] || 0) - getMinEVs(origNature.minus, {minus: stat}); + } + bestMinus = stat; + bestMinusMinEVs = minEVs; + } + } + } + if (!plusTooHigh) { + for (const stat of Dex.statNamesExceptHP) { + // Don't move the plus to an uninvested stat + if (stat !== origNature.plus && origSpread.evs[stat] && stat !== bestMinus) { + const minEVs = getMinEVs(stat, {plus: stat}); + let plusEVsSaved = (origNature.minus === stat ? getMinEVs(stat, {}) : origSpread.evs[stat] || 0) - minEVs; + if (bestPlus && bestPlus !== bestMinus) { + plusEVsSaved += bestPlusMinEVs! - getMinEVs(bestPlus, {plus: stat, minus: bestMinus}); + } + if (plusEVsSaved > 0 && savedEVs + plusEVsSaved > 0) { + savedEVs += plusEVsSaved; + bestPlus = stat; + bestPlusMinEVs = minEVs; + } else if (plusEVsSaved === 0 && (bestPlus || savedEVs > 0) || plusEVsSaved > 0 && savedEVs + plusEVsSaved === 0) { + if (!bestPlus || getStat(stat, getMinEVs(stat, {plus: stat}), {plus: stat}) > origStats[stat]) { + savedEVs += plusEVsSaved; + bestPlus = stat; + bestPlusMinEVs = minEVs; + } + } + } + } + } + + if (bestPlus && savedEVs >= 0) { + const newSpread: { + evs: Partial, + plus?: Dex.StatNameExceptHP, + minus?: Dex.StatNameExceptHP, + } = {evs: {...origSpread.evs}, plus: bestPlus, minus: bestMinus}; + if (bestPlus !== origNature.plus || bestMinus !== origNature.minus) { + newSpread.evs[bestPlus] = bestPlusMinEVs!; + newSpread.evs[bestMinus] = bestMinusMinEVs!; + if (origNature.plus && origNature.plus !== bestPlus && origNature.plus !== bestMinus) { + newSpread.evs[origNature.plus] = getMinEVs(origNature.plus, newSpread); + } + if (origNature.minus && origNature.minus !== bestPlus && origNature.minus !== bestMinus) { + newSpread.evs[origNature.minus] = getMinEVs(origNature.minus, newSpread); + } + for (const stat of Dex.statNames) { + if (!newSpread.evs[stat]) delete newSpread.evs[stat]; + } + return {...newSpread, savedEVs}; + } else if (!plusTooHigh && !minusTooLow) { + if (Math.floor(getStat(bestPlus, bestMinusMinEVs!, newSpread) / 11) <= Math.ceil(origStats[bestMinus] / 9)) { + // We're not gaining more points from our plus than we're losing to our minus + // So a neutral nature would be better + delete newSpread.plus; + delete newSpread.minus; + newSpread.evs[origNature.plus] = getMinEVs(origNature.plus, newSpread); + newSpread.evs[origNature.minus] = getMinEVs(origNature.minus, newSpread); + savedEVs += (origSpread.evs[origNature.plus] || 0) - newSpread.evs[origNature.plus]!; + savedEVs += (origSpread.evs[origNature.minus] || 0) - newSpread.evs[origNature.minus]!; + if (savedEVs < 0) return null; + for (const stat of Dex.statNames) { + if (!newSpread.evs[stat]) delete newSpread.evs[stat]; + } + return {...newSpread, savedEVs}; + } + } + } + + return null; +} + +declare const require: any; +declare const global: any; if (typeof require === 'function') { // in Node (global as any).BattleStatGuesser = BattleStatGuesser; + (global as any).BattleStatOptimizer = BattleStatOptimizer; } diff --git a/src/battle.ts b/play.pokemonshowdown.com/src/battle.ts similarity index 79% rename from src/battle.ts rename to play.pokemonshowdown.com/src/battle.ts index bc58cc38c5..93a9dab076 100644 --- a/src/battle.ts +++ b/play.pokemonshowdown.com/src/battle.ts @@ -27,14 +27,20 @@ * @license MIT */ +// import $ from 'jquery'; +import {BattleSceneStub} from './battle-scene-stub'; +import {BattleLog} from './battle-log'; +import {BattleScene, PokemonSprite, BattleStatusAnims} from './battle-animations'; +import {Dex, Teams, toID, toUserid, type ID, type ModdedDex} from './battle-dex'; +import {BattleTextParser, type Args, type KWArgs, type SideID} from './battle-text-parser'; + /** [id, element?, ...misc] */ -type EffectState = any[] & {0: ID}; +export type EffectState = any[] & {0: ID}; /** [name, minTimeLeft, maxTimeLeft] */ -type WeatherState = [string, number, number]; -type EffectTable = {[effectid: string]: EffectState}; -type HPColor = 'r' | 'y' | 'g'; +export type WeatherState = [string, number, number]; +export type HPColor = 'r' | 'y' | 'g'; -class Pokemon implements PokemonDetails, PokemonHealth { +export class Pokemon implements PokemonDetails, PokemonHealth { name = ''; speciesForme = ''; @@ -76,7 +82,7 @@ class Pokemon implements PokemonDetails, PokemonHealth { hp = 0; maxhp = 1000; level = 100; - gender: GenderName = 'N'; + gender: Dex.GenderName = 'N'; shiny = false; hpcolor: HPColor = 'g'; @@ -87,18 +93,21 @@ class Pokemon implements PokemonDetails, PokemonHealth { itemEffect = ''; prevItem = ''; prevItemEffect = ''; + terastallized: string | '' = ''; + teraType = ''; boosts: {[stat: string]: number} = {}; - status: StatusName | 'tox' | '' | '???' = ''; + status: Dex.StatusName | 'tox' | '' | '???' = ''; statusStage = 0; - volatiles: EffectTable = {}; - turnstatuses: EffectTable = {}; - movestatuses: EffectTable = {}; + volatiles: {[effectid: string]: EffectState} = {}; + turnstatuses: {[effectid: string]: EffectState} = {}; + movestatuses: {[effectid: string]: EffectState} = {}; lastMove = ''; /** [[moveName, ppUsed]] */ moveTrack: [string, number][] = []; statusData = {sleepTurns: 0, toxicTurns: 0}; + timesAttacked = 0; sprite: PokemonSprite; @@ -112,6 +121,7 @@ class Pokemon implements PokemonDetails, PokemonHealth { this.shiny = data.shiny; this.gender = data.gender || 'N'; this.ident = data.ident; + this.terastallized = data.terastallized || ''; this.searchid = data.searchid; this.sprite = side.battle.scene.addPokemonSprite(this); @@ -329,7 +339,7 @@ class Pokemon implements PokemonDetails, PokemonHealth { } rememberMove(moveName: string, pp = 1, recursionSource?: string) { if (recursionSource === this.ident) return; - moveName = Dex.getMove(moveName).name; + moveName = Dex.moves.get(moveName).name; if (moveName.charAt(0) === '*') return; if (moveName === 'Struggle') return; if (this.volatiles.transform) { @@ -348,13 +358,13 @@ class Pokemon implements PokemonDetails, PokemonHealth { this.moveTrack.push([moveName, pp]); } rememberAbility(ability: string, isNotBase?: boolean) { - ability = Dex.getAbility(ability).name; + ability = Dex.abilities.get(ability).name; this.ability = ability; if (!this.baseAbility && !isNotBase) { this.baseAbility = ability; } } - getBoost(boostStat: BoostStatName) { + getBoost(boostStat: Dex.BoostStatName) { let boostStatTable = { atk: 'Atk', def: 'Def', @@ -402,7 +412,7 @@ class Pokemon implements PokemonDetails, PokemonHealth { let autotomizeFactor = this.volatiles.autotomize?.[1] * 100 || 0; return Math.max(this.getSpecies(serverPokemon).weightkg - autotomizeFactor, 0.1); } - getBoostType(boostStat: BoostStatName) { + getBoostType(boostStat: Dex.BoostStatName) { if (!this.boosts[boostStat]) return 'neutral'; if (this.boosts[boostStat] > 0) return 'good'; return 'bad'; @@ -425,31 +435,30 @@ class Pokemon implements PokemonDetails, PokemonHealth { /** * copyAll = false means Baton Pass, * copyAll = true means Illusion breaking + * copyAll = 'shedtail' means Shed Tail */ - copyVolatileFrom(pokemon: Pokemon, copyAll?: boolean) { + copyVolatileFrom(pokemon: Pokemon, copySource?: | 'shedtail' | boolean) { this.boosts = pokemon.boosts; this.volatiles = pokemon.volatiles; // this.lastMove = pokemon.lastMove; // I think - if (!copyAll) { - delete this.volatiles['airballoon']; - delete this.volatiles['attract']; - delete this.volatiles['autotomize']; - delete this.volatiles['disable']; - delete this.volatiles['encore']; - delete this.volatiles['foresight']; - delete this.volatiles['imprison']; - delete this.volatiles['laserfocus']; - delete this.volatiles['mimic']; - delete this.volatiles['miracleeye']; - delete this.volatiles['nightmare']; - delete this.volatiles['smackdown']; - delete this.volatiles['stockpile1']; - delete this.volatiles['stockpile2']; - delete this.volatiles['stockpile3']; - delete this.volatiles['torment']; - delete this.volatiles['typeadd']; - delete this.volatiles['typechange']; - delete this.volatiles['yawn']; + if (!copySource) { + const volatilesToRemove = [ + 'airballoon', 'attract', 'autotomize', 'disable', 'encore', 'foresight', 'gmaxchistrike', 'imprison', 'laserfocus', 'mimic', 'miracleeye', 'nightmare', 'saltcure', 'smackdown', 'stockpile1', 'stockpile2', 'stockpile3', 'syrupbomb', 'torment', 'typeadd', 'typechange', 'yawn', + ]; + for (const statName of Dex.statNamesExceptHP) { + volatilesToRemove.push('protosynthesis' + statName); + volatilesToRemove.push('quarkdrive' + statName); + } + for (const volatile of volatilesToRemove) { + delete this.volatiles[volatile]; + } + } + if (copySource === 'shedtail') { + for (let i in this.volatiles) { + if (i === 'substitute') continue; + delete this.volatiles[i]; + } + this.boosts = {}; } delete this.volatiles['transform']; delete this.volatiles['formechange']; @@ -459,8 +468,8 @@ class Pokemon implements PokemonDetails, PokemonHealth { pokemon.side.battle.scene.removeTransform(pokemon); pokemon.statusStage = 0; } - copyTypesFrom(pokemon: Pokemon) { - const [types, addedType] = pokemon.getTypes(); + copyTypesFrom(pokemon: Pokemon, preterastallized = false) { + const [types, addedType] = pokemon.getTypes(undefined, preterastallized); this.addVolatile('typechange' as ID, types.join('/')); if (addedType) { this.addVolatile('typeadd' as ID, addedType); @@ -468,9 +477,11 @@ class Pokemon implements PokemonDetails, PokemonHealth { this.removeVolatile('typeadd' as ID); } } - getTypes(serverPokemon?: ServerPokemon): [ReadonlyArray, TypeName | ''] { - let types: ReadonlyArray; - if (this.volatiles.typechange) { + getTypes(serverPokemon?: ServerPokemon, preterastallized = false): [ReadonlyArray, Dex.TypeName | ''] { + let types: ReadonlyArray; + if (!preterastallized && this.terastallized && this.terastallized !== 'Stellar') { + types = [this.terastallized as Dex.TypeName]; + } else if (this.volatiles.typechange) { types = this.volatiles.typechange[1].split('/'); } else { types = this.getSpecies(serverPokemon).types; @@ -493,7 +504,7 @@ class Pokemon implements PokemonDetails, PokemonHealth { } let item = toID(serverPokemon ? serverPokemon.item : this.item); - let ability = toID(this.ability || serverPokemon?.ability); + let ability = toID(this.effectiveAbility(serverPokemon)); if (battle.hasPseudoWeather('Magic Room') || this.volatiles['embargo'] || ability === 'klutz') { item = '' as ID; } @@ -512,8 +523,21 @@ class Pokemon implements PokemonDetails, PokemonHealth { } return !this.getTypeList(serverPokemon).includes('Flying'); } - getTypeList(serverPokemon?: ServerPokemon) { - const [types, addedType] = this.getTypes(serverPokemon); + effectiveAbility(serverPokemon?: ServerPokemon) { + const ability = this.side.battle.dex.abilities.get( + serverPokemon?.ability || this.ability || serverPokemon?.baseAbility || '' + ); + if ( + this.fainted || + (this.volatiles['transform'] && ability.flags['notransform']) || + (!ability.flags['cantsuppress'] && (this.side.battle.ngasActive() || this.volatiles['gastroacid'])) + ) { + return ''; + } + return ability.name; + } + getTypeList(serverPokemon?: ServerPokemon, preterastallized = false) { + const [types, addedType] = this.getTypes(serverPokemon, preterastallized); return addedType ? types.concat(addedType) : types; } getSpeciesForme(serverPokemon?: ServerPokemon): string { @@ -521,10 +545,10 @@ class Pokemon implements PokemonDetails, PokemonHealth { (serverPokemon ? serverPokemon.speciesForme : this.speciesForme); } getSpecies(serverPokemon?: ServerPokemon) { - return this.side.battle.dex.getSpecies(this.getSpeciesForme(serverPokemon)); + return this.side.battle.dex.species.get(this.getSpeciesForme(serverPokemon)); } getBaseSpecies() { - return this.side.battle.dex.getSpecies(this.speciesForme); + return this.side.battle.dex.species.get(this.speciesForme); } reset() { this.clearVolatile(); @@ -560,7 +584,11 @@ class Pokemon implements PokemonDetails, PokemonHealth { } return percentage * maxWidth / 100; } - static getHPText(pokemon: PokemonHealth, precision = 1) { + getHPText(precision = 1) { + return Pokemon.getHPText(this, this.side.battle.reportExactHP, precision); + } + static getHPText(pokemon: PokemonHealth, exactHP: boolean, precision = 1) { + if (exactHP) return pokemon.hp + '/' + pokemon.maxhp; if (pokemon.maxhp === 100) return pokemon.hp + '%'; if (pokemon.maxhp !== 48) return (100 * pokemon.hp / pokemon.maxhp).toFixed(precision) + '%'; let range = Pokemon.getPixelRange(pokemon.hp, pokemon.hpcolor); @@ -573,14 +601,17 @@ class Pokemon implements PokemonDetails, PokemonHealth { } } -class Side { +export class Side { battle: Battle; name = ''; id = ''; + sideid: SideID; n: number; isFar: boolean; foe: Side = null!; + ally: Side | null = null; avatar: string = 'unknown'; + badges: string[] = []; rating: string = ''; totalPokemon = 6; x = 0; @@ -596,12 +627,13 @@ class Side { /** [effectName, levels, minDuration, maxDuration] */ sideConditions: {[id: string]: [string, number, number, number]} = {}; + faintCounter = 0; - constructor(battle: Battle, n: number, isOpp?: boolean) { + constructor(battle: Battle, n: number) { this.battle = battle; this.n = n; - this.isFar = isOpp || !!n; - this.updateSprites(); + this.sideid = ['p1', 'p2', 'p3', 'p4'][n] as SideID; + this.isFar = !!(n % 2); } rollTrainerSprites() { @@ -630,12 +662,8 @@ class Side { } reset() { this.clearPokemon(); - this.updateSprites(); this.sideConditions = {}; - } - updateSprites() { - this.z = (this.isFar ? 200 : 0); - this.battle.scene.updateSpritesForSide(this); + this.faintCounter = 0; } setAvatar(avatar: string) { this.avatar = avatar; @@ -650,7 +678,7 @@ class Side { if (this.foe && this.avatar === this.foe.avatar) this.rollTrainerSprites(); } } - addSideCondition(effect: Effect) { + addSideCondition(effect: Dex.Effect, persist: boolean) { let condition = effect.id; if (this.sideConditions[condition]) { if (condition === 'spikes' || condition === 'toxicspikes') { @@ -668,7 +696,7 @@ class Side { this.sideConditions[condition] = [effect.name, 1, 5, this.battle.gen >= 4 ? 8 : 0]; break; case 'safeguard': - this.sideConditions[condition] = [effect.name, 1, 5, 0]; + this.sideConditions[condition] = [effect.name, 1, persist ? 7 : 5, 0]; break; case 'lightscreen': this.sideConditions[condition] = [effect.name, 1, 5, this.battle.gen >= 4 ? 8 : 0]; @@ -677,7 +705,7 @@ class Side { this.sideConditions[condition] = [effect.name, 1, 5, 0]; break; case 'tailwind': - this.sideConditions[condition] = [effect.name, 1, this.battle.gen >= 5 ? 4 : 3, 0]; + this.sideConditions[condition] = [effect.name, 1, this.battle.gen >= 5 ? persist ? 6 : 4 : persist ? 5 : 3, 0]; break; case 'luckychant': this.sideConditions[condition] = [effect.name, 1, 5, 0]; @@ -716,14 +744,19 @@ class Side { this.battle.scene.removeSideCondition(this.n, id); } addPokemon(name: string, ident: string, details: string, replaceSlot = -1) { - const oldItem = replaceSlot >= 0 ? this.pokemon[replaceSlot].item : undefined; + const oldPokemon = replaceSlot >= 0 ? this.pokemon[replaceSlot] : undefined; const data = this.battle.parseDetails(name, ident, details); const poke = new Pokemon(data, this); - if (oldItem) poke.item = oldItem; + if (oldPokemon) { + poke.item = oldPokemon.item; + poke.baseAbility = oldPokemon.baseAbility; + poke.teraType = oldPokemon.teraType; + } if (!poke.ability && poke.baseAbility) poke.ability = poke.baseAbility; poke.reset(); + if (oldPokemon?.moveTrack.length) poke.moveTrack = oldPokemon.moveTrack; if (replaceSlot >= 0) { this.pokemon[replaceSlot] = poke; @@ -797,15 +830,15 @@ class Side { return poke; } - switchIn(pokemon: Pokemon, slot?: number) { - if (slot === undefined) slot = pokemon.slot; + switchIn(pokemon: Pokemon, kwArgs: KWArgs, slot = pokemon.slot) { this.active[slot] = pokemon; pokemon.slot = slot; pokemon.clearVolatile(); pokemon.lastMove = ''; this.battle.lastMove = 'switch-in'; - if (['batonpass', 'zbatonpass'].includes(this.lastPokemon?.lastMove!)) { - pokemon.copyVolatileFrom(this.lastPokemon!); + const effect = Dex.getEffect(kwArgs.from); + if (['batonpass', 'zbatonpass', 'shedtail'].includes(effect.id)) { + pokemon.copyVolatileFrom(this.lastPokemon!, effect.id === 'shedtail' ? 'shedtail' : false); } this.battle.scene.animSummon(pokemon, slot); @@ -839,6 +872,12 @@ class Side { pokemon.status = oldpokemon.status; pokemon.copyVolatileFrom(oldpokemon, true); pokemon.statusData = {...oldpokemon.statusData}; + if (oldpokemon.terastallized) { + pokemon.terastallized = oldpokemon.terastallized; + pokemon.teraType = oldpokemon.terastallized; + oldpokemon.terastallized = ''; + oldpokemon.teraType = ''; + } // we don't know anything about the illusioned pokemon except that it's not fainted // technically we also know its status but only at the end of the turn, not here oldpokemon.fainted = false; @@ -853,17 +892,16 @@ class Side { } this.battle.scene.animSummon(pokemon, slot, true); } - switchOut(pokemon: Pokemon, slot = pokemon.slot) { - if (pokemon.lastMove !== 'batonpass' && pokemon.lastMove !== 'zbatonpass') { + switchOut(pokemon: Pokemon, kwArgs: KWArgs, slot = pokemon.slot) { + const effect = Dex.getEffect(kwArgs.from); + if (!['batonpass', 'zbatonpass', 'shedtail'].includes(effect.id)) { pokemon.clearVolatile(); } else { pokemon.removeVolatile('transform' as ID); pokemon.removeVolatile('formechange' as ID); } - if (pokemon.lastMove === 'uturn' || pokemon.lastMove === 'voltswitch') { - this.battle.log(['switchout', pokemon.ident], {from: pokemon.lastMove}); - } else if (pokemon.lastMove !== 'batonpass' && pokemon.lastMove !== 'zbatonpass') { - this.battle.log(['switchout', pokemon.ident]); + if (!['batonpass', 'zbatonpass', 'shedtail', 'teleport'].includes(effect.id)) { + this.battle.log(['switchout', pokemon.ident], {from: effect.id}); } pokemon.statusData.toxicTurns = 0; if (this.battle.gen === 5) pokemon.statusData.sleepTurns = 0; @@ -872,7 +910,7 @@ class Side { this.battle.scene.animUnsummon(pokemon); } - swapTo(pokemon: Pokemon, slot: number, kwArgs: KWArgs) { + swapTo(pokemon: Pokemon, slot: number) { if (pokemon.slot === slot) return; let target = this.active[slot]; @@ -915,6 +953,10 @@ class Side { pokemon.fainted = true; pokemon.hp = 0; + pokemon.terastallized = ''; + pokemon.details = pokemon.details.replace(/, tera:[a-z]+/i, ''); + pokemon.searchid = pokemon.searchid.replace(/, tera:[a-z]+/i, ''); + if (pokemon.side.faintCounter < 100) pokemon.side.faintCounter++; this.battle.scene.animFaint(pokemon); } @@ -925,24 +967,25 @@ class Side { } } -interface PokemonDetails { +export interface PokemonDetails { details: string; name: string; speciesForme: string; level: number; shiny: boolean; - gender: GenderName | ''; + gender: Dex.GenderName | ''; ident: string; + terastallized: string; searchid: string; } -interface PokemonHealth { +export interface PokemonHealth { hp: number; maxhp: number; hpcolor: HPColor | ''; - status: StatusName | 'tox' | '' | '???'; + status: Dex.StatusName | 'tox' | '' | '???'; fainted?: boolean; } -interface ServerPokemon extends PokemonDetails, PokemonHealth { +export interface ServerPokemon extends PokemonDetails, PokemonHealth { ident: string; details: string; condition: string; @@ -967,12 +1010,16 @@ interface ServerPokemon extends PokemonDetails, PokemonHealth { pokeball: string; /** false if the pokemon cannot gigantamax, otherwise a string containing the full name of its G-max move */ gigantamax: string | false; + /** always the Tera Type of the Pokemon, regardless of whether it is terastallized or not */ + teraType: string; + /** falsy if the pokemon is not terastallized, otherwise it is the Tera Type of the Pokemon */ + terastallized: string; } -class Battle { - scene: BattleScene | BattleSceneStub; +export class Battle { + scene: BattleSceneStub; - sidesSwitched = false; + viewpointSwitched = false; stepQueue: string[]; /** See battle.instantAdd */ @@ -1026,13 +1073,22 @@ class Battle { pseudoWeather = [] as WeatherState[]; weatherTimeLeft = 0; weatherMinTimeLeft = 0; + /** + * The side from which perspective we're viewing. Should be identical to + * `nearSide` except in multi battles, where `nearSide` is always the first + * near side, and `mySide` is the active player. + */ mySide: Side = null!; nearSide: Side = null!; farSide: Side = null!; p1: Side = null!; p2: Side = null!; + p3?: Side = null!; + p4?: Side = null!; + pokemonControlled = 0; + sides: Side[] = null!; myPokemon: ServerPokemon[] | null = null; - sides: [Side, Side] = [null!, null!]; + myAllyPokemon: ServerPokemon[] | null = null; lastMove = ''; gen = 8; @@ -1040,9 +1096,12 @@ class Battle { teamPreviewCount = 0; speciesClause = false; tier = ''; - gameType: 'singles' | 'doubles' | 'triples' = 'singles'; + gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' = 'singles'; + compatMode = true; rated: string | boolean = false; + rules: {[ruleName: string]: 1 | 0} = {}; isBlitz = false; + reportExactHP = false; endLastTurnPending = false; totalTimeLeft = 0; graceTimeLeft = 0; @@ -1063,6 +1122,7 @@ class Battle { ignoreSpects = !!Dex.prefs('ignorespects'); debug: boolean; joinButtons = false; + autoresize: boolean; /** * The actual pause state. Will only be true if playback is actually @@ -1074,11 +1134,13 @@ class Battle { $frame?: JQuery, $logFrame?: JQuery, id?: ID, - log?: string[], + log?: string[] | string, paused?: boolean, isReplay?: boolean, debug?: boolean, subscription?: Battle['subscription'], + /** autoresize `$frame` for browsers below 640px width (mobile) */ + autoresize?: boolean, } = {}) { this.id = options.id || ''; @@ -1093,8 +1155,10 @@ class Battle { this.paused = !!options.paused; this.started = !this.paused; this.debug = !!options.debug; + if (typeof options.log === 'string') options.log = options.log.split('\n'); this.stepQueue = options.log || []; this.subscription = options.subscription || null; + this.autoresize = !!options.autoresize; this.p1 = new Side(this, 0); this.p2 = new Side(this, 1); @@ -1105,8 +1169,32 @@ class Battle { this.farSide = this.p2; this.resetStep(); + if (this.autoresize) { + window.addEventListener('resize', this.onResize); + this.onResize(); + } } + onResize = () => { + const width = $(window).width()!; + if (width < 950 || this.hardcoreMode) { + this.messageShownTime = 500; + } else { + this.messageShownTime = 1; + } + if (width && width < 640) { + const scale = (width / 640); + this.scene.$frame?.css('transform', 'scale(' + scale + ')'); + this.scene.$frame?.css('transform-origin', 'top left'); + this.scene.$frame?.css('margin-bottom', '' + (360 * scale - 360) + 'px'); + // this.$foeHint.css('transform', 'scale(' + scale + ')'); + } else { + this.scene.$frame?.css('transform', 'none'); + // this.$foeHint.css('transform', 'none'); + this.scene.$frame?.css('margin-bottom', '0'); + } + }; + subscribe(listener: Battle['subscription']) { this.subscription = listener; } @@ -1132,6 +1220,38 @@ class Battle { } return false; } + getAllActive() { + const pokemonList = []; + // Sides 3 and 4 are synced with sides 1 and 2, so they don't need to be checked + for (let i = 0; i < 2; i++) { + const side = this.sides[i]; + for (const active of side.active) { + if (active && !active.fainted) { + pokemonList.push(active); + } + } + } + return pokemonList; + } + // Used in Pokemon#effectiveAbility over abilityActive to prevent infinite recursion + ngasActive() { + for (const active of this.getAllActive()) { + if (active.ability === 'Neutralizing Gas' && !active.volatiles['gastroacid']) { + return true; + } + } + return false; + } + abilityActive(abilities: string | string[]) { + if (typeof abilities === 'string') abilities = [abilities]; + abilities = abilities.map(toID); + for (const active of this.getAllActive()) { + if (abilities.includes(toID(active.effectiveAbility()))) { + return true; + } + } + return false; + } reset() { this.paused = true; this.scene.pause(); @@ -1154,6 +1274,7 @@ class Battle { if (side) side.reset(); } this.myPokemon = null; + this.myAllyPokemon = null; // DOM state this.scene.reset(); @@ -1165,6 +1286,9 @@ class Battle { this.nextStep(); } destroy() { + if (this.autoresize) { + window.removeEventListener('resize', this.onResize); + } this.scene.destroy(); for (let i = 0; i < this.sides.length; i++) { @@ -1176,6 +1300,8 @@ class Battle { this.farSide = null!; this.p1 = null!; this.p2 = null!; + this.p3 = null!; + this.p4 = null!; } log(args: Args, kwArgs?: KWArgs, preempt?: boolean) { @@ -1185,23 +1311,33 @@ class Battle { resetToCurrentTurn() { this.seekTurn(this.ended ? Infinity : this.turn, true); } - switchSides() { - this.setSidesSwitched(!this.sidesSwitched); - this.resetToCurrentTurn(); + switchViewpoint() { + this.setViewpoint(this.viewpointSwitched ? 'p1' : 'p2'); } - setSidesSwitched(sidesSwitched: boolean) { - this.sidesSwitched = sidesSwitched; - if (this.sidesSwitched) { - this.nearSide = this.mySide = this.p2; - this.farSide = this.p1; - } else { - this.nearSide = this.mySide = this.p1; + setViewpoint(sideid: SideID) { + if (this.mySide.sideid === sideid) return; + if (sideid.length !== 2 || !sideid.startsWith('p')) return; + const side = this[sideid]; + if (!side) return; + this.mySide = side; + + if ((side.n % 2) === this.p1.n) { + this.viewpointSwitched = false; + this.nearSide = this.p1; this.farSide = this.p2; + } else { + this.viewpointSwitched = true; + this.nearSide = this.p2; + this.farSide = this.p1; } this.nearSide.isFar = false; this.farSide.isFar = true; + if (this.sides.length > 2) { + this.sides[this.nearSide.n + 2].isFar = false; + this.sides[this.farSide.n + 2].isFar = true; + } - // nothing else should need updating - don't call this function after sending out pokemon + this.resetToCurrentTurn(); } // @@ -1256,7 +1392,7 @@ class Battle { this.turnsSinceMoved = 0; this.scene.updateAcceleration(); } - changeWeather(weatherName: string, poke?: Pokemon, isUpkeep?: boolean, ability?: Effect) { + changeWeather(weatherName: string, poke?: Pokemon, isUpkeep?: boolean, ability?: Dex.Effect) { let weather = toID(weatherName); if (!weather || weather === 'none') { weather = '' as ID; @@ -1290,6 +1426,35 @@ class Battle { this.weather = weather; this.scene.updateWeather(); } + swapSideConditions() { + const sideConditions = [ + 'mist', 'lightscreen', 'reflect', 'spikes', 'safeguard', 'tailwind', 'toxicspikes', 'stealthrock', 'waterpledge', 'firepledge', 'grasspledge', 'stickyweb', 'auroraveil', 'gmaxsteelsurge', 'gmaxcannonade', 'gmaxvinelash', 'gmaxwildfire', + ]; + if (this.gameType === 'freeforall') { + // TODO: Add FFA support + return; + } else { + let side1 = this.sides[0]; + let side2 = this.sides[1]; + for (const id of sideConditions) { + if (side1.sideConditions[id] && side2.sideConditions[id]) { + [side1.sideConditions[id], side2.sideConditions[id]] = [ + side2.sideConditions[id], side1.sideConditions[id], + ]; + this.scene.addSideCondition(side1.n, id as ID); + this.scene.addSideCondition(side2.n, id as ID); + } else if (side1.sideConditions[id] && !side2.sideConditions[id]) { + side2.sideConditions[id] = side1.sideConditions[id]; + this.scene.addSideCondition(side2.n, id as ID); + side1.removeSideCondition(id); + } else if (side2.sideConditions[id] && !side1.sideConditions[id]) { + side1.sideConditions[id] = side2.sideConditions[id]; + this.scene.addSideCondition(side1.n, id as ID); + side2.removeSideCondition(id); + } + } + } + } updateTurnCounters() { for (const pWeather of this.pseudoWeather) { if (pWeather[1]) pWeather[1]--; @@ -1301,16 +1466,16 @@ class Battle { if (cond[2]) cond[2]--; if (cond[3]) cond[3]--; } - for (const poke of side.active) { - if (poke) { - if (poke.status === 'tox') poke.statusData.toxicTurns++; - poke.clearTurnstatuses(); - } + } + for (const poke of [...this.nearSide.active, ...this.farSide.active]) { + if (poke) { + if (poke.status === 'tox') poke.statusData.toxicTurns++; + poke.clearTurnstatuses(); } } this.scene.updateWeather(); } - useMove(pokemon: Pokemon, move: Move, target: Pokemon | null, kwArgs: KWArgs) { + useMove(pokemon: Pokemon, move: Dex.Move, target: Pokemon | null, kwArgs: KWArgs) { let fromeffect = Dex.getEffect(kwArgs.from); this.activateAbility(pokemon, fromeffect); pokemon.clearMovestatuses(); @@ -1320,32 +1485,63 @@ class Battle { this.scene.updateStatbar(pokemon); if (fromeffect.id === 'sleeptalk') { pokemon.rememberMove(move.name, 0); - } else if (!fromeffect.id || fromeffect.id === 'pursuit') { + } + let callerMoveForPressure = null; + // will not include effects that are conditions named after moves like Magic Coat and Snatch, which is good + if (fromeffect.id && kwArgs.from.startsWith("move:")) { + callerMoveForPressure = fromeffect as Dex.Move; + } + if (!fromeffect.id || callerMoveForPressure || fromeffect.id === 'pursuit') { let moveName = move.name; - if (move.isZ) { - pokemon.item = move.isZ; - let item = Dex.getItem(move.isZ); - if (item.zMoveFrom) moveName = item.zMoveFrom; - } else if (move.name.slice(0, 2) === 'Z-') { - moveName = moveName.slice(2); - move = Dex.getMove(moveName); - if (window.BattleItems) { - for (let item in BattleItems) { - if (BattleItems[item].zMoveType === move.type) pokemon.item = item; + if (!callerMoveForPressure) { + if (move.isZ) { + pokemon.item = move.isZ; + let item = Dex.items.get(move.isZ); + if (item.zMoveFrom) moveName = item.zMoveFrom; + } else if (move.name.slice(0, 2) === 'Z-') { + moveName = moveName.slice(2); + move = Dex.moves.get(moveName); + if (window.BattleItems) { + for (let item in BattleItems) { + if (BattleItems[item].zMoveType === move.type) pokemon.item = item; + } } } } let pp = 1; - if (move.target === "all") { - for (const active of pokemon.side.foe.active) { - if (active && toID(active.ability) === 'pressure') { + if (this.abilityActive('Pressure') && move.id !== 'stickyweb') { + const foeTargets = []; + const moveTarget = move.pressureTarget; + + if ( + !target && this.gameType === 'singles' && + !['self', 'allies', 'allySide', 'adjacentAlly', 'adjacentAllyOrSelf', 'allyTeam'].includes(moveTarget) + ) { + // Hardcode for moves without a target in singles + foeTargets.push(pokemon.side.foe.active[0]); + } else if (['all', 'allAdjacent', 'allAdjacentFoes', 'foeSide'].includes(moveTarget)) { + for (const active of this.getAllActive()) { + if (active === pokemon) continue; + // Pressure affects allies in gen 3 and 4 + if (this.gen <= 4 || (active.side !== pokemon.side && active.side.ally !== pokemon.side)) { + foeTargets.push(active); + } + } + } else if (target && target.side !== pokemon.side) { + foeTargets.push(target); + } + + for (const foe of foeTargets) { + if (foe && !foe.fainted && foe.effectiveAbility() === 'Pressure') { pp += 1; } } - } else if (target && target.side !== pokemon.side && toID(target.ability) === 'pressure') { - pp += 1; } - pokemon.rememberMove(moveName, pp); + if (!callerMoveForPressure) { + pokemon.rememberMove(moveName, pp); + } else { + pokemon.rememberMove(callerMoveForPressure.name, pp - 1); // 1 pp was already deducted from using the move itself + } } pokemon.lastMove = move.id; this.lastMove = move.id; @@ -1353,7 +1549,7 @@ class Battle { pokemon.side.wisher = pokemon; } } - animateMove(pokemon: Pokemon, move: Move, target: Pokemon | null, kwArgs: KWArgs) { + animateMove(pokemon: Pokemon, move: Dex.Move, target: Pokemon | null, kwArgs: KWArgs) { this.activeMoveIsSpread = kwArgs.spread; if (this.seeking !== null || kwArgs.still) return; @@ -1371,7 +1567,7 @@ class Battle { return; } - let usedMove = kwArgs.anim ? Dex.getMove(kwArgs.anim) : move; + let usedMove = kwArgs.anim ? Dex.moves.get(kwArgs.anim) : move; if (!kwArgs.spread) { this.scene.runMoveAnim(usedMove.id, [pokemon, target]); return; @@ -1394,7 +1590,7 @@ class Battle { this.scene.runMoveAnim(usedMove.id, targets); } - cantUseMove(pokemon: Pokemon, effect: Effect, move: Move, kwArgs: KWArgs) { + cantUseMove(pokemon: Pokemon, effect: Dex.Effect, move: Dex.Move, kwArgs: KWArgs) { pokemon.clearMovestatuses(); this.scene.updateStatbar(pokemon); if (effect.id in BattleStatusAnims) { @@ -1439,7 +1635,7 @@ class Battle { this.scene.animReset(pokemon); } - activateAbility(pokemon: Pokemon | null, effectOrName: Effect | string, isNotBase?: boolean) { + activateAbility(pokemon: Pokemon | null, effectOrName: Dex.Effect | string, isNotBase?: boolean) { if (!pokemon || !effectOrName) return; if (typeof effectOrName !== 'string') { if (effectOrName.effectType !== 'Ability') return; @@ -1532,6 +1728,9 @@ class Battle { break; } } else { + if (this.dex.moves.get(this.lastMove).category !== 'Status') { + poke.timesAttacked++; + } let damageinfo = '' + Pokemon.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '\u2013'); if (damage[1] !== 100) { let hover = '' + ((damage[0] < 0) ? '\u2212' : '') + @@ -1549,14 +1748,15 @@ class Battle { break; } case '-heal': { - let poke = this.getPokemon(args[1])!; + let poke = this.getPokemon(args[1], Dex.getEffect(kwArgs.from).id === 'revivalblessing')!; let damage = poke.healthParse(args[2], true, true); if (damage === null) break; let range = poke.getDamageRange(damage); if (kwArgs.from) { let effect = Dex.getEffect(kwArgs.from); - this.activateAbility(poke, effect); + let ofpoke = this.getPokemon(kwArgs.of); + this.activateAbility(ofpoke || poke, effect); if (effect.effectType === 'Item' && !CONSUMED.includes(poke.prevItemEffect)) { if (poke.prevItem !== effect.name) { poke.item = effect.name; @@ -1572,10 +1772,20 @@ class Battle { this.lastMove = 'healing-wish'; this.scene.runResidualAnim('healingwish' as ID, poke); poke.side.wisher = null; + poke.statusData.sleepTurns = 0; + poke.statusData.toxicTurns = 0; break; case 'wish': this.scene.runResidualAnim('wish' as ID, poke); break; + case 'revivalblessing': + this.scene.runResidualAnim('wish' as ID, poke); + const {siden} = this.parsePokemonId(args[1]); + const side = this.sides[siden]; + poke.fainted = false; + poke.status = ''; + this.scene.updateSidebar(side); + break; } } this.scene.runOtherAnim('heal' as ID, [poke]); @@ -1603,7 +1813,7 @@ class Battle { } case '-boost': { let poke = this.getPokemon(args[1])!; - let stat = args[2] as BoostStatName; + let stat = args[2] as Dex.BoostStatName; if (this.gen === 1 && stat === 'spd') break; if (this.gen === 1 && stat === 'spa') stat = 'spc'; let amount = parseInt(args[3], 10); @@ -1630,7 +1840,7 @@ class Battle { } case '-unboost': { let poke = this.getPokemon(args[1])!; - let stat = args[2] as BoostStatName; + let stat = args[2] as Dex.BoostStatName; if (this.gen === 1 && stat === 'spd') break; if (this.gen === 1 && stat === 'spa') stat = 'spc'; let amount = parseInt(args[3], 10); @@ -1655,7 +1865,7 @@ class Battle { } case '-setboost': { let poke = this.getPokemon(args[1])!; - let stat = args[2] as BoostStatName; + let stat = args[2] as Dex.BoostStatName; let amount = parseInt(args[3], 10); poke.boosts[stat] = amount; this.scene.resultAnim(poke, poke.getBoost(stat), (amount > 0 ? 'good' : 'bad')); @@ -1712,13 +1922,17 @@ class Battle { case '-copyboost': { let poke = this.getPokemon(args[1])!; let frompoke = this.getPokemon(args[2])!; + if (!kwArgs.silent && kwArgs.from) { + let effect = Dex.getEffect(kwArgs.from); + this.activateAbility(poke, effect); + } let stats = args[3] ? args[3].split(', ') : ['atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; for (const stat of stats) { poke.boosts[stat] = frompoke.boosts[stat]; if (!poke.boosts[stat]) delete poke.boosts[stat]; } if (this.gen >= 6) { - const volatilesToCopy = ['focusenergy', 'laserfocus']; + const volatilesToCopy = ['focusenergy', 'gmaxchistrike', 'laserfocus']; for (const volatile of volatilesToCopy) { if (frompoke.volatiles[volatile]) { poke.addVolatile(volatile as ID); @@ -1757,14 +1971,10 @@ class Battle { } case '-clearallboost': { let timeOffset = this.scene.timeOffset; - for (const side of this.sides) { - for (const active of side.active) { - if (active) { - active.boosts = {}; - this.scene.timeOffset = timeOffset; - this.scene.resultAnim(active, 'Stats reset', 'neutral'); - } - } + for (const active of this.getAllActive()) { + active.boosts = {}; + this.scene.timeOffset = timeOffset; + this.scene.resultAnim(active, 'Stats reset', 'neutral'); } this.log(args, kwArgs); @@ -1817,7 +2027,11 @@ class Battle { let effect = Dex.getEffect(args[2]); let fromeffect = Dex.getEffect(kwArgs.from); let ofpoke = this.getPokemon(kwArgs.of); - this.activateAbility(ofpoke || poke, fromeffect); + if (fromeffect.id === 'clearamulet') { + ofpoke!.item = 'Clear Amulet'; + } else { + this.activateAbility(ofpoke || poke, fromeffect); + } switch (effect.id) { case 'brn': this.scene.resultAnim(poke, 'Already burned', 'neutral'); @@ -1881,6 +2095,9 @@ class Battle { case 'protectivepads': poke.item = 'Protective Pads'; break; + case 'abilityshield': + poke.item = 'Ability Shield'; + break; } this.log(args, kwArgs); break; @@ -1914,8 +2131,7 @@ class Battle { let poke = this.getPokemon(args[1])!; let effect = Dex.getEffect(kwArgs.from); let ofpoke = this.getPokemon(kwArgs.of) || poke; - poke.status = args[2] as StatusName; - poke.removeVolatile('yawn' as ID); + poke.status = args[2] as Dex.StatusName; this.activateAbility(ofpoke || poke, effect); if (effect.effectType === 'Item') { ofpoke.item = effect.name; @@ -2015,9 +2231,23 @@ class Battle { } case '-item': { let poke = this.getPokemon(args[1])!; - let item = Dex.getItem(args[2]); + let item = Dex.items.get(args[2]); let effect = Dex.getEffect(kwArgs.from); let ofpoke = this.getPokemon(kwArgs.of); + if (!poke) { + if (effect.id === 'frisk') { + const possibleTargets = ofpoke!.side.foe.active.filter(p => p !== null); + if (possibleTargets.length === 1) { + poke = possibleTargets[0]!; + } else { + this.activateAbility(ofpoke!, "Frisk"); + this.log(args, kwArgs); + break; + } + } else { + throw new Error('No Pokemon in -item message'); + } + } poke.item = item.name; poke.itemEffect = ''; poke.removeVolatile('airballoon' as ID); @@ -2083,12 +2313,14 @@ class Battle { } case '-enditem': { let poke = this.getPokemon(args[1])!; - let item = Dex.getItem(args[2]); + let item = Dex.items.get(args[2]); let effect = Dex.getEffect(kwArgs.from); - poke.item = ''; - poke.itemEffect = ''; - poke.prevItem = item.name; - poke.prevItemEffect = ''; + if (this.gen > 4 || effect.id !== 'knockoff') { + poke.item = ''; + poke.itemEffect = ''; + poke.prevItem = item.name; + poke.prevItemEffect = ''; + } poke.removeVolatile('airballoon' as ID); poke.addVolatile('itemremoved' as ID); if (kwArgs.eat) { @@ -2104,7 +2336,11 @@ class Battle { poke.prevItemEffect = 'flung'; break; case 'knockoff': - poke.prevItemEffect = 'knocked off'; + if (this.gen <= 4) { + poke.itemEffect = 'knocked off'; + } else { + poke.prevItemEffect = 'knocked off'; + } this.scene.runOtherAnim('itemoff' as ID, [poke]); this.scene.resultAnim(poke, 'Item knocked off', 'neutral'); break; @@ -2145,7 +2381,7 @@ class Battle { } case '-ability': { let poke = this.getPokemon(args[1])!; - let ability = Dex.getAbility(args[2]); + let ability = Dex.abilities.get(args[2]); let effect = Dex.getEffect(kwArgs.from); let ofpoke = this.getPokemon(kwArgs.of); poke.rememberAbility(ability.name, effect.id && !kwArgs.fail); @@ -2185,6 +2421,7 @@ class Battle { } else { this.activateAbility(poke, ability.name); } + this.scene.updateWeather(); this.log(args, kwArgs); break; } @@ -2192,7 +2429,7 @@ class Battle { // deprecated; use |-start| for Gastro Acid // and the third arg of |-ability| for Entrainment et al let poke = this.getPokemon(args[1])!; - let ability = Dex.getAbility(args[2]); + let ability = Dex.abilities.get(args[2]); poke.ability = '(suppressed)'; if (ability.id) { @@ -2216,7 +2453,15 @@ class Battle { } newSpeciesForme = args[2].substr(0, commaIndex); } - let species = this.dex.getSpecies(newSpeciesForme); + let species = this.dex.species.get(newSpeciesForme); + if (nextArgs) { + if (nextArgs[0] === '-mega') { + species = this.dex.species.get(this.dex.items.get(nextArgs[3]).megaStone); + } else if (nextArgs[0] === '-primal' && nextArgs.length > 2) { + if (nextArgs[2] === 'Red Orb') species = this.dex.species.get('Groudon-Primal'); + if (nextArgs[2] === 'Blue Orb') species = this.dex.species.get('Kyogre-Primal'); + } + } poke.speciesForme = newSpeciesForme; poke.ability = poke.baseAbility = (species.abilities ? species.abilities['0'] : ''); @@ -2239,13 +2484,16 @@ class Battle { } poke.boosts = {...tpoke.boosts}; - poke.copyTypesFrom(tpoke); + poke.copyTypesFrom(tpoke, true); poke.ability = tpoke.ability; - const speciesForme = (tpoke.volatiles.formechange ? tpoke.volatiles.formechange[1] : tpoke.speciesForme); + poke.timesAttacked = tpoke.timesAttacked; + const targetForme = tpoke.volatiles.formechange; + const speciesForme = (targetForme && !targetForme[1].endsWith('-Gmax')) ? targetForme[1] : tpoke.speciesForme; const pokemon = tpoke; const shiny = tpoke.shiny; const gender = tpoke.gender; - poke.addVolatile('transform' as ID, pokemon, shiny, gender); + const level = tpoke.level; + poke.addVolatile('transform' as ID, pokemon, shiny, gender, level); poke.addVolatile('formechange' as ID, speciesForme); for (const trackedMove of tpoke.moveTrack) { poke.rememberMove(trackedMove[0], 0); @@ -2257,24 +2505,25 @@ class Battle { } case '-formechange': { let poke = this.getPokemon(args[1])!; - let species = Dex.getSpecies(args[2]); + let species = Dex.species.get(args[2]); let fromeffect = Dex.getEffect(kwArgs.from); - let isCustomAnim = false; - poke.removeVolatile('typeadd' as ID); - poke.removeVolatile('typechange' as ID); - if (this.gen >= 7) poke.removeVolatile('autotomize' as ID); + if (!poke.getSpeciesForme().endsWith('-Gmax') && !species.name.endsWith('-Gmax')) { + poke.removeVolatile('typeadd' as ID); + poke.removeVolatile('typechange' as ID); + if (this.gen >= 6) poke.removeVolatile('autotomize' as ID); + } if (!kwArgs.silent) { this.activateAbility(poke, fromeffect); } poke.addVolatile('formechange' as ID, species.name); // the formechange volatile reminds us to revert the sprite change on switch-out - this.scene.animTransform(poke, isCustomAnim); + this.scene.animTransform(poke, true); this.log(args, kwArgs); break; } case '-mega': { let poke = this.getPokemon(args[1])!; - let item = Dex.getItem(args[3]); + let item = Dex.items.get(args[3]); if (args[3]) { poke.item = item.name; } @@ -2285,6 +2534,27 @@ class Battle { this.log(args, kwArgs); break; } + case '-terastallize': { + let poke = this.getPokemon(args[1])!; + let type = Dex.types.get(args[2]).name; + let lockForme = false; + poke.removeVolatile('typeadd' as ID); + poke.teraType = type; + poke.terastallized = type; + poke.details += `, tera:${type}`; + poke.searchid += `, tera:${type}`; + if (poke.speciesForme.startsWith("Morpeko")) { + lockForme = true; + poke.speciesForme = poke.getSpeciesForme(); + poke.details = poke.details.replace("Morpeko", poke.speciesForme); + poke.searchid = `${poke.ident}|${poke.details}`; + delete poke.volatiles['formechange']; + } + this.scene.animTransform(poke, true, lockForme); + this.scene.resetStatbar(poke); + this.log(args, kwArgs); + break; + } case '-start': { let poke = this.getPokemon(args[1])!; let effect = Dex.getEffect(args[2]); @@ -2295,6 +2565,7 @@ class Battle { this.activateAbility(ofpoke || poke, fromeffect); switch (effect.id) { case 'typechange': + if (poke.terastallized) break; if (ofpoke && fromeffect.id === 'reflecttype') { poke.copyTypesFrom(ofpoke); } else { @@ -2314,7 +2585,7 @@ class Battle { this.scene.typeAnim(poke, type); break; case 'dynamax': - poke.addVolatile('dynamax' as ID); + poke.addVolatile('dynamax' as ID, !!args[3]); this.scene.animTransform(poke, true); break; case 'powertrick': @@ -2431,6 +2702,10 @@ class Battle { } break; + // Gen 1-2 + case 'mist': + this.scene.resultAnim(poke, 'Mist', 'good'); + break; // Gen 1 case 'lightscreen': this.scene.resultAnim(poke, 'Light Screen', 'good'); @@ -2439,7 +2714,9 @@ class Battle { this.scene.resultAnim(poke, 'Reflect', 'good'); break; } - poke.addVolatile(effect.id); + if (!(effect.id === 'typechange' && poke.terastallized)) { + poke.addVolatile(effect.id); + } this.scene.updateStatbar(poke); this.log(args, kwArgs); break; @@ -2450,7 +2727,7 @@ class Battle { let fromeffect = Dex.getEffect(kwArgs.from); poke.removeVolatile(effect.id); - if (kwArgs.silent) { + if (kwArgs.silent && !(effect.id === 'protosynthesis' || effect.id === 'quarkdrive')) { // do nothing } else { switch (effect.id) { @@ -2518,6 +2795,20 @@ class Battle { poke.removeVolatile('stockpile2' as ID); poke.removeVolatile('stockpile3' as ID); break; + case 'protosynthesis': + poke.removeVolatile('protosynthesisatk' as ID); + poke.removeVolatile('protosynthesisdef' as ID); + poke.removeVolatile('protosynthesisspa' as ID); + poke.removeVolatile('protosynthesisspd' as ID); + poke.removeVolatile('protosynthesisspe' as ID); + break; + case 'quarkdrive': + poke.removeVolatile('quarkdriveatk' as ID); + poke.removeVolatile('quarkdrivedef' as ID); + poke.removeVolatile('quarkdrivespa' as ID); + poke.removeVolatile('quarkdrivespd' as ID); + poke.removeVolatile('quarkdrivespe' as ID); + break; default: if (effect.effectType === 'Move') { if (effect.name === 'Doom Desire') { @@ -2586,7 +2877,6 @@ class Battle { let poke = this.getPokemon(args[1])!; let effect = Dex.getEffect(args[2]); poke.addMovestatus(effect.id); - switch (effect.id) { case 'grudge': this.scene.resultAnim(poke, 'Grudge', 'neutral'); @@ -2595,6 +2885,7 @@ class Battle { this.scene.resultAnim(poke, 'Destiny Bond', 'neutral'); break; } + this.scene.updateStatbar(poke); this.log(args, kwArgs); break; } @@ -2654,7 +2945,7 @@ class Battle { case 'eeriespell': case 'gmaxdepletion': case 'spite': - let move = Dex.getMove(kwArgs.move).name; + let move = Dex.moves.get(kwArgs.move).name; let pp = Number(kwArgs.number); if (isNaN(pp)) pp = 4; poke.rememberMove(move, pp); @@ -2683,6 +2974,10 @@ class Battle { break; // ability activations + case 'electromorphosis': + case 'windpower': + poke.addMovestatus('charge' as ID); + break; case 'forewarn': if (target) { target.rememberMove(kwArgs.move, 0); @@ -2696,13 +2991,14 @@ class Battle { } } break; + case 'lingeringaroma': case 'mummy': if (!kwArgs.ability) break; // if Mummy activated but failed, no ability will have been sent - let ability = Dex.getAbility(kwArgs.ability); + let ability = Dex.abilities.get(kwArgs.ability); this.activateAbility(target, ability.name); - this.activateAbility(poke, "Mummy"); + this.activateAbility(poke, effect.name); this.scene.wait(700); - this.activateAbility(target, "Mummy", true); + this.activateAbility(target, effect.name, true); break; // item activations @@ -2713,6 +3009,12 @@ class Battle { case 'focusband': poke.item = 'Focus Band'; break; + case 'quickclaw': + poke.item = 'Quick Claw'; + break; + case 'abilityshield': + poke.item = 'Ability Shield'; + break; default: if (kwArgs.broken) { // for custom moves that break protection this.scene.resultAnim(poke, 'Protection broken', 'bad'); @@ -2724,7 +3026,7 @@ class Battle { case '-sidestart': { let side = this.getSide(args[1]); let effect = Dex.getEffect(args[2]); - side.addSideCondition(effect); + side.addSideCondition(effect, !!kwArgs.persistent); switch (effect.id) { case 'tailwind': @@ -2755,6 +3057,12 @@ class Battle { this.log(args, kwArgs); break; } + case '-swapsideconditions': { + this.swapSideConditions(); + this.scene.updateWeather(); + this.log(args, kwArgs); + break; + } case '-weather': { let effect = Dex.getEffect(args[1]); let poke = this.getPokemon(kwArgs.of) || undefined; @@ -2771,6 +3079,7 @@ class Battle { let poke = this.getPokemon(kwArgs.of); let fromeffect = Dex.getEffect(kwArgs.from); this.activateAbility(poke, fromeffect); + let minTimeLeft = 5; let maxTimeLeft = 0; if (effect.id.endsWith('terrain')) { for (let i = this.pseudoWeather.length - 1; i >= 0; i--) { @@ -2782,15 +3091,14 @@ class Battle { } if (this.gen > 6) maxTimeLeft = 8; } - this.addPseudoWeather(effect.name, 5, maxTimeLeft); + if (kwArgs.persistent) minTimeLeft += 2; + this.addPseudoWeather(effect.name, minTimeLeft, maxTimeLeft); switch (effect.id) { case 'gravity': if (this.seeking !== null) break; - for (const side of this.sides) { - for (const active of side.active) { - if (active) this.scene.runOtherAnim('gravity' as ID, [active]); - } + for (const active of this.getAllActive()) { + this.scene.runOtherAnim('gravity' as ID, [active]); } break; } @@ -2816,7 +3124,7 @@ class Battle { } case '-anim': { let poke = this.getPokemon(args[1])!; - let move = Dex.getMove(args[2]); + let move = Dex.moves.get(args[2]); if (this.checkActive(poke)) return; let poke2 = this.getPokemon(args[3]); this.scene.beforeMove(poke); @@ -2824,7 +3132,7 @@ class Battle { this.scene.afterMove(poke); break; } - case '-hint': case '-message': { + case '-hint': case '-message': case '-candynamax': { this.log(args, kwArgs); break; } @@ -2860,7 +3168,7 @@ class Battle { } if (foe) siden = (siden ? 0 : 1); - let data = Dex.getSpecies(name); + let data = Dex.species.get(name); return data.spriteData[siden]; } */ @@ -2881,12 +3189,16 @@ class Battle { output.ident = (!isTeamPreview ? pokemonid : ''); output.searchid = (!isTeamPreview ? `${pokemonid}|${details}` : ''); let splitDetails = details.split(', '); + if (splitDetails[splitDetails.length - 1].startsWith('tera:')) { + output.terastallized = splitDetails[splitDetails.length - 1].slice(5); + splitDetails.pop(); + } if (splitDetails[splitDetails.length - 1] === 'shiny') { output.shiny = true; splitDetails.pop(); } if (splitDetails[splitDetails.length - 1] === 'M' || splitDetails[splitDetails.length - 1] === 'F') { - output.gender = splitDetails[splitDetails.length - 1] as GenderName; + output.gender = splitDetails[splitDetails.length - 1] as Dex.GenderName; splitDetails.pop(); } if (splitDetails[1]) { @@ -2940,23 +3252,15 @@ class Battle { let siden = -1; let slot = -1; // if there is an explicit slot for this pokemon - let slotChart: {[k: string]: number} = {a: 0, b: 1, c: 2, d: 3, e: 4, f: 5}; - if (name.substr(0, 4) === 'p2: ' || name === 'p2') { - siden = this.p2.n; - name = name.substr(4); - } else if (name.substr(0, 4) === 'p1: ' || name === 'p1') { - siden = this.p1.n; - name = name.substr(4); - } else if (name.substr(0, 2) === 'p2' && name.substr(3, 2) === ': ') { - slot = slotChart[name.substr(2, 1)]; - siden = this.p2.n; - name = name.substr(5); - pokemonid = 'p2: ' + name; - } else if (name.substr(0, 2) === 'p1' && name.substr(3, 2) === ': ') { - slot = slotChart[name.substr(2, 1)]; - siden = this.p1.n; - name = name.substr(5); - pokemonid = 'p1: ' + name; + if (/^p[1-9]($|: )/.test(name)) { + siden = parseInt(name.charAt(1), 10) - 1; + name = name.slice(4); + } else if (/^p[1-9][a-f]: /.test(name)) { + const slotChart: {[k: string]: number} = {a: 0, b: 1, c: 2, d: 3, e: 4, f: 5}; + siden = parseInt(name.charAt(1), 10) - 1; + slot = slotChart[name.charAt(2)]; + name = name.slice(5); + pokemonid = `p${siden + 1}: ${name}`; } return {name, siden, slot, pokemonid}; } @@ -3010,7 +3314,7 @@ class Battle { } return null; } - getPokemon(pokemonid: string | undefined) { + getPokemon(pokemonid: string | undefined, faintedOnly = false) { if (!pokemonid || pokemonid === '??' || pokemonid === 'null' || pokemonid === 'false') { return null; } @@ -3025,7 +3329,8 @@ class Battle { if (!isInactive && side.active[slot]) return side.active[slot]; for (const pokemon of side.pokemon) { - if (isInactive && side.active.includes(pokemon)) continue; + if (isInactive && !this.compatMode && side.active.includes(pokemon)) continue; + if (faintedOnly && pokemon.hp) continue; if (pokemon.ident === pokemonid) { // name matched, good enough if (slot >= 0) pokemon.slot = slot; return pokemon; @@ -3035,8 +3340,10 @@ class Battle { return null; } getSide(sidename: string): Side { - if (sidename === 'p1' || sidename.substr(0, 3) === 'p1:') return this.p1; - if (sidename === 'p2' || sidename.substr(0, 3) === 'p2:') return this.p2; + if (sidename === 'p1' || sidename.startsWith('p1:')) return this.p1; + if (sidename === 'p2' || sidename.startsWith('p2:')) return this.p2; + if ((sidename === 'p3' || sidename.startsWith('p3:')) && this.p3) return this.p3; + if ((sidename === 'p4' || sidename.startsWith('p4:')) && this.p4) return this.p4; if (this.nearSide.id === sidename) return this.nearSide; if (this.farSide.id === sidename) return this.farSide; if (this.nearSide.name === sidename) return this.nearSide; @@ -3071,15 +3378,19 @@ class Battle { runMajor(args: Args, kwArgs: KWArgs, preempt?: boolean) { switch (args[0]) { case 'start': { - this.scene.teamPreviewEnd(); this.nearSide.active[0] = null; this.farSide.active[0] = null; + this.scene.resetSides(); this.start(); break; } case 'upkeep': { this.usesUpkeep = true; this.updateTurnCounters(); + // Prevents getSwitchedPokemon from skipping over a Pokemon that switched out mid turn (e.g. U-turn) + for (const side of this.sides) { + side.lastPokemon = null; + } break; } case 'turn': { @@ -3096,15 +3407,40 @@ class Battle { this.messageFadeTime = 40; this.isBlitz = true; } + if (this.tier.includes(`Let's Go`)) { + this.dex = Dex.mod('gen7letsgo' as ID); + } + if (this.tier.includes('Super Staff Bros')) { + this.dex = Dex.mod('gen9ssb' as ID); + } this.log(args); break; } case 'gametype': { this.gameType = args[1] as any; + this.compatMode = false; switch (args[1]) { - default: - this.nearSide.active = [null]; - this.farSide.active = [null]; + case 'multi': + case 'freeforall': + this.pokemonControlled = 1; + if (!this.p3) this.p3 = new Side(this, 2); + if (!this.p4) this.p4 = new Side(this, 3); + this.p3.foe = this.p2; + this.p4.foe = this.p1; + + if (args[1] === 'multi') { + this.p4.ally = this.p2; + this.p3.ally = this.p1; + this.p1.ally = this.p3; + this.p2.ally = this.p4; + } + + this.p3.isFar = this.p1.isFar; + this.p4.isFar = this.p2.isFar; + this.sides = [this.p1, this.p2, this.p3, this.p4]; + // intentionally sync p1/p3 and p2/p4's active arrays + this.p1.active = this.p3.active = [null, null]; + this.p2.active = this.p4.active = [null, null]; break; case 'doubles': this.nearSide.active = [null, null]; @@ -3115,8 +3451,13 @@ class Battle { this.nearSide.active = [null, null, null]; this.farSide.active = [null, null, null]; break; + default: + for (const side of this.sides) side.active = [null]; + break; } + if (!this.pokemonControlled) this.pokemonControlled = this.nearSide.active.length; this.scene.updateGen(); + this.scene.resetSides(); break; } case 'rule': { @@ -3126,6 +3467,8 @@ class Battle { this.messageFadeTime = 40; this.isBlitz = true; } + if (ruleName === 'Exact HP Mod') this.reportExactHP = true; + this.rules[ruleName] = 1; this.log(args); break; } @@ -3221,9 +3564,18 @@ class Battle { side.setName(args[2]); if (args[3]) side.setAvatar(args[3]); if (args[4]) side.rating = args[4]; - this.scene.updateSidebar(side); if (this.joinButtons) this.scene.hideJoinButtons(); this.log(args); + this.scene.updateSidebar(side); + break; + } + case 'badge': { + let side = this.getSide(args[1]); + // handle all the rendering further down + const badge = args.slice(2).join('|'); + // (don't allow duping) + if (!side.badges.includes(badge)) side.badges.push(badge); + this.scene.updateSidebar(side); break; } case 'teamsize': { @@ -3247,32 +3599,67 @@ class Battle { } case 'poke': { let pokemon = this.rememberTeamPreviewPokemon(args[1], args[2])!; - if (args[3] === 'item') { + if (args[3] === 'mail') { + pokemon.item = '(mail)'; + } else if (args[3] === 'item') { pokemon.item = '(exists)'; } break; } + case 'updatepoke': { + const {siden} = this.parsePokemonId(args[1]); + const side = this.sides[siden]; + for (let i = 0; i < side.pokemon.length; i++) { + const pokemon = side.pokemon[i]; + if (pokemon.details !== args[2] && pokemon.checkDetails(args[2])) { + side.addPokemon('', '', args[2], i); + break; + } + } + break; + } case 'teampreview': { this.teamPreviewCount = parseInt(args[1], 10); this.scene.teamPreview(); break; } + case 'showteam': { + const team = Teams.unpack(args[2]); + if (!team.length) return; + const side = this.getSide(args[1]); + side.clearPokemon(); + for (const set of team) { + const details = set.species + (!set.level || set.level === 100 ? '' : ', L' + set.level) + + (!set.gender || set.gender === 'N' ? '' : ', ' + set.gender) + (set.shiny ? ', shiny' : ''); + const pokemon = side.addPokemon('', '', details); + if (set.item) pokemon.item = set.item; + if (set.ability) pokemon.rememberAbility(set.ability); + for (const move of set.moves) { + pokemon.rememberMove(move, 0); + } + if (set.teraType) pokemon.teraType = set.teraType; + } + this.log(args, kwArgs); + break; + } case 'switch': case 'drag': case 'replace': { this.endLastTurn(); let poke = this.getSwitchedPokemon(args[1], args[2])!; let slot = poke.slot; poke.healthParse(args[3]); poke.removeVolatile('itemremoved' as ID); + poke.terastallized = args[2].match(/tera:([a-z]+)$/i)?.[1] || ''; if (args[0] === 'switch') { if (poke.side.active[slot]) { - poke.side.switchOut(poke.side.active[slot]!); + poke.side.switchOut(poke.side.active[slot]!, kwArgs); } - poke.side.switchIn(poke); + poke.side.switchIn(poke, kwArgs); } else if (args[0] === 'replace') { poke.side.replace(poke); } else { poke.side.dragIn(poke); } + this.scene.updateWeather(); this.log(args, kwArgs); break; } @@ -3293,7 +3680,7 @@ class Battle { const target = poke.side.active[targetIndex]; if (target) args[2] = target.ident; } - poke.side.swapTo(poke, targetIndex, kwArgs); + poke.side.swapTo(poke, targetIndex); } this.log(args, kwArgs); break; @@ -3302,7 +3689,7 @@ class Battle { this.endLastTurn(); this.resetTurnsSinceMoved(); let poke = this.getPokemon(args[1])!; - let move = Dex.getMove(args[2]); + let move = Dex.moves.get(args[2]); if (this.checkActive(poke)) return; let poke2 = this.getPokemon(args[3]); this.scene.beforeMove(poke); @@ -3317,7 +3704,7 @@ class Battle { this.resetTurnsSinceMoved(); let poke = this.getPokemon(args[1])!; let effect = Dex.getEffect(args[2]); - let move = Dex.getMove(args[3]); + let move = Dex.moves.get(args[3]); this.cantUseMove(poke, effect, move, kwArgs); this.log(args, kwArgs); break; @@ -3341,6 +3728,21 @@ class Battle { this.scene.setControlsHTML(BattleLog.sanitizeHTML(args[1])); break; } + case 'custom': { + // Style is always |custom|-subprotocol|pokemon|additional info + if (args[1] === '-endterastallize') { + let poke = this.getPokemon(args[2])!; + poke.removeVolatile('terastallize' as ID); + poke.teraType = ''; + poke.terastallized = ''; + poke.details = poke.details.replace(/, tera:[a-z]+/i, ''); + poke.searchid = poke.searchid.replace(/, tera:[a-z]+/i, ''); + this.scene.animTransform(poke); + this.scene.resetStatbar(poke); + this.log(args, kwArgs); + } + break; + } default: { this.log(args, kwArgs, preempt); break; @@ -3383,7 +3785,7 @@ class Battle { } else { this.runMajor(args, kwArgs, preempt); } - } catch (err) { + } catch (err: any) { this.log(['majorerror', 'Error parsing: ' + str + ' (' + err + ')']); if (err.stack) { let stack = ('' + err.stack).split('\n'); @@ -3434,13 +3836,19 @@ class Battle { this.subscription?.('playing'); } skipTurn() { - this.seekTurn(this.turn + 1); + this.seekBy(1); + } + seekBy(deltaTurn: number) { + if (this.seeking === Infinity && deltaTurn < 0) { + return this.seekTurn(this.turn + 1); + } + this.seekTurn((this.seeking ?? this.turn) + deltaTurn); } seekTurn(turn: number, forceReset?: boolean) { if (isNaN(turn)) return; turn = Math.max(Math.floor(turn), 0); - if (this.seeking !== null && this.seeking > turn && !forceReset) { + if (this.seeking !== null && turn > this.turn && !forceReset) { this.seeking = turn; return; } @@ -3479,9 +3887,11 @@ class Battle { nextStep() { if (!this.shouldStep()) return; + let time = Date.now(); this.scene.startAnimations(); let animations = undefined; + let interruptionCount: number; do { this.waitForAnimations = true; if (this.currentStep >= this.stepQueue.length) { @@ -3502,6 +3912,16 @@ class Battle { } else if (this.waitForAnimations === 'simult') { this.scene.timeOffset = 0; } + + if (Date.now() - time > 300) { + interruptionCount = this.scene.interruptionCount; + setTimeout(() => { + if (interruptionCount === this.scene.interruptionCount) { + this.nextStep(); + } + }, 1); + return; + } } while (!animations && this.shouldStep()); if (this.paused && this.turn >= 0 && this.seeking === null) { @@ -3512,7 +3932,7 @@ class Battle { if (!animations) return; - const interruptionCount = this.scene.interruptionCount; + interruptionCount = this.scene.interruptionCount; animations.done(() => { if (interruptionCount === this.scene.interruptionCount) { this.nextStep(); @@ -3526,10 +3946,12 @@ class Battle { } setMute(mute: boolean) { - BattleSound.setMute(mute); + this.scene.setMute(mute); } } +declare const require: any; +declare const global: any; if (typeof require === 'function') { // in Node (global as any).Battle = Battle; diff --git a/src/client-connection.ts b/play.pokemonshowdown.com/src/client-connection.ts similarity index 95% rename from src/client-connection.ts rename to play.pokemonshowdown.com/src/client-connection.ts index c4929d7659..7dac3cf621 100644 --- a/src/client-connection.ts +++ b/play.pokemonshowdown.com/src/client-connection.ts @@ -5,9 +5,11 @@ * @license MIT */ +import {PS} from "./client-main"; + declare var SockJS: any; -class PSConnection { +export class PSConnection { socket: any = null; connected = false; queue = [] as string[]; @@ -42,6 +44,10 @@ class PSConnection { PS.update(); }; } + disconnect() { + this.socket.close(); + PS.connection = null; + } send(msg: string) { if (!this.connected) { this.queue.push(msg); @@ -53,7 +59,7 @@ class PSConnection { PS.connection = new PSConnection(); -const PSLoginServer = new class { +export const PSLoginServer = new class { query(data: PostData): Promise<{[k: string]: any} | null> { let url = '/~~' + PS.server.id + '/action.php'; if (location.pathname.endsWith('.html')) { @@ -156,7 +162,7 @@ class NetRequest { } } -function Net(uri: string) { +export function Net(uri: string) { return new NetRequest(uri); } diff --git a/src/client-core.ts b/play.pokemonshowdown.com/src/client-core.ts similarity index 98% rename from src/client-core.ts rename to play.pokemonshowdown.com/src/client-core.ts index d63fbd5146..df91bd4a2f 100644 --- a/src/client-core.ts +++ b/play.pokemonshowdown.com/src/client-core.ts @@ -84,7 +84,7 @@ if (!window.console) { const PSURL = `${document.location!.protocol !== 'http:' ? 'https:' : ''}//${Config.routes.client}/`; -class PSSubscription { +export class PSSubscription { observable: PSModel | PSStreamModel; listener: (value?: any) => void; constructor(observable: PSModel | PSStreamModel, listener: (value?: any) => void) { @@ -102,7 +102,7 @@ class PSSubscription { * spec - just the parts we use. PSModel just notifies subscribers of * updates - a simple model for React. */ -class PSModel { +export class PSModel { subscriptions = [] as PSSubscription[]; subscribe(listener: () => void) { const subscription = new PSSubscription(this, listener); @@ -128,7 +128,7 @@ class PSModel { * which hold state, PSStreamModels give state directly to views, * so that the model doesn't need to hold a redundant copy of state. */ -class PSStreamModel { +export class PSStreamModel { subscriptions = [] as PSSubscription[]; updates = [] as T[]; subscribe(listener: (value: T) => void) { @@ -260,7 +260,7 @@ const PSBackground = new class extends PSStreamModel { attrib = { url: 'https://quanyails.deviantart.com/art/Sunrise-Ocean-402667154', title: 'Sunrise Ocean', - artist: 'Yijing Chen', + artist: 'Quanyails', }; break; case 'waterfall': @@ -273,7 +273,7 @@ const PSBackground = new class extends PSStreamModel { "140,38.18181818181818%", ]; attrib = { - url: 'https://yilx.deviantart.com/art/Irie-372292729', + url: 'https://x.com/Yilxaevum', title: 'Irie', artist: 'Samuel Teo', }; @@ -303,7 +303,7 @@ const PSBackground = new class extends PSStreamModel { "210,29.629629629629633%", ]; attrib = { - url: 'https://seiryuuden.deviantart.com/art/The-Ultimate-Mega-Showdown-Charizards-414587079', + url: 'https://lit.link/en/seiryuuden', title: 'Charizards', artist: 'Jessica Valencia', }; diff --git a/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts similarity index 91% rename from src/client-main.ts rename to play.pokemonshowdown.com/src/client-main.ts index 4d3a74564b..2fd42cd9f9 100644 --- a/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -9,6 +9,14 @@ * @license AGPLv3 */ +import { PSConnection, PSLoginServer } from './client-connection'; +import {PSModel, PSStreamModel} from './client-core'; +import type {PSRouter} from './panels'; +import type {ChatRoom} from './panel-chat'; +import type {MainMenuRoom} from './panel-mainmenu'; +import {toID, type ID} from './battle-dex'; +import {BattleTextParser, type Args} from './battle-text-parser'; + /********************************************************************** * Prefs *********************************************************************/ @@ -16,7 +24,7 @@ /** * String that contains only lowercase alphanumeric characters. */ -type RoomID = string & {__isRoomID: true}; +export type RoomID = string & {__isRoomID: true}; const PSPrefsDefaults: {[key: string]: any} = {}; @@ -29,9 +37,9 @@ const PSPrefsDefaults: {[key: string]: any} = {}; */ class PSPrefs extends PSStreamModel { /** - * Dark mode! + * The theme to use. "system" matches the theme of the system accessing the client. */ - dark = false; + theme: 'light' | 'dark' | 'system' = 'light'; /** * Disables animated GIFs, but keeps other animations enabled. * Workaround for a Chrome 64 bug with GIFs. @@ -128,6 +136,17 @@ class PSPrefs extends PSStreamModel { newPrefs['nogif'] = true; alert('Your version of Chrome has a bug that makes animated GIFs freeze games sometimes, so certain animations have been disabled. Only some people have the problem, so you can experiment and enable them in the Options menu setting "Disable GIFs for Chrome 64 bug".'); } + + const colorSchemeQuerySupported = window.matchMedia?.('(prefers-color-scheme: dark)').media !== 'not all'; + if (newPrefs['theme'] === 'system' && !colorSchemeQuerySupported) { + newPrefs['theme'] = 'light'; + } + if (newPrefs['dark'] !== undefined) { + if (newPrefs['dark']) { + newPrefs['theme'] = 'dark'; + } + delete newPrefs['dark']; + } } } @@ -135,7 +154,7 @@ class PSPrefs extends PSStreamModel { * Teams *********************************************************************/ -interface Team { +export interface Team { name: string; format: ID; packedTeam: string; @@ -263,9 +282,9 @@ class PSTeams extends PSStreamModel<'team' | 'format'> { *********************************************************************/ class PSUser extends PSModel { - name = "Guest"; + name = ""; group = ''; - userid = "guest" as ID; + userid = "" as ID; named = false; registered = false; avatar = "1"; @@ -285,6 +304,22 @@ class PSUser extends PSModel { } } } + logOut() { + PSLoginServer.query({ + act: 'logout', + userid: this.userid, + }); + PS.send('|/logout'); + PS.connection?.disconnect(); + + alert("You have been logged out and disconnected.\n\nIf you wanted to change your name while staying connected, use the 'Change Name' button or the '/nick' command."); + this.name = ""; + this.group = ''; + this.userid = "" as ID; + this.named = false; + this.registered = false; + this.update(); + } } /********************************************************************** @@ -339,11 +374,11 @@ class PSServer { // by default, unrecognized ranks go here, between driver and bot '*': { name: "Bot (*)", - order: 108, + order: 109, }, '\u2606': { name: "Player (\u2606)", - order: 109, + order: 110, }, '+': { name: "Voice (+)", @@ -369,7 +404,7 @@ class PSServer { }, }; defaultGroup: PSGroup = { - order: 107, + order: 108, }; getGroup(symbol: string | undefined) { return this.groups[(symbol || ' ').charAt(0)] || this.defaultGroup; @@ -382,7 +417,7 @@ class PSServer { type PSRoomLocation = 'left' | 'right' | 'popup' | 'mini-window' | 'modal-popup' | 'semimodal-popup'; -interface RoomOptions { +export interface RoomOptions { id: RoomID; title?: string; type?: string; @@ -409,7 +444,7 @@ interface PSNotificationState { * As a PSStreamModel, PSRoom can emit `Args` to mean "we received a message", * and `null` to mean "tell Preact to re-render this room" */ -class PSRoom extends PSStreamModel implements RoomOptions { +export class PSRoom extends PSStreamModel implements RoomOptions { id: RoomID; title = ""; type = ''; @@ -507,15 +542,23 @@ class PSRoom extends PSStreamModel implements RoomOptions { } }} } - handleMessage(msg: string) { + handleMessage(line: string) { + if (!line.startsWith('/') || line.startsWith('//')) return false; + const spaceIndex = line.indexOf(' '); + const cmd = spaceIndex >= 0 ? line.slice(1, spaceIndex) : line.slice(1); + // const target = spaceIndex >= 0 ? line.slice(spaceIndex + 1) : ''; + switch (cmd) { + case 'logout': { + PS.user.logOut(); + return true; + }} return false; } send(msg: string, direct?: boolean) { if (!direct && !msg) return; if (!direct && this.handleMessage(msg)) return; - const id = this.id === 'lobby' ? '' : this.id; - PS.send(id + '|' + msg); + PS.send(this.id + '|' + msg); } destroy() { if (this.connected) { @@ -527,8 +570,8 @@ class PSRoom extends PSStreamModel implements RoomOptions { class PlaceholderRoom extends PSRoom { queue = [] as Args[]; - readonly classType: 'placeholder' = 'placeholder'; - receiveLine(args: Args) { + override readonly classType: 'placeholder' = 'placeholder'; + override receiveLine(args: Args) { this.queue.push(args); } } @@ -545,7 +588,7 @@ type RoomType = {Model?: typeof PSRoom, Component: any, title?: string}; * - changing which room is focused * - changing the width of the left room, in two-panel mode */ -const PS = new class extends PSModel { +export const PS = new class extends PSModel { down: string | boolean = false; prefs = new PSPrefs(); @@ -636,6 +679,17 @@ const PS = new class extends PSModel { leftRoomWidth = 0; mainmenu: MainMenuRoom = null!; + /** + * The drag-and-drop API is incredibly dumb and doesn't let us know + * what's being dragged until the `drop` event, so we track it here. + * + * Note that `PS.dragging` will be null if the drag was initiated + * outside PS (e.g. dragging a team from File Explorer to PS), and + * for security reasons it's impossible to know what they are until + * they're dropped. + */ + dragging: {type: 'room', roomid: RoomID} | null = null; + /** Tracks whether or not to display the "Use arrow keys" hint */ arrowKeysUsed = false; @@ -732,7 +786,7 @@ const PS = new class extends PSModel { if (!alreadyUpdating) this.update(true); } } - update(layoutAlreadyUpdated?: boolean) { + override update(layoutAlreadyUpdated?: boolean) { if (!layoutAlreadyUpdated) this.updateLayout(true); super.update(); } @@ -797,7 +851,11 @@ const PS = new class extends PSModel { const roomid = fullMsg.slice(0, pipeIndex) as RoomID; const msg = fullMsg.slice(pipeIndex + 1); console.log('\u25b6\ufe0f ' + (roomid ? '[' + roomid + '] ' : '') + '%c' + msg, "color: #776677"); - this.connection!.send(fullMsg); + if (!this.connection) { + alert(`You are not connected and cannot send ${msg}.`); + return; + } + this.connection.send(fullMsg); } isVisible(room: PSRoom) { if (this.leftRoomWidth === 0) { @@ -855,7 +913,7 @@ const PS = new class extends PSModel { case 'news': options.type = options.id; break; - case 'battle-': case 'user-': case 'team-': + case 'battle-': case 'user-': case 'team-': case 'ladder-': options.type = options.id.slice(0, hyphenIndex); break; case 'view-': @@ -944,7 +1002,7 @@ const PS = new class extends PSModel { } this.room.autoDismissNotifications(); this.update(); - if (this.room.onParentEvent) this.room.onParentEvent('focus', undefined); + this.room.onParentEvent?.('focus', undefined); return true; } focusLeftRoom() { @@ -996,7 +1054,7 @@ const PS = new class extends PSModel { const roomid = `pm-${[userid, myUserid].sort().join('-')}` as RoomID; if (this.rooms[roomid]) return this.rooms[roomid] as ChatRoom; this.join(roomid); - return this.rooms[roomid] as ChatRoom; + return this.rooms[roomid]! as ChatRoom; } addRoom(options: RoomOptions, noFocus?: boolean) { // support hardcoded PM room-IDs diff --git a/play.pokemonshowdown.com/src/globals.d.ts b/play.pokemonshowdown.com/src/globals.d.ts new file mode 100644 index 0000000000..c3e53c2892 --- /dev/null +++ b/play.pokemonshowdown.com/src/globals.d.ts @@ -0,0 +1,42 @@ +// dex data +/////////// + +declare var BattleText: {[id: string]: {[templateName: string]: string}}; +declare var BattleFormats: {[id: string]: import('./panel-teamdropdown').FormatData}; +declare var BattlePokedex: any; +declare var BattleMovedex: any; +declare var BattleAbilities: any; +declare var BattleItems: any; +declare var BattleAliases: any; +declare var BattleStatuses: any; +declare var BattlePokemonSprites: any; +declare var BattlePokemonSpritesBW: any; +declare var NonBattleGames: {[id: string]: string}; + +// PS globals +///////////// + +declare var Config: any; +declare var Replays: any; +declare var exports: any; +type AnyObject = {[k: string]: any}; +declare var app: {user: AnyObject, rooms: AnyObject, ignore?: AnyObject}; + +interface Window { + [k: string]: any; +} + +// Temporary globals (exported from modules, used by non-module files) + +// When creating new module files, these should all be commented out +// to make sure they're not being used globally in modules. + +// declare var Battle: typeof import('./battle').Battle; +// type Battle = import('./battle').Battle; +// declare var BattleScene: typeof import('./battle-animations').BattleScene; +// type BattleScene = import('./battle-animations').BattleScene; +// declare var Pokemon: typeof import('./battle').Pokemon; +// type Pokemon = import('./battle').Pokemon; +// type ServerPokemon = import('./battle').ServerPokemon; +// declare var BattleLog: typeof import('./battle-log').BattleLog; +// type BattleLog = import('./battle-log').BattleLog; diff --git a/play.pokemonshowdown.com/src/miniedit.ts b/play.pokemonshowdown.com/src/miniedit.ts new file mode 100644 index 0000000000..c62e6f55fd --- /dev/null +++ b/play.pokemonshowdown.com/src/miniedit.ts @@ -0,0 +1,235 @@ +// MiniEdit: ContentEditable-based rich source editor + +// True WYSIWYG is really complex, and

    + +

    + +

    + [Hidden] +

    + + +

    + Banned
    Reason: +

    + +

    + + + + .psim.us + +

    +

    Owner:

    + +

    + +

    + + + + + + + + +<?= $pageTitle ?> - Pokémon Showdown! + + + + + + + + +
    + +
    + +
    + +
    + + + +
    +
    + + +scripts(); ?> tab === 'ladder') { ?> - + diff --git a/website/usermodlog.php b/pokemonshowdown.com/usermodlog.php similarity index 98% rename from website/usermodlog.php rename to pokemonshowdown.com/usermodlog.php index b48567fa58..39c7c8d2e4 100644 --- a/website/usermodlog.php +++ b/pokemonshowdown.com/usermodlog.php @@ -13,7 +13,7 @@ $EMAIL_REGEX = '/(?:[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i'; $lowerstaff = $curuser['group'] == 4 || $curuser['group'] == 5; -$upperstaff = $curuser['group'] == 2 || $curuser['group'] == 6; +$upperstaff = $users->isLeader(); if (!($lowerstaff || $upperstaff)) { die("access denied"); diff --git a/website/users.php b/pokemonshowdown.com/users.php similarity index 84% rename from website/users.php rename to pokemonshowdown.com/users.php index c5cc66abb6..71effe5ac3 100644 --- a/website/users.php +++ b/pokemonshowdown.com/users.php @@ -49,29 +49,40 @@ if ($curuser['group'] == 6) $authLevel = 4; // leader if ($curuser['group'] == 6 && $auth2FA) $authLevel = 5; // leader with 2FA if ($curuser['group'] == 2 && $auth2FA) $authLevel = 6; // admin +if ($authLevel === 6 && $auth2FA && ($curuser['userid'] === 'chaos' || $curuser['userid'] === 'zarel')) $authLevel = 10; $userid = false; $user = false; $formats = array( - 'gen8randombattle' => 'Random Battle', - 'gen8ou' => 'OverUsed', - 'gen8ubers' => 'Ubers', - 'gen8uu' => 'UnderUsed', - 'gen8ru' => 'RarelyUsed', - 'gen8nu' => 'NeverUsed', - 'gen8pu' => 'PU', - 'gen8lc' => 'Little Cup', - 'gen8monotype' => 'Monotype', - 'gen8battlestadiumsingles' => 'Battle Stadium Singles', - 'gen8cap' => 'CAP', - 'gen8randomdoublesbattle' => 'Random Doubles Battle', - 'gen8doublesou' => 'Doubles OU', - 'gen8vgc2020' => 'VGC 2020', - 'gen8balancedhackmons' => 'Balanced Hackmons', - 'gen8mixandmega' => 'Mix and Mega', - 'gen8almostanyability' => 'Almost Any Ability', - 'gen8stabmons' => 'STABmons', - 'gen8nfe' => 'NFE', + 'gen9randombattle' => 'Random Battle', + 'gen9ou' => 'OverUsed', + 'gen9ubers' => 'Ubers', + 'gen9uu' => 'UnderUsed', + 'gen9ru' => 'RarelyUsed', + 'gen9nu' => 'NeverUsed', + 'gen9pu' => 'PU', + 'gen9lc' => 'Little Cup', + 'gen9monotype' => 'Monotype', + 'gen9bssregh' => 'Battle Stadium Singles Regulation H', + 'gen9cap' => 'CAP', + 'gen9randomdoublesbattle' => 'Random Doubles Battle', + 'gen9doublesou' => 'Doubles OU', + 'gen9vgc2024regh' => 'VGC 2024 Regulation H', + 'gen9almostanyability' => 'Almost Any Ability', + 'gen9balancedhackmons' => 'Balanced Hackmons', + 'gen9godlygift' => 'Godly Gift', + 'gen9inheritance' => 'Inheritance', + 'gen9mixandmega' => 'Mix and Mega', + 'gen9partnersincrime' => 'Partners in Crime', + 'gen9sharedpower' => 'Shared Power', + 'gen9stabmons' => 'STABmons', + 'gen9nationaldex' => 'National Dex OU', + 'gen9nationaldexubers' => 'National Dex Ubers', + 'gen9nationaldexuu' => 'National Dex UU', + 'gen9nationaldexmonotype' => 'National Dex Monotype', + 'gen9nationaldexdoubles' => 'National Dex Doubles', + 'gen8randombattle' => '[Gen 8] Random Battle', + 'gen8ou' => '[Gen 8] OU', 'gen7randombattle' => '[Gen 7] Random Battle', 'gen7ou' => '[Gen 7] OU', 'gen6randombattle' => '[Gen 6] Random Battle', @@ -179,11 +190,15 @@ if ($authLevel >= 4 && substr($user['email'] ?? '', -1) === '@') echo '[2FA]'; - if ($user['group'] && $user['group'] != 2 && $authLevel >= 3) { + $canChangeGroup = $user['group'] == 2 ? $authLevel >= 10 : $authLevel >= 3; + + if ($user['group'] && $canChangeGroup) { $csrfOk = (!!$users->csrfCheck() && $authLevel >= 4); if ($csrfOk && isset($_POST['group'])) { $group = intval($_POST['group']); - if ($group != 3 && $group != 4 && $group != 5 && $group != 6) $group = 1; + if ($group != 3 && $group != 4 && $group != 5 && $group != 6 && $group != 1) { + die(" Cannot change to group $group - access denied."); + } $psdb->query("UPDATE ntbb_users SET `group` = ".intval($group)." WHERE userid = '".$psdb->escape($user['userid'])."' LIMIT 1"); $user['group'] = $group; @@ -266,30 +281,39 @@ } else if ($csrfOk && isset($_POST['googlelogin'])) { $email = $_POST['googlelogin']; $remove = ($email === 'remove'); - $psdb->query( - "UPDATE {$psdb->prefix}users SET email = ? WHERE userid = ?", - [$remove ? '' : $email . '@', $user['userid']] - ); + if (!$remove && (strpos($email, '@') === false || strpos($email, '.') === false)) { +?> +
    +

    Invalid e-mail address ""

    +
    +query( + "UPDATE {$psdb->prefix}users SET email = ? WHERE userid = ?", + [$remove ? '' : $email . '@', $user['userid']] + ); - $modlogentry = $remove ? "Login method set to password" : "Login method set to Google " . $email; - $psdb->query( - "INSERT INTO `{$psdb->prefix}usermodlog` (`userid`,`actorid`,`date`,`ip`,`entry`) VALUES (?, ?, ?, ?, ?)", - [$user['userid'], $curuser['userid'], time(), $users->getIp(), $modlogentry] - ); + $modlogentry = $remove ? "Login method set to password" : "Login method set to Google " . $email; + $psdb->query( + "INSERT INTO `{$psdb->prefix}usermodlog` (`userid`,`actorid`,`date`,`ip`,`entry`) VALUES (?, ?, ?, ?, ?)", + [$user['userid'], $curuser['userid'], time(), $users->getIp(), $modlogentry] + ); ?>

    Login method updated

    = 5 && @$_POST['passreset']) { + } + } else if ($csrfOk && $authLevel >= 6 && @$_POST['passreset']) { $token = $users->createPasswordResetToken($user['userid']); ?>

    Use this link:

    -

    - https:///resetpassword/ +

    +
    +

    csrfData(); ?> = 4) { ?> +


    @@ -351,7 +375,7 @@

    = 5) { + if ($authLevel >= 6) { ?>

    csrfData(); ?> @@ -363,7 +387,7 @@ ?> isLeader()) { $csrfOk = false; if ($users->csrfCheck()) { $csrfOk = true; @@ -446,13 +470,14 @@

    ;_;7

    '; } // Ladder - if ($user['userid'] === $curuser['userid']) { + $ladderTourID = str_starts_with($user['userid'], 'lt11'); + if ($user['userid'] === $curuser['userid'] && !$ladderTourID) { if ($users->csrfCheck() && @$_POST['resetLadder']) { $formatLadder = new NTBBLadder(@$_POST['resetLadder']); if (substr($formatLadder->formatid, -7) !== 'current' && substr($formatLadder->formatid, -11) !== 'suspecttest') { @@ -475,7 +500,7 @@ } else { $bufs[$buftype] .= '
    '; if (substr($row['formatid'], -7) !== 'current' && substr($row['formatid'], -11) !== 'suspecttest') { $bufs[$buftype] .= ''; @@ -529,7 +554,7 @@ } ?>
    (more games needed)'; } - if ($user['userid'] === $curuser['userid']) { + if ($user['userid'] === $curuser['userid'] && !$ladderTourID) { $bufs[$buftype] .= '' . $row['w'] . '' . $row['l'] . '