pokemon-showdown/data/mods/gen2/moves.ts
Guangcong Luo 78439b4a02
Update to ESLint 9 (#10926)
ESLint has a whole new config format, so I figure it's a good time to
make the config system saner.

- First, we no longer have separate eslint-no-types configs. Lint
  performance shouldn't be enough of a problem to justify the
  relevant maintenance complexity.

- Second, our base config should work out-of-the-box now. `npx eslint`
  will work as expected, without any CLI flags. You should still use
  `npm run lint` which adds the `--cached` flag for performance.

- Third, whatever updates I did fixed style linting, which apparently
  has been bugged for quite some time, considering all the obvious
  mixed-tabs-and-spaces issues I found in the upgrade.

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

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

- String + number concatenation is no longer allowed. We now
  consistently use template strings for this.
2025-02-25 20:03:46 -08:00

965 lines
24 KiB
TypeScript

/**
* Gen 2 moves
*/
export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
aeroblast: {
inherit: true,
critRatio: 3,
},
beatup: {
inherit: true,
onModifyMove(move, pokemon) {
move.type = '???';
move.category = 'Special';
move.allies = pokemon.side.pokemon.filter(ally => !ally.fainted && !ally.status);
move.multihit = move.allies.length;
},
},
bellydrum: {
inherit: true,
onHit(target) {
if (target.boosts.atk >= 6) {
return false;
}
if (target.hp <= target.maxhp / 2) {
this.boost({ atk: 2 }, null, null, this.dex.conditions.get('bellydrum2'));
return false;
}
this.directDamage(target.maxhp / 2);
const originalStage = target.boosts.atk;
let currentStage = originalStage;
let boosts = 0;
let loopStage = 0;
while (currentStage < 6) {
loopStage = currentStage;
currentStage++;
if (currentStage < 6) currentStage++;
target.boosts.atk = loopStage;
if (target.getStat('atk', false, true) < 999) {
target.boosts.atk = currentStage;
continue;
}
target.boosts.atk = currentStage - 1;
break;
}
boosts = target.boosts.atk - originalStage;
target.boosts.atk = originalStage;
this.boost({ atk: boosts });
},
},
bide: {
inherit: true,
condition: {
duration: 3,
durationCallback(target, source, effect) {
return this.random(3, 5);
},
onLockMove: 'bide',
onStart(pokemon) {
this.effectState.totalDamage = 0;
this.add('-start', pokemon, 'move: Bide');
},
onDamagePriority: -101,
onDamage(damage, target, source, move) {
if (!move || move.effectType !== 'Move' || !source) return;
this.effectState.totalDamage += damage;
this.effectState.lastDamageSource = source;
},
onBeforeMove(pokemon, target, move) {
if (this.effectState.duration === 1) {
this.add('-end', pokemon, 'move: Bide');
if (!this.effectState.totalDamage) {
this.add('-fail', pokemon);
return false;
}
target = this.effectState.lastDamageSource;
if (!target) {
this.add('-fail', pokemon);
return false;
}
if (!target.isActive) {
const possibleTarget = this.getRandomTarget(pokemon, this.dex.moves.get('pound'));
if (!possibleTarget) {
this.add('-miss', pokemon);
return false;
}
target = possibleTarget;
}
const moveData = {
id: 'bide',
name: "Bide",
accuracy: 100,
damage: this.effectState.totalDamage * 2,
category: "Physical",
priority: 0,
flags: { contact: 1, protect: 1 },
effectType: 'Move',
type: 'Normal',
} as unknown as ActiveMove;
this.actions.tryMoveHit(target, pokemon, moveData);
pokemon.removeVolatile('bide');
return false;
}
this.add('-activate', pokemon, 'move: Bide');
},
onMoveAborted(pokemon) {
pokemon.removeVolatile('bide');
},
onEnd(pokemon) {
this.add('-end', pokemon, 'move: Bide', '[silent]');
},
},
},
counter: {
inherit: true,
damageCallback(pokemon, target) {
const lastAttackedBy = pokemon.getLastAttackedBy();
if (!lastAttackedBy?.move || !lastAttackedBy.thisTurn) return false;
// Hidden Power counts as physical
if (this.getCategory(lastAttackedBy.move) === 'Physical' && target.lastMove?.id !== 'sleeptalk') {
return 2 * lastAttackedBy.damage;
}
return false;
},
beforeTurnCallback() {},
onTry() {},
condition: {},
priority: -1,
},
crabhammer: {
inherit: true,
critRatio: 3,
},
crosschop: {
inherit: true,
critRatio: 3,
},
curse: {
inherit: true,
condition: {
onStart(pokemon, source) {
this.add('-start', pokemon, 'Curse', `[of] ${source}`);
},
onAfterMoveSelf(pokemon) {
this.damage(pokemon.baseMaxhp / 4);
},
},
},
detect: {
inherit: true,
priority: 2,
},
dig: {
inherit: true,
onPrepareHit(target, source) {
return source.status !== 'slp';
},
condition: {
duration: 2,
onImmunity(type, pokemon) {
if (type === 'sandstorm') return false;
},
onInvulnerability(target, source, move) {
if (move.id === 'earthquake' || move.id === 'magnitude' || move.id === 'fissure') {
return;
}
if (['attract', 'curse', 'foresight', 'meanlook', 'mimic', 'nightmare', 'spiderweb', 'transform'].includes(move.id)) {
// Oversight in the interaction between these moves and the Lock-On effect
return false;
}
if (source.volatiles['lockon'] && target === source.volatiles['lockon'].source) return;
return false;
},
onSourceBasePower(basePower, target, source, move) {
if (move.id === 'earthquake' || move.id === 'magnitude') {
return this.chainModify(2);
}
},
},
},
doubleedge: {
inherit: true,
recoil: [25, 100],
},
encore: {
inherit: true,
condition: {
durationCallback() {
return this.random(3, 7);
},
onStart(target) {
const lockedMove = target.lastMoveEncore?.id || '';
const moveIndex = lockedMove ? target.moves.indexOf(lockedMove) : -1;
if (moveIndex < 0 || target.lastMoveEncore?.flags['failencore'] || target.moveSlots[moveIndex].pp <= 0) {
// it failed
return false;
}
this.effectState.move = lockedMove;
this.add('-start', target, 'Encore');
},
onOverrideAction(pokemon) {
return this.effectState.move;
},
onResidualOrder: 13,
onResidual(target) {
const lockedMoveIndex = target.moves.indexOf(this.effectState.move);
if (lockedMoveIndex >= 0 && target.moveSlots[lockedMoveIndex].pp <= 0) {
// early termination if you run out of PP
target.removeVolatile('encore');
}
},
onEnd(target) {
this.add('-end', target, 'Encore');
},
onDisableMove(pokemon) {
if (!this.effectState.move || !pokemon.hasMove(this.effectState.move)) {
return;
}
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id !== this.effectState.move) {
pokemon.disableMove(moveSlot.id);
}
}
},
},
},
endure: {
inherit: true,
priority: 2,
},
explosion: {
inherit: true,
flags: { protect: 1, mirror: 1, metronome: 1, noparentalbond: 1, nosketch: 1 },
},
flail: {
inherit: true,
noDamageVariance: true,
willCrit: false,
},
fly: {
inherit: true,
onPrepareHit(target, source) {
return source.status !== 'slp';
},
condition: {
duration: 2,
onInvulnerability(target, source, move) {
if (move.id === 'gust' || move.id === 'twister' || move.id === 'thunder' || move.id === 'whirlwind') {
return;
}
if (move.id === 'earthquake' || move.id === 'magnitude' || move.id === 'fissure') {
// These moves miss even during the Lock-On effect
return false;
}
if (['attract', 'curse', 'foresight', 'meanlook', 'mimic', 'nightmare', 'spiderweb', 'transform'].includes(move.id)) {
// Oversight in the interaction between these moves and the Lock-On effect
return false;
}
if (source.volatiles['lockon'] && target === source.volatiles['lockon'].source) return;
return false;
},
onSourceBasePower(basePower, target, source, move) {
if (move.id === 'gust' || move.id === 'twister') {
return this.chainModify(2);
}
},
},
},
focusenergy: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'move: Focus Energy');
},
onModifyCritRatio(critRatio) {
return critRatio + 1;
},
},
},
foresight: {
inherit: true,
onTryHit(target) {
if (target.volatiles['foresight']) return false;
},
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Foresight');
},
onNegateImmunity(pokemon, type) {
if (pokemon.hasType('Ghost') && ['Normal', 'Fighting'].includes(type)) return false;
},
onModifyBoost(boosts) {
if (boosts.evasion && boosts.evasion > 0) {
boosts.evasion = 0;
}
},
},
},
frustration: {
inherit: true,
basePowerCallback(pokemon) {
return Math.floor(((255 - pokemon.happiness) * 10) / 25) || null;
},
},
healbell: {
inherit: true,
onHit(target, source) {
this.add('-cureteam', source, '[from] move: Heal Bell');
for (const pokemon of target.side.pokemon) {
pokemon.clearStatus();
}
},
},
highjumpkick: {
inherit: true,
onMoveFail(target, source, move) {
if (target.runImmunity('Fighting')) {
const damage = this.actions.getDamage(source, target, move, true);
if (typeof damage !== 'number') throw new Error("Couldn't get High Jump Kick recoil");
this.damage(this.clampIntRange(damage / 8, 1), source, source, move);
}
},
},
jumpkick: {
inherit: true,
onMoveFail(target, source, move) {
if (target.runImmunity('Fighting')) {
const damage = this.actions.getDamage(source, target, move, true);
if (typeof damage !== 'number') throw new Error("Couldn't get Jump Kick recoil");
this.damage(this.clampIntRange(damage / 8, 1), source, source, move);
}
},
},
karatechop: {
inherit: true,
critRatio: 3,
},
leechseed: {
inherit: true,
onHit() {},
condition: {
onStart(target) {
this.add('-start', target, 'move: Leech Seed');
},
onAfterMoveSelfPriority: 2,
onAfterMoveSelf(pokemon) {
if (!pokemon.hp) return;
const leecher = this.getAtSlot(pokemon.volatiles['leechseed'].sourceSlot);
if (!leecher || leecher.fainted || leecher.hp <= 0) {
return;
}
const toLeech = this.clampIntRange(pokemon.maxhp / 8, 1);
const damage = this.damage(toLeech, pokemon, leecher);
if (damage) {
this.heal(damage, leecher, pokemon);
}
},
},
},
lightscreen: {
inherit: true,
condition: {
duration: 5,
// Sp. Def boost applied directly in stat calculation
onSideStart(side) {
this.add('-sidestart', side, 'move: Light Screen');
},
onSideResidualOrder: 9,
onSideEnd(side) {
this.add('-sideend', side, 'move: Light Screen');
},
},
},
lockon: {
inherit: true,
onTryHit(target) {
if (target.volatiles['foresight'] || target.volatiles['lockon']) return false;
},
condition: {
duration: 2,
onSourceAccuracy(accuracy, target, source, move) {
if (move && source === this.effectState.target && target === this.effectState.source) return true;
},
},
},
lowkick: {
inherit: true,
accuracy: 90,
basePower: 50,
basePowerCallback() {
return 50;
},
secondary: {
chance: 30,
volatileStatus: 'flinch',
},
},
meanlook: {
inherit: true,
flags: { reflectable: 1, mirror: 1, metronome: 1 },
},
metronome: {
inherit: true,
flags: { failencore: 1, nosketch: 1 },
},
mimic: {
inherit: true,
accuracy: 100,
flags: { protect: 1, bypasssub: 1, allyanim: 1, failencore: 1, noassist: 1, nosketch: 1 },
},
mindreader: {
inherit: true,
onTryHit(target) {
if (target.volatiles['foresight'] || target.volatiles['lockon']) return false;
},
},
mirrorcoat: {
inherit: true,
damageCallback(pokemon, target) {
const lastAttackedBy = pokemon.getLastAttackedBy();
if (!lastAttackedBy?.move || !lastAttackedBy.thisTurn) return false;
// Hidden Power counts as physical
if (this.getCategory(lastAttackedBy.move) === 'Special' && target.lastMove?.id !== 'sleeptalk') {
return 2 * lastAttackedBy.damage;
}
return false;
},
beforeTurnCallback() {},
onTry() {},
condition: {},
priority: -1,
},
mirrormove: {
inherit: true,
flags: { metronome: 1, failencore: 1, nosketch: 1 },
onHit(pokemon) {
const noMirror = ['metronome', 'mimic', 'mirrormove', 'sketch', 'sleeptalk', 'transform'];
const target = pokemon.side.foe.active[0];
const lastMove = target?.lastMove && target?.lastMove.id;
if (!lastMove || (!pokemon.activeTurns && !target.moveThisTurn)) {
return false;
}
if (noMirror.includes(lastMove) || pokemon.moves.includes(lastMove)) {
return false;
}
this.actions.useMove(lastMove, pokemon);
},
},
mist: {
num: 54,
accuracy: true,
basePower: 0,
category: "Status",
name: "Mist",
pp: 30,
priority: 0,
flags: { metronome: 1 },
volatileStatus: 'mist',
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Mist');
},
onTryBoost(boost, target, source, effect) {
if (source && target !== source) {
let showMsg = false;
let i: BoostID;
for (i in boost) {
if (boost[i]! < 0) {
delete boost[i];
showMsg = true;
}
}
if (showMsg && !(effect as ActiveMove).secondaries) {
this.add('-activate', target, 'move: Mist');
}
}
},
},
secondary: null,
target: "self",
type: "Ice",
},
moonlight: {
inherit: true,
onHit(pokemon) {
if (this.field.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp);
} else if (this.field.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.baseMaxhp / 4);
} else {
this.heal(pokemon.baseMaxhp / 2);
}
},
},
morningsun: {
inherit: true,
onHit(pokemon) {
if (this.field.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp);
} else if (this.field.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.baseMaxhp / 4);
} else {
this.heal(pokemon.baseMaxhp / 2);
}
},
},
nightmare: {
inherit: true,
condition: {
noCopy: true,
onStart(pokemon) {
if (pokemon.status !== 'slp') {
return false;
}
this.add('-start', pokemon, 'Nightmare');
},
onAfterMoveSelfPriority: 1,
onAfterMoveSelf(pokemon) {
if (pokemon.status === 'slp') this.damage(pokemon.baseMaxhp / 4);
},
},
},
outrage: {
inherit: true,
onMoveFail(target, source, move) {
source.addVolatile('lockedmove');
},
onAfterMove(pokemon) {
if (pokemon.volatiles['lockedmove'] && pokemon.volatiles['lockedmove'].duration === 1) {
pokemon.removeVolatile('lockedmove');
}
},
},
painsplit: {
inherit: true,
accuracy: 100,
},
perishsong: {
inherit: true,
condition: {
duration: 4,
onEnd(target) {
this.add('-start', target, 'perish0');
target.faint();
},
onResidualOrder: 4,
onResidual(pokemon) {
const duration = pokemon.volatiles['perishsong'].duration;
this.add('-start', pokemon, `perish${duration}`);
},
},
},
petaldance: {
inherit: true,
onMoveFail(target, source, move) {
source.addVolatile('lockedmove');
},
onAfterMove(pokemon) {
if (pokemon.volatiles['lockedmove'] && pokemon.volatiles['lockedmove'].duration === 1) {
pokemon.removeVolatile('lockedmove');
}
},
},
poisongas: {
inherit: true,
ignoreImmunity: false,
},
poisonpowder: {
inherit: true,
ignoreImmunity: false,
},
protect: {
inherit: true,
priority: 2,
},
psywave: {
inherit: true,
damageCallback(pokemon) {
return this.random(1, pokemon.level + Math.floor(pokemon.level / 2));
},
},
pursuit: {
inherit: true,
onModifyMove() {},
condition: {
duration: 1,
onBeforeSwitchOut(pokemon) {
this.debug('Pursuit start');
let alreadyAdded = false;
for (const source of this.effectState.sources) {
if (source.speed < pokemon.speed || (source.speed === pokemon.speed && this.randomChance(1, 2))) {
// Destiny Bond ends if the switch action "outspeeds" the attacker, regardless of host
pokemon.removeVolatile('destinybond');
}
if (!this.queue.cancelMove(source) || !source.hp) continue;
if (!alreadyAdded) {
this.add('-activate', pokemon, 'move: Pursuit');
alreadyAdded = true;
}
// Run through each action in queue to check if the Pursuit user is supposed to Mega Evolve this turn.
// If it is, then Mega Evolve before moving.
if (source.canMegaEvo || source.canUltraBurst) {
for (const [actionIndex, action] of this.queue.entries()) {
if (action.pokemon === source && action.choice === 'megaEvo') {
this.actions.runMegaEvo(source);
this.queue.list.splice(actionIndex, 1);
break;
}
}
}
this.actions.runMove('pursuit', source, source.getLocOf(pokemon));
}
},
},
},
razorleaf: {
inherit: true,
critRatio: 3,
},
razorwind: {
inherit: true,
accuracy: 75,
critRatio: 3,
onPrepareHit(target, source) {
return source.status !== 'slp';
},
},
reflect: {
inherit: true,
condition: {
duration: 5,
// Defense boost applied directly in stat calculation
onSideStart(side) {
this.add('-sidestart', side, 'Reflect');
},
onSideResidualOrder: 9,
onSideEnd(side) {
this.add('-sideend', side, 'Reflect');
},
},
},
rest: {
inherit: true,
onTry(pokemon) {
if (pokemon.hp < pokemon.maxhp) return;
this.add('-fail', pokemon);
return null;
},
onHit(target, source, move) {
if (target.status !== 'slp') {
if (!target.setStatus('slp', source, move)) return;
} else {
this.add('-status', target, 'slp', '[from] move: Rest');
}
target.statusState.time = 3;
target.statusState.startTime = 3;
target.statusState.source = target;
this.heal(target.maxhp);
},
secondary: null,
},
return: {
inherit: true,
basePowerCallback(pokemon) {
return Math.floor((pokemon.happiness * 10) / 25) || null;
},
},
reversal: {
inherit: true,
noDamageVariance: true,
willCrit: false,
},
roar: {
inherit: true,
onTryHit() {
for (const action of this.queue) {
// Roar only works if it is the last action in a turn, including when it's called by Sleep Talk
if (action.choice === 'move' || action.choice === 'switch') return false;
}
},
priority: -1,
},
safeguard: {
inherit: true,
condition: {
duration: 5,
durationCallback(target, source, effect) {
if (source?.hasAbility('persistent')) {
this.add('-activate', source, 'ability: Persistent', effect);
return 7;
}
return 5;
},
onSetStatus(status, target, source, effect) {
if (!effect || !source) return;
if (effect.id === 'yawn') return;
if (effect.effectType === 'Move' && effect.infiltrates && !target.isAlly(source)) return;
if (target !== source) {
this.debug('interrupting setStatus');
if (effect.id === 'synchronize' || (effect.effectType === 'Move' && !effect.secondaries)) {
this.add('-activate', target, 'move: Safeguard');
}
return null;
}
},
onTryAddVolatile(status, target, source, effect) {
if (!effect || !source) return;
if (effect.effectType === 'Move' && effect.infiltrates && !target.isAlly(source)) return;
if ((status.id === 'confusion' || status.id === 'yawn') && target !== source) {
if (effect.effectType === 'Move' && !effect.secondaries) this.add('-activate', target, 'move: Safeguard');
return null;
}
},
onSideStart(side) {
this.add('-sidestart', side, 'Safeguard');
},
onSideResidualOrder: 8,
onSideEnd(side) {
this.add('-sideend', side, 'Safeguard');
},
},
},
selfdestruct: {
inherit: true,
flags: { protect: 1, mirror: 1, metronome: 1, noparentalbond: 1, nosketch: 1 },
},
sketch: {
inherit: true,
flags: { bypasssub: 1, failencore: 1, noassist: 1, nosketch: 1 },
onHit() {
// Sketch always fails in Link Battles
this.add('-nothing');
},
},
skullbash: {
inherit: true,
onPrepareHit(target, source) {
return source.status !== 'slp';
},
},
skyattack: {
inherit: true,
critRatio: 1,
onPrepareHit(target, source) {
return source.status !== 'slp';
},
secondary: null,
},
slash: {
inherit: true,
critRatio: 3,
},
sleeptalk: {
inherit: true,
flags: { failencore: 1, nosleeptalk: 1, nosketch: 1 },
onHit(pokemon) {
const moves = [];
for (const moveSlot of pokemon.moveSlots) {
const moveid = moveSlot.id;
const move = this.dex.moves.get(moveid);
if (moveid && !move.flags['nosleeptalk'] && !move.flags['charge']) {
moves.push(moveid);
}
}
let randomMove = '';
if (moves.length) randomMove = this.sample(moves);
if (!randomMove) return false;
this.actions.useMove(randomMove, pokemon);
},
},
solarbeam: {
inherit: true,
onPrepareHit(target, source) {
return source.status !== 'slp';
},
// Rain weakening done directly in the damage formula
onBasePower() {},
},
spiderweb: {
inherit: true,
flags: { reflectable: 1, mirror: 1, metronome: 1 },
},
spikes: {
inherit: true,
condition: {
// this is a side condition
onSideStart(side) {
if (!this.effectState.layers || this.effectState.layers === 0) {
this.add('-sidestart', side, 'Spikes');
this.effectState.layers = 1;
} else {
return false;
}
},
onSwitchIn(pokemon) {
if (!pokemon.runImmunity('Ground')) return;
const damageAmounts = [0, 3];
this.damage(damageAmounts[this.effectState.layers] * pokemon.maxhp / 24);
},
},
},
substitute: {
inherit: true,
condition: {
onStart(target) {
this.add('-start', target, 'Substitute');
this.effectState.hp = Math.floor(target.maxhp / 4);
delete target.volatiles['partiallytrapped'];
},
onTryPrimaryHitPriority: -1,
onTryPrimaryHit(target, source, move) {
if (move.stallingMove) {
this.add('-fail', source);
return null;
}
if (target === source) {
this.debug('sub bypass: self hit');
return;
}
if (move.id === 'twineedle') {
move.secondaries = move.secondaries!.filter(p => !p.kingsrock);
}
if (move.drain) {
this.add('-miss', source);
this.hint("In Gen 2, draining moves always miss against Substitute.");
return null;
}
if (move.category === 'Status') {
const SubBlocked = ['leechseed', 'lockon', 'mindreader', 'nightmare', 'painsplit', 'sketch'];
if (move.id === 'swagger') {
// this is safe, move is a copy
delete move.volatileStatus;
}
if (
move.status || (move.boosts && move.id !== 'swagger') ||
move.volatileStatus === 'confusion' || SubBlocked.includes(move.id)
) {
this.add('-activate', target, 'Substitute', '[block] ' + move.name);
return null;
}
return;
}
let damage = this.actions.getDamage(source, target, move);
if (!damage) {
return null;
}
damage = this.runEvent('SubDamage', target, source, move, damage);
if (!damage) {
return damage;
}
if (damage > target.volatiles['substitute'].hp) {
damage = target.volatiles['substitute'].hp as number;
}
target.volatiles['substitute'].hp -= damage;
source.lastDamage = damage;
if (target.volatiles['substitute'].hp <= 0) {
target.removeVolatile('substitute');
} else {
this.add('-activate', target, 'Substitute', '[damage]');
}
if (move.recoil) {
this.damage(1, source, target, 'recoil');
}
this.runEvent('AfterSubDamage', target, source, move, damage);
return this.HIT_SUBSTITUTE;
},
onEnd(target) {
this.add('-end', target, 'Substitute');
},
},
},
swagger: {
inherit: true,
onTryHit(target, pokemon) {
if (target.boosts.atk >= 6 || target.getStat('atk', false, true) === 999) {
this.add('-miss', pokemon);
return null;
}
},
},
synthesis: {
inherit: true,
onHit(pokemon) {
if (this.field.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp);
} else if (this.field.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.baseMaxhp / 4);
} else {
this.heal(pokemon.baseMaxhp / 2);
}
},
},
thief: {
inherit: true,
onAfterHit() {},
secondary: {
chance: 100,
onHit(target, source) {
if (source.item || source.volatiles['gem']) {
return;
}
const yourItem = target.takeItem(source);
if (!yourItem) {
return;
}
if (!source.setItem(yourItem)) {
target.item = yourItem.id; // bypass setItem so we don't break choicelock or anything
return;
}
this.add('-item', source, yourItem, '[from] move: Thief', `[of] ${target}`);
},
},
},
thrash: {
inherit: true,
onMoveFail(target, source, move) {
source.addVolatile('lockedmove');
},
onAfterMove(pokemon) {
if (pokemon.volatiles['lockedmove'] && pokemon.volatiles['lockedmove'].duration === 1) {
pokemon.removeVolatile('lockedmove');
}
},
},
toxic: {
inherit: true,
ignoreImmunity: false,
},
transform: {
inherit: true,
flags: { bypasssub: 1, metronome: 1, failencore: 1, nosketch: 1 },
},
triattack: {
inherit: true,
onHit(target, source, move) {
move.statusRoll = ['par', 'frz', 'brn'][this.random(3)];
},
secondary: {
chance: 20,
onHit(target, source, move) {
if (move.statusRoll) {
target.trySetStatus(move.statusRoll, source);
}
},
},
},
triplekick: {
inherit: true,
multiaccuracy: false,
multihit: [1, 3],
},
whirlwind: {
inherit: true,
onTryHit() {
for (const action of this.queue) {
// Whirlwind only works if it is the last action in a turn, including when it's called by Sleep Talk
if (action.choice === 'move' || action.choice === 'switch') return false;
}
},
priority: -1,
},
};