pokemon-showdown/mods/gen2/moves.js

731 lines
20 KiB
JavaScript

/**
* Gen 2 moves
*/
'use strict';
exports.BattleMovedex = {
aeroblast: {
inherit: true,
critRatio: 3,
},
bellydrum: {
inherit: true,
onHit: function (target) {
if (target.boosts.atk >= 6) {
return false;
}
if (target.hp <= target.maxhp / 2) {
this.boost({atk: 2}, null, null, this.getEffect('bellydrum2'));
return false;
}
this.directDamage(target.maxhp / 2);
this.boost({atk: 12});
},
},
bide: {
inherit: true,
effect: {
duration: 3,
durationCallback: function (target, source, effect) {
return this.random(3, 5);
},
onLockMove: 'bide',
onStart: function (pokemon) {
this.effectData.totalDamage = 0;
this.add('-start', pokemon, 'move: Bide');
},
onDamagePriority: -101,
onDamage: function (damage, target, source, move) {
if (!move || move.effectType !== 'Move' || !source) return;
this.effectData.totalDamage += damage;
this.effectData.lastDamageSource = source;
},
onBeforeMove: function (pokemon, target, move) {
if (this.effectData.duration === 1) {
this.add('-end', pokemon, 'move: Bide');
if (!this.effectData.totalDamage) {
this.add('-fail', pokemon);
return false;
}
target = this.effectData.lastDamageSource;
if (!target) {
this.add('-fail', pokemon);
return false;
}
if (!target.isActive) target = this.resolveTarget(pokemon, this.getMove('pound'));
if (!this.isAdjacent(pokemon, target)) {
this.add('-miss', pokemon, target);
return false;
}
let moveData = {
id: 'bide',
name: "Bide",
accuracy: 100,
damage: this.effectData.totalDamage * 2,
category: "Physical",
priority: 0,
flags: {contact: 1, protect: 1},
effectType: 'Move',
type: 'Normal',
};
this.tryMoveHit(target, pokemon, moveData);
return false;
}
this.add('-activate', pokemon, 'move: Bide');
},
onEnd: function (pokemon) {
this.add('-end', pokemon, 'move: Bide', '[silent]');
},
},
},
counter: {
inherit: true,
damageCallback: function (pokemon, target) {
if (pokemon.lastAttackedBy && pokemon.lastAttackedBy.thisTurn && (this.getCategory(pokemon.lastAttackedBy.move) === 'Physical' || this.getMove(pokemon.lastAttackedBy.move).id === 'hiddenpower') && target.lastMove !== 'sleeptalk') {
return 2 * pokemon.lastAttackedBy.damage;
}
return false;
},
beforeTurnCallback: function () {},
onTryHit: function () {},
effect: {},
priority: -1,
},
crabhammer: {
inherit: true,
critRatio: 3,
},
crosschop: {
inherit: true,
critRatio: 3,
},
curse: {
inherit: true,
effect: {
onStart: function (pokemon, source) {
this.add('-start', pokemon, 'Curse', '[of] ' + source);
},
onAfterMoveSelf: function (pokemon) {
this.damage(pokemon.maxhp / 4);
},
},
},
detect: {
inherit: true,
desc: "The user is protected from attacks made by the opponent during this turn. This move has an X/256 chance of being successful, where X starts at 255 and halves, rounded down, each time this move is successfully used. X resets to 255 if this move fails or if the user's last move used is not Detect, Endure, or Protect. Fails if the user moves last this turn.",
priority: 2,
onTryHit: function (pokemon) {
if (!pokemon.volatiles['stall']) {
this.debug("Success chance: 99.6% (255/256)");
return (this.random(256) < 255);
}
},
},
dig: {
inherit: true,
effect: {
duration: 2,
onImmunity: function (type, pokemon) {
if (type === 'sandstorm') return false;
},
onAccuracy: function (accuracy, target, source, move) {
if (move.id === 'earthquake' || move.id === 'magnitude' || move.id === 'fissure') {
return;
}
if (source.volatiles['lockon'] && target === source.volatiles['lockon'].source) return;
return 0;
},
onSourceBasePower: function (basePower, target, source, move) {
if (move.id === 'earthquake' || move.id === 'magnitude') {
return this.chainModify(2);
}
},
},
},
doubleedge: {
inherit: true,
shortDesc: "Has 25% recoil.",
recoil: [25, 100],
},
encore: {
inherit: true,
effect: {
durationCallback: function () {
return this.random(3, 7);
},
onStart: function (target) {
let noEncore = {encore:1, metronome:1, mimic:1, mirrormove:1, sketch:1, sleeptalk:1, struggle:1, transform:1};
let moveIndex = target.moves.indexOf(target.lastMove);
if (!target.lastMove || noEncore[target.lastMove] || (target.moveset[moveIndex] && target.moveset[moveIndex].pp <= 0)) {
// it failed
this.add('-fail', target);
delete target.volatiles['encore'];
return;
}
this.effectData.move = target.lastMove;
this.add('-start', target, 'Encore');
if (!this.willMove(target)) {
this.effectData.duration++;
}
},
onOverrideDecision: function (pokemon) {
return this.effectData.move;
},
onResidualOrder: 13,
onResidual: function (target) {
if (target.moves.indexOf(target.lastMove) >= 0 && target.moveset[target.moves.indexOf(target.lastMove)].pp <= 0) {
// early termination if you run out of PP
delete target.volatiles.encore;
this.add('-end', target, 'Encore');
}
},
onEnd: function (target) {
this.add('-end', target, 'Encore');
},
onDisableMove: function (pokemon) {
if (!this.effectData.move || !pokemon.hasMove(this.effectData.move)) {
return;
}
for (let i = 0; i < pokemon.moveset.length; i++) {
if (pokemon.moveset[i].id !== this.effectData.move) {
pokemon.disableMove(pokemon.moveset[i].id);
}
}
},
},
},
endure: {
inherit: true,
desc: "The user will survive attacks made by the opponent during this turn with at least 1 HP. This move has an X/256 chance of being successful, where X starts at 255 and halves, rounded down, each time this move is successfully used. X resets to 255 if this move fails or if the user's last move used is not Detect, Endure, or Protect. Fails if the user moves last this turn.",
priority: 2,
onTryHit: function (pokemon) {
if (!pokemon.volatiles['stall']) {
this.debug("Success chance: 99.6% (255/256)");
return (this.random(256) < 255);
}
},
},
explosion: {
inherit: true,
basePower: 250,
noSketch: true,
},
flail: {
inherit: true,
noDamageVariance: true,
willCrit: false,
},
fly: {
inherit: true,
effect: {
duration: 2,
onAccuracy: function (accuracy, target, source, move) {
if (move.id === 'gust' || move.id === 'twister' || move.id === 'thunder' || move.id === 'whirlwind') {
return;
}
if (source.volatiles['lockon'] && target === source.volatiles['lockon'].source) return;
return 0;
},
onSourceBasePower: function (basePower, target, source, move) {
if (move.id === 'gust' || move.id === 'twister') {
return this.chainModify(2);
}
},
},
},
focusenergy: {
inherit: true,
effect: {
onStart: function (pokemon) {
this.add('-start', pokemon, 'move: Focus Energy');
},
onModifyCritRatio: function (critRatio) {
return critRatio + 1;
},
},
},
healbell: {
inherit: true,
onHit: function (target, source) {
this.add('-cureteam', source, '[from] move: Heal Bell');
source.side.pokemon.forEach(pokemon => pokemon.clearStatus());
},
},
highjumpkick: {
inherit: true,
onMoveFail: function (target, source, move) {
if (target.runImmunity('Fighting')) {
let damage = this.getDamage(source, target, move, true);
this.damage(this.clampIntRange(damage / 8, 1), source, source, 'highjumpkick');
}
},
},
jumpkick: {
inherit: true,
onMoveFail: function (target, source, move) {
if (target.runImmunity('Fighting')) {
let damage = this.getDamage(source, target, move, true);
this.damage(this.clampIntRange(damage / 8, 1), source, source, 'jumpkick');
}
},
},
karatechop: {
inherit: true,
critRatio: 3,
},
leechseed: {
inherit: true,
onHit: function () {},
effect: {
onStart: function (target) {
this.add('-start', target, 'move: Leech Seed');
},
onAfterMoveSelfPriority: 2,
onAfterMoveSelf: function (pokemon) {
let leecher = pokemon.side.foe.active[pokemon.volatiles['leechseed'].sourcePosition];
if (!leecher || leecher.fainted || leecher.hp <= 0) {
this.debug('Nothing to leech into');
return;
}
let toLeech = this.clampIntRange(pokemon.maxhp / 8, 1);
let damage = this.damage(toLeech, pokemon, leecher);
if (damage) {
this.heal(damage, leecher, pokemon);
}
},
},
},
lightscreen: {
inherit: true,
effect: {
duration: 5,
// Sp. Def boost applied directly in stat calculation
onStart: function (side) {
this.add('-sidestart', side, 'move: Light Screen');
},
onResidualOrder: 21,
onEnd: function (side) {
this.add('-sideend', side, 'move: Light Screen');
},
},
},
lowkick: {
inherit: true,
accuracy: 90,
basePower: 50,
basePowerCallback: function () {
return 50;
},
secondary: {
chance: 30,
volatileStatus: 'flinch',
},
},
metronome: {
inherit: true,
onHit: function (target) {
let moves = [];
for (let i in exports.BattleMovedex) {
let move = exports.BattleMovedex[i];
if (i !== move.id) continue;
if (move.isNonstandard) continue;
let noMetronome = {
counter:1, destinybond:1, detect:1, endure:1, metronome:1, mimic:1, mirrorcoat:1, protect:1, sketch:1, sleeptalk:1, struggle:1, thief:1,
};
if (!noMetronome[move.id] && move.num < 252) {
moves.push(move.id);
}
}
let randomMove = '';
if (moves.length) randomMove = moves[this.random(moves.length)];
if (!randomMove) return false;
this.useMove(randomMove, target);
},
noSketch: true,
},
mirrorcoat: {
inherit: true,
effect: {
duration: 1,
noCopy: true,
onStart: function (target, source, source2, move) {
this.effectData.position = null;
this.effectData.damage = 0;
},
onRedirectTarget: function (target, source, source2) {
if (source !== this.effectData.target) return;
return source.side.foe.active[this.effectData.position];
},
onDamagePriority: -101,
onDamage: function (damage, target, source, effect) {
if (effect && effect.effectType === 'Move' && source.side !== target.side && this.getCategory(effect.id) === 'Special' && target.lastMove !== 'sleeptalk') {
this.effectData.position = source.position;
this.effectData.damage = 2 * damage;
}
},
},
priority: -1,
},
mimic: {
inherit: true,
noSketch: true,
},
mirrormove: {
inherit: true,
onHit: function (pokemon) {
let noMirror = {metronome: 1, mimic: 1, mirrormove: 1, sketch: 1, sleeptalk: 1, transform: 1};
let foe = pokemon.side.foe.active[0];
if (!foe || !foe.lastMove || (!pokemon.activeTurns && !foe.moveThisTurn) || noMirror[foe.lastMove] || pokemon.moves.indexOf(foe.lastMove) >= 0) {
return false;
}
this.useMove(foe.lastMove, pokemon);
},
noSketch: true,
},
moonlight: {
inherit: true,
onHit: function (pokemon) {
if (this.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp);
} else if (this.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.maxhp / 4);
} else {
this.heal(pokemon.maxhp / 2);
}
},
},
morningsun: {
inherit: true,
onHit: function (pokemon) {
if (this.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp);
} else if (this.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.maxhp / 4);
} else {
this.heal(pokemon.maxhp / 2);
}
},
},
nightmare: {
inherit: true,
effect: {
noCopy: true,
onStart: function (pokemon) {
if (pokemon.status !== 'slp') {
return false;
}
this.add('-start', pokemon, 'Nightmare');
},
onAfterMoveSelfPriority: 1,
onAfterMoveSelf: function (pokemon) {
if (pokemon.status === 'slp') this.damage(pokemon.maxhp / 4);
},
onUpdate: function (pokemon) {
if (pokemon.status !== 'slp') {
pokemon.removeVolatile('nightmare');
this.add('-end', pokemon, 'Nightmare', '[silent]');
}
},
},
},
outrage: {
inherit: true,
onMoveFail: function (target, source, move) {
source.addVolatile('lockedmove');
},
onAfterMove: function (pokemon) {
if (pokemon.volatiles['lockedmove'] && pokemon.volatiles['lockedmove'].duration === 1) {
pokemon.removeVolatile('lockedmove');
}
},
},
petaldance: {
inherit: true,
onMoveFail: function (target, source, move) {
source.addVolatile('lockedmove');
},
onAfterMove: function (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,
desc: "The user is protected from attacks made by the opponent during this turn. This move has an X/256 chance of being successful, where X starts at 255 and halves, rounded down, each time this move is successfully used. X resets to 255 if this move fails or if the user's last move used is not Detect, Endure, or Protect. Fails if the user moves last this turn.",
priority: 2,
onTryHit: function (pokemon) {
if (!pokemon.volatiles['stall']) {
this.debug("Success chance: 99.6% (255/256)");
return (this.random(256) < 255);
}
},
},
psywave: {
inherit: true,
damageCallback: function (pokemon) {
return this.random(1, pokemon.level + Math.floor(pokemon.level / 2));
},
},
rage: {
// TODO
// Rage boosts in Gens 2-4 is for the duration of Rage only
// Disable does not build
inherit: true,
},
razorleaf: {
inherit: true,
critRatio: 3,
},
razorwind: {
inherit: true,
accuracy: 75,
critRatio: 3,
},
reflect: {
inherit: true,
effect: {
duration: 5,
// Defense boost applied directly in stat calculation
onStart: function (side) {
this.add('-sidestart', side, 'Reflect');
},
onResidualOrder: 21,
onEnd: function (side) {
this.add('-sideend', side, 'Reflect');
},
},
},
rest: {
inherit: true,
onHit: function (target) {
if (target.hp >= target.maxhp) return false;
if (!target.setStatus('slp') && target.status !== 'slp') return false;
target.statusData.time = 3;
target.statusData.startTime = 3;
target.statusData.source = target;
this.heal(target.maxhp);
this.add('-status', target, 'slp', '[from] move: Rest');
},
secondary: false,
},
reversal: {
inherit: true,
noDamageVariance: true,
willCrit: false,
},
roar: {
inherit: true,
onTryHit: function () {
for (let i = 0; i < this.queue.length; i++) {
// Roar only works if it is the last action in a turn, including when it's called by Sleep Talk
if (this.queue[i].choice === 'move' || this.queue[i].choice === 'switch') return false;
}
},
priority: -1,
},
selfdestruct: {
inherit: true,
basePower: 200,
noSketch: true,
},
sketch: {
inherit: true,
onHit: function () {
// Sketch always fails in Link Battles
this.add('-nothing');
},
},
skyattack: {
inherit: true,
critRatio: 1,
secondary: {},
},
slash: {
inherit: true,
critRatio: 3,
},
sleeptalk: {
inherit: true,
onHit: function (pokemon) {
let moves = [];
for (let i = 0; i < pokemon.moveset.length; i++) {
let move = pokemon.moveset[i].id;
let NoSleepTalk = {
bide:1, dig:1, fly:1, metronome:1, mirrormove:1,
skullbash:1, skyattack:1, sleeptalk:1, solarbeam:1, razorwind:1,
};
if (move && !NoSleepTalk[move]) {
moves.push(move);
}
}
let randomMove = '';
if (moves.length) randomMove = moves[this.random(moves.length)];
if (!randomMove) return false;
this.useMove(randomMove, pokemon);
},
noSketch: true,
},
solarbeam: {
inherit: true,
// Rain weakening done directly in the damage formula
onBasePower: function () {},
},
spikes: {
inherit: true,
effect: {
// this is a side condition
onStart: function (side) {
if (!this.effectData.layers || this.effectData.layers === 0) {
this.add('-sidestart', side, 'Spikes');
this.effectData.layers = 1;
} else {
return false;
}
},
onSwitchIn: function (pokemon) {
if (!pokemon.runImmunity('Ground')) return;
let damageAmounts = [0, 3];
this.damage(damageAmounts[this.effectData.layers] * pokemon.maxhp / 24);
},
},
},
substitute: {
inherit: true,
effect: {
onStart: function (target) {
this.add('-start', target, 'Substitute');
this.effectData.hp = Math.floor(target.maxhp / 4);
delete target.volatiles['partiallytrapped'];
},
onTryPrimaryHitPriority: -1,
onTryPrimaryHit: function (target, source, move) {
if (move.stallingMove) {
this.add('-fail', source);
return null;
}
if (target === source) {
this.debug('sub bypass: self hit');
return;
}
if (move.drain) {
this.add('-hint', "In Gold/Silver/Crystal, draining moves always miss against Substitute.");
this.add('-miss', source);
return null;
}
if (move.category === 'Status') {
let SubBlocked = {
leechseed:1, lockon:1, mindreader:1, nightmare:1, painsplit:1, sketch:1,
};
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[move.id]) {
this.add('-activate', target, 'Substitute', '[block] ' + move.name);
return null;
}
return;
}
let damage = this.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;
}
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(Math.round(damage * move.recoil[0] / move.recoil[1]), 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 0; // hit
},
onEnd: function (target) {
this.add('-end', target, 'Substitute');
},
},
},
synthesis: {
inherit: true,
onHit: function (pokemon) {
if (this.isWeather(['sunnyday', 'desolateland'])) {
this.heal(pokemon.maxhp);
} else if (this.isWeather(['raindance', 'primordialsea', 'sandstorm', 'hail'])) {
this.heal(pokemon.maxhp / 4);
} else {
this.heal(pokemon.maxhp / 2);
}
},
},
thrash: {
inherit: true,
onMoveFail: function (target, source, move) {
source.addVolatile('lockedmove');
},
onAfterMove: function (pokemon) {
if (pokemon.volatiles['lockedmove'] && pokemon.volatiles['lockedmove'].duration === 1) {
pokemon.removeVolatile('lockedmove');
}
},
},
toxic: {
inherit: true,
ignoreImmunity: false,
},
transform: {
inherit: true,
noSketch: true,
},
triattack: {
inherit: true,
secondary: {
chance: 20,
onHit: function (target, source) {
if (!target.hasType('Normal')) {
let result = this.random(3);
if (result === 0) {
target.trySetStatus('brn', source);
} else if (result === 1) {
target.trySetStatus('par', source);
} else {
target.trySetStatus('frz', source);
}
}
},
},
},
whirlwind: {
inherit: true,
onTryHit: function () {
for (let i = 0; i < this.queue.length; i++) {
// Whirlwind only works if it is the last action in a turn, including when it's called by Sleep Talk
if (this.queue[i].choice === 'move' || this.queue[i].choice === 'switch') return false;
}
},
priority: -1,
},
};