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

544 lines
20 KiB
TypeScript

export const Scripts: ModdedBattleScriptsData = {
gen: 9,
init() {
for (const i in this.data.Items) {
const item = this.data.Items[i];
if (!item.megaStone && !item.onDrive && !(item.onPlate && !item.zMove) && !item.onMemory) continue;
this.modData('Items', i).onTakeItem = false;
if (item.isNonstandard) this.modData('Items', i).isNonstandard = null;
if (item.megaStone) {
this.modData('FormatsData', this.toID(item.megaStone)).isNonstandard = null;
}
}
},
start() {
// Deserialized games should use restart()
if (this.deserialized) return;
// need all players to start
if (!this.sides.every(side => !!side)) throw new Error(`Missing sides: ${this.sides}`);
if (this.started) throw new Error(`Battle already started`);
const format = this.format;
this.started = true;
if (this.gameType === 'multi') {
this.sides[1].foe = this.sides[2]!;
this.sides[0].foe = this.sides[3]!;
this.sides[2]!.foe = this.sides[1];
this.sides[3]!.foe = this.sides[0];
this.sides[1].allySide = this.sides[3]!;
this.sides[0].allySide = this.sides[2]!;
this.sides[2]!.allySide = this.sides[0];
this.sides[3]!.allySide = this.sides[1];
// sync side conditions
this.sides[2]!.sideConditions = this.sides[0].sideConditions;
this.sides[3]!.sideConditions = this.sides[1].sideConditions;
} else {
this.sides[1].foe = this.sides[0];
this.sides[0].foe = this.sides[1];
if (this.sides.length > 2) { // ffa
this.sides[2]!.foe = this.sides[3]!;
this.sides[3]!.foe = this.sides[2]!;
}
}
for (const side of this.sides) {
this.add('teamsize', side.id, side.pokemon.length);
}
this.add('gen', this.gen);
this.add('tier', format.name);
if (this.rated) {
if (this.rated === 'Rated battle') this.rated = true;
this.add('rated', typeof this.rated === 'string' ? this.rated : '');
}
if (format.onBegin) format.onBegin.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
if (subFormat.onBegin) subFormat.onBegin.call(this);
}
for (const pokemon of this.getAllPokemon()) {
const item = pokemon.getItem();
if (item.forcedForme && !item.zMove && item.forcedForme !== pokemon.species.name) {
const rawSpecies = (this.actions as any).getMixedSpecies(pokemon.m.originalSpecies, item.forcedForme, pokemon);
const species = pokemon.setSpecies(rawSpecies);
if (!species) continue;
pokemon.baseSpecies = rawSpecies;
pokemon.details = pokemon.getUpdatedDetails();
pokemon.ability = this.toID(species.abilities['0']);
pokemon.baseAbility = pokemon.ability;
}
}
if (this.sides.some(side => !side.pokemon[0])) {
throw new Error('Battle not started: A player has an empty team.');
}
if (this.debugMode) {
this.checkEVBalance();
}
if (format.onTeamPreview) format.onTeamPreview.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
if (subFormat.onTeamPreview) subFormat.onTeamPreview.call(this);
}
this.queue.addChoice({ choice: 'start' });
this.midTurn = true;
if (!this.requestState) this.turnLoop();
},
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');
// Change Pokemon holding Rusted items into their Crowned formes
for (const pokemon of this.getAllPokemon()) {
let rawSpecies: Species | null = null;
const item = pokemon.getItem();
if (item.id === 'rustedsword') {
rawSpecies = (this.actions as any).getMixedSpecies(pokemon.m.originalSpecies, 'Zacian-Crowned', pokemon);
} else if (item.id === 'rustedshield') {
rawSpecies = (this.actions as any).getMixedSpecies(pokemon.m.originalSpecies, 'Zamazenta-Crowned', pokemon);
}
if (!rawSpecies) continue;
const species = pokemon.setSpecies(rawSpecies);
if (!species) continue;
pokemon.baseSpecies = rawSpecies;
pokemon.details = pokemon.getUpdatedDetails();
pokemon.ability = this.toID(species.abilities['0']);
pokemon.baseAbility = pokemon.ability;
const behemothMove: { [k: string]: string } = {
'Rusted Sword': 'behemothblade', 'Rusted Shield': 'behemothbash',
};
const ironHead = pokemon.baseMoves.indexOf('ironhead');
if (ironHead >= 0) {
const move = this.dex.moves.get(behemothMove[pokemon.getItem().name]);
pokemon.baseMoveSlots[ironHead] = {
move: move.name,
id: move.id,
pp: move.noPPBoosts ? move.pp : move.pp * 8 / 5,
maxpp: move.noPPBoosts ? move.pp : move.pp * 8 / 5,
target: move.target,
disabled: false,
disabledSource: '',
used: false,
};
pokemon.moveSlots = pokemon.baseMoveSlots.slice();
}
}
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;
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 'terastallize':
this.actions.terastallize(action.pokemon);
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 'priorityChargeMove':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
this.debug('priority charge callback: ' + action.move.id);
if (!action.move.priorityChargeCallback) throw new Error(`priorityChargeMove has no priorityChargeCallback`);
action.move.priorityChargeCallback.call(this, action.pokemon);
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 'revivalblessing':
action.pokemon.side.pokemonLeft++;
if (action.target.position < action.pokemon.side.active.length) {
this.queue.addChoice({
choice: 'instaswitch',
pokemon: action.target,
target: action.target,
});
}
action.target.fainted = false;
action.target.faintQueued = false;
action.target.subFainted = false;
action.target.status = '';
action.target.hp = 1; // Needed so hp functions works
action.target.sethp(action.target.maxhp / 2);
this.add('-heal', action.target, action.target.getHealth, '[from] move: Revival Blessing');
action.pokemon.side.removeSlotCondition(action.pokemon, 'revivalblessing');
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 && action.choice !== 'start') {
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++) {
let reviveSwitch = false; // Used to ignore the fake switch for Revival Blessing
if (switches[i] && !this.canSwitch(this.sides[i])) {
for (const pokemon of this.sides[i].active) {
if (this.sides[i].slotConditions[pokemon.position]['revivalblessing']) {
reviveSwitch = true;
continue;
}
pokemon.switchFlag = false;
}
if (!reviveSwitch) switches[i] = false;
} else if (switches[i]) {
for (const pokemon of this.sides[i].active) {
if (pokemon.switchFlag && pokemon.switchFlag !== 'revivalblessing' && !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' || this.queue.peek()?.choice === 'runDynamax')) {
// 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: {
canMegaEvo(pokemon) {
if (pokemon.species.isMega) return null;
const item = pokemon.getItem();
if (item.megaStone) {
if (item.megaStone === pokemon.baseSpecies.name) return null;
return item.megaStone;
} else {
return null;
}
},
runMegaEvo(pokemon) {
if (pokemon.species.isMega) return false;
const species: Species = (this as any).getMixedSpecies(pokemon.m.originalSpecies, pokemon.canMegaEvo, pokemon);
/* Do we have a proper sprite for it? Code for when megas actually exist
if (this.dex.species.get(pokemon.canMegaEvo!).baseSpecies === pokemon.m.originalSpecies) {
pokemon.formeChange(species, pokemon.getItem(), true);
} else { */
const oSpecies = this.dex.species.get(pokemon.m.originalSpecies);
const oMegaSpecies = this.dex.species.get((species as any).originalSpecies);
pokemon.formeChange(species, pokemon.getItem(), true);
this.battle.add('-start', pokemon, oMegaSpecies.requiredItem, '[silent]');
if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) {
this.battle.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]');
}
// }
pokemon.canMegaEvo = null;
return true;
},
terastallize(pokemon) {
if (pokemon.illusion?.species.baseSpecies === 'Ogerpon') {
this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon);
}
if (pokemon.illusion?.species.baseSpecies === 'Terapagos') {
this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon);
}
let type = pokemon.teraType;
if (pokemon.species.baseSpecies !== 'Ogerpon' && pokemon.getItem().name.endsWith('Mask')) {
type = this.dex.species.get(pokemon.getItem().forcedForme).forceTeraType!;
}
this.battle.add('-terastallize', pokemon, type);
pokemon.terastallized = type;
for (const ally of pokemon.side.pokemon) {
ally.canTerastallize = null;
}
pokemon.addedType = '';
pokemon.knownType = true;
pokemon.apparentType = type;
if (pokemon.species.baseSpecies === 'Ogerpon') {
const tera = pokemon.species.id === 'ogerpon' ? 'tealtera' : 'tera';
pokemon.formeChange(pokemon.species.id + tera, pokemon.getItem(), true);
} else {
if (pokemon.getItem().name.endsWith('Mask')) {
const species: Species = (this as any).getMixedSpecies(pokemon.m.originalSpecies,
pokemon.getItem().forcedForme! + '-Tera', pokemon);
const oSpecies = this.dex.species.get(pokemon.m.originalSpecies);
const originalTeraSpecies = this.dex.species.get((species as any).originalSpecies);
pokemon.formeChange(species, pokemon.getItem(), true);
this.battle.add('-start', pokemon, originalTeraSpecies.requiredItem, '[silent]');
if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) {
this.battle.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]');
}
}
}
if (pokemon.species.name === 'Terapagos-Terastal' && type === 'Stellar') {
pokemon.formeChange('Terapagos-Stellar', null, true);
}
this.battle.runEvent('AfterTerastallization', pokemon);
},
getMixedSpecies(originalForme, formeChange, pokemon) {
const originalSpecies = this.dex.species.get(originalForme);
const formeChangeSpecies = this.dex.species.get(formeChange);
if (originalSpecies.baseSpecies === formeChangeSpecies.baseSpecies &&
!formeChangeSpecies.isMega && !formeChangeSpecies.isPrimal) {
return formeChangeSpecies;
}
const deltas = (this as any).getFormeChangeDeltas(formeChangeSpecies, pokemon);
const species = (this as any).mutateOriginalSpecies(originalSpecies, deltas);
return species;
},
getFormeChangeDeltas(formeChangeSpecies, pokemon) {
const baseSpecies = this.dex.species.get(formeChangeSpecies.baseSpecies);
const deltas: {
ability: string,
baseStats: SparseStatsTable,
weighthg: number,
heightm: number,
originalSpecies: string,
requiredItem: string | undefined,
type?: string,
formeType?: string,
} = {
ability: formeChangeSpecies.abilities['0'],
baseStats: {},
weighthg: formeChangeSpecies.weighthg - baseSpecies.weighthg,
heightm: ((formeChangeSpecies.heightm * 10) - (baseSpecies.heightm * 10)) / 10,
originalSpecies: formeChangeSpecies.name,
requiredItem: formeChangeSpecies.requiredItem,
};
let statId: StatID;
for (statId in formeChangeSpecies.baseStats) {
deltas.baseStats[statId] = formeChangeSpecies.baseStats[statId] - baseSpecies.baseStats[statId];
}
let formeType: string | null = null;
if (['Arceus', 'Silvally'].includes(baseSpecies.name)) {
deltas.type = formeChangeSpecies.types[0];
formeType = 'Arceus';
} else if (formeChangeSpecies.types.length > baseSpecies.types.length) {
deltas.type = formeChangeSpecies.types[1];
} else if (formeChangeSpecies.types.length < baseSpecies.types.length) {
deltas.type = this.battle.ruleTable.has('mixandmegaoldaggronite') ? 'mono' : baseSpecies.types[0];
} else if (formeChangeSpecies.types[1] !== baseSpecies.types[1]) {
deltas.type = formeChangeSpecies.types[1];
}
if (formeChangeSpecies.isMega) formeType = 'Mega';
if (formeChangeSpecies.isPrimal) formeType = 'Primal';
if (formeChangeSpecies.name.endsWith('Crowned')) formeType = 'Crowned';
if (formeType) deltas.formeType = formeType;
if (!deltas.formeType && formeChangeSpecies.abilities['H'] &&
pokemon && pokemon.baseSpecies.abilities['H'] === pokemon.getAbility().name) {
deltas.ability = formeChangeSpecies.abilities['H'];
}
return deltas;
},
mutateOriginalSpecies(speciesOrForme, deltas) {
if (!deltas) throw new TypeError("Must specify deltas!");
const species = this.dex.deepClone(this.dex.species.get(speciesOrForme));
species.abilities = { '0': deltas.ability };
if (deltas.formeType === 'Arceus') {
const secondType = species.types[1];
species.types = [deltas.type];
if (secondType && secondType !== deltas.type) species.types.push(secondType);
} else if (species.types[0] === deltas.type) {
species.types = [deltas.type];
} else if (deltas.type === 'mono') {
species.types = [species.types[0]];
} else if (deltas.type) {
species.types = [species.types[0], deltas.type];
}
const baseStats = species.baseStats;
for (const statName in baseStats) {
baseStats[statName] = this.battle.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255);
}
species.weighthg = Math.max(1, species.weighthg + deltas.weighthg);
species.heightm = Math.max(0.1, ((species.heightm * 10) + (deltas.heightm * 10)) / 10);
species.originalSpecies = deltas.originalSpecies;
species.requiredItem = deltas.requiredItem;
if (deltas.formeType === 'Mega') species.isMega = true;
if (deltas.formeType === 'Primal') species.isPrimal = true;
return species;
},
},
};