From 1cdb8591edb7e0c74c136e75dd4ba845e1930fa1 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 14:56:54 -0700 Subject: [PATCH 001/125] Update pokemon.js --- sim/pokemon.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sim/pokemon.js b/sim/pokemon.js index d8d8b1eaf0d1..2500ded0cd73 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -156,7 +156,7 @@ class Pokemon { this.lastDamage = 0; /**@type {?{pokemon: Pokemon, damage: number, thisTurn: boolean, move?: string}} */ - this.lastAttackedBy = null; + this.hurtBy = []; this.usedItemThisTurn = false; this.newlySwitched = false; this.beingCalledBack = false; @@ -602,12 +602,12 @@ class Pokemon { gotAttacked(move, damage, source) { if (!damage) damage = 0; move = this.battle.getMove(move); - this.lastAttackedBy = { - pokemon: source, + let lastHurtBy = { + source: source, damage: damage, move: move.id, - thisTurn: true, }; + this.hurtBy.push(lastHurtBy); } /** @@ -1022,7 +1022,7 @@ class Pokemon { this.moveThisTurn = ''; this.lastDamage = 0; - this.lastAttackedBy = null; + this.hurtBy = []; this.newlySwitched = true; this.beingCalledBack = false; From aa6a4635f4c94e3819f5a799aaa7b28d1c132138 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 18:52:56 -0700 Subject: [PATCH 002/125] Refactor lastAttackedBy to hurtBy (#1) Test merge --- data/moves.js | 14 ++++++++++---- mods/gen1/moves.js | 8 ++++---- mods/gen2/moves.js | 14 +++++++++----- mods/gen3/moves.js | 12 ++++++++---- mods/gen4/moves.js | 8 +++++--- mods/gennext/moves.js | 9 ++++++--- mods/stadium/moves.js | 8 ++++---- sim/battle.js | 13 ++++++++----- 8 files changed, 54 insertions(+), 32 deletions(-) diff --git a/data/moves.js b/data/moves.js index a24956e00afa..a3f5099c704f 100644 --- a/data/moves.js +++ b/data/moves.js @@ -948,8 +948,11 @@ let BattleMovedex = { accuracy: 100, basePower: 60, basePowerCallback: function (pokemon, target, move) { - if (target.lastDamage > 0 && pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.pokemon === target) { - this.debug('Boosted for getting hit by ' + pokemon.lastAttackedBy.move); + let hurtByTarget = pokemon.hurtBy.find(function (x) { + return x.source === target && x.damage > 0 && x.thisTurn; + }); + if (hurtByTarget) { + this.debug('Boosted for getting hit by ' + hurtByTarget.move); return move.basePower * 2; } return move.basePower; @@ -13388,8 +13391,11 @@ let BattleMovedex = { accuracy: 100, basePower: 60, basePowerCallback: function (pokemon, target, move) { - if (target.lastDamage > 0 && pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.pokemon === target) { - this.debug('Boosted for getting hit by ' + pokemon.lastAttackedBy.move); + let hurtByTarget = pokemon.hurtBy.find(function (x) { + return x.source === target && x.damage > 0 && x.thisTurn; + }); + if (hurtByTarget) { + this.debug('Boosted for getting hit by ' + hurtByTarget.move); return move.basePower * 2; } return move.basePower; diff --git a/mods/gen1/moves.js b/mods/gen1/moves.js index 030222389c78..324c89de5e80 100644 --- a/mods/gen1/moves.js +++ b/mods/gen1/moves.js @@ -967,11 +967,11 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (!target.lastAttackedBy) { - target.lastAttackedBy = {pokemon: source, move: move.id, thisTurn: true, damage: damage}; + if (target.hurtBy.length == 0) { + target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { - target.lastAttackedBy.move = move.id; - target.lastAttackedBy.damage = damage; + target.hurtBy[target.hurtBy.length - 1].move = move.id; + target.hurtBy[target.hurtBy.length - 1].damage = damage; } return 0; }, diff --git a/mods/gen2/moves.js b/mods/gen2/moves.js index 287529c3b533..ce322a05c114 100644 --- a/mods/gen2/moves.js +++ b/mods/gen2/moves.js @@ -144,9 +144,11 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a physical attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a physical attack this turn, or if the user did not lose HP from the attack. If the opposing Pokemon used Fissure or Horn Drill and missed, this move deals 65535 damage.", damageCallback: function (pokemon, target) { - if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.move && (this.getCategory(pokemon.lastAttackedBy.move) === 'Physical' || this.getMove(pokemon.lastAttackedBy.move).id === 'hiddenpower') && + if (pokemon.hurtBy.length == 0) return false; + let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; + if (lastHurtBy.move && && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower') && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { - return 2 * pokemon.lastAttackedBy.damage; + return 2 * lastHurtBy.damage; } return false; }, @@ -493,9 +495,11 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a special attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a special attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon, target) { - if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.move && this.getCategory(pokemon.lastAttackedBy.move) === 'Special' && - this.getMove(pokemon.lastAttackedBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { - return 2 * pokemon.lastAttackedBy.damage; + if (pokemon.hurtBy.length == 0) return false; + let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; + if (lastHurtBy.move && lastHurtBy.thisTurn && this.getCategory(lastHurtBy.move) === 'Special' && + this.getMove(lastHurtBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { + return 2 * lastHurtBy.damage; } return false; }, diff --git a/mods/gen3/moves.js b/mods/gen3/moves.js index 87b1144a1f76..463404754b02 100644 --- a/mods/gen3/moves.js +++ b/mods/gen3/moves.js @@ -187,9 +187,11 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the last opposing Pokemon to hit the user with a physical attack this turn equal to twice the HP lost by the user from that attack. If that opposing Pokemon's position is no longer in use and there is another opposing Pokemon on the field, the damage is done to it instead. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user was not hit by an opposing Pokemon's physical attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon) { - if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && pokemon.lastAttackedBy.move && (this.getCategory(pokemon.lastAttackedBy.move) === 'Physical' || this.getMove(pokemon.lastAttackedBy.move).id === 'hiddenpower')) { + if (pokemon.hurtBy.length == 0) return false; + let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; + if (lastHurtBy.move && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower')) { // @ts-ignore - return 2 * pokemon.lastAttackedBy.damage; + return 2 * lastHurtBy.damage; } return false; }, @@ -557,10 +559,12 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['assist', 'curse', 'doomdesire', 'focuspunch', 'futuresight', 'magiccoat', 'metronome', 'mimic', 'mirrormove', 'naturepower', 'psychup', 'roleplay', 'sketch', 'sleeptalk', 'spikes', 'spitup', 'taunt', 'teeterdance', 'transform']; - if (!pokemon.lastAttackedBy || !pokemon.lastAttackedBy.pokemon.lastMove || !pokemon.lastAttackedBy.move || noMirror.includes(pokemon.lastAttackedBy.move) || !pokemon.lastAttackedBy.pokemon.hasMove(pokemon.lastAttackedBy.move)) { + if (pokemon.hurtBy.length == 0) return false; + let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; + if (!lastHurtBy.pokemon.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.pokemon.hasMove(lastHurtBy.move)) { return false; } - this.useMove(pokemon.lastAttackedBy.move, pokemon); + this.useMove(lastHurtBy.move, pokemon); }, target: "self", }, diff --git a/mods/gen4/moves.js b/mods/gen4/moves.js index 7091d45e0e62..87b22957fca3 100644 --- a/mods/gen4/moves.js +++ b/mods/gen4/moves.js @@ -1104,10 +1104,12 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['acupressure', 'aromatherapy', 'assist', 'chatter', 'copycat', 'counter', 'curse', 'doomdesire', 'feint', 'focuspunch', 'futuresight', 'gravity', 'hail', 'haze', 'healbell', 'helpinghand', 'lightscreen', 'luckychant', 'magiccoat', 'mefirst', 'metronome', 'mimic', 'mirrorcoat', 'mirrormove', 'mist', 'mudsport', 'naturepower', 'perishsong', 'psychup', 'raindance', 'reflect', 'roleplay', 'safeguard', 'sandstorm', 'sketch', 'sleeptalk', 'snatch', 'spikes', 'spitup', 'stealthrock', 'struggle', 'sunnyday', 'tailwind', 'toxicspikes', 'transform', 'watersport']; - if (!pokemon.lastAttackedBy || !pokemon.lastAttackedBy.pokemon.lastMove || !pokemon.lastAttackedBy.move || noMirror.includes(pokemon.lastAttackedBy.move) || !pokemon.lastAttackedBy.pokemon.hasMove(pokemon.lastAttackedBy.move)) { - return false; + if (pokemon.hurtBy.length == 0) return false; + let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; + if (!lastHurtBy.source.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.source.hasMove(lastHurtBy.move)) { + return false; } - this.useMove(pokemon.lastAttackedBy.move, pokemon); + this.useMove(lastHurtBy.move, pokemon); }, target: "self", }, diff --git a/mods/gennext/moves.js b/mods/gennext/moves.js index 365eecc0aaa0..f2b91dd7214c 100644 --- a/mods/gennext/moves.js +++ b/mods/gennext/moves.js @@ -973,9 +973,12 @@ let BattleMovedex = { avalanche: { inherit: true, basePowerCallback: function (pokemon, source) { - if ((source.lastDamage > 0 && pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn)) { - this.debug('Boosted for getting hit by ' + pokemon.lastAttackedBy.move); - return this.isWeather('hail') ? 180 : 120; + if (pokemon.hurtBy.length > 0) { + let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; + if (lastHurtBy.damage > 0 && lastHurtBy.thisTurn) { + this.debug('Boosted for getting hit by ' + lastHurtBy.move); + return this.isWeather('hail') ? 180 : 120; + } } return this.isWeather('hail') ? 90 : 60; }, diff --git a/mods/stadium/moves.js b/mods/stadium/moves.js index cf3d148ef42e..7d94217e1849 100644 --- a/mods/stadium/moves.js +++ b/mods/stadium/moves.js @@ -159,11 +159,11 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (!target.lastAttackedBy) { - target.lastAttackedBy = {pokemon: source, move: move.id, thisTurn: true, damage: damage}; + if (target.hurtBy.length == 0) { + target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { - target.lastAttackedBy.move = move.id; - target.lastAttackedBy.damage = damage; + target.hurtBy[target.hurtBy.length - 1].move = move.id; + target.hurtBy[target.hurtBy.length - 1].damage = damage; } return 0; }, diff --git a/sim/battle.js b/sim/battle.js index 5ae171961d3b..2b029282ad32 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -1542,13 +1542,16 @@ class Battle extends Dex.ModdedDex { this.runEvent('DisableMove', pokemon); if (!pokemon.ateBerry) pokemon.disableMove('belch'); - if (pokemon.lastAttackedBy) { - if (pokemon.lastAttackedBy.pokemon.isActive) { - pokemon.lastAttackedBy.thisTurn = false; + // If it was an illusion, it's not any more + if (pokemon.hurtBy.length > 0 && this.gen >= 7) pokemon.knownType = true; + + for (let i = pokemon.hurtBy.length - 1; i >= 0; i--) { + let attack = pokemon.hurtBy[i]; + if (attack.source.isActive) { + attack.thisTurn = false; } else { - pokemon.lastAttackedBy = null; + pokemon.hurtBy.slice(pokemon.hurtBy.indexof(attack), 1); } - if (this.gen >= 7) pokemon.knownType = true; // If it was an illusion, it's not any more } if (this.gen >= 7) { From f2eb42d2475470d7523491d8652b81a309a45bb4 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 18:56:54 -0700 Subject: [PATCH 003/125] Update pokemon.js --- sim/pokemon.js | 1 + 1 file changed, 1 insertion(+) diff --git a/sim/pokemon.js b/sim/pokemon.js index 2500ded0cd73..e8f357851be2 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -606,6 +606,7 @@ class Pokemon { source: source, damage: damage, move: move.id, + thisTurn: true, }; this.hurtBy.push(lastHurtBy); } From 1f457ab43075ba6b404d0c6d000766b059778b99 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 19:04:25 -0700 Subject: [PATCH 004/125] Fix typo in Battle.nextTurn --- sim/battle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/battle.js b/sim/battle.js index 2b029282ad32..1f1f2135c64b 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -1550,7 +1550,7 @@ class Battle extends Dex.ModdedDex { if (attack.source.isActive) { attack.thisTurn = false; } else { - pokemon.hurtBy.slice(pokemon.hurtBy.indexof(attack), 1); + pokemon.hurtBy.slice(pokemon.hurtBy.indexOf(attack), 1); } } From c06227799cad9dc698b86dedac14fbb8e590cdc6 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 19:27:16 -0700 Subject: [PATCH 005/125] Corrections Edits to comply with eslint check --- mods/gen1/moves.js | 2 +- mods/gen2/moves.js | 4 ++-- mods/gen3/moves.js | 4 ++-- mods/gen4/moves.js | 2 +- mods/stadium/moves.js | 2 +- sim/battle.js | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mods/gen1/moves.js b/mods/gen1/moves.js index 324c89de5e80..94bae953468d 100644 --- a/mods/gen1/moves.js +++ b/mods/gen1/moves.js @@ -967,7 +967,7 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (target.hurtBy.length == 0) { + if (target.hurtBy.length === 0) { target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { target.hurtBy[target.hurtBy.length - 1].move = move.id; diff --git a/mods/gen2/moves.js b/mods/gen2/moves.js index ce322a05c114..e77771c76cfd 100644 --- a/mods/gen2/moves.js +++ b/mods/gen2/moves.js @@ -144,9 +144,9 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a physical attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a physical attack this turn, or if the user did not lose HP from the attack. If the opposing Pokemon used Fissure or Horn Drill and missed, this move deals 65535 damage.", damageCallback: function (pokemon, target) { - if (pokemon.hurtBy.length == 0) return false; + if (pokemon.hurtBy.length === 0) return false; let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (lastHurtBy.move && && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower') && + if (lastHurtBy.move && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower') && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { return 2 * lastHurtBy.damage; } diff --git a/mods/gen3/moves.js b/mods/gen3/moves.js index 463404754b02..2693c36f1712 100644 --- a/mods/gen3/moves.js +++ b/mods/gen3/moves.js @@ -187,7 +187,7 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the last opposing Pokemon to hit the user with a physical attack this turn equal to twice the HP lost by the user from that attack. If that opposing Pokemon's position is no longer in use and there is another opposing Pokemon on the field, the damage is done to it instead. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user was not hit by an opposing Pokemon's physical attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon) { - if (pokemon.hurtBy.length == 0) return false; + if (pokemon.hurtBy.length === 0) return false; let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; if (lastHurtBy.move && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower')) { // @ts-ignore @@ -559,7 +559,7 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['assist', 'curse', 'doomdesire', 'focuspunch', 'futuresight', 'magiccoat', 'metronome', 'mimic', 'mirrormove', 'naturepower', 'psychup', 'roleplay', 'sketch', 'sleeptalk', 'spikes', 'spitup', 'taunt', 'teeterdance', 'transform']; - if (pokemon.hurtBy.length == 0) return false; + if (pokemon.hurtBy.length === 0) return false; let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; if (!lastHurtBy.pokemon.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.pokemon.hasMove(lastHurtBy.move)) { return false; diff --git a/mods/gen4/moves.js b/mods/gen4/moves.js index 87b22957fca3..d4badd4400c5 100644 --- a/mods/gen4/moves.js +++ b/mods/gen4/moves.js @@ -1104,7 +1104,7 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['acupressure', 'aromatherapy', 'assist', 'chatter', 'copycat', 'counter', 'curse', 'doomdesire', 'feint', 'focuspunch', 'futuresight', 'gravity', 'hail', 'haze', 'healbell', 'helpinghand', 'lightscreen', 'luckychant', 'magiccoat', 'mefirst', 'metronome', 'mimic', 'mirrorcoat', 'mirrormove', 'mist', 'mudsport', 'naturepower', 'perishsong', 'psychup', 'raindance', 'reflect', 'roleplay', 'safeguard', 'sandstorm', 'sketch', 'sleeptalk', 'snatch', 'spikes', 'spitup', 'stealthrock', 'struggle', 'sunnyday', 'tailwind', 'toxicspikes', 'transform', 'watersport']; - if (pokemon.hurtBy.length == 0) return false; + if (pokemon.hurtBy.length === 0) return false; let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; if (!lastHurtBy.source.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.source.hasMove(lastHurtBy.move)) { return false; diff --git a/mods/stadium/moves.js b/mods/stadium/moves.js index 7d94217e1849..181edfb95171 100644 --- a/mods/stadium/moves.js +++ b/mods/stadium/moves.js @@ -159,7 +159,7 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (target.hurtBy.length == 0) { + if (target.hurtBy.length === 0) { target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { target.hurtBy[target.hurtBy.length - 1].move = move.id; diff --git a/sim/battle.js b/sim/battle.js index 1f1f2135c64b..4dcbc14a53c6 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -1544,7 +1544,7 @@ class Battle extends Dex.ModdedDex { // If it was an illusion, it's not any more if (pokemon.hurtBy.length > 0 && this.gen >= 7) pokemon.knownType = true; - + for (let i = pokemon.hurtBy.length - 1; i >= 0; i--) { let attack = pokemon.hurtBy[i]; if (attack.source.isActive) { From 33949682a6dea0c40bc736d97fe10776fd327d08 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 19:32:45 -0700 Subject: [PATCH 006/125] Change == to === --- mods/gen2/moves.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/gen2/moves.js b/mods/gen2/moves.js index e77771c76cfd..d821d4d5ce47 100644 --- a/mods/gen2/moves.js +++ b/mods/gen2/moves.js @@ -495,7 +495,7 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a special attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a special attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon, target) { - if (pokemon.hurtBy.length == 0) return false; + if (pokemon.hurtBy.length === 0) return false; let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; if (lastHurtBy.move && lastHurtBy.thisTurn && this.getCategory(lastHurtBy.move) === 'Special' && this.getMove(lastHurtBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { From 3d0c98ed063f7bf6c6c6be7686ac60105274022d Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 20:21:15 -0700 Subject: [PATCH 007/125] Fix wrong attribute name --- mods/gen3/moves.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/gen3/moves.js b/mods/gen3/moves.js index 2693c36f1712..a22cdaab1aa8 100644 --- a/mods/gen3/moves.js +++ b/mods/gen3/moves.js @@ -561,7 +561,7 @@ let BattleMovedex = { let noMirror = ['assist', 'curse', 'doomdesire', 'focuspunch', 'futuresight', 'magiccoat', 'metronome', 'mimic', 'mirrormove', 'naturepower', 'psychup', 'roleplay', 'sketch', 'sleeptalk', 'spikes', 'spitup', 'taunt', 'teeterdance', 'transform']; if (pokemon.hurtBy.length === 0) return false; let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (!lastHurtBy.pokemon.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.pokemon.hasMove(lastHurtBy.move)) { + if (!lastHurtBy.source.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.source.hasMove(lastHurtBy.move)) { return false; } this.useMove(lastHurtBy.move, pokemon); From eefea85c27c77c05c080eed15e7142d0d89ff77a Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 27 Aug 2018 20:22:18 -0700 Subject: [PATCH 008/125] Update @type for hurtBy --- sim/pokemon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/pokemon.js b/sim/pokemon.js index e8f357851be2..4ece31fc0b91 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -155,7 +155,7 @@ class Pokemon { this.moveThisTurnResult = undefined; this.lastDamage = 0; - /**@type {?{pokemon: Pokemon, damage: number, thisTurn: boolean, move?: string}} */ + /**@type {{source: Pokemon, damage: number, thisTurn: boolean, move?: string}[]} */ this.hurtBy = []; this.usedItemThisTurn = false; this.newlySwitched = false; From 5cd0e0750dfbf3951134b97f39e10f8bbf1662cc Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Tue, 28 Aug 2018 04:57:56 -0700 Subject: [PATCH 009/125] Random Battle: Fix Flyinium Z appearing with other Z Crystals (#4834) --- data/random-teams.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/random-teams.js b/data/random-teams.js index 8dbface89f10..9de79b7d8b8e 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -1400,7 +1400,7 @@ class RandomTeams extends Dex.ModdedDex { item = 'Groundium Z'; } else if (hasMove['mindblown'] && !!counter['Status'] && !teamDetails.zMove) { item = 'Firium Z'; - } else if (!teamDetails.zMove && hasMove['fly'] || ((hasMove['bounce'] || (hasAbility['Gale Wings'] && hasMove['bravebird'])) && counter.setupType)) { + } else if (!teamDetails.zMove && (hasMove['fly'] || ((hasMove['bounce'] || (hasAbility['Gale Wings'] && hasMove['bravebird'])) && counter.setupType))) { item = 'Flyinium Z'; } else if (hasMove['solarbeam'] && !hasAbility['Drought'] && !hasMove['sunnyday'] && !teamDetails['sun']) { item = !teamDetails.zMove ? 'Grassium Z' : 'Power Herb'; From 87a154214796cb1bc43cdb6990996113b94bc705 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Tue, 28 Aug 2018 14:24:57 -0400 Subject: [PATCH 010/125] Update Phantom/Shadow Force interaction with Minimize Fixes #4830 --- data/moves.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/moves.js b/data/moves.js index a24956e00afa..614943fa7dfb 100644 --- a/data/moves.js +++ b/data/moves.js @@ -10394,7 +10394,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "Raises the user's evasiveness by 2 stages. Whether or not the user's evasiveness was changed, Body Slam, Dragon Rush, Flying Press, Heat Crash, Heavy Slam, Malicious Moonsault, Phantom Force, Shadow Force, Steamroller, and Stomp will not check accuracy and have their damage doubled if used against the user while it is active.", + desc: "Raises the user's evasiveness by 2 stages. Whether or not the user's evasiveness was changed, Body Slam, Dragon Rush, Flying Press, Heat Crash, Heavy Slam, Malicious Moonsault, Steamroller, and Stomp will not check accuracy and have their damage doubled if used against the user while it is active.", shortDesc: "Raises the user's evasiveness by 2.", id: "minimize", name: "Minimize", @@ -10405,12 +10405,12 @@ let BattleMovedex = { effect: { noCopy: true, onSourceModifyDamage: function (damage, source, target, move) { - if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'phantomforce', 'heatcrash', 'shadowforce', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { + if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'heatcrash', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { return this.chainModify(2); } }, onAccuracy: function (accuracy, target, source, move) { - if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'phantomforce', 'heatcrash', 'shadowforce', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { + if (['stomp', 'steamroller', 'bodyslam', 'flyingpress', 'dragonrush', 'heatcrash', 'heavyslam', 'maliciousmoonsault'].includes(move.id)) { return true; } return accuracy; @@ -11655,7 +11655,7 @@ let BattleMovedex = { accuracy: 100, basePower: 90, category: "Physical", - desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn. Damage doubles and no accuracy check is done if the target has used Minimize while active.", + desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn.", shortDesc: "Disappears turn 1. Hits turn 2. Breaks protection.", id: "phantomforce", name: "Phantom Force", @@ -14384,7 +14384,7 @@ let BattleMovedex = { accuracy: 100, basePower: 120, category: "Physical", - desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn. Damage doubles and no accuracy check is done if the target has used Minimize while active.", + desc: "If this move is successful, it breaks through the target's Baneful Bunker, Detect, King's Shield, Protect, or Spiky Shield for this turn, allowing other Pokemon to attack the target normally. If the target's side is protected by Crafty Shield, Mat Block, Quick Guard, or Wide Guard, that protection is also broken for this turn and other Pokemon may attack the target's side normally. This attack charges on the first turn and executes on the second. On the first turn, the user avoids all attacks. If the user is holding a Power Herb, the move completes in one turn.", shortDesc: "Disappears turn 1. Hits turn 2. Breaks protection.", id: "shadowforce", isViable: true, From 952e5f9fd9ccd83fb33fd47e9e6301b7f6c919f3 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Wed, 29 Aug 2018 17:12:42 +0400 Subject: [PATCH 011/125] AAA: Ban Zygarde --- config/formats.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/config/formats.js b/config/formats.js index cd8142843d53..eb20d9d797e9 100644 --- a/config/formats.js +++ b/config/formats.js @@ -660,7 +660,7 @@ let Formats = [ mod: 'gen7', ruleset: ['[Gen 7] OU', 'Ability Clause', 'Ignore Illegal Abilities'], - banlist: ['Archeops', 'Dragonite', 'Hoopa-Unbound', 'Kartana', 'Keldeo', 'Kyurem-Black', 'Regigigas', 'Shedinja', 'Slaking', 'Terrakion'], + banlist: ['Archeops', 'Dragonite', 'Hoopa-Unbound', 'Kartana', 'Keldeo', 'Kyurem-Black', 'Regigigas', 'Shedinja', 'Slaking', 'Terrakion', 'Zygarde-Base'], unbanlist: ['Aegislash', 'Genesect', 'Landorus', 'Metagross-Mega', 'Naganadel'], restrictedAbilities: [ 'Comatose', 'Contrary', 'Fluffy', 'Fur Coat', 'Huge Power', 'Illusion', 'Imposter', 'Innards Out', @@ -678,9 +678,6 @@ let Formats = [ if (!legalAbility) return ['The ability ' + set.ability + ' is banned on Pok\u00e9mon that do not naturally have it.']; } }, - onBegin: function () { - if (this.rated) this.add('html', `
Almost Any Ability is currently suspecting Zygarde! For information on how to participate check out the suspect thread.
`); - }, }, { name: "[Gen 7] STABmons", From 101f16939a3f149579bf84a3bad9fefc2cd5895d Mon Sep 17 00:00:00 2001 From: LegoFigure11 Date: Wed, 29 Aug 2018 23:14:52 +1000 Subject: [PATCH 012/125] Add Battle Spot Special 12 (#4835) --- config/formats.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/config/formats.js b/config/formats.js index eb20d9d797e9..6f45cf1a7d86 100644 --- a/config/formats.js +++ b/config/formats.js @@ -195,19 +195,28 @@ let Formats = [ requirePentagon: true, }, { - name: "[Gen 7] Battle Spot Special 11", - threads: [`• Battle Spot Special 11`], + name: "[Gen 7] Battle Spot Special 12", + threads: [`• Battle Spot Special 12`], mod: 'gen7', - forcedLevel: 100, + forcedLevel: 50, teamLength: { validate: [3, 6], battle: 3, }, - ruleset: ['Pokemon', 'Species Clause', 'Item Clause', 'Nickname Clause', 'Team Preview', 'Cancel Mod'], - banlist: ['Illegal', 'Unreleased', 'Mewtwo', 'Lugia', 'Ho-Oh', 'Kyogre', 'Groudon', 'Rayquaza', 'Dialga', 'Palkia', 'Giratina', - 'Arceus', 'Reshiram', 'Zekrom', 'Kyurem', 'Xerneas', 'Yveltal', 'Solgaleo', 'Lunala', 'Necrozma-Dusk-Mane', 'Necrozma-Dawn-Wings', + ruleset: ['Pokemon', 'Standard GBU'], + banlist: ['Articuno', 'Zapdos', 'Moltres', 'Raikou', 'Entei', 'Suicune', 'Regirock', 'Regice', 'Registeel', 'Latias', + 'Latios', 'Uxie', 'Mesprit', 'Azelf', 'Heatran', 'Regigigas', 'Cresselia', 'Cobalion', 'Terrakion', 'Virizion', + 'Tornadus', 'Thundurus', 'Landorus', 'Type: Null', 'Silvally', 'Tapu Koko', 'Tapu Lele', 'Tapu Bulu', 'Tapu Fini', + 'Nihilego', 'Buzzwole', 'Pheromosa', 'Xurkitree', 'Celesteela', 'Kartana', 'Guzzlord', 'Poipole', 'Naganadel', + 'Stakataka', 'Blacephalon', ], + onValidateSet: function (set, format) { + if (set.item) { + let item = this.getItem(set.item); + if (item.megaStone) return [`${set.name || set.species} has ${item.name}, which is banned in ${format.name}.`]; + } + }, }, { name: "[Gen 7] Custom Game", From b97365f52c664ccd74d4eeabb47825210a015b96 Mon Sep 17 00:00:00 2001 From: urkerab Date: Wed, 29 Aug 2018 20:48:30 +0100 Subject: [PATCH 013/125] Restore Mirror Move's short description (#4836) --- data/moves.js | 1 + 1 file changed, 1 insertion(+) diff --git a/data/moves.js b/data/moves.js index 614943fa7dfb..2b1ca021d6ae 100644 --- a/data/moves.js +++ b/data/moves.js @@ -10515,6 +10515,7 @@ let BattleMovedex = { basePower: 0, category: "Status", desc: "The user uses the last move used by the target. The copied move is used against that target, if possible. Fails if the target has not made a move, or if the last move used cannot be copied by this move.", + shortDesc: "User uses the target's last used move against it.", id: "mirrormove", name: "Mirror Move", pp: 20, From 87064b0255a0eeb288607c5ce7e4a893871be3fb Mon Sep 17 00:00:00 2001 From: The Immortal Date: Thu, 30 Aug 2018 15:19:01 +0400 Subject: [PATCH 014/125] Update threads for VGC --- config/formats.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/formats.js b/config/formats.js index 6f45cf1a7d86..c3b1ced9e104 100644 --- a/config/formats.js +++ b/config/formats.js @@ -343,6 +343,10 @@ let Formats = [ }, { name: "[Gen 7] VGC 2019 Ultra Series", + threads: [ + `• VGC 2019 Discussion`, + `• VGC 2019 Viability Rankings`, + ], mod: 'gen7', gameType: 'doubles', @@ -367,6 +371,11 @@ let Formats = [ }, { name: "[Gen 7] VGC 2018", + threads: [ + `• VGC 2018 Discussion`, + `• VGC 2018 Viability Rankings`, + `• VGC 2018 Sample Teams`, + ], mod: 'gen7', gameType: 'doubles', From 1bc1e15e93ccc49a8067c6b92697777e94188963 Mon Sep 17 00:00:00 2001 From: CheeseMuffin Date: Fri, 31 Aug 2018 18:01:15 -0700 Subject: [PATCH 015/125] Trivia: Update levenshtein function (#4720) --- chat-plugins/trivia.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/chat-plugins/trivia.js b/chat-plugins/trivia.js index f5782bb11187..c1ed24625ab9 100644 --- a/chat-plugins/trivia.js +++ b/chat-plugins/trivia.js @@ -543,10 +543,26 @@ class Trivia extends Rooms.RoomGame { */ verifyAnswer(tarAnswer) { return this.curAnswers.some(answer => ( - (answer === tarAnswer) || (answer.length > 5 && Dex.levenshtein(tarAnswer, answer) < 3) + (answer === tarAnswer) || (Dex.levenshtein(tarAnswer, answer) <= this.maxLevenshteinAllowed(answer.length)) )); } + /** + * Return the maximum Levenshtein distance that is allowable for answers of the given length. + * @param {number} answerLength + * @return {number} + */ + maxLevenshteinAllowed(answerLength) { + if (answerLength > 5) { + return 2; + } + + if (answerLength > 4) { + return 1; + } + + return 0; + } /** * This is a noop here since it'd defined properly by mode subclasses later * on. This calculates the points a correct responder earns, which is From 2c15a1663a3b28db89d0b32eeb1821558b6ef720 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Sat, 1 Sep 2018 04:09:35 -0500 Subject: [PATCH 016/125] BrightPowder -> Bright Powder Fixes https://github.com/Zarel/Pokemon-Showdown-Client/issues/1142 --- data/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/items.js b/data/items.js index bb4abf131171..9dbc231414f8 100644 --- a/data/items.js +++ b/data/items.js @@ -563,7 +563,7 @@ let BattleItems = { }, "brightpowder": { id: "brightpowder", - name: "BrightPowder", + name: "Bright Powder", spritenum: 51, fling: { basePower: 10, From bc23b3fa83dceb907b2efa8f869cc2cb171ed705 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Sat, 1 Sep 2018 15:04:29 +0400 Subject: [PATCH 017/125] 2v2: Add Sleep Clause --- config/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/formats.js b/config/formats.js index c3b1ced9e104..fa9c26e20f8e 100644 --- a/config/formats.js +++ b/config/formats.js @@ -770,7 +770,7 @@ let Formats = [ validate: [2, 4], battle: 2, }, - ruleset: ['[Gen 7] Doubles OU'], + ruleset: ['[Gen 7] Doubles OU', 'Sleep Clause Mod'], banlist: [ 'Salamence-Mega', 'Tapu Lele', 'Focus Sash', 'Final Gambit', 'Perish Song', 'Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Sand Attack', 'Smokescreen', From 55f27d88a0ddbd9ca2496a60c09525f0742c601c Mon Sep 17 00:00:00 2001 From: The Immortal Date: Sat, 1 Sep 2018 15:23:31 +0400 Subject: [PATCH 018/125] Gen 6 Random Battle: Update crash message --- mods/gen6/random-teams.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/gen6/random-teams.js b/mods/gen6/random-teams.js index 33a58ae28bde..85ac23024a29 100644 --- a/mods/gen6/random-teams.js +++ b/mods/gen6/random-teams.js @@ -30,7 +30,7 @@ class RandomGen6Teams extends RandomTeams { template = this.getTemplate('unown'); let err = new Error('Template incompatible with random battles: ' + species); - require('../../lib/crashlogger')(err, 'The randbat set generator'); + require('../../lib/crashlogger')(err, 'The gen 6 randbat set generator'); } if (template.battleOnly) { From 0a10b3067435436c470d5c3486f8cbbb040401b6 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Sat, 1 Sep 2018 15:26:11 +0400 Subject: [PATCH 019/125] Add Accuracy Moves Clause --- config/formats.js | 17 +++++++---------- data/rulesets.js | 9 +++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/config/formats.js b/config/formats.js index fa9c26e20f8e..b4b046a2d865 100644 --- a/config/formats.js +++ b/config/formats.js @@ -601,12 +601,12 @@ let Formats = [ validate: [1, 3], battle: 1, }, - ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + ruleset: ['Pokemon', 'Species Clause', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], banlist: [ - 'Illegal', 'Unreleased', 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', 'Ho-Oh', - 'Jirachi', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', 'Necrozma-Dawn-Wings', - 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', 'Solgaleo', 'Tapu Koko', 'Xerneas', 'Yveltal', 'Zekrom', - 'Focus Sash', 'Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Perish Song', 'Sand Attack', 'Smokescreen', + 'Illegal', 'Unreleased', 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', + 'Ho-Oh', 'Jirachi', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', + 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', + 'Solgaleo', 'Tapu Koko', 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Perish Song', ], }, { @@ -770,11 +770,8 @@ let Formats = [ validate: [2, 4], battle: 2, }, - ruleset: ['[Gen 7] Doubles OU', 'Sleep Clause Mod'], - banlist: [ - 'Salamence-Mega', 'Tapu Lele', 'Focus Sash', 'Final Gambit', 'Perish Song', - 'Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Sand Attack', 'Smokescreen', - ], + ruleset: ['[Gen 7] Doubles OU', 'Accuracy Moves Clause', 'Sleep Clause Mod'], + banlist: ['Salamence-Mega', 'Tapu Lele', 'Focus Sash', 'Final Gambit', 'Perish Song'], }, { name: "[Gen 6] Gen-NEXT OU", diff --git a/data/rulesets.js b/data/rulesets.js index 56ba8484c255..69d7be6b8e5f 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -544,6 +544,15 @@ let BattleFormats = { this.add('rule', 'Evasion Moves Clause: Evasion moves are banned'); }, }, + accuracymovesclause: { + effectType: 'ValidatorRule', + name: 'Accuracy Moves Clause', + desc: "Bans moves that have a chance to lower the target's accuracy when used", + banlist: ['Flash', 'Kinesis', 'Leaf Tornado', 'Mirror Shot', 'Mud Bomb', 'Mud-Slap', 'Muddy Water', 'Night Daze', 'Octazooka', 'Sand Attack', 'Smokescreen'], + onStart: function () { + this.add('rule', 'Accuracy Moves Clause: Accuracy-lowering moves are banned'); + }, + }, endlessbattleclause: { effectType: 'Rule', name: 'Endless Battle Clause', From 5813add1e26b7e7c56dd5c58464556bff14a80e9 Mon Sep 17 00:00:00 2001 From: Kris Johnson <11083252+KrisXV@users.noreply.github.com> Date: Sat, 1 Sep 2018 06:02:05 -0600 Subject: [PATCH 020/125] Add September RoA Spotlight, LCotM, and OMotM (#4838) --- config/formats.js | 304 +++++++++++++++++++----------------- data/rulesets.js | 6 - mods/gen3/formats-data.js | 28 ++-- mods/mixandmega6/items.js | 65 -------- mods/mixandmega6/scripts.js | 117 -------------- mods/pic/moves.js | 66 ++++++++ mods/pic/scripts.js | 62 ++++++++ tsconfig.json | 3 +- 8 files changed, 309 insertions(+), 342 deletions(-) delete mode 100644 mods/mixandmega6/items.js delete mode 100644 mods/mixandmega6/scripts.js create mode 100644 mods/pic/moves.js create mode 100644 mods/pic/scripts.js diff --git a/config/formats.js b/config/formats.js index b4b046a2d865..e9bc3dba0ad5 100644 --- a/config/formats.js +++ b/config/formats.js @@ -496,79 +496,135 @@ let Formats = [ column: 2, }, { - name: "[Gen 7] Tier Shift", - desc: `Pokémon get +10 to each stat per tier below OU they are in. UU gets +10, RU +20, NU +30, and PU +40.`, + name: "[Gen 7] Sketchmons", + desc: `Pokémon can learn one of any move they don't normally learn, barring the few that are banned.`, threads: [ - `• Tier Shift`, + `• Sketchmons`, ], mod: 'gen7', ruleset: ['[Gen 7] OU'], - banlist: ['Damp Rock', 'Deep Sea Tooth', 'Eviolite'], - onModifyTemplate: function (template, pokemon) { - let tsTemplate = Object.assign({}, template); - const boosts = {'UU': 10, 'RUBL': 10, 'RU': 20, 'NUBL': 20, 'NU': 30, 'PUBL': 30, 'PU': 40, 'NFE': 40, 'LC Uber': 40, 'LC': 40}; - let tier = tsTemplate.tier; - if (pokemon && pokemon.set.item) { - let item = this.getItem(pokemon.set.item); - if (item.megaEvolves === tsTemplate.species) tier = this.getTemplate(item.megaStone).tier; - } - if (tier.charAt(0) === '(') tier = tier.slice(1, -1); - let boost = (tier in boosts) ? boosts[tier] : 0; - if (boost > 0 && pokemon && (pokemon.set.ability === 'Drizzle' || pokemon.set.item === 'Mewnium Z')) boost = 0; - if (boost > 10 && pokemon && pokemon.set.moves.includes('auroraveil')) boost = 10; - if (boost > 20 && pokemon && pokemon.set.ability === 'Drought') boost = 20; - tsTemplate.baseStats = Object.assign({}, tsTemplate.baseStats); - // `Dex` needs to be used in /data, `this` needs to be used in battles - const clampRange = this && this.clampIntRange ? this.clampIntRange : Dex.clampIntRange; - for (let statName in tsTemplate.baseStats) { + banlist: ['Kartana', 'Porygon-Z', 'Battle Bond'], + restrictedMoves: [ + 'Belly Drum', 'Celebrate', 'Chatter', 'Conversion', "Forest's Curse", 'Geomancy', 'Happy Hour', 'Hold Hands', + 'Lovely Kiss', 'Purify', 'Quiver Dance', 'Shell Smash', 'Shift Gear', 'Sketch', 'Spore', 'Sticky Web', 'Trick-or-Treat', + ], + checkLearnset: function (move, template, lsetData, set) { + let problem = this.checkLearnset(move, template, lsetData, set); + if (!problem) return null; + const restrictedMoves = this.format.restrictedMoves || []; + if (move.isZ || restrictedMoves.includes(move.name)) return problem; + // @ts-ignore + if (set.sketchMove) return {type: 'oversketched', maxSketches: 1}; + // @ts-ignore + set.sketchMove = move.id; + return null; + }, + onValidateTeam: function (team, format, teamHas) { + let sketches = {}; + for (const set of team) { // @ts-ignore - tsTemplate.baseStats[statName] = clampRange(tsTemplate.baseStats[statName] + boost, 1, 255); + if (set.sketchMove) { + // @ts-ignore + if (!sketches[set.sketchMove]) { + // @ts-ignore + sketches[set.sketchMove] = 1; + } else { + // @ts-ignore + sketches[set.sketchMove]++; + } + } } - return tsTemplate; + let overSketched = Object.keys(sketches).filter(move => sketches[move] > 1); + if (overSketched.length) return overSketched.map(move => `You are limited to 1 of ${this.getMove(move).name} by Sketch Clause. (You have sketched ${this.getMove(move).name} ${sketches[move]} times.)`); }, }, { - name: "[Gen 7] Benjamin Butterfree", - desc: `Pokémon that faint reincarnate as their prevo, but without the moves they can't learn.`, + name: "[Gen 7] Partners in Crime", + desc: `Doubles-based metagame where both active ally Pokémon share abilities and moves.`, threads: [ - `• Benjamin Butterfree`, + `• Partners in Crime`, ], - mod: 'gen7', - ruleset: ['[Gen 7] OU'], - banlist: ['Blissey'], - onBeforeFaint: function (pokemon) { - let prevo = pokemon.baseTemplate.isMega ? this.getTemplate(pokemon.baseTemplate.baseSpecies).prevo : pokemon.baseTemplate.prevo; - if (!prevo || pokemon.set.ability === 'Battle Bond') return; - let template = this.getTemplate(pokemon.set.species); + mod: 'pic', + gameType: 'doubles', + ruleset: ['[Gen 7] Doubles OU', 'Sleep Clause Mod'], + banlist: ['Kangaskhanite', 'Mawilite', 'Medichamite', 'Huge Power', 'Imposter', 'Normalize', 'Pure Power', 'Wonder Guard', 'Mimic', 'Sketch', 'Transform'], + onSwitchInPriority: 2, + onSwitchIn: function (pokemon) { + if (this.p1.active.every(ally => ally && !ally.fainted)) { + let p1a = this.p1.active[0], p1b = this.p1.active[1]; + if (p1a.ability !== p1b.ability) { + let p1aInnate = 'ability' + p1b.ability; + p1a.volatiles[p1aInnate] = {id: p1aInnate, target: p1a}; + let p1bInnate = 'ability' + p1a.ability; + p1b.volatiles[p1bInnate] = {id: p1bInnate, target: p1b}; + } + } + if (this.p2.active.every(ally => ally && !ally.fainted)) { + let p2a = this.p2.active[0], p2b = this.p2.active[1]; + if (p2a.ability !== p2b.ability) { + let p2a_innate = 'ability' + p2b.ability; + p2a.volatiles[p2a_innate] = {id: p2a_innate, target: p2a}; + let p2b_innate = 'ability' + p2a.ability; + p2b.volatiles[p2b_innate] = {id: p2b_innate, target: p2b}; + } + } + let ally = pokemon.side.active.find(ally => ally && ally !== pokemon && !ally.fainted); + if (ally && ally.ability !== pokemon.ability) { + // @ts-ignore + if (!pokemon.innate) { + // @ts-ignore + pokemon.innate = 'ability' + ally.ability; + // @ts-ignore + delete pokemon.volatiles[pokemon.innate]; + // @ts-ignore + pokemon.addVolatile(pokemon.innate); + } + // @ts-ignore + if (!ally.innate) { + // @ts-ignore + ally.innate = 'ability' + pokemon.ability; + // @ts-ignore + delete ally.volatiles[ally.innate]; + // @ts-ignore + ally.addVolatile(ally.innate); + } + } + }, + onSwitchOut: function (pokemon) { // @ts-ignore - let abilitySlot = Object.keys(template.abilities).find(slot => template.abilities[slot] === pokemon.set.ability); - template = this.getTemplate(prevo); + if (pokemon.innate) { + // @ts-ignore + pokemon.removeVolatile(pokemon.innate); + // @ts-ignore + delete pokemon.innate; + } + let ally = pokemon.side.active.find(ally => ally && ally !== pokemon && !ally.fainted); // @ts-ignore - if (!template.abilities[abilitySlot]) abilitySlot = '0'; - pokemon.faintQueued = false; - pokemon.hp = pokemon.maxhp; - if (Object.values(pokemon.boosts).find(boost => boost !== 0)) { - pokemon.clearBoosts(); - this.add('-clearboost', pokemon); + if (ally && ally.innate) { + // @ts-ignore + ally.removeVolatile(ally.innate); + // @ts-ignore + delete ally.innate; } - pokemon.formeChange(template, this.getFormat(), true, '', abilitySlot); - this.add('-message', `${pokemon.name} has devolved into ${template.species}!`); - pokemon.cureStatus(true); - let newHP = Math.floor(Math.floor(2 * pokemon.template.baseStats['hp'] + pokemon.set.ivs['hp'] + Math.floor(pokemon.set.evs['hp'] / 4) + 100) * pokemon.level / 100 + 10); - pokemon.maxhp = pokemon.hp = newHP; - this.add('-heal', pokemon, pokemon.getHealth, '[silent]'); - let learnset = template.learnset || this.getTemplate(template.baseSpecies).learnset || {}; - let prevoset = template.prevo && this.getTemplate(template.prevo).learnset || {}; - for (const moveSlot of pokemon.baseMoveSlots) { - if (!learnset[moveSlot.id] && !prevoset[moveSlot.id]) { - moveSlot.used = true; - moveSlot.pp = 0; - } + }, + onFaint: function (pokemon) { + // @ts-ignore + if (pokemon.innate) { + // @ts-ignore + pokemon.removeVolatile(pokemon.innate); + // @ts-ignore + delete pokemon.innate; + } + let ally = pokemon.side.active.find(ally => ally && ally !== pokemon && !ally.fainted); + // @ts-ignore + if (ally && ally.innate) { + // @ts-ignore + ally.removeVolatile(ally.innate); + // @ts-ignore + delete ally.innate; } - pokemon.canMegaEvo = null; - return false; }, }, { @@ -928,91 +984,61 @@ let Formats = [ column: 3, }, { - name: "[Gen 3] Ubers", + name: "[Gen 4] Ubers", threads: [ - `• ADV Ubers Information & Resources`, - `• ADV Ubers Viability Ranking`, + `• DPP Ubers Information & Resources`, + `• DPP Ubers Viability Ranking`, ], - mod: 'gen3', + mod: 'gen4', // searchShow: false, ruleset: ['Pokemon', 'Standard'], - banlist: ['Smeargle + Ingrain', 'Wobbuffet + Leftovers'], + banlist: ['Arceus'], }, { - name: "[Gen 6] Mix and Mega", - desc: `Mega Stones and Primal Orbs can be used on almost any fully evolved Pokémon with no Mega Evolution limit.`, - threads: [`• ORAS Mix and Mega`], + name: "[Gen 6] 1v1", + desc: `Bring three Pokémon to Team Preview and choose one to battle."`, - mod: 'mixandmega6', - ruleset: ['Pokemon', 'Standard', 'Mega Rayquaza Clause', 'Team Preview'], - banlist: ['Baton Pass', 'Dynamic Punch', 'Electrify', 'Zap Cannon'], - restrictedStones: ['Beedrillite', 'Gengarite', 'Kangaskhanite', 'Mawilite', 'Medichamite'], - cannotMega: [ - 'Arceus', 'Cresselia', 'Darkrai', 'Deoxys', 'Deoxys-Attack', 'Dialga', 'Dragonite', 'Genesect', 'Giratina', - 'Groudon', 'Ho-Oh', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lucario', 'Lugia', 'Manaphy', 'Mewtwo', - 'Palkia', 'Rayquaza', 'Regigigas', 'Reshiram', 'Shaymin-Sky', 'Slaking', 'Xerneas', 'Yveltal', 'Zekrom', - ], - onValidateTeam: function (team) { - let itemTable = {}; - for (const set of team) { - let item = this.getItem(set.item); - if (!item) continue; - if (itemTable[item.id] && item.megaStone) return ["You are limited to one of each Mega Stone.", "(You have more than one " + this.getItem(item).name + ")"]; - if (itemTable[item.id] && (item.id === 'blueorb' || item.id === 'redorb')) return ["You are limited to one of each Primal Orb.", "(You have more than one " + this.getItem(item).name + ")"]; - itemTable[item.id] = true; - } - }, - onValidateSet: function (set, format) { - let template = this.getTemplate(set.species || set.name); - let item = this.getItem(set.item); - if (!item.megaEvolves && item.id !== 'blueorb' && item.id !== 'redorb') return; - if (template.baseSpecies === item.megaEvolves || (template.baseSpecies === 'Groudon' && item.id === 'redorb') || (template.baseSpecies === 'Kyogre' && item.id === 'blueorb')) return; - if (template.evos.length) return ["" + template.species + " is not allowed to hold " + item.name + " because it's not fully evolved."]; - let uberStones = format.restrictedStones || []; - let uberPokemon = format.cannotMega || []; - if (uberPokemon.includes(template.name) || uberStones.includes(item.name)) return ["" + template.species + " is not allowed to hold " + item.name + "."]; - }, - onBegin: function () { - for (const pokemon of this.p1.pokemon.concat(this.p2.pokemon)) { - pokemon.originalSpecies = pokemon.baseTemplate.species; - } - }, - onSwitchIn: function (pokemon) { - let oMegaTemplate = this.getTemplate(pokemon.template.originalMega); - if (oMegaTemplate.exists && pokemon.originalSpecies !== oMegaTemplate.baseSpecies) { - // Place volatiles on the Pokémon to show its mega-evolved condition and details - this.add('-start', pokemon, oMegaTemplate.requiredItem || oMegaTemplate.requiredMove, '[silent]'); - let oTemplate = this.getTemplate(pokemon.originalSpecies); - if (oTemplate.types.length !== pokemon.template.types.length || oTemplate.types[1] !== pokemon.template.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.template.types.join('/'), '[silent]'); - } - } - }, - onSwitchOut: function (pokemon) { - let oMegaTemplate = this.getTemplate(pokemon.template.originalMega); - if (oMegaTemplate.exists && pokemon.originalSpecies !== oMegaTemplate.baseSpecies) { - this.add('-end', pokemon, oMegaTemplate.requiredItem || oMegaTemplate.requiredMove, '[silent]'); - } + mod: 'gen6', + teamLength: { + validate: [1, 3], + battle: 1, }, + ruleset: ['Pokemon', 'Nickname Clause', 'Moody Clause', 'OHKO Clause', 'Evasion Moves Clause', 'Accuracy Moves Clause', 'Swagger Clause', 'Endless Battle Clause', 'HP Percentage Mod', 'Cancel Mod', 'Team Preview'], + banlist: [ + 'Illegal', 'Unreleased', 'Arceus', 'Blaziken', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Dialga', 'Giratina', + 'Groudon', 'Ho-Oh', 'Kyogre', 'Kyurem-White', 'Lugia', 'Mewtwo', 'Palkia', 'Rayquaza', 'Reshiram', 'Shaymin-Sky', + 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Kangaskhanite', 'Salamencite', 'Soul Dew', 'Perish Song', + ], }, { - name: "[Gen 2] Mediocremons", + name: "[Gen 3] Tier Shift", + desc: `Pokémon get +5 to each stat per tier below OU, including UUBL, they are in. UUBL gets +5, UU +10, NU +15, and PU +20.`, - mod: 'gen2', - ruleset: ['[Gen 2] OU'], - banlist: [], - onValidateSet: function (set) { - let problems = []; - let template = this.getTemplate(set.species); - let item = this.getItem(set.item); - if (item.megaEvolves === template.species) template = this.getTemplate(item.megaStone); - let statTable = {hp: 'an HP', atk: 'an Attack', def: 'a Defense', spa: 'a Special Attack', spd: 'a Special Defense', spe: 'a Speed'}; - for (let stat in statTable) { + mod: 'gen3', + ruleset: ['[Gen 3] OU'], + onModifyTemplate: function (template, pokemon) { + let tsTemplate = Object.assign({}, template); + let puPokemon = [ + 'Aipom', 'Anorith', 'Ariados', 'Beautifly', 'Beedrill', 'Butterfree', 'Castform', 'Charmeleon', 'Clamperl', 'Combusken', + 'Corsola', 'Croconaw', 'Delcatty', 'Delibird', 'Ditto', 'Doduo', 'Dragonair', 'Drowzee', 'Duskull', 'Dustox', 'Farfetch\'d', + 'Furret', 'Gastly', 'Grovyle', 'Houndour', 'Illumise', 'Ivysaur', 'Lairon', 'Ledian', 'Lombre', 'Luvdisc', 'Machoke', + 'Marshtomp', 'Masquerain', 'Mightyena', 'Minun', 'Noctowl', 'Nosepass', 'Omanyte', 'Parasect', 'Poliwhirl', 'Ponyta', + 'Porygon', 'Quilava', 'Seaking', 'Sealeo', 'Seviper', 'Shedinja', 'Shuckle', 'Smoochum', 'Spinda', 'Sunflora', 'Tentacool', + 'Trapinch', 'Unown', 'Vibrava', 'Wartortle', 'Weepinbell', 'Wigglytuff', 'Yanma', + ]; + if (puPokemon.includes(tsTemplate.species)) tsTemplate.tier = 'PU'; + const boosts = {'UUBL': 5, 'UU': 10, 'NU': 15, 'PU': 20, 'NFE': 20, 'LC': 20}; + let tier = tsTemplate.tier; + let boost = (tier in boosts) ? boosts[tier] : 0; + tsTemplate.baseStats = Object.assign({}, tsTemplate.baseStats); + // `Dex` needs to be used in /data, `this` needs to be used in battles + const clampRange = this && this.clampIntRange ? this.clampIntRange : Dex.clampIntRange; + for (let statName in tsTemplate.baseStats) { // @ts-ignore - if (template.baseStats[stat] >= 100) problems.push(template.species + " has " + statTable[stat] + " of " + template.baseStats[stat] + ", which is banned."); + tsTemplate.baseStats[statName] = clampRange(tsTemplate.baseStats[statName] + boost, 1, 255); } - return problems; + return tsTemplate; }, }, @@ -1500,18 +1526,6 @@ let Formats = [ section: "DPP Singles", column: 4, }, - { - name: "[Gen 4] Ubers", - threads: [ - `• DPP Ubers Information & Resources`, - `• DPP Ubers Viability Ranking`, - ], - - mod: 'gen4', - searchShow: false, - ruleset: ['Pokemon', 'Standard'], - banlist: ['Arceus'], - }, { name: "[Gen 4] UU", threads: [ @@ -1601,6 +1615,18 @@ let Formats = [ section: "Past Generations", column: 4, }, + { + name: "[Gen 3] Ubers", + threads: [ + `• ADV Ubers Information & Resources`, + `• ADV Ubers Viability Ranking`, + ], + + mod: 'gen3', + searchShow: false, + ruleset: ['Pokemon', 'Standard'], + banlist: ['Smeargle + Ingrain', 'Wobbuffet + Leftovers'], + }, { name: "[Gen 3] UU", threads: [ diff --git a/data/rulesets.js b/data/rulesets.js index 69d7be6b8e5f..ab4552523cc7 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -850,12 +850,6 @@ let BattleFormats = { return this.checkLearnset(move, template, lsetData, set); }, }, - allowonesketch: { - effectType: 'ValidatorRule', - name: 'Allow One Sketch', - desc: "Allows each Pokémon to use one move they don't normally have access to via Sketch", - // Implemented in team-validator.js - }, allowcap: { effectType: 'ValidatorRule', name: 'Allow CAP', diff --git a/mods/gen3/formats-data.js b/mods/gen3/formats-data.js index ab9cf6142612..4fd6b2766363 100644 --- a/mods/gen3/formats-data.js +++ b/mods/gen3/formats-data.js @@ -18,7 +18,7 @@ let BattleFormatsData = { tier: "LC", }, charmeleon: { - tier: "NU", + tier: "NFE", }, charizard: { inherit: true, @@ -30,7 +30,7 @@ let BattleFormatsData = { tier: "LC", }, wartortle: { - tier: "NU", + tier: "NFE", }, blastoise: { inherit: true, @@ -207,7 +207,7 @@ let BattleFormatsData = { tier: "NU", }, diglett: { - tier: "LC", + tier: "NU", }, dugtrio: { inherit: true, @@ -252,7 +252,7 @@ let BattleFormatsData = { tier: "LC", }, poliwhirl: { - tier: "NU", + tier: "NFE", }, poliwrath: { inherit: true, @@ -264,7 +264,7 @@ let BattleFormatsData = { tier: "UU", }, abra: { - tier: "LC", + tier: "NU", }, kadabra: { tier: "UUBL", @@ -278,7 +278,7 @@ let BattleFormatsData = { tier: "LC", }, machoke: { - tier: "NU", + tier: "NFE", }, machamp: { inherit: true, @@ -291,7 +291,7 @@ let BattleFormatsData = { }, weepinbell: { inherit: true, - tier: "NU", + tier: "NFE", }, victreebel: { randomBattleMoves: ["sleeppowder", "sludgebomb", "synthesis", "sunnyday", "solarbeam", "gigadrain", "hiddenpowerfire", "hiddenpowerground", "swordsdance"], @@ -660,7 +660,7 @@ let BattleFormatsData = { tier: "LC", }, dragonair: { - tier: "NU", + tier: "NFE", }, dragonite: { inherit: true, @@ -693,7 +693,7 @@ let BattleFormatsData = { tier: "LC", }, quilava: { - tier: "NU", + tier: "NFE", }, typhlosion: { inherit: true, @@ -1021,7 +1021,7 @@ let BattleFormatsData = { tier: "LC", }, grovyle: { - tier: "NU", + tier: "NFE", }, sceptile: { randomBattleMoves: ["leafblade", "dragonclaw", "hiddenpowerfire", "hiddenpowerice", "crunch", "thunderpunch", "leechseed", "substitute"], @@ -1219,7 +1219,7 @@ let BattleFormatsData = { tier: "LC", }, lairon: { - tier: "NU", + tier: "NFE", }, aggron: { inherit: true, @@ -1315,7 +1315,7 @@ let BattleFormatsData = { tier: "NU", }, trapinch: { - tier: "NU", + tier: "LC", }, vibrava: { tier: "NFE", @@ -1474,14 +1474,14 @@ let BattleFormatsData = { tier: "LC", }, sealeo: { - tier: "NU", + tier: "NFE", }, walrein: { randomBattleMoves: ["icebeam", "surf", "earthquake", "hiddenpowergrass", "toxic", "rest", "sleeptalk", "encore", "roar", "yawn"], tier: "UU", }, clamperl: { - tier: "NU", + tier: "LC", }, huntail: { randomBattleMoves: ["doubleedge", "surf", "hydropump", "icebeam", "crunch", "hiddenpowergrass", "raindance"], diff --git a/mods/mixandmega6/items.js b/mods/mixandmega6/items.js deleted file mode 100644 index ec220e3e36a0..000000000000 --- a/mods/mixandmega6/items.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -/**@type {{[k: string]: ModdedItemData}} */ -let BattleItems = { - blueorb: { - inherit: true, - onSwitchIn: function (pokemon) { - if (pokemon.isActive && !pokemon.template.isPrimal) { - this.insertQueue({pokemon: pokemon, choice: 'runPrimal'}); - } - }, - onPrimal: function (pokemon) { - /**@type {Template} */ - // @ts-ignore - let template = this.getMixedTemplate(pokemon.originalSpecies, 'Kyogre-Primal'); - if (pokemon.originalSpecies === 'Kyogre') { - pokemon.formeChange(template, this.effect, true); - } else { - pokemon.formeChange(template, this.effect, true); - pokemon.baseTemplate = template; - this.add('-start', pokemon, 'Blue Orb', '[silent]'); - } - }, - onTakeItem: function (item) { - return false; - }, - }, - redorb: { - inherit: true, - onSwitchIn: function (pokemon) { - if (pokemon.isActive && !pokemon.template.isPrimal) { - this.insertQueue({pokemon: pokemon, choice: 'runPrimal'}); - } - }, - onPrimal: function (pokemon) { - /**@type {Template} */ - // @ts-ignore - let template = this.getMixedTemplate(pokemon.originalSpecies, 'Groudon-Primal'); - if (pokemon.originalSpecies === 'Groudon') { - pokemon.formeChange(template, this.effect, true); - } else { - pokemon.formeChange(template, this.effect, true); - pokemon.baseTemplate = template; - this.add('-start', pokemon, 'Red Orb', '[silent]'); - // @ts-ignore - let oTemplate = this.getTemplate(pokemon.illusion || pokemon.originalSpecies); - this.add('-start', pokemon, 'Red Orb', '[silent]'); - if (pokemon.illusion) { - pokemon.ability = ''; - let types = oTemplate.types; - if (types.length > 1 || types[types.length - 1] !== 'Fire') { - this.add('-start', pokemon, 'typechange', (types[0] !== 'Fire' ? types[0] + '/' : '') + 'Fire', '[silent]'); - } - } else if (oTemplate.types.length !== pokemon.template.types.length || oTemplate.types[1] !== pokemon.template.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.template.types.join('/'), '[silent]'); - } - } - }, - onTakeItem: function (item) { - return false; - }, - }, -}; - -exports.BattleItems = BattleItems; diff --git a/mods/mixandmega6/scripts.js b/mods/mixandmega6/scripts.js deleted file mode 100644 index bf80f27909cf..000000000000 --- a/mods/mixandmega6/scripts.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; - -/**@type {ModdedBattleScriptsData} */ -let BattleScripts = { - inherit: 'gen6', - init: function () { - for (let id in this.data.Items) { - if (!this.data.Items[id].megaStone) continue; - this.modData('Items', id).onTakeItem = false; - } - }, - canMegaEvo: function (pokemon) { - if (pokemon.template.isMega || pokemon.template.isPrimal) return null; - - const item = pokemon.getItem(); - if (item.megaStone) { - if (item.megaStone === pokemon.species) return null; - return item.megaStone; - } else if (pokemon.baseMoves.includes('dragonascent')) { - return 'Rayquaza-Mega'; - } else { - return null; - } - }, - runMegaEvo: function (pokemon) { - if (pokemon.template.isMega || pokemon.template.isPrimal) return false; - - /**@type {Template} */ - // @ts-ignore - const template = this.getMixedTemplate(pokemon.originalSpecies, pokemon.canMegaEvo); - const side = pokemon.side; - - // Pokémon affected by Sky Drop cannot Mega Evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; - } - } - - // Do we have a proper sprite for it? - // @ts-ignore - if (this.getTemplate(pokemon.canMegaEvo).baseSpecies === pokemon.originalSpecies) { - pokemon.formeChange(template, pokemon.getItem(), true); - } else { - let oTemplate = this.getTemplate(pokemon.originalSpecies); - let oMegaTemplate = this.getTemplate(template.originalMega); - pokemon.formeChange(template, pokemon.getItem(), true); - this.add('-start', pokemon, oMegaTemplate.requiredItem || oMegaTemplate.requiredMove, '[silent]'); - if (oTemplate.types.length !== pokemon.template.types.length || oTemplate.types[1] !== pokemon.template.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.template.types.join('/'), '[silent]'); - } - } - - pokemon.canMegaEvo = null; - return true; - }, - getMixedTemplate: function (originalSpecies, megaSpecies) { - let originalTemplate = this.getTemplate(originalSpecies); - let megaTemplate = this.getTemplate(megaSpecies); - if (originalTemplate.baseSpecies === megaTemplate.baseSpecies) return megaTemplate; - // @ts-ignore - let deltas = this.getMegaDeltas(megaTemplate); - // @ts-ignore - let template = this.doGetMixedTemplate(originalTemplate, deltas); - return template; - }, - getMegaDeltas: function (megaTemplate) { - let baseTemplate = this.getTemplate(megaTemplate.baseSpecies); - let deltas = { - ability: megaTemplate.abilities['0'], - baseStats: {}, - weightkg: megaTemplate.weightkg - baseTemplate.weightkg, - originalMega: megaTemplate.species, - requiredItem: megaTemplate.requiredItem, - }; - for (let statId in megaTemplate.baseStats) { - // @ts-ignore - deltas.baseStats[statId] = megaTemplate.baseStats[statId] - baseTemplate.baseStats[statId]; - } - if (megaTemplate.types.length > baseTemplate.types.length) { - deltas.type = megaTemplate.types[1]; - } else if (megaTemplate.types.length < baseTemplate.types.length) { - deltas.type = baseTemplate.types[0]; - } else if (megaTemplate.types[1] !== baseTemplate.types[1]) { - deltas.type = megaTemplate.types[1]; - } - if (megaTemplate.isMega) deltas.isMega = true; - if (megaTemplate.isPrimal) deltas.isPrimal = true; - return deltas; - }, - doGetMixedTemplate: function (template, deltas) { - if (!deltas) throw new TypeError("Must specify deltas!"); - if (!template || typeof template === 'string') template = this.getTemplate(template); - template = Object.assign({}, template); - template.abilities = {'0': deltas.ability}; - if (template.types[0] === deltas.type) { - template.types = [deltas.type]; - } else if (deltas.type) { - template.types = [template.types[0], deltas.type]; - } - let baseStats = template.baseStats; - // @ts-ignore - template.baseStats = {}; - for (let statName in baseStats) { - // @ts-ignore - template.baseStats[statName] = this.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255); - } - template.weightkg = Math.max(0.1, template.weightkg + deltas.weightkg); - template.originalMega = deltas.originalMega; - template.requiredItem = deltas.requiredItem; - if (deltas.isMega) template.isMega = true; - if (deltas.isPrimal) template.isPrimal = true; - return template; - }, -}; - -exports.BattleScripts = BattleScripts; diff --git a/mods/pic/moves.js b/mods/pic/moves.js new file mode 100644 index 000000000000..d622677a1623 --- /dev/null +++ b/mods/pic/moves.js @@ -0,0 +1,66 @@ +'use strict'; + +exports.BattleMovedex = { + "skillswap": { + inherit: true, + onHit: function (target, source, move) { + let targetAbility = this.getAbility(target.ability); + let sourceAbility = this.getAbility(source.ability); + if (target.side === source.side) { + this.add('-activate', source, 'move: Skill Swap', '', '', '[of] ' + target); + } else { + this.add('-activate', source, 'move: Skill Swap', targetAbility, sourceAbility, '[of] ' + target); + } + this.singleEvent('End', sourceAbility, source.abilityData, source); + let sourceAlly = source.side.active.find(ally => ally && ally !== source && !ally.fainted); + if (sourceAlly && sourceAlly.innate) { + sourceAlly.removeVolatile(sourceAlly.innate); + delete sourceAlly.innate; + } + this.singleEvent('End', targetAbility, target.abilityData, target); + let targetAlly = target.side.active.find(ally => ally && ally !== target && !ally.fainted); + if (targetAlly && targetAlly.innate) { + targetAlly.removeVolatile(targetAlly.innate); + delete targetAlly.innate; + } + if (targetAbility.id !== sourceAbility.id) { + source.ability = targetAbility.id; + target.ability = sourceAbility.id; + source.abilityData = {id: source.ability.id, target: source}; + target.abilityData = {id: target.ability.id, target: target}; + } + if (sourceAlly && sourceAlly.ability !== source.ability) { + let volatile = sourceAlly.innate = 'ability' + source.ability; + sourceAlly.volatiles[volatile] = {id: volatile}; + sourceAlly.volatiles[volatile].target = sourceAlly; + sourceAlly.volatiles[volatile].source = source; + sourceAlly.volatiles[volatile].sourcePosition = source.position; + if (!source.innate) { + volatile = source.innate = 'ability' + sourceAlly.ability; + source.volatiles[volatile] = {id: volatile}; + source.volatiles[volatile].target = source; + source.volatiles[volatile].source = sourceAlly; + source.volatiles[volatile].sourcePosition = sourceAlly.position; + } + } + if (targetAlly && targetAlly.ability !== target.ability) { + let volatile = targetAlly.innate = 'ability' + target.ability; + targetAlly.volatiles[volatile] = {id: volatile}; + targetAlly.volatiles[volatile].target = targetAlly; + targetAlly.volatiles[volatile].source = target; + targetAlly.volatiles[volatile].sourcePosition = target.position; + if (!target.innate) { + volatile = target.innate = 'ability' + targetAlly.ability; + target.volatiles[volatile] = {id: volatile}; + target.volatiles[volatile].target = target; + target.volatiles[volatile].source = targetAlly; + target.volatiles[volatile].sourcePosition = targetAlly.position; + } + } + this.singleEvent('Start', targetAbility, source.abilityData, source); + if (sourceAlly) this.singleEvent('Start', sourceAlly.innate, sourceAlly.volatiles[sourceAlly.innate], sourceAlly); + this.singleEvent('Start', sourceAbility, target.abilityData, target); + if (targetAlly) this.singleEvent('Start', targetAlly.innate, targetAlly.volatiles[targetAlly.innate], targetAlly); + }, + }, +}; diff --git a/mods/pic/scripts.js b/mods/pic/scripts.js new file mode 100644 index 000000000000..2aed4151e824 --- /dev/null +++ b/mods/pic/scripts.js @@ -0,0 +1,62 @@ +'use strict'; + +exports.BattleScripts = { + getEffect: function (name) { + if (name && typeof name !== 'string') { + return name; + } + let id = toId(name); + if (id.startsWith('ability')) return Object.assign(Object.create(this.getAbility(id.slice(7))), {id}); + return Object.getPrototypeOf(this).getEffect.call(this, name); + }, + pokemon: { + setAbility: function (ability, source, isFromFormechange) { + if (!this.hp) return false; + ability = this.battle.getAbility(ability); + let oldAbility = this.ability; + if (!isFromFormechange) { + if (['illusion', 'battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange'].includes(ability.id)) return false; + if (['battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange'].includes(oldAbility)) return false; + } + this.battle.singleEvent('End', this.battle.getAbility(oldAbility), this.abilityData, this, source); + let ally = this.side.active.find(ally => ally && ally !== this && !ally.fainted); + if (ally && ally.innate) { + ally.removeVolatile(ally.innate); + delete ally.innate; + } + this.ability = ability.id; + this.abilityData = {id: ability.id, target: this}; + if (ability.id) { + this.battle.singleEvent('Start', ability, this.abilityData, this, source); + if (ally && ally.ability !== this.ability) { + ally.innate = 'ability' + ability.id; + ally.addVolatile(ally.innate); + } + } + this.abilityOrder = this.battle.abilityOrder++; + return oldAbility; + }, + hasAbility: function (ability) { + if (!this.ignoringAbility()) { + if (Array.isArray(ability) ? ability.map(toId).includes(this.ability) : toId(ability) === this.ability) { + return true; + } + } + let ally = this.side.active.find(ally => ally && ally !== this && !ally.fainted); + if (!ally || ally.ignoringAbility()) return false; + if (Array.isArray(ability)) return ability.map(toId).includes(ally.ability); + return toId(ability) === ally.ability; + }, + getRequestData: function () { + let ally = this.side.active.find(ally => ally && ally !== this && !ally.fainted); + this.moveSlots = this.baseMoveSlots.concat(ally ? ally.baseMoveSlots : []); + for (const moveSlot of this.moveSlots) { + moveSlot.disabled = false; + moveSlot.disabledSource = ''; + } + this.battle.runEvent('DisableMove', this); + if (!this.ateBerry) this.disableMove('belch'); + return Object.getPrototypeOf(this).getRequestData.call(this); + }, + }, +}; diff --git a/tsconfig.json b/tsconfig.json index 5aeb6ed83547..2efb39d603c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ }, "types": ["node"], "exclude": [ - "./mods/gen1/pokedex.js" + "./mods/gen1/pokedex.js", + "./mods/pic/*.js" ], "include": [ "./config/formats.js", From 02521f31fc5aec9df1e31bd7ae84ed7e21f0b538 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Sat, 1 Sep 2018 16:02:54 +0400 Subject: [PATCH 021/125] Update OM rotational ladder --- config/formats.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/formats.js b/config/formats.js index e9bc3dba0ad5..70ccb19eda76 100644 --- a/config/formats.js +++ b/config/formats.js @@ -761,7 +761,7 @@ let Formats = [ ], mod: 'gen7', - // searchShow: false, + searchShow: false, ruleset: ['[Gen 7] OU', 'STABmons Move Legality'], banlist: ['Aerodactyl-Mega', 'Blacephalon', 'Kartana', 'Komala', 'Kyurem-Black', 'Porygon-Z', 'Silvally', 'Tapu Koko', 'Tapu Lele', 'King\'s Rock', 'Razor Fang'], restrictedMoves: ['Acupressure', 'Belly Drum', 'Chatter', 'Extreme Speed', 'Geomancy', 'Lovely Kiss', 'Shell Smash', 'Shift Gear', 'Spore', 'Thousand Arrows'], @@ -774,7 +774,7 @@ let Formats = [ ], mod: 'gen7', - searchShow: false, + // searchShow: false, ruleset: ['[Gen 7] PU'], banlist: [ // PU From 3933cf5c7b418011d880db40808fa2e303906e8c Mon Sep 17 00:00:00 2001 From: Marty-D Date: Sat, 1 Sep 2018 09:31:45 -0400 Subject: [PATCH 022/125] Fix past gen Pursuit knockouts on the switch --- sim/pokemon.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sim/pokemon.js b/sim/pokemon.js index d8d8b1eaf0d1..69c36e9ef9d4 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -1557,11 +1557,10 @@ class Pokemon { * @param {Side | boolean} side */ getHealthInner(side) { + if (!this.hp) return '0 fnt'; let hpstring; // side === true in replays - if (!this.hp) { - hpstring = '0'; - } else if (side === this.side || side === true) { + if (side === this.side || side === true) { hpstring = '' + this.hp + '/' + this.maxhp; } else { let ratio = this.hp / this.maxhp; From 8936b8cc07474320ec0c8180439e9d1fa62a8fe4 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Sat, 1 Sep 2018 11:05:26 -0400 Subject: [PATCH 023/125] Gen II: Fix Snubbull event moves --- mods/gen2/learnsets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/gen2/learnsets.js b/mods/gen2/learnsets.js index 4721ea41438a..3a137e088d38 100644 --- a/mods/gen2/learnsets.js +++ b/mods/gen2/learnsets.js @@ -9949,6 +9949,7 @@ let BattleLearnsets = { icepunch: ["2M"], leer: ["2E"], lick: ["2L19", "2E"], + lovelykiss: ["2S0"], metronome: ["2E"], mudslap: ["2M"], present: ["2E"], @@ -9968,7 +9969,6 @@ let BattleLearnsets = { strength: ["2M"], sunnyday: ["2M"], swagger: ["2M"], - sweetkiss: ["2S0"], tackle: ["2L1", "2S0"], tailwhip: ["2L4", "2S0"], takedown: ["2L43"], From be94b72c3b796c544d7432c89095998291c5ee6f Mon Sep 17 00:00:00 2001 From: TheMezStrikes Date: Sat, 1 Sep 2018 17:20:04 +0200 Subject: [PATCH 024/125] Fix typo in gen61v1 desc (#4839) --- config/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/formats.js b/config/formats.js index 70ccb19eda76..80a57488cf16 100644 --- a/config/formats.js +++ b/config/formats.js @@ -997,7 +997,7 @@ let Formats = [ }, { name: "[Gen 6] 1v1", - desc: `Bring three Pokémon to Team Preview and choose one to battle."`, + desc: `Bring three Pokémon to Team Preview and choose one to battle.`, mod: 'gen6', teamLength: { From e5dcb1fa48f3918c24dcded9ed833184285b964f Mon Sep 17 00:00:00 2001 From: Kris Johnson <11083252+KrisXV@users.noreply.github.com> Date: Sat, 1 Sep 2018 12:20:18 -0600 Subject: [PATCH 025/125] TypeScript UNO (#4825) --- chat-plugins/uno.js | 408 +++++++++++++++++++++++++++++++------------- rooms.js | 1 + tsconfig.json | 1 + 3 files changed, 293 insertions(+), 117 deletions(-) diff --git a/chat-plugins/uno.js b/chat-plugins/uno.js index e3eef77235b5..04d79d478d93 100644 --- a/chat-plugins/uno.js +++ b/chat-plugins/uno.js @@ -29,6 +29,14 @@ const textColors = { const textShadow = 'text-shadow: 1px 0px black, -1px 0px black, 0px -1px black, 0px 1px black, 2px -2px black;'; +/** @typedef {'Green' | 'Yellow' | 'Red' | 'Blue' | 'Black'} Color */ +/** @typedef {{value: string, color: Color, changedColor?: Color, name: string}} Card */ + +/** + * @param {Card} card + * @param {boolean} fullsize + * @return {string} + */ function cardHTML(card, fullsize) { let surface = card.value.replace(/[^A-Z0-9+]/g, ""); let background = rgbGradients[card.color]; @@ -37,26 +45,55 @@ function cardHTML(card, fullsize) { return ``; } +/** + * @return {Card[]} + */ function createDeck() { + /** @type {Color[]} */ const colors = ['Red', 'Blue', 'Green', 'Yellow']; const values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'Reverse', 'Skip', '+2']; - let basic = []; + let basic = /** @type {Card[]} */ ([]); for (const color of colors) { basic.push(...values.map(v => { - return {value: v, color: color, name: color + " " + v}; + /** @type {Card} */ + let c = {value: v, color: color, name: color + " " + v}; + return c; })); } - return [...basic, ...basic, // two copies of the basic stuff (total 96) - ...[0, 1, 2, 3].map(v => ({color: colors[v], value: '0', name: colors[v] + ' 0'})), // the 4 0s - ...[0, 1, 2, 3].map(v => ({color: 'Black', value: 'Wild', name: 'Wild'})), // wild cards - ...[0, 1, 2, 3].map(v => ({color: 'Black', value: '+4', name: "Wild +4"})), // wild +4 cards + return [ + // two copies of the basic stuff (total 96) + ...basic, + ...basic, + // The four 0s + ...[0, 1, 2, 3].map(v => { + /** @type {Card} */ + let c = {color: colors[v], value: '0', name: colors[v] + ' 0'}; + return c; + }), + // Wild cards + ...[0, 1, 2, 3].map(v => { + /** @type {Card} */ + let c = {color: 'Black', value: 'Wild', name: 'Wild'}; + return c; + }), + // Wild +4 cards + ...[0, 1, 2, 3].map(v => { + /** @type {Card} */ + let c = {color: 'Black', value: '+4', name: 'Wild +4'}; + return c; + }), ]; // 108 cards } -class UNOgame extends Rooms.RoomGame { +class UnoGame extends Rooms.RoomGame { + /** + * @param {ChatRoom} room + * @param {number} cap + * @param {boolean} suppressMessages + */ constructor(room, cap, suppressMessages) { super(room); @@ -66,34 +103,45 @@ class UNOgame extends Rooms.RoomGame { room.gameNumber = 1; } - cap = parseInt(cap) || 6; - if (cap < 2) cap = 2; - + /** @type {number} */ this.playerCap = cap; this.allowRenames = true; + /** @type {number} */ this.maxTime = maxTime; + /** @type {NodeJS.Timer?} */ + this.timer = null; + /** @type {NodeJS.Timer?} */ this.autostartTimer = null; + /** @type {string} */ this.gameid = 'uno'; this.title = 'UNO'; + /** @type {string} */ this.state = 'signups'; - this.currentPlayer = null; + /** @type {string} */ + this.currentPlayerid = ''; + /** @type {Card[]} */ this.deck = Dex.shuffle(createDeck()); + /** @type {Card[]} */ this.discards = []; this.topCard = null; this.direction = 1; this.suppressMessages = suppressMessages || false; - this.spectators = {}; + this.spectators = Object.create(null); this.sendToRoom(`|uhtml|uno-${this.room.gameNumber}|

A new game of UNO is starting!


Or use /uno join to join the game.

${(this.suppressMessages ? `

Game messages will be shown to only players. If you would like to spectate the game, use /uno spectate

` : '')}
`, true); } onUpdateConnection() {} + /** + * @param {User} user + * @param {Connection} connection + */ onConnect(user, connection) { if (this.state === 'signups') { connection.sendTo(this.room, `|uhtml|uno-${this.room.gameNumber}|

A new game of UNO is starting!


Or use /uno join to join the game.

${(this.suppressMessages ? `

Game messages will be shown to only players. If you would like to spectate the game, use /uno spectate

` : '')}
`); @@ -102,6 +150,9 @@ class UNOgame extends Rooms.RoomGame { } } + /** + * @return {false | void} + */ onStart() { if (this.playerCount < 2) return false; if (this.autostartTimer) clearTimeout(this.autostartTimer); @@ -127,6 +178,10 @@ class UNOgame extends Rooms.RoomGame { this.nextTurn(true); } + /** + * @param {User} user + * @return {boolean} + */ joinGame(user) { if (this.state === 'signups' && this.addPlayer(user)) { this.sendToRoom(`${user.name} has joined the game of UNO.`); @@ -135,6 +190,10 @@ class UNOgame extends Rooms.RoomGame { return false; } + /** + * @param {User} user + * @return {boolean} + */ leaveGame(user) { if (this.state === 'signups' && this.removePlayer(user)) { this.sendToRoom(`${user.name} has left the game of UNO.`); @@ -143,11 +202,22 @@ class UNOgame extends Rooms.RoomGame { return false; } - // overwrite the default makePlayer so it makes a UNOgamePlayer instead. + /** + * Overwrite the default makePlayer so it makes an UnoGamePlayer instead. + * @param {User} user + * @return {UnoGamePlayer} + */ makePlayer(user) { - return new UNOgamePlayer(user, this); + return new UnoGamePlayer(user, this); } + /** + * @param {User} user + * @param {string} oldUserid + * @param {boolean} isJoining + * @param {boolean} isForceRenamed + * @return {false | void} + */ onRename(user, oldUserid, isJoining, isForceRenamed) { if (!(oldUserid in this.players) || user.userid === oldUserid) return false; if (!user.named && !isForceRenamed) { @@ -163,39 +233,47 @@ class UNOgame extends Rooms.RoomGame { this.players[user.userid].userid = user.userid; if (this.awaitUno && this.awaitUno === oldUserid) this.awaitUno = user.userid; - if (this.currentPlayer && this.currentPlayer === oldUserid) this.currentPlayer = user.userid; + if (this.currentPlayerid === oldUserid) this.currentPlayerid = user.userid; } + /** + * @param {string} userid + * @return {string | false} + */ eliminate(userid) { if (!(userid in this.players)) return false; let name = this.players[userid].name; if (this.playerCount === 2) { - this.removePlayer({userid: userid}); + this.removePlayer(this.players[userid]); this.onWin(this.players[Object.keys(this.players)[0]]); return name; } // handle current player... - if (userid === this.currentPlayer) { + if (userid === this.currentPlayerid) { if (this.state === 'color') { this.topCard.changedColor = this.discards[1].changedColor || this.discards[1].color; this.sendToRoom(`|raw|${Chat.escapeHTML(name)} has not picked a color, the color will stay as ${this.topCard.changedColor}.`); } - clearTimeout(this.timer); + if (this.timer) clearTimeout(this.timer); this.nextTurn(); } // put that player's cards into the discard pile to prevent cards from being permanently lost this.discards.push(...this.players[userid].hand); - this.removePlayer({userid: userid}); + this.removePlayer(this.players[userid]); return name; } - sendToRoom(msg, overrideSuppress) { + /** + * @param {string} msg + * @param {boolean} [overrideSuppress] + */ + sendToRoom(msg, overrideSuppress = false) { if (!this.suppressMessages || overrideSuppress) { this.room.add(msg).update(); } else { @@ -213,15 +291,22 @@ class UNOgame extends Rooms.RoomGame { } } + /** + * @param {boolean} [showCards] + * @return {string[]} + */ getPlayers(showCards) { let playerList = Object.keys(this.players); if (!showCards) { return playerList.sort().map(id => Chat.escapeHTML(this.players[id].name)); } if (this.direction === -1) playerList = playerList.reverse(); - return playerList.map(id => `${(this.currentPlayer && this.currentPlayer === id ? '' : '')}${Chat.escapeHTML(this.players[id].name)} (${this.players[id].hand.length}) ${(this.currentPlayer && this.currentPlayer === id ? '' : "")}`); + return playerList.map(id => `${(this.currentPlayerid === id ? '' : '')}${Chat.escapeHTML(this.players[id].name)} (${this.players[id].hand.length}) ${(this.currentPlayerid === id ? '' : "")}`); } + /** + * @return {Promise} + */ onAwaitUno() { return new Promise((resolve, reject) => { if (!this.awaitUno) return resolve(); @@ -237,13 +322,16 @@ class UNOgame extends Rooms.RoomGame { }); } + /** + * @param {boolean} [starting] + */ nextTurn(starting) { this.onAwaitUno() .then(() => { if (!starting) this.onNextPlayer(); - clearTimeout(this.timer); - let player = this.players[this.currentPlayer]; + if (this.timer) clearTimeout(this.timer); + let player = this.players[this.currentPlayerid]; this.sendToRoom(`|c:|${(Math.floor(Date.now() / 1000))}|~|${player.name}'s turn.`); this.state = 'play'; @@ -252,25 +340,28 @@ class UNOgame extends Rooms.RoomGame { this.timer = setTimeout(() => { this.sendToRoom(`${player.name} has been automatically disqualified.`); - this.eliminate(this.currentPlayer); + this.eliminate(this.currentPlayerid); }, this.maxTime * 1000); }); } onNextPlayer() { // if none is set - if (!this.currentPlayer) { + if (!this.currentPlayerid) { let userList = Object.keys(this.players); - this.currentPlayer = userList[Math.floor(this.playerCount * Math.random())]; + this.currentPlayerid = userList[Math.floor(this.playerCount * Math.random())]; } - this.currentPlayer = this.getNextPlayer(); + this.currentPlayerid = this.getNextPlayer(); } + /** + * @return {string} + */ getNextPlayer() { let userList = Object.keys(this.players); - let player = userList[(userList.indexOf(this.currentPlayer) + this.direction)]; + let player = userList[(userList.indexOf(this.currentPlayerid) + this.direction)]; if (!player) { player = this.direction === 1 ? userList[0] : userList[this.playerCount - 1]; @@ -278,8 +369,12 @@ class UNOgame extends Rooms.RoomGame { return player; } + /** + * @param {UnoGamePlayer} user + * @return {boolean | void} + */ onDraw(user) { - if (this.currentPlayer !== user.userid || this.state !== 'play') return false; + if (this.currentPlayerid !== user.userid || this.state !== 'play') return false; if (this.players[user.userid].cardLock) return true; this.onCheckUno(); @@ -287,13 +382,18 @@ class UNOgame extends Rooms.RoomGame { this.sendToRoom(`${user.name} has drawn a card.`); let player = this.players[user.userid]; - let card = this.onDrawCard(user, 1, true); + let card = this.onDrawCard(user, 1); player.sendDisplay(); player.cardLock = card[0].name; } + /** + * @param {UnoGamePlayer} user + * @param {string} cardName + * @return {false | string | void} + */ onPlay(user, cardName) { - if (this.currentPlayer !== user.userid || this.state !== 'play') return false; + if (this.currentPlayerid !== user.userid || this.state !== 'play') return false; let player = this.players[user.userid]; let card = player.hasCard(cardName); @@ -304,7 +404,7 @@ class UNOgame extends Rooms.RoomGame { if (card.color !== 'Black' && card.color !== (this.topCard.changedColor || this.topCard.color) && card.value !== this.topCard.value) return `You cannot play this card - you can only play: Wild cards, ${(this.topCard.changedColor ? 'and' : '')} ${(this.topCard.changedColor || this.topCard.color)} cards${this.topCard.changedColor ? "" : ` and ${this.topCard.value}'s`}.`; if (card.value === '+4' && !player.canPlayWildFour()) return "You cannot play Wild +4 when you still have a card with the same color as the top card."; - clearTimeout(this.timer); // reset the autodq timer. + if (this.timer) clearTimeout(this.timer); // reset the autodq timer. this.onCheckUno(); @@ -334,6 +434,10 @@ class UNOgame extends Rooms.RoomGame { if (this.state === 'play') this.nextTurn(); } + /** + * @param {'Reverse' | 'Skip' | 'Wild' | '+2' | '+4'} value + * @param {boolean} [initialize] + */ onRunEffect(value, initialize) { const colorDisplay = `|uhtml|uno-hand|
`; @@ -345,43 +449,48 @@ class UNOgame extends Rooms.RoomGame { break; case 'Skip': this.onNextPlayer(); - this.sendToRoom(this.players[this.currentPlayer].name + "'s turn has been skipped."); + this.sendToRoom(this.players[this.currentPlayerid].name + "'s turn has been skipped."); break; case '+2': this.onNextPlayer(); - this.sendToRoom(this.players[this.currentPlayer].name + " has been forced to draw 2 cards."); - this.onDrawCard({userid: this.currentPlayer}, 2); + this.sendToRoom(this.players[this.currentPlayerid].name + " has been forced to draw 2 cards."); + this.onDrawCard(this.players[this.currentPlayerid], 2); break; case '+4': - this.players[this.currentPlayer].sendRoom(colorDisplay); + this.players[this.currentPlayerid].sendRoom(colorDisplay); this.state = 'color'; // apply to the next in line, since the current player still has to choose the color let next = this.getNextPlayer(); this.sendToRoom(this.players[next].name + " has been forced to draw 4 cards."); - this.onDrawCard({userid: next}, 4); + this.onDrawCard(this.players[this.getNextPlayer()], 4); this.isPlusFour = true; this.timer = setTimeout(() => { - this.sendToRoom(`${this.players[this.currentPlayer].name} has been automatically disqualified.`); - this.eliminate(this.currentPlayer); + this.sendToRoom(`${this.players[this.currentPlayerid].name} has been automatically disqualified.`); + this.eliminate(this.currentPlayerid); }, this.maxTime * 1000); break; case 'Wild': - this.players[this.currentPlayer].sendRoom(colorDisplay); + this.players[this.currentPlayerid].sendRoom(colorDisplay); this.state = 'color'; this.timer = setTimeout(() => { - this.sendToRoom(`${this.players[this.currentPlayer].name} has been automatically disqualified.`); - this.eliminate(this.currentPlayer); + this.sendToRoom(`${this.players[this.currentPlayerid].name} has been automatically disqualified.`); + this.eliminate(this.currentPlayerid); }, this.maxTime * 1000); break; } if (initialize) this.onNextPlayer(); } - onSelectcolor(user, color) { - if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || user.userid !== this.currentPlayer || this.state !== 'color') return false; + /** + * @param {UnoGamePlayer} user + * @param {Color | string} color + * @return {false | void} + */ + onSelectColor(user, color) { + if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || user.userid !== this.currentPlayerid || this.state !== 'color') return false; this.topCard.changedColor = color; this.sendToRoom(`The color has been changed to ${color}.`); - clearTimeout(this.timer); + if (this.timer) clearTimeout(this.timer); // send the display of their cards again this.players[user.userid].sendDisplay(); @@ -394,9 +503,15 @@ class UNOgame extends Rooms.RoomGame { this.nextTurn(); } + /** + * @param {UnoGamePlayer} user + * @param {number} count + * @return {Card[]} + */ onDrawCard(user, count) { - if (!(user.userid in this.players)) return false; - let drawnCards = this.drawCard(count); + if (typeof count === 'string') count = parseInt(count); + if (!count || isNaN(count) || count < 1) count = 1; + let drawnCards = /** @type {Card[]} */ (this.drawCard(count)); let player = this.players[user.userid]; player.hand.push(...drawnCards); @@ -404,21 +519,31 @@ class UNOgame extends Rooms.RoomGame { return drawnCards; } + /** + * @param {number} count + * @return {Card[]} + */ drawCard(count) { - count = parseInt(count); - if (!count || count < 1) count = 1; - let drawnCards = []; + if (typeof count === 'string') count = parseInt(count); + if (!count || isNaN(count) || count < 1) count = 1; + let drawnCards = /** @type {Card[]} */ ([]); for (let i = 0; i < count; i++) { if (!this.deck.length) { this.deck = this.discards.length ? Dex.shuffle(this.discards) : Dex.shuffle(createDeck()); // shuffle the cards back into the deck, or if there are no discards, add another deck into the game. this.discards = []; // clear discard pile } - drawnCards.push(this.deck.pop()); + drawnCards.push(this.deck[this.deck.length - 1]); + this.deck.pop(); } return drawnCards; } + /** + * @param {UnoGamePlayer} user + * @param {string} unoId + * @return {false | void} + */ onUno(user, unoId) { // uno id makes spamming /uno uno impossible if (this.unoId !== unoId || user.userid !== this.awaitUno) return false; @@ -430,28 +555,35 @@ class UNOgame extends Rooms.RoomGame { onCheckUno() { if (this.awaitUno) { // if the previous player hasn't hit UNO before the next player plays something, they are forced to draw 2 cards; - if (this.awaitUno !== this.currentPlayer) { + if (this.awaitUno !== this.currentPlayerid) { this.sendToRoom(`${this.players[this.awaitUno].name} forgot to say UNO! and is forced to draw 2 cards.`); - this.onDrawCard({userid: this.awaitUno}, 2); + this.onDrawCard(this.players[this.awaitUno], 2); } delete this.awaitUno; delete this.unoId; } } + /** + * @param {UnoGamePlayer | User} user + * @return {false | void} + */ onSendHand(user) { if (!(user.userid in this.players) || this.state === 'signups') return false; this.players[user.userid].sendDisplay(); } + /** + * @param {UnoGamePlayer} player + */ onWin(player) { this.sendToRoom(Chat.html`|raw|
Congratulations to ${player.name} for winning the game of UNO!
`, true); this.destroy(); } destroy() { - clearTimeout(this.timer); + if (this.timer) clearTimeout(this.timer); if (this.autostartTimer) clearTimeout(this.autostartTimer); this.sendToRoom(`|uhtmlchange|uno-${this.room.gameNumber}|
The game of UNO has ended.
`, true); @@ -463,12 +595,20 @@ class UNOgame extends Rooms.RoomGame { } } -class UNOgamePlayer extends Rooms.RoomGamePlayer { +class UnoGamePlayer extends Rooms.RoomGamePlayer { + /** + * @param {User} user + * @param {UnoGame} game + */ constructor(user, game) { super(user, game); - this.hand = []; + this.hand = /** @type {Card[]} */ ([]); + this.game = game; } + /** + * @return {boolean} + */ canPlayWildFour() { let color = (this.game.topCard.changedColor || this.game.topCard.color); @@ -476,10 +616,16 @@ class UNOgamePlayer extends Rooms.RoomGamePlayer { return true; } + /** + * @param {string} cardName + */ hasCard(cardName) { - return this.hand.find(c => c.name === cardName); + return this.hand.find(card => card.name === cardName); } + /** + * @param {string} cardName + */ removeCard(cardName) { for (const [i, card] of this.hand.entries()) { if (card.name === cardName) { @@ -489,6 +635,9 @@ class UNOgamePlayer extends Rooms.RoomGamePlayer { } } + /** + * @return {string[]} + */ buildHand() { return this.hand.sort((a, b) => a.color.localeCompare(b.color) || a.value.localeCompare(b.value)) .map((c, i) => cardHTML(c, i === this.hand.length - 1)); @@ -506,14 +655,18 @@ class UNOgamePlayer extends Rooms.RoomGamePlayer { // clear previous display and show new display this.sendRoom("|uhtmlchange|uno-hand|"); this.sendRoom( - `|uhtml|uno-hand|
${this.game.currentPlayer === this.userid ? `` : ""}` + + `|uhtml|uno-hand|
${hand}
${top}
${this.game.currentPlayerid === this.userid ? `` : ""}` + `
${hand}
${top}
${players}
` + - `${this.game.currentPlayer === this.userid ? `
${draw}${pass}
${uno}
` : ""}
` + `${this.game.currentPlayerid === this.userid ? `
${draw}${pass}
${uno}
` : ""}` ); } } -exports.commands = { +/** @typedef {(this: CommandContext, target: string, room: ChatRoom, user: User, connection: Connection, cmd: string, message: string) => (void)} ChatHandler */ +/** @typedef {{[k: string]: { [k: string]: ChatHandler | string | true | string[] | ChatCommands} | string[]}} ChatCommands */ + +/** @type {ChatCommands} */ +const commands = { uno: { // roomowner commands off: 'disable', @@ -558,15 +711,19 @@ exports.commands = { let suppressMessages = cmd.includes('private') || !(cmd.includes('public') || room.id === 'gamecorner'); - room.game = new UNOgame(room, target, suppressMessages); + let cap = parseInt(target); + if (isNaN(cap)) cap = 6; + if (cap < 2) cap = 2; + room.game = new UnoGame(room, cap, suppressMessages); this.privateModAction(`(A game of UNO was created by ${user.name}.)`); this.modlog('UNO CREATE'); }, start: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno' || room.game.state !== 'signups') return this.errorReply("There is no UNO game in signups phase in this room."); - if (room.game.onStart()) { + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno' || game.state !== 'signups') return this.errorReply("There is no UNO game in signups phase in this room."); + if (game.onStart()) { this.privateModAction(`(The game of UNO was started by ${user.name}.)`); this.modlog('UNO START'); } @@ -584,36 +741,36 @@ exports.commands = { timer: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room."); let amount = parseInt(target); if (!amount || amount < 5 || amount > 300) return this.errorReply("The amount must be a number between 5 and 300."); - room.game.maxTime = amount; - if (room.game.timer) { - clearTimeout(room.game.timer); - room.game.timer = setTimeout(() => { - room.game.eliminate(room.game.currentPlayer); - }, amount * 1000); - } + game.maxTime = amount; + if (game.timer) clearTimeout(game.timer); + game.timer = setTimeout(() => { + game.eliminate(game.currentPlayerid); + }, amount * 1000); this.addModAction(`${user.name} has set the UNO automatic disqualification timer to ${amount} seconds.`); this.modlog('UNO TIMER', null, `${amount} seconds`); }, autostart: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (toId(target) === 'off') { - if (!room.game.autostartTimer) return this.errorReply("There is no autostart timer running on."); + if (!game.autostartTimer) return this.errorReply("There is no autostart timer running on."); this.addModAction(`${user.name} has turned off the UNO autostart timer.`); - clearTimeout(room.game.autostartTimer); + clearTimeout(game.autostartTimer); return; } const amount = parseInt(target); if (!amount || amount < 30 || amount > 600) return this.errorReply("The amount must be a number between 30 and 600 seconds."); - if (room.game.state !== 'signups') return this.errorReply("The game of UNO has already started."); - if (room.game.autostartTimer) clearTimeout(room.game.autostartTimer); - room.game.autostartTimer = setTimeout(() => { - room.game.onStart(); + if (game.state !== 'signups') return this.errorReply("The game of UNO has already started."); + if (game.autostartTimer) clearTimeout(game.autostartTimer); + game.autostartTimer = setTimeout(() => { + game.onStart(); }, amount * 1000); this.addModAction(`${user.name} has set the UNO autostart timer to ${amount} seconds.`); }, @@ -621,9 +778,10 @@ exports.commands = { dq: 'disqualify', disqualify: function (target, room, user) { if (!this.can('minigame', null, room)) return; - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let disqualified = room.game.eliminate(toId(target)); + let disqualified = game.eliminate(toId(target)); if (disqualified === false) return this.errorReply(`Unable to disqualify ${target}.`); this.privateModAction(`(${user.name} has disqualified ${disqualified} from the UNO game.)`); this.modlog('UNO DQ', toId(target)); @@ -631,68 +789,79 @@ exports.commands = { }, // player/user commands - j: 'join', - join: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + j: 'unojoin', + // TypeScript doesn't like 'join' being defined as a function + join: 'unojoin', + unojoin: function (target, room, user) { + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.canTalk()) return false; - if (!room.game.joinGame(user)) return this.errorReply("Unable to join the game."); + if (!game.joinGame(user)) return this.errorReply("Unable to join the game."); return this.sendReply("You have joined the game of UNO."); }, l: 'leave', leave: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (!room.game.leaveGame(user)) return this.errorReply("Unable to leave the game."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + if (!game.leaveGame(user)) return this.errorReply("Unable to leave the game."); return this.sendReply("You have left the game of UNO."); }, play: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let error = room.game.onPlay(user, target); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + let error = game.onPlay(game.players[user.userid], target); if (error) this.errorReply(error); }, draw: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let error = room.game.onDraw(user); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + let error = game.onDraw(game.players[user.userid]); if (error) return this.errorReply("You have already drawn a card this turn."); }, pass: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (room.game.currentPlayer !== user.userid) return this.errorReply("It is currently not your turn."); - if (!room.game.players[user.userid].cardLock) return this.errorReply("You cannot pass until you draw a card."); - if (room.game.state === 'color') return this.errorReply("You cannot pass until you choose a color."); - - room.game.sendToRoom(`${user.name} has passed.`); - room.game.nextTurn(); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + if (game.currentPlayerid !== user.userid) return this.errorReply("It is currently not your turn."); + if (!game.players[user.userid].cardLock) return this.errorReply("You cannot pass until you draw a card."); + if (game.state === 'color') return this.errorReply("You cannot pass until you choose a color."); + + game.sendToRoom(`${user.name} has passed.`); + game.nextTurn(); }, color: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return false; - room.game.onSelectcolor(user, target); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return false; + game.onSelectColor(game.players[user.userid], target); }, uno: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return false; - room.game.onUno(user, target); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return false; + game.onUno(game.players[user.userid], target); }, // information commands '': 'hand', hand: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.parse("/help uno"); - room.game.onSendHand(user); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.parse("/help uno"); + game.onSendHand(user); }, players: 'getusers', users: 'getusers', getplayers: 'getusers', getusers: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.runBroadcast()) return false; - this.sendReplyBox(`Players (${room.game.playerCount}):
${room.game.getPlayers().join(', ')}`); + this.sendReplyBox(`Players (${game.playerCount}):
${game.getPlayers().join(', ')}`); }, help: function (target, room, user) { @@ -701,38 +870,41 @@ exports.commands = { // suppression commands suppress: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.can('minigame', null, room)) return; target = toId(target); let state = target === 'on' ? true : target === 'off' ? false : undefined; - if (state === undefined) return this.sendReply(`Suppression of UNO game messages is currently ${(room.game.suppressMessages ? 'on' : 'off')}.`); - if (state === room.game.suppressMessages) return this.errorReply(`Suppression of UNO game messages is already ${(room.game.suppressMessages ? 'on' : 'off')}.`); + if (state === undefined) return this.sendReply(`Suppression of UNO game messages is currently ${(game.suppressMessages ? 'on' : 'off')}.`); + if (state === game.suppressMessages) return this.errorReply(`Suppression of UNO game messages is already ${(game.suppressMessages ? 'on' : 'off')}.`); - room.game.suppressMessages = state; + game.suppressMessages = state; this.addModAction(`${user.name} has turned ${(state ? 'on' : 'off')} suppression of UNO game messages.`); this.modlog('UNO SUPRESS', null, (state ? 'ON' : 'OFF')); }, spectate: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (!room.game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); - if (user.userid in room.game.spectators) return this.errorReply("You are already spectating this game."); + if (!game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); + if (user.userid in game.spectators) return this.errorReply("You are already spectating this game."); - room.game.spectators[user.userid] = 1; + game.spectators[user.userid] = 1; this.sendReply("You are now spectating this private UNO game."); }, unspectate: function (target, room, user) { - if (!room.game || room.game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); + const game = /** @type {UnoGame} */ (room.game); + if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - if (!room.game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); - if (!(user.userid in room.game.spectators)) return this.errorReply("You are currently not spectating this game."); + if (!game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); + if (!(user.userid in game.spectators)) return this.errorReply("You are currently not spectating this game."); - delete room.game.spectators[user.userid]; + delete game.spectators[user.userid]; this.sendReply("You are no longer spectating this private UNO game."); }, }, @@ -750,3 +922,5 @@ exports.commands = { `/uno suppress [on|off] - Toggles suppression of game messages.`, ], }; + +exports.commands = commands; diff --git a/rooms.js b/rooms.js index 5ec9504ef382..3f45d0bf6054 100644 --- a/rooms.js +++ b/rooms.js @@ -107,6 +107,7 @@ class BasicRoom { this.filterEmojis = false; this.filterCaps = false; this.mafiaEnabled = false; + this.unoDisabled = false; /** @type {Set?} */ this.privacySetter = null; /** @type {Map?} */ diff --git a/tsconfig.json b/tsconfig.json index 2efb39d603c8..3fff4f51f99e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,6 +47,7 @@ "./chat-plugins/poll.js", "./chat-plugins/room-faqs.js", "./chat-plugins/thing-of-the-day.js", + "./chat-plugins/uno.js", "./chat-plugins/wifi.js", "./punishments.js", "./tournaments/generator-round-robin.js" From c7ec922c974fc0bfe7b929a71ac7b2551df487d0 Mon Sep 17 00:00:00 2001 From: Quinton Lee Date: Sat, 1 Sep 2018 23:15:04 -0500 Subject: [PATCH 026/125] Support staff-only HTML boxes (#4840) Also updated relevant help commands. --- chat-plugins/info.js | 44 ++++++++++++++++++++++++++++++++++---------- chat.js | 6 ++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index ed8cb5a92971..b613a53531e4 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -2169,7 +2169,7 @@ const commands = { }, showimagehelp: [`/showimage [url], [width], [height] - Show an image. Any CSS units may be used for the width or height (default: px). Requires: # & ~`], - htmlbox: function (target, room, user, connection, cmd, message) { + htmlbox: function (target, room, user) { if (!target) return this.parse('/help htmlbox'); target = this.canHTML(target); if (!target) return; @@ -2181,8 +2181,13 @@ const commands = { this.sendReplyBox(target); }, - addhtmlbox: function (target, room, user, connection, cmd, message) { - if (!target) return this.parse('/help htmlbox'); + htmlboxhelp: [ + `/htmlbox [message] - Displays a message, parsing HTML code contained.`, + `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, + ], + addmodhtmlbox: 'addhtmlbox', + addhtmlbox: function (target, room, user, connection, cmd) { + if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; target = this.canHTML(target); if (!target) return; @@ -2192,12 +2197,20 @@ const commands = { target += Chat.html`
[${user.name}]
`; } - this.addBox(target); + if (cmd === 'addmodhtmlbox') { + this.addModBox(target); + } else { + this.addBox(target); + } }, - htmlboxhelp: [ - `/htmlbox [message] - Displays a message, parsing HTML code contained.`, - `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, + addhtmlboxhelp: [ + `/addhtmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, + ], + addmodhtmlboxhelp: [ + `/addmodhtmlbox [message] - Shows staff a message, parsing HTML code contained. Requires: ~ & #`, ], + changemoduhtml: 'adduhtml', + addmoduhtml: 'adduhtml', changeuhtml: 'adduhtml', adduhtml: function (target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); @@ -2213,13 +2226,24 @@ const commands = { html += Chat.html`
[${user.name}]
`; } - this.add(`|uhtml${(cmd === 'changeuhtml' ? 'change' : '')}|${name}|${html}`); + html = `|uhtml${(cmd === 'changeuhtml' || cmd === 'changemoduhtml' ? 'change' : '')}|${name}|${html}`; + if (cmd === 'addmoduhtml' || cmd === 'changemoduhtml') { + this.room.sendMods(html); + } else { + this.add(html); + } }, adduhtmlhelp: [ - `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained.`, + `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained. Requires: ~ & #`, ], changeuhtmlhelp: [ - `/changeuhtml [name], [message] - Changes a message previously shown with /adduhtml`, + `/changeuhtml [name], [message] - Changes the message previously shown with /adduhtml [name]. Requires: ~ & #`, + ], + addmoduhtmlhelp: [ + `/addmoduhtml [name], [message] - Shows staff a message that can change, parsing HTML code contained. Requires: ~ & #`, + ], + changemoduhtmlhelp: [ + `/changemoduhtml [name], [message] - Changes the staff message previously shown with /addmoduhtml [name]. Requires: ~ & #`, ], }; diff --git a/chat.js b/chat.js index 1449280685e0..b458476f247c 100644 --- a/chat.js +++ b/chat.js @@ -569,6 +569,12 @@ class CommandContext { addBox(html) { this.add(`|html|
${html}
`); } + /** + * @param {string} html + */ + addModBox(html) { + this.room.sendMods(`|html|
${html}
`); + } /** * @param {string} html */ From 61be74755737fa24dc332787404c806ff17303ed Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Sun, 2 Sep 2018 13:55:15 -0400 Subject: [PATCH 027/125] Uno: Improve Typescript --- chat-plugins/uno.js | 105 ++++++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/chat-plugins/uno.js b/chat-plugins/uno.js index 04d79d478d93..c4636db5a52a 100644 --- a/chat-plugins/uno.js +++ b/chat-plugins/uno.js @@ -126,6 +126,7 @@ class UnoGame extends Rooms.RoomGame { this.deck = Dex.shuffle(createDeck()); /** @type {Card[]} */ this.discards = []; + /** @type {Card?} */ this.topCard = null; this.direction = 1; @@ -254,6 +255,10 @@ class UnoGame extends Rooms.RoomGame { // handle current player... if (userid === this.currentPlayerid) { if (this.state === 'color') { + if (!this.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } this.topCard.changedColor = this.discards[1].changedColor || this.discards[1].color; this.sendToRoom(`|raw|${Chat.escapeHTML(name)} has not picked a color, the color will stay as ${this.topCard.changedColor}.`); } @@ -370,36 +375,38 @@ class UnoGame extends Rooms.RoomGame { } /** - * @param {UnoGamePlayer} user + * @param {UnoGamePlayer} player * @return {boolean | void} */ - onDraw(user) { - if (this.currentPlayerid !== user.userid || this.state !== 'play') return false; - if (this.players[user.userid].cardLock) return true; + onDraw(player) { + if (this.currentPlayerid !== player.userid || this.state !== 'play') return false; + if (player.cardLock) return true; this.onCheckUno(); - this.sendToRoom(`${user.name} has drawn a card.`); - let player = this.players[user.userid]; + this.sendToRoom(`${player.name} has drawn a card.`); - let card = this.onDrawCard(user, 1); + let card = this.onDrawCard(player, 1); player.sendDisplay(); player.cardLock = card[0].name; } /** - * @param {UnoGamePlayer} user + * @param {UnoGamePlayer} player * @param {string} cardName * @return {false | string | void} */ - onPlay(user, cardName) { - if (this.currentPlayerid !== user.userid || this.state !== 'play') return false; - let player = this.players[user.userid]; + onPlay(player, cardName) { + if (this.currentPlayerid !== player.userid || this.state !== 'play') return false; let card = player.hasCard(cardName); if (!card) return "You do not have that card."; // check for legal play + if (!this.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } if (player.cardLock && player.cardLock !== cardName) return `You can only play ${player.cardLock} after drawing.`; if (card.color !== 'Black' && card.color !== (this.topCard.changedColor || this.topCard.color) && card.value !== this.topCard.value) return `You cannot play this card - you can only play: Wild cards, ${(this.topCard.changedColor ? 'and' : '')} ${(this.topCard.changedColor || this.topCard.color)} cards${this.topCard.changedColor ? "" : ` and ${this.topCard.value}'s`}.`; if (card.value === '+4' && !player.canPlayWildFour()) return "You cannot play Wild +4 when you still have a card with the same color as the top card."; @@ -415,7 +422,7 @@ class UnoGame extends Rooms.RoomGame { // update the unoId here, so when the display is sent to the player when the play is made if (player.hand.length === 1) { - this.awaitUno = user.userid; + this.awaitUno = player.userid; this.unoId = Math.floor(Math.random() * 100).toString(); } @@ -435,7 +442,7 @@ class UnoGame extends Rooms.RoomGame { } /** - * @param {'Reverse' | 'Skip' | 'Wild' | '+2' | '+4'} value + * @param {string} value * @param {boolean} [initialize] */ onRunEffect(value, initialize) { @@ -462,7 +469,7 @@ class UnoGame extends Rooms.RoomGame { // apply to the next in line, since the current player still has to choose the color let next = this.getNextPlayer(); this.sendToRoom(this.players[next].name + " has been forced to draw 4 cards."); - this.onDrawCard(this.players[this.getNextPlayer()], 4); + this.onDrawCard(this.players[next], 4); this.isPlusFour = true; this.timer = setTimeout(() => { this.sendToRoom(`${this.players[this.currentPlayerid].name} has been automatically disqualified.`); @@ -482,18 +489,22 @@ class UnoGame extends Rooms.RoomGame { } /** - * @param {UnoGamePlayer} user - * @param {Color | string} color + * @param {UnoGamePlayer} player + * @param {Color} color * @return {false | void} */ - onSelectColor(user, color) { - if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || user.userid !== this.currentPlayerid || this.state !== 'color') return false; + onSelectColor(player, color) { + if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || player.userid !== this.currentPlayerid || this.state !== 'color') return false; + if (!this.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } this.topCard.changedColor = color; this.sendToRoom(`The color has been changed to ${color}.`); if (this.timer) clearTimeout(this.timer); // send the display of their cards again - this.players[user.userid].sendDisplay(); + player.sendDisplay(); if (this.isPlusFour) { this.isPlusFour = false; @@ -504,16 +515,15 @@ class UnoGame extends Rooms.RoomGame { } /** - * @param {UnoGamePlayer} user + * @param {UnoGamePlayer} player * @param {number} count * @return {Card[]} */ - onDrawCard(user, count) { + onDrawCard(player, count) { if (typeof count === 'string') count = parseInt(count); if (!count || isNaN(count) || count < 1) count = 1; let drawnCards = /** @type {Card[]} */ (this.drawCard(count)); - let player = this.players[user.userid]; player.hand.push(...drawnCards); player.sendRoom(`|raw|You have drawn the following card${Chat.plural(drawnCards)}: ${drawnCards.map(card => `${card.name}`).join(', ')}.`); return drawnCards; @@ -540,14 +550,14 @@ class UnoGame extends Rooms.RoomGame { } /** - * @param {UnoGamePlayer} user + * @param {UnoGamePlayer} player * @param {string} unoId * @return {false | void} */ - onUno(user, unoId) { + onUno(player, unoId) { // uno id makes spamming /uno uno impossible - if (this.unoId !== unoId || user.userid !== this.awaitUno) return false; - this.sendToRoom(Chat.html`|raw|UNO! ${user.name} is down to their last card!`); + if (this.unoId !== unoId || player.userid !== this.awaitUno) return false; + this.sendToRoom(Chat.html`|raw|UNO! ${player.name} is down to their last card!`); delete this.awaitUno; delete this.unoId; } @@ -565,7 +575,7 @@ class UnoGame extends Rooms.RoomGame { } /** - * @param {UnoGamePlayer | User} user + * @param {User} user * @return {false | void} */ onSendHand(user) { @@ -604,12 +614,17 @@ class UnoGamePlayer extends Rooms.RoomGamePlayer { super(user, game); this.hand = /** @type {Card[]} */ ([]); this.game = game; + this.cardLock = ''; } /** * @return {boolean} */ canPlayWildFour() { + if (!this.game.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } let color = (this.game.topCard.changedColor || this.game.topCard.color); if (this.hand.some(c => c.color === color)) return false; @@ -650,6 +665,10 @@ class UnoGamePlayer extends Rooms.RoomGamePlayer { let pass = ''; let uno = ``; + if (!this.game.topCard) { + // should never happen + throw new Error(`No top card in the discard pile.`); + } let top = `Top Card: ${this.game.topCard.name}`; // clear previous display and show new display @@ -812,14 +831,20 @@ const commands = { play: function (target, room, user) { const game = /** @type {UnoGame} */ (room.game); if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let error = game.onPlay(game.players[user.userid], target); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + let error = game.onPlay(player, target); if (error) this.errorReply(error); }, draw: function (target, room, user) { const game = /** @type {UnoGame} */ (room.game); if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let error = game.onDraw(game.players[user.userid]); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + let error = game.onDraw(player); if (error) return this.errorReply("You have already drawn a card this turn."); }, @@ -827,7 +852,10 @@ const commands = { const game = /** @type {UnoGame} */ (room.game); if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (game.currentPlayerid !== user.userid) return this.errorReply("It is currently not your turn."); - if (!game.players[user.userid].cardLock) return this.errorReply("You cannot pass until you draw a card."); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + if (!player.cardLock) return this.errorReply("You cannot pass until you draw a card."); if (game.state === 'color') return this.errorReply("You cannot pass until you choose a color."); game.sendToRoom(`${user.name} has passed.`); @@ -837,13 +865,26 @@ const commands = { color: function (target, room, user) { const game = /** @type {UnoGame} */ (room.game); if (!game || game.gameid !== 'uno') return false; - game.onSelectColor(game.players[user.userid], target); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + /** @type {Color} */ + let color; + if (target === 'Red' || target === 'Green' || target === 'Blue' || target === 'Yellow' || target === 'Black') { + color = target; + } else { + return this.errorReply(`"${target}" is not a valid color.`); + } + game.onSelectColor(player, color); }, uno: function (target, room, user) { const game = /** @type {UnoGame} */ (room.game); if (!game || game.gameid !== 'uno') return false; - game.onUno(game.players[user.userid], target); + /** @type {UnoGamePlayer | undefined} */ + let player = game.players[user.userid]; + if (!player) return this.errorReply(`You are not in the game of UNO.`); + game.onUno(player, target); }, // information commands From ce8dd999091852741c465871433efa90c35028f1 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Sun, 2 Sep 2018 16:19:22 -0400 Subject: [PATCH 028/125] Learnsets: Lotad can't learn Dive --- data/learnsets.js | 1 - 1 file changed, 1 deletion(-) diff --git a/data/learnsets.js b/data/learnsets.js index 4e697634fa95..b8779012c094 100644 --- a/data/learnsets.js +++ b/data/learnsets.js @@ -25508,7 +25508,6 @@ let BattleLearnsets = { captivate: ["4M"], confide: ["7M", "6M"], counter: ["7E", "6E", "5E", "4E"], - dive: ["6M"], doubleedge: ["3T"], doubleteam: ["7M", "6M", "5M", "4M", "3M"], echoedvoice: ["7M", "6M", "5M"], From 8d582a4b3390067087086f926fe6798d4b323563 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Sun, 2 Sep 2018 16:20:08 -0400 Subject: [PATCH 029/125] Add new event Pokemon --- data/formats-data.js | 4 ++++ data/learnsets.js | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/data/formats-data.js b/data/formats-data.js index 305947a7b2fb..fec37fd93960 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -3804,6 +3804,8 @@ let BattleFormatsData = { {"generation": 5, "level": 68, "shiny": 1, "moves": ["psychoshift", "charm", "psychic", "healpulse"]}, {"generation": 6, "level": 30, "shiny": 1, "moves": ["healpulse", "dragonbreath", "mistball", "psychoshift"]}, {"generation": 7, "level": 60, "shiny": 1, "moves": ["mistball", "dragonpulse", "psychoshift", "wish"]}, + {"generation": 7, "level": 60, "moves": ["mistball", "dragonpulse", "psychoshift", "wish"], "pokeball": "cherishball"}, + {"generation": 7, "level": 100, "moves": ["mistball", "psychic", "dracometeor", "tailwind"], "pokeball": "cherishball"}, ], eventOnly: true, tier: "UU", @@ -3829,6 +3831,8 @@ let BattleFormatsData = { {"generation": 6, "level": 30, "shiny": 1, "moves": ["healpulse", "dragonbreath", "lusterpurge", "psychoshift"]}, {"generation": 6, "level": 50, "nature": "Modest", "moves": ["dragonpulse", "lusterpurge", "psychic", "healpulse"], "pokeball": "cherishball"}, {"generation": 7, "level": 60, "shiny": 1, "moves": ["lusterpurge", "dragonpulse", "psychoshift", "dragonbreath"]}, + {"generation": 7, "level": 60, "moves": ["lusterpurge", "dragonpulse", "psychoshift", "dragonbreath"], "pokeball": "cherishball"}, + {"generation": 7, "level": 100, "moves": ["lusterpurge", "psychic", "dracometeor", "tailwind"], "pokeball": "cherishball"}, ], eventOnly: true, tier: "OU", diff --git a/data/learnsets.js b/data/learnsets.js index b8779012c094..74362395d8d8 100644 --- a/data/learnsets.js +++ b/data/learnsets.js @@ -34298,10 +34298,10 @@ let BattleLearnsets = { dive: ["6M", "5M", "4T", "3M"], doubleedge: ["3T"], doubleteam: ["7M", "6M", "5M", "4M", "3M"], - dracometeor: ["7T", "6T", "5T", "4T"], + dracometeor: ["7T", "7S9", "6T", "5T", "4T"], dragonbreath: ["7L20", "6L20", "6S6", "5L20", "4L20", "4S3", "3L20"], dragonclaw: ["7M", "6M", "5M", "4M", "3M"], - dragonpulse: ["7T", "7L56", "7S7", "6T", "6L1", "5T", "5L80", "4M", "4L70"], + dragonpulse: ["7T", "7L56", "7S8", "7S7", "6T", "6L1", "5T", "5L80", "4M", "4L70"], dreameater: ["7M", "6M", "5M", "4M", "3T"], earthquake: ["7M", "6M", "5M", "4M", "3M"], endure: ["4M", "3T"], @@ -34328,13 +34328,13 @@ let BattleLearnsets = { magiccoat: ["7T", "6T", "5T", "4T"], magicroom: ["7T", "6T", "5T"], mimic: ["3T"], - mistball: ["7L24", "7S7", "6L24", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], + mistball: ["7L24", "7S9", "7S8", "7S7", "6L24", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], mudslap: ["4T", "3T"], naturalgift: ["4M"], outrage: ["7T", "6T", "5T", "4T"], protect: ["7M", "6M", "5M", "4M", "3M"], - psychic: ["7M", "7L51", "6M", "6L51", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], - psychoshift: ["7L28", "7S7", "6L28", "6S6", "5L50", "5S5", "4L50"], + psychic: ["7M", "7L51", "7S9", "6M", "6L51", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], + psychoshift: ["7L28", "7S8", "7S7", "6L28", "6S6", "5L50", "5S5", "4L50"], psychup: ["7M", "6M", "5M", "4M", "3T"], psyshock: ["7M", "6M", "5M"], psywave: ["7L1", "6L1", "5L1", "4L1", "3L1"], @@ -34367,7 +34367,7 @@ let BattleLearnsets = { surf: ["7M", "6M", "5M", "4M", "3M"], swagger: ["7M", "6M", "5M", "4M", "3T"], swift: ["4T", "3T"], - tailwind: ["7T", "6T", "5T", "4T"], + tailwind: ["7T", "7S9", "6T", "5T", "4T"], telekinesis: ["7T", "5M"], thunder: ["7M", "6M", "5M", "4M", "3M"], thunderbolt: ["7M", "6M", "5M", "4M", "3M"], @@ -34379,7 +34379,7 @@ let BattleLearnsets = { waterpulse: ["7T", "6T", "4M", "3M"], watersport: ["7L4", "6L4", "5L25", "4L25", "4S4", "4S3", "3L25", "3S0"], whirlpool: ["4M"], - wish: ["7L1", "7S7", "6L1", "5L5", "4L5", "3L5"], + wish: ["7L1", "7S8", "7S7", "6L1", "5L5", "4L5", "3L5"], zenheadbutt: ["7T", "7L41", "6T", "6L40", "5T", "5L40", "4T", "4L40", "4S4"], }}, latios: {learnset: { @@ -34397,11 +34397,11 @@ let BattleLearnsets = { dive: ["6M", "5M", "4T", "3M"], doubleedge: ["3T"], doubleteam: ["7M", "6M", "5M", "4M", "3M"], - dracometeor: ["7T", "6T", "5T", "4T"], - dragonbreath: ["7L20", "7S8", "6L20", "6S6", "5L20", "4L20", "4S3", "3L20"], + dracometeor: ["7T", "7S10", "6T", "5T", "4T"], + dragonbreath: ["7L20", "7S9", "7S8", "6L20", "6S6", "5L20", "4L20", "4S3", "3L20"], dragonclaw: ["7M", "6M", "5M", "4M", "3M"], dragondance: ["7L7", "6L1", "5L55", "5S5", "4L55", "3L50", "3S2", "3S1"], - dragonpulse: ["7T", "7L56", "7S8", "6T", "6L1", "6S7", "5T", "5L80", "4M", "4L70"], + dragonpulse: ["7T", "7L56", "7S9", "7S8", "6T", "6L1", "6S7", "5T", "5L80", "4M", "4L70"], dreameater: ["7M", "6M", "5M", "4M", "3T"], earthquake: ["7M", "6M", "5M", "4M", "3M"], endure: ["4M", "3T"], @@ -34424,7 +34424,7 @@ let BattleLearnsets = { laserfocus: ["7T"], lastresort: ["7T", "6T", "5T", "4T"], lightscreen: ["7M", "6M", "5M", "4M", "3M"], - lusterpurge: ["7L24", "7S8", "6L24", "6S7", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], + lusterpurge: ["7L24", "7S10", "7S9", "7S8", "6L24", "6S7", "6S6", "5L35", "4L35", "4S4", "4S3", "3L35", "3S2", "3S1", "3S0"], magiccoat: ["7T", "6T", "5T", "4T"], memento: ["7L61", "7L1", "6L1", "5L85", "4L60", "3L5"], mimic: ["3T"], @@ -34433,8 +34433,8 @@ let BattleLearnsets = { outrage: ["7T", "6T", "5T", "4T"], powersplit: ["7L46", "6L1", "5L75"], protect: ["7M", "7L4", "6M", "6L4", "5M", "5L25", "4M", "4L25", "4S4", "4S3", "3M", "3L25", "3S0"], - psychic: ["7M", "7L51", "6M", "6L51", "6S7", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], - psychoshift: ["7L28", "7S8", "6L28", "6S6", "5L50", "5S5", "4L50"], + psychic: ["7M", "7L51", "7S10", "6M", "6L51", "6S7", "5M", "5L60", "5S5", "4M", "4L65", "3M", "3L40", "3S2", "3S1", "3S0"], + psychoshift: ["7L28", "7S9", "7S8", "6L28", "6S6", "5L50", "5S5", "4L50"], psychup: ["7M", "6M", "5M", "4M", "3T"], psyshock: ["7M", "6M", "5M"], psywave: ["7L1", "6L1", "5L1", "4L1", "3L1"], @@ -34464,7 +34464,7 @@ let BattleLearnsets = { surf: ["7M", "6M", "5M", "4M", "3M"], swagger: ["7M", "6M", "5M", "4M", "3T"], swift: ["4T", "3T"], - tailwind: ["7T", "6T", "5T", "4T"], + tailwind: ["7T", "7S10", "6T", "5T", "4T"], telekinesis: ["7T", "7L36", "6L1", "5M", "5L70"], thunder: ["7M", "6M", "5M", "4M", "3M"], thunderbolt: ["7M", "6M", "5M", "4M", "3M"], From 9623a190bf11ddac9d7f58009257e8b296e8b404 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 3 Sep 2018 16:23:09 +0400 Subject: [PATCH 030/125] Update PU --- config/formats.js | 17 +++++++---------- data/formats-data.js | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/config/formats.js b/config/formats.js index 80a57488cf16..a5bf78ed0218 100644 --- a/config/formats.js +++ b/config/formats.js @@ -104,9 +104,6 @@ let Formats = [ mod: 'gen7', ruleset: ['[Gen 7] NU'], banlist: ['NU', 'PUBL'], - onBegin: function () { - if (this.rated) this.add('html', `
PU is currently suspecting Pyroar! For information on how to participate check out the suspect thread.
`); - }, }, { name: "[Gen 7] LC", @@ -778,13 +775,13 @@ let Formats = [ ruleset: ['[Gen 7] PU'], banlist: [ // PU - 'Absol', 'Aggron', 'Altaria', 'Archeops', 'Aromatisse', 'Articuno', 'Audino', 'Aurorus', 'Claydol', - 'Clefairy', 'Drampa', 'Eelektross', 'Exeggutor-Alola', 'Floatzel', 'Froslass', 'Golurk', 'Gourgeist-Super', - 'Granbull', 'Gurdurr', 'Haunter', 'Hitmonchan', 'Kangaskhan', 'Kingler', 'Komala', 'Lanturn', 'Liepard', - 'Lilligant', 'Lycanroc-Base', 'Manectric', 'Mesprit', 'Mudsdale', 'Omastar', 'Oricorio-Sensu', 'Passimian', - 'Persian-Alola', 'Poliwrath', 'Primeape', 'Probopass', 'Pyroar', 'Pyukumuku', 'Quagsire', 'Qwilfish', - 'Raichu-Alola', 'Regirock', 'Sableye', 'Sandslash-Alola', 'Scyther', 'Silvally-Fairy', 'Silvally-Ghost', - 'Skuntank', 'Spiritomb', 'Stoutland', 'Swanna', 'Togedemaru', 'Weezing', 'Zangoose', + 'Absol', 'Aggron', 'Altaria', 'Archeops', 'Aromatisse', 'Articuno', 'Audino', 'Aurorus', 'Claydol', 'Clefairy', + 'Drampa', 'Eelektross', 'Exeggutor-Alola', 'Floatzel', 'Froslass', 'Golurk', 'Gourgeist-Super', 'Granbull', + 'Gurdurr', 'Haunter', 'Hitmonchan', 'Kangaskhan', 'Kingler', 'Komala', 'Lanturn', 'Liepard', 'Lilligant', + 'Lycanroc-Base', 'Manectric', 'Mesprit', 'Mudsdale', 'Omastar', 'Oricorio-Sensu', 'Passimian', 'Persian-Alola', + 'Poliwrath', 'Primeape', 'Probopass', 'Pyukumuku', 'Quagsire', 'Qwilfish', 'Raichu-Alola', 'Regirock', + 'Sableye', 'Sandslash-Alola', 'Scyther', 'Silvally-Fairy', 'Silvally-Ghost', 'Skuntank', 'Spiritomb', + 'Stoutland', 'Swanna', 'Togedemaru', 'Weezing', 'Zangoose', // ZUBL 'Carracosta', 'Crabominable', 'Exeggutor-Base', 'Gorebyss', 'Jynx', 'Kabutops', 'Ludicolo', 'Musharna', 'Raticate-Alola', 'Raticate-Alola-Totem', 'Throh', 'Turtonator', 'Type: Null', 'Ursaring', 'Victreebel', diff --git a/data/formats-data.js b/data/formats-data.js index fec37fd93960..f1c433b89fe5 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -6054,7 +6054,7 @@ let BattleFormatsData = { eventPokemon: [ {"generation": 6, "level": 49, "gender": "M", "perfectIVs": 2, "isHidden": false, "abilities": ["unnerve"], "moves": ["hypervoice", "fireblast", "darkpulse"], "pokeball": "cherishball"}, ], - tier: "PU", + tier: "PUBL", doublesTier: "DUU", }, flabebe: { From 733ea72d4f3a9db3139ce9330156e4df4b481528 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Mon, 3 Sep 2018 11:24:38 -0400 Subject: [PATCH 031/125] Make a move fail if the target is the user and it's not self-target --- data/scripts.js | 2 +- data/statuses.js | 4 ++-- sim/battle.js | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/data/scripts.js b/data/scripts.js index 0dffcf7d01f9..99c45f925954 100644 --- a/data/scripts.js +++ b/data/scripts.js @@ -271,7 +271,7 @@ let BattleScripts = { lacksTarget = !this.isAdjacent(target, pokemon); } } - if (lacksTarget && (!move.flags['charge'] || pokemon.volatiles['twoturnmove'])) { + if (lacksTarget && (!move.flags['charge'] || pokemon.volatiles['twoturnmove']) && !move.isFutureMove) { this.attrLastMove('[notarget]'); this.add('-notarget'); if (move.target === 'normal') pokemon.isStaleCon = 0; diff --git a/data/statuses.js b/data/statuses.js index 7bcba89063e3..103169740496 100644 --- a/data/statuses.js +++ b/data/statuses.js @@ -391,8 +391,8 @@ let BattleStatuses = { // time's up; time to hit! :D const move = this.getMove(posData.move); - if (target.fainted) { - this.add('-hint', '' + move.name + ' did not hit because the target is fainted.'); + if (target.fainted || target === posData.source) { + this.add('-hint', '' + move.name + ' did not hit because the target is ' + (target.fainted ? 'fainted' : 'the user') + '.'); this.effectData.positions[i] = null; continue; } diff --git a/sim/battle.js b/sim/battle.js index 5ae171961d3b..542b55a246a4 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -2396,8 +2396,12 @@ class Battle extends Dex.ModdedDex { getTarget(pokemon, move, targetLoc) { move = this.getMove(move); let target; - if ((move.target !== 'randomNormal') && - this.validTargetLoc(targetLoc, pokemon, move.target)) { + // Fails if the target is the user and the move can't target its own position + if (['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === -(pokemon.position + 1)) { + if (move.isFutureMove) return pokemon; + return false; + } + if (move.target !== 'randomNormal' && this.validTargetLoc(targetLoc, pokemon, move.target)) { if (targetLoc > 0) { target = pokemon.side.foe.active[targetLoc - 1]; } else { From 058a1ee38bf037b466a82a96b78fb2f4f5d40bdd Mon Sep 17 00:00:00 2001 From: askii93 Date: Mon, 3 Sep 2018 13:05:51 -0400 Subject: [PATCH 032/125] Event sort (#4833) * Add sort for events table * Tested w/scav room users * something * Check for permissions and convert back to objects * Communicate Command to Modlog & User * Truncate Message Building * Fail if no column name is specified --- chat-plugins/room-events.js | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/chat-plugins/room-events.js b/chat-plugins/room-events.js index 7abb0fe71f33..bb0d0dc1ba96 100644 --- a/chat-plugins/room-events.js +++ b/chat-plugins/room-events.js @@ -94,11 +94,71 @@ exports.commands = { help: function (target, room, user) { return this.parse('/help roomevents'); }, + sortby: function (target, room, user) { + // preconditions + if (!room.chatRoomData) return this.errorReply("This command is unavailable in temporary rooms."); + if (!room.events || !Object.keys(room.events).length) { + return this.errorReply("There are currently no planned upcoming events for this room."); + } + if (!this.can('ban', null, room)) return false; + + // declare variables + let multiplier = 1; + let columnName = ""; + let delimited = target.split(target.includes('|') ? '|' : ','); + let sortable = Object.values(room.events); + + // id tokens + if (delimited.length === 1) { + columnName = target; + } else { + let order = ""; + [columnName, order] = delimited; + order = toId(order); + multiplier = (order === 'desc') ? -1 : 1; + } + + // sort the array by the appropriate column name + columnName = toId(columnName); + switch (columnName) { + case "date": + case "eventdate": + sortable.sort((a, b) => { return (toId(a.date) < toId(b.date)) ? -1 * multiplier : (toId(b.date) < toId(a.date)) ? 1 * multiplier : 0; }); + break; + case "desc": + case "description": + case "eventdescription": + sortable.sort((a, b) => { return (toId(a.desc) < toId(b.desc)) ? -1 * multiplier : (toId(b.desc) < toId(a.desc)) ? 1 * multiplier : 0; }); + break; + case "eventname": + case "name": + sortable.sort((a, b) => { return (toId(a.eventName) < toId(b.eventName)) ? -1 * multiplier : (toId(b.eventName) < toId(a.eventName)) ? 1 * multiplier : 0; }); + break; + default: + return this.errorReply("No or invalid column name specified. Please use one of: date, eventdate, desc, description, eventdescription, eventname, name."); + } + + // rebuild the room.events object + room.events = {}; + for (const sortedObj of sortable) { + const eventId = toId(sortedObj.eventName); + room.events[eventId] = sortedObj; + } + room.chatRoomData.events = room.events; + + // build communication string + const resultString = `sorted by column:` + columnName + + ` in ${multiplier === 1 ? "ascending" : "descending"} order` + + `${delimited.length === 1 ? " (by default)" : ""}`; + this.modlog('ROOMEVENT', null, resultString); + return this.sendReply(resultString); + }, }, roomeventshelp: [ `/roomevents - Displays a list of upcoming room-specific events.`, `/roomevents add [event name] | [event date/time] | [event description] - Adds a room event. Requires: @ # & ~`, `/roomevents remove [event name] - Deletes an event. Requires: @ # & ~`, `/roomevents view [event name] - Displays information about a specific event.`, + `/roomevents sortby [column name] | [asc/desc (optional)] - Sorts events table by column name and an optional argument to ascending or descending order. Ascending order is default`, ], }; From 91c03011caf12d1a95dcf537f4d9603f67c6074b Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 3 Sep 2018 23:43:52 +0400 Subject: [PATCH 033/125] 1v1: Ban Snorlax --- config/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/formats.js b/config/formats.js index a5bf78ed0218..455fd6fc6b46 100644 --- a/config/formats.js +++ b/config/formats.js @@ -658,7 +658,7 @@ let Formats = [ banlist: [ 'Illegal', 'Unreleased', 'Arceus', 'Darkrai', 'Deoxys-Base', 'Deoxys-Attack', 'Deoxys-Defense', 'Dialga', 'Giratina', 'Groudon', 'Ho-Oh', 'Jirachi', 'Kangaskhan-Mega', 'Kyogre', 'Kyurem-Black', 'Kyurem-White', 'Lugia', 'Lunala', 'Marshadow', 'Mewtwo', - 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', + 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Rayquaza', 'Reshiram', 'Salamence-Mega', 'Shaymin-Sky', 'Snorlax', 'Solgaleo', 'Tapu Koko', 'Xerneas', 'Yveltal', 'Zekrom', 'Focus Sash', 'Perish Song', ], }, From a96e804c87f55295ddaf51102b5cc6806e2267f4 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 3 Sep 2018 21:09:28 -0500 Subject: [PATCH 034/125] Improve Sim README - Remove reference to "new API" vs "old API" - Fix `TARGETSPEC` table Markdown --- sim/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sim/README.md b/sim/README.md index d0629ea33bc5..781492e7d1b6 100644 --- a/sim/README.md +++ b/sim/README.md @@ -1,9 +1,7 @@ Simulator ========= -Pokémon Showdown's new simulator API is designed to be relatively more straightforward to use than the old one. - -It is implemented as a `ReadWriteStream`. You write to it player choices, and you read protocol messages from it. +Pokémon Showdown's simulator API is implemented as a `ReadWriteStream`. You write player choices to it, and you read protocol messages from it. ```js const Sim = require('Pokemon-Showdown/sim'); @@ -152,11 +150,11 @@ To be exact, `CHOICE` is one of: - `MOVESLOTSPEC` is a move name (capitalization/spacing-insensitive) or 1-based move slot number - `TARGETSPEC` is a 1-based target slot number. Add a `-` in front of it to refer to allies. Remember that slots oppose each other, so in a battle, the slots go as follows: - Triples Doubles Singles - 3 2 1 2 1 1 - -1 -2 -3 -1 -2 -1 + Triples Doubles Singles + 3 2 1 2 1 1 + -1 -2 -3 -1 -2 -1 -(But note that slot numbers are unnecessary in Singles: you can never choose a target in Singles.) + (But note that slot numbers are unnecessary in Singles: you can never choose a target in Singles.) `SWITCHSPEC` is: From 0f2d590e6e5a63388cfdaab595730d39421ed50a Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 3 Sep 2018 21:12:45 -0500 Subject: [PATCH 035/125] Update SIM README MOVESLOTSPEC table --- sim/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sim/README.md b/sim/README.md index 781492e7d1b6..7d7074c3a020 100644 --- a/sim/README.md +++ b/sim/README.md @@ -150,9 +150,9 @@ To be exact, `CHOICE` is one of: - `MOVESLOTSPEC` is a move name (capitalization/spacing-insensitive) or 1-based move slot number - `TARGETSPEC` is a 1-based target slot number. Add a `-` in front of it to refer to allies. Remember that slots oppose each other, so in a battle, the slots go as follows: - Triples Doubles Singles - 3 2 1 2 1 1 - -1 -2 -3 -1 -2 -1 + Triples Doubles Singles + 3 2 1 2 1 1 + -1 -2 -3 -1 -2 -1 (But note that slot numbers are unnecessary in Singles: you can never choose a target in Singles.) From 16faa75ee4b9d760f286058fd668c0409c51af2b Mon Sep 17 00:00:00 2001 From: Marty-D Date: Tue, 4 Sep 2018 14:23:14 -0400 Subject: [PATCH 036/125] CAP: Add Mumbao and update prevos --- data/formats-data.js | 4 + data/learnsets.js | 1104 ++++++++++++++++++++++++------------------ data/pokedex.js | 32 +- 3 files changed, 659 insertions(+), 481 deletions(-) diff --git a/data/formats-data.js b/data/formats-data.js index f1c433b89fe5..df337da98912 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -7533,6 +7533,10 @@ let BattleFormatsData = { isNonstandard: true, tier: "CAP LC", }, + mumbao: { + isNonstandard: true, + tier: "CAP LC", + }, pokestarufo: { isNonstandard: true, eventPokemon: [ diff --git a/data/learnsets.js b/data/learnsets.js index 74362395d8d8..fddde9b2bcc0 100644 --- a/data/learnsets.js +++ b/data/learnsets.js @@ -63257,537 +63257,645 @@ let BattleLearnsets = { wrap: ["7L1", "4L1"], }}, syclar: {learnset: { - attract: ["4M"], - avalanche: ["4M"], - blizzard: ["4M"], + absorb: ["7L1"], + attract: ["7M", "4M"], + avalanche: ["7L48", "4M"], + blizzard: ["7M", "4M"], bugbite: ["4T"], - bugbuzz: ["4L42"], + bugbuzz: ["7L43", "4L42"], captivate: ["4M"], - counter: ["4T"], + confide: ["7M"], + counter: ["7E", "4T"], cut: ["4M"], - doubleteam: ["4M"], - earthpower: ["4T", "4E"], + doubleedge: ["4T"], + doubleteam: ["7M", "4M"], + earthpower: ["7E", "4T", "4E"], endure: ["4M"], - facade: ["4M"], - falseswipe: ["4M"], - fling: ["4M"], - focusenergy: ["4L13"], - frustration: ["4M"], - furyattack: ["4L1"], - furycutter: ["4T"], - hail: ["4M", "4L28"], - hiddenpower: ["4M"], - icebeam: ["4M"], - iceshard: ["4L8"], - icywind: ["4T", "4L18"], - leechlife: ["4L5"], - leer: ["4L1"], - mimic: ["4T"], + facade: ["7M", "4M"], + falseswipe: ["7M", "4M"], + fellstinger: ["7E"], + fling: ["7M", "4M"], + focusenergy: ["7L10", "4L13"], + frostbreath: ["7M"], + frustration: ["7M", "4M"], + furyattack: ["7L14", "4L1"], + furycutter: ["7L23", "4T"], + hail: ["7M", "7L34", "4M", "4L28"], + hiddenpower: ["7M", "4M"], + honeclaws: ["7M"], + icebeam: ["7M", "4M"], + icepunch: ["7L1"], + iceshard: ["7L5", "4L8"], + iciclecrash: ["7L39"], + icywind: ["7T", "7L19", "4T", "4L18"], + leechlife: ["7M", "4L5"], + leer: ["7L1", "4L1"], + mimic: ["7E", "4T"], naturalgift: ["4M"], - pinmissile: ["4E"], - protect: ["4M"], - rest: ["4M"], - return: ["4M"], - secretpower: ["4M"], - sheercold: ["4L49"], + pinmissile: ["7E", "4E"], + protect: ["7M", "4M"], + raindance: ["7M", "4M"], + rest: ["7M", "4M"], + return: ["7M", "4M"], + rocksmash: ["6M", "5M", "4M"], + round: ["7M"], + secretpower: ["7M", "4M"], + sheercold: ["5L55", "4L49"], + signalbeam: ["7T", "7E"], silverwind: ["4M"], - sleeptalk: ["4M"], - snore: ["4T"], - spikes: ["4E"], + slash: ["7L28"], + sleeptalk: ["7M", "4M"], + snore: ["7T", "4T"], + spikes: ["7E", "4E"], + strength: ["6M", "5M"], stringshot: ["4T"], - substitute: ["4M"], - superpower: ["4T", "4E"], - swagger: ["4M"], - swordsdance: ["4M"], - tailglow: ["4E"], - taunt: ["4M"], - toxic: ["4M"], - uturn: ["4M"], - waterpulse: ["4M"], - xscissor: ["4M", "4L23"], + strugglebug: ["7M"], + substitute: ["7M", "4M"], + superpower: ["7T", "7E", "4T", "4E"], + swagger: ["7M", "4M"], + swordsdance: ["7M", "4M"], + tailglow: ["7E", "4E"], + taunt: ["7M", "4M"], + toxic: ["7M", "4M"], + uturn: ["7M", "4M"], + waterpulse: ["6T", "4M"], + xscissor: ["7M", "7L31", "4M", "4L23"], }}, embirch: {learnset: { - aromatherapy: ["4E"], - attract: ["4M"], - block: ["4T"], - bulletseed: ["4M", "4L1"], - counter: ["4T", "4E"], - doubleedge: ["4T", "4L33"], - doubleteam: ["4M"], - dragonbreath: ["4E"], - ember: ["4L9"], + amnesia: ["7L37"], + aromatherapy: ["7E", "4E"], + attract: ["7M", "4M"], + block: ["7T", "4T"], + bulletseed: ["7L1", "4M", "4L1"], + counter: ["7E", "4T", "4E"], + doubleedge: ["7L1", "7E", "4T", "4L33"], + doubleteam: ["7M", "4M"], + dragonbreath: ["7E", "4E"], + earthpower: ["7T"], + ember: ["7L5", "4L9"], endure: ["4M"], energyball: ["4M"], - facade: ["4M"], - firespin: ["4L25"], - flamethrower: ["4M"], - flamewheel: ["4L17"], + facade: ["7M", "4M"], + firespin: ["7E", "4L25"], + flamethrower: ["7M", "4M"], + flamewheel: ["7L19", "4L17"], flash: ["4M"], - frustration: ["4M"], - gigadrain: ["4M", "4L21"], - grassknot: ["4M"], - grasswhistle: ["4E"], - growth: ["4L5"], + frustration: ["7M", "4M"], + gigadrain: ["7T", "7L32", "4M", "4L21"], + grassknot: ["7M", "4M"], + grasswhistle: ["7E", "4E"], + grassyterrain: ["7E"], + growth: ["7L28", "4L5"], headbutt: ["4T"], - heatwave: ["4T"], - hiddenpower: ["4M"], - irondefense: ["4T"], - irontail: ["4M"], - lavaplume: ["4L40"], - leechseed: ["4L13"], - lightscreen: ["4M"], + heatwave: ["7T", "4T"], + hiddenpower: ["7M", "4M"], + irondefense: ["7T", "7L37", "4T"], + ironhead: ["7T"], + irontail: ["7T", "4M"], + lavaplume: ["7L41", "4L40"], + leechseed: ["7L10", "4L13"], + lightscreen: ["7M", "4M"], lowkick: ["4T"], mimic: ["4T"], mudslap: ["4T"], naturalgift: ["4M"], - overheat: ["4M"], - petaldance: ["4L29"], - protect: ["4M"], - psybeam: ["4E"], - rest: ["4M"], - return: ["4M"], - revenge: ["4E"], - roar: ["4M"], + naturepower: ["7M"], + overheat: ["7M", "4M"], + petalblizzard: ["7E"], + petaldance: ["7L 46", "4L29"], + protect: ["7M", "4M"], + psybeam: ["7E", "4E"], + rest: ["7M", "4M"], + return: ["7M", "4M"], + revenge: ["7E", "4E"], + roar: ["7M", "4M"], rockclimb: ["4M"], - rockslide: ["4M"], + rockslide: ["7M", "4M"], rocksmash: ["4M"], - rocktomb: ["4M"], - safeguard: ["4M"], - sandtomb: ["4E"], + rocktomb: ["7M", "4M"], + round: ["7M"], + safeguard: ["7M", "4M"], + sandtomb: ["7L23", "7E", "4E"], secretpower: ["4M"], - seedbomb: ["4T"], - sleeptalk: ["4M"], - snore: ["4T"], - solarbeam: ["4M"], - stealthrock: ["4M"], + seedbomb: ["7T", "4T"], + sleeptalk: ["7M", "4M"], + snore: ["7T", "4T"], + solarbeam: ["7M", "4M"], + stealthrock: ["7T", "4M"], strength: ["4M"], - substitute: ["4M"], - sunnyday: ["4M"], - swagger: ["4M"], - sweetscent: ["4L1"], - swordsdance: ["4M"], - synthesis: ["4T", "4L37"], - toxic: ["4M"], - watersport: ["4E"], - willowisp: ["4M"], - worryseed: ["4T"], + substitute: ["7M", "4M"], + sunnyday: ["7M", "4M"], + swagger: ["7M", "4M"], + sweetscent: ["7L1", "4L1"], + swordsdance: ["7M", "4M"], + synthesis: ["7T", "7L14", "4T", "4L37"], + toxic: ["7M", "4M"], + watersport: ["7E", "4E"], + willowisp: ["7M", "4M"], + worryseed: ["7T", "4T"], zapcannon: ["4L44"], }}, flarelm: {learnset: { - aromatherapy: ["4E"], - attract: ["4M"], - block: ["4T"], - bulletseed: ["4M", "4L1"], - counter: ["4T", "4E"], - doubleedge: ["4T"], - doubleteam: ["4M"], - dragonbreath: ["4E"], - ember: ["4L9"], + amnesia: ["7L37"], + ancientpower: ["4T"], + aromatherapy: ["7E", "4E"], + attract: ["7M", "4M"], + block: ["7T", "4T"], + bulldoze: ["7M"], + bulletseed: ["7L1", "4M", "4L1"], + confide: ["7M"], + counter: ["7E", "4T", "4E"], + doubleedge: ["7L1", "7E", "4T"], + doubleteam: ["7M", "4M"], + dragonbreath: ["7E", "4E"], + dragonpulse: ["7T"], + dragontail: ["7M"], + earthpower: ["7T", "4T"], + earthquake: ["7M", "4M"], + ember: ["7L5", "4L9"], endure: ["4M"], energyball: ["4M"], - facade: ["4M"], - firespin: ["4L28"], - flamethrower: ["4M"], - flamewheel: ["4L17"], + facade: ["7M", "4M"], + fireblast: ["7M", "4M"], + firespin: ["7E", "4L28"], + flameburst: ["7L24"], + flamecharge: ["7M"], + flamethrower: ["7M", "4M"], + flamewheel: ["7L19", "4L17"], flash: ["4M"], - frustration: ["4M"], - gigadrain: ["4M", "4L21"], - grassknot: ["4M"], - grasswhistle: ["4E"], - growth: ["4L5"], + flashcannon: ["7M"], + frustration: ["7M", "4M"], + gigadrain: ["7L32", "4M", "4L21"], + grassknot: ["7M", "4M"], + grasswhistle: ["7E", "4E"], + grassyterrain: ["7E"], + growth: ["7L28", "4L5"], headbutt: ["4T"], - heatwave: ["4T"], - hiddenpower: ["4M"], - irondefense: ["4T", "4L40"], - irontail: ["4M"], - lavaplume: ["4L48"], - leechseed: ["4L13"], - lightscreen: ["4M"], - lowkick: ["4T"], + heatwave: ["7T", "4T"], + hiddenpower: ["7M", "4M"], + incinerate: ["7M"], + irondefense: ["7L37", "4T", "4L40"], + ironhead: ["7T"], + irontail: ["7T", "4M"], + lavaplume: ["7L46", "4L48"], + leechseed: ["7L10", "4L13"], + lightscreen: ["7M", "4M"], + lowkick: ["7T", "4T"], mimic: ["4T"], mudslap: ["4T"], naturalgift: ["4M"], - overheat: ["4M"], - petaldance: ["4L36"], - protect: ["4M"], - psybeam: ["4E"], - rest: ["4M"], - return: ["4M"], - revenge: ["4E"], - roar: ["4M"], + naturepower: ["7M"], + overheat: ["7M", "4M"], + petalblizzard: ["7E"], + petaldance: ["7L50", "4L36"], + protect: ["7M", "4M"], + psybeam: ["7E", "4E"], + rest: ["7M", "4M"], + return: ["7M", "4M"], + revenge: ["7E", "4E"], + roar: ["7M", "4M"], rockclimb: ["4M"], - rockslide: ["4M"], - rocksmash: ["4M"], - rocktomb: ["4M"], - safeguard: ["4M"], - sandtomb: ["4E"], - secretpower: ["4M"], - seedbomb: ["4T"], - sleeptalk: ["4M"], - snore: ["4T"], - solarbeam: ["4M"], - stealthrock: ["4M"], - strength: ["4M"], - substitute: ["4M"], - sunnyday: ["4M"], - swagger: ["4M"], - sweetscent: ["4L1"], - swordsdance: ["4M"], - synthesis: ["4T", "4L44"], - toxic: ["4M"], - watersport: ["4E"], - willowisp: ["4M"], - worryseed: ["4T"], + rockslide: ["7M", "4M"], + rocksmash: ["6M", "5M", "4M"], + rocktomb: ["7M", "4M"], + round: ["7M"], + safeguard: ["7M", "4M"], + sandtomb: ["7E", "4E"], + secretpower: ["7M", "4M"], + seedbomb: ["7L23", "4T"], + sleeptalk: ["7M", "4M"], + snore: ["7T", "4T"], + solarbeam: ["7M", "4M"], + stealthrock: ["7T", "4M"], + strength: ["6M", "5M", "4M"], + substitute: ["7M", "4M"], + sunnyday: ["7M", "4M"], + swagger: ["7M", "4M"], + sweetscent: ["7L1", "4L1"], + swordsdance: ["7M", "4M"], + synthesis: ["7L14", "4T", "4L44"], + toxic: ["7M", "4M"], + watersport: ["7E", "4E"], + wildcharge: ["7M"], + willowisp: ["7M", "4M"], + worryseed: ["7T", "4T"], + zenheadbutt: ["7T"], zapcannon: ["4L52"], }}, breezi: {learnset: { - aerialace: ["4M", "4L55"], - attract: ["4M"], - block: ["4T"], + acrobatics: ["7M", "7L59"], + aerialace: ["7M", "7L30", "4M", "4L55"], + afteryou: ["7T"], + attract: ["7M", "4M"], bodyslam: ["4T", "4L30"], + block: ["7T", "4T"], captivate: ["4M"], - copycat: ["4L19"], - disable: ["4E"], - doubleedge: ["4T"], - doubleteam: ["4M"], - encore: ["4L5"], + confide: ["7M"], + copycat: ["7L19", "4L19"], + disable: ["7E", "4E"], + doubleedge: ["7E", "4T"], + doubleteam: ["7M", "4M"], + encore: ["7L5", "4L5"], endure: ["4M"], - facade: ["4M"], - fling: ["4M"], - followme: ["4E"], - frustration: ["4M"], - gastroacid: ["4T"], - gust: ["4L1"], - healblock: ["4L50"], - helpinghand: ["4T", "4L1"], - hiddenpower: ["4M"], - icywind: ["4T"], - knockoff: ["4T", "4L14"], - lightscreen: ["4M"], - luckychant: ["4L59"], - mefirst: ["4E"], - metronome: ["4E"], - mimic: ["4T", "4E"], + entrainment: ["7E"], + facade: ["7M", "4M"], + fling: ["7M", "4M"], + followme: ["7E", "4E"], + frustration: ["7M", "4M"], + gastroacid: ["7T", "4T"], + gust: ["7L1", "4L1"], + healblock: ["7L54", "4L50"], + helpinghand: ["7T", "7L1", "4T", "4L1"], + hiddenpower: ["7M", "4M"], + icywind: ["7T", "4T"], + knockoff: ["7L14", "4T", "4L14"], + lightscreen: ["7M", "4M"], + luckychant: ["7L55", "4L59"], + mefirst: ["7E", "4E"], + metronome: ["7E", "4T", "4E"], + mimic: ["7E", "4T", "4E"], mudslap: ["4T"], naturalgift: ["4M"], ominouswind: ["4T"], - poisonjab: ["4M"], - protect: ["4M"], - psychup: ["4M"], - raindance: ["4M"], - reflect: ["4M"], - rest: ["4M", "4L44"], - return: ["4M"], - safeguard: ["4M", "4L9"], - sandstorm: ["4M"], - sandtomb: ["4E"], - secretpower: ["4M"], - shadowball: ["4M"], - skillswap: ["4M"], - sleeptalk: ["4M"], - sludgebomb: ["4M", "4L34"], - snatch: ["4M"], - snore: ["4T"], - spikes: ["4E"], - substitute: ["4M"], - sunnyday: ["4M"], - swagger: ["4M"], + poisonjab: ["7M", "4M"], + protect: ["7M", "4M"], + psychup: ["7M", "4M"], + raindance: ["7M", "4M"], + reflect: ["7M", "4M"], + rest: ["7M", "7L44", "4M", "4L44"], + return: ["7M", "4M"], + roleplay: ["7T"], + round: ["7M"], + safeguard: ["7M", "7L9", "4M", "4L9"], + sandstorm: ["7M", "4M"], + sandtomb: ["7E", "4E"], + secretpower: ["7M", "4M"], + shadowball: ["7M", "4M"], + skillswap: ["7M", "4M"], + sleeptalk: ["7M", "4M"], + sludgebomb: ["7M", "7L34", "4M", "4L34"], + snatch: ["7M", "4M"], + snore: ["7T", "4T"], + spikes: ["7E", "4E"], + stealthrock: ["7T"], + substitute: ["7M", "4M"], + sunnyday: ["7M", "4M"], + swagger: ["7M", "4M"], swift: ["4T"], - tailwind: ["4T", "4L1"], - toxic: ["4M"], - toxicspikes: ["4L39"], - trickroom: ["4M"], + tailwind: ["7L1", "4T", "4L1"], + taunt: ["7M"], + thief: ["7M"], + toxic: ["7M", "4M"], + toxicspikes: ["7L39", "4L39"], + trickroom: ["7M", "4M"], twister: ["4T"], - uturn: ["4M"], - whirlwind: ["4L25"], - wish: ["4E"], + uturn: ["7M", "4M"], + venoshock: ["7M"], + whirlwind: ["7L25", "4L25"], + wish: ["7E", "4E"], }}, scratchet: {learnset: { - aerialace: ["5M"], - attract: ["5M"], - batonpass: ["5E"], - brickbreak: ["5M"], - bulkup: ["5M", "5L40"], - bulldoze: ["5M"], - confuseray: ["5E"], - doubleteam: ["5M"], - echoedvoice: ["5M"], - facade: ["5M"], - falseswipe: ["5M"], - flash: ["5M"], - fling: ["5M"], - focusblast: ["5M"], - focusenergy: ["5L13", "5E"], - frustration: ["5M"], - furyswipes: ["5L1"], - grassknot: ["5M"], - harden: ["5L9"], - haze: ["5E"], - hiddenpower: ["5M"], - hypervoice: ["5L36"], - memento: ["5E"], - naturepower: ["5E"], - protect: ["5M"], - quash: ["5M"], - raindance: ["5M"], - rapidspin: ["5E"], - rest: ["5M", "5L53"], - retaliate: ["5M", "5L57"], - return: ["5M"], - roar: ["5M", "5L23"], - rockslide: ["5M"], - rocksmash: ["5M", "5L18"], - rocktomb: ["5M"], - roost: ["5E"], - round: ["5M"], - safeguard: ["5M"], - stealthrock: ["5E"], - strength: ["5M"], - submission: ["5L32"], - substitute: ["5M"], - sunnyday: ["5M"], - superpower: ["5L45"], - swagger: ["5M"], - taunt: ["5M", "5L49"], - thief: ["5M"], - toxic: ["5M"], - workup: ["5M", "5L27"], - yawn: ["5E"], + aerialace: ["7M", "5M"], + attract: ["7M", "5M"], + batonpass: ["7E", "5E"], + brickbreak: ["7M", "7E", "5M"], + bulkup: ["7M", "7L40", "5M", "5L40"], + bulldoze: ["7M", "5M"], + confide: ["7M"], + confuseray: ["7M", "7E", "5E"], + doubleteam: ["7M", "5M"], + echoedvoice: ["7M", "5M"], + facade: ["7M", "5M"], + falseswipe: ["7M", "5M"], + flash: ["6M", "5M"], + fling: ["7M", "5M"], + focusblast: ["7M", "5M"], + focusenergy: ["7L13", "7E", "5L13", "5E"], + frustration: ["7M", "5M"], + furyswipes: ["7L18", "5L1"], + grassknot: ["7M", "5M"], + harden: ["7L4", "5L9"], + haze: ["7E", "5E"], + helpinghand: ["7T"], + hiddenpower: ["7M", "5M"], + hypervoice: ["7L36", "5L36"], + irontail: ["7T"], + memento: ["7E", "5E"], + naturepower: ["7M", "7E", "5E"], + poweruppunch: ["6L99"], + protect: ["7M", "5M"], + quash: ["7M", "5M"], + raindance: ["7M", "5M"], + rapidspin: ["7E", "5E"], + rest: ["7M", "7L53", "5M", "5L53"], + retaliate: ["7L57", "6M", "5M", "5L57"], + return: ["7M", "5M"], + roar: ["7M", "7L23", "5M", "5L23"], + rockslide: ["7M", "5M"], + rocksmash: ["7T", "7L9", "6M", "5M", "5L18"], + rocktomb: ["7M", "5M"], + roost: ["7M", "7E", "5E"], + round: ["7M", "5M"], + safeguard: ["7M", "5M"], + scratch: ["7L1"], + secretpower: ["7M"], + snore: ["7T"], + stealthrock: ["7T", "7E", "5E"], + strength: ["6M", "5M"], + submission: ["7L32", "5L32"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "5M"], + superpower: ["7L45", "5L45"], + swagger: ["7M", "5M"], + taunt: ["7M", "7L49", "5M", "5L49"], + thief: ["7M", "5M"], + toxic: ["7M", "5M"], + workup: ["7M", "7L27", "5M", "5L27"], + yawn: ["7E", "5E"], }}, necturine: {learnset: { - attract: ["5M"], - calmmind: ["5M"], - curse: ["5E"], - cut: ["5M"], - doubleteam: ["5M"], - dreameater: ["5M"], - energyball: ["5M"], - facade: ["5M"], - flash: ["5M"], - frustration: ["5M"], - futuresight: ["5E"], - gigadrain: ["5E"], - grassknot: ["5M"], - gravity: ["5E"], - hex: ["5L25"], - hiddenpower: ["5M"], - ingrain: ["5E"], - leafblade: ["5E"], - leafstorm: ["5E"], - leer: ["5L1"], - naturalgift: ["5L31", "5E"], - nightmare: ["5E"], - ominouswind: ["5L7"], - painsplit: ["5L37"], - payback: ["5M"], - powerwhip: ["5L50"], - protect: ["5M"], - psychic: ["5M"], - psychup: ["5M"], - rest: ["5M"], - return: ["5M"], - round: ["5M"], - shadowball: ["5M", "5L44"], - shadowsneak: ["5L13"], - sketch: ["5E"], - solarbeam: ["5M"], - substitute: ["5M"], - sunnyday: ["5M"], - swagger: ["5M"], - telekinesis: ["5M"], - thief: ["5M"], - torment: ["5M"], - toxic: ["5M"], - toxicspikes: ["5L19"], - vinewhip: ["5L1"], - willowisp: ["5M", "5L19"], + attract: ["7M", "5M"], + calmmind: ["7M", "5M"], + confide: ["7M"], + curse: ["7E", "5E"], + cut: ["7M", "5M"], + darkpulse: ["7M"], + doubleteam: ["7M", "5M"], + dreameater: ["7M", "5M"], + energyball: ["7M", "5M"], + facade: ["7M", "5M"], + flash: ["6M", "5M"], + frustration: ["7M", "5M"], + futuresight: ["7E", "5E"], + gigadrain: ["7E", "6T", "5E"], + grassknot: ["7M", "5M"], + grassyterrain: ["7L22"], + gravity: ["7E", "6T", "5E"], + hex: ["7L18", "5L25"], + hiddenpower: ["7M", "5M"], + ingrain: ["7E", "5E"], + leafblade: ["7E", "5E"], + leafstorm: ["7E", "5E"], + leechlife: ["7M"], + leechseed: ["7L8"], + leer: ["7L1", "5L1"], + magicalleaf: ["7L11"], + naturalgift: ["7E", "5L31", "5E"], + naturepower: ["7M"], + nightmare: ["7E", "5E"], + nightshade: ["7L26"], + ominouswind: ["7E", "5L7"], + painsplit: ["7L34", "6T", "5L37"], + payback: ["7M", "5M"], + phantomforce: ["7L56"], + powerwhip: ["7L50", "5L50"], + protect: ["7M", "5M"], + psychic: ["7M", "5M"], + psychup: ["7M", "5M"], + rest: ["7M", "5M"], + return: ["7M", "5M"], + round: ["7M", "5M"], + secretpower: ["7M"], + seedbomb: ["7T", "7L39"], + shadowball: ["7M", "7L43", "5M", "5L44"], + shadowsneak: ["7L4", "5L13"], + sketch: ["7E", "5E"], + sleeptalk: ["7M"], + snore: ["7T"], + solarbeam: ["7M", "5M"], + spite: ["7T"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "5M"], + swagger: ["7M", "5M"], + telekinesis: ["6M", "5M"], + thief: ["7M", "5M"], + torment: ["7M", "5M"], + toxic: ["7M", "5M"], + toxicspikes: ["7L23", "5L19"], + vinewhip: ["7L1", "5L1"], + willowisp: ["7M", "7L15", "5M", "5L19"], + worryseed: ["7T"], }}, cupra: {learnset: { - allyswitch: ["5M"], - ancientpower: ["5L44"], - attract: ["5M"], - bugbite: ["5T", "5L7"], - bugbuzz: ["5E"], - closecombat: ["5E"], - counter: ["5E"], - cut: ["5M"], - disable: ["5E"], - doubleteam: ["5M"], - dreameater: ["5M"], - echoedvoice: ["5M"], - electroweb: ["5T"], - facade: ["5M"], - feint: ["5E"], - finalgambit: ["5L38"], - flash: ["5M"], - fling: ["5M"], - frustration: ["5M"], - hail: ["5M"], - healpulse: ["5L21"], - helpinghand: ["5T"], - hiddenpower: ["5M"], - hydropump: ["5E"], - lightscreen: ["5M"], - magiccoat: ["5T"], - magicroom: ["5T"], - megahorn: ["5E"], - protect: ["5M"], - psychic: ["5M"], - psychup: ["5M"], - psyshock: ["5M"], - raindance: ["5M"], - recycle: ["5T"], - reflect: ["5M"], - rest: ["5M"], + allyswitch: ["5M"], + ancientpower: ["7L44", "5L44"], + attract: ["7M", "5M"], + bugbite: ["7T", "7L7", "5T", "5L7"], + bugbuzz: ["7E", "5E"], + closecombat: ["7E", "5E"], + confide: ["7M"], + counter: ["7E", "5E"], + cut: ["6M", "5M"], + disable: ["7E", "5E"], + doubleteam: ["7M", "5M"], + dreameater: ["7M", "5M"], + echoedvoice: ["7M", "5M"], + electroweb: ["7T", "5T"], + facade: ["7M", "5M"], + feint: ["7E", "5E"], + finalgambit: ["7L38", "5L38"], + flash: ["6M", "5M"], + fling: ["7M", "5M"], + frustration: ["7M", "5M"], + hail: ["7M", "5M"], + healpulse: ["7L21", "5L21"], + helpinghand: ["7T", "5T"], + hiddenpower: ["7M", "5M"], + hydropump: ["7E", "5E"], + icywind: ["7T", "5T"], + infestation: ["7M"], + lightscreen: ["7M", "5M"], + magiccoat: ["7T", "5T"], + magicroom: ["7T", "5T"], + megahorn: ["7E", "5E"], + protect: ["7M", "5M"], + psychic: ["7M", "5M"], + psychup: ["7M", "5M"], + psyshock: ["7M", "5M"], + raindance: ["7M", "5M"], + recycle: ["7T", "5T"], + reflect: ["7M", "5M"], + rest: ["7M", "5M"], retaliate: ["5M"], - return: ["5M"], - roleplay: ["5T"], - round: ["5M"], - safeguard: ["5M", "5E"], - shadowball: ["5M"], - skillswap: ["5T"], - stringshot: ["5L1"], - strugglebug: ["5M", "5L27"], - substitute: ["5M"], - sunnyday: ["5M", "5L14"], - swagger: ["5M"], - tackle: ["5L1"], + return: ["7M", "5M"], + roleplay: ["7T", "5T"], + round: ["7M", "5M"], + safeguard: ["7M", "7E", "5M", "5E"], + secretpower: ["7M"], + shadowball: ["7M", "5M"], + shockwave: ["7M"], + signalbeam: ["7T"], + skillswap: ["7T", "5T"], + sleeptalk: ["7M"], + snore: ["7T"], + steelwing: ["7M"], + stringshot: ["7L1", "5L1"], + strugglebug: ["7L27", "5M"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "7L14", "5M", "5L14"], + swagger: ["7M", "5M"], + tackle: ["7L1", "5L1"], + tailglow: ["7L60"], telekinesis: ["5M"], - toxic: ["5M"], - trick: ["5T"], - willowisp: ["5M", "5L32"], - wingattack: ["5E"], - wish: ["5L48"], - wonderroom: ["5T"], - xscissor: ["5M"], - zenheadbutt: ["5T", "5L54"], + toxic: ["7M", "5M"], + trick: ["7T", "5T"], + waterpulse: ["7T"], + willowisp: ["7M", "7L32", "5M", "5L32"], + wingattack: ["7E", "5E"], + wish: ["7L48", "5L48"], + wonderroom: ["7T", "5T"], + xscissor: ["7M", "5M"], + zenheadbutt: ["7T", "5L54", "5T", "5L54"], }}, argalis: {learnset: { allyswitch: ["5M"], - ancientpower: ["5L47"], - attract: ["5M"], - bugbite: ["5L7"], - bugbuzz: ["5E"], - closecombat: ["5E"], - counter: ["5E"], - cut: ["5M"], - disable: ["5E"], - doubleteam: ["5M"], - dreameater: ["5M"], - echoedvoice: ["5M"], - electroweb: ["5T"], - facade: ["5M"], - feint: ["5E"], - finalgambit: ["5L41"], - flash: ["5M"], - fling: ["5M"], - focusblast: ["5M"], - frustration: ["5M"], - hail: ["5M"], - healpulse: ["5L21"], - helpinghand: ["5T"], - hiddenpower: ["5M"], - hydropump: ["5E"], - icebeam: ["5M"], - icywind: ["5T"], - lightscreen: ["5L57"], - magiccoat: ["5T"], - magicroom: ["5T"], - megahorn: ["5E"], - ominouswind: ["5L27"], - protect: ["5M"], - psychic: ["5M", "5L62"], - psychup: ["5M"], - psyshock: ["5M"], - raindance: ["5M"], - recycle: ["5T"], - reflect: ["5L57"], - rest: ["5M"], + ancientpower: ["7L47", "5L47"], + attract: ["7M", "5M"], + bugbite: ["7T", "7L7", "5T", "5L7"], + bugbuzz: ["7E", "5E"], + closecombat: ["7E", "5E"], + confide: ["7M"], + counter: ["7E", "5E"], + cut: ["6M", "5M"], + disable: ["7E", "5E"], + doubleteam: ["7M", "5M"], + dreameater: ["7M", "5M"], + echoedvoice: ["7M", "5M"], + electroweb: ["7T", "5T"], + facade: ["7M", "5M"], + feint: ["7E", "5E"], + finalgambit: ["7L41", "5L41"], + flash: ["6M", "5M"], + fling: ["7M", "5M"], + focusblast: ["7M", "5M"], + frustration: ["7M", "5M"], + hail: ["7M", "5M"], + healpulse: ["7L21", "5L21"], + helpinghand: ["7T", "5T"], + hiddenpower: ["7M", "5M"], + hydropump: ["7E", "5E"], + icebeam: ["7M", "5M"], + icywind: ["7T", "5T"], + infestation: ["7M"], + lightscreen: ["7M", "7L57", "5M"], + magiccoat: ["7T", "5T"], + magicroom: ["7T", "5T"], + megahorn: ["7E", "5E"], + ominouswind: ["7L27", "5L27"], + protect: ["7M", "5M"], + psychic: ["7M", "7L62", "5M"], + psychup: ["7M", "5M"], + psyshock: ["7M", "5M"], + raindance: ["7M", "5M"], + recycle: ["7T", "5T"], + reflect: ["7M", "7L57", "5M"], + rest: ["7M", "5M"], retaliate: ["5M"], - return: ["5M"], - roleplay: ["5T"], - round: ["5M"], - safeguard: ["5M", "5E"], - shadowball: ["5M"], - skillswap: ["5T"], - stringshot: ["5L1"], + return: ["7M", "5M"], + roleplay: ["7T", "5T"], + round: ["7M", "5M"], + safeguard: ["7M", "7E", "5M", "5E"], + secretpower: ["7M"], + shadowball: ["7M", "5M"], + shockwave: ["7M"], + signalbeam: ["7T"], + skillswap: ["7T", "5T"], + sleeptalk: ["7M"], + snore: ["7T"], + solarbeam: ["7M", "5M"], + spotlight: ["7L1"], + steelwing: ["7M"], + stringshot: ["7L1", "5L1"], strugglebug: ["5M"], - substitute: ["5M"], - sunnyday: ["5L14"], - swagger: ["5M"], - tackle: ["5L1"], + substitute: ["7M", "5M"], + sunnyday: ["7M", "7L14", "5M", "5L14"], + surf: ["7M", "5M"], + swagger: ["7M", "5M"], + tackle: ["7L1", "5L1"], + tailglow: ["7L65"], telekinesis: ["5M"], - thunderbolt: ["5M"], - toxic: ["5M"], - trick: ["5T"], - willowisp: ["5L34"], - wingattack: ["5E"], - wish: ["5L52"], - wonderroom: ["5T"], - xscissor: ["5M"], - zenheadbutt: ["5T"], + thunderbolt: ["7M", "5M"], + toxic: ["7M", "5M"], + trick: ["7T", "5T"], + waterpulse: ["7T"], + willowisp: ["7M", "7L34", "5M", "5L34"], + wingattack: ["7E", "5E"], + wish: ["7L54", "5L54"], + wonderroom: ["7T", "5T"], + xscissor: ["7M", "5M"], + zenheadbutt: ["7T", "5T"], }}, brattler: {learnset: { - aromatherapy: ["5E"], - attract: ["5M"], - beatup: ["5E"], - bind: ["5T"], - crunch: ["5L39"], - cut: ["5M"], - darkpulse: ["5T"], - doubleteam: ["5M"], - dragontail: ["5M"], - energyball: ["5M"], - facade: ["5M"], - foulplay: ["5T"], - frustration: ["5M"], - gigadrain: ["5T"], - glare: ["5E"], - grassknot: ["5L18"], - haze: ["5E"], - healbell: ["5T"], - hiddenpower: ["5M"], - irontail: ["5T"], - knockoff: ["5T"], - leafblade: ["5L34"], - nightslash: ["5E"], - payback: ["5M"], - poisonpowder: ["5E"], - powerwhip: ["5L50"], - protect: ["5M"], - punishment: ["5L55"], - pursuit: ["5L6", "5L1"], - rest: ["5M"], - retaliate: ["5M"], - return: ["5M"], - roar: ["5M"], - round: ["5M"], - scaryface: ["5E"], - screech: ["5E"], - seedbomb: ["5T"], - slam: ["5L30"], - sleeptalk: ["5T"], - snarl: ["5M"], - snore: ["5T"], - solarbeam: ["5M"], - spite: ["5T"], - strength: ["5M"], - stunspore: ["5E"], - substitute: ["5M"], - suckerpunch: ["5L25"], - sunnyday: ["5M"], - swagger: ["5M"], - sweetscent: ["5E"], - synthesis: ["5T"], - taunt: ["5M"], - thief: ["5M"], - toxic: ["5M"], - uturn: ["5M"], - vinewhip: ["5L1"], - wildcharge: ["5M"], - worryseed: ["5T"], - wrap: ["5L12", "5L1"], - wringout: ["5L44"], + aromatherapy: ["7E", "5E"], + attract: ["7M", "5M"], + beatup: ["7E", "5E"], + bind: ["7T", "5T"], + brutalswing: ["7M"], + confide: ["7M"], + crunch: ["7L43", "5L39"], + cut: ["6M", "5M"], + darkpulse: ["7M", "5T"], + doubleteam: ["7M", "5M"], + dragontail: ["7M", "5M"], + energyball: ["7M", "5M"], + facade: ["7M", "5M"], + feint: ["7E"], + foulplay: ["7T", "5T"], + frustration: ["7M", "5M"], + gigadrain: ["7T", "5T"], + glare: ["7L1", "7L31", "7E", "5E"], + grassknot: ["7M", "7L15", "5M", "5L18"], + haze: ["7E", "5E"], + healbell: ["7T", "5T"], + hiddenpower: ["7M", "5M"], + icefang: ["7E"], + irontail: ["7T", "5T"], + knockoff: ["7T", "5T"], + leafblade: ["7L26", "5L34"], + leer: ["7L1"], + naturepower: ["7M"], + nightslash: ["7E", "5E"], + payback: ["7M", "5M"], + poisonpowder: ["7E", "5E"], + powerwhip: ["7L44", "5L50"], + protect: ["7M", "5M"], + punishment: ["7L55", "5L55"], + pursuit: ["7L8", "5L6", "5L1"], + recycle: ["7T"], + rest: ["7M", "5M"], + retaliate: ["7M", "5M"], + return: ["7M", "5M"], + roar: ["7M", "5M"], + round: ["7M", "5M"], + scaryface: ["7L11", "7E", "5E"], + screech: ["7E", "5E"], + secretpower: ["7M"], + seedbomb: ["7T", "5T"], + slam: ["7L22", "5L30"], + sleeptalk: ["7M", "5T"], + snarl: ["7M", "5M"], + snore: ["7T", "5T"], + solarbeam: ["7M", "5M"], + spikyshield: ["7L40"], + spite: ["7T", "5T"], + strength: ["6M", "5M"], + stunspore: ["7E", "5E"], + substitute: ["7M", "5M"], + suckerpunch: ["7L18", "5L25"], + sunnyday: ["7M", "5M"], + swagger: ["7M", "5M"], + sweetscent: ["7E", "5E"], + synthesis: ["7T", "5T"], + taunt: ["7M", "5M"], + thief: ["7M", "5M"], + thunderfang: ["7E"], + toxic: ["7M", "5M"], + uturn: ["7M", "5M"], + vinewhip: ["7L5", "5L1"], + wildcharge: ["7M", "5M"], + worryseed: ["7T", "5T"], + wrap: ["7L1", "5L1"], + wringout: ["7L49", "5L44"], }}, cawdet: {learnset: { acrobatics: ["5M"], @@ -64431,6 +64539,58 @@ let BattleLearnsets = { workup: ["7M"], wrap: ["7L1", "4L1"], }}, + mumbao: {learnset: { + attract: ["7M"], + bodyslam: ["7L28"], + confide: ["7M"], + dazzlinggleam: ["7M"], + doubleteam: ["7M"], + energyball: ["7M", "7L37"], + facade: ["7M"], + flowershield: ["7L1"], + focusblast: ["7M"], + frustration: ["7M"], + gigadrain: ["7T"], + grassknot: ["7M"], + grassyterrain: ["7E"], + gravity: ["7T"], + gyroball: ["7M"], + healingwish: ["7E"], + helpinghand: ["7T", "7L35"], + hiddenpower: ["7M"], + hyperbeam: ["7M"], + ingrain: ["7L10"], + leafstorm: ["7L46"], + leafage: ["7L13"], + lightscreen: ["7M"], + luckychant: ["7L17"], + magiccoat: ["7T"], + magicalleaf: ["7L19"], + mistyterrain: ["7E"], + moonblast: ["7L44"], + naturalgift: ["7L31"], + protect: ["7M"], + psychup: ["7M", "7L26"], + rest: ["7M"], + return: ["7M"], + rototiller: ["7L8"], + round: ["7M"], + safeguard: ["7M"], + sandstorm: ["7M"], + seedbomb: ["7T"], + shadowball: ["7M"], + sleeptalk: ["7M"], + snore: ["7T"], + solarbeam: ["7M"], + substitute: ["7M"], + sunnyday: ["7M", "7L40"], + swagger: ["7M"], + synthesis: ["7T"], + tackle: ["7L4"], + toxic: ["7M"], + wish: ["7L22"], + worryseed: ["7T"], + }}, }; exports.BattleLearnsets = BattleLearnsets; diff --git a/data/pokedex.js b/data/pokedex.js index 8ecc8b408209..a4189c4bb980 100644 --- a/data/pokedex.js +++ b/data/pokedex.js @@ -13408,7 +13408,7 @@ let BattlePokedex = { color: "Brown", prevo: "voodoll", evoLevel: 32, - eggGroups: ["Human-Like", "Ground"], + eggGroups: ["Human-Like", "Field"], }, tomohawk: { num: -12, @@ -13587,6 +13587,8 @@ let BattlePokedex = { heightm: 2.4, weightkg: 600, color: "Brown", + prevo: "mumbao", + evoLevel: 2, eggGroups: ["Grass"], }, syclar: { @@ -13594,7 +13596,7 @@ let BattlePokedex = { species: "Syclar", types: ["Ice", "Bug"], baseStats: {hp: 40, atk: 76, def: 45, spa: 74, spd: 39, spe: 91}, - abilities: {0: "Compound Eyes", 1: "Snow Cloak"}, + abilities: {0: "Compound Eyes", 1: "Snow Cloak", H: "Ice Body"}, heightm: 0.2, weightkg: 4.0, color: "Blue", @@ -13606,7 +13608,7 @@ let BattlePokedex = { species: "Embirch", types: ["Fire", "Grass"], baseStats: {hp: 60, atk: 40, def: 55, spa: 65, spd: 40, spe: 60}, - abilities: {0: "Reckless", 1: "Leaf Guard"}, + abilities: {0: "Reckless", 1: "Leaf Guard", H: "Chlorophyll"}, heightm: 0.6, weightkg: 15, color: "Brown", @@ -13618,7 +13620,7 @@ let BattlePokedex = { species: "Flarelm", types: ["Fire", "Grass"], baseStats: {hp: 90, atk: 50, def: 95, spa: 75, spd: 70, spe: 40}, - abilities: {0: "Rock Head", 1: "Battle Armor"}, + abilities: {0: "Rock Head", 1: "Battle Armor", H: "White Smoke"}, heightm: 1.4, weightkg: 73, color: "Brown", @@ -13632,7 +13634,7 @@ let BattlePokedex = { species: "Breezi", types: ["Poison", "Flying"], baseStats: {hp: 50, atk: 46, def: 69, spa: 60, spd: 50, spe: 75}, - abilities: {0: "Unburden", 1: "Own Tempo"}, + abilities: {0: "Unburden", 1: "Own Tempo", H: "Frisk"}, heightm: 0.4, weightkg: 0.6, color: "Purple", @@ -13669,7 +13671,7 @@ let BattlePokedex = { species: "Cupra", types: ["Bug", "Psychic"], baseStats: {hp: 50, atk: 60, def: 49, spa: 67, spd: 30, spe: 44}, - abilities: {0: "Shield Dust", 1: "Keen Eye"}, + abilities: {0: "Shield Dust", 1: "Keen Eye", H: "Magic Guard"}, heightm: 0.5, weightkg: 4.8, color: "Brown", @@ -13681,7 +13683,7 @@ let BattlePokedex = { species: "Argalis", types: ["Bug", "Psychic"], baseStats: {hp: 60, atk: 90, def: 89, spa: 87, spd: 40, spe: 54}, - abilities: {0: "Shed Skin", 1: "Compound Eyes"}, + abilities: {0: "Shed Skin", 1: "Compound Eyes", H: "Overcoat"}, heightm: 1.3, weightkg: 341.4, color: "Gray", @@ -13695,7 +13697,7 @@ let BattlePokedex = { species: "Brattler", types: ["Dark", "Grass"], baseStats: {hp: 80, atk: 70, def: 40, spa: 20, spd: 90, spe: 30}, - abilities: {0: "Harvest", 1: "Infiltrator"}, + abilities: {0: "Harvest", 1: "Infiltrator", H: "Rattled"}, heightm: 1.8, weightkg: 11.5, color: "Brown", @@ -13826,7 +13828,19 @@ let BattlePokedex = { weightkg: 25, color: "Brown", evos: ["voodoom"], - eggGroups: ["Human-Like", "Ground"], + eggGroups: ["Human-Like", "Field"], + }, + mumbao: { + num: -120, + species: "Mumbao", + types: ["Grass", "Fairy"], + baseStats: {hp: 55, atk: 30, def: 64, spa: 87, spd: 73, spe: 66}, + abilities: {0: "Solar Power", 1: "Trace", H: "Overcoat"}, + heightm: 1, + weightkg: 250, + color: "Brown", + evos: ["jumbao"], + eggGroups: ["Grass"], }, pokestarsmeargle: { num: -5000, From ae526834a21996e41eab7b38864bc369cd30ee75 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 5 Sep 2018 21:34:32 -0500 Subject: [PATCH 037/125] Fix uhtmlchange Roomlogs function Caught by LGTM: https://lgtm.com/projects/g/Zarel/Pokemon-Showdown/snapshot/fc22f4fb8d8534fa0a63ff07946a206755f53d0a/files/roomlogs.js#xa1bd0a177ef2410d:1 --- roomlogs.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roomlogs.js b/roomlogs.js index 42907bbcc8f7..197842143e33 100644 --- a/roomlogs.js +++ b/roomlogs.js @@ -187,11 +187,11 @@ class Roomlog { * @param {string} message */ uhtmlchange(message) { - let thirdPipe = message.indexOf('|', 13); - let originalStart = '|uhtml|' + message.slice(13, thirdPipe + 1); - for (let line of this.log) { + const thirdPipe = message.indexOf('|', 13); + const originalStart = '|uhtml|' + message.slice(13, thirdPipe + 1); + for (const [i, line] of this.log.entries()) { if (line.startsWith(originalStart)) { - line = originalStart + message.slice(thirdPipe + 1); + this.log[i] = originalStart + message.slice(thirdPipe + 1); break; } } From 9c87d652ec9f5aa4c0b45d8462b999d603369990 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 5 Sep 2018 23:10:51 -0500 Subject: [PATCH 038/125] Refactor /coverage The new version should be more readable. --- chat-plugins/info.js | 62 +++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index b613a53531e4..f15d3889fed4 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -814,46 +814,54 @@ const commands = { bestCoverage[type] = -5; } - for (const arg of targets) { - let move = arg.trim(); - if (toId(move) === mod.currentMod) continue; - move = move.charAt(0).toUpperCase() + move.slice(1).toLowerCase(); - if (move === 'Table' || move === 'All') { + for (let arg of targets) { + arg = toId(arg); + + // arg is the gen? + if (arg === mod.currentMod) continue; + + // arg is 'table' or 'all'? + if (arg === 'table' || arg === 'all') { if (this.broadcasting) return this.sendReplyBox("The full table cannot be broadcast."); dispTable = true; continue; } + // arg is a type? + let type = arg.charAt(0).toUpperCase() + arg.slice(1); let eff; - if (move in mod.data.TypeChart) { - sources.push(move); + if (type in mod.data.TypeChart) { + sources.push(type); for (let type in bestCoverage) { - if (!mod.getImmunity(move, type) && !move.ignoreImmunity) continue; - eff = mod.getEffectiveness(move, type); + if (!mod.getImmunity(type, type) && !type.ignoreImmunity) continue; + eff = mod.getEffectiveness(type, type); if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; } - move = mod.getMove(move); - if (move.exists && move.gen <= mod.gen) { - if (!move.basePower && !move.basePowerCallback) continue; - if (move.id === 'thousandarrows') hasThousandArrows = true; - sources.push(move); - for (let type in bestCoverage) { - if (move.id === "struggle") { - eff = 0; - } else { - if (!mod.getImmunity(move.type, type) && !move.ignoreImmunity) continue; - let baseMod = mod.getEffectiveness(move, type); - let moveMod = move.onEffectiveness && move.onEffectiveness.call(mod, baseMod, type, move); - eff = typeof moveMod === 'number' ? moveMod : baseMod; - } - if (eff > bestCoverage[type]) bestCoverage[type] = eff; - } - continue; + + // arg is a move? + let move = mod.getMove(arg); + if (!move.exists) { + return this.errorReply(`Type or move '${arg}' not found.`); + } else if (move.gen > mod.gen) { + return this.errorReply(`Move '${arg}' is not available in Gen ${mod.gen}.`); } - return this.errorReply(`No type or move '${arg}' found${Dex.gen > mod.gen ? ` in Gen ${mod.gen}` : ""}.`); + if (!move.basePower && !move.basePowerCallback) continue; + if (move.id === 'thousandarrows') hasThousandArrows = true; + sources.push(move); + for (let type in bestCoverage) { + if (move.id === "struggle") { + eff = 0; + } else { + if (!mod.getImmunity(move.type, type) && !move.ignoreImmunity) continue; + let baseMod = mod.getEffectiveness(move, type); + let moveMod = move.onEffectiveness && move.onEffectiveness.call(mod, baseMod, type, move); + eff = typeof moveMod === 'number' ? moveMod : baseMod; + } + if (eff > bestCoverage[type]) bestCoverage[type] = eff; + } } if (sources.length === 0) return this.errorReply("No moves using a type table for determining damage were specified."); if (sources.length > 4) return this.errorReply("Specify a maximum of 4 moves or types."); From eb28444f7963080f3e49fafc53a81a673f4f7c6a Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Thu, 6 Sep 2018 00:03:59 -0500 Subject: [PATCH 039/125] Fix /learn There were previously bugs with specifying formats or pentagon requirements. Bug caught by LGTM: https://lgtm.com/projects/g/Zarel/Pokemon-Showdown/snapshot/fc22f4fb8d8534fa0a63ff07946a206755f53d0a/files/chat-plugins/datasearch.js?sort=name&dir=ASC&mode=heatmap&showExcluded=true#xeb305f9d77a01f51:1 --- chat-plugins/datasearch.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/chat-plugins/datasearch.js b/chat-plugins/datasearch.js index 29ec6f207972..f3c7e0a88c84 100644 --- a/chat-plugins/datasearch.js +++ b/chat-plugins/datasearch.js @@ -1424,6 +1424,7 @@ function runLearn(target, cmd) { format = Dex.getFormat(targetid); formatid = targetid; formatName = format.name; + targets.shift(); } if (targetid.startsWith('gen') && parseInt(targetid.charAt(3))) { gen = parseInt(targetid.slice(3)); @@ -1440,12 +1441,14 @@ function runLearn(target, cmd) { } break; } - if (!formatid) format = new Dex.Data.Format(format); - if (!formatid) formatid = 'gen' + gen + 'ou'; - if (!formatName) formatName = 'Gen ' + gen; + if (!formatName) { + format = new Dex.Data.Format(format); + formatName = 'Gen ' + gen; + if (format.requirePentagon) formatName += ' Pentagon'; + } let lsetData = {set: {}, sources: [], sourcesBefore: gen}; - const validator = TeamValidator(formatid); + const validator = TeamValidator(format); let template = validator.dex.getTemplate(targets.shift()); let move = {}; let all = (cmd === 'learnall'); From ae03a15c744b68b264bba1ab71075cd7a5ce8528 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Thu, 6 Sep 2018 00:19:35 -0500 Subject: [PATCH 040/125] Fix misc bugs discovered by LGTM --- chat-commands.js | 22 ++++++++++------------ chat-formatter.js | 2 +- chat-plugins/info.js | 3 +-- sim/battle.js | 4 ++-- sim/dex.js | 12 ++++++------ users.js | 2 +- 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/chat-commands.js b/chat-commands.js index 3afaa6546b71..c15210f41a9c 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -777,9 +777,7 @@ const commands = { let roomid = target.trim(); if (!roomid) { // allow deleting personal rooms without typing out the room name - if (room.isPersonal && cmd === "deletegroupchat") { - roomid = room.id; - } else { + if (!room.isPersonal || cmd !== "deletegroupchat") { return this.parse(`/help deleteroom`); } } else { @@ -1063,7 +1061,7 @@ const commands = { } if (!this.can('declare')) return false; if (target.length > 80) return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`); - let normalizedTarget = ' ' + target.toLowerCase().replace('[^a-zA-Z0-9]+', ' ').trim() + ' '; + let normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' '; if (normalizedTarget.includes(' welcome ')) { return this.errorReply(`Error: Room description must not contain the word "welcome".`); @@ -1263,6 +1261,7 @@ const commands = { } if (!target) return this.parse('/help roomowner'); target = this.splitTarget(target, true); + if (target) return this.errorReply(`This command does not support specifying a reason.`); let targetUser = this.targetUser; let name = this.targetUsername; let userid = toId(name); @@ -1737,6 +1736,7 @@ const commands = { unmute: function (target, room, user) { if (!target) return this.parse('/help unmute'); target = this.splitTarget(target); + if (target) return this.errorReply(`This command does not support specifying a reason.`); if (!this.canTalk()) return; if (!this.can('mute', null, room)) return false; @@ -2224,6 +2224,7 @@ const commands = { if (!this.can('promote')) return; target = this.splitTarget(target, true); + if (target) return this.errorReply(`This command does not support specifying a reason.`); let targetUser = this.targetUser; let userid = toId(this.targetUsername); let name = targetUser ? targetUser.name : this.targetUsername; @@ -3290,7 +3291,7 @@ const commands = { // errors can occur while rebasing or popping the stash; make sure to recover try { this.sendReply(`Rebasing...`); - [code, stdout, stderr] = await exec(`git rebase FETCH_HEAD`); + [code] = await exec(`git rebase FETCH_HEAD`); if (code) { // conflict while rebasing await exec(`git rebase --abort`); @@ -3299,7 +3300,7 @@ const commands = { if (stashedChanges) { this.sendReply(`Restoring saved changes...`); - [code, stdout, stderr] = await exec(`git stash pop`); + [code] = await exec(`git stash pop`); if (code) { // conflict while popping stash await exec(`git reset HEAD .`); @@ -3802,12 +3803,8 @@ const commands = { let targetUser = Users.getExact(target); if (!targetUser) return this.errorReply(`User '${target}' not found.`); - target = targetUser ? targetUser.userid : ''; - - if (target) { - room.battle.win(targetUser); - this.modlog('FORCEWIN', target); - } + room.battle.win(targetUser); + this.modlog('FORCEWIN', targetUser.userid); }, forcewinhelp: [ `/forcetie - Forces the current match to end in a tie. Requires: & ~`, @@ -3901,6 +3898,7 @@ const commands = { '!accept': true, accept: function (target, room, user, connection) { target = this.splitTarget(target); + if (target) return this.popupReply(`This command does not support specifying multiple users`); const targetUser = this.targetUser || this.pmTarget; if (!targetUser) return this.popupReply(`User "${this.targetUsername}" not found.`); Ladders.acceptChallenge(connection, targetUser); diff --git a/chat-formatter.js b/chat-formatter.js index 70bdd54b7a4a..98577e8cf1d6 100644 --- a/chat-formatter.js +++ b/chat-formatter.js @@ -64,7 +64,7 @@ class TextFormatter { // filter links first str = str.replace(linkRegex, uri => { - let fulluri = uri; + let fulluri; if (/^[a-z0-9.]+@/ig.test(uri)) { fulluri = 'mailto:' + uri; } else { diff --git a/chat-plugins/info.js b/chat-plugins/info.js index f15d3889fed4..54a969ccf095 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -412,7 +412,6 @@ const commands = { let pokemon = Dex.getTemplate(p); if (pokemon.num === targetNum) { target = pokemon.species; - targetId = pokemon.id; break; } } @@ -1536,7 +1535,7 @@ const commands = { break; } - if (!totalMatches) return this.errorReply("No " + (target ? "matched " : "") + "formats found."); + if (!totalMatches) return this.errorReply("No matched formats found."); if (totalMatches === 1) { let rules = []; let rulesetHtml = ''; diff --git a/sim/battle.js b/sim/battle.js index 542b55a246a4..6f5eba6b6913 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -214,9 +214,9 @@ class Battle extends Dex.ModdedDex { let result = this.runEvent('SetWeather', source, source, status); if (!result) { if (result === false) { - if (source && sourceEffect && sourceEffect.weather) { + if (sourceEffect && sourceEffect.weather) { this.add('-fail', source, sourceEffect, '[from]: ' + this.weather); - } else if (source && sourceEffect && sourceEffect.effectType === 'Ability') { + } else if (sourceEffect && sourceEffect.effectType === 'Ability') { this.add('-ability', source, sourceEffect, '[from] ' + this.weather, '[fail]'); } } diff --git a/sim/dex.js b/sim/dex.js index 949247f97358..d4b85a4cfc03 100644 --- a/sim/dex.js +++ b/sim/dex.js @@ -1041,21 +1041,20 @@ class ModdedDex { return false; } - /** @type {DataType[]} */ searchIn = searchIn || ['Pokedex', 'Movedex', 'Abilities', 'Items', 'Natures']; let searchFunctions = {Pokedex: 'getTemplate', Movedex: 'getMove', Abilities: 'getAbility', Items: 'getItem', Natures: 'getNature'}; let searchTypes = {Pokedex: 'pokemon', Movedex: 'move', Abilities: 'ability', Items: 'item', Natures: 'nature'}; /** @type {AnyObject[] | false} */ let searchResults = []; - for (const result of searchIn) { + for (const table of searchIn) { /** @type {AnyObject} */ // @ts-ignore - let res = this[searchFunctions[result]](target); + let res = this[searchFunctions[table]](target); if (res.exists && res.gen <= this.gen) { searchResults.push({ isInexact: isInexact, - searchType: searchTypes[result], + searchType: searchTypes[table], name: res.species ? res.species : res.name, }); } @@ -1077,8 +1076,9 @@ class ModdedDex { maxLd = 2; } searchResults = false; - for (let i = 0; i <= searchIn.length; i++) { - let searchObj = this.data[searchIn[i] || 'Aliases']; + for (const table of [...searchIn, 'Aliases']) { + // @ts-ignore + let searchObj = this.data[table]; if (!searchObj) { continue; } diff --git a/users.js b/users.js index b3cf7976dc15..79a3b4c8e70e 100644 --- a/users.js +++ b/users.js @@ -116,7 +116,7 @@ function merge(user1, user2) { function getUser(name, exactName = false) { if (!name || name === '!') return null; // @ts-ignore - if (name && name.userid) return name; + if (name.userid) return name; let userid = toId(name); let i = 0; if (!exactName) { From 50c2413d0d9a6b672a93e25493ee9fbb04abfcb7 Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Thu, 6 Sep 2018 04:39:37 -0700 Subject: [PATCH 041/125] Random Battle: Update Empoleon (#4844) Hidden Power Fire has virtually no useful coverage for a Pokemon that should always have a Water move, and Defog is a popular move on Empoleon. Also add rejection for Defog+Stealth Rock similar to the already-existing Defog+Spikes rejection. --- data/formats-data.js | 2 +- data/random-teams.js | 2 +- mods/gen6/formats-data.js | 2 +- mods/gen6/random-teams.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/formats-data.js b/data/formats-data.js index df337da98912..4f0fb6324ef0 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -4055,7 +4055,7 @@ let BattleFormatsData = { tier: "NFE", }, empoleon: { - randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "hiddenpowerfire", "icebeam", "scald", "toxic", "roar", "stealthrock"], + randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "defog", "icebeam", "scald", "toxic", "roar", "stealthrock"], randomDoubleBattleMoves: ["icywind", "scald", "surf", "icebeam", "hiddenpowerelectric", "protect", "grassknot", "flashcannon"], eventPokemon: [ {"generation": 5, "level": 100, "gender": "M", "isHidden": false, "moves": ["hydropump", "icebeam", "aquajet", "grassknot"], "pokeball": "cherishball"}, diff --git a/data/random-teams.js b/data/random-teams.js index 9de79b7d8b8e..b2c63336f0a9 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -751,7 +751,7 @@ class RandomTeams extends Dex.ModdedDex { if (!!counter['speedsetup'] || hasMove['encore'] || hasMove['raindance'] || hasMove['roar'] || hasMove['whirlwind']) rejected = true; break; case 'defog': - if (counter.setupType || hasMove['spikes'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; + if (counter.setupType || hasMove['spikes'] || hasMove['stealthrock'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; break; case 'fakeout': if (counter.setupType || hasMove['substitute'] || hasMove['switcheroo'] || hasMove['trick']) rejected = true; diff --git a/mods/gen6/formats-data.js b/mods/gen6/formats-data.js index 3ed53eaee5c4..44305e9fda2e 100644 --- a/mods/gen6/formats-data.js +++ b/mods/gen6/formats-data.js @@ -2816,7 +2816,7 @@ let BattleFormatsData = { }, empoleon: { inherit: true, - randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "hiddenpowerfire", "icebeam", "scald", "toxic", "roar", "stealthrock"], + randomBattleMoves: ["hydropump", "flashcannon", "grassknot", "defog", "icebeam", "scald", "toxic", "roar", "stealthrock"], randomDoubleBattleMoves: ["icywind", "scald", "surf", "icebeam", "hiddenpowerelectric", "protect", "grassknot", "flashcannon"], tier: "UU", doublesTier: "DUU", diff --git a/mods/gen6/random-teams.js b/mods/gen6/random-teams.js index 85ac23024a29..90a63b3bece7 100644 --- a/mods/gen6/random-teams.js +++ b/mods/gen6/random-teams.js @@ -192,7 +192,7 @@ class RandomGen6Teams extends RandomTeams { if (!!counter['speedsetup'] || hasMove['encore'] || hasMove['raindance'] || hasMove['roar'] || hasMove['whirlwind']) rejected = true; break; case 'defog': - if (counter.setupType || hasMove['spikes'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; + if (counter.setupType || hasMove['spikes'] || hasMove['stealthrock'] || (hasMove['rest'] && hasMove['sleeptalk']) || teamDetails.hazardClear) rejected = true; break; case 'fakeout': if (counter.setupType || hasMove['substitute'] || hasMove['switcheroo'] || hasMove['trick']) rejected = true; From 74ab9baa9815ce9359bbf822c67af09d363aebdc Mon Sep 17 00:00:00 2001 From: whales Date: Sat, 8 Sep 2018 13:38:45 +0930 Subject: [PATCH 042/125] UNO: Fix crash on DQing player with uno --- chat-plugins/uno.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chat-plugins/uno.js b/chat-plugins/uno.js index c4636db5a52a..8e6f35fdf5d9 100644 --- a/chat-plugins/uno.js +++ b/chat-plugins/uno.js @@ -266,6 +266,7 @@ class UnoGame extends Rooms.RoomGame { if (this.timer) clearTimeout(this.timer); this.nextTurn(); } + if (this.awaitUno === userid) delete this.awaitUno; // put that player's cards into the discard pile to prevent cards from being permanently lost this.discards.push(...this.players[userid].hand); From c494ac362a12c86713354681440ddae2038f79a7 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Sat, 8 Sep 2018 12:05:55 -0400 Subject: [PATCH 043/125] "Fix" the second turn of called multi-turn moves TODO: Not this. Calling moves that select multi-turn moves currently use the target of the calling move on the second turn, which is probably the user and would otherwise fail if not for being re-targeted again. Maybe store a locking move's randomly selected target in a Pokemon or move volatile instead. --- sim/battle.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sim/battle.js b/sim/battle.js index 6f5eba6b6913..5ea8c1bdf8ab 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -2397,7 +2397,8 @@ class Battle extends Dex.ModdedDex { move = this.getMove(move); let target; // Fails if the target is the user and the move can't target its own position - if (['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === -(pokemon.position + 1)) { + if (['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === -(pokemon.position + 1) && + !pokemon.volatiles['twoturnmove'] && !pokemon.volatiles['iceball'] && !pokemon.volatiles['rollout']) { if (move.isFutureMove) return pokemon; return false; } From dfd08e6d8dc60955ebdfa010ea41fcc60950ab18 Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Sat, 8 Sep 2018 10:41:22 -0400 Subject: [PATCH 044/125] Add more information to /help blacklist Alot of Room Owners have trouble remembering the /showblacklist command. This change makes it easier to find the command. --- chat-commands.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chat-commands.js b/chat-commands.js index c15210f41a9c..1e22e4297bf0 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -2541,7 +2541,12 @@ const commands = { } return true; }, - blacklisthelp: [`/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`], + blacklisthelp: [ + `/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`, + `/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`, + `/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`, + `/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`, + ], battleban: function (target, room, user, connection) { if (!target) return this.parse(`/help battleban`); From 7b58e11613fe424f7030f96a0fae975bbb730417 Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Sat, 8 Sep 2018 12:11:24 -0400 Subject: [PATCH 045/125] Uno: Don't delete properties --- chat-plugins/uno.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/chat-plugins/uno.js b/chat-plugins/uno.js index 8e6f35fdf5d9..c9265b48ddac 100644 --- a/chat-plugins/uno.js +++ b/chat-plugins/uno.js @@ -128,6 +128,10 @@ class UnoGame extends Rooms.RoomGame { this.discards = []; /** @type {Card?} */ this.topCard = null; + /** @type {string?} */ + this.awaitUno = null; + /** @type {string?} */ + this.unoId = null; this.direction = 1; @@ -266,7 +270,7 @@ class UnoGame extends Rooms.RoomGame { if (this.timer) clearTimeout(this.timer); this.nextTurn(); } - if (this.awaitUno === userid) delete this.awaitUno; + if (this.awaitUno === userid) this.awaitUno = null; // put that player's cards into the discard pile to prevent cards from being permanently lost this.discards.push(...this.players[userid].hand); @@ -341,7 +345,7 @@ class UnoGame extends Rooms.RoomGame { this.sendToRoom(`|c:|${(Math.floor(Date.now() / 1000))}|~|${player.name}'s turn.`); this.state = 'play'; - if (player.cardLock) delete player.cardLock; + if (player.cardLock) player.cardLock = null; player.sendDisplay(); this.timer = setTimeout(() => { @@ -559,8 +563,8 @@ class UnoGame extends Rooms.RoomGame { // uno id makes spamming /uno uno impossible if (this.unoId !== unoId || player.userid !== this.awaitUno) return false; this.sendToRoom(Chat.html`|raw|UNO! ${player.name} is down to their last card!`); - delete this.awaitUno; - delete this.unoId; + this.awaitUno = null; + this.unoId = null; } onCheckUno() { @@ -570,8 +574,8 @@ class UnoGame extends Rooms.RoomGame { this.sendToRoom(`${this.players[this.awaitUno].name} forgot to say UNO! and is forced to draw 2 cards.`); this.onDrawCard(this.players[this.awaitUno], 2); } - delete this.awaitUno; - delete this.unoId; + this.awaitUno = null; + this.unoId = null; } } @@ -615,7 +619,8 @@ class UnoGamePlayer extends Rooms.RoomGamePlayer { super(user, game); this.hand = /** @type {Card[]} */ ([]); this.game = game; - this.cardLock = ''; + /** @type {string?} */ + this.cardLock = null; } /** From d154a856c909bc570f692422aee3992b02befa35 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Sun, 9 Sep 2018 00:32:35 +0400 Subject: [PATCH 046/125] Update UU VR thread --- config/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/formats.js b/config/formats.js index 455fd6fc6b46..1fd6b185a03e 100644 --- a/config/formats.js +++ b/config/formats.js @@ -60,7 +60,7 @@ let Formats = [ name: "[Gen 7] UU", threads: [ `• UU Metagame Discussion`, - `• UU Viability Rankings`, + `• UU Viability Rankings`, `• UU Sample Teams`, ], From 2395816d8b93cfbf7f053de5a4a1f4176544bdba Mon Sep 17 00:00:00 2001 From: asgdf Date: Sun, 9 Sep 2018 18:05:47 +0200 Subject: [PATCH 047/125] Include dance move flag in /dt and /ms (#4847) --- chat-plugins/datasearch.js | 2 +- chat-plugins/info.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chat-plugins/datasearch.js b/chat-plugins/datasearch.js index f3c7e0a88c84..2fd83f11a705 100644 --- a/chat-plugins/datasearch.js +++ b/chat-plugins/datasearch.js @@ -755,7 +755,7 @@ function runMovesearch(target, cmd, canAll, message) { let allCategories = ['physical', 'special', 'status']; let allContestTypes = ['beautiful', 'clever', 'cool', 'cute', 'tough']; let allProperties = ['basePower', 'accuracy', 'priority', 'pp']; - let allFlags = ['authentic', 'bite', 'bullet', 'contact', 'defrost', 'powder', 'protect', 'pulse', 'punch', 'secondary', 'snatch', 'sound']; + let allFlags = ['authentic', 'bite', 'bullet', 'contact', 'dance', 'defrost', 'powder', 'protect', 'pulse', 'punch', 'secondary', 'snatch', 'sound']; let allStatus = ['psn', 'tox', 'brn', 'par', 'frz', 'slp']; let allVolatileStatus = ['flinch', 'confusion', 'partiallytrapped']; let allBoosts = ['hp', 'atk', 'def', 'spa', 'spd', 'spe', 'accuracy', 'evasion']; diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 54a969ccf095..789a0a4bb482 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -551,6 +551,7 @@ const commands = { if (move.flags['powder']) details["✓ Powder"] = ""; if (move.flags['reflectable']) details["✓ Bounceable"] = ""; if (move.flags['gravity'] && mod.gen >= 4) details["✗ Suppressed by Gravity"] = ""; + if (move.flags['dance'] && mod.gen >= 7) details["✓ Dance move"] = ""; if (mod.gen >= 7) { if (move.zMovePower) { From 550ec66d379e508bf2f3dfcf749b15885da86d1a Mon Sep 17 00:00:00 2001 From: asgdf Date: Sun, 9 Sep 2018 18:14:39 +0200 Subject: [PATCH 048/125] Allow /dt to find CAPmons by their dex number (#4848) --- chat-plugins/info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 789a0a4bb482..2c1c13616bdd 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -406,7 +406,7 @@ const commands = { target = sep[0].trim(); let targetId = toId(target); if (!targetId) return this.parse('/help data'); - let targetNum = parseInt(targetId); + let targetNum = parseInt(target); if (!isNaN(targetNum) && '' + targetNum === target) { for (let p in Dex.data.Pokedex) { let pokemon = Dex.getTemplate(p); From b7aa41a006bd7c5498574ff9b45899c726f7f7a1 Mon Sep 17 00:00:00 2001 From: asgdf Date: Mon, 10 Sep 2018 04:13:50 +0200 Subject: [PATCH 049/125] Fix /coverage with type arguments (#4849) --- chat-plugins/info.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 2c1c13616bdd..364abab27d41 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -828,13 +828,13 @@ const commands = { } // arg is a type? - let type = arg.charAt(0).toUpperCase() + arg.slice(1); + let argType = arg.charAt(0).toUpperCase() + arg.slice(1); let eff; - if (type in mod.data.TypeChart) { - sources.push(type); + if (argType in mod.data.TypeChart) { + sources.push(argType); for (let type in bestCoverage) { - if (!mod.getImmunity(type, type) && !type.ignoreImmunity) continue; - eff = mod.getEffectiveness(type, type); + if (!mod.getImmunity(argType, type)) continue; + eff = mod.getEffectiveness(argType, type); if (eff > bestCoverage[type]) bestCoverage[type] = eff; } continue; From 6c6c898a59a61fd12c6bcc43757138af93a4050a Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 10 Sep 2018 22:19:10 +0400 Subject: [PATCH 050/125] 2v2: Ban Z-Crystals --- config/formats.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/formats.js b/config/formats.js index 1fd6b185a03e..bb37f734553f 100644 --- a/config/formats.js +++ b/config/formats.js @@ -825,6 +825,10 @@ let Formats = [ }, ruleset: ['[Gen 7] Doubles OU', 'Accuracy Moves Clause', 'Sleep Clause Mod'], banlist: ['Salamence-Mega', 'Tapu Lele', 'Focus Sash', 'Final Gambit', 'Perish Song'], + onValidateSet: function (set) { + const item = this.getItem(set.item); + if (item.zMove) return [(set.name || set.species) + "'s item " + item.name + " is banned."]; + }, }, { name: "[Gen 6] Gen-NEXT OU", From f4e2b4fbed83deef415402e23c9b4548951f8fbe Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 10 Sep 2018 22:28:09 +0400 Subject: [PATCH 051/125] September tier shifts --- config/formats.js | 11 +++++------ data/formats-data.js | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/formats.js b/config/formats.js index bb37f734553f..f791fb734a00 100644 --- a/config/formats.js +++ b/config/formats.js @@ -776,12 +776,11 @@ let Formats = [ banlist: [ // PU 'Absol', 'Aggron', 'Altaria', 'Archeops', 'Aromatisse', 'Articuno', 'Audino', 'Aurorus', 'Claydol', 'Clefairy', - 'Drampa', 'Eelektross', 'Exeggutor-Alola', 'Floatzel', 'Froslass', 'Golurk', 'Gourgeist-Super', 'Granbull', - 'Gurdurr', 'Haunter', 'Hitmonchan', 'Kangaskhan', 'Kingler', 'Komala', 'Lanturn', 'Liepard', 'Lilligant', - 'Lycanroc-Base', 'Manectric', 'Mesprit', 'Mudsdale', 'Omastar', 'Oricorio-Sensu', 'Passimian', 'Persian-Alola', - 'Poliwrath', 'Primeape', 'Probopass', 'Pyukumuku', 'Quagsire', 'Qwilfish', 'Raichu-Alola', 'Regirock', - 'Sableye', 'Sandslash-Alola', 'Scyther', 'Silvally-Fairy', 'Silvally-Ghost', 'Skuntank', 'Spiritomb', - 'Stoutland', 'Swanna', 'Togedemaru', 'Weezing', 'Zangoose', + 'Drampa', 'Eelektross', 'Exeggutor-Alola', 'Floatzel', 'Froslass', 'Golurk', 'Gourgeist-Super', 'Gurdurr', + 'Haunter', 'Hitmonchan', 'Kangaskhan', 'Kingler', 'Komala', 'Lanturn', 'Liepard', 'Lilligant', 'Lycanroc-Base', + 'Manectric', 'Mesprit', 'Mudsdale', 'Omastar', 'Oricorio-Sensu', 'Passimian', 'Persian-Alola', 'Poliwrath', + 'Primeape', 'Pyukumuku', 'Quagsire', 'Qwilfish', 'Raichu-Alola', 'Regirock', 'Sableye', 'Sandslash-Alola', + 'Scyther', 'Silvally-Fairy', 'Silvally-Ghost', 'Skuntank', 'Spiritomb', 'Swanna', 'Togedemaru', 'Weezing', 'Zangoose', // ZUBL 'Carracosta', 'Crabominable', 'Exeggutor-Base', 'Gorebyss', 'Jynx', 'Kabutops', 'Ludicolo', 'Musharna', 'Raticate-Alola', 'Raticate-Alola-Totem', 'Throh', 'Turtonator', 'Type: Null', 'Ursaring', 'Victreebel', diff --git a/data/formats-data.js b/data/formats-data.js index 4f0fb6324ef0..5b9e6a8e56a6 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -471,7 +471,7 @@ let BattleFormatsData = { sandslash: { randomBattleMoves: ["earthquake", "swordsdance", "rapidspin", "toxic", "stealthrock", "knockoff"], randomDoubleBattleMoves: ["earthquake", "rockslide", "stoneedge", "swordsdance", "xscissor", "knockoff", "protect"], - tier: "NU", + tier: "PU", doublesTier: "DUU", }, sandslashalola: { From fb0f401a6bc83a9b176040ede94e3e67fadaaa95 Mon Sep 17 00:00:00 2001 From: whales Date: Wed, 12 Sep 2018 00:06:52 +0930 Subject: [PATCH 052/125] Mafia: Support cohosts, resetting roles (#4837) --- chat-plugins/mafia.js | 450 ++++++++++++++++++++++++------------------ 1 file changed, 255 insertions(+), 195 deletions(-) diff --git a/chat-plugins/mafia.js b/chat-plugins/mafia.js index 8bdf8af40c10..324b4fa89752 100644 --- a/chat-plugins/mafia.js +++ b/chat-plugins/mafia.js @@ -175,115 +175,6 @@ class MafiaPlayer extends Rooms.RoomGamePlayer { } } -// Parses a single role into an object -/** - * - * @param {string} roleString - * @return {MafiaParsedRole} - */ -function parseRole(roleString) { - /** @type {MafiaRole} */ - let role = { - name: roleString.split(' ').map(p => toId(p) === 'solo' ? '' : p).join(' '), - safeName: '', // MAKE SURE THESE ARE SET BELOW - id: '', - image: '', - memo: ['During the Day, you may vote for whomever you want lynched.'], - alignment: '', - }; - roleString = roleString.replace(/\s*\(.*?\)\s*/g, ' '); - let target = roleString.toLowerCase().replace(/[^\w\d\s]/g, '').split(' '); - let problems = []; - role.safeName = Chat.escapeHTML(role.name); - role.id = toId(role.name); - for (let key in MafiaData.roles) { - if (key.includes('_')) { - let roleKey = target.slice().map(toId).join('_'); - if (roleKey.includes(key)) { - let originalKey = key; - if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; - if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; - if (MafiaData.roles[key].alignment) { - if (role.alignment && role.alignment !== MafiaData.roles[key].alignment) { - // A role cant have multiple alignments - problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.roles[key].alignment} or ${role.alignment})`); - break; - } - role.alignment = MafiaData.roles[key].alignment; - } - if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); - let index = roleKey.split('_').indexOf(originalKey.split('_')[0]); - target.splice(index, originalKey.split('_').length); - } - } else if (target.includes(key)) { - let index = target.indexOf(key); - if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; - if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; - if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); - target.splice(index, 1); - } - } - // Add modifiers - for (let key in MafiaData.modifiers) { - if (key.includes('_')) { - let roleKey = target.slice().map(toId).join('_'); - if (roleKey.includes(key)) { - if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; - if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; - if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); - let index = roleKey.split('_').indexOf(key.split('_')[0]); - target.splice(index, key.split('_').length); - } - } else if (key === 'xshot') { - // Special case for X-Shot modifier - for (let [i, xModifier] of target.entries()) { - if (toId(xModifier).endsWith('shot')) { - let num = parseInt(toId(xModifier).substring(0, toId(xModifier).length - 4)); - if (isNaN(num)) continue; - let memo = MafiaData.modifiers.xshot.memo.slice(); - memo = memo.map((/** @type {string} */m) => m.replace(/X/g, num.toString())); - role.memo = role.memo.concat(memo); - target.splice(i, 1); - i--; - } - } - } else if (target.includes(key)) { - let index = target.indexOf(key); - if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; - if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; - if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); - target.splice(index, 1); - } - } - // Determine the role's alignment - for (let [j, targetId] of target.entries()) { - let id = toId(targetId); - if (MafiaData.alignments[id]) { - if (typeof MafiaData.alignments[id] === 'string') id = MafiaData.alignments[id]; - if (role.alignment && role.alignment !== MafiaData.alignments[id].id) { - // A role cant have multiple alignments - problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.alignments[id].id} or ${role.alignment})`); - break; - } - role.alignment = MafiaData.alignments[id].id; - role.memo = role.memo.concat(MafiaData.alignments[id].memo); - if (!role.image && MafiaData.alignments[id].image) role.image = MafiaData.alignments[id].image; - target.splice(j, 1); - j--; - } - } - if (!role.alignment) { - // Default to town - role.alignment = 'town'; - role.memo = role.memo.concat(MafiaData.alignments.town.memo); - } - // Handle anything that is unknown - if (target.length) { - role.memo.push(`To learn more about your role, PM the host.`); - } - return {role, problems}; -} - class MafiaTracker extends Rooms.RoomGame { /** * @param {ChatRoom} room @@ -303,6 +194,8 @@ class MafiaTracker extends Rooms.RoomGame { this.hostid = host.userid; this.host = Chat.escapeHTML(host.name); + /** @type {string[]} */ + this.cohosts = []; /** @type {{[userid: string]: MafiaPlayer}} */ this.players = Object.create(null); @@ -404,9 +297,10 @@ class MafiaTracker extends Rooms.RoomGame { * @param {User} user * @param {string} roleString * @param {boolean} force + * @param {boolean} reset * @return {void} */ - setRoles(user, roleString, force = false) { + setRoles(user, roleString, force = false, reset = false) { let roles = (/** @type {string[]} */roleString.split(',').map(x => x.trim())); if (roles.length === 1) { // Attempt to set roles from a theme @@ -426,14 +320,7 @@ class MafiaTracker extends Rooms.RoomGame { } else if (roles.length > this.playerCount) { user.sendTo(this.room, `|error|You have provided too many roles, ${roles.length - this.playerCount} ${Chat.plural(roles.length - this.playerCount, 'roles', 'role')} will not be assigned.`); } - if (this.originalRoles.length) { - // Reset roles - this.originalRoles = []; - this.originalRoleString = ''; - this.roles = []; - this.roleString = ''; - } - if (this.IDEA.data) this.IDEA.data = null; + if (force) { this.originalRoles = roles.map(r => { return { @@ -450,6 +337,8 @@ class MafiaTracker extends Rooms.RoomGame { this.roleString = this.originalRoleString; return this.sendRoom(`The roles have been set.`); } + + let newRoles = []; /** @type {string[]} */ let problems = []; /** @type {string[]} */ @@ -459,29 +348,141 @@ class MafiaTracker extends Rooms.RoomGame { for (const string of roles) { const roleId = string.toLowerCase().replace(/[^\w\d\s]/g, ''); if (roleId in cache) { - this.originalRoles.push(Object.assign(Object.create(null), cache[roleId])); + newRoles.push(Object.assign(Object.create(null), cache[roleId])); } else { - const role = parseRole(string); + const role = MafiaTracker.parseRole(string); if (role.problems.length) problems = problems.concat(role.problems); if (alignments.indexOf(role.role.alignment) === -1) alignments.push(role.role.alignment); cache[roleId] = role.role; - this.originalRoles.push(role.role); + newRoles.push(role.role); } } if (alignments.length < 2 && alignments[0] !== 'solo') problems.push(`There must be at least 2 different alignments in a game!`); if (problems.length) { - this.originalRoles = []; for (const problem of problems) { user.sendTo(this.room, `|error|${problem}`); } - return user.sendTo(this.room, `|error|To forcibly set the roles, use /mafia forcesetroles`); + return user.sendTo(this.room, `|error|To forcibly set the roles, use /mafia force${reset ? "re" : ""}setroles`); } + + this.IDEA.data = null; + + this.originalRoles = newRoles; this.roles = this.originalRoles.slice(); this.originalRoleString = this.originalRoles.slice().map(r => `${r.safeName}`).join(', '); this.roleString = this.originalRoleString; - this.phase = 'locked'; + if (!reset) this.phase = 'locked'; this.updatePlayers(); - this.sendRoom(`The roles have been set.`); + this.sendRoom(`The roles have been ${reset ? 're' : ''}set.`); + if (reset) this.distributeRoles(); + } + + /** + * Parses a single role into an object + * @param {string} roleString + * @return {MafiaParsedRole} + */ + static parseRole(roleString) { + /** @type {MafiaRole} */ + let role = { + name: roleString.split(' ').map(p => toId(p) === 'solo' ? '' : p).join(' '), + safeName: '', // MAKE SURE THESE ARE SET BELOW + id: '', + image: '', + memo: ['During the Day, you may vote for whomever you want lynched.'], + alignment: '', + }; + roleString = roleString.replace(/\s*\(.*?\)\s*/g, ' '); + let target = roleString.toLowerCase().replace(/[^\w\d\s]/g, '').split(' '); + let problems = []; + role.safeName = Chat.escapeHTML(role.name); + role.id = toId(role.name); + for (let key in MafiaData.roles) { + if (key.includes('_')) { + let roleKey = target.slice().map(toId).join('_'); + if (roleKey.includes(key)) { + let originalKey = key; + if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; + if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; + if (MafiaData.roles[key].alignment) { + if (role.alignment && role.alignment !== MafiaData.roles[key].alignment) { + // A role cant have multiple alignments + problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.roles[key].alignment} or ${role.alignment})`); + break; + } + role.alignment = MafiaData.roles[key].alignment; + } + if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); + let index = roleKey.split('_').indexOf(originalKey.split('_')[0]); + target.splice(index, originalKey.split('_').length); + } + } else if (target.includes(key)) { + let index = target.indexOf(key); + if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key]; + if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image; + if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo); + target.splice(index, 1); + } + } + // Add modifiers + for (let key in MafiaData.modifiers) { + if (key.includes('_')) { + let roleKey = target.slice().map(toId).join('_'); + if (roleKey.includes(key)) { + if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; + if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; + if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); + let index = roleKey.split('_').indexOf(key.split('_')[0]); + target.splice(index, key.split('_').length); + } + } else if (key === 'xshot') { + // Special case for X-Shot modifier + for (let [i, xModifier] of target.entries()) { + if (toId(xModifier).endsWith('shot')) { + let num = parseInt(toId(xModifier).substring(0, toId(xModifier).length - 4)); + if (isNaN(num)) continue; + let memo = MafiaData.modifiers.xshot.memo.slice(); + memo = memo.map((/** @type {string} */m) => m.replace(/X/g, num.toString())); + role.memo = role.memo.concat(memo); + target.splice(i, 1); + i--; + } + } + } else if (target.includes(key)) { + let index = target.indexOf(key); + if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key]; + if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image; + if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo); + target.splice(index, 1); + } + } + // Determine the role's alignment + for (let [j, targetId] of target.entries()) { + let id = toId(targetId); + if (MafiaData.alignments[id]) { + if (typeof MafiaData.alignments[id] === 'string') id = MafiaData.alignments[id]; + if (role.alignment && role.alignment !== MafiaData.alignments[id].id) { + // A role cant have multiple alignments + problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.alignments[id].id} or ${role.alignment})`); + break; + } + role.alignment = MafiaData.alignments[id].id; + role.memo = role.memo.concat(MafiaData.alignments[id].memo); + if (!role.image && MafiaData.alignments[id].image) role.image = MafiaData.alignments[id].image; + target.splice(j, 1); + j--; + } + } + if (!role.alignment) { + // Default to town + role.alignment = 'town'; + role.memo = role.memo.concat(MafiaData.alignments.town.memo); + } + // Handle anything that is unknown + if (target.length) { + role.memo.push(`To learn more about your role, PM the host.`); + } + return {role, problems}; } /** @@ -505,9 +506,8 @@ class MafiaTracker extends Rooms.RoomGame { if (Object.keys(this.roles).length < this.playerCount) return user.sendTo(this.room, `You have not provided enough roles for the players.`); } this.started = true; - this.played = Object.keys(this.players); this.sendRoom(`The game of ${this.title} is starting!`, {declare: true}); - this.played.push(this.hostid); + // MafiaTracker#played gets set in distributeRoles this.distributeRoles(); this.day(null, true); if (this.IDEA.data) this.room.add(`|html|
IDEA discards:${this.IDEA.discardsHtml}
`).update(); @@ -517,8 +517,7 @@ class MafiaTracker extends Rooms.RoomGame { * @return {void} */ distributeRoles() { - if (this.phase !== 'locked' || !Object.keys(this.roles).length) return; - this.sendRoom(`The roles are being distributed...`); + if (!Object.keys(this.roles).length) return; let roles = Dex.shuffle(this.roles.slice()); for (let p in this.players) { let role = roles.shift(); @@ -526,6 +525,8 @@ class MafiaTracker extends Rooms.RoomGame { let u = Users(p); if (u && u.connected) u.send(`>${this.room.id}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`); } + this.dead = {}; + this.played = [this.hostid, ...this.cohosts, ...Object.keys(this.players)]; this.sendRoom(`The roles have been distributed.`, {declare: true}); this.updatePlayers(); } @@ -580,8 +581,10 @@ class MafiaTracker extends Rooms.RoomGame { if (this.phase !== 'day') return; if (this.timer) this.setDeadline(0, true); this.phase = 'night'; - let host = Users(this.hostid); - if (host && host.connected) host.send(`>${this.room.id}\n|notify|It's night in your game of Mafia!`); + for (const hostid of [...this.cohosts, this.hostid]) { + let host = Users(hostid); + if (host && host.connected) host.send(`>${this.room.id}\n|notify|It's night in your game of Mafia!`); + } this.sendRoom(`Night ${this.dayNum}. PM the host your action, or idle.`, {declare: true}); const hasPlurality = this.getPlurality(); if (!early && hasPlurality) this.sendRoom(`Plurality is on ${this.players[hasPlurality] ? this.players[hasPlurality].name : 'No Lynch'}`); @@ -1008,7 +1011,6 @@ class MafiaTracker extends Rooms.RoomGame { sub(player, replacement) { let oldPlayer = this.players[player]; if (!oldPlayer) return; // should never happen - if (oldPlayer.lynching) this.unlynch(oldPlayer.userid, true); const newUser = Users(replacement); if (!newUser) return; // should never happen @@ -1231,7 +1233,7 @@ class MafiaTracker extends Rooms.RoomGame { // if there's only one option, it's their role, parse it properly let roleName = ''; if (this.IDEA.data.picks.length === 1) { - const role = parseRole(player.IDEA.picks[this.IDEA.data.picks[0]]); + const role = MafiaTracker.parseRole(player.IDEA.picks[this.IDEA.data.picks[0]]); player.role = role.role; if (role.problems.length && !this.IDEA.data.untrusted) this.sendRoom(`Problems found when parsing IDEA role ${player.IDEA.picks[this.IDEA.data.picks[0]]}. Please report this to a mod.`); } else { @@ -1248,7 +1250,7 @@ class MafiaTracker extends Rooms.RoomGame { if (!this.IDEA.data.untrusted) { for (const pick of role) { if (pick.substr(0, 10) === 'alignment:') { - const parsedRole = parseRole(pick.substr(9)); + const parsedRole = MafiaTracker.parseRole(pick.substr(9)); if (parsedRole.problems.length) this.sendRoom(`Problems found when parsing IDEA role ${pick}. Please report this to a mod.`); player.role.alignment = parsedRole.role.alignment; } @@ -1295,11 +1297,13 @@ class MafiaTracker extends Rooms.RoomGame { * @return {void} */ updateHost() { - const host = Users(this.hostid); - if (!host || !host.connected) return; - if (this.ended) return host.send(`>view-mafia-${this.room.id}\n|deinit`); - const buf = Chat.pages.mafia([this.room.id], host); - host.send(`>view-mafia-${this.room.id}\n|init|html\n${buf}`); + for (const hostid of [...this.cohosts, this.hostid]) { + const host = Users(hostid); + if (!host || !host.connected) return; + if (this.ended) return host.send(`>view-mafia-${this.room.id}\n|deinit`); + const buf = Chat.pages.mafia([this.room.id], host); + host.send(`>view-mafia-${this.room.id}\n|init|html\n${buf}`); + } } /** @@ -1348,10 +1352,11 @@ class MafiaTracker extends Rooms.RoomGame { if (!this.room.users[user.userid]) return `${targetString} not in the room.`; if (this.players[user.userid]) return `${targetString} already in the game.`; if (this.hostid === user.userid) return `${targetString} the host.`; + if (this.cohosts.includes(user.userid)) return `${targetString} a cohost.`; if (!force) { for (const alt of user.getAltUsers(true)) { if (this.players[alt.userid]) return `${self ? `You already have` : `${user.userid} already has`} an alt in the game.`; - if (this.hostid === alt.userid) return `${self ? `You have` : `${user.userid} has`} an alt as the game host.`; + if (this.hostid === alt.userid || this.cohosts.includes(alt.userid)) return `${self ? `You have` : `${user.userid} has`} an alt as a game host.`; } } return false; @@ -1421,10 +1426,12 @@ class MafiaTracker extends Rooms.RoomGame { const subIndex = this.hostRequestedSub.indexOf(user.userid); if (subIndex !== -1) { this.hostRequestedSub.splice(subIndex, 1); - this.sendUser(this.hostid, `${user.userid} has spoken and been removed from the host sublist.`); + for (const hostid of [...this.cohosts, this.hostid]) { + this.sendUser(hostid, `${user.userid} has spoken and been removed from the host sublist.`); + } } - if (user.isStaff || (this.room.auth && this.room.auth[user.userid] && this.room.auth[user.userid] !== '+') || this.hostid === user.userid || !this.started) return false; + if (user.isStaff || (this.room.auth && this.room.auth[user.userid] && this.room.auth[user.userid] !== '+') || this.hostid === user.userid || this.cohosts.includes(user.userid) || !this.started) return false; if (!this.players[user.userid] && (!this.dead[user.userid] || !this.dead[user.userid].treestump)) return `You cannot talk while a game of ${this.title} is going on.`; if (this.phase === 'night') return `You cannot talk at night.`; return false; @@ -1498,8 +1505,10 @@ class MafiaTracker extends Rooms.RoomGame { logs.plays[month][player]++; } if (!logs.hosts[month]) logs.hosts[month] = {}; - if (!logs.hosts[month][this.hostid]) logs.hosts[month][this.hostid] = 0; - logs.hosts[month][this.hostid]++; + for (const hostid of [...this.cohosts, this.host]) { + if (!logs.hosts[month][hostid]) logs.hosts[month][hostid] = 0; + logs.hosts[month][hostid]++; + } writeFile(LOGS_FILE, logs); } if (this.timer) { @@ -1537,7 +1546,7 @@ const pages = { if (!room || !room.users[user.userid] || !room.game || room.game.gameid !== 'mafia' || room.game.ended) return `|deinit`; const game = /** @type {MafiaTracker} */ (room.game); const isPlayer = user.userid in game.players; - const isHost = user.userid === game.hostid; + const isHost = user.userid === game.hostid || game.cohosts.includes(user.userid); let buf = `|title|${game.title}\n|pagehtml|
`; buf += ``; buf += `

${game.title}

Host: ${game.host}

`; @@ -1559,7 +1568,7 @@ const pages = { if (i === selectedIndex) { buf += ``; } else { - buf += ``; + buf += ``; } } buf += `
`; @@ -1567,7 +1576,7 @@ const pages = { buf += `

`; buf += `

Role details:

`; for (const role of IDEA.originalChoices) { - const roleObject = parseRole(role).role; + const roleObject = MafiaTracker.parseRole(role).role; buf += `

${role}`; buf += `
    ${roleObject.memo.map(m => `
  • ${m}
  • `).join('')}
`; buf += `
`; @@ -1766,8 +1775,10 @@ const commands = { if (room.game) return this.errorReply(`There is already a game of ${room.game.title} in progress in this room.`); if (!user.can('broadcast', null, room)) return this.errorReply(`/mafia ${cmd} - Access denied.`); + let nextHost = false; if (room.id === 'mafia') { if (cmd === 'nexthost') { + nextHost = true; if (!hostQueue.length) return this.errorReply(`Nobody is on the host queue.`); let skipped = []; do { @@ -1789,7 +1800,7 @@ const commands = { } if (!this.targetUser || !this.targetUser.connected) return this.errorReply(`The user "${this.targetUsername}" was not found.`); - if (this.targetUser.userid !== user.userid && !this.can('mute', null, room)) return false; + if (!nextHost && this.targetUser.userid !== user.userid && !this.can('mute', null, room)) return false; if (!room.users[this.targetUser.userid]) return this.errorReply(`${this.targetUser.name} is not in this room, and cannot be hosted.`); if (room.id === 'mafia' && isHostBanned(this.targetUser.userid)) return this.errorReply(`${this.targetUser.name} is banned from hosting games.`); @@ -1815,8 +1826,8 @@ const commands = { if (!room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`); if (room.id !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`); const args = target.split(',').map(toId); - if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0]) && !this.can('broadcast', null, room)) { - return false; + if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0])) { + if (user.userid !== toId(args[1]) && !this.can('mute', null, room)) return; } else { if (!this.runBroadcast()) return false; } @@ -1901,7 +1912,7 @@ const commands = { playercap: function (target, room, user) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia playercap - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (game.phase !== 'signups') return this.errorReply(`Signups are already closed.`); if (toId(target) === 'none') target = '20'; const num = parseInt(target); @@ -1922,7 +1933,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia close - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (game.phase !== 'signups') return user.sendTo(targetRoom, `|error|Signups are already closed.`); if (game.playerCount < 2) return user.sendTo(targetRoom, `|error|You need at least 2 players to start.`); game.phase = 'locked'; @@ -1944,7 +1955,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia closedsetup - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const action = toId(args.join('')); if (!['on', 'off'].includes(action)) return this.parse('/help mafia closedsetup'); if (game.started) return user.sendTo(targetRoom, `|error|You can't ${action === 'on' ? 'enable' : 'disable'} closed setup because the game has already started.`); @@ -1966,7 +1977,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia reveal - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const action = toId(args.join('')); if (!['on', 'off'].includes(action)) return this.parse('/help mafia reveal'); if ((action === 'off' && game.noReveal) || (action === 'on' && !game.noReveal)) return user.sendTo(targetRoom, `|error|Revealing of roles is already ${game.noReveal ? 'disabled' : 'enabled'}.`); @@ -1976,25 +1987,33 @@ const commands = { }, revealhelp: [`/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ * # & ~`], + resetroles: 'setroles', + forceresetroles: 'setroles', forcesetroles: 'setroles', setroles: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); - if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(game.phase === 'signups' ? `You need to close signups first.` : `The game has already started.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; + const reset = cmd.includes('reset'); + if (reset) { + if (game.phase !== 'day' && game.phase !== 'night') return this.errorReply(`The game has not started yet.`); + } else { + if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(game.phase === 'signups' ? `You need to close signups first.` : `The game has already started.`); + } if (!target) return this.parse('/help mafia setroles'); - game.setRoles(user, target, cmd === 'forcesetroles'); + game.setRoles(user, target, cmd.includes('force'), reset); }, setroleshelp: [ - `/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player.`, - `/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`, + `/mafia setroles [comma separated roles] - Set the roles for a game of mafia. You need to provide one role per player.`, + `/mafia forcesetroles [comma separated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`, + `/mafia resetroles [comma separated roles] - Reset the roles in an ongoing game.`, ], idea: function (target, room, user) { if (!room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('broadcast', null, room) || (!user.can('mute', null, room) && game.hostid !== user.userid)) return this.errorReply(`/mafia idea - Access denied.`); + if (!user.can('broadcast', null, room) || (!user.can('mute', null, room) && game.hostid !== user.userid && !game.cohosts.includes(user.userid))) return this.errorReply(`/mafia idea - Access denied.`); if (game.started) return this.errorReply(`You cannot start an IDEA after the game has started.`); if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(`You need to close the signups first.`); game.ideaInit(user, toId(target)); @@ -2050,7 +2069,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia idereroll - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; game.ideaDistributeRoles(user); }, idearerollhelp: [`/mafia ideareroll - rerolls the roles for the current IDEA module. Requires host % @ * # & ~`], @@ -2073,7 +2092,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia start - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; game.start(user); }, starthelp: [`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`], @@ -2092,7 +2111,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (cmd === 'night') { game.night(); } else { @@ -2167,7 +2186,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; let action = toId(args.shift()); if (!action) return this.parse(`/help mafia selflynch`); if (this.meansYes(action)) { @@ -2198,7 +2217,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia kill - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const player = game.players[toId(args.join(''))]; if (!player) return user.sendTo(targetRoom, `|error|"${args.join(',')}" is not a living player.`); if (game.phase === 'IDEApicking') return this.errorReply(`You cannot add or remove players while IDEA roles are being picked.`); // needs to be here since eliminate doesn't pass the user @@ -2225,7 +2244,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia revive - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!toId(args.join(''))) return this.parse('/help mafia revive'); for (const targetUser of args) { game.revive(user, toId(targetUser), cmd === 'forceadd'); @@ -2238,13 +2257,13 @@ const commands = { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); target = toId(target); - if (!user.can('mute', null, room) && game.hostid !== user.userid && target) return this.errorReply(`/mafia deadline - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (target === 'off') { return game.setDeadline(0); } else { const num = parseInt(target); if (isNaN(num)) { - if (game.hostid === user.userid && this.cmdToken === "!") { + if ((game.hostid === user.userid || game.cohosts.includes(user.userid)) && this.cmdToken === "!") { const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); if (room && room.lastBroadcast === broadcastMessage && room.lastBroadcastTime >= Date.now() - 20 * 1000) { @@ -2273,7 +2292,7 @@ const commands = { applyhammermodifier: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); const [player, mod] = target.split(','); if (cmd === 'applyhammermodifier') { @@ -2286,7 +2305,7 @@ const commands = { clearhammermodifiers: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); if (cmd === 'clearhammermodifiers') { game.clearHammerModifiers(user); @@ -2339,7 +2358,7 @@ const commands = { hammer: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return this.errorReply(`/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (!game.started) return this.errorReply(`The game has not started yet.`); const hammer = parseInt(target); if ((isNaN(hammer) || hammer < 1) && cmd.toLowerCase() !== `resethammer`) return this.errorReply(`${target} is not a valid hammer count.`); @@ -2371,7 +2390,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia ${cmd} - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (cmd === 'enablenl') { game.setNoLynch(user, true); } else { @@ -2384,7 +2403,7 @@ const commands = { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); if (!game.started) return this.errorReply(`The game of mafia has not started yet.`); - if (game.hostid === user.userid && this.cmdToken === "!") { + if ((game.hostid === user.userid || game.cohosts.includes(user.userid)) && this.cmdToken === "!") { const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); if (room && room.lastBroadcast === broadcastMessage && room.lastBroadcastTime >= Date.now() - 20 * 1000) { @@ -2415,7 +2434,7 @@ const commands = { players: function (target, room, user) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); - if (game.hostid === user.userid && this.cmdToken === "!") { + if ((game.hostid === user.userid || game.cohosts.includes(user.userid)) && this.cmdToken === "!") { const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); if (room && room.lastBroadcast === broadcastMessage && room.lastBroadcastTime >= Date.now() - 20 * 1000) { @@ -2458,6 +2477,15 @@ const commands = { this.sendReplyBox(`${showOrl ? `Original Rolelist: ` : `Rolelist: `}${roleString}`); }, + playerroles: function (target, room, user) { + if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); + const game = /** @type {MafiaTracker} */ (room.game); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid)) return this.errorReply(`Only the host can view roles.`); + if (!game.started) return this.errorReply(`The game has not started.`); + const players = [...Object.values(game.players), ...Object.values(game.dead)]; + this.sendReplyBox(players.map(p => `${p.safeName}: ${p.role ? p.role.safeName : 'No role'}`).join('
')); + }, + spectate: 'view', view: function (target, room, user, connection) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); @@ -2508,7 +2536,7 @@ const commands = { game.players[user.userid].updateHtmlRoom(); game.nextSub(); } else { - if (game.hostid === user.userid) return user.sendTo(targetRoom, `|error|The host cannot sub out of the game.`); + if (game.hostid === user.userid || game.cohosts.includes(user.userid)) return user.sendTo(targetRoom, `|error|The host cannot sub out of the game.`); if (!game.subs.includes(user.userid)) return user.sendTo(targetRoom, `|error|You are not on the sub list.`); game.subs.splice(game.subs.indexOf(user.userid), 1); // Update spectator's view @@ -2516,7 +2544,7 @@ const commands = { } break; case 'next': - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for force substituting a player.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; let toSub = args.shift(); if (!(toId(toSub) in game.players)) return user.sendTo(targetRoom, `|error|${toSub} is not in the game.`); if (!game.subs.length) { @@ -2528,7 +2556,7 @@ const commands = { } break; case 'remove': - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for force substituting a player.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const toRemove = toId(args.shift()); const toRemoveIndex = game.subs.indexOf(toRemove); if (toRemoveIndex === -1) return user.sendTo(room, `|error|${toRemove} is not on the sub list.`); @@ -2536,7 +2564,7 @@ const commands = { user.sendTo(room, `${toRemove} has been removed from the sublist`); break; case 'unrequest': - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for removing a player's sub request.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const toUnrequest = toId(args.shift()); const userIndex = game.requestedSub.indexOf(toUnrequest); const hostIndex = game.hostRequestedSub.indexOf(toUnrequest); @@ -2551,7 +2579,7 @@ const commands = { } break; default: - if (!user.can('mute', null, room) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia sub - Access denied for force substituting a player.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; const toSubOut = action; const toSubIn = toId(args.shift()); if (!(toSubOut in game.players)) return user.sendTo(targetRoom, `|error|${toSubOut} is not in the game.`); @@ -2587,6 +2615,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (this.meansYes(toId(args.join('')))) { if (game.autoSub) return user.sendTo(targetRoom, `|error|Automatic subbing of players is already enabled.`); game.autoSub = true; @@ -2602,38 +2631,69 @@ const commands = { }, autosubhelp: [`/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Requires host % @ * # & ~`], + cohost: 'subhost', + forcecohost: 'subhost', forcesubhost: 'subhost', subhost: function (target, room, user, connection, cmd) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); if (!this.canTalk()) return; - if (!target) return this.parse('/help mafia subhost'); + if (!target) return this.parse(`/help mafia ${cmd}`); if (!this.can('mute', null, room)) return false; this.splitTarget(target, false); let targetUser = this.targetUser; if (!targetUser || !targetUser.connected) return this.errorReply(`The user "${this.targetUsername}" was not found.`); if (!room.users[targetUser.userid]) return this.errorReply(`${targetUser.name} is not in this room, and cannot be hosted.`); if (game.hostid === targetUser.userid) return this.errorReply(`${targetUser.name} is already the host.`); - if (targetUser.userid in game.players) return this.errorReply(`You cannot subhost to a user in the game.`); + if (game.cohosts.includes(targetUser.userid)) return this.errorReply(`${targetUser.name} is already a cohost.`); + if (targetUser.userid in game.players) return this.errorReply(`The host cannot be ingame.`); if (targetUser.userid in game.dead) { - if (cmd !== 'forcesubhost') return this.errorReply(`${targetUser.name} could potentially be revived. To subhost anyway, use /mafia forcesubhost ${target}.`); + if (!cmd.includes('force')) return this.errorReply(`${targetUser.name} could potentially be revived. To continue anyway, use /mafia force${cmd} ${target}.`); + if (game.dead[targetUser.userid].lynching) game.unlynch(targetUser.userid); game.dead[targetUser.userid].destroy(); delete game.dead[targetUser.userid]; } - const oldHostid = game.hostid; - const oldHost = Users(game.hostid); - if (oldHost) oldHost.send(`>view-mafia-${room.id}\n|deinit`); - if (game.subs.includes(targetUser.userid)) game.subs.splice(game.subs.indexOf(targetUser.userid), 1); - const queueIndex = hostQueue.indexOf(targetUser.userid); - if (queueIndex > -1) hostQueue.splice(queueIndex, 1); - game.host = Chat.escapeHTML(targetUser.name); - game.hostid = targetUser.userid; - game.played.push(targetUser.userid); - targetUser.send(`>view-mafia-${room.id}\n|init|html\n${Chat.pages.mafia([room.id], targetUser)}`); - game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been substituted as the new host, replacing ${oldHostid}.`, {declare: true}); - this.modlog('MAFIASUBHOST', targetUser, `replacing ${oldHostid}`, {noalts: true, noip: true}); + if (cmd.includes('cohost')) { + game.cohosts.push(targetUser.userid); + game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true}); + targetUser.send(`>view-mafia-${room.id}\n|init|html\n|${Chat.pages.mafia([room.id], targetUser)}`); + this.modlog('MAFIACOHOST', targetUser, null, {noalts: true, noip: true}); + } else { + const oldHostid = game.hostid; + const oldHost = Users(game.hostid); + if (oldHost) oldHost.send(`>view-mafia-${room.id}\n|deinit`); + if (game.subs.includes(targetUser.userid)) game.subs.splice(game.subs.indexOf(targetUser.userid), 1); + const queueIndex = hostQueue.indexOf(targetUser.userid); + if (queueIndex > -1) hostQueue.splice(queueIndex, 1); + game.host = Chat.escapeHTML(targetUser.name); + game.hostid = targetUser.userid; + game.played.push(targetUser.userid); + targetUser.send(`>view-mafia-${room.id}\n|init|html\n${Chat.pages.mafia([room.id], targetUser)}`); + game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been substituted as the new host, replacing ${oldHostid}.`, {declare: true}); + this.modlog('MAFIASUBHOST', targetUser, `replacing ${oldHostid}`, {noalts: true, noip: true}); + } }, subhosthelp: [`/mafia subhost [user] - Substitues the user as the new game host.`], + cohosthelp: [`/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions.`], + + uncohost: 'removecohost', + removecohost: function (target, room, user) { + if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); + const game = /** @type {MafiaTracker} */ (room.game); + if (!this.canTalk()) return; + if (!target) return this.parse('/help mafia subhost'); + if (!this.can('mute', null, room)) return false; + target = toId(target); + + const cohostIndex = game.cohosts.indexOf(target); + if (cohostIndex < 0) { + if (game.hostid === target) return this.errorReply(`${target} is the host, not a cohost. Use /mafia subhost to replace them.`); + return this.errorReply(`${target} is not a cohost.`); + } + game.cohosts.splice(cohostIndex, 1); + game.sendRoom(`${target} was removed as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true}); + this.modlog('MAFIAUNCOHOST', target, null, {noalts: true, noip: true}); + }, '!end': true, end: function (target, room, user) { @@ -2644,7 +2704,7 @@ const commands = { } if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (targetRoom.game); - if (!user.can('broadcast', null, targetRoom) && game.hostid !== user.userid) return user.sendTo(targetRoom, `|error|/mafia end - Access denied.`); + if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('broadcast', null, room)) return; game.end(); this.room = targetRoom; this.modlog('MAFIAEND', null); From c206315509090c49f6a01ab331ce93681975d62a Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Wed, 12 Sep 2018 13:38:14 -0400 Subject: [PATCH 053/125] Mafia: Update help & bug fixes --- chat-plugins/mafia-data.js | 3 +- chat-plugins/mafia.js | 199 +++++++++++++++++++++++++------------ 2 files changed, 140 insertions(+), 62 deletions(-) diff --git a/chat-plugins/mafia-data.js b/chat-plugins/mafia-data.js index 88af2f73cff0..724624b64ddd 100644 --- a/chat-plugins/mafia-data.js +++ b/chat-plugins/mafia-data.js @@ -1,5 +1,5 @@ 'use strict'; -/** @typedef {{name: string, plural: string, id: string, color?: string, memo: string[], image?: string}} MafiaAlignment */ +/** @typedef {{name: string, plural: string, id: string, color?: string, buttonColor?: string, memo: string[], image?: string}} MafiaAlignment */ /** @typedef {{[k: string]: MafiaAlignment | string}} MafiaAlignments */ /** @typedef {{name: string, id: string, memo: string[], alignment?: string, image?: string}} MafiaRole */ @@ -23,6 +23,7 @@ const alignments = { plural: `Town`, id: `town`, color: `#060`, + buttonColor: `#0A0`, memo: [`You are aligned with the Town. You win when all threats to the Town are eliminated and at least one Town-aligned player is still alive, or nothing can prevent the same.`], image: ``, }, diff --git a/chat-plugins/mafia.js b/chat-plugins/mafia.js index 324b4fa89752..768ba43ec298 100644 --- a/chat-plugins/mafia.js +++ b/chat-plugins/mafia.js @@ -161,9 +161,14 @@ class MafiaPlayer extends Rooms.RoomGamePlayer { this.IDEA = null; } - getRole() { + /** + * @param {boolean} button + */ + getRole(button = false) { if (!this.role) return; - return `${this.role.safeName}`; + let color = MafiaData.alignments[this.role.alignment].color; + if (button && MafiaData.alignments[this.role.alignment].buttonColor) color = MafiaData.alignments[this.role.alignment].buttonColor; + return `${this.role.safeName}`; } updateHtmlRoom() { @@ -588,6 +593,7 @@ class MafiaTracker extends Rooms.RoomGame { this.sendRoom(`Night ${this.dayNum}. PM the host your action, or idle.`, {declare: true}); const hasPlurality = this.getPlurality(); if (!early && hasPlurality) this.sendRoom(`Plurality is on ${this.players[hasPlurality] ? this.players[hasPlurality].name : 'No Lynch'}`); + if (!early) this.sendRoom(`|raw|
${this.lynchBox()}
`); this.updatePlayers(); } @@ -631,6 +637,7 @@ class MafiaTracker extends Rooms.RoomGame { if (this.getHammerValue(target) <= lynch.trueCount) { // HAMMER this.sendRoom(`Hammer! ${target === 'nolynch' ? 'Nobody' : Chat.escapeHTML(name)} was lynched!`, {declare: true}); + this.sendRoom(`|raw|
${this.lynchBox()}
`); if (target !== 'nolynch') this.eliminate(this.players[target], 'kill'); this.night(true); return; @@ -668,6 +675,25 @@ class MafiaTracker extends Rooms.RoomGame { player.updateHtmlRoom(); } + /** + * Returns HTML code that contains information on the current lynch vote. + * @return {string} + */ + lynchBox() { + if (!this.started) return `The game has not started yet.`; + let buf = `Lynches (Hammer: ${this.hammerCount})
`; + const plur = this.getPlurality(); + const list = Object.keys(this.lynches).sort((a, b) => { + if (a === plur) return -1; + if (b === plur) return 1; + return this.lynches[b].count - this.lynches[a].count; + }); + for (const key of list) { + buf += `${this.lynches[key].count}${plur === key ? '*' : ''} ${this.players[key] ? this.players[key].safeName : 'No Lynch'} (${this.lynches[key].lynchers.map(a => this.players[a] ? this.players[a].safeName : a).join(', ')})
`; + } + return buf; + } + /** * @param {User} user * @param {string} target @@ -1017,6 +1043,12 @@ class MafiaTracker extends Rooms.RoomGame { let newPlayer = this.makePlayer(newUser); newPlayer.role = oldPlayer.role; newPlayer.IDEA = oldPlayer.IDEA; + if (oldPlayer.lynching) { + // Dont change plurality + let lynch = this.lynches[oldPlayer.lynching]; + lynch.lynchers.splice(lynch.lynchers.indexOf(oldPlayer.userid, 1)); + lynch.lynchers.push(newPlayer.userid); + } this.players[newPlayer.userid] = newPlayer; this.players[oldPlayer.userid].destroy(); delete this.players[oldPlayer.userid]; @@ -1664,7 +1696,7 @@ const pages = { for (let p in game.players) { let player = game.players[p]; buf += `

`; - buf += `${player.safeName} (${player.role ? player.getRole() : ''})${game.lynchModifiers[p] !== undefined ? `(lynches worth ${game.getLynchValue(p)})` : ''}`; + buf += `${player.safeName} (${player.role ? player.getRole(true) : ''})${game.lynchModifiers[p] !== undefined ? `(lynches worth ${game.getLynchValue(p)})` : ''}`; buf += ` `; buf += ` `; buf += ` `; @@ -1827,7 +1859,8 @@ const commands = { if (room.id !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`); const args = target.split(',').map(toId); if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0])) { - if (user.userid !== toId(args[1]) && !this.can('mute', null, room)) return; + if (['forceadd', 'add'].includes(args[0]) && !this.can('mute', null, room)) return; + if (['remove', 'del', 'delete'].includes(args[0]) && user.userid !== args[1] && !this.can('mute', null, room)) return; } else { if (!this.runBroadcast()) return false; } @@ -2257,7 +2290,7 @@ const commands = { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); const game = /** @type {MafiaTracker} */ (room.game); target = toId(target); - if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; + if (target && game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return; if (target === 'off') { return game.setDeadline(0); } else { @@ -2375,8 +2408,8 @@ const commands = { } }, hammerhelp: [ - `/mafia hammer (hammer) - sets the hammer count to (hammer) and resets lynches`, - `/mafia shifthammer (hammer) - sets the hammer count to (hammer) without resetting lynches`, + `/mafia hammer [hammer] - sets the hammer count to [hammer] and resets lynches`, + `/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting lynches`, `/mafia resethammer - sets the hammer to the default, resetting lynches`, ], @@ -2397,7 +2430,7 @@ const commands = { game.setNoLynch(user, false); } }, - enablenlhelp: [`/mafia enablenl OR /mafia disablenl - Allows or disallows players abstain from lynching. Requires host % @ # & ~`], + enablenlhelp: [`/mafia [enablenl|disablenl] - Allows or disallows players abstain from lynching. Requires host % @ # & ~`], lynches: function (target, room, user) { if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`); @@ -2417,17 +2450,7 @@ const commands = { } if (!this.runBroadcast()) return false; - let buf = `Lynches (Hammer: ${game.hammerCount})
`; - const plur = game.getPlurality(); - const list = Object.keys(game.lynches).sort((a, b) => { - if (a === plur) return -1; - if (b === plur) return 1; - return game.lynches[b].count - game.lynches[a].count; - }); - for (const key of list) { - buf += `${game.lynches[key].count}${plur === key ? '*' : ''} ${game.players[key] ? game.players[key].safeName : 'No Lynch'} (${game.lynches[key].lynchers.map(a => game.players[a] ? game.players[a].safeName : a).join(', ')})
`; - } - this.sendReplyBox(buf); + this.sendReplyBox(game.lynchBox()); }, pl: 'players', @@ -2925,7 +2948,7 @@ const commands = { }, hostbanhelp: [ `/mafia hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ * # & ~`, - `/mafia (un)hostban [user] - Unbans a user from hosting games. Requires % @ * # & ~`, + `/mafia unhostban [user] - Unbans a user from hosting games. Requires % @ * # & ~`, ], disable: function (target, room, user) { @@ -2958,47 +2981,101 @@ const commands = { }, enablehelp: [`/mafia enable - Enables mafia in this room. Requires # & ~`], }, - mafiahelp: [ - `Commands for the Mafia plugin:`, - `/mafia host [user] - Create a game of Mafia with [user] as the host. Requires + % @ * # & ~`, - `/mafia nexthost - Host the next user in the host queue. Requires + % @ * # & ~`, - `/mafia queue [add|remove|view], [user] - Add: Adds the user to the queue. Requires + % @ * # & ~. Remove: Removes the user from the queue. Requires + % @ * # & ~. View: Shows the upcoming users who are going to host.`, - `/mafia join - Join the game.`, - `/mafia leave - Leave the game. Can only be done while signups are open.`, - `/mafia close - Closes signups for the current game. Requires: host % @ * # & ~`, - `/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ * # & ~`, - `/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ * # & ~`, - `/mafia selflynch [on|hammer|off] - Allows players to self lynch themselves either at hammer or anytime. Requires host % @ * # & ~`, - `/mafia enablenl OR /mafia disablenl - Allows or disallows players abstain from lynching. Requires host % @ # & ~`, - `/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player.`, - `/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`, - `/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`, - `/mafia day - Move to the next game day. Requires host % @ * # & ~`, - `/mafia night - Move to the next game night. Requires host % @ * # & ~`, - `/mafia extend (minutes) - Return to the previous game day. If (minutes) is provided, set the deadline for (minutes) minutes. Requires host % @ * # & ~`, - `/mafia lynch [player|nolynch] - Vote to lynch the specified player or to not lynch anyone.`, - `/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`, - `/mafia kill [player] - Kill a player, eliminating them from the game. Requires host % @ * # & ~`, - `/mafia treestump [player] - Kills a player, but allows them to talk during the day still.`, - `/mafia spirit [player] - Kills a player, but allows them to vote on the lynch still.`, - `/mafia spiritstump [player] - Kills a player, but allows them to talk during the day, and vote on the lynch.`, - `/mafia kick [player] - Kicks a player from the game without revealing their role.`, - `/mafia revive [player] - Revive a player who died or add a new player to the game. Requires host % @ * # & ~`, - `/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`, - `/mafia sub in - Request to sub into the game, or cancel a request to sub out.`, - `/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`, - `/mafia sub next [player] - Forcibly sub [player] out of the game. Requires host % @ * # & ~`, - `/mafia subhost [user] - Substitues the user as the new game host.`, - `/mafia end - End the current game of mafia. Requires host % @ * # & ~`, - `/mafia data [alignment|role|modifier|theme] - Get information on a mafia alignment, role, modifier, or theme.`, - `/mafia win (points) [user1], [user2], [user3], ... - Award the specified users points to the mafia leaderboard for this month. The amount of points can be negative to take points. Defaults to 10 points.`, - `/mafia mvp [user1], [user2], ... - Gives a MVP point and 5 leaderboard points to the users specified.`, - `/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 5 leaderboard points from the users specified.`, - `/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`, - `/mafia [hostlost|playlogs] - View the host logs or play logs for the current or last month. Requires % @ * # & ~`, - `/mafia disable - Disables mafia in this room. Requires # & ~`, - `/mafia enable - Enables mafia in this room. Requires # & ~`, - ], + mafiahelp: function (target, room, user) { + if (!this.runBroadcast()) return; + let buf = `Commands for the Mafia Plugin
Most commands are used through buttons in the game screen.

`; + buf += `
General Commands`; + buf += [ + `
General Commands for the Mafia Plugin:
`, + `/mafia host [user] - Create a game of Mafia with [user] as the host. Roomvoices can only host themselves. Requires + % @ * # & ~`, + `/mafia nexthost - Host the next user in the host queue. Only works in the Mafia Room. Requires + % @ * # & ~`, + `/mafia forcehost [user] - Bypass the host queue and host [user]. Only works in the Mafia Room. Requires % @ * # & ~`, + `/mafia sub in - Request to sub into the game, or cancel a request to sub out.`, + `/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`, + `/mafia spectate - Spectate the game of mafia.`, + `/mafia ideadiscards - Shows the discarded roles list for an IDEA module.`, + `/mafia lynches - Display the current lynch count, and whos lynching who.`, + `/mafia players - Display the current list of players, will highlight players.`, + `/mafia [rl|orl] - Display the role list or the original role list for the current game.`, + `/mafia data [alignment|role|modifier|theme] - Get information on a mafia alignment, role, modifier, or theme.`, + `/mafia subhost [user] - Substitues the user as the new game host. Requires % @ * # & ~`, + `/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions. Requires % @ * # & ~`, + `/mafia uncohost [user] - Remove [user]'s cohost status. Requires % @ * # & ~`, + `/mafia disable - Disables mafia in this room. Requires # & ~`, + `/mafia enable - Enables mafia in this room. Requires # & ~`, + ].join('
'); + buf += `
Player Commands`; + buf += [ + `
Commands that players can use:
`, + `/mafia join - Join the game.`, + `/mafia leave - Leave the game. Can only be done while signups are open.`, + `/mafia lynch [player|nolynch] - Vote to lynch the specified player or to not lynch anyone.`, + `/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`, + `/mafia deadline - View the deadline for the current game.`, + `/mafia sub in - Request to sub into the game, or cancel a request to sub out.`, + `/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`, + `/mafia ideapick [selection], [role] - Selects a role from an IDEA module`, + ].join('
'); + buf += `
Host Commands`; + buf += [ + `
Commands for game hosts and Cohosts to use:
`, + `/mafia playercap [cap|none]- Limit the number of players able to join the game. Player cap cannot be more than 20 or less than 2. Requires: host % @ # & ~`, + `/mafia close - Closes signups for the current game. Requires: host % @ * # & ~`, + `/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ * # & ~`, + `/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ * # & ~`, + `/mafia selflynch [on|hammer|off] - Allows players to self lynch themselves either at hammer or anytime. Requires host % @ * # & ~`, + `/mafia [enablenl|disablenl] - Allows or disallows players abstain from lynching. Requires host % @ # & ~`, + `/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player. Requires host % @ # & ~`, + `/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set. Requires host % @ # & ~`, + `/mafia idea [idea] - starts an IDEA module. Requires + % @ * # & ~, voices can only start for themselves`, + `/mafia ideareroll - rerolls the current IDEA module. Requires host % @ * # & ~`, + `/mafia customidea choices, picks (new line here, shift+enter)`, + `(comma or newline separated rolelist) - Starts an IDEA module with custom roles. Requires % @ # & ~`, + `/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`, + `/mafia day - Move to the next game day. Requires host % @ * # & ~`, + `/mafia night - Move to the next game night. Requires host % @ * # & ~`, + `/mafia extend (minutes) - Return to the previous game day. If (minutes) is provided, set the deadline for (minutes) minutes. Requires host % @ * # & ~`, + `/mafia kill [player] - Kill a player, eliminating them from the game. Requires host % @ * # & ~`, + `/mafia treestump [player] - Kills a player, but allows them to talk during the day still. Requires host % @ * # & ~`, + `/mafia spirit [player] - Kills a player, but allows them to vote on the lynch still. Requires host % @ * # & ~`, + `/mafia spiritstump [player] - Kills a player, but allows them to talk during the day, and vote on the lynch. Requires host % @ * # & ~`, + `/mafia kick [player] - Kicks a player from the game without revealing their role. Requires host % @ * # & ~`, + `/mafia revive [player] - Revive a player who died or add a new player to the game. Requires host % @ * # & ~`, + `/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`, + `/mafia sub next, [player] - Forcibly sub [player] out of the game. Requires host % @ * # & ~`, + `/mafia sub remove, [user] - Forcibly remove [user] from the sublist. Requres host % @ * # & ~`, + `/mafia sub unrequest, [player] - Remove's a player's request to sub out of the game. Requires host % @ * # & ~`, + `/mafia sub [player], [user] - Forcibly sub [player] for [user]. Requires host % @ * # & ~`, + `/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Defaults to yes. Requires host % @ * # & ~`, + `/mafia [love|hate] [player] - Makes it take 1 more (love) or less (hate) lynch to hammer [player]. Requires host % @ * # & ~`, + `/mafia [unlove|unhate] [player] - Removes loved or hated status from [player]. Requires host % @ * # & ~`, + `/mafia [mayor|voteless] [player] - Makes [player]'s' lynch worth 2 votes (mayor) or makes [player]'s lynch worth 0 votes (voteless). Requires host % @ * # & ~`, + `/mafia [unmayor|unvoteless] [player] - Removes mayor or voteless status from [player]. Requires host % @ * # & ~`, + `/mafia hammer [hammer] - sets the hammer count to [hammer] and resets lynches`, + `/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting lynches`, + `/mafia resethammer - sets the hammer to the default, resetting lynches`, + `/mafia playerroles - View all the player's roles in chat. Requires host`, + `/mafia end - End the current game of mafia. Requires host % @ * # & ~`, + ].join('
'); + buf += `
Mafia Room Specific Commands`; + buf += [ + `
Commands that are only useable in the Mafia Room:
`, + `/mafia queue add, [user] - Adds the user to the host queue. Requires % @ * # & ~.`, + `/mafia queue remove, [user] - Removes the user from the queue. You can remove yourself regardless of rank. Requires % @ * # & ~.`, + `/mafia queue - Shows the list of users who are in queue to host.`, + `/mafia win (points) [user1], [user2], [user3], ... - Award the specified users points to the mafia leaderboard for this month. The amount of points can be negative to take points. Defaults to 10 points.`, + `/mafia winfaction (points), [faction] - Award the specified points to all the players in the given faction. Requires % @ * # & ~`, + `/mafia mvp [user1], [user2], ... - Gives a MVP point and 10 leaderboard points to the users specified.`, + `/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 10 leaderboard points from the users specified.`, + `/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`, + `/mafia [hostlost|playlogs] - View the host logs or play logs for the current or last month. Requires % @ * # & ~`, + `/mafia hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ * # & ~`, + `/mafia unhostban [user] - Unbans a user from hosting games. Requires % @ * # & ~`, + ].join('
'); + buf += `
`; + + return this.sendReplyBox(buf); + }, }; module.exports = { From 2a25f4e3336148b7a41736535610fb255c2ce748 Mon Sep 17 00:00:00 2001 From: Kris Johnson <11083252+KrisXV@users.noreply.github.com> Date: Wed, 12 Sep 2018 13:07:54 -0600 Subject: [PATCH 054/125] Battle Factory: Update Ubers (#4851) --- data/factory-sets.json | 866 ++++------------------------------------- 1 file changed, 69 insertions(+), 797 deletions(-) diff --git a/data/factory-sets.json b/data/factory-sets.json index 260644edef5b..6b27e9af48b9 100644 --- a/data/factory-sets.json +++ b/data/factory-sets.json @@ -8,7 +8,7 @@ "ability": ["Drought"], "evs": {"hp": 252, "def": 56, "spd": 200}, "nature": "Relaxed", - "moves": [["Precipice Blades"], ["Toxic", "Roar"], ["Overheat"], ["Stealth Rock"]] + "moves": [["Precipice Blades"], ["Toxic"], ["Overheat"], ["Stealth Rock"]] }, { "species": "Groudon", "item": ["Red Orb"], @@ -34,16 +34,16 @@ "species": "Groudon", "item": ["Red Orb"], "ability": ["Drought"], - "evs": {"atk": 168, "spd": 144, "spe": 196}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Precipice Blades"], ["Stone Edge"], ["Stealth Rock"]] + "evs": {"spa": 212, "spd": 144, "spe": 152}, + "nature": "Mild", + "moves": [["Swords Dance"], ["Precipice Blades"], ["Rock Tomb"], ["Overheat"]] }, { "species": "Groudon", - "item": ["Choice Band"], + "item": ["Red Orb"], "ability": ["Drought"], - "evs": {"hp": 152, "atk": 252, "spd": 44, "spe": 60}, + "evs": {"hp": 144, "atk": 156, "spd": 56, "spe": 152}, "nature": "Adamant", - "moves": [["Precipice Blades"], ["Stone Edge"], ["Dragon Claw"], ["Fire Punch"]] + "moves": [["Precipice Blades"], ["Swords Dance"], ["Stealth Rock"], ["Rock Tomb"]] }] }, "xerneas": { @@ -59,16 +59,9 @@ "species": "Xerneas", "item": ["Power Herb"], "ability": ["Fairy Aura"], - "evs": {"hp": 72, "spa": 252, "spd": 100, "spe": 84}, + "evs": {"def": 168, "spa": 252, "spe": 88}, "nature": "Modest", "moves": [["Geomancy"], ["Moonblast"], ["Psyshock"], ["Hidden Power Fire"]] - }, { - "species": "Xerneas", - "item": ["Leftovers"], - "ability": ["Fairy Aura"], - "evs": {"hp": 252, "def": 252, "spa": 4}, - "nature": "Bold", - "moves": [["Moonblast"], ["Aromatherapy"], ["Rest"], ["Sleep Talk"]] }, { "species": "Xerneas", "item": ["Fairium Z"], @@ -83,27 +76,6 @@ "evs": {"spa": 252, "spd": 4, "spe": 252}, "nature": "Modest", "moves": [["Moonblast"], ["Focus Blast"], ["Aromatherapy"], ["Thunder"]] - }, { - "species": "Xerneas", - "item": ["Choice Scarf"], - "ability": ["Fairy Aura"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Modest", - "moves": [["Moonblast"], ["Focus Blast"], ["Aromatherapy"], ["Defog"]] - }, { - "species": "Xerneas", - "item": ["Choice Scarf"], - "ability": ["Fairy Aura"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Modest", - "moves": [["Moonblast"], ["Hidden Power Fire"], ["Aromatherapy"], ["Thunder"]] - }, { - "species": "Xerneas", - "item": ["Choice Scarf"], - "ability": ["Fairy Aura"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Modest", - "moves": [["Moonblast"], ["Hidden Power Fire"], ["Aromatherapy"], ["Defog"]] }, { "species": "Xerneas", "item": ["Choice Specs"], @@ -112,13 +84,6 @@ "ivs": {"atk": 0}, "nature": "Timid", "moves": [["Moonblast"], ["Grass Knot"], ["Thunder"], ["Hidden Power Fire"]] - }, { - "species": "Xerneas", - "item": ["Expert Belt"], - "ability": ["Fairy Aura"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Hasty", - "moves": [["Moonblast"], ["Close Combat"], ["Rock Slide"], ["Hidden Power Fire"]] }] }, "arceusground": { @@ -129,14 +94,7 @@ "ability": ["Multitype"], "evs": {"hp": 252, "def": 64, "spe": 192}, "nature": "Timid", - "moves": [["Judgment"], ["Ice Beam"], ["Defog"], ["Recover"]] - }, { - "species": "Arceus-Ground", - "item": ["Earth Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, - "nature": "Timid", - "moves": [["Judgment"], ["Ice Beam"], ["Stealth Rock"], ["Recover"]] + "moves": [["Judgment"], ["Ice Beam"], ["Defog", "Stealth Rock"], ["Recover"]] }, { "species": "Arceus-Ground", "item": ["Groundium Z"], @@ -151,13 +109,6 @@ "evs": {"hp": 252, "spa": 4, "spe": 252}, "nature": "Timid", "moves": [["Judgment"], ["Ice Beam"], ["Calm Mind"], ["Recover"]] - }, { - "species": "Arceus-Ground", - "item": ["Earth Plate"], - "ability": ["Multitype"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Earthquake"], ["Stone Edge"], ["Swords Dance"], ["Recover", "Substitute"]] }] }, "gengar": { @@ -168,37 +119,16 @@ "species": "Gengar", "item": ["Gengarite"], "ability": ["Cursed Body"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Will-O-Wisp"], ["Taunt"], ["Hex"], ["Sludge Wave", "Sludge Bomb"]] - }, { - "species": "Gengar", - "item": ["Gengarite"], - "ability": ["Cursed Body"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Taunt"], ["Destiny Bond"], ["Focus Blast", "Sludge Wave"], ["Shadow Ball"]] - }, { - "species": "Gengar", - "item": ["Gengarite"], - "ability": ["Cursed Body"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Perish Song"], ["Protect"], ["Disable"], ["Sludge Wave"]] - }, { - "species": "Gengar", - "item": ["Gengarite"], - "ability": ["Cursed Body"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, + "evs": {"spa": 252, "spd": 4, "spe": 252}, "nature": "Timid", - "moves": [["Perish Song"], ["Protect"], ["Substitute"], ["Sludge Wave"]] + "moves": [["Will-O-Wisp"], ["Taunt", "Substitute"], ["Hex"], ["Sludge Wave"]] }, { "species": "Gengar", "item": ["Gengarite"], "ability": ["Cursed Body"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, + "evs": {"spa": 252, "spd": 4, "spe": 252}, "nature": "Timid", - "moves": [["Perish Song"], ["Substitute"], ["Disable"], ["Sludge Wave"]] + "moves": [["Shadow Ball"], ["Sludge Wave"], ["Icy Wind", "Thunder"], ["Taunt", "Substitute"]] }] }, "salamence": { @@ -216,23 +146,9 @@ "species": "Salamence", "item": ["Salamencite"], "ability": ["Intimidate"], - "evs": {"hp": 184, "atk": 136, "spe": 188}, + "evs": {"hp": 44, "atk": 252, "spe": 212}, "nature": "Adamant", "moves": [["Dragon Dance"], ["Double-Edge"], ["Roost"], ["Facade", "Earthquake"]] - }, { - "species": "Salamence", - "item": ["Salamencite"], - "ability": ["Intimidate"], - "evs": {"hp": 248, "def": 244, "spe": 16}, - "nature": "Impish", - "moves": [["Body Slam"], ["Wish"], ["Roost"], ["Dragon Tail"]] - }, { - "species": "Salamence", - "item": ["Salamencite"], - "ability": ["Intimidate"], - "evs": {"hp": 248, "atk": 68, "def": 124, "spe": 68}, - "nature": "Impish", - "moves": [["Return"], ["Substitute"], ["Dragon Dance"], ["Roost"]] }] }, "marshadow": { @@ -250,14 +166,7 @@ "ability": ["Technician"], "evs": {"atk": 252, "spa": 4, "spe": 252}, "nature": "Naive", - "moves": [["Spectral Thief"], ["Close Combat"], ["Rock Tomb"], ["Shadow Sneak"]] - }, { - "species": "Marshadow", - "item": ["Life Orb"], - "ability": ["Technician"], - "evs": {"atk": 252, "spa": 4, "spe": 252}, - "nature": "Naive", - "moves": [["Spectral Thief"], ["Close Combat"], ["Hidden Power Ice"], ["Shadow Sneak"]] + "moves": [["Spectral Thief"], ["Close Combat"], ["Rock Tomb", "Hidden Power Ice"], ["Shadow Sneak"]] }] }, "hooh": { @@ -268,21 +177,14 @@ "ability": ["Regenerator"], "evs": {"hp": 252, "def": 204, "spd": 52}, "nature": "Impish", - "moves": [["Sacred Fire"], ["Brave Bird"], ["Toxic"], ["Recover"]] - }, { - "species": "Ho-Oh", - "item": ["Leftovers"], - "ability": ["Regenerator"], - "evs": {"hp": 252, "def": 204, "spd": 52}, - "nature": "Impish", - "moves": [["Sacred Fire"], ["Defog"], ["Toxic"], ["Recover"]] + "moves": [["Sacred Fire"], ["Brave Bird", "Defog"], ["Toxic"], ["Recover"]] }, { "species": "Ho-Oh", "item": ["Life Orb"], "ability": ["Regenerator"], - "evs": {"hp": 224, "atk": 136, "spd": 72, "spe": 76}, + "evs": {"hp": 104, "atk": 252, "spe": 152}, "nature": "Adamant", - "moves": [["Brave Bird"], ["Sacred Fire"], ["Recover"], ["Toxic", "Earthquake"]] + "moves": [["Brave Bird"], ["Sacred Fire"], ["Recover"], ["Toxic"]] }, { "species": "Ho-Oh", "item": ["Choice Band"], @@ -300,7 +202,7 @@ "ability": ["Dark Aura"], "evs": {"atk": 4, "spa": 252, "spe": 252}, "nature": "Rash", - "moves": [["Oblivion Wing"], ["Dark Pulse"], ["Sucker Punch", "Knock Off"], ["Taunt", "Heat Wave"]] + "moves": [["Oblivion Wing"], ["Dark Pulse"], ["Sucker Punch", "Toxic"], ["Taunt"]] }, { "species": "Yveltal", "item": ["Choice Scarf"], @@ -341,21 +243,7 @@ "ability": ["Justified"], "evs": {"atk": 252, "spd": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Close Combat"], ["Meteor Mash"], ["Bullet Punch"], ["Swords Dance", "Stone Edge"]] - }, { - "species": "Lucario", - "item": ["Lucarionite"], - "ability": ["Justified"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Nasty Plot"], ["Focus Blast", "Aura Sphere"], ["Flash Cannon"], ["Vacuum Wave"]] - }, { - "species": "Lucario", - "item": ["Lucarionite"], - "ability": ["Justified"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Close Combat"], ["Meteor Mash"], ["Bullet Punch"], ["Earthquake", "Ice Punch"]] + "moves": [["Close Combat"], ["Meteor Mash"], ["Bullet Punch"], ["Swords Dance"]] }] }, "kyogre": { @@ -383,14 +271,7 @@ "moves": [["Scald"], ["Rest"], ["Sleep Talk"], ["Ice Beam"]] }, { "species": "Kyogre", - "item": ["Choice Specs"], - "ability": ["Drizzle"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Water Spout"], ["Origin Pulse"], ["Ice Beam"], ["Thunder"]] - }, { - "species": "Kyogre", - "item": ["Choice Scarf"], + "item": ["Choice Specs", "Choice Scarf"], "ability": ["Drizzle"], "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Modest", @@ -405,28 +286,14 @@ "ability": ["Multitype"], "evs": {"hp": 252, "def": 64, "spe": 192}, "nature": "Timid", - "moves": [["Ice Beam"], ["Judgment"], ["Toxic", "Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Water", - "item": ["Splash Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, - "nature": "Timid", - "moves": [["Ice Beam"], ["Defog"], ["Toxic", "Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Water", - "item": ["Splash Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, - "nature": "Timid", - "moves": [["Fire Blast"], ["Ice Beam"], ["Toxic"], ["Recover"]] + "moves": [["Ice Beam"], ["Judgment"], ["Toxic"], ["Recover"]] }, { "species": "Arceus-Water", "item": ["Splash Plate"], "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, + "evs": {"hp": 252, "def": 16, "spa": 144, "spe": 96}, "nature": "Timid", - "moves": [["Fire Blast"], ["Defog"], ["Toxic"], ["Recover"]] + "moves": [["Ice Beam"], ["Judgment"], ["Stealth Rock"], ["Recover"]] }] }, "arceusfairy": { @@ -442,7 +309,7 @@ "species": "Arceus-Fairy", "item": ["Pixie Plate"], "ability": ["Multitype"], - "evs": {"hp": 252, "def": 64, "spe": 192}, + "evs": {"hp": 252, "spa": 4, "spe": 252}, "nature": "Timid", "moves": [["Calm Mind"], ["Recover"], ["Judgment"], ["Earth Power"]] }] @@ -466,28 +333,7 @@ "ability": ["Pressure"], "evs": {"atk": 4, "spa": 252, "spe": 252}, "nature": "Rash", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Extreme Speed", "Dark Pulse"]] - }, { - "species": "Deoxys-Attack", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Spikes"], ["Psycho Boost"], ["Knock Off"], ["Extreme Speed"]] - }, { - "species": "Deoxys-Attack", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Stealth Rock"], ["Psycho Boost"], ["Knock Off"], ["Extreme Speed"]] - }, { - "species": "Deoxys-Attack", - "item": ["Life Orb"], - "ability": ["Pressure"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Rash", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Dark Pulse", "Extreme Speed"]] + "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Spikes", "Dark Pulse", "Extreme Speed"]] }] }, "giratinaorigin": { @@ -538,13 +384,6 @@ "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Adamant", "moves": [["Dragon Dance"], ["Thousand Arrows"], ["Substitute"], ["Dragon Tail", "Glare"]] - }, { - "species": "Zygarde", - "item": ["Dragonium Z"], - "ability": ["Power Construct"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Dragon Dance"], ["Thousand Arrows"], ["Substitute"], ["Outrage", "Dragon Tail"]] }] }, "rayquaza": { @@ -601,21 +440,14 @@ "evs": {"hp": 252, "atk": 4, "spd": 252}, "ivs": {"spe": 0}, "nature": "Sassy", - "moves": [["Spikes"], ["Leech Seed"], ["Gyro Ball", "Power Whip"], ["Protect"]] + "moves": [["Spikes"], ["Leech Seed"], ["Gyro Ball"], ["Power Whip", "Protect"]] }] }, "toxapex": { "flags": {}, "sets": [{ "species": "Toxapex", - "item": ["Rocky Helmet"], - "ability": ["Regenerator"], - "evs": {"hp": 252, "def": 252, "spd": 4}, - "nature": "Bold", - "moves": [["Toxic Spikes"], ["Scald"], ["Recover"], ["Haze"]] - }, { - "species": "Toxapex", - "item": ["Shed Shell"], + "item": ["Rocky Helmet", "Shed Shell"], "ability": ["Regenerator"], "evs": {"hp": 252, "def": 252, "spd": 4}, "nature": "Bold", @@ -630,7 +462,7 @@ "ability": ["Multiscale"], "evs": {"hp": 252, "def": 160, "spe": 96}, "nature": "Bold", - "moves": [["Toxic"], ["Roost"], ["Whirlwind", "Dragon Tail"], ["Substitute", "Ice Beam"]] + "moves": [["Toxic"], ["Roost"], ["Whirlwind", "Dragon Tail"], ["Psychic", "Ice Beam"]] }] }, "arceus": { @@ -641,34 +473,16 @@ "ability": ["Multitype"], "evs": {"hp": 240, "atk": 252, "spe": 16}, "nature": "Adamant", - "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Earthquake"]] + "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Recover"]] }, { "species": "Arceus", "item": ["Normalium Z"], "ability": ["Multitype"], - "evs": {"hp": 132, "atk": 252, "spe": 124}, - "nature": "Adamant", - "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Refresh"]] - }, { - "species": "Arceus", - "item": ["Leftovers"], - "ability": ["Multitype"], "evs": {"hp": 240, "atk": 252, "spe": 16}, "nature": "Adamant", "moves": [["Swords Dance"], ["Extreme Speed"], ["Shadow Claw"], ["Substitute"]] }] }, - "arceusbug": { - "flags": {}, - "sets": [{ - "species": "Arceus-Bug", - "item": ["Buginium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["X-Scissor"], ["Earthquake"], ["Stone Edge"]] - }] - }, "deoxysspeed": { "flags": {}, "sets": [{ @@ -701,69 +515,26 @@ "item": ["Ghostium Z"], "ability": ["Shadow Shield"], "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Calm Mind"], ["Moongeist Beam"], ["Psyshock"], ["Focus Blast"]] - }, { - "species": "Lunala", - "item": ["Psychium Z"], - "ability": ["Shadow Shield"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Hypnosis"], ["Calm Mind"], ["Moongeist Beam"], ["Focus Blast"]] - }, { - "species": "Lunala", - "item": ["Choice Specs"], - "ability": ["Shadow Shield"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Moongeist Beam"], ["Psyshock"], ["Focus Blast"], ["Ice Beam"]] + "moves": [["Calm Mind"], ["Moongeist Beam"], ["Psyshock"], ["Ice Beam", "Focus Blast"]] }, { "species": "Lunala", "item": ["Choice Scarf"], "ability": ["Shadow Shield"], "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Modest", - "moves": [["Moongeist Beam"], ["Psyshock"], ["Focus Blast"], ["Ice Beam"]] + "moves": [["Moongeist Beam"], ["Psyshock"], ["Ice Beam"], ["Focus Blast"]] }] }, "dialga": { "flags": {}, "sets": [{ - "species": "Dialga", - "item": ["Dragonium Z"], - "ability": ["Pressure"], - "evs": {"hp": 180, "spa": 252, "spe": 76}, - "nature": "Modest", - "moves": [["Draco Meteor"], ["Fire Blast"], ["Stealth Rock"], ["Toxic", "Thunder"]] - }, { - "species": "Dialga", - "item": ["Dragonium Z"], - "ability": ["Pressure"], - "evs": {"hp": 252, "def": 4, "spa": 252}, - "nature": "Quiet", - "moves": [["Trick Room"], ["Draco Meteor"], ["Flash Cannon"], ["Fire Blast"]] - }, { - "species": "Dialga", - "item": ["Shuca Berry"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Stealth Rock"], ["Draco Meteor"], ["Flash Cannon"], ["Fire Blast"]] - }, { - "species": "Dialga", - "item": ["Chople Berry"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Stealth Rock"], ["Draco Meteor"], ["Flash Cannon"], ["Fire Blast"]] - }, { "species": "Dialga", "item": ["Life Orb"], "ability": ["Pressure"], "evs": {"hp": 104, "spa": 252, "spe": 152}, - "ivs": {"atk": 0}, "nature": "Modest", - "moves": [["Draco Meteor"], ["Fire Blast"], ["Thunder", "Toxic"], ["Ice Beam", "Flash Cannon"]] + "moves": [["Draco Meteor"], ["Fire Blast"], ["Stealth Rock"], ["Thunder"]] }] }, "excadrill": { @@ -774,7 +545,7 @@ "ability": ["Mold Breaker"], "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Jolly", - "moves": [["Stealth Rock"], ["Toxic", "Rock Tomb"], ["Earthquake"], ["Rapid Spin"]] + "moves": [["Stealth Rock"], ["Earthquake", "Rock Tomb"], ["Toxic"], ["Rapid Spin"]] }] }, "diancie": { @@ -802,21 +573,13 @@ "megaOnly": 1 }, "sets": [{ - "species": "Scizor", - "item": ["Scizorite"], - "ability": ["Light Metal"], - "evs": {"hp": 252, "atk": 108, "spd": 148}, - "ivs": {"spe": 0}, - "nature": "Brave", - "moves": [["Bullet Punch"], ["Roost"], ["U-turn"], ["Swords Dance", "Toxic"]] - }, { "species": "Scizor", "item": ["Scizorite"], "ability": ["Light Metal"], "evs": {"hp": 252, "atk": 4, "spd": 252}, "ivs": {"spe": 0}, "nature": "Sassy", - "moves": [["Bullet Punch"], ["Roost"], ["U-turn"], ["Toxic", "Swords Dance"]] + "moves": [["Bullet Punch"], ["Roost"], ["U-turn"], ["Swords Dance", "Toxic"]] }] }, "tapulele": { @@ -828,13 +591,6 @@ "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Timid", "moves": [["Psychic"], ["Moonblast"], ["Grass Knot", "Aromatherapy"], ["Nature's Madness"]] - }, { - "species": "Tapu Lele", - "item": ["Terrain Extender"], - "ability": ["Psychic Surge"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Psychic"], ["Moonblast"], ["Taunt"], ["Nature's Madness"]] }] }, "mewtwo": { @@ -852,21 +608,7 @@ "ability": ["Pressure"], "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Low Kick"], ["Taunt"], ["Ice Beam"], ["Stone Edge"]] - }, { - "species": "Mewtwo", - "item": ["Mewtwonite X"], - "ability": ["Pressure"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Low Kick"], ["Taunt"], ["Ice Beam"], ["Zen Headbutt"]] - }, { - "species": "Mewtwo", - "item": ["Mewtwonite X"], - "ability": ["Pressure"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Low Kick"], ["Taunt"], ["Zen Headbutt"], ["Stone Edge"]] + "moves": [["Low Kick"], ["Taunt"], ["Ice Beam", "Zen Headbutt"], ["Stone Edge"]] }, { "species": "Mewtwo", "item": ["Psychium Z"], @@ -882,23 +624,9 @@ "species": "Blissey", "item": ["Shed Shell"], "ability": ["Natural Cure"], - "evs": {"def": 252, "spd": 252, "spe": 4}, - "nature": "Calm", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Confide"]] - }, { - "species": "Blissey", - "item": ["Shed Shell"], - "ability": ["Natural Cure"], - "evs": {"def": 252, "spd": 252, "spe": 4}, - "nature": "Calm", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Stealth Rock"]] - }, { - "species": "Blissey", - "item": ["Shed Shell"], - "ability": ["Natural Cure"], - "evs": {"def": 252, "spd": 252, "spe": 4}, + "evs": {"hp": 4, "def": 252, "spd": 252}, "nature": "Calm", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Wish", "Seismic Toss"]] + "moves": [["Soft-Boiled"], ["Toxic", "Wish"], ["Heal Bell"], ["Confide"]] }] }, "chansey": { @@ -909,14 +637,7 @@ "ability": ["Natural Cure"], "evs": {"hp": 4, "def": 252, "spd": 252}, "nature": "Bold", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Wish"]] - }, { - "species": "Chansey", - "item": ["Eviolite"], - "ability": ["Natural Cure"], - "evs": {"hp": 4, "def": 252, "spd": 252}, - "nature": "Bold", - "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Stealth Rock"]] + "moves": [["Soft-Boiled"], ["Toxic"], ["Heal Bell"], ["Wish", "Stealth Rock"]] }] }, "arceusdark": { @@ -934,7 +655,7 @@ "ability": ["Multitype"], "evs": {"hp": 252, "def": 160, "spe": 96}, "nature": "Timid", - "moves": [["Judgment"], ["Defog"], ["Recover"], ["Toxic", "Will-O-Wisp"]] + "moves": [["Judgment"], ["Ice Beam"], ["Recover"], ["Toxic", "Will-O-Wisp"]] }] }, "tyranitar": { @@ -953,20 +674,6 @@ "evs": {"hp": 248, "def": 8, "spd": 252}, "nature": "Careful", "moves": [["Stealth Rock"], ["Rock Tomb"], ["Pursuit"], ["Toxic", "Rest"]] - }, { - "species": "Tyranitar", - "item": ["Shuca Berry"], - "ability": ["Sand Stream"], - "evs": {"hp": 248, "def": 8, "spd": 252}, - "nature": "Careful", - "moves": [["Stealth Rock"], ["Rock Tomb"], ["Pursuit"], ["Toxic", "Rest"]] - }, { - "species": "Tyranitar", - "item": ["Tyranitarite"], - "ability": ["Sand Stream"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Dragon Dance"], ["Stone Edge"], ["Crunch", "Ice Punch"], ["Earthquake", "Taunt"]] }] }, "magearna": { @@ -978,13 +685,6 @@ "evs": {"hp": 252, "spa": 4, "spd": 252}, "nature": "Calm", "moves": [["Fleur Cannon"], ["Heart Swap"], ["Volt Switch"], ["Pain Split"]] - }, { - "species": "Magearna", - "item": ["Choice Specs"], - "ability": ["Soul-Heart"], - "evs": {"hp": 252, "spa": 252, "spd": 4}, - "nature": "Modest", - "moves": [["Fleur Cannon"], ["Volt Switch"], ["Grass Knot"], ["Hidden Power Fire"]] }, { "species": "Magearna", "item": ["Normalium Z"], @@ -1022,13 +722,6 @@ "megaOnly": 1 }, "sets": [{ - "species": "Gyarados", - "item": ["Gyaradosite"], - "ability": ["Intimidate"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Dragon Dance"], ["Crunch"], ["Taunt"], ["Waterfall", "Earthquake"]] - }, { "species": "Gyarados", "item": ["Gyaradosite"], "ability": ["Intimidate"], @@ -1040,13 +733,6 @@ "palkia": { "flags": {}, "sets": [{ - "species": "Palkia", - "item": ["Psychium Z"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Heal Block"], ["Hydro Pump"], ["Spacial Rend"], ["Fire Blast", "Thunder"]] - }, { "species": "Palkia", "item": ["Life Orb"], "ability": ["Pressure"], @@ -1055,17 +741,6 @@ "moves": [["Hydro Pump"], ["Spacial Rend", "Draco Meteor"], ["Thunder"], ["Focus Punch", "Fire Blast"]] }] }, - "bronzong": { - "flags": {}, - "sets": [{ - "species": "Bronzong", - "item": ["Leftovers"], - "ability": ["Levitate"], - "evs": {"hp": 252, "atk": 112, "spd": 144}, - "nature": "Sassy", - "moves": [["Stealth Rock"], ["Gyro Ball"], ["Toxic"], ["Protect"]] - }] - }, "blaziken": { "flags": {}, "sets": [{ @@ -1075,25 +750,11 @@ "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Adamant", "moves": [["Swords Dance", "Protect"], ["Flare Blitz"], ["Low Kick"], ["Stone Edge"]] - }, { - "species": "Blaziken", - "item": ["Life Orb"], - "ability": ["Speed Boost"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Protect"], ["Flare Blitz"], ["Low Kick"], ["Hidden Power Ice"]] }] }, "skarmory": { "flags": {}, "sets": [{ - "species": "Skarmory", - "item": ["Leftovers"], - "ability": ["Sturdy"], - "evs": {"hp": 252, "def": 252, "spd": 4}, - "nature": "Bold", - "moves": [["Spikes"], ["Toxic"], ["Whirlwind"], ["Roost"]] - }, { "species": "Skarmory", "item": ["Shed Shell"], "ability": ["Sturdy"], @@ -1108,16 +769,9 @@ "species": "Gothitelle", "item": ["Leftovers"], "ability": ["Shadow Tag"], - "evs": {"hp": 252, "spa": 4, "spd": 252}, - "nature": "Calm", - "moves": [["Charm", "Heal Bell"], ["Calm Mind"], ["Rest"], ["Psyshock"]] - }, { - "species": "Gothitelle", - "item": ["Leftovers"], - "ability": ["Shadow Tag"], - "evs": {"hp": 252, "spa": 4, "spd": 252}, + "evs": {"hp": 244, "def": 108, "spd": 156}, "nature": "Calm", - "moves": [["Charm", "Heal Bell"], ["Confide"], ["Rest"], ["Taunt"]] + "moves": [["Charm"], ["Confide"], ["Rest"], ["Taunt"]] }] }, "buzzwole": { @@ -1128,25 +782,7 @@ "ability": ["Beast Boost"], "evs": {"hp": 248, "def": 184, "spd": 76}, "nature": "Impish", - "moves": [["Hammer Arm"], ["Toxic"], ["Roost"], ["Earthquake"]] - }] - }, - "landorustherian": { - "flags": {}, - "sets": [{ - "species": "Landorus-Therian", - "item": ["Flyinium Z"], - "ability": ["Intimidate"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Earthquake"], ["Fly"], ["Stealth Rock"], ["Swords Dance"]] - }, { - "species": "Landorus-Therian", - "item": ["Rocky Helmet"], - "ability": ["Intimidate"], - "evs": {"hp": 252, "def": 252, "spe": 4}, - "nature": "Impish", - "moves": [["Earthquake"], ["U-turn"], ["Stealth Rock"], ["Stone Edge"]] + "moves": [["Hammer Arm"], ["Toxic"], ["Roost"], ["Earthquake", "Taunt"]] }] }, "arceusghost": { @@ -1157,14 +793,7 @@ "ability": ["Multitype"], "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Jolly", - "moves": [["Swords Dance"], ["Shadow Force"], ["Brick Break"], ["Shadow Claw", "Extreme Speed"]] - }, { - "species": "Arceus-Ghost", - "item": ["Spooky Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Will-O-Wisp", "Toxic"], ["Recover"]] + "moves": [["Swords Dance"], ["Shadow Force"], ["Substitute"], ["Shadow Claw", "Brick Break"]] }] }, "cloyster": { @@ -1175,26 +804,12 @@ "ability": ["Skill Link"], "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Spikes"], ["Rapid Spin"], ["Icicle Spear"], ["Shell Smash"]] - }, { - "species": "Cloyster", - "item": ["Focus Sash"], - "ability": ["Skill Link"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Toxic Spikes"], ["Rapid Spin"], ["Icicle Spear"], ["Shell Smash"]] + "moves": [["Spikes", "Toxic Spikes"], ["Rapid Spin"], ["Icicle Spear"], ["Shell Smash"]] }] }, "arceuspoison": { "flags": {}, "sets": [{ - "species": "Arceus-Poison", - "item": ["Toxic Plate"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Poison Jab"], ["Earthquake"], ["Recover", "Extreme Speed"]] - }, { "species": "Arceus-Poison", "item": ["Toxic Plate"], "ability": ["Multitype"], @@ -1211,46 +826,18 @@ "ability": ["Multitype"], "evs": {"hp": 252, "spa": 4, "spe": 252}, "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Recover"], ["Refresh", "Fire Blast"]] + "moves": [["Calm Mind"], ["Judgment"], ["Recover"], ["Refresh"]] }] }, "shayminsky": { "flags": {}, "sets": [{ "species": "Shaymin-Sky", - "item": ["Leftovers"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Seed Flare"], ["Air Slash"], ["Substitute"], ["Leech Seed"]] - }, { - "species": "Shaymin-Sky", - "item": ["Choice Scarf"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Seed Flare"], ["Air Slash"], ["Hidden Power Rock"], ["Healing Wish"]] - }, { - "species": "Shaymin-Sky", - "item": ["Choice Scarf"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Seed Flare"], ["Air Slash"], ["Hidden Power Ice"], ["Healing Wish"]] - }, { - "species": "Shaymin-Sky", - "item": ["Normalium Z"], - "ability": ["Serene Grace"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Seed Flare"], ["Air Slash"], ["Earth Power"], ["Growth"]] - }, { - "species": "Shaymin-Sky", - "item": ["Normalium Z"], + "item": ["Life Orb"], "ability": ["Serene Grace"], "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Seed Flare"], ["Air Slash"], ["Hidden Power Ice"], ["Growth"]] + "moves": [["Seed Flare"], ["Air Slash"], ["Healing Wish"], ["Hidden Power Ice", "Hidden Power Rock"]] }] }, "smeargle": { @@ -1261,14 +848,7 @@ "ability": ["Own Tempo"], "evs": {"hp": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Sticky Web"], ["Nuzzle"], ["Spore"], ["Skill Swap"]] - }, { - "species": "Smeargle", - "item": ["Focus Sash"], - "ability": ["Own Tempo"], - "evs": {"hp": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Sticky Web"], ["Nuzzle"], ["Spore"], ["Rapid Spin"]] + "moves": [["Sticky Web"], ["Nuzzle"], ["Taunt"], ["Whirlwind", "Toxic Thread"]] }] }, "klefki": { @@ -1303,21 +883,7 @@ "ability": ["Multitype"], "evs": {"hp": 248, "def": 8, "spe": 252}, "nature": "Timid", - "moves": [["Judgment"], ["Calm Mind"], ["Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Rock", - "item": ["Stone Plate"], - "ability": ["Multitype"], - "evs": {"hp": 248, "def": 8, "spe": 252}, - "nature": "Timid", - "moves": [["Judgment"], ["Defog"], ["Will-O-Wisp"], ["Recover"]] - }, { - "species": "Arceus-Rock", - "item": ["Rockium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Stone Edge"], ["Earthquake"], ["Substitute", "Recover"]] + "moves": [["Judgment"], ["Calm Mind", "Defog"], ["Will-O-Wisp"], ["Recover"]] }] }, "zekrom": { @@ -1329,20 +895,6 @@ "evs": {"hp": 64, "atk": 252, "spe": 192}, "nature": "Adamant", "moves": [["Hone Claws"], ["Outrage"], ["Bolt Strike"], ["Substitute", "Draco Meteor"]] - }, { - "species": "Zekrom", - "item": ["Shuca Berry"], - "ability": ["Teravolt"], - "evs": {"hp": 64, "atk": 252, "spe": 192}, - "nature": "Adamant", - "moves": [["Hone Claws"], ["Bolt Strike"], ["Outrage"], ["Dragon Claw"]] - }, { - "species": "Zekrom", - "item": ["Choice Scarf"], - "ability": ["Teravolt"], - "evs": {"atk": 252, "spa": 4, "spe": 252}, - "nature": "Naughty", - "moves": [["Bolt Strike"], ["Outrage"], ["Draco Meteor"], ["Volt Switch"]] }] }, "dugtrio": { @@ -1367,17 +919,6 @@ "moves": [["Calm Mind"], ["Judgment"], ["Ice Beam"], ["Recover"]] }] }, - "shuckle": { - "flags": {}, - "sets": [{ - "species": "Shuckle", - "item": ["Mental Herb"], - "ability": ["Sturdy"], - "evs": {"hp": 252, "def": 4, "spd": 252}, - "nature": "Calm", - "moves": [["Sticky Web"], ["Rock Tomb"], ["Encore"], ["Toxic"]] - }] - }, "ditto": { "flags": {}, "sets": [{ @@ -1394,18 +935,11 @@ "flags": {}, "sets": [{ "species": "Darkrai", - "item": ["Psychium Z"], + "item": ["Life Orb"], "ability": ["Bad Dreams"], "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Nasty Plot"], ["Dark Pulse"], ["Hypnosis"], ["Sludge Bomb"]] - }, { - "species": "Darkrai", - "item": ["Choice Scarf"], - "ability": ["Bad Dreams"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Dark Pulse"], ["Ice Beam"], ["Trick"], ["Haze"]] + "moves": [["Nasty Plot"], ["Dark Pulse"], ["Hypnosis"], ["Thunder", "Sludge Bomb"]] }] }, "tapukoko": { @@ -1419,52 +953,9 @@ "moves": [["Thunder"], ["Nature's Madness"], ["Taunt"], ["U-turn"]] }] }, - "solgaleo": { - "flags": {}, - "sets": [{ - "species": "Solgaleo", - "item": ["Choice Scarf"], - "ability": ["Full Metal Body"], - "evs": {"atk": 252, "spd": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Sunsteel Strike"], ["Flare Blitz"], ["Earthquake"], ["Stone Edge"]] - }, { - "species": "Solgaleo", - "item": ["Normalium Z"], - "ability": ["Full Metal Body"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Adamant", - "moves": [["Splash"], ["Sunsteel Strike"], ["Earthquake"], ["Flame Charge"]] - }] - }, - "arceusice": { - "flags": {}, - "sets": [{ - "species": "Arceus-Ice", - "item": ["Icicle Plate"], - "ability": ["Multitype"], - "evs": {"hp": 248, "spa": 8, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Fire Blast"], ["Recover"]] - }, { - "species": "Arceus-Ice", - "item": ["Icium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Ice Beam"], ["Fire Blast"], ["Thunder"]] - }] - }, "arceussteel": { "flags": {}, "sets": [{ - "species": "Arceus-Steel", - "item": ["Steelium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Swords Dance"], ["Iron Head"], ["Earthquake"], ["Stone Edge", "Recover"]] - }, { "species": "Arceus-Steel", "item": ["Iron Plate"], "ability": ["Multitype"], @@ -1477,7 +968,7 @@ "flags": {}, "sets": [{ "species": "Aegislash", - "item": ["Leftovers"], + "item": ["Iron Ball"], "ability": ["Stance Change"], "evs": {"hp": 252, "atk": 4, "spd": 252}, "nature": "Sassy", @@ -1493,24 +984,6 @@ "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Naive", "moves": [["Earth Power"], ["Rock Slide"], ["Hidden Power Ice"], ["Knock Off"]] - }, { - "species": "Landorus", - "item": ["Life Orb"], - "ability": ["Sheer Force"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Earth Power"], ["Rock Slide"], ["Hidden Power Ice"], ["Stealth Rock"]] - }] - }, - "pheromosa": { - "flags": {}, - "sets": [{ - "species": "Pheromosa", - "item": ["Life Orb"], - "ability": ["Beast Boost"], - "evs": {"atk": 252, "spa": 4, "spe": 252}, - "nature": "Naive", - "moves": [["U-turn"], ["Low Kick"], ["Ice Beam"], ["Rapid Spin"]] }] }, "arceusdragon": { @@ -1522,44 +995,17 @@ "evs": {"hp": 252, "def": 64, "spe": 192}, "nature": "Timid", "moves": [["Judgment"], ["Fire Blast"], ["Defog"], ["Recover"]] - }, { - "species": "Arceus-Dragon", - "item": ["Dragonium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Swords Dance"], ["Outrage"], ["Earthquake"], ["Iron Tail"]] }] }, "kyuremwhite": { "flags": {}, "sets": [{ - "species": "Kyurem-White", - "item": ["Choice Specs"], - "ability": ["Turboblaze"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Ice Beam"], ["Draco Meteor"], ["Fusion Flare"], ["Earth Power"]] - }, { "species": "Kyurem-White", "item": ["Life Orb"], "ability": ["Turboblaze"], "evs": {"hp": 4, "spa": 252, "spe": 252}, "nature": "Timid", - "moves": [["Ice Beam"], ["Draco Meteor"], ["Fusion Flare"], ["Stone Edge", "Focus Blast"]] - }] - }, - "kangaskhan": { - "flags": { - "megaOnly": 1 - }, - "sets": [{ - "species": "Kangaskhan", - "item": ["Kangaskhanite"], - "ability": ["Early Bird"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Jolly", - "moves": [["Seismic Toss"], ["Body Slam"], ["Crunch", "Ice Punch"], ["Fake Out"]] + "moves": [["Ice Beam"], ["Draco Meteor"], ["Fusion Flare"], ["Roost"]] }] }, "arceusgrass": { @@ -1573,125 +1019,6 @@ "moves": [["Grass Knot"], ["Ice Beam"], ["Fire Blast"], ["Recover"]] }] }, - "genesect": { - "flags": {}, - "sets": [{ - "species": "Genesect", - "item": ["Choice Specs"], - "ability": ["Download"], - "evs": {"hp": 68, "spa": 188, "spe": 252}, - "nature": "Timid", - "moves": [["Techno Blast"], ["Flamethrower", "Dark Pulse"], ["Flash Cannon"], ["U-turn"]] - }, { - "species": "Genesect", - "item": ["Choice Scarf"], - "ability": ["Download"], - "evs": {"hp": 4, "atk": 252, "spe": 252}, - "nature": "Hasty", - "moves": [["U-turn"], ["Iron Head"], ["Flamethrower"], ["Ice Beam"]] - }] - }, - "arceusfighting": { - "flags": {}, - "sets": [{ - "species": "Arceus-Fighting", - "item": ["Fist Plate"], - "ability": ["Multitype"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Judgment"], ["Ice Beam"], ["Shadow Ball"], ["Recover"]] - }] - }, - "arceusfire": { - "flags": {}, - "sets": [{ - "species": "Arceus-Fire", - "item": ["Firium Z"], - "ability": ["Multitype"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Blast Burn"], ["Ice Beam"], ["Earth Power", "Thunder"]] - }] - }, - "arceuspsychic": { - "flags": {}, - "sets": [{ - "species": "Arceus-Psychic", - "item": ["Mind Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "spd": 64, "spe": 192}, - "nature": "Calm", - "moves": [["Fire Blast"], ["Dark Pulse"], ["Recover"], ["Toxic", "Judgment"]] - }, { - "species": "Arceus-Psychic", - "item": ["Mind Plate"], - "ability": ["Multitype"], - "evs": {"hp": 252, "spa": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Calm Mind"], ["Judgment"], ["Recover"], ["Ice Beam", "Dark Pulse"]] - }] - }, - "deoxys": { - "flags": {}, - "sets": [{ - "species": "Deoxys", - "item": ["Life Orb"], - "ability": ["Pressure"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Rash", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Superpower"], ["Extreme Speed"]] - }, { - "species": "Deoxys", - "item": ["Choice Scarf"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Psycho Boost"], ["Ice Beam"], ["Focus Blast"], ["Trick"]] - }, { - "species": "Deoxys", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Spikes"], ["Stealth Rock"], ["Taunt"], ["Psycho Boost"]] - }, { - "species": "Deoxys", - "item": ["Focus Sash"], - "ability": ["Pressure"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Nasty Plot"], ["Psyshock", "Psycho Boost"], ["Ice Beam"], ["Focus Blast"]] - }] - }, - "deoxysdefense": { - "flags": {}, - "sets": [{ - "species": "Deoxys-Defense", - "item": ["Leftovers"], - "ability": ["Pressure"], - "evs": {"hp": 252, "def": 252, "spd": 4}, - "nature": "Bold", - "moves": [["Spikes"], ["Recover"], ["Toxic"], ["Knock Off"]] - }] - }, - "reshiram": { - "flags": {}, - "sets": [{ - "species": "Reshiram", - "item": ["Life Orb"], - "ability": ["Turboblaze"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Blue Flare"], ["Draco Meteor"], ["Stone Edge"], ["Flame Charge"]] - }, { - "species": "Reshiram", - "item": ["Grassium Z"], - "ability": ["Turboblaze"], - "evs": {"def": 4, "spa": 252, "spe": 252}, - "nature": "Modest", - "moves": [["Blue Flare"], ["Draco Meteor"], ["Solar Beam"], ["Roost"]] - }] - }, "wobbuffet": { "flags": {}, "sets": [{ @@ -1704,17 +1031,6 @@ "moves": [["Encore"], ["Counter"], ["Mirror Coat"], ["Safeguard"]] }] }, - "xurkitree": { - "flags": {}, - "sets": [{ - "species": "Xurkitree", - "item": ["Psychium Z"], - "ability": ["Beast Boost"], - "evs": {"hp": 4, "spa": 252, "spe": 252}, - "nature": "Timid", - "moves": [["Hypnosis"], ["Tail Glow"], ["Thunderbolt"], ["Grass Knot"]] - }] - }, "latias": { "flags": { "megaOnly": 1 @@ -1728,49 +1044,13 @@ "moves": [["Ice Beam"], ["Defog"], ["Toxic"], ["Roost"]] }] }, - "greninja": { - "flags": {}, - "sets": [{ - "species": "Greninja", - "item": ["Focus Sash"], - "ability": ["Protean"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Spikes"], ["Hydro Pump"], ["Taunt"], ["Shadow Sneak"]] - }, { - "species": "Greninja", - "item": ["Focus Sash"], - "ability": ["Protean"], - "evs": {"spa": 252, "spd": 4, "spe": 252}, - "nature": "Timid", - "moves": [["Toxic Spikes"], ["Hydro Pump"], ["Taunt"], ["Shadow Sneak"]] - }] - }, - "scolipede": { - "flags": {}, - "sets": [{ - "species": "Scolipede", - "item": ["Focus Sash"], - "ability": ["Speed Boost"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Pin Missile"], ["Toxic Spikes"], ["Endeavor"], ["Protect"]] - }, { - "species": "Scolipede", - "item": ["Focus Sash"], - "ability": ["Speed Boost"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Jolly", - "moves": [["Pin Missile"], ["Spikes"], ["Endeavor"], ["Protect"]] - }] - }, "necrozmaduskmane": { "flags": {}, "sets": [{ "species": "Necrozma-Dusk-Mane", "item": ["Solganium Z"], "ability": ["Prism Armor"], - "evs": {"atk": 252, "def": 4, "spe": 252}, + "evs": {"hp": 4, "atk": 252, "spe": 252}, "nature": "Adamant", "moves": [["Rock Polish"], ["Swords Dance"], ["Earthquake"], ["Sunsteel Strike"]] }, { @@ -1784,28 +1064,28 @@ "species": "Necrozma-Dusk-Mane", "item": ["Solganium Z"], "ability": ["Prism Armor"], - "evs": {"atk": 252, "def": 4, "spe": 252}, - "nature": "Adamant", - "moves": [["Swords Dance"], ["Moonlight"], ["Sunsteel Strike"], ["Earthquake"]] - }, { - "species": "Necrozma-Dusk-Mane", - "item": ["Solganium Z"], - "ability": ["Prism Armor"], - "evs": {"hp": 252, "atk": 252, "def": 4}, + "evs": {"hp": 252, "atk": 252, "spd": 4}, "ivs": {"spe": 0}, "nature": "Brave", "moves": [["Swords Dance"], ["Trick Room"], ["Sunsteel Strike"], ["Earthquake"]] + }, { + "species": "Necrozma-Dusk-Mane", + "item": ["Life Orb"], + "ability": ["Prism Armor"], + "evs": {"hp": 108, "atk": 252, "spe": 148}, + "nature": "Adamant", + "moves": [["Swords Dance"], ["Morning Sun"], ["Sunsteel Strike"], ["Earthquake", "Photon Geyser"]] }, { "species": "Necrozma-Dusk-Mane", "item": ["Ultranecrozium Z"], - "ability": ["Neuroforce"], + "ability": ["Prism Armor"], "evs": {"atk": 252, "def": 4, "spe": 252}, "nature": "Jolly", - "moves": [["Swords Dance"], ["Photon Geyser"], ["Earthquake", "Knock Off"], ["Stone Edge", "Sunsteel Strike", "Brick Break"]] + "moves": [["Swords Dance"], ["Photon Geyser"], ["Earthquake"], ["Stone Edge", "Sunsteel Strike"]] }, { "species": "Necrozma-Dusk-Mane", "item": ["Ultranecrozium Z"], - "ability": ["Neuroforce"], + "ability": ["Prism Armor"], "evs": {"def": 4, "spa": 252, "spe": 252}, "nature": "Timid", "moves": [["Calm Mind"], ["Photon Geyser"], ["Heat Wave"], ["Power Gem", "Dragon Pulse"]] @@ -1826,20 +1106,12 @@ "necrozmadawnwings": { "flags": {}, "sets": [{ - "species": "Necrozma-Dawn-Wings", - "item": ["Lunalium Z"], - "ability": ["Prism Armor"], - "evs": {"hp": 252, "def": 4, "spa": 252}, - "ivs": {"spe": 0}, - "nature": "Quiet", - "moves": [["Trick Room"], ["Moongeist Beam"], ["Power Gem"], ["Calm Mind", "Psyshock"]] - }, { "species": "Necrozma-Dawn-Wings", "item": ["Ultranecrozium Z"], "ability": ["Prism Armor"], - "evs": {"atk": 4, "spa": 252, "spe": 252}, - "nature": "Naive", - "moves": [["Moongeist Beam"], ["Power Gem"], ["Brick Break", "Calm Mind"], ["Photon Geyser", "Heat Wave"]] + "evs": {"atk": 252, "def": 32, "spe": 224}, + "nature": "Jolly", + "moves": [["Swords Dance"], ["Photon Geyser"], ["Earthquake"], ["Stone Edge"]] }] } }, From f46e0fc86089b00882bccb3c9095ea68e73ed084 Mon Sep 17 00:00:00 2001 From: whales Date: Thu, 13 Sep 2018 21:21:46 +0930 Subject: [PATCH 055/125] Remove user's text from scrollback log (#4751) --- chat-commands.js | 29 ++++++++++++----------------- roomlogs.js | 21 +++++++++++++++++++++ rooms.js | 18 ++++++++++++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/chat-commands.js b/chat-commands.js index 1e22e4297bf0..1c4bc8fb861f 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -1550,8 +1550,7 @@ const commands = { this.privateModAction(displayMessage); } } - this.add(`|unlink|hide|${userid}`); - if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); + room.hideText([userid, toId(this.inputUsername)]); if (room.isPrivate !== true && room.chatRoomData) { this.globalModlog("ROOMBAN", targetUser, ` by ${user.userid} ${(target ? `: ${target}` : ``)}`); @@ -1860,8 +1859,7 @@ const commands = { displayMessage = `(${name}'s ac account: ${acAccount})`; this.privateModAction(displayMessage); } - this.add(`|unlink|hide|${userid}`); - if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); + room.hideText([userid, toId(this.inputUsername)]); const globalReason = (target ? `: ${userReason} ${proof}` : ''); this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.userid}${globalReason}`); @@ -1982,8 +1980,7 @@ const commands = { this.privateModAction(displayMessage); } - this.add(`|unlink|hide|${userid}`); - if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); + room.hideText([userid, toId(this.inputUsername)]); const globalReason = (target ? `: ${userReason} ${proof}` : ''); this.globalModlog("BAN", targetUser, ` by ${user.userid}${globalReason}`); @@ -2455,20 +2452,18 @@ const commands = { if (!(user.can('lock') || localPunished)) return this.errorReply(`User ${name} is neither locked nor muted/banned from this room.`); if (targetUser && (cmd === 'hidealtstext' || cmd === 'hidetextalts' || cmd === 'hidealttext')) { - this.addModAction(`${name}'s alts' messages were cleared from ${room.title} by ${user.name}.`); + room.sendByUser(user, `${name}'s alts messages were cleared from ${room.title} by ${user.name}.`); + this.modlog('HIDEALTSTEXT', targetUser, null, {noip: 1}); - this.add(`|unlink|hide|${userid}`); - const alts = targetUser.getAltUsers(true); - for (const alt of alts) { - this.add(`|unlink|hide|${alt.getLastId()}`); - } - for (const prevName in targetUser.prevNames) { - this.add(`|unlink|hide|${targetUser.prevNames[prevName]}`); - } + room.hideText([ + userid, + ...Object.keys(targetUser.prevNames), + ...targetUser.getAltUsers(true).map(user => user.getLastId()), + ]); } else { - this.addModAction(`${name}'s messages were cleared from ${room.title} by ${user.name}.`); + room.sendByUser(user, `${name}'s messages were cleared from ${room.title} by ${user.name}.`); this.modlog('HIDETEXT', targetUser || userid, null, {noip: 1, noalts: 1}); - this.add(`|unlink|hide|${userid}`); + room.hideText([userid]); } }, hidetexthelp: [ diff --git a/roomlogs.js b/roomlogs.js index 197842143e33..9d0dc3cf47c7 100644 --- a/roomlogs.js +++ b/roomlogs.js @@ -183,6 +183,27 @@ class Roomlog { } return false; } + /** + * @param {string[]} userids + */ + clearText(userids) { + const messageStart = this.logTimes ? '|c:|' : '|c|'; + const section = this.logTimes ? 4 : 3; // ['', 'c' timestamp?, author, message] + /** @type {string[]} */ + let cleared = []; + this.log = this.log.filter(line => { + if (line.startsWith(messageStart)) { + const parts = Chat.splitFirst(line, '|', section); + const userid = toId(parts[section - 1]); + if (userids.includes(userid)) { + if (!cleared.includes(userid)) cleared.push(userid); + return false; + } + } + return true; + }); + return cleared; + } /** * @param {string} message */ diff --git a/rooms.js b/rooms.js index 3f45d0bf6054..ff996ef46bb9 100644 --- a/rooms.js +++ b/rooms.js @@ -181,6 +181,14 @@ class BasicRoom { addByUser(user, text) { return this.add('|c|' + user.getIdentity(this.id) + '|/log ' + text).update(); } + /** + * Like addByUser, but without logging + * @param {User} user + * @param {string} text + */ + sendByUser(user, text) { + return this.send('|c|' + user.getIdentity(this.id) + '|/log ' + text); + } /** * Like addByUser, but sends to mods only. * @param {User} user @@ -1072,6 +1080,16 @@ class BasicChatRoom extends BasicRoom { this.log.modlog(message); return this; } + /** + * @param {string[]} userids + */ + hideText(userids) { + const cleared = this.log.clearText(userids); + for (const userid of cleared) { + this.send(`|unlink|hide|${userid}`); + } + this.update(); + } logUserStats() { let total = 0; let guests = 0; From 952ccb2d804a7a1e8b5085d59b8b7f0432a02579 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Thu, 13 Sep 2018 11:34:30 -0400 Subject: [PATCH 056/125] Correct Soul-Heart activation order --- data/abilities.js | 1 + dev-tools/globals.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/data/abilities.js b/data/abilities.js index 59964033bdaf..1e3f65ca5989 100644 --- a/data/abilities.js +++ b/data/abilities.js @@ -3173,6 +3173,7 @@ let BattleAbilities = { "soulheart": { desc: "This Pokemon's Special Attack is raised by 1 stage when another Pokemon faints.", shortDesc: "This Pokemon's Sp. Atk is raised by 1 stage when another Pokemon faints.", + onAnyFaintPriority: 1, onAnyFaint: function () { this.boost({spa: 1}, this.effectData.target); }, diff --git a/dev-tools/globals.ts b/dev-tools/globals.ts index c3d4ce7150b3..f65612c52bc6 100644 --- a/dev-tools/globals.ts +++ b/dev-tools/globals.ts @@ -307,6 +307,7 @@ interface EffectData extends EventMethods { onAfterMoveSecondaryPriority?: number onAfterMoveSecondarySelfPriority?: number onAfterMoveSelfPriority?: number + onAnyFaintPriority?: number onAttractPriority?: number onBasePowerPriority?: number onBeforeMovePriority?: number From b4b2f1ebf065ff9ee13f63202c50dbcf8a1fbc86 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Thu, 13 Sep 2018 11:35:29 -0400 Subject: [PATCH 057/125] Fix Laser Focus --- data/moves.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/moves.js b/data/moves.js index 2b1ca021d6ae..bbd499700c70 100644 --- a/data/moves.js +++ b/data/moves.js @@ -8955,6 +8955,10 @@ let BattleMovedex = { onStart: function (pokemon) { this.add('-start', pokemon, 'move: Laser Focus'); }, + onRestart: function (pokemon) { + this.effectData.duration = 2; + this.add('-start', pokemon, 'move: Laser Focus'); + }, onModifyCritRatio: function (critRatio) { return 5; }, From 54c32e32e95db8cdf516aa079625fa8022310f60 Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Fri, 14 Sep 2018 09:45:32 -0400 Subject: [PATCH 058/125] Datacenters: Update outdated range --- config/datacenters.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/datacenters.csv b/config/datacenters.csv index 61aaff888323..4bd6d06a94a4 100644 --- a/config/datacenters.csv +++ b/config/datacenters.csv @@ -859,7 +859,7 @@ 69.175.0.0,69.175.127.255,SingleHop,http://www.singlehop.com/ 69.176.80.0,69.176.95.255,ethr.net,http://ethr.net/ 69.192.0.0,69.192.255.255,Akamai,http://akamai.com/ -69.194.0.0,69.194.127.255,Peak Web Hosting,http://www.peakwebhosting.com/ +69.194.7.0,69.194.7.255,Peak Web Hosting,http://www.peakwebhosting.com/ 69.194.128.0,69.194.143.255,SWITCH Communications Group LLC,http://www.switchnap.com/ 69.194.160.0,69.194.175.255,Fortress Integrated Technologies,http://fortressitx.com/ 69.194.224.0,69.194.239.255,180Servers,http://www.180servers.com/ From 5989f957bde149a77f936f375a0f05914332b9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Fri, 14 Sep 2018 22:50:20 +0200 Subject: [PATCH 059/125] Clarify that banwords are room-specific --- chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat.js b/chat.js index b458476f247c..a0d2419553bc 100644 --- a/chat.js +++ b/chat.js @@ -928,7 +928,7 @@ class CommandContext { return false; } if (!this.checkBanwords(room, message) && !user.can('mute', null, room)) { - this.errorReply("Your message contained banned words."); + this.errorReply("Your message contained banned words in this room."); return false; } From faf04449df73e801217be880c0d7312c06ff28b1 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Fri, 14 Sep 2018 20:31:02 -0400 Subject: [PATCH 060/125] Clarify Mind Blown description --- data/moves.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/moves.js b/data/moves.js index bbd499700c70..97c596fc3b0b 100644 --- a/data/moves.js +++ b/data/moves.js @@ -10348,7 +10348,7 @@ let BattleMovedex = { accuracy: 100, basePower: 150, category: "Special", - desc: "Whether or not this move is successful, the user loses 1/2 of its maximum HP, rounded up, unless the user has the Magic Guard Ability. This move is prevented from executing and the user does not lose HP if any active Pokemon has the Damp Ability, or if this move is Fire type and the user is affected by Powder or the weather is Primordial Sea.", + desc: "Whether or not this move is successful and even if it would cause fainting, the user loses 1/2 of its maximum HP, rounded up, unless the user has the Magic Guard Ability. This move is prevented from executing and the user does not lose HP if any active Pokemon has the Damp Ability, or if this move is Fire type and the user is affected by Powder or the weather is Primordial Sea.", shortDesc: "User loses 50% max HP. Hits adjacent Pokemon.", id: "mindblown", isViable: true, From aae3fad273162e42cf7d65cdd71093a0f22d842a Mon Sep 17 00:00:00 2001 From: asgdf Date: Sat, 15 Sep 2018 16:49:20 +0200 Subject: [PATCH 061/125] Datacenters: Update ranges (#4857) --- config/datacenters.csv | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/datacenters.csv b/config/datacenters.csv index 4bd6d06a94a4..10f01595105d 100644 --- a/config/datacenters.csv +++ b/config/datacenters.csv @@ -8,7 +8,7 @@ 5.39.216.0,5.39.223.255,HostKey,http://www.hostkey.com/ 5.44.26.144,5.44.26.159,PEER 1,http://www.peer1.com/ 5.45.72.0,5.45.75.255,3NT UK,http://3nt.com/ -5.62.46.0,5.62.47.255,Privax LTD,http://www.privax.com/ +5.62.0.0,5.62.63.255,Privax LTD,http://www.privax.com/ 5.77.32.0,5.77.63.255,eukhost.com,http://eukhost.com/ 5.79.0.0,5.79.7.255,Rackspace,http://www.rackspace.com/ 5.79.16.0,5.79.24.255,Rackspace,http://www.rackspace.com/ @@ -1014,6 +1014,7 @@ 77.111.244.0,77.111.247.255,Opera Mini Proxy,https://www.opera.com/ 77.120.108.128,77.120.108.191,Volia Datacenter,http://www.hosting-service.com.ua 77.222.40.0,77.222.43.255,SpaceWeb,https://sweb.ru/ +77.234.40.0,77.234.47.255,Avast Cloud,https://www.avast.com 77.235.32.0,77.235.63.255,EuroVPS,http://eurovps.com/ 77.237.228.0,77.237.231.255,servhost.de,http://servhost.de/ 77.237.241.128,77.237.241.255,webhoster.de,http://webhoster.de/ @@ -2391,7 +2392,7 @@ 192.110.160.0,192.110.167.255,Input Output Flood LLC,http://ioflood.com/ 192.111.128.0,192.111.143.255,Total Server Solutions,http://totalserversolutions.com/ 192.119.64.0,192.119.127.255,hostwinds.com,http://www.hostwinds.com/ -192.119.144.0,192.119.159.255,avantehosting.net,https://avantehosting.net/ +192.119.160.0,192.119.175.255,madgenius.com,http://www.madgenius.com/ 192.124.170.0,192.124.219.255,demos,http://demos.ru/ 192.129.128.0,192.129.255.255,hostwinds.com,http://www.hostwinds.com/ 192.138.16.0,192.138.23.255,wiredtree,http://www.wiredtree.com/ @@ -2608,7 +2609,7 @@ 198.50.96.0,198.50.127.255,iWeb Technologies Inc.,http://iweb.com/ 198.50.128.0,198.50.255.255,OVH,http://www.ovh.co.uk/ 198.52.96.0,198.52.127.255,multacom.com,http://multacom.com/ -198.52.128.0,198.52.255.255,avantehosting.net,https://avantehosting.net/ +198.52.192.0,198.52.255.255,avantehosting.net,https://avantehosting.net/ 198.55.96.0,198.55.127.255,QuadraNet,http://quadranet.com/ 198.58.96.0,198.58.127.255,Linode,http://www.linode.com/ 198.61.128.0,198.61.255.255,Rackspace,http://www.rackspace.com/ @@ -2746,7 +2747,7 @@ 199.193.248.0,199.193.255.255,Enzu Inc,https://www.enzu.com/ 199.195.116.0,199.195.119.255,A2 Hosting,http://www.a2hosting.com/ 199.195.128.0,199.195.131.255,fiberhub.com,http://www.fiberhub.com/ -199.195.156.0,199.195.159.255,avantehosting.net,https://avantehosting.net/ +199.195.156.0,199.195.159.255,softsyshosting.com,https://www.softsyshosting.com 199.195.192.0,199.195.199.255,Midphase,http://www.midphase.com/ 199.195.212.0,199.195.215.255,Forta Trust,http://www.fortatrust.com/ 199.204.44.0,199.204.47.255,vexxhost web hosting,http://vexxhost.com/ From 142172a57b9749939c65832f8f9fc0784059ea3d Mon Sep 17 00:00:00 2001 From: Quinton Lee Date: Sat, 15 Sep 2018 13:15:37 -0500 Subject: [PATCH 062/125] Support roomauth-only HTML boxes (#4855) --- chat-plugins/info.js | 64 ++++++++++++++++++++++++++++++-------------- chat.js | 6 ----- rooms.js | 10 ++++++- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 364abab27d41..8cfe8c9b1de8 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -2193,7 +2193,6 @@ const commands = { `/htmlbox [message] - Displays a message, parsing HTML code contained.`, `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, ], - addmodhtmlbox: 'addhtmlbox', addhtmlbox: function (target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); if (!this.canTalk()) return; @@ -2205,20 +2204,29 @@ const commands = { target += Chat.html`
[${user.name}]
`; } - if (cmd === 'addmodhtmlbox') { - this.addModBox(target); - } else { - this.addBox(target); - } + this.addBox(target); }, addhtmlboxhelp: [ `/addhtmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, ], - addmodhtmlboxhelp: [ - `/addmodhtmlbox [message] - Shows staff a message, parsing HTML code contained. Requires: ~ & #`, + addrankhtmlbox: function (target, room, user, connection, cmd) { + if (!target) return this.parse('/help ' + cmd); + if (!this.canTalk()) return; + let [rank, html] = this.splitOne(target); + if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); + html = this.canHTML(html); + if (!html) return; + if (!this.can('addhtml', null, room)) return; + + if (!user.can('addhtml')) { + html += Chat.html`
[${user.name}]
`; + } + + this.room.sendRankedUsers(`|html|
${html}
`, rank); + }, + addrankhtmlboxhelp: [ + `/addrankhtmlbox [rank], [message] - Shows everyone with the specified rank or higher a message, parsing HTML code contained. Requires: ~ & #`, ], - changemoduhtml: 'adduhtml', - addmoduhtml: 'adduhtml', changeuhtml: 'adduhtml', adduhtml: function (target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); @@ -2234,12 +2242,8 @@ const commands = { html += Chat.html`
[${user.name}]
`; } - html = `|uhtml${(cmd === 'changeuhtml' || cmd === 'changemoduhtml' ? 'change' : '')}|${name}|${html}`; - if (cmd === 'addmoduhtml' || cmd === 'changemoduhtml') { - this.room.sendMods(html); - } else { - this.add(html); - } + html = `|uhtml${(cmd === 'changeuhtml' ? 'change' : '')}|${name}|${html}`; + this.add(html); }, adduhtmlhelp: [ `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained. Requires: ~ & #`, @@ -2247,11 +2251,31 @@ const commands = { changeuhtmlhelp: [ `/changeuhtml [name], [message] - Changes the message previously shown with /adduhtml [name]. Requires: ~ & #`, ], - addmoduhtmlhelp: [ - `/addmoduhtml [name], [message] - Shows staff a message that can change, parsing HTML code contained. Requires: ~ & #`, + changerankuhtml: 'addrankuhtml', + addrankuhtml: function (target, room, user, connection, cmd) { + if (!target) return this.parse('/help ' + cmd); + if (!this.canTalk()) return; + + let [rank, uhtml] = this.splitOne(target); + if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); + let [name, html] = this.splitOne(uhtml); + name = toId(name); + html = this.canHTML(html); + if (!html) return; + if (!this.can('addhtml', null, room)) return; + + if (!user.can('addhtml')) { + html += Chat.html`
[${user.name}]
`; + } + + html = `|uhtml${(cmd === 'changerankuhtml' ? 'change' : '')}|${name}|${html}`; + this.room.sendRankedUsers(html, rank); + }, + addrankuhtmlhelp: [ + `/addrankuhtml [rank], [name], [message] - Shows everyone with the specified rank or higher a message that can change, parsing HTML code contained. Requires: ~ & #`, ], - changemoduhtmlhelp: [ - `/changemoduhtml [name], [message] - Changes the staff message previously shown with /addmoduhtml [name]. Requires: ~ & #`, + changerankuhtmlhelp: [ + `/changerankuhtml [rank], [name], [message] - Changes the message previously shown with /addrankuhtml [rank], [name]. Requires: ~ & #`, ], }; diff --git a/chat.js b/chat.js index a0d2419553bc..21a4ada1b6e0 100644 --- a/chat.js +++ b/chat.js @@ -569,12 +569,6 @@ class CommandContext { addBox(html) { this.add(`|html|
${html}
`); } - /** - * @param {string} html - */ - addModBox(html) { - this.room.sendMods(`|html|
${html}
`); - } /** * @param {string} html */ diff --git a/rooms.js b/rooms.js index ff996ef46bb9..e6cf545c1e29 100644 --- a/rooms.js +++ b/rooms.js @@ -131,15 +131,23 @@ class BasicRoom { * @param {string} data */ sendMods(data) { + this.sendRankedUsers(data, '%'); + } + /** + * @param {string} data + * @param {string} [minRank] + */ + sendRankedUsers(data, minRank = '+') { if (this.staffRoom) { if (!this.log) throw new Error(`Staff room ${this.id} has no log`); this.log.add(data); return; } + for (let i in this.users) { let user = this.users[i]; // hardcoded for performance reasons (this is an inner loop) - if (user.isStaff || (this.auth && (this.auth[user.userid] || '+') !== '+')) { + if (user.isStaff || (this.auth && this.auth[user.userid] && this.auth[user.userid] in Config.groups && Config.groups[this.auth[user.userid]].rank >= Config.groups[minRank].rank)) { user.sendTo(this, data); } } From a02e924386ad970a914bf42f472cc9704d6aa7b9 Mon Sep 17 00:00:00 2001 From: whales Date: Sun, 16 Sep 2018 10:12:25 +0930 Subject: [PATCH 063/125] Mafia: Fix bug with subs (#4856) --- chat-plugins/mafia.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/chat-plugins/mafia.js b/chat-plugins/mafia.js index 768ba43ec298..30d01c7ecc15 100644 --- a/chat-plugins/mafia.js +++ b/chat-plugins/mafia.js @@ -522,13 +522,14 @@ class MafiaTracker extends Rooms.RoomGame { * @return {void} */ distributeRoles() { - if (!Object.keys(this.roles).length) return; let roles = Dex.shuffle(this.roles.slice()); - for (let p in this.players) { - let role = roles.shift(); - this.players[p].role = role; - let u = Users(p); - if (u && u.connected) u.send(`>${this.room.id}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`); + if (roles.length) { + for (let p in this.players) { + let role = roles.shift(); + this.players[p].role = role; + let u = Users(p); + if (u && u.connected) u.send(`>${this.room.id}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`); + } } this.dead = {}; this.played = [this.hostid, ...this.cohosts, ...Object.keys(this.players)]; @@ -1120,7 +1121,7 @@ class MafiaTracker extends Rooms.RoomGame { } this.IDEA.data = { - name: `${Chat.escapeHTML(this.host)}'s IDEA`, + name: `${this.host}'s IDEA`, // already escaped untrusted: true, roles: roleList, picks, @@ -1537,7 +1538,7 @@ class MafiaTracker extends Rooms.RoomGame { logs.plays[month][player]++; } if (!logs.hosts[month]) logs.hosts[month] = {}; - for (const hostid of [...this.cohosts, this.host]) { + for (const hostid of [...this.cohosts, this.hostid]) { if (!logs.hosts[month][hostid]) logs.hosts[month][hostid] = 0; logs.hosts[month][hostid]++; } From 12f26b8b2e2f761fc564df966bb2b2c40724d870 Mon Sep 17 00:00:00 2001 From: Kris Johnson <11083252+KrisXV@users.noreply.github.com> Date: Sat, 15 Sep 2018 20:01:08 -0600 Subject: [PATCH 064/125] Sketchmons: Restrict Extreme Speed (#4859) https://www.smogon.com/forums/threads/sketchmons-om-of-the-month.3587743/page-14#post-7908288 --- config/formats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/formats.js b/config/formats.js index f791fb734a00..0ab7bdad51a7 100644 --- a/config/formats.js +++ b/config/formats.js @@ -503,7 +503,7 @@ let Formats = [ ruleset: ['[Gen 7] OU'], banlist: ['Kartana', 'Porygon-Z', 'Battle Bond'], restrictedMoves: [ - 'Belly Drum', 'Celebrate', 'Chatter', 'Conversion', "Forest's Curse", 'Geomancy', 'Happy Hour', 'Hold Hands', + 'Belly Drum', 'Celebrate', 'Chatter', 'Conversion', 'Extreme Speed', "Forest's Curse", 'Geomancy', 'Happy Hour', 'Hold Hands', 'Lovely Kiss', 'Purify', 'Quiver Dance', 'Shell Smash', 'Shift Gear', 'Sketch', 'Spore', 'Sticky Web', 'Trick-or-Treat', ], checkLearnset: function (move, template, lsetData, set) { From 76e486f7c13274c96895444e8c53724324bb8fc4 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Sun, 16 Sep 2018 09:12:49 -0400 Subject: [PATCH 065/125] Fix Conversion 2 Who did this... --- data/moves.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/moves.js b/data/moves.js index 97c596fc3b0b..98e94c589769 100644 --- a/data/moves.js +++ b/data/moves.js @@ -2577,7 +2577,7 @@ let BattleMovedex = { let possibleTypes = []; let attackType = target.lastMove.type; for (let type in this.data.TypeChart) { - if (source.hasType(type) || target.hasType(type)) continue; + if (source.hasType(type)) continue; let typeCheck = this.data.TypeChart[type].damageTaken[attackType]; if (typeCheck === 2 || typeCheck === 3) { possibleTypes.push(type); From 03492bdae276bc39686994da6d7c7f06a1d69151 Mon Sep 17 00:00:00 2001 From: whales Date: Mon, 17 Sep 2018 00:07:55 +0930 Subject: [PATCH 066/125] Fix *html permissions in help (#4860) --- chat-plugins/info.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 8cfe8c9b1de8..aa20b09406ac 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -2191,7 +2191,7 @@ const commands = { }, htmlboxhelp: [ `/htmlbox [message] - Displays a message, parsing HTML code contained.`, - `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, + `!htmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: * # & ~`, ], addhtmlbox: function (target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); @@ -2207,7 +2207,7 @@ const commands = { this.addBox(target); }, addhtmlboxhelp: [ - `/addhtmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: ~ & #`, + `/addhtmlbox [message] - Shows everyone a message, parsing HTML code contained. Requires: * & ~`, ], addrankhtmlbox: function (target, room, user, connection, cmd) { if (!target) return this.parse('/help ' + cmd); @@ -2225,7 +2225,7 @@ const commands = { this.room.sendRankedUsers(`|html|
${html}
`, rank); }, addrankhtmlboxhelp: [ - `/addrankhtmlbox [rank], [message] - Shows everyone with the specified rank or higher a message, parsing HTML code contained. Requires: ~ & #`, + `/addrankhtmlbox [rank], [message] - Shows everyone with the specified rank or higher a message, parsing HTML code contained. Requires: * & ~`, ], changeuhtml: 'adduhtml', adduhtml: function (target, room, user, connection, cmd) { @@ -2246,10 +2246,10 @@ const commands = { this.add(html); }, adduhtmlhelp: [ - `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained. Requires: ~ & #`, + `/adduhtml [name], [message] - Shows everyone a message that can change, parsing HTML code contained. Requires: * & ~`, ], changeuhtmlhelp: [ - `/changeuhtml [name], [message] - Changes the message previously shown with /adduhtml [name]. Requires: ~ & #`, + `/changeuhtml [name], [message] - Changes the message previously shown with /adduhtml [name]. Requires: * & ~`, ], changerankuhtml: 'addrankuhtml', addrankuhtml: function (target, room, user, connection, cmd) { @@ -2272,10 +2272,10 @@ const commands = { this.room.sendRankedUsers(html, rank); }, addrankuhtmlhelp: [ - `/addrankuhtml [rank], [name], [message] - Shows everyone with the specified rank or higher a message that can change, parsing HTML code contained. Requires: ~ & #`, + `/addrankuhtml [rank], [name], [message] - Shows everyone with the specified rank or higher a message that can change, parsing HTML code contained. Requires: * & ~`, ], changerankuhtmlhelp: [ - `/changerankuhtml [rank], [name], [message] - Changes the message previously shown with /addrankuhtml [rank], [name]. Requires: ~ & #`, + `/changerankuhtml [rank], [name], [message] - Changes the message previously shown with /addrankuhtml [rank], [name]. Requires: * & ~`, ], }; From e0167ef730c4220442356a6d1fdbc5767a1ea4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 02:18:30 +0200 Subject: [PATCH 067/125] Add a chat monitor plugin (#4755) --- chat-commands.js | 1 + chat-plugins/chat-monitor.js | 307 +++++++++++++++++++++++++++++++++++ chat.js | 37 +++-- punishments.js | 14 +- tsconfig.json | 1 + users.js | 3 + 6 files changed, 349 insertions(+), 14 deletions(-) create mode 100644 chat-plugins/chat-monitor.js diff --git a/chat-commands.js b/chat-commands.js index 1c4bc8fb861f..47cccb8ec3b6 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -2377,6 +2377,7 @@ const commands = { Ladders.cancelSearches(targetUser); targetUser.resetName(true); targetUser.send(`|nametaken||${user.name} considers your name inappropriate${(reason ? `: ${reason}` : ".")}`); + targetUser.trackRename = targetUser.name; return true; }, forcerenamehelp: [`/forcerename OR /fr [username], [reason] - Forcibly change a user's name and shows them the [reason]. Requires: % @ * & ~`], diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js new file mode 100644 index 000000000000..2ee58167c459 --- /dev/null +++ b/chat-plugins/chat-monitor.js @@ -0,0 +1,307 @@ +'use strict'; + +const FS = require('../lib/fs'); + +const MONITOR_FILE = 'config/chat-plugins/chat-monitor.tsv'; +const WRITE_THROTTLE_TIME = 5 * 60 * 1000; + +/** @type {{[k: string]: string[]}} */ +let filterKeys = Chat.filterKeys = Object.assign(Chat.filterKeys, {publicwarn: ['PUBLIC', 'WARN'], warn: ['EVERYWHERE', 'WARN'], autolock: ['EVERYWHERE', 'AUTOLOCK'], namefilter: ['NAMES', 'WARN'], wordfilter: ['EVERYWHERE', 'FILTERTO']}); +/** @type {{[k: string]: ([(string | RegExp), string, string?, number][])}} */ +let filterWords = Chat.filterWords; + +setImmediate(() => { + for (const key in filterKeys) { + if (!filterWords[key]) filterWords[key] = []; + } + + /* + * Columns Location and Punishment use keywords. Possible values: + * + * Location: EVERYWHERE, PUBLIC, NAMES + * Punishment: AUTOLOCK, WARN, FILTERTO + */ + FS(MONITOR_FILE).readIfExists().then(data => { + const lines = data.split('\n'); + loop: for (const line of lines) { + if (!line || line === '\r') continue; + let [location, word, punishment, reason, times, ...rest] = line.split('\t').map(param => param.trim()); + if (location === 'Location') continue; + if (!(location && word && punishment)) continue; + + for (const key in filterKeys) { + if (filterKeys[key][0] === location && filterKeys[key][1] === punishment) { + if (punishment === 'FILTERTO') { + const filterTo = rest[0]; + filterWords[key].push([new RegExp(word, 'g'), reason, filterTo, parseInt(times) || 0]); + continue loop; + } else { + filterWords[key].push([word, reason, null, parseInt(times) || 0]); + continue loop; + } + } + } + throw new Error(`Unrecognized [location, punishment] pair for filter word entry: ${[location, word, punishment, reason, times]}`); + } + }); +}); + +/** + * @param {string} location + * @param {[(string | RegExp), string, string?, number]} word + * @param {string} punishment + */ +function renderEntry(location, word, punishment) { + const str = word[0] instanceof RegExp ? String(word[0]).slice(1, -2) : word[0]; + return `${location}\t${str}\t${punishment}\t${word[1]}\t${word[3]}${word[2] ? `\t${word[2]}` : ''}\r\n`; +} + +/** + * @param {boolean} force + */ +function saveFilters(force = false) { + FS(MONITOR_FILE).writeUpdate(() => { + let buf = 'Location\tWord\tPunishment\tReason\tTimes\r\n'; + for (const key in filterKeys) { + buf += filterWords[key].map(word => renderEntry(filterKeys[key][0], word, filterKeys[key][1])).join(''); + } + return buf; + }, {throttle: force ? 0 : WRITE_THROTTLE_TIME}); +} + +/** @typedef {(this: CommandContext, target: string, room: ChatRoom, user: User, connection: Connection, cmd: string, message: string) => (void)} ChatHandler */ +/** @typedef {(this: CommandContext, message: string, user: User, room: ChatRoom?, connection: Connection, targetUser: User?) => (string | boolean)} ChatFilter */ +/** @typedef {(name: string, user: User) => (string)} NameFilter */ +/** @typedef {{[k: string]: ChatHandler | string | true | string[] | ChatCommands}} ChatCommands */ + +/** @type {ChatFilter} */ +let chatfilter = function (message, user, room) { + let lcMessage = message.replace(/\u039d/g, 'N').toLowerCase().replace(/[\u200b\u007F\u00AD]/g, '').replace(/\u03bf/g, 'o').replace(/\u043e/g, 'o').replace(/\u0430/g, 'a').replace(/\u0435/g, 'e').replace(/\u039d/g, 'e'); + lcMessage = lcMessage.replace(/__|\*\*|``|\[\[|\]\]/g, ''); + + for (let i = 0; i < filterWords.autolock.length; i++) { + let [line, reason] = filterWords.autolock[i]; + let matched = false; + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (line.startsWith('\\b') || line.endsWith('\\b')) { + matched = new RegExp(line).test(lcMessage); + } else { + matched = lcMessage.includes(line); + } + if (matched) { + if (room && room.id.endsWith('staff')) return `${message} __[would be locked: ${line}]__`; + if (user.isStaff) { + return message; + } + message = message.replace(/(https?):\/\//g, '$1__:__//'); + message = message.replace(/\./g, '__.__'); + if (room) { + Punishments.autolock(user, room, 'ChatMonitor', `Filtered phrase: ${line}`, `<${room.id}> ${user.name}: ${message}${reason ? ` __(${reason})__` : ''}`, true); + } else { + this.errorReply(`Please do not say '${line}'.`); + } + filterWords.autolock[i][3]++; + saveFilters(); + return false; + } + } + for (let i = 0; i < filterWords.warn.length; i++) { + let [line, reason] = filterWords.warn[i]; + let matched = false; + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (line.startsWith('\\b') || line.endsWith('\\b')) { + matched = new RegExp(line).test(lcMessage); + } else { + matched = lcMessage.includes(line); + } + if (matched) { + if (room && room.id.endsWith('staff')) return `${message} __[would be filtered: ${line}${reason ? ` (${reason})` : ''}]__`; + if (user.isStaff) { + return message; + } + this.errorReply(`Please do not say '${line}'.`); + filterWords.warn[i][3]++; + saveFilters(); + return false; + } + } + if ((room && room.isPrivate !== true) || !room) { + for (let [line, reason] of filterWords.publicwarn) { + let matched = false; + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (line.startsWith('\\b') || line.endsWith('\\b')) { + matched = new RegExp(line).test(lcMessage); + } else { + matched = lcMessage.includes(line); + } + if (matched) { + if (room && room.id.endsWith('staff')) return `${message} __[would be filtered in public: ${line}${reason ? ` (${reason})` : ''}]__`; + if (user.isStaff) { + return message; + } + this.errorReply(`Please do not say '${line}'.`); + return false; + } + } + } + for (let line of filterWords.wordfilter) { + if (typeof line === 'string') continue; // Failsafe to appease typescript. + message = message.replace(line[0], line[1]); + } + + return message; +}; + +const namefilterwhitelist = new Map(); +/** @type {NameFilter} */ +let namefilter = function (name, user) { + let id = toId(name); + if (namefilterwhitelist.has(id)) return name; + if (id === user.trackRename) return ''; + let lcName = name.replace(/\u039d/g, 'N').toLowerCase().replace(/[\u200b\u007F\u00AD]/g, '').replace(/\u03bf/g, 'o').replace(/\u043e/g, 'o').replace(/\u0430/g, 'a').replace(/\u0435/g, 'e').replace(/\u039d/g, 'e'); + // Remove false positives. + lcName = lcName.replace('herapist', '').replace('grape', '').replace('scrape', ''); + + for (let [line] of filterWords.autolock) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + Punishments.autolock(user, Rooms('staff'), `NameMonitor`, `inappropriate name: ${name}`, `using an inappropriate name: ${name}`, false, true); + return ''; + } + } + for (let [line] of filterWords.warn) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + user.trackRename = name; + return ''; + } + } + for (let [line] of filterWords.publicwarn) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + user.trackRename = name; + return ''; + } + } + for (let [line] of filterWords.namefilter) { + if (typeof line !== 'string') continue; // Failsafe to appease typescript. + if (lcName.includes(line)) { + user.trackRename = name; + return ''; + } + } + + if (user.trackRename) { + Monitor.log(`[NameMonitor] Username used: ${name} (forcerenamed from ${user.trackRename})`); + user.trackRename = ''; + } + return name; +}; + + +/** @type {ChatCommands} */ +let commands = { + addfilterword: function (target, room, user) { + if (!this.can('updateserver')) return false; + + let [list, ...rest] = target.split(','); + list = toId(list); + + if (!list || !rest.length) return this.errorReply("Syntax: /addfilterword list, word, reason"); + + if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); + + if (filterKeys[list][1] === 'FILTERTO') { + let [word, filterTo, ...reasonParts] = rest; + word = word.trim(); + filterTo = filterTo.trim(); + let reason = reasonParts.join(','); + if (!filterTo) return this.errorReply(`Syntax for word filters: /addfilterword ${list}, regex, filter to, reason`); + + let regex; + try { + regex = new RegExp(word, 'g'); // eslint-disable-line no-unused-vars + } catch (e) { + return this.errorReply(e.message.startsWith('Invalid regular expression: ') ? e.message : `Invalid regular expression: /${word}/: ${e.message}`); + } + + filterWords[list].push([regex, reason, filterTo, 0]); + this.globalModlog(`ADDFILTER`, null, `'${String(regex)} => ${filterTo}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); + saveFilters(true); + return this.sendReply(`'${String(regex)} => ${filterTo}' was added to the ${list} list.`); + } else { + let [word, ...reasonParts] = rest; + word = word.trim(); + let reason = reasonParts.join(','); + if (filterWords[list].some(val => val[0] === word)) return this.errorReply(`${word} is already added to the ${list} list.`); + filterWords[list].push([word, reason, null, 0]); + this.globalModlog(`ADDFILTER`, null, `'${word}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); + saveFilters(true); + return this.sendReply(`'${word}' was added to the ${list} list.`); + } + }, + removefilterword: function (target, room, user) { + if (!this.can('updateserver')) return false; + + let [list, ...words] = target.split(',').map(param => param.trim()); + list = toId(list); + + if (!list || !words.length) return this.errorReply("Syntax: /removefilterword list, words"); + + if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); + + if (filterKeys[list][1] === 'FILTERTO') { + const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -2) === val).length); + if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -2))); + this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); + saveFilters(); + return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); + } else { + const notFound = words.filter(val => !filterWords[list].filter(entry => entry[0] === val).length); + if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]))); // This feels wrong + this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); + saveFilters(); + return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); + } + }, + viewfilters: function (target, room, user) { + if (!this.can('lock')) return false; + + let content = ''; + if (filterWords.publicwarn.length) { + content += `

Filtered in public rooms:

${filterWords.publicwarn.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.warn.length) { + content += `

Filtered:

${filterWords.warn.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.autolock.length) { + content += `

Weeklock:

${filterWords.autolock.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.namefilter.length) { + content += `

Filtered in names:

${filterWords.namefilter.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.wordfilter.length) { + content += `

Filtered in public rooms:

${filterWords.wordfilter.map(([str, reason, filterTo]) => `${str} => ${filterTo}`).join('')}`; + } + if (!content) return this.sendReplyBox("There are no filtered words."); + return this.sendReply(`|raw|
${content}
`); + }, + allowname: function (target, room, user) { + if (!this.can('forcerename')) return false; + target = toId(target); + if (!target) return this.errorReply(`Syntax: /allowname username`); + namefilterwhitelist.set(target, user.name); + + const msg = `${target} was allowed as a username by ${user.name}.`; + const staffRoom = Rooms('staff'); + const upperStaffRoom = Rooms('upperstaff'); + if (staffRoom) staffRoom.add(msg).update(); + if (upperStaffRoom) upperStaffRoom.add(msg).update(); + }, +}; + +exports.commands = commands; +exports.chatfilter = chatfilter; +exports.namefilter = namefilter; diff --git a/chat.js b/chat.js index 21a4ada1b6e0..e7653e536280 100644 --- a/chat.js +++ b/chat.js @@ -165,8 +165,17 @@ Chat.namefilter = function (name, user) { // \u534d\u5350 swastika // \u2a0d crossed integral (f) name = name.replace(/[\u00a1\u2580-\u2590\u25A0\u25Ac\u25AE\u25B0\u2a0d\u534d\u5350]/g, ''); + // e-mail address if (name.includes('@') && name.includes('.')) return ''; + + // url + if (/[a-z0-9]\.(com|net|org)/.test(name)) name = name.replace(/\./g, ''); + + // Limit the amount of symbols allowed in usernames to 4 maximum, and disallow (R) and (C) from being used in the middle of names. + let nameSymbols = name.replace(/[^\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2090-\u23FA\u2500-\u2BD1]+/g, ''); + // \u00ae\u00a9 (R) (C) + if (nameSymbols.length > 4 || /[^a-z0-9][a-z0-9][^a-z0-9]/.test(name.toLowerCase() + ' ') || /[\u00ae\u00a9].*[a-zA-Z0-9]/.test(name)) name = name.replace(/[\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2190-\u23FA\u2500-\u2BD1\u2E80-\u32FF\u3400-\u9FFF\uF900-\uFAFF\uFE00-\uFE6F]+/g, '').replace(/[^A-Za-z0-9]{2,}/g, ' ').trim(); } name = name.replace(/^[^A-Za-z0-9]+/, ""); // remove symbols from start @@ -624,20 +633,22 @@ class CommandContext { } /** * @param {string} action - * @param {string | User} user + * @param {string | User?} user * @param {string} note */ globalModlog(action, user, note) { let buf = `(${this.room.id}) ${action}: `; - if (typeof user === 'string') { - buf += `[${user}]`; - } else { - let userid = user.getLastId(); - buf += `[${userid}]`; - if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; - const alts = user.getAltUsers(false, true).map(user => user.getLastId()).join('], ['); - if (alts.length) buf += ` alts:[${alts}]`; - buf += ` [${user.latestIp}]`; + if (user) { + if (typeof user === 'string') { + buf += `[${user}]`; + } else { + let userid = user.getLastId(); + buf += `[${userid}]`; + if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; + const alts = user.getAltUsers(false, true).map(user => user.getLastId()).join('], ['); + if (alts.length) buf += ` alts:[${alts}]`; + buf += ` [${user.latestIp}]`; + } } buf += note; @@ -1587,3 +1598,9 @@ Chat.stringify = function (value, depth = 0) { Chat.formatText = require('./chat-formatter').formatText; Chat.linkRegex = require('./chat-formatter').linkRegex; Chat.updateServerLock = false; + +// Used (and populated) by ChatMonitor. +/** @type {{[k: string]: string[]}} */ +Chat.filterKeys = {}; +/** @type {{[k: string]: string[]}} */ +Chat.filterWords = {}; diff --git a/punishments.js b/punishments.js index 27f82007c6dd..8604535a5f99 100644 --- a/punishments.js +++ b/punishments.js @@ -742,9 +742,10 @@ Punishments.lock = function (user, expireTime, id, ...reason) { * @param {string} source * @param {string} reason * @param {string?} message - * @param {boolean?} week + * @param {boolean} week + * @param {boolean} name */ -Punishments.autolock = function (user, room, source, reason, message, week) { +Punishments.autolock = function (user, room, source, reason, message, week = false, name = false) { if (!message) message = reason; let punishment = `LOCKED`; @@ -753,9 +754,14 @@ Punishments.autolock = function (user, room, source, reason, message, week) { expires = Date.now() + 7 * 24 * 60 * 60 * 1000; punishment = `WEEKLOCKED`; } - Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`); + if (name) { + punishment = `NAMELOCKED`; + Punishments.namelock(user, expires, toId(user), `Autonamelock: ${user.name || toId(user)}: ${reason}`); + } else { + Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`); + } Monitor.log(`[${source}] ${punishment}: ${message}`); - Rooms.global.modlog(`(${toId(room)}) AUTOLOCK: [${toId(user)}]: ${reason}`); + Rooms.global.modlog(`(${toId(room)}) AUTO${name ? `NAME` : ''}LOCK: [${toId(user)}]: ${reason}`); }; /** * @param {string} name diff --git a/tsconfig.json b/tsconfig.json index 3fff4f51f99e..64976f482af0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,6 +39,7 @@ "./verifier.js", "./chat.js", "./users.js", + "./chat-plugins/chat-monitor.js", "./chat-plugins/hangman.js", "./chat-plugins/helptickets.js", "./chat-plugins/mafia.js", diff --git a/users.js b/users.js index 79a3b4c8e70e..8e8f2dfab66e 100644 --- a/users.js +++ b/users.js @@ -513,6 +513,9 @@ class User { this.lockNotified = false; /**@type {string} */ this.autoconfirmed = ''; + // Used in punishments + /** @type {string} */ + this.trackRename = ''; // initialize Users.add(this); } From 50324e580027fb37ef3da4284c12866ca9290d74 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 17 Sep 2018 20:24:09 -0500 Subject: [PATCH 068/125] Fix typo in Sim Readme --- sim/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/README.md b/sim/README.md index 7d7074c3a020..b86b872a4cc9 100644 --- a/sim/README.md +++ b/sim/README.md @@ -14,7 +14,7 @@ stream = new Sim.BattleStream(); } })(); -stream.write(`>start {"format":"gen7randombattle"}`); +stream.write(`>start {"formatid":"gen7randombattle"}`); stream.write(`>player p1 {"name":"Alice"}`); stream.write(`>player p2 {"name":"Bob"}`); ``` From bcefeb29255de9c0a2a4c83f16b36f86c8ba3af9 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 17 Sep 2018 20:28:50 -0500 Subject: [PATCH 069/125] Fix other typo in Sim Readme --- sim/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sim/README.md b/sim/README.md index b86b872a4cc9..6717e02b6258 100644 --- a/sim/README.md +++ b/sim/README.md @@ -35,7 +35,7 @@ Writing to the simulator In a standard battle, what you write to the simulator looks something like this: ``` ->start {"format":"gen7ou"} +>start {"formatid":"gen7ou"} >player p1 {"name":"Alice","team":"insert packed team here"} >player p2 {"name":"Bob","team":"insert packed team here"} >p1 team 123456 From b09ea1dfd0d4176e2848a46e128eeed35fc86dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 03:44:46 +0200 Subject: [PATCH 070/125] Chat monitor: fix typo --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 2ee58167c459..e64747bb3cda 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -146,7 +146,7 @@ let chatfilter = function (message, user, room) { } for (let line of filterWords.wordfilter) { if (typeof line === 'string') continue; // Failsafe to appease typescript. - message = message.replace(line[0], line[1]); + message = message.replace(line[0], line[2] || ''); } return message; From d97a8ac6e69b0eaac8aed5806482d68e4788c420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 03:47:32 +0200 Subject: [PATCH 071/125] Chat monitor: namespace filter commands --- chat-plugins/chat-monitor.js | 153 ++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index e64747bb3cda..1ca4c6851acf 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -201,92 +201,95 @@ let namefilter = function (name, user) { /** @type {ChatCommands} */ let commands = { - addfilterword: function (target, room, user) { - if (!this.can('updateserver')) return false; + filters: 'filter', + filter: { + add: function (target, room, user) { + if (!this.can('updateserver')) return false; - let [list, ...rest] = target.split(','); - list = toId(list); + let [list, ...rest] = target.split(','); + list = toId(list); - if (!list || !rest.length) return this.errorReply("Syntax: /addfilterword list, word, reason"); + if (!list || !rest.length) return this.errorReply("Syntax: /filter add list, word, reason"); - if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); + if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); - if (filterKeys[list][1] === 'FILTERTO') { - let [word, filterTo, ...reasonParts] = rest; - word = word.trim(); - filterTo = filterTo.trim(); - let reason = reasonParts.join(','); - if (!filterTo) return this.errorReply(`Syntax for word filters: /addfilterword ${list}, regex, filter to, reason`); + if (filterKeys[list][1] === 'FILTERTO') { + let [word, filterTo, ...reasonParts] = rest; + word = word.trim(); + filterTo = filterTo.trim(); + let reason = reasonParts.join(','); + if (!filterTo) return this.errorReply(`Syntax for word filters: /filter add ${list}, regex, filter to, reason`); - let regex; - try { - regex = new RegExp(word, 'g'); // eslint-disable-line no-unused-vars - } catch (e) { - return this.errorReply(e.message.startsWith('Invalid regular expression: ') ? e.message : `Invalid regular expression: /${word}/: ${e.message}`); - } + let regex; + try { + regex = new RegExp(word, 'g'); // eslint-disable-line no-unused-vars + } catch (e) { + return this.errorReply(e.message.startsWith('Invalid regular expression: ') ? e.message : `Invalid regular expression: /${word}/: ${e.message}`); + } - filterWords[list].push([regex, reason, filterTo, 0]); - this.globalModlog(`ADDFILTER`, null, `'${String(regex)} => ${filterTo}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); - saveFilters(true); - return this.sendReply(`'${String(regex)} => ${filterTo}' was added to the ${list} list.`); - } else { - let [word, ...reasonParts] = rest; - word = word.trim(); - let reason = reasonParts.join(','); - if (filterWords[list].some(val => val[0] === word)) return this.errorReply(`${word} is already added to the ${list} list.`); - filterWords[list].push([word, reason, null, 0]); - this.globalModlog(`ADDFILTER`, null, `'${word}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); - saveFilters(true); - return this.sendReply(`'${word}' was added to the ${list} list.`); - } - }, - removefilterword: function (target, room, user) { - if (!this.can('updateserver')) return false; + filterWords[list].push([regex, reason, filterTo, 0]); + this.globalModlog(`ADDFILTER`, null, `'${String(regex)} => ${filterTo}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); + saveFilters(true); + return this.sendReply(`'${String(regex)} => ${filterTo}' was added to the ${list} list.`); + } else { + let [word, ...reasonParts] = rest; + word = word.trim(); + let reason = reasonParts.join(','); + if (filterWords[list].some(val => val[0] === word)) return this.errorReply(`${word} is already added to the ${list} list.`); + filterWords[list].push([word, reason, null, 0]); + this.globalModlog(`ADDFILTER`, null, `'${word}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); + saveFilters(true); + return this.sendReply(`'${word}' was added to the ${list} list.`); + } + }, + remove: function (target, room, user) { + if (!this.can('updateserver')) return false; - let [list, ...words] = target.split(',').map(param => param.trim()); - list = toId(list); + let [list, ...words] = target.split(',').map(param => param.trim()); + list = toId(list); - if (!list || !words.length) return this.errorReply("Syntax: /removefilterword list, words"); + if (!list || !words.length) return this.errorReply("Syntax: /filter remove list, words"); - if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); + if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); - if (filterKeys[list][1] === 'FILTERTO') { - const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -2) === val).length); - if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); - filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -2))); - this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); - saveFilters(); - return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); - } else { - const notFound = words.filter(val => !filterWords[list].filter(entry => entry[0] === val).length); - if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); - filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]))); // This feels wrong - this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); - saveFilters(); - return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); - } - }, - viewfilters: function (target, room, user) { - if (!this.can('lock')) return false; + if (filterKeys[list][1] === 'FILTERTO') { + const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -2) === val).length); + if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -2))); + this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); + saveFilters(); + return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); + } else { + const notFound = words.filter(val => !filterWords[list].filter(entry => entry[0] === val).length); + if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]))); // This feels wrong + this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); + saveFilters(); + return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); + } + }, + view: function (target, room, user) { + if (!this.can('lock')) return false; - let content = ''; - if (filterWords.publicwarn.length) { - content += `

Filtered in public rooms:

${filterWords.publicwarn.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.warn.length) { - content += `

Filtered:

${filterWords.warn.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.autolock.length) { - content += `

Weeklock:

${filterWords.autolock.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.namefilter.length) { - content += `

Filtered in names:

${filterWords.namefilter.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.wordfilter.length) { - content += `

Filtered in public rooms:

${filterWords.wordfilter.map(([str, reason, filterTo]) => `${str} => ${filterTo}`).join('')}`; - } - if (!content) return this.sendReplyBox("There are no filtered words."); - return this.sendReply(`|raw|
${content}
`); + let content = ''; + if (filterWords.publicwarn.length) { + content += `

Filtered in public rooms:

${filterWords.publicwarn.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.warn.length) { + content += `

Filtered:

${filterWords.warn.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.autolock.length) { + content += `

Weeklock:

${filterWords.autolock.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.namefilter.length) { + content += `

Filtered in names:

${filterWords.namefilter.map(([str, reason]) => `${str}`).join('')}`; + } + if (filterWords.wordfilter.length) { + content += `

Filtered in public rooms:

${filterWords.wordfilter.map(([str, reason, filterTo]) => `${str} => ${filterTo}`).join('')}`; + } + if (!content) return this.sendReplyBox("There are no filtered words."); + return this.sendReply(`|raw|
${content}
`); + }, }, allowname: function (target, room, user) { if (!this.can('forcerename')) return false; From 08d960ef2e7a24339d65d363ff2a34693f545be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 05:08:00 +0200 Subject: [PATCH 072/125] Chat monitor: improve stat tracking --- chat-plugins/chat-monitor.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 1ca4c6851acf..a2a4ebd618a0 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -126,7 +126,8 @@ let chatfilter = function (message, user, room) { } } if ((room && room.isPrivate !== true) || !room) { - for (let [line, reason] of filterWords.publicwarn) { + for (let i = 0; i < filterWords.publicwarn.length; i++) { + let [line, reason] = filterWords.publicwarn[i]; let matched = false; if (typeof line !== 'string') continue; // Failsafe to appease typescript. if (line.startsWith('\\b') || line.endsWith('\\b')) { @@ -140,13 +141,22 @@ let chatfilter = function (message, user, room) { return message; } this.errorReply(`Please do not say '${line}'.`); + filterWords.publicwarn[i][3]++; + saveFilters(); return false; } } } for (let line of filterWords.wordfilter) { - if (typeof line === 'string') continue; // Failsafe to appease typescript. - message = message.replace(line[0], line[2] || ''); + const regex = line[0]; + if (typeof regex === 'string') continue; + let matches = regex.exec(lcMessage); + if (!matches) continue; + for (let match of matches) { + message.replace(match[0], line[2] || ''); + line[3]++; + saveFilters(); + } } return message; @@ -183,10 +193,11 @@ let namefilter = function (name, user) { return ''; } } - for (let [line] of filterWords.namefilter) { - if (typeof line !== 'string') continue; // Failsafe to appease typescript. - if (lcName.includes(line)) { + for (let line of filterWords.namefilter) { + if (lcName.includes(String(line[0]))) { user.trackRename = name; + line[3]++; + saveFilters(); return ''; } } From e448e318ad647e131f5c6d2a6297fd081c6d8799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 05:09:06 +0200 Subject: [PATCH 073/125] Chat monitor: also list reason when using autolock words in staff --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index a2a4ebd618a0..7fa85a4f6a06 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -89,7 +89,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.id.endsWith('staff')) return `${message} __[would be locked: ${line}]__`; + if (room && room.id.endsWith('staff')) return `${message} __[would be locked: ${line}${reason ? ` (${reason})` : ''}]__`; if (user.isStaff) { return message; } From 8a1b59c9e47321a69c2cd5bc5786c05382fca257 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 17 Sep 2018 20:58:15 -0400 Subject: [PATCH 074/125] Chat monitor: Move /viewfilters to HTML panel It's way too long for something in-chat --- chat-plugins/chat-monitor.js | 63 ++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 7fa85a4f6a06..bc0ec9b670aa 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -209,6 +209,48 @@ let namefilter = function (name, user) { return name; }; +/** @typedef {(query: string[], user: User, connection: Connection) => (string | null | void)} PageHandler */ +/** @typedef {{[k: string]: PageHandler | PageTable}} PageTable */ + +/** @type {PageTable} */ +const pages = { + filters(query, user, connection) { + if (!user.named) return Rooms.RETRY_AFTER_LOGIN; + let buf = `|title|Filters\n|pagehtml|

Filters

`; + if (!user.can('lock')) { + return buf + `

Access denied

`; + } + let content = ``; + if (filterWords.publicwarn.length) { + content += `

Filtered in public rooms

`; + content += filterWords.publicwarn.map(([str, reason, , hits]) => `${str}${hits}`).join(''); + } + if (filterWords.warn.length) { + content += `

Filtered

`; + content += filterWords.warn.map(([str, reason, , hits]) => `${str}${hits}`).join(''); + } + if (filterWords.autolock.length) { + content += `

Weeklock

`; + content += filterWords.autolock.map(([str, reason, , hits]) => `${str}${hits}`).join(''); + } + if (filterWords.namefilter.length) { + content += `

Filtered in names

`; + content += filterWords.namefilter.map(([str, reason, , hits]) => `${str}${hits}`).join(''); + } + if (filterWords.wordfilter.length) { + content += `

Filtered in public rooms

`; + content += filterWords.wordfilter.map(([str, reason, filterTo, hits]) => `${str} ⇒ ${filterTo}${hits}`).join(''); + } + if (!content) { + buf += `

There are no filtered words.

`; + } else { + buf += `${content}
`; + } + buf += `
`; + return buf; + }, +}; +exports.pages = pages; /** @type {ChatCommands} */ let commands = { @@ -280,26 +322,7 @@ let commands = { } }, view: function (target, room, user) { - if (!this.can('lock')) return false; - - let content = ''; - if (filterWords.publicwarn.length) { - content += `

Filtered in public rooms:

${filterWords.publicwarn.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.warn.length) { - content += `

Filtered:

${filterWords.warn.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.autolock.length) { - content += `

Weeklock:

${filterWords.autolock.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.namefilter.length) { - content += `

Filtered in names:

${filterWords.namefilter.map(([str, reason]) => `${str}`).join('')}`; - } - if (filterWords.wordfilter.length) { - content += `

Filtered in public rooms:

${filterWords.wordfilter.map(([str, reason, filterTo]) => `${str} => ${filterTo}`).join('')}`; - } - if (!content) return this.sendReplyBox("There are no filtered words."); - return this.sendReply(`|raw|
${content}
`); + this.parse(`/join view-filters`); }, }, allowname: function (target, room, user) { From 0edfea30051cb42fa171b6fbe87fd2b04d207cac Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Tue, 18 Sep 2018 01:24:12 -0400 Subject: [PATCH 075/125] Improve modlog - Fix breakage on corrupt modlogs - Rename some variables/functions to be clearer --- chat-plugins/modlog.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/chat-plugins/modlog.js b/chat-plugins/modlog.js index 79a543a991a1..6e0ac24d7931 100644 --- a/chat-plugins/modlog.js +++ b/chat-plugins/modlog.js @@ -38,7 +38,7 @@ class SortedLimitedLengthList { return this.list.slice(); } - tryInsert(element) { + insert(element) { let insertedAt = -1; for (let i = this.list.length - 1; i >= 0; i--) { if (element.localeCompare(this.list[i]) < 0) { @@ -54,9 +54,7 @@ class SortedLimitedLengthList { if (insertedAt < 0) this.list.splice(0, 0, element); if (this.list.length > this.maxSize) { this.list.pop(); - if (insertedAt === this.list.length) return false; } - return true; } } @@ -76,7 +74,7 @@ function checkRipgrepAvailability() { return Config.ripgrepmodlog; } -function getMoreButton(room, search, useExactSearch, lines, maxLines) { +function getMoreButton(roomid, search, useExactSearch, lines, maxLines) { let newLines = 0; for (let increase of MORE_BUTTON_INCREMENTS) { if (increase > lines) { @@ -88,7 +86,7 @@ function getMoreButton(room, search, useExactSearch, lines, maxLines) { return ''; // don't show a button if no more pre-set increments are valid or if the amount of results is already below the max } else { if (useExactSearch) search = Chat.escapeHTML(`"${search}"`); - return `
`; + return `
`; } } @@ -144,8 +142,7 @@ async function checkRoomModlog(path, regex, results) { let line; while ((line = await fileStream.readLine()) !== null) { if (!regex || regex.test(line)) { - const insertionSuccessful = results.tryInsert(line); - if (!insertionSuccessful) break; + results.insert(line); } } fileStream.destroy(); @@ -160,17 +157,17 @@ function runRipgrepModlog(paths, regexString, results) { return results; } for (const fileName of stdout.toString().split('\n').reverse()) { - if (fileName) results.tryInsert(fileName); + if (fileName) results.insert(fileName); } return results; } -function prettifyResults(resultArray, room, searchString, exactSearch, addModlogLinks, hideIps, maxLines) { +function prettifyResults(resultArray, roomid, searchString, exactSearch, addModlogLinks, hideIps, maxLines) { if (resultArray === null) { return "The modlog query has crashed."; } let roomName; - switch (room) { + switch (roomid) { case 'all': roomName = "all rooms"; break; @@ -178,13 +175,13 @@ function prettifyResults(resultArray, room, searchString, exactSearch, addModlog roomName = "all public rooms"; break; default: - roomName = `room ${room}`; + roomName = `room ${roomid}`; } if (!resultArray.length) { return `|popup|No moderator actions containing ${searchString} found on ${roomName}.` + (exactSearch ? "" : " Add quotes to the search parameter to search for a phrase, rather than a user."); } - const title = `[${room}]` + (searchString ? ` ${searchString}` : ``); + const title = `[${roomid}]` + (searchString ? ` ${searchString}` : ``); let lines = resultArray.length; let curDate = ''; resultArray.unshift(''); @@ -218,15 +215,17 @@ function prettifyResults(resultArray, room, searchString, exactSearch, addModlog return `${date}[${timestamp}] (${thisRoomID})${Chat.escapeHTML(line.slice(parenIndex + 1))}`; }).join(`
`); let preamble; - const modlogid = room + (searchString ? '-' + Dashycode.encode(searchString) : ''); + const modlogid = roomid + (searchString ? '-' + Dashycode.encode(searchString) : ''); if (searchString) { const searchStringDescription = (exactSearch ? `containing the string "${searchString}"` : `matching the username "${searchString}"`); - preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n|pagehtml|

The last ${Chat.count(lines, "logged actions")} ${searchStringDescription} on ${roomName}.` + + preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n` + + `|pagehtml|

The last ${Chat.count(lines, "logged actions")} ${searchStringDescription} on ${roomName}.` + (exactSearch ? "" : " Add quotes to the search parameter to search for a phrase, rather than a user."); } else { - preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n|pagehtml|

The last ${Chat.count(lines, "lines")} of the Moderator Log of ${roomName}.`; + preamble = `>view-modlog-${modlogid}\n|init|html\n|title|[Modlog]${title}\n` + + `|pagehtml|

The last ${Chat.count(lines, "lines")} of the Moderator Log of ${roomName}.`; } - let moreButton = getMoreButton(room, searchString, exactSearch, lines, maxLines); + let moreButton = getMoreButton(roomid, searchString, exactSearch, lines, maxLines); return `${preamble}${resultString}${moreButton}

`; } From a0440a8f1a41535a64324797fa43d3ebcf58256b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 13:28:59 +0200 Subject: [PATCH 076/125] Chat monitor: also display allowed names on the filter list and move namefilterwhitelist to Chat --- chat-plugins/chat-monitor.js | 13 +++++++++---- chat.js | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index bc0ec9b670aa..cc4484a601af 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -162,11 +162,10 @@ let chatfilter = function (message, user, room) { return message; }; -const namefilterwhitelist = new Map(); /** @type {NameFilter} */ let namefilter = function (name, user) { let id = toId(name); - if (namefilterwhitelist.has(id)) return name; + if (Chat.namefilterwhitelist.has(id)) return name; if (id === user.trackRename) return ''; let lcName = name.replace(/\u039d/g, 'N').toLowerCase().replace(/[\u200b\u007F\u00AD]/g, '').replace(/\u03bf/g, 'o').replace(/\u043e/g, 'o').replace(/\u0430/g, 'a').replace(/\u0435/g, 'e').replace(/\u039d/g, 'e'); // Remove false positives. @@ -238,9 +237,15 @@ const pages = { content += filterWords.namefilter.map(([str, reason, , hits]) => `${str}${hits}`).join(''); } if (filterWords.wordfilter.length) { - content += `

Filtered in public rooms

`; + content += `

Filtered to different phrases

`; content += filterWords.wordfilter.map(([str, reason, filterTo, hits]) => `${str} ⇒ ${filterTo}${hits}`).join(''); } + if (Chat.namefilterwhitelist.size) { + content += `

Whitelisted names

`; + for (const [val] of Chat.namefilterwhitelist) { + content += `${val}`; + } + } if (!content) { buf += `

There are no filtered words.

`; } else { @@ -329,7 +334,7 @@ let commands = { if (!this.can('forcerename')) return false; target = toId(target); if (!target) return this.errorReply(`Syntax: /allowname username`); - namefilterwhitelist.set(target, user.name); + Chat.namefilterwhitelist.set(target, user.name); const msg = `${target} was allowed as a username by ${user.name}.`; const staffRoom = Rooms('staff'); diff --git a/chat.js b/chat.js index e7653e536280..738b952ef63e 100644 --- a/chat.js +++ b/chat.js @@ -1604,3 +1604,5 @@ Chat.updateServerLock = false; Chat.filterKeys = {}; /** @type {{[k: string]: string[]}} */ Chat.filterWords = {}; +/** @type {Map} */ +Chat.namefilterwhitelist = new Map(); From c1d047e674d3255494960e618e84c6b21ee6adfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 15:03:41 +0200 Subject: [PATCH 077/125] Allow word boundaries in name filters --- chat-plugins/chat-monitor.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index cc4484a601af..aeec317334d0 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -193,7 +193,14 @@ let namefilter = function (name, user) { } } for (let line of filterWords.namefilter) { - if (lcName.includes(String(line[0]))) { + const word = String(line[0]); + let matched; + if (word.startsWith('\\b') || word.endsWith('\\b')) { + matched = new RegExp(word).test(lcName); + } else { + matched = lcName.includes(word); + } + if (matched) { user.trackRename = name; line[3]++; saveFilters(); From 334af1a7ab6bef8940c5288155d3cac74d3ebf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 15:49:21 +0200 Subject: [PATCH 078/125] Chat monitor: do not bypass filters in groupchats ending in 'staff' --- chat-plugins/chat-monitor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index aeec317334d0..ad901b9d4c6a 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -89,7 +89,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.id.endsWith('staff')) return `${message} __[would be locked: ${line}${reason ? ` (${reason})` : ''}]__`; + if (room && room.chatRoomData && room.id.endsWith('staff')) return `${message} __[would be locked: ${line}${reason ? ` (${reason})` : ''}]__`; if (user.isStaff) { return message; } @@ -115,7 +115,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.id.endsWith('staff')) return `${message} __[would be filtered: ${line}${reason ? ` (${reason})` : ''}]__`; + if (room && room.chatRoomData && room.id.endsWith('staff')) return `${message} __[would be filtered: ${line}${reason ? ` (${reason})` : ''}]__`; if (user.isStaff) { return message; } @@ -136,7 +136,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.id.endsWith('staff')) return `${message} __[would be filtered in public: ${line}${reason ? ` (${reason})` : ''}]__`; + if (room && room.chatRoomData && room.id.endsWith('staff')) return `${message} __[would be filtered in public: ${line}${reason ? ` (${reason})` : ''}]__`; if (user.isStaff) { return message; } From 4037d2282c03fa0c38f1ea46375e7c63300f92c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 15:59:59 +0200 Subject: [PATCH 079/125] Chat monitor: fix wordfilter --- chat-plugins/chat-monitor.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index ad901b9d4c6a..3e52d2d3a0f0 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -150,12 +150,12 @@ let chatfilter = function (message, user, room) { for (let line of filterWords.wordfilter) { const regex = line[0]; if (typeof regex === 'string') continue; - let matches = regex.exec(lcMessage); - if (!matches) continue; - for (let match of matches) { - message.replace(match[0], line[2] || ''); + let match = regex.exec(lcMessage); + while (match) { + message = message.replace(match[0], line[2] || ''); line[3]++; saveFilters(); + match = regex.exec(lcMessage); } } From 54b6ba46bb96334fe96a205be2cd15d593b4e39f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 17:16:47 +0200 Subject: [PATCH 080/125] Chat monitor: trim reason --- chat-plugins/chat-monitor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 3e52d2d3a0f0..c513001335c0 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -282,7 +282,7 @@ let commands = { let [word, filterTo, ...reasonParts] = rest; word = word.trim(); filterTo = filterTo.trim(); - let reason = reasonParts.join(','); + let reason = reasonParts.join(',').trim(); if (!filterTo) return this.errorReply(`Syntax for word filters: /filter add ${list}, regex, filter to, reason`); let regex; @@ -299,7 +299,7 @@ let commands = { } else { let [word, ...reasonParts] = rest; word = word.trim(); - let reason = reasonParts.join(','); + let reason = reasonParts.join(',').trim(); if (filterWords[list].some(val => val[0] === word)) return this.errorReply(`${word} is already added to the ${list} list.`); filterWords[list].push([word, reason, null, 0]); this.globalModlog(`ADDFILTER`, null, `'${word}' to ${list} list by ${user.name}${reason ? ` (${reason})` : ''}`); From bfa0d845afdeca2c29fee99b0879a3d317403403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 17:31:01 +0200 Subject: [PATCH 081/125] Chat monitor: add alias for /filter view --- chat-plugins/chat-monitor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index c513001335c0..4d1d476e216f 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -333,6 +333,7 @@ let commands = { return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); } }, + list: 'view', view: function (target, room, user) { this.parse(`/join view-filters`); }, From 8a9ce5daea4f3a14905b0b38680efc01e7dcb5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 17:43:31 +0200 Subject: [PATCH 082/125] Chat monitor: add help command --- chat-plugins/chat-monitor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 4d1d476e216f..be52f09a6254 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -338,6 +338,9 @@ let commands = { this.parse(`/join view-filters`); }, }, + filterhelp: [`- /filter add list, word, reason - Adds a word to the given filter list. Requires: ~`, + `- /filter remove list, words - Removes words from the given filter list. Requires: ~`, + `- /filter view - Opens the list of filtered words. Requires: % @ * & ~`], allowname: function (target, room, user) { if (!this.can('forcerename')) return false; target = toId(target); From 373e3dc1c80f5d2e7fc604dbcde447c30b6d6b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 18 Sep 2018 17:54:25 +0200 Subject: [PATCH 083/125] Chat monitor: Label ids in /filter view --- chat-plugins/chat-monitor.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index be52f09a6254..852a68566f9d 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -227,24 +227,24 @@ const pages = { return buf + `

Access denied

`; } let content = ``; + content += `

Filtered in public rooms [publicwarn]

`; if (filterWords.publicwarn.length) { - content += `

Filtered in public rooms

`; content += filterWords.publicwarn.map(([str, reason, , hits]) => `${str}${hits}`).join(''); } + content += `

Filtered [warn]

`; if (filterWords.warn.length) { - content += `

Filtered

`; content += filterWords.warn.map(([str, reason, , hits]) => `${str}${hits}`).join(''); } + content += `

Weeklock [autolock]

`; if (filterWords.autolock.length) { - content += `

Weeklock

`; content += filterWords.autolock.map(([str, reason, , hits]) => `${str}${hits}`).join(''); } + content += `

Filtered in names [namefilter]

`; if (filterWords.namefilter.length) { - content += `

Filtered in names

`; content += filterWords.namefilter.map(([str, reason, , hits]) => `${str}${hits}`).join(''); } + content += `

Filtered to different phrases [wordfilter]

`; if (filterWords.wordfilter.length) { - content += `

Filtered to different phrases

`; content += filterWords.wordfilter.map(([str, reason, filterTo, hits]) => `${str} ⇒ ${filterTo}${hits}`).join(''); } if (Chat.namefilterwhitelist.size) { From 9e93c4cf1d54ec3da65cbc95a8c64b3214e628d8 Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Tue, 18 Sep 2018 14:26:22 -0400 Subject: [PATCH 084/125] Add basic language support (#4862) --- chat-plugins/info.js | 30 +++++++++++++++++++--- chat-plugins/roomsettings.js | 48 ++++++++++++++++++++++++++++++++++++ rooms.js | 2 ++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index aa20b09406ac..9a0289ccc37f 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -1682,11 +1682,25 @@ const commands = { rule: 'rules', rules: function (target, room, user) { if (!target) { + const languageTable = { + portuguese: ['Por favor siga as regras:', 'pages/rules-pt', 'Regras Globais', `Regras da sala ${room.title}`], + spanish: ['Por favor sigue las reglas:', 'pages/rules-es', 'Reglas Globales', `Reglas de la sala ${room.title}`], + italian: ['Per favore, rispetta le seguenti regole:', 'pages/rules-it', 'Regole Globali', `Regole della room ${room.title}`], + french: ['Veuillez suivre ces règles:', 'pages/rules-fr', 'Règles Générales', `Règles de la room ${room.title}`], + simplifiedchinese: ['请遵守规则:', 'pages/rules-zh', '全站规则', `${room.title}房间规则`], + traditionalchinese: ['請遵守規則:', 'pages/rules-tw', '全站規則', `${room.title}房間規則`], + japanese: ['ルールを守ってください:', 'pages/rules-ja', '全部屋共通ルール', `${room.title}部屋のルール`], + hindi: ['कृपया इन नियमों का पालन करें:', 'pages/rules-hi', 'आप सभी के लिए नियम:', `${room.title} इस Room के नियम:`], + turkish: ['Lütfen kurallara uyun:', 'pages/rules-tr', 'Genel kurallar', `${room.title} odası kuralları`], + dutch: ['Volg de regels:', 'pages/rules-nl', 'Globale Regels ', `Regels van de ${room.title} room`], + german: ['Bitte befolgt die Regeln:', 'pages/rules-de', 'Globale Regeln', `Regeln des ${room.title} Raumes`], + english: ['Please follow the rules:', 'rules', 'Global Rules', `${room.title} room rules`], + }; if (!this.runBroadcast()) return; this.sendReplyBox( - `Please follow the rules:
` + - (room && room.rulesLink ? Chat.html`- ${room.title} room rules
` : ``) + - `- ${room && room.rulesLink ? "Global rules" : "Rules"}` + `${languageTable[room.language || 'english'][0]}
` + + (room && room.rulesLink ? Chat.html`- ${languageTable[room.language || 'english'][3]}
` : ``) + + `- ${languageTable[room.language || 'english'][2]}` ); return; } @@ -1845,11 +1859,21 @@ const commands = { } else if (extraFormat.effectType !== 'Format') { formatName = formatId = ''; } + const supportedLanguages = { + spanish: 'es', + french: 'fr', + italian: 'ita', + german: 'ger', + portuguese: 'por', + }; let speciesid = pokemon.speciesid; // Special case for Meowstic-M if (speciesid === 'meowstic') speciesid = 'meowsticm'; if (pokemon.tier === 'CAP') { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis preview, brought to you by Smogon University CAP Project`); + } else if (formatId === 'ou' && generation === 'sm' && room.language in supportedLanguages) { + // Limited support for translated analysis + this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } else { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } diff --git a/chat-plugins/roomsettings.js b/chat-plugins/roomsettings.js index b5d3e735c7ea..6042fc7d745c 100644 --- a/chat-plugins/roomsettings.js +++ b/chat-plugins/roomsettings.js @@ -157,8 +157,20 @@ class RoomSettings { return `${this.button('Mafia enabled', null, 'mafia enable')} ${this.button('off', true)}`; } } + language() { + const languageList = ['Portuguese', 'Spanish', 'Italian', 'French', 'Simplified Chinese', 'Traditional Chinese', 'Japanese', 'Hindi', 'Turkish', 'Dutch', 'German']; + if (!this.user.can('editroom', null, this.room)) return this.button(this.room.language ? this.room.language : 'English', true); + + let languageOutput = []; + languageOutput.push(this.button(`English`, !this.room.language, 'roomlanguage english')); + for (let language of languageList) { + languageOutput.push(this.button(`${language}`, this.room.language === toId(language), `roomlanguage ${toId(language)}`)); + } + return languageOutput.join(' '); + } generateDisplay(user, room, connection) { let output = Chat.html`
Room Settings for ${this.room.title}
`; + output += `Language:
${this.language()}
`; output += `Modchat:
${this.modchat()}
`; output += `Modjoin:
${this.modjoin()}
`; output += `Stretch filter:
${this.stretching()}
`; @@ -349,6 +361,42 @@ exports.commands = { `/modjoin [sync|off] - Sets modjoin. Only users who can speak in modchat can join this room. Requires: \u2606 # & ~`, ], + roomlanguage: function (target, room, user) { + const languageTable = { + portuguese: 'Portuguese', + spanish: 'Spanish', + italian: 'Italian', + french: 'French', + simplifiedchinese: 'Simplified Chinese', + traditionalchinese: 'Traditional Chinese', + japanese: 'Japanese', + hindi: 'Hindi', + turkish: 'Turkish', + dutch: 'Dutch', + german: 'German', + // Listed as "false" under room.language + english: 'English', + }; + if (!target) return this.sendReply(`This room's primary language is ${languageTable[room.language] || 'English'}`); + if (!this.can('editroom', null, room)) return false; + + let targetLanguage = toId(target); + if (!(targetLanguage in languageTable)) return this.errorReply(`"${target}" is not a supported language.`); + + room.language = targetLanguage === 'english' ? false : targetLanguage; + + if (room.chatRoomData) { + room.chatRoomData.language = room.language; + Rooms.global.writeChatRoomData(); + } + this.modlog(`LANGUAGE`, null, languageTable[targetLanguage]); + this.sendReply(`The room's language has been set to ${languageTable[targetLanguage]}`); + }, + roomlanguagehelp: [ + `/roomlanguage [language] - Sets the the language for the room, which changes language of a few commands. Requires # & ~`, + `Supported Languages: English, Spanish, Italian, French, Simplified Chinese, Traditional Chinese, Japanese, Hindi, Turkish, Dutch, German.`, + ], + slowchat: function (target, room, user) { if (!target) { const slowchatSetting = (room.slowchat || "OFF"); diff --git a/rooms.js b/rooms.js index e6cf545c1e29..f7b4d0e176af 100644 --- a/rooms.js +++ b/rooms.js @@ -101,6 +101,8 @@ class BasicRoom { /** @type {string?} */ this.modchat = null; this.staffRoom = false; + /** @type {string | false} */ + this.language = false; /** @type {false | number} */ this.slowchat = false; this.filterStretching = false; From fe2097dd8259b8194d4dd71fa6fc6785ca4cc5c9 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Tue, 18 Sep 2018 22:28:24 +0400 Subject: [PATCH 085/125] Reduce Emergency Exit's rating --- data/abilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/abilities.js b/data/abilities.js index 1e3f65ca5989..315b7817af10 100644 --- a/data/abilities.js +++ b/data/abilities.js @@ -872,7 +872,7 @@ let BattleAbilities = { }, id: "emergencyexit", name: "Emergency Exit", - rating: 2, + rating: 1.5, num: 194, }, "fairyaura": { From cf24fab45dc680c9255622cab4a02cc46fdcfb62 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Tue, 18 Sep 2018 22:28:42 +0400 Subject: [PATCH 086/125] Gen 5: Random Battle improvements --- mods/gen5/formats-data.js | 24 ++++++++++++------------ mods/gen5/random-teams.js | 23 ++++++++++++----------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/mods/gen5/formats-data.js b/mods/gen5/formats-data.js index 99b5f0f4fd6b..0778a9168150 100644 --- a/mods/gen5/formats-data.js +++ b/mods/gen5/formats-data.js @@ -98,7 +98,7 @@ let BattleFormatsData = { }, raticate: { inherit: true, - randomBattleMoves: ["facade", "flamewheel", "suckerpunch", "uturn"], + randomBattleMoves: ["facade", "flamewheel", "suckerpunch", "uturn", "swordsdance"], tier: "NU", }, spearow: { @@ -715,7 +715,7 @@ let BattleFormatsData = { }, lapras: { inherit: true, - randomBattleMoves: ["icebeam", "thunderbolt", "healbell", "toxic", "surf", "dragondance", "substitute", "waterfall", "return", "avalanche", "rest", "sleeptalk", "curse", "iceshard", "drillrun"], + randomBattleMoves: ["icebeam", "thunderbolt", "healbell", "toxic", "surf", "dragondance", "waterfall", "return", "avalanche", "rest", "sleeptalk", "curse", "iceshard", "drillrun"], tier: "NU", }, ditto: { @@ -833,7 +833,7 @@ let BattleFormatsData = { }, dragonite: { inherit: true, - randomBattleMoves: ["dragondance", "outrage", "firepunch", "extremespeed", "dragonclaw", "earthquake", "roost", "waterfall", "substitute", "thunderwave", "dragontail", "hurricane", "superpower", "dracometeor"], + randomBattleMoves: ["dragondance", "outrage", "firepunch", "extremespeed", "dragonclaw", "earthquake", "roost", "substitute", "thunderwave", "dragontail", "hurricane", "superpower", "dracometeor"], tier: "OU", }, mewtwo: { @@ -1502,7 +1502,7 @@ let BattleFormatsData = { }, sableye: { inherit: true, - randomBattleMoves: ["recover", "willowisp", "taunt", "trick", "toxic", "nightshade", "seismictoss"], + randomBattleMoves: ["recover", "willowisp", "taunt", "trick", "toxic", "nightshade", "foulplay"], tier: "UU", }, mawile: { @@ -1587,7 +1587,7 @@ let BattleFormatsData = { tier: "LC", }, sharpedo: { - randomBattleMoves: ["protect", "hydropump", "icebeam", "crunch", "earthquake", "waterfall", "hiddenpowergrass", "aquajet"], + randomBattleMoves: ["protect", "hydropump", "icebeam", "crunch", "earthquake", "waterfall", "hiddenpowergrass"], tier: "UU", }, wailmer: { @@ -1714,7 +1714,7 @@ let BattleFormatsData = { tier: "LC", }, cradily: { - randomBattleMoves: ["stealthrock", "recover", "stockpile", "seedbomb", "rockslide", "earthquake", "curse", "swordsdance"], + randomBattleMoves: ["stealthrock", "recover", "toxic", "seedbomb", "rockslide", "earthquake", "curse", "swordsdance"], tier: "NU", }, anorith: { @@ -1733,7 +1733,7 @@ let BattleFormatsData = { }, milotic: { inherit: true, - randomBattleMoves: ["recover", "scald", "hypnosis", "toxic", "icebeam", "dragontail", "rest", "sleeptalk", "hiddenpowergrass"], + randomBattleMoves: ["recover", "scald", "toxic", "icebeam", "dragontail", "rest", "sleeptalk", "hiddenpowergrass"], tier: "UU", }, castform: { @@ -1913,7 +1913,7 @@ let BattleFormatsData = { }, jirachi: { inherit: true, - randomBattleMoves: ["bodyslam", "ironhead", "firepunch", "thunderwave", "stealthrock", "wish", "uturn", "calmmind", "psychic", "thunder", "icepunch", "flashcannon", "meteormash"], + randomBattleMoves: ["bodyslam", "ironhead", "firepunch", "thunderwave", "stealthrock", "wish", "uturn", "calmmind", "psychic", "thunder", "icepunch", "flashcannon"], tier: "OU", }, deoxys: { @@ -2026,7 +2026,7 @@ let BattleFormatsData = { tier: "NFE", }, luxray: { - randomBattleMoves: ["wildcharge", "icefang", "firefang", "crunch", "superpower"], + randomBattleMoves: ["wildcharge", "icefang", "facade", "crunch", "superpower"], tier: "NU", }, cranidos: { @@ -2194,7 +2194,7 @@ let BattleFormatsData = { tier: "LC", }, drapion: { - randomBattleMoves: ["crunch", "whirlwind", "toxicspikes", "pursuit", "earthquake", "aquatail", "swordsdance", "poisonjab", "rest", "sleeptalk"], + randomBattleMoves: ["crunch", "whirlwind", "toxicspikes", "pursuit", "earthquake", "aquatail", "swordsdance", "poisonjab", "taunt"], tier: "RU", }, croagunk: { @@ -2363,7 +2363,7 @@ let BattleFormatsData = { requiredItem: "Flame Plate", }, arceusflying: { - randomBattleMoves: ["calmmind", "judgment", "refresh", "recover"], + randomBattleMoves: ["calmmind", "judgment", "refresh", "recover", "focusblast"], eventOnly: true, requiredItem: "Sky Plate", }, @@ -2428,7 +2428,7 @@ let BattleFormatsData = { }, serperior: { inherit: true, - randomBattleMoves: ["leafstorm", "hiddenpowerfire", "substitute", "leechseed", "dragonpulse", "gigadrain"], + randomBattleMoves: ["calmmind", "hiddenpowerfire", "substitute", "leechseed", "dragonpulse", "gigadrain"], tier: "NU", }, tepig: { diff --git a/mods/gen5/random-teams.js b/mods/gen5/random-teams.js index 95ee461ec0ee..77a00ad5f88d 100644 --- a/mods/gen5/random-teams.js +++ b/mods/gen5/random-teams.js @@ -232,8 +232,8 @@ class RandomGen5Teams extends RandomGen6Teams { case 'drainpunch': if (hasMove['closecombat'] || hasMove['crosschop'] || hasMove['highjumpkick']) rejected = true; break; - case 'fierydance': case 'flamethrower': - if (hasMove['blueflare'] || hasMove['fireblast'] || hasMove['lavaplume'] || hasMove['overheat']) rejected = true; + case 'flareblitz': case 'fierydance': case 'flamethrower': case 'lavaplume': + if (hasMove['blueflare'] || hasMove['fireblast'] || hasMove['overheat']) rejected = true; break; case 'firepunch': if (hasMove['flareblitz']) rejected = true; @@ -241,11 +241,8 @@ class RandomGen5Teams extends RandomGen6Teams { case 'overheat': if (counter.setupType === 'Special' || hasMove['fireblast']) rejected = true; break; - case 'airslash': - if (hasMove['hurricane']) rejected = true; - break; - case 'bravebird': case 'drillpeck': case 'pluck': - if (hasMove['acrobatics']) rejected = true; + case 'airslash': case 'bravebird': case 'drillpeck': case 'pluck': + if (hasMove['acrobatics'] || hasMove['hurricane']) rejected = true; break; case 'gigadrain': if ((!counter.setupType && hasMove['leafstorm']) || hasMove['petaldance']) rejected = true; @@ -294,7 +291,7 @@ class RandomGen5Teams extends RandomGen6Teams { break; // Status: - case 'encore': case 'suckerpunch': + case 'encore': case 'iceshard': case 'suckerpunch': if (hasMove['rest'] && hasMove['sleeptalk']) rejected = true; break; case 'moonlight': case 'painsplit': case 'recover': case 'roost': case 'softboiled': case 'synthesis': @@ -467,6 +464,8 @@ class RandomGen5Teams extends RandomGen6Teams { rejectAbility = counter['damage'] >= counter.damagingMoves.length || (counter.Status > 2 && !counter.setupType); } else if (ability === 'Torrent') { rejectAbility = !counter['Water']; + } else if (ability === 'Unburden') { + rejectAbility = template.baseStats.spe > 100; } else if (ability === 'Water Absorb') { rejectAbility = abilities.includes('Volt Absorb'); } @@ -559,9 +558,11 @@ class RandomGen5Teams extends RandomGen6Teams { // Medium priority } else if (counter.Physical >= 4 && !hasMove['fakeout'] && !hasMove['suckerpunch'] && !hasMove['flamecharge'] && !hasMove['rapidspin']) { - item = this.randomChance(2, 3) ? 'Choice Band' : 'Expert Belt'; + item = !counter['Normal'] && this.randomChance(1, 3) ? 'Expert Belt' : 'Choice Band'; } else if (counter.Special >= 4) { - item = this.randomChance(2, 3) ? 'Choice Specs' : 'Expert Belt'; + item = this.randomChance(1, 3) ? 'Expert Belt' : 'Choice Specs'; + } else if (ability === 'Speed Boost' && !hasMove['substitute'] && counter.Physical + counter.Special > 2) { + item = 'Life Orb'; } else if ((hasMove['eruption'] || hasMove['waterspout']) && !counter['Status']) { item = 'Choice Scarf'; } else if (this.getEffectiveness('Ground', template) >= 2 && ability !== 'Levitate' && !hasMove['magnetrise']) { @@ -587,7 +588,7 @@ class RandomGen5Teams extends RandomGen6Teams { } else if (counter.Physical + counter.Special >= 3 && counter.setupType) { item = hasMove['outrage'] ? 'Lum Berry' : 'Life Orb'; } else if (counter.Physical + counter.Special >= 4) { - item = 'Expert Belt'; + item = counter['Normal'] ? 'Life Orb' : 'Expert Belt'; } else if (slot === 0 && ability !== 'Sturdy' && !counter['recoil']) { item = 'Focus Sash'; From d9bd8bd30ce37cad6074234c8fe97face16c02b8 Mon Sep 17 00:00:00 2001 From: Jeremy Piemonte Date: Tue, 18 Sep 2018 16:12:02 -0400 Subject: [PATCH 087/125] Rules: Fix crash when used in PMs (#4863) Introduced in https://github.com/Zarel/Pokemon-Showdown/commit/9e93c4cf1d54ec3da65cbc95a8c64b3214e628d8 --- chat-plugins/info.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 9a0289ccc37f..487ad49ef69f 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -1683,24 +1683,26 @@ const commands = { rules: function (target, room, user) { if (!target) { const languageTable = { - portuguese: ['Por favor siga as regras:', 'pages/rules-pt', 'Regras Globais', `Regras da sala ${room.title}`], - spanish: ['Por favor sigue las reglas:', 'pages/rules-es', 'Reglas Globales', `Reglas de la sala ${room.title}`], - italian: ['Per favore, rispetta le seguenti regole:', 'pages/rules-it', 'Regole Globali', `Regole della room ${room.title}`], - french: ['Veuillez suivre ces règles:', 'pages/rules-fr', 'Règles Générales', `Règles de la room ${room.title}`], - simplifiedchinese: ['请遵守规则:', 'pages/rules-zh', '全站规则', `${room.title}房间规则`], - traditionalchinese: ['請遵守規則:', 'pages/rules-tw', '全站規則', `${room.title}房間規則`], - japanese: ['ルールを守ってください:', 'pages/rules-ja', '全部屋共通ルール', `${room.title}部屋のルール`], - hindi: ['कृपया इन नियमों का पालन करें:', 'pages/rules-hi', 'आप सभी के लिए नियम:', `${room.title} इस Room के नियम:`], - turkish: ['Lütfen kurallara uyun:', 'pages/rules-tr', 'Genel kurallar', `${room.title} odası kuralları`], - dutch: ['Volg de regels:', 'pages/rules-nl', 'Globale Regels ', `Regels van de ${room.title} room`], - german: ['Bitte befolgt die Regeln:', 'pages/rules-de', 'Globale Regeln', `Regeln des ${room.title} Raumes`], - english: ['Please follow the rules:', 'rules', 'Global Rules', `${room.title} room rules`], + portuguese: ['Por favor siga as regras:', 'pages/rules-pt', 'Regras Globais', room ? `Regras da sala ${room.title}` : ``], + spanish: ['Por favor sigue las reglas:', 'pages/rules-es', 'Reglas Globales', room ? `Reglas de la sala ${room.title}` : ``], + italian: ['Per favore, rispetta le seguenti regole:', 'pages/rules-it', 'Regole Globali', room ? `Regole della room ${room.title}` : ``], + french: ['Veuillez suivre ces règles:', 'pages/rules-fr', 'Règles Générales', room ? `Règles de la room ${room.title}` : ``], + simplifiedchinese: ['请遵守规则:', 'pages/rules-zh', '全站规则', room ? `${room.title}房间规则` : ``], + traditionalchinese: ['請遵守規則:', 'pages/rules-tw', '全站規則', room ? `${room.title}房間規則` : ``], + japanese: ['ルールを守ってください:', 'pages/rules-ja', '全部屋共通ルール', room ? `${room.title}部屋のルール` : ``], + hindi: ['कृपया इन नियमों का पालन करें:', 'pages/rules-hi', 'आप सभी के लिए नियम:', room ? `${room.title} इस Room के नियम:` : ``], + turkish: ['Lütfen kurallara uyun:', 'pages/rules-tr', 'Genel kurallar', room ? `${room.title} odası kuralları` : ``], + dutch: ['Volg de regels:', 'pages/rules-nl', 'Globale Regels ', room ? `Regels van de ${room.title} room` : ``], + german: ['Bitte befolgt die Regeln:', 'pages/rules-de', 'Globale Regeln', room ? `Regeln des ${room.title} Raumes` : ``], + english: ['Please follow the rules:', 'rules', 'Global Rules', room ? `${room.title} room rules` : ``], }; if (!this.runBroadcast()) return; + const globalRulesLink = `https://pokemonshowdown.com/${languageTable[room && room.language ? room.language : 'english'][1]}`; + const globalRulesLinkText = languageTable[room && room.language ? room.language : 'english'][2]; this.sendReplyBox( - `${languageTable[room.language || 'english'][0]}
` + + `${room ? languageTable[room.language || 'english'][0] + '
' : ``}` + (room && room.rulesLink ? Chat.html`- ${languageTable[room.language || 'english'][3]}
` : ``) + - `- ${languageTable[room.language || 'english'][2]}` + `- ${globalRulesLinkText}` ); return; } From ddbdedd10bf3ca5c64a4c41e24dc7dbbcd02c334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 12:53:51 +0200 Subject: [PATCH 088/125] Chat monitor: make word filters case insensitive --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 852a68566f9d..faf874dcf8d9 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -33,7 +33,7 @@ setImmediate(() => { if (filterKeys[key][0] === location && filterKeys[key][1] === punishment) { if (punishment === 'FILTERTO') { const filterTo = rest[0]; - filterWords[key].push([new RegExp(word, 'g'), reason, filterTo, parseInt(times) || 0]); + filterWords[key].push([new RegExp(word, 'ig'), reason, filterTo, parseInt(times) || 0]); continue loop; } else { filterWords[key].push([word, reason, null, parseInt(times) || 0]); From 2c98acaf542eb354b6ba5d737671aaaeace8d494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 13:15:00 +0200 Subject: [PATCH 089/125] Chat monitor: fix saving wordfilters --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index faf874dcf8d9..c6f9a05b327f 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -52,7 +52,7 @@ setImmediate(() => { * @param {string} punishment */ function renderEntry(location, word, punishment) { - const str = word[0] instanceof RegExp ? String(word[0]).slice(1, -2) : word[0]; + const str = word[0] instanceof RegExp ? String(word[0]).slice(1, -3) : word[0]; return `${location}\t${str}\t${punishment}\t${word[1]}\t${word[3]}${word[2] ? `\t${word[2]}` : ''}\r\n`; } From 29ca700bbd6983dff7fd08235679be38facdd0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 13:22:58 +0200 Subject: [PATCH 090/125] Chat monitor: also fix removing wordfilters I need to think before I push my sincere apologies --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index c6f9a05b327f..44891a064198 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -318,7 +318,7 @@ let commands = { if (!(list in filterWords)) return this.errorReply(`Invalid list: ${list}. Possible options: ${Object.keys(filterWords).join(', ')}`); if (filterKeys[list][1] === 'FILTERTO') { - const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -2) === val).length); + const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -3) === val).length); if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -2))); this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); From 1df5c26d5cc96d42cb1220002676691a31ecc895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 13:39:21 +0200 Subject: [PATCH 091/125] Chat monitor: one final failure NOTE TO SELF: focus on either work on PS code not both at once --- chat-plugins/chat-monitor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 44891a064198..b3744e80fb00 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -287,7 +287,7 @@ let commands = { let regex; try { - regex = new RegExp(word, 'g'); // eslint-disable-line no-unused-vars + regex = new RegExp(word, 'ig'); // eslint-disable-line no-unused-vars } catch (e) { return this.errorReply(e.message.startsWith('Invalid regular expression: ') ? e.message : `Invalid regular expression: /${word}/: ${e.message}`); } @@ -320,7 +320,7 @@ let commands = { if (filterKeys[list][1] === 'FILTERTO') { const notFound = words.filter(val => !filterWords[list].filter(entry => String(entry[0]).slice(1, -3) === val).length); if (notFound.length) return this.errorReply(`${notFound.join(', ')} ${Chat.plural(notFound, "are", "is")} not on the ${list} list.`); - filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -2))); + filterWords[list] = filterWords[list].filter(entry => !words.includes(String(entry[0]).slice(1, -3))); this.globalModlog(`REMOVEFILTER`, null, `'${words.join(', ')}' from ${list} list by ${user.name}`); saveFilters(); return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); From a0c921323c7b1350c67a80be0dae8967804b2869 Mon Sep 17 00:00:00 2001 From: Alexander B Date: Wed, 19 Sep 2018 08:11:56 -0500 Subject: [PATCH 092/125] Fix double Sky Drop message (#4864) --- data/moves.js | 1 - 1 file changed, 1 deletion(-) diff --git a/data/moves.js b/data/moves.js index 98e94c589769..fd371a9d840e 100644 --- a/data/moves.js +++ b/data/moves.js @@ -14951,7 +14951,6 @@ let BattleMovedex = { if (target.hasType('Flying')) { this.add('-immune', target, '[msg]'); - this.add('-end', target, 'Sky Drop'); return null; } } else { From fac9e369e570b420f4deae18a0c3efa02ccee1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 15:34:32 +0200 Subject: [PATCH 093/125] Chat monitor: use message to parse wordfilter regex to avoid failing the replace --- chat-plugins/chat-monitor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index b3744e80fb00..454d97d9e925 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -150,12 +150,12 @@ let chatfilter = function (message, user, room) { for (let line of filterWords.wordfilter) { const regex = line[0]; if (typeof regex === 'string') continue; - let match = regex.exec(lcMessage); + let match = regex.exec(message); while (match) { message = message.replace(match[0], line[2] || ''); line[3]++; saveFilters(); - match = regex.exec(lcMessage); + match = regex.exec(message); } } From e3fc3e7d6aee2a8920a7886520f8cf1ae07311b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 19:29:35 +0200 Subject: [PATCH 094/125] Chat monitor: exclude staff PMs and help rooms from filters --- chat-plugins/chat-monitor.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 454d97d9e925..012604f100a0 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -89,10 +89,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.chatRoomData && room.id.endsWith('staff')) return `${message} __[would be locked: ${line}${reason ? ` (${reason})` : ''}]__`; - if (user.isStaff) { - return message; - } + if ((room && ((room.chatRoomData && room.id.endsWith('staff')) || room.id.startsWith('help-'))) || user.isStaff || (this.pmTarget && this.pmTarget.isStaff)) return `${message} __[would be locked: ${line}${reason ? ` (${reason})` : ''}]__`; message = message.replace(/(https?):\/\//g, '$1__:__//'); message = message.replace(/\./g, '__.__'); if (room) { @@ -115,10 +112,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.chatRoomData && room.id.endsWith('staff')) return `${message} __[would be filtered: ${line}${reason ? ` (${reason})` : ''}]__`; - if (user.isStaff) { - return message; - } + if ((room && ((room.chatRoomData && room.id.endsWith('staff')) || room.id.startsWith('help-'))) || user.isStaff || (this.pmTarget && this.pmTarget.isStaff)) return `${message} __[would be filtered: ${line}${reason ? ` (${reason})` : ''}]__`; this.errorReply(`Please do not say '${line}'.`); filterWords.warn[i][3]++; saveFilters(); @@ -136,10 +130,7 @@ let chatfilter = function (message, user, room) { matched = lcMessage.includes(line); } if (matched) { - if (room && room.chatRoomData && room.id.endsWith('staff')) return `${message} __[would be filtered in public: ${line}${reason ? ` (${reason})` : ''}]__`; - if (user.isStaff) { - return message; - } + if ((room && ((room.chatRoomData && room.id.endsWith('staff')) || room.id.startsWith('help-'))) || user.isStaff || (this.pmTarget && this.pmTarget.isStaff)) return `${message} __[would be filtered in public: ${line}${reason ? ` (${reason})` : ''}]__`; this.errorReply(`Please do not say '${line}'.`); filterWords.publicwarn[i][3]++; saveFilters(); From 3d8d314e3fc731e82394779f38e49cb22ae773c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 20:46:10 +0200 Subject: [PATCH 095/125] Chat monitor: Allow filtered words in the staff room --- chat-plugins/chat-monitor.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 012604f100a0..8ef6f84de491 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -138,15 +138,17 @@ let chatfilter = function (message, user, room) { } } } - for (let line of filterWords.wordfilter) { - const regex = line[0]; - if (typeof regex === 'string') continue; - let match = regex.exec(message); - while (match) { - message = message.replace(match[0], line[2] || ''); - line[3]++; - saveFilters(); - match = regex.exec(message); + if (room && room.chatRoomData && room.id.endsWith('staff')) { + for (let line of filterWords.wordfilter) { + const regex = line[0]; + if (typeof regex === 'string') continue; + let match = regex.exec(message); + while (match) { + message = message.replace(match[0], line[2] || ''); + line[3]++; + saveFilters(); + match = regex.exec(message); + } } } From 14ac21df92cc6262cb308c66fe692b0776ccb8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 22:48:17 +0200 Subject: [PATCH 096/125] Chat monitor: Keep case the same when filtering words --- chat-plugins/chat-monitor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 8ef6f84de491..b28da545e093 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -144,7 +144,10 @@ let chatfilter = function (message, user, room) { if (typeof regex === 'string') continue; let match = regex.exec(message); while (match) { - message = message.replace(match[0], line[2] || ''); + let filtered = line[2] || ''; + if (match[0] === match[0].toUpperCase()) filtered = filtered.toUpperCase(); + if (match[0][0] === match[0][0].toUpperCase()) filtered = `${filtered ? filtered[0].toUpperCase() : ''}${filtered.slice(1)}`; + message = message.replace(match[0], filtered); line[3]++; saveFilters(); match = regex.exec(message); From 8a1e49465d464baecc657b2a039efea66b3cda3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 19 Sep 2018 22:49:18 +0200 Subject: [PATCH 097/125] Chat monitor: move check for !filterTo up to avoid a crash --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index b28da545e093..9fd29e0aee36 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -276,10 +276,10 @@ let commands = { if (filterKeys[list][1] === 'FILTERTO') { let [word, filterTo, ...reasonParts] = rest; + if (!filterTo) return this.errorReply(`Syntax for word filters: /filter add ${list}, regex, filter to, reason`); word = word.trim(); filterTo = filterTo.trim(); let reason = reasonParts.join(',').trim(); - if (!filterTo) return this.errorReply(`Syntax for word filters: /filter add ${list}, regex, filter to, reason`); let regex; try { From 2def7525a42652b816b0c6f7f103a7cc612f6322 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Thu, 20 Sep 2018 00:52:07 +0400 Subject: [PATCH 098/125] Random Battle: Add Z-Move Mimikyu --- data/random-teams.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/random-teams.js b/data/random-teams.js index b2c63336f0a9..230bffdd0709 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -1347,6 +1347,8 @@ class RandomTeams extends Dex.ModdedDex { item = 'Lycanium Z'; } else if (template.species === 'Marshadow' && hasMove['spectralthief'] && counter.setupType && !teamDetails.zMove) { item = 'Marshadium Z'; + } else if (template.species === 'Mimikyu' && hasMove['playrough'] && counter.setupType && !teamDetails.zMove) { + item = 'Mimikium Z'; } else if ((template.species === 'Necrozma-Dusk-Mane' || template.species === 'Necrozma-Dawn-Wings') && !teamDetails.zMove) { if (hasMove['autotomize'] && hasMove['sunsteelstrike']) { item = 'Solganium Z'; From 70ef2a4658a6c646cbf94db4c5a9329ec46ee7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Thu, 20 Sep 2018 02:32:07 +0200 Subject: [PATCH 099/125] Chat monitor: fix a very unfortunate mistake you know what i give up for today --- chat-plugins/chat-monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 9fd29e0aee36..67e1fdf7f107 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -138,7 +138,7 @@ let chatfilter = function (message, user, room) { } } } - if (room && room.chatRoomData && room.id.endsWith('staff')) { + if (!(room && room.chatRoomData && room.id.endsWith('staff'))) { for (let line of filterWords.wordfilter) { const regex = line[0]; if (typeof regex === 'string') continue; From 0818b1cd9e756497d0a982b8ba2dabcd4ff5e7ee Mon Sep 17 00:00:00 2001 From: The Immortal Date: Thu, 20 Sep 2018 04:47:40 +0400 Subject: [PATCH 100/125] Random Battle: Various improvements --- data/formats-data.js | 6 +++--- data/random-teams.js | 6 +++--- mods/gen5/formats-data.js | 4 ++-- mods/gen5/random-teams.js | 6 +++--- mods/gen6/formats-data.js | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/data/formats-data.js b/data/formats-data.js index 5b9e6a8e56a6..b0aebe42e91e 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -5122,7 +5122,7 @@ let BattleFormatsData = { tier: "LC", }, whimsicott: { - randomBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast", "defog"], + randomBattleMoves: ["encore", "taunt", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast", "defog"], randomDoubleBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "helpinghand", "stunspore", "moonblast", "tailwind", "dazzlinggleam", "gigadrain", "protect", "defog"], eventPokemon: [ {"generation": 5, "level": 50, "gender": "F", "nature": "Timid", "ivs": {"spe": 31}, "isHidden": false, "abilities": ["prankster"], "moves": ["swagger", "gigadrain", "beatup", "helpinghand"], "pokeball": "cherishball"}, @@ -6176,7 +6176,7 @@ let BattleFormatsData = { tier: "LC", }, malamar: { - randomBattleMoves: ["superpower", "knockoff", "psychocut", "rockslide", "substitute", "trickroom"], + randomBattleMoves: ["superpower", "knockoff", "psychocut", "rest", "sleeptalk", "happyhour"], randomDoubleBattleMoves: ["superpower", "psychocut", "rockslide", "trickroom", "knockoff", "protect"], eventPokemon: [ {"generation": 6, "level": 50, "nature": "Adamant", "ivs": {"hp": 31, "atk": 31}, "isHidden": false, "abilities": ["contrary"], "moves": ["superpower", "knockoff", "facade", "rockslide"], "pokeball": "cherishball"}, @@ -6960,7 +6960,7 @@ let BattleFormatsData = { doublesTier: "DUU", }, silvallywater: { - randomBattleMoves: ["multiattack", "icebeam", "thunderbolt", "partingshot"], + randomBattleMoves: ["multiattack", "icebeam", "thunderbolt", "partingshot", "defog"], randomDoubleBattleMoves: ["protect", "multiattack", "icebeam", "thunderbolt", "flamethrower", "partingshot", "uturn", "thunderwave"], requiredItem: "Water Memory", tier: "PU", diff --git a/data/random-teams.js b/data/random-teams.js index 230bffdd0709..e19cbfcc12e0 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -678,8 +678,8 @@ class RandomTeams extends Dex.ModdedDex { switch (moveid) { // Not very useful without their supporting moves - case 'clangingscales': - if (teamDetails.zMove) rejected = true; + case 'clangingscales': case 'happyhour': + if (teamDetails.zMove || hasMove['rest']) rejected = true; break; case 'cottonguard': case 'defendorder': if (!counter['recovery'] && !hasMove['rest']) rejected = true; @@ -1396,7 +1396,7 @@ class RandomTeams extends Dex.ModdedDex { } else { item = (counter.Physical > counter.Special) ? 'Choice Band' : 'Choice Specs'; } - } else if (hasMove['conversion']) { + } else if (hasMove['conversion'] || hasMove['happyhour']) { item = 'Normalium Z'; } else if (hasMove['dig'] && !teamDetails.zMove) { item = 'Groundium Z'; diff --git a/mods/gen5/formats-data.js b/mods/gen5/formats-data.js index 0778a9168150..3fd3895e0af8 100644 --- a/mods/gen5/formats-data.js +++ b/mods/gen5/formats-data.js @@ -587,7 +587,7 @@ let BattleFormatsData = { }, blissey: { inherit: true, - randomBattleMoves: ["wish", "softboiled", "protect", "toxic", "aromatherapy", "seismictoss", "counter", "thunderwave", "stealthrock", "flamethrower", "icebeam"], + randomBattleMoves: ["wish", "softboiled", "protect", "toxic", "aromatherapy", "seismictoss", "counter", "thunderwave", "stealthrock", "flamethrower"], tier: "OU", }, tangela: { @@ -1087,7 +1087,7 @@ let BattleFormatsData = { tier: "UU", }, gliscor: { - randomBattleMoves: ["swordsdance", "acrobatics", "earthquake", "roost", "substitute", "taunt", "icefang", "protect", "toxic", "stealthrock"], + randomBattleMoves: ["swordsdance", "earthquake", "roost", "substitute", "taunt", "icefang", "protect", "toxic", "stealthrock"], tier: "OU", }, snubbull: { diff --git a/mods/gen5/random-teams.js b/mods/gen5/random-teams.js index 77a00ad5f88d..5e961422c287 100644 --- a/mods/gen5/random-teams.js +++ b/mods/gen5/random-teams.js @@ -245,7 +245,7 @@ class RandomGen5Teams extends RandomGen6Teams { if (hasMove['acrobatics'] || hasMove['hurricane']) rejected = true; break; case 'gigadrain': - if ((!counter.setupType && hasMove['leafstorm']) || hasMove['petaldance']) rejected = true; + if ((!counter.setupType && hasMove['leafstorm']) || hasMove['petaldance'] || hasMove['powerwhip']) rejected = true; break; case 'solarbeam': if ((!hasAbility['Drought'] && !hasMove['sunnyday']) || hasMove['gigadrain'] || hasMove['leafstorm']) rejected = true; @@ -424,7 +424,7 @@ class RandomGen5Teams extends RandomGen6Teams { rejectAbility = !counter['inaccurate']; } else if (ability === 'Defiant' || ability === 'Moxie') { rejectAbility = !counter['Physical'] && !hasMove['batonpass']; - } else if (ability === 'Gluttony' || ability === 'Moody') { + } else if (ability === 'Gluttony' || ability === 'Inner Focus' || ability === 'Moody') { rejectAbility = true; } else if (ability === 'Hydration' || ability === 'Rain Dish' || ability === 'Swift Swim') { rejectAbility = !hasMove['raindance'] && !teamDetails['rain']; @@ -589,7 +589,7 @@ class RandomGen5Teams extends RandomGen6Teams { item = hasMove['outrage'] ? 'Lum Berry' : 'Life Orb'; } else if (counter.Physical + counter.Special >= 4) { item = counter['Normal'] ? 'Life Orb' : 'Expert Belt'; - } else if (slot === 0 && ability !== 'Sturdy' && !counter['recoil']) { + } else if (slot === 0 && ability !== 'Regenerator' && ability !== 'Sturdy' && !counter['recoil'] && !counter['recovery'] && template.baseStats.hp + template.baseStats.def + template.baseStats.spd < 285) { item = 'Focus Sash'; // This is the "REALLY can't think of a good item" cutoff diff --git a/mods/gen6/formats-data.js b/mods/gen6/formats-data.js index 44305e9fda2e..41f47fafef8d 100644 --- a/mods/gen6/formats-data.js +++ b/mods/gen6/formats-data.js @@ -3682,7 +3682,7 @@ let BattleFormatsData = { }, whimsicott: { inherit: true, - randomBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast"], + randomBattleMoves: ["encore", "taunt", "leechseed", "uturn", "toxic", "stunspore", "memento", "tailwind", "moonblast"], randomDoubleBattleMoves: ["encore", "taunt", "substitute", "leechseed", "uturn", "helpinghand", "stunspore", "moonblast", "tailwind", "dazzlinggleam", "gigadrain", "protect"], tier: "UU", doublesTier: "DOU", @@ -4518,7 +4518,7 @@ let BattleFormatsData = { }, malamar: { inherit: true, - randomBattleMoves: ["superpower", "knockoff", "psychocut", "rockslide", "substitute", "trickroom"], + randomBattleMoves: ["superpower", "knockoff", "psychocut", "substitute", "trickroom"], randomDoubleBattleMoves: ["superpower", "psychocut", "rockslide", "trickroom", "knockoff", "protect"], tier: "NU", doublesTier: "DUU", From 0212424becf9236470a37368b735d093cebced47 Mon Sep 17 00:00:00 2001 From: Charlie Kobayashi Date: Wed, 19 Sep 2018 23:39:09 -0400 Subject: [PATCH 101/125] Scavengers: Incognito + filter improvements (#4824) --- chat-plugins/scavenger-games.js | 5 +++- chat-plugins/scavengers.js | 42 ++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/chat-plugins/scavenger-games.js b/chat-plugins/scavenger-games.js index 67f635b96ec9..9a34feb3169e 100644 --- a/chat-plugins/scavenger-games.js +++ b/chat-plugins/scavenger-games.js @@ -526,7 +526,10 @@ class Incognito extends ScavGame { value = toId(value); let player = hunt.players[user.userid]; - if (player.completed) return player.sendRoom(`That may or may not be the right answer - if you aren't confident, you can try again!`); + if (player.completed) { + if (!this.blind) return; + return player.sendRoom(`That may or may not be the right answer - if you aren't confident, you can try again!`); + } hunt.validatePlayer(player); diff --git a/chat-plugins/scavengers.js b/chat-plugins/scavengers.js index 4f36c9d3aa47..5349819fb3d0 100644 --- a/chat-plugins/scavengers.js +++ b/chat-plugins/scavengers.js @@ -522,7 +522,7 @@ class ScavengerHunt extends Rooms.RoomGame { } let uniqueConnections = this.getUniqueConnections(Users(player.userid)); - if (uniqueConnections > 1) { + if (uniqueConnections > 1 && this.room.scavmod && this.room.scavmod.ipcheck) { // multiple users on one alt player.sendRoom("You have been caught for attempting a hunt with multiple connections on your account. Staff has been notified."); @@ -1373,6 +1373,46 @@ let commands = { this.privateModAction(`(${user.name} has ${(change > 0 ? 'given' : 'taken')} one infraction point ${(change > 0 ? 'to' : 'from')} '${targetId}'.)`); this.modlog(`SCAV ${this.cmd.toUpperCase()}`, user); }, + + modsettings: { + '': 'update', + 'update': function (target, room, user) { + if (!this.can('declare', null, room) || room.id !== 'scavengers') return false; + let settings = room.scavmod || {}; + + this.sendReply(`|uhtml${this.cmd === 'update' ? 'change' : ''}|scav-modsettings|
Scavenger Moderation Settings:

` + + ` Multiple connection verification: ${settings.ipcheck ? 'ON' : 'OFF'}` + + `
`); + }, + + 'ipcheck': function (target, room, user) { + if (!this.can('declare', null, room) || room.id !== 'scavengers') return false; + + let settings = scavsRoom.scavmod || {}; + target = toId(target); + + let setting = { + 'on': true, + 'off': false, + 'toggle': !settings.ipcheck, + }; + + if (!(target in setting)) return this.sendReply('Invalid setting - ON, OFF, TOGGLE'); + + settings.ipcheck = setting[target]; + room.scavmod = settings; + + if (scavsRoom.chatRoomData) { + scavsRoom.chatRoomData.scavmod = scavsRoom.scavmod; + Rooms.global.writeChatRoomData(); + } + + this.privateModAction(`(${user.name} has set multiple connections verification to ${setting[target] ? 'ON' : 'OFF'}.)`); + this.modlog('SCAV MODSETTINGS IPCHECK', null, setting[target] ? 'ON' : 'OFF'); + + this.parse('/scav modsettings update'); + }, + }, }; exports.commands = { From 5dae4b1c3d490783fc728d018ce4866150852216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Thu, 20 Sep 2018 23:13:07 +0200 Subject: [PATCH 102/125] Chat monitor: Make /filter view more flexible --- chat-plugins/chat-monitor.js | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 67e1fdf7f107..2a962ffc209d 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -6,7 +6,7 @@ const MONITOR_FILE = 'config/chat-plugins/chat-monitor.tsv'; const WRITE_THROTTLE_TIME = 5 * 60 * 1000; /** @type {{[k: string]: string[]}} */ -let filterKeys = Chat.filterKeys = Object.assign(Chat.filterKeys, {publicwarn: ['PUBLIC', 'WARN'], warn: ['EVERYWHERE', 'WARN'], autolock: ['EVERYWHERE', 'AUTOLOCK'], namefilter: ['NAMES', 'WARN'], wordfilter: ['EVERYWHERE', 'FILTERTO']}); +let filterKeys = Chat.filterKeys = Object.assign(Chat.filterKeys, {publicwarn: ['PUBLIC', 'WARN', 'Filtered in public'], warn: ['EVERYWHERE', 'WARN', 'Filtered'], autolock: ['EVERYWHERE', 'AUTOLOCK', 'Autolock'], namefilter: ['NAMES', 'WARN', 'Filtered in usernames'], wordfilter: ['EVERYWHERE', 'FILTERTO', 'Filtered to a different phrase']}); /** @type {{[k: string]: ([(string | RegExp), string, string?, number][])}} */ let filterWords = Chat.filterWords; @@ -223,26 +223,21 @@ const pages = { return buf + `

Access denied

`; } let content = ``; - content += `

Filtered in public rooms [publicwarn]

`; - if (filterWords.publicwarn.length) { - content += filterWords.publicwarn.map(([str, reason, , hits]) => `${str}${hits}`).join(''); - } - content += `

Filtered [warn]

`; - if (filterWords.warn.length) { - content += filterWords.warn.map(([str, reason, , hits]) => `${str}${hits}`).join(''); - } - content += `

Weeklock [autolock]

`; - if (filterWords.autolock.length) { - content += filterWords.autolock.map(([str, reason, , hits]) => `${str}${hits}`).join(''); - } - content += `

Filtered in names [namefilter]

`; - if (filterWords.namefilter.length) { - content += filterWords.namefilter.map(([str, reason, , hits]) => `${str}${hits}`).join(''); - } - content += `

Filtered to different phrases [wordfilter]

`; - if (filterWords.wordfilter.length) { - content += filterWords.wordfilter.map(([str, reason, filterTo, hits]) => `${str} ⇒ ${filterTo}${hits}`).join(''); + for (const key in filterKeys) { + content += `

${filterKeys[key][2]} [${key}]

`; + if (filterWords[key].length) { + content += filterWords[key].map(([str, reason, filterTo, hits]) => { + let entry = ''; + if (filterTo) { + entry = `${str} ⇒ ${filterTo}`; + } else { + entry = `${str}`; + } + return `${entry}${hits}`; + }).join(''); + } } + if (Chat.namefilterwhitelist.size) { content += `

Whitelisted names

`; for (const [val] of Chat.namefilterwhitelist) { From 83be94a29096f03721a2743283883a0e91b3f6e3 Mon Sep 17 00:00:00 2001 From: whales Date: Fri, 21 Sep 2018 08:29:27 +0930 Subject: [PATCH 103/125] Mafia: Fix sub bug, update permissions (#4865) --- chat-plugins/mafia.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chat-plugins/mafia.js b/chat-plugins/mafia.js index 30d01c7ecc15..d87fe2c1ea01 100644 --- a/chat-plugins/mafia.js +++ b/chat-plugins/mafia.js @@ -977,6 +977,7 @@ class MafiaTracker extends Rooms.RoomGame { this.roleString = ''; } if (this.subs.includes(targetUser.userid)) this.subs.splice(this.subs.indexOf(targetUser.userid), 1); + this.played.push(targetUser.userid); this.players[targetUser.userid] = player; this.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added to the game by ${Chat.escapeHTML(user.name)}!`, {declare: true}); } @@ -1049,6 +1050,8 @@ class MafiaTracker extends Rooms.RoomGame { let lynch = this.lynches[oldPlayer.lynching]; lynch.lynchers.splice(lynch.lynchers.indexOf(oldPlayer.userid, 1)); lynch.lynchers.push(newPlayer.userid); + newPlayer.lynching = oldPlayer.lynching; + oldPlayer.lynching = ''; } this.players[newPlayer.userid] = newPlayer; this.players[oldPlayer.userid].destroy(); @@ -1860,7 +1863,8 @@ const commands = { if (room.id !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`); const args = target.split(',').map(toId); if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0])) { - if (['forceadd', 'add'].includes(args[0]) && !this.can('mute', null, room)) return; + const permission = (user.userid === args[1]) ? 'broadcast' : 'mute'; + if (['forceadd', 'add'].includes(args[0]) && !this.can(permission, null, room)) return; if (['remove', 'del', 'delete'].includes(args[0]) && user.userid !== args[1] && !this.can('mute', null, room)) return; } else { if (!this.runBroadcast()) return false; From 6024c4a1b16186a30d606ab4d82f3ac1e062e186 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Thu, 20 Sep 2018 21:37:23 -0400 Subject: [PATCH 104/125] Add new event Pokemon --- data/formats-data.js | 1 + data/learnsets.js | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/data/formats-data.js b/data/formats-data.js index b0aebe42e91e..0c5deab0275d 100644 --- a/data/formats-data.js +++ b/data/formats-data.js @@ -3733,6 +3733,7 @@ let BattleFormatsData = { {"generation": 5, "level": 45, "isHidden": true, "moves": ["irondefense", "agility", "hammerarm", "doubleedge"]}, {"generation": 5, "level": 45, "isHidden": true, "moves": ["psychic", "meteormash", "hammerarm", "doubleedge"]}, {"generation": 5, "level": 58, "nature": "Serious", "ivs": {"hp": 30, "atk": 30, "def": 30, "spa": 30, "spd": 30, "spe": 30}, "isHidden": false, "moves": ["earthquake", "hyperbeam", "psychic", "meteormash"], "pokeball": "cherishball"}, + {"generation": 7, "level": 50, "nature": "Jolly", "ivs": {"hp": 31, "atk": 31, "def": 31, "spa": 31, "spd": 31, "spe": 31}, "isHidden": false, "moves": ["ironhead", "icepunch", "bulletpunch", "stompingtantrum"], "pokeball": "cherishball"}, ], tier: "UU", doublesTier: "DUU", diff --git a/data/learnsets.js b/data/learnsets.js index fddde9b2bcc0..01fca4f9be9d 100644 --- a/data/learnsets.js +++ b/data/learnsets.js @@ -33966,7 +33966,7 @@ let BattleLearnsets = { bodyslam: ["3T"], brickbreak: ["7M", "6M", "5M", "4M", "3M"], bulldoze: ["7M", "6M", "5M"], - bulletpunch: ["7L26", "6L26", "5L32", "5S2", "5S1", "4L32", "4S0"], + bulletpunch: ["7L26", "7S7", "6L26", "5L32", "5S2", "5S1", "4L32", "4S0"], confide: ["7M", "6M"], confusion: ["7L1", "6L1", "5L1", "4L1", "3L1"], cut: ["6M", "5M", "4M", "3M"], @@ -33991,10 +33991,10 @@ let BattleLearnsets = { hiddenpower: ["7M", "6M", "5M", "4M", "3M"], honeclaws: ["6M", "5M"], hyperbeam: ["7M", "7L60", "6M", "6L60", "5M", "5L71", "5S6", "4M", "4L71", "3M", "3L77"], - icepunch: ["7T", "6T", "5T", "5S2", "4T", "3T"], + icepunch: ["7T", "7S7", "6T", "5T", "5S2", "4T", "3T"], icywind: ["7T", "6T", "5T", "4T", "3T"], irondefense: ["7T", "7L52", "6T", "6L52", "5T", "5L40", "5S4", "4T", "4L40", "3L44"], - ironhead: ["7T", "6T", "5T", "4T"], + ironhead: ["7T", "7S7", "6T", "5T", "4T"], laserfocus: ["7T"], lightscreen: ["7M", "6M", "5M", "4M", "3M"], magnetrise: ["7T", "7L1", "6T", "6L1", "5T", "5L1", "4T", "4L1"], @@ -34030,7 +34030,7 @@ let BattleLearnsets = { sludgebomb: ["7M", "6M", "5M", "4M", "3M"], snore: ["7T", "6T", "5T", "4T", "3T"], stealthrock: ["7T", "6T", "5T", "4M"], - stompingtantrum: ["7T"], + stompingtantrum: ["7T", "7S7"], strength: ["6M", "5M", "4M", "3M"], substitute: ["7M", "6M", "5M", "4M", "3T"], sunnyday: ["7M", "6M", "5M", "4M", "3M"], From 2a7ffcbc3273a5064fa75f5d0aa15308770a72fa Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Fri, 21 Sep 2018 16:01:53 -0400 Subject: [PATCH 105/125] Allow namechanges that don't change the userid anytime --- users.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/users.js b/users.js index 8e8f2dfab66e..3c57b6548391 100644 --- a/users.js +++ b/users.js @@ -721,7 +721,9 @@ class User { * @param {Connection} connection The connection asking for the rename */ async rename(name, token, newlyRegistered, connection) { + let userid = toId(name); for (const roomid of this.games) { + if (userid === this.userid) break; const game = Rooms(roomid).game; if (!game || game.ended) continue; // should never happen if (game.allowRenames || !this.named) continue; @@ -747,7 +749,6 @@ class User { return false; } - let userid = toId(name); if (userid.length > 18) { this.send(`|nametaken||Your name must be 18 characters or shorter.`); return false; From 41f404e17b5b6590e3de1a69c2106f97a7b81dcc Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Thu, 20 Sep 2018 16:21:27 -0500 Subject: [PATCH 106/125] Fix minor unused variable --- users.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/users.js b/users.js index 3c57b6548391..8a200412bbed 100644 --- a/users.js +++ b/users.js @@ -604,7 +604,8 @@ class User { return true; } - let group = ' '; + /** @type {string} */ + let group; let targetGroup = ''; let targetUser = null; From 60075830b694679332a3a92ae3118ceabb001957 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Thu, 20 Sep 2018 20:37:52 -0500 Subject: [PATCH 107/125] Implement Illusion Level Mod Normally, Illusion copies everything except the level, so if Zoroark is a different level from the pokemon it's copying, it's very obvious that it's a copy. This isn't a problem normally (because everyone is 50 or 100), but in randbats, it's a big tell, and makes Zoroark weaker than it's supposed to be. And, unrelatedly, everyone thinks it's a bug even though it's not. --- config/formats.js | 16 ++++++++-------- data/rulesets.js | 9 +++++++++ sim/battle.js | 1 + sim/pokemon.js | 1 + 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/config/formats.js b/config/formats.js index 0ab7bdad51a7..5d9c63eb4316 100644 --- a/config/formats.js +++ b/config/formats.js @@ -21,7 +21,7 @@ let Formats = [ mod: 'gen7', team: 'random', - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 7] Unrated Random Battle", @@ -30,7 +30,7 @@ let Formats = [ team: 'random', challengeShow: false, rated: false, - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 7] OU", @@ -245,7 +245,7 @@ let Formats = [ mod: 'gen7', gameType: 'doubles', team: 'random', - ruleset: ['PotD', 'Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 7] Doubles OU", @@ -874,7 +874,7 @@ let Formats = [ mod: 'gen7', team: 'random', searchShow: false, - ruleset: ['Pokemon', 'Same Type Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Pokemon', 'Same Type Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 7] Challenge Cup 1v1", @@ -904,7 +904,7 @@ let Formats = [ mod: 'gen7', team: 'randomHC', - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 7] Doubles Hackmons Cup", @@ -913,14 +913,14 @@ let Formats = [ gameType: 'doubles', team: 'randomHC', searchShow: false, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 6] Random Battle", mod: 'gen6', team: 'random', - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 6] Battle Factory", @@ -936,7 +936,7 @@ let Formats = [ mod: 'gen5', team: 'random', - ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], + ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], }, { name: "[Gen 4] Random Battle", diff --git a/data/rulesets.js b/data/rulesets.js index ab4552523cc7..5733cfec1ea8 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -724,6 +724,15 @@ let BattleFormats = { this.add('rule', 'Switch Priority Clause Mod: Faster Pokémon switch first'); }, }, + illusionlevelmod: { + effectType: 'Rule', + name: 'Illusion Level Mod', + desc: "Makes Illusion also copy levels - normally, Illusion copies everything except levels, which isn't a problem in tiers where everyone is level 100 or 50, but is a problem in Randbats because the level balancing makes Zoroark much weaker than intended", + onStart: function () { + this.add('rule', 'Illusion Level Mod: Illusion also fakes levels'); + this.illusionCopiesLevels = true; + }, + }, freezeclausemod: { effectType: 'Rule', name: 'Freeze Clause Mod', diff --git a/sim/battle.js b/sim/battle.js index 5ea8c1bdf8ab..8c7622f7326d 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -120,6 +120,7 @@ class Battle extends Dex.ModdedDex { this.currentRequest = ''; this.lastMoveLine = 0; this.reportPercentages = false; + this.illusionCopiesLevels = false; this.supportCancel = false; this.events = null; diff --git a/sim/pokemon.js b/sim/pokemon.js index 69c36e9ef9d4..33e4b88b6f4f 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -331,6 +331,7 @@ class Pokemon { */ getDetailsInner(side) { if (this.illusion) { + if (this.battle.illusionCopiesLevels) return this.illusion.details + '|' + this.getHealthInner(side); let illusionDetails = this.illusion.species + (this.level === 100 ? '' : ', L' + this.level) + (this.illusion.gender === '' ? '' : ', ' + this.illusion.gender) + (this.illusion.set.shiny ? ', shiny' : ''); return illusionDetails + '|' + this.getHealthInner(side); } From 79152f3a7cd5dd7d3658edee2d1aa8222148360e Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Fri, 21 Sep 2018 18:48:57 -0400 Subject: [PATCH 108/125] Fix bugs in /analysis --- chat-plugins/info.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 487ad49ef69f..df071948a6ef 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -1873,9 +1873,11 @@ const commands = { if (speciesid === 'meowstic') speciesid = 'meowsticm'; if (pokemon.tier === 'CAP') { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis preview, brought to you by Smogon University CAP Project`); - } else if (formatId === 'ou' && generation === 'sm' && room.language in supportedLanguages) { + } else if (formatId === 'ou' && generation === 'sm' && room && room.language in supportedLanguages) { // Limited support for translated analysis - this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); + // Translated analysis do not support automatic redirects from a speciesid to the proper page + let pageid = pokemon.name.toLowerCase().replace(' ', '_'); + this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } else { this.sendReplyBox(`${generation.toUpperCase()} ${Chat.escapeHTML(formatName)} ${pokemon.name} analysis, brought to you by Smogon University`); } From 7432d3fc8f2e74304adee608331baca15d156039 Mon Sep 17 00:00:00 2001 From: whales Date: Sun, 23 Sep 2018 06:20:49 +0930 Subject: [PATCH 109/125] Fix set gen/format /learn (#4868) --- chat-plugins/datasearch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chat-plugins/datasearch.js b/chat-plugins/datasearch.js index 2fd83f11a705..2fd2da8c0c1c 100644 --- a/chat-plugins/datasearch.js +++ b/chat-plugins/datasearch.js @@ -1425,6 +1425,7 @@ function runLearn(target, cmd) { formatid = targetid; formatName = format.name; targets.shift(); + continue; } if (targetid.startsWith('gen') && parseInt(targetid.charAt(3))) { gen = parseInt(targetid.slice(3)); @@ -1442,8 +1443,8 @@ function runLearn(target, cmd) { break; } if (!formatName) { - format = new Dex.Data.Format(format); - formatName = 'Gen ' + gen; + format = new Dex.Data.Format(format, {mod: `gen${gen}`}); + formatName = `Gen ${gen}`; if (format.requirePentagon) formatName += ' Pentagon'; } let lsetData = {set: {}, sources: [], sourcesBefore: gen}; From 49c1e3f6e33931383868ef7fa2d1efc8dd179d61 Mon Sep 17 00:00:00 2001 From: Quinton Lee Date: Sat, 22 Sep 2018 19:26:31 -0500 Subject: [PATCH 110/125] Add /notifyrank (#4858) --- chat-commands.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/chat-commands.js b/chat-commands.js index 47cccb8ec3b6..12c7224c567d 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -2356,6 +2356,32 @@ const commands = { }, announcehelp: [`/announce OR /wall [message] - Makes an announcement. Requires: % @ * # & ~`], + notifyoffrank: 'notifyrank', + notifyrank: function (target, room, user, connection, cmd) { + if (!target) return this.parse(`/help notifyrank`); + if (!this.can('addhtml', null, room)) return false; + if (!this.canTalk()) return; + let [rank, notification] = this.splitOne(target); + if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`); + const id = `${room.id}-rank-${Config.groups[rank].id}`; + if (cmd === 'notifyoffrank') { + room.sendRankedUsers(`|tempnotifyoff|${id}`, rank); + } else { + let [title, message] = this.splitOne(notification); + if (!title) title = `${room.title} ${Config.groups[rank].name}+ message!`; + if (!user.can('addhtml')) { + title += ` (notification from ${user.name})`; + } + if (message.length > 300) return this.errorReply(`Notifications should not exceed 300 characters.`); + room.sendRankedUsers(`|tempnotify|${id}|${title}|${message}`, rank); + this.modlog(`NOTIFYRANK`, null, target); + } + }, + notifyrankhelp: [ + `/notifyrank [rank], [title], [message] - Sends a notification to everyone with the specified rank or higher. Requires: # * & ~`, + `/notifyoffrank [rank] - Closes the notification previously sent with /notifyrank [rank]. Requires: # * & ~`, + ], + fr: 'forcerename', forcerename: function (target, room, user) { if (!target) return this.parse('/help forcerename'); From 5ef985710145c612cd558e1105e560ad5b29b54b Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 24 Sep 2018 07:07:37 +0400 Subject: [PATCH 111/125] Random Battle: Fix Arceus --- data/random-teams.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/random-teams.js b/data/random-teams.js index e19cbfcc12e0..7244301b5460 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -1318,7 +1318,7 @@ class RandomTeams extends Dex.ModdedDex { item = !isDoubles ? 'Leftovers' : 'Sitrus Berry'; if (template.requiredItems) { - if (template.baseSpecies === 'Arceus' && hasMove['judgment']) { + if (template.baseSpecies === 'Arceus' && (hasMove['judgment'] || !counter[template.types[0]])) { // Judgment doesn't change type with Z-Crystals item = template.requiredItems[0]; } else { From 48e0e8755dbde1c1db8290b83f19af03c5ef0bdd Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Mon, 24 Sep 2018 03:13:54 -0700 Subject: [PATCH 112/125] Random Battle: Fix Torterra (#4870) --- data/random-teams.js | 2 ++ mods/gen6/random-teams.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/data/random-teams.js b/data/random-teams.js index 7244301b5460..369771c171b5 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -1307,6 +1307,8 @@ class RandomTeams extends Dex.ModdedDex { ability = 'Klutz'; } else if ((template.species === 'Rampardos' && !hasMove['headsmash']) || hasMove['rockclimb']) { ability = 'Sheer Force'; + } else if (template.species === 'Torterra' && !counter['Grass']) { + ability = 'Shell Armor'; } else if (template.species === 'Umbreon') { ability = 'Synchronize'; } else if (template.id === 'venusaurmega') { diff --git a/mods/gen6/random-teams.js b/mods/gen6/random-teams.js index 90a63b3bece7..70116ae70b86 100644 --- a/mods/gen6/random-teams.js +++ b/mods/gen6/random-teams.js @@ -700,6 +700,8 @@ class RandomGen6Teams extends RandomTeams { ability = 'Klutz'; } else if ((template.species === 'Rampardos' && !hasMove['headsmash']) || hasMove['rockclimb']) { ability = 'Sheer Force'; + } else if (template.species === 'Torterra' && !counter['Grass']) { + ability = 'Shell Armor'; } else if (template.species === 'Umbreon') { ability = 'Synchronize'; } else if (template.id === 'venusaurmega') { From 5df0a02fcbdf237f84c7f615ec2e69d4b778d0ea Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 24 Sep 2018 15:54:33 +0400 Subject: [PATCH 113/125] Revert "Implement Illusion Level Mod" This reverts commit 60075830b694679332a3a92ae3118ceabb001957. --- config/formats.js | 16 ++++++++-------- data/rulesets.js | 9 --------- sim/battle.js | 1 - sim/pokemon.js | 1 - 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/config/formats.js b/config/formats.js index 5d9c63eb4316..0ab7bdad51a7 100644 --- a/config/formats.js +++ b/config/formats.js @@ -21,7 +21,7 @@ let Formats = [ mod: 'gen7', team: 'random', - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Unrated Random Battle", @@ -30,7 +30,7 @@ let Formats = [ team: 'random', challengeShow: false, rated: false, - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] OU", @@ -245,7 +245,7 @@ let Formats = [ mod: 'gen7', gameType: 'doubles', team: 'random', - ruleset: ['PotD', 'Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['PotD', 'Pokemon', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Doubles OU", @@ -874,7 +874,7 @@ let Formats = [ mod: 'gen7', team: 'random', searchShow: false, - ruleset: ['Pokemon', 'Same Type Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['Pokemon', 'Same Type Clause', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Challenge Cup 1v1", @@ -904,7 +904,7 @@ let Formats = [ mod: 'gen7', team: 'randomHC', - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 7] Doubles Hackmons Cup", @@ -913,14 +913,14 @@ let Formats = [ gameType: 'doubles', team: 'randomHC', searchShow: false, - ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['Pokemon', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 6] Random Battle", mod: 'gen6', team: 'random', - ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['PotD', 'Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 6] Battle Factory", @@ -936,7 +936,7 @@ let Formats = [ mod: 'gen5', team: 'random', - ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod', 'Illusion Level Mod'], + ruleset: ['Pokemon', 'Sleep Clause Mod', 'HP Percentage Mod', 'Cancel Mod'], }, { name: "[Gen 4] Random Battle", diff --git a/data/rulesets.js b/data/rulesets.js index 5733cfec1ea8..ab4552523cc7 100644 --- a/data/rulesets.js +++ b/data/rulesets.js @@ -724,15 +724,6 @@ let BattleFormats = { this.add('rule', 'Switch Priority Clause Mod: Faster Pokémon switch first'); }, }, - illusionlevelmod: { - effectType: 'Rule', - name: 'Illusion Level Mod', - desc: "Makes Illusion also copy levels - normally, Illusion copies everything except levels, which isn't a problem in tiers where everyone is level 100 or 50, but is a problem in Randbats because the level balancing makes Zoroark much weaker than intended", - onStart: function () { - this.add('rule', 'Illusion Level Mod: Illusion also fakes levels'); - this.illusionCopiesLevels = true; - }, - }, freezeclausemod: { effectType: 'Rule', name: 'Freeze Clause Mod', diff --git a/sim/battle.js b/sim/battle.js index 8c7622f7326d..5ea8c1bdf8ab 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -120,7 +120,6 @@ class Battle extends Dex.ModdedDex { this.currentRequest = ''; this.lastMoveLine = 0; this.reportPercentages = false; - this.illusionCopiesLevels = false; this.supportCancel = false; this.events = null; diff --git a/sim/pokemon.js b/sim/pokemon.js index 33e4b88b6f4f..69c36e9ef9d4 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -331,7 +331,6 @@ class Pokemon { */ getDetailsInner(side) { if (this.illusion) { - if (this.battle.illusionCopiesLevels) return this.illusion.details + '|' + this.getHealthInner(side); let illusionDetails = this.illusion.species + (this.level === 100 ? '' : ', L' + this.level) + (this.illusion.gender === '' ? '' : ', ' + this.illusion.gender) + (this.illusion.set.shiny ? ', shiny' : ''); return illusionDetails + '|' + this.getHealthInner(side); } From d80c813118b95740745a5512e902cd619d06b073 Mon Sep 17 00:00:00 2001 From: The Immortal Date: Mon, 24 Sep 2018 17:10:17 +0400 Subject: [PATCH 114/125] Update Random Battle --- data/random-teams.js | 14 ++++++++++++-- dev-tools/globals.ts | 1 + mods/gen5/random-teams.js | 19 +++++++++++++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/data/random-teams.js b/data/random-teams.js index 369771c171b5..fa24332a5e48 100644 --- a/data/random-teams.js +++ b/data/random-teams.js @@ -1644,8 +1644,8 @@ class RandomTeams extends Dex.ModdedDex { randomTeam() { let pokemon = []; - let excludedTiers = ['NFE', 'LC Uber', 'LC']; - let allowedNFE = ['Chansey', 'Doublade', 'Gligar', 'Porygon2', 'Scyther', 'Togetic']; + const excludedTiers = ['NFE', 'LC Uber', 'LC']; + const allowedNFE = ['Chansey', 'Doublade', 'Gligar', 'Porygon2', 'Scyther', 'Togetic']; // For Monotype let isMonotype = this.format.id === 'gen7monotyperandombattle'; @@ -1774,6 +1774,13 @@ class RandomTeams extends Dex.ModdedDex { // Okay, the set passes, add it to our team pokemon.push(set); + if (pokemon.length === 6) { + // Set Zoroark's level to be the same as the last Pokemon + let illusion = teamDetails['illusion']; + if (illusion) pokemon[illusion - 1].level = pokemon[5].level; + break; + } + // Now that our Pokemon has passed all checks, we can increment our counters baseFormes[template.baseSpecies] = 1; @@ -1809,6 +1816,9 @@ class RandomTeams extends Dex.ModdedDex { if (set.moves.includes('stealthrock')) teamDetails['stealthRock'] = 1; if (set.moves.includes('toxicspikes')) teamDetails['toxicSpikes'] = 1; if (set.moves.includes('defog') || set.moves.includes('rapidspin')) teamDetails['hazardClear'] = 1; + + // For setting Zoroark's level + if (set.ability === 'Illusion') teamDetails['illusion'] = pokemon.length; } return pokemon; } diff --git a/dev-tools/globals.ts b/dev-tools/globals.ts index f65612c52bc6..f305361e5331 100644 --- a/dev-tools/globals.ts +++ b/dev-tools/globals.ts @@ -874,6 +874,7 @@ interface RandomTeamsTypes { toxicSpikes?: number hazardClear?: number rapidSpin?: number + illusion?: number } FactoryTeamDetails: { megaCount: number diff --git a/mods/gen5/random-teams.js b/mods/gen5/random-teams.js index 5e961422c287..1e5ac09cceff 100644 --- a/mods/gen5/random-teams.js +++ b/mods/gen5/random-teams.js @@ -652,7 +652,7 @@ class RandomGen5Teams extends RandomGen6Teams { randomTeam() { let pokemon = []; - let allowedNFE = ['Porygon2', 'Scyther']; + const allowedNFE = ['Porygon2', 'Scyther']; let pokemonPool = []; for (let id in this.data.FormatsData) { @@ -706,6 +706,8 @@ class RandomGen5Teams extends RandomGen6Teams { break; } + let types = template.types; + // Limit 2 of any type let skip = false; for (const type of template.types) { @@ -718,6 +720,9 @@ class RandomGen5Teams extends RandomGen6Teams { let set = this.randomSet(template, pokemon.length, teamDetails); + // Illusion shouldn't be the last Pokemon of the team + if (set.ability === 'Illusion' && pokemon.length > 4) continue; + // Limit 1 of any type combination let typeCombo = template.types.slice().sort().join(); if (set.ability === 'Drought' || set.ability === 'Drizzle' || set.ability === 'Sand Stream') { @@ -731,11 +736,18 @@ class RandomGen5Teams extends RandomGen6Teams { // Okay, the set passes, add it to our team pokemon.push(set); + if (pokemon.length === 6) { + // Set Zoroark's level to be the same as the last Pokemon + let illusion = teamDetails['illusion']; + if (illusion) pokemon[illusion - 1].level = pokemon[5].level; + break; + } + // Now that our Pokemon has passed all checks, we can increment our counters baseFormes[template.baseSpecies] = 1; // Increment type counters - for (const type of template.types) { + for (const type of types) { if (type in typeCount) { typeCount[type]++; } else { @@ -762,6 +774,9 @@ class RandomGen5Teams extends RandomGen6Teams { if (set.moves.includes('stealthrock')) teamDetails['stealthRock'] = 1; if (set.moves.includes('toxicspikes')) teamDetails['toxicSpikes'] = 1; if (set.moves.includes('rapidspin')) teamDetails['rapidSpin'] = 1; + + // For setting Zoroark's level + if (set.ability === 'Illusion') teamDetails['illusion'] = pokemon.length; } return pokemon; } From 8e4f83778f1f0c995e0122e54f6c88057262d979 Mon Sep 17 00:00:00 2001 From: Marty-D Date: Mon, 24 Sep 2018 13:56:59 -0400 Subject: [PATCH 115/125] Gen VI-VII: Update breaking protection --- data/moves.js | 16 ++++++++-------- data/scripts.js | 1 + mods/gen6/moves.js | 14 +++++++------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/data/moves.js b/data/moves.js index fd371a9d840e..17af6a0d1c99 100644 --- a/data/moves.js +++ b/data/moves.js @@ -995,7 +995,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user become poisoned. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user become poisoned. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Protects from moves. Contact: poison.", id: "banefulbunker", isViable: true, @@ -3274,7 +3274,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Prevents moves from affecting the user this turn.", id: "detect", isViable: true, @@ -4483,7 +4483,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "User survives attacks this turn with at least 1 HP.", id: "endure", name: "Endure", @@ -8831,7 +8831,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Protects from attacks. Contact: lowers Atk by 2.", id: "kingsshield", isViable: true, @@ -12315,7 +12315,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Prevents moves from affecting the user this turn.", id: "protect", isViable: true, @@ -12834,7 +12834,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", shortDesc: "Protects allies from priority attacks this turn.", id: "quickguard", name: "Quick Guard", @@ -15502,7 +15502,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", shortDesc: "Protects from moves. Contact: loses 1/8 max HP.", id: "spikyshield", isViable: true, @@ -18681,7 +18681,7 @@ let BattleMovedex = { accuracy: true, basePower: 0, category: "Status", - desc: "The user and its party members are protected from moves made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from moves made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Baneful Bunker, Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", shortDesc: "Protects allies from multi-target moves this turn.", id: "wideguard", name: "Wide Guard", diff --git a/data/scripts.js b/data/scripts.js index 99c45f925954..0fd8b929798a 100644 --- a/data/scripts.js +++ b/data/scripts.js @@ -432,6 +432,7 @@ let BattleScripts = { } else { this.add('-activate', target, 'move: ' + move.name, '[broken]'); } + if (this.gen >= 6) delete target.volatiles['stall']; } } diff --git a/mods/gen6/moves.js b/mods/gen6/moves.js index 0d299ad700d2..3544ca9c0113 100644 --- a/mods/gen6/moves.js +++ b/mods/gen6/moves.js @@ -56,7 +56,7 @@ let BattleMovedex = { }, detect: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, diamondstorm: { inherit: true, @@ -117,7 +117,7 @@ let BattleMovedex = { }, endure: { inherit: true, - desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user will survive attacks made by other Pokemon during this turn with at least 1 HP. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, entrainment: { inherit: true, @@ -170,7 +170,7 @@ let BattleMovedex = { }, kingsshield: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon trying to make contact with the user have their Attack lowered by 2 stages. Non-damaging moves go through this protection. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, knockoff: { inherit: true, @@ -314,7 +314,7 @@ let BattleMovedex = { }, protect: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, pursuit: { inherit: true, @@ -322,7 +322,7 @@ let BattleMovedex = { }, quickguard: { inherit: true, - desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from attacks with original or altered priority greater than 0 made by other Pokemon, including allies, during this turn. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", }, ragepowder: { inherit: true, @@ -380,7 +380,7 @@ let BattleMovedex = { }, spikyshield: { inherit: true, - desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn.", + desc: "The user is protected from most attacks made by other Pokemon during this turn, and Pokemon making contact with the user lose 1/8 of their maximum HP, rounded down. This move has a 1/X chance of being successful, where X starts at 1 and triples each time this move is successfully used. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn.", }, struggle: { inherit: true, @@ -449,7 +449,7 @@ let BattleMovedex = { }, wideguard: { inherit: true, - desc: "The user and its party members are protected from damaging attacks made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails or if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard. Fails if the user moves last this turn or if this move is already in effect for the user's side.", + desc: "The user and its party members are protected from damaging attacks made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This move modifies the same 1/X chance of being successful used by other protection moves, where X starts at 1 and triples each time this move is successfully used, but does not use the chance to check for failure. X resets to 1 if this move fails, if the user's last move used is not Detect, Endure, King's Shield, Protect, Quick Guard, Spiky Shield, or Wide Guard, or if it was one of those moves and the user's protection was broken. Fails if the user moves last this turn or if this move is already in effect for the user's side.", shortDesc: "Protects allies from multi-target hits this turn.", effect: { duration: 1, From f77d8d6baa1cf4d96ec9682ccc4c40c377cc55c6 Mon Sep 17 00:00:00 2001 From: whales Date: Tue, 25 Sep 2018 06:08:11 +0930 Subject: [PATCH 116/125] Fix a constructor bug with room languages (#4871) --- chat-plugins/roomsettings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chat-plugins/roomsettings.js b/chat-plugins/roomsettings.js index 6042fc7d745c..45c35dcd9f20 100644 --- a/chat-plugins/roomsettings.js +++ b/chat-plugins/roomsettings.js @@ -363,6 +363,7 @@ exports.commands = { roomlanguage: function (target, room, user) { const languageTable = { + __proto__: null, portuguese: 'Portuguese', spanish: 'Spanish', italian: 'Italian', From df1b799d2e1a231c7d8bead36ec79b4083af5faa Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Mon, 24 Sep 2018 17:07:44 -0400 Subject: [PATCH 117/125] Slowchat: Don't announce when the setting changes --- chat-plugins/roomsettings.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/chat-plugins/roomsettings.js b/chat-plugins/roomsettings.js index 45c35dcd9f20..ef1c5f1dac27 100644 --- a/chat-plugins/roomsettings.js +++ b/chat-plugins/roomsettings.js @@ -410,14 +410,12 @@ exports.commands = { if (this.meansNo(target)) { if (!room.slowchat) return this.errorReply(`Slow chat is already disabled in this room.`); room.slowchat = false; - this.add("|raw|
Slow chat was disabled!
There is no longer a set minimum time between messages.
"); } else if (targetInt) { if (!user.can('bypassall') && room.userCount < SLOWCHAT_USER_REQUIREMENT) return this.errorReply(`This room must have at least ${SLOWCHAT_USER_REQUIREMENT} users to set slowchat; it only has ${room.userCount} right now.`); if (room.slowchat === targetInt) return this.errorReply(`Slow chat is already set to ${room.slowchat} seconds in this room.`); if (targetInt < SLOWCHAT_MINIMUM) targetInt = SLOWCHAT_MINIMUM; if (targetInt > SLOWCHAT_MAXIMUM) targetInt = SLOWCHAT_MAXIMUM; room.slowchat = targetInt; - this.add(`|raw|
Slow chat was enabled!
Messages must have at least ${room.slowchat} seconds between them.
`); } else { return this.parse("/help slowchat"); } From 55900355233aeb44db4358f3518262f07c6d674d Mon Sep 17 00:00:00 2001 From: HoeenHero Date: Mon, 24 Sep 2018 17:12:17 -0400 Subject: [PATCH 118/125] Rules: Update grammar for Hindi --- chat-plugins/info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index df071948a6ef..2af689b8bdd5 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -1690,7 +1690,7 @@ const commands = { simplifiedchinese: ['请遵守规则:', 'pages/rules-zh', '全站规则', room ? `${room.title}房间规则` : ``], traditionalchinese: ['請遵守規則:', 'pages/rules-tw', '全站規則', room ? `${room.title}房間規則` : ``], japanese: ['ルールを守ってください:', 'pages/rules-ja', '全部屋共通ルール', room ? `${room.title}部屋のルール` : ``], - hindi: ['कृपया इन नियमों का पालन करें:', 'pages/rules-hi', 'आप सभी के लिए नियम:', room ? `${room.title} इस Room के नियम:` : ``], + hindi: ['कृपया इन नियमों का पालन करें:', 'pages/rules-hi', 'सामान्य नियम', room ? `${room.title} Room के नियम` : ``], turkish: ['Lütfen kurallara uyun:', 'pages/rules-tr', 'Genel kurallar', room ? `${room.title} odası kuralları` : ``], dutch: ['Volg de regels:', 'pages/rules-nl', 'Globale Regels ', room ? `Regels van de ${room.title} room` : ``], german: ['Bitte befolgt die Regeln:', 'pages/rules-de', 'Globale Regeln', room ? `Regeln des ${room.title} Raumes` : ``], From bd969afb66f05f71d58d3e0ee61ff543f478706c Mon Sep 17 00:00:00 2001 From: Jeremy Piemonte Date: Mon, 24 Sep 2018 22:40:34 -0400 Subject: [PATCH 119/125] Chat monitor: Add some aliases (#4872) And change spacing of help command to match the rest --- chat-plugins/chat-monitor.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/chat-plugins/chat-monitor.js b/chat-plugins/chat-monitor.js index 2a962ffc209d..c1b247e96d26 100644 --- a/chat-plugins/chat-monitor.js +++ b/chat-plugins/chat-monitor.js @@ -324,14 +324,20 @@ let commands = { return this.sendReply(`'${words.join(', ')}' ${Chat.plural(words, "were", "was")} removed from the ${list} list.`); } }, + '': 'view', list: 'view', view: function (target, room, user) { this.parse(`/join view-filters`); }, + help: function (target, room, user) { + this.parse(`/help filter`); + }, }, - filterhelp: [`- /filter add list, word, reason - Adds a word to the given filter list. Requires: ~`, + filterhelp: [ + `- /filter add list, word, reason - Adds a word to the given filter list. Requires: ~`, `- /filter remove list, words - Removes words from the given filter list. Requires: ~`, - `- /filter view - Opens the list of filtered words. Requires: % @ * & ~`], + `- /filter view - Opens the list of filtered words. Requires: % @ * & ~`, + ], allowname: function (target, room, user) { if (!this.can('forcerename')) return false; target = toId(target); From 63ad3559f47d010cc1431743cf4858197564afbf Mon Sep 17 00:00:00 2001 From: asgdf Date: Tue, 25 Sep 2018 10:24:36 +0200 Subject: [PATCH 120/125] Modlog: Don't include main account in list of alts (#4873) --- chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat.js b/chat.js index 738b952ef63e..0233d2cc7a1d 100644 --- a/chat.js +++ b/chat.js @@ -645,7 +645,7 @@ class CommandContext { let userid = user.getLastId(); buf += `[${userid}]`; if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; - const alts = user.getAltUsers(false, true).map(user => user.getLastId()).join('], ['); + const alts = user.getAltUsers(false, true).slice(1).map(user => user.getLastId()).join('], ['); if (alts.length) buf += ` alts:[${alts}]`; buf += ` [${user.latestIp}]`; } @@ -671,7 +671,7 @@ class CommandContext { buf += `[${userid}]`; if (!options.noalts) { if (user.autoconfirmed && user.autoconfirmed !== userid) buf += ` ac:[${user.autoconfirmed}]`; - const alts = user.getAltUsers(false, true).map(user => user.getLastId()).join('], ['); + const alts = user.getAltUsers(false, true).slice(1).map(user => user.getLastId()).join('], ['); if (alts.length) buf += ` alts:[${alts}]`; } if (!options.noip) buf += ` [${user.latestIp}]`; From 2accf99e7fb2caa3f0199b2e7fc0c722eea54e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Tue, 25 Sep 2018 23:19:16 +0200 Subject: [PATCH 121/125] Introduce /unlockname and redesign /unlockip (#4869) - Allow unlocking specific names with /unlockname - Expand /unlockip to work for mods - Clarify which names are locked on /ip --- chat-commands.js | 60 ++++++++++++++++++++++++++++++++++---------- chat-plugins/info.js | 14 ++++++++--- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/chat-commands.js b/chat-commands.js index 12c7224c567d..0521e544b614 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -1902,6 +1902,53 @@ const commands = { this.errorReply(`User '${target}' is not locked.`); } }, + unlockname: function (target, room, user) { + if (!target) return this.parse('/help unlock'); + if (!this.can('lock')) return false; + + const userid = toId(target); + const punishment = Punishments.userids.get(userid); + if (!punishment) return this.errorReply("This name isn't locked."); + if (punishment[1] === userid) return this.errorReply(`"${userid}" was specifically locked by a staff member (check the global modlog). Use /unlock if you really want to unlock this name.`); + + Punishments.userids.delete(userid); + Punishments.savePunishments(); + + for (const curUser of Users.findUsers([userid], [])) { + if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) { + curUser.locked = false; + curUser.namelocked = false; + curUser.updateIdentity(); + } + } + this.globalModlog("UNLOCKNAME", userid, ` by ${user.name}`); + this.addModAction(`The name '${target}' was unlocked by ${user.name}.`); + }, + unlockip: function (target, room, user) { + target = target.trim(); + if (!target) return this.parse('/help unlock'); + if (!this.can('ban')) return false; + const range = target.charAt(target.length - 1) === '*'; + if (range && !this.can('rangeban')) return false; + + if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address."); + + const punishment = Punishments.ips.get(target); + if (!punishment) return this.errorReply(`${target} is not a locked/banned IP or IP range.`); + + Punishments.ips.delete(target); + Punishments.savePunishments(); + + for (const curUser of Users.findUsers([], [target])) { + if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) { + curUser.locked = false; + curUser.namelocked = false; + curUser.updateIdentity(); + } + } + this.globalModlog(`UNLOCK${range ? 'RANGE' : 'IP'}`, target, ` by ${user.name}`); + this.addModAction(`${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`); + }, unlockhelp: [`/unlock [username] - Unlocks the user. Requires: % @ * & ~`], forceglobalban: 'globalban', @@ -2123,19 +2170,6 @@ const commands = { unrangelock: 'unlockip', rangeunlock: 'unlockip', - unlockip: function (target, room, user) { - target = target.trim(); - if (!target) { - return this.parse('/help unbanip'); - } - if (!this.can('rangeban')) return false; - if (!Punishments.ips.has(target)) { - return this.errorReply(`${target} is not a locked/banned IP or IP range.`); - } - Punishments.ips.delete(target); - this.addModAction(`${user.name} unlocked the ${(target.charAt(target.length - 1) === '*' ? "IP range" : "IP")}: ${target}`); - this.modlog('UNRANGELOCK', null, target); - }, /********************************************************* * Moderating: Other diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 2af689b8bdd5..1a15cc5b1ea8 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -84,16 +84,24 @@ const commands = { } buf += '
'; if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) { - let prevNames = Object.keys(targetUser.prevNames).join(", "); + let prevNames = Object.keys(targetUser.prevNames).map(userid => { + const punishment = Punishments.userids.get(userid); + return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.userid ? ` as ${punishment[1]}` : ''})` : ''); + }).join(", "); if (prevNames) buf += Chat.html`
Previous names: ${prevNames}`; for (const targetAlt of targetUser.getAltUsers(true)) { if (!targetAlt.named && !targetAlt.connected) continue; if (targetAlt.group === '~' && user.group !== '~') continue; - buf += Chat.html`
Alt: ${targetAlt.name}`; + const punishment = Punishments.userids.get(targetAlt.userid); + const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''; + buf += Chat.html`
Alt: ${targetAlt.name}${punishMsg}`; if (!targetAlt.connected) buf += ` (offline)`; - prevNames = Object.keys(targetAlt.prevNames).join(", "); + prevNames = Object.keys(targetAlt.prevNames).map(userid => { + const punishment = Punishments.userids.get(userid); + return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''); + }).join(", "); if (prevNames) buf += `
Previous names: ${prevNames}`; } if (targetUser.namelocked) { From 0ffdde58f12dbf526099ed37bfd6e5c11ae141da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A4r=20Halberkamp?= Date: Wed, 26 Sep 2018 01:06:43 +0200 Subject: [PATCH 122/125] Add help for the new /unlock commands --- chat-commands.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chat-commands.js b/chat-commands.js index 0521e544b614..79b44141af30 100644 --- a/chat-commands.js +++ b/chat-commands.js @@ -1949,7 +1949,11 @@ const commands = { this.globalModlog(`UNLOCK${range ? 'RANGE' : 'IP'}`, target, ` by ${user.name}`); this.addModAction(`${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`); }, - unlockhelp: [`/unlock [username] - Unlocks the user. Requires: % @ * & ~`], + unlockhelp: [ + `/unlock [username] - Unlocks the user. Requires: % @ * & ~`, + `/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ * & ~`, + `/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ * & ~`, + ], forceglobalban: 'globalban', gban: 'globalban', From 868db27e94498f389a7a626aa1a1bd2d22216cd7 Mon Sep 17 00:00:00 2001 From: Kris Johnson <11083252+KrisXV@users.noreply.github.com> Date: Thu, 27 Sep 2018 23:54:47 -0600 Subject: [PATCH 123/125] BW OU: Ban Arena Trap, Unban Dugtrio (#4874) --- config/formats.js | 2 +- mods/gen5/formats-data.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/formats.js b/config/formats.js index 0ab7bdad51a7..06ca50e67b0a 100644 --- a/config/formats.js +++ b/config/formats.js @@ -1069,7 +1069,7 @@ let Formats = [ mod: 'gen5', ruleset: ['Pokemon', 'Standard', 'Evasion Abilities Clause', 'Baton Pass Clause', 'Swagger Clause', 'Team Preview'], - banlist: ['Uber', 'Drizzle ++ Swift Swim', 'Drought ++ Chlorophyll', 'Sand Stream ++ Sand Rush', 'Soul Dew'], + banlist: ['Uber', 'Arena Trap', 'Drizzle ++ Swift Swim', 'Drought ++ Chlorophyll', 'Sand Stream ++ Sand Rush', 'Soul Dew'], }, { name: "[Gen 4] OU", diff --git a/mods/gen5/formats-data.js b/mods/gen5/formats-data.js index 3fd3895e0af8..03a93e4bebfa 100644 --- a/mods/gen5/formats-data.js +++ b/mods/gen5/formats-data.js @@ -258,7 +258,7 @@ let BattleFormatsData = { dugtrio: { inherit: true, randomBattleMoves: ["earthquake", "stoneedge", "stealthrock", "suckerpunch", "reversal", "substitute"], - tier: "Uber", + tier: "OU", }, meowth: { inherit: true, From 84fbbe06b49309dd444ecc8deb56fa94018d8f4d Mon Sep 17 00:00:00 2001 From: asgdf Date: Fri, 28 Sep 2018 18:59:30 +0200 Subject: [PATCH 124/125] Fix user popup for locked alts in /ip (#4875) --- chat-plugins/info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 1a15cc5b1ea8..8e46855242f1 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -96,7 +96,7 @@ const commands = { const punishment = Punishments.userids.get(targetAlt.userid); const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : ''; - buf += Chat.html`
Alt: ${targetAlt.name}${punishMsg}`; + buf += Chat.html`
Alt: ${targetAlt.name}${punishMsg}`; if (!targetAlt.connected) buf += ` (offline)`; prevNames = Object.keys(targetAlt.prevNames).map(userid => { const punishment = Punishments.userids.get(userid); From a7e3a237635dec13cba9b54717884aa702d82278 Mon Sep 17 00:00:00 2001 From: bgsamm Date: Mon, 8 Oct 2018 09:41:34 -0700 Subject: [PATCH 125/125] Added lastHurtBy getter --- mods/gen1/moves.js | 6 +++--- mods/gen2/moves.js | 16 ++++++---------- mods/gen3/moves.js | 14 ++++++-------- mods/gen4/moves.js | 7 +++---- mods/gennext/moves.js | 7 +++---- mods/stadium/moves.js | 6 +++--- sim/battle.js | 2 +- sim/pokemon.js | 5 +++++ 8 files changed, 30 insertions(+), 33 deletions(-) diff --git a/mods/gen1/moves.js b/mods/gen1/moves.js index 94bae953468d..a0a88fbd51dd 100644 --- a/mods/gen1/moves.js +++ b/mods/gen1/moves.js @@ -967,11 +967,11 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (target.hurtBy.length === 0) { + if (!target.lastHurtBy) { target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { - target.hurtBy[target.hurtBy.length - 1].move = move.id; - target.hurtBy[target.hurtBy.length - 1].damage = damage; + target.lastHurtBy.move = move.id; + target.lastHurtBy.damage = damage; } return 0; }, diff --git a/mods/gen2/moves.js b/mods/gen2/moves.js index d821d4d5ce47..e8c402634685 100644 --- a/mods/gen2/moves.js +++ b/mods/gen2/moves.js @@ -144,11 +144,9 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a physical attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a physical attack this turn, or if the user did not lose HP from the attack. If the opposing Pokemon used Fissure or Horn Drill and missed, this move deals 65535 damage.", damageCallback: function (pokemon, target) { - if (pokemon.hurtBy.length === 0) return false; - let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (lastHurtBy.move && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower') && - (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { - return 2 * lastHurtBy.damage; + if (!pokemon.lastHurtBy) return false; + if (pokemon.lastHurtBy.move && pokemon.lastHurtBy.thisTurn && (this.getCategory(pokemon.lastHurtBy.move) === 'Physical' || this.getMove(pokemon.lastHurtBy.move).id === 'hiddenpower') && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { + return 2 * pokemon.lastHurtBy.damage; } return false; }, @@ -495,11 +493,9 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the opposing Pokemon equal to twice the HP lost by the user from a special attack this turn. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user moves first, if the user was not hit by a special attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon, target) { - if (pokemon.hurtBy.length === 0) return false; - let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (lastHurtBy.move && lastHurtBy.thisTurn && this.getCategory(lastHurtBy.move) === 'Special' && - this.getMove(lastHurtBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { - return 2 * lastHurtBy.damage; + if (!pokemon.lastHurtBy) return false; + if (pokemon.lastHurtBy.move && pokemon.lastHurtBy.thisTurn && this.getCategory(pokemon.lastHurtBy.move) === 'Special' && this.getMove(pokemon.lastHurtBy.move).id !== 'hiddenpower' && (!target.lastMove || target.lastMove.id !== 'sleeptalk')) { + return 2 * pokemon.lastHurtBy.damage; } return false; }, diff --git a/mods/gen3/moves.js b/mods/gen3/moves.js index a22cdaab1aa8..6f747feebee7 100644 --- a/mods/gen3/moves.js +++ b/mods/gen3/moves.js @@ -187,11 +187,10 @@ let BattleMovedex = { inherit: true, desc: "Deals damage to the last opposing Pokemon to hit the user with a physical attack this turn equal to twice the HP lost by the user from that attack. If that opposing Pokemon's position is no longer in use and there is another opposing Pokemon on the field, the damage is done to it instead. This move considers Hidden Power as Normal type, and only the last hit of a multi-hit attack is counted. Fails if the user was not hit by an opposing Pokemon's physical attack this turn, or if the user did not lose HP from the attack.", damageCallback: function (pokemon) { - if (pokemon.hurtBy.length === 0) return false; - let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (lastHurtBy.move && lastHurtBy.thisTurn && (this.getCategory(lastHurtBy.move) === 'Physical' || this.getMove(lastHurtBy.move).id === 'hiddenpower')) { + if (!pokemon.lastHurtBy) return false; + if (pokemon.lastHurtBy.move && pokemon.lastHurtBy.thisTurn && (this.getCategory(pokemon.lastHurtBy.move) === 'Physical' || this.getMove(pokemon.lastHurtBy.move).id === 'hiddenpower')) { // @ts-ignore - return 2 * lastHurtBy.damage; + return 2 * pokemon.lastHurtBy.damage; } return false; }, @@ -559,12 +558,11 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['assist', 'curse', 'doomdesire', 'focuspunch', 'futuresight', 'magiccoat', 'metronome', 'mimic', 'mirrormove', 'naturepower', 'psychup', 'roleplay', 'sketch', 'sleeptalk', 'spikes', 'spitup', 'taunt', 'teeterdance', 'transform']; - if (pokemon.hurtBy.length === 0) return false; - let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (!lastHurtBy.source.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.source.hasMove(lastHurtBy.move)) { + if (!pokemon.lastHurtBy) return false; + if (!pokemon.lastHurtBy.source.lastMove || !pokemon.lastHurtBy.move || noMirror.includes(pokemon.lastHurtBy.move) || !pokemon.lastHurtBy.source.hasMove(pokemon.lastHurtBy.move)) { return false; } - this.useMove(lastHurtBy.move, pokemon); + this.useMove(pokemon.lastHurtBy.move, pokemon); }, target: "self", }, diff --git a/mods/gen4/moves.js b/mods/gen4/moves.js index d4badd4400c5..b100f27c6a4d 100644 --- a/mods/gen4/moves.js +++ b/mods/gen4/moves.js @@ -1104,12 +1104,11 @@ let BattleMovedex = { onTryHit: function () { }, onHit: function (pokemon) { let noMirror = ['acupressure', 'aromatherapy', 'assist', 'chatter', 'copycat', 'counter', 'curse', 'doomdesire', 'feint', 'focuspunch', 'futuresight', 'gravity', 'hail', 'haze', 'healbell', 'helpinghand', 'lightscreen', 'luckychant', 'magiccoat', 'mefirst', 'metronome', 'mimic', 'mirrorcoat', 'mirrormove', 'mist', 'mudsport', 'naturepower', 'perishsong', 'psychup', 'raindance', 'reflect', 'roleplay', 'safeguard', 'sandstorm', 'sketch', 'sleeptalk', 'snatch', 'spikes', 'spitup', 'stealthrock', 'struggle', 'sunnyday', 'tailwind', 'toxicspikes', 'transform', 'watersport']; - if (pokemon.hurtBy.length === 0) return false; - let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (!lastHurtBy.source.lastMove || !lastHurtBy.move || noMirror.includes(lastHurtBy.move) || !lastHurtBy.source.hasMove(lastHurtBy.move)) { + if (!pokemon.lastHurtBy) return false; + if (!pokemon.lastHurtBy.source.lastMove || !pokemon.lastHurtBy.move || noMirror.includes(pokemon.lastHurtBy.move) || !pokemon.lastHurtBy.source.hasMove(pokemon.lastHurtBy.move)) { return false; } - this.useMove(lastHurtBy.move, pokemon); + this.useMove(pokemon.lastHurtBy.move, pokemon); }, target: "self", }, diff --git a/mods/gennext/moves.js b/mods/gennext/moves.js index f2b91dd7214c..79701744f956 100644 --- a/mods/gennext/moves.js +++ b/mods/gennext/moves.js @@ -973,10 +973,9 @@ let BattleMovedex = { avalanche: { inherit: true, basePowerCallback: function (pokemon, source) { - if (pokemon.hurtBy.length > 0) { - let lastHurtBy = pokemon.hurtBy[pokemon.hurtBy.length - 1]; - if (lastHurtBy.damage > 0 && lastHurtBy.thisTurn) { - this.debug('Boosted for getting hit by ' + lastHurtBy.move); + if (pokemon.lastHurtBy) { + if (pokemon.lastHurtBy.damage > 0 && pokemon.lastHurtBy.thisTurn) { + this.debug('Boosted for getting hit by ' + pokemon.lastHurtBy.move); return this.isWeather('hail') ? 180 : 120; } } diff --git a/mods/stadium/moves.js b/mods/stadium/moves.js index 181edfb95171..96d880a31056 100644 --- a/mods/stadium/moves.js +++ b/mods/stadium/moves.js @@ -159,11 +159,11 @@ let BattleMovedex = { } this.runEvent('AfterSubDamage', target, source, move, damage); // Add here counter damage - if (target.hurtBy.length === 0) { + if (!target.lastHurtBy) { target.hurtBy.push({source: source, move: move.id, damage: damage, thisTurn: true}); } else { - target.hurtBy[target.hurtBy.length - 1].move = move.id; - target.hurtBy[target.hurtBy.length - 1].damage = damage; + target.lastHurtBy.move = move.id; + target.lastHurtBy.damage = damage; } return 0; }, diff --git a/sim/battle.js b/sim/battle.js index b423276da331..54cd8c177243 100644 --- a/sim/battle.js +++ b/sim/battle.js @@ -1543,7 +1543,7 @@ class Battle extends Dex.ModdedDex { if (!pokemon.ateBerry) pokemon.disableMove('belch'); // If it was an illusion, it's not any more - if (pokemon.hurtBy.length > 0 && this.gen >= 7) pokemon.knownType = true; + if (pokemon.lastHurtBy && this.gen >= 7) pokemon.knownType = true; for (let i = pokemon.hurtBy.length - 1; i >= 0; i--) { let attack = pokemon.hurtBy[i]; diff --git a/sim/pokemon.js b/sim/pokemon.js index 87ab872551ac..eb54bbe490fe 100644 --- a/sim/pokemon.js +++ b/sim/pokemon.js @@ -610,6 +610,11 @@ class Pokemon { }; this.hurtBy.push(lastHurtBy); } + + get lastHurtBy() { + if (this.hurtBy.length === 0) return undefined; + return this.hurtBy[this.hurtBy.length - 1]; + } /** * @return {string | null}