mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
"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.
945 lines
33 KiB
JavaScript
945 lines
33 KiB
JavaScript
'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);
|
||
},
|
||
};
|