From fb18721e8241c5ed0c54a3ffdcfbb4081fda4495 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Fri, 8 May 2020 19:24:02 -0700 Subject: [PATCH] Update "not useless" moves (#1511) Viable moves no longer use the `isViable` flag. Instead, we start by assuming moves with 75+ base power are not useless (and others are useless), and manually list exceptions in the relevant dexsearch function. In general, the move viability system has been a bit too specific. I don't want to hardcode species because it's nearly always also hardcoding specific metagames. It may help to think of it as "would this move be useful in any metagame ever?" rather than "would it be useful in the current metagame". We instead use a significantly lower bar of "not useless", although this hasn't been made clear until now. The word "viable" is now banished from the codebase as well as the teambuilder's actual text, which may also help. Select changes: - doubles moves now have separate viability lists - Rock Tomb - it's good on Groudon for reasons that don't seem Groudon-specific, and is also good with Technician and in Doubles, so we'll default to yes - Belly Drum - viable with priority, high speed, or Trick Room: too many conditions, so we'll default to yes - Reflect Type - viable on Latias and Starmie for reasons that aren't specific to them, we'll default to yes - Transform - required on Ditto, useful in Doubles: default to yes - Fake Out - no longer viable if you have Sheer Force, should hopefully make people choose it less - Nature's Madness, Super Fang - viable in Doubles, has uses in Singles: default to yes - Dragon Darts, Sparkling Aria - mistakenly categorized as unviable; fixed --- src/battle-dex-data.ts | 2 - src/battle-dex-search.ts | 203 ++++++++++++++++++++++++--------------- 2 files changed, 126 insertions(+), 79 deletions(-) diff --git a/src/battle-dex-data.ts b/src/battle-dex-data.ts index 7cc3a722d..cd750fae4 100644 --- a/src/battle-dex-data.ts +++ b/src/battle-dex-data.ts @@ -1090,7 +1090,6 @@ class Move implements Effect { readonly desc: string; readonly shortDesc: string; - readonly isViable: boolean; readonly isNonstandard: string | null; readonly isZ: ID; readonly zMove?: { @@ -1130,7 +1129,6 @@ class Move implements Effect { // TODO: move to text.js this.desc = data.desc; this.shortDesc = data.shortDesc; - this.isViable = !!data.isViable; this.isNonstandard = data.isNonstandard || null; this.isZ = data.isZ || ''; this.zMove = data.zMove || {}; diff --git a/src/battle-dex-search.ts b/src/battle-dex-search.ts index 06475376e..edc095cd9 100644 --- a/src/battle-dex-search.ts +++ b/src/battle-dex-search.ts @@ -1117,26 +1117,25 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> { } return results; } - private moveIsViable(id: ID, species: Species, abilityid: ID, itemid: ID, moves: string[]) { + private moveIsNotUseless(id: ID, species: Species, abilityid: ID, itemid: ID, moves: string[]) { const dex = this.dex; - const format = this.format; if (dex.gen === 1) { - // Usually viable for Gen 1 + // Usually not useless for Gen 1 if ([ 'acidarmor', 'amnesia', 'barrier', 'bind', 'blizzard', 'clamp', 'confuseray', 'counter', 'firespin', 'hyperbeam', 'mirrormove', 'pinmissile', 'razorleaf', 'sing', 'slash', 'sludge', 'twineedle', 'wrap', ].includes(id)) { return true; } - // Usually not viable for Gen 1 + // Usually useless for Gen 1 if ([ 'disable', 'firepunch', 'icepunch', 'leechseed', 'quickattack', 'roar', 'thunderpunch', 'toxic', 'triattack', 'whirlwind', ].includes(id)) { return false; } - // Viable only when certain moves aren't present + // Not useless only when certain moves aren't present switch (id) { case 'bubblebeam': return (!moves.includes('surf') && !moves.includes('blizzard')); case 'doubleedge': return !moves.includes('bodyslam'); @@ -1152,79 +1151,129 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> { if (id === 'megadrain') return true; } - if (format === 'mixandmega') { - switch (id) { - case 'blizzard': return itemid === 'abomasite' || itemid === 'pidgeotite'; - case 'feint': return species.id === 'weavile'; - case 'grasswhistle': return itemid === 'pidgeotite'; - case 'hypnosis': return itemid === 'pidgeotite'; - case 'inferno': return itemid === 'pidgeotite' && !moves.includes('fireblast'); - case 'sing': return itemid === 'pidgeotite'; - case 'thunder': return itemid === 'pidgeotite' && !moves.includes('zapcannon'); - case 'waterpulse': return itemid === 'blastoisinite' && !moves.includes('originpulse'); - case 'weatherball': return itemid === 'redorb'; - case 'zapcannon': return itemid === 'pidgeotite'; - } - } + if (itemid === 'pidgeotite') abilityid = 'noguard' as ID; + if (itemid === 'blastoiseinite') abilityid = 'megalauncher' as ID; + if (itemid === 'aerodactylite') abilityid = 'toughclaws' as ID; + if (itemid === 'glalitite') abilityid = 'refrigerate' as ID; switch (id) { - case 'aerialace': return ['scyther', 'aerodactylmega', 'kricketune'].includes(species.id); - case 'ancientpower': return ( - abilityid === 'technician' || abilityid === 'serenegrace' || - (species.types.includes('Rock') && !moves.includes('powergem')) - ); - case 'aurawheel': return (species.id.startsWith('morpeko')); - case 'bellydrum': return ['azumarill', 'linoone', 'slurpuff'].includes(species.id); - case 'blizzard': return (abilityid === 'snowwarning'); - case 'counter': return ['chansey', 'skarmory', 'clefable', 'wobbuffet', 'alakazam'].includes(species.id); - case 'curse': return (species.id === 'snorlax'); - case 'darkvoid': return (dex.gen < 7); - case 'drainingkiss': return (abilityid === 'triage'); - case 'dynamicpunch': return (abilityid === 'noguard'); - case 'electroball': return (abilityid === 'surgesurfer'); - case 'gyroball': return (species.baseStats.spe <= 60); - case 'headbutt': return (abilityid === 'serenegrace' && species.types.includes('Normal')); - case 'heartswap': return (species.id === 'magearna'); - case 'hiddenpowerelectric': return (!moves.includes('thunderbolt')); - case 'hiddenpowerfighting': return (!moves.includes('aurasphere') && !moves.includes('focusblast')); - case 'hiddenpowerfire': return (!moves.includes('flamethrower')); - case 'hiddenpowergrass': return (!moves.includes('energyball') && !moves.includes('gigadrain')); - case 'hiddenpowerice': return (!moves.includes('icebeam') && species.id !== 'xerneas'); - case 'hyperspacefury': return (species.id === 'hoopaunbound'); - case 'hypnosis': return ((dex.gen < 4 && !moves.includes('sleeppowder')) || (species.id === 'darkrai')); - case 'icywind': return (species.id.substr(0, 6) === 'keldeo'); - case 'infestation': return (species.id === 'shuckle'); - case 'irontail': return ( - (species.types.includes('Steel') && !moves.includes('ironhead')) || - ( - (species.types.includes('Dark') || species.types.includes('Dragon')) && - !moves.includes('ironhead') && !moves.includes('gunkshot') - ) - ); - case 'jumpkick': return (!moves.includes('highjumpkick')); - case 'leechlife': return (dex.gen > 6); - case 'petaldance': return (abilityid === 'owntempo'); - case 'reflecttype': return ['latias', 'starmie'].includes(species.id); - case 'rocktomb': return (species.id === 'groudon' || abilityid === 'technician'); - case 'selfdestruct': return (dex.gen < 5 && !moves.includes('explosion')); - case 'skyattack': return (species.id === 'hawlucha'); - case 'smackdown': return (species.types.includes('Ground')); - case 'smartstrike': return (species.types.includes('Steel') && !moves.includes('ironhead')); - case 'solarbeam': case 'solarblade': return ['desolateland', 'drought', 'chlorophyll'].includes(abilityid); - case 'stompingtantrum': return ( - (!moves.includes('earthquake') && !moves.includes('drillrun')) || - (abilityid === 'toughclaws' && !moves.includes('drillrun') && !moves.includes('earthquake')) - ); - case 'storedpower': return ['necrozma', 'espeon', 'sigilyph'].includes(species.id); - case 'stunspore': return (!moves.includes('thunderwave')); - case 'teleport': return (dex.gen > 7); - case 'thunder': return (abilityid === 'drizzle' || (abilityid === 'primordialsea') || (species.id === 'xerneas')); - case 'trickroom': return (species.baseStats.spe <= 100); - case 'waterpulse': return (abilityid === 'megalauncher' && !moves.includes('originpulse')); + case 'fakeout': case 'flamecharge': case 'nuzzle': case 'poweruppunch': + return abilityid !== 'sheerforce'; + case 'solarbeam': case 'solarblade': + return ['desolateland', 'drought', 'chlorophyll'].includes(abilityid) || itemid === 'powerherb'; + case 'dynamicpunch': case 'grasswhistle': case 'inferno': case 'sing': case 'zapcannon': + return abilityid === 'noguard'; + + case 'aerialace': + return ['technician', 'toughclaws'].includes(abilityid) && !moves.includes('bravebird'); + case 'ancientpower': + return ['technician', 'serenegrace'].includes(abilityid) || !moves.includes('powergem'); + case 'aurawheel': + return species.baseSpecies === 'Morpeko'; + case 'bellydrum': + return moves.includes('extremespeed') || moves.includes('aquajet') || abilityid === 'unburden'; + case 'counter': + return species.baseStats.hp >= 65; + case 'darkvoid': + return dex.gen < 7; + case 'drainingkiss': + return abilityid === 'triage'; + case 'electroball': + return abilityid === 'surgesurfer'; + case 'feint': + return abilityid === 'refrigerate'; + case 'gyroball': + return species.baseStats.spe <= 60; + case 'headbutt': + return abilityid === 'serenegrace'; + case 'hiddenpowerelectric': + return !moves.includes('thunderbolt'); + case 'hiddenpowerfighting': + return !moves.includes('aurasphere') && !moves.includes('focusblast'); + case 'hiddenpowerfire': + return !moves.includes('flamethrower'); + case 'hiddenpowergrass': + return !moves.includes('energyball') && !moves.includes('gigadrain'); + case 'hiddenpowerice': + return !moves.includes('icebeam') && !moves.includes('aurorabeam'); + case 'hiddenpowerpsychic': + return species.baseSpecies === 'Unown'; + case 'hyperspacefury': + return species.id === 'hoopaunbound'; + case 'hypnosis': + return (dex.gen < 4 && !moves.includes('sleeppowder')) || ['baddreams', 'noguard'].includes(abilityid) || + this.formatType === 'doubles'; + case 'icywind': + // Keldeo needs Hidden Power for Electric/Ghost + return species.baseSpecies === 'Keldeo' || this.formatType === 'doubles'; + case 'infestation': + return moves.includes('stickyweb'); + case 'irontail': + return !moves.includes('ironhead') && (species.types.includes('Steel') || !moves.includes('gunkshot')); + case 'jumpkick': + return !moves.includes('highjumpkick'); + case 'leechlife': + return dex.gen > 6; + case 'petaldance': + return abilityid === 'owntempo'; + case 'phantomforce': + return !moves.includes('shadowclaw') || abilityid === 'harvest' || this.formatType === 'doubles'; + case 'relicsong': + return species.id === 'meloetta'; + case 'selfdestruct': + return dex.gen < 5 && !moves.includes('explosion'); + case 'smackdown': + return species.types.includes('Ground'); + case 'smartstrike': + return species.types.includes('Steel') && !moves.includes('ironhead'); + case 'stompingtantrum': + return (!moves.includes('earthquake') && !moves.includes('drillrun')) || this.formatType === 'doubles'; + case 'stunspore': + return !moves.includes('thunderwave'); + case 'technoblast': + return dex.gen > 5 && itemid.endsWith('drive') || itemid === 'dousedrive'; + case 'teleport': + return dex.gen > 7; + case 'trickroom': + return species.baseStats.spe <= 100; + case 'waterpulse': + return ['megalauncher', 'technician'].includes(abilityid) && !moves.includes('originpulse'); } - return !!BattleMovedex[id]?.isViable; + if (this.formatType === 'doubles' && BattleMoveSearch.GOOD_DOUBLES_MOVES.includes(id)) { + return true; + } + + const moveData = BattleMovedex[id]; + if (!moveData) return true; + if (moveData.category === 'Status') { + return BattleMoveSearch.GOOD_STATUS_MOVES.includes(id); + } + if (moveData.basePower < 75) { + return BattleMoveSearch.GOOD_WEAK_MOVES.includes(id); + } + if (id === 'skydrop') return true; + // strong moves + if (moveData.flags?.charge) { + return itemid === 'powerherb'; + } + if (moveData.flags?.recharge) { + return false; + } + return !BattleMoveSearch.BAD_STRONG_MOVES.includes(id); } + static readonly GOOD_STATUS_MOVES = [ + 'agility', 'aromatherapy', 'auroraveil', 'autotomize', 'banefulbunker', 'batonpass', 'bellydrum', 'bulkup', 'calmmind', 'clangoroussoul', 'coil', 'cottonguard', 'courtchange', 'curse', 'defog', 'destinybond', 'detect', 'disable', 'dragondance', 'encore', 'extremeevoboost', 'glare', 'haze', 'healbell', 'healingwish', 'healorder', 'heartswap', 'honeclaws', 'kingsshield', 'leechseed', 'lifedew', 'lightscreen', 'lovelykiss', 'magiccoat', 'maxguard', 'memento', 'milkdrink', 'moonlight', 'morningsun', 'nastyplot', 'naturepower', 'naturesmadness', 'noretreat', 'obstruct', 'painsplit', 'partingshot', 'perishsong', 'protect', 'quiverdance', 'recover', 'reflect', 'reflecttype', 'refresh', 'rest', 'roar', 'rockpolish', 'roost', 'shellsmash', 'shiftgear', 'slackoff', 'sleeppowder', 'sleeptalk', 'softboiled', 'spikes', 'spikyshield', 'spore', 'stealthrock', 'stickyweb', 'strengthsap', 'substitute', 'superfang', 'switcheroo', 'swordsdance', 'synthesis', 'tailglow', 'tailwind', 'taunt', 'thunderwave', 'toxic', 'toxicspikes', 'transform', 'trick', 'whirlwind', 'willowisp', 'wish', + ] as ID[] as readonly ID[]; + static readonly GOOD_WEAK_MOVES = [ + 'accelerock', 'acrobatics', 'aquajet', 'avalanche', 'bonemerang', 'bouncybubble', 'bulletpunch', 'bulletseed', 'buzzybuzz', 'circlethrow', 'clearsmog', 'doubleironbash', 'dragondarts', 'dragontail', 'endeavor', 'facade', 'firefang', 'freezedry', 'frustration', 'geargrind', 'grassknot', 'gyroball', 'heavyslam', 'hex', 'icefang', 'iceshard', 'iciclespear', 'knockoff', 'lowkick', 'machpunch', 'nightshade', 'nightslash', 'pikapapow', 'psychocut', 'pursuit', 'quickattack', 'rapidspin', 'return', 'rockblast', 'rocktomb', 'seismictoss', 'shadowclaw', 'shadowpunch', 'shadowsneak', 'sizzlyslide', 'storedpower', 'stormthrow', 'suckerpunch', 'tailslap', 'uturn', 'veeveevolley', 'voltswitch', 'watershuriken', 'weatherball', + ] as ID[] as readonly ID[]; + static readonly BAD_STRONG_MOVES = [ + 'appleacid', 'beakblast', 'belch', 'burnup', 'crushclaw', 'doomdesire', 'dragonrush', 'dreameater', 'eggbomb', 'firepledge', 'flyingpress', 'futuresight', 'grasspledge', 'gravapple', 'hyperfang', 'hyperspacehole', 'jawlock', 'landswrath', 'lastresort', 'lightthatburnsthesky', 'megakick', 'megapunch', 'muddywater', 'mysticalfire', 'nightdaze', 'pollenpuff', 'rockclimb', 'seedflare', 'selfdestruct', 'shelltrap', 'skyuppercut', 'slam', 'snipeshot', 'strength', 'submission', 'synchronoise', 'takedown', 'thrash', 'uproar', 'waterpledge', + ] as ID[] as readonly ID[]; + static readonly GOOD_DOUBLES_MOVES = [ + 'allyswitch', 'bulldoze', 'electroweb', 'faketears', 'fling', 'followme', 'healpulse', 'helpinghand', 'muddywater', 'pollenpuff', 'psychup', 'ragepowder', 'safeguard', 'skillswap', 'wideguard', 'yawn', + ] as ID[] as readonly ID[]; getBaseResults() { if (!this.species) return this.getDefaultResults(); const dex = this.dex; @@ -1376,8 +1425,8 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> { let usableMoves: SearchRow[] = []; let uselessMoves: SearchRow[] = []; for (const id of moves) { - const isViable = this.moveIsViable(id as ID, species, abilityid, itemid, moves); - if (isViable) { + const isUsable = this.moveIsNotUseless(id as ID, species, abilityid, itemid, moves); + if (isUsable) { if (!usableMoves.length) usableMoves.push(['header', "Moves"]); usableMoves.push(['move', id as ID]); } else { @@ -1390,8 +1439,8 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> { uselessMoves.push(['header', "Useless sketched moves"]); } for (const id of sketchMoves) { - const isViable = this.moveIsViable(id as ID, species, abilityid, itemid, sketchMoves); - if (isViable) { + const isUsable = this.moveIsNotUseless(id as ID, species, abilityid, itemid, sketchMoves); + if (isUsable) { usableMoves.push(['move', id as ID]); } else { uselessMoves.push(['move', id as ID]);