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

467 lines
14 KiB
TypeScript

export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
pursuit: {
inherit: true,
beforeTurnCallback(pokemon, target) {
// @ts-expect-error modded
const linkedMoves: [string, string] = pokemon.getLinkedMoves();
if (linkedMoves.length) {
if (linkedMoves[0] !== 'pursuit' && linkedMoves[1] === 'pursuit') return;
}
target.side.addSideCondition('pursuit', pokemon);
if (!target.side.sideConditions['pursuit'].sources) {
target.side.sideConditions['pursuit'].sources = [];
}
target.side.sideConditions['pursuit'].sources.push(pokemon);
},
},
mefirst: {
inherit: true,
onTryHit(target, pokemon) {
const action = this.queue.willMove(target);
if (action) {
// Mod-specific: Me First copies the first move in the link
// @ts-expect-error modded
const move = this.dex.getActiveMove(action.linked?.[0] || action.move);
if (move.category !== 'Status' && !move.flags['failmefirst']) {
pokemon.addVolatile('mefirst');
this.actions.useMove(move, pokemon, { target });
return null;
}
}
return false;
},
},
// Modify Sucker Punch to check if both moves in a link are status
suckerpunch: {
inherit: true,
onTry(source, target) {
const action = this.queue.willMove(target);
if (!action || action.choice !== 'move') {
this.attrLastMove('[still]');
this.add('-fail', source);
return null;
}
if (target.volatiles.mustrecharge && target.volatiles.mustrecharge.duration! < 2) {
// Duration may not be lower than 2 if Sucker Punch is used as a low-priority move
// i.e. if Sucker Punch is linked with a negative priority move
this.attrLastMove('[still]');
this.add('-fail', source);
return null;
}
// @ts-expect-error modded
if (!action.linked) {
if (action.move.category === 'Status' && action.move.id !== 'mefirst') {
this.attrLastMove('[still]');
this.add('-fail', source);
return null;
}
} else {
// @ts-expect-error modded
for (const linkedMove of action.linked) {
if (linkedMove.category !== 'Status' || linkedMove.id === 'mefirst') return;
}
this.attrLastMove('[still]');
this.add('-fail', source);
return null;
}
},
},
// Copy the last used move of a link
sketch: {
inherit: true,
onHit(target, source) {
const disallowedMoves = ['chatter', 'sketch', 'struggle'];
const lastMove: Move = target.m.lastMoveAbsolute;
if (source.transformed || !lastMove || disallowedMoves.includes(lastMove.id) ||
source.moves.includes(lastMove.id) || lastMove.isZ) return false;
const sketchIndex = source.moves.indexOf('sketch');
if (sketchIndex < 0) return false;
const move = this.dex.moves.get(lastMove);
const sketchedMove = {
move: move.name,
id: move.id,
pp: move.pp,
maxpp: move.pp,
target: move.target,
disabled: false,
used: false,
};
source.moveSlots[sketchIndex] = sketchedMove;
source.baseMoveSlots[sketchIndex] = sketchedMove;
this.add('-activate', source, 'move: Sketch', move.name);
},
},
mimic: {
inherit: true,
onHit(target, source) {
const lastMove: Move = target.m.lastMoveAbsolute;
if (source.transformed || !lastMove || lastMove.flags['failmimic'] ||
source.moves.includes(lastMove.id) || lastMove.isZ) return false;
const mimicIndex = source.moves.indexOf('mimic');
if (mimicIndex < 0) return false;
const move = this.dex.moves.get(lastMove);
source.moveSlots[mimicIndex] = {
move: move.name,
id: move.id,
pp: move.pp,
maxpp: move.pp,
target: move.target,
disabled: false,
used: false,
virtual: true,
};
this.add('-start', source, 'Mimic', move.name);
},
},
// Copy/call last move of a link
instruct: {
inherit: true,
onHit(target, source) {
const lastMove: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (!lastMove || target.volatiles['dynamax']) return false;
const moveIndex = target.moves.indexOf(lastMove.id);
if (
lastMove.flags['failinstruct'] || lastMove.isZ || lastMove.isMax ||
lastMove.flags['charge'] || lastMove.flags['recharge'] ||
target.volatiles['beakblast'] || target.volatiles['focuspunch'] || target.volatiles['shelltrap'] ||
(target.moveSlots[moveIndex] && target.moveSlots[moveIndex].pp <= 0)
) {
return false;
}
this.add('-singleturn', target, 'move: Instruct', `[of] ${source}`);
this.actions.runMove(lastMove.id, target, target.lastMoveTargetLoc!);
},
},
mirrormove: {
inherit: true,
onTryHit(target, pokemon) {
const move: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (!move?.flags['mirror'] || move.isZ || move.isMax) {
return false;
}
this.actions.useMove(move.id, pokemon, { target });
return null;
},
},
// Disabling effects
disable: {
inherit: true,
condition: {
duration: 5,
noCopy: true, // doesn't get copied by Baton Pass
onStart(pokemon, source, effect) {
const lastMove: Move | ActiveMove | null = pokemon.m.lastMoveAbsolute;
if (
this.queue.willMove(pokemon) ||
(pokemon === this.activePokemon && this.activeMove && !this.activeMove.isExternal)
) {
this.effectState.duration!--;
}
if (!lastMove) {
this.debug('pokemon hasn\'t moved yet');
return false;
}
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id === lastMove.id) {
if (!moveSlot.pp) {
this.debug('Move out of PP');
return false;
} else {
if (effect.id === 'cursedbody') {
this.add('-start', pokemon, 'Disable', moveSlot.move, '[from] ability: Cursed Body', `[of] ${source}`);
} else {
this.add('-start', pokemon, 'Disable', moveSlot.move);
}
this.effectState.move = lastMove.id;
return;
}
}
}
return false;
},
onResidualOrder: 14,
onEnd(pokemon) {
this.add('-end', pokemon, 'Disable');
},
onBeforeMovePriority: 7,
onBeforeMove(attacker, defender, move) {
if (!move.isZ && 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);
}
}
},
},
},
encore: {
inherit: true,
condition: {
duration: 3,
noCopy: true, // doesn't get copied by Z-Baton Pass
onStart(target) {
let lastMove: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (!lastMove || target.volatiles['dynamax']) return false;
if ((lastMove as ActiveMove).isZOrMaxPowered) lastMove = this.dex.moves.get(lastMove.baseMove);
// @ts-expect-error modded
const linkedMoves: [string, string] = target.getLinkedMoves(true);
const moveIndex = target.moves.indexOf(lastMove.id);
if (linkedMoves.includes(lastMove.id) && this.dex.moves.get((linkedMoves[0])).flags['failencore'] &&
this.dex.moves.get((linkedMoves[1])).flags['failencore']) {
// both moves cannot be encored
delete target.volatiles['encore'];
return false;
}
if (lastMove.isZ || lastMove.flags['failencore'] ||
(target.moveSlots[moveIndex] && target.moveSlots[moveIndex].pp <= 0)) {
// it failed
delete target.volatiles['encore'];
return false;
}
this.effectState.turnsActivated = {};
this.effectState.move = lastMove.id;
this.add('-start', target, 'Encore');
if (linkedMoves.includes(lastMove.id)) {
this.effectState.move = linkedMoves;
}
if (!this.queue.willMove(target)) {
this.effectState.duration!++;
}
},
onOverrideAction(pokemon, target, move) {
if (!this.effectState.turnsActivated[this.turn]) {
// Initialize Encore effect for this turn
this.effectState.turnsActivated[this.turn] = 0;
} else if (
this.effectState.turnsActivated[this.turn] >= (Array.isArray(this.effectState.move) ?
this.effectState.move.length : 1)) {
// Finish Encore effect for this turn
return;
}
this.effectState.turnsActivated[this.turn]++;
if (!Array.isArray(this.effectState.move)) {
this.queue.cancelAction(pokemon);
if (move.id !== this.effectState.move) return this.effectState.move;
return;
}
// Locked into a link
switch (this.effectState.turnsActivated[this.turn]) {
case 1: {
if (this.effectState.move[0] !== move.id) return this.effectState.move[0];
return;
}
case 2:
if (this.effectState.move[1] !== move.id) return this.effectState.move[1];
return;
}
},
onResidualOrder: 13,
onResidual(target) {
// early termination if you run out of PP
const lastMove = target.m.lastMoveAbsolute;
const index = target.moves.indexOf(lastMove.id);
if (index === -1) return; // no last move
// @ts-expect-error modded
if (target.hasLinkedMove(lastMove.id)) {
// TODO: Check instead whether the last executed move was linked
if (target.moveSlots[0].pp <= 0 || target.moveSlots[1].pp <= 0) {
delete target.volatiles.encore;
this.add('-end', target, 'Encore');
}
} else {
if (target.moveSlots[index].pp <= 0) {
delete target.volatiles.encore;
this.add('-end', target, 'Encore');
}
}
},
onEnd(target) {
this.add('-end', target, 'Encore');
},
onDisableMove(pokemon) {
if (Array.isArray(this.effectState.move)) {
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id !== this.effectState.move[0] && moveSlot.id !== this.effectState.move[1]) {
pokemon.disableMove(moveSlot.id);
}
}
}
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);
}
}
},
},
},
torment: {
inherit: true,
condition: {
noCopy: true,
onStart(pokemon) {
if (pokemon.volatiles['dynamax']) {
delete pokemon.volatiles['torment'];
return false;
}
this.add('-start', pokemon, 'Torment');
},
onEnd(pokemon) {
this.add('-end', pokemon, 'Torment');
},
onDisableMove(pokemon) {
const lastMove = pokemon.lastMove;
if (!lastMove || lastMove.id === 'struggle') return;
if (Array.isArray(lastMove)) {
for (const move of lastMove) {
pokemon.disableMove(move.id);
}
} else {
pokemon.disableMove(lastMove.id);
}
},
},
},
// PP-decreasing moves
grudge: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-singlemove', pokemon, 'Grudge');
},
onFaint(target, source, effect) {
if (!source || source.fainted || !effect) return;
const lastMove: Move | ActiveMove | null = source.m.lastMoveAbsolute;
if (effect.effectType === 'Move' && !effect.flags['futuremove'] && lastMove) {
for (const moveSlot of source.moveSlots) {
if (moveSlot.id === lastMove.id) {
moveSlot.pp = 0;
this.add('-activate', source, 'move: Grudge', this.dex.moves.get(lastMove.id).name);
}
}
}
},
onBeforeMovePriority: 100,
onBeforeMove(pokemon) {
if (pokemon.moveThisTurn) return; // Second stage of a Linked move
this.debug('removing Grudge before attack');
pokemon.removeVolatile('grudge');
},
},
},
spite: {
inherit: true,
onHit(target) {
const lastMove: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (!lastMove || lastMove.isZ || lastMove.isMax) return false;
const ppDeducted = target.deductPP(lastMove.id, 4);
if (!ppDeducted) return false;
this.add("-activate", target, 'move: Spite', lastMove.name, ppDeducted);
},
},
// Other lastMove checks
conversion2: {
inherit: true,
onHit(target, source) {
const lastMove: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (!lastMove) return false;
const possibleTypes = [];
const attackType = lastMove.type;
for (const typeName of this.dex.types.names()) {
if (source.hasType(typeName)) continue;
const typeCheck = this.dex.types.get(typeName).damageTaken[attackType];
if (typeCheck === 2 || typeCheck === 3) {
possibleTypes.push(typeName);
}
}
if (!possibleTypes.length) {
return false;
}
const randomType = this.sample(possibleTypes);
if (!source.setType(randomType)) return false;
this.add('-start', source, 'typechange', randomType);
},
},
destinybond: {
inherit: true,
condition: {
onStart(pokemon) {
this.add('-singlemove', pokemon, 'Destiny Bond');
},
onFaint(target, source, effect) {
if (!source || !effect || target.side === source.side) return;
if (effect.effectType === 'Move' && !effect.flags['futuremove']) {
if (source.volatiles['dynamax']) {
this.add('-hint', "Dynamaxed Pokémon are immune to Destiny Bond.");
return;
}
this.add('-activate', target, 'move: Destiny Bond');
source.faint();
}
},
onBeforeMovePriority: -1,
onBeforeMove(pokemon, target, move) {
// Second stage of a Linked move does not remove Destiny Bond
if (pokemon.moveThisTurn || move.id === 'destinybond') return;
this.debug('removing Destiny Bond before attack');
pokemon.removeVolatile('destinybond');
},
onMoveAborted(pokemon, target, move) {
pokemon.removeVolatile('destinybond');
},
},
},
iceball: {
inherit: true,
condition: {
duration: 1,
onLockMove: 'iceball',
onStart() {
this.effectState.hitCount = 0;
},
onResidual(target) {
// This is just to ensure the volatile is deleted correctly
const lastMove: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (lastMove?.id === 'struggle') {
delete target.volatiles['iceball'];
}
},
},
},
rollout: {
inherit: true,
condition: {
duration: 1,
onLockMove: 'rollout',
onStart() {
this.effectState.hitCount = 0;
},
onResidual(target) {
// This is just to ensure the volatile is deleted correctly
const lastMove: Move | ActiveMove | null = target.m.lastMoveAbsolute;
if (lastMove?.id === 'struggle') {
delete target.volatiles['rollout'];
}
},
},
},
};