pokemon-showdown/data/mods/gen4/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

1965 lines
50 KiB
TypeScript

export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
acupressure: {
inherit: true,
flags: { snatch: 1, metronome: 1 },
onHit(target) {
if (target.volatiles['substitute']) {
return false;
}
const stats: BoostID[] = [];
let stat: BoostID;
for (stat in target.boosts) {
if (target.boosts[stat] < 6) {
stats.push(stat);
}
}
if (stats.length) {
const randomStat = this.sample(stats);
const boost: SparseBoostsTable = {};
boost[randomStat] = 2;
this.boost(boost);
} else {
return false;
}
},
},
aromatherapy: {
inherit: true,
onHit(target, source) {
this.add('-cureteam', source, '[from] move: Aromatherapy');
const allies = [...target.side.pokemon, ...target.side.allySide?.pokemon || []];
for (const ally of allies) {
ally.clearStatus();
}
},
},
aquaring: {
inherit: true,
flags: { metronome: 1 },
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Aqua Ring');
},
onResidualOrder: 10,
onResidualSubOrder: 2,
onResidual(pokemon) {
this.heal(pokemon.baseMaxhp / 16);
},
},
},
assist: {
inherit: true,
onHit(target) {
const moves = [];
for (const pokemon of target.side.pokemon) {
if (pokemon === target) continue;
for (const moveSlot of pokemon.moveSlots) {
const moveid = moveSlot.id;
const move = this.dex.moves.get(moveid);
if (
move.flags['noassist'] ||
(this.field.pseudoWeather['gravity'] && move.flags['gravity']) ||
(target.volatiles['healblock'] && move.flags['heal'])
) {
continue;
}
moves.push(moveid);
}
}
let randomMove = '';
if (moves.length) randomMove = this.sample(moves);
if (!randomMove) {
return false;
}
this.actions.useMove(randomMove, target);
},
},
beatup: {
inherit: true,
basePower: 10,
basePowerCallback(pokemon, target, move) {
if (!move.allies?.length) return null;
return 10;
},
onModifyMove(move, pokemon) {
pokemon.addVolatile('beatup');
move.type = '???';
move.category = 'Physical';
move.allies = pokemon.side.pokemon.filter(ally => !ally.fainted && !ally.status);
move.multihit = move.allies.length;
},
condition: {
duration: 1,
onModifyAtkPriority: -101,
onModifyAtk(atk, pokemon, defender, move) {
// https://www.smogon.com/forums/posts/8992145/
// this.add('-activate', pokemon, 'move: Beat Up', '[of] ' + move.allies![0].name);
this.event.modifier = 1;
return move.allies!.shift()!.species.baseStats.atk;
},
onFoeModifyDefPriority: -101,
onFoeModifyDef(def, pokemon) {
this.event.modifier = 1;
return pokemon.species.baseStats.def;
},
},
},
bide: {
inherit: true,
condition: {
duration: 3,
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;
},
onAfterSetStatus(status, pokemon) {
if (status.id === 'slp' || status.id === 'frz') {
pokemon.removeVolatile('bide');
}
},
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: true,
damage: this.effectState.totalDamage * 2,
category: "Physical",
priority: 1,
flags: { contact: 1, protect: 1 },
ignoreImmunity: true,
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]');
},
},
},
bind: {
inherit: true,
accuracy: 75,
},
bonerush: {
inherit: true,
accuracy: 80,
},
bravebird: {
inherit: true,
recoil: [1, 3],
},
bulletseed: {
inherit: true,
basePower: 10,
},
camouflage: {
inherit: true,
onHit(target) {
if (target.hasType('Normal') || !target.setType('Normal')) return false;
this.add('-start', target, 'typechange', 'Normal');
},
},
chatter: {
inherit: true,
secondary: {
chance: 31,
volatileStatus: 'confusion',
},
},
clamp: {
inherit: true,
accuracy: 75,
pp: 10,
},
conversion: {
inherit: true,
flags: { metronome: 1 },
onHit(target) {
const possibleTypes = target.moveSlots.map(moveSlot => {
const move = this.dex.moves.get(moveSlot.id);
if (move.id !== 'conversion' && move.id !== 'curse' && !target.hasType(move.type)) {
return move.type;
}
return '';
}).filter(type => type);
if (!possibleTypes.length) {
return false;
}
const type = this.sample(possibleTypes);
if (!target.setType(type)) return false;
this.add('-start', target, 'typechange', type);
},
},
copycat: {
inherit: true,
onHit(pokemon) {
const move: Move | ActiveMove | null = this.lastMove;
if (!move) return;
if (
move.flags['failcopycat'] ||
(this.field.pseudoWeather['gravity'] && move.flags['gravity']) ||
(pokemon.volatiles['healblock'] && move.flags['heal'])
) {
return false;
}
this.actions.useMove(move.id, pokemon);
},
},
cottonspore: {
inherit: true,
accuracy: 85,
},
covet: {
inherit: true,
basePower: 40,
},
crabhammer: {
inherit: true,
accuracy: 85,
},
crushgrip: {
inherit: true,
basePowerCallback(pokemon, target) {
const bp = Math.floor(target.hp * 120 / target.maxhp) + 1;
this.debug(`BP for ${target.hp}/${target.maxhp} HP: ${bp}`);
return bp;
},
},
curse: {
inherit: true,
flags: { metronome: 1 },
onModifyMove(move, source, target) {
if (!source.hasType('Ghost')) {
delete move.volatileStatus;
delete move.onHit;
move.self = { boosts: { atk: 1, def: 1, spe: -1 } };
move.target = move.nonGhostTarget!;
} else if (target?.volatiles['substitute']) {
delete move.volatileStatus;
delete move.onHit;
}
},
condition: {
onStart(pokemon, source) {
this.add('-start', pokemon, 'Curse', `[of] ${source}`);
},
onResidualOrder: 10,
onResidualSubOrder: 8,
onResidual(pokemon) {
this.damage(pokemon.baseMaxhp / 4);
},
},
type: "???",
},
defog: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
detect: {
inherit: true,
priority: 3,
condition: {
duration: 1,
onStart(target) {
this.add('-singleturn', target, 'Protect');
},
onTryHitPriority: 3,
onTryHit(target, source, move) {
if (!move.flags['protect']) return;
this.add('-activate', target, 'Protect');
const lockedmove = source.getVolatile('lockedmove');
if (lockedmove) {
// Outrage counter is NOT reset
if (source.volatiles['lockedmove'].trueDuration >= 2) {
source.volatiles['lockedmove'].duration = 2;
}
}
return null;
},
},
},
disable: {
inherit: true,
accuracy: 80,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
volatileStatus: 'disable',
condition: {
durationCallback() {
return this.random(4, 8);
},
noCopy: true,
onStart(pokemon) {
if (!this.queue.willMove(pokemon)) {
this.effectState.duration!++;
}
if (!pokemon.lastMove) {
return false;
}
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id === pokemon.lastMove.id) {
if (!moveSlot.pp) {
return false;
} else {
this.add('-start', pokemon, 'Disable', moveSlot.move);
this.effectState.move = pokemon.lastMove.id;
return;
}
}
}
return false;
},
onResidualOrder: 10,
onResidualSubOrder: 13,
onEnd(pokemon) {
this.add('-end', pokemon, 'move: Disable');
},
onBeforeMovePriority: 7,
onBeforeMove(attacker, defender, move) {
if (move.id === this.effectState.move) {
this.add('cant', attacker, 'Disable', move);
return false;
}
},
onDisableMove(pokemon) {
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id === this.effectState.move) {
pokemon.disableMove(moveSlot.id);
}
}
},
},
},
doomdesire: {
inherit: true,
accuracy: 85,
basePower: 120,
onTry(source, target) {
if (!target.side.addSlotCondition(target, 'futuremove')) return false;
const moveData = {
name: "Doom Desire",
basePower: 120,
category: "Special",
flags: { metronome: 1, futuremove: 1 },
willCrit: false,
type: '???',
} as unknown as ActiveMove;
const damage = this.actions.getDamage(source, target, moveData, true);
Object.assign(target.side.slotConditions[target.position]['futuremove'], {
duration: 3,
move: 'doomdesire',
source,
moveData: {
id: 'doomdesire',
name: "Doom Desire",
accuracy: 85,
basePower: 0,
damage,
category: "Special",
flags: { metronome: 1, futuremove: 1 },
effectType: 'Move',
type: '???',
},
});
this.add('-start', source, 'Doom Desire');
return null;
},
},
doubleedge: {
inherit: true,
recoil: [1, 3],
},
drainpunch: {
inherit: true,
basePower: 60,
pp: 5,
},
dreameater: {
inherit: true,
onTryImmunity(target) {
return target.status === 'slp' && !target.volatiles['substitute'];
},
},
embargo: {
inherit: true,
flags: { protect: 1, mirror: 1, metronome: 1 },
onTryHit(pokemon) {
if (pokemon.ability === 'multitype' || pokemon.item === 'griseousorb') {
return false;
}
},
condition: {
duration: 5,
onStart(pokemon) {
this.add('-start', pokemon, 'Embargo');
},
// Item suppression implemented in Pokemon.ignoringItem() within sim/pokemon.js
onResidualOrder: 10,
onResidualSubOrder: 18,
onEnd(pokemon) {
this.add('-end', pokemon, 'Embargo');
},
},
},
encore: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1, failencore: 1 },
volatileStatus: 'encore',
condition: {
durationCallback() {
return this.random(4, 9);
},
onStart(target, source) {
const moveIndex = target.lastMove ? target.moves.indexOf(target.lastMove.id) : -1;
if (
!target.lastMove || target.lastMove.flags['failencore'] ||
!target.moveSlots[moveIndex] || target.moveSlots[moveIndex].pp <= 0
) {
// it failed
return false;
}
this.effectState.move = target.lastMove.id;
this.add('-start', target, 'Encore');
},
onOverrideAction(pokemon) {
return this.effectState.move;
},
onResidualOrder: 10,
onResidualSubOrder: 14,
onResidual(target) {
if (
target.moves.includes(this.effectState.move) &&
target.moveSlots[target.moves.indexOf(this.effectState.move)].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);
}
}
},
},
},
endeavor: {
inherit: true,
onTry(pokemon, target) {
if (pokemon.hp >= target.hp) {
this.add('-fail', pokemon);
return null;
}
},
},
extremespeed: {
inherit: true,
priority: 1,
},
fakeout: {
inherit: true,
priority: 1,
},
feint: {
inherit: true,
basePower: 50,
onTry(source, target) {
if (!target.volatiles['protect']) {
this.add('-fail', source);
return null;
}
},
},
firespin: {
inherit: true,
accuracy: 70,
basePower: 15,
},
flail: {
inherit: true,
basePowerCallback(pokemon) {
const ratio = Math.max(Math.floor(pokemon.hp * 64 / pokemon.maxhp), 1);
let bp;
if (ratio < 2) {
bp = 200;
} else if (ratio < 6) {
bp = 150;
} else if (ratio < 13) {
bp = 100;
} else if (ratio < 22) {
bp = 80;
} else if (ratio < 43) {
bp = 40;
} else {
bp = 20;
}
this.debug(`BP: ${bp}`);
return bp;
},
},
flareblitz: {
inherit: true,
recoil: [1, 3],
},
fling: {
inherit: true,
onPrepareHit(target, source, move) {
if (source.ignoringItem()) return false;
if (source.hasAbility('multitype')) return false;
const item = source.getItem();
if (!this.singleEvent('TakeItem', item, source.itemState, source, source, move, item)) return false;
if (!item.fling) return false;
move.basePower = item.fling.basePower;
this.debug(`BP: ${move.basePower}`);
if (item.isBerry) {
move.onHit = function (foe) {
if (this.singleEvent('Eat', item, null, foe, null, null)) {
this.runEvent('EatItem', foe, null, null, item);
if (item.id === 'leppaberry') foe.staleness = 'external';
}
if (item.onEat) foe.ateBerry = true;
};
} else if (item.fling.effect) {
move.onHit = item.fling.effect;
} else {
if (!move.secondaries) move.secondaries = [];
if (item.fling.status) {
move.secondaries.push({ status: item.fling.status });
} else if (item.fling.volatileStatus) {
move.secondaries.push({ volatileStatus: item.fling.volatileStatus });
}
}
source.addVolatile('fling');
},
},
focuspunch: {
inherit: true,
priorityChargeCallback() {},
beforeTurnCallback(pokemon) {
pokemon.addVolatile('focuspunch');
},
beforeMoveCallback() {},
onTry(pokemon) {
if (pokemon.volatiles['focuspunch']?.lostFocus) {
this.attrLastMove('[still]');
this.add('cant', pokemon, 'Focus Punch', 'Focus Punch');
return null;
}
},
},
foresight: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
furycutter: {
inherit: true,
basePower: 10,
condition: {
duration: 2,
onStart() {
this.effectState.multiplier = 1;
},
onRestart() {
if (this.effectState.multiplier < 16) {
this.effectState.multiplier <<= 1;
}
this.effectState.duration = 2;
},
},
},
futuresight: {
inherit: true,
accuracy: 90,
basePower: 80,
pp: 15,
onTry(source, target) {
if (!target.side.addSlotCondition(target, 'futuremove')) return false;
const moveData = {
name: "Future Sight",
basePower: 80,
category: "Special",
flags: { metronome: 1, futuremove: 1 },
willCrit: false,
type: '???',
} as unknown as ActiveMove;
const damage = this.actions.getDamage(source, target, moveData, true);
Object.assign(target.side.slotConditions[target.position]['futuremove'], {
duration: 3,
move: 'futuresight',
source,
moveData: {
id: 'futuresight',
name: "Future Sight",
accuracy: 90,
basePower: 0,
damage,
category: "Special",
flags: { metronome: 1, futuremove: 1 },
effectType: 'Move',
type: '???',
},
});
this.add('-start', source, 'Future Sight');
return null;
},
},
gigadrain: {
inherit: true,
basePower: 60,
},
glare: {
inherit: true,
accuracy: 75,
},
gravity: {
inherit: true,
condition: {
duration: 5,
durationCallback(source, effect) {
if (source?.hasAbility('persistent')) {
this.add('-activate', source, 'ability: Persistent', '[move] Gravity');
return 7;
}
return 5;
},
onFieldStart(target, source) {
if (source?.hasAbility('persistent')) {
this.add('-fieldstart', 'move: Gravity', '[persistent]');
} else {
this.add('-fieldstart', 'move: Gravity');
}
for (const pokemon of this.getAllActive()) {
let applies = false;
if (pokemon.removeVolatile('bounce') || pokemon.removeVolatile('fly')) {
applies = true;
this.queue.cancelMove(pokemon);
pokemon.removeVolatile('twoturnmove');
}
if (pokemon.volatiles['skydrop']) {
applies = true;
this.queue.cancelMove(pokemon);
if (pokemon.volatiles['skydrop'].source) {
this.add('-end', pokemon.volatiles['twoturnmove'].source, 'Sky Drop', '[interrupt]');
}
pokemon.removeVolatile('skydrop');
pokemon.removeVolatile('twoturnmove');
}
if (pokemon.volatiles['magnetrise']) {
applies = true;
delete pokemon.volatiles['magnetrise'];
}
if (pokemon.volatiles['telekinesis']) {
applies = true;
delete pokemon.volatiles['telekinesis'];
}
if (applies) this.add('-activate', pokemon, 'move: Gravity');
}
},
onModifyAccuracy(accuracy) {
if (typeof accuracy !== 'number') return;
return this.chainModify([6840, 4096]);
},
onDisableMove(pokemon) {
for (const moveSlot of pokemon.moveSlots) {
if (this.dex.moves.get(moveSlot.id).flags['gravity']) {
pokemon.disableMove(moveSlot.id);
}
}
},
// groundedness implemented in battle.engine.js:BattlePokemon#isGrounded
onBeforeMovePriority: 6,
onBeforeMove(pokemon, target, move) {
if (move.flags['gravity'] && !move.isZ) {
this.add('cant', pokemon, 'move: Gravity', move);
return false;
}
},
onModifyMove(move, pokemon, target) {
if (move.flags['gravity'] && !move.isZ) {
this.add('cant', pokemon, 'move: Gravity', move);
return false;
}
},
onFieldResidualOrder: 9,
onFieldEnd() {
this.add('-fieldend', 'move: Gravity');
},
},
},
growth: {
inherit: true,
onModifyMove() {},
boosts: {
spa: 1,
},
},
healbell: {
inherit: true,
onHit(target, source) {
this.add('-activate', source, 'move: Heal Bell');
const allies = [...target.side.pokemon, ...target.side.allySide?.pokemon || []];
for (const ally of allies) {
if (ally.hasAbility('soundproof')) {
if (ally.isActive) this.add('-immune', ally, '[from] ability: Soundproof');
continue;
}
ally.cureStatus(true);
}
},
},
healblock: {
inherit: true,
flags: { protect: 1, mirror: 1, metronome: 1 },
condition: {
duration: 5,
durationCallback(target, source, effect) {
if (source?.hasAbility('persistent')) {
this.add('-activate', source, 'ability: Persistent', '[move] Heal Block');
return 7;
}
return 5;
},
onStart(pokemon) {
this.add('-start', pokemon, 'move: Heal Block');
},
onDisableMove(pokemon) {
for (const moveSlot of pokemon.moveSlots) {
if (this.dex.moves.get(moveSlot.id).flags['heal']) {
pokemon.disableMove(moveSlot.id);
}
}
},
onBeforeMovePriority: 6,
onBeforeMove(pokemon, target, move) {
if (move.flags['heal']) {
this.add('cant', pokemon, 'move: Heal Block', move);
return false;
}
},
onResidualOrder: 10,
onResidualSubOrder: 17,
onEnd(pokemon) {
this.add('-end', pokemon, 'move: Heal Block');
},
onTryHeal(damage, pokemon, source, effect) {
if (effect && (effect.id === 'drain' || effect.id === 'leechseed' || effect.id === 'wish')) {
return false;
}
},
},
},
healingwish: {
inherit: true,
flags: { heal: 1, metronome: 1 },
onAfterMove(pokemon) {
pokemon.switchFlag = true;
},
condition: {
duration: 1,
onSwitchInPriority: -1,
onSwitchIn(target) {
if (target.hp > 0) {
target.heal(target.maxhp);
target.clearStatus();
this.add('-heal', target, target.getHealth, '[from] move: Healing Wish');
target.side.removeSlotCondition(target, 'healingwish');
target.lastMove = this.lastMove;
} else {
target.switchFlag = true;
}
},
},
},
highjumpkick: {
inherit: true,
basePower: 100,
pp: 20,
onMoveFail(target, source, move) {
move.causedCrashDamage = true;
let damage = this.actions.getDamage(source, target, move, true);
if (!damage) damage = target.maxhp;
this.damage(this.clampIntRange(damage / 2, 1, Math.floor(target.maxhp / 2)), source, source, move);
},
},
iciclespear: {
inherit: true,
basePower: 10,
},
imprison: {
inherit: true,
flags: { bypasssub: 1, metronome: 1 },
onTryHit(pokemon) {
for (const target of pokemon.foes()) {
for (const move of pokemon.moves) {
if (target.moves.includes(move)) return;
}
}
return false;
},
},
ingrain: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'move: Ingrain');
},
onResidualOrder: 10,
onResidualSubOrder: 1,
onResidual(pokemon) {
this.heal(pokemon.baseMaxhp / 16);
},
onTrapPokemon(pokemon) {
pokemon.tryTrap();
},
// groundedness implemented in battle.engine.js:BattlePokemon#isGrounded
onDragOut(pokemon) {
this.add('-activate', pokemon, 'move: Ingrain');
return null;
},
},
},
jumpkick: {
inherit: true,
basePower: 85,
pp: 25,
onMoveFail(target, source, move) {
move.causedCrashDamage = true;
let damage = this.actions.getDamage(source, target, move, true);
if (!damage) damage = target.maxhp;
this.damage(this.clampIntRange(damage / 2, 1, Math.floor(target.maxhp / 2)), source, source, move);
},
},
knockoff: {
inherit: true,
onAfterHit(target, source, move) {
if (!target.item || target.itemState.knockedOff) return;
if (target.ability === 'multitype') return;
const item = target.getItem();
if (this.runEvent('TakeItem', target, source, move, item)) {
target.itemState.knockedOff = true;
this.add('-enditem', target, item.name, '[from] move: Knock Off', `[of] ${source}`);
this.hint("In Gens 3-4, Knock Off only makes the target's item unusable; it cannot obtain a new item.", true);
}
},
},
lastresort: {
inherit: true,
basePower: 130,
},
leechseed: {
inherit: true,
condition: {
onStart(target) {
this.add('-start', target, 'move: Leech Seed');
},
onResidualOrder: 10,
onResidualSubOrder: 5,
onResidual(pokemon) {
const target = this.getAtSlot(pokemon.volatiles['leechseed'].sourceSlot);
if (!target || target.fainted || target.hp <= 0) {
this.debug('Nothing to leech into');
return;
}
const damage = this.damage(pokemon.baseMaxhp / 8, pokemon, target);
if (damage) {
this.heal(damage, target, pokemon);
}
},
},
},
lightscreen: {
inherit: true,
condition: {
duration: 5,
durationCallback(target, source, effect) {
if (source?.hasItem('lightclay')) {
return 8;
}
return 5;
},
onAnyModifyDamagePhase1(damage, source, target, move) {
if (target !== source && this.effectState.target.hasAlly(target) && this.getCategory(move) === 'Special') {
if (!target.getMoveHitData(move).crit && !move.infiltrates) {
this.debug('Light Screen weaken');
if (target.alliesAndSelf().length > 1) return this.chainModify(2, 3);
return this.chainModify(0.5);
}
}
},
onSideStart(side) {
this.add('-sidestart', side, 'Light Screen');
},
onSideResidualOrder: 2,
onSideEnd(side) {
this.add('-sideend', side, 'Light Screen');
},
},
},
lockon: {
inherit: true,
condition: {
duration: 2,
onSourceInvulnerabilityPriority: 1,
onSourceInvulnerability(target, source, move) {
if (move && source === this.effectState.target && target === this.effectState.source) return 0;
},
onSourceAccuracy(accuracy, target, source, move) {
if (move && source === this.effectState.target && target === this.effectState.source) return true;
},
},
},
luckychant: {
inherit: true,
flags: { metronome: 1 },
condition: {
duration: 5,
onSideStart(side) {
this.add('-sidestart', side, 'move: Lucky Chant');
},
onCriticalHit: false,
onSideResidualOrder: 6,
onSideEnd(side) {
this.add('-sideend', side, 'move: Lucky Chant');
},
},
},
lunardance: {
inherit: true,
flags: { heal: 1, metronome: 1 },
onAfterMove(pokemon) {
pokemon.switchFlag = true;
},
condition: {
duration: 1,
onSideStart(side) {
this.debug('Lunar Dance started on ' + side.name);
},
onSwitchInPriority: -1,
onSwitchIn(target) {
if (target.getSlot() !== this.effectState.sourceSlot) {
return;
}
if (target.hp > 0) {
target.heal(target.maxhp);
target.clearStatus();
for (const moveSlot of target.moveSlots) {
moveSlot.pp = moveSlot.maxpp;
}
this.add('-heal', target, target.getHealth, '[from] move: Lunar Dance');
target.side.removeSlotCondition(target, 'lunardance');
target.lastMove = this.lastMove;
} else {
target.switchFlag = true;
}
},
},
},
magiccoat: {
inherit: true,
condition: {
duration: 1,
onTryHitPriority: 2,
onTryHit(target, source, move) {
if (target === source || move.hasBounced || !move.flags['reflectable']) {
return;
}
target.removeVolatile('magiccoat');
const newMove = this.dex.getActiveMove(move.id);
newMove.hasBounced = true;
this.actions.useMove(newMove, target, { target: source });
return null;
},
},
},
magmastorm: {
inherit: true,
accuracy: 70,
},
magnetrise: {
inherit: true,
flags: { gravity: 1, metronome: 1 },
volatileStatus: 'magnetrise',
condition: {
duration: 5,
onStart(target) {
if (target.volatiles['ingrain'] || target.ability === 'levitate') return false;
this.add('-start', target, 'Magnet Rise');
},
onImmunity(type) {
if (type === 'Ground') return false;
},
onResidualOrder: 10,
onResidualSubOrder: 16,
onEnd(target) {
this.add('-end', target, 'Magnet Rise');
},
},
},
mefirst: {
inherit: true,
condition: {
duration: 1,
onModifyDamagePhase2(damage) {
return damage * 1.5;
},
},
},
metalburst: {
inherit: true,
flags: { protect: 1, mirror: 1, metronome: 1 },
},
metronome: {
inherit: true,
flags: { noassist: 1, failcopycat: 1, nosleeptalk: 1, failmimic: 1 },
onHit(pokemon) {
const moves = this.dex.moves.all().filter(move => (
(![2, 4].includes(this.gen) || !pokemon.moves.includes(move.id)) &&
(!move.isNonstandard || move.isNonstandard === 'Unobtainable') &&
move.flags['metronome'] &&
!(this.field.pseudoWeather['gravity'] && move.flags['gravity']) &&
!(pokemon.volatiles['healblock'] && move.flags['heal'])
));
let randomMove = '';
if (moves.length) {
moves.sort((a, b) => a.num - b.num);
randomMove = this.sample(moves).id;
}
if (!randomMove) return false;
pokemon.side.lastSelectedMove = this.toID(randomMove);
this.actions.useMove(randomMove, pokemon);
},
},
mimic: {
inherit: true,
flags: {
protect: 1, allyanim: 1, noassist: 1, failcopycat: 1, failencore: 1, failinstruct: 1, failmimic: 1,
},
onHit(target, source) {
if (source.transformed || !target.lastMove || target.volatiles['substitute']) {
return false;
}
if (target.lastMove.flags['failmimic'] || source.moves.includes(target.lastMove.id)) {
return false;
}
const mimicIndex = source.moves.indexOf('mimic');
if (mimicIndex < 0) return false;
const move = this.dex.moves.get(target.lastMove.id);
source.moveSlots[mimicIndex] = {
move: move.name,
id: move.id,
pp: 5,
maxpp: move.pp * 8 / 5,
disabled: false,
used: false,
virtual: true,
};
this.add('-activate', source, 'move: Mimic', move.name);
},
},
minimize: {
inherit: true,
boosts: {
evasion: 1,
},
},
miracleeye: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
mirrormove: {
inherit: true,
onTryHit() {},
onHit(pokemon) {
const lastAttackedBy = pokemon.getLastAttackedBy();
if (!lastAttackedBy?.source.lastMove || !lastAttackedBy.move) {
return false;
}
const 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 (noMirror.includes(lastAttackedBy.move) || !lastAttackedBy.source.hasMove(lastAttackedBy.move)) {
return false;
}
this.actions.useMove(lastAttackedBy.move, pokemon);
},
target: "self",
},
mist: {
inherit: true,
condition: {
duration: 5,
onTryBoost(boost, target, source, effect) {
if (effect.effectType === 'Move' && effect.infiltrates && !target.isAlly(source)) return;
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');
}
}
},
onSideStart(side) {
this.add('-sidestart', side, 'Mist');
},
onSideResidualOrder: 3,
onSideEnd(side) {
this.add('-sideend', side, 'Mist');
},
},
},
moonlight: {
inherit: true,
onHit(pokemon) {
if (this.field.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp * 2 / 3);
} 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 * 2 / 3);
} else if (this.field.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.baseMaxhp / 4);
} else {
this.heal(pokemon.baseMaxhp / 2);
}
},
},
mudsport: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'move: Mud Sport');
},
onAnyBasePowerPriority: 3,
onAnyBasePower(basePower, user, target, move) {
if (move.type === 'Electric') {
this.debug('Mud Sport weaken');
return this.chainModify(0.5);
}
},
},
},
naturepower: {
inherit: true,
flags: { metronome: 1 },
onHit(pokemon) {
this.actions.useMove('triattack', pokemon);
},
},
nightmare: {
inherit: true,
condition: {
noCopy: true,
onStart(pokemon) {
if (pokemon.status !== 'slp' && !pokemon.hasAbility('comatose')) {
return false;
}
this.add('-start', pokemon, 'Nightmare');
},
onResidualOrder: 10,
onResidualSubOrder: 7,
onResidual(pokemon) {
this.damage(pokemon.baseMaxhp / 4);
},
},
},
odorsleuth: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
outrage: {
inherit: true,
pp: 15,
onAfterMove() {},
},
payback: {
inherit: true,
basePowerCallback(pokemon, target) {
if (this.queue.willMove(target)) {
return 50;
}
this.debug('BP doubled');
return 100;
},
},
payday: {
inherit: true,
onHit() {
this.add('-fieldactivate', 'move: Pay Day');
},
},
perishsong: {
inherit: true,
condition: {
duration: 4,
onEnd(target) {
this.add('-start', target, 'perish0');
target.faint();
},
onResidualOrder: 12,
onResidual(pokemon) {
const duration = pokemon.volatiles['perishsong'].duration;
this.add('-start', pokemon, `perish${duration}`);
},
},
},
petaldance: {
inherit: true,
basePower: 90,
pp: 20,
onAfterMove() {},
},
poisongas: {
inherit: true,
accuracy: 55,
target: "normal",
},
powertrick: {
inherit: true,
flags: { metronome: 1 },
},
protect: {
inherit: true,
priority: 3,
condition: {
duration: 1,
onStart(target) {
this.add('-singleturn', target, 'Protect');
},
onTryHitPriority: 3,
onTryHit(target, source, move) {
if (!move.flags['protect']) return;
this.add('-activate', target, 'Protect');
const lockedmove = source.getVolatile('lockedmove');
if (lockedmove) {
// Outrage counter is NOT reset
if (source.volatiles['lockedmove'].trueDuration >= 2) {
source.volatiles['lockedmove'].duration = 2;
}
}
return null;
},
},
},
psychup: {
inherit: true,
flags: { snatch: 1, bypasssub: 1, metronome: 1 },
},
pursuit: {
inherit: true,
condition: {
duration: 1,
onBeforeSwitchOut(pokemon) {
this.debug('Pursuit start');
let alreadyAdded = false;
for (const source of this.effectState.sources) {
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));
}
},
},
},
rapidspin: {
inherit: true,
self: {
onHit(pokemon) {
if (pokemon.removeVolatile('leechseed')) {
this.add('-end', pokemon, 'Leech Seed', '[from] move: Rapid Spin', `[of] ${pokemon}`);
}
const sideConditions = ['spikes', 'toxicspikes', 'stealthrock', 'stickyweb'];
for (const condition of sideConditions) {
if (pokemon.side.removeSideCondition(condition)) {
this.add('-sideend', pokemon.side, this.dex.conditions.get(condition).name, '[from] move: Rapid Spin', `[of] ${pokemon}`);
}
}
if (pokemon.volatiles['partiallytrapped']) {
pokemon.removeVolatile('partiallytrapped');
}
},
},
},
recycle: {
inherit: true,
flags: { metronome: 1 },
},
reflect: {
inherit: true,
condition: {
duration: 5,
durationCallback(target, source, effect) {
if (source?.hasItem('lightclay')) {
return 8;
}
return 5;
},
onAnyModifyDamagePhase1(damage, source, target, move) {
if (target !== source && this.effectState.target.hasAlly(target) && this.getCategory(move) === 'Physical') {
if (!target.getMoveHitData(move).crit && !move.infiltrates) {
this.debug('Reflect weaken');
if (target.alliesAndSelf().length > 1) return this.chainModify(2, 3);
return this.chainModify(0.5);
}
}
},
onSideStart(side) {
this.add('-sidestart', side, 'Reflect');
},
onSideResidualOrder: 1,
onSideEnd(side) {
this.add('-sideend', side, 'Reflect');
},
},
},
reversal: {
inherit: true,
basePowerCallback(pokemon) {
const ratio = Math.max(Math.floor(pokemon.hp * 64 / pokemon.maxhp), 1);
let bp;
if (ratio < 2) {
bp = 200;
} else if (ratio < 6) {
bp = 150;
} else if (ratio < 13) {
bp = 100;
} else if (ratio < 22) {
bp = 80;
} else if (ratio < 43) {
bp = 40;
} else {
bp = 20;
}
this.debug(`BP: ${bp}`);
return bp;
},
},
roar: {
inherit: true,
flags: { protect: 1, mirror: 1, sound: 1, bypasssub: 1, metronome: 1 },
},
rockblast: {
inherit: true,
accuracy: 80,
},
roleplay: {
inherit: true,
onTryHit(target, source) {
if (target.ability === source.ability || source.hasItem('griseousorb')) return false;
if (target.getAbility().flags['failroleplay'] || source.ability === 'multitype') {
return false;
}
},
},
safeguard: {
inherit: true,
condition: {
duration: 5,
durationCallback(target, source, effect) {
if (source?.hasAbility('persistent')) {
this.add('-activate', source, 'ability: Persistent', '[move] Safeguard');
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, source) {
if (source?.hasAbility('persistent')) {
this.add('-sidestart', side, 'Safeguard', '[persistent]');
} else {
this.add('-sidestart', side, 'Safeguard');
}
},
onSideResidualOrder: 4,
onSideEnd(side) {
this.add('-sideend', side, 'Safeguard');
},
},
},
sandtomb: {
inherit: true,
accuracy: 70,
basePower: 15,
},
scaryface: {
inherit: true,
accuracy: 90,
},
secretpower: {
inherit: true,
secondary: {
chance: 30,
status: 'par',
},
},
sketch: {
inherit: true,
flags: {
bypasssub: 1, allyanim: 1, failencore: 1, noassist: 1,
failcopycat: 1, failinstruct: 1, failmimic: 1, nosketch: 1,
},
onHit(target, source) {
if (source.transformed || !target.lastMove || target.volatiles['substitute']) {
return false;
}
if (target.lastMove.flags['nosketch'] || source.moves.includes(target.lastMove.id)) {
return false;
}
const sketchIndex = source.moves.indexOf('sketch');
if (sketchIndex < 0) return false;
const move = this.dex.moves.get(target.lastMove.id);
const sketchedMove = {
move: move.name,
id: move.id,
pp: move.pp,
maxpp: move.pp,
disabled: false,
used: false,
};
source.moveSlots[sketchIndex] = sketchedMove;
source.baseMoveSlots[sketchIndex] = sketchedMove;
this.add('-activate', source, 'move: Mimic', move.name);
},
},
skillswap: {
inherit: true,
onHit(target, source) {
const targetAbility = target.ability;
const sourceAbility = source.ability;
if (targetAbility === sourceAbility || source.hasItem('griseousorb') || target.hasItem('griseousorb')) {
return false;
}
this.add('-activate', source, 'move: Skill Swap');
source.setAbility(targetAbility);
target.setAbility(sourceAbility);
},
},
sleeptalk: {
inherit: true,
onTryHit(pokemon) {
return !pokemon.volatiles['choicelock'] && !pokemon.volatiles['encore'];
},
},
snatch: {
inherit: true,
flags: { bypasssub: 1, noassist: 1, failcopycat: 1 },
condition: {
duration: 1,
onStart(pokemon) {
this.add('-singleturn', pokemon, 'Snatch');
},
onAnyPrepareHitPriority: -1,
onAnyPrepareHit(source, target, move) {
const snatchUser = this.effectState.source;
if (snatchUser.isSkyDropped()) return;
if (!move || move.isZ || move.isMax || !move.flags['snatch']) {
return;
}
snatchUser.removeVolatile('snatch');
this.add('-activate', snatchUser, 'move: Snatch', `[of] ${source}`);
this.actions.useMove(move.id, snatchUser);
return null;
},
},
},
snore: {
inherit: true,
flags: { protect: 1, mirror: 1, sound: 1, metronome: 1 },
},
spikes: {
inherit: true,
flags: { metronome: 1, mustpressure: 1 },
condition: {
// this is a side condition
onSideStart(side) {
this.add('-sidestart', side, 'Spikes');
this.effectState.layers = 1;
},
onSideRestart(side) {
if (this.effectState.layers >= 3) return false;
this.add('-sidestart', side, 'Spikes');
this.effectState.layers++;
},
onEntryHazard(pokemon) {
if (!pokemon.isGrounded() || pokemon.hasItem('heavydutyboots')) return;
const damageAmounts = [0, 3, 4, 6]; // 1/8, 1/6, 1/4
this.damage(damageAmounts[this.effectState.layers] * pokemon.maxhp / 24);
},
},
},
spite: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
stealthrock: {
inherit: true,
flags: { metronome: 1, mustpressure: 1 },
condition: {
// this is a side condition
onSideStart(side) {
this.add('-sidestart', side, 'move: Stealth Rock');
},
onEntryHazard(pokemon) {
if (pokemon.hasItem('heavydutyboots')) return;
const typeMod = this.clampIntRange(pokemon.runEffectiveness(this.dex.getActiveMove('stealthrock')), -6, 6);
this.damage(pokemon.maxhp * 2 ** typeMod / 8);
},
},
},
struggle: {
inherit: true,
flags: {
contact: 1, protect: 1, failencore: 1, failmefirst: 1,
noassist: 1, failcopycat: 1, failinstruct: 1, failmimic: 1, nosketch: 1,
},
onModifyMove(move) {
move.type = '???';
},
},
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 (target === source || move.flags['bypasssub']) {
return;
}
let damage = this.actions.getDamage(source, target, move);
if (!damage && damage !== 0) {
this.add('-fail', source);
this.attrLastMove('[still]');
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');
target.addVolatile('substitutebroken');
if (target.volatiles['substitutebroken']) target.volatiles['substitutebroken'].move = move.id;
if (move.ohko) this.add('-ohko');
} else {
this.add('-activate', target, 'Substitute', '[damage]');
}
if (move.recoil && damage) {
this.damage(this.actions.calcRecoilDamage(damage, move, source), source, target, 'recoil');
}
if (move.drain) {
this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain');
}
this.runEvent('AfterSubDamage', target, source, move, damage);
return this.HIT_SUBSTITUTE;
},
onEnd(target) {
this.add('-end', target, 'Substitute');
},
},
},
suckerpunch: {
inherit: true,
onTry(source, target) {
const action = this.queue.willMove(target);
if (!action || action.choice !== 'move' || action.move.category === 'Status' || target.volatiles['mustrecharge']) {
this.add('-fail', source);
return null;
}
},
},
swallow: {
inherit: true,
onTry(source) {
return !!source.volatiles['stockpile'];
},
},
switcheroo: {
inherit: true,
onTryHit(target, source, move) {
if (target.hasAbility('multitype') || source.hasAbility('multitype')) return false;
},
},
synthesis: {
inherit: true,
onHit(pokemon) {
if (this.field.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp * 2 / 3);
} else if (this.field.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.baseMaxhp / 4);
} else {
this.heal(pokemon.baseMaxhp / 2);
}
},
},
tackle: {
inherit: true,
accuracy: 95,
basePower: 35,
},
tailglow: {
inherit: true,
boosts: {
spa: 2,
},
},
tailwind: {
inherit: true,
condition: {
duration: 3,
durationCallback(target, source, effect) {
if (source?.hasAbility('persistent')) {
this.add('-activate', source, 'ability: Persistent', '[move] Tailwind');
return 5;
}
return 3;
},
onSideStart(side, source) {
if (source?.hasAbility('persistent')) {
this.add('-sidestart', side, 'move: Tailwind', '[persistent]');
} else {
this.add('-sidestart', side, 'move: Tailwind');
}
},
onModifySpe(spe) {
return spe * 2;
},
onSideResidualOrder: 5,
onSideEnd(side) {
this.add('-sideend', side, 'move: Tailwind');
},
},
},
taunt: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
condition: {
durationCallback() {
return this.random(3, 6);
},
onStart(target) {
this.add('-start', target, 'move: Taunt');
},
onResidualOrder: 10,
onResidualSubOrder: 15,
onEnd(target) {
this.add('-end', target, 'move: Taunt');
},
onDisableMove(pokemon) {
for (const moveSlot of pokemon.moveSlots) {
if (this.dex.moves.get(moveSlot.id).category === 'Status') {
pokemon.disableMove(moveSlot.id);
}
}
},
onBeforeMovePriority: 5,
onBeforeMove(attacker, defender, move) {
if (move.category === 'Status') {
this.add('cant', attacker, 'move: Taunt', move);
return false;
}
},
},
},
thrash: {
inherit: true,
basePower: 90,
pp: 20,
onAfterMove() {},
},
torment: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
toxic: {
inherit: true,
accuracy: 85,
},
toxicspikes: {
inherit: true,
flags: { metronome: 1, mustpressure: 1 },
condition: {
// this is a side condition
onSideStart(side) {
this.add('-sidestart', side, 'move: Toxic Spikes');
this.effectState.layers = 1;
},
onSideRestart(side) {
if (this.effectState.layers >= 2) return false;
this.add('-sidestart', side, 'move: Toxic Spikes');
this.effectState.layers++;
},
onEntryHazard(pokemon) {
if (!pokemon.isGrounded()) return;
if (pokemon.hasType('Poison')) {
this.add('-sideend', pokemon.side, 'move: Toxic Spikes', `[of] ${pokemon}`);
pokemon.side.removeSideCondition('toxicspikes');
} else if (pokemon.volatiles['substitute'] || pokemon.hasType('Steel')) {
// do nothing
} else if (this.effectState.layers >= 2) {
pokemon.trySetStatus('tox', pokemon.side.foe.active[0]);
} else {
pokemon.trySetStatus('psn', pokemon.side.foe.active[0]);
}
},
},
},
transform: {
inherit: true,
flags: { bypasssub: 1, metronome: 1, failencore: 1 },
},
trick: {
inherit: true,
onTryHit(target, source, move) {
if (target.hasAbility('multitype') || source.hasAbility('multitype')) return false;
},
},
trickroom: {
inherit: true,
condition: {
duration: 5,
durationCallback(source, effect) {
if (source?.hasAbility('persistent')) {
this.add('-activate', source, 'ability: Persistent', '[move] Trick Room');
return 7;
}
return 5;
},
onFieldStart(target, source) {
if (source?.hasAbility('persistent')) {
this.add('-fieldstart', 'move: Trick Room', `[of] ${source}`, '[persistent]');
} else {
this.add('-fieldstart', 'move: Trick Room', `[of] ${source}`);
}
},
onFieldRestart(target, source) {
this.field.removePseudoWeather('trickroom');
},
// Speed modification is changed in Pokemon.getActionSpeed() in sim/pokemon.js
onFieldResidualOrder: 13,
onFieldEnd() {
this.add('-fieldend', 'move: Trick Room');
},
},
},
uproar: {
inherit: true,
basePower: 50,
condition: {
onStart(target) {
this.add('-start', target, 'Uproar');
// 3-6 turns
this.effectState.duration = this.random(3, 7);
},
onResidual(target) {
if (target.volatiles['throatchop']) {
target.removeVolatile('uproar');
return;
}
if (target.lastMove && target.lastMove.id === 'struggle') {
// don't lock
delete target.volatiles['uproar'];
}
this.add('-start', target, 'Uproar', '[upkeep]');
},
onResidualOrder: 10,
onResidualSubOrder: 11,
onEnd(target) {
this.add('-end', target, 'Uproar');
},
onLockMove: 'uproar',
onAnySetStatus(status, pokemon) {
if (status.id === 'slp') {
if (pokemon === this.effectState.target) {
this.add('-fail', pokemon, 'slp', '[from] Uproar', '[msg]');
} else {
this.add('-fail', pokemon, 'slp', '[from] Uproar');
}
return null;
}
},
},
},
volttackle: {
inherit: true,
recoil: [1, 3],
},
watersport: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'move: Water Sport');
},
onAnyBasePowerPriority: 3,
onAnyBasePower(basePower, user, target, move) {
if (move.type === 'Fire') {
this.debug('Water Sport weaken');
return this.chainModify(0.5);
}
},
},
},
whirlpool: {
inherit: true,
accuracy: 70,
basePower: 15,
},
whirlwind: {
inherit: true,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
},
wish: {
inherit: true,
flags: { heal: 1, metronome: 1 },
slotCondition: 'Wish',
condition: {
duration: 2,
onResidualOrder: 7,
onEnd(target) {
if (!target.fainted) {
const source = this.effectState.source;
const damage = this.heal(target.baseMaxhp / 2, target, target);
if (damage) this.add('-heal', target, target.getHealth, '[from] move: Wish', '[wisher] ' + source.name);
}
},
},
},
woodhammer: {
inherit: true,
recoil: [1, 3],
},
worryseed: {
inherit: true,
onTryHit(pokemon) {
const bannedAbilities = ['multitype', 'truant'];
if (bannedAbilities.includes(pokemon.ability) || pokemon.hasItem('griseousorb')) {
return false;
}
},
},
wrap: {
inherit: true,
accuracy: 85,
},
wringout: {
inherit: true,
basePowerCallback(pokemon, target) {
const bp = Math.floor(target.hp * 120 / target.maxhp) + 1;
this.debug(`BP for ${target.hp}/${target.maxhp} HP: ${bp}`);
return bp;
},
},
yawn: {
inherit: true,
condition: {
noCopy: true, // doesn't get copied by Baton Pass
duration: 2,
onStart(target, source) {
this.add('-start', target, 'move: Yawn', `[of] ${source}`);
},
onResidualOrder: 10,
onResidualSubOrder: 19,
onEnd(target) {
this.add('-end', target, 'move: Yawn', '[silent]');
target.trySetStatus('slp', this.effectState.source);
},
},
},
};