Update to ESLint 9 (#2326)
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run

This finally removes the tslint dependency and switches to eslint.

There are a lot of other changes here, too, to bring the codebase up to
server standards. TSLint never had much in the way of indentation
enforcement.

Not very happy about eslint splitting itself up over 6 dependencies,
or its documentation over three websites, nor how poorly documented the
new flat config is, but I mean, eslint's gonna eslint. Customizing
would be even harder if we tried to use Biome or something. They mostly
seem to go full Prettier.

Also here are some changes to our style rules. In particular:

- Curly brackets (for objects etc) now have spaces inside them. Sorry
  for the huge change. ESLint doesn't support our old style, and most
  projects use Prettier style, so we might as well match them in this way.
  See https://github.com/eslint-stylistic/eslint-stylistic/issues/415

- String + number concatenation is no longer allowed (except in ES3
  code). We otherwise now consistently use template strings for this.
This commit is contained in:
Guangcong Luo 2025-02-25 20:05:32 -08:00 committed by GitHub
parent 3d4de88706
commit a10821ab8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 6892 additions and 5302 deletions

View File

@ -1,16 +0,0 @@
node_modules/
/play.pokemonshowdown.com/data/*
/play.pokemonshowdown.com/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

View File

@ -1,210 +0,0 @@
'use strict';
module.exports = {
"root": true,
"parserOptions": {
"ecmaVersion": 3,
"sourceType": "script"
},
"env": {
"node": true,
"browser": true
},
"globals": {
// Libraries
"_": false, "$": false, "Backbone": false, "d3": false, "html": false, "html4": false, "jQuery": false, "SockJS": false, "ColorThief": false,
// Environment-specific
"fs": false, "gui": false, "ga": false, "macgap": false, "nwWindow": false, "webkitNotifications": false, "nw": false,
// Battle stuff
"Battle": true, "Pokemon": true, "BattleSound": true, "BattleTooltips": true, "BattleLog": true,
"BattleAbilities": false, "BattleAliases": false, "BattleBackdrops": false, "BattleBackdropsFive": false, "BattleBackdropsFour": false, "BattleBackdropsThree": false, "BattleEffects": false,
"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, "BattleStatOptimizer": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false,
"BattleTextParser": false,
// Generic global variables
"Config": false, "BattleSearch": false, "Storage": false, "Dex": false, "DexSearch": false,
"app": false, "toID": false, "toRoomid": false, "toUserid": false, "toName": false, "PSUtils": false, "MD5": false,
"ChatHistory": false, "Topbar": false, "UserList": false,
// Rooms
"Room": false, "BattleRoom": false, "ChatRoom": false, "ConsoleRoom": false, "HTMLRoom": false, "LadderRoom": false, "MainMenuRoom": false, "RoomsRoom": false, "BattlesRoom": false, "TeambuilderRoom": false,
// Tons of popups
"Popup": false, "ForfeitPopup": false, "BracketPopup": false, "LoginPasswordPopup": false, "UserPopup": false, "UserOptionsPopup": false, "UserOptions": false, "TeamPopup": false,
"AvatarsPopup": false, "CreditsPopup": false, "FormatPopup": false, "FormattingPopup": false, "LoginPopup": false,
"MovePopup": false, "SoundsPopup": false, "OptionsPopup": false, "PromptPopup": false, "ProxyPopup": false, "ReconnectPopup": false,
"RegisterPopup": false, "ReplayUploadedPopup": false, "RulesPopup": false, "TabListPopup": false, "TournamentBox": false,
"CustomBackgroundPopup": false,
// Test client
"POKEMON_SHOWDOWN_TESTCLIENT_KEY": false
},
"extends": "eslint:recommended",
"rules": {
"no-cond-assign": ["error", "except-parens"],
"no-console": "off",
"no-constant-condition": "off",
"no-control-regex": "off",
"no-empty": ["error", {"allowEmptyCatch": true}],
"no-inner-declarations": ["error", "functions"],
"no-redeclare": "off",
"valid-jsdoc": "off",
// TODO: actually fix useless escapes
"no-useless-escape": "off",
"array-callback-return": "error",
"complexity": "off",
"consistent-return": "off",
"default-case": "off",
"dot-location": ["error", "property"],
"dot-notation": "off",
"eqeqeq": "off",
"guard-for-in": "off",
"no-caller": "error",
"no-case-declarations": "off",
"no-div-regex": "error",
"no-else-return": "off",
"no-labels": ["error", {"allowLoop": true, "allowSwitch": true}],
"no-eval": "off",
"no-implied-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "warn",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-implicit-coercion": "off",
"no-invalid-this": "off",
"no-iterator": "error",
"no-lone-blocks": "off",
"no-loop-func": "off",
"no-magic-numbers": "off",
"no-multi-spaces": "warn",
"no-multi-str": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-proto": "error",
"no-prototype-builtins": "error",
"no-return-assign": ["error", "except-parens"],
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-useless-call": "error",
"no-useless-concat": "off",
"no-void": "off",
"no-warning-comments": "off",
"no-with": "error",
"radix": ["error", "always"],
"vars-on-top": "off",
"wrap-iife": ["error", "inside"],
"yoda": "off",
"init-declarations": "off",
"no-catch-shadow": "off",
"no-label-var": "error",
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet"],
"no-shadow-restricted-names": "error",
"no-shadow": "off",
"no-undef-init": "off",
"no-undef": ["error", {"typeof": true}],
"no-undefined": "off",
"no-unused-vars": "off",
"no-mixed-requires": "error",
"no-new-require": "error",
"no-path-concat": "off",
"no-process-env": "off",
"no-process-exit": "off",
"no-restricted-modules": ["error", "moment", "request", "sugar"],
"no-sync": "off",
"array-bracket-spacing": ["error", "never"],
"block-spacing": "off",
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
"camelcase": "off",
"comma-spacing": ["error", {"before": false, "after": true}],
"comma-style": ["error", "last"],
"computed-property-spacing": ["error", "never"],
"consistent-this": "off",
"func-names": "off",
"func-style": "off",
"id-length": "off",
"id-match": "off",
"indent": ["error", "tab"],
"key-spacing": "off",
"lines-around-comment": "off",
"max-nested-callbacks": "off",
"max-statements-per-line": "off",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"no-array-constructor": "error",
"no-continue": "off",
"no-inline-comments": "off",
"no-lonely-if": "off",
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1}],
"no-negated-condition": "off",
"no-nested-ternary": "off",
"no-new-object": "error",
"no-spaced-func": "error",
"no-ternary": "off",
"no-trailing-spaces": ["error", {"ignoreComments": false}],
"no-underscore-dangle": "off",
"no-unneeded-ternary": "error",
"object-curly-spacing": ["error", "never"],
"one-var": "off",
"operator-assignment": "off",
"operator-linebreak": ["error", "after"],
"quote-props": "off",
"quotes": "off",
"require-jsdoc": "off",
"semi-spacing": ["error", {"before": false, "after": true}],
"semi": ["error", "always"],
"sort-vars": "off",
"keyword-spacing": ["error", {"before": true, "after": true}],
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
"wrap-regex": "off",
"arrow-parens": ["error", "as-needed"],
"arrow-spacing": ["error", {"before": true, "after": true}],
"no-confusing-arrow": "off",
"no-useless-computed-key": "error",
"no-useless-rename": "error",
"prefer-arrow-callback": "off",
"rest-spread-spacing": ["error", "never"],
"template-curly-spacing": ["error", "never"],
"no-restricted-syntax": ["error", "TaggedTemplateExpression", "ObjectPattern", "ArrayPattern"],
// Rules enabled in the server code, but disabled here
/*
"block-scoped-var": "error",
"callback-return": [2, ["callback", "cb", "done"]],
"comma-dangle": ["error", "always-multiline"],
"curly": ["error", "multi-line", "consistent"],
"eqeqeq": "error",
"no-constant-condition": "error",
"no-floating-decimal": "error",
"no-new": "error",
"no-redeclare": "error",
"no-unused-vars": ["warn", {"args": "none"}],
"no-use-before-define": ["error", {"functions": false, "classes": false}],
"no-var": "error",
"new-cap": ["error", {"newIsCap": true, "capIsNew: false}],
"padded-blocks": ["error", "never"],
"strict": ["error", "global"]
*/
}
};

View File

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [14.x] node-version: [22.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -2,6 +2,5 @@
"editor.formatOnSave": false, "editor.formatOnSave": false,
"showdown.server": "", // e.g., "?~~localhost:8000" "showdown.server": "", // e.g., "?~~localhost:8000"
"showdown.clientUrl": "http://localhost:8080", "showdown.clientUrl": "http://localhost:8080",
"tslint.configFile": "tslint.json",
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@ -1,44 +0,0 @@
'use strict';
const baseRules = Object.assign({}, require('./../.eslintrc.js').rules);
module.exports = {
"root": true,
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "script",
"ecmaFeatures": {
"globalReturn": true
}
},
"env": {
"es6": true,
"node": true
},
"rules": Object.assign(baseRules, {
"comma-dangle": [2, "always-multiline"],
"eqeqeq": 2,
"no-floating-decimal": 2,
"no-new": 2,
"no-redeclare": 2,
"radix": [1, "as-needed"],
"strict": [2, "global"],
"no-unused-vars": [1, {"args": "none"}],
"no-use-before-define": [2, {"functions": false, "classes": false}],
"new-cap": [2, {"newIsCap": true, "capIsNew": false}],
"padded-blocks": [2, "never"],
"arrow-parens": [2, "as-needed"],
"arrow-spacing": [2, {"before": true, "after": true}],
"constructor-super": 2,
"no-class-assign": 2,
"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,
"template-curly-spacing": [2, "never"],
}),
};

View File

@ -15,8 +15,8 @@ if (!fs.existsSync('caches/pokemon-showdown')) {
} }
process.stdout.write("Syncing data from Git repository... "); process.stdout.write("Syncing data from Git repository... ");
child_process.execSync('git pull', {cwd: 'caches/pokemon-showdown'}); child_process.execSync('git pull', { cwd: 'caches/pokemon-showdown' });
child_process.execSync('npm run build', {cwd: 'caches/pokemon-showdown'}); child_process.execSync('npm run build', { cwd: 'caches/pokemon-showdown' });
console.log("DONE"); console.log("DONE");
const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex; const Dex = require('../caches/pokemon-showdown/dist/sim/dex').Dex;
@ -27,7 +27,7 @@ console.log("DONE");
function es3stringify(obj) { function es3stringify(obj) {
const buf = JSON.stringify(obj); const buf = JSON.stringify(obj);
return buf.replace(/\"([A-Za-z][A-Za-z0-9]*)\"\:/g, (fullMatch, key) => ( return buf.replace(/"([A-Za-z][A-Za-z0-9]*)":/g, (fullMatch, key) => (
['return', 'new', 'delete'].includes(key) ? fullMatch : `${key}:` ['return', 'new', 'delete'].includes(key) ? fullMatch : `${key}:`
)); ));
} }
@ -52,10 +52,14 @@ function requireNoCache(pathSpec) {
index = index.concat(Object.keys(Dex.data.Abilities).map(x => x + ' ability')); index = index.concat(Object.keys(Dex.data.Abilities).map(x => x + ' ability'));
index = index.concat(Object.keys(Dex.data.TypeChart).map(x => toID(x) + ' type')); 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(['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([
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')); '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', 'zu', 'lc', 'nfe', 'uber', 'uubl', 'rubl', 'nubl', 'publ', 'zubl', 'cap', 'caplc', 'capnfe',
].map(x => toID(x) + ' tier'));
let BattleArticleTitles = {}; const BattleArticleTitles = {};
try { try {
for (const file of fs.readdirSync('../dex.pokemonshowdown.com/articles/')) { for (const file of fs.readdirSync('../dex.pokemonshowdown.com/articles/')) {
@ -71,7 +75,7 @@ function requireNoCache(pathSpec) {
index.push('' + id + ' article'); index.push('' + id + ' article');
} }
} }
} catch (e) { } catch {
console.log('\n(WARNING: NO ARTICLES)'); console.log('\n(WARNING: NO ARTICLES)');
} }
index.push('pokemon article'); index.push('pokemon article');
@ -79,8 +83,8 @@ function requireNoCache(pathSpec) {
// generate aliases // generate aliases
function generateAlias(id, name, type) { function generateAlias(id, name, type) {
let i = name.lastIndexOf(' '); let offset = name.lastIndexOf(' ');
if (i < 0) i = name.lastIndexOf('-'); if (offset < 0) offset = name.lastIndexOf('-');
if (name.endsWith('-Mega-X') || name.endsWith('-Mega-Y')) { if (name.endsWith('-Mega-X') || name.endsWith('-Mega-Y')) {
index.push('mega' + toID(name.slice(0, -7) + name.slice(-1)) + ' ' + type + ' ' + id + ' 0'); index.push('mega' + toID(name.slice(0, -7) + name.slice(-1)) + ' ' + type + ' ' + id + ' 0');
index.push('m' + toID(name.slice(0, -7) + name.slice(-1)) + ' ' + type + ' ' + id + ' 0'); index.push('m' + toID(name.slice(0, -7) + name.slice(-1)) + ' ' + type + ' ' + id + ' 0');
@ -96,86 +100,86 @@ function requireNoCache(pathSpec) {
index.push('alolan' + toID(name.slice(0, -6)) + ' ' + type + ' ' + id + ' 0'); index.push('alolan' + toID(name.slice(0, -6)) + ' ' + type + ' ' + id + ' 0');
return; return;
} }
let oldI = i; const oldOffset = offset;
if (name === 'Alakazam') i = 5; if (name === 'Alakazam') offset = 5;
if (name === 'Arctovish') i = 5; if (name === 'Arctovish') offset = 5;
if (name === 'Arctozolt') i = 5; if (name === 'Arctozolt') offset = 5;
if (name === 'Articuno') i = 5; if (name === 'Articuno') offset = 5;
if (name === 'Breloom') i = 3; if (name === 'Breloom') offset = 3;
if (name === 'Bronzong') i = 4; if (name === 'Bronzong') offset = 4;
if (name === 'Celebi') i = 4; if (name === 'Celebi') offset = 4;
if (name === 'Charizard') i = 5; if (name === 'Charizard') offset = 5;
if (name === 'Donphan') i = 3; if (name === 'Donphan') offset = 3;
if (name === 'Dracovish') i = 5; if (name === 'Dracovish') offset = 5;
if (name === 'Dracozolt') i = 5; if (name === 'Dracozolt') offset = 5;
if (name === 'Dragapult') i = 5; if (name === 'Dragapult') offset = 5;
if (name === 'Dusclops') i = 3; if (name === 'Dusclops') offset = 3;
if (name === 'Electabuzz') i = 6; if (name === 'Electabuzz') offset = 6;
if (name === 'Exeggutor') i = 2; if (name === 'Exeggutor') offset = 2;
if (name === 'Garchomp') i = 3; if (name === 'Garchomp') offset = 3;
if (name === 'Hariyama') i = 4; if (name === 'Hariyama') offset = 4;
if (name === 'Magearna') i = 2; if (name === 'Magearna') offset = 2;
if (name === 'Magnezone') i = 5; if (name === 'Magnezone') offset = 5;
if (name === 'Mamoswine') i = 4; if (name === 'Mamoswine') offset = 4;
if (name === 'Moltres') i = 3; if (name === 'Moltres') offset = 3;
if (name === 'Nidoking') i = 4; if (name === 'Nidoking') offset = 4;
if (name === 'Nidoqueen') i = 4; if (name === 'Nidoqueen') offset = 4;
if (name === 'Nidorina') i = 4; if (name === 'Nidorina') offset = 4;
if (name === 'Nidorino') i = 4; if (name === 'Nidorino') offset = 4;
if (name === 'Regice') i = 3; if (name === 'Regice') offset = 3;
if (name === 'Regidrago') i = 4; if (name === 'Regidrago') offset = 4;
if (name === 'Regieleki') i = 4; if (name === 'Regieleki') offset = 4;
if (name === 'Regigigas') i = 4; if (name === 'Regigigas') offset = 4;
if (name === 'Regirock') i = 4; if (name === 'Regirock') offset = 4;
if (name === 'Registeel') i = 4; if (name === 'Registeel') offset = 4;
if (name === 'Slowbro') i = 4; if (name === 'Slowbro') offset = 4;
if (name === 'Slowking') i = 4; if (name === 'Slowking') offset = 4;
if (name === 'Starmie') i = 4; if (name === 'Starmie') offset = 4;
if (name === 'Tyranitar') i = 6; if (name === 'Tyranitar') offset = 6;
if (name === 'Zapdos') i = 3; if (name === 'Zapdos') offset = 3;
if (name === 'Acupressure') i = 3; if (name === 'Acupressure') offset = 3;
if (name === 'Aromatherapy') i = 5; if (name === 'Aromatherapy') offset = 5;
if (name === 'Boomburst') i = 4; if (name === 'Boomburst') offset = 4;
if (name === 'Crabhammer') i = 4; if (name === 'Crabhammer') offset = 4;
if (name === 'Discharge') i = 3; if (name === 'Discharge') offset = 3;
if (name === 'Earthquake') i = 5; if (name === 'Earthquake') offset = 5;
if (name === 'Extrasensory') i = 5; if (name === 'Extrasensory') offset = 5;
if (name === 'Flamethrower') i = 5; if (name === 'Flamethrower') offset = 5;
if (name === 'Headbutt') i = 4; if (name === 'Headbutt') offset = 4;
if (name === 'Moonblast') i = 4; if (name === 'Moonblast') offset = 4;
if (name === 'Moonlight') i = 4; if (name === 'Moonlight') offset = 4;
if (name === 'Overheat') i = 4; if (name === 'Overheat') offset = 4;
if (name === 'Outrage') i = 3; if (name === 'Outrage') offset = 3;
if (name === 'Octazooka') i = 4; if (name === 'Octazooka') offset = 4;
if (name === 'Payback') i = 3; if (name === 'Payback') offset = 3;
if (name === 'Psyshock') i = 3; if (name === 'Psyshock') offset = 3;
if (name === 'Psywave') i = 3; if (name === 'Psywave') offset = 3;
if (name === 'Rototiller') i = 4; if (name === 'Rototiller') offset = 4;
if (name === 'Rollout') i = 4; if (name === 'Rollout') offset = 4;
if (name === 'Safeguard') i = 4; if (name === 'Safeguard') offset = 4;
if (name === 'Sandstorm') i = 4; if (name === 'Sandstorm') offset = 4;
if (name === 'Smokescreen') i = 5; if (name === 'Smokescreen') offset = 5;
if (name === 'Stockpile') i = 5; if (name === 'Stockpile') offset = 5;
if (name === 'Steamroller') i = 5; if (name === 'Steamroller') offset = 5;
if (name === 'Superpower') i = 5; if (name === 'Superpower') offset = 5;
if (name === 'Supersonic') i = 5; if (name === 'Supersonic') offset = 5;
if (name === 'Synchronoise') i = 7; if (name === 'Synchronoise') offset = 7;
if (name === 'Tailwind') i = 4; if (name === 'Tailwind') offset = 4;
if (name === 'Telekinesis') i = 4; if (name === 'Telekinesis') offset = 4;
if (name === 'Teleport') i = 4; if (name === 'Teleport') offset = 4;
if (name === 'Thunderbolt') i = 7; if (name === 'Thunderbolt') offset = 7;
if (name === 'Twineedle') i = 3; if (name === 'Twineedle') offset = 3;
if (name === 'Uproar') i = 2; if (name === 'Uproar') offset = 2;
if (name === 'Venoshock') i = 4; if (name === 'Venoshock') offset = 4;
if (name === 'Whirlpool') i = 5; if (name === 'Whirlpool') offset = 5;
if (name === 'Whirlwind') i = 5; if (name === 'Whirlwind') offset = 5;
let acronym; let acronym;
if (oldI < 0 && i > 0) { if (oldOffset < 0 && offset > 0) {
acronym = toID(name.charAt(0) + name.slice(i)); acronym = toID(name.charAt(0) + name.slice(offset));
} }
if (i < 0) return; if (offset < 0) return;
index.push('' + toID(name.slice(i)) + ' ' + type + ' ' + id + ' ' + toID(name.slice(0, i)).length); index.push('' + toID(name.slice(offset)) + ' ' + type + ' ' + id + ' ' + toID(name.slice(0, offset)).length);
if (name.startsWith('Hidden Power ')) { if (name.startsWith('Hidden Power ')) {
acronym = 'hp' + toID(name.substr(13)); acronym = 'hp' + toID(name.substr(13));
index.push('' + acronym + ' ' + type + ' ' + id + ' 0'); index.push('' + acronym + ' ' + type + ' ' + id + ' 0');
@ -200,8 +204,8 @@ function requireNoCache(pathSpec) {
index.push('cuno ' + type + ' ' + id + ' 4'); index.push('cuno ' + type + ' ' + id + ' 4');
} }
let i2 = name.lastIndexOf(' ', i - 1); let i2 = name.lastIndexOf(' ', offset - 1);
if (i2 < 0) i2 = name.lastIndexOf('-', i - 1); if (i2 < 0) i2 = name.lastIndexOf('-', offset - 1);
if (name === 'Zen Headbutt') i2 = 8; if (name === 'Zen Headbutt') i2 = 8;
if (i2 >= 0) { if (i2 >= 0) {
index.push('' + toID(name.slice(i2)) + ' ' + type + ' ' + id + ' ' + toID(name.slice(0, i2)).length); index.push('' + toID(name.slice(i2)) + ' ' + type + ' ' + id + ' ' + toID(name.slice(0, i2)).length);
@ -248,8 +252,7 @@ function requireNoCache(pathSpec) {
index[index.indexOf('ditto pokemon')] = 'ditto egggroup'; index[index.indexOf('ditto pokemon')] = 'ditto egggroup';
index[index.indexOf('ditto egggroup')] = 'ditto pokemon'; index[index.indexOf('ditto egggroup')] = 'ditto pokemon';
const BattleSearchIndex = index.map(x => {
let BattleSearchIndex = index.map(x => {
x = x.split(' '); x = x.split(' ');
if (x.length > 3) { if (x.length > 3) {
x[3] = Number(x[3]); x[3] = Number(x[3]);
@ -258,7 +261,7 @@ function requireNoCache(pathSpec) {
return x; return x;
}); });
let BattleSearchIndexOffset = BattleSearchIndex.map((entry, i) => { const BattleSearchIndexOffset = BattleSearchIndex.map(entry => {
const id = entry[0]; const id = entry[0];
let name = ''; let name = '';
switch (entry[1]) { switch (entry[1]) {
@ -281,13 +284,15 @@ function requireNoCache(pathSpec) {
return ''; return '';
}); });
let BattleSearchCountIndex = {}; const BattleSearchCountIndex = {};
for (const type in Dex.data.TypeChart) { for (const type in Dex.data.TypeChart) {
BattleSearchCountIndex[type + ' move'] = Object.keys(Dex.data.Moves).filter(id => (Dex.data.Moves[id].type === type)).length; BattleSearchCountIndex[type + ' move'] = Object.keys(Dex.data.Moves)
.filter(id => (Dex.data.Moves[id].type === type)).length;
} }
for (const type in Dex.data.TypeChart) { for (const type in Dex.data.TypeChart) {
BattleSearchCountIndex[type + ' pokemon'] = Object.keys(Dex.data.Pokedex).filter(id => (Dex.data.Pokedex[id].types.indexOf(type) >= 0)).length; BattleSearchCountIndex[type + ' pokemon'] = Object.keys(Dex.data.Pokedex)
.filter(id => (Dex.data.Pokedex[id].types.indexOf(type) >= 0)).length;
} }
let buf = '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n'; let buf = '// DO NOT EDIT - automatically built with build-tools/build-indexes\n\n';
@ -369,7 +374,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
const species = Dex.mod(gen).species.get(id); const species = Dex.mod(gen).species.get(id);
const baseSpecies = Dex.mod(gen).species.get(species.baseSpecies); const baseSpecies = Dex.mod(gen).species.get(species.baseSpecies);
if (species.gen > genNum) continue; if (species.gen > genNum) continue;
const tier = (() => { const speciesTier = (() => {
if (isMetBattle) { if (isMetBattle) {
let tier = species.tier; let tier = species.tier;
if (species.isNonstandard) { if (species.isNonstandard) {
@ -416,7 +421,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
return tier; return tier;
} }
if (isLetsGo) { if (isLetsGo) {
let validNum = (baseSpecies.num <= 151 && species.num >= 1) || [808, 809].includes(baseSpecies.num); const validNum = (baseSpecies.num <= 151 && species.num >= 1) || [808, 809].includes(baseSpecies.num);
if (!validNum) return 'Illegal'; if (!validNum) return 'Illegal';
if (species.forme && !['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme)) return 'Illegal'; if (species.forme && !['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme)) return 'Illegal';
if (species.name === 'Pikachu-Alola') return 'Illegal'; if (species.name === 'Pikachu-Alola') return 'Illegal';
@ -455,7 +460,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
return species.tier; return species.tier;
})(); })();
overrideTier[species.id] = tier; overrideTier[species.id] = speciesTier;
if (species.forme) { if (species.forme) {
if ( if (
[ [
@ -467,8 +472,8 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
} }
if (!tierTable[tier]) tierTable[tier] = []; if (!tierTable[speciesTier]) tierTable[speciesTier] = [];
tierTable[tier].push(id); tierTable[speciesTier].push(id);
if (genNum === 9) { if (genNum === 9) {
const ubersUU = Dex.formats.get(gen + 'ubersuu'); const ubersUU = Dex.formats.get(gen + 'ubersuu');
@ -610,11 +615,16 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
if (gen === 'gen4') { if (gen === 'gen4') {
return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "OU", "(OU)", "UUBL", "UU", "NUBL", "NU", "NFE", "LC"]; return ["CAP", "CAP NFE", "CAP LC", "AG", "Uber", "OU", "(OU)", "UUBL", "UU", "NUBL", "NU", "NFE", "LC"];
} }
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"]; 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) { for (const tier of tierOrder) {
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}) { 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; let usedTier = tier;
if (usedTier === "(DUU)") usedTier = "DNU"; if (usedTier === "(DUU)") usedTier = "DNU";
formatSlices[usedTier] = tiers.length; formatSlices[usedTier] = tiers.length;
@ -907,7 +917,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
if (available) available = !gen4HMs.has(moveid); if (available) available = !gen4HMs.has(moveid);
let minUpperGen = available ? 5 : Math.min( const minUpperGen = available ? 5 : Math.min(
...gens.filter(gen => gen > 4) ...gens.filter(gen => gen > 4)
); );
legalGens += '0123456789'.slice(minUpperGen); legalGens += '0123456789'.slice(minUpperGen);
@ -1001,7 +1011,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
if (available) available = !gen4HMs.has(moveid); if (available) available = !gen4HMs.has(moveid);
let minUpperGen = available ? 5 : Math.min( const minUpperGen = available ? 5 : Math.min(
...gens.filter(gen => gen > 4) ...gens.filter(gen => gen > 4)
); );
legalGens += '012345678'.slice(minUpperGen); legalGens += '012345678'.slice(minUpperGen);
@ -1044,7 +1054,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
if (available) available = !gen4HMs.has(moveid); if (available) available = !gen4HMs.has(moveid);
let minUpperGen = available ? 5 : Math.min( const minUpperGen = available ? 5 : Math.min(
...gens.filter(gen => gen > 4) ...gens.filter(gen => gen > 4)
); );
legalGens += '0123456789'.slice(minUpperGen); legalGens += '0123456789'.slice(minUpperGen);
@ -1090,7 +1100,7 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
if (available) available = !gen4HMs.has(moveid); if (available) available = !gen4HMs.has(moveid);
let minUpperGen = available ? 5 : Math.min( const minUpperGen = available ? 5 : Math.min(
...gens.filter(gen => gen > 4) ...gens.filter(gen => gen > 4)
); );
legalGens += '0123456789'.slice(minUpperGen); legalGens += '0123456789'.slice(minUpperGen);
@ -1113,8 +1123,12 @@ process.stdout.write("Building `data/teambuilder-tables.js`... ");
} }
// Client relevant data that should be overriden by past gens and mods // Client relevant data that should be overriden by past gens and mods
const overrideSpeciesKeys = ['abilities', 'baseStats', 'cosmeticFormes', 'isNonstandard', 'requiredItems', 'types', 'unreleasedHidden']; const overrideSpeciesKeys = [
const overrideMoveKeys = ['accuracy', 'basePower', 'category', 'desc', 'flags', 'isNonstandard', 'pp', 'priority', 'shortDesc', 'target', 'type']; '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 overrideAbilityKeys = ['desc', 'flags', 'isNonstandard', 'rating', 'shortDesc'];
const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'naturalGift', 'shortDesc']; const overrideItemKeys = ['desc', 'fling', 'isNonstandard', 'naturalGift', 'shortDesc'];

View File

@ -53,11 +53,11 @@ function updateLearnsets(callback) {
); );
} }
newLearnsetsG6[speciesid] = {learnset: newLearnset}; newLearnsetsG6[speciesid] = { learnset: newLearnset };
} }
const buf = []; const buf = [];
const pokemonList = Object.keys(Pokedex).map(speciesId => Pokedex[speciesId]).sort(function (a, b) { const pokemonList = Object.keys(Pokedex).map(speciesId => Pokedex[speciesId]).sort((a, b) => {
// Missingno. goes first (zeroth); afterwards, CAP in descending dex order (increasingly negative) // Missingno. goes first (zeroth); afterwards, CAP in descending dex order (increasingly negative)
// Finally, standard Pokémon in ascending dex order // Finally, standard Pokémon in ascending dex order
if (a.num <= 0 && b.num > 0) return -1; if (a.num <= 0 && b.num > 0) return -1;
@ -89,11 +89,9 @@ try {
// It doesn't exist currently, but it will by the end of the script execution. // It doesn't exist currently, but it will by the end of the script execution.
// Any other error is unacceptable and will throw. // Any other error is unacceptable and will throw.
} }
try { {
updateStats = fs.statSync(thisFile); updateStats = fs.statSync(thisFile);
updateMTime = updateStats.mtime.getTime(); updateMTime = updateStats.mtime.getTime();
} catch (err) {
throw err; // !!
} }
// update learnsets-g6 // update learnsets-g6
@ -105,7 +103,7 @@ let learnsetsG6ToUpdate = true;
try { try {
learnsetsStats = fs.statSync(path.join(rootDir, 'data', 'learnsets.js')); learnsetsStats = fs.statSync(path.join(rootDir, 'data', 'learnsets.js'));
} catch (err) { } catch {
// Couldn't find learnsets.js, but that's not the end of the world: skip to next task. // Couldn't find learnsets.js, but that's not the end of the world: skip to next task.
console.error("Couldn't find `data/learnsets.js`. Task aborted."); console.error("Couldn't find `data/learnsets.js`. Task aborted.");
learnsetsG6ToUpdate = false; learnsetsG6ToUpdate = false;
@ -124,10 +122,13 @@ if (learnsetsG6ToUpdate) {
} }
} }
if (learnsetsG6ToUpdate && (!indexStats || !learnsetsG6Stats || indexMTime < updateMTime || indexMTime < learnsetsStats.mtime.getTime() || indexMTime < learnsetsG6Stats.mtime.getTime())) { if (learnsetsG6ToUpdate && (
!indexStats || !learnsetsG6Stats || indexMTime < updateMTime || indexMTime < learnsetsStats.mtime.getTime() ||
indexMTime < learnsetsG6Stats.mtime.getTime()
)) {
// Only merge learnsets.js with learnsets-g6.js if any of those files, or this one, have been modified recently (or if we don't know what "recently" means) // Only merge learnsets.js with learnsets-g6.js if any of those files, or this one, have been modified recently (or if we don't know what "recently" means)
updateLearnsets(function (err) { updateLearnsets(err => {
if (err) { if (err) {
let stack = err.stack || ''; let stack = err.stack || '';
stack = "File `data/learnsets-g6` failed to update.\n" + stack; stack = "File `data/learnsets-g6` failed to update.\n" + stack;

View File

@ -21,7 +21,6 @@ exports.BattlePokemonSprites = {
substitute:{exists:false, front:{w:34, h:39}, back:{w:37, h:38}}, substitute:{exists:false, front:{w:34, h:39}, back:{w:37, h:38}},
`; `;
let g5buf = `/* let g5buf = `/*
DO NOT EDIT DO NOT EDIT
@ -31,26 +30,26 @@ THIS FILE IS AUTOGENERATED BY ./build-tools/build-minidex
exports.BattlePokemonSpritesBW = { exports.BattlePokemonSpritesBW = {
`; `;
function sizeObj(path) { function sizeObj(objPath) {
try { try {
let size = imageSize(path); const size = imageSize(objPath);
return { return {
w: size.width, w: size.width,
h: size.height, h: size.height,
}; };
} catch (e) {} } catch {}
} }
function updateSizes() { function updateSizes() {
for (let baseid in Dex.data.Pokedex) { for (const baseid in Dex.data.Pokedex) {
let species = Dex.species.get(baseid); const species = Dex.species.get(baseid);
for (let formeName of [''].concat(species.cosmeticFormes || [])) { for (const formeName of [''].concat(species.cosmeticFormes || [])) {
let spriteid = species.spriteid; let spriteid = species.spriteid;
if (formeName) spriteid += '-' + toID(formeName).slice(species.id.length); if (formeName) spriteid += '-' + toID(formeName).slice(species.id.length);
let id = toID(spriteid); const id = toID(spriteid);
{ {
let row = {num: species.num}; const row = { num: species.num };
const frontSize = sizeObj('sprites/ani/' + spriteid + '.gif'); const frontSize = sizeObj('sprites/ani/' + spriteid + '.gif');
if (frontSize) row.front = frontSize; if (frontSize) row.front = frontSize;
const frontSizeF = sizeObj('sprites/ani/' + spriteid + '-f.gif'); const frontSizeF = sizeObj('sprites/ani/' + spriteid + '-f.gif');
@ -65,7 +64,7 @@ function updateSizes() {
} }
{ {
let g5row = {num: species.num}; const g5row = { num: species.num };
const frontSize = sizeObj('sprites/gen5ani/' + spriteid + '.gif'); const frontSize = sizeObj('sprites/gen5ani/' + spriteid + '.gif');
if (frontSize) g5row.front = frontSize; if (frontSize) g5row.front = frontSize;
const frontSizeF = sizeObj('sprites/gen5ani/' + spriteid + '-f.gif'); const frontSizeF = sizeObj('sprites/gen5ani/' + spriteid + '-f.gif');
@ -99,6 +98,6 @@ if (fs.existsSync('sprites/ani/')) {
try { try {
fs.unlinkSync('data/pokedex-mini.js'); fs.unlinkSync('data/pokedex-mini.js');
fs.unlinkSync('data/pokedex-mini-bw.js'); fs.unlinkSync('data/pokedex-mini-bw.js');
} catch (e) {} } catch {}
console.log('SKIPPED'); console.log('SKIPPED');
} }

View File

@ -18,7 +18,7 @@ const sourceMap = require('source-map');
const VERBOSE = false; const VERBOSE = false;
function outputFileSync(filePath, res, opts) { function outputFileSync(filePath, res, opts) {
fs.mkdirSync(path.dirname(filePath), {recursive: true}); fs.mkdirSync(path.dirname(filePath), { recursive: true });
// we've requested explicit sourcemaps to be written to disk // we've requested explicit sourcemaps to be written to disk
if ( if (
@ -35,15 +35,16 @@ function outputFileSync(filePath, res, opts) {
fs.writeFileSync(filePath, res.code); fs.writeFileSync(filePath, res.code);
} }
function slash(path) { function slash(filePath) {
const isExtendedLengthPath = /^\\\\\?\\/.test(path); const isExtendedLengthPath = /^\\\\\?\\/.test(filePath);
const hasNonAscii = /[^\u0000-\u0080]+/.test(path); // eslint-disable-next-line no-control-regex
const hasNonAscii = /[^\u0000-\u0080]+/.test(filePath);
if (isExtendedLengthPath || hasNonAscii) { if (isExtendedLengthPath || hasNonAscii) {
return path; return filePath;
} }
return path.replace(/\\/g, '/'); return filePath.replace(/\\/g, '/');
} }
async function combineResults(fileResults, sourceMapOptions, opts) { async function combineResults(fileResults, sourceMapOptions, opts) {
@ -64,7 +65,7 @@ async function combineResults(fileResults, sourceMapOptions, opts) {
const consumer = await new sourceMap.SourceMapConsumer(result.map); const consumer = await new sourceMap.SourceMapConsumer(result.map);
const sources = new Set(); const sources = new Set();
consumer.eachMapping(function (mapping) { consumer.eachMapping(mapping => {
if (mapping.source != null) sources.add(mapping.source); if (mapping.source != null) sources.add(mapping.source);
map.addMapping({ map.addMapping({
@ -74,9 +75,8 @@ async function combineResults(fileResults, sourceMapOptions, opts) {
}, },
source: mapping.source, source: mapping.source,
original: original:
mapping.source == null mapping.source == null ?
? null null : {
: {
line: mapping.originalLine, line: mapping.originalLine,
column: mapping.originalColumn, column: mapping.originalColumn,
}, },
@ -100,19 +100,16 @@ async function combineResults(fileResults, sourceMapOptions, opts) {
code += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + base64; code += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + base64;
} }
return { return { map, code };
map: map,
code: code,
};
} }
function noRebuildNeeded(src, dest) { function noRebuildNeeded(src, dest) {
try { try {
const srcStat = fs.statSync(src, {throwIfNoEntry: false}); const srcStat = fs.statSync(src, { throwIfNoEntry: false });
if (!srcStat) return true; if (!srcStat) return true;
const destStat = fs.statSync(dest); const destStat = fs.statSync(dest);
if (srcStat.ctimeMs < destStat.ctimeMs) return true; if (srcStat.ctimeMs < destStat.ctimeMs) return true;
} catch (e) {} } catch {}
return false; return false;
} }
@ -153,7 +150,7 @@ function compileToDir(srcDir, destDir, opts = {}) {
} }
function handle(src, base) { function handle(src, base) {
const stat = fs.statSync(src, {throwIfNoEntry: false}); const stat = fs.statSync(src, { throwIfNoEntry: false });
if (!stat) return 0; if (!stat) return 0;
@ -179,7 +176,7 @@ function compileToDir(srcDir, destDir, opts = {}) {
} }
let total = 0; let total = 0;
fs.mkdirSync(destDir, {recursive: true}); fs.mkdirSync(destDir, { recursive: true });
const srcDirs = typeof srcDir === 'string' ? [srcDir] : srcDir; const srcDirs = typeof srcDir === 'string' ? [srcDir] : srcDir;
for (const dir of srcDirs) total += handle(dir); for (const dir of srcDirs) total += handle(dir);
if (incremental) opts.incremental = true; // incredibly dumb hack to preserve the option if (incremental) opts.incremental = true; // incredibly dumb hack to preserve the option

View File

@ -3,7 +3,3 @@ cd "$(dirname "$0")"
cd .. cd ..
cp config/config.js config/config.js.old cp config/config.js config/config.js.old
cp config/head-custom.html config/head-custom.html.old cp config/head-custom.html config/head-custom.html.old
mv -f config/config-test.js config/config.js
mv -f config/head-custom-test.html config/head-custom.html
mv -i config/config.js.old config/config-test.js
mv -i config/head-custom.html.old config/head-custom-test.html

View File

@ -20,9 +20,10 @@ process.chdir(rootDir);
const AUTOCONFIG_START = '/*** Begin automatically generated configuration ***/'; const AUTOCONFIG_START = '/*** Begin automatically generated configuration ***/';
const AUTOCONFIG_END = '/*** End automatically generated configuration ***/'; const AUTOCONFIG_END = '/*** End automatically generated configuration ***/';
const UTF8 = { encoding: 'utf8' };
function escapeRegex(string) { function escapeRegex(string) {
return string.replace(/[\/\*\.]/g, '\\$&'); return string.replace(/[/*.]/g, '\\$&');
} }
/********************************************************* /*********************************************************
@ -42,7 +43,7 @@ try {
}); });
const origin = ('' + commit).trim(); const origin = ('' + commit).trim();
version += ` (${head.slice(0, 8)}${head !== origin ? `/${origin.slice(0, 8)}` : ''})`; version += ` (${head.slice(0, 8)}${head !== origin ? `/${origin.slice(0, 8)}` : ''})`;
} catch (e) {} } catch {}
const routes = JSON.parse(fs.readFileSync('config/routes.json')); const routes = JSON.parse(fs.readFileSync('config/routes.json'));
const autoconfigRegex = new RegExp(`${escapeRegex(AUTOCONFIG_START)}[^]+${escapeRegex(AUTOCONFIG_END)}`); const autoconfigRegex = new RegExp(`${escapeRegex(AUTOCONFIG_START)}[^]+${escapeRegex(AUTOCONFIG_END)}`);
@ -59,7 +60,7 @@ Config.routes = {
${AUTOCONFIG_END}`; ${AUTOCONFIG_END}`;
// remove old automatically generated configuration and add the new one // remove old automatically generated configuration and add the new one
let configBuf = fs.readFileSync('config/config.js', {encoding: 'utf8'}); let configBuf = fs.readFileSync('config/config.js', UTF8);
if (autoconfigRegex.test(configBuf)) { if (autoconfigRegex.test(configBuf)) {
configBuf = configBuf.replace(autoconfigRegex, autoconfig); configBuf = configBuf.replace(autoconfigRegex, autoconfig);
} else { } else {
@ -74,11 +75,12 @@ console.log("DONE");
process.stdout.write("Compiling TS files... "); process.stdout.write("Compiling TS files... ");
let compileStartTime = process.hrtime(); const compileStartTime = process.hrtime();
let compiledFiles = 0; let compiledFiles = 0;
// Babel can't find babelrc if we try to compile stuff in caches/pokemon-showdown/ fsr // Babel can't find babelrc if we try to compile stuff in caches/pokemon-showdown/ fsr
let compileOpts = Object.assign(eval('(' + fs.readFileSync('.babelrc') + ')'), { // eslint-disable-next-line no-eval
const compileOpts = Object.assign(eval('(' + fs.readFileSync('.babelrc') + ')'), {
babelrc: false, babelrc: false,
incremental: true, incremental: true,
ignore: ['play.pokemonshowdown.com/src/battle-animations.js', 'play.pokemonshowdown.com/src/battle-animations-moves.js'], ignore: ['play.pokemonshowdown.com/src/battle-animations.js', 'play.pokemonshowdown.com/src/battle-animations-moves.js'],
@ -95,7 +97,7 @@ if (process.argv[2] === 'full') {
fs.statSync('play.pokemonshowdown.com/data/graphics.js'); fs.statSync('play.pokemonshowdown.com/data/graphics.js');
// graphics.js exists, recompile it // graphics.js exists, recompile it
delete compileOpts.ignore; delete compileOpts.ignore;
} catch (e) {} } catch {}
} }
compiledFiles += compiler.compileToDir(`play.pokemonshowdown.com/src`, `play.pokemonshowdown.com/js`, compileOpts); compiledFiles += compiler.compileToDir(`play.pokemonshowdown.com/src`, `play.pokemonshowdown.com/js`, compileOpts);
@ -148,18 +150,18 @@ function addCachebuster(_, attr, url, urlQuery) {
let hash = Math.random(); // just in case creating the hash fails let hash = Math.random(); // just in case creating the hash fails
try { try {
const filename = url.slice(1).replace('/' + routes.client + '/', ''); const filename = url.slice(1).replace('/' + routes.client + '/', '');
const fstr = fs.readFileSync(filename, {encoding: 'utf8'}); const fstr = fs.readFileSync(filename, UTF8);
hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8); hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8);
} catch (e) {} } catch {}
return attr + '="' + url + '?' + hash + '"'; return attr + '="' + url + '?' + hash + '"';
} else { } else {
// hardcoded to Replays rn; TODO: generalize // hardcoded to Replays rn; TODO: generalize
let hash; let hash;
try { try {
const fstr = fs.readFileSync('replay.pokemonshowdown.com/' + url, {encoding: 'utf8'}); const fstr = fs.readFileSync('replay.pokemonshowdown.com/' + url, UTF8);
hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8); hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8);
} catch (e) {} } catch {}
return attr + '="' + url + '?' + (hash || 'v1') + '"'; return attr + '="' + url + '?' + (hash || 'v1') + '"';
} }
@ -169,13 +171,13 @@ function addCachebuster(_, attr, url, urlQuery) {
} }
// add hashes to js and css files and rewrite URLs // add hashes to js and css files and rewrite URLs
let indexContents = fs.readFileSync('play.pokemonshowdown.com/index.template.html', {encoding: 'utf8'}); let indexContents = fs.readFileSync('play.pokemonshowdown.com/index.template.html', UTF8);
indexContents = indexContents.replace(URL_REGEX, addCachebuster); indexContents = indexContents.replace(URL_REGEX, addCachebuster);
let preactIndexContents = fs.readFileSync('play.pokemonshowdown.com/preactalpha.template.html', {encoding: 'utf8'}); let preactIndexContents = fs.readFileSync('play.pokemonshowdown.com/preactalpha.template.html', UTF8);
preactIndexContents = preactIndexContents.replace(URL_REGEX, addCachebuster); preactIndexContents = preactIndexContents.replace(URL_REGEX, addCachebuster);
let crossprotocolContents = fs.readFileSync('play.pokemonshowdown.com/crossprotocol.template.html', {encoding: 'utf8'}); let crossprotocolContents = fs.readFileSync('play.pokemonshowdown.com/crossprotocol.template.html', UTF8);
crossprotocolContents = crossprotocolContents.replace(URL_REGEX, addCachebuster); crossprotocolContents = crossprotocolContents.replace(URL_REGEX, addCachebuster);
let replayEmbedContents = fs.readFileSync('play.pokemonshowdown.com/js/replay-embed.template.js', {encoding: 'utf8'}); let replayEmbedContents = fs.readFileSync('play.pokemonshowdown.com/js/replay-embed.template.js', UTF8);
replayEmbedContents = replayEmbedContents.replace(/play\.pokemonshowdown\.com/g, routes.client); replayEmbedContents = replayEmbedContents.replace(/play\.pokemonshowdown\.com/g, routes.client);
// add news, only if it's actually likely to exist // add news, only if it's actually likely to exist
@ -199,11 +201,11 @@ indexContents = indexContents.replace(/<!-- news -->/g, news);
let indexContents2 = ''; let indexContents2 = '';
try { try {
let indexContentsOld = indexContents; const indexContentsOld = indexContents;
indexContents = indexContents.replace(/<!-- head custom -->/g, '' + fs.readFileSync('config/head-custom.html')); indexContents = indexContents.replace(/<!-- head custom -->/g, '' + fs.readFileSync('config/head-custom.html'));
indexContents2 = indexContentsOld.replace(/<!-- head custom -->/g, '' + fs.readFileSync('config/head-custom-test.html')); indexContents2 = indexContentsOld
indexContents2 = indexContents2.replace(/src="\/\/play.pokemonshowdown.com\/config\/config.js\?[a-z0-9]*"/, 'src="//play.pokemonshowdown.com/config/config-test.js?4"'); .replace(/<!-- head custom -->/g, '' + fs.readFileSync('config/head-custom-test.html'));
} catch (e) {} } catch {}
fs.writeFileSync('play.pokemonshowdown.com/index.html', indexContents); fs.writeFileSync('play.pokemonshowdown.com/index.html', indexContents);
if (indexContents2) { if (indexContents2) {
@ -213,7 +215,7 @@ fs.writeFileSync('play.pokemonshowdown.com/preactalpha.html', preactIndexContent
fs.writeFileSync('play.pokemonshowdown.com/crossprotocol.html', crossprotocolContents); fs.writeFileSync('play.pokemonshowdown.com/crossprotocol.html', crossprotocolContents);
fs.writeFileSync('play.pokemonshowdown.com/js/replay-embed.js', replayEmbedContents); fs.writeFileSync('play.pokemonshowdown.com/js/replay-embed.js', replayEmbedContents);
let replaysContents = fs.readFileSync('replay.pokemonshowdown.com/index.template.php', {encoding: 'utf8'}); let replaysContents = fs.readFileSync('replay.pokemonshowdown.com/index.template.php', UTF8);
replaysContents = replaysContents.replace(URL_REGEX, addCachebuster); replaysContents = replaysContents.replace(URL_REGEX, addCachebuster);
fs.writeFileSync('replay.pokemonshowdown.com/index.php', replaysContents); fs.writeFileSync('replay.pokemonshowdown.com/index.php', replaysContents);

View File

@ -6,4 +6,4 @@ This directory is for caches. Everything here should be safe to delete.
Things cached here: Things cached here:
- `pokemon-showdown` a checkout of the server repo, used in the build process (mostly for stuff in `play.pokemonshowdown.com/data/`) - `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 - `eslintcache.json` eslint cache files

View File

@ -5,7 +5,7 @@ var Config = Config || {};
Config.bannedHosts = ['cool.jit.su', 'pokeball-nixonserver.rhcloud.com']; Config.bannedHosts = ['cool.jit.su', 'pokeball-nixonserver.rhcloud.com'];
Config.whitelist = [ Config.whitelist = [
'wikipedia.org', 'wikipedia.org'
// The full list is maintained outside of this repository so changes to it // The full list is maintained outside of this repository so changes to it
// don't clutter the commit log. Feel free to copy our list for your own // don't clutter the commit log. Feel free to copy our list for your own

435
eslint-ps-standard.mjs Normal file
View File

@ -0,0 +1,435 @@
/**
* Pokemon Showdown standard style
*
* This is Showdown's shared ESLint configuration. Each project overrides
* at least a little of it here and there, but these are the rules we use
* unless there's a good reason otherwise.
*/
// @ts-check
import eslint from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import stylistic from '@stylistic/eslint-plugin';
/** @typedef {import('typescript-eslint').Config} ConfigFile */
/** @typedef {Awaited<ConfigFile>[number]} Config */
/** @typedef {NonNullable<Config['rules']>} Rules */
export { eslint, globals, tseslint, stylistic };
/** @type {Config} */
export const plugin = {
plugins: {
'@stylistic': stylistic,
'@typescript-eslint': tseslint.plugin,
},
};
/** @type {typeof tseslint.config} */
export const configure = (...args) => [
plugin,
...tseslint.config(...args),
];
/** @type {NonNullable<Config['rules']>} */
export const defaultRules = {
...stylistic.configs.customize({
braceStyle: '1tbs',
indent: 'tab',
semi: true,
jsx: true,
// ...
}).rules,
// TODO rules to revisit
// =====================
// nice to have but we mostly know && || precedence so not urgent to fix
"@stylistic/no-mixed-operators": "off",
// test only (should never be committed, but useful when testing)
// ==============================================================
// do we want unused args/destructures to start with _? unsure
"no-unused-vars": ["warn", {
args: "all",
argsIgnorePattern: ".",
caughtErrors: "all",
destructuredArrayIgnorePattern: ".",
ignoreRestSiblings: true,
}],
// "no-unused-vars": ["warn", {
// args: "all",
// argsIgnorePattern: "^_",
// caughtErrors: "all",
// destructuredArrayIgnorePattern: "^_",
// ignoreRestSiblings: true
// }],
"@stylistic/max-len": ["warn", {
"code": 120, "tabWidth": 0,
// DO NOT EDIT DIRECTLY: see bottom of file for source
"ignorePattern": "^\\s*(?:\\/\\/ \\s*)?(?:(?:export )?(?:let |const |readonly )?[a-zA-Z0-9_$.]+(?: \\+?=>? )|[a-zA-Z0-9$]+: \\[?|(?:return |throw )?(?:new )?(?:[a-zA-Z0-9$.]+\\()?)?(?:Utils\\.html|(?:this\\.)?(?:room\\.)?tr|\\$\\()?['\"`/]",
}],
"prefer-const": ["warn", { "destructuring": "all" }],
// PS code (code specific to PS)
// =============================
"@stylistic/new-parens": "off", // used for the `new class {...}` pattern
"no-prototype-builtins": "off",
// defaults too strict
// ===================
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-case-declarations": "off",
// probably bugs
// =============
"array-callback-return": "error",
"no-constructor-return": "error",
"no-dupe-class-members": "error",
"no-extend-native": "error",
"no-extra-bind": "warn",
"no-extra-label": "warn",
"no-eval": "error",
"no-implied-eval": "error",
"no-inner-declarations": ["error", "functions"],
"no-iterator": "error",
"no-fallthrough": ["error", { allowEmptyCase: true, reportUnusedFallthroughComment: true }],
"no-promise-executor-return": ["error", { allowVoid: true }],
"no-return-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-template-curly-in-string": "error",
"no-throw-literal": "warn",
"no-unmodified-loop-condition": "error",
// best way to read first key of object
// "no-unreachable-loop": "error",
// ternary is used to convert callbacks to Promises
// tagged templates are used for the SQL library
"no-unused-expressions": ["error", { allowTernary: true, allowTaggedTemplates: true, enforceForJSX: true }],
"no-useless-call": "error",
// "no-useless-assignment": "error",
"require-atomic-updates": "error",
// syntax style (local syntactical, usually autofixable formatting decisions)
// ===========================================================================
"@stylistic/member-delimiter-style": ["error", {
multiline: { delimiter: "comma", requireLast: true },
singleline: { delimiter: "comma", requireLast: false },
overrides: { interface: {
multiline: { delimiter: "semi", requireLast: true },
singleline: { delimiter: "semi", requireLast: false },
} },
}],
"default-case-last": "error",
"eqeqeq": ["error", "always", { null: "ignore" }],
"no-array-constructor": "error",
"no-duplicate-imports": "error",
"no-implicit-coercion": ["error", { allow: ["!!", "+"] }],
"no-multi-str": "error",
"no-object-constructor": "error",
"no-proto": "error",
"no-unneeded-ternary": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"object-shorthand": ["error", "always"],
"operator-assignment": ["error", "always"],
"prefer-arrow-callback": "error",
"prefer-exponentiation-operator": "error",
"prefer-numeric-literals": "error",
"prefer-object-has-own": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"radix": ["error", "as-needed"],
// syntax style, overriding base
// =============================
"@stylistic/quotes": "off",
"@stylistic/quote-props": "off",
"@stylistic/function-call-spacing": "error",
"@stylistic/arrow-parens": ["error", "as-needed"],
"@stylistic/comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "never",
"importAttributes": "always-multiline",
"dynamicImports": "always-multiline",
"enums": "always-multiline",
"generics": "always-multiline",
"tuples": "always-multiline",
}],
"@stylistic/jsx-wrap-multilines": "off",
"@stylistic/jsx-closing-bracket-location": ["error", "line-aligned"],
// "@stylistic/jsx-closing-tag-location": ["error", "line-aligned"],
"@stylistic/jsx-closing-tag-location": "off",
"@stylistic/jsx-one-expression-per-line": "off",
"@stylistic/jsx-max-props-per-line": "off",
"@stylistic/jsx-function-call-newline": "off",
"no-restricted-syntax": ["error",
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
],
// whitespace
// ==========
"@stylistic/block-spacing": "error",
"@stylistic/operator-linebreak": ["error", "after"],
"@stylistic/max-statements-per-line": ["error", { max: 3, ignoredNodes: ['BreakStatement'] }],
"@stylistic/lines-between-class-members": "off",
"@stylistic/multiline-ternary": "off",
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/indent": ["error", "tab", { "flatTernaryExpressions": true }],
};
/** @type {NonNullable<Config['rules']>} */
export const defaultRulesTS = {
...defaultRules,
// TODO: revisit
// we should do this someday but it'd have to be a gradual manual process
// "@typescript-eslint/explicit-module-boundary-types": "off",
// like above but slightly harder, so do that one first
// "@typescript-eslint/explicit-function-return-type": "off",
// probably we should settle on a standard someday
// "@typescript-eslint/member-ordering": "off",
// "@typescript-eslint/no-extraneous-class": "error",
// maybe we should consider this
"@typescript-eslint/consistent-indexed-object-style": "off",
// typescript-eslint specific
// ==========================
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": defaultRules["no-unused-vars"],
"no-shadow": "off",
"@typescript-eslint/no-shadow": defaultRules["no-shadow"],
"no-dupe-class-members": "off",
"@typescript-eslint/no-dupe-class-members": defaultRules["no-dupe-class-members"],
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": defaultRules["no-unused-expressions"],
// defaults too strict
// ===================
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
// probably bugs
// =============
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
// no way to get it to be less strict unfortunately
// "@typescript-eslint/no-misused-spread": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
// naming style
// ============
"@typescript-eslint/naming-convention": ["error", {
"selector": ["class", "interface", "typeAlias"],
"format": ["PascalCase"],
}],
// syntax style (local syntactical, usually autofixable formatting decisions)
// ===========================================================================
"@typescript-eslint/no-namespace": ["error", { allowDeclarations: true }],
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/consistent-type-assertions": ["error", { "assertionStyle": "as" }],
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }],
"@typescript-eslint/explicit-member-accessibility": ["error", { "accessibility": "no-public" }],
"@typescript-eslint/parameter-properties": "error",
// `source` and `target` are frequently used as variables that may point to `this`
// or to another `Pokemon` object, depending on how the given method is invoked
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["source", "target"] }],
// unfortunately this has lots of false positives without strict array/object property access
// "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-return-this-type": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unified-signatures": "error",
};
/** @type {NonNullable<Config['rules']>} */
export const defaultRulesTSChecked = {
...defaultRulesTS,
// style
// =====
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/restrict-plus-operands": ["error", {
allowBoolean: false, allowNullish: false, allowNumberAndString: false, allowRegExp: false,
}],
"@typescript-eslint/restrict-template-expressions": ["error", {
allow: [{ name: ['Error', 'URL', 'URLSearchParams'], from: 'lib' }],
allowBoolean: false, allowNever: false, allowNullish: false, allowRegExp: false,
}],
// we use `any`
// ============
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
// yes-types syntax style, overriding base
// =======================================
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-confusing-non-null-assertion": "off",
};
/** @type {NonNullable<Config['rules']>} */
export const defaultRulesES3 = {
...defaultRules,
// required in ES3
// ================
"no-var": "off",
"object-shorthand": ["error", "never"],
"prefer-arrow-callback": "off",
"prefer-exponentiation-operator": "off",
"prefer-object-has-own": "off",
"prefer-object-spread": "off",
"prefer-rest-params": "off",
"prefer-spread": "off",
"radix": "off",
"@stylistic/comma-dangle": "error",
"no-unused-vars": ["warn", {
args: "all",
argsIgnorePattern: ".",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^e(rr)?$",
destructuredArrayIgnorePattern: ".",
ignoreRestSiblings: true,
}],
"no-restricted-syntax": ["error",
{ selector: "TaggedTemplateExpression", message: "Hard to compile down to ES3" },
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
],
// with no block scoping, coming up with original variable names is too hard
"no-redeclare": "off",
// treat var as let
// unfortunately doesn't actually let me redeclare
// "block-scoped-var": "error",
"no-caller": "error",
"no-invalid-this": "error",
"no-new-wrappers": "error",
// Map/Set can be polyfilled but it's nontrivial and it's easier just to use bare objects
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"],
"unicode-bom": "error",
};
/**
* Actually very different from defaultRulesES3, because we don't have to
* worry about syntax that's easy to transpile to ES3 (which is basically
* all syntax).
* @type {NonNullable<Config['rules']>}
*/
export const defaultRulesES3TSChecked = {
...defaultRulesTSChecked,
"radix": "off",
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"],
"no-restricted-syntax": ["error", "TaggedTemplateExpression", "YieldExpression", "AwaitExpression", "BigIntLiteral"],
};
/**
* @param {Config[]} configs
* @returns {Config}
*/
function extractPlugin(configs) {
return configs.find(config => !config.rules) ||
(() => { throw new Error('No plugin found'); })();
}
/**
* @param {Config[]} configs
* @returns {Rules}
*/
function extractRules(configs) {
const rules = {};
for (const config of configs.filter(c => c.rules)) {
Object.assign(rules, config.rules);
}
return rules;
}
const tseslintPlugin = extractPlugin(tseslint.configs.stylisticTypeChecked);
/** @type {{[k: string]: Config[]}} */
export const configs = {
js: [{
rules: {
...eslint.configs.recommended.rules,
...defaultRules,
},
}],
ts: [tseslintPlugin, {
rules: {
...eslint.configs.recommended.rules,
...extractRules(tseslint.configs.recommendedTypeChecked),
...extractRules(tseslint.configs.stylisticTypeChecked),
...defaultRulesTSChecked,
},
}],
es3: [{
rules: {
...eslint.configs.recommended.rules,
...defaultRulesES3,
},
}],
es3ts: [tseslintPlugin, {
rules: {
...eslint.configs.recommended.rules,
...extractRules(tseslint.configs.recommendedTypeChecked),
...extractRules(tseslint.configs.stylisticTypeChecked),
...defaultRulesES3TSChecked,
},
}],
};
/*
SOURCE FOR IGNOREPATTERN (compile with https://regexfree.k55.io/ )
# indentation
^\s*
# possibly commented out
(\/\/\ \s*)?
(
# define a variable, append to a variable, or define a single-arg arrow function
(export\ )? (let\ |const\ |readonly\ )? [a-zA-Z0-9_$.]+ (\ \+?=>?\ )
|
# define a property (oversize arrays are only allowed in properties)
[a-zA-Z0-9$]+:\ \[?
|
# optionally return or throw
(return\ |throw\ )?
# call a function or constructor
(new\ )?([a-zA-Z0-9$.]+\()?
)?
(
Utils\.html
|
(this\.)?(room\.)?tr
|
\$\(
)?
# start of string or regex
['"`\/]
*/

134
eslint.config.mjs Normal file
View File

@ -0,0 +1,134 @@
// @ts-check
import { configs, configure, globals } from './eslint-ps-standard.mjs';
export default configure([
{
ignores: [
'caches/**',
'play.pokemonshowdown.com/config/config-test.js',
'play.pokemonshowdown.com/src/battle-log-misc.js',
'play.pokemonshowdown.com/js/replay-embed.js',
],
},
{
name: "JavaScript for browsers (ES3)",
files: [
'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',
'config/config-example.js',
],
extends: [configs.es3],
languageOptions: {
ecmaVersion: 3,
sourceType: "script",
globals: {
...globals.builtin,
...globals.browser,
...globals.node,
// Libraries
"_": false, "$": false, "Backbone": false, "d3": false, "html": false, "html4": false, "jQuery": false, "SockJS": false, "ColorThief": false,
// Environment-specific
"fs": false, "gui": false, "ga": false, "macgap": false, "nwWindow": false, "webkitNotifications": false, "nw": false,
// Battle stuff
"Battle": true, "Pokemon": true, "BattleSound": true, "BattleTooltips": true, "BattleLog": true,
"BattleAbilities": false, "BattleAliases": false, "BattleBackdrops": false, "BattleBackdropsFive": false, "BattleBackdropsFour": false, "BattleBackdropsThree": false, "BattleEffects": false,
"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, "BattleStatOptimizer": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false,
"BattleTextParser": false,
// Generic global variables
"Config": false, "BattleSearch": false, "Storage": false, "Dex": false, "DexSearch": false,
"app": false, "toID": false, "toRoomid": false, "toUserid": false, "toName": false, "PSUtils": false, "MD5": false,
"ChatHistory": false, "Topbar": false, "UserList": false,
// Rooms
"Room": false, "BattleRoom": false, "ChatRoom": false, "ConsoleRoom": false, "HTMLRoom": false, "LadderRoom": false, "MainMenuRoom": false, "RoomsRoom": false, "BattlesRoom": false, "TeambuilderRoom": false,
// Tons of popups
"Popup": false, "ForfeitPopup": false, "BracketPopup": false, "LoginPasswordPopup": false, "UserPopup": false, "UserOptionsPopup": false, "UserOptions": false, "TeamPopup": false,
"AvatarsPopup": false, "CreditsPopup": false, "FormatPopup": false, "FormattingPopup": false, "LoginPopup": false,
"MovePopup": false, "SoundsPopup": false, "OptionsPopup": false, "PromptPopup": false, "ProxyPopup": false, "ReconnectPopup": false,
"RegisterPopup": false, "ReplayUploadedPopup": false, "RulesPopup": false, "TabListPopup": false, "TournamentBox": false,
"CustomBackgroundPopup": false,
// Test client
"POKEMON_SHOWDOWN_TESTCLIENT_KEY": false,
},
},
rules: {
"@stylistic/max-len": "off",
// we use these for the big IIFEs that wrap entire files
"@stylistic/padded-blocks": "off",
// TODO: actually fix useless escapes
"no-useless-escape": "off",
"no-shadow-restricted-names": "error",
"no-shadow": "off",
},
},
{
name: "JavaScript for Node",
files: [
'*.mjs', // look mom I'm linting myself!
'build-tools/*.js',
'build-tools/update',
'build-tools/build-*',
],
extends: [configs.js],
languageOptions: {
globals: {
...globals.builtin,
...globals.node,
},
},
rules: {
},
},
{
name: "TypeScript",
files: [
'play.pokemonshowdown.com/src/*.ts',
'play.pokemonshowdown.com/src/*.tsx',
'replay.pokemonshowdown.com/src/*.ts',
'replay.pokemonshowdown.com/src/*.tsx',
],
extends: [configs.es3ts],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// temporary
"prefer-const": "off",
// we use these for grouping
"@stylistic/padded-blocks": "off",
// too many of these on client
"@typescript-eslint/no-floating-promises": "off",
// we use these for animations
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/restrict-template-expressions": ["error", {
allow: [
{ name: ['Error', 'URL', 'URLSearchParams'], from: 'lib' },
{ name: ['ModifiableValue'], from: 'file' },
],
allowBoolean: false, allowNever: false, allowNullish: false, allowRegExp: false,
}],
},
},
]);

5166
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,9 @@
"url": "https://github.com/Zarel/Pokemon-Showdown-Client.git" "url": "https://github.com/Zarel/Pokemon-Showdown-Client.git"
}, },
"scripts": { "scripts": {
"lint": "eslint --config=.eslintrc.js --cache --cache-file=caches/eslint-base.json play.pokemonshowdown.com/js/ && eslint --config=build-tools/.eslintrc.js --cache --cache-file=caches/eslint-build.json build-tools/update build-tools/build-indexes && tslint --project .", "lint": "eslint --cache --cache-location caches/eslintcache.json",
"test": "npm run lint && tsc && node build && mocha test/*.js", "test": "node build && tsc && eslint --cache --cache-location caches/eslintcache.json --max-warnings 0 && mocha test/*.js",
"fix": "eslint --config=.eslintrc.js --fix js/ && eslint --config=build-tools/.eslintrc.js --fix build-tools/update build-tools/build-indexes", "fix": "eslint --cache --cache-location caches/eslintcache.json --fix",
"build": "node build", "build": "node build",
"build-full": "node build full" "build-full": "node build full"
}, },
@ -26,14 +26,16 @@
"image-size": "^0.7.5" "image-size": "^0.7.5"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^4.0.1",
"@types/jquery": "^3.5.3", "@types/jquery": "^3.5.3",
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"eslint": "^5.16.0", "eslint": "^9.20.1",
"globals": "^16.0.0",
"mocha": "^6.0.2", "mocha": "^6.0.2",
"preact": "^8.3.1", "preact": "^8.3.1",
"source-map": "^0.7.3", "source-map": "^0.7.3",
"tslint": "^5.20.1", "typescript": "^5.7.3",
"typescript": "^5.7.3" "typescript-eslint": "^8.24.1"
}, },
"private": true "private": true
} }

View File

@ -39,7 +39,7 @@
this.battle.subscribe(function () { self.updateControls(); }); this.battle.subscribe(function () { self.updateControls(); });
this.users = {}; this.users = {};
this.userCount = {users: 0}; this.userCount = { users: 0 };
this.$userList = this.$('.userlist'); this.$userList = this.$('.userlist');
this.userList = new UserList({ this.userList = new UserList({
el: this.$userList, el: this.$userList,
@ -76,7 +76,7 @@
}, },
requestLeave: function (e) { requestLeave: function (e) {
if ((this.side || this.requireForfeit) && this.battle && !this.battleEnded && !this.expired && !this.battle.forfeitPending) { if ((this.side || this.requireForfeit) && this.battle && !this.battleEnded && !this.expired && !this.battle.forfeitPending) {
app.addPopup(ForfeitPopup, {room: this, sourceEl: e && e.currentTarget, gameType: 'battle'}); app.addPopup(ForfeitPopup, { room: this, sourceEl: e && e.currentTarget, gameType: 'battle' });
return false; return false;
} }
return true; return true;
@ -217,7 +217,8 @@
this.battle.stepQueue.push('|' + args.join('|')); this.battle.stepQueue.push('|' + args.join('|'));
break; break;
} }
} else if (logLine.substr(0, 7) === '|title|') { // eslint-disable-line no-empty } else if (logLine.substr(0, 7) === '|title|') {
// empty
} else if (logLine.substr(0, 5) === '|win|' || logLine === '|tie') { } else if (logLine.substr(0, 5) === '|win|' || logLine === '|tie') {
this.battleEnded = true; this.battleEnded = true;
this.battle.stepQueue.push(logLine); this.battle.stepQueue.push(logLine);
@ -410,8 +411,8 @@
}; };
if (this.request.forceSwitch !== true) { if (this.request.forceSwitch !== true) {
var faintedLength = _.filter(this.request.forceSwitch, function (fainted) {return fainted;}).length; var faintedLength = _.filter(this.request.forceSwitch, function (fainted) { return fainted; }).length;
var freedomDegrees = faintedLength - _.filter(switchables.slice(this.battle.pokemonControlled), function (mon) {return !mon.fainted;}).length; var freedomDegrees = faintedLength - _.filter(switchables.slice(this.battle.pokemonControlled), function (mon) { return !mon.fainted; }).length;
this.choice.freedomDegrees = Math.max(freedomDegrees, 0); this.choice.freedomDegrees = Math.max(freedomDegrees, 0);
this.choice.canSwitch = faintedLength - this.choice.freedomDegrees; this.choice.canSwitch = faintedLength - this.choice.freedomDegrees;
} }
@ -542,7 +543,7 @@
this.$('.timerbutton').replaceWith(this.getTimerHTML()); this.$('.timerbutton').replaceWith(this.getTimerHTML());
}, },
openTimer: function () { openTimer: function () {
app.addPopup(TimerPopup, {room: this}); app.addPopup(TimerPopup, { room: this });
}, },
updateMoveControls: function (type) { updateMoveControls: function (type) {
var switchables = this.request && this.request.side ? this.battle.myPokemon : []; var switchables = this.request && this.request.side ? this.battle.myPokemon : [];
@ -577,7 +578,7 @@
var canTerastallize = curActive.canTerastallize || switchables[pos].canTerastallize; var canTerastallize = curActive.canTerastallize || switchables[pos].canTerastallize;
if (canZMove && typeof canZMove[0] === 'string') { if (canZMove && typeof canZMove[0] === 'string') {
canZMove = _.map(canZMove, function (move) { canZMove = _.map(canZMove, function (move) {
return {move: move, target: Dex.moves.get(move).target}; return { move: move, target: Dex.moves.get(move).target };
}); });
} }
if (gigantamax) gigantamax = Dex.moves.get(gigantamax); if (gigantamax) gigantamax = Dex.moves.get(gigantamax);
@ -641,7 +642,7 @@
} else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') { } else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
if (Math.abs(activePos - i) > 1) disabled = true; if (Math.abs(activePos - i) > 1) disabled = true;
} }
if (moveTarget !== 'adjacentAllyOrSelf' && activePos == i) disabled = true; if (moveTarget !== 'adjacentAllyOrSelf' && activePos === i) disabled = true;
if (disabled) { if (disabled) {
targetMenus[1] += '<button disabled style="visibility:hidden"></button> '; targetMenus[1] += '<button disabled style="visibility:hidden"></button> ';
@ -840,7 +841,7 @@
} }
var switchables = this.request && this.request.side ? this.battle.myPokemon : []; var switchables = this.request && this.request.side ? this.battle.myPokemon : [];
var nearActive = this.battle.nearSide.active; // var nearActive = this.battle.nearSide.active;
var isReviving = !!switchables[pos].reviving; var isReviving = !!switchables[pos].reviving;
var requestTitle = ''; var requestTitle = '';
@ -1106,7 +1107,7 @@
request.requestType = 'wait'; request.requestType = 'wait';
} }
this.choice = choiceText ? {waiting: true} : null; this.choice = choiceText ? { waiting: true } : null;
this.finalDecision = this.finalDecisionMove = this.finalDecisionSwitch = false; this.finalDecision = this.finalDecisionMove = this.finalDecisionSwitch = false;
this.request = request; this.request = request;
if (request.side) { if (request.side) {
@ -1184,7 +1185,7 @@
this.send('/savereplay'); this.send('/savereplay');
}, },
openBattleOptions: function () { openBattleOptions: function () {
app.addPopup(BattleOptionsPopup, {battle: this.battle, room: this}); app.addPopup(BattleOptionsPopup, { battle: this.battle, room: this });
}, },
clickReplayDownloadButton: function (e) { clickReplayDownloadButton: function (e) {
var filename = (this.battle.tier || 'Battle').replace(/[^A-Za-z0-9]/g, ''); var filename = (this.battle.tier || 'Battle').replace(/[^A-Za-z0-9]/g, '');
@ -1277,7 +1278,7 @@
var isTerastal = !!(this.$('input[name=terastallize]')[0] || '').checked; var isTerastal = !!(this.$('input[name=terastallize]')[0] || '').checked;
var target = e.getAttribute('data-target'); var target = e.getAttribute('data-target');
var choosableTargets = {normal: 1, any: 1, adjacentAlly: 1, adjacentAllyOrSelf: 1, adjacentFoe: 1}; var choosableTargets = { normal: 1, any: 1, adjacentAlly: 1, adjacentAllyOrSelf: 1, adjacentFoe: 1 };
if (this.battle.gameType === 'freeforall') delete choosableTargets['adjacentAllyOrSelf']; if (this.battle.gameType === 'freeforall') delete choosableTargets['adjacentAllyOrSelf'];
this.choice.choices.push('move ' + pos + (isMega ? ' mega' : '') + (isMegaX ? ' megax' : isMegaY ? ' megay' : '') + (isZMove ? ' zmove' : '') + (isUltraBurst ? ' ultra' : '') + (isDynamax ? ' dynamax' : '') + (isTerastal ? ' terastallize' : '')); this.choice.choices.push('move ' + pos + (isMega ? ' mega' : '') + (isMegaX ? ' megax' : isMegaY ? ' megay' : '') + (isZMove ? ' zmove' : '') + (isUltraBurst ? ' ultra' : '') + (isDynamax ? ' dynamax' : '') + (isTerastal ? ' terastallize' : ''));
@ -1328,8 +1329,8 @@
} }
// After choosing the position to which a pokemon will switch in (Doubles/Triples end-game). // After choosing the position to which a pokemon will switch in (Doubles/Triples end-game).
if (!this.request || this.request.requestType !== 'switch') return false; //?? if (!this.request || this.request.requestType !== 'switch') return false; // ??
if (this.choice.canSwitch > _.filter(this.choice.choices, function (choice) {return choice;}).length) { if (this.choice.canSwitch > _.filter(this.choice.choices, function (choice) { return choice; }).length) {
// More switches are pending. // More switches are pending.
this.choice.type = 'switch2'; this.choice.type = 'switch2';
this.updateControlsForPlayer(); this.updateControlsForPlayer();
@ -1414,8 +1415,10 @@
} }
} else if (this.request.requestType === 'move') { } else if (this.request.requestType === 'move') {
var requestDetails = this.request && this.request.side ? this.battle.myPokemon : []; var requestDetails = this.request && this.request.side ? this.battle.myPokemon : [];
while (choices.length < this.battle.pokemonControlled && while (
(!nearActive[choices.length] || requestDetails[choices.length].commanding)) { choices.length < this.battle.pokemonControlled &&
(!nearActive[choices.length] || requestDetails[choices.length].commanding)
) {
choices.push('pass'); choices.push('pass');
} }

View File

@ -88,30 +88,30 @@
$wrapper.html( $wrapper.html(
'<div class="tournament-title">' + '<div class="tournament-title">' +
'<span class="tournament-format"></span> <span class="tournament-generator"></span> Tournament' + ' <span class="tournament-format"></span> <span class="tournament-generator"></span> Tournament' +
'<div class="tournament-status"></div>' + ' <div class="tournament-status"></div>' +
'<div class="tournament-toggle">Toggle</div>' + ' <div class="tournament-toggle">Toggle</div>' +
'</div>' + '</div>' +
'<div class="tournament-box">' + '<div class="tournament-box">' +
'<div class="tournament-bracket"></div>' + ' <div class="tournament-bracket"></div>' +
'<div class="tournament-tools">' + ' <div class="tournament-tools">' +
'<div class="tournament-team"></div>' + ' <div class="tournament-team"></div>' +
'<button class="button tournament-join">Join</button><button class="button tournament-validate"><i class="fa fa-check"></i> Validate</button> <button class="button tournament-leave">Leave</button>' + ' <button class="button tournament-join">Join</button><button class="button tournament-validate"><i class="fa fa-check"></i> Validate</button> <button class="button tournament-leave">Leave</button>' +
'<div class="tournament-nomatches">Waiting for battles to become available...</div>' + ' <div class="tournament-nomatches">Waiting for battles to become available...</div>' +
'<div class="tournament-challenge">' + ' <div class="tournament-challenge">' +
'<div class="tournament-challenge-user"></div>' + ' <div class="tournament-challenge-user"></div>' +
'<button class="button tournament-challenge-challenge"><strong>Ready!</strong></button><span class="tournament-challenge-user-menu"></span>' + ' <button class="button tournament-challenge-challenge"><strong>Ready!</strong></button><span class="tournament-challenge-user-menu"></span>' +
'</div>' + ' </div>' +
'<div class="tournament-challengeby"></div>' + ' <div class="tournament-challengeby"></div>' +
'<div class="tournament-challenging">' + ' <div class="tournament-challenging">' +
'<div class="tournament-challenging-message"></div>' + ' <div class="tournament-challenging-message"></div>' +
'<button class="button tournament-challenge-cancel">Cancel</button>' + ' <button class="button tournament-challenge-cancel">Cancel</button>' +
'</div>' + ' </div>' +
'<div class="tournament-challenged">' + ' <div class="tournament-challenged">' +
'<div class="tournament-challenged-message"></div>' + ' <div class="tournament-challenged-message"></div>' +
'<button class="button tournament-challenge-accept"><strong>Ready!</strong></button>' + ' <button class="button tournament-challenge-accept"><strong>Ready!</strong></button>' +
'</div>' + ' </div>' +
'</div>' + ' </div>' +
'</div>'); '</div>');
this.$title = $wrapper.find('.tournament-title'); this.$title = $wrapper.find('.tournament-title');
@ -609,7 +609,7 @@
}; };
var nodesByDepth = []; var nodesByDepth = [];
var stack = [{node: data.rootNode, depth: 0}]; var stack = [{ node: data.rootNode, depth: 0 }];
while (stack.length > 0) { while (stack.length > 0) {
var frame = stack.pop(); var frame = stack.pop();
@ -619,7 +619,7 @@
if (!frame.node.children) frame.node.children = []; if (!frame.node.children) frame.node.children = [];
frame.node.children.forEach(function (child) { frame.node.children.forEach(function (child) {
stack.push({node: child, depth: frame.depth + 1}); stack.push({ node: child, depth: frame.depth + 1 });
}); });
} }
var maxDepth = nodesByDepth.length; var maxDepth = nodesByDepth.length;
@ -653,10 +653,10 @@
var link = d3.svg.diagonal() var link = d3.svg.diagonal()
.source(function (link) { .source(function (link) {
return {x: link.source.x, y: link.source.y + nodeSize.realWidth / 2}; return { x: link.source.x, y: link.source.y + nodeSize.realWidth / 2 };
}) })
.target(function (link) { .target(function (link) {
return {x: link.target.x, y: link.target.y - nodeSize.realWidth / 2}; return { x: link.target.x, y: link.target.y - nodeSize.realWidth / 2 };
}) })
.projection(function (link) { .projection(function (link) {
return [size.width - link.y, link.x]; return [size.width - link.y, link.x];
@ -793,20 +793,20 @@
}; };
TournamentBox.prototype.showBracketPopup = function (data, isStandalone) { TournamentBox.prototype.showBracketPopup = function (data, isStandalone) {
if (isStandalone) if (isStandalone)
app.addPopup(BracketPopup, {$bracket: this.generateBracket(data)}); app.addPopup(BracketPopup, { $bracket: this.generateBracket(data) });
else else
this.bracketPopup = app.addPopup(BracketPopup, {parent: this, $bracket: this.generateBracket(data)}); this.bracketPopup = app.addPopup(BracketPopup, { parent: this, $bracket: this.generateBracket(data) });
}; };
TournamentBox.prototype.renderChallengeUsers = function () { TournamentBox.prototype.renderChallengeUsers = function () {
return ' <button class="button" value="' + toID(this.info.challenges[0]) + '" name="tournamentButton" data-type="challengeUser">Change opponent</button>'; return ' <button class="button" value="' + toID(this.info.challenges[0]) + '" name="tournamentButton" data-type="challengeUser">Change opponent</button>';
}; };
TournamentBox.prototype.challengeUser = function (user, button) { TournamentBox.prototype.challengeUser = function (user, button) {
app.addPopup(UserPopup, {user: user, users: this.info.challenges, sourceEl: button}); app.addPopup(UserPopup, { user: user, users: this.info.challenges, sourceEl: button });
}; };
TournamentBox.prototype.teamSelect = function (team, button) { TournamentBox.prototype.teamSelect = function (team, button) {
app.addPopup(TeamPopup, {team: team, format: this.info.teambuilderFormat, sourceEl: button, room: this.room.id, isMoreTeams: false, folderToggleOn: true, folderNotExpanded: []}); app.addPopup(TeamPopup, { team: team, format: this.info.teambuilderFormat, sourceEl: button, room: this.room.id, isMoreTeams: false, folderToggleOn: true, folderNotExpanded: [] });
}; };
return TournamentBox; return TournamentBox;

View File

@ -215,7 +215,7 @@
var name = $(e.currentTarget).data('name') || $(e.currentTarget).text(); var name = $(e.currentTarget).data('name') || $(e.currentTarget).text();
var away = $(e.currentTarget).data('away') || false; var away = $(e.currentTarget).data('away') || false;
var status = $(e.currentTarget).data('status'); var status = $(e.currentTarget).data('status');
app.addPopup(UserPopup, {roomGroup: roomGroup, name: name, away: away, status: status, sourceEl: e.currentTarget, position: position}); app.addPopup(UserPopup, { roomGroup: roomGroup, name: name, away: away, status: status, sourceEl: e.currentTarget, position: position });
}, },
openPM: function (e) { openPM: function (e) {
e.preventDefault(); e.preventDefault();
@ -236,10 +236,10 @@
app.addPopup(AvatarsPopup); app.addPopup(AvatarsPopup);
}, },
openSounds: function () { openSounds: function () {
app.addPopup(SoundsPopup, {type: 'semimodal'}); app.addPopup(SoundsPopup, { type: 'semimodal' });
}, },
openOptions: function () { openOptions: function () {
app.addPopup(OptionsPopup, {type: 'semimodal'}); app.addPopup(OptionsPopup, { type: 'semimodal' });
}, },
// highlight // highlight
@ -247,7 +247,7 @@
getHighlight: function (message) { getHighlight: function (message) {
var highlights = Dex.prefs('highlights') || {}; var highlights = Dex.prefs('highlights') || {};
if (Array.isArray(highlights)) { if (Array.isArray(highlights)) {
highlights = {global: highlights}; highlights = { global: highlights };
// Migrate from the old highlight system // Migrate from the old highlight system
Storage.prefs('highlights', highlights); Storage.prefs('highlights', highlights);
} }
@ -362,7 +362,7 @@
var currentLine = prefix.substr(prefix.lastIndexOf('\n') + 1); var currentLine = prefix.substr(prefix.lastIndexOf('\n') + 1);
var shouldSearchCommands = !cmds || (cmds.length ? !!cmds.length && !cmds.filter(function (x) { var shouldSearchCommands = !cmds || (cmds.length ? !!cmds.length && !cmds.filter(function (x) {
return x.startsWith(currentLine); return x.startsWith(currentLine);
}).length : prefix != this.tabComplete.prefix); }).length : prefix !== this.tabComplete.prefix);
var isCommandSearch = (currentLine.startsWith('/') && !currentLine.startsWith('//')) || currentLine.startsWith('!'); var isCommandSearch = (currentLine.startsWith('/') && !currentLine.startsWith('//')) || currentLine.startsWith('!');
var resultsExist = this.tabComplete.lastSearch === text && this.tabComplete.commands; var resultsExist = this.tabComplete.lastSearch === text && this.tabComplete.commands;
if (isCommandSearch && shouldSearchCommands && !resultsExist) { if (isCommandSearch && shouldSearchCommands && !resultsExist) {
@ -417,7 +417,7 @@
return bidx - aidx; return bidx - aidx;
} }
return -1; // a comes first return -1; // a comes first
} else if (bidx != -1) { } else if (bidx !== -1) {
return 1; // b comes first return 1; // b comes first
} }
return (a[0] < b[0]) ? -1 : 1; // alphabetical order return (a[0] < b[0]) ? -1 : 1; // alphabetical order
@ -502,7 +502,7 @@
var self = this; var self = this;
var challenge = function (targets) { var challenge = function (targets) {
target = toID(targets[0]); target = toID(targets[0]);
self.challengeData = {userid: target, format: targets.length > 1 ? targets.slice(1).join(',') : '', team: ''}; self.challengeData = { userid: target, format: targets.length > 1 ? targets.slice(1).join(',') : '', team: '' };
app.on('response:userdetails', self.challengeUserdetails, self); app.on('response:userdetails', self.challengeUserdetails, self);
app.send('/cmd userdetails ' + target); app.send('/cmd userdetails ' + target);
}; };
@ -575,7 +575,7 @@
case 'open': case 'open':
if (this.checkBroadcast(cmd, text)) return false; if (this.checkBroadcast(cmd, text)) return false;
var openUser = function (target) { var openUser = function (target) {
app.addPopup(UserPopup, {name: target}); app.addPopup(UserPopup, { name: target });
}; };
target = toName(target); target = toName(target);
if (!target) { if (!target) {
@ -710,7 +710,7 @@
if (!userid) { if (!userid) {
var newsId = $(this).data('newsid'); var newsId = $(this).data('newsid');
if (newsId) { if (newsId) {
$.cookie('showdown_readnews', '' + newsId, {expires: 365}); $.cookie('showdown_readnews', '' + newsId, { expires: 365 });
} }
$(this).remove(); $(this).remove();
return; return;
@ -772,7 +772,7 @@
} }
this.add('Join/leave messages on room ' + room + ': ALWAYS ON'); this.add('Join/leave messages on room ' + room + ': ALWAYS ON');
} else { } else {
serverShowjoins = {global: 1}; serverShowjoins = { global: 1 };
this.add('Join/leave messages: ALWAYS ON'); this.add('Join/leave messages: ALWAYS ON');
} }
showjoins[Config.server.id] = serverShowjoins; showjoins[Config.server.id] = serverShowjoins;
@ -791,7 +791,7 @@
} }
this.add('Join/leave messages on room ' + room + ': AUTOMATIC'); this.add('Join/leave messages on room ' + room + ': AUTOMATIC');
} else { } else {
serverShowjoins = {global: 0}; serverShowjoins = { global: 0 };
this.add('Join/leave messages: AUTOMATIC'); this.add('Join/leave messages: AUTOMATIC');
} }
showjoins[Config.server.id] = serverShowjoins; showjoins[Config.server.id] = serverShowjoins;
@ -1373,10 +1373,10 @@
}, },
requestLeave: function (e) { requestLeave: function (e) {
if (app.rooms[''].games && app.rooms[''].games[this.id]) { if (app.rooms[''].games && app.rooms[''].games[this.id]) {
app.addPopup(ForfeitPopup, {room: this, sourceEl: e && e.currentTarget, gameType: (this.id.substring(0, 5) === 'help-' ? 'help' : 'game')}); app.addPopup(ForfeitPopup, { room: this, sourceEl: e && e.currentTarget, gameType: (this.id.substring(0, 5) === 'help-' ? 'help' : 'game') });
return false; return false;
} else if (Dex.prefs('leavePopupRoom')) { } else if (Dex.prefs('leavePopupRoom')) {
app.addPopup(ForfeitPopup, {room: this, sourceEl: e && e.currentTarget, gameType: 'room'}); app.addPopup(ForfeitPopup, { room: this, sourceEl: e && e.currentTarget, gameType: 'room' });
return false; return false;
} }
return true; return true;
@ -1385,7 +1385,7 @@
this.add(data); this.add(data);
}, },
getUserGroup: function (userid) { getUserGroup: function (userid) {
return (app.rooms[this.id].users[userid] || {group: ' '}).group; return (app.rooms[this.id].users[userid] || { group: ' ' }).group;
}, },
add: function (log) { add: function (log) {
if (typeof log === 'string') log = log.split('\n'); if (typeof log === 'string') log = log.split('\n');
@ -1500,7 +1500,6 @@
this.addJoinLeave('rename', row[1], row[2], true); this.addJoinLeave('rename', row[1], row[2], true);
break; break;
case 'users': case 'users':
this.parseUserList(row[1]); this.parseUserList(row[1]);
break; break;
@ -1738,9 +1737,9 @@
break; break;
} }
if (j > 0) { if (j > 0) {
if (j == 1 && list.length == 2) { if (j === 1 && list.length === 2) {
message += ' and '; message += ' and ';
} else if (j == list.length - 1) { } else if (j === list.length - 1) {
message += ', and '; message += ', and ';
} else { } else {
message += ', '; message += ', ';
@ -1900,7 +1899,7 @@
buf += this.getNoNamedUsersOnline(); buf += this.getNoNamedUsersOnline();
} }
if (this.room.userCount.guests) { if (this.room.userCount.guests) {
buf += '<li id="' + this.room.id + '-userlist-guests" style="text-align:center;padding:2px 0"><small>(<span id="' + this.room.id + '-usercount-guests">' + this.room.userCount.guests + '</span> guest' + (this.room.userCount.guests == 1 ? '' : 's') + ')</small></li>'; buf += '<li id="' + this.room.id + '-userlist-guests" style="text-align:center;padding:2px 0"><small>(<span id="' + this.room.id + '-usercount-guests">' + this.room.userCount.guests + '</span> guest' + (this.room.userCount.guests === 1 ? '' : 's') + ')</small></li>';
} }
this.$el.html(buf); this.$el.html(buf);
}, },
@ -1960,7 +1959,7 @@
text += '<button class="userbutton username" data-roomgroup="' + BattleLog.escapeHTML(user.group) + '" data-name="' + BattleLog.escapeHTML(user.name) + '"'; text += '<button class="userbutton username" data-roomgroup="' + BattleLog.escapeHTML(user.group) + '" data-name="' + BattleLog.escapeHTML(user.name) + '"';
text += (user.away ? ' data-away=true' : '') + (user.status ? ' data-status="' + BattleLog.escapeHTML(user.status) + '"' : '') + '>'; text += (user.away ? ' data-away=true' : '') + (user.status ? ' data-status="' + BattleLog.escapeHTML(user.status) + '"' : '') + '>';
var group = user.group; var group = user.group;
var details = Config.groups[group] || {type: 'user'}; var details = Config.groups[group] || { type: 'user' };
var color = user.away ? 'color:#888;' : BattleLog.hashColor(userid); var color = user.away ? 'color:#888;' : BattleLog.hashColor(userid);
text += '<em class="group' + (details.group === 2 ? ' staffgroup' : '') + '">' + BattleLog.escapeHTML(group) + '</em>'; text += '<em class="group' + (details.group === 2 ? ' staffgroup' : '') + '">' + BattleLog.escapeHTML(group) + '</em>';
if (details.type === 'leadership') { if (details.type === 'leadership') {
@ -1992,16 +1991,16 @@
comparator: function (a, b) { comparator: function (a, b) {
if (a === b) return 0; if (a === b) return 0;
var aUser = this.room.users[a] || {group: Config.defaultGroup, away: false}; var aUser = this.room.users[a] || { group: Config.defaultGroup, away: false };
var bUser = this.room.users[b] || {group: Config.defaultGroup, away: false}; var bUser = this.room.users[b] || { group: Config.defaultGroup, away: false };
var aRank = ( var aRank = (
Config.groups[aUser.group || ' '] || Config.groups[aUser.group || ' '] ||
{order: (Config.defaultOrder || 10006.5)} { order: (Config.defaultOrder || 10006.5) }
).order; ).order;
var bRank = ( var bRank = (
Config.groups[bUser.group || ' '] || Config.groups[bUser.group || ' '] ||
{order: (Config.defaultOrder || 10006.5)} { order: (Config.defaultOrder || 10006.5) }
).order; ).order;
if (aRank !== bRank) return aRank - bRank; if (aRank !== bRank) return aRank - bRank;

View File

@ -92,8 +92,8 @@
clickUsername: function (e) { clickUsername: function (e) {
e.stopPropagation(); e.stopPropagation();
var name = $(e.currentTarget).data('name') || $(e.currentTarget).text(); var name = $(e.currentTarget).data('name') || $(e.currentTarget).text();
app.addPopup(UserPopup, {name: name, sourceEl: e.currentTarget}); app.addPopup(UserPopup, { name: name, sourceEl: e.currentTarget });
}, }
}); });
this.LadderRoom = HTMLRoom.extend({ this.LadderRoom = HTMLRoom.extend({
@ -124,7 +124,7 @@
update: function () { update: function () {
if (!this.curFormat) { if (!this.curFormat) {
var buf = '<div class="ladder pad"><p>See a user\'s ranking with <a class="button" href="//' + Config.routes.users + '/" target="_blank">User lookup</a></p>' + var buf = '<div class="ladder pad"><p>See a user\'s ranking with <a class="button" href="//' + Config.routes.users + '/" target="_blank">User lookup</a></p>' +
//'<p><strong style="color:red">I\'m really really sorry, but as a warning: we\'re going to reset the ladder again soon to fix some more ladder bugs.</strong></p>' + // '<p><strong style="color:red">I\'m really really sorry, but as a warning: we\'re going to reset the ladder again soon to fix some more ladder bugs.</strong></p>' +
'<p>(btw if you couldn\'t tell the ladder screens aren\'t done yet; they\'ll look nicer than this once I\'m done.)</p>' + '<p>(btw if you couldn\'t tell the ladder screens aren\'t done yet; they\'ll look nicer than this once I\'m done.)</p>' +
'<p><button name="selectFormat" value="help" class="button"><i class="fa fa-info-circle"></i> How the ladder works</button></p><ul>'; '<p><button name="selectFormat" value="help" class="button"><i class="fa fa-info-circle"></i> How the ladder works</button></p><ul>';
if (!window.BattleFormats) { if (!window.BattleFormats) {
@ -196,7 +196,7 @@
this.update(); this.update();
} }
}, { }, {
COIL_B: {}, COIL_B: {}
}); });
}).call(this, jQuery); }).call(this, jQuery);

View File

@ -12,7 +12,7 @@
'click .closebutton': 'closePM', 'click .closebutton': 'closePM',
'click .minimizebutton': 'minimizePM', 'click .minimizebutton': 'minimizePM',
'click .pm-challenge': 'clickPMButtonBarChallenge', 'click .pm-challenge': 'clickPMButtonBarChallenge',
'click .pm-userOptions':'clickPMButtonBarUserOptions', 'click .pm-userOptions': 'clickPMButtonBarUserOptions',
'click .pm-window': 'clickPMBackground', 'click .pm-window': 'clickPMBackground',
'dblclick .pm-window h3': 'dblClickPMHeader', 'dblclick .pm-window h3': 'dblClickPMHeader',
'focus textarea': 'onFocusPM', 'focus textarea': 'onFocusPM',
@ -347,7 +347,7 @@
$pmWindow = $(e.currentTarget).closest('.pm-window'); $pmWindow = $(e.currentTarget).closest('.pm-window');
var newsId = $pmWindow.data('newsid'); var newsId = $pmWindow.data('newsid');
if (newsId) { if (newsId) {
$.cookie('showdown_readnews', '' + newsId, {expires: 365}); $.cookie('showdown_readnews', '' + newsId, { expires: 365 });
} }
$pmWindow.remove(); $pmWindow.remove();
return; return;
@ -414,7 +414,7 @@
clickUsername: function (e) { clickUsername: function (e) {
e.stopPropagation(); e.stopPropagation();
var name = $(e.currentTarget).data('name') || $(e.currentTarget).text(); var name = $(e.currentTarget).data('name') || $(e.currentTarget).text();
app.addPopup(UserPopup, {name: name, sourceEl: e.currentTarget}); app.addPopup(UserPopup, { name: name, sourceEl: e.currentTarget });
}, },
clickPMButtonBarChallenge: function (e) { clickPMButtonBarChallenge: function (e) {
var name = $(e.currentTarget).closest('.pm-window').data('name'); var name = $(e.currentTarget).closest('.pm-window').data('name');
@ -426,7 +426,7 @@
e.stopPropagation(); e.stopPropagation();
var name = $(e.currentTarget).closest('.pm-window').data('name'); var name = $(e.currentTarget).closest('.pm-window').data('name');
var userid = toID($(e.currentTarget).closest('.pm-window').data('name')); var userid = toID($(e.currentTarget).closest('.pm-window').data('name'));
app.addPopup(UserOptions, {name: name, userid: userid, sourceEl: e.currentTarget}); app.addPopup(UserOptions, { name: name, userid: userid, sourceEl: e.currentTarget });
}, },
focusPM: function (name) { focusPM: function (name) {
this.openPM(name).prependTo(this.$pmBox).find('textarea[name=message]').focus(); this.openPM(name).prependTo(this.$pmBox).find('textarea[name=message]').focus();
@ -668,10 +668,10 @@
app.addPopup(AvatarsPopup); app.addPopup(AvatarsPopup);
}, },
openSounds: function () { openSounds: function () {
app.addPopup(SoundsPopup, {type: 'semimodal'}); app.addPopup(SoundsPopup, { type: 'semimodal' });
}, },
openOptions: function () { openOptions: function () {
app.addPopup(OptionsPopup, {type: 'semimodal'}); app.addPopup(OptionsPopup, { type: 'semimodal' });
}, },
// challenges and searching // challenges and searching
@ -1002,7 +1002,7 @@
} }
}, },
format: function (format, button) { format: function (format, button) {
if (window.BattleFormats) app.addPopup(FormatPopup, {format: format, sourceEl: button}); if (window.BattleFormats) app.addPopup(FormatPopup, { format: format, sourceEl: button });
}, },
adjustPrivacy: function (disallowSpectators) { adjustPrivacy: function (disallowSpectators) {
Storage.prefs('disallowspectators', disallowSpectators); Storage.prefs('disallowspectators', disallowSpectators);
@ -1012,7 +1012,7 @@
}, },
team: function (team, button) { team: function (team, button) {
var format = $(button).closest('form').find('button[name=format]').val(); var format = $(button).closest('form').find('button[name=format]').val();
app.addPopup(TeamPopup, {team: team, format: format, sourceEl: button, folderToggleOn: true, folderNotExpanded: []}); app.addPopup(TeamPopup, { team: team, format: format, sourceEl: button, folderToggleOn: true, folderNotExpanded: [] });
}, },
// format/team selection // format/team selection
@ -1142,7 +1142,7 @@
app.addPopupPrompt("Username", "Open", function (target) { app.addPopupPrompt("Username", "Open", function (target) {
if (!target) return; if (!target) return;
if (toID(target) === 'zarel') { if (toID(target) === 'zarel') {
app.addPopup(Popup, {htmlMessage: "Zarel is very busy; please don't contact him this way. If you're looking for help, try <a href=\"/help\">joining the Help room</a>?"}); app.addPopup(Popup, { htmlMessage: "Zarel is very busy; please don't contact him this way. If you're looking for help, try <a href=\"/help\">joining the Help room</a>?" });
return; return;
} }
if (target === '~') { if (target === '~') {
@ -1150,7 +1150,7 @@
app.rooms[''].focusPM('~'); app.rooms[''].focusPM('~');
return; return;
} }
app.addPopup(UserPopup, {name: target}); app.addPopup(UserPopup, { name: target });
}); });
} }
}, { }, {
@ -1202,14 +1202,14 @@
case 'data-move': case 'data-move':
return '[outdated message type not supported]'; return '[outdated message type not supported]';
case 'text': case 'text':
return {message: '<div class="chat">' + BattleLog.parseMessage(target) + '</div>', noNotify: true}; return { message: '<div class="chat">' + BattleLog.parseMessage(target) + '</div>', noNotify: true };
case 'error': case 'error':
return '<div class="chat message-error">' + BattleLog.escapeHTML(target) + '</div>'; return '<div class="chat message-error">' + BattleLog.escapeHTML(target) + '</div>';
case 'html': case 'html':
if (!name) { if (!name) {
return {message: '<div class="chat' + hlClass + '">' + timestamp + '<em>' + BattleLog.sanitizeHTML(target) + '</em></div>', noNotify: isNotPM}; return { message: '<div class="chat' + hlClass + '">' + timestamp + '<em>' + BattleLog.sanitizeHTML(target) + '</em></div>', noNotify: isNotPM };
} }
return {message: '<div class="chat chatmessage-' + toID(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.sanitizeHTML(target) + '</em></div>', noNotify: isNotPM}; return { message: '<div class="chat chatmessage-' + toID(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.sanitizeHTML(target) + '</em></div>', noNotify: isNotPM };
case 'uhtml': case 'uhtml':
case 'uhtmlchange': case 'uhtmlchange':
var parts = target.split(','); var parts = target.split(',');
@ -1225,13 +1225,13 @@
$elements.remove(); $elements.remove();
$chatElem.append('<div class="chat uhtml-' + toID(parts[0]) + ' chatmessage-' + toID(name) + '">' + BattleLog.sanitizeHTML(html) + '</div>'); $chatElem.append('<div class="chat uhtml-' + toID(parts[0]) + ' chatmessage-' + toID(name) + '">' + BattleLog.sanitizeHTML(html) + '</div>');
} }
return {message: '', noNotify: isNotPM}; return { message: '', noNotify: isNotPM };
case 'raw': case 'raw':
return {message: '<div class="chat chatmessage-' + toID(name) + '">' + BattleLog.sanitizeHTML(target) + '</div>', noNotify: isNotPM}; return { message: '<div class="chat chatmessage-' + toID(name) + '">' + BattleLog.sanitizeHTML(target) + '</div>', noNotify: isNotPM };
case 'nonotify': case 'nonotify':
return {message: '<div class="chat">' + timestamp + BattleLog.sanitizeHTML(target) + '</div>', noNotify: true}; return { message: '<div class="chat">' + timestamp + BattleLog.sanitizeHTML(target) + '</div>', noNotify: true };
case 'challenge': case 'challenge':
return {challenge: target}; return { challenge: target };
default: default:
// Not a command or unsupported. Parsed as a normal chat message. // Not a command or unsupported. Parsed as a normal chat message.
if (!name) { if (!name) {
@ -1246,7 +1246,7 @@
events: { events: {
'keyup input[name=search]': 'updateSearch', 'keyup input[name=search]': 'updateSearch',
'click details': 'updateOpen', 'click details': 'updateOpen',
'click i.fa': 'updateStar', 'click i.fa': 'updateStar'
}, },
initialize: function (data) { initialize: function (data) {
this.data = data; this.data = data;
@ -1258,7 +1258,7 @@
"S/V Singles": true, "S/V Doubles": true, "Unofficial Metagames": true, "National Dex": true, "OM of the Month": true, "S/V Singles": true, "S/V Doubles": true, "Unofficial Metagames": true, "National Dex": true, "OM of the Month": true,
"Other Metagames": true, "Randomized Format Spotlight": true, "RoA Spotlight": true, "Other Metagames": true, "Randomized Format Spotlight": true, "RoA Spotlight": true,
// For AFD // For AFD
"Random Meta of the Decade": true, "Random Meta of the Decade": true
}; };
} }
if (!this.starred) this.starred = Storage.prefs('starredformats') || {}; if (!this.starred) this.starred = Storage.prefs('starredformats') || {};
@ -1387,7 +1387,7 @@
if (!format.isTeambuilderFormat) return false; if (!format.isTeambuilderFormat) return false;
} else { } else {
if (format.effectType !== 'Format' || format.battleFormat) return false; if (format.effectType !== 'Format' || format.battleFormat) return false;
if (this.selectType != 'watch' && !format[this.selectType + 'Show']) return false; if (this.selectType !== 'watch' && !format[this.selectType + 'Show']) return false;
} }
return true; return true;
}, },
@ -1531,7 +1531,7 @@
} else { } else {
bufs[curBuf] += '<li><button name="selectFolder" class="button" value="(No Folder)"><i class="fa fa-folder" style="margin-right: 7px; margin-left: 4px;"></i>(No Folder)</button></li>'; bufs[curBuf] += '<li><button name="selectFolder" class="button" value="(No Folder)"><i class="fa fa-folder" style="margin-right: 7px; margin-left: 4px;"></i>(No Folder)</button></li>';
count++; count++;
if (count % bufBoundary === 0 && count != 0 && curBuf < 4) curBuf++; if (count % bufBoundary === 0 && count !== 0 && curBuf < 4) curBuf++;
} }
if (!isNoFolder) { if (!isNoFolder) {
for (var i = 0; i < teams.length; i++) { for (var i = 0; i < teams.length; i++) {
@ -1578,11 +1578,11 @@
} }
}, },
events: { events: {
'click input[type=checkbox]': 'foldersToggle', 'click input[type=checkbox]': 'foldersToggle'
}, },
moreTeams: function () { moreTeams: function () {
this.close(); this.close();
app.addPopup(TeamPopup, {team: this.team, format: this.format, sourceEl: this.sourceEl, room: this.room, isMoreTeams: true, folderToggleOn: this.folderToggleOn, folderNotExpanded: this.folderNotExpanded}); app.addPopup(TeamPopup, { team: this.team, format: this.format, sourceEl: this.sourceEl, room: this.room, isMoreTeams: true, folderToggleOn: this.folderToggleOn, folderNotExpanded: this.folderNotExpanded });
}, },
teambuilder: function () { teambuilder: function () {
var teamFormat = this.teamFormat; var teamFormat = this.teamFormat;
@ -1599,19 +1599,18 @@
if (folder === key) { if (folder === key) {
keyExists = true; keyExists = true;
return false; return false;
} else {
return true;
} }
return true;
}); });
if (!keyExists) { if (!keyExists) {
folderNotExpanded.push(key); folderNotExpanded.push(key);
} }
this.close(); this.close();
app.addPopup(TeamPopup, {team: this.team, format: this.format, sourceEl: this.sourceEl, room: this.room, isMoreTeams: this.isMoreTeams, folderToggleOn: this.folderToggleOn, folderNotExpanded: folderNotExpanded}); app.addPopup(TeamPopup, { team: this.team, format: this.format, sourceEl: this.sourceEl, room: this.room, isMoreTeams: this.isMoreTeams, folderToggleOn: this.folderToggleOn, folderNotExpanded: folderNotExpanded });
}, },
foldersToggle: function () { foldersToggle: function () {
this.close(); this.close();
app.addPopup(TeamPopup, {team: this.team, format: this.format, sourceEl: this.sourceEl, room: this.room, isMoreTeams: this.isMoreTeams, folderToggleOn: !this.folderToggleOn, folderNotExpanded: this.folderNotExpanded}); app.addPopup(TeamPopup, { team: this.team, format: this.format, sourceEl: this.sourceEl, room: this.room, isMoreTeams: this.isMoreTeams, folderToggleOn: !this.folderToggleOn, folderNotExpanded: this.folderNotExpanded });
}, },
selectTeam: function (i) { selectTeam: function (i) {
i = +i; i = +i;

View File

@ -117,8 +117,8 @@
if (rooms.userCount) { if (rooms.userCount) {
var userCount = Number(rooms.userCount); var userCount = Number(rooms.userCount);
var battleCount = Number(rooms.battleCount); var battleCount = Number(rooms.battleCount);
var leftSide = '<button class="button" name="finduser" title="Find an online user"><span class="pixelated usercount" title="Meloetta is PS\'s mascot! The Aria forme is about using its voice, and represents our chatrooms." ></span><strong>' + userCount + '</strong> ' + (userCount == 1 ? 'user' : 'users') + ' online</button> '; var leftSide = '<button class="button" name="finduser" title="Find an online user"><span class="pixelated usercount" title="Meloetta is PS\'s mascot! The Aria forme is about using its voice, and represents our chatrooms." ></span><strong>' + userCount + '</strong> ' + (userCount === 1 ? 'user' : 'users') + ' online</button> ';
var rightSide = '<button class="button" name="roomlist" title="Watch an active battle"><span class="pixelated battlecount" title="Meloetta is PS\'s mascot! The Pirouette forme is Fighting-type, and represents our battles." ></span><strong>' + battleCount + '</strong> active ' + (battleCount == 1 ? 'battle' : 'battles') + '</button>'; var rightSide = '<button class="button" name="roomlist" title="Watch an active battle"><span class="pixelated battlecount" title="Meloetta is PS\'s mascot! The Pirouette forme is Fighting-type, and represents our battles." ></span><strong>' + battleCount + '</strong> active ' + (battleCount === 1 ? 'battle' : 'battles') + '</button>';
this.$('.roomlisttop').html('<div class="roomcounters">' + leftSide + '</td><td>' + rightSide + '</div>'); this.$('.roomlisttop').html('<div class="roomcounters">' + leftSide + '</td><td>' + rightSide + '</div>');
} }
@ -174,9 +174,11 @@
); );
this.$('.roomlist').last().html( this.$('.roomlist').last().html(
(otherRooms.length ? (otherRooms.length ?
'<h2 class="rooms-chatrooms">Chat rooms</h2>' + otherRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : '') + '<h2 class="rooms-chatrooms">Chat rooms</h2>' + otherRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : ''
) +
(hiddenRooms.length && this.showMoreRooms ? (hiddenRooms.length && this.showMoreRooms ?
'<h2 class="rooms-chatrooms">Hidden rooms</h2>' + hiddenRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : '') '<h2 class="rooms-chatrooms">Hidden rooms</h2>' + hiddenRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : ''
)
); );
}, },
roomlist: function () { roomlist: function () {
@ -196,10 +198,10 @@
app.addPopupPrompt("Username", "Open", function (target) { app.addPopupPrompt("Username", "Open", function (target) {
if (!target) return; if (!target) return;
if (toID(target) === 'zarel') { if (toID(target) === 'zarel') {
app.addPopup(Popup, {htmlMessage: "Zarel is very busy; please don't contact him this way. If you're looking for help, try <a href=\"/help\">joining the Help room</a>?"}); app.addPopup(Popup, { htmlMessage: "Zarel is very busy; please don't contact him this way. If you're looking for help, try <a href=\"/help\">joining the Help room</a>?" });
return; return;
} }
app.addPopup(UserPopup, {name: target}); app.addPopup(UserPopup, { name: target });
}); });
}, },
refresh: function () { refresh: function () {
@ -243,9 +245,9 @@
return; return;
} }
var self = this; var self = this;
app.addPopup(FormatPopup, {format: format, sourceEl: button, selectType: 'watch', onselect: function (newFormat) { app.addPopup(FormatPopup, { format: format, sourceEl: button, selectType: 'watch', onselect: function (newFormat) {
self.changeFormat(newFormat); self.changeFormat(newFormat);
}}); } });
}, },
changeFormat: function (format) { changeFormat: function (format) {
this.format = format; this.format = format;

View File

@ -52,8 +52,8 @@
'change .detailsform input': 'detailsChange', 'change .detailsform input': 'detailsChange',
'change .detailsform select': 'detailsChange', 'change .detailsform select': 'detailsChange',
'submit .detailsform': 'detailsChange', 'submit .detailsform': 'detailsChange',
'click .changeform' : 'altForm', 'click .changeform': 'altForm',
'click .altform' : 'altForm', 'click .altform': 'altForm',
// stats // stats
'keyup .statform input.numform': 'statChange', 'keyup .statform input.numform': 'statChange',
@ -544,7 +544,7 @@
this.teamScrollPos = 0; this.teamScrollPos = 0;
} }
//reset focus to searchbar // reset focus to searchbar
var teamSearchBar = this.$("#teamSearchBar"); var teamSearchBar = this.$("#teamSearchBar");
var strLength = teamSearchBar.val().length; var strLength = teamSearchBar.val().length;
if (strLength) { if (strLength) {
@ -555,7 +555,6 @@
updatePersistence: function (state) { updatePersistence: function (state) {
if (state) { if (state) {
this.$('.storage-warning').html(''); this.$('.storage-warning').html('');
return;
} }
}, },
greeting: function (answer, button) { greeting: function (answer, button) {
@ -640,9 +639,9 @@
if (format === '+') { if (format === '+') {
e.stopImmediatePropagation(); e.stopImmediatePropagation();
var self = this; var self = this;
app.addPopup(FormatPopup, {format: '', sourceEl: e.currentTarget, selectType: 'teambuilder', onselect: function (newFormat) { app.addPopup(FormatPopup, { format: '', sourceEl: e.currentTarget, selectType: 'teambuilder', onselect: function (newFormat) {
self.selectFolder(newFormat); self.selectFolder(newFormat);
}}); } });
return; return;
} }
if (format === '++') { if (format === '++') {
@ -651,7 +650,7 @@
// app.addPopupPrompt("Folder name:", "Create folder", function (newFormat) { // app.addPopupPrompt("Folder name:", "Create folder", function (newFormat) {
// self.selectFolder(newFormat + '/'); // self.selectFolder(newFormat + '/');
// }); // });
app.addPopup(PromptPopup, {message: "Folder name:", button: "Create folder", sourceEl: e.currentTarget, callback: function (name) { app.addPopup(PromptPopup, { message: "Folder name:", button: "Create folder", sourceEl: e.currentTarget, callback: function (name) {
name = $.trim(name); name = $.trim(name);
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) { if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) {
app.addPopupMessage("Names can't contain slashes, since they're used as a folder separator."); app.addPopupMessage("Names can't contain slashes, since they're used as a folder separator.");
@ -663,7 +662,7 @@
} }
if (!name) return; if (!name) return;
self.selectFolder(name + '/'); self.selectFolder(name + '/');
}}); } });
return; return;
} }
} else { } else {
@ -678,7 +677,7 @@
if (this.curFolder.slice(-1) !== '/') return; if (this.curFolder.slice(-1) !== '/') return;
var oldFolder = this.curFolder.slice(0, -1); var oldFolder = this.curFolder.slice(0, -1);
var self = this; var self = this;
app.addPopup(PromptPopup, {message: "Folder name:", button: "Rename folder", value: oldFolder, callback: function (name) { app.addPopup(PromptPopup, { message: "Folder name:", button: "Rename folder", value: oldFolder, callback: function (name) {
name = $.trim(name); name = $.trim(name);
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) { if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) {
app.addPopupMessage("Names can't contain slashes, since they're used as a folder separator."); app.addPopupMessage("Names can't contain slashes, since they're used as a folder separator.");
@ -698,10 +697,10 @@
} }
if (!window.nodewebkit) Storage.saveTeams(); if (!window.nodewebkit) Storage.saveTeams();
self.selectFolder(name + '/'); self.selectFolder(name + '/');
}}); } });
}, },
promptDeleteFolder: function () { promptDeleteFolder: function () {
app.addPopup(DeleteFolderPopup, {folder: this.curFolder, room: this}); app.addPopup(DeleteFolderPopup, { folder: this.curFolder, room: this });
}, },
deleteFolder: function (format, addName) { deleteFolder: function (format, addName) {
if (format.slice(-1) !== '/') return; if (format.slice(-1) !== '/') return;
@ -1397,7 +1396,7 @@
evBuf += '<small>&minus;</small>'; evBuf += '<small>&minus;</small>';
} }
var width = stats[j] * 75 / 504; var width = stats[j] * 75 / 504;
if (j == 'hp') width = stats[j] * 75 / 704; if (j === 'hp') width = stats[j] * 75 / 704;
if (width > 75) width = 75; if (width > 75) width = 75;
var color = Math.floor(stats[j] * 180 / 714); var color = Math.floor(stats[j] * 180 / 714);
if (color > 360) color = 360; if (color > 360) color = 360;
@ -1430,7 +1429,7 @@
if (format) self.changeFormat(format.id); if (format) self.changeFormat(format.id);
notes.shift(); notes.shift();
} }
var teamNotes = notes.join('\n'); // Not implemented yet // var teamNotes = notes.join('\n'); // Not implemented yet
var title = data.title; var title = data.title;
if (title && !title.startsWith('Untitled')) { if (title && !title.startsWith('Untitled')) {
@ -1595,9 +1594,9 @@
return; return;
} }
var self = this; var self = this;
app.addPopup(FormatPopup, {format: format, sourceEl: button, selectType: 'teambuilder', onselect: function (newFormat) { app.addPopup(FormatPopup, { format: format, sourceEl: button, selectType: 'teambuilder', onselect: function (newFormat) {
self.changeFormat(newFormat); self.changeFormat(newFormat);
}}); } });
}, },
changeFormat: function (format) { changeFormat: function (format) {
this.curTeam.format = format; this.curTeam.format = format;
@ -1683,7 +1682,7 @@
clipboardExpanded: false, clipboardExpanded: false,
clipboardExpand: function () { clipboardExpand: function () {
var $clipboard = $('.teambuilder-clipboard-data'); var $clipboard = $('.teambuilder-clipboard-data');
$clipboard.animate({height: this.clipboardCount() * 34}, 500, function () { $clipboard.animate({ height: this.clipboardCount() * 34 }, 500, function () {
setTimeout(function () { $clipboard.focus(); }, 100); setTimeout(function () { $clipboard.focus(); }, 100);
}); });
@ -1693,7 +1692,7 @@
}, },
clipboardShrink: function () { clipboardShrink: function () {
var $clipboard = $('.teambuilder-clipboard-data'); var $clipboard = $('.teambuilder-clipboard-data');
$clipboard.animate({height: 32}, 500); $clipboard.animate({ height: 32 }, 500);
setTimeout(function () { setTimeout(function () {
this.clipboardExpanded = false; this.clipboardExpanded = false;
@ -1725,7 +1724,7 @@
if (this.clipboardCount() === 1) { if (this.clipboardCount() === 1) {
var $clipboard = $('.teambuilder-clipboard-container').css('opacity', 0); var $clipboard = $('.teambuilder-clipboard-container').css('opacity', 0);
$clipboard.slideDown(250, function () { $clipboard.slideDown(250, function () {
$clipboard.animate({opacity: 1}, 250); $clipboard.animate({ opacity: 1 }, 250);
}); });
} }
}, },
@ -1734,7 +1733,7 @@
var self = this; var self = this;
var $clipboard = $('.teambuilder-clipboard-container'); var $clipboard = $('.teambuilder-clipboard-container');
$clipboard.animate({opacity: 0}, 250, function () { $clipboard.animate({ opacity: 0 }, 250, function () {
$clipboard.slideUp(250, function () { $clipboard.slideUp(250, function () {
self.clipboardUpdate(); self.clipboardUpdate();
}); });
@ -2016,7 +2015,7 @@
if (!set.species) { if (!set.species) {
buf += '<button disabled class="addpokemon" aria-label="Add Pok&eacute;mon"><i class="fa fa-plus"></i></button> '; buf += '<button disabled class="addpokemon" aria-label="Add Pok&eacute;mon"><i class="fa fa-plus"></i></button> ';
isAdd = true; isAdd = true;
} else if (i == this.curSetLoc) { } else if (i === this.curSetLoc) {
buf += '<button disabled class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species).baseSpecies || '<i class="fa fa-plus"></i>') + '</button> '; buf += '<button disabled class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species).baseSpecies || '<i class="fa fa-plus"></i>') + '</button> ';
} else { } else {
buf += '<button name="selectPokemon" value="' + i + '" class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species).baseSpecies) + '</button> '; buf += '<button name="selectPokemon" value="' + i + '" class="pokemon">' + pokemonicon + BattleLog.escapeHTML(set.name || this.curTeam.dex.species.get(set.species).baseSpecies) + '</button> ';
@ -2048,7 +2047,7 @@
var set = this.curSet; var set = this.curSet;
if (!set) return; if (!set) return;
var stats = {hp:'', atk:'', def:'', spa:'', spd:'', spe:''}; var stats = { hp: '', atk: '', def: '', spa: '', spd: '', spe: '' };
var supportsEVs = !this.curTeam.format.includes('letsgo'); var supportsEVs = !this.curTeam.format.includes('letsgo');
@ -2066,7 +2065,7 @@
evBuf += '<small>&minus;</small>'; evBuf += '<small>&minus;</small>';
} }
var width = stats[stat] * 75 / 504; var width = stats[stat] * 75 / 504;
if (stat == 'hp') width = stats[stat] * 75 / 704; if (stat === 'hp') width = stats[stat] * 75 / 704;
if (width > 75) width = 75; if (width > 75) width = 75;
var color = Math.floor(stats[stat] * 180 / 714); var color = Math.floor(stats[stat] * 180 / 714);
if (color > 360) color = 360; if (color > 360) color = 360;
@ -2089,7 +2088,7 @@
for (var stat in stats) { for (var stat in stats) {
if (stat === 'spd' && this.curTeam.gen === 1) continue; if (stat === 'spd' && this.curTeam.gen === 1) continue;
var width = stats[stat] * 180 / 504; var width = stats[stat] * 180 / 504;
if (stat == 'hp') width = stats[stat] * 180 / 704; if (stat === 'hp') width = stats[stat] * 180 / 704;
if (width > 179) width = 179; if (width > 179) width = 179;
var color = Math.floor(stats[stat] * 180 / 714); var color = Math.floor(stats[stat] * 180 / 714);
if (color > 360) color = 360; if (color > 360) color = 360;
@ -2311,7 +2310,7 @@
return; return;
} }
var stats = {hp:'', atk:'', def:'', spa:'', spd:'', spe:''}; var stats = { hp: '', atk: '', def: '', spa: '', spd: '', spe: '' };
if (this.curTeam.gen === 1) delete stats.spd; if (this.curTeam.gen === 1) delete stats.spd;
if (!set) return; if (!set) return;
var nature = BattleNatures[set.nature || 'Serious']; var nature = BattleNatures[set.nature || 'Serious'];
@ -2344,7 +2343,7 @@
for (var i in stats) { for (var i in stats) {
stats[i] = this.getStat(i); stats[i] = this.getStat(i);
var width = stats[i] * 180 / 504; var width = stats[i] * 180 / 504;
if (i == 'hp') width = Math.floor(stats[i] * 180 / 704); if (i === 'hp') width = Math.floor(stats[i] * 180 / 704);
if (width > 179) width = 179; if (width > 179) width = 179;
var color = Math.floor(stats[i] * 180 / 714); var color = Math.floor(stats[i] * 180 / 714);
if (color > 360) color = 360; if (color > 360) color = 360;
@ -2709,7 +2708,7 @@
} else { } else {
var hpTypeX = 0; var hpTypeX = 0;
var i = 1; var i = 1;
var stats = {hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31}; var stats = { hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31 };
for (var s in stats) { for (var s in stats) {
if (set.ivs[s] === undefined) set.ivs[s] = 31; if (set.ivs[s] === undefined) set.ivs[s] = 31;
hpTypeX += i * (set.ivs[s] % 2); hpTypeX += i * (set.ivs[s] % 2);
@ -2735,7 +2734,7 @@
var supportsEVs = !this.curTeam.format.includes('letsgo'); var supportsEVs = !this.curTeam.format.includes('letsgo');
var supportsAVs = !supportsEVs && this.curTeam.format.endsWith('norestrictions'); var supportsAVs = !supportsEVs && this.curTeam.format.endsWith('norestrictions');
if (supportsEVs) { if (supportsEVs) {
while (val > 0 && this.getStat(stat, set, val - 4) == result) val -= 4; while (val > 0 && this.getStat(stat, set, val - 4) === result) val -= 4;
} }
if (supportsEVs && !this.ignoreEVLimits && set.evs) { if (supportsEVs && !this.ignoreEVLimits && set.evs) {
@ -2848,7 +2847,7 @@
if (this.curTeam.gen > 1) { if (this.curTeam.gen > 1) {
buf += '<div class="formrow"><label class="formlabel">Gender:</label><div>'; buf += '<div class="formrow"><label class="formlabel">Gender:</label><div>';
if (species.gender && !isHackmons) { if (species.gender && !isHackmons) {
var genderTable = {'M': "Male", 'F': "Female", 'N': "Genderless"}; var genderTable = { 'M': "Male", 'F': "Female", 'N': "Genderless" };
buf += genderTable[species.gender]; buf += genderTable[species.gender];
} else { } else {
buf += '<label class="checkbox inline"><input type="radio" name="gender" value="M"' + (set.gender === 'M' ? ' checked' : '') + ' /> Male</label> '; buf += '<label class="checkbox inline"><input type="radio" name="gender" value="M"' + (set.gender === 'M' ? ' checked' : '') + ' /> Male</label> ';
@ -3050,7 +3049,7 @@
i = +$(e.currentTarget).closest('li').attr('value'); i = +$(e.currentTarget).closest('li').attr('value');
set = this.curSetList[i]; set = this.curSetList[i];
} }
app.addPopup(AltFormPopup, {curSet: set, index: i, room: this}); app.addPopup(AltFormPopup, { curSet: set, index: i, room: this });
}, },
/********************************************************* /*********************************************************
@ -3125,7 +3124,6 @@
var entry = $firstResult.data('entry'); var entry = $firstResult.data('entry');
var val = entry.slice(entry.indexOf("|") + 1); var val = entry.slice(entry.indexOf("|") + 1);
this.chartSet(val, true); this.chartSet(val, true);
return;
} else if (e.keyCode === 38) { // up } else if (e.keyCode === 38) { // up
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -3153,7 +3151,6 @@
} else if (e.keyCode === 27 || e.keyCode === 8) { // esc, backspace } else if (e.keyCode === 27 || e.keyCode === 8) { // esc, backspace
if (!e.currentTarget.value && this.search.removeFilter()) { if (!e.currentTarget.value && this.search.removeFilter()) {
this.search.find(''); this.search.find('');
return;
} }
} else if (e.keyCode === 188) { } else if (e.keyCode === 188) {
var $firstResult = this.$chart.find('a').first(); var $firstResult = this.$chart.find('a').first();
@ -3163,7 +3160,6 @@
e.stopPropagation(); e.stopPropagation();
$(e.currentTarget).val('').select(); $(e.currentTarget).val('').select();
this.search.find(''); this.search.find('');
return;
} }
} }
}, },
@ -3294,7 +3290,7 @@
set.item = 'Starf Berry'; set.item = 'Starf Berry';
set.ability = 'Harvest'; set.ability = 'Harvest';
set.moves = ['Substitute', 'Horn Leech', 'Earthquake', 'Phantom Force']; set.moves = ['Substitute', 'Horn Leech', 'Earthquake', 'Phantom Force'];
set.evs = {hp: 36, atk: 252, def: 0, spa: 0, spd: 0, spe: 220}; set.evs = { hp: 36, atk: 252, def: 0, spa: 0, spd: 0, spe: 220 };
set.ivs = {}; set.ivs = {};
set.nature = 'Jolly'; set.nature = 'Jolly';
this.updateSetTop(); this.updateSetTop();
@ -3328,7 +3324,7 @@
set.item = 'Leftovers'; set.item = 'Leftovers';
set.ability = 'Battle Armor'; set.ability = 'Battle Armor';
set.moves = ['Acupressure', 'Knock Off', 'Rest', 'Sleep Talk']; set.moves = ['Acupressure', 'Knock Off', 'Rest', 'Sleep Talk'];
set.evs = {hp: 248, atk: 0, def: 96, spa: 0, spd: 108, spe: 56}; set.evs = { hp: 248, atk: 0, def: 96, spa: 0, spd: 108, spe: 56 };
set.ivs = {}; set.ivs = {};
set.nature = 'Impish'; set.nature = 'Impish';
this.updateSetTop(); this.updateSetTop();
@ -3428,7 +3424,7 @@
if (!this.canHyperTrain(set)) { if (!this.canHyperTrain(set)) {
var hpType = moveName.substr(13); var hpType = moveName.substr(13);
set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
if (this.curTeam.gen > 2) { if (this.curTeam.gen > 2) {
var HPivs = this.curTeam.dex.types.get(hpType).HPivs; var HPivs = this.curTeam.dex.types.get(hpType).HPivs;
for (var i in HPivs) { for (var i in HPivs) {
@ -3736,7 +3732,7 @@
buf += '<div style="clear:both"></div>'; buf += '<div style="clear:both"></div>';
buf += '</div>'; buf += '</div>';
this.$el.html(buf).css({'max-width': (4 + spriteSize) * 7}); this.$el.html(buf).css({ 'max-width': (4 + spriteSize) * 7 });
}, },
setForm: function (form) { setForm: function (form) {
var species = Dex.species.get(this.curSet.species); var species = Dex.species.get(this.curSet.species);

View File

@ -56,7 +56,7 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var name = $(e.currentTarget).data('name'); var name = $(e.currentTarget).data('name');
app.addPopup(UserPopup, {name: name, sourceEl: e.currentTarget}); app.addPopup(UserPopup, { name: name, sourceEl: e.currentTarget });
}, },
toggleMute: function () { toggleMute: function () {
var muted = !Dex.prefs('mute'); var muted = !Dex.prefs('mute');
@ -224,7 +224,7 @@
e.preventDefault(); e.preventDefault();
var $target = $(e.currentTarget); var $target = $(e.currentTarget);
if ($target.hasClass('minilogo')) { if ($target.hasClass('minilogo')) {
app.addPopup(TabListPopup, {sourceEl: e.currentTarget}); app.addPopup(TabListPopup, { sourceEl: e.currentTarget });
return; return;
} }
var id = $target.attr('href'); var id = $target.attr('href');
@ -543,7 +543,7 @@
"हिंदी": 'hindi', "हिंदी": 'hindi',
"日本語": 'japanese', "日本語": 'japanese',
"简体中文": 'simplifiedchinese', "简体中文": 'simplifiedchinese',
"中文": 'traditionalchinese', "中文": 'traditionalchinese'
}; };
buf += '<p><label class="optlabel">Language: <select name="language" class="button">'; buf += '<p><label class="optlabel">Language: <select name="language" class="button">';
for (var name in possibleLanguages) { for (var name in possibleLanguages) {
@ -877,7 +877,7 @@
popup.$('.cur').removeClass('cur'); popup.$('.cur').removeClass('cur');
Storage.bg.set(e.target.result, 'custom'); Storage.bg.set(e.target.result, 'custom');
} else { } else {
app.addPopup(ConfirmBackgroundPopup, {bgUrl: e.target.result}); app.addPopup(ConfirmBackgroundPopup, { bgUrl: e.target.result });
} }
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);

View File

@ -1,3 +1,4 @@
/* exported toId */
function toId() { function toId() {
// toId has been renamed toID // toId has been renamed toID
alert("You have an old extension/script for Pokemon Showdown which is incompatible with this client. It needs to be removed or updated."); alert("You have an old extension/script for Pokemon Showdown which is incompatible with this client. It needs to be removed or updated.");
@ -20,7 +21,7 @@ function toId() {
} }
$(document).on('keydown', function (e) { $(document).on('keydown', function (e) {
if (e.keyCode == 27) { // Esc if (e.keyCode === 27) { // Esc
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
@ -178,8 +179,8 @@ function toId() {
Storage.prefs('serversettings', self.get('settings')); Storage.prefs('serversettings', self.get('settings'));
}); });
var replaceList = {'A': 'AⱯȺ', 'B': 'BƂƁɃ', 'C': 'CꜾȻ', 'D': 'DĐƋƊƉꝹ', 'E': 'EƐƎ', 'F': 'FƑꝻ', 'G': 'GꞠꝽꝾ', 'H': 'HĦⱧⱵꞍ', 'I': 'IƗ', 'J': 'JɈ', 'K': 'KꞢ', 'L': 'LꝆꞀ', 'M': 'MⱮƜ', 'N': 'NȠƝꞐꞤ', 'O': 'OǪǬØǾƆƟꝊꝌ', 'P': 'PƤⱣꝐꝒꝔ', 'Q': 'QꝖꝘɊ', 'R': 'RɌⱤꝚꞦꞂ', 'S': 'SẞꞨꞄ', 'T': 'TŦƬƮȾꞆ', 'U': 'UɄ', 'V': 'VƲꝞɅ', 'W': 'WⱲ', 'X': '', 'Y': 'YɎỾ', 'Z': 'ZƵȤⱿⱫꝢ', 'a': 'aąⱥɐ', 'b': 'bƀƃɓ', 'c': 'cȼꜿↄ', 'd': 'dđƌɖɗꝺ', 'e': 'eɇɛǝ', 'f': 'fḟƒꝼ', 'g': 'gɠꞡᵹꝿ', 'h': 'hħⱨⱶɥ', 'i': 'iɨı', 'j': 'jɉ', 'k': 'kƙⱪꝁꝃꝅꞣ', 'l': 'lſłƚɫⱡꝉꞁꝇ', 'm': 'mɱɯ', 'n': 'nƞɲʼnꞑꞥ', 'o': 'oǫǭøǿɔꝋꝍɵ', 'p': 'pƥᵽꝑꝓꝕ', 'q': 'qɋꝗꝙ', 'r': 'rɍɽꝛꞧꞃ', 's': 'sꞩꞅẛ', 't': 'tŧƭʈⱦꞇ', 'u': 'uưừứữửựųṷṵʉ', 'v': 'vʋꝟʌ', 'w': 'wⱳ', 'x': '', 'y': 'yɏỿ', 'z': 'zƶȥɀⱬꝣ', 'AA': 'Ꜳ', 'AE': 'ÆǼǢ', 'AO': 'Ꜵ', 'AU': 'Ꜷ', 'AV': 'ꜸꜺ', 'AY': 'Ꜽ', 'DZ': 'DZDŽ', 'Dz': 'DzDž', 'LJ': 'LJ', 'Lj': 'Lj', 'NJ': 'NJ', 'Nj': 'Nj', 'OI': 'Ƣ', 'OO': 'Ꝏ', 'OU': 'Ȣ', 'TZ': 'Ꜩ', 'VY': 'Ꝡ', 'aa': 'ꜳ', 'ae': 'æǽǣ', 'ao': 'ꜵ', 'au': 'ꜷ', 'av': 'ꜹꜻ', 'ay': 'ꜽ', 'dz': 'dzdž', 'hv': 'ƕ', 'lj': 'lj', 'nj': 'nj', 'oi': 'ƣ', 'ou': 'ȣ', 'oo': 'ꝏ', 'ss': 'ß', 'tz': 'ꜩ', 'vy': 'ꝡ'}; var replaceList = { 'A': 'AⱯȺ', 'B': 'BƂƁɃ', 'C': 'CꜾȻ', 'D': 'DĐƋƊƉꝹ', 'E': 'EƐƎ', 'F': 'FƑꝻ', 'G': 'GꞠꝽꝾ', 'H': 'HĦⱧⱵꞍ', 'I': 'IƗ', 'J': 'JɈ', 'K': 'KꞢ', 'L': 'LꝆꞀ', 'M': 'MⱮƜ', 'N': 'NȠƝꞐꞤ', 'O': 'OǪǬØǾƆƟꝊꝌ', 'P': 'PƤⱣꝐꝒꝔ', 'Q': 'QꝖꝘɊ', 'R': 'RɌⱤꝚꞦꞂ', 'S': 'SẞꞨꞄ', 'T': 'TŦƬƮȾꞆ', 'U': 'UɄ', 'V': 'VƲꝞɅ', 'W': 'WⱲ', 'X': '', 'Y': 'YɎỾ', 'Z': 'ZƵȤⱿⱫꝢ', 'a': 'aąⱥɐ', 'b': 'bƀƃɓ', 'c': 'cȼꜿↄ', 'd': 'dđƌɖɗꝺ', 'e': 'eɇɛǝ', 'f': 'fḟƒꝼ', 'g': 'gɠꞡᵹꝿ', 'h': 'hħⱨⱶɥ', 'i': 'iɨı', 'j': 'jɉ', 'k': 'kƙⱪꝁꝃꝅꞣ', 'l': 'lſłƚɫⱡꝉꞁꝇ', 'm': 'mɱɯ', 'n': 'nƞɲʼnꞑꞥ', 'o': 'oǫǭøǿɔꝋꝍɵ', 'p': 'pƥᵽꝑꝓꝕ', 'q': 'qɋꝗꝙ', 'r': 'rɍɽꝛꞧꞃ', 's': 'sꞩꞅẛ', 't': 'tŧƭʈⱦꞇ', 'u': 'uưừứữửựųṷṵʉ', 'v': 'vʋꝟʌ', 'w': 'wⱳ', 'x': '', 'y': 'yɏỿ', 'z': 'zƶȥɀⱬꝣ', 'AA': 'Ꜳ', 'AE': 'ÆǼǢ', 'AO': 'Ꜵ', 'AU': 'Ꜷ', 'AV': 'ꜸꜺ', 'AY': 'Ꜽ', 'DZ': 'DZDŽ', 'Dz': 'DzDž', 'LJ': 'LJ', 'Lj': 'Lj', 'NJ': 'NJ', 'Nj': 'Nj', 'OI': 'Ƣ', 'OO': 'Ꝏ', 'OU': 'Ȣ', 'TZ': 'Ꜩ', 'VY': 'Ꝡ', 'aa': 'ꜳ', 'ae': 'æǽǣ', 'ao': 'ꜵ', 'au': 'ꜷ', 'av': 'ꜹꜻ', 'ay': 'ꜽ', 'dz': 'dzdž', 'hv': 'ƕ', 'lj': 'lj', 'nj': 'nj', 'oi': 'ƣ', 'ou': 'ȣ', 'oo': 'ꝏ', 'ss': 'ß', 'tz': 'ꜩ', 'vy': 'ꝡ' };
var normalizeList = {'A': 'ÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄ', 'B': 'ḂḄḆ', 'C': 'ĆĈĊČÇḈƇ', 'D': 'ḊĎḌḐḒḎ', 'E': 'ÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚ', 'F': 'Ḟ', 'G': 'ǴĜḠĞĠǦĢǤƓ', 'H': 'ĤḢḦȞḤḨḪ', 'I': 'ÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬ', 'J': 'Ĵ', 'K': 'ḰǨḲĶḴƘⱩꝀꝂꝄ', 'L': 'ĿĹĽḶḸĻḼḺŁȽⱢⱠꝈ', 'M': 'ḾṀṂ', 'N': 'ǸŃÑṄŇṆŅṊṈ', 'O': 'ÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘ', 'P': 'ṔṖ', 'Q': '', 'R': 'ŔṘŘȐȒṚṜŖṞ', 'S': 'ŚṤŜṠŠṦṢṨȘŞⱾ', 'T': 'ṪŤṬȚŢṰṮ', 'U': 'ÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴ', 'V': 'ṼṾ', 'W': 'ẀẂŴẆẄẈ', 'X': 'ẊẌ', 'Y': 'ỲÝŶỸȲẎŸỶỴƳ', 'Z': 'ŹẐŻŽẒẔ', 'a': 'ẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁ', 'b': 'ḃḅḇ', 'c': 'ćĉċčçḉƈ', 'd': 'ḋďḍḑḓḏ', 'e': 'èéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛ', 'f': '', 'g': 'ǵĝḡğġǧģǥ', 'h': 'ĥḣḧȟḥḩḫẖ', 'i': 'ìíîĩīĭïḯỉǐȉȋịįḭ', 'j': 'ĵǰ', 'k': 'ḱǩḳķḵ', 'l': 'ŀĺľḷḹļḽḻ', 'm': 'ḿṁṃ', 'n': 'ǹńñṅňṇņṋṉ', 'o': 'òóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộ', 'p': 'ṕṗ', 'q': '', 'r': 'ŕṙřȑȓṛṝŗṟ', 's': 'śṥŝṡšṧṣṩșşȿ', 't': 'ṫẗťṭțţṱṯ', 'u': 'ùúûũṹūṻŭüǜǘǖǚủůűǔȕȗụṳ', 'v': 'ṽṿ', 'w': 'ẁẃŵẇẅẘẉ', 'x': 'ẋẍ', 'y': 'ỳýŷỹȳẏÿỷẙỵƴ', 'z': 'źẑżžẓẕ'}; var normalizeList = { 'A': 'ÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄ', 'B': 'ḂḄḆ', 'C': 'ĆĈĊČÇḈƇ', 'D': 'ḊĎḌḐḒḎ', 'E': 'ÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚ', 'F': 'Ḟ', 'G': 'ǴĜḠĞĠǦĢǤƓ', 'H': 'ĤḢḦȞḤḨḪ', 'I': 'ÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬ', 'J': 'Ĵ', 'K': 'ḰǨḲĶḴƘⱩꝀꝂꝄ', 'L': 'ĿĹĽḶḸĻḼḺŁȽⱢⱠꝈ', 'M': 'ḾṀṂ', 'N': 'ǸŃÑṄŇṆŅṊṈ', 'O': 'ÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘ', 'P': 'ṔṖ', 'Q': '', 'R': 'ŔṘŘȐȒṚṜŖṞ', 'S': 'ŚṤŜṠŠṦṢṨȘŞⱾ', 'T': 'ṪŤṬȚŢṰṮ', 'U': 'ÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴ', 'V': 'ṼṾ', 'W': 'ẀẂŴẆẄẈ', 'X': 'ẊẌ', 'Y': 'ỲÝŶỸȲẎŸỶỴƳ', 'Z': 'ŹẐŻŽẒẔ', 'a': 'ẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁ', 'b': 'ḃḅḇ', 'c': 'ćĉċčçḉƈ', 'd': 'ḋďḍḑḓḏ', 'e': 'èéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛ', 'f': '', 'g': 'ǵĝḡğġǧģǥ', 'h': 'ĥḣḧȟḥḩḫẖ', 'i': 'ìíîĩīĭïḯỉǐȉȋịįḭ', 'j': 'ĵǰ', 'k': 'ḱǩḳķḵ', 'l': 'ŀĺľḷḹļḽḻ', 'm': 'ḿṁṃ', 'n': 'ǹńñṅňṇņṋṉ', 'o': 'òóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộ', 'p': 'ṕṗ', 'q': '', 'r': 'ŕṙřȑȓṛṝŗṟ', 's': 'śṥŝṡšṧṣṩșşȿ', 't': 'ṫẗťṭțţṱṯ', 'u': 'ùúûũṹūṻŭüǜǘǖǚủůűǔȕȗụṳ', 'v': 'ṽṿ', 'w': 'ẁẃŵẇẅẘẉ', 'x': 'ẋẍ', 'y': 'ỳýŷỹȳẏÿỷẙỵƴ', 'z': 'źẑżžẓẕ' };
for (var i in replaceList) { for (var i in replaceList) {
replaceList[i] = new RegExp('[' + replaceList[i] + ']', 'g'); replaceList[i] = new RegExp('[' + replaceList[i] + ']', 'g');
} }
@ -409,7 +410,7 @@ function toId() {
// this.down = true; // this.down = true;
this.addRoom(''); this.addRoom('');
this.topbar = new Topbar({el: $('#header')}); this.topbar = new Topbar({ el: $('#header') });
if (this.down) { if (this.down) {
this.isDisconnected = true; this.isDisconnected = true;
// } else if (location.origin === 'http://smogtours.psim.us') { // } else if (location.origin === 'http://smogtours.psim.us') {
@ -427,7 +428,7 @@ function toId() {
Storage.whenPrefsLoaded(function () { Storage.whenPrefsLoaded(function () {
if (!Config.server.registered) { if (!Config.server.registered) {
app.send('/autojoin'); app.send('/autojoin');
Backbone.history.start({pushState: !Config.testclient}); Backbone.history.start({ pushState: !Config.testclient });
return; return;
} }
// Support legacy tournament setting and migrate to new pref // Support legacy tournament setting and migrate to new pref
@ -459,7 +460,7 @@ function toId() {
if (Object.keys(settings).length) app.user.set('settings', settings); if (Object.keys(settings).length) app.user.set('settings', settings);
// HTML5 history throws exceptions when running on file:// // HTML5 history throws exceptions when running on file://
var useHistory = !Config.testclient && (location.pathname.slice(-5) !== '.html'); var useHistory = !Config.testclient && (location.pathname.slice(-5) !== '.html');
Backbone.history.start({pushState: useHistory}); Backbone.history.start({ pushState: useHistory });
app.ignore = app.loadIgnore(); app.ignore = app.loadIgnore();
}); });
} }
@ -539,21 +540,21 @@ function toId() {
$('.battle-log-add').html('<small>You are disconnected and cannot chat.</small>'); $('.battle-log-add').html('<small>You are disconnected and cannot chat.</small>');
self.reconnectPending = (message || true); self.reconnectPending = (message || true);
if (!self.popups.length) self.addPopup(ReconnectPopup, {message: message}); if (!self.popups.length) self.addPopup(ReconnectPopup, { message: message });
}); });
this.on('init:connectionerror', function () { this.on('init:connectionerror', function () {
self.isDisconnected = true; self.isDisconnected = true;
self.rooms[''].updateFormats(); self.rooms[''].updateFormats();
self.addPopup(ReconnectPopup, {cantconnect: true}); self.addPopup(ReconnectPopup, { cantconnect: true });
}); });
this.user.on('login:invalidname', function (name, reason) { this.user.on('login:invalidname', function (name, reason) {
self.addPopup(LoginPopup, {name: name, reason: reason}); self.addPopup(LoginPopup, { name: name, reason: reason });
}); });
this.user.on('login:authrequired', function (name, special) { this.user.on('login:authrequired', function (name, special) {
self.addPopup(LoginPasswordPopup, {username: name, special: special}); self.addPopup(LoginPasswordPopup, { username: name, special: special });
}); });
this.on('loggedin', function () { this.on('loggedin', function () {
@ -790,7 +791,7 @@ function toId() {
} }
return new SockJS( return new SockJS(
protocol + '://' + Config.server.host + ':' + Config.server.port + Config.sockjsprefix, protocol + '://' + Config.server.host + ':' + Config.server.port + Config.sockjsprefix,
[], {timeout: 5 * 60 * 1000} [], { timeout: 5 * 60 * 1000 }
); );
} catch (err) { } catch (err) {
// The most common case this happens is if an HTTPS connection fails, // The most common case this happens is if an HTTPS connection fails,
@ -942,7 +943,7 @@ function toId() {
this.loadingTeam = true; this.loadingTeam = true;
$.get(app.user.getActionPHP(), { $.get(app.user.getActionPHP(), {
act: 'getteam', act: 'getteam',
teamid: team.teamid, teamid: team.teamid
}, Storage.safeJSON(function (data) { }, Storage.safeJSON(function (data) {
app.loadingTeam = false; app.loadingTeam = false;
if (data.actionerror) { if (data.actionerror) {
@ -1041,7 +1042,7 @@ function toId() {
var replayid = roomid.slice(7); var replayid = roomid.slice(7);
if (Config.server.id !== 'showdown') replayid = Config.server.id + '-' + replayid; if (Config.server.id !== 'showdown') replayid = Config.server.id + '-' + replayid;
var replayLink = 'https://' + Config.routes.replays + '/' + replayid; var replayLink = 'https://' + Config.routes.replays + '/' + replayid;
$.ajax(replayLink + '.json', {dataType: 'json'}).done(function (replay) { $.ajax(replayLink + '.json', { dataType: 'json' }).done(function (replay) {
if (replay) { if (replay) {
var title = replay.players[0] + ' vs. ' + replay.players[1]; var title = replay.players[0] + ' vs. ' + replay.players[1];
app.receive('>battle-' + replayid + '\n|init|battle\n|title|' + title + '\n' + replay.log); app.receive('>battle-' + replayid + '\n|init|battle\n|title|' + title + '\n' + replay.log);
@ -1169,7 +1170,7 @@ function toId() {
break; break;
case 'nametaken': case 'nametaken':
app.addPopup(LoginPopup, {name: parts[1] || '', error: parts[2] || ''}); app.addPopup(LoginPopup, { name: parts[1] || '', error: parts[2] || '' });
break; break;
case 'queryresponse': case 'queryresponse':
@ -1258,7 +1259,7 @@ function toId() {
case 'chat': case 'chat':
if (parts[1] === '~') { if (parts[1] === '~') {
if (parts[2].substr(0, 6) === '/warn ') { if (parts[2].substr(0, 6) === '/warn ') {
app.addPopup(RulesPopup, {warning: parts[2].substr(6)}); app.addPopup(RulesPopup, { warning: parts[2].substr(6) });
break; break;
} }
} }
@ -1322,8 +1323,8 @@ function toId() {
var column = 0; var column = 0;
var columnChanged = false; var columnChanged = false;
window.NonBattleGames = {rps: 'Rock Paper Scissors'}; window.NonBattleGames = { rps: 'Rock Paper Scissors' };
for (var i = 3; i <= 9; i = i + 2) { for (var i = 3; i <= 9; i += 2) {
window.NonBattleGames['bestof' + i] = 'Best-of-' + i; window.NonBattleGames['bestof' + i] = 'Best-of-' + i;
} }
window.BattleFormats = {}; window.BattleFormats = {};
@ -1473,7 +1474,7 @@ function toId() {
if (silent) return; if (silent) return;
var sData = data.split(':'); var sData = data.split(':');
if (sData[0] === 'success') { if (sData[0] === 'success') {
app.addPopup(ReplayUploadedPopup, {id: sData[1] || id}); app.addPopup(ReplayUploadedPopup, { id: sData[1] || id });
} else if (data === 'hash mismatch') { } else if (data === 'hash mismatch') {
app.addPopupMessage("Someone else is already uploading a replay of this battle. Try again in five seconds."); app.addPopupMessage("Someone else is already uploading a replay of this battle. Try again in five seconds.");
} else if (data === 'not found') { } else if (data === 'not found') {
@ -1603,7 +1604,7 @@ function toId() {
*/ */
unjoinRoom: function (id, reason) { unjoinRoom: function (id, reason) {
this.removeRoom(id, true); this.removeRoom(id, true);
if (this.curRoom) this.navigate(this.curRoom.id, {replace: true}); if (this.curRoom) this.navigate(this.curRoom.id, { replace: true });
this.updateAutojoin(); this.updateAutojoin();
}, },
tryJoinRoom: function (id) { tryJoinRoom: function (id) {
@ -1724,7 +1725,6 @@ function toId() {
} }
room.focus(null, focusTextbox); room.focus(null, focusTextbox);
return;
}, },
focusRoomLeft: function (id) { focusRoomLeft: function (id) {
var room = this.rooms[id]; var room = this.rooms[id];
@ -1750,7 +1750,6 @@ function toId() {
if (this.curRoom.id === id) this.navigate(id); if (this.curRoom.id === id) this.navigate(id);
room.focus(null, true); room.focus(null, true);
return;
}, },
focusRoomRight: function (id) { focusRoomRight: function (id) {
var room = this.rooms[id]; var room = this.rooms[id];
@ -1774,7 +1773,6 @@ function toId() {
// if (this.curRoom.id === id) this.navigate(id); // if (this.curRoom.id === id) this.navigate(id);
room.focus(null, true); room.focus(null, true);
return;
}, },
/** /**
* This is the function for handling the two-panel layout * This is the function for handling the two-panel layout
@ -2068,7 +2066,7 @@ function toId() {
} else { } else {
if (Config.server.id !== 'showdown') { if (Config.server.id !== 'showdown') {
// Switch to the autojoin object to handle multiple servers // Switch to the autojoin object to handle multiple servers
curAutojoin = {showdown: curAutojoin}; curAutojoin = { showdown: curAutojoin };
if (!autojoins.length) return; if (!autojoins.length) return;
curAutojoin[Config.server.id] = autojoins.join(','); curAutojoin[Config.server.id] = autojoins.join(',');
} else { } else {
@ -2142,19 +2140,19 @@ function toId() {
addPopupMessage: function (message) { addPopupMessage: function (message) {
// shorthand for adding a popup message // shorthand for adding a popup message
// this is the equivalent of alert(message) // this is the equivalent of alert(message)
app.addPopup(Popup, {message: message}); app.addPopup(Popup, { message: message });
}, },
addPopupPrompt: function (message, buttonOrCallback, callback) { addPopupPrompt: function (message, buttonOrCallback, callback) {
var button = (callback ? buttonOrCallback : 'OK'); var button = (callback ? buttonOrCallback : 'OK');
callback = (!callback ? buttonOrCallback : callback); callback = (!callback ? buttonOrCallback : callback);
app.addPopup(PromptPopup, {message: message, button: button, callback: callback}); app.addPopup(PromptPopup, { message: message, button: button, callback: callback });
}, },
closePopup: function (id) { closePopup: function (id) {
if (this.popups.length) { if (this.popups.length) {
var popup = this.popups.pop(); var popup = this.popups.pop();
if (popup.lastFocusedEl && popup.lastFocusedEl.focus) popup.lastFocusedEl.focus(); if (popup.lastFocusedEl && popup.lastFocusedEl.focus) popup.lastFocusedEl.focus();
popup.remove(); popup.remove();
if (this.reconnectPending) this.addPopup(ReconnectPopup, {message: this.reconnectPending}); if (this.reconnectPending) this.addPopup(ReconnectPopup, { message: this.reconnectPending });
return true; return true;
} }
return false; return false;
@ -2229,9 +2227,9 @@ function toId() {
*/ */
selectformat: function (value, target) { selectformat: function (value, target) {
var format = value || 'gen9randombattle'; var format = value || 'gen9randombattle';
app.addPopup(FormatPopup, {format: format, sourceEl: target, selectType: 'watch', onselect: function (newFormat) { app.addPopup(FormatPopup, { format: format, sourceEl: target, selectType: 'watch', onselect: function (newFormat) {
target.value = newFormat; target.value = newFormat;
}}); } });
}, },
copyText: function (value, target) { copyText: function (value, target) {
@ -2256,14 +2254,14 @@ function toId() {
this.leftWidth = 0; this.leftWidth = 0;
switch (position) { switch (position) {
case 'left': case 'left':
this.$el.css({left: 0, width: leftWidth, right: 'auto'}); this.$el.css({ left: 0, width: leftWidth, right: 'auto' });
break; break;
case 'right': case 'right':
this.$el.css({left: leftWidth + 1, width: 'auto', right: 0}); this.$el.css({ left: leftWidth + 1, width: 'auto', right: 0 });
this.leftWidth = leftWidth; this.leftWidth = leftWidth;
break; break;
case 'full': case 'full':
this.$el.css({left: 0, width: 'auto', right: 0}); this.$el.css({ left: 0, width: 'auto', right: 0 });
break; break;
} }
this.$el.show(); this.$el.show();
@ -2298,14 +2296,14 @@ function toId() {
notifications: null, notifications: null,
subtleNotification: false, subtleNotification: false,
notify: function (title, body, tag, once) { notify: function (title, body, tag, once) {
if (once && app.focused && (this === app.curRoom || this == app.curSideRoom)) return; if (once && app.focused && (this === app.curRoom || this === app.curSideRoom)) return;
if (!tag) tag = 'message'; if (!tag) tag = 'message';
var needsTabbarUpdate = false; var needsTabbarUpdate = false;
if (!this.notifications) { if (!this.notifications) {
this.notifications = {}; this.notifications = {};
needsTabbarUpdate = true; needsTabbarUpdate = true;
} }
if (app.focused && (this === app.curRoom || this == app.curSideRoom)) { if (app.focused && (this === app.curRoom || this === app.curSideRoom)) {
this.notifications[tag] = {}; this.notifications[tag] = {};
} else if (window.nodewebkit && !nwWindow.setBadgeLabel) { } else if (window.nodewebkit && !nwWindow.setBadgeLabel) {
// old desktop client // old desktop client
@ -2330,9 +2328,9 @@ function toId() {
}; };
if (Dex.prefs('temporarynotifications')) { if (Dex.prefs('temporarynotifications')) {
if (notification.cancel) { if (notification.cancel) {
setTimeout(function () {notification.cancel();}, 5000); setTimeout(function () { notification.cancel(); }, 5000);
} else if (notification.close) { } else if (notification.close) {
setTimeout(function () {notification.close();}, 5000); setTimeout(function () { notification.close(); }, 5000);
} }
} }
if (once) notification.psAutoclose = true; if (once) notification.psAutoclose = true;
@ -2359,7 +2357,7 @@ function toId() {
} }
}, },
subtleNotifyOnce: function () { subtleNotifyOnce: function () {
if (app.focused && (this === app.curRoom || this == app.curSideRoom)) return; if (app.focused && (this === app.curRoom || this === app.curSideRoom)) return;
if (this.notifications || this.subtleNotification) return; if (this.notifications || this.subtleNotification) return;
this.subtleNotification = true; this.subtleNotification = true;
this.notificationClass = ' subtle-notifying'; this.notificationClass = ' subtle-notifying';
@ -2629,7 +2627,7 @@ function toId() {
} else { } else {
app.addPopupMessage("You are already registered!"); app.addPopupMessage("You are already registered!");
} }
}, }
}); });
var PromptPopup = this.PromptPopup = Popup.extend({ var PromptPopup = this.PromptPopup = Popup.extend({
@ -2872,7 +2870,7 @@ function toId() {
app.addPopup(UserOptionsPopup, { app.addPopup(UserOptionsPopup, {
name: this.data.name, name: this.data.name,
userid: this.data.userid, userid: this.data.userid,
friended: this.data.friended, friended: this.data.friended
}); });
} }
}, { }, {

View File

@ -49,8 +49,8 @@ if (!Array.prototype.filter) {
} }
// ES2016, predates nomodule // ES2016, predates nomodule
if (!Array.prototype.includes) { if (!Array.prototype.includes) {
Array.prototype.includes = function includes(thing) { Array.prototype.includes = function includes(thing, offset) {
return this.indexOf(thing) !== -1; return this.indexOf(thing, offset) !== -1;
}; };
} }
// ES5 // ES5
@ -61,8 +61,8 @@ if (!Array.isArray) {
} }
// ES6 // ES6
if (!String.prototype.includes) { if (!String.prototype.includes) {
String.prototype.includes = function includes(thing) { String.prototype.includes = function includes(thing, offset) {
return this.indexOf(thing) !== -1; return this.indexOf(thing, offset) !== -1;
}; };
} }
// ES6 // ES6

View File

@ -79,7 +79,7 @@ var Replays = {
log: log.split('\n'), log: log.split('\n'),
isReplay: true, isReplay: true,
paused: true, paused: true,
autoresize: true, autoresize: true
}); });
this.$('.replay-controls-2').html('<div class="chooser leftchooser speedchooser"> <em>Speed:</em> <div><button value="hyperfast">Hyperfast</button><button value="fast">Fast</button><button value="normal" class="sel">Normal</button><button value="slow">Slow</button><button value="reallyslow">Really Slow</button></div> </div> <div class="chooser colorchooser"> <em>Color&nbsp;scheme:</em> <div><button class="sel" value="light">Light</button><button value="dark">Dark</button></div> </div> <div class="chooser soundchooser" style="display:none"> <em>Music:</em> <div><button class="sel" value="on">On</button><button value="off">Off</button></div> </div>'); this.$('.replay-controls-2').html('<div class="chooser leftchooser speedchooser"> <em>Speed:</em> <div><button value="hyperfast">Hyperfast</button><button value="fast">Fast</button><button value="normal" class="sel">Normal</button><button value="slow">Slow</button><button value="reallyslow">Really Slow</button></div> </div> <div class="chooser colorchooser"> <em>Color&nbsp;scheme:</em> <div><button class="sel" value="light">Light</button><button value="dark">Dark</button></div> </div> <div class="chooser soundchooser" style="display:none"> <em>Music:</em> <div><button class="sel" value="on">On</button><button value="off">Off</button></div> </div>');
@ -202,7 +202,7 @@ var Replays = {
}, },
switchViewpoint: function () { switchViewpoint: function () {
this.battle.switchViewpoint(); this.battle.switchViewpoint();
}, }
}; };
window.onload = function () { window.onload = function () {

View File

@ -209,7 +209,7 @@
var ability = this.engine.dex.abilities.get(id); var ability = this.engine.dex.abilities.get(id);
return this.renderAbilityRow(ability, matchStart, matchLength, errorMessage, attrs); return this.renderAbilityRow(ability, matchStart, matchLength, errorMessage, attrs);
case 'type': case 'type':
var type = {name: id[0].toUpperCase() + id.substr(1)}; var type = { name: id[0].toUpperCase() + id.substr(1) };
return this.renderTypeRow(type, matchStart, matchLength, errorMessage); return this.renderTypeRow(type, matchStart, matchLength, errorMessage);
case 'egggroup': case 'egggroup':
// very hardcode // very hardcode
@ -223,7 +223,7 @@
} else { } else {
egName = id[0].toUpperCase() + id.substr(1); egName = id[0].toUpperCase() + id.substr(1);
} }
var egggroup = {name: egName}; var egggroup = { name: egName };
return this.renderEggGroupRow(egggroup, matchStart, matchLength, errorMessage); return this.renderEggGroupRow(egggroup, matchStart, matchLength, errorMessage);
case 'tier': case 'tier':
// very hardcode // very hardcode
@ -246,14 +246,14 @@
publ: "PUBL", publ: "PUBL",
zubl: "ZUBL" zubl: "ZUBL"
}; };
var tier = {name: tierTable[id]}; var tier = { name: tierTable[id] };
return this.renderTierRow(tier, matchStart, matchLength, errorMessage); return this.renderTierRow(tier, matchStart, matchLength, errorMessage);
case 'category': case 'category':
var category = {name: id[0].toUpperCase() + id.substr(1), id: id}; var category = { name: id[0].toUpperCase() + id.substr(1), id: id };
return this.renderCategoryRow(category, matchStart, matchLength, errorMessage); return this.renderCategoryRow(category, matchStart, matchLength, errorMessage);
case 'article': case 'article':
var articleTitle = (window.BattleArticleTitles && BattleArticleTitles[id]) || (id[0].toUpperCase() + id.substr(1)); var articleTitle = (window.BattleArticleTitles && BattleArticleTitles[id]) || (id[0].toUpperCase() + id.substr(1));
var article = {name: articleTitle, id: id}; var article = { name: articleTitle, id: id };
return this.renderArticleRow(article, matchStart, matchLength, errorMessage); return this.renderArticleRow(article, matchStart, matchLength, errorMessage);
} }
return 'Error: not found'; return 'Error: not found';

View File

@ -15,7 +15,7 @@ Storage.initialize = function () {
Storage.safeJSON = function (callback) { Storage.safeJSON = function (callback) {
return function (data) { return function (data) {
if (data.length < 1) return; if (data.length < 1) return;
if (data[0] == ']') data = data.substr(1); if (data[0] === ']') data = data.substr(1);
return callback(JSON.parse(data)); return callback(JSON.parse(data));
}; };
}; };
@ -183,7 +183,7 @@ Storage.bg = {
var l = (max + min) / 2; var l = (max + min) / 2;
if (max === min) { if (max === min) {
return '0, 0%'; return '0, 0%';
} else { }
var d = max - min; var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { switch (max) {
@ -192,7 +192,6 @@ Storage.bg = {
case b: h = (r - g) / d + 4; break; case b: h = (r - g) / d + 4; break;
} }
h /= 6; h /= 6;
}
return '' + (h * 360) + ',' + (s * 100) + '%'; return '' + (h * 360) + ',' + (s * 100) + '%';
} }
}; };
@ -304,7 +303,7 @@ var updatePrefs = function () {
var oldShowjoins = Storage.prefs('showjoins'); var oldShowjoins = Storage.prefs('showjoins');
if (oldShowjoins !== undefined && typeof oldShowjoins !== 'object') { if (oldShowjoins !== undefined && typeof oldShowjoins !== 'object') {
var showjoins = {}; var showjoins = {};
var serverShowjoins = {global: (oldShowjoins ? 1 : 0)}; var serverShowjoins = { global: (oldShowjoins ? 1 : 0) };
var showroomjoins = Storage.prefs('showroomjoins'); var showroomjoins = Storage.prefs('showroomjoins');
for (var roomid in showroomjoins) { for (var roomid in showroomjoins) {
serverShowjoins[roomid] = (showroomjoins[roomid] ? 1 : 0); serverShowjoins[roomid] = (showroomjoins[roomid] ? 1 : 0);
@ -538,7 +537,7 @@ Storage.initTestClient = function () {
data.sid = sid; data.sid = sid;
get(uri, data, callback, type); get(uri, data, callback, type);
} else { } else {
app.addPopup(ProxyPopup, {uri: uri, callback: callback}); app.addPopup(ProxyPopup, { uri: uri, callback: callback });
} }
}; };
var post = $.post; var post = $.post;
@ -546,7 +545,7 @@ Storage.initTestClient = function () {
if (type === 'html') { if (type === 'html') {
uri += '&testclient'; uri += '&testclient';
} }
if (uri[0] === '/') { //relative URI if (uri[0] === '/') { // relative URI
uri = Dex.resourcePrefix + uri.substr(1); uri = Dex.resourcePrefix + uri.substr(1);
} }
@ -560,7 +559,7 @@ Storage.initTestClient = function () {
src += '<input type=hidden name="' + i + '" value="' + BattleLog.escapeHTML(data[i]) + '">'; src += '<input type=hidden name="' + i + '" value="' + BattleLog.escapeHTML(data[i]) + '">';
} }
src += '<input type=submit value="Please click this button first."></form></body></html>'; src += '<input type=submit value="Please click this button first."></form></body></html>';
app.addPopup(ProxyPopup, {uri: "data:text/html;charset=UTF-8," + encodeURIComponent(src), callback: callback}); app.addPopup(ProxyPopup, { uri: "data:text/html;charset=UTF-8," + encodeURIComponent(src), callback: callback });
} }
}; };
Storage.whenPrefsLoaded.load(); Storage.whenPrefsLoaded.load();
@ -622,7 +621,7 @@ Storage.compareTeams = function (serverTeam, localTeam) {
}; };
Storage.loadRemoteTeams = function (after) { Storage.loadRemoteTeams = function (after) {
$.get(app.user.getActionPHP(), {act: 'getteams'}, Storage.safeJSON(function (data) { $.get(app.user.getActionPHP(), { act: 'getteams' }, Storage.safeJSON(function (data) {
if (data.actionerror) { if (data.actionerror) {
return app.addPopupMessage('Error loading uploaded teams: ' + data.actionerror); return app.addPopupMessage('Error loading uploaded teams: ' + data.actionerror);
} }
@ -652,7 +651,7 @@ Storage.loadRemoteTeams = function (after) {
// team comes down from loginserver as comma-separated list of mons // team comes down from loginserver as comma-separated list of mons
// to save bandwidth // to save bandwidth
var mons = team.team.split(',').map(function (mon) { var mons = team.team.split(',').map(function (mon) {
return {species: mon}; return { species: mon };
}); });
team.team = Storage.packTeam(mons); team.team = Storage.packTeam(mons);
Storage.teams.unshift(team); Storage.teams.unshift(team);
@ -854,7 +853,7 @@ Storage.packTeam = function (team) {
} }
// level // level
if (set.level && set.level != 100) { if (set.level && set.level !== 100) {
buf += '|' + set.level; buf += '|' + set.level;
} else { } else {
buf += '|'; buf += '|';
@ -938,7 +937,7 @@ Storage.fastUnpackTeam = function (buf) {
spe: Number(evs[5]) || 0 spe: Number(evs[5]) || 0
}; };
} else if (evstring === '0') { } else if (evstring === '0') {
set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
} }
} }
i = j + 1; i = j + 1;
@ -1025,7 +1024,7 @@ Storage.unpackTeam = function (buf) {
j = buf.indexOf('|', i); j = buf.indexOf('|', i);
var ability = Dex.abilities.get(buf.substring(i, j)).name; var ability = Dex.abilities.get(buf.substring(i, j)).name;
var species = Dex.species.get(set.species); var species = Dex.species.get(set.species);
set.ability = (species.abilities && ability in {'':1, 0:1, 1:1, H:1} ? species.abilities[ability || '0'] : ability); set.ability = (species.abilities && ability in { '': 1, 0: 1, 1: 1, H: 1 } ? species.abilities[ability || '0'] : ability);
i = j + 1; i = j + 1;
// moves // moves
@ -1056,7 +1055,7 @@ Storage.unpackTeam = function (buf) {
spe: Number(evs[5]) || 0 spe: Number(evs[5]) || 0
}; };
} else if (evstring === '0') { } else if (evstring === '0') {
set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
} }
} }
i = j + 1; i = j + 1;
@ -1239,7 +1238,7 @@ Storage.importTeam = function (buffer, teams) {
curSet = null; curSet = null;
teams.push(Storage.unpackLine(line)); teams.push(Storage.unpackLine(line));
} else if (!curSet) { } else if (!curSet) {
curSet = {name: '', species: '', gender: ''}; curSet = { name: '', species: '', gender: '' };
team.push(curSet); team.push(curSet);
var atIndex = line.lastIndexOf(' @ '); var atIndex = line.lastIndexOf(' @ ');
if (atIndex !== -1) { if (atIndex !== -1) {
@ -1296,7 +1295,7 @@ Storage.importTeam = function (buffer, teams) {
} else if (line.substr(0, 5) === 'EVs: ') { } else if (line.substr(0, 5) === 'EVs: ') {
line = line.substr(5); line = line.substr(5);
var evLines = line.split('/'); var evLines = line.split('/');
curSet.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; curSet.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
for (var j = 0; j < evLines.length; j++) { for (var j = 0; j < evLines.length; j++) {
var evLine = $.trim(evLines[j]); var evLine = $.trim(evLines[j]);
var spaceIndex = evLine.indexOf(' '); var spaceIndex = evLine.indexOf(' ');
@ -1309,7 +1308,7 @@ Storage.importTeam = function (buffer, teams) {
} else if (line.substr(0, 5) === 'IVs: ') { } else if (line.substr(0, 5) === 'IVs: ') {
line = line.substr(5); line = line.substr(5);
var ivLines = line.split(' / '); var ivLines = line.split(' / ');
curSet.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; curSet.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
for (var j = 0; j < ivLines.length; j++) { for (var j = 0; j < ivLines.length; j++) {
var ivLine = ivLines[j]; var ivLine = ivLines[j];
var spaceIndex = ivLine.indexOf(' '); var spaceIndex = ivLine.indexOf(' ');
@ -1397,7 +1396,7 @@ Storage.exportTeam = function (team, gen, hidestats) {
if (curSet.ability) { if (curSet.ability) {
text += 'Ability: ' + curSet.ability + " \n"; text += 'Ability: ' + curSet.ability + " \n";
} }
if (curSet.level && curSet.level != 100) { if (curSet.level && curSet.level !== 100) {
text += 'Level: ' + curSet.level + " \n"; text += 'Level: ' + curSet.level + " \n";
} }
if (curSet.shiny) { if (curSet.shiny) {
@ -1472,7 +1471,7 @@ Storage.exportTeam = function (team, gen, hidestats) {
} }
if (!defaultIvs) { if (!defaultIvs) {
for (var stat in BattleStatNames) { for (var stat in BattleStatNames) {
if (typeof curSet.ivs[stat] === 'undefined' || isNaN(curSet.ivs[stat]) || curSet.ivs[stat] == 31) continue; if (typeof curSet.ivs[stat] === 'undefined' || isNaN(curSet.ivs[stat]) || curSet.ivs[stat] === 31) continue;
if (first) { if (first) {
text += 'IVs: '; text += 'IVs: ';
first = false; first = false;
@ -1509,7 +1508,7 @@ Storage.initDirectory = function () {
var self = this; var self = this;
var dir = process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH; var dir = process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH;
if (!(dir.charAt(dir.length - 1) in {'/': 1, '\\': 1})) dir += '/'; if (!(dir.charAt(dir.length - 1) in { '/': 1, '\\': 1 })) dir += '/';
fs.stat(dir + 'Documents', function (err, stats) { fs.stat(dir + 'Documents', function (err, stats) {
if (err || !stats.isDirectory()) { if (err || !stats.isDirectory()) {
fs.stat(dir + 'My Documents', function (err, stats) { fs.stat(dir + 'My Documents', function (err, stats) {
@ -1860,7 +1859,7 @@ Storage.nwLogChat = function (roomid, line) {
var timestamp = '[' + hours + ':' + minutes + '] '; var timestamp = '[' + hours + ':' + minutes + '] ';
if (!this.chatLogStreams[roomid]) { if (!this.chatLogStreams[roomid]) {
this.chatLogStreams[roomid] = fs.createWriteStream(this.dir + 'Logs/' + chatLogFdMonth + '/' + roomid + '.txt', {flags: 'a'}); this.chatLogStreams[roomid] = fs.createWriteStream(this.dir + 'Logs/' + chatLogFdMonth + '/' + roomid + '.txt', { flags: 'a' });
this.chatLogStreams[roomid].write('\n\n\nLog starting ' + now + '\n\n'); this.chatLogStreams[roomid].write('\n\n\nLog starting ' + now + '\n\n');
} }
this.chatLogStreams[roomid].write(timestamp + line + '\n'); this.chatLogStreams[roomid].write(timestamp + line + '\n');

View File

@ -51,9 +51,9 @@
linkStyle("style/battle-search.css"); linkStyle("style/battle-search.css");
linkStyle("/style/font-awesome.css"); linkStyle("/style/font-awesome.css");
</script> </script>
<script nomodule defer src="/js/lib/ps-polyfill.js"></script>
<script defer src="/config/config.js?"></script> <script defer src="/config/config.js?"></script>
<script defer src="/js/client-core.js?"></script> <script defer src="/js/client-core.js?"></script>
<script nomodule defer src="/js/lib/ps-polyfill.js"></script>
<script defer src="/js/battle-dex.js?"></script> <script defer src="/js/battle-dex.js?"></script>
<script defer src="/js/battle-text-parser.js?"></script> <script defer src="/js/battle-text-parser.js?"></script>

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,15 @@
* @license MIT * @license MIT
*/ */
import type {Battle, Pokemon, Side, WeatherState} from './battle'; import type { Battle, Pokemon, Side, WeatherState } from './battle';
import type {BattleSceneStub} from './battle-scene-stub'; import type { BattleSceneStub } from './battle-scene-stub';
import {BattleMoveAnims} from './battle-animations-moves'; import { BattleMoveAnims } from './battle-animations-moves';
import {BattleLog} from './battle-log'; import { BattleLog } from './battle-log';
import {BattleBGM, BattleSound} from './battle-sound'; import { type BattleBGM, BattleSound } from './battle-sound';
import {Dex, toID, type ID, type SpriteData} from './battle-dex'; import { Dex, toID, type ID, type SpriteData } from './battle-dex';
import {BattleNatures} from './battle-dex-data'; import { BattleNatures } from './battle-dex-data';
import {BattleTooltips} from './battle-tooltips'; import { BattleTooltips } from './battle-tooltips';
import {BattleTextParser, type Args, type KWArgs} from './battle-text-parser'; import { BattleTextParser, type Args, type KWArgs } from './battle-text-parser';
/* /*
@ -74,14 +74,14 @@ export class BattleScene implements BattleSceneStub {
$tooltips: JQuery = null!; $tooltips: JQuery = null!;
tooltips: BattleTooltips; tooltips: BattleTooltips;
sideConditions: [{[id: string]: Sprite[]}, {[id: string]: Sprite[]}] = [{}, {}]; sideConditions: [{ [id: string]: Sprite[] }, { [id: string]: Sprite[] }] = [{}, {}];
preloadDone = 0; preloadDone = 0;
preloadNeeded = 0; preloadNeeded = 0;
bgm: BattleBGM | null = null; bgm: BattleBGM | null = null;
backdropImage: string = ''; backdropImage = '';
bgmNum = 0; bgmNum = 0;
preloadCache: {[url: string]: HTMLImageElement} = {}; preloadCache: { [url: string]: HTMLImageElement } = {};
messagebarOpen = false; messagebarOpen = false;
customControls = false; customControls = false;
@ -271,7 +271,7 @@ export class BattleScene implements BattleSceneStub {
effect: string | SpriteData, start: ScenePos, end: ScenePos, effect: string | SpriteData, start: ScenePos, end: ScenePos,
transition: string, after?: string, additionalCss?: JQuery.PlainObject transition: string, after?: string, additionalCss?: JQuery.PlainObject
) { ) {
if (typeof effect === 'string') effect = BattleEffects[effect] as SpriteData; if (typeof effect === 'string') effect = BattleEffects[effect];
if (!start.time) start.time = 0; if (!start.time) start.time = 0;
if (!end.time) end.time = start.time + 500; if (!end.time) end.time = start.time + 500;
start.time += this.timeOffset; start.time += this.timeOffset;
@ -279,18 +279,18 @@ export class BattleScene implements BattleSceneStub {
if (!end.scale && end.scale !== 0 && start.scale) end.scale = start.scale; if (!end.scale && end.scale !== 0 && start.scale) end.scale = start.scale;
if (!end.xscale && end.xscale !== 0 && start.xscale) end.xscale = start.xscale; if (!end.xscale && end.xscale !== 0 && start.xscale) end.xscale = start.xscale;
if (!end.yscale && end.yscale !== 0 && start.yscale) end.yscale = start.yscale; if (!end.yscale && end.yscale !== 0 && start.yscale) end.yscale = start.yscale;
end = {...start, ...end}; end = { ...start, ...end };
let startpos = this.pos(start, effect); let startpos = this.pos(start, effect);
let endpos = this.posT(end, effect, transition, start); let endpos = this.posT(end, effect, transition, start);
let $effect = $('<img src="' + effect.url + '" style="display:block;position:absolute" />'); let $effect = $(`<img src="${effect.url!}" style="display:block;position:absolute" />`);
this.$fx.append($effect); this.$fx.append($effect);
if (additionalCss) $effect.css(additionalCss); if (additionalCss) $effect.css(additionalCss);
$effect = this.$fx.children().last(); $effect = this.$fx.children().last();
if (start.time) { if (start.time) {
$effect.css({...startpos, opacity: 0}); $effect.css({ ...startpos, opacity: 0 });
$effect.delay(start.time).animate({ $effect.delay(start.time).animate({
opacity: startpos.opacity, opacity: startpos.opacity,
}, 1); }, 1);
@ -349,10 +349,10 @@ export class BattleScene implements BattleSceneStub {
let left = 210; let left = 210;
let top = 245; let top = 245;
let scale = (obj.gen === 5 let scale = (obj.gen === 5 ?
? 2.0 - ((loc.z!) / 200) 2.0 - ((loc.z!) / 200) :
: 1.5 - 0.5 * ((loc.z!) / 200)); 1.5 - 0.5 * ((loc.z!) / 200));
if (scale < .1) scale = .1; if (scale < 0.1) scale = 0.1;
left += (410 - 190) * ((loc.z!) / 200); left += (410 - 190) * ((loc.z!) / 200);
top += (135 - 245) * ((loc.z!) / 200); top += (135 - 245) * ((loc.z!) / 200);
@ -476,7 +476,7 @@ export class BattleScene implements BattleSceneStub {
}, this.battle.messageFadeTime / this.acceleration); }, this.battle.messageFadeTime / this.acceleration);
} }
} }
if (this.battle.hardcoreMode && message.slice(0, 8) === '<small>(') { if (this.battle.hardcoreMode && message.startsWith('<small>(')) {
message = ''; message = '';
} }
if (message && this.animating) { if (message && this.animating) {
@ -580,15 +580,15 @@ export class BattleScene implements BattleSceneStub {
} else { } else {
if (gen <= 1) bg = 'fx/bg-gen1.png?'; if (gen <= 1) bg = 'fx/bg-gen1.png?';
else if (gen <= 2) bg = 'fx/bg-gen2.png?'; else if (gen <= 2) bg = 'fx/bg-gen2.png?';
else if (gen <= 3) bg = 'fx/' + BattleBackdropsThree[this.numericId % BattleBackdropsThree.length] + '?'; else if (gen <= 3) bg = `fx/${BattleBackdropsThree[this.numericId % BattleBackdropsThree.length]}?`;
else if (gen <= 4) bg = 'fx/' + BattleBackdropsFour[this.numericId % BattleBackdropsFour.length]; else if (gen <= 4) bg = `fx/${BattleBackdropsFour[this.numericId % BattleBackdropsFour.length]}`;
else if (gen <= 5) bg = 'fx/' + BattleBackdropsFive[this.numericId % BattleBackdropsFive.length]; else if (gen <= 5) bg = `fx/${BattleBackdropsFive[this.numericId % BattleBackdropsFive.length]}`;
else bg = 'sprites/gen6bgs/' + BattleBackdrops[this.numericId % BattleBackdrops.length]; else bg = `sprites/gen6bgs/${BattleBackdrops[this.numericId % BattleBackdrops.length]}`;
} }
this.backdropImage = bg; this.backdropImage = bg;
if (this.$bg) { if (this.$bg) {
this.$bg.css('background-image', 'url(' + Dex.resourcePrefix + '' + this.backdropImage + ')'); this.$bg.css('background-image', `url(${Dex.resourcePrefix}${this.backdropImage})`);
} }
} }
@ -665,29 +665,29 @@ export class BattleScene implements BattleSceneStub {
for (let i = 0; i < sidebarIcons.length; i++) { for (let i = 0; i < sidebarIcons.length; i++) {
const [iconType, pokeIndex] = sidebarIcons[i]; const [iconType, pokeIndex] = sidebarIcons[i];
const poke = pokeIndex !== null ? side.pokemon[pokeIndex] : null; const poke = pokeIndex !== null ? side.pokemon[pokeIndex] : null;
const tooltipCode = ` class="picon has-tooltip" data-tooltip="pokemon|${side.n}|${pokeIndex}${iconType === 'pokemon-illusion' ? '|illusion' : ''}"`; const tooltipCode = ` class="picon has-tooltip" data-tooltip="pokemon|${side.n}|${pokeIndex!}${iconType === 'pokemon-illusion' ? '|illusion' : ''}"`;
if (iconType === 'empty') { if (iconType === 'empty') {
pokemonhtml += `<span class="picon" style="` + Dex.getPokemonIcon('pokeball-none') + `"></span>`; pokemonhtml += `<span class="picon" style="${Dex.getPokemonIcon('pokeball-none')}"></span>`;
} else if (noShow) { } else if (noShow) {
if (poke?.fainted) { if (poke?.fainted) {
pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon('pokeball-fainted') + `" aria-label="Fainted"></span>`; pokemonhtml += `<span${tooltipCode} style="${Dex.getPokemonIcon('pokeball-fainted')}" aria-label="Fainted"></span>`;
} else if (poke?.status) { } else if (poke?.status) {
pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon('pokeball-statused') + `" aria-label="Statused"></span>`; pokemonhtml += `<span${tooltipCode} style="${Dex.getPokemonIcon('pokeball-statused')}" aria-label="Statused"></span>`;
} else { } else {
pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon('pokeball') + `" aria-label="Non-statused"></span>`; pokemonhtml += `<span${tooltipCode} style="${Dex.getPokemonIcon('pokeball')}" aria-label="Non-statused"></span>`;
} }
} else if (iconType === 'pseudo-zoroark') { } else if (iconType === 'pseudo-zoroark') {
pokemonhtml += `<span class="picon" style="` + Dex.getPokemonIcon('zoroark') + `" title="Unrevealed Illusion user" aria-label="Unrevealed Illusion user"></span>`; pokemonhtml += `<span class="picon" style="${Dex.getPokemonIcon('zoroark')}" title="Unrevealed Illusion user" aria-label="Unrevealed Illusion user"></span>`;
} else if (!poke) { } else if (!poke) {
pokemonhtml += `<span class="picon" style="` + Dex.getPokemonIcon('pokeball') + `" title="Not revealed" aria-label="Not revealed"></span>`; pokemonhtml += `<span class="picon" style="${Dex.getPokemonIcon('pokeball')}" title="Not revealed" aria-label="Not revealed"></span>`;
} else if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) { } else if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) {
// in VGC (bring 6 pick 4) and other pick-less-than-you-bring formats, this is // in VGC (bring 6 pick 4) and other pick-less-than-you-bring formats, this is
// a pokemon that's been brought but not necessarily picked // a pokemon that's been brought but not necessarily picked
const details = this.getDetailsText(poke); const details = this.getDetailsText(poke);
pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon(poke, !side.isFar) + `;opacity:0.6" aria-label="${details}"></span>`; pokemonhtml += `<span${tooltipCode} style="${Dex.getPokemonIcon(poke, !side.isFar)};opacity:0.6" aria-label="${details}"></span>`;
} else { } else {
const details = this.getDetailsText(poke); const details = this.getDetailsText(poke);
pokemonhtml += `<span${tooltipCode} style="` + Dex.getPokemonIcon(poke, !side.isFar) + `" aria-label="${details}"></span>`; pokemonhtml += `<span${tooltipCode} style="${Dex.getPokemonIcon(poke, !side.isFar)}" aria-label="${details}"></span>`;
} }
if (i % 3 === 2) pokemonhtml += `</div><div class="teamicons">`; if (i % 3 === 2) pokemonhtml += `</div><div class="teamicons">`;
} }
@ -737,7 +737,7 @@ export class BattleScene implements BattleSceneStub {
const side = this.battle.nearSide; const side = this.battle.nearSide;
if (side.ally) { if (side.ally) {
const side2 = side.ally!; const side2 = side.ally;
this.$leftbar.html(this.getSidebarHTML(side, 'near2') + this.getSidebarHTML(side2, 'near')); this.$leftbar.html(this.getSidebarHTML(side, 'near2') + this.getSidebarHTML(side2, 'near'));
} else if (this.battle.sides.length > 2) { // FFA } else if (this.battle.sides.length > 2) { // FFA
const side2 = this.battle.sides[side.n === 0 ? 3 : 2]; const side2 = this.battle.sides[side.n === 0 ? 3 : 2];
@ -750,7 +750,7 @@ export class BattleScene implements BattleSceneStub {
const side = this.battle.farSide; const side = this.battle.farSide;
if (side.ally) { if (side.ally) {
const side2 = side.ally!; const side2 = side.ally;
this.$rightbar.html(this.getSidebarHTML(side, 'far2') + this.getSidebarHTML(side2, 'far')); this.$rightbar.html(this.getSidebarHTML(side, 'far2') + this.getSidebarHTML(side2, 'far'));
} else if (this.battle.sides.length > 2) { // FFA } else if (this.battle.sides.length > 2) { // FFA
const side2 = this.battle.sides[side.n === 0 ? 3 : 2]; const side2 = this.battle.sides[side.n === 0 ? 3 : 2];
@ -802,17 +802,17 @@ export class BattleScene implements BattleSceneStub {
const tooltips = this.battle.gameType === 'freeforall' ? { const tooltips = this.battle.gameType === 'freeforall' ? {
// FFA battles are visually rendered as triple battle with the center slots empty // FFA battles are visually rendered as triple battle with the center slots empty
// so we swap the 2nd and 3rd tooltips on each side // so we swap the 2nd and 3rd tooltips on each side
p2b: {top: 70, left: 250, width: 80, height: 100, tooltip: 'activepokemon|1|1'}, p2b: { top: 70, left: 250, width: 80, height: 100, tooltip: 'activepokemon|1|1' },
p2a: {top: 90, left: 390, width: 100, height: 100, tooltip: 'activepokemon|1|0'}, p2a: { top: 90, left: 390, width: 100, height: 100, tooltip: 'activepokemon|1|0' },
p1a: {top: 200, left: 130, width: 120, height: 160, tooltip: 'activepokemon|0|0'}, p1a: { top: 200, left: 130, width: 120, height: 160, tooltip: 'activepokemon|0|0' },
p1b: {top: 200, left: 350, width: 150, height: 160, tooltip: 'activepokemon|0|1'}, p1b: { top: 200, left: 350, width: 150, height: 160, tooltip: 'activepokemon|0|1' },
} : { } : {
p2c: {top: 70, left: 250, width: 80, height: 100, tooltip: 'activepokemon|1|2'}, p2c: { top: 70, left: 250, width: 80, height: 100, tooltip: 'activepokemon|1|2' },
p2b: {top: 85, left: 320, width: 90, height: 100, tooltip: 'activepokemon|1|1'}, p2b: { top: 85, left: 320, width: 90, height: 100, tooltip: 'activepokemon|1|1' },
p2a: {top: 90, left: 390, width: 100, height: 100, tooltip: 'activepokemon|1|0'}, p2a: { top: 90, left: 390, width: 100, height: 100, tooltip: 'activepokemon|1|0' },
p1a: {top: 200, left: 130, width: 120, height: 160, tooltip: 'activepokemon|0|0'}, p1a: { top: 200, left: 130, width: 120, height: 160, tooltip: 'activepokemon|0|0' },
p1b: {top: 200, left: 250, width: 150, height: 160, tooltip: 'activepokemon|0|1'}, p1b: { top: 200, left: 250, width: 150, height: 160, tooltip: 'activepokemon|0|1' },
p1c: {top: 200, left: 350, width: 150, height: 160, tooltip: 'activepokemon|0|2'}, p1c: { top: 200, left: 350, width: 150, height: 160, tooltip: 'activepokemon|0|2' },
}; };
for (const id in tooltips) { for (const id in tooltips) {
let layout = tooltips[id as 'p1a']; let layout = tooltips[id as 'p1a'];
@ -861,26 +861,26 @@ export class BattleScene implements BattleSceneStub {
textBuf += pokemon.speciesForme; textBuf += pokemon.speciesForme;
let url = spriteData.url; let url = spriteData.url;
// if (this.paused) url.replace('/xyani', '/xy').replace('.gif', '.png'); // if (this.paused) url.replace('/xyani', '/xy').replace('.gif', '.png');
buf += '<img src="' + url + '" width="' + spriteData.w + '" height="' + spriteData.h + '" style="position:absolute;top:' + Math.floor(y - spriteData.h / 2) + 'px;left:' + Math.floor(x - spriteData.w / 2) + 'px" />'; buf += `<img src="${url}" width="${spriteData.w}" height="${spriteData.h}" style="position:absolute;top:${Math.floor(y - spriteData.h / 2)}px;left:${Math.floor(x - spriteData.w / 2)}px" />`;
buf2 += '<div style="position:absolute;top:' + (y + 45) + 'px;left:' + (x - 40) + 'px;width:80px;font-size:10px;text-align:center;color:#FFF;">'; buf2 += `<div style="position:absolute;top:${y + 45}px;left:${x - 40}px;width:80px;font-size:10px;text-align:center;color:#FFF;">`;
const gender = pokemon.gender; const gender = pokemon.gender;
if (gender === 'M' || gender === 'F') { if (gender === 'M' || gender === 'F') {
buf2 += `<img src="${Dex.fxPrefix}gender-${gender.toLowerCase()}.png" alt="${gender}" width="7" height="10" class="pixelated" style="margin-bottom:-1px" /> `; buf2 += `<img src="${Dex.fxPrefix}gender-${gender.toLowerCase()}.png" alt="${gender}" width="7" height="10" class="pixelated" style="margin-bottom:-1px" /> `;
} }
if (pokemon.level !== 100) { if (pokemon.level !== 100) {
buf2 += '<span style="text-shadow:#000 1px 1px 0,#000 1px -1px 0,#000 -1px 1px 0,#000 -1px -1px 0"><small>L</small>' + pokemon.level + '</span>'; buf2 += `<span style="text-shadow:#000 1px 1px 0,#000 1px -1px 0,#000 -1px 1px 0,#000 -1px -1px 0"><small>L</small>${pokemon.level}</span>`;
} }
if (pokemon.item === '(mail)') { if (pokemon.item === '(mail)') {
buf2 += ' <img src="' + Dex.resourcePrefix + 'fx/mail.png" width="8" height="10" alt="F" style="margin-bottom:-1px" />'; buf2 += ` <img src="${Dex.resourcePrefix}fx/mail.png" width="8" height="10" alt="F" style="margin-bottom:-1px" />`;
} else if (pokemon.item) { } else if (pokemon.item) {
buf2 += ' <img src="' + Dex.resourcePrefix + 'fx/item.png" width="8" height="10" alt="F" style="margin-bottom:-1px" />'; buf2 += ` <img src="${Dex.resourcePrefix}fx/item.png" width="8" height="10" alt="F" style="margin-bottom:-1px" />`;
} }
buf2 += '</div>'; buf2 += '</div>';
} }
side.totalPokemon = side.pokemon.length; side.totalPokemon = side.pokemon.length;
if (textBuf) { if (textBuf) {
this.log.addDiv('chat battle-history', this.log.addDiv('chat battle-history',
'<strong>' + BattleLog.escapeHTML(side.name) + '\'s team:</strong> <em style="color:#445566;display:block;">' + BattleLog.escapeHTML(textBuf) + '</em>' `<strong>${BattleLog.escapeHTML(side.name)}'s team:</strong> <em style="color:#445566;display:block;">${BattleLog.escapeHTML(textBuf)}</em>`
); );
} }
this.$sprites[spriteIndex].html(buf + buf2); this.$sprites[spriteIndex].html(buf + buf2);
@ -916,21 +916,21 @@ export class BattleScene implements BattleSceneStub {
} }
pseudoWeatherLeft(pWeather: WeatherState) { pseudoWeatherLeft(pWeather: WeatherState) {
let buf = '<br />' + Dex.moves.get(pWeather[0]).name; let buf = `<br />${Dex.moves.get(pWeather[0]).name}`;
if (!pWeather[1] && pWeather[2]) { if (!pWeather[1] && pWeather[2]) {
pWeather[1] = pWeather[2]; pWeather[1] = pWeather[2];
pWeather[2] = 0; pWeather[2] = 0;
} }
if (this.battle.gen < 7 && this.battle.hardcoreMode) return buf; if (this.battle.gen < 7 && this.battle.hardcoreMode) return buf;
if (pWeather[2]) { if (pWeather[2]) {
return buf + ' <small>(' + pWeather[1] + ' or ' + pWeather[2] + ' turns)</small>'; return `${buf} <small>(${pWeather[1]} or ${pWeather[2]} turns)</small>`;
} }
if (pWeather[1]) { if (pWeather[1]) {
return buf + ' <small>(' + pWeather[1] + ' turn' + (pWeather[1] === 1 ? '' : 's') + ')</small>'; return `${buf} <small>(${pWeather[1]} turn${pWeather[1] === 1 ? '' : 's'})</small>`;
} }
return buf; // weather not found return buf; // weather not found
} }
sideConditionLeft(cond: [string, number, number, number], isFoe: boolean, all?: boolean) { sideConditionLeft(cond: Side['sideConditions'][string], isFoe: boolean, all?: boolean) {
if (!cond[2] && !cond[3] && !all) return ''; if (!cond[2] && !cond[3] && !all) return '';
let buf = `<br />${isFoe && !all ? "Foe's " : ""}${Dex.moves.get(cond[0]).name}`; let buf = `<br />${isFoe && !all ? "Foe's " : ""}${Dex.moves.get(cond[0]).name}`;
if (this.battle.gen < 7 && this.battle.hardcoreMode) return buf; if (this.battle.gen < 7 && this.battle.hardcoreMode) return buf;
@ -941,9 +941,9 @@ export class BattleScene implements BattleSceneStub {
cond[3] = 0; cond[3] = 0;
} }
if (!cond[3]) { if (!cond[3]) {
return buf + ' <small>(' + cond[2] + ' turn' + (cond[2] === 1 ? '' : 's') + ')</small>'; return `${buf} <small>(${cond[2]} turn${cond[2] === 1 ? '' : 's'})</small>`;
} }
return buf + ' <small>(' + cond[2] + ' or ' + cond[3] + ' turns)</small>'; return `${buf} <small>(${cond[2]} or ${cond[3]} turns)</small>`;
} }
weatherLeft() { weatherLeft() {
if (this.battle.gen < 7 && this.battle.hardcoreMode) return ''; if (this.battle.gen < 7 && this.battle.hardcoreMode) return '';
@ -951,7 +951,7 @@ export class BattleScene implements BattleSceneStub {
let weatherhtml = ``; let weatherhtml = ``;
if (this.battle.weather) { if (this.battle.weather) {
const weatherNameTable: {[id: string]: string} = { const weatherNameTable: { [id: string]: string } = {
sunnyday: 'Sun', sunnyday: 'Sun',
desolateland: 'Intense Sun', desolateland: 'Intense Sun',
raindance: 'Rain', raindance: 'Rain',
@ -1030,7 +1030,7 @@ export class BattleScene implements BattleSceneStub {
}, this.curWeather ? 300 : 100, () => { }, this.curWeather ? 300 : 100, () => {
this.$weather.html('<em>' + weatherhtml + '</em>'); this.$weather.html('<em>' + weatherhtml + '</em>');
this.$weather.attr('class', weather ? 'weather ' + weather + 'weather' : 'weather'); this.$weather.attr('class', weather ? 'weather ' + weather + 'weather' : 'weather');
this.$weather.animate({opacity: isIntense || !weather ? 0.9 : 0.5}, 300); this.$weather.animate({ opacity: isIntense || !weather ? 0.9 : 0.5 }, 300);
}); });
this.curWeather = weather; this.curWeather = weather;
} else { } else {
@ -1043,7 +1043,7 @@ export class BattleScene implements BattleSceneStub {
opacity: 0, opacity: 0,
}, this.curTerrain ? 400 : 1, () => { }, this.curTerrain ? 400 : 1, () => {
this.$terrain.attr('class', terrain ? 'weather ' + terrain + 'weather' : 'weather'); this.$terrain.attr('class', terrain ? 'weather ' + terrain + 'weather' : 'weather');
this.$terrain.animate({top: 0, opacity: 1}, 400); this.$terrain.animate({ top: 0, opacity: 1 }, 400);
}); });
this.curTerrain = terrain; this.curTerrain = terrain;
} }
@ -1053,7 +1053,7 @@ export class BattleScene implements BattleSceneStub {
this.$turn.html(''); this.$turn.html('');
return; return;
} }
this.$turn.html('<div class="turn has-tooltip" data-tooltip="field" data-ownheight="1">Turn ' + this.battle.turn + '</div>'); this.$turn.html(`<div class="turn has-tooltip" data-tooltip="field" data-ownheight="1">Turn ${this.battle.turn}</div>`);
} }
incrementTurn() { incrementTurn() {
if (!this.animating) return; if (!this.animating) return;
@ -1061,7 +1061,7 @@ export class BattleScene implements BattleSceneStub {
const turn = this.battle.turn; const turn = this.battle.turn;
if (turn <= 0) return; if (turn <= 0) return;
const $prevTurn = this.$turn.children(); const $prevTurn = this.$turn.children();
const $newTurn = $('<div class="turn has-tooltip" data-tooltip="field" data-ownheight="1">Turn ' + turn + '</div>'); const $newTurn = $(`<div class="turn has-tooltip" data-tooltip="field" data-ownheight="1">Turn ${turn}</div>`);
$newTurn.css({ $newTurn.css({
opacity: 0, opacity: 0,
left: 160, left: 160,
@ -1071,7 +1071,7 @@ export class BattleScene implements BattleSceneStub {
opacity: 1, opacity: 1,
left: 110, left: 110,
}, 500).animate({ }, 500).animate({
opacity: .4, opacity: 0.4,
}, 1500); }, 1500);
$prevTurn.animate({ $prevTurn.animate({
opacity: 0, opacity: 0,
@ -1130,7 +1130,7 @@ export class BattleScene implements BattleSceneStub {
yscale: 0, yscale: 0,
opacity: 0.1, opacity: 0.1,
}, this); }, this);
this.$spritesFront[spriteIndex].append(auroraveil.$el!); this.$spritesFront[spriteIndex].append(auroraveil.$el);
this.sideConditions[siden][id] = [auroraveil]; this.sideConditions[siden][id] = [auroraveil];
auroraveil.anim({ auroraveil.anim({
opacity: 0.7, opacity: 0.7,
@ -1150,7 +1150,7 @@ export class BattleScene implements BattleSceneStub {
yscale: 0, yscale: 0,
opacity: 0.1, opacity: 0.1,
}, this); }, this);
this.$spritesFront[spriteIndex].append(reflect.$el!); this.$spritesFront[spriteIndex].append(reflect.$el);
this.sideConditions[siden][id] = [reflect]; this.sideConditions[siden][id] = [reflect];
reflect.anim({ reflect.anim({
opacity: 0.7, opacity: 0.7,
@ -1170,7 +1170,7 @@ export class BattleScene implements BattleSceneStub {
yscale: 0, yscale: 0,
opacity: 0.1, opacity: 0.1,
}, this); }, this);
this.$spritesFront[spriteIndex].append(safeguard.$el!); this.$spritesFront[spriteIndex].append(safeguard.$el);
this.sideConditions[siden][id] = [safeguard]; this.sideConditions[siden][id] = [safeguard];
safeguard.anim({ safeguard.anim({
opacity: 0.7, opacity: 0.7,
@ -1190,7 +1190,7 @@ export class BattleScene implements BattleSceneStub {
yscale: 0, yscale: 0,
opacity: 0.1, opacity: 0.1,
}, this); }, this);
this.$spritesFront[spriteIndex].append(lightscreen.$el!); this.$spritesFront[spriteIndex].append(lightscreen.$el);
this.sideConditions[siden][id] = [lightscreen]; this.sideConditions[siden][id] = [lightscreen];
lightscreen.anim({ lightscreen.anim({
opacity: 0.7, opacity: 0.7,
@ -1210,7 +1210,7 @@ export class BattleScene implements BattleSceneStub {
yscale: 0, yscale: 0,
opacity: 0.1, opacity: 0.1,
}, this); }, this);
this.$spritesFront[spriteIndex].append(mist.$el!); this.$spritesFront[spriteIndex].append(mist.$el);
this.sideConditions[siden][id] = [mist]; this.sideConditions[siden][id] = [mist];
mist.anim({ mist.anim({
opacity: 0.7, opacity: 0.7,
@ -1257,10 +1257,10 @@ export class BattleScene implements BattleSceneStub {
scale: 0.2, scale: 0.2,
}, this); }, this);
this.$spritesFront[spriteIndex].append(rock1.$el!); this.$spritesFront[spriteIndex].append(rock1.$el);
this.$spritesFront[spriteIndex].append(rock2.$el!); this.$spritesFront[spriteIndex].append(rock2.$el);
this.$spritesFront[spriteIndex].append(rock3.$el!); this.$spritesFront[spriteIndex].append(rock3.$el);
this.$spritesFront[spriteIndex].append(rock4.$el!); this.$spritesFront[spriteIndex].append(rock4.$el);
this.sideConditions[siden][id] = [rock1, rock2, rock3, rock4]; this.sideConditions[siden][id] = [rock1, rock2, rock3, rock4];
break; break;
case 'gmaxsteelsurge': case 'gmaxsteelsurge':
@ -1289,9 +1289,9 @@ export class BattleScene implements BattleSceneStub {
scale: 0.8, scale: 0.8,
}, this); }, this);
this.$spritesFront[spriteIndex].append(surge1.$el!); this.$spritesFront[spriteIndex].append(surge1.$el);
this.$spritesFront[spriteIndex].append(surge2.$el!); this.$spritesFront[spriteIndex].append(surge2.$el);
this.$spritesFront[spriteIndex].append(surge3.$el!); this.$spritesFront[spriteIndex].append(surge3.$el);
this.sideConditions[siden][id] = [surge1, surge2, surge3]; this.sideConditions[siden][id] = [surge1, surge2, surge3];
break; break;
case 'spikes': case 'spikes':
@ -1309,7 +1309,7 @@ export class BattleScene implements BattleSceneStub {
z: side.z, z: side.z,
scale: 0.3, scale: 0.3,
}, this); }, this);
this.$spritesFront[spriteIndex].append(spike1.$el!); this.$spritesFront[spriteIndex].append(spike1.$el);
spikeArray.push(spike1); spikeArray.push(spike1);
} }
if (spikeArray.length < 2 && levels >= 2) { if (spikeArray.length < 2 && levels >= 2) {
@ -1318,9 +1318,9 @@ export class BattleScene implements BattleSceneStub {
x: x + 30, x: x + 30,
y: y - 45, y: y - 45,
z: side.z, z: side.z,
scale: .3, scale: 0.3,
}, this); }, this);
this.$spritesFront[spriteIndex].append(spike2.$el!); this.$spritesFront[spriteIndex].append(spike2.$el);
spikeArray.push(spike2); spikeArray.push(spike2);
} }
if (spikeArray.length < 3 && levels >= 3) { if (spikeArray.length < 3 && levels >= 3) {
@ -1329,9 +1329,9 @@ export class BattleScene implements BattleSceneStub {
x: x + 50, x: x + 50,
y: y - 40, y: y - 40,
z: side.z, z: side.z,
scale: .3, scale: 0.3,
}, this); }, this);
this.$spritesFront[spriteIndex].append(spike3.$el!); this.$spritesFront[spriteIndex].append(spike3.$el);
spikeArray.push(spike3); spikeArray.push(spike3);
} }
break; break;
@ -1350,7 +1350,7 @@ export class BattleScene implements BattleSceneStub {
z: side.z, z: side.z,
scale: 0.3, scale: 0.3,
}, this); }, this);
this.$spritesFront[spriteIndex].append(tspike1.$el!); this.$spritesFront[spriteIndex].append(tspike1.$el);
tspikeArray.push(tspike1); tspikeArray.push(tspike1);
} }
if (tspikeArray.length < 2 && tspikeLevels >= 2) { if (tspikeArray.length < 2 && tspikeLevels >= 2) {
@ -1359,9 +1359,9 @@ export class BattleScene implements BattleSceneStub {
x: x - 15, x: x - 15,
y: y - 35, y: y - 35,
z: side.z, z: side.z,
scale: .3, scale: 0.3,
}, this); }, this);
this.$spritesFront[spriteIndex].append(tspike2.$el!); this.$spritesFront[spriteIndex].append(tspike2.$el);
tspikeArray.push(tspike2); tspikeArray.push(tspike2);
} }
break; break;
@ -1374,7 +1374,7 @@ export class BattleScene implements BattleSceneStub {
opacity: 0.4, opacity: 0.4,
scale: 0.7, scale: 0.7,
}, this); }, this);
this.$spritesFront[spriteIndex].append(web.$el!); this.$spritesFront[spriteIndex].append(web.$el);
this.sideConditions[siden][id] = [web]; this.sideConditions[siden][id] = [web];
break; break;
} }
@ -1399,7 +1399,7 @@ export class BattleScene implements BattleSceneStub {
typeAnim(pokemon: Pokemon, types: string) { typeAnim(pokemon: Pokemon, types: string) {
const result = BattleLog.escapeHTML(types).split('/').map(type => const result = BattleLog.escapeHTML(types).split('/').map(type =>
'<img src="' + Dex.resourcePrefix + 'sprites/types/' + encodeURIComponent(type) + '.png" alt="' + type + '" class="pixelated" />' `<img src="${Dex.resourcePrefix}sprites/types/${encodeURIComponent(type)}.png" alt="${type}" class="pixelated" />`
).join(' '); ).join(' ');
this.resultAnim(pokemon, result, 'neutral'); this.resultAnim(pokemon, result, 'neutral');
} }
@ -1425,7 +1425,7 @@ export class BattleScene implements BattleSceneStub {
} }
abilityActivateAnim(pokemon: Pokemon, result: string) { abilityActivateAnim(pokemon: Pokemon, result: string) {
if (!this.animating) return; if (!this.animating) return;
this.$fx.append('<div class="result abilityresult"><strong>' + result + '</strong></div>'); this.$fx.append(`<div class="result abilityresult"><strong>${result}</strong></div>`);
let $effect = this.$fx.children().last(); let $effect = this.$fx.children().last();
$effect.delay(this.timeOffset).css({ $effect.delay(this.timeOffset).css({
display: 'block', display: 'block',
@ -1459,7 +1459,7 @@ export class BattleScene implements BattleSceneStub {
} }
if (damage === '100%' && pokemon.hp > 0) damage = '99%'; if (damage === '100%' && pokemon.hp > 0) damage = '99%';
this.resultAnim(pokemon, this.battle.hardcoreMode ? 'Damage' : '&minus;' + damage, 'bad'); this.resultAnim(pokemon, this.battle.hardcoreMode ? 'Damage' : `&minus;${damage}`, 'bad');
$hp.animate({ $hp.animate({
width: w, width: w,
@ -1482,7 +1482,7 @@ export class BattleScene implements BattleSceneStub {
callback = () => { $hp.removeClass('hp-red'); }; callback = () => { $hp.removeClass('hp-red'); };
} }
this.resultAnim(pokemon, this.battle.hardcoreMode ? 'Heal' : '+' + damage, 'good'); this.resultAnim(pokemon, this.battle.hardcoreMode ? 'Heal' : `+${damage}`, 'good');
$hp.animate({ $hp.animate({
width: w, width: w,
@ -1685,7 +1685,7 @@ export class BattleScene implements BattleSceneStub {
} }
this.battle = null!; this.battle = null!;
} }
static getHPColor(pokemon: {hp: number, maxhp: number}) { static getHPColor(pokemon: { hp: number, maxhp: number }) {
let ratio = pokemon.hp / pokemon.maxhp; let ratio = pokemon.hp / pokemon.maxhp;
if (ratio > 0.5) return 'g'; if (ratio > 0.5) return 'g';
if (ratio > 0.2) return 'y'; if (ratio > 0.2) return 'y';
@ -1732,7 +1732,7 @@ export class Sprite {
if (spriteData) { if (spriteData) {
sp = spriteData; sp = spriteData;
let rawHTML = sp.rawHTML || let rawHTML = sp.rawHTML ||
'<img src="' + sp.url + '" style="display:none;position:absolute"' + (sp.pixelated ? ' class="pixelated"' : '') + ' />'; `<img src="${sp.url!}" style="display:none;position:absolute"${sp.pixelated ? ' class="pixelated"' : ''} />`;
this.$el = $(rawHTML); this.$el = $(rawHTML);
} else { } else {
sp = { sp = {
@ -1746,7 +1746,7 @@ export class Sprite {
this.x = pos.x; this.x = pos.x;
this.y = pos.y; this.y = pos.y;
this.z = pos.z; this.z = pos.z;
if (pos.opacity !== 0 && spriteData) this.$el!.css(scene.pos(pos, sp)); if (pos.opacity !== 0 && spriteData) this.$el.css(scene.pos(pos, sp));
if (!spriteData) { if (!spriteData) {
this.delay = function () { return this; }; this.delay = function () { return this; };
@ -1760,7 +1760,7 @@ export class Sprite {
this.scene = null!; this.scene = null!;
} }
delay(time: number) { delay(time: number) {
this.$el!.delay(time); this.$el.delay(time);
return this; return this;
} }
anim(end: ScenePos, transition?: string) { anim(end: ScenePos, transition?: string) {
@ -1774,17 +1774,17 @@ export class Sprite {
...end, ...end,
}; };
if (end.time === 0) { if (end.time === 0) {
this.$el!.css(this.scene.pos(end, this.sp)); this.$el.css(this.scene.pos(end, this.sp));
return this; return this;
} }
this.$el!.animate(this.scene.posT(end, this.sp, transition, this), end.time!); this.$el.animate(this.scene.posT(end, this.sp, transition, this), end.time!);
return this; return this;
} }
} }
export class PokemonSprite extends Sprite { export class PokemonSprite extends Sprite {
// HTML strings are constructed from this table and stored back in it to cache them // HTML strings are constructed from this table and stored back in it to cache them
protected static statusTable: {[id: string]: [string, 'good' | 'bad' | 'neutral'] | null | string} = { protected static statusTable: { [id: string]: [string, 'good' | 'bad' | 'neutral'] | null | string } = {
formechange: null, formechange: null,
typechange: null, typechange: null,
typeadd: null, typeadd: null,
@ -1921,7 +1921,7 @@ export class PokemonSprite extends Sprite {
left = 0; left = 0;
top = 0; top = 0;
effects: {[id: string]: Sprite[]} = {}; effects: { [id: string]: Sprite[] } = {};
constructor(spriteData: SpriteData | null, pos: InitScenePos, scene: BattleScene, isFrontSprite: boolean) { constructor(spriteData: SpriteData | null, pos: InitScenePos, scene: BattleScene, isFrontSprite: boolean) {
super(spriteData, pos, scene); super(spriteData, pos, scene);
@ -1985,7 +1985,7 @@ export class PokemonSprite extends Sprite {
x: this.x, x: this.x,
y: this.y, y: this.y,
z: (this.isSubActive ? this.behind(30) : this.z), z: (this.isSubActive ? this.behind(30) : this.z),
opacity: (this.$sub ? .3 : 1), opacity: (this.$sub ? 0.3 : 1),
}, sp)); }, sp));
} }
animSub(instant?: boolean, noAnim?: boolean) { animSub(instant?: boolean, noAnim?: boolean) {
@ -2043,14 +2043,14 @@ export class PokemonSprite extends Sprite {
}, this.subsp!), 500); }, this.subsp!), 500);
this.$sub = null; this.$sub = null;
this.anim({time: 500}); this.anim({ time: 500 });
if (this.scene.animating) this.scene.waitFor(this.$el); if (this.scene.animating) this.scene.waitFor(this.$el);
} }
beforeMove() { beforeMove() {
if (!this.scene.animating) return false; if (!this.scene.animating) return false;
if (!this.isSubActive) return false; if (!this.isSubActive) return false;
this.isSubActive = false; this.isSubActive = false;
this.anim({time: 300}); this.anim({ time: 300 });
this.$sub!.animate(this.scene.pos({ this.$sub!.animate(this.scene.pos({
x: this.leftof(-50), x: this.leftof(-50),
y: this.y, y: this.y,
@ -2082,7 +2082,7 @@ export class PokemonSprite extends Sprite {
z: this.behind(30), z: this.behind(30),
opacity: 0.3, opacity: 0.3,
}, this.sp), 300); }, this.sp), 300);
this.anim({time: 300}); this.anim({ time: 300 });
}); });
return false; return false;
} }
@ -2128,7 +2128,7 @@ export class PokemonSprite extends Sprite {
if (this.$el) { if (this.$el) {
this.$el.stop(true, false); this.$el.stop(true, false);
this.$el.remove(); this.$el.remove();
const $newEl = $('<img src="' + this.sp.url + '" style="display:none;position:absolute"' + (this.sp.pixelated ? ' class="pixelated"' : '') + ' />'); const $newEl = $(`<img src="${this.sp.url!}" style="display:none;position:absolute"${this.sp.pixelated ? ' class="pixelated"' : ''} />`);
this.$el = $newEl; this.$el = $newEl;
} }
@ -2164,7 +2164,7 @@ export class PokemonSprite extends Sprite {
x: this.x, x: this.x,
y: this.y, y: this.y,
z: this.behind(30), z: this.behind(30),
opacity: .3, opacity: 0.3,
}, this.sp)); }, this.sp));
this.$sub.css(this.scene.pos({ this.$sub.css(this.scene.pos({
x: this.x, x: this.x,
@ -2278,7 +2278,7 @@ export class PokemonSprite extends Sprite {
x: this.x, x: this.x,
y: this.y + 30, y: this.y + 30,
z: this.behind(50), z: this.behind(50),
scale: .7, scale: 0.7,
}, { }, {
opacity: 1, opacity: 1,
x: this.x, x: this.x,
@ -2409,7 +2409,7 @@ export class PokemonSprite extends Sprite {
left: this.statbarLeft - (this.isFrontSprite ? -100 : 100), left: this.statbarLeft - (this.isFrontSprite ? -100 : 100),
opacity: 0, opacity: 0,
}, 300 / this.scene.acceleration, () => { }, 300 / this.scene.acceleration, () => {
$statbar!.remove(); $statbar.remove();
}); });
} }
} }
@ -2447,7 +2447,7 @@ export class PokemonSprite extends Sprite {
x: this.x, x: this.x,
y: this.y - 40, y: this.y - 40,
z: this.z, z: this.z,
scale: .7, scale: 0.7,
time: 300 / this.scene.acceleration, time: 300 / this.scene.acceleration,
}, { }, {
opacity: 0, opacity: 0,
@ -2466,7 +2466,7 @@ export class PokemonSprite extends Sprite {
left: this.statbarLeft + (this.isFrontSprite ? 50 : -50), left: this.statbarLeft + (this.isFrontSprite ? 50 : -50),
opacity: 0, opacity: 0,
}, 300 / this.scene.acceleration, () => { }, 300 / this.scene.acceleration, () => {
$statbar!.remove(); $statbar.remove();
}); });
} }
} }
@ -2500,7 +2500,7 @@ export class PokemonSprite extends Sprite {
$statbar.animate({ $statbar.animate({
opacity: 0, opacity: 0,
}, 300, () => { }, 300, () => {
$statbar!.remove(); $statbar.remove();
}); });
} }
} }
@ -2617,7 +2617,7 @@ export class PokemonSprite extends Sprite {
opacity: 1, opacity: 1,
time: 100, time: 100,
}).anim({ }).anim({
opacity: .4, opacity: 0.4,
time: 300, time: 300,
}); });
} }
@ -2636,32 +2636,32 @@ export class PokemonSprite extends Sprite {
x: this.x - 30, x: this.x - 30,
y: this.y - 40, y: this.y - 40,
z: this.z, z: this.z,
scale: .2, scale: 0.2,
opacity: .6, opacity: 0.6,
}; };
const pos2 = { const pos2 = {
display: 'block', display: 'block',
x: this.x + 40, x: this.x + 40,
y: this.y - 35, y: this.y - 35,
z: this.z, z: this.z,
scale: .2, scale: 0.2,
opacity: .6, opacity: 0.6,
}; };
const pos3 = { const pos3 = {
display: 'block', display: 'block',
x: this.x + 20, x: this.x + 20,
y: this.y - 25, y: this.y - 25,
z: this.z, z: this.z,
scale: .2, scale: 0.2,
opacity: .6, opacity: 0.6,
}; };
const leechseed1 = new Sprite(BattleEffects.energyball, pos1, this.scene); const leechseed1 = new Sprite(BattleEffects.energyball, pos1, this.scene);
const leechseed2 = new Sprite(BattleEffects.energyball, pos2, this.scene); const leechseed2 = new Sprite(BattleEffects.energyball, pos2, this.scene);
const leechseed3 = new Sprite(BattleEffects.energyball, pos3, this.scene); const leechseed3 = new Sprite(BattleEffects.energyball, pos3, this.scene);
this.scene.$spritesFront[spriten].append(leechseed1.$el!); this.scene.$spritesFront[spriten].append(leechseed1.$el);
this.scene.$spritesFront[spriten].append(leechseed2.$el!); this.scene.$spritesFront[spriten].append(leechseed2.$el);
this.scene.$spritesFront[spriten].append(leechseed3.$el!); this.scene.$spritesFront[spriten].append(leechseed3.$el);
this.effects['leechseed'] = [leechseed1, leechseed2, leechseed3]; this.effects['leechseed'] = [leechseed1, leechseed2, leechseed3];
} else if (id === 'protect' || id === 'magiccoat') { } else if (id === 'protect' || id === 'magiccoat') {
const protect = new Sprite(BattleEffects.protect, { const protect = new Sprite(BattleEffects.protect, {
@ -2671,15 +2671,15 @@ export class PokemonSprite extends Sprite {
z: this.behind(-15), z: this.behind(-15),
xscale: 1, xscale: 1,
yscale: 0, yscale: 0,
opacity: .1, opacity: 0.1,
}, this.scene); }, this.scene);
this.scene.$spritesFront[spriten].append(protect.$el!); this.scene.$spritesFront[spriten].append(protect.$el);
this.effects[id] = [protect]; this.effects[id] = [protect];
protect.anim({ protect.anim({
opacity: .9, opacity: 0.9,
time: instant ? 0 : 400, time: instant ? 0 : 400,
}).anim({ }).anim({
opacity: .4, opacity: 0.4,
time: instant ? 0 : 300, time: instant ? 0 : 300,
}); });
} }
@ -2702,7 +2702,7 @@ export class PokemonSprite extends Sprite {
dogarsCheck(pokemon: Pokemon) { dogarsCheck(pokemon: Pokemon) {
if (pokemon.side.isFar) return; if (pokemon.side.isFar) return;
if (pokemon.speciesForme === 'Koffing' && pokemon.name.match(/dogars/i)) { if (pokemon.speciesForme === 'Koffing' && (/dogars/i.exec(pokemon.name))) {
this.scene.setBgm(-1); this.scene.setBgm(-1);
} else if (this.scene.bgmNum === -1) { } else if (this.scene.bgmNum === -1) {
this.scene.rollBgm(); this.scene.rollBgm();
@ -2734,7 +2734,7 @@ export class PokemonSprite extends Sprite {
buf += (pokemon.level === 100 ? `` : ` <small>L${pokemon.level}</small>`); buf += (pokemon.level === 100 ? `` : ` <small>L${pokemon.level}</small>`);
let symbol = ''; let symbol = '';
if (pokemon.speciesForme.indexOf('-Mega') >= 0) symbol = 'mega'; if (pokemon.speciesForme.includes('-Mega')) symbol = 'mega';
else if (pokemon.speciesForme === 'Kyogre-Primal') symbol = 'alpha'; else if (pokemon.speciesForme === 'Kyogre-Primal') symbol = 'alpha';
else if (pokemon.speciesForme === 'Groudon-Primal') symbol = 'omega'; else if (pokemon.speciesForme === 'Groudon-Primal') symbol = 'omega';
if (symbol) { if (symbol) {
@ -2852,7 +2852,7 @@ export class PokemonSprite extends Sprite {
private static getEffectTag(id: string) { private static getEffectTag(id: string) {
let effect = PokemonSprite.statusTable[id]; let effect = PokemonSprite.statusTable[id];
if (typeof effect === 'string') return effect; if (typeof effect === 'string') return effect;
if (effect === null) return PokemonSprite.statusTable[id] = ''; if (effect === null) return (PokemonSprite.statusTable[id] = '');
if (effect === undefined) { if (effect === undefined) {
let label = `[[${id}]]`; let label = `[[${id}]]`;
if (Dex.species.get(id).exists) { if (Dex.species.get(id).exists) {
@ -2868,7 +2868,7 @@ export class PokemonSprite extends Sprite {
} }
effect = [label, 'neutral']; effect = [label, 'neutral'];
} }
return PokemonSprite.statusTable[id] = `<span class="${effect[1]}">${effect[0].replace(/ /g, '&nbsp;')}</span> `; return (PokemonSprite.statusTable[id] = `<span class="${effect[1]}">${effect[0].replace(/ /g, '&nbsp;')}</span> `);
} }
updateHPText(pokemon: Pokemon) { updateHPText(pokemon: Pokemon) {
@ -2879,11 +2879,11 @@ export class PokemonSprite extends Sprite {
$hptext.hide(); $hptext.hide();
$hptextborder.hide(); $hptextborder.hide();
} else if (this.scene.battle.hardcoreMode || this.scene.battle.reportExactHP) { } else if (this.scene.battle.hardcoreMode || this.scene.battle.reportExactHP) {
$hptext.html(pokemon.hp + '/'); $hptext.html(`${pokemon.hp}/`);
$hptext.show(); $hptext.show();
$hptextborder.show(); $hptextborder.show();
} else { } else {
$hptext.html(pokemon.hpWidth(100) + '%'); $hptext.html(`${pokemon.hpWidth(100)}%`);
$hptext.show(); $hptext.show();
$hptextborder.show(); $hptextborder.show();
} }
@ -2897,7 +2897,6 @@ export class PokemonSprite extends Sprite {
// slp: -webkit-filter: grayscale(100%); // slp: -webkit-filter: grayscale(100%);
// frz: -webkit-filter: sepia(100%) hue-rotate(154deg) saturate(759%) brightness(23%); // frz: -webkit-filter: sepia(100%) hue-rotate(154deg) saturate(759%) brightness(23%);
// @ts-ignore
Object.assign($.easing, { Object.assign($.easing, {
ballisticUp(x: number, t: number, b: number, c: number, d: number) { ballisticUp(x: number, t: number, b: number, c: number, d: number) {
return -3 * x * x + 4 * x; return -3 * x * x + 4 * x;
@ -2920,9 +2919,9 @@ interface AnimData {
prepareAnim?(scene: BattleScene, args: PokemonSprite[]): void; prepareAnim?(scene: BattleScene, args: PokemonSprite[]): void;
residualAnim?(scene: BattleScene, args: PokemonSprite[]): void; residualAnim?(scene: BattleScene, args: PokemonSprite[]): void;
} }
export type AnimTable = {[k: string]: AnimData}; export type AnimTable = { [k: string]: AnimData };
const BattleEffects: {[k: string]: SpriteData} = { const BattleEffects: { [k: string]: SpriteData } = {
wisp: { wisp: {
url: 'wisp.png', url: 'wisp.png',
w: 100, h: 100, w: 100, h: 100,
@ -4266,16 +4265,16 @@ export const BattleOtherAnims: AnimTable = {
}, },
shake: { shake: {
anim(scene, [attacker]) { anim(scene, [attacker]) {
attacker.anim({x: attacker.x - 10, time: 200}); attacker.anim({ x: attacker.x - 10, time: 200 });
attacker.anim({x: attacker.x + 10, time: 300}); attacker.anim({ x: attacker.x + 10, time: 300 });
attacker.anim({x: attacker.x, time: 200}); attacker.anim({ x: attacker.x, time: 200 });
}, },
}, },
dance: { dance: {
anim(scene, [attacker]) { anim(scene, [attacker]) {
attacker.anim({x: attacker.x - 10}); attacker.anim({ x: attacker.x - 10 });
attacker.anim({x: attacker.x + 10}); attacker.anim({ x: attacker.x + 10 });
attacker.anim({x: attacker.x}); attacker.anim({ x: attacker.x });
}, },
}, },
consume: { consume: {
@ -6072,11 +6071,11 @@ export const BattleStatusAnims: AnimTable = {
anim(scene, [attacker]) { anim(scene, [attacker]) {
scene.backgroundEffect('#000000', 700, 0.2); scene.backgroundEffect('#000000', 700, 0.2);
attacker.delay(300); attacker.delay(300);
attacker.anim({x: attacker.x - 5, time: 50}); attacker.anim({ x: attacker.x - 5, time: 50 });
attacker.anim({x: attacker.x + 5, time: 50}); attacker.anim({ x: attacker.x + 5, time: 50 });
attacker.anim({x: attacker.x - 5, time: 50}); attacker.anim({ x: attacker.x - 5, time: 50 });
attacker.anim({x: attacker.x + 5, time: 50}); attacker.anim({ x: attacker.x + 5, time: 50 });
attacker.anim({x: attacker.x, time: 50}); attacker.anim({ x: attacker.x, time: 50 });
scene.showEffect(attacker.sp, { scene.showEffect(attacker.sp, {
x: attacker.x, x: attacker.x,
@ -6172,4 +6171,4 @@ export const BattleStatusAnims: AnimTable = {
}, },
}, },
}; };
BattleStatusAnims['focuspunch'] = {anim: BattleStatusAnims['flinch'].anim}; BattleStatusAnims['focuspunch'] = { anim: BattleStatusAnims['flinch'].anim };

View File

@ -12,8 +12,8 @@
* @license MIT * @license MIT
*/ */
import type {Battle, ServerPokemon} from "./battle"; import type { Battle, ServerPokemon } from "./battle";
import {Dex, toID, type ID} from "./battle-dex"; import { Dex, toID, type ID } from "./battle-dex";
export interface BattleRequestSideInfo { export interface BattleRequestSideInfo {
name: string; name: string;
@ -281,7 +281,7 @@ export class BattleChoiceBuilder {
const index = this.choices.length; const index = this.choices.length;
if (choice === 'shift') return {choiceType: 'shift'}; if (choice === 'shift') return { choiceType: 'shift' };
if (choice.startsWith('move ')) { if (choice.startsWith('move ')) {
if (request.requestType !== 'move') { if (request.requestType !== 'move') {
@ -397,7 +397,7 @@ export class BattleChoiceBuilder {
const choiceid = toID(choice); const choiceid = toID(choice);
let matchLevel = 0; let matchLevel = 0;
let match = 0; let match = 0;
for (let i = 0 ; i < request.side.pokemon.length; i++) { for (let i = 0; i < request.side.pokemon.length; i++) {
const serverPokemon = request.side.pokemon[i]; const serverPokemon = request.side.pokemon[i];
let curMatchLevel = 0; let curMatchLevel = 0;
if (choice === serverPokemon.name) { if (choice === serverPokemon.name) {
@ -429,7 +429,7 @@ export class BattleChoiceBuilder {
throw new Error(`Couldn't find Pokémon "${choice}" to switch to!`); throw new Error(`Couldn't find Pokémon "${choice}" to switch to!`);
} }
if (target.fainted) { if (target.fainted) {
throw new Error(`${target} is fainted and cannot battle!`); throw new Error(`${target.name} is fainted and cannot battle!`);
} }
return current; return current;
} }

View File

@ -14,19 +14,19 @@
* @license MIT * @license MIT
*/ */
import {Dex, toID} from "./battle-dex"; import { Dex, toID } from "./battle-dex";
/** /**
* String that contains only lowercase alphanumeric characters. * String that contains only lowercase alphanumeric characters.
*/ */
export type ID = string & {__isID: true}; export type ID = string & { __isID: true };
export interface Nature { export interface Nature {
plus?: StatNameExceptHP; plus?: StatNameExceptHP;
minus?: StatNameExceptHP; minus?: StatNameExceptHP;
} }
export const BattleNatures: {[k in NatureName]: Nature} = { export const BattleNatures: { [k in NatureName]: Nature } = {
Adamant: { Adamant: {
plus: 'atk', plus: 'atk',
minus: 'spa', minus: 'spa',
@ -113,7 +113,7 @@ export const BattleNatures: {[k in NatureName]: Nature} = {
minus: 'atk', minus: 'atk',
}, },
}; };
export const BattleStatIDs: {[k: string]: StatName | undefined} = { export const BattleStatIDs: { [k: string]: StatName | undefined } = {
HP: 'hp', HP: 'hp',
hp: 'hp', hp: 'hp',
Atk: 'atk', Atk: 'atk',
@ -148,7 +148,7 @@ export const BattleBaseSpeciesChart = [
"unown", "burmy", "shellos", "gastrodon", "deerling", "sawsbuck", "vivillon", "flabebe", "floette", "florges", "furfrou", "minior", "alcremie", "tatsugiri", "pokestarufo", "pokestarbrycenman", "pokestarmt", "pokestarmt2", "pokestartransport", "pokestargiant", "pokestarhumanoid", "pokestarmonster", "pokestarf00", "pokestarf002", "pokestarspirit", "pokestarblackdoor", "pokestarwhitedoor", "pokestarblackbelt", "unown", "burmy", "shellos", "gastrodon", "deerling", "sawsbuck", "vivillon", "flabebe", "floette", "florges", "furfrou", "minior", "alcremie", "tatsugiri", "pokestarufo", "pokestarbrycenman", "pokestarmt", "pokestarmt2", "pokestartransport", "pokestargiant", "pokestarhumanoid", "pokestarmonster", "pokestarf00", "pokestarf002", "pokestarspirit", "pokestarblackdoor", "pokestarwhitedoor", "pokestarblackbelt",
] as ID[]; ] as ID[];
export const BattlePokemonIconIndexes: {[id: string]: number} = { export const BattlePokemonIconIndexes: { [id: string]: number } = {
// alt forms // alt forms
egg: 1032 + 1, egg: 1032 + 1,
pikachubelle: 1032 + 2, pikachubelle: 1032 + 2,
@ -628,7 +628,7 @@ export const BattlePokemonIconIndexes: {[id: string]: number} = {
draggalong: 1512 + 77, draggalong: 1512 + 77,
}; };
export const BattlePokemonIconIndexesLeft: {[id: string]: number} = { export const BattlePokemonIconIndexesLeft: { [id: string]: number } = {
pikachubelle: 1404 + 0, pikachubelle: 1404 + 0,
pikachupopstar: 1404 + 1, pikachupopstar: 1404 + 1,
clefairy: 1404 + 2, clefairy: 1404 + 2,
@ -738,7 +738,7 @@ export const BattlePokemonIconIndexesLeft: {[id: string]: number} = {
blacephalon: 1404 + 105, blacephalon: 1404 + 105,
}; };
export const BattleAvatarNumbers: {[k: string]: string} = { export const BattleAvatarNumbers: { [k: string]: string } = {
1: 'lucas', 1: 'lucas',
2: 'dawn', 2: 'dawn',
3: 'youngster-gen4dp', 3: 'youngster-gen4dp',
@ -1240,10 +1240,10 @@ export class Move implements Effect {
readonly zMove?: { readonly zMove?: {
basePower?: number, basePower?: number,
effect?: string, effect?: string,
boost?: {[stat in StatName]?: number}, boost?: { [stat in StatName]?: number },
}; };
readonly isMax: boolean | string; readonly isMax: boolean | string;
readonly maxMove: {basePower: number}; readonly maxMove: { basePower: number };
readonly ohko: true | 'Ice' | null; readonly ohko: true | 'Ice' | null;
readonly recoil: number[] | null; readonly recoil: number[] | null;
readonly heal: number[] | null; readonly heal: number[] | null;
@ -1252,7 +1252,7 @@ export class Move implements Effect {
readonly basePowerCallback: boolean; readonly basePowerCallback: boolean;
readonly noPPBoosts: boolean; readonly noPPBoosts: boolean;
readonly status: string; readonly status: string;
readonly secondaries: ReadonlyArray<any> | null; readonly secondaries: readonly any[] | null;
readonly num: number; readonly num: number;
constructor(id: ID, name: string, data: any) { constructor(id: ID, name: string, data: any) {
@ -1291,43 +1291,43 @@ export class Move implements Effect {
this.secondaries = data.secondaries || (data.secondary ? [data.secondary] : null); this.secondaries = data.secondaries || (data.secondary ? [data.secondary] : null);
this.isMax = data.isMax || false; this.isMax = data.isMax || false;
this.maxMove = data.maxMove || {basePower: 0}; this.maxMove = data.maxMove || { basePower: 0 };
if (this.category !== 'Status' && !this.maxMove?.basePower) { if (this.category !== 'Status' && !this.maxMove?.basePower) {
if (this.isZ || this.isMax) { if (this.isZ || this.isMax) {
this.maxMove = {basePower: 1}; this.maxMove = { basePower: 1 };
} else if (!this.basePower) { } else if (!this.basePower) {
this.maxMove = {basePower: 100}; this.maxMove = { basePower: 100 };
} else if (['Fighting', 'Poison'].includes(this.type)) { } else if (['Fighting', 'Poison'].includes(this.type)) {
if (this.basePower >= 150) { if (this.basePower >= 150) {
this.maxMove = {basePower: 100}; this.maxMove = { basePower: 100 };
} else if (this.basePower >= 110) { } else if (this.basePower >= 110) {
this.maxMove = {basePower: 95}; this.maxMove = { basePower: 95 };
} else if (this.basePower >= 75) { } else if (this.basePower >= 75) {
this.maxMove = {basePower: 90}; this.maxMove = { basePower: 90 };
} else if (this.basePower >= 65) { } else if (this.basePower >= 65) {
this.maxMove = {basePower: 85}; this.maxMove = { basePower: 85 };
} else if (this.basePower >= 55) { } else if (this.basePower >= 55) {
this.maxMove = {basePower: 80}; this.maxMove = { basePower: 80 };
} else if (this.basePower >= 45) { } else if (this.basePower >= 45) {
this.maxMove = {basePower: 75}; this.maxMove = { basePower: 75 };
} else { } else {
this.maxMove = {basePower: 70}; this.maxMove = { basePower: 70 };
} }
} else { } else {
if (this.basePower >= 150) { if (this.basePower >= 150) {
this.maxMove = {basePower: 150}; this.maxMove = { basePower: 150 };
} else if (this.basePower >= 110) { } else if (this.basePower >= 110) {
this.maxMove = {basePower: 140}; this.maxMove = { basePower: 140 };
} else if (this.basePower >= 75) { } else if (this.basePower >= 75) {
this.maxMove = {basePower: 130}; this.maxMove = { basePower: 130 };
} else if (this.basePower >= 65) { } else if (this.basePower >= 65) {
this.maxMove = {basePower: 120}; this.maxMove = { basePower: 120 };
} else if (this.basePower >= 55) { } else if (this.basePower >= 55) {
this.maxMove = {basePower: 110}; this.maxMove = { basePower: 110 };
} else if (this.basePower >= 45) { } else if (this.basePower >= 45) {
this.maxMove = {basePower: 100}; this.maxMove = { basePower: 100 };
} else { } else {
this.maxMove = {basePower: 90}; this.maxMove = { basePower: 90 };
} }
} }
} }
@ -1468,7 +1468,7 @@ export class Species implements Effect {
// basic data // basic data
readonly num: number; readonly num: number;
readonly types: ReadonlyArray<TypeName>; readonly types: readonly TypeName[];
readonly abilities: Readonly<{ readonly abilities: Readonly<{
0: string, 1?: string, H?: string, S?: string, 0: string, 1?: string, H?: string, S?: string,
}>; }>;
@ -1482,21 +1482,21 @@ export class Species implements Effect {
readonly heightm: number; readonly heightm: number;
readonly gender: GenderName; readonly gender: GenderName;
readonly color: string; readonly color: string;
readonly genderRatio: Readonly<{M: number, F: number}> | null; readonly genderRatio: Readonly<{ M: number, F: number }> | null;
readonly eggGroups: ReadonlyArray<string>; readonly eggGroups: readonly string[];
readonly tags: ReadonlyArray<string>; readonly tags: readonly string[];
// format data // format data
readonly otherFormes: ReadonlyArray<string> | null; readonly otherFormes: readonly string[] | null;
readonly cosmeticFormes: ReadonlyArray<string> | null; readonly cosmeticFormes: readonly string[] | null;
readonly evos: ReadonlyArray<string> | null; readonly evos: readonly string[] | null;
readonly prevo: string; readonly prevo: string;
readonly evoType: 'trade' | 'useItem' | 'levelMove' | 'levelExtra' | 'levelFriendship' | 'levelHold' | 'other' | ''; readonly evoType: 'trade' | 'useItem' | 'levelMove' | 'levelExtra' | 'levelFriendship' | 'levelHold' | 'other' | '';
readonly evoLevel: number; readonly evoLevel: number;
readonly evoMove: string; readonly evoMove: string;
readonly evoItem: string; readonly evoItem: string;
readonly evoCondition: string; readonly evoCondition: string;
readonly requiredItems: ReadonlyArray<string>; readonly requiredItems: readonly string[];
readonly tier: string; readonly tier: string;
readonly isTotem: boolean; readonly isTotem: boolean;
readonly isMega: boolean; readonly isMega: boolean;
@ -1521,15 +1521,15 @@ export class Species implements Effect {
const baseId = toID(this.baseSpecies); const baseId = toID(this.baseSpecies);
this.formeid = (baseId === this.id ? '' : '-' + toID(this.forme)); this.formeid = (baseId === this.id ? '' : '-' + toID(this.forme));
this.spriteid = baseId + this.formeid; this.spriteid = baseId + this.formeid;
if (this.spriteid.slice(-5) === 'totem') this.spriteid = this.spriteid.slice(0, -5); if (this.spriteid.endsWith('totem')) this.spriteid = this.spriteid.slice(0, -5);
if (this.spriteid === 'greninja-bond') this.spriteid = 'greninja'; if (this.spriteid === 'greninja-bond') this.spriteid = 'greninja';
if (this.spriteid.slice(-1) === '-') this.spriteid = this.spriteid.slice(0, -1); if (this.spriteid.endsWith('-')) this.spriteid = this.spriteid.slice(0, -1);
this.baseForme = data.baseForme || ''; this.baseForme = data.baseForme || '';
this.num = data.num || 0; this.num = data.num || 0;
this.types = data.types || ['???']; this.types = data.types || ['???'];
this.abilities = data.abilities || {0: "No Ability"}; this.abilities = data.abilities || { 0: "No Ability" };
this.baseStats = data.baseStats || {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; this.baseStats = data.baseStats || { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
this.bst = this.baseStats.hp + this.baseStats.atk + this.baseStats.def + this.bst = this.baseStats.hp + this.baseStats.atk + this.baseStats.def +
this.baseStats.spa + this.baseStats.spd + this.baseStats.spe; this.baseStats.spa + this.baseStats.spd + this.baseStats.spe;
this.weightkg = data.weightkg || 0; this.weightkg = data.weightkg || 0;

View File

@ -11,7 +11,7 @@
* @license MIT * @license MIT
*/ */
import {Dex, ModdedDex, toID, type ID} from "./battle-dex"; import { Dex, type ModdedDex, toID, type ID } from "./battle-dex";
type SearchType = ( type SearchType = (
'pokemon' | 'type' | 'tier' | 'move' | 'item' | 'ability' | 'egggroup' | 'category' | 'article' 'pokemon' | 'type' | 'tier' | 'move' | 'item' | 'ability' | 'egggroup' | 'category' | 'article'
@ -217,7 +217,7 @@ export class DexSearch {
/** searching for "Psychic type" will make the type come up over the move */ /** searching for "Psychic type" will make the type come up over the move */
let qFilterType: 'type' | '' = ''; let qFilterType: 'type' | '' = '';
if (query.slice(-4) === 'type') { if (query.endsWith('type')) {
if (query.slice(0, -4) in window.BattleTypeChart) { if (query.slice(0, -4) in window.BattleTypeChart) {
query = query.slice(0, -4); query = query.slice(0, -4);
qFilterType = 'type'; qFilterType = 'type';
@ -265,7 +265,7 @@ export class DexSearch {
// the alias text before any other passes. // the alias text before any other passes.
let queryAlias; let queryAlias;
if (query in BattleAliases) { if (query in BattleAliases) {
if (['sub', 'tr'].includes(query) || toID(BattleAliases[query]).slice(0, query.length) !== query) { if (['sub', 'tr'].includes(query) || !toID(BattleAliases[query]).startsWith(query)) {
queryAlias = toID(BattleAliases[query]); queryAlias = toID(BattleAliases[query]);
let aliasPassType: SearchPassType = (queryAlias === 'hiddenpower' ? 'exact' : 'normal'); let aliasPassType: SearchPassType = (queryAlias === 'hiddenpower' ? 'exact' : 'normal');
searchPasses.unshift([aliasPassType, DexSearch.getClosest(queryAlias), queryAlias]); searchPasses.unshift([aliasPassType, DexSearch.getClosest(queryAlias), queryAlias]);
@ -565,7 +565,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
* is wondering why a specific result isn't showing up. * is wondering why a specific result isn't showing up.
*/ */
baseIllegalResults: SearchRow[] | null = null; baseIllegalResults: SearchRow[] | null = null;
illegalReasons: {[id: string]: string} | null = null; illegalReasons: { [id: string]: string } | null = null;
results: SearchRow[] | null = null; results: SearchRow[] | null = null;
protected readonly sortRow: SearchRow | null = null; protected readonly sortRow: SearchRow | null = null;
@ -576,7 +576,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
this.baseResults = null; this.baseResults = null;
this.baseIllegalResults = null; this.baseIllegalResults = null;
if (format.slice(0, 3) === 'gen') { if (format.startsWith('gen')) {
const gen = (Number(format.charAt(3)) || 6); const gen = (Number(format.charAt(3)) || 6);
format = (format.slice(4) || 'customgame') as ID; format = (format.slice(4) || 'customgame') as ID;
this.dex = Dex.forGen(gen); this.dex = Dex.forGen(gen);
@ -668,10 +668,10 @@ abstract class BattleTypedSearch<T extends SearchType> {
if (typeof speciesOrSet === 'string') { if (typeof speciesOrSet === 'string') {
if (speciesOrSet) this.species = speciesOrSet; if (speciesOrSet) this.species = speciesOrSet;
} else { } else {
this.set = speciesOrSet as Dex.PokemonSet; this.set = speciesOrSet;
this.species = toID(this.set.species); this.species = toID(this.set.species);
} }
if (!searchType || !this.set) return; // if (!searchType || !this.set) return;
} }
getResults(filters?: SearchFilter[] | null, sortCol?: string | null, reverseSort?: boolean): SearchRow[] { getResults(filters?: SearchFilter[] | null, sortCol?: string | null, reverseSort?: boolean): SearchRow[] {
if (sortCol === 'type') { if (sortCol === 'type') {
@ -687,7 +687,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
} }
if (!this.baseIllegalResults) { if (!this.baseIllegalResults) {
const legalityFilter: {[id: string]: 1} = {}; const legalityFilter: { [id: string]: 1 } = {};
for (const [resultType, value] of this.baseResults) { for (const [resultType, value] of this.baseResults) {
if (resultType === this.searchType) legalityFilter[value] = 1; if (resultType === this.searchType) legalityFilter[value] = 1;
} }
@ -742,7 +742,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
if (this.sortRow) { if (this.sortRow) {
results = [this.sortRow, ...results]; results = [this.sortRow, ...results];
} }
if (illegalResults && illegalResults.length) { if (illegalResults?.length) {
results = [...results, ['header', "Illegal results"], ...illegalResults]; results = [...results, ['header', "Illegal results"], ...illegalResults];
} }
return results; return results;
@ -856,7 +856,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
this.formatType === 'natdex' ? `gen${gen}natdex` : this.formatType === 'natdex' ? `gen${gen}natdex` :
this.formatType === 'stadium' ? `gen${gen}stadium${gen > 1 ? gen : ''}` : this.formatType === 'stadium' ? `gen${gen}stadium${gen > 1 ? gen : ''}` :
`gen${gen}`; `gen${gen}`;
if (table && table[tableKey]) { if (table?.[tableKey]) {
table = table[tableKey]; table = table[tableKey];
} }
if (!table) return pokemon.tier; if (!table) return pokemon.tier;
@ -865,7 +865,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
if (id in table.overrideTier) { if (id in table.overrideTier) {
return table.overrideTier[id]; return table.overrideTier[id];
} }
if (id.slice(-5) === 'totem' && id.slice(0, -5) in table.overrideTier) { if (id.endsWith('totem') && id.slice(0, -5) in table.overrideTier) {
return table.overrideTier[id.slice(0, -5)]; return table.overrideTier[id.slice(0, -5)];
} }
id = toID(pokemon.baseSpecies); id = toID(pokemon.baseSpecies);
@ -884,7 +884,7 @@ abstract class BattleTypedSearch<T extends SearchType> {
} }
return true; return true;
} }
abstract getTable(): {[id: string]: any}; abstract getTable(): { [id: string]: any };
abstract getDefaultResults(): SearchRow[]; abstract getDefaultResults(): SearchRow[];
abstract getBaseResults(): SearchRow[]; abstract getBaseResults(): SearchRow[];
abstract filter(input: SearchRow, filters: string[][]): boolean; abstract filter(input: SearchRow, filters: string[][]): boolean;
@ -951,13 +951,13 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
let table = BattleTeambuilderTable; let table = BattleTeambuilderTable;
if ((format.endsWith('cap') || format.endsWith('caplc')) && dex.gen < 9) { if ((format.endsWith('cap') || format.endsWith('caplc')) && dex.gen < 9) {
table = table['gen' + dex.gen]; table = table[`gen${dex.gen}`];
} else if (isVGCOrBS) { } else if (isVGCOrBS) {
table = table['gen' + dex.gen + 'vgc']; table = table[`gen${dex.gen}vgc`];
} else if (dex.gen === 9 && isHackmons && !this.formatType) { } else if (dex.gen === 9 && isHackmons && !this.formatType) {
table = table['bh']; table = table['bh'];
} else if ( } else if (
table['gen' + dex.gen + 'doubles'] && dex.gen > 4 && table[`gen${dex.gen}doubles`] && dex.gen > 4 &&
this.formatType !== 'letsgo' && this.formatType !== 'bdspdoubles' && this.formatType !== 'letsgo' && this.formatType !== 'bdspdoubles' &&
this.formatType !== 'ssdlc1doubles' && this.formatType !== 'predlcdoubles' && this.formatType !== 'ssdlc1doubles' && this.formatType !== 'predlcdoubles' &&
this.formatType !== 'svdlc1doubles' && !this.formatType?.includes('natdex') && this.formatType !== 'svdlc1doubles' && !this.formatType?.includes('natdex') &&
@ -967,10 +967,10 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
format === 'partnersincrime' format === 'partnersincrime'
) )
) { ) {
table = table['gen' + dex.gen + 'doubles']; table = table[`gen${dex.gen}doubles`];
isDoublesOrBS = true; isDoublesOrBS = true;
} else if (dex.gen < 9 && !this.formatType) { } else if (dex.gen < 9 && !this.formatType) {
table = table['gen' + dex.gen]; table = table[`gen${dex.gen}`];
} else if (this.formatType?.startsWith('bdsp')) { } else if (this.formatType?.startsWith('bdsp')) {
table = table['gen8' + this.formatType]; table = table['gen8' + this.formatType];
} else if (this.formatType === 'letsgo') { } else if (this.formatType === 'letsgo') {
@ -978,13 +978,13 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
} else if (this.formatType === 'bw1') { } else if (this.formatType === 'bw1') {
table = table['gen5bw1']; table = table['gen5bw1'];
} else if (this.formatType === 'natdex') { } else if (this.formatType === 'natdex') {
table = table['gen' + dex.gen + 'natdex']; table = table[`gen${dex.gen}natdex`];
} else if (this.formatType === 'metronome') { } else if (this.formatType === 'metronome') {
table = table['gen' + dex.gen + 'metronome']; table = table[`gen${dex.gen}metronome`];
} else if (this.formatType === 'nfe') { } else if (this.formatType === 'nfe') {
table = table['gen' + dex.gen + 'nfe']; table = table[`gen${dex.gen}nfe`];
} else if (this.formatType === 'lc') { } else if (this.formatType === 'lc') {
table = table['gen' + dex.gen + 'lc']; table = table[`gen${dex.gen}lc`];
} else if (this.formatType?.startsWith('ssdlc1')) { } else if (this.formatType?.startsWith('ssdlc1')) {
if (this.formatType.includes('doubles')) { if (this.formatType.includes('doubles')) {
table = table['gen8dlc1doubles']; table = table['gen8dlc1doubles'];
@ -1008,7 +1008,7 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
table = table['gen9dlc1']; table = table['gen9dlc1'];
} }
} else if (this.formatType === 'stadium') { } else if (this.formatType === 'stadium') {
table = table['gen' + dex.gen + 'stadium' + (dex.gen > 1 ? dex.gen : '')]; table = table[`gen${dex.gen}stadium${dex.gen > 1 ? dex.gen : ''}`];
} }
if (!table.tierSet) { if (!table.tierSet) {
@ -1019,7 +1019,7 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
table.tiers = null; table.tiers = null;
} }
let tierSet: SearchRow[] = table.tierSet; let tierSet: SearchRow[] = table.tierSet;
let slices: {[k: string]: number} = table.formatSlices; let slices: { [k: string]: number } = table.formatSlices;
if (format === 'ubers' || format === 'uber' || format === 'ubersuu' || format === 'nationaldexdoubles') { if (format === 'ubers' || format === 'uber' || format === 'ubersuu' || format === 'nationaldexdoubles') {
tierSet = tierSet.slice(slices.Uber); tierSet = tierSet.slice(slices.Uber);
} else if (isVGCOrBS || (isHackmons && dex.gen === 9 && !this.formatType)) { } else if (isVGCOrBS || (isHackmons && dex.gen === 9 && !this.formatType)) {
@ -1049,7 +1049,9 @@ class BattlePokemonSearch extends BattleTypedSearch<'pokemon'> {
else if (format === 'pu') tierSet = tierSet.slice(slices.PU || slices.NU); else if (format === 'pu') tierSet = tierSet.slice(slices.PU || slices.NU);
else if (format === 'zu' && dex.gen === 5) tierSet = tierSet.slice(slices.PU || slices.NU); else if (format === 'zu' && dex.gen === 5) tierSet = tierSet.slice(slices.PU || slices.NU);
else if (format === 'zu') tierSet = tierSet.slice(slices.ZU || slices.PU || slices.NU); else if (format === 'zu') tierSet = tierSet.slice(slices.ZU || slices.PU || slices.NU);
else if (format === 'lc' || format === 'lcuu' || format.startsWith('lc') || (format !== 'caplc' && format.endsWith('lc'))) tierSet = tierSet.slice(slices.LC); else if (
format === 'lc' || format === 'lcuu' || format.startsWith('lc') || (format !== 'caplc' && format.endsWith('lc'))
) tierSet = tierSet.slice(slices.LC);
else if (format === 'cap' || format.endsWith('cap')) { else if (format === 'cap' || format.endsWith('cap')) {
tierSet = tierSet.slice(0, slices.AG || slices.Uber).concat(tierSet.slice(slices.OU)); tierSet = tierSet.slice(0, slices.AG || slices.Uber).concat(tierSet.slice(slices.OU));
} else if (format === 'caplc') { } else if (format === 'caplc') {
@ -1276,11 +1278,11 @@ class BattleItemSearch extends BattleTypedSearch<'item'> {
} else if (this.formatType === 'bw1') { } else if (this.formatType === 'bw1') {
table = table['gen5bw1']; table = table['gen5bw1'];
} else if (this.formatType === 'natdex') { } else if (this.formatType === 'natdex') {
table = table['gen' + this.dex.gen + 'natdex']; table = table[`gen${this.dex.gen}natdex`];
} else if (this.formatType === 'metronome') { } else if (this.formatType === 'metronome') {
table = table['gen' + this.dex.gen + 'metronome']; table = table[`gen${this.dex.gen}metronome`];
} else if (this.dex.gen < 9) { } else if (this.dex.gen < 9) {
table = table['gen' + this.dex.gen]; table = table[`gen${this.dex.gen}`];
} }
if (!table.itemSet) { if (!table.itemSet) {
table.itemSet = table.items.map((r: any) => { table.itemSet = table.items.map((r: any) => {
@ -1635,7 +1637,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
let moves: string[] = []; let moves: string[] = [];
let sketchMoves: string[] = []; let sketchMoves: string[] = [];
let sketch = false; let sketch = false;
let gen = '' + dex.gen; let gen = `${dex.gen}`;
let lsetTable = BattleTeambuilderTable; let lsetTable = BattleTeambuilderTable;
if (this.formatType?.startsWith('bdsp')) lsetTable = lsetTable['gen8bdsp']; if (this.formatType?.startsWith('bdsp')) lsetTable = lsetTable['gen8bdsp'];
if (this.formatType === 'letsgo') lsetTable = lsetTable['gen7letsgo']; if (this.formatType === 'letsgo') lsetTable = lsetTable['gen7letsgo'];
@ -1649,7 +1651,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
for (let moveid in learnset) { for (let moveid in learnset) {
let learnsetEntry = learnset[moveid]; let learnsetEntry = learnset[moveid];
const move = dex.moves.get(moveid); const move = dex.moves.get(moveid);
const minGenCode: {[gen: number]: string} = {6: 'p', 7: 'q', 8: 'g', 9: 'a'}; const minGenCode: { [gen: number]: string } = { 6: 'p', 7: 'q', 8: 'g', 9: 'a' };
if (regionBornLegality && !learnsetEntry.includes(minGenCode[dex.gen])) { if (regionBornLegality && !learnsetEntry.includes(minGenCode[dex.gen])) {
continue; continue;
} }
@ -1661,7 +1663,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
} }
if ( if (
!learnsetEntry.includes(gen) && !learnsetEntry.includes(gen) &&
(!isTradebacks ? true : !(move.gen <= dex.gen && learnsetEntry.includes('' + (dex.gen + 1)))) (!isTradebacks ? true : !(move.gen <= dex.gen && learnsetEntry.includes(`${dex.gen + 1}`)))
) { ) {
continue; continue;
} }
@ -1819,7 +1821,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
const sortOrder = reverseSort ? -1 : 1; const sortOrder = reverseSort ? -1 : 1;
switch (sortCol) { switch (sortCol) {
case 'power': case 'power':
let powerTable: {[id: string]: number | undefined} = { let powerTable: { [id: string]: number | undefined } = {
return: 102, frustration: 102, spitup: 300, trumpcard: 200, naturalgift: 80, grassknot: 120, return: 102, frustration: 102, spitup: 300, trumpcard: 200, naturalgift: 80, grassknot: 120,
lowkick: 120, gyroball: 150, electroball: 150, flail: 200, reversal: 200, present: 120, lowkick: 120, gyroball: 150, electroball: 150, flail: 200, reversal: 200, present: 120,
wringout: 120, crushgrip: 120, heatcrash: 120, heavyslam: 120, fling: 130, magnitude: 150, wringout: 120, crushgrip: 120, heatcrash: 120, heavyslam: 120, fling: 130, magnitude: 150,
@ -1861,7 +1863,7 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> {
class BattleCategorySearch extends BattleTypedSearch<'category'> { class BattleCategorySearch extends BattleTypedSearch<'category'> {
getTable() { getTable() {
return {physical: 1, special: 1, status: 1}; return { physical: 1, special: 1, status: 1 };
} }
getDefaultResults(): SearchRow[] { getDefaultResults(): SearchRow[] {
return [ return [

View File

@ -18,16 +18,15 @@
* @license MIT * @license MIT
*/ */
import {Pokemon, type ServerPokemon} from "./battle"; import { Pokemon, type ServerPokemon } from "./battle";
import { import {
BattleAvatarNumbers, BattleBaseSpeciesChart, BattlePokemonIconIndexes, BattlePokemonIconIndexesLeft, BattleStatNames, BattleAvatarNumbers, BattleBaseSpeciesChart, BattlePokemonIconIndexes, BattlePokemonIconIndexesLeft, BattleStatNames,
Ability, Item, Move, Species, PureEffect, type ID, type Type, Ability, Item, Move, Species, PureEffect, type ID, type Type,
} from "./battle-dex-data"; } from "./battle-dex-data";
// tslint:disable-next-line import type * as DexData from "./battle-dex-data";
import * as DexData from "./battle-dex-data";
export declare namespace Dex { export declare namespace Dex {
/* tslint:disable:no-shadowed-variable */ /* eslint-disable @typescript-eslint/no-shadow */
export type Ability = DexData.Ability; export type Ability = DexData.Ability;
export type Item = DexData.Item; export type Item = DexData.Item;
export type Move = DexData.Move; export type Move = DexData.Move;
@ -37,7 +36,7 @@ export declare namespace Dex {
export type PureEffect = DexData.PureEffect; export type PureEffect = DexData.PureEffect;
export type Effect = DexData.Effect; export type Effect = DexData.Effect;
export type ID = DexData.ID; export type ID = DexData.ID;
/* tslint:enable:no-shadowed-variable */ /* eslint-enable @typescript-eslint/no-shadow */
export type StatName = DexData.StatName; export type StatName = DexData.StatName;
export type StatNameExceptHP = DexData.StatNameExceptHP; export type StatNameExceptHP = DexData.StatNameExceptHP;
export type BoostStatName = DexData.BoostStatName; export type BoostStatName = DexData.BoostStatName;
@ -46,7 +45,7 @@ export declare namespace Dex {
export type GenderName = DexData.GenderName; export type GenderName = DexData.GenderName;
export type NatureName = DexData.NatureName; export type NatureName = DexData.NatureName;
export type MoveTarget = DexData.MoveTarget; export type MoveTarget = DexData.MoveTarget;
export type StatsTable = {hp: number, atk: number, def: number, spa: number, spd: number, spe: number}; export type StatsTable = { hp: number, atk: number, def: number, spa: number, spd: number, spe: number };
/** /**
* Dex.PokemonSet can be sparse, in which case that entry should be * Dex.PokemonSet can be sparse, in which case that entry should be
* inferred from the rest of the set, according to sensible * inferred from the rest of the set, according to sensible
@ -87,10 +86,11 @@ export declare namespace Dex {
teraType?: string; teraType?: string;
} }
} }
export type {ID}; export type { ID };
declare const require: any; declare const require: any;
declare const global: any; declare const global: any;
declare const process: any;
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
// Node // Node
@ -100,8 +100,7 @@ if (typeof window === 'undefined') {
window.exports = window; window.exports = window;
} }
// @ts-ignore window.nodewebkit = !!(typeof process !== 'undefined' && process.versions?.['node-webkit']);
window.nodewebkit = !!(typeof process !== 'undefined' && process.versions && process.versions['node-webkit']);
export function toID(text: any) { export function toID(text: any) {
if (text?.id) { if (text?.id) {
@ -110,14 +109,14 @@ export function toID(text: any) {
text = text.userid; text = text.userid;
} }
if (typeof text !== 'string' && typeof text !== 'number') return '' as ID; if (typeof text !== 'string' && typeof text !== 'number') return '' as ID;
return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '') as ID; return `${text}`.toLowerCase().replace(/[^a-z0-9]+/g, '') as ID;
} }
export function toUserid(text: any) { export function toUserid(text: any) {
return toID(text); return toID(text);
} }
type Comparable = number | string | boolean | Comparable[] | {reverse: Comparable}; type Comparable = number | string | boolean | Comparable[] | { reverse: Comparable };
export const PSUtils = new class { export const PSUtils = new class {
/** /**
* Like string.split(delimiter), but only recognizes the first `limit` * Like string.split(delimiter), but only recognizes the first `limit`
@ -129,7 +128,7 @@ export const PSUtils = new class {
* *
* Returns an array of length exactly limit + 1. * Returns an array of length exactly limit + 1.
*/ */
splitFirst(str: string, delimiter: string, limit: number = 1) { splitFirst(str: string, delimiter: string, limit = 1) {
let splitStr: string[] = []; let splitStr: string[] = [];
while (splitStr.length < limit) { while (splitStr.length < limit) {
let delimiterIndex = str.indexOf(delimiter); let delimiterIndex = str.indexOf(delimiter);
@ -174,9 +173,9 @@ export const PSUtils = new class {
return 0; return 0;
} }
if (a.reverse) { if (a.reverse) {
return PSUtils.compare((b as {reverse: string}).reverse, a.reverse); return PSUtils.compare((b as { reverse: string }).reverse, a.reverse);
} }
throw new Error(`Passed value ${a} is not comparable`); throw new Error(`Passed value ${a as any} is not comparable`);
} }
/** /**
* Sorts an array according to the callback's output on its elements. * Sorts an array according to the callback's output on its elements.
@ -206,7 +205,7 @@ export function toRoomid(roomid: string) {
export function toName(name: any) { export function toName(name: any) {
if (typeof name !== 'string' && typeof name !== 'number') return ''; if (typeof name !== 'string' && typeof name !== 'number') return '';
name = ('' + name).replace(/[\|\s\[\]\,\u202e]+/g, ' ').trim(); name = `${name}`.replace(/[|\s[\],\u202e]+/g, ' ').trim();
if (name.length > 18) name = name.substr(0, 18).trim(); if (name.length > 18) name = name.substr(0, 18).trim();
// remove zalgo // remove zalgo
@ -250,8 +249,8 @@ export const Dex = new class implements ModdedDex {
readonly modid = 'gen9' as ID; readonly modid = 'gen9' as ID;
readonly cache = null!; readonly cache = null!;
readonly statNames: ReadonlyArray<Dex.StatName> = ['hp', 'atk', 'def', 'spa', 'spd', 'spe']; readonly statNames: readonly Dex.StatName[] = ['hp', 'atk', 'def', 'spa', 'spd', 'spe'];
readonly statNamesExceptHP: ReadonlyArray<Dex.StatNameExceptHP> = ['atk', 'def', 'spa', 'spd', 'spe']; readonly statNamesExceptHP: readonly Dex.StatNameExceptHP[] = ['atk', 'def', 'spa', 'spd', 'spe'];
pokeballs: string[] | null = null; pokeballs: string[] | null = null;
@ -266,8 +265,8 @@ export const Dex = new class implements ModdedDex {
return `${protocol}//${window.Config ? Config.routes.client : 'play.pokemonshowdown.com'}/fx/`; return `${protocol}//${window.Config ? Config.routes.client : 'play.pokemonshowdown.com'}/fx/`;
})(); })();
loadedSpriteData = {xy: 1, bw: 0}; loadedSpriteData = { xy: 1, bw: 0 };
moddedDexes: {[mod: string]: ModdedDex} = {}; moddedDexes: { [mod: string]: ModdedDex } = {};
mod(modid: ID): ModdedDex { mod(modid: ID): ModdedDex {
if (modid === 'gen9') return this; if (modid === 'gen9') return this;
@ -287,14 +286,14 @@ export const Dex = new class implements ModdedDex {
if (window.BattleAvatarNumbers && avatar in BattleAvatarNumbers) { if (window.BattleAvatarNumbers && avatar in BattleAvatarNumbers) {
avatar = BattleAvatarNumbers[avatar]; avatar = BattleAvatarNumbers[avatar];
} }
if (avatar.charAt(0) === '#') { if (avatar.startsWith('#')) {
return Dex.resourcePrefix + 'sprites/trainers-custom/' + toID(avatar.substr(1)) + '.png'; return Dex.resourcePrefix + 'sprites/trainers-custom/' + toID(avatar.substr(1)) + '.png';
} }
if (avatar.includes('.') && window.Config?.server?.registered) { if (avatar.includes('.') && window.Config?.server?.registered) {
// custom avatar served by the server // custom avatar served by the server
let protocol = (Config.server.port === 443) ? 'https' : 'http'; let protocol = (Config.server.port === 443) ? 'https' : 'http';
return protocol + '://' + Config.server.host + ':' + Config.server.port + return protocol + '://' + Config.server.host + ':' + Config.server.port +
'/avatars/' + encodeURIComponent(avatar).replace(/\%3F/g, '?'); '/avatars/' + encodeURIComponent(avatar).replace(/%3F/g, '?');
} }
return Dex.resourcePrefix + 'sprites/trainers/' + Dex.sanitizeName(avatar || 'unknown') + '.png'; return Dex.resourcePrefix + 'sprites/trainers/' + Dex.sanitizeName(avatar || 'unknown') + '.png';
} }
@ -318,14 +317,14 @@ export const Dex = new class implements ModdedDex {
} }
prefs(prop: string) { prefs(prop: string) {
// @ts-ignore // @ts-expect-error this is what I get for calling it Storage...
return window.Storage?.prefs?.(prop); return window.Storage?.prefs?.(prop);
} }
getShortName(name: string) { getShortName(name: string) {
let shortName = name.replace(/[^A-Za-z0-9]+$/, ''); let shortName = name.replace(/[^A-Za-z0-9]+$/, '');
if (shortName.indexOf('(') >= 0) { if (shortName.includes('(')) {
shortName += name.slice(shortName.length).replace(/[^\(\)]+/g, '').replace(/\(\)/g, ''); shortName += name.slice(shortName.length).replace(/[^()]+/g, '').replace(/\(\)/g, '');
} }
return shortName; return shortName;
} }
@ -379,7 +378,7 @@ export const Dex = new class implements ModdedDex {
}; };
} }
if (!data) data = {exists: false}; if (!data) data = { exists: false };
let move = new Move(id, name, data); let move = new Move(id, name, data);
window.BattleMovedex[id] = move; window.BattleMovedex[id] = move;
return move; return move;
@ -407,7 +406,7 @@ export const Dex = new class implements ModdedDex {
if (!window.BattleItems) window.BattleItems = {}; if (!window.BattleItems) window.BattleItems = {};
let data = window.BattleItems[id]; let data = window.BattleItems[id];
if (data && typeof data.exists === 'boolean') return data; if (data && typeof data.exists === 'boolean') return data;
if (!data) data = {exists: false}; if (!data) data = { exists: false };
let item = new Item(id, name, data); let item = new Item(id, name, data);
window.BattleItems[id] = item; window.BattleItems[id] = item;
return item; return item;
@ -429,7 +428,7 @@ export const Dex = new class implements ModdedDex {
if (!window.BattleAbilities) window.BattleAbilities = {}; if (!window.BattleAbilities) window.BattleAbilities = {};
let data = window.BattleAbilities[id]; let data = window.BattleAbilities[id];
if (data && typeof data.exists === 'boolean') return data; if (data && typeof data.exists === 'boolean') return data;
if (!data) data = {exists: false}; if (!data) data = { exists: false };
let ability = new Ability(id, name, data); let ability = new Ability(id, name, data);
window.BattleAbilities[id] = ability; window.BattleAbilities[id] = ability;
return ability; return ability;
@ -465,8 +464,8 @@ export const Dex = new class implements ModdedDex {
if (data && typeof data.exists === 'boolean') { if (data && typeof data.exists === 'boolean') {
species = data; species = data;
} else { } else {
if (!data) data = {exists: false}; if (!data) data = { exists: false };
if (!data.tier && id.slice(-5) === 'totem') { if (!data.tier && id.endsWith('totem')) {
data.tier = this.species.get(id.slice(0, -5)).tier; data.tier = this.species.get(id.slice(0, -5)).tier;
} }
if (!data.tier && data.baseSpecies && toID(data.baseSpecies) !== id) { if (!data.tier && data.baseSpecies && toID(data.baseSpecies) !== id) {
@ -503,7 +502,7 @@ export const Dex = new class implements ModdedDex {
if (!type || typeof type === 'string') { if (!type || typeof type === 'string') {
const id = toID(type) as string; const id = toID(type) as string;
const name = id.substr(0, 1).toUpperCase() + id.substr(1); const name = id.substr(0, 1).toUpperCase() + id.substr(1);
type = (window.BattleTypeChart && window.BattleTypeChart[id]) || {}; type = window.BattleTypeChart?.[id] || {};
if (type.damageTaken) type.exists = true; if (type.damageTaken) type.exists = true;
if (!type.id) type.id = id; if (!type.id) type.id = id;
if (!type.name) type.name = name; if (!type.name) type.name = name;
@ -525,14 +524,13 @@ export const Dex = new class implements ModdedDex {
isName: (name: string | null): boolean => { isName: (name: string | null): boolean => {
const id = toID(name); const id = toID(name);
if (name !== id.substr(0, 1).toUpperCase() + id.substr(1)) return false; if (name !== id.substr(0, 1).toUpperCase() + id.substr(1)) return false;
return (window.BattleTypeChart || {}).hasOwnProperty(id); return window.BattleTypeChart?.hasOwnProperty(id);
}, },
}; };
hasAbility(species: Species, ability: string) { hasAbility(species: Species, ability: string) {
for (const i in species.abilities) { for (const i in species.abilities) {
// @ts-ignore if (ability === species.abilities[i as '0']) return true;
if (ability === species.abilities[i]) return true;
} }
return false; return false;
} }
@ -543,7 +541,7 @@ export const Dex = new class implements ModdedDex {
let path = $('script[src*="pokedex-mini.js"]').attr('src') || ''; let path = $('script[src*="pokedex-mini.js"]').attr('src') || '';
let qs = '?' + (path.split('?')[1] || ''); let qs = '?' + (path.split('?')[1] || '');
path = (path.match(/.+?(?=data\/pokedex-mini\.js)/) || [])[0] || ''; path = ((/.+?(?=data\/pokedex-mini\.js)/.exec(path)) || [])[0] || '';
let el = document.createElement('script'); let el = document.createElement('script');
el.src = path + 'data/pokedex-mini-bw.js' + qs; el.src = path + 'data/pokedex-mini-bw.js' + qs;
@ -557,7 +555,7 @@ export const Dex = new class implements ModdedDex {
noScale?: boolean, noScale?: boolean,
mod?: string, mod?: string,
dynamax?: boolean, dynamax?: boolean,
} = {gen: 6}) { } = { gen: 6 }) {
const mechanicsGen = options.gen || 6; const mechanicsGen = options.gen || 6;
let isDynamax = !!options.dynamax; let isDynamax = !!options.dynamax;
if (pokemon instanceof Pokemon) { if (pokemon instanceof Pokemon) {
@ -706,7 +704,7 @@ export const Dex = new class implements ModdedDex {
let allowAnim = !Dex.prefs('noanim') && !Dex.prefs('nogif'); let allowAnim = !Dex.prefs('noanim') && !Dex.prefs('nogif');
if (allowAnim && spriteData.gen >= 6) spriteData.pixelated = false; if (allowAnim && spriteData.gen >= 6) spriteData.pixelated = false;
if (allowAnim && animationData[facing] && spriteData.gen >= 5) { if (allowAnim && animationData[facing] && spriteData.gen >= 5) {
if (facing.slice(-1) === 'f') name += '-f'; if (facing.endsWith('f')) name += '-f';
dir = baseDir + 'ani' + dir; dir = baseDir + 'ani' + dir;
spriteData.w = animationData[facing].w; spriteData.w = animationData[facing].w;
@ -794,31 +792,32 @@ export const Dex = new class implements ModdedDex {
let id = toID(pokemon); let id = toID(pokemon);
if (!pokemon || typeof pokemon === 'string') pokemon = null; if (!pokemon || typeof pokemon === 'string') pokemon = null;
// @ts-ignore // @ts-expect-error safe, but too lazy to cast
if (pokemon?.speciesForme) id = toID(pokemon.speciesForme); if (pokemon?.speciesForme) id = toID(pokemon.speciesForme);
// @ts-ignore // @ts-expect-error safe, but too lazy to cast
if (pokemon?.species) id = toID(pokemon.species); if (pokemon?.species) id = toID(pokemon.species);
// @ts-ignore // @ts-expect-error safe, but too lazy to cast
if (pokemon?.volatiles?.formechange && !pokemon.volatiles.transform) { if (pokemon?.volatiles?.formechange && !pokemon.volatiles.transform) {
// @ts-ignore // @ts-expect-error safe, but too lazy to cast
id = toID(pokemon.volatiles.formechange[1]); id = toID(pokemon.volatiles.formechange[1]);
} }
let num = this.getPokemonIconNum(id, pokemon?.gender === 'F', facingLeft); let num = this.getPokemonIconNum(id, pokemon?.gender === 'F', facingLeft);
let top = Math.floor(num / 12) * 30; let top = Math.floor(num / 12) * 30;
let left = (num % 12) * 40; let left = (num % 12) * 40;
let fainted = ((pokemon as Pokemon | ServerPokemon)?.fainted ? `;opacity:.3;filter:grayscale(100%) brightness(.5)` : ``); let fainted = ((pokemon as Pokemon | ServerPokemon)?.fainted ?
`;opacity:.3;filter:grayscale(100%) brightness(.5)` : ``);
return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-sheet.png?v18) no-repeat scroll -${left}px -${top}px${fainted}`; return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-sheet.png?v18) no-repeat scroll -${left}px -${top}px${fainted}`;
} }
getTeambuilderSpriteData(pokemon: any, gen: number = 0): TeambuilderSpriteData { getTeambuilderSpriteData(pokemon: any, gen = 0): TeambuilderSpriteData {
let id = toID(pokemon.species); let id = toID(pokemon.species);
let spriteid = pokemon.spriteid; let spriteid = pokemon.spriteid;
let species = Dex.species.get(pokemon.species); let species = Dex.species.get(pokemon.species);
if (pokemon.species && !spriteid) { if (pokemon.species && !spriteid) {
spriteid = species.spriteid || toID(pokemon.species); spriteid = species.spriteid || toID(pokemon.species);
} }
if (species.exists === false) return { spriteDir: 'sprites/gen5', spriteid: '0', x: 10, y: 5 }; if (!species.exists) return { spriteDir: 'sprites/gen5', spriteid: '0', x: 10, y: 5 };
if (window.Config?.server?.afd || Dex.prefs('afd')) { if (window.Config?.server?.afd || Dex.prefs('afd')) {
return { return {
spriteid, spriteid,
@ -867,11 +866,11 @@ export const Dex = new class implements ModdedDex {
return spriteData; return spriteData;
} }
getTeambuilderSprite(pokemon: any, gen: number = 0) { getTeambuilderSprite(pokemon: any, gen = 0) {
if (!pokemon) return ''; if (!pokemon) return '';
const data = this.getTeambuilderSpriteData(pokemon, gen); const data = this.getTeambuilderSpriteData(pokemon, gen);
const shiny = (data.shiny ? '-shiny' : ''); const shiny = (data.shiny ? '-shiny' : '');
return 'background-image:url(' + Dex.resourcePrefix + data.spriteDir + shiny + '/' + data.spriteid + '.png);background-position:' + data.x + 'px ' + data.y + 'px;background-repeat:no-repeat'; return `background-image:url(${Dex.resourcePrefix}${data.spriteDir}${shiny}/${data.spriteid}.png);background-position:${data.x}px ${data.y}px;background-repeat:no-repeat`;
} }
getItemIcon(item: any) { getItemIcon(item: any) {
@ -881,7 +880,7 @@ export const Dex = new class implements ModdedDex {
let top = Math.floor(num / 16) * 24; let top = Math.floor(num / 16) * 24;
let left = (num % 16) * 24; let left = (num % 16) * 24;
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/itemicons-sheet.png?v1) no-repeat scroll -' + left + 'px -' + top + 'px'; return `background:transparent url(${Dex.resourcePrefix}sprites/itemicons-sheet.png?v1) no-repeat scroll -${left}px -${top}px`;
} }
getTypeIcon(type: string | null, b?: boolean) { // b is just for utilichart.js getTypeIcon(type: string | null, b?: boolean) { // b is just for utilichart.js
@ -911,7 +910,7 @@ export const Dex = new class implements ModdedDex {
if (this.pokeballs) return this.pokeballs; if (this.pokeballs) return this.pokeballs;
this.pokeballs = []; this.pokeballs = [];
if (!window.BattleItems) window.BattleItems = {}; if (!window.BattleItems) window.BattleItems = {};
for (const data of Object.values(window.BattleItems) as AnyObject[]) { for (const data of Object.values<AnyObject>(window.BattleItems)) {
if (!data.isPokeball) continue; if (!data.isPokeball) continue;
this.pokeballs.push(data.name); this.pokeballs.push(data.name);
} }
@ -923,11 +922,11 @@ export class ModdedDex {
readonly gen: number; readonly gen: number;
readonly modid: ID; readonly modid: ID;
readonly cache = { readonly cache = {
Moves: {} as any as {[k: string]: Move}, Moves: {} as { [k: string]: Move },
Items: {} as any as {[k: string]: Item}, Items: {} as { [k: string]: Item },
Abilities: {} as any as {[k: string]: Ability}, Abilities: {} as { [k: string]: Ability },
Species: {} as any as {[k: string]: Species}, Species: {} as { [k: string]: Species },
Types: {} as any as {[k: string]: Dex.Effect}, Types: {} as { [k: string]: Dex.Effect },
}; };
pokeballs: string[] | null = null; pokeballs: string[] | null = null;
constructor(modid: ID) { constructor(modid: ID) {
@ -945,7 +944,7 @@ export class ModdedDex {
} }
if (this.cache.Moves.hasOwnProperty(id)) return this.cache.Moves[id]; if (this.cache.Moves.hasOwnProperty(id)) return this.cache.Moves[id];
let data = {...Dex.moves.get(name)}; let data = { ...Dex.moves.get(name) };
for (let i = Dex.gen - 1; i >= this.gen; i--) { for (let i = Dex.gen - 1; i >= this.gen; i--) {
const table = window.BattleTeambuilderTable[`gen${i}`]; const table = window.BattleTeambuilderTable[`gen${i}`];
@ -978,7 +977,7 @@ export class ModdedDex {
} }
if (this.cache.Items.hasOwnProperty(id)) return this.cache.Items[id]; if (this.cache.Items.hasOwnProperty(id)) return this.cache.Items[id];
let data = {...Dex.items.get(name)}; let data = { ...Dex.items.get(name) };
for (let i = Dex.gen - 1; i >= this.gen; i--) { for (let i = Dex.gen - 1; i >= this.gen; i--) {
const table = window.BattleTeambuilderTable[`gen${i}`]; const table = window.BattleTeambuilderTable[`gen${i}`];
@ -1008,7 +1007,7 @@ export class ModdedDex {
} }
if (this.cache.Abilities.hasOwnProperty(id)) return this.cache.Abilities[id]; if (this.cache.Abilities.hasOwnProperty(id)) return this.cache.Abilities[id];
let data = {...Dex.abilities.get(name)}; let data = { ...Dex.abilities.get(name) };
for (let i = Dex.gen - 1; i >= this.gen; i--) { for (let i = Dex.gen - 1; i >= this.gen; i--) {
const table = window.BattleTeambuilderTable[`gen${i}`]; const table = window.BattleTeambuilderTable[`gen${i}`];
@ -1038,7 +1037,7 @@ export class ModdedDex {
} }
if (this.cache.Species.hasOwnProperty(id)) return this.cache.Species[id]; if (this.cache.Species.hasOwnProperty(id)) return this.cache.Species[id];
let data = {...Dex.species.get(name)}; let data = { ...Dex.species.get(name) };
for (let i = Dex.gen - 1; i >= this.gen; i--) { for (let i = Dex.gen - 1; i >= this.gen; i--) {
const table = window.BattleTeambuilderTable[`gen${i}`]; const table = window.BattleTeambuilderTable[`gen${i}`];
@ -1053,12 +1052,12 @@ export class ModdedDex {
} }
} }
if (this.gen < 3 || this.modid === 'gen7letsgo') { if (this.gen < 3 || this.modid === 'gen7letsgo') {
data.abilities = {0: "No Ability"}; data.abilities = { 0: "No Ability" };
} }
const table = window.BattleTeambuilderTable[this.modid]; const table = window.BattleTeambuilderTable[this.modid];
if (id in table.overrideTier) data.tier = table.overrideTier[id]; if (id in table.overrideTier) data.tier = table.overrideTier[id];
if (!data.tier && id.slice(-5) === 'totem') { if (!data.tier && id.endsWith('totem')) {
data.tier = this.species.get(id.slice(0, -5)).tier; data.tier = this.species.get(id.slice(0, -5)).tier;
} }
if (!data.tier && data.baseSpecies && toID(data.baseSpecies) !== id) { if (!data.tier && data.baseSpecies && toID(data.baseSpecies) !== id) {
@ -1074,22 +1073,22 @@ export class ModdedDex {
types = { types = {
get: (name: string): Dex.Effect => { get: (name: string): Dex.Effect => {
const id = toID(name) as ID; const id = toID(name);
name = id.substr(0, 1).toUpperCase() + id.substr(1); name = id.substr(0, 1).toUpperCase() + id.substr(1);
if (this.cache.Types.hasOwnProperty(id)) return this.cache.Types[id]; if (this.cache.Types.hasOwnProperty(id)) return this.cache.Types[id];
let data = {...Dex.types.get(name)}; let data = { ...Dex.types.get(name) };
for (let i = 7; i >= this.gen; i--) { for (let i = 7; i >= this.gen; i--) {
const table = window.BattleTeambuilderTable['gen' + i]; const table = window.BattleTeambuilderTable[`gen${i}`];
if (id in table.removeType) { if (id in table.removeType) {
data.exists = false; data.exists = false;
// don't bother correcting its attributes given it doesn't exist // don't bother correcting its attributes given it doesn't exist
break; break;
} }
if (id in table.overrideTypeChart) { if (id in table.overrideTypeChart) {
data = {...data, ...table.overrideTypeChart[id]}; data = { ...data, ...table.overrideTypeChart[id] };
} }
} }
@ -1102,7 +1101,7 @@ export class ModdedDex {
if (this.pokeballs) return this.pokeballs; if (this.pokeballs) return this.pokeballs;
this.pokeballs = []; this.pokeballs = [];
if (!window.BattleItems) window.BattleItems = {}; if (!window.BattleItems) window.BattleItems = {};
for (const data of Object.values(window.BattleItems) as AnyObject[]) { for (const data of Object.values<AnyObject>(window.BattleItems)) {
if (data.gen && data.gen > this.gen) continue; if (data.gen && data.gen > this.gen) continue;
if (!data.isPokeball) continue; if (!data.isPokeball) continue;
this.pokeballs.push(data.name); this.pokeballs.push(data.name);
@ -1148,9 +1147,9 @@ export const Teams = new class {
// moves // moves
j = buf.indexOf('|', i); j = buf.indexOf('|', i);
set.moves = buf.substring(i, j).split(',').map(function (moveid) { set.moves = buf.substring(i, j).split(',').map(
return Dex.moves.get(moveid).name; moveid => Dex.moves.get(moveid).name
}); );
i = j + 1; i = j + 1;
// nature // nature
@ -1174,7 +1173,7 @@ export const Teams = new class {
spe: Number(evs[5]) || 0, spe: Number(evs[5]) || 0,
}; };
} else if (evstring === '0') { } else if (evstring === '0') {
set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
} }
} }
i = j + 1; i = j + 1;
@ -1234,49 +1233,49 @@ export const Teams = new class {
export(team: Dex.PokemonSet[] | string, gen: number, hidestats = false) { export(team: Dex.PokemonSet[] | string, gen: number, hidestats = false) {
if (!team) return ''; if (!team) return '';
if (typeof team === 'string') { if (typeof team === 'string') {
if (team.indexOf('\n') >= 0) return team; if (team.includes('\n')) return team;
team = this.unpack(team); team = this.unpack(team);
} }
let text = ''; let text = '';
for (const curSet of team) { for (const curSet of team) {
if (curSet.name && curSet.name !== curSet.species) { if (curSet.name && curSet.name !== curSet.species) {
text += '' + curSet.name + ' (' + curSet.species + ')'; text += `${curSet.name} (${curSet.species})`;
} else { } else {
text += '' + curSet.species; text += `${curSet.species}`;
} }
if (curSet.gender === 'M') text += ' (M)'; if (curSet.gender === 'M') text += ' (M)';
if (curSet.gender === 'F') text += ' (F)'; if (curSet.gender === 'F') text += ' (F)';
if (curSet.item) { if (curSet.item) {
text += ' @ ' + curSet.item; text += ` @ ${curSet.item}`;
} }
text += " \n"; text += " \n";
if (curSet.ability) { if (curSet.ability) {
text += 'Ability: ' + curSet.ability + " \n"; text += `Ability: ${curSet.ability} \n`;
} }
if (curSet.level && curSet.level !== 100) { if (curSet.level && curSet.level !== 100) {
text += 'Level: ' + curSet.level + " \n"; text += `Level: ${curSet.level} \n`;
} }
if (curSet.shiny) { if (curSet.shiny) {
text += 'Shiny: Yes \n'; text += 'Shiny: Yes \n';
} }
if (typeof curSet.happiness === 'number' && curSet.happiness !== 255 && !isNaN(curSet.happiness)) { if (typeof curSet.happiness === 'number' && curSet.happiness !== 255 && !isNaN(curSet.happiness)) {
text += 'Happiness: ' + curSet.happiness + " \n"; text += `Happiness: ${curSet.happiness} \n`;
} }
if (curSet.pokeball) { if (curSet.pokeball) {
text += 'Pokeball: ' + curSet.pokeball + " \n"; text += `Pokeball: ${curSet.pokeball} \n`;
} }
if (curSet.hpType) { if (curSet.hpType) {
text += 'Hidden Power: ' + curSet.hpType + " \n"; text += `Hidden Power: ${curSet.hpType} \n`;
} }
if (typeof curSet.dynamaxLevel === 'number' && curSet.dynamaxLevel !== 10 && !isNaN(curSet.dynamaxLevel)) { if (typeof curSet.dynamaxLevel === 'number' && curSet.dynamaxLevel !== 10 && !isNaN(curSet.dynamaxLevel)) {
text += 'Dynamax Level: ' + curSet.dynamaxLevel + " \n"; text += `Dynamax Level: ${curSet.dynamaxLevel} \n`;
} }
if (curSet.gigantamax) { if (curSet.gigantamax) {
text += 'Gigantamax: Yes \n'; text += 'Gigantamax: Yes \n';
} }
if (gen === 9) { if (gen === 9) {
const species = Dex.species.get(curSet.species); const species = Dex.species.get(curSet.species);
text += 'Tera Type: ' + (species.forceTeraType || curSet.teraType || species.types[0]) + " \n"; text += `Tera Type: ${species.forceTeraType || curSet.teraType || species.types[0]} \n`;
} }
if (!hidestats) { if (!hidestats) {
let first = true; let first = true;
@ -1290,14 +1289,14 @@ export const Teams = new class {
} else { } else {
text += ' / '; text += ' / ';
} }
text += '' + curSet.evs[j] + ' ' + BattleStatNames[j]; text += `${curSet.evs[j]!} ${BattleStatNames[j]}`;
} }
} }
if (!first) { if (!first) {
text += " \n"; text += " \n";
} }
if (curSet.nature) { if (curSet.nature) {
text += '' + curSet.nature + ' Nature' + " \n"; text += `${curSet.nature} Nature \n`;
} }
first = true; first = true;
if (curSet.ivs) { if (curSet.ivs) {
@ -1338,7 +1337,7 @@ export const Teams = new class {
} else { } else {
text += ' / '; text += ' / ';
} }
text += '' + curSet.ivs[stat] + ' ' + BattleStatNames[stat]; text += `${curSet.ivs[stat]} ${BattleStatNames[stat]}`;
} }
} }
} }
@ -1348,11 +1347,11 @@ export const Teams = new class {
} }
if (curSet.moves) { if (curSet.moves) {
for (let move of curSet.moves) { for (let move of curSet.moves) {
if (move.substr(0, 13) === 'Hidden Power ') { if (move.startsWith('Hidden Power ')) {
move = move.substr(0, 13) + '[' + move.substr(13) + ']'; move = `${move.slice(0, 13)}[${move.slice(13)}]`;
} }
if (move) { if (move) {
text += '- ' + move + " \n"; text += `- ${move} \n`;
} }
} }
} }
@ -1364,6 +1363,6 @@ export const Teams = new class {
if (typeof require === 'function') { if (typeof require === 'function') {
// in Node // in Node
(global as any).Dex = Dex; global.Dex = Dex;
(global as any).toID = toID; global.toID = toID;
} }

View File

@ -13,10 +13,10 @@
* @license MIT * @license MIT
*/ */
import type {Battle} from './battle'; import type { Battle } from './battle';
import type {BattleScene} from './battle-animations'; import type { BattleScene } from './battle-animations';
import {Dex, Teams, toID, toRoomid, toUserid, type ID} from './battle-dex'; import { Dex, Teams, toID, toRoomid, toUserid, type ID } from './battle-dex';
import {BattleTextParser, type Args, type KWArgs} from './battle-text-parser'; import { BattleTextParser, type Args, type KWArgs } from './battle-text-parser';
// Caja // Caja
declare const html4: any; declare const html4: any;
@ -25,7 +25,7 @@ declare const html: any;
// defined in battle-log-misc // defined in battle-log-misc
declare function MD5(input: string): string; declare function MD5(input: string): string;
declare function formatText(input: string, isTrusted?: boolean): string; declare function formatText(input: string, isTrusted?: boolean): string;
export {MD5, formatText}; export { MD5, formatText };
export class BattleLog { export class BattleLog {
elem: HTMLDivElement; elem: HTMLDivElement;
@ -113,7 +113,7 @@ export class BattleLog {
if ( if (
battle.seeking === Infinity ? battle.seeking === Infinity ?
battle.currentStep < battle.stepQueue.length - 2000 : battle.currentStep < battle.stepQueue.length - 2000 :
battle.turn < battle.seeking! - 100 battle.turn < battle.seeking - 100
) { ) {
this.addSeekEarlierButton(); this.addSeekEarlierButton();
return; return;
@ -154,7 +154,7 @@ export class BattleLog {
const user = BattleTextParser.parseNameParts(args[1]); const user = BattleTextParser.parseNameParts(args[1]);
if (battle?.ignoreSpects && ' +'.includes(user.group)) return; if (battle?.ignoreSpects && ' +'.includes(user.group)) return;
const formattedUser = user.group + user.name; const formattedUser = user.group + user.name;
const isJoin = (args[0].charAt(0) === 'j'); const isJoin = (args[0].startsWith('j'));
if (!this.joinLeave) { if (!this.joinLeave) {
this.joinLeave = { this.joinLeave = {
joins: [], joins: [],
@ -271,9 +271,11 @@ export class BattleLog {
const exportedTeam = team.map(set => { const exportedTeam = team.map(set => {
let buf = Teams.export([set], battle.gen).replace(/\n/g, '<br />'); let buf = Teams.export([set], battle.gen).replace(/\n/g, '<br />');
if (set.name && set.name !== set.species) { if (set.name && set.name !== set.species) {
buf = buf.replace(set.name, BattleLog.sanitizeHTML(`<span class="picon" style="${Dex.getPokemonIcon(set.species)}"></span><br />${set.name}`)); buf = buf.replace(set.name, BattleLog.sanitizeHTML(
`<span class="picon" style="${Dex.getPokemonIcon(set.species)}"></span><br />${set.name}`));
} else { } else {
buf = buf.replace(set.species, `<span class="picon" style="${Dex.getPokemonIcon(set.species)}"></span><br />${set.species}`); buf = buf.replace(set.species,
`<span class="picon" style="${Dex.getPokemonIcon(set.species)}"></span><br />${set.species}`);
} }
if (set.item) { if (set.item) {
buf = buf.replace(set.item, `${set.item} <span class="itemicon" style="${Dex.getItemIcon(set.item)}"></span>`); buf = buf.replace(set.item, `${set.item} <span class="itemicon" style="${Dex.getItemIcon(set.item)}"></span>`);
@ -380,7 +382,7 @@ export class BattleLog {
const messages = message.split('\n').map(line => { const messages = message.split('\n').map(line => {
line = BattleLog.escapeHTML(line); line = BattleLog.escapeHTML(line);
line = line.replace(/\*\*(.*)\*\*/, '<strong>$1</strong>'); line = line.replace(/\*\*(.*)\*\*/, '<strong>$1</strong>');
line = line.replace(/\|\|([^\|]*)\|\|([^\|]*)\|\|/, '<abbr title="$1">$2</abbr>'); line = line.replace(/\|\|([^|]*)\|\|([^|]*)\|\|/, '<abbr title="$1">$2</abbr>');
if (line.startsWith(' ')) line = '<small>' + line.trim() + '</small>'; if (line.startsWith(' ')) line = '<small>' + line.trim() + '</small>';
return line; return line;
}); });
@ -550,7 +552,7 @@ export class BattleLog {
return str.replace(/&quot;/g, '"').replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&'); return str.replace(/&quot;/g, '"').replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
} }
static colorCache: {[userid: string]: string} = {}; static colorCache: { [userid: string]: string } = {};
/** @deprecated */ /** @deprecated */
static hashColor(name: ID) { static hashColor(name: ID) {
@ -569,12 +571,12 @@ export class BattleLog {
let S = parseInt(hash.substr(0, 4), 16) % 50 + 40; // 40 to 89 let S = parseInt(hash.substr(0, 4), 16) % 50 + 40; // 40 to 89
let L = Math.floor(parseInt(hash.substr(8, 4), 16) % 20 + 30); // 30 to 49 let L = Math.floor(parseInt(hash.substr(8, 4), 16) % 20 + 30); // 30 to 49
let {R, G, B} = this.HSLToRGB(H, S, L); let { R, G, B } = this.HSLToRGB(H, S, L);
let lum = R * R * R * 0.2126 + G * G * G * 0.7152 + B * B * B * 0.0722; // 0.013 (dark blue) to 0.737 (yellow) let lum = R * R * R * 0.2126 + G * G * G * 0.7152 + B * B * B * 0.0722; // 0.013 (dark blue) to 0.737 (yellow)
let HLmod = (lum - 0.2) * -150; // -80 (yellow) to 28 (dark blue) let HLmod = (lum - 0.2) * -150; // -80 (yellow) to 28 (dark blue)
if (HLmod > 18) HLmod = (HLmod - 18) * 2.5; if (HLmod > 18) HLmod = (HLmod - 18) * 2.5;
else if (HLmod < 0) HLmod = (HLmod - 0) / 3; else if (HLmod < 0) HLmod /= 3;
else HLmod = 0; else HLmod = 0;
// let mod = ';border-right: ' + Math.abs(HLmod) + 'px solid ' + (HLmod > 0 ? 'red' : '#0088FF'); // let mod = ';border-right: ' + Math.abs(HLmod) + 'px solid ' + (HLmod > 0 ? 'red' : '#0088FF');
let Hdist = Math.min(Math.abs(180 - H), Math.abs(240 - H)); let Hdist = Math.min(Math.abs(180 - H), Math.abs(240 - H));
@ -584,7 +586,7 @@ export class BattleLog {
L += HLmod; L += HLmod;
let {R: r, G: g, B: b} = this.HSLToRGB(H, S, L); let { R: r, G: g, B: b } = this.HSLToRGB(H, S, L);
const toHex = (x: number) => { const toHex = (x: number) => {
const hex = Math.round(x * 255).toString(16); const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex; return hex.length === 1 ? '0' + hex : hex;
@ -612,14 +614,15 @@ export class BattleLog {
let R = R1 + m; let R = R1 + m;
let G = G1 + m; let G = G1 + m;
let B = B1 + m; let B = B1 + m;
return {R, G, B}; return { R, G, B };
} }
static prefs(name: string) { static prefs(name: string) {
// @ts-ignore // @ts-expect-error optional, for old client
if (window.Storage?.prefs) return Storage.prefs(name); if (window.Storage?.prefs) return Storage.prefs(name);
// @ts-ignore // @ts-expect-error optional, for Preact client
if (window.PS) return PS.prefs[name]; if (window.PS) return PS.prefs[name];
// may be neither, for e.g. Replays
return undefined; return undefined;
} }
@ -641,7 +644,7 @@ export class BattleLog {
let cmd = ''; let cmd = '';
let target = ''; let target = '';
if (message.charAt(0) === '/') { if (message.startsWith('/')) {
if (message.charAt(1) === '/') { if (message.charAt(1) === '/') {
message = message.slice(1); message = message.slice(1);
} else { } else {
@ -753,11 +756,11 @@ export class BattleLog {
static interstice = (() => { static interstice = (() => {
const whitelist: string[] = Config.whitelist; const whitelist: string[] = Config.whitelist;
const patterns = whitelist.map(entry => new RegExp( const patterns = whitelist.map(entry => new RegExp(
`^(https?:)?//([A-Za-z0-9-]*\\.)?${entry.replace(/\./g, '\\.')}(/.*)?`, `^(https?:)?//([A-Za-z0-9-]*\\.)?${entry.replace(/\./g, '\\.')}(/.*)?`, 'i'
'i')); ));
return { return {
isWhitelisted(uri: string) { isWhitelisted(uri: string) {
if (uri[0] === '/' && uri[1] !== '/') { if (uri.startsWith('/') && uri[1] !== '/') {
// domain-relative URIs are safe // domain-relative URIs are safe
return true; return true;
} }
@ -904,7 +907,7 @@ export class BattleLog {
return { return {
tagName: 'iframe', tagName: 'iframe',
attribs: [ attribs: [
'src', `https://player.twitch.tv/?channel=${channelId}&parent=${location.hostname}&autoplay=false`, 'src', `https://player.twitch.tv/?channel=${channelId!}&parent=${location.hostname}&autoplay=false`,
'allowfullscreen', 'true', 'height', `${height}`, 'width', `${width}`, 'allowfullscreen', 'true', 'height', `${height}`, 'width', `${width}`,
], ],
}; };
@ -912,7 +915,7 @@ export class BattleLog {
// <username> is a custom element that handles namecolors // <username> is a custom element that handles namecolors
tagName = 'strong'; tagName = 'strong';
const color = this.usernameColor(toID(getAttrib('name'))); const color = this.usernameColor(toID(getAttrib('name')));
const style = getAttrib('style'); const style = getAttrib('style') || '';
setAttrib('style', `${style};color:${color}`); setAttrib('style', `${style};color:${color}`);
} else if (tagName === 'spotify') { } else if (tagName === 'spotify') {
// <iframe src="https://open.spotify.com/embed/track/6aSYnCIwcLpnDXngGKAEzZ" width="300" height="380" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe> // <iframe src="https://open.spotify.com/embed/track/6aSYnCIwcLpnDXngGKAEzZ" width="300" height="380" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
@ -921,7 +924,7 @@ export class BattleLog {
return { return {
tagName: 'iframe', tagName: 'iframe',
attribs: ['src', `https://open.spotify.com/embed/track/${songId}`, 'width', '300', 'height', '380', 'frameborder', '0', 'allowtransparency', 'true', 'allow', 'encrypted-media'], attribs: ['src', `https://open.spotify.com/embed/track/${songId!}`, 'width', '300', 'height', '380', 'frameborder', '0', 'allowtransparency', 'true', 'allow', 'encrypted-media'],
}; };
} else if (tagName === 'youtube') { } else if (tagName === 'youtube') {
// <iframe width="320" height="180" src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> // <iframe width="320" height="180" src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
@ -936,8 +939,8 @@ export class BattleLog {
if (Number(height) < 200) { if (Number(height) < 200) {
height = window.innerWidth >= 400 ? '225' : '200'; height = window.innerWidth >= 400 ? '225' : '200';
} }
const videoId = /(?:\?v=|\/embed\/)([A-Za-z0-9_\-]+)/.exec(src)?.[1]; const videoId = /(?:\?v=|\/embed\/)([A-Za-z0-9_-]+)/.exec(src)?.[1];
if (!videoId) return {tagName: 'img', attribs: ['alt', `invalid src for <youtube>`]}; if (!videoId) return { tagName: 'img', attribs: ['alt', `invalid src for <youtube>`] };
const time = /(?:\?|&)(?:t|start)=([0-9]+)/.exec(src)?.[1]; const time = /(?:\?|&)(?:t|start)=([0-9]+)/.exec(src)?.[1];
this.players.push(null); this.players.push(null);
@ -950,7 +953,7 @@ export class BattleLog {
'width', width, 'height', height, 'width', width, 'height', height,
'src', `https://www.youtube.com/embed/${videoId}?enablejsapi=1&playsinline=1${time ? `&start=${time}` : ''}`, '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', 'frameborder', '0', 'allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', 'allowfullscreen', 'allowfullscreen',
'time', (time || 0) + "", 'time', `${time || 0}`,
], ],
}; };
} else if (tagName === 'formatselect') { } else if (tagName === 'formatselect') {
@ -1026,7 +1029,7 @@ export class BattleLog {
setAttrib('rel', 'noopener'); setAttrib('rel', 'noopener');
} }
} }
return {tagName, attribs}; return { tagName, attribs };
}; };
} }
static localizeTime(full: string, date: string, time: string, timezone?: string) { static localizeTime(full: string, date: string, time: string, timezone?: string) {
@ -1081,7 +1084,8 @@ export class BattleLog {
// allows T, however it's more practical to also allow spaces. // allows T, however it's more practical to also allow spaces.
return sanitized.replace( return sanitized.replace(
/<time>\s*([+-]?\d{4,}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?)(Z|[+-]\d{2}:\d{2})?\s*<\/time>/ig, /<time>\s*([+-]?\d{4,}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?)(Z|[+-]\d{2}:\d{2})?\s*<\/time>/ig,
this.localizeTime); this.localizeTime
);
} }
static initYoutubePlayer(idx: number) { static initYoutubePlayer(idx: number) {
@ -1106,7 +1110,6 @@ export class BattleLog {
player.seekTo(time); player.seekTo(time);
} }
this.players[idx - 1] = player; this.players[idx - 1] = player;
}; };
// wait for html element to be in DOM // wait for html element to be in DOM
this.ensureYoutube().then(() => { this.ensureYoutube().then(() => {
@ -1164,7 +1167,7 @@ export class BattleLog {
// This allows pretty much anything about the replay viewer to be // This allows pretty much anything about the replay viewer to be
// updated as desired. // updated as desired.
static createReplayFile(room: {battle: Battle, id?: string, fragment?: string}) { static createReplayFile(room: { battle: Battle, id?: string, fragment?: string }) {
let battle = room.battle; let battle = room.battle;
let replayid = room.id; let replayid = room.id;
if (replayid) { if (replayid) {
@ -1208,10 +1211,12 @@ export class BattleLog {
return buf; return buf;
} }
static createReplayFileHref(room: {battle: Battle, id?: string, fragment?: string}) { static createReplayFileHref(room: { battle: Battle, id?: string, fragment?: string }) {
// unescape(encodeURIComponent()) is necessary because btoa doesn't support Unicode // unescape(encodeURIComponent()) is necessary because btoa doesn't support Unicode
const replayFile = 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'; 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)))); return 'data:text/plain;base64,' + encodeURIComponent(btoa(unescape(encodeURIComponent(replayFile))));
} }
} }

View File

@ -1,36 +1,36 @@
import type {Pokemon, Side} from './battle'; import type { Pokemon, Side } from './battle';
import type {ScenePos, PokemonSprite} from './battle-animations'; import type { ScenePos, PokemonSprite } from './battle-animations';
import type {BattleLog} from './battle-log'; import type { BattleLog } from './battle-log';
import type {ID} from './battle-dex'; import type { ID } from './battle-dex';
import type {Args, KWArgs} from './battle-text-parser'; import type { Args, KWArgs } from './battle-text-parser';
export class BattleSceneStub { export class BattleSceneStub {
animating: boolean = false; animating = false;
acceleration: number = NaN; acceleration = NaN;
gen: number = NaN; gen = NaN;
activeCount: number = NaN; activeCount = NaN;
numericId: number = NaN; numericId = NaN;
timeOffset: number = NaN; timeOffset = NaN;
interruptionCount: number = NaN; interruptionCount = NaN;
messagebarOpen: boolean = false; messagebarOpen = false;
log: BattleLog = {add: (args: Args, kwargs?: KWArgs) => {}} as any; log: BattleLog = { add: (args: Args, kwargs?: KWArgs) => {} } as any;
$frame?: JQuery; $frame?: JQuery;
abilityActivateAnim(pokemon: Pokemon, result: string): void { } abilityActivateAnim(pokemon: Pokemon, result: string): void { }
addPokemonSprite(pokemon: Pokemon): PokemonSprite { return null!; } addPokemonSprite(pokemon: Pokemon): PokemonSprite { return null!; }
addSideCondition(siden: number, id: ID, instant?: boolean | undefined): void { } addSideCondition(siden: number, id: ID, instant?: boolean): void { }
animationOff(): void { } animationOff(): void { }
animationOn(): void { } animationOn(): void { }
maybeCloseMessagebar(args: Args, kwArgs: KWArgs): boolean { return false; } maybeCloseMessagebar(args: Args, kwArgs: KWArgs): boolean { return false; }
closeMessagebar(): boolean { return false; } closeMessagebar(): boolean { return false; }
damageAnim(pokemon: Pokemon, damage: string | number): void { } damageAnim(pokemon: Pokemon, damage: string | number): void { }
destroy(): void { } destroy(): void { }
finishAnimations(): JQuery.Promise<JQuery<HTMLElement>, any, any> | undefined { return void(0); } finishAnimations(): JQuery.Promise<JQuery> | undefined { return undefined; }
healAnim(pokemon: Pokemon, damage: string | number): void { } healAnim(pokemon: Pokemon, damage: string | number): void { }
hideJoinButtons(): void { } hideJoinButtons(): void { }
incrementTurn(): void { } incrementTurn(): void { }
updateAcceleration(): void { } updateAcceleration(): void { }
message(message: string, hiddenMessage?: string | undefined): void { } message(message: string, hiddenMessage?: string): void { }
pause(): void { } pause(): void { }
setMute(muted: boolean): void { } setMute(muted: boolean): void { }
preemptCatchup(): void { } preemptCatchup(): void { }
@ -55,7 +55,7 @@ export class BattleSceneStub {
updateSidebar(side: Side): void { } updateSidebar(side: Side): void { }
updateSidebars(): void { } updateSidebars(): void { }
updateStatbars(): void { } updateStatbars(): void { }
updateWeather(instant?: boolean | undefined): void { } updateWeather(instant?: boolean): void { }
upkeepWeather(): void { } upkeepWeather(): void { }
wait(time: number): void { } wait(time: number): void { }
setFrameHTML(html: any): void { } setFrameHTML(html: any): void { }

View File

@ -8,10 +8,10 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {Dex, type ID} from "./battle-dex"; import { Dex, type ID } from "./battle-dex";
import type {DexSearch, SearchRow} from "./battle-dex-search"; import type { DexSearch, SearchRow } from "./battle-dex-search";
export class PSSearchResults extends preact.Component<{search: DexSearch}> { export class PSSearchResults extends preact.Component<{ search: DexSearch }> {
readonly URL_ROOT = `//${Config.routes.dex}/`; readonly URL_ROOT = `//${Config.routes.dex}/`;
renderPokemonSortRow() { renderPokemonSortRow() {
@ -57,7 +57,8 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
if (search.dex.gen < 2) bst -= stats['spd']; if (search.dex.gen < 2) bst -= stats['spd'];
if (errorMessage) { if (errorMessage) {
return <li class="result"><a href={`${this.URL_ROOT}pokemon/${id}`} data-target="push" data-entry={`pokemon|${pokemon.name}`}> return <li class="result">
<a href={`${this.URL_ROOT}pokemon/${id}`} data-target="push" data-entry={`pokemon|${pokemon.name}`}>
<span class="col numcol">{search.getTier(pokemon)}</span> <span class="col numcol">{search.getTier(pokemon)}</span>
<span class="col iconcol"> <span class="col iconcol">
@ -67,10 +68,12 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
<span class="col pokemonnamecol">{this.renderName(pokemon.name, matchStart, matchEnd, tagStart)}</span> <span class="col pokemonnamecol">{this.renderName(pokemon.name, matchStart, matchEnd, tagStart)}</span>
{errorMessage} {errorMessage}
</a></li>; </a>
</li>;
} }
return <li class="result"><a href={`${this.URL_ROOT}pokemon/${id}`} data-target="push" data-entry={`pokemon|${pokemon.name}`}> return <li class="result">
<a href={`${this.URL_ROOT}pokemon/${id}`} data-target="push" data-entry={`pokemon|${pokemon.name}`}>
<span class="col numcol">{search.getTier(pokemon)}</span> <span class="col numcol">{search.getTier(pokemon)}</span>
<span class="col iconcol"> <span class="col iconcol">
@ -85,17 +88,25 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
)} )}
</span> </span>
{search.dex.gen >= 3 && (pokemon.abilities['1'] ? {search.dex.gen >= 3 && (
pokemon.abilities['1'] ? (
<span class="col twoabilitycol">{pokemon.abilities['0']}<br />{pokemon.abilities['1']}</span> <span class="col twoabilitycol">{pokemon.abilities['0']}<br />{pokemon.abilities['1']}</span>
: ) : (
<span class="col abilitycol">{pokemon.abilities['0']}</span> <span class="col abilitycol">{pokemon.abilities['0']}</span>
)
)} )}
{search.dex.gen >= 5 && (pokemon.abilities['S'] ? {search.dex.gen >= 5 && (
<span class={`col twoabilitycol${pokemon.unreleasedHidden ? ' unreleasedhacol' : ''}`}>{pokemon.abilities['H'] || ''}<br />{pokemon.abilities['S']}</span> pokemon.abilities['S'] ? (
: pokemon.abilities['H'] ? <span class={`col twoabilitycol${pokemon.unreleasedHidden ? ' unreleasedhacol' : ''}`}>
<span class={`col abilitycol${pokemon.unreleasedHidden ? ' unreleasedhacol' : ''}`}>{pokemon.abilities['H']}</span> {pokemon.abilities['H'] || ''}<br />{pokemon.abilities['S']}
: </span>
) : pokemon.abilities['H'] ? (
<span class={`col abilitycol${pokemon.unreleasedHidden ? ' unreleasedhacol' : ''}`}>
{pokemon.abilities['H']}
</span>
) : (
<span class="col abilitycol"></span> <span class="col abilitycol"></span>
)
)} )}
<span class="col statcol"><em>HP</em><br />{stats.hp}</span> <span class="col statcol"><em>HP</em><br />{stats.hp}</span>
@ -106,7 +117,8 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
{search.dex.gen < 2 && <span class="col statcol"><em>Spc</em><br />{stats.spa}</span>} {search.dex.gen < 2 && <span class="col statcol"><em>Spc</em><br />{stats.spa}</span>}
<span class="col statcol"><em>Spe</em><br />{stats.spe}</span> <span class="col statcol"><em>Spe</em><br />{stats.spe}</span>
<span class="col bstcol"><em>BST<br />{bst}</em></span> <span class="col bstcol"><em>BST<br />{bst}</em></span>
</a></li>; </a>
</li>;
} }
renderName(name: string, matchStart: number, matchEnd: number, tagStart?: number) { renderName(name: string, matchStart: number, matchEnd: number, tagStart?: number) {
@ -166,13 +178,15 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
const ability = search.dex.abilities.get(id); const ability = search.dex.abilities.get(id);
if (!ability) return <li class="result">Unrecognized ability</li>; if (!ability) return <li class="result">Unrecognized ability</li>;
return <li class="result"><a href={`${this.URL_ROOT}abilitys/${id}`} data-target="push" data-entry={`ability|${ability.name}`}> return <li class="result">
<a href={`${this.URL_ROOT}abilitys/${id}`} data-target="push" data-entry={`ability|${ability.name}`}>
<span class="col namecol">{this.renderName(ability.name, matchStart, matchEnd)}</span> <span class="col namecol">{this.renderName(ability.name, matchStart, matchEnd)}</span>
{errorMessage} {errorMessage}
{!errorMessage && <span class="col abilitydesccol">{ability.shortDesc}</span>} {!errorMessage && <span class="col abilitydesccol">{ability.shortDesc}</span>}
</a></li>; </a>
</li>;
} }
renderMoveRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { renderMoveRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
@ -196,8 +210,14 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
<span class="col movenamecol">{this.renderName(move.name, matchStart, matchEnd, tagStart)}</span> <span class="col movenamecol">{this.renderName(move.name, matchStart, matchEnd, tagStart)}</span>
<span class="col typecol"> <span class="col typecol">
<img src={`${Dex.resourcePrefix}sprites/types/${move.type}.png`} alt={move.type} height="14" width="32" class="pixelated" /> <img
<img src={`${Dex.resourcePrefix}sprites/categories/${move.category}.png`} alt={move.category} height="14" width="32" class="pixelated" /> src={`${Dex.resourcePrefix}sprites/types/${move.type}.png`}
alt={move.type} height="14" width="32" class="pixelated"
/>
<img
src={`${Dex.resourcePrefix}sprites/categories/${move.category}.png`}
alt={move.category} height="14" width="32" class="pixelated"
/>
</span> </span>
<span class="col labelcol"> <span class="col labelcol">
@ -216,7 +236,6 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
} }
renderTypeRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { renderTypeRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const name = id.charAt(0).toUpperCase() + id.slice(1); const name = id.charAt(0).toUpperCase() + id.slice(1);
return <li class="result"><a href={`${this.URL_ROOT}types/${id}`} data-target="push" data-entry={`type|${name}`}> return <li class="result"><a href={`${this.URL_ROOT}types/${id}`} data-target="push" data-entry={`type|${name}`}>
@ -231,10 +250,10 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
} }
renderCategoryRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { renderCategoryRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const name = id.charAt(0).toUpperCase() + id.slice(1); const name = id.charAt(0).toUpperCase() + id.slice(1);
return <li class="result"><a href={`${this.URL_ROOT}categories/${id}`} data-target="push" data-entry={`category|${name}`}> return <li class="result">
<a href={`${this.URL_ROOT}categories/${id}`} data-target="push" data-entry={`category|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span> <span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col typecol"> <span class="col typecol">
@ -242,14 +261,13 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
</span> </span>
{errorMessage} {errorMessage}
</a></li>; </a>
</li>;
} }
renderArticleRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { renderArticleRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const isSearchType = (id === 'pokemon' || id === 'moves'); const isSearchType = (id === 'pokemon' || id === 'moves');
const name = (window.BattleArticleTitles && window.BattleArticleTitles[id]) || const name = window.BattleArticleTitles?.[id] || (id.charAt(0).toUpperCase() + id.substr(1));
(id.charAt(0).toUpperCase() + id.substr(1));
return <li class="result"><a href={`${this.URL_ROOT}articles/${id}`} data-target="push" data-entry={`article|${name}`}> return <li class="result"><a href={`${this.URL_ROOT}articles/${id}`} data-target="push" data-entry={`article|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span> <span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
@ -261,7 +279,6 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
} }
renderEggGroupRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { renderEggGroupRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
// very hardcode // very hardcode
let name: string | undefined; let name: string | undefined;
if (id === 'humanlike') name = 'Human-Like'; if (id === 'humanlike') name = 'Human-Like';
@ -274,19 +291,20 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
name = id.charAt(0).toUpperCase() + id.slice(1); name = id.charAt(0).toUpperCase() + id.slice(1);
} }
return <li class="result"><a href={`${this.URL_ROOT}egggroups/${id}`} data-target="push" data-entry={`egggroup|${name}`}> return <li class="result">
<a href={`${this.URL_ROOT}egggroups/${id}`} data-target="push" data-entry={`egggroup|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span> <span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col movedesccol">(egg group)</span> <span class="col movedesccol">(egg group)</span>
{errorMessage} {errorMessage}
</a></li>; </a>
</li>;
} }
renderTierRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) { renderTierRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
// very hardcode // very hardcode
const tierTable: {[id: string]: string} = { const tierTable: { [id: string]: string } = {
uber: "Uber", uber: "Uber",
caplc: "CAP LC", caplc: "CAP LC",
capnfe: "CAP NFE", capnfe: "CAP NFE",
@ -314,9 +332,9 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
let errorMessage: preact.ComponentChild = null; let errorMessage: preact.ComponentChild = null;
let label; let label;
if ((label = search.filterLabel(type))) { // tslint:disable-line if ((label = search.filterLabel(type))) {
errorMessage = <span class="col filtercol"><em>{label}</em></span>; errorMessage = <span class="col filtercol"><em>{label}</em></span>;
} else if ((label = search.illegalLabel(id as ID))) { // tslint:disable-line } else if ((label = search.illegalLabel(id as ID))) {
errorMessage = <span class="col illegalcol"><em>{label}</em></span>; errorMessage = <span class="col illegalcol"><em>{label}</em></span>;
} }
@ -326,7 +344,7 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
.replace(/&lt;em>/g, '<em>').replace(/&lt;\/em>/g, '</em>') .replace(/&lt;em>/g, '<em>').replace(/&lt;\/em>/g, '</em>')
.replace(/&lt;strong>/g, '<strong>').replace(/&lt;\/strong>/g, '</strong>'); .replace(/&lt;strong>/g, '<strong>').replace(/&lt;\/strong>/g, '</strong>');
return <li class="result"> return <li class="result">
<p dangerouslySetInnerHTML={{__html: sanitizedHTML}}></p> <p dangerouslySetInnerHTML={{ __html: sanitizedHTML }}></p>
</li>; </li>;
case 'header': case 'header':
return <li class="result"><h3>{id}</h3></li>; return <li class="result"><h3>{id}</h3></li>;
@ -335,23 +353,23 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
case 'sortmove': case 'sortmove':
return this.renderMoveSortRow(); return this.renderMoveSortRow();
case 'pokemon': case 'pokemon':
return this.renderPokemonRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderPokemonRow(id, matchStart, matchEnd, errorMessage);
case 'move': case 'move':
return this.renderMoveRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderMoveRow(id, matchStart, matchEnd, errorMessage);
case 'item': case 'item':
return this.renderItemRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderItemRow(id, matchStart, matchEnd, errorMessage);
case 'ability': case 'ability':
return this.renderAbilityRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderAbilityRow(id, matchStart, matchEnd, errorMessage);
case 'type': case 'type':
return this.renderTypeRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderTypeRow(id, matchStart, matchEnd, errorMessage);
case 'egggroup': case 'egggroup':
return this.renderEggGroupRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderEggGroupRow(id, matchStart, matchEnd, errorMessage);
case 'tier': case 'tier':
return this.renderTierRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderTierRow(id, matchStart, matchEnd, errorMessage);
case 'category': case 'category':
return this.renderCategoryRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderCategoryRow(id, matchStart, matchEnd, errorMessage);
case 'article': case 'article':
return this.renderArticleRow(id as ID, matchStart, matchEnd, errorMessage); return this.renderArticleRow(id, matchStart, matchEnd, errorMessage);
} }
return <li>Error: not found</li>; return <li>Error: not found</li>;
} }
@ -368,12 +386,11 @@ export class PSSearchResults extends preact.Component<{search: DexSearch}> {
)} )}
{!search.query && <small style="color: #888">(backspace = delete filter)</small>} {!search.query && <small style="color: #888">(backspace = delete filter)</small>}
</p>} </p>}
{search.results && {
// TODO: implement windowing // TODO: implement windowing
// for now, just show first twenty results // for now, just show first twenty results
search.results.slice(0, 20).map(result => search.results?.slice(0, 20).map(result => this.renderRow(result))
this.renderRow(result) }
)}
</ul>; </ul>;
} }
} }

View File

@ -1,4 +1,4 @@
import {PS} from "./client-main"; import { PS } from "./client-main";
export class BattleBGM { export class BattleBGM {
/** /**
@ -102,7 +102,7 @@ export class BattleBGM {
} }
export const BattleSound = new class { export const BattleSound = new class {
soundCache: {[url: string]: HTMLAudioElement | undefined} = {}; soundCache: { [url: string]: HTMLAudioElement | undefined } = {};
bgm: BattleBGM[] = []; bgm: BattleBGM[] = [];
@ -171,7 +171,7 @@ export const BattleSound = new class {
loudnessPercentToAmplitudePercent(loudnessPercent: number) { loudnessPercentToAmplitudePercent(loudnessPercent: number) {
// 10 dB is perceived as approximately twice as loud // 10 dB is perceived as approximately twice as loud
let decibels = 10 * Math.log(loudnessPercent / 100) / Math.log(2); let decibels = 10 * Math.log(loudnessPercent / 100) / Math.log(2);
return Math.pow(10, decibels / 20) * 100; return 10 ** (decibels / 20) * 100;
} }
setBgmVolume(bgmVolume: number) { setBgmVolume(bgmVolume: number) {
this.bgmVolume = this.loudnessPercentToAmplitudePercent(bgmVolume); this.bgmVolume = this.loudnessPercentToAmplitudePercent(bgmVolume);

View File

@ -8,10 +8,10 @@
* @license MIT * @license MIT
*/ */
import {toID, type ID} from "./battle-dex"; import { toID, type ID } from "./battle-dex";
export type Args = [string, ...string[]]; export type Args = [string, ...string[]];
export type KWArgs = {[kw: string]: string}; export type KWArgs = { [kw: string]: string };
export type SideID = 'p1' | 'p2' | 'p3' | 'p4'; export type SideID = 'p1' | 'p2' | 'p3' | 'p4';
export class BattleTextParser { export class BattleTextParser {
@ -64,22 +64,22 @@ export class BattleTextParser {
return line.slice(1).split('|') as [string, ...string[]]; return line.slice(1).split('|') as [string, ...string[]];
} }
static parseBattleLine(line: string): {args: Args, kwArgs: KWArgs} { static parseBattleLine(line: string): { args: Args, kwArgs: KWArgs } {
let args = this.parseLine(line, true); let args = this.parseLine(line, true);
if (args) return {args, kwArgs: {}}; if (args) return { args, kwArgs: {} };
args = line.slice(1).split('|') as [string, ...string[]]; args = line.slice(1).split('|') as [string, ...string[]];
const kwArgs: KWArgs = {}; const kwArgs: KWArgs = {};
while (args.length > 1) { while (args.length > 1) {
const lastArg = args[args.length - 1]; const lastArg = args[args.length - 1];
if (lastArg.charAt(0) !== '[') break; if (!lastArg.startsWith('[')) break;
const bracketPos = lastArg.indexOf(']'); const bracketPos = lastArg.indexOf(']');
if (bracketPos <= 0) break; if (bracketPos <= 0) break;
// default to '.' so it evaluates to boolean true // default to '.' so it evaluates to boolean true
kwArgs[lastArg.slice(1, bracketPos)] = lastArg.slice(bracketPos + 1).trim() || '.'; kwArgs[lastArg.slice(1, bracketPos)] = lastArg.slice(bracketPos + 1).trim() || '.';
args.pop(); args.pop();
} }
return BattleTextParser.upgradeArgs({args, kwArgs}); return BattleTextParser.upgradeArgs({ args, kwArgs });
} }
static parseNameParts(text: string) { static parseNameParts(text: string) {
@ -102,7 +102,7 @@ export class BattleTextParser {
status = status.slice(1); status = status.slice(1);
} }
} }
return {group, name, away, status}; return { group, name, away, status };
} }
/** /**
@ -110,19 +110,19 @@ export class BattleTextParser {
* them to modern versions. Used to keep battle.ts itself cleaner. Not * them to modern versions. Used to keep battle.ts itself cleaner. Not
* guaranteed to mutate or not mutate its inputs. * guaranteed to mutate or not mutate its inputs.
*/ */
static upgradeArgs({args, kwArgs}: {args: Args, kwArgs: KWArgs}): {args: Args, kwArgs: KWArgs} { static upgradeArgs({ args, kwArgs }: { args: Args, kwArgs: KWArgs }): { args: Args, kwArgs: KWArgs } {
switch (args[0]) { switch (args[0]) {
case '-activate': { case '-activate': {
if (kwArgs.item || kwArgs.move || kwArgs.number || kwArgs.ability) return {args, kwArgs}; if (kwArgs.item || kwArgs.move || kwArgs.number || kwArgs.ability) return { args, kwArgs };
let [, pokemon, effect, arg3, arg4] = args; let [, pokemon, effect, arg3, arg4] = args;
let target = kwArgs.of; let target = kwArgs.of;
const id = BattleTextParser.effectId(effect); const id = BattleTextParser.effectId(effect);
if (kwArgs.block) return {args: ['-fail', pokemon], kwArgs}; if (kwArgs.block) return { args: ['-fail', pokemon], kwArgs };
if (id === 'wonderguard') return {args: ['-immune', pokemon], kwArgs: {from: 'ability:Wonder Guard'}}; if (id === 'wonderguard') return { args: ['-immune', pokemon], kwArgs: { from: 'ability:Wonder Guard' } };
if (id === 'beatup' && kwArgs.of) return {args, kwArgs: {name: kwArgs.of}}; if (id === 'beatup' && kwArgs.of) return { args, kwArgs: { name: kwArgs.of } };
if ([ if ([
'ingrain', 'quickguard', 'wideguard', 'craftyshield', 'matblock', 'protect', 'mist', 'safeguard', 'ingrain', 'quickguard', 'wideguard', 'craftyshield', 'matblock', 'protect', 'mist', 'safeguard',
@ -131,22 +131,22 @@ export class BattleTextParser {
].includes(id)) { ].includes(id)) {
if (target) { if (target) {
kwArgs.of = pokemon; kwArgs.of = pokemon;
return {args: ['-block', target, effect, arg3], kwArgs}; return { args: ['-block', target, effect, arg3], kwArgs };
} }
return {args: ['-block', pokemon, effect, arg3], kwArgs}; return { args: ['-block', pokemon, effect, arg3], kwArgs };
} }
if (id === 'charge') { if (id === 'charge') {
return {args: ['-singlemove', pokemon, effect], kwArgs: {of: target}}; return { args: ['-singlemove', pokemon, effect], kwArgs: { of: target } };
} }
if ([ if ([
'bind', 'wrap', 'clamp', 'whirlpool', 'firespin', 'magmastorm', 'sandtomb', 'infestation', 'snaptrap', 'thundercage', 'trapped', 'bind', 'wrap', 'clamp', 'whirlpool', 'firespin', 'magmastorm', 'sandtomb', 'infestation', 'snaptrap', 'thundercage', 'trapped',
].includes(id)) { ].includes(id)) {
return {args: ['-start', pokemon, effect], kwArgs: {of: target}}; return { args: ['-start', pokemon, effect], kwArgs: { of: target } };
} }
if (id === 'fairylock') { if (id === 'fairylock') {
return {args: ['-fieldactivate', effect], kwArgs: {}}; return { args: ['-fieldactivate', effect], kwArgs: {} };
} }
if (id === 'symbiosis' || id === 'poltergeist') { if (id === 'symbiosis' || id === 'poltergeist') {
@ -168,7 +168,7 @@ export class BattleTextParser {
case '-fail': { case '-fail': {
if (kwArgs.from === 'ability: Flower Veil') { if (kwArgs.from === 'ability: Flower Veil') {
return {args: ['-block', kwArgs.of, 'ability: Flower Veil'], kwArgs: {of: args[1]}}; return { args: ['-block', kwArgs.of, 'ability: Flower Veil'], kwArgs: { of: args[1] } };
} }
break; break;
} }
@ -187,7 +187,7 @@ export class BattleTextParser {
let [, pokemon, effect, move] = args; let [, pokemon, effect, move] = args;
if (['ability: Damp', 'ability: Dazzling', 'ability: Queenly Majesty', 'ability: Armor Tail'].includes(effect)) { if (['ability: Damp', 'ability: Dazzling', 'ability: Queenly Majesty', 'ability: Armor Tail'].includes(effect)) {
args[0] = '-block'; args[0] = '-block';
return {args: ['-block', pokemon, effect, move, kwArgs.of], kwArgs: {}}; return { args: ['-block', pokemon, effect, move, kwArgs.of], kwArgs: {} };
} }
break; break;
} }
@ -211,15 +211,15 @@ export class BattleTextParser {
case '-nothing': case '-nothing':
// OLD: |-nothing // OLD: |-nothing
// NEW: |-activate||move:Splash // NEW: |-activate||move:Splash
return {args: ['-activate', '', 'move:Splash'], kwArgs}; return { args: ['-activate', '', 'move:Splash'], kwArgs };
} }
return {args, kwArgs}; return { args, kwArgs };
} }
extractMessage(buf: string) { extractMessage(buf: string) {
let out = ''; let out = '';
for (const line of buf.split('\n')) { for (const line of buf.split('\n')) {
const {args, kwArgs} = BattleTextParser.parseBattleLine(line); const { args, kwArgs } = BattleTextParser.parseBattleLine(line);
out += this.parseArgs(args, kwArgs) || ''; out += this.parseArgs(args, kwArgs) || '';
} }
return out; return out;
@ -229,13 +229,13 @@ export class BattleTextParser {
if (this.lowercaseRegExp === undefined) { if (this.lowercaseRegExp === undefined) {
const prefixes = ['pokemon', 'opposingPokemon', 'team', 'opposingTeam', 'party', 'opposingParty'].map(templateId => { const prefixes = ['pokemon', 'opposingPokemon', 'team', 'opposingTeam', 'party', 'opposingParty'].map(templateId => {
const template = BattleText.default[templateId]; const template = BattleText.default[templateId];
if (template.charAt(0) === template.charAt(0).toUpperCase()) return ''; if (template.startsWith(template.charAt(0).toUpperCase())) return '';
const bracketIndex = template.indexOf('['); const bracketIndex = template.indexOf('[');
if (bracketIndex >= 0) return template.slice(0, bracketIndex); if (bracketIndex >= 0) return template.slice(0, bracketIndex);
return template; return template;
}).filter(prefix => prefix); }).filter(prefix => prefix);
if (prefixes.length) { if (prefixes.length) {
let buf = `((?:^|\n)(?: | \\\(|\\\[)?)(` + let buf = `((?:^|\n)(?: | \\(|\\[)?)(` +
prefixes.map(BattleTextParser.escapeRegExp).join('|') + prefixes.map(BattleTextParser.escapeRegExp).join('|') +
`)`; `)`;
this.lowercaseRegExp = new RegExp(buf, 'g'); this.lowercaseRegExp = new RegExp(buf, 'g');
@ -302,7 +302,7 @@ export class BattleTextParser {
return ''; return '';
} }
team(side: string, isFar: boolean = false) { team(side: string, isFar = false) {
side = side.slice(0, 2); side = side.slice(0, 2);
if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) { if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) {
return !isFar ? BattleText.default.team : BattleText.default.opposingTeam; return !isFar ? BattleText.default.team : BattleText.default.opposingTeam;
@ -358,7 +358,7 @@ export class BattleTextParser {
let id = BattleTextParser.effectId(namespace); let id = BattleTextParser.effectId(namespace);
if (BattleText[id] && type in BattleText[id]) { if (BattleText[id] && type in BattleText[id]) {
if (BattleText[id][type].charAt(1) === '.') type = BattleText[id][type].slice(2) as ID; if (BattleText[id][type].charAt(1) === '.') type = BattleText[id][type].slice(2) as ID;
if (BattleText[id][type].charAt(0) === '#') id = BattleText[id][type].slice(1) as ID; if (BattleText[id][type].startsWith('#')) id = BattleText[id][type].slice(1) as ID;
if (!BattleText[id][type]) return ''; if (!BattleText[id][type]) return '';
return BattleText[id][type] + '\n'; return BattleText[id][type] + '\n';
} }
@ -380,7 +380,7 @@ export class BattleTextParser {
static stat(stat: string) { static stat(stat: string) {
const entry = BattleText[stat || "stats"]; const entry = BattleText[stat || "stats"];
if (!entry || !entry.statName) return `???stat:${stat}???`; if (!entry?.statName) return `???stat:${stat}???`;
return entry.statName; return entry.statName;
} }
@ -417,7 +417,7 @@ export class BattleTextParser {
return 'postMajor'; return 'postMajor';
} }
} }
return (cmd.charAt(0) === '-' ? 'postMajor' : ''); return (cmd.startsWith('-') ? 'postMajor' : '');
} }
sectionBreak(args: Args, kwArgs: KWArgs) { sectionBreak(args: Args, kwArgs: KWArgs) {
@ -601,7 +601,8 @@ export class BattleTextParser {
let id = BattleTextParser.effectId(effect); let id = BattleTextParser.effectId(effect);
if (id === 'typechange') { if (id === 'typechange') {
const template = this.template('typeChange', kwArgs.from); const template = this.template('typeChange', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TYPE]', arg3).replace('[SOURCE]', this.pokemon(kwArgs.of)); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TYPE]', arg3)
.replace('[SOURCE]', this.pokemon(kwArgs.of));
} }
if (id === 'typeadd') { if (id === 'typeadd') {
const template = this.template('typeAdd', kwArgs.from); const template = this.template('typeAdd', kwArgs.from);
@ -629,13 +630,14 @@ export class BattleTextParser {
if (kwArgs.damage) templateId = 'activate'; if (kwArgs.damage) templateId = 'activate';
if (kwArgs.block) templateId = 'block'; if (kwArgs.block) templateId = 'block';
if (kwArgs.upkeep) templateId = 'upkeep'; if (kwArgs.upkeep) templateId = 'upkeep';
if (id === 'mist' && this.gen <= 2) templateId = 'startGen' + this.gen; if (id === 'mist' && this.gen <= 2) templateId = `startGen${this.gen}`;
if (id === 'reflect' || id === 'lightscreen') templateId = 'startGen1'; if (id === 'reflect' || id === 'lightscreen') templateId = 'startGen1';
if (templateId === 'start' && kwArgs.from?.startsWith('item:')) { if (templateId === 'start' && kwArgs.from?.startsWith('item:')) {
templateId += 'FromItem'; templateId += 'FromItem';
} }
const template = this.template(templateId, kwArgs.from, 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)); 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));
} }
case '-end': { case '-end': {
@ -652,7 +654,8 @@ export class BattleTextParser {
template = this.template('endFromItem', effect); template = this.template('endFromItem', effect);
} }
if (!template) template = this.template(templateId, 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)).replace('[ITEM]', this.effect(kwArgs.from)); 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': { case '-ability': {
@ -671,7 +674,8 @@ export class BattleTextParser {
if (kwArgs.from) { if (kwArgs.from) {
line1 = this.maybeAbility(kwArgs.from, pokemon) + line1; line1 = this.maybeAbility(kwArgs.from, pokemon) + line1;
const template = this.template('changeAbility', kwArgs.from); const template = this.template('changeAbility', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ABILITY]', this.effect(ability)).replace('[SOURCE]', this.pokemon(kwArgs.of)); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ABILITY]', this.effect(ability))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
} }
const id = BattleTextParser.effectId(ability); const id = BattleTextParser.effectId(ability);
if (id === 'unnerve') { if (id === 'unnerve') {
@ -702,12 +706,14 @@ export class BattleTextParser {
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (['thief', 'covet', 'bestow', 'magician', 'pickpocket'].includes(id)) { if (['thief', 'covet', 'bestow', 'magician', 'pickpocket'].includes(id)) {
const template = this.template('takeItem', kwArgs.from); const template = this.template('takeItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[SOURCE]', this.pokemon(target || kwArgs.of)); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item))
.replace('[SOURCE]', this.pokemon(target || kwArgs.of));
} }
if (id === 'frisk') { if (id === 'frisk') {
const hasTarget = kwArgs.of && pokemon && kwArgs.of !== pokemon; const hasTarget = kwArgs.of && pokemon && kwArgs.of !== pokemon;
const template = this.template(hasTarget ? 'activate' : 'activateNoTarget', "Frisk"); const template = this.template(hasTarget ? 'activate' : 'activateNoTarget', "Frisk");
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(item)).replace('[TARGET]', this.pokemon(pokemon)); return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(item))
.replace('[TARGET]', this.pokemon(pokemon));
} }
if (kwArgs.from) { if (kwArgs.from) {
const template = this.template('addItem', kwArgs.from); const template = this.template('addItem', kwArgs.from);
@ -727,7 +733,8 @@ export class BattleTextParser {
const id = BattleTextParser.effectId(kwArgs.from); const id = BattleTextParser.effectId(kwArgs.from);
if (id === 'gem') { if (id === 'gem') {
const template = this.template('useGem', item); const template = this.template('useGem', item);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[MOVE]', kwArgs.move); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item))
.replace('[MOVE]', kwArgs.move);
} }
if (id === 'stealeat') { if (id === 'stealeat') {
const template = this.template('removeItem', "Bug Bite"); const template = this.template('removeItem', "Bug Bite");
@ -735,7 +742,8 @@ export class BattleTextParser {
} }
if (kwArgs.from) { if (kwArgs.from) {
const template = this.template('removeItem', kwArgs.from); const template = this.template('removeItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[SOURCE]', this.pokemon(kwArgs.of)); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
} }
if (kwArgs.weaken) { if (kwArgs.weaken) {
const template = this.template('activateWeaken'); const template = this.template('activateWeaken');
@ -792,7 +800,8 @@ export class BattleTextParser {
} }
let template = this.template('start', effect, 'NODEFAULT'); let template = this.template('start', effect, 'NODEFAULT');
if (!template) template = this.template('start').replace('[EFFECT]', this.effect(effect)); if (!template) template = this.template('start').replace('[EFFECT]', this.effect(effect));
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[TEAM]', this.team(pokemon.slice(0, 2))); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of))
.replace('[TEAM]', this.team(pokemon.slice(0, 2)));
} }
case '-sidestart': { case '-sidestart': {
@ -922,9 +931,11 @@ export class BattleTextParser {
line1 += this.ability(kwArgs.ability2, target); line1 += this.ability(kwArgs.ability2, target);
} }
if (kwArgs.move || kwArgs.number || kwArgs.item || kwArgs.name) { if (kwArgs.move || kwArgs.number || kwArgs.item || kwArgs.name) {
template = template.replace('[MOVE]', kwArgs.move).replace('[NUMBER]', kwArgs.number).replace('[ITEM]', kwArgs.item).replace('[NAME]', kwArgs.name); template = template.replace('[MOVE]', kwArgs.move).replace('[NUMBER]', kwArgs.number)
.replace('[ITEM]', kwArgs.item).replace('[NAME]', kwArgs.name);
} }
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target)).replace('[SOURCE]', this.pokemon(kwArgs.of)); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
} }
case '-prepare': { case '-prepare': {
@ -948,7 +959,8 @@ export class BattleTextParser {
} }
if (kwArgs.from.startsWith('item:')) { if (kwArgs.from.startsWith('item:')) {
template = this.template(kwArgs.of ? 'damageFromPokemon' : 'damageFromItem'); template = this.template(kwArgs.of ? 'damageFromPokemon' : 'damageFromItem');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from)).replace('[SOURCE]', this.pokemon(kwArgs.of)); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
} }
if (kwArgs.partiallytrapped || id === 'bind' || id === 'wrap') { if (kwArgs.partiallytrapped || id === 'bind' || id === 'wrap') {
template = this.template('damageFromPartialTrapping'); template = this.template('damageFromPartialTrapping');
@ -964,7 +976,8 @@ export class BattleTextParser {
let template = this.template('heal', kwArgs.from, 'NODEFAULT'); let template = this.template('heal', kwArgs.from, 'NODEFAULT');
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon); const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (template) { if (template) {
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[NICKNAME]', kwArgs.wisher); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of))
.replace('[NICKNAME]', kwArgs.wisher);
} }
if (kwArgs.from && !kwArgs.from.startsWith('ability:')) { if (kwArgs.from && !kwArgs.from.startsWith('ability:')) {
@ -989,7 +1002,8 @@ export class BattleTextParser {
templateId += (kwArgs.multiple ? 'MultipleFromZEffect' : 'FromZEffect'); templateId += (kwArgs.multiple ? 'MultipleFromZEffect' : 'FromZEffect');
} else if (amount && kwArgs.from?.startsWith('item:')) { } else if (amount && kwArgs.from?.startsWith('item:')) {
const template = this.template(templateId + 'FromItem', kwArgs.from); const template = this.template(templateId + 'FromItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat)).replace('[ITEM]', this.effect(kwArgs.from)); 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); const template = this.template(templateId, kwArgs.from);
return line1 + template.replace(/\[POKEMON\]/g, this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat)); return line1 + template.replace(/\[POKEMON\]/g, this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat));
@ -1055,9 +1069,10 @@ export class BattleTextParser {
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon); const line1 = this.maybeAbility(effect, kwArgs.of || pokemon);
let id = BattleTextParser.effectId(effect); let id = BattleTextParser.effectId(effect);
let templateId = 'block'; let templateId = 'block';
if (id === 'mist' && this.gen <= 2) templateId = 'blockGen' + this.gen; if (id === 'mist' && this.gen <= 2) templateId = `blockGen${this.gen}`;
const template = this.template(templateId, effect); const template = this.template(templateId, effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(attacker || kwArgs.of)).replace('[MOVE]', move); return line1 + template.replace('[POKEMON]', this.pokemon(pokemon))
.replace('[SOURCE]', this.pokemon(attacker || kwArgs.of)).replace('[MOVE]', move);
} }
case '-fail': { case '-fail': {
@ -1199,5 +1214,5 @@ declare const require: any;
declare const global: any; declare const global: any;
if (typeof require === 'function') { if (typeof require === 'function') {
// in Node // in Node
(global as any).BattleTextParser = BattleTextParser; global.BattleTextParser = BattleTextParser;
} }

View File

@ -8,12 +8,12 @@
* @license MIT * @license MIT
*/ */
import {Pokemon, type Battle, type ServerPokemon} from "./battle"; import { Pokemon, type Battle, type ServerPokemon } from "./battle";
import {Dex, ModdedDex, toID, type ID} from "./battle-dex"; import { Dex, type ModdedDex, toID, type ID } from "./battle-dex";
import type {BattleScene} from "./battle-animations"; import type { BattleScene } from "./battle-animations";
import {BattleLog} from "./battle-log"; import { BattleLog } from "./battle-log";
import {BattleNatures} from "./battle-dex-data"; import { BattleNatures } from "./battle-dex-data";
import {BattleTextParser} from "./battle-text-parser"; import { BattleTextParser } from "./battle-text-parser";
class ModifiableValue { class ModifiableValue {
value = 0; value = 0;
@ -197,7 +197,7 @@ export class BattleTooltips {
if (!BattleTooltips.isLocked) BattleTooltips.hideTooltip(); if (!BattleTooltips.isLocked) BattleTooltips.hideTooltip();
} }
listen(elem: HTMLElement | JQuery<HTMLElement>) { listen(elem: HTMLElement | JQuery) {
const $elem = $(elem); const $elem = $(elem);
$elem.on('mouseover', '.has-tooltip', this.showTooltipEvent); $elem.on('mouseover', '.has-tooltip', this.showTooltipEvent);
$elem.on('click', '.has-tooltip', this.clickTooltipEvent); $elem.on('click', '.has-tooltip', this.clickTooltipEvent);
@ -359,9 +359,9 @@ export class BattleTooltips {
// let side = this.battle.mySide.ally; // let side = this.battle.mySide.ally;
let activeIndex = parseInt(args[1], 10); let activeIndex = parseInt(args[1], 10);
let pokemon = null; let pokemon = null;
/*if (activeIndex < side.pokemon.length) { /* if (activeIndex < side.pokemon.length) {
pokemon = side.pokemon[activeIndex] || side.ally ? side.ally.pokemon[activeIndex] : null; pokemon = side.pokemon[activeIndex] || side.ally ? side.ally.pokemon[activeIndex] : null;
}*/ } */
let serverPokemon = this.battle.myAllyPokemon ? this.battle.myAllyPokemon[activeIndex] : null; let serverPokemon = this.battle.myAllyPokemon ? this.battle.myAllyPokemon[activeIndex] : null;
buf = this.showPokemonTooltip(pokemon, serverPokemon); buf = this.showPokemonTooltip(pokemon, serverPokemon);
break; break;
@ -373,7 +373,7 @@ export class BattleTooltips {
default: default:
// "throws" an error without crashing // "throws" an error without crashing
Promise.resolve(new Error(`unrecognized type`)); Promise.resolve(new Error(`unrecognized type`));
buf = `<p class="message-error" style="white-space: pre-wrap">${new Error(`unrecognized type`).stack}</p>`; buf = `<p class="message-error" style="white-space: pre-wrap">${new Error(`unrecognized type`).stack!}</p>`;
} }
this.placeTooltip(buf, elem, ownHeight, type); this.placeTooltip(buf, elem, ownHeight, type);
@ -412,7 +412,7 @@ export class BattleTooltips {
try { try {
const selection = window.getSelection()!; const selection = window.getSelection()!;
if (selection.type === 'Range') return; if (selection.type === 'Range') return;
} catch (err) {} } catch {}
BattleTooltips.hideTooltip(); BattleTooltips.hideTooltip();
}); });
} else { } else {
@ -422,7 +422,7 @@ export class BattleTooltips {
left: x, left: x,
top: y, top: y,
}); });
innerHTML = `<div class="tooltipinner"><div class="tooltip tooltip-${type}">${innerHTML}</div></div>`; innerHTML = `<div class="tooltipinner"><div class="tooltip tooltip-${type!}">${innerHTML}</div></div>`;
$wrapper.html(innerHTML).appendTo(document.body); $wrapper.html(innerHTML).appendTo(document.body);
BattleTooltips.elem = $wrapper.find('.tooltip')[0] as HTMLDivElement; BattleTooltips.elem = $wrapper.find('.tooltip')[0] as HTMLDivElement;
BattleTooltips.isLocked = false; BattleTooltips.isLocked = false;
@ -461,7 +461,7 @@ export class BattleTooltips {
BattleTooltips.hideTooltip(); BattleTooltips.hideTooltip();
} }
static zMoveEffects: {[zEffect: string]: string} = { static zMoveEffects: { [zEffect: string]: string } = {
'clearnegativeboost': "Restores negative stat stages to 0", 'clearnegativeboost': "Restores negative stat stages to 0",
'crit2': "Crit ratio +2", 'crit2': "Crit ratio +2",
'heal': "Restores HP 100%", 'heal': "Restores HP 100%",
@ -476,15 +476,14 @@ export class BattleTooltips {
} }
let boostText = ''; let boostText = '';
if (move.zMove!.boost) { if (move.zMove!.boost) {
let boosts = Object.keys(move.zMove!.boost) as Dex.StatName[]; boostText = Object.entries(move.zMove!.boost).map(([stat, boost]) =>
boostText = boosts.map(stat => `${BattleTextParser.stat(stat)} +${boost}`
BattleTextParser.stat(stat) + ' +' + move.zMove!.boost![stat]
).join(', '); ).join(', ');
} }
return boostText; return boostText;
} }
static zMoveTable: {[type in Dex.TypeName]: string} = { static zMoveTable: { [type in Dex.TypeName]: string } = {
Poison: "Acid Downpour", Poison: "Acid Downpour",
Fighting: "All-Out Pummeling", Fighting: "All-Out Pummeling",
Dark: "Black Hole Eclipse", Dark: "Black Hole Eclipse",
@ -507,7 +506,7 @@ export class BattleTooltips {
"???": "", "???": "",
}; };
static maxMoveTable: {[type in Dex.TypeName]: string} = { static maxMoveTable: { [type in Dex.TypeName]: string } = {
Poison: "Max Ooze", Poison: "Max Ooze",
Fighting: "Max Knuckle", Fighting: "Max Knuckle",
Dark: "Max Darkness", Dark: "Max Darkness",
@ -623,7 +622,7 @@ export class BattleTooltips {
}); });
} }
text += '<h2>' + move.name + '<br />'; text += `<h2>${move.name}<br />`;
text += Dex.getTypeIcon(moveType); text += Dex.getTypeIcon(moveType);
text += ` ${Dex.getCategoryIcon(category)}</h2>`; text += ` ${Dex.getCategoryIcon(category)}</h2>`;
@ -635,16 +634,16 @@ export class BattleTooltips {
// Otherwise, it is just shown as in singles. // Otherwise, it is just shown as in singles.
// The trick is that we need to calculate it first for each Pokémon to see if it changes. // The trick is that we need to calculate it first for each Pokémon to see if it changes.
let prevBasePower: string | null = null; let prevBasePower: string | null = null;
let basePower: string = ''; let basePower = '';
let difference = false; let difference = false;
let basePowers = []; let basePowers = [];
for (const active of foeActive) { for (const active of foeActive) {
if (!active) continue; if (!active) continue;
value = this.getMoveBasePower(move, moveType, value, active); value = this.getMoveBasePower(move, moveType, value, active);
basePower = '' + value; basePower = `${value}`;
if (prevBasePower === null) prevBasePower = basePower; if (prevBasePower === null) prevBasePower = basePower;
if (prevBasePower !== basePower) difference = true; if (prevBasePower !== basePower) difference = true;
basePowers.push('Base power vs ' + active.name + ': ' + basePower); basePowers.push(`Base power vs ${active.name}: ${basePower}`);
} }
if (difference) { if (difference) {
text += '<p>' + basePowers.join('<br />') + '</p>'; text += '<p>' + basePowers.join('<br />') + '</p>';
@ -655,7 +654,7 @@ export class BattleTooltips {
if (!showingMultipleBasePowers && category !== 'Status') { if (!showingMultipleBasePowers && category !== 'Status') {
let activeTarget = foeActive[0] || foeActive[1] || foeActive[2]; let activeTarget = foeActive[0] || foeActive[1] || foeActive[2];
value = this.getMoveBasePower(move, moveType, value, activeTarget); value = this.getMoveBasePower(move, moveType, value, activeTarget);
text += '<p>Base power: ' + value + '</p>'; text += `<p>Base power: ${value}</p>`;
} }
let accuracy = this.getMoveAccuracy(move, value); let accuracy = this.getMoveAccuracy(move, value);
@ -683,22 +682,22 @@ export class BattleTooltips {
calls = 'Swift'; calls = 'Swift';
} }
let calledMove = this.battle.dex.moves.get(calls); let calledMove = this.battle.dex.moves.get(calls);
text += 'Calls ' + Dex.getTypeIcon(this.getMoveType(calledMove, value)[0]) + ' ' + calledMove.name; text += `Calls ${Dex.getTypeIcon(this.getMoveType(calledMove, value)[0])} ${calledMove.name}`;
} }
text += '<p>Accuracy: ' + accuracy + '</p>'; text += `<p>Accuracy: ${accuracy}</p>`;
if (zEffect) text += '<p>Z-Effect: ' + zEffect + '</p>'; if (zEffect) text += `<p>Z-Effect: ${zEffect}</p>`;
if (this.battle.hardcoreMode) { if (this.battle.hardcoreMode) {
text += '<p class="tooltip-section">' + move.shortDesc + '</p>'; text += `<p class="tooltip-section">${move.shortDesc}</p>`;
} else { } else {
text += '<p class="tooltip-section">'; text += '<p class="tooltip-section">';
if (move.priority > 1) { if (move.priority > 1) {
text += 'Nearly always moves first <em>(priority +' + move.priority + ')</em>.</p><p>'; text += `Nearly always moves first <em>(priority +${move.priority})</em>.</p><p>`;
} else if (move.priority <= -1) { } else if (move.priority <= -1) {
text += 'Nearly always moves last <em>(priority &minus;' + (-move.priority) + ')</em>.</p><p>'; text += `Nearly always moves last <em>(priority &minus;${-move.priority})</em>.</p><p>`;
} else if (move.priority === 1) { } else if (move.priority === 1) {
text += 'Usually moves first <em>(priority +' + move.priority + ')</em>.</p><p>'; text += `Usually moves first <em>(priority +${move.priority})</em>.</p><p>`;
} else { } else {
if (move.id === 'grassyglide' && this.battle.hasPseudoWeather('Grassy Terrain')) { if (move.id === 'grassyglide' && this.battle.hasPseudoWeather('Grassy Terrain')) {
text += 'Usually moves first <em>(priority +1)</em>.</p><p>'; text += 'Usually moves first <em>(priority +1)</em>.</p><p>';
@ -803,7 +802,7 @@ export class BattleTooltips {
let name = BattleLog.escapeHTML(pokemon.name); let name = BattleLog.escapeHTML(pokemon.name);
if (pokemon.speciesForme !== pokemon.name) { if (pokemon.speciesForme !== pokemon.name) {
name += ' <small>(' + BattleLog.escapeHTML(pokemon.speciesForme) + ')</small>'; name += ` <small>(${BattleLog.escapeHTML(pokemon.speciesForme)})</small>`;
} }
let levelBuf = (pokemon.level !== 100 ? ` <small>L${pokemon.level}</small>` : ``); let levelBuf = (pokemon.level !== 100 ? ` <small>L${pokemon.level}</small>` : ``);
@ -843,25 +842,31 @@ export class BattleTooltips {
text += '<p><small>HP:</small> (fainted)</p>'; text += '<p><small>HP:</small> (fainted)</p>';
} else if (this.battle.hardcoreMode) { } else if (this.battle.hardcoreMode) {
if (serverPokemon) { if (serverPokemon) {
text += '<p><small>HP:</small> ' + serverPokemon.hp + '/' + serverPokemon.maxhp + (pokemon.status ? ' <span class="status ' + pokemon.status + '">' + pokemon.status.toUpperCase() + '</span>' : '') + '</p>'; const status = pokemon.status ? ` <span class="status ${pokemon.status}">${pokemon.status.toUpperCase()}</span>` : '';
text += `<p><small>HP:</small> ${serverPokemon.hp}/${serverPokemon.maxhp}${status}</p>`;
} }
} else { } else {
let exacthp = ''; let exacthp = '';
if (serverPokemon) { if (serverPokemon) {
exacthp = ' (' + serverPokemon.hp + '/' + serverPokemon.maxhp + ')'; exacthp = ` (${serverPokemon.hp}/${serverPokemon.maxhp})`;
} else if (pokemon.maxhp === 48) { } else if (pokemon.maxhp === 48) {
exacthp = ' <small>(' + pokemon.hp + '/' + pokemon.maxhp + ' pixels)</small>'; exacthp = ` <small>(${pokemon.hp}/${pokemon.maxhp} pixels)</small>`;
} }
text += '<p><small>HP:</small> ' + Pokemon.getHPText(pokemon, this.battle.reportExactHP) + exacthp + (pokemon.status ? ' <span class="status ' + pokemon.status + '">' + pokemon.status.toUpperCase() + '</span>' : ''); const status = pokemon.status ? ` <span class="status ${pokemon.status}">${pokemon.status.toUpperCase()}</span>` : '';
text += `<p><small>HP:</small> ${Pokemon.getHPText(pokemon, this.battle.reportExactHP)}${exacthp}${status}`;
if (clientPokemon) { if (clientPokemon) {
if (pokemon.status === 'tox') { if (pokemon.status === 'tox') {
if (pokemon.ability === 'Poison Heal' || pokemon.ability === 'Magic Guard') { if (pokemon.ability === 'Poison Heal' || pokemon.ability === 'Magic Guard') {
text += ' <small>Would take if ability removed: ' + Math.floor(100 / 16 * Math.min(clientPokemon.statusData.toxicTurns + 1, 15)) + '%</small>'; text += ` <small>Would take if ability removed: ${Math.floor(
100 / 16 * Math.min(clientPokemon.statusData.toxicTurns + 1, 15)
)}%</small>`;
} else { } else {
text += ' Next damage: ' + Math.floor(100 / (clientPokemon.volatiles['dynamax'] ? 32 : 16) * Math.min(clientPokemon.statusData.toxicTurns + 1, 15)) + '%'; text += ` Next damage: ${Math.floor(
100 / (clientPokemon.volatiles['dynamax'] ? 32 : 16) * Math.min(clientPokemon.statusData.toxicTurns + 1, 15)
)}%`;
} }
} else if (pokemon.status === 'slp') { } else if (pokemon.status === 'slp') {
text += ' Turns asleep: ' + clientPokemon.statusData.sleepTurns; text += ` Turns asleep: ${clientPokemon.statusData.sleepTurns}`;
} }
} }
text += '</p>'; text += '</p>';
@ -940,7 +945,7 @@ export class BattleTooltips {
text += `${this.getPPUseText(row)}<br />`; text += `${this.getPPUseText(row)}<br />`;
} }
if (clientPokemon.moveTrack.filter(([moveName]) => { if (clientPokemon.moveTrack.filter(([moveName]) => {
if (moveName.charAt(0) === '*') return false; if (moveName.startsWith('*')) return false;
const move = this.battle.dex.moves.get(moveName); const move = this.battle.dex.moves.get(moveName);
return !move.isZ && !move.isMax && move.name !== 'Mimic'; return !move.isZ && !move.isMax && move.name !== 'Mimic';
}).length > 4) { }).length > 4) {
@ -995,7 +1000,7 @@ export class BattleTooltips {
} }
calculateModifiedStats(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon, statStagesOnly?: boolean) { calculateModifiedStats(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon, statStagesOnly?: boolean) {
let stats = {...serverPokemon.stats}; let stats = { ...serverPokemon.stats };
let pokemon = clientPokemon || serverPokemon; let pokemon = clientPokemon || serverPokemon;
const isPowerTrick = clientPokemon?.volatiles['powertrick']; const isPowerTrick = clientPokemon?.volatiles['powertrick'];
for (const statName of Dex.statNamesExceptHP) { for (const statName of Dex.statNamesExceptHP) {
@ -1049,7 +1054,9 @@ export class BattleTooltips {
} }
let item = toID(serverPokemon.item); let item = toID(serverPokemon.item);
let speedHalvingEVItems = ['machobrace', 'poweranklet', 'powerband', 'powerbelt', 'powerbracer', 'powerlens', 'powerweight']; let speedHalvingEVItems = [
'machobrace', 'poweranklet', 'powerband', 'powerbelt', 'powerbracer', 'powerlens', 'powerweight',
];
if ( if (
(ability === 'klutz' && !speedHalvingEVItems.includes(item)) || (ability === 'klutz' && !speedHalvingEVItems.includes(item)) ||
this.battle.hasPseudoWeather('Magic Room') || this.battle.hasPseudoWeather('Magic Room') ||
@ -1362,7 +1369,7 @@ export class BattleTooltips {
chainedSpeedModifier *= modifier; chainedSpeedModifier *= modifier;
} }
// Chained modifiers round down on 0.5 // Chained modifiers round down on 0.5
stats.spe = stats.spe * chainedSpeedModifier; stats.spe *= chainedSpeedModifier;
stats.spe = stats.spe % 1 > 0.5 ? Math.ceil(stats.spe) : Math.floor(stats.spe); stats.spe = stats.spe % 1 > 0.5 ? Math.ceil(stats.spe) : Math.floor(stats.spe);
if (pokemon.status === 'par' && ability !== 'quickfeet') { if (pokemon.status === 'par' && ability !== 'quickfeet') {
@ -1381,7 +1388,7 @@ export class BattleTooltips {
if (!serverPokemon || isTransformed) { if (!serverPokemon || isTransformed) {
if (!clientPokemon) throw new Error('Must pass either clientPokemon or serverPokemon'); if (!clientPokemon) throw new Error('Must pass either clientPokemon or serverPokemon');
let [min, max] = this.getSpeedRange(clientPokemon); let [min, max] = this.getSpeedRange(clientPokemon);
return '<p><small>Spe</small> ' + min + ' to ' + max + ' <small>(before items/abilities/modifiers)</small></p>'; return `<p><small>Spe</small> ${min} to ${max} <small>(before items/abilities/modifiers)</small></p>`;
} }
const stats = serverPokemon.stats; const stats = serverPokemon.stats;
const modifiedStats = this.calculateModifiedStats(clientPokemon, serverPokemon); const modifiedStats = this.calculateModifiedStats(clientPokemon, serverPokemon);
@ -1394,8 +1401,8 @@ export class BattleTooltips {
if (this.battle.gen === 1 && statName === 'spd') continue; if (this.battle.gen === 1 && statName === 'spd') continue;
let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName; let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName;
buf += statName === 'atk' ? '<small>' : '<small> / '; buf += statName === 'atk' ? '<small>' : '<small> / ';
buf += '' + BattleText[statLabel].statShortName + '&nbsp;</small>'; buf += `${BattleText[statLabel].statShortName}&nbsp;</small>`;
buf += '' + stats[statName]; buf += `${stats[statName]}`;
if (modifiedStats[statName] !== stats[statName]) hasModifiedStat = true; if (modifiedStats[statName] !== stats[statName]) hasModifiedStat = true;
} }
buf += '</p>'; buf += '</p>';
@ -1410,13 +1417,13 @@ export class BattleTooltips {
if (this.battle.gen === 1 && statName === 'spd') continue; if (this.battle.gen === 1 && statName === 'spd') continue;
let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName; let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName;
buf += statName === 'atk' ? '<small>' : '<small> / '; buf += statName === 'atk' ? '<small>' : '<small> / ';
buf += '' + BattleText[statLabel].statShortName + '&nbsp;</small>'; buf += `${BattleText[statLabel].statShortName}&nbsp;</small>`;
if (modifiedStats[statName] === stats[statName]) { if (modifiedStats[statName] === stats[statName]) {
buf += '' + modifiedStats[statName]; buf += `${modifiedStats[statName]}`;
} else if (modifiedStats[statName] < stats[statName]) { } else if (modifiedStats[statName] < stats[statName]) {
buf += '<strong class="stat-lowered">' + modifiedStats[statName] + '</strong>'; buf += `<strong class="stat-lowered">${modifiedStats[statName]}</strong>`;
} else { } else {
buf += '<strong class="stat-boosted">' + modifiedStats[statName] + '</strong>'; buf += `<strong class="stat-boosted">${modifiedStats[statName]}</strong>`;
} }
} }
buf += '</p>'; buf += '</p>';
@ -1427,7 +1434,7 @@ export class BattleTooltips {
let [moveName, ppUsed] = moveTrackRow; let [moveName, ppUsed] = moveTrackRow;
let move; let move;
let maxpp; let maxpp;
if (moveName.charAt(0) === '*') { if (moveName.startsWith('*')) {
// Transformed move // Transformed move
move = this.battle.dex.moves.get(moveName.substr(1)); move = this.battle.dex.moves.get(moveName.substr(1));
maxpp = 5; maxpp = 5;
@ -1436,11 +1443,11 @@ export class BattleTooltips {
maxpp = (move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5); maxpp = (move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5);
if (this.battle.gen < 3) maxpp = Math.min(61, maxpp); if (this.battle.gen < 3) maxpp = Math.min(61, maxpp);
} }
const bullet = moveName.charAt(0) === '*' || move.isZ ? '<span style="color:#888">&#8226;</span>' : '&#8226;'; const bullet = moveName.startsWith('*') || move.isZ ? '<span style="color:#888">&#8226;</span>' : '&#8226;';
if (ppUsed === Infinity) { if (ppUsed === Infinity) {
return `${bullet} ${move.name} <small>(0/${maxpp})</small>`; return `${bullet} ${move.name} <small>(0/${maxpp})</small>`;
} }
if (ppUsed || moveName.charAt(0) === '*') { if (ppUsed || moveName.startsWith('*')) {
return `${bullet} ${move.name} <small>(${maxpp - ppUsed}/${maxpp})</small>`; return `${bullet} ${move.name} <small>(${maxpp - ppUsed}/${maxpp})</small>`;
} }
return `${bullet} ${move.name} ${showKnown ? ' <small>(revealed)</small>' : ''}`; return `${bullet} ${move.name} ${showKnown ? ' <small>(revealed)</small>' : ''}`;
@ -1448,7 +1455,7 @@ export class BattleTooltips {
ppUsed(move: Dex.Move, pokemon: Pokemon) { ppUsed(move: Dex.Move, pokemon: Pokemon) {
for (let [moveName, ppUsed] of pokemon.moveTrack) { for (let [moveName, ppUsed] of pokemon.moveTrack) {
if (moveName.charAt(0) === '*') moveName = moveName.substr(1); if (moveName.startsWith('*')) moveName = moveName.substr(1);
if (move.name === moveName) return ppUsed; if (move.name === moveName) return ppUsed;
} }
return 0; return 0;
@ -1472,7 +1479,7 @@ export class BattleTooltips {
if (rules['Frantic Fusions Mod']) { if (rules['Frantic Fusions Mod']) {
const fusionSpecies = this.battle.dex.species.get(pokemon.name); const fusionSpecies = this.battle.dex.species.get(pokemon.name);
if (fusionSpecies.exists && fusionSpecies.name !== species.name) { if (fusionSpecies.exists && fusionSpecies.name !== species.name) {
baseSpe = baseSpe + tr(fusionSpecies.baseStats.spe / 4); baseSpe += tr(fusionSpecies.baseStats.spe / 4);
if (baseSpe < 1) baseSpe = 1; if (baseSpe < 1) baseSpe = 1;
if (baseSpe > 255) baseSpe = 255; if (baseSpe > 255) baseSpe = 255;
} }
@ -1716,7 +1723,7 @@ export class BattleTooltips {
getMoveAccuracy(move: Dex.Move, value: ModifiableValue, target?: Pokemon) { getMoveAccuracy(move: Dex.Move, value: ModifiableValue, target?: Pokemon) {
value.reset(move.accuracy === true ? 0 : move.accuracy, true); value.reset(move.accuracy === true ? 0 : move.accuracy, true);
let pokemon = value.pokemon!; let pokemon = value.pokemon;
// Sure-hit accuracy // Sure-hit accuracy
if (move.id === 'toxic' && this.battle.gen >= 6 && this.pokemonHasType(pokemon, 'Poison')) { if (move.id === 'toxic' && this.battle.gen >= 6 && this.pokemonHasType(pokemon, 'Poison')) {
value.set(0, "Poison type"); value.set(0, "Poison type");
@ -1859,7 +1866,7 @@ export class BattleTooltips {
// Takes into account the target for some moves. // Takes into account the target for some moves.
// If it is unsure of the actual base power, it gives an estimate. // If it is unsure of the actual base power, it gives an estimate.
getMoveBasePower(move: Dex.Move, moveType: Dex.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 pokemon = value.pokemon;
const serverPokemon = value.serverPokemon; const serverPokemon = value.serverPokemon;
// apply modifiers for moves that depend on the actual stats // apply modifiers for moves that depend on the actual stats
@ -2059,9 +2066,9 @@ export class BattleTooltips {
// Base power based on times hit // Base power based on times hit
if (move.id === 'ragefist') { if (move.id === 'ragefist') {
value.set(Math.min(350, 50 + 50 * pokemon.timesAttacked), value.set(Math.min(350, 50 + 50 * pokemon.timesAttacked),
pokemon.timesAttacked > 0 pokemon.timesAttacked > 0 ?
? `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}` `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}` :
: undefined); undefined);
} }
if (!value.value) return value; if (!value.value) return value;
@ -2236,16 +2243,16 @@ export class BattleTooltips {
if (this.battle.tier.includes('Super Staff Bros')) { if (this.battle.tier.includes('Super Staff Bros')) {
if (move.id === 'bodycount') { if (move.id === 'bodycount') {
value.set(50 + 50 * pokemon.side.faintCounter, value.set(50 + 50 * pokemon.side.faintCounter,
pokemon.side.faintCounter > 0 pokemon.side.faintCounter > 0 ?
? `${pokemon.side.faintCounter} teammate${pokemon.side.faintCounter > 1 ? 's' : ''} KOed` `${pokemon.side.faintCounter} teammate${pokemon.side.faintCounter > 1 ? 's' : ''} KOed` :
: undefined); undefined);
} }
// Base power based on times hit // Base power based on times hit
if (move.id === 'vengefulmood') { if (move.id === 'vengefulmood') {
value.set(Math.min(140, 60 + 20 * pokemon.timesAttacked), value.set(Math.min(140, 60 + 20 * pokemon.timesAttacked),
pokemon.timesAttacked > 0 pokemon.timesAttacked > 0 ?
? `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}` `Hit ${pokemon.timesAttacked} time${pokemon.timesAttacked > 1 ? 's' : ''}` :
: undefined); undefined);
} }
if (move.id === 'alting' && pokemon.shiny) { if (move.id === 'alting' && pokemon.shiny) {
value.set(69, 'Shiny'); value.set(69, 'Shiny');
@ -2305,14 +2312,14 @@ export class BattleTooltips {
return value; return value;
} }
static incenseTypes: {[itemName: string]: Dex.TypeName} = { static incenseTypes: { [itemName: string]: Dex.TypeName } = {
'Odd Incense': 'Psychic', 'Odd Incense': 'Psychic',
'Rock Incense': 'Rock', 'Rock Incense': 'Rock',
'Rose Incense': 'Grass', 'Rose Incense': 'Grass',
'Sea Incense': 'Water', 'Sea Incense': 'Water',
'Wave Incense': 'Water', 'Wave Incense': 'Water',
}; };
static itemTypes: {[itemName: string]: Dex.TypeName} = { static itemTypes: { [itemName: string]: Dex.TypeName } = {
'Black Belt': 'Fighting', 'Black Belt': 'Fighting',
'Black Glasses': 'Dark', 'Black Glasses': 'Dark',
'Charcoal': 'Fire', 'Charcoal': 'Fire',
@ -2332,7 +2339,7 @@ export class BattleTooltips {
'Spell Tag': 'Ghost', 'Spell Tag': 'Ghost',
'Twisted Spoon': 'Psychic', 'Twisted Spoon': 'Psychic',
}; };
static orbUsers: {[speciesForme: string]: string[]} = { static orbUsers: { [speciesForme: string]: string[] } = {
'Latias': ['Soul Dew'], 'Latias': ['Soul Dew'],
'Latios': ['Soul Dew'], 'Latios': ['Soul Dew'],
'Dialga': ['Adamant Crystal', 'Adamant Orb'], 'Dialga': ['Adamant Crystal', 'Adamant Orb'],
@ -2340,7 +2347,7 @@ export class BattleTooltips {
'Giratina': ['Griseous Core', 'Griseous Orb'], 'Giratina': ['Griseous Core', 'Griseous Orb'],
'Venomicon': ['Vile Vial'], 'Venomicon': ['Vile Vial'],
}; };
static orbTypes: {[itemName: string]: Dex.TypeName[]} = { static orbTypes: { [itemName: string]: Dex.TypeName[] } = {
'Soul Dew': ['Psychic', 'Dragon'], 'Soul Dew': ['Psychic', 'Dragon'],
'Adamant Crystal': ['Steel', 'Dragon'], 'Adamant Crystal': ['Steel', 'Dragon'],
'Adamant Orb': ['Steel', 'Dragon'], 'Adamant Orb': ['Steel', 'Dragon'],
@ -2399,9 +2406,11 @@ export class BattleTooltips {
} }
if (speciesName === 'Ogerpon') { if (speciesName === 'Ogerpon') {
const speciesForme = value.pokemon.getSpeciesForme(); const speciesForme = value.pokemon.getSpeciesForme();
if ((speciesForme.startsWith('Ogerpon-Wellspring') && itemName === 'Wellspring Mask') || if (
(speciesForme.startsWith('Ogerpon-Wellspring') && itemName === 'Wellspring Mask') ||
(speciesForme.startsWith('Ogerpon-Hearthflame') && itemName === 'Hearthflame Mask') || (speciesForme.startsWith('Ogerpon-Hearthflame') && itemName === 'Hearthflame Mask') ||
(speciesForme.startsWith('Ogerpon-Cornerstone') && itemName === 'Cornerstone Mask')) { (speciesForme.startsWith('Ogerpon-Cornerstone') && itemName === 'Cornerstone Mask')
) {
value.itemModify(1.2); value.itemModify(1.2);
return value; return value;
} }
@ -2422,14 +2431,14 @@ export class BattleTooltips {
return value; return value;
} }
getPokemonTypes(pokemon: Pokemon | ServerPokemon, preterastallized = false): ReadonlyArray<Dex.TypeName> { getPokemonTypes(pokemon: Pokemon | ServerPokemon, preterastallized = false): readonly Dex.TypeName[] {
if (!(pokemon as Pokemon).getTypes) { if (!(pokemon as Pokemon).getTypes) {
return this.battle.dex.species.get(pokemon.speciesForme).types; return this.battle.dex.species.get(pokemon.speciesForme).types;
} }
return (pokemon as Pokemon).getTypeList(undefined, preterastallized); return (pokemon as Pokemon).getTypeList(undefined, preterastallized);
} }
pokemonHasType(pokemon: Pokemon | ServerPokemon, type: Dex.TypeName, types?: ReadonlyArray<Dex.TypeName>) { pokemonHasType(pokemon: Pokemon | ServerPokemon, type: Dex.TypeName, types?: readonly Dex.TypeName[]) {
if (!types) types = this.getPokemonTypes(pokemon); if (!types) types = this.getPokemonTypes(pokemon);
for (const curType of types) { for (const curType of types) {
if (curType === type) return true; if (curType === type) return true;
@ -2446,7 +2455,7 @@ export class BattleTooltips {
return ally.effectiveAbility(serverPokemon); return ally.effectiveAbility(serverPokemon);
} }
getPokemonAbilityData(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon | null | undefined) { getPokemonAbilityData(clientPokemon: Pokemon | null, serverPokemon: ServerPokemon | null | undefined) {
const abilityData: {ability: string, baseAbility: string, possibilities: string[]} = { const abilityData: { ability: string, baseAbility: string, possibilities: string[] } = {
ability: '', baseAbility: '', possibilities: [], ability: '', baseAbility: '', possibilities: [],
}; };
if (clientPokemon) { if (clientPokemon) {
@ -2459,16 +2468,14 @@ export class BattleTooltips {
const speciesForme = clientPokemon.getSpeciesForme() || serverPokemon?.speciesForme || ''; const speciesForme = clientPokemon.getSpeciesForme() || serverPokemon?.speciesForme || '';
const species = this.battle.dex.species.get(speciesForme); const species = this.battle.dex.species.get(speciesForme);
if (species.exists && species.abilities) { if (species.exists && species.abilities) {
abilityData.possibilities = [species.abilities['0']]; abilityData.possibilities = Object.values(species.abilities);
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']) { if (this.battle.rules['Frantic Fusions Mod']) {
const fusionSpecies = this.battle.dex.species.get(clientPokemon.name); const fusionSpecies = this.battle.dex.species.get(clientPokemon.name);
if (fusionSpecies.exists && fusionSpecies.name !== species.name) { if (fusionSpecies.exists && fusionSpecies.name !== species.name) {
abilityData.possibilities = Array.from( for (const newAbility of Object.values(fusionSpecies.abilities)) {
new Set(abilityData.possibilities.concat(Object.values(fusionSpecies.abilities))) if (abilityData.possibilities.includes(newAbility)) continue;
); abilityData.possibilities.push(newAbility);
}
} }
} }
} }
@ -2537,13 +2544,13 @@ class BattleStatGuesser {
guess(set: Dex.PokemonSet) { guess(set: Dex.PokemonSet) {
let role = this.guessRole(set); let role = this.guessRole(set);
let comboEVs = this.guessEVs(set, role); let comboEVs = this.guessEVs(set, role);
let evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; let evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
for (let stat in evs) { for (let stat in evs) {
evs[stat as Dex.StatName] = comboEVs[stat as Dex.StatName] || 0; evs[stat as Dex.StatName] = comboEVs[stat as Dex.StatName] || 0;
} }
let plusStat = comboEVs.plusStat || ''; let plusStat = comboEVs.plusStat || '';
let minusStat = comboEVs.minusStat || ''; let minusStat = comboEVs.minusStat || '';
return {role, evs, plusStat, minusStat, moveCount: this.moveCount, hasMove: this.hasMove}; return { role, evs, plusStat, minusStat, moveCount: this.moveCount, hasMove: this.hasMove };
} }
guessRole(set: Dex.PokemonSet) { guessRole(set: Dex.PokemonSet) {
if (!set) return '?'; if (!set) return '?';
@ -2569,7 +2576,7 @@ class BattleStatGuesser {
'specialBulk': 0, 'specialBulk': 0,
'physicalBulk': 0, 'physicalBulk': 0,
}; };
let hasMove: {[moveid: string]: 1} = {}; let hasMove: { [moveid: string]: 1 } = {};
let itemid = toID(set.item); let itemid = toID(set.item);
let item = this.dex.items.get(itemid); let item = this.dex.items.get(itemid);
let abilityid = toID(set.ability); let abilityid = toID(set.ability);
@ -2835,7 +2842,7 @@ class BattleStatGuesser {
diff -= change; diff -= change;
} }
if (diff <= 0) return evTotal; if (diff <= 0) return evTotal;
let evPriority = {def: 1, spd: 1, hp: 1, atk: 1, spa: 1, spe: 1}; let evPriority = { def: 1, spd: 1, hp: 1, atk: 1, spa: 1, spe: 1 };
let prioStat: Dex.StatName; let prioStat: Dex.StatName;
for (prioStat in evPriority) { for (prioStat in evPriority) {
if (prioStat === stat) continue; if (prioStat === stat) continue;
@ -2857,7 +2864,7 @@ class BattleStatGuesser {
} }
guessEVs( guessEVs(
set: Dex.PokemonSet, role: string set: Dex.PokemonSet, role: string
): Partial<Dex.StatsTable> & {plusStat?: Dex.StatName | '', minusStat?: Dex.StatName | ''} { ): Partial<Dex.StatsTable> & { plusStat?: Dex.StatName | '', minusStat?: Dex.StatName | '' } {
if (!set) return {}; if (!set) return {};
if (role === '?') return {}; if (role === '?') return {};
let species = this.dex.species.get(set.species || set.name!); let species = this.dex.species.get(set.species || set.name!);
@ -2866,13 +2873,13 @@ class BattleStatGuesser {
let hasMove = this.hasMove; let hasMove = this.hasMove;
let moveCount = this.moveCount; let moveCount = this.moveCount;
let evs: Dex.StatsTable & {plusStat?: Dex.StatName | '', minusStat?: Dex.StatName | ''} = { let evs: Dex.StatsTable & { plusStat?: Dex.StatName | '', minusStat?: Dex.StatName | '' } = {
hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0,
}; };
let plusStat: Dex.StatName | '' = ''; let plusStat: Dex.StatName | '' = '';
let minusStat: Dex.StatName | '' = ''; let minusStat: Dex.StatName | '' = '';
let statChart: {[role: string]: [Dex.StatName, Dex.StatName]} = { let statChart: { [role: string]: [Dex.StatName, Dex.StatName] } = {
'Bulky Band': ['atk', 'hp'], 'Bulky Band': ['atk', 'hp'],
'Fast Band': ['spe', 'atk'], 'Fast Band': ['spe', 'atk'],
'Bulky Specs': ['spa', 'hp'], 'Bulky Specs': ['spa', 'hp'],
@ -2906,7 +2913,7 @@ class BattleStatGuesser {
if (this.supportsAVs) { if (this.supportsAVs) {
// Let's Go, AVs enabled // Let's Go, AVs enabled
evs = {hp: 200, atk: 200, def: 200, spa: 200, spd: 200, spe: 200}; evs = { hp: 200, atk: 200, def: 200, spa: 200, spd: 200, spe: 200 };
if (!moveCount['PhysicalAttack']) evs.atk = 0; if (!moveCount['PhysicalAttack']) evs.atk = 0;
if (!moveCount['SpecialAttack']) evs.spa = 0; if (!moveCount['SpecialAttack']) evs.spa = 0;
if (hasMove['gyroball'] || hasMove['trickroom']) evs.spe = 0; if (hasMove['gyroball'] || hasMove['trickroom']) evs.spe = 0;
@ -2915,7 +2922,7 @@ class BattleStatGuesser {
// no change // no change
} else if (this.ignoreEVLimits) { } else if (this.ignoreEVLimits) {
// Gen 1-2, hackable EVs (like Hackmons) // Gen 1-2, hackable EVs (like Hackmons)
evs = {hp: 252, atk: 252, def: 252, spa: 252, spd: 252, spe: 252}; evs = { hp: 252, atk: 252, def: 252, spa: 252, spd: 252, spe: 252 };
if (!moveCount['PhysicalAttack']) evs.atk = 0; if (!moveCount['PhysicalAttack']) evs.atk = 0;
if (!moveCount['SpecialAttack'] && this.dex.gen > 1) evs.spa = 0; if (!moveCount['SpecialAttack'] && this.dex.gen > 1) evs.spa = 0;
if (hasMove['gyroball'] || hasMove['trickroom']) evs.spe = 0; if (hasMove['gyroball'] || hasMove['trickroom']) evs.spe = 0;
@ -2946,14 +2953,14 @@ class BattleStatGuesser {
let SRresistances = ['Ground', 'Steel', 'Fighting']; let SRresistances = ['Ground', 'Steel', 'Fighting'];
let SRweak = 0; let SRweak = 0;
if (set.ability !== 'Magic Guard' && set.ability !== 'Mountaineer') { if (set.ability !== 'Magic Guard' && set.ability !== 'Mountaineer') {
if (SRweaknesses.indexOf(species.types[0]) >= 0) { if (SRweaknesses.includes(species.types[0])) {
SRweak++; SRweak++;
} else if (SRresistances.indexOf(species.types[0]) >= 0) { } else if (SRresistances.includes(species.types[0])) {
SRweak--; SRweak--;
} }
if (SRweaknesses.indexOf(species.types[1]) >= 0) { if (SRweaknesses.includes(species.types[1])) {
SRweak++; SRweak++;
} else if (SRresistances.indexOf(species.types[1]) >= 0) { } else if (SRresistances.includes(species.types[1])) {
SRweak--; SRweak--;
} }
} }
@ -2965,10 +2972,10 @@ class BattleStatGuesser {
hpDivisibility = 4; hpDivisibility = 4;
} else if (set.item === 'Leftovers' || set.item === 'Black Sludge') { } else if (set.item === 'Leftovers' || set.item === 'Black Sludge') {
hpDivisibility = 0; hpDivisibility = 0;
} else if (hasMove['bellydrum'] && (set.item || '').slice(-5) === 'Berry') { } else if (hasMove['bellydrum'] && (set.item || '').endsWith('Berry')) {
hpDivisibility = 2; hpDivisibility = 2;
hpShouldBeDivisible = true; hpShouldBeDivisible = true;
} else if (hasMove['substitute'] && (set.item || '').slice(-5) === 'Berry') { } else if (hasMove['substitute'] && (set.item || '').endsWith('Berry')) {
hpDivisibility = 4; hpDivisibility = 4;
hpShouldBeDivisible = true; hpShouldBeDivisible = true;
} else if (SRweak >= 2 || hasMove['bellydrum']) { } else if (SRweak >= 2 || hasMove['bellydrum']) {
@ -3044,7 +3051,6 @@ class BattleStatGuesser {
if (ev) evs['spe'] = ev; if (ev) evs['spe'] = ev;
} }
} }
} }
if (hasMove['gyroball'] || hasMove['trickroom']) { if (hasMove['gyroball'] || hasMove['trickroom']) {
@ -3085,11 +3091,11 @@ class BattleStatGuesser {
let baseStat = species.baseStats[stat]; let baseStat = species.baseStats[stat];
let iv = (set.ivs && set.ivs[stat]); let iv = set.ivs?.[stat];
if (typeof iv !== 'number') iv = 31; if (typeof iv !== 'number') iv = 31;
if (this.dex.gen <= 2) iv &= 30; if (this.dex.gen <= 2) iv &= 30;
let ev = (set.evs && set.evs[stat]); let ev = set.evs?.[stat];
if (typeof ev !== 'number') ev = (this.dex.gen > 2 ? 0 : 252); if (typeof ev !== 'number') ev = (this.dex.gen > 2 ? 0 : 252);
if (evOverride !== undefined) ev = evOverride; if (evOverride !== undefined) ev = evOverride;
@ -3160,7 +3166,7 @@ function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) {
return ev; return ev;
}; };
const origSpread = {evs: set.evs, ...origNature}; const origSpread = { evs: set.evs, ...origNature };
let origLeftoverEVs = 508; let origLeftoverEVs = 508;
for (const stat of Dex.statNames) { for (const stat of Dex.statNames) {
origLeftoverEVs -= origSpread.evs?.[stat] || 0; origLeftoverEVs -= origSpread.evs?.[stat] || 0;
@ -3185,12 +3191,12 @@ function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) {
if (!minusTooLow) { if (!minusTooLow) {
for (const stat of Dex.statNamesExceptHP) { for (const stat of Dex.statNamesExceptHP) {
if (origStats[stat] < origStats[bestMinus]) { if (origStats[stat] < origStats[bestMinus]) {
const minEVs = getMinEVs(stat, {minus: stat}); const minEVs = getMinEVs(stat, { minus: stat });
if (minEVs > 252) continue; 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) // 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; savedEVs = (origSpread.evs[stat] || 0) - minEVs;
if (origNature.minus) { if (origNature.minus) {
savedEVs += (origSpread.evs[origNature.minus] || 0) - getMinEVs(origNature.minus, {minus: stat}); savedEVs += (origSpread.evs[origNature.minus] || 0) - getMinEVs(origNature.minus, { minus: stat });
} }
bestMinus = stat; bestMinus = stat;
bestMinusMinEVs = minEVs; bestMinusMinEVs = minEVs;
@ -3201,17 +3207,17 @@ function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) {
for (const stat of Dex.statNamesExceptHP) { for (const stat of Dex.statNamesExceptHP) {
// Don't move the plus to an uninvested stat // Don't move the plus to an uninvested stat
if (stat !== origNature.plus && origSpread.evs[stat] && stat !== bestMinus) { if (stat !== origNature.plus && origSpread.evs[stat] && stat !== bestMinus) {
const minEVs = getMinEVs(stat, {plus: stat}); const minEVs = getMinEVs(stat, { plus: stat });
let plusEVsSaved = (origNature.minus === stat ? getMinEVs(stat, {}) : origSpread.evs[stat] || 0) - minEVs; let plusEVsSaved = (origNature.minus === stat ? getMinEVs(stat, {}) : origSpread.evs[stat] || 0) - minEVs;
if (bestPlus && bestPlus !== bestMinus) { if (bestPlus && bestPlus !== bestMinus) {
plusEVsSaved += bestPlusMinEVs! - getMinEVs(bestPlus, {plus: stat, minus: bestMinus}); plusEVsSaved += bestPlusMinEVs! - getMinEVs(bestPlus, { plus: stat, minus: bestMinus });
} }
if (plusEVsSaved > 0 && savedEVs + plusEVsSaved > 0) { if (plusEVsSaved > 0 && savedEVs + plusEVsSaved > 0) {
savedEVs += plusEVsSaved; savedEVs += plusEVsSaved;
bestPlus = stat; bestPlus = stat;
bestPlusMinEVs = minEVs; bestPlusMinEVs = minEVs;
} else if (plusEVsSaved === 0 && (bestPlus || savedEVs > 0) || plusEVsSaved > 0 && savedEVs + plusEVsSaved === 0) { } else if (plusEVsSaved === 0 && (bestPlus || savedEVs > 0) || plusEVsSaved > 0 && savedEVs + plusEVsSaved === 0) {
if (!bestPlus || getStat(stat, getMinEVs(stat, {plus: stat}), {plus: stat}) > origStats[stat]) { if (!bestPlus || getStat(stat, getMinEVs(stat, { plus: stat }), { plus: stat }) > origStats[stat]) {
savedEVs += plusEVsSaved; savedEVs += plusEVsSaved;
bestPlus = stat; bestPlus = stat;
bestPlusMinEVs = minEVs; bestPlusMinEVs = minEVs;
@ -3226,7 +3232,7 @@ function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) {
evs: Partial<Dex.StatsTable>, evs: Partial<Dex.StatsTable>,
plus?: Dex.StatNameExceptHP, plus?: Dex.StatNameExceptHP,
minus?: Dex.StatNameExceptHP, minus?: Dex.StatNameExceptHP,
} = {evs: {...origSpread.evs}, plus: bestPlus, minus: bestMinus}; } = { evs: { ...origSpread.evs }, plus: bestPlus, minus: bestMinus };
if (bestPlus !== origNature.plus || bestMinus !== origNature.minus) { if (bestPlus !== origNature.plus || bestMinus !== origNature.minus) {
newSpread.evs[bestPlus] = bestPlusMinEVs!; newSpread.evs[bestPlus] = bestPlusMinEVs!;
newSpread.evs[bestMinus] = bestMinusMinEVs!; newSpread.evs[bestMinus] = bestMinusMinEVs!;
@ -3239,7 +3245,7 @@ function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) {
for (const stat of Dex.statNames) { for (const stat of Dex.statNames) {
if (!newSpread.evs[stat]) delete newSpread.evs[stat]; if (!newSpread.evs[stat]) delete newSpread.evs[stat];
} }
return {...newSpread, savedEVs}; return { ...newSpread, savedEVs };
} else if (!plusTooHigh && !minusTooLow) { } else if (!plusTooHigh && !minusTooLow) {
if (Math.floor(getStat(bestPlus, bestMinusMinEVs!, newSpread) / 11) <= Math.ceil(origStats[bestMinus] / 9)) { 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 // We're not gaining more points from our plus than we're losing to our minus
@ -3254,7 +3260,7 @@ function BattleStatOptimizer(set: Dex.PokemonSet, formatid: ID) {
for (const stat of Dex.statNames) { for (const stat of Dex.statNames) {
if (!newSpread.evs[stat]) delete newSpread.evs[stat]; if (!newSpread.evs[stat]) delete newSpread.evs[stat];
} }
return {...newSpread, savedEVs}; return { ...newSpread, savedEVs };
} }
} }
} }
@ -3266,6 +3272,6 @@ declare const require: any;
declare const global: any; declare const global: any;
if (typeof require === 'function') { if (typeof require === 'function') {
// in Node // in Node
(global as any).BattleStatGuesser = BattleStatGuesser; global.BattleStatGuesser = BattleStatGuesser;
(global as any).BattleStatOptimizer = BattleStatOptimizer; global.BattleStatOptimizer = BattleStatOptimizer;
} }

View File

@ -28,16 +28,16 @@
*/ */
// import $ from 'jquery'; // import $ from 'jquery';
import {BattleSceneStub} from './battle-scene-stub'; import { BattleSceneStub } from './battle-scene-stub';
import {BattleLog} from './battle-log'; import { BattleLog } from './battle-log';
import {BattleScene, PokemonSprite, BattleStatusAnims} from './battle-animations'; import { BattleScene, type PokemonSprite, BattleStatusAnims } from './battle-animations';
import {Dex, Teams, toID, toUserid, type ID, type ModdedDex} from './battle-dex'; import { Dex, Teams, toID, toUserid, type ID, type ModdedDex } from './battle-dex';
import {BattleTextParser, type Args, type KWArgs, type SideID} from './battle-text-parser'; import { BattleTextParser, type Args, type KWArgs, type SideID } from './battle-text-parser';
declare const app: { user: AnyObject, rooms: AnyObject, ignore?: AnyObject } | undefined;
/** [id, element?, ...misc] */ /** [id, element?, ...misc] */
export type EffectState = any[] & {0: ID}; export type EffectState = any[] & { 0: ID };
/** [name, minTimeLeft, maxTimeLeft] */ export type WeatherState = [name: string, minTimeLeft: number, maxTimeLeft: number];
export type WeatherState = [string, number, number];
export type HPColor = 'r' | 'y' | 'g'; export type HPColor = 'r' | 'y' | 'g';
export class Pokemon implements PokemonDetails, PokemonHealth { export class Pokemon implements PokemonDetails, PokemonHealth {
@ -93,20 +93,20 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
itemEffect = ''; itemEffect = '';
prevItem = ''; prevItem = '';
prevItemEffect = ''; prevItemEffect = '';
terastallized: string | '' = ''; terastallized = '';
teraType = ''; teraType = '';
boosts: {[stat: string]: number} = {}; boosts: { [stat: string]: number } = {};
status: Dex.StatusName | 'tox' | '' | '???' = ''; status: Dex.StatusName | 'tox' | '' | '???' = '';
statusStage = 0; statusStage = 0;
volatiles: {[effectid: string]: EffectState} = {}; volatiles: { [effectid: string]: EffectState } = {};
turnstatuses: {[effectid: string]: EffectState} = {}; turnstatuses: { [effectid: string]: EffectState } = {};
movestatuses: {[effectid: string]: EffectState} = {}; movestatuses: { [effectid: string]: EffectState } = {};
lastMove = ''; lastMove = '';
/** [[moveName, ppUsed]] */ /** [[moveName, ppUsed]] */
moveTrack: [string, number][] = []; moveTrack: [string, number][] = [];
statusData = {sleepTurns: 0, toxicTurns: 0}; statusData = { sleepTurns: 0, toxicTurns: 0 };
timesAttacked = 0; timesAttacked = 0;
sprite: PokemonSprite; sprite: PokemonSprite;
@ -174,7 +174,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
if (range[0] === range[1]) { if (range[0] === range[1]) {
let percentage = Math.abs(range[0] * 100); let percentage = Math.abs(range[0] * 100);
if (Math.floor(percentage) === percentage) { if (Math.floor(percentage) === percentage) {
return percentage + '%'; return `${percentage}%`;
} }
return percentage.toFixed(precision) + '%'; return percentage.toFixed(precision) + '%';
} }
@ -187,7 +187,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
lower = (range[0] * 100).toFixed(precision); lower = (range[0] * 100).toFixed(precision);
upper = (range[1] * 100).toFixed(precision); upper = (range[1] * 100).toFixed(precision);
} }
return '' + lower + separator + upper + '%'; return `${lower}${separator}${upper}%`;
} }
// Returns [min, max] damage dealt as a proportion of total HP from 0 to 1 // Returns [min, max] damage dealt as a proportion of total HP from 0 to 1
getDamageRange(damage: any): [number, number] { getDamageRange(damage: any): [number, number] {
@ -216,7 +216,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
healthParse(hpstring: string, parsedamage?: boolean, heal?: boolean): healthParse(hpstring: string, parsedamage?: boolean, heal?: boolean):
[number, number, number] | [number, number, number, number, HPColor] | null { [number, number, number] | [number, number, number, number, HPColor] | null {
// returns [delta, denominator, percent(, oldnum, oldcolor)] or null // returns [delta, denominator, percent(, oldnum, oldcolor)] or null
if (!hpstring || !hpstring.length) return null; if (!hpstring?.length) return null;
let parenIndex = hpstring.lastIndexOf('('); let parenIndex = hpstring.lastIndexOf('(');
if (parenIndex >= 0) { if (parenIndex >= 0) {
// old style damage and health reporting // old style damage and health reporting
@ -266,7 +266,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
if (!details) return false; if (!details) return false;
if (details === this.details) return true; if (details === this.details) return true;
if (this.searchid) return false; if (this.searchid) return false;
if (details.indexOf(', shiny') >= 0) { if (details.includes(', shiny')) {
if (this.checkDetails(details.replace(', shiny', ''))) return true; if (this.checkDetails(details.replace(', shiny', ''))) return true;
} }
// the actual forme was hidden on Team Preview // the actual forme was hidden on Team Preview
@ -340,7 +340,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
rememberMove(moveName: string, pp = 1, recursionSource?: string) { rememberMove(moveName: string, pp = 1, recursionSource?: string) {
if (recursionSource === this.ident) return; if (recursionSource === this.ident) return;
moveName = Dex.moves.get(moveName).name; moveName = Dex.moves.get(moveName).name;
if (moveName.charAt(0) === '*') return; if (moveName.startsWith('*')) return;
if (moveName === 'Struggle') return; if (moveName === 'Struggle') return;
if (this.volatiles.transform) { if (this.volatiles.transform) {
// make sure there is no infinite recursion if both Pokemon are transformed into each other // make sure there is no infinite recursion if both Pokemon are transformed into each other
@ -422,7 +422,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
this.boosts = {}; this.boosts = {};
this.clearVolatiles(); this.clearVolatiles();
for (let i = 0; i < this.moveTrack.length; i++) { for (let i = 0; i < this.moveTrack.length; i++) {
if (this.moveTrack[i][0].charAt(0) === '*') { if (this.moveTrack[i][0].startsWith('*')) {
this.moveTrack.splice(i, 1); this.moveTrack.splice(i, 1);
i--; i--;
} }
@ -477,8 +477,8 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
this.removeVolatile('typeadd' as ID); this.removeVolatile('typeadd' as ID);
} }
} }
getTypes(serverPokemon?: ServerPokemon, preterastallized = false): [ReadonlyArray<Dex.TypeName>, Dex.TypeName | ''] { getTypes(serverPokemon?: ServerPokemon, preterastallized = false): [readonly Dex.TypeName[], Dex.TypeName | ''] {
let types: ReadonlyArray<Dex.TypeName>; let types: readonly Dex.TypeName[];
if (!preterastallized && this.terastallized && this.terastallized !== 'Stellar') { if (!preterastallized && this.terastallized && this.terastallized !== 'Stellar') {
types = [this.terastallized as Dex.TypeName]; types = [this.terastallized as Dex.TypeName];
} else if (this.volatiles.typechange) { } else if (this.volatiles.typechange) {
@ -588,8 +588,8 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
return Pokemon.getHPText(this, this.side.battle.reportExactHP, precision); return Pokemon.getHPText(this, this.side.battle.reportExactHP, precision);
} }
static getHPText(pokemon: PokemonHealth, exactHP: boolean, precision = 1) { static getHPText(pokemon: PokemonHealth, exactHP: boolean, precision = 1) {
if (exactHP) return pokemon.hp + '/' + pokemon.maxhp; if (exactHP) return `${pokemon.hp}/${pokemon.maxhp}`;
if (pokemon.maxhp === 100) return pokemon.hp + '%'; if (pokemon.maxhp === 100) return `${pokemon.hp}%`;
if (pokemon.maxhp !== 48) return (100 * pokemon.hp / pokemon.maxhp).toFixed(precision) + '%'; if (pokemon.maxhp !== 48) return (100 * pokemon.hp / pokemon.maxhp).toFixed(precision) + '%';
let range = Pokemon.getPixelRange(pokemon.hp, pokemon.hpcolor); let range = Pokemon.getPixelRange(pokemon.hp, pokemon.hpcolor);
return Pokemon.getFormattedRange(range, precision, ''); return Pokemon.getFormattedRange(range, precision, '');
@ -610,9 +610,9 @@ export class Side {
isFar: boolean; isFar: boolean;
foe: Side = null!; foe: Side = null!;
ally: Side | null = null; ally: Side | null = null;
avatar: string = 'unknown'; avatar = 'unknown';
badges: string[] = []; badges: string[] = [];
rating: string = ''; rating = '';
totalPokemon = 6; totalPokemon = 6;
x = 0; x = 0;
y = 0; y = 0;
@ -625,8 +625,9 @@ export class Side {
lastPokemon = null as Pokemon | null; lastPokemon = null as Pokemon | null;
pokemon = [] as Pokemon[]; pokemon = [] as Pokemon[];
/** [effectName, levels, minDuration, maxDuration] */ sideConditions: {
sideConditions: {[id: string]: [string, number, number, number]} = {}; [id: string]: [effectName: string, levels: number, minDuration: number, maxDuration: number],
} = {};
faintCounter = 0; faintCounter = 0;
constructor(battle: Battle, n: number) { constructor(battle: Battle, n: number) {
@ -765,7 +766,7 @@ export class Side {
} }
if (this.pokemon.length > this.totalPokemon || this.battle.speciesClause) { if (this.pokemon.length > this.totalPokemon || this.battle.speciesClause) {
// check for Illusion // check for Illusion
let existingTable: {[searchid: string]: number} = {}; let existingTable: { [searchid: string]: number } = {};
let toRemove = -1; let toRemove = -1;
for (let poke1i = 0; poke1i < this.pokemon.length; poke1i++) { for (let poke1i = 0; poke1i < this.pokemon.length; poke1i++) {
let poke1 = this.pokemon[poke1i]; let poke1 = this.pokemon[poke1i];
@ -777,9 +778,9 @@ export class Side {
toRemove = poke2i; toRemove = poke2i;
} else if (poke === poke2) { } else if (poke === poke2) {
toRemove = poke1i; toRemove = poke1i;
} else if (this.active.indexOf(poke1) >= 0) { } else if (this.active.includes(poke1)) {
toRemove = poke2i; toRemove = poke2i;
} else if (this.active.indexOf(poke2) >= 0) { } else if (this.active.includes(poke2)) {
toRemove = poke1i; toRemove = poke1i;
} else if (poke1.fainted && !poke2.fainted) { } else if (poke1.fainted && !poke2.fainted) {
toRemove = poke2i; toRemove = poke2i;
@ -797,7 +798,7 @@ export class Side {
for (const curPoke of this.pokemon) { for (const curPoke of this.pokemon) {
if (curPoke === poke) continue; if (curPoke === poke) continue;
if (curPoke.fainted) continue; if (curPoke.fainted) continue;
if (this.active.indexOf(curPoke) >= 0) continue; if (this.active.includes(curPoke)) continue;
if (curPoke.speciesForme === 'Zoroark' || curPoke.speciesForme === 'Zorua' || curPoke.ability === 'Illusion') { if (curPoke.speciesForme === 'Zoroark' || curPoke.speciesForme === 'Zorua' || curPoke.ability === 'Illusion') {
illusionFound = curPoke; illusionFound = curPoke;
break; break;
@ -811,7 +812,7 @@ export class Side {
for (const curPoke of this.pokemon) { for (const curPoke of this.pokemon) {
if (curPoke === poke) continue; if (curPoke === poke) continue;
if (curPoke.fainted) continue; if (curPoke.fainted) continue;
if (this.active.indexOf(curPoke) >= 0) continue; if (this.active.includes(curPoke)) continue;
illusionFound = curPoke; illusionFound = curPoke;
break; break;
} }
@ -871,7 +872,7 @@ export class Side {
pokemon.hpcolor = oldpokemon.hpcolor; pokemon.hpcolor = oldpokemon.hpcolor;
pokemon.status = oldpokemon.status; pokemon.status = oldpokemon.status;
pokemon.copyVolatileFrom(oldpokemon, true); pokemon.copyVolatileFrom(oldpokemon, true);
pokemon.statusData = {...oldpokemon.statusData}; pokemon.statusData = { ...oldpokemon.statusData };
if (oldpokemon.terastallized) { if (oldpokemon.terastallized) {
pokemon.terastallized = oldpokemon.terastallized; pokemon.terastallized = oldpokemon.terastallized;
pokemon.teraType = oldpokemon.terastallized; pokemon.teraType = oldpokemon.terastallized;
@ -901,7 +902,7 @@ export class Side {
pokemon.removeVolatile('formechange' as ID); pokemon.removeVolatile('formechange' as ID);
} }
if (!['batonpass', 'zbatonpass', 'shedtail', 'teleport'].includes(effect.id)) { if (!['batonpass', 'zbatonpass', 'shedtail', 'teleport'].includes(effect.id)) {
this.battle.log(['switchout', pokemon.ident], {from: effect.id}); this.battle.log(['switchout', pokemon.ident], { from: effect.id });
} }
pokemon.statusData.toxicTurns = 0; pokemon.statusData.toxicTurns = 0;
if (this.battle.gen === 5) pokemon.statusData.sleepTurns = 0; if (this.battle.gen === 5) pokemon.statusData.sleepTurns = 0;
@ -1099,7 +1100,7 @@ export class Battle {
gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' = 'singles'; gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' = 'singles';
compatMode = true; compatMode = true;
rated: string | boolean = false; rated: string | boolean = false;
rules: {[ruleName: string]: 1 | 0} = {}; rules: { [ruleName: string]: 1 | undefined } = {};
isBlitz = false; isBlitz = false;
reportExactHP = false; reportExactHP = false;
endLastTurnPending = false; endLastTurnPending = false;
@ -1131,8 +1132,8 @@ export class Battle {
paused: boolean; paused: boolean;
constructor(options: { constructor(options: {
$frame?: JQuery<HTMLElement>, $frame?: JQuery,
$logFrame?: JQuery<HTMLElement>, $logFrame?: JQuery,
id?: ID, id?: ID,
log?: string[] | string, log?: string[] | string,
paused?: boolean, paused?: boolean,
@ -1184,9 +1185,9 @@ export class Battle {
} }
if (width && width < 640) { if (width && width < 640) {
const scale = (width / 640); const scale = (width / 640);
this.scene.$frame?.css('transform', 'scale(' + scale + ')'); this.scene.$frame?.css('transform', `scale(${scale})`);
this.scene.$frame?.css('transform-origin', 'top left'); this.scene.$frame?.css('transform-origin', 'top left');
this.scene.$frame?.css('margin-bottom', '' + (360 * scale - 360) + 'px'); this.scene.$frame?.css('margin-bottom', `${360 * scale - 360}px`);
// this.$foeHint.css('transform', 'scale(' + scale + ')'); // this.$foeHint.css('transform', 'scale(' + scale + ')');
} else { } else {
this.scene.$frame?.css('transform', 'none'); this.scene.$frame?.css('transform', 'none');
@ -1433,7 +1434,7 @@ export class Battle {
if (this.gameType === 'freeforall') { if (this.gameType === 'freeforall') {
// TODO: Add FFA support // TODO: Add FFA support
return; return;
} else { }
let side1 = this.sides[0]; let side1 = this.sides[0];
let side2 = this.sides[1]; let side2 = this.sides[1];
for (const id of sideConditions) { for (const id of sideConditions) {
@ -1454,7 +1455,6 @@ export class Battle {
} }
} }
} }
}
updateTurnCounters() { updateTurnCounters() {
for (const pWeather of this.pseudoWeather) { for (const pWeather of this.pseudoWeather) {
if (pWeather[1]) pWeather[1]--; if (pWeather[1]) pWeather[1]--;
@ -1498,7 +1498,7 @@ export class Battle {
pokemon.item = move.isZ; pokemon.item = move.isZ;
let item = Dex.items.get(move.isZ); let item = Dex.items.get(move.isZ);
if (item.zMoveFrom) moveName = item.zMoveFrom; if (item.zMoveFrom) moveName = item.zMoveFrom;
} else if (move.name.slice(0, 2) === 'Z-') { } else if (move.name.startsWith('Z-')) {
moveName = moveName.slice(2); moveName = moveName.slice(2);
move = Dex.moves.get(moveName); move = Dex.moves.get(moveName);
if (window.BattleItems) { if (window.BattleItems) {
@ -1733,8 +1733,7 @@ export class Battle {
} }
let damageinfo = '' + Pokemon.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '\u2013'); let damageinfo = '' + Pokemon.getFormattedRange(range, damage[1] === 100 ? 0 : 1, '\u2013');
if (damage[1] !== 100) { if (damage[1] !== 100) {
let hover = '' + ((damage[0] < 0) ? '\u2212' : '') + let hover = `${(damage[0] < 0) ? '\u2212' : ''}${Math.abs(damage[0])}/${damage[1]}`;
Math.abs(damage[0]) + '/' + damage[1];
if (damage[1] === 48) { // this is a hack if (damage[1] === 48) { // this is a hack
hover += ' pixels'; hover += ' pixels';
} }
@ -1780,7 +1779,7 @@ export class Battle {
break; break;
case 'revivalblessing': case 'revivalblessing':
this.scene.runResidualAnim('wish' as ID, poke); this.scene.runResidualAnim('wish' as ID, poke);
const {siden} = this.parsePokemonId(args[1]); const { siden } = this.parsePokemonId(args[1]);
const side = this.sides[siden]; const side = this.sides[siden];
poke.fainted = false; poke.fainted = false;
poke.status = ''; poke.status = '';
@ -2216,7 +2215,6 @@ export class Battle {
} }
this.log(args, kwArgs); this.log(args, kwArgs);
break; break;
} }
case '-cureteam': { // For old gens when the whole team was always cured case '-cureteam': { // For old gens when the whole team was always cured
let poke = this.getPokemon(args[1])!; let poke = this.getPokemon(args[1])!;
@ -2240,7 +2238,7 @@ export class Battle {
if (possibleTargets.length === 1) { if (possibleTargets.length === 1) {
poke = possibleTargets[0]!; poke = possibleTargets[0]!;
} else { } else {
this.activateAbility(ofpoke!, "Frisk"); this.activateAbility(ofpoke, "Frisk");
this.log(args, kwArgs); this.log(args, kwArgs);
break; break;
} }
@ -2263,7 +2261,7 @@ export class Battle {
this.scene.resultAnim(poke, item.name, 'neutral'); this.scene.resultAnim(poke, item.name, 'neutral');
break; break;
case 'frisk': case 'frisk':
this.activateAbility(ofpoke!, "Frisk"); this.activateAbility(ofpoke, "Frisk");
if (poke && poke !== ofpoke) { // used for gen 6 if (poke && poke !== ofpoke) { // used for gen 6
poke.itemEffect = 'frisked'; poke.itemEffect = 'frisked';
this.scene.resultAnim(poke, item.name, 'neutral'); this.scene.resultAnim(poke, item.name, 'neutral');
@ -2448,7 +2446,7 @@ export class Battle {
let commaIndex = newSpeciesForme.indexOf(','); let commaIndex = newSpeciesForme.indexOf(',');
if (commaIndex !== -1) { if (commaIndex !== -1) {
let level = newSpeciesForme.substr(commaIndex + 1).trim(); let level = newSpeciesForme.substr(commaIndex + 1).trim();
if (level.charAt(0) === 'L') { if (level.startsWith('L')) {
poke.level = parseInt(level.substr(1), 10); poke.level = parseInt(level.substr(1), 10);
} }
newSpeciesForme = args[2].substr(0, commaIndex); newSpeciesForme = args[2].substr(0, commaIndex);
@ -2483,7 +2481,7 @@ export class Battle {
this.activateAbility(poke, effect); this.activateAbility(poke, effect);
} }
poke.boosts = {...tpoke.boosts}; poke.boosts = { ...tpoke.boosts };
poke.copyTypesFrom(tpoke, true); poke.copyTypesFrom(tpoke, true);
poke.ability = tpoke.ability; poke.ability = tpoke.ability;
poke.timesAttacked = tpoke.timesAttacked; poke.timesAttacked = tpoke.timesAttacked;
@ -2742,7 +2740,7 @@ export class Battle {
break; break;
case 'skydrop': case 'skydrop':
if (kwArgs.interrupt) { if (kwArgs.interrupt) {
this.scene.anim(poke, {time: 100}); this.scene.anim(poke, { time: 100 });
} }
break; break;
case 'confusion': case 'confusion':
@ -2953,7 +2951,7 @@ export class Battle {
case 'gravity': case 'gravity':
poke.removeVolatile('magnetrise' as ID); poke.removeVolatile('magnetrise' as ID);
poke.removeVolatile('telekinesis' as ID); poke.removeVolatile('telekinesis' as ID);
this.scene.anim(poke, {time: 100}); this.scene.anim(poke, { time: 100 });
break; break;
case 'skillswap': case 'wanderingspirit': case 'skillswap': case 'wanderingspirit':
if (this.gen <= 4) break; if (this.gen <= 4) break;
@ -3139,7 +3137,8 @@ export class Battle {
default: { default: {
throw new Error(`Unrecognized minor action: ${args[0]}`); throw new Error(`Unrecognized minor action: ${args[0]}`);
break; break;
}} }
}
} }
/* /*
parseSpriteData(name) { parseSpriteData(name) {
@ -3256,17 +3255,17 @@ export class Battle {
siden = parseInt(name.charAt(1), 10) - 1; siden = parseInt(name.charAt(1), 10) - 1;
name = name.slice(4); name = name.slice(4);
} else if (/^p[1-9][a-f]: /.test(name)) { } 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}; const slotChart: { [k: string]: number } = { a: 0, b: 1, c: 2, d: 3, e: 4, f: 5 };
siden = parseInt(name.charAt(1), 10) - 1; siden = parseInt(name.charAt(1), 10) - 1;
slot = slotChart[name.charAt(2)]; slot = slotChart[name.charAt(2)];
name = name.slice(5); name = name.slice(5);
pokemonid = `p${siden + 1}: ${name}`; pokemonid = `p${siden + 1}: ${name}`;
} }
return {name, siden, slot, pokemonid}; return { name, siden, slot, pokemonid };
} }
getSwitchedPokemon(pokemonid: string, details: string) { getSwitchedPokemon(pokemonid: string, details: string) {
if (pokemonid === '??') throw new Error(`pokemonid not passed`); if (pokemonid === '??') throw new Error(`pokemonid not passed`);
const {name, siden, slot, pokemonid: parsedPokemonid} = this.parsePokemonId(pokemonid); const { name, siden, slot, pokemonid: parsedPokemonid } = this.parsePokemonId(pokemonid);
pokemonid = parsedPokemonid; pokemonid = parsedPokemonid;
const searchid = `${pokemonid}|${details}`; const searchid = `${pokemonid}|${details}`;
@ -3300,12 +3299,12 @@ export class Battle {
return pokemon; return pokemon;
} }
rememberTeamPreviewPokemon(sideid: string, details: string) { rememberTeamPreviewPokemon(sideid: string, details: string) {
const {siden} = this.parsePokemonId(sideid); const { siden } = this.parsePokemonId(sideid);
return this.sides[siden].addPokemon('', '', details); return this.sides[siden].addPokemon('', '', details);
} }
findCorrespondingPokemon(serverPokemon: {ident: string, details: string}) { findCorrespondingPokemon(serverPokemon: { ident: string, details: string }) {
const {siden} = this.parsePokemonId(serverPokemon.ident); const { siden } = this.parsePokemonId(serverPokemon.ident);
const searchid = `${serverPokemon.ident}|${serverPokemon.details}`; const searchid = `${serverPokemon.ident}|${serverPokemon.details}`;
for (const pokemon of this.sides[siden].pokemon) { for (const pokemon of this.sides[siden].pokemon) {
if (pokemon.searchid === searchid) { if (pokemon.searchid === searchid) {
@ -3318,7 +3317,7 @@ export class Battle {
if (!pokemonid || pokemonid === '??' || pokemonid === 'null' || pokemonid === 'false') { if (!pokemonid || pokemonid === '??' || pokemonid === 'null' || pokemonid === 'false') {
return null; return null;
} }
const {siden, slot, pokemonid: parsedPokemonid} = this.parsePokemonId(pokemonid); const { siden, slot, pokemonid: parsedPokemonid } = this.parsePokemonId(pokemonid);
pokemonid = parsedPokemonid; pokemonid = parsedPokemonid;
/** if true, don't match an active pokemon */ /** if true, don't match an active pokemon */
@ -3400,10 +3399,10 @@ export class Battle {
} }
case 'tier': { case 'tier': {
this.tier = args[1]; this.tier = args[1];
if (this.tier.slice(-13) === 'Random Battle') { if (this.tier.endsWith('Random Battle')) {
this.speciesClause = true; this.speciesClause = true;
} }
if (this.tier.slice(-8) === ' (Blitz)') { if (this.tier.endsWith(' (Blitz)')) {
this.messageFadeTime = 40; this.messageFadeTime = 40;
this.isBlitz = true; this.isBlitz = true;
} }
@ -3480,26 +3479,26 @@ export class Battle {
} }
case 'inactive': { case 'inactive': {
if (!this.kickingInactive) this.kickingInactive = true; if (!this.kickingInactive) this.kickingInactive = true;
if (args[1].slice(0, 11) === "Time left: ") { if (args[1].startsWith("Time left: ")) {
let [time, totalTime, graceTime] = args[1].split(' | '); let [time, totalTime, graceTime] = args[1].split(' | ');
this.kickingInactive = parseInt(time.slice(11), 10) || true; this.kickingInactive = parseInt(time.slice(11), 10) || true;
this.totalTimeLeft = parseInt(totalTime, 10); this.totalTimeLeft = parseInt(totalTime, 10);
this.graceTimeLeft = parseInt(graceTime || '', 10) || 0; this.graceTimeLeft = parseInt(graceTime || '', 10) || 0;
if (this.totalTimeLeft === this.kickingInactive) this.totalTimeLeft = 0; if (this.totalTimeLeft === this.kickingInactive) this.totalTimeLeft = 0;
return; return;
} else if (args[1].slice(0, 9) === "You have ") { } else if (args[1].startsWith("You have ")) {
// this is ugly but parseInt is documented to work this way // this is ugly but parseInt is documented to work this way
// so I'm going to be lazy and not chop off the rest of the // so I'm going to be lazy and not chop off the rest of the
// sentence // sentence
this.kickingInactive = parseInt(args[1].slice(9), 10) || true; this.kickingInactive = parseInt(args[1].slice(9), 10) || true;
return; return;
} else if (args[1].slice(-14) === ' seconds left.') { } else if (args[1].endsWith(' seconds left.')) {
let hasIndex = args[1].indexOf(' has '); let hasIndex = args[1].indexOf(' has ');
let userid = window.app?.user?.get('userid'); let userid = window.app?.user?.get('userid');
if (toID(args[1].slice(0, hasIndex)) === userid) { if (toID(args[1].slice(0, hasIndex)) === userid) {
this.kickingInactive = parseInt(args[1].slice(hasIndex + 5), 10) || true; this.kickingInactive = parseInt(args[1].slice(hasIndex + 5), 10) || true;
} }
} else if (args[1].slice(-27) === ' 15 seconds left this turn.') { } else if (args[1].endsWith(' 15 seconds left this turn.')) {
if (this.isBlitz) return; if (this.isBlitz) return;
} }
this.log(args, undefined, preempt); this.log(args, undefined, preempt);
@ -3598,7 +3597,7 @@ export class Battle {
break; break;
} }
case 'poke': { case 'poke': {
let pokemon = this.rememberTeamPreviewPokemon(args[1], args[2])!; let pokemon = this.rememberTeamPreviewPokemon(args[1], args[2]);
if (args[3] === 'mail') { if (args[3] === 'mail') {
pokemon.item = '(mail)'; pokemon.item = '(mail)';
} else if (args[3] === 'item') { } else if (args[3] === 'item') {
@ -3607,7 +3606,7 @@ export class Battle {
break; break;
} }
case 'updatepoke': { case 'updatepoke': {
const {siden} = this.parsePokemonId(args[1]); const { siden } = this.parsePokemonId(args[1]);
const side = this.sides[siden]; const side = this.sides[siden];
for (let i = 0; i < side.pokemon.length; i++) { for (let i = 0; i < side.pokemon.length; i++) {
const pokemon = side.pokemon[i]; const pokemon = side.pokemon[i];
@ -3629,8 +3628,8 @@ export class Battle {
const side = this.getSide(args[1]); const side = this.getSide(args[1]);
side.clearPokemon(); side.clearPokemon();
for (const set of team) { for (const set of team) {
const details = set.species + (!set.level || set.level === 100 ? '' : ', L' + set.level) + const details = set.species + (!set.level || set.level === 100 ? '' : `, L${set.level}`) +
(!set.gender || set.gender === 'N' ? '' : ', ' + set.gender) + (set.shiny ? ', shiny' : ''); (!set.gender || set.gender === 'N' ? '' : `, ${set.gender}`) + (set.shiny ? ', shiny' : '');
const pokemon = side.addPokemon('', '', details); const pokemon = side.addPokemon('', '', details);
if (set.item) pokemon.item = set.item; if (set.item) pokemon.item = set.item;
if (set.ability) pokemon.rememberAbility(set.ability); if (set.ability) pokemon.rememberAbility(set.ability);
@ -3644,14 +3643,14 @@ export class Battle {
} }
case 'switch': case 'drag': case 'replace': { case 'switch': case 'drag': case 'replace': {
this.endLastTurn(); this.endLastTurn();
let poke = this.getSwitchedPokemon(args[1], args[2])!; let poke = this.getSwitchedPokemon(args[1], args[2]);
let slot = poke.slot; let slot = poke.slot;
poke.healthParse(args[3]); poke.healthParse(args[3]);
poke.removeVolatile('itemremoved' as ID); poke.removeVolatile('itemremoved' as ID);
poke.terastallized = args[2].match(/tera:([a-z]+)$/i)?.[1] || ''; poke.terastallized = (/tera:([a-z]+)$/i.exec(args[2]))?.[1] || '';
if (args[0] === 'switch') { if (args[0] === 'switch') {
if (poke.side.active[slot]) { if (poke.side.active[slot]) {
poke.side.switchOut(poke.side.active[slot]!, kwArgs); poke.side.switchOut(poke.side.active[slot], kwArgs);
} }
poke.side.switchIn(poke, kwArgs); poke.side.switchIn(poke, kwArgs);
} else if (args[0] === 'replace') { } else if (args[0] === 'replace') {
@ -3746,7 +3745,8 @@ export class Battle {
default: { default: {
this.log(args, kwArgs, preempt); this.log(args, kwArgs, preempt);
break; break;
}} }
}
} }
run(str: string, preempt?: boolean) { run(str: string, preempt?: boolean) {
@ -3756,7 +3756,7 @@ export class Battle {
return; return;
} }
if (!str) return; if (!str) return;
const {args, kwArgs} = BattleTextParser.parseBattleLine(str); const { args, kwArgs } = BattleTextParser.parseBattleLine(str);
if (this.scene.maybeCloseMessagebar(args, kwArgs)) { if (this.scene.maybeCloseMessagebar(args, kwArgs)) {
this.currentStep--; this.currentStep--;
@ -3768,19 +3768,19 @@ export class Battle {
let nextArgs: Args = ['']; let nextArgs: Args = [''];
let nextKwargs: KWArgs = {}; let nextKwargs: KWArgs = {};
const nextLine = this.stepQueue[this.currentStep + 1] || ''; const nextLine = this.stepQueue[this.currentStep + 1] || '';
if (nextLine.slice(0, 2) === '|-') { if (nextLine.startsWith('|-')) {
({args: nextArgs, kwArgs: nextKwargs} = BattleTextParser.parseBattleLine(nextLine)); ({ args: nextArgs, kwArgs: nextKwargs } = BattleTextParser.parseBattleLine(nextLine));
} }
if (this.debug) { if (this.debug) {
if (args[0].charAt(0) === '-' || args[0] === 'detailschange') { if (args[0].startsWith('-') || args[0] === 'detailschange') {
this.runMinor(args, kwArgs, nextArgs, nextKwargs); this.runMinor(args, kwArgs, nextArgs, nextKwargs);
} else { } else {
this.runMajor(args, kwArgs, preempt); this.runMajor(args, kwArgs, preempt);
} }
} else { } else {
try { try {
if (args[0].charAt(0) === '-' || args[0] === 'detailschange') { if (args[0].startsWith('-') || args[0] === 'detailschange') {
this.runMinor(args, kwArgs, nextArgs, nextKwargs); this.runMinor(args, kwArgs, nextArgs, nextKwargs);
} else { } else {
this.runMajor(args, kwArgs, preempt); this.runMajor(args, kwArgs, preempt);
@ -3893,7 +3893,8 @@ export class Battle {
let interruptionCount: number; let interruptionCount: number;
do { do {
this.waitForAnimations = true; // modified in this.run() but idk how to tell TS that
this.waitForAnimations = true as this['waitForAnimations'];
if (this.currentStep >= this.stepQueue.length) { if (this.currentStep >= this.stepQueue.length) {
this.atQueueEnd = true; this.atQueueEnd = true;
if (!this.ended && this.isReplay) this.prematureEnd(); if (!this.ended && this.isReplay) this.prematureEnd();
@ -3954,6 +3955,6 @@ declare const require: any;
declare const global: any; declare const global: any;
if (typeof require === 'function') { if (typeof require === 'function') {
// in Node // in Node
(global as any).Battle = Battle; global.Battle = Battle;
(global as any).Pokemon = Pokemon; global.Pokemon = Pokemon;
} }

View File

@ -5,9 +5,10 @@
* @license MIT * @license MIT
*/ */
import {PS} from "./client-main"; import { PS } from "./client-main";
declare var SockJS: any; declare const SockJS: any;
declare const POKEMON_SHOWDOWN_TESTCLIENT_KEY: string | undefined;
export class PSConnection { export class PSConnection {
socket: any = null; socket: any = null;
@ -20,7 +21,7 @@ export class PSConnection {
const server = PS.server; const server = PS.server;
const port = server.protocol === 'https' ? '' : ':' + server.port; const port = server.protocol === 'https' ? '' : ':' + server.port;
const url = server.protocol + '://' + server.host + port + server.prefix; const url = server.protocol + '://' + server.host + port + server.prefix;
const socket = this.socket = new SockJS(url, [], {timeout: 5 * 60 * 1000}); const socket = this.socket = new SockJS(url, [], { timeout: 5 * 60 * 1000 });
socket.onopen = () => { socket.onopen = () => {
console.log('\u2705 (CONNECTED)'); console.log('\u2705 (CONNECTED)');
this.connected = true; this.connected = true;
@ -60,17 +61,15 @@ export class PSConnection {
PS.connection = new PSConnection(); PS.connection = new PSConnection();
export const PSLoginServer = new class { export const PSLoginServer = new class {
query(data: PostData): Promise<{[k: string]: any} | null> { query(data: PostData): Promise<{ [k: string]: any } | null> {
let url = '/~~' + PS.server.id + '/action.php'; let url = '/~~' + PS.server.id + '/action.php';
if (location.pathname.endsWith('.html')) { if (location.pathname.endsWith('.html')) {
url = 'https://' + Config.routes.client + url; url = 'https://' + Config.routes.client + url;
// @ts-ignore
if (typeof POKEMON_SHOWDOWN_TESTCLIENT_KEY === 'string') { if (typeof POKEMON_SHOWDOWN_TESTCLIENT_KEY === 'string') {
// @ts-ignore data.sid = POKEMON_SHOWDOWN_TESTCLIENT_KEY.replace(/%2C/g, ',');
data.sid = POKEMON_SHOWDOWN_TESTCLIENT_KEY.replace(/\%2C/g, ',');
} }
} }
return Net(url).get({method: data ? 'POST' : 'GET', body: data}).then( return Net(url).get({ method: data ? 'POST' : 'GET', body: data }).then(
res => res ? JSON.parse(res.slice(1)) : null res => res ? JSON.parse(res.slice(1)) : null
).catch( ).catch(
() => null () => null
@ -96,7 +95,7 @@ class HttpError extends Error {
this.body = body; this.body = body;
try { try {
(Error as any).captureStackTrace(this, HttpError); (Error as any).captureStackTrace(this, HttpError);
} catch (err) {} } catch {}
} }
} }
class NetRequest { class NetRequest {

View File

@ -13,76 +13,12 @@
* @license AGPLv3 * @license AGPLv3
*/ */
/**********************************************************************
* Polyfills
*********************************************************************/
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
for (let i = (fromIndex || 0); i < this.length; i++) {
if (this[i] === searchElement) return i;
}
return -1;
};
}
if (!Array.prototype.includes) {
Array.prototype.includes = function (thing) {
return this.indexOf(thing) !== -1;
};
}
if (!String.prototype.includes) {
String.prototype.includes = function (thing) {
return this.indexOf(thing) !== -1;
};
}
if (!String.prototype.startsWith) {
String.prototype.startsWith = function (thing) {
return this.slice(0, thing.length) === thing;
};
}
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (thing) {
return this.slice(-thing.length) === thing;
};
}
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
if (!Object.assign) {
Object.assign = function (thing: any, rest: any) {
for (let i = 1; i < arguments.length; i++) {
let source = arguments[i];
for (let k in source) {
thing[k] = source[k];
}
}
return thing;
};
}
if (!Object.create) {
Object.create = function (proto: any) {
function F() {}
F.prototype = proto;
return new (F as any)();
};
}
if (!window.console) {
// in IE8, the console object is only defined when devtools is open
// I don't actually know if this will cause problems when you open devtools,
// but that's something I can figure out if I ever bother testing in IE8
(window as any).console = {
log() {},
};
}
/********************************************************************** /**********************************************************************
* PS Models * PS Models
*********************************************************************/ *********************************************************************/
// PS's model classes are defined here // PS's model classes are defined here
const PSURL = `${document.location!.protocol !== 'http:' ? 'https:' : ''}//${Config.routes.client}/`; const PSURL = `${document.location.protocol !== 'http:' ? 'https:' : ''}//${Config.routes.client}/`;
export class PSSubscription { export class PSSubscription {
observable: PSModel | PSStreamModel<any>; observable: PSModel | PSStreamModel<any>;
@ -179,7 +115,7 @@ declare const ColorThief: any;
const PSBackground = new class extends PSStreamModel { const PSBackground = new class extends PSStreamModel {
id = ''; id = '';
curId = ''; curId = '';
attrib: {url: string, title: string, artist: string} | null = null; attrib: { url: string, title: string, artist: string } | null = null;
changeCount = 0; changeCount = 0;
menuColors: string[] | null = null; menuColors: string[] | null = null;
@ -318,7 +254,7 @@ const PSBackground = new class extends PSStreamModel {
"", "",
]; ];
} }
if (!menuColors && bgUrl.charAt(0) === '#') { if (!menuColors && bgUrl.startsWith('#')) {
const r = parseInt(bgUrl.slice(1, 3), 16) / 255; const r = parseInt(bgUrl.slice(1, 3), 16) / 255;
const g = parseInt(bgUrl.slice(3, 5), 16) / 255; const g = parseInt(bgUrl.slice(3, 5), 16) / 255;
const b = parseInt(bgUrl.slice(5, 7), 16) / 255; const b = parseInt(bgUrl.slice(5, 7), 16) / 255;
@ -396,7 +332,7 @@ PSBackground.subscribe(bgUrl => {
if (bgUrl !== null) { if (bgUrl !== null) {
let background; let background;
if (bgUrl.charAt(0) === '#') { if (bgUrl.startsWith('#')) {
background = bgUrl; background = bgUrl;
} else if (PSBackground.curId !== 'custom') { } else if (PSBackground.curId !== 'custom') {
background = `#546bac url(${bgUrl}) no-repeat left center fixed`; background = `#546bac url(${bgUrl}) no-repeat left center fixed`;
@ -424,7 +360,7 @@ PSBackground.subscribe(bgUrl => {
buttonStyleElem = new HTMLStyleElement(); buttonStyleElem = new HTMLStyleElement();
buttonStyleElem.id = 'mainmenubuttoncolors'; buttonStyleElem.id = 'mainmenubuttoncolors';
buttonStyleElem.textContent = cssBuf; buttonStyleElem.textContent = cssBuf;
document.head!.appendChild(buttonStyleElem); document.head.appendChild(buttonStyleElem);
} }
} else { } else {
buttonStyleElem.textContent = cssBuf; buttonStyleElem.textContent = cssBuf;

View File

@ -9,13 +9,13 @@
* @license AGPLv3 * @license AGPLv3
*/ */
import { PSConnection, PSLoginServer } from './client-connection'; import { type PSConnection, PSLoginServer } from './client-connection';
import {PSModel, PSStreamModel} from './client-core'; import { PSModel, PSStreamModel } from './client-core';
import type {PSRouter} from './panels'; import type { PSRouter } from './panels';
import type {ChatRoom} from './panel-chat'; import type { ChatRoom } from './panel-chat';
import type {MainMenuRoom} from './panel-mainmenu'; import type { MainMenuRoom } from './panel-mainmenu';
import {toID, type ID} from './battle-dex'; import { toID, type ID } from './battle-dex';
import {BattleTextParser, type Args} from './battle-text-parser'; import { BattleTextParser, type Args } from './battle-text-parser';
/********************************************************************** /**********************************************************************
* Prefs * Prefs
@ -24,9 +24,9 @@ import {BattleTextParser, type Args} from './battle-text-parser';
/** /**
* String that contains only lowercase alphanumeric characters. * String that contains only lowercase alphanumeric characters.
*/ */
export type RoomID = string & {__isRoomID: true}; export type RoomID = string & { __isRoomID: true };
const PSPrefsDefaults: {[key: string]: any} = {}; const PSPrefsDefaults: { [key: string]: any } = {};
/** /**
* Tracks user preferences, stored in localStorage. Contains most local * Tracks user preferences, stored in localStorage. Contains most local
@ -54,7 +54,7 @@ class PSPrefs extends PSStreamModel<string | null> {
* table. Uses 1 and 0 instead of true/false for JSON packing * table. Uses 1 and 0 instead of true/false for JSON packing
* reasons. * reasons.
*/ */
showjoins: {[serverid: string]: {[roomid: string]: 1 | 0}} | null = null; showjoins: { [serverid: string]: { [roomid: string]: 1 | 0 } } | null = null;
/** /**
* true = one panel, false = two panels, left and right * true = one panel, false = two panels, left and right
*/ */
@ -66,7 +66,7 @@ class PSPrefs extends PSStreamModel<string | null> {
notifvolume = 50; notifvolume = 50;
storageEngine: 'localStorage' | 'iframeLocalStorage' | '' = ''; storageEngine: 'localStorage' | 'iframeLocalStorage' | '' = '';
storage: {[k: string]: any} = {}; storage: { [k: string]: any } = {};
readonly origin = `https://${Config.routes.client}`; readonly origin = `https://${Config.routes.client}`;
constructor() { constructor() {
super(); super();
@ -116,9 +116,9 @@ class PSPrefs extends PSStreamModel<string | null> {
fixPrefs(newPrefs: any) { fixPrefs(newPrefs: any) {
const oldShowjoins = newPrefs['showjoins']; const oldShowjoins = newPrefs['showjoins'];
if (oldShowjoins !== undefined && typeof oldShowjoins !== 'object') { if (oldShowjoins !== undefined && typeof oldShowjoins !== 'object') {
const showjoins: {[serverid: string]: {[roomid: string]: 1 | 0}} = {}; const showjoins: { [serverid: string]: { [roomid: string]: 1 | 0 } } = {};
const serverShowjoins: {[roomid: string]: 1 | 0} = {global: (oldShowjoins ? 1 : 0)}; const serverShowjoins: { [roomid: string]: 1 | 0 } = { global: (oldShowjoins ? 1 : 0) };
const showroomjoins = newPrefs['showroomjoins'] as {[roomid: string]: boolean}; const showroomjoins = newPrefs['showroomjoins'] as { [roomid: string]: boolean };
for (const roomid in showroomjoins) { for (const roomid in showroomjoins) {
serverShowjoins[roomid] = (showroomjoins[roomid] ? 1 : 0); serverShowjoins[roomid] = (showroomjoins[roomid] ? 1 : 0);
} }
@ -172,7 +172,7 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
/** false if it uses the ladder in the website */ /** false if it uses the ladder in the website */
usesLocalLadder = false; usesLocalLadder = false;
list: Team[] = []; list: Team[] = [];
byKey: {[key: string]: Team | undefined} = {}; byKey: { [key: string]: Team | undefined } = {};
deletedTeams: [Team, number][] = []; deletedTeams: [Team, number][] = [];
constructor() { constructor() {
super(); super();
@ -204,7 +204,7 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
return; return;
} }
if (buffer.charAt(0) === '[' && !buffer.trim().includes('\n')) { if (buffer.startsWith('[') && !buffer.trim().includes('\n')) {
this.unpackOldBuffer(buffer); this.unpackOldBuffer(buffer);
return; return;
} }
@ -243,7 +243,6 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
unpackOldBuffer(buffer: string) { unpackOldBuffer(buffer: string) {
alert(`Your team storage format is too old for PS. You'll need to upgrade it at https://${Config.routes.client}/recoverteams.html`); alert(`Your team storage format is too old for PS. You'll need to upgrade it at https://${Config.routes.client}/recoverteams.html`);
this.list = []; this.list = [];
return;
} }
packAll(teams: Team[]) { packAll(teams: Team[]) {
return teams.map(team => ( return teams.map(team => (
@ -264,7 +263,7 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
let slashIndex = line.lastIndexOf('/', pipeIndex); let slashIndex = line.lastIndexOf('/', pipeIndex);
if (slashIndex < 0) slashIndex = bracketIndex; // line.slice(slashIndex + 1, pipeIndex) will be '' if (slashIndex < 0) slashIndex = bracketIndex; // line.slice(slashIndex + 1, pipeIndex) will be ''
let format = bracketIndex > 0 ? line.slice(0, bracketIndex) : 'gen7'; let format = bracketIndex > 0 ? line.slice(0, bracketIndex) : 'gen7';
if (format.slice(0, 3) !== 'gen') format = 'gen6' + format; if (!format.startsWith('gen')) format = 'gen6' + format;
const name = line.slice(slashIndex + 1, pipeIndex); const name = line.slice(slashIndex + 1, pipeIndex);
return { return {
name, name,
@ -290,7 +289,7 @@ class PSUser extends PSModel {
avatar = "1"; avatar = "1";
setName(fullName: string, named: boolean, avatar: string) { setName(fullName: string, named: boolean, avatar: string) {
const loggingIn = (!this.named && named); const loggingIn = (!this.named && named);
const {name, group} = BattleTextParser.parseNameParts(fullName); const { name, group } = BattleTextParser.parseNameParts(fullName);
this.name = name; this.name = name;
this.group = group; this.group = group;
this.userid = toID(name); this.userid = toID(name);
@ -340,7 +339,7 @@ class PSServer {
registered = Config.defaultserver.registered; registered = Config.defaultserver.registered;
prefix = '/showdown'; prefix = '/showdown';
protocol: 'http' | 'https' = Config.defaultserver.httpport ? 'https' : 'http'; protocol: 'http' | 'https' = Config.defaultserver.httpport ? 'https' : 'http';
groups: {[symbol: string]: PSGroup} = { groups: { [symbol: string]: PSGroup } = {
'~': { '~': {
name: "Administrator (~)", name: "Administrator (~)",
type: 'leadership', type: 'leadership',
@ -458,14 +457,14 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
* In particular, this is `true` after sending `/join`, and `false` * In particular, this is `true` after sending `/join`, and `false`
* after sending `/leave`, even before the server responds. * after sending `/leave`, even before the server responds.
*/ */
connected: boolean = false; connected = false;
/** /**
* Can this room even be connected to at all? * Can this room even be connected to at all?
* `true` = pass messages from the server to subscribers * `true` = pass messages from the server to subscribers
* `false` = throw an error if we receive messages from the server * `false` = throw an error if we receive messages from the server
*/ */
readonly canConnect: boolean = false; readonly canConnect: boolean = false;
connectWhenLoggedIn: boolean = false; connectWhenLoggedIn = false;
onParentEvent: ((eventId: 'focus' | 'keydown', e?: Event) => false | void) | null = null; onParentEvent: ((eventId: 'focus' | 'keydown', e?: Event) => false | void) | null = null;
width = 0; width = 0;
@ -491,7 +490,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
if (options.rightPopup) this.rightPopup = true; if (options.rightPopup) this.rightPopup = true;
if (options.connected) this.connected = true; if (options.connected) this.connected = true;
} }
notify(options: {title: string, body?: string, noAutoDismiss?: boolean, id?: string}) { notify(options: { title: string, body?: string, noAutoDismiss?: boolean, id?: string }) {
if (options.noAutoDismiss && !options.id) { if (options.noAutoDismiss && !options.id) {
throw new Error(`Must specify id for manual dismissing`); throw new Error(`Must specify id for manual dismissing`);
} }
@ -528,7 +527,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
break; break;
} case 'tempnotify': { } case 'tempnotify': {
const [, id, title, body, toHighlight] = args; const [, id, title, body, toHighlight] = args;
this.notify({title, body, id}); this.notify({ title, body, id });
break; break;
} case 'tempnotifyoff': { } case 'tempnotifyoff': {
const [, id] = args; const [, id] = args;
@ -540,7 +539,8 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
} else { } else {
throw new Error(`This room is not designed to receive messages`); throw new Error(`This room is not designed to receive messages`);
} }
}} }
}
} }
handleMessage(line: string) { handleMessage(line: string) {
if (!line.startsWith('/') || line.startsWith('//')) return false; if (!line.startsWith('/') || line.startsWith('//')) return false;
@ -551,7 +551,8 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
case 'logout': { case 'logout': {
PS.user.logOut(); PS.user.logOut();
return true; return true;
}} }
}
return false; return false;
} }
send(msg: string, direct?: boolean) { send(msg: string, direct?: boolean) {
@ -570,7 +571,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
class PlaceholderRoom extends PSRoom { class PlaceholderRoom extends PSRoom {
queue = [] as Args[]; queue = [] as Args[];
override readonly classType: 'placeholder' = 'placeholder'; override readonly classType = 'placeholder';
override receiveLine(args: Args) { override receiveLine(args: Args) {
this.queue.push(args); this.queue.push(args);
} }
@ -580,7 +581,7 @@ class PlaceholderRoom extends PSRoom {
* PS * PS
*********************************************************************/ *********************************************************************/
type RoomType = {Model?: typeof PSRoom, Component: any, title?: string}; type RoomType = { Model?: typeof PSRoom, Component: any, title?: string };
/** /**
* This model updates: * This model updates:
@ -609,7 +610,7 @@ export const PS = new class extends PSModel {
router: PSRouter = null!; router: PSRouter = null!;
rooms: {[roomid: string]: PSRoom | undefined} = {}; rooms: { [roomid: string]: PSRoom | undefined } = {};
roomTypes: { roomTypes: {
[type: string]: RoomType | undefined, [type: string]: RoomType | undefined,
} = {}; } = {};
@ -667,7 +668,7 @@ export const PS = new class extends PSModel {
* the Rooms panel and clicking "Hide") * the Rooms panel and clicking "Hide")
* *
* Will NOT be true if only one panel fits onto the screen at the * Will NOT be true if only one panel fits onto the screen at the
* moment, but resizing will display multiple panels  for that, * moment, but resizing will display multiple panels for that,
* check `PS.leftRoomWidth === 0` * check `PS.leftRoomWidth === 0`
*/ */
onePanelMode = false; onePanelMode = false;
@ -688,7 +689,7 @@ export const PS = new class extends PSModel {
* for security reasons it's impossible to know what they are until * for security reasons it's impossible to know what they are until
* they're dropped. * they're dropped.
*/ */
dragging: {type: 'room', roomid: RoomID} | null = null; dragging: { type: 'room', roomid: RoomID } | null = null;
/** Tracks whether or not to display the "Use arrow keys" hint */ /** Tracks whether or not to display the "Use arrow keys" hint */
arrowKeysUsed = false; arrowKeysUsed = false;
@ -841,7 +842,8 @@ export const PS = new class extends PSModel {
} }
this.update(); this.update();
continue; continue;
}} }
}
if (room) room.receiveLine(args); if (room) room.receiveLine(args);
} }
if (room) room.update(isInit ? [`initdone`] : null); if (room) room.update(isInit ? [`initdone`] : null);
@ -1062,7 +1064,7 @@ export const PS = new class extends PSModel {
options.id = `pm-${options.id.slice(10)}` as RoomID; options.id = `pm-${options.id.slice(10)}` as RoomID;
options.challengeMenuOpen = true; options.challengeMenuOpen = true;
} }
if (options.id.startsWith('pm-') && options.id.indexOf('-', 3) < 0) { if (options.id.startsWith('pm-') && !options.id.includes('-', 3)) {
const userid1 = PS.user.userid; const userid1 = PS.user.userid;
const userid2 = options.id.slice(3); const userid2 = options.id.slice(3);
options.id = `pm-${[userid1, userid2].sort().join('-')}` as RoomID; options.id = `pm-${[userid1, userid2].sort().join('-')}` as RoomID;
@ -1174,7 +1176,7 @@ export const PS = new class extends PSModel {
} }
join(roomid: RoomID, side?: PSRoomLocation | null, noFocus?: boolean) { join(roomid: RoomID, side?: PSRoomLocation | null, noFocus?: boolean) {
if (this.room.id === roomid) return; if (this.room.id === roomid) return;
this.addRoom({id: roomid, side}, noFocus); this.addRoom({ id: roomid, side }, noFocus);
this.update(); this.update();
} }
leave(roomid: RoomID) { leave(roomid: RoomID) {

View File

@ -1,26 +1,28 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
// dex data // dex data
/////////// ///////////
declare var BattleText: {[id: string]: {[templateName: string]: string}}; declare const BattleText: { [id: string]: { [templateName: string]: string } };
declare var BattleFormats: {[id: string]: import('./panel-teamdropdown').FormatData}; declare const BattleFormats: { [id: string]: import('./panel-teamdropdown').FormatData };
declare var BattlePokedex: any; declare const BattlePokedex: any;
declare var BattleMovedex: any; declare const BattleMovedex: any;
declare var BattleAbilities: any; declare const BattleAbilities: any;
declare var BattleItems: any; declare const BattleItems: any;
declare var BattleAliases: any; declare const BattleAliases: any;
declare var BattleStatuses: any; declare const BattleStatuses: any;
declare var BattlePokemonSprites: any; declare const BattlePokemonSprites: any;
declare var BattlePokemonSpritesBW: any; declare const BattlePokemonSpritesBW: any;
declare var NonBattleGames: {[id: string]: string}; declare const NonBattleGames: { [id: string]: string };
// PS globals // PS globals
///////////// /////////////
declare var Config: any; declare const Config: any;
declare var Replays: any; declare const Replays: any;
declare var exports: any; declare const exports: any;
type AnyObject = {[k: string]: any}; type AnyObject = { [k: string]: any };
declare var app: {user: AnyObject, rooms: AnyObject, ignore?: AnyObject}; declare const app: { user: AnyObject, rooms: AnyObject, ignore?: AnyObject };
interface Window { interface Window {
[k: string]: any; [k: string]: any;

View File

@ -14,7 +14,7 @@
const MAX_UNDO_HISTORY = 100; const MAX_UNDO_HISTORY = 100;
export type MiniEditPlugin = new (editor: MiniEdit) => unknown; export type MiniEditPlugin = new (editor: MiniEdit) => unknown;
export type MiniEditSelection = {start: number, end: number} | null; export type MiniEditSelection = { start: number, end: number } | null;
export class MiniEdit { export class MiniEdit {
static plugins: MiniEditPlugin[] = []; static plugins: MiniEditPlugin[] = [];
@ -31,7 +31,6 @@ export class MiniEdit {
* it doesn't already exist and the user types a newline at the end * it doesn't already exist and the user types a newline at the end
* of the text, it wouldn't appear. * of the text, it wouldn't appear.
*/ */
// tslint:disable-next-line
_setContent: (text: string) => void; _setContent: (text: string) => void;
pushHistory?: (text: string, selection: MiniEditSelection) => void; pushHistory?: (text: string, selection: MiniEditSelection) => void;
onKeyDown = (ev: KeyboardEvent) => { onKeyDown = (ev: KeyboardEvent) => {
@ -41,7 +40,9 @@ export class MiniEdit {
} }
}; };
constructor(el: HTMLElement, options: {setContent: MiniEdit['_setContent'], onKeyDown?: (ev: KeyboardEvent) => void}) { constructor(
el: HTMLElement, options: { setContent: MiniEdit['_setContent'], onKeyDown?: (ev: KeyboardEvent) => void }
) {
this.element = el; this.element = el;
this._setContent = options.setContent; this._setContent = options.setContent;
@ -55,7 +56,6 @@ export class MiniEdit {
}); });
this.element.addEventListener('keydown', this.onKeyDown); this.element.addEventListener('keydown', this.onKeyDown);
// tslint:disable-next-line
for (const Plugin of MiniEdit.plugins) new Plugin(this); for (const Plugin of MiniEdit.plugins) new Plugin(this);
} }
@ -79,7 +79,7 @@ export class MiniEdit {
this.pushHistory?.(text, selection); this.pushHistory?.(text, selection);
} }
getValue(): string { getValue(): string {
let text = this.element.textContent || ''; const text = this.element.textContent || '';
if (text.endsWith('\n')) return text.slice(0, -1); if (text.endsWith('\n')) return text.slice(0, -1);
return text; return text;
} }
@ -90,7 +90,7 @@ export class MiniEdit {
const selection = this.getSelection()!; const selection = this.getSelection()!;
const oldContent = this.getValue(); const oldContent = this.getValue();
const newText = oldContent.slice(0, selection.start) + text + oldContent.slice(selection.end); const newText = oldContent.slice(0, selection.start) + text + oldContent.slice(selection.end);
this.setValue(newText, {start: selection.start + text.length, end: selection.start + text.length}); this.setValue(newText, { start: selection.start + text.length, end: selection.start + text.length });
} }
getSelection(): MiniEditSelection { getSelection(): MiniEditSelection {
@ -116,7 +116,7 @@ export class MiniEdit {
}); });
} }
return (start === null || end === null) ? null : {start, end}; return (start === null || end === null) ? null : { start, end };
} }
setSelection(sel: MiniEditSelection): void { setSelection(sel: MiniEditSelection): void {
@ -149,7 +149,7 @@ export class MiniEdit {
} }
} }
select(): void { select(): void {
this.setSelection({start: 0, end: this.getValue().length}); this.setSelection({ start: 0, end: this.getValue().length });
} }
} }
@ -174,11 +174,11 @@ export class MiniEditUndoPlugin {
editor: MiniEdit; editor: MiniEdit;
undoIndex: number | null = null; undoIndex: number | null = null;
ignoreInput = false; ignoreInput = false;
history: {text: string, selection: MiniEditSelection}[] = []; history: { text: string, selection: MiniEditSelection }[] = [];
constructor(editor: MiniEdit) { constructor(editor: MiniEdit) {
this.editor = editor; this.editor = editor;
this.history.push({text: editor.getValue(), selection: {start: 0, end: 0}}); this.history.push({ text: editor.getValue(), selection: { start: 0, end: 0 } });
this.editor.pushHistory = this.onPushHistory; this.editor.pushHistory = this.onPushHistory;
editor.element.addEventListener('keydown', this.onKeyDown); editor.element.addEventListener('keydown', this.onKeyDown);
@ -197,7 +197,7 @@ export class MiniEditUndoPlugin {
this.undoIndex = null; this.undoIndex = null;
} }
this.history.push({text, selection}); this.history.push({ text, selection });
if (this.history.length > MAX_UNDO_HISTORY) this.history.shift(); if (this.history.length > MAX_UNDO_HISTORY) this.history.shift();
}; };
@ -227,7 +227,7 @@ export class MiniEditUndoPlugin {
return; return;
} }
const {text, selection} = this.history[this.undoIndex]; const { text, selection } = this.history[this.undoIndex];
this.ignoreInput = true; this.ignoreInput = true;
this.editor.setValue(text, selection); this.editor.setValue(text, selection);
}; };

View File

@ -6,18 +6,18 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {PS, PSRoom, type RoomOptions, type RoomID} from "./client-main"; import { PS, PSRoom, type RoomOptions, type RoomID } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import {ChatLog, ChatRoom, ChatTextEntry, ChatUserList} from "./panel-chat"; import { ChatLog, ChatRoom, ChatTextEntry, ChatUserList } from "./panel-chat";
import {FormatDropdown} from "./panel-mainmenu"; import { FormatDropdown } from "./panel-mainmenu";
import {Battle, Pokemon, type ServerPokemon} from "./battle"; import { Battle, type Pokemon, type ServerPokemon } from "./battle";
import {BattleScene} from "./battle-animations"; import { BattleScene } from "./battle-animations";
import { Dex, toID } from "./battle-dex"; import { Dex, toID } from "./battle-dex";
import { import {
BattleChoiceBuilder, type BattleMoveRequest, type BattleRequest, type BattleRequestSideInfo, BattleChoiceBuilder, type BattleMoveRequest, type BattleRequest, type BattleRequestSideInfo,
type BattleSwitchRequest, type BattleTeamRequest type BattleSwitchRequest, type BattleTeamRequest,
} from "./battle-choices"; } from "./battle-choices";
import type {Args} from "./battle-text-parser"; import type { Args } from "./battle-text-parser";
type BattleDesc = { type BattleDesc = {
id: RoomID, id: RoomID,
@ -69,29 +69,41 @@ class BattlesPanel extends PSRoomPanel<BattlesRoom> {
override render() { override render() {
const room = this.props.room; const room = this.props.room;
return <PSPanelWrapper room={room} scrollable><div class="pad"> return <PSPanelWrapper room={room} scrollable><div class="pad">
<button class="button" style="float:right;font-size:10pt;margin-top:3px" name="close"><i class="fa fa-times"></i> Close</button> <button class="button" style="float:right;font-size:10pt;margin-top:3px" name="close">
<i class="fa fa-times"></i> Close
</button>
<div class="roomlist"> <div class="roomlist">
<p> <p>
<button class="button" name="refresh" onClick={this.refresh}><i class="fa fa-refresh"></i> Refresh</button> <span style={Dex.getPokemonIcon('meloetta-pirouette') + ';display:inline-block;vertical-align:middle'} class="picon" title="Meloetta is PS's mascot! The Pirouette forme is Fighting-type, and represents our battles."></span> <button class="button" name="refresh" onClick={this.refresh}><i class="fa fa-refresh"></i> Refresh</button> {}
<span
style={Dex.getPokemonIcon('meloetta-pirouette') + ';display:inline-block;vertical-align:middle'} class="picon"
title="Meloetta is PS's mascot! The Pirouette forme is Fighting-type, and represents our battles."
></span>
</p> </p>
<p> <p>
<label class="label">Format:</label><FormatDropdown onChange={this.changeFormat} /> <label class="label">Format:</label><FormatDropdown onChange={this.changeFormat} />
</p> </p>
{/* <label>Minimum Elo: <select name="elofilter"><option value="none">None</option><option value="1100">1100</option><option value="1300">1300</option><option value="1500">1500</option><option value="1700">1700</option><option value="1900">1900</option></select></label> {/* <label>
Minimum Elo: <select name="elofilter">
<option value="none">None</option><option value="1100">1100</option><option value="1300">1300</option>
<option value="1500">1500</option><option value="1700">1700</option><option value="1900">1900</option>
</select>
</label>
<form class="search"> <form class="search">
<p> <p>
<input type="text" name="prefixsearch" class="textbox" placeholder="Username prefix"/><button type="submit" class="button">Search</button> <input type="text" name="prefixsearch" class="textbox" placeholder="Username prefix"/>
<button type="submit" class="button">Search</button>
</p> </p>
</form> */} </form> */}
<div class="list">{!room.battles ? <div class="list">{!room.battles ? (
<p>Loading...</p> <p>Loading...</p>
: !room.battles.length ? ) : !room.battles.length ? (
<p>No battles are going on</p> <p>No battles are going on</p>
: ) : (
room.battles.map(battle => this.renderBattleLink(battle)) room.battles.map(battle => this.renderBattleLink(battle))
}</div> )}</div>
</div> </div>
</div></PSPanelWrapper>; </div></PSPanelWrapper>;
} }
@ -129,7 +141,7 @@ class BattleRoom extends ChatRoom {
return true; return true;
} case 'ffto': case 'fastfowardto': { } case 'ffto': case 'fastfowardto': {
let turnNum = Number(target); let turnNum = Number(target);
if (target.charAt(0) === '+' || turnNum < 0) { if (target.startsWith('+') || turnNum < 0) {
turnNum += this.battle.turn; turnNum += this.battle.turn;
if (turnNum < 0) turnNum = 0; if (turnNum < 0) turnNum = 0;
} else if (target === 'end') { } else if (target === 'end') {
@ -169,7 +181,8 @@ class BattleRoom extends ChatRoom {
if (this.choices.isDone()) this.send(`/choose ${this.choices.toString()}`, true); if (this.choices.isDone()) this.send(`/choose ${this.choices.toString()}`, true);
this.update(null); this.update(null);
return true; return true;
}} }
}
return super.handleMessage(line); return super.handleMessage(line);
} }
} }
@ -184,7 +197,7 @@ class BattleDiv extends preact.Component {
} }
function MoveButton(props: { function MoveButton(props: {
children: string, cmd: string, moveData: {pp: number, maxpp: number}, type: Dex.TypeName, tooltip: string, children: string, cmd: string, moveData: { pp: number, maxpp: number }, type: Dex.TypeName, tooltip: string,
}) { }) {
return <button name="cmd" value={props.cmd} class={`type-${props.type} has-tooltip`} data-tooltip={props.tooltip}> return <button name="cmd" value={props.cmd} class={`type-${props.type} has-tooltip`} data-tooltip={props.tooltip}>
{props.children}<br /> {props.children}<br />
@ -198,7 +211,7 @@ function PokemonButton(props: {
if (!pokemon) { if (!pokemon) {
return <button return <button
name="cmd" value={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`} name="cmd" value={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`}
style={{opacity: props.disabled === 'fade' ? 0.5 : 1}} data-tooltip={props.tooltip} style={{ opacity: props.disabled === 'fade' ? 0.5 : 1 }} data-tooltip={props.tooltip}
> >
(empty slot) (empty slot)
</button>; </button>;
@ -213,14 +226,14 @@ function PokemonButton(props: {
return <button return <button
name="cmd" value={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`} name="cmd" value={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`}
style={{opacity: props.disabled === 'fade' ? 0.5 : 1}} data-tooltip={props.tooltip} style={{ opacity: props.disabled === 'fade' ? 0.5 : 1 }} data-tooltip={props.tooltip}
> >
<span class="picon" style={Dex.getPokemonIcon(pokemon)}></span> <span class="picon" style={Dex.getPokemonIcon(pokemon)}></span>
{pokemon.name} {pokemon.name}
{ {
!props.noHPBar && !pokemon.fainted && !props.noHPBar && !pokemon.fainted &&
<span class={hpColorClass}> <span class={hpColorClass}>
<span style={{width: Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1}}></span> <span style={{ width: Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1 }}></span>
</span> </span>
} }
{!props.noHPBar && pokemon.status && <span class={`status ${pokemon.status}`}></span>} {!props.noHPBar && pokemon.status && <span class={`status ${pokemon.status}`}></span>}
@ -343,20 +356,30 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
const atEnd = room.battle.atQueueEnd; const atEnd = room.battle.atQueueEnd;
return <div class="controls"> return <div class="controls">
<p> <p>
{atEnd ? {atEnd ? (
<button class="button disabled" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button> <button class="button disabled" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button>
: room.battle.paused ? ) : room.battle.paused ? (
<button class="button" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button> <button class="button" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button>
: ) : (
<button class="button" name="cmd" value="/pause"><i class="fa fa-pause"></i><br />Pause</button> <button class="button" name="cmd" value="/pause"><i class="fa fa-pause"></i><br />Pause</button>
} {} )} {}
<button class="button" name="cmd" value="/ffto -1"><i class="fa fa-step-backward"></i><br />Last turn</button> <button class="button" name="cmd" value="/ffto -1">
<button class={"button" + (atEnd ? " disabled" : "")} name="cmd" value="/ffto +1"><i class="fa fa-step-forward"></i><br />Skip turn</button> {} <i class="fa fa-step-backward"></i><br />Last turn
<button class="button" name="cmd" value="/ffto 0"><i class="fa fa-undo"></i><br />First turn</button> </button>
<button class={"button" + (atEnd ? " disabled" : "")} name="cmd" value="/ffto end"><i class="fa fa-fast-forward"></i><br />Skip to end</button> <button class={"button" + (atEnd ? " disabled" : "")} name="cmd" value="/ffto +1">
<i class="fa fa-step-forward"></i><br />Skip turn
</button> {}
<button class="button" name="cmd" value="/ffto 0">
<i class="fa fa-undo"></i><br />First turn
</button>
<button class={"button" + (atEnd ? " disabled" : "")} name="cmd" value="/ffto end">
<i class="fa fa-fast-forward"></i><br />Skip to end
</button>
</p> </p>
<p> <p>
<button class="button" name="cmd" value="/switchsides"><i class="fa fa-random"></i> Switch sides</button> <button class="button" name="cmd" value="/switchsides">
<i class="fa fa-random"></i> Switch sides
</button>
</p> </p>
</div>; </div>;
} }
@ -392,7 +415,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
return <button disabled>&nbsp;</button>; return <button disabled>&nbsp;</button>;
} }
const tooltip = `zmove|${moveData.name}|${pokemonIndex}`; const tooltip = `zmove|${moveData.name}|${pokemonIndex}`;
return <MoveButton cmd={`/move ${i + 1} zmove`} type={move.type} tooltip={tooltip} moveData={{pp: 1, maxpp: 1}}> return <MoveButton cmd={`/move ${i + 1} zmove`} type={move.type} tooltip={tooltip} moveData={{ pp: 1, maxpp: 1 }}>
{zMoveData.name} {zMoveData.name}
</MoveButton>; </MoveButton>;
}); });
@ -475,7 +498,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
<div class="switchmenu"> <div class="switchmenu">
{team.map((serverPokemon, i) => { {team.map((serverPokemon, i) => {
return <PokemonButton return <PokemonButton
pokemon={serverPokemon} cmd={``} noHPBar disabled={true} tooltip={`switchpokemon|${i}`} pokemon={serverPokemon} cmd="" noHPBar disabled tooltip={`switchpokemon|${i}`}
/>; />;
})} })}
</div> </div>
@ -660,15 +683,17 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
return <div class="controls"> return <div class="controls">
<div class="whatdo"> <div class="whatdo">
<button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button> <button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button>
{choices.alreadySwitchingIn.length > 0 ? {choices.alreadySwitchingIn.length > 0 ? (
[<button name="cmd" value="/cancel" class="button"><i class="fa fa-chevron-left"></i> Back</button>, [<button name="cmd" value="/cancel" class="button"><i class="fa fa-chevron-left"></i> Back</button>,
" What about the rest of your team? "] " What about the rest of your team? "]
: ) : (
"How will you start the battle? " "How will you start the battle? "
} )}
</div> </div>
<div class="switchcontrols"> <div class="switchcontrols">
<h3 class="switchselect">Choose {choices.alreadySwitchingIn.length <= 0 ? `lead` : `slot ${choices.alreadySwitchingIn.length + 1}`}</h3> <h3 class="switchselect">
Choose {choices.alreadySwitchingIn.length <= 0 ? `lead` : `slot ${choices.alreadySwitchingIn.length + 1}`}
</h3>
<div class="switchmenu"> <div class="switchmenu">
{this.renderTeamControls(request, choices)} {this.renderTeamControls(request, choices)}
<div style="clear:left"></div> <div style="clear:left"></div>
@ -681,7 +706,8 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
</div> </div>
</div> </div>
</div>; </div>;
}} }
}
return null; return null;
} }
override render() { override render() {
@ -689,7 +715,9 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
return <PSPanelWrapper room={room}> return <PSPanelWrapper room={room}>
<BattleDiv></BattleDiv> <BattleDiv></BattleDiv>
<ChatLog class="battle-log hasuserlist" room={this.props.room} onClick={this.focusIfNoSelection} left={640} noSubscription> <ChatLog
class="battle-log hasuserlist" room={this.props.room} onClick={this.focusIfNoSelection} left={640} noSubscription
>
{} {}
</ChatLog> </ChatLog>
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={640} /> <ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={640} />

View File

@ -6,20 +6,20 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import type {PSSubscription} from "./client-core"; import type { PSSubscription } from "./client-core";
import {PS, PSRoom, type RoomOptions, type RoomID, type Team} from "./client-main"; import { PS, PSRoom, type RoomOptions, type RoomID, type Team } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import {TeamForm} from "./panel-mainmenu"; import { TeamForm } from "./panel-mainmenu";
import {BattleLog} from "./battle-log"; import { BattleLog } from "./battle-log";
import type {Battle} from "./battle"; import type { Battle } from "./battle";
import {MiniEdit} from "./miniedit"; import { MiniEdit } from "./miniedit";
import {PSUtils, toID, type ID} from "./battle-dex"; import { PSUtils, toID, type ID } from "./battle-dex";
declare const formatText: any; declare const formatText: any; // from js/server/chat-formatter.js
export class ChatRoom extends PSRoom { export class ChatRoom extends PSRoom {
override readonly classType: 'chat' | 'battle' = 'chat'; override readonly classType: 'chat' | 'battle' = 'chat';
users: {[userid: string]: string} = {}; users: { [userid: string]: string } = {};
userCount = 0; userCount = 0;
override readonly canConnect = true; override readonly canConnect = true;
@ -92,7 +92,8 @@ export class ChatRoom extends PSRoom {
this.challengedFormat = null; this.challengedFormat = null;
this.update(null); this.update(null);
return false; return false;
}} }
}
return super.handleMessage(line); return super.handleMessage(line);
} }
openChallenge() { openChallenge() {
@ -219,7 +220,7 @@ export class ChatTextEntry extends preact.Component<{
getValue() { getValue() {
return this.miniedit ? this.miniedit.getValue() : this.textbox.value; return this.miniedit ? this.miniedit.getValue() : this.textbox.value;
} }
setValue(value: string, selection?: {start: number, end: number}) { setValue(value: string, selection?: { start: number, end: number }) {
if (this.miniedit) { if (this.miniedit) {
this.miniedit.setValue(value, selection); this.miniedit.setValue(value, selection);
} else { } else {
@ -256,7 +257,7 @@ export class ChatTextEntry extends preact.Component<{
} }
handleKey(ev: KeyboardEvent) { handleKey(ev: KeyboardEvent) {
const cmdKey = ((ev.metaKey ? 1 : 0) + (ev.ctrlKey ? 1 : 0) === 1) && !ev.altKey && !ev.shiftKey; const cmdKey = ((ev.metaKey ? 1 : 0) + (ev.ctrlKey ? 1 : 0) === 1) && !ev.altKey && !ev.shiftKey;
const anyModifier = ev.ctrlKey || ev.altKey || ev.metaKey || ev.shiftKey; // const anyModifier = ev.ctrlKey || ev.altKey || ev.metaKey || ev.shiftKey;
if (ev.keyCode === 13 && !ev.shiftKey) { // Enter key if (ev.keyCode === 13 && !ev.shiftKey) { // Enter key
return this.submit(); return this.submit();
} else if (ev.keyCode === 13 && this.miniedit) { // enter } else if (ev.keyCode === 13 && this.miniedit) { // enter
@ -285,19 +286,19 @@ export class ChatTextEntry extends preact.Component<{
} }
getSelection() { getSelection() {
return this.miniedit ? return this.miniedit ?
(this.miniedit.getSelection() || {start: 0, end: 0}) : (this.miniedit.getSelection() || { start: 0, end: 0 }) :
{start: this.textbox.selectionStart, end: this.textbox.selectionEnd}; { start: this.textbox.selectionStart, end: this.textbox.selectionEnd };
} }
setSelection(start: number, end: number) { setSelection(start: number, end: number) {
if (this.miniedit) { if (this.miniedit) {
this.miniedit.setSelection({start, end}); this.miniedit.setSelection({ start, end });
} else { } else {
this.textbox.setSelectionRange?.(start, end); this.textbox.setSelectionRange?.(start, end);
} }
} }
toggleFormatChar(formatChar: string) { toggleFormatChar(formatChar: string) {
let value = this.getValue(); let value = this.getValue();
let {start, end} = this.getSelection(); let { start, end } = this.getSelection();
// make sure start and end aren't midway through the syntax // make sure start and end aren't midway through the syntax
if (value.charAt(start) === formatChar && value.charAt(start - 1) === formatChar && if (value.charAt(start) === formatChar && value.charAt(start - 1) === formatChar &&
@ -333,23 +334,23 @@ export class ChatTextEntry extends preact.Component<{
end -= 2; end -= 2;
} }
this.setValue(value, {start, end}); this.setValue(value, { start, end });
return true; return true;
} }
override render() { override render() {
const OLD_TEXTBOX = false; const OLD_TEXTBOX = false;
return <div return <div
class="chat-log-add hasuserlist" onClick={this.focusIfNoSelection} style={{left: this.props.left || 0}} class="chat-log-add hasuserlist" onClick={this.focusIfNoSelection} style={{ left: this.props.left || 0 }}
> >
<form class="chatbox"> <form class="chatbox">
<label style={{color: BattleLog.usernameColor(PS.user.userid)}}>{PS.user.name}:</label> <label style={{ color: BattleLog.usernameColor(PS.user.userid) }}>{PS.user.name}:</label>
{OLD_TEXTBOX ? <textarea {OLD_TEXTBOX ? <textarea
class={this.props.room.connected ? 'textbox' : 'textbox disabled'} class={this.props.room.connected ? 'textbox' : 'textbox disabled'}
autofocus autofocus
rows={1} rows={1}
onInput={this.update} onInput={this.update}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
style={{resize: 'none', width: '100%', height: '16px', padding: '2px 3px 1px 3px'}} style={{ resize: 'none', width: '100%', height: '16px', padding: '2px 3px 1px 3px' }}
placeholder={PS.focusPreview(this.props.room)} placeholder={PS.focusPreview(this.props.room)}
/> : <ChatTextBox /> : <ChatTextBox
class={this.props.room.connected ? 'textbox' : 'textbox disabled'} class={this.props.room.connected ? 'textbox' : 'textbox disabled'}
@ -360,7 +361,7 @@ export class ChatTextEntry extends preact.Component<{
} }
} }
class ChatTextBox extends preact.Component<{placeholder: string, class: string}> { class ChatTextBox extends preact.Component<{ placeholder: string, class: string }> {
override shouldComponentUpdate() { override shouldComponentUpdate() {
return false; return false;
} }
@ -380,7 +381,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
// `display: none` element, but contentEditable boxes are pickier. // `display: none` element, but contentEditable boxes are pickier.
// Waiting for a 0 timeout turns out to be enough. // Waiting for a 0 timeout turns out to be enough.
setTimeout(() => { setTimeout(() => {
(this.base!.querySelector('textarea, pre.textbox') as HTMLElement).focus(); this.base!.querySelector<HTMLElement>('textarea, pre.textbox')!.focus();
}, 0); }, 0);
} }
focusIfNoSelection = () => { focusIfNoSelection = () => {
@ -452,13 +453,13 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
} }
} }
export class ChatUserList extends preact.Component<{room: ChatRoom, left?: number, minimized?: boolean}> { export class ChatUserList extends preact.Component<{ room: ChatRoom, left?: number, minimized?: boolean }> {
subscription: PSSubscription | null = null; subscription: PSSubscription | null = null;
override state = { override state = {
expanded: false, expanded: false,
}; };
toggleExpanded = () => { toggleExpanded = () => {
this.setState({expanded: !this.state.expanded}); this.setState({ expanded: !this.state.expanded });
}; };
override componentDidMount() { override componentDidMount() {
this.subscription = this.props.room.subscribe(msg => { this.subscription = this.props.room.subscribe(msg => {
@ -474,11 +475,14 @@ export class ChatUserList extends preact.Component<{room: ChatRoom, left?: numbe
PSUtils.sortBy(userList, ([id, name]) => ( PSUtils.sortBy(userList, ([id, name]) => (
[PS.server.getGroup(name.charAt(0)).order, !name.endsWith('@!'), id] [PS.server.getGroup(name.charAt(0)).order, !name.endsWith('@!'), id]
)); ));
return <ul class={'userlist' + (this.props.minimized ? (this.state.expanded ? ' userlist-maximized' : ' userlist-minimized') : '')} style={{left: this.props.left || 0}}> return <ul
class={'userlist' + (this.props.minimized ? (this.state.expanded ? ' userlist-maximized' : ' userlist-minimized') : '')}
style={{ left: this.props.left || 0 }}
>
<li class="userlist-count" onClick={this.toggleExpanded}><small>{room.userCount} users</small></li> <li class="userlist-count" onClick={this.toggleExpanded}><small>{room.userCount} users</small></li>
{userList.map(([userid, name]) => { {userList.map(([userid, name]) => {
const groupSymbol = name.charAt(0); const groupSymbol = name.charAt(0);
const group = PS.server.groups[groupSymbol] || {type: 'user', order: 0}; const group = PS.server.groups[groupSymbol] || { type: 'user', order: 0 };
let color; let color;
if (name.endsWith('@!')) { if (name.endsWith('@!')) {
name = name.slice(0, -2); name = name.slice(0, -2);
@ -490,13 +494,13 @@ export class ChatUserList extends preact.Component<{room: ChatRoom, left?: numbe
<em class={`group${['leadership', 'staff'].includes(group.type!) ? ' staffgroup' : ''}`}> <em class={`group${['leadership', 'staff'].includes(group.type!) ? ' staffgroup' : ''}`}>
{groupSymbol} {groupSymbol}
</em> </em>
{group.type === 'leadership' ? {group.type === 'leadership' ? (
<strong><em style={{color}}>{name.substr(1)}</em></strong> <strong><em style={{ color }}>{name.substr(1)}</em></strong>
: group.type === 'staff' ? ) : group.type === 'staff' ? (
<strong style={{color}}>{name.substr(1)}</strong> <strong style={{ color }}>{name.substr(1)}</strong>
: ) : (
<span style={{color}}>{name.substr(1)}</span> <span style={{ color }}>{name.substr(1)}</span>
} )}
</button></li>; </button></li>;
})} })}
</ul>; </ul>;
@ -505,7 +509,7 @@ export class ChatUserList extends preact.Component<{room: ChatRoom, left?: numbe
export class ChatLog extends preact.Component<{ export class ChatLog extends preact.Component<{
class: string, room: ChatRoom, onClick?: (e: Event) => void, children?: preact.ComponentChildren, class: string, room: ChatRoom, onClick?: (e: Event) => void, children?: preact.ComponentChildren,
left?: number, top?: number, noSubscription?: boolean; left?: number, top?: number, noSubscription?: boolean,
}> { }> {
log: BattleLog | null = null; log: BattleLog | null = null;
subscription: PSSubscription | null = null; subscription: PSSubscription | null = null;
@ -575,9 +579,10 @@ export class ChatLog extends preact.Component<{
} }
} }
render() { render() {
return <div class={this.props.class} role="log" onClick={this.props.onClick} style={{ return <div
left: this.props.left || 0, top: this.props.top || 0, class={this.props.class} role="log" onClick={this.props.onClick}
}}></div>; style={{ left: this.props.left || 0, top: this.props.top || 0 }}
></div>;
} }
} }

View File

@ -7,13 +7,14 @@
* @license AGPLv3 * @license AGPLv3
*/ */
import {PS, PSRoom, type RoomOptions} from "./client-main"; import { PS, PSRoom, type RoomOptions } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
// Example room with panel // Example room with panel
class ExampleRoom extends PSRoom { class ExampleRoom extends PSRoom {
override readonly classType: string = 'example'; override readonly classType: string = 'example';
// eslint-disable-next-line no-useless-constructor
constructor(options: RoomOptions) { constructor(options: RoomOptions) {
super(options); super(options);
} }

View File

@ -8,19 +8,19 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {PS, PSRoom} from "./client-main"; import { PS, PSRoom } from "./client-main";
import {Net} from "./client-connection"; import { Net } from "./client-connection";
import {PSPanelWrapper, PSRoomPanel, SanitizedHTML} from "./panels"; import { PSPanelWrapper, PSRoomPanel, SanitizedHTML } from "./panels";
import {BattleLog} from "./battle-log"; import { BattleLog } from "./battle-log";
import {toID} from "./battle-dex"; import { toID } from "./battle-dex";
export class LadderRoom extends PSRoom { export class LadderRoom extends PSRoom {
override readonly classType: string = 'ladder'; override readonly classType: string = 'ladder';
readonly format?: string = this.id.split('-')[1]; readonly format?: string = this.id.split('-')[1];
notice?: string; notice?: string;
searchValue: string = ''; searchValue = '';
lastSearch: string = ''; lastSearch = '';
loading: boolean = false; loading = false;
error?: string; error?: string;
ladderData?: string; ladderData?: string;
@ -53,7 +53,7 @@ export class LadderRoom extends PSRoom {
requestLadderData = (searchValue?: string) => { requestLadderData = (searchValue?: string) => {
const { teams } = PS; const { teams } = PS;
if (teams.usesLocalLadder) { if (teams.usesLocalLadder) {
this.send(`/cmd laddertop ${this.format} ${toID(this.searchValue)}`); this.send(`/cmd laddertop ${this.format!} ${toID(this.searchValue)}`);
} else if (this.format !== undefined) { } else if (this.format !== undefined) {
Net('/ladder.php') Net('/ladder.php')
.get({ .get({
@ -71,8 +71,8 @@ export class LadderRoom extends PSRoom {
}; };
} }
function LadderFormat(props: {room: LadderRoom}) { function LadderFormat(props: { room: LadderRoom }) {
const {room} = props; const { room } = props;
const { const {
format, searchValue, lastSearch, loading, error, ladderData, format, searchValue, lastSearch, loading, error, ladderData,
setSearchValue, setLastSearch, requestLadderData, setSearchValue, setLastSearch, requestLadderData,
@ -124,12 +124,12 @@ function LadderFormat(props: {room: LadderRoom}) {
} }
return <> return <>
<p> <p>
<button class="button" data-href="ladder" data-target="replace" > <button class="button" data-href="ladder" data-target="replace">
<i class="fa fa-refresh"></i> Refresh <i class="fa fa-refresh"></i> Refresh
</button> </button>
<RenderSearch/> <RenderSearch />
</p> </p>
<RenderHeader/> <RenderHeader />
<SanitizedHTML>{ladderData}</SanitizedHTML> <SanitizedHTML>{ladderData}</SanitizedHTML>
</>; </>;
}; };
@ -145,7 +145,7 @@ function LadderFormat(props: {room: LadderRoom}) {
class LadderPanel extends PSRoomPanel<LadderRoom> { class LadderPanel extends PSRoomPanel<LadderRoom> {
override componentDidMount() { override componentDidMount() {
const {room} = this.props; const { room } = this.props;
// Request ladder data either on mount or after BattleFormats are loaded // Request ladder data either on mount or after BattleFormats are loaded
if (BattleFormats && room.format !== undefined) room.requestLadderData(); if (BattleFormats && room.format !== undefined) room.requestLadderData();
this.subscriptions.push( this.subscriptions.push(
@ -170,8 +170,8 @@ class LadderPanel extends PSRoomPanel<LadderRoom> {
}) })
); );
} }
static Notice = (props: {notice: string | undefined}) => { static Notice = (props: { notice: string | undefined }) => {
const {notice} = props; const { notice } = props;
if (notice) { if (notice) {
return ( return (
<p> <p>
@ -185,7 +185,7 @@ class LadderPanel extends PSRoomPanel<LadderRoom> {
if (!BattleFormats) { if (!BattleFormats) {
return <p>Loading...</p>; return <p>Loading...</p>;
} }
let currentSection: string = ""; let currentSection = "";
let sections: JSX.Element[] = []; let sections: JSX.Element[] = [];
let formats: JSX.Element[] = []; let formats: JSX.Element[] = [];
for (const [key, format] of Object.entries(BattleFormats)) { for (const [key, format] of Object.entries(BattleFormats)) {
@ -217,8 +217,8 @@ class LadderPanel extends PSRoomPanel<LadderRoom> {
} }
return <>{sections}</>; return <>{sections}</>;
}; };
static ShowFormatList = (props: {room: LadderRoom}) => { static ShowFormatList = (props: { room: LadderRoom }) => {
const {room} = props; const { room } = props;
return <> return <>
<p> <p>
<a class="button" href={`/${Config.routes.users}/`} target="_blank"> <a class="button" href={`/${Config.routes.users}/`} target="_blank">
@ -235,7 +235,7 @@ class LadderPanel extends PSRoomPanel<LadderRoom> {
</>; </>;
}; };
override render() { override render() {
const {room} = this.props; const { room } = this.props;
return <PSPanelWrapper room={room} scrollable> return <PSPanelWrapper room={room} scrollable>
<div class="ladder pad"> <div class="ladder pad">
{room.format === undefined && ( {room.format === undefined && (

View File

@ -6,17 +6,17 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {PSLoginServer} from "./client-connection"; import { PSLoginServer } from "./client-connection";
import {PS, PSRoom, type RoomID, type Team} from "./client-main"; import { PS, PSRoom, type RoomID, type Team } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import type {BattlesRoom} from "./panel-battle"; import type { BattlesRoom } from "./panel-battle";
import type {ChatRoom} from "./panel-chat"; import type { ChatRoom } from "./panel-chat";
import type {LadderRoom} from "./panel-ladder"; import type { LadderRoom } from "./panel-ladder";
import type {RoomsRoom} from "./panel-rooms"; import type { RoomsRoom } from "./panel-rooms";
import {TeamBox} from "./panel-teamdropdown"; import { TeamBox } from "./panel-teamdropdown";
import type {UserRoom} from "./panel-topbar"; import type { UserRoom } from "./panel-topbar";
import {Dex, toID, type ID} from "./battle-dex"; import { Dex, toID, type ID } from "./battle-dex";
import type {Args} from "./battle-text-parser"; import type { Args } from "./battle-text-parser";
export type RoomInfo = { export type RoomInfo = {
title: string, desc?: string, userCount?: number, section?: string, spotlight?: string, subRooms?: string[], title: string, desc?: string, userCount?: number, section?: string, spotlight?: string, subRooms?: string[],
@ -24,14 +24,16 @@ export type RoomInfo = {
export class MainMenuRoom extends PSRoom { export class MainMenuRoom extends PSRoom {
override readonly classType: string = 'mainmenu'; override readonly classType: string = 'mainmenu';
userdetailsCache: {[userid: string]: { userdetailsCache: {
[userid: string]: {
userid: ID, userid: ID,
avatar?: string | number, avatar?: string | number,
status?: string, status?: string,
group?: string, group?: string,
customgroup?: string, customgroup?: string,
rooms?: {[roomid: string]: {isPrivate?: true, p1?: string, p2?: string}}, rooms?: { [roomid: string]: { isPrivate?: true, p1?: string, p2?: string } },
}} = {}; },
} = {};
roomsCache: { roomsCache: {
battleCount?: number, battleCount?: number,
userCount?: number, userCount?: number,
@ -75,7 +77,8 @@ export class MainMenuRoom extends PSRoom {
const [, message] = args; const [, message] = args;
alert(message.replace(/\|\|/g, '\n')); alert(message.replace(/\|\|/g, '\n'));
return; return;
}} }
}
const lobby = PS.rooms['lobby']; const lobby = PS.rooms['lobby'];
if (lobby) lobby.receiveLine(args); if (lobby) lobby.receiveLine(args);
} }
@ -109,9 +112,9 @@ export class MainMenuRoom extends PSRoom {
let column = 0; let column = 0;
window.NonBattleGames = {rps: 'Rock Paper Scissors'}; window.NonBattleGames = { rps: 'Rock Paper Scissors' };
for (let i = 3; i <= 9; i = i + 2) { for (let i = 3; i <= 9; i += 2) {
window.NonBattleGames['bestof' + i] = 'Best-of-' + i; window.NonBattleGames[`bestof${i}`] = `Best-of-${i}`;
} }
window.BattleFormats = {}; window.BattleFormats = {};
for (let j = 1; j < formatsList.length; j++) { for (let j = 1; j < formatsList.length; j++) {
@ -121,7 +124,7 @@ export class MainMenuRoom extends PSRoom {
isSection = false; isSection = false;
} else if (entry === ',LL') { } else if (entry === ',LL') {
PS.teams.usesLocalLadder = true; PS.teams.usesLocalLadder = true;
} else if (entry === '' || (entry.charAt(0) === ',' && !isNaN(Number(entry.slice(1))))) { } else if (entry === '' || (entry.startsWith(',') && !isNaN(Number(entry.slice(1))))) {
isSection = true; isSection = true;
if (entry) { if (entry) {
@ -158,16 +161,16 @@ export class MainMenuRoom extends PSRoom {
} }
} }
let id = toID(name); let id = toID(name);
let isTeambuilderFormat = !team && name.slice(-11) !== 'Custom Game'; let isTeambuilderFormat = !team && !name.endsWith('Custom Game');
let teambuilderFormat = '' as ID; let teambuilderFormat = '' as ID;
let teambuilderFormatName = ''; let teambuilderFormatName = '';
if (isTeambuilderFormat) { if (isTeambuilderFormat) {
teambuilderFormatName = name; teambuilderFormatName = name;
if (id.slice(0, 3) !== 'gen') { if (!id.startsWith('gen')) {
teambuilderFormatName = '[Gen 6] ' + name; teambuilderFormatName = '[Gen 6] ' + name;
} }
let parenPos = teambuilderFormatName.indexOf('('); let parenPos = teambuilderFormatName.indexOf('(');
if (parenPos > 0 && name.slice(-1) === ')') { if (parenPos > 0 && name.endsWith(')')) {
// variation of existing tier // variation of existing tier
teambuilderFormatName = teambuilderFormatName.slice(0, parenPos).trim(); teambuilderFormatName = teambuilderFormatName.slice(0, parenPos).trim();
} }
@ -214,7 +217,7 @@ export class MainMenuRoom extends PSRoom {
} }
// Match base formats to their variants, if they are unavailable in the server. // Match base formats to their variants, if they are unavailable in the server.
let multivariantFormats: {[id: string]: 1} = {}; let multivariantFormats: { [id: string]: 1 } = {};
for (let id in BattleFormats) { for (let id in BattleFormats) {
let teambuilderFormat = BattleFormats[BattleFormats[id].teambuilderFormat!]; let teambuilderFormat = BattleFormats[BattleFormats[id].teambuilderFormat!];
if (!teambuilderFormat || multivariantFormats[teambuilderFormat.id]) continue; if (!teambuilderFormat || multivariantFormats[teambuilderFormat.id]) continue;
@ -300,21 +303,21 @@ export class MainMenuRoom extends PSRoom {
class NewsPanel extends PSRoomPanel { class NewsPanel extends PSRoomPanel {
override render() { override render() {
return <PSPanelWrapper room={this.props.room} scrollable> return <PSPanelWrapper room={this.props.room} scrollable>
<div class="mini-window-body" dangerouslySetInnerHTML={{__html: PS.newsHTML}}></div> <div class="mini-window-body" dangerouslySetInnerHTML={{ __html: PS.newsHTML }}></div>
</PSPanelWrapper>; </PSPanelWrapper>;
} }
} }
class MainMenuPanel extends PSRoomPanel<MainMenuRoom> { class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
override focus() { override focus() {
(this.base!.querySelector('button.big') as HTMLButtonElement).focus(); this.base!.querySelector<HTMLButtonElement>('button.big')!.focus();
} }
submit = (e: Event) => { submit = (e: Event) => {
alert('todo: implement'); alert('todo: implement');
}; };
handleDragStart = (e: DragEvent) => { handleDragStart = (e: DragEvent) => {
const roomid = (e.currentTarget as HTMLElement).getAttribute('data-roomid') as RoomID; const roomid = (e.currentTarget as HTMLElement).getAttribute('data-roomid') as RoomID;
PS.dragging = {type: 'room', roomid}; PS.dragging = { type: 'room', roomid };
}; };
renderMiniRoom(room: PSRoom) { renderMiniRoom(room: PSRoom) {
const roomType = PS.roomTypes[room.type]; const roomType = PS.roomTypes[room.type];
@ -327,7 +330,9 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
return <div class="pmbox"> return <div class="pmbox">
<div class="mini-window"> <div class="mini-window">
<h3 draggable onDragStart={this.handleDragStart} data-roomid={roomid}> <h3 draggable onDragStart={this.handleDragStart} data-roomid={roomid}>
<button class="closebutton" name="closeRoom" value={roomid} aria-label="Close" tabIndex={-1}><i class="fa fa-times-circle"></i></button> <button class="closebutton" name="closeRoom" value={roomid} aria-label="Close" tabIndex={-1}>
<i class="fa fa-times-circle"></i>
</button>
<button class="minimizebutton" tabIndex={-1}><i class="fa fa-minus-circle"></i></button> <button class="minimizebutton" tabIndex={-1}><i class="fa fa-minus-circle"></i></button>
{room.title} {room.title}
</h3> </h3>
@ -339,13 +344,13 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
renderSearchButton() { renderSearchButton() {
if (PS.down) { if (PS.down) {
return <div class="menugroup" style="background: rgba(10,10,10,.6)"> return <div class="menugroup" style="background: rgba(10,10,10,.6)">
{PS.down === 'ddos' ? {PS.down === 'ddos' ? (
<p class="error"><strong>Pok&eacute;mon Showdown is offline due to a DDoS attack!</strong></p> <p class="error"><strong>Pok&eacute;mon Showdown is offline due to a DDoS attack!</strong></p>
: ) : (
<p class="error"><strong>Pok&eacute;mon Showdown is offline due to technical difficulties!</strong></p> <p class="error"><strong>Pok&eacute;mon Showdown is offline due to technical difficulties!</strong></p>
} )}
<p> <p>
<div style={{textAlign: 'center'}}> <div style={{ textAlign: 'center' }}>
<img width="96" height="96" src={`//${Config.routes.client}/sprites/gen5/teddiursa.png`} alt="" /> <img width="96" height="96" src={`//${Config.routes.client}/sprites/gen5/teddiursa.png`} alt="" />
</div> </div>
Bear with us as we freak out. Bear with us as we freak out.
@ -391,13 +396,13 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
</div> </div>
</div> </div>
</div> </div>
<div class="rightmenu" style={{display: PS.leftRoomWidth ? 'none' : 'block'}}> <div class="rightmenu" style={{ display: PS.leftRoomWidth ? 'none' : 'block' }}>
<div class="menugroup"> <div class="menugroup">
{PS.server.id === 'showdown' ? {PS.server.id === 'showdown' ? (
<p><button class={"mainmenu1" + onlineButton} name="joinRoom" value="rooms">Join chat</button></p> <p><button class={"mainmenu1" + onlineButton} name="joinRoom" value="rooms">Join chat</button></p>
: ) : (
<p><button class={"mainmenu1" + onlineButton} name="joinRoom" value="lobby">Join lobby chat</button></p> <p><button class={"mainmenu1" + onlineButton} name="joinRoom" value="lobby">Join lobby chat</button></p>
} )}
</div> </div>
</div> </div>
<div class="mainmenufooter"> <div class="mainmenufooter">
@ -415,7 +420,7 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
} }
} }
export class FormatDropdown extends preact.Component<{format?: string, onChange?: JSX.EventHandler<Event>}> { export class FormatDropdown extends preact.Component<{ format?: string, onChange?: JSX.EventHandler<Event> }> {
declare base?: HTMLButtonElement; declare base?: HTMLButtonElement;
format = '[Gen 7] Random Battle'; format = '[Gen 7] Random Battle';
change = (e: Event) => { change = (e: Event) => {
@ -439,7 +444,7 @@ export class FormatDropdown extends preact.Component<{format?: string, onChange?
} }
} }
class TeamDropdown extends preact.Component<{format: string}> { class TeamDropdown extends preact.Component<{ format: string }> {
teamFormat = ''; teamFormat = '';
teamKey = ''; teamKey = '';
change = () => { change = () => {
@ -456,7 +461,7 @@ class TeamDropdown extends preact.Component<{format: string}> {
render() { render() {
const teamFormat = PS.teams.teambuilderFormat(this.props.format); const teamFormat = PS.teams.teambuilderFormat(this.props.format);
const formatData = window.BattleFormats?.[teamFormat]; const formatData = window.BattleFormats?.[teamFormat];
if (formatData && formatData.team) { if (formatData?.team) {
return <button class="select teamselect preselected" name="team" value="random" disabled> return <button class="select teamselect preselected" name="team" value="random" disabled>
<div class="team"> <div class="team">
<strong>Random team</strong> <strong>Random team</strong>
@ -489,14 +494,14 @@ export class TeamForm extends preact.Component<{
children: preact.ComponentChildren, class?: string, format?: string, children: preact.ComponentChildren, class?: string, format?: string,
onSubmit: null | ((e: Event, format: string, team?: Team) => void), onSubmit: null | ((e: Event, format: string, team?: Team) => void),
}> { }> {
override state = {format: '[Gen 7] Random Battle'}; override state = { format: '[Gen 7] Random Battle' };
changeFormat = (e: Event) => { changeFormat = (e: Event) => {
this.setState({format: (e.target as HTMLButtonElement).value}); this.setState({ format: (e.target as HTMLButtonElement).value });
}; };
submit = (e: Event) => { submit = (e: Event) => {
e.preventDefault(); e.preventDefault();
const format = (this.base!.querySelector('button[name=format]') as HTMLButtonElement).value; const format = this.base!.querySelector<HTMLButtonElement>('button[name=format]')!.value;
const teamKey = (this.base!.querySelector('button[name=team]') as HTMLButtonElement).value; const teamKey = this.base!.querySelector<HTMLButtonElement>('button[name=team]')!.value;
const team = teamKey ? PS.teams.byKey[teamKey] : undefined; const team = teamKey ? PS.teams.byKey[teamKey] : undefined;
if (this.props.onSubmit) this.props.onSubmit(e, format, team); if (this.props.onSubmit) this.props.onSubmit(e, format, team);
}; };

View File

@ -7,17 +7,17 @@
* @license MIT * @license MIT
*/ */
import {PS, PSRoom, type RoomOptions} from "./client-main"; import { PS, PSRoom, type RoomOptions } from "./client-main";
import {PSPanelWrapper, PSRoomPanel, SanitizedHTML} from "./panels"; import { PSPanelWrapper, PSRoomPanel, SanitizedHTML } from "./panels";
import {BattleLog} from "./battle-log"; import { BattleLog } from "./battle-log";
import type {Args} from "./battle-text-parser"; import type { Args } from "./battle-text-parser";
class PageRoom extends PSRoom { class PageRoom extends PSRoom {
override readonly classType: string = 'html'; override readonly classType: string = 'html';
readonly page?: string = this.id.split("-")[1]; readonly page?: string = this.id.split("-")[1];
override readonly canConnect = true; override readonly canConnect = true;
loading: boolean = true; loading = true;
htmlData?: string; htmlData?: string;
setHTMLData = (htmlData?: string) => { setHTMLData = (htmlData?: string) => {
@ -39,8 +39,7 @@ class PageRoom extends PSRoom {
} }
} }
function PageLadderHelp(props: {room: PageRoom}) { function PageLadderHelp(props: { room: PageRoom }) {
const {room} = props;
return <div class="ladder pad"> return <div class="ladder pad">
<p> <p>
<button name="selectFormat" data-href="ladder" data-target="replace"> <button name="selectFormat" data-href="ladder" data-target="replace">
@ -74,13 +73,13 @@ function PageLadderHelp(props: {room: PageRoom}) {
} }
class PagePanel extends PSRoomPanel<PageRoom> { class PagePanel extends PSRoomPanel<PageRoom> {
clientRooms: {[key: string]: JSX.Element} = {'ladderhelp': <PageLadderHelp room={this.props.room}/>}; clientRooms: { [key: string]: JSX.Element } = { 'ladderhelp': <PageLadderHelp room={this.props.room} /> };
/** /**
* @return true to prevent line from being sent to server * @return true to prevent line from being sent to server
*/ */
override receiveLine(args: Args) { override receiveLine(args: Args) {
const {room} = this.props; const { room } = this.props;
switch (args[0]) { switch (args[0]) {
case 'title': case 'title':
room.title = args[1]; room.title = args[1];
@ -88,7 +87,7 @@ class PagePanel extends PSRoomPanel<PageRoom> {
return true; return true;
case 'tempnotify': { case 'tempnotify': {
const [, id, title, body] = args; const [, id, title, body] = args;
room.notify({title, body, id}); room.notify({ title, body, id });
return true; return true;
} }
case 'tempnotifyoff': { case 'tempnotifyoff': {
@ -114,7 +113,7 @@ class PagePanel extends PSRoomPanel<PageRoom> {
} }
} }
override render() { override render() {
const {room} = this.props; const { room } = this.props;
let renderPage; let renderPage;
if (room.page !== undefined && this.clientRooms[room.page]) { if (room.page !== undefined && this.clientRooms[room.page]) {
renderPage = this.clientRooms[room.page]; renderPage = this.clientRooms[room.page];

View File

@ -5,10 +5,10 @@
* @license AGPLv3 * @license AGPLv3
*/ */
import {PS, PSRoom, type RoomID, type RoomOptions} from "./client-main"; import { PS, PSRoom, type RoomID, type RoomOptions } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import type {RoomInfo} from "./panel-mainmenu"; import type { RoomInfo } from "./panel-mainmenu";
import {toID} from "./battle-dex"; import { toID } from "./battle-dex";
export class RoomsRoom extends PSRoom { export class RoomsRoom extends PSRoom {
override readonly classType: string = 'rooms'; override readonly classType: string = 'rooms';
@ -36,7 +36,7 @@ class RoomsPanel extends PSRoomPanel {
PS.update(); PS.update();
}; };
changeSearch = (e: Event) => { changeSearch = (e: Event) => {
const target = (e.currentTarget as HTMLInputElement); const target = e.currentTarget as HTMLInputElement;
if (target.selectionStart !== target.selectionEnd) return; if (target.selectionStart !== target.selectionEnd) return;
this.search = target.value; this.search = target.value;
this.forceUpdate(); this.forceUpdate();
@ -44,7 +44,7 @@ class RoomsPanel extends PSRoomPanel {
keyDownSearch = (e: KeyboardEvent) => { keyDownSearch = (e: KeyboardEvent) => {
this.lastKeyCode = e.keyCode; this.lastKeyCode = e.keyCode;
if (e.keyCode === 13) { if (e.keyCode === 13) {
const target = (e.currentTarget as HTMLInputElement); const target = e.currentTarget as HTMLInputElement;
let value = target.value; let value = target.value;
const arrowIndex = value.indexOf(' \u21d2 '); const arrowIndex = value.indexOf(' \u21d2 ');
if (arrowIndex >= 0) value = value.slice(arrowIndex + 3); if (arrowIndex >= 0) value = value.slice(arrowIndex + 3);
@ -86,7 +86,7 @@ class RoomsPanel extends PSRoomPanel {
room.title.replace(/[^A-Z0-9]+/g, '').toLowerCase().startsWith(searchid) room.title.replace(/[^A-Z0-9]+/g, '').toLowerCase().startsWith(searchid)
); );
const hidden = !exactMatch ? [{title: this.search, desc: "(Private room?)"}] : []; const hidden = !exactMatch ? [{ title: this.search, desc: "(Private room?)" }] : [];
const autoFill = this.lastKeyCode !== 127 && this.lastKeyCode >= 32; const autoFill = this.lastKeyCode !== 127 && this.lastKeyCode >= 32;
if (autoFill) { if (autoFill) {
@ -103,15 +103,15 @@ class RoomsPanel extends PSRoomPanel {
autoFillValue = ' \u21d2 ' + firstTitle; autoFillValue = ' \u21d2 ' + firstTitle;
} }
const oldSearch = this.search; const oldSearch = this.search;
const searchElem = this.base!.querySelector('input[type=search]') as HTMLInputElement; const searchElem = this.base!.querySelector<HTMLInputElement>('input[type=search]')!;
searchElem.value = oldSearch + autoFillValue; searchElem.value = oldSearch + autoFillValue;
searchElem.setSelectionRange(oldSearch.length, oldSearch.length + autoFillValue.length); searchElem.setSelectionRange(oldSearch.length, oldSearch.length + autoFillValue.length);
} }
return {start, abbr, hidden}; return { start, abbr, hidden };
} }
override focus() { override focus() {
(this.base!.querySelector('input[type=search]') as HTMLInputElement).focus(); this.base!.querySelector<HTMLInputElement>('input[type=search]')!.focus();
} }
override render() { override render() {
if (this.hidden && PS.isVisible(this.props.room)) this.hidden = false; if (this.hidden && PS.isVisible(this.props.room)) this.hidden = false;
@ -166,7 +166,7 @@ class RoomsPanel extends PSRoomPanel {
</div></PSPanelWrapper>; </div></PSPanelWrapper>;
} }
renderRoomList(title: string, rooms?: RoomInfo[]) { renderRoomList(title: string, rooms?: RoomInfo[]) {
if (!rooms || !rooms.length) return null; if (!rooms?.length) return null;
// Descending order // Descending order
const sortedRooms = rooms.sort((a, b) => (b.userCount || 0) - (a.userCount || 0)); const sortedRooms = rooms.sort((a, b) => (b.userCount || 0) - (a.userCount || 0));
return <div class="roomlist"> return <div class="roomlist">

View File

@ -6,18 +6,18 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {PS, PSRoom, type Team} from "./client-main"; import { PS, PSRoom, type Team } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import {PSTeambuilder} from "./panel-teamdropdown"; import { PSTeambuilder } from "./panel-teamdropdown";
import {Dex, toID, type ID} from "./battle-dex"; import { Dex, toID, type ID } from "./battle-dex";
import {DexSearch} from "./battle-dex-search"; import { DexSearch } from "./battle-dex-search";
import {PSSearchResults} from "./battle-searchresults"; import { PSSearchResults } from "./battle-searchresults";
class TeamRoom extends PSRoom { class TeamRoom extends PSRoom {
team: Team | null = null; team: Team | null = null;
} }
class TeamTextbox extends preact.Component<{team: Team}> { class TeamTextbox extends preact.Component<{ team: Team }> {
setInfo: { setInfo: {
species: string, species: string,
bottomY: number, bottomY: number,
@ -158,7 +158,9 @@ class TeamTextbox extends preact.Component<{team: Team}> {
} }
render() { render() {
return <div class="teameditor"> return <div class="teameditor">
<textarea class="textbox teamtextbox" onInput={this.input} onSelect={this.select} onClick={this.select} onKeyUp={this.select} /> <textarea
class="textbox teamtextbox" onInput={this.input} onSelect={this.select} onClick={this.select} onKeyUp={this.select}
/>
<textarea <textarea
class="textbox teamtextbox heighttester" style="visibility:hidden" tabIndex={-1} aria-hidden={true} class="textbox teamtextbox heighttester" style="visibility:hidden" tabIndex={-1} aria-hidden={true}
/> />
@ -177,18 +179,22 @@ class TeamTextbox extends preact.Component<{team: Team}> {
const left = (num % 12) * 40; const left = (num % 12) * 40;
const iconStyle = `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-sheet.png) no-repeat scroll -${left}px -${top}px`; const iconStyle = `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-sheet.png) no-repeat scroll -${left}px -${top}px`;
return <span class="picon" style={ return <span
`top:${prevOffset + 1}px;left:50px;position:absolute;${iconStyle}` class="picon" style={`top:${prevOffset + 1}px;left:50px;position:absolute;${iconStyle}`}
}></span>; ></span>;
})} })}
{this.activeOffsetY >= 0 && {this.activeOffsetY >= 0 && (
<div class="teaminnertextbox" style={{top: this.activeOffsetY - 1}}></div> <div class="teaminnertextbox" style={{ top: this.activeOffsetY - 1 }}></div>
} )}
</div> </div>
{this.activeType && <div class="searchresults" style={{top: this.activeSetIndex >= 0 ? this.setInfo[this.activeSetIndex].bottomY - 12 : 0}}> {this.activeType && (
<div
class="searchresults" style={{ top: this.activeSetIndex >= 0 ? this.setInfo[this.activeSetIndex].bottomY - 12 : 0 }}
>
<button class="button closesearch" onClick={this.closeMenu}><i class="fa fa-times"></i> Close</button> <button class="button closesearch" onClick={this.closeMenu}><i class="fa fa-times"></i> Close</button>
<PSSearchResults search={this.search} /> <PSSearchResults search={this.search} />
</div>} </div>
)}
</div>; </div>;
} }
} }
@ -223,7 +229,9 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
</button> </button>
<label class="label teamname"> <label class="label teamname">
Team name: Team name:
<input class="textbox" type="text" value={team.name} onInput={this.rename} onChange={this.rename} onKeyUp={this.rename} /> <input
class="textbox" type="text" value={team.name} onInput={this.rename} onChange={this.rename} onKeyUp={this.rename}
/>
</label> </label>
<TeamTextbox team={team} /> <TeamTextbox team={team} />
</div> </div>

View File

@ -5,10 +5,10 @@
* @license AGPLv3 * @license AGPLv3
*/ */
import {PS, PSRoom, type Team} from "./client-main"; import { PS, PSRoom, type Team } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import {TeamBox, TeamFolder} from "./panel-teamdropdown"; import { TeamBox, TeamFolder } from "./panel-teamdropdown";
import {PSUtils, type ID} from "./battle-dex"; import { PSUtils, type ID } from "./battle-dex";
class TeambuilderRoom extends PSRoom { class TeambuilderRoom extends PSRoom {
readonly DEFAULT_FORMAT = 'gen8' as ID; readonly DEFAULT_FORMAT = 'gen8' as ID;
@ -49,7 +49,8 @@ class TeambuilderRoom extends PSRoom {
PS.teams.undelete(); PS.teams.undelete();
this.update(null); this.update(null);
return true; return true;
}} }
}
// unrecognized command // unrecognized command
alert(`Unrecognized command: ${line}`); alert(`Unrecognized command: ${line}`);
@ -110,7 +111,7 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
// (This is why folders you create will automatically disappear // (This is why folders you create will automatically disappear
// if you leave them without adding anything to them.) // if you leave them without adding anything to them.)
const folderTable: {[folder: string]: 1 | undefined} = {'': 1}; const folderTable: { [folder: string]: 1 | undefined } = { '': 1 };
const folders: string[] = []; const folders: string[] = [];
for (const team of PS.teams.list) { for (const team of PS.teams.list) {
const folder = team.folder; const folder = team.folder;
@ -169,9 +170,9 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
const folderOpenIcon = room.curFolder === format ? 'fa-folder-open' : 'fa-folder'; const folderOpenIcon = room.curFolder === format ? 'fa-folder-open' : 'fa-folder';
if (gen === 0) { if (gen === 0) {
renderedFolders.push(<TeamFolder cur={room.curFolder === format} value={format}> renderedFolders.push(<TeamFolder cur={room.curFolder === format} value={format}>
<i class={ <i
`fa ${folderOpenIcon}${format === '/' ? '-o' : ''}` class={`fa ${folderOpenIcon}${format === '/' ? '-o' : ''}`}
}></i> ></i>
{format.slice(0, -1) || '(uncategorized)'} {format.slice(0, -1) || '(uncategorized)'}
</TeamFolder>); </TeamFolder>);
continue; continue;
@ -212,7 +213,7 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
let filterFolder: string | null = null; let filterFolder: string | null = null;
let filterFormat: string | null = null; let filterFormat: string | null = null;
if (room.curFolder) { if (room.curFolder) {
if (room.curFolder.slice(-1) === '/') { if (room.curFolder.endsWith('/')) {
filterFolder = room.curFolder.slice(0, -1); filterFolder = room.curFolder.slice(0, -1);
teams = teams.filter(team => !team || team.folder === filterFolder); teams = teams.filter(team => !team || team.folder === filterFolder);
} else { } else {
@ -226,7 +227,7 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
{this.renderFolderList()} {this.renderFolderList()}
</div> </div>
<div class="teampane"> <div class="teampane">
{filterFolder ? {filterFolder ? (
<h2> <h2>
<i class="fa fa-folder-open"></i> {filterFolder} {} <i class="fa fa-folder-open"></i> {filterFolder} {}
<button class="button small" style="margin-left:5px" name="renameFolder"> <button class="button small" style="margin-left:5px" name="renameFolder">
@ -236,13 +237,13 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
<i class="fa fa-times"></i> Remove <i class="fa fa-times"></i> Remove
</button> </button>
</h2> </h2>
: filterFolder === '' ? ) : filterFolder === '' ? (
<h2><i class="fa fa-folder-open-o"></i> Teams not in any folders</h2> <h2><i class="fa fa-folder-open-o"></i> Teams not in any folders</h2>
: filterFormat ? ) : filterFormat ? (
<h2><i class="fa fa-folder-open-o"></i> {filterFormat} <small>({teams.length})</small></h2> <h2><i class="fa fa-folder-open-o"></i> {filterFormat} <small>({teams.length})</small></h2>
: ) : (
<h2>All Teams <small>({teams.length})</small></h2> <h2>All Teams <small>({teams.length})</small></h2>
} )}
<p> <p>
<button name="cmd" value="/newteam" class="button big"><i class="fa fa-plus-circle"></i> New Team</button> <button name="cmd" value="/newteam" class="button big"><i class="fa fa-plus-circle"></i> New Team</button>
</p> </p>
@ -254,7 +255,7 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
</li> </li>
) : ( ) : (
<li key="undelete"> <li key="undelete">
<button name="cmd" value={`/undeleteteam`}><i class="fa fa-undo"></i> Undo delete</button> <button name="cmd" value="/undeleteteam"><i class="fa fa-undo"></i> Undo delete</button>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -5,9 +5,9 @@
* @license AGPLv3 * @license AGPLv3
*/ */
import {PS, type Team} from "./client-main"; import { PS, type Team } from "./client-main";
import {PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSPanelWrapper, PSRoomPanel } from "./panels";
import {Dex, toID, type ID} from "./battle-dex"; import { Dex, toID, type ID } from "./battle-dex";
import { BattleStatIDs, BattleStatNames } from "./battle-dex-data"; import { BattleStatIDs, BattleStatNames } from "./battle-dex-data";
export class PSTeambuilder { export class PSTeambuilder {
@ -24,14 +24,14 @@ export class PSTeambuilder {
// species // species
let id = toID(set.species); let id = toID(set.species);
buf += '|' + (toID(set.name || set.species) === id ? '' : id); buf += `|${toID(set.name || set.species) === id ? '' : id}`;
// item // item
buf += '|' + toID(set.item); buf += `|${toID(set.item)}`;
// ability // ability
id = toID(set.ability); id = toID(set.ability);
buf += '|' + (id || '-'); buf += `|${id || '-'}`;
// moves // moves
buf += '|'; buf += '|';
@ -39,7 +39,7 @@ export class PSTeambuilder {
for (let j = 0; j < set.moves.length; j++) { for (let j = 0; j < set.moves.length; j++) {
let moveid = toID(set.moves[j]); let moveid = toID(set.moves[j]);
if (j && !moveid) continue; if (j && !moveid) continue;
buf += (j ? ',' : '') + moveid; buf += `${j ? ',' : ''}${moveid}`;
if (moveid.substr(0, 11) === 'hiddenpower' && moveid.length > 11) { if (moveid.substr(0, 11) === 'hiddenpower' && moveid.length > 11) {
hasHP = moveid.slice(11); hasHP = moveid.slice(11);
} }
@ -47,35 +47,24 @@ export class PSTeambuilder {
} }
// nature // nature
buf += '|' + (set.nature || ''); buf += `|${set.nature || ''}`;
// evs // evs
if (set.evs) { if (set.evs) {
buf += '|' + (set.evs['hp'] || '') + ',' + buf += `|${set.evs['hp'] || ''},${set.evs['atk'] || ''},${set.evs['def'] || ''},` +
(set.evs['atk'] || '') + ',' + `${set.evs['spa'] || ''},${set.evs['spd'] || ''},${set.evs['spe'] || ''}`;
(set.evs['def'] || '') + ',' +
(set.evs['spa'] || '') + ',' +
(set.evs['spd'] || '') + ',' +
(set.evs['spe'] || '');
} else { } else {
buf += '|'; buf += '|';
} }
// gender // gender
if (set.gender) { buf += `|${set.gender || ''}`;
buf += '|' + set.gender;
} else {
buf += '|';
}
// ivs // ivs
if (set.ivs) { if (set.ivs) {
buf += '|' + (set.ivs['hp'] === 31 ? '' : set.ivs['hp']) + ',' + buf += `|${set.ivs['hp'] === 31 ? '' : set.ivs['hp']},${set.ivs['atk'] === 31 ? '' : set.ivs['atk']},` +
(set.ivs['atk'] === 31 ? '' : set.ivs['atk']) + ',' + `${set.ivs['def'] === 31 ? '' : set.ivs['def']},${set.ivs['spa'] === 31 ? '' : set.ivs['spa']},` +
(set.ivs['def'] === 31 ? '' : set.ivs['def']) + ',' + `${set.ivs['spd'] === 31 ? '' : set.ivs['spd']},${set.ivs['spe'] === 31 ? '' : set.ivs['spe']}`;
(set.ivs['spa'] === 31 ? '' : set.ivs['spa']) + ',' +
(set.ivs['spd'] === 31 ? '' : set.ivs['spd']) + ',' +
(set.ivs['spe'] === 31 ? '' : set.ivs['spe']);
} else { } else {
buf += '|'; buf += '|';
} }
@ -89,14 +78,14 @@ export class PSTeambuilder {
// level // level
if (set.level) { if (set.level) {
buf += '|' + set.level; buf += `|${set.level}`;
} else { } else {
buf += '|'; buf += '|';
} }
// happiness // happiness
if (set.happiness !== undefined) { if (set.happiness !== undefined) {
buf += '|' + set.happiness; buf += `|${set.happiness}`;
} else { } else {
buf += '|'; buf += '|';
} }
@ -105,10 +94,10 @@ export class PSTeambuilder {
set.pokeball || (set.hpType && toID(set.hpType) !== hasHP) || set.gigantamax || set.pokeball || (set.hpType && toID(set.hpType) !== hasHP) || set.gigantamax ||
(set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10) (set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10)
) { ) {
buf += ',' + (set.hpType || ''); buf += `,${set.hpType || ''}`;
buf += ',' + toID(set.pokeball); buf += `,${toID(set.pokeball)}`;
buf += ',' + (set.gigantamax ? 'G' : ''); buf += `,${set.gigantamax ? 'G' : ''}`;
buf += ',' + (set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10 ? set.dynamaxLevel : ''); buf += `,${set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10 ? set.dynamaxLevel : ''}`;
} }
} }
@ -123,7 +112,7 @@ export class PSTeambuilder {
for (const setBuf of buf.split(`]`)) { for (const setBuf of buf.split(`]`)) {
const parts = setBuf.split(`|`); const parts = setBuf.split(`|`);
if (parts.length < 11) continue; if (parts.length < 11) continue;
let set: Dex.PokemonSet = {species: '', moves: []}; let set: Dex.PokemonSet = { species: '', moves: [] };
team.push(set); team.push(set);
// name // name
@ -137,10 +126,9 @@ export class PSTeambuilder {
// ability // ability
const species = Dex.species.get(set.species); const species = Dex.species.get(set.species);
set.ability = parts[3] === '-' ? set.ability =
'' : parts[3] === '-' ? '' :
(species.baseSpecies === 'Zygarde' && parts[3] === 'H') ? (species.baseSpecies === 'Zygarde' && parts[3] === 'H') ? 'Power Construct' :
'Power Construct' :
['', '0', '1', 'H', 'S'].includes(parts[3]) ? ['', '0', '1', 'H', 'S'].includes(parts[3]) ?
species.abilities[parts[3] as '0' || '0'] || (parts[3] === '' ? '' : '!!!ERROR!!!') : species.abilities[parts[3] as '0' || '0'] || (parts[3] === '' ? '' : '!!!ERROR!!!') :
Dex.abilities.get(parts[3]).name; Dex.abilities.get(parts[3]).name;
@ -167,7 +155,7 @@ export class PSTeambuilder {
spe: Number(evs[5]) || 0, spe: Number(evs[5]) || 0,
}; };
} else if (parts[6] === '0') { } else if (parts[6] === '0') {
set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
} }
} }
@ -333,7 +321,7 @@ export class PSTeambuilder {
line = line.slice(0, -4); line = line.slice(0, -4);
} }
let parenIndex = line.lastIndexOf(' ('); let parenIndex = line.lastIndexOf(' (');
if (line.charAt(line.length - 1) === ')' && parenIndex !== -1) { if (line.endsWith(')') && parenIndex !== -1) {
set.species = Dex.species.get(line.slice(parenIndex + 2, -1)).name; set.species = Dex.species.get(line.slice(parenIndex + 2, -1)).name;
set.name = line.slice(0, parenIndex); set.name = line.slice(0, parenIndex);
} else { } else {
@ -368,7 +356,7 @@ export class PSTeambuilder {
} else if (line.startsWith('EVs: ')) { } else if (line.startsWith('EVs: ')) {
line = line.slice(5); line = line.slice(5);
let evLines = line.split('/'); let evLines = line.split('/');
set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
for (let evLine of evLines) { for (let evLine of evLines) {
evLine = evLine.trim(); evLine = evLine.trim();
let spaceIndex = evLine.indexOf(' '); let spaceIndex = evLine.indexOf(' ');
@ -381,7 +369,7 @@ export class PSTeambuilder {
} else if (line.startsWith('IVs: ')) { } else if (line.startsWith('IVs: ')) {
line = line.slice(5); line = line.slice(5);
let ivLines = line.split(' / '); let ivLines = line.split(' / ');
set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
for (let ivLine of ivLines) { for (let ivLine of ivLines) {
ivLine = ivLine.trim(); ivLine = ivLine.trim();
let spaceIndex = ivLine.indexOf(' '); let spaceIndex = ivLine.indexOf(' ');
@ -392,19 +380,19 @@ export class PSTeambuilder {
if (isNaN(statval)) statval = 31; if (isNaN(statval)) statval = 31;
set.ivs[statid] = statval; set.ivs[statid] = statval;
} }
} else if (line.match(/^[A-Za-z]+ (N|n)ature/)) { } else if (/^[A-Za-z]+ (N|n)ature/.exec(line)) {
let natureIndex = line.indexOf(' Nature'); let natureIndex = line.indexOf(' Nature');
if (natureIndex === -1) natureIndex = line.indexOf(' nature'); if (natureIndex === -1) natureIndex = line.indexOf(' nature');
if (natureIndex === -1) return; if (natureIndex === -1) return;
line = line.substr(0, natureIndex); line = line.substr(0, natureIndex);
if (line !== 'undefined') set.nature = line as Dex.NatureName; if (line !== 'undefined') set.nature = line as Dex.NatureName;
} else if (line.charAt(0) === '-' || line.charAt(0) === '~') { } else if (line.startsWith('-') || line.startsWith('~')) {
line = line.slice(line.charAt(1) === ' ' ? 2 : 1); line = line.slice(line.charAt(1) === ' ' ? 2 : 1);
if (line.startsWith('Hidden Power [')) { if (line.startsWith('Hidden Power [')) {
const hpType = line.slice(14, -1) as Dex.TypeName; const hpType = line.slice(14, -1) as Dex.TypeName;
line = 'Hidden Power ' + hpType; line = 'Hidden Power ' + hpType;
if (!set.ivs && Dex.types.isName(hpType)) { if (!set.ivs && Dex.types.isName(hpType)) {
set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
const hpIVs = Dex.types.get(hpType).HPivs || {}; const hpIVs = Dex.types.get(hpType).HPivs || {};
for (let stat in hpIVs) { for (let stat in hpIVs) {
set.ivs[stat as Dex.StatName] = hpIVs[stat as Dex.StatName]!; set.ivs[stat as Dex.StatName] = hpIVs[stat as Dex.StatName]!;
@ -546,7 +534,7 @@ export class PSTeambuilder {
} }
} }
export function TeamFolder(props: {cur?: boolean, value: string, children: preact.ComponentChildren}) { export function TeamFolder(props: { cur?: boolean, value: string, children: preact.ComponentChildren }) {
// folders are <div>s rather than <button>s because in theory it has // folders are <div>s rather than <button>s because in theory it has
// less weird interactions with HTML5 drag-and-drop // less weird interactions with HTML5 drag-and-drop
if (props.cur) { if (props.cur) {
@ -560,7 +548,7 @@ export function TeamFolder(props: {cur?: boolean, value: string, children: preac
</div>; </div>;
} }
export function TeamBox(props: {team: Team | null, noLink?: boolean, button?: boolean}) { export function TeamBox(props: { team: Team | null, noLink?: boolean, button?: boolean }) {
const team = props.team; const team = props.team;
let contents; let contents;
if (team) { if (team) {
@ -659,7 +647,7 @@ class TeamDropdownPanel extends PSRoomPanel {
if (availableWidth > 636) width = 613; if (availableWidth > 636) width = 613;
if (availableWidth > 945) width = 919; if (availableWidth > 945) width = 919;
let teamBuckets: {[folder: string]: Team[]} = {}; let teamBuckets: { [folder: string]: Team[] } = {};
for (const team of teams) { for (const team of teams) {
const list = teamBuckets[team.folder] || (teamBuckets[team.folder] = []); const list = teamBuckets[team.folder] || (teamBuckets[team.folder] = []);
list.push(team); list.push(team);
@ -676,13 +664,27 @@ class TeamDropdownPanel extends PSRoomPanel {
const hasOtherGens = genList.length > 1 || genList[0] !== baseGen; const hasOtherGens = genList.length > 1 || genList[0] !== baseGen;
teamList.push(<p> teamList.push(<p>
{baseFormat.length > 4 && <button class={'button' + (baseFormat === this.format ? ' disabled' : '')} onClick={this.setFormat} name="format" value={baseFormat}> {baseFormat.length > 4 && (
<button
class={'button' + (baseFormat === this.format ? ' disabled' : '')}
onClick={this.setFormat} name="format" value={baseFormat}
>
<i class="fa fa-folder-o"></i> [{baseFormat.slice(0, 4)}] {baseFormat.slice(4)} <i class="fa fa-folder-o"></i> [{baseFormat.slice(0, 4)}] {baseFormat.slice(4)}
</button>} <button class={'button' + (baseGen === this.format ? ' disabled' : '')} onClick={this.setFormat} name="format" value={baseGen}> </button>
)} {}
<button
class={'button' + (baseGen === this.format ? ' disabled' : '')} onClick={this.setFormat} name="format" value={baseGen}
>
<i class="fa fa-folder-o"></i> [{baseGen}] <em>(uncategorized)</em> <i class="fa fa-folder-o"></i> [{baseGen}] <em>(uncategorized)</em>
</button> <button class={'button' + (baseGen === this.gen ? ' disabled' : '')} onClick={this.setFormat} name="gen" value={baseGen}> </button> {}
<button
class={'button' + (baseGen === this.gen ? ' disabled' : '')} onClick={this.setFormat} name="gen" value={baseGen}
>
<i class="fa fa-folder-o"></i> [{baseGen}] <em>(all)</em> <i class="fa fa-folder-o"></i> [{baseGen}] <em>(all)</em>
</button> {hasOtherGens && !this.gen && <button class="button" onClick={this.setFormat} name="gen" value={baseGen}>Other gens</button>} </button> {}
{hasOtherGens && !this.gen && (
<button class="button" onClick={this.setFormat} name="gen" value={baseGen}>Other gens</button>
)}
</p>); </p>);
if (hasOtherGens && this.gen) { if (hasOtherGens && this.gen) {
@ -716,7 +718,7 @@ class TeamDropdownPanel extends PSRoomPanel {
</h2>); </h2>);
} }
teamList.push(<ul class="teamdropdown" onClick={this.click}> teamList.push(<ul class="teamdropdown" onClick={this.click}>
{teamBuckets[folder].map(team => <li key={team.key} style={"display:inline-block"}> {teamBuckets[folder].map(team => <li key={team.key} style={{ display: 'inline-block' }}>
<TeamBox team={team} button /> <TeamBox team={team} button />
</li>)} </li>)}
</ul>); </ul>);
@ -747,9 +749,7 @@ export interface FormatData {
effectType: 'Format'; effectType: 'Format';
} }
declare const BattleFormats: {[id: string]: FormatData}; declare const BattleFormats: { [id: string]: FormatData };
/** id:name */
declare const NonBattleGames: {[id: string]: string};
class FormatDropdownPanel extends PSRoomPanel { class FormatDropdownPanel extends PSRoomPanel {
gen = ''; gen = '';
@ -778,6 +778,7 @@ class FormatDropdownPanel extends PSRoomPanel {
let formatsLoaded = !!window.BattleFormats; let formatsLoaded = !!window.BattleFormats;
if (formatsLoaded) { if (formatsLoaded) {
formatsLoaded = false; formatsLoaded = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (let i in window.BattleFormats) { for (let i in window.BattleFormats) {
formatsLoaded = true; formatsLoaded = true;
break; break;
@ -806,7 +807,7 @@ class FormatDropdownPanel extends PSRoomPanel {
let curSection = ''; let curSection = '';
let curColumnNum = 0; let curColumnNum = 0;
let curColumn: (FormatData | {id: null, section: string})[] = []; let curColumn: (FormatData | { id: null, section: string })[] = [];
const columns = [curColumn]; const columns = [curColumn];
for (const format of formats) { for (const format of formats) {
if (format.column !== curColumnNum) { if (format.column !== curColumnNum) {
@ -819,7 +820,7 @@ class FormatDropdownPanel extends PSRoomPanel {
if (format.section !== curSection) { if (format.section !== curSection) {
curSection = format.section; curSection = format.section;
if (curSection) { if (curSection) {
curColumn.push({id: null, section: curSection}); curColumn.push({ id: null, section: curSection });
} }
} }
curColumn.push(format); curColumn.push(format);

View File

@ -10,16 +10,16 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {PS, PSRoom, type RoomOptions, type RoomID} from "./client-main"; import { PS, PSRoom, type RoomOptions, type RoomID } from "./client-main";
import {PSMain, PSPanelWrapper, PSRoomPanel} from "./panels"; import { PSMain, PSPanelWrapper, PSRoomPanel } from "./panels";
import type {Battle} from "./battle"; import type { Battle } from "./battle";
import {Dex, toRoomid, toUserid, type ID} from "./battle-dex"; import { Dex, toRoomid, toUserid, type ID } from "./battle-dex";
import {BattleLog} from "./battle-log"; import { BattleLog } from "./battle-log";
window.addEventListener('drop', e => { window.addEventListener('drop', e => {
console.log('drop ' + e.dataTransfer!.dropEffect); console.log('drop ' + e.dataTransfer!.dropEffect);
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (/^text/.test((target as HTMLInputElement).type)) { if ((target as HTMLInputElement).type.startsWith("text")) {
PS.dragging = null; PS.dragging = null;
return; // Ignore text fields return; // Ignore text fields
} }
@ -40,7 +40,7 @@ window.addEventListener('dragover', e => {
e.preventDefault(); e.preventDefault();
}); });
export class PSHeader extends preact.Component<{style: {}}> { export class PSHeader extends preact.Component<{ style: object }> {
handleDragEnter = (e: DragEvent) => { handleDragEnter = (e: DragEvent) => {
console.log('dragenter ' + e.dataTransfer!.dropEffect); console.log('dragenter ' + e.dataTransfer!.dropEffect);
e.preventDefault(); e.preventDefault();
@ -63,6 +63,7 @@ export class PSHeader extends preact.Component<{style: {}}> {
if (rightIndex >= 0) { if (rightIndex >= 0) {
this.dragOnto(draggingRoom, 'rightRoomList', rightIndex); this.dragOnto(draggingRoom, 'rightRoomList', rightIndex);
} else { } else {
// eslint-disable-next-line no-useless-return
return; return;
} }
} }
@ -75,7 +76,7 @@ export class PSHeader extends preact.Component<{style: {}}> {
const roomid = PS.router.extractRoomID((e.currentTarget as HTMLAnchorElement).href); const roomid = PS.router.extractRoomID((e.currentTarget as HTMLAnchorElement).href);
if (!roomid) return; // should never happen if (!roomid) return; // should never happen
PS.dragging = {type: 'room', roomid}; PS.dragging = { type: 'room', roomid };
}; };
dragOnto(fromRoom: RoomID, toRoomList: 'leftRoomList' | 'rightRoomList' | 'miniRoomList', toIndex: number) { dragOnto(fromRoom: RoomID, toRoomList: 'leftRoomList' | 'rightRoomList' | 'miniRoomList', toIndex: number) {
// one day you will be able to rearrange mainmenu and rooms, but not today // one day you will be able to rearrange mainmenu and rooms, but not today
@ -186,7 +187,7 @@ export class PSHeader extends preact.Component<{style: {}}> {
break; break;
case 'html': case 'html':
default: default:
if (title.charAt(0) === '[') { if (title.startsWith('[')) {
let closeBracketIndex = title.indexOf(']'); let closeBracketIndex = title.indexOf(']');
if (closeBracketIndex > 0) { if (closeBracketIndex > 0) {
icon = <i class="text">{title.slice(1, closeBracketIndex)}</i>; icon = <i class="text">{title.slice(1, closeBracketIndex)}</i>;
@ -222,7 +223,7 @@ export class PSHeader extends preact.Component<{style: {}}> {
if (!PS.user.named) { if (!PS.user.named) {
return <a class="button" href="login">Choose name</a>; return <a class="button" href="login">Choose name</a>;
} }
const userColor = window.BattleLog && {color: BattleLog.usernameColor(PS.user.userid)}; const userColor = window.BattleLog && { color: BattleLog.usernameColor(PS.user.userid) };
return <span class="username" data-name={PS.user.name} style={userColor}> return <span class="username" data-name={PS.user.name} style={userColor}>
<i class="fa fa-user" style="color:#779EC5"></i> <span class="usernametext">{PS.user.name}</span> <i class="fa fa-user" style="color:#779EC5"></i> <span class="usernametext">{PS.user.name}</span>
</span>; </span>;
@ -244,7 +245,7 @@ export class PSHeader extends preact.Component<{style: {}}> {
<ul> <ul>
{PS.leftRoomList.slice(1).map(roomid => this.renderRoomTab(roomid))} {PS.leftRoomList.slice(1).map(roomid => this.renderRoomTab(roomid))}
</ul> </ul>
<ul class="siderooms" style={{float: 'none', marginLeft: PS.leftRoomWidth - 144}}> <ul class="siderooms" style={{ float: 'none', marginLeft: PS.leftRoomWidth - 144 }}>
{PS.rightRoomList.map(roomid => this.renderRoomTab(roomid))} {PS.rightRoomList.map(roomid => this.renderRoomTab(roomid))}
</ul> </ul>
</div></div> </div></div>
@ -285,19 +286,19 @@ export class UserRoom extends PSRoom {
class UserPanel extends PSRoomPanel<UserRoom> { class UserPanel extends PSRoomPanel<UserRoom> {
override render() { override render() {
const room = this.props.room; const room = this.props.room;
const user = PS.mainmenu.userdetailsCache[room.userid] || {userid: room.userid, avatar: '[loading]'}; const user = PS.mainmenu.userdetailsCache[room.userid] || { userid: room.userid, avatar: '[loading]' };
const name = room.name.slice(1); const name = room.name.slice(1);
const group = PS.server.getGroup(room.name); const group = PS.server.getGroup(room.name);
let groupName: preact.ComponentChild = group.name || null; let groupName: preact.ComponentChild = group.name || null;
if (group.type === 'punishment') { if (group.type === 'punishment') {
groupName = <span style='color:#777777'>{groupName}</span>; groupName = <span style="color:#777777">{groupName}</span>;
} }
const globalGroup = PS.server.getGroup(user.group); const globalGroup = PS.server.getGroup(user.group);
let globalGroupName: preact.ComponentChild = globalGroup.name && `Global ${globalGroup.name}` || null; let globalGroupName: preact.ComponentChild = globalGroup.name && `Global ${globalGroup.name}` || null;
if (globalGroup.type === 'punishment') { if (globalGroup.type === 'punishment') {
globalGroupName = <span style='color:#777777'>{globalGroupName}</span>; globalGroupName = <span style="color:#777777">{globalGroupName}</span>;
} }
if (globalGroup.name === group.name) groupName = null; if (globalGroup.name === group.name) groupName = null;
@ -319,7 +320,8 @@ class UserPanel extends PSRoomPanel<UserRoom> {
const p1 = curRoom.p1!.substr(1); const p1 = curRoom.p1!.substr(1);
const p2 = curRoom.p2!.substr(1); const p2 = curRoom.p2!.substr(1);
const ownBattle = (PS.user.userid === toUserid(p1) || PS.user.userid === toUserid(p2)); const ownBattle = (PS.user.userid === toUserid(p1) || PS.user.userid === toUserid(p2));
const roomLink = <a href={`/${roomid}`} class={'ilink' + (ownBattle || roomid in PS.rooms ? ' yours' : '')} const roomLink = <a
href={`/${roomid}`} class={'ilink' + (ownBattle || roomid in PS.rooms ? ' yours' : '')}
title={`${p1 || '?'} v. ${p2 || '?'}`} title={`${p1 || '?'} v. ${p2 || '?'}`}
>{roomrank}{roomid.substr(7)}</a>; >{roomrank}{roomid.substr(7)}</a>;
if (curRoom.isPrivate) { if (curRoom.isPrivate) {
@ -365,28 +367,31 @@ class UserPanel extends PSRoomPanel<UserRoom> {
{user.avatar !== '[loading]' && {user.avatar !== '[loading]' &&
<img <img
class={'trainersprite' + (room.isSelf ? ' yours' : '')} class={'trainersprite' + (room.isSelf ? ' yours' : '')}
src={Dex.resolveAvatar('' + (user.avatar || 'unknown'))} src={Dex.resolveAvatar(`${user.avatar || 'unknown'}`)}
/> />}
} <strong><a
<strong><a href={`//${Config.routes.users}/${user.userid}`} target="_blank" style={away ? {color: '#888888'} : null}>{name}</a></strong><br /> href={`//${Config.routes.users}/${user.userid}`} target="_blank" style={away ? { color: '#888888' } : null}
>
{name}
</a></strong><br />
{status && <div class="userstatus">{status}</div>} {status && <div class="userstatus">{status}</div>}
{groupName && <div class="usergroup roomgroup">{groupName}</div>} {groupName && <div class="usergroup roomgroup">{groupName}</div>}
{globalGroupName && <div class="usergroup globalgroup">{globalGroupName}</div>} {globalGroupName && <div class="usergroup globalgroup">{globalGroupName}</div>}
{user.customgroup && <div class="usergroup globalgroup">{user.customgroup}</div>} {user.customgroup && <div class="usergroup globalgroup">{user.customgroup}</div>}
{roomsList} {roomsList}
</div> </div>
{isSelf || !PS.user.named ? {isSelf || !PS.user.named ? (
<p class="buttonbar"> <p class="buttonbar">
<button class="button" disabled>Challenge</button> {} <button class="button" disabled>Challenge</button> {}
<button class="button" disabled>Chat</button> <button class="button" disabled>Chat</button>
</p> </p>
: ) : (
<p class="buttonbar"> <p class="buttonbar">
<button class="button" data-href={`/challenge-${user.userid}`}>Challenge</button> {} <button class="button" data-href={`/challenge-${user.userid}`}>Challenge</button> {}
<button class="button" data-href={`/pm-${user.userid}`}>Chat</button> {} <button class="button" data-href={`/pm-${user.userid}`}>Chat</button> {}
<button class="button disabled" name="userOptions">{'\u2026'}</button> <button class="button disabled" name="userOptions">{'\u2026'}</button>
</p> </p>
} )}
{isSelf && <hr />} {isSelf && <hr />}
{isSelf && <p class="buttonbar" style="text-align: right"> {isSelf && <p class="buttonbar" style="text-align: right">
<button class="button disabled" name="login"><i class="fa fa-pencil"></i> Change name</button> {} <button class="button disabled" name="login"><i class="fa fa-pencil"></i> Change name</button> {}
@ -423,7 +428,9 @@ class VolumePanel extends PSRoomPanel {
return <PSPanelWrapper room={room}> return <PSPanelWrapper room={room}>
<h3>Volume</h3> <h3>Volume</h3>
<p class="volume"> <p class="volume">
<label class="optlabel">Effects: <span class="value">{!PS.prefs.mute && PS.prefs.effectvolume ? `${PS.prefs.effectvolume}%` : `muted`}</span></label> <label class="optlabel">
Effects: <span class="value">{!PS.prefs.mute && PS.prefs.effectvolume ? `${PS.prefs.effectvolume}%` : `muted`}</span>
</label>
{PS.prefs.mute ? {PS.prefs.mute ?
<em>(muted)</em> : <em>(muted)</em> :
<input <input
@ -432,7 +439,9 @@ class VolumePanel extends PSRoomPanel {
/>} />}
</p> </p>
<p class="volume"> <p class="volume">
<label class="optlabel">Music: <span class="value">{!PS.prefs.mute && PS.prefs.musicvolume ? `${PS.prefs.musicvolume}%` : `muted`}</span></label> <label class="optlabel">
Music: <span class="value">{!PS.prefs.mute && PS.prefs.musicvolume ? `${PS.prefs.musicvolume}%` : `muted`}</span>
</label>
{PS.prefs.mute ? {PS.prefs.mute ?
<em>(muted)</em> : <em>(muted)</em> :
<input <input
@ -441,7 +450,10 @@ class VolumePanel extends PSRoomPanel {
/>} />}
</p> </p>
<p class="volume"> <p class="volume">
<label class="optlabel">Notifications: <span class="value">{!PS.prefs.mute && PS.prefs.notifvolume ? `${PS.prefs.notifvolume}%` : `muted`}</span></label> <label class="optlabel">
Notifications: {}
<span class="value">{!PS.prefs.mute && PS.prefs.notifvolume ? `${PS.prefs.notifvolume}%` : `muted`}</span>
</label>
{PS.prefs.mute ? {PS.prefs.mute ?
<em>(muted)</em> : <em>(muted)</em> :
<input <input
@ -450,7 +462,9 @@ class VolumePanel extends PSRoomPanel {
/>} />}
</p> </p>
<p> <p>
<label class="checkbox"><input type="checkbox" name="mute" checked={PS.prefs.mute} onChange={this.setMute} /> Mute all</label> <label class="checkbox">
<input type="checkbox" name="mute" checked={PS.prefs.mute} onChange={this.setMute} /> Mute all
</label>
</p> </p>
</PSPanelWrapper>; </PSPanelWrapper>;
} }

View File

@ -10,13 +10,13 @@
*/ */
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import {toID} from "./battle-dex"; import { toID } from "./battle-dex";
import {BattleLog} from "./battle-log"; import { BattleLog } from "./battle-log";
import type { Args } from "./battle-text-parser"; import type { Args } from "./battle-text-parser";
import {BattleTooltips} from "./battle-tooltips"; import { BattleTooltips } from "./battle-tooltips";
import type {PSSubscription} from "./client-core"; import type { PSSubscription } from "./client-core";
import {PS, PSRoom, type RoomID} from "./client-main"; import { PS, type PSRoom, type RoomID } from "./client-main";
import {PSHeader} from "./panel-topbar"; import { PSHeader } from "./panel-topbar";
export class PSRouter { export class PSRouter {
roomid = '' as RoomID; roomid = '' as RoomID;
@ -127,7 +127,7 @@ export class PSRouter {
} }
PS.router = new PSRouter(); PS.router = new PSRouter();
export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{room: T}> { export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ room: T }> {
subscriptions: PSSubscription[] = []; subscriptions: PSSubscription[] = [];
override componentDidMount() { override componentDidMount() {
if (PS.room === this.props.room) this.focus(); if (PS.room === this.props.room) this.focus();
@ -277,6 +277,7 @@ export class PSMain extends preact.Component {
} }
if (PS.room !== clickedRoom) { if (PS.room !== clickedRoom) {
if (clickedRoom) PS.room = clickedRoom; if (clickedRoom) PS.room = clickedRoom;
// eslint-disable-next-line no-unmodified-loop-condition
while (PS.popups.length && (!clickedRoom || clickedRoom.id !== PS.popups[PS.popups.length - 1])) { while (PS.popups.length && (!clickedRoom || clickedRoom.id !== PS.popups[PS.popups.length - 1])) {
PS.closePopup(); PS.closePopup();
} }
@ -318,7 +319,7 @@ export class PSMain extends preact.Component {
const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)'); const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
if (colorSchemeQuery?.media !== 'not all') { if (colorSchemeQuery?.media !== 'not all') {
colorSchemeQuery.addEventListener('change', function (cs) { colorSchemeQuery.addEventListener('change', cs => {
if (PS.prefs.theme === 'system') document.body.className = cs.matches ? 'dark' : ''; if (PS.prefs.theme === 'system') document.body.className = cs.matches ? 'dark' : '';
}); });
} }
@ -326,7 +327,7 @@ export class PSMain extends preact.Component {
PS.prefs.subscribeAndRun(key => { PS.prefs.subscribeAndRun(key => {
if (!key || key === 'theme') { if (!key || key === 'theme') {
const dark = PS.prefs.theme === 'dark' || const dark = PS.prefs.theme === 'dark' ||
(PS.prefs.theme === 'system' && colorSchemeQuery && colorSchemeQuery.matches); (PS.prefs.theme === 'system' && colorSchemeQuery?.matches);
document.body.className = dark ? 'dark' : ''; document.body.className = dark ? 'dark' : '';
} }
}); });
@ -374,21 +375,21 @@ export class PSMain extends preact.Component {
try { try {
const selection = window.getSelection()!; const selection = window.getSelection()!;
if (selection.type === 'Range') return false; if (selection.type === 'Range') return false;
} catch (err) {} } catch {}
BattleTooltips.hideTooltip(); BattleTooltips.hideTooltip();
} }
static posStyle(room: PSRoom) { static posStyle(room: PSRoom) {
let pos: PanelPosition | null = null; let pos: PanelPosition | null = null;
if (PS.leftRoomWidth === 0) { if (PS.leftRoomWidth === 0) {
// one panel visible // one panel visible
if (room === PS.activePanel) pos = {top: 56}; if (room === PS.activePanel) pos = { top: 56 };
} else { } else {
// both panels visible // both panels visible
if (room === PS.leftRoom) pos = {top: 56, right: PS.leftRoomWidth}; if (room === PS.leftRoom) pos = { top: 56, right: PS.leftRoomWidth };
if (room === PS.rightRoom) pos = {top: 56, left: PS.leftRoomWidth}; if (room === PS.rightRoom) pos = { top: 56, left: PS.leftRoomWidth };
} }
if (!pos) return {display: 'none'}; if (!pos) return { display: 'none' };
let top: number | null = (pos.top || 0); let top: number | null = (pos.top || 0);
let height: number | null = null; let height: number | null = null;
@ -422,7 +423,7 @@ export class PSMain extends preact.Component {
} }
static getPopupStyle(room: PSRoom, width?: number | 'auto'): any { static getPopupStyle(room: PSRoom, width?: number | 'auto'): any {
if (room.location === 'modal-popup' || !room.parentElem) { if (room.location === 'modal-popup' || !room.parentElem) {
return {width: width || 480}; return { width: width || 480 };
} }
if (!room.width || !room.height) { if (!room.width || !room.height) {
return { return {
@ -513,15 +514,15 @@ export class PSMain extends preact.Component {
} }
} }
return <div class="ps-frame"> return <div class="ps-frame">
<PSHeader style={{top: 0, left: 0, right: 0, height: '50px'}} /> <PSHeader style={{ top: 0, left: 0, right: 0, height: '50px' }} />
{rooms} {rooms}
{PS.popups.map(roomid => this.renderPopup(PS.rooms[roomid]!))} {PS.popups.map(roomid => this.renderPopup(PS.rooms[roomid]!))}
</div>; </div>;
} }
} }
type PanelPosition = {top?: number, bottom?: number, left?: number, right?: number} | null; type PanelPosition = { top?: number, bottom?: number, left?: number, right?: number } | null;
export function SanitizedHTML(props: {children: string}) { export function SanitizedHTML(props: { children: string }) {
return <div dangerouslySetInnerHTML={{__html: BattleLog.sanitizeHTML(props.children)}}/>; return <div dangerouslySetInnerHTML={{ __html: BattleLog.sanitizeHTML(props.children) }} />;
} }

View File

@ -93,9 +93,9 @@
} }
})(); })();
</script> </script>
<script nomodule src="/js/lib/ps-polyfill.js"></script>
<script src="../config/testclient-key.js"></script> <script src="../config/testclient-key.js"></script>
<script src="js/client-core.js"></script> <script src="js/client-core.js"></script>
<script nomodule src="/js/lib/ps-polyfill.js"></script>
<script src="js/battle-dex.js"></script> <script src="js/battle-dex.js"></script>
<script src="js/battle-text-parser.js"></script> <script src="js/battle-text-parser.js"></script>

View File

@ -27,7 +27,7 @@ var ReplaySidebarPanel = Panels.StaticPanel.extend({
if (!page) page = 2; if (!page) page = 2;
var user = elem.dataset.user; var user = elem.dataset.user;
var format = elem.dataset.format; var format = elem.dataset.format;
var private = !!elem.dataset.private; var priv = !!elem.dataset.private;
var self = this; var self = this;
elem.innerHTML = 'Loading...<br /><i class="fa fa-caret-down"></i>' elem.innerHTML = 'Loading...<br /><i class="fa fa-caret-down"></i>'
$.get('/search', Object.assign({ $.get('/search', Object.assign({
@ -35,7 +35,7 @@ var ReplaySidebarPanel = Panels.StaticPanel.extend({
format: format, format: format,
page: page, page: page,
output: 'html' output: 'html'
}, private ? {private: 1} : {}), function (data) { }, priv ? {private: 1} : {}), function (data) {
self.$('ul.linklist').append(data); self.$('ul.linklist').append(data);
// var $nextOffset = self.$('input.offset'); // var $nextOffset = self.$('input.offset');
// var val = $nextOffset.val(); // var val = $nextOffset.val();

View File

@ -1,39 +1,40 @@
/** @jsx preact.h */ /** @jsx preact.h */
import preact from 'preact'; import preact from '../../play.pokemonshowdown.com/js/lib/preact';
import $ from 'jquery'; import $ from 'jquery';
import {Net} from './utils'; import { Net } from './utils';
import {PSRouter, PSReplays} from './replays'; import { PSRouter, PSReplays } from './replays';
import {Battle} from '../../play.pokemonshowdown.com/src/battle'; import { Battle } from '../../play.pokemonshowdown.com/src/battle';
import {BattleLog} from '../../play.pokemonshowdown.com/src/battle-log'; import { BattleLog } from '../../play.pokemonshowdown.com/src/battle-log';
import {BattleSound} from '../../play.pokemonshowdown.com/src/battle-sound'; import { BattleSound } from '../../play.pokemonshowdown.com/src/battle-sound';
import type { ID } from '../../play.pokemonshowdown.com/src/battle-dex';
declare function toID(input: string): string; declare function toID(input: string): string;
function showAd(id: string) { function showAd(id: string) {
// @ts-expect-error // @ts-expect-error no clue how to declare this one
window.top.__vm_add = window.top.__vm_add || []; window.top.__vm_add = window.top.__vm_add || [];
//this is a x-browser way to make sure content has loaded. // this is a x-browser way to make sure content has loaded.
(function (success) { (success => {
if (window.document.readyState !== "loading") { if (window.document.readyState !== "loading") {
success(); success();
} else { } else {
window.document.addEventListener("DOMContentLoaded", function () { window.document.addEventListener("DOMContentLoaded", () => {
success(); success();
}); });
} }
})(function () { })(() => {
var placement = document.createElement("div"); const placement = document.createElement("div");
placement.setAttribute("class", "vm-placement"); placement.setAttribute("class", "vm-placement");
if (window.innerWidth > 1000) { if (window.innerWidth > 1000) {
//load desktop placement // load desktop placement
placement.setAttribute("data-id", "6452680c0b35755a3f09b59b"); placement.setAttribute("data-id", "6452680c0b35755a3f09b59b");
} else { } else {
//load mobile placement // load mobile placement
placement.setAttribute("data-id", "645268557bc7b571c2f06f62"); placement.setAttribute("data-id", "645268557bc7b571c2f06f62");
} }
document.querySelector("#" + id)!.appendChild(placement); document.querySelector("#" + id)!.appendChild(placement);
// @ts-expect-error // @ts-expect-error no clue how to declare this one
window.top.__vm_add.push(placement); window.top.__vm_add.push(placement);
}); });
} }
@ -43,7 +44,7 @@ export class BattleDiv extends preact.Component {
return false; return false;
} }
override render() { override render() {
return <div class="battle" style={{position: 'relative'}}></div>; return <div class="battle" style={{ position: 'relative' }}></div>;
} }
} }
class BattleLogDiv extends preact.Component { class BattleLogDiv extends preact.Component {
@ -55,20 +56,20 @@ class BattleLogDiv extends preact.Component {
} }
} }
export class BattlePanel extends preact.Component<{id: string}> { export class BattlePanel extends preact.Component<{ id: string }> {
result: { result: {
uploadtime: number; uploadtime: number,
id: string; id: string,
format: string; format: string,
players: string[]; players: string[],
log: string; log: string,
views: number; views: number,
rating: number; rating: number,
private: number; private: number,
password: string; password: string,
} | null | undefined = undefined; } | null | undefined = undefined;
resultError = ''; resultError = '';
battle: Battle | null; battle!: Battle | null;
/** debug purposes */ /** debug purposes */
lastUsedKeyCode = '0'; lastUsedKeyCode = '0';
turnView: boolean | string = false; turnView: boolean | string = false;
@ -78,7 +79,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
showAd('LeaderboardBTF'); showAd('LeaderboardBTF');
window.onkeydown = this.keyPressed; window.onkeydown = this.keyPressed;
} }
override componentWillReceiveProps(nextProps) { override componentWillReceiveProps(nextProps: this['props']) {
if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) { if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) {
this.loadBattle(nextProps.id); this.loadBattle(nextProps.id);
} }
@ -115,7 +116,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
this.result = replay; this.result = replay;
const $base = $(this.base!); const $base = $(this.base!);
this.battle = new Battle({ this.battle = new Battle({
id: replay.id, id: replay.id as ID,
$frame: $base.find('.battle'), $frame: $base.find('.battle'),
$logFrame: $base.find('.battle-log'), $logFrame: $base.find('.battle-log'),
log: replay.log.split('\n'), log: replay.log.split('\n'),
@ -135,7 +136,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
if (query.turn || query.t) { if (query.turn || query.t) {
this.battle.seekTurn(parseInt(query.turn || query.t, 10)); this.battle.seekTurn(parseInt(query.turn || query.t, 10));
} }
} catch (err) { } catch (err: any) {
this.result = null; this.result = null;
this.resultError = result.startsWith('{') ? err.toString() : result; this.resultError = result.startsWith('{') ? err.toString() : result;
} }
@ -159,14 +160,13 @@ export class BattlePanel extends preact.Component<{id: string}> {
} }
} }
keyPressed = (e: KeyboardEvent) => { keyPressed = (e: KeyboardEvent) => {
// @ts-ignore
this.lastUsedKeyCode = `${e.keyCode}`; this.lastUsedKeyCode = `${e.keyCode}`;
if (e.ctrlKey || e.metaKey || e.altKey) return; if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.keyCode === 27 && this.turnView) { // Esc if (e.keyCode === 27 && this.turnView) { // Esc
this.closeTurn(); this.closeTurn();
return; return;
} }
// @ts-ignore // @ts-expect-error really wish they let me assert that the target is an HTMLElement
if (e.target?.tagName === 'INPUT' || e.target?.tagName === 'SELECT') return; if (e.target?.tagName === 'INPUT' || e.target?.tagName === 'SELECT') return;
switch (e.keyCode) { switch (e.keyCode) {
case 75: // k case 75: // k
@ -245,7 +245,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
lastTurn = () => { lastTurn = () => {
this.battle?.seekTurn(Infinity); this.battle?.seekTurn(Infinity);
}; };
goToTurn = (e) => { goToTurn = (e: Event) => {
const turn = this.base?.querySelector<HTMLInputElement>('input[name=turn]')?.value; const turn = this.base?.querySelector<HTMLInputElement>('input[name=turn]')?.value;
if (!turn?.trim()) return this.closeTurn(e); if (!turn?.trim()) return this.closeTurn(e);
let turnNum = Number(turn); let turnNum = Number(turn);
@ -273,15 +273,15 @@ export class BattlePanel extends preact.Component<{id: string}> {
// ladies and gentlemen, JavaScript dates // ladies and gentlemen, JavaScript dates
const timestamp = (this.result?.uploadtime || 0) * 1000; const timestamp = (this.result?.uploadtime || 0) * 1000;
const date = new Date(timestamp); const date = new Date(timestamp);
filename += '-' + date.getFullYear(); filename += `-${date.getFullYear()}`;
filename += (date.getMonth() >= 9 ? '-' : '-0') + (date.getMonth() + 1); filename += `${date.getMonth() >= 9 ? '-' : '-0'}${date.getMonth() + 1}`;
filename += (date.getDate() >= 10 ? '-' : '-0') + date.getDate(); filename += `${date.getDate() >= 10 ? '-' : '-0'}${date.getDate()}`;
filename += '-' + toID(this.battle.p1.name); filename += '-' + toID(this.battle.p1.name);
filename += '-' + toID(this.battle.p2.name); filename += '-' + toID(this.battle.p2.name);
const a = e.currentTarget as HTMLAnchorElement; const a = e.currentTarget as HTMLAnchorElement;
a.href = BattleLog.createReplayFileHref({battle: this.battle}); a.href = BattleLog.createReplayFileHref({ battle: this.battle });
a.download = filename + '.html'; a.download = filename + '.html';
e.stopPropagation(); e.stopPropagation();
@ -299,25 +299,25 @@ export class BattlePanel extends preact.Component<{id: string}> {
} }
return 'normal'; return 'normal';
} }
changeSpeed = (e: Event | {target: HTMLSelectElement}) => { changeSpeed = (e: Event | { target: HTMLSelectElement }) => {
const speed = (e.target as HTMLSelectElement).value; const speed = (e.target as HTMLSelectElement).value;
const fadeTable = { const fadeTable = {
hyperfast: 40, hyperfast: 40,
fast: 50, fast: 50,
normal: 300, normal: 300,
slow: 500, slow: 500,
reallyslow: 1000 reallyslow: 1000,
}; };
const delayTable = { const delayTable = {
hyperfast: 1, hyperfast: 1,
fast: 1, fast: 1,
normal: 1, normal: 1,
slow: 1000, slow: 1000,
reallyslow: 3000 reallyslow: 3000,
}; };
if (!this.battle) return; if (!this.battle) return;
this.battle.messageShownTime = delayTable[speed]; this.battle.messageShownTime = delayTable[speed as 'fast'];
this.battle.messageFadeTime = fadeTable[speed]; this.battle.messageFadeTime = fadeTable[speed as 'fast'];
this.battle.scene.updateAcceleration(); this.battle.scene.updateAcceleration();
}; };
stepSpeed(delta: number) { stepSpeed(delta: number) {
@ -327,7 +327,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
const newValue = values[values.indexOf(target.value) + delta]; const newValue = values[values.indexOf(target.value) + delta];
if (newValue) { if (newValue) {
target.value = newValue; target.value = newValue;
this.changeSpeed({target}); this.changeSpeed({ target });
} }
} }
toggleMute() { toggleMute() {
@ -338,7 +338,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
const muted = (e.target as HTMLSelectElement).value; const muted = (e.target as HTMLSelectElement).value;
this.battle?.setMute(muted === 'off'); this.battle?.setMute(muted === 'off');
// Wolfram Alpha says that default volume is 100 e^(-(2 log^2(2))/log(10)) which is around 65.881 // Wolfram Alpha says that default volume is 100 e^(-(2 log^2(2))/log(10)) which is around 65.881
BattleSound.setBgmVolume(muted === 'musicoff' ? 0 : 65.881258001265573); BattleSound.setBgmVolume(muted === 'musicoff' ? 0 : 65.88125800126558);
this.forceUpdate(); this.forceUpdate();
}; };
changeDarkMode = (e: Event) => { changeDarkMode = (e: Event) => {
@ -348,7 +348,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
this.forceUpdate(); this.forceUpdate();
}; };
openTurn = (e: Event) => { openTurn = (e: Event) => {
this.turnView = `${this.battle?.turn}` || true; this.turnView = `${this.battle?.turn || ''}` || true;
this.autofocusTurnView = 'select'; this.autofocusTurnView = 'select';
e.preventDefault(); e.preventDefault();
this.forceUpdate(); this.forceUpdate();
@ -360,30 +360,33 @@ export class BattlePanel extends preact.Component<{id: string}> {
}; };
renderError(position: any) { renderError(position: any) {
if (this.resultError) { if (this.resultError) {
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><section class="section"> return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}>
<section class="section">
<h1>Error</h1> <h1>Error</h1>
<p> <p>
{this.resultError} {this.resultError}
</p> </p>
</section></div>; </section>
</div>;
} }
// In theory, this should almost never happen, because Replays will // In theory, this should almost never happen, because Replays will
// never link to a nonexistent replay, but this might happen if e.g. // never link to a nonexistent replay, but this might happen if e.g.
// a replay gets deleted or made private after you searched for it // a replay gets deleted or made private after you searched for it
// but before you clicked it. // but before you clicked it.
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><section class="section" style={{maxWidth: '200px'}}> return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}>
<div style={{textAlign: 'center'}}> <section class="section" style={{ maxWidth: '200px' }}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} /> <div style={{ textAlign: 'center' }}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{ imageRendering: 'pixelated' }} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{ imageRendering: 'pixelated' }} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style={{ imageRendering: 'pixelated' }} />
</div> </div>
<div style={{textAlign: 'center'}}> <div style={{ textAlign: 'center' }}>
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style={{ imageRendering: 'pixelated' }} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{ imageRendering: 'pixelated' }} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style={{ imageRendering: 'pixelated' }} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{ imageRendering: 'pixelated' }} />
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style={{imageRendering: 'pixelated'}} /> <img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style={{ imageRendering: 'pixelated' }} />
</div> </div>
</section><section class="section"> </section><section class="section">
<h1>Not Found</h1> <h1>Not Found</h1>
@ -393,7 +396,8 @@ export class BattlePanel extends preact.Component<{id: string}> {
<p> <p>
In the future, remember to click <strong>Upload and share replay</strong> to save a replay permanently. In the future, remember to click <strong>Upload and share replay</strong> to save a replay permanently.
</p> </p>
</section></div>; </section>
</div>;
} }
renderControls() { renderControls() {
const atEnd = !this.battle || this.battle.atQueueEnd; const atEnd = !this.battle || this.battle.atQueueEnd;
@ -409,33 +413,38 @@ export class BattlePanel extends preact.Component<{id: string}> {
<button type="button" class="button" onClick={this.closeTurn}>Cancel</button> <button type="button" class="button" onClick={this.closeTurn}>Cancel</button>
</form> </form>
<p> <p>
<em>Pro tip:</em> You don't need to click "Skip to turn" if you have a keyboard, just start typing the turn number and press <kbd>Enter</kbd>. For more shortcuts, press <kbd>Shift</kbd>+<kbd>/</kbd> when a text box isn't focused. <em>Pro tip:</em> You don't need to click "Skip to turn" if you have a keyboard, just start typing
the turn number and press <kbd>Enter</kbd>. For more shortcuts, press <kbd>Shift</kbd>+<kbd>/</kbd>
when a text box isn't focused.
</p> </p>
</section></div>; </section></div>;
} }
return <div class="replay-controls"> return <div class="replay-controls">
<p> <p>
{atEnd && this.battle ? {atEnd && this.battle ? (
<button onClick={this.replay} class="button" style={{width: '5em', marginRight: '3px'}}> <button onClick={this.replay} class="button" style={{ width: '5em', marginRight: '3px' }}>
<i class="fa fa-undo"></i><br />Replay <i class="fa fa-undo"></i><br />Replay
</button> </button>
: (!this.battle || this.battle.paused) ? ) : !this.battle || this.battle.paused ? (
<button onClick={this.play} class="button" disabled={!this.battle} style={{width: '5em', marginRight: '3px'}}> <button onClick={this.play} class="button" disabled={!this.battle} style={{ width: '5em', marginRight: '3px' }}>
<i class="fa fa-play"></i><br /><strong>Play</strong> <i class="fa fa-play"></i><br /><strong>Play</strong>
</button> </button>
: ) : (
<button onClick={this.pause} class="button" style={{width: '5em', marginRight: '3px'}}> <button onClick={this.pause} class="button" style={{ width: '5em', marginRight: '3px' }}>
<i class="fa fa-pause"></i><br /><strong>Pause</strong> <i class="fa fa-pause"></i><br /><strong>Pause</strong>
</button> </button>
} {} )} {}
<button class="button button-first" disabled={atStart} onClick={this.firstTurn}> <button class="button button-first" disabled={atStart} onClick={this.firstTurn}>
<i class="fa fa-fast-backward"></i><br />First turn <i class="fa fa-fast-backward"></i><br />First turn
</button> </button>
<button class="button button-first" disabled={atStart} style={{marginLeft:'1px',position:'relative',zIndex:'1'}} onClick={this.prevTurn}> <button
class="button button-first" disabled={atStart} style={{ marginLeft: '1px', position: 'relative', zIndex: '1' }}
onClick={this.prevTurn}
>
<i class="fa fa-step-backward"></i><br />Prev turn <i class="fa fa-step-backward"></i><br />Prev turn
</button> </button>
<button class="button button-last" disabled={atEnd} style={{marginRight:'2px'}} onClick={this.nextTurn}> <button class="button button-last" disabled={atEnd} style={{ marginRight: '2px' }} onClick={this.nextTurn}>
<i class="fa fa-step-forward"></i><br />Skip turn <i class="fa fa-step-forward"></i><br />Skip turn
</button> </button>
<button class="button button-last" disabled={atEnd} onClick={this.lastTurn}> <button class="button button-last" disabled={atEnd} onClick={this.lastTurn}>
@ -458,7 +467,10 @@ export class BattlePanel extends preact.Component<{id: string}> {
</label> {} </label> {}
<label class="optgroup"> <label class="optgroup">
Sound:<br /> Sound:<br />
<select name="sound" class="button" onChange={this.changeSound} value={BattleSound.muted ? 'off' : BattleSound.bgmVolume ? 'on' : 'musicoff'}> <select
name="sound" class="button" onChange={this.changeSound}
value={BattleSound.muted ? 'off' : BattleSound.bgmVolume ? 'on' : 'musicoff'}
>
<option value="on">On</option> <option value="on">On</option>
<option value="musicoff">Music Off</option> <option value="musicoff">Music Off</option>
<option value="off">Muted</option> <option value="off">Muted</option>
@ -486,7 +498,7 @@ export class BattlePanel extends preact.Component<{id: string}> {
<em>Loading...</em> <em>Loading...</em>
</h1>} </h1>}
{this.result ? <p> {this.result ? <p>
<a class="button" href="#" onClick={this.clickDownload} style={{float: 'right'}}> <a class="button" href="#" onClick={this.clickDownload} style={{ float: 'right' }}>
<i class="fa fa-download" aria-hidden></i> Download <i class="fa fa-download" aria-hidden></i> Download
</a> </a>
{this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"} {this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"}
@ -502,19 +514,21 @@ export class BattlePanel extends preact.Component<{id: string}> {
let position: any = {}; let position: any = {};
if (PSRouter.showingLeft()) { if (PSRouter.showingLeft()) {
if (PSRouter.stickyRight) { if (PSRouter.stickyRight) {
position = {position: 'sticky', top: '0'}; position = { position: 'sticky', top: '0' };
} else { } else {
position = {position: 'sticky', bottom: '0'}; position = { position: 'sticky', bottom: '0' };
} }
} }
if (this.result === null) return this.renderError(position); if (this.result === null) return this.renderError(position);
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><div style={{position: 'relative'}}> return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}>
<div style={{ position: 'relative' }}>
<BattleDiv /> <BattleDiv />
<BattleLogDiv /> <BattleLogDiv />
{this.renderControls()} {this.renderControls()}
<div id="LeaderboardBTF"></div> <div id="LeaderboardBTF"></div>
</div></div>; </div>
</div>;
} }
} }

View File

@ -1,7 +1,7 @@
/** @jsx preact.h */ /** @jsx preact.h */
import preact from 'preact'; import preact from '../../play.pokemonshowdown.com/js/lib/preact';
import {Net, PSModel} from './utils'; import { Net, PSModel } from './utils';
import {BattlePanel} from './replays-battle'; import { BattlePanel } from './replays-battle';
declare function toID(input: string): string; declare function toID(input: string): string;
declare const Config: any; declare const Config: any;
@ -15,7 +15,7 @@ interface ReplayResult {
rating?: number; rating?: number;
} }
class SearchPanel extends preact.Component<{id: string}> { class SearchPanel extends preact.Component<{ id: string }> {
results: ReplayResult[] | null = null; results: ReplayResult[] | null = null;
resultError: string | null = null; resultError: string | null = null;
format = ''; format = '';
@ -28,7 +28,7 @@ class SearchPanel extends preact.Component<{id: string}> {
sort = 'date'; sort = 'date';
override componentDidMount() { override componentDidMount() {
Net('/check-login.php').get().then(result => { Net('/check-login.php').get().then(result => {
if (result.charAt(0) !== ']') return; if (!result.startsWith(']')) return;
const [userid, sysop] = result.slice(1).split(','); const [userid, sysop] = result.slice(1).split(',');
this.loggedInUser = userid; this.loggedInUser = userid;
this.loggedInUserIsSysop = !!sysop; this.loggedInUserIsSysop = !!sysop;
@ -43,7 +43,7 @@ class SearchPanel extends preact.Component<{id: string}> {
const byRating = (query.sort === 'rating'); const byRating = (query.sort === 'rating');
if (page !== this.page || byRating !== this.byRating) this.updateSearch(query); if (page !== this.page || byRating !== this.byRating) this.updateSearch(query);
} }
updateSearch(query: {[k: string]: string}) { updateSearch(query: { [k: string]: string }) {
const user = query.user || ''; const user = query.user || '';
const format = query.format || ''; const format = query.format || '';
const page = parseInt(query.page || '1'); const page = parseInt(query.page || '1');
@ -56,7 +56,7 @@ class SearchPanel extends preact.Component<{id: string}> {
this.resultError = null; this.resultError = null;
if (isPrivate) { if (isPrivate) {
if (response.charAt(0) !== ']') { if (!response.startsWith(']')) {
this.resultError = `Unrecognized response: ${response}`; this.resultError = `Unrecognized response: ${response}`;
return; return;
} }
@ -72,7 +72,7 @@ class SearchPanel extends preact.Component<{id: string}> {
search(user: string, format: string, isPrivate?: boolean, page = 1) { search(user: string, format: string, isPrivate?: boolean, page = 1) {
this.base!.querySelector<HTMLInputElement>('input[name=user]')!.value = user; this.base!.querySelector<HTMLInputElement>('input[name=user]')!.value = user;
this.base!.querySelector<HTMLInputElement>('input[name=format]')!.value = format; this.base!.querySelector<HTMLInputElement>('input[name=format]')!.value = format;
this.base!.querySelectorAll<HTMLInputElement>('input[name=private]')[isPrivate ? 1 : 0]!.checked = true; this.base!.querySelectorAll<HTMLInputElement>('input[name=private]')[isPrivate ? 1 : 0].checked = true;
if (!format && !user) return this.recent(); if (!format && !user) return this.recent();
this.user = user; this.user = user;
@ -84,7 +84,7 @@ class SearchPanel extends preact.Component<{id: string}> {
if (user || !format) this.byRating = false; if (user || !format) this.byRating = false;
if (!format && !user) { if (!format && !user) {
PSRouter.replace('') PSRouter.replace('');
} else { } else {
PSRouter.replace('?' + Net.encodeQuery({ PSRouter.replace('?' + Net.encodeQuery({
user: user || undefined, user: user || undefined,
@ -112,7 +112,7 @@ class SearchPanel extends preact.Component<{id: string}> {
this.forceUpdate(); this.forceUpdate();
}); });
} }
modLink(overrides: {page?: number, sort?: string}) { modLink(overrides: { page?: number, sort?: string }) {
const newPage = (overrides.page !== undefined ? this.page + overrides.page : 1); const newPage = (overrides.page !== undefined ? this.page + overrides.page : 1);
return './?' + Net.encodeQuery({ return './?' + Net.encodeQuery({
user: this.user || undefined, user: this.user || undefined,
@ -150,7 +150,7 @@ class SearchPanel extends preact.Component<{id: string}> {
}; };
searchLoggedIn = (e: Event) => { searchLoggedIn = (e: Event) => {
if (!this.loggedInUser) return; // shouldn't happen if (!this.loggedInUser) return; // shouldn't happen
(this.base!.querySelector('input[name=user]') as HTMLInputElement).value = this.loggedInUser; this.base!.querySelector<HTMLInputElement>('input[name=user]')!.value = this.loggedInUser;
this.submitForm(e); this.submitForm(e);
}; };
url(replay: ReplayResult) { url(replay: ReplayResult) {
@ -196,7 +196,8 @@ class SearchPanel extends preact.Component<{id: string}> {
<label> <label>
Username:<br /> Username:<br />
<input type="search" class="textbox" name="user" placeholder="(blank = any user)" size={20} /> {} <input type="search" class="textbox" name="user" placeholder="(blank = any user)" size={20} /> {}
{this.loggedInUser && <button type="button" class="button" onClick={this.searchLoggedIn}>{this.loggedInUser}'s replays</button>} {this.loggedInUser &&
<button type="button" class="button" onClick={this.searchLoggedIn}>{this.loggedInUser}'s replays</button>}
</label> </label>
</p> </p>
<p> <p>
@ -214,19 +215,19 @@ class SearchPanel extends preact.Component<{id: string}> {
{activelySearching && <h1 aria-label="Results"></h1>} {activelySearching && <h1 aria-label="Results"></h1>}
{activelySearching && this.format && !this.user && <p> {activelySearching && this.format && !this.user && <p>
Sort by: {} Sort by: {}
<a href={this.modLink({sort: 'date'})} class={`button button-first${this.byRating ? '' : ' disabled'}`}> <a href={this.modLink({ sort: 'date' })} class={`button button-first${this.byRating ? '' : ' disabled'}`}>
Date Date
</a> </a>
<a href={this.modLink({sort: 'rating'})} class={`button button-last${this.byRating ? ' disabled' : ''}`}> <a href={this.modLink({ sort: 'rating' })} class={`button button-last${this.byRating ? ' disabled' : ''}`}>
Rating Rating
</a> </a>
</p>} </p>}
{activelySearching && this.page > 1 && <p class="pagelink"> {activelySearching && this.page > 1 && <p class="pagelink">
<a href={this.modLink({page: -1})} class="button"><i class="fa fa-caret-up"></i><br />Page {this.page - 1}</a> <a href={this.modLink({ page: -1 })} class="button"><i class="fa fa-caret-up"></i><br />Page {this.page - 1}</a>
</p>} </p>}
{activelySearching && searchResults} {activelySearching && searchResults}
{activelySearching && (this.results?.length || 0) > 50 && <p class="pagelink"> {activelySearching && (this.results?.length || 0) > 50 && <p class="pagelink">
<a href={this.modLink({page: 1})} class="button">Page {this.page + 1}<br /><i class="fa fa-caret-down"></i></a> <a href={this.modLink({ page: 1 })} class="button">Page {this.page + 1}<br /><i class="fa fa-caret-down"></i></a>
</p>} </p>}
</form> </form>
</section> </section>
@ -274,7 +275,7 @@ class FeaturedReplays extends preact.Component {
<strong>Metal Brellow</strong> vs. <strong>zig100</strong> <strong>Metal Brellow</strong> vs. <strong>zig100</strong>
<small><br />Topsy-Turvy</small> <small><br />Topsy-Turvy</small>
</a></li> </a></li>
{!this.moreFun && <li style={{paddingLeft: '8px'}}> {!this.moreFun && <li style={{ paddingLeft: '8px' }}>
<button class="button" onClick={this.showMoreFun}>More <i class="fa fa-caret-right" aria-hidden></i></button> <button class="button" onClick={this.showMoreFun}>More <i class="fa fa-caret-right" aria-hidden></i></button>
</li>} </li>}
{this.moreFun && <li><a href="smogondoubles-75588440?p2" class="blocklink"> {this.moreFun && <li><a href="smogondoubles-75588440?p2" class="blocklink">
@ -296,55 +297,81 @@ class FeaturedReplays extends preact.Component {
<li><a href="doublesou-232753081" class="blocklink"> <li><a href="doublesou-232753081" class="blocklink">
<small>[gen6-doublesou]<br /></small> <small>[gen6-doublesou]<br /></small>
<strong>Electrolyte</strong> vs. <strong>finally</strong> <strong>Electrolyte</strong> vs. <strong>finally</strong>
<small><br />finally steals Electrolyte's spot in the finals of the Doubles Winter Seasonal by outplaying Toxic Aegislash.</small> <small><br />
finally steals Electrolyte's spot in the finals of the Doubles Winter Seasonal by outplaying Toxic Aegislash.
</small>
</a></li> </a></li>
<li><a href="smogtours-gen5ou-59402" class="blocklink"> <li><a href="smogtours-gen5ou-59402" class="blocklink">
<small>[gen5-ou]<br /></small> <small>[gen5-ou]<br /></small>
<strong>Reymedy</strong> vs. <strong>Leftiez</strong> <strong>Reymedy</strong> vs. <strong>Leftiez</strong>
<small><br />Reymedy's superior grasp over BW OU lead to his claim of victory over Leftiez in the No Johns Tournament.</small> <small><br />
Reymedy's superior grasp over BW OU lead to his claim of victory over Leftiez in the No Johns Tournament.
</small>
</a></li> </a></li>
<li><a href="smogtours-gen3ou-56583" class="blocklink"> <li><a href="smogtours-gen3ou-56583" class="blocklink">
<small>[gen3-ou]<br /></small> <small>[gen3-ou]<br /></small>
<strong>pokebasket</strong> vs. <strong>Alf'</strong> <strong>pokebasket</strong> vs. <strong>Alf'</strong>
<small><br />pokebasket proved Blissey isn't really one to take a Focus Punch well in his victory match over Alf' in the Fuck Trappers ADV OU tournament.</small> <small><br />
pokebasket proved Blissey isn't really one to take a Focus Punch well in his victory match over Alf' in the
Fuck Trappers ADV OU tournament.
</small>
</a></li> </a></li>
<li><a href="smogtours-ou-55891" class="blocklink"> <li><a href="smogtours-ou-55891" class="blocklink">
<small>[gen6-ou]<br /></small> <small>[gen6-ou]<br /></small>
<strong>Marshall.Law</strong> vs. <strong>Malekith</strong> <strong>Marshall.Law</strong> vs. <strong>Malekith</strong>
<small><br />In a "match full of reverses", Marshall.Law takes on Malekith in the finals of It's No Use.</small> <small><br />
In a "match full of reverses", Marshall.Law takes on Malekith in the finals of It's No Use.
</small>
</a></li> </a></li>
<li><a href="smogtours-ubers-54583" class="blocklink"> <li><a href="smogtours-ubers-54583" class="blocklink">
<small>[gen6-custom]<br /></small> <small>[gen6-custom]<br /></small>
<strong>hard</strong> vs. <strong>panamaxis</strong> <strong>hard</strong> vs. <strong>panamaxis</strong>
<small><br />Dark horse panamaxis proves his worth as the rightful winner of The Walkthrough Tournament in this exciting final versus hard.</small> <small><br />
Dark horse panamaxis proves his worth as the rightful winner of The Walkthrough Tournament in this exciting
final versus hard.
</small>
</a></li> </a></li>
{!this.moreCompetitive && <li style={{paddingLeft: '8px'}}> {!this.moreCompetitive && <li style={{ paddingLeft: '8px' }}>
<button class="button" onClick={this.showMoreCompetitive}>More <i class="fa fa-caret-right" aria-hidden></i></button> <button class="button" onClick={this.showMoreCompetitive}>More <i class="fa fa-caret-right" aria-hidden></i></button>
</li>} </li>}
{this.moreCompetitive && <li><a href="smogtours-ubers-34646" class="blocklink"> {this.moreCompetitive && <li><a href="smogtours-ubers-34646" class="blocklink">
<small>[gen6-ubers]<br /></small> <small>[gen6-ubers]<br /></small>
<strong>steelphoenix</strong> vs. <strong>Jibaku</strong> <strong>steelphoenix</strong> vs. <strong>Jibaku</strong>
<small><br />In this SPL Week 4 battle, Jibaku's clever plays with Mega Sableye keep the momentum mostly in his favor.</small> <small><br />
In this SPL Week 4 battle, Jibaku's clever plays with Mega Sableye keep the momentum mostly in his favor.
</small>
</a></li>} </a></li>}
{this.moreCompetitive && <li><a href="smogtours-uu-36860" class="blocklink"> {this.moreCompetitive && <li><a href="smogtours-uu-36860" class="blocklink">
<small>[gen6-uu]<br /></small> <small>[gen6-uu]<br /></small>
<strong>IronBullet93</strong> vs. <strong>Laurel</strong> <strong>IronBullet93</strong> vs. <strong>Laurel</strong>
<small><br />Laurel outplays IronBullet's Substitute Tyrantrum with the sly use of a Shuca Berry Cobalion, but luck was inevitably the deciding factor in this SPL Week 6 match.</small> <small><br />
Laurel outplays IronBullet's Substitute Tyrantrum with the sly use of a Shuca Berry Cobalion, but luck was
inevitably the deciding factor in this SPL Week 6 match.
</small>
</a></li>} </a></li>}
{this.moreCompetitive && <li><a href="smogtours-gen5ou-36900" class="blocklink"> {this.moreCompetitive && <li><a href="smogtours-gen5ou-36900" class="blocklink">
<small>[gen5-ou]<br /></small> <small>[gen5-ou]<br /></small>
<strong>Lowgock</strong> vs. <strong>Meridian</strong> <strong>Lowgock</strong> vs. <strong>Meridian</strong>
<small><br />This SPL Week 6 match features impressive plays, from Jirachi sacrificing itself to paralysis to avoid a burn to some clever late-game switches.</small> <small><br />
This SPL Week 6 match features impressive plays, from Jirachi sacrificing itself to paralysis to avoid a
burn to some clever late-game switches.
</small>
</a></li>} </a></li>}
{this.moreCompetitive && <li><a href="smogtours-gen4ou-36782" class="blocklink"> {this.moreCompetitive && <li><a href="smogtours-gen4ou-36782" class="blocklink">
<small>[gen4-ou]<br /></small> <small>[gen4-ou]<br /></small>
<strong>Heist</strong> vs. <strong>liberty32</strong> <strong>Heist</strong> vs. <strong>liberty32</strong>
<small><br />Starting out as an entry hazard-filled stallfest, this close match is eventually decided by liberty32's efficient use of Aerodactyl.</small> <small><br />
Starting out as an entry hazard-filled stallfest, this close match is eventually decided by liberty32's
efficient use of Aerodactyl.
</small>
</a></li>} </a></li>}
{this.moreCompetitive && <li><a href="randombattle-213274483" class="blocklink"> {this.moreCompetitive && <li><a href="randombattle-213274483" class="blocklink">
<small>[gen6-randombattle]<br /></small> <small>[gen6-randombattle]<br /></small>
<strong>The Immortal</strong> vs. <strong>Amphinobite</strong> <strong>The Immortal</strong> vs. <strong>Amphinobite</strong>
<small><br />Substitute Lugia and Rotom-Fan take advantage of Slowking's utility and large HP stat, respectively, in this high ladder match.</small> <small><br />
Substitute Lugia and Rotom-Fan take advantage of Slowking's utility and large HP stat, respectively,
in this high ladder match.
</small>
</a></li>} </a></li>}
</ul> </ul>
</section>; </section>;
@ -465,11 +492,13 @@ export class PSReplays extends preact.Component {
} }
override render() { override render() {
const position = PSRouter.showingLeft() && PSRouter.showingRight() && !PSRouter.stickyRight ? const position = PSRouter.showingLeft() && PSRouter.showingRight() && !PSRouter.stickyRight ?
{display: 'flex', flexDirection: 'column', justifyContent: 'flex-end'} : {}; { display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' } : {};
return <div class={'bar-wrapper' + (PSRouter.showingLeft() && PSRouter.showingRight() ? ' has-sidebar' : '')} style={position}> return <div
class={'bar-wrapper' + (PSRouter.showingLeft() && PSRouter.showingRight() ? ' has-sidebar' : '')} style={position}
>
{PSRouter.showingLeft() && <SearchPanel id={PSRouter.leftLoc!} />} {PSRouter.showingLeft() && <SearchPanel id={PSRouter.leftLoc!} />}
{PSRouter.showingRight() && <BattlePanel id={PSRouter.rightLoc!} />} {PSRouter.showingRight() && <BattlePanel id={PSRouter.rightLoc!} />}
<div style={{clear: 'both'}}></div> <div style={{ clear: 'both' }}></div>
</div>; </div>;
} }
} }

View File

@ -20,7 +20,7 @@ export class HttpError extends Error {
this.body = body; this.body = body;
try { try {
(Error as any).captureStackTrace(this, HttpError); (Error as any).captureStackTrace(this, HttpError);
} catch (err) {} } catch {}
} }
} }
export class NetRequest { export class NetRequest {
@ -100,8 +100,8 @@ Net.encodeQuery = function (data: string | PostData) {
} }
return urlencodedData; return urlencodedData;
}; };
Net.decodeQuery = function (query: string): {[key: string]: string} { Net.decodeQuery = function (query: string): { [key: string]: string } {
let out = {}; let out: { [key: string]: string } = {};
const questionIndex = query.indexOf('?'); const questionIndex = query.indexOf('?');
if (questionIndex >= 0) query = query.slice(questionIndex + 1); if (questionIndex >= 0) query = query.slice(questionIndex + 1);
for (const queryPart of query.split('&')) { for (const queryPart of query.split('&')) {

View File

@ -15,6 +15,7 @@
"types": [], "types": [],
"include": [ "include": [
"./play.pokemonshowdown.com/js/lib/preact.d.ts", "./play.pokemonshowdown.com/js/lib/preact.d.ts",
"./play.pokemonshowdown.com/src/*" "./play.pokemonshowdown.com/src/*",
"./replay.pokemonshowdown.com/src/*"
] ]
} }

View File

@ -1,58 +0,0 @@
{
"extends": "tslint:latest",
"defaultSeverity": "error",
"jsRules": {},
"rules": {
"array-type": [true, "array"],
"arrow-parens": [true, "ban-single-arg-parens"],
"curly": [true, "ignore-same-line"],
"indent": [true, "tabs", 2],
"quotemark": false,
"member-access": [true, "no-public"],
"no-empty": [true, "allow-empty-catch", "allow-empty-functions"],
"no-string-literal": false,
"interface-over-type-literal": false,
// TODO: turn on
"prefer-const": false,
"max-line-length": {
"options": {
"limit": 120,
"ignore-pattern": "template\\.replace\\(('\\[|/)|[a-z0-9]=|^\\s*(// \\s*)?((let |const )?[a-zA-Z0-9$.]+ \\+?= (\\$\\()?|(return |throw )?(new )?([a-zA-Z0-9$.]+\\()?)?['\"`/]"
}
},
"interface-name": false,
"forin": false,
// maybe one day
"member-ordering": false,
"max-classes-per-file": false,
// they look weird in `new class {...}`
"new-parens": false,
"no-bitwise": false,
"no-console": false,
"no-namespace": [true, "allow-declarations"],
"only-arrow-functions": false,
"prefer-conditional-expression": false,
"no-shadowed-variable": [true, {"temporalDeadZone": false}],
"no-switch-case-fall-through": true,
"no-unnecessary-initializer": false,
"object-literal-sort-keys": false,
"object-literal-key-quotes": false,
"ordered-imports": false,
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "never",
"typeLiterals": "always"
},
"singleline": "never",
"esSpecCompliant": true
}
],
"semicolon": [true, "always", "strict-bound-class-methods"],
"space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}]
},
"rulesDirectory": []
}