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

978 lines
26 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: import('../../../sim/dex-moves').ModdedMoveDataTable = {
acid: {
inherit: true,
secondary: {
chance: 33,
boosts: {
def: -1,
},
},
target: "normal",
},
amnesia: {
inherit: true,
boosts: {
spa: 2,
spd: 2,
},
},
aurorabeam: {
inherit: true,
secondary: {
chance: 33,
boosts: {
atk: -1,
},
},
},
bide: {
inherit: true,
priority: 0,
accuracy: true,
condition: {
onStart(pokemon) {
this.effectState.damage = 0;
this.effectState.time = this.random(2, 4);
this.add('-start', pokemon, 'Bide');
},
onBeforeMove(pokemon, t, move) {
const currentMove = this.dex.getActiveMove('bide');
this.effectState.damage += this.lastDamage;
this.effectState.time--;
if (!this.effectState.time) {
this.add('-end', pokemon, currentMove);
if (!this.effectState.damage) {
this.debug("Bide failed because no damage was stored");
this.add('-fail', pokemon);
pokemon.removeVolatile('bide');
return false;
}
const target = this.getRandomTarget(pokemon, 'Pound');
this.actions.moveHit(target, pokemon, currentMove, { damage: this.effectState.damage * 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',
},
onTryMove(source, target) {
if (target.volatiles['mustrecharge']) {
target.removeVolatile('mustrecharge');
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
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',
},
onTryMove(source, target) {
if (target.volatiles['mustrecharge']) {
target.removeVolatile('mustrecharge');
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
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;
},
flags: { contact: 1, protect: 1, metronome: 1 },
},
crabhammer: {
inherit: true,
critRatio: 2,
},
dig: {
inherit: true,
basePower: 100,
condition: {},
onTryMove(attacker, defender, move) {
if (attacker.removeVolatile('twoturnmove')) {
attacker.removeVolatile('invulnerability');
return;
}
this.add('-prepare', attacker, move.name);
attacker.addVolatile('twoturnmove', defender);
attacker.addVolatile('invulnerability', defender);
return null;
},
},
disable: {
num: 50,
accuracy: 55,
basePower: 0,
category: "Status",
name: "Disable",
pp: 20,
priority: 0,
flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 },
volatileStatus: 'disable',
onTryHit(target) {
// This function should not return if the checks are met. Adding && undefined ensures this happens.
return target.moveSlots.some(ms => ms.pp > 0) &&
!('disable' in target.volatiles) &&
undefined;
},
condition: {
onStart(pokemon) {
// disable can only select moves that have pp > 0, hence the onTryHit modification
const moveSlot = this.sample(pokemon.moveSlots.filter(ms => ms.pp > 0));
this.add('-start', pokemon, 'Disable', moveSlot.move);
this.effectState.move = moveSlot.id;
// 1-8 turns (which will in effect translate to 0-7 missed turns for the target)
this.effectState.time = this.random(1, 9);
},
onEnd(pokemon) {
this.add('-end', pokemon, 'Disable');
},
onBeforeMovePriority: 6,
onBeforeMove(pokemon, target, move) {
pokemon.volatiles['disable'].time--;
if (!pokemon.volatiles['disable'].time) {
pokemon.removeVolatile('disable');
return;
}
if (pokemon.volatiles['bide']) move = this.dex.getActiveMove('bide');
if (move.id === this.effectState.move) {
this.add('cant', pokemon, 'Disable', move);
pokemon.removeVolatile('twoturnmove');
return false;
}
},
onDisableMove(pokemon) {
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id === this.effectState.move) {
pokemon.disableMove(moveSlot.id);
}
}
},
},
secondary: null,
target: "normal",
type: "Normal",
},
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',
},
onTryMove(source, target) {
if (target.volatiles['mustrecharge']) {
target.removeVolatile('mustrecharge');
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
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: {},
onTryMove(attacker, defender, move) {
if (attacker.removeVolatile('twoturnmove')) {
attacker.removeVolatile('invulnerability');
return;
}
this.add('-prepare', attacker, move.name);
attacker.addVolatile('twoturnmove', defender);
attacker.addVolatile('invulnerability', defender);
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', null, null, true);
}
pokemon.updateSpeed();
// should only clear a specific set of volatiles
// while technically the toxic counter shouldn't be cleared, the preserved toxic counter is never used again
// in-game, so it is equivalent to just clear it.
const silentHack = '|[silent]';
const silentHackVolatiles = ['disable', 'confusion'];
const hazeVolatiles: { [key: string]: string } = {
'disable': '',
'confusion': '',
'mist': 'Mist',
'focusenergy': 'move: Focus Energy',
'leechseed': 'move: Leech Seed',
'lightscreen': 'Light Screen',
'reflect': 'Reflect',
'residualdmg': 'Toxic counter',
};
for (const v in hazeVolatiles) {
if (!pokemon.removeVolatile(v)) {
continue;
}
if (silentHackVolatiles.includes(v)) {
// these volatiles have their own onEnd method that prints, so to avoid
// double printing and ensure they are still silent, we need to tack on a
// silent attribute at the end
this.log[this.log.length - 1] += silentHack;
} else {
this.add('-end', pokemon, hazeVolatiles[v], '[silent]');
}
}
}
},
target: "self",
},
highjumpkick: {
inherit: true,
onMoveFail(target, source, move) {
this.directDamage(1, source, target);
},
},
jumpkick: {
inherit: true,
onMoveFail(target, source, move) {
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: { metronome: 1 },
volatileStatus: 'lightscreen',
onTryHit(pokemon) {
if (pokemon.volatiles['lightscreen']) {
return false;
}
},
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Light Screen');
},
},
target: "self",
type: "Psychic",
},
mimic: {
inherit: true,
flags: { protect: 1, bypasssub: 1, metronome: 1 },
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;
}
pokemon.side.lastSelectedMove = foe.lastMove.id;
this.actions.useMove(foe.lastMove.id, pokemon);
},
},
mist: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-start', pokemon, 'Mist');
},
onTryBoost(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 && !(effect as ActiveMove).secondaries) {
this.add('-activate', target, 'move: Mist');
}
}
},
},
},
nightshade: {
inherit: true,
ignoreImmunity: true,
basePower: 1,
},
petaldance: {
inherit: true,
onMoveFail() {},
},
poisonsting: {
inherit: true,
secondary: {
chance: 20,
status: 'psn',
},
},
psychic: {
inherit: true,
secondary: {
chance: 33,
boosts: {
spa: -1,
spd: -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
onStart(target, source, effect) {
this.effectState.move = 'rage';
this.effectState.accuracy = 255;
},
onLockMove: 'rage',
onHit(target, source, move) {
// Disable and exploding moves boost Rage even if they miss/fail, so they are dealt with separately.
if (target.boosts.atk < 6 && (move.category !== 'Status' && !move.selfdestruct)) {
this.boost({ atk: 1 });
}
},
},
},
razorleaf: {
inherit: true,
critRatio: 2,
target: "normal",
},
razorwind: {
inherit: true,
critRatio: 1,
target: "normal",
onTryMove(attacker, defender, move) {
if (attacker.removeVolatile('twoturnmove')) {
attacker.removeVolatile('invulnerability');
return;
}
this.add('-prepare', attacker, move.name);
attacker.addVolatile('twoturnmove', defender);
return null;
},
},
recover: {
inherit: true,
heal: null,
onHit(target) {
if (target.hp === target.maxhp) return false;
// Fail when health is 255 or 511 less than max, unless it is divisible by 256
if (
target.hp === target.maxhp ||
((target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) && target.hp % 256 !== 0)
) {
this.hint(
"In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256, " +
"unless the current hp is also 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: { metronome: 1 },
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, unless it is divisible by 256
if (
target.hp === target.maxhp ||
((target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) && target.hp % 256 !== 0)
) {
this.hint(
"In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256, " +
"unless the current hp is also 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('twoturnmove')) {
attacker.removeVolatile('invulnerability');
return;
}
this.add('-prepare', attacker, move.name);
attacker.addVolatile('twoturnmove', defender);
return null;
},
},
skyattack: {
inherit: true,
onTryMove(attacker, defender, move) {
if (attacker.removeVolatile('twoturnmove')) {
attacker.removeVolatile('invulnerability');
return;
}
this.add('-prepare', attacker, move.name);
attacker.addVolatile('twoturnmove', defender);
return null;
},
},
slash: {
inherit: true,
critRatio: 2,
},
sludge: {
inherit: true,
secondary: {
chance: 40,
status: 'psn',
},
},
solarbeam: {
inherit: true,
onTryMove(attacker, defender, move) {
if (attacker.removeVolatile('twoturnmove')) {
attacker.removeVolatile('invulnerability');
return;
}
this.add('-prepare', attacker, move.name);
attacker.addVolatile('twoturnmove', defender);
return null;
},
},
sonicboom: {
inherit: true,
ignoreImmunity: true,
basePower: 1,
},
softboiled: {
inherit: true,
heal: null,
onHit(target) {
if (target.hp === target.maxhp) return false;
// Fail when health is 255 or 511 less than max, unless it is divisible by 256
if (
target.hp === target.maxhp ||
((target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) && target.hp % 256 !== 0)
) {
this.hint(
"In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256, " +
"unless the current hp is also 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,
flags: { metronome: 1 },
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) {
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 ? this.lastDamage : this.actions.getDamage(source, target, move);
if (move.id === 'bide') uncappedDamage = source.volatiles['bide'].damage * 2;
if (!uncappedDamage && uncappedDamage !== 0) return null;
uncappedDamage = this.runEvent('SubDamage', target, source, move, uncappedDamage);
if (!uncappedDamage && uncappedDamage !== 0) return uncappedDamage;
this.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/secondary effect confusion do not happen if the substitute breaks
if (target.volatiles['substitute']) {
if (move.recoil) {
this.damage(this.clampIntRange(Math.floor(uncappedDamage * move.recoil[0] / move.recoil[1]), 1),
source, target, 'recoil');
}
if (move.drain) {
const amount = this.clampIntRange(Math.floor(uncappedDamage * move.drain[0] / move.drain[1]), 1);
this.lastDamage = amount;
this.heal(amount, source, target, 'drain');
}
if (move.secondary?.volatileStatus === 'confusion') {
const secondary = move.secondary;
if (secondary.chance === undefined || this.randomChance(Math.ceil(secondary.chance * 256 / 100) - 1, 256)) {
target.addVolatile(move.secondary.volatileStatus, source, move);
this.hint(
"In Gen 1, moves that inflict confusion as a secondary effect can confuse targets with a Substitute, " +
"as long as the move does not break the Substitute."
);
}
}
}
this.runEvent('AfterSubDamage', target, source, move, uncappedDamage);
// Add here counter damage
const lastAttackedBy = target.getLastAttackedBy();
if (!lastAttackedBy) {
target.attackedBy.push({ 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",
},
superfang: {
inherit: true,
ignoreImmunity: true,
basePower: 1,
},
thrash: {
inherit: true,
onMoveFail() {},
},
thunder: {
inherit: true,
secondary: {
chance: 10,
status: 'par',
},
},
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',
},
onTryMove(source, target) {
if (target.volatiles['mustrecharge']) {
target.removeVolatile('mustrecharge');
this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true);
}
},
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;
}
}
},
},
};