mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-18 03:01:00 -05:00
This mostly serves to provide cleaner and more consistent field naming. maxMove currently doesn't have boosts or effects to group together but who knows what will be thrown at us via DLC, and being symmetrical with zMoves is a nice.
483 lines
17 KiB
TypeScript
483 lines
17 KiB
TypeScript
export const BattleScripts: ModdedBattleScriptsData = {
|
|
inherit: 'gen7',
|
|
runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect, zMove, externalMove) {
|
|
pokemon.activeMoveActions++;
|
|
let target = this.getTarget(pokemon, zMove || moveOrMoveName, targetLoc);
|
|
let baseMove = this.dex.getActiveMove(moveOrMoveName);
|
|
const pranksterBoosted = baseMove.pranksterBoosted;
|
|
if (!sourceEffect && baseMove.id !== 'struggle' && !zMove) {
|
|
const changedMove = this.runEvent('OverrideAction', pokemon, target, baseMove);
|
|
if (changedMove && changedMove !== true) {
|
|
baseMove = this.dex.getActiveMove(changedMove);
|
|
if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted;
|
|
target = this.getRandomTarget(pokemon, baseMove);
|
|
}
|
|
}
|
|
let move = zMove ? this.getActiveZMove(baseMove, pokemon) : baseMove;
|
|
|
|
move.isExternal = externalMove;
|
|
|
|
this.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.runEvent('BeforeMove', pokemon, target, move);
|
|
if (!willTryMove) {
|
|
this.runEvent('MoveAborted', pokemon, target, move);
|
|
this.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, pokemon, target, move)) {
|
|
this.clearActiveMove(true);
|
|
pokemon.moveThisTurnResult = false;
|
|
return;
|
|
}
|
|
}
|
|
pokemon.lastDamage = 0;
|
|
let lockedMove;
|
|
if (!externalMove) {
|
|
lockedMove = this.runEvent('LockMove', pokemon);
|
|
if (lockedMove === true) lockedMove = false;
|
|
if (!lockedMove) {
|
|
if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) {
|
|
this.add('cant', pokemon, 'nopp', move);
|
|
const gameConsole = [
|
|
null, 'Game Boy', 'Game Boy Color', 'Game Boy Advance', 'DS', 'DS', '3DS', '3DS',
|
|
][this.gen] || 'Switch';
|
|
this.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.clearActiveMove(true);
|
|
pokemon.moveThisTurnResult = false;
|
|
return;
|
|
}
|
|
} else {
|
|
sourceEffect = this.dex.getEffect('lockedmove');
|
|
}
|
|
pokemon.moveUsed(move, targetLoc);
|
|
}
|
|
|
|
// Dancer Petal Dance hack
|
|
// TODO: implement properly
|
|
const noLock = externalMove && !pokemon.volatiles.lockedmove;
|
|
|
|
if (zMove) {
|
|
if (pokemon.illusion) {
|
|
this.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon);
|
|
}
|
|
this.add('-zpower', pokemon);
|
|
pokemon.m.zMoveUsed = true;
|
|
}
|
|
const moveDidSomething = this.useMove(baseMove, pokemon, target, sourceEffect, zMove);
|
|
if (this.activeMove) move = this.activeMove;
|
|
this.singleEvent('AfterMove', move, null, pokemon, target, move);
|
|
this.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.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.abilityOrder - a.abilityOrder
|
|
);
|
|
for (const dancer of dancers) {
|
|
if (this.faintMessages()) break;
|
|
this.add('-activate', dancer, 'ability: Dancer');
|
|
this.runMove(move.id, dancer, 0, this.dex.getAbility('dancer'), undefined, true);
|
|
}
|
|
}
|
|
if (noLock && pokemon.volatiles.lockedmove) delete pokemon.volatiles.lockedmove;
|
|
},
|
|
// Modded to allow arrays as Mega Stone options
|
|
canMegaEvo(pokemon) {
|
|
const altForme = pokemon.baseSpecies.otherFormes && this.dex.getSpecies(pokemon.baseSpecies.otherFormes[0]);
|
|
const item = pokemon.getItem();
|
|
if (
|
|
altForme?.isMega && altForme?.requiredMove &&
|
|
pokemon.baseMoves.includes(toID(altForme.requiredMove)) && !item.zMove
|
|
) {
|
|
return altForme.name;
|
|
}
|
|
if (
|
|
item.megaEvolves !== pokemon.baseSpecies.name ||
|
|
(Array.isArray(item.megaStone) && item.megaStone.includes(pokemon.species.name)) ||
|
|
(typeof item.megaStone === 'string' && item.megaStone === pokemon.species.name)
|
|
) {
|
|
return null;
|
|
}
|
|
if (Array.isArray(item.megaStone)) {
|
|
return item.megaStone[this.random(item.megaStone.length)];
|
|
}
|
|
return item.megaStone;
|
|
},
|
|
// Modded to allow unlimited mega evos
|
|
runMegaEvo(pokemon) {
|
|
const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst;
|
|
if (!speciesid) return false;
|
|
const side = pokemon.side;
|
|
|
|
// Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now.
|
|
for (const foeActive of side.foe.active) {
|
|
if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
pokemon.formeChange(speciesid, pokemon.getItem(), true);
|
|
|
|
// Limit mega evolution to once-per-Pokemon
|
|
pokemon.canMegaEvo = null;
|
|
|
|
this.runEvent('AfterMega', pokemon);
|
|
|
|
// E4 flint gains fire type when mega evolving
|
|
if (pokemon.name === 'E4 Flint' && !pokemon.illusion) this.add('-start', pokemon, 'typeadd', 'Fire');
|
|
// Overneat gains the fairy type when mega evolving
|
|
if (pokemon.name === 'Overneat' && !pokemon.illusion) this.add('-start', pokemon, 'typeadd', 'Fairy');
|
|
|
|
return true;
|
|
},
|
|
getZMove(move, pokemon, skipChecks) {
|
|
const item = pokemon.getItem();
|
|
if (!skipChecks) {
|
|
if (!item.zMove) return;
|
|
if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return;
|
|
const moveData = pokemon.getMoveData(move);
|
|
// Draining the PP of the base move prevents the corresponding Z-move from being used.
|
|
if (!moveData || !moveData.pp) return;
|
|
}
|
|
|
|
if (item.zMoveFrom) {
|
|
if (Array.isArray(item.zMoveFrom)) {
|
|
if (item.zMoveFrom.includes(move.name)) return item.zMove as string;
|
|
} else {
|
|
if (move.name === item.zMoveFrom) return item.zMove as string;
|
|
}
|
|
} else if (item.zMove === true) {
|
|
if (move.type === item.zMoveType) {
|
|
if (move.category === "Status") {
|
|
return move.name;
|
|
} else if (move.zMove?.basePower) {
|
|
return this.zMoveTable[move.type];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
getActiveZMove(move, pokemon) {
|
|
let zMove;
|
|
if (pokemon) {
|
|
const item = pokemon.getItem();
|
|
const zMoveFrom = Array.isArray(item.zMoveFrom) ? item.zMoveFrom : item.zMoveFrom ? [item.zMoveFrom] : null;
|
|
if (zMoveFrom?.includes(move.name)) {
|
|
zMove = this.dex.getActiveMove(item.zMove as string);
|
|
// Hack for Snaquaza's Z move
|
|
zMove.baseMove = move.id;
|
|
zMove.isZOrMaxPowered = true;
|
|
return zMove;
|
|
}
|
|
}
|
|
|
|
if (move.category === 'Status') {
|
|
zMove = this.dex.getActiveMove(move);
|
|
zMove.isZ = true;
|
|
zMove.isZOrMaxPowered = true;
|
|
return zMove;
|
|
}
|
|
zMove = this.dex.getActiveMove(this.zMoveTable[move.type]);
|
|
zMove.basePower = move.zMove!.basePower!;
|
|
zMove.category = move.category;
|
|
zMove.isZOrMaxPowered = true;
|
|
return zMove;
|
|
},
|
|
// Modded to allow each Pokemon on a team to use a Z move once per battle
|
|
canZMove(pokemon) {
|
|
if (
|
|
pokemon.m?.zMoveUsed ||
|
|
(pokemon.transformed && (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra"))
|
|
) {
|
|
return;
|
|
}
|
|
const item = pokemon.getItem();
|
|
if (!item.zMove) return;
|
|
if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return;
|
|
let atLeastOne = false;
|
|
let mustStruggle = true;
|
|
const zMoves: ZMoveOptions = [];
|
|
for (const moveSlot of pokemon.moveSlots) {
|
|
if (moveSlot.pp <= 0) {
|
|
zMoves.push(null);
|
|
continue;
|
|
}
|
|
if (!moveSlot.disabled) {
|
|
mustStruggle = false;
|
|
}
|
|
const move = this.dex.getMove(moveSlot.move);
|
|
let zMoveName = this.getZMove(move, pokemon, true) || '';
|
|
if (zMoveName) {
|
|
const zMove = this.dex.getMove(zMoveName);
|
|
if (!zMove.isZ && zMove.category === 'Status') zMoveName = "Z-" + zMoveName;
|
|
zMoves.push({move: zMoveName, target: zMove.target});
|
|
} else {
|
|
zMoves.push(null);
|
|
}
|
|
if (zMoveName) atLeastOne = true;
|
|
}
|
|
if (atLeastOne && !mustStruggle) return zMoves;
|
|
},
|
|
runZPower(move, pokemon) {
|
|
const zPower = this.dex.getEffect('zpower');
|
|
if (move.category !== 'Status') {
|
|
this.attrLastMove('[zeffect]');
|
|
} else if (move.zMove?.boost) {
|
|
this.boost(move.zMove.boost, pokemon, pokemon, zPower);
|
|
} else {
|
|
switch (move.zMove?.effect) {
|
|
case 'heal':
|
|
this.heal(pokemon.maxhp, pokemon, pokemon, zPower);
|
|
break;
|
|
case 'healhalf':
|
|
// For DragonWhale
|
|
this.heal(pokemon.baseMaxhp / 2, pokemon, pokemon, zPower);
|
|
break;
|
|
case 'healreplacement':
|
|
move.self = {sideCondition: 'healreplacement'};
|
|
break;
|
|
case 'boostreplacement':
|
|
// For nui
|
|
move.self = {sideCondition: 'boostreplacement'};
|
|
break;
|
|
case 'clearnegativeboost':
|
|
const boosts: SparseBoostsTable = {};
|
|
let i: BoostName;
|
|
for (i in pokemon.boosts) {
|
|
if (pokemon.boosts[i] < 0) {
|
|
boosts[i] = 0;
|
|
}
|
|
}
|
|
pokemon.setBoost(boosts);
|
|
this.add('-clearnegativeboost', pokemon, '[zeffect]');
|
|
break;
|
|
case 'redirect':
|
|
pokemon.addVolatile('followme', pokemon, zPower);
|
|
break;
|
|
case 'crit2':
|
|
pokemon.addVolatile('focusenergy', pokemon, zPower);
|
|
break;
|
|
case 'curse':
|
|
if (pokemon.hasType('Ghost')) {
|
|
this.heal(pokemon.maxhp, pokemon, pokemon, zPower);
|
|
} else {
|
|
this.boost({atk: 1}, pokemon, pokemon, zPower);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Modded to account for guts clones
|
|
modifyDamage(baseDamage, pokemon, target, move, suppressMessages) {
|
|
const tr = this.trunc;
|
|
if (!move.type) move.type = '???';
|
|
const type = move.type;
|
|
|
|
baseDamage += 2;
|
|
|
|
// multi-target modifier (doubles only)
|
|
if (move.spreadHit) {
|
|
const spreadModifier = move.spreadModifier || (this.gameType === 'free-for-all' ? 0.5 : 0.75);
|
|
this.debug('Spread modifier: ' + spreadModifier);
|
|
baseDamage = this.modify(baseDamage, spreadModifier);
|
|
}
|
|
|
|
// weather modifier
|
|
baseDamage = this.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage);
|
|
|
|
// crit - not a modifier
|
|
const isCrit = target.getMoveHitData(move).crit;
|
|
if (isCrit) {
|
|
baseDamage = tr(baseDamage * (move.critModifier || (this.gen >= 6 ? 1.5 : 2)));
|
|
}
|
|
|
|
// random factor - also not a modifier
|
|
baseDamage = this.randomizer(baseDamage);
|
|
|
|
// STAB
|
|
if (move.forceSTAB || (type !== '???' && pokemon.hasType(type))) {
|
|
// The "???" type never gets STAB
|
|
// Not even if you Roost in Gen 4 and somehow manage to use
|
|
// Struggle in the same turn.
|
|
// (On second thought, it might be easier to get a MissingNo.)
|
|
baseDamage = this.modify(baseDamage, move.stab || 1.5);
|
|
}
|
|
// types
|
|
let typeMod = target.runEffectiveness(move);
|
|
typeMod = this.dex.clampIntRange(typeMod, -6, 6);
|
|
target.getMoveHitData(move).typeMod = typeMod;
|
|
if (typeMod > 0) {
|
|
if (!suppressMessages) this.add('-supereffective', target);
|
|
|
|
for (let i = 0; i < typeMod; i++) {
|
|
baseDamage *= 2;
|
|
}
|
|
}
|
|
if (typeMod < 0) {
|
|
if (!suppressMessages) this.add('-resisted', target);
|
|
|
|
for (let i = 0; i > typeMod; i--) {
|
|
baseDamage = tr(baseDamage / 2);
|
|
}
|
|
}
|
|
|
|
if (isCrit && !suppressMessages) this.add('-crit', target);
|
|
|
|
if (
|
|
pokemon.status === 'brn' && move.category === 'Physical' && !pokemon.hasAbility('guts') &&
|
|
!pokemon.hasAbility('superguarda') && !pokemon.hasAbility('radioactive')
|
|
) {
|
|
if (this.gen < 6 || move.id !== 'facade') {
|
|
baseDamage = this.modify(baseDamage, 0.5);
|
|
}
|
|
}
|
|
|
|
// Generation 5, but nothing later, sets damage to 1 before the final damage modifiers
|
|
if (this.gen === 5 && !baseDamage) baseDamage = 1;
|
|
|
|
// Final modifier. Modifiers that modify damage after min damage check, such as Life Orb.
|
|
baseDamage = this.runEvent('ModifyDamage', pokemon, target, move, baseDamage);
|
|
|
|
if ((move.isZOrMaxPowered || move.isMax) && target.getMoveHitData(move).zBrokeProtect) {
|
|
baseDamage = this.modify(baseDamage, 0.25);
|
|
this.add('-zbroken', target);
|
|
}
|
|
|
|
// Generation 6-7 moves the check for minimum 1 damage after the final modifier...
|
|
if (this.gen !== 5 && !baseDamage) return 1;
|
|
|
|
// ...but 16-bit truncation happens even later, and can truncate to 0
|
|
return tr(baseDamage, 16);
|
|
},
|
|
pokemon: {
|
|
ignoringAbility() {
|
|
const abilities = [
|
|
'battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange',
|
|
];
|
|
// Neutralizing Spores modded into ignoringAbility
|
|
let sporeEffect = false;
|
|
for (const foeActive of this.side.foe.active) {
|
|
// foeActive can be null when a pokemon isn't active
|
|
if (foeActive?.ability.includes('neutralizingspores') && !foeActive?.volatiles['gastroacid']) sporeEffect = true;
|
|
}
|
|
for (const allyActive of this.side.active) {
|
|
// allyActive can be null when a pokemon isn't active
|
|
if (allyActive?.ability.includes('neutralizingspores') && !allyActive?.volatiles['gastroacid']) sporeEffect = true;
|
|
}
|
|
return !!((this.battle.gen >= 5 && !this.isActive) ||
|
|
(this.volatiles['gastroacid'] && !abilities.includes(this.ability)) ||
|
|
(sporeEffect && !this.ability.includes('neutralizingspores')));
|
|
},
|
|
getActionSpeed() {
|
|
let speed = this.getStat('spe', false, false);
|
|
if ((this.battle.field.getPseudoWeather('trickroom') || this.battle.field.getPseudoWeather('alienwave')) &&
|
|
!(this.battle.field.getPseudoWeather('trickroom') && this.battle.field.getPseudoWeather('alienwave'))) {
|
|
speed = 0x2710 - speed;
|
|
}
|
|
if (this.battle.field.getPseudoWeather('distortionworld')) {
|
|
speed = 0; // Anything times 0 is still 0
|
|
}
|
|
return this.battle.trunc(speed, 13);
|
|
},
|
|
isGrounded(negateImmunity = false) {
|
|
if ('gravity' in this.battle.field.pseudoWeather) return true;
|
|
if ('ingrain' in this.volatiles && this.battle.gen >= 4) return true;
|
|
if ('smackdown' in this.volatiles) return true;
|
|
const item = (this.ignoringItem() ? '' : this.item);
|
|
if (item === 'ironball') return true;
|
|
// If a Fire/Flying type uses Burn Up and Roost, it becomes ???/Flying-type, but it's still grounded.
|
|
if (!negateImmunity && this.hasType('Flying') && !('roost' in this.volatiles)) return false;
|
|
if (this.hasAbility('levitate') && !this.battle.suppressingAttackEvents()) return null;
|
|
// Innate levitate
|
|
if ((('tony' in this.volatiles) && !this.illusion) && !this.battle.suppressingAttackEvents()) return null;
|
|
if ('magnetrise' in this.volatiles) return false;
|
|
if ('telekinesis' in this.volatiles) return false;
|
|
return item !== 'airballoon';
|
|
},
|
|
setStatus(status, source = null, sourceEffect = null, ignoreImmunities = false) {
|
|
if (!this.hp) return false;
|
|
status = this.battle.dex.getEffect(status);
|
|
if (this.battle.event) {
|
|
if (!source) source = this.battle.event.source;
|
|
if (!sourceEffect) sourceEffect = this.battle.effect;
|
|
}
|
|
if (!source) source = this;
|
|
|
|
if (this.status === status.id) {
|
|
if ((sourceEffect as Move)?.status === this.status) {
|
|
this.battle.add('-fail', this, this.status);
|
|
} else if ((sourceEffect as Move)?.status) {
|
|
this.battle.add('-fail', source);
|
|
this.battle.attrLastMove('[still]');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!ignoreImmunities && status.id &&
|
|
!(source?.hasAbility('corrosion') && ['tox', 'psn'].includes(status.id)) &&
|
|
!(sourceEffect?.id === 'corrosivetoxic')) {
|
|
// the game currently never ignores immunities
|
|
if (!this.runStatusImmunity(status.id === 'tox' ? 'psn' : status.id)) {
|
|
this.battle.debug('immune to status');
|
|
if ((sourceEffect as Move)?.status) {
|
|
this.battle.add('-immune', this);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
const prevStatus = this.status;
|
|
const prevStatusData = this.statusData;
|
|
if (status.id) {
|
|
const result: boolean = this.battle.runEvent('SetStatus', this, source, sourceEffect, status);
|
|
if (!result) {
|
|
this.battle.debug('set status [' + status.id + '] interrupted');
|
|
return result;
|
|
}
|
|
}
|
|
|
|
this.status = status.id;
|
|
this.statusData = {id: status.id, target: this};
|
|
if (source) this.statusData.source = source;
|
|
if (status.duration) this.statusData.duration = status.duration;
|
|
if (status.durationCallback) {
|
|
this.statusData.duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
|
|
if (status.id && !this.battle.singleEvent('Start', status, this.statusData, this, source, sourceEffect)) {
|
|
this.battle.debug('status start [' + status.id + '] interrupted');
|
|
// cancel the setstatus
|
|
this.status = prevStatus;
|
|
this.statusData = prevStatusData;
|
|
return false;
|
|
}
|
|
if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
};
|