mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
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.
965 lines
24 KiB
TypeScript
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,
|
|
},
|
|
};
|