pokemon-showdown/data/scripts.js
Guangcong Luo f3dbfbe685 Refactor Decision -> Action
"Decision" and "Choice" were always kind of unclear, so Decision is now
Action. It should now be a lot clearer.

Actions are also now strongly typed.
2017-12-02 11:34:55 -06:00

945 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const CHOOSABLE_TARGETS = new Set(['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe']);
exports.BattleScripts = {
gen: 7,
/**
* runMove is the "outside" move caller. It handles deducting PP,
* flinching, full paralysis, etc. All the stuff up to and including
* the "POKEMON used MOVE" message.
*
* For details of the difference between runMove and useMove, see
* useMove's info.
*
* externalMove skips LockMove and PP deduction, mostly for use by
* Dancer.
*/
runMove: function (move, pokemon, targetLoc, sourceEffect, zMove, externalMove) {
let target = this.getTarget(pokemon, zMove || move, targetLoc);
if (!sourceEffect && toId(move) !== 'struggle' && !zMove) {
let changedMove = this.runEvent('OverrideAction', pokemon, target, move);
if (changedMove && changedMove !== true) {
move = changedMove;
target = null;
}
}
let baseMove = this.getMove(move);
move = zMove ? this.getZMoveCopy(move, pokemon) : baseMove;
if (!target && target !== false) target = this.resolveTarget(pokemon, move);
// copy the priority for Quick Guard
if (zMove) move.priority = baseMove.priority;
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.cancelMove INSTEAD
this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn);
this.clearActiveMove(true);
return;
} */
if (!this.runEvent('BeforeMove', pokemon, target, move)) {
this.runEvent('MoveAborted', pokemon, target, move);
this.clearActiveMove(true);
return;
}
if (move.beforeMoveCallback) {
if (move.beforeMoveCallback.call(this, pokemon, target, move)) {
this.clearActiveMove(true);
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);
let gameConsole = [null, 'Game Boy', 'Game Boy', 'Game Boy Advance', 'DS', 'DS'][this.gen] || '3DS';
this.add('-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);
return;
}
} else {
sourceEffect = this.getEffect('lockedmove');
}
pokemon.moveUsed(move, targetLoc);
}
// Dancer Petal Dance hack
// TODO: implement properly
let noLock = externalMove && !pokemon.volatiles.lockedmove;
if (zMove) {
if (pokemon.illusion) {
this.singleEvent('End', this.getAbility('Illusion'), pokemon.abilityData, pokemon);
}
this.add('-zpower', pokemon);
pokemon.side.zMoveUsed = true;
}
let moveDidSomething = this.useMove(baseMove, pokemon, target, sourceEffect, zMove);
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) {
let dancers = [];
for (const side of this.sides) {
for (const currentPoke of side.active) {
if (!currentPoke || !currentPoke.hp || pokemon === currentPoke) continue;
if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) {
dancers.push(currentPoke);
}
}
}
// Dancer activates in order of lowest speed stat to highest
// Ties go to whichever Pokemon has had the ability for the least amount of time
dancers.sort(function (a, b) { return -(b.stats['spe'] - a.stats['spe']) || b.abilityOrder - a.abilityOrder; });
for (const dancer of dancers) {
this.faintMessages();
this.add('-activate', dancer, 'ability: Dancer');
this.runMove(baseMove.id, dancer, 0, this.getAbility('dancer'), undefined, true);
}
}
if (noLock && pokemon.volatiles.lockedmove) delete pokemon.volatiles.lockedmove;
},
/**
* useMove is the "inside" move caller. It handles effects of the
* move itself, but not the idea of using the move.
*
* Most caller effects, like Sleep Talk, Nature Power, Magic Bounce,
* etc use useMove.
*
* The only ones that use runMove are Instruct, Pursuit, and
* Dancer.
*/
useMove: function (move, pokemon, target, sourceEffect, zMove) {
if (!sourceEffect && this.effect.id) sourceEffect = this.effect;
if (zMove && move.id === 'weatherball') {
let baseMove = move;
this.singleEvent('ModifyMove', move, null, pokemon, target, move, move);
move = this.getZMoveCopy(move, pokemon);
if (move.type !== 'Normal') sourceEffect = baseMove;
} else if (zMove || (sourceEffect && sourceEffect.isZ && sourceEffect.id !== 'instruct')) {
move = this.getZMoveCopy(move, pokemon);
} else {
move = this.getMoveCopy(move);
}
if (this.activeMove) {
move.priority = this.activeMove.priority;
move.pranksterBoosted = move.hasBounced ? false : this.activeMove.pranksterBoosted;
}
let baseTarget = move.target;
if (!target && target !== false) target = this.resolveTarget(pokemon, move);
if (move.target === 'self' || move.target === 'allies') {
target = pokemon;
}
if (sourceEffect) move.sourceEffect = sourceEffect.id;
let moveResult = false;
this.setActiveMove(move, pokemon, target);
this.singleEvent('ModifyMove', move, null, pokemon, target, move, move);
if (baseTarget !== move.target) {
// Target changed in ModifyMove, so we must adjust it here
// Adjust before the next event so the correct target is passed to the
// event
target = this.resolveTarget(pokemon, move);
}
move = this.runEvent('ModifyMove', pokemon, target, move, move);
if (baseTarget !== move.target) {
// Adjust again
target = this.resolveTarget(pokemon, move);
}
if (!move) return false;
let attrs = '';
if (pokemon.fainted) {
return false;
}
if (move.flags['charge'] && !pokemon.volatiles[move.id]) {
attrs = '|[still]'; // suppress the default move animation
}
let movename = move.name;
if (move.id === 'hiddenpower') movename = 'Hidden Power';
if (sourceEffect) attrs += '|[from]' + this.getEffect(sourceEffect);
if (zMove && move.isZ === true) {
attrs = '|[anim]' + movename + attrs;
movename = 'Z-' + movename;
}
this.addMove('move', pokemon, movename, target + attrs);
if (zMove && move.category !== 'Status') {
this.attrLastMove('[zeffect]');
} else if (zMove && move.zMoveBoost) {
this.boost(move.zMoveBoost, pokemon, pokemon, {id: 'zpower'});
} else if (zMove && move.zMoveEffect === 'heal') {
this.heal(pokemon.maxhp, pokemon, pokemon, {id: 'zpower'});
} else if (zMove && move.zMoveEffect === 'healreplacement') {
move.self = {sideCondition: 'healreplacement'};
} else if (zMove && move.zMoveEffect === 'clearnegativeboost') {
let boosts = {};
for (let i in pokemon.boosts) {
if (pokemon.boosts[i] < 0) {
boosts[i] = 0;
}
}
pokemon.setBoost(boosts);
this.add('-clearnegativeboost', pokemon, '[zeffect]');
} else if (zMove && move.zMoveEffect === 'redirect') {
pokemon.addVolatile('followme', pokemon, {id: 'zpower'});
} else if (zMove && move.zMoveEffect === 'crit2') {
pokemon.addVolatile('focusenergy', pokemon, {id: 'zpower'});
} else if (zMove && move.zMoveEffect === 'curse') {
if (pokemon.hasType('Ghost')) {
this.heal(pokemon.maxhp, pokemon, pokemon, {id: 'zpower'});
} else {
this.boost({atk: 1}, pokemon, pokemon, {id: 'zpower'});
}
}
if (target === false) {
this.attrLastMove('[notarget]');
this.add('-notarget');
if (move.target === 'normal') pokemon.isStaleCon = 0;
return false;
}
let targets = pokemon.getMoveTargets(move, target);
if (!sourceEffect || sourceEffect.id === 'pursuit') {
let extraPP = 0;
for (let i = 0; i < targets.length; i++) {
let ppDrop = this.singleEvent('DeductPP', targets[i].getAbility(), targets[i].abilityData, targets[i], pokemon, move);
if (ppDrop !== true) {
extraPP += ppDrop || 0;
}
}
if (extraPP > 0) {
pokemon.deductPP(move, extraPP);
}
}
if (!this.singleEvent('TryMove', move, null, pokemon, target, move)) {
move.mindBlownRecoil = false;
return false;
}
if (!this.runEvent('TryMove', pokemon, target, move)) {
move.mindBlownRecoil = false;
return false;
}
this.singleEvent('UseMoveMessage', move, null, pokemon, target, move);
if (move.ignoreImmunity === undefined) {
move.ignoreImmunity = (move.category === 'Status');
}
if (move.selfdestruct === 'always') {
this.faint(pokemon, pokemon, move);
}
let damage = false;
if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') {
damage = this.tryMoveHit(target, pokemon, move);
if (damage || damage === 0 || damage === undefined) moveResult = true;
} else if (move.target === 'allAdjacent' || move.target === 'allAdjacentFoes') {
if (!targets.length) {
this.attrLastMove('[notarget]');
this.add('-notarget');
return false;
}
if (targets.length > 1) move.spreadHit = true;
let hitTargets = [];
for (let i = 0; i < targets.length; i++) {
let hitResult = this.tryMoveHit(targets[i], pokemon, move);
if (hitResult || hitResult === 0 || hitResult === undefined) {
moveResult = true;
hitTargets.push(targets[i].toString().substr(0, 3));
}
if (damage !== false) {
damage += hitResult || 0;
} else {
damage = hitResult;
}
}
if (move.spreadHit) this.attrLastMove('[spread] ' + hitTargets.join(','));
} else {
target = targets[0];
let lacksTarget = target.fainted;
if (!lacksTarget) {
if (move.target === 'adjacentFoe' || move.target === 'adjacentAlly' || move.target === 'normal' || move.target === 'randomNormal') {
lacksTarget = !this.isAdjacent(target, pokemon);
}
}
if (lacksTarget && (!move.flags['charge'] || pokemon.volatiles['twoturnmove'])) {
this.attrLastMove('[notarget]');
this.add('-notarget');
if (move.target === 'normal') pokemon.isStaleCon = 0;
return false;
}
damage = this.tryMoveHit(target, pokemon, move);
if (damage || damage === 0 || damage === undefined) moveResult = true;
}
if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true);
if (!pokemon.hp) {
this.faint(pokemon, pokemon, move);
}
if (!moveResult) {
this.singleEvent('MoveFail', move, null, target, pokemon, move);
return false;
}
if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) {
this.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move);
this.runEvent('AfterMoveSecondarySelf', pokemon, target, move);
}
return true;
},
tryMoveHit: function (target, pokemon, move) {
this.setActiveMove(move, pokemon, target);
let hitResult = true;
hitResult = this.singleEvent('PrepareHit', move, {}, target, pokemon, move);
if (!hitResult) {
if (hitResult === false) this.add('-fail', target);
return false;
}
this.runEvent('PrepareHit', pokemon, target, move);
if (!this.singleEvent('Try', move, null, pokemon, target, move)) {
return false;
}
if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') {
if (move.target === 'all') {
hitResult = this.runEvent('TryHitField', target, pokemon, move);
} else {
hitResult = this.runEvent('TryHitSide', target, pokemon, move);
}
if (!hitResult) {
if (hitResult === false) this.add('-fail', target);
return false;
}
return this.moveHit(target, pokemon, move);
}
if (move.ignoreImmunity === undefined) {
move.ignoreImmunity = (move.category === 'Status');
}
if (this.gen < 7 && move.ignoreImmunity !== true && !move.ignoreImmunity[move.type] && !target.runImmunity(move.type, true)) {
return false;
}
hitResult = this.runEvent('TryHit', target, pokemon, move);
if (!hitResult) {
if (hitResult === false) this.add('-fail', target);
return false;
}
if (this.gen >= 7 && move.ignoreImmunity !== true && !move.ignoreImmunity[move.type] && !target.runImmunity(move.type, true)) {
return false;
}
if (move.flags['powder'] && target !== pokemon && !this.getImmunity('powder', target)) {
this.debug('natural powder immunity');
this.add('-immune', target, '[msg]');
return false;
}
if (this.gen >= 7 && move.pranksterBoosted && target.side !== pokemon.side && !this.getImmunity('prankster', target)) {
this.debug('natural prankster immunity');
if (!target.illusion) this.add('-hint', "In gen 7, Dark is immune to Prankster moves.");
this.add('-immune', target, '[msg]');
return false;
}
let boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3];
// calculate true accuracy
let accuracy = move.accuracy;
let boosts, boost;
if (accuracy !== true) {
if (!move.ignoreAccuracy) {
boosts = this.runEvent('ModifyBoost', pokemon, null, null, Object.assign({}, pokemon.boosts));
boost = this.clampIntRange(boosts['accuracy'], -6, 6);
if (boost > 0) {
accuracy *= boostTable[boost];
} else {
accuracy /= boostTable[-boost];
}
}
if (!move.ignoreEvasion) {
boosts = this.runEvent('ModifyBoost', target, null, null, Object.assign({}, target.boosts));
boost = this.clampIntRange(boosts['evasion'], -6, 6);
if (boost > 0) {
accuracy /= boostTable[boost];
} else if (boost < 0) {
accuracy *= boostTable[-boost];
}
}
}
if (move.ohko) { // bypasses accuracy modifiers
if (!target.isSemiInvulnerable()) {
accuracy = 30;
if (move.ohko === 'Ice' && this.gen >= 7 && !pokemon.hasType('Ice')) {
accuracy = 20;
}
if (pokemon.level >= target.level && (move.ohko === true || !target.hasType(move.ohko))) {
accuracy += (pokemon.level - target.level);
} else {
this.add('-immune', target, '[ohko]');
return false;
}
}
} else {
accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy);
}
if (move.alwaysHit || (move.id === 'toxic' && this.gen >= 6 && pokemon.hasType('Poison'))) {
accuracy = true; // bypasses ohko accuracy modifiers
} else {
accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy);
}
if (accuracy !== true && this.random(100) >= accuracy) {
if (!move.spreadHit) this.attrLastMove('[miss]');
this.add('-miss', pokemon, target);
return false;
}
if (move.breaksProtect) {
let broke = false;
for (const effectid of ['banefulbunker', 'kingsshield', 'protect', 'spikyshield']) {
if (target.removeVolatile(effectid)) broke = true;
}
if (this.gen >= 6 || target.side !== pokemon.side) {
for (const effectid of ['craftyshield', 'matblock', 'quickguard', 'wideguard']) {
if (target.side.removeSideCondition(effectid)) broke = true;
}
}
if (broke) {
if (move.id === 'feint') {
this.add('-activate', target, 'move: Feint');
} else {
this.add('-activate', target, 'move: ' + move.name, '[broken]');
}
}
}
if (move.stealsBoosts) {
let boosts = {};
let stolen = false;
for (let statName in target.boosts) {
let stage = target.boosts[statName];
if (stage > 0) {
boosts[statName] = stage;
stolen = true;
}
}
if (stolen) {
this.attrLastMove('[still]');
this.add('-clearpositiveboost', target, pokemon, 'move: ' + move.name);
this.boost(boosts, pokemon);
for (let statName in boosts) {
boosts[statName] = 0;
}
target.setBoost(boosts);
this.add('-anim', pokemon, "Spectral Thief", target);
}
}
move.totalDamage = 0;
let damage = 0;
pokemon.lastDamage = 0;
if (move.multihit) {
let hits = move.multihit;
if (hits.length) {
// yes, it's hardcoded... meh
if (hits[0] === 2 && hits[1] === 5) {
if (this.gen >= 5) {
hits = [2, 2, 3, 3, 4, 5][this.random(6)];
} else {
hits = [2, 2, 2, 3, 3, 3, 4, 5][this.random(8)];
}
} else {
hits = this.random(hits[0], hits[1] + 1);
}
}
hits = Math.floor(hits);
let nullDamage = true;
let moveDamage;
// There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep.
let isSleepUsable = move.sleepUsable || this.getMove(move.sourceEffect).sleepUsable;
let i;
for (i = 0; i < hits && target.hp && pokemon.hp; i++) {
if (pokemon.status === 'slp' && !isSleepUsable) break;
if (move.multiaccuracy && i > 0) {
accuracy = move.accuracy;
if (accuracy !== true) {
if (!move.ignoreAccuracy) {
boosts = this.runEvent('ModifyBoost', pokemon, null, null, Object.assign({}, pokemon.boosts));
boost = this.clampIntRange(boosts['accuracy'], -6, 6);
if (boost > 0) {
accuracy *= boostTable[boost];
} else {
accuracy /= boostTable[-boost];
}
}
if (!move.ignoreEvasion) {
boosts = this.runEvent('ModifyBoost', target, null, null, Object.assign({}, target.boosts));
boost = this.clampIntRange(boosts['evasion'], -6, 6);
if (boost > 0) {
accuracy /= boostTable[boost];
} else if (boost < 0) {
accuracy *= boostTable[-boost];
}
}
}
accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy);
if (!move.alwaysHit) {
accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy);
if (accuracy !== true && this.random(100) >= accuracy) break;
}
}
moveDamage = this.moveHit(target, pokemon, move);
if (moveDamage === false) break;
if (nullDamage && (moveDamage || moveDamage === 0 || moveDamage === undefined)) nullDamage = false;
// Damage from each hit is individually counted for the
// purposes of Counter, Metal Burst, and Mirror Coat.
damage = (moveDamage || 0);
// Total damage dealt is accumulated for the purposes of recoil (Parental Bond).
move.totalDamage += damage;
if (move.mindBlownRecoil && i === 0) {
this.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, null, true);
}
this.eachEvent('Update');
}
if (i === 0) return false;
if (nullDamage) damage = false;
this.add('-hitcount', target, i);
} else {
damage = this.moveHit(target, pokemon, move);
move.totalDamage = damage;
}
if (move.recoil && move.totalDamage) {
this.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, target, 'recoil');
}
if (move.struggleRecoil) {
this.directDamage(this.clampIntRange(Math.round(pokemon.maxhp / 4), 1), pokemon, pokemon, {id: 'strugglerecoil'});
}
if (target && pokemon !== target) target.gotAttacked(move, damage, pokemon);
if (move.ohko) this.add('-ohko');
if (!damage && damage !== 0) return damage;
this.eachEvent('Update');
if (target && !move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) {
this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move);
this.runEvent('AfterMoveSecondary', target, pokemon, move);
}
return damage;
},
moveHit: function (target, pokemon, move, moveData, isSecondary, isSelf) {
let damage;
move = this.getMoveCopy(move);
if (!moveData) moveData = move;
if (!moveData.flags) moveData.flags = {};
let hitResult = true;
// TryHit events:
// STEP 1: we see if the move will succeed at all:
// - TryHit, TryHitSide, or TryHitField are run on the move,
// depending on move target (these events happen in useMove
// or tryMoveHit, not below)
// == primary hit line ==
// Everything after this only happens on the primary hit (not on
// secondary or self-hits)
// STEP 2: we see if anything blocks the move from hitting:
// - TryFieldHit is run on the target
// STEP 3: we see if anything blocks the move from hitting the target:
// - If the move's target is a pokemon, TryHit is run on that pokemon
// Note:
// If the move target is `foeSide`:
// event target = pokemon 0 on the target side
// If the move target is `allySide` or `all`:
// event target = the move user
//
// This is because events can't accept actual sides or fields as
// targets. Choosing these event targets ensures that the correct
// side or field is hit.
//
// It is the `TryHitField` event handler's responsibility to never
// use `target`.
// It is the `TryFieldHit` event handler's responsibility to read
// move.target and react accordingly.
// An exception is `TryHitSide` as a single event (but not as a normal
// event), which is passed the target side.
if (move.target === 'all' && !isSelf) {
hitResult = this.singleEvent('TryHitField', moveData, {}, target, pokemon, move);
} else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) {
hitResult = this.singleEvent('TryHitSide', moveData, {}, target.side, pokemon, move);
} else if (target) {
hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move);
}
if (!hitResult) {
if (hitResult === false) this.add('-fail', target);
return false;
}
if (target && !isSecondary && !isSelf) {
if (move.target !== 'all' && move.target !== 'allySide' && move.target !== 'foeSide') {
hitResult = this.runEvent('TryPrimaryHit', target, pokemon, moveData);
if (hitResult === 0) {
// special Substitute flag
hitResult = true;
target = null;
}
}
}
if (target && isSecondary && !moveData.self) {
hitResult = true;
}
if (!hitResult) {
return false;
}
if (target) {
let didSomething = false;
damage = this.getDamage(pokemon, target, moveData);
// getDamage has several possible return values:
//
// a number:
// means that much damage is dealt (0 damage still counts as dealing
// damage for the purposes of things like Static)
// false:
// gives error message: "But it failed!" and move ends
// null:
// the move ends, with no message (usually, a custom fail message
// was already output by an event handler)
// undefined:
// means no damage is dealt and the move continues
//
// basically, these values have the same meanings as they do for event
// handlers.
if (damage === false || damage === null) {
if (damage === false && !isSecondary && !isSelf) {
this.add('-fail', target);
}
this.debug('damage calculation interrupted');
return false;
}
if (move.selfdestruct === 'ifHit') {
this.faint(pokemon, pokemon, move);
}
if ((damage || damage === 0) && !target.fainted) {
if (move.noFaint && damage >= target.hp) {
damage = target.hp - 1;
}
damage = this.damage(damage, target, pokemon, move);
if (!(damage || damage === 0)) {
this.debug('damage interrupted');
return false;
}
didSomething = true;
}
if (moveData.boosts && !target.fainted) {
hitResult = this.boost(moveData.boosts, target, pokemon, move, isSecondary, isSelf);
didSomething = didSomething || hitResult;
}
if (moveData.heal && !target.fainted) {
let d = target.heal((this.gen < 5 ? Math.floor : Math.round)(target.maxhp * moveData.heal[0] / moveData.heal[1]));
if (!d && d !== 0) {
this.add('-fail', target);
this.debug('heal interrupted');
return false;
}
this.add('-heal', target, target.getHealth);
didSomething = true;
}
if (moveData.status) {
hitResult = target.trySetStatus(moveData.status, pokemon, moveData.ability ? moveData.ability : move);
if (!hitResult && move.status) return hitResult;
didSomething = didSomething || hitResult;
}
if (moveData.forceStatus) {
hitResult = target.setStatus(moveData.forceStatus, pokemon, move);
didSomething = didSomething || hitResult;
}
if (moveData.volatileStatus) {
hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move);
didSomething = didSomething || hitResult;
}
if (moveData.sideCondition) {
hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move);
didSomething = didSomething || hitResult;
}
if (moveData.weather) {
hitResult = this.setWeather(moveData.weather, pokemon, move);
didSomething = didSomething || hitResult;
}
if (moveData.terrain) {
hitResult = this.setTerrain(moveData.terrain, pokemon, move);
didSomething = didSomething || hitResult;
}
if (moveData.pseudoWeather) {
hitResult = this.addPseudoWeather(moveData.pseudoWeather, pokemon, move);
didSomething = didSomething || hitResult;
}
if (moveData.forceSwitch) {
if (this.canSwitch(target.side)) didSomething = true; // at least defer the fail message to later
}
if (moveData.selfSwitch) {
// If the move is Parting Shot and it fails to change the target's stats in gen 7, didSomething will be null instead of false.
// Leaving didSomething as null will cause this function to return before setting the switch flag, preventing the switch.
if (this.canSwitch(pokemon.side) && (didSomething !== null || this.gen < 7)) didSomething = true; // at least defer the fail message to later
}
// Hit events
// These are like the TryHit events, except we don't need a FieldHit event.
// Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;)
hitResult = null;
if (move.target === 'all' && !isSelf) {
if (moveData.onHitField) hitResult = this.singleEvent('HitField', moveData, {}, target, pokemon, move);
} else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) {
if (moveData.onHitSide) hitResult = this.singleEvent('HitSide', moveData, {}, target.side, pokemon, move);
} else {
if (moveData.onHit) hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move);
if (!isSelf && !isSecondary) {
this.runEvent('Hit', target, pokemon, move);
}
if (moveData.onAfterHit) hitResult = this.singleEvent('AfterHit', moveData, {}, target, pokemon, move);
}
if (!hitResult && !didSomething && !moveData.self && !moveData.selfdestruct) {
if (!isSelf && !isSecondary) {
if (hitResult === false || didSomething === false) this.add('-fail', pokemon);
}
this.debug('move failed because it did nothing');
return false;
}
}
if (moveData.self && !move.selfDropped) {
let selfRoll;
if (!isSecondary && moveData.self.boosts) {
selfRoll = this.random(100);
if (!move.multihit) move.selfDropped = true;
}
// This is done solely to mimic in-game RNG behaviour. All self drops have a 100% chance of happening but still grab a random number.
if (typeof moveData.self.chance === 'undefined' || selfRoll < moveData.self.chance) {
this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true);
}
}
if (moveData.secondaries) {
let secondaryRoll;
let secondaries = this.runEvent('ModifySecondaries', target, pokemon, moveData, moveData.secondaries.slice());
for (let i = 0; i < secondaries.length; i++) {
secondaryRoll = this.random(100);
if (typeof secondaries[i].chance === 'undefined' || secondaryRoll < secondaries[i].chance) {
this.moveHit(target, pokemon, move, secondaries[i], true, isSelf);
}
}
}
if (target && target.hp > 0 && pokemon.hp > 0 && moveData.forceSwitch && this.canSwitch(target.side)) {
hitResult = this.runEvent('DragOut', target, pokemon, move);
if (hitResult) {
target.forceSwitchFlag = true;
} else if (hitResult === false && move.category === 'Status') {
this.add('-fail', target);
return false;
}
}
if (move.selfSwitch && pokemon.hp) {
pokemon.switchFlag = move.selfSwitch;
}
return damage;
},
calcRecoilDamage: function (damageDealt, move) {
return this.clampIntRange(Math.round(damageDealt * move.recoil[0] / move.recoil[1]), 1);
},
zMoveTable: {
Poison: "Acid Downpour",
Fighting: "All-Out Pummeling",
Dark: "Black Hole Eclipse",
Grass: "Bloom Doom",
Normal: "Breakneck Blitz",
Rock: "Continental Crush",
Steel: "Corkscrew Crash",
Dragon: "Devastating Drake",
Electric: "Gigavolt Havoc",
Water: "Hydro Vortex",
Fire: "Inferno Overdrive",
Ghost: "Never-Ending Nightmare",
Bug: "Savage Spin-Out",
Psychic: "Shattered Psyche",
Ice: "Subzero Slammer",
Flying: "Supersonic Skystrike",
Ground: "Tectonic Rage",
Fairy: "Twinkle Tackle",
},
getZMove: function (move, pokemon, skipChecks) {
let item = pokemon.getItem();
if (!skipChecks) {
if (pokemon.side.zMoveUsed) return;
if (!item.zMove) return;
if (item.zMoveUser && !item.zMoveUser.includes(pokemon.template.species)) return;
let moveData = pokemon.getMoveData(move);
if (!moveData || !moveData.pp) return; // Draining the PP of the base move prevents the corresponding Z-move from being used.
}
if (item.zMoveFrom) {
if (move.name === item.zMoveFrom) return item.zMove;
} else if (item.zMove === true) {
if (move.type === item.zMoveType) {
if (move.category === "Status") {
return move.name;
} else if (move.zMovePower) {
return this.zMoveTable[move.type];
}
}
}
},
getZMoveCopy: function (move, pokemon) {
move = this.getMove(move);
let zMove;
if (pokemon) {
let item = pokemon.getItem();
if (move.name === item.zMoveFrom) {
return this.getMoveCopy(item.zMove);
}
}
if (move.category === 'Status') {
zMove = this.getMoveCopy(move);
zMove.isZ = true;
return zMove;
}
zMove = this.getMoveCopy(this.zMoveTable[move.type]);
zMove.basePower = move.zMovePower;
zMove.category = move.category;
return zMove;
},
canZMove: function (pokemon) {
if (pokemon.side.zMoveUsed || (pokemon.transformed && (pokemon.template.isMega || pokemon.template.isPrimal))) return;
let item = pokemon.getItem();
if (!item.zMove) return;
if (item.zMoveUser && !item.zMoveUser.includes(pokemon.template.species)) return;
let atLeastOne = false;
let zMoves = [];
for (let i = 0; i < pokemon.moves.length; i++) {
if (pokemon.moveset[i].pp <= 0) {
zMoves.push(null);
continue;
}
let move = this.getMove(pokemon.moves[i]);
let zMoveName = this.getZMove(move, pokemon, true) || '';
if (zMoveName) {
let zMove = this.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) return zMoves;
},
canMegaEvo: function (pokemon) {
let altForme = pokemon.baseTemplate.otherFormes && this.getTemplate(pokemon.baseTemplate.otherFormes[0]);
let item = pokemon.getItem();
if (altForme && altForme.isMega && altForme.requiredMove && pokemon.moves.includes(toId(altForme.requiredMove)) && !item.zMove) return altForme.species;
if (item.megaEvolves !== pokemon.baseTemplate.baseSpecies || item.megaStone === pokemon.species) {
return null;
}
return item.megaStone;
},
canUltraBurst: function (pokemon) {
if (['Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane'].includes(pokemon.baseTemplate.species) &&
pokemon.getItem().id === 'ultranecroziumz') {
return "Necrozma-Ultra";
}
return null;
},
runMegaEvo: function (pokemon) {
const isUltraBurst = !pokemon.canMegaEvo;
const template = this.getTemplate(pokemon.canMegaEvo || pokemon.canUltraBurst);
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(template);
pokemon.baseTemplate = template; // mega evolution is permanent
pokemon.details = template.species + (pokemon.level === 100 ? '' : ', L' + pokemon.level) + (pokemon.gender === '' ? '' : ', ' + pokemon.gender) + (pokemon.set.shiny ? ', shiny' : '');
if (pokemon.illusion) {
pokemon.ability = ''; // Don't allow Illusion to wear off
this.add(isUltraBurst ? '-burst' : '-mega', pokemon, pokemon.illusion.template.baseSpecies, template.requiredItem);
} else {
if (isUltraBurst) {
this.add('-burst', pokemon, template.baseSpecies, template.requiredItem);
} else {
this.add('-mega', pokemon, template.baseSpecies, template.requiredItem);
}
this.add('detailschange', pokemon, pokemon.details);
}
pokemon.setAbility(template.abilities['0']);
pokemon.baseAbility = pokemon.ability;
// Limit one mega evolution
for (const ally of side.pokemon) {
if (isUltraBurst) {
ally.canUltraBurst = null;
} else {
ally.canMegaEvo = null;
}
}
this.runEvent('AfterMega', pokemon);
return true;
},
isAdjacent: function (pokemon1, pokemon2) {
if (pokemon1.fainted || pokemon2.fainted) return false;
if (pokemon1.side === pokemon2.side) return Math.abs(pokemon1.position - pokemon2.position) === 1;
return Math.abs(pokemon1.position + pokemon2.position + 1 - pokemon1.side.active.length) <= 1;
},
targetTypeChoices: function (targetType) {
return CHOOSABLE_TARGETS.has(targetType);
},
};