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

555 lines
20 KiB
TypeScript

export const Scripts: ModdedBattleScriptsData = {
inherit: 'gen8',
gen: 8,
getActionSpeed(action) {
if (action.choice === 'move') {
let move = action.move;
if (action.zmove) {
const zMoveName = this.actions.getZMove(action.move, action.pokemon, true);
if (zMoveName) {
const zMove = this.dex.getActiveMove(zMoveName);
if (zMove.exists && zMove.isZ) {
move = zMove;
}
}
}
if (action.maxMove) {
const maxMoveName = this.actions.getMaxMove(action.maxMove, action.pokemon);
if (maxMoveName) {
const maxMove = this.actions.getActiveMaxMove(action.move, action.pokemon);
if (maxMove.exists && maxMove.isMax) {
move = maxMove;
}
}
}
// take priority from the base move, so abilities like Prankster only apply once
// (instead of compounding every time `getActionSpeed` is called)
let priority = this.dex.moves.get(move.id).priority;
// Grassy Glide priority
priority = this.singleEvent('ModifyPriority', move, null, action.pokemon, null, null, priority);
priority = this.runEvent('ModifyPriority', action.pokemon, null, move, priority);
// Linked mod
const linkedMoves: [string, string] = action.pokemon.getLinkedMoves();
let linkIndex = -1;
if (linkedMoves.length && !move.isZ && !move.isMax && (linkIndex = linkedMoves.indexOf(this.toID(action.move))) >= 0) {
const linkedActions = action.linked || linkedMoves.map(moveid => this.dex.getActiveMove(moveid));
const altMove = linkedActions[1 - linkIndex];
const thisPriority = this.runEvent('ModifyPriority', action.pokemon, null, linkedActions[linkIndex], priority);
const thatPriority = this.runEvent('ModifyPriority', action.pokemon, null, altMove, altMove.priority);
priority = Math.min(thisPriority, thatPriority);
action.priority = priority + action.fractionalPriority;
if (this.gen > 5) {
// Gen 6+: Quick Guard blocks moves with artificially enhanced priority.
// This also applies to Psychic Terrain.
linkedActions[linkIndex].priority = priority;
altMove.priority = priority;
}
} else {
action.priority = priority + action.fractionalPriority;
// In Gen 6, Quick Guard blocks moves with artificially enhanced priority.
if (this.gen > 5) action.move.priority = priority;
}
}
if (!action.pokemon) {
action.speed = 1;
} else {
action.speed = action.pokemon.getActionSpeed();
}
},
runAction(action) {
const pokemonOriginalHP = action.pokemon?.hp;
let residualPokemon: (readonly [Pokemon, number])[] = [];
// returns whether or not we ended in a callback
switch (action.choice) {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
}
this.add('start');
if (this.format.onBattleStart) this.format.onBattleStart.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
if (subFormat.onBattleStart) subFormat.onBattleStart.call(this);
}
for (const side of this.sides) {
for (let i = 0; i < side.active.length; i++) {
if (!side.pokemonLeft) {
// forfeited before starting
side.active[i] = side.pokemon[i];
side.active[i].fainted = true;
side.active[i].hp = 0;
} else {
this.actions.switchIn(side.pokemon[i], i);
}
}
}
for (const pokemon of this.getAllPokemon()) {
this.singleEvent('Start', this.dex.conditions.getByID(pokemon.species.id), pokemon.speciesState, pokemon);
}
this.midTurn = true;
break;
}
case 'move':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
// Linked moves
// @ts-expect-error modded
if (action.linked) {
// @ts-expect-error modded
const linkedMoves: ActiveMove[] = action.linked;
for (let i = linkedMoves.length - 1; i >= 0; i--) {
const validTarget = this.validTargetLoc(action.targetLoc, action.pokemon, linkedMoves[i].target);
const targetLoc = validTarget ? action.targetLoc : 0;
const pseudoAction: Action = {
choice: 'move', priority: action.priority, speed: action.speed, pokemon: action.pokemon,
targetLoc, moveid: linkedMoves[i].id, move: linkedMoves[i], mega: action.mega,
order: action.order, fractionalPriority: action.fractionalPriority, originalTarget: action.originalTarget,
};
this.queue.unshift(pseudoAction);
}
return;
}
this.actions.runMove(action.move, action.pokemon, action.targetLoc, {
sourceEffect: action.sourceEffect, zMove: action.zmove,
maxMove: action.maxMove, originalTarget: action.originalTarget,
});
break;
case 'megaEvo':
this.actions.runMegaEvo(action.pokemon);
break;
case 'runDynamax':
action.pokemon.addVolatile('dynamax');
action.pokemon.side.dynamaxUsed = true;
if (action.pokemon.side.allySide) action.pokemon.side.allySide.dynamaxUsed = true;
break;
case 'beforeTurnMove': {
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
this.debug('before turn callback: ' + action.move.id);
const target = this.getTarget(action.pokemon, action.move, action.targetLoc);
if (!target) return false;
if (!action.move.beforeTurnCallback) throw new Error(`beforeTurnMove has no beforeTurnCallback`);
action.move.beforeTurnCallback.call(this, action.pokemon, target);
break;
}
case 'event':
this.runEvent(action.event!, action.pokemon);
break;
case 'team':
if (action.index === 0) {
action.pokemon.side.pokemon = [];
}
action.pokemon.side.pokemon.push(action.pokemon);
action.pokemon.position = action.index;
// we return here because the update event would crash since there are no active pokemon yet
return;
case 'pass':
return;
case 'instaswitch':
case 'switch':
if (action.choice === 'switch' && action.pokemon.status) {
this.singleEvent('CheckShow', this.dex.abilities.getByID('naturalcure' as ID), null, action.pokemon);
}
if (this.actions.switchIn(action.target, action.pokemon.position, action.sourceEffect) === 'pursuitfaint') {
// a pokemon fainted from Pursuit before it could switch
if (this.gen <= 4) {
// in gen 2-4, the switch still happens
this.hint("Previously chosen switches continue in Gen 2-4 after a Pursuit target faints.");
action.priority = -101;
this.queue.unshift(action);
break;
} else {
// in gen 5+, the switch is cancelled
this.hint("A Pokemon can't switch between when it runs out of HP and when it faints");
break;
}
}
break;
case 'runSwitch':
this.actions.runSwitch(action.pokemon);
break;
case 'shift':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
this.swapPosition(action.pokemon, 1);
break;
case 'beforeTurn':
this.eachEvent('BeforeTurn');
break;
case 'residual':
this.add('');
this.clearActiveMove(true);
this.updateSpeed();
residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const);
this.fieldEvent('Residual');
this.add('upkeep');
break;
}
// phazing (Roar, etc)
for (const side of this.sides) {
for (const pokemon of side.active) {
if (pokemon.forceSwitchFlag) {
if (pokemon.hp) this.actions.dragIn(pokemon.side, pokemon.position);
pokemon.forceSwitchFlag = false;
}
}
}
this.clearActiveMove();
// fainting
this.faintMessages();
if (this.ended) return true;
// switching (fainted pokemon, U-turn, Baton Pass, etc)
if (!this.queue.peek() || (this.gen <= 3 && ['move', 'residual'].includes(this.queue.peek()!.choice))) {
// in gen 3 or earlier, switching in fainted pokemon is done after
// every move, rather than only at the end of the turn.
this.checkFainted();
} else if (action.choice === 'megaEvo' && this.gen === 7) {
this.eachEvent('Update');
// In Gen 7, the action order is recalculated for a Pokémon that mega evolves.
for (const [i, queuedAction] of this.queue.list.entries()) {
if (queuedAction.pokemon === action.pokemon && queuedAction.choice === 'move') {
this.queue.list.splice(i, 1);
queuedAction.mega = 'done';
this.queue.insertChoice(queuedAction, true);
break;
}
}
return false;
} else if (this.queue.peek()?.choice === 'instaswitch') {
return false;
}
if (this.gen >= 5) {
this.eachEvent('Update');
for (const [pokemon, originalHP] of residualPokemon) {
const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp);
if (pokemon.hp && pokemon.getUndynamaxedHP() <= maxhp / 2 && originalHP > maxhp / 2) {
this.runEvent('EmergencyExit', pokemon);
}
}
}
if (action.choice === 'runSwitch') {
const pokemon = action.pokemon;
if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP! > pokemon.maxhp / 2) {
this.runEvent('EmergencyExit', pokemon);
}
}
const switches = this.sides.map(
side => side.active.some(pokemon => pokemon && !!pokemon.switchFlag)
);
for (let i = 0; i < this.sides.length; i++) {
if (switches[i] && !this.canSwitch(this.sides[i])) {
for (const pokemon of this.sides[i].active) {
pokemon.switchFlag = false;
}
switches[i] = false;
} else if (switches[i]) {
for (const pokemon of this.sides[i].active) {
if (pokemon.switchFlag && !pokemon.skipBeforeSwitchOutEventFlag) {
this.runEvent('BeforeSwitchOut', pokemon);
pokemon.skipBeforeSwitchOutEventFlag = true;
this.faintMessages(); // Pokemon may have fainted in BeforeSwitchOut
if (this.ended) return true;
if (pokemon.fainted) {
switches[i] = this.sides[i].active.some(sidePokemon => sidePokemon && !!sidePokemon.switchFlag);
}
}
}
}
}
for (const playerSwitch of switches) {
if (playerSwitch) {
this.makeRequest('switch');
return true;
}
}
if (this.gen < 5) this.eachEvent('Update');
if (this.gen >= 8 && this.queue.peek()?.choice === 'move') {
// In gen 8, speed is updated dynamically so update the queue's speed properties and sort it.
this.updateSpeed();
for (const queueAction of this.queue.list) {
if (queueAction.pokemon) this.getActionSpeed(queueAction);
}
this.queue.sort();
}
return false;
},
actions: {
runMove(moveOrMoveName, pokemon, targetLoc, options) {
pokemon.activeMoveActions++;
const zMove = options?.zMove;
const maxMove = options?.maxMove;
const externalMove = options?.externalMove;
const originalTarget = options?.originalTarget;
let sourceEffect = options?.sourceEffect;
let target = this.battle.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget);
let baseMove = this.dex.getActiveMove(moveOrMoveName);
const priority = baseMove.priority;
const pranksterBoosted = baseMove.pranksterBoosted;
if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) {
const changedMove = this.battle.runEvent('OverrideAction', pokemon, target, baseMove);
if (changedMove && changedMove !== true) {
baseMove = this.dex.getActiveMove(changedMove);
baseMove.priority = priority;
if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted;
target = this.battle.getRandomTarget(pokemon, baseMove);
}
}
let move = baseMove;
if (zMove) {
move = this.getActiveZMove(baseMove, pokemon);
} else if (maxMove) {
move = this.getActiveMaxMove(baseMove, pokemon);
}
move.isExternal = externalMove;
this.battle.setActiveMove(move, pokemon, target);
/* if (pokemon.moveThisTurn) {
// THIS IS PURELY A SANITY CHECK
// DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING;
// USE this.queue.cancelMove INSTEAD
this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn);
this.clearActiveMove(true);
return;
} */
const willTryMove = this.battle.runEvent('BeforeMove', pokemon, target, move);
if (!willTryMove) {
if (pokemon.volatiles['twoturnmove']?.move === move.id) {
pokemon.removeVolatile('twoturnmove');
}
this.battle.runEvent('MoveAborted', pokemon, target, move);
this.battle.clearActiveMove(true);
// The event 'BeforeMove' could have returned false or null
// false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power
// null indicates the opposite, as the Pokemon didn't have an option to choose anything
pokemon.moveThisTurnResult = willTryMove;
return;
}
if (move.beforeMoveCallback) {
if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) {
this.battle.clearActiveMove(true);
pokemon.moveThisTurnResult = false;
return;
}
}
pokemon.lastDamage = 0;
let lockedMove;
if (!externalMove) {
lockedMove = this.battle.runEvent('LockMove', pokemon);
if (lockedMove === true) lockedMove = false;
if (!lockedMove) {
if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) {
this.battle.add('cant', pokemon, 'nopp', move);
const gameConsole = [
null, 'Game Boy', 'Game Boy Color', 'Game Boy Advance', 'DS', 'DS', '3DS', '3DS',
][this.battle.gen] || 'Switch';
this.battle.hint(`This is not a bug, this is really how it works on the ${gameConsole}; try it yourself if you don't believe us.`);
this.battle.clearActiveMove(true);
pokemon.moveThisTurnResult = false;
return;
}
} else {
sourceEffect = this.dex.conditions.get('lockedmove');
}
pokemon.moveUsed(move, targetLoc);
}
// Dancer Petal Dance hack
// TODO: implement properly
const noLock = externalMove && !pokemon.volatiles.lockedmove;
if (zMove) {
if (pokemon.illusion) {
this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon);
}
this.battle.add('-zpower', pokemon);
pokemon.side.zMoveUsed = true;
}
const moveDidSomething = this.useMove(baseMove, pokemon, { target, sourceEffect, zMove, maxMove });
this.battle.lastSuccessfulMoveThisTurn = moveDidSomething ? this.battle.activeMove && this.battle.activeMove.id : null;
if (this.battle.activeMove) move = this.battle.activeMove;
this.battle.singleEvent('AfterMove', move, null, pokemon, target, move);
this.battle.runEvent('AfterMove', pokemon, target, move);
// Dancer's activation order is completely different from any other event, so it's handled separately
if (move.flags['dance'] && moveDidSomething && !move.isExternal) {
const dancers = [];
for (const currentPoke of this.battle.getAllActive()) {
if (pokemon === currentPoke) continue;
if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) {
dancers.push(currentPoke);
}
}
// Dancer activates in order of lowest speed stat to highest
// Note that the speed stat used is after any volatile replacements like Speed Swap,
// but before any multipliers like Agility or Choice Scarf
// Ties go to whichever Pokemon has had the ability for the least amount of time
dancers.sort((a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) ||
b.abilityState.effectOrder - a.abilityState.effectOrder);
for (const dancer of dancers) {
if (this.battle.faintMessages()) break;
if (dancer.fainted) continue;
this.battle.add('-activate', dancer, 'ability: Dancer');
const dancersTarget = !target!.isAlly(dancer) && pokemon.isAlly(dancer) ? target! : pokemon;
this.runMove(move.id, dancer, dancer.getLocOf(dancersTarget),
{ sourceEffect: this.dex.abilities.get('dancer'), externalMove: true });
}
}
if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove'];
this.battle.faintMessages();
this.battle.checkWin();
},
},
queue: {
resolveAction(action, midTurn = false) {
if (!action) throw new Error(`Action not passed to resolveAction`);
if (action.choice === 'pass') return [];
const actions = [action];
if (!action.side && action.pokemon) action.side = action.pokemon.side;
if (!action.move && action.moveid) action.move = this.battle.dex.getActiveMove(action.moveid);
if (!action.order) {
const orders: { [choice: string]: number } = {
team: 1,
start: 2,
instaswitch: 3,
beforeTurn: 4,
beforeTurnMove: 5,
runUnnerve: 100,
runSwitch: 101,
// runPrimal: 102, (deprecated)
switch: 103,
megaEvo: 104,
runDynamax: 105,
shift: 200,
// default is 200 (for moves)
residual: 300,
};
if (action.choice in orders) {
action.order = orders[action.choice];
} else {
action.order = 200;
if (!['move', 'event'].includes(action.choice)) {
throw new Error(`Unexpected orderless action ${action.choice}`);
}
}
}
if (!midTurn) {
if (action.choice === 'move') {
if (!action.maxMove && !action.zmove && action.move.beforeTurnCallback) {
actions.unshift(...this.resolveAction({
choice: 'beforeTurnMove', pokemon: action.pokemon, move: action.move, targetLoc: action.targetLoc,
}));
}
if (action.mega && !action.pokemon.isSkyDropped()) {
actions.unshift(...this.resolveAction({
choice: 'megaEvo',
pokemon: action.pokemon,
}));
}
if (action.maxMove && !action.pokemon.volatiles['dynamax']) {
actions.unshift(...this.resolveAction({
choice: 'runDynamax',
pokemon: action.pokemon,
}));
}
action.fractionalPriority = this.battle.runEvent('FractionalPriority', action.pokemon, null, action.move, 0);
const linkedMoves: [string, string] = action.pokemon.getLinkedMoves();
if (
linkedMoves.length &&
!(action.pokemon.getItem().isChoice || action.pokemon.hasAbility('gorillatactics')) &&
!action.zmove && !action.maxMove
) {
const decisionMove = this.battle.toID(action.move);
if (linkedMoves.includes(decisionMove)) {
action.linked = linkedMoves.map(moveid => this.battle.dex.getActiveMove(moveid));
const linkedOtherMove = action.linked[1 - linkedMoves.indexOf(decisionMove)];
if (linkedOtherMove.beforeTurnCallback) {
this.addChoice({
choice: 'beforeTurnMove',
pokemon: action.pokemon,
move: linkedOtherMove,
targetLoc: action.targetLoc,
});
}
}
}
} else if (['switch', 'instaswitch'].includes(action.choice)) {
if (typeof action.pokemon.switchFlag === 'string') {
action.sourceEffect = this.battle.dex.moves.get(action.pokemon.switchFlag as ID) as any;
}
action.pokemon.switchFlag = false;
}
}
const deferPriority = this.battle.gen === 7 && action.mega && action.mega !== 'done';
if (action.move) {
let target = null;
action.move = this.battle.dex.getActiveMove(action.move);
if (!action.targetLoc) {
target = this.battle.getRandomTarget(action.pokemon, action.move);
// TODO: what actually happens here?
if (target) action.targetLoc = action.pokemon.getLocOf(target);
}
action.originalTarget = action.pokemon.getAtLoc(action.targetLoc);
}
if (!deferPriority) this.battle.getActionSpeed(action);
return actions as any;
},
},
pokemon: {
moveUsed(move, targetLoc) {
if (!this.moveThisTurn) this.m.lastMoveAbsolute = move;
this.lastMove = move;
this.moveThisTurn = move.id;
this.lastMoveTargetLoc = targetLoc;
},
getLinkedMoves(ignoreDisabled) {
const linkedMoves = this.moveSlots.slice(0, 2);
if (linkedMoves.length !== 2 || linkedMoves[0].pp <= 0 || linkedMoves[1].pp <= 0) return [];
const ret = [linkedMoves[0].id, linkedMoves[1].id];
if (ignoreDisabled) return ret;
if (!this.ateBerry && ret.includes('belch' as ID)) return [];
if (this.hasItem('assaultvest') &&
(this.battle.dex.moves.get(ret[0]).category === 'Status' || this.battle.dex.moves.get(ret[1]).category === 'Status')) {
return [];
}
return ret;
},
hasLinkedMove(moveid) {
// @ts-expect-error modded
const linkedMoves: ID[] = this.getLinkedMoves(true);
if (!linkedMoves.length) return false;
return linkedMoves.some(x => x === moveid);
},
},
};