pokemon-showdown/data/mods/gen1/moves.ts
Kris Johnson 77b1596c13
Gen 1: Fix Bide duration
this.random is exclusive of the upper boundary, so gen 1 Bide was always calling 3 turn bide instead of 3-4 turns
2022-05-14 15:18:36 -06:00

909 lines
23 KiB
TypeScript

/**
* A lot of Gen 1 moves have to be updated due to different mechanics.
* Some moves have had major changes, such as Bite's typing.
*/
export const Moves: {[k: string]: ModdedMoveData} = {
acid: {
inherit: true,
secondary: {
chance: 33,
boosts: {
def: -1,
},
},
target: "normal",
},
amnesia: {
inherit: true,
boosts: {
spd: 2,
spa: 2,
},
},
aurorabeam: {
inherit: true,
secondary: {
chance: 33,
boosts: {
atk: -1,
},
},
},
bide: {
inherit: true,
priority: 0,
accuracy: true,
ignoreEvasion: true,
condition: {
duration: 2,
durationCallback(target, source, effect) {
return this.random(3, 5);
},
onStart(pokemon) {
this.effectState.totalDamage = 0;
this.effectState.lastDamage = 0;
this.add('-start', pokemon, 'Bide');
},
onHit(target, source, move) {
if (source && source !== target && move.category !== 'Physical' && move.category !== 'Special') {
const damage = this.effectState.totalDamage;
this.effectState.totalDamage += damage;
this.effectState.lastDamage = damage;
this.effectState.sourceSlot = source.getSlot();
}
},
onDamage(damage, target, source, move) {
if (!source || source.isAlly(target)) return;
if (!move || move.effectType !== 'Move') return;
if (!damage && this.effectState.lastDamage > 0) {
damage = this.effectState.totalDamage;
}
this.effectState.totalDamage += damage;
this.effectState.lastDamage = damage;
this.effectState.sourceSlot = source.getSlot();
},
onAfterSetStatus(status, pokemon) {
// Sleep, freeze, and partial trap will just pause duration.
if (pokemon.volatiles['flinch']) {
this.effectState.duration++;
} else if (pokemon.volatiles['partiallytrapped']) {
this.effectState.duration++;
} else {
switch (status.id) {
case 'slp':
case 'frz':
this.effectState.duration++;
break;
}
}
},
onBeforeMove(pokemon, t, move) {
if (this.effectState.duration === 1) {
this.add('-end', pokemon, 'Bide');
if (!this.effectState.totalDamage) {
this.debug("Bide failed because no damage was taken");
this.add('-fail', pokemon);
return false;
}
const target = this.getAtSlot(this.effectState.sourceSlot);
this.actions.moveHit(target, pokemon, move, {damage: this.effectState.totalDamage * 2} as ActiveMove);
pokemon.removeVolatile('bide');
return false;
}
this.add('-activate', pokemon, 'Bide');
return false;
},
onDisableMove(pokemon) {
if (!pokemon.hasMove('bide')) {
return;
}
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id !== 'bide') {
pokemon.disableMove(moveSlot.id);
}
}
},
},
type: "???", // Will look as Normal but it's STAB-less
},
bind: {
inherit: true,
ignoreImmunity: true,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
// FIXME: onBeforeMove(pokemon, target) {target.removeVolatile('mustrecharge')}
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
bite: {
inherit: true,
category: "Physical",
secondary: {
chance: 10,
volatileStatus: 'flinch',
},
type: "Normal",
},
blizzard: {
inherit: true,
accuracy: 90,
target: "normal",
},
bubble: {
inherit: true,
secondary: {
chance: 33,
boosts: {
spe: -1,
},
},
target: "normal",
},
bubblebeam: {
inherit: true,
secondary: {
chance: 33,
boosts: {
spe: -1,
},
},
},
clamp: {
inherit: true,
accuracy: 75,
pp: 10,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
// FIXME: onBeforeMove(pokemon, target) {target.removeVolatile('mustrecharge')}
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
constrict: {
inherit: true,
secondary: {
chance: 33,
boosts: {
spe: -1,
},
},
},
conversion: {
inherit: true,
target: "normal",
onHit(target, source) {
source.setType(target.getTypes(true));
this.add('-start', source, 'typechange', source.types.join('/'), '[from] move: Conversion', '[of] ' + target);
},
},
counter: {
inherit: true,
ignoreImmunity: true,
willCrit: false,
basePower: 1,
damageCallback(pokemon, target) {
// Counter mechanics in gen 1:
// - a move is Counterable if it is Normal or Fighting type, has nonzero Base Power, and is not Counter
// - if Counter is used by the player, it will succeed if the opponent's last used move is Counterable
// - if Counter is used by the opponent, it will succeed if the player's last selected move is Counterable
// - (Counter will thus desync if the target's last used move is not as counterable as the target's last selected move)
// - if Counter succeeds it will deal twice the last move damage dealt in battle (even if it's from a different pokemon because of a switch)
const lastMove = target.side.lastMove && this.dex.moves.get(target.side.lastMove.id);
const lastMoveIsCounterable = lastMove && lastMove.basePower > 0 &&
['Normal', 'Fighting'].includes(lastMove.type) && lastMove.id !== 'counter';
const lastSelectedMove = target.side.lastSelectedMove && this.dex.moves.get(target.side.lastSelectedMove);
const lastSelectedMoveIsCounterable = lastSelectedMove && lastSelectedMove.basePower > 0 &&
['Normal', 'Fighting'].includes(lastSelectedMove.type) && lastSelectedMove.id !== 'counter';
if (!lastMoveIsCounterable && !lastSelectedMoveIsCounterable) {
this.debug("Gen 1 Counter: last move was not Counterable");
this.add('-fail', pokemon);
return false;
}
if (this.lastDamage <= 0) {
this.debug("Gen 1 Counter: no previous damage exists");
this.add('-fail', pokemon);
return false;
}
if (!lastMoveIsCounterable || !lastSelectedMoveIsCounterable) {
this.hint("Desync Clause Mod activated!");
this.add('-fail', pokemon);
return false;
}
return 2 * this.lastDamage;
},
},
crabhammer: {
inherit: true,
critRatio: 2,
},
dig: {
inherit: true,
basePower: 100,
condition: {
duration: 2,
onLockMove: 'dig',
onInvulnerability(target, source, move) {
if (move.id === 'swift') return true;
this.add('-message', 'The foe ' + target.name + ' can\'t be hit underground!');
return false;
},
onDamage(damage, target, source, move) {
if (!move || move.effectType !== 'Move') return;
if (!source) return;
if (move.id === 'earthquake') {
this.add('-message', 'The foe ' + target.name + ' can\'t be hit underground!');
return null;
}
},
},
},
disable: {
inherit: true,
condition: {
duration: 4,
durationCallback(target, source, effect) {
const duration = this.random(1, 7);
return duration;
},
onStart(pokemon) {
if (!this.queue.willMove(pokemon)) {
this.effectState.duration++;
}
const moves = pokemon.moves;
const move = this.dex.moves.get(this.sample(moves));
this.add('-start', pokemon, 'Disable', move.name);
this.effectState.move = move.id;
return;
},
onResidualOrder: 14,
onEnd(pokemon) {
this.add('-end', pokemon, 'Disable');
},
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);
}
}
},
},
},
dizzypunch: {
inherit: true,
secondary: null,
},
doubleedge: {
inherit: true,
basePower: 100,
},
dragonrage: {
inherit: true,
basePower: 1,
},
explosion: {
inherit: true,
basePower: 170,
target: "normal",
},
fireblast: {
inherit: true,
secondary: {
chance: 30,
status: 'brn',
},
},
firespin: {
inherit: true,
accuracy: 70,
basePower: 15,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
// FIXME: onBeforeMove(pokemon, target) {target.removeVolatile('mustrecharge')}
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
fly: {
inherit: true,
condition: {
duration: 2,
onLockMove: 'fly',
onInvulnerability(target, source, move) {
if (move.id === 'swift') return true;
this.add('-message', 'The foe ' + target.name + ' can\'t be hit while flying!');
return false;
},
onDamage(damage, target, source, move) {
if (!move || move.effectType !== 'Move') return;
if (!source || source.isAlly(target)) return;
if (move.id === 'gust' || move.id === 'thunder') {
this.add('-message', 'The foe ' + target.name + ' can\'t be hit while flying!');
return null;
}
},
},
},
focusenergy: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'move: Focus Energy');
},
// This does nothing as it's dealt with on critical hit calculation.
onModifyMove() {},
},
},
glare: {
inherit: true,
ignoreImmunity: true,
},
growth: {
inherit: true,
boosts: {
spa: 1,
spd: 1,
},
},
gust: {
inherit: true,
type: "Normal",
},
haze: {
inherit: true,
onHit(target, source) {
this.add('-activate', target, 'move: Haze');
this.add('-clearallboost', '[silent]');
for (const pokemon of this.getAllActive()) {
pokemon.clearBoosts();
if (pokemon !== source) {
pokemon.cureStatus(true);
}
if (pokemon.status === 'tox') {
pokemon.setStatus('psn');
}
for (const id of Object.keys(pokemon.volatiles)) {
if (id === 'residualdmg') {
pokemon.volatiles[id].counter = 0;
} else {
pokemon.removeVolatile(id);
this.add('-end', pokemon, id, '[silent]');
}
}
}
},
target: "self",
},
highjumpkick: {
inherit: true,
onMoveFail(target, source, move) {
if (!target.types.includes('Ghost')) {
this.directDamage(1, source, target);
}
},
},
jumpkick: {
inherit: true,
onMoveFail(target, source, move) {
if (!target.types.includes('Ghost')) {
this.directDamage(1, source, target);
}
},
},
karatechop: {
inherit: true,
critRatio: 2,
type: "Normal",
},
leechseed: {
inherit: true,
onHit() {},
condition: {
onStart(target) {
this.add('-start', target, 'move: Leech Seed');
},
onAfterMoveSelfPriority: 1,
onAfterMoveSelf(pokemon) {
const leecher = this.getAtSlot(pokemon.volatiles['leechseed'].sourceSlot);
if (!leecher || leecher.fainted || leecher.hp <= 0) {
this.debug('Nothing to leech into');
return;
}
// We check if leeched Pokémon has Toxic to increase leeched damage.
let toxicCounter = 1;
const residualdmg = pokemon.volatiles['residualdmg'];
if (residualdmg) {
residualdmg.counter++;
toxicCounter = residualdmg.counter;
}
const toLeech = this.clampIntRange(Math.floor(pokemon.baseMaxhp / 16), 1) * toxicCounter;
const damage = this.damage(toLeech, pokemon, leecher);
if (residualdmg) this.hint("In Gen 1, Leech Seed's damage is affected by Toxic's counter.", true);
if (!damage || toLeech > damage) {
this.hint("In Gen 1, Leech Seed recovery is not limited by the remaining HP of the seeded Pokemon.", true);
}
this.heal(toLeech, leecher, pokemon);
},
},
},
lightscreen: {
num: 113,
accuracy: true,
basePower: 0,
category: "Status",
name: "Light Screen",
pp: 30,
priority: 0,
flags: {},
volatileStatus: 'lightscreen',
onTryHit(pokemon) {
if (pokemon.volatiles['lightscreen']) {
return false;
}
},
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Light Screen');
},
},
target: "self",
type: "Psychic",
},
metronome: {
inherit: true,
noMetronome: ["Metronome", "Struggle"],
},
mimic: {
inherit: true,
onHit(target, source) {
const moveslot = source.moves.indexOf('mimic');
if (moveslot < 0) return false;
const moves = target.moves;
const moveid = this.sample(moves);
if (!moveid) return false;
const move = this.dex.moves.get(moveid);
source.moveSlots[moveslot] = {
move: move.name,
id: move.id,
pp: source.moveSlots[moveslot].pp,
maxpp: move.pp * 8 / 5,
target: move.target,
disabled: false,
used: false,
virtual: true,
};
this.add('-start', source, 'Mimic', move.name);
},
},
mirrormove: {
inherit: true,
onHit(pokemon) {
const foe = pokemon.side.foe.active[0];
if (!foe?.lastMove || foe.lastMove.id === 'mirrormove') {
return false;
}
this.actions.useMove(foe.lastMove.id, pokemon);
},
},
mist: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Mist');
},
onBoost(boost, target, source, effect) {
if (effect.effectType === 'Move' && effect.category !== 'Status') 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) this.add('-activate', target, 'move: Mist');
}
},
},
},
nightshade: {
inherit: true,
ignoreImmunity: true,
basePower: 1,
},
poisonsting: {
inherit: true,
secondary: {
chance: 20,
status: 'psn',
},
},
psychic: {
inherit: true,
secondary: {
chance: 33,
boosts: {
spd: -1,
spa: -1,
},
},
},
psywave: {
inherit: true,
basePower: 1,
damageCallback(pokemon) {
const psywaveDamage = (this.random(0, this.trunc(1.5 * pokemon.level)));
if (psywaveDamage <= 0) {
this.hint("Desync Clause Mod activated!");
return false;
}
return psywaveDamage;
},
},
rage: {
inherit: true,
self: {
volatileStatus: 'rage',
},
condition: {
// Rage lock
duration: 255,
onStart(target, source, effect) {
this.effectState.move = 'rage';
},
onLockMove: 'rage',
onTryHit(target, source, move) {
if (target.boosts.atk < 6 && move.id === 'disable') {
this.boost({atk: 1});
}
},
onHit(target, source, move) {
if (target.boosts.atk < 6 && move.category !== 'Status') {
this.boost({atk: 1});
}
},
},
},
razorleaf: {
inherit: true,
critRatio: 2,
target: "normal",
},
razorwind: {
inherit: true,
critRatio: 1,
target: "normal",
},
recover: {
inherit: true,
heal: null,
onHit(target) {
if (target.hp === target.maxhp) return false;
// Fail when health is 255 or 511 less than max
if (target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511) || target.hp === target.maxhp) {
this.hint("In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256.");
return false;
}
this.heal(Math.floor(target.maxhp / 2), target, target);
},
},
reflect: {
num: 115,
accuracy: true,
basePower: 0,
category: "Status",
name: "Reflect",
pp: 20,
priority: 0,
flags: {},
volatileStatus: 'reflect',
onTryHit(pokemon) {
if (pokemon.volatiles['reflect']) {
return false;
}
},
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Reflect');
},
},
secondary: null,
target: "self",
type: "Psychic",
},
rest: {
inherit: true,
onTry() {},
onHit(target, source, move) {
if (target.hp === target.maxhp) return false;
// Fail when health is 255 or 511 less than max
if (target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) {
this.hint("In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256.");
return false;
}
if (!target.setStatus('slp', source, move)) return false;
target.statusState.time = 2;
target.statusState.startTime = 2;
this.heal(target.maxhp); // Aesthetic only as the healing happens after you fall asleep in-game
},
},
roar: {
inherit: true,
forceSwitch: false,
onTryHit() {},
priority: 0,
},
rockslide: {
inherit: true,
secondary: null,
target: "normal",
},
rockthrow: {
inherit: true,
accuracy: 65,
},
sandattack: {
inherit: true,
ignoreImmunity: true,
type: "Normal",
},
seismictoss: {
inherit: true,
ignoreImmunity: true,
basePower: 1,
},
selfdestruct: {
inherit: true,
basePower: 130,
target: "normal",
},
skullbash: {
inherit: true,
onTryMove(attacker, defender, move) {
if (attacker.removeVolatile(move.id)) {
return;
}
this.add('-prepare', attacker, move.name);
if (!this.runEvent('ChargeMove', attacker, defender, move)) {
return;
}
attacker.addVolatile('twoturnmove', defender);
return null;
},
},
slash: {
inherit: true,
critRatio: 2,
},
sludge: {
inherit: true,
secondary: {
chance: 40,
status: 'psn',
},
},
softboiled: {
inherit: true,
heal: null,
onHit(target) {
if (target.hp === target.maxhp) return false;
// Fail when health is 255 or 511 less than max
if (target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511) || target.hp === target.maxhp) {
this.hint("In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256.");
return false;
}
this.heal(Math.floor(target.maxhp / 2), target, target);
},
},
struggle: {
inherit: true,
pp: 10,
recoil: [1, 2],
onModifyMove() {},
},
substitute: {
num: 164,
accuracy: true,
basePower: 0,
category: "Status",
name: "Substitute",
pp: 10,
priority: 0,
volatileStatus: 'substitute',
onTryHit(target) {
if (target.volatiles['substitute']) {
this.add('-fail', target, 'move: Substitute');
return null;
}
// We only prevent when hp is less than one quarter.
// If you use substitute at exactly one quarter, you faint.
if (target.hp === target.maxhp / 4) target.faint();
if (target.hp < target.maxhp / 4) {
this.add('-fail', target, 'move: Substitute', '[weak]');
return null;
}
},
onHit(target) {
// If max HP is 3 or less substitute makes no damage
if (target.maxhp > 3) {
this.directDamage(target.maxhp / 4, target, target);
}
},
condition: {
onStart(target) {
this.add('-start', target, 'Substitute');
this.effectState.hp = Math.floor(target.maxhp / 4) + 1;
delete target.volatiles['partiallytrapped'];
},
onTryHitPriority: -1,
onTryHit(target, source, move) {
if (move.category === 'Status') {
// In gen 1 it only blocks:
// poison, confusion, secondary effect confusion, stat reducing moves and Leech Seed.
const SubBlocked = ['lockon', 'meanlook', 'mindreader', 'nightmare'];
if (
move.status === 'psn' || move.status === 'tox' || (move.boosts && target !== source) ||
move.volatileStatus === 'confusion' || SubBlocked.includes(move.id)
) {
return false;
}
return;
}
if (move.volatileStatus && target === source) return;
// NOTE: In future generations the damage is capped to the remaining HP of the
// Substitute, here we deliberately use the uncapped damage when tracking lastDamage etc.
// Also, multi-hit moves must always deal the same damage as the first hit for any subsequent hits
let uncappedDamage = move.hit > 1 ? source.lastDamage : this.actions.getDamage(source, target, move);
if (!uncappedDamage) return null;
uncappedDamage = this.runEvent('SubDamage', target, source, move, uncappedDamage);
if (!uncappedDamage) return uncappedDamage;
source.lastDamage = uncappedDamage;
target.volatiles['substitute'].hp -= uncappedDamage > target.volatiles['substitute'].hp ?
target.volatiles['substitute'].hp : uncappedDamage;
if (target.volatiles['substitute'].hp <= 0) {
target.removeVolatile('substitute');
target.subFainted = true;
} else {
this.add('-activate', target, 'Substitute', '[damage]');
}
// Drain/recoil does not happen if the substitute breaks
if (target.volatiles['substitute']) {
if (move.recoil) {
this.damage(Math.round(uncappedDamage * move.recoil[0] / move.recoil[1]), source, target, 'recoil');
}
if (move.drain) {
this.heal(Math.ceil(uncappedDamage * move.drain[0] / move.drain[1]), source, target, 'drain');
}
}
this.runEvent('AfterSubDamage', target, source, move, uncappedDamage);
// Add here counter damage
const lastAttackedBy = target.getLastAttackedBy();
if (!lastAttackedBy) {
target.attackedBy.push({source: source, move: move.id, damage: uncappedDamage, slot: source.getSlot(), thisTurn: true});
} else {
lastAttackedBy.move = move.id;
lastAttackedBy.damage = uncappedDamage;
}
return 0;
},
onEnd(target) {
this.add('-end', target, 'Substitute');
},
},
secondary: null,
target: "self",
type: "Normal",
flags: {},
},
superfang: {
inherit: true,
ignoreImmunity: true,
basePower: 1,
},
thunder: {
inherit: true,
secondary: {
chance: 10,
status: 'par',
},
},
thunderwave: {
inherit: true,
accuracy: 100,
onTryHit(target) {
if (target.hasType('Ground')) {
this.add('-immune', target);
return null;
}
},
},
triattack: {
inherit: true,
onHit() {},
secondary: null,
},
whirlwind: {
inherit: true,
accuracy: 85,
forceSwitch: false,
onTryHit() {},
priority: 0,
},
wingattack: {
inherit: true,
basePower: 35,
},
wrap: {
inherit: true,
accuracy: 85,
ignoreImmunity: true,
volatileStatus: 'partiallytrapped',
self: {
volatileStatus: 'partialtrappinglock',
},
// FIXME: onBeforeMove(pokemon, target) {target.removeVolatile('mustrecharge')}
onHit(target, source) {
/**
* The duration of the partially trapped must be always renewed to 2
* so target doesn't move on trapper switch out as happens in gen 1.
* However, this won't happen if there's no switch and the trapper is
* about to end its partial trapping.
**/
if (target.volatiles['partiallytrapped']) {
if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration > 1) {
target.volatiles['partiallytrapped'].duration = 2;
}
}
},
},
};