diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c90dc3ec2b2e..c369b59a7b2d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -9,6 +9,15 @@ At the highest level, PS is split into three parts: All three communicate directly with each other. +A user starts by visiting `https://play.pokemonshowdown.com/`. This is handled by an Apache server (in the Client), which serves mostly static files but uses some PHP (legacy, intended to be migrated to Loginserver). + +The user's web browser (running Client code) will then communicate with the Login server (mounted at `https://play.pokemonshowdown.com/api/` to handle logins mostly, or otherwise interface with the Client databases one way or another). + +The user's web browser will also connect to the Game server, through SockJS. The Game server handles the chat rooms, matchmaking, and actual battle simulation. + +The Game server also communicates with the Login server, to handle replay uploads (and, for the main server, ladder updates). + + Game server ----------- @@ -26,6 +35,7 @@ Its entry point is [server/index.ts](./server/index.ts), which launches several `Rooms` also includes support for battle rooms, which is where the server connects to the game simulator itself. Game simulation code is in [sim/](./sim/). + Client ------ @@ -35,9 +45,12 @@ Its entry point is [index.template.html](https://github.com/smogon/pokemon-showd It was written long ago, so instead of a single JS entry point, it includes a lot of JS files. Everything important is launched from [js/client.js](https://github.com/smogon/pokemon-showdown-client/blob/master/play.pokemonshowdown.com/js/client.js) + Login server ------------ -The client’s login server, which handles logins and most database interaction, is written in TypeScript in progress. The backend is split between a MySQL InnoDB database (for most things) and a Postgres database (for Replays). +The client’s login server, which handles logins and most database interaction, is written in TypeScript. The backend is currently split between a MySQL InnoDB database (for users, ladder, and most other things) and a Postgres (technically Cockroach) database (for Replays). Its entry point is [server.ts](https://github.com/smogon/pokemon-showdown-loginserver/blob/master/src/server.ts). + +It's intended to replace all of the old PHP code in the Client, but that migration is only halfway done at the moment. diff --git a/config/formats.ts b/config/formats.ts index d4664ca96341..b028c8dc69df 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -241,12 +241,6 @@ export const Formats: import('../sim/dex-formats').FormatList = [ ruleset: ['[Gen 3] OU', 'Pre RU Tier Shift Mod'], searchShow: true, }, - { - name: "[Gen 3] RU", - mod: 'gen3', - ruleset: ['[Gen 3] UU'], - banlist: ['UU', 'RUBL'], - }, { name: "[Gen 3] SU", mod: 'gen3', @@ -940,13 +934,13 @@ export const Formats: import('../sim/dex-formats').FormatList = [ desc: `All forms of passive damage deal type-based damage based on the primary type of the Pokémon that inflicted the passive damage against the target Pokémon.`, mod: 'passiveaggressive', // searchShow: false, - ruleset: ['Standard OMs', 'Sleep Moves Clause', 'Evasion Items Clause'], + ruleset: ['Standard OMs', 'Sleep Moves Clause', 'Evasion Items Clause', 'Terastal Clause'], banlist: [ - 'Arceus', 'Baxcalibur', 'Calyrex-Ice', 'Calyrex-Shadow', 'Chi-Yu', 'Chien-Pao', 'Deoxys-Attack', 'Deoxys-Normal', 'Dialga', 'Dialga-Origin', 'Eternatus', 'Flutter Mane', - 'Gholdengo', 'Giratina', 'Giratina-Origin', 'Gouging Fire', 'Groudon', 'Ho-Oh', 'Iron Bundle', 'Koraidon', 'Kyogre', 'Kyurem', 'Kyurem-Black', 'Kyurem-White', 'Lugia', - 'Lunala', 'Magearna', 'Mewtwo', 'Miraidon', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Ogerpon-Hearthflame', 'Palafin', 'Palkia', 'Palkia-Origin', 'Raging Bolt', - 'Rayquaza', 'Reshiram', 'Shaymin-Sky', 'Sneasler', 'Solgaleo', 'Zacian', 'Zacian-Crowned', 'Zamazenta-Crowned', 'Zekrom', 'Arena Trap', 'Moody', 'Shadow Tag', 'Speed Boost', - 'Heat Rock', 'King\'s Rock', 'Razor Fang', 'Quick Claw', 'Baton Pass', 'Last Respects', 'Shed Tail', + 'Annihilape', 'Arceus', 'Baxcalibur', 'Calyrex-Ice', 'Calyrex-Shadow', 'Chi-Yu', 'Chien-Pao', 'Deoxys-Attack', 'Deoxys-Normal', 'Dialga', 'Dialga-Origin', 'Eternatus', 'Flutter Mane', + 'Gholdengo', 'Giratina', 'Giratina-Origin', 'Gouging Fire', 'Groudon', 'Ho-Oh', 'Iron Bundle', 'Koraidon', 'Kyogre', 'Kyurem', 'Kyurem-Black', 'Kyurem-White', 'Landorus-Incarnate', + 'Lugia', 'Lunala', 'Magearna', 'Mewtwo', 'Miraidon', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Ogerpon-Hearthflame', 'Palafin', 'Palkia', 'Palkia-Origin', 'Raging Bolt', 'Rayquaza', + 'Reshiram', 'Shaymin-Sky', 'Sneasler', 'Solgaleo', 'Spectrier', 'Ursaluna-Bloodmoon', 'Zacian', 'Zacian-Crowned', 'Zamazenta-Crowned', 'Zekrom', 'Arena Trap', 'Moody', 'Shadow Tag', + 'Speed Boost', 'Heat Rock', 'King\'s Rock', 'Razor Fang', 'Quick Claw', 'Baton Pass', 'Last Respects', 'Shed Tail', ], }, { @@ -1660,7 +1654,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ 'Sand Veil', 'Snow Cloak', 'King\'s Rock', 'Razor Fang', 'Baton Pass', 'Dire Claw', 'Last Respects', 'Shed Tail', ], restricted: [ - 'Beat Up', 'Belly Drum', 'Clangorous Soul', 'Dragon Dance', 'Endeavor', 'Quiver Dance', 'Shell Smash', 'Shift Gear', 'Tail Glow', 'Tidy Up', 'Victory Dance', + 'Belly Drum', 'Clangorous Soul', 'Dragon Dance', 'Endeavor', 'Quiver Dance', 'Shell Smash', 'Shift Gear', 'Tail Glow', 'Tidy Up', 'Victory Dance', ], onValidateSet(set) { const fsMove = this.dex.moves.get(set.moves[0]); @@ -1680,6 +1674,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ const moveData = this.dex.getActiveMove(move.id); moveData.flags['futuremove'] = 1; delete moveData.flags['protect']; + if (moveData.id === 'beatup') this.singleEvent('ModifyMove', moveData, null, pokemon, null, null, moveData); Object.assign(t.side.slotConditions[t.position]['futuremove'], { duration: 3, move: moveData.id, @@ -2673,13 +2668,13 @@ export const Formats: import('../sim/dex-formats').FormatList = [ 'Sleep Clause Mod', 'Forme Clause', 'Z-Move Clause', 'Terastal Clause', 'Mega Rayquaza Clause', ], banlist: [ - 'ND Uber', 'ND AG', 'ND OU', 'ND UUBL', 'ND UU', 'ND RUBL', 'ND RU', 'ND NFE', ' ND LC', + 'ND Uber', 'ND AG', 'ND OU', 'ND UUBL', 'ND UU', 'ND RUBL', 'ND RU', 'ND NFE', 'ND LC', 'Battle Bond', 'Moody', 'Shadow Tag', 'Berserk Gene', 'King\'s Rock', 'Quick Claw', 'Razor Fang', 'Acupressure', 'Last Respects', ], unbanlist: [ - 'Ampharos-Base', 'Appletun', 'Araquanid', 'Arbok', 'Centiskorch', 'Drampa', 'Dusknoir', 'Exeggutor-Alola', 'Exeggutor-Base', 'Frosmoth', 'Gabite', 'Glaceon', - 'Golduck', 'Gourgeist-Average', 'Gourgeist-Super', 'Granbull', 'Gumshoos', 'Guzzlord', 'Kecleon', 'Ledian', 'Lurantis', 'Oricorio-Baile', 'Overqwil', - 'Pinsir-Base', 'Qwilfish-Base', 'Reuniclus', 'Shedinja', 'Shelgon', 'Spiritomb', 'Trapinch', 'Tsareena', 'Turtonator', 'Unown', 'Wigglytuff', 'Wishiwashi', + 'Ampharos-Base', 'Appletun', 'Araquanid', 'Arbok', 'Centiskorch', 'Drampa', 'Dusknoir', 'Exeggutor-Alola', 'Exeggutor-Base', 'Frosmoth', 'Gabite', 'Galvantula', + 'Glaceon', 'Golduck', 'Gorebyss', 'Gourgeist-Average', 'Gourgeist-Super', 'Granbull', 'Gumshoos', 'Guzzlord', 'Kecleon', 'Ledian', 'Lurantis', 'Pinsir-Base', + 'Qwilfish-Base', 'Reuniclus', 'Shedinja', 'Shelgon', 'Spiritomb', 'Trapinch', 'Tsareena', 'Turtonator', 'Unown', 'Wigglytuff', 'Wishiwashi', ], // Stupid hardcode onValidateSet(set, format, setHas, teamHas) { @@ -4547,18 +4542,13 @@ export const Formats: import('../sim/dex-formats').FormatList = [ ruleset: ['Standard'], banlist: ['Uber', 'OU', 'UUBL', 'Smeargle + Ingrain', 'Arena Trap', 'Baton Pass', 'Swagger'], }, - // { - // name: "[Gen 3] RU", - // mod: 'gen3', - // searchShow: false, - // ruleset: ['[Gen 3] UU'], - // banlist: [ - // 'Altaria', 'Ampharos', 'Arcanine', 'Blastoise', 'Clefable', 'Cradily', 'Electabuzz', 'Electrode', 'Fearow', 'Feraligatr', 'Gligar', 'Golduck', - // 'Golem', 'Gorebyss', 'Granbull', 'Grumpig', 'Hitmonlee', 'Hitmontop', 'Jumpluff', 'Kangaskhan', 'Lanturn', 'Lunatone', 'Manectric', 'Misdreavus', - // 'Muk', 'Nidoking', 'Nidoqueen', 'Ninjask', 'Omastar', 'Pinsir', 'Qwilfish', 'Sandslash', 'Scyther', 'Slowking', 'Solrock', 'Tentacruel', 'Vileplume', - // 'Walrein', 'Xatu', - // ], - // }, + { + name: "[Gen 3] RU", + mod: 'gen3', + searchShow: false, + ruleset: ['Standard'], + banlist: ['Uber', 'OU', 'UUBL', 'UU', 'RUBL', 'Smeargle + Ingrain', 'Arena Trap', 'Baton Pass', 'Swagger'], + }, { name: "[Gen 3] NU", mod: 'gen3', @@ -4571,7 +4561,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ mod: 'gen3', searchShow: false, ruleset: ['Standard', 'Baton Pass Stat Clause'], - banlist: ['Uber', 'OU', 'UUBL', 'UU', 'NUBL', 'NU', 'PUBL'], + banlist: ['Uber', 'OU', 'UUBL', 'UU', 'RUBL', 'RU', 'NUBL', 'NU', 'PUBL'], }, { name: "[Gen 3] LC", @@ -4600,7 +4590,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [ mod: 'gen3', searchShow: false, ruleset: ['Standard', 'Sleep Moves Clause', 'Baton Pass Stat Trap Clause', 'Swagger Clause'], - banlist: ['Uber', 'OU', 'UUBL', 'UU', 'NUBL', 'NU', 'PUBL', 'PU', 'ZUBL', 'Baton Pass + Substitute'], + banlist: ['Uber', 'OU', 'UUBL', 'UU', 'RUBL', 'RU', 'NUBL', 'NU', 'PUBL', 'PU', 'ZUBL', 'Baton Pass + Substitute'], }, { name: "[Gen 3] Custom Game", diff --git a/data/cg-teams.ts b/data/cg-teams.ts index 5748bbbc2033..2de0d5b345a2 100644 --- a/data/cg-teams.ts +++ b/data/cg-teams.ts @@ -297,7 +297,7 @@ export default class TeamGenerator { let types = nonStatusMoves.map(move => TeamGenerator.moveType(this.dex.moves.get(move), species)); const noStellar = ability === 'Adaptability' || new Set(types).size < 3; if (hasTeraBlast || hasRevelationDance || !nonStatusMoves.length) { - types = [...this.dex.types.all().map(t => t.name)]; + types = [...this.dex.types.names()]; if (noStellar) types.splice(types.indexOf('Stellar')); } else { if (!noStellar) types.push('Stellar'); @@ -327,23 +327,23 @@ export default class TeamGenerator { */ protected speciesIsGoodFit(species: Species, stats: TeamStats): boolean { // type check - for (const type of this.dex.types.all()) { - const effectiveness = this.dex.getEffectiveness(type.name, species.types); + for (const typeName of this.dex.types.names()) { + const effectiveness = this.dex.getEffectiveness(typeName, species.types); if (effectiveness === 1) { // WEAKNESS! - if (stats.typeWeaknesses[type.name] === undefined) { - stats.typeWeaknesses[type.name] = 0; + if (stats.typeWeaknesses[typeName] === undefined) { + stats.typeWeaknesses[typeName] = 0; } - if (stats.typeWeaknesses[type.name] >= MAX_WEAK_TO_SAME_TYPE) { + if (stats.typeWeaknesses[typeName] >= MAX_WEAK_TO_SAME_TYPE) { // too many weaknesses to this type return false; } } } // species passes; increment counters - for (const type of this.dex.types.all()) { - const effectiveness = this.dex.getEffectiveness(type.name, species.types); + for (const typeName of this.dex.types.names()) { + const effectiveness = this.dex.getEffectiveness(typeName, species.types); if (effectiveness === 1) { - stats.typeWeaknesses[type.name]++; + stats.typeWeaknesses[typeName]++; } } return true; diff --git a/data/formats-data.ts b/data/formats-data.ts index eedf824c717a..76ffe6e72c10 100644 --- a/data/formats-data.ts +++ b/data/formats-data.ts @@ -5605,7 +5605,7 @@ export const FormatsData: import('../sim/dex-species').SpeciesFormatsDataTable = natDexTier: "RU", }, okidogi: { - tier: "UU", + tier: "UUBL", doublesTier: "(DUU)", natDexTier: "RU", }, diff --git a/data/learnsets.ts b/data/learnsets.ts index b4d0c64d0bed..4473fb447a5f 100644 --- a/data/learnsets.ts +++ b/data/learnsets.ts @@ -67280,7 +67280,7 @@ export const Learnsets: import('../sim/dex-species').LearnsetDataTable = { drainpunch: ["9M", "7T", "6T", "5T"], dreameater: ["7M", "6M", "5M"], dualchop: ["7T", "6T", "5T"], - echoedvoice: ["9L36", "7M", "7L36", "6M", "6L36", "5M", "5L36"], + echoedvoice: ["9L36", "9S5", "7M", "7L36", "6M", "6L36", "5M", "5L36"], embargo: ["7M", "6M", "5M"], endure: ["9M"], energyball: ["9M", "7M", "6M", "5M"], @@ -67316,14 +67316,14 @@ export const Learnsets: import('../sim/dex-species').LearnsetDataTable = { playrough: ["9M"], poweruppunch: ["6M"], protect: ["9M", "7M", "6M", "5M"], - psybeam: ["9M", "9L31", "7L31", "6L31", "5L31"], + psybeam: ["9M", "9L31", "9S5", "7L31", "6L31", "5L31"], psychic: ["9M", "9L57", "9S4", "7M", "7L57", "7S2", "6M", "6L57", "5M", "5L57", "5S1"], psychup: ["9M", "7M", "6M", "5M"], psyshock: ["9M", "7M", "6M", "5M"], quickattack: ["9L1", "7L1", "6L6", "5L6", "5S0"], raindance: ["9M", "7M", "6M", "5M"], recycle: ["7T", "6T", "5T"], - relicsong: ["9L50", "9S4", "7T", "7S3", "6T", "5T"], + relicsong: ["9L50", "9S4", "9S5", "7T", "7S3", "6T", "5T"], rest: ["9M", "7M", "6M", "5M"], retaliate: ["6M", "5M"], return: ["7M", "6M", "5M"], @@ -67337,7 +67337,7 @@ export const Learnsets: import('../sim/dex-species').LearnsetDataTable = { shadowclaw: ["9M", "7M", "6M", "5M"], shockwave: ["7T", "6T"], signalbeam: ["7T", "6T", "5T"], - sing: ["9L1", "9S4", "7L1", "7S2", "7S3", "6L16", "5L16"], + sing: ["9L1", "9S4", "9S5", "7L1", "7S2", "7S3", "6L16", "5L16"], skillswap: ["9M", "7T", "6T", "5T"], sleeptalk: ["9M", "7M", "6M", "5T"], snatch: ["7T", "6T", "5T"], @@ -67373,6 +67373,7 @@ export const Learnsets: import('../sim/dex-species').LearnsetDataTable = { {generation: 7, level: 15, moves: ["sing", "psychic", "closecombat"], pokeball: "cherishball"}, {generation: 7, level: 50, moves: ["sing", "celebrate", "round", "relicsong"], pokeball: "cherishball"}, {generation: 9, level: 70, moves: ["relicsong", "hypervoice", "sing", "psychic"]}, + {generation: 9, level: 50, shiny: true, nature: "Modest", ivs: {hp: 20, atk: 20, def: 20, spa: 31, spd: 31, spe: 31}, moves: ["relicsong", "echoedvoice", "psybeam", "sing"], pokeball: "cherishball"}, ], eventOnly: true, }, diff --git a/data/mods/gen1rbycap/moves.ts b/data/mods/gen1rbycap/moves.ts index 14e95c12f70a..7be673a05905 100644 --- a/data/mods/gen1rbycap/moves.ts +++ b/data/mods/gen1rbycap/moves.ts @@ -74,7 +74,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { icicle: { accuracy: 100, basePower: 70, - category: "Physical", + category: "Special", shortDesc: "High critical hit ratio.", name: "Icicle", pp: 15, diff --git a/data/mods/gen3/formats-data.ts b/data/mods/gen3/formats-data.ts index 3cce6eac8c5b..7069a5a73680 100644 --- a/data/mods/gen3/formats-data.ts +++ b/data/mods/gen3/formats-data.ts @@ -111,7 +111,7 @@ export const FormatsData: import('../../../sim/dex-species').ModdedSpeciesFormat tier: "SU", }, clefable: { - tier: "UU", + tier: "RU", }, vulpix: { tier: "IU", @@ -576,7 +576,7 @@ export const FormatsData: import('../../../sim/dex-species').ModdedSpeciesFormat tier: "SU", }, xatu: { - tier: "UU", + tier: "RU", }, mareep: { tier: "IU", diff --git a/data/mods/gen8linked/moves.ts b/data/mods/gen8linked/moves.ts index 8e2427e4aef2..db0878bba43a 100644 --- a/data/mods/gen8linked/moves.ts +++ b/data/mods/gen8linked/moves.ts @@ -384,11 +384,11 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { if (!lastMove) return false; const possibleTypes = []; const attackType = lastMove.type; - for (const type of this.dex.types.all()) { - if (source.hasType(type.name)) continue; - const typeCheck = type.damageTaken[attackType]; + for (const typeName of this.dex.types.names()) { + if (source.hasType(typeName)) continue; + const typeCheck = this.dex.types.get(typeName).damageTaken[attackType]; if (typeCheck === 2 || typeCheck === 3) { - possibleTypes.push(type.name); + possibleTypes.push(typeName); } } if (!possibleTypes.length) { diff --git a/data/mods/gen9ssb/scripts.ts b/data/mods/gen9ssb/scripts.ts index 428c94ab672c..3fe0e55a73b8 100644 --- a/data/mods/gen9ssb/scripts.ts +++ b/data/mods/gen9ssb/scripts.ts @@ -85,12 +85,9 @@ export function changeSet(context: Battle, pokemon: Pokemon, newSet: SSBSet, cha if (newSet.species === 'Shedinja') percent = 1; pokemon.formeChange(newSet.species, context.effect, true); if (!pokemon.terastallized && newSet.teraType) { - const allTypes = context.dex.types.all().map(x => x.name); - pokemon.teraType = newSet.teraType === 'Any' ? - allTypes[Math.floor(Math.random() * allTypes.length)] : - Array.isArray(newSet.teraType) ? - newSet.teraType[Math.floor(Math.random() * newSet.teraType.length)] : - newSet.teraType; + const allTypes = context.dex.types.names(); + pokemon.teraType = newSet.teraType === 'Any' ? context.sample(allTypes) : + Array.isArray(newSet.teraType) ? context.sample(newSet.teraType) : newSet.teraType; } const details = pokemon.species.name + (pokemon.level === 100 ? '' : ', L' + pokemon.level) + (pokemon.gender === '' ? '' : ', ' + pokemon.gender) + (pokemon.set.shiny ? ', shiny' : ''); diff --git a/data/moves.ts b/data/moves.ts index 1f4bd3143341..e7fa04db92b7 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -2908,11 +2908,11 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { } const possibleTypes = []; const attackType = target.lastMoveUsed.type; - for (const type of this.dex.types.all()) { - if (source.hasType(type.name)) continue; - const typeCheck = type.damageTaken[attackType]; + for (const typeName of this.dex.types.names()) { + if (source.hasType(typeName)) continue; + const typeCheck = this.dex.types.get(typeName).damageTaken[attackType]; if (typeCheck === 2 || typeCheck === 3) { - possibleTypes.push(type.name); + possibleTypes.push(typeName); } } if (!possibleTypes.length) { diff --git a/data/random-battles/gen3/teams.ts b/data/random-battles/gen3/teams.ts index 3570009cf673..511fd1377296 100644 --- a/data/random-battles/gen3/teams.ts +++ b/data/random-battles/gen3/teams.ts @@ -129,9 +129,7 @@ export class RandomGen3Teams extends RandomGen4Teams { // Develop additional move lists const badWithSetup = ['knockoff', 'rapidspin', 'toxic']; - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen4/teams.ts b/data/random-battles/gen4/teams.ts index 1b84d3bdeae6..1ce2723fe08e 100644 --- a/data/random-battles/gen4/teams.ts +++ b/data/random-battles/gen4/teams.ts @@ -76,6 +76,9 @@ export class RandomGen4Teams extends RandomGen5Teams { Steel: (movePool, moves, abilities, types, counter, species) => (!counter.get('Steel') && species.id === 'metagross'), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status') + .map(move => move.id); } cullMovePool( @@ -164,9 +167,7 @@ export class RandomGen4Teams extends RandomGen5Teams { // Develop additional move lists const badWithSetup = ['pursuit', 'toxic']; - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen5/teams.ts b/data/random-battles/gen5/teams.ts index 4cc13067b4a2..14636cc136c3 100644 --- a/data/random-battles/gen5/teams.ts +++ b/data/random-battles/gen5/teams.ts @@ -89,6 +89,10 @@ export class RandomGen5Teams extends RandomGen6Teams { ), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + // Nature Power is Earthquake this gen + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status' && move.id !== 'naturepower') + .map(move => move.id); } cullMovePool( @@ -177,10 +181,7 @@ export class RandomGen5Teams extends RandomGen6Teams { // Develop additional move lists const badWithSetup = ['healbell', 'pursuit', 'toxic']; - // Nature Power is Earthquake this gen - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status' && move.id !== 'naturepower') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen6/teams.ts b/data/random-battles/gen6/teams.ts index e54839e0ce74..b989b40c62fe 100644 --- a/data/random-battles/gen6/teams.ts +++ b/data/random-battles/gen6/teams.ts @@ -103,6 +103,9 @@ export class RandomGen6Teams extends RandomGen7Teams { Steel: (movePool, moves, abilities, types, counter, species) => (!counter.get('Steel') && species.baseStats.atk >= 100), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status') + .map(move => move.id); } cullMovePool( @@ -196,9 +199,7 @@ export class RandomGen6Teams extends RandomGen7Teams { // Develop additional move lists const badWithSetup = ['defog', 'dragontail', 'haze', 'healbell', 'nuzzle', 'pursuit', 'rapidspin', 'toxic']; - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen7/teams.ts b/data/random-battles/gen7/teams.ts index fbd939b601a8..3ed12defd32f 100644 --- a/data/random-battles/gen7/teams.ts +++ b/data/random-battles/gen7/teams.ts @@ -99,6 +99,7 @@ function sereneGraceBenefits(move: Move) { export class RandomGen7Teams extends RandomGen8Teams { randomSets: {[species: string]: RandomTeamsTypes.RandomSpeciesData} = require('./sets.json'); + protected cachedStatusMoves: ID[]; constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { super(format, prng); @@ -141,6 +142,10 @@ export class RandomGen7Teams extends RandomGen8Teams { Steel: (movePool, moves, abilities, types, counter, species) => (!counter.get('Steel') && species.baseStats.atk >= 100), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + // Nature Power is Tri Attack this gen + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status' && move.id !== 'naturepower') + .map(move => move.id); } newQueryMoves( @@ -311,10 +316,7 @@ export class RandomGen7Teams extends RandomGen8Teams { // Develop additional move lists const badWithSetup = ['defog', 'dragontail', 'haze', 'healbell', 'nuzzle', 'pursuit', 'rapidspin', 'toxic']; - // Nature Power is Tri Attack this gen - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status' && move.id !== 'naturepower') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen8/teams.ts b/data/random-battles/gen8/teams.ts index e8e68b1dc84e..210968ed3df2 100644 --- a/data/random-battles/gen8/teams.ts +++ b/data/random-battles/gen8/teams.ts @@ -95,7 +95,7 @@ function sereneGraceBenefits(move: Move) { } export class RandomGen8Teams { - dex: ModdedDex; + readonly dex: ModdedDex; gen: number; factoryTier: string; format: Format; @@ -116,6 +116,11 @@ export class RandomGen8Teams { */ moveEnforcementCheckers: {[k: string]: MoveEnforcementChecker}; + /** Used by .getPools() */ + private poolsCacheKey: [string | undefined, number | undefined, RuleTable | undefined, boolean] | undefined; + private cachedPool: number[] | undefined; + private cachedSpeciesPool: Species[] | undefined; + constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { format = Dex.formats.get(format); this.dex = Dex.forFormat(format); @@ -232,6 +237,9 @@ export class RandomGen8Teams { return abilities.includes('Huge Power') && movePool.includes('aquajet'); }, }; + this.poolsCacheKey = undefined; + this.cachedPool = undefined; + this.cachedSpeciesPool = undefined; } setSeed(prng?: PRNG | PRNGSeed) { @@ -518,7 +526,7 @@ export class RandomGen8Teams { }; if (this.gen === 9) { // Tera type - set.teraType = this.sample(this.dex.types.all()).name; + set.teraType = this.sample(this.dex.types.names()); } team.push(set); } @@ -526,19 +534,18 @@ export class RandomGen8Teams { return team; } - randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { - // Picks `n` random pokemon--no repeats, even among formes - // Also need to either normalize for formes or select formes at random - // Unreleased are okay but no CAP - if (requiredType && !this.dex.types.get(requiredType).exists) { - throw new Error(`"${requiredType}" is not a valid type.`); - } - + private getPools(requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Memoize pool and speciesPool because, at least during tests, they are constructed with the same parameters + // hundreds of times and are expensive to compute. const isNotCustom = !ruleTable; - - const pool: number[] = []; + let pool: number[] = []; let speciesPool: Species[] = []; - if (isNotCustom) { + const ck = this.poolsCacheKey; + if (ck && this.cachedPool && this.cachedSpeciesPool && + ck[0] === requiredType && ck[1] === minSourceGen && ck[2] === ruleTable && ck[3] === requireMoves) { + speciesPool = this.cachedSpeciesPool.slice(); + pool = this.cachedPool.slice(); + } else if (isNotCustom) { speciesPool = [...this.dex.species.all()]; for (const species of speciesPool) { if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue; @@ -552,6 +559,9 @@ export class RandomGen8Teams { if (num <= 0 || pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } else { const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent']; const nonexistentBanReason = ruleTable.check('nonexistent'); @@ -596,7 +606,23 @@ export class RandomGen8Teams { if (pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } + return {pool, speciesPool}; + } + + randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Picks `n` random pokemon--no repeats, even among formes + // Also need to either normalize for formes or select formes at random + // Unreleased are okay but no CAP + if (requiredType && !this.dex.types.get(requiredType).exists) { + throw new Error(`"${requiredType}" is not a valid type.`); + } + + const {pool, speciesPool} = this.getPools(requiredType, minSourceGen, ruleTable, requireMoves); + const isNotCustom = !ruleTable; const hasDexNumber: {[k: string]: number} = {}; for (let i = 0; i < n; i++) { @@ -867,7 +893,7 @@ export class RandomGen8Teams { }; if (this.gen === 9) { // Random Tera type - set.teraType = this.sample(this.dex.types.all()).name; + set.teraType = this.sample(this.dex.types.names()); } team.push(set); } diff --git a/data/random-battles/gen9/teams.ts b/data/random-battles/gen9/teams.ts index cbab9c31e234..0f858d935e9b 100644 --- a/data/random-battles/gen9/teams.ts +++ b/data/random-battles/gen9/teams.ts @@ -145,7 +145,7 @@ function sereneGraceBenefits(move: Move) { } export class RandomTeams { - dex: ModdedDex; + readonly dex: ModdedDex; gen: number; factoryTier: string; format: Format; @@ -164,6 +164,12 @@ export class RandomTeams { */ moveEnforcementCheckers: {[k: string]: MoveEnforcementChecker}; + /** Used by .getPools() */ + private poolsCacheKey: [string | undefined, number | undefined, RuleTable | undefined, boolean] | undefined; + private cachedPool: number[] | undefined; + private cachedSpeciesPool: Species[] | undefined; + protected cachedStatusMoves: ID[]; + constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { format = Dex.formats.get(format); this.dex = Dex.forFormat(format); @@ -233,6 +239,10 @@ export class RandomTeams { ), Water: (movePool, moves, abilities, types, counter) => (!counter.get('Water') && !types.includes('Ground')), }; + this.poolsCacheKey = undefined; + this.cachedPool = undefined; + this.cachedSpeciesPool = undefined; + this.cachedStatusMoves = this.dex.moves.all().filter(move => move.category === 'Status').map(move => move.id); } setSeed(prng?: PRNG | PRNGSeed) { @@ -476,9 +486,7 @@ export class RandomTeams { } // Develop additional move lists - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // Team-based move culls if (teamDetails.screens) { @@ -1963,7 +1971,7 @@ export class RandomTeams { if (this.forceTeraType) { set.teraType = this.forceTeraType; } else { - set.teraType = this.sample(this.dex.types.all()).name; + set.teraType = this.sample(this.dex.types.names()); } } team.push(set); @@ -1972,19 +1980,18 @@ export class RandomTeams { return team; } - randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { - // Picks `n` random pokemon--no repeats, even among formes - // Also need to either normalize for formes or select formes at random - // Unreleased are okay but no CAP - if (requiredType && !this.dex.types.get(requiredType).exists) { - throw new Error(`"${requiredType}" is not a valid type.`); - } - + private getPools(requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Memoize pool and speciesPool because, at least during tests, they are constructed with the same parameters + // hundreds of times and are expensive to compute. const isNotCustom = !ruleTable; - - const pool: number[] = []; + let pool: number[] = []; let speciesPool: Species[] = []; - if (isNotCustom) { + const ck = this.poolsCacheKey; + if (ck && this.cachedPool && this.cachedSpeciesPool && + ck[0] === requiredType && ck[1] === minSourceGen && ck[2] === ruleTable && ck[3] === requireMoves) { + speciesPool = this.cachedSpeciesPool.slice(); + pool = this.cachedPool.slice(); + } else if (isNotCustom) { speciesPool = [...this.dex.species.all()]; for (const species of speciesPool) { if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue; @@ -1998,6 +2005,9 @@ export class RandomTeams { if (num <= 0 || pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } else { const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent']; const nonexistentBanReason = ruleTable.check('nonexistent'); @@ -2042,7 +2052,23 @@ export class RandomTeams { if (pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } + return {pool, speciesPool}; + } + + randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Picks `n` random pokemon--no repeats, even among formes + // Also need to either normalize for formes or select formes at random + // Unreleased are okay but no CAP + if (requiredType && !this.dex.types.get(requiredType).exists) { + throw new Error(`"${requiredType}" is not a valid type.`); + } + + const {pool, speciesPool} = this.getPools(requiredType, minSourceGen, ruleTable, requireMoves); + const isNotCustom = !ruleTable; const hasDexNumber: {[k: string]: number} = {}; for (let i = 0; i < n; i++) { @@ -2319,7 +2345,7 @@ export class RandomTeams { if (this.forceTeraType) { set.teraType = this.forceTeraType; } else { - set.teraType = this.sample(this.dex.types.all()).name; + set.teraType = this.sample(this.dex.types.names()); } } team.push(set); diff --git a/data/random-battles/gen9baby/teams.ts b/data/random-battles/gen9baby/teams.ts index 18836784c297..da9977951a17 100644 --- a/data/random-battles/gen9baby/teams.ts +++ b/data/random-battles/gen9baby/teams.ts @@ -102,9 +102,7 @@ export class RandomBabyTeams extends RandomTeams { } // Create list of all status moves to be used later - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // Team-based move culls if (teamDetails.screens && movePool.length >= this.maxMoveCount + 2) { diff --git a/data/text/items.ts b/data/text/items.ts index bc1d3b8d64a7..0fcbda76f457 100644 --- a/data/text/items.ts +++ b/data/text/items.ts @@ -2270,8 +2270,11 @@ export const ItemsText: {[id: IDEntry]: ItemText} = { }, utilityumbrella: { name: "Utility Umbrella", - desc: "The holder ignores rain- and sun-based effects. Damage and accuracy calculations from attacks used by the holder are affected by rain and sun, but not attacks used against the holder.", + desc: "The holder ignores rain- and sun-based effects, including those of its Ability unless it is Orichalcum Pulse or Protosynthesis. Damage and accuracy calculations from attacks used by the holder are affected by rain and sun, but not attacks used against the holder.", shortDesc: "The holder ignores rain- and sun-based effects.", + gen8: { + desc: "The holder ignores rain- and sun-based effects, including those of its Ability. Damage and accuracy calculations from attacks used by the holder are affected by rain and sun, but not attacks used against the holder.", + }, }, venusaurite: { name: "Venusaurite", diff --git a/databases/schemas/teams.sql b/databases/schemas/teams.sql index 128599dabedb..f16c05408210 100644 --- a/databases/schemas/teams.sql +++ b/databases/schemas/teams.sql @@ -10,5 +10,5 @@ CREATE TABLE teams ( ); CREATE INDEX owner_idx ON teams(ownerid); -CREATE INDEX format_idx ON teams(ownerid); +CREATE INDEX format_idx ON teams(format); CREATE INDEX owner_fmt_idx ON teams(ownerid, format); diff --git a/package-lock.json b/package-lock.json index 059c0f423b8d..25a94b4dbe76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "typescript": "^5.0.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "optionalDependencies": { "better-sqlite3": "^7.6.2", diff --git a/package.json b/package.json index 7c30ade6fbad..97613739752e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "node-oom-heapdump": "^1.2.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "scripts": { "start": "node pokemon-showdown start", diff --git a/server/README.md b/server/README.md index b9d670b02c07..ed1635869466 100644 --- a/server/README.md +++ b/server/README.md @@ -11,7 +11,7 @@ Installing ./pokemon-showdown -(Requires Node.js v14+) +(Requires Node.js v16+) If your distro package manager has an old Node.js version, the simplest way to upgrade is `n` – usually no root necessary: diff --git a/server/chat-commands/admin.ts b/server/chat-commands/admin.ts index 20afaaa63a5a..3f11448f1466 100644 --- a/server/chat-commands/admin.ts +++ b/server/chat-commands/admin.ts @@ -368,7 +368,8 @@ export const commands: Chat.ChatCommands = { const closeHtmlPage = cmd === 'closehtmlpage'; - const {targetUser, rest} = this.requireUser(target); + const [targetStr, rest] = this.splitOne(target).map(str => str.trim()); + const targets = targetStr.split('|').map(u => u.trim()); let [pageid, content] = this.splitOne(rest); let selector: string | undefined; if (cmd === 'changehtmlpageselector') { @@ -381,47 +382,70 @@ export const commands: Chat.ChatCommands = { pageid = `${user.id}-${toID(pageid)}`; - if (targetUser.locked && !this.user.can('lock')) { - this.errorReply("This user is currently locked, so you cannot send them HTML."); - return false; - } + const successes: string[] = [], errors: string[] = []; - let targetConnections = []; - // find if a connection has specifically requested this page - for (const c of targetUser.connections) { - if (c.lastRequestedPage === pageid) { - targetConnections.push(c); + content = this.checkHTML(content); + + targets.forEach(targetUsername => { + const targetUser = Users.get(targetUsername); + if (!targetUser) return errors.push(`${targetUsername} [offline/misspelled]`); + + if (targetUser.locked && !this.user.can('lock')) { + return errors.push(`${targetUser.name} [locked]`); } - } - if (!targetConnections.length) { - // no connection has requested it - verify that we share a room - this.checkPMHTML(targetUser); - targetConnections = targetUser.connections; - } - content = this.checkHTML(content); + let targetConnections = []; + // find if a connection has specifically requested this page + for (const c of targetUser.connections) { + if (c.lastRequestedPage === pageid) { + targetConnections.push(c); + } + } + if (!targetConnections.length) { + // no connection has requested it - verify that we share a room + try { + this.checkPMHTML(targetUser); + } catch { + return errors.push(`${targetUser.name} [not in room / blocking PMs]`); + } + targetConnections = targetUser.connections; + } - for (const targetConnection of targetConnections) { - const context = new Chat.PageContext({ - user: targetUser, - connection: targetConnection, - pageid: `view-bot-${pageid}`, - }); - if (closeHtmlPage) { - context.send(`|deinit|`); - } else if (selector) { - context.send(`|selectorhtml|${selector}|${content}`); - } else { - context.title = `[${user.name}] ${pageid}`; - context.setHTML(content); + for (const targetConnection of targetConnections) { + const context = new Chat.PageContext({ + user: targetUser, + connection: targetConnection, + pageid: `view-bot-${pageid}`, + }); + if (closeHtmlPage) { + context.send(`|deinit|`); + } else if (selector) { + context.send(`|selectorhtml|${selector}|${content}`); + } else { + context.title = `[${user.name}] ${pageid}`; + context.setHTML(content); + } } - } + successes.push(targetUser.name); + }); if (closeHtmlPage) { - this.sendReply(`Closed the bot page ${pageid} for ${targetUser.name}.`); + if (successes.length) { + this.sendReply(`Closed the bot page ${pageid} for ${Chat.toListString(successes)}.`); + } + if (errors.length) { + this.errorReply(`Unable to close the bot page for ${Chat.toListString(errors)}.`); + } } else { - this.sendReply(`Sent ${targetUser.name}${selector ? ` the selector ${selector} on` : ''} the bot page ${pageid}.`); + if (successes.length) { + this.sendReply(`Sent ${Chat.toListString(successes)}${selector ? ` the selector ${selector} on` : ''} the bot page ${pageid}.`); + } + if (errors.length) { + this.errorReply(`Unable to send the bot page ${pageid} to ${Chat.toListString(errors)}.`); + } } + + if (!successes.length) return false; }, sendhtmlpagehelp: [ `/sendhtmlpage [userid], [pageid], [html] - Sends [userid] the bot page [pageid] with the content [html]. Requires: * # ~`, @@ -473,15 +497,8 @@ export const commands: Chat.ChatCommands = { room = this.requireRoom(); this.checkCan('addhtml', null, room); - const {targetUser, rest} = this.requireUser(target); - - if (targetUser.locked && !this.user.can('lock')) { - throw new Chat.ErrorMessage("This user is currently locked, so you cannot send them private HTML."); - } - - if (!(targetUser.id in room.users)) { - throw new Chat.ErrorMessage("You cannot send private HTML to users who are not in this room."); - } + const [targetStr, rest] = this.splitOne(target).map(str => str.trim()); + const targets = targetStr.split('|').map(u => u.trim()); let html: string; let messageType: string; @@ -499,13 +516,33 @@ export const commands: Chat.ChatCommands = { html = this.checkHTML(html); if (!html) return this.parse('/help sendprivatehtmlbox'); - html = `${Utils.html`
/suspects remove [tier]
: deletes a suspect test. Requires: ~/suspects whitelist [username]
: allows [username] to add suspect tests. Requires: ~/suspects unwhitelist [username]
: disallows [username] from adding suspect tests. Requires: ~/suspects setcoil OR /suspects sc [formatid], [B value]
: Activate COIL ranking for the given [formatid] with the given [B value].` +
+ `/suspects setbvalue OR /suspects sbv [formatid], [B value]
: Activate COIL ranking for the given [formatid] with the given [B value].` +
`Requires: suspect whitelist ~`
);
},
diff --git a/server/private-messages/index.ts b/server/private-messages/index.ts
index 330f91a424f1..1c48b49a3492 100644
--- a/server/private-messages/index.ts
+++ b/server/private-messages/index.ts
@@ -140,8 +140,8 @@ export const PrivateMessages = new class {
if (!PM.isParentProcess) return null!;
const time = Date.now();
// even though we expire once a week atm, we check once a day
- const nextMidnight = new Date(time + 24 * 60 * 60 * 1000);
- nextMidnight.setHours(0, 0, 1);
+ const nextMidnight = new Date();
+ nextMidnight.setHours(24, 0, 0, 0);
if (this.clearInterval) clearTimeout(this.clearInterval);
this.clearInterval = setTimeout(() => {
void this.clearOffline();
diff --git a/server/room-battle.ts b/server/room-battle.ts
index c1cc67c70ab1..23b390a07544 100644
--- a/server/room-battle.ts
+++ b/server/room-battle.ts
@@ -1116,7 +1116,7 @@ export class RoomBattle extends RoomGame