pokemon-showdown/data/statuses.js
Guangcong Luo 20e7e604eb Unify more switch-in code
This removes the redundant 'SwitchOut' code, and continues to ensure
that switching and dragging share as much code as possible, which
should help avoid bugs where they previously were different for no
reason.
2020-04-03 19:08:54 -07:00

820 lines
22 KiB
JavaScript

'use strict';
/**@type {{[k: string]: PureEffectData}} */
let BattleStatuses = {
brn: {
name: 'brn',
id: 'brn',
num: 0,
effectType: 'Status',
onStart(target, source, sourceEffect) {
if (sourceEffect && sourceEffect.id === 'flameorb') {
this.add('-status', target, 'brn', '[from] item: Flame Orb');
} else if (sourceEffect && sourceEffect.effectType === 'Ability') {
this.add('-status', target, 'brn', '[from] ability: ' + sourceEffect.name, '[of] ' + source);
} else {
this.add('-status', target, 'brn');
}
},
// Damage reduction is handled directly in the sim/battle.js damage function
onResidualOrder: 9,
onResidual(pokemon) {
this.damage(pokemon.baseMaxhp / 16);
},
},
par: {
name: 'par',
id: 'par',
num: 0,
effectType: 'Status',
onStart(target, source, sourceEffect) {
if (sourceEffect && sourceEffect.effectType === 'Ability') {
this.add('-status', target, 'par', '[from] ability: ' + sourceEffect.name, '[of] ' + source);
} else {
this.add('-status', target, 'par');
}
},
onModifySpe(spe, pokemon) {
if (!pokemon.hasAbility('quickfeet')) {
return this.chainModify(0.5);
}
},
onBeforeMovePriority: 1,
onBeforeMove(pokemon) {
if (this.randomChance(1, 4)) {
this.add('cant', pokemon, 'par');
return false;
}
},
},
slp: {
name: 'slp',
id: 'slp',
num: 0,
effectType: 'Status',
onStart(target, source, sourceEffect) {
if (sourceEffect && sourceEffect.effectType === 'Ability') {
this.add('-status', target, 'slp', '[from] ability: ' + sourceEffect.name, '[of] ' + source);
} else if (sourceEffect && sourceEffect.effectType === 'Move') {
this.add('-status', target, 'slp', '[from] move: ' + sourceEffect.name);
} else {
this.add('-status', target, 'slp');
}
// 1-3 turns
this.effectData.startTime = this.random(2, 5);
this.effectData.time = this.effectData.startTime;
},
onBeforeMovePriority: 10,
onBeforeMove(pokemon, target, move) {
if (pokemon.hasAbility('earlybird')) {
pokemon.statusData.time--;
}
pokemon.statusData.time--;
if (pokemon.statusData.time <= 0) {
pokemon.cureStatus();
return;
}
this.add('cant', pokemon, 'slp');
if (move.sleepUsable) {
return;
}
return false;
},
},
frz: {
name: 'frz',
id: 'frz',
num: 0,
effectType: 'Status',
onStart(target, source, sourceEffect) {
if (sourceEffect && sourceEffect.effectType === 'Ability') {
this.add('-status', target, 'frz', '[from] ability: ' + sourceEffect.name, '[of] ' + source);
} else {
this.add('-status', target, 'frz');
}
if (target.species.name === 'Shaymin-Sky' && target.baseSpecies.baseSpecies === 'Shaymin') {
target.formeChange('Shaymin', this.effect, true);
}
},
onBeforeMovePriority: 10,
onBeforeMove(pokemon, target, move) {
if (move.flags['defrost']) return;
if (this.randomChance(1, 5)) {
pokemon.cureStatus();
return;
}
this.add('cant', pokemon, 'frz');
return false;
},
onModifyMove(move, pokemon) {
if (move.flags['defrost']) {
this.add('-curestatus', pokemon, 'frz', '[from] move: ' + move);
pokemon.setStatus('');
}
},
onHit(target, source, move) {
if (move.thawsTarget || move.type === 'Fire' && move.category !== 'Status') {
target.cureStatus();
}
},
},
psn: {
name: 'psn',
id: 'psn',
num: 0,
effectType: 'Status',
onStart(target, source, sourceEffect) {
if (sourceEffect && sourceEffect.effectType === 'Ability') {
this.add('-status', target, 'psn', '[from] ability: ' + sourceEffect.name, '[of] ' + source);
} else {
this.add('-status', target, 'psn');
}
},
onResidualOrder: 9,
onResidual(pokemon) {
this.damage(pokemon.baseMaxhp / 8);
},
},
tox: {
name: 'tox',
id: 'tox',
num: 0,
effectType: 'Status',
onStart(target, source, sourceEffect) {
this.effectData.stage = 0;
if (sourceEffect && sourceEffect.id === 'toxicorb') {
this.add('-status', target, 'tox', '[from] item: Toxic Orb');
} else if (sourceEffect && sourceEffect.effectType === 'Ability') {
this.add('-status', target, 'tox', '[from] ability: ' + sourceEffect.name, '[of] ' + source);
} else {
this.add('-status', target, 'tox');
}
},
onSwitchIn() {
this.effectData.stage = 0;
},
onResidualOrder: 9,
onResidual(pokemon) {
if (this.effectData.stage < 15) {
this.effectData.stage++;
}
this.damage(this.dex.clampIntRange(pokemon.baseMaxhp / 16, 1) * this.effectData.stage);
},
},
confusion: {
name: 'confusion',
id: 'confusion',
num: 0,
// this is a volatile status
onStart(target, source, sourceEffect) {
if (sourceEffect && sourceEffect.id === 'lockedmove') {
this.add('-start', target, 'confusion', '[fatigue]');
} else {
this.add('-start', target, 'confusion');
}
this.effectData.time = this.random(2, 6);
},
onEnd(target) {
this.add('-end', target, 'confusion');
},
onBeforeMovePriority: 3,
onBeforeMove(pokemon) {
pokemon.volatiles.confusion.time--;
if (!pokemon.volatiles.confusion.time) {
pokemon.removeVolatile('confusion');
return;
}
this.add('-activate', pokemon, 'confusion');
if (!this.randomChance(1, 3)) {
return;
}
this.activeTarget = pokemon;
let damage = this.getDamage(pokemon, pokemon, 40);
if (typeof damage !== 'number') throw new Error("Confusion damage not dealt");
this.damage(damage, pokemon, pokemon, /** @type {ActiveMove} */ ({
id: 'confused',
effectType: 'Move',
type: '???',
}));
return false;
},
},
flinch: {
name: 'flinch',
id: 'flinch',
num: 0,
duration: 1,
onBeforeMovePriority: 8,
onBeforeMove(pokemon) {
this.add('cant', pokemon, 'flinch');
this.runEvent('Flinch', pokemon);
return false;
},
},
trapped: {
name: 'trapped',
id: 'trapped',
num: 0,
noCopy: true,
onTrapPokemon(pokemon) {
pokemon.tryTrap();
},
onStart(target) {
this.add('-activate', target, 'trapped');
},
},
trapper: {
name: 'trapper',
id: 'trapper',
num: 0,
noCopy: true,
},
partiallytrapped: {
name: 'partiallytrapped',
id: 'partiallytrapped',
num: 0,
duration: 5,
durationCallback(target, source) {
if (source && source.hasItem('gripclaw')) return 8;
return this.random(5, 7);
},
onStart(pokemon, source) {
this.add('-activate', pokemon, 'move: ' + this.effectData.sourceEffect, '[of] ' + source);
},
onResidualOrder: 11,
onResidual(pokemon) {
const source = this.effectData.source;
if (source && (!source.isActive || source.hp <= 0 || !source.activeTurns)) {
// G-Max Centiferno and G-Max Sandblast continue even after the user leaves the field
if (['gmaxcentiferno', 'gmaxsandblast'].includes(this.effectData.sourceEffect.id)) return;
delete pokemon.volatiles['partiallytrapped'];
this.add('-end', pokemon, this.effectData.sourceEffect, '[partiallytrapped]', '[silent]');
return;
}
if (source.hasItem('bindingband')) {
this.damage(pokemon.baseMaxhp / 6);
} else {
this.damage(pokemon.baseMaxhp / 8);
}
},
onEnd(pokemon) {
this.add('-end', pokemon, this.effectData.sourceEffect, '[partiallytrapped]');
},
onTrapPokemon(pokemon) {
if (this.effectData.source && this.effectData.source.isActive) pokemon.tryTrap();
},
},
lockedmove: {
// Outrage, Thrash, Petal Dance...
name: 'lockedmove',
id: 'lockedmove',
num: 0,
duration: 2,
onResidual(target) {
if (target.status === 'slp') {
// don't lock, and bypass confusion for calming
delete target.volatiles['lockedmove'];
}
this.effectData.trueDuration--;
},
onStart(target, source, effect) {
this.effectData.trueDuration = this.random(2, 4);
this.effectData.move = effect.id;
},
onRestart() {
if (this.effectData.trueDuration >= 2) {
this.effectData.duration = 2;
}
},
onEnd(target) {
if (this.effectData.trueDuration > 1) return;
target.addVolatile('confusion');
},
onLockMove(pokemon) {
if (pokemon.volatiles['dynamax']) return;
return this.effectData.move;
},
},
twoturnmove: {
// Skull Bash, SolarBeam, Sky Drop...
name: 'twoturnmove',
id: 'twoturnmove',
num: 0,
duration: 2,
onStart(target, source, effect) {
this.effectData.move = effect.id;
target.addVolatile(effect.id, source);
this.attrLastMove('[still]');
},
onEnd(target) {
target.removeVolatile(this.effectData.move);
},
onLockMove() {
return this.effectData.move;
},
onMoveAborted(pokemon) {
pokemon.removeVolatile('twoturnmove');
},
},
choicelock: {
name: 'choicelock',
id: 'choicelock',
num: 0,
noCopy: true,
onStart(pokemon) {
if (!this.activeMove) throw new Error("Battle.activeMove is null");
if (!this.activeMove.id || this.activeMove.hasBounced) return false;
this.effectData.move = this.activeMove.id;
},
onBeforeMove(pokemon, target, move) {
if (!pokemon.getItem().isChoice) {
pokemon.removeVolatile('choicelock');
return;
}
if (!pokemon.ignoringItem() && !pokemon.volatiles['dynamax'] && move.id !== this.effectData.move && move.id !== 'struggle') {
// Fails unless the Choice item is being ignored, and no PP is lost
this.addMove('move', pokemon, move.name);
this.attrLastMove('[still]');
this.debug("Disabled by Choice item lock");
this.add('-fail', pokemon);
return false;
}
},
onDisableMove(pokemon) {
if (!pokemon.getItem().isChoice || !pokemon.hasMove(this.effectData.move)) {
pokemon.removeVolatile('choicelock');
return;
}
if (pokemon.ignoringItem() || pokemon.volatiles['dynamax']) {
return;
}
for (const moveSlot of pokemon.moveSlots) {
if (moveSlot.id !== this.effectData.move) {
pokemon.disableMove(moveSlot.id, false, this.effectData.sourceEffect);
}
}
},
},
mustrecharge: {
name: 'mustrecharge',
id: 'mustrecharge',
num: 0,
duration: 2,
onBeforeMovePriority: 11,
onBeforeMove(pokemon) {
this.add('cant', pokemon, 'recharge');
pokemon.removeVolatile('mustrecharge');
pokemon.removeVolatile('truant');
return null;
},
onStart(pokemon) {
this.add('-mustrecharge', pokemon);
},
onLockMove: 'recharge',
},
futuremove: {
// this is a slot condition
name: 'futuremove',
id: 'futuremove',
num: 0,
duration: 3,
onResidualOrder: 3,
onEnd(target) {
const data = this.effectData;
// time's up; time to hit! :D
const move = this.dex.getMove(data.move);
if (target.fainted || target === data.source) {
this.hint(`${move.name} did not hit because the target is ${(data.fainted ? 'fainted' : 'the user')}.`);
return;
}
this.add('-end', target, 'move: ' + move.name);
target.removeVolatile('Protect');
target.removeVolatile('Endure');
if (data.source.hasAbility('infiltrator') && this.gen >= 6) {
data.moveData.infiltrates = true;
}
if (data.source.hasAbility('normalize') && this.gen >= 6) {
data.moveData.type = 'Normal';
}
if (data.source.hasAbility('adaptability') && this.gen >= 6) {
data.moveData.stab = 2;
}
const hitMove = new this.dex.Data.Move(data.moveData);
this.trySpreadMoveHit([target], data.source, /** @type {ActiveMove} */(/** @type {unknown} */(hitMove)));
},
},
healreplacement: {
// this is a slot condition
name: 'healreplacement',
id: 'healreplacement',
num: 0,
onStart(side, source, sourceEffect) {
this.effectData.sourceEffect = sourceEffect;
this.add('-activate', source, 'healreplacement');
},
onSwitchInPriority: 1,
onSwitchIn(target) {
if (!target.fainted) {
target.heal(target.maxhp);
this.add('-heal', target, target.getHealth, '[from] move: ' + this.effectData.sourceEffect, '[zeffect]');
target.side.removeSlotCondition(target, 'healreplacement');
}
},
},
stall: {
// Protect, Detect, Endure counter
name: 'stall',
id: 'stall',
num: 0,
duration: 2,
counterMax: 729,
onStart() {
this.effectData.counter = 3;
},
onStallMove(pokemon) {
// this.effectData.counter should never be undefined here.
// However, just in case, use 1 if it is undefined.
let counter = this.effectData.counter || 1;
this.debug("Success chance: " + Math.round(100 / counter) + "%");
let success = this.randomChance(1, counter);
if (!success) delete pokemon.volatiles['stall'];
return success;
},
onRestart() {
// @ts-ignore
if (this.effectData.counter < this.effect.counterMax) {
this.effectData.counter *= 3;
}
this.effectData.duration = 2;
},
},
gem: {
name: 'gem',
id: 'gem',
num: 0,
duration: 1,
affectsFainted: true,
onBasePower(basePower, user, target, move) {
this.debug('Gem Boost');
return this.chainModify([0x14CD, 0x1000]);
},
},
// weather is implemented here since it's so important to the game
raindance: {
name: 'RainDance',
id: 'raindance',
num: 0,
effectType: 'Weather',
duration: 5,
durationCallback(source, effect) {
if (source && source.hasItem('damprock')) {
return 8;
}
return 5;
},
onWeatherModifyDamage(damage, attacker, defender, move) {
if (defender.hasItem('utilityumbrella')) return;
if (move.type === 'Water') {
this.debug('rain water boost');
return this.chainModify(1.5);
}
if (move.type === 'Fire') {
this.debug('rain fire suppress');
return this.chainModify(0.5);
}
},
onStart(battle, source, effect) {
if (effect && effect.effectType === 'Ability') {
if (this.gen <= 5) this.effectData.duration = 0;
this.add('-weather', 'RainDance', '[from] ability: ' + effect, '[of] ' + source);
} else {
this.add('-weather', 'RainDance');
}
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'RainDance', '[upkeep]');
this.eachEvent('Weather');
},
onEnd() {
this.add('-weather', 'none');
},
},
primordialsea: {
name: 'PrimordialSea',
id: 'primordialsea',
num: 0,
effectType: 'Weather',
duration: 0,
onTryMovePriority: 1,
onTryMove(attacker, defender, move) {
if (move.type === 'Fire' && move.category !== 'Status') {
this.debug('Primordial Sea fire suppress');
this.add('-fail', attacker, move, '[from] Primordial Sea');
this.attrLastMove('[still]');
return null;
}
},
onWeatherModifyDamage(damage, attacker, defender, move) {
if (defender.hasItem('utilityumbrella')) return;
if (move.type === 'Water') {
this.debug('Rain water boost');
return this.chainModify(1.5);
}
},
onStart(battle, source, effect) {
this.add('-weather', 'PrimordialSea', '[from] ability: ' + effect, '[of] ' + source);
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'PrimordialSea', '[upkeep]');
this.eachEvent('Weather');
},
onEnd() {
this.add('-weather', 'none');
},
},
sunnyday: {
name: 'SunnyDay',
id: 'sunnyday',
num: 0,
effectType: 'Weather',
duration: 5,
durationCallback(source, effect) {
if (source && source.hasItem('heatrock')) {
return 8;
}
return 5;
},
onWeatherModifyDamage(damage, attacker, defender, move) {
if (defender.hasItem('utilityumbrella')) return;
if (move.type === 'Fire') {
this.debug('Sunny Day fire boost');
return this.chainModify(1.5);
}
if (move.type === 'Water') {
this.debug('Sunny Day water suppress');
return this.chainModify(0.5);
}
},
onStart(battle, source, effect) {
if (effect && effect.effectType === 'Ability') {
if (this.gen <= 5) this.effectData.duration = 0;
this.add('-weather', 'SunnyDay', '[from] ability: ' + effect, '[of] ' + source);
} else {
this.add('-weather', 'SunnyDay');
}
},
onImmunity(type, pokemon) {
if (pokemon.hasItem('utilityumbrella')) return;
if (type === 'frz') return false;
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'SunnyDay', '[upkeep]');
this.eachEvent('Weather');
},
onEnd() {
this.add('-weather', 'none');
},
},
desolateland: {
name: 'DesolateLand',
id: 'desolateland',
num: 0,
effectType: 'Weather',
duration: 0,
onTryMovePriority: 1,
onTryMove(attacker, defender, move) {
if (move.type === 'Water' && move.category !== 'Status') {
this.debug('Desolate Land water suppress');
this.add('-fail', attacker, move, '[from] Desolate Land');
this.attrLastMove('[still]');
return null;
}
},
onWeatherModifyDamage(damage, attacker, defender, move) {
if (defender.hasItem('utilityumbrella')) return;
if (move.type === 'Fire') {
this.debug('Sunny Day fire boost');
return this.chainModify(1.5);
}
},
onStart(battle, source, effect) {
this.add('-weather', 'DesolateLand', '[from] ability: ' + effect, '[of] ' + source);
},
onImmunity(type, pokemon) {
if (pokemon.hasItem('utilityumbrella')) return;
if (type === 'frz') return false;
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'DesolateLand', '[upkeep]');
this.eachEvent('Weather');
},
onEnd() {
this.add('-weather', 'none');
},
},
sandstorm: {
name: 'Sandstorm',
id: 'sandstorm',
num: 0,
effectType: 'Weather',
duration: 5,
durationCallback(source, effect) {
if (source && source.hasItem('smoothrock')) {
return 8;
}
return 5;
},
// This should be applied directly to the stat before any of the other modifiers are chained
// So we give it increased priority.
onModifySpDPriority: 10,
onModifySpD(spd, pokemon) {
if (pokemon.hasType('Rock') && this.field.isWeather('sandstorm')) {
return this.modify(spd, 1.5);
}
},
onStart(battle, source, effect) {
if (effect && effect.effectType === 'Ability') {
if (this.gen <= 5) this.effectData.duration = 0;
this.add('-weather', 'Sandstorm', '[from] ability: ' + effect, '[of] ' + source);
} else {
this.add('-weather', 'Sandstorm');
}
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'Sandstorm', '[upkeep]');
if (this.field.isWeather('sandstorm')) this.eachEvent('Weather');
},
onWeather(target) {
this.damage(target.baseMaxhp / 16);
},
onEnd() {
this.add('-weather', 'none');
},
},
hail: {
name: 'Hail',
id: 'hail',
num: 0,
effectType: 'Weather',
duration: 5,
durationCallback(source, effect) {
if (source && source.hasItem('icyrock')) {
return 8;
}
return 5;
},
onStart(battle, source, effect) {
if (effect && effect.effectType === 'Ability') {
if (this.gen <= 5) this.effectData.duration = 0;
this.add('-weather', 'Hail', '[from] ability: ' + effect, '[of] ' + source);
} else {
this.add('-weather', 'Hail');
}
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'Hail', '[upkeep]');
if (this.field.isWeather('hail')) this.eachEvent('Weather');
},
onWeather(target) {
this.damage(target.baseMaxhp / 16);
},
onEnd() {
this.add('-weather', 'none');
},
},
deltastream: {
name: 'DeltaStream',
id: 'deltastream',
num: 0,
effectType: 'Weather',
duration: 0,
onEffectiveness(typeMod, target, type, move) {
if (move && move.effectType === 'Move' && move.category !== 'Status' && type === 'Flying' && typeMod > 0) {
this.add('-activate', '', 'deltastream');
return 0;
}
},
onStart(battle, source, effect) {
this.add('-weather', 'DeltaStream', '[from] ability: ' + effect, '[of] ' + source);
},
onResidualOrder: 1,
onResidual() {
this.add('-weather', 'DeltaStream', '[upkeep]');
this.eachEvent('Weather');
},
onEnd() {
this.add('-weather', 'none');
},
},
dynamax: {
name: 'Dynamax',
id: 'dynamax',
num: 0,
noCopy: true,
duration: 3,
onStart(pokemon) {
pokemon.removeVolatile('substitute');
if (pokemon.illusion) this.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon);
if (pokemon.volatiles['torment']) {
delete pokemon.volatiles['torment'];
this.add('-end', pokemon, 'Torment', '[silent]');
}
if (['cramorantgulping', 'cramorantgorging'].includes(pokemon.species.id) && !pokemon.transformed) {
pokemon.formeChange('cramorant');
}
this.add('-start', pokemon, 'Dynamax');
if (pokemon.canGigantamax) this.add('-formechange', pokemon, pokemon.canGigantamax);
if (pokemon.forme === 'Shedinja') return;
// Changes based on dynamax level, 2 is max (at LVL 10)
const ratio = 2;
pokemon.maxhp = Math.floor(pokemon.maxhp * ratio);
pokemon.hp = Math.ceil(pokemon.hp * ratio);
this.add('-heal', pokemon, pokemon.getHealth, '[silent]');
},
onTryAddVolatile(status, pokemon) {
if (status.id === 'flinch') return null;
},
onBeforeSwitchOutPriority: -1,
onBeforeSwitchOut(pokemon) {
pokemon.removeVolatile('dynamax');
},
onSourceModifyDamage(damage, source, target, move) {
if (move.id === 'behemothbash' || move.id === 'behemothblade' || move.id === 'dynamaxcannon') {
return this.chainModify(2);
}
},
onDragOutPriority: 2,
onDragOut(pokemon) {
this.add('-block', pokemon, 'Dynamax');
return null;
},
onEnd(pokemon) {
this.add('-end', pokemon, 'Dynamax');
if (pokemon.canGigantamax) this.add('-formechange', pokemon, pokemon.species.name);
if (pokemon.forme === 'Shedinja') return;
pokemon.hp = pokemon.getUndynamaxedHP();
pokemon.maxhp = pokemon.baseMaxhp;
this.add('-heal', pokemon, pokemon.getHealth, '[silent]');
},
},
// Arceus and Silvally's actual typing is implemented here.
// Their true typing for all their formes is Normal, and it's only
// Multitype and RKS System, respectively, that changes their type,
// but their formes are specified to be their corresponding type
// in the Pokedex, so that needs to be overridden.
// This is mainly relevant for Hackmons Cup and Balanced Hackmons.
arceus: {
name: 'Arceus',
id: 'arceus',
num: 493,
onTypePriority: 1,
onType(types, pokemon) {
if (pokemon.transformed || pokemon.ability !== 'multitype' && this.gen >= 8) return types;
/** @type {string | undefined} */
let type = 'Normal';
if (pokemon.ability === 'multitype') {
type = pokemon.getItem().onPlate;
if (!type) {
type = 'Normal';
}
}
return [type];
},
},
silvally: {
name: 'Silvally',
id: 'silvally',
num: 773,
onTypePriority: 1,
onType(types, pokemon) {
if (pokemon.transformed || pokemon.ability !== 'rkssystem' && this.gen >= 8) return types;
/** @type {string | undefined} */
let type = 'Normal';
if (pokemon.ability === 'rkssystem') {
type = pokemon.getItem().onMemory;
if (!type) {
type = 'Normal';
}
}
return [type];
},
},
};
exports.BattleStatuses = BattleStatuses;